v0.6.0: Add Shelly Cloud WS90 sync (background thread, config via GUI)

This commit is contained in:
Tilman Hampl
2026-09-20 22:07:12 +02:00
parent 219471ef0f
commit 61d8890025
2 changed files with 107 additions and 2 deletions
+8 -2
View File
@@ -1,5 +1,5 @@
name: X3 Dashboard Custom name: X3 Dashboard Custom
version: "0.5.0" version: "0.6.0"
slug: x3_dashboard_custom slug: x3_dashboard_custom
description: Custom XTeink X3 dashboard for kitchen with solar/battery/calendar description: Custom XTeink X3 dashboard for kitchen with solar/battery/calendar
url: https://github.com/CraigBell/xteink-x3-home-assistant-dashboard url: https://github.com/CraigBell/xteink-x3-home-assistant-dashboard
@@ -59,6 +59,9 @@ options:
bridge: "" bridge: ""
external: "" external: ""
secure_external: "" secure_external: ""
shelly_cloud_key: ""
shelly_cloud_server: "https://shelly-158-eu.shelly.cloud"
shelly_ws90_device_id: ""
schema: schema:
title: str title: str
subtitle: str subtitle: str
@@ -100,4 +103,7 @@ schema:
dashboard_pubsub: str? dashboard_pubsub: str?
bridge: str? bridge: str?
external: str? external: str?
secure_external: str? secure_external: str?
shelly_cloud_key: str?
shelly_cloud_server: str?
shelly_ws90_device_id: str?
+99
View File
@@ -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) 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: def main() -> int:
parser = argparse.ArgumentParser(description="Serve XTeink X3 Home Assistant feed") parser = argparse.ArgumentParser(description="Serve XTeink X3 Home Assistant feed")
parser.add_argument("--host", default=os.getenv("X3_FEED_HOST", "127.0.0.1")) parser.add_argument("--host", default=os.getenv("X3_FEED_HOST", "127.0.0.1"))
@@ -1235,6 +1330,10 @@ def main() -> int:
global CONFIG global CONFIG
load_env(Path(args.env_file)) load_env(Path(args.env_file))
CONFIG = load_options(Path(args.options_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) server = ThreadingHTTPServer((args.host, args.port), X3Handler)
print(f"X3 feed listening on http://{args.host}:{args.port}/") print(f"X3 feed listening on http://{args.host}:{args.port}/")
try: try: