├── old ├── src │ ├── main │ │ ├── python │ │ │ ├── zcan │ │ │ │ ├── __init__.py │ │ │ │ ├── exception.py │ │ │ │ ├── util.py │ │ │ │ ├── queue.py │ │ │ │ ├── influxdb_writer.py │ │ │ │ ├── main.py │ │ │ │ ├── cli.py │ │ │ │ ├── can.py │ │ │ │ └── mapping.py │ │ │ └── pyserial.py │ │ └── scripts │ │ │ └── zcan │ └── unittest │ │ └── python │ │ └── can_tests.py ├── install.sh ├── build.py └── setup.py ├── README.md ├── .gitignore ├── sendmsg.py ├── zcanbridge.py ├── ComfoNetCan.py ├── mapping2.py └── testcan.py /old/src/main/python/zcan/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | __version__ = '${version}' 3 | -------------------------------------------------------------------------------- /old/src/main/python/zcan/exception.py: -------------------------------------------------------------------------------- 1 | class ZCanBaseException(Exception): 2 | pass -------------------------------------------------------------------------------- /old/src/main/scripts/zcan: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from zcan.cli import cli 4 | 5 | cli() -------------------------------------------------------------------------------- /old/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source /root/zcan_env/bin/activate 4 | cd /root/zcan 5 | git pull -r 6 | pip uninstall -y zcan 7 | pyb install 8 | killall zcan 9 | cd /tmp 10 | nohup zcan run & 11 | -------------------------------------------------------------------------------- /old/src/main/python/zcan/util.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import datetime 4 | 5 | 6 | def get_logger(suffix=None): 7 | logging.basicConfig(format='%(asctime)s %(levelname)s: %(message)s', 8 | datefmt='%d.%m.%Y %H:%M:%S') 9 | 10 | if suffix: 11 | logger = logging.getLogger('zcan.{0}'.format(suffix)) 12 | else: 13 | logger = logging.getLogger('zcan') 14 | 15 | logger.setLevel(logging.INFO) 16 | return logger 17 | 18 | 19 | def get_current_time(): 20 | return datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ') 21 | -------------------------------------------------------------------------------- /old/src/main/python/zcan/queue.py: -------------------------------------------------------------------------------- 1 | import pika 2 | from pika import BlockingConnection 3 | 4 | 5 | def callback(ch, method, properties, body): 6 | print(" [x] Received %r" % body) 7 | 8 | 9 | connection = BlockingConnection(pika.ConnectionParameters('odroid64')) 10 | channel = connection.channel() 11 | 12 | channel.queue_declare(queue='hello') 13 | 14 | channel.basic_publish(exchange='', 15 | routing_key='hello', 16 | body='Hello World!') 17 | print("[x] Sent 'Hello World!'") 18 | 19 | channel.basic_consume(callback, 20 | queue='hello', 21 | no_ack=True) 22 | 23 | print(' [*] Waiting for messages. To exit press CTRL+C') 24 | channel.start_consuming() 25 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /old/src/main/python/zcan/influxdb_writer.py: -------------------------------------------------------------------------------- 1 | from influxdb import InfluxDBClient 2 | 3 | from zcan.can import Measurement 4 | from zcan.util import get_logger, get_current_time 5 | 6 | 7 | class InfluxDbWriter(object): 8 | def __init__(self, host="influxdb", port=8086, user="root", password="root", db="zcan"): 9 | self.logger = get_logger("InfluxDbWriter") 10 | self.db = db 11 | 12 | self.client = InfluxDBClient(host, port, user, password, db) 13 | self.client.create_database(db) 14 | 15 | def send_metric_datapoint(self, measurement: Measurement): 16 | json_body = [ 17 | { 18 | "measurement": measurement.name, 19 | "tags": { 20 | "id": measurement.id, 21 | "unit": measurement.unit 22 | }, 23 | "time": get_current_time(), 24 | "fields": { 25 | "value": measurement.value 26 | } 27 | } 28 | ] 29 | try: 30 | self.client.write_points(json_body, database=self.db) 31 | except Exception as e: 32 | self.logger.exception(e) 33 | -------------------------------------------------------------------------------- /old/src/unittest/python/can_tests.py: -------------------------------------------------------------------------------- 1 | from unittest2 import TestCase 2 | from zcan.can import CanBusInterface, Message 3 | 4 | 5 | class CanTests(TestCase): 6 | def test_to_can_message_converts_extended_transmit_frame_with_one_byte(self): 7 | result = CanBusInterface._to_can_message(b'T001D804111D\r') 8 | print(result) 9 | self.assertEqual(result, Message("T", "001D8041", 1, [29])) 10 | 11 | def test_to_can_message_converts_extended_transmit_frame_with_two_bytes(self): 12 | result = CanBusInterface._to_can_message(b'T001E804123B05\r') 13 | self.assertEqual(result, Message("T", "001E8041", 2, [59, 5])) 14 | 15 | def test_to_can_message_converts_extended_transmit_frame_with_four_bytes(self): 16 | result = CanBusInterface._to_can_message(b'T00144041422180200\r') 17 | self.assertEqual(result, Message("T", "00144041", 4, [34, 24, 2, 0])) 18 | 19 | def test_to_can_message_converts_extended_transmit_frame_with_strange_prefix(self): 20 | result = CanBusInterface._to_can_message(b'\x07\x07T00144041420180200\r') 21 | self.assertEqual(result, Message("T", "00144041", 4, [32, 24, 2, 0])) 22 | 23 | def test_to_can_message_converts_extended_transmit_frame_without_data(self): 24 | result = CanBusInterface._to_can_message(b'T100000010\r') 25 | self.assertEqual(result, Message("T", "10000001", 0, [])) 26 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /old/build.py: -------------------------------------------------------------------------------- 1 | from pybuilder.core import use_plugin, init, Author 2 | 3 | use_plugin("python.core") 4 | use_plugin("python.unittest") 5 | use_plugin("python.integrationtest") 6 | use_plugin("python.install_dependencies") 7 | use_plugin("python.flake8") 8 | use_plugin("python.coverage") 9 | use_plugin("python.distutils") 10 | use_plugin('copy_resources') 11 | use_plugin('filter_resources') 12 | 13 | name = "zcan" 14 | 15 | authors = [Author('Marco Hoyer', 'marco_hoyer@gmx.de')] 16 | description = 'zcan - CAN bus adapter daemon for Zehnder ComfoAir Q series ventilation systems' 17 | license = 'APACHE LICENSE, VERSION 2.0' 18 | summary = 'CAN bus adapter daemon for Zehnder ComfoAir Q series ventilation systems' 19 | url = 'https://github.com/marco-hoyer/zcan' 20 | version = '0.0.1' 21 | 22 | default_task = ['clean', 'analyze', 'package'] 23 | 24 | 25 | @init 26 | def set_properties(project): 27 | project.build_depends_on("unittest2") 28 | project.build_depends_on("mock") 29 | project.depends_on("argparse") 30 | project.depends_on("pyyaml") 31 | project.depends_on("xknx") 32 | project.depends_on("influxdb") 33 | project.depends_on("pyserial") 34 | project.depends_on("click") 35 | 36 | project.set_property('integrationtest_inherit_environment', True) 37 | project.set_property('coverage_break_build', False) 38 | project.set_property('install_dependencies_upgrade', True) 39 | 40 | project.set_property('copy_resources_target', '$dir_dist') 41 | 42 | project.get_property('filter_resources_glob').extend(['**/zcan/__init__.py']) 43 | project.set_property('distutils_console_scripts', ['zcan=zcan.cli:cli']) 44 | 45 | project.set_property('distutils_classifiers', [ 46 | 'Development Status :: 4 - Beta', 47 | 'Environment :: Console', 48 | 'Intended Audience :: Developers', 49 | 'Intended Audience :: System Administrators', 50 | 'License :: OSI Approved :: Apache Software License', 51 | 'Programming Language :: Python', 52 | 'Topic :: System :: Systems Administration' 53 | ]) 54 | -------------------------------------------------------------------------------- /old/src/main/python/zcan/main.py: -------------------------------------------------------------------------------- 1 | from multiprocessing import Process, Manager 2 | from zcan.can import CanBus 3 | import time 4 | 5 | from zcan.influxdb_writer import InfluxDbWriter 6 | from zcan.util import get_logger 7 | 8 | logger = get_logger() 9 | 10 | 11 | def write(payload): 12 | CanBus().write(payload) 13 | 14 | 15 | def write_ventilation_level(iterator): 16 | messages = [ 17 | bytes("T1F0{}505180084150101000000".format(iterator), encoding="ASCII"), 18 | bytes("T1F0{}505180100201C00000300".format(iterator), encoding="ASCII"), 19 | bytes("T1F0{}14410".format(int(iterator) - 1), encoding="ASCII") 20 | ] 21 | CanBus().write_messages(messages) 22 | 23 | 24 | def get_error_state(): 25 | CanBus().get_error_state() 26 | 27 | 28 | def listen(all: bool): 29 | CanBus().print_messages(all) 30 | 31 | 32 | def main(): 33 | manager = Manager() 34 | measurements = manager.dict() 35 | unknown_messages = manager.dict() 36 | 37 | p1 = Process(target=read_can_bus, args=(measurements, unknown_messages,)) 38 | p2 = Process(target=write_to_influxdb, args=(measurements,)) 39 | # p3 = Process(target=log_unknown_messages, args=(unknown_messages,)) 40 | 41 | p1.start() 42 | p2.start() 43 | # p3.start() 44 | p1.join() 45 | p2.join() 46 | # p3.join() 47 | 48 | 49 | def read_can_bus(m, u): 50 | while True: 51 | try: 52 | CanBus().read_messages(m, u) 53 | except Exception as e: 54 | logger.exception(e) 55 | 56 | 57 | def write_to_influxdb(m): 58 | writer = InfluxDbWriter() 59 | while True: 60 | try: 61 | logger.info("Sending values to influxdb") 62 | for item in m.values(): 63 | logger.debug("Sending to influxdb: {}".format(item)) 64 | writer.send_metric_datapoint(item) 65 | except Exception as e: 66 | logger.exception(e) 67 | 68 | time.sleep(10) 69 | 70 | 71 | def log_unknown_messages(u): 72 | while True: 73 | logger.info("Unknown messages so far") 74 | for item in u.values(): 75 | logger.warn(item) 76 | time.sleep(3600) 77 | -------------------------------------------------------------------------------- /old/setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | 4 | # -*- coding: utf-8 -*- 5 | # 6 | # This file is part of PyBuilder 7 | # 8 | # Copyright 2011-2015 PyBuilder Team 9 | # 10 | # Licensed under the Apache License, Version 2.0 (the "License"); 11 | # you may not use this file except in compliance with the License. 12 | # You may obtain a copy of the License at 13 | # 14 | # http://www.apache.org/licenses/LICENSE-2.0 15 | # 16 | # Unless required by applicable law or agreed to in writing, software 17 | # distributed under the License is distributed on an "AS IS" BASIS, 18 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 19 | # See the License for the specific language governing permissions and 20 | # limitations under the License. 21 | 22 | # 23 | # This script allows to support installation via: 24 | # pip install git+git://@ 25 | # 26 | # This script is designed to be used in combination with `pip install` ONLY 27 | # 28 | # DO NOT RUN MANUALLY 29 | # 30 | 31 | import os 32 | import subprocess 33 | import sys 34 | import glob 35 | import shutil 36 | 37 | from sys import version_info 38 | py3 = version_info[0] == 3 39 | py2 = not py3 40 | if py2: 41 | FileNotFoundError = OSError 42 | 43 | script_dir = os.path.dirname(os.path.realpath(__file__)) 44 | exit_code = 0 45 | try: 46 | subprocess.check_call(["pyb", "--version"]) 47 | except FileNotFoundError as e: 48 | if py3 or py2 and e.errno == 2: 49 | try: 50 | subprocess.check_call([sys.executable, "-m", "pip.__main__", "install", "pybuilder"]) 51 | except subprocess.CalledProcessError as e: 52 | sys.exit(e.returncode) 53 | else: 54 | raise 55 | except subprocess.CalledProcessError as e: 56 | sys.exit(e.returncode) 57 | 58 | try: 59 | subprocess.check_call(["pyb", "clean", "install_build_dependencies", "package", "-o"]) 60 | dist_dir = glob.glob(os.path.join(script_dir, "target", "dist", "*"))[0] 61 | for src_file in glob.glob(os.path.join(dist_dir, "*")): 62 | file_name = os.path.basename(src_file) 63 | target_file_name = os.path.join(script_dir, file_name) 64 | if os.path.exists(target_file_name): 65 | if os.path.isdir(target_file_name): 66 | shutil.rmtree(target_file_name) 67 | else: 68 | os.remove(target_file_name) 69 | shutil.move(src_file, script_dir) 70 | setup_args = sys.argv[1:] 71 | subprocess.check_call([sys.executable, "setup.py"] + setup_args, cwd=script_dir) 72 | except subprocess.CalledProcessError as e: 73 | exit_code = e.returncode 74 | sys.exit(exit_code) 75 | -------------------------------------------------------------------------------- /old/src/main/python/zcan/cli.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | import click 4 | 5 | from zcan.main import main, listen, write, write_ventilation_level, get_error_state 6 | 7 | from zcan.util import get_logger 8 | 9 | LOGGER = get_logger() 10 | 11 | 12 | @click.group(help="zcan Zehnder ComfoAir CAN bus adapter") 13 | def cli(): 14 | pass 15 | 16 | 17 | @cli.command(help="run") 18 | @click.option('--debug', '-d', is_flag=True, default=False, envvar='ZCAN_DEBUG', help="Debug output") 19 | def run(debug): 20 | if debug: 21 | LOGGER.setLevel(logging.DEBUG) 22 | else: 23 | LOGGER.setLevel(logging.INFO) 24 | 25 | try: 26 | 27 | main() 28 | except Exception as e: 29 | LOGGER.error("Failed with unexpected error") 30 | LOGGER.exception(e) 31 | sys.exit(1) 32 | 33 | 34 | @cli.command(help="show messages") 35 | @click.option('--debug', '-d', is_flag=True, default=False, envvar='ZCAN_DEBUG', help="Debug output") 36 | @click.option('--all', '-a', is_flag=True, default=False, envvar='ZCAN_ALL', help="show all messages") 37 | def show(all, debug): 38 | if debug: 39 | LOGGER.setLevel(logging.DEBUG) 40 | else: 41 | LOGGER.setLevel(logging.INFO) 42 | 43 | try: 44 | 45 | listen(all) 46 | except Exception as e: 47 | LOGGER.error("Failed with unexpected error") 48 | LOGGER.exception(e) 49 | sys.exit(1) 50 | 51 | 52 | @cli.command(help="write") 53 | @click.option('--debug', '-d', is_flag=True, default=False, envvar='ZCAN_DEBUG', help="Debug output") 54 | def test(debug): 55 | if debug: 56 | LOGGER.setLevel(logging.DEBUG) 57 | else: 58 | LOGGER.setLevel(logging.INFO) 59 | 60 | try: 61 | payload = click.prompt("Payload") 62 | write(payload) 63 | except Exception as e: 64 | LOGGER.error("Failed with unexpected error") 65 | LOGGER.exception(e) 66 | sys.exit(1) 67 | 68 | @cli.command(help="write") 69 | @click.option('--debug', '-d', is_flag=True, default=False, envvar='ZCAN_DEBUG', help="Debug output") 70 | def set_ventilation_level(debug): 71 | if debug: 72 | LOGGER.setLevel(logging.DEBUG) 73 | else: 74 | LOGGER.setLevel(logging.INFO) 75 | 76 | try: 77 | iterator = click.prompt("Iterator") 78 | write_ventilation_level(iterator) 79 | except Exception as e: 80 | LOGGER.error("Failed with unexpected error") 81 | LOGGER.exception(e) 82 | sys.exit(1) 83 | 84 | @cli.command(help="write") 85 | @click.option('--debug', '-d', is_flag=True, default=False, envvar='ZCAN_DEBUG', help="Debug output") 86 | def check_for_error(debug): 87 | if debug: 88 | LOGGER.setLevel(logging.DEBUG) 89 | else: 90 | LOGGER.setLevel(logging.INFO) 91 | 92 | try: 93 | get_error_state() 94 | except Exception as e: 95 | LOGGER.error("Failed with unexpected error") 96 | LOGGER.exception(e) 97 | sys.exit(1) 98 | 99 | if __name__ == "__main__": 100 | cli() -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /zcanbridge.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from mem_top import mem_top 4 | 5 | import mapping2 as mapping 6 | import asyncio 7 | import websockets 8 | import socket 9 | import struct 10 | import logging 11 | import logging.handlers 12 | import json 13 | import requests 14 | import datetime 15 | import pytz 16 | from tzlocal import get_localzone 17 | from dateutil import parser 18 | import math 19 | import time 20 | 21 | humidities={} 22 | lastupdated={} 23 | temperatures={} 24 | names={} 25 | 26 | @asyncio.coroutine 27 | def hello(uri): 28 | websocket = yield from websockets.connect(uri) 29 | while True: 30 | msg = yield from websocket.recv() 31 | info = json.loads(msg) 32 | #print(info) 33 | if 'state' in info and 'id' in info and info['id'] in names: 34 | name = names[info['id']] 35 | if 'temperature' in info['state']: 36 | temperatures[name] = info['state']['temperature']/100 37 | if data[key]['state']['lastupdated'] != "none": 38 | lastupdated[name] = parser.parse(info['state']['lastupdated']+" UTC").astimezone(get_localzone()) 39 | else: 40 | lastupdated[name] = "none" 41 | if 'humidity' in info['state']: 42 | humidities[name] = info['state']['humidity']/100 43 | 44 | can_frame_fmt = "=IB3x8s" 45 | 46 | def dissect_can_frame(frame): 47 | can_id, can_dlc, data = struct.unpack(can_frame_fmt, frame) 48 | if can_id & socket.CAN_RTR_FLAG != 0: 49 | print("RTR received from %08X"%(can_id&socket.CAN_EFF_MASK)) 50 | return(0,0,[]) 51 | can_id &= socket.CAN_EFF_MASK 52 | 53 | return (can_id, can_dlc, data[:can_dlc]) 54 | 55 | sensormap = { 56 | "temperature_inlet_before_recuperator": {"sensor":52,"type":"temperature"}, 57 | "air_humidity_inlet_before_recuperator":{"sensor":53,"type":"humidity"}, 58 | "temperature_inlet_after_recuperator": {"sensor":55,"type":"temperature"}, 59 | "air_humidity_inlet_after_recuperator":{"sensor":54,"type":"humidity"}, 60 | "temperature_outlet_before_recuperator": {"sensor":56,"type":"temperature"}, 61 | "air_humidity_outlet_before_recuperator":{"sensor":58,"type":"humidity"}, 62 | "temperature_outlet_after_recuperator": {"sensor":57,"type":"temperature"}, 63 | "air_humidity_outlet_after_recuperator":{"sensor":59,"type":"humidity"}, 64 | } 65 | 66 | async def handle_client(cansocket): 67 | request = None 68 | while True: 69 | msg = await loop.sock_recv(cansocket, 16) 70 | can_id, can_dlc, data = dissect_can_frame(msg) 71 | if can_id & 0xFF800000 == 0: 72 | pdid = (can_id>>14)&0x7ff 73 | if pdid in mapping.mapping: 74 | stuff = mapping.mapping[pdid] 75 | if stuff["name"] in sensormap: 76 | sensor = sensormap[stuff["name"]] 77 | future1 = loop.run_in_executor(None, requests.put, f'{url}/{sensor["sensor"]}/state', json.dumps({f'{sensor["type"]}':stuff["transformation"](data)*100})) 78 | loop.call_soon(future1) 79 | #print(mem_top()) 80 | 81 | 82 | url = 'http://192.168.2.5/api/9E82617AE7/sensors' 83 | response = requests.get(url) 84 | #print(response.content.decode("utf-8") ) 85 | 86 | data = {}#json.loads(response.content.decode("utf-8") ) 87 | for key in data: 88 | if 'state' in data[key]: 89 | if 'temperature' in data[key]['state']: 90 | if "plug" in data[key]['modelid']: 91 | continue 92 | names[key] = data[key]['name'] 93 | temperatures[names[key]] = data[key]['state']['temperature']/100 94 | if data[key]['state']['lastupdated'] != "none": 95 | lastupdated[names[key]] = parser.parse(data[key]['state']['lastupdated']+" UTC").astimezone(get_localzone()) 96 | else: 97 | lastupdated[names[key]] = "none" 98 | elif 'humidity' in data[key]['state']: 99 | names[key] = data[key]['name'] 100 | humidities[names[key]] = data[key]['state']['humidity']/100 101 | 102 | 103 | # create a raw socket and bind it to the given CAN interface 104 | loop = asyncio.get_event_loop() 105 | while True: 106 | try: 107 | s = socket.socket(socket.AF_CAN, socket.SOCK_RAW, socket.CAN_RAW) 108 | s.bind(("slcan0",)) 109 | loop.run_until_complete( 110 | handle_client(s)) 111 | 112 | # hello('ws://192.168.2.5:8088')) 113 | except: 114 | time.sleep(1) 115 | pass 116 | 117 | # vim: et:sw=4:ts=4:smarttab:foldmethod=indent:si 118 | 119 | -------------------------------------------------------------------------------- /old/src/main/python/pyserial.py: -------------------------------------------------------------------------------- 1 | # import logging 2 | # import serial 3 | # import time 4 | # import datetime 5 | # import asyncio 6 | # from xknx import XKNX 7 | # from influxdb import InfluxDBClient 8 | # 9 | # ser = serial.serial_for_url("/dev/ttyACM0", do_not_open=True) 10 | # ser.baudrate = 115200 11 | # ser.open() 12 | # 13 | # ser.write(b'C\r') 14 | # ser.write(b'S2\r') 15 | # ser.write(b'O\r') 16 | # 17 | # data = {} 18 | # 19 | # mapping = { 20 | # b"001DC041": { 21 | # "id": "outlet_fan_air_volume", 22 | # "name": "Luftmenge Abluftventilator", 23 | # "unit": "m3", 24 | # }, 25 | # b"001E0041": { 26 | # "id": "inlet_fan_air_volume", 27 | # "name": "Luftmenge Zuluftventilator", 28 | # "unit": "m3", 29 | # }, 30 | # b"001D4041": { 31 | # "id": "outlet_fan_percent", 32 | # "name": "Leistung Abluftventilator", 33 | # "unit": "%", 34 | # }, 35 | # b"001D8041": { 36 | # "id": "inlet_fan_percent", 37 | # "name": "Leistung Zuluftventilator", 38 | # "unit": "%", 39 | # }, 40 | # b"00458041": { 41 | # "id": "inlet_temperature", 42 | # "name": "Temperatur Zuluft", 43 | # "unit": "^C", 44 | # }, 45 | # b"00454041": { 46 | # "id": "outside_temperature", 47 | # "name": "Temperatur Außenluft", 48 | # "unit": "^C", 49 | # }, 50 | # b"00200041": { 51 | # "id": "power", 52 | # "name": "Stromverbrauch aktuell", 53 | # "unit": "W", 54 | # }, 55 | # b"004C4041": { 56 | # "id": "inlet_humidity", 57 | # "name": "Feuchtigkeit Zuluft", 58 | # "unit": "%", 59 | # }, 60 | # b"0048C041": { 61 | # "id": "putlet_humidity", 62 | # "name": "Feuchtigkeit Abluft", 63 | # "unit": "%", 64 | # }, 65 | # } 66 | # 67 | # client = InfluxDBClient('localhost', 8086, 'root', 'root', 'ventilation') 68 | # 69 | # try: 70 | # logging.info("CREATING DATABASE") 71 | # client.create_database('ventilation') 72 | # except Exception as e: 73 | # logging.error("create_database: {}".format(e)) 74 | # 75 | # 76 | # def get_current_time(): 77 | # return datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ') 78 | # 79 | # 80 | # def send_metric_datapoint(measurement, location, value, timestamp): 81 | # json_body = [ 82 | # { 83 | # "measurement": measurement, 84 | # "tags": { 85 | # "location": location 86 | # }, 87 | # "time": timestamp, 88 | # "fields": { 89 | # "value": value 90 | # } 91 | # } 92 | # ] 93 | # try: 94 | # client.write_points(json_body, database="ventilation") 95 | # except Exception as e: 96 | # logging.error("send_metric_datapoint: {}".format(e)) 97 | # 98 | # 99 | # try: 100 | # next = datetime.datetime.now() + datetime.timedelta(minutes=1) 101 | # print(next) 102 | # time.sleep(1) 103 | # while True: 104 | # out = b'' 105 | # while ser.inWaiting() > 0: 106 | # char = ser.read(1) 107 | # if char == b'\r': 108 | # break 109 | # else: 110 | # out += char 111 | # 112 | # if out: 113 | # raw = out 114 | # logging.info(raw) 115 | # 116 | # try: 117 | # flag = raw[0:1] 118 | # id = raw[1:9] 119 | # 120 | # length = int(raw[9:10], 16) 121 | # 122 | # index = 10 123 | # value = [] 124 | # 125 | # for item in range(0, length): 126 | # value.append(int(raw[index:index + 2], 16)) 127 | # index += 2 128 | # 129 | # if id == b"00454041" or id == b"00458041": 130 | # dvalue = (parts[0] - parts[1]) / 10 131 | # else: 132 | # if len(value) > 1: 133 | # parts = value[0:2] 134 | # dvalue = float("{}.{}".format(parts[0], parts[1])) 135 | # elif len(value) > 0: 136 | # dvalue = float(value[0]) 137 | # 138 | # send_metric_datapoint(id, "hwr", dvalue, get_current_time()) 139 | # print("{} {} - {} (length: {})".format(flag, id, value, length)) 140 | # # print(int(value,16)) 141 | # 142 | # m = mapping.get(id) 143 | # if m: 144 | # data[m["id"]] = value 145 | # else: 146 | # data[id] = value 147 | # time.sleep(0.1) 148 | # except Exception as e: 149 | # logging.error(e) 150 | # logging.exception(e) 151 | # 152 | # # if datetime.datetime.now() > next: 153 | # # next = datetime.datetime.now() + datetime.timedelta(minutes=1) 154 | # # 155 | # # print("") 156 | # # print("") 157 | # # print(datetime.datetime.now()) 158 | # # for k,v in data.items(): 159 | # # print("{} = {}".format(k,v)) 160 | # 161 | # except KeyboardInterrupt: 162 | # pass 163 | # finally: 164 | # print("finally closing everything") 165 | # ser.write(b'C\r') 166 | # ser.close() 167 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /old/src/main/python/zcan/can.py: -------------------------------------------------------------------------------- 1 | import serial 2 | import logging 3 | from zcan.exception import ZCanBaseException 4 | from zcan.mapping import mapping 5 | from zcan.util import get_logger 6 | 7 | 8 | class Message(object): 9 | def __init__(self, type: str, id: str, length: int, data, raw=None): 10 | self.type = type 11 | self.id = id 12 | self.length = length 13 | self.data = data 14 | self.raw = raw 15 | 16 | def __str__(self): 17 | return "type:{} id:{} length:{} data:{}".format(self.type, self.id, self.length, self.data) 18 | 19 | def print_full_repr(self): 20 | return "type:{} id:{} length:{} data:{} (raw: {})".format(self.type, self.id, self.length, self.data, self.raw) 21 | 22 | def __eq__(self, other): 23 | if self.type == other.type and self.id == other.id and self.length == other.length and self.data == other.data: 24 | return True 25 | else: 26 | return False 27 | 28 | 29 | class Measurement(object): 30 | def __init__(self, name: str, id: str, value, unit: str): 31 | self.name = name 32 | self.id = id 33 | self.value = value 34 | self.unit = unit 35 | 36 | @staticmethod 37 | def from_message(message: Message): 38 | try: 39 | message_mapping = mapping[message.id] 40 | except KeyError: 41 | return None 42 | 43 | name = message_mapping["name"] 44 | unit = message_mapping["unit"] 45 | transform_function = message_mapping["transformation"] 46 | try: 47 | value = transform_function(message.data) 48 | except Exception as e: 49 | raise RuntimeError("Could not transform data {} to value for {}, error: {}".format(message.data, name, e)) 50 | 51 | return Measurement(name, message.id, value, unit) 52 | 53 | def __str__(self): 54 | return "name: {} id:{} value:{} unit:{}".format(self.name, self.id, self.value, self.unit) 55 | 56 | def __eq__(self, other): 57 | if self.name == other.name and self.id == other.id and self.value == other.value and self.unit == other.unit: 58 | return True 59 | else: 60 | return False 61 | 62 | 63 | class CanBus(object): 64 | def __init__(self): 65 | self.logger = get_logger("CanBusReader") 66 | self.can = CanBusInterface() 67 | 68 | def read_messages(self, measurements, unknown_messages): 69 | self.can.open() 70 | try: 71 | while True: 72 | try: 73 | message = self.can.read_message() 74 | if message: 75 | self.logger.debug(message) 76 | measurement = Measurement.from_message(message) 77 | if measurement: 78 | measurements[measurement.name] = measurement 79 | else: 80 | unknown_messages[message.id] = message 81 | except Exception as e: 82 | self.logger.exception(e) 83 | finally: 84 | self.can.close() 85 | 86 | def print_messages(self, all: bool): 87 | self.can.open() 88 | try: 89 | while True: 90 | try: 91 | message = self.can.read_message() 92 | if message: 93 | try: 94 | mapping[message.id] 95 | if all: 96 | print(message.print_full_repr()) 97 | except KeyError: 98 | print(message.print_full_repr()) 99 | except Exception as e: 100 | print(e) 101 | finally: 102 | self.can.close() 103 | 104 | def write(self, payload): 105 | self.can.open() 106 | 107 | try: 108 | if isinstance(payload, str): 109 | self.can.write_message_string(payload) 110 | else: 111 | self.can.write_message_bytes(payload) 112 | finally: 113 | self.can.close() 114 | 115 | def write_messages(self, payloads: list): 116 | self.can.open() 117 | try: 118 | for payload in payloads: 119 | if isinstance(payload, str): 120 | self.can.write_message_string(payload) 121 | else: 122 | self.can.write_message_bytes(payload) 123 | finally: 124 | self.can.close() 125 | 126 | def get_error_state(self): 127 | self.can.open() 128 | try: 129 | print(self.can.get_error_state()) 130 | finally: 131 | self.can.close() 132 | 133 | 134 | class CanBusInterface(object): 135 | def __init__(self, device="/dev/ttyACM0"): 136 | self.logger = get_logger("CanBusInterface") 137 | self.connection = serial.serial_for_url(device, do_not_open=True) 138 | self.connection.baudrate = 115200 139 | 140 | def close(self): 141 | """ 142 | tell SUBtin to close the CAN bus connection and close serial connection 143 | """ 144 | self.connection.write(b'C\r') 145 | self.connection.close() 146 | 147 | def open(self): 148 | """ 149 | open serial connection and tell USBtin to use a CAN baud rate of 50k (S2) 150 | """ 151 | self.connection.open() 152 | self.connection.write(b'S2\r') 153 | self.connection.write(b'O\r') 154 | 155 | @staticmethod 156 | def _to_can_message(frame: bytes) -> Message: 157 | try: 158 | frame = frame.strip(b'\x07') 159 | 160 | type = frame[0:1].decode("ascii") 161 | assert type.lower() in ["t", "r"], "message type must be T or R, not '{}'".format(type) 162 | 163 | id = frame[1:9].decode("ascii") 164 | length = int(frame[9:10], 16) 165 | 166 | index = 10 167 | data = [] 168 | 169 | for item in range(0, length): 170 | data.append(int(frame[index:index + 2], 16)) 171 | index += 2 172 | 173 | return Message(type, id, length, data, frame) 174 | except Exception as e: 175 | print("Could not parse can frame '{}', error was: {}".format(frame, e)) 176 | 177 | def read(self): 178 | return self.connection.read_until(b'\r') 179 | 180 | def read_message(self): 181 | """ 182 | read until stop character is found or timeout occurs 183 | :return: one line as bytestring 184 | """ 185 | frame = self.read() 186 | self.logger.debug(frame) 187 | return self._to_can_message(frame) 188 | 189 | def get_error_state(self): 190 | self.connection.write(b"F\r") 191 | return self.read() 192 | 193 | def write_message_string(self, payload: str): 194 | message = bytearray("{}\r".format(payload), encoding="ascii") 195 | print("Going to send: {}".format(message)) 196 | self.connection.write(message) 197 | 198 | def write_message_bytes(self, payload: bytes): 199 | print("Going to send bytes: {}".format(payload)) 200 | self.connection.write(payload) 201 | 202 | 203 | if __name__ == "__main__": 204 | interface = CanBusInterface() 205 | interface.open() 206 | while True: 207 | print(interface.read_message()) 208 | -------------------------------------------------------------------------------- /old/src/main/python/zcan/mapping.py: -------------------------------------------------------------------------------- 1 | def transform_temperature(value: list) -> float: 2 | parts = value[0:2] 3 | return (parts[0] - parts[1]) / 10 4 | 5 | 6 | def transform_air_volume(value: list) -> float: 7 | parts = value[0:2] 8 | return float(parts[0] + parts[1] * 255) 9 | 10 | 11 | mapping = { 12 | "00148041": { 13 | "name": "unknown_decreasing_number", 14 | "unit": "unknown", 15 | "transformation": lambda x: float(x[0]) 16 | }, 17 | "00454041": { 18 | "name": "temperature_inlet_before_recuperator", 19 | "unit": "°C", 20 | "transformation": transform_temperature 21 | }, 22 | "00458041": { 23 | "name": "temperature_inlet_after_recuperator", 24 | "unit": "°C", 25 | "transformation": transform_temperature 26 | }, 27 | "001E0041": { 28 | "name": "air_volume_input_ventilator", 29 | "unit": "m3", 30 | "transformation": transform_air_volume 31 | }, 32 | "001DC041": { 33 | "name": "air_volume_output_ventilator", 34 | "unit": "m3", 35 | "transformation": transform_air_volume 36 | }, 37 | "001E8041": { 38 | "name": "speed_input_ventilator", 39 | "unit": "rpm", 40 | "transformation": transform_air_volume 41 | }, 42 | "001E4041": { 43 | "name": "speed_output_ventilator", 44 | "unit": "rpm", 45 | "transformation": transform_air_volume 46 | }, 47 | "00488041": { 48 | "name": "air_humidity_outlet_before_recuperator", 49 | "unit": "%", 50 | "transformation": lambda x: float(x[0]) 51 | }, 52 | "0048C041": { 53 | "name": "air_humidity_inlet_before_recuperator", 54 | "unit": "%", 55 | "transformation": lambda x: float(x[0]) 56 | }, 57 | "00200041": { 58 | "name": "total_power_consumption", 59 | "unit": "W", 60 | "transformation": lambda x: float(x[0]) 61 | }, 62 | "001D4041": { 63 | "name": "power_percent_output_ventilator", 64 | "unit": "%", 65 | "transformation": lambda x: float(x[0]) 66 | }, 67 | "001D8041": { 68 | "name": "power_percent_input_ventilator", 69 | "unit": "%", 70 | "transformation": lambda x: float(x[0]) 71 | }, 72 | "0082C042": { 73 | "name": "0082C042", 74 | "unit": "unknown", 75 | "transformation": transform_air_volume 76 | }, 77 | "004C4041": { 78 | "name": "004C4041", 79 | "unit": "unknown", 80 | "transformation": transform_air_volume 81 | }, 82 | "00384041": { 83 | "name": "00384041", 84 | "unit": "unknown", 85 | "transformation": lambda x: float(x[0]) 86 | }, 87 | "00144041": { 88 | "name": "remaining_s_in_currend_ventilation_mode", 89 | "unit": "s", 90 | "transformation": transform_air_volume 91 | }, 92 | "00824042": { 93 | "name": "00824042", 94 | "unit": "unknown", 95 | "transformation": transform_air_volume 96 | }, 97 | "00810042": { 98 | "name": "00810042", 99 | "unit": "unknown", 100 | "transformation": transform_air_volume 101 | }, 102 | "00208041": { 103 | "name": "total_power_consumption", 104 | "unit": "kWh", 105 | "transformation": transform_air_volume 106 | }, 107 | "00344041": { 108 | "name": "00344041", 109 | "unit": "unknown", 110 | "transformation": transform_air_volume 111 | }, 112 | "00370041": { 113 | "name": "00370041", 114 | "unit": "unknown", 115 | "transformation": transform_air_volume 116 | }, 117 | "00300041": { 118 | "name": "days_until_next_filter_change", 119 | "unit": "days", 120 | "transformation": transform_air_volume 121 | }, 122 | "00044041": { 123 | "name": "00044041", 124 | "unit": "unknown", 125 | "transformation": lambda x: float(x[0]) 126 | }, 127 | "00204041": { 128 | "name": "total_power_consumption_this_year", 129 | "unit": "kWh", 130 | "transformation": transform_air_volume 131 | }, 132 | "00084041": { 133 | "name": "00084041", 134 | "unit": "unknown", 135 | "transformation": lambda x: float(x[0]) 136 | }, 137 | "00804042": { 138 | "name": "00804042", 139 | "unit": "unknown", 140 | "transformation": transform_air_volume 141 | }, 142 | "00644041": { 143 | "name": "00644041", 144 | "unit": "unknown", 145 | "transformation": lambda x: float(x[0]) 146 | }, 147 | "00354041": { 148 | "name": "00354041", 149 | "unit": "unknown", 150 | "transformation": transform_air_volume 151 | }, 152 | "00390041": { 153 | "name": "frost_disbalance", 154 | "unit": "%", 155 | "transformation": lambda x: float(x[0]) 156 | }, 157 | 158 | "0035C041": { 159 | "name": "total_power_savings", 160 | "unit": "kWh", 161 | "transformation": transform_air_volume 162 | }, 163 | 164 | "0044C041": { 165 | "name": "0044C041", 166 | "unit": "unknown", 167 | "transformation": transform_air_volume 168 | }, 169 | 170 | "0080C042": { 171 | "name": "0080C042", 172 | "unit": "unknown", 173 | "transformation": transform_air_volume 174 | }, 175 | 176 | "000E0041": { 177 | "name": "000E0041", 178 | "unit": "unknown", 179 | "transformation": lambda x: float(x[0]) 180 | }, 181 | 182 | "00604041": { 183 | "name": "00604041", 184 | "unit": "unknown", 185 | "transformation": lambda x: float(x[0]) 186 | }, 187 | 188 | "00450041": { 189 | "name": "00450041", 190 | "unit": "unknown", 191 | "transformation": transform_air_volume 192 | }, 193 | 194 | "00378041": { 195 | "name": "00378041", 196 | "unit": "unknown", 197 | "transformation": lambda x: float(x[0]) 198 | }, 199 | 200 | "00818042": { 201 | "name": "00818042", 202 | "unit": "unknown", 203 | "transformation": lambda x: float(x[0]) 204 | }, 205 | 206 | "00820042": { 207 | "name": "00820042", 208 | "unit": "unknown", 209 | "transformation": lambda x: float(x[0]) 210 | }, 211 | 212 | "001D0041": { 213 | "name": "001D0041", 214 | "unit": "unknown", 215 | "transformation": lambda x: float(x[0]) 216 | }, 217 | 218 | "00490041": { 219 | "name": "00490041", 220 | "unit": "unknown", 221 | "transformation": lambda x: float(x[0]) 222 | }, 223 | 224 | "00350041": { 225 | "name": "00350041", 226 | "unit": "unknown", 227 | "transformation": transform_air_volume 228 | }, 229 | 230 | "0081C042": { 231 | "name": "0081C042", 232 | "unit": "unknown", 233 | "transformation": lambda x: float(x[0]) 234 | }, 235 | 236 | "00448041": { 237 | "name": "00448041", 238 | "unit": "unknown", 239 | "transformation": lambda x: float(x[0]) 240 | }, 241 | 242 | "00560041": { 243 | "name": "00560041", 244 | "unit": "unknown", 245 | "transformation": lambda x: float(x[0]) 246 | }, 247 | 248 | "00374041": { 249 | "name": "00374041", 250 | "unit": "unknown", 251 | "transformation": transform_air_volume 252 | }, 253 | 254 | "00808042": { 255 | "name": "00808042", 256 | "unit": "unknown", 257 | "transformation": lambda x: float(x[0]) 258 | }, 259 | 260 | "00040041": { 261 | "name": "00040041", 262 | "unit": "unknown", 263 | "transformation": lambda x: float(x[0]) 264 | }, 265 | 266 | "10040001": { 267 | "name": "10040001", 268 | "unit": "unknown", 269 | "transformation": lambda x: float(x[0]) 270 | }, 271 | 272 | "00120041": { 273 | "name": "00120041", 274 | "unit": "unknown", 275 | "transformation": lambda x: float(x[0]) 276 | }, 277 | 278 | "00688041": { 279 | "name": "00688041", 280 | "unit": "unknown", 281 | "transformation": lambda x: float(x[0]) 282 | }, 283 | 284 | "00358041": { 285 | "name": "total_power_savings_this_year", 286 | "unit": "kWh", 287 | "transformation": transform_air_volume 288 | }, 289 | 290 | "ventilation_level": { 291 | "name": "00104041", 292 | "unit": "ventilation_level", 293 | "transformation": lambda x: float(x[0]) 294 | }, 295 | 296 | "00544041": { 297 | "name": "00544041", 298 | "unit": "unknown", 299 | "transformation": lambda x: float(x[0]) 300 | }, 301 | 302 | "00498041": { 303 | "name": "00498041", 304 | "unit": "unknown", 305 | "transformation": lambda x: float(x[0]) 306 | }, 307 | 308 | "00814042": { 309 | "name": "00814042", 310 | "unit": "unknown", 311 | "transformation": lambda x: float(x[0]) 312 | }, 313 | 314 | "000C4041": { 315 | "name": "000C4041", 316 | "unit": "unknown", 317 | "transformation": lambda x: float(x[0]) 318 | }, 319 | 320 | "00828042": { 321 | "name": "00828042", 322 | "unit": "unknown", 323 | "transformation": lambda x: float(x[0]) 324 | }, 325 | 326 | "00494041": { 327 | "name": "00494041", 328 | "unit": "unknown", 329 | "transformation": lambda x: float(x[0]) 330 | }, 331 | 332 | "004C8041": { 333 | "name": "004C8041", 334 | "unit": "unknown", 335 | "transformation": transform_air_volume 336 | }, 337 | "00388041": { 338 | "name": "00388041", 339 | "unit": "unknown", 340 | "transformation": transform_air_volume 341 | }, 342 | "00188041": { 343 | "name": "bypass_a_status", 344 | "unit": "unknown", 345 | "transformation": transform_air_volume 346 | }, 347 | "00184041": { 348 | "name": "bypass_b_status", 349 | "unit": "unknown", 350 | "transformation": transform_air_volume 351 | }, 352 | "00108041": { 353 | "name": "bypass_state", 354 | "unit": "0=auto,1=open,2=close", 355 | "transformation": lambda x: float(x[0]) 356 | }, 357 | "0038C041": { 358 | "name": "bypass_open", 359 | "unit": "%", 360 | "transformation": lambda x: float(x[0]) 361 | } 362 | } 363 | 364 | command_mapping = { 365 | "set_ventilation_level_0": b'T1F07505180100201C00000000\r', 366 | "set_ventilation_level_1": b'T1F07505180100201C00000100\r', 367 | "set_ventilation_level_2": b'T1F07505180100201C00000200\r', 368 | "set_ventilation_level_3": b'T1F07505180100201C00000300\r', 369 | "auto_mode": b'T1F075051485150801\r', # verified (also: T1F051051485150801\r) 370 | "manual_mode": b'T1F07505180084150101000000\r', # verified (also: T1F051051485150801\r) 371 | "temperature_profile_cool": b'T0010C041101\r', 372 | "temperature_profile_normal": b'T0010C041100\r', 373 | "temperature_profile_warm": b'T0010C041102\r', 374 | "close_bypass": b'T00108041102\r', 375 | "open_bypass": b'T00108041101\r', 376 | "auto_bypass": b'T00108041100\r', 377 | "basis_menu": b"T00400041100\r", 378 | "extended_menu": b"T00400041101\r" 379 | } 380 | -------------------------------------------------------------------------------- /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 | mapping = { 21 | 16: { 22 | "name": "z_unknown_NwoNode", 23 | "unit": "", 24 | "transformation": transform_any 25 | }, 26 | 17: { 27 | "name": "z_unknown_NwoNode", 28 | "unit": "", 29 | "transformation": transform_any 30 | }, 31 | 18: { 32 | "name": "z_unknown_NwoNode", 33 | "unit": "", 34 | "transformation": transform_any 35 | }, 36 | 65: { 37 | "name": "ventilation_level", 38 | "unit": "level", 39 | "transformation": lambda x: float(x[0]) 40 | }, 41 | 66: { 42 | "name": "bypass_state", 43 | "unit": "0=auto,1=open,2=close", 44 | "transformation": lambda x: float(x[0]) 45 | }, 46 | 81: { 47 | "name": "Timer1", 48 | "unit": "s", 49 | "transformation": transform_any 50 | }, 51 | 82: { 52 | "name": "Timer2", 53 | "unit": "s", 54 | "transformation": transform_any 55 | }, 56 | 83: { 57 | "name": "Timer3", 58 | "unit": "s", 59 | "transformation": transform_any 60 | }, 61 | 84: { 62 | "name": "Timer4", 63 | "unit": "s", 64 | "transformation": transform_any 65 | }, 66 | 85: { 67 | "name": "Timer5", 68 | "unit": "s", 69 | "transformation": transform_any 70 | }, 71 | 86: { 72 | "name": "Timer6", 73 | "unit": "s", 74 | "transformation": transform_any 75 | }, 76 | 87: { 77 | "name": "Timer7", 78 | "unit": "s", 79 | "transformation": transform_any 80 | }, 81 | 88: { 82 | "name": "Timer8", 83 | "unit": "s", 84 | "transformation": transform_any 85 | }, 86 | 87 | 96: { 88 | "name": "bypass ??? ValveMsg", 89 | "unit": "unknown", 90 | "transformation": transform_any 91 | }, 92 | 97: { 93 | "name": "bypass_b_status", 94 | "unit": "unknown", 95 | "transformation": transform_air_volume 96 | }, 97 | 98: { 98 | "name": "bypass_a_status", 99 | "unit": "unknown", 100 | "transformation": transform_air_volume 101 | }, 102 | 103 | 115: { 104 | "name": "ventilator enabled output", 105 | "unit": "", 106 | "transformation": transform_any 107 | }, 108 | 116: { 109 | "name": "ventilator enabled input", 110 | "unit": "", 111 | "transformation": transform_any 112 | }, 113 | 117: { 114 | "name": "ventilator power_percent output", 115 | "unit": "%", 116 | "transformation": lambda x: float(x[0]) 117 | }, 118 | 118: { 119 | "name": "ventilator power_percent input", 120 | "unit": "%", 121 | "transformation": lambda x: float(x[0]) 122 | }, 123 | 119: { 124 | "name": "ventilator air_volume output", 125 | "unit": "m3", 126 | "transformation": transform_air_volume 127 | }, 128 | 120: { 129 | "name": "ventilator air_volume input", 130 | "unit": "m3", 131 | "transformation": transform_air_volume 132 | }, 133 | 121: { 134 | "name": "ventilator speed output", 135 | "unit": "rpm", 136 | "transformation": transform_air_volume 137 | }, 138 | 122: { 139 | "name": "ventilator speed input", 140 | "unit": "rpm", 141 | "transformation": transform_air_volume 142 | }, 143 | 128: { 144 | "name": "Power_consumption_actual", 145 | "unit": "W", 146 | "transformation": lambda x: float(x[0]) 147 | }, 148 | 129: { 149 | "name": "Power_consumption_this_year", 150 | "unit": "kWh", 151 | "transformation": transform_air_volume 152 | }, 153 | 130: { 154 | "name": "Power_consumption_lifetime", 155 | "unit": "kWh", 156 | "transformation": transform_air_volume 157 | }, 158 | 144: { 159 | "name": "Power PreHeater this year", 160 | "unit": "kWh", 161 | "transformation": transform_any 162 | }, 163 | 145: { 164 | "name": "Power PreHeater total", 165 | "unit": "kWh", 166 | "transformation": transform_any 167 | }, 168 | 146: { 169 | "name": "Power PreHeater actual", 170 | "unit": "W", 171 | "transformation": transform_any 172 | }, 173 | 192: { 174 | "name": "days_until_next_filter_change", 175 | "unit": "days", 176 | "transformation": transform_air_volume 177 | }, 178 | 179 | 208: { 180 | "name": "z_Unknown_TempHumConf", 181 | "unit": "", 182 | "transformation": transform_any 183 | }, 184 | 209: { 185 | "name" : "RMOT", 186 | "unit":"°C", 187 | "transformation":transform_temperature 188 | }, 189 | 210: { 190 | "name": "z_Unknown_TempHumConf", 191 | "unit": "", 192 | "transformation": transform_any 193 | }, 194 | 211: { 195 | "name": "z_Unknown_TempHumConf", 196 | "unit": "", 197 | "transformation": transform_any 198 | }, 199 | 212: { 200 | "name": "Target_temperature", 201 | "unit": "°C", 202 | "transformation": transform_temperature 203 | }, 204 | 213: { 205 | "name": "Power_avoided_heating_actual", 206 | "unit": "W", 207 | "transformation": transform_any 208 | }, 209 | 214: { 210 | "name": "Power_avoided_heating_this_year", 211 | "unit": "kWh", 212 | "transformation": transform_air_volume 213 | }, 214 | 215: { 215 | "name": "Power_avoided_heating_lifetime", 216 | "unit": "kWh", 217 | "transformation": transform_air_volume 218 | }, 219 | 216: { 220 | "name": "Power_avoided_cooling_actual", 221 | "unit": "W", 222 | "transformation": transform_any 223 | }, 224 | 217: { 225 | "name": "Power_avoided_cooling_this_year", 226 | "unit": "kWh", 227 | "transformation": transform_air_volume 228 | }, 229 | 218: { 230 | "name": "Power_avoided_cooling_lifetime", 231 | "unit": "kWh", 232 | "transformation": transform_air_volume 233 | }, 234 | 219: { 235 | "name": "Power PreHeater Target", 236 | "unit": "W", 237 | "transformation": transform_any 238 | }, 239 | 220: { 240 | "name": "temperature_inlet_before_preheater", 241 | "unit": "°C", 242 | "transformation": transform_temperature 243 | }, 244 | 221: { 245 | "name": "temperature_inlet_after_recuperator", 246 | "unit": "°C", 247 | "transformation": transform_temperature 248 | }, 249 | 222: { 250 | "name": "z_Unknown_TempHumConf", 251 | "unit": "", 252 | "transformation": transform_any 253 | }, 254 | 224: { 255 | "name": "z_Unknown_VentConf", 256 | "unit": "", 257 | "transformation": transform_any 258 | }, 259 | 225: { 260 | "name": "z_Unknown_VentConf", 261 | "unit": "", 262 | "transformation": transform_any 263 | }, 264 | 226: { 265 | "name": "z_Unknown_VentConf", 266 | "unit": "", 267 | "transformation": transform_any 268 | }, 269 | 227: { 270 | "name": "bypass_open", 271 | "unit": "%", 272 | "transformation": lambda x: float(x[0]) 273 | }, 274 | 228: { 275 | "name": "frost_disbalance", 276 | "unit": "%", 277 | "transformation": lambda x: float(x[0]) 278 | }, 279 | 229: { 280 | "name": "z_Unknown_VentConf", 281 | "unit": "", 282 | "transformation": transform_any 283 | }, 284 | 230: { 285 | "name": "z_Unknown_VentConf", 286 | "unit": "", 287 | "transformation": transform_any 288 | }, 289 | 290 | 256: { 291 | "name": "z_Unknown_NodeConf", 292 | "unit": "unknown", 293 | "transformation": transform_any 294 | }, 295 | 257: { 296 | "name": "z_Unknown_NodeConf", 297 | "unit": "unknown", 298 | "transformation": transform_any 299 | }, 300 | 301 | 273: { 302 | "name": "temperature_something...", 303 | "unit": "°C", 304 | "transformation": transform_temperature 305 | }, 306 | 274: { 307 | "name": "temperature_outlet_before_recuperator", 308 | "unit": "°C", 309 | "transformation": transform_temperature 310 | }, 311 | 275: { 312 | "name": "temperature_outlet_after_recuperator", 313 | "unit": "°C", 314 | "transformation": transform_temperature 315 | }, 316 | 276: { 317 | "name": "temperature_inlet_before_preheater", 318 | "unit": "°C", 319 | "transformation": transform_temperature 320 | }, 321 | 277: { 322 | "name": "temperature_inlet_before_recuperator", 323 | "unit": "°C", 324 | "transformation": transform_temperature 325 | }, 326 | 278: { 327 | "name": "temperature_inlet_after_recuperator", 328 | "unit": "°C", 329 | "transformation": transform_temperature 330 | }, 331 | 332 | 333 | 289: { 334 | "name": "z_unknown_HumSens", 335 | "unit": "", 336 | "transformation": transform_any 337 | }, 338 | 290: { 339 | "name": "air_humidity_outlet_before_recuperator", 340 | "unit": "%", 341 | "transformation": lambda x: float(x[0]) 342 | }, 343 | 291: { 344 | "name": "air_humidity_outlet_after_recuperator", 345 | "unit": "%", 346 | "transformation": lambda x: float(x[0]) 347 | }, 348 | 292: { 349 | "name": "air_humidity_inlet_before_preheater", 350 | "unit": "%", 351 | "transformation": lambda x: float(x[0]) 352 | }, 353 | 293: { 354 | "name": "air_humidity_inlet_before_recuperator", 355 | "unit": "%", 356 | "transformation": lambda x: float(x[0]) 357 | }, 358 | 294: { 359 | "name": "air_humidity_inlet_after_recuperator", 360 | "unit": "%", 361 | "transformation": lambda x: float(x[0]) 362 | }, 363 | 364 | 305: { 365 | "name": "PresSens_exhaust", 366 | "unit": "Pa", 367 | "transformation": transform_any 368 | }, 369 | 306: { 370 | "name": "PresSens_inlet", 371 | "unit": "Pa", 372 | "transformation": transform_any 373 | }, 374 | 375 | 369: { 376 | "name": "z_Unknown_AnalogInput", 377 | "unit": "V?", 378 | "transformation": transform_any 379 | }, 380 | 370: { 381 | "name": "z_Unknown_AnalogInput", 382 | "unit": "V?", 383 | "transformation": transform_any 384 | }, 385 | 371: { 386 | "name": "z_Unknown_AnalogInput", 387 | "unit": "V?", 388 | "transformation": transform_any 389 | }, 390 | 372: { 391 | "name": "z_Unknown_AnalogInput", 392 | "unit": "V?", 393 | "transformation": transform_any 394 | }, 395 | 400: { 396 | "name": "z_Unknown_PostHeater_ActualPower", 397 | "unit": "W", 398 | "transformation": transform_any 399 | }, 400 | 401: { 401 | "name": "z_Unknown_PostHeater_ThisYear", 402 | "unit": "kWh", 403 | "transformation": transform_any 404 | }, 405 | 402: { 406 | "name": "z_Unknown_PostHeater_Total", 407 | "unit": "kWh", 408 | "transformation": transform_any 409 | }, 410 | #00398041 unknown 0 0 0 0 0 0 0 0 411 | } 412 | 413 | command_mapping = { 414 | "set_ventilation_level_0": b'T1F07505180100201C00000000\r', 415 | "set_ventilation_level_1": b'T1F07505180100201C00000100\r', 416 | "set_ventilation_level_2": b'T1F07505180100201C00000200\r', 417 | "set_ventilation_level_3": b'T1F07505180100201C00000300\r', 418 | "auto_mode": b'T1F075051485150801\r', # verified (also: T1F051051485150801\r) 419 | "manual_mode": b'T1F07505180084150101000000\r', # verified (also: T1F051051485150801\r) 420 | "temperature_profile_cool": b'T0010C041101\r', 421 | "temperature_profile_normal": b'T0010C041100\r', 422 | "temperature_profile_warm": b'T0010C041102\r', 423 | "close_bypass": b'T00108041102\r', 424 | "open_bypass": b'T00108041101\r', 425 | "auto_bypass": b'T00108041100\r', 426 | "basis_menu": b"T00400041100\r", 427 | "extended_menu": b"T00400041101\r" 428 | } 429 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------