mirror of
https://github.com/home-assistant/core.git
synced 2026-10-07 23:09:19 -04:00
Allow integration of the Shelly BLU TRV via a device other than the BLU Gateway (#182422)
This commit is contained in:
@@ -4,7 +4,7 @@ from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Any, Final, override
|
||||
|
||||
from aioshelly.const import MODEL_BLU_GATEWAY_G3, RPC_GENERATIONS
|
||||
from aioshelly.const import RPC_GENERATIONS
|
||||
from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCallError
|
||||
|
||||
from homeassistant.components.button import (
|
||||
@@ -368,7 +368,6 @@ RPC_BUTTONS = {
|
||||
translation_key="calibrate",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
entity_class=ShellyBluTrvButton,
|
||||
models={MODEL_BLU_GATEWAY_G3},
|
||||
),
|
||||
"smoke_mute": RpcButtonDescription(
|
||||
key="smoke",
|
||||
|
||||
@@ -13,7 +13,6 @@ from aioshelly.const import (
|
||||
DEFAULT_COAP_PORT,
|
||||
DEFAULT_HTTP_PORT,
|
||||
MODEL_1L,
|
||||
MODEL_BLU_GATEWAY_G3,
|
||||
MODEL_DIMMER,
|
||||
MODEL_DIMMER_2,
|
||||
MODEL_EM3,
|
||||
@@ -922,9 +921,6 @@ def remove_stale_blu_trv_devices(
|
||||
hass: HomeAssistant, rpc_device: RpcDevice, entry: ConfigEntry
|
||||
) -> None:
|
||||
"""Remove stale BLU TRV devices."""
|
||||
if rpc_device.model != MODEL_BLU_GATEWAY_G3:
|
||||
return
|
||||
|
||||
dev_reg = dr.async_get(hass)
|
||||
devices = dr.async_entries_for_config_entry(dev_reg, entry.entry_id)
|
||||
config = rpc_device.config
|
||||
@@ -936,6 +932,12 @@ def remove_stale_blu_trv_devices(
|
||||
# Device is not a sub-device, skip
|
||||
continue
|
||||
|
||||
if not any(
|
||||
connection[0] == CONNECTION_BLUETOOTH for connection in device.connections
|
||||
):
|
||||
# Channel sub-devices have no Bluetooth connection
|
||||
continue
|
||||
|
||||
if any(
|
||||
identifier[0] == DOMAIN and identifier[1] in trv_addrs
|
||||
for identifier in device.identifiers
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# serializer version: 1
|
||||
# name: test_rpc_blu_trv_button[button.trv_name_calibrate-entry]
|
||||
# name: test_rpc_blu_trv_button[S3GW-1DBT001][button.trv_name_calibrate-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
@@ -36,7 +36,57 @@
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_rpc_blu_trv_button[button.trv_name_calibrate-state]
|
||||
# name: test_rpc_blu_trv_button[S3GW-1DBT001][button.trv_name_calibrate-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'TRV-Name Calibrate',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'button.trv_name_calibrate',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_rpc_blu_trv_button[SNSW-002P16EU][button.trv_name_calibrate-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'button',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'button.trv_name_calibrate',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Calibrate',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Calibrate',
|
||||
'platform': 'shelly',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'calibrate',
|
||||
'unique_id': 'F8447725F0DD-blutrv:200-calibrate',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_rpc_blu_trv_button[SNSW-002P16EU][button.trv_name_calibrate-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'TRV-Name Calibrate',
|
||||
|
||||
@@ -3,7 +3,12 @@
|
||||
from copy import deepcopy
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
from aioshelly.const import MODEL_BLU_GATEWAY_G3, MODEL_PLUS_SMOKE, MODEL_WALL_DISPLAY
|
||||
from aioshelly.const import (
|
||||
MODEL_BLU_GATEWAY_G3,
|
||||
MODEL_PLUS_2PM,
|
||||
MODEL_PLUS_SMOKE,
|
||||
MODEL_WALL_DISPLAY,
|
||||
)
|
||||
from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCallError
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
@@ -153,19 +158,21 @@ async def test_rpc_button_reauth_error(
|
||||
assert flow["context"].get("entry_id") == entry.entry_id
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", [MODEL_BLU_GATEWAY_G3, MODEL_PLUS_2PM])
|
||||
async def test_rpc_blu_trv_button(
|
||||
hass: HomeAssistant,
|
||||
mock_blu_trv: Mock,
|
||||
entity_registry: EntityRegistry,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
snapshot: SnapshotAssertion,
|
||||
model: str,
|
||||
) -> None:
|
||||
"""Test RPC BLU TRV button."""
|
||||
monkeypatch.delitem(mock_blu_trv.status, "script:1")
|
||||
monkeypatch.delitem(mock_blu_trv.status, "script:2")
|
||||
monkeypatch.delitem(mock_blu_trv.status, "script:3")
|
||||
|
||||
await init_integration(hass, 3, model=MODEL_BLU_GATEWAY_G3)
|
||||
await init_integration(hass, 3, model=model)
|
||||
|
||||
entity_id = "button.trv_name_calibrate"
|
||||
|
||||
|
||||
@@ -7,7 +7,12 @@ from unittest.mock import AsyncMock, Mock, call, patch
|
||||
|
||||
from aioshelly.block_device import COAP
|
||||
from aioshelly.common import ConnectionOptions
|
||||
from aioshelly.const import DEFAULT_HTTPS_PORT, MODEL_BLU_GATEWAY_G3, MODEL_PLUS_2PM
|
||||
from aioshelly.const import (
|
||||
DEFAULT_HTTPS_PORT,
|
||||
MODEL_2PM_G3,
|
||||
MODEL_BLU_GATEWAY_G3,
|
||||
MODEL_PLUS_2PM,
|
||||
)
|
||||
from aioshelly.exceptions import (
|
||||
DeviceConnectionError,
|
||||
InvalidAuthError,
|
||||
@@ -42,6 +47,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.helpers.device_registry import (
|
||||
CONNECTION_NETWORK_MAC,
|
||||
EVENT_DEVICE_REGISTRY_UPDATED,
|
||||
DeviceRegistry,
|
||||
format_mac,
|
||||
)
|
||||
@@ -50,7 +56,11 @@ from homeassistant.setup import async_setup_component
|
||||
|
||||
from . import MOCK_MAC, init_integration, mutate_rpc_device_status, register_sub_device
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.common import (
|
||||
MockConfigEntry,
|
||||
async_capture_events,
|
||||
async_load_json_object_fixture,
|
||||
)
|
||||
|
||||
|
||||
async def test_custom_coap_port(
|
||||
@@ -760,19 +770,21 @@ async def test_migrate_ble_scanner_mode_future_minor_version(
|
||||
assert entry.options[CONF_BLE_SCANNER_MODE] == BLEScannerMode.ACTIVE
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", [MODEL_BLU_GATEWAY_G3, MODEL_PLUS_2PM])
|
||||
async def test_blu_trv_stale_device_removal(
|
||||
hass: HomeAssistant,
|
||||
mock_blu_trv: Mock,
|
||||
entity_registry: EntityRegistry,
|
||||
device_registry: DeviceRegistry,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
model: str,
|
||||
) -> None:
|
||||
"""Test BLU TRV removal of stale a device after un-pairing."""
|
||||
trv_200_entity_id = "climate.trv_name"
|
||||
trv_201_entity_id = "climate.trv_201"
|
||||
|
||||
monkeypatch.setattr(mock_blu_trv, "model", MODEL_BLU_GATEWAY_G3)
|
||||
gw_entry = await init_integration(hass, 3, model=MODEL_BLU_GATEWAY_G3)
|
||||
monkeypatch.setattr(mock_blu_trv, "model", model)
|
||||
gw_entry = await init_integration(hass, 3, model=model)
|
||||
|
||||
# verify that both trv devices are present
|
||||
assert hass.states.get(trv_200_entity_id) is not None
|
||||
@@ -833,6 +845,39 @@ async def test_empty_device_removal(
|
||||
assert device_registry.async_get(sub_device_entry.id) is None
|
||||
|
||||
|
||||
async def test_sub_device_kept_on_reload(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: EntityRegistry,
|
||||
device_registry: DeviceRegistry,
|
||||
mock_rpc_device: Mock,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Test channel sub-devices are not removed as stale BLU TRV devices."""
|
||||
device_fixture = await async_load_json_object_fixture(hass, "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"])
|
||||
config_entry = await init_integration(hass, gen=3, model=MODEL_2PM_G3)
|
||||
|
||||
sub_device_ids = set()
|
||||
for entity_id in ("switch.test_name_output_0", "switch.test_name_output_1"):
|
||||
entry = entity_registry.async_get(entity_id)
|
||||
assert entry
|
||||
device_entry = device_registry.async_get(entry.device_id)
|
||||
assert device_entry
|
||||
assert device_entry.via_device_id
|
||||
sub_device_ids.add(device_entry.id)
|
||||
|
||||
events = async_capture_events(hass, EVENT_DEVICE_REGISTRY_UPDATED)
|
||||
|
||||
await hass.config_entries.async_reload(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert not [event for event in events if event.data["action"] == "remove"]
|
||||
for device_id in sub_device_ids:
|
||||
assert device_registry.async_get(device_id)
|
||||
|
||||
|
||||
async def test_rpc_waits_for_ble_scanner_at_startup(
|
||||
hass: HomeAssistant,
|
||||
mock_rpc_device: Mock,
|
||||
|
||||
Reference in New Issue
Block a user