Home Assistant, Raspberry PI, MQTT e Relay: Python per il Controllo e il Ripristino dello Stato

Home Assistant, Raspberry PI, MQTT e Relay: Python per il Controllo e il Ripristino dello Stato

1024 683 Nicola Montemurro

Premessa

Mi perdonerete, se il titolo riferisce a Raspberry, mentre per la programmazione è stata usata una SBC, Small Computer Board, Orange PI, modello One Plus H6, ma, mio malgrado, l’ultimo Raspberry in mio possesso ha gettato la spugna.

La soluzione si applica, comunque, a tutte le SBC, per le quali è stata scritta e/o adattata la libreria per il controllo GPIO (General Purpose, Input, Output);  RPi.GPIO per Raspberry PI, OPi.GPIO per Orange PI, etc.

Il progetto

Il progetto nasce dalla fusione di altri due precedenti, Multipresa smart MQTT integrata in Home AssistantMultipresa smart con Raspberry PI che hanno ispirato lo sviluppo di questo codice che all’occorrenza può essere esteso e adattato ad esigenze analoghe.

L'obiettivo

L’obiettivo è stato ottenere una scheda relay, comandata tramite comandi MQTT, pienamente integrata nella piattaforma Home Assistant, in grado di essere il più possibile resiliente e capace di ripristinare lo stato di esercizio al variare delle condizione dell’ambiente che si possono creare, come l’assenza di rete, il riavvio del server MQTT o il riavvio della stessa piattaforma Home Assistant.

Il codice

relayd.py - Il deamon (demone) o servizio Linux

Relayd.py è il cuore del sistema, viene eseguito all’avvio dell’ SBC e attende di ricevere messaggi dalle code MQTT relativi a ciascun relay, configurato nel un file JSON. Quando si eseguono i vari comandi in Home Assistant, per un device che utilizza MQTT, questi sono inviati alla coda alla quale il device viene “subscripted” (abbonato) per uno o più “topic” (argomento)  che riguardano il comando e il cambio di stato avvenuto a seguito del comando appena eseguito.

# ----------------------------------------------------------------------------------
# - Script file name    :       relayd..py
# - Author              :       Nicola Montemurro
# - DNS administrator   :       Nicola Montemurro  - Tel. xxx, Mobile: xxx
# - Create              :       18.01.2025
# - Last Update         :       01.02.2025
# - Description         :
# - Position            :       /usr/local/script/python
# - note                :       NON modificare senza AUTORIZZAZIONE dell'AMMINISTRATORE
#  -----------------------------------------------------------------------------------

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

'''

# PAHO Usage and API ==> https://eclipse.dev/paho/files/paho.mqtt.python/html/index.html

# MQTT Reason Code ==> https://www.emqx.com/en/blog/mqtt5-new-features-reason-code-and-ack

### ORANGPI.ONEPLUS GPIOs

# ORANGEPI.ONEPLUS IN USE
# |---------------|-----|
# | PIN     GPI   |     |
# |               |     |
# | 3   |   230   |  *  |
# | 5   |   229   |  *  |
# | 7   |   228   |     |
# | 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 OPi import sysfs
from time import sleep
from paho.mqtt import client as mqtt
from paho.mqtt.properties import Properties
from paho.mqtt.packettypes import PacketTypes
#from paho.mqtt.enums import CallbackAPIVersion, ConnackCode, LogLevel, MessageState, MessageType, MQTTErrorCode, MQTTProtocolVersion, PahoClientMode, _ConnectionState
from paho.mqtt.enums import CallbackAPIVersion, MQTTErrorCode, _ConnectionState
import argparse
import logging
from logging.handlers import RotatingFileHandler
import time
import json
import uuid
import sys
import os

GPIO.setmode(orangepi.oneplus.BOARD)
GPIO.setwarnings(False)

# client ID
cid = str(uuid.uuid4())
# mqtt client
mqttc = None
# mqttc state
isconnected = False
# json file
json_config_file = None
# json object
config=None

# channel instant states list
chan_states = []
# channel pin list
chan_pins = []
# channel name list
chan_names = []

if sys.version_info < (3,):
    class error(IOError): ...
else:
    error = OSError

def init_json():

    scriptdir = os.path.dirname(os.path.abspath(__file__))
    def_json_file = os.path.splitext(os.path.basename(__file__))[0] + '.json'

    global json_config_file

    if args.config:
        json_config_file = args.config
    else:
        json_config_file = os.path.join(scriptdir, def_json_file)

    try:
        with open(json_config_file, "r") as config_file:
            config = json.load(config_file)
    except FileNotFoundError:
        print("Content-Type: text/plain\n")
        print(f"ERROR: Configuration file not found or invalid JSON.")
        return[]

    except json.JSONDecodeError:
        print(f"ERROR: '{json_config_file}'  invalid JSON.")
        logging.error(f"Error copying {json_config_file}: ")
        return []

    return config

def validate_config(_config):

    if 'channels' not in _config:
        logging.error("Missing channels in configuration.")
        return False

    if  ('mqtt' not in _config or 'host' not in _config['mqtt'] or 'port' not in _config['mqtt'] or
        'username' not in _config['mqtt'] or 'password' not in _config['mqtt']):
        logging.error("json: Invalid mqtt configuration.")
        return False

    if  ('mqttc' not in _config or 'bind_address' not in _config['mqttc']):
        logging.error("json: Invalid mqttc configuration.")
        return False

    if 'log' not in _config or 'log_dir' not in _config['log'] or 'log_file' not in _config['log']:
        print("json: Invalid logging configuration.")
        logging.error("json: Invalid logging configuration.")
        return False

    return True

def init_logging(_config):

    if validate_config:

        log_file = os.path.join(_config['log']['log_dir'], _config['log']['log_file'])
        # File Logger
        #file_handler = RotatingFileHandler(log_file, maxBytes=10*1024*1024, backupCount=5)
        file_handler = RotatingFileHandler(log_file, _config['log']['max_size_MB']*1024*1024, _config['log']['rotate_count'])
        file_handler.setLevel(getattr(logging, args.log_level.upper(), logging.INFO))
        logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', handlers=[file_handler])
        logging.info("Logging is set up. Log file created at: {}".format(log_file))

        return True

def init_channels():

    global chan_states
    global chan_names
    global chan_pins

    for name, channel in config['channels'].items():
        pin = channel['rpi_pin']
        json_init_state = channel['rpi_init_state']

        # init lists
        chan_names.append(name)
        chan_pins.append(pin)

        if ((json_init_state == "on") or (json_init_state == "0") or (json_init_state == 0) or (json_init_state == "False")):
            # LOW = on
            GPIO.setup(pin, GPIO.OUT, GPIO.LOW)
            # init states list with channel current state
            chan_states.append(GPIO.LOW)
            #chan_states.append(0)

        elif ((json_init_state == "off") or (json_init_state == "1") or (json_init_state == 1) or (json_init_state == "True")):
            # HIGH = off
            print(str(pin))
            GPIO.setup(pin, GPIO.OUT, GPIO.HIGH)
            print(str(pin))

            # init states list with channel current state
            chan_states.append(GPIO.HIGH)
            #chan_states.append(1)

        else:
            # HIGH = off
            #GPIO.output(pin, GPIO.HIGH)
            pass

        sleep(0.1)

    return[]

def channel_on(client, pin):
    # here switch pin and SET state on list
    state = GPIO.LOW
    GPIO.output(pin, state)
    idx = chan_pins.index(pin)
    chan_states[idx] = state

    print("pin: " + str(pin) + ", state: " + str(state))

def channel_off(client, pin):
    # here switch pin and SET state on list
    state = GPIO.HIGH
    GPIO.output(pin, state)
    idx = chan_pins.index(pin)
    chan_states[idx] = state

    print("pin: " + str(pin) + ", state: " + str(state))

def on_connect(client, userdata, flags, rc, properties):
    print("function on_connect")
    logging.debug("called function function on_connect")

    global isconnected

    if flags.session_present:
        # ...
        pass
    if rc == 0:
        # success connect
        isconnected = True
        subscribe(client)
        print("Client MQTT Connected (code: " + str(rc) + ")")
        logging.info("Client MQTT Connected (code: " + str(rc) + ")")

    if rc > 0:
        # error processing
        isconnected = False
    return []

def on_connect_fail(client, userdata):
    print("Called function on_connect_fail - Userdata: " + str(userdata))
    pass

def on_disconnect(client, userdata, flags, rc, properties):
    print("Called function on_disconnect - Error: " + str(rc))
    logging.debug("Called function on_disconnect - Error: " + str(rc))

    global isconnected

    if rc == 0:
        # success disconnect
        client.loop_stop()
        client.disconnect()
        isconnected = False
        pass
    elif rc == MQTTErrorCode.MQTT_ERR_CONN_LOST:
        # error processing
        isconnected = False
    else:
        isconnected = False

    return[]

def on_publish(client, userdata, msd, rc, properties):
    print("Called function on_publish")
    logging.debug("Called function on_publish")
    print("data published \n")

    return []

def on_message(client, userdata, msg):
    print("userdata: " + str(userdata))
    print("msg: " + str(msg))
    logging.debug("Called function on_message")
    logging.info("userdata: " + str(userdata))
    logging.info("msg: " + str(msg))

    print("Received " + msg.payload.decode() + " from topic: " + msg.topic)
    logging.info("Received " + msg.payload.decode() + " from topic: " + msg.topic)

    for name, channel in config['channels'].items():
        pin = channel['rpi_pin']
        cmdtopic = channel['mqtt_msg_command_topic']
        statetopic = channel['mqtt_msg_state_topic']

        if msg.topic == cmdtopic:
            if msg.payload.decode() == "on":
                channel_on(client, pin)
            elif msg.payload.decode() == "off":
               channel_off(client, pin)
            else:
                print( "decode " + msg.topic + " failed")

    if msg.topic == "homeassistant/status":
        if msg.payload.decode() == "online":
            hastatus = True
            update_all_states_and_publish(client)

        elif msg.payload.decode() == "offline":
            hastatus = False

        else:
            print( "decode " + msg.topic + " failed")

    return []

def on_subscribe(client, userdata, mid, rc_list, properties):
    print("Called function on_subscribe")

    if rc_list[0].is_failure:
        print(f"Broker rejected the subscription: {rc_list[0]}")
        logging.debug(f"Broker rejected the subscription: {rc_list[0]}")
    else:
        print(f"Broker granted the following QoS: {rc_list[0].value}")
        logging.debug(f"Broker granted the following QoS: {rc_list[0].value}")

    return rc_list[0].value

def on_unsubscribe(client, userdata, mid, rc_list, properties):

    if len(rc_list) == 0 or not rc_list[0].is_failure:
        print("Unsubscribe succeeded (if SUBACK is received in MQTTv3 it success)")
        logging.debug("Unsubscribe succeeded (if SUBACK is received in MQTTv3 it success)")
    else:
        print(f"Broker replied with failure: {reason_code_list[0]}")
        logging.debug(f"Broker replied with failure: {reason_code_list[0]}")

    client.disconnect()

def publish(client, userdata, msg):
    print("function on_publish")
    logging.info("Called function publish")
    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("messages: %s" % (msg_count) + " sent at " + topic)
            logging.info("messages: %s" % (msg_count) + " sent at " + topic)
        else:
            print("Failed to send messages to topic " + topic)
            logging.error("Failed to send messages to topic " + topic)
        msg_count += 1

    return ret

def subscribe(client):
    print("function subscribe")
    logging.info("Called function subscribe")

    for name, channel in config['channels'].items():
        cmdtopic = channel['mqtt_msg_command_topic']
        print(cmdtopic)
        ret = client.subscribe(cmdtopic, 0)

# not implemented yet
#    for name, channel in config['channels'].items():
#        statetopic = channel['mqtt_msg_state_topic']
#        print(statetopic)
#        ret = client.subscribe(statetopic, 0)

        update_state_and_publish(client, name)

    client.subscribe("homeassistant/status", 0)

    return ret[0]

def connect_to_broker(client):
    print("function connect_to_broker")
    logging.debug("called function connect_to_broker")

    mqtt_info = config['mqtt']

    username = mqtt_info['username']
    password = mqtt_info['password']
    broker_address = mqtt_info['host']
    broker_port = mqtt_info['port']
    broker_keepalive = mqtt_info['keepalive']

    mqttc_info = config['mqttc']

    client_transport = mqttc_info['use_transport']
    client_bind_address=mqttc_info['bind_address']

    client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id = cid, protocol = mqtt.MQTTv5)

    if ((username) != "" or (password != "")):
        client.username_pw_set(username, password)
        client.user_data_set([])

    client.on_connect = on_connect
    client.on_disconnect = on_disconnect
    client.on_subscribe = on_subscribe
    client.on_message = on_message
    client.on_publish = on_publish

    ret = client.connect(host = broker_address, port = broker_port,  keepalive = broker_keepalive,
                      bind_address = client_bind_address, clean_start = mqtt.MQTT_CLEAN_START_FIRST_ONLY)

    while True:
        #if ret == mqttc.MQTT_ERR_SUCCESS:
        if ret == 0:
            if not client.is_connected:
                print("Attendo...")
                sleep(0.1)
            else:
                break
        else:
            return[]

        sleep(0.2)

    ret = client.loop_start()

        ### False = loop forever and handles the reconnecting
    #ret = client.loop_forever(retry_first_connection = True)

    print("Received the following message: {client.user_data_get()}")
    logging.info("Received the following message: {client.user_data_get()}")

    return client

def update_all_states_and_publish(client):

    i = 0
    for name, channel in config['channels'].items():
        statetopic = channel['mqtt_msg_state_topic']
        print(statetopic)

        if chan_states[i] == 0:
            state = "on"

        elif chan_states[i] == 1:
            state = "off"

        ret = client.publish(statetopic, state)

        i = i+1

    return[]

def update_state_and_publish(client, chan_name):

  #  print(config['channels'][channel]['mqtt_msg_state_topic'])

    for i in range(len(chan_states)):
        if chan_names[i] == chan_name:
            statetopic = config['channels'][chan_names[i]]['mqtt_msg_state_topic']

            if chan_states[i] == 0:
                state = "on"

            elif chan_states[i] == 1:
                state = "off"

            else:
                print("failed")

            ret = client.publish(statetopic, state)

#            print("client.publish " + statetopic + " state: " + state)

            print("Published " + state + " to topic: " + statetopic + ". Returned error: " + str(ret[0]))

    return client

def check_connection(client):
    try:
        client = connect_to_broker()
        print("connection SUCCESS")
        return True

    except:
        return False

def on_connection_lost():
    print("Event: no connection available! Calling callback...")

    global mqttc

    try:
        #mqttc = mqtt.Client.reinitialise(cid, True, None)
        mqttc = connect_to_broker(mqttc)

        # ...code here
        reconnect()
    except:
        pass

def reconnect():
    print("try to reconnect...")

    if check_connection():
        print("connection restored...")
    else:
        print("Huston, we have a problem...")

def monitor_connection(event_callback):

    while True:

        if not isconnected:
            current_connection_status = check_connection(mqttc)

        if not current_connection_status and not isconnected:
            event_callback()

        elif current_connection_status and isconnected:
            print("...connection restored")

        print ("check")
        #logging.info("check")
        sleep(5)


parser = argparse.ArgumentParser(description="Script")
parser.add_argument('--config', type=str,
                    help='Path to the JSON configuration file (default: <scriptdir/basename>.json)')
parser.add_argument('--log-level', type=str, default='INFO',
                    help='Set the logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)')
args = parser.parse_args()


def main():
    global config

    try:
        config = init_json()

        if init_logging(config):
            init_channels()

            monitor_connection(on_connection_lost)

    finally:
        GPIO.cleanup()
        print("GPIO cleanup")
        logging.info("GPIO cleanup")
        pass

if __name__ == "__main__":
    try:
        main()

    except KeyboardInterrupt:
#        raise
        pass

    finally:
        logging.shutdown

NOTA: il termine “channel” può essere fonte di confusione, in quanto a volte, si intendono oggetti diversi dello stesso contesto; infatti un “channel” può essere riferito a un gpio, un pin o un relay; in questo contesto ci riferiremo al channel come a un “canale“, che parte dal gpio (logico) => al pin (della board) riferito al gpio stesso => e al relay (collegato al pin)… channel appunto. (ndr)

relayd.json - Il file di configurazione json

{
    "channels": {
        "channel0": {
            "id": 0,
            "rpi_pin": 22,
            "rpi_gpio": 230,
            "rpi_init_state": "off",
            "mqtt_msg_state_on": "on",
            "mqtt_msg_state_off": "off",
            "mqtt_msg_state_topic": "garage/powerstrip01/channel0",
            "mqtt_msg_command_topic": "garage/powerstrip01/channel0",
            "mqtt_msg_payload_on": "on",
            "mqtt_msg_payload_off": "off"
        },
        "channel1": {
            "id": 1,
            "rpi_pin": 23,
            "rpi_gpio": 229,
            "rpi_init_state": "off",
            "mqtt_msg_payload_on": "on",
            "mqtt_msg_payload_off": "off",
            "mqtt_msg_state_on": "on",
            "mqtt_msg_state_off": "off",
            "mqtt_msg_state_topic": "garage/powerstrip01/channel1",
            "mqtt_msg_command_topic": "garage/powerstrip01/channel1"
        },
        "channel2": {
            "id": 2,
            "rpi_pin": 24,
            "rpi_gpio": 117,
            "rpi_init_state": "off",
            "mqtt_msg_payload_on": "on",
            "mqtt_msg_payload_off": "off",
            "mqtt_msg_state_on": "on",
            "mqtt_msg_state_off": "off",
            "mqtt_msg_state_topic": "garage/powerstrip01/channel2",
            "mqtt_msg_command_topic": "garage/powerstrip01/channel2"
        },
        "channel3": {
            "id": 3,
            "rpi_pin": 21,
            "rpi_gpio": 118,
            "rpi_init_state": "off",
            "mqtt_msg_payload_on": "on",
            "mqtt_msg_payload_off": "off",
            "mqtt_msg_state_on": "on",
            "mqtt_msg_state_off": "off",
            "mqtt_msg_state_topic": "garage/powerstrip01/channel3",
            "mqtt_msg_command_topic": "garage/powerstrip01/channel3"
        },
        "channel4": {
            "id": 4,
            "rpi_pin": 10,
            "rpi_gpio": 67,
            "rpi_init_state": "off",
            "mqtt_msg_payload_on": "on",
            "mqtt_msg_payload_off": "off",
            "mqtt_msg_state_on": "on",
            "mqtt_msg_state_off": "off",
            "mqtt_msg_state_topic": "garage/powerstrip01/channel4",
            "mqtt_msg_command_topic": "garage/powerstrip01/channel4"
        },
        "channel5": {
            "id": 5,
            "rpi_pin": 8,
            "rpi_gpio": 69,
            "rpi_init_state": "off",
            "mqtt_msg_payload_on": "on",
            "mqtt_msg_payload_off": "off",
            "mqtt_msg_state_on": "on",
            "mqtt_msg_state_off": "off",
            "mqtt_msg_state_topic": "garage/powerstrip01/channel5",
            "mqtt_msg_command_topic": "garage/powerstrip01/channel5"
        },
        "channel6": {
            "id": 6,
            "rpi_pin": 5,
            "rpi_gpio": 64,
            "rpi_init_state": "off",
            "mqtt_msg_payload_on": "on",
            "mqtt_msg_payload_off": "off",
            "mqtt_msg_state_on": "on",
            "mqtt_msg_state_off": "off",
            "mqtt_msg_state_topic": "garage/powerstrip01/channel6",
            "mqtt_msg_command_topic": "garage/powerstrip01/channel6"
        },
        "channel7": {
            "id": 7,
            "rpi_pin": 3,
            "rpi_gpio": 121,
            "rpi_init_state": "off",
            "mqtt_msg_payload_on": "on",
            "mqtt_msg_payload_off": "off",
            "mqtt_msg_state_on": "on",
            "mqtt_msg_state_off": "off",
            "mqtt_msg_state_topic": "garage/powerstrip01/channel7",
            "mqtt_msg_command_topic": "garage/powerstrip01/channel7"
        }
    },
    "mqtt": {
        "host": "10.150.110.16",
        "port": 1883,
        "keepalive": 10,
        "username": "",
        "password": ""
    },
    "mqttc": {
        "bind_address": "0.0.0.0",
        "use_transport": "tcp"
    },
    "log": {
        "log_dir": "/usr/local/scripts/python",
        "log_file": "relayd.log",
        "rotate_count": 5,
        "max_size_MB": 2
    }
}

Script Powerstrip.py

# ----------------------------------------------------------------------------------
# - Script file name    :       powerstrip..py
# - Author              :       Nicola Montemurro
# - DNS administrator   :       Nicola Montemurro  - Tel. xxx, Mobile: xxx
# - Create              :       21.01.2025
# - Last Update         :       24.01.2025
# - Description         :
# - Position            :       /usr/local/script/python
# - note                :       NON modificare senza AUTORIZZAZIONE dell'AMMINISTRATORE
#  -----------------------------------------------------------------------------------

#!/usr/bin/env python
# -*- coding: utf-8 -*-

# paho info => https://eclipse.dev/paho/files/paho.mqtt.python/html/index.html

#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
#
### ORANGPI.ONEPLUS GPIOs
#
# ORANGEPI.ONEPLUS  IN USE
# |----------------|-----|
# |  PIN |   GPIO  |     |
# |      |         |     |
# |  3   |   230   |  *  |
# |  5   |   229   |  *  |
# |  7   |   228   |     |
# |  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
from paho.mqtt import client as mqtt
import argparse
import uuid
import sys

# Relays GPIO numbers
pins = [ 22, 23, 24, 21, 10, 8, 5, 3 ]
gpios = [ 230, 229, 117, 118, 67, 69, 64, 121 ]
channels = [ "channel0", "channel1", "channel2", "channel3", "channel4", "channel5", "channel6", "channel7"]

# MQTT broker settings
broker_address = '10.150.110.16'
broker_port = 1883
username = ""
password = ""

topic = "garage/powerstrip01"

if sys.version_info < (3,):
    class error(IOError): ...
else:
    error = OSError

GPIO.setmode(orangepi.oneplus.BOARD)
GPIO.setwarnings(False)

isconnected = False
mqttc = None

def channel_on(pin):
    GPIO.setup(pin, GPIO.OUT)
    # GPIO.LOW = on
    GPIO.output(pin, GPIO.LOW) # out
    #print("channel on pin " + str(pin) + " GPIO LOW")
    sleep(0.01)

def channel_off(pin):
    GPIO.setup(pin, GPIO.OUT)
    # GPIO.HIGH = off
    GPIO.output(pin, GPIO.HIGH)
    #print("channel on pin " + str(pin) + " GPIO HIGH")
    sleep(0.01)

def gpio_control_channel(pin, cstate):
    try:
        if cstate == 'on':
            channel_on(pin)
        elif  cstate == 'off':
            channel_off(pin)
        else:
            print("error")
    except:
        pass

def connect_to_broker():

    ## code for both version MQTTv3 and MQTTv5
    def on_connect(client, userdata, flags, rc, properties):
        global isconnected               #Use global variable

        if rc == 0:
            print("Client MQTT Connected")
            isconnected = True               #Signal connection

    ## code for both version MQTTv3 and MQTTv5
    def on_disconnect(client, userdata, flags, rc, properties):
        global isconnected               #Use global variable

        isconnected = False
        print("Client MQTT Disconnected")

    client_id = str(uuid.uuid4())

    #mqttc = mqtt.Client(client_id=client_id, clean_session=True, userdata=None, transport="tcp")
    mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=client_id, clean_session=True, userdata=None, transport="tcp")

#    client.username_pw_set(username, password)
    if ((username) != "" or (password != "")):
        client.username_pw_set(username, password)
        mqttc.user_data_set([])

    mqttc.on_connect = on_connect
    mqttc.on_publish = on_publish
    mqttc.on_disconnect = on_disconnect
    mqttc.connect(broker_address, broker_port)

    global isconnected

    mqttc.loop_start()

    while not isconnected:    #Wait for connection
        sleep(0.01)

    return mqttc

def disconnect_from_broker(client):
    client.loop_stop()
    client.disconnect(client)

def publish(topic, msg, qos=0, retain=False, properties=None):
    print("function publish")
    pass

## code for both version MQTTv3 and MQTTv5
def on_publish(client, userdata, mid, reason_codes, properties):            #create function for callback
    print("data published")
    print("on_publish, mid {}".format(mid))
    #log.debug("on_publish, mid {}".format(mid))
    pass

# mode 1
def runmode_mqttc_none(ch, cstate):
    for i in range(len(ch)):
        gpio_control_channel(pins[ch[i]], cstate)
        sleep(0.01)

# mode 2
def runmode_mqttc_notify(ch, cstate):
    try:
        for i in range(len(ch)):
            gpio_control_channel(pins[ch[i]], cstate)
            sleep(0.01)
        
        client = connect_to_broker()
        for i in range(len(ch)):
            if isconnected:
                fulltopic = (topic + "/" + str(channels[ch[i]]))
                print(fulltopic)
                client.publish(fulltopic, cstate, 0)  ### qos MUST be 0
                sleep(0.01)

        disconnect_from_broker(client)

    except error as exc:
        print('Error: ' + str(exc))

# mode 3
def runmode_mqttc_request(ch, cstate):
    try:
        client = connect_to_broker()

        if isconnected:
            for i in range(len(ch)):
                print(len(ch))
                fulltopic = (topic + "/" + str(channels[ch[i]]))
                print(fulltopic)
                client.publish(fulltopic, cstate, 0) ### qos MUST be 0
                sleep(0.01)

        disconnect_from_broker(client)
    except error as exc:
        print('Error: ' + str(exc))

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, 4, 5, 6, 7], nargs="+", required=True)
parser.add_argument("-s", "--state", choices=["on", "off"], required=True)

args=parser.parse_args()

mode=(args.mode)
state=(args.state)
channel=(args.channel)

def run():

    if mode == [1]:
        print("runmode mode 1")
        runmode_mqttc_none(channel, state)

    if mode == [2]:
        print("runmode mode 2")
        runmode_mqttc_notify(channel, state)

    if mode == [3]:
        print("runmode mode 3")
        runmode_mqttc_request(channel, state)

def main():
    try:
        run()
#    except error as exc:
#        print('Error: ' + str(exc))

    finally:
        pass

if __name__ == "__main__":
    try:
        main()

    finally:
        ###GPIO.cleanup()
        pass
# ----------------------------------------------------------------------------------
# - Script file name    :       remote-powerstrip.py
# - Author              :       Nicola Montemurro
# - DNS administrator   :       Nicola Montemurro  - Tel. xxx, Mobile: xxx
# - Create              :       24.01.2025
# - Last Update         :       26.01.2025
# - Description         :
# - Position            :       E:\Progetti\software\scripts\python\orangepi.oneplus\release
# - note                :       NON modificare senza AUTORIZZAZIONE dell'AMMINISTRATORE
#  -----------------------------------------------------------------------------------

#!/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
#
### ORANGPI.ONEPLUS GPIOs
#
# ORANGEPI.ONEPLUS  IN USE
# |----------------|-----|
# |  PIN |   GPIO  |     |
# |      |         |     |
# |  3   |   230   |  *  |
# |  5   |   229   |  *  |
# |  7   |   228   |     |
# |  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   |     |
# |     |          |     |
# |----------------|-----|
#

from time import sleep
from paho.mqtt import client as mqtt
import argparse
import uuid
import sys

# Relays GPIO numbers
pins = [ 22, 23, 24, 21, 10, 8, 5, 3 ]
gpios = [ 230, 229, 117, 118, 67, 69, 64, 121 ]
channels = [ "channel0", "channel1", "channel2", "channel3", "channel4", "channel5", "channel6", "channel7"]

# MQTT broker settings
broker_address = '10.150.110.16'
broker_port = 1883
username = ""
password = ""

topic = "garage/powerstrip01"

if sys.version_info < (3,):
    class error(IOError): ...
else:
    error = OSError

isconnected = False
mqttc = None

def connect_to_broker():
    ## code for both version MQTTv3 and MQTTv5
    def on_connect(client, userdata, flags, rc, properties):
        global isconnected               #Use global variable

        if rc == 0:
            print("Client MQTT Connected")
            isconnected = True               #Signal connection

    ## code for both version MQTTv3 and MQTTv5
    def on_disconnect(client, userdata, flags, rc, properties):
        global isconnected               #Use global variable

        isconnected = False
        print("Client MQTT Disconnected")

    client_id = str(uuid.uuid4())

    mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=client_id, clean_session=True, userdata=None, transport="tcp")

#    client.username_pw_set(username, password)

    if ((username) != "" or (password != "")):
        client.username_pw_set(username, password)
        mqttc.user_data_set([])

    mqttc.on_connect = on_connect
    mqttc.on_publish = on_publish
    mqttc.on_disconnect = on_disconnect
    mqttc.connect(broker_address, broker_port)

    global isconnected

    mqttc.loop_start()

    while not isconnected:    #Wait for connection
        sleep(0.01)

    return mqttc

def disconnect_from_broker(client):
    client.loop_stop()
    client.disconnect(client)

def publish(topic, msg, qos=0, retain=False, properties=None):
    print("function publish")
    pass

## code for both version MQTTv3 and MQTTv5
def on_publish(client, userdata, mid, reason_codes, properties):            #create function for callback
    print("data published")
    print("on_publish, mid {}".format(mid))
    #log.debug("on_publish, mid {}".format(mid))
    pass

# mode 3
def runmode_mqttc_request(ch, cstate):
    try:
        client = connect_to_broker()
        if isconnected:
            for i in range(len(ch)):
                print(len(ch))
                fulltopic = (topic + "/" + str(channels[ch[i]]))
                print(fulltopic)
                client.publish(fulltopic, cstate, 0)
                sleep(0.01)

        disconnect_from_broker(client)
    except error as exc:

        print('Error: ' + str(exc))

import argparse

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('-ch', '--channel', type=int, choices=[ 0, 1, 2, 3, 4, 5, 6 , 7 ], nargs='+', required=True)
    parser.add_argument('-s', '--state', choices=['on', 'off'], required=True)

    args=parser.parse_args()

    channel=(args.channel)
    state=(args.state)

def run():
    runmode_mqttc_request(channel, state)

if __name__ == '__main__':
    run()

set-relayd-service.sh, creare il file per l'avvio del servizio

relay-service.sh

#!/bin/bash

while read -r line
do
        echo "$line" >> /etc/systemd/system/relayd.service

done << EOF

[Unit]
Description=relayd python service
After=multi-user.target

[Service]
Type=simple
ExecStart=/usr/bin/python3 /usr/local/scripts/python/relayd.py
RemainAfterExit=yes
KillSignal=SIGINT

[Install]
WantedBy=multi-user.target

    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.