#!/usr/bin/python3 from serial.tools import list_ports import serial import io import time import subprocess import csv import getopt import sys import os import math import paramiko from pathlib import Path import configparser import re import threading import syslog import csv import git import logging from paho.mqtt import client as mqtt_client def connect_mqtt(config, broker, port, client_id): # def on_connect(client, userdata, flags, rc): # For paho-mqtt 2.0.0, you need to add the properties parameter. def on_connect(client, userdata, flags, rc, properties): if rc == 0: print("Connected to MQTT Broker!") subscribe(client, config.get('mqtt', 'state-bitlair.subject')) subscribe(client, config.get('mqtt', 'state-djo.subject')) else: print("Failed to connect, return code %d\n", rc) # Set Connecting Client ID # For paho-mqtt 2.0.0, you need to set callback_api_version. client = mqtt_client.Client(client_id=client_id, callback_api_version=mqtt_client.CallbackAPIVersion.VERSION2) # client.username_pw_set(username, password) client.on_connect = on_connect client.connect(broker, port) return client FIRST_RECONNECT_DELAY = 1 RECONNECT_RATE = 2 MAX_RECONNECT_COUNT = 12 MAX_RECONNECT_DELAY = 60 def on_disconnect(client, userdata, rc): logging.info("Disconnected with result code: %s", rc) reconnect_count, reconnect_delay = 0, FIRST_RECONNECT_DELAY while reconnect_count < MAX_RECONNECT_COUNT: logging.info("Reconnecting in %d seconds...", reconnect_delay) time.sleep(reconnect_delay) try: client.reconnect() logging.info("Reconnected successfully!") return except Exception as err: logging.error("%s. Reconnect failed. Retrying...", err) reconnect_delay *= RECONNECT_RATE reconnect_delay = min(reconnect_delay, MAX_RECONNECT_DELAY) reconnect_count += 1 logging.info("Reconnect failed after %s attempts. Exiting...", reconnect_count) states = {} def update_spacestate(): global ser open = False for key in states: if states[key] == "open": open = True try: if open: print("Send spacestate open") ser.write(b"\n") ser.write(b"spacestate open\n"); else: print("Send spacestate closed") ser.write(b"\n") ser.write(b"spacestate closed\n") except serial.SerialException: print("Serial connection error") time.sleep(2) def subscribe(client: mqtt_client, topic): def on_message(client, userdata, msg): states[msg.topic] = msg.payload.decode() print(f"Received `{msg.payload.decode()}` from `{msg.topic}` topic") update_spacestate() client.subscribe(topic) client.on_message = on_message def read_configuration(configdir): if (configdir == ''): print("Missing configdir.") sys.exit(2) if not os.path.exists(configdir): print("Directory ", configdir, " does not exist"); sys.exit(2) configfile = Path(os.path.join(configdir, 'settings')) if not configfile.is_file(): print(configfile, " does not exist") sys.exit(2) config = configparser.ConfigParser() config.read_file(configfile.open()) expected_config_options = { 'mqtt': [ 'doorbell.subject', 'dooropen.subject', 'lockstate.subject', 'server', 'mqtt-simple' ], 'serial': [ 'device' ] } for section, options in expected_config_options.items(): for option in options: if not config.has_option(section, option): print("Missing config option ", option, "in section", section) sys.exit(2) return config def mqtt_send_thread(config, subject, value, persistent): if persistent: subprocess.call([config.get('mqtt', 'mqtt-simple'), "-h", config.get('mqtt', 'server'), "-r", "-p", subject, "-m", value]) else: subprocess.call([config.get('mqtt', 'mqtt-simple'), "-h", config.get('mqtt', 'server'), "-p", subject, "-m", value]) def mqtt(config, subject, value, persistent=False): threading.Thread(target = mqtt_send_thread, args = (config, subject, value, persistent)).start() def log(message): print("LOG " + message) syslog.syslog(message) def mqtt_thread(): global client global config client = connect_mqtt(config, config.get('mqtt', 'server'), 1883, config.get('mqtt', 'client-id')) client.loop_forever() buttons = [] def serial_monitor_thread(): global ser global config global buttons while True: try: ser = serial.Serial(config.get('serial', 'device'), 115200, rtscts=False, dsrdtr=True) time.sleep(2); print("Doorduino started"); while True: data = ser.readline() action = data.decode("iso-8859-1").strip() print("Data:" + action) if action == "Horn activated": print("Horn activated") log("Horn activated") mqtt(config, config.get('mqtt','doorbell.subject'), '1', False) time.sleep(2) mqtt(config, config.get('mqtt','doorbell.subject'), '0', False) elif action == "Solenoid activated": print("Solenoid activated") log("Solenoid activated") mqtt(config, config.get('mqtt','dooropen.subject'), '1', False) time.sleep(2) mqtt(config, config.get('mqtt','dooropen.subject'), '0', False) elif action == "iButton authenticated": print("iButton authenticated") log("iButton authenticated") elif action == "opening lock": print("lock open") log("lock open") mqtt(config, config.get('mqtt','lockstate.subject'), 'open', True) elif action == "closing lock": print("lock closed") log("lock closed") mqtt(config, config.get('mqtt','lockstate.subject'), 'closed', True) elif action[:7] == "button:": # print("got button ") # print(action[8:]) if not action[8:] in buttons: buttons.append(action[8:]) elif action == "DEBUG: Board started": print("Arduino was reset, sending spacestate") update_spacestate() except serial.SerialException: print("Serial connection error") time.sleep(2) except StopIteration: print("No device found") time.sleep(2) def git_update(git_dir): log("Updating git") # subprocess.call(["git", "-C", git_dir, "pull"]) repo = git.Repo(git_dir) # print('Remotes:') # for remote in repo.remotes: # print(f'- {remote.name} {remote.url}') try: pull = repo.remotes.origin.pull() except git.exc.GitCommandError: print("Git pull failed") return False # if pull[0].flags == 128: # print("Git pull failed") # return False return True def git_thread(): global ser global buttons print("GIT init") if(not git_update("toegang")): print("Aborting GIT update thread") return False git_buttons = {} with open('toegang/toegang.csv', newline='') as csvfile: data = csvfile.read() data = data.replace(' ', '') reader = csv.DictReader(data.splitlines(), delimiter=',') for row in reader: ibutton = row['ibutton'].split(':') git_buttons[ibutton[0].lower()] = ibutton[1].lower() # print(row['naam'] + " " + row['ibutton']) if len(git_buttons) < 25: print("Something wrong, not enough buttons in git") return buttons = [] ser.write(b"\n") ser.write(b"list_buttons\n"); time.sleep(10) if len(buttons) < 5: print("Something wrong, not enough buttons in doorduino") return print(buttons) for button in git_buttons: if button not in buttons: print("should add " + button) ser.write(b"\n") ser.write(b"add_button "+button.encode('ascii')+b" "+git_buttons[button].encode('ascii')+b"\n") time.sleep(2) # else: # print("already there " + button) for button in buttons: if button not in git_buttons: print("should remove " + button) ser.write(b"\n") ser.write(b"remove_button "+button.encode('ascii')+b"\n") time.sleep(2) # else: # print("should be there " + button) def threadwrap(threadfunc): def wrapper(): while True: try: threadfunc() except BaseException as e: print('{!r}; restarting thread'.format(e)) else: print('exited normally, bad thread; restarting') return wrapper def main(argv): global config configdir = '' syslog.openlog('doorduino') try: opts, args = getopt.getopt(argv,"c:",["config="]) except getopt.GetoptError: print('doorduino.py -c ') sys.exit(2) for opt, arg in opts: if opt == "-c" or opt == "--config": configdir = arg config = read_configuration(configdir) threading.Thread(target = threadwrap(serial_monitor_thread)).start() time.sleep(5) # Give doorduino time to start before sending spacestate data threading.Thread(target = threadwrap(mqtt_thread)).start() # threading.Thread(target = threadwrap(git_thread)).start() git_thread() if __name__ == "__main__": main(sys.argv[1:])