Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 3 additions & 0 deletions homeassistant/components/bosch_shc/icons.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
}
},
"switch": {
"child_lock": {
"default": "mdi:lock"
},
"routing": {
"default": "mdi:wifi"
}
Expand Down
3 changes: 3 additions & 0 deletions homeassistant/components/bosch_shc/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@
}
},
"switch": {
"child_lock": {
"name": "Child lock"
},
"routing": {
"name": "Routing"
}
Expand Down
60 changes: 59 additions & 1 deletion homeassistant/components/bosch_shc/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
PowerSwitchService,
PrivacyModeService,
SHCSmartPlug,
ThermostatService,
)
from boschshcpy.device import SHCDevice

Expand All @@ -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


Expand Down Expand Up @@ -72,6 +73,24 @@ class SHCSwitchEntityDescription(SwitchEntityDescription):
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,
),
}


Expand Down Expand Up @@ -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)


Expand All @@ -167,10 +222,13 @@ def __init__(
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
Expand Down
60 changes: 59 additions & 1 deletion tests/components/bosch_shc/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@
from typing import Any
from unittest.mock import MagicMock, create_autospec, patch

from boschshcpy import BatteryLevelService, SHCBatteryDevice
from boschshcpy import (
BatteryLevelService,
SHCBatteryDevice,
SHCMicromoduleRelay,
SHCThermostat,
ThermostatService,
)
import pytest

from homeassistant.components.bosch_shc.const import (
Expand Down Expand Up @@ -43,9 +49,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",
Expand Down Expand Up @@ -112,3 +130,43 @@ 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
91 changes: 91 additions & 0 deletions tests/components/bosch_shc/test_switch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"""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 .conftest import 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
Loading