Mahlzeit allerseits!
Nachdem es ja beim Wiregate schon ein schönes Plugin für die Anbindung der Zehnder CA350 bzw. artverwandten Wohnraumlüftungsgeräten über RS232 gibt, habe ich mich mal an ein entsprechendes Smarthome.py Plugin gemacht, da mir der Smarthome.py Ansatz am Besten gefällt.
Bei mir läuft es so:
Das Plugin ist noch ganz frisch, ich würde es als "Alpha" einstufen.
Ich bin zwar Informatiker, aber beruflich nicht in Python unterwegs (ist mein erstes Python Projekt) - ich bin daher dankbar für Fehlermeldungen und Verbesserungsvorschläge.
Momentan existiert nur die Anbindung über TCP, wenn jemand eine KWL direkt über die RS232 angeschlossen hat, ist eine Anpassung an "direkte serielle Verbindung" sicher kein großes Thema.
Die Protokollbeschreibung ist von hier: http://www.see-solutions.de/sonstige...g_ComfoAir.pdf
Konzeptuell hab ich mich an bestehende Plugins wie squeezebox angelehnt, das Plugin soll möglichst allgemein-tauglich sein und über die Items erst richtig konfiguriert werden. Hier mal meine Beispielkonfiguration:
./plugins/comfoair/__init__.py
./etc/plugin_conf
./items/kwl.conf
Folgende Attribute stehen in der items.conf zur Verfügung:
Wenn das Plugin wen interessiert, kann ich mich noch weiter in technische Diskussionen vertiefen - bin noch nicht ganz zufrieden mit der Implementierung, auch wenn sie jetzt mal so weit funktioniert.
@mknx: Wie sieht es mit der Python 3 Migration aus, was müsste ich dafür machen? Bin zur Zeit noch im alten 2.7 stable unterwegs, da der WAF bei mir oberste Priorität hat.
LG Stefan
Nachdem es ja beim Wiregate schon ein schönes Plugin für die Anbindung der Zehnder CA350 bzw. artverwandten Wohnraumlüftungsgeräten über RS232 gibt, habe ich mich mal an ein entsprechendes Smarthome.py Plugin gemacht, da mir der Smarthome.py Ansatz am Besten gefällt.
Bei mir läuft es so:
- Wernig G90-380 KWL (Baugleich Zehnder CA 350)
- RS232 Verbindung zu einem Moxa NPort DE-311
- Die Moxa spielt TCP Server auf Port 5555, dorthin verbindet sich mein Plugin
Das Plugin ist noch ganz frisch, ich würde es als "Alpha" einstufen.

Ich bin zwar Informatiker, aber beruflich nicht in Python unterwegs (ist mein erstes Python Projekt) - ich bin daher dankbar für Fehlermeldungen und Verbesserungsvorschläge.
Momentan existiert nur die Anbindung über TCP, wenn jemand eine KWL direkt über die RS232 angeschlossen hat, ist eine Anpassung an "direkte serielle Verbindung" sicher kein großes Thema.
Die Protokollbeschreibung ist von hier: http://www.see-solutions.de/sonstige...g_ComfoAir.pdf
Konzeptuell hab ich mich an bestehende Plugins wie squeezebox angelehnt, das Plugin soll möglichst allgemein-tauglich sein und über die Items erst richtig konfiguriert werden. Hier mal meine Beispielkonfiguration:
./plugins/comfoair/__init__.py
Code:
#!/usr/bin/env python
#########################################################################
# Copyright 2013 Stefan Kals
#########################################################################
# ComfoAir-Plugin for SmartHome.py. http://mknx.github.com/smarthome/
#
# This plugin is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This plugin is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this plugin. If not, see <http://www.gnu.org/licenses/>.
#########################################################################
import logging
import struct
import time
import urllib2
import lib.my_asynchat
import re
import encodings
import threading
logger = logging.getLogger("ComfoAir")
START_SEQUENCE = chr(0x07) + chr(0xF0)
END_SEQUENCE = chr(0x07) + chr(0x0F)
ACK_SEQUENCE = chr(0x07) + chr(0xF3)
class ComfoAir(lib.my_asynchat.AsynChat):
def __init__(self, smarthome, host, port):
lib.my_asynchat.AsynChat.__init__(self, smarthome, host, port)
self.terminator = END_SEQUENCE
self._sh = smarthome
self.params = {}
self._init_cmds = []
self._cyclic_cmds = {}
self._lock = threading.Lock()
smarthome.monitor_connection(self)
def parse_item(self, item):
# Process the read config
if "comfoair_read" in item.conf:
cmd = item.conf["comfoair_read"]
if (cmd == None):
return None
# Parse command (e.g. 00cd.9 = send a telegram with command 00cd, interpret the answer (telegram with command + 1 from ComfoAir) and use the data byte number 9 to get a value)
bytepos = 1
cmdparts = cmd.split(".")
if (len(cmdparts) > 1):
bytepos = int(cmdparts[1])
cmd = cmdparts[0]
# Remember the read config to later update this item if the configured response comes in
logger.debug("ComfoAir: {0} reads by using command \"{1}\" and interpreting answer byte number \"{2}\"".format(item, cmd, bytepos))
if not cmd in self.params:
self.params[cmd] = {"items": [item], "bytepos": [bytepos]}
else:
if not item in self.params[cmd]["items"]:
self.params[cmd]["items"].append(item)
self.params[cmd]["bytepos"].append(bytepos)
# Allow items to be automatically initiated on startup
if ("comfoair_init" in item.conf and item.conf["comfoair_init"] == "true"):
logger.debug("ComfoAir: {0} is initialized on startup.".format(item))
# Only add the item to the initial commands if it is not cyclic. Cyclic commands get called on init because this is the first cycle...
if not cmd in self._init_cmds and "comfoair_read_cycle" not in item.conf:
self._init_cmds.append(cmd)
# Allow items to be cyclically updated
if ("comfoair_read_cycle" in item.conf):
cycle = int(item.conf["comfoair_read_cycle"])
logger.debug("ComfoAir: {0} should read cyclic every {1} seconds.".format(item, cycle))
if not cmd in self._cyclic_cmds:
self._cyclic_cmds[cmd] = {"cycle": cycle, "nexttime": 0}
else:
# If another item requested this command already with a longer cycle, use the shorter cycle now
if self._cyclic_cmds[cmd]["cycle"] > cycle:
self._cyclic_cmds[cmd]["cycle"] = cycle
# Process the send config
if "comfoair_send" in item.conf:
cmd = item.conf["comfoair_send"]
if (cmd == None):
return None
logger.debug("ComfoAir: {0} sends by using command \"{1}\"".format(item, cmd))
return self.update_item
else:
return None
def parse_logic(self, logic):
pass
def update_item(self, item, caller=None, source=None, dest=None):
if caller != "ComfoAir" and "comfoair_send" in item.conf:
cmd = item.conf["comfoair_send"]
if item._type == "bool":
value = int(item())
else:
value = item()
# Restrict to 0..255 and format hex
value = value % 256
cmd = cmd.format("%02x" % value)
self._send(cmd)
# If a read command should be sent after write
if "comfoair_read" in item.conf and "comfoair_read_afterwrite" in item.conf:
cmd = item.conf["comfoair_read"]
readafterwrite = item.conf["comfoair_read_afterwrite"]
logger.debug("ComfoAir: Attempting read after write for item {0}, command {1}, delay {2}".format(item, cmd, readafterwrite))
if cmd != None and readafterwrite != None:
cmdparts = cmd.split(".")
aw = float(readafterwrite)
if len(cmdparts) > 0:
time.sleep(aw)
self._send(cmdparts[0])
def _handle_cyclic_cmds(self):
# Read all cyclic commands
currenttime = time.time()
for cmd in self._cyclic_cmds.keys():
entry = self._cyclic_cmds[cmd]
# Is the command already due?
if entry["nexttime"] <= currenttime:
logger.debug("ComfoAir: Triggering cyclic read command: {0}".format(cmd))
self._send(cmd)
entry["nexttime"] = currenttime + entry["cycle"]
def _send(self, cmd):
# Use a lock to allow only one sender at a time
self._lock.acquire()
try:
# For simple telegrams without given length and data, just add 00 as length
if len(cmd) < 6:
cmd = cmd + "00"
# First calculate the checksum
checksum = (sum(bytearray(cmd.decode("hex"))) + 173) % 256
# Split the command and "encode" 0x07 data bytes by doubling them
cmdpart = cmd[:4]
lenpart = cmd[4:6]
datapart = cmd[6:].replace("07", "0707")
packet = START_SEQUENCE + (cmdpart + lenpart + datapart).decode("hex") + chr(checksum) + END_SEQUENCE
logger.debug("ComfoAir: Sending request: {0}".format(packet.encode("hex")))
self.push(packet)
# This is ugly but give some time to not overfill the ComfoAir buffer
time.sleep(0.1)
except Exception, e:
logger.error("ComfoAir: Exception while sending: {0}".format(e))
finally:
self._lock.release()
def _parse_response(self, resp):
logger.debug("ComfoAir: Received telegram: {0}".format(resp.encode("hex")))
# A telegram looks like this: start sequence (2 bytes), command (2 bytes), data length (1 byte), data, checksum (1 byte), end sequence (2 bytes, already cut away)
cmd = resp[2:4]
reqcmd = cmd[0] + chr(ord(cmd[1]) - 1) # The request command of this response is -1
cmd = cmd.encode("hex")
reqcmd = reqcmd.encode("hex")
# Remove begin and checksum to get the data (end = terminator => already cut away)
data = resp[4:-1]
# Remove double 0x07 data bytes
data = data.replace(chr(0x07) + chr(0x07), chr(0x07))
logger.debug("ComfoAir: Received command: {0}, corresponding request command: {1}, data: {2}".format(cmd, reqcmd, data.encode("hex")))
# Find items using this response command
if reqcmd in self.params:
for i in range(0, len(self.params[reqcmd]["items"])):
item = self.params[reqcmd]["items"][i]
bytepos = self.params[reqcmd]["bytepos"][i]
value = ord(data[bytepos])
logger.debug("ComfoAir: Updating item {0} from byte position {1} with value {2}".format(item, bytepos, value))
item(value, "ComfoAir")
def found_terminator(self):
response = self.buffer
self.buffer = ""
# Cut old ACK responses from ComfoAir before the real message
while response.startswith(ACK_SEQUENCE):
response = response[2:]
if len(response) > 0:
self._parse_response(response)
self.push(ACK_SEQUENCE)
def handle_connect(self):
self.discard_buffers()
# Do the init read commands
if self._init_cmds != []:
if self.is_connected:
logger.debug("ComfoAir: init read")
for cmd in self._init_cmds:
self._send(cmd)
# Find the shortest cycle
shortestcycle = -1
for cmd in self._cyclic_cmds.keys():
entry = self._cyclic_cmds[cmd]
if shortestcycle == -1 or entry["cycle"] < shortestcycle:
shortestcycle = entry["cycle"]
# Start the worker thread
if shortestcycle != -1:
# Balance unnecessary calls and precision
workercycle = int(shortestcycle / 5)
self._sh.scheduler.add('ComfoAir-cyclic-reads', self._handle_cyclic_cmds, cycle=workercycle, prio=5, offset=0)
logger.debug("ComfoAir: shortest cycle time found: {0} sec".format(shortestcycle))
logger.debug("ComfoAir: added cyclic worker thread ({0} sec cycle)".format(workercycle))
def run(self):
self.alive = True
def stop(self):
self.alive = False
self.handle_close()
Code:
[comfoair]
class_name = ComfoAir
class_path = plugins.comfoair
host = 192.168.123.6
port = 5555
Code:
['kwl']
[['stufe']]
visu = true
type = num
comfoair_send = 009901{}
comfoair_read = 00cd.9
comfoair_read_afterwrite = 1 # seconds
comfoair_init = true
[['abluft']]
[[['aktuell']]]
visu = true
type = num
comfoair_read = 00cd.7
comfoair_init = true
[['zuluft']]
[[['aktuell']]]
visu = true
type = num
comfoair_read = 00cd.8
comfoair_init = true
[['filter']]
[[['ruecksetzen']]]
visu = true
type = bool
comfoair_send = 00db0400000001{}
[['temp']]
[[['komfort']]]
visu = true
type = num
eval = value / 2 - 20
comfoair_read = 00d1.1
comfoair_read_cycle = 60 # seconds
comfoair_init = true
[[['auszenluft']]]
visu = true
type = num
eval = value / 2 - 20
comfoair_read = 00d1.2
comfoair_read_cycle = 60 # seconds
comfoair_init = true
[[['zuluft']]]
visu = true
type = num
eval = value / 2 - 20
comfoair_read = 00d1.3
comfoair_read_cycle = 60 # seconds
comfoair_init = true
[[['abluft']]]
visu = true
type = num
eval = value / 2 - 20
comfoair_read = 00d1.4
comfoair_read_cycle = 60 # seconds
comfoair_init = true
[[['fortluft']]]
visu = true
type = num
eval = value / 2 - 20
comfoair_read = 00d1.5
comfoair_read_cycle = 60 # seconds
comfoair_init = true
[[['heizstab']]]
visu = true
type = num
eval = value / 2 - 20
comfoair_read = 00d1.8
comfoair_read_cycle = 60 # seconds
comfoair_init = true
[['bypass']]
visu = true
type = num
comfoair_read = 000d.1
comfoair_read_cycle = 600 # seconds
comfoair_init = true
[['heizstab']]
visu = true
type = num
comfoair_read = 000d.2
comfoair_read_cycle = 600 # seconds
comfoair_init = true
[[['leistung']]]
visu = true
type = num
comfoair_read = 000d.4
comfoair_read_cycle = 600 # seconds
comfoair_init = true
- comfoair_send: Sendet dieses Telegramm an die KWL bei Änderung des Items. Angegeben wird der Befehl (2 Bytes = 4 Zeichen Hex), die Datenlänge (1 Byte = 2 Zeichen hex) und die Daten (hex). Beispiel: 009901{} = Befehl 0x00 0x99, Datenlänge 0x01 = 1 Byte, Daten = {} = wird durch Value des Items ersetzt
- comfoair_read: Dieses Telegramm wird zum Lesen des Wertes benutzt. Nach dem Punkt wird die Byteposition im Antworttelegramm angegeben, aus der der Wert genommen werden soll. Beispiel: 00cd.9 = Befehl 0x00 0xCD wird gesendet; Aus dem Antworttelegramm wird das Datenbyte Nr. 9 als Wert für das Item gelesen.
- comfoair_read_afterwrite: Wenn das Item in der KWL upgedated wurde, soll nach x Sekunden das unter comfoair_read konfigurierte Lesetelegramm benutzt werden, um das Item upzudaten.
- comfoair_read_cycle: Das Item soll alle x Sekunden zyklisch von der KWL gelesen werden.
- comfoair_init: Wenn auf "true" gesetzt, wird das Item beim Start von smarthome.py mit dem konfigurierten Lesetelegramm initialisiert.
Wenn das Plugin wen interessiert, kann ich mich noch weiter in technische Diskussionen vertiefen - bin noch nicht ganz zufrieden mit der Implementierung, auch wenn sie jetzt mal so weit funktioniert.
@mknx: Wie sieht es mit der Python 3 Migration aus, was müsste ich dafür machen? Bin zur Zeit noch im alten 2.7 stable unterwegs, da der WAF bei mir oberste Priorität hat.

LG Stefan



Kommentar