iobroker.govee-smart 2.3.1 → 2.4.1

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 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.4.1 (2026-05-04)
128
+
129
+ - Group-Fan-Out-Pfad (Mitglieder-Steuerung beim Schalten der Gruppe) ist jetzt eine eigene Klasse mit Host-Interface — `main.ts` nochmal kleiner. Verhalten identisch.
130
+
131
+ ### 2.4.0 (2026-05-04)
132
+
133
+ - 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.
134
+
127
135
  ### 2.3.1 (2026-05-04)
128
136
 
129
137
  - Smoke-Tests für GoveeCloudClient + GoveeMqttClient — Initial-State-Checks für getFailureReason, token, connected, plus Setter-Smoke-Tests (637 → 637+9 Tests). Volle Pfade über https/mqtt-Mocks kommen separat.
@@ -141,18 +149,6 @@ This adapter's MQTT authentication and BLE-over-LAN (ptReal) protocol implementa
141
149
  - Memory-Leaks beim Device-Remove geschlossen — Diagnostics-Buffer und State-Tree werden gemeinsam aufgeräumt wenn ein Gerät aus dem Govee-Account verschwindet.
142
150
  - 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`.
143
151
 
144
- ### 2.1.4 (2026-05-03)
145
-
146
- - 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).
147
-
148
- ### 2.1.3 (2026-05-03)
149
-
150
- - Critical fix: no more restart-loop after entering the verification code. The cached login is now stored in a state, not in the adapter config — saving the config doesn't trigger a restart anymore.
151
- - Saving email + password in the adapter config works again. The previous loop made it look like only the "Test login" button worked.
152
- - Honest startup messages: when MQTT really doesn't connect within the first minute, the log says why ("login rejected", "verification needed", etc.) instead of "still pending".
153
- - Verification warning shortened. The full step-by-step instructions live in the Wiki, the log only states the action.
154
- - "MQTT connected to AWS IoT" → "MQTT connected". "OpenAPI MQTT" → "Cloud-events" in user-facing logs.
155
-
156
152
  Older entries are in [CHANGELOG_OLD.md](CHANGELOG_OLD.md).
157
153
 
158
154
  ## 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": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,132 @@
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 snapshot_handler_exports = {};
20
+ __export(snapshot_handler_exports, {
21
+ SnapshotHandler: () => SnapshotHandler
22
+ });
23
+ module.exports = __toCommonJS(snapshot_handler_exports);
24
+ class SnapshotHandler {
25
+ /**
26
+ * @param host Adapter dependencies via Host-Interface (testbar via Mocks)
27
+ */
28
+ constructor(host) {
29
+ this.host = host;
30
+ }
31
+ /**
32
+ * Save current device state as a local snapshot.
33
+ *
34
+ * @param device Target device
35
+ * @param name Snapshot name
36
+ */
37
+ async save(device, name) {
38
+ var _a;
39
+ const prefix = this.host.devicePrefix(device);
40
+ const ns = this.host.namespace;
41
+ const [powerState, brightState, colorState, ctState] = await Promise.all([
42
+ this.host.getState(`${ns}.${prefix}.control.power`),
43
+ this.host.getState(`${ns}.${prefix}.control.brightness`),
44
+ this.host.getState(`${ns}.${prefix}.control.colorRgb`),
45
+ this.host.getState(`${ns}.${prefix}.control.colorTemperature`)
46
+ ]);
47
+ let segments;
48
+ const segCount = (_a = device.segmentCount) != null ? _a : 0;
49
+ if (segCount > 0) {
50
+ const segReads = [];
51
+ for (let i = 0; i < segCount; i++) {
52
+ segReads.push(
53
+ Promise.all([
54
+ this.host.getState(`${ns}.${prefix}.segments.${i}.color`),
55
+ this.host.getState(`${ns}.${prefix}.segments.${i}.brightness`)
56
+ ])
57
+ );
58
+ }
59
+ const segResults = await Promise.all(segReads);
60
+ segments = segResults.map(([segColor, segBright]) => ({
61
+ color: typeof (segColor == null ? void 0 : segColor.val) === "string" ? segColor.val : "#000000",
62
+ brightness: typeof (segBright == null ? void 0 : segBright.val) === "number" ? segBright.val : 100
63
+ }));
64
+ }
65
+ const snapshot = {
66
+ name,
67
+ power: (powerState == null ? void 0 : powerState.val) === true,
68
+ brightness: typeof (brightState == null ? void 0 : brightState.val) === "number" ? brightState.val : 0,
69
+ colorRgb: typeof (colorState == null ? void 0 : colorState.val) === "string" ? colorState.val : "#000000",
70
+ colorTemperature: typeof (ctState == null ? void 0 : ctState.val) === "number" ? ctState.val : 0,
71
+ segments,
72
+ savedAt: Date.now()
73
+ };
74
+ this.host.store.saveSnapshot(device.sku, device.deviceId, snapshot);
75
+ this.host.log.info(`Local snapshot saved: "${name}" for ${device.name}`);
76
+ this.host.refreshDeviceStates(device);
77
+ }
78
+ /**
79
+ * Restore a local snapshot by index.
80
+ *
81
+ * @param device Target device
82
+ * @param val Dropdown index value
83
+ */
84
+ async restore(device, val) {
85
+ const idx = parseInt(String(val), 10);
86
+ if (idx < 1) {
87
+ return;
88
+ }
89
+ const snaps = this.host.store.getSnapshots(device.sku, device.deviceId);
90
+ const snap = snaps[idx - 1];
91
+ if (!snap) {
92
+ this.host.log.warn(`Local snapshot index ${idx} not found for ${device.name}`);
93
+ return;
94
+ }
95
+ this.host.log.info(`Restoring local snapshot "${snap.name}" for ${device.name}`);
96
+ await this.host.sendCommand(device, "power", snap.power);
97
+ if (snap.power) {
98
+ await this.host.sendCommand(device, "brightness", snap.brightness);
99
+ if (snap.colorTemperature > 0) {
100
+ await this.host.sendCommand(device, "colorTemperature", snap.colorTemperature);
101
+ } else {
102
+ await this.host.sendCommand(device, "colorRgb", snap.colorRgb);
103
+ }
104
+ if (snap.segments && snap.segments.length > 0) {
105
+ for (let i = 0; i < snap.segments.length; i++) {
106
+ const seg = snap.segments[i];
107
+ await this.host.sendCommand(device, `segmentColor:${i}`, seg.color);
108
+ await this.host.sendCommand(device, `segmentBrightness:${i}`, seg.brightness);
109
+ }
110
+ }
111
+ }
112
+ }
113
+ /**
114
+ * Delete a local snapshot by name.
115
+ *
116
+ * @param device Target device
117
+ * @param name Snapshot name to delete
118
+ */
119
+ delete(device, name) {
120
+ if (this.host.store.deleteSnapshot(device.sku, device.deviceId, name)) {
121
+ this.host.log.info(`Local snapshot deleted: "${name}" for ${device.name}`);
122
+ this.host.refreshDeviceStates(device);
123
+ } else {
124
+ this.host.log.warn(`Local snapshot "${name}" not found for ${device.name}`);
125
+ }
126
+ }
127
+ }
128
+ // Annotate the CommonJS export names for ESM import in node:
129
+ 0 && (module.exports = {
130
+ SnapshotHandler
131
+ });
132
+ //# sourceMappingURL=snapshot-handler.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/lib/snapshot-handler.ts"],
4
+ "sourcesContent": ["import type { LocalSnapshot, LocalSnapshotStore, SnapshotSegment } from \"./local-snapshots\";\nimport type { GoveeDevice } from \"./types\";\n\n/**\n * Host-Interface \u2014 die Adapter-Funktionen die der SnapshotHandler braucht\n * ohne von der Adapter-Klasse direkt zu h\u00E4ngen.\n *\n * Pattern analog `WizardHost` in segment-wizard.ts. Vorteil: testbar mit\n * Mocks, dependencies-flow ist explizit.\n */\nexport interface SnapshotHandlerHost {\n /** Adapter logger. */\n log: ioBroker.Logger;\n /** Local snapshot persistence (file-based JSON store). */\n store: LocalSnapshotStore;\n /** Adapter-namespace prefix (z.B. \"govee-smart.0\"). */\n namespace: string;\n /** Resolved object-prefix f\u00FCr ein Ger\u00E4t (z.B. \"devices.h61be_525f\"). */\n devicePrefix: (device: GoveeDevice) => string;\n /** State-read (volles ID `<namespace>.<prefix>.<channel>.<state>`). */\n getState: (id: string) => Promise<ioBroker.State | null | undefined>;\n /** Send-command via LAN\u2192Cloud-Routing (DeviceManager.sendCommand). */\n sendCommand: (device: GoveeDevice, command: string, value: unknown) => Promise<void>;\n /** Targeted state-tree refresh nach save/delete (snapshot_local Dropdown). */\n refreshDeviceStates: (device: GoveeDevice) => void;\n}\n\n/**\n * Lokaler Snapshot-Manager \u2014 kapselt save/restore/delete f\u00FCr die\n * snapshot_save / snapshot_local / snapshot_delete Dropdown-States.\n *\n * Vorher in `main.ts` als 3 private Methoden mit ~105 Zeilen. Hier in\n * eigenen Klasse mit Host-Interface \u2014 testbar isoliert, main.ts wird\n * kleiner, Maintainability gesteigert.\n */\nexport class SnapshotHandler {\n /**\n * @param host Adapter dependencies via Host-Interface (testbar via Mocks)\n */\n constructor(private readonly host: SnapshotHandlerHost) {}\n\n /**\n * Save current device state as a local snapshot.\n *\n * @param device Target device\n * @param name Snapshot name\n */\n async save(device: GoveeDevice, name: string): Promise<void> {\n const prefix = this.host.devicePrefix(device);\n const ns = this.host.namespace;\n\n // Read device-level state in parallel\n const [powerState, brightState, colorState, ctState] = await Promise.all([\n this.host.getState(`${ns}.${prefix}.control.power`),\n this.host.getState(`${ns}.${prefix}.control.brightness`),\n this.host.getState(`${ns}.${prefix}.control.colorRgb`),\n this.host.getState(`${ns}.${prefix}.control.colorTemperature`),\n ]);\n\n // Read per-segment states in parallel \u2014 sequenziell w\u00E4ren 20\u00D72 reads\n // ~80 ms; parallel = single round-trip.\n let segments: SnapshotSegment[] | undefined;\n const segCount = device.segmentCount ?? 0;\n if (segCount > 0) {\n const segReads: Promise<[ioBroker.State | null | undefined, ioBroker.State | null | undefined]>[] = [];\n for (let i = 0; i < segCount; i++) {\n segReads.push(\n Promise.all([\n this.host.getState(`${ns}.${prefix}.segments.${i}.color`),\n this.host.getState(`${ns}.${prefix}.segments.${i}.brightness`),\n ]),\n );\n }\n const segResults = await Promise.all(segReads);\n segments = segResults.map(([segColor, segBright]) => ({\n color: typeof segColor?.val === \"string\" ? segColor.val : \"#000000\",\n brightness: typeof segBright?.val === \"number\" ? segBright.val : 100,\n }));\n }\n\n const snapshot: LocalSnapshot = {\n name,\n power: powerState?.val === true,\n brightness: typeof brightState?.val === \"number\" ? brightState.val : 0,\n colorRgb: typeof colorState?.val === \"string\" ? colorState.val : \"#000000\",\n colorTemperature: typeof ctState?.val === \"number\" ? ctState.val : 0,\n segments,\n savedAt: Date.now(),\n };\n\n this.host.store.saveSnapshot(device.sku, device.deviceId, snapshot);\n this.host.log.info(`Local snapshot saved: \"${name}\" for ${device.name}`);\n // Targeted refresh \u2014 only this device's snapshot_local dropdown changed.\n this.host.refreshDeviceStates(device);\n }\n\n /**\n * Restore a local snapshot by index.\n *\n * @param device Target device\n * @param val Dropdown index value\n */\n async restore(device: GoveeDevice, val: ioBroker.StateValue): Promise<void> {\n const idx = parseInt(String(val), 10);\n if (idx < 1) {\n return;\n }\n const snaps = this.host.store.getSnapshots(device.sku, device.deviceId);\n const snap = snaps[idx - 1];\n if (!snap) {\n this.host.log.warn(`Local snapshot index ${idx} not found for ${device.name}`);\n return;\n }\n this.host.log.info(`Restoring local snapshot \"${snap.name}\" for ${device.name}`);\n\n // Send each state via LAN \u2192 Cloud routing\n await this.host.sendCommand(device, \"power\", snap.power);\n if (snap.power) {\n await this.host.sendCommand(device, \"brightness\", snap.brightness);\n if (snap.colorTemperature > 0) {\n await this.host.sendCommand(device, \"colorTemperature\", snap.colorTemperature);\n } else {\n await this.host.sendCommand(device, \"colorRgb\", snap.colorRgb);\n }\n // Restore per-segment states via ptReal\n if (snap.segments && snap.segments.length > 0) {\n for (let i = 0; i < snap.segments.length; i++) {\n const seg = snap.segments[i];\n await this.host.sendCommand(device, `segmentColor:${i}`, seg.color);\n await this.host.sendCommand(device, `segmentBrightness:${i}`, seg.brightness);\n }\n }\n }\n }\n\n /**\n * Delete a local snapshot by name.\n *\n * @param device Target device\n * @param name Snapshot name to delete\n */\n delete(device: GoveeDevice, name: string): void {\n if (this.host.store.deleteSnapshot(device.sku, device.deviceId, name)) {\n this.host.log.info(`Local snapshot deleted: \"${name}\" for ${device.name}`);\n // Targeted refresh \u2014 only this device's snapshot_local dropdown changed.\n this.host.refreshDeviceStates(device);\n } else {\n this.host.log.warn(`Local snapshot \"${name}\" not found for ${device.name}`);\n }\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
package/build/main.js CHANGED
@@ -31,6 +31,8 @@ var import_govee_lan_client = require("./lib/govee-lan-client");
31
31
  var import_govee_mqtt_client = require("./lib/govee-mqtt-client");
32
32
  var import_govee_openapi_mqtt_client = require("./lib/govee-openapi-mqtt-client");
33
33
  var import_local_snapshots = require("./lib/local-snapshots");
34
+ var import_snapshot_handler = require("./lib/snapshot-handler");
35
+ var import_group_fanout = require("./lib/group-fanout");
34
36
  var import_cloud_retry = require("./lib/cloud-retry");
35
37
  var import_rate_limiter = require("./lib/rate-limiter");
36
38
  var import_segment_wizard = require("./lib/segment-wizard");
@@ -101,6 +103,8 @@ class GoveeAdapter extends utils.Adapter {
101
103
  // === Sub-Komponenten ===
102
104
  skuCache = null;
103
105
  localSnapshots = null;
106
+ snapshotHandler = null;
107
+ groupFanout = null;
104
108
  stateCreationQueue = [];
105
109
  lanScanTimer;
106
110
  cleanupTimer;
@@ -183,6 +187,8 @@ class GoveeAdapter extends utils.Adapter {
183
187
  });
184
188
  this.skuCache = new import_sku_cache.SkuCache(dataDir, this.log);
185
189
  this.localSnapshots = new import_local_snapshots.LocalSnapshotStore(dataDir, this.log);
190
+ this.snapshotHandler = new import_snapshot_handler.SnapshotHandler(this.buildSnapshotHost());
191
+ this.groupFanout = new import_group_fanout.GroupFanoutHandler(this.buildGroupFanoutHost());
186
192
  this.deviceManager.setSkuCache(this.skuCache);
187
193
  const apiClient = new import_govee_api_client.GoveeApiClient();
188
194
  apiClient.setEmail(config.goveeEmail);
@@ -589,7 +595,7 @@ class GoveeAdapter extends utils.Adapter {
589
595
  }
590
596
  const val = resolved.val;
591
597
  if (device.sku === "BaseGroup" && device.groupMembers) {
592
- await this.handleGroupFanOut(device, stateSuffix, val);
598
+ await this.groupFanout.fanOut(device, stateSuffix, val);
593
599
  await this.setStateAsync(id, { val, ack: true });
594
600
  if (stateSuffix === "scenes.light_scene" || stateSuffix === "music.music_mode") {
595
601
  await this.resetRelatedDropdowns(prefix, stateSuffix === "scenes.light_scene" ? "lightScene" : "music");
@@ -597,20 +603,20 @@ class GoveeAdapter extends utils.Adapter {
597
603
  return;
598
604
  }
599
605
  if (stateSuffix === "snapshots.snapshot_save" && typeof val === "string" && val.trim()) {
600
- await this.handleSnapshotSave(device, val.trim());
606
+ await this.snapshotHandler.save(device, val.trim());
601
607
  await this.setStateAsync(id, { val: "", ack: true });
602
608
  return;
603
609
  }
604
610
  if (stateSuffix === "snapshots.snapshot_local") {
605
611
  if (val !== "0" && val !== 0) {
606
- await this.handleSnapshotRestore(device, val);
612
+ await this.snapshotHandler.restore(device, val);
607
613
  await this.resetRelatedDropdowns(prefix, "snapshotLocal");
608
614
  }
609
615
  await this.setStateAsync(id, { val, ack: true });
610
616
  return;
611
617
  }
612
618
  if (stateSuffix === "snapshots.snapshot_delete" && typeof val === "string" && val.trim()) {
613
- this.handleSnapshotDelete(device, val.trim());
619
+ this.snapshotHandler.delete(device, val.trim());
614
620
  await this.setStateAsync(id, { val: "", ack: true });
615
621
  return;
616
622
  }
@@ -776,93 +782,35 @@ class GoveeAdapter extends utils.Adapter {
776
782
  * @param stateSuffix State path suffix (e.g. "control.power")
777
783
  * @param value Command value
778
784
  */
779
- async handleGroupFanOut(group, stateSuffix, value) {
780
- if (!this.deviceManager || !group.groupMembers) {
781
- return;
782
- }
783
- const devices = this.deviceManager.getDevices();
784
- const members = this.resolveGroupMembers(group, devices).filter((d) => d.state.online);
785
- if (members.length === 0) {
786
- this.log.debug(`Group "${group.name}": no reachable members for fan-out`);
787
- return;
788
- }
789
- const command = this.stateToCommand(stateSuffix);
790
- if (!command) {
791
- return;
792
- }
793
- if ((command === "lightScene" || command === "music") && (value === "0" || value === 0)) {
794
- return;
795
- }
796
- for (const member of members) {
797
- try {
798
- if (command === "lightScene") {
799
- await this.fanOutScene(group, member, value);
800
- } else if (command === "music") {
801
- await this.fanOutMusic(group, member, stateSuffix, value);
802
- } else {
803
- await this.deviceManager.sendCommand(member, command, value);
804
- }
805
- } catch (err) {
806
- this.log.debug(`Group fan-out to ${member.name}: ${(0, import_types.errMessage)(err)}`);
807
- }
808
- }
809
- }
810
- /**
811
- * Fan out a scene command: match group scene name to member scene index.
812
- *
813
- * @param group BaseGroup device
814
- * @param member Target member device
815
- * @param value Dropdown index value
816
- */
817
- async fanOutScene(group, member, value) {
818
- var _a;
819
- if (!this.deviceManager || !this.stateManager) {
820
- return;
821
- }
822
- const groupPrefix = this.stateManager.devicePrefix(group);
823
- const obj = await this.getObjectAsync(`${this.namespace}.${groupPrefix}.scenes.light_scene`);
824
- const groupStates = (_a = obj == null ? void 0 : obj.common) == null ? void 0 : _a.states;
825
- const sceneName = groupStates == null ? void 0 : groupStates[String(value)];
826
- if (!sceneName) {
827
- return;
828
- }
829
- const memberIdx = member.scenes.findIndex((s) => s.name === sceneName);
830
- if (memberIdx >= 0) {
831
- await this.deviceManager.sendCommand(member, "lightScene", memberIdx + 1);
832
- }
833
- }
834
- /**
835
- * Fan out a music command: match group music name to member music index.
836
- *
837
- * @param group BaseGroup device
838
- * @param member Target member device
839
- * @param stateSuffix Music state path suffix
840
- * @param value Command value
841
- */
842
- async fanOutMusic(group, member, stateSuffix, value) {
843
- var _a;
844
- if (!this.deviceManager || !this.stateManager) {
845
- return;
846
- }
847
- if (stateSuffix !== "music.music_mode") {
848
- await this.sendMusicCommand(member, this.stateManager.devicePrefix(member), stateSuffix, value);
849
- return;
850
- }
851
- const groupPrefix = this.stateManager.devicePrefix(group);
852
- const obj = await this.getObjectAsync(`${this.namespace}.${groupPrefix}.music.music_mode`);
853
- const groupStates = (_a = obj == null ? void 0 : obj.common) == null ? void 0 : _a.states;
854
- const musicName = groupStates == null ? void 0 : groupStates[String(value)];
855
- if (!musicName) {
856
- return;
857
- }
858
- const memberIdx = member.musicLibrary.findIndex((m) => m.name === musicName);
859
- if (memberIdx >= 0) {
860
- const memberPrefix = this.stateManager.devicePrefix(member);
861
- await this.sendMusicCommand(member, memberPrefix, "music.music_mode", memberIdx + 1);
862
- }
785
+ /** Construct host object for GroupFanoutHandler. */
786
+ buildGroupFanoutHost() {
787
+ return {
788
+ log: this.log,
789
+ namespace: this.namespace,
790
+ getDevices: () => {
791
+ var _a, _b;
792
+ return (_b = (_a = this.deviceManager) == null ? void 0 : _a.getDevices()) != null ? _b : [];
793
+ },
794
+ sendCommand: async (device, command, value) => {
795
+ var _a;
796
+ await ((_a = this.deviceManager) == null ? void 0 : _a.sendCommand(device, command, value));
797
+ },
798
+ devicePrefix: (device) => {
799
+ var _a, _b;
800
+ return (_b = (_a = this.stateManager) == null ? void 0 : _a.devicePrefix(device)) != null ? _b : "";
801
+ },
802
+ stateToCommand: (suffix) => {
803
+ var _a;
804
+ return (_a = this.stateToCommand(suffix)) != null ? _a : void 0;
805
+ },
806
+ getObject: (id) => this.getObjectAsync(id),
807
+ sendMusicCommand: (device, devicePrefix, stateSuffix, value) => this.sendMusicCommand(device, devicePrefix, stateSuffix, value)
808
+ };
863
809
  }
864
810
  /**
865
811
  * Resolve group member references to actual device objects.
812
+ * Bleibt in main.ts weil `updateGroupReachability` (auch in main.ts) das
813
+ * direkt nutzt — Helper-Pfad ist von der Fan-Out-Klasse unabhängig.
866
814
  *
867
815
  * @param group BaseGroup device with groupMembers
868
816
  * @param devices Full device list to search
@@ -1649,110 +1597,26 @@ class GoveeAdapter extends utils.Adapter {
1649
1597
  });
1650
1598
  return response;
1651
1599
  }
1652
- /**
1653
- * Save current device state as a local snapshot.
1654
- *
1655
- * @param device Target device
1656
- * @param name Snapshot name
1657
- */
1658
- async handleSnapshotSave(device, name) {
1659
- var _a;
1660
- if (!this.localSnapshots || !this.stateManager) {
1661
- return;
1662
- }
1663
- const prefix = this.stateManager.devicePrefix(device);
1664
- const ns = this.namespace;
1665
- const [powerState, brightState, colorState, ctState] = await Promise.all([
1666
- this.getStateAsync(`${ns}.${prefix}.control.power`),
1667
- this.getStateAsync(`${ns}.${prefix}.control.brightness`),
1668
- this.getStateAsync(`${ns}.${prefix}.control.colorRgb`),
1669
- this.getStateAsync(`${ns}.${prefix}.control.colorTemperature`)
1670
- ]);
1671
- let segments;
1672
- const segCount = (_a = device.segmentCount) != null ? _a : 0;
1673
- if (segCount > 0) {
1674
- const segReads = [];
1675
- for (let i = 0; i < segCount; i++) {
1676
- segReads.push(
1677
- Promise.all([
1678
- this.getStateAsync(`${ns}.${prefix}.segments.${i}.color`),
1679
- this.getStateAsync(`${ns}.${prefix}.segments.${i}.brightness`)
1680
- ])
1681
- );
1600
+ /** Construct host object for SnapshotHandler — adapter dependencies injected. */
1601
+ buildSnapshotHost() {
1602
+ return {
1603
+ log: this.log,
1604
+ store: this.localSnapshots,
1605
+ namespace: this.namespace,
1606
+ devicePrefix: (device) => {
1607
+ var _a, _b;
1608
+ return (_b = (_a = this.stateManager) == null ? void 0 : _a.devicePrefix(device)) != null ? _b : "";
1609
+ },
1610
+ getState: (id) => this.getStateAsync(id),
1611
+ sendCommand: async (device, command, value) => {
1612
+ var _a;
1613
+ await ((_a = this.deviceManager) == null ? void 0 : _a.sendCommand(device, command, value));
1614
+ },
1615
+ refreshDeviceStates: (device) => {
1616
+ var _a, _b;
1617
+ this.refreshDeviceStates(device, (_b = (_a = this.deviceManager) == null ? void 0 : _a.getDevices()) != null ? _b : []);
1682
1618
  }
1683
- const segResults = await Promise.all(segReads);
1684
- segments = segResults.map(([segColor, segBright]) => ({
1685
- color: typeof (segColor == null ? void 0 : segColor.val) === "string" ? segColor.val : "#000000",
1686
- brightness: typeof (segBright == null ? void 0 : segBright.val) === "number" ? segBright.val : 100
1687
- }));
1688
- }
1689
- const snapshot = {
1690
- name,
1691
- power: (powerState == null ? void 0 : powerState.val) === true,
1692
- brightness: typeof (brightState == null ? void 0 : brightState.val) === "number" ? brightState.val : 0,
1693
- colorRgb: typeof (colorState == null ? void 0 : colorState.val) === "string" ? colorState.val : "#000000",
1694
- colorTemperature: typeof (ctState == null ? void 0 : ctState.val) === "number" ? ctState.val : 0,
1695
- segments,
1696
- savedAt: Date.now()
1697
1619
  };
1698
- this.localSnapshots.saveSnapshot(device.sku, device.deviceId, snapshot);
1699
- this.log.info(`Local snapshot saved: "${name}" for ${device.name}`);
1700
- this.refreshDeviceStates(device, this.deviceManager.getDevices());
1701
- }
1702
- /**
1703
- * Restore a local snapshot by index.
1704
- *
1705
- * @param device Target device
1706
- * @param val Dropdown index value
1707
- */
1708
- async handleSnapshotRestore(device, val) {
1709
- if (!this.localSnapshots || !this.deviceManager) {
1710
- return;
1711
- }
1712
- const idx = parseInt(String(val), 10);
1713
- if (idx < 1) {
1714
- return;
1715
- }
1716
- const snaps = this.localSnapshots.getSnapshots(device.sku, device.deviceId);
1717
- const snap = snaps[idx - 1];
1718
- if (!snap) {
1719
- this.log.warn(`Local snapshot index ${idx} not found for ${device.name}`);
1720
- return;
1721
- }
1722
- this.log.info(`Restoring local snapshot "${snap.name}" for ${device.name}`);
1723
- await this.deviceManager.sendCommand(device, "power", snap.power);
1724
- if (snap.power) {
1725
- await this.deviceManager.sendCommand(device, "brightness", snap.brightness);
1726
- if (snap.colorTemperature > 0) {
1727
- await this.deviceManager.sendCommand(device, "colorTemperature", snap.colorTemperature);
1728
- } else {
1729
- await this.deviceManager.sendCommand(device, "colorRgb", snap.colorRgb);
1730
- }
1731
- if (snap.segments && snap.segments.length > 0) {
1732
- for (let i = 0; i < snap.segments.length; i++) {
1733
- const seg = snap.segments[i];
1734
- await this.deviceManager.sendCommand(device, `segmentColor:${i}`, seg.color);
1735
- await this.deviceManager.sendCommand(device, `segmentBrightness:${i}`, seg.brightness);
1736
- }
1737
- }
1738
- }
1739
- }
1740
- /**
1741
- * Delete a local snapshot by name.
1742
- *
1743
- * @param device Target device
1744
- * @param name Snapshot name to delete
1745
- */
1746
- handleSnapshotDelete(device, name) {
1747
- if (!this.localSnapshots) {
1748
- return;
1749
- }
1750
- if (this.localSnapshots.deleteSnapshot(device.sku, device.deviceId, name)) {
1751
- this.log.info(`Local snapshot deleted: "${name}" for ${device.name}`);
1752
- this.refreshDeviceStates(device, this.deviceManager.getDevices());
1753
- } else {
1754
- this.log.warn(`Local snapshot "${name}" not found for ${device.name}`);
1755
- }
1756
1620
  }
1757
1621
  /** Dropdowns whose value is a mode-selection — reset to "---" (0) when the mode stops. */
1758
1622
  static MODE_DROPDOWNS = [