Skip to content

Commit

Permalink
Add support for Platform.DATETIME
Browse files Browse the repository at this point in the history
  • Loading branch information
ankohanse committed Nov 14, 2024
1 parent 3c5c757 commit adff4cd
Show file tree
Hide file tree
Showing 10 changed files with 256 additions and 12 deletions.
19 changes: 19 additions & 0 deletions custom_components/studer_xcom/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
BINARY_SENSOR_VALUES_ON,
BINARY_SENSOR_VALUES_OFF,
BINARY_SENSOR_VALUES_ALL,
ATTR_XCOM_STATE,
)
from .coordinator import (
StuderCoordinatorFactory,
Expand Down Expand Up @@ -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)

Expand All @@ -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
Expand Down Expand Up @@ -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):

Expand Down
5 changes: 0 additions & 5 deletions custom_components/studer_xcom/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
2 changes: 2 additions & 0 deletions custom_components/studer_xcom/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
Platform.NUMBER,
Platform.SELECT,
Platform.SWITCH,
Platform.DATETIME,
]

HUB = "Hub"
Expand Down Expand Up @@ -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"

Expand Down
8 changes: 7 additions & 1 deletion custom_components/studer_xcom/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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] = {}

Expand Down
206 changes: 206 additions & 0 deletions custom_components/studer_xcom/datetime.py
Original file line number Diff line number Diff line change
@@ -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

4 changes: 3 additions & 1 deletion custom_components/studer_xcom/entity_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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

Expand Down
5 changes: 2 additions & 3 deletions custom_components/studer_xcom/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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}")

1 change: 0 additions & 1 deletion custom_components/studer_xcom/select.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
17 changes: 17 additions & 0 deletions custom_components/studer_xcom/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
CONF_OPTIONS,
CONF_NR,
CONF_ADDRESS,
ATTR_XCOM_STATE,
)
from .coordinator import (
StuderCoordinatorFactory,
Expand Down Expand Up @@ -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)

Expand All @@ -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
Expand Down Expand Up @@ -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}")
Expand Down
Loading

0 comments on commit adff4cd

Please sign in to comment.