diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index ed5a00fffb3..e7d7b46b322 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -15,7 +15,6 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.const import STATE_ON, EntityCategory from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity @@ -36,6 +35,7 @@ from .entity import ( ) from .utils import ( async_remove_orphaned_entities, + get_blu_trv_device_info, get_device_entry_gen, get_virtual_component_ids, is_block_momentary_input, @@ -87,8 +87,8 @@ class RpcBluTrvBinarySensor(RpcBinarySensor): super().__init__(coordinator, key, attribute, description) ble_addr: str = coordinator.device.config[key]["addr"] - self._attr_device_info = DeviceInfo( - connections={(CONNECTION_BLUETOOTH, ble_addr)} + self._attr_device_info = get_blu_trv_device_info( + coordinator.device.config[key], ble_addr, coordinator.mac ) @@ -190,7 +190,6 @@ RPC_SENSORS: Final = { "input": RpcBinarySensorDescription( key="input", sub_key="state", - name="Input", device_class=BinarySensorDeviceClass.POWER, entity_registry_enabled_default=False, removal_condition=is_rpc_momentary_input, @@ -264,7 +263,6 @@ RPC_SENSORS: Final = { "boolean": RpcBinarySensorDescription( key="boolean", sub_key="value", - has_entity_name=True, ), "calibration": RpcBinarySensorDescription( key="blutrv", diff --git a/homeassistant/components/shelly/button.py b/homeassistant/components/shelly/button.py index 77b4021b03b..44f81cc8b36 100644 --- a/homeassistant/components/shelly/button.py +++ b/homeassistant/components/shelly/button.py @@ -19,18 +19,20 @@ from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.device_registry import ( - CONNECTION_BLUETOOTH, - CONNECTION_NETWORK_MAC, - DeviceInfo, -) +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import slugify from .const import DOMAIN, LOGGER, SHELLY_GAS_MODELS 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 @@ -168,6 +170,7 @@ class ShellyBaseButton( ): """Defines a Shelly base button.""" + _attr_has_entity_name = True entity_description: ShellyButtonDescription[ ShellyRpcCoordinator | ShellyBlockCoordinator ] @@ -228,8 +231,15 @@ class ShellyButton(ShellyBaseButton): """Initialize Shelly button.""" super().__init__(coordinator, description) - self._attr_name = f"{coordinator.device.name} {description.name}" 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( connections={(CONNECTION_NETWORK_MAC, coordinator.mac)} ) @@ -256,15 +266,11 @@ class ShellyBluTrvButton(ShellyBaseButton): """Initialize.""" super().__init__(coordinator, description) - ble_addr: str = coordinator.device.config[f"{BLU_TRV_IDENTIFIER}:{id_}"]["addr"] - device_name = ( - coordinator.device.config[f"{BLU_TRV_IDENTIFIER}:{id_}"]["name"] - or f"shellyblutrv-{ble_addr.replace(':', '')}" - ) - self._attr_name = f"{device_name} {description.name}" + config = coordinator.device.config[f"{BLU_TRV_IDENTIFIER}:{id_}"] + ble_addr: str = config["addr"] self._attr_unique_id = f"{ble_addr}_{description.key}" - self._attr_device_info = DeviceInfo( - connections={(CONNECTION_BLUETOOTH, ble_addr)} + self._attr_device_info = get_blu_trv_device_info( + config, ble_addr, coordinator.mac ) self._id = id_ diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index e1c55591da0..26fabe7e8b5 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -7,7 +7,7 @@ from dataclasses import asdict, dataclass from typing import Any, cast 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 homeassistant.components.climate import ( @@ -22,11 +22,6 @@ from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant, State, callback from homeassistant.exceptions import HomeAssistantError 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_registry import RegistryEntry 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 .utils import ( async_remove_shelly_entity, + get_block_device_info, + get_block_entity_name, + get_blu_trv_device_info, get_device_entry_gen, get_rpc_key_ids, is_rpc_thermostat_internal_actuator, @@ -181,6 +179,7 @@ class BlockSleepingClimate( ) _attr_target_temperature_step = SHTRV_01_TEMPERATURE_SETTINGS["step"] _attr_temperature_unit = UnitOfTemperature.CELSIUS + _attr_has_entity_name = True def __init__( self, @@ -199,7 +198,6 @@ class BlockSleepingClimate( self.last_state_attributes: Mapping[str, Any] self._preset_modes: list[str] = [] 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: self._unique_id = f"{self.coordinator.mac}-{self.block.description}" @@ -212,8 +210,11 @@ class BlockSleepingClimate( ] elif entry is not None: self._unique_id = entry.unique_id - self._attr_device_info = DeviceInfo( - connections={(CONNECTION_NETWORK_MAC, coordinator.mac)}, + self._attr_device_info = get_block_device_info( + 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]) @@ -553,7 +554,6 @@ class RpcBluTrvClimate(ShellyRpcEntity, ClimateEntity): _attr_hvac_mode = HVACMode.HEAT _attr_target_temperature_step = BLU_TRV_TEMPERATURE_SETTINGS["step"] _attr_temperature_unit = UnitOfTemperature.CELSIUS - _attr_has_entity_name = True def __init__(self, coordinator: ShellyRpcCoordinator, id_: int) -> None: """Initialize.""" @@ -563,19 +563,9 @@ class RpcBluTrvClimate(ShellyRpcEntity, ClimateEntity): self._config = coordinator.device.config[f"{BLU_TRV_IDENTIFIER}:{id_}"] ble_addr: str = self._config["addr"] self._attr_unique_id = f"{ble_addr}-{self.key}" - name = self._config["name"] or f"shellyblutrv-{ble_addr.replace(':', '')}" - model_id = self._config.get("local_name") - 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, + self._attr_device_info = get_blu_trv_device_info( + self._config, ble_addr, self.coordinator.mac ) - # Added intentionally to the constructor to avoid double name from base class - self._attr_name = None @property def target_temperature(self) -> float | None: diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index 87fc50a6666..7462766e2d4 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -258,6 +258,7 @@ DEVICES_WITHOUT_FIRMWARE_CHANGELOG = ( CONF_GEN = "gen" +VIRTUAL_COMPONENTS = ("boolean", "enum", "input", "number", "text") VIRTUAL_COMPONENTS_MAP = { "binary_sensor": {"types": ["boolean"], "modes": ["label"]}, "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() # so that the integration startup remains fast. MAX_SCRIPT_SIZE = 5120 + +All_LIGHT_TYPES = ("cct", "light", "rgb", "rgbw") diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 806f5fea700..1b0078890af 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -13,7 +13,6 @@ from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCal from homeassistant.core import HomeAssistant, State, callback from homeassistant.exceptions import HomeAssistantError 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_platform import AddEntitiesCallback 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 .utils import ( async_remove_shelly_entity, + get_block_device_info, get_block_entity_name, + get_rpc_device_info, get_rpc_entity_name, get_rpc_key_instances, ) @@ -353,13 +354,15 @@ def rpc_call[_T: ShellyRpcEntity, **_P]( class ShellyBlockEntity(CoordinatorEntity[ShellyBlockCoordinator]): """Helper class to represent a block entity.""" + _attr_has_entity_name = True + def __init__(self, coordinator: ShellyBlockCoordinator, block: Block) -> None: """Initialize Shelly entity.""" super().__init__(coordinator) self.block = block self._attr_name = get_block_entity_name(coordinator.device, block) - self._attr_device_info = DeviceInfo( - connections={(CONNECTION_NETWORK_MAC, coordinator.mac)} + self._attr_device_info = get_block_device_info( + coordinator.device, coordinator.mac, block ) self._attr_unique_id = f"{coordinator.mac}-{block.description}" @@ -395,12 +398,14 @@ class ShellyBlockEntity(CoordinatorEntity[ShellyBlockCoordinator]): class ShellyRpcEntity(CoordinatorEntity[ShellyRpcCoordinator]): """Helper class to represent a rpc entity.""" + _attr_has_entity_name = True + def __init__(self, coordinator: ShellyRpcCoordinator, key: str) -> None: """Initialize Shelly entity.""" super().__init__(coordinator) self.key = key - self._attr_device_info = DeviceInfo( - connections={(CONNECTION_NETWORK_MAC, coordinator.mac)} + self._attr_device_info = get_rpc_device_info( + coordinator.device, coordinator.mac, key ) self._attr_unique_id = f"{coordinator.mac}-{key}" self._attr_name = get_rpc_entity_name(coordinator.device, key) @@ -497,6 +502,7 @@ class ShellyBlockAttributeEntity(ShellyBlockEntity, Entity): class ShellyRestAttributeEntity(CoordinatorEntity[ShellyBlockCoordinator]): """Class to load info from REST.""" + _attr_has_entity_name = True entity_description: RestEntityDescription def __init__( @@ -514,8 +520,8 @@ class ShellyRestAttributeEntity(CoordinatorEntity[ShellyBlockCoordinator]): coordinator.device, None, description.name ) self._attr_unique_id = f"{coordinator.mac}-{attribute}" - self._attr_device_info = DeviceInfo( - connections={(CONNECTION_NETWORK_MAC, coordinator.mac)} + self._attr_device_info = get_block_device_info( + coordinator.device, coordinator.mac ) self._last_value = None @@ -623,8 +629,8 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity): self.block: Block | None = block # type: ignore[assignment] self.entity_description = description - self._attr_device_info = DeviceInfo( - connections={(CONNECTION_NETWORK_MAC, coordinator.mac)} + self._attr_device_info = get_block_device_info( + coordinator.device, coordinator.mac, block ) if block is not None: @@ -632,7 +638,7 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity): f"{self.coordinator.mac}-{block.description}-{attribute}" ) 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: self._attr_unique_id = entry.unique_id @@ -691,8 +697,8 @@ class ShellySleepingRpcAttributeEntity(ShellyRpcAttributeEntity): self.attribute = attribute self.entity_description = description - self._attr_device_info = DeviceInfo( - connections={(CONNECTION_NETWORK_MAC, coordinator.mac)} + self._attr_device_info = get_rpc_device_info( + coordinator.device, coordinator.mac, key ) self._attr_unique_id = self._attr_unique_id = ( f"{coordinator.mac}-{key}-{attribute}" diff --git a/homeassistant/components/shelly/event.py b/homeassistant/components/shelly/event.py index c858e7b591f..677ea1f6138 100644 --- a/homeassistant/components/shelly/event.py +++ b/homeassistant/components/shelly/event.py @@ -17,7 +17,6 @@ from homeassistant.components.event import ( EventEntityDescription, ) 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.update_coordinator import CoordinatorEntity @@ -32,6 +31,7 @@ from .utils import ( async_remove_orphaned_entities, async_remove_shelly_entity, get_device_entry_gen, + get_rpc_device_info, get_rpc_entity_name, get_rpc_key_instances, is_block_momentary_input, @@ -77,7 +77,6 @@ SCRIPT_EVENT: Final = ShellyRpcEventDescription( translation_key="script", device_class=None, entity_registry_enabled_default=False, - has_entity_name=True, ) @@ -195,6 +194,7 @@ class ShellyBlockEvent(ShellyBlockEntity, EventEntity): class ShellyRpcEvent(CoordinatorEntity[ShellyRpcCoordinator], EventEntity): """Represent RPC event entity.""" + _attr_has_entity_name = True entity_description: ShellyRpcEventDescription def __init__( @@ -206,8 +206,8 @@ class ShellyRpcEvent(CoordinatorEntity[ShellyRpcCoordinator], EventEntity): """Initialize Shelly entity.""" super().__init__(coordinator) self.event_id = int(key.split(":")[-1]) - self._attr_device_info = DeviceInfo( - connections={(CONNECTION_NETWORK_MAC, coordinator.mac)} + self._attr_device_info = get_rpc_device_info( + coordinator.device, coordinator.mac, key ) self._attr_unique_id = f"{coordinator.mac}-{key}" self._attr_name = get_rpc_entity_name(coordinator.device, key) diff --git a/homeassistant/components/shelly/logbook.py b/homeassistant/components/shelly/logbook.py index e18cd7ca465..e10b5cb57cf 100644 --- a/homeassistant/components/shelly/logbook.py +++ b/homeassistant/components/shelly/logbook.py @@ -43,7 +43,7 @@ def async_describe_events( rpc_coordinator = get_rpc_coordinator_by_device_id(hass, device_id) if rpc_coordinator and rpc_coordinator.device.initialized: 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: block_coordinator = get_block_coordinator_by_device_id(hass, device_id) diff --git a/homeassistant/components/shelly/number.py b/homeassistant/components/shelly/number.py index 49726f436d0..e406d63bdc2 100644 --- a/homeassistant/components/shelly/number.py +++ b/homeassistant/components/shelly/number.py @@ -21,7 +21,6 @@ from homeassistant.components.number import ( from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTemperature from homeassistant.core import HomeAssistant 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_registry import RegistryEntry @@ -38,6 +37,7 @@ from .entity import ( ) from .utils import ( async_remove_orphaned_entities, + get_blu_trv_device_info, get_device_entry_gen, get_virtual_component_ids, ) @@ -124,8 +124,8 @@ class RpcBluTrvNumber(RpcNumber): super().__init__(coordinator, key, attribute, description) ble_addr: str = coordinator.device.config[key]["addr"] - self._attr_device_info = DeviceInfo( - connections={(CONNECTION_BLUETOOTH, ble_addr)} + self._attr_device_info = get_blu_trv_device_info( + coordinator.device.config[key], ble_addr, coordinator.mac ) @@ -183,7 +183,6 @@ RPC_NUMBERS: Final = { "number": RpcNumberDescription( key="number", sub_key="value", - has_entity_name=True, max_fn=lambda config: config["max"], min_fn=lambda config: config["min"], mode_fn=lambda config: VIRTUAL_NUMBER_MODE_MAP.get( diff --git a/homeassistant/components/shelly/quality_scale.yaml b/homeassistant/components/shelly/quality_scale.yaml index 39a032a57f6..753b2ee4a93 100644 --- a/homeassistant/components/shelly/quality_scale.yaml +++ b/homeassistant/components/shelly/quality_scale.yaml @@ -17,7 +17,7 @@ rules: docs-removal-instructions: done entity-event-setup: done entity-unique-id: done - has-entity-name: todo + has-entity-name: done runtime-data: done test-before-configure: done test-before-setup: done diff --git a/homeassistant/components/shelly/select.py b/homeassistant/components/shelly/select.py index aec368f356b..0e367a9df37 100644 --- a/homeassistant/components/shelly/select.py +++ b/homeassistant/components/shelly/select.py @@ -40,7 +40,6 @@ RPC_SELECT_ENTITIES: Final = { "enum": RpcSelectDescription( key="enum", sub_key="value", - has_entity_name=True, ), } diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 78eff171daf..0ea246c7734 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -34,7 +34,6 @@ from homeassistant.const import ( UnitOfTemperature, ) 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_registry import RegistryEntry from homeassistant.helpers.typing import StateType @@ -56,8 +55,10 @@ from .entity import ( ) from .utils import ( async_remove_orphaned_entities, + get_blu_trv_device_info, get_device_entry_gen, get_device_uptime, + get_rpc_device_info, get_shelly_air_lamp_life, get_virtual_component_ids, is_rpc_wifi_stations_disabled, @@ -76,6 +77,7 @@ class RpcSensorDescription(RpcEntityDescription, SensorEntityDescription): """Class to describe a RPC sensor.""" device_class_fn: Callable[[dict], SensorDeviceClass | None] | None = None + emeter_phase: str | None = None @dataclass(frozen=True, kw_only=True) @@ -121,6 +123,26 @@ class RpcSensor(ShellyRpcAttributeEntity, SensorEntity): 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): """Represent a RPC BluTrv sensor.""" @@ -135,8 +157,8 @@ class RpcBluTrvSensor(RpcSensor): super().__init__(coordinator, key, attribute, description) ble_addr: str = coordinator.device.config[key]["addr"] - self._attr_device_info = DeviceInfo( - connections={(CONNECTION_BLUETOOTH, ble_addr)} + self._attr_device_info = get_blu_trv_device_info( + coordinator.device.config[key], ble_addr, coordinator.mac ) @@ -507,26 +529,32 @@ RPC_SENSORS: Final = { "a_act_power": RpcSensorDescription( key="em", sub_key="a_act_power", - name="Phase A active power", + name="Active power", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, + emeter_phase="A", + entity_class=RpcEmeterPhaseSensor, ), "b_act_power": RpcSensorDescription( key="em", sub_key="b_act_power", - name="Phase B active power", + name="Active power", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, + emeter_phase="B", + entity_class=RpcEmeterPhaseSensor, ), "c_act_power": RpcSensorDescription( key="em", sub_key="c_act_power", - name="Phase C active power", + name="Active power", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, + emeter_phase="C", + entity_class=RpcEmeterPhaseSensor, ), "total_act_power": RpcSensorDescription( key="em", @@ -539,26 +567,32 @@ RPC_SENSORS: Final = { "a_aprt_power": RpcSensorDescription( key="em", sub_key="a_aprt_power", - name="Phase A apparent power", + name="Apparent power", native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE, device_class=SensorDeviceClass.APPARENT_POWER, state_class=SensorStateClass.MEASUREMENT, + emeter_phase="A", + entity_class=RpcEmeterPhaseSensor, ), "b_aprt_power": RpcSensorDescription( key="em", sub_key="b_aprt_power", - name="Phase B apparent power", + name="Apparent power", native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE, device_class=SensorDeviceClass.APPARENT_POWER, state_class=SensorStateClass.MEASUREMENT, + emeter_phase="B", + entity_class=RpcEmeterPhaseSensor, ), "c_aprt_power": RpcSensorDescription( key="em", sub_key="c_aprt_power", - name="Phase C apparent power", + name="Apparent power", native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE, device_class=SensorDeviceClass.APPARENT_POWER, state_class=SensorStateClass.MEASUREMENT, + emeter_phase="C", + entity_class=RpcEmeterPhaseSensor, ), "aprt_power_em1": RpcSensorDescription( key="em1", @@ -586,23 +620,29 @@ RPC_SENSORS: Final = { "a_pf": RpcSensorDescription( key="em", sub_key="a_pf", - name="Phase A power factor", + name="Power factor", device_class=SensorDeviceClass.POWER_FACTOR, state_class=SensorStateClass.MEASUREMENT, + emeter_phase="A", + entity_class=RpcEmeterPhaseSensor, ), "b_pf": RpcSensorDescription( key="em", sub_key="b_pf", - name="Phase B power factor", + name="Power factor", device_class=SensorDeviceClass.POWER_FACTOR, state_class=SensorStateClass.MEASUREMENT, + emeter_phase="B", + entity_class=RpcEmeterPhaseSensor, ), "c_pf": RpcSensorDescription( key="em", sub_key="c_pf", - name="Phase C power factor", + name="Power factor", device_class=SensorDeviceClass.POWER_FACTOR, state_class=SensorStateClass.MEASUREMENT, + emeter_phase="C", + entity_class=RpcEmeterPhaseSensor, ), "voltage": RpcSensorDescription( key="switch", @@ -684,29 +724,35 @@ RPC_SENSORS: Final = { "a_voltage": RpcSensorDescription( key="em", sub_key="a_voltage", - name="Phase A voltage", + name="Voltage", native_unit_of_measurement=UnitOfElectricPotential.VOLT, device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, + emeter_phase="A", + entity_class=RpcEmeterPhaseSensor, ), "b_voltage": RpcSensorDescription( key="em", sub_key="b_voltage", - name="Phase B voltage", + name="Voltage", native_unit_of_measurement=UnitOfElectricPotential.VOLT, device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, + emeter_phase="B", + entity_class=RpcEmeterPhaseSensor, ), "c_voltage": RpcSensorDescription( key="em", sub_key="c_voltage", - name="Phase C voltage", + name="Voltage", native_unit_of_measurement=UnitOfElectricPotential.VOLT, device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, + emeter_phase="C", + entity_class=RpcEmeterPhaseSensor, ), "current": RpcSensorDescription( key="switch", @@ -781,29 +827,35 @@ RPC_SENSORS: Final = { "a_current": RpcSensorDescription( key="em", sub_key="a_current", - name="Phase A current", + name="Current", native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, + emeter_phase="A", + entity_class=RpcEmeterPhaseSensor, ), "b_current": RpcSensorDescription( key="em", sub_key="b_current", - name="Phase B current", + name="Current", native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, + emeter_phase="B", + entity_class=RpcEmeterPhaseSensor, ), "c_current": RpcSensorDescription( key="em", sub_key="c_current", - name="Phase C current", + name="Current", native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, + emeter_phase="C", + entity_class=RpcEmeterPhaseSensor, ), "n_current": RpcSensorDescription( key="em", @@ -944,7 +996,7 @@ RPC_SENSORS: Final = { "a_total_act_energy": RpcSensorDescription( key="emdata", sub_key="a_total_act_energy", - name="Phase A total active energy", + name="Total active energy", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, value=lambda status, _: float(status), @@ -952,11 +1004,13 @@ RPC_SENSORS: Final = { device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, entity_registry_enabled_default=False, + emeter_phase="A", + entity_class=RpcEmeterPhaseSensor, ), "b_total_act_energy": RpcSensorDescription( key="emdata", sub_key="b_total_act_energy", - name="Phase B total active energy", + name="Total active energy", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, value=lambda status, _: float(status), @@ -964,11 +1018,13 @@ RPC_SENSORS: Final = { device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, entity_registry_enabled_default=False, + emeter_phase="B", + entity_class=RpcEmeterPhaseSensor, ), "c_total_act_energy": RpcSensorDescription( key="emdata", sub_key="c_total_act_energy", - name="Phase C total active energy", + name="Total active energy", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, value=lambda status, _: float(status), @@ -976,6 +1032,8 @@ RPC_SENSORS: Final = { device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, entity_registry_enabled_default=False, + emeter_phase="C", + entity_class=RpcEmeterPhaseSensor, ), "total_act_ret": RpcSensorDescription( key="emdata", @@ -1003,7 +1061,7 @@ RPC_SENSORS: Final = { "a_total_act_ret_energy": RpcSensorDescription( key="emdata", 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, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, value=lambda status, _: float(status), @@ -1011,11 +1069,13 @@ RPC_SENSORS: Final = { device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, entity_registry_enabled_default=False, + emeter_phase="A", + entity_class=RpcEmeterPhaseSensor, ), "b_total_act_ret_energy": RpcSensorDescription( key="emdata", 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, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, value=lambda status, _: float(status), @@ -1023,11 +1083,13 @@ RPC_SENSORS: Final = { device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, entity_registry_enabled_default=False, + emeter_phase="B", + entity_class=RpcEmeterPhaseSensor, ), "c_total_act_ret_energy": RpcSensorDescription( key="emdata", 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, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, value=lambda status, _: float(status), @@ -1035,6 +1097,8 @@ RPC_SENSORS: Final = { device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, entity_registry_enabled_default=False, + emeter_phase="C", + entity_class=RpcEmeterPhaseSensor, ), "freq": RpcSensorDescription( key="switch", @@ -1069,32 +1133,38 @@ RPC_SENSORS: Final = { "a_freq": RpcSensorDescription( key="em", sub_key="a_freq", - name="Phase A frequency", + name="Frequency", native_unit_of_measurement=UnitOfFrequency.HERTZ, suggested_display_precision=0, device_class=SensorDeviceClass.FREQUENCY, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, + emeter_phase="A", + entity_class=RpcEmeterPhaseSensor, ), "b_freq": RpcSensorDescription( key="em", sub_key="b_freq", - name="Phase B frequency", + name="Frequency", native_unit_of_measurement=UnitOfFrequency.HERTZ, suggested_display_precision=0, device_class=SensorDeviceClass.FREQUENCY, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, + emeter_phase="B", + entity_class=RpcEmeterPhaseSensor, ), "c_freq": RpcSensorDescription( key="em", sub_key="c_freq", - name="Phase C frequency", + name="Frequency", native_unit_of_measurement=UnitOfFrequency.HERTZ, suggested_display_precision=0, device_class=SensorDeviceClass.FREQUENCY, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, + emeter_phase="C", + entity_class=RpcEmeterPhaseSensor, ), "illuminance": RpcSensorDescription( key="illuminance", @@ -1107,7 +1177,7 @@ RPC_SENSORS: Final = { "temperature": RpcSensorDescription( key="switch", sub_key="temperature", - name="Device temperature", + name="Temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, value=lambda status, _: status["tC"], suggested_display_precision=1, @@ -1120,7 +1190,7 @@ RPC_SENSORS: Final = { "temperature_light": RpcSensorDescription( key="light", sub_key="temperature", - name="Device temperature", + name="Temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, value=lambda status, _: status["tC"], suggested_display_precision=1, @@ -1133,7 +1203,7 @@ RPC_SENSORS: Final = { "temperature_cct": RpcSensorDescription( key="cct", sub_key="temperature", - name="Device temperature", + name="Temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, value=lambda status, _: status["tC"], suggested_display_precision=1, @@ -1146,7 +1216,7 @@ RPC_SENSORS: Final = { "temperature_rgb": RpcSensorDescription( key="rgb", sub_key="temperature", - name="Device temperature", + name="Temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, value=lambda status, _: status["tC"], suggested_display_precision=1, @@ -1159,7 +1229,7 @@ RPC_SENSORS: Final = { "temperature_rgbw": RpcSensorDescription( key="rgbw", sub_key="temperature", - name="Device temperature", + name="Temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, value=lambda status, _: status["tC"], suggested_display_precision=1, @@ -1308,12 +1378,10 @@ RPC_SENSORS: Final = { "text": RpcSensorDescription( key="text", sub_key="value", - has_entity_name=True, ), "number": RpcSensorDescription( key="number", sub_key="value", - has_entity_name=True, unit=lambda config: config["meta"]["ui"]["unit"] if config["meta"]["ui"]["unit"] else None, @@ -1324,7 +1392,6 @@ RPC_SENSORS: Final = { "enum": RpcSensorDescription( key="enum", sub_key="value", - has_entity_name=True, options_fn=lambda config: config["options"], device_class=SensorDeviceClass.ENUM, ), diff --git a/homeassistant/components/shelly/switch.py b/homeassistant/components/shelly/switch.py index 507f701795e..1c184d260f8 100644 --- a/homeassistant/components/shelly/switch.py +++ b/homeassistant/components/shelly/switch.py @@ -291,7 +291,6 @@ class RpcSwitch(ShellyRpcAttributeEntity, SwitchEntity): """Entity that controls a switch on RPC based Shelly devices.""" entity_description: RpcSwitchDescription - _attr_has_entity_name = True @property def is_on(self) -> bool: @@ -316,9 +315,6 @@ class RpcSwitch(ShellyRpcAttributeEntity, SwitchEntity): class RpcRelaySwitch(RpcSwitch): """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__( self, coordinator: ShellyRpcCoordinator, diff --git a/homeassistant/components/shelly/text.py b/homeassistant/components/shelly/text.py index a780c464947..d89531e2338 100644 --- a/homeassistant/components/shelly/text.py +++ b/homeassistant/components/shelly/text.py @@ -40,7 +40,6 @@ RPC_TEXT_ENTITIES: Final = { "text": RpcTextDescription( key="text", sub_key="value", - has_entity_name=True, ), } diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index 0c8048d34e4..eff5c95125c 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -11,6 +11,8 @@ from aiohttp.web import Request, WebSocketResponse from aioshelly.block_device import COAP, Block, BlockDevice from aioshelly.const import ( BLOCK_GENERATIONS, + BLU_TRV_IDENTIFIER, + BLU_TRV_MODEL_NAME, DEFAULT_COAP_PORT, DEFAULT_HTTP_PORT, MODEL_1L, @@ -40,7 +42,11 @@ from homeassistant.helpers import ( issue_registry as ir, 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.util.dt import utcnow @@ -65,7 +71,9 @@ from .const import ( SHELLY_EMIT_EVENT_PATTERN, SHIX3_1_INPUTS_EVENTS_TYPES, UPTIME_DEVIATION, + VIRTUAL_COMPONENTS, VIRTUAL_COMPONENTS_MAP, + All_LIGHT_TYPES, ) @@ -109,26 +117,24 @@ def get_block_entity_name( device: BlockDevice, block: Block | None, description: str | None = None, -) -> str: +) -> str | None: """Naming for block based switch and sensors.""" channel_name = get_block_channel_name(device, block) if description: - return f"{channel_name} {description.lower()}" + return f"{channel_name} {description.lower()}" if channel_name else description 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.""" - entity_name = device.name - if ( not block - or block.type == "device" + or block.type in ("device", "light", "relay", "emeter") or get_number_of_channels(device, block) == 1 ): - return entity_name + return None assert block.channel @@ -140,12 +146,28 @@ def get_block_channel_name(device: BlockDevice, block: Block | None) -> str: if 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: base = ord("A") - else: - base = ord("1") + return f"{device.name} Phase {chr(int(block.channel) + base)}" - 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( @@ -364,39 +386,64 @@ def get_shelly_model_name( 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.""" + 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("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: - channel = key.split(":")[0] - 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 + component = key.split(":")[0] + component_id = key.split(":")[-1] - 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( device: RpcDevice, key: str, description: str | None = None -) -> str: +) -> str | None: """Naming for RPC based switch and sensors.""" channel_name = get_rpc_channel_name(device, key) if description: - return f"{channel_name} {description.lower()}" + return f"{channel_name} {description.lower()}" if channel_name else description return channel_name @@ -406,7 +453,9 @@ def get_device_entry_gen(entry: ConfigEntry) -> int: 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.""" if key in keys_dict: 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: 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}:")] @@ -691,3 +743,81 @@ async def get_rpc_scripts_event_types( script_events[script_id] = await get_rpc_script_event_types(device, script_id) 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), + ) diff --git a/tests/components/shelly/__init__.py b/tests/components/shelly/__init__.py index ec2d3d2c829..6c835d2a636 100644 --- a/tests/components/shelly/__init__.py +++ b/tests/components/shelly/__init__.py @@ -53,7 +53,7 @@ async def init_integration( data[CONF_GEN] = gen 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) diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py index dd17fe34cc8..ac70226a20a 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -189,7 +189,7 @@ MOCK_BLOCKS = [ ] 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": { "id": 1, "type": "analog", @@ -204,7 +204,7 @@ MOCK_CONFIG = { "xcounts": {"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:1": {"name": "test light_1"}, "light:2": {"name": "test light_2"}, diff --git a/tests/components/shelly/fixtures/2pm_gen3.json b/tests/components/shelly/fixtures/2pm_gen3.json new file mode 100644 index 00000000000..bf3b4867585 --- /dev/null +++ b/tests/components/shelly/fixtures/2pm_gen3.json @@ -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 + } + } +} diff --git a/tests/components/shelly/fixtures/2pm_gen3_cover.json b/tests/components/shelly/fixtures/2pm_gen3_cover.json new file mode 100644 index 00000000000..4aa2bad677e --- /dev/null +++ b/tests/components/shelly/fixtures/2pm_gen3_cover.json @@ -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 + } + } +} diff --git a/tests/components/shelly/fixtures/pro_3em.json b/tests/components/shelly/fixtures/pro_3em.json new file mode 100644 index 00000000000..93351e9bc65 --- /dev/null +++ b/tests/components/shelly/fixtures/pro_3em.json @@ -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 + } + } +} diff --git a/tests/components/shelly/snapshots/test_binary_sensor.ambr b/tests/components/shelly/snapshots/test_binary_sensor.ambr index fcc6377837e..df8ed9cff4f 100644 --- a/tests/components/shelly/snapshots/test_binary_sensor.ambr +++ b/tests/components/shelly/snapshots/test_binary_sensor.ambr @@ -13,7 +13,7 @@ 'domain': 'binary_sensor', 'entity_category': , 'entity_id': 'binary_sensor.trv_name_calibration', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -24,7 +24,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'TRV-Name calibration', + 'original_name': 'Calibration', 'platform': 'shelly', 'previous_unique_id': None, 'supported_features': 0, @@ -37,7 +37,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', - 'friendly_name': 'TRV-Name calibration', + 'friendly_name': 'TRV-Name Calibration', }), 'context': , 'entity_id': 'binary_sensor.trv_name_calibration', @@ -47,7 +47,7 @@ '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({ 'aliases': set({ }), @@ -60,8 +60,8 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': None, - 'entity_id': 'binary_sensor.test_name_flood', - 'has_entity_name': False, + 'entity_id': 'binary_sensor.test_name_kitchen_flood', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -72,7 +72,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Test name flood', + 'original_name': 'Kitchen flood', 'platform': 'shelly', 'previous_unique_id': None, 'supported_features': 0, @@ -81,21 +81,21 @@ '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({ 'attributes': ReadOnlyDict({ 'device_class': 'moisture', - 'friendly_name': 'Test name flood', + 'friendly_name': 'Test name Kitchen flood', }), 'context': , - 'entity_id': 'binary_sensor.test_name_flood', + 'entity_id': 'binary_sensor.test_name_kitchen_flood', 'last_changed': , 'last_reported': , 'last_updated': , '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({ 'aliases': set({ }), @@ -108,8 +108,8 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': , - 'entity_id': 'binary_sensor.test_name_mute', - 'has_entity_name': False, + 'entity_id': 'binary_sensor.test_name_kitchen_mute', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -120,7 +120,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Test name mute', + 'original_name': 'Kitchen mute', 'platform': 'shelly', 'previous_unique_id': None, 'supported_features': 0, @@ -129,13 +129,13 @@ '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({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test name mute', + 'friendly_name': 'Test name Kitchen mute', }), 'context': , - 'entity_id': 'binary_sensor.test_name_mute', + 'entity_id': 'binary_sensor.test_name_kitchen_mute', 'last_changed': , 'last_reported': , 'last_updated': , diff --git a/tests/components/shelly/snapshots/test_button.ambr b/tests/components/shelly/snapshots/test_button.ambr index f5a38f1b847..33410ec2bbf 100644 --- a/tests/components/shelly/snapshots/test_button.ambr +++ b/tests/components/shelly/snapshots/test_button.ambr @@ -13,7 +13,7 @@ 'domain': 'button', 'entity_category': , 'entity_id': 'button.trv_name_calibrate', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -24,7 +24,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'TRV-Name Calibrate', + 'original_name': 'Calibrate', 'platform': 'shelly', 'previous_unique_id': None, 'supported_features': 0, @@ -60,7 +60,7 @@ 'domain': 'button', 'entity_category': , 'entity_id': 'button.test_name_reboot', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -71,7 +71,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Test name Reboot', + 'original_name': 'Reboot', 'platform': 'shelly', 'previous_unique_id': None, 'supported_features': 0, diff --git a/tests/components/shelly/snapshots/test_climate.ambr b/tests/components/shelly/snapshots/test_climate.ambr index 991c570172e..a434e1d8a9b 100644 --- a/tests/components/shelly/snapshots/test_climate.ambr +++ b/tests/components/shelly/snapshots/test_climate.ambr @@ -90,7 +90,7 @@ 'domain': 'climate', 'entity_category': None, 'entity_id': 'climate.test_name', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -101,7 +101,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Test name', + 'original_name': None, 'platform': 'shelly', 'previous_unique_id': None, 'supported_features': , @@ -140,7 +140,7 @@ '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({ 'aliases': set({ }), @@ -161,8 +161,8 @@ 'disabled_by': None, 'domain': 'climate', 'entity_category': None, - 'entity_id': 'climate.test_name_thermostat_0', - 'has_entity_name': False, + 'entity_id': 'climate.test_name', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -173,7 +173,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Test name Thermostat 0', + 'original_name': None, 'platform': 'shelly', 'previous_unique_id': None, 'supported_features': , @@ -182,12 +182,12 @@ '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({ 'attributes': ReadOnlyDict({ 'current_humidity': 44.4, 'current_temperature': 12.3, - 'friendly_name': 'Test name Thermostat 0', + 'friendly_name': 'Test name', 'hvac_action': , 'hvac_modes': list([ , @@ -200,14 +200,14 @@ 'temperature': 23, }), 'context': , - 'entity_id': 'climate.test_name_thermostat_0', + 'entity_id': 'climate.test_name', 'last_changed': , 'last_reported': , 'last_updated': , '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({ 'aliases': set({ }), @@ -228,8 +228,8 @@ 'disabled_by': None, 'domain': 'climate', 'entity_category': None, - 'entity_id': 'climate.test_name_thermostat_0', - 'has_entity_name': False, + 'entity_id': 'climate.test_name', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -240,7 +240,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Test name Thermostat 0', + 'original_name': None, 'platform': 'shelly', 'previous_unique_id': None, 'supported_features': , @@ -249,12 +249,12 @@ '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({ 'attributes': ReadOnlyDict({ 'current_humidity': 44.4, 'current_temperature': 12.3, - 'friendly_name': 'Test name Thermostat 0', + 'friendly_name': 'Test name', 'hvac_action': , 'hvac_modes': list([ , @@ -267,7 +267,7 @@ 'temperature': 23, }), 'context': , - 'entity_id': 'climate.test_name_thermostat_0', + 'entity_id': 'climate.test_name', 'last_changed': , 'last_reported': , 'last_updated': , diff --git a/tests/components/shelly/snapshots/test_number.ambr b/tests/components/shelly/snapshots/test_number.ambr index 07fda999556..d715b342e79 100644 --- a/tests/components/shelly/snapshots/test_number.ambr +++ b/tests/components/shelly/snapshots/test_number.ambr @@ -18,7 +18,7 @@ 'domain': 'number', 'entity_category': , 'entity_id': 'number.trv_name_external_temperature', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -29,7 +29,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'TRV-Name external temperature', + 'original_name': 'External temperature', 'platform': 'shelly', 'previous_unique_id': None, 'supported_features': 0, @@ -41,7 +41,7 @@ # name: test_blu_trv_number_entity[number.trv_name_external_temperature-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'TRV-Name external temperature', + 'friendly_name': 'TRV-Name External temperature', 'max': 50, 'min': -50, 'mode': , @@ -75,7 +75,7 @@ 'domain': 'number', 'entity_category': None, 'entity_id': 'number.trv_name_valve_position', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -86,7 +86,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'TRV-Name valve position', + 'original_name': 'Valve position', 'platform': 'shelly', 'previous_unique_id': None, 'supported_features': 0, @@ -98,7 +98,7 @@ # name: test_blu_trv_number_entity[number.trv_name_valve_position-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'TRV-Name valve position', + 'friendly_name': 'TRV-Name Valve position', 'max': 100, 'min': 0, 'mode': , diff --git a/tests/components/shelly/snapshots/test_sensor.ambr b/tests/components/shelly/snapshots/test_sensor.ambr index c5c1427e3dc..6fd0bd716b7 100644 --- a/tests/components/shelly/snapshots/test_sensor.ambr +++ b/tests/components/shelly/snapshots/test_sensor.ambr @@ -15,7 +15,7 @@ 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.trv_name_battery', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -26,7 +26,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'TRV-Name battery', + 'original_name': 'Battery', 'platform': 'shelly', 'previous_unique_id': None, 'supported_features': 0, @@ -39,7 +39,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', - 'friendly_name': 'TRV-Name battery', + 'friendly_name': 'TRV-Name Battery', 'state_class': , 'unit_of_measurement': '%', }), @@ -67,7 +67,7 @@ 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.trv_name_signal_strength', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -78,7 +78,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'TRV-Name signal strength', + 'original_name': 'Signal strength', 'platform': 'shelly', 'previous_unique_id': None, 'supported_features': 0, @@ -91,7 +91,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'signal_strength', - 'friendly_name': 'TRV-Name signal strength', + 'friendly_name': 'TRV-Name Signal strength', 'state_class': , 'unit_of_measurement': 'dBm', }), @@ -119,7 +119,7 @@ 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.trv_name_valve_position', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -130,7 +130,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'TRV-Name valve position', + 'original_name': 'Valve position', 'platform': 'shelly', 'previous_unique_id': None, 'supported_features': 0, @@ -142,7 +142,7 @@ # name: test_blu_trv_sensor_entity[sensor.trv_name_valve_position-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'TRV-Name valve position', + 'friendly_name': 'TRV-Name Valve position', 'state_class': , 'unit_of_measurement': '%', }), @@ -154,7 +154,7 @@ '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({ 'aliases': set({ }), @@ -169,8 +169,8 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.test_switch_0_energy', - 'has_entity_name': False, + 'entity_id': 'sensor.test_name_test_switch_0_energy', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -196,23 +196,23 @@ 'unit_of_measurement': , }) # --- -# 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({ 'attributes': ReadOnlyDict({ 'device_class': 'energy', - 'friendly_name': 'test switch_0 energy', + 'friendly_name': 'Test name test switch_0 energy', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.test_switch_0_energy', + 'entity_id': 'sensor.test_name_test_switch_0_energy', 'last_changed': , 'last_reported': , 'last_updated': , '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({ 'aliases': set({ }), @@ -227,8 +227,8 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.test_switch_0_returned_energy', - 'has_entity_name': False, + 'entity_id': 'sensor.test_name_test_switch_0_returned_energy', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -254,16 +254,16 @@ 'unit_of_measurement': , }) # --- -# 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({ 'attributes': ReadOnlyDict({ 'device_class': 'energy', - 'friendly_name': 'test switch_0 returned energy', + 'friendly_name': 'Test name test switch_0 returned energy', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.test_switch_0_returned_energy', + 'entity_id': 'sensor.test_name_test_switch_0_returned_energy', 'last_changed': , 'last_reported': , 'last_updated': , diff --git a/tests/components/shelly/test_binary_sensor.py b/tests/components/shelly/test_binary_sensor.py index fc79853f29e..f67e0bbb564 100644 --- a/tests/components/shelly/test_binary_sensor.py +++ b/tests/components/shelly/test_binary_sensor.py @@ -36,7 +36,8 @@ async def test_block_binary_sensor( entity_registry: EntityRegistry, ) -> None: """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) assert (state := hass.states.get(entity_id)) @@ -239,7 +240,7 @@ async def test_rpc_binary_sensor( entity_registry: EntityRegistry, ) -> None: """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) assert (state := hass.states.get(entity_id)) @@ -521,7 +522,7 @@ async def test_rpc_flood_entities( await init_integration(hass, 4) 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) assert state == snapshot(name=f"{entity_id}-state") diff --git a/tests/components/shelly/test_climate.py b/tests/components/shelly/test_climate.py index eddd9ab6fd0..c19bd916fed 100644 --- a/tests/components/shelly/test_climate.py +++ b/tests/components/shelly/test_climate.py @@ -613,7 +613,7 @@ async def test_rpc_climate_hvac_mode( snapshot: SnapshotAssertion, ) -> None: """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) @@ -651,7 +651,7 @@ async def test_rpc_climate_without_humidity( monkeypatch: pytest.MonkeyPatch, ) -> None: """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.pop("humidity:0") 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 ) -> None: """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) @@ -700,7 +700,7 @@ async def test_rpc_climate_hvac_mode_cool( hass: HomeAssistant, mock_rpc_device: Mock, monkeypatch: pytest.MonkeyPatch ) -> None: """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["thermostat:0"]["type"] = "cooling" monkeypatch.setattr(mock_rpc_device, "config", new_config) @@ -720,8 +720,8 @@ async def test_wall_display_thermostat_mode( snapshot: SnapshotAssertion, ) -> None: """Test Wall Display in thermostat mode.""" - climate_entity_id = "climate.test_name_thermostat_0" - switch_entity_id = "switch.test_switch_0" + climate_entity_id = "climate.test_name" + switch_entity_id = "switch.test_name_test_switch_0" 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, ) -> None: """Test Wall Display in thermostat mode with an external actuator.""" - climate_entity_id = "climate.test_name_thermostat_0" - switch_entity_id = "switch.test_switch_0" + climate_entity_id = "climate.test_name" + switch_entity_id = "switch.test_name_test_switch_0" new_status = deepcopy(mock_rpc_device.status) new_status["sys"]["relay_in_thermostat"] = False diff --git a/tests/components/shelly/test_coordinator.py b/tests/components/shelly/test_coordinator.py index cf7f82014a0..5b4372fe938 100644 --- a/tests/components/shelly/test_coordinator.py +++ b/tests/components/shelly/test_coordinator.py @@ -56,6 +56,8 @@ async def test_block_reload_on_cfg_change( ) -> None: """Test block reload on config change.""" 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) mock_block_device.mock_update() @@ -71,7 +73,7 @@ async def test_block_reload_on_cfg_change( async_fire_time_changed(hass) 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 monkeypatch.setitem( @@ -81,14 +83,14 @@ async def test_block_reload_on_cfg_change( mock_block_device.mock_update() await hass.async_block_till_done() - assert hass.states.get("switch.test_name_channel_1") + assert hass.states.get(entity_id) # Wait for debouncer freezer.tick(timedelta(seconds=ENTRY_RELOAD_COOLDOWN)) async_fire_time_changed(hass) 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( @@ -98,6 +100,9 @@ async def test_block_no_reload_on_bulb_changes( monkeypatch: pytest.MonkeyPatch, ) -> None: """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) 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() await hass.async_block_till_done() - assert hass.states.get("switch.test_name_channel_1") + assert hass.states.get(entity_id) # Wait for debouncer freezer.tick(timedelta(seconds=ENTRY_RELOAD_COOLDOWN)) async_fire_time_changed(hass) 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 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() await hass.async_block_till_done() - assert hass.states.get("switch.test_name_channel_1") + assert hass.states.get(entity_id) # Wait for debouncer freezer.tick(timedelta(seconds=ENTRY_RELOAD_COOLDOWN)) async_fire_time_changed(hass) 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( @@ -242,9 +247,11 @@ async def test_block_polling_connection_error( "update", 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) - assert (state := hass.states.get("switch.test_name_channel_1")) + assert (state := hass.states.get(entity_id)) assert state.state == STATE_ON # Move time to generate polling @@ -252,7 +259,7 @@ async def test_block_polling_connection_error( async_fire_time_changed(hass) 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 @@ -391,6 +398,7 @@ async def test_rpc_reload_on_cfg_change( monkeypatch: pytest.MonkeyPatch, ) -> None: """Test RPC reload on config change.""" + entity_id = "switch.test_name_test_switch_0" monkeypatch.delitem(mock_rpc_device.status, "cover:0") monkeypatch.setitem(mock_rpc_device.status["sys"], "relay_in_thermostat", False) await init_integration(hass, 2) @@ -421,14 +429,14 @@ async def test_rpc_reload_on_cfg_change( ) await hass.async_block_till_done() - assert hass.states.get("switch.test_switch_0") + assert hass.states.get(entity_id) # Wait for debouncer freezer.tick(timedelta(seconds=ENTRY_RELOAD_COOLDOWN)) async_fire_time_changed(hass) 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( @@ -719,11 +727,12 @@ async def test_rpc_reconnect_error( exc: Exception, ) -> None: """Test RPC reconnect error.""" + entity_id = "switch.test_name_test_switch_0" monkeypatch.delitem(mock_rpc_device.status, "cover:0") monkeypatch.setitem(mock_rpc_device.status["sys"], "relay_in_thermostat", False) 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 monkeypatch.setattr(mock_rpc_device, "connected", False) @@ -734,7 +743,7 @@ async def test_rpc_reconnect_error( async_fire_time_changed(hass) 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 @@ -746,6 +755,7 @@ async def test_rpc_error_running_connected_events( caplog: pytest.LogCaptureFixture, ) -> None: """Test RPC error while running connected events.""" + entity_id = "switch.test_name_test_switch_0" monkeypatch.delitem(mock_rpc_device.status, "cover:0") monkeypatch.setitem(mock_rpc_device.status["sys"], "relay_in_thermostat", False) 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 (state := hass.states.get("switch.test_switch_0")) + assert (state := hass.states.get(entity_id)) assert state.state == STATE_UNAVAILABLE # Move time to generate reconnect without error @@ -766,7 +776,7 @@ async def test_rpc_error_running_connected_events( async_fire_time_changed(hass) 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 diff --git a/tests/components/shelly/test_cover.py b/tests/components/shelly/test_cover.py index df3ab4f288d..4f8e8a7650d 100644 --- a/tests/components/shelly/test_cover.py +++ b/tests/components/shelly/test_cover.py @@ -116,7 +116,7 @@ async def test_rpc_device_services( entity_registry: EntityRegistry, ) -> None: """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 hass.services.async_call( @@ -178,23 +178,24 @@ async def test_rpc_device_no_cover_keys( monkeypatch.delitem(mock_rpc_device.status, "cover:0") 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( hass: HomeAssistant, mock_rpc_device: Mock, monkeypatch: pytest.MonkeyPatch ) -> None: """Test RPC device update.""" + entity_id = "cover.test_name_test_cover_0" mutate_rpc_device_status(monkeypatch, mock_rpc_device, "cover:0", "state", "closed") await init_integration(hass, 2) - state = hass.states.get("cover.test_cover_0") + state = hass.states.get(entity_id) assert state assert state.state == CoverState.CLOSED mutate_rpc_device_status(monkeypatch, mock_rpc_device, "cover:0", "state", "open") mock_rpc_device.mock_update() - state = hass.states.get("cover.test_cover_0") + state = hass.states.get(entity_id) assert state assert state.state == CoverState.OPEN @@ -208,7 +209,7 @@ async def test_rpc_device_no_position_control( ) 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.state == CoverState.OPEN @@ -220,7 +221,7 @@ async def test_rpc_cover_tilt( entity_registry: EntityRegistry, ) -> None: """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["cover:0"]["slat"] = {"enable": True} diff --git a/tests/components/shelly/test_devices.py b/tests/components/shelly/test_devices.py new file mode 100644 index 00000000000..e894a393ac5 --- /dev/null +++ b/tests/components/shelly/test_devices.py @@ -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" diff --git a/tests/components/shelly/test_diagnostics.py b/tests/components/shelly/test_diagnostics.py index 84ebd50c425..300b67abe75 100644 --- a/tests/components/shelly/test_diagnostics.py +++ b/tests/components/shelly/test_diagnostics.py @@ -147,7 +147,7 @@ async def test_rpc_config_entry_diagnostics( ], "last_detection": ANY, "monotonic_time": ANY, - "name": "Mock Title (12:34:56:78:9A:BE)", + "name": "Test name (12:34:56:78:9A:BE)", "scanning": True, "start_time": ANY, "source": "12:34:56:78:9A:BE", diff --git a/tests/components/shelly/test_event.py b/tests/components/shelly/test_event.py index a3c96b6b247..520233eaf60 100644 --- a/tests/components/shelly/test_event.py +++ b/tests/components/shelly/test_event.py @@ -31,7 +31,7 @@ async def test_rpc_button( ) -> None: """Test RPC device event.""" 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.state == STATE_UNKNOWN @@ -176,6 +176,7 @@ async def test_block_event( ) -> None: """Test block device event.""" await init_integration(hass, 1) + # num_outputs is 2, device name and channel name is used entity_id = "event.test_name_channel_1" assert (state := hass.states.get(entity_id)) @@ -201,11 +202,12 @@ async def test_block_event( async def test_block_event_shix3_1( - hass: HomeAssistant, mock_block_device: Mock + hass: HomeAssistant, mock_block_device: Mock, monkeypatch: pytest.MonkeyPatch ) -> None: """Test block device event for SHIX3-1.""" + monkeypatch.setitem(mock_block_device.shelly, "num_outputs", 1) 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.attributes.get(ATTR_EVENT_TYPES) == unordered( diff --git a/tests/components/shelly/test_init.py b/tests/components/shelly/test_init.py index 4cf49a2dab8..283de897d8d 100644 --- a/tests/components/shelly/test_init.py +++ b/tests/components/shelly/test_init.py @@ -346,7 +346,7 @@ async def test_sleeping_rpc_device_offline_during_setup( ("gen", "entity_id"), [ (1, "switch.test_name_channel_1"), - (2, "switch.test_switch_0"), + (2, "switch.test_name_test_switch_0"), ], ) async def test_entry_unload( @@ -378,7 +378,7 @@ async def test_entry_unload( ("gen", "entity_id"), [ (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( @@ -417,7 +417,7 @@ async def test_entry_unload_not_connected( ) 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 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 (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 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 + # num_outputs is 2, channel name is used assert (state := hass.states.get("switch.test_name_channel_1")) assert state.state == STATE_ON diff --git a/tests/components/shelly/test_light.py b/tests/components/shelly/test_light.py index 0dab06f53a9..9c79cf5d988 100644 --- a/tests/components/shelly/test_light.py +++ b/tests/components/shelly/test_light.py @@ -58,10 +58,14 @@ SHELLY_PLUS_RGBW_CHANNELS = 4 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: """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) # Test initial @@ -142,7 +146,8 @@ async def test_block_device_rgb_bulb( caplog: pytest.LogCaptureFixture, ) -> None: """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.setattr( mock_block_device.blocks[LIGHT_BLOCK_ID], "description", "light_1" @@ -246,7 +251,8 @@ async def test_block_device_white_bulb( monkeypatch: pytest.MonkeyPatch, ) -> None: """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], "green") monkeypatch.delattr(mock_block_device.blocks[LIGHT_BLOCK_ID], "blue") @@ -322,6 +328,7 @@ async def test_block_device_support_transition( monkeypatch: pytest.MonkeyPatch, ) -> None: """Test block device supports transition.""" + # num_outputs is 2, device name and channel name is used entity_id = "light.test_name_channel_1" monkeypatch.setitem( 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, ) -> None: """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( 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): monkeypatch.delitem(mock_rpc_device.status, f"light:{i}") 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) # Test initial @@ -639,7 +646,7 @@ async def test_rpc_device_rgbw_profile( for i in range(SHELLY_PLUS_RGBW_CHANNELS): monkeypatch.delitem(mock_rpc_device.status, f"light:{i}") 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) # Test initial @@ -753,7 +760,7 @@ async def test_rpc_rgbw_device_rgb_w_modes_remove_others( # register lights for i in range(SHELLY_PLUS_RGBW_CHANNELS): 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( hass, LIGHT_DOMAIN, @@ -781,7 +788,7 @@ async def test_rpc_rgbw_device_rgb_w_modes_remove_others( await hass.async_block_till_done() # 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.state == STATE_ON diff --git a/tests/components/shelly/test_logbook.py b/tests/components/shelly/test_logbook.py index 8962b26544b..08256e03f4e 100644 --- a/tests/components/shelly/test_logbook.py +++ b/tests/components/shelly/test_logbook.py @@ -108,7 +108,7 @@ async def test_humanify_shelly_click_event_rpc_device( assert event1["domain"] == DOMAIN assert ( 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" diff --git a/tests/components/shelly/test_sensor.py b/tests/components/shelly/test_sensor.py index a3d0a0f59c9..e95d4cfaeb2 100644 --- a/tests/components/shelly/test_sensor.py +++ b/tests/components/shelly/test_sensor.py @@ -62,6 +62,7 @@ async def test_block_sensor( monkeypatch: pytest.MonkeyPatch, ) -> None: """Test block sensor.""" + # num_outputs is 2, channel name is used entity_id = f"{SENSOR_DOMAIN}.test_name_channel_1_power" await init_integration(hass, 1) @@ -82,6 +83,7 @@ async def test_energy_sensor( hass: HomeAssistant, mock_block_device: Mock, entity_registry: EntityRegistry ) -> None: """Test energy sensor.""" + # num_outputs is 2, channel name is used entity_id = f"{SENSOR_DOMAIN}.test_name_channel_1_energy" await init_integration(hass, 1) @@ -430,7 +432,9 @@ async def test_block_shelly_air_lamp_life( percentage: float, ) -> None: """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( 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 ) -> None: """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) 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") -async def test_rpc_em1_sensors( +async def test_rpc_energy_meter_1_sensors( hass: HomeAssistant, entity_registry: EntityRegistry, mock_rpc_device: Mock ) -> None: """Test RPC sensors for EM1 component.""" 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 (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 (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 (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 (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 ( - 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 (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 ( - 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" @@ -901,7 +913,7 @@ async def test_rpc_pulse_counter_sensors( 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.state == "56174" 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.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.state == "561.74" 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) - 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.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 @@ -980,7 +992,7 @@ async def test_rpc_pulse_counter_frequency_sensors( 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.state == "208.0" 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.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.state == "6.11" 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.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.state == "54.3" @@ -1544,7 +1556,7 @@ async def test_rpc_switch_energy_sensors( await init_integration(hass, 3) 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) 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) 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 diff --git a/tests/components/shelly/test_switch.py b/tests/components/shelly/test_switch.py index 824742d1798..54923b538f6 100644 --- a/tests/components/shelly/test_switch.py +++ b/tests/components/shelly/test_switch.py @@ -42,6 +42,7 @@ async def test_block_device_services( ) -> None: """Test block device turn on/off services.""" await init_integration(hass, 1) + # num_outputs is 2, device_name and channel name is used entity_id = "switch.test_name_channel_1" await hass.services.async_call( @@ -192,7 +193,7 @@ async def test_block_restored_motion_switch_no_last_state( @pytest.mark.parametrize( ("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, 1000, @@ -205,12 +206,15 @@ async def test_block_device_unique_ids( hass: HomeAssistant, entity_registry: EntityRegistry, mock_block_device: Mock, + monkeypatch: pytest.MonkeyPatch, model: str, sleep: int, entity: str, unique_id: str, ) -> None: """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) if sleep: @@ -332,7 +336,7 @@ async def test_rpc_device_services( monkeypatch.setitem(mock_rpc_device.status["sys"], "relay_in_thermostat", False) await init_integration(hass, 2) - entity_id = "switch.test_switch_0" + entity_id = "switch.test_name_test_switch_0" await hass.services.async_call( SWITCH_DOMAIN, 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) 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" @@ -386,11 +390,11 @@ async def test_rpc_device_switch_type_lights_mode( [ ( 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"), - "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( SWITCH_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "switch.test_switch_0"}, + {ATTR_ENTITY_ID: "switch.test_name_test_switch_0"}, blocking=True, ) @@ -434,7 +438,7 @@ async def test_rpc_auth_error( await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "switch.test_switch_0"}, + {ATTR_ENTITY_ID: "switch.test_name_test_switch_0"}, blocking=True, ) @@ -476,8 +480,8 @@ async def test_wall_display_relay_mode( monkeypatch: pytest.MonkeyPatch, ) -> None: """Test Wall Display in relay mode.""" - climate_entity_id = "climate.test_name_thermostat_0" - switch_entity_id = "switch.test_switch_0" + climate_entity_id = "climate.test_name" + switch_entity_id = "switch.test_name_test_switch_0" config_entry = await init_integration(hass, 2, model=MODEL_WALL_DISPLAY) diff --git a/tests/components/shelly/test_utils.py b/tests/components/shelly/test_utils.py index ae3caa93825..0cdd1640e65 100644 --- a/tests/components/shelly/test_utils.py +++ b/tests/components/shelly/test_utils.py @@ -79,37 +79,38 @@ async def test_block_get_block_channel_name( mock_block_device: Mock, monkeypatch: pytest.MonkeyPatch ) -> None: """Test block get block channel name.""" - monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "type", "relay") - - assert ( - get_block_channel_name( - mock_block_device, - mock_block_device.blocks[DEVICE_BLOCK_ID], - ) - == "Test name channel 1" + 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.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) - - assert ( - get_block_channel_name( - mock_block_device, - mock_block_device.blocks[DEVICE_BLOCK_ID], - ) - == "Test name channel A" + 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, "relays", [{"name": "test-channel"}] ) - - assert ( - get_block_channel_name( - mock_block_device, - mock_block_device.blocks[DEVICE_BLOCK_ID], - ) - == "test-channel" + 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 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: """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:3") == "Test name Input 3" + assert get_rpc_channel_name(mock_rpc_device, "input:0") == "Test input 0" + assert get_rpc_channel_name(mock_rpc_device, "input:3") == "Input 3" @pytest.mark.parametrize( ("component", "expected"), [ - ("cover", "Cover"), - ("input", "Input"), - ("light", "Light"), - ("rgb", "RGB light"), - ("rgbw", "RGBW light"), - ("switch", "Switch"), - ("thermostat", "Thermostat"), + ("cover", None), + ("light", None), + ("rgb", None), + ("rgbw", None), + ("switch", None), + ("thermostat", None), ], ) 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) - assert ( - get_rpc_channel_name(mock_rpc_device, f"{component}:0") - == f"Test name {expected} 0" - ) - assert ( - get_rpc_channel_name(mock_rpc_device, f"{component}:1") - == f"Test name {expected} 1" - ) + # we use sub-devices, so the entity name is not set + assert get_rpc_channel_name(mock_rpc_device, f"{component}:0") == expected + assert get_rpc_channel_name(mock_rpc_device, f"{component}:1") == expected async def test_get_rpc_input_triggers(