docs(hass): Quick 面板 card-mod 等高 + 饼图图例右侧 + WS 客户端脚本
- 安装 card-mod (HACS),用 CSS `ha-card { height: Npx }` 强制等高
- 饼图+人体感应 520px,温湿度双图 520px
- 饼图图例从底部移到右侧 (legend position: right),高度 510→520
- 关键认知:apexcharts chart.height 含图例,timescale-plotly height 是纯绘图区
- 提交 scripts/ha-ws-client.py (纯 stdlib WS 客户端,文档已引用)
This commit is contained in:
+173
-14
@@ -305,14 +305,58 @@ supervisor 代理 WS 被拒(loop prevention),用
|
||||
0.589 元/kWh、15 个 csg_* 数值齐全、回归值不变(14.09/198.65/331.22/
|
||||
304.99/181.89)、automations on、`ha core check` OK、日志无 template 错误。
|
||||
|
||||
**CSG 长期归档(W1N-243, 2026-08-29):** scribe 库新增 `csg_history`
|
||||
表(逐日 usage/cost/ladder/balance + 逐月累计;2026-07-01 起回填,永久),
|
||||
由 TimescaleDB 每日任务 **1008** `csg_daily_snapshot()`(22:30
|
||||
Asia/Shanghai;**TS job 非 pg_cron**)upsert 维护。日费用在原生
|
||||
`latest_day_cost` 缺失时回退 = 昨日用电 × 当前档费率(模板
|
||||
`csg_current_ladder_tariff` 0.639);月费用回退模板
|
||||
`csg_this_month_ladder_cost`。**语义**:day 行 usage/cost 为该日值,
|
||||
ladder/balance 为 22:30 快照值。详见 [hosts/pgdb.md](../hosts/pgdb.md)。
|
||||
**CSG 长期归档(W1N-243, 2026-08-29;任务改号 2026-09-21):** scribe 库新增
|
||||
`csg_history` 表(逐日 usage/cost/ladder/balance + 逐月累计;2026-07-01 起回填,
|
||||
永久),由 TimescaleDB 每日任务 **1010**(原 1008,2026-09-21 重建)
|
||||
`csg_daily_snapshot()`(**14:30 UTC = 22:30 Asia/Shanghai**;**TS job 非 pg_cron**)
|
||||
upsert 维护;计费用 `csg_ladder_cost(kwh, month)`。日行 usage 取集成
|
||||
`yesterday_kwh`,日费用在原生 `latest_day_cost` 缺失时回退 = 昨日用电 ×
|
||||
当前档费率(模板 `csg_current_ladder_tariff`);月行由阶梯助手重算。**语义**:
|
||||
day 行 usage/cost 为该日值,ladder/balance 为快照值。详见
|
||||
[hosts/pgdb.md](../hosts/pgdb.md) § Known issues 2026-09-21(含原 1008 从未成功
|
||||
的根因与回填)。
|
||||
|
||||
**CSG 预测值复核(2026-09-21,只读):** 对
|
||||
`sensor.csg_*` 全部 14 个派生值用独立 Python 实现按同一输入重算,逐个与 HA
|
||||
live state 完全一致(`csg_30d_avg` 9.07 / `csg_predicted_usage` 268.45 /
|
||||
`csg_predicted_cost_ladder` 158.54 / `csg_this_month_ladder_cost` 99.35 /
|
||||
`csg_mom_change` -21.6 等)。同时把 9 月逐日口径对回 scribe:集成
|
||||
`this_month_by_day`(09-01..09-19,19 天,和 168.68)与 `states_raw` 里
|
||||
`yesterday_kwh` 观测值逐日一致(唯一差异 09-06 = 9.29 是 scribe 漏采、集成有值),
|
||||
确认输入数据无缺口。**因此「预测本月电费」不是算错,而是估计器的窗口问题**:
|
||||
`csg_predicted_usage = 本月累计 + avg × 剩余天数`,旧版 `avg = csg_30d_avg`
|
||||
(`history_data[-30:]` 的**普通 30 日均值、跨月重叠**)→ 月初被上月(更热)拖高
|
||||
(09-02 实测 317.93 kWh / 190.16 元),随本月凉快日置换回落到 268.45 / 158.54,
|
||||
一个月内摆动 **-17%**。**另注**:集成原生
|
||||
`latest_day_cost` / `this_month_total_cost` / `last_month_total_cost` 自
|
||||
2026-08-29 起**恒为 unknown**(`states_raw` 无一条有值),故面板上所有「元」
|
||||
均为模板阶梯估算值,从未与账单口径对账。
|
||||
|
||||
**`csg_sensors.yaml` 预测速率 v2(2026-09-21,已实施):** 只改
|
||||
`csg_predicted_usage` 的速率项,`csg_30d_avg` 语义不动(面板「近30日平均」照旧)。
|
||||
新公式:`avg = mtd × w + recent7 × (1 − w)`,其中 `mtd = 本月累计 / 已过天数`、
|
||||
`recent7` = `history_data` 末 7 条均值、`w = min(已过天数, 14) / 14`;两者都取不到
|
||||
才退回 `csg_30d_avg`。**动机**:本月至今速率按天数爬升可信度,避免月初被上月污染;
|
||||
近 7 日速率兜底,因为月初唯一的新数据就是上月末。Block C 触发器加
|
||||
`sensor.csg_history`。**19 天回测**(基准 = 第 19 天本月至今速率外推,逐日重放
|
||||
集成 `this_month_by_day` + `last_month_by_day`):
|
||||
|
||||
| 速率方案 | 预测电费摆动 | 最大偏差 | 平均绝对偏差 |
|
||||
|---|---|---|---|
|
||||
| 旧:30 日均值 | 21.1% | 35.99 元 | 14.04 元 |
|
||||
| v2:K=7 | 14.6% | 16.81 元 | 5.65 元 |
|
||||
| **v2:K=14(采用)** | **13.9%** | **15.33 元** | **4.94 元** |
|
||||
| v2:K=14 但兜底用 30 日均值 | 23.3% | 33.12 元 | 6.28 元 |
|
||||
|
||||
(末行证实病根就是那个跨月的 30 日均值兜底。)备份
|
||||
`/homeassistant/.csg-backups/csg_sensors.yaml.bak-20260921-pre-predictor`
|
||||
(改前)与 `…-pre-k14`(K=7 中间态)。**验证**:`ha core check` OK;Core 重启两次
|
||||
(trigger 模板块不吃 `template.reload`,W1N-114 先例);15 个 `csg_*` 全部数值、
|
||||
无 template 错误;当天 `n=19 ≥ 14` 故两版同值,面板读回
|
||||
`csg_predicted_usage` 266.34 kWh / `csg_predicted_cost_ladder` 157.19 元 /
|
||||
`csg_prediction_progress` 63.3%;新值已进 scribe 归档
|
||||
(`states_raw` 08:17:04Z 158.54 → 08:17:10Z 157.19)。**注意**:`w` 的爬升窗口
|
||||
(14 天)是回测选出来的单月参数,属弱证据,换季/换季后值得复看。
|
||||
|
||||
> **Seasonal gauge switch (W1N-240 已知事项):** 每年 **11-01** 把
|
||||
> `power-monitor` 视图「本月累计」gauge 切到冬季 `max:450` /
|
||||
@@ -563,13 +607,128 @@ manual copies): reader `remmob/timescale_database_reader` **v1.1.0** (main
|
||||
各 timescale 卡可选档上调——大功率电器/常驻负载 `…,24h` → `+3d,7d`;
|
||||
环境 `6h,12h,24h,48h` → `+7d,14d,30d`;人体感应 `…,24h` → `+3d,7d`;
|
||||
用电量(按插座) `energy_time_ranges` `today,week,month,custom` → `+3mo`。
|
||||
**默认档未改**(6h / 6h / today / 24h / 12h)。卡片 JS 只接受
|
||||
`<n>m|<n>h|<n>d`(`parseDurationToMs` 正则 `/^(\d+)(m|h|d)$/`,
|
||||
仅 m/h/d,无 w)与命名档 `today|week|month|3mo|6mo|year|years|custom`;
|
||||
`energy_mode` 卡必须用后者。**数据下界注意**:scribe `sensor_minute`
|
||||
目前最早只到 **2026-08-29**,所以 >15d 的档(14d 边缘、30d 明显)前半段
|
||||
会是空白,等归档继续累积才好看。备份
|
||||
**默认档未改**(6h / 6h / today / 24h / 12h)。备份
|
||||
`.lovelace-backups/dashboard-quick-20260913-190912-pre-timerange.json`。
|
||||
⚠️ **本条的 `d` 档当天其实是坏的**——见下方 2026-09-21 修复;`energy_mode`
|
||||
命名档(today/week/month/3mo)当时就是好的。卡片命名档只能是
|
||||
`today|week|month|3mo|6mo|year|years|custom`。**数据下界注意**:scribe
|
||||
`sensor_minute` 最早只到 **2026-08-29**,所以 >15d 的档(14d 边缘、30d 明显)
|
||||
前半段仍是空白,等归档继续累积才好看。
|
||||
|
||||
- **Quick 面板改造 + timescale 卡两个真 bug 修复 (2026-09-21)**:起因是用户
|
||||
觉得「面板标题 + 统计方式」不对劲。只读审查查出三个问题并全部修掉:
|
||||
|
||||
**① 卡 JS:`<n>d` 档实际渲染成 n 小时(VPS-92 的扩容其实没生效)**。
|
||||
非 energy 档走 `const hours = parseInt(this._selectedRange) || 24;`,
|
||||
`parseInt("7d") === 7`。全卡唯一能解析 `d` 的 `parseDurationToMs()`
|
||||
(正则 `/^(\d+)(m|h|d)$/`)**只被 `resolveEnergyWindow()` 调用一次**,
|
||||
即只服务 energy 卡。后果:`3d` 比 `24h` 还短(3h),`14d`→14h、`30d`→30h。
|
||||
**修复**:改用 `Math.max(1, parseDurationToMs(...)/3600000)`;顺带把采样密度
|
||||
目标 80 → 160 点(7d 档从 2.1h/桶降到 1.05h/桶)。备份
|
||||
`.lovelace-backups/timescale-plotly-card.js.bak-20260921-pre-dayfix`。
|
||||
实测(本地 node 跑卡里的 `parseDurationToMs`):`3d→72h / 7d→168h /
|
||||
14d→336h / 30d→720h`,各档均 ~160 点;`node --check` JS 语法 OK。
|
||||
**资源 URL 已加 `?v=20260921` 强制浏览器重取**(原来无 hacstag,缓存问题会
|
||||
让修复「看不见」)。
|
||||
|
||||
**② 功率卡画的是瞬时采样而非统计量**。reader 对 `table=sensor_minute`
|
||||
(时间列名 `minute`)默认 `downsample_method = "last"`
|
||||
(`custom_components/timescale_database_reader/__init__.py` L385-389),
|
||||
即每桶取「末一分钟的瞬时值」。实测空调 1 小时桶:03:00 真实均值 241.6 W /
|
||||
峰值 1128 W,图上只画 157;19:00 真实均值 97.6 / 峰值 676,图上画 **13**
|
||||
(看起来像停机)。因 `sensor_minute` 已是均匀 1 分钟 LOCF 序列,`avg()`
|
||||
才是正确时间均值。**修复**:两张功率卡加 `downsample_method: "avg"`。
|
||||
**用电量卡不用改**——累计计数器配 `last()` 就是桶末读数,卡片再 diff,
|
||||
本来就是对的;人体感应卡显式 `last` 对分类信号也对。
|
||||
|
||||
**③ 标题与分组名不副实**。按 7d 实测(峰值/均值)重分组并改名:
|
||||
| 位置 | 改前 | 改后 |
|
||||
|---|---|---|
|
||||
| 分区 | 大功率电器 | **峰值负载**(厨房电器 1839 / 空调 1128 / 电水壶 957 / 咖啡机 882 / 电脑 363 W) |
|
||||
| 分区 | 常驻负载 | **基础负载**(主网络 75 / 冰箱 137 / 大房 290 / 水设备 171 / 无线AP 976* W) |
|
||||
| 分区 | 用电 | **今日用电** |
|
||||
| 分区 | 灯 + 开关(分开在最顶和最底) | **灯与开关**(合并置顶,可操作项集中) |
|
||||
| 分区(新) | — | **此刻耗电** |
|
||||
| 徽标 | 总功率 | **插座合计**(只是 9 个插座之和,不含灯/换气) |
|
||||
| 实体 | 迷你 / mini数显 / 数显 | **卫生间 / 客厅 / 卧室**(HA area 本来就有,之前用的是设备型号名) |
|
||||
| 实体 | 电池(移动) | **厨房(电池)**(该设备 area 也是厨房) |
|
||||
删除 3 张与分区标题重复的卡 title(用电量(按插座) / 室内外温湿度 / 活动状态)。
|
||||
\* 无线AP 7 天里只有 2 分钟 >150 W 却记到 976 W —— 单点毛刺,会把 y 轴顶飞,
|
||||
待查。
|
||||
|
||||
**④ 新展示**:新增「此刻耗电」区 = `custom:apexcharts-card` **donut**,11 个
|
||||
插座当前功率(`graph_span: 10min` + `update_interval: 1min`),回答「现在什么
|
||||
在吃电」;环境区拆成**室内外温差**(新模板传感器 `sensor.quick_wen_cha` =
|
||||
3 个室内温度均值 − 室外,画零线上下 fill)+ **湿度**(4 条),4 个温度进徽标。
|
||||
传感器加在 `templates/quick_sensors.yaml`(W1N-238 约定的落点,顶层列表,
|
||||
`quick_` 前缀 + unique_id),非 trigger 型模板卡,**`template.reload` 即可生效,
|
||||
无需重启 Core**(实测 `POST /core/api/services/template/reload` → 200,
|
||||
`sensor.quick_wen_cha` = 3.9 °C)。备份
|
||||
`templates/quick_sensors.yaml.bak-20260921-pre-wencha`。
|
||||
最终分区顺序(按「可变性」):灯与开关 → 此刻耗电 → 今日用电 →
|
||||
峰值负载(2) → 基础负载(2) → 环境 → 人体感应。
|
||||
|
||||
**⑤ Quick 布局重构 (2026-09-21 晚)**:用户反馈饼图(apexcharts donut)撑满
|
||||
整行(span 4)且不满、马桶换气开关 tile `columns: "full"` 占满整行太宽、整体
|
||||
布局需要重新思考。改动:
|
||||
|
||||
**布局重排**(阅读流:控制 → 此刻 → 趋势 → 今日 → 环境):
|
||||
| 行 | 左 (span 2) | 右 (span 2) | 主题 |
|
||||
|---|---|---|---|
|
||||
| 1 | 灯与开关 (span 4) | | 可操作项置顶 |
|
||||
| 2 | 实时功率 (donut) | 人体感应 | 「此刻状态」 |
|
||||
| 3 | 峰值负载 | 基础负载 | 「功率趋势」(原配对保留) |
|
||||
| 4 | 今日用电 (span 4) | | 11 条堆叠柱需要宽度 |
|
||||
| 5 | 温湿度 (span 4) | | 温差+湿度两图并排 |
|
||||
|
||||
**标题改名**:此刻耗电→**实时功率**(与峰值负载对仗)、环境→**温湿度**
|
||||
(更具体)。删饼图卡内冗余标题 "各插座当前功率 (W)"。
|
||||
|
||||
**开关收窄**:马桶换气电源 tile 从独立 `columns: "full"`(48 列全宽)→ 移入
|
||||
灯的 2 列嵌套网格第 5 格,与灯卡等宽(半行)。
|
||||
|
||||
**饼图高度**:`apex_config.chart.height` 280→350(11 项底部图例占 ~100px,
|
||||
280 时饼图本体太扁)。
|
||||
|
||||
**⑥ card-mod 等高 (2026-09-21 晚)**:不同卡类型(apexcharts vs timescale-plotly)
|
||||
的 `height` 渲染比例不同(apexcharts 含图例/内边距,timescale 是纯绘图区),
|
||||
直接对齐数字无法视觉等高。安装 **card-mod**(HACS → card-mod + card-mod-studio)
|
||||
后用 CSS 强制 `ha-card` 高度:
|
||||
|
||||
```yaml
|
||||
card_mod:
|
||||
style: "ha-card { height: 520px !important; }"
|
||||
```
|
||||
|
||||
**最终高度**:饼图+人体感应 = **520px**,温湿度双图 = **520px**。其余图表
|
||||
(峰值/基础负载 280、今日用电 320)保持各自原生高度,不强制统一——不同图表
|
||||
类型适合不同高度,强行统一反而让简单图表显得太空。
|
||||
|
||||
**关键认知**:apexcharts `chart.height` 包含图例区域,timescale-plotly `height`
|
||||
是纯绘图区。同一数字渲染出来 apexcharts 视觉上更矮。card-mod 直接控制
|
||||
`ha-card` 容器高度,绕过内部渲染差异。
|
||||
|
||||
备份:`.lovelace-backups/dashboard-quick-20260921-pre-titles.json`(同日早期
|
||||
备份,覆盖标题+布局全部改动的回滚)。
|
||||
|
||||
**验证**:`ha core check` OK;WS 读回与写入 payload **逐字节相同**;
|
||||
新配置引用的 **40 个实体全部存在**且无 unknown/unavailable;
|
||||
卡 JS `node --check` 通过、时长解析实测正确。
|
||||
**改法(可复用)**:写 Lovelace 配置必须走 WS
|
||||
`lovelace/config/save`(**勿直改 `.storage/`**)。本机 python/HA 容器内经
|
||||
supervisor 代理连 WS 会被 loop-prevention 拒;可行路径是
|
||||
`docker run --rm -i --network host -e SUPERVISOR_TOKEN --entrypoint python3
|
||||
r.hassbus.com/home-assistant/aarch64-hassio-supervisor:<ver> -c "..."`
|
||||
跑本仓库的 [`scripts/ha-ws-client.py`](../scripts/ha-ws-client.py)(纯 stdlib,
|
||||
不依赖 websockets/aiohttp,也不依赖 `-v` 挂载——`-v /tmp/...` 会落到 Docker 宿主
|
||||
而不是 SSH add-on 的 `/tmp`)。读配置:`{"action":"get","url_path":"dashboard-quick"}`;
|
||||
写:`{"action":"save",...,"config":{...}}`。备份
|
||||
`.lovelace-backups/dashboard-quick-20260921-pre-titles.json`。
|
||||
|
||||
**回滚**:还原 `dashboard-quick-20260921-pre-titles.json`(WS save)+
|
||||
`timescale-plotly-card.js.bak-20260921-pre-dayfix` + 删 `quick_wen_cha`
|
||||
(还原 `quick_sensors.yaml.bak-20260921-pre-wencha` + `template.reload`)。
|
||||
改了卡 JS 就要把资源 URL 的 `?v=` 再 bump 一次。
|
||||
|
||||
### 地图仪表盘:CARTO keyed tiles via `custom:map-card` (verified 2026-08-30, W1N-261)
|
||||
|
||||
|
||||
Executable
+172
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Stdlib-only Home Assistant WebSocket client (via the Supervisor core proxy).
|
||||
|
||||
Why this exists
|
||||
---------------
|
||||
Some HA operations have **no REST route** and must go through the WebSocket API
|
||||
-- writing Lovelace config (`lovelace/config/save`) is the main one, and the
|
||||
`.storage/` files must not be hand-edited (HA may overwrite them, and edits skip
|
||||
validation). The obvious paths are dead ends on this host: the HA host and the
|
||||
core container have no usable WS client (the Supervisor proxy rejects core's own
|
||||
loop), and the Supervisor image has neither `websockets` nor `aiohttp`.
|
||||
HA's WS protocol is plain text frames, so ~90 lines of stdlib is enough.
|
||||
|
||||
Run it from the HA host (see hosts/hass.windy.lan.md "改法(可复用)"):
|
||||
|
||||
B64=$(base64 < scripts/ha-ws-client.py | tr -d '\n')
|
||||
echo '{"action":"get","url_path":"dashboard-quick"}' | \
|
||||
docker run --rm -i --network host -e SUPERVISOR_TOKEN --entrypoint python3 \
|
||||
r.hassbus.com/home-assistant/aarch64-hassio-supervisor:<ver> \
|
||||
-c "import base64,sys;exec(base64.b64decode('$B64').decode())" - -
|
||||
|
||||
Do NOT use `-v /tmp/...`: that path is resolved by the Docker daemon on the
|
||||
HAOS host, not inside the SSH add-on, so the mount comes up empty.
|
||||
|
||||
Payload / output
|
||||
----------------
|
||||
{"action":"get", "url_path":"dashboard-quick"}
|
||||
{"action":"save", "url_path":"dashboard-quick", "config": {...}}
|
||||
{"action":"raw", "command": {"type":"lovelace/resources/list"}}
|
||||
|
||||
python3 ha-ws-client.py <payload.json|-> <out.json|->
|
||||
|
||||
"-" reads the payload from stdin / prints the result to stdout, wrapped in
|
||||
<<<DSH_RESULT>>> / <<<DSH_END>>> markers so it survives the add-on login banner.
|
||||
"""
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import socket
|
||||
import struct
|
||||
import sys
|
||||
|
||||
HOST = os.environ.get("WS_HOST", "172.30.32.2")
|
||||
PORT = int(os.environ.get("WS_PORT", "80"))
|
||||
PATH = os.environ.get("WS_PATH", "/core/websocket")
|
||||
|
||||
|
||||
class WS:
|
||||
def __init__(self, host, port, path, extra_headers=None):
|
||||
self.s = socket.create_connection((host, port), timeout=30)
|
||||
key = base64.b64encode(bytes(random.getrandbits(8) for _ in range(16))).decode()
|
||||
req = (
|
||||
f"GET {path} HTTP/1.1\r\nHost: {host}\r\nUpgrade: websocket\r\n"
|
||||
f"Connection: Upgrade\r\nSec-WebSocket-Key: {key}\r\n"
|
||||
f"Sec-WebSocket-Version: 13\r\n"
|
||||
)
|
||||
for k, v in (extra_headers or {}).items():
|
||||
req += f"{k}: {v}\r\n"
|
||||
self.s.sendall((req + "\r\n").encode())
|
||||
buf = b""
|
||||
while b"\r\n\r\n" not in buf:
|
||||
d = self.s.recv(4096)
|
||||
if not d:
|
||||
raise RuntimeError("closed during handshake")
|
||||
buf += d
|
||||
head, _, rest = buf.partition(b"\r\n\r\n")
|
||||
status = head.split(b"\r\n")[0]
|
||||
if b"101" not in status:
|
||||
raise RuntimeError("handshake failed: " + status.decode(errors="replace"))
|
||||
self.buf = rest
|
||||
|
||||
def _exact(self, n):
|
||||
while len(self.buf) < n:
|
||||
d = self.s.recv(65536)
|
||||
if not d:
|
||||
raise RuntimeError("socket closed")
|
||||
self.buf += d
|
||||
out, self.buf = self.buf[:n], self.buf[n:]
|
||||
return out
|
||||
|
||||
def send(self, text):
|
||||
data = text.encode()
|
||||
mask = bytes(random.getrandbits(8) for _ in range(4))
|
||||
n = len(data)
|
||||
hdr = bytearray([0x81])
|
||||
if n < 126:
|
||||
hdr.append(0x80 | n)
|
||||
elif n < 65536:
|
||||
hdr.append(0x80 | 126)
|
||||
hdr += struct.pack(">H", n)
|
||||
else:
|
||||
hdr.append(0x80 | 127)
|
||||
hdr += struct.pack(">Q", n)
|
||||
hdr += mask
|
||||
self.s.sendall(bytes(hdr) + bytes(b ^ mask[i % 4] for i, b in enumerate(data)))
|
||||
|
||||
def recv(self):
|
||||
while True:
|
||||
b0, b1 = self._exact(2)
|
||||
op = b0 & 0x0F
|
||||
ln = b1 & 0x7F
|
||||
if ln == 126:
|
||||
ln = struct.unpack(">H", self._exact(2))[0]
|
||||
elif ln == 127:
|
||||
ln = struct.unpack(">Q", self._exact(8))[0]
|
||||
payload = self._exact(ln) if ln else b""
|
||||
if op == 0x8:
|
||||
raise RuntimeError("server closed")
|
||||
if op in (0x9, 0xA):
|
||||
continue
|
||||
return payload.decode()
|
||||
|
||||
|
||||
def main():
|
||||
token = os.environ["SUPERVISOR_TOKEN"]
|
||||
src = sys.argv[1]
|
||||
payload = json.load(sys.stdin if src == "-" else open(src, encoding="utf-8"))
|
||||
action = payload.pop("action")
|
||||
url_path = payload.pop("url_path", "dashboard-quick")
|
||||
out_path = sys.argv[2]
|
||||
|
||||
ws = WS(HOST, PORT, PATH, {"Authorization": f"Bearer {token}"})
|
||||
while True:
|
||||
hello = json.loads(ws.recv())
|
||||
if hello.get("type") == "auth_required":
|
||||
break
|
||||
if hello.get("type") == "auth_ok":
|
||||
break
|
||||
ws.send(json.dumps({"type": "auth", "access_token": token}))
|
||||
while True:
|
||||
r = json.loads(ws.recv())
|
||||
if r.get("type") == "auth_ok":
|
||||
break
|
||||
if r.get("type") == "auth_invalid":
|
||||
print("AUTH_INVALID:", json.dumps(r, ensure_ascii=False)[:300])
|
||||
return 1
|
||||
|
||||
cmd = {
|
||||
"id": 1,
|
||||
"type": "lovelace/config/save" if action == "save" else "lovelace/config",
|
||||
"url_path": url_path,
|
||||
}
|
||||
if action == "save":
|
||||
cmd["config"] = payload["config"]
|
||||
elif action == "raw":
|
||||
cmd = dict(payload["command"])
|
||||
cmd["id"] = 1
|
||||
ws.send(json.dumps(cmd))
|
||||
while True:
|
||||
r = json.loads(ws.recv())
|
||||
if r.get("id") != 1:
|
||||
continue
|
||||
if not r.get("success"):
|
||||
print("ERROR:", json.dumps(r, ensure_ascii=False)[:600])
|
||||
return 1
|
||||
result = r.get("result")
|
||||
blob = json.dumps(result if result is not None else {"ok": True},
|
||||
ensure_ascii=False, indent=1)
|
||||
if out_path == "-":
|
||||
print("<<<DSH_RESULT>>>")
|
||||
print(blob)
|
||||
print("<<<DSH_END>>>")
|
||||
else:
|
||||
with open(out_path, "w", encoding="utf-8") as f:
|
||||
f.write(blob)
|
||||
print("OK ->", out_path)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user