Support local push updates for most ScreenLogic entities (#87438)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Kevin Worrel 2023-02-06 18:13:36 -08:00 committed by GitHub
parent 4fbb14ecc7
commit 687d326bb4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 352 additions and 252 deletions

View file

@ -1008,6 +1008,7 @@ omit =
homeassistant/components/screenlogic/__init__.py
homeassistant/components/screenlogic/binary_sensor.py
homeassistant/components/screenlogic/climate.py
homeassistant/components/screenlogic/entity.py
homeassistant/components/screenlogic/light.py
homeassistant/components/screenlogic/number.py
homeassistant/components/screenlogic/sensor.py

View file

@ -1,12 +1,12 @@
"""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,
ON_OFF,
SL_GATEWAY_IP,
SL_GATEWAY_NAME,
SL_GATEWAY_PORT,
@ -17,15 +17,8 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, CONF_SCAN_INTERVAL, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
UpdateFailed,
)
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
@ -52,12 +45,12 @@ PLATFORMS = [
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Screenlogic from a config entry."""
gateway = ScreenLogicGateway()
connect_info = await async_get_connect_info(hass, entry)
gateway = ScreenLogicGateway(**connect_info)
try:
await gateway.async_connect()
await gateway.async_connect(**connect_info)
except ScreenLogicError as ex:
_LOGGER.error("Error while connecting to the gateway %s: %s", connect_info, ex)
raise ConfigEntryNotReady from ex
@ -119,11 +112,16 @@ async def async_get_connect_info(hass: HomeAssistant, entry: ConfigEntry):
class ScreenlogicDataUpdateCoordinator(DataUpdateCoordinator):
"""Class to manage the data update for the Screenlogic component."""
def __init__(self, hass, *, config_entry, gateway):
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
self.screenlogic_data = {}
interval = timedelta(
seconds=config_entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
@ -140,17 +138,34 @@ class ScreenlogicDataUpdateCoordinator(DataUpdateCoordinator):
),
)
@property
def gateway_data(self) -> dict[str | int, Any]:
"""Return the gateway data."""
return self.gateway.get_data()
async def _async_update_configured_data(self):
"""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):
"""Fetch data from the Screenlogic gateway."""
try:
await self.gateway.async_update()
await self._async_update_configured_data()
except ScreenLogicError as error:
_LOGGER.warning("Update error - attempting reconnect: %s", error)
await self._async_reconnect_update_data()
except ScreenLogicWarning as warn:
raise UpdateFailed(f"Incomplete update: {warn}") from warn
return self.gateway.get_data()
return None
async def _async_reconnect_update_data(self):
"""Attempt to reconnect to the gateway and fetch data."""
@ -159,125 +174,9 @@ class ScreenlogicDataUpdateCoordinator(DataUpdateCoordinator):
await self.gateway.async_disconnect()
connect_info = await async_get_connect_info(self.hass, self.config_entry)
self.gateway = ScreenLogicGateway(**connect_info)
await self.gateway.async_connect(**connect_info)
await self.gateway.async_update()
await self._async_update_configured_data()
except (ScreenLogicError, ScreenLogicWarning) as ex:
raise UpdateFailed(ex) from ex
class ScreenlogicEntity(CoordinatorEntity[ScreenlogicDataUpdateCoordinator]):
"""Base class for all ScreenLogic entities."""
def __init__(self, coordinator, data_key, enabled=True):
"""Initialize of the entity."""
super().__init__(coordinator)
self._data_key = data_key
self._enabled_default = enabled
@property
def entity_registry_enabled_default(self):
"""Entity enabled by default."""
return self._enabled_default
@property
def mac(self):
"""Mac address."""
return self.coordinator.config_entry.unique_id
@property
def unique_id(self):
"""Entity Unique ID."""
return f"{self.mac}_{self._data_key}"
@property
def config_data(self):
"""Shortcut for config data."""
return self.coordinator.data["config"]
@property
def gateway(self):
"""Return the gateway."""
return self.coordinator.gateway
@property
def gateway_name(self):
"""Return the configured name of the gateway."""
return self.gateway.name
@property
def device_info(self) -> DeviceInfo:
"""Return device information for the controller."""
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}"
return DeviceInfo(
connections={(dr.CONNECTION_NETWORK_MAC, self.mac)},
manufacturer="Pentair",
model=equipment_model,
name=self.gateway_name,
sw_version=self.gateway.version,
)
async def _async_refresh(self):
"""Refresh the data from the gateway."""
await self.coordinator.async_refresh()
# Second debounced refresh to catch any secondary
# changes in the device
await self.coordinator.async_request_refresh()
async def _async_refresh_timed(self, now):
"""Refresh from a timed called."""
await self.coordinator.async_request_refresh()
class ScreenLogicCircuitEntity(ScreenlogicEntity):
"""ScreenLogic circuit entity."""
_attr_has_entity_name = True
@property
def name(self):
"""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
async def async_turn_on(self, **kwargs) -> None:
"""Send the ON command."""
await self._async_set_circuit(ON_OFF.ON)
async def async_turn_off(self, **kwargs) -> None:
"""Send the OFF command."""
await self._async_set_circuit(ON_OFF.OFF)
# Turning off spa or pool circuit may require more time for the
# heater to reflect changes depending on the pool controller,
# so we schedule an extra refresh a bit farther out
if self._data_key in PRIMARY_CIRCUIT_IDS:
async_call_later(
self.hass, HEATER_COOLDOWN_DELAY, self._async_refresh_timed
)
async def _async_set_circuit(self, circuit_value) -> None:
if await self.gateway.async_set_circuit(self._data_key, circuit_value):
_LOGGER.debug("Turn %s %s", self._data_key, circuit_value)
await self._async_refresh()
else:
_LOGGER.warning(
"Failed to set_circuit %s %s", self._data_key, circuit_value
)
@property
def circuit(self):
"""Shortcut to access the circuit."""
return self.coordinator.data[SL_DATA.KEY_CIRCUITS][self._data_key]

View file

@ -1,5 +1,5 @@
"""Support for a ScreenLogic Binary Sensor."""
from screenlogicpy.const import DATA as SL_DATA, DEVICE_TYPE, EQUIPMENT, ON_OFF
from screenlogicpy.const import CODE, DATA as SL_DATA, DEVICE_TYPE, EQUIPMENT, ON_OFF
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
@ -10,8 +10,9 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import ScreenlogicEntity
from . import ScreenlogicDataUpdateCoordinator
from .const import DOMAIN
from .entity import ScreenlogicEntity, ScreenLogicPushEntity
SL_DEVICE_TYPE_TO_HA_DEVICE_CLASS = {DEVICE_TYPE.ALARM: BinarySensorDeviceClass.PROBLEM}
@ -29,69 +30,70 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up entry."""
entities = []
coordinator = hass.data[DOMAIN][config_entry.entry_id]
entities: list[ScreenLogicBinarySensorEntity] = []
coordinator: ScreenlogicDataUpdateCoordinator = hass.data[DOMAIN][
config_entry.entry_id
]
gateway_data = coordinator.gateway_data
chemistry = gateway_data[SL_DATA.KEY_CHEMISTRY]
config = gateway_data[SL_DATA.KEY_CONFIG]
# Generic binary sensor
entities.append(ScreenLogicBinarySensor(coordinator, "chem_alarm"))
entities.append(
ScreenLogicStatusBinarySensor(coordinator, "chem_alarm", CODE.STATUS_CHANGED)
)
entities.extend(
[
ScreenlogicConfigBinarySensor(coordinator, cfg_sensor)
for cfg_sensor in coordinator.data[SL_DATA.KEY_CONFIG]
ScreenlogicConfigBinarySensor(coordinator, cfg_sensor, CODE.STATUS_CHANGED)
for cfg_sensor in config
if cfg_sensor in SUPPORTED_CONFIG_BINARY_SENSORS
]
)
if (
coordinator.data[SL_DATA.KEY_CONFIG]["equipment_flags"]
& EQUIPMENT.FLAG_INTELLICHEM
):
if config["equipment_flags"] & EQUIPMENT.FLAG_INTELLICHEM:
# IntelliChem alarm sensors
entities.extend(
[
ScreenlogicChemistryAlarmBinarySensor(coordinator, chem_alarm)
for chem_alarm in coordinator.data[SL_DATA.KEY_CHEMISTRY][
SL_DATA.KEY_ALERTS
]
if chem_alarm != "_raw"
ScreenlogicChemistryAlarmBinarySensor(
coordinator, chem_alarm, CODE.CHEMISTRY_CHANGED
)
for chem_alarm in chemistry[SL_DATA.KEY_ALERTS]
if not chem_alarm.startswith("_")
]
)
# Intellichem notification sensors
entities.extend(
[
ScreenlogicChemistryNotificationBinarySensor(coordinator, chem_notif)
for chem_notif in coordinator.data[SL_DATA.KEY_CHEMISTRY][
SL_DATA.KEY_NOTIFICATIONS
]
if chem_notif != "_raw"
ScreenlogicChemistryNotificationBinarySensor(
coordinator, chem_notif, CODE.CHEMISTRY_CHANGED
)
for chem_notif in chemistry[SL_DATA.KEY_NOTIFICATIONS]
if not chem_notif.startswith("_")
]
)
if (
coordinator.data[SL_DATA.KEY_CONFIG]["equipment_flags"]
& EQUIPMENT.FLAG_CHLORINATOR
):
if config["equipment_flags"] & EQUIPMENT.FLAG_CHLORINATOR:
# SCG binary sensor
entities.append(ScreenlogicSCGBinarySensor(coordinator, "scg_status"))
async_add_entities(entities)
class ScreenLogicBinarySensor(ScreenlogicEntity, BinarySensorEntity):
"""Representation of the basic ScreenLogic binary sensor entity."""
class ScreenLogicBinarySensorEntity(ScreenlogicEntity, BinarySensorEntity):
"""Base class for all ScreenLogic binary sensor entities."""
_attr_has_entity_name = True
_attr_entity_category = EntityCategory.DIAGNOSTIC
@property
def name(self):
def name(self) -> str | None:
"""Return the sensor name."""
return self.sensor["name"]
@property
def device_class(self):
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)
@ -102,46 +104,58 @@ class ScreenLogicBinarySensor(ScreenlogicEntity, BinarySensorEntity):
return self.sensor["value"] == ON_OFF.ON
@property
def sensor(self):
def sensor(self) -> dict:
"""Shortcut to access the sensor data."""
return self.coordinator.data[SL_DATA.KEY_SENSORS][self._data_key]
return self.gateway_data[SL_DATA.KEY_SENSORS][self._data_key]
class ScreenlogicChemistryAlarmBinarySensor(ScreenLogicBinarySensor):
class ScreenLogicStatusBinarySensor(
ScreenLogicBinarySensorEntity, ScreenLogicPushEntity
):
"""Representation of a basic ScreenLogic sensor entity."""
class ScreenlogicChemistryAlarmBinarySensor(
ScreenLogicBinarySensorEntity, ScreenLogicPushEntity
):
"""Representation of a ScreenLogic IntelliChem alarm binary sensor entity."""
@property
def sensor(self):
def sensor(self) -> dict:
"""Shortcut to access the sensor data."""
return self.coordinator.data[SL_DATA.KEY_CHEMISTRY][SL_DATA.KEY_ALERTS][
return self.gateway_data[SL_DATA.KEY_CHEMISTRY][SL_DATA.KEY_ALERTS][
self._data_key
]
class ScreenlogicChemistryNotificationBinarySensor(ScreenLogicBinarySensor):
class ScreenlogicChemistryNotificationBinarySensor(
ScreenLogicBinarySensorEntity, ScreenLogicPushEntity
):
"""Representation of a ScreenLogic IntelliChem notification binary sensor entity."""
@property
def sensor(self):
def sensor(self) -> dict:
"""Shortcut to access the sensor data."""
return self.coordinator.data[SL_DATA.KEY_CHEMISTRY][SL_DATA.KEY_NOTIFICATIONS][
return self.gateway_data[SL_DATA.KEY_CHEMISTRY][SL_DATA.KEY_NOTIFICATIONS][
self._data_key
]
class ScreenlogicSCGBinarySensor(ScreenLogicBinarySensor):
class ScreenlogicSCGBinarySensor(ScreenLogicBinarySensorEntity):
"""Representation of a ScreenLogic SCG binary sensor entity."""
@property
def sensor(self):
def sensor(self) -> dict:
"""Shortcut to access the sensor data."""
return self.coordinator.data[SL_DATA.KEY_SCG][self._data_key]
return self.gateway_data[SL_DATA.KEY_SCG][self._data_key]
class ScreenlogicConfigBinarySensor(ScreenLogicBinarySensor):
class ScreenlogicConfigBinarySensor(
ScreenLogicBinarySensorEntity, ScreenLogicPushEntity
):
"""Representation of a ScreenLogic config data binary sensor entity."""
@property
def sensor(self):
def sensor(self) -> dict:
"""Shortcut to access the sensor data."""
return self.coordinator.data[SL_DATA.KEY_CONFIG][self._data_key]
return self.gateway_data[SL_DATA.KEY_CONFIG][self._data_key]

View file

@ -2,7 +2,7 @@
import logging
from typing import Any
from screenlogicpy.const import DATA as SL_DATA, EQUIPMENT, HEAT_MODE
from screenlogicpy.const import CODE, DATA as SL_DATA, EQUIPMENT, HEAT_MODE
from homeassistant.components.climate import (
ATTR_PRESET_MODE,
@ -18,8 +18,9 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
from . import ScreenlogicEntity
from . import ScreenlogicDataUpdateCoordinator
from .const import DOMAIN
from .entity import ScreenLogicPushEntity
_LOGGER = logging.getLogger(__name__)
@ -40,15 +41,17 @@ async def async_setup_entry(
) -> None:
"""Set up entry."""
entities = []
coordinator = hass.data[DOMAIN][config_entry.entry_id]
coordinator: ScreenlogicDataUpdateCoordinator = hass.data[DOMAIN][
config_entry.entry_id
]
for body in coordinator.data[SL_DATA.KEY_BODIES]:
for body in coordinator.gateway_data[SL_DATA.KEY_BODIES]:
entities.append(ScreenLogicClimate(coordinator, body))
async_add_entities(entities)
class ScreenLogicClimate(ScreenlogicEntity, ClimateEntity, RestoreEntity):
class ScreenLogicClimate(ScreenLogicPushEntity, ClimateEntity, RestoreEntity):
"""Represents a ScreenLogic climate entity."""
_attr_has_entity_name = True
@ -60,10 +63,10 @@ class ScreenLogicClimate(ScreenlogicEntity, ClimateEntity, RestoreEntity):
def __init__(self, coordinator, body):
"""Initialize a ScreenLogic climate entity."""
super().__init__(coordinator, body)
super().__init__(coordinator, body, CODE.STATUS_CHANGED)
self._configured_heat_modes = []
# Is solar listed as available equipment?
if self.coordinator.data["config"]["equipment_flags"] & EQUIPMENT.FLAG_SOLAR:
if self.gateway_data["config"]["equipment_flags"] & EQUIPMENT.FLAG_SOLAR:
self._configured_heat_modes.extend(
[HEAT_MODE.SOLAR, HEAT_MODE.SOLAR_PREFERRED]
)
@ -126,7 +129,7 @@ class ScreenLogicClimate(ScreenlogicEntity, ClimateEntity, RestoreEntity):
return HEAT_MODE.NAME_FOR_NUM[self.body["heat_mode"]["value"]]
@property
def preset_modes(self):
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
@ -137,15 +140,14 @@ class ScreenLogicClimate(ScreenlogicEntity, ClimateEntity, RestoreEntity):
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None:
raise ValueError(f"Expected attribute {ATTR_TEMPERATURE}")
if await self.gateway.async_set_heat_temp(
if not await self.gateway.async_set_heat_temp(
int(self._data_key), int(temperature)
):
await self._async_refresh()
else:
raise HomeAssistantError(
f"Failed to set_temperature {temperature} on body"
f" {self.body['body_type']['value']}"
)
_LOGGER.debug("Set temperature for body %s to %s", self._data_key, temperature)
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set the operation mode."""
@ -154,13 +156,12 @@ class ScreenLogicClimate(ScreenlogicEntity, ClimateEntity, RestoreEntity):
else:
mode = HEAT_MODE.NUM_FOR_NAME[self.preset_mode]
if await self.gateway.async_set_heat_mode(int(self._data_key), int(mode)):
await self._async_refresh()
else:
if not await self.gateway.async_set_heat_mode(int(self._data_key), int(mode)):
raise HomeAssistantError(
f"Failed to set_hvac_mode {mode} on body"
f" {self.body['body_type']['value']}"
)
_LOGGER.debug("Set hvac_mode on body %s to %s", self._data_key, mode)
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set the preset mode."""
@ -169,13 +170,12 @@ class ScreenLogicClimate(ScreenlogicEntity, ClimateEntity, RestoreEntity):
if self.hvac_mode == HVACMode.OFF:
return
if await self.gateway.async_set_heat_mode(int(self._data_key), int(mode)):
await self._async_refresh()
else:
if not await self.gateway.async_set_heat_mode(int(self._data_key), int(mode)):
raise HomeAssistantError(
f"Failed to set_preset_mode {mode} on body"
f" {self.body['body_type']['value']}"
)
_LOGGER.debug("Set preset_mode on body %s to %s", self._data_key, mode)
async def async_added_to_hass(self) -> None:
"""Run when entity is about to be added."""
@ -206,4 +206,4 @@ class ScreenLogicClimate(ScreenlogicEntity, ClimateEntity, RestoreEntity):
@property
def body(self):
"""Shortcut to access body data."""
return self.coordinator.data[SL_DATA.KEY_BODIES][self._data_key]
return self.gateway_data[SL_DATA.KEY_BODIES][self._data_key]

View file

@ -19,6 +19,6 @@ async def async_get_config_entry_diagnostics(
return {
"config_entry": config_entry.as_dict(),
"data": coordinator.data,
"data": coordinator.gateway.get_data(),
"debug": coordinator.gateway.get_debug(),
}

View file

@ -0,0 +1,138 @@
"""Base ScreenLogicEntity definitions."""
import logging
from typing import Any
# from screenlogicpy import ScreenLogicError, ScreenLogicGateway
from screenlogicpy.const import DATA as SL_DATA, EQUIPMENT, ON_OFF
from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import ScreenlogicDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
class ScreenlogicEntity(CoordinatorEntity[ScreenlogicDataUpdateCoordinator]):
"""Base class for all ScreenLogic entities."""
def __init__(self, coordinator, data_key, enabled=True):
"""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._attr_device_info = DeviceInfo(
connections={(dr.CONNECTION_NETWORK_MAC, self.mac)},
manufacturer="Pentair",
model=equipment_model,
name=self.gateway_name,
sw_version=self.gateway.version,
)
@property
def mac(self):
"""Mac address."""
return self.coordinator.config_entry.unique_id
@property
def config_data(self):
"""Shortcut for config data."""
return self.gateway_data[SL_DATA.KEY_CONFIG]
@property
def gateway(self):
"""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):
"""Return the configured name of the gateway."""
return self.gateway.name
async def _async_refresh(self):
"""Refresh the data from the gateway."""
await self.coordinator.async_refresh()
# Second debounced refresh to catch any secondary
# changes in the device
await self.coordinator.async_request_refresh()
async def _async_refresh_timed(self, now):
"""Refresh from a timed called."""
await self.coordinator.async_request_refresh()
class ScreenLogicPushEntity(ScreenlogicEntity):
"""Base class for all ScreenLogic push entities."""
def __init__(self, coordinator, data_key, message_code, enabled=True):
"""Initialize the entity."""
super().__init__(coordinator, data_key, enabled)
self._update_message_code = message_code
@callback
def _async_data_updated(self) -> None:
"""Handle data updates."""
self.async_write_ha_state()
async def async_added_to_hass(self) -> None:
"""When entity is added to hass."""
self.async_on_remove(
await self.gateway.async_subscribe_client(
self._async_data_updated, self._update_message_code
)
)
class ScreenLogicCircuitEntity(ScreenLogicPushEntity):
"""Base class for all ScreenLogic switch and light entities."""
_attr_has_entity_name = True
@property
def name(self):
"""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
async def async_turn_on(self, **kwargs) -> None:
"""Send the ON command."""
await self._async_set_circuit(ON_OFF.ON)
async def async_turn_off(self, **kwargs) -> None:
"""Send the OFF command."""
await self._async_set_circuit(ON_OFF.OFF)
async def _async_set_circuit(self, circuit_value) -> None:
if not await self.gateway.async_set_circuit(self._data_key, circuit_value):
raise HomeAssistantError(
f"Failed to set_circuit {self._data_key} {circuit_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]

View file

@ -1,15 +1,16 @@
"""Support for a ScreenLogic light 'circuit' switch."""
import logging
from screenlogicpy.const import DATA as SL_DATA, GENERIC_CIRCUIT_NAMES
from screenlogicpy.const import CODE, DATA as SL_DATA, GENERIC_CIRCUIT_NAMES
from homeassistant.components.light import ColorMode, LightEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import ScreenLogicCircuitEntity
from . import ScreenlogicDataUpdateCoordinator
from .const import DOMAIN, LIGHT_CIRCUIT_FUNCTIONS
from .entity import ScreenLogicCircuitEntity
_LOGGER = logging.getLogger(__name__)
@ -20,13 +21,19 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up entry."""
coordinator = hass.data[DOMAIN][config_entry.entry_id]
coordinator: ScreenlogicDataUpdateCoordinator = hass.data[DOMAIN][
config_entry.entry_id
]
circuits = coordinator.gateway_data[SL_DATA.KEY_CIRCUITS]
async_add_entities(
[
ScreenLogicLight(
coordinator, circuit_num, circuit["name"] not in GENERIC_CIRCUIT_NAMES
coordinator,
circuit_num,
CODE.STATUS_CHANGED,
circuit["name"] not in GENERIC_CIRCUIT_NAMES,
)
for circuit_num, circuit in coordinator.data[SL_DATA.KEY_CIRCUITS].items()
for circuit_num, circuit in circuits.items()
if circuit["function"] in LIGHT_CIRCUIT_FUNCTIONS
]
)

View file

@ -3,7 +3,7 @@
"name": "Pentair ScreenLogic",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/screenlogic",
"requirements": ["screenlogicpy==0.6.4"],
"requirements": ["screenlogicpy==0.7.0"],
"codeowners": ["@dieselrabbit", "@bdraco"],
"dhcp": [
{ "registered_devices": true },
@ -12,6 +12,6 @@
"macaddress": "00C033*"
}
],
"iot_class": "local_polling",
"iot_class": "local_push",
"loggers": ["screenlogicpy"]
}

View file

@ -9,8 +9,9 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import ScreenlogicEntity
from . import ScreenlogicDataUpdateCoordinator
from .const import DOMAIN
from .entity import ScreenlogicEntity
_LOGGER = logging.getLogger(__name__)
@ -28,13 +29,15 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up entry."""
coordinator = hass.data[DOMAIN][config_entry.entry_id]
equipment_flags = coordinator.data[SL_DATA.KEY_CONFIG]["equipment_flags"]
coordinator: ScreenlogicDataUpdateCoordinator = hass.data[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.data[SL_DATA.KEY_SCG]
for scg_level in coordinator.gateway_data[SL_DATA.KEY_SCG]
if scg_level in SUPPORTED_SCG_NUMBERS
]
)
@ -65,7 +68,7 @@ class ScreenLogicNumber(ScreenlogicEntity, NumberEntity):
# both existing level values and override the one that changed.
levels = {}
for level in SUPPORTED_SCG_NUMBERS:
levels[level] = self.coordinator.data[SL_DATA.KEY_SCG][level]["value"]
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(
@ -88,4 +91,4 @@ class ScreenLogicNumber(ScreenlogicEntity, NumberEntity):
@property
def sensor(self) -> dict:
"""Shortcut to access the level sensor data."""
return self.coordinator.data[SL_DATA.KEY_SCG][self._data_key]
return self.gateway_data[SL_DATA.KEY_SCG][self._data_key]

View file

@ -1,9 +1,13 @@
"""Support for a ScreenLogic Sensor."""
from typing import Any
from screenlogicpy.const import (
CHEM_DOSING_STATE,
CODE,
DATA as SL_DATA,
DEVICE_TYPE,
EQUIPMENT,
STATE_TYPE,
UNIT,
)
@ -26,8 +30,9 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import ScreenlogicEntity
from . import ScreenlogicDataUpdateCoordinator
from .const import DOMAIN
from .entity import ScreenlogicEntity, ScreenLogicPushEntity
SUPPORTED_BASIC_SENSORS = (
"air_temperature",
@ -68,12 +73,18 @@ SUPPORTED_PUMP_SENSORS = ("currentWatts", "currentRPM", "currentGPM")
SL_DEVICE_TYPE_TO_HA_DEVICE_CLASS = {
DEVICE_TYPE.DURATION: SensorDeviceClass.DURATION,
DEVICE_TYPE.ENUM: SensorDeviceClass.ENUM,
DEVICE_TYPE.ENERGY: SensorDeviceClass.POWER,
DEVICE_TYPE.POWER: SensorDeviceClass.POWER,
DEVICE_TYPE.TEMPERATURE: SensorDeviceClass.TEMPERATURE,
DEVICE_TYPE.VOLUME: SensorDeviceClass.VOLUME,
}
SL_STATE_TYPE_TO_HA_STATE_CLASS = {
STATE_TYPE.MEASUREMENT: SensorStateClass.MEASUREMENT,
STATE_TYPE.TOTAL_INCREASING: SensorStateClass.TOTAL_INCREASING,
}
SL_UNIT_TO_HA_UNIT = {
UNIT.CELSIUS: UnitOfTemperature.CELSIUS,
UNIT.FAHRENHEIT: UnitOfTemperature.FAHRENHEIT,
@ -93,14 +104,18 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up entry."""
entities = []
coordinator = hass.data[DOMAIN][config_entry.entry_id]
equipment_flags = coordinator.data[SL_DATA.KEY_CONFIG]["equipment_flags"]
entities: list[ScreenLogicSensorEntity] = []
coordinator: ScreenlogicDataUpdateCoordinator = hass.data[DOMAIN][
config_entry.entry_id
]
equipment_flags = coordinator.gateway_data[SL_DATA.KEY_CONFIG]["equipment_flags"]
# Generic sensors
for sensor_name in coordinator.data[SL_DATA.KEY_SENSORS]:
# Generic push sensors
for sensor_name in coordinator.gateway_data[SL_DATA.KEY_SENSORS]:
if sensor_name in SUPPORTED_BASIC_SENSORS:
entities.append(ScreenLogicSensor(coordinator, sensor_name))
entities.append(
ScreenLogicStatusSensor(coordinator, sensor_name, CODE.STATUS_CHANGED)
)
# 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
@ -109,10 +124,12 @@ async def async_setup_entry(
equipment_flags & EQUIPMENT.FLAG_INTELLICHEM
and sensor_name in SUPPORTED_BASIC_CHEM_SENSORS
):
entities.append(ScreenLogicSensor(coordinator, sensor_name))
entities.append(
ScreenLogicStatusSensor(coordinator, sensor_name, CODE.STATUS_CHANGED)
)
# Pump sensors
for pump_num, pump_data in coordinator.data[SL_DATA.KEY_PUMPS].items():
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
@ -129,14 +146,16 @@ async def async_setup_entry(
# IntelliChem sensors
if equipment_flags & EQUIPMENT.FLAG_INTELLICHEM:
for chem_sensor_name in coordinator.data[SL_DATA.KEY_CHEMISTRY]:
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, enabled)
ScreenLogicChemistrySensor(
coordinator, chem_sensor_name, CODE.CHEMISTRY_CHANGED, enabled
)
)
# SCG sensors
@ -144,7 +163,7 @@ async def async_setup_entry(
entities.extend(
[
ScreenLogicSCGSensor(coordinator, scg_sensor)
for scg_sensor in coordinator.data[SL_DATA.KEY_SCG]
for scg_sensor in coordinator.gateway_data[SL_DATA.KEY_SCG]
if scg_sensor in SUPPORTED_SCG_SENSORS
]
)
@ -152,54 +171,66 @@ async def async_setup_entry(
async_add_entities(entities)
class ScreenLogicSensor(ScreenlogicEntity, SensorEntity):
"""Representation of the basic ScreenLogic sensor entity."""
class ScreenLogicSensorEntity(ScreenlogicEntity, SensorEntity):
"""Base class for all ScreenLogic sensor entities."""
_attr_has_entity_name = True
@property
def name(self):
def name(self) -> str | None:
"""Name of the sensor."""
return self.sensor["name"]
@property
def native_unit_of_measurement(self):
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):
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):
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):
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 SensorStateClass.MEASUREMENT
return SL_STATE_TYPE_TO_HA_STATE_CLASS.get(
state_type, SensorStateClass.MEASUREMENT
)
@property
def native_value(self):
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):
def sensor(self) -> dict[str | int, Any]:
"""Shortcut to access the sensor data."""
return self.coordinator.data[SL_DATA.KEY_SENSORS][self._data_key]
return self.gateway_data[SL_DATA.KEY_SENSORS][self._data_key]
class ScreenLogicPumpSensor(ScreenLogicSensor):
class ScreenLogicStatusSensor(ScreenLogicSensorEntity, ScreenLogicPushEntity):
"""Representation of a basic ScreenLogic sensor entity."""
class ScreenLogicPumpSensor(ScreenLogicSensorEntity):
"""Representation of a ScreenLogic pump sensor entity."""
def __init__(self, coordinator, pump, key, enabled=True):
@ -209,21 +240,21 @@ class ScreenLogicPumpSensor(ScreenLogicSensor):
self._key = key
@property
def sensor(self):
def sensor(self) -> dict[str | int, Any]:
"""Shortcut to access the pump sensor data."""
return self.coordinator.data[SL_DATA.KEY_PUMPS][self._pump_id][self._key]
return self.gateway_data[SL_DATA.KEY_PUMPS][self._pump_id][self._key]
class ScreenLogicChemistrySensor(ScreenLogicSensor):
class ScreenLogicChemistrySensor(ScreenLogicSensorEntity, ScreenLogicPushEntity):
"""Representation of a ScreenLogic IntelliChem sensor entity."""
def __init__(self, coordinator, key, enabled=True):
def __init__(self, coordinator, key, message_code, enabled=True):
"""Initialize of the pump sensor."""
super().__init__(coordinator, f"chem_{key}", enabled)
super().__init__(coordinator, f"chem_{key}", message_code, enabled)
self._key = key
@property
def native_value(self):
def native_value(self) -> str | int | float:
"""State of the sensor."""
value = self.sensor["value"]
if "dosing_state" in self._key:
@ -231,15 +262,15 @@ class ScreenLogicChemistrySensor(ScreenLogicSensor):
return (value - 1) if "supply" in self._data_key else value
@property
def sensor(self):
def sensor(self) -> dict[str | int, Any]:
"""Shortcut to access the pump sensor data."""
return self.coordinator.data[SL_DATA.KEY_CHEMISTRY][self._key]
return self.gateway_data[SL_DATA.KEY_CHEMISTRY][self._key]
class ScreenLogicSCGSensor(ScreenLogicSensor):
class ScreenLogicSCGSensor(ScreenLogicSensorEntity):
"""Representation of ScreenLogic SCG sensor entity."""
@property
def sensor(self):
def sensor(self) -> dict[str | int, Any]:
"""Shortcut to access the pump sensor data."""
return self.coordinator.data[SL_DATA.KEY_SCG][self._data_key]
return self.gateway_data[SL_DATA.KEY_SCG][self._data_key]

View file

@ -1,15 +1,16 @@
"""Support for a ScreenLogic 'circuit' switch."""
import logging
from screenlogicpy.const import DATA as SL_DATA, GENERIC_CIRCUIT_NAMES
from screenlogicpy.const import CODE, DATA as SL_DATA, GENERIC_CIRCUIT_NAMES
from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import ScreenLogicCircuitEntity
from . import ScreenlogicDataUpdateCoordinator
from .const import DOMAIN, LIGHT_CIRCUIT_FUNCTIONS
from .entity import ScreenLogicCircuitEntity
_LOGGER = logging.getLogger(__name__)
@ -20,13 +21,19 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up entry."""
coordinator = hass.data[DOMAIN][config_entry.entry_id]
coordinator: ScreenlogicDataUpdateCoordinator = hass.data[DOMAIN][
config_entry.entry_id
]
circuits = coordinator.gateway_data[SL_DATA.KEY_CIRCUITS]
async_add_entities(
[
ScreenLogicSwitch(
coordinator, circuit_num, circuit["name"] not in GENERIC_CIRCUIT_NAMES
coordinator,
circuit_num,
CODE.STATUS_CHANGED,
circuit["name"] not in GENERIC_CIRCUIT_NAMES,
)
for circuit_num, circuit in coordinator.data[SL_DATA.KEY_CIRCUITS].items()
for circuit_num, circuit in circuits.items()
if circuit["function"] not in LIGHT_CIRCUIT_FUNCTIONS
]
)

View file

@ -4717,7 +4717,7 @@
"name": "Pentair ScreenLogic",
"integration_type": "hub",
"config_flow": true,
"iot_class": "local_polling"
"iot_class": "local_push"
},
"scsgate": {
"name": "SCSGate",

View file

@ -2297,7 +2297,7 @@ satel_integra==0.3.7
scapy==2.5.0
# homeassistant.components.screenlogic
screenlogicpy==0.6.4
screenlogicpy==0.7.0
# homeassistant.components.scsgate
scsgate==0.1.0

View file

@ -1618,7 +1618,7 @@ samsungtvws[async,encrypted]==2.5.0
scapy==2.5.0
# homeassistant.components.screenlogic
screenlogicpy==0.6.4
screenlogicpy==0.7.0
# homeassistant.components.backup
securetar==2022.2.0