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:
|