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.

Schemat sieci na bazie modułu
Podłączenie modułu
Opis wyprowadzeń

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