Refactor Shelly wrapper to coordinator (#79628)

pull/79676/head
Shay Levy 2022-10-05 15:39:58 +03:00 committed by GitHub
parent 4d3d22320f
commit 22c68b95bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 336 additions and 321 deletions

View File

@ -36,10 +36,10 @@ from .const import (
RPC_POLL,
)
from .coordinator import (
BlockDeviceWrapper,
RpcDeviceWrapper,
RpcPollingWrapper,
ShellyDeviceRestWrapper,
ShellyBlockCoordinator,
ShellyRestCoordinator,
ShellyRpcCoordinator,
ShellyRpcPollingCoordinator,
)
from .utils import get_block_device_sleep_period, get_coap_context, get_device_entry_gen
@ -200,17 +200,17 @@ def async_block_device_setup(
hass: HomeAssistant, entry: ConfigEntry, device: BlockDevice
) -> None:
"""Set up a block based device that is online."""
device_wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][
block_coordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][
BLOCK
] = BlockDeviceWrapper(hass, entry, device)
device_wrapper.async_setup()
] = ShellyBlockCoordinator(hass, entry, device)
block_coordinator.async_setup()
platforms = BLOCK_SLEEPING_PLATFORMS
if not entry.data.get(CONF_SLEEP_PERIOD):
hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][
REST
] = ShellyDeviceRestWrapper(hass, device, entry)
] = ShellyRestCoordinator(hass, device, entry)
platforms = BLOCK_PLATFORMS
hass.config_entries.async_setup_platforms(entry, platforms)
@ -237,14 +237,14 @@ async def async_setup_rpc_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool
except (AuthRequired, InvalidAuthError) as err:
raise ConfigEntryAuthFailed from err
device_wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][
rpc_coordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][
RPC
] = RpcDeviceWrapper(hass, entry, device)
device_wrapper.async_setup()
] = ShellyRpcCoordinator(hass, entry, device)
rpc_coordinator.async_setup()
hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][RPC_POLL] = RpcPollingWrapper(
hass, entry, device
)
hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][
RPC_POLL
] = ShellyRpcPollingCoordinator(hass, entry, device)
hass.config_entries.async_setup_platforms(entry, RPC_PLATFORMS)
@ -265,7 +265,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
device = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id].get(DEVICE)
if device is not None:
# If device is present, device wrapper is not setup yet
# If device is present, block coordinator is not setup yet
device.shutdown()
return True
@ -283,10 +283,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return unload_ok
def get_block_device_wrapper(
def get_block_device_coordinator(
hass: HomeAssistant, device_id: str
) -> BlockDeviceWrapper | None:
"""Get a Shelly block device wrapper for the given device id."""
) -> ShellyBlockCoordinator | None:
"""Get a Shelly block device coordinator for the given device id."""
if not hass.data.get(DOMAIN):
return None
@ -296,16 +296,18 @@ def get_block_device_wrapper(
if not hass.data[DOMAIN][DATA_CONFIG_ENTRY].get(config_entry):
continue
if wrapper := hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry].get(BLOCK):
return cast(BlockDeviceWrapper, wrapper)
if coordinator := hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry].get(
BLOCK
):
return cast(ShellyBlockCoordinator, coordinator)
return None
def get_rpc_device_wrapper(
def get_rpc_device_coordinator(
hass: HomeAssistant, device_id: str
) -> RpcDeviceWrapper | None:
"""Get a Shelly RPC device wrapper for the given device id."""
) -> ShellyRpcCoordinator | None:
"""Get a Shelly RPC device coordinator for the given device id."""
if not hass.data.get(DOMAIN):
return None
@ -315,7 +317,9 @@ def get_rpc_device_wrapper(
if not hass.data[DOMAIN][DATA_CONFIG_ENTRY].get(config_entry):
continue
if wrapper := hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry].get(RPC):
return cast(RpcDeviceWrapper, wrapper)
if coordinator := hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry].get(
RPC
):
return cast(ShellyRpcCoordinator, coordinator)
return None

View File

@ -15,10 +15,11 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.entity import DeviceInfo, EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import slugify
from . import BlockDeviceWrapper, RpcDeviceWrapper
from .const import BLOCK, DATA_CONFIG_ENTRY, DOMAIN, RPC, SHELLY_GAS_MODELS
from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator
from .utils import get_block_device_name, get_device_entry_gen, get_rpc_device_name
@ -42,31 +43,31 @@ BUTTONS: Final = [
name="Reboot",
device_class=ButtonDeviceClass.RESTART,
entity_category=EntityCategory.CONFIG,
press_action=lambda wrapper: wrapper.device.trigger_reboot(),
press_action=lambda coordinator: coordinator.device.trigger_reboot(),
),
ShellyButtonDescription(
key="self_test",
name="Self Test",
icon="mdi:progress-wrench",
entity_category=EntityCategory.DIAGNOSTIC,
press_action=lambda wrapper: wrapper.device.trigger_shelly_gas_self_test(),
supported=lambda wrapper: wrapper.device.model in SHELLY_GAS_MODELS,
press_action=lambda coordinator: coordinator.device.trigger_shelly_gas_self_test(),
supported=lambda coordinator: coordinator.device.model in SHELLY_GAS_MODELS,
),
ShellyButtonDescription(
key="mute",
name="Mute",
icon="mdi:volume-mute",
entity_category=EntityCategory.CONFIG,
press_action=lambda wrapper: wrapper.device.trigger_shelly_gas_mute(),
supported=lambda wrapper: wrapper.device.model in SHELLY_GAS_MODELS,
press_action=lambda coordinator: coordinator.device.trigger_shelly_gas_mute(),
supported=lambda coordinator: coordinator.device.model in SHELLY_GAS_MODELS,
),
ShellyButtonDescription(
key="unmute",
name="Unmute",
icon="mdi:volume-high",
entity_category=EntityCategory.CONFIG,
press_action=lambda wrapper: wrapper.device.trigger_shelly_gas_unmute(),
supported=lambda wrapper: wrapper.device.model in SHELLY_GAS_MODELS,
press_action=lambda coordinator: coordinator.device.trigger_shelly_gas_unmute(),
supported=lambda coordinator: coordinator.device.model in SHELLY_GAS_MODELS,
),
]
@ -77,54 +78,54 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set buttons for device."""
wrapper: RpcDeviceWrapper | BlockDeviceWrapper | None = None
coordinator: ShellyRpcCoordinator | ShellyBlockCoordinator | None = None
if get_device_entry_gen(config_entry) == 2:
if rpc_wrapper := hass.data[DOMAIN][DATA_CONFIG_ENTRY][
if rpc_coordinator := hass.data[DOMAIN][DATA_CONFIG_ENTRY][
config_entry.entry_id
].get(RPC):
wrapper = cast(RpcDeviceWrapper, rpc_wrapper)
coordinator = cast(ShellyRpcCoordinator, rpc_coordinator)
else:
if block_wrapper := hass.data[DOMAIN][DATA_CONFIG_ENTRY][
if block_coordinator := hass.data[DOMAIN][DATA_CONFIG_ENTRY][
config_entry.entry_id
].get(BLOCK):
wrapper = cast(BlockDeviceWrapper, block_wrapper)
coordinator = cast(ShellyBlockCoordinator, block_coordinator)
if wrapper is not None:
if coordinator is not None:
entities = []
for button in BUTTONS:
if not button.supported(wrapper):
if not button.supported(coordinator):
continue
entities.append(ShellyButton(wrapper, button))
entities.append(ShellyButton(coordinator, button))
async_add_entities(entities)
class ShellyButton(ButtonEntity):
class ShellyButton(CoordinatorEntity, ButtonEntity):
"""Defines a Shelly base button."""
entity_description: ShellyButtonDescription
def __init__(
self,
wrapper: RpcDeviceWrapper | BlockDeviceWrapper,
coordinator: ShellyRpcCoordinator | ShellyBlockCoordinator,
description: ShellyButtonDescription,
) -> None:
"""Initialize Shelly button."""
super().__init__(coordinator)
self.entity_description = description
self.wrapper = wrapper
if isinstance(wrapper, RpcDeviceWrapper):
device_name = get_rpc_device_name(wrapper.device)
if isinstance(coordinator, ShellyRpcCoordinator):
device_name = get_rpc_device_name(coordinator.device)
else:
device_name = get_block_device_name(wrapper.device)
device_name = get_block_device_name(coordinator.device)
self._attr_name = f"{device_name} {description.name}"
self._attr_unique_id = slugify(self._attr_name)
self._attr_device_info = DeviceInfo(
connections={(CONNECTION_NETWORK_MAC, wrapper.mac)}
connections={(CONNECTION_NETWORK_MAC, coordinator.mac)}
)
async def async_press(self) -> None:
"""Triggers the Shelly button press service."""
await self.entity_description.press_action(self.wrapper)
await self.entity_description.press_action(self.coordinator)

View File

@ -20,12 +20,12 @@ from homeassistant.components.climate import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
from homeassistant.core import HomeAssistant, State, callback
from homeassistant.helpers import device_registry, entity_registry, update_coordinator
from homeassistant.helpers import device_registry, entity_registry
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import BlockDeviceWrapper
from .const import (
AIOSHELLY_DEVICE_TIMEOUT_SEC,
BLOCK,
@ -34,6 +34,7 @@ from .const import (
LOGGER,
SHTRV_01_TEMPERATURE_SETTINGS,
)
from .coordinator import ShellyBlockCoordinator
from .utils import get_device_entry_gen
@ -47,37 +48,41 @@ async def async_setup_entry(
if get_device_entry_gen(config_entry) == 2:
return
wrapper: BlockDeviceWrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][
coordinator: ShellyBlockCoordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][
config_entry.entry_id
][BLOCK]
if wrapper.device.initialized:
async_setup_climate_entities(async_add_entities, wrapper)
if coordinator.device.initialized:
async_setup_climate_entities(async_add_entities, coordinator)
else:
async_restore_climate_entities(hass, config_entry, async_add_entities, wrapper)
async_restore_climate_entities(
hass, config_entry, async_add_entities, coordinator
)
@callback
def async_setup_climate_entities(
async_add_entities: AddEntitiesCallback,
wrapper: BlockDeviceWrapper,
coordinator: ShellyBlockCoordinator,
) -> None:
"""Set up online climate devices."""
device_block: Block | None = None
sensor_block: Block | None = None
assert wrapper.device.blocks
assert coordinator.device.blocks
for block in wrapper.device.blocks:
for block in coordinator.device.blocks:
if block.type == "device":
device_block = block
if hasattr(block, "targetTemp"):
sensor_block = block
if sensor_block and device_block:
LOGGER.debug("Setup online climate device %s", wrapper.name)
async_add_entities([BlockSleepingClimate(wrapper, sensor_block, device_block)])
LOGGER.debug("Setup online climate device %s", coordinator.name)
async_add_entities(
[BlockSleepingClimate(coordinator, sensor_block, device_block)]
)
@callback
@ -85,7 +90,7 @@ def async_restore_climate_entities(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
wrapper: BlockDeviceWrapper,
coordinator: ShellyBlockCoordinator,
) -> None:
"""Restore sleeping climate devices."""
@ -99,16 +104,14 @@ def async_restore_climate_entities(
if entry.domain != CLIMATE_DOMAIN:
continue
LOGGER.debug("Setup sleeping climate device %s", wrapper.name)
LOGGER.debug("Setup sleeping climate device %s", coordinator.name)
LOGGER.debug("Found entry %s [%s]", entry.original_name, entry.domain)
async_add_entities([BlockSleepingClimate(wrapper, None, None, entry)])
async_add_entities([BlockSleepingClimate(coordinator, None, None, entry)])
break
class BlockSleepingClimate(
update_coordinator.CoordinatorEntity,
RestoreEntity,
ClimateEntity,
CoordinatorEntity[ShellyBlockCoordinator], RestoreEntity, ClimateEntity
):
"""Representation of a Shelly climate device."""
@ -124,16 +127,14 @@ class BlockSleepingClimate(
def __init__(
self,
wrapper: BlockDeviceWrapper,
coordinator: ShellyBlockCoordinator,
sensor_block: Block | None,
device_block: Block | None,
entry: entity_registry.RegistryEntry | None = None,
) -> None:
"""Initialize climate."""
super().__init__(coordinator)
super().__init__(wrapper)
self.wrapper = wrapper
self.block: Block | None = sensor_block
self.control_result: dict[str, Any] | None = None
self.device_block: Block | None = device_block
@ -142,11 +143,11 @@ class BlockSleepingClimate(
self._preset_modes: list[str] = []
if self.block is not None and self.device_block is not None:
self._unique_id = f"{self.wrapper.mac}-{self.block.description}"
self._unique_id = f"{self.coordinator.mac}-{self.block.description}"
assert self.block.channel
self._preset_modes = [
PRESET_NONE,
*wrapper.device.settings["thermostats"][int(self.block.channel)][
*coordinator.device.settings["thermostats"][int(self.block.channel)][
"schedule_profile_names"
],
]
@ -163,7 +164,7 @@ class BlockSleepingClimate(
@property
def name(self) -> str:
"""Name of entity."""
return self.wrapper.name
return self.coordinator.name
@property
def target_temperature(self) -> float | None:
@ -184,7 +185,7 @@ class BlockSleepingClimate(
"""Device availability."""
if self.device_block is not None:
return not cast(bool, self.device_block.valveError)
return self.wrapper.last_update_success
return self.coordinator.last_update_success
@property
def hvac_mode(self) -> HVACMode:
@ -229,7 +230,9 @@ class BlockSleepingClimate(
def device_info(self) -> DeviceInfo:
"""Device info."""
return {
"connections": {(device_registry.CONNECTION_NETWORK_MAC, self.wrapper.mac)}
"connections": {
(device_registry.CONNECTION_NETWORK_MAC, self.coordinator.mac)
}
}
def _check_is_off(self) -> bool:
@ -244,7 +247,7 @@ class BlockSleepingClimate(
LOGGER.debug("Setting state for entity %s, state: %s", self.name, kwargs)
try:
async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC):
return await self.wrapper.device.http_request(
return await self.coordinator.device.http_request(
"get", f"thermostat/{self._channel}", kwargs
)
except (asyncio.TimeoutError, OSError) as err:
@ -254,7 +257,7 @@ class BlockSleepingClimate(
kwargs,
repr(err),
)
self.wrapper.last_update_success = False
self.coordinator.last_update_success = False
return None
async def async_set_temperature(self, **kwargs: Any) -> None:
@ -302,13 +305,13 @@ class BlockSleepingClimate(
@callback
def _handle_coordinator_update(self) -> None:
"""Handle device update."""
if not self.wrapper.device.initialized:
if not self.coordinator.device.initialized:
self.async_write_ha_state()
return
assert self.wrapper.device.blocks
assert self.coordinator.device.blocks
for block in self.wrapper.device.blocks:
for block in self.coordinator.device.blocks:
if block.type == "device":
self.device_block = block
if hasattr(block, "targetTemp"):
@ -322,11 +325,11 @@ class BlockSleepingClimate(
try:
self._preset_modes = [
PRESET_NONE,
*self.wrapper.device.settings["thermostats"][
*self.coordinator.device.settings["thermostats"][
int(self.block.channel)
]["schedule_profile_names"],
]
except AuthRequired:
self.wrapper.entry.async_start_reauth(self.hass)
self.coordinator.entry.async_start_reauth(self.hass)
else:
self.async_write_ha_state()

View File

@ -14,8 +14,9 @@ import async_timeout
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_DEVICE_ID, CONF_HOST, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.helpers import device_registry, update_coordinator
from homeassistant.helpers import device_registry
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import (
AIOSHELLY_DEVICE_TIMEOUT_SEC,
@ -44,13 +45,13 @@ from .const import (
from .utils import device_update_info, get_block_device_name, get_rpc_device_name
class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator):
"""Wrapper for a Shelly block based device with Home Assistant specific functions."""
class ShellyBlockCoordinator(DataUpdateCoordinator):
"""Coordinator for a Shelly block based device."""
def __init__(
self, hass: HomeAssistant, entry: ConfigEntry, device: BlockDevice
) -> None:
"""Initialize the Shelly device wrapper."""
"""Initialize the Shelly block device coordinator."""
self.device_id: str | None = None
if sleep_period := entry.data[CONF_SLEEP_PERIOD]:
@ -186,7 +187,7 @@ class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator):
"""Fetch data."""
if sleep_period := self.entry.data.get(CONF_SLEEP_PERIOD):
# Sleeping device, no point polling it, just mark it unavailable
raise update_coordinator.UpdateFailed(
raise UpdateFailed(
f"Sleeping device did not update within {sleep_period} seconds interval"
)
@ -196,7 +197,7 @@ class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator):
await self.device.update()
device_update_info(self.hass, self.device, self.entry)
except OSError as err:
raise update_coordinator.UpdateFailed("Error fetching data") from err
raise UpdateFailed("Error fetching data") from err
@property
def model(self) -> str:
@ -214,7 +215,7 @@ class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator):
return self.device.firmware_version if self.device.initialized else ""
def async_setup(self) -> None:
"""Set up the wrapper."""
"""Set up the coordinator."""
dev_reg = device_registry.async_get(self.hass)
entry = dev_reg.async_get_or_create(
config_entry_id=self.entry.entry_id,
@ -265,23 +266,23 @@ class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator):
LOGGER.debug("Result of OTA update call: %s", result)
def shutdown(self) -> None:
"""Shutdown the wrapper."""
"""Shutdown the coordinator."""
self.device.shutdown()
@callback
def _handle_ha_stop(self, _event: Event) -> None:
"""Handle Home Assistant stopping."""
LOGGER.debug("Stopping BlockDeviceWrapper for %s", self.name)
LOGGER.debug("Stopping block device coordinator for %s", self.name)
self.shutdown()
class ShellyDeviceRestWrapper(update_coordinator.DataUpdateCoordinator):
"""Rest Wrapper for a Shelly device with Home Assistant specific functions."""
class ShellyRestCoordinator(DataUpdateCoordinator):
"""Coordinator for a Shelly REST device."""
def __init__(
self, hass: HomeAssistant, device: BlockDevice, entry: ConfigEntry
) -> None:
"""Initialize the Shelly device wrapper."""
"""Initialize the Shelly REST device coordinator."""
if (
device.settings["device"]["type"]
in BATTERY_DEVICES_WITH_PERMANENT_CONNECTION
@ -316,7 +317,7 @@ class ShellyDeviceRestWrapper(update_coordinator.DataUpdateCoordinator):
return
device_update_info(self.hass, self.device, self.entry)
except OSError as err:
raise update_coordinator.UpdateFailed("Error fetching data") from err
raise UpdateFailed("Error fetching data") from err
@property
def mac(self) -> str:
@ -324,13 +325,13 @@ class ShellyDeviceRestWrapper(update_coordinator.DataUpdateCoordinator):
return cast(str, self.device.settings["device"]["mac"])
class RpcDeviceWrapper(update_coordinator.DataUpdateCoordinator):
"""Wrapper for a Shelly RPC based device with Home Assistant specific functions."""
class ShellyRpcCoordinator(DataUpdateCoordinator):
"""Coordinator for a Shelly RPC based device."""
def __init__(
self, hass: HomeAssistant, entry: ConfigEntry, device: RpcDevice
) -> None:
"""Initialize the Shelly device wrapper."""
"""Initialize the Shelly RPC device coordinator."""
self.device_id: str | None = None
device_name = get_rpc_device_name(device) if device.initialized else entry.title
@ -413,7 +414,7 @@ class RpcDeviceWrapper(update_coordinator.DataUpdateCoordinator):
await self.device.initialize()
device_update_info(self.hass, self.device, self.entry)
except OSError as err:
raise update_coordinator.UpdateFailed("Device disconnected") from err
raise UpdateFailed("Device disconnected") from err
@property
def model(self) -> str:
@ -431,7 +432,7 @@ class RpcDeviceWrapper(update_coordinator.DataUpdateCoordinator):
return self.device.firmware_version if self.device.initialized else ""
def async_setup(self) -> None:
"""Set up the wrapper."""
"""Set up the coordinator."""
dev_reg = device_registry.async_get(self.hass)
entry = dev_reg.async_get_or_create(
config_entry_id=self.entry.entry_id,
@ -482,17 +483,17 @@ class RpcDeviceWrapper(update_coordinator.DataUpdateCoordinator):
LOGGER.debug("OTA update call successful")
async def shutdown(self) -> None:
"""Shutdown the wrapper."""
"""Shutdown the coordinator."""
await self.device.shutdown()
async def _handle_ha_stop(self, _event: Event) -> None:
"""Handle Home Assistant stopping."""
LOGGER.debug("Stopping RpcDeviceWrapper for %s", self.name)
LOGGER.debug("Stopping RPC device coordinator for %s", self.name)
await self.shutdown()
class RpcPollingWrapper(update_coordinator.DataUpdateCoordinator):
"""Polling Wrapper for a Shelly RPC based device."""
class ShellyRpcPollingCoordinator(DataUpdateCoordinator):
"""Polling coordinator for a Shelly RPC based device."""
def __init__(
self, hass: HomeAssistant, entry: ConfigEntry, device: RpcDevice
@ -513,14 +514,14 @@ class RpcPollingWrapper(update_coordinator.DataUpdateCoordinator):
async def _async_update_data(self) -> None:
"""Fetch data."""
if not self.device.connected:
raise update_coordinator.UpdateFailed("Device disconnected")
raise 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, aioshelly.exceptions.RPCTimeout) as err:
raise update_coordinator.UpdateFailed("Device disconnected") from err
raise UpdateFailed("Device disconnected") from err
@property
def model(self) -> str:

View File

@ -15,8 +15,8 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import BlockDeviceWrapper, RpcDeviceWrapper
from .const import BLOCK, DATA_CONFIG_ENTRY, DOMAIN, RPC
from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator
from .entity import ShellyBlockEntity, ShellyRpcEntity
from .utils import get_device_entry_gen, get_rpc_key_ids
@ -40,13 +40,13 @@ def async_setup_block_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up cover for device."""
wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][BLOCK]
blocks = [block for block in wrapper.device.blocks if block.type == "roller"]
coordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][BLOCK]
blocks = [block for block in coordinator.device.blocks if block.type == "roller"]
if not blocks:
return
async_add_entities(BlockShellyCover(wrapper, block) for block in blocks)
async_add_entities(BlockShellyCover(coordinator, block) for block in blocks)
@callback
@ -56,14 +56,14 @@ def async_setup_rpc_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up entities for RPC device."""
wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][RPC]
coordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][RPC]
cover_key_ids = get_rpc_key_ids(wrapper.device.status, "cover")
cover_key_ids = get_rpc_key_ids(coordinator.device.status, "cover")
if not cover_key_ids:
return
async_add_entities(RpcShellyCover(wrapper, id_) for id_ in cover_key_ids)
async_add_entities(RpcShellyCover(coordinator, id_) for id_ in cover_key_ids)
class BlockShellyCover(ShellyBlockEntity, CoverEntity):
@ -71,14 +71,14 @@ class BlockShellyCover(ShellyBlockEntity, CoverEntity):
_attr_device_class = CoverDeviceClass.SHUTTER
def __init__(self, wrapper: BlockDeviceWrapper, block: Block) -> None:
def __init__(self, coordinator: ShellyBlockCoordinator, block: Block) -> None:
"""Initialize block cover."""
super().__init__(wrapper, block)
super().__init__(coordinator, block)
self.control_result: dict[str, Any] | None = None
self._attr_supported_features: int = (
CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE | CoverEntityFeature.STOP
)
if self.wrapper.device.settings["rollers"][0]["positioning"]:
if self.coordinator.device.settings["rollers"][0]["positioning"]:
self._attr_supported_features |= CoverEntityFeature.SET_POSITION
@property
@ -147,9 +147,9 @@ class RpcShellyCover(ShellyRpcEntity, CoverEntity):
_attr_device_class = CoverDeviceClass.SHUTTER
def __init__(self, wrapper: RpcDeviceWrapper, id_: int) -> None:
def __init__(self, coordinator: ShellyRpcCoordinator, id_: int) -> None:
"""Initialize rpc cover."""
super().__init__(wrapper, f"cover:{id_}")
super().__init__(coordinator, f"cover:{id_}")
self._id = id_
self._attr_supported_features: int = (
CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE | CoverEntityFeature.STOP

View File

@ -22,7 +22,7 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant
from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
from homeassistant.helpers.typing import ConfigType
from . import get_block_device_wrapper, get_rpc_device_wrapper
from . import get_block_device_coordinator, get_rpc_device_coordinator
from .const import (
ATTR_CHANNEL,
ATTR_CLICK_TYPE,
@ -78,23 +78,23 @@ async def async_validate_trigger_config(
trigger = (config[CONF_TYPE], config[CONF_SUBTYPE])
if config[CONF_TYPE] in RPC_INPUTS_EVENTS_TYPES:
rpc_wrapper = get_rpc_device_wrapper(hass, config[CONF_DEVICE_ID])
if not rpc_wrapper or not rpc_wrapper.device.initialized:
rpc_coordinator = get_rpc_device_coordinator(hass, config[CONF_DEVICE_ID])
if not rpc_coordinator or not rpc_coordinator.device.initialized:
return config
input_triggers = get_rpc_input_triggers(rpc_wrapper.device)
input_triggers = get_rpc_input_triggers(rpc_coordinator.device)
if trigger in input_triggers:
return config
elif config[CONF_TYPE] in BLOCK_INPUTS_EVENTS_TYPES:
block_wrapper = get_block_device_wrapper(hass, config[CONF_DEVICE_ID])
if not block_wrapper or not block_wrapper.device.initialized:
block_coordinator = get_block_device_coordinator(hass, config[CONF_DEVICE_ID])
if not block_coordinator or not block_coordinator.device.initialized:
return config
assert block_wrapper.device.blocks
assert block_coordinator.device.blocks
for block in block_wrapper.device.blocks:
input_triggers = get_block_input_triggers(block_wrapper.device, block)
for block in block_coordinator.device.blocks:
input_triggers = get_block_input_triggers(block_coordinator.device, block)
if trigger in input_triggers:
return config
@ -109,24 +109,24 @@ async def async_get_triggers(
"""List device triggers for Shelly devices."""
triggers: list[dict[str, str]] = []
if rpc_wrapper := get_rpc_device_wrapper(hass, device_id):
input_triggers = get_rpc_input_triggers(rpc_wrapper.device)
if rpc_coordinator := get_rpc_device_coordinator(hass, device_id):
input_triggers = get_rpc_input_triggers(rpc_coordinator.device)
append_input_triggers(triggers, input_triggers, device_id)
return triggers
if block_wrapper := get_block_device_wrapper(hass, device_id):
if block_wrapper.model in SHBTN_MODELS:
if block_coordinator := get_block_device_coordinator(hass, device_id):
if block_coordinator.model in SHBTN_MODELS:
input_triggers = get_shbtn_input_triggers()
append_input_triggers(triggers, input_triggers, device_id)
return triggers
if not block_wrapper.device.initialized:
if not block_coordinator.device.initialized:
return triggers
assert block_wrapper.device.blocks
assert block_coordinator.device.blocks
for block in block_wrapper.device.blocks:
input_triggers = get_block_input_triggers(block_wrapper.device, block)
for block in block_coordinator.device.blocks:
input_triggers = get_block_input_triggers(block_coordinator.device, block)
append_input_triggers(triggers, input_triggers, device_id)
return triggers

View File

@ -6,8 +6,8 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from . import BlockDeviceWrapper, RpcDeviceWrapper
from .const import BLOCK, DATA_CONFIG_ENTRY, DOMAIN, RPC
from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator
TO_REDACT = {CONF_USERNAME, CONF_PASSWORD}
@ -21,21 +21,21 @@ async def async_get_config_entry_diagnostics(
device_settings: str | dict = "not initialized"
device_status: str | dict = "not initialized"
if BLOCK in data:
block_wrapper: BlockDeviceWrapper = data[BLOCK]
block_coordinator: ShellyBlockCoordinator = data[BLOCK]
device_info = {
"name": block_wrapper.name,
"model": block_wrapper.model,
"sw_version": block_wrapper.sw_version,
"name": block_coordinator.name,
"model": block_coordinator.model,
"sw_version": block_coordinator.sw_version,
}
if block_wrapper.device.initialized:
if block_coordinator.device.initialized:
device_settings = {
k: v
for k, v in block_wrapper.device.settings.items()
for k, v in block_coordinator.device.settings.items()
if k in ["cloud", "coiot"]
}
device_status = {
k: v
for k, v in block_wrapper.device.status.items()
for k, v in block_coordinator.device.status.items()
if k
in [
"update",
@ -51,19 +51,19 @@ async def async_get_config_entry_diagnostics(
]
}
else:
rpc_wrapper: RpcDeviceWrapper = data[RPC]
rpc_coordinator: ShellyRpcCoordinator = data[RPC]
device_info = {
"name": rpc_wrapper.name,
"model": rpc_wrapper.model,
"sw_version": rpc_wrapper.sw_version,
"name": rpc_coordinator.name,
"model": rpc_coordinator.model,
"sw_version": rpc_coordinator.sw_version,
}
if rpc_wrapper.device.initialized:
if rpc_coordinator.device.initialized:
device_settings = {
k: v for k, v in rpc_wrapper.device.config.items() if k in ["cloud"]
k: v for k, v in rpc_coordinator.device.config.items() if k in ["cloud"]
}
device_status = {
k: v
for k, v in rpc_wrapper.device.status.items()
for k, v in rpc_coordinator.device.status.items()
if k in ["sys", "wifi"]
}

View File

@ -11,23 +11,13 @@ import async_timeout
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import (
device_registry,
entity,
entity_registry,
update_coordinator,
)
from homeassistant.helpers import device_registry, entity, entity_registry
from homeassistant.helpers.entity import DeviceInfo, EntityDescription
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import (
BlockDeviceWrapper,
RpcDeviceWrapper,
RpcPollingWrapper,
ShellyDeviceRestWrapper,
)
from .const import (
AIOSHELLY_DEVICE_TIMEOUT_SEC,
BLOCK,
@ -38,6 +28,12 @@ from .const import (
RPC,
RPC_POLL,
)
from .coordinator import (
ShellyBlockCoordinator,
ShellyRestCoordinator,
ShellyRpcCoordinator,
ShellyRpcPollingCoordinator,
)
from .utils import (
async_remove_shelly_entity,
get_block_entity_name,
@ -58,20 +54,20 @@ def async_setup_entry_attribute_entities(
],
) -> None:
"""Set up entities for attributes."""
wrapper: BlockDeviceWrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][
coordinator: ShellyBlockCoordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][
config_entry.entry_id
][BLOCK]
if wrapper.device.initialized:
if coordinator.device.initialized:
async_setup_block_attribute_entities(
hass, async_add_entities, wrapper, sensors, sensor_class
hass, async_add_entities, coordinator, sensors, sensor_class
)
else:
async_restore_block_attribute_entities(
hass,
config_entry,
async_add_entities,
wrapper,
coordinator,
sensors,
sensor_class,
description_class,
@ -82,16 +78,16 @@ def async_setup_entry_attribute_entities(
def async_setup_block_attribute_entities(
hass: HomeAssistant,
async_add_entities: AddEntitiesCallback,
wrapper: BlockDeviceWrapper,
coordinator: ShellyBlockCoordinator,
sensors: Mapping[tuple[str, str], BlockEntityDescription],
sensor_class: Callable,
) -> None:
"""Set up entities for block attributes."""
blocks = []
assert wrapper.device.blocks
assert coordinator.device.blocks
for block in wrapper.device.blocks:
for block in coordinator.device.blocks:
for sensor_id in block.sensor_ids:
description = sensors.get((block.type, sensor_id))
if description is None:
@ -103,10 +99,10 @@ def async_setup_block_attribute_entities(
# Filter and remove entities that according to settings should not create an entity
if description.removal_condition and description.removal_condition(
wrapper.device.settings, block
coordinator.device.settings, block
):
domain = sensor_class.__module__.split(".")[-1]
unique_id = f"{wrapper.mac}-{block.description}-{sensor_id}"
unique_id = f"{coordinator.mac}-{block.description}-{sensor_id}"
async_remove_shelly_entity(hass, domain, unique_id)
else:
blocks.append((block, sensor_id, description))
@ -116,7 +112,7 @@ def async_setup_block_attribute_entities(
async_add_entities(
[
sensor_class(wrapper, block, sensor_id, description)
sensor_class(coordinator, block, sensor_id, description)
for block, sensor_id, description in blocks
]
)
@ -127,7 +123,7 @@ def async_restore_block_attribute_entities(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
wrapper: BlockDeviceWrapper,
coordinator: ShellyBlockCoordinator,
sensors: Mapping[tuple[str, str], BlockEntityDescription],
sensor_class: Callable,
description_class: Callable[
@ -152,7 +148,7 @@ def async_restore_block_attribute_entities(
description = description_class(entry)
entities.append(
sensor_class(wrapper, None, attribute, description, entry, sensors)
sensor_class(coordinator, None, attribute, description, entry, sensors)
)
if not entities:
@ -170,40 +166,44 @@ def async_setup_entry_rpc(
sensor_class: Callable,
) -> None:
"""Set up entities for REST sensors."""
wrapper: RpcDeviceWrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][
coordinator: ShellyRpcCoordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][
config_entry.entry_id
][RPC]
polling_wrapper: RpcPollingWrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][
config_entry.entry_id
][RPC_POLL]
polling_coordinator: ShellyRpcPollingCoordinator = hass.data[DOMAIN][
DATA_CONFIG_ENTRY
][config_entry.entry_id][RPC_POLL]
entities = []
for sensor_id in sensors:
description = sensors[sensor_id]
key_instances = get_rpc_key_instances(wrapper.device.status, description.key)
key_instances = get_rpc_key_instances(
coordinator.device.status, description.key
)
for key in key_instances:
# Filter non-existing sensors
if description.sub_key not in wrapper.device.status[
if description.sub_key not in coordinator.device.status[
key
] and not description.supported(wrapper.device.status[key]):
] and not description.supported(coordinator.device.status[key]):
continue
# Filter and remove entities that according to settings/status should not create an entity
if description.removal_condition and description.removal_condition(
wrapper.device.config, wrapper.device.status, key
coordinator.device.config, coordinator.device.status, key
):
domain = sensor_class.__module__.split(".")[-1]
unique_id = f"{wrapper.mac}-{key}-{sensor_id}"
unique_id = f"{coordinator.mac}-{key}-{sensor_id}"
async_remove_shelly_entity(hass, domain, unique_id)
else:
if description.use_polling_wrapper:
if description.use_polling_coordinator:
entities.append(
sensor_class(polling_wrapper, key, sensor_id, description)
sensor_class(polling_coordinator, key, sensor_id, description)
)
else:
entities.append(sensor_class(wrapper, key, sensor_id, description))
entities.append(
sensor_class(coordinator, key, sensor_id, description)
)
if not entities:
return
@ -220,7 +220,7 @@ def async_setup_entry_rest(
sensor_class: Callable,
) -> None:
"""Set up entities for REST sensors."""
wrapper: ShellyDeviceRestWrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][
coordinator: ShellyRestCoordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][
config_entry.entry_id
][REST]
@ -228,7 +228,7 @@ def async_setup_entry_rest(
for sensor_id in sensors:
description = sensors.get(sensor_id)
if not wrapper.device.settings.get("sleep_mode"):
if not coordinator.device.settings.get("sleep_mode"):
entities.append((sensor_id, description))
if not entities:
@ -236,7 +236,7 @@ def async_setup_entry_rest(
async_add_entities(
[
sensor_class(wrapper, sensor_id, description)
sensor_class(coordinator, sensor_id, description)
for sensor_id, description in entities
]
)
@ -270,7 +270,7 @@ class RpcEntityDescription(EntityDescription, RpcEntityRequiredKeysMixin):
available: Callable[[dict], bool] | None = None
removal_condition: Callable[[dict, dict, str], bool] | None = None
extra_state_attributes: Callable[[dict, dict], dict | None] | None = None
use_polling_wrapper: bool = False
use_polling_coordinator: bool = False
supported: Callable = lambda _: False
@ -282,32 +282,32 @@ class RestEntityDescription(EntityDescription):
extra_state_attributes: Callable[[dict], dict | None] | None = None
class ShellyBlockEntity(entity.Entity):
class ShellyBlockEntity(CoordinatorEntity[ShellyBlockCoordinator]):
"""Helper class to represent a block entity."""
def __init__(self, wrapper: BlockDeviceWrapper, block: Block) -> None:
def __init__(self, coordinator: ShellyBlockCoordinator, block: Block) -> None:
"""Initialize Shelly entity."""
self.wrapper = wrapper
super().__init__(coordinator)
self.block = block
self._attr_name = get_block_entity_name(wrapper.device, block)
self._attr_name = get_block_entity_name(coordinator.device, block)
self._attr_should_poll = False
self._attr_device_info = DeviceInfo(
connections={(device_registry.CONNECTION_NETWORK_MAC, wrapper.mac)}
connections={(device_registry.CONNECTION_NETWORK_MAC, coordinator.mac)}
)
self._attr_unique_id = f"{wrapper.mac}-{block.description}"
self._attr_unique_id = f"{coordinator.mac}-{block.description}"
@property
def available(self) -> bool:
"""Available."""
return self.wrapper.last_update_success
return self.coordinator.last_update_success
async def async_added_to_hass(self) -> None:
"""When entity is added to HASS."""
self.async_on_remove(self.wrapper.async_add_listener(self._update_callback))
self.async_on_remove(self.coordinator.async_add_listener(self._update_callback))
async def async_update(self) -> None:
"""Update entity with latest info."""
await self.wrapper.async_request_refresh()
await self.coordinator.async_request_refresh()
@callback
def _update_callback(self) -> None:
@ -327,7 +327,7 @@ class ShellyBlockEntity(entity.Entity):
kwargs,
repr(err),
)
self.wrapper.last_update_success = False
self.coordinator.last_update_success = False
return None
@ -336,36 +336,36 @@ class ShellyRpcEntity(entity.Entity):
def __init__(
self,
wrapper: RpcDeviceWrapper | RpcPollingWrapper,
coordinator: ShellyRpcCoordinator | ShellyRpcPollingCoordinator,
key: str,
) -> None:
"""Initialize Shelly entity."""
self.wrapper = wrapper
self.coordinator = coordinator
self.key = key
self._attr_should_poll = False
self._attr_device_info = {
"connections": {(device_registry.CONNECTION_NETWORK_MAC, wrapper.mac)}
"connections": {(device_registry.CONNECTION_NETWORK_MAC, coordinator.mac)}
}
self._attr_unique_id = f"{wrapper.mac}-{key}"
self._attr_name = get_rpc_entity_name(wrapper.device, key)
self._attr_unique_id = f"{coordinator.mac}-{key}"
self._attr_name = get_rpc_entity_name(coordinator.device, key)
@property
def available(self) -> bool:
"""Available."""
return self.wrapper.device.connected
return self.coordinator.device.connected
@property
def status(self) -> dict:
"""Device status by entity key."""
return cast(dict, self.wrapper.device.status[self.key])
return cast(dict, self.coordinator.device.status[self.key])
async def async_added_to_hass(self) -> None:
"""When entity is added to HASS."""
self.async_on_remove(self.wrapper.async_add_listener(self._update_callback))
self.async_on_remove(self.coordinator.async_add_listener(self._update_callback))
async def async_update(self) -> None:
"""Update entity with latest info."""
await self.wrapper.async_request_refresh()
await self.coordinator.async_request_refresh()
@callback
def _update_callback(self) -> None:
@ -382,7 +382,7 @@ class ShellyRpcEntity(entity.Entity):
)
try:
async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC):
return await self.wrapper.device.call_rpc(method, params)
return await self.coordinator.device.call_rpc(method, params)
except asyncio.TimeoutError as err:
LOGGER.error(
"Call RPC for entity %s failed, method: %s, params: %s, error: %s",
@ -391,7 +391,7 @@ class ShellyRpcEntity(entity.Entity):
params,
repr(err),
)
self.wrapper.last_update_success = False
self.coordinator.last_update_success = False
return None
@ -402,18 +402,20 @@ class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity):
def __init__(
self,
wrapper: BlockDeviceWrapper,
coordinator: ShellyBlockCoordinator,
block: Block,
attribute: str,
description: BlockEntityDescription,
) -> None:
"""Initialize sensor."""
super().__init__(wrapper, block)
super().__init__(coordinator, block)
self.attribute = attribute
self.entity_description = description
self._attr_unique_id: str = f"{super().unique_id}-{self.attribute}"
self._attr_name = get_block_entity_name(wrapper.device, block, description.name)
self._attr_name = get_block_entity_name(
coordinator.device, block, description.name
)
@property
def attribute_value(self) -> StateType:
@ -442,40 +444,42 @@ class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity):
return self.entity_description.extra_state_attributes(self.block)
class ShellyRestAttributeEntity(update_coordinator.CoordinatorEntity):
class ShellyRestAttributeEntity(CoordinatorEntity[ShellyBlockCoordinator]):
"""Class to load info from REST."""
entity_description: RestEntityDescription
def __init__(
self,
wrapper: BlockDeviceWrapper,
coordinator: ShellyBlockCoordinator,
attribute: str,
description: RestEntityDescription,
) -> None:
"""Initialize sensor."""
super().__init__(wrapper)
self.wrapper = wrapper
super().__init__(coordinator)
self.block_coordinator = coordinator
self.attribute = attribute
self.entity_description = description
self._attr_name = get_block_entity_name(wrapper.device, None, description.name)
self._attr_unique_id = f"{wrapper.mac}-{attribute}"
self._attr_name = get_block_entity_name(
coordinator.device, None, description.name
)
self._attr_unique_id = f"{coordinator.mac}-{attribute}"
self._attr_device_info = DeviceInfo(
connections={(device_registry.CONNECTION_NETWORK_MAC, wrapper.mac)}
connections={(device_registry.CONNECTION_NETWORK_MAC, coordinator.mac)}
)
self._last_value = None
@property
def available(self) -> bool:
"""Available."""
return self.wrapper.last_update_success
return self.block_coordinator.last_update_success
@property
def attribute_value(self) -> StateType:
"""Value of sensor."""
if callable(self.entity_description.value):
self._last_value = self.entity_description.value(
self.wrapper.device.status, self._last_value
self.block_coordinator.device.status, self._last_value
)
return self._last_value
@ -486,7 +490,7 @@ class ShellyRestAttributeEntity(update_coordinator.CoordinatorEntity):
return None
return self.entity_description.extra_state_attributes(
self.wrapper.device.status
self.block_coordinator.device.status
)
@ -497,18 +501,18 @@ class ShellyRpcAttributeEntity(ShellyRpcEntity, entity.Entity):
def __init__(
self,
wrapper: RpcDeviceWrapper,
coordinator: ShellyRpcCoordinator,
key: str,
attribute: str,
description: RpcEntityDescription,
) -> None:
"""Initialize sensor."""
super().__init__(wrapper, key)
super().__init__(coordinator, key)
self.attribute = attribute
self.entity_description = description
self._attr_unique_id = f"{super().unique_id}-{attribute}"
self._attr_name = get_rpc_entity_name(wrapper.device, key, description.name)
self._attr_name = get_rpc_entity_name(coordinator.device, key, description.name)
self._last_value = None
@property
@ -516,13 +520,13 @@ class ShellyRpcAttributeEntity(ShellyRpcEntity, entity.Entity):
"""Value of sensor."""
if callable(self.entity_description.value):
self._last_value = self.entity_description.value(
self.wrapper.device.status[self.key].get(
self.coordinator.device.status[self.key].get(
self.entity_description.sub_key
),
self._last_value,
)
else:
self._last_value = self.wrapper.device.status[self.key][
self._last_value = self.coordinator.device.status[self.key][
self.entity_description.sub_key
]
@ -537,7 +541,7 @@ class ShellyRpcAttributeEntity(ShellyRpcEntity, entity.Entity):
return available
return self.entity_description.available(
self.wrapper.device.status[self.key][self.entity_description.sub_key]
self.coordinator.device.status[self.key][self.entity_description.sub_key]
)
@property
@ -546,11 +550,11 @@ class ShellyRpcAttributeEntity(ShellyRpcEntity, entity.Entity):
if self.entity_description.extra_state_attributes is None:
return None
assert self.wrapper.device.shelly
assert self.coordinator.device.shelly
return self.entity_description.extra_state_attributes(
self.wrapper.device.status[self.key][self.entity_description.sub_key],
self.wrapper.device.shelly,
self.coordinator.device.status[self.key][self.entity_description.sub_key],
self.coordinator.device.shelly,
)
@ -560,7 +564,7 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti
# pylint: disable=super-init-not-called
def __init__(
self,
wrapper: BlockDeviceWrapper,
coordinator: ShellyBlockCoordinator,
block: Block | None,
attribute: str,
description: BlockEntityDescription,
@ -570,20 +574,22 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti
"""Initialize the sleeping sensor."""
self.sensors = sensors
self.last_state: StateType = None
self.wrapper = wrapper
self.coordinator = coordinator
self.attribute = attribute
self.block: Block | None = block # type: ignore[assignment]
self.entity_description = description
self._attr_should_poll = False
self._attr_device_info = DeviceInfo(
connections={(device_registry.CONNECTION_NETWORK_MAC, wrapper.mac)}
connections={(device_registry.CONNECTION_NETWORK_MAC, coordinator.mac)}
)
if block is not None:
self._attr_unique_id = f"{self.wrapper.mac}-{block.description}-{attribute}"
self._attr_unique_id = (
f"{self.coordinator.mac}-{block.description}-{attribute}"
)
self._attr_name = get_block_entity_name(
self.wrapper.device, block, self.entity_description.name
self.coordinator.device, block, self.entity_description.name
)
elif entry is not None:
self._attr_unique_id = entry.unique_id
@ -603,7 +609,7 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti
"""Handle device update."""
if (
self.block is not None
or not self.wrapper.device.initialized
or not self.coordinator.device.initialized
or self.sensors is None
):
super()._update_callback()
@ -611,9 +617,9 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti
_, entity_block, entity_sensor = self._attr_unique_id.split("-")
assert self.wrapper.device.blocks
assert self.coordinator.device.blocks
for block in self.wrapper.device.blocks:
for block in self.coordinator.device.blocks:
if block.description != entity_block:
continue

View File

@ -25,7 +25,6 @@ from homeassistant.util.color import (
color_temperature_mired_to_kelvin,
)
from . import BlockDeviceWrapper, RpcDeviceWrapper
from .const import (
BLOCK,
DATA_CONFIG_ENTRY,
@ -44,6 +43,7 @@ from .const import (
SHBLB_1_RGB_EFFECTS,
STANDARD_RGB_EFFECTS,
)
from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator
from .entity import ShellyBlockEntity, ShellyRpcEntity
from .utils import (
async_remove_shelly_entity,
@ -77,28 +77,28 @@ def async_setup_block_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up entities for block device."""
wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][BLOCK]
coordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][BLOCK]
blocks = []
assert wrapper.device.blocks
for block in wrapper.device.blocks:
assert coordinator.device.blocks
for block in coordinator.device.blocks:
if block.type == "light":
blocks.append(block)
elif block.type == "relay":
if not is_block_channel_type_light(
wrapper.device.settings, int(block.channel)
coordinator.device.settings, int(block.channel)
):
continue
blocks.append(block)
assert wrapper.device.shelly
unique_id = f"{wrapper.mac}-{block.type}_{block.channel}"
assert coordinator.device.shelly
unique_id = f"{coordinator.mac}-{block.type}_{block.channel}"
async_remove_shelly_entity(hass, "switch", unique_id)
if not blocks:
return
async_add_entities(BlockShellyLight(wrapper, block) for block in blocks)
async_add_entities(BlockShellyLight(coordinator, block) for block in blocks)
@callback
@ -108,22 +108,22 @@ def async_setup_rpc_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up entities for RPC device."""
wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][RPC]
switch_key_ids = get_rpc_key_ids(wrapper.device.status, "switch")
coordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][RPC]
switch_key_ids = get_rpc_key_ids(coordinator.device.status, "switch")
switch_ids = []
for id_ in switch_key_ids:
if not is_rpc_channel_type_light(wrapper.device.config, id_):
if not is_rpc_channel_type_light(coordinator.device.config, id_):
continue
switch_ids.append(id_)
unique_id = f"{wrapper.mac}-switch:{id_}"
unique_id = f"{coordinator.mac}-switch:{id_}"
async_remove_shelly_entity(hass, "switch", unique_id)
if not switch_ids:
return
async_add_entities(RpcShellyLight(wrapper, id_) for id_ in switch_ids)
async_add_entities(RpcShellyLight(coordinator, id_) for id_ in switch_ids)
class BlockShellyLight(ShellyBlockEntity, LightEntity):
@ -131,9 +131,9 @@ class BlockShellyLight(ShellyBlockEntity, LightEntity):
_attr_supported_color_modes: set[str]
def __init__(self, wrapper: BlockDeviceWrapper, block: Block) -> None:
def __init__(self, coordinator: ShellyBlockCoordinator, block: Block) -> None:
"""Initialize light."""
super().__init__(wrapper, block)
super().__init__(coordinator, block)
self.control_result: dict[str, Any] | None = None
self._attr_supported_color_modes = set()
self._attr_min_mireds = MIRED_MIN_VALUE
@ -144,7 +144,7 @@ class BlockShellyLight(ShellyBlockEntity, LightEntity):
if hasattr(block, "red") and hasattr(block, "green") and hasattr(block, "blue"):
self._attr_max_mireds = MIRED_MAX_VALUE_COLOR
self._min_kelvin = KELVIN_MIN_VALUE_COLOR
if wrapper.model in RGBW_MODELS:
if coordinator.model in RGBW_MODELS:
self._attr_supported_color_modes.add(ColorMode.RGBW)
else:
self._attr_supported_color_modes.add(ColorMode.RGB)
@ -161,8 +161,8 @@ class BlockShellyLight(ShellyBlockEntity, LightEntity):
if hasattr(block, "effect"):
self._attr_supported_features |= LightEntityFeature.EFFECT
if wrapper.model in MODELS_SUPPORTING_LIGHT_TRANSITION:
match = FIRMWARE_PATTERN.search(wrapper.device.settings.get("fw", ""))
if coordinator.model in MODELS_SUPPORTING_LIGHT_TRANSITION:
match = FIRMWARE_PATTERN.search(coordinator.device.settings.get("fw", ""))
if (
match is not None
and int(match[0]) >= LIGHT_TRANSITION_MIN_FIRMWARE_DATE
@ -215,7 +215,7 @@ class BlockShellyLight(ShellyBlockEntity, LightEntity):
def color_mode(self) -> ColorMode:
"""Return the color mode of the light."""
if self.mode == "color":
if self.wrapper.model in RGBW_MODELS:
if self.coordinator.model in RGBW_MODELS:
return ColorMode.RGBW
return ColorMode.RGB
@ -268,7 +268,7 @@ class BlockShellyLight(ShellyBlockEntity, LightEntity):
if not self.supported_features & LightEntityFeature.EFFECT:
return None
if self.wrapper.model == "SHBLB-1":
if self.coordinator.model == "SHBLB-1":
return list(SHBLB_1_RGB_EFFECTS.values())
return list(STANDARD_RGB_EFFECTS.values())
@ -284,7 +284,7 @@ class BlockShellyLight(ShellyBlockEntity, LightEntity):
else:
effect_index = self.block.effect
if self.wrapper.model == "SHBLB-1":
if self.coordinator.model == "SHBLB-1":
return SHBLB_1_RGB_EFFECTS[effect_index]
return STANDARD_RGB_EFFECTS[effect_index]
@ -334,7 +334,7 @@ class BlockShellyLight(ShellyBlockEntity, LightEntity):
if ATTR_EFFECT in kwargs and ATTR_COLOR_TEMP not in kwargs:
# Color effect change - used only in color mode, switch device mode to color
set_mode = "color"
if self.wrapper.model == "SHBLB-1":
if self.coordinator.model == "SHBLB-1":
effect_dict = SHBLB_1_RGB_EFFECTS
else:
effect_dict = STANDARD_RGB_EFFECTS
@ -346,13 +346,13 @@ class BlockShellyLight(ShellyBlockEntity, LightEntity):
LOGGER.error(
"Effect '%s' not supported by device %s",
kwargs[ATTR_EFFECT],
self.wrapper.model,
self.coordinator.model,
)
if (
set_mode
and set_mode != self.mode
and self.wrapper.model in DUAL_MODE_LIGHT_MODELS
and self.coordinator.model in DUAL_MODE_LIGHT_MODELS
):
params["mode"] = set_mode
@ -385,15 +385,15 @@ class RpcShellyLight(ShellyRpcEntity, LightEntity):
_attr_color_mode = ColorMode.ONOFF
_attr_supported_color_modes = {ColorMode.ONOFF}
def __init__(self, wrapper: RpcDeviceWrapper, id_: int) -> None:
def __init__(self, coordinator: ShellyRpcCoordinator, id_: int) -> None:
"""Initialize light."""
super().__init__(wrapper, f"switch:{id_}")
super().__init__(coordinator, f"switch:{id_}")
self._id = id_
@property
def is_on(self) -> bool:
"""If light is on."""
return bool(self.wrapper.device.status[self.key]["output"])
return bool(self.coordinator.device.status[self.key]["output"])
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on light."""

View File

@ -8,7 +8,7 @@ from homeassistant.const import ATTR_DEVICE_ID
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.typing import EventType
from . import get_block_device_wrapper, get_rpc_device_wrapper
from . import get_block_device_coordinator, get_rpc_device_coordinator
from .const import (
ATTR_CHANNEL,
ATTR_CLICK_TYPE,
@ -37,15 +37,15 @@ def async_describe_events(
input_name = f"{event.data[ATTR_DEVICE]} channel {channel}"
if click_type in RPC_INPUTS_EVENTS_TYPES:
rpc_wrapper = get_rpc_device_wrapper(hass, device_id)
if rpc_wrapper and rpc_wrapper.device.initialized:
rpc_coordinator = get_rpc_device_coordinator(hass, device_id)
if rpc_coordinator and rpc_coordinator.device.initialized:
key = f"input:{channel-1}"
input_name = get_rpc_entity_name(rpc_wrapper.device, key)
input_name = get_rpc_entity_name(rpc_coordinator.device, key)
elif click_type in BLOCK_INPUTS_EVENTS_TYPES:
block_wrapper = get_block_device_wrapper(hass, device_id)
if block_wrapper and block_wrapper.device.initialized:
device_name = get_block_device_name(block_wrapper.device)
block_coordinator = get_block_device_coordinator(hass, device_id)
if block_coordinator and block_coordinator.device.initialized:
device_name = get_block_device_name(block_coordinator.device)
input_name = f"{device_name} channel {channel}"
return {

View File

@ -119,7 +119,7 @@ class BlockSleepingNumber(ShellySleepingBlockAttributeEntity, NumberEntity):
LOGGER.debug("Setting state for entity %s, state: %s", self.name, params)
try:
async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC):
return await self.wrapper.device.http_request("get", path, params)
return await self.coordinator.device.http_request("get", path, params)
except (asyncio.TimeoutError, OSError) as err:
LOGGER.error(
"Setting state for entity %s failed, state: %s, error: %s",

View File

@ -32,8 +32,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.entity_registry import RegistryEntry
from homeassistant.helpers.typing import StateType
from . import BlockDeviceWrapper
from .const import CONF_SLEEP_PERIOD, SHAIR_MAX_WORK_HOURS
from .coordinator import ShellyBlockCoordinator
from .entity import (
BlockEntityDescription,
RestEntityDescription,
@ -355,7 +355,7 @@ RPC_SENSORS: Final = {
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
use_polling_wrapper=True,
use_polling_coordinator=True,
),
"temperature_0": RpcSensorDescription(
key="temperature:0",
@ -376,7 +376,7 @@ RPC_SENSORS: Final = {
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
use_polling_wrapper=True,
use_polling_coordinator=True,
),
"uptime": RpcSensorDescription(
key="sys",
@ -386,7 +386,7 @@ RPC_SENSORS: Final = {
device_class=SensorDeviceClass.TIMESTAMP,
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
use_polling_wrapper=True,
use_polling_coordinator=True,
),
"humidity_0": RpcSensorDescription(
key="humidity:0",
@ -465,13 +465,13 @@ class BlockSensor(ShellyBlockAttributeEntity, SensorEntity):
def __init__(
self,
wrapper: BlockDeviceWrapper,
coordinator: ShellyBlockCoordinator,
block: Block,
attribute: str,
description: BlockSensorDescription,
) -> None:
"""Initialize sensor."""
super().__init__(wrapper, block, attribute, description)
super().__init__(coordinator, block, attribute, description)
self._attr_native_unit_of_measurement = description.native_unit_of_measurement
if unit_fn := description.unit_fn:
@ -512,7 +512,7 @@ class BlockSleepingSensor(ShellySleepingBlockAttributeEntity, SensorEntity):
def __init__(
self,
wrapper: BlockDeviceWrapper,
coordinator: ShellyBlockCoordinator,
block: Block | None,
attribute: str,
description: BlockSensorDescription,
@ -520,7 +520,7 @@ class BlockSleepingSensor(ShellySleepingBlockAttributeEntity, SensorEntity):
sensors: Mapping[tuple[str, str], BlockSensorDescription] | None = None,
) -> None:
"""Initialize the sleeping sensor."""
super().__init__(wrapper, block, attribute, description, entry, sensors)
super().__init__(coordinator, block, attribute, description, entry, sensors)
self._attr_native_unit_of_measurement = description.native_unit_of_measurement
if block and (unit_fn := description.unit_fn):

View File

@ -10,8 +10,8 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import BlockDeviceWrapper, RpcDeviceWrapper
from .const import BLOCK, DATA_CONFIG_ENTRY, DOMAIN, RPC
from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator
from .entity import ShellyBlockEntity, ShellyRpcEntity
from .utils import (
async_remove_shelly_entity,
@ -41,31 +41,31 @@ def async_setup_block_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up entities for block device."""
wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][BLOCK]
coordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][BLOCK]
# In roller mode the relay blocks exist but do not contain required info
if (
wrapper.model in ["SHSW-21", "SHSW-25"]
and wrapper.device.settings["mode"] != "relay"
coordinator.model in ["SHSW-21", "SHSW-25"]
and coordinator.device.settings["mode"] != "relay"
):
return
relay_blocks = []
assert wrapper.device.blocks
for block in wrapper.device.blocks:
assert coordinator.device.blocks
for block in coordinator.device.blocks:
if block.type != "relay" or is_block_channel_type_light(
wrapper.device.settings, int(block.channel)
coordinator.device.settings, int(block.channel)
):
continue
relay_blocks.append(block)
unique_id = f"{wrapper.mac}-{block.type}_{block.channel}"
unique_id = f"{coordinator.mac}-{block.type}_{block.channel}"
async_remove_shelly_entity(hass, "light", unique_id)
if not relay_blocks:
return
async_add_entities(BlockRelaySwitch(wrapper, block) for block in relay_blocks)
async_add_entities(BlockRelaySwitch(coordinator, block) for block in relay_blocks)
@callback
@ -75,31 +75,31 @@ def async_setup_rpc_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up entities for RPC device."""
wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][RPC]
coordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][RPC]
switch_key_ids = get_rpc_key_ids(wrapper.device.status, "switch")
switch_key_ids = get_rpc_key_ids(coordinator.device.status, "switch")
switch_ids = []
for id_ in switch_key_ids:
if is_rpc_channel_type_light(wrapper.device.config, id_):
if is_rpc_channel_type_light(coordinator.device.config, id_):
continue
switch_ids.append(id_)
unique_id = f"{wrapper.mac}-switch:{id_}"
unique_id = f"{coordinator.mac}-switch:{id_}"
async_remove_shelly_entity(hass, "light", unique_id)
if not switch_ids:
return
async_add_entities(RpcRelaySwitch(wrapper, id_) for id_ in switch_ids)
async_add_entities(RpcRelaySwitch(coordinator, id_) for id_ in switch_ids)
class BlockRelaySwitch(ShellyBlockEntity, SwitchEntity):
"""Entity that controls a relay on Block based Shelly devices."""
def __init__(self, wrapper: BlockDeviceWrapper, block: Block) -> None:
def __init__(self, coordinator: ShellyBlockCoordinator, block: Block) -> None:
"""Initialize relay switch."""
super().__init__(wrapper, block)
super().__init__(coordinator, block)
self.control_result: dict[str, Any] | None = None
@property
@ -130,15 +130,15 @@ class BlockRelaySwitch(ShellyBlockEntity, SwitchEntity):
class RpcRelaySwitch(ShellyRpcEntity, SwitchEntity):
"""Entity that controls a relay on RPC based Shelly devices."""
def __init__(self, wrapper: RpcDeviceWrapper, id_: int) -> None:
def __init__(self, coordinator: ShellyRpcCoordinator, id_: int) -> None:
"""Initialize relay switch."""
super().__init__(wrapper, f"switch:{id_}")
super().__init__(coordinator, f"switch:{id_}")
self._id = id_
@property
def is_on(self) -> bool:
"""If switch is on."""
return bool(self.wrapper.device.status[self.key]["output"])
return bool(self.coordinator.device.status[self.key]["output"])
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on relay."""

View File

@ -17,8 +17,8 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import BlockDeviceWrapper, RpcDeviceWrapper
from .const import BLOCK, CONF_SLEEP_PERIOD, DATA_CONFIG_ENTRY, DOMAIN
from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator
from .entity import (
RestEntityDescription,
RpcEntityDescription,
@ -67,7 +67,7 @@ REST_UPDATES: Final = {
name="Firmware Update",
key="fwupdate",
latest_version=lambda status: status["update"]["new_version"],
install=lambda wrapper: wrapper.async_trigger_ota_update(),
install=lambda coordinator: coordinator.async_trigger_ota_update(),
device_class=UpdateDeviceClass.FIRMWARE,
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
@ -76,7 +76,7 @@ REST_UPDATES: Final = {
name="Beta Firmware Update",
key="fwupdate",
latest_version=lambda status: status["update"].get("beta_version"),
install=lambda wrapper: wrapper.async_trigger_ota_update(beta=True),
install=lambda coordinator: coordinator.async_trigger_ota_update(beta=True),
device_class=UpdateDeviceClass.FIRMWARE,
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
@ -91,7 +91,7 @@ RPC_UPDATES: Final = {
latest_version=lambda status: status.get("stable", {"version": None})[
"version"
],
install=lambda wrapper: wrapper.async_trigger_ota_update(),
install=lambda coordinator: coordinator.async_trigger_ota_update(),
device_class=UpdateDeviceClass.FIRMWARE,
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
@ -101,7 +101,7 @@ RPC_UPDATES: Final = {
key="sys",
sub_key="available_updates",
latest_version=lambda status: status.get("beta", {"version": None})["version"],
install=lambda wrapper: wrapper.async_trigger_ota_update(beta=True),
install=lambda coordinator: coordinator.async_trigger_ota_update(beta=True),
device_class=UpdateDeviceClass.FIRMWARE,
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
@ -140,18 +140,18 @@ class RestUpdateEntity(ShellyRestAttributeEntity, UpdateEntity):
def __init__(
self,
wrapper: BlockDeviceWrapper,
block_coordinator: ShellyBlockCoordinator,
attribute: str,
description: RestEntityDescription,
) -> None:
"""Initialize update entity."""
super().__init__(wrapper, attribute, description)
super().__init__(block_coordinator, attribute, description)
self._in_progress_old_version: str | None = None
@property
def installed_version(self) -> str | None:
"""Version currently in use."""
version = self.wrapper.device.status["update"]["old_version"]
version = self.block_coordinator.device.status["update"]["old_version"]
if version is None:
return None
@ -161,7 +161,7 @@ class RestUpdateEntity(ShellyRestAttributeEntity, UpdateEntity):
def latest_version(self) -> str | None:
"""Latest version available for install."""
new_version = self.entity_description.latest_version(
self.wrapper.device.status,
self.block_coordinator.device.status,
)
if new_version not in (None, ""):
return cast(str, new_version)
@ -177,12 +177,12 @@ class RestUpdateEntity(ShellyRestAttributeEntity, UpdateEntity):
self, version: str | None, backup: bool, **kwargs: Any
) -> None:
"""Install the latest firmware version."""
config_entry = self.wrapper.entry
block_wrapper = self.hass.data[DOMAIN][DATA_CONFIG_ENTRY][
config_entry = self.block_coordinator.entry
block_coordinator = self.hass.data[DOMAIN][DATA_CONFIG_ENTRY][
config_entry.entry_id
].get(BLOCK)
self._in_progress_old_version = self.installed_version
await self.entity_description.install(block_wrapper)
await self.entity_description.install(block_coordinator)
class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity):
@ -195,28 +195,28 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity):
def __init__(
self,
wrapper: RpcDeviceWrapper,
coordinator: ShellyRpcCoordinator,
key: str,
attribute: str,
description: RpcEntityDescription,
) -> None:
"""Initialize update entity."""
super().__init__(wrapper, key, attribute, description)
super().__init__(coordinator, key, attribute, description)
self._in_progress_old_version: str | None = None
@property
def installed_version(self) -> str | None:
"""Version currently in use."""
if self.wrapper.device.shelly is None:
if self.coordinator.device.shelly is None:
return None
return cast(str, self.wrapper.device.shelly["ver"])
return cast(str, self.coordinator.device.shelly["ver"])
@property
def latest_version(self) -> str | None:
"""Latest version available for install."""
new_version = self.entity_description.latest_version(
self.wrapper.device.status[self.key][self.entity_description.sub_key],
self.coordinator.device.status[self.key][self.entity_description.sub_key],
)
if new_version is not None:
return cast(str, new_version)
@ -233,4 +233,4 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity):
) -> None:
"""Install the latest firmware version."""
self._in_progress_old_version = self.installed_version
await self.entity_description.install(self.wrapper)
await self.entity_description.install(self.coordinator)