1 """Shelly entity helper."""
3 from __future__
import annotations
5 from collections.abc
import Callable, Mapping
6 from dataclasses
import dataclass
7 from typing
import Any, cast
9 from aioshelly.block_device
import Block
10 from aioshelly.exceptions
import DeviceConnectionError, InvalidAuthError, RpcCallError
22 from .const
import CONF_SLEEP_PERIOD, LOGGER
23 from .coordinator
import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator
25 async_remove_shelly_entity,
26 get_block_entity_name,
28 get_rpc_key_instances,
35 config_entry: ShellyConfigEntry,
36 async_add_entities: AddEntitiesCallback,
37 sensors: Mapping[tuple[str, str], BlockEntityDescription],
38 sensor_class: Callable,
40 """Set up entities for attributes."""
41 coordinator = config_entry.runtime_data.block
43 if coordinator.device.initialized:
45 hass, async_add_entities, coordinator, sensors, sensor_class
61 async_add_entities: AddEntitiesCallback,
62 coordinator: ShellyBlockCoordinator,
63 sensors: Mapping[tuple[str, str], BlockEntityDescription],
64 sensor_class: Callable,
66 """Set up entities for block attributes."""
69 assert coordinator.device.blocks
71 for block
in coordinator.device.blocks:
72 for sensor_id
in block.sensor_ids:
73 description = sensors.get((cast(str, block.type), sensor_id))
74 if description
is None:
78 if getattr(block, sensor_id,
None)
is None:
83 if description.removal_condition
and description.removal_condition(
84 coordinator.device.settings, block
86 domain = sensor_class.__module__.split(
".")[-1]
87 unique_id = f
"{coordinator.mac}-{block.description}-{sensor_id}"
91 sensor_class(coordinator, block, sensor_id, description)
103 config_entry: ShellyConfigEntry,
104 async_add_entities: AddEntitiesCallback,
105 coordinator: ShellyBlockCoordinator,
106 sensors: Mapping[tuple[str, str], BlockEntityDescription],
107 sensor_class: Callable,
109 """Restore block attributes entities."""
112 ent_reg = er.async_get(hass)
113 entries = er.async_entries_for_config_entry(ent_reg, config_entry.entry_id)
115 domain = sensor_class.__module__.split(
".")[-1]
117 for entry
in entries:
118 if entry.domain != domain:
121 attribute = entry.unique_id.split(
"-")[-1]
122 block_type = entry.unique_id.split(
"-")[-2].split(
"_")[0]
124 if description := sensors.get((block_type, attribute)):
126 sensor_class(coordinator,
None, attribute, description, entry)
138 config_entry: ShellyConfigEntry,
139 async_add_entities: AddEntitiesCallback,
140 sensors: Mapping[str, RpcEntityDescription],
141 sensor_class: Callable,
143 """Set up entities for RPC sensors."""
144 coordinator = config_entry.runtime_data.rpc
147 if coordinator.device.initialized:
149 hass, config_entry, async_add_entities, sensors, sensor_class
153 hass, config_entry, async_add_entities, coordinator, sensors, sensor_class
160 config_entry: ShellyConfigEntry,
161 async_add_entities: AddEntitiesCallback,
162 sensors: Mapping[str, RpcEntityDescription],
163 sensor_class: Callable,
165 """Set up entities for RPC attributes."""
166 coordinator = config_entry.runtime_data.rpc
169 polling_coordinator =
None
170 if not (sleep_period := config_entry.data[CONF_SLEEP_PERIOD]):
171 polling_coordinator = config_entry.runtime_data.rpc_poll
172 assert polling_coordinator
175 for sensor_id
in sensors:
176 description = sensors[sensor_id]
178 coordinator.device.status, description.key
181 for key
in key_instances:
183 if description.sub_key
not in coordinator.device.status[
185 ]
and not description.supported(coordinator.device.status[key]):
190 if description.removal_condition
and description.removal_condition(
191 coordinator.device.config, coordinator.device.status, key
193 domain = sensor_class.__module__.split(
".")[-1]
194 unique_id = f
"{coordinator.mac}-{key}-{sensor_id}"
196 elif description.use_polling_coordinator:
199 sensor_class(polling_coordinator, key, sensor_id, description)
202 entities.append(sensor_class(coordinator, key, sensor_id, description))
212 config_entry: ShellyConfigEntry,
213 async_add_entities: AddEntitiesCallback,
214 coordinator: ShellyRpcCoordinator,
215 sensors: Mapping[str, RpcEntityDescription],
216 sensor_class: Callable,
218 """Restore block attributes entities."""
221 ent_reg = er.async_get(hass)
222 entries = er.async_entries_for_config_entry(ent_reg, config_entry.entry_id)
224 domain = sensor_class.__module__.split(
".")[-1]
226 for entry
in entries:
227 if entry.domain != domain:
230 key = entry.unique_id.split(
"-")[-2]
231 attribute = entry.unique_id.split(
"-")[-1]
233 if description := sensors.get(attribute):
235 sensor_class(coordinator, key, attribute, description, entry)
247 config_entry: ShellyConfigEntry,
248 async_add_entities: AddEntitiesCallback,
249 sensors: Mapping[str, RestEntityDescription],
250 sensor_class: Callable,
252 """Set up entities for REST sensors."""
253 coordinator = config_entry.runtime_data.rest
257 sensor_class(coordinator, sensor_id, sensors[sensor_id])
258 for sensor_id
in sensors
262 @dataclass(frozen=True)
264 """Class to describe a BLOCK entity."""
270 unit_fn: Callable[[dict], str] |
None =
None
271 value: Callable[[Any], Any] =
lambda val: val
272 available: Callable[[Block], bool] |
None =
None
274 removal_condition: Callable[[dict, Block], bool] |
None =
None
275 extra_state_attributes: Callable[[Block], dict |
None] |
None =
None
278 @dataclass(frozen=True, kw_only=True)
280 """Class to describe a RPC entity."""
288 value: Callable[[Any, Any], Any] |
None =
None
289 available: Callable[[dict], bool] |
None =
None
290 removal_condition: Callable[[dict, dict, str], bool] |
None =
None
291 extra_state_attributes: Callable[[dict, dict], dict |
None] |
None =
None
292 use_polling_coordinator: bool =
False
293 supported: Callable =
lambda _:
False
294 unit: Callable[[dict], str |
None] |
None =
None
295 options_fn: Callable[[dict], list[str]] |
None =
None
298 @dataclass(frozen=True)
300 """Class to describe a REST entity."""
306 value: Callable[[dict, Any], Any] |
None =
None
307 extra_state_attributes: Callable[[dict], dict |
None] |
None =
None
311 """Helper class to represent a block entity."""
313 def __init__(self, coordinator: ShellyBlockCoordinator, block: Block) ->
None:
314 """Initialize Shelly entity."""
319 connections={(CONNECTION_NETWORK_MAC, coordinator.mac)}
325 """When entity is added to HASS."""
330 """Handle device update."""
331 self.async_write_ha_state()
334 """Set block state (HTTP request)."""
335 LOGGER.debug(
"Setting state for entity %s, state: %s", self.name, kwargs)
338 except DeviceConnectionError
as err:
339 self.coordinator.last_update_success =
False
341 f
"Setting state for entity {self.name} failed, state: {kwargs}, error:"
344 except InvalidAuthError:
349 """Helper class to represent a rpc entity."""
351 def __init__(self, coordinator: ShellyRpcCoordinator, key: str) ->
None:
352 """Initialize Shelly entity."""
356 "connections": {(CONNECTION_NETWORK_MAC, coordinator.mac)}
363 """Check if device is available and initialized or sleepy."""
364 coordinator = self.coordinator
365 return super().available
and (
366 coordinator.device.initialized
or bool(coordinator.sleep_period)
371 """Device status by entity key."""
372 return cast(dict, self.coordinator.device.status[self.
keykey])
376 """When entity is added to HASS."""
381 """Handle device update."""
382 self.async_write_ha_state()
384 async
def call_rpc(self, method: str, params: Any) -> Any:
385 """Call RPC method."""
387 "Call RPC for entity %s, method: %s, params: %s",
393 return await self.coordinator.device.call_rpc(method, params)
394 except DeviceConnectionError
as err:
395 self.coordinator.last_update_success =
False
397 f
"Call RPC for {self.name} connection error, method: {method}, params:"
398 f
" {params}, error: {err!r}"
400 except RpcCallError
as err:
402 f
"Call RPC for {self.name} request error, method: {method}, params:"
403 f
" {params}, error: {err!r}"
405 except InvalidAuthError:
410 """Helper class to represent a block attribute."""
412 entity_description: BlockEntityDescription
416 coordinator: ShellyBlockCoordinator,
419 description: BlockEntityDescription,
421 """Initialize sensor."""
422 super().
__init__(coordinator, block)
426 self.
_attr_unique_id_attr_unique_id: str = f
"{super().unique_id}-{self.attribute}"
428 coordinator.device, block, description.name
433 """Value of sensor."""
434 if (value := getattr(self.
blockblock, self.
attributeattribute))
is None:
442 available = super().available
451 """Return the state attributes."""
459 """Class to load info from REST."""
461 entity_description: RestEntityDescription
465 coordinator: ShellyBlockCoordinator,
467 description: RestEntityDescription,
469 """Initialize sensor."""
475 coordinator.device,
None, description.name
479 connections={(CONNECTION_NETWORK_MAC, coordinator.mac)}
490 """Value of sensor."""
499 """Helper class to represent a rpc attribute."""
501 entity_description: RpcEntityDescription
505 coordinator: ShellyRpcCoordinator,
508 description: RpcEntityDescription,
510 """Initialize sensor."""
518 id_key = key.split(
":")[-1]
519 self.
_id_id =
int(id_key)
if id_key.isnumeric()
else None
521 if description.unit
is not None:
523 coordinator.device.config[key]
526 self.
option_mapoption_map: dict[str, str] = {}
529 titles = self.coordinator.device.config[key][
"meta"][
"ui"][
"titles"]
530 options = self.coordinator.device.config[key][
"options"]
532 opt: (titles[opt]
if titles.get(opt)
is not None else opt)
536 tit: opt
for opt, tit
in self.
option_mapoption_map.items()
541 """Device status by entity key."""
546 """Value of sensor."""
560 available = super().available
569 """Represent a shelly sleeping block attribute entity."""
574 coordinator: ShellyBlockCoordinator,
577 description: BlockEntityDescription,
578 entry: RegistryEntry |
None =
None,
580 """Initialize the sleeping sensor."""
581 self.last_state: State |
None =
None
588 connections={(CONNECTION_NETWORK_MAC, coordinator.mac)}
591 if block
is not None:
593 f
"{self.coordinator.mac}-{block.description}-{attribute}"
598 elif entry
is not None:
604 """Handle device update."""
613 for block
in self.
coordinatorcoordinator.device.blocks:
614 if block.description != entity_block:
617 for sensor_id
in block.sensor_ids:
618 if sensor_id != entity_sensor:
622 LOGGER.debug(
"Entity %s attached to block", self.
namename)
627 """Update the entity."""
629 "Entity %s comes from a sleeping device, update is not possible",
635 """Helper class to represent a sleeping rpc attribute."""
637 entity_description: RpcEntityDescription
642 coordinator: ShellyRpcCoordinator,
645 description: RpcEntityDescription,
646 entry: RegistryEntry |
None =
None,
648 """Initialize the sleeping sensor."""
649 self.last_state: State |
None =
None
656 connections={(CONNECTION_NETWORK_MAC, coordinator.mac)}
659 f
"{coordinator.mac}-{key}-{attribute}"
663 if coordinator.device.initialized:
665 coordinator.device, key, description.name
667 elif entry
is not None:
671 """Update the entity."""
673 "Entity %s comes from a sleeping device, update is not possible",
dict[str, Any]|None extra_state_attributes(self)
None __init__(self, ShellyBlockCoordinator coordinator, Block block, str attribute, BlockEntityDescription description)
StateType attribute_value(self)
None __init__(self, ShellyBlockCoordinator coordinator, Block block)
None _update_callback(self)
None async_added_to_hass(self)
Any set_state(self, **Any kwargs)
None __init__(self, ShellyBlockCoordinator coordinator, str attribute, RestEntityDescription description)
StateType attribute_value(self)
StateType attribute_value(self)
None __init__(self, ShellyRpcCoordinator coordinator, str key, str attribute, RpcEntityDescription description)
_attr_native_unit_of_measurement
None __init__(self, ShellyRpcCoordinator coordinator, str key)
None async_added_to_hass(self)
None _update_callback(self)
Any call_rpc(self, str method, Any params)
None _update_callback(self)
None __init__(self, ShellyBlockCoordinator coordinator, Block|None block, str attribute, BlockEntityDescription description, RegistryEntry|None entry=None)
None __init__(self, ShellyRpcCoordinator coordinator, str key, str attribute, RpcEntityDescription description, RegistryEntry|None entry=None)
str|UndefinedType|None name(self)
web.Response get(self, web.Request request, str config_key)
None async_add_listener(HomeAssistant hass, Callable[[], None] listener)
None async_shutdown_device_and_start_reauth(self)
None async_restore_block_attribute_entities(HomeAssistant hass, ShellyConfigEntry config_entry, AddEntitiesCallback async_add_entities, ShellyBlockCoordinator coordinator, Mapping[tuple[str, str], BlockEntityDescription] sensors, Callable sensor_class)
None async_setup_rpc_attribute_entities(HomeAssistant hass, ShellyConfigEntry config_entry, AddEntitiesCallback async_add_entities, Mapping[str, RpcEntityDescription] sensors, Callable sensor_class)
None async_setup_entry_rpc(HomeAssistant hass, ShellyConfigEntry config_entry, AddEntitiesCallback async_add_entities, Mapping[str, RpcEntityDescription] sensors, Callable sensor_class)
None async_setup_entry_rest(HomeAssistant hass, ShellyConfigEntry config_entry, AddEntitiesCallback async_add_entities, Mapping[str, RestEntityDescription] sensors, Callable sensor_class)
None async_setup_block_attribute_entities(HomeAssistant hass, AddEntitiesCallback async_add_entities, ShellyBlockCoordinator coordinator, Mapping[tuple[str, str], BlockEntityDescription] sensors, Callable sensor_class)
None async_restore_rpc_attribute_entities(HomeAssistant hass, ShellyConfigEntry config_entry, AddEntitiesCallback async_add_entities, ShellyRpcCoordinator coordinator, Mapping[str, RpcEntityDescription] sensors, Callable sensor_class)
None async_setup_entry_attribute_entities(HomeAssistant hass, ShellyConfigEntry config_entry, AddEntitiesCallback async_add_entities, Mapping[tuple[str, str], BlockEntityDescription] sensors, Callable sensor_class)
list[str] get_rpc_key_instances(dict[str, Any] keys_dict, str key)
str get_block_entity_name(BlockDevice device, Block|None block, str|None description=None)
None async_remove_shelly_entity(HomeAssistant hass, str domain, str unique_id)
str get_rpc_entity_name(RpcDevice device, str key, str|None description=None)