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

    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

    #2
    Wenn ich das richtig überflogen habe, fängst du das "Problem" mit den verdoppelten 07 07 nicht ab. Sollte man machen
    Gruß Matthias
    EIB übersetzt meine Frau mit "Ehemann Ist Beschäftigt"
    Meine Homepage - PN nur für PERSÖNLICHES!

    Kommentar


      #3
      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

      Kommentar


        #4
        Hallo Stefan,

        ich habe die letzten Wochen das Wiregate-Plugin nach Python portiert. Bin auch absolute Python-Anfänger und habe es mehr oder weniger 1:1 übernommen. Bei mir läuft das Plugin nun seit ca. 2 Wochen problemlos. Ich benutze die RS232 Schnittstelle. Ich denke zwei Plugins werden nicht benötigt und somit sollten wir uns absprechen.

        Gruß Marcus

        Kommentar


          #5
          Zitat von mannmawg Beitrag anzeigen
          Hallo Stefan,

          ich habe die letzten Wochen das Wiregate-Plugin nach Python portiert. Bin auch absolute Python-Anfänger und habe es mehr oder weniger 1:1 übernommen. Bei mir läuft das Plugin nun seit ca. 2 Wochen problemlos. Ich benutze die RS232 Schnittstelle. Ich denke zwei Plugins werden nicht benötigt und somit sollten wir uns absprechen.

          Gruß Marcus
          Hehe, das hab ich befürchtet und bin deswegen damit online gegangen, bevor 3 Leute dasselbe bauen. Postet du mal deinen Code?

          LG Stefan

          Kommentar


            #6
            Hi Stefan,

            du findest das Plugin unter
            Github ComfoAir Plugin

            Wie gesagt, ist fast eine 1zu1 Umsetzung des Wiregate-Plugins und sicherlich verbesserungswürdig.

            Gruß Marcus

            Kommentar


              #7
              Hallo Stefan,

              ist das Plugin auch für eine Paul Lüftung?
              Habe eine Novus 300.

              Gruss

              Kommentar


                #8
                Hallo Stefan,

                danke erst einmal für das Plugin. Es wäre schön, wenn Du zusammen mit Marcus das Plugin weiterentwickelst.

                Bis bald

                Marcus

                Kommentar


                  #9
                  Hallo René,
                  Zitat von Traqso Beitrag anzeigen
                  ist das Plugin auch für eine Paul Lüftung?
                  laut Thread-Titel schon. Ob es mit Deiner funktioniert, kannst wahrscheinlich Du am besten beantworten, da die wenigsten Entwickler verschieden Lüftungssystem zu Hause haben.

                  Bis bald

                  Marcus

                  Kommentar


                    #10
                    Zitat von Traqso Beitrag anzeigen
                    Hallo Stefan,

                    ist das Plugin auch für eine Paul Lüftung?
                    Habe eine Novus 300.

                    Gruss
                    Hi,

                    soweit ich gesehen habe, hat die Novus 300 keine RS232 sondern eine RS485 Schnittstelle, die anders funktioniert und daher mit dieser Anbindung nicht zusammenspielt.

                    LG Stefan

                    Kommentar


                      #11
                      Zitat von mknx Beitrag anzeigen
                      Hallo Stefan,

                      danke erst einmal für das Plugin. Es wäre schön, wenn Du zusammen mit Marcus das Plugin weiterentwickelst.

                      Bis bald

                      Marcus
                      Hi 2x Marcus ;-),

                      Ich hatte mir das Wiregate-Plugin eh auch angesehen, fand den sehr starren Ansatz aber nicht gut (z.B. folgende 10 Werte werden der Reihe nach ausgelesen, dann werden folgende 4 Temperaturen ausgelesen und als RRD gespeichert).

                      Das tolle an deinem Smarthome.py und den Items ist ja gerade die komplette Flexibilität - diese wollte ich mit meinem Plugin dem Benutzer auch ermöglichen, dass er dann frei entscheiden kann, welche Items er wie oft ausgelesen bekommt etc...

                      Das Plugin läuft jetzt auch schon 2 Wochen stabil, melde mich wenn es was Neues gibt.

                      LG Stefan

                      Kommentar


                        #12
                        Hallo Stefan, hallo Marcus,

                        was haltet Ihr davon, wenn wir die Entscheidung unter uns besprechen?
                        Schickt mir doch bitte eine PN mit Euere Mail-Adresse.

                        Bis bald

                        Marcus

                        Kommentar


                          #13
                          Hallo, ich habe eine Comfortair 550 und würde gerne das bei mir auch umsetzen


                          Spricht etwas dagegen den rs232 Server direkt auf dem Raspi auszuführen ? Also z.b. so: Serial Ethernet Converter with Raspi | blog.schwabl.net

                          Mir währe die Umsetzung ohne wiregate am Liebsten, da mei Raspi eh in den Keller neben die Lüftung wandert.

                          Gruss Stephan

                          Kommentar


                            #14
                            Zitat von shs2 Beitrag anzeigen
                            Hallo, ich habe eine Comfortair 550 und würde gerne das bei mir auch umsetzen


                            Spricht etwas dagegen den rs232 Server direkt auf dem Raspi auszuführen ? Also z.b. so: Serial Ethernet Converter with Raspi | blog.schwabl.net

                            Mir währe die Umsetzung ohne wiregate am Liebsten, da mei Raspi eh in den Keller neben die Lüftung wandert.

                            Gruss Stephan
                            Die ComfoAir 550 soll angeblich mit dem gleichen Protokoll funktionieren. :-)
                            RS232 Server auf Raspi wäre gleichwertig zu einer Moxa, sollte also auch gehen. Kannst ja mal aufsetzen und ausprobieren!

                            LG Stefan

                            Kommentar


                              #15
                              Hallo Stefan,

                              ist eine Comfortair 500, aber ich denke das ist nicht das Problem.
                              Da ich die Schnittstelle noch nie angesprochen habe, wollte ich erst einmal mit dem PV versuchen. Leider zunächst ohne Erfolg.
                              Was braucht denn die Anlage 1:1 oder Nullmodemkabel?

                              Gruß Stephan

                              Kommentar

                              Lädt...
                              X