Extract Geniushub base entities in separate module (#122331)

This commit is contained in:
Joost Lekkerkerker 2024-07-23 09:34:00 +02:00 committed by GitHub
parent be4c7291bd
commit cd48278671
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 180 additions and 170 deletions

View File

@ -2,9 +2,8 @@
from __future__ import annotations from __future__ import annotations
from datetime import datetime, timedelta from datetime import timedelta
import logging import logging
from typing import Any
import aiohttp import aiohttp
from geniushubclient import GeniusHub from geniushubclient import GeniusHub
@ -21,7 +20,6 @@ from homeassistant.const import (
CONF_TOKEN, CONF_TOKEN,
CONF_USERNAME, CONF_USERNAME,
Platform, Platform,
UnitOfTemperature,
) )
from homeassistant.core import ( from homeassistant.core import (
DOMAIN as HOMEASSISTANT_DOMAIN, DOMAIN as HOMEASSISTANT_DOMAIN,
@ -32,31 +30,16 @@ from homeassistant.core import (
from homeassistant.data_entry_flow import FlowResultType from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers import config_validation as cv, entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.dispatcher import async_dispatcher_send
async_dispatcher_connect,
async_dispatcher_send,
)
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.service import verify_domain_control from homeassistant.helpers.service import verify_domain_control
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
import homeassistant.util.dt as dt_util
from .const import DOMAIN from .const import DOMAIN
_LOGGER = logging.getLogger(__name__) _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) SCAN_INTERVAL = timedelta(seconds=60)
@ -279,149 +262,3 @@ class GeniusBroker:
self.client._zones, # noqa: SLF001 self.client._zones, # noqa: SLF001
self.client._devices, # 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)
)

View File

@ -6,7 +6,8 @@ from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import GeniusDevice, GeniusHubConfigEntry from . import GeniusHubConfigEntry
from .entity import GeniusDevice
GH_STATE_ATTR = "outputOnOff" GH_STATE_ATTR = "outputOnOff"
GH_TYPE = "Receiver" GH_TYPE = "Receiver"

View File

@ -13,7 +13,8 @@ from homeassistant.components.climate import (
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback 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 # GeniusHub Zones support: Off, Timer, Override/Boost, Footprint & Linked modes
HA_HVAC_TO_GH = {HVACMode.OFF: "off", HVACMode.HEAT: "timer"} HA_HVAC_TO_GH = {HVACMode.OFF: "off", HVACMode.HEAT: "timer"}

View File

@ -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)
)

View File

@ -11,7 +11,8 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from . import GeniusDevice, GeniusEntity, GeniusHubConfigEntry from . import GeniusHubConfigEntry
from .entity import GeniusDevice, GeniusEntity
GH_STATE_ATTR = "batteryLevel" GH_STATE_ATTR = "batteryLevel"

View File

@ -13,7 +13,8 @@ from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import VolDictType 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" GH_ON_OFF_ZONE = "on / off"

View File

@ -10,7 +10,8 @@ from homeassistant.const import STATE_OFF
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import GeniusHeatingZone, GeniusHubConfigEntry from . import GeniusHubConfigEntry
from .entity import GeniusHeatingZone
STATE_AUTO = "auto" STATE_AUTO = "auto"
STATE_MANUAL = "manual" STATE_MANUAL = "manual"