Use sub-devices for Shelly multi-channel devices (#144100)

* Shelly RPC sub-devices

* Better varaible name

* Add get_rpc_device_info helper

* Revert channel name changes

* Use get_rpc_device_info

* Add get_rpc_device_info helper

* Use get_block_device_info

* Use helpers in the button platform

* Fix channel name and roller mode for block devices

* Fix EM3 gen1

* Fix channel name for RPC devices

* Revert test changes

* Fix/improve test_block_get_block_channel_name

* Fix test_get_rpc_channel_name_multiple_components

* Fix tests

* Fix tests

* Fix tests

* Use key instead of index to generate sub-device identifier

* Improve logic for Pro RGBWW PM

* Split channels for em1

* Better channel name

* Cleaning

* has_entity_name is True

* Add get_block_sub_device_name() function

* Improve block functions

* Add get_rpc_sub_device_name() function

* Remove _attr_name

* Remove name for button with device class

* Fix names of virtual components

* Better Input name

* Fix get_rpc_channel_name()

* Fix names for Inputs

* get_rpc_channel_name() improvement

* Better variable name

* Clean RPC functions

* Fix input_name type

* Fix test

* Fix entity_ids for Blu Trv

* Fix get_block_channel_name()

* Fix for Blu Trv, once again

* Revert name for reboot button

* Fix button tests

* Fix tests

* Fix coordinator tests

* Fix tests for cover platform

* Fix tests for event platform

* Fix entity_ids in init tests

* Fix get_block_channel_name() for lights

* Fix tests for light platform

* Fix test for logbook

* Update snapshots for number platform

* Fix tests for sensor platform

* Fix tests for switch platform

* Fix tests for utils

* Uncomment

* Fix tests for flood

* Fix Valve entity name

* Fix climate tests

* Fix test for diagnostics

* Fix tests for init

* Remove old snapshots

* Add tests for 2PM Gen3

* Add comment

* More tests

* Cleaning

* Clean fixtures

* Update tests

* Anonymize coordinates in fixtures

* Split Pro 3EM entities into sub-devices

* Make sub-device names more unique

* 3EM (gen1) does not support sub-devices

* Coverage

* Rename "device temperature" sensor to the "relay temperature"

* Update tests after rebase

* Support sub-devices for 3EM (gen1)

* Mark has-entity-name rule as done 🎉

* Rename `relay temperature` to `temperature`
This commit is contained in:
Maciej Bieniek 2025-05-26 10:47:22 +02:00 committed by GitHub
parent d4333665fc
commit 7f4cc99a3e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 1744 additions and 322 deletions

View File

@ -15,7 +15,6 @@ from homeassistant.components.binary_sensor import (
) )
from homeassistant.const import STATE_ON, EntityCategory from homeassistant.const import STATE_ON, EntityCategory
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.restore_state import RestoreEntity
@ -36,6 +35,7 @@ from .entity import (
) )
from .utils import ( from .utils import (
async_remove_orphaned_entities, async_remove_orphaned_entities,
get_blu_trv_device_info,
get_device_entry_gen, get_device_entry_gen,
get_virtual_component_ids, get_virtual_component_ids,
is_block_momentary_input, is_block_momentary_input,
@ -87,8 +87,8 @@ class RpcBluTrvBinarySensor(RpcBinarySensor):
super().__init__(coordinator, key, attribute, description) super().__init__(coordinator, key, attribute, description)
ble_addr: str = coordinator.device.config[key]["addr"] ble_addr: str = coordinator.device.config[key]["addr"]
self._attr_device_info = DeviceInfo( self._attr_device_info = get_blu_trv_device_info(
connections={(CONNECTION_BLUETOOTH, ble_addr)} coordinator.device.config[key], ble_addr, coordinator.mac
) )
@ -190,7 +190,6 @@ RPC_SENSORS: Final = {
"input": RpcBinarySensorDescription( "input": RpcBinarySensorDescription(
key="input", key="input",
sub_key="state", sub_key="state",
name="Input",
device_class=BinarySensorDeviceClass.POWER, device_class=BinarySensorDeviceClass.POWER,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
removal_condition=is_rpc_momentary_input, removal_condition=is_rpc_momentary_input,
@ -264,7 +263,6 @@ RPC_SENSORS: Final = {
"boolean": RpcBinarySensorDescription( "boolean": RpcBinarySensorDescription(
key="boolean", key="boolean",
sub_key="value", sub_key="value",
has_entity_name=True,
), ),
"calibration": RpcBinarySensorDescription( "calibration": RpcBinarySensorDescription(
key="blutrv", key="blutrv",

View File

@ -19,18 +19,20 @@ from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.device_registry import ( from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
CONNECTION_BLUETOOTH,
CONNECTION_NETWORK_MAC,
DeviceInfo,
)
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import slugify from homeassistant.util import slugify
from .const import DOMAIN, LOGGER, SHELLY_GAS_MODELS from .const import DOMAIN, LOGGER, SHELLY_GAS_MODELS
from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator
from .utils import get_device_entry_gen, get_rpc_key_ids from .utils import (
get_block_device_info,
get_blu_trv_device_info,
get_device_entry_gen,
get_rpc_device_info,
get_rpc_key_ids,
)
PARALLEL_UPDATES = 0 PARALLEL_UPDATES = 0
@ -168,6 +170,7 @@ class ShellyBaseButton(
): ):
"""Defines a Shelly base button.""" """Defines a Shelly base button."""
_attr_has_entity_name = True
entity_description: ShellyButtonDescription[ entity_description: ShellyButtonDescription[
ShellyRpcCoordinator | ShellyBlockCoordinator ShellyRpcCoordinator | ShellyBlockCoordinator
] ]
@ -228,8 +231,15 @@ class ShellyButton(ShellyBaseButton):
"""Initialize Shelly button.""" """Initialize Shelly button."""
super().__init__(coordinator, description) super().__init__(coordinator, description)
self._attr_name = f"{coordinator.device.name} {description.name}"
self._attr_unique_id = f"{coordinator.mac}_{description.key}" self._attr_unique_id = f"{coordinator.mac}_{description.key}"
if isinstance(coordinator, ShellyBlockCoordinator):
self._attr_device_info = get_block_device_info(
coordinator.device, coordinator.mac
)
else:
self._attr_device_info = get_rpc_device_info(
coordinator.device, coordinator.mac
)
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
connections={(CONNECTION_NETWORK_MAC, coordinator.mac)} connections={(CONNECTION_NETWORK_MAC, coordinator.mac)}
) )
@ -256,15 +266,11 @@ class ShellyBluTrvButton(ShellyBaseButton):
"""Initialize.""" """Initialize."""
super().__init__(coordinator, description) super().__init__(coordinator, description)
ble_addr: str = coordinator.device.config[f"{BLU_TRV_IDENTIFIER}:{id_}"]["addr"] config = coordinator.device.config[f"{BLU_TRV_IDENTIFIER}:{id_}"]
device_name = ( ble_addr: str = config["addr"]
coordinator.device.config[f"{BLU_TRV_IDENTIFIER}:{id_}"]["name"]
or f"shellyblutrv-{ble_addr.replace(':', '')}"
)
self._attr_name = f"{device_name} {description.name}"
self._attr_unique_id = f"{ble_addr}_{description.key}" self._attr_unique_id = f"{ble_addr}_{description.key}"
self._attr_device_info = DeviceInfo( self._attr_device_info = get_blu_trv_device_info(
connections={(CONNECTION_BLUETOOTH, ble_addr)} config, ble_addr, coordinator.mac
) )
self._id = id_ self._id = id_

View File

@ -7,7 +7,7 @@ from dataclasses import asdict, dataclass
from typing import Any, cast from typing import Any, cast
from aioshelly.block_device import Block from aioshelly.block_device import Block
from aioshelly.const import BLU_TRV_IDENTIFIER, BLU_TRV_MODEL_NAME, RPC_GENERATIONS from aioshelly.const import BLU_TRV_IDENTIFIER, RPC_GENERATIONS
from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError
from homeassistant.components.climate import ( from homeassistant.components.climate import (
@ -22,11 +22,6 @@ from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant, State, callback from homeassistant.core import HomeAssistant, State, callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er, issue_registry as ir from homeassistant.helpers import entity_registry as er, issue_registry as ir
from homeassistant.helpers.device_registry import (
CONNECTION_BLUETOOTH,
CONNECTION_NETWORK_MAC,
DeviceInfo,
)
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.entity_registry import RegistryEntry from homeassistant.helpers.entity_registry import RegistryEntry
from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity
@ -46,6 +41,9 @@ from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoo
from .entity import ShellyRpcEntity, rpc_call from .entity import ShellyRpcEntity, rpc_call
from .utils import ( from .utils import (
async_remove_shelly_entity, async_remove_shelly_entity,
get_block_device_info,
get_block_entity_name,
get_blu_trv_device_info,
get_device_entry_gen, get_device_entry_gen,
get_rpc_key_ids, get_rpc_key_ids,
is_rpc_thermostat_internal_actuator, is_rpc_thermostat_internal_actuator,
@ -181,6 +179,7 @@ class BlockSleepingClimate(
) )
_attr_target_temperature_step = SHTRV_01_TEMPERATURE_SETTINGS["step"] _attr_target_temperature_step = SHTRV_01_TEMPERATURE_SETTINGS["step"]
_attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_has_entity_name = True
def __init__( def __init__(
self, self,
@ -199,7 +198,6 @@ class BlockSleepingClimate(
self.last_state_attributes: Mapping[str, Any] self.last_state_attributes: Mapping[str, Any]
self._preset_modes: list[str] = [] self._preset_modes: list[str] = []
self._last_target_temp = SHTRV_01_TEMPERATURE_SETTINGS["default"] self._last_target_temp = SHTRV_01_TEMPERATURE_SETTINGS["default"]
self._attr_name = coordinator.name
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.coordinator.mac}-{self.block.description}" self._unique_id = f"{self.coordinator.mac}-{self.block.description}"
@ -212,8 +210,11 @@ class BlockSleepingClimate(
] ]
elif entry is not None: elif entry is not None:
self._unique_id = entry.unique_id self._unique_id = entry.unique_id
self._attr_device_info = DeviceInfo( self._attr_device_info = get_block_device_info(
connections={(CONNECTION_NETWORK_MAC, coordinator.mac)}, coordinator.device, coordinator.mac, sensor_block
)
self._attr_name = get_block_entity_name(
self.coordinator.device, sensor_block, None
) )
self._channel = cast(int, self._unique_id.split("_")[1]) self._channel = cast(int, self._unique_id.split("_")[1])
@ -553,7 +554,6 @@ class RpcBluTrvClimate(ShellyRpcEntity, ClimateEntity):
_attr_hvac_mode = HVACMode.HEAT _attr_hvac_mode = HVACMode.HEAT
_attr_target_temperature_step = BLU_TRV_TEMPERATURE_SETTINGS["step"] _attr_target_temperature_step = BLU_TRV_TEMPERATURE_SETTINGS["step"]
_attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_has_entity_name = True
def __init__(self, coordinator: ShellyRpcCoordinator, id_: int) -> None: def __init__(self, coordinator: ShellyRpcCoordinator, id_: int) -> None:
"""Initialize.""" """Initialize."""
@ -563,19 +563,9 @@ class RpcBluTrvClimate(ShellyRpcEntity, ClimateEntity):
self._config = coordinator.device.config[f"{BLU_TRV_IDENTIFIER}:{id_}"] self._config = coordinator.device.config[f"{BLU_TRV_IDENTIFIER}:{id_}"]
ble_addr: str = self._config["addr"] ble_addr: str = self._config["addr"]
self._attr_unique_id = f"{ble_addr}-{self.key}" self._attr_unique_id = f"{ble_addr}-{self.key}"
name = self._config["name"] or f"shellyblutrv-{ble_addr.replace(':', '')}" self._attr_device_info = get_blu_trv_device_info(
model_id = self._config.get("local_name") self._config, ble_addr, self.coordinator.mac
self._attr_device_info = DeviceInfo(
connections={(CONNECTION_BLUETOOTH, ble_addr)},
identifiers={(DOMAIN, ble_addr)},
via_device=(DOMAIN, self.coordinator.mac),
manufacturer="Shelly",
model=BLU_TRV_MODEL_NAME.get(model_id),
model_id=model_id,
name=name,
) )
# Added intentionally to the constructor to avoid double name from base class
self._attr_name = None
@property @property
def target_temperature(self) -> float | None: def target_temperature(self) -> float | None:

View File

@ -258,6 +258,7 @@ DEVICES_WITHOUT_FIRMWARE_CHANGELOG = (
CONF_GEN = "gen" CONF_GEN = "gen"
VIRTUAL_COMPONENTS = ("boolean", "enum", "input", "number", "text")
VIRTUAL_COMPONENTS_MAP = { VIRTUAL_COMPONENTS_MAP = {
"binary_sensor": {"types": ["boolean"], "modes": ["label"]}, "binary_sensor": {"types": ["boolean"], "modes": ["label"]},
"number": {"types": ["number"], "modes": ["field", "slider"]}, "number": {"types": ["number"], "modes": ["field", "slider"]},
@ -285,3 +286,5 @@ ROLE_TO_DEVICE_CLASS_MAP = {
# We want to check only the first 5 KB of the script if it contains emitEvent() # We want to check only the first 5 KB of the script if it contains emitEvent()
# so that the integration startup remains fast. # so that the integration startup remains fast.
MAX_SCRIPT_SIZE = 5120 MAX_SCRIPT_SIZE = 5120
All_LIGHT_TYPES = ("cct", "light", "rgb", "rgbw")

View File

@ -13,7 +13,6 @@ from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCal
from homeassistant.core import HomeAssistant, State, callback from homeassistant.core import HomeAssistant, State, callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.entity_registry import RegistryEntry from homeassistant.helpers.entity_registry import RegistryEntry
@ -24,7 +23,9 @@ from .const import CONF_SLEEP_PERIOD, DOMAIN, LOGGER
from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator
from .utils import ( from .utils import (
async_remove_shelly_entity, async_remove_shelly_entity,
get_block_device_info,
get_block_entity_name, get_block_entity_name,
get_rpc_device_info,
get_rpc_entity_name, get_rpc_entity_name,
get_rpc_key_instances, get_rpc_key_instances,
) )
@ -353,13 +354,15 @@ def rpc_call[_T: ShellyRpcEntity, **_P](
class ShellyBlockEntity(CoordinatorEntity[ShellyBlockCoordinator]): class ShellyBlockEntity(CoordinatorEntity[ShellyBlockCoordinator]):
"""Helper class to represent a block entity.""" """Helper class to represent a block entity."""
_attr_has_entity_name = True
def __init__(self, coordinator: ShellyBlockCoordinator, block: Block) -> None: def __init__(self, coordinator: ShellyBlockCoordinator, block: Block) -> None:
"""Initialize Shelly entity.""" """Initialize Shelly entity."""
super().__init__(coordinator) super().__init__(coordinator)
self.block = block self.block = block
self._attr_name = get_block_entity_name(coordinator.device, block) self._attr_name = get_block_entity_name(coordinator.device, block)
self._attr_device_info = DeviceInfo( self._attr_device_info = get_block_device_info(
connections={(CONNECTION_NETWORK_MAC, coordinator.mac)} coordinator.device, coordinator.mac, block
) )
self._attr_unique_id = f"{coordinator.mac}-{block.description}" self._attr_unique_id = f"{coordinator.mac}-{block.description}"
@ -395,12 +398,14 @@ class ShellyBlockEntity(CoordinatorEntity[ShellyBlockCoordinator]):
class ShellyRpcEntity(CoordinatorEntity[ShellyRpcCoordinator]): class ShellyRpcEntity(CoordinatorEntity[ShellyRpcCoordinator]):
"""Helper class to represent a rpc entity.""" """Helper class to represent a rpc entity."""
_attr_has_entity_name = True
def __init__(self, coordinator: ShellyRpcCoordinator, key: str) -> None: def __init__(self, coordinator: ShellyRpcCoordinator, key: str) -> None:
"""Initialize Shelly entity.""" """Initialize Shelly entity."""
super().__init__(coordinator) super().__init__(coordinator)
self.key = key self.key = key
self._attr_device_info = DeviceInfo( self._attr_device_info = get_rpc_device_info(
connections={(CONNECTION_NETWORK_MAC, coordinator.mac)} coordinator.device, coordinator.mac, key
) )
self._attr_unique_id = f"{coordinator.mac}-{key}" self._attr_unique_id = f"{coordinator.mac}-{key}"
self._attr_name = get_rpc_entity_name(coordinator.device, key) self._attr_name = get_rpc_entity_name(coordinator.device, key)
@ -497,6 +502,7 @@ class ShellyBlockAttributeEntity(ShellyBlockEntity, Entity):
class ShellyRestAttributeEntity(CoordinatorEntity[ShellyBlockCoordinator]): class ShellyRestAttributeEntity(CoordinatorEntity[ShellyBlockCoordinator]):
"""Class to load info from REST.""" """Class to load info from REST."""
_attr_has_entity_name = True
entity_description: RestEntityDescription entity_description: RestEntityDescription
def __init__( def __init__(
@ -514,8 +520,8 @@ class ShellyRestAttributeEntity(CoordinatorEntity[ShellyBlockCoordinator]):
coordinator.device, None, description.name coordinator.device, None, description.name
) )
self._attr_unique_id = f"{coordinator.mac}-{attribute}" self._attr_unique_id = f"{coordinator.mac}-{attribute}"
self._attr_device_info = DeviceInfo( self._attr_device_info = get_block_device_info(
connections={(CONNECTION_NETWORK_MAC, coordinator.mac)} coordinator.device, coordinator.mac
) )
self._last_value = None self._last_value = None
@ -623,8 +629,8 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity):
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_device_info = DeviceInfo( self._attr_device_info = get_block_device_info(
connections={(CONNECTION_NETWORK_MAC, coordinator.mac)} coordinator.device, coordinator.mac, block
) )
if block is not None: if block is not None:
@ -632,7 +638,7 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity):
f"{self.coordinator.mac}-{block.description}-{attribute}" f"{self.coordinator.mac}-{block.description}-{attribute}"
) )
self._attr_name = get_block_entity_name( self._attr_name = get_block_entity_name(
self.coordinator.device, block, self.entity_description.name coordinator.device, block, 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
@ -691,8 +697,8 @@ class ShellySleepingRpcAttributeEntity(ShellyRpcAttributeEntity):
self.attribute = attribute self.attribute = attribute
self.entity_description = description self.entity_description = description
self._attr_device_info = DeviceInfo( self._attr_device_info = get_rpc_device_info(
connections={(CONNECTION_NETWORK_MAC, coordinator.mac)} coordinator.device, coordinator.mac, key
) )
self._attr_unique_id = self._attr_unique_id = ( self._attr_unique_id = self._attr_unique_id = (
f"{coordinator.mac}-{key}-{attribute}" f"{coordinator.mac}-{key}-{attribute}"

View File

@ -17,7 +17,6 @@ from homeassistant.components.event import (
EventEntityDescription, EventEntityDescription,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
@ -32,6 +31,7 @@ from .utils import (
async_remove_orphaned_entities, async_remove_orphaned_entities,
async_remove_shelly_entity, async_remove_shelly_entity,
get_device_entry_gen, get_device_entry_gen,
get_rpc_device_info,
get_rpc_entity_name, get_rpc_entity_name,
get_rpc_key_instances, get_rpc_key_instances,
is_block_momentary_input, is_block_momentary_input,
@ -77,7 +77,6 @@ SCRIPT_EVENT: Final = ShellyRpcEventDescription(
translation_key="script", translation_key="script",
device_class=None, device_class=None,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
has_entity_name=True,
) )
@ -195,6 +194,7 @@ class ShellyBlockEvent(ShellyBlockEntity, EventEntity):
class ShellyRpcEvent(CoordinatorEntity[ShellyRpcCoordinator], EventEntity): class ShellyRpcEvent(CoordinatorEntity[ShellyRpcCoordinator], EventEntity):
"""Represent RPC event entity.""" """Represent RPC event entity."""
_attr_has_entity_name = True
entity_description: ShellyRpcEventDescription entity_description: ShellyRpcEventDescription
def __init__( def __init__(
@ -206,8 +206,8 @@ class ShellyRpcEvent(CoordinatorEntity[ShellyRpcCoordinator], EventEntity):
"""Initialize Shelly entity.""" """Initialize Shelly entity."""
super().__init__(coordinator) super().__init__(coordinator)
self.event_id = int(key.split(":")[-1]) self.event_id = int(key.split(":")[-1])
self._attr_device_info = DeviceInfo( self._attr_device_info = get_rpc_device_info(
connections={(CONNECTION_NETWORK_MAC, coordinator.mac)} coordinator.device, coordinator.mac, key
) )
self._attr_unique_id = f"{coordinator.mac}-{key}" self._attr_unique_id = f"{coordinator.mac}-{key}"
self._attr_name = get_rpc_entity_name(coordinator.device, key) self._attr_name = get_rpc_entity_name(coordinator.device, key)

View File

@ -43,7 +43,7 @@ def async_describe_events(
rpc_coordinator = get_rpc_coordinator_by_device_id(hass, device_id) rpc_coordinator = get_rpc_coordinator_by_device_id(hass, device_id)
if rpc_coordinator and rpc_coordinator.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_coordinator.device, key) input_name = f"{rpc_coordinator.device.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_coordinator = get_block_coordinator_by_device_id(hass, device_id) block_coordinator = get_block_coordinator_by_device_id(hass, device_id)

View File

@ -21,7 +21,6 @@ from homeassistant.components.number import (
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTemperature from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTemperature
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.entity_registry import RegistryEntry from homeassistant.helpers.entity_registry import RegistryEntry
@ -38,6 +37,7 @@ from .entity import (
) )
from .utils import ( from .utils import (
async_remove_orphaned_entities, async_remove_orphaned_entities,
get_blu_trv_device_info,
get_device_entry_gen, get_device_entry_gen,
get_virtual_component_ids, get_virtual_component_ids,
) )
@ -124,8 +124,8 @@ class RpcBluTrvNumber(RpcNumber):
super().__init__(coordinator, key, attribute, description) super().__init__(coordinator, key, attribute, description)
ble_addr: str = coordinator.device.config[key]["addr"] ble_addr: str = coordinator.device.config[key]["addr"]
self._attr_device_info = DeviceInfo( self._attr_device_info = get_blu_trv_device_info(
connections={(CONNECTION_BLUETOOTH, ble_addr)} coordinator.device.config[key], ble_addr, coordinator.mac
) )
@ -183,7 +183,6 @@ RPC_NUMBERS: Final = {
"number": RpcNumberDescription( "number": RpcNumberDescription(
key="number", key="number",
sub_key="value", sub_key="value",
has_entity_name=True,
max_fn=lambda config: config["max"], max_fn=lambda config: config["max"],
min_fn=lambda config: config["min"], min_fn=lambda config: config["min"],
mode_fn=lambda config: VIRTUAL_NUMBER_MODE_MAP.get( mode_fn=lambda config: VIRTUAL_NUMBER_MODE_MAP.get(

View File

@ -17,7 +17,7 @@ rules:
docs-removal-instructions: done docs-removal-instructions: done
entity-event-setup: done entity-event-setup: done
entity-unique-id: done entity-unique-id: done
has-entity-name: todo has-entity-name: done
runtime-data: done runtime-data: done
test-before-configure: done test-before-configure: done
test-before-setup: done test-before-setup: done

View File

@ -40,7 +40,6 @@ RPC_SELECT_ENTITIES: Final = {
"enum": RpcSelectDescription( "enum": RpcSelectDescription(
key="enum", key="enum",
sub_key="value", sub_key="value",
has_entity_name=True,
), ),
} }

View File

@ -34,7 +34,6 @@ from homeassistant.const import (
UnitOfTemperature, UnitOfTemperature,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
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
@ -56,8 +55,10 @@ from .entity import (
) )
from .utils import ( from .utils import (
async_remove_orphaned_entities, async_remove_orphaned_entities,
get_blu_trv_device_info,
get_device_entry_gen, get_device_entry_gen,
get_device_uptime, get_device_uptime,
get_rpc_device_info,
get_shelly_air_lamp_life, get_shelly_air_lamp_life,
get_virtual_component_ids, get_virtual_component_ids,
is_rpc_wifi_stations_disabled, is_rpc_wifi_stations_disabled,
@ -76,6 +77,7 @@ class RpcSensorDescription(RpcEntityDescription, SensorEntityDescription):
"""Class to describe a RPC sensor.""" """Class to describe a RPC sensor."""
device_class_fn: Callable[[dict], SensorDeviceClass | None] | None = None device_class_fn: Callable[[dict], SensorDeviceClass | None] | None = None
emeter_phase: str | None = None
@dataclass(frozen=True, kw_only=True) @dataclass(frozen=True, kw_only=True)
@ -121,6 +123,26 @@ class RpcSensor(ShellyRpcAttributeEntity, SensorEntity):
return self.option_map[attribute_value] return self.option_map[attribute_value]
class RpcEmeterPhaseSensor(RpcSensor):
"""Represent a RPC energy meter phase sensor."""
entity_description: RpcSensorDescription
def __init__(
self,
coordinator: ShellyRpcCoordinator,
key: str,
attribute: str,
description: RpcSensorDescription,
) -> None:
"""Initialize select."""
super().__init__(coordinator, key, attribute, description)
self._attr_device_info = get_rpc_device_info(
coordinator.device, coordinator.mac, key, description.emeter_phase
)
class RpcBluTrvSensor(RpcSensor): class RpcBluTrvSensor(RpcSensor):
"""Represent a RPC BluTrv sensor.""" """Represent a RPC BluTrv sensor."""
@ -135,8 +157,8 @@ class RpcBluTrvSensor(RpcSensor):
super().__init__(coordinator, key, attribute, description) super().__init__(coordinator, key, attribute, description)
ble_addr: str = coordinator.device.config[key]["addr"] ble_addr: str = coordinator.device.config[key]["addr"]
self._attr_device_info = DeviceInfo( self._attr_device_info = get_blu_trv_device_info(
connections={(CONNECTION_BLUETOOTH, ble_addr)} coordinator.device.config[key], ble_addr, coordinator.mac
) )
@ -507,26 +529,32 @@ RPC_SENSORS: Final = {
"a_act_power": RpcSensorDescription( "a_act_power": RpcSensorDescription(
key="em", key="em",
sub_key="a_act_power", sub_key="a_act_power",
name="Phase A active power", name="Active power",
native_unit_of_measurement=UnitOfPower.WATT, native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER, device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
emeter_phase="A",
entity_class=RpcEmeterPhaseSensor,
), ),
"b_act_power": RpcSensorDescription( "b_act_power": RpcSensorDescription(
key="em", key="em",
sub_key="b_act_power", sub_key="b_act_power",
name="Phase B active power", name="Active power",
native_unit_of_measurement=UnitOfPower.WATT, native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER, device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
emeter_phase="B",
entity_class=RpcEmeterPhaseSensor,
), ),
"c_act_power": RpcSensorDescription( "c_act_power": RpcSensorDescription(
key="em", key="em",
sub_key="c_act_power", sub_key="c_act_power",
name="Phase C active power", name="Active power",
native_unit_of_measurement=UnitOfPower.WATT, native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER, device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
emeter_phase="C",
entity_class=RpcEmeterPhaseSensor,
), ),
"total_act_power": RpcSensorDescription( "total_act_power": RpcSensorDescription(
key="em", key="em",
@ -539,26 +567,32 @@ RPC_SENSORS: Final = {
"a_aprt_power": RpcSensorDescription( "a_aprt_power": RpcSensorDescription(
key="em", key="em",
sub_key="a_aprt_power", sub_key="a_aprt_power",
name="Phase A apparent power", name="Apparent power",
native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE, native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE,
device_class=SensorDeviceClass.APPARENT_POWER, device_class=SensorDeviceClass.APPARENT_POWER,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
emeter_phase="A",
entity_class=RpcEmeterPhaseSensor,
), ),
"b_aprt_power": RpcSensorDescription( "b_aprt_power": RpcSensorDescription(
key="em", key="em",
sub_key="b_aprt_power", sub_key="b_aprt_power",
name="Phase B apparent power", name="Apparent power",
native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE, native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE,
device_class=SensorDeviceClass.APPARENT_POWER, device_class=SensorDeviceClass.APPARENT_POWER,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
emeter_phase="B",
entity_class=RpcEmeterPhaseSensor,
), ),
"c_aprt_power": RpcSensorDescription( "c_aprt_power": RpcSensorDescription(
key="em", key="em",
sub_key="c_aprt_power", sub_key="c_aprt_power",
name="Phase C apparent power", name="Apparent power",
native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE, native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE,
device_class=SensorDeviceClass.APPARENT_POWER, device_class=SensorDeviceClass.APPARENT_POWER,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
emeter_phase="C",
entity_class=RpcEmeterPhaseSensor,
), ),
"aprt_power_em1": RpcSensorDescription( "aprt_power_em1": RpcSensorDescription(
key="em1", key="em1",
@ -586,23 +620,29 @@ RPC_SENSORS: Final = {
"a_pf": RpcSensorDescription( "a_pf": RpcSensorDescription(
key="em", key="em",
sub_key="a_pf", sub_key="a_pf",
name="Phase A power factor", name="Power factor",
device_class=SensorDeviceClass.POWER_FACTOR, device_class=SensorDeviceClass.POWER_FACTOR,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
emeter_phase="A",
entity_class=RpcEmeterPhaseSensor,
), ),
"b_pf": RpcSensorDescription( "b_pf": RpcSensorDescription(
key="em", key="em",
sub_key="b_pf", sub_key="b_pf",
name="Phase B power factor", name="Power factor",
device_class=SensorDeviceClass.POWER_FACTOR, device_class=SensorDeviceClass.POWER_FACTOR,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
emeter_phase="B",
entity_class=RpcEmeterPhaseSensor,
), ),
"c_pf": RpcSensorDescription( "c_pf": RpcSensorDescription(
key="em", key="em",
sub_key="c_pf", sub_key="c_pf",
name="Phase C power factor", name="Power factor",
device_class=SensorDeviceClass.POWER_FACTOR, device_class=SensorDeviceClass.POWER_FACTOR,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
emeter_phase="C",
entity_class=RpcEmeterPhaseSensor,
), ),
"voltage": RpcSensorDescription( "voltage": RpcSensorDescription(
key="switch", key="switch",
@ -684,29 +724,35 @@ RPC_SENSORS: Final = {
"a_voltage": RpcSensorDescription( "a_voltage": RpcSensorDescription(
key="em", key="em",
sub_key="a_voltage", sub_key="a_voltage",
name="Phase A voltage", name="Voltage",
native_unit_of_measurement=UnitOfElectricPotential.VOLT, native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE, device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
emeter_phase="A",
entity_class=RpcEmeterPhaseSensor,
), ),
"b_voltage": RpcSensorDescription( "b_voltage": RpcSensorDescription(
key="em", key="em",
sub_key="b_voltage", sub_key="b_voltage",
name="Phase B voltage", name="Voltage",
native_unit_of_measurement=UnitOfElectricPotential.VOLT, native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE, device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
emeter_phase="B",
entity_class=RpcEmeterPhaseSensor,
), ),
"c_voltage": RpcSensorDescription( "c_voltage": RpcSensorDescription(
key="em", key="em",
sub_key="c_voltage", sub_key="c_voltage",
name="Phase C voltage", name="Voltage",
native_unit_of_measurement=UnitOfElectricPotential.VOLT, native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE, device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
emeter_phase="C",
entity_class=RpcEmeterPhaseSensor,
), ),
"current": RpcSensorDescription( "current": RpcSensorDescription(
key="switch", key="switch",
@ -781,29 +827,35 @@ RPC_SENSORS: Final = {
"a_current": RpcSensorDescription( "a_current": RpcSensorDescription(
key="em", key="em",
sub_key="a_current", sub_key="a_current",
name="Phase A current", name="Current",
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT, device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
emeter_phase="A",
entity_class=RpcEmeterPhaseSensor,
), ),
"b_current": RpcSensorDescription( "b_current": RpcSensorDescription(
key="em", key="em",
sub_key="b_current", sub_key="b_current",
name="Phase B current", name="Current",
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT, device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
emeter_phase="B",
entity_class=RpcEmeterPhaseSensor,
), ),
"c_current": RpcSensorDescription( "c_current": RpcSensorDescription(
key="em", key="em",
sub_key="c_current", sub_key="c_current",
name="Phase C current", name="Current",
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT, device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
emeter_phase="C",
entity_class=RpcEmeterPhaseSensor,
), ),
"n_current": RpcSensorDescription( "n_current": RpcSensorDescription(
key="em", key="em",
@ -944,7 +996,7 @@ RPC_SENSORS: Final = {
"a_total_act_energy": RpcSensorDescription( "a_total_act_energy": RpcSensorDescription(
key="emdata", key="emdata",
sub_key="a_total_act_energy", sub_key="a_total_act_energy",
name="Phase A total active energy", name="Total active energy",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
value=lambda status, _: float(status), value=lambda status, _: float(status),
@ -952,11 +1004,13 @@ RPC_SENSORS: Final = {
device_class=SensorDeviceClass.ENERGY, device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING, state_class=SensorStateClass.TOTAL_INCREASING,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
emeter_phase="A",
entity_class=RpcEmeterPhaseSensor,
), ),
"b_total_act_energy": RpcSensorDescription( "b_total_act_energy": RpcSensorDescription(
key="emdata", key="emdata",
sub_key="b_total_act_energy", sub_key="b_total_act_energy",
name="Phase B total active energy", name="Total active energy",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
value=lambda status, _: float(status), value=lambda status, _: float(status),
@ -964,11 +1018,13 @@ RPC_SENSORS: Final = {
device_class=SensorDeviceClass.ENERGY, device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING, state_class=SensorStateClass.TOTAL_INCREASING,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
emeter_phase="B",
entity_class=RpcEmeterPhaseSensor,
), ),
"c_total_act_energy": RpcSensorDescription( "c_total_act_energy": RpcSensorDescription(
key="emdata", key="emdata",
sub_key="c_total_act_energy", sub_key="c_total_act_energy",
name="Phase C total active energy", name="Total active energy",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
value=lambda status, _: float(status), value=lambda status, _: float(status),
@ -976,6 +1032,8 @@ RPC_SENSORS: Final = {
device_class=SensorDeviceClass.ENERGY, device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING, state_class=SensorStateClass.TOTAL_INCREASING,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
emeter_phase="C",
entity_class=RpcEmeterPhaseSensor,
), ),
"total_act_ret": RpcSensorDescription( "total_act_ret": RpcSensorDescription(
key="emdata", key="emdata",
@ -1003,7 +1061,7 @@ RPC_SENSORS: Final = {
"a_total_act_ret_energy": RpcSensorDescription( "a_total_act_ret_energy": RpcSensorDescription(
key="emdata", key="emdata",
sub_key="a_total_act_ret_energy", sub_key="a_total_act_ret_energy",
name="Phase A total active returned energy", name="Total active returned energy",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
value=lambda status, _: float(status), value=lambda status, _: float(status),
@ -1011,11 +1069,13 @@ RPC_SENSORS: Final = {
device_class=SensorDeviceClass.ENERGY, device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING, state_class=SensorStateClass.TOTAL_INCREASING,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
emeter_phase="A",
entity_class=RpcEmeterPhaseSensor,
), ),
"b_total_act_ret_energy": RpcSensorDescription( "b_total_act_ret_energy": RpcSensorDescription(
key="emdata", key="emdata",
sub_key="b_total_act_ret_energy", sub_key="b_total_act_ret_energy",
name="Phase B total active returned energy", name="Total active returned energy",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
value=lambda status, _: float(status), value=lambda status, _: float(status),
@ -1023,11 +1083,13 @@ RPC_SENSORS: Final = {
device_class=SensorDeviceClass.ENERGY, device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING, state_class=SensorStateClass.TOTAL_INCREASING,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
emeter_phase="B",
entity_class=RpcEmeterPhaseSensor,
), ),
"c_total_act_ret_energy": RpcSensorDescription( "c_total_act_ret_energy": RpcSensorDescription(
key="emdata", key="emdata",
sub_key="c_total_act_ret_energy", sub_key="c_total_act_ret_energy",
name="Phase C total active returned energy", name="Total active returned energy",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
value=lambda status, _: float(status), value=lambda status, _: float(status),
@ -1035,6 +1097,8 @@ RPC_SENSORS: Final = {
device_class=SensorDeviceClass.ENERGY, device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING, state_class=SensorStateClass.TOTAL_INCREASING,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
emeter_phase="C",
entity_class=RpcEmeterPhaseSensor,
), ),
"freq": RpcSensorDescription( "freq": RpcSensorDescription(
key="switch", key="switch",
@ -1069,32 +1133,38 @@ RPC_SENSORS: Final = {
"a_freq": RpcSensorDescription( "a_freq": RpcSensorDescription(
key="em", key="em",
sub_key="a_freq", sub_key="a_freq",
name="Phase A frequency", name="Frequency",
native_unit_of_measurement=UnitOfFrequency.HERTZ, native_unit_of_measurement=UnitOfFrequency.HERTZ,
suggested_display_precision=0, suggested_display_precision=0,
device_class=SensorDeviceClass.FREQUENCY, device_class=SensorDeviceClass.FREQUENCY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
emeter_phase="A",
entity_class=RpcEmeterPhaseSensor,
), ),
"b_freq": RpcSensorDescription( "b_freq": RpcSensorDescription(
key="em", key="em",
sub_key="b_freq", sub_key="b_freq",
name="Phase B frequency", name="Frequency",
native_unit_of_measurement=UnitOfFrequency.HERTZ, native_unit_of_measurement=UnitOfFrequency.HERTZ,
suggested_display_precision=0, suggested_display_precision=0,
device_class=SensorDeviceClass.FREQUENCY, device_class=SensorDeviceClass.FREQUENCY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
emeter_phase="B",
entity_class=RpcEmeterPhaseSensor,
), ),
"c_freq": RpcSensorDescription( "c_freq": RpcSensorDescription(
key="em", key="em",
sub_key="c_freq", sub_key="c_freq",
name="Phase C frequency", name="Frequency",
native_unit_of_measurement=UnitOfFrequency.HERTZ, native_unit_of_measurement=UnitOfFrequency.HERTZ,
suggested_display_precision=0, suggested_display_precision=0,
device_class=SensorDeviceClass.FREQUENCY, device_class=SensorDeviceClass.FREQUENCY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
emeter_phase="C",
entity_class=RpcEmeterPhaseSensor,
), ),
"illuminance": RpcSensorDescription( "illuminance": RpcSensorDescription(
key="illuminance", key="illuminance",
@ -1107,7 +1177,7 @@ RPC_SENSORS: Final = {
"temperature": RpcSensorDescription( "temperature": RpcSensorDescription(
key="switch", key="switch",
sub_key="temperature", sub_key="temperature",
name="Device temperature", name="Temperature",
native_unit_of_measurement=UnitOfTemperature.CELSIUS, native_unit_of_measurement=UnitOfTemperature.CELSIUS,
value=lambda status, _: status["tC"], value=lambda status, _: status["tC"],
suggested_display_precision=1, suggested_display_precision=1,
@ -1120,7 +1190,7 @@ RPC_SENSORS: Final = {
"temperature_light": RpcSensorDescription( "temperature_light": RpcSensorDescription(
key="light", key="light",
sub_key="temperature", sub_key="temperature",
name="Device temperature", name="Temperature",
native_unit_of_measurement=UnitOfTemperature.CELSIUS, native_unit_of_measurement=UnitOfTemperature.CELSIUS,
value=lambda status, _: status["tC"], value=lambda status, _: status["tC"],
suggested_display_precision=1, suggested_display_precision=1,
@ -1133,7 +1203,7 @@ RPC_SENSORS: Final = {
"temperature_cct": RpcSensorDescription( "temperature_cct": RpcSensorDescription(
key="cct", key="cct",
sub_key="temperature", sub_key="temperature",
name="Device temperature", name="Temperature",
native_unit_of_measurement=UnitOfTemperature.CELSIUS, native_unit_of_measurement=UnitOfTemperature.CELSIUS,
value=lambda status, _: status["tC"], value=lambda status, _: status["tC"],
suggested_display_precision=1, suggested_display_precision=1,
@ -1146,7 +1216,7 @@ RPC_SENSORS: Final = {
"temperature_rgb": RpcSensorDescription( "temperature_rgb": RpcSensorDescription(
key="rgb", key="rgb",
sub_key="temperature", sub_key="temperature",
name="Device temperature", name="Temperature",
native_unit_of_measurement=UnitOfTemperature.CELSIUS, native_unit_of_measurement=UnitOfTemperature.CELSIUS,
value=lambda status, _: status["tC"], value=lambda status, _: status["tC"],
suggested_display_precision=1, suggested_display_precision=1,
@ -1159,7 +1229,7 @@ RPC_SENSORS: Final = {
"temperature_rgbw": RpcSensorDescription( "temperature_rgbw": RpcSensorDescription(
key="rgbw", key="rgbw",
sub_key="temperature", sub_key="temperature",
name="Device temperature", name="Temperature",
native_unit_of_measurement=UnitOfTemperature.CELSIUS, native_unit_of_measurement=UnitOfTemperature.CELSIUS,
value=lambda status, _: status["tC"], value=lambda status, _: status["tC"],
suggested_display_precision=1, suggested_display_precision=1,
@ -1308,12 +1378,10 @@ RPC_SENSORS: Final = {
"text": RpcSensorDescription( "text": RpcSensorDescription(
key="text", key="text",
sub_key="value", sub_key="value",
has_entity_name=True,
), ),
"number": RpcSensorDescription( "number": RpcSensorDescription(
key="number", key="number",
sub_key="value", sub_key="value",
has_entity_name=True,
unit=lambda config: config["meta"]["ui"]["unit"] unit=lambda config: config["meta"]["ui"]["unit"]
if config["meta"]["ui"]["unit"] if config["meta"]["ui"]["unit"]
else None, else None,
@ -1324,7 +1392,6 @@ RPC_SENSORS: Final = {
"enum": RpcSensorDescription( "enum": RpcSensorDescription(
key="enum", key="enum",
sub_key="value", sub_key="value",
has_entity_name=True,
options_fn=lambda config: config["options"], options_fn=lambda config: config["options"],
device_class=SensorDeviceClass.ENUM, device_class=SensorDeviceClass.ENUM,
), ),

View File

@ -291,7 +291,6 @@ class RpcSwitch(ShellyRpcAttributeEntity, SwitchEntity):
"""Entity that controls a switch on RPC based Shelly devices.""" """Entity that controls a switch on RPC based Shelly devices."""
entity_description: RpcSwitchDescription entity_description: RpcSwitchDescription
_attr_has_entity_name = True
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
@ -316,9 +315,6 @@ class RpcSwitch(ShellyRpcAttributeEntity, SwitchEntity):
class RpcRelaySwitch(RpcSwitch): class RpcRelaySwitch(RpcSwitch):
"""Entity that controls a switch on RPC based Shelly devices.""" """Entity that controls a switch on RPC based Shelly devices."""
# False to avoid double naming as True is inerithed from base class
_attr_has_entity_name = False
def __init__( def __init__(
self, self,
coordinator: ShellyRpcCoordinator, coordinator: ShellyRpcCoordinator,

View File

@ -40,7 +40,6 @@ RPC_TEXT_ENTITIES: Final = {
"text": RpcTextDescription( "text": RpcTextDescription(
key="text", key="text",
sub_key="value", sub_key="value",
has_entity_name=True,
), ),
} }

View File

@ -11,6 +11,8 @@ from aiohttp.web import Request, WebSocketResponse
from aioshelly.block_device import COAP, Block, BlockDevice from aioshelly.block_device import COAP, Block, BlockDevice
from aioshelly.const import ( from aioshelly.const import (
BLOCK_GENERATIONS, BLOCK_GENERATIONS,
BLU_TRV_IDENTIFIER,
BLU_TRV_MODEL_NAME,
DEFAULT_COAP_PORT, DEFAULT_COAP_PORT,
DEFAULT_HTTP_PORT, DEFAULT_HTTP_PORT,
MODEL_1L, MODEL_1L,
@ -40,7 +42,11 @@ from homeassistant.helpers import (
issue_registry as ir, issue_registry as ir,
singleton, singleton,
) )
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.device_registry import (
CONNECTION_BLUETOOTH,
CONNECTION_NETWORK_MAC,
DeviceInfo,
)
from homeassistant.helpers.network import NoURLAvailableError, get_url from homeassistant.helpers.network import NoURLAvailableError, get_url
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
@ -65,7 +71,9 @@ from .const import (
SHELLY_EMIT_EVENT_PATTERN, SHELLY_EMIT_EVENT_PATTERN,
SHIX3_1_INPUTS_EVENTS_TYPES, SHIX3_1_INPUTS_EVENTS_TYPES,
UPTIME_DEVIATION, UPTIME_DEVIATION,
VIRTUAL_COMPONENTS,
VIRTUAL_COMPONENTS_MAP, VIRTUAL_COMPONENTS_MAP,
All_LIGHT_TYPES,
) )
@ -109,26 +117,24 @@ def get_block_entity_name(
device: BlockDevice, device: BlockDevice,
block: Block | None, block: Block | None,
description: str | None = None, description: str | None = None,
) -> str: ) -> str | None:
"""Naming for block based switch and sensors.""" """Naming for block based switch and sensors."""
channel_name = get_block_channel_name(device, block) channel_name = get_block_channel_name(device, block)
if description: if description:
return f"{channel_name} {description.lower()}" return f"{channel_name} {description.lower()}" if channel_name else description
return channel_name return channel_name
def get_block_channel_name(device: BlockDevice, block: Block | None) -> str: def get_block_channel_name(device: BlockDevice, block: Block | None) -> str | None:
"""Get name based on device and channel name.""" """Get name based on device and channel name."""
entity_name = device.name
if ( if (
not block not block
or block.type == "device" or block.type in ("device", "light", "relay", "emeter")
or get_number_of_channels(device, block) == 1 or get_number_of_channels(device, block) == 1
): ):
return entity_name return None
assert block.channel assert block.channel
@ -140,12 +146,28 @@ def get_block_channel_name(device: BlockDevice, block: Block | None) -> str:
if channel_name: if channel_name:
return channel_name return channel_name
base = ord("1")
return f"Channel {chr(int(block.channel) + base)}"
def get_block_sub_device_name(device: BlockDevice, block: Block) -> str:
"""Get name of block sub-device."""
if TYPE_CHECKING:
assert block.channel
mode = cast(str, block.type) + "s"
if mode in device.settings:
if channel_name := device.settings[mode][int(block.channel)].get("name"):
return cast(str, channel_name)
if device.settings["device"]["type"] == MODEL_EM3: if device.settings["device"]["type"] == MODEL_EM3:
base = ord("A") base = ord("A")
else: return f"{device.name} Phase {chr(int(block.channel) + base)}"
base = ord("1")
return f"{entity_name} channel {chr(int(block.channel) + base)}" base = ord("1")
return f"{device.name} Channel {chr(int(block.channel) + base)}"
def is_block_momentary_input( def is_block_momentary_input(
@ -364,39 +386,64 @@ def get_shelly_model_name(
return cast(str, MODEL_NAMES.get(model)) return cast(str, MODEL_NAMES.get(model))
def get_rpc_channel_name(device: RpcDevice, key: str) -> str: def get_rpc_channel_name(device: RpcDevice, key: str) -> str | None:
"""Get name based on device and channel name.""" """Get name based on device and channel name."""
if BLU_TRV_IDENTIFIER in key:
return None
instances = len(
get_rpc_key_instances(device.status, key.split(":")[0], all_lights=True)
)
component = key.split(":")[0]
component_id = key.split(":")[-1]
if key in device.config and key != "em:0":
# workaround for Pro 3EM, we don't want to get name for em:0
if component_name := device.config[key].get("name"):
if component in (*VIRTUAL_COMPONENTS, "script"):
return cast(str, component_name)
return cast(str, component_name) if instances == 1 else None
if component in VIRTUAL_COMPONENTS:
return f"{component.title()} {component_id}"
return None
def get_rpc_sub_device_name(
device: RpcDevice, key: str, emeter_phase: str | None = None
) -> str:
"""Get name based on device and channel name."""
if key in device.config and key != "em:0":
# workaround for Pro 3EM, we don't want to get name for em:0
if entity_name := device.config[key].get("name"):
return cast(str, entity_name)
key = key.replace("emdata", "em") key = key.replace("emdata", "em")
key = key.replace("em1data", "em1") key = key.replace("em1data", "em1")
device_name = device.name
entity_name: str | None = None
if key in device.config:
entity_name = device.config[key].get("name")
if entity_name is None: component = key.split(":")[0]
channel = key.split(":")[0] component_id = key.split(":")[-1]
channel_id = key.split(":")[-1]
if key.startswith(("cover:", "input:", "light:", "switch:", "thermostat:")):
return f"{device_name} {channel.title()} {channel_id}"
if key.startswith(("cct", "rgb:", "rgbw:")):
return f"{device_name} {channel.upper()} light {channel_id}"
if key.startswith("em1"):
return f"{device_name} EM{channel_id}"
if key.startswith(("boolean:", "enum:", "number:", "text:")):
return f"{channel.title()} {channel_id}"
return device_name
return entity_name if component in ("cct", "rgb", "rgbw"):
return f"{device.name} {component.upper()} light {component_id}"
if component == "em1":
return f"{device.name} Energy Meter {component_id}"
if component == "em" and emeter_phase is not None:
return f"{device.name} Phase {emeter_phase}"
return f"{device.name} {component.title()} {component_id}"
def get_rpc_entity_name( def get_rpc_entity_name(
device: RpcDevice, key: str, description: str | None = None device: RpcDevice, key: str, description: str | None = None
) -> str: ) -> str | None:
"""Naming for RPC based switch and sensors.""" """Naming for RPC based switch and sensors."""
channel_name = get_rpc_channel_name(device, key) channel_name = get_rpc_channel_name(device, key)
if description: if description:
return f"{channel_name} {description.lower()}" return f"{channel_name} {description.lower()}" if channel_name else description
return channel_name return channel_name
@ -406,7 +453,9 @@ def get_device_entry_gen(entry: ConfigEntry) -> int:
return entry.data.get(CONF_GEN, 1) return entry.data.get(CONF_GEN, 1)
def get_rpc_key_instances(keys_dict: dict[str, Any], key: str) -> list[str]: def get_rpc_key_instances(
keys_dict: dict[str, Any], key: str, all_lights: bool = False
) -> list[str]:
"""Return list of key instances for RPC device from a dict.""" """Return list of key instances for RPC device from a dict."""
if key in keys_dict: if key in keys_dict:
return [key] return [key]
@ -414,6 +463,9 @@ def get_rpc_key_instances(keys_dict: dict[str, Any], key: str) -> list[str]:
if key == "switch" and "cover:0" in keys_dict: if key == "switch" and "cover:0" in keys_dict:
key = "cover" key = "cover"
if key in All_LIGHT_TYPES and all_lights:
return [k for k in keys_dict if k.startswith(All_LIGHT_TYPES)]
return [k for k in keys_dict if k.startswith(f"{key}:")] return [k for k in keys_dict if k.startswith(f"{key}:")]
@ -691,3 +743,81 @@ async def get_rpc_scripts_event_types(
script_events[script_id] = await get_rpc_script_event_types(device, script_id) script_events[script_id] = await get_rpc_script_event_types(device, script_id)
return script_events return script_events
def get_rpc_device_info(
device: RpcDevice,
mac: str,
key: str | None = None,
emeter_phase: str | None = None,
) -> DeviceInfo:
"""Return device info for RPC device."""
if key is None:
return DeviceInfo(connections={(CONNECTION_NETWORK_MAC, mac)})
# workaround for Pro EM50
key = key.replace("em1data", "em1")
# workaround for Pro 3EM
key = key.replace("emdata", "em")
key_parts = key.split(":")
component = key_parts[0]
idx = key_parts[1] if len(key_parts) > 1 else None
if emeter_phase is not None:
return DeviceInfo(
identifiers={(DOMAIN, f"{mac}-{key}-{emeter_phase.lower()}")},
name=get_rpc_sub_device_name(device, key, emeter_phase),
manufacturer="Shelly",
via_device=(DOMAIN, mac),
)
if (
component not in (*All_LIGHT_TYPES, "cover", "em1", "switch")
or idx is None
or len(get_rpc_key_instances(device.status, component, all_lights=True)) < 2
):
return DeviceInfo(connections={(CONNECTION_NETWORK_MAC, mac)})
return DeviceInfo(
identifiers={(DOMAIN, f"{mac}-{key}")},
name=get_rpc_sub_device_name(device, key),
manufacturer="Shelly",
via_device=(DOMAIN, mac),
)
def get_blu_trv_device_info(
config: dict[str, Any], ble_addr: str, parent_mac: str
) -> DeviceInfo:
"""Return device info for RPC device."""
model_id = config.get("local_name")
return DeviceInfo(
connections={(CONNECTION_BLUETOOTH, ble_addr)},
identifiers={(DOMAIN, ble_addr)},
via_device=(DOMAIN, parent_mac),
manufacturer="Shelly",
model=BLU_TRV_MODEL_NAME.get(model_id) if model_id else None,
model_id=config.get("local_name"),
name=config["name"] or f"shellyblutrv-{ble_addr.replace(':', '')}",
)
def get_block_device_info(
device: BlockDevice, mac: str, block: Block | None = None
) -> DeviceInfo:
"""Return device info for Block device."""
if (
block is None
or block.type not in ("light", "relay", "emeter")
or device.settings.get("mode") == "roller"
or get_number_of_channels(device, block) < 2
):
return DeviceInfo(connections={(CONNECTION_NETWORK_MAC, mac)})
return DeviceInfo(
identifiers={(DOMAIN, f"{mac}-{block.description}")},
name=get_block_sub_device_name(device, block),
manufacturer="Shelly",
via_device=(DOMAIN, mac),
)

View File

@ -53,7 +53,7 @@ async def init_integration(
data[CONF_GEN] = gen data[CONF_GEN] = gen
entry = MockConfigEntry( entry = MockConfigEntry(
domain=DOMAIN, data=data, unique_id=MOCK_MAC, options=options domain=DOMAIN, data=data, unique_id=MOCK_MAC, options=options, title="Test name"
) )
entry.add_to_hass(hass) entry.add_to_hass(hass)

View File

@ -189,7 +189,7 @@ MOCK_BLOCKS = [
] ]
MOCK_CONFIG = { MOCK_CONFIG = {
"input:0": {"id": 0, "name": "Test name input 0", "type": "button"}, "input:0": {"id": 0, "name": "Test input 0", "type": "button"},
"input:1": { "input:1": {
"id": 1, "id": 1,
"type": "analog", "type": "analog",
@ -204,7 +204,7 @@ MOCK_CONFIG = {
"xcounts": {"expr": None, "unit": None}, "xcounts": {"expr": None, "unit": None},
"xfreq": {"expr": None, "unit": None}, "xfreq": {"expr": None, "unit": None},
}, },
"flood:0": {"id": 0, "name": "Test name"}, "flood:0": {"id": 0, "name": "Kitchen"},
"light:0": {"name": "test light_0"}, "light:0": {"name": "test light_0"},
"light:1": {"name": "test light_1"}, "light:1": {"name": "test light_1"},
"light:2": {"name": "test light_2"}, "light:2": {"name": "test light_2"},

View File

@ -0,0 +1,259 @@
{
"config": {
"ble": {
"enable": true,
"rpc": {
"enable": true
}
},
"bthome": {},
"cloud": {
"enable": false,
"server": "iot.shelly.cloud:6012/jrpc"
},
"input:0": {
"enable": true,
"factory_reset": true,
"id": 0,
"invert": false,
"name": null,
"type": "switch"
},
"input:1": {
"enable": true,
"factory_reset": true,
"id": 1,
"invert": false,
"name": null,
"type": "switch"
},
"knx": {
"enable": false,
"ia": "15.15.255",
"routing": {
"addr": "224.0.23.12:3671"
}
},
"matter": {
"enable": false
},
"mqtt": {
"client_id": "shelly2pmg3-aabbccddeeff",
"enable": true,
"enable_control": true,
"enable_rpc": true,
"rpc_ntf": true,
"server": "mqtt.test.server",
"ssl_ca": null,
"status_ntf": true,
"topic_prefix": "shelly2pmg3-aabbccddeeff",
"use_client_cert": false,
"user": "iot"
},
"switch:0": {
"auto_off": false,
"auto_off_delay": 60.0,
"auto_on": false,
"auto_on_delay": 60.0,
"autorecover_voltage_errors": false,
"current_limit": 10.0,
"id": 0,
"in_locked": false,
"in_mode": "follow",
"initial_state": "match_input",
"name": null,
"power_limit": 2800,
"reverse": false,
"undervoltage_limit": 0,
"voltage_limit": 280
},
"switch:1": {
"auto_off": false,
"auto_off_delay": 60.0,
"auto_on": false,
"auto_on_delay": 60.0,
"autorecover_voltage_errors": false,
"current_limit": 10.0,
"id": 1,
"in_locked": false,
"in_mode": "follow",
"initial_state": "match_input",
"name": null,
"power_limit": 2800,
"reverse": false,
"undervoltage_limit": 0,
"voltage_limit": 280
},
"sys": {
"cfg_rev": 170,
"debug": {
"file_level": null,
"level": 2,
"mqtt": {
"enable": false
},
"udp": {
"addr": null
},
"websocket": {
"enable": true
}
},
"device": {
"addon_type": null,
"discoverable": true,
"eco_mode": true,
"fw_id": "20250508-110823/1.6.1-g8dbd358",
"mac": "AABBCCDDEEFF",
"name": "Test Name",
"profile": "switch"
},
"location": {
"lat": 15.2201,
"lon": 33.0121,
"tz": "Europe/Warsaw"
},
"rpc_udp": {
"dst_addr": null,
"listen_port": null
},
"sntp": {
"server": "sntp.test.server"
}
},
"wifi": {
"sta": {
"ssid": "Wifi-Network-Name",
"is_open": false,
"enable": true,
"ipv4mode": "dhcp",
"ip": null,
"netmask": null,
"gw": null,
"nameserver": null
}
},
"ws": {
"enable": false,
"server": null,
"ssl_ca": "ca.pem"
}
},
"shelly": {
"app": "S2PMG3",
"auth_domain": null,
"auth_en": false,
"fw_id": "20250508-110823/1.6.1-g8dbd358",
"gen": 3,
"id": "shelly2pmg3-aabbccddeeff",
"mac": "AABBCCDDEEFF",
"matter": false,
"model": "S3SW-002P16EU",
"name": "Test Name",
"profile": "switch",
"slot": 0,
"ver": "1.6.1"
},
"status": {
"ble": {},
"bthome": {},
"cloud": {
"connected": false
},
"input:0": {
"id": 0,
"state": false
},
"input:1": {
"id": 1,
"state": false
},
"knx": {},
"matter": {
"commissionable": false,
"num_fabrics": 0
},
"mqtt": {
"connected": true
},
"switch:0": {
"aenergy": {
"by_minute": [0.0, 0.0, 0.0],
"minute_ts": 1747488720,
"total": 0.0
},
"apower": 0.0,
"current": 0.0,
"freq": 50.0,
"id": 0,
"output": false,
"pf": 0.0,
"ret_aenergy": {
"by_minute": [0.0, 0.0, 0.0],
"minute_ts": 1747488720,
"total": 0.0
},
"source": "init",
"temperature": {
"tC": 40.6,
"tF": 105.1
},
"voltage": 216.2
},
"switch:1": {
"aenergy": {
"by_minute": [0.0, 0.0, 0.0],
"minute_ts": 1747488720,
"total": 0.0
},
"apower": 0.0,
"current": 0.0,
"freq": 50.0,
"id": 1,
"output": false,
"pf": 0.0,
"ret_aenergy": {
"by_minute": [0.0, 0.0, 0.0],
"minute_ts": 1747488720,
"total": 0.0
},
"source": "init",
"temperature": {
"tC": 40.6,
"tF": 105.1
},
"voltage": 216.3
},
"sys": {
"available_updates": {},
"btrelay_rev": 0,
"cfg_rev": 170,
"fs_free": 430080,
"fs_size": 917504,
"kvs_rev": 0,
"last_sync_ts": 1747488676,
"mac": "AABBCCDDEEFF",
"ram_free": 66440,
"ram_min_free": 49448,
"ram_size": 245788,
"reset_reason": 3,
"restart_required": false,
"schedule_rev": 22,
"time": "15:32",
"unixtime": 1747488776,
"uptime": 103,
"utc_offset": 7200,
"webhook_rev": 22
},
"wifi": {
"rssi": -52,
"ssid": "Wifi-Network-Name",
"sta_ip": "192.168.2.24",
"sta_ip6": [],
"status": "got ip"
},
"ws": {
"connected": false
}
}
}

View File

@ -0,0 +1,242 @@
{
"config": {
"ble": {
"enable": true,
"rpc": {
"enable": true
}
},
"bthome": {},
"cloud": {
"enable": false,
"server": "iot.shelly.cloud:6012/jrpc"
},
"cover:0": {
"current_limit": 10.0,
"id": 0,
"in_locked": false,
"in_mode": "dual",
"initial_state": "stopped",
"invert_directions": false,
"maintenance_mode": false,
"maxtime_close": 60.0,
"maxtime_open": 60.0,
"motor": {
"idle_confirm_period": 0.25,
"idle_power_thr": 2.0
},
"name": null,
"obstruction_detection": {
"action": "stop",
"direction": "both",
"enable": false,
"holdoff": 1.0,
"power_thr": 1000
},
"power_limit": 2800,
"safety_switch": {
"action": "stop",
"allowed_move": null,
"direction": "both",
"enable": false
},
"slat": {
"close_time": 1.5,
"enable": false,
"open_time": 1.5,
"precise_ctl": false,
"retain_pos": false,
"step": 20
},
"swap_inputs": false,
"undervoltage_limit": 0,
"voltage_limit": 280
},
"input:0": {
"enable": true,
"factory_reset": true,
"id": 0,
"invert": false,
"name": null,
"type": "switch"
},
"input:1": {
"enable": true,
"factory_reset": true,
"id": 1,
"invert": false,
"name": null,
"type": "switch"
},
"knx": {
"enable": false,
"ia": "15.15.255",
"routing": {
"addr": "224.0.23.12:3671"
}
},
"matter": {
"enable": false
},
"mqtt": {
"client_id": "shelly2pmg3-aabbccddeeff",
"enable": true,
"enable_control": true,
"enable_rpc": true,
"rpc_ntf": true,
"server": "mqtt.test.server",
"ssl_ca": null,
"status_ntf": true,
"topic_prefix": "shellies-gen3/shelly-2pm-gen3-365730",
"use_client_cert": false,
"user": "iot"
},
"sys": {
"cfg_rev": 171,
"debug": {
"file_level": null,
"level": 2,
"mqtt": {
"enable": false
},
"udp": {
"addr": null
},
"websocket": {
"enable": true
}
},
"device": {
"addon_type": null,
"discoverable": true,
"eco_mode": true,
"fw_id": "20250508-110823/1.6.1-g8dbd358",
"mac": "AABBCCDDEEFF",
"name": "Test Name",
"profile": "cover"
},
"location": {
"lat": 19.2201,
"lon": 34.0121,
"tz": "Europe/Warsaw"
},
"rpc_udp": {
"dst_addr": null,
"listen_port": null
},
"sntp": {
"server": "sntp.test.server"
},
"ui_data": {
"consumption_types": ["", "light"]
}
},
"wifi": {
"sta": {
"ssid": "Wifi-Network-Name",
"is_open": false,
"enable": true,
"ipv4mode": "dhcp",
"ip": null,
"netmask": null,
"gw": null,
"nameserver": null
}
},
"ws": {
"enable": false,
"server": null,
"ssl_ca": "ca.pem"
}
},
"shelly": {
"app": "S2PMG3",
"auth_domain": null,
"auth_en": false,
"fw_id": "20250508-110823/1.6.1-g8dbd358",
"gen": 3,
"id": "shelly2pmg3-aabbccddeeff",
"mac": "AABBCCDDEEFF",
"matter": false,
"model": "S3SW-002P16EU",
"name": "Test Name",
"profile": "cover",
"slot": 0,
"ver": "1.6.1"
},
"status": {
"ble": {},
"bthome": {},
"cloud": {
"connected": false
},
"cover:0": {
"aenergy": {
"by_minute": [0.0, 0.0, 0.0],
"minute_ts": 1747492440,
"total": 0.0
},
"apower": 0.0,
"current": 0.0,
"freq": 50.0,
"id": 0,
"last_direction": null,
"pf": 0.0,
"pos_control": false,
"source": "init",
"state": "stopped",
"temperature": {
"tC": 36.4,
"tF": 97.5
},
"voltage": 217.7
},
"input:0": {
"id": 0,
"state": false
},
"input:1": {
"id": 1,
"state": false
},
"knx": {},
"matter": {
"commissionable": false,
"num_fabrics": 0
},
"mqtt": {
"connected": true
},
"sys": {
"available_updates": {},
"btrelay_rev": 0,
"cfg_rev": 171,
"fs_free": 430080,
"fs_size": 917504,
"kvs_rev": 0,
"last_sync_ts": 1747492085,
"mac": "AABBCCDDEEFF",
"ram_free": 64632,
"ram_min_free": 51660,
"ram_size": 245568,
"reset_reason": 3,
"restart_required": false,
"schedule_rev": 23,
"time": "16:34",
"unixtime": 1747492463,
"uptime": 381,
"utc_offset": 7200,
"webhook_rev": 23
},
"wifi": {
"rssi": -53,
"ssid": "Wifi-Network-Name",
"sta_ip": "192.168.2.24",
"sta_ip6": [],
"status": "got ip"
},
"ws": {
"connected": false
}
}
}

View File

@ -0,0 +1,216 @@
{
"config": {
"ble": {
"enable": false,
"rpc": {
"enable": true
}
},
"bthome": {},
"cloud": {
"enable": false,
"server": "iot.shelly.cloud:6012/jrpc"
},
"em:0": {
"blink_mode_selector": "active_energy",
"ct_type": "120A",
"id": 0,
"monitor_phase_sequence": false,
"name": null,
"phase_selector": "all",
"reverse": {}
},
"emdata:0": {},
"eth": {
"enable": false,
"gw": null,
"ip": null,
"ipv4mode": "dhcp",
"nameserver": null,
"netmask": null,
"server_mode": false
},
"modbus": {
"enable": true
},
"mqtt": {
"client_id": "shellypro3em-aabbccddeeff",
"enable": false,
"enable_control": true,
"enable_rpc": true,
"rpc_ntf": true,
"server": "mqtt.test.server",
"ssl_ca": null,
"status_ntf": true,
"topic_prefix": "shellypro3em-aabbccddeeff",
"use_client_cert": false,
"user": "iot"
},
"sys": {
"cfg_rev": 50,
"debug": {
"file_level": null,
"level": 2,
"mqtt": {
"enable": false
},
"udp": {
"addr": null
},
"websocket": {
"enable": false
}
},
"device": {
"addon_type": null,
"discoverable": true,
"eco_mode": false,
"fw_id": "20250508-110717/1.6.1-g8dbd358",
"mac": "AABBCCDDEEFF",
"name": "Test Name",
"profile": "triphase",
"sys_btn_toggle": true
},
"location": {
"lat": 22.55775,
"lon": 54.94637,
"tz": "Europe/Warsaw"
},
"rpc_udp": {
"dst_addr": null,
"listen_port": null
},
"sntp": {
"server": "sntp.test.server"
},
"ui_data": {}
},
"temperature:0": {
"id": 0,
"name": null,
"offset_C": 0.0,
"report_thr_C": 5.0
},
"wifi": {
"sta": {
"ssid": "Wifi-Network-Name",
"is_open": false,
"enable": true,
"ipv4mode": "dhcp",
"ip": null,
"netmask": null,
"gw": null,
"nameserver": null
}
},
"ws": {
"enable": false,
"server": null,
"ssl_ca": "ca.pem"
}
},
"shelly": {
"app": "Pro3EM",
"auth_domain": "shellypro3em-aabbccddeeff",
"auth_en": true,
"fw_id": "20250508-110717/1.6.1-g8dbd358",
"gen": 2,
"id": "shellypro3em-aabbccddeeff",
"mac": "AABBCCDDEEFF",
"model": "SPEM-003CEBEU",
"name": "Test Name",
"profile": "triphase",
"slot": 0,
"ver": "1.6.1"
},
"status": {
"ble": {},
"bthome": {
"errors": ["bluetooth_disabled"]
},
"cloud": {
"connected": false
},
"em:0": {
"a_act_power": 2166.2,
"a_aprt_power": 2175.9,
"a_current": 9.592,
"a_freq": 49.9,
"a_pf": 0.99,
"a_voltage": 227.0,
"b_act_power": 3.6,
"b_aprt_power": 10.1,
"b_current": 0.044,
"b_freq": 49.9,
"b_pf": 0.36,
"b_voltage": 230.0,
"c_act_power": 244.0,
"c_aprt_power": 339.7,
"c_current": 1.479,
"c_freq": 49.9,
"c_pf": 0.72,
"c_voltage": 230.2,
"id": 0,
"n_current": null,
"total_act_power": 2413.825,
"total_aprt_power": 2525.779,
"total_current": 11.116,
"user_calibrated_phase": []
},
"emdata:0": {
"a_total_act_energy": 3105576.42,
"a_total_act_ret_energy": 0.0,
"b_total_act_energy": 195765.72,
"b_total_act_ret_energy": 0.0,
"c_total_act_energy": 2114072.05,
"c_total_act_ret_energy": 0.0,
"id": 0,
"total_act": 5415414.19,
"total_act_ret": 0.0
},
"eth": {
"ip": null,
"ip6": null
},
"modbus": {},
"mqtt": {
"connected": false
},
"sys": {
"available_updates": {},
"btrelay_rev": 0,
"cfg_rev": 50,
"fs_free": 180224,
"fs_size": 524288,
"kvs_rev": 1,
"last_sync_ts": 1747561099,
"mac": "AABBCCDDEEFF",
"ram_free": 113080,
"ram_min_free": 97524,
"ram_size": 247524,
"reset_reason": 3,
"restart_required": false,
"schedule_rev": 0,
"time": "11:38",
"unixtime": 1747561101,
"uptime": 501683,
"utc_offset": 7200,
"webhook_rev": 0
},
"temperature:0": {
"id": 0,
"tC": 46.3,
"tF": 115.4
},
"wifi": {
"rssi": -57,
"ssid": "Wifi-Network-Name",
"sta_ip": "192.168.2.151",
"sta_ip6": [],
"status": "got ip"
},
"ws": {
"connected": false
}
}
}

View File

@ -13,7 +13,7 @@
'domain': 'binary_sensor', 'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>, 'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.trv_name_calibration', 'entity_id': 'binary_sensor.trv_name_calibration',
'has_entity_name': False, 'has_entity_name': True,
'hidden_by': None, 'hidden_by': None,
'icon': None, 'icon': None,
'id': <ANY>, 'id': <ANY>,
@ -24,7 +24,7 @@
}), }),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>, 'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None, 'original_icon': None,
'original_name': 'TRV-Name calibration', 'original_name': 'Calibration',
'platform': 'shelly', 'platform': 'shelly',
'previous_unique_id': None, 'previous_unique_id': None,
'supported_features': 0, 'supported_features': 0,
@ -37,7 +37,7 @@
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'problem', 'device_class': 'problem',
'friendly_name': 'TRV-Name calibration', 'friendly_name': 'TRV-Name Calibration',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'binary_sensor.trv_name_calibration', 'entity_id': 'binary_sensor.trv_name_calibration',
@ -47,7 +47,7 @@
'state': 'off', 'state': 'off',
}) })
# --- # ---
# name: test_rpc_flood_entities[binary_sensor.test_name_flood-entry] # name: test_rpc_flood_entities[binary_sensor.test_name_kitchen_flood-entry]
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
}), }),
@ -60,8 +60,8 @@
'disabled_by': None, 'disabled_by': None,
'domain': 'binary_sensor', 'domain': 'binary_sensor',
'entity_category': None, 'entity_category': None,
'entity_id': 'binary_sensor.test_name_flood', 'entity_id': 'binary_sensor.test_name_kitchen_flood',
'has_entity_name': False, 'has_entity_name': True,
'hidden_by': None, 'hidden_by': None,
'icon': None, 'icon': None,
'id': <ANY>, 'id': <ANY>,
@ -72,7 +72,7 @@
}), }),
'original_device_class': <BinarySensorDeviceClass.MOISTURE: 'moisture'>, 'original_device_class': <BinarySensorDeviceClass.MOISTURE: 'moisture'>,
'original_icon': None, 'original_icon': None,
'original_name': 'Test name flood', 'original_name': 'Kitchen flood',
'platform': 'shelly', 'platform': 'shelly',
'previous_unique_id': None, 'previous_unique_id': None,
'supported_features': 0, 'supported_features': 0,
@ -81,21 +81,21 @@
'unit_of_measurement': None, 'unit_of_measurement': None,
}) })
# --- # ---
# name: test_rpc_flood_entities[binary_sensor.test_name_flood-state] # name: test_rpc_flood_entities[binary_sensor.test_name_kitchen_flood-state]
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'moisture', 'device_class': 'moisture',
'friendly_name': 'Test name flood', 'friendly_name': 'Test name Kitchen flood',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'binary_sensor.test_name_flood', 'entity_id': 'binary_sensor.test_name_kitchen_flood',
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'state': 'off', 'state': 'off',
}) })
# --- # ---
# name: test_rpc_flood_entities[binary_sensor.test_name_mute-entry] # name: test_rpc_flood_entities[binary_sensor.test_name_kitchen_mute-entry]
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
}), }),
@ -108,8 +108,8 @@
'disabled_by': None, 'disabled_by': None,
'domain': 'binary_sensor', 'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>, 'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.test_name_mute', 'entity_id': 'binary_sensor.test_name_kitchen_mute',
'has_entity_name': False, 'has_entity_name': True,
'hidden_by': None, 'hidden_by': None,
'icon': None, 'icon': None,
'id': <ANY>, 'id': <ANY>,
@ -120,7 +120,7 @@
}), }),
'original_device_class': None, 'original_device_class': None,
'original_icon': None, 'original_icon': None,
'original_name': 'Test name mute', 'original_name': 'Kitchen mute',
'platform': 'shelly', 'platform': 'shelly',
'previous_unique_id': None, 'previous_unique_id': None,
'supported_features': 0, 'supported_features': 0,
@ -129,13 +129,13 @@
'unit_of_measurement': None, 'unit_of_measurement': None,
}) })
# --- # ---
# name: test_rpc_flood_entities[binary_sensor.test_name_mute-state] # name: test_rpc_flood_entities[binary_sensor.test_name_kitchen_mute-state]
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'friendly_name': 'Test name mute', 'friendly_name': 'Test name Kitchen mute',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'binary_sensor.test_name_mute', 'entity_id': 'binary_sensor.test_name_kitchen_mute',
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,

View File

@ -13,7 +13,7 @@
'domain': 'button', 'domain': 'button',
'entity_category': <EntityCategory.CONFIG: 'config'>, 'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'button.trv_name_calibrate', 'entity_id': 'button.trv_name_calibrate',
'has_entity_name': False, 'has_entity_name': True,
'hidden_by': None, 'hidden_by': None,
'icon': None, 'icon': None,
'id': <ANY>, 'id': <ANY>,
@ -24,7 +24,7 @@
}), }),
'original_device_class': None, 'original_device_class': None,
'original_icon': None, 'original_icon': None,
'original_name': 'TRV-Name Calibrate', 'original_name': 'Calibrate',
'platform': 'shelly', 'platform': 'shelly',
'previous_unique_id': None, 'previous_unique_id': None,
'supported_features': 0, 'supported_features': 0,
@ -60,7 +60,7 @@
'domain': 'button', 'domain': 'button',
'entity_category': <EntityCategory.CONFIG: 'config'>, 'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'button.test_name_reboot', 'entity_id': 'button.test_name_reboot',
'has_entity_name': False, 'has_entity_name': True,
'hidden_by': None, 'hidden_by': None,
'icon': None, 'icon': None,
'id': <ANY>, 'id': <ANY>,
@ -71,7 +71,7 @@
}), }),
'original_device_class': <ButtonDeviceClass.RESTART: 'restart'>, 'original_device_class': <ButtonDeviceClass.RESTART: 'restart'>,
'original_icon': None, 'original_icon': None,
'original_name': 'Test name Reboot', 'original_name': 'Reboot',
'platform': 'shelly', 'platform': 'shelly',
'previous_unique_id': None, 'previous_unique_id': None,
'supported_features': 0, 'supported_features': 0,

View File

@ -90,7 +90,7 @@
'domain': 'climate', 'domain': 'climate',
'entity_category': None, 'entity_category': None,
'entity_id': 'climate.test_name', 'entity_id': 'climate.test_name',
'has_entity_name': False, 'has_entity_name': True,
'hidden_by': None, 'hidden_by': None,
'icon': None, 'icon': None,
'id': <ANY>, 'id': <ANY>,
@ -101,7 +101,7 @@
}), }),
'original_device_class': None, 'original_device_class': None,
'original_icon': None, 'original_icon': None,
'original_name': 'Test name', 'original_name': None,
'platform': 'shelly', 'platform': 'shelly',
'previous_unique_id': None, 'previous_unique_id': None,
'supported_features': <ClimateEntityFeature: 401>, 'supported_features': <ClimateEntityFeature: 401>,
@ -140,7 +140,7 @@
'state': 'off', 'state': 'off',
}) })
# --- # ---
# name: test_rpc_climate_hvac_mode[climate.test_name_thermostat_0-entry] # name: test_rpc_climate_hvac_mode[climate.test_name-entry]
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
}), }),
@ -161,8 +161,8 @@
'disabled_by': None, 'disabled_by': None,
'domain': 'climate', 'domain': 'climate',
'entity_category': None, 'entity_category': None,
'entity_id': 'climate.test_name_thermostat_0', 'entity_id': 'climate.test_name',
'has_entity_name': False, 'has_entity_name': True,
'hidden_by': None, 'hidden_by': None,
'icon': None, 'icon': None,
'id': <ANY>, 'id': <ANY>,
@ -173,7 +173,7 @@
}), }),
'original_device_class': None, 'original_device_class': None,
'original_icon': None, 'original_icon': None,
'original_name': 'Test name Thermostat 0', 'original_name': None,
'platform': 'shelly', 'platform': 'shelly',
'previous_unique_id': None, 'previous_unique_id': None,
'supported_features': <ClimateEntityFeature: 385>, 'supported_features': <ClimateEntityFeature: 385>,
@ -182,12 +182,12 @@
'unit_of_measurement': None, 'unit_of_measurement': None,
}) })
# --- # ---
# name: test_rpc_climate_hvac_mode[climate.test_name_thermostat_0-state] # name: test_rpc_climate_hvac_mode[climate.test_name-state]
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'current_humidity': 44.4, 'current_humidity': 44.4,
'current_temperature': 12.3, 'current_temperature': 12.3,
'friendly_name': 'Test name Thermostat 0', 'friendly_name': 'Test name',
'hvac_action': <HVACAction.HEATING: 'heating'>, 'hvac_action': <HVACAction.HEATING: 'heating'>,
'hvac_modes': list([ 'hvac_modes': list([
<HVACMode.OFF: 'off'>, <HVACMode.OFF: 'off'>,
@ -200,14 +200,14 @@
'temperature': 23, 'temperature': 23,
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'climate.test_name_thermostat_0', 'entity_id': 'climate.test_name',
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'state': 'heat', 'state': 'heat',
}) })
# --- # ---
# name: test_wall_display_thermostat_mode[climate.test_name_thermostat_0-entry] # name: test_wall_display_thermostat_mode[climate.test_name-entry]
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
}), }),
@ -228,8 +228,8 @@
'disabled_by': None, 'disabled_by': None,
'domain': 'climate', 'domain': 'climate',
'entity_category': None, 'entity_category': None,
'entity_id': 'climate.test_name_thermostat_0', 'entity_id': 'climate.test_name',
'has_entity_name': False, 'has_entity_name': True,
'hidden_by': None, 'hidden_by': None,
'icon': None, 'icon': None,
'id': <ANY>, 'id': <ANY>,
@ -240,7 +240,7 @@
}), }),
'original_device_class': None, 'original_device_class': None,
'original_icon': None, 'original_icon': None,
'original_name': 'Test name Thermostat 0', 'original_name': None,
'platform': 'shelly', 'platform': 'shelly',
'previous_unique_id': None, 'previous_unique_id': None,
'supported_features': <ClimateEntityFeature: 385>, 'supported_features': <ClimateEntityFeature: 385>,
@ -249,12 +249,12 @@
'unit_of_measurement': None, 'unit_of_measurement': None,
}) })
# --- # ---
# name: test_wall_display_thermostat_mode[climate.test_name_thermostat_0-state] # name: test_wall_display_thermostat_mode[climate.test_name-state]
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'current_humidity': 44.4, 'current_humidity': 44.4,
'current_temperature': 12.3, 'current_temperature': 12.3,
'friendly_name': 'Test name Thermostat 0', 'friendly_name': 'Test name',
'hvac_action': <HVACAction.HEATING: 'heating'>, 'hvac_action': <HVACAction.HEATING: 'heating'>,
'hvac_modes': list([ 'hvac_modes': list([
<HVACMode.OFF: 'off'>, <HVACMode.OFF: 'off'>,
@ -267,7 +267,7 @@
'temperature': 23, 'temperature': 23,
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'climate.test_name_thermostat_0', 'entity_id': 'climate.test_name',
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,

View File

@ -18,7 +18,7 @@
'domain': 'number', 'domain': 'number',
'entity_category': <EntityCategory.CONFIG: 'config'>, 'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'number.trv_name_external_temperature', 'entity_id': 'number.trv_name_external_temperature',
'has_entity_name': False, 'has_entity_name': True,
'hidden_by': None, 'hidden_by': None,
'icon': None, 'icon': None,
'id': <ANY>, 'id': <ANY>,
@ -29,7 +29,7 @@
}), }),
'original_device_class': None, 'original_device_class': None,
'original_icon': None, 'original_icon': None,
'original_name': 'TRV-Name external temperature', 'original_name': 'External temperature',
'platform': 'shelly', 'platform': 'shelly',
'previous_unique_id': None, 'previous_unique_id': None,
'supported_features': 0, 'supported_features': 0,
@ -41,7 +41,7 @@
# name: test_blu_trv_number_entity[number.trv_name_external_temperature-state] # name: test_blu_trv_number_entity[number.trv_name_external_temperature-state]
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'friendly_name': 'TRV-Name external temperature', 'friendly_name': 'TRV-Name External temperature',
'max': 50, 'max': 50,
'min': -50, 'min': -50,
'mode': <NumberMode.BOX: 'box'>, 'mode': <NumberMode.BOX: 'box'>,
@ -75,7 +75,7 @@
'domain': 'number', 'domain': 'number',
'entity_category': None, 'entity_category': None,
'entity_id': 'number.trv_name_valve_position', 'entity_id': 'number.trv_name_valve_position',
'has_entity_name': False, 'has_entity_name': True,
'hidden_by': None, 'hidden_by': None,
'icon': None, 'icon': None,
'id': <ANY>, 'id': <ANY>,
@ -86,7 +86,7 @@
}), }),
'original_device_class': None, 'original_device_class': None,
'original_icon': None, 'original_icon': None,
'original_name': 'TRV-Name valve position', 'original_name': 'Valve position',
'platform': 'shelly', 'platform': 'shelly',
'previous_unique_id': None, 'previous_unique_id': None,
'supported_features': 0, 'supported_features': 0,
@ -98,7 +98,7 @@
# name: test_blu_trv_number_entity[number.trv_name_valve_position-state] # name: test_blu_trv_number_entity[number.trv_name_valve_position-state]
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'friendly_name': 'TRV-Name valve position', 'friendly_name': 'TRV-Name Valve position',
'max': 100, 'max': 100,
'min': 0, 'min': 0,
'mode': <NumberMode.SLIDER: 'slider'>, 'mode': <NumberMode.SLIDER: 'slider'>,

View File

@ -15,7 +15,7 @@
'domain': 'sensor', 'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>, 'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.trv_name_battery', 'entity_id': 'sensor.trv_name_battery',
'has_entity_name': False, 'has_entity_name': True,
'hidden_by': None, 'hidden_by': None,
'icon': None, 'icon': None,
'id': <ANY>, 'id': <ANY>,
@ -26,7 +26,7 @@
}), }),
'original_device_class': <SensorDeviceClass.BATTERY: 'battery'>, 'original_device_class': <SensorDeviceClass.BATTERY: 'battery'>,
'original_icon': None, 'original_icon': None,
'original_name': 'TRV-Name battery', 'original_name': 'Battery',
'platform': 'shelly', 'platform': 'shelly',
'previous_unique_id': None, 'previous_unique_id': None,
'supported_features': 0, 'supported_features': 0,
@ -39,7 +39,7 @@
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'battery', 'device_class': 'battery',
'friendly_name': 'TRV-Name battery', 'friendly_name': 'TRV-Name Battery',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%', 'unit_of_measurement': '%',
}), }),
@ -67,7 +67,7 @@
'domain': 'sensor', 'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>, 'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.trv_name_signal_strength', 'entity_id': 'sensor.trv_name_signal_strength',
'has_entity_name': False, 'has_entity_name': True,
'hidden_by': None, 'hidden_by': None,
'icon': None, 'icon': None,
'id': <ANY>, 'id': <ANY>,
@ -78,7 +78,7 @@
}), }),
'original_device_class': <SensorDeviceClass.SIGNAL_STRENGTH: 'signal_strength'>, 'original_device_class': <SensorDeviceClass.SIGNAL_STRENGTH: 'signal_strength'>,
'original_icon': None, 'original_icon': None,
'original_name': 'TRV-Name signal strength', 'original_name': 'Signal strength',
'platform': 'shelly', 'platform': 'shelly',
'previous_unique_id': None, 'previous_unique_id': None,
'supported_features': 0, 'supported_features': 0,
@ -91,7 +91,7 @@
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'signal_strength', 'device_class': 'signal_strength',
'friendly_name': 'TRV-Name signal strength', 'friendly_name': 'TRV-Name Signal strength',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'dBm', 'unit_of_measurement': 'dBm',
}), }),
@ -119,7 +119,7 @@
'domain': 'sensor', 'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>, 'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.trv_name_valve_position', 'entity_id': 'sensor.trv_name_valve_position',
'has_entity_name': False, 'has_entity_name': True,
'hidden_by': None, 'hidden_by': None,
'icon': None, 'icon': None,
'id': <ANY>, 'id': <ANY>,
@ -130,7 +130,7 @@
}), }),
'original_device_class': None, 'original_device_class': None,
'original_icon': None, 'original_icon': None,
'original_name': 'TRV-Name valve position', 'original_name': 'Valve position',
'platform': 'shelly', 'platform': 'shelly',
'previous_unique_id': None, 'previous_unique_id': None,
'supported_features': 0, 'supported_features': 0,
@ -142,7 +142,7 @@
# name: test_blu_trv_sensor_entity[sensor.trv_name_valve_position-state] # name: test_blu_trv_sensor_entity[sensor.trv_name_valve_position-state]
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'friendly_name': 'TRV-Name valve position', 'friendly_name': 'TRV-Name Valve position',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%', 'unit_of_measurement': '%',
}), }),
@ -154,7 +154,7 @@
'state': '0', 'state': '0',
}) })
# --- # ---
# name: test_rpc_switch_energy_sensors[sensor.test_switch_0_energy-entry] # name: test_rpc_switch_energy_sensors[sensor.test_name_test_switch_0_energy-entry]
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
}), }),
@ -169,8 +169,8 @@
'disabled_by': None, 'disabled_by': None,
'domain': 'sensor', 'domain': 'sensor',
'entity_category': None, 'entity_category': None,
'entity_id': 'sensor.test_switch_0_energy', 'entity_id': 'sensor.test_name_test_switch_0_energy',
'has_entity_name': False, 'has_entity_name': True,
'hidden_by': None, 'hidden_by': None,
'icon': None, 'icon': None,
'id': <ANY>, 'id': <ANY>,
@ -196,23 +196,23 @@
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>, 'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}) })
# --- # ---
# name: test_rpc_switch_energy_sensors[sensor.test_switch_0_energy-state] # name: test_rpc_switch_energy_sensors[sensor.test_name_test_switch_0_energy-state]
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'energy', 'device_class': 'energy',
'friendly_name': 'test switch_0 energy', 'friendly_name': 'Test name test switch_0 energy',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>, 'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>, 'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.test_switch_0_energy', 'entity_id': 'sensor.test_name_test_switch_0_energy',
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'state': '1234.56789', 'state': '1234.56789',
}) })
# --- # ---
# name: test_rpc_switch_energy_sensors[sensor.test_switch_0_returned_energy-entry] # name: test_rpc_switch_energy_sensors[sensor.test_name_test_switch_0_returned_energy-entry]
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
}), }),
@ -227,8 +227,8 @@
'disabled_by': None, 'disabled_by': None,
'domain': 'sensor', 'domain': 'sensor',
'entity_category': None, 'entity_category': None,
'entity_id': 'sensor.test_switch_0_returned_energy', 'entity_id': 'sensor.test_name_test_switch_0_returned_energy',
'has_entity_name': False, 'has_entity_name': True,
'hidden_by': None, 'hidden_by': None,
'icon': None, 'icon': None,
'id': <ANY>, 'id': <ANY>,
@ -254,16 +254,16 @@
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>, 'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}) })
# --- # ---
# name: test_rpc_switch_energy_sensors[sensor.test_switch_0_returned_energy-state] # name: test_rpc_switch_energy_sensors[sensor.test_name_test_switch_0_returned_energy-state]
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'energy', 'device_class': 'energy',
'friendly_name': 'test switch_0 returned energy', 'friendly_name': 'Test name test switch_0 returned energy',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>, 'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>, 'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.test_switch_0_returned_energy', 'entity_id': 'sensor.test_name_test_switch_0_returned_energy',
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,

View File

@ -36,7 +36,8 @@ async def test_block_binary_sensor(
entity_registry: EntityRegistry, entity_registry: EntityRegistry,
) -> None: ) -> None:
"""Test block binary sensor.""" """Test block binary sensor."""
entity_id = f"{BINARY_SENSOR_DOMAIN}.test_name_channel_1_overpowering" monkeypatch.setitem(mock_block_device.shelly, "num_outputs", 1)
entity_id = f"{BINARY_SENSOR_DOMAIN}.test_name_overpowering"
await init_integration(hass, 1) await init_integration(hass, 1)
assert (state := hass.states.get(entity_id)) assert (state := hass.states.get(entity_id))
@ -239,7 +240,7 @@ async def test_rpc_binary_sensor(
entity_registry: EntityRegistry, entity_registry: EntityRegistry,
) -> None: ) -> None:
"""Test RPC binary sensor.""" """Test RPC binary sensor."""
entity_id = f"{BINARY_SENSOR_DOMAIN}.test_cover_0_overpowering" entity_id = f"{BINARY_SENSOR_DOMAIN}.test_name_test_cover_0_overpowering"
await init_integration(hass, 2) await init_integration(hass, 2)
assert (state := hass.states.get(entity_id)) assert (state := hass.states.get(entity_id))
@ -521,7 +522,7 @@ async def test_rpc_flood_entities(
await init_integration(hass, 4) await init_integration(hass, 4)
for entity in ("flood", "mute"): for entity in ("flood", "mute"):
entity_id = f"{BINARY_SENSOR_DOMAIN}.test_name_{entity}" entity_id = f"{BINARY_SENSOR_DOMAIN}.test_name_kitchen_{entity}"
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
assert state == snapshot(name=f"{entity_id}-state") assert state == snapshot(name=f"{entity_id}-state")

View File

@ -613,7 +613,7 @@ async def test_rpc_climate_hvac_mode(
snapshot: SnapshotAssertion, snapshot: SnapshotAssertion,
) -> None: ) -> None:
"""Test climate hvac mode service.""" """Test climate hvac mode service."""
entity_id = "climate.test_name_thermostat_0" entity_id = "climate.test_name"
await init_integration(hass, 2, model=MODEL_WALL_DISPLAY) await init_integration(hass, 2, model=MODEL_WALL_DISPLAY)
@ -651,7 +651,7 @@ async def test_rpc_climate_without_humidity(
monkeypatch: pytest.MonkeyPatch, monkeypatch: pytest.MonkeyPatch,
) -> None: ) -> None:
"""Test climate entity without the humidity value.""" """Test climate entity without the humidity value."""
entity_id = "climate.test_name_thermostat_0" entity_id = "climate.test_name"
new_status = deepcopy(mock_rpc_device.status) new_status = deepcopy(mock_rpc_device.status)
new_status.pop("humidity:0") new_status.pop("humidity:0")
monkeypatch.setattr(mock_rpc_device, "status", new_status) monkeypatch.setattr(mock_rpc_device, "status", new_status)
@ -673,7 +673,7 @@ async def test_rpc_climate_set_temperature(
hass: HomeAssistant, mock_rpc_device: Mock, monkeypatch: pytest.MonkeyPatch hass: HomeAssistant, mock_rpc_device: Mock, monkeypatch: pytest.MonkeyPatch
) -> None: ) -> None:
"""Test climate set target temperature.""" """Test climate set target temperature."""
entity_id = "climate.test_name_thermostat_0" entity_id = "climate.test_name"
await init_integration(hass, 2, model=MODEL_WALL_DISPLAY) await init_integration(hass, 2, model=MODEL_WALL_DISPLAY)
@ -700,7 +700,7 @@ async def test_rpc_climate_hvac_mode_cool(
hass: HomeAssistant, mock_rpc_device: Mock, monkeypatch: pytest.MonkeyPatch hass: HomeAssistant, mock_rpc_device: Mock, monkeypatch: pytest.MonkeyPatch
) -> None: ) -> None:
"""Test climate with hvac mode cooling.""" """Test climate with hvac mode cooling."""
entity_id = "climate.test_name_thermostat_0" entity_id = "climate.test_name"
new_config = deepcopy(mock_rpc_device.config) new_config = deepcopy(mock_rpc_device.config)
new_config["thermostat:0"]["type"] = "cooling" new_config["thermostat:0"]["type"] = "cooling"
monkeypatch.setattr(mock_rpc_device, "config", new_config) monkeypatch.setattr(mock_rpc_device, "config", new_config)
@ -720,8 +720,8 @@ async def test_wall_display_thermostat_mode(
snapshot: SnapshotAssertion, snapshot: SnapshotAssertion,
) -> None: ) -> None:
"""Test Wall Display in thermostat mode.""" """Test Wall Display in thermostat mode."""
climate_entity_id = "climate.test_name_thermostat_0" climate_entity_id = "climate.test_name"
switch_entity_id = "switch.test_switch_0" switch_entity_id = "switch.test_name_test_switch_0"
await init_integration(hass, 2, model=MODEL_WALL_DISPLAY) await init_integration(hass, 2, model=MODEL_WALL_DISPLAY)
@ -745,8 +745,8 @@ async def test_wall_display_thermostat_mode_external_actuator(
monkeypatch: pytest.MonkeyPatch, monkeypatch: pytest.MonkeyPatch,
) -> None: ) -> None:
"""Test Wall Display in thermostat mode with an external actuator.""" """Test Wall Display in thermostat mode with an external actuator."""
climate_entity_id = "climate.test_name_thermostat_0" climate_entity_id = "climate.test_name"
switch_entity_id = "switch.test_switch_0" switch_entity_id = "switch.test_name_test_switch_0"
new_status = deepcopy(mock_rpc_device.status) new_status = deepcopy(mock_rpc_device.status)
new_status["sys"]["relay_in_thermostat"] = False new_status["sys"]["relay_in_thermostat"] = False

View File

@ -56,6 +56,8 @@ async def test_block_reload_on_cfg_change(
) -> None: ) -> None:
"""Test block reload on config change.""" """Test block reload on config change."""
await init_integration(hass, 1) await init_integration(hass, 1)
# num_outputs is 2, devicename and channel name is used
entity_id = "switch.test_name_channel_1"
monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "cfgChanged", 1) monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "cfgChanged", 1)
mock_block_device.mock_update() mock_block_device.mock_update()
@ -71,7 +73,7 @@ async def test_block_reload_on_cfg_change(
async_fire_time_changed(hass) async_fire_time_changed(hass)
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.states.get("switch.test_name_channel_1") assert hass.states.get(entity_id)
# Generate config change from switch to light # Generate config change from switch to light
monkeypatch.setitem( monkeypatch.setitem(
@ -81,14 +83,14 @@ async def test_block_reload_on_cfg_change(
mock_block_device.mock_update() mock_block_device.mock_update()
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.states.get("switch.test_name_channel_1") assert hass.states.get(entity_id)
# Wait for debouncer # Wait for debouncer
freezer.tick(timedelta(seconds=ENTRY_RELOAD_COOLDOWN)) freezer.tick(timedelta(seconds=ENTRY_RELOAD_COOLDOWN))
async_fire_time_changed(hass) async_fire_time_changed(hass)
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.states.get("switch.test_name_channel_1") is None assert hass.states.get(entity_id) is None
async def test_block_no_reload_on_bulb_changes( async def test_block_no_reload_on_bulb_changes(
@ -98,6 +100,9 @@ async def test_block_no_reload_on_bulb_changes(
monkeypatch: pytest.MonkeyPatch, monkeypatch: pytest.MonkeyPatch,
) -> None: ) -> None:
"""Test block no reload on bulb mode/effect change.""" """Test block no reload on bulb mode/effect change."""
monkeypatch.setitem(mock_block_device.shelly, "num_outputs", 1)
# num_outputs is 1, device name is used
entity_id = "switch.test_name"
await init_integration(hass, 1, model=MODEL_BULB) await init_integration(hass, 1, model=MODEL_BULB)
monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "cfgChanged", 1) monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "cfgChanged", 1)
@ -113,14 +118,14 @@ async def test_block_no_reload_on_bulb_changes(
mock_block_device.mock_update() mock_block_device.mock_update()
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.states.get("switch.test_name_channel_1") assert hass.states.get(entity_id)
# Wait for debouncer # Wait for debouncer
freezer.tick(timedelta(seconds=ENTRY_RELOAD_COOLDOWN)) freezer.tick(timedelta(seconds=ENTRY_RELOAD_COOLDOWN))
async_fire_time_changed(hass) async_fire_time_changed(hass)
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.states.get("switch.test_name_channel_1") assert hass.states.get(entity_id)
# Test no reload on effect change # Test no reload on effect change
monkeypatch.setattr(mock_block_device.blocks[LIGHT_BLOCK_ID], "effect", 1) monkeypatch.setattr(mock_block_device.blocks[LIGHT_BLOCK_ID], "effect", 1)
@ -128,14 +133,14 @@ async def test_block_no_reload_on_bulb_changes(
mock_block_device.mock_update() mock_block_device.mock_update()
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.states.get("switch.test_name_channel_1") assert hass.states.get(entity_id)
# Wait for debouncer # Wait for debouncer
freezer.tick(timedelta(seconds=ENTRY_RELOAD_COOLDOWN)) freezer.tick(timedelta(seconds=ENTRY_RELOAD_COOLDOWN))
async_fire_time_changed(hass) async_fire_time_changed(hass)
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.states.get("switch.test_name_channel_1") assert hass.states.get(entity_id)
async def test_block_polling_auth_error( async def test_block_polling_auth_error(
@ -242,9 +247,11 @@ async def test_block_polling_connection_error(
"update", "update",
AsyncMock(side_effect=DeviceConnectionError), AsyncMock(side_effect=DeviceConnectionError),
) )
# num_outputs is 2, device name and channel name is used
entity_id = "switch.test_name_channel_1"
await init_integration(hass, 1) await init_integration(hass, 1)
assert (state := hass.states.get("switch.test_name_channel_1")) assert (state := hass.states.get(entity_id))
assert state.state == STATE_ON assert state.state == STATE_ON
# Move time to generate polling # Move time to generate polling
@ -252,7 +259,7 @@ async def test_block_polling_connection_error(
async_fire_time_changed(hass) async_fire_time_changed(hass)
await hass.async_block_till_done() await hass.async_block_till_done()
assert (state := hass.states.get("switch.test_name_channel_1")) assert (state := hass.states.get(entity_id))
assert state.state == STATE_UNAVAILABLE assert state.state == STATE_UNAVAILABLE
@ -391,6 +398,7 @@ async def test_rpc_reload_on_cfg_change(
monkeypatch: pytest.MonkeyPatch, monkeypatch: pytest.MonkeyPatch,
) -> None: ) -> None:
"""Test RPC reload on config change.""" """Test RPC reload on config change."""
entity_id = "switch.test_name_test_switch_0"
monkeypatch.delitem(mock_rpc_device.status, "cover:0") monkeypatch.delitem(mock_rpc_device.status, "cover:0")
monkeypatch.setitem(mock_rpc_device.status["sys"], "relay_in_thermostat", False) monkeypatch.setitem(mock_rpc_device.status["sys"], "relay_in_thermostat", False)
await init_integration(hass, 2) await init_integration(hass, 2)
@ -421,14 +429,14 @@ async def test_rpc_reload_on_cfg_change(
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.states.get("switch.test_switch_0") assert hass.states.get(entity_id)
# Wait for debouncer # Wait for debouncer
freezer.tick(timedelta(seconds=ENTRY_RELOAD_COOLDOWN)) freezer.tick(timedelta(seconds=ENTRY_RELOAD_COOLDOWN))
async_fire_time_changed(hass) async_fire_time_changed(hass)
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.states.get("switch.test_switch_0") is None assert hass.states.get(entity_id) is None
async def test_rpc_reload_with_invalid_auth( async def test_rpc_reload_with_invalid_auth(
@ -719,11 +727,12 @@ async def test_rpc_reconnect_error(
exc: Exception, exc: Exception,
) -> None: ) -> None:
"""Test RPC reconnect error.""" """Test RPC reconnect error."""
entity_id = "switch.test_name_test_switch_0"
monkeypatch.delitem(mock_rpc_device.status, "cover:0") monkeypatch.delitem(mock_rpc_device.status, "cover:0")
monkeypatch.setitem(mock_rpc_device.status["sys"], "relay_in_thermostat", False) monkeypatch.setitem(mock_rpc_device.status["sys"], "relay_in_thermostat", False)
await init_integration(hass, 2) await init_integration(hass, 2)
assert (state := hass.states.get("switch.test_switch_0")) assert (state := hass.states.get(entity_id))
assert state.state == STATE_ON assert state.state == STATE_ON
monkeypatch.setattr(mock_rpc_device, "connected", False) monkeypatch.setattr(mock_rpc_device, "connected", False)
@ -734,7 +743,7 @@ async def test_rpc_reconnect_error(
async_fire_time_changed(hass) async_fire_time_changed(hass)
await hass.async_block_till_done() await hass.async_block_till_done()
assert (state := hass.states.get("switch.test_switch_0")) assert (state := hass.states.get(entity_id))
assert state.state == STATE_UNAVAILABLE assert state.state == STATE_UNAVAILABLE
@ -746,6 +755,7 @@ async def test_rpc_error_running_connected_events(
caplog: pytest.LogCaptureFixture, caplog: pytest.LogCaptureFixture,
) -> None: ) -> None:
"""Test RPC error while running connected events.""" """Test RPC error while running connected events."""
entity_id = "switch.test_name_test_switch_0"
monkeypatch.delitem(mock_rpc_device.status, "cover:0") monkeypatch.delitem(mock_rpc_device.status, "cover:0")
monkeypatch.setitem(mock_rpc_device.status["sys"], "relay_in_thermostat", False) monkeypatch.setitem(mock_rpc_device.status["sys"], "relay_in_thermostat", False)
with patch( with patch(
@ -758,7 +768,7 @@ async def test_rpc_error_running_connected_events(
assert "Error running connected events for device" in caplog.text assert "Error running connected events for device" in caplog.text
assert (state := hass.states.get("switch.test_switch_0")) assert (state := hass.states.get(entity_id))
assert state.state == STATE_UNAVAILABLE assert state.state == STATE_UNAVAILABLE
# Move time to generate reconnect without error # Move time to generate reconnect without error
@ -766,7 +776,7 @@ async def test_rpc_error_running_connected_events(
async_fire_time_changed(hass) async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True) await hass.async_block_till_done(wait_background_tasks=True)
assert (state := hass.states.get("switch.test_switch_0")) assert (state := hass.states.get(entity_id))
assert state.state == STATE_ON assert state.state == STATE_ON

View File

@ -116,7 +116,7 @@ async def test_rpc_device_services(
entity_registry: EntityRegistry, entity_registry: EntityRegistry,
) -> None: ) -> None:
"""Test RPC device cover services.""" """Test RPC device cover services."""
entity_id = "cover.test_cover_0" entity_id = "cover.test_name_test_cover_0"
await init_integration(hass, 2) await init_integration(hass, 2)
await hass.services.async_call( await hass.services.async_call(
@ -178,23 +178,24 @@ async def test_rpc_device_no_cover_keys(
monkeypatch.delitem(mock_rpc_device.status, "cover:0") monkeypatch.delitem(mock_rpc_device.status, "cover:0")
await init_integration(hass, 2) await init_integration(hass, 2)
assert hass.states.get("cover.test_cover_0") is None assert hass.states.get("cover.test_name_test_cover_0") is None
async def test_rpc_device_update( async def test_rpc_device_update(
hass: HomeAssistant, mock_rpc_device: Mock, monkeypatch: pytest.MonkeyPatch hass: HomeAssistant, mock_rpc_device: Mock, monkeypatch: pytest.MonkeyPatch
) -> None: ) -> None:
"""Test RPC device update.""" """Test RPC device update."""
entity_id = "cover.test_name_test_cover_0"
mutate_rpc_device_status(monkeypatch, mock_rpc_device, "cover:0", "state", "closed") mutate_rpc_device_status(monkeypatch, mock_rpc_device, "cover:0", "state", "closed")
await init_integration(hass, 2) await init_integration(hass, 2)
state = hass.states.get("cover.test_cover_0") state = hass.states.get(entity_id)
assert state assert state
assert state.state == CoverState.CLOSED assert state.state == CoverState.CLOSED
mutate_rpc_device_status(monkeypatch, mock_rpc_device, "cover:0", "state", "open") mutate_rpc_device_status(monkeypatch, mock_rpc_device, "cover:0", "state", "open")
mock_rpc_device.mock_update() mock_rpc_device.mock_update()
state = hass.states.get("cover.test_cover_0") state = hass.states.get(entity_id)
assert state assert state
assert state.state == CoverState.OPEN assert state.state == CoverState.OPEN
@ -208,7 +209,7 @@ async def test_rpc_device_no_position_control(
) )
await init_integration(hass, 2) await init_integration(hass, 2)
state = hass.states.get("cover.test_cover_0") state = hass.states.get("cover.test_name_test_cover_0")
assert state assert state
assert state.state == CoverState.OPEN assert state.state == CoverState.OPEN
@ -220,7 +221,7 @@ async def test_rpc_cover_tilt(
entity_registry: EntityRegistry, entity_registry: EntityRegistry,
) -> None: ) -> None:
"""Test RPC cover that supports tilt.""" """Test RPC cover that supports tilt."""
entity_id = "cover.test_cover_0" entity_id = "cover.test_name_test_cover_0"
config = deepcopy(mock_rpc_device.config) config = deepcopy(mock_rpc_device.config)
config["cover:0"]["slat"] = {"enable": True} config["cover:0"]["slat"] = {"enable": True}

View File

@ -0,0 +1,479 @@
"""Test real devices."""
from unittest.mock import Mock
from aioshelly.const import MODEL_2PM_G3, MODEL_PRO_EM3
import pytest
from homeassistant.components.shelly.const import DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceRegistry
from homeassistant.helpers.entity_registry import EntityRegistry
from . import init_integration
from tests.common import load_json_object_fixture
async def test_shelly_2pm_gen3_no_relay_names(
hass: HomeAssistant,
mock_rpc_device: Mock,
entity_registry: EntityRegistry,
device_registry: DeviceRegistry,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Test Shelly 2PM Gen3 without relay names.
This device has two relays/channels,we should get a main device and two sub
devices.
"""
device_fixture = load_json_object_fixture("2pm_gen3.json", DOMAIN)
monkeypatch.setattr(mock_rpc_device, "shelly", device_fixture["shelly"])
monkeypatch.setattr(mock_rpc_device, "status", device_fixture["status"])
monkeypatch.setattr(mock_rpc_device, "config", device_fixture["config"])
await init_integration(hass, gen=3, model=MODEL_2PM_G3)
# Relay 0 sub-device
entity_id = "switch.test_name_switch_0"
state = hass.states.get(entity_id)
assert state
entry = entity_registry.async_get(entity_id)
assert entry
device_entry = device_registry.async_get(entry.device_id)
assert device_entry
assert device_entry.name == "Test name Switch 0"
entity_id = "sensor.test_name_switch_0_power"
state = hass.states.get(entity_id)
assert state
entry = entity_registry.async_get(entity_id)
assert entry
device_entry = device_registry.async_get(entry.device_id)
assert device_entry
assert device_entry.name == "Test name Switch 0"
# Relay 1 sub-device
entity_id = "switch.test_name_switch_1"
state = hass.states.get(entity_id)
assert state
entry = entity_registry.async_get(entity_id)
assert entry
device_entry = device_registry.async_get(entry.device_id)
assert device_entry
assert device_entry.name == "Test name Switch 1"
entity_id = "sensor.test_name_switch_1_power"
state = hass.states.get(entity_id)
assert state
entry = entity_registry.async_get(entity_id)
assert entry
device_entry = device_registry.async_get(entry.device_id)
assert device_entry
assert device_entry.name == "Test name Switch 1"
# Main device
entity_id = "update.test_name_firmware"
state = hass.states.get(entity_id)
assert state
entry = entity_registry.async_get(entity_id)
assert entry
device_entry = device_registry.async_get(entry.device_id)
assert device_entry
assert device_entry.name == "Test name"
async def test_shelly_2pm_gen3_relay_names(
hass: HomeAssistant,
mock_rpc_device: Mock,
entity_registry: EntityRegistry,
device_registry: DeviceRegistry,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Test Shelly 2PM Gen3 with relay names.
This device has two relays/channels,we should get a main device and two sub
devices.
"""
device_fixture = load_json_object_fixture("2pm_gen3.json", DOMAIN)
device_fixture["config"]["switch:0"]["name"] = "Kitchen light"
device_fixture["config"]["switch:1"]["name"] = "Living room light"
monkeypatch.setattr(mock_rpc_device, "shelly", device_fixture["shelly"])
monkeypatch.setattr(mock_rpc_device, "status", device_fixture["status"])
monkeypatch.setattr(mock_rpc_device, "config", device_fixture["config"])
await init_integration(hass, gen=3, model=MODEL_2PM_G3)
# Relay 0 sub-device
entity_id = "switch.kitchen_light"
state = hass.states.get(entity_id)
assert state
entry = entity_registry.async_get(entity_id)
assert entry
device_entry = device_registry.async_get(entry.device_id)
assert device_entry
assert device_entry.name == "Kitchen light"
entity_id = "sensor.kitchen_light_power"
state = hass.states.get(entity_id)
assert state
entry = entity_registry.async_get(entity_id)
assert entry
device_entry = device_registry.async_get(entry.device_id)
assert device_entry
assert device_entry.name == "Kitchen light"
# Relay 1 sub-device
entity_id = "switch.living_room_light"
state = hass.states.get(entity_id)
assert state
entry = entity_registry.async_get(entity_id)
assert entry
device_entry = device_registry.async_get(entry.device_id)
assert device_entry
assert device_entry.name == "Living room light"
entity_id = "sensor.living_room_light_power"
state = hass.states.get(entity_id)
assert state
entry = entity_registry.async_get(entity_id)
assert entry
device_entry = device_registry.async_get(entry.device_id)
assert device_entry
assert device_entry.name == "Living room light"
# Main device
entity_id = "update.test_name_firmware"
state = hass.states.get(entity_id)
assert state
entry = entity_registry.async_get(entity_id)
assert entry
device_entry = device_registry.async_get(entry.device_id)
assert device_entry
assert device_entry.name == "Test name"
async def test_shelly_2pm_gen3_cover(
hass: HomeAssistant,
mock_rpc_device: Mock,
entity_registry: EntityRegistry,
device_registry: DeviceRegistry,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Test Shelly 2PM Gen3 with cover profile.
With the cover profile we should only get the main device and no subdevices.
"""
device_fixture = load_json_object_fixture("2pm_gen3_cover.json", DOMAIN)
monkeypatch.setattr(mock_rpc_device, "shelly", device_fixture["shelly"])
monkeypatch.setattr(mock_rpc_device, "status", device_fixture["status"])
monkeypatch.setattr(mock_rpc_device, "config", device_fixture["config"])
await init_integration(hass, gen=3, model=MODEL_2PM_G3)
entity_id = "cover.test_name"
state = hass.states.get(entity_id)
assert state
entry = entity_registry.async_get(entity_id)
assert entry
device_entry = device_registry.async_get(entry.device_id)
assert device_entry
assert device_entry.name == "Test name"
entity_id = "sensor.test_name_power"
state = hass.states.get(entity_id)
assert state
entry = entity_registry.async_get(entity_id)
assert entry
device_entry = device_registry.async_get(entry.device_id)
assert device_entry
assert device_entry.name == "Test name"
entity_id = "update.test_name_firmware"
state = hass.states.get(entity_id)
assert state
entry = entity_registry.async_get(entity_id)
assert entry
device_entry = device_registry.async_get(entry.device_id)
assert device_entry
assert device_entry.name == "Test name"
async def test_shelly_2pm_gen3_cover_with_name(
hass: HomeAssistant,
mock_rpc_device: Mock,
entity_registry: EntityRegistry,
device_registry: DeviceRegistry,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Test Shelly 2PM Gen3 with cover profile and the cover name.
With the cover profile we should only get the main device and no subdevices.
"""
device_fixture = load_json_object_fixture("2pm_gen3_cover.json", DOMAIN)
device_fixture["config"]["cover:0"]["name"] = "Bedroom blinds"
monkeypatch.setattr(mock_rpc_device, "shelly", device_fixture["shelly"])
monkeypatch.setattr(mock_rpc_device, "status", device_fixture["status"])
monkeypatch.setattr(mock_rpc_device, "config", device_fixture["config"])
await init_integration(hass, gen=3, model=MODEL_2PM_G3)
entity_id = "cover.test_name_bedroom_blinds"
state = hass.states.get(entity_id)
assert state
entry = entity_registry.async_get(entity_id)
assert entry
device_entry = device_registry.async_get(entry.device_id)
assert device_entry
assert device_entry.name == "Test name"
entity_id = "sensor.test_name_bedroom_blinds_power"
state = hass.states.get(entity_id)
assert state
entry = entity_registry.async_get(entity_id)
assert entry
device_entry = device_registry.async_get(entry.device_id)
assert device_entry
assert device_entry.name == "Test name"
entity_id = "update.test_name_firmware"
state = hass.states.get(entity_id)
assert state
entry = entity_registry.async_get(entity_id)
assert entry
device_entry = device_registry.async_get(entry.device_id)
assert device_entry
assert device_entry.name == "Test name"
async def test_shelly_pro_3em(
hass: HomeAssistant,
mock_rpc_device: Mock,
entity_registry: EntityRegistry,
device_registry: DeviceRegistry,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Test Shelly Pro 3EM.
We should get the main device and three subdevices, one subdevice per one phase.
"""
device_fixture = load_json_object_fixture("pro_3em.json", DOMAIN)
monkeypatch.setattr(mock_rpc_device, "shelly", device_fixture["shelly"])
monkeypatch.setattr(mock_rpc_device, "status", device_fixture["status"])
monkeypatch.setattr(mock_rpc_device, "config", device_fixture["config"])
await init_integration(hass, gen=2, model=MODEL_PRO_EM3)
# Main device
entity_id = "sensor.test_name_total_active_power"
state = hass.states.get(entity_id)
assert state
entry = entity_registry.async_get(entity_id)
assert entry
device_entry = device_registry.async_get(entry.device_id)
assert device_entry
assert device_entry.name == "Test name"
# Phase A sub-device
entity_id = "sensor.test_name_phase_a_active_power"
state = hass.states.get(entity_id)
assert state
entry = entity_registry.async_get(entity_id)
assert entry
device_entry = device_registry.async_get(entry.device_id)
assert device_entry
assert device_entry.name == "Test name Phase A"
# Phase B sub-device
entity_id = "sensor.test_name_phase_b_active_power"
state = hass.states.get(entity_id)
assert state
entry = entity_registry.async_get(entity_id)
assert entry
device_entry = device_registry.async_get(entry.device_id)
assert device_entry
assert device_entry.name == "Test name Phase B"
# Phase C sub-device
entity_id = "sensor.test_name_phase_c_active_power"
state = hass.states.get(entity_id)
assert state
entry = entity_registry.async_get(entity_id)
assert entry
device_entry = device_registry.async_get(entry.device_id)
assert device_entry
assert device_entry.name == "Test name Phase C"
async def test_shelly_pro_3em_with_emeter_name(
hass: HomeAssistant,
mock_rpc_device: Mock,
entity_registry: EntityRegistry,
device_registry: DeviceRegistry,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Test Shelly Pro 3EM when the name for Emeter is set.
We should get the main device and three subdevices, one subdevice per one phase.
"""
device_fixture = load_json_object_fixture("pro_3em.json", DOMAIN)
device_fixture["config"]["em:0"]["name"] = "Emeter name"
monkeypatch.setattr(mock_rpc_device, "shelly", device_fixture["shelly"])
monkeypatch.setattr(mock_rpc_device, "status", device_fixture["status"])
monkeypatch.setattr(mock_rpc_device, "config", device_fixture["config"])
await init_integration(hass, gen=2, model=MODEL_PRO_EM3)
# Main device
entity_id = "sensor.test_name_total_active_power"
state = hass.states.get(entity_id)
assert state
entry = entity_registry.async_get(entity_id)
assert entry
device_entry = device_registry.async_get(entry.device_id)
assert device_entry
assert device_entry.name == "Test name"
# Phase A sub-device
entity_id = "sensor.test_name_phase_a_active_power"
state = hass.states.get(entity_id)
assert state
entry = entity_registry.async_get(entity_id)
assert entry
device_entry = device_registry.async_get(entry.device_id)
assert device_entry
assert device_entry.name == "Test name Phase A"
# Phase B sub-device
entity_id = "sensor.test_name_phase_b_active_power"
state = hass.states.get(entity_id)
assert state
entry = entity_registry.async_get(entity_id)
assert entry
device_entry = device_registry.async_get(entry.device_id)
assert device_entry
assert device_entry.name == "Test name Phase B"
# Phase C sub-device
entity_id = "sensor.test_name_phase_c_active_power"
state = hass.states.get(entity_id)
assert state
entry = entity_registry.async_get(entity_id)
assert entry
device_entry = device_registry.async_get(entry.device_id)
assert device_entry
assert device_entry.name == "Test name Phase C"
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_block_channel_with_name(
hass: HomeAssistant,
mock_block_device: Mock,
monkeypatch: pytest.MonkeyPatch,
entity_registry: EntityRegistry,
device_registry: DeviceRegistry,
) -> None:
"""Test block channel with name."""
monkeypatch.setitem(
mock_block_device.settings["relays"][0], "name", "Kitchen light"
)
await init_integration(hass, 1)
# channel 1 sub-device; num_outputs is 2 so the name of the channel should be used
entity_id = "switch.kitchen_light"
state = hass.states.get(entity_id)
assert state
entry = entity_registry.async_get(entity_id)
assert entry
device_entry = device_registry.async_get(entry.device_id)
assert device_entry
assert device_entry.name == "Kitchen light"
# main device
entity_id = "update.test_name_firmware"
state = hass.states.get(entity_id)
assert state
entry = entity_registry.async_get(entity_id)
assert entry
device_entry = device_registry.async_get(entry.device_id)
assert device_entry
assert device_entry.name == "Test name"

View File

@ -147,7 +147,7 @@ async def test_rpc_config_entry_diagnostics(
], ],
"last_detection": ANY, "last_detection": ANY,
"monotonic_time": ANY, "monotonic_time": ANY,
"name": "Mock Title (12:34:56:78:9A:BE)", "name": "Test name (12:34:56:78:9A:BE)",
"scanning": True, "scanning": True,
"start_time": ANY, "start_time": ANY,
"source": "12:34:56:78:9A:BE", "source": "12:34:56:78:9A:BE",

View File

@ -31,7 +31,7 @@ async def test_rpc_button(
) -> None: ) -> None:
"""Test RPC device event.""" """Test RPC device event."""
await init_integration(hass, 2) await init_integration(hass, 2)
entity_id = "event.test_name_input_0" entity_id = "event.test_name_test_input_0"
assert (state := hass.states.get(entity_id)) assert (state := hass.states.get(entity_id))
assert state.state == STATE_UNKNOWN assert state.state == STATE_UNKNOWN
@ -176,6 +176,7 @@ async def test_block_event(
) -> None: ) -> None:
"""Test block device event.""" """Test block device event."""
await init_integration(hass, 1) await init_integration(hass, 1)
# num_outputs is 2, device name and channel name is used
entity_id = "event.test_name_channel_1" entity_id = "event.test_name_channel_1"
assert (state := hass.states.get(entity_id)) assert (state := hass.states.get(entity_id))
@ -201,11 +202,12 @@ async def test_block_event(
async def test_block_event_shix3_1( async def test_block_event_shix3_1(
hass: HomeAssistant, mock_block_device: Mock hass: HomeAssistant, mock_block_device: Mock, monkeypatch: pytest.MonkeyPatch
) -> None: ) -> None:
"""Test block device event for SHIX3-1.""" """Test block device event for SHIX3-1."""
monkeypatch.setitem(mock_block_device.shelly, "num_outputs", 1)
await init_integration(hass, 1, model=MODEL_I3) await init_integration(hass, 1, model=MODEL_I3)
entity_id = "event.test_name_channel_1" entity_id = "event.test_name"
assert (state := hass.states.get(entity_id)) assert (state := hass.states.get(entity_id))
assert state.attributes.get(ATTR_EVENT_TYPES) == unordered( assert state.attributes.get(ATTR_EVENT_TYPES) == unordered(

View File

@ -346,7 +346,7 @@ async def test_sleeping_rpc_device_offline_during_setup(
("gen", "entity_id"), ("gen", "entity_id"),
[ [
(1, "switch.test_name_channel_1"), (1, "switch.test_name_channel_1"),
(2, "switch.test_switch_0"), (2, "switch.test_name_test_switch_0"),
], ],
) )
async def test_entry_unload( async def test_entry_unload(
@ -378,7 +378,7 @@ async def test_entry_unload(
("gen", "entity_id"), ("gen", "entity_id"),
[ [
(1, "switch.test_name_channel_1"), (1, "switch.test_name_channel_1"),
(2, "switch.test_switch_0"), (2, "switch.test_name_test_switch_0"),
], ],
) )
async def test_entry_unload_device_not_ready( async def test_entry_unload_device_not_ready(
@ -417,7 +417,7 @@ async def test_entry_unload_not_connected(
) )
assert entry.state is ConfigEntryState.LOADED assert entry.state is ConfigEntryState.LOADED
assert (state := hass.states.get("switch.test_switch_0")) assert (state := hass.states.get("switch.test_name_test_switch_0"))
assert state.state == STATE_ON assert state.state == STATE_ON
assert not mock_stop_scanner.call_count assert not mock_stop_scanner.call_count
@ -448,7 +448,7 @@ async def test_entry_unload_not_connected_but_we_think_we_are(
) )
assert entry.state is ConfigEntryState.LOADED assert entry.state is ConfigEntryState.LOADED
assert (state := hass.states.get("switch.test_switch_0")) assert (state := hass.states.get("switch.test_name_test_switch_0"))
assert state.state == STATE_ON assert state.state == STATE_ON
assert not mock_stop_scanner.call_count assert not mock_stop_scanner.call_count
@ -483,6 +483,7 @@ async def test_entry_missing_gen(hass: HomeAssistant, mock_block_device: Mock) -
assert entry.state is ConfigEntryState.LOADED assert entry.state is ConfigEntryState.LOADED
# num_outputs is 2, channel name is used
assert (state := hass.states.get("switch.test_name_channel_1")) assert (state := hass.states.get("switch.test_name_channel_1"))
assert state.state == STATE_ON assert state.state == STATE_ON

View File

@ -58,10 +58,14 @@ SHELLY_PLUS_RGBW_CHANNELS = 4
async def test_block_device_rgbw_bulb( async def test_block_device_rgbw_bulb(
hass: HomeAssistant, mock_block_device: Mock, entity_registry: EntityRegistry hass: HomeAssistant,
mock_block_device: Mock,
entity_registry: EntityRegistry,
monkeypatch: pytest.MonkeyPatch,
) -> None: ) -> None:
"""Test block device RGBW bulb.""" """Test block device RGBW bulb."""
entity_id = "light.test_name_channel_1" monkeypatch.setitem(mock_block_device.shelly, "num_outputs", 1)
entity_id = "light.test_name"
await init_integration(hass, 1, model=MODEL_BULB) await init_integration(hass, 1, model=MODEL_BULB)
# Test initial # Test initial
@ -142,7 +146,8 @@ async def test_block_device_rgb_bulb(
caplog: pytest.LogCaptureFixture, caplog: pytest.LogCaptureFixture,
) -> None: ) -> None:
"""Test block device RGB bulb.""" """Test block device RGB bulb."""
entity_id = "light.test_name_channel_1" monkeypatch.setitem(mock_block_device.shelly, "num_outputs", 1)
entity_id = "light.test_name"
monkeypatch.delattr(mock_block_device.blocks[LIGHT_BLOCK_ID], "mode") monkeypatch.delattr(mock_block_device.blocks[LIGHT_BLOCK_ID], "mode")
monkeypatch.setattr( monkeypatch.setattr(
mock_block_device.blocks[LIGHT_BLOCK_ID], "description", "light_1" mock_block_device.blocks[LIGHT_BLOCK_ID], "description", "light_1"
@ -246,7 +251,8 @@ async def test_block_device_white_bulb(
monkeypatch: pytest.MonkeyPatch, monkeypatch: pytest.MonkeyPatch,
) -> None: ) -> None:
"""Test block device white bulb.""" """Test block device white bulb."""
entity_id = "light.test_name_channel_1" monkeypatch.setitem(mock_block_device.shelly, "num_outputs", 1)
entity_id = "light.test_name"
monkeypatch.delattr(mock_block_device.blocks[LIGHT_BLOCK_ID], "red") monkeypatch.delattr(mock_block_device.blocks[LIGHT_BLOCK_ID], "red")
monkeypatch.delattr(mock_block_device.blocks[LIGHT_BLOCK_ID], "green") monkeypatch.delattr(mock_block_device.blocks[LIGHT_BLOCK_ID], "green")
monkeypatch.delattr(mock_block_device.blocks[LIGHT_BLOCK_ID], "blue") monkeypatch.delattr(mock_block_device.blocks[LIGHT_BLOCK_ID], "blue")
@ -322,6 +328,7 @@ async def test_block_device_support_transition(
monkeypatch: pytest.MonkeyPatch, monkeypatch: pytest.MonkeyPatch,
) -> None: ) -> None:
"""Test block device supports transition.""" """Test block device supports transition."""
# num_outputs is 2, device name and channel name is used
entity_id = "light.test_name_channel_1" entity_id = "light.test_name_channel_1"
monkeypatch.setitem( monkeypatch.setitem(
mock_block_device.settings, "fw", "20220809-122808/v1.12-g99f7e0b" mock_block_device.settings, "fw", "20220809-122808/v1.12-g99f7e0b"
@ -448,7 +455,7 @@ async def test_rpc_device_switch_type_lights_mode(
monkeypatch: pytest.MonkeyPatch, monkeypatch: pytest.MonkeyPatch,
) -> None: ) -> None:
"""Test RPC device with switch in consumption type lights mode.""" """Test RPC device with switch in consumption type lights mode."""
entity_id = "light.test_switch_0" entity_id = "light.test_name_test_switch_0"
monkeypatch.setitem( monkeypatch.setitem(
mock_rpc_device.config["sys"]["ui_data"], "consumption_types", ["lights"] mock_rpc_device.config["sys"]["ui_data"], "consumption_types", ["lights"]
) )
@ -595,7 +602,7 @@ async def test_rpc_device_rgb_profile(
for i in range(SHELLY_PLUS_RGBW_CHANNELS): for i in range(SHELLY_PLUS_RGBW_CHANNELS):
monkeypatch.delitem(mock_rpc_device.status, f"light:{i}") monkeypatch.delitem(mock_rpc_device.status, f"light:{i}")
monkeypatch.delitem(mock_rpc_device.status, "rgbw:0") monkeypatch.delitem(mock_rpc_device.status, "rgbw:0")
entity_id = "light.test_rgb_0" entity_id = "light.test_name_test_rgb_0"
await init_integration(hass, 2) await init_integration(hass, 2)
# Test initial # Test initial
@ -639,7 +646,7 @@ async def test_rpc_device_rgbw_profile(
for i in range(SHELLY_PLUS_RGBW_CHANNELS): for i in range(SHELLY_PLUS_RGBW_CHANNELS):
monkeypatch.delitem(mock_rpc_device.status, f"light:{i}") monkeypatch.delitem(mock_rpc_device.status, f"light:{i}")
monkeypatch.delitem(mock_rpc_device.status, "rgb:0") monkeypatch.delitem(mock_rpc_device.status, "rgb:0")
entity_id = "light.test_rgbw_0" entity_id = "light.test_name_test_rgbw_0"
await init_integration(hass, 2) await init_integration(hass, 2)
# Test initial # Test initial
@ -753,7 +760,7 @@ async def test_rpc_rgbw_device_rgb_w_modes_remove_others(
# register lights # register lights
for i in range(SHELLY_PLUS_RGBW_CHANNELS): for i in range(SHELLY_PLUS_RGBW_CHANNELS):
monkeypatch.delitem(mock_rpc_device.status, f"light:{i}") monkeypatch.delitem(mock_rpc_device.status, f"light:{i}")
entity_id = f"light.test_light_{i}" entity_id = f"light.test_name_test_light_{i}"
register_entity( register_entity(
hass, hass,
LIGHT_DOMAIN, LIGHT_DOMAIN,
@ -781,7 +788,7 @@ async def test_rpc_rgbw_device_rgb_w_modes_remove_others(
await hass.async_block_till_done() await hass.async_block_till_done()
# verify we have RGB/w light # verify we have RGB/w light
entity_id = f"light.test_{active_mode}_0" entity_id = f"light.test_name_test_{active_mode}_0"
assert (state := hass.states.get(entity_id)) assert (state := hass.states.get(entity_id))
assert state.state == STATE_ON assert state.state == STATE_ON

View File

@ -108,7 +108,7 @@ async def test_humanify_shelly_click_event_rpc_device(
assert event1["domain"] == DOMAIN assert event1["domain"] == DOMAIN
assert ( assert (
event1["message"] event1["message"]
== "'single_push' click event for Test name input 0 Input was fired" == "'single_push' click event for Test name Test input 0 Input was fired"
) )
assert event2["name"] == "Shelly" assert event2["name"] == "Shelly"

View File

@ -62,6 +62,7 @@ async def test_block_sensor(
monkeypatch: pytest.MonkeyPatch, monkeypatch: pytest.MonkeyPatch,
) -> None: ) -> None:
"""Test block sensor.""" """Test block sensor."""
# num_outputs is 2, channel name is used
entity_id = f"{SENSOR_DOMAIN}.test_name_channel_1_power" entity_id = f"{SENSOR_DOMAIN}.test_name_channel_1_power"
await init_integration(hass, 1) await init_integration(hass, 1)
@ -82,6 +83,7 @@ async def test_energy_sensor(
hass: HomeAssistant, mock_block_device: Mock, entity_registry: EntityRegistry hass: HomeAssistant, mock_block_device: Mock, entity_registry: EntityRegistry
) -> None: ) -> None:
"""Test energy sensor.""" """Test energy sensor."""
# num_outputs is 2, channel name is used
entity_id = f"{SENSOR_DOMAIN}.test_name_channel_1_energy" entity_id = f"{SENSOR_DOMAIN}.test_name_channel_1_energy"
await init_integration(hass, 1) await init_integration(hass, 1)
@ -430,7 +432,9 @@ async def test_block_shelly_air_lamp_life(
percentage: float, percentage: float,
) -> None: ) -> None:
"""Test block Shelly Air lamp life percentage sensor.""" """Test block Shelly Air lamp life percentage sensor."""
entity_id = f"{SENSOR_DOMAIN}.{'test_name_channel_1_lamp_life'}" monkeypatch.setitem(mock_block_device.shelly, "num_outputs", 1)
# num_outputs is 1, device name is used
entity_id = f"{SENSOR_DOMAIN}.{'test_name_lamp_life'}"
monkeypatch.setattr( monkeypatch.setattr(
mock_block_device.blocks[RELAY_BLOCK_ID], "totalWorkTime", lamp_life_seconds mock_block_device.blocks[RELAY_BLOCK_ID], "totalWorkTime", lamp_life_seconds
) )
@ -444,7 +448,7 @@ async def test_rpc_sensor(
hass: HomeAssistant, mock_rpc_device: Mock, monkeypatch: pytest.MonkeyPatch hass: HomeAssistant, mock_rpc_device: Mock, monkeypatch: pytest.MonkeyPatch
) -> None: ) -> None:
"""Test RPC sensor.""" """Test RPC sensor."""
entity_id = f"{SENSOR_DOMAIN}.test_cover_0_power" entity_id = f"{SENSOR_DOMAIN}.test_name_test_cover_0_power"
await init_integration(hass, 2) await init_integration(hass, 2)
assert (state := hass.states.get(entity_id)) assert (state := hass.states.get(entity_id))
@ -673,37 +677,45 @@ async def test_rpc_restored_sleeping_sensor_no_last_state(
@pytest.mark.usefixtures("entity_registry_enabled_by_default") @pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_rpc_em1_sensors( async def test_rpc_energy_meter_1_sensors(
hass: HomeAssistant, entity_registry: EntityRegistry, mock_rpc_device: Mock hass: HomeAssistant, entity_registry: EntityRegistry, mock_rpc_device: Mock
) -> None: ) -> None:
"""Test RPC sensors for EM1 component.""" """Test RPC sensors for EM1 component."""
await init_integration(hass, 2) await init_integration(hass, 2)
assert (state := hass.states.get("sensor.test_name_em0_power")) assert (state := hass.states.get("sensor.test_name_energy_meter_0_power"))
assert state.state == "85.3" assert state.state == "85.3"
assert (entry := entity_registry.async_get("sensor.test_name_em0_power")) assert (entry := entity_registry.async_get("sensor.test_name_energy_meter_0_power"))
assert entry.unique_id == "123456789ABC-em1:0-power_em1" assert entry.unique_id == "123456789ABC-em1:0-power_em1"
assert (state := hass.states.get("sensor.test_name_em1_power")) assert (state := hass.states.get("sensor.test_name_energy_meter_1_power"))
assert state.state == "123.3" assert state.state == "123.3"
assert (entry := entity_registry.async_get("sensor.test_name_em1_power")) assert (entry := entity_registry.async_get("sensor.test_name_energy_meter_1_power"))
assert entry.unique_id == "123456789ABC-em1:1-power_em1" assert entry.unique_id == "123456789ABC-em1:1-power_em1"
assert (state := hass.states.get("sensor.test_name_em0_total_active_energy")) assert (
state := hass.states.get("sensor.test_name_energy_meter_0_total_active_energy")
)
assert state.state == "123.4564" assert state.state == "123.4564"
assert ( assert (
entry := entity_registry.async_get("sensor.test_name_em0_total_active_energy") entry := entity_registry.async_get(
"sensor.test_name_energy_meter_0_total_active_energy"
)
) )
assert entry.unique_id == "123456789ABC-em1data:0-total_act_energy" assert entry.unique_id == "123456789ABC-em1data:0-total_act_energy"
assert (state := hass.states.get("sensor.test_name_em1_total_active_energy")) assert (
state := hass.states.get("sensor.test_name_energy_meter_1_total_active_energy")
)
assert state.state == "987.6543" assert state.state == "987.6543"
assert ( assert (
entry := entity_registry.async_get("sensor.test_name_em1_total_active_energy") entry := entity_registry.async_get(
"sensor.test_name_energy_meter_1_total_active_energy"
)
) )
assert entry.unique_id == "123456789ABC-em1data:1-total_act_energy" assert entry.unique_id == "123456789ABC-em1data:1-total_act_energy"
@ -901,7 +913,7 @@ async def test_rpc_pulse_counter_sensors(
await init_integration(hass, 2) await init_integration(hass, 2)
entity_id = f"{SENSOR_DOMAIN}.gas_pulse_counter" entity_id = f"{SENSOR_DOMAIN}.test_name_gas_pulse_counter"
assert (state := hass.states.get(entity_id)) assert (state := hass.states.get(entity_id))
assert state.state == "56174" assert state.state == "56174"
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "pulse" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "pulse"
@ -910,7 +922,7 @@ async def test_rpc_pulse_counter_sensors(
assert (entry := entity_registry.async_get(entity_id)) assert (entry := entity_registry.async_get(entity_id))
assert entry.unique_id == "123456789ABC-input:2-pulse_counter" assert entry.unique_id == "123456789ABC-input:2-pulse_counter"
entity_id = f"{SENSOR_DOMAIN}.gas_counter_value" entity_id = f"{SENSOR_DOMAIN}.test_name_gas_counter_value"
assert (state := hass.states.get(entity_id)) assert (state := hass.states.get(entity_id))
assert state.state == "561.74" assert state.state == "561.74"
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == expected_unit assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == expected_unit
@ -949,11 +961,11 @@ async def test_rpc_disabled_xtotal_counter(
) )
await init_integration(hass, 2) await init_integration(hass, 2)
entity_id = f"{SENSOR_DOMAIN}.gas_pulse_counter" entity_id = f"{SENSOR_DOMAIN}.test_name_gas_pulse_counter"
assert (state := hass.states.get(entity_id)) assert (state := hass.states.get(entity_id))
assert state.state == "20635" assert state.state == "20635"
entity_id = f"{SENSOR_DOMAIN}.gas_counter_value" entity_id = f"{SENSOR_DOMAIN}.test_name_gas_counter_value"
assert hass.states.get(entity_id) is None assert hass.states.get(entity_id) is None
@ -980,7 +992,7 @@ async def test_rpc_pulse_counter_frequency_sensors(
await init_integration(hass, 2) await init_integration(hass, 2)
entity_id = f"{SENSOR_DOMAIN}.gas_pulse_counter_frequency" entity_id = f"{SENSOR_DOMAIN}.test_name_gas_pulse_counter_frequency"
assert (state := hass.states.get(entity_id)) assert (state := hass.states.get(entity_id))
assert state.state == "208.0" assert state.state == "208.0"
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfFrequency.HERTZ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfFrequency.HERTZ
@ -989,7 +1001,7 @@ async def test_rpc_pulse_counter_frequency_sensors(
assert (entry := entity_registry.async_get(entity_id)) assert (entry := entity_registry.async_get(entity_id))
assert entry.unique_id == "123456789ABC-input:2-counter_frequency" assert entry.unique_id == "123456789ABC-input:2-counter_frequency"
entity_id = f"{SENSOR_DOMAIN}.gas_pulse_counter_frequency_value" entity_id = f"{SENSOR_DOMAIN}.test_name_gas_pulse_counter_frequency_value"
assert (state := hass.states.get(entity_id)) assert (state := hass.states.get(entity_id))
assert state.state == "6.11" assert state.state == "6.11"
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == expected_unit assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == expected_unit
@ -1411,7 +1423,7 @@ async def test_rpc_rgbw_sensors(
assert (entry := entity_registry.async_get(entity_id)) assert (entry := entity_registry.async_get(entity_id))
assert entry.unique_id == f"123456789ABC-{light_type}:0-voltage_{light_type}" assert entry.unique_id == f"123456789ABC-{light_type}:0-voltage_{light_type}"
entity_id = f"sensor.test_name_{light_type}_light_0_device_temperature" entity_id = f"sensor.test_name_{light_type}_light_0_temperature"
assert (state := hass.states.get(entity_id)) assert (state := hass.states.get(entity_id))
assert state.state == "54.3" assert state.state == "54.3"
@ -1544,7 +1556,7 @@ async def test_rpc_switch_energy_sensors(
await init_integration(hass, 3) await init_integration(hass, 3)
for entity in ("energy", "returned_energy"): for entity in ("energy", "returned_energy"):
entity_id = f"{SENSOR_DOMAIN}.test_switch_0_{entity}" entity_id = f"{SENSOR_DOMAIN}.test_name_test_switch_0_{entity}"
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
assert state == snapshot(name=f"{entity_id}-state") assert state == snapshot(name=f"{entity_id}-state")
@ -1572,4 +1584,4 @@ async def test_rpc_switch_no_returned_energy_sensor(
monkeypatch.setattr(mock_rpc_device, "status", status) monkeypatch.setattr(mock_rpc_device, "status", status)
await init_integration(hass, 3) await init_integration(hass, 3)
assert hass.states.get("sensor.test_switch_0_returned_energy") is None assert hass.states.get("sensor.test_name_test_switch_0_returned_energy") is None

View File

@ -42,6 +42,7 @@ async def test_block_device_services(
) -> None: ) -> None:
"""Test block device turn on/off services.""" """Test block device turn on/off services."""
await init_integration(hass, 1) await init_integration(hass, 1)
# num_outputs is 2, device_name and channel name is used
entity_id = "switch.test_name_channel_1" entity_id = "switch.test_name_channel_1"
await hass.services.async_call( await hass.services.async_call(
@ -192,7 +193,7 @@ async def test_block_restored_motion_switch_no_last_state(
@pytest.mark.parametrize( @pytest.mark.parametrize(
("model", "sleep", "entity", "unique_id"), ("model", "sleep", "entity", "unique_id"),
[ [
(MODEL_1PM, 0, "switch.test_name_channel_1", "123456789ABC-relay_0"), (MODEL_1PM, 0, "switch.test_name", "123456789ABC-relay_0"),
( (
MODEL_MOTION, MODEL_MOTION,
1000, 1000,
@ -205,12 +206,15 @@ async def test_block_device_unique_ids(
hass: HomeAssistant, hass: HomeAssistant,
entity_registry: EntityRegistry, entity_registry: EntityRegistry,
mock_block_device: Mock, mock_block_device: Mock,
monkeypatch: pytest.MonkeyPatch,
model: str, model: str,
sleep: int, sleep: int,
entity: str, entity: str,
unique_id: str, unique_id: str,
) -> None: ) -> None:
"""Test block device unique_ids.""" """Test block device unique_ids."""
monkeypatch.setitem(mock_block_device.shelly, "num_outputs", 1)
# num_outputs is 1, device name is used
await init_integration(hass, 1, model=model, sleep_period=sleep) await init_integration(hass, 1, model=model, sleep_period=sleep)
if sleep: if sleep:
@ -332,7 +336,7 @@ async def test_rpc_device_services(
monkeypatch.setitem(mock_rpc_device.status["sys"], "relay_in_thermostat", False) monkeypatch.setitem(mock_rpc_device.status["sys"], "relay_in_thermostat", False)
await init_integration(hass, 2) await init_integration(hass, 2)
entity_id = "switch.test_switch_0" entity_id = "switch.test_name_test_switch_0"
await hass.services.async_call( await hass.services.async_call(
SWITCH_DOMAIN, SWITCH_DOMAIN,
SERVICE_TURN_ON, SERVICE_TURN_ON,
@ -365,7 +369,7 @@ async def test_rpc_device_unique_ids(
monkeypatch.setitem(mock_rpc_device.status["sys"], "relay_in_thermostat", False) monkeypatch.setitem(mock_rpc_device.status["sys"], "relay_in_thermostat", False)
await init_integration(hass, 2) await init_integration(hass, 2)
assert (entry := entity_registry.async_get("switch.test_switch_0")) assert (entry := entity_registry.async_get("switch.test_name_test_switch_0"))
assert entry.unique_id == "123456789ABC-switch:0" assert entry.unique_id == "123456789ABC-switch:0"
@ -386,11 +390,11 @@ async def test_rpc_device_switch_type_lights_mode(
[ [
( (
DeviceConnectionError, DeviceConnectionError,
"Device communication error occurred while calling action for switch.test_switch_0 of Test name", "Device communication error occurred while calling action for switch.test_name_test_switch_0 of Test name",
), ),
( (
RpcCallError(-1, "error"), RpcCallError(-1, "error"),
"RPC call error occurred while calling action for switch.test_switch_0 of Test name", "RPC call error occurred while calling action for switch.test_name_test_switch_0 of Test name",
), ),
], ],
) )
@ -411,7 +415,7 @@ async def test_rpc_set_state_errors(
await hass.services.async_call( await hass.services.async_call(
SWITCH_DOMAIN, SWITCH_DOMAIN,
SERVICE_TURN_OFF, SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: "switch.test_switch_0"}, {ATTR_ENTITY_ID: "switch.test_name_test_switch_0"},
blocking=True, blocking=True,
) )
@ -434,7 +438,7 @@ async def test_rpc_auth_error(
await hass.services.async_call( await hass.services.async_call(
SWITCH_DOMAIN, SWITCH_DOMAIN,
SERVICE_TURN_OFF, SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: "switch.test_switch_0"}, {ATTR_ENTITY_ID: "switch.test_name_test_switch_0"},
blocking=True, blocking=True,
) )
@ -476,8 +480,8 @@ async def test_wall_display_relay_mode(
monkeypatch: pytest.MonkeyPatch, monkeypatch: pytest.MonkeyPatch,
) -> None: ) -> None:
"""Test Wall Display in relay mode.""" """Test Wall Display in relay mode."""
climate_entity_id = "climate.test_name_thermostat_0" climate_entity_id = "climate.test_name"
switch_entity_id = "switch.test_switch_0" switch_entity_id = "switch.test_name_test_switch_0"
config_entry = await init_integration(hass, 2, model=MODEL_WALL_DISPLAY) config_entry = await init_integration(hass, 2, model=MODEL_WALL_DISPLAY)

View File

@ -79,37 +79,38 @@ async def test_block_get_block_channel_name(
mock_block_device: Mock, monkeypatch: pytest.MonkeyPatch mock_block_device: Mock, monkeypatch: pytest.MonkeyPatch
) -> None: ) -> None:
"""Test block get block channel name.""" """Test block get block channel name."""
monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "type", "relay") result = get_block_channel_name(
mock_block_device,
assert ( mock_block_device.blocks[DEVICE_BLOCK_ID],
get_block_channel_name(
mock_block_device,
mock_block_device.blocks[DEVICE_BLOCK_ID],
)
== "Test name channel 1"
) )
# when has_entity_name is True the result should be None
assert result is None
monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "type", "relay")
result = get_block_channel_name(
mock_block_device,
mock_block_device.blocks[DEVICE_BLOCK_ID],
)
# when has_entity_name is True the result should be None
assert result is None
monkeypatch.setitem(mock_block_device.settings["device"], "type", MODEL_EM3) monkeypatch.setitem(mock_block_device.settings["device"], "type", MODEL_EM3)
result = get_block_channel_name(
assert ( mock_block_device,
get_block_channel_name( mock_block_device.blocks[DEVICE_BLOCK_ID],
mock_block_device,
mock_block_device.blocks[DEVICE_BLOCK_ID],
)
== "Test name channel A"
) )
# when has_entity_name is True the result should be None
assert result is None
monkeypatch.setitem( monkeypatch.setitem(
mock_block_device.settings, "relays", [{"name": "test-channel"}] mock_block_device.settings, "relays", [{"name": "test-channel"}]
) )
result = get_block_channel_name(
assert ( mock_block_device,
get_block_channel_name( mock_block_device.blocks[DEVICE_BLOCK_ID],
mock_block_device,
mock_block_device.blocks[DEVICE_BLOCK_ID],
)
== "test-channel"
) )
# when has_entity_name is True the result should be None
assert result is None
async def test_is_block_momentary_input( async def test_is_block_momentary_input(
@ -241,20 +242,19 @@ async def test_get_block_input_triggers(
async def test_get_rpc_channel_name(mock_rpc_device: Mock) -> None: async def test_get_rpc_channel_name(mock_rpc_device: Mock) -> None:
"""Test get RPC channel name.""" """Test get RPC channel name."""
assert get_rpc_channel_name(mock_rpc_device, "input:0") == "Test name input 0" assert get_rpc_channel_name(mock_rpc_device, "input:0") == "Test input 0"
assert get_rpc_channel_name(mock_rpc_device, "input:3") == "Test name Input 3" assert get_rpc_channel_name(mock_rpc_device, "input:3") == "Input 3"
@pytest.mark.parametrize( @pytest.mark.parametrize(
("component", "expected"), ("component", "expected"),
[ [
("cover", "Cover"), ("cover", None),
("input", "Input"), ("light", None),
("light", "Light"), ("rgb", None),
("rgb", "RGB light"), ("rgbw", None),
("rgbw", "RGBW light"), ("switch", None),
("switch", "Switch"), ("thermostat", None),
("thermostat", "Thermostat"),
], ],
) )
async def test_get_rpc_channel_name_multiple_components( async def test_get_rpc_channel_name_multiple_components(
@ -270,14 +270,9 @@ async def test_get_rpc_channel_name_multiple_components(
} }
monkeypatch.setattr(mock_rpc_device, "config", config) monkeypatch.setattr(mock_rpc_device, "config", config)
assert ( # we use sub-devices, so the entity name is not set
get_rpc_channel_name(mock_rpc_device, f"{component}:0") assert get_rpc_channel_name(mock_rpc_device, f"{component}:0") == expected
== f"Test name {expected} 0" assert get_rpc_channel_name(mock_rpc_device, f"{component}:1") == expected
)
assert (
get_rpc_channel_name(mock_rpc_device, f"{component}:1")
== f"Test name {expected} 1"
)
async def test_get_rpc_input_triggers( async def test_get_rpc_input_triggers(