Kevin Worrel 092580a3ed
Bump screenlogicpy to v0.9.0 (#92475)
Co-authored-by: J. Nick Koston <nick@koston.org>
2023-09-09 17:39:54 -05:00

305 lines
9.1 KiB
Python

"""Support for configurable supported data values for the ScreenLogic integration."""
from collections.abc import Callable, Generator
from dataclasses import dataclass
from enum import StrEnum
from typing import Any
from screenlogicpy import ScreenLogicGateway
from screenlogicpy.const.data import ATTR, DEVICE, VALUE
from screenlogicpy.const.msg import CODE
from screenlogicpy.device_const.system import EQUIPMENT_FLAG
from homeassistant.const import EntityCategory
from .const import SL_UNIT_TO_HA_UNIT, ScreenLogicDataPath
class PathPart(StrEnum):
"""Placeholders for local data_path values."""
DEVICE = "!device"
KEY = "!key"
INDEX = "!index"
VALUE = "!sensor"
ScreenLogicDataPathTemplate = tuple[PathPart | str | int, ...]
class ScreenLogicRule:
"""Represents a base default passing rule."""
def __init__(
self, test: Callable[..., bool] = lambda gateway, data_path: True
) -> None:
"""Initialize a ScreenLogic rule."""
self._test = test
def test(self, gateway: ScreenLogicGateway, data_path: ScreenLogicDataPath) -> bool:
"""Method to check the rule."""
return self._test(gateway, data_path)
class ScreenLogicDataRule(ScreenLogicRule):
"""Represents a data rule."""
def __init__(
self, test: Callable[..., bool], test_path_template: tuple[PathPart, ...]
) -> None:
"""Initialize a ScreenLogic data rule."""
self._test_path_template = test_path_template
super().__init__(test)
def test(self, gateway: ScreenLogicGateway, data_path: ScreenLogicDataPath) -> bool:
"""Check the rule against the gateway's data."""
test_path = realize_path_template(self._test_path_template, data_path)
return self._test(gateway.get_data(*test_path))
class ScreenLogicEquipmentRule(ScreenLogicRule):
"""Represents an equipment flag rule."""
def test(self, gateway: ScreenLogicGateway, data_path: ScreenLogicDataPath) -> bool:
"""Check the rule against the gateway's equipment flags."""
return self._test(gateway.equipment_flags)
@dataclass
class SupportedValueParameters:
"""Base supported values for ScreenLogic Entities."""
enabled: ScreenLogicRule = ScreenLogicRule()
included: ScreenLogicRule = ScreenLogicRule()
subscription_code: int | None = None
entity_category: EntityCategory | None = EntityCategory.DIAGNOSTIC
SupportedValueDescriptions = dict[str, SupportedValueParameters]
SupportedGroupDescriptions = dict[int | str, SupportedValueDescriptions]
SupportedDeviceDescriptions = dict[str, SupportedGroupDescriptions]
DEVICE_INCLUSION_RULES = {
DEVICE.PUMP: ScreenLogicDataRule(
lambda pump_data: pump_data[VALUE.DATA] != 0,
(PathPart.DEVICE, PathPart.INDEX),
),
DEVICE.INTELLICHEM: ScreenLogicEquipmentRule(
lambda flags: EQUIPMENT_FLAG.INTELLICHEM in flags,
),
DEVICE.SCG: ScreenLogicEquipmentRule(
lambda flags: EQUIPMENT_FLAG.CHLORINATOR in flags,
),
}
DEVICE_SUBSCRIPTION = {
DEVICE.CONTROLLER: CODE.STATUS_CHANGED,
DEVICE.INTELLICHEM: CODE.CHEMISTRY_CHANGED,
}
# not run-time
def get_ha_unit(entity_data: dict) -> StrEnum | str | None:
"""Return a Home Assistant unit of measurement from a UNIT."""
sl_unit = entity_data.get(ATTR.UNIT)
return SL_UNIT_TO_HA_UNIT.get(sl_unit, sl_unit)
# partial run-time
def realize_path_template(
template_path: ScreenLogicDataPathTemplate, data_path: ScreenLogicDataPath
) -> ScreenLogicDataPath:
"""Create a new data path using a template and an existing data path.
Construct new ScreenLogicDataPath from data_path using
template_path to specify values from data_path.
"""
if not data_path or len(data_path) < 3:
raise KeyError(
f"Missing or invalid required parameter: 'data_path' for template path '{template_path}'"
)
device, group, data_key = data_path
realized_path: list[str | int] = []
for part in template_path:
match part:
case PathPart.DEVICE:
realized_path.append(device)
case PathPart.INDEX | PathPart.KEY:
realized_path.append(group)
case PathPart.VALUE:
realized_path.append(data_key)
case _:
realized_path.append(part)
return tuple(realized_path)
def preprocess_supported_values(
supported_devices: SupportedDeviceDescriptions,
) -> list[tuple[ScreenLogicDataPath, Any]]:
"""Expand config dict into list of ScreenLogicDataPaths and settings."""
processed: list[tuple[ScreenLogicDataPath, Any]] = []
for device, device_groups in supported_devices.items():
for group, group_values in device_groups.items():
for value_key, value_params in group_values.items():
value_data_path = (device, group, value_key)
processed.append((value_data_path, value_params))
return processed
def iterate_expand_group_wildcard(
gateway: ScreenLogicGateway,
preprocessed_data: list[tuple[ScreenLogicDataPath, Any]],
) -> Generator[tuple[ScreenLogicDataPath, Any], None, None]:
"""Iterate and expand any group wildcards to all available entries in gateway."""
for data_path, value_params in preprocessed_data:
device, group, value_key = data_path
if group == "*":
for index in gateway.get_data(device):
yield ((device, index, value_key), value_params)
else:
yield (data_path, value_params)
def build_base_entity_description(
gateway: ScreenLogicGateway,
entity_key: str,
data_path: ScreenLogicDataPath,
value_data: dict,
value_params: SupportedValueParameters,
) -> dict:
"""Build base entity description.
Returns a dict of entity description key value pairs common to all entities.
"""
return {
"data_path": data_path,
"key": entity_key,
"entity_category": value_params.entity_category,
"entity_registry_enabled_default": value_params.enabled.test(
gateway, data_path
),
"name": value_data.get(ATTR.NAME),
}
ENTITY_MIGRATIONS = {
"chem_alarm": {
"new_key": VALUE.ACTIVE_ALERT,
"old_name": "Chemistry Alarm",
"new_name": "Active Alert",
},
"chem_calcium_harness": {
"new_key": VALUE.CALCIUM_HARNESS,
},
"chem_current_orp": {
"new_key": VALUE.ORP_NOW,
"old_name": "Current ORP",
"new_name": "ORP Now",
},
"chem_current_ph": {
"new_key": VALUE.PH_NOW,
"old_name": "Current pH",
"new_name": "pH Now",
},
"chem_cya": {
"new_key": VALUE.CYA,
},
"chem_orp_dosing_state": {
"new_key": VALUE.ORP_DOSING_STATE,
},
"chem_orp_last_dose_time": {
"new_key": VALUE.ORP_LAST_DOSE_TIME,
},
"chem_orp_last_dose_volume": {
"new_key": VALUE.ORP_LAST_DOSE_VOLUME,
},
"chem_orp_setpoint": {
"new_key": VALUE.ORP_SETPOINT,
},
"chem_orp_supply_level": {
"new_key": VALUE.ORP_SUPPLY_LEVEL,
},
"chem_ph_dosing_state": {
"new_key": VALUE.PH_DOSING_STATE,
},
"chem_ph_last_dose_time": {
"new_key": VALUE.PH_LAST_DOSE_TIME,
},
"chem_ph_last_dose_volume": {
"new_key": VALUE.PH_LAST_DOSE_VOLUME,
},
"chem_ph_probe_water_temp": {
"new_key": VALUE.PH_PROBE_WATER_TEMP,
},
"chem_ph_setpoint": {
"new_key": VALUE.PH_SETPOINT,
},
"chem_ph_supply_level": {
"new_key": VALUE.PH_SUPPLY_LEVEL,
},
"chem_salt_tds_ppm": {
"new_key": VALUE.SALT_TDS_PPM,
},
"chem_total_alkalinity": {
"new_key": VALUE.TOTAL_ALKALINITY,
},
"currentGPM": {
"new_key": VALUE.GPM_NOW,
"old_name": "Current GPM",
"new_name": "GPM Now",
"device": DEVICE.PUMP,
},
"currentRPM": {
"new_key": VALUE.RPM_NOW,
"old_name": "Current RPM",
"new_name": "RPM Now",
"device": DEVICE.PUMP,
},
"currentWatts": {
"new_key": VALUE.WATTS_NOW,
"old_name": "Current Watts",
"new_name": "Watts Now",
"device": DEVICE.PUMP,
},
"orp_alarm": {
"new_key": VALUE.ORP_LOW_ALARM,
"old_name": "ORP Alarm",
"new_name": "ORP LOW Alarm",
},
"ph_alarm": {
"new_key": VALUE.PH_HIGH_ALARM,
"old_name": "pH Alarm",
"new_name": "pH HIGH Alarm",
},
"scg_status": {
"new_key": VALUE.STATE,
"old_name": "SCG Status",
"new_name": "Chlorinator",
"device": DEVICE.SCG,
},
"scg_level1": {
"new_key": VALUE.POOL_SETPOINT,
"old_name": "Pool SCG Level",
"new_name": "Pool Chlorinator Setpoint",
},
"scg_level2": {
"new_key": VALUE.SPA_SETPOINT,
"old_name": "Spa SCG Level",
"new_name": "Spa Chlorinator Setpoint",
},
"scg_salt_ppm": {
"new_key": VALUE.SALT_PPM,
"old_name": "SCG Salt",
"new_name": "Chlorinator Salt",
"device": DEVICE.SCG,
},
"scg_super_chlor_timer": {
"new_key": VALUE.SUPER_CHLOR_TIMER,
"old_name": "SCG Super Chlorination Timer",
"new_name": "Super Chlorination Timer",
},
}