v0.6.0: Add Shelly Cloud WS90 sync (background thread, config via GUI)
This commit is contained in:
@@ -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?
|
||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user