Skip to content
This repository has been archived by the owner on Apr 6, 2022. It is now read-only.

Commit

Permalink
Merge pull request #1 from roopesh/separate_socket
Browse files Browse the repository at this point in the history
Background socket and mqtt
  • Loading branch information
mzac committed Mar 13, 2021
2 parents 3df4413 + 952f223 commit f0e4889
Show file tree
Hide file tree
Showing 8 changed files with 459 additions and 77 deletions.
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,22 @@ Python package to talk to Qolsys Alarm Panels
This package was inspired by a conversation on the Home Assistant forum:

<https://community.home-assistant.io/t/qolsys-iq-panel-2-and-3rd-party-integration/231405>


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
23 changes: 0 additions & 23 deletions demo.py

This file was deleted.

1 change: 1 addition & 0 deletions node-red/node-red.md
Original file line number Diff line number Diff line change
@@ -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":""}]
134 changes: 134 additions & 0 deletions qolsys_client/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import sys
import time
import json
import logging
from paho.mqtt.client import MQTTMessage
import qolsys_socket, mqtt_client
from mqtt_subscriber import MQTTSubscriber

################################################################################
# Code
#args = {}

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'''

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
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"

logging.debug(("publishing " + event_type + " event to: " + topic))
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.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")
help()#sys.exit()
if not "host" in args:
print("Qolsys IQ 2 Panel IP address or hostname required")
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
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()

qolsys.create_socket(hostname=host, port=port, token=token, cb=qolsys_data_received, timeout=timeout)

logging.debug("main: qolsys_socket created")

#logging.debug("doing the info check on startup")
#qolsys_status(qolsys, token)

#qolsys_arm(qolsys,token,"stay")
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 = {}
for arg in sys.argv[1:]:
arg_name = ""
arg_value = ""
arg_name = arg.split("=",1)[0]
arg_name = arg_name.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
--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()

if __name__ == "__main__":
main()
35 changes: 35 additions & 0 deletions qolsys_client/mqtt_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import paho.mqtt.client as pmqtt
import paho.mqtt.subscribe as smqtt
import json
import time
import logging

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")
published = self.client.publish(topic, message)
while not published.is_published():
time.sleep(0.5)
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

Loading

0 comments on commit f0e4889

Please sign in to comment.