Skip to content

Commit

Permalink
Update screenlogic use asyncio API (home-assistant#60466)
Browse files Browse the repository at this point in the history
Co-authored-by: J. Nick Koston <[email protected]>
  • Loading branch information
dieselrabbit and bdraco authored Dec 1, 2021
1 parent cc8e02c commit 8240b8c
Show file tree
Hide file tree
Showing 9 changed files with 87 additions and 145 deletions.
128 changes: 63 additions & 65 deletions homeassistant/components/screenlogic/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
"""The Screenlogic integration."""
import asyncio
from datetime import timedelta
import logging

Expand All @@ -11,6 +10,7 @@
SL_GATEWAY_IP,
SL_GATEWAY_NAME,
SL_GATEWAY_PORT,
ScreenLogicWarning,
)

from homeassistant.config_entries import ConfigEntry
Expand All @@ -20,43 +20,43 @@
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
UpdateFailed,
)

from .config_flow import async_discover_gateways_by_unique_id, name_for_mac
from .const import DEFAULT_SCAN_INTERVAL, DISCOVERED_GATEWAYS, DOMAIN
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN
from .services import async_load_screenlogic_services, async_unload_screenlogic_services

_LOGGER = logging.getLogger(__name__)


REQUEST_REFRESH_DELAY = 1
REQUEST_REFRESH_DELAY = 2
HEATER_COOLDOWN_DELAY = 6

PLATFORMS = ["switch", "sensor", "binary_sensor", "climate", "light"]
# These seem to be constant across all controller models
PRIMARY_CIRCUIT_IDS = [500, 505] # [Spa, Pool]


async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Screenlogic component."""
domain_data = hass.data[DOMAIN] = {}
domain_data[DISCOVERED_GATEWAYS] = await async_discover_gateways_by_unique_id(hass)
return True
PLATFORMS = ["binary_sensor", "climate", "light", "sensor", "switch"]


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 = await hass.async_add_executor_job(get_new_gateway, hass, entry)
gateway = ScreenLogicGateway(**connect_info)

# The api library uses a shared socket connection and does not handle concurrent
# requests very well.
api_lock = asyncio.Lock()
try:
await gateway.async_connect()
except ScreenLogicError as ex:
_LOGGER.error("Error while connecting to the gateway %s: %s", connect_info, ex)
raise ConfigEntryNotReady from ex

coordinator = ScreenlogicDataUpdateCoordinator(
hass, config_entry=entry, gateway=gateway, api_lock=api_lock
hass, config_entry=entry, gateway=gateway
)

async_load_screenlogic_services(hass)
Expand All @@ -65,7 +65,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:

entry.async_on_unload(entry.add_update_listener(async_update_listener))

hass.data[DOMAIN][entry.entry_id] = coordinator
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator

hass.config_entries.async_setup_platforms(entry, PLATFORMS)

Expand All @@ -75,8 +75,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
hass.data[DOMAIN][entry.entry_id]["listener"]()
if unload_ok:
coordinator = hass.data[DOMAIN][entry.entry_id]
await coordinator.gateway.async_disconnect()
hass.data[DOMAIN].pop(entry.entry_id)

async_unload_screenlogic_services(hass)
Expand All @@ -89,11 +90,11 @@ async def async_update_listener(hass: HomeAssistant, entry: ConfigEntry):
await hass.config_entries.async_reload(entry.entry_id)


def get_connect_info(hass: HomeAssistant, entry: ConfigEntry):
async def async_get_connect_info(hass: HomeAssistant, entry: ConfigEntry):
"""Construct connect_info from configuration entry and returns it to caller."""
mac = entry.unique_id
# Attempt to re-discover named gateway to follow IP changes
discovered_gateways = hass.data[DOMAIN][DISCOVERED_GATEWAYS]
# Attempt to rediscover gateway to follow IP changes
discovered_gateways = await async_discover_gateways_by_unique_id(hass)
if mac in discovered_gateways:
connect_info = discovered_gateways[mac]
else:
Expand All @@ -108,28 +109,13 @@ def get_connect_info(hass: HomeAssistant, entry: ConfigEntry):
return connect_info


def get_new_gateway(hass: HomeAssistant, entry: ConfigEntry):
"""Instantiate a new ScreenLogicGateway, connect to it and return it to caller."""

connect_info = get_connect_info(hass, entry)

try:
gateway = ScreenLogicGateway(**connect_info)
except ScreenLogicError as ex:
_LOGGER.error("Error while connecting to the gateway %s: %s", connect_info, ex)
raise ConfigEntryNotReady from ex

return gateway


class ScreenlogicDataUpdateCoordinator(DataUpdateCoordinator):
"""Class to manage the data update for the Screenlogic component."""

def __init__(self, hass, *, config_entry, gateway, api_lock):
def __init__(self, hass, *, config_entry, gateway):
"""Initialize the Screenlogic Data Update Coordinator."""
self.config_entry = config_entry
self.gateway = gateway
self.api_lock = api_lock
self.screenlogic_data = {}

interval = timedelta(
Expand All @@ -140,40 +126,38 @@ def __init__(self, hass, *, config_entry, gateway, api_lock):
_LOGGER,
name=DOMAIN,
update_interval=interval,
# We don't want an immediate refresh since the device
# takes a moment to reflect the state change
# 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
),
)

def reconnect_gateway(self):
"""Instantiate a new ScreenLogicGateway, connect to it and update. Return new gateway to caller."""

connect_info = get_connect_info(self.hass, self.config_entry)

async def _async_update_data(self):
"""Fetch data from the Screenlogic gateway."""
try:
gateway = ScreenLogicGateway(**connect_info)
gateway.update()
await self.gateway.async_update()
except ScreenLogicError as error:
raise UpdateFailed(error) from 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 gateway
return self.gateway.get_data()

async def _async_update_data(self):
"""Fetch data from the Screenlogic gateway."""
async def _async_reconnect_update_data(self):
"""Attempt to reconnect to the gateway and fetch data."""
try:
async with self.api_lock:
await self.hass.async_add_executor_job(self.gateway.update)
except ScreenLogicError as error:
_LOGGER.warning("ScreenLogicError - attempting reconnect: %s", error)
# Clean up the previous connection as we're about to create a new one
await self.gateway.async_disconnect()

async with self.api_lock:
self.gateway = await self.hass.async_add_executor_job(
self.reconnect_gateway
)
connect_info = await async_get_connect_info(self.hass, self.config_entry)
self.gateway = ScreenLogicGateway(**connect_info)

return self.gateway.get_data()
await self.gateway.async_update()

except (ScreenLogicError, ScreenLogicWarning) as ex:
raise UpdateFailed(ex) from ex


class ScreenlogicEntity(CoordinatorEntity):
Expand Down Expand Up @@ -233,6 +217,17 @@ def device_info(self) -> DeviceInfo:
name=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 ScreenLogicCircuitEntity(ScreenlogicEntity):
"""ScreenLogic circuit entity."""
Expand All @@ -255,15 +250,18 @@ 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:
async with self.coordinator.api_lock:
success = await self.hass.async_add_executor_job(
self.gateway.set_circuit, self._data_key, circuit_value
# 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
)

if success:
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.coordinator.async_request_refresh()
await self._async_refresh()
else:
_LOGGER.warning(
"Failed to set_circuit %s %s", self._data_key, circuit_value
Expand Down
29 changes: 8 additions & 21 deletions homeassistant/components/screenlogic/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,13 +138,10 @@ async def async_set_temperature(self, **kwargs) -> None:
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None:
raise ValueError(f"Expected attribute {ATTR_TEMPERATURE}")

async with self.coordinator.api_lock:
success = await self.hass.async_add_executor_job(
self.gateway.set_heat_temp, int(self._data_key), int(temperature)
)

if success:
await self.coordinator.async_request_refresh()
if 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 {self.body['body_type']['value']}"
Expand All @@ -157,13 +154,8 @@ async def async_set_hvac_mode(self, hvac_mode) -> None:
else:
mode = HEAT_MODE.NUM_FOR_NAME[self.preset_mode]

async with self.coordinator.api_lock:
success = await self.hass.async_add_executor_job(
self.gateway.set_heat_mode, int(self._data_key), int(mode)
)

if success:
await self.coordinator.async_request_refresh()
if await self.gateway.async_set_heat_mode(int(self._data_key), int(mode)):
await self._async_refresh()
else:
raise HomeAssistantError(
f"Failed to set_hvac_mode {mode} on body {self.body['body_type']['value']}"
Expand All @@ -176,13 +168,8 @@ async def async_set_preset_mode(self, preset_mode: str) -> None:
if self.hvac_mode == HVAC_MODE_OFF:
return

async with self.coordinator.api_lock:
success = await self.hass.async_add_executor_job(
self.gateway.set_heat_mode, int(self._data_key), int(mode)
)

if success:
await self.coordinator.async_request_refresh()
if await self.gateway.async_set_heat_mode(int(self._data_key), int(mode)):
await self._async_refresh()
else:
raise HomeAssistantError(
f"Failed to set_preset_mode {mode} on body {self.body['body_type']['value']}"
Expand Down
16 changes: 1 addition & 15 deletions homeassistant/components/screenlogic/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,18 +56,6 @@ def name_for_mac(mac):
return f"Pentair: {short_mac(mac)}"


async def async_get_mac_address(hass, ip_address, port):
"""Connect to a screenlogic gateway and return the mac address."""
connected_socket = await hass.async_add_executor_job(
login.create_socket,
ip_address,
port,
)
if not connected_socket:
raise ScreenLogicError("Unknown socket error")
return await hass.async_add_executor_job(login.gateway_connect, connected_socket)


class ScreenlogicConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Config flow to setup screen logic devices."""

Expand Down Expand Up @@ -155,9 +143,7 @@ async def async_step_gateway_entry(self, user_input=None):
ip_address = user_input[CONF_IP_ADDRESS]
port = user_input[CONF_PORT]
try:
mac = format_mac(
await async_get_mac_address(self.hass, ip_address, port)
)
mac = format_mac(await login.async_get_mac_address(ip_address, port))
except ScreenLogicError as ex:
_LOGGER.debug(ex)
errors[CONF_IP_ADDRESS] = "cannot_connect"
Expand Down
2 changes: 0 additions & 2 deletions homeassistant/components/screenlogic/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,3 @@
}

LIGHT_CIRCUIT_FUNCTIONS = {CIRCUIT_FUNCTION.INTELLIBRITE, CIRCUIT_FUNCTION.LIGHT}

DISCOVERED_GATEWAYS = "_discovered_gateways"
2 changes: 1 addition & 1 deletion homeassistant/components/screenlogic/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"name": "Pentair ScreenLogic",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/screenlogic",
"requirements": ["screenlogicpy==0.4.1"],
"requirements": ["screenlogicpy==0.5.3"],
"codeowners": ["@dieselrabbit"],
"dhcp": [
{
Expand Down
14 changes: 7 additions & 7 deletions homeassistant/components/screenlogic/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,13 @@ async def async_set_color_mode(service_call: ServiceCall):
color_num,
)
try:
async with coordinator.api_lock:
if not await hass.async_add_executor_job(
coordinator.gateway.set_color_lights, color_num
):
raise HomeAssistantError(
f"Failed to call service '{SERVICE_SET_COLOR_MODE}'"
)
if not await coordinator.gateway.async_set_color_lights(color_num):
raise HomeAssistantError(
f"Failed to call service '{SERVICE_SET_COLOR_MODE}'"
)
# Debounced refresh to catch any secondary
# changes in the device
await coordinator.async_request_refresh()
except ScreenLogicError as error:
raise HomeAssistantError(error) from error

Expand Down
2 changes: 1 addition & 1 deletion requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2115,7 +2115,7 @@ scapy==2.4.5
schiene==0.23

# homeassistant.components.screenlogic
screenlogicpy==0.4.1
screenlogicpy==0.5.3

# homeassistant.components.scsgate
scsgate==0.1.0
Expand Down
2 changes: 1 addition & 1 deletion requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1257,7 +1257,7 @@ samsungtvws==1.6.0
scapy==2.4.5

# homeassistant.components.screenlogic
screenlogicpy==0.4.1
screenlogicpy==0.5.3

# homeassistant.components.emulated_kasa
# homeassistant.components.sense
Expand Down
Loading

0 comments on commit 8240b8c

Please sign in to comment.