diff --git a/homeassistant/components/bosch_shc/strings.json b/homeassistant/components/bosch_shc/strings.json index f5e4cc400c81..b9c92503ab5d 100644 --- a/homeassistant/components/bosch_shc/strings.json +++ b/homeassistant/components/bosch_shc/strings.json @@ -101,6 +101,9 @@ "child_lock": { "name": "Child lock" }, + "nightly_promise_enabled": { + "name": "Heartbeat" + }, "pet_immunity_enabled": { "name": "Pet immunity" }, diff --git a/homeassistant/components/bosch_shc/switch.py b/homeassistant/components/bosch_shc/switch.py index 6ffd56be5220..313db8b32806 100644 --- a/homeassistant/components/bosch_shc/switch.py +++ b/homeassistant/components/bosch_shc/switch.py @@ -108,6 +108,15 @@ SWITCH_TYPES: dict[str, SHCSwitchEntityDescription] = { on_value=BypassService.State.BYPASS_ACTIVE, should_poll=False, ), + "nightly_promise_enabled": SHCSwitchEntityDescription( + key="nightly_promise_enabled", + translation_key="nightly_promise_enabled", + device_class=SwitchDeviceClass.SWITCH, + entity_category=EntityCategory.CONFIG, + on_key="nightly_promise_enabled", + on_value=True, + should_poll=False, + ), "pet_immunity_enabled": SHCSwitchEntityDescription( key="pet_immunity_enabled", translation_key="pet_immunity_enabled", @@ -278,6 +287,18 @@ async def async_setup_entry( for switch in session.device_helper.motion_detectors2 ) + entities.extend( + SHCSwitch( + hass=hass, + device=switch, + parent_id=shc_info.unique_id, + entry_id=config_entry.entry_id, + description=SWITCH_TYPES["nightly_promise_enabled"], + ) + for switch in session.device_helper.twinguards + if switch.supports_nightly_promise + ) + async_add_entities(entities) diff --git a/tests/components/bosch_shc/conftest.py b/tests/components/bosch_shc/conftest.py index 1fc833076608..0c7e2017e68d 100644 --- a/tests/components/bosch_shc/conftest.py +++ b/tests/components/bosch_shc/conftest.py @@ -20,6 +20,7 @@ from boschshcpy import ( SHCShutterControl, SHCSmartPlug, SHCThermostat, + SHCTwinguard, ShutterControlService, ThermostatService, ) @@ -354,3 +355,25 @@ def motion_detector2_device( device.status = "AVAILABLE" device.pet_immunity_enabled = pet_immunity_enabled return device + + +def twinguard_device( + device_id: str = "hdm:HomeMaticIP:twinguard1", + name: str = "Twinguard", + supports_nightly_promise: bool = False, + nightly_promise_enabled: bool = False, +) -> SHCTwinguard: + """Build a minimal device double for the twinguards bucket.""" + device = create_autospec(SHCTwinguard, 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 = "TWINGUARD" + device.device_services = [] + device.deleted = False + device.status = "AVAILABLE" + device.supports_nightly_promise = supports_nightly_promise + device.nightly_promise_enabled = nightly_promise_enabled + return device diff --git a/tests/components/bosch_shc/test_switch.py b/tests/components/bosch_shc/test_switch.py index d21f63c378e4..9eba20234345 100644 --- a/tests/components/bosch_shc/test_switch.py +++ b/tests/components/bosch_shc/test_switch.py @@ -25,6 +25,7 @@ from .conftest import ( shutter_contact2_device, smart_plug_device, thermostat_device, + twinguard_device, ) from tests.common import MockConfigEntry @@ -354,3 +355,63 @@ async def test_motion_detector2_pet_immunity( blocking=True, ) assert device.pet_immunity_enabled is False + + +@pytest.mark.parametrize( + "device_buckets", + [ + { + "twinguards": [ + twinguard_device( + supports_nightly_promise=True, nightly_promise_enabled=False + ) + ] + } + ], + indirect=True, +) +@pytest.mark.usefixtures("mock_session") +async def test_twinguard_nightly_promise( + hass: HomeAssistant, + mock_session: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """A Twinguard's nightly promise (Heartbeat) is exposed and controllable as a switch.""" + await setup_integration(hass, mock_config_entry) + device = mock_session.device_helper.twinguards[0] + + state = hass.states.get("switch.twinguard_heartbeat") + assert state is not None + assert state.state == "off" + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.twinguard_heartbeat"}, + blocking=True, + ) + assert device.nightly_promise_enabled is True + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.twinguard_heartbeat"}, + blocking=True, + ) + assert device.nightly_promise_enabled is False + + +@pytest.mark.parametrize( + "device_buckets", + [{"twinguards": [twinguard_device(supports_nightly_promise=False)]}], + indirect=True, +) +@pytest.mark.usefixtures("mock_session") +async def test_twinguard_no_nightly_promise_support( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """No switch is created for a Twinguard without nightly-promise support.""" + await setup_integration(hass, mock_config_entry) + + assert hass.states.get("switch.twinguard_heartbeat") is None