pax_global_header00006660000000000000000000000064143272355360014524gustar00rootroot0000000000000052 comment=bb06833ca696512629217a9343d56d146ba46bb4 vpathuis-ultraheat-bb06833/000077500000000000000000000000001432723553600157075ustar00rootroot00000000000000vpathuis-ultraheat-bb06833/.gitignore000066400000000000000000000002261432723553600176770ustar00rootroot00000000000000/__pycache__ /tests/__pycache__ /venv /dist .coverage tests/example_report.txt .vscode/launch.json ultraheat_api/__pycache__/ ultraheat_api.egg-info/ vpathuis-ultraheat-bb06833/.vscode/000077500000000000000000000000001432723553600172505ustar00rootroot00000000000000vpathuis-ultraheat-bb06833/.vscode/settings.json000066400000000000000000000004141432723553600220020ustar00rootroot00000000000000{ "python.formatting.provider": "black", "python.testing.unittestArgs": [ "-v", "-s", "./tests", "-p", "test_*.py" ], "python.testing.pytestEnabled": false, "python.testing.unittestEnabled": true }vpathuis-ultraheat-bb06833/LICENCE000066400000000000000000000021031432723553600166700ustar00rootroot00000000000000Copyright (c) 2018 The Python Packaging Authority Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.vpathuis-ultraheat-bb06833/README.md000066400000000000000000000071541432723553600171750ustar00rootroot00000000000000# Landis+Gyr Heat Meter Python package This module reads from the Landys & Gyr Ultraheat heat meter unit and returns the meter values. Note: An (USB) IR reader is needed and connected to the machine running the python script WARNING: everytime this is called, battery time of the Ultraheat will go down by about 30 minutes! This package has been tested with the Landys & Gyr Ultraheat type UH50 and T550. Other models are likely to work as well (please contact me if you want to help/test with adding support for other models). ## Using the python integration as API ```python import ultraheat_api as hm # Check available ports ports = hm.find_ports() for p in ports: print(p.device) print(len(ports), 'ports found') # Read the device from file for integration testing purposes path = os.path.abspath(os.path.dirname(__file__)) file_name = os.path.join(path, "tests", "LUGCUH50_dummy.txt") heat_meter_service = hm.HeatMeterService(hm.FileReader(file_name)) response_data = heat_meter_service.read() # Read the Ultraheat device heat_meter_service = hm.HeatMeterService(hm.UltraheatReader(args.port)) response_data = heat_meter_service.read() print('model :',heat_meter.model) print('GJ :',heat_meter.heat_usage_gj) # UH50 print('MWh :',heat_meter.heat_usage_mwh) # T550 print('m3 :',heat_meter.volume_usage_m3) etc.. ``` ## Full list of available data - heat_usage_gj (empty for T550) - heat_usage_mwh (empty for UH50) - volume_usage_m3 - ownership_number - volume_previous_year_m3 - heat_previous_year_gj (empty for T550) - heat_previous_year_mwh (empty for UH50) - error_number - device_number - measurement_period_minutes - power_max_kw - power_max_previous_year_kw - flowrate_max_m3ph - flowrate_max_previous_year_m3ph - flow_temperature_max_c - return_temperature_max_c - flow_temperature_max_previous_year_c - return_temperature_max_previous_year_c - operating_hours - fault_hours - fault_hours_previous_year - yearly_set_day - monthly_set_day - meter_date_time - measuring_range_m3ph - settings_and_firmware - flow_hours - raw_response ## Telegram parsing The telegram that is read from the Heat Meter is parsed as follows. For the UH50 (shown below) GJ is parsed. For the T550 this will be MWh. 6.8(**heat_usage_gj**\*GJ)6.26(**volume_usage_m3**\*m3)9.21(**ownership_number**) 6.26\*01(**volume_previous_year_m3**\*m3)6.8\*01(**heat_previous_year_gj**\*GJ) F(**error_number**)9.20(**device_number**)6.35(**measurement_period_minutes**\*m) 6.6(**power_max_kw**\*kW)6.6\*01(**power_max_previous_year_kw**\*kW)6.33(**flowrate_max_m3ph**\*m3ph)9.4(**flow_temperature_max_c**\*C&**return_temperature_max_c**\*C) 6.31(**operating_hours**\*h)6.32(**fault_hours**\*h)9.22(R)9.6(000&66153690&0&000&66153690&0) 9.7(20000)6.32\*01(**fault_hours_previous_year**\*h)6.36(**yearly_set_day**)6.33\*01(**flowrate_max_previous_year_m3ph**\*m3ph) 6.8.1()6.8.2()6.8.3()6.8.4()6.8.5() 6.8.1\*01()6.8.2\*01()6.8.3\*01() 6.8.4\*01()6.8.5\*01() 9.4\*01(**flow_temperature_max_previous_year_c**\*C&**return_temperature_max_previous_year_c**\*C) 6.36.1(2018-03-03)6.36.1\*01(2018-03-03) 6.36.2(2020-06-23)6.36.2\*01(2020-06-23) 6.36.3(2012-02-03)6.36.3\*01(2012-02-03) 6.36.4(2017-01-18)6.36.4\*01(2017-01-18) 6.36.5()6.36\*02(**monthly_set_day**)9.36(**meter_date_time**)9.24(**measuring_range_m3ph**\*m3ph) 9.17(0)9.18()9.19()9.25() 9.1(**settings_and_firmware**) 9.2(&&)9.29()9.31(**flow_hours**\*h) 9.0.1(00000000)9.0.2(00000000)9.34.1(000.00000\*m3)9.34.2(000.00000\*m3) 8.26.1(00000000\*m3)8.26.2(00000000\*m3) 8.26.1\*01(00000000\*m3)8.26.2\*01(00000000\*m3) 6.26.1()6.26.4()6.26.5() 6.26.1\*01()6.26.4\*01()6.26.5\*01()0.0(66153690) ! vpathuis-ultraheat-bb06833/__main__.py000066400000000000000000000035371432723553600200110ustar00rootroot00000000000000import argparse, sys from pprint import pprint import os import sys from ultraheat_api.find_ports import find_ports from ultraheat_api.service import HeatMeterService from ultraheat_api.file_reader import FileReader from ultraheat_api.ultraheat_reader import UltraheatReader parser = argparse.ArgumentParser() def parse_arguments(): parser.add_argument( "--file", help="Choose file mode and supply the filename or type default" ) parser.add_argument("--ports", help="Show available ports", action="store_true") parser.add_argument( "--port", help="Choose port mode, supply the port name, eg. '/dev/ttyUSB0' or 'COM5'", ) parser.add_argument( "--validate", help="Choose validate mode. Combine with --file or --port", action="store_true", ) return parser.parse_args() args = parse_arguments() if args.ports: print("showing available ports: ") ports = find_ports() for p in ports: print(p.device) print(len(ports), "ports found") exit() if args.file: if args.file == "default": print("Using default dummy file") path = os.path.abspath(os.path.dirname(__file__)) file_name = os.path.join(path, "tests", "LUGCUH50_dummy_utf8.txt") else: file_name = args.file reader = FileReader(file_name) elif args.port: print( "WARNING: everytime the unit is read, battery time will go down by about 30 minutes!" ) print("Reading ... this will take some time...") reader = UltraheatReader(args.port) else: parser.print_help() exit() heat_meter_service = HeatMeterService(reader) if args.validate: model = heat_meter_service.validate() print("model: " + model) else: response_data = heat_meter_service.read() pprint(response_data) vpathuis-ultraheat-bb06833/setup.py000066400000000000000000000011571432723553600174250ustar00rootroot00000000000000from setuptools import setup # read the contents of your README file from pathlib import Path this_directory = Path(__file__).parent long_description = (this_directory / "README.md").read_text() setup( name="ultraheat_api", version="0.5.1", description="Reading usage data from the Landys & Gyr Ultraheat heat meter unit", long_description=long_description, long_description_content_type="text/markdown", url="https://github.com/vpathuis/ultraheat", author="vpathuis", license="MIT", packages=["ultraheat_api"], install_requires=[ "pyserial", ], zip_safe=False, ) vpathuis-ultraheat-bb06833/tests/000077500000000000000000000000001432723553600170515ustar00rootroot00000000000000vpathuis-ultraheat-bb06833/tests/LGUHT550_dummy_error_utf8.txt000066400000000000000000000020011432723553600243120ustar00rootroot00000000000000/LUGCUH50 6.8(032A.062*MWh)6.26(07939.56*m3)9.21(00073600) 6.26*01(07843.48*m3)6.8*01(0323.272*MWh) F(0)9.20(66935074)6.35(60*m) 6.6(0028.0*kW)6.6*01(0028.0*kW)6.33(000.840*m3ph)9.4(108.5*C&088.1*C) 6.31(0088324*h)6.32(0000001*h)9.22(R)9.6(000&00073600&0&000&00073600&0) 9.7(20000)6.32*01(0000001*h)6.36(01-01&00:00)6.33*01(000.840*m3ph) 6.8.1()6.8.2()6.8.3()6.8.4()6.8.5() 6.8.1*01()6.8.2*01()6.8.3*01() 6.8.4*01()6.8.5*01() 9.4*01(108.5*C&088.1*C) 6.36.1(2018-09-07)6.36.1*01(2018-09-07) 6.36.2(2013-06-14)6.36.2*01(2013-06-14) 6.36.3(2012-10-03)6.36.3*01(2012-10-03) 6.36.4(2017-11-09)6.36.4*01(2017-11-09) 6.36.5()6.36*02(01&00:00)9.36(2022-06-08&10:06:51)9.24(1.5*m3ph) 9.17(0)9.18()9.19()9.25() 9.1(0&1&0&0017&CECV&CECV&1&5.19&5.19&F&081008&040404&08&0&00&?:) 9.2(&&)9.29()9.31(0059615*h) 9.0.1(00000000)9.0.2(00000000)9.34.1(000.00000*m3)9.34.2(000.00000*m3) 8.26.1(00000000*m3)8.26.2(00000000*m3) 8.26.1*01(00000000*m3)8.26.2*01(00000000*m3) 6.26.1()6.26.4()6.26.5() 6.26.1*01()6.26.4*01()6.26.5*01()0.0(00073600) !vpathuis-ultraheat-bb06833/tests/LGUHT550_dummy_utf8.txt000066400000000000000000000020011432723553600231010ustar00rootroot00000000000000/LUGCUH50 6.8(0326.062*MWh)6.26(07939.56*m3)9.21(00073600) 6.26*01(07843.48*m3)6.8*01(0323.272*MWh) F(0)9.20(66935074)6.35(60*m) 6.6(0028.0*kW)6.6*01(0028.0*kW)6.33(000.840*m3ph)9.4(108.5*C&088.1*C) 6.31(0088324*h)6.32(0000001*h)9.22(R)9.6(000&00073600&0&000&00073600&0) 9.7(20000)6.32*01(0000001*h)6.36(01-01&00:00)6.33*01(000.840*m3ph) 6.8.1()6.8.2()6.8.3()6.8.4()6.8.5() 6.8.1*01()6.8.2*01()6.8.3*01() 6.8.4*01()6.8.5*01() 9.4*01(108.5*C&088.1*C) 6.36.1(2018-09-07)6.36.1*01(2018-09-07) 6.36.2(2013-06-14)6.36.2*01(2013-06-14) 6.36.3(2012-10-03)6.36.3*01(2012-10-03) 6.36.4(2017-11-09)6.36.4*01(2017-11-09) 6.36.5()6.36*02(01&00:00)9.36(2022-06-08&10:06:51)9.24(1.5*m3ph) 9.17(0)9.18()9.19()9.25() 9.1(0&1&0&0017&CECV&CECV&1&5.19&5.19&F&081008&040404&08&0&00&?:) 9.2(&&)9.29()9.31(0059615*h) 9.0.1(00000000)9.0.2(00000000)9.34.1(000.00000*m3)9.34.2(000.00000*m3) 8.26.1(00000000*m3)8.26.2(00000000*m3) 8.26.1*01(00000000*m3)8.26.2*01(00000000*m3) 6.26.1()6.26.4()6.26.5() 6.26.1*01()6.26.4*01()6.26.5*01()0.0(00073600) !vpathuis-ultraheat-bb06833/tests/LUGCUH50_dummy_error_utf8.txt000066400000000000000000000017711432723553600243460ustar00rootroot00000000000000!LUGCUH50 6.8(032A.871*GJ)6.26(03329.67*m3)9.21(66153690) 6.26*01(03188.07*m3)6.8*01(0314.658*GJ) F(0)9.20(66153690)6.35(60*m) 6.6(0022.4*kW)6.6*01(0022.4*kW)6.33(000.744*m3ph)9.4(098.5*C&096.1*C) 6.31(0107988*h)6.32(0000005*h)9.22(R)9.6(000&66153690&0&000&66153690&0) 9.7(20000)6.32*01(0000005*h)6.36(01-01&00:00)6.33*01(000.744*m3ph) 6.8.1()6.8.2()6.8.3()6.8.4()6.8.5() 6.8.1*01()6.8.2*01()6.8.3*01() 6.8.4*01()6.8.5*01() 9.4*01(098.5*C&096.1*C) 6.36.1(2018-03-03)6.36.1*01(2018-03-03) 6.36.2(2020-06-23)6.36.2*01(2020-06-23) 6.36.3(2012-02-03)6.36.3*01(2012-02-03) 6.36.4(2017-01-18)6.36.4*01(2017-01-18) 6.36.5()6.36*02(01&00:00)9.36(2022-05-19&19:41:17)9.24(1.5*m3ph) 9.17(0)9.18()9.19()9.25() 9.1(0&1&0&0000&CECV&CECV&1&5.16&5.16&F&101008&040404&08&0) 9.2(&&)9.29()9.31(0028849*h) 9.0.1(00000000)9.0.2(00000000)9.34.1(000.00000*m3)9.34.2(000.00000*m3) 8.26.1(00000000*m3)8.26.2(00000000*m3) 8.26.1*01(00000000*m3)8.26.2*01(00000000*m3) 6.26.1()6.26.4()6.26.5() 6.26.1*01()6.26.4*01()6.26.5*01()0.0(66153690) ! hvpathuis-ultraheat-bb06833/tests/LUGCUH50_dummy_utf8.txt000066400000000000000000000017741432723553600231400ustar00rootroot00000000000000!LUGCUH50 6.8(0328.871*GJ)6.26(03329.67*m3)9.21(66153690) 6.26*01(03188.07*m3)6.8*01(0314.658*GJ) F(0)9.20(66153690)6.35(60*m) 6.6(0022.4*kW)6.6*01(0022.4*kW)6.33(000.744*m3ph)9.4(098.5*C&096.1*C) 6.31(0107988*h)6.32(0000005*h)9.22(R)9.6(000&66153690&0&000&66153690&0) 9.7(20000)6.32*01(0000005*h)6.36(01-01&00:00)6.33*01(000.744*m3ph) 6.8.1()6.8.2()6.8.3()6.8.4()6.8.5() 6.8.1*01()6.8.2*01()6.8.3*01() 6.8.4*01()6.8.5*01() 9.4*01(098.5*C&096.1*C) 6.36.1(2018-03-03)6.36.1*01(2018-03-03) 6.36.2(2020-06-23)6.36.2*01(2020-06-23) 6.36.3(2012-02-03)6.36.3*01(2012-02-03) 6.36.4(2017-01-18)6.36.4*01(2017-01-18) 6.36.5()6.36*02(01&00:00)9.36(2022-05-19&19:41:17)9.24(1.5*m3ph) 9.17(0)9.18()9.19()9.25() 9.1(0&1&0&0000&CECV&CECV&1&5.16&5.16&F&101008&040404&08&0) 9.2(&&)9.29()9.31(0028849*h) 9.0.1(00000000)9.0.2(00000000)9.34.1(000.00000*m3)9.34.2(000.00000*m3) 8.26.1(00000000*m3)8.26.2(00000000*m3) 8.26.1*01(00000000*m3)8.26.2*01(00000000*m3) 6.26.1()6.26.4()6.26.5() 6.26.1*01()6.26.4*01()6.26.5*01()0.0(66153690) ! h vpathuis-ultraheat-bb06833/tests/test_t550.py000066400000000000000000000070611432723553600211630ustar00rootroot00000000000000from datetime import datetime import os import unittest from unittest.mock import patch from ultraheat_api import FileReader, UltraheatReader, HeatMeterService DUMMY_FILE = 'LGUHT550_dummy_utf8.txt' DUMMY_FILE_ERROR = 'LGUHT550_dummy_error_utf8.txt' DUMMY_PORT = 'DUMMY' path = os.path.abspath(os.path.dirname(__file__)) dummy_file_path = os.path.join(path, DUMMY_FILE) dummy_file_path_error = os.path.join(path, DUMMY_FILE_ERROR) # Create a list from the dummy file to use as mock for reading the port with open(dummy_file_path, "rb") as f: mock_readline = f.read().splitlines() class HeatMeterTest(unittest.TestCase): def assert_response_data(self, response_data): # check the response dummy data self.assertEqual('LUGCUH50', response_data.model) self.assertEqual(326.062, response_data.heat_usage_mwh) self.assertEqual(7939.56, response_data.volume_usage_m3) self.assertEqual('00073600', response_data.ownership_number) self.assertEqual(7843.48, response_data.volume_previous_year_m3) self.assertEqual(323.272, response_data.heat_previous_year_mwh) self.assertEqual('0', response_data.error_number) self.assertEqual('66935074', response_data.device_number) self.assertEqual(60, response_data.measurement_period_minutes) self.assertEqual(28.0, response_data.power_max_kw) self.assertEqual(28.0, response_data.power_max_previous_year_kw) self.assertEqual(0.840, response_data.flowrate_max_m3ph) self.assertEqual(108.5, response_data.flow_temperature_max_c) self.assertEqual(88.1, response_data.return_temperature_max_c) self.assertEqual(88324, response_data.operating_hours) self.assertEqual(1, response_data.fault_hours) self.assertEqual(1, response_data.fault_hours_previous_year) self.assertEqual('01-01 00:00', response_data.yearly_set_day) self.assertEqual(0.840, response_data.flowrate_max_previous_year_m3ph) self.assertEqual( 108.5, response_data.flow_temperature_max_previous_year_c) self.assertEqual( 88.1, response_data.return_temperature_max_previous_year_c) self.assertEqual('01 00:00', response_data.monthly_set_day) self.assertEqual(datetime(2022, 6, 8, 10, 6, 51), response_data.meter_date_time) self.assertEqual(1.5, response_data.measuring_range_m3ph) self.assertEqual('0 1 0 0017 CECV CECV 1 5.19 5.19 F 081008 040404 08 0 00 ?:', response_data.settings_and_firmware) self.assertEqual(59615, response_data.flow_hours) @patch('ultraheat_api.ultraheat_reader.Serial') def test_read_port(self, mock_Serial): mock_Serial().__enter__().readline.side_effect = mock_readline reader = UltraheatReader(DUMMY_PORT) heat_meter_service = HeatMeterService(reader) response_data = heat_meter_service.read() self.assert_response_data(response_data) def test_heat_meter_read_file(self): heat_meter_service = HeatMeterService( FileReader(dummy_file_path) ) response_data = heat_meter_service.read() self.assert_response_data(response_data) def test_heat_meter_read_file_conversion_error(self): heat_meter_service: HeatMeterService = HeatMeterService( FileReader(dummy_file_path_error)) with self.assertRaises(ValueError): _ = heat_meter_service.read() if __name__ == '__main__': unittest.main() vpathuis-ultraheat-bb06833/tests/test_uh50.py000066400000000000000000000070471432723553600212530ustar00rootroot00000000000000from datetime import datetime import os import unittest from unittest.mock import patch from ultraheat_api import FileReader, UltraheatReader, HeatMeterService DUMMY_FILE = 'LUGCUH50_dummy_utf8.txt' DUMMY_FILE_ERROR = 'LUGCUH50_dummy_error_utf8.txt' DUMMY_PORT = 'DUMMY' path = os.path.abspath(os.path.dirname(__file__)) dummy_file_path = os.path.join(path, DUMMY_FILE) dummy_file_path_error = os.path.join(path, DUMMY_FILE_ERROR) # Create a list from the dummy file to use as mock for reading the port with open(dummy_file_path, "rb") as f: mock_readline = f.read().splitlines() class HeatMeterTest(unittest.TestCase): def assert_response_data(self, response_data): # check the response dummy data self.assertEqual('LUGCUH50', response_data.model) self.assertEqual(328.871, response_data.heat_usage_gj) self.assertEqual(3329.67, response_data.volume_usage_m3) self.assertEqual('66153690', response_data.ownership_number) self.assertEqual(3188.07, response_data.volume_previous_year_m3) self.assertEqual(314.658, response_data.heat_previous_year_gj) self.assertEqual('0', response_data.error_number) self.assertEqual('66153690', response_data.device_number) self.assertEqual(60, response_data.measurement_period_minutes) self.assertEqual(22.4, response_data.power_max_kw) self.assertEqual(22.4, response_data.power_max_previous_year_kw) self.assertEqual(0.744, response_data.flowrate_max_m3ph) self.assertEqual(98.5, response_data.flow_temperature_max_c) self.assertEqual(96.1, response_data.return_temperature_max_c) self.assertEqual(107988, response_data.operating_hours) self.assertEqual(5, response_data.fault_hours) self.assertEqual(5, response_data.fault_hours_previous_year) self.assertEqual('01-01 00:00', response_data.yearly_set_day) self.assertEqual(0.744, response_data.flowrate_max_previous_year_m3ph) self.assertEqual( 98.5, response_data.flow_temperature_max_previous_year_c) self.assertEqual( 96.1, response_data.return_temperature_max_previous_year_c) self.assertEqual('01 00:00', response_data.monthly_set_day) self.assertEqual(datetime(2022, 5, 19, 19, 41, 17), response_data.meter_date_time) self.assertEqual(1.5, response_data.measuring_range_m3ph) self.assertEqual('0 1 0 0000 CECV CECV 1 5.16 5.16 F 101008 040404 08 0', response_data.settings_and_firmware) self.assertEqual(28849, response_data.flow_hours) @patch('ultraheat_api.ultraheat_reader.Serial') def test_read_port(self, mock_Serial): mock_Serial().__enter__().readline.side_effect = mock_readline reader = UltraheatReader(DUMMY_PORT) heat_meter_service = HeatMeterService(reader) response_data = heat_meter_service.read() self.assert_response_data(response_data) def test_heat_meter_read_file(self): heat_meter_service = HeatMeterService( FileReader(dummy_file_path) ) response_data = heat_meter_service.read() self.assert_response_data(response_data) def test_heat_meter_read_file_conversion_error(self): heat_meter_service: HeatMeterService = HeatMeterService( FileReader(dummy_file_path_error)) with self.assertRaises(ValueError): _ = heat_meter_service.read() if __name__ == '__main__': unittest.main() vpathuis-ultraheat-bb06833/ultraheat_api/000077500000000000000000000000001432723553600205315ustar00rootroot00000000000000vpathuis-ultraheat-bb06833/ultraheat_api/__init__.py000066400000000000000000000003771432723553600226510ustar00rootroot00000000000000""" Landis+Gyr Ultraheat heat meter reader. """ from .find_ports import find_ports from .ultraheat_reader import UltraheatReader from .file_reader import FileReader from .service import HeatMeterService from .response import HeatMeterResponse vpathuis-ultraheat-bb06833/ultraheat_api/file_reader.py000066400000000000000000000010101432723553600233340ustar00rootroot00000000000000""" Reads raw response data from a file. This is for (integration) testing purposes, so you won't need to drain the battery while doing initial integration testing. """ from typing import Tuple class FileReader: def __init__(self, file_name) -> None: self._file_name = file_name def read(self) -> tuple[str, str]: with open(self._file_name, "rb") as f: model = f.readline().decode("utf-8")[1:9] return model, f.read().decode("utf-8") vpathuis-ultraheat-bb06833/ultraheat_api/find_ports.py000066400000000000000000000002131432723553600232460ustar00rootroot00000000000000import serial.tools.list_ports def find_ports(): """Returns the available ports""" return serial.tools.list_ports.comports() vpathuis-ultraheat-bb06833/ultraheat_api/response.py000066400000000000000000000165101432723553600227440ustar00rootroot00000000000000""" Formats the raw reponse data into a HeatMeterResponse object For different models, the raw data could be different. In these cases the RESPONSE_CONFIG might have to be modified. """ from dataclasses import dataclass import datetime import re # defines the search expressions used when parsing the response from the heat meter RESPONSE_CONFIG = { "heat_usage_gj": {"regex": r"6.8\((.*?)\*GJ\)", "unit": "GJ", "type": float}, "heat_usage_mwh": {"regex": r"6.8\((.*?)\*MWh\)", "unit": "MWh", "type": float}, "volume_usage_m3": {"regex": r"6.26\((.*?)\*m3\)", "unit": "m3", "type": float}, "ownership_number": {"regex": r"9.21\((.*?)\)", "type": str}, "volume_previous_year_m3": {"regex": r"6.26\*01\((.*?)\*m3\)", "unit": "m3", "type": float}, "heat_previous_year_gj": {"regex": r"6.8\*01\((.*?)\*GJ\)", "unit": "GJ", "type": float}, "heat_previous_year_mwh": {"regex": r"6.8\*01\((.*?)\*MWh\)", "unit": "MWh", "type": float}, "error_number": {"regex": r"F\((.*?)\)", "type": str}, "device_number": {"regex": r"9.20\((.*?)\)", "type": str}, "measurement_period_minutes": {"regex": r"6.35\((.*?)\*m\)", "type": int}, "power_max_kw": {"regex": r"6.6\((.*?)\*kW\)", "unit": "kW", "type": float}, "power_max_previous_year_kw": {"regex": r"6.6\*01\((.*?)\*kW\)", "unit": "kW", "type": float}, "flowrate_max_m3ph": {"regex": r"6.33\((.*?)\*m3ph\)", "unit": "m3ph", "type": float}, "flowrate_max_previous_year_m3ph": { "regex": r"6.33\*01\((.*?)\*m3ph\)", "unit": "m3ph", "type": float, }, "flow_temperature_max_c": {"regex": r"9.4\((.*?)\*C", "unit": "°C", "type": float}, "return_temperature_max_c": {"regex": r"9.4\(.*?\*C&(.*?)\*C", "unit": "°C", "type": float}, "flow_temperature_max_previous_year_c": { "regex": r"9.4\*01\((.*?)\*C", "unit": "°C", "type": float, }, "return_temperature_max_previous_year_c": { "regex": r"9.4\*01\(.*?\*C&(.*?)\*C", "unit": "°C", "type": float, }, "operating_hours": {"regex": r"6.31\((.*?)\*h\)", "type": int}, "fault_hours": {"regex": r"6.32\((.*?)\*h\)", "type": int}, "fault_hours_previous_year": {"regex": r"6.32\*01\((.*?)\*h\)", "type": int}, "yearly_set_day": {"regex": r"6.36\((.*?)\)", "type": lambda a: a.replace("&", " ")}, "monthly_set_day": {"regex": r"6.36\*02\((.*?)\)", "type": lambda a: a.replace("&", " ")}, "meter_date_time": { "regex": r"9.36\((.*?)\)", "type": lambda a: datetime.datetime.strptime(a, "%Y-%m-%d&%H:%M:%S"), }, "measuring_range_m3ph": {"regex": r"9.24\((.*?)\*m3ph\)", "unit": "m3ph", "type": float}, "settings_and_firmware": {"regex": r"9.1\((.*?)\)", "type": lambda a: a.replace("&", " ")}, "flow_hours": {"regex": r"9.31\((.*?)\*h\)", "type": int}, } @dataclass class HeatMeterResponse: model: str heat_usage_gj: float heat_usage_mwh: float volume_usage_m3: float ownership_number: str volume_previous_year_m3: float heat_previous_year_gj: float heat_previous_year_mwh: float error_number: str device_number: str measurement_period_minutes: int power_max_kw: float power_max_previous_year_kw: float flowrate_max_m3ph: float flow_temperature_max_c: float flowrate_max_previous_year_m3ph: float return_temperature_max_c: float flow_temperature_max_previous_year_c: float return_temperature_max_previous_year_c: float operating_hours: int fault_hours: int fault_hours_previous_year: int yearly_set_day: str monthly_set_day: str meter_date_time: datetime.datetime measuring_range_m3ph: float settings_and_firmware: str flow_hours: int raw_response: str class HeatMeterResponseParser: def parse(self, model, raw_response) -> HeatMeterResponse: heat_usage_gj = self._match("heat_usage_gj", raw_response) heat_usage_mwh = self._match("heat_usage_mwh", raw_response) volume_usage_m3 = self._match("volume_usage_m3", raw_response) ownership_number = self._match("ownership_number", raw_response) volume_previous_year_m3 = self._match("volume_previous_year_m3", raw_response) heat_previous_year_gj = self._match("heat_previous_year_gj", raw_response) heat_previous_year_mwh = self._match("heat_previous_year_mwh", raw_response) error_number = self._match("error_number", raw_response) device_number = self._match("device_number", raw_response) measurement_period_minutes = self._match("measurement_period_minutes", raw_response) power_max_kw = self._match("power_max_kw", raw_response) power_max_previous_year_kw = self._match("power_max_previous_year_kw", raw_response) flowrate_max_m3ph = self._match("flowrate_max_m3ph", raw_response) flow_temperature_max_c = self._match("flow_temperature_max_c", raw_response) flowrate_max_previous_year_m3ph = self._match("flowrate_max_previous_year_m3ph", raw_response) return_temperature_max_c = self._match("return_temperature_max_c", raw_response) flow_temperature_max_previous_year_c = self._match("flow_temperature_max_previous_year_c", raw_response) return_temperature_max_previous_year_c = self._match("return_temperature_max_previous_year_c", raw_response) operating_hours = self._match("operating_hours", raw_response) fault_hours = self._match("fault_hours", raw_response) fault_hours_previous_year = self._match("fault_hours_previous_year", raw_response) yearly_set_day = self._match("yearly_set_day", raw_response) monthly_set_day = self._match("monthly_set_day", raw_response) meter_date_time = self._match("meter_date_time", raw_response) measuring_range_m3ph = self._match("measuring_range_m3ph", raw_response) settings_and_firmware = self._match("settings_and_firmware", raw_response) flow_hours = self._match("flow_hours", raw_response) return HeatMeterResponse( model, heat_usage_gj, heat_usage_mwh, volume_usage_m3, ownership_number, volume_previous_year_m3, heat_previous_year_gj, heat_previous_year_mwh, error_number, device_number, measurement_period_minutes, power_max_kw, power_max_previous_year_kw, flowrate_max_m3ph, flow_temperature_max_c, flowrate_max_previous_year_m3ph, return_temperature_max_c, flow_temperature_max_previous_year_c, return_temperature_max_previous_year_c, operating_hours, fault_hours, fault_hours_previous_year, yearly_set_day, monthly_set_day, meter_date_time, measuring_range_m3ph, settings_and_firmware, flow_hours, raw_response ) def _match(self, name, raw_response): str_match = re.search( RESPONSE_CONFIG[name]["regex"], str(raw_response), re.M | re.I ) if str_match: try: return RESPONSE_CONFIG[name]["type"](str_match.group(1)) except ValueError: raise vpathuis-ultraheat-bb06833/ultraheat_api/service.py000066400000000000000000000007511432723553600225460ustar00rootroot00000000000000""" Reads raw Heat Meter data and returns a HeatMeterResponse object """ from ultraheat_api.response import HeatMeterResponse, HeatMeterResponseParser class HeatMeterService: """ Reads the heat meter and returns its value. """ def __init__(self, reader) -> None: self.reader = reader def read(self) -> HeatMeterResponse: (model, raw_response) = self.reader.read() return HeatMeterResponseParser().parse(model, raw_response) vpathuis-ultraheat-bb06833/ultraheat_api/ultraheat_reader.py000066400000000000000000000047211432723553600244220ustar00rootroot00000000000000""" Reads raw response data from the Ultraheat unit. To test the connection use validate, which will return the model name. """ import logging from typing import Tuple import serial from serial import Serial _LOGGER = logging.getLogger(__name__) MAX_LINES_ULTRAHEAT_REPONSE = 26 class UltraheatReader: def __init__(self, port) -> None: _LOGGER.debug("Initializing UltraheatReader on port: %s", port) self._port = port def read(self): """Reads the device on the specified port, returning the full string""" with self._connect_serial() as conn: return self._get_data(conn) def _connect_serial(self) -> Serial: """Make the connection to the serial device""" return Serial( self._port, baudrate=300, bytesize=serial.SEVENBITS, parity=serial.PARITY_EVEN, stopbits=serial.STOPBITS_TWO, timeout=5, xonxoff=0, rtscts=0, ) def _wake_up(self, conn) -> str: """Wake up the device and get the model number. Waking up should be done at 300 baud.""" # Sending /?! _LOGGER.debug("Waking up Ultraheat") conn.write( b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x2F\x3F\x21\x0D\x0A" ) # checking if we can read the model (eg. 'LUGCUH50') model = conn.readline().decode("utf-8")[1:9] if model: _LOGGER.debug("Got model %s", model) else: _LOGGER.error("No model could be read") raise Exception("No model could be read") return model def _get_data(self, conn) -> tuple[str, str]: model = self._wake_up(conn) _LOGGER.debug("Receiving data") # Now switch to 2400 BAUD. This could be different for other models. Let me know if you experience problems. conn.baudrate = 2400 ir_lines = "" ir_line = "" iteration = 0 # reading all lines (typically 25 lines) while "!" not in ir_line and iteration < MAX_LINES_ULTRAHEAT_REPONSE: iteration += 1 ir_line = conn.readline().decode("utf-8") ir_lines += ir_line _LOGGER.debug("Read %s lines of data", iteration) return model, str(ir_lines)