Compare commits

..

1 commit

Author SHA1 Message Date
dd7c43f7e7 Add support for the load switch
Rework mqtt structure
2023-04-10 03:39:19 +02:00
28 changed files with 349 additions and 933 deletions

View file

@ -16,6 +16,3 @@ indent_style = space
[*.{yaml,yml,md}]
indent_size = 2
[.vscode/*.json]
insert_final_newline = false

View file

@ -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

16
.vscode/settings.json vendored
View file

@ -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"
]
}
"python.linting.flake8Enabled": true
}

View file

@ -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'

View file

@ -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,
)
)
#

View file

@ -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,
)
)

View file

@ -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│
├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤
│ │ ~│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │
├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤
└────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘

View file

@ -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",

View file

@ -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)

View file

@ -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}")

View file

@ -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))

View file

@ -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!")

View file

@ -4,6 +4,6 @@ bluepy
libscrc
paho-mqtt
pyserial
graypy
types-PyYAML
types-paho-mqtt

View file

@ -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

View file

@ -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

View file

@ -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:

View file

@ -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

View file

@ -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(

View file

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

View file

@ -4,4 +4,4 @@ from io import RawIOBase
class BaseInterface(RawIOBase, metaclass=ABCMeta):
timeout: float | None
pass

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
import serial # type: ignore
import serial
from . import BaseInterface

View file

@ -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

View file

@ -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)

View file

@ -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,
}

103
srnemqtt/srne.py Normal file
View file

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

View file

@ -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

View file

@ -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"

View file

@ -1,7 +1,3 @@
[flake8]
max-line-length = 120
max-line-length = 88
extend-ignore = E203, I201, I101
[pytest]
pythonpath = .
testpaths = tests