From b8633d19c2142ccfc01b3509851fd2ac84b33880 Mon Sep 17 00:00:00 2001 From: crazeeeyez Date: Tue, 2 Mar 2021 06:19:31 -0800 Subject: [PATCH 1/7] separated the qolsys listener is running in a separate thread. You can independently send requests to it. Not a lot of error management, retries, or resiliency in general. Still some janky code. --- demo.py | 20 ++++---- qolsys_client/mqtt_client.py | 27 +++++++++++ qolsys_client/qolsys_socket.py | 87 ++++++++++++++++++++++++++++++++++ qolsys_client/status_nosock.py | 59 +++++++++++++++++++++++ 4 files changed, 183 insertions(+), 10 deletions(-) create mode 100644 qolsys_client/mqtt_client.py create mode 100644 qolsys_client/qolsys_socket.py create mode 100644 qolsys_client/status_nosock.py diff --git a/demo.py b/demo.py index 6f7aa83..cd139c6 100644 --- a/demo.py +++ b/demo.py @@ -1,9 +1,9 @@ from qolsys_client import arm from qolsys_client import status -qolsysPanel = "192.168.0.20" +qolsysPanel = "192.168.10.34" qolsysPort = 12345 -qolsysToken = "abc123" +qolsysToken = "shw9s8" qolsysTimeout = 20 # Status @@ -11,13 +11,13 @@ print (result) # Arm Away -result = arm.qolsysArm(qolsysPanel, qolsysPort, qolsysToken, qolsysTimeout, 0, "away") -print (result) +# result = arm.qolsysArm(qolsysPanel, qolsysPort, qolsysToken, qolsysTimeout, 0, "away") +# print (result) -# Arm Stay -result = arm.qolsysArm(qolsysPanel, qolsysPort, qolsysToken, qolsysTimeout, 0, "stay") -print (result) +# # Arm Stay +# result = arm.qolsysArm(qolsysPanel, qolsysPort, qolsysToken, qolsysTimeout, 0, "stay") +# print (result) -# Disarm -result = arm.qolsysArm(qolsysPanel, qolsysPort, qolsysToken, qolsysTimeout, 0, "disarm") -print (result) +# # Disarm +# result = arm.qolsysArm(qolsysPanel, qolsysPort, qolsysToken, qolsysTimeout, 0, "disarm") +# print (result) diff --git a/qolsys_client/mqtt_client.py b/qolsys_client/mqtt_client.py new file mode 100644 index 0000000..78bdb27 --- /dev/null +++ b/qolsys_client/mqtt_client.py @@ -0,0 +1,27 @@ +import paho.mqtt.client as pmqtt +import json +import time + +class mqtt: + + def __init__(self, broker: str, port=1883): + self.client = "" + self.broker = broker + self.port = port + self.connect() + + def connect(self): + self.client = pmqtt.Client() + self.client.connect(host=self.broker, port=self.port) + + def publish(self, topic:str, message:str): + if topic == "" or message == "": + raise Exception("Topic and Message required") + # if json.dumps(message): + # message = json.dumps((message).encode()) + # else: + # message = message.encode() + published = self.client.publish(topic, message) + while not published.is_published(): + time.sleep(0.5) + print("published:", published.rc) \ No newline at end of file diff --git a/qolsys_client/qolsys_socket.py b/qolsys_client/qolsys_socket.py new file mode 100644 index 0000000..ec2b5ea --- /dev/null +++ b/qolsys_client/qolsys_socket.py @@ -0,0 +1,87 @@ +import json +import socket +import ssl +import sys +import time +import asyncio +import threading + +class qolsys: + ################################################################################ + # Code + + def __init__(self): + self.sock = socket.socket + self.wrappedSocket = ssl.SSLContext.wrap_socket + + def create_socket(self, hostname, port, token, cb: callable, timeout=60): + try: + self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.sock.settimeout(timeout) + except socket.error: + print('Could not create a socket') + raise + + # Wrap SSL + print("wrapping socket") + self.wrappedSocket = ssl.wrap_socket(self.sock, cert_reqs=ssl.CERT_NONE, ssl_version=ssl.PROTOCOL_TLSv1_2) + # print("wrappedSocket: ", self.wrappedSocket) + # Connect to server + try: + #The stupid Qolsys panel requires blocking + # wrappedSocket.setblocking(False) + print("connecting to socket") + self.wrappedSocket.connect((hostname, port)) + print("Connected wrappedSocket:", self.wrappedSocket) + print("Starting listener thread") + #print("timeout:", self.sock.gettimeout()) + + thread = threading.Thread(target=self.listen, args=([cb])) + thread.start() + + print("started listener") + return True + except socket.error: + print(sys.exc_info()[0]) + return False + + def send_to_socket(self, message: json): + + self.wrappedSocket.send(b'\n') + self.wrappedSocket.send((json.dumps(message)).encode()) + + return True + + def listen(self, cb: callable): + listening = True + print("starting listen") + data = "" + err = "" + while not (self.wrappedSocket._connected): + print("not connected yet") + print(self.wrappedSocket._connected) + time.sleep(1) + try: + while self.wrappedSocket._connected: + data = self.wrappedSocket.recv(4096).decode() + if is_json(data): + try: + cb(data) + except: + print("Error calling callback:", cb, sys.exc_info()) + #print(data) + else: + print("non json data:", data) + except socket.timeout: + print ("socket timeout") + except: + print("listen failed/stopped:", sys.exc_info()) + + +def is_json(myjson): + try: + json_object = json.loads(myjson) + if json_object: return True + except ValueError as e: + #print("not json:", myjson) + return False \ No newline at end of file diff --git a/qolsys_client/status_nosock.py b/qolsys_client/status_nosock.py new file mode 100644 index 0000000..5e42927 --- /dev/null +++ b/qolsys_client/status_nosock.py @@ -0,0 +1,59 @@ +import sys +import time +import qolsys_socket +import asyncio +import threading +import mqtt_client +import json + +################################################################################ +# Code + +token = "shw9s8" + +def qolsys_status(qolsys, token): + statusString = { + "nonce": "qolsys", + "action": "INFO", + "info_type": "SUMMARY", + "version": 0, + "source": "C4", + "token": token, + } + + try: + qolsys.send_to_socket(statusString) + except: + print('Could not send to socket') + +def data_received(data): + # print(data) + topic = "" + jdata = json.loads(data) + event_type = jdata["event"] + if event_type == "INFO": + topic = "qolsys/info" + if event_type == "ZONE_EVENT" or event_type == "ZONE_UDPATE": + topic = "qolsys/zone_event" + mq = mqtt_client.mqtt("192.168.10.4") + mq.publish(topic, data) + print(mq) + +def main(): + + qolsys = qolsys_socket.qolsys() + qolsys.create_socket(hostname="192.168.10.34", port=12345, token=token, cb=data_received, timeout=4) + + print("main: sock created") + print("main: creating mqtt client") + + + print("doing the info check") + qolsys_status(qolsys, token) + time.sleep(2) + qolsys_status(qolsys, token) + time.sleep(2.9) + qolsys_status(qolsys, token) + +if __name__ == "__main__": + main() \ No newline at end of file From ec347de5ae774ef2d92c0a435f7bb9ce577cb80a Mon Sep 17 00:00:00 2001 From: crazeeeyez Date: Tue, 2 Mar 2021 19:06:19 -0800 Subject: [PATCH 2/7] Improved logging --- qolsys_client/qolsys_socket.py | 36 +++++++++--------- qolsys_client/status.py | 54 --------------------------- qolsys_client/status_nosock.py | 68 +++++++++++++++++++++++----------- 3 files changed, 66 insertions(+), 92 deletions(-) delete mode 100644 qolsys_client/status.py diff --git a/qolsys_client/qolsys_socket.py b/qolsys_client/qolsys_socket.py index ec2b5ea..ee7f0eb 100644 --- a/qolsys_client/qolsys_socket.py +++ b/qolsys_client/qolsys_socket.py @@ -5,6 +5,7 @@ import time import asyncio import threading +import logging class qolsys: ################################################################################ @@ -19,30 +20,29 @@ def create_socket(self, hostname, port, token, cb: callable, timeout=60): self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.settimeout(timeout) except socket.error: - print('Could not create a socket') + logging.error('Could not create a socket') raise # Wrap SSL - print("wrapping socket") + logging.debug("wrapping socket") self.wrappedSocket = ssl.wrap_socket(self.sock, cert_reqs=ssl.CERT_NONE, ssl_version=ssl.PROTOCOL_TLSv1_2) - # print("wrappedSocket: ", self.wrappedSocket) + # Connect to server try: #The stupid Qolsys panel requires blocking # wrappedSocket.setblocking(False) - print("connecting to socket") + logging.debug("connecting to socket") self.wrappedSocket.connect((hostname, port)) - print("Connected wrappedSocket:", self.wrappedSocket) - print("Starting listener thread") - #print("timeout:", self.sock.gettimeout()) + logging.debug(("Connected wrappedSocket:", self.wrappedSocket)) + logging.debug("Starting listener thread") thread = threading.Thread(target=self.listen, args=([cb])) thread.start() + logging.debug("started listener") - print("started listener") return True except socket.error: - print(sys.exc_info()[0]) + logging.error(("Error creating or connecting to socket", sys.exc_info())) return False def send_to_socket(self, message: json): @@ -54,12 +54,12 @@ def send_to_socket(self, message: json): def listen(self, cb: callable): listening = True - print("starting listen") + logging.debug("starting listen") data = "" err = "" while not (self.wrappedSocket._connected): - print("not connected yet") - print(self.wrappedSocket._connected) + logging.warning("not connected yet") + logging.debug(self.wrappedSocket._connected) time.sleep(1) try: while self.wrappedSocket._connected: @@ -68,14 +68,15 @@ def listen(self, cb: callable): try: cb(data) except: - print("Error calling callback:", cb, sys.exc_info()) + logging.error(("Error calling callback:", cb, sys.exc_info())) #print(data) else: - print("non json data:", data) + if data != 'ACK\n': + logging.warning(("non json data:", data)) except socket.timeout: - print ("socket timeout") + logging.debug("socket timeout") except: - print("listen failed/stopped:", sys.exc_info()) + logging.error(("listen failed/stopped:", sys.exc_info())) def is_json(myjson): @@ -83,5 +84,6 @@ def is_json(myjson): json_object = json.loads(myjson) if json_object: return True except ValueError as e: - #print("not json:", myjson) + if myjson != 'ACK\n': + logging.debug(("not json:", myjson)) return False \ No newline at end of file diff --git a/qolsys_client/status.py b/qolsys_client/status.py deleted file mode 100644 index 565ecaf..0000000 --- a/qolsys_client/status.py +++ /dev/null @@ -1,54 +0,0 @@ -import json -import socket -import ssl -import sys -import time - -################################################################################ -# Code -def qolsysStatus(hostname, port, token, timeout): - statusString = { - "nonce": "qolsys", - "action": "INFO", - "info_type": "SUMMARY", - "version": 0, - "source": "C4", - "token": token, - } - - try: - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.settimeout(timeout) - except socket.error: - print('Could not create a socket') - sys.exit() - - # Wrap SSL - wrappedSocket = ssl.wrap_socket(sock, cert_reqs=ssl.CERT_NONE, ssl_version=ssl.PROTOCOL_TLSv1_2) - - # Connect to server - try: - wrappedSocket.connect((hostname, port)) - except socket.error: - print('Could not connect to server') - sys.exit() - - # Send message and print reply - online = True - while online: - wrappedSocket.send(b'\n') - wrappedSocket.send((json.dumps(statusString)).encode()) - while True: - response = wrappedSocket.recv(4096).decode() - if is_json(response): - online = False - return(response) - break # stop receiving - time.sleep(timeout / 4) - -def is_json(myjson): - try: - json_object = json.loads(myjson) - except ValueError as e: - return False - return True diff --git a/qolsys_client/status_nosock.py b/qolsys_client/status_nosock.py index 5e42927..83a7175 100644 --- a/qolsys_client/status_nosock.py +++ b/qolsys_client/status_nosock.py @@ -1,16 +1,14 @@ import sys import time import qolsys_socket -import asyncio import threading import mqtt_client import json +import logging ################################################################################ # Code -token = "shw9s8" - def qolsys_status(qolsys, token): statusString = { "nonce": "qolsys", @@ -24,36 +22,64 @@ def qolsys_status(qolsys, token): try: qolsys.send_to_socket(statusString) except: - print('Could not send to socket') + logging.error('Could not send to socket') + -def data_received(data): +def data_received(data:dict): + ''' This is where any json data coming from the qolsys panel will be sent. In this case I have the data being published to a mqtt topic, but you can do what you want. + + Parameters: + data: json object containing the output from the qolsys panel''' # print(data) - topic = "" + topic = "qolsys" jdata = json.loads(data) event_type = jdata["event"] if event_type == "INFO": - topic = "qolsys/info" - if event_type == "ZONE_EVENT" or event_type == "ZONE_UDPATE": - topic = "qolsys/zone_event" + topic += "/info" + if event_type == "ZONE_EVENT": + topic += "/zone_event" + if event_type == "ZONE_UDPATE": + topic += "/zone_update" + mq = mqtt_client.mqtt("192.168.10.4") mq.publish(topic, data) - print(mq) - + def main(): - + logging.basicConfig(level=logging.DEBUG, format='%(asctime)s: %(levelname)s: %(module)s: %(funcName)s: %(lineno)d: %(message)s') + logging.debug(("Command line arguments:", sys.argv[1:])) + args = get_command_line_args() + token, host = args["token"], args["host"] + if args["port"]: + port = args["port"] + else: + port = 1883 + if args["timeout"]: + timeout = args["timeout"] + else: + #Setting a default of one day timeout + timeout = 86400 + logging.debug("Creating qolsys_socket") qolsys = qolsys_socket.qolsys() - qolsys.create_socket(hostname="192.168.10.34", port=12345, token=token, cb=data_received, timeout=4) - print("main: sock created") - print("main: creating mqtt client") + qolsys.create_socket(hostname=host, port=port, token=token, cb=data_received, timeout=4) + logging.debug("main: sock created") + logging.debug("main: creating mqtt client") - print("doing the info check") - qolsys_status(qolsys, token) - time.sleep(2) - qolsys_status(qolsys, token) - time.sleep(2.9) + + logging.debug("doing the info check") qolsys_status(qolsys, token) - + +def get_command_line_args() -> dict: + args = {} + for arg in sys.argv[1:]: + arg_name = arg.split("=",1)[0] + arg_name = arg_name.split("--",1)[1] + arg_value = arg.split("=",1)[1] + logging.debug(("argument name: ", arg_name, " | ", arg_value)) + this_arg = { arg_name: arg_value } + args.update(this_arg) + logging.debug(("all arguments: ", args)) + return args if __name__ == "__main__": main() \ No newline at end of file From c8d84a5bd3a5a19ad5b5c04748802ec76a008e60 Mon Sep 17 00:00:00 2001 From: crazeeeyez Date: Tue, 2 Mar 2021 22:48:53 -0800 Subject: [PATCH 3/7] Added help, arguments --- qolsys_client/mqtt_client.py | 4 -- qolsys_client/status_nosock.py | 89 +++++++++++++++++++++++----------- 2 files changed, 60 insertions(+), 33 deletions(-) diff --git a/qolsys_client/mqtt_client.py b/qolsys_client/mqtt_client.py index 78bdb27..75f7592 100644 --- a/qolsys_client/mqtt_client.py +++ b/qolsys_client/mqtt_client.py @@ -17,10 +17,6 @@ def connect(self): def publish(self, topic:str, message:str): if topic == "" or message == "": raise Exception("Topic and Message required") - # if json.dumps(message): - # message = json.dumps((message).encode()) - # else: - # message = message.encode() published = self.client.publish(topic, message) while not published.is_published(): time.sleep(0.5) diff --git a/qolsys_client/status_nosock.py b/qolsys_client/status_nosock.py index 83a7175..ea40866 100644 --- a/qolsys_client/status_nosock.py +++ b/qolsys_client/status_nosock.py @@ -1,13 +1,14 @@ import sys import time import qolsys_socket -import threading +#import threading import mqtt_client import json import logging ################################################################################ # Code +args = {} def qolsys_status(qolsys, token): statusString = { @@ -30,56 +31,86 @@ def data_received(data:dict): Parameters: data: json object containing the output from the qolsys panel''' - # print(data) - topic = "qolsys" - jdata = json.loads(data) - event_type = jdata["event"] - if event_type == "INFO": - topic += "/info" - if event_type == "ZONE_EVENT": - topic += "/zone_event" - if event_type == "ZONE_UDPATE": - topic += "/zone_update" - - mq = mqtt_client.mqtt("192.168.10.4") - mq.publish(topic, data) + + if "mqtt-broker" in args: + mqtt_broker = args["mqtt-broker"] + mqtt_port = args["mqtt-port"] if "mqtt-port" in args else 1883 + topic = "qolsys" + jdata = json.loads(data) + event_type = jdata["event"] + if event_type == "INFO": + topic += "/info" + if event_type == "ZONE_EVENT": + topic += "/zone_event" + if event_type == "ZONE_UDPATE": + topic += "/zone_update" + + mq = mqtt_client.mqtt(mqtt_broker, mqtt_port) + mq.publish(topic, data) + else: + print(data) def main(): - logging.basicConfig(level=logging.DEBUG, format='%(asctime)s: %(levelname)s: %(module)s: %(funcName)s: %(lineno)d: %(message)s') + logging.basicConfig(level=logging.WARNING, format='%(asctime)s: %(levelname)s: %(module)s: %(funcName)s: %(lineno)d: %(message)s') logging.debug(("Command line arguments:", sys.argv[1:])) args = get_command_line_args() + + #Deal with some arguments that stop execution + if "help" in args: + help() + if not "token" in args: + print("Qolsys IQ 2 Panel token required") + sys.exit() + if not "host" in args: + print("Qolsys IQ 2 Panel IP address or hostname required") + sys.exit() + token, host = args["token"], args["host"] - if args["port"]: - port = args["port"] - else: - port = 1883 - if args["timeout"]: - timeout = args["timeout"] - else: - #Setting a default of one day timeout - timeout = 86400 + port = int(args["port"]) if "port" in args else 12345 + timeout = int(args["timeout"]) if "timeout" in args else 86400 + logging.debug("Creating qolsys_socket") qolsys = qolsys_socket.qolsys() - qolsys.create_socket(hostname=host, port=port, token=token, cb=data_received, timeout=4) + qolsys.create_socket(hostname=host, port=port, token=token, cb=data_received, timeout=timeout) logging.debug("main: sock created") - logging.debug("main: creating mqtt client") - logging.debug("doing the info check") qolsys_status(qolsys, token) def get_command_line_args() -> dict: - args = {} + #args = {} for arg in sys.argv[1:]: + arg_name = "" + arg_value = "" arg_name = arg.split("=",1)[0] arg_name = arg_name.split("--",1)[1] - arg_value = arg.split("=",1)[1] + try: + arg_value = arg.split("=",1)[1] + except: + arg_value = "" logging.debug(("argument name: ", arg_name, " | ", arg_value)) this_arg = { arg_name: arg_value } args.update(this_arg) logging.debug(("all arguments: ", args)) return args + +def help(): + help_text = """ + Parameters: + Required: + --host IP address or hostname of the Qolsys IQ 2(+) Panel + --port Port to connect on the Qolsys panel. Usually 12345 + --timeout Timeout (seconds) to wait after last data sent/received from the panel before disconnecting. Default will be one day. + --token Token from the Qolsys panel + + Optional: + --mqtt-broker IP address or hostname of the MQTT broker + --mqtt-port MQTT broker port to connect to (default is 1883) + """ + print(help_text) + sys.exit() + if __name__ == "__main__": main() \ No newline at end of file From d01ea79e2ba9c3e7ab35bfd58d0fdf19a5a76eb5 Mon Sep 17 00:00:00 2001 From: crazeeeyez Date: Sun, 7 Mar 2021 20:03:43 -0800 Subject: [PATCH 4/7] All basically working --- demo.py | 23 ---- qolsys_client/{status_nosock.py => main.py} | 50 ++++----- qolsys_client/mqtt_client.py | 14 ++- qolsys_client/mqtt_subscriber.py | 112 ++++++++++++++++++++ qolsys_client/qolsys_socket.py | 10 +- 5 files changed, 152 insertions(+), 57 deletions(-) delete mode 100644 demo.py rename qolsys_client/{status_nosock.py => main.py} (72%) create mode 100644 qolsys_client/mqtt_subscriber.py diff --git a/demo.py b/demo.py deleted file mode 100644 index cd139c6..0000000 --- a/demo.py +++ /dev/null @@ -1,23 +0,0 @@ -from qolsys_client import arm -from qolsys_client import status - -qolsysPanel = "192.168.10.34" -qolsysPort = 12345 -qolsysToken = "shw9s8" -qolsysTimeout = 20 - -# Status -result = status.qolsysStatus(qolsysPanel, qolsysPort, qolsysToken, qolsysTimeout) -print (result) - -# Arm Away -# result = arm.qolsysArm(qolsysPanel, qolsysPort, qolsysToken, qolsysTimeout, 0, "away") -# print (result) - -# # Arm Stay -# result = arm.qolsysArm(qolsysPanel, qolsysPort, qolsysToken, qolsysTimeout, 0, "stay") -# print (result) - -# # Disarm -# result = arm.qolsysArm(qolsysPanel, qolsysPort, qolsysToken, qolsysTimeout, 0, "disarm") -# print (result) diff --git a/qolsys_client/status_nosock.py b/qolsys_client/main.py similarity index 72% rename from qolsys_client/status_nosock.py rename to qolsys_client/main.py index ea40866..051f53d 100644 --- a/qolsys_client/status_nosock.py +++ b/qolsys_client/main.py @@ -1,33 +1,19 @@ import sys import time -import qolsys_socket #import threading -import mqtt_client import json import logging +from paho.mqtt.client import MQTTMessage +import qolsys_socket, mqtt_client +from mqtt_subscriber import MQTTSubscriber ################################################################################ # Code args = {} -def qolsys_status(qolsys, token): - statusString = { - "nonce": "qolsys", - "action": "INFO", - "info_type": "SUMMARY", - "version": 0, - "source": "C4", - "token": token, - } - - try: - qolsys.send_to_socket(statusString) - except: - logging.error('Could not send to socket') - - -def data_received(data:dict): - ''' This is where any json data coming from the qolsys panel will be sent. In this case I have the data being published to a mqtt topic, but you can do what you want. +def qolsys_data_received(data:dict): + ''' This is where any json data coming from the qolsys panel will be sent. + In this case I have the data being published to a mqtt topic, but you can do what you want. Parameters: data: json object containing the output from the qolsys panel''' @@ -35,15 +21,15 @@ def data_received(data:dict): if "mqtt-broker" in args: mqtt_broker = args["mqtt-broker"] mqtt_port = args["mqtt-port"] if "mqtt-port" in args else 1883 - topic = "qolsys" + topic = "qolsys/" jdata = json.loads(data) event_type = jdata["event"] if event_type == "INFO": - topic += "/info" + topic += "info" if event_type == "ZONE_EVENT": - topic += "/zone_event" + topic += "zone_event" if event_type == "ZONE_UDPATE": - topic += "/zone_update" + topic += "zone_update" mq = mqtt_client.mqtt(mqtt_broker, mqtt_port) mq.publish(topic, data) @@ -51,7 +37,7 @@ def data_received(data:dict): print(data) def main(): - logging.basicConfig(level=logging.WARNING, format='%(asctime)s: %(levelname)s: %(module)s: %(funcName)s: %(lineno)d: %(message)s') + logging.basicConfig(level=logging.DEBUG, format='%(asctime)s: %(levelname)s: %(module)s: %(funcName)s: %(lineno)d: %(message)s') logging.debug(("Command line arguments:", sys.argv[1:])) args = get_command_line_args() @@ -68,16 +54,21 @@ def main(): token, host = args["token"], args["host"] port = int(args["port"]) if "port" in args else 12345 timeout = int(args["timeout"]) if "timeout" in args else 86400 + #usercode = (args["usercode"]) if "usercode" in args else None logging.debug("Creating qolsys_socket") qolsys = qolsys_socket.qolsys() - qolsys.create_socket(hostname=host, port=port, token=token, cb=data_received, timeout=timeout) + qolsys.create_socket(hostname=host, port=port, token=token, cb=qolsys_data_received, timeout=timeout) + + logging.debug("main: qolsys_socket created") - logging.debug("main: sock created") + #logging.debug("doing the info check on startup") + #qolsys_status(qolsys, token) - logging.debug("doing the info check") - qolsys_status(qolsys, token) + #qolsys_arm(qolsys,token,"stay") + mqtt_sub = MQTTSubscriber(broker="192.168.10.4", port=1883, qolsys=qolsys, topics=["qolsys/requests"]) + def get_command_line_args() -> dict: #args = {} @@ -104,6 +95,7 @@ def help(): --port Port to connect on the Qolsys panel. Usually 12345 --timeout Timeout (seconds) to wait after last data sent/received from the panel before disconnecting. Default will be one day. --token Token from the Qolsys panel + --usercode If you want to use disarm, you need to supply a usercode Optional: --mqtt-broker IP address or hostname of the MQTT broker diff --git a/qolsys_client/mqtt_client.py b/qolsys_client/mqtt_client.py index 75f7592..2f1f6da 100644 --- a/qolsys_client/mqtt_client.py +++ b/qolsys_client/mqtt_client.py @@ -1,6 +1,8 @@ import paho.mqtt.client as pmqtt +import paho.mqtt.subscribe as smqtt import json import time +import logging class mqtt: @@ -20,4 +22,14 @@ def publish(self, topic:str, message:str): published = self.client.publish(topic, message) while not published.is_published(): time.sleep(0.5) - print("published:", published.rc) \ No newline at end of file + print("published:", published.rc) + + def subscribe(self, topics:[], cb:callable): + if topics == []: + raise Exception("Need a topic to listen to") + logging.debug("Starting the MQTT subscriber") + smqtt.callback(cb, topics, hostname=self.broker) + #subscribed.callback(cb,) + #self.client.subscribe(topic) + #self.client.on + diff --git a/qolsys_client/mqtt_subscriber.py b/qolsys_client/mqtt_subscriber.py new file mode 100644 index 0000000..ed7dee2 --- /dev/null +++ b/qolsys_client/mqtt_subscriber.py @@ -0,0 +1,112 @@ +import sys +import time +import qolsys_socket, mqtt_client +#import threading +import json +import logging +from paho.mqtt.client import MQTTMessage + +class MQTTSubscriber: + def __init__(self, broker="", qolsys:qolsys_socket.qolsys=None, port=1883, topics=[]): + self.broker = broker + self.qolsys = qolsys + self.port = port + self.topics = topics + self._arming_types = ["away", "stay", "disarm"] + + + mqtt_sub = mqtt_client.mqtt("192.168.10.4", 1883) + mqtt_sub.subscribe(topics=self.topics, cb=self.mqtt_request_received) + + def mqtt_request_received(self, client, userdata, message:MQTTMessage): + '''Runs when a MQTT event is received on the request topic + + Parameters: + data: json object containing the request to send to the qolsys panel''' + + logging.debug(("client:", client)) + logging.debug(("userdata:", userdata)) + payload = message.payload + logging.debug(("message:", payload)) + payload_json = {} + event_type = "" + + try: + if json.loads(payload): + payload_json = json.loads(payload) + logging.debug(("payload:", payload_json)) + event_type = payload_json["event"] + logging.debug(("event:", event_type)) + except: + logging.debug(("Error converting to JSON:", sys.exc_info())) + logging.debug(("Not JSON:", payload)) + + if event_type != "": + token = payload_json["token"] if "token" in payload_json else None + usercode = payload_json["usercode"] if "usercode" in payload_json else None + partition_id = payload_json["partition_id"] if "partition_id" in payload_json else None + arm_type = payload_json["arm_type"] if "arm_type" in payload_json else None + + if token == None: + raise("Token required for anything you want to do") + + if event_type == "INFO": + self.qolsys_status(self.qolsys, token) + + if event_type == "ARM": + self.qolsys_arm(self.qolsys, token, arm_type, partition_id) + + if event_type == "DISARM": + self.qolsys_arm(self.qolsys, token, "disarm", partition_id, usercode) + + def qolsys_arm(self, qolsys, token:str, arming_type:str, partition_id:int, usercode=""): + if not arming_type in self._arming_types: + raise("Invalid arm command") + + arm_type = "" + + if arming_type.lower() == 'away': + arm_type = "ARM_AWAY" + elif arming_type.lower() == 'stay': + arm_type = "ARM_STAY" + elif arming_type.lower() == 'disarm': + arm_type = "DISARM" + else: + raise("Invalid arm command") + + armString = { + "partition_id": partition_id, + "action": "ARMING", + "arming_type": arm_type, + "version": 0, + "nonce": "qolsys", + "source": "C4", + "version_key": 1, + "source_key": "C4", + "token": token + } + + #Disarm requires a usercode + if arming_type.lower() == "disarm": + armString.update({"usercode":usercode}) + + try: + logging.debug(("armString:", armString)) + qolsys.send_to_socket(armString) + except socket.error: + logging.error("Could not send arm command to qolsys socket") + + def qolsys_status(self, qolsys, token): + statusString = { + "nonce": "qolsys", + "action": "INFO", + "info_type": "SUMMARY", + "version": 0, + "source": "C4", + "token": token, + } + + try: + qolsys.send_to_socket(statusString) + except: + logging.error('Could not send status request to qolsys socket') diff --git a/qolsys_client/qolsys_socket.py b/qolsys_client/qolsys_socket.py index ee7f0eb..5b8da30 100644 --- a/qolsys_client/qolsys_socket.py +++ b/qolsys_client/qolsys_socket.py @@ -53,10 +53,10 @@ def send_to_socket(self, message: json): return True def listen(self, cb: callable): - listening = True + #listening = True logging.debug("starting listen") data = "" - err = "" + #err = "" while not (self.wrappedSocket._connected): logging.warning("not connected yet") logging.debug(self.wrappedSocket._connected) @@ -72,7 +72,8 @@ def listen(self, cb: callable): #print(data) else: if data != 'ACK\n': - logging.warning(("non json data:", data)) + pass + #logging.warning(("non json data:", data)) except socket.timeout: logging.debug("socket timeout") except: @@ -83,7 +84,8 @@ def is_json(myjson): try: json_object = json.loads(myjson) if json_object: return True - except ValueError as e: + except: if myjson != 'ACK\n': logging.debug(("not json:", myjson)) + logging.debug(("Error:", sys.exc_info())) return False \ No newline at end of file From 8bd038370d487bba00b356b627c11dc260a690a0 Mon Sep 17 00:00:00 2001 From: crazeeeyez Date: Wed, 10 Mar 2021 06:27:36 -0800 Subject: [PATCH 5/7] Socket resiliency when socket errors occur Added exception handling when the socket returns no data e.g. when the token is wrong. --- qolsys_client/main.py | 36 +++++++++++-- qolsys_client/mqtt_subscriber.py | 57 ++++++++++++++------ qolsys_client/qolsys_socket.py | 91 +++++++++++++++++++++++--------- 3 files changed, 138 insertions(+), 46 deletions(-) diff --git a/qolsys_client/main.py b/qolsys_client/main.py index 051f53d..d1761b9 100644 --- a/qolsys_client/main.py +++ b/qolsys_client/main.py @@ -1,6 +1,5 @@ import sys import time -#import threading import json import logging from paho.mqtt.client import MQTTMessage @@ -9,7 +8,7 @@ ################################################################################ # Code -args = {} +#args = {} def qolsys_data_received(data:dict): ''' This is where any json data coming from the qolsys panel will be sent. @@ -18,6 +17,7 @@ def qolsys_data_received(data:dict): Parameters: data: json object containing the output from the qolsys panel''' + args = get_command_line_args() if "mqtt-broker" in args: mqtt_broker = args["mqtt-broker"] mqtt_port = args["mqtt-port"] if "mqtt-port" in args else 1883 @@ -54,8 +54,25 @@ def main(): token, host = args["token"], args["host"] port = int(args["port"]) if "port" in args else 12345 timeout = int(args["timeout"]) if "timeout" in args else 86400 + mqtt_broker = args["mqtt-broker"] if "mqtt-broker" in args else None + mqtt_port = args["mqtt-port"] if "mqtt-port" in args else 1883 #usercode = (args["usercode"]) if "usercode" in args else None + topics = [] #args["topics"] if "topics" in args else ["qolsys/requests"] + if args["topics"]: + topic_data = str(args["topics"]) + logging.debug(("topics arg:", topic_data)) + if topic_data.find(",") > 0: + topic_array = args["topics"].split(",") + logging.debug(("topic_array:", topic_array)) + for t in topic_array: + logging.debug(("t:", t)) + topics.append(t) + else: + topics.append(topic_data) + else: + raise("No topics") + logging.debug("Creating qolsys_socket") qolsys = qolsys_socket.qolsys() @@ -67,11 +84,14 @@ def main(): #qolsys_status(qolsys, token) #qolsys_arm(qolsys,token,"stay") - mqtt_sub = MQTTSubscriber(broker="192.168.10.4", port=1883, qolsys=qolsys, topics=["qolsys/requests"]) + if mqtt_broker: + mqtt_sub = MQTTSubscriber(broker=mqtt_broker, port=mqtt_port, qolsys=qolsys, topics=topics) + else: + logging.info("No MQTT Broker. Only getting status events from panel") def get_command_line_args() -> dict: - #args = {} + args = {} for arg in sys.argv[1:]: arg_name = "" arg_value = "" @@ -95,11 +115,17 @@ def help(): --port Port to connect on the Qolsys panel. Usually 12345 --timeout Timeout (seconds) to wait after last data sent/received from the panel before disconnecting. Default will be one day. --token Token from the Qolsys panel - --usercode If you want to use disarm, you need to supply a usercode + --usercode (UNUSED) If you want to use disarm, you need to supply a usercode Optional: --mqtt-broker IP address or hostname of the MQTT broker --mqtt-port MQTT broker port to connect to (default is 1883) + --topics A list (array) of topics to subscribe to for qolsys event requests e.g. --topics=["qolsys/requests"] (Default) + + Usage: + python3 main.py --host=192.168.1.123 --port=12345 --token=yourtoken --timeout=86400 --mqtt-broker=192.168.1.2 --mqtt-port=1883 --topics=["qolsys/requests"] + + """ print(help_text) sys.exit() diff --git a/qolsys_client/mqtt_subscriber.py b/qolsys_client/mqtt_subscriber.py index ed7dee2..d2c1282 100644 --- a/qolsys_client/mqtt_subscriber.py +++ b/qolsys_client/mqtt_subscriber.py @@ -15,32 +15,50 @@ def __init__(self, broker="", qolsys:qolsys_socket.qolsys=None, port=1883, topic self._arming_types = ["away", "stay", "disarm"] - mqtt_sub = mqtt_client.mqtt("192.168.10.4", 1883) + logging.debug(("MQTT Subscriber: ", self.broker, ":", self.port, ":", self.topics)) + mqtt_sub = mqtt_client.mqtt(self.broker, self.port) mqtt_sub.subscribe(topics=self.topics, cb=self.mqtt_request_received) def mqtt_request_received(self, client, userdata, message:MQTTMessage): '''Runs when a MQTT event is received on the request topic Parameters: - data: json object containing the request to send to the qolsys panel''' + data: json object containing the request to send to the qolsys panel + + Expected JSON Message: + Required: + event INFO, ARM, DISARM + token Qolsys IQ Panel token + + Optional + usercode Required if disarming + partition_id Required if arming or disarming. 0 is a good value if you don't know what to use + arm_type Required if arming. Options are "away" or "stay" + ''' logging.debug(("client:", client)) logging.debug(("userdata:", userdata)) + logging.debug(("message:", message)) payload = message.payload - logging.debug(("message:", payload)) + logging.debug(("payload:", payload)) payload_json = {} event_type = "" try: if json.loads(payload): payload_json = json.loads(payload) - logging.debug(("payload:", payload_json)) - event_type = payload_json["event"] - logging.debug(("event:", event_type)) + logging.debug(("payload_json:", payload_json)) except: logging.debug(("Error converting to JSON:", sys.exc_info())) logging.debug(("Not JSON:", payload)) + try: + event_type = payload_json["event"] + logging.debug(("event:", event_type)) + except: + logging.error(('Unable to find "event"', payload_json)) + #raise("Unable to receive MQTT requests") + if event_type != "": token = payload_json["token"] if "token" in payload_json else None usercode = payload_json["usercode"] if "usercode" in payload_json else None @@ -48,16 +66,23 @@ def mqtt_request_received(self, client, userdata, message:MQTTMessage): arm_type = payload_json["arm_type"] if "arm_type" in payload_json else None if token == None: - raise("Token required for anything you want to do") + #raise("Token required for anything you want to do") + logging.error("No token provided. Token is required for anything you want to do with the Qolsys panel") + else: + if event_type == "INFO": + self.qolsys_status(self.qolsys, token) - if event_type == "INFO": - self.qolsys_status(self.qolsys, token) - - if event_type == "ARM": - self.qolsys_arm(self.qolsys, token, arm_type, partition_id) - - if event_type == "DISARM": - self.qolsys_arm(self.qolsys, token, "disarm", partition_id, usercode) + if event_type == "ARM": + if partition_id is None or arm_type is None: + logging.error(("arm_type and partition_id are required")) + else: + self.qolsys_arm(self.qolsys, token, arm_type, partition_id) + + if event_type == "DISARM": + if partition_id is None or arm_type is None or usercode is None: + logging.error(("arm_type, partition_id, and usercode are required")) + else: + self.qolsys_arm(self.qolsys, token, "disarm", partition_id, usercode) def qolsys_arm(self, qolsys, token:str, arming_type:str, partition_id:int, usercode=""): if not arming_type in self._arming_types: @@ -109,4 +134,4 @@ def qolsys_status(self, qolsys, token): try: qolsys.send_to_socket(statusString) except: - logging.error('Could not send status request to qolsys socket') + logging.error('Could not send status request to qolsys socket') \ No newline at end of file diff --git a/qolsys_client/qolsys_socket.py b/qolsys_client/qolsys_socket.py index 5b8da30..dd38228 100644 --- a/qolsys_client/qolsys_socket.py +++ b/qolsys_client/qolsys_socket.py @@ -12,43 +12,71 @@ class qolsys: # Code def __init__(self): - self.sock = socket.socket - self.wrappedSocket = ssl.SSLContext.wrap_socket + self._sock = socket.socket + self._wrappedSocket = ssl.SSLContext.wrap_socket + self._listening_thread = threading.Thread() + self._listener_callback = callable + self._hostname = "" + self._port = 12345 + self._token = "" + self._timeout = 60 def create_socket(self, hostname, port, token, cb: callable, timeout=60): + self._hostname = hostname + self._port = port + self._token = token + self._listener_callback = cb + self._timeout = timeout try: - self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.sock.settimeout(timeout) + self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self._sock.settimeout(timeout) + #Set the listener callback at the instance level so we can restart the listener if needed except socket.error: logging.error('Could not create a socket') raise # Wrap SSL logging.debug("wrapping socket") - self.wrappedSocket = ssl.wrap_socket(self.sock, cert_reqs=ssl.CERT_NONE, ssl_version=ssl.PROTOCOL_TLSv1_2) + self._wrappedSocket = ssl.wrap_socket(self._sock, cert_reqs=ssl.CERT_NONE, ssl_version=ssl.PROTOCOL_TLSv1_2) # Connect to server try: #The stupid Qolsys panel requires blocking # wrappedSocket.setblocking(False) logging.debug("connecting to socket") - self.wrappedSocket.connect((hostname, port)) - logging.debug(("Connected wrappedSocket:", self.wrappedSocket)) + self._wrappedSocket.connect((hostname, port)) + logging.debug(("Connected wrappedSocket:", self._wrappedSocket)) logging.debug("Starting listener thread") - thread = threading.Thread(target=self.listen, args=([cb])) - thread.start() + self._start_listener() + #self.listening_thread = threading.Thread(target=self.listen, args=([cb])) + #self.listening_thread.start() logging.debug("started listener") return True except socket.error: logging.error(("Error creating or connecting to socket", sys.exc_info())) return False + def _start_listener(self): + logging.debug(("Starting listener thread")) + self._listening_thread = threading.Thread(target=self.listen, args=([self._listener_callback])) + self._listening_thread.start() + logging.debug(("started listener thread")) + + def _reset_socket(self): + logging.debug(("Detatching from wrapped socket")) + self._wrappedSocket.detach() + logging.debug(("Closing socket")) + self._sock.close() + time.sleep(2) + #self._listening_thread = threading.Thread(target=self.listen, args=([self._listener_callback])) + logging.debug(("Creating socket")) + self.create_socket(self._hostname, self._port, self._token, self._listener_callback, self._timeout) def send_to_socket(self, message: json): - self.wrappedSocket.send(b'\n') - self.wrappedSocket.send((json.dumps(message)).encode()) + self._wrappedSocket.send(b'\n') + self._wrappedSocket.send((json.dumps(message)).encode()) return True @@ -57,29 +85,39 @@ def listen(self, cb: callable): logging.debug("starting listen") data = "" #err = "" - while not (self.wrappedSocket._connected): + while not (self._wrappedSocket._connected): logging.warning("not connected yet") - logging.debug(self.wrappedSocket._connected) + logging.debug(self._wrappedSocket._connected) time.sleep(1) try: - while self.wrappedSocket._connected: - data = self.wrappedSocket.recv(4096).decode() - if is_json(data): - try: - cb(data) - except: - logging.error(("Error calling callback:", cb, sys.exc_info())) - #print(data) - else: - if data != 'ACK\n': - pass + while self._wrappedSocket._connected: + data = self._wrappedSocket.recv(4096).decode() + if len(data) > 0: + logging.debug(("data received from qolsys panel:", data, "len(data): ", len(data))) + if is_json(data): + try: + cb(data) + except: + logging.error(("Error calling callback:", cb, sys.exc_info())) + #print(data) + else: + if data != 'ACK\n': + pass #logging.warning(("non json data:", data)) + else: + logging.error(("No data received. Bad token? Detatching.")) + self._wrappedSocket.detach() + raise NoDataError except socket.timeout: logging.debug("socket timeout") + except NoDataError: + self._reset_socket() + raise NoDataError except: logging.error(("listen failed/stopped:", sys.exc_info())) + def is_json(myjson): try: json_object = json.loads(myjson) @@ -88,4 +126,7 @@ def is_json(myjson): if myjson != 'ACK\n': logging.debug(("not json:", myjson)) logging.debug(("Error:", sys.exc_info())) - return False \ No newline at end of file + return False + +class NoDataError(Exception): + pass \ No newline at end of file From e47cef26ff34b0793e613044a76ab2a1ca0f6519 Mon Sep 17 00:00:00 2001 From: crazeeeyez Date: Sat, 13 Mar 2021 06:43:22 -0800 Subject: [PATCH 6/7] Documentation! --- README.md | 17 +++++++++++++++++ node-red/node-red.md | 1 + qolsys_client/main.py | 6 +++--- qolsys_client/mqtt_subscriber.py | 3 ++- 4 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 node-red/node-red.md diff --git a/README.md b/README.md index de404ed..bac5eb1 100644 --- a/README.md +++ b/README.md @@ -5,3 +5,20 @@ Python package to talk to Qolsys Alarm Panels This package was inspired by a conversation on the Home Assistant forum: + + +1. Run main.py (--help for parameters and help) + +2. Publish JSON formatted messages to the mqtt topic (qolsys/requests by default): + +{"event":"INFO", "token":"blah"} +{"event":"ARM", "arm_type":"stay", "partition_id": 0, "token":"blah"} +{"event":"ARM", "arm_type":"away", "partition_id": 0, "token":"blah"} +{"event":"DISARM", "usercode":"0000", "token":"blah"} + +3. Events will publish to the following topics: + qolsys/INFO + qolsys/ZONE_EVENT + qolsys/ZONE_UPDATE + +4. I have a node-red workflow listening to those events creating the sensors \ No newline at end of file diff --git a/node-red/node-red.md b/node-red/node-red.md new file mode 100644 index 0000000..8620cfd --- /dev/null +++ b/node-red/node-red.md @@ -0,0 +1 @@ +[{"id":"cbfaf013.0e5e","type":"tab","label":"Monitor QolSys Panel","disabled":false,"info":""},{"id":"20c47d1f.7e57d2","type":"debug","z":"cbfaf013.0e5e","name":"mqtt out","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":980,"y":120,"wires":[]},{"id":"a272ddf7.56ba6","type":"debug","z":"cbfaf013.0e5e","name":"Zone id to zone name mapping","active":false,"tosidebar":true,"console":false,"tostatus":true,"complete":"$flowContext(\"zones\")\t","targetType":"jsonata","statusVal":"$flowContext(\"zones\")","statusType":"auto","x":1030,"y":260,"wires":[]},{"id":"7593a9f9.4b4dc8","type":"function","z":"cbfaf013.0e5e","name":"initialize zone ids and names","func":"class door_window {\n constructor(zoneid, entity_id, name, state=\"Closed\", partition_id=0, device_class=\"door\") {\n this.state = state\n this.id = zoneid\n this.device_class = device_class\n this.name = name\n this.entity_id = entity_id\n this.partition_id = partition_id\n this.payload_on = \"Open\"\n this.payload_off = \"Closed\"\n this.config_topic = \"homeassistant/binary_sensor/\" + entity_id + \"/config\"\n this.state_topic = \"mqtt_states/binary_sensor/\" + entity_id + \"/state\"\n //node.log(\"Name: \" + name + \"\\n\" + \n // \"ZoneID: \" + zoneid + \"\\n\" +\n // \"EntityID: \" + entity_id + \"\\n\" + \n // \"PartitionID: \" + partition_id + \"\\n\" + \n // \"Device Class: \" + device_class + \"\\n\" +\n // \"State: \" + state)\n }\n toString(self){\n return self.name\n } \n}\n\nvar zones = flow.get(\"zones\")\n\nfor (var zone in msg.payload) {\n// node.log(zone)\n var id = msg.payload[zone][\"zone_id\"]\n var name = msg.payload[zone][\"name\"]\n var state = msg.payload[zone][\"status\"]\n var partition_id = msg.payload[zone][\"partition_id\"]\n var entity_id = name.replace(/\\W/g, \"_\").toLowerCase()\n var thisZone = new door_window(zoneid=id, entity_id=entity_id, name=name, state=state, partition_id=partition_id)\n zones[id] = thisZone\n// node.log(thisZone)\n \n}\n//msg.payload = zones\n//node.log(\"Zones:\" + zones)\nflow.set(\"zones\", zones)\nreturn msg\n","outputs":1,"noerr":0,"initialize":"// Code added here will be run once\n// whenever the node is deployed.\n\nclass door_window {\n constructor(zoneid, entity_id=\"\", name, device_class=\"door\", state=\"Closed\", partition_id=0 ) {\n this.state = state\n this.id = zoneid\n this.name = name\n this.device_class = device_class\n this.entity_id = entity_id\n this.partition_id = partition_id\n this.payload_on = \"Open\"\n this.payload_off = \"Closed\"\n this.config_topic = \"homeassistant/binary_sensor/\" + entity_id + \"/config\"\n this.state_topic = \"mqtt_states/binary_sensor/\" + entity_id + \"/state\" \n }\n \n}\nzones = []\n\n//zones[1] = new door_window(1, \"front_door\", \"Front Door\")\n//zones[2] = new door_window(2, \"trash_gate\", \"Trash Gate\")\n//zones[3] = new door_window(3, \"neela_s_garage_entry\", \"Neela's Garage Entry\")\n//zones[4] = new door_window(4, \"roopesh_s_garage_entry\", \"Roopesh's Garage Entry\")\n//zones[6] = new door_window(6, \"great_room_slider\", \"Great Room Slider\")\n//zones[7] = new door_window(7, \"garage_side_door\", \"Garage Side Door\")\n//zones[9] = new door_window(9, \"kitchen_slider\", \"Kitchen Slider\")\n//zones[10] = new door_window(10, \"game_room_slider\", \"Game Room Slider\")\n//zones[11] = new door_window(11, \"loft_slider\", \"Loft Slider\")\n//zones[12] = new door_window(12, \"guest_room_side_gate\", \"Guest Room Side Gate\")\nflow.set(\"zones\", zones)\n","finalize":"","x":460,"y":260,"wires":[["a272ddf7.56ba6","4e0e16a3.7cd0f8"]]},{"id":"59822226.0248cc","type":"function","z":"cbfaf013.0e5e","name":"Build MQTT Update Payload","func":"var zid = msg.payload[\"zone\"][\"zone_id\"]\nvar zones = flow.get(\"zones\")\nvar state_topic = zones[zid][\"state_topic\"]\nvar state = msg.payload[\"zone\"][\"status\"]\nzones[zid][\"state\"] = state\nflow.set(\"zones\", zones)\nmsg.payload = state\nmsg.topic = state_topic\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":580,"y":120,"wires":[["7a8a7809.26cf68","20c47d1f.7e57d2"]]},{"id":"4e0e16a3.7cd0f8","type":"array-loop","z":"cbfaf013.0e5e","name":"loop zones","key":"al4e0e16a37cd0f8","keyType":"msg","reset":true,"resetValue":"value-null","array":"zones","arrayType":"flow","x":430,"y":320,"wires":[["89ebb812.5ea3a8"],["a51867e3.622258"]]},{"id":"89ebb812.5ea3a8","type":"debug","z":"cbfaf013.0e5e","name":"All HA Init'd","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":970,"y":340,"wires":[]},{"id":"a51867e3.622258","type":"switch","z":"cbfaf013.0e5e","name":"","property":"payload","propertyType":"msg","rules":[{"t":"null"},{"t":"empty"},{"t":"else"}],"checkall":"false","repair":false,"outputs":3,"x":230,"y":420,"wires":[["4e0e16a3.7cd0f8"],["4e0e16a3.7cd0f8"],["48744f25.dce7a"]]},{"id":"3f8da6fc.a7238a","type":"catch","z":"cbfaf013.0e5e","name":"Catch","scope":null,"uncaught":false,"x":530,"y":40,"wires":[["cd788dfe.d978"]]},{"id":"cd788dfe.d978","type":"debug","z":"cbfaf013.0e5e","name":"All Uncaught Errors","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":990,"y":40,"wires":[]},{"id":"dfd2c176.68a0e","type":"debug","z":"cbfaf013.0e5e","name":"Initialized in HA","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":980,"y":420,"wires":[]},{"id":"1534a5cd.807cea","type":"mqtt out","z":"cbfaf013.0e5e","name":"","topic":"","qos":"","retain":"","broker":"b1167957.68aa08","x":810,"y":460,"wires":[]},{"id":"48744f25.dce7a","type":"function","z":"cbfaf013.0e5e","name":"Build MQTT topic and payload","func":"//var topic = \"\"\ntry {\n var type = msg.payload[\"type\"]\n var entity_id = msg.payload[\"entity_id\"]\n var name = msg.payload[\"name\"]\n var device_class = msg.payload[\"device_class\"]\n var state_topic = msg.payload[\"state_topic\"]\n var config_topic = msg.payload[\"config_topic\"]\n var payload_on = msg.payload[\"payload_on\"]\n var payload_off = msg.payload[\"payload_off\"]\n var state = msg.payload[\"state\"]\n var config_msg = {\n \"topic\": config_topic,\n \"payload\": {\n \"name\": name,\n \"device_class\": device_class,\n \"state_topic\": state_topic,\n \"payload_on\": payload_on,\n \"payload_off\": payload_off\n }\n }\n}\ncatch(err) {\n node.log(\"failed making variables\", err)\n}\n\n\n//msg.topic = config_topic\n\n//msg.payload = config_msg\ntry {\n var zone_update_msg = {\n \"topic\": state_topic,\n \"payload\": state\n }\n}\ncatch(err) {\n node.log(\"failed making zone_update_msg\")\n}\n//msg.zone_update_msg = zone_update_msg\n//msg.config_msg = config_msg\n//flow.set(\"zone_update_msg\", zone_update_msg)\n//flow.set(\"config_msg\", config_msg)\ntry {\n node.log(\"config msg: \" + config_msg.topic + config_msg.payload.name)\n node.log(\"zone msg: \" + zone_update_msg.topic + zone_update_msg.payload)\n node.log(\"msg: \" + msg.payload.name)\n}\ncatch(err) {\n node.log(\"error logging data\")\n}\n//return(msg, config_msg, zone_update_msg)\n//return(zone_update_msg, config_msg, msg)\nreturn[msg, config_msg, zone_update_msg]","outputs":3,"noerr":0,"initialize":"","finalize":"","x":470,"y":420,"wires":[["4e0e16a3.7cd0f8"],["dfd2c176.68a0e","1534a5cd.807cea"],["94a3b1f.1389e5"]]},{"id":"7a8a7809.26cf68","type":"mqtt out","z":"cbfaf013.0e5e","name":"","topic":"","qos":"","retain":"","broker":"b1167957.68aa08","x":790,"y":160,"wires":[]},{"id":"c1e92bd3.455468","type":"mqtt in","z":"cbfaf013.0e5e","name":"INFO","topic":"qolsys/info","qos":"0","datatype":"auto","broker":"b1167957.68aa08","x":110,"y":100,"wires":[["15befb3c.f83975"]]},{"id":"ec802f9d.c402f","type":"change","z":"cbfaf013.0e5e","name":"Set Zones payload for INFO","rules":[{"t":"set","p":"payload","pt":"msg","to":"$lookup($lookup(payload, \"partition_list\"[0]), \"zone_list\")","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":180,"y":260,"wires":[["ff9d620d.a7c55","7593a9f9.4b4dc8"]]},{"id":"df81c870.4ecda8","type":"mqtt out","z":"cbfaf013.0e5e","name":"Send INFO request","topic":"qolsys/requests","qos":"","retain":"","broker":"b1167957.68aa08","x":330,"y":40,"wires":[]},{"id":"46573582.43dbcc","type":"inject","z":"cbfaf013.0e5e","name":"Start with INFO","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"{\"event\":\"INFO\",\"token\":\"shw9s8\"}","payloadType":"json","x":120,"y":40,"wires":[["df81c870.4ecda8"]]},{"id":"15befb3c.f83975","type":"json","z":"cbfaf013.0e5e","name":"","property":"payload","action":"","pretty":false,"x":230,"y":140,"wires":[["7488361e.748058"]]},{"id":"ff9d620d.a7c55","type":"debug","z":"cbfaf013.0e5e","name":"Zones Payload","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":980,"y":180,"wires":[]},{"id":"7488361e.748058","type":"switch","z":"cbfaf013.0e5e","name":"","property":"payload","propertyType":"msg","rules":[{"t":"jsonata_exp","v":"\"ZONE_EVENT\" = $lookup(payload, \"event\")","vt":"jsonata"},{"t":"jsonata_exp","v":"\"ZONE_UPDATE\" = $lookup(payload, \"event\")","vt":"jsonata"},{"t":"jsonata_exp","v":"\"INFO\" = $lookup(payload, \"event\")","vt":"jsonata"},{"t":"jsonata_exp","v":"\"ARMING\" = $lookup(payload, \"event\")","vt":"jsonata"}],"checkall":"true","repair":false,"outputs":4,"x":350,"y":140,"wires":[["59822226.0248cc"],["59822226.0248cc"],["ec802f9d.c402f"],[]]},{"id":"94a3b1f.1389e5","type":"delay","z":"cbfaf013.0e5e","name":"Zone Update Delay","pauseType":"delay","timeout":"1","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":590,"y":500,"wires":[["1534a5cd.807cea","20ab17af.414598"]]},{"id":"e3a9fee7.6b71e","type":"mqtt in","z":"cbfaf013.0e5e","name":"ZONE UPDATE","topic":"qolsys/zone_update","qos":"0","datatype":"auto","broker":"b1167957.68aa08","x":80,"y":180,"wires":[["15befb3c.f83975"]]},{"id":"b49e6dbf.498da","type":"mqtt in","z":"cbfaf013.0e5e","name":"ZONE EVENT","topic":"qolsys/zone_event","qos":"0","datatype":"auto","broker":"b1167957.68aa08","x":90,"y":140,"wires":[["15befb3c.f83975"]]},{"id":"20ab17af.414598","type":"debug","z":"cbfaf013.0e5e","name":"Zone Update in HA","active":false,"tosidebar":true,"console":false,"tostatus":true,"complete":"true","targetType":"full","statusVal":"payload","statusType":"auto","x":990,"y":500,"wires":[]},{"id":"b1167957.68aa08","type":"mqtt-broker","name":"HA MQTT","broker":"127.0.0.1","port":"1883","clientid":"","usetls":false,"compatmode":true,"keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","closeTopic":"","closeQos":"0","closePayload":"","willTopic":"","willQos":"0","willPayload":""}] \ No newline at end of file diff --git a/qolsys_client/main.py b/qolsys_client/main.py index d1761b9..65e996f 100644 --- a/qolsys_client/main.py +++ b/qolsys_client/main.py @@ -31,6 +31,7 @@ def qolsys_data_received(data:dict): if event_type == "ZONE_UDPATE": topic += "zone_update" + logging.debug(("publishing " + event_type + " event to: " + topic)) mq = mqtt_client.mqtt(mqtt_broker, mqtt_port) mq.publish(topic, data) else: @@ -46,17 +47,16 @@ def main(): help() if not "token" in args: print("Qolsys IQ 2 Panel token required") - sys.exit() + help()#sys.exit() if not "host" in args: print("Qolsys IQ 2 Panel IP address or hostname required") - sys.exit() + help()#sys.exit() token, host = args["token"], args["host"] port = int(args["port"]) if "port" in args else 12345 timeout = int(args["timeout"]) if "timeout" in args else 86400 mqtt_broker = args["mqtt-broker"] if "mqtt-broker" in args else None mqtt_port = args["mqtt-port"] if "mqtt-port" in args else 1883 - #usercode = (args["usercode"]) if "usercode" in args else None topics = [] #args["topics"] if "topics" in args else ["qolsys/requests"] if args["topics"]: topic_data = str(args["topics"]) diff --git a/qolsys_client/mqtt_subscriber.py b/qolsys_client/mqtt_subscriber.py index d2c1282..3d9eb63 100644 --- a/qolsys_client/mqtt_subscriber.py +++ b/qolsys_client/mqtt_subscriber.py @@ -64,7 +64,7 @@ def mqtt_request_received(self, client, userdata, message:MQTTMessage): usercode = payload_json["usercode"] if "usercode" in payload_json else None partition_id = payload_json["partition_id"] if "partition_id" in payload_json else None arm_type = payload_json["arm_type"] if "arm_type" in payload_json else None - + logging.debug(("event:", event_type,"usercode:", usercode, "partition_id", partition_id, "arm_type", arm_type)) if token == None: #raise("Token required for anything you want to do") logging.error("No token provided. Token is required for anything you want to do with the Qolsys panel") @@ -79,6 +79,7 @@ def mqtt_request_received(self, client, userdata, message:MQTTMessage): self.qolsys_arm(self.qolsys, token, arm_type, partition_id) if event_type == "DISARM": + arm_type = "disarm" if partition_id is None or arm_type is None or usercode is None: logging.error(("arm_type, partition_id, and usercode are required")) else: From 952f223753aa863426c11e2321b1e3e95b2436e4 Mon Sep 17 00:00:00 2001 From: mzac <10270879+mzac@users.noreply.github.com> Date: Sat, 13 Mar 2021 10:15:23 -0500 Subject: [PATCH 7/7] Update README.md fix up markdown a bit --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index bac5eb1..73d0795 100644 --- a/README.md +++ b/README.md @@ -11,14 +11,16 @@ This package was inspired by a conversation on the Home Assistant forum: 2. Publish JSON formatted messages to the mqtt topic (qolsys/requests by default): +``` {"event":"INFO", "token":"blah"} {"event":"ARM", "arm_type":"stay", "partition_id": 0, "token":"blah"} {"event":"ARM", "arm_type":"away", "partition_id": 0, "token":"blah"} {"event":"DISARM", "usercode":"0000", "token":"blah"} +``` 3. Events will publish to the following topics: - qolsys/INFO - qolsys/ZONE_EVENT - qolsys/ZONE_UPDATE + - qolsys/INFO + - qolsys/ZONE_EVENT + - qolsys/ZONE_UPDATE -4. I have a node-red workflow listening to those events creating the sensors \ No newline at end of file +4. I have a node-red workflow listening to those events creating the sensors