Use DataUpdateCoordinator for supla (#38921)

* Linter suggestions

* Store coordinator in hass.data[supla_coordinators]

* Server cleanup

* Spelling mistake

* Fixes suggested in review

* Pass server and coordinator during async_setup_platform

* Linter changes

* Rename fetch_channels to _fetch_channels

* Linter suggestions

* Store coordinator in hass.data[supla_coordinators]

* Server cleanup

* Fixes suggested in review

* Pass server and coordinator during async_setup_platform

* Linter changes

* Remove scan interval configuration option

* Linting

* Isort

* Disable polling, update asyncpysupla version

* Black fixes

* Update manifest.json

Co-authored-by: Chris Talkington <chris@talkingtontech.com>
This commit is contained in:
Michał Węgrzynek 2020-09-03 18:25:30 +02:00 committed by GitHub
parent 9baa7c6c24
commit 8818a5ab6c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 161 additions and 66 deletions

View File

@ -1,32 +1,41 @@
"""Support for Supla devices.""" """Support for Supla devices."""
from datetime import timedelta
import logging import logging
from typing import Optional from typing import Optional
from pysupla import SuplaAPI import async_timeout
from asyncpysupla import SuplaAPI
import voluptuous as vol import voluptuous as vol
from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv 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.entity import Entity
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DOMAIN = "supla"
DOMAIN = "supla"
CONF_SERVER = "server" CONF_SERVER = "server"
CONF_SERVERS = "servers" CONF_SERVERS = "servers"
SCAN_INTERVAL = timedelta(seconds=10)
SUPLA_FUNCTION_HA_CMP_MAP = { SUPLA_FUNCTION_HA_CMP_MAP = {
"CONTROLLINGTHEROLLERSHUTTER": "cover", "CONTROLLINGTHEROLLERSHUTTER": "cover",
"CONTROLLINGTHEGATE": "cover", "CONTROLLINGTHEGATE": "cover",
"LIGHTSWITCH": "switch", "LIGHTSWITCH": "switch",
} }
SUPLA_FUNCTION_NONE = "NONE" SUPLA_FUNCTION_NONE = "NONE"
SUPLA_CHANNELS = "supla_channels"
SUPLA_SERVERS = "supla_servers" SUPLA_SERVERS = "supla_servers"
SUPLA_COORDINATORS = "supla_coordinators"
SERVER_CONFIG = vol.Schema( 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( 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.""" """Set up the Supla component."""
server_confs = base_config[DOMAIN][CONF_SERVERS] server_confs = base_config[DOMAIN][CONF_SERVERS]
hass.data[SUPLA_SERVERS] = {} hass.data[DOMAIN] = {SUPLA_SERVERS: {}, SUPLA_COORDINATORS: {}}
hass.data[SUPLA_CHANNELS] = {}
session = async_get_clientsession(hass)
for server_conf in server_confs: for server_conf in server_confs:
server_address = server_conf[CONF_SERVER] 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 # Test connection
try: try:
srv_info = server.get_server_info() srv_info = await server.get_server_info()
if srv_info.get("authenticated"): 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: else:
_LOGGER.error( _LOGGER.error(
"Server: %s not configured. API call returned: %s", "Server: %s not configured. API call returned: %s",
@ -71,23 +82,46 @@ def setup(hass, base_config):
) )
return False return False
discover_devices(hass, base_config) await discover_devices(hass, base_config)
return True return True
def discover_devices(hass, hass_config): async def discover_devices(hass, hass_config):
""" """
Run periodically to discover new devices. Run periodically to discover new devices.
Currently it's only run at startup. Currently it is only run at startup.
""" """
component_configs = {} 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"] channel_function = channel["function"]["name"]
if channel_function == SUPLA_FUNCTION_NONE: if channel_function == SUPLA_FUNCTION_NONE:
_LOGGER.debug( _LOGGER.debug(
"Ignored function: %s, channel id: %s", "Ignored function: %s, channel id: %s",
@ -107,25 +141,38 @@ def discover_devices(hass, hass_config):
continue continue
channel["server_name"] = server_name 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 # Load discovered devices
for component_name, channel in component_configs.items(): for component_name, config in component_configs.items():
load_platform(hass, component_name, "supla", channel, hass_config) await async_load_platform(hass, component_name, DOMAIN, config, hass_config)
class SuplaChannel(Entity): class SuplaChannel(Entity):
"""Base class of a Supla Channel (an equivalent of HA's Entity).""" """Base class of a Supla Channel (an equivalent of HA's Entity)."""
def __init__(self, channel_data): def __init__(self, config, server, coordinator):
"""Channel data -- raw channel information from PySupla.""" """Init from config, hookup[ server and coordinator."""
self.server_name = channel_data["server_name"] self.server_name = config["server_name"]
self.channel_data = channel_data self.channel_id = config["channel_id"]
self.server = server
self.coordinator = coordinator
@property @property
def server(self): def channel_data(self):
"""Return PySupla's server component associated with entity.""" """Return channel data taken from coordinator."""
return self.hass.data[SUPLA_SERVERS][self.server_name] return self.coordinator.data.get(self.channel_id)
@property
def should_poll(self):
"""Supla uses DataUpdateCoordinator, so no additional polling needed."""
return False
@property @property
def unique_id(self) -> str: def unique_id(self) -> str:
@ -150,7 +197,13 @@ class SuplaChannel(Entity):
return False return False
return state.get("connected") 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. Run server action.
@ -163,10 +216,14 @@ class SuplaChannel(Entity):
self.channel_data["id"], self.channel_data["id"],
add_pars, 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): # Update state
"""Call to update state.""" await self.coordinator.async_request_refresh()
self.channel_data = self.server.get_channel(
self.channel_data["id"], include=["connected", "state"] async def async_update(self):
) """Update the entity.
Only used by the generic entity update service.
"""
await self.coordinator.async_request_refresh()

View File

@ -7,7 +7,12 @@ from homeassistant.components.cover import (
DEVICE_CLASS_GARAGE, DEVICE_CLASS_GARAGE,
CoverEntity, CoverEntity,
) )
from homeassistant.components.supla import SuplaChannel from homeassistant.components.supla import (
DOMAIN,
SUPLA_COORDINATORS,
SUPLA_SERVERS,
SuplaChannel,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -15,7 +20,7 @@ SUPLA_SHUTTER = "CONTROLLINGTHEROLLERSHUTTER"
SUPLA_GATE = "CONTROLLINGTHEGATE" 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.""" """Set up the Supla covers."""
if discovery_info is None: if discovery_info is None:
return return
@ -24,12 +29,28 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
entities = [] entities = []
for device in discovery_info: 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: 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: elif device_name == SUPLA_GATE:
entities.append(SuplaGateDoor(device)) entities.append(
add_entities(entities) SuplaGateDoor(
device,
hass.data[DOMAIN][SUPLA_SERVERS][server_name],
hass.data[DOMAIN][SUPLA_COORDINATORS][server_name],
)
)
async_add_entities(entities)
class SuplaCover(SuplaChannel, CoverEntity): class SuplaCover(SuplaChannel, CoverEntity):
@ -43,9 +64,9 @@ class SuplaCover(SuplaChannel, CoverEntity):
return 100 - state["shut"] return 100 - state["shut"]
return None return None
def set_cover_position(self, **kwargs): async def async_set_cover_position(self, **kwargs):
"""Move the cover to a specific position.""" """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 @property
def is_closed(self): def is_closed(self):
@ -54,17 +75,17 @@ class SuplaCover(SuplaChannel, CoverEntity):
return None return None
return self.current_cover_position == 0 return self.current_cover_position == 0
def open_cover(self, **kwargs): async def async_open_cover(self, **kwargs):
"""Open the cover.""" """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.""" """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.""" """Stop the cover."""
self.action("STOP") await self.async_action("STOP")
class SuplaGateDoor(SuplaChannel, CoverEntity): class SuplaGateDoor(SuplaChannel, CoverEntity):
@ -78,23 +99,23 @@ class SuplaGateDoor(SuplaChannel, CoverEntity):
return state.get("hi") return state.get("hi")
return None return None
def open_cover(self, **kwargs) -> None: async def async_open_cover(self, **kwargs) -> None:
"""Open the gate.""" """Open the gate."""
if self.is_closed: 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.""" """Close the gate."""
if not self.is_closed: 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.""" """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.""" """Toggle the gate."""
self.action("OPEN_CLOSE") await self.async_action("OPEN_CLOSE")
@property @property
def device_class(self): def device_class(self):

View File

@ -2,6 +2,6 @@
"domain": "supla", "domain": "supla",
"name": "Supla", "name": "Supla",
"documentation": "https://www.home-assistant.io/integrations/supla", "documentation": "https://www.home-assistant.io/integrations/supla",
"requirements": ["pysupla==0.0.3"], "requirements": ["asyncpysupla==0.0.5"],
"codeowners": ["@mwegrzynek"] "codeowners": ["@mwegrzynek"]
} }

View File

@ -2,32 +2,49 @@
import logging import logging
from pprint import pformat 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 from homeassistant.components.switch import SwitchEntity
_LOGGER = logging.getLogger(__name__) _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.""" """Set up the Supla switches."""
if discovery_info is None: if discovery_info is None:
return return
_LOGGER.debug("Discovery: %s", pformat(discovery_info)) _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): class SuplaSwitch(SuplaChannel, SwitchEntity):
"""Representation of a Supla Switch.""" """Representation of a Supla Switch."""
def turn_on(self, **kwargs): async def async_turn_on(self, **kwargs):
"""Turn on the switch.""" """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.""" """Turn off the switch."""
self.action("TURN_OFF") await self.async_action("TURN_OFF")
@property @property
def is_on(self): def is_on(self):

View File

@ -287,6 +287,9 @@ asterisk_mbox==0.5.0
# homeassistant.components.upnp # homeassistant.components.upnp
async-upnp-client==0.14.13 async-upnp-client==0.14.13
# homeassistant.components.supla
asyncpysupla==0.0.5
# homeassistant.components.aten_pe # homeassistant.components.aten_pe
atenpdu==0.3.0 atenpdu==0.3.0
@ -1660,9 +1663,6 @@ pystiebeleltron==0.0.1.dev2
# homeassistant.components.suez_water # homeassistant.components.suez_water
pysuez==0.1.19 pysuez==0.1.19
# homeassistant.components.supla
pysupla==0.0.3
# homeassistant.components.syncthru # homeassistant.components.syncthru
pysyncthru==0.7.0 pysyncthru==0.7.0