mirror of
https://github.com/home-assistant/core.git
synced 2026-09-26 17:31:15 -04:00
bosch_shc: add alarm bypass switch entities (#182246)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
cf3580a8e4
commit
1f949d9234
@@ -12,6 +12,12 @@
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"bypass": {
|
||||
"default": "mdi:shield-off-outline"
|
||||
},
|
||||
"bypass_infinite": {
|
||||
"default": "mdi:timer-off-outline"
|
||||
},
|
||||
"child_lock": {
|
||||
"default": "mdi:lock"
|
||||
},
|
||||
|
||||
@@ -70,6 +70,12 @@
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"bypass": {
|
||||
"name": "Break function"
|
||||
},
|
||||
"bypass_infinite": {
|
||||
"name": "Break function never expires"
|
||||
},
|
||||
"child_lock": {
|
||||
"name": "Child lock"
|
||||
},
|
||||
|
||||
@@ -5,9 +5,11 @@ from enum import Enum
|
||||
from typing import TYPE_CHECKING, Any, override
|
||||
|
||||
from boschshcpy import (
|
||||
BypassService,
|
||||
CameraLightService,
|
||||
PowerSwitchService,
|
||||
PrivacyModeService,
|
||||
SHCShutterContact2,
|
||||
SHCSmartPlug,
|
||||
ThermostatService,
|
||||
)
|
||||
@@ -98,6 +100,14 @@ SWITCH_TYPES: dict[str, SHCSwitchEntityDescription] = {
|
||||
on_value=True,
|
||||
should_poll=False,
|
||||
),
|
||||
"bypass": SHCSwitchEntityDescription(
|
||||
key="bypass",
|
||||
translation_key="bypass",
|
||||
device_class=SwitchDeviceClass.SWITCH,
|
||||
on_key="bypass",
|
||||
on_value=BypassService.State.BYPASS_ACTIVE,
|
||||
should_poll=False,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -226,6 +236,27 @@ async def async_setup_entry(
|
||||
)
|
||||
)
|
||||
|
||||
entities.extend(
|
||||
SHCSwitch(
|
||||
hass=hass,
|
||||
device=switch,
|
||||
parent_id=shc_info.unique_id,
|
||||
entry_id=config_entry.entry_id,
|
||||
description=SWITCH_TYPES["bypass"],
|
||||
)
|
||||
for switch in session.device_helper.shutter_contacts2
|
||||
)
|
||||
|
||||
entities.extend(
|
||||
SHCBypassInfiniteSwitch(
|
||||
hass=hass,
|
||||
device=switch,
|
||||
parent_id=shc_info.unique_id,
|
||||
entry_id=config_entry.entry_id,
|
||||
)
|
||||
for switch in session.device_helper.shutter_contacts2
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
@@ -308,3 +339,35 @@ class SHCRoutingSwitch(SHCEntity, SwitchEntity):
|
||||
def turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch off."""
|
||||
self._device.routing = False
|
||||
|
||||
|
||||
class SHCBypassInfiniteSwitch(SHCEntity, SwitchEntity):
|
||||
"""Representation of a SHC alarm-bypass "never expires" switch."""
|
||||
|
||||
_attr_translation_key = "bypass_infinite"
|
||||
_attr_device_class = SwitchDeviceClass.SWITCH
|
||||
_attr_entity_category = EntityCategory.CONFIG
|
||||
_device: SHCShutterContact2
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, device: SHCDevice, parent_id: str, entry_id: str
|
||||
) -> None:
|
||||
"""Initialize an SHC bypass-never-expires switch."""
|
||||
super().__init__(hass, device, parent_id, entry_id)
|
||||
self._attr_unique_id = f"{device.serial}_bypass_infinite"
|
||||
|
||||
@property
|
||||
@override
|
||||
def is_on(self) -> bool:
|
||||
"""Return the state of the switch."""
|
||||
return self._device.bypass_infinite
|
||||
|
||||
@override
|
||||
def turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch on."""
|
||||
self._device.set_bypass_configuration(infinite=True)
|
||||
|
||||
@override
|
||||
def turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch off."""
|
||||
self._device.set_bypass_configuration(infinite=False)
|
||||
|
||||
@@ -7,6 +7,7 @@ from unittest.mock import MagicMock, create_autospec, patch
|
||||
|
||||
from boschshcpy import (
|
||||
BatteryLevelService,
|
||||
BypassService,
|
||||
PowerSwitchService,
|
||||
RoutingService,
|
||||
SHCBatteryDevice,
|
||||
@@ -14,6 +15,7 @@ from boschshcpy import (
|
||||
SHCMicromoduleBlinds,
|
||||
SHCMicromoduleRelay,
|
||||
SHCPresenceSimulationSystem,
|
||||
SHCShutterContact2,
|
||||
SHCShutterControl,
|
||||
SHCSmartPlug,
|
||||
SHCThermostat,
|
||||
@@ -301,3 +303,25 @@ def presence_simulation_system_device(
|
||||
device.status = "AVAILABLE"
|
||||
device.enabled = enabled
|
||||
return device
|
||||
|
||||
|
||||
def shutter_contact2_device(
|
||||
device_id: str = "hdm:ZigBee:shuttercontact1",
|
||||
name: str = "Shutter contact",
|
||||
bypass: BypassService.State = BypassService.State.BYPASS_INACTIVE,
|
||||
bypass_infinite: bool = False,
|
||||
) -> SHCShutterContact2:
|
||||
"""Build a minimal device double for the shutter_contacts2 bucket."""
|
||||
device = create_autospec(SHCShutterContact2, 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 = "SWD2"
|
||||
device.device_services = []
|
||||
device.deleted = False
|
||||
device.status = "AVAILABLE"
|
||||
device.bypass = bypass
|
||||
device.bypass_infinite = bypass_infinite
|
||||
return device
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from boschshcpy import ThermostatService
|
||||
from boschshcpy import BypassService, ThermostatService
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||
@@ -21,6 +21,7 @@ from .conftest import (
|
||||
micromodule_relay_device,
|
||||
presence_simulation_system_device,
|
||||
setup_integration,
|
||||
shutter_contact2_device,
|
||||
smart_plug_device,
|
||||
thermostat_device,
|
||||
)
|
||||
@@ -215,3 +216,104 @@ async def test_smart_plug_routing_switch_name(
|
||||
state = hass.states.get("switch.smart_plug_range_extension")
|
||||
assert state is not None
|
||||
assert state.attributes["friendly_name"] == "Smart Plug Range extension"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"device_buckets",
|
||||
[
|
||||
{
|
||||
"shutter_contacts2": [
|
||||
shutter_contact2_device(bypass=BypassService.State.BYPASS_INACTIVE)
|
||||
]
|
||||
}
|
||||
],
|
||||
indirect=True,
|
||||
)
|
||||
@pytest.mark.usefixtures("mock_session")
|
||||
async def test_shutter_contact2_bypass(
|
||||
hass: HomeAssistant,
|
||||
mock_session: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""A Door/Window Contact II's alarm bypass is exposed and controllable."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
device = mock_session.device_helper.shutter_contacts2[0]
|
||||
|
||||
state = hass.states.get("switch.shutter_contact_break_function")
|
||||
assert state is not None
|
||||
assert state.state == "off"
|
||||
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: "switch.shutter_contact_break_function"},
|
||||
blocking=True,
|
||||
)
|
||||
assert device.bypass is True
|
||||
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: "switch.shutter_contact_break_function"},
|
||||
blocking=True,
|
||||
)
|
||||
assert device.bypass is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"device_buckets",
|
||||
[{"shutter_contacts2": [shutter_contact2_device(bypass_infinite=False)]}],
|
||||
indirect=True,
|
||||
)
|
||||
@pytest.mark.usefixtures("mock_session")
|
||||
async def test_shutter_contact2_bypass_infinite(
|
||||
hass: HomeAssistant,
|
||||
mock_session: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""A Door/Window Contact II's bypass-never-expires option is exposed and controllable."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
device = mock_session.device_helper.shutter_contacts2[0]
|
||||
|
||||
state = hass.states.get("switch.shutter_contact_break_function_never_expires")
|
||||
assert state is not None
|
||||
assert state.state == "off"
|
||||
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: "switch.shutter_contact_break_function_never_expires"},
|
||||
blocking=True,
|
||||
)
|
||||
device.set_bypass_configuration.assert_called_once_with(infinite=True)
|
||||
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: "switch.shutter_contact_break_function_never_expires"},
|
||||
blocking=True,
|
||||
)
|
||||
device.set_bypass_configuration.assert_called_with(infinite=False)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"device_buckets",
|
||||
[{"shutter_contacts2": [shutter_contact2_device()]}],
|
||||
indirect=True,
|
||||
)
|
||||
@pytest.mark.usefixtures("mock_session")
|
||||
async def test_shutter_contact2_bypass_unique_id(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""A Door/Window Contact II's two bypass switches use distinct unique_ids."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
bypass_entry = entity_registry.async_get("switch.shutter_contact_break_function")
|
||||
bypass_infinite_entry = entity_registry.async_get(
|
||||
"switch.shutter_contact_break_function_never_expires"
|
||||
)
|
||||
assert bypass_entry is not None
|
||||
assert bypass_infinite_entry is not None
|
||||
assert bypass_entry.unique_id != bypass_infinite_entry.unique_id
|
||||
|
||||
Reference in New Issue
Block a user