-
Notifications
You must be signed in to change notification settings - Fork 0
en_install_server
A complete server setup exeeds the scope of the zuul documentation, so this can be seen as a sample reference guide how to create a complete server.
As this sample is made for a company environment, it contains additional elements like access control and HTTPS connections, which you (hopefully) won't need if you run Zuul in your own home
This sample is made based on ubuntu-20.04-live-server-amd64.iso
It should also work on RaspberryOS (aka Raspian)
Install Ubuntu or RaspberryOS first. Then add some more packages with
sudo apt install mosquitto npm
sudo npm install -g --unsafe-perm node-red node-red-admin
With the same command the package can be updated, if needed.
Following https://nodered.org/docs/getting-started/raspberrypi , we execute
bash <(curl -sL https://raw.githubusercontent.com/node-red/linux-installers/master/deb/update-nodejs-and-nodered)
The installer script creates a systemd service, which is executed by the actual user in his homedir
The following commands are provided to work with the service:
- node-red-start - this starts the Node-RED service and displays its log output. Pressing Ctrl-C or closing the window does not stop the service; it keeps running in the background
- node-red-stop - this stops the Node-RED service
- node-red-restart - this stops and restarts the Node-RED service
- node-red-log - this displays the log output of the service
If you want Node-RED to run when the computer is is turned on, or re-booted, you can enable the service to autostart by running the command:
sudo systemctl enable nodered.service
To disable the service, run the command:
sudo systemctl disable nodered.service
If the web server shall run on https, we need some key file. These will be generated with
openssl req -newkey rsa:2048 -new -nodes -x509 -days 3650 -keyout key.pem -out server.pem && echo "saved as key.pem and server.pem"
Start and stop node-red to generate it's settings file
Then open the settings file
nano .node-red/settings.js
uncommend the adminAuth section
adminAuth: {
type: "credentials",
users: [{
username: "pi",
password: "$2a$08$zZWtXTja0fB1pzD4sHCMyOCMYz2Z6dNbM6tl8sJogENOMcxWV9DN.",
permissions: "*"
}]
},
adapt the https and requireHttps like this:
https: {
key: require("fs").readFileSync('/home/pi/key.pem'),
cert: require("fs").readFileSync('/home/pi/server.pem')
},
// The following property can be used to cause insecure HTTP connections to
// be redirected to HTTPS.
requireHttps: true,
To calculate your own password hash, use
node-red-admin hash-pw
create a new config file
sudo nano /etc/mosquitto/conf.d/zuul.conf
containing
# force login password
password_file /etc/mosquitto/passwd
# add websocket
# actual causes mosquitto to crash at start, so commended out
# listener 9001
# protocol websockets
and create a password file
mosquitto_passwd -c /etc/mosquitto/passwd pi
git clone https://github.com/stko/zuul-ac.git
(workaround, as the zuul server has a hardcoded key file path): Copy the key file to the server
cp *.pem zuul-ac/
Install python packages
sudo apt install python3-pip
sudo pip3 install ecdsa python-telegram-bot qrcode[pil] --upgrade
Create the systemd zuul service. Do not forget to adapt user name and path to the user name you've used during the OS installation
cat << 'EOF' | sudo tee /etc/systemd/system/zuul.service
[Unit]
Description=Zuul Telegram Bot
Wants=network.target
[Service]
User=pi
Group=pi
WorkingDirectory=/home/pi/zuul-ac/
ExecStart= /usr/bin/python3 /home/pi/zuul-ac/zuulac.py -s
Restart=always
[Install]
WantedBy=default.target
EOF
Enable zuul
sudo systemctl enable zuul
Start zuul
sudo systemctl start zuul
Configure zuul via web UI. After configuration a config file (config.json) can be found in the config subdirectory.
To protect your UI with a password, open that config.json file with a text editor and enter your password in the current_password
field.
restart zuul with
sudo systemctl restart zuul
to activate your password.
When a password is given, then
- changes through the UI will only be recocnized if the right password is given in the password field
- the Telegram API token will not shown in the UI anymore and can only be edited directly in the config file
This sample config reads the QR from the build in webcam and send a dial string to a connected modem to open the door(s).
sudo usermod -aG video $USER
sudo usermod -aG dialout $USER
sudo pip3 install paho-mqtt
log out and login again to make these changes active
sudo apt install zbar-tools
cat << 'EOF' | tee /home/pi/zuul-ac/qr_to_mqtt.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# little hack to read QR code from local webcam
# setup:
# sudo apt-get install zbar-tools
# sudo pip3 install paho-mqtt
# usage:
# zbarcam [--nodisplay] | python3 qr_to_mqtt.py
import sys
import json
import paho.mqtt.client as mqtt
def on_connect(client, userdata, flags, rc):
print("Connected with result code " + str(rc))
client = mqtt.Client()
client.username_pw_set('pi', 'YOURPASSWORD')
client.on_connect = on_connect
client.connect("localhost", 1883, 60)
client.loop_start()
for line in sys.stdin:
elements=line.strip().split(':')
if elements[0]=="QR-Code":
qrcode=":".join(elements[1:])
json_msg={'qrcode':qrcode,'doorid':sys.argv[1]}
print(line, json.dumps(json_msg))
client.publish("door/qrcode", json.dumps(json_msg))
EOF
cat << 'EOF' | sudo tee /etc/systemd/system/zuul-qr.service
[Unit]
Description=MQTT QR Code Reader
Wants=mosquitto.service
[Service]
User=pi
Group=pi
WorkingDirectory=/home/pi/zuul-ac/
ExecStart= /usr/bin/sh -c "/usr/bin/zbarcam --nodisplay | /usr/bin/python3 qr_to_mqtt.py 'yourdoorid'"
Restart=always
[Install]
WantedBy=default.target
EOF
sudo systemctl enable zuul-qr.service
cat << 'EOF' | tee /home/pi/zuul-ac/mqtt_to_serial.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# little hack to write MQTT to serial
# setup:
# sudo -H pip3 install paho-mqtt
# usage:
# python3 MQTT_to_serial.py
import sys
import paho.mqtt.client as mqtt
import serial
import time
def on_connect(client, userdata, flags, rc):
print("Connected with result code " + str(rc))
def on_message(client, userdata, message):
print("message received ",str(message.payload.decode("utf-8")), "topic",message.topic,"retained ",message.retain)
send_serial(message.payload + b'\r\n')
def send_serial(cmd):
global ser
ser.write(cmd) # write the cmd
line = ser.readline() # read a '\n' terminated line
while line:
print(line.decode().replace('\r',''), end='')
line = ser.readline() # read a '\n' terminated line
client = mqtt.Client()
client.username_pw_set('pi', 'YOURPASSWORD')
client.on_connect = on_connect
client.on_message = on_message
client.connect("localhost", 1883, 60)
client.subscribe('serial/dial')
client.loop_start()
ser= serial.Serial(sys.argv[1], 9600, timeout=1)
print(ser.name) # check which port was really used
send_serial(b'AT S7=45 L1 V1 X4 E1\r\n') # write a string
while True:
time.sleep(1)
ser.close() # close portfor line in sys.stdin:
EOF
cat << 'EOF' | sudo tee /etc/systemd/system/zuul-serial.service
[Unit]
Description=MQTT Serial Output
Wants=mosquitto.service
[Service]
User=pi
Group=pi
WorkingDirectory=/home/pi/zuul-ac/
ExecStart= /usr/bin/python3 mqtt_to_serial.py /dev/ttyUSB0
Restart=always
[Install]
WantedBy=default.target
EOF
sudo systemctl enable zuul-serial.service
cat << 'EOF' | tee /home/pi/zuul-ac/mqtt_to_console.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# little hack to write MQTT to system console
# usage:
# python3 MQTT_to_console.py
import sys
import os
import paho.mqtt.client as mqtt
import time
import json
## colors can be found here: https://stackoverflow.com/a/51708889
def on_connect(client, userdata, flags, rc):
print("Connected with result code " + str(rc))
def on_message(client, userdata, message):
#print("message received ",str(message.payload.decode("utf-8")), "topic",message.topic,"retained ",message.retain)
data=json.loads(str(message.payload.decode("utf-8")))
startcolor=data['startcolor'].replace('ESC','\x1b')
endcolor=data['endcolor'].replace('ESC','\x1b')
messagetext=data['text']
print(startcolor)
os.system("/usr/bin/banner "+ messagetext)
print(endcolor)
client = mqtt.Client()
client.username_pw_set('pi', 'YOURPASSWORD')
client.on_connect = on_connect
client.on_message = on_message
client.connect("localhost", 1883, 60)
client.subscribe('console/out')
client.loop_start()
while True:
time.sleep(1)
EOF
cat << 'EOF' | sudo tee /etc/systemd/system/zuul-console.service
[Unit]
Description=MQTT to Console
Wants=mosquitto.service
[Service]
StandardOutput=tty
User=pi
Group=pi
WorkingDirectory=/home/pi/zuul-ac/
ExecStart= /usr/bin/python3 mqtt_to_console.py
Restart=always
[Install]
WantedBy=default.target
EOF
sudo systemctl enable zuul-console.service
And this Node-Red Flow makes it all work:
[
{
"id": "ec4b3da9.4704c",
"type": "tab",
"label": "Zuul- Websocket",
"disabled": false,
"info": ""
},
{
"id": "a57cd6a1.14bb38",
"type": "websocket out",
"z": "ec4b3da9.4704c",
"name": "zuul",
"server": "",
"client": "e837f008.6ba9b",
"x": 1090,
"y": 140,
"wires": []
},
{
"id": "92c0a055.c2151",
"type": "websocket in",
"z": "ec4b3da9.4704c",
"name": "Zuul",
"server": "",
"client": "e837f008.6ba9b",
"x": 70,
"y": 240,
"wires": [
[
"ed415b1a.f7e578",
"8e5b87bd.ea3dc8"
]
]
},
{
"id": "331b8c0a.1ca674",
"type": "debug",
"z": "ec4b3da9.4704c",
"name": "",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"x": 990,
"y": 80,
"wires": []
},
{
"id": "7d1829ba.0991d8",
"type": "comment",
"z": "ec4b3da9.4704c",
"name": "Client",
"info": "",
"x": 90,
"y": 60,
"wires": []
},
{
"id": "4a1b1808.e71a28",
"type": "json",
"z": "ec4b3da9.4704c",
"name": "",
"property": "payload",
"action": "",
"pretty": false,
"x": 370,
"y": 240,
"wires": [
[
"4325f655.0e83f8",
"8e5b87bd.ea3dc8"
]
]
},
{
"id": "23160e29.073202",
"type": "change",
"z": "ec4b3da9.4704c",
"name": "Set OTP Parameters",
"rules": [
{
"t": "set",
"p": "payload.type",
"pt": "msg",
"to": "ac_otprequest",
"tot": "str"
},
{
"t": "set",
"p": "payload.config.result",
"pt": "msg",
"to": "zuul_active",
"tot": "flow"
},
{
"t": "set",
"p": "payload.config.msg",
"pt": "msg",
"to": "zuul_msg",
"tot": "flow"
},
{
"t": "set",
"p": "payload.config.type",
"pt": "msg",
"to": "qrcode",
"tot": "str"
},
{
"t": "set",
"p": "payload.config.keypadchars",
"pt": "msg",
"to": "1234567890ABCD",
"tot": "str"
},
{
"t": "set",
"p": "payload.config.length",
"pt": "msg",
"to": "10",
"tot": "num"
},
{
"t": "set",
"p": "payload.config.valid_time",
"pt": "msg",
"to": "60",
"tot": "num"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 760,
"y": 220,
"wires": [
[
"331b8c0a.1ca674",
"a57cd6a1.14bb38"
]
]
},
{
"id": "4325f655.0e83f8",
"type": "switch",
"z": "ec4b3da9.4704c",
"name": "OTP Request",
"property": "payload",
"propertyType": "msg",
"rules": [
{
"t": "jsonata_exp",
"v": "payload.type = \"otprequest\"",
"vt": "jsonata"
},
{
"t": "jsonata_exp",
"v": "payload.type = \"tokenstate\" and payload.config.valid = true",
"vt": "jsonata"
},
{
"t": "jsonata_exp",
"v": "payload.type = \"tokenstate\" and payload.config.valid = false",
"vt": "jsonata"
},
{
"t": "else"
}
],
"checkall": "false",
"repair": false,
"outputs": 4,
"x": 520,
"y": 240,
"wires": [
[
"23160e29.073202"
],
[
"8f9e515e.98f4e",
"6314622f.dc5d6c",
"1ea807e8.ca9a78"
],
[
"1931ffd3.4d8f7"
],
[]
]
},
{
"id": "ed415b1a.f7e578",
"type": "function",
"z": "ec4b3da9.4704c",
"name": "Kill Session",
"func": "msg._session = \"\";\nreturn msg;",
"outputs": 1,
"noerr": 0,
"x": 230,
"y": 240,
"wires": [
[
"4a1b1808.e71a28"
]
]
},
{
"id": "5efc758.108a58c",
"type": "inject",
"z": "ec4b3da9.4704c",
"name": "TRUE",
"repeat": "",
"crontab": "",
"once": true,
"onceDelay": 0.1,
"topic": "A",
"payload": "true",
"payloadType": "bool",
"x": 110,
"y": 540,
"wires": [
[
"107dfb8c.62def4"
]
]
},
{
"id": "d02bf871.39f4d8",
"type": "inject",
"z": "ec4b3da9.4704c",
"name": "FALSE",
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "A",
"payload": "false",
"payloadType": "bool",
"x": 110,
"y": 600,
"wires": [
[
"107dfb8c.62def4"
]
]
},
{
"id": "107dfb8c.62def4",
"type": "rbe",
"z": "ec4b3da9.4704c",
"name": "Detect Change",
"func": "rbe",
"gap": "",
"start": "",
"inout": "out",
"property": "payload",
"x": 300,
"y": 560,
"wires": [
[
"306b57e1.49fd08",
"1c2473d6.c9eccc"
]
]
},
{
"id": "306b57e1.49fd08",
"type": "change",
"z": "ec4b3da9.4704c",
"name": "zuul_active",
"rules": [
{
"t": "set",
"p": "zuul_active",
"pt": "flow",
"to": "payload",
"tot": "msg"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 730,
"y": 560,
"wires": [
[]
]
},
{
"id": "c6bc57cd.8f1ae8",
"type": "change",
"z": "ec4b3da9.4704c",
"name": "zuul_msg_Ok",
"rules": [
{
"t": "set",
"p": "zuul_msg",
"pt": "flow",
"to": "Hinein spaziert! Dein QRCode ist {0} Sekunden gültig",
"tot": "str"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 740,
"y": 600,
"wires": [
[]
]
},
{
"id": "1c2473d6.c9eccc",
"type": "switch",
"z": "ec4b3da9.4704c",
"name": "Set OTP Message",
"property": "payload",
"propertyType": "msg",
"rules": [
{
"t": "true"
},
{
"t": "false"
}
],
"checkall": "true",
"repair": false,
"outputs": 2,
"x": 510,
"y": 620,
"wires": [
[
"c6bc57cd.8f1ae8"
],
[
"bc26f0cd.c14c4"
]
]
},
{
"id": "bc26f0cd.c14c4",
"type": "change",
"z": "ec4b3da9.4704c",
"name": "zuul_msg_Denied",
"rules": [
{
"t": "set",
"p": "zuul_msg",
"pt": "flow",
"to": "Passt gerade nicht..",
"tot": "str"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 750,
"y": 640,
"wires": [
[]
]
},
{
"id": "5656393a.02e438",
"type": "template",
"z": "ec4b3da9.4704c",
"name": "Send Token",
"field": "payload",
"fieldType": "msg",
"format": "handlebars",
"syntax": "mustache",
"template": "{\"type\": \"ac_tokenquery\", \"config\":{ \"token\": \"{{{payload.qrcode}}}\", \"doorid\": \"{{{payload.doorid}}}\" } }",
"output": "json",
"x": 730,
"y": 140,
"wires": [
[
"331b8c0a.1ca674",
"a57cd6a1.14bb38",
"e9c73fda.d586c"
]
]
},
{
"id": "8f9e515e.98f4e",
"type": "trigger",
"z": "ec4b3da9.4704c",
"name": "Door Opener 3 Secs",
"op1": "ON",
"op2": "OFF",
"op1type": "str",
"op2type": "str",
"duration": "3",
"extend": false,
"units": "s",
"reset": "",
"bytopic": "all",
"outputs": 1,
"x": 760,
"y": 340,
"wires": [
[
"df79b20d.83f3a",
"8cb2f7f6.140528",
"5b4ed54a.c43e2c"
]
]
},
{
"id": "df79b20d.83f3a",
"type": "mqtt out",
"z": "ec4b3da9.4704c",
"name": "",
"topic": "cmnd/tasmota/POWER",
"qos": "",
"retain": "",
"broker": "1d164033.b808e",
"x": 1030,
"y": 340,
"wires": []
},
{
"id": "8e5b87bd.ea3dc8",
"type": "debug",
"z": "ec4b3da9.4704c",
"name": "",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"x": 510,
"y": 340,
"wires": []
},
{
"id": "f8391752.3ddb88",
"type": "mqtt in",
"z": "ec4b3da9.4704c",
"name": "",
"topic": "door/qrcode",
"qos": "2",
"datatype": "json",
"broker": "1d164033.b808e",
"x": 150,
"y": 140,
"wires": [
[
"5656393a.02e438",
"331b8c0a.1ca674"
]
]
},
{
"id": "6314622f.dc5d6c",
"type": "change",
"z": "ec4b3da9.4704c",
"name": "Filter Door-ID",
"rules": [
{
"t": "set",
"p": "payload",
"pt": "msg",
"to": "payload.config.msg.doorid",
"tot": "msg"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 740,
"y": 480,
"wires": [
[
"9d550ee6.4a84d",
"5b4ed54a.c43e2c"
]
]
},
{
"id": "9d550ee6.4a84d",
"type": "mqtt out",
"z": "ec4b3da9.4704c",
"name": "Modem",
"topic": "serial/dial",
"qos": "",
"retain": "",
"broker": "1d164033.b808e",
"x": 980,
"y": 480,
"wires": []
},
{
"id": "5b4ed54a.c43e2c",
"type": "debug",
"z": "ec4b3da9.4704c",
"name": "",
"active": false,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"x": 1130,
"y": 560,
"wires": []
},
{
"id": "8cb2f7f6.140528",
"type": "ui_led",
"z": "ec4b3da9.4704c",
"group": "a681ccbf.f47c8",
"order": 0,
"width": 0,
"height": 0,
"label": "Tür-Öffner",
"labelPlacement": "left",
"labelAlignment": "left",
"colorForValue": [
{
"color": "green",
"value": "ON",
"valueType": "str"
},
{
"color": "grey",
"value": "OFF",
"valueType": "str"
}
],
"allowColorForValueInMessage": false,
"name": "",
"x": 980,
"y": 440,
"wires": []
},
{
"id": "d831a280.a5dfa",
"type": "mqtt out",
"z": "ec4b3da9.4704c",
"name": "",
"topic": "console/out",
"qos": "",
"retain": "",
"broker": "1d164033.b808e",
"x": 1250,
"y": 260,
"wires": []
},
{
"id": "1931ffd3.4d8f7",
"type": "change",
"z": "ec4b3da9.4704c",
"name": "Leider abgelehnt",
"rules": [
{
"t": "set",
"p": "payload",
"pt": "msg",
"to": "{\"startcolor\":\"ESC[41m\",\"text\":\"Leider abgelehnt\",\"endcolor\":\"ESC[49m\"}",
"tot": "json"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 750,
"y": 300,
"wires": [
[
"d831a280.a5dfa",
"542ada55.a5bb24"
]
]
},
{
"id": "1ea807e8.ca9a78",
"type": "change",
"z": "ec4b3da9.4704c",
"name": "Tuer geht auf",
"rules": [
{
"t": "set",
"p": "payload",
"pt": "msg",
"to": "{\"startcolor\":\"ESC[42m\",\"text\":\"Tuer oeffnet sich in ein paar Sekunden\",\"endcolor\":\"ESC[49m\"}",
"tot": "json"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 730,
"y": 260,
"wires": [
[
"d831a280.a5dfa",
"542ada55.a5bb24"
]
]
},
{
"id": "e9c73fda.d586c",
"type": "change",
"z": "ec4b3da9.4704c",
"name": "Code wird geprueft",
"rules": [
{
"t": "set",
"p": "payload",
"pt": "msg",
"to": "{\"startcolor\":\"ESC[104m\",\"text\":\"Code wird geprueft\",\"endcolor\":\"ESC[49m\"}",
"tot": "json"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 1030,
"y": 220,
"wires": [
[
"d831a280.a5dfa",
"542ada55.a5bb24"
]
]
},
{
"id": "542ada55.a5bb24",
"type": "debug",
"z": "ec4b3da9.4704c",
"name": "",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"x": 1290,
"y": 340,
"wires": []
},
{
"id": "e837f008.6ba9b",
"type": "websocket-client",
"z": "",
"path": "wss://localhost:8000/ws/requests",
"tls": "5291fdc.dc4a604",
"wholemsg": "false"
},
{
"id": "1d164033.b808e",
"type": "mqtt-broker",
"z": "",
"name": "docker mqtt",
"broker": "localhost",
"port": "1883",
"clientid": "",
"usetls": false,
"compatmode": false,
"keepalive": "60",
"cleansession": true,
"birthTopic": "",
"birthQos": "0",
"birthPayload": "",
"closeTopic": "",
"closeQos": "0",
"closePayload": "",
"willTopic": "",
"willQos": "0",
"willPayload": ""
},
{
"id": "a681ccbf.f47c8",
"type": "ui_group",
"z": "",
"name": "Tür",
"tab": "4cb4268d.fe44f8",
"order": 1,
"disp": true,
"width": "6",
"collapse": false
},
{
"id": "5291fdc.dc4a604",
"type": "tls-config",
"z": "",
"name": "",
"cert": "",
"key": "",
"ca": "",
"certname": "",
"keyname": "",
"caname": "",
"servername": "",
"verifyservercert": false
},
{
"id": "4cb4268d.fe44f8",
"type": "ui_tab",
"z": "",
"name": "Company",
"icon": "dashboard",
"disabled": false,
"hidden": false
}
]