diff --git a/.editorconfig b/.editorconfig index d9c54e2..dc806e6 100644 --- a/.editorconfig +++ b/.editorconfig @@ -16,6 +16,3 @@ indent_style = space [*.{yaml,yml,md}] indent_size = 2 - -[.vscode/*.json] -insert_final_newline = false diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0103a8e..c63df7c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ repos: rev: v4.4.0 hooks: - id: trailing-whitespace - #- id: end-of-file-fixer + - id: end-of-file-fixer - id: fix-byte-order-marker - id: fix-encoding-pragma - id: check-executables-have-shebangs @@ -41,9 +41,7 @@ repos: args: - "--install-types" - "--non-interactive" - - "--check-untyped-defs" - additional_dependencies: - - typing_extensions==4.8.0 + - "--ignore-missing-imports" - repo: https://github.com/psf/black rev: 23.3.0 diff --git a/.vscode/settings.json b/.vscode/settings.json index 727a3b0..f9808e9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,14 +1,6 @@ { + "python.linting.mypyEnabled": true, + "python.formatting.provider": "black", "editor.formatOnSave": true, - "pylint.args": [ - "--disable=missing-function-docstring,missing-class-docstring,missing-module-docstring" - ], - "python.testing.unittestEnabled": false, - "python.testing.pytestEnabled": true, - "mypy-type-checker.importStrategy": "fromEnvironment", - "mypy-type-checker.reportingScope": "workspace", - "mypy-type-checker.preferDaemon": true, - "mypy-type-checker.args": [ - "--check-untyped-defs" - ] -} \ No newline at end of file + "python.linting.flake8Enabled": true +} diff --git a/deploy.sh b/deploy.sh index 7640268..dc92d5b 100755 --- a/deploy.sh +++ b/deploy.sh @@ -1,5 +1,2 @@ #!/bin/bash rsync --exclude=.git --exclude='packet format.java' --exclude-from=.gitignore -ravC . pi@solarpi:ble -ssh pi@solarpi './ble-venv/bin/pip install -r ble/requirements.txt' -ssh pi@solarpi 'ln -fs $(pwd)/ble/solarmppt.service ~/.config/systemd/user/solarmppt.service' -ssh pi@solarpi 'loginctl enable-linger; systemctl --user daemon-reload; systemctl --user restart solarmppt' diff --git a/misc/draw_memory_map.py b/misc/draw_memory_map.py index 5aedbbd..37a3513 100644 --- a/misc/draw_memory_map.py +++ b/misc/draw_memory_map.py @@ -99,20 +99,20 @@ def parse_log(fh, chunksize=32): yield None -if __name__ == "__main__": - with open("z_solar copy.log") as fh: - data = list(parse_log(fh)) - # print(data) +with open("z_solar copy.log") as fh: + data = list(parse_log(fh)) + # print(data) - # data = list(range(256)) +# data = list(range(256)) - print( - memory_table( - data, - wordsize=2, - skip_nullrows=True, - ) + +print( + memory_table( + data, + wordsize=2, + skip_nullrows=True, ) +) # diff --git a/misc/dump_memory_map.py b/misc/dump_memory_map.py deleted file mode 100644 index f9cbc2a..0000000 --- a/misc/dump_memory_map.py +++ /dev/null @@ -1,29 +0,0 @@ -# -*- coding: utf-8 -*- -import os -import sys -from typing import List - -sys.path.insert(1, os.path.dirname(os.path.dirname(sys.argv[0]))) - -from draw_memory_map import memory_table # noqa: E402 - -from srnemqtt.config import get_config, get_interface # noqa: E402 -from srnemqtt.protocol import readMemory # noqa: E402 - -if __name__ == "__main__": - conf = get_config() - iface = get_interface(conf) - - data: List[int] = [] - for i in range(0, 0xFFFF, 16): - newdata = readMemory(iface, i, 16) - if newdata: - data.extend(newdata) - # !!! FIXME: Naively assumes all queries return the exact words requested - print( - memory_table( - data, - wordsize=2, - skip_nullrows=True, - ) - ) diff --git a/misc/memory_dump_MT2410.txt b/misc/memory_dump_MT2410.txt deleted file mode 100644 index a329446..0000000 --- a/misc/memory_dump_MT2410.txt +++ /dev/null @@ -1,153 +0,0 @@ -MT2410N10 -1.1.0 -13-19-740 -┌────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐ -│ │ ···0│ ···1│ ···2│ ···3│ ···4│ ···5│ ···6│ ···7│ ···8│ ···9│ ···A│ ···B│ ···C│ ···D│ ···E│ ···F│ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│000·│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│18 0A│0A 00│20 20│20 20│4D 54│32 34│ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ M T│ 2 4│ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│001·│31 30│4E 31│30 20│20 20│00 01│01 00│02 00│00 01│0D 13│02 E4│00 01│00 00│00 00│03 09│14 02│0A 00│ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│ │ 1 0│ N 1│ 0 │ │ │ │ │ │ │ ä│ │ │ │ │ │ │ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│002·│00 02│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│003·│00 00│00 00│00 00│00 00│00 00│00 31│00 32│00 33│00 34│00 35│00 36│00 37│00 38│00 39│00 3A│00 3B│ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│ │ │ │ │ │ │ 1│ 2│ 3│ 4│ 5│ 6│ 7│ 8│ 9│ :│ ;│ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│004·│00 3C│00 3D│00 3E│00 3F│00 40│00 41│00 42│00 43│00 44│00 53│00 6F│00 6C│00 61│00 72│00 20│00 43│ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│ │ <│ =│ >│ ?│ @│ A│ B│ C│ D│ S│ o│ l│ a│ r│ │ C│ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│005·│00 68│00 61│00 72│00 67│00 65│00 72│00 20│00 20│00 20│00 00│00 00│00 00│00 00│00 00│00 00│00 00│ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│ │ h│ a│ r│ g│ e│ r│ │ │ │ │ │ │ │ │ │ │ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│010·│00 64│00 85│00 00│15 19│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 7E│00 86│00 00│00 00│00 00│ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│ │ d│ │ │ │ │ │ │ │ │ │ │ ~│ │ │ │ │ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│011·│00 00│00 00│00 00│00 00│00 00│00 01│00 01│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│DF0·│00 01│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│DF2·│44 44│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│ │ D D│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│DF4·│00 00│00 00│00 00│00 00│00 00│00 00│00 00│44 44│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│ │ │ │ │ │ │ │ │ D D│ │ │ │ │ │ │ │ │ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│E00·│00 00│03 E8│00 C8│FF 0C│00 02│00 A0│00 9B│00 92│00 90│00 8A│00 84│00 7E│00 78│00 6F│00 6A│64 32│ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│ │ │ è│ È│ ÿ │ │ │ │ │ │ │ │ ~│ x│ o│ j│ d 2│ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│E01·│00 05│00 78│00 78│00 1E│00 03│00 41│00 A3│00 4B│00 A3│00 00│00 00│00 00│00 00│00 0F│00 05│00 05│ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│ │ │ x│ x│ │ │ A│ £│ K│ £│ │ │ │ │ │ │ │ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│E02·│00 04│01 00│00 00│00 01│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│E30·│66 66│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│ │ f f│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│E31·│00 00│00 00│00 00│00 64│00 32│00 64│00 32│00 3C│00 05│00 C8│00 02│02 BC│00 0A│03 84│03 84│02 58│ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│ │ │ │ │ d│ 2│ d│ 2│ <│ │ È│ │ ¼│ │ │ │ X│ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│E32·│00 14│00 60│00 00│00 00│00 00│00 00│00 00│00 00│00 01│66 66│00 00│00 00│00 00│00 00│00 00│00 00│ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│ │ │ `│ │ │ │ │ │ │ │ f f│ │ │ │ │ │ │ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│F00·│00 7E│00 86│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│ │ ~│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│F0A·│00 7E│00 86│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│ │ ~│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│F14·│00 7E│00 86│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│ │ ~│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│F1E·│00 7E│00 86│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│ │ ~│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│F28·│00 7E│00 86│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│ │ ~│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│F32·│00 7E│00 86│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│ │ ~│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│F3C·│00 7E│00 86│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│ │ ~│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│F46·│00 7E│00 86│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│ │ ~│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│F50·│00 7E│00 86│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│ │ ~│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│F5A·│00 7E│00 86│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│ │ ~│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│F64·│00 7E│00 86│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│ │ ~│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│F6E·│00 7E│00 86│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│ │ ~│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│F78·│00 7E│00 86│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│ │ ~│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│F82·│00 7E│00 86│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│ │ ~│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│F8C·│00 7E│00 86│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│ │ ~│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│F96·│00 7E│00 86│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│00 00│ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│ │ ~│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ -├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -└────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘ diff --git a/misc/render_rrd.py b/misc/render_rrd.py index aebf051..2ca196e 100644 --- a/misc/render_rrd.py +++ b/misc/render_rrd.py @@ -5,7 +5,7 @@ from ast import literal_eval from collections import namedtuple from typing import Any, Dict -import rrdtool # type: ignore +import rrdtool from srnemqtt.solar_types import DataName @@ -20,7 +20,7 @@ HISTORICAL_KEYS = { DataName.BATTERY_VOLTAGE_MIN, DataName.BATTERY_VOLTAGE_MAX, DataName.CHARGE_MAX_CURRENT, - DataName.DISCHARGE_MAX_CURRENT, + DataName._DISCHARGE_MAX_CURRENT, DataName.CHARGE_MAX_POWER, DataName.DISCHARGE_MAX_POWER, DataName.CHARGE_AMP_HOUR, @@ -58,7 +58,7 @@ KNOWN_KEYS = HISTORICAL_KEYS.union(INSTANT_KEYS) MAP = { "_internal_temperature?": "internal_temp", "unknown1": "charge_max_current", - "unknown2": "discharge_max_current", + "unknown2": "_discharge_max_current?", "internal_temperature": "internal_temp", "battery_temperature": "battery_temp", } @@ -147,6 +147,7 @@ def rrdupdate(file: str, timestamp: int, data: dict): def re_read(): + rrdtool.create( RRDFILE, # "--no-overwrite", diff --git a/misc/test_bleuart.py b/misc/test_bleuart.py index 7e79f1f..1f28414 100644 --- a/misc/test_bleuart.py +++ b/misc/test_bleuart.py @@ -1,12 +1,13 @@ # -*- coding: utf-8 -*- from srnemqtt.constants import MAC from srnemqtt.lib.feasycom_ble import BTLEUart -from srnemqtt.protocol import construct_read_request, write +from srnemqtt.protocol import construct_request, write with BTLEUart(MAC, timeout=1) as x: + print(x) - write(x, construct_read_request(0x0E, words=3)) + write(x, construct_request(0x0E, words=3)) x.read(3, timeout=1) print(x.read(6, timeout=0.01)) x.read(2, timeout=0.01) diff --git a/misc/test_load_switch.py b/misc/test_load_switch.py deleted file mode 100644 index 91a39ee..0000000 --- a/misc/test_load_switch.py +++ /dev/null @@ -1,26 +0,0 @@ -# -*- coding: utf-8 -*- -import os -import sys -from time import sleep - -sys.path.insert(1, os.path.dirname(os.path.dirname(sys.argv[0]))) - -from srnemqtt.config import get_config, get_interface # noqa: E402 -from srnemqtt.protocol import ChargeController # noqa: E402 - -if __name__ == "__main__": - conf = get_config() - iface = get_interface(conf) - cc = ChargeController(iface) - - print(f"Serial: {cc.serial}") - print(f"Load enabled: {cc.load_enabled}") - cc.load_enabled = True - print(f"Load enabled: {cc.load_enabled}") - sleep(5) - cc.load_enabled = False - print(f"Load enabled: {cc.load_enabled}") - - # print(f"Name: {cc.name}") - # cc.name = "☀️ 🔌🔋Charger" - # print(f"Name: {cc.name}") diff --git a/misc/test_serial.py b/misc/test_serial.py index dacf5b8..7ea6505 100644 --- a/misc/test_serial.py +++ b/misc/test_serial.py @@ -3,23 +3,22 @@ import os import sys from time import sleep -from serial import Serial # type: ignore +from serial import Serial print(sys.path) sys.path.insert(1, os.path.dirname(os.path.dirname(sys.argv[0]))) # from srnemqtt.constants import MAC # from srnemqtt.lib.feasycom_ble import BTLEUart -from srnemqtt.protocol import construct_read_request, write # noqa: E402 +from srnemqtt.protocol import construct_request, write # noqa: E402 -# for rate in [1200, 2400, 4800, 9600, 115200]: -for rate in [9600]: +for rate in [1200, 2400, 4800, 9600, 115200]: print(rate) with Serial("/dev/ttyUSB0", baudrate=rate, timeout=2) as x: sleep(2) print(x) - write(x, construct_read_request(0x0E, words=3)) + write(x, construct_request(0x0E, words=3)) print(x.read(3)) print(x.read(6)) print(x.read(2)) diff --git a/misc/test_serial_loopback.py b/misc/test_serial_loopback.py index 590a14f..3351171 100644 --- a/misc/test_serial_loopback.py +++ b/misc/test_serial_loopback.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from serial import Serial # type: ignore +from serial import Serial with Serial("/dev/ttyUSB0", baudrate=9600, timeout=2) as x: x.write(b"Hello, World!") diff --git a/requirements.txt b/requirements.txt index cdc6045..dafbd39 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,6 @@ bluepy libscrc paho-mqtt pyserial -graypy types-PyYAML +types-paho-mqtt diff --git a/solarmppt.service b/solarmppt.service deleted file mode 100644 index bdc48a8..0000000 --- a/solarmppt.service +++ /dev/null @@ -1,14 +0,0 @@ -[Unit] -Description=Daemon for bridging a Serial SRNE MPPT charge controller to MQTT - -[Service] -Type=exec -Restart=on-failure -#StandardOutput=append:/home/pi/z_solar_systemd.log -#StandardError=append:/home/pi/z_solar_systemd_err.log -WorkingDirectory=/home/pi/ -Environment=PYTHONPATH=/home/pi/ble/ -ExecStart=/home/pi/ble-venv/bin/python -m srnemqtt - -[Install] -WantedBy=default.target diff --git a/srnemqtt/__main__.py b/srnemqtt/__main__.py index 1fed6c5..f13b40a 100755 --- a/srnemqtt/__main__.py +++ b/srnemqtt/__main__.py @@ -2,33 +2,25 @@ # -*- coding: utf-8 -*- import time -from logging import getLogger -from logging import root as logging_root -from logging.config import dictConfig as loggingDictConfig +from typing import List, Optional, cast -from bluepy.btle import BTLEDisconnectError # type: ignore -from serial import SerialException # type: ignore +from bluepy.btle import BTLEDisconnectError +from serial import SerialException + +from srnemqtt.consumers import BaseConsumer from .config import get_config, get_consumers, get_interface -from .protocol import ChargeController -from .util import LazyJSON, LoggingDictFilter, Periodical - -logger = getLogger("SolarMPPT") +from .srne import Srne +from .util import Periodical, log -class CommunicationError(BTLEDisconnectError, SerialException, IOError): +class CommunicationError(BTLEDisconnectError, SerialException, TimeoutError): pass -def main(): +def main() -> None: conf = get_config() - - loggingDictConfig(conf.get("logging", {})) - logging_dict_filter = LoggingDictFilter() - logging_dict_filter.data["service"] = "SolarMPPT" - logging_root.addFilter(logging_dict_filter) - - consumers = get_consumers(conf) + consumers: Optional[List[BaseConsumer]] = None per_voltages = Periodical(interval=15) per_current_hist = Periodical(interval=60) @@ -36,66 +28,53 @@ def main(): try: while True: try: - logger.info("Connecting...") + log("Connecting...") with get_interface() as dev: - cc = ChargeController(dev) - logging_dict_filter.data["srne_model"] = cc.model - logging_dict_filter.data["srne_version"] = cc.version - logging_dict_filter.data["srne_serial"] = cc.serial + srne = Srne(dev) + log("Connected.") - logger.info("Connected.") + if consumers is None: + consumers = get_consumers(srne, conf) - logger.info(f"Controller model: {cc.model}") - logger.info(f"Controller version: {cc.version}") - logger.info(f"Controller serial: {cc.serial}") - for consumer in consumers: - consumer.controller = cc - - # write(dev, construct_request(0, 32)) - - # Memory dump - # for address in range(0, 0x10000, 16): - # log(f"Reading 0x{address:04X}...") - # write(wd, construct_request(address, 16)) - extra = cc.extra - days = extra.run_days - - res = cc.today.as_dict() - res.update(extra.as_dict()) - for consumer in consumers: - consumer.write(res) - del extra - - # Historical data isn't actually used anywhere yet - # Limit to 4 days for now - for i in range(min(days, 4)): - hist = cc.get_historical(i) - res = hist.as_dict() - logger.debug(LazyJSON({i: res})) + days = 7 + res = srne.get_historical_entry() + if res: + log(res) for consumer in consumers: - consumer.write({str(i): res}) + consumer.write(res) + days = cast(int, res.get("run_days", 7)) + + for i in range(days): + res = srne.get_historical_entry(i) + if res: + log({i: res}) + for consumer in consumers: + consumer.write({str(i): res}) while True: now = time.time() if per_voltages(now): - data = cc.state.as_dict() - logger.debug(LazyJSON(data)) - for consumer in consumers: - consumer.write(data) + data = srne.get_battery_state() + if data: + log(data) + for consumer in consumers: + consumer.write(data) if per_current_hist(now): - data = cc.today.as_dict() - data.update(cc.extra.as_dict()) - logger.debug(LazyJSON(data)) - for consumer in consumers: - consumer.write(data) + try: + data = srne.get_historical_entry() + log(data) + for consumer in consumers: + consumer.write(data) + except TimeoutError: + pass # print(".") for consumer in consumers: consumer.poll() - time.sleep(max(0, 1 - (time.time() - now))) + time.sleep(max(0, 1 - time.time() - now)) # 1s loop # if STATUS.get('load_enabled'): # write(wd, CMD_DISABLE_LOAD) @@ -103,12 +82,13 @@ def main(): # write(wd, CMD_ENABLE_LOAD) except CommunicationError: - logger.error("Disconnected") + log("ERROR: Disconnected") time.sleep(1) except (KeyboardInterrupt, SystemExit, Exception) as e: - for consumer in consumers: - consumer.exit() + if consumers is not None: + for consumer in consumers: + consumer.exit() if type(e) is not KeyboardInterrupt: raise diff --git a/srnemqtt/config.py b/srnemqtt/config.py index b82357b..301f2f7 100644 --- a/srnemqtt/config.py +++ b/srnemqtt/config.py @@ -8,6 +8,7 @@ import yaml from .consumers import BaseConsumer from .interfaces import BaseInterface +from .srne import Srne def get_consumer(name: str) -> Optional[Type[BaseConsumer]]: @@ -27,29 +28,6 @@ def get_config() -> Dict[str, Any]: with open("config.yaml", "r") as fh: conf: dict = yaml.safe_load(fh) conf.setdefault("consumers", {}) - logging = conf.setdefault("logging", {}) - logging.setdefault("version", 1) - logging.setdefault("disable_existing_loggers", False) - logging.setdefault( - "handlers", - { - "console": { - "class": "logging.StreamHandler", - "formatter": "default", - "level": "INFO", - "stream": "ext://sys.stdout", - } - }, - ) - logging.setdefault( - "formatters", - { - "format": "%(asctime)s %(levelname)-8s %(name)-15s %(message)s", - "datefmt": "%Y-%m-%d %H:%M:%S", - }, - ) - loggers = logging.setdefault("loggers", {}) - loggers.setdefault("root", {"handlers": ["console"], "level": "DEBUG"}) return conf @@ -57,12 +35,12 @@ def get_config() -> Dict[str, Any]: def write_config(conf: Dict[str, Any]): with open(".config.yaml~writing", "w") as fh: yaml.safe_dump(conf, fh, indent=2, encoding="utf-8") - fh.flush() - os.fsync(fh.fileno()) - os.replace(".config.yaml~writing", "config.yaml") + os.rename(".config.yaml~writing", "config.yaml") -def get_consumers(conf: Optional[Dict[str, Any]] = None) -> List[BaseConsumer]: +def get_consumers( + srne: Srne, conf: Optional[Dict[str, Any]] = None +) -> List[BaseConsumer]: if conf is None: conf = get_config() @@ -72,7 +50,7 @@ def get_consumers(conf: Optional[Dict[str, Any]] = None) -> List[BaseConsumer]: mod = get_consumer(name) if mod: # print(mod) - consumers.append(mod(consumer_config)) + consumers.append(mod(settings=consumer_config, srne=srne)) write_config(conf) return consumers @@ -105,7 +83,7 @@ def get_interface(conf: Optional[Dict[str, Any]] = None) -> BaseInterface: if __name__ == "__main__": conf = get_config() - consumers = get_consumers(conf) + consumers = get_consumers(Srne(BaseInterface()), conf) try: while True: diff --git a/srnemqtt/consumers/__init__.py b/srnemqtt/consumers/__init__.py index bd21596..bc12ed4 100644 --- a/srnemqtt/consumers/__init__.py +++ b/srnemqtt/consumers/__init__.py @@ -2,15 +2,16 @@ from abc import ABC, abstractmethod from typing import Any, Dict -from ..protocol import ChargeController +from ..srne import Srne class BaseConsumer(ABC): settings: Dict[str, Any] - controller: ChargeController | None = None + srne: Srne @abstractmethod - def __init__(self, settings: Dict[str, Any]) -> None: + def __init__(self, settings: Dict[str, Any], srne: Srne) -> None: + self.srne = srne self.config(settings) @abstractmethod diff --git a/srnemqtt/consumers/mqtt.py b/srnemqtt/consumers/mqtt.py index 7a29e1c..772600a 100644 --- a/srnemqtt/consumers/mqtt.py +++ b/srnemqtt/consumers/mqtt.py @@ -1,22 +1,20 @@ # -*- coding: utf-8 -*- import json -from logging import getLogger from time import sleep -from typing import Any, Dict, List, Optional, TypeAlias +from typing import Any, Dict, List, Optional from uuid import uuid4 import paho.mqtt.client as mqtt from ..solar_types import DataName +from ..srne import Srne from . import BaseConsumer -logger = getLogger(__name__) - MAP_VALUES: Dict[DataName, Dict[str, Any]] = { # DataName.BATTERY_VOLTAGE_MIN: {}, # DataName.BATTERY_VOLTAGE_MAX: {}, # DataName.CHARGE_MAX_CURRENT: {}, - # DataName.DISCHARGE_MAX_CURRENT: {}, + # DataName._DISCHARGE_MAX_CURRENT: {}, # DataName.CHARGE_MAX_POWER: {}, # DataName.DISCHARGE_MAX_POWER: {}, # DataName.CHARGE_AMP_HOUR: {}, @@ -84,7 +82,11 @@ MAP_VALUES: Dict[DataName, Dict[str, Any]] = { "type": "current", "state_class": "measurement", }, - DataName.LOAD_POWER: {"unit": "W", "type": "power", "state_class": "measurement"}, + DataName.LOAD_POWER: { + "unit": "W", + "type": "power", + "state_class": "measurement", + }, DataName.LOAD_ENABLED: { "type": "outlet", "platform": "switch", @@ -119,41 +121,30 @@ MAP_VALUES: Dict[DataName, Dict[str, Any]] = { } -PayloadType: TypeAlias = str | bytes | bytearray | int | float | None - - class MqttConsumer(BaseConsumer): + client: mqtt.Client initialized: List[str] + srne: Srne - _client: mqtt.Client | None = None - - def __init__(self, settings: Dict[str, Any]) -> None: + def __init__(self, settings: Dict[str, Any], srne: Srne) -> None: self.initialized = [] - super().__init__(settings) - - @property - def client(self) -> mqtt.Client: - if self._client is not None: - return self._client - - self._client = mqtt.Client( - client_id=self.settings["client"]["id"], userdata=self - ) - self._client.on_connect = self.on_connect - self._client.on_message = self.on_message - self._client.on_disconnect = self.on_disconnect - self._client.on_connect_fail = self.on_connect_fail + super().__init__(settings, srne) + self.client = mqtt.Client(client_id=settings["client"]["id"], userdata=self) + self.client.on_connect = self.on_connect + self.client.on_message = self.on_message + self.client.on_disconnect = self.on_disconnect + self.client.on_connect_fail = self.on_connect_fail # Will must be set before connecting!! - self._client.will_set( + self.client.will_set( f"{self.topic_prefix}/available", payload="offline", retain=True ) while True: try: - self._client.connect( - self.settings["client"]["host"], - self.settings["client"]["port"], - self.settings["client"]["keepalive"], + self.client.connect( + settings["client"]["host"], + settings["client"]["port"], + settings["client"]["keepalive"], ) break except OSError as err: @@ -164,13 +155,9 @@ class MqttConsumer(BaseConsumer): elif err.errno == -3: pass else: - logger.exception("Unknown error connecting to mqtt server") raise - logger.warning( - "Temporary failure connecting to mqtt server", exc_info=True - ) + print(err) sleep(0.1) - return self._client def config(self, settings: Dict[str, Any]): super().config(settings) @@ -187,24 +174,14 @@ class MqttConsumer(BaseConsumer): settings.setdefault("discovery_prefix", "homeassistant") - _controller_id: str | None = None - - @property - def controller_id(self) -> str: - assert self.controller is not None - # Controller serial is fetched from device, cache it. - if self._controller_id is None: - self._controller_id = self.controller.serial - return f"{self.controller.manufacturer_id}_{self._controller_id}" - @property def topic_prefix(self): - return f"{self.settings['prefix']}/{self.controller_id}" + return f"{self.settings['prefix']}/{self.srne.serial}" def get_ha_config( self, - id: str, - name: str, + id, + name, unit: Optional[str] = None, type: Optional[str] = None, expiry: int = 90, @@ -212,25 +189,27 @@ class MqttConsumer(BaseConsumer): platform: str = "sensor", ): assert state_class in [None, "measurement", "total", "total_increasing"] - assert self.controller is not None res = { "~": f"{self.topic_prefix}", - "unique_id": f"{self.controller_id}_{id}", - "object_id": f"{self.controller_id}_{id}", # Used for entity id + "unique_id": f"srne_{self.srne.serial}_{id}", + "object_id": f"srne_{self.srne.serial}_{id}", # Used for entity id "availability_topic": "~/available", "state_topic": f"~/{id}", "name": name, "device": { "identifiers": [ - self.controller_id, + self.srne.serial, ], - "manufacturer": self.controller.manufacturer, - "model": self.controller.model, - "sw_version": self.controller.version, - "via_device": self.settings["device_id"], + # TODO: Get charger serial and use for identifier instead + # See: https://www.home-assistant.io/integrations/sensor.mqtt/#device + # "via_device": self.settings["device_id"], "suggested_area": "Solar panel", - "name": self.controller.name, + "manufacturer": "SRNE Solar", + "model": self.srne.model, + "name": self.srne.name, + "sw_version": self.srne.version, + "via_device": self.settings["device_id"], }, "force_update": True, "expire_after": expiry, @@ -246,12 +225,13 @@ class MqttConsumer(BaseConsumer): res["command_topic"] = f"{res['state_topic']}/set" res["payload_on"] = True res["payload_off"] = False + return res # The callback for when the client receives a CONNACK response from the server. @staticmethod def on_connect(client: mqtt.Client, userdata: "MqttConsumer", flags, rc): - logger.info("MQTT connected with result code %s", rc) + print("Connected with result code " + str(rc)) # Subscribing in on_connect() means that if we lose the connection and # reconnect then subscriptions will be renewed. @@ -268,66 +248,65 @@ class MqttConsumer(BaseConsumer): def on_load_switch( client: mqtt.Client, userdata: "MqttConsumer", message: mqtt.MQTTMessage ): - assert userdata.controller is not None - logger.debug(message.payload) + print(message) + print(message.info) + print(message.state) + print(message.payload) payload = message.payload.decode().upper() in ("ON", "TRUE", "ENABLE", "YES") - - res = userdata.controller.load_enabled = payload - client.publish( - f"{userdata.topic_prefix}/load_enabled", payload=res, retain=True - ) + if type(payload) is bool: + res = userdata.srne.enable_load(payload) + client.publish( + f"{userdata.topic_prefix}/load_enabled", payload=res, retain=True + ) + else: + print(f"!!! Unknown payload for switch callback: {message.payload!r}") @staticmethod def on_connect_fail(client: mqtt.Client, userdata: "MqttConsumer"): - logger.warning("on_connect_fail") + print(userdata.__class__.__name__, "on_connect_fail") # The callback for when a PUBLISH message is received from the server. @staticmethod def on_message(client, userdata, msg): - logger.info(msg.topic + " " + str(msg.payload)) + print(msg.topic + " " + str(msg.payload)) @staticmethod def on_disconnect(client: mqtt.Client, userdata: "MqttConsumer", rc, prop=None): - logger.warning("on_disconnect %s", rc) + print(userdata.__class__.__name__, "on_disconnect", rc) def poll(self): res = self.client.loop(timeout=0.1, max_packets=5) if res != mqtt.MQTT_ERR_SUCCESS: - logger.warning("loop returned non-success: %s", res) + print(self.__class__.__name__, "loop returned non-success:", res) try: sleep(1) res = self.client.reconnect() if res != mqtt.MQTT_ERR_SUCCESS: - logger.error("Reconnect failed: %s", res) + print(self.__class__.__name__, "Reconnect failed:", res) except (OSError, mqtt.WebsocketConnectionError) as err: - logger.error("Reconnect failed: %s", err) + print(self.__class__.__name__, "Reconnect failed:", err) return super().poll() - def write(self, data: Dict[str, PayloadType]): + def write(self, data: Dict[str, Any]): self.client.publish(f"{self.topic_prefix}/raw", payload=json.dumps(data)) - for dataname, data_value in data.items(): - if dataname in MAP_VALUES: - if dataname not in self.initialized: - km = MAP_VALUES[DataName(dataname)] - pretty_name = dataname.replace("_", " ").capitalize() + for k, v in data.items(): + if k in MAP_VALUES: + if k not in self.initialized: + km = MAP_VALUES[DataName(k)] + pretty_name = k.replace("_", " ").capitalize() disc_prefix = self.settings["discovery_prefix"] platform = km.get("platform", "sensor") - self.client.publish( - f"{disc_prefix}/{platform}/{self.controller_id}/{dataname}/config", - payload=json.dumps( - self.get_ha_config(dataname, pretty_name, **km) - ), + f"{disc_prefix}/{platform}/srne_{self.srne.serial}_{k}/config", + payload=json.dumps(self.get_ha_config(k, pretty_name, **km)), retain=True, ) - self.initialized.append(dataname) + self.initialized.append(k) - self.client.publish( - f"{self.topic_prefix}/{dataname}", data_value, retain=True - ) + self.client.publish(f"{self.topic_prefix}/{k}", v, retain=True) def exit(self): self.client.publish( diff --git a/srnemqtt/consumers/stdio.py b/srnemqtt/consumers/stdio.py index df63e70..bf5a8e2 100644 --- a/srnemqtt/consumers/stdio.py +++ b/srnemqtt/consumers/stdio.py @@ -2,12 +2,13 @@ import json from typing import Any, Dict +from ..srne import Srne from . import BaseConsumer class StdoutConsumer(BaseConsumer): - def __init__(self, settings: Dict[str, Any]) -> None: - super().__init__(settings) + def __init__(self, settings: Dict[str, Any], srne: Srne) -> None: + super().__init__(settings, srne) def poll(self): return super().poll() diff --git a/srnemqtt/interfaces/__init__.py b/srnemqtt/interfaces/__init__.py index 5b3bdbd..e8ecc37 100644 --- a/srnemqtt/interfaces/__init__.py +++ b/srnemqtt/interfaces/__init__.py @@ -4,4 +4,4 @@ from io import RawIOBase class BaseInterface(RawIOBase, metaclass=ABCMeta): - timeout: float | None + pass diff --git a/srnemqtt/interfaces/serial.py b/srnemqtt/interfaces/serial.py index 82af005..bee3ff6 100644 --- a/srnemqtt/interfaces/serial.py +++ b/srnemqtt/interfaces/serial.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -import serial # type: ignore +import serial from . import BaseInterface diff --git a/srnemqtt/lib/feasycom_ble.py b/srnemqtt/lib/feasycom_ble.py index 624093d..2f7f262 100644 --- a/srnemqtt/lib/feasycom_ble.py +++ b/srnemqtt/lib/feasycom_ble.py @@ -4,7 +4,11 @@ import queue import time from typing import TYPE_CHECKING, Optional, cast -from bluepy import btle # type: ignore +from bluepy import btle + +if TYPE_CHECKING: + from _typeshed import ReadableBuffer, WriteableBuffer + WRITE_DEVICE = "0000ffd1-0000-1000-8000-00805f9b34fb" READ_DEVICE = "0000fff1-0000-1000-8000-00805f9b34fb" @@ -14,7 +18,7 @@ class BTLEUart(io.RawIOBase): mac: str write_endpoint: str read_endpoint: str - timeout: float | None + timeout: float device: Optional[btle.Peripheral] = None _write_handle: Optional[btle.Characteristic] = None @@ -82,12 +86,13 @@ class BTLEUart(io.RawIOBase): self._write_handle = self.device.getCharacteristics(uuid=self.write_endpoint)[0] # print("Handles:", self._read_handle.handle, self._write_handle.handle) - def _read(self, num: Optional[int] = None): + def _read(self, num: Optional[int] = None, timeout: Optional[float] = None): self._ensure_connected() if TYPE_CHECKING: self.device = cast(btle.Peripheral, self.device) - timeout = self.timeout or 30 + if timeout is None: + timeout = self.timeout if num is None: start = time.time() @@ -127,9 +132,7 @@ class BTLEUart(io.RawIOBase): del self._read_buffer[:num] return data or None - def readinto(self, buffer: bytearray | memoryview) -> Optional[int]: # type: ignore [override] - # Buffer does not provide Sized, and bytes is read only. - # bytearray | memoryview is the default implementations that provide WriteableBuffer + def readinto(self, buffer: "WriteableBuffer") -> Optional[int]: data = self._read(len(buffer)) if data is None: @@ -141,15 +144,23 @@ class BTLEUart(io.RawIOBase): def readall(self) -> bytes: return self._read() - def read(self, size: Optional[int] = None) -> Optional[bytes]: + def read( + self, size: Optional[int] = None, timeout: Optional[float] = None + ) -> Optional[bytes]: + if timeout: + _timeout = self.timeout + self.timeout = timeout + if size is None: res = super().read() else: res = super().read(size) + if timeout: + self.timeout = _timeout return res - def write(self, b: bytes | bytearray | memoryview) -> Optional[int]: # type: ignore [override] + def write(self, b: "ReadableBuffer") -> Optional[int]: self._ensure_connected() if TYPE_CHECKING: self.device = cast(btle.Peripheral, self.device) @@ -163,9 +174,8 @@ class BTLEUart(io.RawIOBase): return self def __exit__(self, type, value, traceback): - if self.device is not None: - self.device.disconnect() - self.device = None + self.device.disconnect() + del self.device def seekable(self) -> bool: return False diff --git a/srnemqtt/protocol.py b/srnemqtt/protocol.py index 5b66ea6..11dcd73 100644 --- a/srnemqtt/protocol.py +++ b/srnemqtt/protocol.py @@ -1,23 +1,16 @@ # -*- coding: utf-8 -*- import struct +import sys import time -from logging import getLogger -from typing import Callable, Collection, List, Optional +from io import RawIOBase +from typing import Callable, Collection, Optional -from libscrc import modbus # type: ignore +from libscrc import modbus from .constants import ACTION_READ, ACTION_WRITE, POSSIBLE_MARKER from .interfaces import BaseInterface -from .solar_types import ( - DATA_BATTERY_STATE, - HISTORICAL_DATA, - ChargerState, - DataItem, - HistoricalData, - HistoricalExtraInfo, -) - -logger = getLogger(__name__) +from .solar_types import DATA_BATTERY_STATE, HISTORICAL_DATA, DataItem +from .util import log def write(fh, data): @@ -27,14 +20,14 @@ def write(fh, data): fh.write(data + bcrc) -def construct_read_request(address, words=1, marker=0xFF): +def construct_request(address, words=1, action=ACTION_READ, marker=0xFF): assert marker in POSSIBLE_MARKER, f"marker should be one of {POSSIBLE_MARKER}" - return struct.pack("!BBHH", marker, ACTION_READ, address, words) + return struct.pack("!BBHH", marker, action, address, words) -def construct_write_request(address, marker=0xFF): +def construct_write(address, data: bytes, action=ACTION_WRITE, marker=0xFF): assert marker in POSSIBLE_MARKER, f"marker should be one of {POSSIBLE_MARKER}" - return struct.pack("!BBH", marker, ACTION_WRITE, address) + return struct.pack("!BBH", marker, action, address) + data def parse(data: bytes, items: Collection[DataItem], offset: int = 0) -> dict: @@ -73,33 +66,35 @@ def parse_packet(data): if crc != calculated_crc: e = ValueError(f"CRC missmatch: expected {crc:04X}, got {calculated_crc:04X}.") - # e.tag = tag - # e.operation = operation - # e.size = size - # e.payload = payload - # e.crc = crc - # e.calculated_crc = calculated_crc + e.tag = tag + e.operation = operation + e.size = size + e.payload = payload + e.crc = crc + e.calculated_crc = calculated_crc raise e return payload -def discardUntil(fh: BaseInterface, byte: int, timeout=10) -> Optional[int]: +def discardUntil(fh: RawIOBase, byte: int, timeout=10) -> Optional[int]: assert byte >= 0 and byte < 256, f"byte: Expected 8bit unsigned int, got {byte}" def expand(b: Optional[bytes]): - if not b: - return None + if b is None: + return b return b[0] start = time.time() - discarded: List[str] = [] + discarded = 0 read_byte = expand(fh.read(1)) while read_byte != byte: if read_byte is not None: if not discarded: - discarded.append("Discarding") - discarded.append(f"{read_byte:02X}") + log("Discarding", end="") + discarded += 1 + print(f" {read_byte:02X}", end="") + sys.stdout.flush() if time.time() - start > timeout: read_byte = None @@ -108,14 +103,15 @@ def discardUntil(fh: BaseInterface, byte: int, timeout=10) -> Optional[int]: read_byte = expand(fh.read(1)) if discarded: - logger.debug(" ".join(discarded)) + print() + sys.stdout.flush() return read_byte def readMemory(fh: BaseInterface, address: int, words: int = 1) -> Optional[bytes]: # log(f"Reading {words} words from 0x{address:04X}") - request = construct_read_request(address, words=words) + request = construct_request(address, words=words) # log("Request:", request) write(fh, request) @@ -132,79 +128,51 @@ def readMemory(fh: BaseInterface, address: int, words: int = 1) -> Optional[byte try: crc = struct.unpack_from("<H", _crc)[0] except struct.error: - logger.error( - "readMemory: CRC error; read %s bytes (2 expected)", len(_crc) - ) + log(f"readMemory: CRC error; read {len(_crc)} bytes (2 expected)") return None calculated_crc = modbus(bytes([tag, operation, size, *data])) if crc == calculated_crc: return data else: - logger.error( - f"readMemory: CRC error; {crc:04X} != {calculated_crc:04X}" - ) - logger.error("data or crc is falsely %s %s %s", header, data, _crc) + log(f"readMemory: CRC error; {crc:04X} != {calculated_crc:04X}") + log("data or crc is falsely", header, data, _crc) return None -# set(255, 266, 1 or 0) -# ff 06 01 0a 00 01 -# CMD_ENABLE_LOAD = b"\xff\x06\x01\x0a\x00\x01" -# CMD_DISABLE_LOAD = b"\xff\x06\x01\x0a\x00\x00" -# REG_LOAD_ENABLE = 0x010A - - -def writeMemory(fh: BaseInterface, address: int, data: bytes): - if len(data) != 2: - raise ValueError(f"Data must consist of a two-byte word, got {len(data)} bytes") - - header = construct_write_request(address) - write(fh, header + data) +def writeMemory(fh: BaseInterface, address: int, output_data: bytes) -> Optional[bytes]: + # TODO: Verify behavior on multi-word writes + # log(f"Reading {words} words from 0x{address:04X}") + request = construct_write(address, data=output_data) + # log("Request:", request) + write(fh, request) tag = discardUntil(fh, 0xFF) if tag is None: return None - header = fh.read(3) - if header and len(header) == 3: - operation, size, address = header - logger.log(5, header) - # size field is zero when writing device name for whatever reason - # write command seems to only accept a single word, so this is fine; - # we just hardcode the number of bytes read to two here. - rdata = fh.read(2) + _operation = fh.read(1) + result_addr = fh.read(2) + # log("Operation:", _operation) + if _operation is not None and result_addr is not None: + operation = _operation[0] + data = fh.read(2) + # log("Data:", data) _crc = fh.read(2) - if rdata and _crc: + if data and _crc: try: crc = struct.unpack_from("<H", _crc)[0] except struct.error: - logger.error( - f"writeMemory: CRC error; read {len(_crc)} bytes (2 expected)" - ) + log(f"readMemory: CRC error; read {len(_crc)} bytes (2 expected)") return None - calculated_crc = modbus(bytes([tag, operation, size, address, *rdata])) + calculated_crc = modbus(bytes([tag, operation, *result_addr, *data])) if crc == calculated_crc: - return rdata + return data else: - logger.error( - f"writeMemory: CRC error; {crc:04X} != {calculated_crc:04X}" - ) - logger.error("data or crc is falsely %s %s %s", header, rdata, _crc) + log(f"readMemory: CRC error; {crc:04X} != {calculated_crc:04X}") + log("data or crc is falsely", operation, result_addr, data, _crc) return None -def writeMemoryMultiple(fh: BaseInterface, address: int, data: bytes): - if len(data) % 2: - raise ValueError(f"Data must consist of two-byte words, got {len(data)} bytes") - res = bytearray() - for i in range(len(data) // 2): - d = data[i * 2 : (i + 1) * 2] - r = writeMemory(fh, address + i, d) - if r: - res.extend(r) - return res - - def try_read_parse( dev: BaseInterface, address: int, @@ -219,156 +187,10 @@ def try_read_parse( try: if parser: return parser(res) - except struct.error: - logger.exception("0x0100 Unpack error: %s %s", len(res), res) - _timeout = dev.timeout - dev.timeout = 0.5 - logger.warning("Flushed from read buffer; %s", dev.read()) - dev.timeout = _timeout + except struct.error as e: + log(e) + log("0x0100 Unpack error:", len(res), res) + log("Flushed from read buffer; ", dev.read()) # TODO: timeout=0.5 else: - logger.warning( - f"No data read, expected {words*2} bytes (attempts left: {attempts})" - ) + log(f"No data read, expected {words*2} bytes (attempts left: {attempts})") return None - - -class ChargeController: - device: BaseInterface - - manufacturer: str = "SRNE Solar Co., Ltd." - manufacturer_id: str = "srne" - - def __init__(self, device: BaseInterface): - self.device = device - - _cached_serial: str | None = None - - @property - def serial(self) -> str: - if self._cached_serial is not None: - return self._cached_serial - - data = readMemory(self.device, 0x18, 3) - if data is None: - raise IOError # FIXME: Raise specific error in readMemory - - p1 = data[0] - p2 = data[1] - p3 = (data[2] << 8) + data[3] - - self._cached_serial = f"{p1:02n}-{p2:02n}-{p3:04n}" - return self._cached_serial - - _cached_model: str | None = None - - @property - def model(self) -> str: - if self._cached_model is not None: - return self._cached_model - - data = readMemory(self.device, 0x0C, 8) - if data is None: - raise IOError # FIXME: Raise specific error in readMemory - - self._cached_model = data.decode("utf-8").strip() - return self._cached_model - - _cached_version: str | None = None - - @property - def version(self) -> str: - if self._cached_version is not None: - return self._cached_version - - data = readMemory(self.device, 0x14, 4) - if data is None: - raise IOError # FIXME: Raise specific error in readMemory - - major = (data[0] << 8) + data[1] - minor = data[2] - patch = data[3] - - self._cached_version = f"{major}.{minor}.{patch}" - return self._cached_version - - _cached_name: str | None = None - - @property - def name(self) -> str: - if self._cached_name is not None: - return self._cached_name - data = readMemory(self.device, 0x0049, 16) - if data is None: - raise IOError - res = data.decode("UTF-16BE").strip() - return res - - @name.setter - def name(self, value: str): - bin_value = bytearray(value.encode("UTF-16BE")) - if len(bin_value) > 32: - raise ValueError( - f"value must be no more than 32 bytes once encoded as UTF-16BE. {len(bin_value)} bytes supplied" - ) - - # Pad name to 32 bytes to ensure ensure nothing is left of old name - while len(bin_value) < 32: - bin_value.extend(b"\x00\x20") - - data = writeMemoryMultiple(self.device, 0x0049, bin_value) - if data is None: - raise IOError # FIXME: Raise specific error in readMemory - - res = data.decode("UTF-16BE").strip() - if res != value: - logger.error("setting device name failed; %r != %r", res, value) - self._cached_name = value - - @property - def load_enabled(self) -> bool: - data = readMemory(self.device, 0x010A, 1) - if data is None: - raise IOError # FIXME: Raise specific error in readMemory - - return struct.unpack("x?", data)[0] - - @load_enabled.setter - def load_enabled(self, value: bool): - data = writeMemory(self.device, 0x010A, struct.pack("x?", value)) - if data is not None: - res = struct.unpack("x?", data)[0] - if res != value: - logger.error("setting load_enabled failed; %r != %r", res, value) - else: - logger.error("setting load_enabled failed; communications error") - - @property - def state(self) -> ChargerState: - data = readMemory(self.device, 0x0100, 11) - if data is None: - raise IOError # FIXME: Raise specific error in readMemory - - return ChargerState(data) - - def get_historical(self, day) -> HistoricalData: - data = readMemory(self.device, 0xF000 + day, 10) - if data is None: - raise IOError # FIXME: Raise specific error in readMemory - - return HistoricalData(data) - - @property - def today(self) -> HistoricalData: - data = readMemory(self.device, 0x010B, 10) - if data is None: - raise IOError # FIXME: Raise specific error in readMemory - - return HistoricalData(data) - - @property - def extra(self) -> HistoricalExtraInfo: - data = readMemory(self.device, 0x0115, 11) - if data is None: - raise IOError # FIXME: Raise specific error in readMemory - - return HistoricalExtraInfo(data) diff --git a/srnemqtt/solar_types.py b/srnemqtt/solar_types.py index 94c387f..a4833ae 100644 --- a/srnemqtt/solar_types.py +++ b/srnemqtt/solar_types.py @@ -1,8 +1,7 @@ # -*- coding: utf-8 -*- import struct -from abc import ABC, abstractmethod from enum import Enum, unique -from typing import Any, Callable, Dict, Optional +from typing import Callable, Optional @unique @@ -22,7 +21,7 @@ class DataName(str, Enum): BATTERY_VOLTAGE_MIN = "battery_voltage_min" BATTERY_VOLTAGE_MAX = "battery_voltage_max" CHARGE_MAX_CURRENT = "charge_max_current" - DISCHARGE_MAX_CURRENT = "discharge_max_current" + _DISCHARGE_MAX_CURRENT = "_discharge_max_current?" CHARGE_MAX_POWER = "charge_max_power" DISCHARGE_MAX_POWER = "discharge_max_power" CHARGE_AMP_HOUR = "charge_amp_hour" @@ -45,7 +44,7 @@ class DataName(str, Enum): return repr(self.value) def __str__(self): - return self.value + return str(self.value) class DataItem: @@ -105,14 +104,13 @@ HISTORICAL_DATA = [ DataItem(DataName.BATTERY_VOLTAGE_MIN, "H", "V", lambda n: n / 10), DataItem(DataName.BATTERY_VOLTAGE_MAX, "H", "V", lambda n: n / 10), DataItem(DataName.CHARGE_MAX_CURRENT, "H", "A", lambda n: n / 100), - DataItem(DataName.DISCHARGE_MAX_CURRENT, "H", "A", lambda n: n / 100), + DataItem(DataName._DISCHARGE_MAX_CURRENT, "H", "A", lambda n: n / 100), DataItem(DataName.CHARGE_MAX_POWER, "H", "W"), DataItem(DataName.DISCHARGE_MAX_POWER, "H", "W"), DataItem(DataName.CHARGE_AMP_HOUR, "H", "Ah"), DataItem(DataName.DISCHARGE_AMP_HOUR, "H", "Ah"), DataItem(DataName.PRODUCTION_ENERGY, "H", "Wh"), DataItem(DataName.CONSUMPTION_ENERGY, "H", "Wh"), - # DataItem(DataName.RUN_DAYS, "H"), DataItem(DataName.DISCHARGE_COUNT, "H"), DataItem(DataName.FULL_CHARGE_COUNT, "H"), @@ -121,180 +119,3 @@ HISTORICAL_DATA = [ DataItem(DataName.TOTAL_PRODUCTION_ENERGY, "L", "Wh"), DataItem(DataName.TOTAL_CONSUMPTION_ENERGY, "L", "Wh"), ] - - -class DecodedData(ABC): - @abstractmethod - def __init__(self, data: bytes | bytearray | memoryview) -> None: - ... - - @abstractmethod - def as_dict(self) -> Dict[DataName, Any]: - ... - - -class ChargerState(DecodedData): - battery_charge: int - battery_voltage: float - battery_current: float - internal_temperature: int - battery_temperature: int - load_voltage: float - load_current: float - load_power: float - panel_voltage: float - panel_current: float - panel_power: float - load_enabled: bool - - def __init__(self, data: bytes | bytearray | memoryview) -> None: - ( - _battery_charge, - _battery_voltage, - _battery_current, - _internal_temperature, - _battery_temperature, - _load_voltage, - _load_current, - _load_power, - _panel_voltage, - _panel_current, - _panel_power, - _load_enabled, - ) = struct.unpack("!HHHBBHHHHHHx?", data) - - self.battery_charge = _battery_charge - self.battery_voltage = _battery_voltage / 10 - self.battery_current = _battery_current / 100 - self.internal_temperature = parse_temperature(_internal_temperature) - self.battery_temperature = parse_temperature(_battery_temperature) - self.load_voltage = _load_voltage / 10 - self.load_current = _load_current / 100 - self.load_power = _load_power - self.panel_voltage = _panel_voltage / 10 - self.panel_current = _panel_current / 100 - self.panel_power = _panel_power - self.load_enabled = bool(_load_enabled) - - @property - def calculated_battery_power(self) -> float: - return self.battery_voltage * self.battery_current - - @property - def calculated_panel_power(self) -> float: - return self.panel_voltage * self.panel_current - - @property - def calculated_load_power(self) -> float: - return self.load_voltage * self.load_current - - def as_dict(self): - return { - DataName.BATTERY_CHARGE: self.battery_charge, - DataName.BATTERY_VOLTAGE: self.battery_voltage, - DataName.BATTERY_CURRENT: self.battery_current, - DataName.INTERNAL_TEMPERATURE: self.internal_temperature, - DataName.BATTERY_TEMPERATURE: self.battery_temperature, - DataName.LOAD_VOLTAGE: self.load_voltage, - DataName.LOAD_CURRENT: self.load_current, - DataName.LOAD_POWER: self.load_power, - DataName.PANEL_VOLTAGE: self.panel_voltage, - DataName.PANEL_CURRENT: self.panel_current, - DataName.PANEL_POWER: self.panel_power, - DataName.LOAD_ENABLED: self.load_enabled, - DataName.CALCULATED_BATTERY_POWER: self.calculated_battery_power, - DataName.CALCULATED_PANEL_POWER: self.calculated_panel_power, - DataName.CALCULATED_LOAD_POWER: self.calculated_load_power, - } - - -class HistoricalData(DecodedData): - battery_voltage_min: float - battery_voltage_max: float - charge_max_current: float - discharge_max_current: float - charge_max_power: int - discharge_max_power: int - charge_amp_hour: int - discharge_amp_hour: int - production_energy: int - consumption_energy: int - - def __init__(self, data: bytes | bytearray | memoryview) -> None: - ( - _battery_voltage_min, - _battery_voltage_max, - _charge_max_current, - _discharge_max_current, - _charge_max_power, - _discharge_max_power, - _charge_amp_hour, - _discharge_amp_hour, - _production_energy, - _consumption_energy, - ) = struct.unpack("!HHHHHHHHHH", data) - - self.battery_voltage_min = _battery_voltage_min / 10 - self.battery_voltage_max = _battery_voltage_max / 10 - self.charge_max_current = _charge_max_current / 100 - self.discharge_max_current = _discharge_max_current / 100 - self.charge_max_power = _charge_max_power - self.discharge_max_power = _discharge_max_power - self.charge_amp_hour = _charge_amp_hour - self.discharge_amp_hour = _discharge_amp_hour - self.production_energy = _production_energy - self.consumption_energy = _consumption_energy - - def as_dict(self): - return { - DataName.BATTERY_VOLTAGE_MIN: self.battery_voltage_min, - DataName.BATTERY_VOLTAGE_MAX: self.battery_voltage_max, - DataName.CHARGE_MAX_CURRENT: self.charge_max_current, - DataName.DISCHARGE_MAX_CURRENT: self.discharge_max_current, - DataName.CHARGE_MAX_POWER: self.charge_max_power, - DataName.DISCHARGE_MAX_POWER: self.discharge_max_power, - DataName.CHARGE_AMP_HOUR: self.charge_amp_hour, - DataName.DISCHARGE_AMP_HOUR: self.discharge_amp_hour, - DataName.PRODUCTION_ENERGY: self.production_energy, - DataName.CONSUMPTION_ENERGY: self.consumption_energy, - } - - -class HistoricalExtraInfo(DecodedData): - run_days: int - discharge_count: int - full_charge_count: int - total_charge_amp_hours: int - total_discharge_amp_hours: int - total_production_energy: int - total_consumption_energy: int - - def __init__(self, data: bytes | bytearray | memoryview) -> None: - ( - _run_days, - _discharge_count, - _full_charge_count, - _total_charge_amp_hours, - _total_discharge_amp_hours, - _total_production_energy, - _total_consumption_energy, - ) = struct.unpack("!HHHLLLL", data) - - self.run_days = _run_days - self.discharge_count = _discharge_count - self.full_charge_count = _full_charge_count - self.total_charge_amp_hours = _total_charge_amp_hours - self.total_discharge_amp_hours = _total_discharge_amp_hours - self.total_production_energy = _total_production_energy - self.total_consumption_energy = _total_consumption_energy - - def as_dict(self): - return { - DataName.RUN_DAYS: self.run_days, - DataName.DISCHARGE_COUNT: self.discharge_count, - DataName.FULL_CHARGE_COUNT: self.full_charge_count, - DataName.TOTAL_CHARGE_AMP_HOURS: self.total_charge_amp_hours, - DataName.TOTAL_DISCHARGE_AMP_HOURS: self.total_discharge_amp_hours, - DataName.TOTAL_PRODUCTION_ENERGY: self.total_production_energy, - DataName.TOTAL_CONSUMPTION_ENERGY: self.total_consumption_energy, - } diff --git a/srnemqtt/srne.py b/srnemqtt/srne.py new file mode 100644 index 0000000..58b063e --- /dev/null +++ b/srnemqtt/srne.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +import struct +from decimal import Decimal +from functools import cached_property +from typing import Optional + +from .interfaces import BaseInterface +from .protocol import ( + parse_battery_state, + parse_historical_entry, + readMemory, + try_read_parse, + writeMemory, +) +from .solar_types import DataName + + +class Srne: + _dev: BaseInterface + + def __init__(self, dev: BaseInterface) -> None: + self._dev = dev + + def get_historical_entry(self, day: Optional[int] = None) -> dict: + address = 0x010B + words = 21 + if day is not None: + address = 0xF000 + day + res = try_read_parse(self._dev, address, words, parse_historical_entry) + + if res is None: + raise TimeoutError("Timeout reading historical entry") + return res + + def run_days(self) -> int: + return self.get_historical_entry()["run_days"] + + def get_battery_state(self) -> dict: + data = try_read_parse(self._dev, 0x0100, 11, parse_battery_state) + + if data is None: + raise TimeoutError("Timeout reading battery state") + + data[DataName.CALCULATED_BATTERY_POWER] = float( + Decimal(str(data.get(DataName.BATTERY_VOLTAGE, 0))) + * Decimal(str(data.get(DataName.BATTERY_CURRENT, 0))) + ) + data[DataName.CALCULATED_PANEL_POWER] = float( + Decimal(str(data.get(DataName.PANEL_VOLTAGE, 0))) + * Decimal(str(data.get(DataName.PANEL_CURRENT, 0))) + ) + data[DataName.CALCULATED_LOAD_POWER] = float( + Decimal(str(data.get(DataName.LOAD_VOLTAGE, 0))) + * Decimal(str(data.get(DataName.LOAD_CURRENT, 0))) + ) + return data + + @cached_property + def model(self) -> str: + data = readMemory(self._dev, address=0x000C, words=8) + if data is None: + raise TimeoutError("Timeout reading model") + + return data.decode().strip() + + @cached_property + def version(self) -> str: + data = readMemory(self._dev, address=0x0014, words=2) + if data is None: + raise TimeoutError("Timeout reading version") + + return "{}.{}.{}".format(*struct.unpack("!HBB", data)) + + @cached_property + def serial(self) -> str: + data = readMemory(self._dev, address=0x0018, words=2) + if data is None: + raise TimeoutError("Timeout reading serial") + + return "{:02n}-{:02n}-{:04n}".format(*struct.unpack("!BBH", data)) + + @property + def load_enabled(self) -> bool: + data = readMemory(self._dev, address=0x010A) + if data is None: + raise TimeoutError("Timeout reading serial") + + return bool(struct.unpack("!xB", data)[0]) + + def enable_load(self, enable: bool) -> bool: + data = writeMemory(self._dev, 0x010A, bytes((0, enable))) + if data is None: + raise TimeoutError("Timeout reading serial") + print(data) + return bool(struct.unpack("!xB", data)[0]) + + @cached_property + def name(self) -> str: + data = readMemory(self._dev, address=0x0049, words=16) + if data is None: + raise TimeoutError("Timeout reading name") + + return data.decode("utf-16be").strip() diff --git a/srnemqtt/util.py b/srnemqtt/util.py index a95f68f..b641e70 100644 --- a/srnemqtt/util.py +++ b/srnemqtt/util.py @@ -1,18 +1,14 @@ # -*- coding: utf-8 -*- -import json -import time -from logging import Filter as LoggingFilter -from logging import getLogger -from typing import Dict, Optional -__all__ = ["humanize_number", "Periodical", "LazyJSON", "LoggingDictFilter"] +import datetime +import sys +import time +from typing import Optional # Only factor of 1000 SI_PREFIXES_LARGE = "kMGTPEZY" SI_PREFIXES_SMALL = "mµnpfazy" -logger = getLogger(__name__) - def humanize_number(data, unit: str = ""): counter = 0 @@ -39,30 +35,9 @@ def humanize_number(data, unit: str = ""): return f"{data:.3g} {prefix}{unit}" -class LazyJSON: - def __init__(self, data): - self.data = data - - def __str__(self) -> str: - return json.dumps(self.data) - - def __repr__(self) -> str: - return repr(self.data) - - -class LoggingDictFilter(LoggingFilter): - data: Dict[str, str] - - def __init__(self): - self.data = {} - - def filter(self, record): - print(self.data) - for key, value in self.data.items(): - print(key, value) - assert not hasattr(record, key) - setattr(record, key, value) - return True +def log(*message: object, **kwargs): + print(datetime.datetime.utcnow().isoformat(" "), *message, **kwargs) + sys.stdout.flush() class Periodical: @@ -81,9 +56,7 @@ class Periodical: skipped, overshoot = divmod(now - self.prev, self.interval) skipped -= 1 if skipped: - logger.debug( - "Skipped:", skipped, overshoot, now - self.prev, self.interval - ) + log("Skipped:", skipped, overshoot, now - self.prev, self.interval) self.prev = now - overshoot return True diff --git a/tests/test_protocol.py b/tests/test_protocol.py deleted file mode 100644 index f375394..0000000 --- a/tests/test_protocol.py +++ /dev/null @@ -1,11 +0,0 @@ -from io import BytesIO - -from srnemqtt.protocol import write as protocol_write - - -def test_write(): - fh = BytesIO() - protocol_write(fh, b"Hello, World!") - fh.seek(0) - - assert fh.read() == b"Hello, World!\x4E\x11" diff --git a/tox.ini b/tox.ini index c24b36c..edfab12 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,3 @@ [flake8] -max-line-length = 120 +max-line-length = 88 extend-ignore = E203, I201, I101 - -[pytest] -pythonpath = . -testpaths = tests