@homebridge-plugins/homebridge-govee 11.32.2-beta.3 → 11.32.2-beta.5

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.
Files changed (67) hide show
  1. package/CHANGELOG.md +9 -1
  2. package/config.schema.json +1 -1
  3. package/lib/connection/ble.js +26 -1
  4. package/lib/device/dehumidifier-H7150.js +7 -1
  5. package/lib/device/dehumidifier-H7151.js +7 -1
  6. package/lib/device/dehumidifier-H7152.js +7 -1
  7. package/lib/device/diffuser-H7161.js +7 -1
  8. package/lib/device/diffuser-H7162.js +7 -1
  9. package/lib/device/fan-H1310.js +7 -1
  10. package/lib/device/fan-H1370.js +7 -1
  11. package/lib/device/fan-H7100.js +7 -1
  12. package/lib/device/fan-H7101.js +7 -1
  13. package/lib/device/fan-H7102.js +7 -1
  14. package/lib/device/fan-H7105.js +7 -1
  15. package/lib/device/fan-H7106.js +7 -1
  16. package/lib/device/fan-H7107.js +7 -1
  17. package/lib/device/fan-H7111.js +7 -1
  18. package/lib/device/fan-basic.js +143 -0
  19. package/lib/device/heater1a.js +44 -12
  20. package/lib/device/heater1b.js +14 -3
  21. package/lib/device/heater2.js +7 -1
  22. package/lib/device/humidifier-H7140.js +7 -1
  23. package/lib/device/humidifier-H7141.js +7 -1
  24. package/lib/device/humidifier-H7142.js +7 -1
  25. package/lib/device/humidifier-H7143.js +7 -1
  26. package/lib/device/humidifier-H7145.js +7 -1
  27. package/lib/device/humidifier-H7147.js +7 -1
  28. package/lib/device/humidifier-H7148.js +7 -1
  29. package/lib/device/humidifier-H7149.js +7 -1
  30. package/lib/device/humidifier-H714E.js +7 -1
  31. package/lib/device/humidifier-H7160.js +7 -1
  32. package/lib/device/ice-maker-H7172.js +7 -1
  33. package/lib/device/ice-maker-H717D.js +7 -1
  34. package/lib/device/ice-maker-H8120.js +7 -1
  35. package/lib/device/ice-maker-H8121.js +9 -3
  36. package/lib/device/ice-maker-H8122.js +9 -3
  37. package/lib/device/index.js +2 -0
  38. package/lib/device/kettle.js +7 -1
  39. package/lib/device/outlet-single.js +7 -1
  40. package/lib/device/purifier-H7120.js +7 -1
  41. package/lib/device/purifier-H7121.js +7 -1
  42. package/lib/device/purifier-H7122.js +7 -1
  43. package/lib/device/purifier-H7123.js +7 -1
  44. package/lib/device/purifier-H7124.js +7 -1
  45. package/lib/device/purifier-H7126.js +7 -1
  46. package/lib/device/purifier-H7127.js +7 -1
  47. package/lib/device/purifier-H7128.js +7 -1
  48. package/lib/device/purifier-H7129.js +7 -1
  49. package/lib/device/purifier-H712C.js +7 -1
  50. package/lib/device/sensor-button.js +6 -1
  51. package/lib/device/sensor-contact.js +6 -1
  52. package/lib/device/sensor-presence.js +7 -1
  53. package/lib/device/sensor-thermo4.js +13 -2
  54. package/lib/device/template.js +15 -2
  55. package/lib/platform.js +24 -5
  56. package/lib/utils/appliance-capabilities.js +45 -0
  57. package/lib/utils/appliance-capabilities.test.js +31 -0
  58. package/lib/utils/command-builder.js +4 -4
  59. package/lib/utils/constants.js +118 -6
  60. package/lib/utils/decode.js +4 -1
  61. package/lib/utils/decode.test.js +105 -0
  62. package/lib/utils/device-capabilities.js +62 -3
  63. package/lib/utils/device-capabilities.test.js +62 -0
  64. package/lib/utils/lang-en.js +0 -2
  65. package/lib/utils/report-unknown.js +156 -0
  66. package/lib/utils/report-unknown.test.js +187 -0
  67. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -9,7 +9,15 @@ All notable changes to `@homebridge-plugins/homebridge-govee` will be documented
9
9
  - fix: implement the H7107's oscillation toggle, which threw a not-implemented error when tapped (#1334)
10
10
  - fix: also send the H7107's oscillation command as the multiSync frame the fan itself uses, since the first shape only turned it off sometimes (#1334)
11
11
  - fix: send the H7107's oscillation-off command without the swing-range bytes, matching the zero-tailed frame the fan echoes back (#1334)
12
- - feat: add support for the H8121 and H8122 ice makers, with ice size derived from the govee app (pending owner confirmation)
12
+ - feat: add support for the H8121 and H8122 ice makers, with small, medium and large ice sizes (pending owner confirmation)
13
+ - fix: send the H6102 the colour command its firmware expects, since the previous one was accepted but ignored (#1332)
14
+ - fix: read the H5179 sensor's temperature from its own bytes, since it was picking up part of the humidity and reporting slightly wrong
15
+ - feat: log govee sensors whose bluetooth broadcast is not recognised, so unsupported models can be identified
16
+ - feat: report unrecognised device data in one line with the model, firmware and connection, so it can be sent in without follow-up questions
17
+ - feat: work out for itself whether a heater reports the room temperature, and add the H7137 and H713E
18
+ - feat: add support for 85 more light models, covering everything currently sold that was missing
19
+ - feat: add 24 more light models, three thermo-hygrometers and a leak detector
20
+ - feat: add the H7103 and H7112 fans and the H5085 plug, with unknown controls saying so instead of guessing
13
21
 
14
22
  ## v11.32.1 (2026-08-02)
15
23
 
@@ -1260,7 +1260,7 @@
1260
1260
  "tempReporting": {
1261
1261
  "type": "boolean",
1262
1262
  "title": "Temperature Reporting",
1263
- "description": "Enable this if your H7130 device reports ambient temperature.",
1263
+ "description": "Only needed to override the plugin. It works out on its own whether this heater reports the room temperature, and picks the right tile.",
1264
1264
  "condition": {
1265
1265
  "functionBody": "return (model.heaterDevices && model.heaterDevices[arrayIndices] && model.heaterDevices[arrayIndices].deviceId && model.heaterDevices[arrayIndices].deviceId.length === 23);"
1266
1266
  }
@@ -7,6 +7,7 @@ import { BLE_STAGE_TIMEOUTS, UUID } from '../utils/ble-protocol.js'
7
7
  import { decodeAny } from '../utils/decode.js'
8
8
  import { base64ToHex, generateCodeFromHexValues, hexToTwoItems, sleep } from '../utils/functions.js'
9
9
  import platformLang from '../utils/lang-en.js'
10
+ import { isFirstReport } from '../utils/report-unknown.js'
10
11
  import { isValidPeripheral } from '../utils/validation.js'
11
12
 
12
13
  const HYPHEN_REGEX = /-/g
@@ -120,8 +121,32 @@ export default class BLEConnection {
120
121
  try {
121
122
  const { uuid, address, rssi, advertisement } = peripheral
122
123
 
123
- // Skip if not a valid Govee sensor
124
+ // Skip if not a valid Govee sensor. Sensors whose broadcast the plugin
125
+ // cannot decode yet are logged rather than dropped in silence, since the
126
+ // hex is the one thing needed to add a model, and there is no other way
127
+ // for an owner to get hold of it.
124
128
  if (!isValidPeripheral(peripheral)) {
129
+ const unknownName = advertisement?.localName
130
+ if (unknownName && unknownName.toLowerCase().startsWith('govee')) {
131
+ const raw = advertisement.manufacturerData
132
+ ? advertisement.manufacturerData.toString('hex')
133
+ : 'no manufacturer data'
134
+ const line = `[BLE] unrecognised govee sensor - please include this line if you report it: ${JSON.stringify({
135
+ name: unknownName,
136
+ address,
137
+ source: 'BLE',
138
+ kind: 'broadcast',
139
+ raw,
140
+ })}`
141
+
142
+ // Say it once so it is actually noticed, then stay quiet about this
143
+ // sensor for the rest of the session - it rebroadcasts constantly.
144
+ if (isFirstReport(`ble|${address}|${raw}`)) {
145
+ this.log.warn(line)
146
+ } else {
147
+ this.log.debug(line)
148
+ }
149
+ }
125
150
  return
126
151
  }
127
152
 
@@ -5,6 +5,7 @@ import {
5
5
  parseError,
6
6
  } from '../utils/functions.js'
7
7
  import platformLang from '../utils/lang-en.js'
8
+ import { logUnknownData } from '../utils/report-unknown.js'
8
9
 
9
10
  /*
10
11
  H7150
@@ -206,7 +207,12 @@ export default class {
206
207
  break
207
208
  }
208
209
  default:
209
- this.accessory.logDebugWarn(`${platformLang.newScene}: [${command}] [${hexString}]`)
210
+ logUnknownData(this.accessory, {
211
+ kind: 'scene',
212
+ source: params.source,
213
+ raw: command,
214
+ hex: hexString,
215
+ })
210
216
  break
211
217
  }
212
218
  })
@@ -5,6 +5,7 @@ import {
5
5
  parseError,
6
6
  } from '../utils/functions.js'
7
7
  import platformLang from '../utils/lang-en.js'
8
+ import { logUnknownData } from '../utils/report-unknown.js'
8
9
 
9
10
  export default class {
10
11
  constructor(platform, accessory) {
@@ -181,7 +182,12 @@ export default class {
181
182
  break
182
183
  }
183
184
  default:
184
- this.accessory.logDebugWarn(`${platformLang.newScene}: [${command}] [${hexString}]`)
185
+ logUnknownData(this.accessory, {
186
+ kind: 'scene',
187
+ source: params.source,
188
+ raw: command,
189
+ hex: hexString,
190
+ })
185
191
  break
186
192
  }
187
193
  })
@@ -5,6 +5,7 @@ import {
5
5
  parseError,
6
6
  } from '../utils/functions.js'
7
7
  import platformLang from '../utils/lang-en.js'
8
+ import { logUnknownData } from '../utils/report-unknown.js'
8
9
 
9
10
  export default class {
10
11
  constructor(platform, accessory) {
@@ -172,7 +173,12 @@ export default class {
172
173
  break
173
174
  }
174
175
  default:
175
- this.accessory.logDebugWarn(`${platformLang.newScene}: [${command}] [${hexString}]`)
176
+ logUnknownData(this.accessory, {
177
+ kind: 'scene',
178
+ source: params.source,
179
+ raw: command,
180
+ hex: hexString,
181
+ })
176
182
  break
177
183
  }
178
184
  })
@@ -5,6 +5,7 @@ import {
5
5
  parseError,
6
6
  } from '../utils/functions.js'
7
7
  import platformLang from '../utils/lang-en.js'
8
+ import { logUnknownData } from '../utils/report-unknown.js'
8
9
 
9
10
  export default class {
10
11
  constructor(platform, accessory) {
@@ -115,7 +116,12 @@ export default class {
115
116
 
116
117
  switch (deviceFunction) {
117
118
  default:
118
- this.accessory.logDebugWarn(`${platformLang.newScene}: [${command}] [${hexString}]`)
119
+ logUnknownData(this.accessory, {
120
+ kind: 'scene',
121
+ source: params.source,
122
+ raw: command,
123
+ hex: hexString,
124
+ })
119
125
  break
120
126
  }
121
127
  })
@@ -5,6 +5,7 @@ import {
5
5
  parseError,
6
6
  } from '../utils/functions.js'
7
7
  import platformLang from '../utils/lang-en.js'
8
+ import { logUnknownData } from '../utils/report-unknown.js'
8
9
 
9
10
  export default class {
10
11
  constructor(platform, accessory) {
@@ -115,7 +116,12 @@ export default class {
115
116
 
116
117
  switch (deviceFunction) {
117
118
  default:
118
- this.accessory.logDebugWarn(`${platformLang.newScene}: [${command}] [${hexString}]`)
119
+ logUnknownData(this.accessory, {
120
+ kind: 'scene',
121
+ source: params.source,
122
+ raw: command,
123
+ hex: hexString,
124
+ })
119
125
  break
120
126
  }
121
127
  })
@@ -7,6 +7,7 @@ import {
7
7
  parseError,
8
8
  } from '../utils/functions.js'
9
9
  import platformLang from '../utils/lang-en.js'
10
+ import { logUnknownData } from '../utils/report-unknown.js'
10
11
 
11
12
  export default class {
12
13
  constructor(platform, accessory) {
@@ -276,7 +277,12 @@ export default class {
276
277
  // break
277
278
  // }
278
279
  default:
279
- this.accessory.logDebugWarn(`${platformLang.newScene}: [${command}] [${hexString}]`)
280
+ logUnknownData(this.accessory, {
281
+ kind: 'scene',
282
+ source: params.source,
283
+ raw: command,
284
+ hex: hexString,
285
+ })
280
286
  break
281
287
  }
282
288
  })
@@ -10,6 +10,7 @@ import {
10
10
  sleep,
11
11
  } from '../utils/functions.js'
12
12
  import platformLang from '../utils/lang-en.js'
13
+ import { logUnknownData } from '../utils/report-unknown.js'
13
14
 
14
15
  /*
15
16
  Codes below were decoded from a real device's AWS status reports in
@@ -365,7 +366,12 @@ export default class {
365
366
  break
366
367
  }
367
368
  default:
368
- this.accessory.logDebugWarn(`${platformLang.newScene}: [${command}] [${hexString}]`)
369
+ logUnknownData(this.accessory, {
370
+ kind: 'scene',
371
+ source: params.source,
372
+ raw: command,
373
+ hex: hexString,
374
+ })
369
375
  break
370
376
  }
371
377
  })
@@ -7,6 +7,7 @@ import {
7
7
  parseError,
8
8
  } from '../utils/functions.js'
9
9
  import platformLang from '../utils/lang-en.js'
10
+ import { logUnknownData } from '../utils/report-unknown.js'
10
11
 
11
12
  export default class {
12
13
  constructor(platform, accessory) {
@@ -276,7 +277,12 @@ export default class {
276
277
  break
277
278
  }
278
279
  default:
279
- this.accessory.logDebugWarn(`${platformLang.newScene}: [${command}] [${hexString}]`)
280
+ logUnknownData(this.accessory, {
281
+ kind: 'scene',
282
+ source: params.source,
283
+ raw: command,
284
+ hex: hexString,
285
+ })
280
286
  break
281
287
  }
282
288
  })
@@ -7,6 +7,7 @@ import {
7
7
  parseError,
8
8
  } from '../utils/functions.js'
9
9
  import platformLang from '../utils/lang-en.js'
10
+ import { logUnknownData } from '../utils/report-unknown.js'
10
11
 
11
12
  /*
12
13
  H7101
@@ -332,7 +333,12 @@ export default class {
332
333
  break
333
334
  }
334
335
  default:
335
- this.accessory.logDebugWarn(`${platformLang.newScene}: [${command}] [${hexString}]`)
336
+ logUnknownData(this.accessory, {
337
+ kind: 'scene',
338
+ source: params.source,
339
+ raw: command,
340
+ hex: hexString,
341
+ })
336
342
  break
337
343
  }
338
344
  })
@@ -7,6 +7,7 @@ import {
7
7
  parseError,
8
8
  } from '../utils/functions.js'
9
9
  import platformLang from '../utils/lang-en.js'
10
+ import { logUnknownData } from '../utils/report-unknown.js'
10
11
 
11
12
  export default class {
12
13
  constructor(platform, accessory) {
@@ -276,7 +277,12 @@ export default class {
276
277
  break
277
278
  }
278
279
  default:
279
- this.accessory.logDebugWarn(`${platformLang.newScene}: [${command}] [${hexString}]`)
280
+ logUnknownData(this.accessory, {
281
+ kind: 'scene',
282
+ source: params.source,
283
+ raw: command,
284
+ hex: hexString,
285
+ })
280
286
  break
281
287
  }
282
288
  })
@@ -12,6 +12,7 @@ import {
12
12
  sleep,
13
13
  } from '../utils/functions.js'
14
14
  import platformLang from '../utils/lang-en.js'
15
+ import { logUnknownData } from '../utils/report-unknown.js'
15
16
 
16
17
  export default class {
17
18
  constructor(platform, accessory) {
@@ -514,7 +515,12 @@ export default class {
514
515
  break
515
516
  }
516
517
  default:
517
- this.accessory.logDebugWarn(`${platformLang.newScene}: [${command}] [${hexString}]`)
518
+ logUnknownData(this.accessory, {
519
+ kind: 'scene',
520
+ source: params.source,
521
+ raw: command,
522
+ hex: hexString,
523
+ })
518
524
  break
519
525
  }
520
526
  })
@@ -7,6 +7,7 @@ import {
7
7
  parseError,
8
8
  } from '../utils/functions.js'
9
9
  import platformLang from '../utils/lang-en.js'
10
+ import { logUnknownData } from '../utils/report-unknown.js'
10
11
 
11
12
  export default class {
12
13
  constructor(platform, accessory) {
@@ -276,7 +277,12 @@ export default class {
276
277
  break
277
278
  }
278
279
  default:
279
- this.accessory.logDebugWarn(`${platformLang.newScene}: [${command}] [${hexString}]`)
280
+ logUnknownData(this.accessory, {
281
+ kind: 'scene',
282
+ source: params.source,
283
+ raw: command,
284
+ hex: hexString,
285
+ })
280
286
  break
281
287
  }
282
288
  })
@@ -12,6 +12,7 @@ import {
12
12
  sleep,
13
13
  } from '../utils/functions.js'
14
14
  import platformLang from '../utils/lang-en.js'
15
+ import { logUnknownData } from '../utils/report-unknown.js'
15
16
 
16
17
  export default class {
17
18
  constructor(platform, accessory) {
@@ -527,7 +528,12 @@ export default class {
527
528
  break
528
529
  }
529
530
  default:
530
- this.accessory.logDebugWarn(`${platformLang.newScene}: [${command}] [${hexString}]`)
531
+ logUnknownData(this.accessory, {
532
+ kind: 'scene',
533
+ source: params.source,
534
+ raw: command,
535
+ hex: hexString,
536
+ })
531
537
  break
532
538
  }
533
539
  })
@@ -7,6 +7,7 @@ import {
7
7
  parseError,
8
8
  } from '../utils/functions.js'
9
9
  import platformLang from '../utils/lang-en.js'
10
+ import { logUnknownData } from '../utils/report-unknown.js'
10
11
 
11
12
  /*
12
13
 
@@ -337,7 +338,12 @@ export default class {
337
338
  break
338
339
  }
339
340
  default:
340
- this.accessory.logDebugWarn(`${platformLang.newScene}: [${command}] [${hexString}]`)
341
+ logUnknownData(this.accessory, {
342
+ kind: 'scene',
343
+ source: params.source,
344
+ raw: command,
345
+ hex: hexString,
346
+ })
341
347
  break
342
348
  }
343
349
  })
@@ -0,0 +1,143 @@
1
+ import { base64ToHex, getTwoItemPosition, hexToTwoItems, parseError } from '../utils/functions.js'
2
+ import platformLang from '../utils/lang-en.js'
3
+ import { logUnknownData, reportUnsupportedControl } from '../utils/report-unknown.js'
4
+
5
+ /**
6
+ * A fan whose power command is known but whose speeds are not.
7
+ *
8
+ * Govee's power command is the same across every appliance the plugin handles,
9
+ * so on and off can be offered with confidence for a model nobody has tested.
10
+ * The speeds are the part that differs per model, and getting the number of
11
+ * them wrong is worse than not offering them - so the slider reports what it
12
+ * needs and fails the request rather than quietly sending a guess.
13
+ *
14
+ * Once an owner sends in what their fan reports, the model gets its own handler
15
+ * with real speeds and stops using this one.
16
+ */
17
+ export default class {
18
+ constructor(platform, accessory) {
19
+ // Set up variables from the platform
20
+ this.hapChar = platform.api.hap.Characteristic
21
+ this.hapErr = platform.api.hap.HapStatusError
22
+ this.hapServ = platform.api.hap.Service
23
+ this.platform = platform
24
+
25
+ // Set up variables from the accessory
26
+ this.accessory = accessory
27
+
28
+ // Add the fan service if it doesn't already exist
29
+ this.service = this.accessory.getService(this.hapServ.Fanv2)
30
+ || this.accessory.addService(this.hapServ.Fanv2)
31
+
32
+ // Add the set handler to the fan active characteristic
33
+ this.service
34
+ .getCharacteristic(this.hapChar.Active)
35
+ .onSet(async value => this.internalStateUpdate(value))
36
+ this.cacheState = this.service.getCharacteristic(this.hapChar.Active).value ? 'on' : 'off'
37
+
38
+ // Offer the speed slider so the fan can report what it wants, but make it
39
+ // clear it does nothing yet rather than sending a guessed command
40
+ this.service
41
+ .getCharacteristic(this.hapChar.RotationSpeed)
42
+ .setProps({
43
+ maxValue: 100,
44
+ minStep: 1,
45
+ minValue: 0,
46
+ unit: 'unitless',
47
+ })
48
+ .onSet(async value => this.internalSpeedUpdate(value))
49
+ this.cacheSpeed = this.service.getCharacteristic(this.hapChar.RotationSpeed).value
50
+
51
+ // Output the customised options to the log
52
+ const opts = JSON.stringify({})
53
+ platform.log('[%s] %s %s.', accessory.displayName, platformLang.devInitOpts, opts)
54
+ this.accessory.log('on and off are supported for this model, the speeds are not known yet')
55
+ }
56
+
57
+ async internalStateUpdate(value) {
58
+ try {
59
+ const newValue = value ? 'on' : 'off'
60
+
61
+ // Don't continue if the new value is the same as before
62
+ if (this.cacheState === newValue) {
63
+ return
64
+ }
65
+
66
+ // Send the request to the platform sender function. This power command is
67
+ // the same for every govee appliance, so it is safe for an untested model
68
+ await this.platform.sendDeviceUpdate(this.accessory, {
69
+ cmd: 'ptReal',
70
+ value: value ? 'MwEBAAAAAAAAAAAAAAAAAAAAADM=' : 'MwEAAAAAAAAAAAAAAAAAAAAAADI=',
71
+ openApi: this.accessory.context.openApiCapabilities?.powerSwitch
72
+ ? { instance: 'powerSwitch', capabilityType: 'devices.capabilities.on_off', value: value ? 1 : 0 }
73
+ : undefined,
74
+ })
75
+
76
+ // Cache the new state and log if appropriate
77
+ this.cacheState = newValue
78
+ this.accessory.log(`${platformLang.curState} [${newValue}]`)
79
+ } catch (err) {
80
+ // Catch any errors during the process
81
+ this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`)
82
+
83
+ // Throw a 'no response' error and set a timeout to revert this after 2 seconds
84
+ setTimeout(() => {
85
+ this.service.updateCharacteristic(this.hapChar.Active, this.cacheState === 'on' ? 1 : 0)
86
+ }, 2000)
87
+ throw new this.hapErr(-70402)
88
+ }
89
+ }
90
+
91
+ async internalSpeedUpdate(value) {
92
+ // 0 is the slider following the fan being turned off, not a speed request
93
+ if (value === 0) {
94
+ return
95
+ }
96
+
97
+ reportUnsupportedControl(this.accessory, { control: 'fan speed', value })
98
+
99
+ // Put the slider back where it was, and fail the request so HomeKit shows
100
+ // it as not responding - that is what sends the owner to the log
101
+ setTimeout(() => {
102
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSpeed)
103
+ }, 2000)
104
+ throw new this.hapErr(-70402)
105
+ }
106
+
107
+ externalUpdate(params) {
108
+ // Power is the one thing that can be read back with confidence
109
+ if (params.state && params.state !== this.cacheState) {
110
+ this.cacheState = params.state
111
+ this.service.updateCharacteristic(this.hapChar.Active, this.cacheState === 'on' ? 1 : 0)
112
+ this.accessory.log(`${platformLang.curState} [${this.cacheState}]`)
113
+ }
114
+
115
+ // Everything else is reported so the model can be finished off
116
+ ;(params.commands || []).forEach((command) => {
117
+ const hexString = base64ToHex(command)
118
+ const hexParts = hexToTwoItems(hexString)
119
+
120
+ if (getTwoItemPosition(hexParts, 1) !== 'aa') {
121
+ return
122
+ }
123
+
124
+ // The power report is understood, anything else is worth sending in
125
+ if (getTwoItemPosition(hexParts, 2) === '01') {
126
+ const newState = getTwoItemPosition(hexParts, 3) === '01' ? 'on' : 'off'
127
+ if (this.cacheState !== newState) {
128
+ this.cacheState = newState
129
+ this.service.updateCharacteristic(this.hapChar.Active, this.cacheState === 'on' ? 1 : 0)
130
+ this.accessory.log(`${platformLang.curState} [${this.cacheState}]`)
131
+ }
132
+ return
133
+ }
134
+
135
+ logUnknownData(this.accessory, {
136
+ kind: 'scene',
137
+ source: params.source,
138
+ raw: command,
139
+ hex: hexString,
140
+ })
141
+ })
142
+ }
143
+ }
@@ -1,3 +1,4 @@
1
+ import { getHeaterFeatures } from '../utils/appliance-capabilities.js'
1
2
  import {
2
3
  base64ToHex,
3
4
  farToCen,
@@ -8,6 +9,7 @@ import {
8
9
  parseError,
9
10
  } from '../utils/functions.js'
10
11
  import platformLang from '../utils/lang-en.js'
12
+ import { logUnknownData } from '../utils/report-unknown.js'
11
13
 
12
14
  /*
13
15
  H7130 (without temperature reporting)
@@ -90,17 +92,29 @@ export default class {
90
92
  .onSet(async value => this.internalSpeedUpdate(value))
91
93
  this.cacheSpeed = this.service.getCharacteristic(this.hapChar.RotationSpeed).value
92
94
 
95
+ // Only offer the controls this model actually has, so a heater without a
96
+ // turning head does not get an oscillation switch that does nothing
97
+ this.features = getHeaterFeatures(this.accessory.context.gvModel)
98
+
93
99
  // Add the set handler to the heater swing mode characteristic (for oscillation)
94
- this.service
95
- .getCharacteristic(this.hapChar.SwingMode)
96
- .onSet(async value => this.internalSwingUpdate(value))
97
- this.cacheSwing = this.service.getCharacteristic(this.hapChar.SwingMode).value === 1 ? 'on' : 'off'
100
+ if (this.features.swing) {
101
+ this.service
102
+ .getCharacteristic(this.hapChar.SwingMode)
103
+ .onSet(async value => this.internalSwingUpdate(value))
104
+ this.cacheSwing = this.service.getCharacteristic(this.hapChar.SwingMode).value === 1 ? 'on' : 'off'
105
+ } else if (this.service.testCharacteristic(this.hapChar.SwingMode)) {
106
+ this.service.removeCharacteristic(this.service.getCharacteristic(this.hapChar.SwingMode))
107
+ }
98
108
 
99
- // Add the set handler to the heater lock characteristic (for oscillation)
100
- this.service
101
- .getCharacteristic(this.hapChar.LockPhysicalControls)
102
- .onSet(async value => this.internalLockUpdate(value))
103
- this.cacheLock = this.service.getCharacteristic(this.hapChar.LockPhysicalControls).value === 1 ? 'on' : 'off'
109
+ // Add the set handler to the heater lock characteristic
110
+ if (this.features.lock) {
111
+ this.service
112
+ .getCharacteristic(this.hapChar.LockPhysicalControls)
113
+ .onSet(async value => this.internalLockUpdate(value))
114
+ this.cacheLock = this.service.getCharacteristic(this.hapChar.LockPhysicalControls).value === 1 ? 'on' : 'off'
115
+ } else if (this.service.testCharacteristic(this.hapChar.LockPhysicalControls)) {
116
+ this.service.removeCharacteristic(this.service.getCharacteristic(this.hapChar.LockPhysicalControls))
117
+ }
104
118
 
105
119
  // Output the customised options to the log
106
120
  const opts = JSON.stringify({
@@ -275,8 +289,15 @@ export default class {
275
289
  if (hasProperty(params, 'temperature')) {
276
290
  const newTemp = nearestHalf(farToCen(params.temperature / 100))
277
291
  if (newTemp <= 100) {
278
- // Device must be one that DOES support ambient temperature
279
- this.accessory.logWarn('you should enable `tempReporting` in the config for this device')
292
+ // A real reading, so this heater does report the room temperature and
293
+ // belongs on the other tile. Remember that for next time rather than
294
+ // asking the owner to work it out and tick a box themselves. The tile
295
+ // is not swapped mid-run, since that would rebuild the accessory
296
+ // underneath HomeKit while it is in use.
297
+ if (!this.accessory.context.supportsAmbientTemp) {
298
+ this.accessory.context.supportsAmbientTemp = true
299
+ this.accessory.logWarn('this heater reports the room temperature, so it will move to a temperature-capable tile the next time homebridge restarts')
300
+ }
280
301
  }
281
302
  }
282
303
 
@@ -330,6 +351,9 @@ export default class {
330
351
  case '1800':
331
352
  case '1801': {
332
353
  // Swing Mode
354
+ if (!this.features.swing) {
355
+ break
356
+ }
333
357
  const newSwing = getTwoItemPosition(hexParts, 3) === '01' ? 'on' : 'off'
334
358
  if (this.cacheSwing !== newSwing) {
335
359
  this.cacheSwing = newSwing
@@ -341,6 +365,9 @@ export default class {
341
365
  case '1000':
342
366
  case '1001': {
343
367
  // Child Lock
368
+ if (!this.features.lock) {
369
+ break
370
+ }
344
371
  const newLock = getTwoItemPosition(hexParts, 3) === '01' ? 'on' : 'off'
345
372
  if (this.cacheLock !== newLock) {
346
373
  this.cacheLock = newLock
@@ -388,7 +415,12 @@ export default class {
388
415
  break
389
416
  }
390
417
  default:
391
- this.accessory.logDebugWarn(`${platformLang.newScene}: [${command}] [${hexString}]`)
418
+ logUnknownData(this.accessory, {
419
+ kind: 'scene',
420
+ source: params.source,
421
+ raw: command,
422
+ hex: hexString,
423
+ })
392
424
  break
393
425
  }
394
426
  })
@@ -8,6 +8,7 @@ import {
8
8
  parseError,
9
9
  } from '../utils/functions.js'
10
10
  import platformLang from '../utils/lang-en.js'
11
+ import { logUnknownData } from '../utils/report-unknown.js'
11
12
 
12
13
  /*
13
14
  H7130 (with temperature reporting)
@@ -498,8 +499,13 @@ export default class {
498
499
  const newTemp = nearestHalf(farToCen(params.temperature / 100))
499
500
  if (newTemp !== this.cacheTemp) {
500
501
  if (newTemp > 100) {
501
- // Device must be one that does not support ambient temperature
502
- this.accessory.logWarn('you should disable `tempReporting` in the config for this device')
502
+ // Not a real reading, so this heater does not report the room
503
+ // temperature and belongs on the simpler tile. Remember that for next
504
+ // time rather than asking the owner to work it out themselves.
505
+ if (this.accessory.context.supportsAmbientTemp !== false) {
506
+ this.accessory.context.supportsAmbientTemp = false
507
+ this.accessory.logWarn('this heater does not report the room temperature, so it will move to a simpler tile the next time homebridge restarts')
508
+ }
503
509
  } else {
504
510
  this.cacheTemp = newTemp
505
511
  this.service.updateCharacteristic(this.hapChar.CurrentTemperature, this.cacheTemp)
@@ -664,7 +670,12 @@ export default class {
664
670
  case '1601': // DND on?
665
671
  break
666
672
  default:
667
- this.accessory.logDebugWarn(`${platformLang.newScene}: [${command}] [${hexString}]`)
673
+ logUnknownData(this.accessory, {
674
+ kind: 'scene',
675
+ source: params.source,
676
+ raw: command,
677
+ hex: hexString,
678
+ })
668
679
  break
669
680
  }
670
681
  })