Ankündigung

Einklappen
Keine Ankündigung bisher.

Neues Plugin ComfoAir (KWL Wohnraumlüftung Zehnder, Paul, Wernig)

Einklappen
X
 
  • Filter
  • Zeit
  • Anzeigen
Alles löschen
neue Beiträge

  • SvStefan
    antwortet
    Zitat von MatthiasS Beitrag anzeigen
    Wenn ich das richtig überflogen habe, fängst du das "Problem" mit den verdoppelten 07 07 nicht ab. Sollte man machen
    Stimmt, das hab ich in der Beschreibung gelesen, aber dann wieder vergessen. Anscheinend hatte ich noch keine 7er in meinen Daten oder es ist mir nicht aufgefallen.

    Danke für den Hinweis, hab die Änderung in den Code in Posting 1 eingebaut.

    LG Stefan

    Einen Kommentar schreiben:


  • MatthiasS
    antwortet
    Wenn ich das richtig überflogen habe, fängst du das "Problem" mit den verdoppelten 07 07 nicht ab. Sollte man machen

    Einen Kommentar schreiben:


  • Neues Plugin ComfoAir (KWL Wohnraumlüftung Zehnder, Paul, Wernig)

    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:
    • 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()
    ./etc/plugin_conf
    Code:
    [comfoair]
        class_name = ComfoAir
        class_path = plugins.comfoair
        host = 192.168.123.6
        port = 5555
    ./items/kwl.conf
    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
    Folgende Attribute stehen in der items.conf zur Verfügung:
    • 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
Lädt...
X