bosch_shc: add presence simulation switch entity (#182335)

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Thomas
2026-09-20 17:45:46 +02:00
committed by GitHub
co-authored by Claude Sonnet 5
parent bb56e6c5c8
commit 7e197f4516
3 changed files with 130 additions and 4 deletions
@@ -91,6 +91,13 @@ SWITCH_TYPES: dict[str, SHCSwitchEntityDescription] = {
on_value=ThermostatService.State.ON,
should_poll=False,
),
"presencesimulation": SHCSwitchEntityDescription(
key="presencesimulation",
device_class=SwitchDeviceClass.SWITCH,
on_key="enabled",
on_value=True,
should_poll=False,
),
}
@@ -207,6 +214,18 @@ async def async_setup_entry(
)
)
presence_simulation_system = session.device_helper.presence_simulation_system
if presence_simulation_system is not None:
entities.append(
SHCSwitch(
hass=hass,
device=presence_simulation_system,
parent_id=shc_info.unique_id,
entry_id=config_entry.entry_id,
description=SWITCH_TYPES["presencesimulation"],
)
)
async_add_entities(entities)
+34 -4
View File
@@ -12,11 +12,13 @@ from boschshcpy import (
SHCLightSwitchBSM,
SHCMicromoduleBlinds,
SHCMicromoduleRelay,
SHCPresenceSimulationSystem,
SHCShutterControl,
SHCThermostat,
ShutterControlService,
ThermostatService,
)
from boschshcpy.services_impl import PresenceSimulationConfigurationService
import pytest
from homeassistant.components.bosch_shc.const import (
@@ -51,7 +53,7 @@ def mock_config_entry() -> MockConfigEntry:
# Keep in sync with binary_sensor.py's device_helper buckets — a bucket
# missing here breaks the mock_session fixture.
_EMPTY_DEVICE_BUCKETS: dict[str, list[Any]] = {
_EMPTY_DEVICE_BUCKETS: dict[str, Any] = {
bucket: []
for bucket in (
"camera_360",
@@ -77,22 +79,26 @@ _EMPTY_DEVICE_BUCKETS: dict[str, list[Any]] = {
"wallthermostats",
"water_leakage_detectors",
)
} | {
# Not a list bucket — presence_simulation_system is a single optional
# device on device_helper, not a device_helper.<x> list of devices.
"presence_simulation_system": None,
}
@pytest.fixture
def device_buckets(request: pytest.FixtureRequest) -> dict[str, list[Any]]:
def device_buckets(request: pytest.FixtureRequest) -> dict[str, Any]:
"""device_helper buckets for the mock session.
Empty by default; a test overrides specific buckets via
``@pytest.mark.parametrize("device_buckets", [{...}], indirect=True)``.
"""
overrides: dict[str, list[Any]] = getattr(request, "param", {})
overrides: dict[str, Any] = getattr(request, "param", {})
return {**_EMPTY_DEVICE_BUCKETS, **overrides}
@pytest.fixture
def mock_session(device_buckets: dict[str, list[Any]]) -> Generator[MagicMock]:
def mock_session(device_buckets: dict[str, Any]) -> Generator[MagicMock]:
"""Mock SHCSession, patched in for the duration of the test."""
session = MagicMock()
session.information.unique_id = "test-mac"
@@ -248,3 +254,27 @@ def light_switch_bsm_device(
device.switchstate = PowerSwitchService.State.OFF
device.child_lock = child_lock
return device
def presence_simulation_system_device(
device_id: str = "presenceSimulationService",
name: str = "Presence Simulation",
enabled: bool = False,
) -> SHCPresenceSimulationSystem:
"""Build a minimal device double for the presence_simulation_system slot."""
device = create_autospec(SHCPresenceSimulationSystem, 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 = "PRESENCE_SIMULATION_SERVICE"
device.device_services = [
create_autospec(
PresenceSimulationConfigurationService, instance=True, spec_set=True
)
]
device.deleted = False
device.status = "AVAILABLE"
device.enabled = enabled
return device
+77
View File
@@ -19,6 +19,7 @@ from homeassistant.helpers import entity_registry as er
from .conftest import (
light_switch_bsm_device,
micromodule_relay_device,
presence_simulation_system_device,
setup_integration,
thermostat_device,
)
@@ -116,3 +117,79 @@ async def test_light_switch_bsm_child_lock_unique_id(
assert lightswitch_entry is not None
assert child_lock_entry is not None
assert lightswitch_entry.unique_id != child_lock_entry.unique_id
@pytest.mark.parametrize(
"device_buckets",
[{"presence_simulation_system": presence_simulation_system_device(enabled=False)}],
indirect=True,
)
@pytest.mark.usefixtures("mock_session")
async def test_presence_simulation_system(
hass: HomeAssistant,
mock_session: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""The presence simulation system is exposed and controllable as a switch."""
await setup_integration(hass, mock_config_entry)
device = mock_session.device_helper.presence_simulation_system
state = hass.states.get("switch.presence_simulation")
assert state is not None
assert state.state == "off"
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: "switch.presence_simulation"},
blocking=True,
)
assert device.enabled is True
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: "switch.presence_simulation"},
blocking=True,
)
assert device.enabled is False
@pytest.mark.parametrize(
"device_buckets",
[{"presence_simulation_system": presence_simulation_system_device(enabled=False)}],
indirect=True,
)
@pytest.mark.usefixtures("mock_session")
async def test_presence_simulation_system_push_update(
hass: HomeAssistant,
mock_session: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""A controller-side push update reaches the switch without polling."""
await setup_integration(hass, mock_config_entry)
device = mock_session.device_helper.presence_simulation_system
service = device.device_services[0]
on_state_changed = service.subscribe_callback.call_args.args[1]
device.enabled = True
on_state_changed()
await hass.async_block_till_done()
assert hass.states.get("switch.presence_simulation").state == "on"
@pytest.mark.parametrize(
"device_buckets",
[{"presence_simulation_system": None}],
indirect=True,
)
@pytest.mark.usefixtures("mock_session")
async def test_no_presence_simulation_system(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
) -> None:
"""No switch is created when the controller has no presence simulation system."""
await setup_integration(hass, mock_config_entry)
assert hass.states.get("switch.presence_simulation") is None