Explicitly define ScreenLogic entity descriptions (#100173)

This commit is contained in:
Kevin Worrel 2023-09-25 04:26:26 -07:00 committed by GitHub
parent 2ae07096d2
commit 77007ef091
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 3144 additions and 681 deletions

View File

@ -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))

View File

@ -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)

View File

@ -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

View File

@ -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": {

View File

@ -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,
)
)

View File

@ -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

View File

@ -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

View File

@ -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)
)

View File

@ -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

View File

@ -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
):

View File

@ -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(

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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),
)

View File

@ -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