@homebridge-plugins/homebridge-govee 11.35.0 → 11.35.1-beta.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/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  All notable changes to `@homebridge-plugins/homebridge-govee` will be documented in this file.
4
4
 
5
+ ## v11.35.1 (Pending Release)
6
+
7
+ ### Changed
8
+
9
+ - fix: stop reading an h7173 startup frame as a kettle preset temperature (#1351)
10
+ - fix: call the kettle's first temperature slot the target, not preset 1 (#1337)
11
+ - feat: show how long the kettle will keep the water warm for (#1337)
12
+
5
13
  ## v11.35.0 (2026-08-09)
6
14
 
7
15
  ### Changed
@@ -75,6 +75,18 @@ function readingToCelsius(hexParts) {
75
75
  * Only these four have been seen, all with keep warm switched on. Anything else
76
76
  * falls through to the unknown-data report, which is how these were found.
77
77
  */
78
+ // The range a kettle preset can plausibly sit in. The lowest Govee kettles go
79
+ // is around 40°C for delicate teas, and water stops being water at 100°C - so
80
+ // 90°F to 212°F covers every real preset with room to spare, while excluding
81
+ // the 0 and 1 the H7173 puts in that byte (#1351).
82
+ const KETTLE_PRESET_MIN_F = 90
83
+ const KETTLE_PRESET_MAX_F = 212
84
+
85
+ // The app offers a keep warm of 30 minutes to 2 hours. A day is far past
86
+ // anything a kettle would offer, while leaving room for a model that allows
87
+ // more than the H7171 does.
88
+ const KETTLE_KEEP_WARM_MAX_MINS = 1440
89
+
78
90
  const HEATING_STATES = {
79
91
  '00': { label: 'idle', heating: false },
80
92
  '01': { label: 'heating', heating: true },
@@ -364,6 +376,55 @@ export default class extends GoveeDevice {
364
376
  this.accessory.log(`current status [${state.label}]`)
365
377
  }
366
378
 
379
+ /**
380
+ * Show how long the kettle will hold the water once it has boiled.
381
+ *
382
+ * An `aa 22 01` frame carries the keep warm duration twice over, as two
383
+ * sixteen-bit counts of minutes. The owner in #1337 proved that outright:
384
+ * with the app set to two hours the frame read `aa 22 01 00 78 00 78`, and
385
+ * seconds after they changed it to thirty minutes the kettle sent
386
+ * `aa 22 01 00 1e 00 1e`. 0x78 is 120 and 0x1e is 30.
387
+ *
388
+ * The frame's checksum did not move between those two, which fits: the two
389
+ * copies are equal, so whatever one contributes the other takes back out.
390
+ *
391
+ * What the second copy is for is still unknown - a remaining time alongside
392
+ * the configured one would explain it, but it has only ever been seen equal
393
+ * to the first. So a pair that disagrees is treated as a frame this plugin
394
+ * cannot read rather than quietly ignored, which is what would get it
395
+ * decoded.
396
+ *
397
+ * @param {string} hexString the whole frame, for the log if it cannot be read
398
+ * @param {string[]} hexParts the frame split into bytes
399
+ */
400
+ updateKeepWarmDuration(hexString, hexParts) {
401
+ const configured = hexToDecimal(`${getTwoItemPosition(hexParts, 4)}${getTwoItemPosition(hexParts, 5)}`)
402
+ const second = hexToDecimal(`${getTwoItemPosition(hexParts, 6)}${getTwoItemPosition(hexParts, 7)}`)
403
+
404
+ // A keep warm that runs for no time at all, or for more than a day, is not
405
+ // a duration - it means this model packs the frame differently
406
+ const plausible = Number.isFinite(configured)
407
+ && configured > 0
408
+ && configured <= KETTLE_KEEP_WARM_MAX_MINS
409
+
410
+ if (!plausible || configured !== second) {
411
+ logUnknownData(this.accessory, {
412
+ kind: 'status',
413
+ source: 'AWS',
414
+ raw: hexString,
415
+ hex: hexParts.join(''),
416
+ extra: { note: 'aa 22 01 frame that does not carry one keep warm duration twice' },
417
+ })
418
+ return
419
+ }
420
+
421
+ if (this.cacheKeepWarmMins === configured) {
422
+ return
423
+ }
424
+ this.cacheKeepWarmMins = configured
425
+ this.accessory.log(`keep warm is set to ${configured} minutes`)
426
+ }
427
+
367
428
  externalUpdate(params) {
368
429
  // Handle OpenAPI workMode
369
430
  if (params.workMode) {
@@ -383,15 +444,52 @@ export default class extends GoveeDevice {
383
444
 
384
445
  const deviceFunction = `${getTwoItemPosition(hexParts, 2)}${getTwoItemPosition(hexParts, 3)}`
385
446
 
386
- // On startup the kettle lists the temperature each of its presets is set
387
- // to, one frame per preset: aa 05 <slot> <degrees F>. That is a different
388
- // thing from aa 05 00 <mode> below, which is a mode change - the slot
389
- // byte is what tells them apart. Nothing here needs the values, so they
390
- // are logged for reference rather than reported as unrecognised (#1337)
447
+ // On startup the kettle lists the temperature held in each of its
448
+ // temperature slots, one frame per slot: aa 05 <slot> <degrees F>. That
449
+ // is a different thing from aa 05 00 <mode> below, which is a mode
450
+ // change - the slot byte is what tells them apart. Nothing here needs the
451
+ // values, so they are logged for reference rather than reported as
452
+ // unrecognised (#1337)
453
+ //
454
+ // Slot 01 is NOT a memory preset, it is the target the kettle is set to
455
+ // right now. Two runs from the owner in #1337 show it moving on its own:
456
+ // 212°F on 3 Aug, then 142°F on both 8 and 11 Aug, matching the `setTem`
457
+ // of 14200 carried in the same message, while slots 02-05 never budged.
458
+ // The memory presets are therefore slots 02-05, which is why the number
459
+ // printed below is one less than the slot.
460
+ //
461
+ // Those four are also not in the same order as the mode commands at the
462
+ // top of this file. The owner's app lists M1-M4 as black tea 212, green
463
+ // tea 180, oolong 195, coffee 205, which is the order slots 02-05 arrive
464
+ // in - whereas the switch commands number green tea 02, oolong 03, coffee
465
+ // 04 and black tea 05. So the slots follow the app's list rather than the
466
+ // mode ids, and no attempt is made here to name them.
391
467
  if (getTwoItemPosition(hexParts, 2) === '05' && getTwoItemPosition(hexParts, 3) !== '00') {
392
468
  const slot = Number.parseInt(getTwoItemPosition(hexParts, 3), 16)
393
469
  const tempInF = Number.parseInt(getTwoItemPosition(hexParts, 4), 16)
394
- const line = `preset ${slot} is set to ${tempInF}°F [${Math.round(((tempInF - 32) * 5) / 9)}°C]`
470
+
471
+ // ⚠️ The `aa 05 <slot> <degrees F>` reading came from ONE kettle, the
472
+ // H7171 in #1337. The H7173 sends the same two leading bytes with a
473
+ // fourth byte of 0 or 1, which is plainly not a temperature - and the
474
+ // plugin cheerfully announced "preset 1 is set to 0°F [-18°C]" (#1351).
475
+ //
476
+ // So the shape alone is not enough to claim a meaning. A preset a
477
+ // kettle could actually hold is the test; anything else is a frame this
478
+ // plugin cannot read yet, and reporting it as such is what gets it
479
+ // decoded rather than guessed at.
480
+ if (tempInF < KETTLE_PRESET_MIN_F || tempInF > KETTLE_PRESET_MAX_F) {
481
+ logUnknownData(this.accessory, {
482
+ kind: 'status',
483
+ source: 'AWS',
484
+ raw: hexString,
485
+ hex: hexParts.join(''),
486
+ extra: { note: 'aa 05 frame whose fourth byte is not a preset temperature' },
487
+ })
488
+ return
489
+ }
490
+
491
+ const label = slot === 1 ? 'target temperature' : `memory preset ${slot - 1}`
492
+ const line = `${label} is set to ${tempInF}°F [${Math.round(((tempInF - 32) * 5) / 9)}°C]`
395
493
  // Worth seeing in a normal log, but only once each. The kettle sends
396
494
  // these when it connects, and if a firmware ever repeats them on a
397
495
  // timer this keeps five lines from becoming five every few minutes
@@ -433,8 +531,11 @@ export default class extends GoveeDevice {
433
531
  }
434
532
  break
435
533
  }
534
+ case '2201': { // keep warm on, and for how long
535
+ this.updateKeepWarmDuration(hexString, hexParts)
536
+ break
537
+ }
436
538
  case '2200': // keep warm off
437
- case '2201': // keep warm on
438
539
  case '2300': // scheduled start off
439
540
  case '2301': { // scheduled start on
440
541
  break
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@homebridge-plugins/homebridge-govee",
3
3
  "alias": "Govee",
4
4
  "type": "module",
5
- "version": "11.35.0",
5
+ "version": "11.35.1-beta.1",
6
6
  "description": "Homebridge plugin to integrate Govee devices into HomeKit.",
7
7
  "author": {
8
8
  "name": "bwp91",