Skip to content

Commit

Permalink
Add support for TimeEntity
Browse files Browse the repository at this point in the history
  • Loading branch information
ankohanse committed Nov 15, 2024
1 parent 17ca5ed commit 8b93837
Show file tree
Hide file tree
Showing 4 changed files with 227 additions and 1 deletion.
1 change: 1 addition & 0 deletions custom_components/studer_xcom/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
Platform.SELECT,
Platform.SWITCH,
Platform.DATETIME,
Platform.TIME,
]

HUB = "Hub"
Expand Down
2 changes: 2 additions & 0 deletions custom_components/studer_xcom/entity_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,8 @@ def _get_entity_platform(self, entity):
return Platform.BUTTON
elif entity.unit == "Seconds":
return Platform.DATETIME
elif entity.unit == "Minutes":
return Platform.TIME
else:
return Platform.NUMBER

Expand Down
2 changes: 1 addition & 1 deletion custom_components/studer_xcom/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ def _update_attributes(self, entity, is_create):
attr_precision = None
attr_min = int(entity.min) * weight if entity.min is not None else None
attr_max = int(entity.max) * weight if entity.max is not None else None
attr_val = int(value) * weight if entity.value is not None else None
attr_val = int(value) * weight if value is not None else None
attr_step = self.get_number_step()

case _:
Expand Down
223 changes: 223 additions & 0 deletions custom_components/studer_xcom/time.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import asyncio
import logging
import math

from homeassistant import config_entries
from homeassistant import exceptions
from homeassistant.components.time import TimeEntity
from homeassistant.components.time 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 time, timedelta

from collections import defaultdict
from collections import namedtuple
from collections.abc import Mapping


from .const import (
DOMAIN,
COORDINATOR,
MANUFACTURER,
ATTR_XCOM_FLASH_STATE,
ATTR_XCOM_RAM_STATE,
)
from .coordinator import (
StuderCoordinator,
StuderEntityData,
)
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.TIME, StuderTime, async_add_entities)


class StuderTime(CoordinatorEntity, TimeEntity, 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: StuderCoordinator, install_id, entity: StuderEntityData) -> 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: str = entity.object_id
self.entity_id: str = ENTITY_ID_FORMAT.format(entity.unique_id)
self.install_id: str = install_id

self._coordinator: StuderCoordinator = coordinator

# Custom extra attributes for the entity
self._attributes: dict[str, str | list[str]] = {}
self._xcom_flash_state: str = None
self._xcom_ram_state: str = 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_flash_state:
self._attributes[ATTR_XCOM_FLASH_STATE] = self._xcom_flash_state
if self._xcom_ram_state:
self._attributes[ATTR_XCOM_RAM_STATE] = self._xcom_ram_state

return self._attributes


@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
super()._handle_coordinator_update()

entity_map: dict[str, StuderEntityData] = self._coordinator.data

# find the correct device and status corresponding to this sensor
status: StuderEntityData|None = 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: StuderEntityData, is_create: bool):

# Process any changes
changed = False
value = entity.valueModified if entity.valueModified is not None else entity.value

match entity.format:
case FORMAT.INT32:
# Studer entity value is minutes since midnight with values between 0 (00:00) and 1440 (24:00).
# TimeEntity expects time object and can only be between 00:00 and 23:59
# We sneakily replace value 1440 (24:00) into 23:59
if value is None:
attr_val = None
elif int(value) >= 1440:
attr_val = time(23, 59).replace(tzinfo=self._coordinator.time_zone)
else:
attr_val = time(int(value // 60), int(value % 60)).replace(tzinfo=self._coordinator.time_zone)

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_flash_state != entity.value:
self._xcom_flash_state = entity.value
self._xcom_ram_state = entity.valueModified

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: time) -> None:
"""Change the date/time"""

entity_map: dict[str, StuderEntityData] = self._coordinator.data
entity: StuderEntityData = entity_map.get(self.object_id)

match entity.format:
case FORMAT.INT32:
# TimeEntity is a time object and can only be between 00:00 and 23:59
# Studer entity value is minutes since midnight with values between 0 (00:00) and 1440 (24:00).

minutes = value.hour * 60 + value.minute
if minutes < 1439:
entity_value = minutes
trace_value = value
else:
# We sneakily replace input 23:59 into 1440 (24:00)
entity_value = 1440
trace_value = "24:00"

case _:
_LOGGER.error(f"Unexpected format ({entity.format}) for a time entity")
return

_LOGGER.debug(f"Set {self.entity_id} to {trace_value} ({entity_value})")

success = await self._coordinator.async_modify_data(entity, entity_value)
if success:
self._attr_native_value = value
self._xcom_ram_state = entity_value
self.async_write_ha_state()

# No need to update self._xcom_ram_state for this entity

0 comments on commit 8b93837

Please sign in to comment.