diff --git a/homeassistant/components/supla/__init__.py b/homeassistant/components/supla/__init__.py index 6c9bfb8d16e..8912ee8a59b 100644 --- a/homeassistant/components/supla/__init__.py +++ b/homeassistant/components/supla/__init__.py @@ -1,32 +1,41 @@ """Support for Supla devices.""" +from datetime import timedelta import logging from typing import Optional -from pysupla import SuplaAPI +import async_timeout +from asyncpysupla import SuplaAPI import voluptuous as vol from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.discovery import load_platform +from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator _LOGGER = logging.getLogger(__name__) -DOMAIN = "supla" +DOMAIN = "supla" CONF_SERVER = "server" CONF_SERVERS = "servers" +SCAN_INTERVAL = timedelta(seconds=10) + SUPLA_FUNCTION_HA_CMP_MAP = { "CONTROLLINGTHEROLLERSHUTTER": "cover", "CONTROLLINGTHEGATE": "cover", "LIGHTSWITCH": "switch", } SUPLA_FUNCTION_NONE = "NONE" -SUPLA_CHANNELS = "supla_channels" SUPLA_SERVERS = "supla_servers" +SUPLA_COORDINATORS = "supla_coordinators" SERVER_CONFIG = vol.Schema( - {vol.Required(CONF_SERVER): cv.string, vol.Required(CONF_ACCESS_TOKEN): cv.string} + { + vol.Required(CONF_SERVER): cv.string, + vol.Required(CONF_ACCESS_TOKEN): cv.string, + } ) CONFIG_SCHEMA = vol.Schema( @@ -39,25 +48,27 @@ CONFIG_SCHEMA = vol.Schema( ) -def setup(hass, base_config): +async def async_setup(hass, base_config): """Set up the Supla component.""" server_confs = base_config[DOMAIN][CONF_SERVERS] - hass.data[SUPLA_SERVERS] = {} - hass.data[SUPLA_CHANNELS] = {} + hass.data[DOMAIN] = {SUPLA_SERVERS: {}, SUPLA_COORDINATORS: {}} + + session = async_get_clientsession(hass) for server_conf in server_confs: server_address = server_conf[CONF_SERVER] - server = SuplaAPI(server_address, server_conf[CONF_ACCESS_TOKEN]) + server = SuplaAPI(server_address, server_conf[CONF_ACCESS_TOKEN], session) # Test connection try: - srv_info = server.get_server_info() + srv_info = await server.get_server_info() if srv_info.get("authenticated"): - hass.data[SUPLA_SERVERS][server_conf[CONF_SERVER]] = server + hass.data[DOMAIN][SUPLA_SERVERS][server_conf[CONF_SERVER]] = server + else: _LOGGER.error( "Server: %s not configured. API call returned: %s", @@ -71,23 +82,46 @@ def setup(hass, base_config): ) return False - discover_devices(hass, base_config) + await discover_devices(hass, base_config) return True -def discover_devices(hass, hass_config): +async def discover_devices(hass, hass_config): """ Run periodically to discover new devices. - Currently it's only run at startup. + Currently it is only run at startup. """ component_configs = {} - for server_name, server in hass.data[SUPLA_SERVERS].items(): + for server_name, server in hass.data[DOMAIN][SUPLA_SERVERS].items(): - for channel in server.get_channels(include=["iodevice"]): + async def _fetch_channels(): + async with async_timeout.timeout(SCAN_INTERVAL.total_seconds()): + channels = { + channel["id"]: channel + for channel in await server.get_channels( + include=["iodevice", "state", "connected"] + ) + } + return channels + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name=f"{DOMAIN}-{server_name}", + update_method=_fetch_channels, + update_interval=SCAN_INTERVAL, + ) + + await coordinator.async_refresh() + + hass.data[DOMAIN][SUPLA_COORDINATORS][server_name] = coordinator + + for channel_id, channel in coordinator.data.items(): channel_function = channel["function"]["name"] + if channel_function == SUPLA_FUNCTION_NONE: _LOGGER.debug( "Ignored function: %s, channel id: %s", @@ -107,25 +141,38 @@ def discover_devices(hass, hass_config): continue channel["server_name"] = server_name - component_configs.setdefault(component_name, []).append(channel) + component_configs.setdefault(component_name, []).append( + { + "channel_id": channel_id, + "server_name": server_name, + "function_name": channel["function"]["name"], + } + ) # Load discovered devices - for component_name, channel in component_configs.items(): - load_platform(hass, component_name, "supla", channel, hass_config) + for component_name, config in component_configs.items(): + await async_load_platform(hass, component_name, DOMAIN, config, hass_config) class SuplaChannel(Entity): """Base class of a Supla Channel (an equivalent of HA's Entity).""" - def __init__(self, channel_data): - """Channel data -- raw channel information from PySupla.""" - self.server_name = channel_data["server_name"] - self.channel_data = channel_data + def __init__(self, config, server, coordinator): + """Init from config, hookup[ server and coordinator.""" + self.server_name = config["server_name"] + self.channel_id = config["channel_id"] + self.server = server + self.coordinator = coordinator @property - def server(self): - """Return PySupla's server component associated with entity.""" - return self.hass.data[SUPLA_SERVERS][self.server_name] + def channel_data(self): + """Return channel data taken from coordinator.""" + return self.coordinator.data.get(self.channel_id) + + @property + def should_poll(self): + """Supla uses DataUpdateCoordinator, so no additional polling needed.""" + return False @property def unique_id(self) -> str: @@ -150,7 +197,13 @@ class SuplaChannel(Entity): return False return state.get("connected") - def action(self, action, **add_pars): + async def async_added_to_hass(self): + """When entity is added to hass.""" + self.async_on_remove( + self.coordinator.async_add_listener(self.async_write_ha_state) + ) + + async def async_action(self, action, **add_pars): """ Run server action. @@ -163,10 +216,14 @@ class SuplaChannel(Entity): self.channel_data["id"], add_pars, ) - self.server.execute_action(self.channel_data["id"], action, **add_pars) + await self.server.execute_action(self.channel_data["id"], action, **add_pars) - def update(self): - """Call to update state.""" - self.channel_data = self.server.get_channel( - self.channel_data["id"], include=["connected", "state"] - ) + # Update state + await self.coordinator.async_request_refresh() + + async def async_update(self): + """Update the entity. + + Only used by the generic entity update service. + """ + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/supla/cover.py b/homeassistant/components/supla/cover.py index 1c0f2f60431..ac71bc4ea8f 100644 --- a/homeassistant/components/supla/cover.py +++ b/homeassistant/components/supla/cover.py @@ -7,7 +7,12 @@ from homeassistant.components.cover import ( DEVICE_CLASS_GARAGE, CoverEntity, ) -from homeassistant.components.supla import SuplaChannel +from homeassistant.components.supla import ( + DOMAIN, + SUPLA_COORDINATORS, + SUPLA_SERVERS, + SuplaChannel, +) _LOGGER = logging.getLogger(__name__) @@ -15,7 +20,7 @@ SUPLA_SHUTTER = "CONTROLLINGTHEROLLERSHUTTER" SUPLA_GATE = "CONTROLLINGTHEGATE" -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Supla covers.""" if discovery_info is None: return @@ -24,12 +29,28 @@ def setup_platform(hass, config, add_entities, discovery_info=None): entities = [] for device in discovery_info: - device_name = device["function"]["name"] + device_name = device["function_name"] + server_name = device["server_name"] + if device_name == SUPLA_SHUTTER: - entities.append(SuplaCover(device)) + entities.append( + SuplaCover( + device, + hass.data[DOMAIN][SUPLA_SERVERS][server_name], + hass.data[DOMAIN][SUPLA_COORDINATORS][server_name], + ) + ) + elif device_name == SUPLA_GATE: - entities.append(SuplaGateDoor(device)) - add_entities(entities) + entities.append( + SuplaGateDoor( + device, + hass.data[DOMAIN][SUPLA_SERVERS][server_name], + hass.data[DOMAIN][SUPLA_COORDINATORS][server_name], + ) + ) + + async_add_entities(entities) class SuplaCover(SuplaChannel, CoverEntity): @@ -43,9 +64,9 @@ class SuplaCover(SuplaChannel, CoverEntity): return 100 - state["shut"] return None - def set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs): """Move the cover to a specific position.""" - self.action("REVEAL", percentage=kwargs.get(ATTR_POSITION)) + await self.async_action("REVEAL", percentage=kwargs.get(ATTR_POSITION)) @property def is_closed(self): @@ -54,17 +75,17 @@ class SuplaCover(SuplaChannel, CoverEntity): return None return self.current_cover_position == 0 - def open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs): """Open the cover.""" - self.action("REVEAL") + await self.async_action("REVEAL") - def close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs): """Close the cover.""" - self.action("SHUT") + await self.async_action("SHUT") - def stop_cover(self, **kwargs): + async def async_stop_cover(self, **kwargs): """Stop the cover.""" - self.action("STOP") + await self.async_action("STOP") class SuplaGateDoor(SuplaChannel, CoverEntity): @@ -78,23 +99,23 @@ class SuplaGateDoor(SuplaChannel, CoverEntity): return state.get("hi") return None - def open_cover(self, **kwargs) -> None: + async def async_open_cover(self, **kwargs) -> None: """Open the gate.""" if self.is_closed: - self.action("OPEN_CLOSE") + await self.async_action("OPEN_CLOSE") - def close_cover(self, **kwargs) -> None: + async def async_close_cover(self, **kwargs) -> None: """Close the gate.""" if not self.is_closed: - self.action("OPEN_CLOSE") + await self.async_action("OPEN_CLOSE") - def stop_cover(self, **kwargs) -> None: + async def async_stop_cover(self, **kwargs) -> None: """Stop the gate.""" - self.action("OPEN_CLOSE") + await self.async_action("OPEN_CLOSE") - def toggle(self, **kwargs) -> None: + async def async_toggle(self, **kwargs) -> None: """Toggle the gate.""" - self.action("OPEN_CLOSE") + await self.async_action("OPEN_CLOSE") @property def device_class(self): diff --git a/homeassistant/components/supla/manifest.json b/homeassistant/components/supla/manifest.json index a4ab0e72719..1a2dcf3cbc5 100644 --- a/homeassistant/components/supla/manifest.json +++ b/homeassistant/components/supla/manifest.json @@ -2,6 +2,6 @@ "domain": "supla", "name": "Supla", "documentation": "https://www.home-assistant.io/integrations/supla", - "requirements": ["pysupla==0.0.3"], + "requirements": ["asyncpysupla==0.0.5"], "codeowners": ["@mwegrzynek"] } diff --git a/homeassistant/components/supla/switch.py b/homeassistant/components/supla/switch.py index 61f218b75d9..9122d9d1970 100644 --- a/homeassistant/components/supla/switch.py +++ b/homeassistant/components/supla/switch.py @@ -2,32 +2,49 @@ import logging from pprint import pformat -from homeassistant.components.supla import SuplaChannel +from homeassistant.components.supla import ( + DOMAIN, + SUPLA_COORDINATORS, + SUPLA_SERVERS, + SuplaChannel, +) from homeassistant.components.switch import SwitchEntity _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Supla switches.""" if discovery_info is None: return _LOGGER.debug("Discovery: %s", pformat(discovery_info)) - add_entities([SuplaSwitch(device) for device in discovery_info]) + entities = [] + for device in discovery_info: + server_name = device["server_name"] + + entities.append( + SuplaSwitch( + device, + hass.data[DOMAIN][SUPLA_SERVERS][server_name], + hass.data[DOMAIN][SUPLA_COORDINATORS][server_name], + ) + ) + + async_add_entities(entities) class SuplaSwitch(SuplaChannel, SwitchEntity): """Representation of a Supla Switch.""" - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn on the switch.""" - self.action("TURN_ON") + await self.async_action("TURN_ON") - def turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn off the switch.""" - self.action("TURN_OFF") + await self.async_action("TURN_OFF") @property def is_on(self): diff --git a/requirements_all.txt b/requirements_all.txt index 8a9e3748931..ff53d0e6dd6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -287,6 +287,9 @@ asterisk_mbox==0.5.0 # homeassistant.components.upnp async-upnp-client==0.14.13 +# homeassistant.components.supla +asyncpysupla==0.0.5 + # homeassistant.components.aten_pe atenpdu==0.3.0 @@ -1660,9 +1663,6 @@ pystiebeleltron==0.0.1.dev2 # homeassistant.components.suez_water pysuez==0.1.19 -# homeassistant.components.supla -pysupla==0.0.3 - # homeassistant.components.syncthru pysyncthru==0.7.0