mirror of
https://github.com/home-assistant/core.git
synced 2025-07-24 05:37:44 +00:00
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:
parent
9baa7c6c24
commit
8818a5ab6c
@ -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()
|
||||||
|
@ -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):
|
||||||
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
@ -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):
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user