From 61d88900252a5a9616482b5b9acb4d8a41c07e40 Mon Sep 17 00:00:00 2001 From: Tilman Hampl Date: Sun, 20 Sep 2026 22:07:12 +0200 Subject: [PATCH] v0.6.0: Add Shelly Cloud WS90 sync (background thread, config via GUI) --- x3_dashboard/config.yaml | 10 +++- x3_dashboard/server.py | 99 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 2 deletions(-) diff --git a/x3_dashboard/config.yaml b/x3_dashboard/config.yaml index b17fee8..f5cc799 100644 --- a/x3_dashboard/config.yaml +++ b/x3_dashboard/config.yaml @@ -1,5 +1,5 @@ name: X3 Dashboard Custom -version: "0.5.0" +version: "0.6.0" slug: x3_dashboard_custom description: Custom XTeink X3 dashboard for kitchen with solar/battery/calendar url: https://github.com/CraigBell/xteink-x3-home-assistant-dashboard @@ -59,6 +59,9 @@ options: bridge: "" external: "" secure_external: "" + shelly_cloud_key: "" + shelly_cloud_server: "https://shelly-158-eu.shelly.cloud" + shelly_ws90_device_id: "" schema: title: str subtitle: str @@ -100,4 +103,7 @@ schema: dashboard_pubsub: str? bridge: str? external: str? - secure_external: str? \ No newline at end of file + secure_external: str? + shelly_cloud_key: str? + shelly_cloud_server: str? + shelly_ws90_device_id: str? \ No newline at end of file diff --git a/x3_dashboard/server.py b/x3_dashboard/server.py index db67238..c0a3884 100644 --- a/x3_dashboard/server.py +++ b/x3_dashboard/server.py @@ -1224,6 +1224,101 @@ class X3Handler(BaseHTTPRequestHandler): self.send_text(json.dumps(payload, indent=2) + "\n", "application/json; charset=utf-8", HTTPStatus.INTERNAL_SERVER_ERROR) +import threading +import time as _time + + +def sync_shelly_cloud() -> None: + """Fetch WS90 data from Shelly Cloud and push to HA as sensors.""" + auth_key = str(CONFIG.get("shelly_cloud_key") or "").strip() + server = str(CONFIG.get("shelly_cloud_server") or "").strip() + device_id = str(CONFIG.get("shelly_ws90_device_id") or "").strip() + if not auth_key or not device_id: + return + + try: + data = urllib.parse.urlencode({"id": device_id, "auth_key": auth_key}).encode() + req = urllib.request.Request(f"{server}/device/status", data=data, method="POST") + with urllib.request.urlopen(req, timeout=15) as resp: + result = json.loads(resp.read().decode("utf-8")) + if not result.get("isok"): + print(f"Shelly Cloud error: {result}", file=sys.stderr) + return + status = result.get("data", {}).get("device_status", {}) + + sensors = { + "sensor.shelly_cloud_ws90_precipitation": ( + str(status.get("precipitation:0", {}).get("value", "unknown")), + {"unit_of_measurement": "mm", "device_class": "precipitation", "state_class": "total_increasing", "friendly_name": "WS90 Niederschlag (Cloud)"}, + ), + "sensor.shelly_cloud_ws90_temperature": ( + str(status.get("temperature:0", {}).get("tC", "unknown")), + {"unit_of_measurement": "°C", "device_class": "temperature", "state_class": "measurement", "friendly_name": "WS90 Temperatur (Cloud)"}, + ), + "sensor.shelly_cloud_ws90_humidity": ( + str(status.get("humidity:0", {}).get("rh", "unknown")), + {"unit_of_measurement": "%", "device_class": "humidity", "state_class": "measurement", "friendly_name": "WS90 Luftfeuchtigkeit (Cloud)"}, + ), + "sensor.shelly_cloud_ws90_wind_speed": ( + str(status.get("speed:0", {}).get("value", "unknown")), + {"unit_of_measurement": "km/h", "device_class": "wind_speed", "state_class": "measurement", "friendly_name": "WS90 Windgeschwindigkeit (Cloud)"}, + ), + "sensor.shelly_cloud_ws90_gust_speed": ( + str(status.get("speed:1", {}).get("value", "unknown")), + {"unit_of_measurement": "km/h", "device_class": "wind_speed", "state_class": "measurement", "friendly_name": "WS90 Böengeschwindigkeit (Cloud)"}, + ), + "sensor.shelly_cloud_ws90_wind_direction": ( + str(status.get("direction:0", {}).get("value", "unknown")), + {"unit_of_measurement": "°", "device_class": "wind_direction", "state_class": "measurement", "friendly_name": "WS90 Windrichtung (Cloud)"}, + ), + "sensor.shelly_cloud_ws90_pressure": ( + str(status.get("pressure:0", {}).get("value", "unknown")), + {"unit_of_measurement": "hPa", "device_class": "pressure", "state_class": "measurement", "friendly_name": "WS90 Luftdruck (Cloud)"}, + ), + "sensor.shelly_cloud_ws90_uv_index": ( + str(status.get("UV:0", {}).get("value", "unknown")), + {"state_class": "measurement", "friendly_name": "WS90 UV-Index (Cloud)"}, + ), + "sensor.shelly_cloud_ws90_illuminance": ( + str(status.get("illuminance:0", {}).get("lux", "unknown")), + {"unit_of_measurement": "lx", "device_class": "illuminance", "state_class": "measurement", "friendly_name": "WS90 Beleuchtungsstärke (Cloud)"}, + ), + "sensor.shelly_cloud_ws90_battery": ( + str(status.get("devicepower:0", {}).get("battery", {}).get("percent", "unknown")), + {"unit_of_measurement": "%", "device_class": "battery", "state_class": "measurement", "friendly_name": "WS90 Batterie (Cloud)"}, + ), + "binary_sensor.shelly_cloud_ws90_rain_detected": ( + "on" if status.get("moisture_alarm:0", {}).get("value") else "off", + {"device_class": "moisture", "friendly_name": "WS90 Regen erkannt (Cloud)"}, + ), + } + + client = HomeAssistantClient() + for entity_id, (state, attrs) in sensors.items(): + payload = {"state": state, "attributes": attrs} + body = json.dumps(payload).encode("utf-8") + req = urllib.request.Request( + f"{client.base_url}/api/states/{entity_id}", + data=body, + method="POST", + headers={"Authorization": f"Bearer {client.token}", "Content-Type": "application/json"}, + ) + with urllib.request.urlopen(req, timeout=10) as resp: + resp.read() + + precip = status.get("precipitation:0", {}).get("value", "?") + print(f"Shelly Cloud sync: precip={precip}mm") + except Exception as exc: + print(f"Shelly Cloud sync error: {exc}", file=sys.stderr) + + +def shelly_cloud_loop() -> None: + """Background thread: sync Shelly Cloud every 10 minutes.""" + while True: + sync_shelly_cloud() + _time.sleep(600) + + def main() -> int: parser = argparse.ArgumentParser(description="Serve XTeink X3 Home Assistant feed") parser.add_argument("--host", default=os.getenv("X3_FEED_HOST", "127.0.0.1")) @@ -1235,6 +1330,10 @@ def main() -> int: global CONFIG load_env(Path(args.env_file)) CONFIG = load_options(Path(args.options_file)) + + shelly_thread = threading.Thread(target=shelly_cloud_loop, daemon=True) + shelly_thread.start() + server = ThreadingHTTPServer((args.host, args.port), X3Handler) print(f"X3 feed listening on http://{args.host}:{args.port}/") try: