1
mirror of https://github.com/home-assistant/core synced 2024-08-02 23:40:32 +02:00
ha-core/homeassistant/components/deconz/gateway.py
Robert Svensson c2f026d0a7
Minor deCONZ clean up (#76323)
* Rename secondary_temperature with internal_temperature

* Prefix binary and sensor descriptions matching on all sensor devices with COMMON_

* Always create entities in the same order

Its been reported previously that if the integration is removed and setup again that entity IDs can change if not sorted in the numerical order

* Rename alarmsystems to alarm_systems

* Use websocket enums

* Don't use legacy pydeconz constants

* Bump pydeconz to v103

* unsub -> unsubscribe
2022-08-06 01:34:27 +02:00

351 lines
12 KiB
Python

"""Representation of a deCONZ gateway."""
from __future__ import annotations
import asyncio
from collections.abc import Callable
from types import MappingProxyType
from typing import TYPE_CHECKING, Any, cast
import async_timeout
from pydeconz import DeconzSession, errors
from pydeconz.interfaces import sensors
from pydeconz.interfaces.api_handlers import APIHandler, GroupedAPIHandler
from pydeconz.interfaces.groups import GroupHandler
from pydeconz.models.event import EventType
from homeassistant.config_entries import SOURCE_HASSIO, ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.helpers import (
aiohttp_client,
device_registry as dr,
entity_registry as er,
)
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.dispatcher import async_dispatcher_send
from .const import (
CONF_ALLOW_CLIP_SENSOR,
CONF_ALLOW_DECONZ_GROUPS,
CONF_ALLOW_NEW_DEVICES,
CONF_MASTER_GATEWAY,
DEFAULT_ALLOW_CLIP_SENSOR,
DEFAULT_ALLOW_DECONZ_GROUPS,
DEFAULT_ALLOW_NEW_DEVICES,
DOMAIN as DECONZ_DOMAIN,
HASSIO_CONFIGURATION_URL,
LOGGER,
PLATFORMS,
)
from .errors import AuthenticationRequired, CannotConnect
if TYPE_CHECKING:
from .deconz_event import DeconzAlarmEvent, DeconzEvent
SENSORS = (
sensors.SensorResourceManager,
sensors.AirPurifierHandler,
sensors.AirQualityHandler,
sensors.AlarmHandler,
sensors.AncillaryControlHandler,
sensors.BatteryHandler,
sensors.CarbonMonoxideHandler,
sensors.ConsumptionHandler,
sensors.DaylightHandler,
sensors.DoorLockHandler,
sensors.FireHandler,
sensors.GenericFlagHandler,
sensors.GenericStatusHandler,
sensors.HumidityHandler,
sensors.LightLevelHandler,
sensors.OpenCloseHandler,
sensors.PowerHandler,
sensors.PresenceHandler,
sensors.PressureHandler,
sensors.RelativeRotaryHandler,
sensors.SwitchHandler,
sensors.TemperatureHandler,
sensors.ThermostatHandler,
sensors.TimeHandler,
sensors.VibrationHandler,
sensors.WaterHandler,
)
class DeconzGateway:
"""Manages a single deCONZ gateway."""
def __init__(
self, hass: HomeAssistant, config_entry: ConfigEntry, api: DeconzSession
) -> None:
"""Initialize the system."""
self.hass = hass
self.config_entry = config_entry
self.api = api
api.connection_status_callback = self.async_connection_status_callback
self.available = True
self.ignore_state_updates = False
self.signal_reachable = f"deconz-reachable-{config_entry.entry_id}"
self.deconz_ids: dict[str, str] = {}
self.entities: dict[str, set[str]] = {}
self.events: list[DeconzAlarmEvent | DeconzEvent] = []
self.clip_sensors: set[tuple[Callable[[EventType, str], None], str]] = set()
self.deconz_groups: set[tuple[Callable[[EventType, str], None], str]] = set()
self.ignored_devices: set[tuple[Callable[[EventType, str], None], str]] = set()
self.option_allow_clip_sensor = self.config_entry.options.get(
CONF_ALLOW_CLIP_SENSOR, DEFAULT_ALLOW_CLIP_SENSOR
)
self.option_allow_deconz_groups = config_entry.options.get(
CONF_ALLOW_DECONZ_GROUPS, DEFAULT_ALLOW_DECONZ_GROUPS
)
self.option_allow_new_devices = config_entry.options.get(
CONF_ALLOW_NEW_DEVICES, DEFAULT_ALLOW_NEW_DEVICES
)
@property
def bridgeid(self) -> str:
"""Return the unique identifier of the gateway."""
return cast(str, self.config_entry.unique_id)
@property
def host(self) -> str:
"""Return the host of the gateway."""
return cast(str, self.config_entry.data[CONF_HOST])
@property
def master(self) -> bool:
"""Gateway which is used with deCONZ services without defining id."""
return cast(bool, self.config_entry.options[CONF_MASTER_GATEWAY])
@callback
def register_platform_add_device_callback(
self,
add_device_callback: Callable[[EventType, str], None],
deconz_device_interface: APIHandler | GroupedAPIHandler,
always_ignore_clip_sensors: bool = False,
) -> None:
"""Wrap add_device_callback to check allow_new_devices option."""
initializing = True
def async_add_device(_: EventType, device_id: str) -> None:
"""Add device or add it to ignored_devices set.
If ignore_state_updates is True means device_refresh service is used.
Device_refresh is expected to load new devices.
"""
if (
not initializing
and not self.option_allow_new_devices
and not self.ignore_state_updates
):
self.ignored_devices.add((async_add_device, device_id))
return
if isinstance(deconz_device_interface, GroupHandler):
self.deconz_groups.add((async_add_device, device_id))
if not self.option_allow_deconz_groups:
return
if isinstance(deconz_device_interface, SENSORS):
device = deconz_device_interface[device_id]
if device.type.startswith("CLIP") and not always_ignore_clip_sensors:
self.clip_sensors.add((async_add_device, device_id))
if not self.option_allow_clip_sensor:
return
add_device_callback(EventType.ADDED, device_id)
self.config_entry.async_on_unload(
deconz_device_interface.subscribe(
async_add_device,
EventType.ADDED,
)
)
for device_id in sorted(deconz_device_interface, key=int):
async_add_device(EventType.ADDED, device_id)
initializing = False
@callback
def load_ignored_devices(self) -> None:
"""Load previously ignored devices."""
for add_entities, device_id in self.ignored_devices:
add_entities(EventType.ADDED, device_id)
self.ignored_devices.clear()
# Callbacks
@callback
def async_connection_status_callback(self, available: bool) -> None:
"""Handle signals of gateway connection status."""
self.available = available
self.ignore_state_updates = False
async_dispatcher_send(self.hass, self.signal_reachable)
async def async_update_device_registry(self) -> None:
"""Update device registry."""
if self.api.config.mac is None:
return
device_registry = dr.async_get(self.hass)
# Host device
device_registry.async_get_or_create(
config_entry_id=self.config_entry.entry_id,
connections={(CONNECTION_NETWORK_MAC, self.api.config.mac)},
)
# Gateway service
configuration_url = f"http://{self.host}:{self.config_entry.data[CONF_PORT]}"
if self.config_entry.source == SOURCE_HASSIO:
configuration_url = HASSIO_CONFIGURATION_URL
device_registry.async_get_or_create(
config_entry_id=self.config_entry.entry_id,
configuration_url=configuration_url,
entry_type=dr.DeviceEntryType.SERVICE,
identifiers={(DECONZ_DOMAIN, self.api.config.bridge_id)},
manufacturer="Dresden Elektronik",
model=self.api.config.model_id,
name=self.api.config.name,
sw_version=self.api.config.software_version,
via_device=(CONNECTION_NETWORK_MAC, self.api.config.mac),
)
@staticmethod
async def async_config_entry_updated(
hass: HomeAssistant, entry: ConfigEntry
) -> None:
"""Handle signals of config entry being updated.
This is a static method because a class method (bound method), can not be used with weak references.
Causes for this is either discovery updating host address or config entry options changing.
"""
gateway = get_gateway_from_config_entry(hass, entry)
if gateway.api.host != gateway.host:
gateway.api.close()
gateway.api.host = gateway.host
gateway.api.start()
return
await gateway.options_updated()
async def options_updated(self) -> None:
"""Manage entities affected by config entry options."""
deconz_ids = []
# Allow CLIP sensors
option_allow_clip_sensor = self.config_entry.options.get(
CONF_ALLOW_CLIP_SENSOR, DEFAULT_ALLOW_CLIP_SENSOR
)
if option_allow_clip_sensor != self.option_allow_clip_sensor:
self.option_allow_clip_sensor = option_allow_clip_sensor
if option_allow_clip_sensor:
for add_device, device_id in self.clip_sensors:
add_device(EventType.ADDED, device_id)
else:
deconz_ids += [
sensor.deconz_id
for sensor in self.api.sensors.values()
if sensor.type.startswith("CLIP")
]
# Allow Groups
option_allow_deconz_groups = self.config_entry.options.get(
CONF_ALLOW_DECONZ_GROUPS, DEFAULT_ALLOW_DECONZ_GROUPS
)
if option_allow_deconz_groups != self.option_allow_deconz_groups:
self.option_allow_deconz_groups = option_allow_deconz_groups
if option_allow_deconz_groups:
for add_device, device_id in self.deconz_groups:
add_device(EventType.ADDED, device_id)
else:
deconz_ids += [group.deconz_id for group in self.api.groups.values()]
# Allow adding new devices
option_allow_new_devices = self.config_entry.options.get(
CONF_ALLOW_NEW_DEVICES, DEFAULT_ALLOW_NEW_DEVICES
)
if option_allow_new_devices != self.option_allow_new_devices:
self.option_allow_new_devices = option_allow_new_devices
if option_allow_new_devices:
self.load_ignored_devices()
# Remove entities based on above categories
entity_registry = er.async_get(self.hass)
for entity_id, deconz_id in self.deconz_ids.items():
if deconz_id in deconz_ids and entity_registry.async_is_registered(
entity_id
):
# Removing an entity from the entity registry will also remove them
# from Home Assistant
entity_registry.async_remove(entity_id)
@callback
def shutdown(self, event: Event) -> None:
"""Wrap the call to deconz.close.
Used as an argument to EventBus.async_listen_once.
"""
self.api.close()
async def async_reset(self) -> bool:
"""Reset this gateway to default state."""
self.api.connection_status_callback = None
self.api.close()
await self.hass.config_entries.async_unload_platforms(
self.config_entry, PLATFORMS
)
self.deconz_ids = {}
return True
@callback
def get_gateway_from_config_entry(
hass: HomeAssistant, config_entry: ConfigEntry
) -> DeconzGateway:
"""Return gateway with a matching config entry ID."""
return cast(DeconzGateway, hass.data[DECONZ_DOMAIN][config_entry.entry_id])
async def get_deconz_session(
hass: HomeAssistant,
config: MappingProxyType[str, Any],
) -> DeconzSession:
"""Create a gateway object and verify configuration."""
session = aiohttp_client.async_get_clientsession(hass)
deconz_session = DeconzSession(
session,
config[CONF_HOST],
config[CONF_PORT],
config[CONF_API_KEY],
)
try:
async with async_timeout.timeout(10):
await deconz_session.refresh_state()
return deconz_session
except errors.Unauthorized as err:
LOGGER.warning("Invalid key for deCONZ at %s", config[CONF_HOST])
raise AuthenticationRequired from err
except (asyncio.TimeoutError, errors.RequestError, errors.ResponseError) as err:
LOGGER.error("Error connecting to deCONZ gateway at %s", config[CONF_HOST])
raise CannotConnect from err