initial commit

This commit is contained in:
2026-01-20 18:55:06 +01:00
commit 5956185a1c
6 changed files with 494 additions and 0 deletions

104
src/main.py Normal file
View File

@@ -0,0 +1,104 @@
#!/usr/bin/env python3
import asyncio
import socket
import json
import os
import logging
import datetime
import goodwe
import paho.mqtt.client as mqtt
# -----------------------------
# CONFIGURATION
# -----------------------------
MQTT_BROKER = os.getenv("MQTT_BROKER", "localhost")
MQTT_PORT = int(os.getenv("MQTT_PORT", 1883))
MQTT_TOPIC = os.getenv("MQTT_TOPIC", "goodwe/runtime")
DISCOVERY_INTERVAL = int(os.getenv("DISCOVERY_INTERVAL", 60)) # seconds
BROADCAST_SUBNET = os.getenv("BROADCAST_SUBNET", "192.168.1.255") # directed broadcast
UDP_PORT = 48899
UDP_MSG = b"WIFIKIT-214028-READ"
# -----------------------------
# LOGGING SETUP
# -----------------------------
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s"
)
logger = logging.getLogger("goodwe-daemon")
# -----------------------------
# MQTT CLIENT SETUP
# -----------------------------
mqtt_client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
mqtt_client.connect(MQTT_BROKER, MQTT_PORT)
mqtt_client.loop_start()
# -----------------------------
# HELPER FUNCTIONS
# -----------------------------
async def discover_inverters(timeout=3):
"""Discover inverters using UDP broadcast (non-root)."""
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
sock.settimeout(timeout)
inverters = []
try:
sock.sendto(UDP_MSG, (BROADCAST_SUBNET, UDP_PORT))
while True:
data, addr = sock.recvfrom(1024)
inverters.append(addr[0])
except socket.timeout:
pass
finally:
sock.close()
return list(set(inverters))
async def read_runtime(ip):
"""Connect to a GoodWe inverter and read runtime data."""
try:
inverter = await goodwe.connect(ip)
runtime = await inverter.read_runtime_data()
print(runtime)
return runtime
except Exception as e:
logger.warning(f"Failed to read {ip}: {e}")
return None
def json_serializer(obj):
if isinstance(obj, datetime.datetime):
return obj.isoformat()
raise TypeError(f"Type {type(obj)} not serializable")
async def publish_runtime(ip):
runtime = await read_runtime(ip)
if runtime:
payload = {"ip": ip, "data": runtime}
mqtt_client.publish(MQTT_TOPIC, json.dumps(payload, default=json_serializer))
logger.info(f"Published runtime for {ip}")
# -----------------------------
# MAIN LOOP
# -----------------------------
async def main_loop():
while True:
logger.info("Discovering inverters...")
inverters = await discover_inverters()
if not inverters:
logger.warning("No inverters found")
for ip in inverters:
await publish_runtime(ip)
await asyncio.sleep(DISCOVERY_INTERVAL)
# -----------------------------
# ENTRY POINT
# -----------------------------
if __name__ == "__main__":
try:
asyncio.run(main_loop())
except KeyboardInterrupt:
logger.info("Daemon stopped")
mqtt_client.loop_stop()
mqtt_client.disconnect()