iobroker.govee-smart 2.34.0 → 2.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -4
- package/admin/i18n/de.json +4 -1
- package/admin/i18n/en.json +4 -1
- package/admin/i18n/es.json +4 -1
- package/admin/i18n/fr.json +4 -1
- package/admin/i18n/it.json +4 -1
- package/admin/i18n/nl.json +4 -1
- package/admin/i18n/pl.json +4 -1
- package/admin/i18n/pt.json +4 -1
- package/admin/i18n/ru.json +4 -1
- package/admin/i18n/uk.json +4 -1
- package/admin/i18n/zh-cn.json +4 -1
- package/build/lib/appliance-frames.js +98 -0
- package/build/lib/appliance-frames.js.map +7 -0
- package/build/lib/capability-mapper.js +15 -10
- package/build/lib/capability-mapper.js.map +2 -2
- package/build/lib/command-router.js +25 -1
- package/build/lib/command-router.js.map +2 -2
- package/build/lib/device-manager.js +69 -5
- package/build/lib/device-manager.js.map +3 -3
- package/build/lib/govee-api-client.js +5 -0
- package/build/lib/govee-api-client.js.map +2 -2
- package/build/lib/govee-cloud-client.js +13 -2
- package/build/lib/govee-cloud-client.js.map +2 -2
- package/build/lib/handlers/device-events.js +4 -4
- package/build/lib/handlers/device-events.js.map +2 -2
- package/build/lib/state-manager.js +35 -0
- package/build/lib/state-manager.js.map +2 -2
- package/build/lib/types.js.map +2 -2
- package/build/main.js +9 -12
- package/build/main.js.map +3 -3
- package/io-package.json +14 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -103,6 +103,19 @@ This adapter's MQTT authentication and BLE-over-LAN (ptReal) protocol implementa
|
|
|
103
103
|
### **WORK IN PROGRESS**
|
|
104
104
|
-->
|
|
105
105
|
|
|
106
|
+
### 2.35.0 (2026-09-11)
|
|
107
|
+
|
|
108
|
+
- New: An air purifier's mode, level and filter life follow the device's own status report — a change made in the Govee app shows in ioBroker within a second, no cloud call (H7127, #47)
|
|
109
|
+
- Fixed: Filter life, air quality, mode and level of an appliance are read from Govee's device-state query at start — the adapter read that answer from the wrong field since its first version (#47)
|
|
110
|
+
- Fixed: A light without a local connection gets its power, brightness and colour from the same query at start; Govee's empty answers no longer turn into false or blank values
|
|
111
|
+
- Fixed: A light without a local connection on an installation with only an API key stays reachable — the 20-minute check meant to renew it never received an answer before
|
|
112
|
+
- Fixed: An installation using only an API key lost its appliance commands by mid-morning — a reachability poll that never got an answer used up the device's daily budget
|
|
113
|
+
- Changed: An appliance's reachability is no longer polled every 20 minutes; its own status push, a command and the start-up query count instead — polling would cost 72 of its 90 daily calls
|
|
114
|
+
- Fixed: The diagnostics report now records mode, level, temperature and music commands with their outcome — it only listed power, brightness and colour before
|
|
115
|
+
- Fixed: A datapoint Govee newly reports for a device is there from the first start on — it used to disappear again and only show up after the next restart
|
|
116
|
+
- Fixed: The filter life of an air purifier now carries its unit (%) — Govee declares none, and the datapoint had no unit since its first version
|
|
117
|
+
- Changed: The DreamView switch, the music auto-colour switch and the DIY-scene selector now carry an explanation in the object tree
|
|
118
|
+
|
|
106
119
|
### 2.34.0 (2026-09-10)
|
|
107
120
|
|
|
108
121
|
- Fixed: Air purifiers, heaters, humidifiers and fans — choosing a mode or a speed now reaches the device, where the adapter used to send a value Govee rejected as "Invalid parameter type" (#47)
|
|
@@ -144,10 +157,6 @@ This adapter's MQTT authentication and BLE-over-LAN (ptReal) protocol implementa
|
|
|
144
157
|
- New: Datapoints carry an explanation in all 11 languages wherever the name alone does not say enough — 99 of them instead of 26
|
|
145
158
|
- Changed: The adapter can no longer be installed directly from GitHub — install it from the ioBroker repository or from npm, as with every other adapter
|
|
146
159
|
|
|
147
|
-
### 2.31.1 (2026-09-04)
|
|
148
|
-
|
|
149
|
-
- Fixed: When the adapter met a device model it does not know yet, its log asked the user to press a button that 2.31.0 had removed — it now points at the Expert tab, where the report is actually made
|
|
150
|
-
|
|
151
160
|
[Older changelogs can be found there](CHANGELOG_OLD.md)
|
|
152
161
|
|
|
153
162
|
## Support
|
package/admin/i18n/de.json
CHANGED
|
@@ -155,5 +155,8 @@
|
|
|
155
155
|
"descBodyDetected": "Meldet den Moment, in dem der Sensor jemanden sieht; setzt sich von selbst zurück.",
|
|
156
156
|
"descDirtDetected": "Meldet den Moment, in dem das Gerät erkannten Schmutz signalisiert; setzt sich von selbst zurück.",
|
|
157
157
|
"descCloudOnline": "Ob der Adapter die Govee-Cloud erreicht — nicht, ob ein einzelnes Gerät da ist.",
|
|
158
|
-
"descMembers": "Die Geräte, die diese App-Gruppe gemeinsam schaltet, mit ihrem Namen im Objektbaum."
|
|
158
|
+
"descMembers": "Die Geräte, die diese App-Gruppe gemeinsam schaltet, mit ihrem Namen im Objektbaum.",
|
|
159
|
+
"descMusicAutoColor": "Ob die Lampe im Musikmodus ihre Farben selbst wählt, statt bei einer festen Farbe zu bleiben.",
|
|
160
|
+
"descDreamViewToggle": "Ob die Lampe den Bildschirmfarben folgt, die eine Govee-DreamView-Kamera aufnimmt.",
|
|
161
|
+
"descDiyScene": "Eine der Szenen, die du selbst im DIY-Editor der Govee-App gebaut hast — leer, bis du eine angelegt hast."
|
|
159
162
|
}
|
package/admin/i18n/en.json
CHANGED
|
@@ -155,5 +155,8 @@
|
|
|
155
155
|
"descBodyDetected": "Reports the moment the sensor sees someone; it clears itself again.",
|
|
156
156
|
"descDirtDetected": "Reports the moment the device signals detected dirt; it clears itself again.",
|
|
157
157
|
"descCloudOnline": "Whether the adapter reaches the Govee cloud — not whether any single device is there.",
|
|
158
|
-
"descMembers": "The devices this app group switches together, as they are named in the object tree."
|
|
158
|
+
"descMembers": "The devices this app group switches together, as they are named in the object tree.",
|
|
159
|
+
"descMusicAutoColor": "Whether the light picks its own colours in music mode instead of keeping one fixed colour.",
|
|
160
|
+
"descDreamViewToggle": "Whether the light follows the screen colours a Govee DreamView camera captures.",
|
|
161
|
+
"descDiyScene": "One of the scenes you built yourself in the DIY editor of the Govee app — empty until you have made one."
|
|
159
162
|
}
|
package/admin/i18n/es.json
CHANGED
|
@@ -155,5 +155,8 @@
|
|
|
155
155
|
"descBodyDetected": "Informa del momento en que el sensor detecta a alguien; se restablece solo.",
|
|
156
156
|
"descDirtDetected": "Informa del momento en que el aparato señala suciedad detectada; se restablece solo.",
|
|
157
157
|
"descCloudOnline": "Si el adaptador alcanza la nube de Govee — no si un dispositivo concreto está presente.",
|
|
158
|
-
"descMembers": "Los dispositivos que este grupo de la app conmuta juntos, con su nombre en el árbol de objetos."
|
|
158
|
+
"descMembers": "Los dispositivos que este grupo de la app conmuta juntos, con su nombre en el árbol de objetos.",
|
|
159
|
+
"descMusicAutoColor": "Si la luz elige sus propios colores en el modo música en lugar de mantener un color fijo.",
|
|
160
|
+
"descDreamViewToggle": "Si la luz sigue los colores de la pantalla que capta una cámara Govee DreamView.",
|
|
161
|
+
"descDiyScene": "Una de las escenas que creaste tú mismo en el editor DIY de la app Govee — vacío hasta que crees una."
|
|
159
162
|
}
|
package/admin/i18n/fr.json
CHANGED
|
@@ -155,5 +155,8 @@
|
|
|
155
155
|
"descBodyDetected": "Signale l'instant où le capteur détecte quelqu'un ; se réinitialise seul.",
|
|
156
156
|
"descDirtDetected": "Signale l'instant où l'appareil annonce une saleté détectée ; se réinitialise seul.",
|
|
157
157
|
"descCloudOnline": "Si l'adaptateur atteint le cloud Govee — pas si un appareil donné est présent.",
|
|
158
|
-
"descMembers": "Les appareils que ce groupe de l'application commute ensemble, tels que nommés dans l'arbre d'objets."
|
|
158
|
+
"descMembers": "Les appareils que ce groupe de l'application commute ensemble, tels que nommés dans l'arbre d'objets.",
|
|
159
|
+
"descMusicAutoColor": "Si la lampe choisit elle-même ses couleurs en mode musique au lieu de garder une couleur fixe.",
|
|
160
|
+
"descDreamViewToggle": "Si la lampe suit les couleurs de l'écran captées par une caméra Govee DreamView.",
|
|
161
|
+
"descDiyScene": "Une des scènes que vous avez créées vous-même dans l'éditeur DIY de l'application Govee — vide tant que vous n'en avez pas créé."
|
|
159
162
|
}
|
package/admin/i18n/it.json
CHANGED
|
@@ -155,5 +155,8 @@
|
|
|
155
155
|
"descBodyDetected": "Segnala il momento in cui il sensore rileva qualcuno; si azzera da solo.",
|
|
156
156
|
"descDirtDetected": "Segnala il momento in cui l'apparecchio indica sporco rilevato; si azzera da solo.",
|
|
157
157
|
"descCloudOnline": "Se l'adattatore raggiunge il cloud Govee — non se un singolo dispositivo è presente.",
|
|
158
|
-
"descMembers": "I dispositivi che questo gruppo dell'app comanda insieme, con il nome che hanno nell'albero degli oggetti."
|
|
158
|
+
"descMembers": "I dispositivi che questo gruppo dell'app comanda insieme, con il nome che hanno nell'albero degli oggetti.",
|
|
159
|
+
"descMusicAutoColor": "Se la luce sceglie da sola i colori in modalità musica invece di mantenere un colore fisso.",
|
|
160
|
+
"descDreamViewToggle": "Se la luce segue i colori dello schermo ripresi da una telecamera Govee DreamView.",
|
|
161
|
+
"descDiyScene": "Una delle scene create da te nell'editor DIY dell'app Govee — vuoto finché non ne crei una."
|
|
159
162
|
}
|
package/admin/i18n/nl.json
CHANGED
|
@@ -155,5 +155,8 @@
|
|
|
155
155
|
"descBodyDetected": "Meldt het moment waarop de sensor iemand ziet; herstelt zichzelf.",
|
|
156
156
|
"descDirtDetected": "Meldt het moment waarop het apparaat vuil signaleert; herstelt zichzelf.",
|
|
157
157
|
"descCloudOnline": "Of de adapter de Govee-cloud bereikt — niet of een afzonderlijk apparaat aanwezig is.",
|
|
158
|
-
"descMembers": "De apparaten die deze app-groep samen schakelt, zoals ze in de objectboom heten."
|
|
158
|
+
"descMembers": "De apparaten die deze app-groep samen schakelt, zoals ze in de objectboom heten.",
|
|
159
|
+
"descMusicAutoColor": "Of de lamp in de muziekmodus zelf kleuren kiest in plaats van één vaste kleur te houden.",
|
|
160
|
+
"descDreamViewToggle": "Of de lamp de schermkleuren volgt die een Govee DreamView-camera opneemt.",
|
|
161
|
+
"descDiyScene": "Een van de scènes die je zelf in de DIY-editor van de Govee-app hebt gemaakt — leeg tot je er een hebt gemaakt."
|
|
159
162
|
}
|
package/admin/i18n/pl.json
CHANGED
|
@@ -155,5 +155,8 @@
|
|
|
155
155
|
"descBodyDetected": "Zgłasza moment, w którym czujnik wykrywa kogoś; sam się resetuje.",
|
|
156
156
|
"descDirtDetected": "Zgłasza moment, w którym urządzenie sygnalizuje wykryty brud; sam się resetuje.",
|
|
157
157
|
"descCloudOnline": "Czy adapter łączy się z chmurą Govee — nie czy pojedyncze urządzenie jest obecne.",
|
|
158
|
-
"descMembers": "Urządzenia przełączane razem przez tę grupę z aplikacji, pod nazwami z drzewa obiektów."
|
|
158
|
+
"descMembers": "Urządzenia przełączane razem przez tę grupę z aplikacji, pod nazwami z drzewa obiektów.",
|
|
159
|
+
"descMusicAutoColor": "Czy lampa w trybie muzycznym sama dobiera kolory, zamiast trzymać jeden stały kolor.",
|
|
160
|
+
"descDreamViewToggle": "Czy lampa podąża za kolorami ekranu rejestrowanymi przez kamerę Govee DreamView.",
|
|
161
|
+
"descDiyScene": "Jedna ze scen, które sam zbudowałeś w edytorze DIY aplikacji Govee — puste, dopóki nie utworzysz żadnej."
|
|
159
162
|
}
|
package/admin/i18n/pt.json
CHANGED
|
@@ -155,5 +155,8 @@
|
|
|
155
155
|
"descBodyDetected": "Comunica o momento em que o sensor deteta alguém; repõe-se sozinho.",
|
|
156
156
|
"descDirtDetected": "Comunica o momento em que o aparelho sinaliza sujidade detetada; repõe-se sozinho.",
|
|
157
157
|
"descCloudOnline": "Se o adaptador alcança a nuvem Govee — não se um dispositivo em particular está presente.",
|
|
158
|
-
"descMembers": "Os dispositivos que este grupo da app comuta em conjunto, com o nome que têm na árvore de objetos."
|
|
158
|
+
"descMembers": "Os dispositivos que este grupo da app comuta em conjunto, com o nome que têm na árvore de objetos.",
|
|
159
|
+
"descMusicAutoColor": "Se a luz escolhe as suas próprias cores no modo música em vez de manter uma cor fixa.",
|
|
160
|
+
"descDreamViewToggle": "Se a luz segue as cores do ecrã captadas por uma câmara Govee DreamView.",
|
|
161
|
+
"descDiyScene": "Uma das cenas que criou no editor DIY da app Govee — vazio até criar uma."
|
|
159
162
|
}
|
package/admin/i18n/ru.json
CHANGED
|
@@ -155,5 +155,8 @@
|
|
|
155
155
|
"descBodyDetected": "Сообщает момент, когда датчик кого-то обнаруживает; сбрасывается сам.",
|
|
156
156
|
"descDirtDetected": "Сообщает момент, когда прибор сигнализирует об обнаруженной грязи; сбрасывается сам.",
|
|
157
157
|
"descCloudOnline": "Достигает ли адаптер облака Govee — а не есть ли отдельное устройство.",
|
|
158
|
-
"descMembers": "Устройства, которые эта группа приложения переключает вместе, под именами из дерева объектов."
|
|
158
|
+
"descMembers": "Устройства, которые эта группа приложения переключает вместе, под именами из дерева объектов.",
|
|
159
|
+
"descMusicAutoColor": "Выбирает ли лампа цвета сама в музыкальном режиме вместо одного постоянного цвета.",
|
|
160
|
+
"descDreamViewToggle": "Следует ли лампа за цветами экрана, которые снимает камера Govee DreamView.",
|
|
161
|
+
"descDiyScene": "Одна из сцен, созданных вами в DIY-редакторе приложения Govee — пусто, пока вы не создали ни одной."
|
|
159
162
|
}
|
package/admin/i18n/uk.json
CHANGED
|
@@ -155,5 +155,8 @@
|
|
|
155
155
|
"descBodyDetected": "Повідомляє момент, коли датчик когось виявляє; скидається самостійно.",
|
|
156
156
|
"descDirtDetected": "Повідомляє момент, коли прилад сигналізує про виявлений бруд; скидається самостійно.",
|
|
157
157
|
"descCloudOnline": "Чи досягає адаптер хмари Govee — а не чи присутній окремий пристрій.",
|
|
158
|
-
"descMembers": "Пристрої, які ця група застосунку перемикає разом, під іменами з дерева об'єктів."
|
|
158
|
+
"descMembers": "Пристрої, які ця група застосунку перемикає разом, під іменами з дерева об'єктів.",
|
|
159
|
+
"descMusicAutoColor": "Чи лампа сама обирає кольори в музичному режимі замість одного сталого кольору.",
|
|
160
|
+
"descDreamViewToggle": "Чи лампа повторює кольори екрана, які знімає камера Govee DreamView.",
|
|
161
|
+
"descDiyScene": "Одна зі сцен, які ви самі створили в DIY-редакторі застосунку Govee — порожньо, поки ви не створили жодної."
|
|
159
162
|
}
|
package/admin/i18n/zh-cn.json
CHANGED
|
@@ -155,5 +155,8 @@
|
|
|
155
155
|
"descBodyDetected": "在传感器检测到有人的瞬间上报;随后自行复位。",
|
|
156
156
|
"descDirtDetected": "在设备提示检测到污垢的瞬间上报;随后自行复位。",
|
|
157
157
|
"descCloudOnline": "适配器能否连通 Govee 云端 —— 而非某个具体设备是否在线。",
|
|
158
|
-
"descMembers": "该 App 分组一起控制的设备,以对象树中的名称列出。"
|
|
158
|
+
"descMembers": "该 App 分组一起控制的设备,以对象树中的名称列出。",
|
|
159
|
+
"descMusicAutoColor": "灯在音乐模式下是否自行选择颜色,而不是保持一种固定颜色。",
|
|
160
|
+
"descDreamViewToggle": "灯是否跟随 Govee DreamView 摄像头捕捉到的屏幕颜色。",
|
|
161
|
+
"descDiyScene": "你在 Govee 应用的 DIY 编辑器中自建的场景之一——在你创建之前为空。"
|
|
159
162
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var appliance_frames_exports = {};
|
|
20
|
+
__export(appliance_frames_exports, {
|
|
21
|
+
decodeApplianceFrames: () => decodeApplianceFrames
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(appliance_frames_exports);
|
|
24
|
+
var import_govee_constants = require("./govee-constants");
|
|
25
|
+
function parseFrame(raw) {
|
|
26
|
+
if (typeof raw !== "string" || raw.length === 0) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const bytes = Buffer.from(raw, "base64");
|
|
30
|
+
if (bytes.length !== 20 || bytes[0] !== 170) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
let xor = 0;
|
|
34
|
+
for (let i = 0; i < 19; i++) {
|
|
35
|
+
xor ^= bytes[i];
|
|
36
|
+
}
|
|
37
|
+
if (xor !== bytes[19]) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
return { fn: bytes[1], sub: bytes[2], bytes };
|
|
41
|
+
}
|
|
42
|
+
function declaredWorkMode(device) {
|
|
43
|
+
var _a, _b, _c, _d;
|
|
44
|
+
const cap = device.capabilities.find((c) => c.type === import_govee_constants.GOVEE_CAP_TYPE.WORK_MODE && c.instance === "workMode");
|
|
45
|
+
const fields = (_b = (_a = cap == null ? void 0 : cap.parameters) == null ? void 0 : _a.fields) != null ? _b : [];
|
|
46
|
+
const modes = (_c = fields.find((f) => f && f.fieldName === "workMode")) == null ? void 0 : _c.options;
|
|
47
|
+
const levels = (_d = fields.find((f) => f && f.fieldName === "modeValue")) == null ? void 0 : _d.options;
|
|
48
|
+
return Array.isArray(modes) && Array.isArray(levels) ? { modes, levels } : null;
|
|
49
|
+
}
|
|
50
|
+
const purifierH7127 = (frames, device) => {
|
|
51
|
+
const out = [];
|
|
52
|
+
const declared = declaredWorkMode(device);
|
|
53
|
+
const modeFrame = frames.find((f) => f.fn === 5 && f.sub === 0);
|
|
54
|
+
const levelFrame = frames.find((f) => f.fn === 5 && f.sub === 1);
|
|
55
|
+
if (declared && modeFrame) {
|
|
56
|
+
const mode = modeFrame.bytes[3];
|
|
57
|
+
const modeOpt = declared.modes.find((o) => o && o.value === mode);
|
|
58
|
+
if (modeOpt) {
|
|
59
|
+
const group = declared.levels.find((o) => o && o.name === modeOpt.name && Array.isArray(o.options));
|
|
60
|
+
let modeValue = 0;
|
|
61
|
+
if (group && levelFrame) {
|
|
62
|
+
const level = levelFrame.bytes[3];
|
|
63
|
+
if (group.options.some((o) => o && o.value === level)) {
|
|
64
|
+
modeValue = level;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
out.push({
|
|
68
|
+
type: import_govee_constants.GOVEE_CAP_TYPE.WORK_MODE,
|
|
69
|
+
instance: "workMode",
|
|
70
|
+
state: { value: { workMode: mode, modeValue } }
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const sensors = frames.find((f) => f.fn === 25 && f.sub === 0);
|
|
75
|
+
if (sensors) {
|
|
76
|
+
const filter = sensors.bytes[7];
|
|
77
|
+
if (filter <= 100 && device.capabilities.some((c) => c.instance === "filterLifeTime")) {
|
|
78
|
+
out.push({ type: import_govee_constants.GOVEE_CAP_TYPE.PROPERTY, instance: "filterLifeTime", state: { value: filter } });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
};
|
|
83
|
+
const DECODERS = {
|
|
84
|
+
H7127: purifierH7127
|
|
85
|
+
};
|
|
86
|
+
function decodeApplianceFrames(device, opCommand) {
|
|
87
|
+
const decoder = DECODERS[device.sku];
|
|
88
|
+
if (!decoder || !Array.isArray(opCommand)) {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
const frames = opCommand.map(parseFrame).filter((f) => f !== null);
|
|
92
|
+
return frames.length > 0 ? decoder(frames, device) : [];
|
|
93
|
+
}
|
|
94
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
95
|
+
0 && (module.exports = {
|
|
96
|
+
decodeApplianceFrames
|
|
97
|
+
});
|
|
98
|
+
//# sourceMappingURL=appliance-frames.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/lib/appliance-frames.ts"],
|
|
4
|
+
"sourcesContent": ["import { GOVEE_CAP_TYPE } from \"./govee-constants\";\nimport type { CapabilityOption, CloudStateCapability, GoveeDevice } from \"./types\";\n\n/**\n * A 20-byte status frame from an appliance's own status push: `aa <fn> <sub>\n * <payload\u2026> <xor>` \u2014 Govee's BLE notification format, relayed verbatim in\n * the account broker's `op.command`. The XOR over bytes 0\u201318 sits in byte 19;\n * measured on 794 of 794 `aa` frames of two H7127 exports (issue #47).\n */\ninterface Frame {\n fn: number;\n sub: number;\n bytes: Buffer;\n}\n\nfunction parseFrame(raw: unknown): Frame | null {\n if (typeof raw !== \"string\" || raw.length === 0) {\n return null;\n }\n const bytes = Buffer.from(raw, \"base64\");\n if (bytes.length !== 20 || bytes[0] !== 0xaa) {\n return null;\n }\n let xor = 0;\n for (let i = 0; i < 19; i++) {\n xor ^= bytes[i];\n }\n if (xor !== bytes[19]) {\n return null;\n }\n return { fn: bytes[1], sub: bytes[2], bytes };\n}\n\ntype FrameDecoder = (frames: Frame[], device: GoveeDevice) => CloudStateCapability[];\n\n/**\n * The `workMode` STRUCT capability the device declared, if any.\n *\n * @param device The device whose declared capabilities are searched\n */\nfunction declaredWorkMode(device: GoveeDevice): { modes: CapabilityOption[]; levels: CapabilityOption[] } | null {\n const cap = device.capabilities.find(c => c.type === GOVEE_CAP_TYPE.WORK_MODE && c.instance === \"workMode\");\n const fields = cap?.parameters?.fields ?? [];\n const modes = fields.find(f => f && f.fieldName === \"workMode\")?.options;\n const levels = fields.find(f => f && f.fieldName === \"modeValue\")?.options;\n return Array.isArray(modes) && Array.isArray(levels) ? { modes, levels } : null;\n}\n\n/**\n * H7127 air purifier (issue #47; spec \u00A711). Measured frames:\n * aa 05 00 <mode> work mode (declared option value)\n * aa 05 01 <level> last manual level (declared gearMode option value)\n * aa 19 00 ff ff <aq> 00 <filter> filter life in % (byte 7 \u2014 two values against\n * two references); byte 5 is probably air quality\n * but was measured once and never seen changing,\n * so it is NOT decoded \u2014 the cloud seed carries it.\n * The level is reported only for a mode that declares levels \u2014 the same shape\n * the cloud state read returns (`{workMode: 2, modeValue: 0}` for Custom).\n *\n * @param frames The checksum-valid frames of one status packet\n * @param device The device the packet belongs to (declared capabilities)\n */\nconst purifierH7127: FrameDecoder = (frames, device) => {\n const out: CloudStateCapability[] = [];\n const declared = declaredWorkMode(device);\n const modeFrame = frames.find(f => f.fn === 0x05 && f.sub === 0x00);\n const levelFrame = frames.find(f => f.fn === 0x05 && f.sub === 0x01);\n if (declared && modeFrame) {\n const mode = modeFrame.bytes[3];\n const modeOpt = declared.modes.find(o => o && o.value === mode);\n if (modeOpt) {\n const group = declared.levels.find(o => o && o.name === modeOpt.name && Array.isArray(o.options));\n let modeValue = 0;\n if (group && levelFrame) {\n const level = levelFrame.bytes[3];\n if (group.options!.some(o => o && o.value === level)) {\n modeValue = level;\n }\n }\n out.push({\n type: GOVEE_CAP_TYPE.WORK_MODE,\n instance: \"workMode\",\n state: { value: { workMode: mode, modeValue } },\n });\n }\n }\n const sensors = frames.find(f => f.fn === 0x19 && f.sub === 0x00);\n if (sensors) {\n const filter = sensors.bytes[7];\n if (filter <= 100 && device.capabilities.some(c => c.instance === \"filterLifeTime\")) {\n out.push({ type: GOVEE_CAP_TYPE.PROPERTY, instance: \"filterLifeTime\", state: { value: filter } });\n }\n }\n return out;\n};\n\n/** One decoder per measured device family. A SKU not listed here gets nothing from its push. */\nconst DECODERS: Readonly<Record<string, FrameDecoder>> = {\n H7127: purifierH7127,\n};\n\n/**\n * Decode an appliance's own status push into the capability shape the cloud\n * pipeline already understands (`applyCloudCapabilities`). Local first: the\n * device says what it does; the cloud read is only the seed at start.\n *\n * @param device The device the packet belongs to\n * @param opCommand `op.command` of the status packet (base64 frames), unchecked\n */\nexport function decodeApplianceFrames(device: GoveeDevice, opCommand: unknown): CloudStateCapability[] {\n const decoder = DECODERS[device.sku];\n if (!decoder || !Array.isArray(opCommand)) {\n return [];\n }\n const frames = opCommand.map(parseFrame).filter((f): f is Frame => f !== null);\n return frames.length > 0 ? decoder(frames, device) : [];\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -365,6 +365,8 @@ function mapProperty(cap) {
|
|
|
365
365
|
({ role, unit } = SENSOR_ROLE_UNIT.battery);
|
|
366
366
|
} else if (instance.includes("co2") || instance.includes("carbondioxide")) {
|
|
367
367
|
({ role, unit } = SENSOR_ROLE_UNIT.co2);
|
|
368
|
+
} else if (instance === "filterlifetime") {
|
|
369
|
+
unit = "%";
|
|
368
370
|
}
|
|
369
371
|
return [
|
|
370
372
|
{
|
|
@@ -721,6 +723,7 @@ function mapMusicSetting(cap) {
|
|
|
721
723
|
states.push({
|
|
722
724
|
id: "music_auto_color",
|
|
723
725
|
name: (0, import_i18n.tName)("musicAutoColor"),
|
|
726
|
+
desc: (0, import_i18n.tDesc)("descMusicAutoColor"),
|
|
724
727
|
type: "boolean",
|
|
725
728
|
role: "switch",
|
|
726
729
|
write: true,
|
|
@@ -799,6 +802,7 @@ const CAPABILITY_NAME_KEYS = {
|
|
|
799
802
|
};
|
|
800
803
|
const CAPABILITY_DESC_KEYS = {
|
|
801
804
|
gradientToggle: "descGradientToggle",
|
|
805
|
+
dreamViewToggle: "descDreamViewToggle",
|
|
802
806
|
airQuality: "descAirQuality",
|
|
803
807
|
filterLifeTime: "descFilterLifeTime",
|
|
804
808
|
// Events reach the tree through TWO paths — this cloud-capability one and
|
|
@@ -835,7 +839,7 @@ function mapCloudStateValue(cap) {
|
|
|
835
839
|
}
|
|
836
840
|
const shortType = cap.type.replace("devices.capabilities.", "");
|
|
837
841
|
const raw = (_a = cap.state) == null ? void 0 : _a.value;
|
|
838
|
-
if (raw === void 0 || raw === null) {
|
|
842
|
+
if (raw === void 0 || raw === null || raw === "") {
|
|
839
843
|
return null;
|
|
840
844
|
}
|
|
841
845
|
switch (shortType) {
|
|
@@ -858,7 +862,7 @@ function mapCloudStateValue(cap) {
|
|
|
858
862
|
}
|
|
859
863
|
if (cap.instance.includes("colorTem")) {
|
|
860
864
|
const n = coerceNum(raw);
|
|
861
|
-
if (n === null) {
|
|
865
|
+
if (n === null || n <= 0) {
|
|
862
866
|
return null;
|
|
863
867
|
}
|
|
864
868
|
return { stateId: "color_temperature", value: n };
|
|
@@ -875,13 +879,7 @@ function mapCloudStateValue(cap) {
|
|
|
875
879
|
value: safeStringify(raw)
|
|
876
880
|
};
|
|
877
881
|
case "dynamic_scene":
|
|
878
|
-
|
|
879
|
-
return null;
|
|
880
|
-
}
|
|
881
|
-
return {
|
|
882
|
-
stateId: sanitizeId(cap.instance),
|
|
883
|
-
value: safeStringify(raw)
|
|
884
|
-
};
|
|
882
|
+
return null;
|
|
885
883
|
case "work_mode": {
|
|
886
884
|
const struct = typeof raw === "object" && raw !== null ? raw : void 0;
|
|
887
885
|
const n = coerceNum(struct ? struct.workMode : raw);
|
|
@@ -967,7 +965,14 @@ function mapCloudStateValues(cap, declared) {
|
|
|
967
965
|
}
|
|
968
966
|
const SCENE_DROPDOWN_RULES = [
|
|
969
967
|
{ id: "light_scene", cap: "lightScene", nameKey: "lightScene", channel: "scenes", source: (d) => d.scenes },
|
|
970
|
-
{
|
|
968
|
+
{
|
|
969
|
+
id: "diy_scene",
|
|
970
|
+
cap: "diyScene",
|
|
971
|
+
nameKey: "diyScene",
|
|
972
|
+
descKey: "descDiyScene",
|
|
973
|
+
channel: "scenes",
|
|
974
|
+
source: (d) => d.diyScenes
|
|
975
|
+
},
|
|
971
976
|
{
|
|
972
977
|
id: "snapshot_cloud",
|
|
973
978
|
cap: "snapshot",
|