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 +8 -12
- package/build/lib/group-fanout.js +136 -0
- package/build/lib/group-fanout.js.map +7 -0
- package/build/lib/snapshot-handler.js +132 -0
- package/build/lib/snapshot-handler.js.map +7 -0
- package/build/main.js +54 -190
- 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.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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
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
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
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 = [
|