Multipresa smart MQTT integrata in Home Assistant

Multipresa smart MQTT integrata in Home Assistant

1920 1067 Nicola Montemurro

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.

    Preferenze Privacy

    Quando visiti il nostro sito web, possono essere memorizzate alcune informazioni, di servizi specifici, tramite il tuo browser, di solito sotto forma di cookie. Qui puoi modificare le tue preferenze sulla privacy. Il blocco di alcuni cookie può influire sulla tua esperienza sul nostro sito Web e sui servizi che offriamo.

    Click to enable/disable Google Analytics tracking code.
    Click to enable/disable Google Fonts.
    Click to enable/disable Google Maps.
    Click to enable/disable video embeds.
    Il nostro sito web utilizza cookie, principalmente di terze parti. Personalizza le preferenze sulla privacy e/o acconsenti all'utilizzo dei cookie.