iobroker.govee-smart 2.4.0 → 2.5.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 +8 -12
- package/build/lib/group-fanout.js +136 -0
- package/build/lib/group-fanout.js.map +7 -0
- package/build/lib/message-router.js +161 -0
- package/build/lib/message-router.js.map +7 -0
- package/build/main.js +64 -209
- package/build/main.js.map +2 -2
- package/io-package.json +27 -27
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -124,6 +124,14 @@ This adapter's MQTT authentication and BLE-over-LAN (ptReal) protocol implementa
|
|
|
124
124
|
---
|
|
125
125
|
|
|
126
126
|
## Changelog
|
|
127
|
+
### 2.5.0 (2026-05-04)
|
|
128
|
+
|
|
129
|
+
- F4 final: `onMessage`-Handler (sendTo aus dem Admin-UI) ist jetzt eine eigene Klasse mit Host-Interface. main.ts deutlich kleiner, Login-Test/2FA-Code-Anforderung isoliert testbar. Verhalten identisch.
|
|
130
|
+
|
|
131
|
+
### 2.4.1 (2026-05-04)
|
|
132
|
+
|
|
133
|
+
- Group-Fan-Out-Pfad (Mitglieder-Steuerung beim Schalten der Gruppe) ist jetzt eine eigene Klasse mit Host-Interface — `main.ts` nochmal kleiner. Verhalten identisch.
|
|
134
|
+
|
|
127
135
|
### 2.4.0 (2026-05-04)
|
|
128
136
|
|
|
129
137
|
- Lokaler Snapshot-Manager (Save/Restore/Delete) ist jetzt eine eigene Klasse mit Host-Interface — `main.ts` ist kleiner und der Snapshot-Pfad ist isoliert testbar. Verhalten identisch.
|
|
@@ -137,18 +145,6 @@ This adapter's MQTT authentication and BLE-over-LAN (ptReal) protocol implementa
|
|
|
137
145
|
- App-Version-Drift-Monitor: täglicher iTunes-Lookup vergleicht die im Adapter hinterlegte Govee-App-Version mit der aktuellen iOS-Version. Bei Drift > 2 Minor wird gewarnt — Govees undokumentierte Endpoints rejecten gelegentlich zu alte Clients.
|
|
138
146
|
- Code-Hygiene: `onStateChange`-Handler in eigene Methoden für Diagnostics-Export und Generic-Capability-Routing aufgeteilt. Magic-Numbers durch `timing-constants.ts` ersetzt. Lifecycle-Flags besser dokumentiert.
|
|
139
147
|
|
|
140
|
-
### 2.2.0 (2026-05-04)
|
|
141
|
-
|
|
142
|
-
- 2FA-Verifizierung triggert keinen Restart mehr. MQTT-Pushes typsicher, Sensor-Datenpunkte im richtigen Kanal (`sensor/`, `events/` statt `control/`).
|
|
143
|
-
- Ready-Log zeigt was operational ist: Channel-Status (LAN+Cloud+MQTT+Cloud-events), Lichter-Online-Count, Sensoren mit Datenmarker. Wartet auf den ersten Sensor-Poll.
|
|
144
|
-
- Persistente UDP-Sockets, abbrechbare HTTP-Calls, HTTP keep-alive (~200 ms schneller), Backoff-Jitter gegen Thundering Herd.
|
|
145
|
-
- Memory-Leaks beim Device-Remove geschlossen — Diagnostics-Buffer und State-Tree werden gemeinsam aufgeräumt wenn ein Gerät aus dem Govee-Account verschwindet.
|
|
146
|
-
- Kein WARN-Spam mehr für Group-State `info.membersUnreachable`. Plus XOR-Validierung für MQTT-BLE-Pakete, type-Guards an allen API-Boundaries, `tier: 2`.
|
|
147
|
-
|
|
148
|
-
### 2.1.4 (2026-05-03)
|
|
149
|
-
|
|
150
|
-
- Online status correct again after adapter restart — lights flip to online with the first LAN scan, sensors with the first cloud poll (5 s after start instead of 2 minutes).
|
|
151
|
-
|
|
152
148
|
Older entries are in [CHANGELOG_OLD.md](CHANGELOG_OLD.md).
|
|
153
149
|
|
|
154
150
|
## Support
|
|
@@ -0,0 +1,136 @@
|
|
|
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 group_fanout_exports = {};
|
|
20
|
+
__export(group_fanout_exports, {
|
|
21
|
+
GroupFanoutHandler: () => GroupFanoutHandler
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(group_fanout_exports);
|
|
24
|
+
var import_types = require("./types");
|
|
25
|
+
class GroupFanoutHandler {
|
|
26
|
+
/**
|
|
27
|
+
* @param host Adapter dependencies via Host-Interface
|
|
28
|
+
*/
|
|
29
|
+
constructor(host) {
|
|
30
|
+
this.host = host;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Fan out a group command to all online member devices.
|
|
34
|
+
* Basic controls (power/brightness/color) gehen direkt durch.
|
|
35
|
+
* Scenes/music werden Name-basiert gemappt.
|
|
36
|
+
*
|
|
37
|
+
* @param group BaseGroup-Device
|
|
38
|
+
* @param stateSuffix State-Suffix (z.B. "control.power" oder "scenes.light_scene")
|
|
39
|
+
* @param value Command-Value
|
|
40
|
+
*/
|
|
41
|
+
async fanOut(group, stateSuffix, value) {
|
|
42
|
+
if (!group.groupMembers) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const devices = this.host.getDevices();
|
|
46
|
+
const members = this.resolveMembers(group, devices).filter((d) => d.state.online);
|
|
47
|
+
if (members.length === 0) {
|
|
48
|
+
this.host.log.debug(`Group "${group.name}": no reachable members for fan-out`);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const command = this.host.stateToCommand(stateSuffix);
|
|
52
|
+
if (!command) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if ((command === "lightScene" || command === "music") && (value === "0" || value === 0)) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
for (const member of members) {
|
|
59
|
+
try {
|
|
60
|
+
if (command === "lightScene") {
|
|
61
|
+
await this.fanOutScene(group, member, value);
|
|
62
|
+
} else if (command === "music") {
|
|
63
|
+
await this.fanOutMusic(group, member, stateSuffix, value);
|
|
64
|
+
} else {
|
|
65
|
+
await this.host.sendCommand(member, command, value);
|
|
66
|
+
}
|
|
67
|
+
} catch (err) {
|
|
68
|
+
this.host.log.debug(`Group fan-out to ${member.name}: ${(0, import_types.errMessage)(err)}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Resolve group member references to actual device objects.
|
|
74
|
+
*
|
|
75
|
+
* @param group BaseGroup device with groupMembers
|
|
76
|
+
* @param devices Full device list to search
|
|
77
|
+
*/
|
|
78
|
+
resolveMembers(group, devices) {
|
|
79
|
+
if (!group.groupMembers) {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
return group.groupMembers.map((m) => devices.find((d) => d.sku === m.sku && d.deviceId === m.deviceId)).filter((d) => d !== void 0);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Fan out a scene command: match group scene name → member scene index.
|
|
86
|
+
*
|
|
87
|
+
* @param group BaseGroup device
|
|
88
|
+
* @param member Target member device
|
|
89
|
+
* @param value Dropdown index value
|
|
90
|
+
*/
|
|
91
|
+
async fanOutScene(group, member, value) {
|
|
92
|
+
var _a;
|
|
93
|
+
const groupPrefix = this.host.devicePrefix(group);
|
|
94
|
+
const obj = await this.host.getObject(`${this.host.namespace}.${groupPrefix}.scenes.light_scene`);
|
|
95
|
+
const groupStates = (_a = obj == null ? void 0 : obj.common) == null ? void 0 : _a.states;
|
|
96
|
+
const sceneName = groupStates == null ? void 0 : groupStates[String(value)];
|
|
97
|
+
if (!sceneName) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const memberIdx = member.scenes.findIndex((s) => s.name === sceneName);
|
|
101
|
+
if (memberIdx >= 0) {
|
|
102
|
+
await this.host.sendCommand(member, "lightScene", memberIdx + 1);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Fan out a music command: match group music name → member music index.
|
|
107
|
+
*
|
|
108
|
+
* @param group BaseGroup device
|
|
109
|
+
* @param member Target member device
|
|
110
|
+
* @param stateSuffix Music-state-suffix
|
|
111
|
+
* @param value Command value
|
|
112
|
+
*/
|
|
113
|
+
async fanOutMusic(group, member, stateSuffix, value) {
|
|
114
|
+
var _a;
|
|
115
|
+
if (stateSuffix !== "music.music_mode") {
|
|
116
|
+
await this.host.sendMusicCommand(member, this.host.devicePrefix(member), stateSuffix, value);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const groupPrefix = this.host.devicePrefix(group);
|
|
120
|
+
const obj = await this.host.getObject(`${this.host.namespace}.${groupPrefix}.music.music_mode`);
|
|
121
|
+
const groupStates = (_a = obj == null ? void 0 : obj.common) == null ? void 0 : _a.states;
|
|
122
|
+
const musicName = groupStates == null ? void 0 : groupStates[String(value)];
|
|
123
|
+
if (!musicName) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const memberIdx = member.musicLibrary.findIndex((m) => m.name === musicName);
|
|
127
|
+
if (memberIdx >= 0) {
|
|
128
|
+
await this.host.sendMusicCommand(member, this.host.devicePrefix(member), "music.music_mode", memberIdx + 1);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
133
|
+
0 && (module.exports = {
|
|
134
|
+
GroupFanoutHandler
|
|
135
|
+
});
|
|
136
|
+
//# sourceMappingURL=group-fanout.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/lib/group-fanout.ts"],
|
|
4
|
+
"sourcesContent": ["import { errMessage, type GoveeDevice } from \"./types\";\n\n/**\n * Host-Interface f\u00FCr GroupFanoutHandler \u2014 die Adapter-Funktionen die der\n * Handler braucht ohne von der Adapter-Klasse direkt zu h\u00E4ngen.\n *\n * Pattern analog `WizardHost` und `SnapshotHandlerHost`. main.ts bleibt\n * schlank, der Group-Fan-Out-Pfad ist isoliert testbar.\n */\nexport interface GroupFanoutHost {\n /** Adapter logger. */\n log: ioBroker.Logger;\n /** Adapter-namespace prefix (z.B. \"govee-smart.0\"). */\n namespace: string;\n /** Device-Liste \u2014 typisch DeviceManager.getDevices(). */\n getDevices: () => GoveeDevice[];\n /** Send-command via LAN\u2192Cloud-Routing (DeviceManager.sendCommand). */\n sendCommand: (device: GoveeDevice, command: string, value: unknown) => Promise<void>;\n /** Resolved object-prefix f\u00FCr ein Ger\u00E4t. */\n devicePrefix: (device: GoveeDevice) => string;\n /** State-Suffix \u2192 Command-Name lookup (main.ts STATE_TO_COMMAND-Map). */\n stateToCommand: (stateSuffix: string) => string | undefined;\n /** Get-object \u2014 f\u00FCr common.states-Lookup beim scene/music Mapping. */\n getObject: (id: string) => Promise<ioBroker.Object | null | undefined>;\n /** Music-Command-Sender (kapselt die music_mode/sensitivity/auto_color STRUCT). */\n sendMusicCommand: (\n device: GoveeDevice,\n devicePrefix: string,\n stateSuffix: string,\n value: ioBroker.StateValue,\n ) => Promise<void>;\n}\n\n/**\n * Group fan-out handler \u2014 dispatcht Group-Commands an die einzelnen\n * Mitglieder mit Capability-Match. Vorher in main.ts als 4 private\n * Methoden mit ~100 Zeilen.\n *\n * Die scene/music-Spezial-Pfade matchen den Group-Dropdown-Namen gegen\n * den Member-Dropdown-Namen \u2014 nicht 1:1-Indizes, weil die Member\n * verschiedene Scene-Listen haben k\u00F6nnen.\n */\nexport class GroupFanoutHandler {\n /**\n * @param host Adapter dependencies via Host-Interface\n */\n constructor(private readonly host: GroupFanoutHost) {}\n\n /**\n * Fan out a group command to all online member devices.\n * Basic controls (power/brightness/color) gehen direkt durch.\n * Scenes/music werden Name-basiert gemappt.\n *\n * @param group BaseGroup-Device\n * @param stateSuffix State-Suffix (z.B. \"control.power\" oder \"scenes.light_scene\")\n * @param value Command-Value\n */\n async fanOut(group: GoveeDevice, stateSuffix: string, value: ioBroker.StateValue): Promise<void> {\n if (!group.groupMembers) {\n return;\n }\n const devices = this.host.getDevices();\n const members = this.resolveMembers(group, devices).filter(d => d.state.online);\n if (members.length === 0) {\n this.host.log.debug(`Group \"${group.name}\": no reachable members for fan-out`);\n return;\n }\n const command = this.host.stateToCommand(stateSuffix);\n if (!command) {\n return;\n }\n // Dropdown-Reset \u2014 kein Command n\u00F6tig\n if ((command === \"lightScene\" || command === \"music\") && (value === \"0\" || value === 0)) {\n return;\n }\n for (const member of members) {\n try {\n if (command === \"lightScene\") {\n await this.fanOutScene(group, member, value);\n } else if (command === \"music\") {\n await this.fanOutMusic(group, member, stateSuffix, value);\n } else {\n await this.host.sendCommand(member, command, value);\n }\n } catch (err) {\n this.host.log.debug(`Group fan-out to ${member.name}: ${errMessage(err)}`);\n }\n }\n }\n\n /**\n * Resolve group member references to actual device objects.\n *\n * @param group BaseGroup device with groupMembers\n * @param devices Full device list to search\n */\n resolveMembers(group: GoveeDevice, devices: GoveeDevice[]): GoveeDevice[] {\n if (!group.groupMembers) {\n return [];\n }\n return group.groupMembers\n .map(m => devices.find(d => d.sku === m.sku && d.deviceId === m.deviceId))\n .filter((d): d is GoveeDevice => d !== undefined);\n }\n\n /**\n * Fan out a scene command: match group scene name \u2192 member scene index.\n *\n * @param group BaseGroup device\n * @param member Target member device\n * @param value Dropdown index value\n */\n private async fanOutScene(group: GoveeDevice, member: GoveeDevice, value: ioBroker.StateValue): Promise<void> {\n const groupPrefix = this.host.devicePrefix(group);\n const obj = await this.host.getObject(`${this.host.namespace}.${groupPrefix}.scenes.light_scene`);\n const groupStates = obj?.common?.states as Record<string, string> | undefined;\n const sceneName = groupStates?.[String(value)];\n if (!sceneName) {\n return;\n }\n const memberIdx = member.scenes.findIndex(s => s.name === sceneName);\n if (memberIdx >= 0) {\n await this.host.sendCommand(member, \"lightScene\", memberIdx + 1);\n }\n }\n\n /**\n * Fan out a music command: match group music name \u2192 member music index.\n *\n * @param group BaseGroup device\n * @param member Target member device\n * @param stateSuffix Music-state-suffix\n * @param value Command value\n */\n private async fanOutMusic(\n group: GoveeDevice,\n member: GoveeDevice,\n stateSuffix: string,\n value: ioBroker.StateValue,\n ): Promise<void> {\n // Sensitivity/auto_color werden direkt forwarded\n if (stateSuffix !== \"music.music_mode\") {\n await this.host.sendMusicCommand(member, this.host.devicePrefix(member), stateSuffix, value);\n return;\n }\n const groupPrefix = this.host.devicePrefix(group);\n const obj = await this.host.getObject(`${this.host.namespace}.${groupPrefix}.music.music_mode`);\n const groupStates = obj?.common?.states as Record<string, string> | undefined;\n const musicName = groupStates?.[String(value)];\n if (!musicName) {\n return;\n }\n const memberIdx = member.musicLibrary.findIndex(m => m.name === musicName);\n if (memberIdx >= 0) {\n await this.host.sendMusicCommand(member, this.host.devicePrefix(member), \"music.music_mode\", memberIdx + 1);\n }\n }\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAA6C;AA0CtC,MAAM,mBAAmB;AAAA;AAAA;AAAA;AAAA,EAI9B,YAA6B,MAAuB;AAAvB;AAAA,EAAwB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWrD,MAAM,OAAO,OAAoB,aAAqB,OAA2C;AAC/F,QAAI,CAAC,MAAM,cAAc;AACvB;AAAA,IACF;AACA,UAAM,UAAU,KAAK,KAAK,WAAW;AACrC,UAAM,UAAU,KAAK,eAAe,OAAO,OAAO,EAAE,OAAO,OAAK,EAAE,MAAM,MAAM;AAC9E,QAAI,QAAQ,WAAW,GAAG;AACxB,WAAK,KAAK,IAAI,MAAM,UAAU,MAAM,IAAI,qCAAqC;AAC7E;AAAA,IACF;AACA,UAAM,UAAU,KAAK,KAAK,eAAe,WAAW;AACpD,QAAI,CAAC,SAAS;AACZ;AAAA,IACF;AAEA,SAAK,YAAY,gBAAgB,YAAY,aAAa,UAAU,OAAO,UAAU,IAAI;AACvF;AAAA,IACF;AACA,eAAW,UAAU,SAAS;AAC5B,UAAI;AACF,YAAI,YAAY,cAAc;AAC5B,gBAAM,KAAK,YAAY,OAAO,QAAQ,KAAK;AAAA,QAC7C,WAAW,YAAY,SAAS;AAC9B,gBAAM,KAAK,YAAY,OAAO,QAAQ,aAAa,KAAK;AAAA,QAC1D,OAAO;AACL,gBAAM,KAAK,KAAK,YAAY,QAAQ,SAAS,KAAK;AAAA,QACpD;AAAA,MACF,SAAS,KAAK;AACZ,aAAK,KAAK,IAAI,MAAM,oBAAoB,OAAO,IAAI,SAAK,yBAAW,GAAG,CAAC,EAAE;AAAA,MAC3E;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,eAAe,OAAoB,SAAuC;AACxE,QAAI,CAAC,MAAM,cAAc;AACvB,aAAO,CAAC;AAAA,IACV;AACA,WAAO,MAAM,aACV,IAAI,OAAK,QAAQ,KAAK,OAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,aAAa,EAAE,QAAQ,CAAC,EACxE,OAAO,CAAC,MAAwB,MAAM,MAAS;AAAA,EACpD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAc,YAAY,OAAoB,QAAqB,OAA2C;AAhHhH;AAiHI,UAAM,cAAc,KAAK,KAAK,aAAa,KAAK;AAChD,UAAM,MAAM,MAAM,KAAK,KAAK,UAAU,GAAG,KAAK,KAAK,SAAS,IAAI,WAAW,qBAAqB;AAChG,UAAM,eAAc,gCAAK,WAAL,mBAAa;AACjC,UAAM,YAAY,2CAAc,OAAO,KAAK;AAC5C,QAAI,CAAC,WAAW;AACd;AAAA,IACF;AACA,UAAM,YAAY,OAAO,OAAO,UAAU,OAAK,EAAE,SAAS,SAAS;AACnE,QAAI,aAAa,GAAG;AAClB,YAAM,KAAK,KAAK,YAAY,QAAQ,cAAc,YAAY,CAAC;AAAA,IACjE;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAc,YACZ,OACA,QACA,aACA,OACe;AA3InB;AA6II,QAAI,gBAAgB,oBAAoB;AACtC,YAAM,KAAK,KAAK,iBAAiB,QAAQ,KAAK,KAAK,aAAa,MAAM,GAAG,aAAa,KAAK;AAC3F;AAAA,IACF;AACA,UAAM,cAAc,KAAK,KAAK,aAAa,KAAK;AAChD,UAAM,MAAM,MAAM,KAAK,KAAK,UAAU,GAAG,KAAK,KAAK,SAAS,IAAI,WAAW,mBAAmB;AAC9F,UAAM,eAAc,gCAAK,WAAL,mBAAa;AACjC,UAAM,YAAY,2CAAc,OAAO,KAAK;AAC5C,QAAI,CAAC,WAAW;AACd;AAAA,IACF;AACA,UAAM,YAAY,OAAO,aAAa,UAAU,OAAK,EAAE,SAAS,SAAS;AACzE,QAAI,aAAa,GAAG;AAClB,YAAM,KAAK,KAAK,iBAAiB,QAAQ,KAAK,KAAK,aAAa,MAAM,GAAG,oBAAoB,YAAY,CAAC;AAAA,IAC5G;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
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 message_router_exports = {};
|
|
20
|
+
__export(message_router_exports, {
|
|
21
|
+
MessageRouter: () => MessageRouter
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(message_router_exports);
|
|
24
|
+
var import_types = require("./types");
|
|
25
|
+
var import_timing_constants = require("./timing-constants");
|
|
26
|
+
class MessageRouter {
|
|
27
|
+
/**
|
|
28
|
+
* @param host Adapter dependencies via Host-Interface
|
|
29
|
+
*/
|
|
30
|
+
constructor(host) {
|
|
31
|
+
this.host = host;
|
|
32
|
+
}
|
|
33
|
+
/** Last time `requestCode` was triggered — guards against double-click email spam. */
|
|
34
|
+
lastVerificationRequestMs = 0;
|
|
35
|
+
/**
|
|
36
|
+
* Sync entry-point — registered as `this.on("message", ...)`. Wraps
|
|
37
|
+
* the async handler in a catch so unhandled rejections können den
|
|
38
|
+
* Adapter nicht crashen.
|
|
39
|
+
*
|
|
40
|
+
* @param obj Inkommende ioBroker-Message
|
|
41
|
+
*/
|
|
42
|
+
onMessage(obj) {
|
|
43
|
+
if (!(obj == null ? void 0 : obj.command)) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
this.handleMessage(obj).catch((e) => {
|
|
47
|
+
this.host.log.warn(`onMessage handler crashed for ${obj.command}: ${(0, import_types.errMessage)(e)}`);
|
|
48
|
+
this.host.sendResponse(obj, { error: e instanceof Error ? e.message : String(e) });
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Async handler — dispatcht zu den 3 Sub-Handlern.
|
|
53
|
+
*
|
|
54
|
+
* @param obj Inkommende ioBroker-Message
|
|
55
|
+
*/
|
|
56
|
+
async handleMessage(obj) {
|
|
57
|
+
var _a, _b, _c, _d, _e;
|
|
58
|
+
try {
|
|
59
|
+
if (obj.command === "getSegmentDevices") {
|
|
60
|
+
this.host.sendResponse(obj, this.host.getSegmentDeviceList());
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (obj.command === "segmentWizard") {
|
|
64
|
+
const payload = (_a = obj.message) != null ? _a : {};
|
|
65
|
+
const response = await this.host.runWizardStep((_b = payload.action) != null ? _b : "", (_c = payload.device) != null ? _c : "");
|
|
66
|
+
this.host.sendResponse(obj, response);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (obj.command === "mqttAuth") {
|
|
70
|
+
const payload = (_d = obj.message) != null ? _d : {};
|
|
71
|
+
const response = await this.runMqttAuthAction((_e = payload.action) != null ? _e : "");
|
|
72
|
+
this.host.sendResponse(obj, response);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
} catch (e) {
|
|
76
|
+
this.host.log.warn(`onMessage failed for ${obj.command}: ${(0, import_types.errMessage)(e)}`);
|
|
77
|
+
this.host.sendResponse(obj, { error: e instanceof Error ? e.message : String(e) });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Handle the `mqttAuth` onMessage commands.
|
|
82
|
+
*
|
|
83
|
+
* Two actions:
|
|
84
|
+
* - `test` — try a one-shot login mit der aktuellen Settings-Combo
|
|
85
|
+
* und liefere ein einzelnes user-readable Ergebnis.
|
|
86
|
+
* - `requestCode` — POST an /verification, Govee mailt fresh code.
|
|
87
|
+
* 30s in-memory throttle gegen double-click email-spam.
|
|
88
|
+
*
|
|
89
|
+
* @param action Action-Name aus dem jsonConfig sendTo-Button
|
|
90
|
+
*/
|
|
91
|
+
async runMqttAuthAction(action) {
|
|
92
|
+
var _a;
|
|
93
|
+
const config = this.host.getConfig();
|
|
94
|
+
if (!config.goveeEmail || !config.goveePassword) {
|
|
95
|
+
return { result: "Email + Passwort in den Adapter-Einstellungen n\xF6tig." };
|
|
96
|
+
}
|
|
97
|
+
if (action === "test") {
|
|
98
|
+
const probe = this.host.createMqttProbeClient();
|
|
99
|
+
probe.setVerificationCode((_a = config.mqttVerificationCode) != null ? _a : "");
|
|
100
|
+
try {
|
|
101
|
+
let connected = false;
|
|
102
|
+
await probe.connect(
|
|
103
|
+
() => {
|
|
104
|
+
},
|
|
105
|
+
(isConnected) => {
|
|
106
|
+
connected = isConnected;
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
probe.disconnect();
|
|
110
|
+
return {
|
|
111
|
+
result: connected ? "Login erfolgreich \u2014 Echtzeit-Status-Updates aktiv." : "Login angenommen, MQTT-Verbindung steht aber noch nicht \u2014 Adapter neu starten."
|
|
112
|
+
};
|
|
113
|
+
} catch (e) {
|
|
114
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
115
|
+
if (/Verification required/i.test(msg)) {
|
|
116
|
+
return {
|
|
117
|
+
result: "Govee verlangt 2-Faktor-Best\xE4tigung. Bitte 'Verifizierungs-Code anfordern' klicken, Code aus der E-Mail eintragen und Speichern."
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
if (/Verification code invalid/i.test(msg)) {
|
|
121
|
+
return { result: "2-Faktor-Code ung\xFCltig oder abgelaufen \u2014 bitte einen neuen Code anfordern." };
|
|
122
|
+
}
|
|
123
|
+
if (/email not registered/i.test(msg)) {
|
|
124
|
+
return { result: "Diese E-Mail ist bei Govee nicht registriert." };
|
|
125
|
+
}
|
|
126
|
+
if (/Login failed/i.test(msg)) {
|
|
127
|
+
return { result: "Passwort wurde von Govee abgelehnt." };
|
|
128
|
+
}
|
|
129
|
+
if (/Rate limited/i.test(msg)) {
|
|
130
|
+
return { result: "Govee meldet Rate-Limit \u2014 bitte sp\xE4ter erneut versuchen." };
|
|
131
|
+
}
|
|
132
|
+
if (/Account temporarily locked/i.test(msg)) {
|
|
133
|
+
return { result: "Govee-Account vor\xFCbergehend gesperrt \u2014 Govee Home App \xF6ffnen und Status pr\xFCfen." };
|
|
134
|
+
}
|
|
135
|
+
return { result: `Login fehlgeschlagen: ${msg}` };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (action === "requestCode") {
|
|
139
|
+
const now = Date.now();
|
|
140
|
+
if (now - this.lastVerificationRequestMs < import_timing_constants.VERIFICATION_REQUEST_THROTTLE_MS) {
|
|
141
|
+
const wait = Math.ceil((import_timing_constants.VERIFICATION_REQUEST_THROTTLE_MS - (now - this.lastVerificationRequestMs)) / 1e3);
|
|
142
|
+
return { result: `Bitte ${wait}s warten \u2014 gerade wurde schon ein Code angefordert.` };
|
|
143
|
+
}
|
|
144
|
+
this.lastVerificationRequestMs = now;
|
|
145
|
+
const probe = this.host.createMqttProbeClient();
|
|
146
|
+
try {
|
|
147
|
+
await probe.requestVerificationCode();
|
|
148
|
+
return { result: "Code wurde an deine Govee-E-Mail-Adresse gesendet (Spam-Ordner pr\xFCfen)." };
|
|
149
|
+
} catch (e) {
|
|
150
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
151
|
+
return { result: `Govee hat den Code-Versand abgelehnt: ${msg}` };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return { result: `Unbekannte Aktion '${action}'.` };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
158
|
+
0 && (module.exports = {
|
|
159
|
+
MessageRouter
|
|
160
|
+
});
|
|
161
|
+
//# sourceMappingURL=message-router.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/lib/message-router.ts"],
|
|
4
|
+
"sourcesContent": ["import { errMessage } from \"./types\";\nimport type { GoveeMqttClient } from \"./govee-mqtt-client\";\nimport { VERIFICATION_REQUEST_THROTTLE_MS } from \"./timing-constants\";\n\n/**\n * Host-Interface f\u00FCr MessageRouter.\n *\n * Pattern analog SnapshotHandler/GroupFanoutHandler. main.ts bleibt\n * schlank, der onMessage/sendTo-Pfad ist isoliert testbar.\n */\nexport interface MessageRouterHost {\n /** Adapter logger. */\n log: ioBroker.Logger;\n /** Liefert die Adapter-Konfiguration f\u00FCr den runMqttAuthAction-Pfad. */\n getConfig: () => { goveeEmail: string; goveePassword: string; mqttVerificationCode?: string };\n /** Sendet die JSON-Response zur\u00FCck an den Caller (sendMessageResponse-Pfad). */\n sendResponse: (obj: ioBroker.Message, data: unknown) => void;\n /** Factory f\u00FCr ein One-Shot-MqttClient (f\u00FCr Login-Test). */\n createMqttProbeClient: () => GoveeMqttClient;\n /** Liefert die Liste der Devices die Segmente haben (f\u00FCr getSegmentDevices). */\n getSegmentDeviceList: () => Array<{ value: string; label: string }>;\n /** Wizard-Step-Routing \u2014 main.ts beh\u00E4lt den Wizard-State. */\n runWizardStep: (action: string, deviceKey: string) => Promise<Record<string, unknown>>;\n}\n\n/**\n * Router f\u00FCr ioBroker.Message events (sendTo aus dem Admin-UI).\n *\n * Dispatcht 3 Commands:\n * - `getSegmentDevices` \u2014 selectSendTo-Datenquelle f\u00FCr den Wizard\n * - `segmentWizard` \u2014 Wizard-Step (start/yes/no/done/abort)\n * - `mqttAuth` \u2014 Login-Test + Verification-Code-Anforderung\n */\nexport class MessageRouter {\n /** Last time `requestCode` was triggered \u2014 guards against double-click email spam. */\n private lastVerificationRequestMs = 0;\n\n /**\n * @param host Adapter dependencies via Host-Interface\n */\n constructor(private readonly host: MessageRouterHost) {}\n\n /**\n * Sync entry-point \u2014 registered as `this.on(\"message\", ...)`. Wraps\n * the async handler in a catch so unhandled rejections k\u00F6nnen den\n * Adapter nicht crashen.\n *\n * @param obj Inkommende ioBroker-Message\n */\n onMessage(obj: ioBroker.Message): void {\n if (!obj?.command) {\n return;\n }\n this.handleMessage(obj).catch(e => {\n this.host.log.warn(`onMessage handler crashed for ${obj.command}: ${errMessage(e)}`);\n this.host.sendResponse(obj, { error: e instanceof Error ? e.message : String(e) });\n });\n }\n\n /**\n * Async handler \u2014 dispatcht zu den 3 Sub-Handlern.\n *\n * @param obj Inkommende ioBroker-Message\n */\n private async handleMessage(obj: ioBroker.Message): Promise<void> {\n try {\n if (obj.command === \"getSegmentDevices\") {\n this.host.sendResponse(obj, this.host.getSegmentDeviceList());\n return;\n }\n if (obj.command === \"segmentWizard\") {\n const payload = (obj.message ?? {}) as { action?: string; device?: string };\n const response = await this.host.runWizardStep(payload.action ?? \"\", payload.device ?? \"\");\n this.host.sendResponse(obj, response);\n return;\n }\n if (obj.command === \"mqttAuth\") {\n const payload = (obj.message ?? {}) as { action?: string };\n const response = await this.runMqttAuthAction(payload.action ?? \"\");\n this.host.sendResponse(obj, response);\n return;\n }\n } catch (e) {\n this.host.log.warn(`onMessage failed for ${obj.command}: ${errMessage(e)}`);\n this.host.sendResponse(obj, { error: e instanceof Error ? e.message : String(e) });\n }\n }\n\n /**\n * Handle the `mqttAuth` onMessage commands.\n *\n * Two actions:\n * - `test` \u2014 try a one-shot login mit der aktuellen Settings-Combo\n * und liefere ein einzelnes user-readable Ergebnis.\n * - `requestCode` \u2014 POST an /verification, Govee mailt fresh code.\n * 30s in-memory throttle gegen double-click email-spam.\n *\n * @param action Action-Name aus dem jsonConfig sendTo-Button\n */\n private async runMqttAuthAction(action: string): Promise<{ result: string }> {\n const config = this.host.getConfig();\n if (!config.goveeEmail || !config.goveePassword) {\n return { result: \"Email + Passwort in den Adapter-Einstellungen n\u00F6tig.\" };\n }\n if (action === \"test\") {\n const probe = this.host.createMqttProbeClient();\n probe.setVerificationCode(config.mqttVerificationCode ?? \"\");\n try {\n let connected = false;\n await probe.connect(\n () => {},\n isConnected => {\n connected = isConnected;\n },\n );\n probe.disconnect();\n return {\n result: connected\n ? \"Login erfolgreich \u2014 Echtzeit-Status-Updates aktiv.\"\n : \"Login angenommen, MQTT-Verbindung steht aber noch nicht \u2014 Adapter neu starten.\",\n };\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n if (/Verification required/i.test(msg)) {\n return {\n result:\n \"Govee verlangt 2-Faktor-Best\u00E4tigung. Bitte 'Verifizierungs-Code anfordern' klicken, Code aus der E-Mail eintragen und Speichern.\",\n };\n }\n if (/Verification code invalid/i.test(msg)) {\n return { result: \"2-Faktor-Code ung\u00FCltig oder abgelaufen \u2014 bitte einen neuen Code anfordern.\" };\n }\n if (/email not registered/i.test(msg)) {\n return { result: \"Diese E-Mail ist bei Govee nicht registriert.\" };\n }\n if (/Login failed/i.test(msg)) {\n return { result: \"Passwort wurde von Govee abgelehnt.\" };\n }\n if (/Rate limited/i.test(msg)) {\n return { result: \"Govee meldet Rate-Limit \u2014 bitte sp\u00E4ter erneut versuchen.\" };\n }\n if (/Account temporarily locked/i.test(msg)) {\n return { result: \"Govee-Account vor\u00FCbergehend gesperrt \u2014 Govee Home App \u00F6ffnen und Status pr\u00FCfen.\" };\n }\n return { result: `Login fehlgeschlagen: ${msg}` };\n }\n }\n if (action === \"requestCode\") {\n const now = Date.now();\n if (now - this.lastVerificationRequestMs < VERIFICATION_REQUEST_THROTTLE_MS) {\n const wait = Math.ceil((VERIFICATION_REQUEST_THROTTLE_MS - (now - this.lastVerificationRequestMs)) / 1000);\n return { result: `Bitte ${wait}s warten \u2014 gerade wurde schon ein Code angefordert.` };\n }\n this.lastVerificationRequestMs = now;\n const probe = this.host.createMqttProbeClient();\n try {\n await probe.requestVerificationCode();\n return { result: \"Code wurde an deine Govee-E-Mail-Adresse gesendet (Spam-Ordner pr\u00FCfen).\" };\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n return { result: `Govee hat den Code-Versand abgelehnt: ${msg}` };\n }\n }\n return { result: `Unbekannte Aktion '${action}'.` };\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|