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

Background socket and mqtt #1

Merged
merged 7 commits into from
Mar 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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