Refactor Shelly wrapper to coordinator (#79628)

This commit is contained in:
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, RPC_POLL,
) )
from .coordinator import ( from .coordinator import (
BlockDeviceWrapper, ShellyBlockCoordinator,
RpcDeviceWrapper, ShellyRestCoordinator,
RpcPollingWrapper, ShellyRpcCoordinator,
ShellyDeviceRestWrapper, ShellyRpcPollingCoordinator,
) )
from .utils import get_block_device_sleep_period, get_coap_context, get_device_entry_gen 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 hass: HomeAssistant, entry: ConfigEntry, device: BlockDevice
) -> None: ) -> None:
"""Set up a block based device that is online.""" """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 BLOCK
] = BlockDeviceWrapper(hass, entry, device) ] = ShellyBlockCoordinator(hass, entry, device)
device_wrapper.async_setup() block_coordinator.async_setup()
platforms = BLOCK_SLEEPING_PLATFORMS platforms = BLOCK_SLEEPING_PLATFORMS
if not entry.data.get(CONF_SLEEP_PERIOD): if not entry.data.get(CONF_SLEEP_PERIOD):
hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][ hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][
REST REST
] = ShellyDeviceRestWrapper(hass, device, entry) ] = ShellyRestCoordinator(hass, device, entry)
platforms = BLOCK_PLATFORMS platforms = BLOCK_PLATFORMS
hass.config_entries.async_setup_platforms(entry, 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: except (AuthRequired, InvalidAuthError) as err:
raise ConfigEntryAuthFailed from 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 RPC
] = RpcDeviceWrapper(hass, entry, device) ] = ShellyRpcCoordinator(hass, entry, device)
device_wrapper.async_setup() rpc_coordinator.async_setup()
hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][RPC_POLL] = RpcPollingWrapper( hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][
hass, entry, device RPC_POLL
) ] = ShellyRpcPollingCoordinator(hass, entry, device)
hass.config_entries.async_setup_platforms(entry, RPC_PLATFORMS) 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) device = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id].get(DEVICE)
if device is not None: 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() device.shutdown()
return True return True
@ -283,10 +283,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return unload_ok return unload_ok
def get_block_device_wrapper( def get_block_device_coordinator(
hass: HomeAssistant, device_id: str hass: HomeAssistant, device_id: str
) -> BlockDeviceWrapper | None: ) -> ShellyBlockCoordinator | None:
"""Get a Shelly block device wrapper for the given device id.""" """Get a Shelly block device coordinator for the given device id."""
if not hass.data.get(DOMAIN): if not hass.data.get(DOMAIN):
return None return None
@ -296,16 +296,18 @@ def get_block_device_wrapper(
if not hass.data[DOMAIN][DATA_CONFIG_ENTRY].get(config_entry): if not hass.data[DOMAIN][DATA_CONFIG_ENTRY].get(config_entry):
continue continue
if wrapper := hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry].get(BLOCK): if coordinator := hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry].get(
return cast(BlockDeviceWrapper, wrapper) BLOCK
):
return cast(ShellyBlockCoordinator, coordinator)
return None return None
def get_rpc_device_wrapper( def get_rpc_device_coordinator(
hass: HomeAssistant, device_id: str hass: HomeAssistant, device_id: str
) -> RpcDeviceWrapper | None: ) -> ShellyRpcCoordinator | None:
"""Get a Shelly RPC device wrapper for the given device id.""" """Get a Shelly RPC device coordinator for the given device id."""
if not hass.data.get(DOMAIN): if not hass.data.get(DOMAIN):
return None return None
@ -315,7 +317,9 @@ def get_rpc_device_wrapper(
if not hass.data[DOMAIN][DATA_CONFIG_ENTRY].get(config_entry): if not hass.data[DOMAIN][DATA_CONFIG_ENTRY].get(config_entry):
continue continue
if wrapper := hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry].get(RPC): if coordinator := hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry].get(
return cast(RpcDeviceWrapper, wrapper) RPC
):
return cast(ShellyRpcCoordinator, coordinator)
return None 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.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity import DeviceInfo, EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import slugify from homeassistant.util import slugify
from . import BlockDeviceWrapper, RpcDeviceWrapper
from .const import BLOCK, DATA_CONFIG_ENTRY, DOMAIN, RPC, SHELLY_GAS_MODELS 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 from .utils import get_block_device_name, get_device_entry_gen, get_rpc_device_name
@ -42,31 +43,31 @@ BUTTONS: Final = [
name="Reboot", name="Reboot",
device_class=ButtonDeviceClass.RESTART, device_class=ButtonDeviceClass.RESTART,
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
press_action=lambda wrapper: wrapper.device.trigger_reboot(), press_action=lambda coordinator: coordinator.device.trigger_reboot(),
), ),
ShellyButtonDescription( ShellyButtonDescription(
key="self_test", key="self_test",
name="Self Test", name="Self Test",
icon="mdi:progress-wrench", icon="mdi:progress-wrench",
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
press_action=lambda wrapper: wrapper.device.trigger_shelly_gas_self_test(), press_action=lambda coordinator: coordinator.device.trigger_shelly_gas_self_test(),
supported=lambda wrapper: wrapper.device.model in SHELLY_GAS_MODELS, supported=lambda coordinator: coordinator.device.model in SHELLY_GAS_MODELS,
), ),
ShellyButtonDescription( ShellyButtonDescription(
key="mute", key="mute",
name="Mute", name="Mute",
icon="mdi:volume-mute", icon="mdi:volume-mute",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
press_action=lambda wrapper: wrapper.device.trigger_shelly_gas_mute(), press_action=lambda coordinator: coordinator.device.trigger_shelly_gas_mute(),
supported=lambda wrapper: wrapper.device.model in SHELLY_GAS_MODELS, supported=lambda coordinator: coordinator.device.model in SHELLY_GAS_MODELS,
), ),
ShellyButtonDescription( ShellyButtonDescription(
key="unmute", key="unmute",
name="Unmute", name="Unmute",
icon="mdi:volume-high", icon="mdi:volume-high",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
press_action=lambda wrapper: wrapper.device.trigger_shelly_gas_unmute(), press_action=lambda coordinator: coordinator.device.trigger_shelly_gas_unmute(),
supported=lambda wrapper: wrapper.device.model in SHELLY_GAS_MODELS, supported=lambda coordinator: coordinator.device.model in SHELLY_GAS_MODELS,
), ),
] ]
@ -77,54 +78,54 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set buttons for device.""" """Set buttons for device."""
wrapper: RpcDeviceWrapper | BlockDeviceWrapper | None = None coordinator: ShellyRpcCoordinator | ShellyBlockCoordinator | None = None
if get_device_entry_gen(config_entry) == 2: 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 config_entry.entry_id
].get(RPC): ].get(RPC):
wrapper = cast(RpcDeviceWrapper, rpc_wrapper) coordinator = cast(ShellyRpcCoordinator, rpc_coordinator)
else: else:
if block_wrapper := hass.data[DOMAIN][DATA_CONFIG_ENTRY][ if block_coordinator := hass.data[DOMAIN][DATA_CONFIG_ENTRY][
config_entry.entry_id config_entry.entry_id
].get(BLOCK): ].get(BLOCK):
wrapper = cast(BlockDeviceWrapper, block_wrapper) coordinator = cast(ShellyBlockCoordinator, block_coordinator)
if wrapper is not None: if coordinator is not None:
entities = [] entities = []
for button in BUTTONS: for button in BUTTONS:
if not button.supported(wrapper): if not button.supported(coordinator):
continue continue
entities.append(ShellyButton(wrapper, button)) entities.append(ShellyButton(coordinator, button))
async_add_entities(entities) async_add_entities(entities)
class ShellyButton(ButtonEntity): class ShellyButton(CoordinatorEntity, ButtonEntity):
"""Defines a Shelly base button.""" """Defines a Shelly base button."""
entity_description: ShellyButtonDescription entity_description: ShellyButtonDescription
def __init__( def __init__(
self, self,
wrapper: RpcDeviceWrapper | BlockDeviceWrapper, coordinator: ShellyRpcCoordinator | ShellyBlockCoordinator,
description: ShellyButtonDescription, description: ShellyButtonDescription,
) -> None: ) -> None:
"""Initialize Shelly button.""" """Initialize Shelly button."""
super().__init__(coordinator)
self.entity_description = description self.entity_description = description
self.wrapper = wrapper
if isinstance(wrapper, RpcDeviceWrapper): if isinstance(coordinator, ShellyRpcCoordinator):
device_name = get_rpc_device_name(wrapper.device) device_name = get_rpc_device_name(coordinator.device)
else: 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_name = f"{device_name} {description.name}"
self._attr_unique_id = slugify(self._attr_name) self._attr_unique_id = slugify(self._attr_name)
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
connections={(CONNECTION_NETWORK_MAC, wrapper.mac)} connections={(CONNECTION_NETWORK_MAC, coordinator.mac)}
) )
async def async_press(self) -> None: async def async_press(self) -> None:
"""Triggers the Shelly button press service.""" """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.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
from homeassistant.core import HomeAssistant, State, callback 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 import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import BlockDeviceWrapper
from .const import ( from .const import (
AIOSHELLY_DEVICE_TIMEOUT_SEC, AIOSHELLY_DEVICE_TIMEOUT_SEC,
BLOCK, BLOCK,
@ -34,6 +34,7 @@ from .const import (
LOGGER, LOGGER,
SHTRV_01_TEMPERATURE_SETTINGS, SHTRV_01_TEMPERATURE_SETTINGS,
) )
from .coordinator import ShellyBlockCoordinator
from .utils import get_device_entry_gen from .utils import get_device_entry_gen
@ -47,37 +48,41 @@ async def async_setup_entry(
if get_device_entry_gen(config_entry) == 2: if get_device_entry_gen(config_entry) == 2:
return return
wrapper: BlockDeviceWrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][ coordinator: ShellyBlockCoordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][
config_entry.entry_id config_entry.entry_id
][BLOCK] ][BLOCK]
if wrapper.device.initialized: if coordinator.device.initialized:
async_setup_climate_entities(async_add_entities, wrapper) async_setup_climate_entities(async_add_entities, coordinator)
else: else:
async_restore_climate_entities(hass, config_entry, async_add_entities, wrapper) async_restore_climate_entities(
hass, config_entry, async_add_entities, coordinator
)
@callback @callback
def async_setup_climate_entities( def async_setup_climate_entities(
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
wrapper: BlockDeviceWrapper, coordinator: ShellyBlockCoordinator,
) -> None: ) -> None:
"""Set up online climate devices.""" """Set up online climate devices."""
device_block: Block | None = None device_block: Block | None = None
sensor_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": if block.type == "device":
device_block = block device_block = block
if hasattr(block, "targetTemp"): if hasattr(block, "targetTemp"):
sensor_block = block sensor_block = block
if sensor_block and device_block: if sensor_block and device_block:
LOGGER.debug("Setup online climate device %s", wrapper.name) LOGGER.debug("Setup online climate device %s", coordinator.name)
async_add_entities([BlockSleepingClimate(wrapper, sensor_block, device_block)]) async_add_entities(
[BlockSleepingClimate(coordinator, sensor_block, device_block)]
)
@callback @callback
@ -85,7 +90,7 @@ def async_restore_climate_entities(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
wrapper: BlockDeviceWrapper, coordinator: ShellyBlockCoordinator,
) -> None: ) -> None:
"""Restore sleeping climate devices.""" """Restore sleeping climate devices."""
@ -99,16 +104,14 @@ def async_restore_climate_entities(
if entry.domain != CLIMATE_DOMAIN: if entry.domain != CLIMATE_DOMAIN:
continue 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) 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 break
class BlockSleepingClimate( class BlockSleepingClimate(
update_coordinator.CoordinatorEntity, CoordinatorEntity[ShellyBlockCoordinator], RestoreEntity, ClimateEntity
RestoreEntity,
ClimateEntity,
): ):
"""Representation of a Shelly climate device.""" """Representation of a Shelly climate device."""
@ -124,16 +127,14 @@ class BlockSleepingClimate(
def __init__( def __init__(
self, self,
wrapper: BlockDeviceWrapper, coordinator: ShellyBlockCoordinator,
sensor_block: Block | None, sensor_block: Block | None,
device_block: Block | None, device_block: Block | None,
entry: entity_registry.RegistryEntry | None = None, entry: entity_registry.RegistryEntry | None = None,
) -> None: ) -> None:
"""Initialize climate.""" """Initialize climate."""
super().__init__(coordinator)
super().__init__(wrapper)
self.wrapper = wrapper
self.block: Block | None = sensor_block self.block: Block | None = sensor_block
self.control_result: dict[str, Any] | None = None self.control_result: dict[str, Any] | None = None
self.device_block: Block | None = device_block self.device_block: Block | None = device_block
@ -142,11 +143,11 @@ class BlockSleepingClimate(
self._preset_modes: list[str] = [] self._preset_modes: list[str] = []
if self.block is not None and self.device_block is not None: 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 assert self.block.channel
self._preset_modes = [ self._preset_modes = [
PRESET_NONE, PRESET_NONE,
*wrapper.device.settings["thermostats"][int(self.block.channel)][ *coordinator.device.settings["thermostats"][int(self.block.channel)][
"schedule_profile_names" "schedule_profile_names"
], ],
] ]
@ -163,7 +164,7 @@ class BlockSleepingClimate(
@property @property
def name(self) -> str: def name(self) -> str:
"""Name of entity.""" """Name of entity."""
return self.wrapper.name return self.coordinator.name
@property @property
def target_temperature(self) -> float | None: def target_temperature(self) -> float | None:
@ -184,7 +185,7 @@ class BlockSleepingClimate(
"""Device availability.""" """Device availability."""
if self.device_block is not None: if self.device_block is not None:
return not cast(bool, self.device_block.valveError) return not cast(bool, self.device_block.valveError)
return self.wrapper.last_update_success return self.coordinator.last_update_success
@property @property
def hvac_mode(self) -> HVACMode: def hvac_mode(self) -> HVACMode:
@ -229,7 +230,9 @@ class BlockSleepingClimate(
def device_info(self) -> DeviceInfo: def device_info(self) -> DeviceInfo:
"""Device info.""" """Device info."""
return { 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: def _check_is_off(self) -> bool:
@ -244,7 +247,7 @@ class BlockSleepingClimate(
LOGGER.debug("Setting state for entity %s, state: %s", self.name, kwargs) LOGGER.debug("Setting state for entity %s, state: %s", self.name, kwargs)
try: try:
async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): 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 "get", f"thermostat/{self._channel}", kwargs
) )
except (asyncio.TimeoutError, OSError) as err: except (asyncio.TimeoutError, OSError) as err:
@ -254,7 +257,7 @@ class BlockSleepingClimate(
kwargs, kwargs,
repr(err), repr(err),
) )
self.wrapper.last_update_success = False self.coordinator.last_update_success = False
return None return None
async def async_set_temperature(self, **kwargs: Any) -> None: async def async_set_temperature(self, **kwargs: Any) -> None:
@ -302,13 +305,13 @@ class BlockSleepingClimate(
@callback @callback
def _handle_coordinator_update(self) -> None: def _handle_coordinator_update(self) -> None:
"""Handle device update.""" """Handle device update."""
if not self.wrapper.device.initialized: if not self.coordinator.device.initialized:
self.async_write_ha_state() self.async_write_ha_state()
return 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": if block.type == "device":
self.device_block = block self.device_block = block
if hasattr(block, "targetTemp"): if hasattr(block, "targetTemp"):
@ -322,11 +325,11 @@ class BlockSleepingClimate(
try: try:
self._preset_modes = [ self._preset_modes = [
PRESET_NONE, PRESET_NONE,
*self.wrapper.device.settings["thermostats"][ *self.coordinator.device.settings["thermostats"][
int(self.block.channel) int(self.block.channel)
]["schedule_profile_names"], ]["schedule_profile_names"],
] ]
except AuthRequired: except AuthRequired:
self.wrapper.entry.async_start_reauth(self.hass) self.coordinator.entry.async_start_reauth(self.hass)
else: else:
self.async_write_ha_state() self.async_write_ha_state()

View file

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

View file

@ -15,8 +15,8 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import BlockDeviceWrapper, RpcDeviceWrapper
from .const import BLOCK, DATA_CONFIG_ENTRY, DOMAIN, RPC from .const import BLOCK, DATA_CONFIG_ENTRY, DOMAIN, RPC
from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator
from .entity import ShellyBlockEntity, ShellyRpcEntity from .entity import ShellyBlockEntity, ShellyRpcEntity
from .utils import get_device_entry_gen, get_rpc_key_ids from .utils import get_device_entry_gen, get_rpc_key_ids
@ -40,13 +40,13 @@ def async_setup_block_entry(
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up cover for device.""" """Set up cover for 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 = [block for block in wrapper.device.blocks if block.type == "roller"] blocks = [block for block in coordinator.device.blocks if block.type == "roller"]
if not blocks: if not blocks:
return return
async_add_entities(BlockShellyCover(wrapper, block) for block in blocks) async_add_entities(BlockShellyCover(coordinator, block) for block in blocks)
@callback @callback
@ -56,14 +56,14 @@ def async_setup_rpc_entry(
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up entities for RPC device.""" """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: if not cover_key_ids:
return 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): class BlockShellyCover(ShellyBlockEntity, CoverEntity):
@ -71,14 +71,14 @@ class BlockShellyCover(ShellyBlockEntity, CoverEntity):
_attr_device_class = CoverDeviceClass.SHUTTER _attr_device_class = CoverDeviceClass.SHUTTER
def __init__(self, wrapper: BlockDeviceWrapper, block: Block) -> None: def __init__(self, coordinator: ShellyBlockCoordinator, block: Block) -> None:
"""Initialize block cover.""" """Initialize block cover."""
super().__init__(wrapper, block) super().__init__(coordinator, block)
self.control_result: dict[str, Any] | None = None self.control_result: dict[str, Any] | None = None
self._attr_supported_features: int = ( self._attr_supported_features: int = (
CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE | CoverEntityFeature.STOP 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 self._attr_supported_features |= CoverEntityFeature.SET_POSITION
@property @property
@ -147,9 +147,9 @@ class RpcShellyCover(ShellyRpcEntity, CoverEntity):
_attr_device_class = CoverDeviceClass.SHUTTER _attr_device_class = CoverDeviceClass.SHUTTER
def __init__(self, wrapper: RpcDeviceWrapper, id_: int) -> None: def __init__(self, coordinator: ShellyRpcCoordinator, id_: int) -> None:
"""Initialize rpc cover.""" """Initialize rpc cover."""
super().__init__(wrapper, f"cover:{id_}") super().__init__(coordinator, f"cover:{id_}")
self._id = id_ self._id = id_
self._attr_supported_features: int = ( self._attr_supported_features: int = (
CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE | CoverEntityFeature.STOP 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.trigger import TriggerActionType, TriggerInfo
from homeassistant.helpers.typing import ConfigType 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 ( from .const import (
ATTR_CHANNEL, ATTR_CHANNEL,
ATTR_CLICK_TYPE, ATTR_CLICK_TYPE,
@ -78,23 +78,23 @@ async def async_validate_trigger_config(
trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) trigger = (config[CONF_TYPE], config[CONF_SUBTYPE])
if config[CONF_TYPE] in RPC_INPUTS_EVENTS_TYPES: if config[CONF_TYPE] in RPC_INPUTS_EVENTS_TYPES:
rpc_wrapper = get_rpc_device_wrapper(hass, config[CONF_DEVICE_ID]) rpc_coordinator = get_rpc_device_coordinator(hass, config[CONF_DEVICE_ID])
if not rpc_wrapper or not rpc_wrapper.device.initialized: if not rpc_coordinator or not rpc_coordinator.device.initialized:
return config 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: if trigger in input_triggers:
return config return config
elif config[CONF_TYPE] in BLOCK_INPUTS_EVENTS_TYPES: elif config[CONF_TYPE] in BLOCK_INPUTS_EVENTS_TYPES:
block_wrapper = get_block_device_wrapper(hass, config[CONF_DEVICE_ID]) block_coordinator = get_block_device_coordinator(hass, config[CONF_DEVICE_ID])
if not block_wrapper or not block_wrapper.device.initialized: if not block_coordinator or not block_coordinator.device.initialized:
return config return config
assert block_wrapper.device.blocks assert block_coordinator.device.blocks
for block in block_wrapper.device.blocks: for block in block_coordinator.device.blocks:
input_triggers = get_block_input_triggers(block_wrapper.device, block) input_triggers = get_block_input_triggers(block_coordinator.device, block)
if trigger in input_triggers: if trigger in input_triggers:
return config return config
@ -109,24 +109,24 @@ async def async_get_triggers(
"""List device triggers for Shelly devices.""" """List device triggers for Shelly devices."""
triggers: list[dict[str, str]] = [] triggers: list[dict[str, str]] = []
if rpc_wrapper := get_rpc_device_wrapper(hass, device_id): if rpc_coordinator := get_rpc_device_coordinator(hass, device_id):
input_triggers = get_rpc_input_triggers(rpc_wrapper.device) input_triggers = get_rpc_input_triggers(rpc_coordinator.device)
append_input_triggers(triggers, input_triggers, device_id) append_input_triggers(triggers, input_triggers, device_id)
return triggers return triggers
if block_wrapper := get_block_device_wrapper(hass, device_id): if block_coordinator := get_block_device_coordinator(hass, device_id):
if block_wrapper.model in SHBTN_MODELS: if block_coordinator.model in SHBTN_MODELS:
input_triggers = get_shbtn_input_triggers() input_triggers = get_shbtn_input_triggers()
append_input_triggers(triggers, input_triggers, device_id) append_input_triggers(triggers, input_triggers, device_id)
return triggers return triggers
if not block_wrapper.device.initialized: if not block_coordinator.device.initialized:
return triggers return triggers
assert block_wrapper.device.blocks assert block_coordinator.device.blocks
for block in block_wrapper.device.blocks: for block in block_coordinator.device.blocks:
input_triggers = get_block_input_triggers(block_wrapper.device, block) input_triggers = get_block_input_triggers(block_coordinator.device, block)
append_input_triggers(triggers, input_triggers, device_id) append_input_triggers(triggers, input_triggers, device_id)
return triggers return triggers

View file

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

View file

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

View file

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

View file

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

View file

@ -119,7 +119,7 @@ class BlockSleepingNumber(ShellySleepingBlockAttributeEntity, NumberEntity):
LOGGER.debug("Setting state for entity %s, state: %s", self.name, params) LOGGER.debug("Setting state for entity %s, state: %s", self.name, params)
try: try:
async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): 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: except (asyncio.TimeoutError, OSError) as err:
LOGGER.error( LOGGER.error(
"Setting state for entity %s failed, state: %s, error: %s", "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.entity_registry import RegistryEntry
from homeassistant.helpers.typing import StateType from homeassistant.helpers.typing import StateType
from . import BlockDeviceWrapper
from .const import CONF_SLEEP_PERIOD, SHAIR_MAX_WORK_HOURS from .const import CONF_SLEEP_PERIOD, SHAIR_MAX_WORK_HOURS
from .coordinator import ShellyBlockCoordinator
from .entity import ( from .entity import (
BlockEntityDescription, BlockEntityDescription,
RestEntityDescription, RestEntityDescription,
@ -355,7 +355,7 @@ RPC_SENSORS: Final = {
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
use_polling_wrapper=True, use_polling_coordinator=True,
), ),
"temperature_0": RpcSensorDescription( "temperature_0": RpcSensorDescription(
key="temperature:0", key="temperature:0",
@ -376,7 +376,7 @@ RPC_SENSORS: Final = {
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
use_polling_wrapper=True, use_polling_coordinator=True,
), ),
"uptime": RpcSensorDescription( "uptime": RpcSensorDescription(
key="sys", key="sys",
@ -386,7 +386,7 @@ RPC_SENSORS: Final = {
device_class=SensorDeviceClass.TIMESTAMP, device_class=SensorDeviceClass.TIMESTAMP,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
use_polling_wrapper=True, use_polling_coordinator=True,
), ),
"humidity_0": RpcSensorDescription( "humidity_0": RpcSensorDescription(
key="humidity:0", key="humidity:0",
@ -465,13 +465,13 @@ class BlockSensor(ShellyBlockAttributeEntity, SensorEntity):
def __init__( def __init__(
self, self,
wrapper: BlockDeviceWrapper, coordinator: ShellyBlockCoordinator,
block: Block, block: Block,
attribute: str, attribute: str,
description: BlockSensorDescription, description: BlockSensorDescription,
) -> None: ) -> None:
"""Initialize sensor.""" """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 self._attr_native_unit_of_measurement = description.native_unit_of_measurement
if unit_fn := description.unit_fn: if unit_fn := description.unit_fn:
@ -512,7 +512,7 @@ class BlockSleepingSensor(ShellySleepingBlockAttributeEntity, SensorEntity):
def __init__( def __init__(
self, self,
wrapper: BlockDeviceWrapper, coordinator: ShellyBlockCoordinator,
block: Block | None, block: Block | None,
attribute: str, attribute: str,
description: BlockSensorDescription, description: BlockSensorDescription,
@ -520,7 +520,7 @@ class BlockSleepingSensor(ShellySleepingBlockAttributeEntity, SensorEntity):
sensors: Mapping[tuple[str, str], BlockSensorDescription] | None = None, sensors: Mapping[tuple[str, str], BlockSensorDescription] | None = None,
) -> None: ) -> None:
"""Initialize the sleeping sensor.""" """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 self._attr_native_unit_of_measurement = description.native_unit_of_measurement
if block and (unit_fn := description.unit_fn): 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.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import BlockDeviceWrapper, RpcDeviceWrapper
from .const import BLOCK, DATA_CONFIG_ENTRY, DOMAIN, RPC from .const import BLOCK, DATA_CONFIG_ENTRY, DOMAIN, RPC
from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator
from .entity import ShellyBlockEntity, ShellyRpcEntity from .entity import ShellyBlockEntity, ShellyRpcEntity
from .utils import ( from .utils import (
async_remove_shelly_entity, async_remove_shelly_entity,
@ -41,31 +41,31 @@ def async_setup_block_entry(
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up entities for block device.""" """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 # In roller mode the relay blocks exist but do not contain required info
if ( if (
wrapper.model in ["SHSW-21", "SHSW-25"] coordinator.model in ["SHSW-21", "SHSW-25"]
and wrapper.device.settings["mode"] != "relay" and coordinator.device.settings["mode"] != "relay"
): ):
return return
relay_blocks = [] relay_blocks = []
assert wrapper.device.blocks assert coordinator.device.blocks
for block in wrapper.device.blocks: for block in coordinator.device.blocks:
if block.type != "relay" or is_block_channel_type_light( if block.type != "relay" or is_block_channel_type_light(
wrapper.device.settings, int(block.channel) coordinator.device.settings, int(block.channel)
): ):
continue continue
relay_blocks.append(block) 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) async_remove_shelly_entity(hass, "light", unique_id)
if not relay_blocks: if not relay_blocks:
return return
async_add_entities(BlockRelaySwitch(wrapper, block) for block in relay_blocks) async_add_entities(BlockRelaySwitch(coordinator, block) for block in relay_blocks)
@callback @callback
@ -75,31 +75,31 @@ def async_setup_rpc_entry(
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up entities for RPC device.""" """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 = [] switch_ids = []
for id_ in switch_key_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 continue
switch_ids.append(id_) 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) async_remove_shelly_entity(hass, "light", unique_id)
if not switch_ids: if not switch_ids:
return 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): class BlockRelaySwitch(ShellyBlockEntity, SwitchEntity):
"""Entity that controls a relay on Block based Shelly devices.""" """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.""" """Initialize relay switch."""
super().__init__(wrapper, block) super().__init__(coordinator, block)
self.control_result: dict[str, Any] | None = None self.control_result: dict[str, Any] | None = None
@property @property
@ -130,15 +130,15 @@ class BlockRelaySwitch(ShellyBlockEntity, SwitchEntity):
class RpcRelaySwitch(ShellyRpcEntity, SwitchEntity): class RpcRelaySwitch(ShellyRpcEntity, SwitchEntity):
"""Entity that controls a relay on RPC based Shelly devices.""" """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.""" """Initialize relay switch."""
super().__init__(wrapper, f"switch:{id_}") super().__init__(coordinator, f"switch:{id_}")
self._id = id_ self._id = id_
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""If switch is on.""" """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: async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on relay.""" """Turn on relay."""

View file

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