Compare commits

...
3 Commits
Author SHA1 Message Date
retr0andClaude Sonnet 5 789e25967a ShineBridge v1.10.3 — Fix: HA-Entity-Anbindung auf Long-Lived Token umgestellt (host_network verhindert SUPERVISOR_TOKEN)
homeassistant_api: true funktioniert nicht, solange host_network: true gesetzt
ist — der Container ist dann nicht im internen Supervisor-Netzwerk und bekommt
keinen SUPERVISOR_TOKEN injiziert (bestätigt per Diagnose-Log: keine TOKEN/
SUPERVISOR/HASSIO-Env-Var im Container vorhanden).

Statt host_network anzufassen (Risiko für Modbus/Goodwe-UDP): HAEntityReader
nutzt jetzt ein normales, manuell in HA erstelltes Long-Lived Access Token
gegen http://localhost:8123/api — funktioniert dank host_network direkt ohne
Umweg über den Supervisor-Proxy. homeassistant_api wieder aus config.yaml
entfernt (unnötig).

Neue Felder im Geräte-Dialog: 'Home Assistant URL' (Default localhost:8123)
und 'Long-Lived Access Token'.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 16:26:57 +02:00
retr0andClaude Sonnet 5 1c787349e3 ShineBridge v1.10.2 — Diagnose: Env-Var-Dump wenn SUPERVISOR_TOKEN fehlt
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 16:22:34 +02:00
retr0andClaude Sonnet 5 1b78857b3d ShineBridge v1.10.1 — Fix: Config-Import scheiterte an Geräten ohne modbus_ip (ha_entity-Typ)
/api/import-config verlangte bisher zwingend modbus_ip fuer jedes Geraet in der
importierten Config — der neue GROWATT_WIFI_X-Typ (ha_entity) hat aber keins,
nutzt stattdessen ha_entity_prefix. Import schlug dadurch mit 400 fehl, sobald
so ein Geraet enthalten war. Validierung jetzt protokollabhaengig.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 16:11:13 +02:00
4 changed files with 40 additions and 17 deletions
+1 -2
View File
@@ -1,5 +1,5 @@
name: ShineBridge
version: "1.10.0"
version: "1.10.3"
slug: shinebridge
description: Energiemanagement für Growatt/Goodwe-Wechselrichter — PV-Überschussladen, Wallbox-EMS, Tarifvergleich, Zigbee2MQTT-Überschussgeräte
url: https://gitea.bitfire.work/retr0/shinebridge
@@ -13,7 +13,6 @@ host_network: true
ingress: true
ingress_port: 8099
panel_icon: mdi:solar-power
homeassistant_api: true
options:
mqtt_broker: "core-mosquitto"
mqtt_port: 1883
+11 -10
View File
@@ -1,6 +1,5 @@
import json
import logging
import os
import urllib.error
import urllib.request
from typing import Dict, Optional
@@ -29,22 +28,24 @@ class HAEntityReader:
(z.B. ein per ESPHome direkt in HA eingebundener ShineWifi-X-Stick) statt sie
selbst per Modbus/UDP zu pollen.
Braucht 'homeassistant_api: true' in config.yaml — HAOS injiziert dann
SUPERVISOR_TOKEN und die interne API ist unter http://supervisor/core/api
erreichbar (kein manuell erstelltes Long-Lived Token nötig)."""
Nutzt bewusst ein normales Long-Lived Access Token gegen die HA-REST-API statt
den Supervisor-Proxy (http://supervisor/core/api): ShineBridge läuft mit
host_network: true, dadurch ist der Container NICHT im internen Supervisor-
Netzwerk und bekommt keinen SUPERVISOR_TOKEN injiziert. Der direkte API-Weg
funktioniert trotzdem, weil HA Core dank host_network ganz normal unter
localhost erreichbar ist."""
BASE_URL = "http://supervisor/core/api"
def __init__(self, entity_prefix: str):
def __init__(self, entity_prefix: str, base_url: str, token: str):
self.entity_prefix = entity_prefix
self.token = os.environ.get("SUPERVISOR_TOKEN", "")
self.base_url = (base_url or "http://localhost:8123").rstrip("/")
self.token = token
def read(self, inverter=None) -> Optional[Dict[str, float]]:
if not self.entity_prefix:
log.error("[HA-Entity] Kein Entity-Prefix konfiguriert")
return None
if not self.token:
log.error("[HA-Entity %s] Kein SUPERVISOR_TOKEN — 'homeassistant_api: true' in config.yaml gesetzt und Add-on neu installiert?", self.entity_prefix)
log.error("[HA-Entity %s] Kein Long-Lived Access Token konfiguriert", self.entity_prefix)
return None
result: Dict[str, float] = {}
@@ -58,7 +59,7 @@ class HAEntityReader:
def _get_state(self, entity_id: str) -> Optional[float]:
req = urllib.request.Request(
f"{self.BASE_URL}/states/{entity_id}",
f"{self.base_url}/api/states/{entity_id}",
headers={"Authorization": f"Bearer {self.token}"},
)
try:
+15 -4
View File
@@ -280,7 +280,11 @@ def _poll_loop(inv_cfg: Dict[str, Any], stop: threading.Event):
log.info("[%s] Poll-Loop: %s @ %s (Goodwe UDP/8899) alle %ds",
inv_id, inverter.name, host, interval)
elif inverter.protocol == "ha_entity":
reader = HAEntityReader(entity_prefix=inv_cfg.get("ha_entity_prefix", ""))
reader = HAEntityReader(
entity_prefix=inv_cfg.get("ha_entity_prefix", ""),
base_url=inv_cfg.get("ha_base_url", "http://localhost:8123"),
token=inv_cfg.get("ha_token", ""),
)
log.info("[%s] Poll-Loop: %s (HA-Entity Prefix: %s) alle %ds",
inv_id, inverter.name, inv_cfg.get("ha_entity_prefix", ""), interval)
elif inverter.protocol == "kathrein":
@@ -837,10 +841,17 @@ def api_import_config():
if not isinstance(inverters, list):
return jsonify({"error": "inverters ungültig"}), 400
for inv in inverters:
if not inv.get("modbus_ip"):
model_id = inv.get("inverter_model")
if model_id not in INVERTERS:
return jsonify({"error": f"Unbekanntes Modell: {model_id}"}), 400
protocol = INVERTERS[model_id].protocol
# modbus_ip wird als generisches Host-Feld fuer alle IP-basierten Protokolle
# wiederverwendet (modbus, goodwe_udp, kathrein) — nur ha_entity braucht keins.
if protocol == "ha_entity":
if not inv.get("ha_entity_prefix"):
return jsonify({"error": f"ha_entity_prefix fehlt in Gerät {inv.get('name', '?')}"}), 400
elif not inv.get("modbus_ip"):
return jsonify({"error": f"modbus_ip fehlt in Gerät {inv.get('name', '?')}"}), 400
if inv.get("inverter_model") not in INVERTERS:
return jsonify({"error": f"Unbekanntes Modell: {inv.get('inverter_model')}"}), 400
with State.lock:
if mqtt := data.get("mqtt"):
+13 -1
View File
@@ -346,7 +346,13 @@
<div id="ha-entity-fields" style="display:none">
<div class="field"><label>HA Entity-Prefix</label>
<input type="text" id="modal-ha-prefix" placeholder="growatt_wifi_x_1">
<div style="font-size:11px;color:var(--muted);margin-top:3px">Ohne "sensor."-Präfix — z.B. bei <code>sensor.growatt_wifi_x_1_dcpower</code> ist der Wert <code>growatt_wifi_x_1</code>. Braucht ein bereits per ESPHome in HA eingebundenes Gerät, keinen direkten Netzwerkzugriff.</div></div>
<div style="font-size:11px;color:var(--muted);margin-top:3px">Ohne "sensor."-Präfix — z.B. bei <code>sensor.growatt_wifi_x_1_dcpower</code> ist der Wert <code>growatt_wifi_x_1</code>. Braucht ein bereits per ESPHome in HA eingebundenes Gerät.</div></div>
<div class="field"><label>Home Assistant URL</label>
<input type="text" id="modal-ha-url" placeholder="http://localhost:8123">
<div style="font-size:11px;color:var(--muted);margin-top:3px">Meist unverändert lassen — dank host_network erreicht ShineBridge HA Core direkt über localhost.</div></div>
<div class="field"><label>Long-Lived Access Token</label>
<input type="password" id="modal-ha-token" placeholder="Einstellungen → Profil → Sicherheit → Langlebige Zugriffs-Token">
<div style="font-size:11px;color:var(--muted);margin-top:3px">Kein Supervisor-Token nutzbar (host_network) — braucht ein manuell in HA erstelltes Token.</div></div>
</div>
<div class="field"><label>MQTT Topic-Präfix</label>
<input type="text" id="modal-prefix" placeholder="growatt/wechselrichter1"></div>
@@ -1081,6 +1087,8 @@ async function openModal(invId) {
document.getElementById("modal-port").value = inv.modbus_port || 502;
document.getElementById("modal-addr").value = inv.modbus_address || 1;
document.getElementById("modal-ha-prefix").value = inv.ha_entity_prefix || "";
document.getElementById("modal-ha-url").value = inv.ha_base_url || "http://localhost:8123";
document.getElementById("modal-ha-token").value = inv.ha_token || "";
document.getElementById("modal-prefix").value = inv.mqtt_topic_prefix || "";
document.getElementById("modal-interval").value = inv.update_interval || 30;
document.getElementById("ems-min-pv").value = inv.ems_min_pv ?? 1400;
@@ -1097,6 +1105,8 @@ async function openModal(invId) {
document.getElementById("modal-port").value = "502";
document.getElementById("modal-addr").value = "1";
document.getElementById("modal-ha-prefix").value = "";
document.getElementById("modal-ha-url").value = "http://localhost:8123";
document.getElementById("modal-ha-token").value = "";
document.getElementById("modal-prefix").value = "growatt/wechselrichter" + (invertersList.length + 1);
document.getElementById("modal-interval").value = "30";
document.getElementById("ems-min-pv").value = "1400";
@@ -1135,6 +1145,8 @@ async function saveInverter() {
};
if (model === "GROWATT_WIFI_X") {
inv.ha_entity_prefix = document.getElementById("modal-ha-prefix").value.trim();
inv.ha_base_url = document.getElementById("modal-ha-url").value.trim() || "http://localhost:8123";
inv.ha_token = document.getElementById("modal-ha-token").value.trim();
}
if (model === "KATHREIN_WALLBOX") {
inv.ems_min_pv = parseInt(document.getElementById("ems-min-pv").value);