def send_initial_status(alarm): sendMessage("labels_loaded", data=dict( partition={ 1: dict( id=1, label='Partition 1', key='Partition_1' ) } )) sendMessage("status_update", status=dict( partition={ 1: dict( arm=False, alarm_in_memory=False, audible_alarm=False, exit_delay=False, was_in_alarm=False ) } )) alarm.storage.update_container_object.assert_any_call('partition', 'Partition_1', { 'current_state': 'disarmed', 'target_state': 'disarmed' })
async def send_initial_status(alarm): sendMessage( "labels_loaded", data=dict( partition={ 1: dict(id=1, label="Partition 1", key="Partition_1"), 2: dict(id=1, label="Partition 2", key="Partition_2"), } ), ) sendMessage( "status_update", status=dict( partition={ 1: dict( arm=False, alarm_in_memory=False, audible_alarm=False, exit_delay=False, was_in_alarm=False, ), 2: dict( arm=False, alarm_in_memory=False, audible_alarm=False, exit_delay=False, was_in_alarm=False, ), } ), ) await asyncio.sleep(0.01)
def handle_message(self, timestamp, source, message): """ Handle GSM message. It should be a command """ self.logger.debug("Received Message {} {} {}".format( timestamp, source, message)) if self.alarm is None: return if source in cfg.GSM_CONTACTS: ret = self.send_command(message) if ret: self.logger.info("ACCEPTED: {}".format(message)) self.send_sms(source, "ACCEPTED: {}".format(message)) message = "ACCEPTED: {}: {}".format(source, message) else: self.logger.warning("REJECTED: {}".format(message)) self.send_sms(source, "REJECTED: {}".format(message)) message = "REJECTED: {}: {}".format(source, message) else: self.logger.warning("REJECTED: {}".format(message)) message = "REJECTED: {}: {}".format(source, message) ps.sendMessage("notifications", message=dict(source=self.name, message=message, level=logging.INFO))
async def test_hass(): interface = HomeAssistantMQTTInterface() interface.mqtt = MagicMock() interface.start() try: await asyncio.sleep(0.1) # TODO: Bad way to wait for a start sendMessage( "labels_loaded", data=dict(partition={ 1: dict(id=1, label='Partition 1', key='Partition_1') })) sendMessage("status_update", status=dict(partition={1: dict(arm=False)})) interface.mqtt.publish.assert_called_with( 'homeassistant/alarm_control_panel/pai/Partition_1/config', json.dumps( dict(name='Partition 1', unique_id="pai_partition_Partition_1", command_topic='paradox/control/partitions/Partition_1', state_topic= 'paradox/states/partitions/Partition_1/current_state', availability_topic='paradox/interface/MQTTInterface', device=dict(), payload_disarm="disarm", payload_arm_home="arm_stay", payload_arm_away="arm", payload_arm_night="arm_sleep")), 0, True) finally: interface.stop()
def run(self): self.logger.info("Starting GSM Interface") ps.subscribe(self.handle_panel_event, "events") ps.subscribe(self.handle_notify, "notifications") try: while not self.stop_running.isSet(): time.sleep(1) while not self.connected() and self.stop_running.isSet(): logging.warning("Could not connect to modem") time.sleep(10) try: data = self.port.read(200) if len(data) > 0: tokens = data.decode('latin-1').strip().split('"') for i in range(len(tokens)): tokens[i] = tokens[i].strip() if len(tokens) > 0: if tokens[0] == '+CMT:': source = tokens[1] timestamp = datetime.datetime.strptime( tokens[5].split('+')[0], '%y/%m/%d,%H:%M:%S') message = tokens[6] self.handle_message(timestamp, source, message) elif tokens[0].startswith('+CUSD:'): ps.sendMessage("notifications", message=dict( source=self.name, message=tokens[1], level=logging.INFO)) else: self.run_loop() except Exception: self.modem_connected = False # self.logger.exception("") except (KeyboardInterrupt, SystemExit): self.logger.debug("GSM loop stopping") return except Exception: self.logger.exception("GSM loop") return True
def test_partitions(mocker): alarm = Paradox() alarm.panel = mocker.MagicMock() alarm.panel.property_map = { "arm": dict(level=EventLevel.INFO, message={"True": "{Type} {label} is armed", "False": "{Type} {label} is disarmed"}), } event = mocker.MagicMock() mocker.patch("paradox.lib.ps.sendChange") mocker.patch("paradox.lib.ps.sendEvent") mocker.patch('paradox.event.ChangeEvent', return_value=event) ps.sendMessage("labels_loaded", data=dict( partition={ 1: dict( id=1, label='Partition 1', key='Partition_1' ) } )) assert isinstance(alarm.panel, mocker.MagicMock) alarm.storage.update_container_object("partition", "Partition_1", dict(arm=True)) ps.sendChange.assert_called_once_with(Change('partition', 'Partition_1', 'arm', True, initial=True)) ps.sendChange.reset_mock() assert isinstance(alarm.panel, mocker.MagicMock) ps.sendMessage("status_update", status=dict( partition={ 1: dict( arm=False ) } )) assert isinstance(alarm.panel, mocker.MagicMock) ps.sendChange.assert_any_call(Change('partition', 'Partition_1', 'current_state', 'disarmed', initial=True)) ps.sendChange.assert_any_call(Change('partition', 'Partition_1', 'target_state', 'disarmed', initial=True)) ps.sendChange.assert_any_call(Change('partition', 'Partition_1', 'arm', False, initial=False)) assert ps.sendChange.call_count == 3 assert ps.sendEvent.call_count == 0
def _process_status(raw_status: Container) -> None: status = convert_raw_status(raw_status) for limit_key, limit_arr in cfg.LIMITS.items(): if limit_key not in status: continue status[limit_key].filter(limit_arr) # # TODO: throttle power update messages # if time.time() - self.last_power_update >= cfg.POWER_UPDATE_INTERVAL: # force = PublishPropertyChange.YES if cfg.PUSH_POWER_UPDATE_WITHOUT_CHANGE else PublishPropertyChange.NO if cfg.LOGGING_DUMP_STATUS: logger.debug("properties: %s", status) ps.sendMessage('status_update', status=status)
def test_current_state_armed_away(mocker): alarm = Paradox(None) mocker.spy(alarm.storage, 'update_container_object') alarm.panel = MagicMock() send_initial_status(alarm) sendMessage("status_update", status=dict( partition={ 1: dict( arm=True ) } )) alarm.storage.update_container_object.assert_any_call('partition', 'Partition_1', { 'current_state': 'armed_away', 'target_state': 'armed_away' })
def test_on_labels_load(): alarm = Paradox(None) sendMessage("labels_loaded", data=dict( partition={ 1: dict( id=1, label='Partition 1', key='Partition_1' ) } )) assert isinstance(alarm.storage.get_container('partition'), ElementTypeContainer) assert alarm.storage.get_container_object('partition', 'Partition_1') == dict( id=1, label='Partition 1', key='Partition_1' )
async def update_labels(self): logger.info("Updating Labels from Panel") for elem_type in self.mem_map['elements']: elem_def = self.mem_map['elements'][elem_type] addresses = list(chain.from_iterable(elem_def['addresses'])) limits = cfg.LIMITS.get(elem_type) if limits is not None: addresses = [ a for i, a in enumerate(addresses) if i + 1 in limits ] await self.load_labels(self.core.data[elem_type], addresses, label_offset=elem_def['label_offset']) logger.info("{}: {}".format( elem_type.title(), ', '.join( [v["label"] for v in self.core.data[elem_type].values()]))) ps.sendMessage('labels_loaded', data=self.core.data)
def test_partitions_callable_prop(mocker): alarm = Paradox() alarm.panel = mocker.MagicMock() alarm.panel.property_map = { "arm": dict(level=EventLevel.INFO, message={"True": "{Type} {label} is armed", "False": "{Type} {label} is disarmed"}), } event = mocker.MagicMock() mocker.patch.object(ps, "sendChange") mocker.patch.object(ps, "sendEvent") mocker.patch('paradox.event.ChangeEvent', return_value=event) ps.sendMessage("labels_loaded", data=dict( partition={ 1: dict( id=1, label='Partition 1', key='Partition_1' ) } )) ps.sendMessage("status_update", status=dict( partition={ 1: dict( arm=False ) } )) ps.sendChange.assert_any_call(Change('partition', 'Partition_1', 'arm', False, initial=True)) ps.sendChange.reset_mock() alarm.storage.update_container_object("partition", "Partition_1", dict(arm=lambda old: not old)) ps.sendChange.assert_any_call(Change('partition', 'Partition_1', 'arm', True)) ps.sendEvent.call_count = 0
async def full_connect(self) -> bool: try: if not await self.connect(): return False logger.info("Loading data from panel memory") await self.panel.load_memory() logger.info("Running") self.run_state = RunState.RUN self.request_status_refresh() # Trigger status update ps.sendMessage("connected") return True except asyncio.TimeoutError: logger.error( "Timeout while connecting to panel. Is an other connection active?" ) except ConnectionError as e: logger.error("Failed to connect: %s" % str(e)) self.run_state = RunState.ERROR return False
async def connect(self): if self._connection: self.disconnect() # socket needs to be also closed self.panel = None self.run_state = RunState.INIT logger.info("Connecting to interface") if not await self.connection.connect(): self.run_state = RunState.ERROR logger.error('Failed to connect to interface') return False logger.info("Connecting to panel") if not self.panel: self.panel = create_panel(self) self.connection.variable_message_length(self.panel.variable_message_length) try: logger.info("Initiating communication") initiate_reply = await self.send_wait( self.panel.get_message('InitiateCommunication'), None, reply_expected=0x07 ) if initiate_reply: model = initiate_reply.fields.value.label.strip(b'\0 ').decode(cfg.LABEL_ENCODING) firmware_version = "{}.{} build {}".format( initiate_reply.fields.value.application.version, initiate_reply.fields.value.application.revision, initiate_reply.fields.value.application.build ) serial_number = hexlify(initiate_reply.fields.value.serial_number).decode() logger.info("Found Panel {} version {}".format(model, firmware_version)) else: raise ConnectionError("Panel did not replied to InitiateCommunication") logger.info("Starting communication") reply = await self.send_wait(self.panel.get_message('StartCommunication'), args=dict(source_id=0x02), reply_expected=0x00) if reply is None: raise ConnectionError("Panel did not replied to StartCommunication") if reply.fields.value.product_id is not None: self.panel = create_panel(self, reply.fields.value.product_id) # Now we know what panel it is. Let's # recreate panel object. ps.sendMessage( 'panel_detected', panel=DetectedPanel( product_id=reply.fields.value.product_id, model=model, firmware_version=firmware_version, serial_number=serial_number ) ) else: raise PanelNotDetected('Failed to detect panel') result = await self.panel.initialize_communication(reply, cfg.PASSWORD) if not result: raise ConnectionError("Failed to initialize communication") if cfg.SYNC_TIME: await self.sync_time() if cfg.DEVELOPMENT_DUMP_MEMORY: if hasattr(self.panel, 'dump_memory') and callable(self.panel.dump_memory): logger.warning("Requested memory dump. Dumping...") await self.panel.dump_memory() logger.warning("Memory dump completed. Exiting pai.") raise SystemExit() else: logger.warning("Requested memory dump, but current panel type does not support it yet.") logger.info("Loading definitions") definitions = await self.panel.load_definitions() ps.sendMessage('definitions_loaded', data=definitions) logger.info("Loading labels") labels = await self.panel.load_labels() ps.sendMessage('labels_loaded', data=labels) logger.info("Connection OK") self.run_state = RunState.RUN self.request_status_refresh() # Trigger status update ps.sendMessage('connected') return True except asyncio.TimeoutError as e: logger.error("Timeout while connecting to panel: %s" % str(e)) except ConnectionError as e: logger.error("Failed to connect: %s" % str(e)) self.run_state = RunState.ERROR return False
def run_state(self, value: RunState): self._run_state = value ps.sendMessage("run-state", state=value)
def handle_message(self, client, userdata, message): """Handle message received from the MQTT broker""" self.logger.info("message topic={}, payload={}".format( message.topic, str(message.payload.decode("utf-8")))) if message.retain: return if self.alarm is None: self.logger.warning("No alarm. Ignoring command") return topic = message.topic.split(cfg.MQTT_BASE_TOPIC)[1] topics = topic.split("/") if len(topics) < 3: self.logger.error("Invalid topic in mqtt message: {}".format( message.topic)) return if topics[1] == cfg.MQTT_NOTIFICATIONS_TOPIC: if topics[2].upper() == "CRITICAL": level = logging.CRITICAL elif topics[2].upper() == "INFO": level = logging.INFO else: self.logger.error("Invalid notification level: {}".format( topics[2])) return payload = message.payload.decode("latin").strip() ps.sendMessage("notifications", message=dict(source=self.name, payload=payload, level=level)) return if topics[1] != cfg.MQTT_CONTROL_TOPIC: self.logger.error("Invalid subtopic in mqtt message: {}".format( message.topic)) return command = message.payload.decode("latin").strip() element = topics[3] # Process a Zone Command if topics[2] == cfg.MQTT_ZONE_TOPIC: if not self.alarm.control_zone(element, command): self.logger.warning("Zone command refused: {}={}".format( element, command)) # Process a Partition Command elif topics[2] == cfg.MQTT_PARTITION_TOPIC: if command in cfg.MQTT_PARTITION_HOMEBRIDGE_COMMANDS and cfg.MQTT_HOMEBRIDGE_ENABLE: command = cfg.MQTT_PARTITION_HOMEBRIDGE_COMMANDS[command] elif command in cfg.MQTT_PARTITION_HOMEASSISTANT_COMMANDS and cfg.MQTT_HOMEASSISTANT_ENABLE: command = cfg.MQTT_PARTITION_HOMEASSISTANT_COMMANDS[command] if command.startswith('code_toggle-'): tokens = command.split('-') if len(tokens) < 2: return if tokens[1] not in cfg.MQTT_TOGGLE_CODES: self.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 desarm 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: self.logger.debug("Element {} not found".format(element)) return ps.sendMessage('notifications', message=dict( source="mqtt", message="Command by {}: {}".format( cfg.MQTT_TOGGLE_CODES[tokens[1]], command), level=logging.INFO)) self.logger.debug("Partition command: {} = {}".format( element, command)) if not self.alarm.control_partition(element, command): self.logger.warning("Partition command refused: {}={}".format( element, command)) # Process an Output Command elif topics[2] == cfg.MQTT_OUTPUT_TOPIC: self.logger.debug("Output command: {} = {}".format( element, command)) if not self.alarm.control_output(element, command): self.logger.warning("Output command refused: {}={}".format( element, command)) else: self.logger.error("Invalid control property {}".format(topics[2]))
async def connect(self) -> bool: if self._connection: await self.disconnect() # socket needs to be also closed self.panel = None self.run_state = RunState.INIT logger.info("Connecting to interface") if not await self.connection.connect(): self.run_state = RunState.ERROR logger.error("Failed to connect to interface") return False logger.info("Connecting to Panel") if not self.panel: self.panel = create_panel(self) self.connection.variable_message_length( self.panel.variable_message_length) try: initiate_reply = await self.send_wait( self.panel.get_message("InitiateCommunication"), None, reply_expected=0x07, ) if initiate_reply: model = initiate_reply.fields.value.label.strip(b"\0 ").decode( cfg.LABEL_ENCODING) firmware_version = "{}.{} build {}".format( initiate_reply.fields.value.application.version, initiate_reply.fields.value.application.revision, initiate_reply.fields.value.application.build, ) serial_number = hexlify( initiate_reply.fields.value.serial_number).decode() logger.info("Panel Identified {} version {}".format( model, firmware_version)) else: raise ConnectionError( "Panel did not replied to InitiateCommunication") logger.info("Initiating panel connection") reply = await self.send_wait( self.panel.get_message("StartCommunication"), args=dict(source_id=0x02), reply_expected=0x00, ) if reply is None: raise ConnectionError( "Panel did not replied to StartCommunication") if reply.fields.value.product_id is not None: self.panel = create_panel( self, reply) # Now we know what panel it is. Let's # recreate panel object. ps.sendMessage( "panel_detected", panel=DetectedPanel( product_id=reply.fields.value.product_id, model=model, firmware_version=firmware_version, serial_number=serial_number, ), ) else: raise PanelNotDetected("Failed to detect panel") result = await self.panel.initialize_communication(cfg.PASSWORD) if not result: raise ConnectionError("Failed to initialize communication") self.run_state = RunState.CONNECTED logger.info("Connection OK") return True except asyncio.TimeoutError: logger.error( "Timeout while connecting to panel. Is an other connection active?" ) except ConnectionError as e: logger.error("Failed to connect: %s" % str(e)) self.run_state = RunState.ERROR return False
async def test_hass(mocker): mocker.patch("paradox.lib.utils.main_thread_loop", asyncio.get_event_loop()) mocker.patch.multiple(cfg, MQTT_HOMEASSISTANT_AUTODISCOVERY_ENABLE=True) con = mocker.patch("paradox.interfaces.mqtt.core.MQTTConnection") con.get_instance.return_value.availability_topic = "paradox/interface/availability" con.get_instance.return_value.pai_status_topic = "paradox/interface/pai_status" alarm = mocker.MagicMock() alarm.panel = create_evo192_panel(alarm) interface = HomeAssistantMQTTInterface(alarm) interface.start() interface.on_connect(None, None, None, None) assert (interface.connected_future.done() and interface.connected_future.result() is True) try: await asyncio.sleep(0.01) # TODO: Bad way to wait for a start sendMessage( "panel_detected", panel=DetectedPanel(ProductIdEnum.parse(b"\x05"), "EVO192", "6.80 build 5", "aabbccdd"), ) sendMessage( "labels_loaded", data=dict(partition={ 1: dict(id=1, label="Partition 1", key="Partition_1") }), ) sendMessage("status_update", status=dict(partition={1: dict(arm=False)})) await asyncio.sleep(0.01) interface.mqtt.publish.assert_any_call( "homeassistant/sensor/aabbccdd/pai_status/config", json.dumps({ "name": "Run status", "unique_id": "aabbccdd_partition_pai_status", "state_topic": "paradox/interface/pai_status", "device": { "manufacturer": "Paradox", "model": "EVO192", "identifiers": ["Paradox", "EVO192", "aabbccdd"], "name": "EVO192", "sw_version": "6.80 build 5", }, }), 0, True, ) interface.mqtt.publish.assert_any_call( "homeassistant/alarm_control_panel/aabbccdd/Partition_1/config", json.dumps({ "name": "Partition 1", "unique_id": "aabbccdd_partition_Partition_1", "command_topic": "paradox/control/partitions/Partition_1", "state_topic": "paradox/states/partitions/Partition_1/current_state", "availability_topic": "paradox/interface/availability", "device": { "manufacturer": "Paradox", "model": "EVO192", "identifiers": ["Paradox", "EVO192", "aabbccdd"], "name": "EVO192", "sw_version": "6.80 build 5", }, "payload_disarm": "disarm", "payload_arm_home": "arm_stay", "payload_arm_away": "arm", "payload_arm_night": "arm_sleep", }), 0, True, ) finally: interface.stop() interface.join() assert not interface.is_alive()
async def connect_async(self): self.disconnect() # socket needs to be also closed logger.info("Connecting to interface") if not await self.connection.connect(): logger.error('Failed to connect to interface') self.run = STATE_STOP return False self.run = STATE_STOP self.connection.timeout(0.5) logger.info("Connecting to panel") # Reset all states self.reset() if not self.panel: self.panel = create_panel(self) self.connection.variable_message_length(self.panel.variable_message_length) try: logger.info("Initiating communication") reply = await self.send_wait(self.panel.get_message('InitiateCommunication'), None, reply_expected=0x07) if reply: logger.info("Found Panel {} version {}.{} build {}".format( (reply.fields.value.label.strip(b'\0 ').decode(cfg.LABEL_ENCODING)), reply.fields.value.application.version, reply.fields.value.application.revision, reply.fields.value.application.build)) else: raise ConnectionError("Panel did not replied to InitiateCommunication") logger.info("Starting communication") reply = await self.send_wait(self.panel.get_message('StartCommunication'), args=dict(source_id=0x02), reply_expected=0x00) if reply is None: raise ConnectionError("Panel did not replied to StartCommunication") if reply.fields.value.product_id is not None: self.panel = create_panel(self, reply.fields.value.product_id) # Now we know what panel it is. Let's ps.sendMessage('panel_detected', product_id=reply.fields.value.product_id) # recreate panel object. result = await self.panel.initialize_communication(reply, cfg.PASSWORD) if not result: raise ConnectionError("Failed to initialize communication") # Now we need to start async message reading worker self.run = STATE_RUN self.receive_worker_task = self.work_loop.create_task(self.receive_worker()) if cfg.SYNC_TIME: await self.sync_time() if cfg.DEVELOPMENT_DUMP_MEMORY: if hasattr(self.panel, 'dump_memory') and callable(self.panel.dump_memory): logger.warn("Requested memory dump. Dumping...") await self.panel.dump_memory() logger.warn("Memory dump completed. Exiting pai.") raise SystemExit() else: logger.warn("Requested memory dump, but current panel type does not support it yet.") await self.panel.update_labels() logger.info("Connection OK") self.loop_wait = False ps.sendMessage('connected') return True except ConnectionError as e: logger.error("Failed to connect: %s" % str(e)) except Exception: logger.exception("Connect error") self.run = STATE_STOP return False