Skip to content

Commit 2cd6c2b

Browse files
authored
Add alarm platform to Comelit (home-assistant#104718)
* initial work on alarm * final work on alarm * coveragerc * add tests * add code validation * remove sensor changes for a dedicated PR * code optimization and cleanup * tweaks * tweak home-assistant#2 * apply suggestion * code quality * code quality home-assistant#2 * fix cover.py * api typing * use base classes where possibile * apply const as per review comment * cleanup unload entry * apply review comments
1 parent c6d1f1c commit 2cd6c2b

15 files changed

+328
-73
lines changed

.coveragerc

+1
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ omit =
173173
homeassistant/components/coinbase/sensor.py
174174
homeassistant/components/comed_hourly_pricing/sensor.py
175175
homeassistant/components/comelit/__init__.py
176+
homeassistant/components/comelit/alarm_control_panel.py
176177
homeassistant/components/comelit/const.py
177178
homeassistant/components/comelit/cover.py
178179
homeassistant/components/comelit/coordinator.py

homeassistant/components/comelit/__init__.py

+40-12
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,66 @@
11
"""Comelit integration."""
22

33

4+
from aiocomelit.const import BRIDGE
5+
46
from homeassistant.config_entries import ConfigEntry
5-
from homeassistant.const import CONF_HOST, CONF_PIN, CONF_PORT, Platform
7+
from homeassistant.const import CONF_HOST, CONF_PIN, CONF_PORT, CONF_TYPE, Platform
68
from homeassistant.core import HomeAssistant
79

810
from .const import DEFAULT_PORT, DOMAIN
9-
from .coordinator import ComelitSerialBridge
11+
from .coordinator import ComelitBaseCoordinator, ComelitSerialBridge, ComelitVedoSystem
1012

11-
PLATFORMS = [Platform.COVER, Platform.LIGHT, Platform.SENSOR, Platform.SWITCH]
13+
BRIDGE_PLATFORMS = [
14+
Platform.COVER,
15+
Platform.LIGHT,
16+
Platform.SENSOR,
17+
Platform.SWITCH,
18+
]
19+
VEDO_PLATFORMS = [
20+
Platform.ALARM_CONTROL_PANEL,
21+
]
1222

1323

1424
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
1525
"""Set up Comelit platform."""
16-
coordinator = ComelitSerialBridge(
17-
hass,
18-
entry.data[CONF_HOST],
19-
entry.data.get(CONF_PORT, DEFAULT_PORT),
20-
entry.data[CONF_PIN],
21-
)
26+
27+
coordinator: ComelitBaseCoordinator
28+
if entry.data.get(CONF_TYPE, BRIDGE) == BRIDGE:
29+
coordinator = ComelitSerialBridge(
30+
hass,
31+
entry.data[CONF_HOST],
32+
entry.data.get(CONF_PORT, DEFAULT_PORT),
33+
entry.data[CONF_PIN],
34+
)
35+
platforms = BRIDGE_PLATFORMS
36+
else:
37+
coordinator = ComelitVedoSystem(
38+
hass,
39+
entry.data[CONF_HOST],
40+
entry.data.get(CONF_PORT, DEFAULT_PORT),
41+
entry.data[CONF_PIN],
42+
)
43+
platforms = VEDO_PLATFORMS
2244

2345
await coordinator.async_config_entry_first_refresh()
2446

2547
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
2648

27-
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
49+
await hass.config_entries.async_forward_entry_setups(entry, platforms)
2850

2951
return True
3052

3153

3254
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
3355
"""Unload a config entry."""
34-
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
35-
coordinator: ComelitSerialBridge = hass.data[DOMAIN][entry.entry_id]
56+
57+
if entry.data.get(CONF_TYPE, BRIDGE) == BRIDGE:
58+
platforms = BRIDGE_PLATFORMS
59+
else:
60+
platforms = VEDO_PLATFORMS
61+
62+
coordinator: ComelitBaseCoordinator = hass.data[DOMAIN][entry.entry_id]
63+
if unload_ok := await hass.config_entries.async_unload_platforms(entry, platforms):
3664
await coordinator.api.logout()
3765
await coordinator.api.close()
3866
hass.data[DOMAIN].pop(entry.entry_id)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
"""Support for Comelit VEDO system."""
2+
from __future__ import annotations
3+
4+
import logging
5+
6+
from aiocomelit.api import ComelitVedoAreaObject
7+
from aiocomelit.const import ALARM_AREAS, AlarmAreaState
8+
9+
from homeassistant.components.alarm_control_panel import (
10+
AlarmControlPanelEntity,
11+
AlarmControlPanelEntityFeature,
12+
CodeFormat,
13+
)
14+
from homeassistant.config_entries import ConfigEntry
15+
from homeassistant.const import (
16+
STATE_ALARM_ARMED_AWAY,
17+
STATE_ALARM_ARMED_HOME,
18+
STATE_ALARM_ARMED_NIGHT,
19+
STATE_ALARM_ARMING,
20+
STATE_ALARM_DISARMED,
21+
STATE_ALARM_DISARMING,
22+
STATE_ALARM_TRIGGERED,
23+
)
24+
from homeassistant.core import HomeAssistant
25+
from homeassistant.helpers.entity_platform import AddEntitiesCallback
26+
from homeassistant.helpers.typing import StateType
27+
from homeassistant.helpers.update_coordinator import CoordinatorEntity
28+
29+
from .const import DOMAIN
30+
from .coordinator import ComelitVedoSystem
31+
32+
_LOGGER = logging.getLogger(__name__)
33+
34+
AWAY = "away"
35+
DISABLE = "disable"
36+
HOME = "home"
37+
HOME_P1 = "home_p1"
38+
HOME_P2 = "home_p2"
39+
NIGHT = "night"
40+
41+
ALARM_ACTIONS: dict[str, str] = {
42+
DISABLE: "dis", # Disarm
43+
HOME: "p1", # Arm P1
44+
NIGHT: "p12", # Arm P1+P2
45+
AWAY: "tot", # Arm P1+P2 + IR / volumetric
46+
}
47+
48+
49+
ALARM_AREA_ARMED_STATUS: dict[str, int] = {
50+
HOME_P1: 1,
51+
HOME_P2: 2,
52+
NIGHT: 3,
53+
AWAY: 4,
54+
}
55+
56+
57+
async def async_setup_entry(
58+
hass: HomeAssistant,
59+
config_entry: ConfigEntry,
60+
async_add_entities: AddEntitiesCallback,
61+
) -> None:
62+
"""Set up the Comelit VEDO system alarm control panel devices."""
63+
64+
coordinator: ComelitVedoSystem = hass.data[DOMAIN][config_entry.entry_id]
65+
66+
async_add_entities(
67+
ComelitAlarmEntity(coordinator, device, config_entry.entry_id)
68+
for device in coordinator.data[ALARM_AREAS].values()
69+
)
70+
71+
72+
class ComelitAlarmEntity(CoordinatorEntity[ComelitVedoSystem], AlarmControlPanelEntity):
73+
"""Representation of a Ness alarm panel."""
74+
75+
_attr_has_entity_name = True
76+
_attr_name = None
77+
_attr_code_format = CodeFormat.NUMBER
78+
_attr_code_arm_required = False
79+
_attr_supported_features = (
80+
AlarmControlPanelEntityFeature.ARM_AWAY
81+
| AlarmControlPanelEntityFeature.ARM_HOME
82+
)
83+
84+
def __init__(
85+
self,
86+
coordinator: ComelitVedoSystem,
87+
area: ComelitVedoAreaObject,
88+
config_entry_entry_id: str,
89+
) -> None:
90+
"""Initialize the alarm panel."""
91+
self._api = coordinator.api
92+
self._area_index = area.index
93+
super().__init__(coordinator)
94+
# Use config_entry.entry_id as base for unique_id
95+
# because no serial number or mac is available
96+
self._attr_unique_id = f"{config_entry_entry_id}-{area.index}"
97+
self._attr_device_info = coordinator.platform_device_info(area, "area")
98+
if area.p2:
99+
self._attr_supported_features |= AlarmControlPanelEntityFeature.ARM_NIGHT
100+
101+
@property
102+
def _area(self) -> ComelitVedoAreaObject:
103+
"""Return area object."""
104+
return self.coordinator.data[ALARM_AREAS][self._area_index]
105+
106+
@property
107+
def available(self) -> bool:
108+
"""Return True if alarm is available."""
109+
if self._area.human_status in [AlarmAreaState.ANOMALY, AlarmAreaState.UNKNOWN]:
110+
return False
111+
return super().available
112+
113+
@property
114+
def state(self) -> StateType:
115+
"""Return the state of the alarm."""
116+
117+
_LOGGER.debug(
118+
"Area %s status is: %s. Armed is %s",
119+
self._area.name,
120+
self._area.human_status,
121+
self._area.armed,
122+
)
123+
if self._area.human_status == AlarmAreaState.ARMED:
124+
if self._area.armed == ALARM_AREA_ARMED_STATUS[AWAY]:
125+
return STATE_ALARM_ARMED_AWAY
126+
if self._area.armed == ALARM_AREA_ARMED_STATUS[NIGHT]:
127+
return STATE_ALARM_ARMED_NIGHT
128+
return STATE_ALARM_ARMED_HOME
129+
130+
{
131+
AlarmAreaState.DISARMED: STATE_ALARM_DISARMED,
132+
AlarmAreaState.ENTRY_DELAY: STATE_ALARM_DISARMING,
133+
AlarmAreaState.EXIT_DELAY: STATE_ALARM_ARMING,
134+
AlarmAreaState.TRIGGERED: STATE_ALARM_TRIGGERED,
135+
}.get(self._area.human_status)
136+
137+
return None
138+
139+
async def async_alarm_disarm(self, code: str | None = None) -> None:
140+
"""Send disarm command."""
141+
if code != str(self._api.device_pin):
142+
return
143+
await self._api.set_zone_status(self._area.index, ALARM_ACTIONS[DISABLE])
144+
145+
async def async_alarm_arm_away(self, code: str | None = None) -> None:
146+
"""Send arm away command."""
147+
await self._api.set_zone_status(self._area.index, ALARM_ACTIONS[AWAY])
148+
149+
async def async_alarm_arm_home(self, code: str | None = None) -> None:
150+
"""Send arm home command."""
151+
await self._api.set_zone_status(self._area.index, ALARM_ACTIONS[HOME])
152+
153+
async def async_alarm_arm_night(self, code: str | None = None) -> None:
154+
"""Send arm night command."""
155+
await self._api.set_zone_status(self._area.index, ALARM_ACTIONS[NIGHT])

homeassistant/components/comelit/config_flow.py

+15-4
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,22 @@
44
from collections.abc import Mapping
55
from typing import Any
66

7-
from aiocomelit import ComeliteSerialBridgeApi, exceptions as aiocomelit_exceptions
7+
from aiocomelit import (
8+
ComeliteSerialBridgeApi,
9+
ComelitVedoApi,
10+
exceptions as aiocomelit_exceptions,
11+
)
12+
from aiocomelit.api import ComelitCommonApi
13+
from aiocomelit.const import BRIDGE
814
import voluptuous as vol
915

1016
from homeassistant import core, exceptions
1117
from homeassistant.config_entries import ConfigEntry, ConfigFlow
12-
from homeassistant.const import CONF_HOST, CONF_PIN, CONF_PORT
18+
from homeassistant.const import CONF_HOST, CONF_PIN, CONF_PORT, CONF_TYPE
1319
from homeassistant.data_entry_flow import FlowResult
1420
import homeassistant.helpers.config_validation as cv
1521

16-
from .const import _LOGGER, DEFAULT_PORT, DOMAIN
22+
from .const import _LOGGER, DEFAULT_PORT, DEVICE_TYPE_LIST, DOMAIN
1723

1824
DEFAULT_HOST = "192.168.1.252"
1925
DEFAULT_PIN = 111111
@@ -27,6 +33,7 @@ def user_form_schema(user_input: dict[str, Any] | None) -> vol.Schema:
2733
vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string,
2834
vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port,
2935
vol.Optional(CONF_PIN, default=DEFAULT_PIN): cv.positive_int,
36+
vol.Required(CONF_TYPE, default=BRIDGE): vol.In(DEVICE_TYPE_LIST),
3037
}
3138
)
3239

@@ -39,7 +46,11 @@ async def validate_input(
3946
) -> dict[str, str]:
4047
"""Validate the user input allows us to connect."""
4148

42-
api = ComeliteSerialBridgeApi(data[CONF_HOST], data[CONF_PORT], data[CONF_PIN])
49+
api: ComelitCommonApi
50+
if data.get(CONF_TYPE, BRIDGE) == BRIDGE:
51+
api = ComeliteSerialBridgeApi(data[CONF_HOST], data[CONF_PORT], data[CONF_PIN])
52+
else:
53+
api = ComelitVedoApi(data[CONF_HOST], data[CONF_PORT], data[CONF_PIN])
4354

4455
try:
4556
await api.login()
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
"""Comelit constants."""
22
import logging
33

4+
from aiocomelit.const import BRIDGE, VEDO
5+
46
_LOGGER = logging.getLogger(__package__)
57

68
DOMAIN = "comelit"
79
DEFAULT_PORT = 80
10+
DEVICE_TYPE_LIST = [BRIDGE, VEDO]

0 commit comments

Comments
 (0)