diff --git a/homeassistant/components/screenlogic/__init__.py b/homeassistant/components/screenlogic/__init__.py index 298e1c1ca00..7276ec28323 100644 --- a/homeassistant/components/screenlogic/__init__.py +++ b/homeassistant/components/screenlogic/__init__.py @@ -129,9 +129,12 @@ async def _async_migrate_entries( new_key, ) continue - assert device is not None and ( - device != "pump" or (device == "pump" and source_index is not None) - ) + if device == "pump" and source_index is None: + _LOGGER.debug( + "Unable to parse 'source_index' from existing unique_id for pump entity '%s'", + source_key, + ) + continue new_unique_id = ( f"{source_mac}_{generate_unique_id(device, source_index, new_key)}" ) @@ -152,7 +155,6 @@ async def _async_migrate_entries( updates["new_unique_id"] = new_unique_id if (old_name := migrations.get("old_name")) is not None: - assert old_name new_name = migrations["new_name"] if (s_old_name := slugify(old_name)) in entry.entity_id: new_entity_id = entry.entity_id.replace(s_old_name, slugify(new_name)) diff --git a/homeassistant/components/screenlogic/binary_sensor.py b/homeassistant/components/screenlogic/binary_sensor.py index 337d308d8d9..9192458dde4 100644 --- a/homeassistant/components/screenlogic/binary_sensor.py +++ b/homeassistant/components/screenlogic/binary_sensor.py @@ -1,9 +1,12 @@ """Support for a ScreenLogic Binary Sensor.""" +from copy import copy from dataclasses import dataclass import logging -from screenlogicpy.const.common import DEVICE_TYPE, ON_OFF +from screenlogicpy.const.common import ON_OFF from screenlogicpy.const.data import ATTR, DEVICE, GROUP, VALUE +from screenlogicpy.const.msg import CODE +from screenlogicpy.device_const.system import EQUIPMENT_FLAG from homeassistant.components.binary_sensor import ( DOMAIN, @@ -12,85 +15,157 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN as SL_DOMAIN, ScreenLogicDataPath +from .const import DOMAIN as SL_DOMAIN from .coordinator import ScreenlogicDataUpdateCoordinator -from .data import ( - DEVICE_INCLUSION_RULES, - DEVICE_SUBSCRIPTION, - SupportedValueParameters, - build_base_entity_description, - iterate_expand_group_wildcard, - preprocess_supported_values, -) from .entity import ( ScreenlogicEntity, ScreenLogicEntityDescription, ScreenLogicPushEntity, ScreenLogicPushEntityDescription, ) -from .util import cleanup_excluded_entity, generate_unique_id +from .util import cleanup_excluded_entity _LOGGER = logging.getLogger(__name__) @dataclass -class SupportedBinarySensorValueParameters(SupportedValueParameters): - """Supported predefined data for a ScreenLogic binary sensor entity.""" - - device_class: BinarySensorDeviceClass | None = None +class ScreenLogicBinarySensorDescription( + BinarySensorEntityDescription, ScreenLogicEntityDescription +): + """A class that describes ScreenLogic binary sensor eneites.""" -SUPPORTED_DATA: list[ - tuple[ScreenLogicDataPath, SupportedValueParameters] -] = preprocess_supported_values( - { - DEVICE.CONTROLLER: { - GROUP.SENSOR: { - VALUE.ACTIVE_ALERT: SupportedBinarySensorValueParameters(), - VALUE.CLEANER_DELAY: SupportedBinarySensorValueParameters(), - VALUE.FREEZE_MODE: SupportedBinarySensorValueParameters(), - VALUE.POOL_DELAY: SupportedBinarySensorValueParameters(), - VALUE.SPA_DELAY: SupportedBinarySensorValueParameters(), - }, - }, - DEVICE.PUMP: { - "*": { - VALUE.STATE: SupportedBinarySensorValueParameters(), - }, - }, - DEVICE.INTELLICHEM: { - GROUP.ALARM: { - VALUE.FLOW_ALARM: SupportedBinarySensorValueParameters(), - VALUE.ORP_HIGH_ALARM: SupportedBinarySensorValueParameters(), - VALUE.ORP_LOW_ALARM: SupportedBinarySensorValueParameters(), - VALUE.ORP_SUPPLY_ALARM: SupportedBinarySensorValueParameters(), - VALUE.PH_HIGH_ALARM: SupportedBinarySensorValueParameters(), - VALUE.PH_LOW_ALARM: SupportedBinarySensorValueParameters(), - VALUE.PH_SUPPLY_ALARM: SupportedBinarySensorValueParameters(), - VALUE.PROBE_FAULT_ALARM: SupportedBinarySensorValueParameters(), - }, - GROUP.ALERT: { - VALUE.ORP_LIMIT: SupportedBinarySensorValueParameters(), - VALUE.PH_LIMIT: SupportedBinarySensorValueParameters(), - VALUE.PH_LOCKOUT: SupportedBinarySensorValueParameters(), - }, - GROUP.WATER_BALANCE: { - VALUE.CORROSIVE: SupportedBinarySensorValueParameters(), - VALUE.SCALING: SupportedBinarySensorValueParameters(), - }, - }, - DEVICE.SCG: { - GROUP.SENSOR: { - VALUE.STATE: SupportedBinarySensorValueParameters(), - }, - }, - } -) +@dataclass +class ScreenLogicPushBinarySensorDescription( + ScreenLogicBinarySensorDescription, ScreenLogicPushEntityDescription +): + """Describes a ScreenLogicPushBinarySensor.""" -SL_DEVICE_TYPE_TO_HA_DEVICE_CLASS = {DEVICE_TYPE.ALARM: BinarySensorDeviceClass.PROBLEM} + +SUPPORTED_CORE_SENSORS = [ + ScreenLogicPushBinarySensorDescription( + subscription_code=CODE.STATUS_CHANGED, + data_root=(DEVICE.CONTROLLER, GROUP.SENSOR), + key=VALUE.ACTIVE_ALERT, + device_class=BinarySensorDeviceClass.PROBLEM, + ), + ScreenLogicPushBinarySensorDescription( + subscription_code=CODE.STATUS_CHANGED, + data_root=(DEVICE.CONTROLLER, GROUP.SENSOR), + key=VALUE.CLEANER_DELAY, + ), + ScreenLogicPushBinarySensorDescription( + subscription_code=CODE.STATUS_CHANGED, + data_root=(DEVICE.CONTROLLER, GROUP.SENSOR), + key=VALUE.FREEZE_MODE, + ), + ScreenLogicPushBinarySensorDescription( + subscription_code=CODE.STATUS_CHANGED, + data_root=(DEVICE.CONTROLLER, GROUP.SENSOR), + key=VALUE.POOL_DELAY, + ), + ScreenLogicPushBinarySensorDescription( + subscription_code=CODE.STATUS_CHANGED, + data_root=(DEVICE.CONTROLLER, GROUP.SENSOR), + key=VALUE.SPA_DELAY, + ), +] + +SUPPORTED_PUMP_SENSORS = [ + ScreenLogicBinarySensorDescription( + data_root=(DEVICE.PUMP,), + key=VALUE.STATE, + ) +] + +SUPPORTED_INTELLICHEM_SENSORS = [ + ScreenLogicPushBinarySensorDescription( + subscription_code=CODE.CHEMISTRY_CHANGED, + data_root=(DEVICE.INTELLICHEM, GROUP.ALARM), + key=VALUE.FLOW_ALARM, + device_class=BinarySensorDeviceClass.PROBLEM, + ), + ScreenLogicPushBinarySensorDescription( + subscription_code=CODE.CHEMISTRY_CHANGED, + data_root=(DEVICE.INTELLICHEM, GROUP.ALARM), + key=VALUE.ORP_HIGH_ALARM, + device_class=BinarySensorDeviceClass.PROBLEM, + ), + ScreenLogicPushBinarySensorDescription( + subscription_code=CODE.CHEMISTRY_CHANGED, + data_root=(DEVICE.INTELLICHEM, GROUP.ALARM), + key=VALUE.ORP_LOW_ALARM, + device_class=BinarySensorDeviceClass.PROBLEM, + ), + ScreenLogicPushBinarySensorDescription( + subscription_code=CODE.CHEMISTRY_CHANGED, + data_root=(DEVICE.INTELLICHEM, GROUP.ALARM), + key=VALUE.ORP_SUPPLY_ALARM, + device_class=BinarySensorDeviceClass.PROBLEM, + ), + ScreenLogicPushBinarySensorDescription( + subscription_code=CODE.CHEMISTRY_CHANGED, + data_root=(DEVICE.INTELLICHEM, GROUP.ALARM), + key=VALUE.PH_HIGH_ALARM, + device_class=BinarySensorDeviceClass.PROBLEM, + ), + ScreenLogicPushBinarySensorDescription( + subscription_code=CODE.CHEMISTRY_CHANGED, + data_root=(DEVICE.INTELLICHEM, GROUP.ALARM), + key=VALUE.PH_LOW_ALARM, + device_class=BinarySensorDeviceClass.PROBLEM, + ), + ScreenLogicPushBinarySensorDescription( + subscription_code=CODE.CHEMISTRY_CHANGED, + data_root=(DEVICE.INTELLICHEM, GROUP.ALARM), + key=VALUE.PH_SUPPLY_ALARM, + device_class=BinarySensorDeviceClass.PROBLEM, + ), + ScreenLogicPushBinarySensorDescription( + subscription_code=CODE.CHEMISTRY_CHANGED, + data_root=(DEVICE.INTELLICHEM, GROUP.ALARM), + key=VALUE.PROBE_FAULT_ALARM, + device_class=BinarySensorDeviceClass.PROBLEM, + ), + ScreenLogicPushBinarySensorDescription( + subscription_code=CODE.CHEMISTRY_CHANGED, + data_root=(DEVICE.INTELLICHEM, GROUP.ALERT), + key=VALUE.ORP_LIMIT, + ), + ScreenLogicPushBinarySensorDescription( + subscription_code=CODE.CHEMISTRY_CHANGED, + data_root=(DEVICE.INTELLICHEM, GROUP.ALERT), + key=VALUE.PH_LIMIT, + ), + ScreenLogicPushBinarySensorDescription( + subscription_code=CODE.CHEMISTRY_CHANGED, + data_root=(DEVICE.INTELLICHEM, GROUP.ALERT), + key=VALUE.PH_LOCKOUT, + ), + ScreenLogicPushBinarySensorDescription( + subscription_code=CODE.CHEMISTRY_CHANGED, + data_root=(DEVICE.INTELLICHEM, GROUP.WATER_BALANCE), + key=VALUE.CORROSIVE, + device_class=BinarySensorDeviceClass.PROBLEM, + ), + ScreenLogicPushBinarySensorDescription( + subscription_code=CODE.CHEMISTRY_CHANGED, + data_root=(DEVICE.INTELLICHEM, GROUP.WATER_BALANCE), + key=VALUE.SCALING, + device_class=BinarySensorDeviceClass.PROBLEM, + ), +] + +SUPPORTED_SCG_SENSORS = [ + ScreenLogicBinarySensorDescription( + data_root=(DEVICE.SCG, GROUP.SENSOR), + key=VALUE.STATE, + ) +] async def async_setup_entry( @@ -104,72 +179,65 @@ async def async_setup_entry( config_entry.entry_id ] gateway = coordinator.gateway - data_path: ScreenLogicDataPath - value_params: SupportedBinarySensorValueParameters - for data_path, value_params in iterate_expand_group_wildcard( - gateway, SUPPORTED_DATA - ): - entity_key = generate_unique_id(*data_path) - - device = data_path[0] - - if not (DEVICE_INCLUSION_RULES.get(device) or value_params.included).test( - gateway, data_path - ): - cleanup_excluded_entity(coordinator, DOMAIN, entity_key) - continue - - try: - value_data = gateway.get_data(*data_path, strict=True) - except KeyError: - _LOGGER.debug("Failed to find %s", data_path) - continue - - entity_description_kwargs = { - **build_base_entity_description( - gateway, entity_key, data_path, value_data, value_params - ), - "device_class": SL_DEVICE_TYPE_TO_HA_DEVICE_CLASS.get( - value_data.get(ATTR.DEVICE_TYPE) - ), - } + for core_sensor_description in SUPPORTED_CORE_SENSORS: if ( - sub_code := ( - value_params.subscription_code or DEVICE_SUBSCRIPTION.get(device) + gateway.get_data( + *core_sensor_description.data_root, core_sensor_description.key ) - ) is not None: + is not None + ): entities.append( - ScreenLogicPushBinarySensor( - coordinator, - ScreenLogicPushBinarySensorDescription( - subscription_code=sub_code, **entity_description_kwargs - ), + ScreenLogicPushBinarySensor(coordinator, core_sensor_description) + ) + + for p_index, p_data in gateway.get_data(DEVICE.PUMP).items(): + if not p_data or not p_data.get(VALUE.DATA): + continue + for proto_pump_sensor_description in SUPPORTED_PUMP_SENSORS: + entities.append( + ScreenLogicPumpBinarySensor( + coordinator, copy(proto_pump_sensor_description), p_index ) ) - else: + + chem_sensor_description: ScreenLogicPushBinarySensorDescription + for chem_sensor_description in SUPPORTED_INTELLICHEM_SENSORS: + chem_sensor_data_path = ( + *chem_sensor_description.data_root, + chem_sensor_description.key, + ) + if EQUIPMENT_FLAG.INTELLICHEM not in gateway.equipment_flags: + cleanup_excluded_entity(coordinator, DOMAIN, chem_sensor_data_path) + continue + if gateway.get_data(*chem_sensor_data_path): entities.append( - ScreenLogicBinarySensor( - coordinator, - ScreenLogicBinarySensorDescription(**entity_description_kwargs), - ) + ScreenLogicPushBinarySensor(coordinator, chem_sensor_description) + ) + + scg_sensor_description: ScreenLogicBinarySensorDescription + for scg_sensor_description in SUPPORTED_SCG_SENSORS: + scg_sensor_data_path = ( + *scg_sensor_description.data_root, + scg_sensor_description.key, + ) + if EQUIPMENT_FLAG.CHLORINATOR not in gateway.equipment_flags: + cleanup_excluded_entity(coordinator, DOMAIN, scg_sensor_data_path) + continue + if gateway.get_data(*scg_sensor_data_path): + entities.append( + ScreenLogicBinarySensor(coordinator, scg_sensor_description) ) async_add_entities(entities) -@dataclass -class ScreenLogicBinarySensorDescription( - BinarySensorEntityDescription, ScreenLogicEntityDescription -): - """A class that describes ScreenLogic binary sensor eneites.""" - - class ScreenLogicBinarySensor(ScreenlogicEntity, BinarySensorEntity): - """Base class for all ScreenLogic binary sensor entities.""" + """Representation of a ScreenLogic binary sensor entity.""" entity_description: ScreenLogicBinarySensorDescription _attr_has_entity_name = True + _attr_entity_category = EntityCategory.DIAGNOSTIC @property def is_on(self) -> bool: @@ -177,14 +245,21 @@ class ScreenLogicBinarySensor(ScreenlogicEntity, BinarySensorEntity): return self.entity_data[ATTR.VALUE] == ON_OFF.ON -@dataclass -class ScreenLogicPushBinarySensorDescription( - ScreenLogicBinarySensorDescription, ScreenLogicPushEntityDescription -): - """Describes a ScreenLogicPushBinarySensor.""" - - class ScreenLogicPushBinarySensor(ScreenLogicPushEntity, ScreenLogicBinarySensor): - """Representation of a basic ScreenLogic sensor entity.""" + """Representation of a ScreenLogic push binary sensor entity.""" entity_description: ScreenLogicPushBinarySensorDescription + + +class ScreenLogicPumpBinarySensor(ScreenLogicBinarySensor): + """Representation of a ScreenLogic binary sensor entity for pump data.""" + + def __init__( + self, + coordinator: ScreenlogicDataUpdateCoordinator, + entity_description: ScreenLogicBinarySensorDescription, + pump_index: int, + ) -> None: + """Initialize of the entity.""" + entity_description.data_root = (DEVICE.PUMP, pump_index) + super().__init__(coordinator, entity_description) diff --git a/homeassistant/components/screenlogic/climate.py b/homeassistant/components/screenlogic/climate.py index 889c8617274..1d3f366a498 100644 --- a/homeassistant/components/screenlogic/climate.py +++ b/homeassistant/components/screenlogic/climate.py @@ -53,16 +53,14 @@ async def async_setup_entry( gateway = coordinator.gateway - for body_index, body_data in gateway.get_data(DEVICE.BODY).items(): - body_path = (DEVICE.BODY, body_index) + for body_index in gateway.get_data(DEVICE.BODY): entities.append( ScreenLogicClimate( coordinator, ScreenLogicClimateDescription( subscription_code=CODE.STATUS_CHANGED, - data_path=body_path, + data_root=(DEVICE.BODY,), key=body_index, - name=body_data[VALUE.HEAT_STATE][ATTR.NAME], ), ) ) @@ -99,6 +97,7 @@ class ScreenLogicClimate(ScreenLogicPushEntity, ClimateEntity, RestoreEntity): self._attr_min_temp = self.entity_data[ATTR.MIN_SETPOINT] self._attr_max_temp = self.entity_data[ATTR.MAX_SETPOINT] + self._attr_name = self.entity_data[VALUE.HEAT_STATE][ATTR.NAME] self._last_preset = None @property diff --git a/homeassistant/components/screenlogic/data.py b/homeassistant/components/screenlogic/data.py index 5679b7e4dc9..719cebc1ef6 100644 --- a/homeassistant/components/screenlogic/data.py +++ b/homeassistant/components/screenlogic/data.py @@ -1,189 +1,5 @@ """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), - } - +from screenlogicpy.const.data import DEVICE, VALUE ENTITY_MIGRATIONS = { "chem_alarm": { diff --git a/homeassistant/components/screenlogic/entity.py b/homeassistant/components/screenlogic/entity.py index a29aaa9125b..3b45aa699d3 100644 --- a/homeassistant/components/screenlogic/entity.py +++ b/homeassistant/components/screenlogic/entity.py @@ -1,4 +1,5 @@ """Base ScreenLogicEntity definitions.""" +from collections.abc import Callable from dataclasses import dataclass from datetime import datetime import logging @@ -18,15 +19,16 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ScreenLogicDataPath from .coordinator import ScreenlogicDataUpdateCoordinator +from .util import generate_unique_id _LOGGER = logging.getLogger(__name__) @dataclass class ScreenLogicEntityRequiredKeyMixin: - """Mixin for required ScreenLogic entity key.""" + """Mixin for required ScreenLogic entity data_path.""" - data_path: ScreenLogicDataPath + data_root: ScreenLogicDataPath @dataclass @@ -35,6 +37,8 @@ class ScreenLogicEntityDescription( ): """Base class for a ScreenLogic entity description.""" + enabled_lambda: Callable[..., bool] | None = None + class ScreenlogicEntity(CoordinatorEntity[ScreenlogicDataUpdateCoordinator]): """Base class for all ScreenLogic entities.""" @@ -50,10 +54,11 @@ class ScreenlogicEntity(CoordinatorEntity[ScreenlogicDataUpdateCoordinator]): """Initialize of the entity.""" super().__init__(coordinator) self.entity_description = entity_description - self._data_path = self.entity_description.data_path - self._data_key = self._data_path[-1] - self._attr_unique_id = f"{self.mac}_{self.entity_description.key}" + self._data_key = self.entity_description.key + self._data_path = (*self.entity_description.data_root, self._data_key) mac = self.mac + self._attr_unique_id = f"{mac}_{generate_unique_id(*self._data_path)}" + self._attr_name = self.entity_data[ATTR.NAME] assert mac is not None self._attr_device_info = DeviceInfo( connections={(dr.CONNECTION_NETWORK_MAC, mac)}, @@ -88,9 +93,10 @@ class ScreenlogicEntity(CoordinatorEntity[ScreenlogicDataUpdateCoordinator]): @property def entity_data(self) -> dict: """Shortcut to the data for this entity.""" - if (data := self.gateway.get_data(*self._data_path)) is None: - raise KeyError(f"Data not found: {self._data_path}") - return data + try: + return self.gateway.get_data(*self._data_path, strict=True) + except KeyError as ke: + raise HomeAssistantError(f"Data not found: {self._data_path}") from ke @dataclass @@ -120,6 +126,7 @@ class ScreenLogicPushEntity(ScreenlogicEntity): ) -> None: """Initialize of the entity.""" super().__init__(coordinator, entity_description) + self._subscription_code = entity_description.subscription_code self._last_update_success = True @callback @@ -134,7 +141,7 @@ class ScreenLogicPushEntity(ScreenlogicEntity): self.async_on_remove( await self.gateway.async_subscribe_client( self._async_data_updated, - self.entity_description.subscription_code, + self._subscription_code, ) ) diff --git a/homeassistant/components/screenlogic/light.py b/homeassistant/components/screenlogic/light.py index 3875e34fbaa..80499f7790a 100644 --- a/homeassistant/components/screenlogic/light.py +++ b/homeassistant/components/screenlogic/light.py @@ -34,7 +34,11 @@ async def async_setup_entry( ] gateway = coordinator.gateway for circuit_index, circuit_data in gateway.get_data(DEVICE.CIRCUIT).items(): - if circuit_data[ATTR.FUNCTION] not in LIGHT_CIRCUIT_FUNCTIONS: + if ( + not circuit_data + or ((circuit_function := circuit_data.get(ATTR.FUNCTION)) is None) + or circuit_function not in LIGHT_CIRCUIT_FUNCTIONS + ): continue circuit_name = circuit_data[ATTR.NAME] circuit_interface = INTERFACE(circuit_data[ATTR.INTERFACE]) @@ -43,9 +47,8 @@ async def async_setup_entry( coordinator, ScreenLogicLightDescription( subscription_code=CODE.STATUS_CHANGED, - data_path=(DEVICE.CIRCUIT, circuit_index), + data_root=(DEVICE.CIRCUIT,), key=circuit_index, - name=circuit_name, entity_registry_enabled_default=( circuit_name not in GENERIC_CIRCUIT_NAMES and circuit_interface != INTERFACE.DONT_SHOW diff --git a/homeassistant/components/screenlogic/number.py b/homeassistant/components/screenlogic/number.py index 22805ffc3c1..d3ed25f5570 100644 --- a/homeassistant/components/screenlogic/number.py +++ b/homeassistant/components/screenlogic/number.py @@ -4,10 +4,10 @@ from dataclasses import dataclass import logging from screenlogicpy.const.data import ATTR, DEVICE, GROUP, VALUE +from screenlogicpy.device_const.system import EQUIPMENT_FLAG from homeassistant.components.number import ( DOMAIN, - NumberDeviceClass, NumberEntity, NumberEntityDescription, ) @@ -16,20 +16,10 @@ from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN as SL_DOMAIN, ScreenLogicDataPath +from .const import DOMAIN as SL_DOMAIN from .coordinator import ScreenlogicDataUpdateCoordinator -from .data import ( - DEVICE_INCLUSION_RULES, - PathPart, - SupportedValueParameters, - build_base_entity_description, - get_ha_unit, - iterate_expand_group_wildcard, - preprocess_supported_values, - realize_path_template, -) from .entity import ScreenlogicEntity, ScreenLogicEntityDescription -from .util import cleanup_excluded_entity, generate_unique_id +from .util import cleanup_excluded_entity, get_ha_unit _LOGGER = logging.getLogger(__name__) @@ -37,47 +27,44 @@ PARALLEL_UPDATES = 1 @dataclass -class SupportedNumberValueParametersMixin: - """Mixin for supported predefined data for a ScreenLogic number entity.""" +class ScreenLogicNumberRequiredMixin: + """Describes a required mixin for a ScreenLogic number entity.""" - set_value_config: tuple[str, tuple[tuple[PathPart | str | int, ...], ...]] - device_class: NumberDeviceClass | None = None + set_value_name: str + set_value_args: tuple[tuple[str | int, ...], ...] @dataclass -class SupportedNumberValueParameters( - SupportedValueParameters, SupportedNumberValueParametersMixin +class ScreenLogicNumberDescription( + NumberEntityDescription, + ScreenLogicEntityDescription, + ScreenLogicNumberRequiredMixin, ): - """Supported predefined data for a ScreenLogic number entity.""" + """Describes a ScreenLogic number entity.""" -SET_SCG_CONFIG_FUNC_DATA = ( - "async_set_scg_config", - ( - (DEVICE.SCG, GROUP.CONFIGURATION, VALUE.POOL_SETPOINT), - (DEVICE.SCG, GROUP.CONFIGURATION, VALUE.SPA_SETPOINT), +SUPPORTED_SCG_NUMBERS = [ + ScreenLogicNumberDescription( + set_value_name="async_set_scg_config", + set_value_args=( + (DEVICE.SCG, GROUP.CONFIGURATION, VALUE.POOL_SETPOINT), + (DEVICE.SCG, GROUP.CONFIGURATION, VALUE.SPA_SETPOINT), + ), + data_root=(DEVICE.SCG, GROUP.CONFIGURATION), + key=VALUE.POOL_SETPOINT, + entity_category=EntityCategory.CONFIG, ), -) - - -SUPPORTED_DATA: list[ - tuple[ScreenLogicDataPath, SupportedValueParameters] -] = preprocess_supported_values( - { - DEVICE.SCG: { - GROUP.CONFIGURATION: { - VALUE.POOL_SETPOINT: SupportedNumberValueParameters( - entity_category=EntityCategory.CONFIG, - set_value_config=SET_SCG_CONFIG_FUNC_DATA, - ), - VALUE.SPA_SETPOINT: SupportedNumberValueParameters( - entity_category=EntityCategory.CONFIG, - set_value_config=SET_SCG_CONFIG_FUNC_DATA, - ), - } - } - } -) + ScreenLogicNumberDescription( + set_value_name="async_set_scg_config", + set_value_args=( + (DEVICE.SCG, GROUP.CONFIGURATION, VALUE.POOL_SETPOINT), + (DEVICE.SCG, GROUP.CONFIGURATION, VALUE.SPA_SETPOINT), + ), + data_root=(DEVICE.SCG, GROUP.CONFIGURATION), + key=VALUE.SPA_SETPOINT, + entity_category=EntityCategory.CONFIG, + ), +] async def async_setup_entry( @@ -91,70 +78,21 @@ async def async_setup_entry( config_entry.entry_id ] gateway = coordinator.gateway - data_path: ScreenLogicDataPath - value_params: SupportedNumberValueParameters - for data_path, value_params in iterate_expand_group_wildcard( - gateway, SUPPORTED_DATA - ): - entity_key = generate_unique_id(*data_path) - device = data_path[0] - - if not (DEVICE_INCLUSION_RULES.get(device) or value_params.included).test( - gateway, data_path - ): - cleanup_excluded_entity(coordinator, DOMAIN, entity_key) - continue - - try: - value_data = gateway.get_data(*data_path, strict=True) - except KeyError: - _LOGGER.debug("Failed to find %s", data_path) - continue - - set_value_str, set_value_params = value_params.set_value_config - set_value_func = getattr(gateway, set_value_str) - - entity_description_kwargs = { - **build_base_entity_description( - gateway, entity_key, data_path, value_data, value_params - ), - "device_class": value_params.device_class, - "native_unit_of_measurement": get_ha_unit(value_data), - "native_max_value": value_data.get(ATTR.MAX_SETPOINT), - "native_min_value": value_data.get(ATTR.MIN_SETPOINT), - "native_step": value_data.get(ATTR.STEP), - "set_value": set_value_func, - "set_value_params": set_value_params, - } - - entities.append( - ScreenLogicNumber( - coordinator, - ScreenLogicNumberDescription(**entity_description_kwargs), - ) + for scg_number_description in SUPPORTED_SCG_NUMBERS: + scg_number_data_path = ( + *scg_number_description.data_root, + scg_number_description.key, ) + if EQUIPMENT_FLAG.CHLORINATOR not in gateway.equipment_flags: + cleanup_excluded_entity(coordinator, DOMAIN, scg_number_data_path) + continue + if gateway.get_data(*scg_number_data_path): + entities.append(ScreenLogicNumber(coordinator, scg_number_description)) async_add_entities(entities) -@dataclass -class ScreenLogicNumberRequiredMixin: - """Describes a required mixin for a ScreenLogic number entity.""" - - set_value: Callable[..., bool] - set_value_params: tuple[tuple[str | int, ...], ...] - - -@dataclass -class ScreenLogicNumberDescription( - NumberEntityDescription, - ScreenLogicEntityDescription, - ScreenLogicNumberRequiredMixin, -): - """Describes a ScreenLogic number entity.""" - - class ScreenLogicNumber(ScreenlogicEntity, NumberEntity): """Class to represent a ScreenLogic Number entity.""" @@ -166,9 +104,30 @@ class ScreenLogicNumber(ScreenlogicEntity, NumberEntity): entity_description: ScreenLogicNumberDescription, ) -> None: """Initialize a ScreenLogic number entity.""" - self._set_value_func = entity_description.set_value - self._set_value_params = entity_description.set_value_params super().__init__(coordinator, entity_description) + if not callable( + func := getattr(self.gateway, entity_description.set_value_name) + ): + raise TypeError( + f"set_value_name '{entity_description.set_value_name}' is not a callable" + ) + self._set_value_func: Callable[..., bool] = func + self._set_value_args = entity_description.set_value_args + self._attr_native_unit_of_measurement = get_ha_unit( + self.entity_data.get(ATTR.UNIT) + ) + if entity_description.native_max_value is None and isinstance( + max_val := self.entity_data.get(ATTR.MAX_SETPOINT), int | float + ): + self._attr_native_max_value = max_val + if entity_description.native_min_value is None and isinstance( + min_val := self.entity_data.get(ATTR.MIN_SETPOINT), int | float + ): + self._attr_native_min_value = min_val + if entity_description.native_step is None and isinstance( + step := self.entity_data.get(ATTR.STEP), int | float + ): + self._attr_native_step = step @property def native_value(self) -> float: @@ -182,12 +141,9 @@ class ScreenLogicNumber(ScreenlogicEntity, NumberEntity): # gathers the existing values and updates the particular value being # set by this entity. args = {} - for data_path in self._set_value_params: - data_path = realize_path_template(data_path, self._data_path) - data_value = data_path[-1] - args[data_value] = self.coordinator.gateway.get_value( - *data_path, strict=True - ) + for data_path in self._set_value_args: + data_key = data_path[-1] + args[data_key] = self.coordinator.gateway.get_value(*data_path, strict=True) args[self._data_key] = value diff --git a/homeassistant/components/screenlogic/sensor.py b/homeassistant/components/screenlogic/sensor.py index 39805173961..bbcf8458014 100644 --- a/homeassistant/components/screenlogic/sensor.py +++ b/homeassistant/components/screenlogic/sensor.py @@ -1,10 +1,11 @@ """Support for a ScreenLogic Sensor.""" from collections.abc import Callable +from copy import copy from dataclasses import dataclass import logging -from screenlogicpy.const.common import DEVICE_TYPE, STATE_TYPE from screenlogicpy.const.data import ATTR, DEVICE, GROUP, VALUE +from screenlogicpy.const.msg import CODE from screenlogicpy.device_const.chemistry import DOSE_STATE from screenlogicpy.device_const.pump import PUMP_TYPE from screenlogicpy.device_const.system import EQUIPMENT_FLAG @@ -17,221 +18,23 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN as SL_DOMAIN, ScreenLogicDataPath +from .const import DOMAIN as SL_DOMAIN from .coordinator import ScreenlogicDataUpdateCoordinator -from .data import ( - DEVICE_INCLUSION_RULES, - DEVICE_SUBSCRIPTION, - PathPart, - ScreenLogicDataRule, - ScreenLogicEquipmentRule, - SupportedValueParameters, - build_base_entity_description, - get_ha_unit, - iterate_expand_group_wildcard, - preprocess_supported_values, -) from .entity import ( ScreenlogicEntity, ScreenLogicEntityDescription, ScreenLogicPushEntity, ScreenLogicPushEntityDescription, ) -from .util import cleanup_excluded_entity, generate_unique_id +from .util import cleanup_excluded_entity, get_ha_unit _LOGGER = logging.getLogger(__name__) -@dataclass -class SupportedSensorValueParameters(SupportedValueParameters): - """Supported predefined data for a ScreenLogic sensor entity.""" - - device_class: SensorDeviceClass | None = None - value_modification: Callable[[int], int | str] | None = lambda val: val - - -SUPPORTED_DATA: list[ - tuple[ScreenLogicDataPath, SupportedValueParameters] -] = preprocess_supported_values( - { - DEVICE.CONTROLLER: { - GROUP.SENSOR: { - VALUE.AIR_TEMPERATURE: SupportedSensorValueParameters( - device_class=SensorDeviceClass.TEMPERATURE, entity_category=None - ), - VALUE.ORP: SupportedSensorValueParameters( - included=ScreenLogicEquipmentRule( - lambda flags: EQUIPMENT_FLAG.INTELLICHEM in flags - ) - ), - VALUE.PH: SupportedSensorValueParameters( - included=ScreenLogicEquipmentRule( - lambda flags: EQUIPMENT_FLAG.INTELLICHEM in flags - ) - ), - }, - }, - DEVICE.PUMP: { - "*": { - VALUE.WATTS_NOW: SupportedSensorValueParameters(), - VALUE.GPM_NOW: SupportedSensorValueParameters( - enabled=ScreenLogicDataRule( - lambda pump_data: pump_data[VALUE.TYPE] - != PUMP_TYPE.INTELLIFLO_VS, - (PathPart.DEVICE, PathPart.INDEX), - ) - ), - VALUE.RPM_NOW: SupportedSensorValueParameters( - enabled=ScreenLogicDataRule( - lambda pump_data: pump_data[VALUE.TYPE] - != PUMP_TYPE.INTELLIFLO_VF, - (PathPart.DEVICE, PathPart.INDEX), - ) - ), - }, - }, - DEVICE.INTELLICHEM: { - GROUP.SENSOR: { - VALUE.ORP_NOW: SupportedSensorValueParameters(), - VALUE.ORP_SUPPLY_LEVEL: SupportedSensorValueParameters( - value_modification=lambda val: val - 1 - ), - VALUE.PH_NOW: SupportedSensorValueParameters(), - VALUE.PH_PROBE_WATER_TEMP: SupportedSensorValueParameters(), - VALUE.PH_SUPPLY_LEVEL: SupportedSensorValueParameters( - value_modification=lambda val: val - 1 - ), - VALUE.SATURATION: SupportedSensorValueParameters(), - }, - GROUP.CONFIGURATION: { - VALUE.CALCIUM_HARNESS: SupportedSensorValueParameters(), - VALUE.CYA: SupportedSensorValueParameters(), - VALUE.ORP_SETPOINT: SupportedSensorValueParameters(), - VALUE.PH_SETPOINT: SupportedSensorValueParameters(), - VALUE.SALT_TDS_PPM: SupportedSensorValueParameters( - included=ScreenLogicEquipmentRule( - lambda flags: EQUIPMENT_FLAG.INTELLICHEM in flags - and EQUIPMENT_FLAG.CHLORINATOR not in flags, - ) - ), - VALUE.TOTAL_ALKALINITY: SupportedSensorValueParameters(), - }, - GROUP.DOSE_STATUS: { - VALUE.ORP_DOSING_STATE: SupportedSensorValueParameters( - value_modification=lambda val: DOSE_STATE(val).title, - ), - VALUE.ORP_LAST_DOSE_TIME: SupportedSensorValueParameters(), - VALUE.ORP_LAST_DOSE_VOLUME: SupportedSensorValueParameters(), - VALUE.PH_DOSING_STATE: SupportedSensorValueParameters( - value_modification=lambda val: DOSE_STATE(val).title, - ), - VALUE.PH_LAST_DOSE_TIME: SupportedSensorValueParameters(), - VALUE.PH_LAST_DOSE_VOLUME: SupportedSensorValueParameters(), - }, - }, - DEVICE.SCG: { - GROUP.SENSOR: { - VALUE.SALT_PPM: SupportedSensorValueParameters(), - }, - GROUP.CONFIGURATION: { - VALUE.SUPER_CHLOR_TIMER: SupportedSensorValueParameters(), - }, - }, - } -) - -SL_DEVICE_TYPE_TO_HA_DEVICE_CLASS = { - DEVICE_TYPE.DURATION: SensorDeviceClass.DURATION, - DEVICE_TYPE.ENUM: SensorDeviceClass.ENUM, - DEVICE_TYPE.ENERGY: SensorDeviceClass.POWER, - DEVICE_TYPE.POWER: SensorDeviceClass.POWER, - DEVICE_TYPE.TEMPERATURE: SensorDeviceClass.TEMPERATURE, - DEVICE_TYPE.VOLUME: SensorDeviceClass.VOLUME, -} - -SL_STATE_TYPE_TO_HA_STATE_CLASS = { - STATE_TYPE.MEASUREMENT: SensorStateClass.MEASUREMENT, - STATE_TYPE.TOTAL_INCREASING: SensorStateClass.TOTAL_INCREASING, -} - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up entry.""" - entities: list[ScreenLogicSensor] = [] - coordinator: ScreenlogicDataUpdateCoordinator = hass.data[SL_DOMAIN][ - config_entry.entry_id - ] - gateway = coordinator.gateway - data_path: ScreenLogicDataPath - value_params: SupportedSensorValueParameters - for data_path, value_params in iterate_expand_group_wildcard( - gateway, SUPPORTED_DATA - ): - entity_key = generate_unique_id(*data_path) - - device = data_path[0] - - if not (DEVICE_INCLUSION_RULES.get(device) or value_params.included).test( - gateway, data_path - ): - cleanup_excluded_entity(coordinator, DOMAIN, entity_key) - continue - - try: - value_data = gateway.get_data(*data_path, strict=True) - except KeyError: - _LOGGER.debug("Failed to find %s", data_path) - continue - - entity_description_kwargs = { - **build_base_entity_description( - gateway, entity_key, data_path, value_data, value_params - ), - "device_class": SL_DEVICE_TYPE_TO_HA_DEVICE_CLASS.get( - value_data.get(ATTR.DEVICE_TYPE) - ), - "native_unit_of_measurement": get_ha_unit(value_data), - "options": value_data.get(ATTR.ENUM_OPTIONS), - "state_class": SL_STATE_TYPE_TO_HA_STATE_CLASS.get( - value_data.get(ATTR.STATE_TYPE) - ), - "value_mod": value_params.value_modification, - } - - if ( - sub_code := ( - value_params.subscription_code or DEVICE_SUBSCRIPTION.get(device) - ) - ) is not None: - entities.append( - ScreenLogicPushSensor( - coordinator, - ScreenLogicPushSensorDescription( - subscription_code=sub_code, - **entity_description_kwargs, - ), - ) - ) - else: - entities.append( - ScreenLogicSensor( - coordinator, - ScreenLogicSensorDescription( - **entity_description_kwargs, - ), - ) - ) - - async_add_entities(entities) - - @dataclass class ScreenLogicSensorMixin: """Mixin for SecreenLogic sensor entity.""" @@ -246,12 +49,265 @@ class ScreenLogicSensorDescription( """Describes a ScreenLogic sensor.""" +@dataclass +class ScreenLogicPushSensorDescription( + ScreenLogicSensorDescription, ScreenLogicPushEntityDescription +): + """Describes a ScreenLogic push sensor.""" + + +SUPPORTED_CORE_SENSORS = [ + ScreenLogicPushSensorDescription( + subscription_code=CODE.STATUS_CHANGED, + data_root=(DEVICE.CONTROLLER, GROUP.SENSOR), + key=VALUE.AIR_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), +] + +SUPPORTED_PUMP_SENSORS = [ + ScreenLogicSensorDescription( + data_root=(DEVICE.PUMP,), + key=VALUE.WATTS_NOW, + device_class=SensorDeviceClass.POWER, + ), + ScreenLogicSensorDescription( + data_root=(DEVICE.PUMP,), + key=VALUE.GPM_NOW, + enabled_lambda=lambda type: type != PUMP_TYPE.INTELLIFLO_VS, + ), + ScreenLogicSensorDescription( + data_root=(DEVICE.PUMP,), + key=VALUE.RPM_NOW, + enabled_lambda=lambda type: type != PUMP_TYPE.INTELLIFLO_VF, + ), +] + +SUPPORTED_INTELLICHEM_SENSORS = [ + ScreenLogicPushSensorDescription( + subscription_code=CODE.STATUS_CHANGED, + data_root=(DEVICE.CONTROLLER, GROUP.SENSOR), + key=VALUE.ORP, + state_class=SensorStateClass.MEASUREMENT, + ), + ScreenLogicPushSensorDescription( + subscription_code=CODE.STATUS_CHANGED, + data_root=(DEVICE.CONTROLLER, GROUP.SENSOR), + key=VALUE.PH, + state_class=SensorStateClass.MEASUREMENT, + ), + ScreenLogicPushSensorDescription( + subscription_code=CODE.CHEMISTRY_CHANGED, + data_root=(DEVICE.INTELLICHEM, GROUP.SENSOR), + key=VALUE.ORP_NOW, + state_class=SensorStateClass.MEASUREMENT, + ), + ScreenLogicPushSensorDescription( + subscription_code=CODE.CHEMISTRY_CHANGED, + data_root=(DEVICE.INTELLICHEM, GROUP.SENSOR), + key=VALUE.PH_NOW, + state_class=SensorStateClass.MEASUREMENT, + ), + ScreenLogicPushSensorDescription( + subscription_code=CODE.CHEMISTRY_CHANGED, + data_root=(DEVICE.INTELLICHEM, GROUP.SENSOR), + key=VALUE.ORP_SUPPLY_LEVEL, + state_class=SensorStateClass.MEASUREMENT, + value_mod=lambda val: int(val) - 1, + ), + ScreenLogicPushSensorDescription( + subscription_code=CODE.CHEMISTRY_CHANGED, + data_root=(DEVICE.INTELLICHEM, GROUP.SENSOR), + key=VALUE.PH_SUPPLY_LEVEL, + state_class=SensorStateClass.MEASUREMENT, + value_mod=lambda val: int(val) - 1, + ), + ScreenLogicPushSensorDescription( + subscription_code=CODE.CHEMISTRY_CHANGED, + data_root=(DEVICE.INTELLICHEM, GROUP.SENSOR), + key=VALUE.PH_PROBE_WATER_TEMP, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + ScreenLogicPushSensorDescription( + subscription_code=CODE.CHEMISTRY_CHANGED, + data_root=(DEVICE.INTELLICHEM, GROUP.SENSOR), + key=VALUE.SATURATION, + state_class=SensorStateClass.MEASUREMENT, + ), + ScreenLogicPushSensorDescription( + subscription_code=CODE.CHEMISTRY_CHANGED, + data_root=(DEVICE.INTELLICHEM, GROUP.CONFIGURATION), + key=VALUE.CALCIUM_HARNESS, + ), + ScreenLogicPushSensorDescription( + subscription_code=CODE.CHEMISTRY_CHANGED, + data_root=(DEVICE.INTELLICHEM, GROUP.CONFIGURATION), + key=VALUE.CYA, + ), + ScreenLogicPushSensorDescription( + subscription_code=CODE.CHEMISTRY_CHANGED, + data_root=(DEVICE.INTELLICHEM, GROUP.CONFIGURATION), + key=VALUE.ORP_SETPOINT, + ), + ScreenLogicPushSensorDescription( + subscription_code=CODE.CHEMISTRY_CHANGED, + data_root=(DEVICE.INTELLICHEM, GROUP.CONFIGURATION), + key=VALUE.PH_SETPOINT, + ), + ScreenLogicPushSensorDescription( + subscription_code=CODE.CHEMISTRY_CHANGED, + data_root=(DEVICE.INTELLICHEM, GROUP.CONFIGURATION), + key=VALUE.TOTAL_ALKALINITY, + ), + ScreenLogicPushSensorDescription( + subscription_code=CODE.CHEMISTRY_CHANGED, + data_root=(DEVICE.INTELLICHEM, GROUP.CONFIGURATION), + key=VALUE.SALT_TDS_PPM, + ), + ScreenLogicPushSensorDescription( + subscription_code=CODE.CHEMISTRY_CHANGED, + data_root=(DEVICE.INTELLICHEM, GROUP.DOSE_STATUS), + key=VALUE.ORP_DOSING_STATE, + device_class=SensorDeviceClass.ENUM, + options=["Dosing", "Mixing", "Monitoring"], + value_mod=lambda val: DOSE_STATE(val).title, + ), + ScreenLogicPushSensorDescription( + subscription_code=CODE.CHEMISTRY_CHANGED, + data_root=(DEVICE.INTELLICHEM, GROUP.DOSE_STATUS), + key=VALUE.ORP_LAST_DOSE_TIME, + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + ScreenLogicPushSensorDescription( + subscription_code=CODE.CHEMISTRY_CHANGED, + data_root=(DEVICE.INTELLICHEM, GROUP.DOSE_STATUS), + key=VALUE.ORP_LAST_DOSE_VOLUME, + device_class=SensorDeviceClass.VOLUME, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + ScreenLogicPushSensorDescription( + subscription_code=CODE.CHEMISTRY_CHANGED, + data_root=(DEVICE.INTELLICHEM, GROUP.DOSE_STATUS), + key=VALUE.PH_DOSING_STATE, + device_class=SensorDeviceClass.ENUM, + options=["Dosing", "Mixing", "Monitoring"], + value_mod=lambda val: DOSE_STATE(val).title, + ), + ScreenLogicPushSensorDescription( + subscription_code=CODE.CHEMISTRY_CHANGED, + data_root=(DEVICE.INTELLICHEM, GROUP.DOSE_STATUS), + key=VALUE.PH_LAST_DOSE_TIME, + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + ScreenLogicPushSensorDescription( + subscription_code=CODE.CHEMISTRY_CHANGED, + data_root=(DEVICE.INTELLICHEM, GROUP.DOSE_STATUS), + key=VALUE.PH_LAST_DOSE_VOLUME, + device_class=SensorDeviceClass.VOLUME, + state_class=SensorStateClass.TOTAL_INCREASING, + ), +] + +SUPPORTED_SCG_SENSORS = [ + ScreenLogicSensorDescription( + data_root=(DEVICE.SCG, GROUP.SENSOR), + key=VALUE.SALT_PPM, + state_class=SensorStateClass.MEASUREMENT, + ), + ScreenLogicSensorDescription( + data_root=(DEVICE.SCG, GROUP.CONFIGURATION), + key=VALUE.SUPER_CHLOR_TIMER, + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up entry.""" + entities: list[ScreenLogicSensor] = [] + coordinator: ScreenlogicDataUpdateCoordinator = hass.data[SL_DOMAIN][ + config_entry.entry_id + ] + gateway = coordinator.gateway + + for core_sensor_description in SUPPORTED_CORE_SENSORS: + if ( + gateway.get_data( + *core_sensor_description.data_root, core_sensor_description.key + ) + is not None + ): + entities.append(ScreenLogicPushSensor(coordinator, core_sensor_description)) + + for pump_index, pump_data in gateway.get_data(DEVICE.PUMP).items(): + if not pump_data or not pump_data.get(VALUE.DATA): + continue + pump_type = pump_data[VALUE.TYPE] + for proto_pump_sensor_description in SUPPORTED_PUMP_SENSORS: + if not pump_data.get(proto_pump_sensor_description.key): + continue + entities.append( + ScreenLogicPumpSensor( + coordinator, + copy(proto_pump_sensor_description), + pump_index, + pump_type, + ) + ) + + chem_sensor_description: ScreenLogicPushSensorDescription + for chem_sensor_description in SUPPORTED_INTELLICHEM_SENSORS: + chem_sensor_data_path = ( + *chem_sensor_description.data_root, + chem_sensor_description.key, + ) + if EQUIPMENT_FLAG.INTELLICHEM not in gateway.equipment_flags: + cleanup_excluded_entity(coordinator, DOMAIN, chem_sensor_data_path) + continue + if gateway.get_data(*chem_sensor_data_path): + chem_sensor_description.entity_category = EntityCategory.DIAGNOSTIC + entities.append(ScreenLogicPushSensor(coordinator, chem_sensor_description)) + + scg_sensor_description: ScreenLogicSensorDescription + for scg_sensor_description in SUPPORTED_SCG_SENSORS: + scg_sensor_data_path = ( + *scg_sensor_description.data_root, + scg_sensor_description.key, + ) + if EQUIPMENT_FLAG.CHLORINATOR not in gateway.equipment_flags: + cleanup_excluded_entity(coordinator, DOMAIN, scg_sensor_data_path) + continue + if gateway.get_data(*scg_sensor_data_path): + scg_sensor_description.entity_category = EntityCategory.DIAGNOSTIC + entities.append(ScreenLogicSensor(coordinator, scg_sensor_description)) + + async_add_entities(entities) + + class ScreenLogicSensor(ScreenlogicEntity, SensorEntity): """Representation of a ScreenLogic sensor entity.""" entity_description: ScreenLogicSensorDescription _attr_has_entity_name = True + def __init__( + self, + coordinator: ScreenlogicDataUpdateCoordinator, + entity_description: ScreenLogicSensorDescription, + ) -> None: + """Initialize of the entity.""" + super().__init__(coordinator, entity_description) + self._attr_native_unit_of_measurement = get_ha_unit( + self.entity_data.get(ATTR.UNIT) + ) + @property def native_value(self) -> str | int | float: """State of the sensor.""" @@ -260,14 +316,29 @@ class ScreenLogicSensor(ScreenlogicEntity, SensorEntity): return value_mod(val) if value_mod else val -@dataclass -class ScreenLogicPushSensorDescription( - ScreenLogicSensorDescription, ScreenLogicPushEntityDescription -): - """Describes a ScreenLogic push sensor.""" - - class ScreenLogicPushSensor(ScreenLogicSensor, ScreenLogicPushEntity): """Representation of a ScreenLogic push sensor entity.""" entity_description: ScreenLogicPushSensorDescription + + +class ScreenLogicPumpSensor(ScreenLogicSensor): + """Representation of a ScreenLogic pump sensor.""" + + _attr_entity_category = EntityCategory.DIAGNOSTIC + _attr_state_class = SensorStateClass.MEASUREMENT + + def __init__( + self, + coordinator: ScreenlogicDataUpdateCoordinator, + entity_description: ScreenLogicSensorDescription, + pump_index: int, + pump_type: int, + ) -> None: + """Initialize of the entity.""" + entity_description.data_root = (DEVICE.PUMP, pump_index) + super().__init__(coordinator, entity_description) + if entity_description.enabled_lambda: + self._attr_entity_registry_enabled_default = ( + entity_description.enabled_lambda(pump_type) + ) diff --git a/homeassistant/components/screenlogic/switch.py b/homeassistant/components/screenlogic/switch.py index 247ec4f2f03..4900ed938a1 100644 --- a/homeassistant/components/screenlogic/switch.py +++ b/homeassistant/components/screenlogic/switch.py @@ -30,7 +30,11 @@ async def async_setup_entry( ] gateway = coordinator.gateway for circuit_index, circuit_data in gateway.get_data(DEVICE.CIRCUIT).items(): - if circuit_data[ATTR.FUNCTION] in LIGHT_CIRCUIT_FUNCTIONS: + if ( + not circuit_data + or ((circuit_function := circuit_data.get(ATTR.FUNCTION)) is None) + or circuit_function in LIGHT_CIRCUIT_FUNCTIONS + ): continue circuit_name = circuit_data[ATTR.NAME] circuit_interface = INTERFACE(circuit_data[ATTR.INTERFACE]) @@ -39,9 +43,8 @@ async def async_setup_entry( coordinator, ScreenLogicSwitchDescription( subscription_code=CODE.STATUS_CHANGED, - data_path=(DEVICE.CIRCUIT, circuit_index), + data_root=(DEVICE.CIRCUIT,), key=circuit_index, - name=circuit_name, entity_registry_enabled_default=( circuit_name not in GENERIC_CIRCUIT_NAMES and circuit_interface != INTERFACE.DONT_SHOW diff --git a/homeassistant/components/screenlogic/util.py b/homeassistant/components/screenlogic/util.py index c8d9d5f0f77..928effc73fc 100644 --- a/homeassistant/components/screenlogic/util.py +++ b/homeassistant/components/screenlogic/util.py @@ -5,32 +5,40 @@ from screenlogicpy.const.data import SHARED_VALUES from homeassistant.helpers import entity_registry as er -from .const import DOMAIN as SL_DOMAIN +from .const import DOMAIN as SL_DOMAIN, SL_UNIT_TO_HA_UNIT, ScreenLogicDataPath from .coordinator import ScreenlogicDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) -def generate_unique_id( - device: str | int, group: str | int | None, data_key: str | int -) -> str: +def generate_unique_id(*args: str | int | None) -> str: """Generate new unique_id for a screenlogic entity from specified parameters.""" - if data_key in SHARED_VALUES and device is not None: - if group is not None and (isinstance(group, int) or group.isdigit()): - return f"{device}_{group}_{data_key}" - return f"{device}_{data_key}" - return str(data_key) + _LOGGER.debug("gen_uid called with %s", args) + if len(args) == 3: + if args[2] in SHARED_VALUES: + if args[1] is not None and (isinstance(args[1], int) or args[1].isdigit()): + return f"{args[0]}_{args[1]}_{args[2]}" + return f"{args[0]}_{args[2]}" + return f"{args[2]}" + return f"{args[1]}" + + +def get_ha_unit(sl_unit) -> str: + """Return equivalent Home Assistant unit of measurement if exists.""" + if (ha_unit := SL_UNIT_TO_HA_UNIT.get(sl_unit)) is not None: + return ha_unit + return sl_unit def cleanup_excluded_entity( coordinator: ScreenlogicDataUpdateCoordinator, platform_domain: str, - entity_key: str, + data_path: ScreenLogicDataPath, ) -> None: """Remove excluded entity if it exists.""" assert coordinator.config_entry entity_registry = er.async_get(coordinator.hass) - unique_id = f"{coordinator.config_entry.unique_id}_{entity_key}" + unique_id = f"{coordinator.config_entry.unique_id}_{generate_unique_id(*data_path)}" if entity_id := entity_registry.async_get_entity_id( platform_domain, SL_DOMAIN, unique_id ): diff --git a/tests/components/screenlogic/__init__.py b/tests/components/screenlogic/__init__.py index 48362722312..e5400e3ca15 100644 --- a/tests/components/screenlogic/__init__.py +++ b/tests/components/screenlogic/__init__.py @@ -35,12 +35,21 @@ def num_key_string_to_int(data: dict) -> None: DATA_FULL_CHEM = num_key_string_to_int( load_json_object_fixture("screenlogic/data_full_chem.json") ) +DATA_FULL_NO_GPM = num_key_string_to_int( + load_json_object_fixture("screenlogic/data_full_no_gpm.json") +) +DATA_FULL_NO_SALT_PPM = num_key_string_to_int( + load_json_object_fixture("screenlogic/data_full_no_salt_ppm.json") +) DATA_MIN_MIGRATION = num_key_string_to_int( load_json_object_fixture("screenlogic/data_min_migration.json") ) DATA_MIN_ENTITY_CLEANUP = num_key_string_to_int( load_json_object_fixture("screenlogic/data_min_entity_cleanup.json") ) +DATA_MISSING_VALUES_CHEM_CHLOR = num_key_string_to_int( + load_json_object_fixture("screenlogic/data_missing_values_chem_chlor.json") +) async def stub_async_connect( diff --git a/tests/components/screenlogic/fixtures/data_full_no_gpm.json b/tests/components/screenlogic/fixtures/data_full_no_gpm.json new file mode 100644 index 00000000000..93e3040f911 --- /dev/null +++ b/tests/components/screenlogic/fixtures/data_full_no_gpm.json @@ -0,0 +1,784 @@ +{ + "adapter": { + "firmware": { + "name": "Protocol Adapter Firmware", + "value": "POOL: 5.2 Build 738.0 Rel" + } + }, + "controller": { + "controller_id": 100, + "configuration": { + "body_type": { + "0": { + "min_setpoint": 40, + "max_setpoint": 104 + }, + "1": { + "min_setpoint": 40, + "max_setpoint": 104 + } + }, + "is_celsius": { + "name": "Is Celsius", + "value": 0 + }, + "controller_type": 1, + "hardware_type": 0, + "controller_data": 0, + "generic_circuit_name": "Water Features", + "circuit_count": 7, + "color_count": 8, + "color": [ + { + "name": "White", + "value": [255, 255, 255] + }, + { + "name": "Light Green", + "value": [160, 255, 160] + }, + { + "name": "Green", + "value": [0, 255, 80] + }, + { + "name": "Cyan", + "value": [0, 255, 200] + }, + { + "name": "Blue", + "value": [100, 140, 255] + }, + { + "name": "Lavender", + "value": [230, 130, 255] + }, + { + "name": "Magenta", + "value": [255, 0, 128] + }, + { + "name": "Light Magenta", + "value": [255, 180, 210] + } + ], + "interface_tab_flags": 127, + "show_alarms": 1, + "remotes": 0, + "unknown_at_offset_09": 0, + "unknown_at_offset_10": 0, + "unknown_at_offset_11": 0 + }, + "model": { + "name": "Model", + "value": "IntelliTouch i7+3" + }, + "equipment": { + "flags": 56, + "list": ["INTELLIBRITE", "INTELLIFLO_0", "INTELLIFLO_1"] + }, + "sensor": { + "state": { + "name": "Controller State", + "value": 1, + "device_type": "enum", + "enum_options": ["Unknown", "Ready", "Sync", "Service"] + }, + "freeze_mode": { + "name": "Freeze Mode", + "value": 0 + }, + "pool_delay": { + "name": "Pool Delay", + "value": 0 + }, + "spa_delay": { + "name": "Spa Delay", + "value": 0 + }, + "cleaner_delay": { + "name": "Cleaner Delay", + "value": 0 + }, + "air_temperature": { + "name": "Air Temperature", + "value": 91, + "unit": "\u00b0F", + "device_type": "temperature", + "state_type": "measurement" + }, + "ph": { + "name": "pH", + "value": 0.0, + "unit": "pH", + "state_type": "measurement" + }, + "orp": { + "name": "ORP", + "value": 0, + "unit": "mV", + "state_type": "measurement" + }, + "saturation": { + "name": "Saturation Index", + "value": 0.0, + "unit": "lsi", + "state_type": "measurement" + }, + "salt_ppm": { + "name": "Salt", + "value": 0, + "unit": "ppm", + "state_type": "measurement" + }, + "ph_supply_level": { + "name": "pH Supply Level", + "value": 0, + "state_type": "measurement" + }, + "orp_supply_level": { + "name": "ORP Supply Level", + "value": 0, + "state_type": "measurement" + }, + "active_alert": { + "name": "Active Alert", + "value": 0, + "device_type": "alarm" + } + } + }, + "circuit": { + "500": { + "circuit_id": 500, + "name": "Spa", + "configuration": { + "name_index": 71, + "flags": 1, + "default_runtime": 720, + "unknown_at_offset_62": 0, + "unknown_at_offset_63": 0, + "delay": 0 + }, + "function": 1, + "interface": 1, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 1, + "value": 0 + }, + "501": { + "circuit_id": 501, + "name": "Cleaner", + "configuration": { + "name_index": 21, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_90": 0, + "unknown_at_offset_91": 0, + "delay": 0 + }, + "function": 5, + "interface": 5, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 2, + "value": 0 + }, + "502": { + "circuit_id": 502, + "name": "Jets", + "configuration": { + "name_index": 45, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_114": 0, + "unknown_at_offset_115": 0, + "delay": 0 + }, + "function": 0, + "interface": 5, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 3, + "value": 0 + }, + "503": { + "circuit_id": 503, + "name": "Pool Light", + "configuration": { + "name_index": 62, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_146": 0, + "unknown_at_offset_147": 0, + "delay": 0 + }, + "function": 16, + "interface": 3, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 10 + }, + "device_id": 4, + "value": 0 + }, + "504": { + "circuit_id": 504, + "name": "Spa Light", + "configuration": { + "name_index": 73, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_178": 0, + "unknown_at_offset_179": 0, + "delay": 0 + }, + "function": 16, + "interface": 3, + "color": { + "color_set": 0, + "color_position": 1, + "color_stagger": 10 + }, + "device_id": 5, + "value": 0 + }, + "505": { + "circuit_id": 505, + "name": "Pool", + "configuration": { + "name_index": 60, + "flags": 1, + "default_runtime": 720, + "unknown_at_offset_202": 0, + "unknown_at_offset_203": 0, + "delay": 0 + }, + "function": 2, + "interface": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 6, + "value": 1 + }, + "506": { + "circuit_id": 506, + "name": "Air Blower", + "configuration": { + "name_index": 1, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_234": 0, + "unknown_at_offset_235": 0, + "delay": 0 + }, + "function": 0, + "interface": 5, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 7, + "value": 0 + } + }, + "pump": { + "0": { + "data": 134, + "type": 2, + "state": { + "name": "Pool Pump", + "value": 1 + }, + "watts_now": { + "name": "Pool Pump Watts Now", + "value": 63, + "unit": "W", + "device_type": "power", + "state_type": "measurement" + }, + "rpm_now": { + "name": "Pool Pump RPM Now", + "value": 1050, + "unit": "rpm", + "state_type": "measurement" + }, + "unknown_at_offset_16": 0, + "unknown_at_offset_24": 255, + "preset": { + "0": { + "device_id": 6, + "setpoint": 1050, + "is_rpm": 1 + }, + "1": { + "device_id": 1, + "setpoint": 1850, + "is_rpm": 1 + }, + "2": { + "device_id": 2, + "setpoint": 1500, + "is_rpm": 1 + }, + "3": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + }, + "4": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + }, + "5": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + }, + "6": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + }, + "7": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + } + } + }, + "1": { + "data": 131, + "type": 2, + "state": { + "name": "Jets Pump", + "value": 0 + }, + "watts_now": { + "name": "Jets Pump Watts Now", + "value": 0, + "unit": "W", + "device_type": "power", + "state_type": "measurement" + }, + "rpm_now": { + "name": "Jets Pump RPM Now", + "value": 0, + "unit": "rpm", + "state_type": "measurement" + }, + "unknown_at_offset_16": 0, + "gpm_now": { + "name": "Jets Pump GPM Now", + "value": 0, + "unit": "gpm", + "state_type": "measurement" + }, + "unknown_at_offset_24": 255, + "preset": { + "0": { + "device_id": 3, + "setpoint": 2970, + "is_rpm": 1 + }, + "1": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + }, + "2": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + }, + "3": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + }, + "4": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + }, + "5": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + }, + "6": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + }, + "7": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + } + } + }, + "2": { + "data": 0 + }, + "3": { + "data": 0 + }, + "4": { + "data": 0 + }, + "5": { + "data": 0 + }, + "6": { + "data": 0 + }, + "7": { + "data": 0 + } + }, + "body": { + "0": { + "body_type": 0, + "min_setpoint": 40, + "max_setpoint": 104, + "name": "Pool", + "last_temperature": { + "name": "Last Pool Temperature", + "value": 86, + "unit": "\u00b0F", + "device_type": "temperature", + "state_type": "measurement" + }, + "heat_state": { + "name": "Pool Heat", + "value": 0, + "device_type": "enum", + "enum_options": ["Off", "Solar", "Heater", "Both"] + }, + "heat_setpoint": { + "name": "Pool Heat Set Point", + "value": 85, + "unit": "\u00b0F", + "device_type": "temperature" + }, + "cool_setpoint": { + "name": "Pool Cool Set Point", + "value": 100, + "unit": "\u00b0F", + "device_type": "temperature" + }, + "heat_mode": { + "name": "Pool Heat Mode", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Solar Preferred", + "Heater", + "Don't Change" + ] + } + }, + "1": { + "body_type": 1, + "min_setpoint": 40, + "max_setpoint": 104, + "name": "Spa", + "last_temperature": { + "name": "Last Spa Temperature", + "value": 84, + "unit": "\u00b0F", + "device_type": "temperature", + "state_type": "measurement" + }, + "heat_state": { + "name": "Spa Heat", + "value": 0, + "device_type": "enum", + "enum_options": ["Off", "Solar", "Heater", "Both"] + }, + "heat_setpoint": { + "name": "Spa Heat Set Point", + "value": 102, + "unit": "\u00b0F", + "device_type": "temperature" + }, + "cool_setpoint": { + "name": "Spa Cool Set Point", + "value": 91, + "unit": "\u00b0F", + "device_type": "temperature" + }, + "heat_mode": { + "name": "Spa Heat Mode", + "value": 3, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Solar Preferred", + "Heater", + "Don't Change" + ] + } + } + }, + "intellichem": { + "unknown_at_offset_00": 42, + "unknown_at_offset_04": 0, + "sensor": { + "ph_now": { + "name": "pH Now", + "value": 0.0, + "unit": "pH", + "state_type": "measurement" + }, + "orp_now": { + "name": "ORP Now", + "value": 0, + "unit": "mV", + "state_type": "measurement" + }, + "ph_supply_level": { + "name": "pH Supply Level", + "value": 0, + "state_type": "measurement" + }, + "orp_supply_level": { + "name": "ORP Supply Level", + "value": 0, + "state_type": "measurement" + }, + "saturation": { + "name": "Saturation Index", + "value": 0.0, + "unit": "lsi", + "state_type": "measurement" + }, + "ph_probe_water_temp": { + "name": "pH Probe Water Temperature", + "value": 0, + "unit": "\u00b0F", + "device_type": "temperature", + "state_type": "measurement" + } + }, + "configuration": { + "ph_setpoint": { + "name": "pH Setpoint", + "value": 0.0, + "unit": "pH" + }, + "orp_setpoint": { + "name": "ORP Setpoint", + "value": 0, + "unit": "mV" + }, + "calcium_harness": { + "name": "Calcium Hardness", + "value": 0, + "unit": "ppm" + }, + "cya": { + "name": "Cyanuric Acid", + "value": 0, + "unit": "ppm" + }, + "total_alkalinity": { + "name": "Total Alkalinity", + "value": 0, + "unit": "ppm" + }, + "salt_tds_ppm": { + "name": "Salt/TDS", + "value": 0, + "unit": "ppm" + }, + "probe_is_celsius": 0, + "flags": 0 + }, + "dose_status": { + "ph_last_dose_time": { + "name": "Last pH Dose Time", + "value": 0, + "unit": "sec", + "device_type": "duration", + "state_type": "total_increasing" + }, + "orp_last_dose_time": { + "name": "Last ORP Dose Time", + "value": 0, + "unit": "sec", + "device_type": "duration", + "state_type": "total_increasing" + }, + "ph_last_dose_volume": { + "name": "Last pH Dose Volume", + "value": 0, + "unit": "mL", + "device_type": "volume", + "state_type": "total_increasing" + }, + "orp_last_dose_volume": { + "name": "Last ORP Dose Volume", + "value": 0, + "unit": "mL", + "device_type": "volume", + "state_type": "total_increasing" + }, + "flags": 0, + "ph_dosing_state": { + "name": "pH Dosing State", + "value": 0, + "device_type": "enum", + "enum_options": ["Dosing", "Mixing", "Monitoring"] + }, + "orp_dosing_state": { + "name": "ORP Dosing State", + "value": 0, + "device_type": "enum", + "enum_options": ["Dosing", "Mixing", "Monitoring"] + } + }, + "alarm": { + "flags": 0, + "flow_alarm": { + "name": "Flow Alarm", + "value": 0, + "device_type": "alarm" + }, + "ph_high_alarm": { + "name": "pH HIGH Alarm", + "value": 0, + "device_type": "alarm" + }, + "ph_low_alarm": { + "name": "pH LOW Alarm", + "value": 0, + "device_type": "alarm" + }, + "orp_high_alarm": { + "name": "ORP HIGH Alarm", + "value": 0, + "device_type": "alarm" + }, + "orp_low_alarm": { + "name": "ORP LOW Alarm", + "value": 0, + "device_type": "alarm" + }, + "ph_supply_alarm": { + "name": "pH Supply Alarm", + "value": 0, + "device_type": "alarm" + }, + "orp_supply_alarm": { + "name": "ORP Supply Alarm", + "value": 0, + "device_type": "alarm" + }, + "probe_fault_alarm": { + "name": "Probe Fault", + "value": 0, + "device_type": "alarm" + } + }, + "alert": { + "flags": 0, + "ph_lockout": { + "name": "pH Lockout", + "value": 0 + }, + "ph_limit": { + "name": "pH Dose Limit Reached", + "value": 0 + }, + "orp_limit": { + "name": "ORP Dose Limit Reached", + "value": 0 + } + }, + "firmware": { + "name": "IntelliChem Firmware", + "value": "0.000" + }, + "water_balance": { + "flags": 0, + "corrosive": { + "name": "SI Corrosive", + "value": 0, + "device_type": "alarm" + }, + "scaling": { + "name": "SI Scaling", + "value": 0, + "device_type": "alarm" + } + }, + "unknown_at_offset_44": 0, + "unknown_at_offset_45": 0, + "unknown_at_offset_46": 0 + }, + "scg": { + "scg_present": 0, + "sensor": { + "state": { + "name": "Chlorinator", + "value": 1 + }, + "salt_ppm": { + "name": "Chlorinator Salt", + "value": 0, + "unit": "ppm", + "state_type": "measurement" + } + }, + "configuration": { + "pool_setpoint": { + "name": "Pool Chlorinator Setpoint", + "value": 50, + "unit": "%", + "min_setpoint": 0, + "max_setpoint": 100, + "step": 5, + "body_type": 0 + }, + "spa_setpoint": { + "name": "Spa Chlorinator Setpoint", + "value": 0, + "unit": "%", + "min_setpoint": 0, + "max_setpoint": 100, + "step": 5, + "body_type": 1 + }, + "super_chlor_timer": { + "name": "Super Chlorination Timer", + "value": 0, + "unit": "hr", + "min_setpoint": 1, + "max_setpoint": 72, + "step": 1 + } + }, + "flags": 0 + } +} diff --git a/tests/components/screenlogic/fixtures/data_full_no_salt_ppm.json b/tests/components/screenlogic/fixtures/data_full_no_salt_ppm.json new file mode 100644 index 00000000000..d17d0e41170 --- /dev/null +++ b/tests/components/screenlogic/fixtures/data_full_no_salt_ppm.json @@ -0,0 +1,859 @@ +{ + "adapter": { + "firmware": { + "name": "Protocol Adapter Firmware", + "value": "POOL: 5.2 Build 736.0 Rel" + } + }, + "controller": { + "controller_id": 100, + "configuration": { + "body_type": { + "0": { + "min_setpoint": 40, + "max_setpoint": 104 + }, + "1": { + "min_setpoint": 40, + "max_setpoint": 104 + } + }, + "is_celsius": { + "name": "Is Celsius", + "value": 0 + }, + "controller_type": 13, + "hardware_type": 0, + "controller_data": 0, + "generic_circuit_name": "Water Features", + "circuit_count": 11, + "color_count": 8, + "color": [ + { + "name": "White", + "value": [255, 255, 255] + }, + { + "name": "Light Green", + "value": [160, 255, 160] + }, + { + "name": "Green", + "value": [0, 255, 80] + }, + { + "name": "Cyan", + "value": [0, 255, 200] + }, + { + "name": "Blue", + "value": [100, 140, 255] + }, + { + "name": "Lavender", + "value": [230, 130, 255] + }, + { + "name": "Magenta", + "value": [255, 0, 128] + }, + { + "name": "Light Magenta", + "value": [255, 180, 210] + } + ], + "interface_tab_flags": 127, + "show_alarms": 0, + "remotes": 0, + "unknown_at_offset_09": 0, + "unknown_at_offset_10": 0, + "unknown_at_offset_11": 0 + }, + "model": { + "name": "Model", + "value": "EasyTouch2 8" + }, + "equipment": { + "flags": 60, + "list": ["CHLORINATOR", "INTELLIBRITE", "INTELLIFLO_0", "INTELLIFLO_1"] + }, + "sensor": { + "state": { + "name": "Controller State", + "value": 1, + "device_type": "enum", + "enum_options": ["Unknown", "Ready", "Sync", "Service"] + }, + "freeze_mode": { + "name": "Freeze Mode", + "value": 0 + }, + "pool_delay": { + "name": "Pool Delay", + "value": 0 + }, + "spa_delay": { + "name": "Spa Delay", + "value": 0 + }, + "cleaner_delay": { + "name": "Cleaner Delay", + "value": 0 + }, + "air_temperature": { + "name": "Air Temperature", + "value": 69, + "unit": "\u00b0F", + "device_type": "temperature", + "state_type": "measurement" + }, + "ph": { + "name": "pH", + "value": 7.61, + "unit": "pH", + "state_type": "measurement" + }, + "orp": { + "name": "ORP", + "value": 728, + "unit": "mV", + "state_type": "measurement" + }, + "saturation": { + "name": "Saturation Index", + "value": 0.06, + "unit": "lsi", + "state_type": "measurement" + }, + "salt_ppm": { + "name": "Salt", + "value": 0, + "unit": "ppm", + "state_type": "measurement" + }, + "ph_supply_level": { + "name": "pH Supply Level", + "value": 2, + "state_type": "measurement" + }, + "orp_supply_level": { + "name": "ORP Supply Level", + "value": 3, + "state_type": "measurement" + }, + "active_alert": { + "name": "Active Alert", + "value": 0, + "device_type": "alarm" + } + } + }, + "circuit": { + "500": { + "circuit_id": 500, + "name": "Spa", + "configuration": { + "name_index": 71, + "flags": 1, + "default_runtime": 720, + "unknown_at_offset_62": 0, + "unknown_at_offset_63": 0, + "delay": 0 + }, + "function": 1, + "interface": 1, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 1, + "value": 0 + }, + "501": { + "circuit_id": 501, + "name": "Waterfall", + "configuration": { + "name_index": 85, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_94": 0, + "unknown_at_offset_95": 0, + "delay": 0 + }, + "function": 0, + "interface": 2, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 2, + "value": 0 + }, + "502": { + "circuit_id": 502, + "name": "Pool Light", + "configuration": { + "name_index": 62, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_126": 0, + "unknown_at_offset_127": 0, + "delay": 0 + }, + "function": 16, + "interface": 3, + "color": { + "color_set": 2, + "color_position": 0, + "color_stagger": 2 + }, + "device_id": 3, + "value": 0 + }, + "503": { + "circuit_id": 503, + "name": "Spa Light", + "configuration": { + "name_index": 73, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_158": 0, + "unknown_at_offset_159": 0, + "delay": 0 + }, + "function": 16, + "interface": 3, + "color": { + "color_set": 6, + "color_position": 1, + "color_stagger": 10 + }, + "device_id": 4, + "value": 0 + }, + "504": { + "circuit_id": 504, + "name": "Cleaner", + "configuration": { + "name_index": 21, + "flags": 0, + "default_runtime": 240, + "unknown_at_offset_186": 0, + "unknown_at_offset_187": 0, + "delay": 0 + }, + "function": 5, + "interface": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 5, + "value": 0 + }, + "505": { + "circuit_id": 505, + "name": "Pool Low", + "configuration": { + "name_index": 63, + "flags": 1, + "default_runtime": 720, + "unknown_at_offset_214": 0, + "unknown_at_offset_215": 0, + "delay": 0 + }, + "function": 2, + "interface": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 6, + "value": 0 + }, + "506": { + "circuit_id": 506, + "name": "Yard Light", + "configuration": { + "name_index": 91, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_246": 0, + "unknown_at_offset_247": 0, + "delay": 0 + }, + "function": 7, + "interface": 4, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 7, + "value": 0 + }, + "507": { + "circuit_id": 507, + "name": "Cameras", + "configuration": { + "name_index": 101, + "flags": 0, + "default_runtime": 1620, + "unknown_at_offset_274": 0, + "unknown_at_offset_275": 0, + "delay": 0 + }, + "function": 0, + "interface": 2, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 8, + "value": 1 + }, + "508": { + "circuit_id": 508, + "name": "Pool High", + "configuration": { + "name_index": 61, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_306": 0, + "unknown_at_offset_307": 0, + "delay": 0 + }, + "function": 0, + "interface": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 9, + "value": 0 + }, + "510": { + "circuit_id": 510, + "name": "Spillway", + "configuration": { + "name_index": 78, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_334": 0, + "unknown_at_offset_335": 0, + "delay": 0 + }, + "function": 14, + "interface": 1, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 11, + "value": 0 + }, + "511": { + "circuit_id": 511, + "name": "Pool High", + "configuration": { + "name_index": 61, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_366": 0, + "unknown_at_offset_367": 0, + "delay": 0 + }, + "function": 0, + "interface": 5, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 12, + "value": 0 + } + }, + "pump": { + "0": { + "data": 70, + "type": 3, + "state": { + "name": "Pool Low Pump", + "value": 0 + }, + "watts_now": { + "name": "Pool Low Pump Watts Now", + "value": 0, + "unit": "W", + "device_type": "power", + "state_type": "measurement" + }, + "rpm_now": { + "name": "Pool Low Pump RPM Now", + "value": 0, + "unit": "rpm", + "state_type": "measurement" + }, + "unknown_at_offset_16": 0, + "gpm_now": { + "name": "Pool Low Pump GPM Now", + "value": 0, + "unit": "gpm", + "state_type": "measurement" + }, + "unknown_at_offset_24": 255, + "preset": { + "0": { + "device_id": 6, + "setpoint": 63, + "is_rpm": 0 + }, + "1": { + "device_id": 9, + "setpoint": 72, + "is_rpm": 0 + }, + "2": { + "device_id": 1, + "setpoint": 3450, + "is_rpm": 1 + }, + "3": { + "device_id": 130, + "setpoint": 75, + "is_rpm": 0 + }, + "4": { + "device_id": 12, + "setpoint": 72, + "is_rpm": 0 + }, + "5": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "6": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "7": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + } + } + }, + "1": { + "data": 66, + "type": 3, + "state": { + "name": "Waterfall Pump", + "value": 0 + }, + "watts_now": { + "name": "Waterfall Pump Watts Now", + "value": 0, + "unit": "W", + "device_type": "power", + "state_type": "measurement" + }, + "rpm_now": { + "name": "Waterfall Pump RPM Now", + "value": 0, + "unit": "rpm", + "state_type": "measurement" + }, + "unknown_at_offset_16": 0, + "gpm_now": { + "name": "Waterfall Pump GPM Now", + "value": 0, + "unit": "gpm", + "state_type": "measurement" + }, + "unknown_at_offset_24": 255, + "preset": { + "0": { + "device_id": 2, + "setpoint": 2700, + "is_rpm": 1 + }, + "1": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "2": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "3": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "4": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "5": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "6": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "7": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + } + } + }, + "2": { + "data": 0 + }, + "3": { + "data": 0 + }, + "4": { + "data": 0 + }, + "5": { + "data": 0 + }, + "6": { + "data": 0 + }, + "7": { + "data": 0 + } + }, + "body": { + "0": { + "body_type": 0, + "min_setpoint": 40, + "max_setpoint": 104, + "name": "Pool", + "last_temperature": { + "name": "Last Pool Temperature", + "value": 81, + "unit": "\u00b0F", + "device_type": "temperature", + "state_type": "measurement" + }, + "heat_state": { + "name": "Pool Heat", + "value": 0, + "device_type": "enum", + "enum_options": ["Off", "Solar", "Heater", "Both"] + }, + "heat_setpoint": { + "name": "Pool Heat Set Point", + "value": 83, + "unit": "\u00b0F", + "device_type": "temperature" + }, + "cool_setpoint": { + "name": "Pool Cool Set Point", + "value": 100, + "unit": "\u00b0F", + "device_type": "temperature" + }, + "heat_mode": { + "name": "Pool Heat Mode", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Solar Preferred", + "Heater", + "Don't Change" + ] + } + }, + "1": { + "body_type": 1, + "min_setpoint": 40, + "max_setpoint": 104, + "name": "Spa", + "last_temperature": { + "name": "Last Spa Temperature", + "value": 84, + "unit": "\u00b0F", + "device_type": "temperature", + "state_type": "measurement" + }, + "heat_state": { + "name": "Spa Heat", + "value": 0, + "device_type": "enum", + "enum_options": ["Off", "Solar", "Heater", "Both"] + }, + "heat_setpoint": { + "name": "Spa Heat Set Point", + "value": 94, + "unit": "\u00b0F", + "device_type": "temperature" + }, + "cool_setpoint": { + "name": "Spa Cool Set Point", + "value": 69, + "unit": "\u00b0F", + "device_type": "temperature" + }, + "heat_mode": { + "name": "Spa Heat Mode", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Solar Preferred", + "Heater", + "Don't Change" + ] + } + } + }, + "intellichem": { + "unknown_at_offset_00": 42, + "unknown_at_offset_04": 0, + "sensor": { + "ph_now": { + "name": "pH Now", + "value": 0.0, + "unit": "pH", + "state_type": "measurement" + }, + "orp_now": { + "name": "ORP Now", + "value": 0, + "unit": "mV", + "state_type": "measurement" + }, + "ph_supply_level": { + "name": "pH Supply Level", + "value": 2, + "state_type": "measurement" + }, + "orp_supply_level": { + "name": "ORP Supply Level", + "value": 3, + "state_type": "measurement" + }, + "saturation": { + "name": "Saturation Index", + "value": 0.06, + "unit": "lsi", + "state_type": "measurement" + }, + "ph_probe_water_temp": { + "name": "pH Probe Water Temperature", + "value": 81, + "unit": "\u00b0F", + "device_type": "temperature", + "state_type": "measurement" + } + }, + "configuration": { + "ph_setpoint": { + "name": "pH Setpoint", + "value": 7.6, + "unit": "pH" + }, + "orp_setpoint": { + "name": "ORP Setpoint", + "value": 720, + "unit": "mV" + }, + "calcium_harness": { + "name": "Calcium Hardness", + "value": 800, + "unit": "ppm" + }, + "cya": { + "name": "Cyanuric Acid", + "value": 45, + "unit": "ppm" + }, + "total_alkalinity": { + "name": "Total Alkalinity", + "value": 45, + "unit": "ppm" + }, + "salt_tds_ppm": { + "name": "Salt/TDS", + "value": 1000, + "unit": "ppm" + }, + "probe_is_celsius": 0, + "flags": 32 + }, + "dose_status": { + "ph_last_dose_time": { + "name": "Last pH Dose Time", + "value": 5, + "unit": "sec", + "device_type": "duration", + "state_type": "total_increasing" + }, + "orp_last_dose_time": { + "name": "Last ORP Dose Time", + "value": 4, + "unit": "sec", + "device_type": "duration", + "state_type": "total_increasing" + }, + "ph_last_dose_volume": { + "name": "Last pH Dose Volume", + "value": 8, + "unit": "mL", + "device_type": "volume", + "state_type": "total_increasing" + }, + "orp_last_dose_volume": { + "name": "Last ORP Dose Volume", + "value": 8, + "unit": "mL", + "device_type": "volume", + "state_type": "total_increasing" + }, + "flags": 149, + "ph_dosing_state": { + "name": "pH Dosing State", + "value": 1, + "device_type": "enum", + "enum_options": ["Dosing", "Mixing", "Monitoring"] + }, + "orp_dosing_state": { + "name": "ORP Dosing State", + "value": 2, + "device_type": "enum", + "enum_options": ["Dosing", "Mixing", "Monitoring"] + } + }, + "alarm": { + "flags": 1, + "flow_alarm": { + "name": "Flow Alarm", + "value": 1, + "device_type": "alarm" + }, + "ph_high_alarm": { + "name": "pH HIGH Alarm", + "value": 0, + "device_type": "alarm" + }, + "ph_low_alarm": { + "name": "pH LOW Alarm", + "value": 0, + "device_type": "alarm" + }, + "orp_high_alarm": { + "name": "ORP HIGH Alarm", + "value": 0, + "device_type": "alarm" + }, + "orp_low_alarm": { + "name": "ORP LOW Alarm", + "value": 0, + "device_type": "alarm" + }, + "ph_supply_alarm": { + "name": "pH Supply Alarm", + "value": 0, + "device_type": "alarm" + }, + "orp_supply_alarm": { + "name": "ORP Supply Alarm", + "value": 0, + "device_type": "alarm" + }, + "probe_fault_alarm": { + "name": "Probe Fault", + "value": 0, + "device_type": "alarm" + } + }, + "alert": { + "flags": 0, + "ph_lockout": { + "name": "pH Lockout", + "value": 0 + }, + "ph_limit": { + "name": "pH Dose Limit Reached", + "value": 0 + }, + "orp_limit": { + "name": "ORP Dose Limit Reached", + "value": 0 + } + }, + "firmware": { + "name": "IntelliChem Firmware", + "value": "1.060" + }, + "water_balance": { + "flags": 0, + "corrosive": { + "name": "SI Corrosive", + "value": 0, + "device_type": "alarm" + }, + "scaling": { + "name": "SI Scaling", + "value": 0, + "device_type": "alarm" + } + }, + "unknown_at_offset_44": 0, + "unknown_at_offset_45": 0, + "unknown_at_offset_46": 0 + }, + "scg": { + "scg_present": 1, + "sensor": { + "state": { + "name": "Chlorinator", + "value": 0 + } + }, + "configuration": { + "pool_setpoint": { + "name": "Pool Chlorinator Setpoint", + "value": 50, + "unit": "%", + "min_setpoint": 0, + "max_setpoint": 100, + "step": 5, + "body_type": 0 + }, + "super_chlor_timer": { + "name": "Super Chlorination Timer", + "value": 0, + "unit": "hr", + "min_setpoint": 1, + "max_setpoint": 72, + "step": 1 + } + }, + "flags": 0 + } +} diff --git a/tests/components/screenlogic/fixtures/data_missing_values_chem_chlor.json b/tests/components/screenlogic/fixtures/data_missing_values_chem_chlor.json new file mode 100644 index 00000000000..c30ee690f8a --- /dev/null +++ b/tests/components/screenlogic/fixtures/data_missing_values_chem_chlor.json @@ -0,0 +1,849 @@ +{ + "adapter": { + "firmware": { + "name": "Protocol Adapter Firmware", + "value": "POOL: 5.2 Build 736.0 Rel" + } + }, + "controller": { + "controller_id": 100, + "configuration": { + "body_type": { + "0": { + "min_setpoint": 40, + "max_setpoint": 104 + }, + "1": { + "min_setpoint": 40, + "max_setpoint": 104 + } + }, + "is_celsius": { + "name": "Is Celsius", + "value": 0 + }, + "controller_type": 13, + "hardware_type": 0, + "controller_data": 0, + "generic_circuit_name": "Water Features", + "circuit_count": 11, + "color_count": 8, + "color": [ + { + "name": "White", + "value": [255, 255, 255] + }, + { + "name": "Light Green", + "value": [160, 255, 160] + }, + { + "name": "Green", + "value": [0, 255, 80] + }, + { + "name": "Cyan", + "value": [0, 255, 200] + }, + { + "name": "Blue", + "value": [100, 140, 255] + }, + { + "name": "Lavender", + "value": [230, 130, 255] + }, + { + "name": "Magenta", + "value": [255, 0, 128] + }, + { + "name": "Light Magenta", + "value": [255, 180, 210] + } + ], + "interface_tab_flags": 127, + "show_alarms": 0, + "remotes": 0, + "unknown_at_offset_09": 0, + "unknown_at_offset_10": 0, + "unknown_at_offset_11": 0 + }, + "model": { + "name": "Model", + "value": "EasyTouch2 8" + }, + "equipment": { + "flags": 32828, + "list": [ + "CHLORINATOR", + "INTELLIBRITE", + "INTELLIFLO_0", + "INTELLIFLO_1", + "INTELLICHEM" + ] + }, + "sensor": { + "state": { + "name": "Controller State", + "value": 1, + "device_type": "enum", + "enum_options": ["Unknown", "Ready", "Sync", "Service"] + }, + "pool_delay": { + "name": "Pool Delay", + "value": 0 + }, + "spa_delay": { + "name": "Spa Delay", + "value": 0 + }, + "cleaner_delay": { + "name": "Cleaner Delay", + "value": 0 + }, + "air_temperature": { + "name": "Air Temperature", + "value": 69, + "unit": "\u00b0F", + "device_type": "temperature", + "state_type": "measurement" + }, + "orp": { + "name": "ORP", + "value": 728, + "unit": "mV", + "state_type": "measurement" + }, + "saturation": { + "name": "Saturation Index", + "value": 0.06, + "unit": "lsi", + "state_type": "measurement" + }, + "salt_ppm": { + "name": "Salt", + "value": 0, + "unit": "ppm", + "state_type": "measurement" + }, + "ph_supply_level": { + "name": "pH Supply Level", + "value": 2, + "state_type": "measurement" + }, + "orp_supply_level": { + "name": "ORP Supply Level", + "value": 3, + "state_type": "measurement" + }, + "active_alert": { + "name": "Active Alert", + "value": 0, + "device_type": "alarm" + } + } + }, + "circuit": { + "500": { + "circuit_id": 500, + "name": "Spa", + "configuration": { + "name_index": 71, + "flags": 1, + "default_runtime": 720, + "unknown_at_offset_62": 0, + "unknown_at_offset_63": 0, + "delay": 0 + }, + "function": 1, + "interface": 1, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 1, + "value": 0 + }, + "501": { + "circuit_id": 501, + "name": "Waterfall", + "configuration": { + "name_index": 85, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_94": 0, + "unknown_at_offset_95": 0, + "delay": 0 + }, + "function": 0, + "interface": 2, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 2, + "value": 0 + }, + "502": { + "circuit_id": 502, + "name": "Pool Light", + "configuration": { + "name_index": 62, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_126": 0, + "unknown_at_offset_127": 0, + "delay": 0 + }, + "function": 16, + "interface": 3, + "color": { + "color_set": 2, + "color_position": 0, + "color_stagger": 2 + }, + "device_id": 3, + "value": 0 + }, + "503": { + "circuit_id": 503, + "name": "Spa Light", + "configuration": { + "name_index": 73, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_158": 0, + "unknown_at_offset_159": 0, + "delay": 0 + }, + "function": 16, + "interface": 3, + "color": { + "color_set": 6, + "color_position": 1, + "color_stagger": 10 + }, + "device_id": 4, + "value": 0 + }, + "504": { + "circuit_id": 504, + "name": "Cleaner", + "configuration": { + "name_index": 21, + "flags": 0, + "default_runtime": 240, + "unknown_at_offset_186": 0, + "unknown_at_offset_187": 0, + "delay": 0 + }, + "function": 5, + "interface": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 5, + "value": 0 + }, + "505": { + "circuit_id": 505, + "name": "Pool Low", + "configuration": { + "name_index": 63, + "flags": 1, + "default_runtime": 720, + "unknown_at_offset_214": 0, + "unknown_at_offset_215": 0, + "delay": 0 + }, + "function": 2, + "interface": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 6, + "value": 0 + }, + "506": { + "circuit_id": 506, + "name": "Yard Light", + "configuration": { + "name_index": 91, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_246": 0, + "unknown_at_offset_247": 0, + "delay": 0 + }, + "function": 7, + "interface": 4, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 7, + "value": 0 + }, + "507": { + "circuit_id": 507, + "name": "Cameras", + "configuration": { + "name_index": 101, + "flags": 0, + "default_runtime": 1620, + "unknown_at_offset_274": 0, + "unknown_at_offset_275": 0, + "delay": 0 + }, + "function": 0, + "interface": 2, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 8, + "value": 1 + }, + "508": { + "circuit_id": 508, + "name": "Pool High", + "configuration": { + "name_index": 61, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_306": 0, + "unknown_at_offset_307": 0, + "delay": 0 + }, + "function": 0, + "interface": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 9, + "value": 0 + }, + "510": { + "circuit_id": 510, + "name": "Spillway", + "configuration": { + "name_index": 78, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_334": 0, + "unknown_at_offset_335": 0, + "delay": 0 + }, + "function": 14, + "interface": 1, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 11, + "value": 0 + }, + "511": { + "circuit_id": 511, + "name": "Pool High", + "configuration": { + "name_index": 61, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_366": 0, + "unknown_at_offset_367": 0, + "delay": 0 + }, + "function": 0, + "interface": 5, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 12, + "value": 0 + } + }, + "pump": { + "0": { + "data": 70, + "type": 3, + "state": { + "name": "Pool Low Pump", + "value": 0 + }, + "watts_now": { + "name": "Pool Low Pump Watts Now", + "value": 0, + "unit": "W", + "device_type": "power", + "state_type": "measurement" + }, + "rpm_now": { + "name": "Pool Low Pump RPM Now", + "value": 0, + "unit": "rpm", + "state_type": "measurement" + }, + "unknown_at_offset_16": 0, + "unknown_at_offset_24": 255, + "preset": { + "0": { + "device_id": 6, + "setpoint": 63, + "is_rpm": 0 + }, + "1": { + "device_id": 9, + "setpoint": 72, + "is_rpm": 0 + }, + "2": { + "device_id": 1, + "setpoint": 3450, + "is_rpm": 1 + }, + "3": { + "device_id": 130, + "setpoint": 75, + "is_rpm": 0 + }, + "4": { + "device_id": 12, + "setpoint": 72, + "is_rpm": 0 + }, + "5": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "6": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "7": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + } + } + }, + "1": { + "data": 66, + "type": 3, + "state": { + "name": "Waterfall Pump", + "value": 0 + }, + "watts_now": { + "name": "Waterfall Pump Watts Now", + "value": 0, + "unit": "W", + "device_type": "power", + "state_type": "measurement" + }, + "rpm_now": { + "name": "Waterfall Pump RPM Now", + "value": 0, + "unit": "rpm", + "state_type": "measurement" + }, + "unknown_at_offset_16": 0, + "gpm_now": { + "name": "Waterfall Pump GPM Now", + "value": 0, + "unit": "gpm", + "state_type": "measurement" + }, + "unknown_at_offset_24": 255, + "preset": { + "0": { + "device_id": 2, + "setpoint": 2700, + "is_rpm": 1 + }, + "1": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "2": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "3": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "4": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "5": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "6": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "7": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + } + } + }, + "2": { + "data": 0 + }, + "3": { + "data": 0 + }, + "4": { + "data": 0 + }, + "5": { + "data": 0 + }, + "6": { + "data": 0 + }, + "7": { + "data": 0 + } + }, + "body": { + "0": { + "body_type": 0, + "min_setpoint": 40, + "max_setpoint": 104, + "name": "Pool", + "last_temperature": { + "name": "Last Pool Temperature", + "value": 81, + "unit": "\u00b0F", + "device_type": "temperature", + "state_type": "measurement" + }, + "heat_state": { + "name": "Pool Heat", + "value": 0, + "device_type": "enum", + "enum_options": ["Off", "Solar", "Heater", "Both"] + }, + "heat_setpoint": { + "name": "Pool Heat Set Point", + "value": 83, + "unit": "\u00b0F", + "device_type": "temperature" + }, + "cool_setpoint": { + "name": "Pool Cool Set Point", + "value": 100, + "unit": "\u00b0F", + "device_type": "temperature" + }, + "heat_mode": { + "name": "Pool Heat Mode", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Solar Preferred", + "Heater", + "Don't Change" + ] + } + }, + "1": { + "body_type": 1, + "min_setpoint": 40, + "max_setpoint": 104, + "name": "Spa", + "last_temperature": { + "name": "Last Spa Temperature", + "value": 84, + "unit": "\u00b0F", + "device_type": "temperature", + "state_type": "measurement" + }, + "heat_state": { + "name": "Spa Heat", + "value": 0, + "device_type": "enum", + "enum_options": ["Off", "Solar", "Heater", "Both"] + }, + "heat_setpoint": { + "name": "Spa Heat Set Point", + "value": 94, + "unit": "\u00b0F", + "device_type": "temperature" + }, + "cool_setpoint": { + "name": "Spa Cool Set Point", + "value": 69, + "unit": "\u00b0F", + "device_type": "temperature" + }, + "heat_mode": { + "name": "Spa Heat Mode", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Solar Preferred", + "Heater", + "Don't Change" + ] + } + } + }, + "intellichem": { + "unknown_at_offset_00": 42, + "unknown_at_offset_04": 0, + "sensor": { + "orp_now": { + "name": "ORP Now", + "value": 0, + "unit": "mV", + "state_type": "measurement" + }, + "ph_supply_level": { + "name": "pH Supply Level", + "value": 2, + "state_type": "measurement" + }, + "orp_supply_level": { + "name": "ORP Supply Level", + "value": 3, + "state_type": "measurement" + }, + "saturation": { + "name": "Saturation Index", + "value": 0.06, + "unit": "lsi", + "state_type": "measurement" + }, + "ph_probe_water_temp": { + "name": "pH Probe Water Temperature", + "value": 81, + "unit": "\u00b0F", + "device_type": "temperature", + "state_type": "measurement" + } + }, + "configuration": { + "ph_setpoint": { + "name": "pH Setpoint", + "value": 7.6, + "unit": "pH" + }, + "orp_setpoint": { + "name": "ORP Setpoint", + "value": 720, + "unit": "mV" + }, + "calcium_harness": { + "name": "Calcium Hardness", + "value": 800, + "unit": "ppm" + }, + "cya": { + "name": "Cyanuric Acid", + "value": 45, + "unit": "ppm" + }, + "total_alkalinity": { + "name": "Total Alkalinity", + "value": 45, + "unit": "ppm" + }, + "salt_tds_ppm": { + "name": "Salt/TDS", + "value": 1000, + "unit": "ppm" + }, + "probe_is_celsius": 0, + "flags": 32 + }, + "dose_status": { + "ph_last_dose_time": { + "name": "Last pH Dose Time", + "value": 5, + "unit": "sec", + "device_type": "duration", + "state_type": "total_increasing" + }, + "orp_last_dose_time": { + "name": "Last ORP Dose Time", + "value": 4, + "unit": "sec", + "device_type": "duration", + "state_type": "total_increasing" + }, + "ph_last_dose_volume": { + "name": "Last pH Dose Volume", + "value": 8, + "unit": "mL", + "device_type": "volume", + "state_type": "total_increasing" + }, + "orp_last_dose_volume": { + "name": "Last ORP Dose Volume", + "value": 8, + "unit": "mL", + "device_type": "volume", + "state_type": "total_increasing" + }, + "flags": 149, + "ph_dosing_state": { + "name": "pH Dosing State", + "value": 1, + "device_type": "enum", + "enum_options": ["Dosing", "Mixing", "Monitoring"] + }, + "orp_dosing_state": { + "name": "ORP Dosing State", + "value": 2, + "device_type": "enum", + "enum_options": ["Dosing", "Mixing", "Monitoring"] + } + }, + "alarm": { + "flags": 1, + "flow_alarm": { + "name": "Flow Alarm", + "value": 1, + "device_type": "alarm" + }, + "ph_high_alarm": { + "name": "pH HIGH Alarm", + "value": 0, + "device_type": "alarm" + }, + "ph_low_alarm": { + "name": "pH LOW Alarm", + "value": 0, + "device_type": "alarm" + }, + "orp_high_alarm": { + "name": "ORP HIGH Alarm", + "value": 0, + "device_type": "alarm" + }, + "orp_low_alarm": { + "name": "ORP LOW Alarm", + "value": 0, + "device_type": "alarm" + }, + "ph_supply_alarm": { + "name": "pH Supply Alarm", + "value": 0, + "device_type": "alarm" + }, + "orp_supply_alarm": { + "name": "ORP Supply Alarm", + "value": 0, + "device_type": "alarm" + }, + "probe_fault_alarm": { + "name": "Probe Fault", + "value": 0, + "device_type": "alarm" + } + }, + "alert": { + "flags": 0, + "ph_lockout": { + "name": "pH Lockout", + "value": 0 + }, + "ph_limit": { + "name": "pH Dose Limit Reached", + "value": 0 + }, + "orp_limit": { + "name": "ORP Dose Limit Reached", + "value": 0 + } + }, + "firmware": { + "name": "IntelliChem Firmware", + "value": "1.060" + }, + "water_balance": { + "flags": 0, + "corrosive": { + "name": "SI Corrosive", + "value": 0, + "device_type": "alarm" + }, + "scaling": { + "name": "SI Scaling", + "value": 0, + "device_type": "alarm" + } + }, + "unknown_at_offset_44": 0, + "unknown_at_offset_45": 0, + "unknown_at_offset_46": 0 + }, + "scg": { + "scg_present": 1, + "sensor": { + "state": { + "name": "Chlorinator", + "value": 0 + }, + "salt_ppm": { + "name": "Chlorinator Salt", + "value": 0, + "unit": "ppm", + "state_type": "measurement" + } + }, + "configuration": { + "pool_setpoint": { + "name": "Pool Chlorinator Setpoint", + "value": 50, + "unit": "%", + "min_setpoint": 0, + "max_setpoint": 100, + "step": 5, + "body_type": 0 + }, + "super_chlor_timer": { + "name": "Super Chlorination Timer", + "value": 0, + "unit": "hr", + "min_setpoint": 1, + "max_setpoint": 72, + "step": 1 + } + }, + "flags": 0 + } +} diff --git a/tests/components/screenlogic/test_data.py b/tests/components/screenlogic/test_data.py index 9686dc81586..ead064f7d93 100644 --- a/tests/components/screenlogic/test_data.py +++ b/tests/components/screenlogic/test_data.py @@ -1,12 +1,9 @@ """Tests for ScreenLogic integration data processing.""" from unittest.mock import DEFAULT, patch -import pytest from screenlogicpy import ScreenLogicGateway -from screenlogicpy.const.data import ATTR, DEVICE, GROUP, VALUE from homeassistant.components.screenlogic import DOMAIN -from homeassistant.components.screenlogic.data import PathPart, realize_path_template from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -71,21 +68,3 @@ async def test_async_cleanup_entries( deleted_entity = entity_registry.async_get(unused_entity.entity_id) assert deleted_entity is None - - -def test_realize_path_templates() -> None: - """Test path template realization.""" - assert realize_path_template( - (PathPart.DEVICE, PathPart.INDEX), (DEVICE.PUMP, 0, VALUE.WATTS_NOW) - ) == (DEVICE.PUMP, 0) - - assert realize_path_template( - (PathPart.DEVICE, PathPart.INDEX, PathPart.VALUE, ATTR.NAME_INDEX), - (DEVICE.CIRCUIT, 500, GROUP.CONFIGURATION), - ) == (DEVICE.CIRCUIT, 500, GROUP.CONFIGURATION, ATTR.NAME_INDEX) - - with pytest.raises(KeyError): - realize_path_template( - (PathPart.DEVICE, PathPart.KEY, ATTR.VALUE), - (DEVICE.ADAPTER, VALUE.FIRMWARE), - ) diff --git a/tests/components/screenlogic/test_init.py b/tests/components/screenlogic/test_init.py index 3b99354a1df..cf0a7ef3f38 100644 --- a/tests/components/screenlogic/test_init.py +++ b/tests/components/screenlogic/test_init.py @@ -6,6 +6,7 @@ import pytest from screenlogicpy import ScreenLogicGateway from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN from homeassistant.components.screenlogic import DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.core import HomeAssistant @@ -14,6 +15,7 @@ from homeassistant.util import slugify from . import ( DATA_MIN_MIGRATION, + DATA_MISSING_VALUES_CHEM_CHLOR, GATEWAY_DISCOVERY_IMPORT_PATH, MOCK_ADAPTER_MAC, MOCK_ADAPTER_NAME, @@ -77,6 +79,13 @@ TEST_MIGRATING_ENTITIES = [ "old_sensor", SENSOR_DOMAIN, ), + EntityMigrationData( + "Pump Sensor Missing Index", + "currentWatts", + "Pump Sensor Missing Index", + "currentWatts", + SENSOR_DOMAIN, + ), ] MIGRATION_CONNECT = lambda *args, **kwargs: stub_async_connect( @@ -234,3 +243,37 @@ async def test_entity_migration_data( entity_not_migrated = entity_registry.async_get(old_eid) assert entity_not_migrated == original_entity + + +async def test_platform_setup( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test setup for platforms that define expected data.""" + stub_connect = lambda *args, **kwargs: stub_async_connect( + DATA_MISSING_VALUES_CHEM_CHLOR, *args, **kwargs + ) + + device_prefix = slugify(MOCK_ADAPTER_NAME) + + tested_entity_ids = [ + f"{BINARY_SENSOR_DOMAIN}.{device_prefix}_active_alert", + f"{SENSOR_DOMAIN}.{device_prefix}_air_temperature", + f"{NUMBER_DOMAIN}.{device_prefix}_pool_chlorinator_setpoint", + ] + + mock_config_entry.add_to_hass(hass) + + with patch( + GATEWAY_DISCOVERY_IMPORT_PATH, + return_value={}, + ), patch.multiple( + ScreenLogicGateway, + async_connect=stub_connect, + is_connected=True, + _async_connected_request=DEFAULT, + ): + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + for entity_id in tested_entity_ids: + assert hass.states.get(entity_id) is not None