adjust polling during daytime, retries with backoff and authentication

This commit is contained in:
2026-02-04 20:14:42 +01:00
parent ccb4ed2f5b
commit 3bebea717d

View File

@@ -4,7 +4,10 @@ import socket
import json import json
import os import os
import logging import logging
import datetime from datetime import datetime, timedelta, timezone
from zoneinfo import ZoneInfo
import backoff
from suntime import Sun
import goodwe import goodwe
import paho.mqtt.client as mqtt import paho.mqtt.client as mqtt
@@ -16,106 +19,155 @@ MQTT_PORT = int(os.getenv("MQTT_PORT", 1883))
MQTT_TOPIC = os.getenv("MQTT_TOPIC", "goodwe") MQTT_TOPIC = os.getenv("MQTT_TOPIC", "goodwe")
MQTT_USER = os.getenv("MQTT_USER", "goodwe") MQTT_USER = os.getenv("MQTT_USER", "goodwe")
MQTT_PASSWORD = os.getenv("MQTT_PASSWORD", "goodwe") MQTT_PASSWORD = os.getenv("MQTT_PASSWORD", "goodwe")
DISCOVERY_INTERVAL = int(os.getenv("DISCOVERY_INTERVAL", 60)) # seconds POLLING_INTERVAL = int(os.getenv("POLLING_INTERVAL", 20))
BROADCAST_SUBNET = os.getenv("BROADCAST_SUBNET", "192.168.1.255") # directed broadcast DISCOVERY_INTERVAL = int(os.getenv("DISCOVERY_INTERVAL", 60))
BROADCAST_SUBNET = os.getenv("BROADCAST_SUBNET", "192.168.1.255")
LATITUDE = float(os.getenv("LATITUDE", 52.3676))
LONGITUDE = float(os.getenv("LONGITUDE", 4.9041))
LOCAL_TZ = ZoneInfo(os.getenv("TIMEZONE", "Europe/Amsterdam"))
UDP_PORT = 48899 UDP_PORT = 48899
UDP_MSG = b"WIFIKIT-214028-READ" UDP_MSG = b"WIFIKIT-214028-READ"
# ----------------------------- # -----------------------------
# LOGGING SETUP # LOGGING HELPER
# ----------------------------- # -----------------------------
logging.basicConfig( class MqttLogger:
level=logging.INFO, """Helper to log to both standard logger and MQTT subtopics."""
format="%(asctime)s [%(levelname)s] %(message)s" def __init__(self, base_topic, mqtt_client):
) self.logger = logging.getLogger("goodwe-daemon")
logger = logging.getLogger("goodwe-daemon") self.client = mqtt_client
self.base_topic = f"{base_topic}/logs"
def _log(self, level, msg):
# Standard Logging
getattr(self.logger, level)(msg)
# MQTT Publishing (if connected)
if self.client.is_connected():
self.client.publish(f"{self.base_topic}/{level}", msg)
def debug(self, msg): self._log("debug", msg)
def info(self, msg): self._log("info", msg)
def warning(self, msg): self._log("warning", msg)
def error(self, msg): self._log("error", msg)
# Global Logger Instance (initialized after MQTT client)
mqtt_log = None
# ----------------------------- # -----------------------------
# MQTT CLIENT SETUP # LOGIC HELPERS
# ----------------------------- # -----------------------------
def is_operational_hours():
sun = Sun(LATITUDE, LONGITUDE)
now_utc = datetime.now(timezone.utc)
try:
sr = sun.get_sunrise_time().replace(tzinfo=timezone.utc)
ss = sun.get_sunset_time().replace(tzinfo=timezone.utc)
# Windows: 2 hours before sunrise until 2 hours after sunset
start_window = sr - timedelta(hours=2)
end_window = ss + timedelta(hours=2)
def on_connect(client, userdata, flags, reason_code, properties): return start_window <= now_utc <= end_window
if reason_code == "Success": except Exception as e:
logger.info("MQTT connected and authenticated") if mqtt_log: mqtt_log.error(f"Error calculating sun times: {e}")
else: return True
raise RuntimeError(f"MQTT connect/auth failed: {reason_code}")
mqtt_client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
mqtt_client.username_pw_set(
username=MQTT_USER,
password=MQTT_PASSWORD,
)
mqtt_client.on_connect = on_connect
mqtt_client.connect(MQTT_BROKER, MQTT_PORT)
mqtt_client.loop_start()
# -----------------------------
# HELPER FUNCTIONS
# -----------------------------
async def discover_inverters(timeout=3): async def discover_inverters(timeout=3):
"""Discover inverters using UDP broadcast (non-root)."""
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
sock.settimeout(timeout) sock.settimeout(timeout)
inverters = [] found_ips = []
try: try:
sock.sendto(UDP_MSG, (BROADCAST_SUBNET, UDP_PORT)) sock.sendto(UDP_MSG, (BROADCAST_SUBNET, UDP_PORT))
while True: while True:
data, addr = sock.recvfrom(1024) data, addr = sock.recvfrom(1024)
inverters.append(addr[0]) found_ips.append(addr[0])
except socket.timeout: except socket.timeout:
pass pass
finally: finally:
sock.close() sock.close()
return list(set(inverters)) return list(set(found_ips))
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}
result = mqtt_client.publish(MQTT_TOPIC, json.dumps(payload, default=json_serializer))
if result.rc != mqtt.MQTT_ERR_SUCCESS:
raise logger.warning(f"Publish failed: {result.rc}")
logger.info(f"Published runtime for {ip}")
# ----------------------------- # -----------------------------
# MAIN LOOP # MAIN LOOP
# ----------------------------- # -----------------------------
async def main_loop(): def on_connect(client, userdata, flags, reason_code, properties):
while True: if reason_code == "Success":
logger.info("Discovering inverters...") logging.info("MQTT connected and authenticated")
inverters = await discover_inverters() else:
if not inverters: logging.error(f"MQTT connect/auth failed: {reason_code}")
logger.warning("No inverters found")
for ip in inverters: async def main_loop():
await publish_runtime(ip) global mqtt_log
await asyncio.sleep(DISCOVERY_INTERVAL)
# Standard logging setup for initial boot
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
# MQTT Setup
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
client.username_pw_set(username=MQTT_USER, password=MQTT_PASSWORD)
client.on_connect = on_connect
client.connect(MQTT_BROKER, MQTT_PORT)
client.loop_start()
# Initialize our helper
mqtt_log = MqttLogger(MQTT_TOPIC, client)
active_inverter = None
inverter_ip = None
# Redefine backoff here to use the mqtt_log
@backoff.on_exception(backoff.expo, Exception, max_tries=5,
on_backoff=lambda details: mqtt_log.warning(f"Retrying connection... {details['wait']:.1f}s"))
async def connect_with_retry(ip):
return await goodwe.connect(ip)
while True:
if not is_operational_hours():
mqtt_log.info("Outside operational hours (Night). Sleeping 10m.")
active_inverter = None
await asyncio.sleep(600)
continue
if active_inverter is None:
mqtt_log.info("Starting discovery...")
ips = await discover_inverters()
if ips:
inverter_ip = ips[0]
try:
active_inverter = await connect_with_retry(inverter_ip)
mqtt_log.info(f"Connected to inverter at {inverter_ip}")
except Exception as e:
mqtt_log.error(f"Discovery failed to bind to {inverter_ip}: {e}")
active_inverter = None
else:
mqtt_log.debug(f"No inverters found. Retrying in {DISCOVERY_INTERVAL}s")
await asyncio.sleep(DISCOVERY_INTERVAL)
continue
try:
runtime_data = await active_inverter.read_runtime_data()
payload = {
"timestamp": datetime.now(LOCAL_TZ).isoformat(),
"inverter_ip": inverter_ip,
"sensors": {k: v for k, v in runtime_data.items()}
}
client.publish(MQTT_TOPIC, json.dumps(payload, default=str))
mqtt_log.debug(f"Data published for {inverter_ip}")
await asyncio.sleep(POLLING_INTERVAL)
except Exception as e:
mqtt_log.warning(f"Inverter {inverter_ip} lost connection: {e}")
active_inverter = None
await asyncio.sleep(5)
# -----------------------------
# ENTRY POINT
# -----------------------------
if __name__ == "__main__": if __name__ == "__main__":
try: try:
asyncio.run(main_loop()) asyncio.run(main_loop())
except KeyboardInterrupt: except KeyboardInterrupt:
logger.info("Daemon stopped") if mqtt_log: mqtt_log.info("Daemon stopping via KeyboardInterrupt")
mqtt_client.loop_stop()
mqtt_client.disconnect()