diff --git a/custom_components/studer_xcom/binary_sensor.py b/custom_components/studer_xcom/binary_sensor.py index 7e7bcdf..fa02482 100644 --- a/custom_components/studer_xcom/binary_sensor.py +++ b/custom_components/studer_xcom/binary_sensor.py @@ -42,6 +42,7 @@ BINARY_SENSOR_VALUES_ON, BINARY_SENSOR_VALUES_OFF, BINARY_SENSOR_VALUES_ALL, + ATTR_XCOM_STATE, ) from .coordinator import ( StuderCoordinatorFactory, @@ -96,6 +97,10 @@ def __init__(self, coordinator, install_id, entity) -> None: self._coordinator = coordinator + # Custom extra attributes for the entity + self._attributes: dict[str, str | list[str]] = {} + self._xcom_state = None + # Create all attributes self._update_attributes(entity, True) @@ -116,6 +121,15 @@ def unique_id(self) -> str: def name(self) -> str: """Return the name of the entity.""" return self._attr_name + + + @property + def extra_state_attributes(self) -> dict[str, str | list[str]]: + """Return the state attributes.""" + if self._xcom_state: + self._attributes[ATTR_XCOM_STATE] = self._xcom_state + + return self._attributes @callback @@ -182,6 +196,11 @@ def _update_attributes(self, entity, is_create): changed = True # update value if it has changed + if is_create \ + or (self._xcom_state != entity.value): + + self._xcom_state = entity.value + if is_create \ or (self._attr_is_on != is_on): diff --git a/custom_components/studer_xcom/button.py b/custom_components/studer_xcom/button.py index 321e02d..cd461ae 100644 --- a/custom_components/studer_xcom/button.py +++ b/custom_components/studer_xcom/button.py @@ -75,11 +75,6 @@ def __init__(self, coordinator, install_id, entity) -> None: self._coordinator = coordinator - # Custom extra attributes for the entity - self._attributes: dict[str, str | list[str]] = {} - self._xcom_state = None - self._set_state = None - # Create all attributes self._update_attributes(entity, True) diff --git a/custom_components/studer_xcom/const.py b/custom_components/studer_xcom/const.py index 388afed..6d280d6 100644 --- a/custom_components/studer_xcom/const.py +++ b/custom_components/studer_xcom/const.py @@ -33,6 +33,7 @@ Platform.NUMBER, Platform.SELECT, Platform.SWITCH, + Platform.DATETIME, ] HUB = "Hub" @@ -79,6 +80,7 @@ PREFIX_NAME = "Studer" # Custom extra attributes to entities +ATTR_XCOM_STATE = "xcom_state" ATTR_XCOM_FLASH_STATE = "xcom_flash_state" ATTR_XCOM_RAM_STATE = "xcom_ram_state" diff --git a/custom_components/studer_xcom/coordinator.py b/custom_components/studer_xcom/coordinator.py index 5b69c54..2c8d933 100644 --- a/custom_components/studer_xcom/coordinator.py +++ b/custom_components/studer_xcom/coordinator.py @@ -8,7 +8,7 @@ import re from collections import namedtuple -from datetime import datetime, timedelta, timezone +from datetime import datetime, timedelta, timezone, tzinfo from typing import Any from homeassistant.components.diagnostics import REDACTED @@ -27,6 +27,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.helpers.update_coordinator import UpdateFailed +from homeassistant.util import dt as dt_util from homeassistant.const import ( CONF_PORT, @@ -287,6 +288,11 @@ def is_temp(self) -> bool: return self._is_temp + @property + def time_zone(self) -> tzinfo | None: + return dt_util.get_time_zone(self._hass.config.time_zone) + + async def _create_entity_map(self): entity_map: dict[str,StuderEntityData] = {} diff --git a/custom_components/studer_xcom/datetime.py b/custom_components/studer_xcom/datetime.py new file mode 100644 index 0000000..c7340cf --- /dev/null +++ b/custom_components/studer_xcom/datetime.py @@ -0,0 +1,206 @@ +import asyncio +import logging +import math + +from homeassistant import config_entries +from homeassistant import exceptions +from homeassistant.components.datetime import DateTimeEntity +from homeassistant.components.datetime import ENTITY_ID_FORMAT +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import IntegrationError +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity_registry import async_get +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.util import dt as dt_util + +from datetime import datetime +from datetime import timezone + +from collections import defaultdict +from collections import namedtuple +from collections.abc import Mapping + + +from .const import ( + DOMAIN, + COORDINATOR, + MANUFACTURER, + ATTR_XCOM_STATE, +) +from .entity_base import ( + StuderEntityHelperFactory, + StuderEntityHelper, + StuderEntity, +) +from aioxcom import ( + FORMAT +) + + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback): + """ + Setting up the adding and updating of number entities + """ + helper = StuderEntityHelperFactory.create(hass, config_entry) + await helper.async_setup_entry(Platform.DATETIME, StuderTime, async_add_entities) + + +class StuderTime(CoordinatorEntity, DateTimeEntity, StuderEntity): + """ + Representation of a Studer Time Entity. + + Could be a configuration setting that is part of a pump like ESybox, Esybox.mini + Or could be part of a communication module like DConnect Box/Box2 + """ + + def __init__(self, coordinator, install_id, entity) -> None: + """ Initialize the sensor. """ + CoordinatorEntity.__init__(self, coordinator) + StuderEntity.__init__(self, coordinator, entity) + + # The unique identifier for this sensor within Home Assistant + self.object_id = entity.object_id + self.entity_id = ENTITY_ID_FORMAT.format(entity.unique_id) + self.install_id = install_id + + self._coordinator = coordinator + + # Custom extra attributes for the entity + self._attributes: dict[str, str | list[str]] = {} + self._xcom_state = None + + # Create all attributes + self._update_attributes(entity, True) + + + @property + def suggested_object_id(self) -> str | None: + """Return input for object id.""" + return self.object_id + + + @property + def unique_id(self) -> str: + """Return a unique ID for use in home assistant.""" + return self._attr_unique_id + + + @property + def name(self) -> str: + """Return the name of the entity.""" + return self._attr_name + + + @property + def extra_state_attributes(self) -> dict[str, str | list[str]]: + """Return the state attributes.""" + if self._xcom_state: + self._attributes[ATTR_XCOM_STATE] = self._xcom_state + + return self._attributes + + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + super()._handle_coordinator_update() + + entity_map = self._coordinator.data + + # find the correct device and status corresponding to this sensor + status = entity_map.get(self.object_id) + + # Update any attributes + if status: + if self._update_attributes(status, False): + self.async_write_ha_state() + + + def _update_attributes(self, entity, is_create): + + # Process any changes + changed = False + + match entity.format: + case FORMAT.INT32: + # Studer entity value is seconds since 1 Jan 1970 in local timezone. DateTimeEntity expects UTC + # When converting we assume the studer local timezone equals the HomeAssistant timezone (Settings->General). + if entity.value is not None: + ts_local = int(entity.value) + dt_local = dt_util.utc_from_timestamp(ts_local).replace(tzinfo=self._coordinator.time_zone) + attr_val = dt_local + else: + attr_val = None + + case _: + _LOGGER.error(f"Unexpected format ({entity.format}) for a time entity") + return + + # update creation-time only attributes + if is_create: + self._attr_unique_id = entity.unique_id + + self._attr_has_entity_name = True + self._attr_name = entity.name + self._name = entity.name + + #self._attr_device_class = self.get_number_device_class() + self._attr_entity_category = self.get_entity_category() + + self._attr_device_info = DeviceInfo( + identifiers = {(DOMAIN, entity.device_id)}, + ) + changed = True + + # update value if it has changed + if is_create or self._xcom_state != entity.value: + self._xcom_state = entity.value + + if is_create or self._attr_native_value != attr_val: + self._attr_state = attr_val + self._attr_native_value = attr_val + + self._attr_icon = self.get_icon() + changed = True + + return changed + + + async def async_set_value(self, value: datetime) -> None: + """Change the date/time""" + + entity_map = self._coordinator.data + entity = entity_map.get(self.object_id) + + match entity.format: + case FORMAT.INT32: + # DateTimeEntity value is UTC, Studer expects seconds since 1 Jan 1970 in local timezone + # When converting we assume the studer local timezone equals the HomeAssistant timezone (Settings->General). + dt_local = value.astimezone(self._coordinator.time_zone) + ts_local = dt_util.as_timestamp(dt_local.replace(tzinfo=timezone.utc)) + entity_value = int(ts_local) + + case _: + _LOGGER.error(f"Unexpected format ({entity.format}) for a number entity") + return + + _LOGGER.debug(f"Set {self.entity_id} to {value} ({entity_value})") + + success = await self._coordinator.async_modify_data(entity, entity_value) + if success: + self._attr_native_value = value + self.async_write_ha_state() + + # No need to update self._xcom_ram_state for this entity + diff --git a/custom_components/studer_xcom/entity_base.py b/custom_components/studer_xcom/entity_base.py index b5bc947..2432be6 100644 --- a/custom_components/studer_xcom/entity_base.py +++ b/custom_components/studer_xcom/entity_base.py @@ -145,7 +145,7 @@ def _get_entity_platform(self, entity): Determine what platform an entry should be added into """ - # Is it a switch/select/number config or control entity? + # Is it a switch/select/number/time config or control entity? if entity.obj_type == OBJ_TYPE.PARAMETER: match entity.format: case FORMAT.BOOL: @@ -163,6 +163,8 @@ def _get_entity_platform(self, entity): case FORMAT.INT32: if entity.default=="S" or entity.min=="S" or entity.max=="S": return Platform.BUTTON + elif entity.unit == "Seconds": + return Platform.DATETIME else: return Platform.NUMBER diff --git a/custom_components/studer_xcom/number.py b/custom_components/studer_xcom/number.py index 49ffe17..84f2920 100644 --- a/custom_components/studer_xcom/number.py +++ b/custom_components/studer_xcom/number.py @@ -79,8 +79,8 @@ def __init__(self, coordinator, install_id, entity) -> None: # Custom extra attributes for the entity self._attributes: dict[str, str | list[str]] = {} - self._xcom_state = None - self._set_state = None + self._xcom_flash_state = None + self._xcom_ram_state = None # Create all attributes self._update_attributes(entity, True) @@ -227,5 +227,4 @@ async def async_set_native_value(self, value: float) -> None: self._attr_native_value = value self._xcom_ram_state = entity_value self.async_write_ha_state() - _LOGGER.debug(f"after modify data for entity {entity.device_code} {entity.nr}. _set_state={self._set_state}") diff --git a/custom_components/studer_xcom/select.py b/custom_components/studer_xcom/select.py index 073bdca..29e258a 100644 --- a/custom_components/studer_xcom/select.py +++ b/custom_components/studer_xcom/select.py @@ -76,7 +76,6 @@ def __init__(self, coordinator, install_id, entity) -> None: self._attributes: dict[str, str | list[str]] = {} self._xcom_flash_state = None self._xcom_ram_state = None - self._set_state = None # Create all attributes self._update_attributes(entity, True) diff --git a/custom_components/studer_xcom/sensor.py b/custom_components/studer_xcom/sensor.py index 1119c66..d866853 100644 --- a/custom_components/studer_xcom/sensor.py +++ b/custom_components/studer_xcom/sensor.py @@ -41,6 +41,7 @@ CONF_OPTIONS, CONF_NR, CONF_ADDRESS, + ATTR_XCOM_STATE, ) from .coordinator import ( StuderCoordinatorFactory, @@ -83,6 +84,10 @@ def __init__(self, coordinator, install_id, entity) -> None: self._coordinator = coordinator + # Custom extra attributes for the entity + self._attributes: dict[str, str | list[str]] = {} + self._xcom_state = None + # Create all attributes self._update_attributes(entity, True) @@ -103,6 +108,15 @@ def unique_id(self) -> str: def name(self) -> str: """Return the name of the entity.""" return self._attr_name + + + @property + def extra_state_attributes(self) -> dict[str, str | list[str]]: + """Return the state attributes.""" + if self._xcom_state: + self._attributes[ATTR_XCOM_STATE] = self._xcom_state + + return self._attributes @callback @@ -173,6 +187,9 @@ def _update_attributes(self, entity, is_create): changed = True # update value if it has changed + if is_create or self._xcom_state != entity.value: + self._xcom_state = entity.value + if is_create or self._attr_native_value != attr_val: if not is_create: _LOGGER.debug(f"Sensor change value {self.object_id} from {self._attr_native_value} to {attr_val}") diff --git a/custom_components/studer_xcom/switch.py b/custom_components/studer_xcom/switch.py index 8635cd4..d308c49 100644 --- a/custom_components/studer_xcom/switch.py +++ b/custom_components/studer_xcom/switch.py @@ -89,7 +89,6 @@ def __init__(self, coordinator, install_id, entity) -> None: self._xcom_flash_state = None self._xcom_ram_state = None self._set_is_on = None - self._set_state = None # Create all attributes self._update_attributes(entity, True)