Compare commits
1 commit
master
...
output-tog
Author | SHA1 | Date | |
---|---|---|---|
dd7c43f7e7 |
28 changed files with 349 additions and 933 deletions
|
@ -16,6 +16,3 @@ indent_style = space
|
||||||
|
|
||||||
[*.{yaml,yml,md}]
|
[*.{yaml,yml,md}]
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
|
||||||
[.vscode/*.json]
|
|
||||||
insert_final_newline = false
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ repos:
|
||||||
rev: v4.4.0
|
rev: v4.4.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
#- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
- id: fix-byte-order-marker
|
- id: fix-byte-order-marker
|
||||||
- id: fix-encoding-pragma
|
- id: fix-encoding-pragma
|
||||||
- id: check-executables-have-shebangs
|
- id: check-executables-have-shebangs
|
||||||
|
@ -41,9 +41,7 @@ repos:
|
||||||
args:
|
args:
|
||||||
- "--install-types"
|
- "--install-types"
|
||||||
- "--non-interactive"
|
- "--non-interactive"
|
||||||
- "--check-untyped-defs"
|
- "--ignore-missing-imports"
|
||||||
additional_dependencies:
|
|
||||||
- typing_extensions==4.8.0
|
|
||||||
|
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 23.3.0
|
rev: 23.3.0
|
||||||
|
|
16
.vscode/settings.json
vendored
16
.vscode/settings.json
vendored
|
@ -1,14 +1,6 @@
|
||||||
{
|
{
|
||||||
|
"python.linting.mypyEnabled": true,
|
||||||
|
"python.formatting.provider": "black",
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"pylint.args": [
|
"python.linting.flake8Enabled": true
|
||||||
"--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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,5 +1,2 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
rsync --exclude=.git --exclude='packet format.java' --exclude-from=.gitignore -ravC . pi@solarpi:ble
|
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'
|
|
||||||
|
|
|
@ -99,20 +99,20 @@ def parse_log(fh, chunksize=32):
|
||||||
yield None
|
yield None
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
with open("z_solar copy.log") as fh:
|
||||||
with open("z_solar copy.log") as fh:
|
data = list(parse_log(fh))
|
||||||
data = list(parse_log(fh))
|
# print(data)
|
||||||
# print(data)
|
|
||||||
|
|
||||||
# data = list(range(256))
|
# data = list(range(256))
|
||||||
|
|
||||||
print(
|
|
||||||
memory_table(
|
print(
|
||||||
data,
|
memory_table(
|
||||||
wordsize=2,
|
data,
|
||||||
skip_nullrows=True,
|
wordsize=2,
|
||||||
)
|
skip_nullrows=True,
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|
|
@ -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,
|
|
||||||
)
|
|
||||||
)
|
|
|
@ -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│
|
|
||||||
├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤
|
|
||||||
│ │ ~│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │
|
|
||||||
├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤
|
|
||||||
└────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
|
|
|
@ -5,7 +5,7 @@ from ast import literal_eval
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
import rrdtool # type: ignore
|
import rrdtool
|
||||||
|
|
||||||
from srnemqtt.solar_types import DataName
|
from srnemqtt.solar_types import DataName
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ HISTORICAL_KEYS = {
|
||||||
DataName.BATTERY_VOLTAGE_MIN,
|
DataName.BATTERY_VOLTAGE_MIN,
|
||||||
DataName.BATTERY_VOLTAGE_MAX,
|
DataName.BATTERY_VOLTAGE_MAX,
|
||||||
DataName.CHARGE_MAX_CURRENT,
|
DataName.CHARGE_MAX_CURRENT,
|
||||||
DataName.DISCHARGE_MAX_CURRENT,
|
DataName._DISCHARGE_MAX_CURRENT,
|
||||||
DataName.CHARGE_MAX_POWER,
|
DataName.CHARGE_MAX_POWER,
|
||||||
DataName.DISCHARGE_MAX_POWER,
|
DataName.DISCHARGE_MAX_POWER,
|
||||||
DataName.CHARGE_AMP_HOUR,
|
DataName.CHARGE_AMP_HOUR,
|
||||||
|
@ -58,7 +58,7 @@ KNOWN_KEYS = HISTORICAL_KEYS.union(INSTANT_KEYS)
|
||||||
MAP = {
|
MAP = {
|
||||||
"_internal_temperature?": "internal_temp",
|
"_internal_temperature?": "internal_temp",
|
||||||
"unknown1": "charge_max_current",
|
"unknown1": "charge_max_current",
|
||||||
"unknown2": "discharge_max_current",
|
"unknown2": "_discharge_max_current?",
|
||||||
"internal_temperature": "internal_temp",
|
"internal_temperature": "internal_temp",
|
||||||
"battery_temperature": "battery_temp",
|
"battery_temperature": "battery_temp",
|
||||||
}
|
}
|
||||||
|
@ -147,6 +147,7 @@ def rrdupdate(file: str, timestamp: int, data: dict):
|
||||||
|
|
||||||
|
|
||||||
def re_read():
|
def re_read():
|
||||||
|
|
||||||
rrdtool.create(
|
rrdtool.create(
|
||||||
RRDFILE,
|
RRDFILE,
|
||||||
# "--no-overwrite",
|
# "--no-overwrite",
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from srnemqtt.constants import MAC
|
from srnemqtt.constants import MAC
|
||||||
from srnemqtt.lib.feasycom_ble import BTLEUart
|
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:
|
with BTLEUart(MAC, timeout=1) as x:
|
||||||
|
|
||||||
print(x)
|
print(x)
|
||||||
|
|
||||||
write(x, construct_read_request(0x0E, words=3))
|
write(x, construct_request(0x0E, words=3))
|
||||||
x.read(3, timeout=1)
|
x.read(3, timeout=1)
|
||||||
print(x.read(6, timeout=0.01))
|
print(x.read(6, timeout=0.01))
|
||||||
x.read(2, timeout=0.01)
|
x.read(2, timeout=0.01)
|
||||||
|
|
|
@ -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}")
|
|
|
@ -3,23 +3,22 @@ import os
|
||||||
import sys
|
import sys
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
from serial import Serial # type: ignore
|
from serial import Serial
|
||||||
|
|
||||||
print(sys.path)
|
print(sys.path)
|
||||||
sys.path.insert(1, os.path.dirname(os.path.dirname(sys.argv[0])))
|
sys.path.insert(1, os.path.dirname(os.path.dirname(sys.argv[0])))
|
||||||
# from srnemqtt.constants import MAC
|
# from srnemqtt.constants import MAC
|
||||||
# from srnemqtt.lib.feasycom_ble import BTLEUart
|
# 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 [1200, 2400, 4800, 9600, 115200]:
|
||||||
for rate in [9600]:
|
|
||||||
print(rate)
|
print(rate)
|
||||||
with Serial("/dev/ttyUSB0", baudrate=rate, timeout=2) as x:
|
with Serial("/dev/ttyUSB0", baudrate=rate, timeout=2) as x:
|
||||||
sleep(2)
|
sleep(2)
|
||||||
|
|
||||||
print(x)
|
print(x)
|
||||||
|
|
||||||
write(x, construct_read_request(0x0E, words=3))
|
write(x, construct_request(0x0E, words=3))
|
||||||
print(x.read(3))
|
print(x.read(3))
|
||||||
print(x.read(6))
|
print(x.read(6))
|
||||||
print(x.read(2))
|
print(x.read(2))
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from serial import Serial # type: ignore
|
from serial import Serial
|
||||||
|
|
||||||
with Serial("/dev/ttyUSB0", baudrate=9600, timeout=2) as x:
|
with Serial("/dev/ttyUSB0", baudrate=9600, timeout=2) as x:
|
||||||
x.write(b"Hello, World!")
|
x.write(b"Hello, World!")
|
||||||
|
|
|
@ -4,6 +4,6 @@ bluepy
|
||||||
libscrc
|
libscrc
|
||||||
paho-mqtt
|
paho-mqtt
|
||||||
pyserial
|
pyserial
|
||||||
graypy
|
|
||||||
|
|
||||||
types-PyYAML
|
types-PyYAML
|
||||||
|
types-paho-mqtt
|
||||||
|
|
|
@ -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
|
|
|
@ -2,33 +2,25 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import time
|
import time
|
||||||
from logging import getLogger
|
from typing import List, Optional, cast
|
||||||
from logging import root as logging_root
|
|
||||||
from logging.config import dictConfig as loggingDictConfig
|
|
||||||
|
|
||||||
from bluepy.btle import BTLEDisconnectError # type: ignore
|
from bluepy.btle import BTLEDisconnectError
|
||||||
from serial import SerialException # type: ignore
|
from serial import SerialException
|
||||||
|
|
||||||
|
from srnemqtt.consumers import BaseConsumer
|
||||||
|
|
||||||
from .config import get_config, get_consumers, get_interface
|
from .config import get_config, get_consumers, get_interface
|
||||||
from .protocol import ChargeController
|
from .srne import Srne
|
||||||
from .util import LazyJSON, LoggingDictFilter, Periodical
|
from .util import Periodical, log
|
||||||
|
|
||||||
logger = getLogger("SolarMPPT")
|
|
||||||
|
|
||||||
|
|
||||||
class CommunicationError(BTLEDisconnectError, SerialException, IOError):
|
class CommunicationError(BTLEDisconnectError, SerialException, TimeoutError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main() -> None:
|
||||||
conf = get_config()
|
conf = get_config()
|
||||||
|
consumers: Optional[List[BaseConsumer]] = None
|
||||||
loggingDictConfig(conf.get("logging", {}))
|
|
||||||
logging_dict_filter = LoggingDictFilter()
|
|
||||||
logging_dict_filter.data["service"] = "SolarMPPT"
|
|
||||||
logging_root.addFilter(logging_dict_filter)
|
|
||||||
|
|
||||||
consumers = get_consumers(conf)
|
|
||||||
|
|
||||||
per_voltages = Periodical(interval=15)
|
per_voltages = Periodical(interval=15)
|
||||||
per_current_hist = Periodical(interval=60)
|
per_current_hist = Periodical(interval=60)
|
||||||
|
@ -36,66 +28,53 @@ def main():
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
logger.info("Connecting...")
|
log("Connecting...")
|
||||||
with get_interface() as dev:
|
with get_interface() as dev:
|
||||||
cc = ChargeController(dev)
|
srne = Srne(dev)
|
||||||
logging_dict_filter.data["srne_model"] = cc.model
|
log("Connected.")
|
||||||
logging_dict_filter.data["srne_version"] = cc.version
|
|
||||||
logging_dict_filter.data["srne_serial"] = cc.serial
|
|
||||||
|
|
||||||
logger.info("Connected.")
|
if consumers is None:
|
||||||
|
consumers = get_consumers(srne, conf)
|
||||||
|
|
||||||
logger.info(f"Controller model: {cc.model}")
|
days = 7
|
||||||
logger.info(f"Controller version: {cc.version}")
|
res = srne.get_historical_entry()
|
||||||
logger.info(f"Controller serial: {cc.serial}")
|
if res:
|
||||||
for consumer in consumers:
|
log(res)
|
||||||
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}))
|
|
||||||
for consumer in consumers:
|
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:
|
while True:
|
||||||
now = time.time()
|
now = time.time()
|
||||||
|
|
||||||
if per_voltages(now):
|
if per_voltages(now):
|
||||||
data = cc.state.as_dict()
|
data = srne.get_battery_state()
|
||||||
logger.debug(LazyJSON(data))
|
if data:
|
||||||
for consumer in consumers:
|
log(data)
|
||||||
consumer.write(data)
|
for consumer in consumers:
|
||||||
|
consumer.write(data)
|
||||||
|
|
||||||
if per_current_hist(now):
|
if per_current_hist(now):
|
||||||
data = cc.today.as_dict()
|
try:
|
||||||
data.update(cc.extra.as_dict())
|
data = srne.get_historical_entry()
|
||||||
logger.debug(LazyJSON(data))
|
log(data)
|
||||||
for consumer in consumers:
|
for consumer in consumers:
|
||||||
consumer.write(data)
|
consumer.write(data)
|
||||||
|
except TimeoutError:
|
||||||
|
pass
|
||||||
|
|
||||||
# print(".")
|
# print(".")
|
||||||
for consumer in consumers:
|
for consumer in consumers:
|
||||||
consumer.poll()
|
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'):
|
# if STATUS.get('load_enabled'):
|
||||||
# write(wd, CMD_DISABLE_LOAD)
|
# write(wd, CMD_DISABLE_LOAD)
|
||||||
|
@ -103,12 +82,13 @@ def main():
|
||||||
# write(wd, CMD_ENABLE_LOAD)
|
# write(wd, CMD_ENABLE_LOAD)
|
||||||
|
|
||||||
except CommunicationError:
|
except CommunicationError:
|
||||||
logger.error("Disconnected")
|
log("ERROR: Disconnected")
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
except (KeyboardInterrupt, SystemExit, Exception) as e:
|
except (KeyboardInterrupt, SystemExit, Exception) as e:
|
||||||
for consumer in consumers:
|
if consumers is not None:
|
||||||
consumer.exit()
|
for consumer in consumers:
|
||||||
|
consumer.exit()
|
||||||
|
|
||||||
if type(e) is not KeyboardInterrupt:
|
if type(e) is not KeyboardInterrupt:
|
||||||
raise
|
raise
|
||||||
|
|
|
@ -8,6 +8,7 @@ import yaml
|
||||||
|
|
||||||
from .consumers import BaseConsumer
|
from .consumers import BaseConsumer
|
||||||
from .interfaces import BaseInterface
|
from .interfaces import BaseInterface
|
||||||
|
from .srne import Srne
|
||||||
|
|
||||||
|
|
||||||
def get_consumer(name: str) -> Optional[Type[BaseConsumer]]:
|
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:
|
with open("config.yaml", "r") as fh:
|
||||||
conf: dict = yaml.safe_load(fh)
|
conf: dict = yaml.safe_load(fh)
|
||||||
conf.setdefault("consumers", {})
|
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
|
return conf
|
||||||
|
|
||||||
|
@ -57,12 +35,12 @@ def get_config() -> Dict[str, Any]:
|
||||||
def write_config(conf: Dict[str, Any]):
|
def write_config(conf: Dict[str, Any]):
|
||||||
with open(".config.yaml~writing", "w") as fh:
|
with open(".config.yaml~writing", "w") as fh:
|
||||||
yaml.safe_dump(conf, fh, indent=2, encoding="utf-8")
|
yaml.safe_dump(conf, fh, indent=2, encoding="utf-8")
|
||||||
fh.flush()
|
os.rename(".config.yaml~writing", "config.yaml")
|
||||||
os.fsync(fh.fileno())
|
|
||||||
os.replace(".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:
|
if conf is None:
|
||||||
conf = get_config()
|
conf = get_config()
|
||||||
|
|
||||||
|
@ -72,7 +50,7 @@ def get_consumers(conf: Optional[Dict[str, Any]] = None) -> List[BaseConsumer]:
|
||||||
mod = get_consumer(name)
|
mod = get_consumer(name)
|
||||||
if mod:
|
if mod:
|
||||||
# print(mod)
|
# print(mod)
|
||||||
consumers.append(mod(consumer_config))
|
consumers.append(mod(settings=consumer_config, srne=srne))
|
||||||
|
|
||||||
write_config(conf)
|
write_config(conf)
|
||||||
return consumers
|
return consumers
|
||||||
|
@ -105,7 +83,7 @@ def get_interface(conf: Optional[Dict[str, Any]] = None) -> BaseInterface:
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
conf = get_config()
|
conf = get_config()
|
||||||
|
|
||||||
consumers = get_consumers(conf)
|
consumers = get_consumers(Srne(BaseInterface()), conf)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
|
|
|
@ -2,15 +2,16 @@
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
from ..protocol import ChargeController
|
from ..srne import Srne
|
||||||
|
|
||||||
|
|
||||||
class BaseConsumer(ABC):
|
class BaseConsumer(ABC):
|
||||||
settings: Dict[str, Any]
|
settings: Dict[str, Any]
|
||||||
controller: ChargeController | None = None
|
srne: Srne
|
||||||
|
|
||||||
@abstractmethod
|
@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)
|
self.config(settings)
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
|
|
|
@ -1,22 +1,20 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import json
|
import json
|
||||||
from logging import getLogger
|
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from typing import Any, Dict, List, Optional, TypeAlias
|
from typing import Any, Dict, List, Optional
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
import paho.mqtt.client as mqtt
|
import paho.mqtt.client as mqtt
|
||||||
|
|
||||||
from ..solar_types import DataName
|
from ..solar_types import DataName
|
||||||
|
from ..srne import Srne
|
||||||
from . import BaseConsumer
|
from . import BaseConsumer
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
|
||||||
|
|
||||||
MAP_VALUES: Dict[DataName, Dict[str, Any]] = {
|
MAP_VALUES: Dict[DataName, Dict[str, Any]] = {
|
||||||
# DataName.BATTERY_VOLTAGE_MIN: {},
|
# DataName.BATTERY_VOLTAGE_MIN: {},
|
||||||
# DataName.BATTERY_VOLTAGE_MAX: {},
|
# DataName.BATTERY_VOLTAGE_MAX: {},
|
||||||
# DataName.CHARGE_MAX_CURRENT: {},
|
# DataName.CHARGE_MAX_CURRENT: {},
|
||||||
# DataName.DISCHARGE_MAX_CURRENT: {},
|
# DataName._DISCHARGE_MAX_CURRENT: {},
|
||||||
# DataName.CHARGE_MAX_POWER: {},
|
# DataName.CHARGE_MAX_POWER: {},
|
||||||
# DataName.DISCHARGE_MAX_POWER: {},
|
# DataName.DISCHARGE_MAX_POWER: {},
|
||||||
# DataName.CHARGE_AMP_HOUR: {},
|
# DataName.CHARGE_AMP_HOUR: {},
|
||||||
|
@ -84,7 +82,11 @@ MAP_VALUES: Dict[DataName, Dict[str, Any]] = {
|
||||||
"type": "current",
|
"type": "current",
|
||||||
"state_class": "measurement",
|
"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: {
|
DataName.LOAD_ENABLED: {
|
||||||
"type": "outlet",
|
"type": "outlet",
|
||||||
"platform": "switch",
|
"platform": "switch",
|
||||||
|
@ -119,41 +121,30 @@ MAP_VALUES: Dict[DataName, Dict[str, Any]] = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
PayloadType: TypeAlias = str | bytes | bytearray | int | float | None
|
|
||||||
|
|
||||||
|
|
||||||
class MqttConsumer(BaseConsumer):
|
class MqttConsumer(BaseConsumer):
|
||||||
|
client: mqtt.Client
|
||||||
initialized: List[str]
|
initialized: List[str]
|
||||||
|
srne: Srne
|
||||||
|
|
||||||
_client: mqtt.Client | None = None
|
def __init__(self, settings: Dict[str, Any], srne: Srne) -> None:
|
||||||
|
|
||||||
def __init__(self, settings: Dict[str, Any]) -> None:
|
|
||||||
self.initialized = []
|
self.initialized = []
|
||||||
|
|
||||||
super().__init__(settings)
|
super().__init__(settings, srne)
|
||||||
|
self.client = mqtt.Client(client_id=settings["client"]["id"], userdata=self)
|
||||||
@property
|
self.client.on_connect = self.on_connect
|
||||||
def client(self) -> mqtt.Client:
|
self.client.on_message = self.on_message
|
||||||
if self._client is not None:
|
self.client.on_disconnect = self.on_disconnect
|
||||||
return self._client
|
self.client.on_connect_fail = self.on_connect_fail
|
||||||
|
|
||||||
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
|
|
||||||
# Will must be set before connecting!!
|
# Will must be set before connecting!!
|
||||||
self._client.will_set(
|
self.client.will_set(
|
||||||
f"{self.topic_prefix}/available", payload="offline", retain=True
|
f"{self.topic_prefix}/available", payload="offline", retain=True
|
||||||
)
|
)
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
self._client.connect(
|
self.client.connect(
|
||||||
self.settings["client"]["host"],
|
settings["client"]["host"],
|
||||||
self.settings["client"]["port"],
|
settings["client"]["port"],
|
||||||
self.settings["client"]["keepalive"],
|
settings["client"]["keepalive"],
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
except OSError as err:
|
except OSError as err:
|
||||||
|
@ -164,13 +155,9 @@ class MqttConsumer(BaseConsumer):
|
||||||
elif err.errno == -3:
|
elif err.errno == -3:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
logger.exception("Unknown error connecting to mqtt server")
|
|
||||||
raise
|
raise
|
||||||
logger.warning(
|
print(err)
|
||||||
"Temporary failure connecting to mqtt server", exc_info=True
|
|
||||||
)
|
|
||||||
sleep(0.1)
|
sleep(0.1)
|
||||||
return self._client
|
|
||||||
|
|
||||||
def config(self, settings: Dict[str, Any]):
|
def config(self, settings: Dict[str, Any]):
|
||||||
super().config(settings)
|
super().config(settings)
|
||||||
|
@ -187,24 +174,14 @@ class MqttConsumer(BaseConsumer):
|
||||||
|
|
||||||
settings.setdefault("discovery_prefix", "homeassistant")
|
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
|
@property
|
||||||
def topic_prefix(self):
|
def topic_prefix(self):
|
||||||
return f"{self.settings['prefix']}/{self.controller_id}"
|
return f"{self.settings['prefix']}/{self.srne.serial}"
|
||||||
|
|
||||||
def get_ha_config(
|
def get_ha_config(
|
||||||
self,
|
self,
|
||||||
id: str,
|
id,
|
||||||
name: str,
|
name,
|
||||||
unit: Optional[str] = None,
|
unit: Optional[str] = None,
|
||||||
type: Optional[str] = None,
|
type: Optional[str] = None,
|
||||||
expiry: int = 90,
|
expiry: int = 90,
|
||||||
|
@ -212,25 +189,27 @@ class MqttConsumer(BaseConsumer):
|
||||||
platform: str = "sensor",
|
platform: str = "sensor",
|
||||||
):
|
):
|
||||||
assert state_class in [None, "measurement", "total", "total_increasing"]
|
assert state_class in [None, "measurement", "total", "total_increasing"]
|
||||||
assert self.controller is not None
|
|
||||||
|
|
||||||
res = {
|
res = {
|
||||||
"~": f"{self.topic_prefix}",
|
"~": f"{self.topic_prefix}",
|
||||||
"unique_id": f"{self.controller_id}_{id}",
|
"unique_id": f"srne_{self.srne.serial}_{id}",
|
||||||
"object_id": f"{self.controller_id}_{id}", # Used for entity id
|
"object_id": f"srne_{self.srne.serial}_{id}", # Used for entity id
|
||||||
"availability_topic": "~/available",
|
"availability_topic": "~/available",
|
||||||
"state_topic": f"~/{id}",
|
"state_topic": f"~/{id}",
|
||||||
"name": name,
|
"name": name,
|
||||||
"device": {
|
"device": {
|
||||||
"identifiers": [
|
"identifiers": [
|
||||||
self.controller_id,
|
self.srne.serial,
|
||||||
],
|
],
|
||||||
"manufacturer": self.controller.manufacturer,
|
# TODO: Get charger serial and use for identifier instead
|
||||||
"model": self.controller.model,
|
# See: https://www.home-assistant.io/integrations/sensor.mqtt/#device
|
||||||
"sw_version": self.controller.version,
|
# "via_device": self.settings["device_id"],
|
||||||
"via_device": self.settings["device_id"],
|
|
||||||
"suggested_area": "Solar panel",
|
"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,
|
"force_update": True,
|
||||||
"expire_after": expiry,
|
"expire_after": expiry,
|
||||||
|
@ -246,12 +225,13 @@ class MqttConsumer(BaseConsumer):
|
||||||
res["command_topic"] = f"{res['state_topic']}/set"
|
res["command_topic"] = f"{res['state_topic']}/set"
|
||||||
res["payload_on"] = True
|
res["payload_on"] = True
|
||||||
res["payload_off"] = False
|
res["payload_off"] = False
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
# The callback for when the client receives a CONNACK response from the server.
|
# The callback for when the client receives a CONNACK response from the server.
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def on_connect(client: mqtt.Client, userdata: "MqttConsumer", flags, rc):
|
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
|
# Subscribing in on_connect() means that if we lose the connection and
|
||||||
# reconnect then subscriptions will be renewed.
|
# reconnect then subscriptions will be renewed.
|
||||||
|
@ -268,66 +248,65 @@ class MqttConsumer(BaseConsumer):
|
||||||
def on_load_switch(
|
def on_load_switch(
|
||||||
client: mqtt.Client, userdata: "MqttConsumer", message: mqtt.MQTTMessage
|
client: mqtt.Client, userdata: "MqttConsumer", message: mqtt.MQTTMessage
|
||||||
):
|
):
|
||||||
assert userdata.controller is not None
|
print(message)
|
||||||
logger.debug(message.payload)
|
print(message.info)
|
||||||
|
print(message.state)
|
||||||
|
print(message.payload)
|
||||||
payload = message.payload.decode().upper() in ("ON", "TRUE", "ENABLE", "YES")
|
payload = message.payload.decode().upper() in ("ON", "TRUE", "ENABLE", "YES")
|
||||||
|
if type(payload) is bool:
|
||||||
res = userdata.controller.load_enabled = payload
|
res = userdata.srne.enable_load(payload)
|
||||||
client.publish(
|
client.publish(
|
||||||
f"{userdata.topic_prefix}/load_enabled", payload=res, retain=True
|
f"{userdata.topic_prefix}/load_enabled", payload=res, retain=True
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
print(f"!!! Unknown payload for switch callback: {message.payload!r}")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def on_connect_fail(client: mqtt.Client, userdata: "MqttConsumer"):
|
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.
|
# The callback for when a PUBLISH message is received from the server.
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def on_message(client, userdata, msg):
|
def on_message(client, userdata, msg):
|
||||||
logger.info(msg.topic + " " + str(msg.payload))
|
print(msg.topic + " " + str(msg.payload))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def on_disconnect(client: mqtt.Client, userdata: "MqttConsumer", rc, prop=None):
|
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):
|
def poll(self):
|
||||||
res = self.client.loop(timeout=0.1, max_packets=5)
|
res = self.client.loop(timeout=0.1, max_packets=5)
|
||||||
|
|
||||||
if res != mqtt.MQTT_ERR_SUCCESS:
|
if res != mqtt.MQTT_ERR_SUCCESS:
|
||||||
logger.warning("loop returned non-success: %s", res)
|
print(self.__class__.__name__, "loop returned non-success:", res)
|
||||||
try:
|
try:
|
||||||
sleep(1)
|
sleep(1)
|
||||||
res = self.client.reconnect()
|
res = self.client.reconnect()
|
||||||
if res != mqtt.MQTT_ERR_SUCCESS:
|
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:
|
except (OSError, mqtt.WebsocketConnectionError) as err:
|
||||||
logger.error("Reconnect failed: %s", err)
|
print(self.__class__.__name__, "Reconnect failed:", err)
|
||||||
|
|
||||||
return super().poll()
|
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))
|
self.client.publish(f"{self.topic_prefix}/raw", payload=json.dumps(data))
|
||||||
|
|
||||||
for dataname, data_value in data.items():
|
for k, v in data.items():
|
||||||
if dataname in MAP_VALUES:
|
if k in MAP_VALUES:
|
||||||
if dataname not in self.initialized:
|
if k not in self.initialized:
|
||||||
km = MAP_VALUES[DataName(dataname)]
|
km = MAP_VALUES[DataName(k)]
|
||||||
pretty_name = dataname.replace("_", " ").capitalize()
|
pretty_name = k.replace("_", " ").capitalize()
|
||||||
disc_prefix = self.settings["discovery_prefix"]
|
disc_prefix = self.settings["discovery_prefix"]
|
||||||
platform = km.get("platform", "sensor")
|
platform = km.get("platform", "sensor")
|
||||||
|
|
||||||
self.client.publish(
|
self.client.publish(
|
||||||
f"{disc_prefix}/{platform}/{self.controller_id}/{dataname}/config",
|
f"{disc_prefix}/{platform}/srne_{self.srne.serial}_{k}/config",
|
||||||
payload=json.dumps(
|
payload=json.dumps(self.get_ha_config(k, pretty_name, **km)),
|
||||||
self.get_ha_config(dataname, pretty_name, **km)
|
|
||||||
),
|
|
||||||
retain=True,
|
retain=True,
|
||||||
)
|
)
|
||||||
self.initialized.append(dataname)
|
self.initialized.append(k)
|
||||||
|
|
||||||
self.client.publish(
|
self.client.publish(f"{self.topic_prefix}/{k}", v, retain=True)
|
||||||
f"{self.topic_prefix}/{dataname}", data_value, retain=True
|
|
||||||
)
|
|
||||||
|
|
||||||
def exit(self):
|
def exit(self):
|
||||||
self.client.publish(
|
self.client.publish(
|
||||||
|
|
|
@ -2,12 +2,13 @@
|
||||||
import json
|
import json
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from ..srne import Srne
|
||||||
from . import BaseConsumer
|
from . import BaseConsumer
|
||||||
|
|
||||||
|
|
||||||
class StdoutConsumer(BaseConsumer):
|
class StdoutConsumer(BaseConsumer):
|
||||||
def __init__(self, settings: Dict[str, Any]) -> None:
|
def __init__(self, settings: Dict[str, Any], srne: Srne) -> None:
|
||||||
super().__init__(settings)
|
super().__init__(settings, srne)
|
||||||
|
|
||||||
def poll(self):
|
def poll(self):
|
||||||
return super().poll()
|
return super().poll()
|
||||||
|
|
|
@ -4,4 +4,4 @@ from io import RawIOBase
|
||||||
|
|
||||||
|
|
||||||
class BaseInterface(RawIOBase, metaclass=ABCMeta):
|
class BaseInterface(RawIOBase, metaclass=ABCMeta):
|
||||||
timeout: float | None
|
pass
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import serial # type: ignore
|
import serial
|
||||||
|
|
||||||
from . import BaseInterface
|
from . import BaseInterface
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,11 @@ import queue
|
||||||
import time
|
import time
|
||||||
from typing import TYPE_CHECKING, Optional, cast
|
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"
|
WRITE_DEVICE = "0000ffd1-0000-1000-8000-00805f9b34fb"
|
||||||
READ_DEVICE = "0000fff1-0000-1000-8000-00805f9b34fb"
|
READ_DEVICE = "0000fff1-0000-1000-8000-00805f9b34fb"
|
||||||
|
@ -14,7 +18,7 @@ class BTLEUart(io.RawIOBase):
|
||||||
mac: str
|
mac: str
|
||||||
write_endpoint: str
|
write_endpoint: str
|
||||||
read_endpoint: str
|
read_endpoint: str
|
||||||
timeout: float | None
|
timeout: float
|
||||||
|
|
||||||
device: Optional[btle.Peripheral] = None
|
device: Optional[btle.Peripheral] = None
|
||||||
_write_handle: Optional[btle.Characteristic] = 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]
|
self._write_handle = self.device.getCharacteristics(uuid=self.write_endpoint)[0]
|
||||||
# print("Handles:", self._read_handle.handle, self._write_handle.handle)
|
# 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()
|
self._ensure_connected()
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
self.device = cast(btle.Peripheral, self.device)
|
self.device = cast(btle.Peripheral, self.device)
|
||||||
|
|
||||||
timeout = self.timeout or 30
|
if timeout is None:
|
||||||
|
timeout = self.timeout
|
||||||
|
|
||||||
if num is None:
|
if num is None:
|
||||||
start = time.time()
|
start = time.time()
|
||||||
|
@ -127,9 +132,7 @@ class BTLEUart(io.RawIOBase):
|
||||||
del self._read_buffer[:num]
|
del self._read_buffer[:num]
|
||||||
return data or None
|
return data or None
|
||||||
|
|
||||||
def readinto(self, buffer: bytearray | memoryview) -> Optional[int]: # type: ignore [override]
|
def readinto(self, buffer: "WriteableBuffer") -> Optional[int]:
|
||||||
# Buffer does not provide Sized, and bytes is read only.
|
|
||||||
# bytearray | memoryview is the default implementations that provide WriteableBuffer
|
|
||||||
data = self._read(len(buffer))
|
data = self._read(len(buffer))
|
||||||
|
|
||||||
if data is None:
|
if data is None:
|
||||||
|
@ -141,15 +144,23 @@ class BTLEUart(io.RawIOBase):
|
||||||
def readall(self) -> bytes:
|
def readall(self) -> bytes:
|
||||||
return self._read()
|
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:
|
if size is None:
|
||||||
res = super().read()
|
res = super().read()
|
||||||
else:
|
else:
|
||||||
res = super().read(size)
|
res = super().read(size)
|
||||||
|
|
||||||
|
if timeout:
|
||||||
|
self.timeout = _timeout
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def write(self, b: bytes | bytearray | memoryview) -> Optional[int]: # type: ignore [override]
|
def write(self, b: "ReadableBuffer") -> Optional[int]:
|
||||||
self._ensure_connected()
|
self._ensure_connected()
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
self.device = cast(btle.Peripheral, self.device)
|
self.device = cast(btle.Peripheral, self.device)
|
||||||
|
@ -163,9 +174,8 @@ class BTLEUart(io.RawIOBase):
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __exit__(self, type, value, traceback):
|
def __exit__(self, type, value, traceback):
|
||||||
if self.device is not None:
|
self.device.disconnect()
|
||||||
self.device.disconnect()
|
del self.device
|
||||||
self.device = None
|
|
||||||
|
|
||||||
def seekable(self) -> bool:
|
def seekable(self) -> bool:
|
||||||
return False
|
return False
|
||||||
|
|
|
@ -1,23 +1,16 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import struct
|
import struct
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
from logging import getLogger
|
from io import RawIOBase
|
||||||
from typing import Callable, Collection, List, Optional
|
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 .constants import ACTION_READ, ACTION_WRITE, POSSIBLE_MARKER
|
||||||
from .interfaces import BaseInterface
|
from .interfaces import BaseInterface
|
||||||
from .solar_types import (
|
from .solar_types import DATA_BATTERY_STATE, HISTORICAL_DATA, DataItem
|
||||||
DATA_BATTERY_STATE,
|
from .util import log
|
||||||
HISTORICAL_DATA,
|
|
||||||
ChargerState,
|
|
||||||
DataItem,
|
|
||||||
HistoricalData,
|
|
||||||
HistoricalExtraInfo,
|
|
||||||
)
|
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def write(fh, data):
|
def write(fh, data):
|
||||||
|
@ -27,14 +20,14 @@ def write(fh, data):
|
||||||
fh.write(data + bcrc)
|
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}"
|
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}"
|
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:
|
def parse(data: bytes, items: Collection[DataItem], offset: int = 0) -> dict:
|
||||||
|
@ -73,33 +66,35 @@ def parse_packet(data):
|
||||||
|
|
||||||
if crc != calculated_crc:
|
if crc != calculated_crc:
|
||||||
e = ValueError(f"CRC missmatch: expected {crc:04X}, got {calculated_crc:04X}.")
|
e = ValueError(f"CRC missmatch: expected {crc:04X}, got {calculated_crc:04X}.")
|
||||||
# e.tag = tag
|
e.tag = tag
|
||||||
# e.operation = operation
|
e.operation = operation
|
||||||
# e.size = size
|
e.size = size
|
||||||
# e.payload = payload
|
e.payload = payload
|
||||||
# e.crc = crc
|
e.crc = crc
|
||||||
# e.calculated_crc = calculated_crc
|
e.calculated_crc = calculated_crc
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
return payload
|
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}"
|
assert byte >= 0 and byte < 256, f"byte: Expected 8bit unsigned int, got {byte}"
|
||||||
|
|
||||||
def expand(b: Optional[bytes]):
|
def expand(b: Optional[bytes]):
|
||||||
if not b:
|
if b is None:
|
||||||
return None
|
return b
|
||||||
return b[0]
|
return b[0]
|
||||||
|
|
||||||
start = time.time()
|
start = time.time()
|
||||||
discarded: List[str] = []
|
discarded = 0
|
||||||
read_byte = expand(fh.read(1))
|
read_byte = expand(fh.read(1))
|
||||||
while read_byte != byte:
|
while read_byte != byte:
|
||||||
if read_byte is not None:
|
if read_byte is not None:
|
||||||
if not discarded:
|
if not discarded:
|
||||||
discarded.append("Discarding")
|
log("Discarding", end="")
|
||||||
discarded.append(f"{read_byte:02X}")
|
discarded += 1
|
||||||
|
print(f" {read_byte:02X}", end="")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
if time.time() - start > timeout:
|
if time.time() - start > timeout:
|
||||||
read_byte = None
|
read_byte = None
|
||||||
|
@ -108,14 +103,15 @@ def discardUntil(fh: BaseInterface, byte: int, timeout=10) -> Optional[int]:
|
||||||
read_byte = expand(fh.read(1))
|
read_byte = expand(fh.read(1))
|
||||||
|
|
||||||
if discarded:
|
if discarded:
|
||||||
logger.debug(" ".join(discarded))
|
print()
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
return read_byte
|
return read_byte
|
||||||
|
|
||||||
|
|
||||||
def readMemory(fh: BaseInterface, address: int, words: int = 1) -> Optional[bytes]:
|
def readMemory(fh: BaseInterface, address: int, words: int = 1) -> Optional[bytes]:
|
||||||
# log(f"Reading {words} words from 0x{address:04X}")
|
# 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)
|
# log("Request:", request)
|
||||||
write(fh, request)
|
write(fh, request)
|
||||||
|
|
||||||
|
@ -132,79 +128,51 @@ def readMemory(fh: BaseInterface, address: int, words: int = 1) -> Optional[byte
|
||||||
try:
|
try:
|
||||||
crc = struct.unpack_from("<H", _crc)[0]
|
crc = struct.unpack_from("<H", _crc)[0]
|
||||||
except struct.error:
|
except struct.error:
|
||||||
logger.error(
|
log(f"readMemory: CRC error; read {len(_crc)} bytes (2 expected)")
|
||||||
"readMemory: CRC error; read %s bytes (2 expected)", len(_crc)
|
|
||||||
)
|
|
||||||
return None
|
return None
|
||||||
calculated_crc = modbus(bytes([tag, operation, size, *data]))
|
calculated_crc = modbus(bytes([tag, operation, size, *data]))
|
||||||
if crc == calculated_crc:
|
if crc == calculated_crc:
|
||||||
return data
|
return data
|
||||||
else:
|
else:
|
||||||
logger.error(
|
log(f"readMemory: CRC error; {crc:04X} != {calculated_crc:04X}")
|
||||||
f"readMemory: CRC error; {crc:04X} != {calculated_crc:04X}"
|
log("data or crc is falsely", header, data, _crc)
|
||||||
)
|
|
||||||
logger.error("data or crc is falsely %s %s %s", header, data, _crc)
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
# set(255, 266, 1 or 0)
|
def writeMemory(fh: BaseInterface, address: int, output_data: bytes) -> Optional[bytes]:
|
||||||
# ff 06 01 0a 00 01
|
# TODO: Verify behavior on multi-word writes
|
||||||
# CMD_ENABLE_LOAD = b"\xff\x06\x01\x0a\x00\x01"
|
# log(f"Reading {words} words from 0x{address:04X}")
|
||||||
# CMD_DISABLE_LOAD = b"\xff\x06\x01\x0a\x00\x00"
|
request = construct_write(address, data=output_data)
|
||||||
# REG_LOAD_ENABLE = 0x010A
|
# log("Request:", request)
|
||||||
|
write(fh, request)
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
tag = discardUntil(fh, 0xFF)
|
tag = discardUntil(fh, 0xFF)
|
||||||
if tag is None:
|
if tag is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
header = fh.read(3)
|
_operation = fh.read(1)
|
||||||
if header and len(header) == 3:
|
result_addr = fh.read(2)
|
||||||
operation, size, address = header
|
# log("Operation:", _operation)
|
||||||
logger.log(5, header)
|
if _operation is not None and result_addr is not None:
|
||||||
# size field is zero when writing device name for whatever reason
|
operation = _operation[0]
|
||||||
# write command seems to only accept a single word, so this is fine;
|
data = fh.read(2)
|
||||||
# we just hardcode the number of bytes read to two here.
|
# log("Data:", data)
|
||||||
rdata = fh.read(2)
|
|
||||||
_crc = fh.read(2)
|
_crc = fh.read(2)
|
||||||
if rdata and _crc:
|
if data and _crc:
|
||||||
try:
|
try:
|
||||||
crc = struct.unpack_from("<H", _crc)[0]
|
crc = struct.unpack_from("<H", _crc)[0]
|
||||||
except struct.error:
|
except struct.error:
|
||||||
logger.error(
|
log(f"readMemory: CRC error; read {len(_crc)} bytes (2 expected)")
|
||||||
f"writeMemory: CRC error; read {len(_crc)} bytes (2 expected)"
|
|
||||||
)
|
|
||||||
return None
|
return None
|
||||||
calculated_crc = modbus(bytes([tag, operation, size, address, *rdata]))
|
calculated_crc = modbus(bytes([tag, operation, *result_addr, *data]))
|
||||||
if crc == calculated_crc:
|
if crc == calculated_crc:
|
||||||
return rdata
|
return data
|
||||||
else:
|
else:
|
||||||
logger.error(
|
log(f"readMemory: CRC error; {crc:04X} != {calculated_crc:04X}")
|
||||||
f"writeMemory: CRC error; {crc:04X} != {calculated_crc:04X}"
|
log("data or crc is falsely", operation, result_addr, data, _crc)
|
||||||
)
|
|
||||||
logger.error("data or crc is falsely %s %s %s", header, rdata, _crc)
|
|
||||||
return None
|
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(
|
def try_read_parse(
|
||||||
dev: BaseInterface,
|
dev: BaseInterface,
|
||||||
address: int,
|
address: int,
|
||||||
|
@ -219,156 +187,10 @@ def try_read_parse(
|
||||||
try:
|
try:
|
||||||
if parser:
|
if parser:
|
||||||
return parser(res)
|
return parser(res)
|
||||||
except struct.error:
|
except struct.error as e:
|
||||||
logger.exception("0x0100 Unpack error: %s %s", len(res), res)
|
log(e)
|
||||||
_timeout = dev.timeout
|
log("0x0100 Unpack error:", len(res), res)
|
||||||
dev.timeout = 0.5
|
log("Flushed from read buffer; ", dev.read()) # TODO: timeout=0.5
|
||||||
logger.warning("Flushed from read buffer; %s", dev.read())
|
|
||||||
dev.timeout = _timeout
|
|
||||||
else:
|
else:
|
||||||
logger.warning(
|
log(f"No data read, expected {words*2} bytes (attempts left: {attempts})")
|
||||||
f"No data read, expected {words*2} bytes (attempts left: {attempts})"
|
|
||||||
)
|
|
||||||
return None
|
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)
|
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import struct
|
import struct
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
from enum import Enum, unique
|
from enum import Enum, unique
|
||||||
from typing import Any, Callable, Dict, Optional
|
from typing import Callable, Optional
|
||||||
|
|
||||||
|
|
||||||
@unique
|
@unique
|
||||||
|
@ -22,7 +21,7 @@ class DataName(str, Enum):
|
||||||
BATTERY_VOLTAGE_MIN = "battery_voltage_min"
|
BATTERY_VOLTAGE_MIN = "battery_voltage_min"
|
||||||
BATTERY_VOLTAGE_MAX = "battery_voltage_max"
|
BATTERY_VOLTAGE_MAX = "battery_voltage_max"
|
||||||
CHARGE_MAX_CURRENT = "charge_max_current"
|
CHARGE_MAX_CURRENT = "charge_max_current"
|
||||||
DISCHARGE_MAX_CURRENT = "discharge_max_current"
|
_DISCHARGE_MAX_CURRENT = "_discharge_max_current?"
|
||||||
CHARGE_MAX_POWER = "charge_max_power"
|
CHARGE_MAX_POWER = "charge_max_power"
|
||||||
DISCHARGE_MAX_POWER = "discharge_max_power"
|
DISCHARGE_MAX_POWER = "discharge_max_power"
|
||||||
CHARGE_AMP_HOUR = "charge_amp_hour"
|
CHARGE_AMP_HOUR = "charge_amp_hour"
|
||||||
|
@ -45,7 +44,7 @@ class DataName(str, Enum):
|
||||||
return repr(self.value)
|
return repr(self.value)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.value
|
return str(self.value)
|
||||||
|
|
||||||
|
|
||||||
class DataItem:
|
class DataItem:
|
||||||
|
@ -105,14 +104,13 @@ HISTORICAL_DATA = [
|
||||||
DataItem(DataName.BATTERY_VOLTAGE_MIN, "H", "V", lambda n: n / 10),
|
DataItem(DataName.BATTERY_VOLTAGE_MIN, "H", "V", lambda n: n / 10),
|
||||||
DataItem(DataName.BATTERY_VOLTAGE_MAX, "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.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.CHARGE_MAX_POWER, "H", "W"),
|
||||||
DataItem(DataName.DISCHARGE_MAX_POWER, "H", "W"),
|
DataItem(DataName.DISCHARGE_MAX_POWER, "H", "W"),
|
||||||
DataItem(DataName.CHARGE_AMP_HOUR, "H", "Ah"),
|
DataItem(DataName.CHARGE_AMP_HOUR, "H", "Ah"),
|
||||||
DataItem(DataName.DISCHARGE_AMP_HOUR, "H", "Ah"),
|
DataItem(DataName.DISCHARGE_AMP_HOUR, "H", "Ah"),
|
||||||
DataItem(DataName.PRODUCTION_ENERGY, "H", "Wh"),
|
DataItem(DataName.PRODUCTION_ENERGY, "H", "Wh"),
|
||||||
DataItem(DataName.CONSUMPTION_ENERGY, "H", "Wh"),
|
DataItem(DataName.CONSUMPTION_ENERGY, "H", "Wh"),
|
||||||
#
|
|
||||||
DataItem(DataName.RUN_DAYS, "H"),
|
DataItem(DataName.RUN_DAYS, "H"),
|
||||||
DataItem(DataName.DISCHARGE_COUNT, "H"),
|
DataItem(DataName.DISCHARGE_COUNT, "H"),
|
||||||
DataItem(DataName.FULL_CHARGE_COUNT, "H"),
|
DataItem(DataName.FULL_CHARGE_COUNT, "H"),
|
||||||
|
@ -121,180 +119,3 @@ HISTORICAL_DATA = [
|
||||||
DataItem(DataName.TOTAL_PRODUCTION_ENERGY, "L", "Wh"),
|
DataItem(DataName.TOTAL_PRODUCTION_ENERGY, "L", "Wh"),
|
||||||
DataItem(DataName.TOTAL_CONSUMPTION_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
103
srnemqtt/srne.py
Normal 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()
|
|
@ -1,18 +1,14 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- 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
|
# Only factor of 1000
|
||||||
SI_PREFIXES_LARGE = "kMGTPEZY"
|
SI_PREFIXES_LARGE = "kMGTPEZY"
|
||||||
SI_PREFIXES_SMALL = "mµnpfazy"
|
SI_PREFIXES_SMALL = "mµnpfazy"
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def humanize_number(data, unit: str = ""):
|
def humanize_number(data, unit: str = ""):
|
||||||
counter = 0
|
counter = 0
|
||||||
|
@ -39,30 +35,9 @@ def humanize_number(data, unit: str = ""):
|
||||||
return f"{data:.3g} {prefix}{unit}"
|
return f"{data:.3g} {prefix}{unit}"
|
||||||
|
|
||||||
|
|
||||||
class LazyJSON:
|
def log(*message: object, **kwargs):
|
||||||
def __init__(self, data):
|
print(datetime.datetime.utcnow().isoformat(" "), *message, **kwargs)
|
||||||
self.data = data
|
sys.stdout.flush()
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
class Periodical:
|
class Periodical:
|
||||||
|
@ -81,9 +56,7 @@ class Periodical:
|
||||||
skipped, overshoot = divmod(now - self.prev, self.interval)
|
skipped, overshoot = divmod(now - self.prev, self.interval)
|
||||||
skipped -= 1
|
skipped -= 1
|
||||||
if skipped:
|
if skipped:
|
||||||
logger.debug(
|
log("Skipped:", skipped, overshoot, now - self.prev, self.interval)
|
||||||
"Skipped:", skipped, overshoot, now - self.prev, self.interval
|
|
||||||
)
|
|
||||||
self.prev = now - overshoot
|
self.prev = now - overshoot
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
|
@ -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"
|
|
6
tox.ini
6
tox.ini
|
@ -1,7 +1,3 @@
|
||||||
[flake8]
|
[flake8]
|
||||||
max-line-length = 120
|
max-line-length = 88
|
||||||
extend-ignore = E203, I201, I101
|
extend-ignore = E203, I201, I101
|
||||||
|
|
||||||
[pytest]
|
|
||||||
pythonpath = .
|
|
||||||
testpaths = tests
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue