Skip to content
This repository has been archived by the owner on Dec 8, 2024. It is now read-only.

Commit

Permalink
Merge branch 'feature/homewizard' into next
Browse files Browse the repository at this point in the history
  • Loading branch information
Roeland authored and Roeland committed Aug 5, 2024
2 parents d3d702d + ec8dd24 commit af64f45
Show file tree
Hide file tree
Showing 13 changed files with 142 additions and 20 deletions.
1 change: 1 addition & 0 deletions .github/workflows/builder.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ on:
push:
branches:
- main
- next
pull_request:
branches:
- main
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -163,5 +163,5 @@ cython_debug/

.devcontainer
.vscode
settings.json
settings.dev.json
__pycache__/
5 changes: 5 additions & 0 deletions sma/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
<!-- https://developers.home-assistant.io/docs/add-ons/presentation#keeping-a-changelog -->

## 0.1.0

- Catch invalid json messages
- Add homewizard support

## 0.0.10

- Updated docs.
Expand Down
2 changes: 2 additions & 0 deletions sma/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
15 changes: 8 additions & 7 deletions sma/config.yaml
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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"
4 changes: 3 additions & 1 deletion sma/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
dynaconf~=3.2.5
paho-mqtt~=2.1.0
paho-mqtt~=2.1.0
zeroconf~=0.132.2
requests~=2.32.3
3 changes: 3 additions & 0 deletions sma/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"sma_mqtt_topic": "sma/emeter"
}
93 changes: 93 additions & 0 deletions sma/src/homewizard.py
Original file line number Diff line number Diff line change
@@ -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}")

11 changes: 9 additions & 2 deletions sma/src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import util
import mqtt
import udp
import homewizard

def main():
util.setup_logging()
Expand All @@ -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()
Expand Down
13 changes: 6 additions & 7 deletions sma/src/mqtt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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}")

Expand Down Expand Up @@ -66,18 +68,15 @@ 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:
logging.error(f"An unexpected error occurred: {e}")

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

Expand Down
4 changes: 4 additions & 0 deletions sma/src/udp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions sma/src/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
5 changes: 5 additions & 0 deletions sma/translations/en.yaml
Original file line number Diff line number Diff line change
@@ -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.

0 comments on commit af64f45

Please sign in to comment.