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 0Aby 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: trueDo 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.