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