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.