mirror of
https://github.com/home-assistant/core.git
synced 2025-07-27 15:17:35 +00:00
Add alarmed binary sensor to Risco integration (#77315)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
623abb4325
commit
64eb316908
@ -28,6 +28,7 @@ from homeassistant.const import (
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||||
from homeassistant.helpers.storage import Store
|
from homeassistant.helpers.storage import Store
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
@ -50,7 +51,6 @@ class LocalData:
|
|||||||
"""A data class for local data passed to the platforms."""
|
"""A data class for local data passed to the platforms."""
|
||||||
|
|
||||||
system: RiscoLocal
|
system: RiscoLocal
|
||||||
zone_updates: dict[int, Callable[[], Any]] = field(default_factory=dict)
|
|
||||||
partition_updates: dict[int, Callable[[], Any]] = field(default_factory=dict)
|
partition_updates: dict[int, Callable[[], Any]] = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
@ -59,6 +59,11 @@ def is_local(entry: ConfigEntry) -> bool:
|
|||||||
return entry.data.get(CONF_TYPE) == TYPE_LOCAL
|
return entry.data.get(CONF_TYPE) == TYPE_LOCAL
|
||||||
|
|
||||||
|
|
||||||
|
def zone_update_signal(zone_id: int) -> str:
|
||||||
|
"""Return a signal for the dispatch of a zone update."""
|
||||||
|
return f"risco_zone_update_{zone_id}"
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Set up Risco from a config entry."""
|
"""Set up Risco from a config entry."""
|
||||||
if is_local(entry):
|
if is_local(entry):
|
||||||
@ -95,9 +100,7 @@ async def _async_setup_local_entry(hass: HomeAssistant, entry: ConfigEntry) -> b
|
|||||||
|
|
||||||
async def _zone(zone_id: int, zone: Zone) -> None:
|
async def _zone(zone_id: int, zone: Zone) -> None:
|
||||||
_LOGGER.debug("Risco zone update for %d", zone_id)
|
_LOGGER.debug("Risco zone update for %d", zone_id)
|
||||||
callback = local_data.zone_updates.get(zone_id)
|
async_dispatcher_send(hass, zone_update_signal(zone_id))
|
||||||
if callback:
|
|
||||||
callback()
|
|
||||||
|
|
||||||
entry.async_on_unload(risco.add_zone_handler(_zone))
|
entry.async_on_unload(risco.add_zone_handler(_zone))
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""Support for Risco alarm zones."""
|
"""Support for Risco alarm zones."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Callable, Mapping
|
from collections.abc import Mapping
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from pyrisco.common import Zone
|
from pyrisco.common import Zone
|
||||||
@ -13,10 +13,11 @@ from homeassistant.components.binary_sensor import (
|
|||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import entity_platform
|
from homeassistant.helpers import entity_platform
|
||||||
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.entity import DeviceInfo
|
from homeassistant.helpers.entity import DeviceInfo
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from . import LocalData, RiscoDataUpdateCoordinator, is_local
|
from . import LocalData, RiscoDataUpdateCoordinator, is_local, zone_update_signal
|
||||||
from .const import DATA_COORDINATOR, DOMAIN
|
from .const import DATA_COORDINATOR, DOMAIN
|
||||||
from .entity import RiscoEntity, binary_sensor_unique_id
|
from .entity import RiscoEntity, binary_sensor_unique_id
|
||||||
|
|
||||||
@ -24,6 +25,10 @@ SERVICE_BYPASS_ZONE = "bypass_zone"
|
|||||||
SERVICE_UNBYPASS_ZONE = "unbypass_zone"
|
SERVICE_UNBYPASS_ZONE = "unbypass_zone"
|
||||||
|
|
||||||
|
|
||||||
|
def _unique_id_for_local(system_id: str, zone_id: int) -> str:
|
||||||
|
return f"{system_id}_zone_{zone_id}_local"
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: ConfigEntry,
|
||||||
@ -39,9 +44,11 @@ async def async_setup_entry(
|
|||||||
if is_local(config_entry):
|
if is_local(config_entry):
|
||||||
local_data: LocalData = hass.data[DOMAIN][config_entry.entry_id]
|
local_data: LocalData = hass.data[DOMAIN][config_entry.entry_id]
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
RiscoLocalBinarySensor(
|
RiscoLocalBinarySensor(local_data.system.id, zone_id, zone)
|
||||||
local_data.system.id, zone_id, zone, local_data.zone_updates
|
for zone_id, zone in local_data.system.zones.items()
|
||||||
)
|
)
|
||||||
|
async_add_entities(
|
||||||
|
RiscoLocalAlarmedBinarySensor(local_data.system.id, zone_id, zone)
|
||||||
for zone_id, zone in local_data.system.zones.items()
|
for zone_id, zone in local_data.system.zones.items()
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@ -118,18 +125,10 @@ class RiscoLocalBinarySensor(RiscoBinarySensor):
|
|||||||
|
|
||||||
_attr_should_poll = False
|
_attr_should_poll = False
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, system_id: str, zone_id: int, zone: Zone) -> None:
|
||||||
self,
|
|
||||||
system_id: str,
|
|
||||||
zone_id: int,
|
|
||||||
zone: Zone,
|
|
||||||
zone_updates: dict[int, Callable[[], Any]],
|
|
||||||
) -> None:
|
|
||||||
"""Init the zone."""
|
"""Init the zone."""
|
||||||
super().__init__(zone_id=zone_id, zone=zone)
|
super().__init__(zone_id=zone_id, zone=zone)
|
||||||
self._system_id = system_id
|
self._attr_unique_id = _unique_id_for_local(system_id, zone_id)
|
||||||
self._zone_updates = zone_updates
|
|
||||||
self._attr_unique_id = f"{system_id}_zone_{zone_id}_local"
|
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
identifiers={(DOMAIN, self._attr_unique_id)},
|
identifiers={(DOMAIN, self._attr_unique_id)},
|
||||||
manufacturer="Risco",
|
manufacturer="Risco",
|
||||||
@ -138,7 +137,10 @@ class RiscoLocalBinarySensor(RiscoBinarySensor):
|
|||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""Subscribe to updates."""
|
"""Subscribe to updates."""
|
||||||
self._zone_updates[self._zone_id] = self.async_write_ha_state
|
signal = zone_update_signal(self._zone_id)
|
||||||
|
self.async_on_remove(
|
||||||
|
async_dispatcher_connect(self.hass, signal, self.async_write_ha_state)
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def extra_state_attributes(self) -> Mapping[str, Any] | None:
|
def extra_state_attributes(self) -> Mapping[str, Any] | None:
|
||||||
@ -150,3 +152,41 @@ class RiscoLocalBinarySensor(RiscoBinarySensor):
|
|||||||
|
|
||||||
async def _bypass(self, bypass: bool) -> None:
|
async def _bypass(self, bypass: bool) -> None:
|
||||||
await self._zone.bypass(bypass)
|
await self._zone.bypass(bypass)
|
||||||
|
|
||||||
|
|
||||||
|
class RiscoLocalAlarmedBinarySensor(BinarySensorEntity):
|
||||||
|
"""Representation whether a zone in Risco local is currently triggering an alarm."""
|
||||||
|
|
||||||
|
_attr_should_poll = False
|
||||||
|
|
||||||
|
def __init__(self, system_id: str, zone_id: int, zone: Zone) -> None:
|
||||||
|
"""Init the zone."""
|
||||||
|
super().__init__()
|
||||||
|
self._zone_id = zone_id
|
||||||
|
self._zone = zone
|
||||||
|
self._attr_has_entity_name = True
|
||||||
|
self._attr_name = "Alarmed"
|
||||||
|
device_unique_id = _unique_id_for_local(system_id, zone_id)
|
||||||
|
self._attr_unique_id = device_unique_id + "_alarmed"
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
identifiers={(DOMAIN, device_unique_id)},
|
||||||
|
manufacturer="Risco",
|
||||||
|
name=self._zone.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_added_to_hass(self) -> None:
|
||||||
|
"""Subscribe to updates."""
|
||||||
|
signal = zone_update_signal(self._zone_id)
|
||||||
|
self.async_on_remove(
|
||||||
|
async_dispatcher_connect(self.hass, signal, self.async_write_ha_state)
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def extra_state_attributes(self) -> Mapping[str, Any] | None:
|
||||||
|
"""Return the state attributes."""
|
||||||
|
return {"zone_id": self._zone_id}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self) -> bool | None:
|
||||||
|
"""Return true if sensor is on."""
|
||||||
|
return self._zone.alarmed
|
||||||
|
@ -13,6 +13,8 @@ from .util import TEST_SITE_UUID, zone_mock
|
|||||||
|
|
||||||
FIRST_ENTITY_ID = "binary_sensor.zone_0"
|
FIRST_ENTITY_ID = "binary_sensor.zone_0"
|
||||||
SECOND_ENTITY_ID = "binary_sensor.zone_1"
|
SECOND_ENTITY_ID = "binary_sensor.zone_1"
|
||||||
|
FIRST_ALARMED_ENTITY_ID = FIRST_ENTITY_ID + "_alarmed"
|
||||||
|
SECOND_ALARMED_ENTITY_ID = SECOND_ENTITY_ID + "_alarmed"
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@ -23,10 +25,14 @@ def two_zone_local():
|
|||||||
zone_mocks[0], "id", new_callable=PropertyMock(return_value=0)
|
zone_mocks[0], "id", new_callable=PropertyMock(return_value=0)
|
||||||
), patch.object(
|
), patch.object(
|
||||||
zone_mocks[0], "name", new_callable=PropertyMock(return_value="Zone 0")
|
zone_mocks[0], "name", new_callable=PropertyMock(return_value="Zone 0")
|
||||||
|
), patch.object(
|
||||||
|
zone_mocks[0], "alarmed", new_callable=PropertyMock(return_value=False)
|
||||||
), patch.object(
|
), patch.object(
|
||||||
zone_mocks[1], "id", new_callable=PropertyMock(return_value=1)
|
zone_mocks[1], "id", new_callable=PropertyMock(return_value=1)
|
||||||
), patch.object(
|
), patch.object(
|
||||||
zone_mocks[1], "name", new_callable=PropertyMock(return_value="Zone 1")
|
zone_mocks[1], "name", new_callable=PropertyMock(return_value="Zone 1")
|
||||||
|
), patch.object(
|
||||||
|
zone_mocks[1], "alarmed", new_callable=PropertyMock(return_value=False)
|
||||||
), patch(
|
), patch(
|
||||||
"homeassistant.components.risco.RiscoLocal.partitions",
|
"homeassistant.components.risco.RiscoLocal.partitions",
|
||||||
new_callable=PropertyMock(return_value={}),
|
new_callable=PropertyMock(return_value={}),
|
||||||
@ -126,6 +132,8 @@ async def test_error_on_connect(hass, connect_with_error, local_config_entry):
|
|||||||
registry = er.async_get(hass)
|
registry = er.async_get(hass)
|
||||||
assert not registry.async_is_registered(FIRST_ENTITY_ID)
|
assert not registry.async_is_registered(FIRST_ENTITY_ID)
|
||||||
assert not registry.async_is_registered(SECOND_ENTITY_ID)
|
assert not registry.async_is_registered(SECOND_ENTITY_ID)
|
||||||
|
assert not registry.async_is_registered(FIRST_ALARMED_ENTITY_ID)
|
||||||
|
assert not registry.async_is_registered(SECOND_ALARMED_ENTITY_ID)
|
||||||
|
|
||||||
|
|
||||||
async def test_local_setup(hass, two_zone_local, setup_risco_local):
|
async def test_local_setup(hass, two_zone_local, setup_risco_local):
|
||||||
@ -133,6 +141,8 @@ async def test_local_setup(hass, two_zone_local, setup_risco_local):
|
|||||||
registry = er.async_get(hass)
|
registry = er.async_get(hass)
|
||||||
assert registry.async_is_registered(FIRST_ENTITY_ID)
|
assert registry.async_is_registered(FIRST_ENTITY_ID)
|
||||||
assert registry.async_is_registered(SECOND_ENTITY_ID)
|
assert registry.async_is_registered(SECOND_ENTITY_ID)
|
||||||
|
assert registry.async_is_registered(FIRST_ALARMED_ENTITY_ID)
|
||||||
|
assert registry.async_is_registered(SECOND_ALARMED_ENTITY_ID)
|
||||||
|
|
||||||
registry = dr.async_get(hass)
|
registry = dr.async_get(hass)
|
||||||
device = registry.async_get_device({(DOMAIN, TEST_SITE_UUID + "_zone_0_local")})
|
device = registry.async_get_device({(DOMAIN, TEST_SITE_UUID + "_zone_0_local")})
|
||||||
@ -157,6 +167,7 @@ async def _check_local_state(
|
|||||||
new_callable=PropertyMock(return_value=bypassed),
|
new_callable=PropertyMock(return_value=bypassed),
|
||||||
):
|
):
|
||||||
await callback(zone_id, zones[zone_id])
|
await callback(zone_id, zones[zone_id])
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
expected_triggered = STATE_ON if triggered else STATE_OFF
|
expected_triggered = STATE_ON if triggered else STATE_OFF
|
||||||
assert hass.states.get(entity_id).state == expected_triggered
|
assert hass.states.get(entity_id).state == expected_triggered
|
||||||
@ -164,6 +175,22 @@ async def _check_local_state(
|
|||||||
assert hass.states.get(entity_id).attributes["zone_id"] == zone_id
|
assert hass.states.get(entity_id).attributes["zone_id"] == zone_id
|
||||||
|
|
||||||
|
|
||||||
|
async def _check_alarmed_local_state(
|
||||||
|
hass, zones, alarmed, entity_id, zone_id, callback
|
||||||
|
):
|
||||||
|
with patch.object(
|
||||||
|
zones[zone_id],
|
||||||
|
"alarmed",
|
||||||
|
new_callable=PropertyMock(return_value=alarmed),
|
||||||
|
):
|
||||||
|
await callback(zone_id, zones[zone_id])
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
expected_alarmed = STATE_ON if alarmed else STATE_OFF
|
||||||
|
assert hass.states.get(entity_id).state == expected_alarmed
|
||||||
|
assert hass.states.get(entity_id).attributes["zone_id"] == zone_id
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def _mock_zone_handler():
|
def _mock_zone_handler():
|
||||||
with patch("homeassistant.components.risco.RiscoLocal.add_zone_handler") as mock:
|
with patch("homeassistant.components.risco.RiscoLocal.add_zone_handler") as mock:
|
||||||
@ -204,6 +231,28 @@ async def test_local_states(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_alarmed_local_states(
|
||||||
|
hass, two_zone_local, _mock_zone_handler, setup_risco_local
|
||||||
|
):
|
||||||
|
"""Test the various alarm states."""
|
||||||
|
callback = _mock_zone_handler.call_args.args[0]
|
||||||
|
|
||||||
|
assert callback is not None
|
||||||
|
|
||||||
|
await _check_alarmed_local_state(
|
||||||
|
hass, two_zone_local, True, FIRST_ALARMED_ENTITY_ID, 0, callback
|
||||||
|
)
|
||||||
|
await _check_alarmed_local_state(
|
||||||
|
hass, two_zone_local, False, FIRST_ALARMED_ENTITY_ID, 0, callback
|
||||||
|
)
|
||||||
|
await _check_alarmed_local_state(
|
||||||
|
hass, two_zone_local, True, SECOND_ALARMED_ENTITY_ID, 1, callback
|
||||||
|
)
|
||||||
|
await _check_alarmed_local_state(
|
||||||
|
hass, two_zone_local, False, SECOND_ALARMED_ENTITY_ID, 1, callback
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_local_bypass(hass, two_zone_local, setup_risco_local):
|
async def test_local_bypass(hass, two_zone_local, setup_risco_local):
|
||||||
"""Test bypassing a zone."""
|
"""Test bypassing a zone."""
|
||||||
with patch.object(two_zone_local[0], "bypass") as mock:
|
with patch.object(two_zone_local[0], "bypass") as mock:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user