From 687d326bb49e4c031ddaa9585560bd7d142bce5b Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Mon, 6 Feb 2023 18:13:36 -0800 Subject: [PATCH] Support local push updates for most ScreenLogic entities (#87438) Co-authored-by: J. Nick Koston --- .coveragerc | 1 + .../components/screenlogic/__init__.py | 167 ++++-------------- .../components/screenlogic/binary_sensor.py | 100 ++++++----- .../components/screenlogic/climate.py | 36 ++-- .../components/screenlogic/diagnostics.py | 2 +- .../components/screenlogic/entity.py | 138 +++++++++++++++ homeassistant/components/screenlogic/light.py | 17 +- .../components/screenlogic/manifest.json | 4 +- .../components/screenlogic/number.py | 15 +- .../components/screenlogic/sensor.py | 103 +++++++---- .../components/screenlogic/switch.py | 17 +- homeassistant/generated/integrations.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 14 files changed, 353 insertions(+), 253 deletions(-) create mode 100644 homeassistant/components/screenlogic/entity.py diff --git a/.coveragerc b/.coveragerc index c125eb8295258f..771333c1c32558 100644 --- a/.coveragerc +++ b/.coveragerc @@ -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 diff --git a/homeassistant/components/screenlogic/__init__.py b/homeassistant/components/screenlogic/__init__.py index d698653a3fc6c6..fad4dc6509b1c8 100644 --- a/homeassistant/components/screenlogic/__init__.py +++ b/homeassistant/components/screenlogic/__init__.py @@ -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.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 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Screenlogic from a config entry.""" - connect_info = await async_get_connect_info(hass, entry) + gateway = ScreenLogicGateway() - gateway = ScreenLogicGateway(**connect_info) + connect_info = await async_get_connect_info(hass, entry) 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 @@ def __init__(self, hass, *, config_entry, gateway): ), ) + @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 @@ async def _async_reconnect_update_data(self): 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] diff --git a/homeassistant/components/screenlogic/binary_sensor.py b/homeassistant/components/screenlogic/binary_sensor.py index 08035e25a87d0c..e952a6104c43e0 100644 --- a/homeassistant/components/screenlogic/binary_sensor.py +++ b/homeassistant/components/screenlogic/binary_sensor.py @@ -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.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 @@ def is_on(self) -> bool: 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 ScreenLogicStatusBinarySensor( + ScreenLogicBinarySensorEntity, ScreenLogicPushEntity +): + """Representation of a basic ScreenLogic sensor entity.""" -class ScreenlogicChemistryAlarmBinarySensor(ScreenLogicBinarySensor): +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] diff --git a/homeassistant/components/screenlogic/climate.py b/homeassistant/components/screenlogic/climate.py index 093d96a285e6ff..cea546262aea2d 100644 --- a/homeassistant/components/screenlogic/climate.py +++ b/homeassistant/components/screenlogic/climate.py @@ -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.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 @@ def preset_mode(self) -> str: 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 @@ async def async_set_temperature(self, **kwargs: Any) -> None: 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 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: 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 @@ async def async_set_preset_mode(self, preset_mode: str) -> None: 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 @@ async def async_added_to_hass(self) -> None: @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] diff --git a/homeassistant/components/screenlogic/diagnostics.py b/homeassistant/components/screenlogic/diagnostics.py index 6de916d951422c..ca949c4514c7f2 100644 --- a/homeassistant/components/screenlogic/diagnostics.py +++ b/homeassistant/components/screenlogic/diagnostics.py @@ -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(), } diff --git a/homeassistant/components/screenlogic/entity.py b/homeassistant/components/screenlogic/entity.py new file mode 100644 index 00000000000000..80b4df4d2de70c --- /dev/null +++ b/homeassistant/components/screenlogic/entity.py @@ -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] diff --git a/homeassistant/components/screenlogic/light.py b/homeassistant/components/screenlogic/light.py index c8067670942812..3eae12178decdd 100644 --- a/homeassistant/components/screenlogic/light.py +++ b/homeassistant/components/screenlogic/light.py @@ -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 ] ) diff --git a/homeassistant/components/screenlogic/manifest.json b/homeassistant/components/screenlogic/manifest.json index 94f7078b70675a..12327ac6ac1721 100644 --- a/homeassistant/components/screenlogic/manifest.json +++ b/homeassistant/components/screenlogic/manifest.json @@ -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"] } diff --git a/homeassistant/components/screenlogic/number.py b/homeassistant/components/screenlogic/number.py index 74a7811b590bfd..102bce2cbf8faa 100644 --- a/homeassistant/components/screenlogic/number.py +++ b/homeassistant/components/screenlogic/number.py @@ -9,8 +9,9 @@ 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 @@ async def async_set_native_value(self, value: float) -> None: # 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 @@ async def async_set_native_value(self, value: float) -> None: @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] diff --git a/homeassistant/components/screenlogic/sensor.py b/homeassistant/components/screenlogic/sensor.py index 3f488db98ebf8d..d4575f2d066652 100644 --- a/homeassistant/components/screenlogic/sensor.py +++ b/homeassistant/components/screenlogic/sensor.py @@ -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.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 @@ 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"] - - # Generic sensors - for sensor_name in coordinator.data[SL_DATA.KEY_SENSORS]: + entities: list[ScreenLogicSensorEntity] = [] + coordinator: ScreenlogicDataUpdateCoordinator = hass.data[DOMAIN][ + config_entry.entry_id + ] + equipment_flags = coordinator.gateway_data[SL_DATA.KEY_CONFIG]["equipment_flags"] + + # 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 options(self) -> list[str] | None: + """Return a set of possible options.""" + return self.sensor.get("enum_options") @property - def native_value(self): + 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 ScreenLogicStatusSensor(ScreenLogicSensorEntity, ScreenLogicPushEntity): + """Representation of a basic ScreenLogic sensor entity.""" -class ScreenLogicPumpSensor(ScreenLogicSensor): +class ScreenLogicPumpSensor(ScreenLogicSensorEntity): """Representation of a ScreenLogic pump sensor entity.""" def __init__(self, coordinator, pump, key, enabled=True): @@ -209,21 +240,21 @@ def __init__(self, coordinator, pump, key, enabled=True): 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 @@ def native_value(self): 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] diff --git a/homeassistant/components/screenlogic/switch.py b/homeassistant/components/screenlogic/switch.py index 1d2b0e32901ed5..35bd1edb84fdeb 100644 --- a/homeassistant/components/screenlogic/switch.py +++ b/homeassistant/components/screenlogic/switch.py @@ -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 ] ) diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 503aa64a13efad..3a1c8bf7fe86a5 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -4717,7 +4717,7 @@ "name": "Pentair ScreenLogic", "integration_type": "hub", "config_flow": true, - "iot_class": "local_polling" + "iot_class": "local_push" }, "scsgate": { "name": "SCSGate", diff --git a/requirements_all.txt b/requirements_all.txt index bc4e5e57526c1f..7c8772151c51a5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b96e3eb0cb76a2..2a9682803d13a7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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