iobroker.govee-smart 2.40.0 → 3.0.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 +15 -17
- package/admin/i18n/de.json +0 -2
- package/admin/i18n/en.json +0 -2
- package/admin/i18n/es.json +0 -2
- package/admin/i18n/fr.json +0 -2
- package/admin/i18n/it.json +0 -2
- package/admin/i18n/nl.json +0 -2
- package/admin/i18n/pl.json +0 -2
- package/admin/i18n/pt.json +0 -2
- package/admin/i18n/ru.json +0 -2
- package/admin/i18n/uk.json +0 -2
- package/admin/i18n/zh-cn.json +0 -2
- package/admin/jsonConfig.json +0 -10
- package/build/lib/appliance-frames.js +10 -1
- package/build/lib/appliance-frames.js.map +2 -2
- package/build/lib/capability-mapper.js +14 -4
- package/build/lib/capability-mapper.js.map +2 -2
- package/build/lib/cloud-outage.js +85 -0
- package/build/lib/cloud-outage.js.map +7 -0
- package/build/lib/device-id-migration.js +136 -0
- package/build/lib/device-id-migration.js.map +7 -0
- package/build/lib/device-id.js +151 -0
- package/build/lib/device-id.js.map +7 -0
- package/build/lib/device-key.js +5 -0
- package/build/lib/device-key.js.map +2 -2
- package/build/lib/device-manager.js +16 -2
- package/build/lib/device-manager.js.map +2 -2
- package/build/lib/device-move.js +184 -0
- package/build/lib/device-move.js.map +7 -0
- package/build/lib/enum-carry.js +65 -0
- package/build/lib/enum-carry.js.map +7 -0
- package/build/lib/govee-cloud-client.js +33 -9
- package/build/lib/govee-cloud-client.js.map +2 -2
- package/build/lib/govee-lan-client.js +34 -26
- package/build/lib/govee-lan-client.js.map +2 -2
- package/build/lib/govee-openapi-mqtt-client.js +1 -1
- package/build/lib/govee-openapi-mqtt-client.js.map +2 -2
- package/build/lib/handlers/cloud-retry-handler.js +27 -8
- package/build/lib/handlers/cloud-retry-handler.js.map +2 -2
- package/build/lib/handlers/connection-state.js +5 -4
- package/build/lib/handlers/connection-state.js.map +2 -2
- package/build/lib/local-snapshots.js +39 -3
- package/build/lib/local-snapshots.js.map +2 -2
- package/build/lib/native-key-migration.js +44 -8
- package/build/lib/native-key-migration.js.map +2 -2
- package/build/lib/sku-cache.js +40 -4
- package/build/lib/sku-cache.js.map +2 -2
- package/build/lib/state-manager.js +86 -39
- package/build/lib/state-manager.js.map +2 -2
- package/build/lib/timing-constants.js +3 -0
- package/build/lib/timing-constants.js.map +2 -2
- package/build/main.js +28 -11
- package/build/main.js.map +3 -3
- package/io-package.json +70 -29
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ Full user documentation lives in the **[Wiki](https://github.com/krobipd/ioBroke
|
|
|
47
47
|
|
|
48
48
|
## Sentry / Error reporting
|
|
49
49
|
|
|
50
|
-
**This adapter uses Sentry libraries to automatically report exceptions and code errors to the developers.** Reporting
|
|
50
|
+
**This adapter uses Sentry libraries to automatically report exceptions and code errors to the developers.** Reporting is active by default. It stays off when the ioBroker diagnostics setting is `none` (`diag` in the system configuration), when data reporting is disabled for this instance or its host (`disableDataReporting`), and on CI systems. A report contains the error with its stack trace and technical context such as versions and platform, plus an anonymous installation ID.
|
|
51
51
|
|
|
52
52
|
For details and how to disable it, see the [Sentry plugin documentation](https://github.com/ioBroker/plugin-sentry#plugin-sentry). Error reporting requires js-controller 3.0 or newer.
|
|
53
53
|
|
|
@@ -55,7 +55,7 @@ For details and how to disable it, see the [Sentry plugin documentation](https:/
|
|
|
55
55
|
|
|
56
56
|
- Node.js >= 22
|
|
57
57
|
- ioBroker js-controller >= 7.2.2
|
|
58
|
-
- ioBroker Admin >= 8.0.
|
|
58
|
+
- ioBroker Admin >= 8.0.14
|
|
59
59
|
- A Govee account and at least one Govee WiFi device. LAN control needs a light with LAN mode enabled in the Govee Home app — see Govee's [LAN-supported device list](https://app-h5.govee.com/user-manual/wlan-guide).
|
|
60
60
|
|
|
61
61
|
> The adapter CANNOT be installed via GitHub: The adapter must be installed via the ioBroker repository (stable or latest).
|
|
@@ -95,6 +95,19 @@ This adapter's MQTT authentication and BLE-over-LAN (ptReal) protocol implementa
|
|
|
95
95
|
### **WORK IN PROGRESS**
|
|
96
96
|
-->
|
|
97
97
|
|
|
98
|
+
### 3.0.0 (2026-09-26)
|
|
99
|
+
|
|
100
|
+
- Changed: every device gets a new object ID once — model and last four characters with a hyphen, e.g. `devices.h61be-525f`; scripts and visualizations need the new IDs
|
|
101
|
+
- Changed: the move carries values, recording settings, rooms, functions and aliases along, and recorded history continues in its old series
|
|
102
|
+
- Fixed: two devices of one model whose IDs end alike now get a tree each and each receives its own commands — until now they shared one
|
|
103
|
+
- New: the H1741 battery table lamp reports its charge level in `sensor.battery`; Govee reports a fully charged battery as about 80 percent
|
|
104
|
+
- Fixed: fans and heaters with a numeric level (H7102, H7130) store it as a number, and the H7121 no longer puts a warning in the log at every refresh
|
|
105
|
+
|
|
106
|
+
### 2.41.0 (2026-09-26)
|
|
107
|
+
|
|
108
|
+
- Changed: Discovery follows the selected network interface only — the additional scan addresses setting is gone, and the broadcast goes to the network of the chosen card
|
|
109
|
+
- Fixed: `info.cloudConnected` turns false while the Govee Cloud stays unreachable and true again with its next answer — until now only a rejected API key cleared it
|
|
110
|
+
|
|
98
111
|
### 2.40.0 (2026-09-25)
|
|
99
112
|
|
|
100
113
|
- New: Optional additional scan addresses — lights in another subnet or behind a router that blocks multicast are found by asking them directly
|
|
@@ -129,21 +142,6 @@ This adapter's MQTT authentication and BLE-over-LAN (ptReal) protocol implementa
|
|
|
129
142
|
- Fixed: The diagnostics report no longer contains your Wi-Fi network name, the Govee app's device number or a group's id — they appeared in clear in every exported file
|
|
130
143
|
- Improved: The diagnostics report keeps Govee's complete account-list entry and shows commands waiting for an offline device and when the adapter started
|
|
131
144
|
|
|
132
|
-
### 2.39.0 (2026-09-22)
|
|
133
|
-
|
|
134
|
-
- New: A device that has gone quiet is asked for its status over the Govee account connection — a bulb or purifier that works but showed as unreachable now stays reachable
|
|
135
|
-
- Improved: The cloud budget follows Govee's per-device limits — a command for one light no longer waits for another light's calls or for library downloads
|
|
136
|
-
- New: A command Govee refused because the device was offline is delivered once the device reports back (within five minutes), instead of being lost
|
|
137
|
-
- Fixed: Scenes and libraries that arrived after a busy start now reach the scene dropdown and the cache — they used to stay at `---` and were fetched again on every start
|
|
138
|
-
- Fixed: A scene list that shrank no longer leaves withdrawn scenes in the dropdown
|
|
139
|
-
- Fixed: The diagnostics report no longer lists a reachability refresh for appliances, which never get one
|
|
140
|
-
- New: 28 more Govee models are recognised — meat thermometers, motion and pressure sensors, heaters, kettles, a composter and the gateways that carry battery sensors
|
|
141
|
-
- New: H1771 Table Lamp reported working by a user
|
|
142
|
-
|
|
143
|
-
### 2.38.3 (2026-09-17) — stable
|
|
144
|
-
|
|
145
|
-
- Changed: Internal cleanup. No user-facing changes.
|
|
146
|
-
|
|
147
145
|
[Older changelogs can be found there](CHANGELOG_OLD.md)
|
|
148
146
|
|
|
149
147
|
## Support
|
package/admin/i18n/de.json
CHANGED
|
@@ -74,8 +74,6 @@
|
|
|
74
74
|
"musicSensitivity": "Musik-Empfindlichkeit",
|
|
75
75
|
"bindLabel": "Netzwerk-Interface",
|
|
76
76
|
"bindTooltip": "Netzwerk-Interface für LAN-Discovery und -Steuerung. Standard (0.0.0.0 = alle Interfaces) beibehalten, außer dieser Host hat mehrere Netzwerk-Interfaces.",
|
|
77
|
-
"scanTargetsLabel": "Zusätzliche Suchadressen",
|
|
78
|
-
"scanTargetsTooltip": "IPv4-Adressen von Govee-Lampen, die die Suche zusätzlich direkt fragen soll, durch Kommas getrennt — für Lampen in einem anderen Subnetz oder hinter einem Router, der Multicast und Broadcast blockt. Normalerweise leer lassen.",
|
|
79
77
|
"online": "Online",
|
|
80
78
|
"power": "Ein/Aus",
|
|
81
79
|
"refreshCloud": "Cloud-Daten aktualisieren",
|
package/admin/i18n/en.json
CHANGED
|
@@ -74,8 +74,6 @@
|
|
|
74
74
|
"musicSensitivity": "Music Sensitivity",
|
|
75
75
|
"bindLabel": "Network Interface",
|
|
76
76
|
"bindTooltip": "Select the network interface for LAN discovery and control. Keep the default (0.0.0.0 = all interfaces) unless this host has multiple network interfaces.",
|
|
77
|
-
"scanTargetsLabel": "Additional scan addresses",
|
|
78
|
-
"scanTargetsTooltip": "IPv4 addresses of Govee lights the discovery should also ask directly, separated by commas — for lights in another subnet or behind a router that blocks multicast and broadcast. Leave empty normally.",
|
|
79
77
|
"online": "Online",
|
|
80
78
|
"power": "Power",
|
|
81
79
|
"refreshCloud": "Refresh Cloud Data",
|
package/admin/i18n/es.json
CHANGED
|
@@ -74,8 +74,6 @@
|
|
|
74
74
|
"musicSensitivity": "Sensibilidad musical",
|
|
75
75
|
"bindLabel": "Interfaz de red",
|
|
76
76
|
"bindTooltip": "Selecciona la interfaz de red para el descubrimiento y control LAN. Mantén el valor predeterminado (0.0.0.0 = todas) salvo que este host tenga varias interfaces.",
|
|
77
|
-
"scanTargetsLabel": "Direcciones de búsqueda adicionales",
|
|
78
|
-
"scanTargetsTooltip": "Direcciones IPv4 de luces Govee que la búsqueda debe consultar también directamente, separadas por comas — para luces en otra subred o detrás de un router que bloquea multicast y broadcast. Normalmente déjelo vacío.",
|
|
79
77
|
"online": "Conectado",
|
|
80
78
|
"power": "Encendido",
|
|
81
79
|
"refreshCloud": "Actualizar datos Cloud",
|
package/admin/i18n/fr.json
CHANGED
|
@@ -74,8 +74,6 @@
|
|
|
74
74
|
"musicSensitivity": "Sensibilité musicale",
|
|
75
75
|
"bindLabel": "Interface réseau",
|
|
76
76
|
"bindTooltip": "Sélectionnez l'interface réseau pour la découverte et le contrôle LAN. Conservez la valeur par défaut (0.0.0.0 = toutes) sauf si cet hôte possède plusieurs interfaces.",
|
|
77
|
-
"scanTargetsLabel": "Adresses de recherche supplémentaires",
|
|
78
|
-
"scanTargetsTooltip": "Adresses IPv4 de lampes Govee que la recherche doit aussi interroger directement, séparées par des virgules — pour des lampes dans un autre sous-réseau ou derrière un routeur qui bloque le multicast et le broadcast. Laisser vide en temps normal.",
|
|
79
77
|
"online": "En ligne",
|
|
80
78
|
"power": "Marche/Arrêt",
|
|
81
79
|
"refreshCloud": "Actualiser données Cloud",
|
package/admin/i18n/it.json
CHANGED
|
@@ -74,8 +74,6 @@
|
|
|
74
74
|
"musicSensitivity": "Sensibilità musica",
|
|
75
75
|
"bindLabel": "Interfaccia di rete",
|
|
76
76
|
"bindTooltip": "Seleziona l'interfaccia di rete per il rilevamento e il controllo LAN. Mantieni il valore predefinito (0.0.0.0 = tutte) a meno che questo host non abbia più interfacce.",
|
|
77
|
-
"scanTargetsLabel": "Indirizzi di ricerca aggiuntivi",
|
|
78
|
-
"scanTargetsTooltip": "Indirizzi IPv4 di luci Govee che la ricerca deve interrogare anche direttamente, separati da virgole — per luci in un'altra sottorete o dietro un router che blocca multicast e broadcast. Di norma lasciare vuoto.",
|
|
79
77
|
"online": "Online",
|
|
80
78
|
"power": "Accensione",
|
|
81
79
|
"refreshCloud": "Aggiorna dati Cloud",
|
package/admin/i18n/nl.json
CHANGED
|
@@ -74,8 +74,6 @@
|
|
|
74
74
|
"musicSensitivity": "Muziekgevoeligheid",
|
|
75
75
|
"bindLabel": "Netwerkinterface",
|
|
76
76
|
"bindTooltip": "Selecteer de netwerkinterface voor LAN-detectie en -besturing. Behoud de standaard (0.0.0.0 = alle interfaces), tenzij deze host meerdere netwerkinterfaces heeft.",
|
|
77
|
-
"scanTargetsLabel": "Extra zoekadressen",
|
|
78
|
-
"scanTargetsTooltip": "IPv4-adressen van Govee-lampen die de zoekactie ook rechtstreeks moet bevragen, gescheiden door komma's — voor lampen in een ander subnet of achter een router die multicast en broadcast blokkeert. Normaal leeg laten.",
|
|
79
77
|
"online": "Online",
|
|
80
78
|
"power": "Aan/Uit",
|
|
81
79
|
"refreshCloud": "Cloudgegevens vernieuwen",
|
package/admin/i18n/pl.json
CHANGED
|
@@ -74,8 +74,6 @@
|
|
|
74
74
|
"musicSensitivity": "Czułość muzyki",
|
|
75
75
|
"bindLabel": "Interfejs sieciowy",
|
|
76
76
|
"bindTooltip": "Wybierz interfejs sieciowy do wykrywania i sterowania LAN. Pozostaw domyślny (0.0.0.0 = wszystkie), chyba że ten host ma wiele interfejsów.",
|
|
77
|
-
"scanTargetsLabel": "Dodatkowe adresy wyszukiwania",
|
|
78
|
-
"scanTargetsTooltip": "Adresy IPv4 lamp Govee, o które wyszukiwanie ma dodatkowo pytać bezpośrednio, rozdzielone przecinkami — dla lamp w innej podsieci lub za routerem blokującym multicast i broadcast. Zwykle zostaw puste.",
|
|
79
77
|
"online": "Online",
|
|
80
78
|
"power": "Zasilanie",
|
|
81
79
|
"refreshCloud": "Odśwież dane Cloud",
|
package/admin/i18n/pt.json
CHANGED
|
@@ -74,8 +74,6 @@
|
|
|
74
74
|
"musicSensitivity": "Sensibilidade musical",
|
|
75
75
|
"bindLabel": "Interface de rede",
|
|
76
76
|
"bindTooltip": "Selecione a interface de rede para descoberta e controlo LAN. Mantenha o padrão (0.0.0.0 = todas) a menos que este host tenha várias interfaces.",
|
|
77
|
-
"scanTargetsLabel": "Endereços de pesquisa adicionais",
|
|
78
|
-
"scanTargetsTooltip": "Endereços IPv4 de luzes Govee que a pesquisa deve também consultar diretamente, separados por vírgulas — para luzes noutra sub-rede ou atrás de um router que bloqueia multicast e broadcast. Normalmente deixe vazio.",
|
|
79
77
|
"online": "Online",
|
|
80
78
|
"power": "Ligar/Desligar",
|
|
81
79
|
"refreshCloud": "Atualizar dados da Cloud",
|
package/admin/i18n/ru.json
CHANGED
|
@@ -74,8 +74,6 @@
|
|
|
74
74
|
"musicSensitivity": "Чувствительность к музыке",
|
|
75
75
|
"bindLabel": "Сетевой интерфейс",
|
|
76
76
|
"bindTooltip": "Выберите сетевой интерфейс для обнаружения и управления по LAN. Оставьте значение по умолчанию (0.0.0.0 = все интерфейсы), если у хоста нет нескольких интерфейсов.",
|
|
77
|
-
"scanTargetsLabel": "Дополнительные адреса поиска",
|
|
78
|
-
"scanTargetsTooltip": "IPv4-адреса ламп Govee, которые поиск должен дополнительно опрашивать напрямую, через запятую — для ламп в другой подсети или за маршрутизатором, блокирующим multicast и broadcast. Обычно оставьте пустым.",
|
|
79
77
|
"online": "Онлайн",
|
|
80
78
|
"power": "Питание",
|
|
81
79
|
"refreshCloud": "Обновить данные Cloud",
|
package/admin/i18n/uk.json
CHANGED
|
@@ -74,8 +74,6 @@
|
|
|
74
74
|
"musicSensitivity": "Чутливість до музики",
|
|
75
75
|
"bindLabel": "Мережевий інтерфейс",
|
|
76
76
|
"bindTooltip": "Виберіть мережевий інтерфейс для виявлення та керування по LAN. Залиште типове значення (0.0.0.0 = всі інтерфейси), якщо хост не має кількох інтерфейсів.",
|
|
77
|
-
"scanTargetsLabel": "Додаткові адреси пошуку",
|
|
78
|
-
"scanTargetsTooltip": "IPv4-адреси ламп Govee, які пошук має додатково опитувати напряму, через кому — для ламп в іншій підмережі або за маршрутизатором, що блокує multicast і broadcast. Зазвичай залиште порожнім.",
|
|
79
77
|
"online": "Онлайн",
|
|
80
78
|
"power": "Живлення",
|
|
81
79
|
"refreshCloud": "Оновити дані Cloud",
|
package/admin/i18n/zh-cn.json
CHANGED
|
@@ -74,8 +74,6 @@
|
|
|
74
74
|
"musicSensitivity": "音乐灵敏度",
|
|
75
75
|
"bindLabel": "网络接口",
|
|
76
76
|
"bindTooltip": "选择用于局域网发现和控制的网络接口。除非此主机有多个网络接口,否则请保留默认值(0.0.0.0 = 所有接口)。",
|
|
77
|
-
"scanTargetsLabel": "附加扫描地址",
|
|
78
|
-
"scanTargetsTooltip": "发现过程还应直接询问的 Govee 灯的 IPv4 地址,用逗号分隔——适用于位于其他子网或位于阻止组播和广播的路由器之后的灯。通常留空。",
|
|
79
77
|
"online": "在线",
|
|
80
78
|
"power": "电源",
|
|
81
79
|
"refreshCloud": "刷新云数据",
|
package/admin/jsonConfig.json
CHANGED
|
@@ -77,16 +77,6 @@
|
|
|
77
77
|
"lg": 6,
|
|
78
78
|
"xl": 6
|
|
79
79
|
},
|
|
80
|
-
"scanTargets": {
|
|
81
|
-
"type": "text",
|
|
82
|
-
"label": "scanTargetsLabel",
|
|
83
|
-
"tooltip": "scanTargetsTooltip",
|
|
84
|
-
"xs": 12,
|
|
85
|
-
"sm": 12,
|
|
86
|
-
"md": 6,
|
|
87
|
-
"lg": 6,
|
|
88
|
-
"xl": 6
|
|
89
|
-
},
|
|
90
80
|
"_advancedInfo": {
|
|
91
81
|
"newLine": true,
|
|
92
82
|
"type": "staticText",
|
|
@@ -80,8 +80,17 @@ const purifierH7127 = (frames, device) => {
|
|
|
80
80
|
}
|
|
81
81
|
return out;
|
|
82
82
|
};
|
|
83
|
+
const lampH1741 = (frames) => {
|
|
84
|
+
const frame = frames.find((f) => f.fn === 66);
|
|
85
|
+
if (!frame) {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
const level = frame.sub & 127;
|
|
89
|
+
return level <= 100 ? [{ type: import_govee_constants.GOVEE_CAP_TYPE.PROPERTY, instance: "battery", state: { value: level } }] : [];
|
|
90
|
+
};
|
|
83
91
|
const DECODERS = {
|
|
84
|
-
H7127: purifierH7127
|
|
92
|
+
H7127: purifierH7127,
|
|
93
|
+
H1741: lampH1741
|
|
85
94
|
};
|
|
86
95
|
function decodeApplianceFrames(device, opCommand) {
|
|
87
96
|
const decoder = DECODERS[device.sku];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
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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",
|
|
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/**\n * H1741 battery table lamp (issue #50). Measured frame:\n * aa 42 <b> battery level in % (bits 0\u20136)\n * Four reports against the app's own battery icon: 0x30 = two of three bars (2026-09-23), 0x50 right\n * after a full charge, 0x20 one bar shortly before the battery saver (2026-09-25/26). Govee reports a\n * full battery as 80\u201384, not 100 \u2014 the value is shown as it comes. Bit 7 was set only in the minutes\n * before the lamp was unplugged (0xd4); that it means \"charging\" is not measured, so it is masked and\n * not decoded. `aa 42` exists on mains devices too (H1310 `aa4200`, H70B1 `aa4202`) \u2014 hence per SKU.\n *\n * @param frames The checksum-valid frames of one status packet\n * @returns the battery reading, or nothing\n */\nconst lampH1741: FrameDecoder = frames => {\n const frame = frames.find(f => f.fn === 0x42);\n if (!frame) {\n return [];\n }\n const level = frame.sub & 0x7f;\n return level <= 100 ? [{ type: GOVEE_CAP_TYPE.PROPERTY, instance: \"battery\", state: { value: level } }] : [];\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 H1741: lampH1741,\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
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1099,7 +1099,7 @@ function planCloudCapabilityWrites(caps, hasLanIp, lanStateIds, declared) {
|
|
|
1099
1099
|
return writes;
|
|
1100
1100
|
}
|
|
1101
1101
|
function mapCloudStateValues(cap, declared) {
|
|
1102
|
-
var _a, _b, _c, _d, _e
|
|
1102
|
+
var _a, _b, _c, _d, _e;
|
|
1103
1103
|
const primary = mapCloudStateValue(cap);
|
|
1104
1104
|
if (!primary) {
|
|
1105
1105
|
return [];
|
|
@@ -1120,9 +1120,19 @@ function mapCloudStateValues(cap, declared) {
|
|
|
1120
1120
|
if (level === null) {
|
|
1121
1121
|
return [primary];
|
|
1122
1122
|
}
|
|
1123
|
-
const
|
|
1124
|
-
|
|
1125
|
-
|
|
1123
|
+
const kind = declared ? modeLevelKind(
|
|
1124
|
+
(_e = (_d = (_c = declared.find((c) => c.type === cap.type && c.instance === cap.instance)) == null ? void 0 : _c.parameters) == null ? void 0 : _d.fields) == null ? void 0 : _e.find((f) => f && f.fieldName === "modeValue")
|
|
1125
|
+
) : "dropdown";
|
|
1126
|
+
if (kind === "none") {
|
|
1127
|
+
return [primary];
|
|
1128
|
+
}
|
|
1129
|
+
return [primary, { stateId: "mode_value", value: kind === "number" ? level : safeStringify(level) }];
|
|
1130
|
+
}
|
|
1131
|
+
function modeLevelKind(field) {
|
|
1132
|
+
if ((field == null ? void 0 : field.options) && field.options.length > 0) {
|
|
1133
|
+
return classifyModeLevels(field.options).kind;
|
|
1134
|
+
}
|
|
1135
|
+
return (field == null ? void 0 : field.range) ? "number" : "none";
|
|
1126
1136
|
}
|
|
1127
1137
|
const SCENE_DROPDOWN_RULES = [
|
|
1128
1138
|
{ id: "light_scene", cap: "lightScene", nameKey: "lightScene", channel: "scenes", source: (d) => d.scenes },
|