Avendo la necessità di raffinare gestione del primo prototipo di multipresa (ciabatta) smart gia vista in questo articolo, che pur funzionando perfettamente, non può certo definirsi “user friendly”, mi sono messo alla ricerca di qualcosa di appropriato nei vari cassetti.
La scelta è ricaduta su una SBC OrangePi One Plus, acquistata come alternativa a Raspberry, nel periodo in cui non se ne trovava alcuno, se non a prezzi esagerati. Si tratta di una scheda, dotata di processore ARM Quad-core 64-bit (per info Orange Pi One Plus), che non ho mai avuto modo di usare, quindi quale occasione migliore per farlo; ho poi utilizzato un modulo a 4 relay, sufficienti a ricreare un ambito quanto più vicino alle condizioni d’uso reale.
Occorre, adesso, installare il sistema operativo Debian, Python versione 3, il modulo OPi.GPIO, package alternativo all’originale RPi.GPIO per Raspberry PI, installare Paho MQTT Client, e gli altri, eventuali, packages necessari, es. NTP o Chrony, per la gestione dell’orario corretto, il broker MQTT (Message Queue Telemetry Transport), può essere installato sulla SBC stessa, se non si intende utilizzare una versione su server indipendente, oppure come modulo di Home Assistant. La programmazione, l’ho eseguita direttamente con VI l’editor di testo caratteristico dei sistemi *nix. La prima parte del programma prevede un daemon che svolge la funzione di server, ovvero viene avviato col sistema operativo e resta permanentemente esecuzione in attesa, sulla coda, di ricevere messaggi MQTT per poi effettuare lo “switch” dei segnali sulle porte GPIO. Il daemon ha, infatti, la funzione di subscriber per ricevere i messaggi relativi ai cambi di stato dalla coda MQTT, inviati da Home Assistant, e contemporaneamente è anche publisher per notificare ad Home Assistant i nuovi stati.
Il daemon, può ricevere messaggi per il cambio di stato anche dalla seconda parte del programma (script ‘powerstrip.py’, indicato in seguito) basato su tre modalità distinte di funzionamento.
- Eseguito sulla SBC, in modo indipendente, per un azionamento “certo” (effettua lo switch diretto dei segnali), indipendente dalla presenza delle parti esterne senza alcuna notifica .
- Eseguito sulla SBC, in modo indipendente, per un azionamento “certo” (effettua lo switch diretto dei segnali) con successiva notifica al broker MQTT.
- Eseguito da un qualsiasi host di rete, che supporti Python, per “richiedere” il cambio di stato mediante un messaggio MQTT, che sarà, poi, eseguito dal server (daemon).
La combinazione di queste modalità, con la presenza o meno di tutte le parti fi qui menzionate, permette un ampio ventaglio di utilizzi.
Si va dalla gestione semplice e “diretta”, manuale o pianificata Crontab, fino alla gestione, attraverso il broker MQTT esterna alla SBC, con la notifica alla piattaforma Home Assistant, esecuzione manuale oppure pianificata Crontab o Unità di pianificazione Windows, oppure, ancora, pianificata Home Assistant.
Sicuramente, rispetto alla prima versione realizzata, questa rappresenta un salto quantico e può essere considerata automazione smart e/o domotica.
Daemon 'relayd.py'
#!/usr/bin/env python3 # -*- coding: utf-8 -*- ''' PAHO Usage and API ==> https://pypi.org/project/paho-mqtt/#on-connect MQTT Reason Code ==> https://www.emqx.com/en/blog/mqtt5-new-features-reason-code-and-ack ### ORANGPI.ONEPLUS GPIOs ORANGEPI.ONEPLUS --------------- PIN GPIO | | 3 | 230 | 5 | 229 | 8 | 117 | 10 | 118 | 11 | 120 | 12 | 73 | 13 | 119 | 15 | 122 | 16 | 72 | 18 | 71 | 19 | 66 | 21 | 67 | 22 | 121 | 23 | 64 | 24 | 69 | 26 | 227 | | | --------------- ''' import orangepi.oneplus from OPi import GPIO from time import sleep import paho.mqtt.client as mqtt # Relays GPIO numbers pins = [ 12, 16, 18, 26 ] gpios = [ 73, 72, 71, 227 ] channels = [ "CHANNEL0", "CHANNEL1", "CHANNEL2", "CHANNEL3" ] # MQTT broker settings broker_address = "10.15.8.1" broker_port = 1883 bind_address = "10.15.8.12" ### localhost IP (127.0.0.1), local IP (es. 10.11.12.13), all IPs (0.0.0.0) username = "" password = "" topic = "garage/powerstrip01" GPIO.setmode(orangepi.oneplus.BOARD) GPIO.setwarnings(False) mqttc = None mqttc_isconnected = False def init(channel, state): try: GPIO.setup(channel, GPIO.OUT, state) except: # print("channel already registered") pass else: # print("channel registered") pass def channel_on(channel): GPIO.output(channel, GPIO.LOW) # out # print("GPIO LOW") def channel_off(channel): GPIO.output(channel, GPIO.HIGH) # out # print("GPIO HIGH") def channel_state(channel): if GPIO.input(channel) == 0: return "on" else: return "off" def on_publish(client, userdata, result): #create function for callback print("data published \n") #pass def on_message(client, userdata, msg): # print("Received " + msg.payload.decode() + " from topic: " + msg.topic) #pass for i in range(len(pins)): fulltopic = topic + "/" + str(channels[i]) if msg.topic == fulltopic: if msg.payload.decode() == "on": init(pins[i], GPIO.LOW) channel_on(pins[i]) elif msg.payload.decode() == "off": init(pins[i], GPIO.HIGH) channel_off(pins[i]) else: print ("failed") def mqtt_connect(): def on_connect(client, userdata, flags, rc): global mqttc_isconnected if rc == 0: # client.connected_flag = True mqttc_isconnected = True print("Client MQTT Connected (code: " + str(rc) + ")") else: mqttc_isconnected = False print("Client MQTT Connected (code: " + str(rc) + ")") # client.bad_connection_flag = True subscribe(client) def on_disconnect(client, userdata, rc): global mqttc_isconnected if rc != 0: mqttc_isconnected = False print("Client MQTT Disconnected (code: " + str(rc) + ")") client = mqtt.Client(client_id=None, clean_session=True, userdata=None, transport="tcp") client.on_connect = on_connect client.on_message = on_message client.on_publish = on_publish client.on_disconnect = on_disconnect if ((username) != "" or (password != "")): client.username_pw_set(username, password) client.connect(broker_address, port=broker_port, keepalive=60, bind_address=bind_address) client.loop_forever(retry_first_connection=True) return client def mqtt_disconnect(client): client.loop_stop() client.disconnect(client) def publish(client, userdata, mid): msg_count = 0 while True: sleep(0.5) msg = "messages: %s" % (msg_count) ret = client.publish(topic, msg) # ret: [0, 1] state = ret[0] if state == 0: print("Message sent") print("Message sent") else: print("Failed to send message to topic " + topic) msg_count += 1 def subscribe(client): for i in range(len(pins)): fulltopic = topic + "/" + str(channels[i]) client.subscribe(fulltopic) def run(): ### if broker is not available, will loop for a while while True: try: mqttc = mqtt_connect() except: # raise pass if mqttc_isconnected == True: break sleep(0.5) run()
Script 'powerstrip.py'
#!/usr/bin/env python # -*- coding: utf-8 -*- ''' RUN MODE: 1: MQTT_NONE = Direct Control GPIO by OPI.GPIO 2: MQTT_NOTIFY = Do Direct Control GPIO by OPI.GPIO With MQTT Notify 3: MQTT_REQUEST = Require GPIO control to deamon by MQTT Notify ARRAY PINS: each element is the PIN number used to manage the relays (the elements count MUST BE the same of GPIOS array) ARRAY GPIOS: each element is the GPIO number corresponding the PIN (the elements count MUST BE the same of PINS array) ARRAY CHANNELS: Used as LABEL in the code (the elements count MUST BE the same of PINS and GPIOS arrays) GPIO mode (setmode): In OPi.GPIO version the mode are only type BOARD, inside the file "oneplus" mode is set as BCM=BOARD ''' import orangepi.oneplus from OPi import GPIO from time import sleep import paho.mqtt.client as mqtt # Relays GPIO numbers pins = [ 12, 16, 18, 26 ] gpios = [ 73, 72, 71, 227 ] channels = [ "CHANNEL0", "CHANNEL1", "CHANNEL2", "CHANNEL3" ] # MQTT broker settings broker_address = '10.15.8.1' broker_port = 1883 #username = '****' #password = '****' topic = "garage/powerstrip01" GPIO.setmode(orangepi.oneplus.BOARD) GPIO.setwarnings(False) isconnected = False def init(channel, state): try: GPIO.setup(channel, GPIO.OUT, state) except: print("channel already registered") pass else: print("channel registered") pass def channel_on(channel): GPIO.output(channel, GPIO.LOW) # out # print("GPIO LOW") def channel_off(channel): GPIO.output(channel, GPIO.HIGH) # out # print("GPIO HIGH") def channel_state(channel): if GPIO.input(channel) == 0: return "on" else: return "off" def gpio_control_channel(pin, chanstate): if chanstate == 'on': GPIO.setup(pin, GPIO.OUT) channel_on(pin) elif chanstate == 'off': GPIO.setup(pin, GPIO.OUT) channel_off(pin) else: print("error") def on_publish(client,userdata,result): #create function for callback #print("data published \n") pass def mqtt_connect(): def on_connect(client, userdata, flags, rc): global isconnected #Use global variable if rc == 0: print("Client MQTT Connected") isconnected = True #Signal connection def on_disconnect(client, userdata, rc): global isconnected #Use global variable isconnected = False print("Client MQTT Disconnected") client = mqtt.Client(client_id=None, clean_session=True, userdata=None, transport="tcp") # client.username_pw_set(username, password) client.on_connect = on_connect client.on_publish = on_publish client.on_disconnect = on_disconnect client.connect(broker_address, broker_port) client.loop_start() global isconnected while not isconnected: #Wait for connection sleep(0.5) return client def mqtt_disconnect(client): client.loop_stop() client.disconnect(client) def publish(client, topic, msg=None, qos=0, retain=False): msg_count = 0 if isconnected == True: sleep(0.4) #msg = "messages: %s" % (msg_count) ret = client.publish(topic, msg, 1, True) # ret: [0, 1] state = ret[0] if state == 0: print("Message " + msg + " sent to topic " + topic) pass else: print("Failed to send message to topic: " + topic) msg_count += 1 def runmode_mqtt_none(): for i in range(len(chan)): gpio_control_channel(pins[i], state) sleep(0.2) def runmode_mqtt_notify(): try: mqttc = mqtt_connect() except: pass finally: for i in range(len(chan)): gpio_control_channel(pins[chan[i]], state) if isconnected: fulltopic = (topic + "/" + str(channels[chan[i]])) # print(fulltopic) publish(mqttc, fulltopic, state) sleep(0.2) if isconnected: mqtt_disconnect(mqttc) def runmode_mqtt_request(): try: mqttc = mqtt_connect() except: pass finally: if isconnected: for i in range(len(chan)): fulltopic = (topic + "/" + str(channels[chan[i]])) # print(fulltopic) publish(mqttc, fulltopic, state) sleep(0.2) mqtt_disconnect(mqttc) import argparse parser = argparse.ArgumentParser() parser.add_argument("-m", "--mode", type=int, choices=[1, 2, 3], nargs=1, required=True) parser.add_argument("-ch", "--channel", type=int, choices=[0, 1, 2, 3], nargs="+", required=True) parser.add_argument("-s", "--state", choices=["on", "off"], required=True) args=parser.parse_args() mode=(args.mode) chan=(args.channel) state=(args.state) def run(): if mode == [1]: runmode_mqtt_none() if mode == [2]: runmode_mqtt_notify() if mode == [3]: runmode_mqtt_request() run()
Script per la creazione del servizio relayd (start e stop)
#!/bin/bash while read -r line do echo "$line" >> /etc/systemd/system/relayd.service done << EOF [Unit] Description= After=multi-user.target [Service] ExecStart=/usr/bin/python3 /usr/local/scripts/python/relayd.py Type=simple [Install] WantedBy=multi-user.target EOF
Script per l'esecuzione remota (da qualsiasi OS che supporti Python)
#!/usr/bin/env python # -*- coding: utf-8 -*- ''' RUN MODE: 1: MQTT_NONE = Direct Control GPIO by OPI.GPIO 2: MQTT_NOTIFY = Do Direct Control GPIO by OPI.GPIO With MQTT Notify 3: MQTT_REQUEST = Require GPIO control to deamon by MQTT Notify ARRAY PINS: each element is the PIN number used to manage the relays (the elements count MUST BE the same of GPIOS array) ARRAY GPIOS: each element is the GPIO number corresponding the PIN (the elements count MUST BE the same of PINS array) ARRAY CHANNELS: Used as LABEL in the code (the elements count MUST BE the same of PINS and GPIOS arrays) GPIO mode (setmode): In OPi.GPIO version the mode are only type BOARD, inside the file "oneplus" mode is set as BCM=BOARD ''' from time import sleep import paho.mqtt.client as mqtt # Relays GPIO numbers pins = [ 12, 16, 18, 26 ] gpios = [ 73, 72, 71, 227 ] channels = [ "CHANNEL0", "CHANNEL1", "CHANNEL2", "CHANNEL3" ] # MQTT broker settings broker = '10.15.8.1' port = 1883 client_id = '1111' #username = '****' #password = '****' topic = "garage/powerstrip01" Connected = False def channel_on(channel): GPIO.output(channel, GPIO.LOW) # out # print("GPIO LOW") def channel_off(channel): GPIO.output(channel, GPIO.HIGH) # out # print("GPIO HIGH") def channel_state(channel): if GPIO.input(channel) == 0: return "on" else: return "off" def gpio_control_channel(pin, chanstate): if chanstate == 'on': channel_on(pin) elif chanstate == 'off': channel_off(pin) else: print("error") def on_connect(client, userdata, flags, rc): if rc == 0: print("MQTT broker Connected") global Connected #Use global variable Connected = True #Signal connection else: print("Connection failed") Connected = False #Signal connection def on_disconnect(client, userdata, rc): client.disconnect() client.loop_stop() print("MQTT broker Disconnected") def on_publish(client,userdata,result): #create function for callback #print("data published \n") pass def mqtt_connect(): client = mqtt.Client(client_id) # client.username_pw_set(username, password) client.on_connect = on_connect client.connect(broker, port) client.loop_start() global Connected while Connected != True: #Wait for connection sleep(0.5) return client def mqtt_disconnect(client): client.on_disconnect = on_disconnect client.loop_stop() client.disconnect(client) def publish(client, topic, msg=None, qos=0, retain=False): client.on_publish = on_publish msg_count = 0 if Connected == True: sleep(0.4) #msg = "messages: %s" % (msg_count) ret = client.publish(topic, msg, 1, True) # ret: [0, 1] state = ret[0] if state == 0: print("Sent " + msg + " to topic " + topic) pass else: print('Failed to send message to topic {topic}') msg_count += 1 def runmode_mqtt_request(): client = mqtt_connect() for i in range(len(chan)): fulltopic = (topic + "/" + str(channels[chan[i]])) # print(fulltopic) publish(client, fulltopic, state) sleep(0.2) mqtt_disconnect(client) import argparse if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('-ch', '--channel', type=int, choices=[0, 1, 2, 3], nargs='+', required=True) parser.add_argument('-s', '--state', choices=['on', 'off'], required=True) args=parser.parse_args() chan=(args.channel) state=(args.state) def run(): runmode_mqtt_request() if __name__ == '__main__': run()
Codice Home Assistant
mqtt: - switch: # unique_id: powerstrip01_switch object_id: ps01ch0 name: ps01ch0 state_topic: "garage/powerstrip01/CHANNEL0" command_topic: "garage/powerstrip01/CHANNEL0" qos: 2 payload_on: "on" payload_off: "off" state_on: "on" state_off: "off" optimistic: false retain: true - switch: object_id: ps01ch1 name: ps01ch1 state_topic: "garage/powerstrip01/CHANNEL1" command_topic: "garage/powerstrip01/CHANNEL1" qos: 2 payload_on: "on" payload_off: "off" state_on: "on" state_off: "off" optimistic: false retain: true - switch: object_id: ps01ch2 name: ps01ch2 state_topic: "garage/powerstrip01/CHANNEL2" command_topic: "garage/powerstrip01/CHANNEL2" qos: 2 payload_on: "on" payload_off: "off" state_on: "on" state_off: "off" optimistic: false retain: true - switch: object_id: ps01ch3 name: ps01ch3 state_topic: "garage/powerstrip01/CHANNEL3" command_topic: "garage/powerstrip01/CHANNEL3" qos: 2 payload_on: "on" payload_off: "off" state_on: "on" state_off: "off" optimistic: false retain: true
Tutto il codice, fin qui, illustrato è la prima versione, può essere, sicuramente, migliorato se non altro per quanto riguarda la formattazione del “payload” nei messaggi MQTT, anche se al momento posso considerarmi soddisfatto, in quanto si è trattato di un “esercizio di stile” soprattutto per quanto riguarda l’applicazione del protocollo MQTT e Home Assistant per i quali mi trovo alle prime esperienze.
Per completare, bene, l’opera, sarebbe necessario dare un look meno “spartano” alla parte hardware, ma che, però, sarà oggetto di un nuovo articolo.