From 601f3f9c6d1fbfb5fd615d5341d940e7dde0c99e Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 17 Jan 2022 11:58:54 +0200 Subject: [PATCH] Add Shelly Gen2 polling for sesnors missing push updates (#64171) --- homeassistant/components/shelly/__init__.py | 48 +++++++++++++++++++++ homeassistant/components/shelly/const.py | 4 ++ homeassistant/components/shelly/entity.py | 33 ++++++++++---- homeassistant/components/shelly/sensor.py | 3 ++ 4 files changed, 79 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 703e47566d7..1665b49b411 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -53,7 +53,9 @@ from .const import ( REST_SENSORS_UPDATE_INTERVAL, RPC, RPC_INPUTS_EVENTS_TYPES, + RPC_POLL, RPC_RECONNECT_INTERVAL, + RPC_SENSORS_POLLING_INTERVAL, SHBTN_MODELS, SLEEP_PERIOD_MULTIPLIER, UPDATE_PERIOD_MULTIPLIER, @@ -253,6 +255,10 @@ async def async_setup_rpc_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool ] = RpcDeviceWrapper(hass, entry, device) device_wrapper.async_setup() + hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][RPC_POLL] = RpcPollingWrapper( + hass, entry, device + ) + hass.config_entries.async_setup_platforms(entry, RPC_PLATFORMS) return True @@ -768,3 +774,45 @@ class RpcDeviceWrapper(update_coordinator.DataUpdateCoordinator): """Handle Home Assistant stopping.""" _LOGGER.debug("Stopping RpcDeviceWrapper for %s", self.name) await self.shutdown() + + +class RpcPollingWrapper(update_coordinator.DataUpdateCoordinator): + """Polling Wrapper for a Shelly RPC based device.""" + + def __init__( + self, hass: HomeAssistant, entry: ConfigEntry, device: RpcDevice + ) -> None: + """Initialize the RPC polling coordinator.""" + self.device_id: str | None = None + + device_name = get_rpc_device_name(device) if device.initialized else entry.title + super().__init__( + hass, + _LOGGER, + name=device_name, + update_interval=timedelta(seconds=RPC_SENSORS_POLLING_INTERVAL), + ) + self.entry = entry + self.device = device + + async def _async_update_data(self) -> None: + """Fetch data.""" + if not self.device.connected: + raise update_coordinator.UpdateFailed("Device disconnected") + + try: + _LOGGER.debug("Polling Shelly RPC Device - %s", self.name) + async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): + await self.device.update_status() + except OSError as err: + raise update_coordinator.UpdateFailed("Device disconnected") from err + + @property + def model(self) -> str: + """Model of the device.""" + return cast(str, self.entry.data["model"]) + + @property + def mac(self) -> str: + """Mac address of the device.""" + return cast(str, self.entry.unique_id) diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index 68053ff0663..cd5735b2888 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -10,6 +10,7 @@ DEVICE: Final = "device" DOMAIN: Final = "shelly" REST: Final = "rest" RPC: Final = "rpc" +RPC_POLL: Final = "rpc_poll" CONF_COAP_PORT: Final = "coap_port" DEFAULT_COAP_PORT: Final = 5683 @@ -53,6 +54,9 @@ POLLING_TIMEOUT_SEC: Final = 18 # Refresh interval for REST sensors REST_SENSORS_UPDATE_INTERVAL: Final = 60 +# Refresh interval for RPC polling sensors +RPC_SENSORS_POLLING_INTERVAL: Final = 60 + # Timeout used for aioshelly calls AIOSHELLY_DEVICE_TIMEOUT_SEC: Final = 10 diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index d4f26c7767d..66a87f58122 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -24,7 +24,12 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import StateType -from . import BlockDeviceWrapper, RpcDeviceWrapper, ShellyDeviceRestWrapper +from . import ( + BlockDeviceWrapper, + RpcDeviceWrapper, + RpcPollingWrapper, + ShellyDeviceRestWrapper, +) from .const import ( AIOSHELLY_DEVICE_TIMEOUT_SEC, BLOCK, @@ -32,6 +37,7 @@ from .const import ( DOMAIN, REST, RPC, + RPC_POLL, ) from .utils import ( async_remove_shelly_entity, @@ -160,6 +166,10 @@ async def async_setup_entry_rpc( config_entry.entry_id ][RPC] + polling_wrapper: RpcPollingWrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][ + config_entry.entry_id + ][RPC_POLL] + entities = [] for sensor_id in sensors: description = sensors[sensor_id] @@ -178,17 +188,17 @@ async def async_setup_entry_rpc( unique_id = f"{wrapper.mac}-{key}-{sensor_id}" await async_remove_shelly_entity(hass, domain, unique_id) else: - entities.append((key, sensor_id, description)) + if description.should_poll: + entities.append( + sensor_class(polling_wrapper, key, sensor_id, description) + ) + else: + entities.append(sensor_class(wrapper, key, sensor_id, description)) if not entities: return - async_add_entities( - [ - sensor_class(wrapper, key, sensor_id, description) - for key, sensor_id, description in entities - ] - ) + async_add_entities(entities) async def async_setup_entry_rest( @@ -257,6 +267,7 @@ class RpcAttributeDescription: removal_condition: Callable[[dict, str], bool] | None = None extra_state_attributes: Callable[[dict, dict], dict | None] | None = None entity_category: EntityCategory | None = None + should_poll: bool = False @dataclass @@ -343,7 +354,11 @@ class ShellyBlockEntity(entity.Entity): class ShellyRpcEntity(entity.Entity): """Helper class to represent a rpc entity.""" - def __init__(self, wrapper: RpcDeviceWrapper, key: str) -> None: + def __init__( + self, + wrapper: RpcDeviceWrapper | RpcPollingWrapper, + key: str, + ) -> None: """Initialize Shelly entity.""" self.wrapper = wrapper self.key = key diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index d715d7871e8..562bf70260d 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -287,6 +287,7 @@ RPC_SENSORS: Final = { state_class=SensorStateClass.MEASUREMENT, default_enabled=False, entity_category=EntityCategory.DIAGNOSTIC, + should_poll=True, ), "rssi": RpcAttributeDescription( key="wifi", @@ -297,6 +298,7 @@ RPC_SENSORS: Final = { state_class=SensorStateClass.MEASUREMENT, default_enabled=False, entity_category=EntityCategory.DIAGNOSTIC, + should_poll=True, ), "uptime": RpcAttributeDescription( key="sys", @@ -306,6 +308,7 @@ RPC_SENSORS: Final = { device_class=SensorDeviceClass.TIMESTAMP, default_enabled=False, entity_category=EntityCategory.DIAGNOSTIC, + should_poll=True, ), }