387 lines
13 KiB
Python
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
|