From cd4827867182791d2cedc717fa1bfe30667b4ac2 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 23 Jul 2024 09:34:00 +0200 Subject: [PATCH] Extract Geniushub base entities in separate module (#122331) --- .../components/geniushub/__init__.py | 167 +---------------- .../components/geniushub/binary_sensor.py | 3 +- homeassistant/components/geniushub/climate.py | 3 +- homeassistant/components/geniushub/entity.py | 168 ++++++++++++++++++ homeassistant/components/geniushub/sensor.py | 3 +- homeassistant/components/geniushub/switch.py | 3 +- .../components/geniushub/water_heater.py | 3 +- 7 files changed, 180 insertions(+), 170 deletions(-) create mode 100644 homeassistant/components/geniushub/entity.py diff --git a/homeassistant/components/geniushub/__init__.py b/homeassistant/components/geniushub/__init__.py index 84e835ac2bb..836add310b6 100644 --- a/homeassistant/components/geniushub/__init__.py +++ b/homeassistant/components/geniushub/__init__.py @@ -2,9 +2,8 @@ from __future__ import annotations -from datetime import datetime, timedelta +from datetime import timedelta import logging -from typing import Any import aiohttp from geniushubclient import GeniusHub @@ -21,7 +20,6 @@ from homeassistant.const import ( CONF_TOKEN, CONF_USERNAME, Platform, - UnitOfTemperature, ) from homeassistant.core import ( DOMAIN as HOMEASSISTANT_DOMAIN, @@ -32,31 +30,16 @@ from homeassistant.core import ( from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.service import verify_domain_control from homeassistant.helpers.typing import ConfigType -import homeassistant.util.dt as dt_util from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -# temperature is repeated here, as it gives access to high-precision temps -GH_ZONE_ATTRS = ["mode", "temperature", "type", "occupied", "override"] -GH_DEVICE_ATTRS = { - "luminance": "luminance", - "measuredTemperature": "measured_temperature", - "occupancyTrigger": "occupancy_trigger", - "setback": "setback", - "setTemperature": "set_temperature", - "wakeupInterval": "wakeup_interval", -} SCAN_INTERVAL = timedelta(seconds=60) @@ -279,149 +262,3 @@ class GeniusBroker: self.client._zones, # noqa: SLF001 self.client._devices, # noqa: SLF001 ) - - -class GeniusEntity(Entity): - """Base for all Genius Hub entities.""" - - _attr_should_poll = False - - def __init__(self) -> None: - """Initialize the entity.""" - self._unique_id: str | None = None - - async def async_added_to_hass(self) -> None: - """Set up a listener when this entity is added to HA.""" - self.async_on_remove(async_dispatcher_connect(self.hass, DOMAIN, self._refresh)) - - async def _refresh(self, payload: dict | None = None) -> None: - """Process any signals.""" - self.async_schedule_update_ha_state(force_refresh=True) - - @property - def unique_id(self) -> str | None: - """Return a unique ID.""" - return self._unique_id - - -class GeniusDevice(GeniusEntity): - """Base for all Genius Hub devices.""" - - def __init__(self, broker, device) -> None: - """Initialize the Device.""" - super().__init__() - - self._device = device - self._unique_id = f"{broker.hub_uid}_device_{device.id}" - self._last_comms: datetime | None = None - self._state_attr = None - - @property - def extra_state_attributes(self) -> dict[str, Any]: - """Return the device state attributes.""" - attrs = {} - attrs["assigned_zone"] = self._device.data["assignedZones"][0]["name"] - if self._last_comms: - attrs["last_comms"] = self._last_comms.isoformat() - - state = dict(self._device.data["state"]) - if "_state" in self._device.data: # only via v3 API - state.update(self._device.data["_state"]) - - attrs["state"] = { - GH_DEVICE_ATTRS[k]: v for k, v in state.items() if k in GH_DEVICE_ATTRS - } - - return attrs - - async def async_update(self) -> None: - """Update an entity's state data.""" - if "_state" in self._device.data: # only via v3 API - self._last_comms = dt_util.utc_from_timestamp( - self._device.data["_state"]["lastComms"] - ) - - -class GeniusZone(GeniusEntity): - """Base for all Genius Hub zones.""" - - def __init__(self, broker, zone) -> None: - """Initialize the Zone.""" - super().__init__() - - self._zone = zone - self._unique_id = f"{broker.hub_uid}_zone_{zone.id}" - - async def _refresh(self, payload: dict | None = None) -> None: - """Process any signals.""" - if payload is None: - self.async_schedule_update_ha_state(force_refresh=True) - return - - if payload["unique_id"] != self._unique_id: - return - - if payload["service"] == SVC_SET_ZONE_OVERRIDE: - temperature = round(payload["data"][ATTR_TEMPERATURE] * 10) / 10 - duration = payload["data"].get(ATTR_DURATION, timedelta(hours=1)) - - await self._zone.set_override(temperature, int(duration.total_seconds())) - return - - mode = payload["data"][ATTR_ZONE_MODE] - - if mode == "footprint" and not self._zone._has_pir: # noqa: SLF001 - raise TypeError( - f"'{self.entity_id}' cannot support footprint mode (it has no PIR)" - ) - - await self._zone.set_mode(mode) - - @property - def name(self) -> str: - """Return the name of the climate device.""" - return self._zone.name - - @property - def extra_state_attributes(self) -> dict[str, Any]: - """Return the device state attributes.""" - status = {k: v for k, v in self._zone.data.items() if k in GH_ZONE_ATTRS} - return {"status": status} - - -class GeniusHeatingZone(GeniusZone): - """Base for Genius Heating Zones.""" - - _max_temp: float - _min_temp: float - - @property - def current_temperature(self) -> float | None: - """Return the current temperature.""" - return self._zone.data.get("temperature") - - @property - def target_temperature(self) -> float: - """Return the temperature we try to reach.""" - return self._zone.data["setpoint"] - - @property - def min_temp(self) -> float: - """Return max valid temperature that can be set.""" - return self._min_temp - - @property - def max_temp(self) -> float: - """Return max valid temperature that can be set.""" - return self._max_temp - - @property - def temperature_unit(self) -> str: - """Return the unit of measurement.""" - return UnitOfTemperature.CELSIUS - - async def async_set_temperature(self, **kwargs) -> None: - """Set a new target temperature for this zone.""" - await self._zone.set_override( - kwargs[ATTR_TEMPERATURE], kwargs.get(ATTR_DURATION, 3600) - ) diff --git a/homeassistant/components/geniushub/binary_sensor.py b/homeassistant/components/geniushub/binary_sensor.py index 2d6acf0c955..01ccc950fd6 100644 --- a/homeassistant/components/geniushub/binary_sensor.py +++ b/homeassistant/components/geniushub/binary_sensor.py @@ -6,7 +6,8 @@ from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import GeniusDevice, GeniusHubConfigEntry +from . import GeniusHubConfigEntry +from .entity import GeniusDevice GH_STATE_ATTR = "outputOnOff" GH_TYPE = "Receiver" diff --git a/homeassistant/components/geniushub/climate.py b/homeassistant/components/geniushub/climate.py index ea2a79be767..99d1bde8099 100644 --- a/homeassistant/components/geniushub/climate.py +++ b/homeassistant/components/geniushub/climate.py @@ -13,7 +13,8 @@ from homeassistant.components.climate import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import GeniusHeatingZone, GeniusHubConfigEntry +from . import GeniusHubConfigEntry +from .entity import GeniusHeatingZone # GeniusHub Zones support: Off, Timer, Override/Boost, Footprint & Linked modes HA_HVAC_TO_GH = {HVACMode.OFF: "off", HVACMode.HEAT: "timer"} diff --git a/homeassistant/components/geniushub/entity.py b/homeassistant/components/geniushub/entity.py new file mode 100644 index 00000000000..7c40c41bda5 --- /dev/null +++ b/homeassistant/components/geniushub/entity.py @@ -0,0 +1,168 @@ +"""Base entity for Geniushub.""" + +from datetime import datetime, timedelta +from typing import Any + +from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity +import homeassistant.util.dt as dt_util + +from . import ATTR_DURATION, ATTR_ZONE_MODE, DOMAIN, SVC_SET_ZONE_OVERRIDE + +# temperature is repeated here, as it gives access to high-precision temps +GH_ZONE_ATTRS = ["mode", "temperature", "type", "occupied", "override"] +GH_DEVICE_ATTRS = { + "luminance": "luminance", + "measuredTemperature": "measured_temperature", + "occupancyTrigger": "occupancy_trigger", + "setback": "setback", + "setTemperature": "set_temperature", + "wakeupInterval": "wakeup_interval", +} + + +class GeniusEntity(Entity): + """Base for all Genius Hub entities.""" + + _attr_should_poll = False + + def __init__(self) -> None: + """Initialize the entity.""" + self._unique_id: str | None = None + + async def async_added_to_hass(self) -> None: + """Set up a listener when this entity is added to HA.""" + self.async_on_remove(async_dispatcher_connect(self.hass, DOMAIN, self._refresh)) + + async def _refresh(self, payload: dict | None = None) -> None: + """Process any signals.""" + self.async_schedule_update_ha_state(force_refresh=True) + + @property + def unique_id(self) -> str | None: + """Return a unique ID.""" + return self._unique_id + + +class GeniusDevice(GeniusEntity): + """Base for all Genius Hub devices.""" + + def __init__(self, broker, device) -> None: + """Initialize the Device.""" + super().__init__() + + self._device = device + self._unique_id = f"{broker.hub_uid}_device_{device.id}" + self._last_comms: datetime | None = None + self._state_attr = None + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return the device state attributes.""" + attrs = {} + attrs["assigned_zone"] = self._device.data["assignedZones"][0]["name"] + if self._last_comms: + attrs["last_comms"] = self._last_comms.isoformat() + + state = dict(self._device.data["state"]) + if "_state" in self._device.data: # only via v3 API + state.update(self._device.data["_state"]) + + attrs["state"] = { + GH_DEVICE_ATTRS[k]: v for k, v in state.items() if k in GH_DEVICE_ATTRS + } + + return attrs + + async def async_update(self) -> None: + """Update an entity's state data.""" + if "_state" in self._device.data: # only via v3 API + self._last_comms = dt_util.utc_from_timestamp( + self._device.data["_state"]["lastComms"] + ) + + +class GeniusZone(GeniusEntity): + """Base for all Genius Hub zones.""" + + def __init__(self, broker, zone) -> None: + """Initialize the Zone.""" + super().__init__() + + self._zone = zone + self._unique_id = f"{broker.hub_uid}_zone_{zone.id}" + + async def _refresh(self, payload: dict | None = None) -> None: + """Process any signals.""" + if payload is None: + self.async_schedule_update_ha_state(force_refresh=True) + return + + if payload["unique_id"] != self._unique_id: + return + + if payload["service"] == SVC_SET_ZONE_OVERRIDE: + temperature = round(payload["data"][ATTR_TEMPERATURE] * 10) / 10 + duration = payload["data"].get(ATTR_DURATION, timedelta(hours=1)) + + await self._zone.set_override(temperature, int(duration.total_seconds())) + return + + mode = payload["data"][ATTR_ZONE_MODE] + + if mode == "footprint" and not self._zone._has_pir: # noqa: SLF001 + raise TypeError( + f"'{self.entity_id}' cannot support footprint mode (it has no PIR)" + ) + + await self._zone.set_mode(mode) + + @property + def name(self) -> str: + """Return the name of the climate device.""" + return self._zone.name + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return the device state attributes.""" + status = {k: v for k, v in self._zone.data.items() if k in GH_ZONE_ATTRS} + return {"status": status} + + +class GeniusHeatingZone(GeniusZone): + """Base for Genius Heating Zones.""" + + _max_temp: float + _min_temp: float + + @property + def current_temperature(self) -> float | None: + """Return the current temperature.""" + return self._zone.data.get("temperature") + + @property + def target_temperature(self) -> float: + """Return the temperature we try to reach.""" + return self._zone.data["setpoint"] + + @property + def min_temp(self) -> float: + """Return max valid temperature that can be set.""" + return self._min_temp + + @property + def max_temp(self) -> float: + """Return max valid temperature that can be set.""" + return self._max_temp + + @property + def temperature_unit(self) -> str: + """Return the unit of measurement.""" + return UnitOfTemperature.CELSIUS + + async def async_set_temperature(self, **kwargs) -> None: + """Set a new target temperature for this zone.""" + await self._zone.set_override( + kwargs[ATTR_TEMPERATURE], kwargs.get(ATTR_DURATION, 3600) + ) diff --git a/homeassistant/components/geniushub/sensor.py b/homeassistant/components/geniushub/sensor.py index ee65e679498..cfe4107428c 100644 --- a/homeassistant/components/geniushub/sensor.py +++ b/homeassistant/components/geniushub/sensor.py @@ -11,7 +11,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.dt as dt_util -from . import GeniusDevice, GeniusEntity, GeniusHubConfigEntry +from . import GeniusHubConfigEntry +from .entity import GeniusDevice, GeniusEntity GH_STATE_ATTR = "batteryLevel" diff --git a/homeassistant/components/geniushub/switch.py b/homeassistant/components/geniushub/switch.py index 2fffbddde01..3af82eb4e92 100644 --- a/homeassistant/components/geniushub/switch.py +++ b/homeassistant/components/geniushub/switch.py @@ -13,7 +13,8 @@ from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import VolDictType -from . import ATTR_DURATION, GeniusHubConfigEntry, GeniusZone +from . import ATTR_DURATION, GeniusHubConfigEntry +from .entity import GeniusZone GH_ON_OFF_ZONE = "on / off" diff --git a/homeassistant/components/geniushub/water_heater.py b/homeassistant/components/geniushub/water_heater.py index 6d3da570547..2807bd60611 100644 --- a/homeassistant/components/geniushub/water_heater.py +++ b/homeassistant/components/geniushub/water_heater.py @@ -10,7 +10,8 @@ from homeassistant.const import STATE_OFF from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import GeniusHeatingZone, GeniusHubConfigEntry +from . import GeniusHubConfigEntry +from .entity import GeniusHeatingZone STATE_AUTO = "auto" STATE_MANUAL = "manual"