Skip to content

Commit

Permalink
Merge 425f64d into 0bdb356
Browse files Browse the repository at this point in the history
  • Loading branch information
nickw444 committed Feb 20, 2019
2 parents 0bdb356 + 425f64d commit b6961a3
Show file tree
Hide file tree
Showing 7 changed files with 234 additions and 89 deletions.
25 changes: 9 additions & 16 deletions nessclient/alarm.py
Expand Up @@ -17,9 +17,8 @@ class ArmingState(Enum):

class Alarm:
"""
In-memory representation of the state of the alarm the client is connected to.
TODO(NW): Handle output state events to determine when alarm is on/off
In-memory representation of the state of the alarm the client is connected
to.
"""
ARM_EVENTS = [
SystemStatusEvent.EventType.ARMED_AWAY,
Expand Down Expand Up @@ -51,19 +50,12 @@ def handle_event(self, event: BaseEvent) -> None:
self._handle_system_status_event(event)

def _handle_arming_update(self, update: ArmingUpdate) -> None:
if update.status == [ArmingUpdate.ArmingStatus.MANUAL_EXCLUDE_MODE]:
if update.status == [ArmingUpdate.ArmingStatus.AREA_1_ARMED]:
return self._update_arming_state(ArmingState.EXIT_DELAY)
if ArmingUpdate.ArmingStatus.MANUAL_EXCLUDE_MODE in update.status and \
ArmingUpdate.ArmingStatus.DAY_ZONE_SELECT:
# TODO(NW): This might not be the correct condition for an "armed"
# system, in fact, the same states are shown throughout armed, and
# entry delay. We should determine a better way to query the
# current alarm state.
if ArmingUpdate.ArmingStatus.AREA_1_ARMED in update.status and \
ArmingUpdate.ArmingStatus.AREA_1_FULLY_ARMED in update.status:
return self._update_arming_state(ArmingState.ARMED)
elif self.arming_state == ArmingState.UNKNOWN:
# TODO(NW): Initially update to disarmed when the state is unknown.
# In the future, it would be ideal to infer other states by making
# calls to other status commands (i.e. zones in delay).
else:
return self._update_arming_state(ArmingState.DISARMED)

def _handle_zone_input_update(self, update: ZoneUpdate) -> None:
Expand Down Expand Up @@ -92,7 +84,8 @@ def _handle_system_status_event(self, event: SystemStatusEvent) -> None:
elif event.type == SystemStatusEvent.EventType.ALARM:
return self._update_arming_state(ArmingState.TRIGGERED)
elif event.type == SystemStatusEvent.EventType.ALARM_RESTORE:
return self._update_arming_state(ArmingState.ARMED)
if self.arming_state != ArmingState.DISARMED:
return self._update_arming_state(ArmingState.ARMED)
elif event.type == SystemStatusEvent.EventType.ENTRY_DELAY_START:
return self._update_arming_state(ArmingState.ENTRY_DELAY)
elif event.type == SystemStatusEvent.EventType.ENTRY_DELAY_END:
Expand All @@ -103,7 +96,7 @@ def _handle_system_status_event(self, event: SystemStatusEvent) -> None:
# Exit delay finished - if we were in the process of arming update
# state to armed
if self.arming_state == ArmingState.EXIT_DELAY:
self._update_arming_state(ArmingState.ARMED)
return self._update_arming_state(ArmingState.ARMED)
elif event.type in Alarm.ARM_EVENTS:
return self._update_arming_state(ArmingState.ARMING)
elif event.type == SystemStatusEvent.EventType.DISARMED:
Expand Down
6 changes: 3 additions & 3 deletions nessclient/cli/server/alarm_server.py
Expand Up @@ -143,11 +143,11 @@ def get_events_for_state_update(
def get_arming_status(state: Alarm.ArmingState) -> List[ArmingUpdate.ArmingStatus]:
if state == Alarm.ArmingState.ARMED_AWAY:
return [
ArmingUpdate.ArmingStatus.MANUAL_EXCLUDE_MODE,
ArmingUpdate.ArmingStatus.DAY_ZONE_SELECT
ArmingUpdate.ArmingStatus.AREA_1_ARMED,
ArmingUpdate.ArmingStatus.AREA_1_FULLY_ARMED
]
elif state == Alarm.ArmingState.EXIT_DELAY:
return [ArmingUpdate.ArmingStatus.MANUAL_EXCLUDE_MODE]
return [ArmingUpdate.ArmingStatus.AREA_1_ARMED]
else:
return []

Expand Down
107 changes: 67 additions & 40 deletions nessclient/event.py
Expand Up @@ -216,19 +216,28 @@ def encode(self) -> Packet:

class MiscellaneousAlarmsUpdate(StatusUpdate):
class AlarmType(Enum):
DURESS = 0x0001
PANIC = 0x0002
MEDICAL = 0x0004
FIRE = 0x0008
INSTALL_END = 0x0010
EXT_TAMPER = 0x0020
PANEL_TAMPER = 0x0040
KEYPAD_TAMPER = 0x0080
PENDANT_PANIC = 0x0100
PANEL_BATTERY_LOW = 0x0200
PANEL_BATTERY_LOW2 = 0x0400
MAINS_FAIL = 0x0800
CBUS_FAIL = 0x1000
"""
Note: The ness provided documentation has the byte endianness
incorrectly documented. For this reason, these enum values have
reversed byte ordering compared to the ness provided documentation.
This only applies to some enums, and thus must be applied on a
case-by-case basis
"""

DURESS = 0x0100
PANIC = 0x0200
MEDICAL = 0x0400
FIRE = 0x0800
INSTALL_END = 0x1000
EXT_TAMPER = 0x2000
PANEL_TAMPER = 0x4000
KEYPAD_TAMPER = 0x8000
PENDANT_PANIC = 0x0001
PANEL_BATTERY_LOW = 0x0002
PANEL_BATTERY_LOW2 = 0x0004
MAINS_FAIL = 0x0008
CBUS_FAIL = 0x0010

def __init__(self, included_alarms: List['MiscellaneousAlarmsUpdate.AlarmType'],
address: Optional[int],
Expand All @@ -252,17 +261,26 @@ def decode(cls, packet: Packet) -> 'MiscellaneousAlarmsUpdate':

class ArmingUpdate(StatusUpdate):
class ArmingStatus(Enum):
AREA_1_ARMED = 0x0001
AREA_2_ARMED = 0x0002
AREA_1_FULLY_ARMED = 0x0004
AREA_2_FULLY_ARMED = 0x0008
MONITOR_ARMED = 0x0010
DAY_MODE_ARMED = 0x0020
ENTRY_DELAY_1_ON = 0x0040
ENTRY_DELAY_2_ON = 0x0080
MANUAL_EXCLUDE_MODE = 0x0100
MEMORY_MODE = 0x0200
DAY_ZONE_SELECT = 0x0400
"""
Note: The ness provided documentation has the byte endianness
incorrectly documented. For this reason, these enum values have
reversed byte ordering compared to the ness provided documentation.
This only applies to some enums, and thus must be applied on a
case-by-case basis
"""

AREA_1_ARMED = 0x0100
AREA_2_ARMED = 0x0200
AREA_1_FULLY_ARMED = 0x0400
AREA_2_FULLY_ARMED = 0x0800
MONITOR_ARMED = 0x1000
DAY_MODE_ARMED = 0x2000
ENTRY_DELAY_1_ON = 0x4000
ENTRY_DELAY_2_ON = 0x8000
MANUAL_EXCLUDE_MODE = 0x0001
MEMORY_MODE = 0x0002
DAY_ZONE_SELECT = 0x0004

def __init__(self, status: List['ArmingUpdate.ArmingStatus'],
address: Optional[int],
Expand Down Expand Up @@ -298,22 +316,31 @@ def encode(self) -> Packet:

class OutputsUpdate(StatusUpdate):
class OutputType(Enum):
SIREN_LOUD = 0x0001
SIREN_SOFT = 0x0002
SIREN_SOFT_MONITOR = 0x0004
SIREN_SOFT_FIRE = 0x0008
STROBE = 0x0010
RESET = 0x0020
SONALART = 0x0040
KEYPAD_DISPLAY_ENABLE = 0x0080
AUX1 = 0x0100
AUX2 = 0x0200
AUX3 = 0x0400
AUX4 = 0x0800
MONITOR_OUT = 0x1000
POWER_FAIL = 0x2000
PANEL_BATT_FAIL = 0x4000
TAMPER_XPAND = 0x8000
"""
Note: The ness provided documentation has the byte endianness
incorrectly documented. For this reason, these enum values have
reversed byte ordering compared to the ness provided documentation.
This only applies to some enums, and thus must be applied on a
case-by-case basis
"""

SIREN_LOUD = 0x0100
SIREN_SOFT = 0x0200
SIREN_SOFT_MONITOR = 0x0400
SIREN_SOFT_FIRE = 0x0800
STROBE = 0x1000
RESET = 0x2000
SONALART = 0x4000
KEYPAD_DISPLAY_ENABLE = 0x8000
AUX1 = 0x0001
AUX2 = 0x0002
AUX3 = 0x0004
AUX4 = 0x0008
MONITOR_OUT = 0x0010
POWER_FAIL = 0x0020
PANEL_BATT_FAIL = 0x0040
TAMPER_XPAND = 0x0080

def __init__(self, outputs: List['OutputsUpdate.OutputType'],
address: Optional[int], timestamp: Optional[datetime.datetime]):
Expand Down
32 changes: 13 additions & 19 deletions nessclient/packet.py
Expand Up @@ -2,7 +2,7 @@
import logging
from dataclasses import dataclass
from enum import Enum
from typing import Optional, Iterable
from typing import Optional

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -107,20 +107,21 @@ def decode(cls, _data: str) -> 'Packet':
start = data.take_hex()

address = None
if has_address(start):
if has_address(start, len(_data)):
address = data.take_hex(half=is_user_interface_req(start))

length = data.take_hex()
data_length = length & 0x7f
seq = length >> 7
command = CommandType(data.take_hex())
msg_data = data.take_bytes(data_length, half=is_user_interface_req(start))
msg_data = data.take_bytes(data_length,
half=is_user_interface_req(start))
timestamp = None
if has_timestamp(start):
timestamp = decode_timestamp(data.take_bytes(6))

# TODO(NW): Figure out checksum validation
checksum = data.take_hex() # noqa
checksum = data.take_hex() # noqa

if not data.is_consumed():
raise ValueError('Unable to consume all data')
Expand Down Expand Up @@ -160,8 +161,14 @@ def is_consumed(self) -> bool:
return self._position >= len(self._data)


def has_address(start: int) -> bool:
return bool(0x01 & start) or start == 0x82
def has_address(start: int, data_length: int) -> bool:
"""
Determine whether the packet has an "address" encoded into it.
There exists an undocumented bug/edge case in the spec - some packets
with 0x82 as _start_, still encode the address into the packet, and thus
throws off decoding. This edge case is handled explicitly.
"""
return bool(0x01 & start) or (start == 0x82 and data_length == 16)


def has_timestamp(start: int) -> bool:
Expand All @@ -178,16 +185,3 @@ def is_user_interface_resp(start: int) -> bool:

def decode_timestamp(data: str) -> datetime.datetime:
return datetime.datetime.strptime(data, '%y%m%d%H%M%S')


def split_string(x: str, n: int) -> Iterable[str]:
for i in range(0, len(x), n):
yield x[i:i + n]


def is_data_valid(data: str) -> bool:
sum = 0
for byte in split_string(data, 2):
sum += int(byte, 16)

return sum & 0xff == 0
14 changes: 14 additions & 0 deletions nessclient_tests/fixtures/sample_output.txt
Expand Up @@ -141,3 +141,17 @@
87008361010700180922123924db
8200036012000009
8300c6012345678912EE7
8709036101050018122709413536
87098361000400181227094135b8
8709036101040018122709413735
8704036100120019012909332957
820003600000001b
8200036014000007
820003600000001b
8709836100050019010509174800
8709836100050019010509174800
820003600001001a
820361230001f6
8300360S00E9
8704036100140019012915060699
87048361001300190129231052b6

0 comments on commit b6961a3

Please sign in to comment.