@homebridge-plugins/homebridge-govee 11.32.2-beta.1 → 11.32.2-beta.10

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 (63) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/config.schema.json +1 -1
  3. package/lib/connection/ble.js +26 -1
  4. package/lib/device/appliance-basic.js +280 -0
  5. package/lib/device/dehumidifier-H7150.js +7 -1
  6. package/lib/device/dehumidifier-H7151.js +7 -1
  7. package/lib/device/dehumidifier-H7152.js +7 -1
  8. package/lib/device/diffuser-H7161.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-H7105.js +7 -1
  14. package/lib/device/fan-H7107.js +16 -7
  15. package/lib/device/fan-H7111.js +7 -1
  16. package/lib/device/heater1a.js +44 -12
  17. package/lib/device/heater1b.js +14 -3
  18. package/lib/device/heater2.js +7 -1
  19. package/lib/device/humidifier-H7140.js +7 -1
  20. package/lib/device/humidifier-H7141.js +7 -1
  21. package/lib/device/humidifier-H7142.js +7 -1
  22. package/lib/device/humidifier-H7143.js +7 -1
  23. package/lib/device/humidifier-H7160.js +7 -1
  24. package/lib/device/ice-maker-H7172.js +7 -1
  25. package/lib/device/{ice-maker-H717D.js → ice-maker-H8121.js} +21 -11
  26. package/lib/device/{ice-maker-H8120.js → ice-maker-H8122.js} +21 -11
  27. package/lib/device/index.js +30 -28
  28. package/lib/device/kettle.js +7 -1
  29. package/lib/device/outlet-single.js +7 -1
  30. package/lib/device/purifier-H7120.js +7 -1
  31. package/lib/device/purifier-H7121.js +7 -1
  32. package/lib/device/purifier-H7122.js +7 -1
  33. package/lib/device/purifier-H7123.js +7 -1
  34. package/lib/device/purifier-H7124.js +7 -1
  35. package/lib/device/purifier-H7126.js +7 -1
  36. package/lib/device/sensor-button.js +6 -1
  37. package/lib/device/sensor-contact.js +6 -1
  38. package/lib/device/sensor-presence.js +7 -1
  39. package/lib/device/sensor-thermo4.js +13 -2
  40. package/lib/device/template.js +15 -2
  41. package/lib/platform.js +28 -9
  42. package/lib/utils/appliance-capabilities.js +45 -0
  43. package/lib/utils/command-builder.js +4 -4
  44. package/lib/utils/constants.js +120 -8
  45. package/lib/utils/decode.js +4 -1
  46. package/lib/utils/device-capabilities.js +62 -3
  47. package/lib/utils/lang-en.js +0 -2
  48. package/lib/utils/report-unknown.js +156 -0
  49. package/package.json +1 -1
  50. package/lib/device/diffuser-H7162.js +0 -123
  51. package/lib/device/fan-H7102.js +0 -284
  52. package/lib/device/fan-H7106.js +0 -284
  53. package/lib/device/humidifier-H7145.js +0 -187
  54. package/lib/device/humidifier-H7147.js +0 -187
  55. package/lib/device/humidifier-H7148.js +0 -187
  56. package/lib/device/humidifier-H7149.js +0 -187
  57. package/lib/device/humidifier-H714E.js +0 -187
  58. package/lib/device/purifier-H7127.js +0 -359
  59. package/lib/device/purifier-H7128.js +0 -359
  60. package/lib/device/purifier-H7129.js +0 -359
  61. package/lib/device/purifier-H712C.js +0 -359
  62. package/lib/utils/ble-timeouts.test.js +0 -44
  63. package/lib/utils/functions.test.js +0 -292
package/CHANGELOG.md CHANGED
@@ -8,6 +8,19 @@ All notable changes to `@homebridge-plugins/homebridge-govee` will be documented
8
8
 
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
+ - 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 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
21
+ - feat: give every appliance type a starting handler, so a new model works for on and off and asks for the rest
22
+ - refactor: point models that behave identically at one shared handler instead of copies of the same file
23
+ - fix: stop shipping the test files to npm, since they are only needed in the repository
11
24
 
12
25
  ## v11.32.1 (2026-08-02)
13
26
 
@@ -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
 
@@ -0,0 +1,280 @@
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 starting point for any appliance whose power command is known but whose
7
+ * other controls are not.
8
+ *
9
+ * Govee uses the same on/off command for every appliance the plugin handles, so
10
+ * power can be offered with confidence for a model nobody has tested. The
11
+ * settings that differ per model - speeds, modes, sizes - are the risky part,
12
+ * and getting one wrong is worse than not offering it. So those controls appear
13
+ * but report what they need and fail the request, rather than sending a guess.
14
+ *
15
+ * Failing rather than doing nothing is deliberate: HomeKit shows the control as
16
+ * not responding, which is what sends the owner to the log, where they find a
17
+ * line to paste into an issue. That turns every owner of an untested model into
18
+ * the source of the data needed to finish it.
19
+ *
20
+ * Once that data arrives the model gets a proper handler and stops using this.
21
+ */
22
+ class ApplianceBasic {
23
+ constructor(platform, accessory, config) {
24
+ // Set up variables from the platform
25
+ this.hapChar = platform.api.hap.Characteristic
26
+ this.hapErr = platform.api.hap.HapStatusError
27
+ this.hapServ = platform.api.hap.Service
28
+ this.platform = platform
29
+
30
+ // Set up variables from the accessory
31
+ this.accessory = accessory
32
+ this.config = config
33
+
34
+ // Some appliances can have a richer tile, but only if the device reports
35
+ // the reading that tile needs - HomeKit's humidifier, for example, has to
36
+ // show a current humidity, and plenty of Govee humidifiers never send one.
37
+ // Where Govee tells us the device has that sensor, use the better tile;
38
+ // otherwise fall back to the plain one, which needs nothing extra.
39
+ const hasSensor = config.richCapability
40
+ && !!accessory.context.openApiCapabilities?.[config.richCapability]
41
+ const serviceName = (config.richService && hasSensor) ? config.richService : config.service
42
+ this.usingRichService = serviceName === config.richService
43
+
44
+ // Drop the other service if this device has switched between the two
45
+ const otherName = this.usingRichService ? config.service : config.richService
46
+ if (otherName && this.hapServ[otherName]) {
47
+ const stale = this.accessory.getService(this.hapServ[otherName])
48
+ if (stale) {
49
+ this.accessory.removeService(stale)
50
+ }
51
+ }
52
+
53
+ const serviceType = this.hapServ[serviceName]
54
+ this.service = this.accessory.getService(serviceType)
55
+ || this.accessory.addService(serviceType)
56
+
57
+ // The plain Fan service switches on a boolean `On`, while Fanv2 and
58
+ // AirPurifier use `Active`. Follow whichever this service uses, so these
59
+ // tiles behave like the hand-written handlers for the same kind of device.
60
+ const usesOn = serviceName === 'Fan'
61
+ this.powerChar = usesOn ? this.hapChar.On : this.hapChar.Active
62
+ this.powerOn = usesOn ? true : 1
63
+ this.powerOff = usesOn ? false : 0
64
+
65
+ // Add the set handler to the power characteristic
66
+ this.service
67
+ .getCharacteristic(this.powerChar)
68
+ .onSet(async value => this.internalStateUpdate(value))
69
+ this.cacheState = this.service.getCharacteristic(this.powerChar).value ? 'on' : 'off'
70
+
71
+ // Some services need a current-state characteristic to look right in the
72
+ // Home app. It belongs to the richer tile, so it is skipped when a device
73
+ // has fallen back to the plain fan for want of a sensor.
74
+ const fellBackToPlain = !!config.richService && !this.usingRichService
75
+ if (config.currentStateChar && !fellBackToPlain) {
76
+ this.currentStateChar = this.hapChar[config.currentStateChar]
77
+ this.service.updateCharacteristic(
78
+ this.currentStateChar,
79
+ this.cacheState === 'on' ? config.currentStateOn : config.currentStateOff,
80
+ )
81
+ }
82
+
83
+ // Offer the variable control so the device can be asked about it, but make
84
+ // it clear it does nothing yet rather than sending a guessed command. The
85
+ // richer services name this control differently, so follow the service.
86
+ const variableChar = this.usingRichService && config.richVariableChar
87
+ ? config.richVariableChar
88
+ : config.variableChar
89
+ if (variableChar) {
90
+ this.variableChar = this.hapChar[variableChar]
91
+ this.service
92
+ .getCharacteristic(this.variableChar)
93
+ .onSet(async value => this.internalVariableUpdate(value))
94
+ this.cacheVariable = this.service.getCharacteristic(this.variableChar).value
95
+ }
96
+
97
+ // Output the customised options to the log
98
+ const opts = JSON.stringify({})
99
+ platform.log('[%s] %s %s.', accessory.displayName, platformLang.devInitOpts, opts)
100
+ this.accessory.log(`on and off are supported for this model, the ${config.variableLabel || 'other settings'} are not known yet`)
101
+ }
102
+
103
+ async internalStateUpdate(value) {
104
+ try {
105
+ const newValue = value ? 'on' : 'off'
106
+
107
+ // Don't continue if the new value is the same as before
108
+ if (this.cacheState === newValue) {
109
+ return
110
+ }
111
+
112
+ // This power command is the same for every govee appliance, so it is safe
113
+ // to send for a model that has not been tested
114
+ await this.platform.sendDeviceUpdate(this.accessory, {
115
+ cmd: 'ptReal',
116
+ value: value ? 'MwEBAAAAAAAAAAAAAAAAAAAAADM=' : 'MwEAAAAAAAAAAAAAAAAAAAAAADI=',
117
+ openApi: this.accessory.context.openApiCapabilities?.powerSwitch
118
+ ? { instance: 'powerSwitch', capabilityType: 'devices.capabilities.on_off', value: value ? 1 : 0 }
119
+ : undefined,
120
+ })
121
+
122
+ // Cache the new state and log if appropriate
123
+ this.cacheState = newValue
124
+ if (this.currentStateChar) {
125
+ this.service.updateCharacteristic(
126
+ this.currentStateChar,
127
+ value ? this.config.currentStateOn : this.config.currentStateOff,
128
+ )
129
+ }
130
+ this.accessory.log(`${platformLang.curState} [${newValue}]`)
131
+ } catch (err) {
132
+ // Catch any errors during the process
133
+ this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`)
134
+
135
+ // Throw a 'no response' error and set a timeout to revert this after 2 seconds
136
+ setTimeout(() => {
137
+ this.service.updateCharacteristic(this.hapChar.Active, this.cacheState === 'on' ? 1 : 0)
138
+ }, 2000)
139
+ throw new this.hapErr(-70402)
140
+ }
141
+ }
142
+
143
+ async internalVariableUpdate(value) {
144
+ // 0 is the control following the device being turned off, not a request
145
+ if (value === 0) {
146
+ return
147
+ }
148
+
149
+ reportUnsupportedControl(this.accessory, {
150
+ control: this.config.variableLabel || 'setting',
151
+ value,
152
+ })
153
+
154
+ // Put the control back, and fail the request so HomeKit shows it as not
155
+ // responding - that is what sends the owner to the log
156
+ setTimeout(() => {
157
+ this.service.updateCharacteristic(this.variableChar, this.cacheVariable)
158
+ }, 2000)
159
+ throw new this.hapErr(-70402)
160
+ }
161
+
162
+ externalUpdate(params) {
163
+ // Feed the sensor reading through when the richer tile is in use - HomeKit
164
+ // needs it, and it is the reason that tile was chosen
165
+ if (this.usingRichService && this.config.richReadingChar && params.humidity !== undefined) {
166
+ const reading = Number(params.humidity)
167
+ if (Number.isFinite(reading) && reading !== this.cacheReading) {
168
+ this.cacheReading = reading
169
+ this.service.updateCharacteristic(this.hapChar[this.config.richReadingChar], reading)
170
+ }
171
+ }
172
+
173
+ // Power is the one thing that can be read back with confidence
174
+ if (params.state && params.state !== this.cacheState) {
175
+ this.cacheState = params.state
176
+ this.service.updateCharacteristic(this.hapChar.Active, this.cacheState === 'on' ? 1 : 0)
177
+ if (this.currentStateChar) {
178
+ this.service.updateCharacteristic(
179
+ this.currentStateChar,
180
+ this.cacheState === 'on' ? this.config.currentStateOn : this.config.currentStateOff,
181
+ )
182
+ }
183
+ this.accessory.log(`${platformLang.curState} [${this.cacheState}]`)
184
+ }
185
+
186
+ // Everything else gets reported so the model can be finished off
187
+ ;(params.commands || []).forEach((command) => {
188
+ const hexString = base64ToHex(command)
189
+ const hexParts = hexToTwoItems(hexString)
190
+
191
+ if (getTwoItemPosition(hexParts, 1) !== 'aa') {
192
+ return
193
+ }
194
+
195
+ // The power report is understood, anything else is worth sending in
196
+ if (getTwoItemPosition(hexParts, 2) === '01') {
197
+ const newState = getTwoItemPosition(hexParts, 3) === '01' ? 'on' : 'off'
198
+ if (this.cacheState !== newState) {
199
+ this.cacheState = newState
200
+ this.service.updateCharacteristic(this.hapChar.Active, this.cacheState === 'on' ? 1 : 0)
201
+ this.accessory.log(`${platformLang.curState} [${this.cacheState}]`)
202
+ }
203
+ return
204
+ }
205
+
206
+ logUnknownData(this.accessory, {
207
+ kind: 'scene',
208
+ source: params.source,
209
+ raw: command,
210
+ hex: hexString,
211
+ })
212
+ })
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Build a basic handler for a kind of appliance.
218
+ *
219
+ * @param {object} config how this kind of appliance should appear
220
+ * @returns {Function} a handler class the platform can construct
221
+ */
222
+ export function makeBasicAppliance(config) {
223
+ return class extends ApplianceBasic {
224
+ constructor(platform, accessory) {
225
+ super(platform, accessory, config)
226
+ }
227
+ }
228
+ }
229
+
230
+ // Fan-shaped appliances: a speed slider that is not worked out yet
231
+ export const deviceFanBasic = makeBasicAppliance({
232
+ service: 'Fanv2',
233
+ variableChar: 'RotationSpeed',
234
+ variableLabel: 'fan speed',
235
+ })
236
+
237
+ // Ice makers reuse the fan tile, with the slider standing in for ice size
238
+ export const deviceIceMakerBasic = makeBasicAppliance({
239
+ service: 'Fanv2',
240
+ variableChar: 'RotationSpeed',
241
+ variableLabel: 'ice size',
242
+ })
243
+
244
+ // Humidifiers get the plain fan tile by default, because HomeKit's humidifier
245
+ // has to show a current humidity and many Govee humidifiers never report one.
246
+ // A model that does report it gets the proper humidifier tile instead.
247
+ export const deviceHumidifierBasic = makeBasicAppliance({
248
+ service: 'Fan',
249
+ variableChar: 'RotationSpeed',
250
+ variableLabel: 'mist level',
251
+ richService: 'HumidifierDehumidifier',
252
+ richCapability: 'sensorHumidity',
253
+ richVariableChar: 'RelativeHumidityHumidifierThreshold',
254
+ richReadingChar: 'CurrentRelativeHumidity',
255
+ currentStateChar: 'CurrentHumidifierDehumidifierState',
256
+ currentStateOn: 2, // humidifying
257
+ currentStateOff: 0, // inactive
258
+ })
259
+
260
+ export const deviceDehumidifierBasic = makeBasicAppliance({
261
+ service: 'Fan',
262
+ variableChar: 'RotationSpeed',
263
+ variableLabel: 'fan speed',
264
+ richService: 'HumidifierDehumidifier',
265
+ richCapability: 'sensorHumidity',
266
+ richVariableChar: 'RelativeHumidityDehumidifierThreshold',
267
+ richReadingChar: 'CurrentRelativeHumidity',
268
+ currentStateChar: 'CurrentHumidifierDehumidifierState',
269
+ currentStateOn: 3, // dehumidifying
270
+ currentStateOff: 0, // inactive
271
+ })
272
+
273
+ export const devicePurifierBasic = makeBasicAppliance({
274
+ service: 'AirPurifier',
275
+ variableChar: 'RotationSpeed',
276
+ variableLabel: 'fan speed',
277
+ currentStateChar: 'CurrentAirPurifierState',
278
+ currentStateOn: 2, // purifying air
279
+ currentStateOff: 0, // inactive
280
+ })
@@ -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
  })
@@ -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
  })
@@ -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
  })
@@ -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) {
@@ -210,12 +211,15 @@ export default class {
210
211
  return
211
212
  }
212
213
 
213
- // The AWS code mirrors the device's own `aa1d..` oscillation report,
214
- // carrying over the extra bytes from the last report seen (#1334).
215
- // Mirroring a status code is not guaranteed on every model, so the
216
- // documented OpenAPI oscillation toggle is also sent when the device
217
- // advertises it
218
- const tailBytes = this.cacheSwingCode
214
+ // The AWS code mirrors the device's own `aa1d..` oscillation report.
215
+ // Turning ON carries over the swing-range bytes from the last report
216
+ // seen, which hardware-confirmed works. Turning OFF must NOT carry
217
+ // them: the fan answered a parameterised off command by echoing back a
218
+ // zero-tailed `331d00..` frame while continuing to oscillate — its own
219
+ // way of showing the shape it expects (#1334). Mirroring a status code
220
+ // is not guaranteed on every model, so the documented OpenAPI
221
+ // oscillation toggle is also sent when the device advertises it
222
+ const tailBytes = value && this.cacheSwingCode
219
223
  ? this.cacheSwingCode.slice(6, 14).match(/.{1,2}/g).map(byte => Number.parseInt(byte, 16))
220
224
  : []
221
225
  await this.platform.sendDeviceUpdate(this.accessory, {
@@ -524,7 +528,12 @@ export default class {
524
528
  break
525
529
  }
526
530
  default:
527
- 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
+ })
528
537
  break
529
538
  }
530
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
  })