Files
homeassistant_config/config/custom_components/irrigation_unlimited/entity.py
2024-05-31 09:39:52 +02:00

387 lines
13 KiB
Python

"""HA entity classes"""
import json
from collections.abc import Iterator
from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.core import ServiceCall
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.util import dt
from homeassistant.const import (
CONF_STATE,
CONF_UNTIL,
STATE_OK,
STATE_ON,
)
from .irrigation_unlimited import (
IUCoordinator,
IUController,
IUZone,
IUSequence,
IUSequenceZone,
IUAdjustment,
IUBase,
)
from .const import (
ATTR_ADJUSTMENT,
ATTR_CONFIGURATION,
ATTR_CONTROLLER_COUNT,
ATTR_ENABLED,
ATTR_NEXT_TICK,
ATTR_TICK_LOG,
ATTR_SUSPENDED,
CONF_CONTROLLER,
CONF_CONTROLLERS,
CONF_ENABLED,
CONF_INDEX,
CONF_RESET,
CONF_SEQUENCE,
CONF_SEQUENCE_ID,
CONF_SEQUENCE_ZONE,
CONF_SEQUENCE_ZONES,
CONF_SEQUENCES,
CONF_ZONE,
CONF_ZONES,
COORDINATOR,
ICON,
SERVICE_ENABLE,
SERVICE_DISABLE,
SERVICE_TIME_ADJUST,
SERVICE_SUSPEND,
STATUS_INITIALISING,
)
class IURestore:
"""Restore class"""
# pylint: disable=too-few-public-methods
def __init__(self, data: dict, coordinator: IUCoordinator):
self._coordinator = coordinator
self._is_on = []
if data is not None and isinstance(data, dict):
for c_data in data.get(CONF_CONTROLLERS, []):
self._restore_controller(c_data)
@property
def is_on(self) -> list:
"""Return the list of objects left in on state"""
return self._is_on
def _add_to_on_list(
self,
controller: IUController,
zone: IUZone = None,
sequence: IUSequence = None,
sequence_zone: IUSequenceZone = None,
) -> None:
# pylint: disable=too-many-arguments
if sequence_zone is None:
if sequence is not None:
for item in self._is_on:
if item[CONF_SEQUENCE] == sequence:
return
elif zone is not None:
for item in self._is_on:
if item[CONF_ZONE] == zone:
return
elif controller is not None:
for item in self._is_on:
if item[CONF_CONTROLLER] == controller:
return
self._is_on.append(
{
CONF_CONTROLLER: controller,
CONF_ZONE: zone,
CONF_SEQUENCE: sequence,
CONF_SEQUENCE_ZONE: sequence_zone,
}
)
return
def _check_is_on(
self,
data: dict,
controller: IUController,
zone: IUZone,
sequence: IUSequence,
sequence_zone: IUSequenceZone,
) -> None:
# pylint: disable=too-many-arguments
if not CONF_STATE in data:
return
if data.get(CONF_STATE) == STATE_ON:
if sequence_zone is not None:
items = data.get(CONF_ZONES)
if isinstance(items, str):
items = items.split(",") # Old style 1's based CSV
for item in items:
try:
item = int(item)
zne = controller.get_zone(item - sequence_zone.ZONE_OFFSET)
if zne is not None:
self._add_to_on_list(
controller, zne, sequence, sequence_zone
)
except ValueError:
pass
else:
self._add_to_on_list(controller, zone, sequence, sequence_zone)
def _restore_enabled(
self,
data: dict,
controller: IUController,
zone: IUZone,
sequence: IUSequence,
sequence_zone: IUSequenceZone,
) -> None:
# pylint: disable=too-many-arguments
if not CONF_ENABLED in data:
return
svc = SERVICE_ENABLE if data.get(CONF_ENABLED) else SERVICE_DISABLE
svd = {}
if sequence is not None:
svd[CONF_SEQUENCE_ID] = sequence.index + 1
if sequence_zone is not None:
svd[CONF_ZONES] = [sequence_zone.index + 1]
self._coordinator.service_call(svc, controller, zone, None, svd)
def _restore_suspend(
self,
data: dict,
controller: IUController,
zone: IUZone,
sequence: IUSequence,
sequence_zone: IUSequenceZone,
) -> None:
# pylint: disable=too-many-arguments
if not ATTR_SUSPENDED in data:
return
svd = {}
if data.get(ATTR_SUSPENDED) is not None:
svd[CONF_UNTIL] = dt.parse_datetime(data.get(ATTR_SUSPENDED))
else:
svd[CONF_RESET] = None
if sequence is not None:
svd[CONF_SEQUENCE_ID] = sequence.index + 1
if sequence_zone is not None:
svd[CONF_ZONES] = [sequence_zone.index + 1]
self._coordinator.service_call(SERVICE_SUSPEND, controller, zone, None, svd)
def _restore_adjustment(
self,
data: dict,
controller: IUController,
zone: IUZone,
sequence: IUSequence,
sequence_zone: IUSequenceZone,
) -> None:
# pylint: disable=too-many-arguments
if not ATTR_ADJUSTMENT in data:
return
if (svd := IUAdjustment(data.get(ATTR_ADJUSTMENT)).to_dict()) == {}:
svd[CONF_RESET] = None
if sequence is not None:
svd[CONF_SEQUENCE_ID] = sequence.index + 1
if sequence_zone is not None:
svd[CONF_ZONES] = [sequence_zone.index + 1]
self._coordinator.service_call(SERVICE_TIME_ADJUST, controller, zone, None, svd)
def _restore_sequence_zone(
self, data: dict, controller: IUController, sequence: IUSequence
) -> None:
if (sequence_zone := sequence.get_zone(data.get(CONF_INDEX))) is None:
return
self._restore_enabled(data, controller, None, sequence, sequence_zone)
self._restore_suspend(data, controller, None, sequence, sequence_zone)
self._restore_adjustment(data, controller, None, sequence, sequence_zone)
self._check_is_on(data, controller, None, sequence, sequence_zone)
def _restore_sequence(self, data: dict, controller: IUController) -> None:
if (sequence := controller.get_sequence(data.get(CONF_INDEX))) is None:
return
self._restore_enabled(data, controller, None, sequence, None)
self._restore_suspend(data, controller, None, sequence, None)
self._restore_adjustment(data, controller, None, sequence, None)
for sz_data in data.get(CONF_SEQUENCE_ZONES, []):
self._restore_sequence_zone(sz_data, controller, sequence)
self._check_is_on(data, controller, None, sequence, None)
def _restore_zone(self, data: dict, controller: IUController) -> None:
if (zone := controller.get_zone(data.get(CONF_INDEX))) is None:
return
self._restore_enabled(data, controller, zone, None, None)
self._restore_suspend(data, controller, zone, None, None)
self._restore_adjustment(data, controller, zone, None, None)
self._check_is_on(data, controller, zone, None, None)
def _restore_controller(self, data: dict) -> None:
if (controller := self._coordinator.get(data.get(CONF_INDEX))) is None:
return
self._restore_enabled(data, controller, None, None, None)
self._restore_suspend(data, controller, None, None, None)
for sq_data in data.get(CONF_SEQUENCES, []):
self._restore_sequence(sq_data, controller)
for z_data in data.get(CONF_ZONES, []):
self._restore_zone(z_data, controller)
self._check_is_on(data, controller, None, None, None)
def report_is_on(self) -> Iterator[str]:
"""Generate a list of incomplete cycles"""
for item in self._is_on:
yield ",".join(
IUBase.idl(
[
item[CONF_CONTROLLER],
item[CONF_ZONE],
item[CONF_SEQUENCE],
item[CONF_SEQUENCE_ZONE],
],
"-",
)
)
class IUEntity(BinarySensorEntity, RestoreEntity):
"""Base class for entities"""
def __init__(
self,
coordinator: IUCoordinator,
controller: IUController,
zone: IUZone,
sequence: IUSequence,
):
"""Base entity class"""
self._coordinator = coordinator
self._controller = controller
self._zone = zone # This will be None if it belongs to a Master/Controller
self._sequence = sequence
if self._sequence is not None:
self.entity_id = self._sequence.entity_id
elif self._zone is not None:
self.entity_id = self._zone.entity_id
else:
self.entity_id = self._controller.entity_id
async def async_added_to_hass(self):
self._coordinator.register_entity(
self._controller, self._zone, self._sequence, self
)
# This code should be removed in future update. Moved to coordinator JSON configuration.
if not self._coordinator.restored_from_configuration:
state = await self.async_get_last_state()
if state is None:
return
service = (
SERVICE_ENABLE
if state.attributes.get(ATTR_ENABLED, True)
else SERVICE_DISABLE
)
self._coordinator.service_call(
service, self._controller, self._zone, None, {}
)
return
async def async_will_remove_from_hass(self):
self._coordinator.deregister_entity(
self._controller, self._zone, self._sequence, self
)
return
def dispatch(self, service: str, call: ServiceCall) -> None:
"""Dispatcher for service calls"""
self._coordinator.service_call(
service, self._controller, self._zone, self._sequence, call.data
)
class IUComponent(RestoreEntity):
"""Representation of IrrigationUnlimitedCoordinator"""
def __init__(self, coordinator: IUCoordinator):
self._coordinator = coordinator
self.entity_id = self._coordinator.entity_id
async def async_added_to_hass(self):
self._coordinator.register_entity(None, None, None, self)
state = await self.async_get_last_state()
if state is None or ATTR_CONFIGURATION not in state.attributes:
return
json_data = state.attributes.get(ATTR_CONFIGURATION, {})
try:
data = json.loads(json_data)
for item in IURestore(data, self._coordinator).is_on:
controller: IUController = item[CONF_CONTROLLER]
zone: IUZone = item[CONF_ZONE]
sequence: IUSequence = item[CONF_SEQUENCE]
sequence_zone: IUSequenceZone = item[CONF_SEQUENCE_ZONE]
self._coordinator.logger.log_incomplete_cycle(
controller,
zone,
sequence,
sequence_zone,
)
self._coordinator.restored_from_configuration = True
# pylint: disable=broad-except
except Exception as exc:
self._coordinator.logger.log_invalid_restore_data(repr(exc), json_data)
return
async def async_will_remove_from_hass(self):
self._coordinator.deregister_entity(None, None, None, self)
return
def dispatch(self, service: str, call: ServiceCall) -> None:
"""Service call dispatcher"""
self._coordinator.service_call(service, None, None, None, call.data)
@property
def should_poll(self):
"""If entity should be polled"""
return False
@property
def unique_id(self):
"""Return a unique ID."""
return COORDINATOR
@property
def name(self):
"""Return the name of the integration."""
return "Irrigation Unlimited Coordinator"
@property
def state(self):
"""Return the state of the entity."""
if not self._coordinator.initialised:
return STATUS_INITIALISING
return STATE_OK
@property
def icon(self):
"""Return the icon to be used for this entity"""
return ICON
@property
def state_attributes(self):
"""Return the state attributes."""
attr = {}
attr[ATTR_CONTROLLER_COUNT] = len(self._coordinator.controllers)
attr[ATTR_CONFIGURATION] = self._coordinator.configuration
if self._coordinator.clock.show_log:
next_tick = self._coordinator.clock.next_tick
attr[ATTR_NEXT_TICK] = (
dt.as_local(next_tick) if next_tick is not None else None
)
attr[ATTR_TICK_LOG] = list(
dt.as_local(tick) for tick in self._coordinator.clock.tick_log
)
return attr