From 50978111c5c569c2d421300663ebe3e882747e3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Odd=20Str=C3=A5b=C3=B8?= Date: Sun, 14 Nov 2021 04:46:17 +0100 Subject: [PATCH] Hook MQTT up to the live data feed --- .gitignore | 3 ++ consumers/mqtt.py | 14 +++-- solar_ble.py | 131 +++++++++++++++++++++++++++------------------- test_config.py | 60 ++++++++++++--------- 4 files changed, 126 insertions(+), 82 deletions(-) diff --git a/.gitignore b/.gitignore index 19eddcf..63a433d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ *.py[ocd] +.mypy_cache/ +__pycache__/ + *.log *.zip diff --git a/consumers/mqtt.py b/consumers/mqtt.py index f7fa483..38f54b6 100644 --- a/consumers/mqtt.py +++ b/consumers/mqtt.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import json from typing import Any, Dict, List, Optional from uuid import uuid4 @@ -34,11 +35,13 @@ MAP_VALUES: Dict[str, Dict[str, Any]] = { "unit": "Wh", "type": "energy", "state_class": "total_increasing", + "expiry": 180, }, "total_consumption_power": { "unit": "Wh", "type": "energy", "state_class": "total_increasing", + "expiry": 180, }, # "battery_charge": {"unit": "%", "type": "battery", "state_class": "measurement"}, @@ -128,6 +131,7 @@ class MqttConsumer(BaseConsumer): # 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", }, "force_update": True, "expire_after": expiry, @@ -140,6 +144,8 @@ class MqttConsumer(BaseConsumer): if state_class: res["state_class"] = state_class + 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): @@ -162,15 +168,17 @@ class MqttConsumer(BaseConsumer): return super().poll() def write(self, data: Dict[str, Any]): - self.client.publish(f"{self.topic_prefix}/raw", payload=data) + self.client.publish(f"{self.topic_prefix}/raw", payload=json.dumps(data)) for k, v in data.items(): if k in MAP_VALUES: if k not in self.initialized: pretty_name = k.replace("_", " ").capitalize() self.client.publish( - f"{self.settings['discovery_prefix']}/sensor/{self.settings['device_id']}/config", # noqa: E501 - self.get_ha_config(k, pretty_name, **MAP_VALUES[k]), + f"{self.settings['discovery_prefix']}/sensor/{self.settings['device_id']}_{k}/config", # noqa: E501 + payload=json.dumps( + self.get_ha_config(k, pretty_name, **MAP_VALUES[k]) + ), retain=True, ) self.initialized.append(k) diff --git a/solar_ble.py b/solar_ble.py index 138225a..0ff0c0f 100755 --- a/solar_ble.py +++ b/solar_ble.py @@ -12,6 +12,7 @@ from bluepy import btle from libscrc import modbus from feasycom_ble import BTLEUart +from test_config import get_config, get_consumers MAC = "DC:0D:30:9C:61:BA" @@ -366,68 +367,92 @@ class Periodical: if __name__ == "__main__": + conf = get_config() + consumers = get_consumers(conf) per_voltages = Periodical(interval=15) per_current_hist = Periodical(interval=60) - while True: - try: - log("Connecting...") - with BTLEUart(MAC, timeout=10) as dev: - log("Connected.") + try: + while True: + try: + log("Connecting...") + with BTLEUart(MAC, timeout=10) as dev: + log("Connected.") - # write(dev, construct_request(0, 32)) + # 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)) - days = 7 - res = readMemory(dev, 0x010B, 21) - if res: - d = parse_historical_entry(res) - log(d) - days = cast(int, d.get("run_days", 7)) - - for i in range(days): - res = readMemory(dev, 0xF000 + i, 10) + # Memory dump + # for address in range(0, 0x10000, 16): + # log(f"Reading 0x{address:04X}...") + # write(wd, construct_request(address, 16)) + days = 7 + res = readMemory(dev, 0x010B, 21) if res: d = parse_historical_entry(res) - log({i: d}) + log(d) + for consumer in consumers: + consumer.write(d) + days = cast(int, d.get("run_days", 7)) - while True: - now = time.time() - if per_voltages(now): - # CMD_GET_BATTERY_STATE + CMD_GET_PANEL_STATUS - res = readMemory(dev, 0x0100, 11) + for i in range(days): + res = readMemory(dev, 0xF000 + i, 10) if res: - try: - d = parse_battery_state(res) - log(d) - except struct.error as e: - log(e) - log("0x0100 Unpack error:", len(res), res) - log("Flushed from read buffer; ", dev.read(timeout=0.5)) - if per_current_hist(now): - res = readMemory(dev, 0x010B, 21) - if res: - try: - d = parse_historical_entry(res) - log(d) - except struct.error as e: - log(e) - log("0x010B Unpack error:", len(res), res) - log("Flushed from read buffer; ", dev.read(timeout=0.5)) - # print(".") - time.sleep(1) + d = parse_historical_entry(res) + log({i: d}) + for consumer in consumers: + consumer.write({str(i): d}) - # if STATUS.get('load_enabled'): - # write(wd, CMD_DISABLE_LOAD) - # else: - # write(wd, CMD_ENABLE_LOAD) + while True: + now = time.time() + if per_voltages(now): + # CMD_GET_BATTERY_STATE + CMD_GET_PANEL_STATUS + res = readMemory(dev, 0x0100, 11) + if res: + try: + d = parse_battery_state(res) + log(d) + for consumer in consumers: + consumer.write(d) + except struct.error as e: + log(e) + log("0x0100 Unpack error:", len(res), res) + log( + "Flushed from read buffer; ", + dev.read(timeout=0.5), + ) + if per_current_hist(now): + res = readMemory(dev, 0x010B, 21) + if res: + try: + d = parse_historical_entry(res) + log(d) + for consumer in consumers: + consumer.write(d) + except struct.error as e: + log(e) + log("0x010B Unpack error:", len(res), res) + log( + "Flushed from read buffer; ", + dev.read(timeout=0.5), + ) + # print(".") + for consumer in consumers: + consumer.poll() + time.sleep(max(0, 1 - time.time() - now)) - except btle.BTLEDisconnectError: - log("ERROR: Disconnected") - time.sleep(1) - except KeyboardInterrupt: - break + # if STATUS.get('load_enabled'): + # write(wd, CMD_DISABLE_LOAD) + # else: + # write(wd, CMD_ENABLE_LOAD) + + except btle.BTLEDisconnectError: + log("ERROR: Disconnected") + time.sleep(1) + + except (KeyboardInterrupt, SystemExit, Exception) as e: + for consumer in consumers: + consumer.exit() + + if type(e) is not KeyboardInterrupt: + raise diff --git a/test_config.py b/test_config.py index a0f9e3f..e555bb7 100644 --- a/test_config.py +++ b/test_config.py @@ -2,7 +2,7 @@ import importlib import os from time import sleep -from typing import Any, Dict, Optional, Type +from typing import Any, Dict, List, Optional, Type import yaml @@ -14,8 +14,8 @@ def get_consumer(name: str) -> Optional[Type[BaseConsumer]]: mod = importlib.import_module(f"consumers.{mod_name}") - print(mod) - print(dir(mod)) + # print(mod) + # print(dir(mod)) res = getattr(mod, cls_name) assert issubclass(res, BaseConsumer) @@ -36,29 +36,37 @@ def write_config(conf: Dict[str, Any]): os.rename(".config.yaml~writing", "config.yaml") -conf = get_config() +def get_consumers(conf: Optional[Dict[str, Any]] = None) -> List[BaseConsumer]: + if conf is None: + conf = get_config() -consumers = [] -for name, consumer_config in conf["consumers"].items(): - print(name, consumer_config) - mod = get_consumer(name) - if mod: - print(mod) - consumers.append(mod(consumer_config)) - -write_config(conf) - - -try: - while True: - for consumer in consumers: - consumer.poll() - sleep(1) -except (KeyboardInterrupt, SystemExit, Exception) as e: - for consumer in consumers: - consumer.exit() - - if type(e) is not KeyboardInterrupt: - raise + consumers = [] + for name, consumer_config in conf["consumers"].items(): + # print(name, consumer_config) + mod = get_consumer(name) + if mod: + # print(mod) + consumers.append(mod(consumer_config)) write_config(conf) + return consumers + + +if __name__ == "__main__": + conf = get_config() + + consumers = get_consumers(conf) + + try: + while True: + for consumer in consumers: + consumer.poll() + sleep(1) + except (KeyboardInterrupt, SystemExit, Exception) as e: + for consumer in consumers: + consumer.exit() + + if type(e) is not KeyboardInterrupt: + raise + + write_config(conf)