Skip to content

Commit

Permalink
Support kwh and much more logging (#12)
Browse files Browse the repository at this point in the history
* Add additional logging with undecoded and decoded data received, for future compatibility of new models

* Add debug logging for model

* Make baudrates variable and add module for probing possible ports

* Remove baudrate option from main

* Make try_baudrates a little more efficient

* Adding break on timeout to shorten trying baudrates

* Set version to 0.5.2

* Increase timeout to 30 seconds

* More debug logging

* More debug logging and add tests

* More debug logging

* Timeout to 60 seconds

* Add support and conversion for kWh

* Remove try_baudrates

* Simplify kwh test

* Make timeout variable through CLI

* Fix kwh-conversion and add more logging

* Set version to 0.5.7

* Fix debugging statement
  • Loading branch information
vpathuis authored Sep 12, 2023
1 parent bb06833 commit c07b666
Show file tree
Hide file tree
Showing 11 changed files with 185 additions and 35 deletions.
45 changes: 33 additions & 12 deletions __main__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import argparse, sys
import argparse
import logging
from pprint import pprint
import os
import sys

from ultraheat_api.const import DEFAULT_TIMEOUT
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(
Expand All @@ -19,16 +22,34 @@ def parse_arguments():
"--port",
help="Choose port mode, supply the port name, eg. '/dev/ttyUSB0' or 'COM5'",
)

parser.add_argument(
"--log",
help="Choose log level DEBUG, INFO, WARNING or ERROR",
)

parser.add_argument(
"--validate",
help="Choose validate mode. Combine with --file or --port",
action="store_true",
"--brw",
help="Set the baudrate for waking up the default. Defaults to 300",
)

parser.add_argument(
"--brd",
help="Set the baudrate for reading the datastream. Defaults to 2400",
)

parser.add_argument(
"--timeout",
help="Set the timeout for reading the datastream. Defaults to 60",
)

return parser.parse_args()


args = parse_arguments()
if args.log:
logging.basicConfig(level=args.log)

if args.ports:
print("showing available ports: ")
ports = find_ports()
Expand All @@ -47,20 +68,20 @@ def parse_arguments():
reader = FileReader(file_name)

elif args.port:
timeout = DEFAULT_TIMEOUT
if args.timeout:
timeout = args.timeout

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)
reader = UltraheatReader(port = args.port, timeout=timeout)
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)
response_data = heat_meter_service.read()
pprint(response_data)
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

setup(
name="ultraheat_api",
version="0.5.1",
version="0.5.7",
description="Reading usage data from the Landys & Gyr Ultraheat heat meter unit",
long_description=long_description,
long_description_content_type="text/markdown",
Expand Down
27 changes: 27 additions & 0 deletions tests/LUGCUH50_dummy_utf8_kwh.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
!LUGCUH50
6.8(0071813*kWh)6.26(01084.58*m3)9.21(70376057)
6.26*01(00898.98*m3)6.8*01(0059059*kWh)
F(0)9.20(70376057)6.35(60*m)
6.6(0026.3*kW)6.6*01(0026.3*kW)6.33(000.300*m3ph)9.4(114.9*C&090.8*C)\
6.31(0032214*h)6.32(0000000*h)9.22(R)9.6(000&70376057&0&000&70376057&0)
9.7(20000)6.32*01(0000000*h)6.36(01-01&00:00)6.33*01(000.300*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(114.9*C&083.9*C)
6.36.1(2021-02-11)6.36.1*01(2021-02-11)
6.36.2(2021-02-11)6.36.2*01(2021-02-11)
6.36.3(2021-02-11)6.36.3*01(2021-02-11)
6.36.4(2023-03-25)6.36.4*01(2020-09-06)
6.36.5()6.36*02(01&00:00)9.36(2023-08-20&10:22:22)9.24(1.5*m3ph)
9.17(0)9.18()9.19()9.25()
9.1(0&1&0&0000&CECV&CECV&1&5.24&5.24&F&101008&1>1>04&08&0&00&:5&00&20)
9.2(&&)9.29()9.31(0016196*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(70376057)
!
h

4 changes: 2 additions & 2 deletions tests/test_t550.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@

# 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()
mock_readline = f.read().splitlines(keepends=True)


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(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)
Expand Down
15 changes: 13 additions & 2 deletions tests/test_uh50.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@

# 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()

mock_readline = f.read().splitlines(keepends=True)

class HeatMeterTest(unittest.TestCase):

Expand Down Expand Up @@ -73,6 +72,18 @@ def test_heat_meter_read_file_conversion_error(self):
with self.assertRaises(ValueError):
_ = heat_meter_service.read()

@patch('ultraheat_api.ultraheat_reader.Serial')
def test_read_port_timeout(self, mock_Serial):
"""Empty data after first 3 lines, implying timeout on the readline."""
mock_readline_timeout = mock_readline[:3]
mock_readline_timeout.append(b"")
mock_Serial().__enter__().readline.side_effect = mock_readline_timeout
reader = UltraheatReader(DUMMY_PORT)

heat_meter_service = HeatMeterService(reader)
response_data = heat_meter_service.read()
self.assertEqual(328.871, response_data.heat_usage_gj)
self.assertEqual(None, response_data.operating_hours)

if __name__ == '__main__':
unittest.main()
33 changes: 33 additions & 0 deletions tests/test_uh50_kwh.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from datetime import datetime
import os
import unittest
from unittest.mock import patch
from ultraheat_api import FileReader, UltraheatReader, HeatMeterService

DUMMY_FILE = 'LUGCUH50_dummy_utf8_kwh.txt'
DUMMY_PORT = 'DUMMY'
path = os.path.abspath(os.path.dirname(__file__))
dummy_file_path = os.path.join(path, DUMMY_FILE)

# 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(keepends=True)

class HeatMeterTest(unittest.TestCase):

def assert_response_data(self, response_data):
# check the response dummy data
self.assertEqual('LUGCUH50', response_data.model)
self.assertEqual(71.813, response_data.heat_usage_mwh)
self.assertEqual(1084.58, response_data.volume_usage_m3)
self.assertEqual(59.059, response_data.heat_previous_year_mwh)

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)

if __name__ == '__main__':
unittest.main()
4 changes: 4 additions & 0 deletions ultraheat_api/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""Define constants for ultraheat"""
DEFAULT_BAUDRATE_WAKE_UP = 300
DEFAULT_BAUDRATE_DATA_STREAM = 2400
DEFAULT_TIMEOUT = 60
8 changes: 6 additions & 2 deletions ultraheat_api/file_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
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.
"""
import logging
from typing import Tuple

_LOGGER = logging.getLogger(__name__)

class FileReader:
def __init__(self, file_name) -> None:
Expand All @@ -12,5 +14,7 @@ def __init__(self, file_name) -> None:
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")

data = f.read().decode("utf-8")
_LOGGER.debug("Read from file:\n%s", data)
return model, data
28 changes: 23 additions & 5 deletions ultraheat_api/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,21 @@
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
from dataclasses import dataclass, fields
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},
"heat_usage_kwh": {"regex": r"6.8\((.*?)\*kWh\)", "unit": "kWh", "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},
"heat_previous_year_kwh": {"regex": r"6.8\*01\((.*?)\*kWh\)", "unit": "kWh", "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},
Expand Down Expand Up @@ -54,7 +55,6 @@
}



@dataclass
class HeatMeterResponse:
model: str
Expand Down Expand Up @@ -87,17 +87,30 @@ class HeatMeterResponse:
flow_hours: int
raw_response: str

def __str__(self):
"""Returns a string containing only the non-default field values."""
return ', '.join(f'{field.name}={getattr(self, field.name)!r}'
for field in fields(self)
if getattr(self, field.name) != field.default)

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)
heat_usage_mwh = 0
if self._match("heat_usage_mwh", raw_response):
heat_usage_mwh = self._match("heat_usage_mwh", raw_response)
if self._match("heat_usage_kwh", raw_response):
heat_usage_mwh = kwh_to_mwh(self._match("heat_usage_kwh", 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)
heat_previous_year_mwh = 0
if self._match("heat_previous_year_mwh", raw_response):
heat_previous_year_mwh = self._match("heat_previous_year_mwh", raw_response)
if self._match("heat_previous_year_kwh", raw_response):
heat_previous_year_mwh = kwh_to_mwh(self._match("heat_previous_year_kwh", 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)
Expand Down Expand Up @@ -160,3 +173,8 @@ def _match(self, name, raw_response):
return RESPONSE_CONFIG[name]["type"](str_match.group(1))
except ValueError:
raise


def kwh_to_mwh(kwh: float) -> float:
"""Convert kWh to MWh"""
return kwh / 1000
7 changes: 6 additions & 1 deletion ultraheat_api/service.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
"""
Reads raw Heat Meter data and returns a HeatMeterResponse object
"""
import logging

from ultraheat_api.response import HeatMeterResponse, HeatMeterResponseParser

_LOGGER = logging.getLogger(__name__)

class HeatMeterService:
"""
Expand All @@ -14,4 +17,6 @@ def __init__(self, reader) -> None:

def read(self) -> HeatMeterResponse:
(model, raw_response) = self.reader.read()
return HeatMeterResponseParser().parse(model, raw_response)
parsed_result = HeatMeterResponseParser().parse(model, raw_response)
_LOGGER.debug("parsed result: %s", parsed_result)
return parsed_result
Loading

0 comments on commit c07b666

Please sign in to comment.