mirror of
https://github.com/home-assistant/core.git
synced 2026-09-26 01:11:51 -04:00
bosch_shc: add whole-home open doors/windows summary sensor (#182869)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
c945eab58a
commit
1ca85dc3bc
@@ -7,6 +7,7 @@ from typing import TYPE_CHECKING, Any, override
|
||||
from boschshcpy import (
|
||||
SHCLightSwitchBSM,
|
||||
SHCMicromoduleShutterControl,
|
||||
SHCSession,
|
||||
SHCSmartPlug,
|
||||
SHCSmartPlugCompact,
|
||||
SHCThermostat,
|
||||
@@ -28,10 +29,12 @@ from homeassistant.const import (
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from . import BoschConfigEntry
|
||||
from .const import DOMAIN
|
||||
from .entity import SHCEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
@@ -297,6 +300,57 @@ async def async_setup_entry(
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
async_add_entities(
|
||||
[SHCOpenWindowsSensor(session=session, parent_id=shc_info.unique_id)],
|
||||
update_before_add=True,
|
||||
)
|
||||
|
||||
|
||||
class SHCOpenWindowsSensor(SensorEntity):
|
||||
"""Whole-home summary of open doors/windows (official OpenAPI spec).
|
||||
|
||||
Not tied to one SHC device, so this does not inherit SHCEntity — it's
|
||||
scoped to the config entry and linked to the hub device directly. The
|
||||
underlying doors-windows/openwindows endpoint is a plain GET, not
|
||||
delivered by the long-poll stream, so this needs should_poll=True.
|
||||
"""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_translation_key = "open_windows_doors"
|
||||
_attr_should_poll = True
|
||||
|
||||
def __init__(self, session: SHCSession, parent_id: str) -> None:
|
||||
"""Initialize the open-windows/doors summary sensor."""
|
||||
self._session = session
|
||||
self._attr_unique_id = f"{parent_id}_open_windows_doors"
|
||||
self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, parent_id)})
|
||||
self._open_doors: list[dict[str, Any]] = []
|
||||
self._open_windows: list[dict[str, Any]] = []
|
||||
self._open_others: list[dict[str, Any]] = []
|
||||
|
||||
@property
|
||||
@override
|
||||
def native_value(self) -> int:
|
||||
"""Return the total count of open doors, windows, and other openings."""
|
||||
return len(self._open_doors) + len(self._open_windows) + len(self._open_others)
|
||||
|
||||
@property
|
||||
@override
|
||||
def extra_state_attributes(self) -> dict[str, list[str]]:
|
||||
"""Return the names of each currently-open door/window/other opening."""
|
||||
return {
|
||||
"open_doors": [d.get("name", "") for d in self._open_doors],
|
||||
"open_windows": [w.get("name", "") for w in self._open_windows],
|
||||
"open_others": [o.get("name", "") for o in self._open_others],
|
||||
}
|
||||
|
||||
def update(self) -> None:
|
||||
"""Poll the whole-home open-doors/open-windows summary."""
|
||||
data = self._session.api.get_open_windows()
|
||||
self._open_doors = data.get("openDoors", [])
|
||||
self._open_windows = data.get("openWindows", [])
|
||||
self._open_others = data.get("openOthers", [])
|
||||
|
||||
|
||||
class SHCSensor[_DeviceT: SHCDevice](SHCEntity, SensorEntity):
|
||||
"""Representation of a SHC sensor."""
|
||||
|
||||
@@ -53,6 +53,9 @@
|
||||
"humidity_rating": {
|
||||
"name": "Humidity rating"
|
||||
},
|
||||
"open_windows_doors": {
|
||||
"name": "Open doors and windows"
|
||||
},
|
||||
"purity": {
|
||||
"name": "Purity"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
"""Tests for the Bosch SHC sensor platform."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.entity_component import async_update_entity
|
||||
|
||||
from .conftest import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def platforms() -> Generator[None]:
|
||||
"""Restrict bosch_shc setup to the sensor platform."""
|
||||
with patch("homeassistant.components.bosch_shc.PLATFORMS", [Platform.SENSOR]):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_session")
|
||||
async def test_open_windows_doors_sensor(
|
||||
hass: HomeAssistant,
|
||||
mock_session: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
) -> None:
|
||||
"""The whole-home open-doors/open-windows summary is exposed and polled."""
|
||||
mock_session.api.get_open_windows.return_value = {
|
||||
"openDoors": [{"name": "Front Door"}],
|
||||
"openWindows": [{"name": "Kitchen Window"}, {"name": "Bedroom Window"}],
|
||||
"openOthers": [{"name": "Cat Flap"}],
|
||||
}
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
entity_id = "sensor.mock_title_open_doors_and_windows"
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.state == "4"
|
||||
assert state.attributes["open_doors"] == ["Front Door"]
|
||||
assert state.attributes["open_windows"] == ["Kitchen Window", "Bedroom Window"]
|
||||
assert state.attributes["open_others"] == ["Cat Flap"]
|
||||
|
||||
hub_device = device_registry.async_get_device_by_identifier(
|
||||
("bosch_shc", "test-mac"), mock_config_entry.entry_id
|
||||
)
|
||||
assert hub_device is not None
|
||||
entry = entity_registry.async_get(entity_id)
|
||||
assert entry is not None
|
||||
assert entry.device_id == hub_device.id
|
||||
|
||||
mock_session.api.get_open_windows.return_value = {
|
||||
"openDoors": [],
|
||||
"openWindows": [],
|
||||
"openOthers": [],
|
||||
}
|
||||
await async_update_entity(hass, entity_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.state == "0"
|
||||
Reference in New Issue
Block a user