From a1f8328b50ceee8ee0b4db13107be7455d877229 Mon Sep 17 00:00:00 2001 From: Pim van den Berg Date: Mon, 5 Jun 2023 21:33:45 +0200 Subject: [PATCH 01/10] feat: log the hex request in debug mode --- itho_i2c.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/itho_i2c.py b/itho_i2c.py index 12eaddb..00dfd6f 100644 --- a/itho_i2c.py +++ b/itho_i2c.py @@ -62,6 +62,8 @@ class I2CMaster: def execute_action(self, action): request = self.compose_request(action) + request_in_hex = [hex(c) for c in request] + logger.debug(f"Request: {request_in_hex}") result = None for i in range(0, 20): logger.debug(f"Executing action: {action}") From 620cdaf880003d0a0536950e0fc913e66f46ca94 Mon Sep 17 00:00:00 2001 From: Pim van den Berg Date: Mon, 10 Jul 2023 20:46:39 +0200 Subject: [PATCH 02/10] refactor: put datatype formatting in a separate function --- itho-wpu.py | 64 +++++++++++++++++++++++++++++++++++------------------ 1 file changed, 42 insertions(+), 22 deletions(-) diff --git a/itho-wpu.py b/itho-wpu.py index 7c277ef..d6b26b6 100755 --- a/itho-wpu.py +++ b/itho-wpu.py @@ -259,35 +259,55 @@ def process_datalog(response, wpu): measurements = {} for d in datalog: if d.type == 0x0 or d.type == 0xC: - m = message[d.index : d.index + 1] # noqa: E203 - num = int(m[0], 0) - elif d.type == 0x10: - m = message[d.index : d.index + 2] # noqa: E203 - num = (int(m[0], 0) << 8) + int(m[1], 0) - elif d.type == 0x12: - m = message[d.index : d.index + 2] # noqa: E203 - num = round((int(m[0], 0) << 8) + int(m[1], 0) / 100, 2) - elif d.type == 0x90: - m = message[d.index : d.index + 2] # noqa: E203 - num = (int(m[0], 0) << 8) + int(m[1], 0) - if num >= 32768: - num -= 65536 - elif d.type == 0x92: - m = message[d.index : d.index + 2] # noqa: E203 - num = (int(m[0], 0) << 8) + int(m[1], 0) - if num >= 32768: - num -= 65536 - num = round(num / 100, 2) + length = 1 + elif d.type == 0x10 or d.type == 0x12 or d.type == 0x90 or d.type == 0x92: + length = 2 elif d.type == 0x20: - m = message[d.index : d.index + 4] # noqa: E203 - num = (int(m[0], 0) << 24) + (int(m[1], 0) << 16) + (int(m[2], 0) << 8) + int(m[3], 0) + length = 4 else: - logger.error(f"Unknown message type for datalog {d.name}: {d.type}") + logger.error(f"Unknown message type for datalog {d.label}: {d.type}") + num = format_datatype(d.label, message[d.index : d.index + length], d.type) # noqa: E203 logger.info(f"{d.description}: {num}") measurements[d.label] = num return measurements +def format_datatype(name, m, dt): + """ + Transform a list of bytes to a readable number based on the datatype. + + :param str name: Name/label of the data + :param list[str] m: List of bytes in hexadecimal string format + :param dt: Datatype + :type dt: str or int + """ + + num = None + if type(dt) is str: + dt = int(dt, 0) + + if dt == 0x0 or dt == 0xC: + num = int(m[-1], 0) + elif dt == 0x10: + num = (int(m[-2], 0) << 8) + int(m[-1], 0) + elif dt == 0x12: + num = round((int(m[-2], 0) << 8) + int(m[-1], 0) / 100, 2) + elif dt == 0x90: + num = (int(m[-2], 0) << 8) + int(m[-1], 0) + if num >= 32768: + num -= 65536 + elif dt == 0x92: + num = (int(m[-2], 0) << 8) + int(m[-1], 0) + if num >= 32768: + num -= 65536 + num = round(num / 100, 2) + elif dt == 0x20: + num = (int(m[-4], 0) << 24) + (int(m[-3], 0) << 16) + (int(m[-2], 0) << 8) + int(m[-1], 0) + else: + logger.error(f"Unknown datatype for '{name}': 0x{dt:X}") + return num + + if __name__ == "__main__": args = parse_args() From d28acae7563cb06db0efa81744b74afc7ca43875 Mon Sep 17 00:00:00 2001 From: Pim van den Berg Date: Mon, 5 Jun 2023 21:35:34 +0200 Subject: [PATCH 03/10] feat(itho-wpu): getsetting support Action "getsetting" retrieves the value of a setting byi the provided id. Example usage: > ./itho-wpu.py --action getsetting --settingid 1 > 1. Hardware Configuratie: 70 (min: 0, max: 65535, step: 1) > ./itho-wpu.py --action getsetting --settingid 2 > 2. Jaar Inbedrijfstelling: 2010 (min: 2004, max: 2099, step: 1) --- README.md | 6 +++ itho-wpu.py | 115 ++++++++++++++++++++++++++++++++++++++++++++++++++-- itho_i2c.py | 23 ++++++++--- 3 files changed, 135 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 59b613d..a65868f 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,12 @@ See the [pislave](https://github.com/ootjersb/pislave#wiring) project ... ``` +* Retrieve a single setting from the WPU + ``` + # ./itho-wpu.py --action getsetting --settingid 1 + 1. Hardware Configuratie: 70 (min: 0, max: 65535, step: 1) + ``` + # Exporting measurements ## InfluxDB diff --git a/itho-wpu.py b/itho-wpu.py index d6b26b6..077528e 100755 --- a/itho-wpu.py +++ b/itho-wpu.py @@ -23,6 +23,7 @@ actions = { "getserial": [0x90, 0xE1], "getdatatype": [0xA4, 0x00], "getdatalog": [0xA4, 0x01], + "getsetting": [0xA4, 0x10], } @@ -36,6 +37,12 @@ def parse_args(): choices=actions.keys(), help="Execute an action", ) + parser.add_argument( + "--settingid", + nargs="?", + type=int, + help="Setting identifier", + ) parser.add_argument( "--loglevel", nargs="?", @@ -75,7 +82,7 @@ class IthoWPU: self.datatype = self.get("getdatatype") self.heatpump_db = db.sqlite("heatpump.sqlite") - def get(self, action): + def get(self, action, identifier=None): if not self.no_cache: response = self.cache.get(action.replace("get", "")) if response is not None: @@ -93,7 +100,7 @@ class IthoWPU: if not self.slave_only: master = I2CMaster(address=0x41, bus=1, queue=self._q) if action: - response = master.execute_action(action) + response = master.execute_action(action, identifier) logger.debug(f"Response: {response}") master.close() @@ -150,6 +157,22 @@ class IthoWPU: return datalog return datalog + def get_setting_by_id(self, settingid): + listversion = self.get_listversion_from_nodeid() + parameterlist_version = self.heatpump_db.execute( + f"SELECT parameterlist FROM versiebeheer WHERE version = {listversion}" + )[0]["parameterlist"] + if parameterlist_version is None or not type(parameterlist_version) == int: + logger.error(f"Parameterlist not found in database for version {listversion}") + return None + setting_details = self.heatpump_db.execute( + "SELECT name, min, max, def, title, description, unit " + + f"FROM parameterlijst_v{parameterlist_version} WHERE id = {settingid}" + ) + if len(setting_details) != 1: + return None + return setting_details[0] + class IthoWPUCache: def __init__(self): @@ -219,6 +242,8 @@ def process_response(action, response, args, wpu): from itho_export import export_to_influxdb export_to_influxdb(action, measurements) + elif action == "getsetting": + process_setting(response, wpu) elif action == "getnodeid": process_nodeid(response) elif action == "getserial": @@ -272,6 +297,48 @@ def process_datalog(response, wpu): return measurements +def parse_setting(response, wpu): + message = response[5:] + + settingid = int(message[17], 0) + setting = wpu.get_setting_by_id(settingid) + if setting is None: + logger.error(f"Setting '{settingid}' is invalid") + return + + datatype = message[16] + value = format_datatype(setting["name"], message[0:4], datatype) + minimum = format_datatype(setting["name"], message[4:8], datatype) + maximum = format_datatype(setting["name"], message[8:12], datatype) + step = format_datatype(setting["name"], message[12:16], datatype) + + return value, minimum, maximum, step + + +def process_setting(response, wpu): + message = response[5:] + + settingid = int(message[17], 0) + setting = wpu.get_setting_by_id(settingid) + if setting is None: + logger.error(f"Setting '{settingid}' is invalid") + return + + value, minimum, maximum, step = parse_setting(response, wpu) + + logger.info( + "{}. {}{}: {} (min: {}, max: {}, step: {})".format( + settingid, + setting["title"].title(), + f' ({setting["unit"]})' if setting["unit"] is not None else "", + value, + minimum, + maximum, + step, + ) + ) + + def format_datatype(name, m, dt): """ Transform a list of bytes to a readable number based on the datatype. @@ -288,14 +355,46 @@ def format_datatype(name, m, dt): if dt == 0x0 or dt == 0xC: num = int(m[-1], 0) + elif dt == 0x1: + num = round(int(m[-1], 0) / 10, 1) + elif dt == 0x2: + num = round(int(m[-1], 0) / 100, 2) elif dt == 0x10: num = (int(m[-2], 0) << 8) + int(m[-1], 0) elif dt == 0x12: num = round((int(m[-2], 0) << 8) + int(m[-1], 0) / 100, 2) + elif dt == 0x13: + num = round((int(m[-2], 0) << 8) + int(m[-1], 0) / 1000, 3) + elif dt == 0x14: + num = round((int(m[-2], 0) << 8) + int(m[-1], 0) / 10000, 4) + elif dt == 0x80: + num = int(m[-1], 0) + if num >= 128: + num -= 256 + elif dt == 0x81: + num = int(m[-1], 0) + if num >= 128: + num -= 256 + num = round(num / 10, 1) + elif dt == 0x82: + num = int(m[-1], 0) + if num >= 128: + num -= 256 + num = round(num / 100, 2) + elif dt == 0x8F: + num = int(m[-1], 0) + if num >= 128: + num -= 256 + num = round(num / 1000, 3) elif dt == 0x90: num = (int(m[-2], 0) << 8) + int(m[-1], 0) if num >= 32768: num -= 65536 + elif dt == 0x91: + num = (int(m[-2], 0) << 8) + int(m[-1], 0) + if num >= 32768: + num -= 65536 + num = round(num / 10, 2) elif dt == 0x92: num = (int(m[-2], 0) << 8) + int(m[-1], 0) if num >= 32768: @@ -308,7 +407,7 @@ def format_datatype(name, m, dt): return num -if __name__ == "__main__": +def main(): args = parse_args() if args.loglevel: @@ -319,7 +418,15 @@ if __name__ == "__main__": logging.Formatter("%(asctime)-15s %(levelname)s: %(message)s") ) + if args.action == "getsetting" and args.settingid is None: + logger.error("`--settingid` is required with `--action getsetting`") + return + wpu = IthoWPU(args.master_only, args.slave_only, args.slave_timeout, args.no_cache) - response = wpu.get(args.action) + response = wpu.get(args.action, args.settingid) if response is not None: process_response(args.action, response, args, wpu) + + +if __name__ == "__main__": + main() diff --git a/itho_i2c.py b/itho_i2c.py index 00dfd6f..64e8f4f 100644 --- a/itho_i2c.py +++ b/itho_i2c.py @@ -12,6 +12,7 @@ actions = { "getserial": [0x90, 0xE1], "getdatatype": [0xA4, 0x00], "getdatalog": [0xA4, 0x01], + "getsetting": [0xA4, 0x10], } @@ -45,9 +46,21 @@ class I2CMaster: self.i = I2CRaw(address=address, bus=bus) self.queue = queue - def compose_request(self, action): - # 0x80 = source, 0x04 = msg_type, 0x00 = length - request = [0x80] + actions[action] + [0x04, 0x00] + def compose_request(self, action, identifier): + if action == "getsetting": + request = ( + [0x80] + + actions[action] + + [0x04, 0x13] # read, length + + [0x00, 0x00, 0x00, 0x00] # current + + [0x00, 0x00, 0x00, 0x00] # min + + [0x00, 0x00, 0x00, 0x00] # max + + [0x00, 0x00, 0x00, 0x00] # step + + [0x00, identifier, 0x00] + ) + else: + # 0x80 = source, 0x04 = msg_type, 0x00 = length + request = [0x80] + actions[action] + [0x04, 0x00] request.append(self.calculate_checksum(request)) return request @@ -60,8 +73,8 @@ class I2CMaster: checksum = 0 return checksum - def execute_action(self, action): - request = self.compose_request(action) + def execute_action(self, action, identifier): + request = self.compose_request(action, identifier) request_in_hex = [hex(c) for c in request] logger.debug(f"Request: {request_in_hex}") result = None From 7a1140e26c99e86ca3566b79aa78c1f00b9f22df Mon Sep 17 00:00:00 2001 From: Pim van den Berg Date: Tue, 11 Jul 2023 16:01:42 +0200 Subject: [PATCH 04/10] feat(itho-wpu): getsettings support A loop around getsetting based on the settings available in the database to retrieve all settings from the WPU. Example usage: > ./itho-wpu.py --action getsettings > 0. Niet Gebruikt: 0 (min: 0, max: 65535, step: 1) > 1. Hardware Configuratie: 70 (min: 0, max: 65535, step: 1) > 2. Jaar Inbedrijfstelling: 2010 (min: 2004, max: 2099, step: 1) --- README.md | 16 ++++++++++++++++ itho-wpu.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a65868f..d51ba9f 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,22 @@ See the [pislave](https://github.com/ootjersb/pislave#wiring) project 1. Hardware Configuratie: 70 (min: 0, max: 65535, step: 1) ``` +* Retrieve all settings from the WPU + ``` + # ./itho-wpu.py --action getsettings + 0. Niet Gebruikt: 0 (min: 0, max: 65535, step: 1) + 1. Hardware Configuratie: 70 (min: 0, max: 65535, step: 1) + 2. Jaar Inbedrijfstelling: 2010 (min: 2004, max: 2099, step: 1) + 3. Datum Van Inbedrijfstelling: 101 (min: 0, max: 3112, step: 1) + 4. Max Handbedieningstijd (min): 0 (min: 0, max: 600, step: 1) + 5. Vorsttemp (°C): 2.0 (min: -10.0, max: 10.0, step: 0.1) + 6. Offset Voor Vorst Temp (K): 2.0 (min: 0.0, max: 10.0, step: 0.1) + 7. Differentie Van Vorst Om Elektrisch Element Te Starten (K): 1.5 (min: 0.0, max: 10.0, step: 0.1) + 8. Fout Reset Tijd (min): 120 (min: 1, max: 1440, step: 1) + 9. Loginterval Tijd (sec): 5 (min: 1, max: 300, step: 1) + ... + ``` + # Exporting measurements ## InfluxDB diff --git a/itho-wpu.py b/itho-wpu.py index 077528e..199472f 100755 --- a/itho-wpu.py +++ b/itho-wpu.py @@ -34,7 +34,7 @@ def parse_args(): "--action", nargs="?", required=True, - choices=actions.keys(), + choices=list(actions.keys()) + ["getsettings"], help="Execute an action", ) parser.add_argument( @@ -157,6 +157,20 @@ class IthoWPU: return datalog return datalog + def get_settings(self): + listversion = self.get_listversion_from_nodeid() + parameterlist_version = self.heatpump_db.execute( + f"SELECT parameterlist FROM versiebeheer WHERE version = {listversion}" + )[0]["parameterlist"] + if parameterlist_version is None or not type(parameterlist_version) == int: + logger.error(f"Parameterlist not found in database for version {listversion}") + return None + settings = self.heatpump_db.execute( + "SELECT id, name, min, max, def, title, description, unit " + + f"FROM parameterlijst_v{parameterlist_version}" + ) + return settings + def get_setting_by_id(self, settingid): listversion = self.get_listversion_from_nodeid() parameterlist_version = self.heatpump_db.execute( @@ -339,6 +353,14 @@ def process_setting(response, wpu): ) +def process_settings(wpu, args): + settings = wpu.get_settings() + for setting in settings: + response = wpu.get("getsetting", int(setting["id"])) + if response is not None: + process_response("getsetting", response, args, wpu) + + def format_datatype(name, m, dt): """ Transform a list of bytes to a readable number based on the datatype. @@ -423,6 +445,11 @@ def main(): return wpu = IthoWPU(args.master_only, args.slave_only, args.slave_timeout, args.no_cache) + + if args.action == "getsettings": + process_settings(wpu, args) + return + response = wpu.get(args.action, args.settingid) if response is not None: process_response(args.action, response, args, wpu) From 1ffd5a72d78b1567c50caf70df28fa3f54e21977 Mon Sep 17 00:00:00 2001 From: Pim van den Berg Date: Wed, 19 Jul 2023 13:34:21 +0200 Subject: [PATCH 05/10] fix(itho_i2c): make loglevel in itho_i2c module adjustable --- itho-wpu.py | 1 + itho_i2c.py | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/itho-wpu.py b/itho-wpu.py index 199472f..431353d 100755 --- a/itho-wpu.py +++ b/itho-wpu.py @@ -434,6 +434,7 @@ def main(): if args.loglevel: logger.setLevel(args.loglevel.upper()) + logging.getLogger("itho_i2c").setLevel(args.loglevel.upper()) if args.timestamp: stdout_log_handler.setFormatter( diff --git a/itho_i2c.py b/itho_i2c.py index 64e8f4f..ca4b660 100644 --- a/itho_i2c.py +++ b/itho_i2c.py @@ -4,8 +4,13 @@ import logging import pigpio import struct import time +import sys logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) +stdout_log_handler = logging.StreamHandler(sys.stdout) +stdout_log_handler.setFormatter(logging.Formatter("%(message)s")) +logger.addHandler(stdout_log_handler) actions = { "getnodeid": [0x90, 0xE0], From 381dd4c6ec45549a03adbb5e27a2c15d73da33ae Mon Sep 17 00:00:00 2001 From: Pim van den Berg Date: Tue, 18 Jul 2023 17:23:26 +0200 Subject: [PATCH 06/10] feat(itho-wpu): setsetting support With action "setsetting" settings of an Itho WPU can be modified. --- README.md | 11 ++++++++++ itho-wpu.py | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++--- itho_i2c.py | 24 ++++++++++++++++++--- 3 files changed, 91 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d51ba9f..b8cd18f 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,17 @@ See the [pislave](https://github.com/ootjersb/pislave#wiring) project ... ``` +* Change a setting of the WPU + ``` + # ./itho-wpu.py --action setsetting --settingid 139 --value 48 + Current setting: + 139. Blokkade Tijd Van Verwarmen Naar Koelen (uur): 24 (min: 0, max: 168, step: 1) + Setting `139` will be changed to `48`? [y/N] y + Updating setting 139 to `48` + Are you really sure? (Type uppercase yes): YES + 139. Blokkade Tijd Van Verwarmen Naar Koelen (uur): 48 (min: 0, max: 168, step: 1) + ``` + # Exporting measurements ## InfluxDB diff --git a/itho-wpu.py b/itho-wpu.py index 431353d..eab40bd 100755 --- a/itho-wpu.py +++ b/itho-wpu.py @@ -24,6 +24,7 @@ actions = { "getdatatype": [0xA4, 0x00], "getdatalog": [0xA4, 0x01], "getsetting": [0xA4, 0x10], + "setsetting": [0xA4, 0x10], } @@ -43,6 +44,11 @@ def parse_args(): type=int, help="Setting identifier", ) + parser.add_argument( + "--value", + nargs="?", + help="Setting value", + ) parser.add_argument( "--loglevel", nargs="?", @@ -82,7 +88,7 @@ class IthoWPU: self.datatype = self.get("getdatatype") self.heatpump_db = db.sqlite("heatpump.sqlite") - def get(self, action, identifier=None): + def get(self, action, identifier=None, value=None): if not self.no_cache: response = self.cache.get(action.replace("get", "")) if response is not None: @@ -100,7 +106,7 @@ class IthoWPU: if not self.slave_only: master = I2CMaster(address=0x41, bus=1, queue=self._q) if action: - response = master.execute_action(action, identifier) + response = master.execute_action(action, identifier, value) logger.debug(f"Response: {response}") master.close() @@ -361,6 +367,52 @@ def process_settings(wpu, args): process_response("getsetting", response, args, wpu) +def process_setsetting(wpu, args): + logger.info("Current setting:") + response = wpu.get("getsetting", int(args.settingid)) + if response is None: + return + process_response("getsetting", response, args, wpu) + message = response[5:] + datatype = message[16] + + if args.value is None: + value = input("Provide a new value: ") + else: + value = args.value + + # TODO: add support for negative values + if float(value) < 0: + logger.error("Negative values are not supported yet.") + return + + logger.debug(f"New setting datatype: {datatype}") + logger.debug(f"New setting (input): {value}") + normalized_value = int(value.replace(".", "")) + logger.debug(f"New setting (normalized): {normalized_value}") + hex_list_value = [hex(v) for v in list(normalized_value.to_bytes(4, byteorder="big"))] + logger.debug(f"New setting (hex): {hex_list_value}") + parsed_value = format_datatype(args.settingid, hex_list_value, datatype) + logger.debug(f"New setting (parsed): {parsed_value}") + + _, minimum, maximum, _ = parse_setting(response, wpu) + if parsed_value < minimum or parsed_value > maximum: + logger.error(f"New value `{parsed_value}` is not between `{minimum}` and `{maximum}`") + return + + sure = input(f"Setting `{args.settingid}` will be changed to `{parsed_value}`? [y/N] ") + if sure in ["y", "Y"]: + logger.info(f"Updating setting {args.settingid} to `{parsed_value}`") + else: + logger.error("Aborted") + return + + response = wpu.get("setsetting", args.settingid, normalized_value) + if response is None: + return + process_response("getsetting", response, args, wpu) + + def format_datatype(name, m, dt): """ Transform a list of bytes to a readable number based on the datatype. @@ -441,7 +493,7 @@ def main(): logging.Formatter("%(asctime)-15s %(levelname)s: %(message)s") ) - if args.action == "getsetting" and args.settingid is None: + if args.action in ["getsetting", "setsetting"] and args.settingid is None: logger.error("`--settingid` is required with `--action getsetting`") return @@ -451,6 +503,10 @@ def main(): process_settings(wpu, args) return + if args.action == "setsetting": + process_setsetting(wpu, args) + return + response = wpu.get(args.action, args.settingid) if response is not None: process_response(args.action, response, args, wpu) diff --git a/itho_i2c.py b/itho_i2c.py index ca4b660..e0f4310 100644 --- a/itho_i2c.py +++ b/itho_i2c.py @@ -18,6 +18,7 @@ actions = { "getdatatype": [0xA4, 0x00], "getdatalog": [0xA4, 0x01], "getsetting": [0xA4, 0x10], + "setsetting": [0xA4, 0x10], } @@ -51,7 +52,7 @@ class I2CMaster: self.i = I2CRaw(address=address, bus=bus) self.queue = queue - def compose_request(self, action, identifier): + def compose_request(self, action, identifier, value): if action == "getsetting": request = ( [0x80] @@ -63,6 +64,18 @@ class I2CMaster: + [0x00, 0x00, 0x00, 0x00] # step + [0x00, identifier, 0x00] ) + elif action == "setsetting": + byte_list_value = list(value.to_bytes(4, byteorder="big")) + request = ( + [0x80] + + actions[action] + + [0x06, 0x13] # write, length + + byte_list_value # new + + [0x00, 0x00, 0x00, 0x00] # min + + [0x00, 0x00, 0x00, 0x00] # max + + [0x00, 0x00, 0x00, 0x00] # step + + [0x00, identifier, 0x00] + ) else: # 0x80 = source, 0x04 = msg_type, 0x00 = length request = [0x80] + actions[action] + [0x04, 0x00] @@ -78,11 +91,16 @@ class I2CMaster: checksum = 0 return checksum - def execute_action(self, action, identifier): - request = self.compose_request(action, identifier) + def execute_action(self, action, identifier, value): + request = self.compose_request(action, identifier, value) request_in_hex = [hex(c) for c in request] logger.debug(f"Request: {request_in_hex}") result = None + if action == "setsetting": + sure = input("Are you really sure? (Type uppercase yes): ") + if sure != "YES": + logger.error("Aborted") + return for i in range(0, 20): logger.debug(f"Executing action: {action}") self.i.write_i2c_block_data(request) From 63f06400ef747f65b1fd12f431405e9ca9d2104c Mon Sep 17 00:00:00 2001 From: Pim van den Berg Date: Fri, 21 Jul 2023 15:26:12 +0200 Subject: [PATCH 07/10] feat: add support for reading manual operations --- README.md | 10 ++++++-- convert-itho-db.py | 28 ++++++++++++++++++--- db.py | 29 +++++++++++++++++++--- itho-wpu.py | 61 ++++++++++++++++++++++++++++++++++++++-------- itho_i2c.py | 11 +++++++++ 5 files changed, 120 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index b8cd18f..2ad6526 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ See the [pislave](https://github.com/ootjersb/pislave#wiring) project * Retrieve a single setting from the WPU ``` - # ./itho-wpu.py --action getsetting --settingid 1 + # ./itho-wpu.py --action getsetting --id 1 1. Hardware Configuratie: 70 (min: 0, max: 65535, step: 1) ``` @@ -123,7 +123,7 @@ See the [pislave](https://github.com/ootjersb/pislave#wiring) project * Change a setting of the WPU ``` - # ./itho-wpu.py --action setsetting --settingid 139 --value 48 + # ./itho-wpu.py --action setsetting --id 139 --value 48 Current setting: 139. Blokkade Tijd Van Verwarmen Naar Koelen (uur): 24 (min: 0, max: 168, step: 1) Setting `139` will be changed to `48`? [y/N] y @@ -132,6 +132,12 @@ See the [pislave](https://github.com/ootjersb/pislave#wiring) project 139. Blokkade Tijd Van Verwarmen Naar Koelen (uur): 48 (min: 0, max: 168, step: 1) ``` +* Retrieve a manual operation setting from the WPU + ``` + # ./itho-wpu.py --loglevel info --action getmanual --id 0 + 0. Buitentemp (°C): 10.0 + ``` + # Exporting measurements ## InfluxDB diff --git a/convert-itho-db.py b/convert-itho-db.py index 6a44154..090f415 100755 --- a/convert-itho-db.py +++ b/convert-itho-db.py @@ -38,7 +38,7 @@ def convert(par_file, sqlite_db): tables = [] for table_info in par_cur.tables(tableType="TABLE"): - if re.match("^(VersieBeheer|Data[Ll]abel|Parameterlijst)", table_info.table_name): + if re.match("^(VersieBeheer|Data[Ll]abel|Parameterlijst|Handbed)", table_info.table_name): tables.append(table_info.table_name) for t in sorted(tables): @@ -69,11 +69,31 @@ def convert(par_file, sqlite_db): r.Eenheid_NL, ) ) - if re.match("^VersieBeheer", t): - par_cur.execute(f"select VersieNummer, DataLabel, ParameterLijst from {t}") + if re.match("^Handbed", t): + par_cur.execute( + "select Index, Naam, Naam_fabriek, Min, Max, Default, " + f"Tekst_NL, Tooltip_NL, Eenheid_NL from {t}" + ) rows = par_cur.fetchall() for r in sorted(rows): - data.append((r.VersieNummer, r.DataLabel, r.ParameterLijst)) + data.append( + ( + r.Index, + r.Naam, + r.Naam_fabriek, + r.Min, + r.Max, + r.Default, + r.Tekst_NL, + r.Tooltip_NL, + r.Eenheid_NL, + ) + ) + if re.match("^VersieBeheer", t): + par_cur.execute(f"select VersieNummer, DataLabel, ParameterLijst, Handbed from {t}") + rows = par_cur.fetchall() + for r in sorted(rows): + data.append((r.VersieNummer, r.DataLabel, r.ParameterLijst, r.Handbed)) sqlite_db.insert(t.lower(), data) diff --git a/db.py b/db.py index ca9d32c..746312d 100644 --- a/db.py +++ b/db.py @@ -44,6 +44,21 @@ class sqlite: );""".format( t ) + elif t.startswith("handbed"): + query = """ + CREATE TABLE {} ( + id real, + name text, + name_factory text, + min real, + max real, + def real, + title text, + tooltip text, + unit text + );""".format( + t + ) elif t.startswith("parameterlijst"): query = """ CREATE TABLE {} ( @@ -64,7 +79,8 @@ class sqlite: CREATE TABLE {} ( version integer primary key, datalabel integer, - parameterlist integer + parameterlist integer, + handbed integer );""".format( t ) @@ -86,10 +102,17 @@ class sqlite: """.format( t ) + elif t.startswith("handbed"): + query = """ + INSERT INTO {} (id, name, name_factory, min, max, def, title, tooltip, unit) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?); + """.format( + t + ) elif t.startswith("versiebeheer"): query = """ - INSERT INTO {} (version, datalabel, parameterlist) - VALUES (?, ?, ?); + INSERT INTO {} (version, datalabel, parameterlist, handbed) + VALUES (?, ?, ?, ?); """.format( t ) diff --git a/itho-wpu.py b/itho-wpu.py index eab40bd..59bd02f 100755 --- a/itho-wpu.py +++ b/itho-wpu.py @@ -25,6 +25,7 @@ actions = { "getdatalog": [0xA4, 0x01], "getsetting": [0xA4, 0x10], "setsetting": [0xA4, 0x10], + "getmanual": [0x40, 0x30], } @@ -39,10 +40,10 @@ def parse_args(): help="Execute an action", ) parser.add_argument( - "--settingid", + "--id", nargs="?", type=int, - help="Setting identifier", + help="Setting or manual identifier", ) parser.add_argument( "--value", @@ -193,6 +194,22 @@ class IthoWPU: return None return setting_details[0] + def get_manual_by_id(self, manualid): + listversion = self.get_listversion_from_nodeid() + handbed_version = self.heatpump_db.execute( + f"SELECT handbed FROM versiebeheer WHERE version = {listversion}" + )[0]["handbed"] + if handbed_version is None or not type(handbed_version) == int: + logger.error(f"Handbed not found in database for version {listversion}") + return None + manual_details = self.heatpump_db.execute( + "SELECT name, min, max, def, title, tooltip, unit " + + f"FROM handbed_v{handbed_version} WHERE id = {manualid}" + ) + if len(manual_details) != 1: + return None + return manual_details[0] + class IthoWPUCache: def __init__(self): @@ -264,6 +281,8 @@ def process_response(action, response, args, wpu): export_to_influxdb(action, measurements) elif action == "getsetting": process_setting(response, wpu) + elif action == "getmanual": + process_manual(response, wpu) elif action == "getnodeid": process_nodeid(response) elif action == "getserial": @@ -369,7 +388,7 @@ def process_settings(wpu, args): def process_setsetting(wpu, args): logger.info("Current setting:") - response = wpu.get("getsetting", int(args.settingid)) + response = wpu.get("getsetting", int(args.id)) if response is None: return process_response("getsetting", response, args, wpu) @@ -392,7 +411,7 @@ def process_setsetting(wpu, args): logger.debug(f"New setting (normalized): {normalized_value}") hex_list_value = [hex(v) for v in list(normalized_value.to_bytes(4, byteorder="big"))] logger.debug(f"New setting (hex): {hex_list_value}") - parsed_value = format_datatype(args.settingid, hex_list_value, datatype) + parsed_value = format_datatype(args.id, hex_list_value, datatype) logger.debug(f"New setting (parsed): {parsed_value}") _, minimum, maximum, _ = parse_setting(response, wpu) @@ -400,19 +419,41 @@ def process_setsetting(wpu, args): logger.error(f"New value `{parsed_value}` is not between `{minimum}` and `{maximum}`") return - sure = input(f"Setting `{args.settingid}` will be changed to `{parsed_value}`? [y/N] ") + sure = input(f"Setting `{args.id}` will be changed to `{parsed_value}`? [y/N] ") if sure in ["y", "Y"]: - logger.info(f"Updating setting {args.settingid} to `{parsed_value}`") + logger.info(f"Updating setting {args.id} to `{parsed_value}`") else: logger.error("Aborted") return - response = wpu.get("setsetting", args.settingid, normalized_value) + response = wpu.get("setsetting", args.id, normalized_value) if response is None: return process_response("getsetting", response, args, wpu) +def process_manual(response, wpu): + message = response[5:] + + manualid = int(message[2], 0) + manual = wpu.get_manual_by_id(manualid) + if manual is None: + logger.error(f"Manual '{manualid}' is invalid") + return + + datatype = message[3] + value = format_datatype(manual["name"], message[4:6], datatype) + + logger.info( + "{}. {}{}: {}".format( + manualid, + manual["title"].title(), + f' ({manual["unit"]})' if manual["unit"] is not None else "", + value, + ) + ) + + def format_datatype(name, m, dt): """ Transform a list of bytes to a readable number based on the datatype. @@ -493,8 +534,8 @@ def main(): logging.Formatter("%(asctime)-15s %(levelname)s: %(message)s") ) - if args.action in ["getsetting", "setsetting"] and args.settingid is None: - logger.error("`--settingid` is required with `--action getsetting`") + if args.action in ["getsetting", "setsetting", "getmanual"] and args.id is None: + logger.error(f"`--id` is required with `--action {args.action}`") return wpu = IthoWPU(args.master_only, args.slave_only, args.slave_timeout, args.no_cache) @@ -507,7 +548,7 @@ def main(): process_setsetting(wpu, args) return - response = wpu.get(args.action, args.settingid) + response = wpu.get(args.action, args.id) if response is not None: process_response(args.action, response, args, wpu) diff --git a/itho_i2c.py b/itho_i2c.py index e0f4310..aa5986d 100644 --- a/itho_i2c.py +++ b/itho_i2c.py @@ -19,6 +19,7 @@ actions = { "getdatalog": [0xA4, 0x01], "getsetting": [0xA4, 0x10], "setsetting": [0xA4, 0x10], + "getmanual": [0x40, 0x30], } @@ -76,6 +77,16 @@ class I2CMaster: + [0x00, 0x00, 0x00, 0x00] # step + [0x00, identifier, 0x00] ) + elif action == "getmanual": + byte_identifier = list(identifier.to_bytes(2, byteorder="big")) + request = ( + [0x80] + + actions[action] + + [0x04, 0x04] # read, length + + [0x01] # bank + + byte_identifier + + [0x01] # 1 = manual + ) else: # 0x80 = source, 0x04 = msg_type, 0x00 = length request = [0x80] + actions[action] + [0x04, 0x00] From a11df838f8601b44b00f04608c043e258f8e0096 Mon Sep 17 00:00:00 2001 From: Pim van den Berg Date: Mon, 4 Sep 2023 19:46:00 +0200 Subject: [PATCH 08/10] feat: add support for writing manual operations Initiating and finishing a manual operation works a bit strange. First of all the Maximum Manual Operation Time is controlled by a setting (--id 4) and defaults to 0 (infinite). And instead of waiting for the timer to expire (which you have to do when its set to 0) you can finish a manual operation by passing "--no-check". Note: For some reason no response is given when a manual operation is written to. That's why execute_action() returns succesfully (with None) when its executing the "setmanual" action and the response queue size is 0. --- README.md | 40 ++++++++++++++++++++++++++++++++++++ itho-wpu.py | 59 +++++++++++++++++++++++++++++++++++++++++++++++++---- itho_i2c.py | 25 +++++++++++++++++++---- 3 files changed, 116 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 2ad6526..54a0257 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,46 @@ See the [pislave](https://github.com/ootjersb/pislave#wiring) project 0. Buitentemp (°C): 10.0 ``` +* Initiate a manual operation + ``` + # ./itho-wpu.py --action setmanual --id 0 --value 29.00 + Current manual operation: + 0. Buitentemp (°C): 10.0 + Manual `0` will be changed to `29.0`? [y/N] y + Updating manual operation 0 to `29.0` + Are you really sure? (Type uppercase yes): YES + ``` + +* Finish a manual operation + ``` + # ./itho-wpu.py --action setmanual --id 0 --value 29.00 --no-check + Current manual operation: + 0. Buitentemp (°C): 29.0 + Manual `0` will be changed to `29.0`? [y/N] y + Updating manual operation 0 to `29.0` + Are you really sure? (Type uppercase yes): YES + ``` + +* Reset all errors: + ``` + # ./itho-wpu.py --action setmanual --id 37 --value 1 + Current manual operation: + 37. Reset Alle Fouten: 0 + Manual `37` will be changed to `1`? [y/N] y + Updating manual operation 37 to `1` + Are you really sure? (Type uppercase yes): YES + ``` + +* Reset timer: + ``` + # ./itho-wpu.py --action setmanual --id 38 --value 1 + Current manual operation: + 38. Reset Timer: 0 + Manual `38` will be changed to `1`? [y/N] y + Updating manual operation 38 to `1` + Are you really sure? (Type uppercase yes): YES + ``` + # Exporting measurements ## InfluxDB diff --git a/itho-wpu.py b/itho-wpu.py index 59bd02f..3bcc1dd 100755 --- a/itho-wpu.py +++ b/itho-wpu.py @@ -26,6 +26,7 @@ actions = { "getsetting": [0xA4, 0x10], "setsetting": [0xA4, 0x10], "getmanual": [0x40, 0x30], + "setmanual": [0x40, 0x30], } @@ -50,6 +51,12 @@ def parse_args(): nargs="?", help="Setting value", ) + parser.add_argument( + "--check", + default=True, + action=argparse.BooleanOptionalAction, + help="Enable/disable manual operation (used with --setmanual)", + ) parser.add_argument( "--loglevel", nargs="?", @@ -89,7 +96,7 @@ class IthoWPU: self.datatype = self.get("getdatatype") self.heatpump_db = db.sqlite("heatpump.sqlite") - def get(self, action, identifier=None, value=None): + def get(self, action, identifier=None, datatype=None, value=None, check=True): if not self.no_cache: response = self.cache.get(action.replace("get", "")) if response is not None: @@ -107,7 +114,7 @@ class IthoWPU: if not self.slave_only: master = I2CMaster(address=0x41, bus=1, queue=self._q) if action: - response = master.execute_action(action, identifier, value) + response = master.execute_action(action, identifier, datatype, value, check) logger.debug(f"Response: {response}") master.close() @@ -426,7 +433,7 @@ def process_setsetting(wpu, args): logger.error("Aborted") return - response = wpu.get("setsetting", args.id, normalized_value) + response = wpu.get("setsetting", args.id, None, normalized_value) if response is None: return process_response("getsetting", response, args, wpu) @@ -454,6 +461,46 @@ def process_manual(response, wpu): ) +def process_setmanual(wpu, args): + logger.info("Current manual operation:") + response = wpu.get("getmanual", int(args.id)) + if response is None: + return + process_response("getmanual", response, args, wpu) + message = response[5:] + datatype = message[3] + + # TODO: check if Max Handbedieningstijd > 0 + + if args.value is None: + value = input("Provide a new value: ") + else: + value = args.value + + # TODO: add support for negative values + if float(value) < 0: + logger.error("Negative values are not supported yet.") + return + + logger.debug(f"New manual operation datatype: {datatype}") + logger.debug(f"New manual operation (input): {value}") + normalized_value = int(value.replace(".", "")) + logger.debug(f"New manual operation (normalized): {normalized_value}") + hex_list_value = [hex(v) for v in list(normalized_value.to_bytes(2, byteorder="big"))] + logger.debug(f"New manual operation (hex): {hex_list_value}") + parsed_value = format_datatype(args.id, hex_list_value, datatype) + logger.debug(f"New manual operation (parsed): {parsed_value}") + + sure = input(f"Manual `{args.id}` will be changed to `{parsed_value}`? [y/N] ") + if sure in ["y", "Y"]: + logger.info(f"Updating manual operation {args.id} to `{parsed_value}`") + else: + logger.error("Aborted") + return + + response = wpu.get("setmanual", args.id, int(datatype, 0), normalized_value, args.check) + + def format_datatype(name, m, dt): """ Transform a list of bytes to a readable number based on the datatype. @@ -534,7 +581,7 @@ def main(): logging.Formatter("%(asctime)-15s %(levelname)s: %(message)s") ) - if args.action in ["getsetting", "setsetting", "getmanual"] and args.id is None: + if args.action in ["getsetting", "setsetting", "getmanual", "setmanual"] and args.id is None: logger.error(f"`--id` is required with `--action {args.action}`") return @@ -548,6 +595,10 @@ def main(): process_setsetting(wpu, args) return + if args.action == "setmanual": + process_setmanual(wpu, args) + return + response = wpu.get(args.action, args.id) if response is not None: process_response(args.action, response, args, wpu) diff --git a/itho_i2c.py b/itho_i2c.py index aa5986d..f826bd3 100644 --- a/itho_i2c.py +++ b/itho_i2c.py @@ -20,6 +20,7 @@ actions = { "getsetting": [0xA4, 0x10], "setsetting": [0xA4, 0x10], "getmanual": [0x40, 0x30], + "setmanual": [0x40, 0x30], } @@ -53,7 +54,7 @@ class I2CMaster: self.i = I2CRaw(address=address, bus=bus) self.queue = queue - def compose_request(self, action, identifier, value): + def compose_request(self, action, identifier, datatype, value, check): if action == "getsetting": request = ( [0x80] @@ -87,6 +88,20 @@ class I2CMaster: + byte_identifier + [0x01] # 1 = manual ) + elif action == "setmanual": + byte_identifier = list(identifier.to_bytes(2, byteorder="big")) + byte_list_value = list(value.to_bytes(2, byteorder="big")) + byte_check = [0x01] if check else [0x00] + request = ( + [0x80] + + actions[action] + + [0x06, 0x07] # write, length + + [0x01] # bank + + byte_identifier + + [datatype] # datatype + + byte_list_value # new + + byte_check + ) else: # 0x80 = source, 0x04 = msg_type, 0x00 = length request = [0x80] + actions[action] + [0x04, 0x00] @@ -102,12 +117,12 @@ class I2CMaster: checksum = 0 return checksum - def execute_action(self, action, identifier, value): - request = self.compose_request(action, identifier, value) + def execute_action(self, action, identifier, datatype, value, check): + request = self.compose_request(action, identifier, datatype, value, check) request_in_hex = [hex(c) for c in request] logger.debug(f"Request: {request_in_hex}") result = None - if action == "setsetting": + if action in ["setsetting", "setmanual"]: sure = input("Are you really sure? (Type uppercase yes): ") if sure != "YES": logger.error("Aborted") @@ -120,6 +135,8 @@ class I2CMaster: if self.queue.qsize() > 0: result = self.queue.get() break + elif action == "setmanual" and self.queue.qsize() == 0: + return None if result is None: logger.error("No valid result in 20 requests") From f0c350971f5271770243963d39c1e0c8bb424bca Mon Sep 17 00:00:00 2001 From: Pim van den Berg Date: Sun, 10 Sep 2023 18:53:32 +0200 Subject: [PATCH 09/10] chore(itho-wpu): rename wpu.get -> wpu.call --- itho-wpu.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/itho-wpu.py b/itho-wpu.py index 3bcc1dd..03e3901 100755 --- a/itho-wpu.py +++ b/itho-wpu.py @@ -92,13 +92,13 @@ class IthoWPU: self._q = queue.Queue() self.no_cache = no_cache self.cache = IthoWPUCache() - self.nodeid = self.get("getnodeid") - self.datatype = self.get("getdatatype") + self.nodeid = self.call("getnodeid") + self.datatype = self.call("getdatatype") self.heatpump_db = db.sqlite("heatpump.sqlite") - def get(self, action, identifier=None, datatype=None, value=None, check=True): + def call(self, action, identifier=None, datatype=None, value=None, check=True): if not self.no_cache: - response = self.cache.get(action.replace("get", "")) + response = self.cache.call(action.replace("get", "")) if response is not None: logger.debug(f"Response (from cache): {response}") return response @@ -245,7 +245,7 @@ class IthoWPUCache: logger.debug(f"Writing to local cache: {json.dumps(self._cache_data)}") json.dump(self._cache_data, cache_file) - def get(self, action): + def call(self, action): if action not in ["nodeid", "serial", "datatype"]: logger.debug(f"Cache for '{action}' is not supported") return None @@ -388,14 +388,14 @@ def process_setting(response, wpu): def process_settings(wpu, args): settings = wpu.get_settings() for setting in settings: - response = wpu.get("getsetting", int(setting["id"])) + response = wpu.call("getsetting", int(setting["id"])) if response is not None: process_response("getsetting", response, args, wpu) def process_setsetting(wpu, args): logger.info("Current setting:") - response = wpu.get("getsetting", int(args.id)) + response = wpu.call("getsetting", int(args.id)) if response is None: return process_response("getsetting", response, args, wpu) @@ -433,7 +433,7 @@ def process_setsetting(wpu, args): logger.error("Aborted") return - response = wpu.get("setsetting", args.id, None, normalized_value) + response = wpu.call("setsetting", args.id, None, normalized_value) if response is None: return process_response("getsetting", response, args, wpu) @@ -463,7 +463,7 @@ def process_manual(response, wpu): def process_setmanual(wpu, args): logger.info("Current manual operation:") - response = wpu.get("getmanual", int(args.id)) + response = wpu.call("getmanual", int(args.id)) if response is None: return process_response("getmanual", response, args, wpu) @@ -498,7 +498,7 @@ def process_setmanual(wpu, args): logger.error("Aborted") return - response = wpu.get("setmanual", args.id, int(datatype, 0), normalized_value, args.check) + response = wpu.call("setmanual", args.id, int(datatype, 0), normalized_value, args.check) def format_datatype(name, m, dt): @@ -599,7 +599,7 @@ def main(): process_setmanual(wpu, args) return - response = wpu.get(args.action, args.id) + response = wpu.call(args.action, args.id) if response is not None: process_response(args.action, response, args, wpu) From 7048014a223431d9024f0c5fca820f5ad146e19c Mon Sep 17 00:00:00 2001 From: Pim van den Berg Date: Sun, 10 Sep 2023 19:02:00 +0200 Subject: [PATCH 10/10] feat: add support for reading counters --- README.md | 25 +++++++++++++++++++++++++ convert-itho-db.py | 23 ++++++++++++++++++++--- db.py | 25 ++++++++++++++++++++++--- itho-wpu.py | 34 ++++++++++++++++++++++++++++++++++ itho_i2c.py | 1 + 5 files changed, 102 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 54a0257..c2b4abe 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,31 @@ See the [pislave](https://github.com/ootjersb/pislave#wiring) project 139. Blokkade Tijd Van Verwarmen Naar Koelen (uur): 48 (min: 0, max: 168, step: 1) ``` +* Retrieve counters of the WPU + ``` + ./itho-wpu.py --action getcounters + 0. Bedrijf Cv Pomp (cnt_chpump): 5295 uur + 1. Bedrijf Bron Pomp (cnt_source): 873 uur + 2. Bedrijf Boiler Pomp (cnt_dhw): 56085 uur + 3. Bedrijf Compressor (cnt_comp): 53066 uur + 4. Bedrijf Elektrisch Element (cnt_elek): 45568 uur + 5. Cv Bedrijf (cnt_chmode): 2869 uur + 6. Boiler Bedrijf (cnt_dhwmode): 42518 uur + 7. Vrijkoel Bedrijf (cnt_fcmode): 64098 uur + 8. Bedrijf (cnt_run): 34559 uur + 9. Cv Pomp Starts (cnt_chpstart): 65317 + 10. Bron Pomp Starts (cnt_srcpstart): 40672 + 11. Boiler Pomp Starts (cnt_dhwpstart): 30485 + 12. Compressor Starts (cnt_compstart): 62495 + 13. Elektrisch Element Start (cnt_elekstart): 44800 + 14. Cv Starts (cnt_chmstart): 9229 + 15. Boiler Starts (cnt_dhwmstart): 2069 + 16. Vrijkoel Starts (cnt_fcmstart): 62218 + 17. Systeem Starts (cnt_system): 61952 + 18. Bedrijf Dhw Element (cnt_dhwelement): 15616 uur + 19. Dhw E-Element Starts (cnt_dhwestart): 0 + ``` + * Retrieve a manual operation setting from the WPU ``` # ./itho-wpu.py --loglevel info --action getmanual --id 0 diff --git a/convert-itho-db.py b/convert-itho-db.py index 090f415..6e2753a 100755 --- a/convert-itho-db.py +++ b/convert-itho-db.py @@ -38,7 +38,9 @@ def convert(par_file, sqlite_db): tables = [] for table_info in par_cur.tables(tableType="TABLE"): - if re.match("^(VersieBeheer|Data[Ll]abel|Parameterlijst|Handbed)", table_info.table_name): + if re.match( + "^(VersieBeheer|Data[Ll]abel|Parameterlijst|Handbed|Counters)", table_info.table_name + ): tables.append(table_info.table_name) for t in sorted(tables): @@ -69,6 +71,19 @@ def convert(par_file, sqlite_db): r.Eenheid_NL, ) ) + if re.match("^Counters", t): + par_cur.execute(f"select Index, Naam, Tekst_NL, Tooltip_NL, Eenheid_NL from {t}") + rows = par_cur.fetchall() + for r in sorted(rows): + data.append( + ( + r.Index, + r.Naam, + r.Tekst_NL, + r.Tooltip_NL, + r.Eenheid_NL, + ) + ) if re.match("^Handbed", t): par_cur.execute( "select Index, Naam, Naam_fabriek, Min, Max, Default, " @@ -90,10 +105,12 @@ def convert(par_file, sqlite_db): ) ) if re.match("^VersieBeheer", t): - par_cur.execute(f"select VersieNummer, DataLabel, ParameterLijst, Handbed from {t}") + par_cur.execute( + f"select VersieNummer, DataLabel, ParameterLijst, Handbed, Counters from {t}" + ) rows = par_cur.fetchall() for r in sorted(rows): - data.append((r.VersieNummer, r.DataLabel, r.ParameterLijst, r.Handbed)) + data.append((r.VersieNummer, r.DataLabel, r.ParameterLijst, r.Handbed, r.Counters)) sqlite_db.insert(t.lower(), data) diff --git a/db.py b/db.py index 746312d..6f8b12e 100644 --- a/db.py +++ b/db.py @@ -44,6 +44,17 @@ class sqlite: );""".format( t ) + elif t.startswith("counters"): + query = """ + CREATE TABLE {} ( + id real, + name text, + title text, + tooltip text, + unit text + );""".format( + t + ) elif t.startswith("handbed"): query = """ CREATE TABLE {} ( @@ -80,7 +91,8 @@ class sqlite: version integer primary key, datalabel integer, parameterlist integer, - handbed integer + handbed integer, + counters interger );""".format( t ) @@ -102,6 +114,13 @@ class sqlite: """.format( t ) + elif t.startswith("counters"): + query = """ + INSERT INTO {} (id, name, title, tooltip, unit) + VALUES (?, ?, ?, ?, ?); + """.format( + t + ) elif t.startswith("handbed"): query = """ INSERT INTO {} (id, name, name_factory, min, max, def, title, tooltip, unit) @@ -111,8 +130,8 @@ class sqlite: ) elif t.startswith("versiebeheer"): query = """ - INSERT INTO {} (version, datalabel, parameterlist, handbed) - VALUES (?, ?, ?, ?); + INSERT INTO {} (version, datalabel, parameterlist, handbed, counters) + VALUES (?, ?, ?, ?, ?); """.format( t ) diff --git a/itho-wpu.py b/itho-wpu.py index 03e3901..42eadab 100755 --- a/itho-wpu.py +++ b/itho-wpu.py @@ -27,6 +27,7 @@ actions = { "setsetting": [0xA4, 0x10], "getmanual": [0x40, 0x30], "setmanual": [0x40, 0x30], + "getcounters": [0x42, 0x10], } @@ -171,6 +172,19 @@ class IthoWPU: return datalog return datalog + def get_counters(self): + listversion = self.get_listversion_from_nodeid() + counters_version = self.heatpump_db.execute( + f"SELECT counters FROM versiebeheer WHERE version = {listversion}" + )[0]["counters"] + if counters_version is None or not type(counters_version) == int: + logger.error(f"Counters not found in database for version {listversion}") + return None + settings = self.heatpump_db.execute( + "SELECT id, name, title, tooltip, unit " + f"FROM counters_v{counters_version}" + ) + return settings + def get_settings(self): listversion = self.get_listversion_from_nodeid() parameterlist_version = self.heatpump_db.execute( @@ -294,6 +308,8 @@ def process_response(action, response, args, wpu): process_nodeid(response) elif action == "getserial": process_serial(response) + elif action == "getcounters": + process_counters(response, wpu) def process_nodeid(response): @@ -324,6 +340,24 @@ def process_serial(response): logger.info(f"Serial: {serial}") +def process_counters(response, wpu): + counters = wpu.get_counters() + message = response[5:] + for c in counters: + index = int(c["id"]) * 2 + num = format_datatype(c["name"], message[index : index + 2], 0x10) # noqa: E203 + + logger.info( + "{}. {} ({}): {}{}".format( + int(c["id"]), + c["title"].title(), + c["name"].lower(), + num, + " " + c["unit"] if c["unit"] is not None else "", + ) + ) + + def process_datalog(response, wpu): datalog = wpu.get_datalog_structure() message = response[5:] diff --git a/itho_i2c.py b/itho_i2c.py index f826bd3..12a99b1 100644 --- a/itho_i2c.py +++ b/itho_i2c.py @@ -21,6 +21,7 @@ actions = { "setsetting": [0xA4, 0x10], "getmanual": [0x40, 0x30], "setmanual": [0x40, 0x30], + "getcounters": [0x42, 0x10], }