├── config-template.py ├── requestall.py ├── README.md ├── .gitignore ├── zcan_mqtt_bridge.py ├── sendmsg.py ├── ComfoNetCan.py ├── testcan.py └── mapping2.py /config-template.py: -------------------------------------------------------------------------------- 1 | config = { 2 | "mqtt_host": "10.81.240.1", 3 | "mqtt_port": 1883, 4 | "can_if": "can1" 5 | } 6 | -------------------------------------------------------------------------------- /requestall.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import mapping2,os 3 | for i in (map(lambda x:(x<<14)+0x41, mapping2.mapping.keys())): 4 | cmd = "/usr/bin/cansend can1 %08x#R" %i 5 | print(cmd) 6 | os.system(cmd) 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zcan 2 | 3 | Although this is a fork from the great work of marco-hoyer, the goal for this repository is quite different. My goal is to create a set of python scripts to monitor and actively control the ComfoAirQ units. 4 | 5 | ## Status 6 | Mapping is quite complete, commands still need a lot of work. 7 | 8 | Also there is no build system or anything, for now just a bunch of scripts. 9 | 10 | file|explanation 11 | ---|--- 12 | ComfoNetCan.py| class to translate between CAN encoded messages to ComfoNet and back 13 | Mapping2.py| list of pdo items and their meaning 14 | Sendmsg.py| Example of how to send a message over the ComfoNet 15 | Testcan.py| Example of monitoring ComfoNet pdo/pdo messages and write to json files (for showing in a html page) 16 | Zcanbridge.py| Transfer the RhT value to my DeCONZ REST API 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | 103 | config.py 104 | -------------------------------------------------------------------------------- /zcan_mqtt_bridge.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from paho.mqtt import client as mqtt 4 | 5 | import mapping2 as mapping 6 | import asyncio 7 | import socket 8 | import struct 9 | import time 10 | import sys 11 | from config import config 12 | 13 | can_frame_fmt = "=IB3x8s" 14 | mqtt_client = mqtt.Client() 15 | mqtt_client.will_set("lueftung/zehnder/available", "offline", retain=True) 16 | mqtt_client.connect(config['mqtt_host'], config['mqtt_port'], 60) 17 | mqtt_client.loop_start() 18 | 19 | def dissect_can_frame(frame): 20 | can_id, can_dlc, data = struct.unpack(can_frame_fmt, frame) 21 | if can_id & socket.CAN_RTR_FLAG != 0: 22 | print("RTR received from %08X"%(can_id&socket.CAN_EFF_MASK)) 23 | return(0,0,[]) 24 | can_id &= socket.CAN_EFF_MASK 25 | 26 | return (can_id, can_dlc, data[:can_dlc]) 27 | 28 | @asyncio.coroutine 29 | def handle_client(cansocket): 30 | mqtt_client.publish("lueftung/zehnder/available", "online", retain=True) 31 | request = None 32 | while True: 33 | msg = yield from loop.sock_recv(cansocket, 16) 34 | can_id, can_dlc, data = dissect_can_frame(msg) 35 | if can_id & 0xFF800000 == 0: 36 | pdid = (can_id>>14)&0x7ff 37 | if pdid in mapping.mapping: 38 | map = mapping.mapping[pdid] 39 | topic = "lueftung/zehnder/state/%s" % map["name"] 40 | info = map["transformation"](data) 41 | mqtt_client.publish(topic, info, retain=True) 42 | #print("Pushing to %i %s %s" % (pdid, topic, str(info))) 43 | else: 44 | print("Unknown message %i %s" % (pdid, repr(data)), file=sys.stderr) 45 | 46 | # create a raw socket and bind it to the given CAN interface 47 | loop = asyncio.get_event_loop() 48 | s = socket.socket(socket.AF_CAN, socket.SOCK_RAW, socket.CAN_RAW) 49 | s.bind((config['can_if'],)) 50 | loop.run_until_complete( 51 | handle_client(s)) 52 | 53 | # vim: et:sw=4:ts=4:smarttab:foldmethod=indent:si 54 | -------------------------------------------------------------------------------- /sendmsg.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import socket 4 | import struct 5 | import time 6 | import ComfoNetCan as CN 7 | 8 | def msgclass(): 9 | data =[ 10 | 0x84, #CmdID 11 | 0x15, #ItemInLookupTable 12 | 0x01, #Type --> selects field1 or field2... if that field is 1, OK 13 | #Start actual command... 14 | 0x01, #FF special case, otherwise -1 selects timer to use..?SubCMD? 15 | 0x00, 0x00, 0x00, 0x00, #v9 16 | 0x00, 0x1C, 0x00, 0x00, #v10 17 | 0x03, #v11 Check vs type-1 0: <=3, 1,2,9:<=2, 3,4,5,6,7,8<=1 18 | 0x00, 19 | 0x00, 20 | 0x00] 21 | 22 | 23 | # create a raw socket and bind it to the given CAN interface 24 | s = socket.socket(socket.AF_CAN, socket.SOCK_RAW, socket.CAN_RAW) 25 | s.bind(("slcan0",)) 26 | cnet = CN.ComfoNet(s) 27 | cnet.FindComfoAirQ() 28 | 29 | #Set Ventilation level 30 | #write_CN_Msg(0x11, 0x32, 1, 0, 1, 0, [0x84,0x15,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x1C,0x00,0x00,0x03,0x00, 0x00, 0x00]) 31 | 32 | #cnet.write_CN_Msg(0x11, cnet.ComfoAddr, 1, 0, 1, [0x84,0x15,0x02,0x01,0x00,0x00,0x00,0x00,0x10,0x3E,0x00,0x00,0x01,0x00, 0x00, 0x00]) 33 | 34 | #Set bypass to auto 35 | #cnet.write_CN_Msg(0x11, cnet.ComfoAddr, 1, 0, 1, [0x85,0x15,0x02,0x01]) 36 | 37 | #set Bypass 38 | #cnet.write_CN_Msg(0x11, cnet.ComfoAddr, 1, 0, 1, [0x84,0x15,0x02,0x01,0x00,0x00,0x00,0x00,0x10,0x0E,0x00,0x00,0x01,0x00, 0x00, 0x00]) 39 | #set Boost 40 | #CN.write_CN_Msg(0x11, 0x32, 1, 0, 1, [0x84,0x15,0x01,0x06,0x00,0x00,0x00,0x00,0x10,0x0E,0x00,0x00,0x03,0x00, 0x00, 0x00]) 41 | 42 | #Set Exhaust only 43 | #cnet.write_CN_Msg(0x11, cnet.ComfoAddr, 1, 0, 1, [0x84,0x15,0x07,0x01,0x00,0x00,0x00,0x00,0x10,0x0E,0x00,0x00,0x01,0x00, 0x00, 0x00]) 44 | #cnet.write_CN_Msg(0x11, cnet.ComfoAddr, 1, 0, 1, [0x85,0x15,0x07,0x01]) 45 | 46 | cnet.write_CN_Msg(0x11, cnet.ComfoAddr, 1, 0, 1, [0x83,0x15,0x01,0x06]) 47 | 48 | #for n in range(0xF): 49 | # write_CN_Msg(0x11, 0x32, 1, 0, 1, 0, [0x87,0x15,n]) 50 | # time.sleep(1) 51 | #write_long_message(0x01, 0x1F005051, [0x84,0x15,0x01,0x01,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x01,0x00]) 52 | #write_long_message(0x01, 0x1F005078, [0x84,0x15,0x01,0x01,0x00,0x00,0x00,0x00,0x20,0x1C,0x00,0x00,0x03,0x00,0x00,0x00]) 53 | #for iterator in range(0xF): 54 | # time.sleep(4) 55 | #write_long_message(1, 0x1F005051, [0x84,0x15,0x01,0x01,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x03,0x00,0x00,0x00]) 56 | #write_long_message(1, 0x1F005051, [0x84,0x15,0x08,0x01,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x01]) #manual mode 57 | #write_long_message(1, 0x1F005051, [0x85,0x15,0x08,0x01]) #auto mode 58 | #canwrite(0x1F015051, [0x85,0x15,0x08,0x01]) #auto mode 59 | #for iterator in range(0xF): 60 | # write_long_message(iterator,0x1F005051, [0x84, 0x15, 0x08, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01]) 61 | # time.sleep(0.5) 62 | # write_long_message(iterator, 0x1F005051, [0x84,0x15,0x01,0x01,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x03,0x00]) 63 | # time.sleep(2) 64 | #canwrite(0x1F075051|socket.CAN_EFF_FLAG, [0x00, 0x84, 0x15, 0x01, 0x01, 0x00, 0x00, 0x00]) 65 | #canwrite(0x1F075051|socket.CAN_EFF_FLAG, [0x01, 0x00, 0x20, 0x1C, 0x00, 0x00, 0x03, 0x00]) 66 | #while True: 67 | # canwrite(0x10140001,[0x21,0x22,0xC2,0x22]) 68 | # time.sleep(4) 69 | 70 | #write_long_message(0x05, 0x1F005051, [0x01,0x01,0x01,0x10,0x08,0x00,0x00]) 71 | 72 | cnet.ShowReplies() 73 | -------------------------------------------------------------------------------- /ComfoNetCan.py: -------------------------------------------------------------------------------- 1 | #!/bin/python3 2 | import struct 3 | import socket 4 | 5 | 6 | cmdMapping = { 7 | "NODE": 0x1, 8 | "COMFOBUS": 0x2, 9 | "ERROR": 0x3, 10 | "SCHEDULE": 0x15, 11 | "VALVE": 0x16, 12 | "FAN": 0x17, 13 | "POWERSENSOR": 0x18, 14 | "PREHEATER": 0x19, 15 | "HMI": 0x1A, 16 | "RFCOMMUNICATION": 0x1B, 17 | "FILTER": 0x1C, 18 | "TEMPHUMCONTROL": 0x1D, 19 | "VENTILATIONCONFIG": 0x1E, 20 | "NODECONFIGURATION": 0x20, 21 | "TEMPERATURESENSOR": 0x21, 22 | "HUMIDITYSENSOR": 0x22, 23 | "PRESSURESENSOR": 0x23, 24 | "PERIPHERALS": 0x24, 25 | "ANALOGINPUT": 0x25, 26 | "COOKERHOOD": 0x26, 27 | "POSTHEATER": 0x27, 28 | "COMFOFOND": 0x28, 29 | "COOLER": 0x15, 30 | "CC_TEMPERATURESENSOR": 0x16, 31 | "IOSENSOR": 0x15, 32 | } 33 | 34 | cmdSchedules = { 35 | "GETSCHEDULEENTRY": 0x80, 36 | "ENABLESCHEDULEENTRY": 0x81, 37 | "DISABLESCHEDULEENTRY": 0x82, 38 | "GETTIMERENTRY": 0x83, 39 | "ENABLETIMERENTRY": 0x84, 40 | "DISABLETIMERENTRY": 0x85, 41 | "GETSCHEDULE": 0x86, 42 | "GETTIMERS": 0x87, 43 | } 44 | 45 | class CN1FAddr: 46 | def __init__(self, SrcAddr, DstAddr, Address, MultiMsg, A8000, A10000, SeqNr): 47 | self.SrcAddr = SrcAddr 48 | self.DstAddr = DstAddr 49 | self.Address = Address 50 | self.MultiMsg = MultiMsg 51 | self.A8000 = A8000 52 | self.A10000 = A10000 53 | self.SeqNr = SeqNr 54 | 55 | @classmethod 56 | def fromCanID(cls, CID): 57 | if (CID>>24) != 0x1F: 58 | raise ValueError('Not 0x1F CMD!') 59 | else: 60 | return cls( 61 | (CID>> 0)&0x3f, 62 | (CID>> 6)&0x3f, 63 | (CID>>12)&0x03, 64 | (CID>>14)&0x01, 65 | (CID>>15)&0x01, 66 | (CID>>16)&0x01, 67 | (CID>>17)&0x03) 68 | 69 | def __repr__(self): 70 | return (f'{self.__class__.__name__}(\n' 71 | f' SrcAddr = {self.SrcAddr:#02x},\n' 72 | f' DstAddr = {self.DstAddr:#02x},\n' 73 | f' Address = {self.Address:#02x},\n' 74 | f' MultiMsg = {self.MultiMsg:#02x},\n' 75 | f' A8000 = {self.A8000:#02x},\n' 76 | f' A10000 = {self.A10000:#02x},\n' 77 | f' SeqNr = {self.SeqNr:#02x})') 78 | 79 | def CanID(self): 80 | addr = 0x0 81 | addr |= self.SrcAddr << 0 82 | addr |= self.DstAddr << 6 83 | 84 | addr |= self.Address <<12 85 | addr |= self.MultiMsg<<14 86 | addr |= self.A8000 <<15 87 | addr |= self.A10000 <<16 88 | addr |= self.SeqNr <<17 89 | addr |= 0x1F <<24 90 | 91 | return addr 92 | 93 | class ComfoNet: 94 | def __init__(self, cansocket): 95 | self.Seq = 0 96 | self.can = cansocket 97 | 98 | def write_CN_Msg(self, SrcAddr, DstAddr, C3000, C8000, C10000, data): 99 | A = CN1FAddr(SrcAddr, DstAddr, C3000, len(data)>8, C8000, C10000, self.Seq) 100 | 101 | self.Seq = (self.Seq + 1)&0x3 102 | 103 | if len(data) > 8: 104 | datagrams = int(len(data)/7) 105 | if len(data) == datagrams*7: 106 | datagrams -= 1 107 | for n in range(datagrams): 108 | self.canwrite(A.CanID(), [n]+data[n*7:n*7+7]) 109 | n+=1 110 | restlen = len(data)-n*7 111 | self.canwrite(A.CanID(), [n|0x80]+data[n*7:n*7+restlen]) 112 | else: 113 | self.canwrite(A.CanID(), data) 114 | 115 | def request_tdpo(self, DpoID): 116 | cid = (DpoID<<14)|0x01<<6|self.ComfoAddr|socket.CAN_EFF_FLAG|socket.CAN_RTR_FLAG 117 | print("0x%8X"%(cid)) 118 | self.can.send(struct.pack("=IB3x8B", cid,0,*[0]*8)) 119 | 120 | def dissect_can_frame(self, frame): 121 | can_id, can_dlc, data = struct.unpack("=IB3x8s", frame) 122 | if can_id & socket.CAN_RTR_FLAG != 0: 123 | print("RTR received from %08X"%(can_id&socket.CAN_EFF_MASK)) 124 | return(0,0,[]) 125 | can_id &= socket.CAN_EFF_MASK 126 | 127 | return (can_id, can_dlc, data[:can_dlc]) 128 | 129 | def canwrite(self, msg, data=[]): 130 | print(('%X#'+'%02X'*len(data))%((msg,)+tuple(data))) 131 | can_id = msg | socket.CAN_EFF_FLAG 132 | can_dlc = len(data) 133 | data = [(data[n] if n < can_dlc else 0) for n in range(8)] 134 | 135 | #print(msg) 136 | #print(data) 137 | self.can.send(struct.pack("=IB3x8B", can_id, can_dlc, *data)) 138 | 139 | def FindComfoAirQ(self): 140 | while True: 141 | cf, addr = self.can.recvfrom(16) 142 | 143 | can_id, can_dlc, data = self.dissect_can_frame(cf) 144 | print('Received: can_id=%x, can_dlc=%x, data=%s' % self.dissect_can_frame(cf)) 145 | if can_id & 0xFFFFFFC0 == 0x10000000: 146 | self.ComfoAddr = can_id&0x3f 147 | print(f'{self.ComfoAddr:#06X}') 148 | break 149 | 150 | def ShowReplies(self): 151 | for n in range(100): 152 | cf, addr = self.can.recvfrom(16) 153 | 154 | can_id, can_dlc, data = self.dissect_can_frame(cf) 155 | if can_id & 0x1F000000 == 0x1F000000: 156 | print(f'Reply: {can_id:#06X} ' + ":".join(f'{c:02x}' for c in data )) 157 | 158 | def ConvertCN1FCmds(self): 159 | while True: 160 | cf, addr = self.SCan.recvfrom(16) 161 | 162 | can_id, can_dlc, data = dissect_can_frame(cf) 163 | print('Received: can_id=%x, can_dlc=%x, data=%s' % dissect_can_frame(cf)) 164 | try: 165 | CNAddr = CN1FAddr.fromCanID(can_id) 166 | except ValueError: 167 | pass 168 | 169 | if self.CN.SrcAddr: 170 | pass 171 | 172 | def DecodeCanID(self, canID): 173 | pass 174 | 175 | def msgclass(): 176 | data =[ 177 | 0x84, #CmdID 178 | 0x15, #ItemInLookupTable 179 | 0x01, #Type --> selects field1 or field2... if that field is 1, OK 180 | #Start actual command... 181 | 0x01, #FF special case, otherwise -1 selects timer to use..?SubCMD? 182 | 0x00, 0x00, 0x00, 0x00, #v9 183 | 0x00, 0x1C, 0x00, 0x00, #v10 184 | 0x03, #v11 Check vs type-1 0: <=3, 1,2,9:<=2, 3,4,5,6,7,8<=1 185 | 0x00, 186 | 0x00, 187 | 0x00] 188 | 189 | 190 | # vim:ts=4:sw=4:si:et:fdm=indent: 191 | -------------------------------------------------------------------------------- /testcan.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import asynchat 4 | import asyncore 5 | import socket 6 | import threading 7 | import time 8 | import optparse 9 | import sys 10 | from time import sleep 11 | import struct 12 | import re 13 | import logging 14 | import logging.handlers 15 | import datetime 16 | import os 17 | import math 18 | import mapping2 as mapping 19 | import traceback 20 | import json 21 | import ComfoNetCan as CN 22 | 23 | # CAN frame packing/unpacking (see `struct can_frame` in ) 24 | can_frame_fmt = "=IB3x8s" 25 | 26 | def dissect_can_frame(frame): 27 | can_id, can_dlc, data = struct.unpack(can_frame_fmt, frame) 28 | if can_id & socket.CAN_RTR_FLAG != 0: 29 | print("RTR received from %08X"%(can_id&socket.CAN_EFF_MASK)) 30 | return(0,0,[]) 31 | can_id &= socket.CAN_EFF_MASK 32 | 33 | return (can_id, can_dlc, data[:can_dlc]) 34 | 35 | class sink(): 36 | def __init__(self): 37 | pass 38 | 39 | def push(self, msg): 40 | pass 41 | 42 | class StreamToLogger(object): 43 | """ 44 | Fake file-like stream object that redirects writes to a logger instance. 45 | """ 46 | def __init__(self, logger, log_level=logging.INFO): 47 | self.logger = logger 48 | self.log_level = log_level 49 | self.linebuf = '' 50 | 51 | def write(self, buf): 52 | for line in buf.rstrip().splitlines(): 53 | self.logger.log(self.log_level, line.rstrip()) 54 | 55 | def flush(self): 56 | pass 57 | 58 | class Redirector: 59 | def __init__(self, socketCAN, client, spy=False): 60 | self.SCan = socketCAN 61 | self.connection = client 62 | self.spy = spy 63 | self._write_lock = threading.Lock() 64 | self.power = 0 65 | self.sendpoweron = 0 66 | self.sendpoweroff = 0 67 | self.tempspy = 0 68 | self.clickcount = 0 69 | self.clickflag = False 70 | self.activated = True 71 | self.activetime = 0 72 | self.time = 0 73 | self.last_steering_key = [0xC0,0x00] 74 | self.AI=-1 75 | self.lastpos = -1 76 | self.d1B8 = [0x0F,0xC0,0xF6,0xFF,0x60,0x21] 77 | self.aux = False 78 | self.torque = 0 79 | self.power = 0 80 | self.torquecnt = 0 81 | self.consumecnt = 0 82 | self.consumed = 0 83 | self.consumption = 0.0 84 | self.speed = 0 85 | self.status = { 86 | "Power":"Off", 87 | "Running":"Off", 88 | "Volts":" 0.00", 89 | } 90 | self.kwplist = [] 91 | self.kwpdata = [] 92 | self.kwpsource = -1 93 | self.kwpenable = True 94 | self.canlist={} 95 | for n in dir(self): 96 | if n[0:3] == 'can': 97 | func = getattr(self, n) 98 | if callable(func): 99 | can_id = int(n[3:],16) 100 | self.canlist[can_id]=func 101 | print(self.canlist) 102 | self.cnet = CN.ComfoNet(self.SCan) 103 | self.cnet.FindComfoAirQ() 104 | 105 | def shortcut(self): 106 | """connect the serial port to the TCP port by copying everything 107 | from one side to the other""" 108 | self.alive = True 109 | self.thread_read = threading.Thread(target=self.reader) 110 | self.thread_read.setDaemon(True) 111 | self.thread_read.setName('serial->socket') 112 | self.thread_read.start() 113 | self.writer() 114 | 115 | def _readline(self): 116 | eol = b'\r' 117 | leneol = len(eol) 118 | line = bytearray() 119 | while True: 120 | c = self.serial.read(1) 121 | if c: 122 | line += c 123 | if line[-leneol:] == eol: 124 | break 125 | else: 126 | break 127 | return bytes(line) 128 | 129 | def _sendkey(self, key): 130 | if self.connection is not None: 131 | #try: 132 | self.connection.push(("000000037ff07bfe 00 "+key+" lcdd\n").encode()) 133 | self.connection.push(("000000037ff07bfe 01 "+key+" lcdd\n").encode()) 134 | #except: 135 | # pass 136 | 137 | def send(self, msg): 138 | self.connection.push((msg+'\n').encode()) 139 | 140 | def write(self, msg, data=[]): 141 | if isinstance(msg, str): 142 | can_id = int(msg[1:4],16) 143 | can_dlc = int(msg[4]) 144 | data = [(int(msg[n*2+5:n*2+7],16) if n < can_dlc else 0) for n in range(8)] 145 | elif isinstance(msg, int): 146 | can_id = msg 147 | can_dlc = len(data) 148 | data = [(data[n] if n < can_dlc else 0) for n in range(8)] 149 | else: 150 | print('Error!!!') 151 | print(msg) 152 | print(data) 153 | pass 154 | 155 | can_frame_fmt2 = "=IB3x8B" 156 | self.SCan.send(struct.pack(can_frame_fmt2, can_id, can_dlc, *data)) 157 | 158 | def update_html(self): 159 | tempdata = [] 160 | for key in self.temperatures: 161 | A = math.log(self.humidities[key] / 100) + (17.62 * self.temperatures[key] / (243.12 + self.temperatures[key])); 162 | Td = 243.12 * A / (17.62 - A); 163 | Tw = self.temperatures[key]*math.atan(0.151977*math.sqrt(self.humidities[key]+8.313659)) + math.atan(self.temperatures[key] + self.humidities[key]) - math.atan(self.humidities[key] - 1.676331) + 0.00391838*math.sqrt((self.humidities[key])**3) * math.atan(0.023101*self.humidities[key]) - 4.686035 164 | tempdata.append({ 165 | "Sensor":key, 166 | "Temp":"%5.02f °C"%self.temperatures[key], 167 | "Humid":"%5.02f %%"%self.humidities[key], 168 | "TDew":"%5.02f °C"%Td, 169 | "Twb":"%5.02f °C"%Tw, 170 | "LastUpdated":self.lastupdated[key].strftime('%H:%M:%S %a %d %b') 171 | }) 172 | json.dump(tempdata, open('/var/www/temperature/confoair.json','w')) 173 | tempdata=[] 174 | for item in sorted(self.gathereddata.items(), key=lambda kv: kv[1]):#sorted(self.gathereddata): 175 | tempdata.append({"measurement":item[1]}) 176 | json.dump(tempdata, open('/var/www/temperature/confoair2.json','w')) 177 | 178 | 179 | def reader(self): 180 | """loop forever and copy serial->socket""" 181 | self.receivelist = [] 182 | self.temperatures = { 183 | "inletbefore":10.0, 184 | "inletafter":10.0, 185 | "outletbefore":10.0, 186 | "outletafter":10.0, 187 | } 188 | self.humidities = { 189 | "inletbefore":10.0, 190 | "outletbefore":10.0, 191 | "inletafter":10.0, 192 | "outletafter":10.0, 193 | } 194 | self.lastupdated = { 195 | "inletbefore":datetime.datetime.now(), 196 | "outletbefore":datetime.datetime.now(), 197 | "inletafter":datetime.datetime.now(), 198 | "outletafter":datetime.datetime.now(), 199 | } 200 | 201 | sys.stdout.write("Starting to read the serial CANBUS input\n") 202 | self.gathereddata = {} 203 | 204 | ntouch = len(mapping.mapping) 205 | while True: 206 | try: 207 | if ntouch > 0: 208 | ntouch -= 1 209 | self.cnet.request_tdpo(list(mapping.mapping)[ntouch]) 210 | 211 | cf, addr = self.SCan.recvfrom(16) 212 | 213 | can_id, can_dlc, data = dissect_can_frame(cf) 214 | #print('Received: can_id=%x, can_dlc=%x, data=%s' % dissect_can_frame(cf)) 215 | if can_id == 0x10040001: 216 | self.write(0x10140001|socket.CAN_EFF_FLAG,data) 217 | if can_id == 0x10000001: 218 | #self.write(0x10000005|socket.CAN_EFF_FLAG, []) 219 | pass 220 | can_str = '%08X'%can_id 221 | pdid = (can_id>>14)&0x7ff 222 | if (can_id&0x1F000000) == 0 and pdid in mapping.mapping: 223 | stuff = mapping.mapping[pdid] 224 | try: 225 | self.gathereddata[can_str]='%s_%d %.2f %s'%(stuff["name"], (can_id>>14), stuff["transformation"](data),stuff["unit"]) 226 | namesplit = stuff["name"].split('_') 227 | #print(namesplit) 228 | if len(namesplit)>0 and namesplit[0]=="temperature": 229 | key = namesplit[1]+namesplit[2] 230 | self.temperatures[key]=stuff["transformation"](data) 231 | self.lastupdated[key] = datetime.datetime.now() 232 | elif len(namesplit)>1 and namesplit[1]=="humidity": 233 | key = namesplit[2]+namesplit[3] 234 | self.humidities[key]=stuff["transformation"](data) 235 | self.lastupdated[key] = datetime.datetime.now() 236 | elif len(namesplit)>1 and namesplit[1]=="volume": 237 | self.speed == stuff["transformation"](data) 238 | 239 | 240 | except: 241 | print(traceback.format_exc()) 242 | pass 243 | else: 244 | word = 0 245 | for n in range(can_dlc): 246 | word += data[n]<<(8*n) 247 | self.gathereddata[can_str]='_'.join(["z--Unknown",can_str, '%d'%(can_id>>14), ' '.join(['%X'%x for x in data]), ' ' if can_dlc<1 else '%d'%(word) ] ) 248 | pass 249 | #print("Unknown: %s"%can_str) 250 | #print("\x1b[2J\x1b[H") 251 | for key in sorted(self.gathereddata): 252 | print(self.gathereddata[key]) 253 | pass 254 | 255 | if (can_id>>8) == 0x100000: 256 | self.update_html() 257 | 258 | except (socket.error): 259 | sys.stderr.write('ERROR in the CAN socket code somewhere...\n') 260 | # probably got disconnected 261 | break 262 | self.alive = False 263 | except: 264 | raise 265 | 266 | def stop(self): 267 | """Stop copying""" 268 | if self.alive: 269 | self.alive = False 270 | self.thread_read.join() 271 | 272 | touchlist = [ 273 | 0x00148068, 274 | 0x00454068, 275 | 0x00458068, 276 | 0x001E0068, 277 | 0x001DC068, 278 | 0x001E8068, 279 | 0x001E4068, 280 | 0x00200068, 281 | 0x001D4068, 282 | 0x001D8068, 283 | 0x0082C068, 284 | 0x00384068, 285 | 0x00144068, 286 | 0x00824068, 287 | 0x00810068, 288 | 0x00208068, 289 | 0x00344068, 290 | 0x00370068, 291 | 0x00300068, 292 | 0x00044068, 293 | 0x00204068, 294 | 0x00084068, 295 | 0x00804068, 296 | 0x00644068, 297 | 0x00354068, 298 | 0x00390068, 299 | 0x0035C068, 300 | 0x0080C068, 301 | 0x000E0068, 302 | 0x00604068, 303 | 0x00450068, 304 | 0x00378068, 305 | 0x00818068, 306 | 0x00820068, 307 | 0x001D0068, 308 | 0x00350068, 309 | 0x0081C068, 310 | 0x00448068, 311 | 0x0044C068, 312 | 0x00560068, 313 | 0x00374068, 314 | 0x00808068, 315 | 0x00040068, 316 | 0x10040001, 317 | 0x00120068, 318 | 0x00688068, 319 | 0x00358068, 320 | 0x00104068, 321 | 0x00544068, 322 | 0x00814068, 323 | 0x000C4068, 324 | 0x00828068, 325 | 0x00488068, 326 | 0x0048C068, 327 | 0x00490068, 328 | 0x00494068, 329 | 0x00498068, 330 | 0x004C4068, 331 | 0x004C8068, 332 | 0x00388068, 333 | 0x00188068, 334 | 0x00184068, 335 | 0x00108068, 336 | 0x0038C068, 337 | 0x00360068, 338 | 0x00398068, 339 | ] 340 | 341 | if __name__ == '__main__': 342 | 343 | parser = optparse.OptionParser( 344 | usage = "%prog [options] [port [baudrate]]", 345 | description = "Simple Serial to Network (TCP/IP) redirector.", 346 | ) 347 | 348 | parser.add_option("-q", "--quiet", 349 | dest = "quiet", 350 | action = "store_true", 351 | help = "suppress non error messages", 352 | default = False 353 | ) 354 | 355 | parser.add_option("--spy", 356 | dest = "spy", 357 | action = "store_true", 358 | help = "peek at the communication and print all data to the console", 359 | default = False 360 | ) 361 | 362 | parser.add_option("-s", "--socket", 363 | dest = "socket", 364 | help = "Socket to create for communication with can app", 365 | default = "/var/run/sockfile", 366 | ) 367 | 368 | (options, args) = parser.parse_args() 369 | 370 | # create a raw socket and bind it to the given CAN interface 371 | s = socket.socket(socket.AF_CAN, socket.SOCK_RAW, socket.CAN_RAW) 372 | s.bind(("slcan0",)) 373 | 374 | if options.quiet: 375 | stdout_logger = logging.getLogger('log') 376 | stdout_logger.setLevel(logging.DEBUG) 377 | handler = logging.handlers.RotatingFileHandler( 378 | '/tmp/can.log', maxBytes=1e6, backupCount=5) 379 | formatter = logging.Formatter('%(asctime)s:%(levelname)s:%(name)s:%(message)s') 380 | handler.setFormatter(formatter) 381 | handler.setLevel(logging.DEBUG) 382 | stdout_logger.addHandler(handler) 383 | sl = StreamToLogger(stdout_logger, logging.INFO) 384 | sys.stdout = sl 385 | sys.stderr = sl 386 | 387 | r = Redirector( 388 | s, 389 | sink(), 390 | options.spy, 391 | ) 392 | 393 | try: 394 | while True: 395 | try: 396 | r.reader() 397 | if options.spy: sys.stdout.write('\n') 398 | sys.stderr.write('Disconnected\n') 399 | s = socket.socket(socket.AF_CAN, socket.SOCK_RAW, socket.CAN_RAW) 400 | s.bind(("slcan0",)) 401 | r.SCan = s 402 | #connection.close() 403 | except KeyboardInterrupt: 404 | break 405 | except (socket.error): 406 | sys.stderr.write('ERROR\n') 407 | sleep(1) 408 | #msg = input('> ') 409 | #msg = 'UP' 410 | #time.sleep(5) 411 | #client.push((msg + '\n').encode()) 412 | #client.push(b'dit is een lang verhaal\nmet terminators erin\nUP\nhoe gaat het ding hiermee om?\n') 413 | finally: 414 | pass 415 | 416 | # vim: et:sw=4:ts=4:smarttab:foldmethod=indent:si 417 | -------------------------------------------------------------------------------- /mapping2.py: -------------------------------------------------------------------------------- 1 | from struct import unpack 2 | 3 | def transform_temperature(value: list) -> float: 4 | parts = bytes(value[0:2]) 5 | word = unpack(' float: 10 | parts = value[0:2] 11 | word = unpack(' float: 15 | word = 0 16 | for n in range(len(value)): 17 | word += value[n]<<(n*8) 18 | return word 19 | 20 | def transform_enum(enum: dict): 21 | def f(value: list) -> str: 22 | v = int(value[0]) 23 | if v in enum: 24 | return enum[v] 25 | return "unknown" 26 | return f 27 | 28 | def uint_to_bits(value): 29 | """Convert an unsigned integer to a list of set bits.""" 30 | bits = [] 31 | j = 0 32 | for i in range(64): 33 | if value & (1 << i): 34 | bits.append(j) 35 | j += 1 36 | return bits 37 | 38 | def calculate_airflow_constraints(value): 39 | """Calculate the airflow constraints based on the bitshift value.""" 40 | bits = uint_to_bits(value) 41 | if 45 not in bits: 42 | return [] 43 | 44 | constraints = [] 45 | if 2 in bits or 3 in bits: 46 | constraints.append("Resistance") 47 | if 4 in bits: 48 | constraints.append("PreheaterNegative") 49 | if 5 in bits or 7 in bits: 50 | constraints.append("NoiseGuard") 51 | if 6 in bits or 8 in bits: 52 | constraints.append("ResistanceGuard") 53 | if 9 in bits: 54 | constraints.append("FrostProtection") 55 | if 10 in bits: 56 | constraints.append("Bypass") 57 | if 12 in bits: 58 | constraints.append("AnalogInput1") 59 | if 13 in bits: 60 | constraints.append("AnalogInput2") 61 | if 14 in bits: 62 | constraints.append("AnalogInput3") 63 | if 15 in bits: 64 | constraints.append("AnalogInput4") 65 | if 16 in bits: 66 | constraints.append("Hood") 67 | if 18 in bits: 68 | constraints.append("AnalogPreset") 69 | if 19 in bits: 70 | constraints.append("ComfoCool") 71 | if 22 in bits: 72 | constraints.append("PreheaterPositive") 73 | if 23 in bits: 74 | constraints.append("RFSensorFlowPreset") 75 | if 24 in bits: 76 | constraints.append("RFSensorFlowProportional") 77 | if 25 in bits: 78 | constraints.append("TemperatureComfort") 79 | if 26 in bits: 80 | constraints.append("HumidityComfort") 81 | if 27 in bits: 82 | constraints.append("HumidityProtection") 83 | if 47 in bits: 84 | constraints.append("CO2ZoneX1") 85 | if 48 in bits: 86 | constraints.append("CO2ZoneX2") 87 | if 49 in bits: 88 | constraints.append("CO2ZoneX3") 89 | if 50 in bits: 90 | constraints.append("CO2ZoneX4") 91 | if 51 in bits: 92 | constraints.append("CO2ZoneX5") 93 | if 52 in bits: 94 | constraints.append("CO2ZoneX6") 95 | if 53 in bits: 96 | constraints.append("CO2ZoneX7") 97 | if 54 in bits: 98 | constraints.append("CO2ZoneX8") 99 | 100 | return constraints 101 | 102 | def transform_ventilation_constraints(value: list) -> str: 103 | print(value) 104 | value = bytes(value[0:8]) 105 | print(value) 106 | value = int.from_bytes(value, "little") 107 | print(value) 108 | return ", ".join(calculate_airflow_constraints(value)) 109 | 110 | device_state_arr = ["init", "normal", "filterwizard", "commissioning", "supplierfactory", "zehnderfactory", "standby", "away", "DFC"] 111 | changing_filters_arr = ["active", "changing_filter"] 112 | operating_mode_enum = {-1: "auto", 1: "limited manual", 5: "unlimited manual"} 113 | bypass_mode_enum = {0: "auto", 1: "open", 2: "close"} 114 | sensor_based_enum = {0: "disabled", 1: "active", 2:"overruling"} 115 | 116 | mapping = { 117 | 16: { 118 | "name": "device_state", 119 | "unit": "".join(["%s=%s" % x for x in enumerate(device_state_arr)]), 120 | "transformation": lambda x: (device_state_arr[int(x[0])] if int(x[0]) < len(device_state_arr) else "unknown") 121 | }, 122 | 17: { 123 | "name": "z_unknown_NwoNode_17", 124 | "unit": "", 125 | "transformation": transform_any 126 | }, 127 | 18: { 128 | "name": "changing_filters", 129 | "unit": "".join(["%s=%s" % x for x in enumerate(changing_filters_arr)]), 130 | "transformation": lambda x: (changing_filters_arr[int(x[0])] if int(x[0]) < len(changing_filters_arr) else "unknown") 131 | }, 132 | 33: { 133 | "name": "z_unknown_Value_33", 134 | "unit": "", 135 | "transformation": transform_any 136 | }, 137 | 49: { 138 | "name": "operating_mode", 139 | "unit": "-1=auto,1=limited_manual,5=unlimited_manual", 140 | "transformation": transform_enum(operating_mode_enum) 141 | }, 142 | 56: { 143 | "name": "z_unknown_Value_56", 144 | "unit": "", 145 | "transformation": transform_any 146 | }, 147 | 65: { 148 | "name": "ventilation_level", 149 | "unit": "level", 150 | "transformation": lambda x: float(x[0]) 151 | }, 152 | 66: { 153 | "name": "bypass_state", 154 | "unit": "0=auto,1=open,2=close", 155 | "transformation": transform_enum(bypass_mode_enum) 156 | }, 157 | 67: { 158 | "name": "comfocool_profile", 159 | "unit": "", 160 | "transformation": lambda x: int(x[0]) 161 | }, 162 | 72: { 163 | "name": "z_unknown_Value_72", 164 | "unit": "", 165 | "transformation": transform_any 166 | }, 167 | 81: { 168 | "name": "Timer1_fan_speed_next_change", 169 | "unit": "s", 170 | "transformation": transform_any 171 | }, 172 | 82: { 173 | "name": "Timer2_bypass_next_change", 174 | "unit": "s", 175 | "transformation": transform_any 176 | }, 177 | 83: { 178 | "name": "Timer3", 179 | "unit": "s", 180 | "transformation": transform_any 181 | }, 182 | 84: { 183 | "name": "Timer4", 184 | "unit": "s", 185 | "transformation": transform_any 186 | }, 187 | 85: { 188 | "name": "Timer5_comfocool_next_change", 189 | "unit": "s", 190 | "transformation": transform_any 191 | }, 192 | 86: { 193 | "name": "Timer6_supply_fan_next_change", 194 | "unit": "s", 195 | "transformation": transform_any 196 | }, 197 | 87: { 198 | "name": "Timer7_exhaust_fan_next_change", 199 | "unit": "s", 200 | "transformation": transform_any 201 | }, 202 | 88: { 203 | "name": "Timer8", 204 | "unit": "s", 205 | "transformation": transform_any 206 | }, 207 | 96: { 208 | "name": "bypass_..._ValveMsg", 209 | "unit": "unknown", 210 | "transformation": transform_any 211 | }, 212 | 97: { 213 | "name": "bypass_b_status", 214 | "unit": "unknown", 215 | "transformation": transform_air_volume 216 | }, 217 | 98: { 218 | "name": "bypass_a_status", 219 | "unit": "unknown", 220 | "transformation": transform_air_volume 221 | }, 222 | 115: { 223 | "name": "ventilator_enabled_output", 224 | "unit": "", 225 | "transformation": transform_any 226 | }, 227 | 116: { 228 | "name": "ventilator_enabled_input", 229 | "unit": "", 230 | "transformation": transform_any 231 | }, 232 | 117: { 233 | "name": "ventilator_power_percent_output", 234 | "unit": "%", 235 | "transformation": lambda x: float(x[0]) 236 | }, 237 | 118: { 238 | "name": "ventilator_power_percent_input", 239 | "unit": "%", 240 | "transformation": lambda x: float(x[0]) 241 | }, 242 | 119: { 243 | "name": "ventilator_air_volume_output", 244 | "unit": "m3", 245 | "transformation": transform_air_volume 246 | }, 247 | 120: { 248 | "name": "ventilator_air_volume_input", 249 | "unit": "m3", 250 | "transformation": transform_air_volume 251 | }, 252 | 121: { 253 | "name": "ventilator_speed_output", 254 | "unit": "rpm", 255 | "transformation": transform_air_volume 256 | }, 257 | 122: { 258 | "name": "ventilator_speed_input", 259 | "unit": "rpm", 260 | "transformation": transform_air_volume 261 | }, 262 | 128: { 263 | "name": "Power_consumption_actual", 264 | "unit": "W", 265 | "transformation": lambda x: float(x[0]) 266 | }, 267 | 129: { 268 | "name": "Power_consumption_this_year", 269 | "unit": "kWh", 270 | "transformation": transform_air_volume 271 | }, 272 | 130: { 273 | "name": "Power_consumption_lifetime", 274 | "unit": "kWh", 275 | "transformation": transform_air_volume 276 | }, 277 | 144: { 278 | "name": "Power_PreHeater_this_year", 279 | "unit": "kWh", 280 | "transformation": transform_any 281 | }, 282 | 145: { 283 | "name": "Power_PreHeater_total", 284 | "unit": "kWh", 285 | "transformation": transform_any 286 | }, 287 | 146: { 288 | "name": "Power_PreHeater_actual", 289 | "unit": "W", 290 | "transformation": transform_any 291 | }, 292 | 176: { 293 | "name": "rf_pairing_mode", 294 | "unit": "0=not_running,1=running,2=done,3=failed,4=aborted", 295 | "transformation": transform_any 296 | }, 297 | 192: { 298 | "name": "days_until_next_filter_change", 299 | "unit": "days", 300 | "transformation": transform_air_volume 301 | }, 302 | 208: { 303 | "name": "device_temperature_unit", 304 | "unit": "0=celsius,1=fahrenheit", 305 | "transformation": transform_any 306 | }, 307 | 209: { 308 | "name": "RMOT", 309 | "unit": "°C", 310 | "transformation":transform_temperature 311 | }, 312 | 210: { 313 | "name": "heating_season_active", 314 | "unit": "0=inactive,1=active", 315 | "transformation": lambda x: "active" if int(x[0]) == 1 else "inactive" 316 | }, 317 | 211: { 318 | "name": "cooling_season_active", 319 | "unit": "0=inactive,1=active", 320 | "transformation": lambda x: "active" if int(x[0]) == 1 else "inactive" 321 | }, 322 | 212: { 323 | "name": "Target_temperature", 324 | "unit": "°C", 325 | "transformation": transform_temperature 326 | }, 327 | 213: { 328 | "name": "Power_avoided_heating_actual", 329 | "unit": "W", 330 | "transformation": transform_any 331 | }, 332 | 214: { 333 | "name": "Power_avoided_heating_this_year", 334 | "unit": "kWh", 335 | "transformation": transform_air_volume 336 | }, 337 | 215: { 338 | "name": "Power_avoided_heating_lifetime", 339 | "unit": "kWh", 340 | "transformation": transform_air_volume 341 | }, 342 | 216: { 343 | "name": "Power_avoided_cooling_actual", 344 | "unit": "W", 345 | "transformation": transform_any 346 | }, 347 | 217: { 348 | "name": "Power_avoided_cooling_this_year", 349 | "unit": "kWh", 350 | "transformation": transform_air_volume 351 | }, 352 | 218: { 353 | "name": "Power_avoided_cooling_lifetime", 354 | "unit": "kWh", 355 | "transformation": transform_air_volume 356 | }, 357 | 219: { 358 | "name": "Power_PreHeater_Target", 359 | "unit": "W", 360 | "transformation": transform_any 361 | }, 362 | 220: { 363 | "name": "temperature_inlet_before_preheater", 364 | "unit": "°C", 365 | "transformation": transform_temperature 366 | }, 367 | 221: { 368 | "name": "temperature_inlet_after_recuperator", 369 | "unit": "°C", 370 | "transformation": transform_temperature 371 | }, 372 | 222: { 373 | "name": "z_Unknown_TempHumConf_222", 374 | "unit": "", 375 | "transformation": transform_any 376 | }, 377 | 224: { 378 | "name": "device_airflow_unit", 379 | "unit": "1=kg/h,2=l/s,3=m3/h", 380 | "transformation": transform_any 381 | }, 382 | 225: { 383 | "name": "sensor_based_ventilation", 384 | "unit": "0=disabled, 1=active, 2=overruling", 385 | "transformation": transform_enum(sensor_based_enum) 386 | }, 387 | 226: { 388 | "name": "fan_speed_0_100_200_300", 389 | "unit": "0,100,200,300", 390 | "transformation": transform_any 391 | }, 392 | 227: { 393 | "name": "bypass_open", 394 | "unit": "%", 395 | "transformation": lambda x: float(x[0]) 396 | }, 397 | 228: { 398 | "name": "frost_disbalance", 399 | "unit": "%", 400 | "transformation": lambda x: float(x[0]) 401 | }, 402 | 229: { 403 | "name": "z_Unknown_VentConf_229", 404 | "unit": "", 405 | "transformation": transform_any 406 | }, 407 | 230: { 408 | "name": "ventilation_constraints", 409 | "unit": "", 410 | "transformation": transform_ventilation_constraints 411 | }, 412 | 256: { 413 | "name": "current_menu_mode", 414 | "unit": "1=basic,2=advanced,3=installer", 415 | "transformation": transform_any 416 | }, 417 | 257: { 418 | "name": "z_Unknown_NodeConf_257", 419 | "unit": "unknown", 420 | "transformation": transform_any 421 | }, 422 | 273: { 423 | "name": "temperature_something...", 424 | "unit": "°C", 425 | "transformation": transform_temperature 426 | }, 427 | 274: { 428 | "name": "temperature_outlet_before_recuperator", 429 | "unit": "°C", 430 | "transformation": transform_temperature 431 | }, 432 | 275: { 433 | "name": "temperature_outlet_after_recuperator", 434 | "unit": "°C", 435 | "transformation": transform_temperature 436 | }, 437 | 276: { 438 | "name": "temperature_inlet_before_preheater", 439 | "unit": "°C", 440 | "transformation": transform_temperature 441 | }, 442 | 277: { 443 | "name": "temperature_inlet_before_recuperator", 444 | "unit": "°C", 445 | "transformation": transform_temperature 446 | }, 447 | 278: { 448 | "name": "temperature_inlet_after_recuperator", 449 | "unit": "°C", 450 | "transformation": transform_temperature 451 | }, 452 | 289: { 453 | "name": "z_unknown_HumSens", 454 | "unit": "", 455 | "transformation": transform_any 456 | }, 457 | 290: { 458 | "name": "air_humidity_outlet_before_recuperator", 459 | "unit": "%", 460 | "transformation": lambda x: float(x[0]) 461 | }, 462 | 291: { 463 | "name": "air_humidity_outlet_after_recuperator", 464 | "unit": "%", 465 | "transformation": lambda x: float(x[0]) 466 | }, 467 | 292: { 468 | "name": "air_humidity_inlet_before_preheater", 469 | "unit": "%", 470 | "transformation": lambda x: float(x[0]) 471 | }, 472 | 293: { 473 | "name": "air_humidity_inlet_before_recuperator", 474 | "unit": "%", 475 | "transformation": lambda x: float(x[0]) 476 | }, 477 | 294: { 478 | "name": "air_humidity_inlet_after_recuperator", 479 | "unit": "%", 480 | "transformation": lambda x: float(x[0]) 481 | }, 482 | 305: { 483 | "name": "PresSens_exhaust", 484 | "unit": "Pa", 485 | "transformation": transform_any 486 | }, 487 | 306: { 488 | "name": "PresSens_inlet", 489 | "unit": "Pa", 490 | "transformation": transform_any 491 | }, 492 | 337: { 493 | "name": "z_unknown_Value_337", 494 | "unit": "", 495 | "transformation": transform_any 496 | }, 497 | 344: { 498 | "name": "z_unknown_Value_344", 499 | "unit": "", 500 | "transformation": transform_any 501 | }, 502 | 369: { 503 | "name": "z_Unknown_AnalogInput_369", 504 | "unit": "V?", 505 | "transformation": transform_any 506 | }, 507 | 370: { 508 | "name": "z_Unknown_AnalogInput_370", 509 | "unit": "V?", 510 | "transformation": transform_any 511 | }, 512 | 371: { 513 | "name": "z_Unknown_AnalogInput_371", 514 | "unit": "V?", 515 | "transformation": transform_any 516 | }, 517 | 372: { 518 | "name": "z_Unknown_AnalogInput_372", 519 | "unit": "V?", 520 | "transformation": transform_any 521 | }, 522 | 385: { 523 | "name": "z_unknown_Value_385", 524 | "unit": "", 525 | "transformation": transform_any 526 | }, 527 | 400: { 528 | "name": "z_Unknown_PostHeater_ActualPower", 529 | "unit": "W", 530 | "transformation": transform_any 531 | }, 532 | 401: { 533 | "name": "z_Unknown_PostHeater_ThisYear", 534 | "unit": "kWh", 535 | "transformation": transform_any 536 | }, 537 | 402: { 538 | "name": "z_Unknown_PostHeater_Total", 539 | "unit": "kWh", 540 | "transformation": transform_any 541 | }, 542 | 418: { 543 | "name": "z_unknown_Value_418", 544 | "unit": "", 545 | "transformation": transform_any 546 | }, 547 | 513: { 548 | "name": "z_unknown_Value_513", 549 | "unit": "", 550 | "transformation": transform_any 551 | }, 552 | 514: { 553 | "name": "z_unknown_Value_514", 554 | "unit": "", 555 | "transformation": transform_any 556 | }, 557 | 515: { 558 | "name": "z_unknown_Value_515", 559 | "unit": "", 560 | "transformation": transform_any 561 | }, 562 | 516: { 563 | "name": "z_unknown_Value_516", 564 | "unit": "", 565 | "transformation": transform_any 566 | }, 567 | 517: { 568 | "name": "z_unknown_Value_517", 569 | "unit": "", 570 | "transformation": transform_any 571 | }, 572 | 518: { 573 | "name": "z_unknown_Value_518", 574 | "unit": "", 575 | "transformation": transform_any 576 | }, 577 | 519: { 578 | "name": "z_unknown_Value_519", 579 | "unit": "", 580 | "transformation": transform_any 581 | }, 582 | 520: { 583 | "name": "z_unknown_Value_520", 584 | "unit": "", 585 | "transformation": transform_any 586 | }, 587 | 521: { 588 | "name": "z_unknown_Value_521", 589 | "unit": "", 590 | "transformation": transform_any 591 | }, 592 | 522: { 593 | "name": "z_unknown_Value_522", 594 | "unit": "", 595 | "transformation": transform_any 596 | }, 597 | 523: { 598 | "name": "z_unknown_Value_523", 599 | "unit": "", 600 | "transformation": transform_any 601 | }, 602 | 16400: { 603 | "name": "z_unknown_Value_16400", 604 | "unit": "", 605 | "transformation": transform_any 606 | }, 607 | #00398041 unknown 0 0 0 0 0 0 0 0 608 | } 609 | 610 | command_mapping = { 611 | "set_ventilation_level_0": b'T1F07505180100201C00000000\r', 612 | "set_ventilation_level_1": b'T1F07505180100201C00000100\r', 613 | "set_ventilation_level_2": b'T1F07505180100201C00000200\r', 614 | "set_ventilation_level_3": b'T1F07505180100201C00000300\r', 615 | "auto_mode": b'T1F075051485150801\r', # verified (also: T1F051051485150801\r) 616 | "manual_mode": b'T1F07505180084150101000000\r', # verified (also: T1F051051485150801\r) 617 | "temperature_profile_cool": b'T0010C041101\r', 618 | "temperature_profile_normal": b'T0010C041100\r', 619 | "temperature_profile_warm": b'T0010C041102\r', 620 | "close_bypass": b'T00108041102\r', 621 | "open_bypass": b'T00108041101\r', 622 | "auto_bypass": b'T00108041100\r', 623 | "basis_menu": b"T00400041100\r", 624 | "extended_menu": b"T00400041101\r" 625 | } 626 | --------------------------------------------------------------------------------