Skip to content

Commit 4b28866

Browse files
authored
Add client support for alternative arming modes (#53)
1 parent 3184027 commit 4b28866

File tree

5 files changed

+55
-25
lines changed

5 files changed

+55
-25
lines changed

nessclient/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from .client import Client
2-
from .alarm import ArmingState
2+
from .alarm import ArmingState, ArmingMode
33
from .event import BaseEvent
44

5-
__all__ = ["Client", "ArmingState", "BaseEvent"]
5+
__all__ = ["Client", "ArmingState", "ArmingMode", "BaseEvent"]
66
__version__ = "0.0.0-dev"

nessclient/alarm.py

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,29 @@ class ArmingState(Enum):
1515
TRIGGERED = "TRIGGERED"
1616

1717

18+
class ArmingMode(Enum):
19+
ARMED_AWAY = "ARMED_AWAY"
20+
ARMED_HOME = "ARMED_HOME"
21+
ARMED_DAY = "ARMED_DAY"
22+
ARMED_NIGHT = "ARMED_NIGHT"
23+
ARMED_VACATION = "ARMED_VACATION"
24+
ARMED_HIGHEST = "ARMED_HIGHEST"
25+
26+
1827
class Alarm:
1928
"""
2029
In-memory representation of the state of the alarm the client is connected
2130
to.
2231
"""
2332

24-
ARM_EVENTS = [
25-
SystemStatusEvent.EventType.ARMED_AWAY,
26-
SystemStatusEvent.EventType.ARMED_HOME,
27-
SystemStatusEvent.EventType.ARMED_DAY,
28-
SystemStatusEvent.EventType.ARMED_NIGHT,
29-
SystemStatusEvent.EventType.ARMED_VACATION,
30-
SystemStatusEvent.EventType.ARMED_HIGHEST,
31-
]
33+
ARM_EVENTS_MAP = {
34+
SystemStatusEvent.EventType.ARMED_AWAY: ArmingMode.ARMED_AWAY,
35+
SystemStatusEvent.EventType.ARMED_HOME: ArmingMode.ARMED_HOME,
36+
SystemStatusEvent.EventType.ARMED_DAY: ArmingMode.ARMED_DAY,
37+
SystemStatusEvent.EventType.ARMED_NIGHT: ArmingMode.ARMED_NIGHT,
38+
SystemStatusEvent.EventType.ARMED_VACATION: ArmingMode.ARMED_VACATION,
39+
SystemStatusEvent.EventType.ARMED_HIGHEST: ArmingMode.ARMED_HIGHEST,
40+
}
3241

3342
@dataclass
3443
class Zone:
@@ -39,7 +48,11 @@ def __init__(self, infer_arming_state: bool = False) -> None:
3948
self.arming_state: ArmingState = ArmingState.UNKNOWN
4049
self.zones: List[Alarm.Zone] = [Alarm.Zone(triggered=None) for _ in range(16)]
4150

42-
self._on_state_change: Optional[Callable[["ArmingState"], None]] = None
51+
self._arming_mode: ArmingMode | None = None
52+
53+
self._on_state_change: Optional[
54+
Callable[[ArmingState, ArmingMode | None], None]
55+
] = None
4356
self._on_zone_change: Optional[Callable[[int, bool], None]] = None
4457

4558
def handle_event(self, event: BaseEvent) -> None:
@@ -121,18 +134,20 @@ def _handle_system_status_event(self, event: SystemStatusEvent) -> None:
121134
# state to armed
122135
if self.arming_state == ArmingState.EXIT_DELAY:
123136
return self._update_arming_state(ArmingState.ARMED)
124-
elif event.type in Alarm.ARM_EVENTS:
137+
elif event.type in Alarm.ARM_EVENTS_MAP.keys():
138+
self._arming_mode = Alarm.ARM_EVENTS_MAP[event.type]
125139
return self._update_arming_state(ArmingState.ARMING)
126140
elif event.type == SystemStatusEvent.EventType.DISARMED:
141+
self._arming_mode = None # Restore arming mode on disarmed.
127142
return self._update_arming_state(ArmingState.DISARMED)
128143
elif event.type == SystemStatusEvent.EventType.ARMING_DELAYED:
129144
pass
130145

131-
def _update_arming_state(self, state: "ArmingState") -> None:
146+
def _update_arming_state(self, state: ArmingState) -> None:
132147
if self.arming_state != state:
133148
self.arming_state = state
134149
if self._on_state_change is not None:
135-
self._on_state_change(state)
150+
self._on_state_change(state, self._arming_mode)
136151

137152
def _update_zone(self, zone_id: int, state: bool) -> None:
138153
zone = self.zones[zone_id - 1]
@@ -141,7 +156,9 @@ def _update_zone(self, zone_id: int, state: bool) -> None:
141156
if self._on_zone_change is not None:
142157
self._on_zone_change(zone_id, state)
143158

144-
def on_state_change(self, f: Callable[[ArmingState], None]) -> None:
159+
def on_state_change(
160+
self, f: Callable[[ArmingState, ArmingMode | None], None]
161+
) -> None:
145162
self._on_state_change = f
146163

147164
def on_zone_change(self, f: Callable[[int, bool], None]) -> None:

nessclient/cli/events.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import click
33

44
from ..client import Client
5-
from ..alarm import ArmingState
5+
from ..alarm import ArmingState, ArmingMode
66
from ..event import BaseEvent
77
from .server import DEFAULT_PORT
88

@@ -25,11 +25,11 @@ def events(
2525

2626
@client.on_zone_change
2727
def on_zone_change(zone: int, triggered: bool) -> None:
28-
print("Zone {} changed to {}".format(zone, triggered))
28+
print(f"Zone {zone} changed to {triggered}")
2929

3030
@client.on_state_change
31-
def on_state_change(state: ArmingState) -> None:
32-
print("Alarm state changed to {}".format(state))
31+
def on_state_change(state: ArmingState, arming_mode: ArmingMode | None) -> None:
32+
print(f"Alarm state changed to {state} (mode: {arming_mode})")
3333

3434
@client.on_event_received
3535
def on_event_received(event: BaseEvent) -> None:

nessclient/client.py

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

77
from justbackoff import Backoff
88

9-
from .alarm import ArmingState, Alarm
9+
from .alarm import ArmingState, Alarm, ArmingMode
1010
from .connection import Connection, IP232Connection
1111
from .event import BaseEvent
1212
from .packet import CommandType, Packet
@@ -167,8 +167,8 @@ async def close(self) -> None:
167167
await self._connection.close()
168168

169169
def on_state_change(
170-
self, f: Callable[[ArmingState], None]
171-
) -> Callable[[ArmingState], None]:
170+
self, f: Callable[[ArmingState, ArmingMode | None], None]
171+
) -> Callable[[ArmingState, ArmingMode | None], None]:
172172
self.alarm.on_state_change(f)
173173
return f
174174

nessclient_tests/test_alarm.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import pytest
44

5-
from nessclient.alarm import Alarm, ArmingState
5+
from nessclient.alarm import Alarm, ArmingState, ArmingMode
66
from nessclient.event import ArmingUpdate, ZoneUpdate, SystemStatusEvent
77

88

@@ -119,14 +119,27 @@ def test_handle_event_arming_update_infer_arming_state_unknown_empty():
119119

120120

121121
def test_handle_event_arming_update_callback(alarm):
122+
# emit a SystemStatusEvent for an arming mode to test that it is emitted
123+
# during EXIT_DELAY state change.
124+
alarm.handle_event(
125+
SystemStatusEvent(
126+
address=None,
127+
timestamp=None,
128+
type=SystemStatusEvent.EventType.ARMED_AWAY,
129+
area=0,
130+
zone=1,
131+
)
132+
)
133+
122134
cb = Mock()
123135
alarm.on_state_change(cb)
136+
124137
event = ArmingUpdate(
125138
status=[ArmingUpdate.ArmingStatus.AREA_1_ARMED], address=None, timestamp=None
126139
)
127140
alarm.handle_event(event)
128141
assert cb.call_count == 1
129-
assert cb.call_args[0] == (ArmingState.EXIT_DELAY,)
142+
assert cb.call_args[0] == (ArmingState.EXIT_DELAY, ArmingMode.ARMED_AWAY)
130143

131144

132145
def test_handle_event_system_status_unsealed_zone(alarm):
@@ -297,7 +310,7 @@ def test_handle_event_system_status_exit_delay_end_from_armed(alarm):
297310

298311

299312
def test_handle_event_system_status_arm_events(alarm):
300-
for event_type in Alarm.ARM_EVENTS:
313+
for event_type in Alarm.ARM_EVENTS_MAP.keys():
301314
alarm.arming_state = ArmingState.DISARMED
302315
event = SystemStatusEvent(
303316
address=None, timestamp=None, type=event_type, area=0, zone=1

0 commit comments

Comments
 (0)