iobroker.govee-smart 2.41.0 → 3.0.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
@@ -55,7 +55,7 @@ For details and how to disable it, see the [Sentry plugin documentation](https:/
55
55
 
56
56
  - Node.js >= 22
57
57
  - ioBroker js-controller >= 7.2.2
58
- - ioBroker Admin >= 8.0.11
58
+ - ioBroker Admin >= 8.0.14
59
59
  - A Govee account and at least one Govee WiFi device. LAN control needs a light with LAN mode enabled in the Govee Home app — see Govee's [LAN-supported device list](https://app-h5.govee.com/user-manual/wlan-guide).
60
60
 
61
61
  > The adapter CANNOT be installed via GitHub: The adapter must be installed via the ioBroker repository (stable or latest).
@@ -95,6 +95,18 @@ This adapter's MQTT authentication and BLE-over-LAN (ptReal) protocol implementa
95
95
  ### **WORK IN PROGRESS**
96
96
  -->
97
97
 
98
+ ### 3.0.1 (2026-09-27)
99
+
100
+ - Improved: the note the Admin shows before an update to 3.x is short now: the warning, one example old → new and a link to the details
101
+
102
+ ### 3.0.0 (2026-09-26)
103
+
104
+ - Changed: every device gets a new object ID once — model and last four characters with a hyphen, e.g. `devices.h61be-525f`; scripts and visualizations need the new IDs
105
+ - Changed: the move carries values, recording settings, rooms, functions and aliases along, and recorded history continues in its old series
106
+ - Fixed: two devices of one model whose IDs end alike now get a tree each and each receives its own commands — until now they shared one
107
+ - New: the H1741 battery table lamp reports its charge level in `sensor.battery`; Govee reports a fully charged battery as about 80 percent
108
+ - Fixed: fans and heaters with a numeric level (H7102, H7130) store it as a number, and the H7121 no longer puts a warning in the log at every refresh
109
+
98
110
  ### 2.41.0 (2026-09-26)
99
111
 
100
112
  - Changed: Discovery follows the selected network interface only — the additional scan addresses setting is gone, and the broadcast goes to the network of the chosen card
@@ -129,22 +141,6 @@ This adapter's MQTT authentication and BLE-over-LAN (ptReal) protocol implementa
129
141
  - Fixed: App groups are no longer asked for a device state at every start — Govee answered each call with an error that only filled the diagnostics report
130
142
  - Fixed: A scene request Govee refuses is no longer taken as "no scenes" — the cached scenes and snapshots stay, and the report names the reason once
131
143
 
132
- ### 2.39.1 (2026-09-22)
133
-
134
- - Fixed: The diagnostics report no longer contains your Wi-Fi network name, the Govee app's device number or a group's id — they appeared in clear in every exported file
135
- - Improved: The diagnostics report keeps Govee's complete account-list entry and shows commands waiting for an offline device and when the adapter started
136
-
137
- ### 2.39.0 (2026-09-22)
138
-
139
- - New: A device that has gone quiet is asked for its status over the Govee account connection — a bulb or purifier that works but showed as unreachable now stays reachable
140
- - Improved: The cloud budget follows Govee's per-device limits — a command for one light no longer waits for another light's calls or for library downloads
141
- - New: A command Govee refused because the device was offline is delivered once the device reports back (within five minutes), instead of being lost
142
- - Fixed: Scenes and libraries that arrived after a busy start now reach the scene dropdown and the cache — they used to stay at `---` and were fetched again on every start
143
- - Fixed: A scene list that shrank no longer leaves withdrawn scenes in the dropdown
144
- - Fixed: The diagnostics report no longer lists a reachability refresh for appliances, which never get one
145
- - New: 28 more Govee models are recognised — meat thermometers, motion and pressure sensors, heaters, kettles, a composter and the gateways that carry battery sensors
146
- - New: H1771 Table Lamp reported working by a user
147
-
148
144
  [Older changelogs can be found there](CHANGELOG_OLD.md)
149
145
 
150
146
  ## Support
@@ -80,8 +80,17 @@ const purifierH7127 = (frames, device) => {
80
80
  }
81
81
  return out;
82
82
  };
83
+ const lampH1741 = (frames) => {
84
+ const frame = frames.find((f) => f.fn === 66);
85
+ if (!frame) {
86
+ return [];
87
+ }
88
+ const level = frame.sub & 127;
89
+ return level <= 100 ? [{ type: import_govee_constants.GOVEE_CAP_TYPE.PROPERTY, instance: "battery", state: { value: level } }] : [];
90
+ };
83
91
  const DECODERS = {
84
- H7127: purifierH7127
92
+ H7127: purifierH7127,
93
+ H1741: lampH1741
85
94
  };
86
95
  function decodeApplianceFrames(device, opCommand) {
87
96
  const decoder = DECODERS[device.sku];
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/appliance-frames.ts"],
4
- "sourcesContent": ["import { GOVEE_CAP_TYPE } from \"./govee-constants\";\nimport type { CapabilityOption, CloudStateCapability, GoveeDevice } from \"./types\";\n\n/**\n * A 20-byte status frame from an appliance's own status push: `aa <fn> <sub>\n * <payload\u2026> <xor>` \u2014 Govee's BLE notification format, relayed verbatim in\n * the account broker's `op.command`. The XOR over bytes 0\u201318 sits in byte 19;\n * measured on 794 of 794 `aa` frames of two H7127 exports (issue #47).\n */\ninterface Frame {\n fn: number;\n sub: number;\n bytes: Buffer;\n}\n\nfunction parseFrame(raw: unknown): Frame | null {\n if (typeof raw !== \"string\" || raw.length === 0) {\n return null;\n }\n const bytes = Buffer.from(raw, \"base64\");\n if (bytes.length !== 20 || bytes[0] !== 0xaa) {\n return null;\n }\n let xor = 0;\n for (let i = 0; i < 19; i++) {\n xor ^= bytes[i];\n }\n if (xor !== bytes[19]) {\n return null;\n }\n return { fn: bytes[1], sub: bytes[2], bytes };\n}\n\ntype FrameDecoder = (frames: Frame[], device: GoveeDevice) => CloudStateCapability[];\n\n/**\n * The `workMode` STRUCT capability the device declared, if any.\n *\n * @param device The device whose declared capabilities are searched\n */\nfunction declaredWorkMode(device: GoveeDevice): { modes: CapabilityOption[]; levels: CapabilityOption[] } | null {\n const cap = device.capabilities.find(c => c.type === GOVEE_CAP_TYPE.WORK_MODE && c.instance === \"workMode\");\n const fields = cap?.parameters?.fields ?? [];\n const modes = fields.find(f => f && f.fieldName === \"workMode\")?.options;\n const levels = fields.find(f => f && f.fieldName === \"modeValue\")?.options;\n return Array.isArray(modes) && Array.isArray(levels) ? { modes, levels } : null;\n}\n\n/**\n * H7127 air purifier (issue #47; spec \u00A711). Measured frames:\n * aa 05 00 <mode> work mode (declared option value)\n * aa 05 01 <level> last manual level (declared gearMode option value)\n * aa 19 00 ff ff <aq> 00 <filter> filter life in % (byte 7 \u2014 two values against\n * two references); byte 5 is probably air quality\n * but was measured once and never seen changing,\n * so it is NOT decoded \u2014 the cloud seed carries it.\n * The level is reported only for a mode that declares levels \u2014 the same shape\n * the cloud state read returns (`{workMode: 2, modeValue: 0}` for Custom).\n *\n * @param frames The checksum-valid frames of one status packet\n * @param device The device the packet belongs to (declared capabilities)\n */\nconst purifierH7127: FrameDecoder = (frames, device) => {\n const out: CloudStateCapability[] = [];\n const declared = declaredWorkMode(device);\n const modeFrame = frames.find(f => f.fn === 0x05 && f.sub === 0x00);\n const levelFrame = frames.find(f => f.fn === 0x05 && f.sub === 0x01);\n if (declared && modeFrame) {\n const mode = modeFrame.bytes[3];\n const modeOpt = declared.modes.find(o => o && o.value === mode);\n if (modeOpt) {\n const group = declared.levels.find(o => o && o.name === modeOpt.name && Array.isArray(o.options));\n let modeValue = 0;\n if (group && levelFrame) {\n const level = levelFrame.bytes[3];\n if (group.options!.some(o => o && o.value === level)) {\n modeValue = level;\n }\n }\n out.push({\n type: GOVEE_CAP_TYPE.WORK_MODE,\n instance: \"workMode\",\n state: { value: { workMode: mode, modeValue } },\n });\n }\n }\n const sensors = frames.find(f => f.fn === 0x19 && f.sub === 0x00);\n if (sensors) {\n const filter = sensors.bytes[7];\n if (filter <= 100 && device.capabilities.some(c => c.instance === \"filterLifeTime\")) {\n out.push({ type: GOVEE_CAP_TYPE.PROPERTY, instance: \"filterLifeTime\", state: { value: filter } });\n }\n }\n return out;\n};\n\n/** One decoder per measured device family. A SKU not listed here gets nothing from its push. */\nconst DECODERS: Readonly<Record<string, FrameDecoder>> = {\n H7127: purifierH7127,\n};\n\n/**\n * Decode an appliance's own status push into the capability shape the cloud\n * pipeline already understands (`applyCloudCapabilities`). Local first: the\n * device says what it does; the cloud read is only the seed at start.\n *\n * @param device The device the packet belongs to\n * @param opCommand `op.command` of the status packet (base64 frames), unchecked\n */\nexport function decodeApplianceFrames(device: GoveeDevice, opCommand: unknown): CloudStateCapability[] {\n const decoder = DECODERS[device.sku];\n if (!decoder || !Array.isArray(opCommand)) {\n return [];\n }\n const frames = opCommand.map(parseFrame).filter((f): f is Frame => f !== null);\n return frames.length > 0 ? decoder(frames, device) : [];\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { GOVEE_CAP_TYPE } from \"./govee-constants\";\nimport type { CapabilityOption, CloudStateCapability, GoveeDevice } from \"./types\";\n\n/**\n * A 20-byte status frame from an appliance's own status push: `aa <fn> <sub>\n * <payload\u2026> <xor>` \u2014 Govee's BLE notification format, relayed verbatim in\n * the account broker's `op.command`. The XOR over bytes 0\u201318 sits in byte 19;\n * measured on 794 of 794 `aa` frames of two H7127 exports (issue #47).\n */\ninterface Frame {\n fn: number;\n sub: number;\n bytes: Buffer;\n}\n\nfunction parseFrame(raw: unknown): Frame | null {\n if (typeof raw !== \"string\" || raw.length === 0) {\n return null;\n }\n const bytes = Buffer.from(raw, \"base64\");\n if (bytes.length !== 20 || bytes[0] !== 0xaa) {\n return null;\n }\n let xor = 0;\n for (let i = 0; i < 19; i++) {\n xor ^= bytes[i];\n }\n if (xor !== bytes[19]) {\n return null;\n }\n return { fn: bytes[1], sub: bytes[2], bytes };\n}\n\ntype FrameDecoder = (frames: Frame[], device: GoveeDevice) => CloudStateCapability[];\n\n/**\n * The `workMode` STRUCT capability the device declared, if any.\n *\n * @param device The device whose declared capabilities are searched\n */\nfunction declaredWorkMode(device: GoveeDevice): { modes: CapabilityOption[]; levels: CapabilityOption[] } | null {\n const cap = device.capabilities.find(c => c.type === GOVEE_CAP_TYPE.WORK_MODE && c.instance === \"workMode\");\n const fields = cap?.parameters?.fields ?? [];\n const modes = fields.find(f => f && f.fieldName === \"workMode\")?.options;\n const levels = fields.find(f => f && f.fieldName === \"modeValue\")?.options;\n return Array.isArray(modes) && Array.isArray(levels) ? { modes, levels } : null;\n}\n\n/**\n * H7127 air purifier (issue #47; spec \u00A711). Measured frames:\n * aa 05 00 <mode> work mode (declared option value)\n * aa 05 01 <level> last manual level (declared gearMode option value)\n * aa 19 00 ff ff <aq> 00 <filter> filter life in % (byte 7 \u2014 two values against\n * two references); byte 5 is probably air quality\n * but was measured once and never seen changing,\n * so it is NOT decoded \u2014 the cloud seed carries it.\n * The level is reported only for a mode that declares levels \u2014 the same shape\n * the cloud state read returns (`{workMode: 2, modeValue: 0}` for Custom).\n *\n * @param frames The checksum-valid frames of one status packet\n * @param device The device the packet belongs to (declared capabilities)\n */\nconst purifierH7127: FrameDecoder = (frames, device) => {\n const out: CloudStateCapability[] = [];\n const declared = declaredWorkMode(device);\n const modeFrame = frames.find(f => f.fn === 0x05 && f.sub === 0x00);\n const levelFrame = frames.find(f => f.fn === 0x05 && f.sub === 0x01);\n if (declared && modeFrame) {\n const mode = modeFrame.bytes[3];\n const modeOpt = declared.modes.find(o => o && o.value === mode);\n if (modeOpt) {\n const group = declared.levels.find(o => o && o.name === modeOpt.name && Array.isArray(o.options));\n let modeValue = 0;\n if (group && levelFrame) {\n const level = levelFrame.bytes[3];\n if (group.options!.some(o => o && o.value === level)) {\n modeValue = level;\n }\n }\n out.push({\n type: GOVEE_CAP_TYPE.WORK_MODE,\n instance: \"workMode\",\n state: { value: { workMode: mode, modeValue } },\n });\n }\n }\n const sensors = frames.find(f => f.fn === 0x19 && f.sub === 0x00);\n if (sensors) {\n const filter = sensors.bytes[7];\n if (filter <= 100 && device.capabilities.some(c => c.instance === \"filterLifeTime\")) {\n out.push({ type: GOVEE_CAP_TYPE.PROPERTY, instance: \"filterLifeTime\", state: { value: filter } });\n }\n }\n return out;\n};\n\n/**\n * H1741 battery table lamp (issue #50). Measured frame:\n * aa 42 <b> battery level in % (bits 0\u20136)\n * Four reports against the app's own battery icon: 0x30 = two of three bars (2026-09-23), 0x50 right\n * after a full charge, 0x20 one bar shortly before the battery saver (2026-09-25/26). Govee reports a\n * full battery as 80\u201384, not 100 \u2014 the value is shown as it comes. Bit 7 was set only in the minutes\n * before the lamp was unplugged (0xd4); that it means \"charging\" is not measured, so it is masked and\n * not decoded. `aa 42` exists on mains devices too (H1310 `aa4200`, H70B1 `aa4202`) \u2014 hence per SKU.\n *\n * @param frames The checksum-valid frames of one status packet\n * @returns the battery reading, or nothing\n */\nconst lampH1741: FrameDecoder = frames => {\n const frame = frames.find(f => f.fn === 0x42);\n if (!frame) {\n return [];\n }\n const level = frame.sub & 0x7f;\n return level <= 100 ? [{ type: GOVEE_CAP_TYPE.PROPERTY, instance: \"battery\", state: { value: level } }] : [];\n};\n\n/** One decoder per measured device family. A SKU not listed here gets nothing from its push. */\nconst DECODERS: Readonly<Record<string, FrameDecoder>> = {\n H7127: purifierH7127,\n H1741: lampH1741,\n};\n\n/**\n * Decode an appliance's own status push into the capability shape the cloud\n * pipeline already understands (`applyCloudCapabilities`). Local first: the\n * device says what it does; the cloud read is only the seed at start.\n *\n * @param device The device the packet belongs to\n * @param opCommand `op.command` of the status packet (base64 frames), unchecked\n */\nexport function decodeApplianceFrames(device: GoveeDevice, opCommand: unknown): CloudStateCapability[] {\n const decoder = DECODERS[device.sku];\n if (!decoder || !Array.isArray(opCommand)) {\n return [];\n }\n const frames = opCommand.map(parseFrame).filter((f): f is Frame => f !== null);\n return frames.length > 0 ? decoder(frames, device) : [];\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1099,7 +1099,7 @@ function planCloudCapabilityWrites(caps, hasLanIp, lanStateIds, declared) {
1099
1099
  return writes;
1100
1100
  }
1101
1101
  function mapCloudStateValues(cap, declared) {
1102
- var _a, _b, _c, _d, _e, _f;
1102
+ var _a, _b, _c, _d, _e;
1103
1103
  const primary = mapCloudStateValue(cap);
1104
1104
  if (!primary) {
1105
1105
  return [];
@@ -1120,9 +1120,19 @@ function mapCloudStateValues(cap, declared) {
1120
1120
  if (level === null) {
1121
1121
  return [primary];
1122
1122
  }
1123
- const levelOptions = (_f = (_e = (_d = (_c = declared == null ? void 0 : declared.find((c) => c.type === cap.type && c.instance === cap.instance)) == null ? void 0 : _c.parameters) == null ? void 0 : _d.fields) == null ? void 0 : _e.find((f) => f && f.fieldName === "modeValue")) == null ? void 0 : _f.options;
1124
- const asNumber = levelOptions !== void 0 && classifyModeLevels(levelOptions).kind === "number";
1125
- return [primary, { stateId: "mode_value", value: asNumber ? level : safeStringify(level) }];
1123
+ const kind = declared ? modeLevelKind(
1124
+ (_e = (_d = (_c = declared.find((c) => c.type === cap.type && c.instance === cap.instance)) == null ? void 0 : _c.parameters) == null ? void 0 : _d.fields) == null ? void 0 : _e.find((f) => f && f.fieldName === "modeValue")
1125
+ ) : "dropdown";
1126
+ if (kind === "none") {
1127
+ return [primary];
1128
+ }
1129
+ return [primary, { stateId: "mode_value", value: kind === "number" ? level : safeStringify(level) }];
1130
+ }
1131
+ function modeLevelKind(field) {
1132
+ if ((field == null ? void 0 : field.options) && field.options.length > 0) {
1133
+ return classifyModeLevels(field.options).kind;
1134
+ }
1135
+ return (field == null ? void 0 : field.range) ? "number" : "none";
1126
1136
  }
1127
1137
  const SCENE_DROPDOWN_RULES = [
1128
1138
  { id: "light_scene", cap: "lightScene", nameKey: "lightScene", channel: "scenes", source: (d) => d.scenes },