From ec8dd24b92a1e1d7247ff6737ba6cf0089a2ba27 Mon Sep 17 00:00:00 2001 From: Roeland Date: Mon, 5 Aug 2024 19:45:29 +0200 Subject: [PATCH] Implementation homewizard --- .github/workflows/builder.yaml | 1 + .gitignore | 2 +- sma/CHANGELOG.md | 5 ++ sma/Dockerfile | 2 + sma/config.yaml | 15 +++--- sma/requirements.txt | 4 +- sma/settings.json | 3 ++ sma/src/homewizard.py | 93 ++++++++++++++++++++++++++++++++++ sma/src/main.py | 11 +++- sma/src/mqtt.py | 13 +++-- sma/src/udp.py | 4 ++ sma/src/util.py | 4 +- sma/translations/en.yaml | 5 ++ 13 files changed, 142 insertions(+), 20 deletions(-) create mode 100644 sma/settings.json create mode 100644 sma/src/homewizard.py diff --git a/.github/workflows/builder.yaml b/.github/workflows/builder.yaml index 4fd6bd7..f11bf0d 100644 --- a/.github/workflows/builder.yaml +++ b/.github/workflows/builder.yaml @@ -8,6 +8,7 @@ on: push: branches: - main + - next pull_request: branches: - main diff --git a/.gitignore b/.gitignore index 21eadf1..4408765 100644 --- a/.gitignore +++ b/.gitignore @@ -163,5 +163,5 @@ cython_debug/ .devcontainer .vscode -settings.json +settings.dev.json __pycache__/ \ No newline at end of file diff --git a/sma/CHANGELOG.md b/sma/CHANGELOG.md index 5c6da43..05a46f8 100644 --- a/sma/CHANGELOG.md +++ b/sma/CHANGELOG.md @@ -1,5 +1,10 @@ +## 0.1.0 + +- Catch invalid json messages +- Add homewizard support + ## 0.0.10 - Updated docs. diff --git a/sma/Dockerfile b/sma/Dockerfile index 7d82771..375f3b0 100644 --- a/sma/Dockerfile +++ b/sma/Dockerfile @@ -10,6 +10,8 @@ RUN wget --no-cache https://raw.githubusercontent.com/Roeland54/SMA-Energy-Meter # install dependencies RUN pip install -r requirements.txt --break-system-packages +RUN pip install zeroconf requests --break-system-packages + COPY / . RUN chmod a+x /src/run.sh diff --git a/sma/config.yaml b/sma/config.yaml index 1196115..3fdec69 100644 --- a/sma/config.yaml +++ b/sma/config.yaml @@ -1,6 +1,6 @@ name: "SMA Energy Meter emulator" description: "Simulate one or more SMA energy meters based on mqtt messages." -version: "0.0.10" +version: "rc-0.0.11" slug: sma url: "https://github.com/Roeland54/SMA-Energy-Meter-emulator" arch: @@ -19,15 +19,16 @@ options: port: "auto_port" username: "auto_user" password: "auto_password" - debug_logging: false - disable_logging: false schema: - enable_mqtt: bool + enable_mqtt: bool? mqtt: broker: str - port: str + port: str? username: str? password: str? - debug_logging: bool - disable_logging: bool + enable_homewizard: bool? + homewizard_destination_addresses: + - match(^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$)? + debug_logging: bool? + disable_logging: bool? image: "ghcr.io/roeland54/{arch}-sma-energy-meter-emulator" diff --git a/sma/requirements.txt b/sma/requirements.txt index 6f904da..3b28c7f 100644 --- a/sma/requirements.txt +++ b/sma/requirements.txt @@ -1,2 +1,4 @@ dynaconf~=3.2.5 -paho-mqtt~=2.1.0 \ No newline at end of file +paho-mqtt~=2.1.0 +zeroconf~=0.132.2 +requests~=2.32.3 \ No newline at end of file diff --git a/sma/settings.json b/sma/settings.json new file mode 100644 index 0000000..101cf1e --- /dev/null +++ b/sma/settings.json @@ -0,0 +1,3 @@ +{ + "sma_mqtt_topic": "sma/emeter" +} \ No newline at end of file diff --git a/sma/src/homewizard.py b/sma/src/homewizard.py new file mode 100644 index 0000000..37c38fb --- /dev/null +++ b/sma/src/homewizard.py @@ -0,0 +1,93 @@ +from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange +import socket +from config import settings +import logging +import requests +from emeter import emeterPacket +import time +import json +import random + +def setup_homewizard(userdata): + if settings.get("enable_homewizard", False) is False: + return None + + zeroconf = Zeroconf() + browser = ServiceBrowser(zeroconf, "_hwenergy._tcp.local.", handlers=[lambda zeroconf, service_type, name, state_change: on_service_state_change(zeroconf, service_type, name, state_change, userdata)]) + random.seed(42) + for ip in settings.get("homewizard_manual_addresses", []): + settings['ip_serial_numbers'][ip] = (ip, hash(ip)) + +def on_service_state_change(zeroconf, service_type, name, state_change, userdata): + if state_change is ServiceStateChange.Added: + info = zeroconf.get_service_info(service_type, name) + if info: + hostname = socket.inet_ntoa(info.address) + if hostname.startswith("p1meter") or hostname.startswith("kwhmeter"): + random.seed(42) + serial_number = hash(hostname) + logging.info(f"Found Homewizard meter with hostname: {hostname}, assigned serial number: {serial_number}") + with userdata['lock']: + userdata['homewizard_meters'][hostname] = (hostname + ".local", serial_number) + +def update_homewizard(userdata): + if settings.get("enable_homewizard", False) is False or (len(userdata['homewizard_meters']) == 0 and len(settings.get("homewizard_manual_addresses", [])) == 0): + return None + + try: + with userdata['lock']: + hostnames = userdata['homewizard_meters'] + + for (hostname, serial_number) in hostnames.items() + settings.get("ip_serial_numbers", []).items(): + # Perform the GET request + response = requests.get(f'http://{hostname}/api/v1/data') + + # Raise an exception for HTTP errors + response.raise_for_status() + + # Parse the JSON response + data = response.json() + logging.debug(f"Message data: {data}") + + # Create a packet instance + packet = emeterPacket(int(serial_number)) + packet.begin(int(time.time() * 1000)) + + # Extract values from the JSON data and add them to the packet + # Process active power values + active_power = data['active_power_w'] + if active_power > 0: + packet.addMeasurementValue(emeterPacket.SMA_POSITIVE_ACTIVE_POWER, round(active_power * 10)) + packet.addMeasurementValue(emeterPacket.SMA_NEGATIVE_ACTIVE_POWER, 0) + else: + packet.addMeasurementValue(emeterPacket.SMA_POSITIVE_ACTIVE_POWER, 0) + packet.addMeasurementValue(emeterPacket.SMA_NEGATIVE_ACTIVE_POWER, round(active_power * -10)) # Sending absolute value for negative + + packet.addMeasurementValue(emeterPacket.SMA_POSITIVE_REACTIVE_POWER, 0) + packet.addMeasurementValue(emeterPacket.SMA_NEGATIVE_REACTIVE_POWER, 0) + + # Sum the total energy imports (t1 and t2) + total_power_import_kwh = data['total_power_import_t1_kwh'] + data['total_power_import_t2_kwh'] + packet.addCounterValue(emeterPacket.SMA_POSITIVE_ENERGY, round(total_power_import_kwh * 1000 * 3600)) + + # Sum the total energy exports (t1 and t2) + total_power_export_kwh = data['total_power_export_t1_kwh'] + data['total_power_export_t2_kwh'] + packet.addCounterValue(emeterPacket.SMA_NEGATIVE_ENERGY, round(total_power_export_kwh * 1000 * 3600)) + + packet.end() + + # Get packet data + packet_data = packet.getData()[:packet.getLength()] + destination_addresses = settings.get("homewizard_destination_addresses", []) + + with userdata['lock']: + userdata['packets'][serial_number] = (packet_data, destination_addresses) + logging.info(f"Updated packet for serial number {serial_number}") + + except requests.RequestException as e: + logging.error(f"HTTP Request failed: {e}") + except json.JSONDecodeError as e: + logging.error(f"Failed to decode JSON payload: {e}") + except Exception as e: + logging.error(f"An unexpected error occurred: {e}") + diff --git a/sma/src/main.py b/sma/src/main.py index f280258..376e9c6 100644 --- a/sma/src/main.py +++ b/sma/src/main.py @@ -2,6 +2,7 @@ import util import mqtt import udp +import homewizard def main(): util.setup_logging() @@ -10,17 +11,23 @@ def main(): 'packets': {}, 'lock': threading.Lock(), 'udp_address': '239.12.255.254', - 'udp_port': 9522 + 'udp_port': 9522, + 'homewizard_meters': {} } threads=[] mqtt_thread = mqtt.setup_mqtt(userdata) + homewizard.setup_homewizard(userdata) + if mqtt_thread is not None: threads.append(mqtt_thread) - udp.setup_udp(userdata) + udp_thread = udp.setup_udp(userdata) + + if udp_thread is not None: + threads.append(udp_thread) for thread in threads: thread.join() diff --git a/sma/src/mqtt.py b/sma/src/mqtt.py index 3bb762c..91e6f16 100644 --- a/sma/src/mqtt.py +++ b/sma/src/mqtt.py @@ -8,7 +8,7 @@ from emeter import emeterPacket def setup_mqtt(userdata): - if settings["enable_mqtt"] is False: + if settings.get("enable_mqtt", False) is False: return None set_mqtt_settings() @@ -34,7 +34,9 @@ def setup_mqtt(userdata): def on_connect(client, userdata, flags, rc, properties=None): if rc == 0: logging.info("Connected to MQTT broker") - client.subscribe("sma/emeter/+/state") + topic = settings["sma_mqtt_topic"] + "/+/state" + client.subscribe(topic) + logging.info(f"Subscribed to topic : \"{topic}\"") else: logging.error(f"Failed to connect, return code {rc}") @@ -66,7 +68,7 @@ def on_message(client, userdata, msg): with userdata['lock']: userdata['packets'][serial_number] = (packet_data, destination_addresses) logging.info(f"Updated packet for serial number {serial_number}") - + except json.JSONDecodeError as e: logging.error(f"Failed to decode JSON payload: {e}") except Exception as e: @@ -74,10 +76,7 @@ def on_message(client, userdata, msg): def set_mqtt_settings(): if os.environ.get("IS_HA_ADDON"): - if settings["mqtt"]["broker"] != "auto_broker" \ - or settings["mqtt"]["port"] != "auto_port" \ - or settings["mqtt"]["username"] != "auto_user" \ - or settings["mqtt"]["password"] != "auto_password": + if settings["mqtt"]["broker"] != "auto_broker": # If settings were manually set, use the manually set settings return None diff --git a/sma/src/udp.py b/sma/src/udp.py index cb4e56e..023ecd1 100644 --- a/sma/src/udp.py +++ b/sma/src/udp.py @@ -2,17 +2,21 @@ import socket import logging import threading +import homewizard def setup_udp(userdata): udp_thread = threading.Thread(target=udp_sender, args=(userdata,)) udp_thread.daemon = True udp_thread.start() + return udp_thread def udp_sender(userdata): udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) udp_socket.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 32) while True: + homewizard.update_homewizard(userdata) + with userdata['lock']: for serial_number, (packet_data, destination_addresses) in userdata['packets'].items(): if destination_addresses: diff --git a/sma/src/util.py b/sma/src/util.py index 304d3ff..cc63f72 100644 --- a/sma/src/util.py +++ b/sma/src/util.py @@ -26,9 +26,9 @@ def setup_logging(): logger = logging.getLogger() logger.setLevel(logging.INFO) if "debug_logging" in config.settings: - if config.settings["debug_logging"]: + if config.settings.get("debug_logging", False): logger.setLevel(logging.DEBUG) if "disable_logging" in config.settings: - if config.settings["disable_logging"]: + if config.settings.get("disable_logging", False): logger.setLevel(logging.ERROR) diff --git a/sma/translations/en.yaml b/sma/translations/en.yaml index 61f29d7..c28492e 100644 --- a/sma/translations/en.yaml +++ b/sma/translations/en.yaml @@ -1,4 +1,9 @@ configuration: + enable_homewizard: + name: Enable Homewizard meters + description: If a homewizard meter is found on the network a sma meter will be automatically added. + enable_mqtt: + name: Enable listening to MQTT mqtt: name: MQTT Broker settings description: Leave the settings as they are if you are using the MQTT Mosquitto Addon. \ No newline at end of file