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