Moduł ma być na tyle mały aby można go było umieścić w puszcze instalacyjnej pod włącznikiem. Jego zadanie polega na sterowaniu domowymi odbiornikami takimi jak oświetlenie czy żaluzje okienne. Sterować nimi możemy manualnie (włączniki naścienne) i zdalnie przez przewodową magistrale Can w ramach sieci inteligentnego domu. W moim przypadku jest to system Home Assistant postawiony na miniaturowym komputerze Rasperry Pi.
Mój moduł współpracuje i wymaga modułu wykonawczego 2SSR systemu GetWired firmy Domatic (Wiki). Razem z nim tworzy kompletny i gotowy do pracy zestaw.
Strona ta będzie systematycznie rozbudowywana i służyła jako dokumentacja do w/w modułu.











Tak jak już napisałem zarządza tymi modułami Home Assistant za pomocą protokołu MQTT. HA jest uruchomiony na platformie Raspberry Pi. Do tego mikrokomputera dołożyłem nakładkę RS485 CAN HAT, która sprzętowo załatwiła komunikację CAN po stronie naszego serwera.
Podobna polska nakładka KAmodRPi RS485 CAN HAT.


Na RPi jako daemon processes pracuje odpowiedni skrypt w Python-nie, który realizuje bramkę pomiędzy protokołom Can a MQTT. Stworzyłem prosty protokuł-ramkę CAN do komunikacji o rozmiarze 3-4 bajtów.
#!/usr/bin/python # -*- coding: utf-8 -*- """ File: demon_can.py Title: bramka Can2Mqtt """ import logging import logging.handlers import argparse import sys import time import os import can import paho.mqtt.client as mqtt CAN_KIER_PRZY = 0 CAN_KIER_WYCH = 1 CAN_FUN_ROLETA = 1 CAN_FUN_SWITCH1 = 2 CAN_FUN_SWITCH2 = 3 CAN_FUN_SEN_BIN_1 = 4 CAN_FUN_SEN_BIN_2 = 5 CAN_FUN_ROLETA_POZ = 6 CAN_FUN_SEN_PRAD = 21 CAN_FUN_KONF_ID = 100 CAN_FUN_KONF_TYP = 101 CAN_FUN_KONF_ZAP_CZAS = 102 CAN_FUN_KONF_KALIBRACJA = 103 DATA_VAL_OFF = 0 DATA_VAL_ON = 1 DATA_VAL_OPEN = 0 DATA_VAL_CLOSE = 1 DATA_VAL_STOP = 255 LOG_FILENAME = "/home/pi/py/can/demon_can.log" LOG_LEVEL = logging.INFO # Could be e.g. "DEBUG" or "WARNING" parser = argparse.ArgumentParser(description="My simple Python service") parser.add_argument("-l", "--log", help="file to write log to (default '" + LOG_FILENAME + "')") args = parser.parse_args() if args.log: LOG_FILENAME = args.log logger = logging.getLogger(__name__) logger.setLevel(LOG_LEVEL) handler = logging.handlers.TimedRotatingFileHandler(LOG_FILENAME, when="W0") formatter = logging.Formatter('%(asctime)s %(levelname)-8s %(message)s') handler.setFormatter(formatter) logger.addHandler(handler) class MyLogger(object): def __init__(self, logger, level): """Needs a logger and a logger level.""" self.logger = logger self.level = level def write(self, message): if message.rstrip() != "": self.logger.log(self.level, message.rstrip()) sys.stdout = MyLogger(logger, logging.INFO) sys.stderr = MyLogger(logger, logging.ERROR) i = 0 topic_root = 'DataSoft/' topic_can = 'DataSoft/can' topic_roleta = '/roleta' topic_switch = '/switch' topic_roleta_pos = '/roleta_pos' print("Start demon_can.py") can0 = can.interface.Bus(channel = 'can0', bustype = 'socketcan_ctypes')# socketcan_native logger = logging.getLogger(__name__) def on_connect(mqttc, obj, flags, rc): print("rc: " + str(rc)) def IDFind(topic): i = topic.rfind(topic_can)+len(topic_can) n = '' while i>-1 and topic[i].isnumeric(): n=n+topic[i] i += 1 if len(n)>0: return int(n) else: return 0 def on_message(mqttc, obj, msg): print "=> MQTT: {} - {}".format(msg.topic, msg.payload) if (msg.topic.find(topic_can) > -1) and (msg.topic.find('/set') > -1): id = IDFind(msg.topic) if id < 1: return if msg.topic.find(topic_roleta) > -1: # roleta if msg.payload == "STOP": v = 255 elif msg.payload == "ON": v = 1 else : v = 0 cm = can.Message(arbitration_id=0x0100+id, data=[0, CAN_FUN_ROLETA, v], extended_id=False) # 0 -> do STM32 1 -> roleta can0.send(cm) print("CANr => "+str(cm)) if msg.topic.find(topic_switch) > -1: # włącznik uint8_t = sw = 9 v = 9; if msg.payload == "ON": sw = CAN_FUN_SWITCH1 v = 1 if msg.payload == "OFF": sw = CAN_FUN_SWITCH1 v = 0 if msg.payload == "ON2": sw = CAN_FUN_SWITCH2 v = 1 if msg.payload == "OFF2": sw = CAN_FUN_SWITCH2 v = 0 cm = can.Message(arbitration_id=0x0100+id, data=[0, sw, v], extended_id=False) # 0 -> do STM32 2 -> switch can0.send(cm) print("CANs => "+str(cm)) if msg.topic.find(topic_roleta_pos) > -1: # v = int(msg.payload) cm = can.Message(arbitration_id=0x0100+id, data=[0, CAN_FUN_ROLETA_POZ, v], extended_id=False) can0.send(cm) print("CANs => "+str(cm)) def on_publish(mqttc, obj, mid): print("mid: " + str(mid)) def on_subscribe(mqttc, obj, mid, granted_qos): print("Subscribed: " + str(mid) + " " + str(granted_qos)) def on_log(mqttc, obj, level, string): print("log: " + string) mqttc = mqtt.Client(client_id = "mqtt_test_py") mqttc.enable_logger(logger) mqttc.on_message = on_message mqttc.on_subscribe = on_subscribe mqttc.connect("localhost", 1883, 60) mqttc.subscribe(topic_root+"#", 0) print("subscribe:"+topic_root+"#") mqttc.loop_start() while 1: msg = can0.recv(30.0) if not(msg is None) and not(msg.is_extended_id): print "=> Can: " print(msg) if msg.data[0] > 0: # CAN=>MQTT; STM=>RPI = id id = msg.data[0] if msg.data[1] == CAN_FUN_ROLETA: #roleta if msg.data[2] == 255: s = "STOP" elif msg.data[2] == 1: s = "ON" else: s = "OFF" t = topic_can + str(id) + topic_roleta mqttc.publish(t, s) print "MQTT => {} - {}".format(t , s) if (msg.data[1] == CAN_FUN_SWITCH1): # wlaczik 1 n = msg.data[1] - 1 if msg.data[2] == 1: s = "ON" else: s = "OFF" t = topic_can + str(id) + topic_switch mqttc.publish(t, s) print "MQTT => {} - {}".format(t , s) if (msg.data[1] == CAN_FUN_SWITCH2): # wlaczik 2 n = msg.data[1] - 1 if msg.data[2] == 1: s = "ON2" else: s = "OFF2" t = topic_can + str(id) + topic_switch mqttc.publish(t, s) print "MQTT => {} - {}".format(t , s) if (msg.data[1] == CAN_FUN_SEN_BIN_1): # sensor binarne if msg.data[2] == 1: s = "ON" else: s = "OFF" t = topic_can + str(id) + '/sensor1' mqttc.publish(t, s) print "MQTT => {} - {}".format(t , s) if (msg.data[1] == CAN_FUN_SEN_BIN_2): # sensor binarne if msg.data[2] == 1: s = "ON" else: s = "OFF" t = topic_can + str(id) + '/sensor2' mqttc.publish(t, s) print "MQTT => {} - {}".format(t , s) if (msg.data[1] == CAN_FUN_ROLETA_POZ): s = str(msg.data[2]) t = topic_can + str(id) + topic_roleta_pos mqttc.publish(t, s) print "MQTT => {} - {}".format(t , s) if (msg.data[1] == CAN_FUN_SEN_PRAD): # prad s = str(255*msg.data[2]+msg.data[3]) t = topic_can + str(id) + '/prad' mqttc.publish(t, s) print "MQTT => {} - {}".format(t , s) if (msg.data[1] == 8): # temp s = str(255*msg.data[2]+msg.data[3]) t = topic_can + str(id) + '/temp' mqttc.publish(t, s) print "MQTT => {} - {}".format(t , s)
Skrypt w shelu do uruchamiania powyższego proc esu w python-nie. w katalogu //etc/init.d
#!/bin/sh ### BEGIN INIT INFO # Provides: demon_can # Required-Start: $all # Required-Stop: $all # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: Inicjowanie interefejsu can0 # Description: Inicjowanie interefejsu can0 ### END INIT INFO # Change the next 3 lines to suit where you install your script and what you want to call it DIR=/home/pi/py/can DAEMON=$DIR/demon_can.py DAEMON_NAME=demon_can # Add any command line options for your daemon here DAEMON_OPTS="" # This next line determines what user the script runs as. # Root generally not recommended but necessary if you are using the Raspberry Pi GPIO from Python. DAEMON_USER=root # The process ID of the script when it runs is stored here: PIDFILE=/var/run/$DAEMON_NAME.pid . /lib/lsb/init-functions do_start () { log_daemon_msg "Starting system $DAEMON_NAME daemon Olek" ifconfig can0 down ip link set can0 type can bitrate 50000 ifconfig can0 up sleep 10 #odczekać aby can0 się zainicjowało log_daemon_msg "Up can0" start-stop-daemon --start --background --pidfile $PIDFILE --make-pidfile --user $DAEMON_USER --chuid $DAEMON_USER --startas $DAEMON -- $DAEMON_OPTS log_end_msg $? } do_stop () { log_daemon_msg "Stopping system $DAEMON_NAME daemon Olek" start-stop-daemon --stop --pidfile $PIDFILE --retry 10 ifconfig can0 down log_daemon_msg "Down can0" log_end_msg $? } case "$1" in start|stop) do_${1} ;; restart|reload|force-reload) do_stop do_start ;; status) status_of_proc "$DAEMON_NAME" "$DAEMON" && exit 0 || exit $? ;; *) echo "Usage: /etc/init.d/$DAEMON_NAME {start|stop|restart|status} olek" exit 1 ;; esac exit 0
Aby HA widział odpowiednie encje trzeba dokonać standardowych wpisów konfiguracyjnych w pliku configuration.yaml.
I tak dla rolet w trybie “cover”.

cover: - platform: mqtt name: "Roleta test" command_topic: "DataSoft/can1/roleta/set" state_topic: "DataSoft/can1/roleta" retain: true payload_open: "OFF" payload_close: "ON" payload_stop: "STOP" state_open: "OFF" state_opening: "opening" state_closed: "ON" state_closing: "closing" optimistic: false position_topic: "DataSoft/can1/roleta_pos" set_position_topic: "DataSoft/can1/roleta_pos/set"
Wejścia binarne
sensor: - platform: mqtt name: "Sensor 1 test" state_topic: "DataSoft/can1/sensor1" - platform: mqtt name: "Sensor 2 test" state_topic: "DataSoft/can1/sensor2"
Przełącznik podwójny

switch: - platform: mqtt name: "Włącznik 1" state_topic: "DataSoft/can1/switch" command_topic: "DataSoft/can1/switch/set" payload_on: "ON" payload_off: "OFF" state_on: "ON" state_off: "OFF" retain: true - platform: mqtt name: "Włącznik 2" state_topic: "DataSoft/can1/switch" command_topic: "DataSoft/can1/switch/set" payload_on: "ON2" payload_off: "OFF2" state_on: "ON2" state_off: "OFF2" retain: true
Do konfigurowania modułu służy python-owski skrypt. Pozwala on z wiersza poleceń na malince zmienić parametry modułu. Każdy moduł w sieci CAN posiada swój unikalny adres – ID. Nowy moduł ma domyślny adres równy 1, który trzeba zmienić na inny wolny z przedziału 2-255. Adres ten jest również ujęty w odpowiednich tematach (topic) protokołu MQTT jako “/canID”. Standardowy parametr pomocy -h(–help) wywoła opis parametrów i ich użycie.
#!/usr/bin/python # -*- coding: utf-8 -*- """ File: can-cfg.py Title: konfikuracja modułów MCU-CAN """ import can import sys import argparse CAN_FUN_KONF_ID = 100 CAN_FUN_KONF_TYP = 101 CAN_FUN_KONF_ZAP_CZAS = 102 CAN_FUN_KONF_KALIBRACJA = 103 #os.system('sudo ifconfig can0 down') #os.system('sudo ip link set can0 type can bitrate 100000') #os.system('sudo ifconfig can0 up') parser = argparse.ArgumentParser(description="can-cfg narzedzie") parser.add_argument('-i', '--id', help="ID urzadzenia", type=int, required=True) parser.add_argument('-n', '--nid', help="Ustaw nowe ID", type=int, required=False) parser.add_argument('-t', '--typ', help="Ustaw typ urzadzenia 1-wylaczik; 2-roleta binarna; 4-roleta z pozycja", type=int, required=False) parser.add_argument('-s', '--czas', help="Ustaw czas zasuwania/otwiernaia rolety 10=1s", type=int, required=False) parser.add_argument('-k', '--kali', help="Kalibracja rolety", required=False) args = parser.parse_args() can0 = can.interface.Bus(channel = 'can0', bustype = 'socketcan_ctypes')# socketcan_native msg = None if args.nid <> None: print args.nid msg = can.Message(arbitration_id=0x0100+args.id, data=[0, CAN_FUN_KONF_ID, args.nid], extended_id=False) if args.typ <> None: msg = can.Message(arbitration_id=0x0100+args.id, data=[0, CAN_FUN_KONF_TYP, args.typ], extended_id=False) if args.czas <> None: msg = can.Message(arbitration_id=0x0100+args.id, data=[0, CAN_FUN_KONF_ZAP_CZAS, args.czas], extended_id=False) if args.kali <> None: msg = can.Message(arbitration_id=0x0100+args.id, data=[0, CAN_FUN_KONF_KALIBRACJA, 0], extended_id=False) if msg: print ("wyslano: ") print msg can0.send(msg) i = 0 msgr = can0.recv(5.0) while msgr <> None and i < 10: if msgr.arbitration_id == msg.arbitration_id: print ("odebrano: ") print msgr break i += 1; msgr = can0.recv(5.0) #os.system('sudo ifconfig can0 down')
Krótki filmik przedstawiający działanie-testowanie modułu
Aktualizacja 2024-10-27.
Jak przeszedłem z HA na dedukowanym mini PC zrezygnowałem RPI. Z braku GPIO musiałem musiałem wykonać samodzielną bramkę CAN – MQTT. Wykonałem ją na bazie NUCLEO-F207ZG czyli STM32.