diff --git a/homeassistant/components/bosch_shc/switch.py b/homeassistant/components/bosch_shc/switch.py index 13c8b71a38da..f1949dfd1480 100644 --- a/homeassistant/components/bosch_shc/switch.py +++ b/homeassistant/components/bosch_shc/switch.py @@ -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) diff --git a/tests/components/bosch_shc/conftest.py b/tests/components/bosch_shc/conftest.py index ab3d7aa2b991..c8cf981a38dd 100644 --- a/tests/components/bosch_shc/conftest.py +++ b/tests/components/bosch_shc/conftest.py @@ -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. 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 diff --git a/tests/components/bosch_shc/test_switch.py b/tests/components/bosch_shc/test_switch.py index 903efc30da97..4955107cc7b4 100644 --- a/tests/components/bosch_shc/test_switch.py +++ b/tests/components/bosch_shc/test_switch.py @@ -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