core/homeassistant/components/voip/devices.py

181 lines
6.1 KiB
Python

"""Class to manage devices."""
from __future__ import annotations
from collections.abc import Callable, Iterator
from dataclasses import dataclass, field
from voip_utils import CallInfo, VoipDatagramProtocol
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.helpers import device_registry as dr, entity_registry as er
from .const import DOMAIN
@dataclass
class VoIPDevice:
"""Class to store device."""
voip_id: str
device_id: str
is_active: bool = False
update_listeners: list[Callable[[VoIPDevice], None]] = field(default_factory=list)
protocol: VoipDatagramProtocol | None = None
@callback
def set_is_active(self, active: bool) -> None:
"""Set active state."""
self.is_active = active
for listener in self.update_listeners:
listener(self)
@callback
def async_listen_update(
self, listener: Callable[[VoIPDevice], None]
) -> Callable[[], None]:
"""Listen for updates."""
self.update_listeners.append(listener)
return lambda: self.update_listeners.remove(listener)
@callback
def async_allow_call(self, hass: HomeAssistant) -> bool:
"""Return if call is allowed."""
ent_reg = er.async_get(hass)
allowed_call_entity_id = ent_reg.async_get_entity_id(
"switch", DOMAIN, f"{self.voip_id}-allow_call"
)
# If 2 requests come in fast, the device registry entry has been created
# but entity might not exist yet.
if allowed_call_entity_id is None:
return False
if state := hass.states.get(allowed_call_entity_id):
return state.state == "on"
return False
def get_pipeline_entity_id(self, hass: HomeAssistant) -> str | None:
"""Return entity id for pipeline select."""
ent_reg = er.async_get(hass)
return ent_reg.async_get_entity_id("select", DOMAIN, f"{self.voip_id}-pipeline")
def get_vad_sensitivity_entity_id(self, hass: HomeAssistant) -> str | None:
"""Return entity id for VAD sensitivity."""
ent_reg = er.async_get(hass)
return ent_reg.async_get_entity_id(
"select", DOMAIN, f"{self.voip_id}-vad_sensitivity"
)
class VoIPDevices:
"""Class to store devices."""
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
"""Initialize VoIP devices."""
self.hass = hass
self.config_entry = config_entry
self._new_device_listeners: list[Callable[[VoIPDevice], None]] = []
self.devices: dict[str, VoIPDevice] = {}
@callback
def async_setup(self) -> None:
"""Set up devices."""
for device in dr.async_entries_for_config_entry(
dr.async_get(self.hass), self.config_entry.entry_id
):
voip_id = next(
(item[1] for item in device.identifiers if item[0] == DOMAIN), None
)
if voip_id is None:
continue
self.devices[voip_id] = VoIPDevice(
voip_id=voip_id,
device_id=device.id,
)
@callback
def async_device_removed(ev: Event[dr.EventDeviceRegistryUpdatedData]) -> None:
"""Handle device removed."""
removed_id = ev.data["device_id"]
self.devices = {
voip_id: voip_device
for voip_id, voip_device in self.devices.items()
if voip_device.device_id != removed_id
}
self.config_entry.async_on_unload(
self.hass.bus.async_listen(
dr.EVENT_DEVICE_REGISTRY_UPDATED,
async_device_removed,
callback(lambda event_data: event_data["action"] == "remove"),
)
)
@callback
def async_add_new_device_listener(
self, listener: Callable[[VoIPDevice], None]
) -> None:
"""Add a new device listener."""
self._new_device_listeners.append(listener)
@callback
def async_get_or_create(self, call_info: CallInfo) -> VoIPDevice:
"""Get or create a device."""
user_agent = call_info.headers.get("user-agent", "")
user_agent_parts = user_agent.split()
if len(user_agent_parts) == 3 and user_agent_parts[0] == "Grandstream":
manuf = user_agent_parts[0]
model = user_agent_parts[1]
fw_version = user_agent_parts[2]
else:
manuf = None
model = user_agent if user_agent else None
fw_version = None
dev_reg = dr.async_get(self.hass)
if call_info.caller_endpoint is None:
raise RuntimeError("Could not identify VOIP caller")
voip_id = call_info.caller_endpoint.uri
voip_device = self.devices.get(voip_id)
if voip_device is None:
# If we couldn't find the device based on SIP URI, see if we can
# find an old device based on just the host/IP and migrate it
voip_device = self.devices.get(call_info.caller_endpoint.host)
if voip_device is not None:
voip_device.voip_id = voip_id
self.devices[voip_id] = voip_device
dev_reg.async_update_device(
voip_device.device_id, new_identifiers={(DOMAIN, voip_id)}
)
# Update device with latest info
device = dev_reg.async_get_or_create(
config_entry_id=self.config_entry.entry_id,
identifiers={(DOMAIN, voip_id)},
name=voip_id,
manufacturer=manuf,
model=model,
sw_version=fw_version,
configuration_url=f"http://{call_info.caller_ip}",
)
if voip_device is not None:
return voip_device
voip_device = self.devices[voip_id] = VoIPDevice(
voip_id=voip_id,
device_id=device.id,
)
for listener in self._new_device_listeners:
listener(voip_device)
return voip_device
def __iter__(self) -> Iterator[VoIPDevice]:
"""Iterate over devices."""
return iter(self.devices.values())