mirror of
https://github.com/home-assistant/core.git
synced 2026-09-26 17:31:15 -04:00
bosch_shc: add child lock switch entities (#181423)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
8cee738f23
commit
247669f151
@@ -12,6 +12,9 @@
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"child_lock": {
|
||||
"default": "mdi:lock"
|
||||
},
|
||||
"routing": {
|
||||
"default": "mdi:wifi"
|
||||
}
|
||||
|
||||
@@ -67,6 +67,9 @@
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"child_lock": {
|
||||
"name": "Child lock"
|
||||
},
|
||||
"routing": {
|
||||
"name": "Routing"
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ from boschshcpy import (
|
||||
PowerSwitchService,
|
||||
PrivacyModeService,
|
||||
SHCSmartPlug,
|
||||
ThermostatService,
|
||||
)
|
||||
from boschshcpy.device import SHCDevice
|
||||
|
||||
@@ -32,7 +33,7 @@ class SHCSwitchEntityDescription(SwitchEntityDescription):
|
||||
"""Class describing SHC switch entities."""
|
||||
|
||||
on_key: str
|
||||
on_value: Enum
|
||||
on_value: bool | Enum
|
||||
should_poll: bool
|
||||
|
||||
|
||||
@@ -72,6 +73,24 @@ SWITCH_TYPES: dict[str, SHCSwitchEntityDescription] = {
|
||||
on_value=PrivacyModeService.State.DISABLED,
|
||||
should_poll=True,
|
||||
),
|
||||
"child_lock": SHCSwitchEntityDescription(
|
||||
key="child_lock",
|
||||
translation_key="child_lock",
|
||||
device_class=SwitchDeviceClass.SWITCH,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
on_key="child_lock",
|
||||
on_value=True,
|
||||
should_poll=False,
|
||||
),
|
||||
"child_lock_thermostat": SHCSwitchEntityDescription(
|
||||
key="child_lock_thermostat",
|
||||
translation_key="child_lock",
|
||||
device_class=SwitchDeviceClass.SWITCH,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
on_key="child_lock",
|
||||
on_value=ThermostatService.State.ON,
|
||||
should_poll=False,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -152,6 +171,42 @@ async def async_setup_entry(
|
||||
for switch in session.device_helper.camera_360
|
||||
)
|
||||
|
||||
entities.extend(
|
||||
SHCSwitch(
|
||||
hass=hass,
|
||||
device=switch,
|
||||
parent_id=shc_info.unique_id,
|
||||
entry_id=config_entry.entry_id,
|
||||
description=SWITCH_TYPES["child_lock_thermostat"],
|
||||
unique_id_suffix="child_lock",
|
||||
)
|
||||
for switch in (
|
||||
*session.device_helper.thermostats,
|
||||
*session.device_helper.roomthermostats,
|
||||
*session.device_helper.wallthermostats,
|
||||
)
|
||||
)
|
||||
|
||||
entities.extend(
|
||||
SHCSwitch(
|
||||
hass=hass,
|
||||
device=switch,
|
||||
parent_id=shc_info.unique_id,
|
||||
entry_id=config_entry.entry_id,
|
||||
description=SWITCH_TYPES["child_lock"],
|
||||
unique_id_suffix="child_lock",
|
||||
)
|
||||
for switch in (
|
||||
*session.device_helper.micromodule_shutter_controls,
|
||||
*session.device_helper.micromodule_blinds,
|
||||
*session.device_helper.micromodule_light_attached,
|
||||
*session.device_helper.micromodule_relays,
|
||||
*session.device_helper.micromodule_impulse_relays,
|
||||
*session.device_helper.micromodule_dimmers,
|
||||
*session.device_helper.light_switches_bsm,
|
||||
)
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
@@ -167,10 +222,13 @@ class SHCSwitch(SHCEntity, SwitchEntity):
|
||||
parent_id: str,
|
||||
entry_id: str,
|
||||
description: SHCSwitchEntityDescription,
|
||||
unique_id_suffix: str | None = None,
|
||||
) -> None:
|
||||
"""Initialize a SHC switch."""
|
||||
super().__init__(hass, device, parent_id, entry_id)
|
||||
self.entity_description = description
|
||||
if unique_id_suffix is not None:
|
||||
self._attr_unique_id = f"{device.serial}_{unique_id_suffix}"
|
||||
|
||||
@property
|
||||
@override
|
||||
|
||||
@@ -5,7 +5,15 @@ from types import SimpleNamespace
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, create_autospec, patch
|
||||
|
||||
from boschshcpy import BatteryLevelService, SHCBatteryDevice
|
||||
from boschshcpy import (
|
||||
BatteryLevelService,
|
||||
PowerSwitchService,
|
||||
SHCBatteryDevice,
|
||||
SHCLightSwitchBSM,
|
||||
SHCMicromoduleRelay,
|
||||
SHCThermostat,
|
||||
ThermostatService,
|
||||
)
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.bosch_shc.const import (
|
||||
@@ -43,9 +51,21 @@ def mock_config_entry() -> MockConfigEntry:
|
||||
_EMPTY_DEVICE_BUCKETS: dict[str, list[Any]] = {
|
||||
bucket: []
|
||||
for bucket in (
|
||||
"camera_360",
|
||||
"camera_eyes",
|
||||
"light_switches_bsm",
|
||||
"micromodule_blinds",
|
||||
"micromodule_dimmers",
|
||||
"micromodule_impulse_relays",
|
||||
"micromodule_light_attached",
|
||||
"micromodule_relays",
|
||||
"micromodule_shutter_controls",
|
||||
"motion_detectors",
|
||||
"roomthermostats",
|
||||
"shutter_contacts",
|
||||
"shutter_contacts2",
|
||||
"smart_plugs",
|
||||
"smart_plugs_compact",
|
||||
"smoke_detectors",
|
||||
"thermostats",
|
||||
"twinguards",
|
||||
@@ -112,3 +132,68 @@ def battery_only_device(
|
||||
device.status = "AVAILABLE"
|
||||
device.deleted = False
|
||||
return device
|
||||
|
||||
|
||||
def thermostat_device(
|
||||
device_id: str = "hdm:ZigBee:thermostat1",
|
||||
name: str = "Thermostat",
|
||||
child_lock: ThermostatService.State = ThermostatService.State.OFF,
|
||||
) -> SHCThermostat:
|
||||
"""Build a minimal device double for the thermostats/roomthermostats/wallthermostats buckets."""
|
||||
device = create_autospec(SHCThermostat, instance=True, spec_set=True)
|
||||
device.name = name
|
||||
device.id = device_id
|
||||
device.root_device_id = "test-mac"
|
||||
device.serial = f"serial-{device_id}"
|
||||
device.manufacturer = "Bosch"
|
||||
device.device_model = "TRV"
|
||||
device.device_services = []
|
||||
device.deleted = False
|
||||
device.status = "AVAILABLE"
|
||||
device.child_lock = child_lock
|
||||
return device
|
||||
|
||||
|
||||
def micromodule_relay_device(
|
||||
device_id: str = "hdm:ZigBee:relay1",
|
||||
name: str = "Relay",
|
||||
child_lock: bool = False,
|
||||
) -> SHCMicromoduleRelay:
|
||||
"""Build a minimal device double for the micromodule_relays bucket."""
|
||||
device = create_autospec(SHCMicromoduleRelay, instance=True, spec_set=True)
|
||||
device.name = name
|
||||
device.id = device_id
|
||||
device.root_device_id = "test-mac"
|
||||
device.serial = f"serial-{device_id}"
|
||||
device.manufacturer = "Bosch"
|
||||
device.device_model = "MICROMODULE_RELAY"
|
||||
device.device_services = []
|
||||
device.deleted = False
|
||||
device.status = "AVAILABLE"
|
||||
device.child_lock = child_lock
|
||||
return device
|
||||
|
||||
|
||||
def light_switch_bsm_device(
|
||||
device_id: str = "hdm:ZigBee:lightswitch1",
|
||||
name: str = "Light switch",
|
||||
child_lock: bool = False,
|
||||
) -> SHCLightSwitchBSM:
|
||||
"""Build a minimal device double for the light_switches_bsm bucket.
|
||||
|
||||
Backs both the primary "lightswitch" switch and the new child-lock
|
||||
switch, so a unique_id collision between the two would surface here.
|
||||
"""
|
||||
device = create_autospec(SHCLightSwitchBSM, instance=True, spec_set=True)
|
||||
device.name = name
|
||||
device.id = device_id
|
||||
device.root_device_id = "test-mac"
|
||||
device.serial = f"serial-{device_id}"
|
||||
device.manufacturer = "Bosch"
|
||||
device.device_model = "LIGHT_SWITCH_BSM"
|
||||
device.device_services = []
|
||||
device.deleted = False
|
||||
device.status = "AVAILABLE"
|
||||
device.switchstate = PowerSwitchService.State.OFF
|
||||
device.child_lock = child_lock
|
||||
return device
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
"""Tests for the Bosch SHC switch platform."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from boschshcpy import ThermostatService
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
SERVICE_TURN_OFF,
|
||||
SERVICE_TURN_ON,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from .conftest import (
|
||||
light_switch_bsm_device,
|
||||
micromodule_relay_device,
|
||||
setup_integration,
|
||||
thermostat_device,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def platforms() -> Generator[None]:
|
||||
"""Restrict bosch_shc setup to the switch platform."""
|
||||
with patch("homeassistant.components.bosch_shc.PLATFORMS", [Platform.SWITCH]):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"device_buckets",
|
||||
[{"thermostats": [thermostat_device(child_lock=ThermostatService.State.OFF)]}],
|
||||
indirect=True,
|
||||
)
|
||||
@pytest.mark.usefixtures("mock_session")
|
||||
async def test_thermostat_child_lock(
|
||||
hass: HomeAssistant,
|
||||
mock_session: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""A thermostat's enum-based child lock is exposed and controllable."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
device = mock_session.device_helper.thermostats[0]
|
||||
|
||||
state = hass.states.get("switch.thermostat_child_lock")
|
||||
assert state is not None
|
||||
assert state.state == "off"
|
||||
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: "switch.thermostat_child_lock"},
|
||||
blocking=True,
|
||||
)
|
||||
assert device.child_lock is True
|
||||
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: "switch.thermostat_child_lock"},
|
||||
blocking=True,
|
||||
)
|
||||
assert device.child_lock is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"device_buckets",
|
||||
[{"micromodule_relays": [micromodule_relay_device(child_lock=False)]}],
|
||||
indirect=True,
|
||||
)
|
||||
@pytest.mark.usefixtures("mock_session")
|
||||
async def test_micromodule_relay_child_lock(
|
||||
hass: HomeAssistant,
|
||||
mock_session: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""A ChildProtection device's bool-based child lock is exposed and controllable."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
device = mock_session.device_helper.micromodule_relays[0]
|
||||
|
||||
state = hass.states.get("switch.relay_child_lock")
|
||||
assert state is not None
|
||||
assert state.state == "off"
|
||||
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: "switch.relay_child_lock"},
|
||||
blocking=True,
|
||||
)
|
||||
assert device.child_lock is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"device_buckets",
|
||||
[{"light_switches_bsm": [light_switch_bsm_device(child_lock=False)]}],
|
||||
indirect=True,
|
||||
)
|
||||
@pytest.mark.usefixtures("mock_session")
|
||||
async def test_light_switch_bsm_child_lock_unique_id(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""A BSM light switch's primary switch and child-lock switch use distinct unique_ids."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
lightswitch_entry = entity_registry.async_get("switch.light_switch")
|
||||
child_lock_entry = entity_registry.async_get("switch.light_switch_child_lock")
|
||||
assert lightswitch_entry is not None
|
||||
assert child_lock_entry is not None
|
||||
assert lightswitch_entry.unique_id != child_lock_entry.unique_id
|
||||
Reference in New Issue
Block a user