iobroker.govee-smart 2.6.4 → 2.6.6
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 +16 -16
- package/build/lib/device-manager/cache.js +124 -0
- package/build/lib/device-manager/cache.js.map +7 -0
- package/build/lib/device-manager/cloud-merge.js +87 -0
- package/build/lib/device-manager/cloud-merge.js.map +7 -0
- package/build/lib/device-manager/lookups.js +147 -0
- package/build/lib/device-manager/lookups.js.map +7 -0
- package/build/lib/device-manager/mapping.js +90 -0
- package/build/lib/device-manager/mapping.js.map +7 -0
- package/build/lib/device-manager.js +50 -335
- package/build/lib/device-manager.js.map +3 -3
- package/build/lib/handlers/cloud-creds-handler.js +120 -0
- package/build/lib/handlers/cloud-creds-handler.js.map +7 -0
- package/build/lib/handlers/cloud-retry-handler.js +104 -0
- package/build/lib/handlers/cloud-retry-handler.js.map +7 -0
- package/build/lib/handlers/cloud-state-loader.js +83 -0
- package/build/lib/handlers/cloud-state-loader.js.map +7 -0
- package/build/lib/handlers/connection-state.js +178 -0
- package/build/lib/handlers/connection-state.js.map +7 -0
- package/build/lib/handlers/device-events.js +100 -0
- package/build/lib/handlers/device-events.js.map +7 -0
- package/build/lib/handlers/diagnostics-handler.js +48 -0
- package/build/lib/handlers/diagnostics-handler.js.map +7 -0
- package/build/lib/handlers/group-fanout-handler.js +87 -0
- package/build/lib/handlers/group-fanout-handler.js.map +7 -0
- package/build/lib/handlers/group-state-helpers.js +103 -0
- package/build/lib/handlers/group-state-helpers.js.map +7 -0
- package/build/lib/handlers/snapshot-handler-glue.js +48 -0
- package/build/lib/handlers/snapshot-handler-glue.js.map +7 -0
- package/build/lib/handlers/state-change-router.js +275 -0
- package/build/lib/handlers/state-change-router.js.map +7 -0
- package/build/lib/handlers/wizard-handler.js +107 -0
- package/build/lib/handlers/wizard-handler.js.map +7 -0
- package/build/main.js +104 -1104
- package/build/main.js.map +2 -2
- package/io-package.json +49 -49
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -128,31 +128,31 @@ This adapter's MQTT authentication and BLE-over-LAN (ptReal) protocol implementa
|
|
|
128
128
|
Placeholder for the next version (at the beginning of the line):
|
|
129
129
|
### **WORK IN PROGRESS**
|
|
130
130
|
-->
|
|
131
|
-
### 2.6.
|
|
131
|
+
### 2.6.6 (2026-05-10)
|
|
132
132
|
|
|
133
|
-
- Internal
|
|
133
|
+
- Internal refactoring. No changes for users.
|
|
134
134
|
|
|
135
|
-
### 2.6.
|
|
135
|
+
### 2.6.5 (2026-05-10)
|
|
136
136
|
|
|
137
|
-
-
|
|
138
|
-
- Restoring a local snapshot on a multi-segment LED strip is fast again — a 30-segment snapshot replays in well under a second instead of about nine.
|
|
139
|
-
- A wrong manual pairing IP, a network blip mid-request, or a Govee Cloud command that the cloud rejects with a payload-level error are now reported instead of failing silently.
|
|
140
|
-
- LED segment number 0 displaying blue when a colour was set with a short hex value (e.g. `#FF`) is fixed — invalid colour input falls back to black instead of an unintended colour.
|
|
141
|
-
- The segment-detection wizard now restores the strip's previous look on adapter-stop, so the test pattern doesn't stay on the wall when you click "Save adapter settings".
|
|
142
|
-
- A configured but rejected Govee API key now surfaces as `Cloud: device list returned empty — check the API key matches the account that owns the devices` so you have a starting point.
|
|
143
|
-
- Various behind-the-scenes hardening of all four communication channels (LAN, real-time MQTT, Cloud-events, Cloud REST) — invisible if everything was already running fine, robustness if something is unstable.
|
|
137
|
+
- Internal refactoring. No changes for users.
|
|
144
138
|
|
|
145
|
-
### 2.6.
|
|
139
|
+
### 2.6.4 (2026-05-10)
|
|
146
140
|
|
|
147
|
-
-
|
|
141
|
+
- Internal tooling refresh. No changes for users.
|
|
148
142
|
|
|
149
|
-
### 2.6.
|
|
143
|
+
### 2.6.3 (2026-05-10)
|
|
150
144
|
|
|
151
|
-
-
|
|
145
|
+
- Connection-status indicators recover on their own after a brief Govee outage instead of staying stuck until you restart the adapter.
|
|
146
|
+
- Restoring a snapshot on a multi-segment LED strip is fast again — a 30-segment snapshot replays in well under a second instead of about nine.
|
|
147
|
+
- A wrong manual pairing IP, a network blip, or a Cloud command that Govee rejects are now reported in the log instead of failing silently.
|
|
148
|
+
- LED segment 0 turning blue when a colour was set with a short hex value (e.g. `#FF`) is fixed — invalid colour input falls back to black.
|
|
149
|
+
- The segment-detection wizard restores the strip's previous look on adapter-stop, so the test pattern doesn't stay on the wall when you save the adapter settings.
|
|
150
|
+
- A rejected Govee API key now surfaces an actionable hint in the log so you know where to look.
|
|
151
|
+
- Various behind-the-scenes hardening of all four communication channels (LAN, MQTT, Cloud-events, Cloud REST) — invisible if everything was already running fine, robustness if something is unstable.
|
|
152
152
|
|
|
153
|
-
### 2.6.
|
|
153
|
+
### 2.6.2 (2026-05-09)
|
|
154
154
|
|
|
155
|
-
-
|
|
155
|
+
- Adapter log messages are now English only, in line with the ioBroker community standard. Localized state names, descriptions and dropdown labels (11 languages) are unchanged. The user-visible segment-detection wizard text in the admin UI also remains localized.
|
|
156
156
|
|
|
157
157
|
Older entries are in [CHANGELOG_OLD.md](CHANGELOG_OLD.md).
|
|
158
158
|
|
|
@@ -0,0 +1,124 @@
|
|
|
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 cache_exports = {};
|
|
20
|
+
__export(cache_exports, {
|
|
21
|
+
cachedToGoveeDevice: () => cachedToGoveeDevice,
|
|
22
|
+
goveeDeviceToCached: () => goveeDeviceToCached,
|
|
23
|
+
persistDeviceToCache: () => persistDeviceToCache,
|
|
24
|
+
populateScenesFromLibrary: () => populateScenesFromLibrary,
|
|
25
|
+
saveDevicesToCache: () => saveDevicesToCache
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(cache_exports);
|
|
28
|
+
function populateScenesFromLibrary(adapter, device) {
|
|
29
|
+
if (device.scenes.length === 0 && device.sceneLibrary.length > 0) {
|
|
30
|
+
device.scenes = device.sceneLibrary.map((entry) => ({
|
|
31
|
+
name: entry.name,
|
|
32
|
+
value: {}
|
|
33
|
+
// ptReal uses sceneLibrary directly, Cloud payload not needed
|
|
34
|
+
}));
|
|
35
|
+
adapter.log.debug(`${device.sku}: ${device.scenes.length} scenes from library (Cloud scenes missing)`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function cachedToGoveeDevice(cached) {
|
|
39
|
+
return {
|
|
40
|
+
sku: cached.sku,
|
|
41
|
+
deviceId: cached.deviceId,
|
|
42
|
+
name: cached.name,
|
|
43
|
+
type: cached.type,
|
|
44
|
+
capabilities: cached.capabilities,
|
|
45
|
+
scenes: cached.scenes,
|
|
46
|
+
diyScenes: cached.diyScenes,
|
|
47
|
+
snapshots: cached.snapshots,
|
|
48
|
+
sceneLibrary: cached.sceneLibrary,
|
|
49
|
+
musicLibrary: cached.musicLibrary,
|
|
50
|
+
diyLibrary: cached.diyLibrary,
|
|
51
|
+
skuFeatures: cached.skuFeatures,
|
|
52
|
+
snapshotBleCmds: cached.snapshotBleCmds,
|
|
53
|
+
scenesChecked: cached.scenesChecked,
|
|
54
|
+
lastSeenOnNetwork: cached.lastSeenOnNetwork,
|
|
55
|
+
// Restore learned count so it wins over Cloud capability on next start.
|
|
56
|
+
segmentCount: cached.segmentCount,
|
|
57
|
+
manualMode: cached.manualMode,
|
|
58
|
+
manualSegments: cached.manualSegments,
|
|
59
|
+
sceneSpeed: cached.sceneSpeed,
|
|
60
|
+
state: { online: false },
|
|
61
|
+
channels: { lan: false, mqtt: false, cloud: false }
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function goveeDeviceToCached(device) {
|
|
65
|
+
return {
|
|
66
|
+
sku: device.sku,
|
|
67
|
+
deviceId: device.deviceId,
|
|
68
|
+
name: device.name,
|
|
69
|
+
type: device.type,
|
|
70
|
+
capabilities: device.capabilities,
|
|
71
|
+
scenes: device.scenes,
|
|
72
|
+
diyScenes: device.diyScenes,
|
|
73
|
+
snapshots: device.snapshots,
|
|
74
|
+
sceneLibrary: device.sceneLibrary,
|
|
75
|
+
musicLibrary: device.musicLibrary,
|
|
76
|
+
diyLibrary: device.diyLibrary,
|
|
77
|
+
skuFeatures: device.skuFeatures,
|
|
78
|
+
snapshotBleCmds: device.snapshotBleCmds,
|
|
79
|
+
scenesChecked: device.scenesChecked,
|
|
80
|
+
lastSeenOnNetwork: device.lastSeenOnNetwork,
|
|
81
|
+
segmentCount: typeof device.segmentCount === "number" && device.segmentCount > 0 ? device.segmentCount : void 0,
|
|
82
|
+
manualMode: device.manualMode ? true : void 0,
|
|
83
|
+
manualSegments: device.manualMode && Array.isArray(device.manualSegments) && device.manualSegments.length > 0 ? device.manualSegments.slice() : void 0,
|
|
84
|
+
sceneSpeed: typeof device.sceneSpeed === "number" && device.sceneSpeed > 0 ? device.sceneSpeed : void 0,
|
|
85
|
+
cachedAt: Date.now()
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function persistDeviceToCache(adapter, device) {
|
|
89
|
+
if (!adapter.skuCache) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
adapter.skuCache.save(goveeDeviceToCached(device));
|
|
93
|
+
}
|
|
94
|
+
function saveDevicesToCache(adapter) {
|
|
95
|
+
if (!adapter.skuCache) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
let cachedCount = 0;
|
|
99
|
+
let skippedCount = 0;
|
|
100
|
+
for (const device of adapter.devices.values()) {
|
|
101
|
+
const isLight = device.type === "devices.types.light";
|
|
102
|
+
if (isLight && !device.scenesChecked) {
|
|
103
|
+
skippedCount++;
|
|
104
|
+
adapter.log.debug(`Not caching ${device.name} (${device.sku}) \u2014 scenes not yet checked`);
|
|
105
|
+
} else {
|
|
106
|
+
adapter.skuCache.save(goveeDeviceToCached(device));
|
|
107
|
+
cachedCount++;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (skippedCount > 0) {
|
|
111
|
+
adapter.log.debug(`Cached ${cachedCount} device(s), skipped ${skippedCount} not yet checked`);
|
|
112
|
+
} else {
|
|
113
|
+
adapter.log.debug(`Cached ${cachedCount} device(s) \u2014 next start uses cache`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
117
|
+
0 && (module.exports = {
|
|
118
|
+
cachedToGoveeDevice,
|
|
119
|
+
goveeDeviceToCached,
|
|
120
|
+
persistDeviceToCache,
|
|
121
|
+
populateScenesFromLibrary,
|
|
122
|
+
saveDevicesToCache
|
|
123
|
+
});
|
|
124
|
+
//# sourceMappingURL=cache.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/device-manager/cache.ts"],
|
|
4
|
+
"sourcesContent": ["import type { CachedDeviceData, SkuCache } from \"../sku-cache\";\nimport type { GoveeDevice } from \"../types\";\n\n/**\n * Adapter surface required by the cache helpers \u2014 DeviceManager exposes\n * `skuCache`, `devices`, and `log` in this shape.\n */\nexport interface DeviceCacheAdapter {\n readonly log: ioBroker.Logger;\n readonly skuCache: SkuCache | null;\n readonly devices: Map<string, GoveeDevice>;\n}\n\n/**\n * Fill device.scenes from sceneLibrary when Cloud scenes are missing.\n * ptReal activation matches by name, so sceneLibrary names are sufficient.\n *\n * @param adapter DeviceManager-shaped surface\n * @param device Device to populate scenes for\n */\nexport function populateScenesFromLibrary(adapter: DeviceCacheAdapter, device: GoveeDevice): void {\n if (device.scenes.length === 0 && device.sceneLibrary.length > 0) {\n device.scenes = device.sceneLibrary.map(entry => ({\n name: entry.name,\n value: {}, // ptReal uses sceneLibrary directly, Cloud payload not needed\n }));\n adapter.log.debug(`${device.sku}: ${device.scenes.length} scenes from library (Cloud scenes missing)`);\n }\n}\n\n/**\n * Convert cached data to a GoveeDevice (runtime fields set to defaults).\n *\n */\nexport function cachedToGoveeDevice(cached: CachedDeviceData): GoveeDevice {\n return {\n sku: cached.sku,\n deviceId: cached.deviceId,\n name: cached.name,\n type: cached.type,\n capabilities: cached.capabilities,\n scenes: cached.scenes,\n diyScenes: cached.diyScenes,\n snapshots: cached.snapshots,\n sceneLibrary: cached.sceneLibrary,\n musicLibrary: cached.musicLibrary,\n diyLibrary: cached.diyLibrary,\n skuFeatures: cached.skuFeatures,\n snapshotBleCmds: cached.snapshotBleCmds,\n scenesChecked: cached.scenesChecked,\n lastSeenOnNetwork: cached.lastSeenOnNetwork,\n // Restore learned count so it wins over Cloud capability on next start.\n segmentCount: cached.segmentCount,\n manualMode: cached.manualMode,\n manualSegments: cached.manualSegments,\n sceneSpeed: cached.sceneSpeed,\n state: { online: false },\n channels: { lan: false, mqtt: false, cloud: false },\n };\n}\n\n/**\n * Extract cacheable data from a GoveeDevice.\n *\n */\nexport function goveeDeviceToCached(device: GoveeDevice): CachedDeviceData {\n return {\n sku: device.sku,\n deviceId: device.deviceId,\n name: device.name,\n type: device.type,\n capabilities: device.capabilities,\n scenes: device.scenes,\n diyScenes: device.diyScenes,\n snapshots: device.snapshots,\n sceneLibrary: device.sceneLibrary,\n musicLibrary: device.musicLibrary,\n diyLibrary: device.diyLibrary,\n skuFeatures: device.skuFeatures,\n snapshotBleCmds: device.snapshotBleCmds,\n scenesChecked: device.scenesChecked,\n lastSeenOnNetwork: device.lastSeenOnNetwork,\n segmentCount: typeof device.segmentCount === \"number\" && device.segmentCount > 0 ? device.segmentCount : undefined,\n manualMode: device.manualMode ? true : undefined,\n manualSegments:\n device.manualMode && Array.isArray(device.manualSegments) && device.manualSegments.length > 0\n ? device.manualSegments.slice()\n : undefined,\n sceneSpeed: typeof device.sceneSpeed === \"number\" && device.sceneSpeed > 0 ? device.sceneSpeed : undefined,\n cachedAt: Date.now(),\n };\n}\n\n/**\n * Persist a device's current runtime state to the SKU cache. Safe no-op\n * when no cache is configured.\n *\n */\nexport function persistDeviceToCache(adapter: DeviceCacheAdapter, device: GoveeDevice): void {\n if (!adapter.skuCache) {\n return;\n }\n adapter.skuCache.save(goveeDeviceToCached(device));\n}\n\n/**\n * Save all devices to SKU cache, skipping only those never confirmed via\n * Cloud yet. Routine persistence \u2014 logs at debug.\n *\n */\nexport function saveDevicesToCache(adapter: DeviceCacheAdapter): void {\n if (!adapter.skuCache) {\n return;\n }\n\n let cachedCount = 0;\n let skippedCount = 0;\n for (const device of adapter.devices.values()) {\n const isLight = device.type === \"devices.types.light\";\n if (isLight && !device.scenesChecked) {\n skippedCount++;\n adapter.log.debug(`Not caching ${device.name} (${device.sku}) \u2014 scenes not yet checked`);\n } else {\n adapter.skuCache.save(goveeDeviceToCached(device));\n cachedCount++;\n }\n }\n if (skippedCount > 0) {\n adapter.log.debug(`Cached ${cachedCount} device(s), skipped ${skippedCount} not yet checked`);\n } else {\n adapter.log.debug(`Cached ${cachedCount} device(s) \u2014 next start uses cache`);\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
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 cloud_merge_exports = {};
|
|
20
|
+
__export(cloud_merge_exports, {
|
|
21
|
+
applyOnlineCap: () => applyOnlineCap,
|
|
22
|
+
mergeCloudDevices: () => mergeCloudDevices
|
|
23
|
+
});
|
|
24
|
+
module.exports = __toCommonJS(cloud_merge_exports);
|
|
25
|
+
var import_device_registry = require("../device-registry");
|
|
26
|
+
var import_mapping = require("./mapping");
|
|
27
|
+
var import_lookups = require("./lookups");
|
|
28
|
+
function mergeCloudDevices(adapter, cloudDevices) {
|
|
29
|
+
let changed = false;
|
|
30
|
+
if (!Array.isArray(cloudDevices)) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
for (const cd of cloudDevices) {
|
|
34
|
+
if (!cd || typeof cd.sku !== "string" || typeof cd.device !== "string") {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const existing = adapter.devices.get((0, import_lookups.deviceKey)(cd.sku, cd.device));
|
|
38
|
+
if (existing) {
|
|
39
|
+
existing.name = cd.deviceName || existing.name;
|
|
40
|
+
existing.capabilities = Array.isArray(cd.capabilities) ? cd.capabilities : [];
|
|
41
|
+
existing.type = cd.type;
|
|
42
|
+
existing.channels.cloud = true;
|
|
43
|
+
} else {
|
|
44
|
+
const device = (0, import_mapping.cloudDeviceToGoveeDevice)(cd);
|
|
45
|
+
adapter.devices.set((0, import_lookups.deviceKey)(cd.sku, cd.device), device);
|
|
46
|
+
changed = true;
|
|
47
|
+
adapter.log.debug(`Cloud: New device ${cd.deviceName} (${cd.sku})`);
|
|
48
|
+
adapter.maybeNudgeSeedSku(cd.sku, cd.deviceName);
|
|
49
|
+
}
|
|
50
|
+
const quirks = (0, import_device_registry.getDeviceQuirks)(cd.sku);
|
|
51
|
+
if (quirks == null ? void 0 : quirks.brokenPlatformApi) {
|
|
52
|
+
adapter.log.debug(`${cd.sku} has known broken platform API metadata \u2014 capabilities may be incomplete`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return changed;
|
|
56
|
+
}
|
|
57
|
+
function applyOnlineCap(adapter, device, caps) {
|
|
58
|
+
var _a;
|
|
59
|
+
let online;
|
|
60
|
+
for (const c of caps) {
|
|
61
|
+
if (c && typeof c.type === "string" && (c.type === "devices.capabilities.online" || c.type === "online") && c.state && typeof c.state.value === "boolean") {
|
|
62
|
+
online = c.state.value;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (online === void 0 && caps.length > 0) {
|
|
67
|
+
online = true;
|
|
68
|
+
}
|
|
69
|
+
if (online === void 0) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (device.state.online === online && online === true) {
|
|
73
|
+
device.lastSeenOnNetwork = Date.now();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
device.state.online = online;
|
|
77
|
+
if (online) {
|
|
78
|
+
device.lastSeenOnNetwork = Date.now();
|
|
79
|
+
}
|
|
80
|
+
(_a = adapter.onDeviceUpdate) == null ? void 0 : _a.call(adapter, device, { online });
|
|
81
|
+
}
|
|
82
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
83
|
+
0 && (module.exports = {
|
|
84
|
+
applyOnlineCap,
|
|
85
|
+
mergeCloudDevices
|
|
86
|
+
});
|
|
87
|
+
//# sourceMappingURL=cloud-merge.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/device-manager/cloud-merge.ts"],
|
|
4
|
+
"sourcesContent": ["import { getDeviceQuirks } from \"../device-registry\";\nimport type { CloudDevice, CloudStateCapability, DeviceState, GoveeDevice } from \"../types\";\nimport { cloudDeviceToGoveeDevice } from \"./mapping\";\nimport { deviceKey } from \"./lookups\";\n\n/**\n * Adapter surface required by the cloud-merge helpers \u2014 DeviceManager\n * exposes `log` and `devices`, plus a few dispatch hooks the merge path\n * fires when devices change.\n */\nexport interface CloudMergeAdapter {\n readonly log: ioBroker.Logger;\n readonly devices: Map<string, GoveeDevice>;\n /** Fired when a device's cap-derived state changes (online flip etc.). */\n onDeviceUpdate?: ((device: GoveeDevice, state: Partial<DeviceState>) => void) | null;\n /** Optional one-shot SKU nudge. */\n maybeNudgeSeedSku(sku: string, displayName: string | undefined): void;\n}\n\n/**\n * Merge a Cloud device list into the registry. Updates existing entries\n * with refreshed name/capabilities/type and registers new ones via\n * {@link cloudDeviceToGoveeDevice}. Returns true when at least one new\n * device was added.\n *\n */\nexport function mergeCloudDevices(adapter: CloudMergeAdapter, cloudDevices: CloudDevice[]): boolean {\n let changed = false;\n if (!Array.isArray(cloudDevices)) {\n return false;\n }\n for (const cd of cloudDevices) {\n if (!cd || typeof cd.sku !== \"string\" || typeof cd.device !== \"string\") {\n continue;\n }\n const existing = adapter.devices.get(deviceKey(cd.sku, cd.device));\n if (existing) {\n existing.name = cd.deviceName || existing.name;\n existing.capabilities = Array.isArray(cd.capabilities) ? cd.capabilities : [];\n existing.type = cd.type;\n existing.channels.cloud = true;\n } else {\n const device = cloudDeviceToGoveeDevice(cd);\n adapter.devices.set(deviceKey(cd.sku, cd.device), device);\n changed = true;\n adapter.log.debug(`Cloud: New device ${cd.deviceName} (${cd.sku})`);\n adapter.maybeNudgeSeedSku(cd.sku, cd.deviceName);\n }\n\n const quirks = getDeviceQuirks(cd.sku);\n if (quirks?.brokenPlatformApi) {\n adapter.log.debug(`${cd.sku} has known broken platform API metadata \u2014 capabilities may be incomplete`);\n }\n }\n return changed;\n}\n\n/**\n * Read the multi-source online indicator from a capability list and apply\n * it to the device's state. Capability list with no explicit online flag\n * is treated as \u201Ewe just heard from the device\" \u2014 assume online so a\n * Cloud poll-success doesn't leave a known-good device flagged offline.\n *\n * Skip the onDeviceUpdate fire if device already-online + still-online,\n * but refresh `lastSeenOnNetwork` either way.\n *\n */\nexport function applyOnlineCap(adapter: CloudMergeAdapter, device: GoveeDevice, caps: CloudStateCapability[]): void {\n let online: boolean | undefined;\n for (const c of caps) {\n if (\n c &&\n typeof c.type === \"string\" &&\n (c.type === \"devices.capabilities.online\" || c.type === \"online\") &&\n c.state &&\n typeof c.state.value === \"boolean\"\n ) {\n online = c.state.value;\n break;\n }\n }\n if (online === undefined && caps.length > 0) {\n online = true;\n }\n if (online === undefined) {\n return;\n }\n if (device.state.online === online && online === true) {\n device.lastSeenOnNetwork = Date.now();\n return;\n }\n device.state.online = online;\n if (online) {\n device.lastSeenOnNetwork = Date.now();\n }\n adapter.onDeviceUpdate?.(device, { online });\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,6BAAgC;AAEhC,qBAAyC;AACzC,qBAA0B;AAuBnB,SAAS,kBAAkB,SAA4B,cAAsC;AAClG,MAAI,UAAU;AACd,MAAI,CAAC,MAAM,QAAQ,YAAY,GAAG;AAChC,WAAO;AAAA,EACT;AACA,aAAW,MAAM,cAAc;AAC7B,QAAI,CAAC,MAAM,OAAO,GAAG,QAAQ,YAAY,OAAO,GAAG,WAAW,UAAU;AACtE;AAAA,IACF;AACA,UAAM,WAAW,QAAQ,QAAQ,QAAI,0BAAU,GAAG,KAAK,GAAG,MAAM,CAAC;AACjE,QAAI,UAAU;AACZ,eAAS,OAAO,GAAG,cAAc,SAAS;AAC1C,eAAS,eAAe,MAAM,QAAQ,GAAG,YAAY,IAAI,GAAG,eAAe,CAAC;AAC5E,eAAS,OAAO,GAAG;AACnB,eAAS,SAAS,QAAQ;AAAA,IAC5B,OAAO;AACL,YAAM,aAAS,yCAAyB,EAAE;AAC1C,cAAQ,QAAQ,QAAI,0BAAU,GAAG,KAAK,GAAG,MAAM,GAAG,MAAM;AACxD,gBAAU;AACV,cAAQ,IAAI,MAAM,qBAAqB,GAAG,UAAU,KAAK,GAAG,GAAG,GAAG;AAClE,cAAQ,kBAAkB,GAAG,KAAK,GAAG,UAAU;AAAA,IACjD;AAEA,UAAM,aAAS,wCAAgB,GAAG,GAAG;AACrC,QAAI,iCAAQ,mBAAmB;AAC7B,cAAQ,IAAI,MAAM,GAAG,GAAG,GAAG,+EAA0E;AAAA,IACvG;AAAA,EACF;AACA,SAAO;AACT;AAYO,SAAS,eAAe,SAA4B,QAAqB,MAAoC;AAnEpH;AAoEE,MAAI;AACJ,aAAW,KAAK,MAAM;AACpB,QACE,KACA,OAAO,EAAE,SAAS,aACjB,EAAE,SAAS,iCAAiC,EAAE,SAAS,aACxD,EAAE,SACF,OAAO,EAAE,MAAM,UAAU,WACzB;AACA,eAAS,EAAE,MAAM;AACjB;AAAA,IACF;AAAA,EACF;AACA,MAAI,WAAW,UAAa,KAAK,SAAS,GAAG;AAC3C,aAAS;AAAA,EACX;AACA,MAAI,WAAW,QAAW;AACxB;AAAA,EACF;AACA,MAAI,OAAO,MAAM,WAAW,UAAU,WAAW,MAAM;AACrD,WAAO,oBAAoB,KAAK,IAAI;AACpC;AAAA,EACF;AACA,SAAO,MAAM,SAAS;AACtB,MAAI,QAAQ;AACV,WAAO,oBAAoB,KAAK,IAAI;AAAA,EACtC;AACA,gBAAQ,mBAAR,iCAAyB,QAAQ,EAAE,OAAO;AAC5C;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
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 lookups_exports = {};
|
|
20
|
+
__export(lookups_exports, {
|
|
21
|
+
SEGMENT_HARD_MAX: () => SEGMENT_HARD_MAX,
|
|
22
|
+
deviceKey: () => deviceKey,
|
|
23
|
+
findDeviceBySkuAndId: () => findDeviceBySkuAndId,
|
|
24
|
+
getEffectiveSegmentIndices: () => getEffectiveSegmentIndices,
|
|
25
|
+
parseMqttSegmentData: () => parseMqttSegmentData,
|
|
26
|
+
resolveSegmentCount: () => resolveSegmentCount
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(lookups_exports);
|
|
29
|
+
var import_types = require("../types");
|
|
30
|
+
function parseMqttSegmentData(commands) {
|
|
31
|
+
if (!Array.isArray(commands)) {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
const segments = [];
|
|
35
|
+
let highestPacket = 0;
|
|
36
|
+
for (const cmd of commands) {
|
|
37
|
+
if (typeof cmd !== "string") {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const bytes = Buffer.from(cmd, "base64");
|
|
41
|
+
if (bytes.length < 20 || bytes[0] !== 170 || bytes[1] !== 165) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
let xor = 0;
|
|
45
|
+
for (let i = 0; i < 19; i++) {
|
|
46
|
+
xor ^= bytes[i];
|
|
47
|
+
}
|
|
48
|
+
if (xor !== bytes[19]) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const packetNum = bytes[2];
|
|
52
|
+
if (packetNum < 1 || packetNum > 5) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (packetNum > highestPacket) {
|
|
56
|
+
highestPacket = packetNum;
|
|
57
|
+
}
|
|
58
|
+
const baseIndex = (packetNum - 1) * 4;
|
|
59
|
+
for (let slot = 0; slot < 4; slot++) {
|
|
60
|
+
const segIdx = baseIndex + slot;
|
|
61
|
+
const offset = 3 + slot * 4;
|
|
62
|
+
segments.push({
|
|
63
|
+
index: segIdx,
|
|
64
|
+
brightness: bytes[offset],
|
|
65
|
+
r: bytes[offset + 1],
|
|
66
|
+
g: bytes[offset + 2],
|
|
67
|
+
b: bytes[offset + 3]
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
while (segments.length > 0) {
|
|
72
|
+
const tail = segments[segments.length - 1];
|
|
73
|
+
if (tail.brightness === 0 && tail.r === 0 && tail.g === 0 && tail.b === 0) {
|
|
74
|
+
segments.pop();
|
|
75
|
+
} else {
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return segments;
|
|
80
|
+
}
|
|
81
|
+
function getEffectiveSegmentIndices(device) {
|
|
82
|
+
var _a;
|
|
83
|
+
if (device.manualMode && Array.isArray(device.manualSegments) && device.manualSegments.length > 0) {
|
|
84
|
+
return device.manualSegments.slice();
|
|
85
|
+
}
|
|
86
|
+
const count = (_a = device.segmentCount) != null ? _a : 0;
|
|
87
|
+
if (count <= 0) {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
return Array.from({ length: count }, (_, i) => i);
|
|
91
|
+
}
|
|
92
|
+
function resolveSegmentCount(device) {
|
|
93
|
+
if (typeof device.segmentCount === "number" && device.segmentCount > 0) {
|
|
94
|
+
return device.segmentCount;
|
|
95
|
+
}
|
|
96
|
+
const caps = Array.isArray(device.capabilities) ? device.capabilities : [];
|
|
97
|
+
let min = Number.POSITIVE_INFINITY;
|
|
98
|
+
for (const c of caps) {
|
|
99
|
+
if (!c || typeof c.type !== "string" || !c.type.includes("segment_color_setting")) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const params = c.parameters;
|
|
103
|
+
const fields = Array.isArray(params == null ? void 0 : params.fields) ? params.fields : [];
|
|
104
|
+
for (const f of fields) {
|
|
105
|
+
if (!f || typeof f !== "object") {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const fn = f.fieldName;
|
|
109
|
+
const er = f.elementRange;
|
|
110
|
+
const rawMax = er && typeof er.max === "number" ? er.max : -1;
|
|
111
|
+
if (fn === "segment" && rawMax >= 0) {
|
|
112
|
+
const n = rawMax + 1;
|
|
113
|
+
if (n > 0 && n < min) {
|
|
114
|
+
min = n;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return Number.isFinite(min) ? min : 0;
|
|
120
|
+
}
|
|
121
|
+
const SEGMENT_HARD_MAX = 55;
|
|
122
|
+
function deviceKey(sku, deviceId) {
|
|
123
|
+
return `${sku}_${(0, import_types.normalizeDeviceId)(deviceId)}`;
|
|
124
|
+
}
|
|
125
|
+
function findDeviceBySkuAndId(devices, sku, deviceId) {
|
|
126
|
+
const direct = devices.get(deviceKey(sku, deviceId));
|
|
127
|
+
if (direct) {
|
|
128
|
+
return direct;
|
|
129
|
+
}
|
|
130
|
+
const normalizedId = (0, import_types.normalizeDeviceId)(deviceId);
|
|
131
|
+
for (const dev of devices.values()) {
|
|
132
|
+
if (dev.sku === sku && (0, import_types.normalizeDeviceId)(dev.deviceId) === normalizedId) {
|
|
133
|
+
return dev;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return void 0;
|
|
137
|
+
}
|
|
138
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
139
|
+
0 && (module.exports = {
|
|
140
|
+
SEGMENT_HARD_MAX,
|
|
141
|
+
deviceKey,
|
|
142
|
+
findDeviceBySkuAndId,
|
|
143
|
+
getEffectiveSegmentIndices,
|
|
144
|
+
parseMqttSegmentData,
|
|
145
|
+
resolveSegmentCount
|
|
146
|
+
});
|
|
147
|
+
//# sourceMappingURL=lookups.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/device-manager/lookups.ts"],
|
|
4
|
+
"sourcesContent": ["import { normalizeDeviceId, type GoveeDevice } from \"../types\";\n\n/** Parsed per-segment data from MQTT BLE packets */\nexport interface MqttSegmentData {\n /** Segment index (0-based) */\n index: number;\n /** Per-segment brightness 0-100 */\n brightness: number;\n /** Red channel 0-255 */\n r: number;\n /** Green channel 0-255 */\n g: number;\n /** Blue channel 0-255 */\n b: number;\n}\n\n/**\n * Parse AA A5 BLE notification packets from MQTT op.command.\n * 5 packets \u00D7 4 segment slots = max 20 segments per push. The device sends\n * exactly as many packets as it has physical segments \u2014 so parsing out all\n * slots (and filtering empty-slot padding) gives us a reliable count of\n * what actually exists on the strip.\n *\n * Format per slot: [Brightness 0-100] [R] [G] [B].\n *\n * An \"empty\" slot (brightness = 0 AND r = g = b = 0) is treated as padding\n * in a partially-filled final packet, not as a real unlit segment \u2014 this\n * matters for devices that don't pad their last packet to 4 slots.\n *\n * @param commands Base64-encoded BLE packets from MQTT op.command\n */\nexport function parseMqttSegmentData(commands: string[]): MqttSegmentData[] {\n if (!Array.isArray(commands)) {\n return [];\n }\n\n const segments: MqttSegmentData[] = [];\n let highestPacket = 0;\n\n for (const cmd of commands) {\n if (typeof cmd !== \"string\") {\n continue;\n }\n const bytes = Buffer.from(cmd, \"base64\");\n if (bytes.length < 20 || bytes[0] !== 0xaa || bytes[1] !== 0xa5) {\n continue;\n }\n\n // M2 \u2014 XOR-Checksum-Validation. Govee BLE-Pakete haben am letzten Byte\n // (Index 19) einen XOR \u00FCber Bytes 0-18. Spoofed/malformed Pakete\n // schl\u00FCpfen sonst durch und persistieren falsche segmentCount.\n let xor = 0;\n for (let i = 0; i < 19; i++) {\n xor ^= bytes[i];\n }\n if (xor !== bytes[19]) {\n continue;\n }\n\n const packetNum = bytes[2];\n if (packetNum < 1 || packetNum > 5) {\n continue;\n }\n if (packetNum > highestPacket) {\n highestPacket = packetNum;\n }\n\n const baseIndex = (packetNum - 1) * 4;\n for (let slot = 0; slot < 4; slot++) {\n const segIdx = baseIndex + slot;\n const offset = 3 + slot * 4;\n segments.push({\n index: segIdx,\n brightness: bytes[offset],\n r: bytes[offset + 1],\n g: bytes[offset + 2],\n b: bytes[offset + 3],\n });\n }\n }\n\n while (segments.length > 0) {\n const tail = segments[segments.length - 1];\n if (tail.brightness === 0 && tail.r === 0 && tail.g === 0 && tail.b === 0) {\n segments.pop();\n } else {\n break;\n }\n }\n\n return segments;\n}\n\n/**\n * Effective physical segment indices for a device.\n * Uses `device.manualSegments` when `device.manualMode=true` (cut strip override),\n * falls back to `0..segmentCount-1` otherwise. Empty if device has no segments.\n *\n * @param device Target device\n */\nexport function getEffectiveSegmentIndices(device: GoveeDevice): number[] {\n if (device.manualMode && Array.isArray(device.manualSegments) && device.manualSegments.length > 0) {\n return device.manualSegments.slice();\n }\n const count = device.segmentCount ?? 0;\n if (count <= 0) {\n return [];\n }\n return Array.from({ length: count }, (_, i) => i);\n}\n\n/**\n * Resolve the authoritative segment count for a device.\n *\n * Priority:\n * 1. `device.segmentCount` if already set (from cache, MQTT discovery, or wizard)\n * 2. Minimum of positive `segment_color_setting` capability counts\n * 3. 0 if no capability advertises segments\n *\n * Why `min` over the capability caps: Govee reports `segmentedBrightness` and\n * `segmentedColorRgb` separately, and on at least one SKU (H70D1) those two\n * disagree \u2014 brightness says 10, colorRgb says 15, real device has 10.\n * Picking the smaller value is the safer starting point; MQTT discovery can\n * then grow it if the real device pushes more slots.\n *\n * @param device Target device\n */\nexport function resolveSegmentCount(device: GoveeDevice): number {\n if (typeof device.segmentCount === \"number\" && device.segmentCount > 0) {\n return device.segmentCount;\n }\n const caps = Array.isArray(device.capabilities) ? device.capabilities : [];\n let min = Number.POSITIVE_INFINITY;\n for (const c of caps) {\n if (!c || typeof c.type !== \"string\" || !c.type.includes(\"segment_color_setting\")) {\n continue;\n }\n const params = (c as { parameters?: { fields?: unknown[] } }).parameters;\n const fields = Array.isArray(params?.fields) ? params.fields : [];\n for (const f of fields) {\n if (!f || typeof f !== \"object\") {\n continue;\n }\n const fn = (f as { fieldName?: unknown }).fieldName;\n const er = (f as { elementRange?: { max?: unknown } }).elementRange;\n const rawMax = er && typeof er.max === \"number\" ? er.max : -1;\n if (fn === \"segment\" && rawMax >= 0) {\n const n = rawMax + 1;\n if (n > 0 && n < min) {\n min = n;\n }\n }\n }\n }\n return Number.isFinite(min) ? min : 0;\n}\n\n/** Protocol limit: Govee's segment bitmask is 7 bytes \u00D7 8 bits = 56 slots (0..55). */\nexport const SEGMENT_HARD_MAX = 55;\n\n/**\n * Generate stable map key for a device.\n *\n */\nexport function deviceKey(sku: string, deviceId: string): string {\n return `${sku}_${normalizeDeviceId(deviceId)}`;\n}\n\n/**\n * Locate a device in the registry by SKU + raw deviceId, with normalized\n * fallback. Direct key-hit first; if that misses, scan for a normalized\n * match (device IDs come from multiple sources with different\n * colon/case conventions).\n *\n */\nexport function findDeviceBySkuAndId(\n devices: Map<string, GoveeDevice>,\n sku: string,\n deviceId: string,\n): GoveeDevice | undefined {\n const direct = devices.get(deviceKey(sku, deviceId));\n if (direct) {\n return direct;\n }\n const normalizedId = normalizeDeviceId(deviceId);\n for (const dev of devices.values()) {\n if (dev.sku === sku && normalizeDeviceId(dev.deviceId) === normalizedId) {\n return dev;\n }\n }\n return undefined;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
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 mapping_exports = {};
|
|
20
|
+
__export(mapping_exports, {
|
|
21
|
+
buildCapabilitiesFromAppEntry: () => buildCapabilitiesFromAppEntry,
|
|
22
|
+
cloudDeviceToGoveeDevice: () => cloudDeviceToGoveeDevice
|
|
23
|
+
});
|
|
24
|
+
module.exports = __toCommonJS(mapping_exports);
|
|
25
|
+
function cloudDeviceToGoveeDevice(cd) {
|
|
26
|
+
return {
|
|
27
|
+
sku: cd.sku,
|
|
28
|
+
deviceId: cd.device,
|
|
29
|
+
name: cd.deviceName || cd.sku,
|
|
30
|
+
type: cd.type || "unknown",
|
|
31
|
+
capabilities: Array.isArray(cd.capabilities) ? cd.capabilities : [],
|
|
32
|
+
scenes: [],
|
|
33
|
+
diyScenes: [],
|
|
34
|
+
snapshots: [],
|
|
35
|
+
sceneLibrary: [],
|
|
36
|
+
musicLibrary: [],
|
|
37
|
+
diyLibrary: [],
|
|
38
|
+
skuFeatures: null,
|
|
39
|
+
state: { online: true },
|
|
40
|
+
channels: { lan: false, mqtt: false, cloud: true }
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function buildCapabilitiesFromAppEntry(entry) {
|
|
44
|
+
const caps = [];
|
|
45
|
+
const last = entry.lastData;
|
|
46
|
+
if (!last) {
|
|
47
|
+
return caps;
|
|
48
|
+
}
|
|
49
|
+
if (typeof last.online === "boolean") {
|
|
50
|
+
caps.push({
|
|
51
|
+
type: "devices.capabilities.online",
|
|
52
|
+
instance: "online",
|
|
53
|
+
state: { value: last.online }
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
if (typeof last.tem === "number" && Number.isFinite(last.tem)) {
|
|
57
|
+
caps.push({
|
|
58
|
+
type: "devices.capabilities.property",
|
|
59
|
+
instance: "sensorTemperature",
|
|
60
|
+
state: { value: last.tem / 100 }
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (typeof last.hum === "number" && Number.isFinite(last.hum)) {
|
|
64
|
+
caps.push({
|
|
65
|
+
type: "devices.capabilities.property",
|
|
66
|
+
instance: "sensorHumidity",
|
|
67
|
+
state: { value: last.hum / 100 }
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
if (typeof last.battery === "number" && Number.isFinite(last.battery)) {
|
|
71
|
+
caps.push({
|
|
72
|
+
type: "devices.capabilities.property",
|
|
73
|
+
instance: "battery",
|
|
74
|
+
state: { value: last.battery }
|
|
75
|
+
});
|
|
76
|
+
} else if (entry.settings && typeof entry.settings.battery === "number" && Number.isFinite(entry.settings.battery)) {
|
|
77
|
+
caps.push({
|
|
78
|
+
type: "devices.capabilities.property",
|
|
79
|
+
instance: "battery",
|
|
80
|
+
state: { value: entry.settings.battery }
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return caps;
|
|
84
|
+
}
|
|
85
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
86
|
+
0 && (module.exports = {
|
|
87
|
+
buildCapabilitiesFromAppEntry,
|
|
88
|
+
cloudDeviceToGoveeDevice
|
|
89
|
+
});
|
|
90
|
+
//# sourceMappingURL=mapping.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/device-manager/mapping.ts"],
|
|
4
|
+
"sourcesContent": ["import type { AppDeviceEntry } from \"../govee-api-client\";\nimport type { CloudDevice, CloudStateCapability, GoveeDevice } from \"../types\";\n\n/**\n * Convert Cloud device to internal device model.\n *\n */\nexport function cloudDeviceToGoveeDevice(cd: CloudDevice): GoveeDevice {\n return {\n sku: cd.sku,\n deviceId: cd.device,\n name: cd.deviceName || cd.sku,\n type: cd.type || \"unknown\",\n capabilities: Array.isArray(cd.capabilities) ? cd.capabilities : [],\n scenes: [],\n diyScenes: [],\n snapshots: [],\n sceneLibrary: [],\n musicLibrary: [],\n diyLibrary: [],\n skuFeatures: null,\n state: { online: true },\n channels: { lan: false, mqtt: false, cloud: true },\n };\n}\n\n/**\n * Convert an AppApi device entry into a synthetic capability list \u2014 the\n * App API doesn't expose capability metadata, but the user wants the same\n * `info.online` / `sensorTemperature` / `sensorHumidity` / `battery`\n * states regardless of which channel delivered the data.\n *\n * Used to bridge App-API events into the same per-device state-tree shape\n * that Cloud-driven devices produce.\n *\n * @param entry App-API device entry from the recent-data endpoint\n */\nexport function buildCapabilitiesFromAppEntry(entry: AppDeviceEntry): CloudStateCapability[] {\n const caps: CloudStateCapability[] = [];\n const last = entry.lastData;\n if (!last) {\n return caps;\n }\n if (typeof last.online === \"boolean\") {\n caps.push({\n type: \"devices.capabilities.online\",\n instance: \"online\",\n state: { value: last.online },\n });\n }\n if (typeof last.tem === \"number\" && Number.isFinite(last.tem)) {\n caps.push({\n type: \"devices.capabilities.property\",\n instance: \"sensorTemperature\",\n state: { value: last.tem / 100 },\n });\n }\n if (typeof last.hum === \"number\" && Number.isFinite(last.hum)) {\n caps.push({\n type: \"devices.capabilities.property\",\n instance: \"sensorHumidity\",\n state: { value: last.hum / 100 },\n });\n }\n if (typeof last.battery === \"number\" && Number.isFinite(last.battery)) {\n caps.push({\n type: \"devices.capabilities.property\",\n instance: \"battery\",\n state: { value: last.battery },\n });\n } else if (entry.settings && typeof entry.settings.battery === \"number\" && Number.isFinite(entry.settings.battery)) {\n caps.push({\n type: \"devices.capabilities.property\",\n instance: \"battery\",\n state: { value: entry.settings.battery },\n });\n }\n return caps;\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOO,SAAS,yBAAyB,IAA8B;AACrE,SAAO;AAAA,IACL,KAAK,GAAG;AAAA,IACR,UAAU,GAAG;AAAA,IACb,MAAM,GAAG,cAAc,GAAG;AAAA,IAC1B,MAAM,GAAG,QAAQ;AAAA,IACjB,cAAc,MAAM,QAAQ,GAAG,YAAY,IAAI,GAAG,eAAe,CAAC;AAAA,IAClE,QAAQ,CAAC;AAAA,IACT,WAAW,CAAC;AAAA,IACZ,WAAW,CAAC;AAAA,IACZ,cAAc,CAAC;AAAA,IACf,cAAc,CAAC;AAAA,IACf,YAAY,CAAC;AAAA,IACb,aAAa;AAAA,IACb,OAAO,EAAE,QAAQ,KAAK;AAAA,IACtB,UAAU,EAAE,KAAK,OAAO,MAAM,OAAO,OAAO,KAAK;AAAA,EACnD;AACF;AAaO,SAAS,8BAA8B,OAA+C;AAC3F,QAAM,OAA+B,CAAC;AACtC,QAAM,OAAO,MAAM;AACnB,MAAI,CAAC,MAAM;AACT,WAAO;AAAA,EACT;AACA,MAAI,OAAO,KAAK,WAAW,WAAW;AACpC,SAAK,KAAK;AAAA,MACR,MAAM;AAAA,MACN,UAAU;AAAA,MACV,OAAO,EAAE,OAAO,KAAK,OAAO;AAAA,IAC9B,CAAC;AAAA,EACH;AACA,MAAI,OAAO,KAAK,QAAQ,YAAY,OAAO,SAAS,KAAK,GAAG,GAAG;AAC7D,SAAK,KAAK;AAAA,MACR,MAAM;AAAA,MACN,UAAU;AAAA,MACV,OAAO,EAAE,OAAO,KAAK,MAAM,IAAI;AAAA,IACjC,CAAC;AAAA,EACH;AACA,MAAI,OAAO,KAAK,QAAQ,YAAY,OAAO,SAAS,KAAK,GAAG,GAAG;AAC7D,SAAK,KAAK;AAAA,MACR,MAAM;AAAA,MACN,UAAU;AAAA,MACV,OAAO,EAAE,OAAO,KAAK,MAAM,IAAI;AAAA,IACjC,CAAC;AAAA,EACH;AACA,MAAI,OAAO,KAAK,YAAY,YAAY,OAAO,SAAS,KAAK,OAAO,GAAG;AACrE,SAAK,KAAK;AAAA,MACR,MAAM;AAAA,MACN,UAAU;AAAA,MACV,OAAO,EAAE,OAAO,KAAK,QAAQ;AAAA,IAC/B,CAAC;AAAA,EACH,WAAW,MAAM,YAAY,OAAO,MAAM,SAAS,YAAY,YAAY,OAAO,SAAS,MAAM,SAAS,OAAO,GAAG;AAClH,SAAK,KAAK;AAAA,MACR,MAAM;AAAA,MACN,UAAU;AAAA,MACV,OAAO,EAAE,OAAO,MAAM,SAAS,QAAQ;AAAA,IACzC,CAAC;AAAA,EACH;AACA,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|