Bump screenlogicpy to v0.9.0 (#92475)
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
8de3945bd4
commit
092580a3ed
28 changed files with 3821 additions and 652 deletions
|
@ -1071,9 +1071,10 @@ omit =
|
|||
homeassistant/components/saj/sensor.py
|
||||
homeassistant/components/satel_integra/*
|
||||
homeassistant/components/schluter/*
|
||||
homeassistant/components/screenlogic/__init__.py
|
||||
homeassistant/components/screenlogic/binary_sensor.py
|
||||
homeassistant/components/screenlogic/climate.py
|
||||
homeassistant/components/screenlogic/coordinator.py
|
||||
homeassistant/components/screenlogic/const.py
|
||||
homeassistant/components/screenlogic/entity.py
|
||||
homeassistant/components/screenlogic/light.py
|
||||
homeassistant/components/screenlogic/number.py
|
||||
|
|
|
@ -1,27 +1,22 @@
|
|||
"""The Screenlogic integration."""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from screenlogicpy import ScreenLogicError, ScreenLogicGateway
|
||||
from screenlogicpy.const import (
|
||||
DATA as SL_DATA,
|
||||
EQUIPMENT,
|
||||
SL_GATEWAY_IP,
|
||||
SL_GATEWAY_NAME,
|
||||
SL_GATEWAY_PORT,
|
||||
)
|
||||
from screenlogicpy.const.data import SHARED_VALUES
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, CONF_SCAN_INTERVAL, Platform
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.debounce import Debouncer
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from .config_flow import async_discover_gateways_by_unique_id, name_for_mac
|
||||
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN
|
||||
from .const import DOMAIN
|
||||
from .coordinator import ScreenlogicDataUpdateCoordinator, async_get_connect_info
|
||||
from .data import ENTITY_MIGRATIONS
|
||||
from .services import async_load_screenlogic_services, async_unload_screenlogic_services
|
||||
from .util import generate_unique_id
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -44,12 +39,16 @@ PLATFORMS = [
|
|||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Screenlogic from a config entry."""
|
||||
|
||||
await _async_migrate_entries(hass, entry)
|
||||
|
||||
gateway = ScreenLogicGateway()
|
||||
|
||||
connect_info = await async_get_connect_info(hass, entry)
|
||||
|
||||
try:
|
||||
await gateway.async_connect(**connect_info)
|
||||
await gateway.async_update()
|
||||
except ScreenLogicError as ex:
|
||||
raise ConfigEntryNotReady(ex.msg) from ex
|
||||
|
||||
|
@ -88,83 +87,88 @@ async def async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None
|
|||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
|
||||
async def async_get_connect_info(
|
||||
hass: HomeAssistant, entry: ConfigEntry
|
||||
) -> dict[str, str | int]:
|
||||
"""Construct connect_info from configuration entry and returns it to caller."""
|
||||
mac = entry.unique_id
|
||||
# Attempt to rediscover gateway to follow IP changes
|
||||
discovered_gateways = await async_discover_gateways_by_unique_id(hass)
|
||||
if mac in discovered_gateways:
|
||||
return discovered_gateways[mac]
|
||||
async def _async_migrate_entries(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry
|
||||
) -> None:
|
||||
"""Migrate to new entity names."""
|
||||
entity_registry = er.async_get(hass)
|
||||
|
||||
_LOGGER.warning("Gateway rediscovery failed")
|
||||
# Static connection defined or fallback from discovery
|
||||
return {
|
||||
SL_GATEWAY_NAME: name_for_mac(mac),
|
||||
SL_GATEWAY_IP: entry.data[CONF_IP_ADDRESS],
|
||||
SL_GATEWAY_PORT: entry.data[CONF_PORT],
|
||||
}
|
||||
for entry in er.async_entries_for_config_entry(
|
||||
entity_registry, config_entry.entry_id
|
||||
):
|
||||
source_mac, source_key = entry.unique_id.split("_", 1)
|
||||
|
||||
source_index = None
|
||||
if (
|
||||
len(key_parts := source_key.rsplit("_", 1)) == 2
|
||||
and key_parts[1].isdecimal()
|
||||
):
|
||||
source_key, source_index = key_parts
|
||||
|
||||
class ScreenlogicDataUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
"""Class to manage the data update for the Screenlogic component."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
*,
|
||||
config_entry: ConfigEntry,
|
||||
gateway: ScreenLogicGateway,
|
||||
) -> None:
|
||||
"""Initialize the Screenlogic Data Update Coordinator."""
|
||||
self.config_entry = config_entry
|
||||
self.gateway = gateway
|
||||
|
||||
interval = timedelta(
|
||||
seconds=config_entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
|
||||
)
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=DOMAIN,
|
||||
update_interval=interval,
|
||||
# Debounced option since the device takes
|
||||
# a moment to reflect the knock-on changes
|
||||
request_refresh_debouncer=Debouncer(
|
||||
hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=False
|
||||
),
|
||||
_LOGGER.debug(
|
||||
"Checking migration status for '%s' against key '%s'",
|
||||
entry.unique_id,
|
||||
source_key,
|
||||
)
|
||||
|
||||
@property
|
||||
def gateway_data(self) -> dict[str | int, Any]:
|
||||
"""Return the gateway data."""
|
||||
return self.gateway.get_data()
|
||||
if source_key not in ENTITY_MIGRATIONS:
|
||||
continue
|
||||
|
||||
async def _async_update_configured_data(self) -> None:
|
||||
"""Update data sets based on equipment config."""
|
||||
equipment_flags = self.gateway.get_data()[SL_DATA.KEY_CONFIG]["equipment_flags"]
|
||||
if not self.gateway.is_client:
|
||||
await self.gateway.async_get_status()
|
||||
if equipment_flags & EQUIPMENT.FLAG_INTELLICHEM:
|
||||
await self.gateway.async_get_chemistry()
|
||||
|
||||
await self.gateway.async_get_pumps()
|
||||
if equipment_flags & EQUIPMENT.FLAG_CHLORINATOR:
|
||||
await self.gateway.async_get_scg()
|
||||
|
||||
async def _async_update_data(self) -> None:
|
||||
"""Fetch data from the Screenlogic gateway."""
|
||||
assert self.config_entry is not None
|
||||
try:
|
||||
if not self.gateway.is_connected:
|
||||
connect_info = await async_get_connect_info(
|
||||
self.hass, self.config_entry
|
||||
_LOGGER.debug(
|
||||
"Evaluating migration of '%s' from migration key '%s'",
|
||||
entry.entity_id,
|
||||
source_key,
|
||||
)
|
||||
migrations = ENTITY_MIGRATIONS[source_key]
|
||||
updates: dict[str, Any] = {}
|
||||
new_key = migrations["new_key"]
|
||||
if new_key in SHARED_VALUES:
|
||||
if (device := migrations.get("device")) is None:
|
||||
_LOGGER.debug(
|
||||
"Shared key '%s' is missing required migration data 'device'",
|
||||
new_key,
|
||||
)
|
||||
await self.gateway.async_connect(**connect_info)
|
||||
continue
|
||||
assert device is not None and (
|
||||
device != "pump" or (device == "pump" and source_index is not None)
|
||||
)
|
||||
new_unique_id = (
|
||||
f"{source_mac}_{generate_unique_id(device, source_index, new_key)}"
|
||||
)
|
||||
else:
|
||||
new_unique_id = entry.unique_id.replace(source_key, new_key)
|
||||
|
||||
await self._async_update_configured_data()
|
||||
except ScreenLogicError as ex:
|
||||
if self.gateway.is_connected:
|
||||
await self.gateway.async_disconnect()
|
||||
raise UpdateFailed(ex.msg) from ex
|
||||
if new_unique_id and new_unique_id != entry.unique_id:
|
||||
if existing_entity_id := entity_registry.async_get_entity_id(
|
||||
entry.domain, entry.platform, new_unique_id
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"Cannot migrate '%s' to unique_id '%s', already exists for entity '%s'. Aborting",
|
||||
entry.unique_id,
|
||||
new_unique_id,
|
||||
existing_entity_id,
|
||||
)
|
||||
continue
|
||||
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))
|
||||
if new_entity_id and new_entity_id != entry.entity_id:
|
||||
updates["new_entity_id"] = new_entity_id
|
||||
|
||||
if entry.original_name and old_name in entry.original_name:
|
||||
new_original_name = entry.original_name.replace(old_name, new_name)
|
||||
if new_original_name and new_original_name != entry.original_name:
|
||||
updates["original_name"] = new_original_name
|
||||
|
||||
if updates:
|
||||
_LOGGER.debug(
|
||||
"Migrating entity '%s' unique_id from '%s' to '%s'",
|
||||
entry.entity_id,
|
||||
entry.unique_id,
|
||||
new_unique_id,
|
||||
)
|
||||
entity_registry.async_update_entity(entry.entity_id, **updates)
|
||||
|
|
|
@ -1,28 +1,97 @@
|
|||
"""Support for a ScreenLogic Binary Sensor."""
|
||||
from screenlogicpy.const import CODE, DATA as SL_DATA, DEVICE_TYPE, EQUIPMENT, ON_OFF
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
from screenlogicpy.const.common import DEVICE_TYPE, ON_OFF
|
||||
from screenlogicpy.const.data import ATTR, DEVICE, GROUP, VALUE
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DOMAIN,
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
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 . import ScreenlogicDataUpdateCoordinator
|
||||
from .const import DOMAIN
|
||||
from .entity import ScreenlogicEntity, ScreenLogicPushEntity
|
||||
from .const import DOMAIN as SL_DOMAIN, ScreenLogicDataPath
|
||||
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
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SupportedBinarySensorValueParameters(SupportedValueParameters):
|
||||
"""Supported predefined data for a ScreenLogic binary sensor entity."""
|
||||
|
||||
device_class: BinarySensorDeviceClass | None = None
|
||||
|
||||
|
||||
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(),
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
SL_DEVICE_TYPE_TO_HA_DEVICE_CLASS = {DEVICE_TYPE.ALARM: BinarySensorDeviceClass.PROBLEM}
|
||||
|
||||
SUPPORTED_CONFIG_BINARY_SENSORS = (
|
||||
"freeze_mode",
|
||||
"pool_delay",
|
||||
"spa_delay",
|
||||
"cleaner_delay",
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
|
@ -30,132 +99,92 @@ async def async_setup_entry(
|
|||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up entry."""
|
||||
entities: list[ScreenLogicBinarySensorEntity] = []
|
||||
coordinator: ScreenlogicDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
entities: list[ScreenLogicBinarySensor] = []
|
||||
coordinator: ScreenlogicDataUpdateCoordinator = hass.data[SL_DOMAIN][
|
||||
config_entry.entry_id
|
||||
]
|
||||
gateway_data = coordinator.gateway_data
|
||||
config = gateway_data[SL_DATA.KEY_CONFIG]
|
||||
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)
|
||||
|
||||
# Generic binary sensor
|
||||
entities.append(
|
||||
ScreenLogicStatusBinarySensor(coordinator, "chem_alarm", CODE.STATUS_CHANGED)
|
||||
)
|
||||
device = data_path[0]
|
||||
|
||||
entities.extend(
|
||||
[
|
||||
ScreenlogicConfigBinarySensor(coordinator, cfg_sensor, CODE.STATUS_CHANGED)
|
||||
for cfg_sensor in config
|
||||
if cfg_sensor in SUPPORTED_CONFIG_BINARY_SENSORS
|
||||
]
|
||||
)
|
||||
if not (DEVICE_INCLUSION_RULES.get(device) or value_params.included).test(
|
||||
gateway, data_path
|
||||
):
|
||||
cleanup_excluded_entity(coordinator, DOMAIN, entity_key)
|
||||
continue
|
||||
|
||||
if config["equipment_flags"] & EQUIPMENT.FLAG_INTELLICHEM:
|
||||
chemistry = gateway_data[SL_DATA.KEY_CHEMISTRY]
|
||||
# IntelliChem alarm sensors
|
||||
entities.extend(
|
||||
[
|
||||
ScreenlogicChemistryAlarmBinarySensor(
|
||||
coordinator, chem_alarm, CODE.CHEMISTRY_CHANGED
|
||||
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)
|
||||
),
|
||||
}
|
||||
|
||||
if (
|
||||
sub_code := (
|
||||
value_params.subscription_code or DEVICE_SUBSCRIPTION.get(device)
|
||||
)
|
||||
) is not None:
|
||||
entities.append(
|
||||
ScreenLogicPushBinarySensor(
|
||||
coordinator,
|
||||
ScreenLogicPushBinarySensorDescription(
|
||||
subscription_code=sub_code, **entity_description_kwargs
|
||||
),
|
||||
)
|
||||
for chem_alarm in chemistry[SL_DATA.KEY_ALERTS]
|
||||
if not chem_alarm.startswith("_")
|
||||
]
|
||||
)
|
||||
|
||||
# Intellichem notification sensors
|
||||
entities.extend(
|
||||
[
|
||||
ScreenlogicChemistryNotificationBinarySensor(
|
||||
coordinator, chem_notif, CODE.CHEMISTRY_CHANGED
|
||||
)
|
||||
else:
|
||||
entities.append(
|
||||
ScreenLogicBinarySensor(
|
||||
coordinator,
|
||||
ScreenLogicBinarySensorDescription(**entity_description_kwargs),
|
||||
)
|
||||
for chem_notif in chemistry[SL_DATA.KEY_NOTIFICATIONS]
|
||||
if not chem_notif.startswith("_")
|
||||
]
|
||||
)
|
||||
|
||||
if config["equipment_flags"] & EQUIPMENT.FLAG_CHLORINATOR:
|
||||
# SCG binary sensor
|
||||
entities.append(ScreenlogicSCGBinarySensor(coordinator, "scg_status"))
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class ScreenLogicBinarySensorEntity(ScreenlogicEntity, BinarySensorEntity):
|
||||
@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."""
|
||||
|
||||
entity_description: ScreenLogicBinarySensorDescription
|
||||
_attr_has_entity_name = True
|
||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
|
||||
@property
|
||||
def name(self) -> str | None:
|
||||
"""Return the sensor name."""
|
||||
return self.sensor["name"]
|
||||
|
||||
@property
|
||||
def device_class(self) -> BinarySensorDeviceClass | None:
|
||||
"""Return the device class."""
|
||||
device_type = self.sensor.get("device_type")
|
||||
return SL_DEVICE_TYPE_TO_HA_DEVICE_CLASS.get(device_type)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Determine if the sensor is on."""
|
||||
return self.sensor["value"] == ON_OFF.ON
|
||||
|
||||
@property
|
||||
def sensor(self) -> dict:
|
||||
"""Shortcut to access the sensor data."""
|
||||
return self.gateway_data[SL_DATA.KEY_SENSORS][self._data_key]
|
||||
return self.entity_data[ATTR.VALUE] == ON_OFF.ON
|
||||
|
||||
|
||||
class ScreenLogicStatusBinarySensor(
|
||||
ScreenLogicBinarySensorEntity, ScreenLogicPushEntity
|
||||
@dataclass
|
||||
class ScreenLogicPushBinarySensorDescription(
|
||||
ScreenLogicBinarySensorDescription, ScreenLogicPushEntityDescription
|
||||
):
|
||||
"""Describes a ScreenLogicPushBinarySensor."""
|
||||
|
||||
|
||||
class ScreenLogicPushBinarySensor(ScreenLogicPushEntity, ScreenLogicBinarySensor):
|
||||
"""Representation of a basic ScreenLogic sensor entity."""
|
||||
|
||||
|
||||
class ScreenlogicChemistryAlarmBinarySensor(
|
||||
ScreenLogicBinarySensorEntity, ScreenLogicPushEntity
|
||||
):
|
||||
"""Representation of a ScreenLogic IntelliChem alarm binary sensor entity."""
|
||||
|
||||
@property
|
||||
def sensor(self) -> dict:
|
||||
"""Shortcut to access the sensor data."""
|
||||
return self.gateway_data[SL_DATA.KEY_CHEMISTRY][SL_DATA.KEY_ALERTS][
|
||||
self._data_key
|
||||
]
|
||||
|
||||
|
||||
class ScreenlogicChemistryNotificationBinarySensor(
|
||||
ScreenLogicBinarySensorEntity, ScreenLogicPushEntity
|
||||
):
|
||||
"""Representation of a ScreenLogic IntelliChem notification binary sensor entity."""
|
||||
|
||||
@property
|
||||
def sensor(self) -> dict:
|
||||
"""Shortcut to access the sensor data."""
|
||||
return self.gateway_data[SL_DATA.KEY_CHEMISTRY][SL_DATA.KEY_NOTIFICATIONS][
|
||||
self._data_key
|
||||
]
|
||||
|
||||
|
||||
class ScreenlogicSCGBinarySensor(ScreenLogicBinarySensorEntity):
|
||||
"""Representation of a ScreenLogic SCG binary sensor entity."""
|
||||
|
||||
@property
|
||||
def sensor(self) -> dict:
|
||||
"""Shortcut to access the sensor data."""
|
||||
return self.gateway_data[SL_DATA.KEY_SCG][self._data_key]
|
||||
|
||||
|
||||
class ScreenlogicConfigBinarySensor(
|
||||
ScreenLogicBinarySensorEntity, ScreenLogicPushEntity
|
||||
):
|
||||
"""Representation of a ScreenLogic config data binary sensor entity."""
|
||||
|
||||
@property
|
||||
def sensor(self) -> dict:
|
||||
"""Shortcut to access the sensor data."""
|
||||
return self.gateway_data[SL_DATA.KEY_CONFIG][self._data_key]
|
||||
entity_description: ScreenLogicPushBinarySensorDescription
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
"""Support for a ScreenLogic heating device."""
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from screenlogicpy.const import CODE, DATA as SL_DATA, EQUIPMENT, HEAT_MODE
|
||||
from screenlogicpy.const.common import UNIT
|
||||
from screenlogicpy.const.data import ATTR, DEVICE, VALUE
|
||||
from screenlogicpy.const.msg import CODE
|
||||
from screenlogicpy.device_const.heat import HEAT_MODE
|
||||
from screenlogicpy.device_const.system import EQUIPMENT_FLAG
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_PRESET_MODE,
|
||||
ClimateEntity,
|
||||
ClimateEntityDescription,
|
||||
ClimateEntityFeature,
|
||||
HVACAction,
|
||||
HVACMode,
|
||||
|
@ -18,9 +24,9 @@ from homeassistant.exceptions import HomeAssistantError
|
|||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
|
||||
from . import ScreenlogicDataUpdateCoordinator
|
||||
from .const import DOMAIN
|
||||
from .entity import ScreenLogicPushEntity
|
||||
from .const import DOMAIN as SL_DOMAIN
|
||||
from .coordinator import ScreenlogicDataUpdateCoordinator
|
||||
from .entity import ScreenLogicPushEntity, ScreenLogicPushEntityDescription
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -41,81 +47,88 @@ async def async_setup_entry(
|
|||
) -> None:
|
||||
"""Set up entry."""
|
||||
entities = []
|
||||
coordinator: ScreenlogicDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
coordinator: ScreenlogicDataUpdateCoordinator = hass.data[SL_DOMAIN][
|
||||
config_entry.entry_id
|
||||
]
|
||||
|
||||
for body in coordinator.gateway_data[SL_DATA.KEY_BODIES]:
|
||||
entities.append(ScreenLogicClimate(coordinator, body))
|
||||
gateway = coordinator.gateway
|
||||
|
||||
for body_index, body_data in gateway.get_data(DEVICE.BODY).items():
|
||||
body_path = (DEVICE.BODY, body_index)
|
||||
entities.append(
|
||||
ScreenLogicClimate(
|
||||
coordinator,
|
||||
ScreenLogicClimateDescription(
|
||||
subscription_code=CODE.STATUS_CHANGED,
|
||||
data_path=body_path,
|
||||
key=body_index,
|
||||
name=body_data[VALUE.HEAT_STATE][ATTR.NAME],
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScreenLogicClimateDescription(
|
||||
ClimateEntityDescription, ScreenLogicPushEntityDescription
|
||||
):
|
||||
"""Describes a ScreenLogic climate entity."""
|
||||
|
||||
|
||||
class ScreenLogicClimate(ScreenLogicPushEntity, ClimateEntity, RestoreEntity):
|
||||
"""Represents a ScreenLogic climate entity."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
entity_description: ScreenLogicClimateDescription
|
||||
_attr_hvac_modes = SUPPORTED_MODES
|
||||
_attr_supported_features = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE
|
||||
)
|
||||
|
||||
def __init__(self, coordinator, body):
|
||||
def __init__(self, coordinator, entity_description) -> None:
|
||||
"""Initialize a ScreenLogic climate entity."""
|
||||
super().__init__(coordinator, body, CODE.STATUS_CHANGED)
|
||||
super().__init__(coordinator, entity_description)
|
||||
self._configured_heat_modes = []
|
||||
# Is solar listed as available equipment?
|
||||
if self.gateway_data["config"]["equipment_flags"] & EQUIPMENT.FLAG_SOLAR:
|
||||
if EQUIPMENT_FLAG.SOLAR in self.gateway.equipment_flags:
|
||||
self._configured_heat_modes.extend(
|
||||
[HEAT_MODE.SOLAR, HEAT_MODE.SOLAR_PREFERRED]
|
||||
)
|
||||
self._configured_heat_modes.append(HEAT_MODE.HEATER)
|
||||
|
||||
self._attr_min_temp = self.entity_data[ATTR.MIN_SETPOINT]
|
||||
self._attr_max_temp = self.entity_data[ATTR.MAX_SETPOINT]
|
||||
self._last_preset = None
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Name of the heater."""
|
||||
return self.body["heat_status"]["name"]
|
||||
|
||||
@property
|
||||
def min_temp(self) -> float:
|
||||
"""Minimum allowed temperature."""
|
||||
return self.body["min_set_point"]["value"]
|
||||
|
||||
@property
|
||||
def max_temp(self) -> float:
|
||||
"""Maximum allowed temperature."""
|
||||
return self.body["max_set_point"]["value"]
|
||||
|
||||
@property
|
||||
def current_temperature(self) -> float:
|
||||
"""Return water temperature."""
|
||||
return self.body["last_temperature"]["value"]
|
||||
return self.entity_data[VALUE.LAST_TEMPERATURE][ATTR.VALUE]
|
||||
|
||||
@property
|
||||
def target_temperature(self) -> float:
|
||||
"""Target temperature."""
|
||||
return self.body["heat_set_point"]["value"]
|
||||
return self.entity_data[VALUE.HEAT_SETPOINT][ATTR.VALUE]
|
||||
|
||||
@property
|
||||
def temperature_unit(self) -> str:
|
||||
"""Return the unit of measurement."""
|
||||
if self.config_data["is_celsius"]["value"] == 1:
|
||||
if self.gateway.temperature_unit == UNIT.CELSIUS:
|
||||
return UnitOfTemperature.CELSIUS
|
||||
return UnitOfTemperature.FAHRENHEIT
|
||||
|
||||
@property
|
||||
def hvac_mode(self) -> HVACMode:
|
||||
"""Return the current hvac mode."""
|
||||
if self.body["heat_mode"]["value"] > 0:
|
||||
if self.entity_data[VALUE.HEAT_MODE][ATTR.VALUE] > 0:
|
||||
return HVACMode.HEAT
|
||||
return HVACMode.OFF
|
||||
|
||||
@property
|
||||
def hvac_action(self) -> HVACAction:
|
||||
"""Return the current action of the heater."""
|
||||
if self.body["heat_status"]["value"] > 0:
|
||||
if self.entity_data[VALUE.HEAT_STATE][ATTR.VALUE] > 0:
|
||||
return HVACAction.HEATING
|
||||
if self.hvac_mode == HVACMode.HEAT:
|
||||
return HVACAction.IDLE
|
||||
|
@ -125,15 +138,13 @@ class ScreenLogicClimate(ScreenLogicPushEntity, ClimateEntity, RestoreEntity):
|
|||
def preset_mode(self) -> str:
|
||||
"""Return current/last preset mode."""
|
||||
if self.hvac_mode == HVACMode.OFF:
|
||||
return HEAT_MODE.NAME_FOR_NUM[self._last_preset]
|
||||
return HEAT_MODE.NAME_FOR_NUM[self.body["heat_mode"]["value"]]
|
||||
return HEAT_MODE(self._last_preset).title
|
||||
return HEAT_MODE(self.entity_data[VALUE.HEAT_MODE][ATTR.VALUE]).title
|
||||
|
||||
@property
|
||||
def preset_modes(self) -> list[str]:
|
||||
"""All available presets."""
|
||||
return [
|
||||
HEAT_MODE.NAME_FOR_NUM[mode_num] for mode_num in self._configured_heat_modes
|
||||
]
|
||||
return [HEAT_MODE(mode_num).title for mode_num in self._configured_heat_modes]
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Change the setpoint of the heater."""
|
||||
|
@ -145,7 +156,7 @@ class ScreenLogicClimate(ScreenLogicPushEntity, ClimateEntity, RestoreEntity):
|
|||
):
|
||||
raise HomeAssistantError(
|
||||
f"Failed to set_temperature {temperature} on body"
|
||||
f" {self.body['body_type']['value']}"
|
||||
f" {self.entity_data[ATTR.BODY_TYPE][ATTR.VALUE]}"
|
||||
)
|
||||
_LOGGER.debug("Set temperature for body %s to %s", self._data_key, temperature)
|
||||
|
||||
|
@ -154,28 +165,33 @@ class ScreenLogicClimate(ScreenLogicPushEntity, ClimateEntity, RestoreEntity):
|
|||
if hvac_mode == HVACMode.OFF:
|
||||
mode = HEAT_MODE.OFF
|
||||
else:
|
||||
mode = HEAT_MODE.NUM_FOR_NAME[self.preset_mode]
|
||||
mode = HEAT_MODE.parse(self.preset_mode)
|
||||
|
||||
if not await self.gateway.async_set_heat_mode(int(self._data_key), int(mode)):
|
||||
if not await self.gateway.async_set_heat_mode(
|
||||
int(self._data_key), int(mode.value)
|
||||
):
|
||||
raise HomeAssistantError(
|
||||
f"Failed to set_hvac_mode {mode} on body"
|
||||
f" {self.body['body_type']['value']}"
|
||||
f"Failed to set_hvac_mode {mode.name} on body"
|
||||
f" {self.entity_data[ATTR.BODY_TYPE][ATTR.VALUE]}"
|
||||
)
|
||||
_LOGGER.debug("Set hvac_mode on body %s to %s", self._data_key, mode)
|
||||
_LOGGER.debug("Set hvac_mode on body %s to %s", self._data_key, mode.name)
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set the preset mode."""
|
||||
_LOGGER.debug("Setting last_preset to %s", HEAT_MODE.NUM_FOR_NAME[preset_mode])
|
||||
self._last_preset = mode = HEAT_MODE.NUM_FOR_NAME[preset_mode]
|
||||
mode = HEAT_MODE.parse(preset_mode)
|
||||
_LOGGER.debug("Setting last_preset to %s", mode.name)
|
||||
self._last_preset = mode.value
|
||||
if self.hvac_mode == HVACMode.OFF:
|
||||
return
|
||||
|
||||
if not await self.gateway.async_set_heat_mode(int(self._data_key), int(mode)):
|
||||
if not await self.gateway.async_set_heat_mode(
|
||||
int(self._data_key), int(mode.value)
|
||||
):
|
||||
raise HomeAssistantError(
|
||||
f"Failed to set_preset_mode {mode} on body"
|
||||
f" {self.body['body_type']['value']}"
|
||||
f"Failed to set_preset_mode {mode.name} on body"
|
||||
f" {self.entity_data[ATTR.BODY_TYPE][ATTR.VALUE]}"
|
||||
)
|
||||
_LOGGER.debug("Set preset_mode on body %s to %s", self._data_key, mode)
|
||||
_LOGGER.debug("Set preset_mode on body %s to %s", self._data_key, mode.name)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Run when entity is about to be added."""
|
||||
|
@ -189,21 +205,16 @@ class ScreenLogicClimate(ScreenLogicPushEntity, ClimateEntity, RestoreEntity):
|
|||
prev_state is not None
|
||||
and prev_state.attributes.get(ATTR_PRESET_MODE) is not None
|
||||
):
|
||||
mode = HEAT_MODE.parse(prev_state.attributes.get(ATTR_PRESET_MODE))
|
||||
_LOGGER.debug(
|
||||
"Startup setting last_preset to %s from prev_state",
|
||||
HEAT_MODE.NUM_FOR_NAME[prev_state.attributes.get(ATTR_PRESET_MODE)],
|
||||
mode.name,
|
||||
)
|
||||
self._last_preset = HEAT_MODE.NUM_FOR_NAME[
|
||||
prev_state.attributes.get(ATTR_PRESET_MODE)
|
||||
]
|
||||
self._last_preset = mode.value
|
||||
else:
|
||||
mode = HEAT_MODE.parse(self._configured_heat_modes[0])
|
||||
_LOGGER.debug(
|
||||
"Startup setting last_preset to default (%s)",
|
||||
self._configured_heat_modes[0],
|
||||
mode.name,
|
||||
)
|
||||
self._last_preset = self._configured_heat_modes[0]
|
||||
|
||||
@property
|
||||
def body(self):
|
||||
"""Shortcut to access body data."""
|
||||
return self.gateway_data[SL_DATA.KEY_BODIES][self._data_key]
|
||||
self._last_preset = mode.value
|
||||
|
|
|
@ -2,9 +2,10 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from screenlogicpy import ScreenLogicError, discovery
|
||||
from screenlogicpy.const import SL_GATEWAY_IP, SL_GATEWAY_NAME, SL_GATEWAY_PORT
|
||||
from screenlogicpy.const.common import SL_GATEWAY_IP, SL_GATEWAY_NAME, SL_GATEWAY_PORT
|
||||
from screenlogicpy.requests import login
|
||||
import voluptuous as vol
|
||||
|
||||
|
@ -64,10 +65,10 @@ class ScreenlogicConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
|
||||
VERSION = 1
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
"""Initialize ScreenLogic ConfigFlow."""
|
||||
self.discovered_gateways = {}
|
||||
self.discovered_ip = None
|
||||
self.discovered_gateways: dict[str, dict[str, Any]] = {}
|
||||
self.discovered_ip: str | None = None
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
|
@ -77,7 +78,7 @@ class ScreenlogicConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
"""Get the options flow for ScreenLogic."""
|
||||
return ScreenLogicOptionsFlowHandler(config_entry)
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
async def async_step_user(self, user_input=None) -> FlowResult:
|
||||
"""Handle the start of the config flow."""
|
||||
self.discovered_gateways = await async_discover_gateways_by_unique_id(self.hass)
|
||||
return await self.async_step_gateway_select()
|
||||
|
@ -93,7 +94,7 @@ class ScreenlogicConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
self.context["title_placeholders"] = {"name": discovery_info.hostname}
|
||||
return await self.async_step_gateway_entry()
|
||||
|
||||
async def async_step_gateway_select(self, user_input=None):
|
||||
async def async_step_gateway_select(self, user_input=None) -> FlowResult:
|
||||
"""Handle the selection of a discovered ScreenLogic gateway."""
|
||||
existing = self._async_current_ids()
|
||||
unconfigured_gateways = {
|
||||
|
@ -105,7 +106,7 @@ class ScreenlogicConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
if not unconfigured_gateways:
|
||||
return await self.async_step_gateway_entry()
|
||||
|
||||
errors = {}
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
if user_input[GATEWAY_SELECT_KEY] == GATEWAY_MANUAL_ENTRY:
|
||||
return await self.async_step_gateway_entry()
|
||||
|
@ -140,9 +141,9 @@ class ScreenlogicConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
description_placeholders={},
|
||||
)
|
||||
|
||||
async def async_step_gateway_entry(self, user_input=None):
|
||||
async def async_step_gateway_entry(self, user_input=None) -> FlowResult:
|
||||
"""Handle the manual entry of a ScreenLogic gateway."""
|
||||
errors = {}
|
||||
errors: dict[str, str] = {}
|
||||
ip_address = self.discovered_ip
|
||||
port = 80
|
||||
|
||||
|
@ -186,7 +187,7 @@ class ScreenLogicOptionsFlowHandler(config_entries.OptionsFlow):
|
|||
"""Init the screen logic options flow."""
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
async def async_step_init(self, user_input=None) -> FlowResult:
|
||||
"""Manage the options."""
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(
|
||||
|
|
|
@ -1,25 +1,48 @@
|
|||
"""Constants for the ScreenLogic integration."""
|
||||
from screenlogicpy.const import CIRCUIT_FUNCTION, COLOR_MODE
|
||||
from screenlogicpy.const.common import UNIT
|
||||
from screenlogicpy.device_const.circuit import FUNCTION
|
||||
from screenlogicpy.device_const.system import COLOR_MODE
|
||||
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
PERCENTAGE,
|
||||
REVOLUTIONS_PER_MINUTE,
|
||||
UnitOfElectricPotential,
|
||||
UnitOfPower,
|
||||
UnitOfTemperature,
|
||||
UnitOfTime,
|
||||
)
|
||||
from homeassistant.util import slugify
|
||||
|
||||
ScreenLogicDataPath = tuple[str | int, ...]
|
||||
|
||||
DOMAIN = "screenlogic"
|
||||
DEFAULT_SCAN_INTERVAL = 30
|
||||
MIN_SCAN_INTERVAL = 10
|
||||
|
||||
SERVICE_SET_COLOR_MODE = "set_color_mode"
|
||||
ATTR_COLOR_MODE = "color_mode"
|
||||
SUPPORTED_COLOR_MODES = {
|
||||
slugify(name): num for num, name in COLOR_MODE.NAME_FOR_NUM.items()
|
||||
}
|
||||
SUPPORTED_COLOR_MODES = {slugify(cm.name): cm.value for cm in COLOR_MODE}
|
||||
|
||||
LIGHT_CIRCUIT_FUNCTIONS = {
|
||||
CIRCUIT_FUNCTION.COLOR_WHEEL,
|
||||
CIRCUIT_FUNCTION.DIMMER,
|
||||
CIRCUIT_FUNCTION.INTELLIBRITE,
|
||||
CIRCUIT_FUNCTION.LIGHT,
|
||||
CIRCUIT_FUNCTION.MAGICSTREAM,
|
||||
CIRCUIT_FUNCTION.PHOTONGEN,
|
||||
CIRCUIT_FUNCTION.SAL_LIGHT,
|
||||
CIRCUIT_FUNCTION.SAM_LIGHT,
|
||||
FUNCTION.COLOR_WHEEL,
|
||||
FUNCTION.DIMMER,
|
||||
FUNCTION.INTELLIBRITE,
|
||||
FUNCTION.LIGHT,
|
||||
FUNCTION.MAGICSTREAM,
|
||||
FUNCTION.PHOTONGEN,
|
||||
FUNCTION.SAL_LIGHT,
|
||||
FUNCTION.SAM_LIGHT,
|
||||
}
|
||||
|
||||
SL_UNIT_TO_HA_UNIT = {
|
||||
UNIT.CELSIUS: UnitOfTemperature.CELSIUS,
|
||||
UNIT.FAHRENHEIT: UnitOfTemperature.FAHRENHEIT,
|
||||
UNIT.MILLIVOLT: UnitOfElectricPotential.MILLIVOLT,
|
||||
UNIT.WATT: UnitOfPower.WATT,
|
||||
UNIT.HOUR: UnitOfTime.HOURS,
|
||||
UNIT.SECOND: UnitOfTime.SECONDS,
|
||||
UNIT.REVOLUTIONS_PER_MINUTE: REVOLUTIONS_PER_MINUTE,
|
||||
UNIT.PARTS_PER_MILLION: CONCENTRATION_PARTS_PER_MILLION,
|
||||
UNIT.PERCENT: PERCENTAGE,
|
||||
}
|
||||
|
|
97
homeassistant/components/screenlogic/coordinator.py
Normal file
97
homeassistant/components/screenlogic/coordinator.py
Normal file
|
@ -0,0 +1,97 @@
|
|||
"""ScreenlogicDataUpdateCoordinator definition."""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from screenlogicpy import ScreenLogicError, ScreenLogicGateway
|
||||
from screenlogicpy.const.common import SL_GATEWAY_IP, SL_GATEWAY_NAME, SL_GATEWAY_PORT
|
||||
from screenlogicpy.device_const.system import EQUIPMENT_FLAG
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, CONF_SCAN_INTERVAL
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.debounce import Debouncer
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .config_flow import async_discover_gateways_by_unique_id, name_for_mac
|
||||
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUEST_REFRESH_DELAY = 2
|
||||
HEATER_COOLDOWN_DELAY = 6
|
||||
|
||||
|
||||
async def async_get_connect_info(
|
||||
hass: HomeAssistant, entry: ConfigEntry
|
||||
) -> dict[str, str | int]:
|
||||
"""Construct connect_info from configuration entry and returns it to caller."""
|
||||
mac = entry.unique_id
|
||||
# Attempt to rediscover gateway to follow IP changes
|
||||
discovered_gateways = await async_discover_gateways_by_unique_id(hass)
|
||||
if mac in discovered_gateways:
|
||||
return discovered_gateways[mac]
|
||||
|
||||
_LOGGER.debug("Gateway rediscovery failed for %s", entry.title)
|
||||
# Static connection defined or fallback from discovery
|
||||
return {
|
||||
SL_GATEWAY_NAME: name_for_mac(mac),
|
||||
SL_GATEWAY_IP: entry.data[CONF_IP_ADDRESS],
|
||||
SL_GATEWAY_PORT: entry.data[CONF_PORT],
|
||||
}
|
||||
|
||||
|
||||
class ScreenlogicDataUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
"""Class to manage the data update for the Screenlogic component."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
*,
|
||||
config_entry: ConfigEntry,
|
||||
gateway: ScreenLogicGateway,
|
||||
) -> None:
|
||||
"""Initialize the Screenlogic Data Update Coordinator."""
|
||||
self.config_entry = config_entry
|
||||
self.gateway = gateway
|
||||
|
||||
interval = timedelta(
|
||||
seconds=config_entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
|
||||
)
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=DOMAIN,
|
||||
update_interval=interval,
|
||||
# Debounced option since the device takes
|
||||
# a moment to reflect the knock-on changes
|
||||
request_refresh_debouncer=Debouncer(
|
||||
hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=False
|
||||
),
|
||||
)
|
||||
|
||||
async def _async_update_configured_data(self) -> None:
|
||||
"""Update data sets based on equipment config."""
|
||||
if not self.gateway.is_client:
|
||||
await self.gateway.async_get_status()
|
||||
if EQUIPMENT_FLAG.INTELLICHEM in self.gateway.equipment_flags:
|
||||
await self.gateway.async_get_chemistry()
|
||||
|
||||
await self.gateway.async_get_pumps()
|
||||
if EQUIPMENT_FLAG.CHLORINATOR in self.gateway.equipment_flags:
|
||||
await self.gateway.async_get_scg()
|
||||
|
||||
async def _async_update_data(self) -> None:
|
||||
"""Fetch data from the Screenlogic gateway."""
|
||||
assert self.config_entry is not None
|
||||
try:
|
||||
if not self.gateway.is_connected:
|
||||
connect_info = await async_get_connect_info(
|
||||
self.hass, self.config_entry
|
||||
)
|
||||
await self.gateway.async_connect(**connect_info)
|
||||
|
||||
await self._async_update_configured_data()
|
||||
except ScreenLogicError as ex:
|
||||
if self.gateway.is_connected:
|
||||
await self.gateway.async_disconnect()
|
||||
raise UpdateFailed(ex.msg) from ex
|
304
homeassistant/components/screenlogic/data.py
Normal file
304
homeassistant/components/screenlogic/data.py
Normal file
|
@ -0,0 +1,304 @@
|
|||
"""Support for configurable supported data values for the ScreenLogic integration."""
|
||||
from collections.abc import Callable, Generator
|
||||
from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
from typing import Any
|
||||
|
||||
from screenlogicpy import ScreenLogicGateway
|
||||
from screenlogicpy.const.data import ATTR, DEVICE, VALUE
|
||||
from screenlogicpy.const.msg import CODE
|
||||
from screenlogicpy.device_const.system import EQUIPMENT_FLAG
|
||||
|
||||
from homeassistant.const import EntityCategory
|
||||
|
||||
from .const import SL_UNIT_TO_HA_UNIT, ScreenLogicDataPath
|
||||
|
||||
|
||||
class PathPart(StrEnum):
|
||||
"""Placeholders for local data_path values."""
|
||||
|
||||
DEVICE = "!device"
|
||||
KEY = "!key"
|
||||
INDEX = "!index"
|
||||
VALUE = "!sensor"
|
||||
|
||||
|
||||
ScreenLogicDataPathTemplate = tuple[PathPart | str | int, ...]
|
||||
|
||||
|
||||
class ScreenLogicRule:
|
||||
"""Represents a base default passing rule."""
|
||||
|
||||
def __init__(
|
||||
self, test: Callable[..., bool] = lambda gateway, data_path: True
|
||||
) -> None:
|
||||
"""Initialize a ScreenLogic rule."""
|
||||
self._test = test
|
||||
|
||||
def test(self, gateway: ScreenLogicGateway, data_path: ScreenLogicDataPath) -> bool:
|
||||
"""Method to check the rule."""
|
||||
return self._test(gateway, data_path)
|
||||
|
||||
|
||||
class ScreenLogicDataRule(ScreenLogicRule):
|
||||
"""Represents a data rule."""
|
||||
|
||||
def __init__(
|
||||
self, test: Callable[..., bool], test_path_template: tuple[PathPart, ...]
|
||||
) -> None:
|
||||
"""Initialize a ScreenLogic data rule."""
|
||||
self._test_path_template = test_path_template
|
||||
super().__init__(test)
|
||||
|
||||
def test(self, gateway: ScreenLogicGateway, data_path: ScreenLogicDataPath) -> bool:
|
||||
"""Check the rule against the gateway's data."""
|
||||
test_path = realize_path_template(self._test_path_template, data_path)
|
||||
return self._test(gateway.get_data(*test_path))
|
||||
|
||||
|
||||
class ScreenLogicEquipmentRule(ScreenLogicRule):
|
||||
"""Represents an equipment flag rule."""
|
||||
|
||||
def test(self, gateway: ScreenLogicGateway, data_path: ScreenLogicDataPath) -> bool:
|
||||
"""Check the rule against the gateway's equipment flags."""
|
||||
return self._test(gateway.equipment_flags)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SupportedValueParameters:
|
||||
"""Base supported values for ScreenLogic Entities."""
|
||||
|
||||
enabled: ScreenLogicRule = ScreenLogicRule()
|
||||
included: ScreenLogicRule = ScreenLogicRule()
|
||||
subscription_code: int | None = None
|
||||
entity_category: EntityCategory | None = EntityCategory.DIAGNOSTIC
|
||||
|
||||
|
||||
SupportedValueDescriptions = dict[str, SupportedValueParameters]
|
||||
|
||||
SupportedGroupDescriptions = dict[int | str, SupportedValueDescriptions]
|
||||
|
||||
SupportedDeviceDescriptions = dict[str, SupportedGroupDescriptions]
|
||||
|
||||
|
||||
DEVICE_INCLUSION_RULES = {
|
||||
DEVICE.PUMP: ScreenLogicDataRule(
|
||||
lambda pump_data: pump_data[VALUE.DATA] != 0,
|
||||
(PathPart.DEVICE, PathPart.INDEX),
|
||||
),
|
||||
DEVICE.INTELLICHEM: ScreenLogicEquipmentRule(
|
||||
lambda flags: EQUIPMENT_FLAG.INTELLICHEM in flags,
|
||||
),
|
||||
DEVICE.SCG: ScreenLogicEquipmentRule(
|
||||
lambda flags: EQUIPMENT_FLAG.CHLORINATOR in flags,
|
||||
),
|
||||
}
|
||||
|
||||
DEVICE_SUBSCRIPTION = {
|
||||
DEVICE.CONTROLLER: CODE.STATUS_CHANGED,
|
||||
DEVICE.INTELLICHEM: CODE.CHEMISTRY_CHANGED,
|
||||
}
|
||||
|
||||
|
||||
# not run-time
|
||||
def get_ha_unit(entity_data: dict) -> StrEnum | str | None:
|
||||
"""Return a Home Assistant unit of measurement from a UNIT."""
|
||||
sl_unit = entity_data.get(ATTR.UNIT)
|
||||
return SL_UNIT_TO_HA_UNIT.get(sl_unit, sl_unit)
|
||||
|
||||
|
||||
# partial run-time
|
||||
def realize_path_template(
|
||||
template_path: ScreenLogicDataPathTemplate, data_path: ScreenLogicDataPath
|
||||
) -> ScreenLogicDataPath:
|
||||
"""Create a new data path using a template and an existing data path.
|
||||
|
||||
Construct new ScreenLogicDataPath from data_path using
|
||||
template_path to specify values from data_path.
|
||||
"""
|
||||
if not data_path or len(data_path) < 3:
|
||||
raise KeyError(
|
||||
f"Missing or invalid required parameter: 'data_path' for template path '{template_path}'"
|
||||
)
|
||||
device, group, data_key = data_path
|
||||
realized_path: list[str | int] = []
|
||||
for part in template_path:
|
||||
match part:
|
||||
case PathPart.DEVICE:
|
||||
realized_path.append(device)
|
||||
case PathPart.INDEX | PathPart.KEY:
|
||||
realized_path.append(group)
|
||||
case PathPart.VALUE:
|
||||
realized_path.append(data_key)
|
||||
case _:
|
||||
realized_path.append(part)
|
||||
|
||||
return tuple(realized_path)
|
||||
|
||||
|
||||
def preprocess_supported_values(
|
||||
supported_devices: SupportedDeviceDescriptions,
|
||||
) -> list[tuple[ScreenLogicDataPath, Any]]:
|
||||
"""Expand config dict into list of ScreenLogicDataPaths and settings."""
|
||||
processed: list[tuple[ScreenLogicDataPath, Any]] = []
|
||||
for device, device_groups in supported_devices.items():
|
||||
for group, group_values in device_groups.items():
|
||||
for value_key, value_params in group_values.items():
|
||||
value_data_path = (device, group, value_key)
|
||||
processed.append((value_data_path, value_params))
|
||||
return processed
|
||||
|
||||
|
||||
def iterate_expand_group_wildcard(
|
||||
gateway: ScreenLogicGateway,
|
||||
preprocessed_data: list[tuple[ScreenLogicDataPath, Any]],
|
||||
) -> Generator[tuple[ScreenLogicDataPath, Any], None, None]:
|
||||
"""Iterate and expand any group wildcards to all available entries in gateway."""
|
||||
for data_path, value_params in preprocessed_data:
|
||||
device, group, value_key = data_path
|
||||
if group == "*":
|
||||
for index in gateway.get_data(device):
|
||||
yield ((device, index, value_key), value_params)
|
||||
else:
|
||||
yield (data_path, value_params)
|
||||
|
||||
|
||||
def build_base_entity_description(
|
||||
gateway: ScreenLogicGateway,
|
||||
entity_key: str,
|
||||
data_path: ScreenLogicDataPath,
|
||||
value_data: dict,
|
||||
value_params: SupportedValueParameters,
|
||||
) -> dict:
|
||||
"""Build base entity description.
|
||||
|
||||
Returns a dict of entity description key value pairs common to all entities.
|
||||
"""
|
||||
return {
|
||||
"data_path": data_path,
|
||||
"key": entity_key,
|
||||
"entity_category": value_params.entity_category,
|
||||
"entity_registry_enabled_default": value_params.enabled.test(
|
||||
gateway, data_path
|
||||
),
|
||||
"name": value_data.get(ATTR.NAME),
|
||||
}
|
||||
|
||||
|
||||
ENTITY_MIGRATIONS = {
|
||||
"chem_alarm": {
|
||||
"new_key": VALUE.ACTIVE_ALERT,
|
||||
"old_name": "Chemistry Alarm",
|
||||
"new_name": "Active Alert",
|
||||
},
|
||||
"chem_calcium_harness": {
|
||||
"new_key": VALUE.CALCIUM_HARNESS,
|
||||
},
|
||||
"chem_current_orp": {
|
||||
"new_key": VALUE.ORP_NOW,
|
||||
"old_name": "Current ORP",
|
||||
"new_name": "ORP Now",
|
||||
},
|
||||
"chem_current_ph": {
|
||||
"new_key": VALUE.PH_NOW,
|
||||
"old_name": "Current pH",
|
||||
"new_name": "pH Now",
|
||||
},
|
||||
"chem_cya": {
|
||||
"new_key": VALUE.CYA,
|
||||
},
|
||||
"chem_orp_dosing_state": {
|
||||
"new_key": VALUE.ORP_DOSING_STATE,
|
||||
},
|
||||
"chem_orp_last_dose_time": {
|
||||
"new_key": VALUE.ORP_LAST_DOSE_TIME,
|
||||
},
|
||||
"chem_orp_last_dose_volume": {
|
||||
"new_key": VALUE.ORP_LAST_DOSE_VOLUME,
|
||||
},
|
||||
"chem_orp_setpoint": {
|
||||
"new_key": VALUE.ORP_SETPOINT,
|
||||
},
|
||||
"chem_orp_supply_level": {
|
||||
"new_key": VALUE.ORP_SUPPLY_LEVEL,
|
||||
},
|
||||
"chem_ph_dosing_state": {
|
||||
"new_key": VALUE.PH_DOSING_STATE,
|
||||
},
|
||||
"chem_ph_last_dose_time": {
|
||||
"new_key": VALUE.PH_LAST_DOSE_TIME,
|
||||
},
|
||||
"chem_ph_last_dose_volume": {
|
||||
"new_key": VALUE.PH_LAST_DOSE_VOLUME,
|
||||
},
|
||||
"chem_ph_probe_water_temp": {
|
||||
"new_key": VALUE.PH_PROBE_WATER_TEMP,
|
||||
},
|
||||
"chem_ph_setpoint": {
|
||||
"new_key": VALUE.PH_SETPOINT,
|
||||
},
|
||||
"chem_ph_supply_level": {
|
||||
"new_key": VALUE.PH_SUPPLY_LEVEL,
|
||||
},
|
||||
"chem_salt_tds_ppm": {
|
||||
"new_key": VALUE.SALT_TDS_PPM,
|
||||
},
|
||||
"chem_total_alkalinity": {
|
||||
"new_key": VALUE.TOTAL_ALKALINITY,
|
||||
},
|
||||
"currentGPM": {
|
||||
"new_key": VALUE.GPM_NOW,
|
||||
"old_name": "Current GPM",
|
||||
"new_name": "GPM Now",
|
||||
"device": DEVICE.PUMP,
|
||||
},
|
||||
"currentRPM": {
|
||||
"new_key": VALUE.RPM_NOW,
|
||||
"old_name": "Current RPM",
|
||||
"new_name": "RPM Now",
|
||||
"device": DEVICE.PUMP,
|
||||
},
|
||||
"currentWatts": {
|
||||
"new_key": VALUE.WATTS_NOW,
|
||||
"old_name": "Current Watts",
|
||||
"new_name": "Watts Now",
|
||||
"device": DEVICE.PUMP,
|
||||
},
|
||||
"orp_alarm": {
|
||||
"new_key": VALUE.ORP_LOW_ALARM,
|
||||
"old_name": "ORP Alarm",
|
||||
"new_name": "ORP LOW Alarm",
|
||||
},
|
||||
"ph_alarm": {
|
||||
"new_key": VALUE.PH_HIGH_ALARM,
|
||||
"old_name": "pH Alarm",
|
||||
"new_name": "pH HIGH Alarm",
|
||||
},
|
||||
"scg_status": {
|
||||
"new_key": VALUE.STATE,
|
||||
"old_name": "SCG Status",
|
||||
"new_name": "Chlorinator",
|
||||
"device": DEVICE.SCG,
|
||||
},
|
||||
"scg_level1": {
|
||||
"new_key": VALUE.POOL_SETPOINT,
|
||||
"old_name": "Pool SCG Level",
|
||||
"new_name": "Pool Chlorinator Setpoint",
|
||||
},
|
||||
"scg_level2": {
|
||||
"new_key": VALUE.SPA_SETPOINT,
|
||||
"old_name": "Spa SCG Level",
|
||||
"new_name": "Spa Chlorinator Setpoint",
|
||||
},
|
||||
"scg_salt_ppm": {
|
||||
"new_key": VALUE.SALT_PPM,
|
||||
"old_name": "SCG Salt",
|
||||
"new_name": "Chlorinator Salt",
|
||||
"device": DEVICE.SCG,
|
||||
},
|
||||
"scg_super_chlor_timer": {
|
||||
"new_key": VALUE.SUPER_CHLOR_TIMER,
|
||||
"old_name": "SCG Super Chlorination Timer",
|
||||
"new_name": "Super Chlorination Timer",
|
||||
},
|
||||
}
|
|
@ -5,8 +5,8 @@ from typing import Any
|
|||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import ScreenlogicDataUpdateCoordinator
|
||||
from .const import DOMAIN
|
||||
from .coordinator import ScreenlogicDataUpdateCoordinator
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
|
|
|
@ -1,52 +1,65 @@
|
|||
"""Base ScreenLogicEntity definitions."""
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from screenlogicpy import ScreenLogicGateway
|
||||
from screenlogicpy.const import CODE, DATA as SL_DATA, EQUIPMENT, ON_OFF
|
||||
from screenlogicpy.const.common import ON_OFF
|
||||
from screenlogicpy.const.data import ATTR
|
||||
from screenlogicpy.const.msg import CODE
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import ScreenlogicDataUpdateCoordinator
|
||||
from .const import ScreenLogicDataPath
|
||||
from .coordinator import ScreenlogicDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScreenLogicEntityRequiredKeyMixin:
|
||||
"""Mixin for required ScreenLogic entity key."""
|
||||
|
||||
data_path: ScreenLogicDataPath
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScreenLogicEntityDescription(
|
||||
EntityDescription, ScreenLogicEntityRequiredKeyMixin
|
||||
):
|
||||
"""Base class for a ScreenLogic entity description."""
|
||||
|
||||
|
||||
class ScreenlogicEntity(CoordinatorEntity[ScreenlogicDataUpdateCoordinator]):
|
||||
"""Base class for all ScreenLogic entities."""
|
||||
|
||||
entity_description: ScreenLogicEntityDescription
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: ScreenlogicDataUpdateCoordinator,
|
||||
data_key: str,
|
||||
enabled: bool = True,
|
||||
entity_description: ScreenLogicEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize of the entity."""
|
||||
super().__init__(coordinator)
|
||||
self._data_key = data_key
|
||||
self._attr_entity_registry_enabled_default = enabled
|
||||
self._attr_unique_id = f"{self.mac}_{self._data_key}"
|
||||
|
||||
controller_type = self.config_data["controller_type"]
|
||||
hardware_type = self.config_data["hardware_type"]
|
||||
try:
|
||||
equipment_model = EQUIPMENT.CONTROLLER_HARDWARE[controller_type][
|
||||
hardware_type
|
||||
]
|
||||
except KeyError:
|
||||
equipment_model = f"Unknown Model C:{controller_type} H:{hardware_type}"
|
||||
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}"
|
||||
mac = self.mac
|
||||
assert mac is not None
|
||||
self._attr_device_info = DeviceInfo(
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, mac)},
|
||||
manufacturer="Pentair",
|
||||
model=equipment_model,
|
||||
name=self.gateway_name,
|
||||
model=self.gateway.controller_model,
|
||||
name=self.gateway.name,
|
||||
sw_version=self.gateway.version,
|
||||
)
|
||||
|
||||
|
@ -56,26 +69,11 @@ class ScreenlogicEntity(CoordinatorEntity[ScreenlogicDataUpdateCoordinator]):
|
|||
assert self.coordinator.config_entry is not None
|
||||
return self.coordinator.config_entry.unique_id
|
||||
|
||||
@property
|
||||
def config_data(self) -> dict[str | int, Any]:
|
||||
"""Shortcut for config data."""
|
||||
return self.gateway_data[SL_DATA.KEY_CONFIG]
|
||||
|
||||
@property
|
||||
def gateway(self) -> ScreenLogicGateway:
|
||||
"""Return the gateway."""
|
||||
return self.coordinator.gateway
|
||||
|
||||
@property
|
||||
def gateway_data(self) -> dict[str | int, Any]:
|
||||
"""Return the gateway data."""
|
||||
return self.gateway.get_data()
|
||||
|
||||
@property
|
||||
def gateway_name(self) -> str:
|
||||
"""Return the configured name of the gateway."""
|
||||
return self.gateway.name
|
||||
|
||||
async def _async_refresh(self) -> None:
|
||||
"""Refresh the data from the gateway."""
|
||||
await self.coordinator.async_refresh()
|
||||
|
@ -87,20 +85,41 @@ class ScreenlogicEntity(CoordinatorEntity[ScreenlogicDataUpdateCoordinator]):
|
|||
"""Refresh from a timed called."""
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
@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
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScreenLogicPushEntityRequiredKeyMixin:
|
||||
"""Mixin for required key for ScreenLogic push entities."""
|
||||
|
||||
subscription_code: CODE
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScreenLogicPushEntityDescription(
|
||||
ScreenLogicEntityDescription,
|
||||
ScreenLogicPushEntityRequiredKeyMixin,
|
||||
):
|
||||
"""Base class for a ScreenLogic push entity description."""
|
||||
|
||||
|
||||
class ScreenLogicPushEntity(ScreenlogicEntity):
|
||||
"""Base class for all ScreenLogic push entities."""
|
||||
|
||||
entity_description: ScreenLogicPushEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: ScreenlogicDataUpdateCoordinator,
|
||||
data_key: str,
|
||||
message_code: CODE,
|
||||
enabled: bool = True,
|
||||
entity_description: ScreenLogicPushEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(coordinator, data_key, enabled)
|
||||
self._update_message_code = message_code
|
||||
"""Initialize of the entity."""
|
||||
super().__init__(coordinator, entity_description)
|
||||
self._last_update_success = True
|
||||
|
||||
@callback
|
||||
|
@ -114,7 +133,8 @@ class ScreenLogicPushEntity(ScreenlogicEntity):
|
|||
await super().async_added_to_hass()
|
||||
self.async_on_remove(
|
||||
await self.gateway.async_subscribe_client(
|
||||
self._async_data_updated, self._update_message_code
|
||||
self._async_data_updated,
|
||||
self.entity_description.subscription_code,
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -129,17 +149,10 @@ class ScreenLogicPushEntity(ScreenlogicEntity):
|
|||
class ScreenLogicCircuitEntity(ScreenLogicPushEntity):
|
||||
"""Base class for all ScreenLogic switch and light entities."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Get the name of the switch."""
|
||||
return self.circuit["name"]
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Get whether the switch is in on state."""
|
||||
return self.circuit["value"] == ON_OFF.ON
|
||||
return self.entity_data[ATTR.VALUE] == ON_OFF.ON
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Send the ON command."""
|
||||
|
@ -149,14 +162,9 @@ class ScreenLogicCircuitEntity(ScreenLogicPushEntity):
|
|||
"""Send the OFF command."""
|
||||
await self._async_set_circuit(ON_OFF.OFF)
|
||||
|
||||
async def _async_set_circuit(self, circuit_value: int) -> None:
|
||||
if not await self.gateway.async_set_circuit(self._data_key, circuit_value):
|
||||
async def _async_set_circuit(self, state: ON_OFF) -> None:
|
||||
if not await self.gateway.async_set_circuit(self._data_key, state.value):
|
||||
raise HomeAssistantError(
|
||||
f"Failed to set_circuit {self._data_key} {circuit_value}"
|
||||
f"Failed to set_circuit {self._data_key} {state.value}"
|
||||
)
|
||||
_LOGGER.debug("Turn %s %s", self._data_key, circuit_value)
|
||||
|
||||
@property
|
||||
def circuit(self) -> dict[str | int, Any]:
|
||||
"""Shortcut to access the circuit."""
|
||||
return self.gateway_data[SL_DATA.KEY_CIRCUITS][self._data_key]
|
||||
_LOGGER.debug("Set circuit %s %s", self._data_key, state.value)
|
||||
|
|
|
@ -1,16 +1,23 @@
|
|||
"""Support for a ScreenLogic light 'circuit' switch."""
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
from screenlogicpy.const import CODE, DATA as SL_DATA, GENERIC_CIRCUIT_NAMES
|
||||
from screenlogicpy.const.data import ATTR, DEVICE
|
||||
from screenlogicpy.const.msg import CODE
|
||||
from screenlogicpy.device_const.circuit import GENERIC_CIRCUIT_NAMES, INTERFACE
|
||||
|
||||
from homeassistant.components.light import ColorMode, LightEntity
|
||||
from homeassistant.components.light import (
|
||||
ColorMode,
|
||||
LightEntity,
|
||||
LightEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import ScreenlogicDataUpdateCoordinator
|
||||
from .const import DOMAIN, LIGHT_CIRCUIT_FUNCTIONS
|
||||
from .entity import ScreenLogicCircuitEntity
|
||||
from .const import DOMAIN as SL_DOMAIN, LIGHT_CIRCUIT_FUNCTIONS
|
||||
from .coordinator import ScreenlogicDataUpdateCoordinator
|
||||
from .entity import ScreenLogicCircuitEntity, ScreenLogicPushEntityDescription
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -21,26 +28,45 @@ async def async_setup_entry(
|
|||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up entry."""
|
||||
coordinator: ScreenlogicDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
entities: list[ScreenLogicLight] = []
|
||||
coordinator: ScreenlogicDataUpdateCoordinator = hass.data[SL_DOMAIN][
|
||||
config_entry.entry_id
|
||||
]
|
||||
circuits = coordinator.gateway_data[SL_DATA.KEY_CIRCUITS]
|
||||
async_add_entities(
|
||||
[
|
||||
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:
|
||||
continue
|
||||
circuit_name = circuit_data[ATTR.NAME]
|
||||
circuit_interface = INTERFACE(circuit_data[ATTR.INTERFACE])
|
||||
entities.append(
|
||||
ScreenLogicLight(
|
||||
coordinator,
|
||||
circuit_num,
|
||||
CODE.STATUS_CHANGED,
|
||||
circuit["name"] not in GENERIC_CIRCUIT_NAMES,
|
||||
ScreenLogicLightDescription(
|
||||
subscription_code=CODE.STATUS_CHANGED,
|
||||
data_path=(DEVICE.CIRCUIT, circuit_index),
|
||||
key=circuit_index,
|
||||
name=circuit_name,
|
||||
entity_registry_enabled_default=(
|
||||
circuit_name not in GENERIC_CIRCUIT_NAMES
|
||||
and circuit_interface != INTERFACE.DONT_SHOW
|
||||
),
|
||||
),
|
||||
)
|
||||
for circuit_num, circuit in circuits.items()
|
||||
if circuit["function"] in LIGHT_CIRCUIT_FUNCTIONS
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScreenLogicLightDescription(
|
||||
LightEntityDescription, ScreenLogicPushEntityDescription
|
||||
):
|
||||
"""Describes a ScreenLogic light entity."""
|
||||
|
||||
|
||||
class ScreenLogicLight(ScreenLogicCircuitEntity, LightEntity):
|
||||
"""Class to represent a ScreenLogic Light."""
|
||||
|
||||
entity_description: ScreenLogicLightDescription
|
||||
_attr_color_mode = ColorMode.ONOFF
|
||||
_attr_supported_color_modes = {ColorMode.ONOFF}
|
||||
|
|
|
@ -15,5 +15,5 @@
|
|||
"documentation": "https://www.home-assistant.io/integrations/screenlogic",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["screenlogicpy"],
|
||||
"requirements": ["screenlogicpy==0.8.2"]
|
||||
"requirements": ["screenlogicpy==0.9.0"]
|
||||
}
|
||||
|
|
|
@ -1,25 +1,82 @@
|
|||
"""Support for a ScreenLogic number entity."""
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
from screenlogicpy.const import BODY_TYPE, DATA as SL_DATA, EQUIPMENT, SCG
|
||||
from screenlogicpy.const.data import ATTR, DEVICE, GROUP, VALUE
|
||||
|
||||
from homeassistant.components.number import NumberEntity
|
||||
from homeassistant.components.number import (
|
||||
DOMAIN,
|
||||
NumberDeviceClass,
|
||||
NumberEntity,
|
||||
NumberEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import ScreenlogicDataUpdateCoordinator
|
||||
from .const import DOMAIN
|
||||
from .entity import ScreenlogicEntity
|
||||
from .const import DOMAIN as SL_DOMAIN, ScreenLogicDataPath
|
||||
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
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
SUPPORTED_SCG_NUMBERS = (
|
||||
"scg_level1",
|
||||
"scg_level2",
|
||||
|
||||
@dataclass
|
||||
class SupportedNumberValueParametersMixin:
|
||||
"""Mixin for supported predefined data for a ScreenLogic number entity."""
|
||||
|
||||
set_value_config: tuple[str, tuple[tuple[PathPart | str | int, ...], ...]]
|
||||
device_class: NumberDeviceClass | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class SupportedNumberValueParameters(
|
||||
SupportedValueParameters, SupportedNumberValueParametersMixin
|
||||
):
|
||||
"""Supported predefined data for 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_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,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
|
@ -29,66 +86,113 @@ async def async_setup_entry(
|
|||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up entry."""
|
||||
coordinator: ScreenlogicDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
entities: list[ScreenLogicNumber] = []
|
||||
coordinator: ScreenlogicDataUpdateCoordinator = hass.data[SL_DOMAIN][
|
||||
config_entry.entry_id
|
||||
]
|
||||
equipment_flags = coordinator.gateway_data[SL_DATA.KEY_CONFIG]["equipment_flags"]
|
||||
if equipment_flags & EQUIPMENT.FLAG_CHLORINATOR:
|
||||
async_add_entities(
|
||||
[
|
||||
ScreenLogicNumber(coordinator, scg_level)
|
||||
for scg_level in coordinator.gateway_data[SL_DATA.KEY_SCG]
|
||||
if scg_level in SUPPORTED_SCG_NUMBERS
|
||||
]
|
||||
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),
|
||||
)
|
||||
)
|
||||
|
||||
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."""
|
||||
"""Class to represent a ScreenLogic Number entity."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
entity_description: ScreenLogicNumberDescription
|
||||
|
||||
def __init__(self, coordinator, data_key, enabled=True):
|
||||
"""Initialize of the entity."""
|
||||
super().__init__(coordinator, data_key, enabled)
|
||||
self._body_type = SUPPORTED_SCG_NUMBERS.index(self._data_key)
|
||||
self._attr_native_max_value = SCG.LIMIT_FOR_BODY[self._body_type]
|
||||
self._attr_name = self.sensor["name"]
|
||||
self._attr_native_unit_of_measurement = self.sensor["unit"]
|
||||
self._attr_entity_category = EntityCategory.CONFIG
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: ScreenlogicDataUpdateCoordinator,
|
||||
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)
|
||||
|
||||
@property
|
||||
def native_value(self) -> float:
|
||||
"""Return the current value."""
|
||||
return self.sensor["value"]
|
||||
return self.entity_data[ATTR.VALUE]
|
||||
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Update the current value."""
|
||||
# Need to set both levels at the same time, so we gather
|
||||
# both existing level values and override the one that changed.
|
||||
levels = {}
|
||||
for level in SUPPORTED_SCG_NUMBERS:
|
||||
levels[level] = self.gateway_data[SL_DATA.KEY_SCG][level]["value"]
|
||||
levels[self._data_key] = int(value)
|
||||
|
||||
if await self.coordinator.gateway.async_set_scg_config(
|
||||
levels[SUPPORTED_SCG_NUMBERS[BODY_TYPE.POOL]],
|
||||
levels[SUPPORTED_SCG_NUMBERS[BODY_TYPE.SPA]],
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"Set SCG to %i, %i",
|
||||
levels[SUPPORTED_SCG_NUMBERS[BODY_TYPE.POOL]],
|
||||
levels[SUPPORTED_SCG_NUMBERS[BODY_TYPE.SPA]],
|
||||
# Current API requires certain values to be set at the same time. This
|
||||
# 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
|
||||
)
|
||||
|
||||
args[self._data_key] = value
|
||||
|
||||
if self._set_value_func(*args.values()):
|
||||
_LOGGER.debug("Set '%s' to %s", self._data_key, value)
|
||||
await self._async_refresh()
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"Failed to set_scg to %i, %i",
|
||||
levels[SUPPORTED_SCG_NUMBERS[BODY_TYPE.POOL]],
|
||||
levels[SUPPORTED_SCG_NUMBERS[BODY_TYPE.SPA]],
|
||||
)
|
||||
|
||||
@property
|
||||
def sensor(self) -> dict:
|
||||
"""Shortcut to access the level sensor data."""
|
||||
return self.gateway_data[SL_DATA.KEY_SCG][self._data_key]
|
||||
_LOGGER.debug("Failed to set '%s' to %s", self._data_key, value)
|
||||
|
|
|
@ -1,76 +1,148 @@
|
|||
"""Support for a ScreenLogic Sensor."""
|
||||
from typing import Any
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
from screenlogicpy.const import (
|
||||
CHEM_DOSING_STATE,
|
||||
CODE,
|
||||
DATA as SL_DATA,
|
||||
DEVICE_TYPE,
|
||||
EQUIPMENT,
|
||||
STATE_TYPE,
|
||||
UNIT,
|
||||
)
|
||||
from screenlogicpy.const.common import DEVICE_TYPE, STATE_TYPE
|
||||
from screenlogicpy.const.data import ATTR, DEVICE, GROUP, VALUE
|
||||
from screenlogicpy.device_const.chemistry import DOSE_STATE
|
||||
from screenlogicpy.device_const.pump import PUMP_TYPE
|
||||
from screenlogicpy.device_const.system import EQUIPMENT_FLAG
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
DOMAIN,
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
PERCENTAGE,
|
||||
REVOLUTIONS_PER_MINUTE,
|
||||
EntityCategory,
|
||||
UnitOfElectricPotential,
|
||||
UnitOfPower,
|
||||
UnitOfTemperature,
|
||||
UnitOfTime,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import ScreenlogicDataUpdateCoordinator
|
||||
from .const import DOMAIN
|
||||
from .entity import ScreenlogicEntity, ScreenLogicPushEntity
|
||||
|
||||
SUPPORTED_BASIC_SENSORS = (
|
||||
"air_temperature",
|
||||
"saturation",
|
||||
from .const import DOMAIN as SL_DOMAIN, ScreenLogicDataPath
|
||||
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,
|
||||
)
|
||||
|
||||
SUPPORTED_BASIC_CHEM_SENSORS = (
|
||||
"orp",
|
||||
"ph",
|
||||
from .entity import (
|
||||
ScreenlogicEntity,
|
||||
ScreenLogicEntityDescription,
|
||||
ScreenLogicPushEntity,
|
||||
ScreenLogicPushEntityDescription,
|
||||
)
|
||||
from .util import cleanup_excluded_entity, generate_unique_id
|
||||
|
||||
SUPPORTED_CHEM_SENSORS = (
|
||||
"calcium_harness",
|
||||
"current_orp",
|
||||
"current_ph",
|
||||
"cya",
|
||||
"orp_dosing_state",
|
||||
"orp_last_dose_time",
|
||||
"orp_last_dose_volume",
|
||||
"orp_setpoint",
|
||||
"orp_supply_level",
|
||||
"ph_dosing_state",
|
||||
"ph_last_dose_time",
|
||||
"ph_last_dose_volume",
|
||||
"ph_probe_water_temp",
|
||||
"ph_setpoint",
|
||||
"ph_supply_level",
|
||||
"salt_tds_ppm",
|
||||
"total_alkalinity",
|
||||
_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(),
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
SUPPORTED_SCG_SENSORS = (
|
||||
"scg_salt_ppm",
|
||||
"scg_super_chlor_timer",
|
||||
)
|
||||
|
||||
SUPPORTED_PUMP_SENSORS = ("currentWatts", "currentRPM", "currentGPM")
|
||||
|
||||
SL_DEVICE_TYPE_TO_HA_DEVICE_CLASS = {
|
||||
DEVICE_TYPE.DURATION: SensorDeviceClass.DURATION,
|
||||
DEVICE_TYPE.ENUM: SensorDeviceClass.ENUM,
|
||||
|
@ -85,18 +157,6 @@ SL_STATE_TYPE_TO_HA_STATE_CLASS = {
|
|||
STATE_TYPE.TOTAL_INCREASING: SensorStateClass.TOTAL_INCREASING,
|
||||
}
|
||||
|
||||
SL_UNIT_TO_HA_UNIT = {
|
||||
UNIT.CELSIUS: UnitOfTemperature.CELSIUS,
|
||||
UNIT.FAHRENHEIT: UnitOfTemperature.FAHRENHEIT,
|
||||
UNIT.MILLIVOLT: UnitOfElectricPotential.MILLIVOLT,
|
||||
UNIT.WATT: UnitOfPower.WATT,
|
||||
UNIT.HOUR: UnitOfTime.HOURS,
|
||||
UNIT.SECOND: UnitOfTime.SECONDS,
|
||||
UNIT.REVOLUTIONS_PER_MINUTE: REVOLUTIONS_PER_MINUTE,
|
||||
UNIT.PARTS_PER_MILLION: CONCENTRATION_PARTS_PER_MILLION,
|
||||
UNIT.PERCENT: PERCENTAGE,
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
|
@ -104,171 +164,110 @@ async def async_setup_entry(
|
|||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up entry."""
|
||||
entities: list[ScreenLogicSensorEntity] = []
|
||||
coordinator: ScreenlogicDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
entities: list[ScreenLogicSensor] = []
|
||||
coordinator: ScreenlogicDataUpdateCoordinator = hass.data[SL_DOMAIN][
|
||||
config_entry.entry_id
|
||||
]
|
||||
equipment_flags = coordinator.gateway_data[SL_DATA.KEY_CONFIG]["equipment_flags"]
|
||||
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)
|
||||
|
||||
# Generic push sensors
|
||||
for sensor_name in coordinator.gateway_data[SL_DATA.KEY_SENSORS]:
|
||||
if sensor_name in SUPPORTED_BASIC_SENSORS:
|
||||
entities.append(
|
||||
ScreenLogicStatusSensor(coordinator, sensor_name, CODE.STATUS_CHANGED)
|
||||
)
|
||||
device = data_path[0]
|
||||
|
||||
# While these values exist in the chemistry data, their last value doesn't
|
||||
# persist there when the pump is off/there is no flow. Pulling them from
|
||||
# the basic sensors keeps the 'last' value and is better for graphs.
|
||||
if (
|
||||
equipment_flags & EQUIPMENT.FLAG_INTELLICHEM
|
||||
and sensor_name in SUPPORTED_BASIC_CHEM_SENSORS
|
||||
if not (DEVICE_INCLUSION_RULES.get(device) or value_params.included).test(
|
||||
gateway, data_path
|
||||
):
|
||||
entities.append(
|
||||
ScreenLogicStatusSensor(coordinator, sensor_name, CODE.STATUS_CHANGED)
|
||||
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)
|
||||
)
|
||||
|
||||
# Pump sensors
|
||||
for pump_num, pump_data in coordinator.gateway_data[SL_DATA.KEY_PUMPS].items():
|
||||
if pump_data["data"] != 0 and "currentWatts" in pump_data:
|
||||
for pump_key in pump_data:
|
||||
enabled = True
|
||||
# Assumptions for Intelliflow VF
|
||||
if pump_data["pumpType"] == 1 and pump_key == "currentRPM":
|
||||
enabled = False
|
||||
# Assumptions for Intelliflow VS
|
||||
if pump_data["pumpType"] == 2 and pump_key == "currentGPM":
|
||||
enabled = False
|
||||
if pump_key in SUPPORTED_PUMP_SENSORS:
|
||||
entities.append(
|
||||
ScreenLogicPumpSensor(coordinator, pump_num, pump_key, enabled)
|
||||
)
|
||||
|
||||
# IntelliChem sensors
|
||||
if equipment_flags & EQUIPMENT.FLAG_INTELLICHEM:
|
||||
for chem_sensor_name in coordinator.gateway_data[SL_DATA.KEY_CHEMISTRY]:
|
||||
enabled = True
|
||||
if equipment_flags & EQUIPMENT.FLAG_CHLORINATOR:
|
||||
if chem_sensor_name in ("salt_tds_ppm",):
|
||||
enabled = False
|
||||
if chem_sensor_name in SUPPORTED_CHEM_SENSORS:
|
||||
entities.append(
|
||||
ScreenLogicChemistrySensor(
|
||||
coordinator, chem_sensor_name, CODE.CHEMISTRY_CHANGED, enabled
|
||||
)
|
||||
) is not None:
|
||||
entities.append(
|
||||
ScreenLogicPushSensor(
|
||||
coordinator,
|
||||
ScreenLogicPushSensorDescription(
|
||||
subscription_code=sub_code,
|
||||
**entity_description_kwargs,
|
||||
),
|
||||
)
|
||||
|
||||
# SCG sensors
|
||||
if equipment_flags & EQUIPMENT.FLAG_CHLORINATOR:
|
||||
entities.extend(
|
||||
[
|
||||
ScreenLogicSCGSensor(coordinator, scg_sensor)
|
||||
for scg_sensor in coordinator.gateway_data[SL_DATA.KEY_SCG]
|
||||
if scg_sensor in SUPPORTED_SCG_SENSORS
|
||||
]
|
||||
)
|
||||
)
|
||||
else:
|
||||
entities.append(
|
||||
ScreenLogicSensor(
|
||||
coordinator,
|
||||
ScreenLogicSensorDescription(
|
||||
**entity_description_kwargs,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class ScreenLogicSensorEntity(ScreenlogicEntity, SensorEntity):
|
||||
"""Base class for all ScreenLogic sensor entities."""
|
||||
@dataclass
|
||||
class ScreenLogicSensorMixin:
|
||||
"""Mixin for SecreenLogic sensor entity."""
|
||||
|
||||
value_mod: Callable[[int | str], int | str] | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScreenLogicSensorDescription(
|
||||
ScreenLogicSensorMixin, SensorEntityDescription, ScreenLogicEntityDescription
|
||||
):
|
||||
"""Describes a ScreenLogic sensor."""
|
||||
|
||||
|
||||
class ScreenLogicSensor(ScreenlogicEntity, SensorEntity):
|
||||
"""Representation of a ScreenLogic sensor entity."""
|
||||
|
||||
entity_description: ScreenLogicSensorDescription
|
||||
_attr_has_entity_name = True
|
||||
|
||||
@property
|
||||
def name(self) -> str | None:
|
||||
"""Name of the sensor."""
|
||||
return self.sensor["name"]
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
"""Return the unit of measurement."""
|
||||
sl_unit = self.sensor.get("unit")
|
||||
return SL_UNIT_TO_HA_UNIT.get(sl_unit, sl_unit)
|
||||
|
||||
@property
|
||||
def device_class(self) -> SensorDeviceClass | None:
|
||||
"""Device class of the sensor."""
|
||||
device_type = self.sensor.get("device_type")
|
||||
return SL_DEVICE_TYPE_TO_HA_DEVICE_CLASS.get(device_type)
|
||||
|
||||
@property
|
||||
def entity_category(self) -> EntityCategory | None:
|
||||
"""Entity Category of the sensor."""
|
||||
return (
|
||||
None if self._data_key == "air_temperature" else EntityCategory.DIAGNOSTIC
|
||||
)
|
||||
|
||||
@property
|
||||
def state_class(self) -> SensorStateClass | None:
|
||||
"""Return the state class of the sensor."""
|
||||
state_type = self.sensor.get("state_type")
|
||||
if self._data_key == "scg_super_chlor_timer":
|
||||
return None
|
||||
return SL_STATE_TYPE_TO_HA_STATE_CLASS.get(state_type)
|
||||
|
||||
@property
|
||||
def options(self) -> list[str] | None:
|
||||
"""Return a set of possible options."""
|
||||
return self.sensor.get("enum_options")
|
||||
|
||||
@property
|
||||
def native_value(self) -> str | int | float:
|
||||
"""State of the sensor."""
|
||||
return self.sensor["value"]
|
||||
|
||||
@property
|
||||
def sensor(self) -> dict[str | int, Any]:
|
||||
"""Shortcut to access the sensor data."""
|
||||
return self.gateway_data[SL_DATA.KEY_SENSORS][self._data_key]
|
||||
val = self.entity_data[ATTR.VALUE]
|
||||
value_mod = self.entity_description.value_mod
|
||||
return value_mod(val) if value_mod else val
|
||||
|
||||
|
||||
class ScreenLogicStatusSensor(ScreenLogicSensorEntity, ScreenLogicPushEntity):
|
||||
"""Representation of a basic ScreenLogic sensor entity."""
|
||||
@dataclass
|
||||
class ScreenLogicPushSensorDescription(
|
||||
ScreenLogicSensorDescription, ScreenLogicPushEntityDescription
|
||||
):
|
||||
"""Describes a ScreenLogic push sensor."""
|
||||
|
||||
|
||||
class ScreenLogicPumpSensor(ScreenLogicSensorEntity):
|
||||
"""Representation of a ScreenLogic pump sensor entity."""
|
||||
class ScreenLogicPushSensor(ScreenLogicSensor, ScreenLogicPushEntity):
|
||||
"""Representation of a ScreenLogic push sensor entity."""
|
||||
|
||||
def __init__(self, coordinator, pump, key, enabled=True):
|
||||
"""Initialize of the pump sensor."""
|
||||
super().__init__(coordinator, f"{key}_{pump}", enabled)
|
||||
self._pump_id = pump
|
||||
self._key = key
|
||||
|
||||
@property
|
||||
def sensor(self) -> dict[str | int, Any]:
|
||||
"""Shortcut to access the pump sensor data."""
|
||||
return self.gateway_data[SL_DATA.KEY_PUMPS][self._pump_id][self._key]
|
||||
|
||||
|
||||
class ScreenLogicChemistrySensor(ScreenLogicSensorEntity, ScreenLogicPushEntity):
|
||||
"""Representation of a ScreenLogic IntelliChem sensor entity."""
|
||||
|
||||
def __init__(self, coordinator, key, message_code, enabled=True):
|
||||
"""Initialize of the pump sensor."""
|
||||
super().__init__(coordinator, f"chem_{key}", message_code, enabled)
|
||||
self._key = key
|
||||
|
||||
@property
|
||||
def native_value(self) -> str | int | float:
|
||||
"""State of the sensor."""
|
||||
value = self.sensor["value"]
|
||||
if "dosing_state" in self._key:
|
||||
return CHEM_DOSING_STATE.NAME_FOR_NUM[value]
|
||||
return (value - 1) if "supply" in self._data_key else value
|
||||
|
||||
@property
|
||||
def sensor(self) -> dict[str | int, Any]:
|
||||
"""Shortcut to access the pump sensor data."""
|
||||
return self.gateway_data[SL_DATA.KEY_CHEMISTRY][self._key]
|
||||
|
||||
|
||||
class ScreenLogicSCGSensor(ScreenLogicSensorEntity):
|
||||
"""Representation of ScreenLogic SCG sensor entity."""
|
||||
|
||||
@property
|
||||
def sensor(self) -> dict[str | int, Any]:
|
||||
"""Shortcut to access the pump sensor data."""
|
||||
return self.gateway_data[SL_DATA.KEY_SCG][self._data_key]
|
||||
entity_description: ScreenLogicPushSensorDescription
|
||||
|
|
|
@ -1,21 +1,19 @@
|
|||
"""Support for a ScreenLogic 'circuit' switch."""
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
from screenlogicpy.const import (
|
||||
CODE,
|
||||
DATA as SL_DATA,
|
||||
GENERIC_CIRCUIT_NAMES,
|
||||
INTERFACE_GROUP,
|
||||
)
|
||||
from screenlogicpy.const.data import ATTR, DEVICE
|
||||
from screenlogicpy.const.msg import CODE
|
||||
from screenlogicpy.device_const.circuit import GENERIC_CIRCUIT_NAMES, INTERFACE
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import ScreenlogicDataUpdateCoordinator
|
||||
from .const import DOMAIN, LIGHT_CIRCUIT_FUNCTIONS
|
||||
from .entity import ScreenLogicCircuitEntity
|
||||
from .const import DOMAIN as SL_DOMAIN, LIGHT_CIRCUIT_FUNCTIONS
|
||||
from .coordinator import ScreenlogicDataUpdateCoordinator
|
||||
from .entity import ScreenLogicCircuitEntity, ScreenLogicPushEntityDescription
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -26,24 +24,43 @@ async def async_setup_entry(
|
|||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up entry."""
|
||||
coordinator: ScreenlogicDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
entities: list[ScreenLogicSwitch] = []
|
||||
coordinator: ScreenlogicDataUpdateCoordinator = hass.data[SL_DOMAIN][
|
||||
config_entry.entry_id
|
||||
]
|
||||
circuits = coordinator.gateway_data[SL_DATA.KEY_CIRCUITS]
|
||||
async_add_entities(
|
||||
[
|
||||
gateway = coordinator.gateway
|
||||
for circuit_index, circuit_data in gateway.get_data(DEVICE.CIRCUIT).items():
|
||||
if circuit_data[ATTR.FUNCTION] in LIGHT_CIRCUIT_FUNCTIONS:
|
||||
continue
|
||||
circuit_name = circuit_data[ATTR.NAME]
|
||||
circuit_interface = INTERFACE(circuit_data[ATTR.INTERFACE])
|
||||
entities.append(
|
||||
ScreenLogicSwitch(
|
||||
coordinator,
|
||||
circuit_num,
|
||||
CODE.STATUS_CHANGED,
|
||||
circuit["name"] not in GENERIC_CIRCUIT_NAMES
|
||||
and circuit["interface"] != INTERFACE_GROUP.DONT_SHOW,
|
||||
ScreenLogicSwitchDescription(
|
||||
subscription_code=CODE.STATUS_CHANGED,
|
||||
data_path=(DEVICE.CIRCUIT, circuit_index),
|
||||
key=circuit_index,
|
||||
name=circuit_name,
|
||||
entity_registry_enabled_default=(
|
||||
circuit_name not in GENERIC_CIRCUIT_NAMES
|
||||
and circuit_interface != INTERFACE.DONT_SHOW
|
||||
),
|
||||
),
|
||||
)
|
||||
for circuit_num, circuit in circuits.items()
|
||||
if circuit["function"] not in LIGHT_CIRCUIT_FUNCTIONS
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScreenLogicSwitchDescription(
|
||||
SwitchEntityDescription, ScreenLogicPushEntityDescription
|
||||
):
|
||||
"""Describes a ScreenLogic switch entity."""
|
||||
|
||||
|
||||
class ScreenLogicSwitch(ScreenLogicCircuitEntity, SwitchEntity):
|
||||
"""Class to represent a ScreenLogic Switch."""
|
||||
|
||||
entity_description: ScreenLogicSwitchDescription
|
||||
|
|
40
homeassistant/components/screenlogic/util.py
Normal file
40
homeassistant/components/screenlogic/util.py
Normal file
|
@ -0,0 +1,40 @@
|
|||
"""Utility functions for the ScreenLogic integration."""
|
||||
import logging
|
||||
|
||||
from screenlogicpy.const.data import SHARED_VALUES
|
||||
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from .const import DOMAIN as SL_DOMAIN
|
||||
from .coordinator import ScreenlogicDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def generate_unique_id(
|
||||
device: str | int, group: str | int | None, data_key: str | int
|
||||
) -> 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)
|
||||
|
||||
|
||||
def cleanup_excluded_entity(
|
||||
coordinator: ScreenlogicDataUpdateCoordinator,
|
||||
platform_domain: str,
|
||||
entity_key: str,
|
||||
) -> 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}"
|
||||
if entity_id := entity_registry.async_get_entity_id(
|
||||
platform_domain, SL_DOMAIN, unique_id
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"Removing existing entity '%s' per data inclusion rule", entity_id
|
||||
)
|
||||
entity_registry.async_remove(entity_id)
|
|
@ -2358,7 +2358,7 @@ satel-integra==0.3.7
|
|||
scapy==2.5.0
|
||||
|
||||
# homeassistant.components.screenlogic
|
||||
screenlogicpy==0.8.2
|
||||
screenlogicpy==0.9.0
|
||||
|
||||
# homeassistant.components.scsgate
|
||||
scsgate==0.1.0
|
||||
|
|
|
@ -1730,7 +1730,7 @@ samsungtvws[async,encrypted]==2.6.0
|
|||
scapy==2.5.0
|
||||
|
||||
# homeassistant.components.screenlogic
|
||||
screenlogicpy==0.8.2
|
||||
screenlogicpy==0.9.0
|
||||
|
||||
# homeassistant.components.backup
|
||||
securetar==2023.3.0
|
||||
|
|
|
@ -1 +1,67 @@
|
|||
"""Tests for the Screenlogic integration."""
|
||||
from collections.abc import Callable
|
||||
import logging
|
||||
|
||||
from tests.common import load_json_object_fixture
|
||||
|
||||
MOCK_ADAPTER_NAME = "Pentair DD-EE-FF"
|
||||
MOCK_ADAPTER_MAC = "aa:bb:cc:dd:ee:ff"
|
||||
MOCK_ADAPTER_IP = "127.0.0.1"
|
||||
MOCK_ADAPTER_PORT = 80
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
GATEWAY_DISCOVERY_IMPORT_PATH = "homeassistant.components.screenlogic.coordinator.async_discover_gateways_by_unique_id"
|
||||
|
||||
|
||||
def num_key_string_to_int(data: dict) -> None:
|
||||
"""Convert all string number dict keys to integer.
|
||||
|
||||
This needed for screenlogicpy's data dict format.
|
||||
"""
|
||||
rpl = []
|
||||
for key, value in data.items():
|
||||
if isinstance(value, dict):
|
||||
num_key_string_to_int(value)
|
||||
if isinstance(key, str) and key.isnumeric():
|
||||
rpl.append(key)
|
||||
for k in rpl:
|
||||
data[int(k)] = data.pop(k)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
DATA_FULL_CHEM = num_key_string_to_int(
|
||||
load_json_object_fixture("screenlogic/data_full_chem.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")
|
||||
)
|
||||
|
||||
|
||||
async def stub_async_connect(
|
||||
data,
|
||||
self,
|
||||
ip=None,
|
||||
port=None,
|
||||
gtype=None,
|
||||
gsubtype=None,
|
||||
name=MOCK_ADAPTER_NAME,
|
||||
connection_closed_callback: Callable = None,
|
||||
) -> bool:
|
||||
"""Initialize minimum attributes needed for tests."""
|
||||
self._ip = ip
|
||||
self._port = port
|
||||
self._type = gtype
|
||||
self._subtype = gsubtype
|
||||
self._name = name
|
||||
self._custom_connection_closed_callback = connection_closed_callback
|
||||
self._mac = MOCK_ADAPTER_MAC
|
||||
self._data = data
|
||||
_LOGGER.debug("Gateway mock connected")
|
||||
|
||||
return True
|
||||
|
|
27
tests/components/screenlogic/conftest.py
Normal file
27
tests/components/screenlogic/conftest.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
"""Setup fixtures for ScreenLogic integration tests."""
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.screenlogic import DOMAIN
|
||||
from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, CONF_SCAN_INTERVAL
|
||||
|
||||
from . import MOCK_ADAPTER_IP, MOCK_ADAPTER_MAC, MOCK_ADAPTER_NAME, MOCK_ADAPTER_PORT
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry() -> MockConfigEntry:
|
||||
"""Return a mocked config entry."""
|
||||
return MockConfigEntry(
|
||||
title=MOCK_ADAPTER_NAME,
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
CONF_IP_ADDRESS: MOCK_ADAPTER_IP,
|
||||
CONF_PORT: MOCK_ADAPTER_PORT,
|
||||
},
|
||||
options={
|
||||
CONF_SCAN_INTERVAL: 30,
|
||||
},
|
||||
unique_id=MOCK_ADAPTER_MAC,
|
||||
entry_id="screenlogictest",
|
||||
)
|
880
tests/components/screenlogic/fixtures/data_full_chem.json
Normal file
880
tests/components/screenlogic/fixtures/data_full_chem.json
Normal file
|
@ -0,0 +1,880 @@
|
|||
{
|
||||
"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": 98360,
|
||||
"list": [
|
||||
"INTELLIBRITE",
|
||||
"INTELLIFLO_0",
|
||||
"INTELLIFLO_1",
|
||||
"INTELLICHEM",
|
||||
"HYBRID_HEATER"
|
||||
]
|
||||
},
|
||||
"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": 0,
|
||||
"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": 51,
|
||||
"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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"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
|
||||
},
|
||||
"model": { "name": "Model", "value": "EasyTouch2 8" },
|
||||
"equipment": {
|
||||
"flags": 24
|
||||
}
|
||||
},
|
||||
"circuit": {},
|
||||
"pump": {
|
||||
"0": { "data": 0 },
|
||||
"1": { "data": 0 },
|
||||
"2": { "data": 0 },
|
||||
"3": { "data": 0 },
|
||||
"4": { "data": 0 },
|
||||
"5": { "data": 0 },
|
||||
"6": { "data": 0 },
|
||||
"7": { "data": 0 }
|
||||
},
|
||||
"body": {},
|
||||
"intellichem": {},
|
||||
"scg": {}
|
||||
}
|
151
tests/components/screenlogic/fixtures/data_min_migration.json
Normal file
151
tests/components/screenlogic/fixtures/data_min_migration.json
Normal file
|
@ -0,0 +1,151 @@
|
|||
{
|
||||
"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
|
||||
},
|
||||
"model": {
|
||||
"name": "Model",
|
||||
"value": "EasyTouch2 8"
|
||||
},
|
||||
"equipment": {
|
||||
"flags": 32796
|
||||
},
|
||||
"sensor": {
|
||||
"active_alert": {
|
||||
"name": "Active Alert",
|
||||
"value": 0,
|
||||
"device_type": "alarm"
|
||||
}
|
||||
}
|
||||
},
|
||||
"circuit": {},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"1": {
|
||||
"data": 0
|
||||
},
|
||||
"2": {
|
||||
"data": 0
|
||||
},
|
||||
"3": {
|
||||
"data": 0
|
||||
},
|
||||
"4": {
|
||||
"data": 0
|
||||
},
|
||||
"5": {
|
||||
"data": 0
|
||||
},
|
||||
"6": {
|
||||
"data": 0
|
||||
},
|
||||
"7": {
|
||||
"data": 0
|
||||
}
|
||||
},
|
||||
"body": {},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": 51,
|
||||
"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
|
||||
}
|
||||
}
|
960
tests/components/screenlogic/snapshots/test_diagnostics.ambr
Normal file
960
tests/components/screenlogic/snapshots/test_diagnostics.ambr
Normal file
|
@ -0,0 +1,960 @@
|
|||
# serializer version: 1
|
||||
# name: test_diagnostics
|
||||
dict({
|
||||
'config_entry': dict({
|
||||
'data': dict({
|
||||
'ip_address': '127.0.0.1',
|
||||
'port': 80,
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'domain': 'screenlogic',
|
||||
'entry_id': 'screenlogictest',
|
||||
'options': dict({
|
||||
'scan_interval': 30,
|
||||
}),
|
||||
'pref_disable_new_entities': False,
|
||||
'pref_disable_polling': False,
|
||||
'source': 'user',
|
||||
'title': 'Pentair DD-EE-FF',
|
||||
'unique_id': 'aa:bb:cc:dd:ee:ff',
|
||||
'version': 1,
|
||||
}),
|
||||
'data': dict({
|
||||
'adapter': dict({
|
||||
'firmware': dict({
|
||||
'name': 'Protocol Adapter Firmware',
|
||||
'value': 'POOL: 5.2 Build 736.0 Rel',
|
||||
}),
|
||||
}),
|
||||
'body': dict({
|
||||
'0': dict({
|
||||
'body_type': 0,
|
||||
'cool_setpoint': dict({
|
||||
'device_type': 'temperature',
|
||||
'name': 'Pool Cool Set Point',
|
||||
'unit': '°F',
|
||||
'value': 100,
|
||||
}),
|
||||
'heat_mode': dict({
|
||||
'device_type': 'enum',
|
||||
'enum_options': list([
|
||||
'Off',
|
||||
'Solar',
|
||||
'Solar Preferred',
|
||||
'Heater',
|
||||
"Don't Change",
|
||||
]),
|
||||
'name': 'Pool Heat Mode',
|
||||
'value': 0,
|
||||
}),
|
||||
'heat_setpoint': dict({
|
||||
'device_type': 'temperature',
|
||||
'name': 'Pool Heat Set Point',
|
||||
'unit': '°F',
|
||||
'value': 83,
|
||||
}),
|
||||
'heat_state': dict({
|
||||
'device_type': 'enum',
|
||||
'enum_options': list([
|
||||
'Off',
|
||||
'Solar',
|
||||
'Heater',
|
||||
'Both',
|
||||
]),
|
||||
'name': 'Pool Heat',
|
||||
'value': 0,
|
||||
}),
|
||||
'last_temperature': dict({
|
||||
'device_type': 'temperature',
|
||||
'name': 'Last Pool Temperature',
|
||||
'state_type': 'measurement',
|
||||
'unit': '°F',
|
||||
'value': 81,
|
||||
}),
|
||||
'max_setpoint': 104,
|
||||
'min_setpoint': 40,
|
||||
'name': 'Pool',
|
||||
}),
|
||||
'1': dict({
|
||||
'body_type': 1,
|
||||
'cool_setpoint': dict({
|
||||
'device_type': 'temperature',
|
||||
'name': 'Spa Cool Set Point',
|
||||
'unit': '°F',
|
||||
'value': 69,
|
||||
}),
|
||||
'heat_mode': dict({
|
||||
'device_type': 'enum',
|
||||
'enum_options': list([
|
||||
'Off',
|
||||
'Solar',
|
||||
'Solar Preferred',
|
||||
'Heater',
|
||||
"Don't Change",
|
||||
]),
|
||||
'name': 'Spa Heat Mode',
|
||||
'value': 0,
|
||||
}),
|
||||
'heat_setpoint': dict({
|
||||
'device_type': 'temperature',
|
||||
'name': 'Spa Heat Set Point',
|
||||
'unit': '°F',
|
||||
'value': 94,
|
||||
}),
|
||||
'heat_state': dict({
|
||||
'device_type': 'enum',
|
||||
'enum_options': list([
|
||||
'Off',
|
||||
'Solar',
|
||||
'Heater',
|
||||
'Both',
|
||||
]),
|
||||
'name': 'Spa Heat',
|
||||
'value': 0,
|
||||
}),
|
||||
'last_temperature': dict({
|
||||
'device_type': 'temperature',
|
||||
'name': 'Last Spa Temperature',
|
||||
'state_type': 'measurement',
|
||||
'unit': '°F',
|
||||
'value': 84,
|
||||
}),
|
||||
'max_setpoint': 104,
|
||||
'min_setpoint': 40,
|
||||
'name': 'Spa',
|
||||
}),
|
||||
}),
|
||||
'circuit': dict({
|
||||
'500': dict({
|
||||
'circuit_id': 500,
|
||||
'color': dict({
|
||||
'color_position': 0,
|
||||
'color_set': 0,
|
||||
'color_stagger': 0,
|
||||
}),
|
||||
'configuration': dict({
|
||||
'default_runtime': 720,
|
||||
'delay': 0,
|
||||
'flags': 1,
|
||||
'name_index': 71,
|
||||
'unknown_at_offset_62': 0,
|
||||
'unknown_at_offset_63': 0,
|
||||
}),
|
||||
'device_id': 1,
|
||||
'function': 1,
|
||||
'interface': 1,
|
||||
'name': 'Spa',
|
||||
'value': 0,
|
||||
}),
|
||||
'501': dict({
|
||||
'circuit_id': 501,
|
||||
'color': dict({
|
||||
'color_position': 0,
|
||||
'color_set': 0,
|
||||
'color_stagger': 0,
|
||||
}),
|
||||
'configuration': dict({
|
||||
'default_runtime': 720,
|
||||
'delay': 0,
|
||||
'flags': 0,
|
||||
'name_index': 85,
|
||||
'unknown_at_offset_94': 0,
|
||||
'unknown_at_offset_95': 0,
|
||||
}),
|
||||
'device_id': 2,
|
||||
'function': 0,
|
||||
'interface': 2,
|
||||
'name': 'Waterfall',
|
||||
'value': 0,
|
||||
}),
|
||||
'502': dict({
|
||||
'circuit_id': 502,
|
||||
'color': dict({
|
||||
'color_position': 0,
|
||||
'color_set': 2,
|
||||
'color_stagger': 2,
|
||||
}),
|
||||
'configuration': dict({
|
||||
'default_runtime': 720,
|
||||
'delay': 0,
|
||||
'flags': 0,
|
||||
'name_index': 62,
|
||||
'unknown_at_offset_126': 0,
|
||||
'unknown_at_offset_127': 0,
|
||||
}),
|
||||
'device_id': 3,
|
||||
'function': 16,
|
||||
'interface': 3,
|
||||
'name': 'Pool Light',
|
||||
'value': 0,
|
||||
}),
|
||||
'503': dict({
|
||||
'circuit_id': 503,
|
||||
'color': dict({
|
||||
'color_position': 1,
|
||||
'color_set': 6,
|
||||
'color_stagger': 10,
|
||||
}),
|
||||
'configuration': dict({
|
||||
'default_runtime': 720,
|
||||
'delay': 0,
|
||||
'flags': 0,
|
||||
'name_index': 73,
|
||||
'unknown_at_offset_158': 0,
|
||||
'unknown_at_offset_159': 0,
|
||||
}),
|
||||
'device_id': 4,
|
||||
'function': 16,
|
||||
'interface': 3,
|
||||
'name': 'Spa Light',
|
||||
'value': 0,
|
||||
}),
|
||||
'504': dict({
|
||||
'circuit_id': 504,
|
||||
'color': dict({
|
||||
'color_position': 0,
|
||||
'color_set': 0,
|
||||
'color_stagger': 0,
|
||||
}),
|
||||
'configuration': dict({
|
||||
'default_runtime': 240,
|
||||
'delay': 0,
|
||||
'flags': 0,
|
||||
'name_index': 21,
|
||||
'unknown_at_offset_186': 0,
|
||||
'unknown_at_offset_187': 0,
|
||||
}),
|
||||
'device_id': 5,
|
||||
'function': 5,
|
||||
'interface': 0,
|
||||
'name': 'Cleaner',
|
||||
'value': 0,
|
||||
}),
|
||||
'505': dict({
|
||||
'circuit_id': 505,
|
||||
'color': dict({
|
||||
'color_position': 0,
|
||||
'color_set': 0,
|
||||
'color_stagger': 0,
|
||||
}),
|
||||
'configuration': dict({
|
||||
'default_runtime': 720,
|
||||
'delay': 0,
|
||||
'flags': 1,
|
||||
'name_index': 63,
|
||||
'unknown_at_offset_214': 0,
|
||||
'unknown_at_offset_215': 0,
|
||||
}),
|
||||
'device_id': 6,
|
||||
'function': 2,
|
||||
'interface': 0,
|
||||
'name': 'Pool Low',
|
||||
'value': 0,
|
||||
}),
|
||||
'506': dict({
|
||||
'circuit_id': 506,
|
||||
'color': dict({
|
||||
'color_position': 0,
|
||||
'color_set': 0,
|
||||
'color_stagger': 0,
|
||||
}),
|
||||
'configuration': dict({
|
||||
'default_runtime': 720,
|
||||
'delay': 0,
|
||||
'flags': 0,
|
||||
'name_index': 91,
|
||||
'unknown_at_offset_246': 0,
|
||||
'unknown_at_offset_247': 0,
|
||||
}),
|
||||
'device_id': 7,
|
||||
'function': 7,
|
||||
'interface': 4,
|
||||
'name': 'Yard Light',
|
||||
'value': 0,
|
||||
}),
|
||||
'507': dict({
|
||||
'circuit_id': 507,
|
||||
'color': dict({
|
||||
'color_position': 0,
|
||||
'color_set': 0,
|
||||
'color_stagger': 0,
|
||||
}),
|
||||
'configuration': dict({
|
||||
'default_runtime': 1620,
|
||||
'delay': 0,
|
||||
'flags': 0,
|
||||
'name_index': 101,
|
||||
'unknown_at_offset_274': 0,
|
||||
'unknown_at_offset_275': 0,
|
||||
}),
|
||||
'device_id': 8,
|
||||
'function': 0,
|
||||
'interface': 2,
|
||||
'name': 'Cameras',
|
||||
'value': 1,
|
||||
}),
|
||||
'508': dict({
|
||||
'circuit_id': 508,
|
||||
'color': dict({
|
||||
'color_position': 0,
|
||||
'color_set': 0,
|
||||
'color_stagger': 0,
|
||||
}),
|
||||
'configuration': dict({
|
||||
'default_runtime': 720,
|
||||
'delay': 0,
|
||||
'flags': 0,
|
||||
'name_index': 61,
|
||||
'unknown_at_offset_306': 0,
|
||||
'unknown_at_offset_307': 0,
|
||||
}),
|
||||
'device_id': 9,
|
||||
'function': 0,
|
||||
'interface': 0,
|
||||
'name': 'Pool High',
|
||||
'value': 0,
|
||||
}),
|
||||
'510': dict({
|
||||
'circuit_id': 510,
|
||||
'color': dict({
|
||||
'color_position': 0,
|
||||
'color_set': 0,
|
||||
'color_stagger': 0,
|
||||
}),
|
||||
'configuration': dict({
|
||||
'default_runtime': 720,
|
||||
'delay': 0,
|
||||
'flags': 0,
|
||||
'name_index': 78,
|
||||
'unknown_at_offset_334': 0,
|
||||
'unknown_at_offset_335': 0,
|
||||
}),
|
||||
'device_id': 11,
|
||||
'function': 14,
|
||||
'interface': 1,
|
||||
'name': 'Spillway',
|
||||
'value': 0,
|
||||
}),
|
||||
'511': dict({
|
||||
'circuit_id': 511,
|
||||
'color': dict({
|
||||
'color_position': 0,
|
||||
'color_set': 0,
|
||||
'color_stagger': 0,
|
||||
}),
|
||||
'configuration': dict({
|
||||
'default_runtime': 720,
|
||||
'delay': 0,
|
||||
'flags': 0,
|
||||
'name_index': 61,
|
||||
'unknown_at_offset_366': 0,
|
||||
'unknown_at_offset_367': 0,
|
||||
}),
|
||||
'device_id': 12,
|
||||
'function': 0,
|
||||
'interface': 5,
|
||||
'name': 'Pool High',
|
||||
'value': 0,
|
||||
}),
|
||||
}),
|
||||
'controller': dict({
|
||||
'configuration': dict({
|
||||
'body_type': dict({
|
||||
'0': dict({
|
||||
'max_setpoint': 104,
|
||||
'min_setpoint': 40,
|
||||
}),
|
||||
'1': dict({
|
||||
'max_setpoint': 104,
|
||||
'min_setpoint': 40,
|
||||
}),
|
||||
}),
|
||||
'circuit_count': 11,
|
||||
'color': list([
|
||||
dict({
|
||||
'name': 'White',
|
||||
'value': list([
|
||||
255,
|
||||
255,
|
||||
255,
|
||||
]),
|
||||
}),
|
||||
dict({
|
||||
'name': 'Light Green',
|
||||
'value': list([
|
||||
160,
|
||||
255,
|
||||
160,
|
||||
]),
|
||||
}),
|
||||
dict({
|
||||
'name': 'Green',
|
||||
'value': list([
|
||||
0,
|
||||
255,
|
||||
80,
|
||||
]),
|
||||
}),
|
||||
dict({
|
||||
'name': 'Cyan',
|
||||
'value': list([
|
||||
0,
|
||||
255,
|
||||
200,
|
||||
]),
|
||||
}),
|
||||
dict({
|
||||
'name': 'Blue',
|
||||
'value': list([
|
||||
100,
|
||||
140,
|
||||
255,
|
||||
]),
|
||||
}),
|
||||
dict({
|
||||
'name': 'Lavender',
|
||||
'value': list([
|
||||
230,
|
||||
130,
|
||||
255,
|
||||
]),
|
||||
}),
|
||||
dict({
|
||||
'name': 'Magenta',
|
||||
'value': list([
|
||||
255,
|
||||
0,
|
||||
128,
|
||||
]),
|
||||
}),
|
||||
dict({
|
||||
'name': 'Light Magenta',
|
||||
'value': list([
|
||||
255,
|
||||
180,
|
||||
210,
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
'color_count': 8,
|
||||
'controller_data': 0,
|
||||
'controller_type': 13,
|
||||
'generic_circuit_name': 'Water Features',
|
||||
'hardware_type': 0,
|
||||
'interface_tab_flags': 127,
|
||||
'is_celsius': dict({
|
||||
'name': 'Is Celsius',
|
||||
'value': 0,
|
||||
}),
|
||||
'remotes': 0,
|
||||
'show_alarms': 0,
|
||||
'unknown_at_offset_09': 0,
|
||||
'unknown_at_offset_10': 0,
|
||||
'unknown_at_offset_11': 0,
|
||||
}),
|
||||
'controller_id': 100,
|
||||
'equipment': dict({
|
||||
'flags': 98360,
|
||||
'list': list([
|
||||
'INTELLIBRITE',
|
||||
'INTELLIFLO_0',
|
||||
'INTELLIFLO_1',
|
||||
'INTELLICHEM',
|
||||
'HYBRID_HEATER',
|
||||
]),
|
||||
}),
|
||||
'model': dict({
|
||||
'name': 'Model',
|
||||
'value': 'EasyTouch2 8',
|
||||
}),
|
||||
'sensor': dict({
|
||||
'active_alert': dict({
|
||||
'device_type': 'alarm',
|
||||
'name': 'Active Alert',
|
||||
'value': 0,
|
||||
}),
|
||||
'air_temperature': dict({
|
||||
'device_type': 'temperature',
|
||||
'name': 'Air Temperature',
|
||||
'state_type': 'measurement',
|
||||
'unit': '°F',
|
||||
'value': 69,
|
||||
}),
|
||||
'cleaner_delay': dict({
|
||||
'name': 'Cleaner Delay',
|
||||
'value': 0,
|
||||
}),
|
||||
'freeze_mode': dict({
|
||||
'name': 'Freeze Mode',
|
||||
'value': 0,
|
||||
}),
|
||||
'orp': dict({
|
||||
'name': 'ORP',
|
||||
'state_type': 'measurement',
|
||||
'unit': 'mV',
|
||||
'value': 728,
|
||||
}),
|
||||
'orp_supply_level': dict({
|
||||
'name': 'ORP Supply Level',
|
||||
'state_type': 'measurement',
|
||||
'value': 3,
|
||||
}),
|
||||
'ph': dict({
|
||||
'name': 'pH',
|
||||
'state_type': 'measurement',
|
||||
'unit': 'pH',
|
||||
'value': 7.61,
|
||||
}),
|
||||
'ph_supply_level': dict({
|
||||
'name': 'pH Supply Level',
|
||||
'state_type': 'measurement',
|
||||
'value': 2,
|
||||
}),
|
||||
'pool_delay': dict({
|
||||
'name': 'Pool Delay',
|
||||
'value': 0,
|
||||
}),
|
||||
'salt_ppm': dict({
|
||||
'name': 'Salt',
|
||||
'state_type': 'measurement',
|
||||
'unit': 'ppm',
|
||||
'value': 0,
|
||||
}),
|
||||
'saturation': dict({
|
||||
'name': 'Saturation Index',
|
||||
'state_type': 'measurement',
|
||||
'unit': 'lsi',
|
||||
'value': 0.06,
|
||||
}),
|
||||
'spa_delay': dict({
|
||||
'name': 'Spa Delay',
|
||||
'value': 0,
|
||||
}),
|
||||
'state': dict({
|
||||
'device_type': 'enum',
|
||||
'enum_options': list([
|
||||
'Unknown',
|
||||
'Ready',
|
||||
'Sync',
|
||||
'Service',
|
||||
]),
|
||||
'name': 'Controller State',
|
||||
'value': 1,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
'intellichem': dict({
|
||||
'alarm': dict({
|
||||
'flags': 1,
|
||||
'flow_alarm': dict({
|
||||
'device_type': 'alarm',
|
||||
'name': 'Flow Alarm',
|
||||
'value': 1,
|
||||
}),
|
||||
'orp_high_alarm': dict({
|
||||
'device_type': 'alarm',
|
||||
'name': 'ORP HIGH Alarm',
|
||||
'value': 0,
|
||||
}),
|
||||
'orp_low_alarm': dict({
|
||||
'device_type': 'alarm',
|
||||
'name': 'ORP LOW Alarm',
|
||||
'value': 0,
|
||||
}),
|
||||
'orp_supply_alarm': dict({
|
||||
'device_type': 'alarm',
|
||||
'name': 'ORP Supply Alarm',
|
||||
'value': 0,
|
||||
}),
|
||||
'ph_high_alarm': dict({
|
||||
'device_type': 'alarm',
|
||||
'name': 'pH HIGH Alarm',
|
||||
'value': 0,
|
||||
}),
|
||||
'ph_low_alarm': dict({
|
||||
'device_type': 'alarm',
|
||||
'name': 'pH LOW Alarm',
|
||||
'value': 0,
|
||||
}),
|
||||
'ph_supply_alarm': dict({
|
||||
'device_type': 'alarm',
|
||||
'name': 'pH Supply Alarm',
|
||||
'value': 0,
|
||||
}),
|
||||
'probe_fault_alarm': dict({
|
||||
'device_type': 'alarm',
|
||||
'name': 'Probe Fault',
|
||||
'value': 0,
|
||||
}),
|
||||
}),
|
||||
'alert': dict({
|
||||
'flags': 0,
|
||||
'orp_limit': dict({
|
||||
'name': 'ORP Dose Limit Reached',
|
||||
'value': 0,
|
||||
}),
|
||||
'ph_limit': dict({
|
||||
'name': 'pH Dose Limit Reached',
|
||||
'value': 0,
|
||||
}),
|
||||
'ph_lockout': dict({
|
||||
'name': 'pH Lockout',
|
||||
'value': 0,
|
||||
}),
|
||||
}),
|
||||
'configuration': dict({
|
||||
'calcium_harness': dict({
|
||||
'name': 'Calcium Hardness',
|
||||
'unit': 'ppm',
|
||||
'value': 800,
|
||||
}),
|
||||
'cya': dict({
|
||||
'name': 'Cyanuric Acid',
|
||||
'unit': 'ppm',
|
||||
'value': 45,
|
||||
}),
|
||||
'flags': 32,
|
||||
'orp_setpoint': dict({
|
||||
'name': 'ORP Setpoint',
|
||||
'unit': 'mV',
|
||||
'value': 720,
|
||||
}),
|
||||
'ph_setpoint': dict({
|
||||
'name': 'pH Setpoint',
|
||||
'unit': 'pH',
|
||||
'value': 7.6,
|
||||
}),
|
||||
'probe_is_celsius': 0,
|
||||
'salt_tds_ppm': dict({
|
||||
'name': 'Salt/TDS',
|
||||
'unit': 'ppm',
|
||||
'value': 1000,
|
||||
}),
|
||||
'total_alkalinity': dict({
|
||||
'name': 'Total Alkalinity',
|
||||
'unit': 'ppm',
|
||||
'value': 45,
|
||||
}),
|
||||
}),
|
||||
'dose_status': dict({
|
||||
'flags': 149,
|
||||
'orp_dosing_state': dict({
|
||||
'device_type': 'enum',
|
||||
'enum_options': list([
|
||||
'Dosing',
|
||||
'Mixing',
|
||||
'Monitoring',
|
||||
]),
|
||||
'name': 'ORP Dosing State',
|
||||
'value': 2,
|
||||
}),
|
||||
'orp_last_dose_time': dict({
|
||||
'device_type': 'duration',
|
||||
'name': 'Last ORP Dose Time',
|
||||
'state_type': 'total_increasing',
|
||||
'unit': 'sec',
|
||||
'value': 4,
|
||||
}),
|
||||
'orp_last_dose_volume': dict({
|
||||
'device_type': 'volume',
|
||||
'name': 'Last ORP Dose Volume',
|
||||
'state_type': 'total_increasing',
|
||||
'unit': 'mL',
|
||||
'value': 8,
|
||||
}),
|
||||
'ph_dosing_state': dict({
|
||||
'device_type': 'enum',
|
||||
'enum_options': list([
|
||||
'Dosing',
|
||||
'Mixing',
|
||||
'Monitoring',
|
||||
]),
|
||||
'name': 'pH Dosing State',
|
||||
'value': 1,
|
||||
}),
|
||||
'ph_last_dose_time': dict({
|
||||
'device_type': 'duration',
|
||||
'name': 'Last pH Dose Time',
|
||||
'state_type': 'total_increasing',
|
||||
'unit': 'sec',
|
||||
'value': 5,
|
||||
}),
|
||||
'ph_last_dose_volume': dict({
|
||||
'device_type': 'volume',
|
||||
'name': 'Last pH Dose Volume',
|
||||
'state_type': 'total_increasing',
|
||||
'unit': 'mL',
|
||||
'value': 8,
|
||||
}),
|
||||
}),
|
||||
'firmware': dict({
|
||||
'name': 'IntelliChem Firmware',
|
||||
'value': '1.060',
|
||||
}),
|
||||
'sensor': dict({
|
||||
'orp_now': dict({
|
||||
'name': 'ORP Now',
|
||||
'state_type': 'measurement',
|
||||
'unit': 'mV',
|
||||
'value': 0,
|
||||
}),
|
||||
'orp_supply_level': dict({
|
||||
'name': 'ORP Supply Level',
|
||||
'state_type': 'measurement',
|
||||
'value': 3,
|
||||
}),
|
||||
'ph_now': dict({
|
||||
'name': 'pH Now',
|
||||
'state_type': 'measurement',
|
||||
'unit': 'pH',
|
||||
'value': 0.0,
|
||||
}),
|
||||
'ph_probe_water_temp': dict({
|
||||
'device_type': 'temperature',
|
||||
'name': 'pH Probe Water Temperature',
|
||||
'state_type': 'measurement',
|
||||
'unit': '°F',
|
||||
'value': 81,
|
||||
}),
|
||||
'ph_supply_level': dict({
|
||||
'name': 'pH Supply Level',
|
||||
'state_type': 'measurement',
|
||||
'value': 2,
|
||||
}),
|
||||
'saturation': dict({
|
||||
'name': 'Saturation Index',
|
||||
'state_type': 'measurement',
|
||||
'unit': 'lsi',
|
||||
'value': 0.06,
|
||||
}),
|
||||
}),
|
||||
'unknown_at_offset_00': 42,
|
||||
'unknown_at_offset_04': 0,
|
||||
'unknown_at_offset_44': 0,
|
||||
'unknown_at_offset_45': 0,
|
||||
'unknown_at_offset_46': 0,
|
||||
'water_balance': dict({
|
||||
'corrosive': dict({
|
||||
'device_type': 'alarm',
|
||||
'name': 'SI Corrosive',
|
||||
'value': 0,
|
||||
}),
|
||||
'flags': 0,
|
||||
'scaling': dict({
|
||||
'device_type': 'alarm',
|
||||
'name': 'SI Scaling',
|
||||
'value': 0,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
'pump': dict({
|
||||
'0': dict({
|
||||
'data': 70,
|
||||
'gpm_now': dict({
|
||||
'name': 'Pool Low Pump GPM Now',
|
||||
'state_type': 'measurement',
|
||||
'unit': 'gpm',
|
||||
'value': 0,
|
||||
}),
|
||||
'preset': dict({
|
||||
'0': dict({
|
||||
'device_id': 6,
|
||||
'is_rpm': 0,
|
||||
'setpoint': 63,
|
||||
}),
|
||||
'1': dict({
|
||||
'device_id': 9,
|
||||
'is_rpm': 0,
|
||||
'setpoint': 72,
|
||||
}),
|
||||
'2': dict({
|
||||
'device_id': 1,
|
||||
'is_rpm': 1,
|
||||
'setpoint': 3450,
|
||||
}),
|
||||
'3': dict({
|
||||
'device_id': 130,
|
||||
'is_rpm': 0,
|
||||
'setpoint': 75,
|
||||
}),
|
||||
'4': dict({
|
||||
'device_id': 12,
|
||||
'is_rpm': 0,
|
||||
'setpoint': 72,
|
||||
}),
|
||||
'5': dict({
|
||||
'device_id': 0,
|
||||
'is_rpm': 0,
|
||||
'setpoint': 30,
|
||||
}),
|
||||
'6': dict({
|
||||
'device_id': 0,
|
||||
'is_rpm': 0,
|
||||
'setpoint': 30,
|
||||
}),
|
||||
'7': dict({
|
||||
'device_id': 0,
|
||||
'is_rpm': 0,
|
||||
'setpoint': 30,
|
||||
}),
|
||||
}),
|
||||
'rpm_now': dict({
|
||||
'name': 'Pool Low Pump RPM Now',
|
||||
'state_type': 'measurement',
|
||||
'unit': 'rpm',
|
||||
'value': 0,
|
||||
}),
|
||||
'state': dict({
|
||||
'name': 'Pool Low Pump',
|
||||
'value': 0,
|
||||
}),
|
||||
'type': 3,
|
||||
'unknown_at_offset_16': 0,
|
||||
'unknown_at_offset_24': 255,
|
||||
'watts_now': dict({
|
||||
'device_type': 'power',
|
||||
'name': 'Pool Low Pump Watts Now',
|
||||
'state_type': 'measurement',
|
||||
'unit': 'W',
|
||||
'value': 0,
|
||||
}),
|
||||
}),
|
||||
'1': dict({
|
||||
'data': 66,
|
||||
'gpm_now': dict({
|
||||
'name': 'Waterfall Pump GPM Now',
|
||||
'state_type': 'measurement',
|
||||
'unit': 'gpm',
|
||||
'value': 0,
|
||||
}),
|
||||
'preset': dict({
|
||||
'0': dict({
|
||||
'device_id': 2,
|
||||
'is_rpm': 1,
|
||||
'setpoint': 2700,
|
||||
}),
|
||||
'1': dict({
|
||||
'device_id': 0,
|
||||
'is_rpm': 0,
|
||||
'setpoint': 30,
|
||||
}),
|
||||
'2': dict({
|
||||
'device_id': 0,
|
||||
'is_rpm': 0,
|
||||
'setpoint': 30,
|
||||
}),
|
||||
'3': dict({
|
||||
'device_id': 0,
|
||||
'is_rpm': 0,
|
||||
'setpoint': 30,
|
||||
}),
|
||||
'4': dict({
|
||||
'device_id': 0,
|
||||
'is_rpm': 0,
|
||||
'setpoint': 30,
|
||||
}),
|
||||
'5': dict({
|
||||
'device_id': 0,
|
||||
'is_rpm': 0,
|
||||
'setpoint': 30,
|
||||
}),
|
||||
'6': dict({
|
||||
'device_id': 0,
|
||||
'is_rpm': 0,
|
||||
'setpoint': 30,
|
||||
}),
|
||||
'7': dict({
|
||||
'device_id': 0,
|
||||
'is_rpm': 0,
|
||||
'setpoint': 30,
|
||||
}),
|
||||
}),
|
||||
'rpm_now': dict({
|
||||
'name': 'Waterfall Pump RPM Now',
|
||||
'state_type': 'measurement',
|
||||
'unit': 'rpm',
|
||||
'value': 0,
|
||||
}),
|
||||
'state': dict({
|
||||
'name': 'Waterfall Pump',
|
||||
'value': 0,
|
||||
}),
|
||||
'type': 3,
|
||||
'unknown_at_offset_16': 0,
|
||||
'unknown_at_offset_24': 255,
|
||||
'watts_now': dict({
|
||||
'device_type': 'power',
|
||||
'name': 'Waterfall Pump Watts Now',
|
||||
'state_type': 'measurement',
|
||||
'unit': 'W',
|
||||
'value': 0,
|
||||
}),
|
||||
}),
|
||||
'2': dict({
|
||||
'data': 0,
|
||||
}),
|
||||
'3': dict({
|
||||
'data': 0,
|
||||
}),
|
||||
'4': dict({
|
||||
'data': 0,
|
||||
}),
|
||||
'5': dict({
|
||||
'data': 0,
|
||||
}),
|
||||
'6': dict({
|
||||
'data': 0,
|
||||
}),
|
||||
'7': dict({
|
||||
'data': 0,
|
||||
}),
|
||||
}),
|
||||
'scg': dict({
|
||||
'configuration': dict({
|
||||
'pool_setpoint': dict({
|
||||
'body_type': 0,
|
||||
'max_setpoint': 100,
|
||||
'min_setpoint': 0,
|
||||
'name': 'Pool Chlorinator Setpoint',
|
||||
'step': 5,
|
||||
'unit': '%',
|
||||
'value': 51,
|
||||
}),
|
||||
'spa_setpoint': dict({
|
||||
'body_type': 1,
|
||||
'max_setpoint': 100,
|
||||
'min_setpoint': 0,
|
||||
'name': 'Spa Chlorinator Setpoint',
|
||||
'step': 5,
|
||||
'unit': '%',
|
||||
'value': 0,
|
||||
}),
|
||||
'super_chlor_timer': dict({
|
||||
'max_setpoint': 72,
|
||||
'min_setpoint': 1,
|
||||
'name': 'Super Chlorination Timer',
|
||||
'step': 1,
|
||||
'unit': 'hr',
|
||||
'value': 0,
|
||||
}),
|
||||
}),
|
||||
'flags': 0,
|
||||
'scg_present': 0,
|
||||
'sensor': dict({
|
||||
'salt_ppm': dict({
|
||||
'name': 'Chlorinator Salt',
|
||||
'state_type': 'measurement',
|
||||
'unit': 'ppm',
|
||||
'value': 0,
|
||||
}),
|
||||
'state': dict({
|
||||
'name': 'Chlorinator',
|
||||
'value': 0,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
'debug': dict({
|
||||
}),
|
||||
})
|
||||
# ---
|
|
@ -2,7 +2,7 @@
|
|||
from unittest.mock import patch
|
||||
|
||||
from screenlogicpy import ScreenLogicError
|
||||
from screenlogicpy.const import (
|
||||
from screenlogicpy.const.common import (
|
||||
SL_GATEWAY_IP,
|
||||
SL_GATEWAY_NAME,
|
||||
SL_GATEWAY_PORT,
|
||||
|
|
91
tests/components/screenlogic/test_data.py
Normal file
91
tests/components/screenlogic/test_data.py
Normal file
|
@ -0,0 +1,91 @@
|
|||
"""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
|
||||
|
||||
from . import (
|
||||
DATA_MIN_ENTITY_CLEANUP,
|
||||
GATEWAY_DISCOVERY_IMPORT_PATH,
|
||||
MOCK_ADAPTER_MAC,
|
||||
MOCK_ADAPTER_NAME,
|
||||
stub_async_connect,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_async_cleanup_entries(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test cleanup of unused entities."""
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
entity_registry = er.async_get(hass)
|
||||
device_registry = dr.async_get(hass)
|
||||
|
||||
device: dr.DeviceEntry = device_registry.async_get_or_create(
|
||||
config_entry_id=mock_config_entry.entry_id,
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, MOCK_ADAPTER_MAC)},
|
||||
)
|
||||
|
||||
TEST_UNUSED_ENTRY = {
|
||||
"domain": SENSOR_DOMAIN,
|
||||
"platform": DOMAIN,
|
||||
"unique_id": f"{MOCK_ADAPTER_MAC}_saturation",
|
||||
"suggested_object_id": f"{MOCK_ADAPTER_NAME} Saturation Index",
|
||||
"disabled_by": None,
|
||||
"has_entity_name": True,
|
||||
"original_name": "Saturation Index",
|
||||
}
|
||||
|
||||
unused_entity: er.RegistryEntry = entity_registry.async_get_or_create(
|
||||
**TEST_UNUSED_ENTRY, device_id=device.id, config_entry=mock_config_entry
|
||||
)
|
||||
|
||||
assert unused_entity
|
||||
assert unused_entity.unique_id == TEST_UNUSED_ENTRY["unique_id"]
|
||||
|
||||
with patch(
|
||||
GATEWAY_DISCOVERY_IMPORT_PATH,
|
||||
return_value={},
|
||||
), patch.multiple(
|
||||
ScreenLogicGateway,
|
||||
async_connect=lambda *args, **kwargs: stub_async_connect(
|
||||
DATA_MIN_ENTITY_CLEANUP, *args, **kwargs
|
||||
),
|
||||
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()
|
||||
|
||||
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),
|
||||
)
|
56
tests/components/screenlogic/test_diagnostics.py
Normal file
56
tests/components/screenlogic/test_diagnostics.py
Normal file
|
@ -0,0 +1,56 @@
|
|||
"""Testing for ScreenLogic diagnostics."""
|
||||
from unittest.mock import DEFAULT, patch
|
||||
|
||||
from screenlogicpy import ScreenLogicGateway
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from . import (
|
||||
DATA_FULL_CHEM,
|
||||
GATEWAY_DISCOVERY_IMPORT_PATH,
|
||||
MOCK_ADAPTER_MAC,
|
||||
stub_async_connect,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.components.diagnostics import get_diagnostics_for_config_entry
|
||||
from tests.typing import ClientSessionGenerator
|
||||
|
||||
|
||||
async def test_diagnostics(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test diagnostics."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
device_registry = dr.async_get(hass)
|
||||
|
||||
device_registry.async_get_or_create(
|
||||
config_entry_id=mock_config_entry.entry_id,
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, MOCK_ADAPTER_MAC)},
|
||||
)
|
||||
with patch(
|
||||
GATEWAY_DISCOVERY_IMPORT_PATH,
|
||||
return_value={},
|
||||
), patch.multiple(
|
||||
ScreenLogicGateway,
|
||||
async_connect=lambda *args, **kwargs: stub_async_connect(
|
||||
DATA_FULL_CHEM, *args, **kwargs
|
||||
),
|
||||
is_connected=True,
|
||||
_async_connected_request=DEFAULT,
|
||||
get_debug=lambda self: {},
|
||||
):
|
||||
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
diag = await get_diagnostics_for_config_entry(
|
||||
hass, hass_client, mock_config_entry
|
||||
)
|
||||
|
||||
assert diag == snapshot
|
236
tests/components/screenlogic/test_init.py
Normal file
236
tests/components/screenlogic/test_init.py
Normal file
|
@ -0,0 +1,236 @@
|
|||
"""Tests for ScreenLogic integration init."""
|
||||
from dataclasses import dataclass
|
||||
from unittest.mock import DEFAULT, patch
|
||||
|
||||
import pytest
|
||||
from screenlogicpy import ScreenLogicGateway
|
||||
|
||||
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
|
||||
from homeassistant.components.screenlogic import DOMAIN
|
||||
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
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from . import (
|
||||
DATA_MIN_MIGRATION,
|
||||
GATEWAY_DISCOVERY_IMPORT_PATH,
|
||||
MOCK_ADAPTER_MAC,
|
||||
MOCK_ADAPTER_NAME,
|
||||
stub_async_connect,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@dataclass
|
||||
class EntityMigrationData:
|
||||
"""Class to organize minimum entity data."""
|
||||
|
||||
old_name: str
|
||||
old_key: str
|
||||
new_name: str
|
||||
new_key: str
|
||||
domain: str
|
||||
|
||||
|
||||
TEST_MIGRATING_ENTITIES = [
|
||||
EntityMigrationData(
|
||||
"Chemistry Alarm",
|
||||
"chem_alarm",
|
||||
"Active Alert",
|
||||
"active_alert",
|
||||
BINARY_SENSOR_DOMAIN,
|
||||
),
|
||||
EntityMigrationData(
|
||||
"Pool Low Pump Current Watts",
|
||||
"currentWatts_0",
|
||||
"Pool Low Pump Watts Now",
|
||||
"pump_0_watts_now",
|
||||
SENSOR_DOMAIN,
|
||||
),
|
||||
EntityMigrationData(
|
||||
"SCG Status",
|
||||
"scg_status",
|
||||
"Chlorinator",
|
||||
"scg_state",
|
||||
BINARY_SENSOR_DOMAIN,
|
||||
),
|
||||
EntityMigrationData(
|
||||
"Non-Migrating Sensor",
|
||||
"nonmigrating",
|
||||
"Non-Migrating Sensor",
|
||||
"nonmigrating",
|
||||
SENSOR_DOMAIN,
|
||||
),
|
||||
EntityMigrationData(
|
||||
"Cyanuric Acid",
|
||||
"chem_cya",
|
||||
"Cyanuric Acid",
|
||||
"chem_cya",
|
||||
SENSOR_DOMAIN,
|
||||
),
|
||||
EntityMigrationData(
|
||||
"Old Sensor",
|
||||
"old_sensor",
|
||||
"Old Sensor",
|
||||
"old_sensor",
|
||||
SENSOR_DOMAIN,
|
||||
),
|
||||
]
|
||||
|
||||
MIGRATION_CONNECT = lambda *args, **kwargs: stub_async_connect(
|
||||
DATA_MIN_MIGRATION, *args, **kwargs
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("entity_def", "ent_data"),
|
||||
[
|
||||
(
|
||||
{
|
||||
"domain": ent_data.domain,
|
||||
"platform": DOMAIN,
|
||||
"unique_id": f"{MOCK_ADAPTER_MAC}_{ent_data.old_key}",
|
||||
"suggested_object_id": f"{MOCK_ADAPTER_NAME} {ent_data.old_name}",
|
||||
"disabled_by": None,
|
||||
"has_entity_name": True,
|
||||
"original_name": ent_data.old_name,
|
||||
},
|
||||
ent_data,
|
||||
)
|
||||
for ent_data in TEST_MIGRATING_ENTITIES
|
||||
],
|
||||
ids=[ent_data.old_name for ent_data in TEST_MIGRATING_ENTITIES],
|
||||
)
|
||||
async def test_async_migrate_entries(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_def: dict,
|
||||
ent_data: EntityMigrationData,
|
||||
) -> None:
|
||||
"""Test migration to new entity names."""
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
entity_registry = er.async_get(hass)
|
||||
device_registry = dr.async_get(hass)
|
||||
|
||||
device: dr.DeviceEntry = device_registry.async_get_or_create(
|
||||
config_entry_id=mock_config_entry.entry_id,
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, MOCK_ADAPTER_MAC)},
|
||||
)
|
||||
|
||||
TEST_EXISTING_ENTRY = {
|
||||
"domain": SENSOR_DOMAIN,
|
||||
"platform": DOMAIN,
|
||||
"unique_id": f"{MOCK_ADAPTER_MAC}_cya",
|
||||
"suggested_object_id": f"{MOCK_ADAPTER_NAME} CYA",
|
||||
"disabled_by": None,
|
||||
"has_entity_name": True,
|
||||
"original_name": "CYA",
|
||||
}
|
||||
|
||||
entity_registry.async_get_or_create(
|
||||
**TEST_EXISTING_ENTRY, device_id=device.id, config_entry=mock_config_entry
|
||||
)
|
||||
|
||||
entity: er.RegistryEntry = entity_registry.async_get_or_create(
|
||||
**entity_def, device_id=device.id, config_entry=mock_config_entry
|
||||
)
|
||||
|
||||
old_eid = f"{ent_data.domain}.{slugify(f'{MOCK_ADAPTER_NAME} {ent_data.old_name}')}"
|
||||
old_uid = f"{MOCK_ADAPTER_MAC}_{ent_data.old_key}"
|
||||
new_eid = f"{ent_data.domain}.{slugify(f'{MOCK_ADAPTER_NAME} {ent_data.new_name}')}"
|
||||
new_uid = f"{MOCK_ADAPTER_MAC}_{ent_data.new_key}"
|
||||
|
||||
assert entity.unique_id == old_uid
|
||||
assert entity.entity_id == old_eid
|
||||
|
||||
with patch(
|
||||
GATEWAY_DISCOVERY_IMPORT_PATH,
|
||||
return_value={},
|
||||
), patch.multiple(
|
||||
ScreenLogicGateway,
|
||||
async_connect=MIGRATION_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()
|
||||
|
||||
entity_migrated = entity_registry.async_get(new_eid)
|
||||
assert entity_migrated
|
||||
assert entity_migrated.entity_id == new_eid
|
||||
assert entity_migrated.unique_id == new_uid
|
||||
assert entity_migrated.original_name == ent_data.new_name
|
||||
|
||||
|
||||
async def test_entity_migration_data(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test ENTITY_MIGRATION data guards."""
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
entity_registry = er.async_get(hass)
|
||||
device_registry = dr.async_get(hass)
|
||||
|
||||
device: dr.DeviceEntry = device_registry.async_get_or_create(
|
||||
config_entry_id=mock_config_entry.entry_id,
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, MOCK_ADAPTER_MAC)},
|
||||
)
|
||||
|
||||
TEST_EXISTING_ENTRY = {
|
||||
"domain": SENSOR_DOMAIN,
|
||||
"platform": DOMAIN,
|
||||
"unique_id": f"{MOCK_ADAPTER_MAC}_missing_device",
|
||||
"suggested_object_id": f"{MOCK_ADAPTER_NAME} Missing Migration Device",
|
||||
"disabled_by": None,
|
||||
"has_entity_name": True,
|
||||
"original_name": "EMissing Migration Device",
|
||||
}
|
||||
|
||||
original_entity: er.RegistryEntry = entity_registry.async_get_or_create(
|
||||
**TEST_EXISTING_ENTRY, device_id=device.id, config_entry=mock_config_entry
|
||||
)
|
||||
|
||||
old_eid = original_entity.entity_id
|
||||
old_uid = original_entity.unique_id
|
||||
|
||||
assert old_uid == f"{MOCK_ADAPTER_MAC}_missing_device"
|
||||
assert (
|
||||
old_eid
|
||||
== f"{SENSOR_DOMAIN}.{slugify(f'{MOCK_ADAPTER_NAME} Missing Migration Device')}"
|
||||
)
|
||||
|
||||
# This patch simulates bad data being added to ENTITY_MIGRATIONS
|
||||
with patch.dict(
|
||||
"homeassistant.components.screenlogic.data.ENTITY_MIGRATIONS",
|
||||
{
|
||||
"missing_device": {
|
||||
"new_key": "state",
|
||||
"old_name": "Missing Migration Device",
|
||||
"new_name": "Bad ENTITY_MIGRATIONS Entry",
|
||||
},
|
||||
},
|
||||
), patch(
|
||||
GATEWAY_DISCOVERY_IMPORT_PATH,
|
||||
return_value={},
|
||||
), patch.multiple(
|
||||
ScreenLogicGateway,
|
||||
async_connect=MIGRATION_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()
|
||||
|
||||
entity_migrated = entity_registry.async_get(
|
||||
slugify(f"{MOCK_ADAPTER_NAME} Bad ENTITY_MIGRATIONS Entry")
|
||||
)
|
||||
assert entity_migrated is None
|
||||
|
||||
entity_not_migrated = entity_registry.async_get(old_eid)
|
||||
assert entity_not_migrated == original_entity
|
Loading…
Add table
Reference in a new issue