@homebridge-plugins/homebridge-govee 11.32.2-beta.2 → 11.32.2-beta.21

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 (73) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/config.schema.json +1 -1
  3. package/lib/connection/ble.js +26 -1
  4. package/lib/connection/openapi.js +18 -0
  5. package/lib/device/appliance-basic.js +377 -0
  6. package/lib/device/dehumidifier-H7150.js +9 -3
  7. package/lib/device/diffuser-H7161.js +7 -1
  8. package/lib/device/fan-H1370.js +7 -1
  9. package/lib/device/fan-H7100.js +36 -4
  10. package/lib/device/fan-H7105.js +7 -1
  11. package/lib/device/fan-H7107.js +7 -1
  12. package/lib/device/heater1a.js +52 -12
  13. package/lib/device/heater1b.js +14 -3
  14. package/lib/device/heater2.js +7 -1
  15. package/lib/device/humidifier-H7140.js +7 -1
  16. package/lib/device/humidifier-H7141.js +7 -1
  17. package/lib/device/humidifier-H7142.js +7 -1
  18. package/lib/device/humidifier-H7143.js +7 -1
  19. package/lib/device/humidifier-H7160.js +7 -1
  20. package/lib/device/ice-maker-H7172.js +19 -7
  21. package/lib/device/index.js +44 -51
  22. package/lib/device/kettle.js +7 -1
  23. package/lib/device/outlet-single.js +7 -1
  24. package/lib/device/purifier-H7120.js +14 -25
  25. package/lib/device/purifier-H7122.js +7 -1
  26. package/lib/device/purifier-H7123.js +9 -3
  27. package/lib/device/purifier-H7126.js +7 -1
  28. package/lib/device/{sensor-button.js → sensor-basic.js} +14 -2
  29. package/lib/device/sensor-presence.js +7 -1
  30. package/lib/device/sensor-thermo4.js +13 -2
  31. package/lib/device/switch-double.js +34 -15
  32. package/lib/device/switch-triple.js +36 -17
  33. package/lib/device/template.js +15 -2
  34. package/lib/platform.js +83 -799
  35. package/lib/utils/capabilities.js +220 -0
  36. package/lib/utils/command-builder.js +6 -4
  37. package/lib/utils/config-parser.js +280 -0
  38. package/lib/utils/constants.js +120 -8
  39. package/lib/utils/decode.js +4 -1
  40. package/lib/utils/device-capabilities.js +77 -3
  41. package/lib/utils/device-context.js +179 -0
  42. package/lib/utils/device-merge.js +251 -0
  43. package/lib/utils/dispatch.js +159 -0
  44. package/lib/utils/functions.js +9 -0
  45. package/lib/utils/lang-en.js +0 -2
  46. package/lib/utils/report-unknown.js +156 -0
  47. package/package.json +1 -1
  48. package/lib/device/dehumidifier-H7151.js +0 -189
  49. package/lib/device/dehumidifier-H7152.js +0 -180
  50. package/lib/device/diffuser-H7162.js +0 -123
  51. package/lib/device/fan-H1310.js +0 -284
  52. package/lib/device/fan-H7101.js +0 -340
  53. package/lib/device/fan-H7102.js +0 -284
  54. package/lib/device/fan-H7106.js +0 -284
  55. package/lib/device/fan-H7111.js +0 -345
  56. package/lib/device/humidifier-H7145.js +0 -187
  57. package/lib/device/humidifier-H7147.js +0 -187
  58. package/lib/device/humidifier-H7148.js +0 -187
  59. package/lib/device/humidifier-H7149.js +0 -187
  60. package/lib/device/humidifier-H714E.js +0 -187
  61. package/lib/device/ice-maker-H717D.js +0 -218
  62. package/lib/device/ice-maker-H8120.js +0 -218
  63. package/lib/device/outlet-double.js +0 -188
  64. package/lib/device/outlet-triple.js +0 -160
  65. package/lib/device/purifier-H7121.js +0 -406
  66. package/lib/device/purifier-H7124.js +0 -504
  67. package/lib/device/purifier-H7127.js +0 -359
  68. package/lib/device/purifier-H7128.js +0 -359
  69. package/lib/device/purifier-H7129.js +0 -359
  70. package/lib/device/purifier-H712C.js +0 -359
  71. package/lib/device/sensor-contact.js +0 -22
  72. package/lib/utils/ble-timeouts.test.js +0 -44
  73. package/lib/utils/functions.test.js +0 -292
package/CHANGELOG.md CHANGED
@@ -9,6 +9,37 @@ 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 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
24
+ - fix: refuse to send a bluetooth command too long to fit in a packet, rather than sending a longer one the device quietly ignores
25
+ - test: record what every supported model turns into, so a change can be shown to have altered nothing
26
+ - feat: work out what an appliance can do from what the device itself reports, so a model nobody has written down still gets the right controls
27
+ - feat: offer oscillation and the child lock on any appliance that reports having them, driven through govee's api rather than needing that model's command codes
28
+ - refactor: point three more models at a shared handler instead of a near-copy of the same file, keeping each one's behaviour exactly as it was
29
+ - fix: give the H7124 the air quality handling the H7123 already had, so a poor reading is named properly and stops being logged over and over (#1261)
30
+ - refactor: point the H7151 and H7111 at the handlers they were byte-for-byte copies of
31
+ - refactor: build the ice maker size codes from the model rather than writing both orderings out by hand
32
+ - refactor: build the switch and outlet tiles of a multi-socket device from one handler instead of two copies of it
33
+ - fix: move the H7152 dehumidifier's speed slider when it reports a new speed, which until now only the H7150 and H7151 did
34
+ - refactor: point the H7101 and H7111 fans at the H7100 handler they matched command for command
35
+ - feat: give a diffuser model that has not been worked out yet the same starting handler the other appliances have, so it works for on and off
36
+ - refactor: choose a device handler from a table, so adding a device type no longer means editing the platform
37
+ - refactor: move the settings parsing into its own file, away from the code that talks to devices
38
+ - refactor: move the work of describing a device to its handler into its own file
39
+ - refactor: move the rules for combining govee's device lists, and for letting go of a device that has gone, into their own file with tests
40
+ - test: check what the plugin sends over aws, the local network and the public api, none of which was covered before
41
+ - feat: say when govee has not listed a capability the plugin is about to use, since the request then succeeds and the device does nothing
42
+ - docs: note that the local network api accepts the raw command form, which is how scenes work without the cloud
12
43
 
13
44
  ## v11.32.1 (2026-08-02)
14
45
 
@@ -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
 
@@ -3,6 +3,7 @@ import mqtt from 'mqtt'
3
3
 
4
4
  import { k2rgb } from '../utils/colour.js'
5
5
  import { describeControlFailure, parseError } from '../utils/functions.js'
6
+ import { isFirstReport } from '../utils/report-unknown.js'
6
7
 
7
8
  const BASE_URL = 'https://openapi.api.govee.com'
8
9
 
@@ -381,6 +382,23 @@ export default class {
381
382
  if (capability?.type && capability?.instance) {
382
383
  return capability
383
384
  }
385
+
386
+ // The device never listed this one. Govee will still accept the request and
387
+ // answer as though it worked, then do nothing - so without saying something
388
+ // here the control simply appears broken with no clue why. Said once per
389
+ // device and capability, then dropped to debug
390
+ const fingerprint = `openapi-unlisted|${accessory.context.gvDeviceId}|${instance}`
391
+ const message = `govee did not list [${instance}] for this device, so that command may be ignored - please report this line if the control does not work: ${JSON.stringify({
392
+ model: accessory.context.gvModel,
393
+ instance,
394
+ listed: Object.keys(lookup),
395
+ })}`
396
+ if (isFirstReport(fingerprint)) {
397
+ accessory.logDebugWarn(message)
398
+ } else {
399
+ accessory.logDebug(message)
400
+ }
401
+
384
402
  return {
385
403
  type: fallbackType,
386
404
  instance,
@@ -0,0 +1,377 @@
1
+ import { describeSources, resolveControls } from '../utils/capabilities.js'
2
+ import { base64ToHex, getTwoItemPosition, hexToTwoItems, parseError } from '../utils/functions.js'
3
+ import platformLang from '../utils/lang-en.js'
4
+ import { logUnknownData, reportUnsupportedControl } from '../utils/report-unknown.js'
5
+
6
+ /**
7
+ * A starting point for any appliance whose power command is known but whose
8
+ * other controls are not.
9
+ *
10
+ * Govee uses the same on/off command for every appliance the plugin handles, so
11
+ * power can be offered with confidence for a model nobody has tested. The
12
+ * settings that differ per model - speeds, modes, sizes - are the risky part,
13
+ * and getting one wrong is worse than not offering it. So those controls appear
14
+ * but report what they need and fail the request, rather than sending a guess.
15
+ *
16
+ * Failing rather than doing nothing is deliberate: HomeKit shows the control as
17
+ * not responding, which is what sends the owner to the log, where they find a
18
+ * line to paste into an issue. That turns every owner of an untested model into
19
+ * the source of the data needed to finish it.
20
+ *
21
+ * Once that data arrives the model gets a proper handler and stops using this.
22
+ */
23
+ class ApplianceBasic {
24
+ constructor(platform, accessory, config) {
25
+ // Set up variables from the platform
26
+ this.hapChar = platform.api.hap.Characteristic
27
+ this.hapErr = platform.api.hap.HapStatusError
28
+ this.hapServ = platform.api.hap.Service
29
+ this.platform = platform
30
+
31
+ // Set up variables from the accessory
32
+ this.accessory = accessory
33
+ this.config = config
34
+
35
+ // Work out what this particular device can do, rather than what its model
36
+ // generally can - for a model nobody has written down, what the device
37
+ // itself reported is all there is to go on
38
+ const resolved = resolveControls({
39
+ model: accessory.context.gvModel,
40
+ family: config.family,
41
+ firmware: accessory.context.firmware,
42
+ openApiCapabilities: accessory.context.openApiCapabilities,
43
+ deviceConf: platform.deviceConf?.[accessory.context.gvDeviceId],
44
+ })
45
+ this.controls = resolved.controls
46
+ this.cacheToggles = {}
47
+ this.discoveredControls = []
48
+
49
+ // Some appliances can have a richer tile, but only if the device reports
50
+ // the reading that tile needs - HomeKit's humidifier, for example, has to
51
+ // show a current humidity, and plenty of Govee humidifiers never send one.
52
+ // Where the device says it has that sensor, use the better tile; otherwise
53
+ // fall back to the plain one, which needs nothing extra.
54
+ const hasSensor = config.richControl && this.controls[config.richControl]
55
+ const serviceName = (config.richService && hasSensor) ? config.richService : config.service
56
+ this.usingRichService = serviceName === config.richService
57
+
58
+ // Drop the other service if this device has switched between the two
59
+ const otherName = this.usingRichService ? config.service : config.richService
60
+ if (otherName && this.hapServ[otherName]) {
61
+ const stale = this.accessory.getService(this.hapServ[otherName])
62
+ if (stale) {
63
+ this.accessory.removeService(stale)
64
+ }
65
+ }
66
+
67
+ const serviceType = this.hapServ[serviceName]
68
+ this.service = this.accessory.getService(serviceType)
69
+ || this.accessory.addService(serviceType)
70
+
71
+ // The plain Fan service switches on a boolean `On`, while Fanv2 and
72
+ // AirPurifier use `Active`. Follow whichever this service uses, so these
73
+ // tiles behave like the hand-written handlers for the same kind of device.
74
+ const usesOn = serviceName === 'Fan'
75
+ this.powerChar = usesOn ? this.hapChar.On : this.hapChar.Active
76
+ this.powerOn = usesOn ? true : 1
77
+ this.powerOff = usesOn ? false : 0
78
+
79
+ // Add the set handler to the power characteristic
80
+ this.service
81
+ .getCharacteristic(this.powerChar)
82
+ .onSet(async value => this.internalStateUpdate(value))
83
+ this.cacheState = this.service.getCharacteristic(this.powerChar).value ? 'on' : 'off'
84
+
85
+ // Some services need a current-state characteristic to look right in the
86
+ // Home app. It belongs to the richer tile, so it is skipped when a device
87
+ // has fallen back to the plain fan for want of a sensor.
88
+ const fellBackToPlain = !!config.richService && !this.usingRichService
89
+ if (config.currentStateChar && !fellBackToPlain) {
90
+ this.currentStateChar = this.hapChar[config.currentStateChar]
91
+ this.service.updateCharacteristic(
92
+ this.currentStateChar,
93
+ this.cacheState === 'on' ? config.currentStateOn : config.currentStateOff,
94
+ )
95
+ }
96
+
97
+ // Offer the variable control so the device can be asked about it, but make
98
+ // it clear it does nothing yet rather than sending a guessed command. The
99
+ // richer services name this control differently, so follow the service.
100
+ const variableChar = this.usingRichService && config.richVariableChar
101
+ ? config.richVariableChar
102
+ : config.variableChar
103
+ if (variableChar) {
104
+ this.variableChar = this.hapChar[variableChar]
105
+ this.service
106
+ .getCharacteristic(this.variableChar)
107
+ .onSet(async value => this.internalVariableUpdate(value))
108
+ this.cacheVariable = this.service.getCharacteristic(this.variableChar).value
109
+ }
110
+
111
+ // A control the device told us about can be driven through Govee's own api
112
+ // by name, without knowing this model's command bytes. So unlike the
113
+ // settings above, these work rather than asking to be reported.
114
+ this.addDiscoveredToggle('swing', 'SwingMode', 'oscillationToggle')
115
+ this.addDiscoveredToggle('lock', 'LockPhysicalControls', 'lockToggle')
116
+
117
+ // Output the customised options to the log
118
+ const opts = JSON.stringify({})
119
+ platform.log('[%s] %s %s.', accessory.displayName, platformLang.devInitOpts, opts)
120
+ this.accessory.logDebug(`controls: ${describeSources(resolved.sources)}`)
121
+
122
+ const working = ['on and off', ...this.discoveredControls]
123
+ this.accessory.log(`${working.join(', ')} ${working.length > 1 ? 'are' : 'is'} supported for this model, the ${config.variableLabel || 'other settings'} are not known yet`)
124
+ }
125
+
126
+ /**
127
+ * Offer a control the device said it has, driven through Govee's api by the
128
+ * name the device gave it.
129
+ *
130
+ * This is the one place a model nobody has tested gets something that really
131
+ * works beyond power: the api takes the capability name, so the command bytes
132
+ * for this model never have to be known.
133
+ */
134
+ addDiscoveredToggle(control, characteristic, instance) {
135
+ const char = this.hapChar[characteristic]
136
+
137
+ // The device has to have named this capability itself. Knowing the model
138
+ // usually has a turning head is not enough - the api is driven by the name
139
+ // the device reported, so without it there is nothing to send, and the
140
+ // switch would fail every time it was touched.
141
+ const reported = !!this.accessory.context.openApiCapabilities?.[instance]
142
+
143
+ if (!reported || !this.controls[control] || !char) {
144
+ if (this.service.testCharacteristic?.(char)) {
145
+ this.service.removeCharacteristic(this.service.getCharacteristic(char))
146
+ }
147
+ return
148
+ }
149
+
150
+ this.service
151
+ .getCharacteristic(char)
152
+ .onSet(async value => this.internalToggleUpdate(control, char, instance, value))
153
+ this.cacheToggles[control] = this.service.getCharacteristic(char).value
154
+
155
+ this.discoveredControls.push(control === 'swing' ? 'oscillation' : 'the child lock')
156
+ }
157
+
158
+ async internalToggleUpdate(control, characteristic, instance, value) {
159
+ const previous = this.cacheToggles[control]
160
+ try {
161
+ if (previous === value) {
162
+ return
163
+ }
164
+
165
+ await this.platform.sendDeviceUpdate(this.accessory, {
166
+ cmd: 'openApi',
167
+ openApi: {
168
+ instance,
169
+ capabilityType: 'devices.capabilities.toggle',
170
+ value: value ? 1 : 0,
171
+ },
172
+ })
173
+
174
+ this.cacheToggles[control] = value
175
+ this.accessory.log(`current ${control} [${value ? 'on' : 'off'}]`)
176
+ } catch (err) {
177
+ this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`)
178
+ setTimeout(() => {
179
+ this.service.updateCharacteristic(characteristic, previous)
180
+ }, 2000)
181
+ throw new this.hapErr(-70402)
182
+ }
183
+ }
184
+
185
+ async internalStateUpdate(value) {
186
+ try {
187
+ const newValue = value ? 'on' : 'off'
188
+
189
+ // Don't continue if the new value is the same as before
190
+ if (this.cacheState === newValue) {
191
+ return
192
+ }
193
+
194
+ // This power command is the same for every govee appliance, so it is safe
195
+ // to send for a model that has not been tested
196
+ await this.platform.sendDeviceUpdate(this.accessory, {
197
+ cmd: 'ptReal',
198
+ value: value ? 'MwEBAAAAAAAAAAAAAAAAAAAAADM=' : 'MwEAAAAAAAAAAAAAAAAAAAAAADI=',
199
+ openApi: this.accessory.context.openApiCapabilities?.powerSwitch
200
+ ? { instance: 'powerSwitch', capabilityType: 'devices.capabilities.on_off', value: value ? 1 : 0 }
201
+ : undefined,
202
+ })
203
+
204
+ // Cache the new state and log if appropriate
205
+ this.cacheState = newValue
206
+ if (this.currentStateChar) {
207
+ this.service.updateCharacteristic(
208
+ this.currentStateChar,
209
+ value ? this.config.currentStateOn : this.config.currentStateOff,
210
+ )
211
+ }
212
+ this.accessory.log(`${platformLang.curState} [${newValue}]`)
213
+ } catch (err) {
214
+ // Catch any errors during the process
215
+ this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`)
216
+
217
+ // Throw a 'no response' error and set a timeout to revert this after 2 seconds
218
+ setTimeout(() => {
219
+ this.service.updateCharacteristic(this.hapChar.Active, this.cacheState === 'on' ? 1 : 0)
220
+ }, 2000)
221
+ throw new this.hapErr(-70402)
222
+ }
223
+ }
224
+
225
+ async internalVariableUpdate(value) {
226
+ // 0 is the control following the device being turned off, not a request
227
+ if (value === 0) {
228
+ return
229
+ }
230
+
231
+ reportUnsupportedControl(this.accessory, {
232
+ control: this.config.variableLabel || 'setting',
233
+ value,
234
+ })
235
+
236
+ // Put the control back, and fail the request so HomeKit shows it as not
237
+ // responding - that is what sends the owner to the log
238
+ setTimeout(() => {
239
+ this.service.updateCharacteristic(this.variableChar, this.cacheVariable)
240
+ }, 2000)
241
+ throw new this.hapErr(-70402)
242
+ }
243
+
244
+ externalUpdate(params) {
245
+ // Feed the sensor reading through when the richer tile is in use - HomeKit
246
+ // needs it, and it is the reason that tile was chosen
247
+ if (this.usingRichService && this.config.richReadingChar && params.humidity !== undefined) {
248
+ const reading = Number(params.humidity)
249
+ if (Number.isFinite(reading) && reading !== this.cacheReading) {
250
+ this.cacheReading = reading
251
+ this.service.updateCharacteristic(this.hapChar[this.config.richReadingChar], reading)
252
+ }
253
+ }
254
+
255
+ // Power is the one thing that can be read back with confidence
256
+ if (params.state && params.state !== this.cacheState) {
257
+ this.cacheState = params.state
258
+ this.service.updateCharacteristic(this.hapChar.Active, this.cacheState === 'on' ? 1 : 0)
259
+ if (this.currentStateChar) {
260
+ this.service.updateCharacteristic(
261
+ this.currentStateChar,
262
+ this.cacheState === 'on' ? this.config.currentStateOn : this.config.currentStateOff,
263
+ )
264
+ }
265
+ this.accessory.log(`${platformLang.curState} [${this.cacheState}]`)
266
+ }
267
+
268
+ // Everything else gets reported so the model can be finished off
269
+ ;(params.commands || []).forEach((command) => {
270
+ const hexString = base64ToHex(command)
271
+ const hexParts = hexToTwoItems(hexString)
272
+
273
+ if (getTwoItemPosition(hexParts, 1) !== 'aa') {
274
+ return
275
+ }
276
+
277
+ // The power report is understood, anything else is worth sending in
278
+ if (getTwoItemPosition(hexParts, 2) === '01') {
279
+ const newState = getTwoItemPosition(hexParts, 3) === '01' ? 'on' : 'off'
280
+ if (this.cacheState !== newState) {
281
+ this.cacheState = newState
282
+ this.service.updateCharacteristic(this.hapChar.Active, this.cacheState === 'on' ? 1 : 0)
283
+ this.accessory.log(`${platformLang.curState} [${this.cacheState}]`)
284
+ }
285
+ return
286
+ }
287
+
288
+ logUnknownData(this.accessory, {
289
+ kind: 'scene',
290
+ source: params.source,
291
+ raw: command,
292
+ hex: hexString,
293
+ })
294
+ })
295
+ }
296
+ }
297
+
298
+ /**
299
+ * Build a basic handler for a kind of appliance.
300
+ *
301
+ * @param {object} config how this kind of appliance should appear
302
+ * @returns {Function} a handler class the platform can construct
303
+ */
304
+ export function makeBasicAppliance(config) {
305
+ return class extends ApplianceBasic {
306
+ constructor(platform, accessory) {
307
+ super(platform, accessory, config)
308
+ }
309
+ }
310
+ }
311
+
312
+ // Fan-shaped appliances: a speed slider that is not worked out yet
313
+ export const deviceFanBasic = makeBasicAppliance({
314
+ family: 'fan',
315
+ service: 'Fanv2',
316
+ variableChar: 'RotationSpeed',
317
+ variableLabel: 'fan speed',
318
+ })
319
+
320
+ // Ice makers reuse the fan tile, with the slider standing in for ice size
321
+ export const deviceIceMakerBasic = makeBasicAppliance({
322
+ family: 'iceMaker',
323
+ service: 'Fanv2',
324
+ variableChar: 'RotationSpeed',
325
+ variableLabel: 'ice size',
326
+ })
327
+
328
+ // Humidifiers get the plain fan tile by default, because HomeKit's humidifier
329
+ // has to show a current humidity and many Govee humidifiers never report one.
330
+ // A model that does report it gets the proper humidifier tile instead.
331
+ export const deviceHumidifierBasic = makeBasicAppliance({
332
+ family: 'humidifier',
333
+ service: 'Fan',
334
+ variableChar: 'RotationSpeed',
335
+ variableLabel: 'mist level',
336
+ richService: 'HumidifierDehumidifier',
337
+ richControl: 'humiditySensor',
338
+ richVariableChar: 'RelativeHumidityHumidifierThreshold',
339
+ richReadingChar: 'CurrentRelativeHumidity',
340
+ currentStateChar: 'CurrentHumidifierDehumidifierState',
341
+ currentStateOn: 2, // humidifying
342
+ currentStateOff: 0, // inactive
343
+ })
344
+
345
+ export const deviceDehumidifierBasic = makeBasicAppliance({
346
+ family: 'dehumidifier',
347
+ service: 'Fan',
348
+ variableChar: 'RotationSpeed',
349
+ variableLabel: 'fan speed',
350
+ richService: 'HumidifierDehumidifier',
351
+ richControl: 'humiditySensor',
352
+ richVariableChar: 'RelativeHumidityDehumidifierThreshold',
353
+ richReadingChar: 'CurrentRelativeHumidity',
354
+ currentStateChar: 'CurrentHumidifierDehumidifierState',
355
+ currentStateOn: 3, // dehumidifying
356
+ currentStateOff: 0, // inactive
357
+ })
358
+
359
+ // Diffusers are power only, so there is no variable control to offer. They use
360
+ // the same tile as the models already handled, so a new one looks the same
361
+ export const deviceDiffuserBasic = makeBasicAppliance({
362
+ family: 'diffuser',
363
+ service: 'AirPurifier',
364
+ currentStateChar: 'CurrentAirPurifierState',
365
+ currentStateOn: 2, // purifying air
366
+ currentStateOff: 0, // inactive
367
+ })
368
+
369
+ export const devicePurifierBasic = makeBasicAppliance({
370
+ family: 'purifier',
371
+ service: 'AirPurifier',
372
+ variableChar: 'RotationSpeed',
373
+ variableLabel: 'fan speed',
374
+ currentStateChar: 'CurrentAirPurifierState',
375
+ currentStateOn: 2, // purifying air
376
+ currentStateOff: 0, // inactive
377
+ })
@@ -5,9 +5,10 @@ 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
- H7150
11
+ H7150, H7151 and H7152
11
12
  {
12
13
  "mode": {
13
14
  "options": [
@@ -31,7 +32,7 @@ import platformLang from '../utils/lang-en.js'
31
32
  }
32
33
  }
33
34
  */
34
- export default class {
35
+ export default class GoveeDehumidifierH7150 {
35
36
  constructor(platform, accessory) {
36
37
  // Set up variables from the platform
37
38
  this.hapChar = platform.api.hap.Characteristic
@@ -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) {
@@ -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
  })
@@ -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
  })
@@ -1,3 +1,4 @@
1
+ import { getDeviceCapabilities } from '../utils/device-capabilities.js'
1
2
  import {
2
3
  base64ToHex,
3
4
  fanSpeedToHkPercent,
@@ -7,8 +8,19 @@ import {
7
8
  parseError,
8
9
  } from '../utils/functions.js'
9
10
  import platformLang from '../utils/lang-en.js'
11
+ import { logUnknownData } from '../utils/report-unknown.js'
10
12
 
11
- export default class {
13
+ /*
14
+ The models on this handler and the modes each offers:
15
+
16
+ H7100, H7102, H7106 -> Sleep, Nature, Custom, Auto
17
+ H7101 -> Sleep, Nature, Turbo, Custom, Auto
18
+ H7111 -> Sleep, Nature, Custom, Auto
19
+ H1310, R1310 -> as the H7100, but its oscillation status message has
20
+ never been confirmed, so it is not read back
21
+ */
22
+
23
+ export default class GoveeFanH7100 {
12
24
  constructor(platform, accessory) {
13
25
  // Set up variables from the platform
14
26
  this.hapChar = platform.api.hap.Characteristic
@@ -19,6 +31,10 @@ export default class {
19
31
  // Set up variables from the accessory
20
32
  this.accessory = accessory
21
33
 
34
+ // Which of the models on this handler have a confirmed meaning for the
35
+ // oscillation status message
36
+ this.readsSwingStatus = getDeviceCapabilities(accessory.context.gvModel).readsSwingStatus
37
+
22
38
  // Codes etc
23
39
  this.speedCodes = {
24
40
  1: 'MwUBAQAAAAAAAAAAAAAAAAAAADY=',
@@ -261,12 +277,23 @@ export default class {
261
277
  this.cacheSpeed = this.autoSpeed
262
278
  // Cache auto so we can tell it apart from "user asked for max speed"
263
279
  this.service.updateCharacteristic(this.hapChar.RotationSpeed, 100)
264
- this.accessory.log(`${platformLang.curSpeed} [auto]`)
280
+ this.accessory.log(`${platformLang.curSpeed} [auto -> 100%]`)
265
281
  }
266
282
  break
267
283
  }
268
284
  case '1f01': {
269
- // Swing Mode
285
+ // Not every model on this handler is known to mean oscillation by
286
+ // this message, so one that has not been confirmed falls through and
287
+ // reports it instead - which is how it gets confirmed
288
+ if (!this.readsSwingStatus) {
289
+ logUnknownData(this.accessory, {
290
+ kind: 'scene',
291
+ source: params.source,
292
+ raw: command,
293
+ hex: hexString,
294
+ })
295
+ break
296
+ }
270
297
  const newSwing = getTwoItemPosition(hexParts, 4) === '01' ? 'on' : 'off'
271
298
  if (this.cacheSwing !== newSwing) {
272
299
  this.cacheSwing = newSwing
@@ -276,7 +303,12 @@ export default class {
276
303
  break
277
304
  }
278
305
  default:
279
- this.accessory.logDebugWarn(`${platformLang.newScene}: [${command}] [${hexString}]`)
306
+ logUnknownData(this.accessory, {
307
+ kind: 'scene',
308
+ source: params.source,
309
+ raw: command,
310
+ hex: hexString,
311
+ })
280
312
  break
281
313
  }
282
314
  })
@@ -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
  })