bosch_shc: add Smart Plug energy-saving power threshold number (#183699)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Thomas
2026-09-30 10:47:39 +01:00
committed by GitHub
co-authored by Claude Sonnet 5.5
parent b3ae7d658f
commit d41802ec50
4 changed files with 150 additions and 2 deletions
+49 -2
View File
@@ -4,7 +4,12 @@ from collections.abc import Callable, Coroutine
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, override
from boschshcpy import SHCMicromoduleRelay, SHCShutterContact2
from boschshcpy import (
SHCMicromoduleRelay,
SHCShutterContact2,
SHCSmartPlug,
SHCSmartPlugCompact,
)
from boschshcpy.device import SHCDevice
from homeassistant.components.number import (
@@ -13,7 +18,7 @@ from homeassistant.components.number import (
NumberEntityDescription,
NumberMode,
)
from homeassistant.const import EntityCategory, UnitOfTime
from homeassistant.const import EntityCategory, UnitOfPower, UnitOfTime
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -50,6 +55,12 @@ async def _impulse_length_set_value_fn(
await device.async_set_impulse_length(round(value * 10))
async def _power_threshold_set_value_fn(
device: SHCSmartPlug | SHCSmartPlugCompact, value: float
) -> None:
await device.async_set_power_threshold(value)
IMPULSE_LENGTH = "impulse_length"
NUMBER_TYPES: dict[str, SHCNumberEntityDescription] = {
@@ -81,6 +92,19 @@ NUMBER_TYPES: dict[str, SHCNumberEntityDescription] = {
round(value)
),
),
"power_threshold": SHCNumberEntityDescription[SHCSmartPlug | SHCSmartPlugCompact](
key="power_threshold",
translation_key="energy_saving_power_threshold",
entity_category=EntityCategory.CONFIG,
device_class=NumberDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.WATT,
native_min_value=0.0,
native_max_value=3680.0,
native_step=1.0,
mode=NumberMode.BOX,
value_fn=lambda device: device.power_threshold,
set_value_fn=_power_threshold_set_value_fn,
),
}
@@ -127,6 +151,29 @@ async def async_setup_entry(
for device in session.device_helper.shutter_contacts2
)
entities.extend(
SHCNumber(
hass=hass,
device=device,
parent_id=shc_info.unique_id,
entry_id=config_entry.entry_id,
description=NUMBER_TYPES["power_threshold"],
)
for device in session.device_helper.smart_plugs
if device.supports_energy_saving_mode and device.power_threshold is not None
)
entities.extend(
SHCNumber(
hass=hass,
device=device,
parent_id=shc_info.unique_id,
entry_id=config_entry.entry_id,
description=NUMBER_TYPES["power_threshold"],
)
for device in session.device_helper.smart_plugs_compact
if device.supports_energy_saving_mode and device.power_threshold is not None
)
async_add_entities(entities)
@@ -63,6 +63,9 @@
"bypass_timeout": {
"name": "Break function timeout"
},
"energy_saving_power_threshold": {
"name": "Energy-saving power threshold"
},
"impulse_length": {
"name": "Pulse length"
}
+4
View File
@@ -261,6 +261,7 @@ def smart_plug_device(
routing: RoutingService.State = RoutingService.State.DISABLED,
supports_energy_saving_mode: bool = False,
energy_saving_mode_enabled: bool = False,
power_threshold: float | None = None,
) -> SHCSmartPlug:
"""Build a minimal device double for the smart_plugs bucket."""
device = create_autospec(SHCSmartPlug, instance=True, spec_set=True)
@@ -277,6 +278,7 @@ def smart_plug_device(
device.routing = routing
device.supports_energy_saving_mode = supports_energy_saving_mode
device.energy_saving_mode_enabled = energy_saving_mode_enabled
device.power_threshold = power_threshold
return device
@@ -285,6 +287,7 @@ def smart_plug_compact_device(
name: str = "Smart Plug Compact",
supports_energy_saving_mode: bool = False,
energy_saving_mode_enabled: bool = False,
power_threshold: float | None = None,
) -> SHCSmartPlugCompact:
"""Build a minimal device double for the smart_plugs_compact bucket."""
device = create_autospec(SHCSmartPlugCompact, instance=True, spec_set=True)
@@ -300,6 +303,7 @@ def smart_plug_compact_device(
device.switchstate = PowerSwitchService.State.OFF
device.supports_energy_saving_mode = supports_energy_saving_mode
device.energy_saving_mode_enabled = energy_saving_mode_enabled
device.power_threshold = power_threshold
return device
+94
View File
@@ -16,12 +16,18 @@ from .conftest import (
micromodule_relay_device,
setup_integration,
shutter_contact2_device,
smart_plug_compact_device,
smart_plug_device,
)
from tests.common import MockConfigEntry
IMPULSE_LENGTH_ENTITY_ID = "number.relay_pulse_length"
BYPASS_TIMEOUT_ENTITY_ID = "number.shutter_contact_break_function_timeout"
POWER_THRESHOLD_ENTITY_ID = "number.smart_plug_energy_saving_power_threshold"
POWER_THRESHOLD_COMPACT_ENTITY_ID = (
"number.smart_plug_compact_energy_saving_power_threshold"
)
@pytest.mark.parametrize(
@@ -146,3 +152,91 @@ async def test_shutter_contact2_bypass_timeout_set_value(
blocking=True,
)
device.async_set_bypass_timeout.assert_awaited_once_with(11)
@pytest.mark.parametrize(
"device_buckets",
[
{
"smart_plugs": [
smart_plug_device(supports_energy_saving_mode=True, power_threshold=5.0)
],
"smart_plugs_compact": [
smart_plug_compact_device(
supports_energy_saving_mode=True, power_threshold=7.0
)
],
}
],
indirect=True,
)
@pytest.mark.usefixtures("mock_session")
async def test_smart_plug_power_threshold_value(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
) -> None:
"""The power threshold is reported for both smart plug variants."""
await setup_integration(hass, mock_config_entry)
state = hass.states.get(POWER_THRESHOLD_ENTITY_ID)
assert state is not None
assert state.state == "5.0"
state = hass.states.get(POWER_THRESHOLD_COMPACT_ENTITY_ID)
assert state is not None
assert state.state == "7.0"
@pytest.mark.parametrize(
"device_buckets",
[
{
"smart_plugs": [
smart_plug_device(supports_energy_saving_mode=True, power_threshold=5.0)
]
}
],
indirect=True,
)
@pytest.mark.usefixtures("mock_session")
async def test_smart_plug_power_threshold_set_value(
hass: HomeAssistant,
mock_session: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Setting a value writes the power threshold to the device."""
await setup_integration(hass, mock_config_entry)
device = mock_session.device_helper.smart_plugs[0]
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{ATTR_ENTITY_ID: POWER_THRESHOLD_ENTITY_ID, ATTR_VALUE: 20},
blocking=True,
)
device.async_set_power_threshold.assert_awaited_once_with(20)
@pytest.mark.parametrize(
"device_buckets",
[
{
"smart_plugs": [smart_plug_device(power_threshold=5.0)],
"smart_plugs_compact": [
smart_plug_compact_device(
supports_energy_saving_mode=True, power_threshold=None
)
],
}
],
indirect=True,
)
@pytest.mark.usefixtures("mock_session")
async def test_smart_plug_no_power_threshold_support(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
) -> None:
"""No entity is created without energy-saving or a power threshold."""
await setup_integration(hass, mock_config_entry)
assert hass.states.get(POWER_THRESHOLD_ENTITY_ID) is None
assert hass.states.get(POWER_THRESHOLD_COMPACT_ENTITY_ID) is None