Skip to content

Commit

Permalink
Merge branch 'dev'
Browse files Browse the repository at this point in the history
  • Loading branch information
yozik04 committed Dec 9, 2019
2 parents 0e471be + 5c983c2 commit 28becd2
Show file tree
Hide file tree
Showing 5 changed files with 173 additions and 129 deletions.
194 changes: 96 additions & 98 deletions paradox/interfaces/mqtt/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,43 @@

logger = logging.getLogger('PAI').getChild(__name__)

PreparseResponse = namedtuple('preparse_response', 'topics element content')
ParsedMessage = namedtuple('parsed_message', 'topics element content')


def mqtt_handle_decorator(func: typing.Callable[["BasicMQTTInterface", ParsedMessage], None]):
def wrapper(self: "BasicMQTTInterface", client: Client, userdata, message: MQTTMessage):
try:
logger.info("message topic={}, payload={}".format(
message.topic, str(message.payload.decode("utf-8"))))

if message.retain:
logger.warning("Ignoring retained commands")
return

if self.alarm is None:
logger.warning("No alarm. Ignoring command")
return

topic = message.topic.split(cfg.MQTT_BASE_TOPIC)[1]

topics = topic.split("/")

if len(topics) < 3:
logger.error(
"Invalid topic in mqtt message: {}".format(message.topic))
return

content = message.payload.decode("utf-8").strip()

element = None
if len(topics) >= 4:
element = topics[3]

func(self, ParsedMessage(topics, element, content))
except Exception:
logger.exception("Failed to execute command")

return wrapper


class BasicMQTTInterface(AbstractMQTTInterface):
Expand Down Expand Up @@ -50,114 +86,76 @@ def on_connect(self, mqttc, userdata, flags, result):
self._mqtt_handle_notifications
)

def _preparse_message(self, message: MQTTMessage) -> typing.Optional[PreparseResponse]:
logger.info("message topic={}, payload={}".format(
message.topic, str(message.payload.decode("utf-8"))))

if message.retain:
logger.warning("Ignoring retained commands")
return None

if self.alarm is None:
logger.warning("No alarm. Ignoring command")
return None
@mqtt_handle_decorator
def _mqtt_handle_notifications(self, prep: ParsedMessage):
topics = prep.topics
try:
level = EventLevel.from_name(topics[2].upper())
except Exception as e:
logger.error(e)
return

topic = message.topic.split(cfg.MQTT_BASE_TOPIC)[1]
ps.sendNotification(Notification(sender=self.name, message=prep.content, level=level))

topics = topic.split("/")
@mqtt_handle_decorator
def _mqtt_handle_zone_control(self, prep: ParsedMessage):
topics, element, command = prep
if not self.alarm.control_zone(element, command):
logger.warning("Zone command refused: {}={}".format(element, command))

if len(topics) < 3:
logger.error(
"Invalid topic in mqtt message: {}".format(message.topic))
return None
@mqtt_handle_decorator
def _mqtt_handle_partition_control(self, prep: ParsedMessage):
topics, element, command = prep
command = cfg.MQTT_COMMAND_ALIAS.get(command, command)

content = message.payload.decode("utf-8").strip()
if command.startswith('code_toggle-'):
tokens = command.split('-')
if len(tokens) < 2:
logger.warning("Invalid token length {}".format(len(tokens)))
return

element = None
if len(topics) >= 4:
element = topics[3]
if tokens[1] not in cfg.MQTT_TOGGLE_CODES:
logger.warning("Invalid toggle code {}".format(tokens[1]))
return

return PreparseResponse(topics, element, content)
if element.lower() == 'all':
command = 'arm'

def _mqtt_handle_notifications(self, client: Client, userdata, message: MQTTMessage):
prep = self._preparse_message(message)
if prep:
topics = prep.topics
try:
level = EventLevel.from_name(topics[2].upper())
except Exception as e:
logger.error(e)
for k, v in self.partitions.items():
# If "all" and a single partition is armed, default is
# to disarm
for k1, v1 in self.partitions[k].items():
if (k1 == 'arm' or k1 == 'exit_delay' or k1 == 'entry_delay') and v1:
command = 'disarm'
break

if command == 'disarm':
break

elif element in self.partitions:
if ('arm' in self.partitions[element] and self.partitions[element]['arm']) \
or ('exit_delay' in self.partitions[element] and self.partitions[element]['exit_delay']):
command = 'disarm'
else:
command = 'arm'
else:
logger.warning("Element {} not found".format(element))
return

ps.sendNotification(Notification(sender=self.name, message=prep.content, level=level))
ps.sendNotification(Notification(sender="mqtt", message="Command by {}: {}".format(
cfg.MQTT_TOGGLE_CODES[tokens[1]], command), level=EventLevel.INFO))

def _mqtt_handle_zone_control(self, client: Client, userdata, message: MQTTMessage):
prep = self._preparse_message(message)
if prep:
topics, element, command = prep
if not self.alarm.control_zone(element, command):
logger.warning(
"Zone command refused: {}={}".format(element, command))
logger.info("Partition command: {} = {}".format(element, command))
if not self.alarm.control_partition(element, command):
logger.warning("Partition command refused: {}={}".format(element, command))

def _mqtt_handle_partition_control(self, client: Client, userdata, message: MQTTMessage):
try:
prep = self._preparse_message(message)
if prep:
topics, element, command = prep
command = cfg.MQTT_COMMAND_ALIAS.get(command, command)

if command.startswith('code_toggle-'):
tokens = command.split('-')
if len(tokens) < 2:
return

if tokens[1] not in cfg.MQTT_TOGGLE_CODES:
logger.warning("Invalid toggle code {}".format(tokens[1]))
return

if element.lower() == 'all':
command = 'arm'

for k, v in self.partitions.items():
# If "all" and a single partition is armed, default is
# to disarm
for k1, v1 in self.partitions[k].items():
if (k1 == 'arm' or k1 == 'exit_delay' or k1 == 'entry_delay') and v1:
command = 'disarm'
break

if command == 'disarm':
break

elif element in self.partitions:
if ('arm' in self.partitions[element] and self.partitions[element]['arm']) \
or ('exit_delay' in self.partitions[element] and self.partitions[element]['exit_delay']):
command = 'disarm'
else:
command = 'arm'
else:
logger.debug("Element {} not found".format(element))
return

ps.sendNotification(Notification(sender="mqtt", message="Command by {}: {}".format(
cfg.MQTT_TOGGLE_CODES[tokens[1]], command), level=EventLevel.INFO))

logger.debug("Partition command: {} = {}".format(element, command))
if not self.alarm.control_partition(element, command):
logger.warning(
"Partition command refused: {}={}".format(element, command))
except:
logger.exception("Handle Partition Control")

def _mqtt_handle_output_control(self, client: Client, userdata, message: MQTTMessage):
prep = self._preparse_message(message)
if prep:
topics, element, command = prep
logger.debug("Output command: {} = {}".format(element, command))

if not self.alarm.control_output(element, command):
logger.warning(
"Output command refused: {}={}".format(element, command))
@mqtt_handle_decorator
def _mqtt_handle_output_control(self, prep: ParsedMessage):
topics, element, command = prep
logger.debug("Output command: {} = {}".format(element, command))

if not self.alarm.control_output(element, command):
logger.warning("Output command refused: {}={}".format(element, command))

def _handle_panel_event(self, event: Event):
"""
Expand Down
31 changes: 1 addition & 30 deletions paradox/interfaces/mqtt/homeassistant.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,33 +137,4 @@ def _process_zone_statuses(self, zone_statuses):
sanitize_key(zone['key'])
)

self.publish(configuration_topic, json.dumps(config), 0, cfg.MQTT_RETAIN)

def _preparse_message(self, message) -> typing.Optional[PreparseResponse]:
logger.info("message topic={}, payload={}".format(
message.topic, str(message.payload.decode("utf-8"))))

if message.retain:
logger.warning("Ignoring retained commands")
return None

if self.alarm is None:
logger.warning("No alarm. Ignoring command")
return None

topic = message.topic.split(cfg.MQTT_BASE_TOPIC)[1]

topics = topic.split("/")

if len(topics) < 3:
logger.error(
"Invalid topic in mqtt message: {}".format(message.topic))
return None

content = message.payload.decode("latin").strip()

element = None
if len(topics) >= 4:
element = topics[3]

return PreparseResponse(topics, element, content)
self.publish(configuration_topic, json.dumps(config), 0, cfg.MQTT_RETAIN)
3 changes: 3 additions & 0 deletions paradox/paradox.py
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,7 @@ def control_zone(self, zone: str, command: str) -> bool:

# Not Found
if len(zones_selected) == 0:
logger.error('No zones selected')
return False

# Apply state changes
Expand Down Expand Up @@ -365,6 +366,7 @@ def control_partition(self, partition: str, command: str) -> bool:

# Not Found
if len(partitions_selected) == 0:
logger.error('No partitions selected')
return False

# Apply state changes
Expand Down Expand Up @@ -392,6 +394,7 @@ def control_output(self, output, command) -> bool:

# Not Found
if len(outputs_selected) == 0:
logger.error('No outputs selected')
return False

# Apply state changes
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

setup(
name="paradox-alarm-interface",
version="1.1.0",
version="1.2.0",
author="João Paulo Barraca",
author_email="[email protected]",
description="Interface to Paradox Alarm Panels",
Expand Down
72 changes: 72 additions & 0 deletions tests/hardware/spectra_magellan/test_label_loading.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import pytest
import binascii

# Label loading
request_reply_map = {
b'50000010000000000000000000000000000000000000000000000000000000000001000061': b'52000010456c04749672202020202020202020204e617070616c69202020202020202020b8',
b'50000020000000000000000000000000000000000000000000000000000000000001000071': b'520000204e617070616c692020202020202020204b6f6e7968612020202020202020202001',
b'50000030000000000000000000000000000000000000000000000000000000000001000081': b'520000304b6f6e796861202020202020202020204d616d6120737a6f6261202020202020a7',
b'50000040000000000000000000000000000000000000000000000000000000000001000091': b'520000404d616d6120737a6f626120202020202047796572656b20737a6f62612020202033'
}
# 2019-12-04 13:09:38,232 - DEBUG - PAI.paradox.connections.serial_connection - PAI -> SER
# 2019-12-04 13:09:38,323 - DEBUG - PAI.paradox.connections.serial_connection - SER -> PAI
# 2019-12-04 13:09:38,332 - DEBUG - PAI.paradox.connections.serial_connection - PAI -> SER
# 2019-12-04 13:09:38,418 - DEBUG - PAI.paradox.connections.serial_connection - SER -> PAI
# 2019-12-04 13:09:38,426 - DEBUG - PAI.paradox.connections.serial_connection - PAI -> SER
# 2019-12-04 13:09:38,514 - DEBUG - PAI.paradox.connections.serial_connection - SER -> PAI
# 2019-12-04 13:09:38,522 - DEBUG - PAI.paradox.connections.serial_connection - PAI -> SER b'500000500000000000000000000000000000000000000000000000000000000000010000a1'
# 2019-12-04 13:09:38,609 - DEBUG - PAI.paradox.connections.serial_connection - SER -> PAI b'5200005047796572656b20737a6f6261202020205a6f7a8d20737a6f626120202020202097'
# 2019-12-04 13:09:38,617 - DEBUG - PAI.paradox.connections.serial_connection - PAI -> SER b'500000600000000000000000000000000000000000000000000000000000000000010000b1'
# 2019-12-04 13:09:38,721 - DEBUG - PAI.paradox.connections.serial_connection - SER -> PAI b'520000605a6f7a8d20737a6f62612020202020204c967063738e689d7a2020202020202096'
# 2019-12-04 13:09:38,729 - DEBUG - PAI.paradox.connections.serial_connection - PAI -> SER b'500000700000000000000000000000000000000000000000000000000000000000010000c1'
# 2019-12-04 13:09:38,816 - DEBUG - PAI.paradox.connections.serial_connection - SER -> PAI b'520000704c967063738e689d7a20202020202020537a61626f749d7a7320202020202020b4'
# 2019-12-04 13:09:38,825 - DEBUG - PAI.paradox.connections.serial_connection - PAI -> SER b'500000800000000000000000000000000000000000000000000000000000000000010000d1'
# 2019-12-04 13:09:38,912 - DEBUG - PAI.paradox.connections.serial_connection - SER -> PAI b'52000080537a61626f749d7a7320202020202020456c04749672206e7969749d7320202034'
# 2019-12-04 13:09:38,920 - DEBUG - PAI.paradox.connections.serial_connection - PAI -> SER b'500000900000000000000000000000000000000000000000000000000000000000010000e1'
# 2019-12-04 13:09:39,007 - DEBUG - PAI.paradox.connections.serial_connection - SER -> PAI b'52000090456c04749672206e7969749d732020204e617070616c69206e7969749d73202060'
# 2019-12-04 13:09:39,016 - DEBUG - PAI.paradox.connections.serial_connection - PAI -> SER b'500000a00000000000000000000000000000000000000000000000000000000000010000f1'
# 2019-12-04 13:09:39,103 - DEBUG - PAI.paradox.connections.serial_connection - SER -> PAI b'520000a04e617070616c69206e7969749d73202042656a9d72617469206e7969749d73205d'
# 2019-12-04 13:09:39,111 - DEBUG - PAI.paradox.connections.serial_connection - PAI -> SER b'500000b0000000000000000000000000000000000000000000000000000000000001000001'
# 2019-12-04 13:09:39,199 - DEBUG - PAI.paradox.connections.serial_connection - SER -> PAI b'520000b042656a9d72617469206e7969749d73204d616d61206e7969749d73202020202084'
# 2019-12-04 13:09:39,207 - DEBUG - PAI.paradox.connections.serial_connection - PAI -> SER b'500000c0000000000000000000000000000000000000000000000000000000000001000011'
# 2019-12-04 13:09:39,310 - DEBUG - PAI.paradox.connections.serial_connection - SER -> PAI b'520000c04d616d61206e7969749d73202020202047796572656b206e7969749d73202020dd'
# 2019-12-04 13:09:39,318 - DEBUG - PAI.paradox.connections.serial_connection - PAI -> SER b'500000d0000000000000000000000000000000000000000000000000000000000001000021'
# 2019-12-04 13:09:39,406 - DEBUG - PAI.paradox.connections.serial_connection - SER -> PAI b'520000d047796572656b206e7969749d73202020466f6c796f738d20202020202020202006'
# 2019-12-04 13:09:39,413 - DEBUG - PAI.paradox.connections.serial_connection - PAI -> SER b'500000e0000000000000000000000000000000000000000000000000000000000001000031'
# 2019-12-04 13:09:39,501 - DEBUG - PAI.paradox.connections.serial_connection - SER -> PAI b'520000e0466f6c796f738d2020202020202020205a6f6e652031352020202020202020209d'
# 2019-12-04 13:09:39,509 - DEBUG - PAI.paradox.connections.serial_connection - PAI -> SER b'500000f0000000000000000000000000000000000000000000000000000000000001000041'
# 2019-12-04 13:09:39,597 - DEBUG - PAI.paradox.connections.serial_connection - SER -> PAI b'520000f05a6f6e652031352020202020202020205a6f6e65203136202020202020202020c7'
# 2019-12-04 13:09:39,605 - DEBUG - PAI.paradox.connections.serial_connection - PAI -> SER b'50000100000000000000000000000000000000000000000000000000000000000001000052'
# 2019-12-04 13:09:39,693 - DEBUG - PAI.paradox.connections.serial_connection - SER -> PAI b'520001005a6f6e652031362020202020202020205a6f6e65203137202020202020202020da'
# 2019-12-04 13:09:39,697 - INFO - PAI.paradox.hardware.panel - Zone: Elt–r, Nappali, Konyha, Mama szoba, Gyerek szoba, Zoz szoba, L–pcsŽhz, Szabotzs, Elt–r nyits, Nappali nyits, Bejrati nyits, Mama nyits, Gyerek nyits, Folyos, Zone 15, Zone 16

from paradox.hardware.spectra_magellan.panel import Panel
from paradox.hardware.spectra_magellan.parsers import ReadEEPROMResponse

async def send_wait(message_type, args, reply_expected):
out_raw = message_type.build(dict(fields=dict(value=args)))
in_raw = binascii.unhexlify(request_reply_map[binascii.hexlify(out_raw)])

return ReadEEPROMResponse.parse(in_raw)

@pytest.mark.asyncio
async def test_label_loading(mocker):
config = mocker.patch("paradox.hardware.panel.cfg")
config.LIMITS = {
"zone": [1],
"pgm": [],
"partition": [],
"user": [],
"bus-module": [],
"repeater": [],
"keypad": [],
"site": [],
"siren": []
}
config.LABEL_ENCODING = 'latin2'

core = mocker.MagicMock()
core.send_wait = send_wait
panel = Panel(core=core, product_id=0)

print(await panel.load_labels())

0 comments on commit 28becd2

Please sign in to comment.