# -*- coding: utf-8 -*- """Solar Online push — wysylka metryk falownikow z HA do app.solar-online.pl. HA jest JEDYNYM masterem falownikow Seby (3x Deye przez ha-solarman, Sumry przez xpert-king) — drugi master na tym samym RS232/RS485 psul ramki (encje robily sie unavailable co 90 s, 2026-08-27). Ta appka AppDaemon co 60 s zbiera stany encji i POST-uje je na VPS: polaczenie WYCHODZACE, dziala zza Starlink CGNAT bez tuneli. Serwer: endpoint POST /api/push (kind='ha_push' w bazie). Konfiguracja: apps.yaml (url, key, deye, sumry). Zrodlo prawdy: E:\\SolarBase\\ha\\solaronline_push.py — wgrywane do /config/apps/ addonu AppDaemon przez init_commands (base64). """ import json import urllib.request import appdaemon.plugins.hass.hassapi as hass # encje ha-solarman nosza te same slugi co metryki profili Solar Online DEYE_METRICS = [ "pv1_power", "pv2_power", "battery", "battery_power", "battery_voltage", "battery_current", "battery_temperature", "load_power", "grid_power", "today_production", "temperature", "dc_temperature", ] # sufiks encji xpert-king (Sumry/Voltronic PI30) -> metryka Solar Online VMII = { "grid_voltage": "grid_voltage", "grid_frequency": "grid_frequency", "ac_output_voltage": "ac_output_voltage", "ac_output_frequency": "ac_output_frequency", "ac_output_apparent_power": "ac_output_va", "ac_output_active_power": "load_power", "output_load_percent": "load_percent", "bus_voltage": "bus_voltage", "battery_voltage": "battery_voltage", "battery_charging_current": "battery_charge_current", "battery_level": "battery", "inverter_heat_sink_temperature": "temperature", "pv1_input_voltage": "pv1_voltage", "battery_discharge_current": "battery_discharge_current", "pv1_charging_power": "pv1_power", } class SolarOnlinePush(hass.Hass): def initialize(self): self.url = self.args["url"] self.key = self.args["key"] self.run_every(self.push, "now+15", int(self.args.get("interval", 60))) self.log("Solar Online push aktywny -> " + self.url) def _num(self, entity): st = self.get_state(entity) try: return float(st) except (TypeError, ValueError): return None def push(self, kwargs): devices = [] for serial, prefix in (self.args.get("deye") or {}).items(): m = {} for k in DEYE_METRICS: v = self._num("sensor.%s_%s" % (prefix, k)) if v is not None: m[k] = v pv = [m[k] for k in ("pv1_power", "pv2_power") if k in m] if pv: m["pv_power"] = round(sum(pv), 1) if m: devices.append({"serial": str(serial), "metrics": m}) s = self.args.get("sumry") or {} if s: base = s["prefix"] m = {} for suffix, metric in VMII.items(): v = self._num("sensor.%s_%s" % (base, suffix)) if v is not None: m[metric] = v if "battery_voltage" in m: chg = m.get("battery_charge_current", 0.0) dis = m.get("battery_discharge_current", 0.0) # konwencja panelu (jak Deye): DODATNIA moc = ROZLADOWANIE m["battery_power"] = round((dis - chg) * m["battery_voltage"], 1) pw, pvv = m.get("pv1_power", 0.0), m.get("pv1_voltage", 0.0) m["pv1_current"] = round(pw / pvv, 1) if pvv > 1 else 0.0 if "pv1_power" in m: m["pv_power"] = m["pv1_power"] m["grid_power"] = 0.0 # PI30 nie mierzy mocy sieci mode = self.get_state("sensor.%s_device_mode" % base) m["on_line_mode"] = 1.0 if mode == "Line" else 0.0 # gotowy licznik dobowy kWh (utility_meter w HA) daily = self._num(s.get("daily_entity", "")) if daily is not None: m["today_production"] = daily if m: devices.append({"serial": str(s["serial"]), "metrics": m}) if not devices: self.log("solar-online push: brak danych (encje niedostepne?)", level="WARNING") return body = json.dumps({"key": self.key, "devices": devices}).encode() req = urllib.request.Request( self.url, data=body, headers={"Content-Type": "application/json"}) try: with urllib.request.urlopen(req, timeout=20) as r: resp = json.loads(r.read().decode()) if not resp.get("ok") or resp.get("skipped"): self.log("solar-online push: %s" % resp, level="WARNING") except Exception as e: self.log("solar-online push blad: %s" % e, level="WARNING")