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."""
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()

View file

@ -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):

View file

@ -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"]
}

View file

@ -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):

View file

@ -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