Skip to content

Commit e8277cb

Browse files
Add alarm control panel platform to NASweb integration (#141582)
Co-authored-by: Erik Montnemery <[email protected]>
1 parent da0fb37 commit e8277cb

File tree

4 files changed

+167
-1
lines changed

4 files changed

+167
-1
lines changed

homeassistant/components/nasweb/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@
1919
from .coordinator import NASwebCoordinator
2020
from .nasweb_data import NASwebData
2121

22-
PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.SWITCH]
22+
PLATFORMS: list[Platform] = [
23+
Platform.ALARM_CONTROL_PANEL,
24+
Platform.SENSOR,
25+
Platform.SWITCH,
26+
]
2327

2428
NASWEB_CONFIG_URL = "https://{host}/page"
2529

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
"""Platform for NASweb alarms."""
2+
3+
from __future__ import annotations
4+
5+
import logging
6+
import time
7+
8+
from webio_api import Zone as NASwebZone
9+
from webio_api.const import STATE_ZONE_ALARM, STATE_ZONE_ARMED, STATE_ZONE_DISARMED
10+
11+
from homeassistant.components.alarm_control_panel import (
12+
DOMAIN as DOMAIN_ALARM_CONTROL_PANEL,
13+
AlarmControlPanelEntity,
14+
AlarmControlPanelEntityFeature,
15+
AlarmControlPanelState,
16+
CodeFormat,
17+
)
18+
from homeassistant.core import HomeAssistant, callback
19+
from homeassistant.helpers.device_registry import DeviceInfo
20+
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
21+
import homeassistant.helpers.entity_registry as er
22+
from homeassistant.helpers.typing import DiscoveryInfoType
23+
from homeassistant.helpers.update_coordinator import (
24+
BaseCoordinatorEntity,
25+
BaseDataUpdateCoordinatorProtocol,
26+
)
27+
28+
from . import NASwebConfigEntry
29+
from .const import DOMAIN, STATUS_UPDATE_MAX_TIME_INTERVAL
30+
31+
_LOGGER = logging.getLogger(__name__)
32+
ALARM_CONTROL_PANEL_TRANSLATION_KEY = "zone"
33+
34+
NASWEB_STATE_TO_HA_STATE = {
35+
STATE_ZONE_ALARM: AlarmControlPanelState.TRIGGERED,
36+
STATE_ZONE_ARMED: AlarmControlPanelState.ARMED_AWAY,
37+
STATE_ZONE_DISARMED: AlarmControlPanelState.DISARMED,
38+
}
39+
40+
41+
async def async_setup_entry(
42+
hass: HomeAssistant,
43+
config: NASwebConfigEntry,
44+
async_add_entities: AddConfigEntryEntitiesCallback,
45+
discovery_info: DiscoveryInfoType | None = None,
46+
) -> None:
47+
"""Set up alarm control panel platform."""
48+
coordinator = config.runtime_data
49+
current_zones: set[int] = set()
50+
51+
@callback
52+
def _check_entities() -> None:
53+
received_zones: dict[int, NASwebZone] = {
54+
entry.index: entry for entry in coordinator.webio_api.zones
55+
}
56+
added = {i for i in received_zones if i not in current_zones}
57+
removed = {i for i in current_zones if i not in received_zones}
58+
entities_to_add: list[ZoneEntity] = []
59+
for index in added:
60+
webio_zone = received_zones[index]
61+
if not isinstance(webio_zone, NASwebZone):
62+
_LOGGER.error("Cannot create ZoneEntity without NASwebZone")
63+
continue
64+
new_zone = ZoneEntity(coordinator, webio_zone)
65+
entities_to_add.append(new_zone)
66+
current_zones.add(index)
67+
async_add_entities(entities_to_add)
68+
entity_registry = er.async_get(hass)
69+
for index in removed:
70+
unique_id = f"{DOMAIN}.{config.unique_id}.zone.{index}"
71+
if entity_id := entity_registry.async_get_entity_id(
72+
DOMAIN_ALARM_CONTROL_PANEL, DOMAIN, unique_id
73+
):
74+
entity_registry.async_remove(entity_id)
75+
current_zones.remove(index)
76+
else:
77+
_LOGGER.warning("Failed to remove old zone: no entity_id")
78+
79+
coordinator.async_add_listener(_check_entities)
80+
_check_entities()
81+
82+
83+
class ZoneEntity(AlarmControlPanelEntity, BaseCoordinatorEntity):
84+
"""Entity representing NASweb zone."""
85+
86+
_attr_has_entity_name = True
87+
_attr_should_poll = False
88+
_attr_translation_key = ALARM_CONTROL_PANEL_TRANSLATION_KEY
89+
90+
def __init__(
91+
self, coordinator: BaseDataUpdateCoordinatorProtocol, nasweb_zone: NASwebZone
92+
) -> None:
93+
"""Initialize zone entity."""
94+
super().__init__(coordinator)
95+
self._zone = nasweb_zone
96+
self._attr_name = nasweb_zone.name
97+
self._attr_translation_placeholders = {"index": f"{nasweb_zone.index:2d}"}
98+
self._attr_unique_id = (
99+
f"{DOMAIN}.{self._zone.webio_serial}.zone.{self._zone.index}"
100+
)
101+
self._attr_device_info = DeviceInfo(
102+
identifiers={(DOMAIN, self._zone.webio_serial)},
103+
)
104+
105+
async def async_added_to_hass(self) -> None:
106+
"""When entity is added to hass."""
107+
await super().async_added_to_hass()
108+
self._handle_coordinator_update()
109+
110+
def _set_attr_available(
111+
self, entity_last_update: float, available: bool | None
112+
) -> None:
113+
if (
114+
self.coordinator.last_update is None
115+
or time.time() - entity_last_update >= STATUS_UPDATE_MAX_TIME_INTERVAL
116+
):
117+
self._attr_available = False
118+
else:
119+
self._attr_available = available if available is not None else False
120+
121+
@callback
122+
def _handle_coordinator_update(self) -> None:
123+
"""Handle updated data from the coordinator."""
124+
self._attr_alarm_state = NASWEB_STATE_TO_HA_STATE[self._zone.state]
125+
if self._zone.pass_type == 0:
126+
self._attr_code_format = CodeFormat.TEXT
127+
elif self._zone.pass_type == 1:
128+
self._attr_code_format = CodeFormat.NUMBER
129+
else:
130+
self._attr_code_format = None
131+
self._attr_code_arm_required = self._attr_code_format is not None
132+
133+
self._set_attr_available(self._zone.last_update, self._zone.available)
134+
self.async_write_ha_state()
135+
136+
async def async_update(self) -> None:
137+
"""Update the entity.
138+
139+
Only used by the generic entity update service.
140+
Scheduling updates is not necessary, the coordinator takes care of updates via push notifications.
141+
"""
142+
143+
@property
144+
def supported_features(self) -> AlarmControlPanelEntityFeature:
145+
"""Return the list of supported features."""
146+
return AlarmControlPanelEntityFeature.ARM_AWAY
147+
148+
async def async_alarm_arm_away(self, code: str | None = None) -> None:
149+
"""Arm away ZoneEntity."""
150+
await self._zone.arm(code)
151+
152+
async def async_alarm_disarm(self, code: str | None = None) -> None:
153+
"""Disarm ZoneEntity."""
154+
await self._zone.disarm(code)

homeassistant/components/nasweb/coordinator.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
KEY_INPUTS = "inputs"
2525
KEY_OUTPUTS = "outputs"
26+
KEY_ZONES = "zones"
2627

2728

2829
class NotificationCoordinator:
@@ -103,6 +104,7 @@ def __init__(
103104
KEY_OUTPUTS: self.webio_api.outputs,
104105
KEY_INPUTS: self.webio_api.inputs,
105106
KEY_TEMP_SENSOR: self.webio_api.temp_sensor,
107+
KEY_ZONES: self.webio_api.zones,
106108
}
107109
self.async_set_updated_data(data)
108110

@@ -197,5 +199,6 @@ async def process_status_update(self, new_status: dict) -> None:
197199
KEY_OUTPUTS: self.webio_api.outputs,
198200
KEY_INPUTS: self.webio_api.inputs,
199201
KEY_TEMP_SENSOR: self.webio_api.temp_sensor,
202+
KEY_ZONES: self.webio_api.zones,
200203
}
201204
self.async_set_updated_data(new_data)

homeassistant/components/nasweb/strings.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@
2424
}
2525
},
2626
"entity": {
27+
"alarm_control_panel": {
28+
"zone": {
29+
"name": "Zone {index}"
30+
}
31+
},
2732
"sensor": {
2833
"sensor_input": {
2934
"name": "Input {index}",

0 commit comments

Comments
 (0)