Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 18 additions & 9 deletions homeassistant/components/sensor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -862,16 +862,25 @@ def _custom_unit_or_undef(
"""Return a custom unit, or UNDEFINED if not compatible with the native unit."""
assert self.registry_entry
if (
(sensor_options := self.registry_entry.options.get(primary_key))
and secondary_key in sensor_options
and (device_class := self.device_class) in UNIT_CONVERTERS
and self.__native_unit_of_measurement_compat
in UNIT_CONVERTERS[device_class].VALID_UNITS
and (custom_unit := sensor_options[secondary_key])
in UNIT_CONVERTERS[device_class].VALID_UNITS
sensor_options := self.registry_entry.options.get(primary_key)
) is None or secondary_key not in sensor_options:
return UNDEFINED

if (device_class := self.device_class) not in UNIT_CONVERTERS:
return UNDEFINED

if (
self.__native_unit_of_measurement_compat
not in UNIT_CONVERTERS[device_class].VALID_UNITS
):
return cast(str, custom_unit)
return UNDEFINED
return UNDEFINED

if (custom_unit := sensor_options[secondary_key]) not in UNIT_CONVERTERS[
device_class
].VALID_UNITS:
return UNDEFINED

return cast(str, custom_unit)

@callback
def async_registry_entry_updated(self) -> None:
Expand Down
16 changes: 8 additions & 8 deletions homeassistant/components/shelly/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
async_remove_orphaned_entities,
get_blu_trv_device_info,
get_device_entry_gen,
get_virtual_component_ids,
is_block_momentary_input,
is_rpc_momentary_input,
is_view_for_platform,
Expand Down Expand Up @@ -307,6 +306,13 @@ def __init__(
device_class=BinarySensorDeviceClass.OCCUPANCY,
entity_class=RpcPresenceBinarySensor,
),
"presencezone_state": RpcBinarySensorDescription(
key="presencezone",
sub_key="state",
name="Occupancy",
device_class=BinarySensorDeviceClass.OCCUPANCY,
entity_class=RpcPresenceBinarySensor,
),
}


Expand All @@ -333,18 +339,12 @@ async def async_setup_entry(
hass, config_entry, async_add_entities, RPC_SENSORS, RpcBinarySensor
)

# the user can remove virtual components from the device configuration, so
# we need to remove orphaned entities
virtual_binary_sensor_ids = get_virtual_component_ids(
coordinator.device.config, BINARY_SENSOR_PLATFORM
)
async_remove_orphaned_entities(
hass,
config_entry.entry_id,
coordinator.mac,
BINARY_SENSOR_PLATFORM,
virtual_binary_sensor_ids,
"boolean",
coordinator.device.status,
)
return

Expand Down
33 changes: 26 additions & 7 deletions homeassistant/components/shelly/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from aioshelly.const import BLU_TRV_IDENTIFIER, MODEL_BLU_GATEWAY_G3, RPC_GENERATIONS
from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCallError
from aioshelly.rpc_device import RpcDevice

from homeassistant.components.button import (
DOMAIN as BUTTON_PLATFORM,
Expand All @@ -22,13 +23,13 @@
from homeassistant.helpers import entity_registry as er
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 .entity import get_entity_block_device_info, get_entity_rpc_device_info
from .utils import (
async_remove_orphaned_entities,
format_ble_addr,
get_blu_trv_device_info,
get_device_entry_gen,
get_rpc_entity_name,
Expand Down Expand Up @@ -112,12 +113,10 @@ def async_migrate_unique_ids(
if not entity_entry.entity_id.startswith("button"):
return None

device_name = slugify(coordinator.device.name)

for key in ("reboot", "self_test", "mute", "unmute"):
old_unique_id = f"{device_name}_{key}"
old_unique_id = f"{coordinator.mac}_{key}"
if entity_entry.unique_id == old_unique_id:
new_unique_id = f"{coordinator.mac}_{key}"
new_unique_id = f"{coordinator.mac}-{key}"
LOGGER.debug(
"Migrating unique_id for %s entity from [%s] to [%s]",
entity_entry.entity_id,
Expand All @@ -130,6 +129,26 @@ def async_migrate_unique_ids(
)
}

if blutrv_key_ids := get_rpc_key_ids(coordinator.device.status, BLU_TRV_IDENTIFIER):
assert isinstance(coordinator.device, RpcDevice)
for _id in blutrv_key_ids:
key = f"{BLU_TRV_IDENTIFIER}:{_id}"
ble_addr: str = coordinator.device.config[key]["addr"]
old_unique_id = f"{ble_addr}_calibrate"
if entity_entry.unique_id == old_unique_id:
new_unique_id = f"{format_ble_addr(ble_addr)}-{key}-calibrate"
LOGGER.debug(
"Migrating unique_id for %s entity from [%s] to [%s]",
entity_entry.entity_id,
old_unique_id,
new_unique_id,
)
return {
"new_unique_id": entity_entry.unique_id.replace(
old_unique_id, new_unique_id
)
}

return None


Expand Down Expand Up @@ -264,7 +283,7 @@ def __init__(
"""Initialize Shelly button."""
super().__init__(coordinator, description)

self._attr_unique_id = f"{coordinator.mac}_{description.key}"
self._attr_unique_id = f"{coordinator.mac}-{description.key}"
if isinstance(coordinator, ShellyBlockCoordinator):
self._attr_device_info = get_entity_block_device_info(coordinator)
else:
Expand Down Expand Up @@ -297,7 +316,7 @@ def __init__(
ble_addr: str = config["addr"]
fw_ver = coordinator.device.status[key].get("fw_ver")

self._attr_unique_id = f"{ble_addr}_{description.key}"
self._attr_unique_id = f"{format_ble_addr(ble_addr)}-{key}-{description.key}"
self._attr_device_info = get_blu_trv_device_info(
config, ble_addr, coordinator.mac, fw_ver
)
Expand Down
10 changes: 9 additions & 1 deletion homeassistant/components/shelly/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,15 @@ class BLEScannerMode(StrEnum):

CONF_GEN = "gen"

VIRTUAL_COMPONENTS = ("boolean", "button", "enum", "input", "number", "text")
VIRTUAL_COMPONENTS = (
"boolean",
"button",
"enum",
"input",
"number",
"presencezone",
"text",
)
VIRTUAL_COMPONENTS_MAP = {
"binary_sensor": {"types": ["boolean"], "modes": ["label"]},
"button": {"types": ["button"], "modes": ["button"]},
Expand Down
24 changes: 8 additions & 16 deletions homeassistant/components/shelly/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@
get_device_entry_gen,
get_device_uptime,
get_shelly_air_lamp_life,
get_virtual_component_ids,
get_virtual_component_unit,
is_rpc_wifi_stations_disabled,
is_view_for_platform,
Expand Down Expand Up @@ -1459,6 +1458,14 @@ def __init__(
state_class=SensorStateClass.MEASUREMENT,
entity_class=RpcPresenceSensor,
),
"presencezone_num_objects": RpcSensorDescription(
key="presencezone",
sub_key="num_objects",
translation_key="detected_objects",
name="Detected objects",
state_class=SensorStateClass.MEASUREMENT,
entity_class=RpcPresenceSensor,
),
"object_water_consumption": RpcSensorDescription(
key="object",
sub_key="value",
Expand Down Expand Up @@ -1570,21 +1577,6 @@ async def async_setup_entry(
SENSOR_PLATFORM,
coordinator.device.status,
)

# the user can remove virtual components from the device configuration, so
# we need to remove orphaned entities
virtual_component_ids = get_virtual_component_ids(
coordinator.device.config, SENSOR_PLATFORM
)
for component in ("enum", "number", "text"):
async_remove_orphaned_entities(
hass,
config_entry.entry_id,
coordinator.mac,
SENSOR_PLATFORM,
virtual_component_ids,
component,
)
return

if config_entry.data[CONF_SLEEP_PERIOD]:
Expand Down
5 changes: 5 additions & 0 deletions homeassistant/components/shelly/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -918,3 +918,8 @@ def remove_empty_sub_devices(hass: HomeAssistant, entry: ConfigEntry) -> None:
dev_reg.async_update_device(
device.id, remove_config_entry_id=entry.entry_id
)


def format_ble_addr(ble_addr: str) -> str:
"""Format BLE address to use in unique_id."""
return ble_addr.replace(":", "").upper()
4 changes: 2 additions & 2 deletions tests/components/shelly/snapshots/test_button.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'calibrate',
'unique_id': 'f8:44:77:25:f0:dd_calibrate',
'unique_id': 'F8447725F0DD-blutrv:200-calibrate',
'unit_of_measurement': None,
})
# ---
Expand Down Expand Up @@ -78,7 +78,7 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '123456789ABC_reboot',
'unique_id': '123456789ABC-reboot',
'unit_of_measurement': None,
})
# ---
Expand Down
6 changes: 3 additions & 3 deletions tests/components/shelly/snapshots/test_devices.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,7 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '123456789ABC_reboot',
'unique_id': '123456789ABC-reboot',
'unit_of_measurement': None,
})
# ---
Expand Down Expand Up @@ -1672,7 +1672,7 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '123456789ABC_reboot',
'unique_id': '123456789ABC-reboot',
'unit_of_measurement': None,
})
# ---
Expand Down Expand Up @@ -2935,7 +2935,7 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '123456789ABC_reboot',
'unique_id': '123456789ABC-reboot',
'unit_of_measurement': None,
})
# ---
Expand Down
43 changes: 43 additions & 0 deletions tests/components/shelly/test_binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -591,3 +591,46 @@ async def test_rpc_presence_component(

assert (state := hass.states.get(entity_id))
assert state.state == STATE_UNAVAILABLE


async def test_rpc_presencezone_component(
hass: HomeAssistant,
mock_rpc_device: Mock,
monkeypatch: pytest.MonkeyPatch,
entity_registry: EntityRegistry,
) -> None:
"""Test RPC binary sensor entity for presencezone component."""
config = deepcopy(mock_rpc_device.config)
config["presencezone:200"] = {"name": "Main zone", "enable": True}
monkeypatch.setattr(mock_rpc_device, "config", config)

status = deepcopy(mock_rpc_device.status)
status["presencezone:200"] = {"state": True, "num_objects": 3}
monkeypatch.setattr(mock_rpc_device, "status", status)

mock_config_entry = await init_integration(hass, 4)

entity_id = f"{BINARY_SENSOR_DOMAIN}.test_name_main_zone_occupancy"

assert (state := hass.states.get(entity_id))
assert state.state == STATE_ON

assert (entry := entity_registry.async_get(entity_id))
assert entry.unique_id == "123456789ABC-presencezone:200-presencezone_state"

mutate_rpc_device_status(
monkeypatch, mock_rpc_device, "presencezone:200", "state", False
)
mock_rpc_device.mock_update()

assert (state := hass.states.get(entity_id))
assert state.state == STATE_OFF

config = deepcopy(mock_rpc_device.config)
config["presencezone:200"] = {"enable": False}
monkeypatch.setattr(mock_rpc_device, "config", config)
await hass.config_entries.async_reload(mock_config_entry.entry_id)
mock_rpc_device.mock_update()

assert (state := hass.states.get(entity_id))
assert state.state == STATE_UNAVAILABLE
39 changes: 35 additions & 4 deletions tests/components/shelly/test_button.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ async def test_block_button(
assert state.state == STATE_UNKNOWN

assert (entry := entity_registry.async_get(entity_id))
assert entry.unique_id == "123456789ABC_reboot"
assert entry.unique_id == "123456789ABC-reboot"

await hass.services.async_call(
BUTTON_DOMAIN,
Expand Down Expand Up @@ -136,9 +136,9 @@ async def test_rpc_button_reauth_error(
@pytest.mark.parametrize(
("gen", "old_unique_id", "new_unique_id", "migration"),
[
(2, "test_name_reboot", "123456789ABC_reboot", True),
(1, "test_name_reboot", "123456789ABC_reboot", True),
(2, "123456789ABC_reboot", "123456789ABC_reboot", False),
(2, "123456789ABC_reboot", "123456789ABC-reboot", True),
(1, "123456789ABC_reboot", "123456789ABC-reboot", True),
(2, "123456789ABC-reboot", "123456789ABC-reboot", False),
],
)
async def test_migrate_unique_id(
Expand Down Expand Up @@ -379,3 +379,34 @@ async def test_wall_display_virtual_button(
blocking=True,
)
mock_rpc_device.button_trigger.assert_called_once_with(200, "single_push")


async def test_migrate_unique_id_blu_trv(
hass: HomeAssistant,
mock_blu_trv: Mock,
entity_registry: EntityRegistry,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test migration of unique_id for BLU TRV button."""
entry = await init_integration(hass, 3, model=MODEL_BLU_GATEWAY_G3, skip_setup=True)

old_unique_id = "f8:44:77:25:f0:dd_calibrate"

entity = entity_registry.async_get_or_create(
suggested_object_id="trv_name_calibrate",
disabled_by=None,
domain=BUTTON_DOMAIN,
platform=DOMAIN,
unique_id=old_unique_id,
config_entry=entry,
)
assert entity.unique_id == old_unique_id

await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()

entity_entry = entity_registry.async_get("button.trv_name_calibrate")
assert entity_entry
assert entity_entry.unique_id == "F8447725F0DD-blutrv:200-calibrate"

assert "Migrating unique_id for button.trv_name_calibrate" in caplog.text
Loading
Loading