iobroker.govee-smart 2.35.1 → 2.35.2

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
@@ -103,6 +103,10 @@ This adapter's MQTT authentication and BLE-over-LAN (ptReal) protocol implementa
103
103
  ### **WORK IN PROGRESS**
104
104
  -->
105
105
 
106
+ ### 2.35.2 (2026-09-11)
107
+
108
+ - Fixed: A light that is unplugged no longer shows as switched on after a start — Govee's answer for a device it cannot reach carries the values of the last contact, and those are no longer written
109
+
106
110
  ### 2.35.1 (2026-09-11)
107
111
 
108
112
  - Fixed: The values Govee reports for a device at start no longer wait behind the loading of the scene libraries — on an installation with a dozen lights they arrived seven minutes after the start
@@ -144,10 +148,6 @@ This adapter's MQTT authentication and BLE-over-LAN (ptReal) protocol implementa
144
148
  - New: An experimental model is tried by enabling "experimental device support"; a diagnostics report from the Expert tab confirms it for everyone
145
149
  - Changed: The wiki's device list folds each device type into one block with its counts, so 602 entries stay readable
146
150
 
147
- ### 2.32.1 (2026-09-07)
148
-
149
- - Fixed: Your devices and their recorded history no longer disappear from the object tree when the Govee cloud cannot be reached at startup
150
-
151
151
  [Older changelogs can be found there](CHANGELOG_OLD.md)
152
152
 
153
153
  ## Support
@@ -19,12 +19,22 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
19
19
  var cloud_state_loader_exports = {};
20
20
  __export(cloud_state_loader_exports, {
21
21
  applyCloudCapabilities: () => applyCloudCapabilities,
22
+ cloudReportsOffline: () => cloudReportsOffline,
22
23
  loadCloudStates: () => loadCloudStates
23
24
  });
24
25
  module.exports = __toCommonJS(cloud_state_loader_exports);
25
26
  var import_capability_mapper = require("../capability-mapper");
27
+ var import_govee_constants = require("../govee-constants");
26
28
  var import_rate_limiter = require("../rate-limiter");
27
29
  var import_types = require("../types");
30
+ function cloudReportsOffline(caps) {
31
+ return caps.some(
32
+ (c) => {
33
+ var _a;
34
+ return c && typeof c.type === "string" && (c.type === import_govee_constants.GOVEE_CAP_TYPE.ONLINE || c.type === "online") && ((_a = c.state) == null ? void 0 : _a.value) === false;
35
+ }
36
+ );
37
+ }
28
38
  async function loadCloudStates(adapter, only) {
29
39
  if (!adapter.cloudClient || !adapter.deviceManager || !adapter.stateManager) {
30
40
  return;
@@ -39,6 +49,12 @@ async function loadCloudStates(adapter, only) {
39
49
  try {
40
50
  const caps = await adapter.cloudClient.getDeviceState(device.sku, device.deviceId);
41
51
  (_a = adapter.deviceManager) == null ? void 0 : _a.applyCloudStateOnline(device, caps);
52
+ if (cloudReportsOffline(caps)) {
53
+ adapter.log.debug(
54
+ `Cloud state for ${(0, import_types.deviceLabel)(device)}: Govee reports the device offline \u2014 its remembered values are not written`
55
+ );
56
+ return;
57
+ }
42
58
  const prefix = adapter.stateManager.devicePrefix(device);
43
59
  const writes = [];
44
60
  for (const mapped of caps.flatMap((cap) => (0, import_capability_mapper.mapCloudStateValues)(cap, device.capabilities))) {
@@ -96,6 +112,7 @@ async function applyCloudCapabilities(adapter, device, caps) {
96
112
  // Annotate the CommonJS export names for ESM import in node:
97
113
  0 && (module.exports = {
98
114
  applyCloudCapabilities,
115
+ cloudReportsOffline,
99
116
  loadCloudStates
100
117
  });
101
118
  //# sourceMappingURL=cloud-state-loader.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/handlers/cloud-state-loader.ts"],
4
- "sourcesContent": ["import { LAN_STATE_IDS, mapCloudStateValues, planCloudCapabilityWrites } from \"../capability-mapper\";\nimport type { DeviceManager } from \"../device-manager\";\nimport type { GoveeCloudClient } from \"../govee-cloud-client\";\nimport { applianceBudget, type RateLimiter } from \"../rate-limiter\";\nimport type { StateManager } from \"../state-manager\";\nimport { deviceLabel, logRejected, type CloudStateCapability, type GoveeDevice } from \"../types\";\n\n/**\n * Adapter surface required by the cloud-state-loader helpers. Loose\n * `setState` for utils.Adapter structural matching.\n */\nexport interface CloudStateLoaderAdapter {\n readonly log: ioBroker.Logger;\n readonly cloudClient: GoveeCloudClient | null;\n readonly deviceManager: DeviceManager | null;\n readonly stateManager: StateManager | null;\n readonly rateLimiter: RateLimiter | null;\n setState(id: string, state: ioBroker.SettableState | ioBroker.StateValue): Promise<unknown>;\n}\n\n/**\n * Load current state for Cloud devices and populate state values.\n * Called after the initial Cloud device list load, on Cloud recovery,\n * and (scoped via `only`) by the per-device refresh_cloud button.\n *\n * LAN-first: never overwrite LAN states with Cloud values. For\n * LAN-capable devices, the LAN state IDs are filtered out \u2014 Cloud only\n * fills the gaps the LAN client doesn't cover.\n *\n * Every /device/state call runs through the RateLimiter (background\n * priority, same as scene loads): a fleet-sized burst would otherwise\n * blow the per-minute safety budget and bypass the daily accounting \u2014\n * including the 100/day appliance budget. On an exhausted budget the\n * calls queue and the values land when budget frees up.\n *\n * @param adapter Adapter surface\n * @param only When set, load only this device (per-device refresh)\n */\nexport async function loadCloudStates(adapter: CloudStateLoaderAdapter, only?: GoveeDevice): Promise<void> {\n if (!adapter.cloudClient || !adapter.deviceManager || !adapter.stateManager) {\n return;\n }\n\n const targets = adapter.deviceManager\n .getDevices()\n .filter(d => d.channels.cloud && d.capabilities.length > 0 && (!only || d === only));\n\n for (const device of targets) {\n const loadOne = async (): Promise<void> => {\n if (!adapter.cloudClient || !adapter.stateManager) {\n return;\n }\n try {\n const caps = await adapter.cloudClient.getDeviceState(device.sku, device.deviceId);\n // Govee's own reachability rides along in this response and used to be\n // discarded \u2014 the value translator has no `online` branch. For a device\n // with no local API this is the ONLY evidence there is, so without it\n // such a device could never be shown as reachable at all.\n adapter.deviceManager?.applyCloudStateOnline(device, caps);\n const prefix = adapter.stateManager.devicePrefix(device);\n\n const writes: Promise<unknown>[] = [];\n // One capability can carry two datapoints (work_mode \u2192 mode + level),\n // so the list is flattened first and the LAN-shadow rule below applies\n // per RESULT \u2014 same shape and same nesting level as before.\n for (const mapped of caps.flatMap(cap => mapCloudStateValues(cap, device.capabilities))) {\n if (device.lanIp && LAN_STATE_IDS.has(mapped.stateId)) {\n continue;\n }\n const statePath = adapter.stateManager.resolveStatePath(prefix, mapped.stateId);\n // Fire-and-forget \u2014 States are created before loadCloudStates runs;\n // a rejection here means the state was deleted out-of-band and\n // can be safely ignored.\n writes.push(\n adapter\n .setState(statePath, { val: mapped.value, ack: true })\n .catch(logRejected(adapter.log, `write ${statePath}`)),\n );\n }\n await Promise.all(writes);\n adapter.log.debug(`Cloud state loaded for ${deviceLabel(device)}`);\n } catch (e) {\n // v2.9.1 \u2014 record failure with HTTP status (and HttpError.responseBody\n // when available) so the diag JSON shows why state-load failed instead\n // of just \"could not load\". Previously this catch was silent \u2014 Class\n // C2 of the v2.9.1 diag-coverage audit.\n if (adapter.deviceManager) {\n const status =\n e && typeof e === \"object\" && \"statusCode\" in e ? (e as { statusCode?: number }).statusCode : undefined;\n adapter.deviceManager\n .getDiagnostics()\n .recordApiFailure(device.deviceId, \"/router/api/v1/device/state\", e, status);\n }\n adapter.log.debug(`Could not load Cloud state for ${deviceLabel(device)}`);\n }\n };\n if (adapter.rateLimiter) {\n // With the device's allowance \u2014 the doc comment above has always claimed\n // these calls are covered by the 100/day appliance budget, and until now\n // none of them were: no budget was passed, so an appliance's state reads\n // only ever counted against the account-wide limit.\n // Status tier (1), not the scene-library tier (2): at start the library\n // loads of every light are already queued (five calls per light, eight\n // calls a minute), and a read queued BEHIND them at the same tier came\n // seven minutes after the start on a 12-device installation (measured\n // 2026-09-11, first start on 2.35.0) \u2014 the values a user looks at first\n // arrived last. A priority-1 call overtakes the queued tier-2 loads.\n await adapter.rateLimiter.tryExecute(loadOne, 1, applianceBudget(device));\n } else {\n await loadOne();\n }\n }\n\n if (targets.length > 0) {\n adapter.log.debug(`Cloud state load dispatched for ${targets.length} device(s) (rate-limited)`);\n }\n}\n\n/**\n * Apply a list of synthesized Cloud-state capabilities to a single device \u2014\n * the App-API poll and OpenAPI-MQTT events both use this path so their\n * values flow through the same `mapCloudStateValue` pipeline that polled\n * Cloud states use.\n *\n * App-API and OpenAPI-MQTT deliver state IDs (battery, temperature,\n * humidity, lack_water, \u2026) that the Cloud-capability pipeline doesn't\n * declare for sensor/appliance SKUs \u2014 the state objects therefore don't\n * exist yet on first write. ensureSyntheticStateObject creates them\n * lazily with the right channel + role + unit.\n *\n */\nexport async function applyCloudCapabilities(\n adapter: CloudStateLoaderAdapter,\n device: GoveeDevice,\n caps: CloudStateCapability[],\n): Promise<void> {\n if (!adapter.stateManager) {\n return;\n }\n const prefix = adapter.stateManager.devicePrefix(device);\n const planned = planCloudCapabilityWrites(caps, Boolean(device.lanIp), LAN_STATE_IDS, device.capabilities);\n for (const mapped of planned) {\n await adapter.stateManager.ensureSyntheticStateObject(prefix, mapped.stateId);\n // v2.9.1 \u2014 mirror appliance/sensor values into device.state so the diag-\n // export `state` field is honest about non-Light runtime state. Without\n // this, `state` only ever held Light fields (power/brightness/color/\n // colorTemperature/scene) because handleLanStatus/handleMqttStatus were\n // the only writers \u2014 App-API + OpenAPI-MQTT routed straight to setState\n // without touching the in-memory device. Diag-export then showed an\n // empty `state: {online: ?}` for sensors / appliances even though the\n // real values lived in the state tree.\n if (mapped.value !== null && mapped.value !== undefined) {\n (device.state as Record<string, unknown>)[mapped.stateId] = mapped.value;\n }\n }\n const writes = planned.map(mapped => {\n const statePath = adapter.stateManager!.resolveStatePath(prefix, mapped.stateId);\n return adapter\n .setState(statePath, { val: mapped.value, ack: true })\n .catch(logRejected(adapter.log, `write ${statePath}`));\n });\n await Promise.all(writes);\n\n // Remove a phantom `humidity` datapoint on a temp-only thermometer:\n // Govee reports `hum:0` for devices without a humidity sensor (e.g. H5109),\n // which older versions turned into a permanent `humidity=0` state\n // (#31 inspee). A device that declares sensorTemperature but not\n // sensorHumidity has no humidity sensor \u2014 drop the orphan once. A real\n // hygrometer (sensorHumidity capability) is never touched.\n const hasTempCap = device.capabilities.some(c => c.instance === \"sensorTemperature\");\n const hasHumidityCap = device.capabilities.some(c => c.instance === \"sensorHumidity\");\n if (hasTempCap && !hasHumidityCap) {\n await adapter.stateManager.removeSyntheticStateOnce(prefix, \"humidity\");\n }\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,+BAA8E;AAG9E,0BAAkD;AAElD,mBAAsF;AAiCtF,eAAsB,gBAAgB,SAAkC,MAAmC;AACzG,MAAI,CAAC,QAAQ,eAAe,CAAC,QAAQ,iBAAiB,CAAC,QAAQ,cAAc;AAC3E;AAAA,EACF;AAEA,QAAM,UAAU,QAAQ,cACrB,WAAW,EACX,OAAO,OAAK,EAAE,SAAS,SAAS,EAAE,aAAa,SAAS,MAAM,CAAC,QAAQ,MAAM,KAAK;AAErF,aAAW,UAAU,SAAS;AAC5B,UAAM,UAAU,YAA2B;AAhD/C;AAiDM,UAAI,CAAC,QAAQ,eAAe,CAAC,QAAQ,cAAc;AACjD;AAAA,MACF;AACA,UAAI;AACF,cAAM,OAAO,MAAM,QAAQ,YAAY,eAAe,OAAO,KAAK,OAAO,QAAQ;AAKjF,sBAAQ,kBAAR,mBAAuB,sBAAsB,QAAQ;AACrD,cAAM,SAAS,QAAQ,aAAa,aAAa,MAAM;AAEvD,cAAM,SAA6B,CAAC;AAIpC,mBAAW,UAAU,KAAK,QAAQ,aAAO,8CAAoB,KAAK,OAAO,YAAY,CAAC,GAAG;AACvF,cAAI,OAAO,SAAS,uCAAc,IAAI,OAAO,OAAO,GAAG;AACrD;AAAA,UACF;AACA,gBAAM,YAAY,QAAQ,aAAa,iBAAiB,QAAQ,OAAO,OAAO;AAI9E,iBAAO;AAAA,YACL,QACG,SAAS,WAAW,EAAE,KAAK,OAAO,OAAO,KAAK,KAAK,CAAC,EACpD,UAAM,0BAAY,QAAQ,KAAK,SAAS,SAAS,EAAE,CAAC;AAAA,UACzD;AAAA,QACF;AACA,cAAM,QAAQ,IAAI,MAAM;AACxB,gBAAQ,IAAI,MAAM,8BAA0B,0BAAY,MAAM,CAAC,EAAE;AAAA,MACnE,SAAS,GAAG;AAKV,YAAI,QAAQ,eAAe;AACzB,gBAAM,SACJ,KAAK,OAAO,MAAM,YAAY,gBAAgB,IAAK,EAA8B,aAAa;AAChG,kBAAQ,cACL,eAAe,EACf,iBAAiB,OAAO,UAAU,+BAA+B,GAAG,MAAM;AAAA,QAC/E;AACA,gBAAQ,IAAI,MAAM,sCAAkC,0BAAY,MAAM,CAAC,EAAE;AAAA,MAC3E;AAAA,IACF;AACA,QAAI,QAAQ,aAAa;AAWvB,YAAM,QAAQ,YAAY,WAAW,SAAS,OAAG,qCAAgB,MAAM,CAAC;AAAA,IAC1E,OAAO;AACL,YAAM,QAAQ;AAAA,IAChB;AAAA,EACF;AAEA,MAAI,QAAQ,SAAS,GAAG;AACtB,YAAQ,IAAI,MAAM,mCAAmC,QAAQ,MAAM,2BAA2B;AAAA,EAChG;AACF;AAeA,eAAsB,uBACpB,SACA,QACA,MACe;AACf,MAAI,CAAC,QAAQ,cAAc;AACzB;AAAA,EACF;AACA,QAAM,SAAS,QAAQ,aAAa,aAAa,MAAM;AACvD,QAAM,cAAU,oDAA0B,MAAM,QAAQ,OAAO,KAAK,GAAG,wCAAe,OAAO,YAAY;AACzG,aAAW,UAAU,SAAS;AAC5B,UAAM,QAAQ,aAAa,2BAA2B,QAAQ,OAAO,OAAO;AAS5E,QAAI,OAAO,UAAU,QAAQ,OAAO,UAAU,QAAW;AACvD,MAAC,OAAO,MAAkC,OAAO,OAAO,IAAI,OAAO;AAAA,IACrE;AAAA,EACF;AACA,QAAM,SAAS,QAAQ,IAAI,YAAU;AACnC,UAAM,YAAY,QAAQ,aAAc,iBAAiB,QAAQ,OAAO,OAAO;AAC/E,WAAO,QACJ,SAAS,WAAW,EAAE,KAAK,OAAO,OAAO,KAAK,KAAK,CAAC,EACpD,UAAM,0BAAY,QAAQ,KAAK,SAAS,SAAS,EAAE,CAAC;AAAA,EACzD,CAAC;AACD,QAAM,QAAQ,IAAI,MAAM;AAQxB,QAAM,aAAa,OAAO,aAAa,KAAK,OAAK,EAAE,aAAa,mBAAmB;AACnF,QAAM,iBAAiB,OAAO,aAAa,KAAK,OAAK,EAAE,aAAa,gBAAgB;AACpF,MAAI,cAAc,CAAC,gBAAgB;AACjC,UAAM,QAAQ,aAAa,yBAAyB,QAAQ,UAAU;AAAA,EACxE;AACF;",
4
+ "sourcesContent": ["import { LAN_STATE_IDS, mapCloudStateValues, planCloudCapabilityWrites } from \"../capability-mapper\";\nimport type { DeviceManager } from \"../device-manager\";\nimport type { GoveeCloudClient } from \"../govee-cloud-client\";\nimport { GOVEE_CAP_TYPE } from \"../govee-constants\";\nimport { applianceBudget, type RateLimiter } from \"../rate-limiter\";\nimport type { StateManager } from \"../state-manager\";\nimport { deviceLabel, logRejected, type CloudStateCapability, type GoveeDevice } from \"../types\";\n\n/**\n * Adapter surface required by the cloud-state-loader helpers. Loose\n * `setState` for utils.Adapter structural matching.\n */\nexport interface CloudStateLoaderAdapter {\n readonly log: ioBroker.Logger;\n readonly cloudClient: GoveeCloudClient | null;\n readonly deviceManager: DeviceManager | null;\n readonly stateManager: StateManager | null;\n readonly rateLimiter: RateLimiter | null;\n setState(id: string, state: ioBroker.SettableState | ioBroker.StateValue): Promise<unknown>;\n}\n\n/**\n * Whether Govee's state answer says the device is offline \u2014 the `online`\n * capability with an explicit `false`. Absent or non-boolean counts as \"no\n * statement\", not as offline.\n *\n * @param caps The capabilities of one `/device/state` answer\n */\nexport function cloudReportsOffline(caps: CloudStateCapability[]): boolean {\n return caps.some(\n c =>\n c &&\n typeof c.type === \"string\" &&\n (c.type === GOVEE_CAP_TYPE.ONLINE || c.type === \"online\") &&\n c.state?.value === false,\n );\n}\n\n/**\n * Load current state for Cloud devices and populate state values.\n * Called after the initial Cloud device list load, on Cloud recovery,\n * and (scoped via `only`) by the per-device refresh_cloud button.\n *\n * LAN-first: never overwrite LAN states with Cloud values. For\n * LAN-capable devices, the LAN state IDs are filtered out \u2014 Cloud only\n * fills the gaps the LAN client doesn't cover.\n *\n * Every /device/state call runs through the RateLimiter (background\n * priority, same as scene loads): a fleet-sized burst would otherwise\n * blow the per-minute safety budget and bypass the daily accounting \u2014\n * including the 100/day appliance budget. On an exhausted budget the\n * calls queue and the values land when budget frees up.\n *\n * @param adapter Adapter surface\n * @param only When set, load only this device (per-device refresh)\n */\nexport async function loadCloudStates(adapter: CloudStateLoaderAdapter, only?: GoveeDevice): Promise<void> {\n if (!adapter.cloudClient || !adapter.deviceManager || !adapter.stateManager) {\n return;\n }\n\n const targets = adapter.deviceManager\n .getDevices()\n .filter(d => d.channels.cloud && d.capabilities.length > 0 && (!only || d === only));\n\n for (const device of targets) {\n const loadOne = async (): Promise<void> => {\n if (!adapter.cloudClient || !adapter.stateManager) {\n return;\n }\n try {\n const caps = await adapter.cloudClient.getDeviceState(device.sku, device.deviceId);\n // Govee's own reachability rides along in this response and used to be\n // discarded \u2014 the value translator has no `online` branch. For a device\n // with no local API this is the ONLY evidence there is, so without it\n // such a device could never be shown as reachable at all.\n adapter.deviceManager?.applyCloudStateOnline(device, caps);\n // Govee's memory of a device it cannot reach is not the device's state:\n // for an unplugged light the same answer carried `online: false` AND\n // `powerSwitch: 1`, `brightness: 100` (krobi's H70C5, unplugged for a\n // week, measured on the first 2.35.0 start) \u2014 the values of the last\n // contact. Written, they showed an unplugged light as switched on. The\n // online evidence above is applied; the remembered values are not.\n if (cloudReportsOffline(caps)) {\n adapter.log.debug(\n `Cloud state for ${deviceLabel(device)}: Govee reports the device offline \u2014 its remembered values are not written`,\n );\n return;\n }\n const prefix = adapter.stateManager.devicePrefix(device);\n\n const writes: Promise<unknown>[] = [];\n // One capability can carry two datapoints (work_mode \u2192 mode + level),\n // so the list is flattened first and the LAN-shadow rule below applies\n // per RESULT \u2014 same shape and same nesting level as before.\n for (const mapped of caps.flatMap(cap => mapCloudStateValues(cap, device.capabilities))) {\n if (device.lanIp && LAN_STATE_IDS.has(mapped.stateId)) {\n continue;\n }\n const statePath = adapter.stateManager.resolveStatePath(prefix, mapped.stateId);\n // Fire-and-forget \u2014 States are created before loadCloudStates runs;\n // a rejection here means the state was deleted out-of-band and\n // can be safely ignored.\n writes.push(\n adapter\n .setState(statePath, { val: mapped.value, ack: true })\n .catch(logRejected(adapter.log, `write ${statePath}`)),\n );\n }\n await Promise.all(writes);\n adapter.log.debug(`Cloud state loaded for ${deviceLabel(device)}`);\n } catch (e) {\n // v2.9.1 \u2014 record failure with HTTP status (and HttpError.responseBody\n // when available) so the diag JSON shows why state-load failed instead\n // of just \"could not load\". Previously this catch was silent \u2014 Class\n // C2 of the v2.9.1 diag-coverage audit.\n if (adapter.deviceManager) {\n const status =\n e && typeof e === \"object\" && \"statusCode\" in e ? (e as { statusCode?: number }).statusCode : undefined;\n adapter.deviceManager\n .getDiagnostics()\n .recordApiFailure(device.deviceId, \"/router/api/v1/device/state\", e, status);\n }\n adapter.log.debug(`Could not load Cloud state for ${deviceLabel(device)}`);\n }\n };\n if (adapter.rateLimiter) {\n // With the device's allowance \u2014 the doc comment above has always claimed\n // these calls are covered by the 100/day appliance budget, and until now\n // none of them were: no budget was passed, so an appliance's state reads\n // only ever counted against the account-wide limit.\n // Status tier (1), not the scene-library tier (2): at start the library\n // loads of every light are already queued (five calls per light, eight\n // calls a minute), and a read queued BEHIND them at the same tier came\n // seven minutes after the start on a 12-device installation (measured\n // 2026-09-11, first start on 2.35.0) \u2014 the values a user looks at first\n // arrived last. A priority-1 call overtakes the queued tier-2 loads.\n await adapter.rateLimiter.tryExecute(loadOne, 1, applianceBudget(device));\n } else {\n await loadOne();\n }\n }\n\n if (targets.length > 0) {\n adapter.log.debug(`Cloud state load dispatched for ${targets.length} device(s) (rate-limited)`);\n }\n}\n\n/**\n * Apply a list of synthesized Cloud-state capabilities to a single device \u2014\n * the App-API poll and OpenAPI-MQTT events both use this path so their\n * values flow through the same `mapCloudStateValue` pipeline that polled\n * Cloud states use.\n *\n * App-API and OpenAPI-MQTT deliver state IDs (battery, temperature,\n * humidity, lack_water, \u2026) that the Cloud-capability pipeline doesn't\n * declare for sensor/appliance SKUs \u2014 the state objects therefore don't\n * exist yet on first write. ensureSyntheticStateObject creates them\n * lazily with the right channel + role + unit.\n *\n */\nexport async function applyCloudCapabilities(\n adapter: CloudStateLoaderAdapter,\n device: GoveeDevice,\n caps: CloudStateCapability[],\n): Promise<void> {\n if (!adapter.stateManager) {\n return;\n }\n const prefix = adapter.stateManager.devicePrefix(device);\n const planned = planCloudCapabilityWrites(caps, Boolean(device.lanIp), LAN_STATE_IDS, device.capabilities);\n for (const mapped of planned) {\n await adapter.stateManager.ensureSyntheticStateObject(prefix, mapped.stateId);\n // v2.9.1 \u2014 mirror appliance/sensor values into device.state so the diag-\n // export `state` field is honest about non-Light runtime state. Without\n // this, `state` only ever held Light fields (power/brightness/color/\n // colorTemperature/scene) because handleLanStatus/handleMqttStatus were\n // the only writers \u2014 App-API + OpenAPI-MQTT routed straight to setState\n // without touching the in-memory device. Diag-export then showed an\n // empty `state: {online: ?}` for sensors / appliances even though the\n // real values lived in the state tree.\n if (mapped.value !== null && mapped.value !== undefined) {\n (device.state as Record<string, unknown>)[mapped.stateId] = mapped.value;\n }\n }\n const writes = planned.map(mapped => {\n const statePath = adapter.stateManager!.resolveStatePath(prefix, mapped.stateId);\n return adapter\n .setState(statePath, { val: mapped.value, ack: true })\n .catch(logRejected(adapter.log, `write ${statePath}`));\n });\n await Promise.all(writes);\n\n // Remove a phantom `humidity` datapoint on a temp-only thermometer:\n // Govee reports `hum:0` for devices without a humidity sensor (e.g. H5109),\n // which older versions turned into a permanent `humidity=0` state\n // (#31 inspee). A device that declares sensorTemperature but not\n // sensorHumidity has no humidity sensor \u2014 drop the orphan once. A real\n // hygrometer (sensorHumidity capability) is never touched.\n const hasTempCap = device.capabilities.some(c => c.instance === \"sensorTemperature\");\n const hasHumidityCap = device.capabilities.some(c => c.instance === \"sensorHumidity\");\n if (hasTempCap && !hasHumidityCap) {\n await adapter.stateManager.removeSyntheticStateOnce(prefix, \"humidity\");\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/io-package.json CHANGED
@@ -1,8 +1,21 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "govee-smart",
4
- "version": "2.35.1",
4
+ "version": "2.35.2",
5
5
  "news": {
6
+ "2.35.2": {
7
+ "en": "A light that is unplugged no longer shows as switched on after a start — Govee's answer for a device it cannot reach carries the values of the last contact, and those are no longer written.",
8
+ "de": "Eine ausgesteckte Lampe zeigt sich nach einem Start nicht mehr als eingeschaltet — Govees Antwort für ein unerreichbares Gerät trägt die Werte des letzten Kontakts, die nicht mehr geschrieben werden.",
9
+ "ru": "Отключённая от сети лампа после старта больше не показывается включённой — ответ Govee для недоступного устройства несёт значения последнего контакта, и они больше не записываются.",
10
+ "pt": "Uma luz desligada da tomada já não aparece como ligada após um arranque — a resposta da Govee para um aparelho inacessível traz os valores do último contacto, e esses já não são escritos.",
11
+ "nl": "Een lamp zonder stroom toont zich na een start niet meer als ingeschakeld — Govees antwoord voor een onbereikbaar apparaat draagt de waarden van het laatste contact, die niet meer worden geschreven.",
12
+ "fr": "Une lampe débranchée ne s'affiche plus comme allumée après un démarrage — la réponse de Govee pour un appareil injoignable porte les valeurs du dernier contact, qui ne sont plus écrites.",
13
+ "it": "Una luce scollegata non appare più accesa dopo un avvio — la risposta di Govee per un dispositivo irraggiungibile porta i valori dell'ultimo contatto, che non vengono più scritti.",
14
+ "es": "Una luz desenchufada ya no aparece encendida tras un inicio — la respuesta de Govee para un dispositivo inalcanzable trae los valores del último contacto, y esos ya no se escriben.",
15
+ "pl": "Odłączona od prądu lampa nie pokazuje się już po starcie jako włączona — odpowiedź Govee dla nieosiągalnego urządzenia niesie wartości ostatniego kontaktu, a te nie są już zapisywane.",
16
+ "uk": "Вимкнена з розетки лампа після старту більше не показується увімкненою — відповідь Govee для недосяжного пристрою несе значення останнього контакту, і вони більше не записуються.",
17
+ "zh-cn": "拔掉电源的灯在启动后不再显示为已开启——Govee 对无法访问的设备返回的是上次联系时的数值,这些数值不再被写入。"
18
+ },
6
19
  "2.35.1": {
7
20
  "en": "The values Govee reports for a device at start no longer wait behind the loading of the scene libraries — with a dozen lights they used to arrive seven minutes after the start.",
8
21
  "de": "Die Werte, die Govee beim Start für ein Gerät meldet, warten nicht mehr hinter dem Laden der Szenen-Bibliotheken — mit einem Dutzend Lampen kamen sie sieben Minuten nach dem Start.",
@@ -80,19 +93,6 @@
80
93
  "pl": "Urządzenia na koncie bez ani jednego światła są po restarcie znowu sterowalne.\nUrządzenie nie jest już pokazywane jako osiągalne, gdy Govee zgłosi je jako offline, a wskazanie pozostaje poprawne także przy samym kluczu API.\nNieudane polecenie w chmurze nie liczy się już jako wykonane, a punkty danych objaśniają się w 11 językach.",
81
94
  "uk": "Пристрої в обліковому записі без жодного світильника знову керуються після перезапуску.\nПристрій більше не показується доступним, щойно Govee повідомить про його від'єднання, і показ лишається правильним навіть з одним ключем API.\nНевдала хмарна команда більше не вважається виконаною, а точки даних пояснюються 11 мовами.",
82
95
  "zh-cn": "账号中没有任何灯具时,设备在重启后重新可控。\n一旦 Govee 报告设备离线,就不再显示为可达;只配置了 API 密钥的安装,显示同样保持正确。\n失败的云端指令不再算作已执行,数据点也会用 11 种语言自我说明。"
83
- },
84
- "2.31.1": {
85
- "en": "Fixed: when the adapter met an unknown device model, its log asked for a button that 2.31.0 had removed — it now points at the Expert tab",
86
- "de": "Behoben: Bei einem unbekannten Gerätemodell verwies das Protokoll auf einen Knopf, den 2.31.0 entfernt hatte — es nennt jetzt den Reiter „Experte“",
87
- "ru": "Исправлено: при неизвестной модели устройства журнал ссылался на кнопку, удалённую в 2.31.0 — теперь он указывает на вкладку «Эксперт»",
88
- "pt": "Corrigido: perante um modelo desconhecido, o registo pedia um botão removido na 2.31.0 — agora indica o separador Especialista",
89
- "nl": "Opgelost: bij een onbekend apparaatmodel verwees het logboek naar een knop die 2.31.0 had verwijderd — het noemt nu het tabblad Expert",
90
- "fr": "Corrigé : face à un modèle inconnu, le journal renvoyait à un bouton supprimé en 2.31.0 — il indique désormais l'onglet Expert",
91
- "it": "Corretto: con un modello sconosciuto il log rimandava a un pulsante rimosso nella 2.31.0 — ora indica la scheda Esperto",
92
- "es": "Corregido: ante un modelo desconocido, el registro pedía un botón eliminado en 2.31.0: ahora indica la pestaña Experto",
93
- "pl": "Naprawiono: przy nieznanym modelu dziennik odsyłał do przycisku usuniętego w 2.31.0 — teraz wskazuje zakładkę Ekspert",
94
- "uk": "Виправлено: для невідомої моделі журнал посилався на кнопку, вилучену в 2.31.0 — тепер він вказує на вкладку «Експерт»",
95
- "zh-cn": "修复:遇到未知设备型号时,日志要求按下 2.31.0 已移除的按钮 — 现在改为指向“专家”选项卡"
96
96
  }
97
97
  },
98
98
  "plugins": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.govee-smart",
3
- "version": "2.35.1",
3
+ "version": "2.35.2",
4
4
  "description": "Control Govee WiFi devices via LAN, MQTT and Cloud.",
5
5
  "author": {
6
6
  "name": "krobi",