From c08496e105faeb60e4d5b0f407d2f77371744fcb Mon Sep 17 00:00:00 2001 From: Thomas <10558666+mosandlt@users.noreply.github.com> Date: Thu, 24 Sep 2026 21:23:12 +0200 Subject: [PATCH] bosch_shc: add Smart Plug energy saving mode switch entity (#182873) Co-authored-by: Claude Sonnet 5 --- .../components/bosch_shc/strings.json | 3 + homeassistant/components/bosch_shc/switch.py | 35 ++++++ tests/components/bosch_shc/conftest.py | 28 +++++ tests/components/bosch_shc/test_switch.py | 105 ++++++++++++++++++ 4 files changed, 171 insertions(+) diff --git a/homeassistant/components/bosch_shc/strings.json b/homeassistant/components/bosch_shc/strings.json index 5ed3e4801022..414c0f92ce4f 100644 --- a/homeassistant/components/bosch_shc/strings.json +++ b/homeassistant/components/bosch_shc/strings.json @@ -101,6 +101,9 @@ "child_lock": { "name": "Child lock" }, + "energy_saving_mode_enabled": { + "name": "Energy-saving mode" + }, "humidity_warning_enabled": { "name": "Humidity warning" }, diff --git a/homeassistant/components/bosch_shc/switch.py b/homeassistant/components/bosch_shc/switch.py index 4e5dd88e3d9f..f72b4075b29b 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, ), + "energy_saving_mode_enabled": SHCSwitchEntityDescription( + key="energy_saving_mode_enabled", + translation_key="energy_saving_mode_enabled", + device_class=SwitchDeviceClass.SWITCH, + entity_category=EntityCategory.CONFIG, + on_key="energy_saving_mode_enabled", + on_value=True, + should_poll=False, + ), "humidity_warning_enabled": SHCSwitchEntityDescription( key="humidity_warning_enabled", translation_key="humidity_warning_enabled", @@ -308,6 +317,32 @@ async def async_setup_entry( if switch.supports_nightly_promise ) + entities.extend( + SHCSwitch( + hass=hass, + device=switch, + parent_id=shc_info.unique_id, + entry_id=config_entry.entry_id, + description=SWITCH_TYPES["energy_saving_mode_enabled"], + unique_id_suffix="energy_saving_mode", + ) + for switch in session.device_helper.smart_plugs + if switch.supports_energy_saving_mode + ) + + entities.extend( + SHCSwitch( + hass=hass, + device=switch, + parent_id=shc_info.unique_id, + entry_id=config_entry.entry_id, + description=SWITCH_TYPES["energy_saving_mode_enabled"], + unique_id_suffix="energy_saving_mode", + ) + for switch in session.device_helper.smart_plugs_compact + if switch.supports_energy_saving_mode + ) + entities.extend( SHCSwitch( hass=hass, diff --git a/tests/components/bosch_shc/conftest.py b/tests/components/bosch_shc/conftest.py index 966129c6834c..30e2fd836124 100644 --- a/tests/components/bosch_shc/conftest.py +++ b/tests/components/bosch_shc/conftest.py @@ -19,6 +19,7 @@ from boschshcpy import ( SHCShutterContact2, SHCShutterControl, SHCSmartPlug, + SHCSmartPlugCompact, SHCThermostat, SHCThermostatGen2, SHCTwinguard, @@ -206,6 +207,8 @@ def smart_plug_device( device_id: str = "hdm:ZigBee:plug1", name: str = "Smart Plug", routing: RoutingService.State = RoutingService.State.DISABLED, + supports_energy_saving_mode: bool = False, + energy_saving_mode_enabled: bool = False, ) -> SHCSmartPlug: """Build a minimal device double for the smart_plugs bucket.""" device = create_autospec(SHCSmartPlug, instance=True, spec_set=True) @@ -220,6 +223,31 @@ def smart_plug_device( device.status = "AVAILABLE" device.switchstate = PowerSwitchService.State.OFF device.routing = routing + device.supports_energy_saving_mode = supports_energy_saving_mode + device.energy_saving_mode_enabled = energy_saving_mode_enabled + return device + + +def smart_plug_compact_device( + device_id: str = "hdm:ZigBee:plugcompact1", + name: str = "Smart Plug Compact", + supports_energy_saving_mode: bool = False, + energy_saving_mode_enabled: bool = False, +) -> SHCSmartPlugCompact: + """Build a minimal device double for the smart_plugs_compact bucket.""" + device = create_autospec(SHCSmartPlugCompact, 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 = "PLUG_COMPACT" + device.device_services = [] + device.deleted = False + device.status = "AVAILABLE" + device.switchstate = PowerSwitchService.State.OFF + device.supports_energy_saving_mode = supports_energy_saving_mode + device.energy_saving_mode_enabled = energy_saving_mode_enabled return device diff --git a/tests/components/bosch_shc/test_switch.py b/tests/components/bosch_shc/test_switch.py index 1e12887872f8..cd335daeb726 100644 --- a/tests/components/bosch_shc/test_switch.py +++ b/tests/components/bosch_shc/test_switch.py @@ -23,6 +23,7 @@ from .conftest import ( presence_simulation_system_device, setup_integration, shutter_contact2_device, + smart_plug_compact_device, smart_plug_device, thermostat_device, thermostat_gen2_device, @@ -516,3 +517,107 @@ async def test_roomthermostat_gen2_humidity_warning( blocking=True, ) assert device.humidity_warning_enabled is True + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.room_thermostat_2_humidity_warning"}, + blocking=True, + ) + assert device.humidity_warning_enabled is False + + +@pytest.mark.parametrize( + "device_buckets", + [ + { + "smart_plugs": [ + smart_plug_device( + supports_energy_saving_mode=True, energy_saving_mode_enabled=False + ) + ] + } + ], + indirect=True, +) +@pytest.mark.usefixtures("mock_session") +async def test_smart_plug_energy_saving_mode( + hass: HomeAssistant, + mock_session: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """A Smart Plug's energy saving mode is exposed and controllable as a switch.""" + await setup_integration(hass, mock_config_entry) + device = mock_session.device_helper.smart_plugs[0] + + state = hass.states.get("switch.smart_plug_energy_saving_mode") + assert state is not None + assert state.state == "off" + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.smart_plug_energy_saving_mode"}, + blocking=True, + ) + assert device.energy_saving_mode_enabled is True + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.smart_plug_energy_saving_mode"}, + blocking=True, + ) + assert device.energy_saving_mode_enabled is False + + +@pytest.mark.parametrize( + "device_buckets", + [{"smart_plugs": [smart_plug_device(supports_energy_saving_mode=False)]}], + indirect=True, +) +@pytest.mark.usefixtures("mock_session") +async def test_smart_plug_no_energy_saving_mode_support( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """No switch is created for a Smart Plug without energy-saving-mode support.""" + await setup_integration(hass, mock_config_entry) + + assert hass.states.get("switch.smart_plug_energy_saving_mode") is None + + +@pytest.mark.parametrize( + "device_buckets", + [ + { + "smart_plugs_compact": [ + smart_plug_compact_device( + supports_energy_saving_mode=True, energy_saving_mode_enabled=False + ) + ] + } + ], + indirect=True, +) +@pytest.mark.usefixtures("mock_session") +async def test_smart_plug_compact_energy_saving_mode( + hass: HomeAssistant, + mock_session: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """A Smart Plug Compact's energy saving mode is exposed and controllable as a switch.""" + await setup_integration(hass, mock_config_entry) + device = mock_session.device_helper.smart_plugs_compact[0] + + state = hass.states.get("switch.smart_plug_compact_energy_saving_mode") + assert state is not None + assert state.state == "off" + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.smart_plug_compact_energy_saving_mode"}, + blocking=True, + ) + assert device.energy_saving_mode_enabled is True