@homebridge-plugins/homebridge-govee 11.32.0-beta.2 → 11.32.0-beta.20

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
@@ -7,9 +7,18 @@ All notable changes to `@homebridge-plugins/homebridge-govee` will be documented
7
7
  ### Changed
8
8
 
9
9
  - chore(github): use the shared homebridge action to deprecate past pre-releases
10
- - feat(ui): add and remove devices from the config via the my devices tab (#1323)
11
10
  - feat(ui): hide or show a device in homekit from the my devices tab
12
- - style(ui): present the device config actions as a settings-style list
11
+ - chore(deps): dependency updates
12
+ - style(ui): standardise the custom ui layout and sync the support tab with the readme
13
+ - chore(deps): override help-me to v5 to clear the high brace-expansion security advisory from the aws-iot dependency chain
14
+ - feat(ui): add a remove all devices action to the my devices tab (#1323)
15
+ - feat: add support for the H5089 smart plug, exposing its outlet and built-in light as separate switches (#1323)
16
+ - chore: declare the supports-hap transport keyword for the homebridge ui
17
+ - feat: add support for the H60B3 uplighter as an rgb light (#1325)
18
+ - feat: add support for the H5901 water timer through the govee openapi (#1324)
19
+ - fix(fans): map rotation speed as a percentage on every fan model, so homekit stops showing speeds like 7% and the top of the slider means full power (#1326)
20
+ - chore: log what each device can do according to the govee openapi, in debug mode, to make working out new device support quicker
21
+ - chore: decode the status codes a device reports into readable hex in debug mode, so support for new device features can be worked out from a normal log
13
22
 
14
23
  ## v11.31.0 (2026-07-24)
15
24
 
@@ -1,7 +1,9 @@
1
1
  import {
2
2
  base64ToHex,
3
+ fanSpeedToHkPercent,
3
4
  getTwoItemPosition,
4
5
  hexToTwoItems,
6
+ hkPercentToFanSpeed,
5
7
  parseError,
6
8
  } from '../utils/functions.js'
7
9
  import platformLang from '../utils/lang-en.js'
@@ -30,6 +32,8 @@ export default class {
30
32
  9: 'MwUAAwAAAAAAAAAAAAAAAAAAADU=', // auto mode
31
33
  }
32
34
  this.autoSpeed = 9
35
+ // Real speed steps, excluding auto - HomeKit percentages map onto these
36
+ this.speedSteps = 8
33
37
 
34
38
  // Remove any old original Fan services
35
39
  if (this.accessory.getService(this.hapServ.Fan)) {
@@ -57,7 +61,7 @@ export default class {
57
61
  this.service
58
62
  .getCharacteristic(this.hapChar.RotationSpeed)
59
63
  .setProps({
60
- maxValue: this.autoSpeed,
64
+ maxValue: 100,
61
65
  minStep: 1,
62
66
  minValue: 0,
63
67
  unit: 'unitless',
@@ -115,6 +119,9 @@ export default class {
115
119
  return
116
120
  }
117
121
 
122
+ // HomeKit sends a percentage - map it onto this model's real speed steps
123
+ value = hkPercentToFanSpeed(value, this.speedSteps)
124
+
118
125
  // Don't continue if the new value is the same as before
119
126
  if (this.cacheSpeed === value) {
120
127
  return
@@ -142,7 +149,7 @@ export default class {
142
149
 
143
150
  // Throw a 'no response' error and set a timeout to revert this after 2 seconds
144
151
  setTimeout(() => {
145
- this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSpeed)
152
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, fanSpeedToHkPercent(this.cacheSpeed, this.speedSteps, this.autoSpeed))
146
153
  }, 2000)
147
154
  throw new this.hapErr(-70402)
148
155
  }
@@ -199,8 +206,9 @@ export default class {
199
206
  }
200
207
  if (Number.isFinite(newSpeed) && newSpeed >= 1 && newSpeed <= this.autoSpeed && this.cacheSpeed !== newSpeed) {
201
208
  this.cacheSpeed = newSpeed
202
- this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSpeed)
203
- this.accessory.log(`${platformLang.curSpeed} [${this.cacheSpeed}]`)
209
+ const hkSpeed = fanSpeedToHkPercent(newSpeed, this.speedSteps, this.autoSpeed)
210
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, hkSpeed)
211
+ this.accessory.log(`${platformLang.curSpeed} [${newSpeed} -> ${hkSpeed}%]`)
204
212
  }
205
213
  }
206
214
 
@@ -237,10 +245,11 @@ export default class {
237
245
  case '0501': {
238
246
  // Fan speed
239
247
  const newSpeed = Number.parseInt(getTwoItemPosition(hexParts, 4), 16)
240
- if (newSpeed >= 1 && newSpeed <= 8 && this.cacheSpeed !== newSpeed) {
248
+ if (newSpeed >= 1 && newSpeed <= this.speedSteps && this.cacheSpeed !== newSpeed) {
241
249
  this.cacheSpeed = newSpeed
242
- this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSpeed)
243
- this.accessory.log(`${platformLang.curSpeed} [${this.cacheSpeed}]`)
250
+ const hkSpeed = fanSpeedToHkPercent(newSpeed, this.speedSteps, this.autoSpeed)
251
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, hkSpeed)
252
+ this.accessory.log(`${platformLang.curSpeed} [${newSpeed} -> ${hkSpeed}%]`)
244
253
  }
245
254
  break
246
255
  }
@@ -248,7 +257,8 @@ export default class {
248
257
  // Mode change (auto = 0x03)
249
258
  if (getTwoItemPosition(hexParts, 4) === '03' && this.cacheSpeed !== this.autoSpeed) {
250
259
  this.cacheSpeed = this.autoSpeed
251
- this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSpeed)
260
+ // Cache auto so we can tell it apart from "user asked for max speed"
261
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, 100)
252
262
  this.accessory.log(`${platformLang.curSpeed} [auto]`)
253
263
  }
254
264
  break
@@ -1,9 +1,11 @@
1
1
  import {
2
2
  base64ToHex,
3
+ fanSpeedToHkPercent,
3
4
  generateCodeFromHexValues,
4
5
  generateRandomString,
5
6
  getTwoItemPosition,
6
7
  hexToTwoItems,
8
+ hkPercentToFanSpeed,
7
9
  parseError,
8
10
  sleep,
9
11
  } from '../utils/functions.js'
@@ -41,11 +43,14 @@ export default class {
41
43
  .onSet(async value => this.internalStateUpdate(value))
42
44
  this.cacheState = this.service.getCharacteristic(this.hapChar.Active).value ? 'on' : 'off'
43
45
 
46
+ // Real speed steps - HomeKit percentages map onto these
47
+ this.speedSteps = 12
48
+
44
49
  // Add the set handler to the fan rotation speed characteristic
45
50
  this.service
46
51
  .getCharacteristic(this.hapChar.RotationSpeed)
47
52
  .setProps({
48
- maxValue: 12,
53
+ maxValue: 100,
49
54
  minStep: 1,
50
55
  minValue: 0,
51
56
  unit: 'unitless',
@@ -140,6 +145,9 @@ export default class {
140
145
  return
141
146
  }
142
147
 
148
+ // HomeKit sends a percentage - map it onto this model's real speed steps
149
+ value = hkPercentToFanSpeed(value, this.speedSteps)
150
+
143
151
  // Don't continue if the new value is the same as before
144
152
  if (this.cacheSpeed === value) {
145
153
  return
@@ -162,7 +170,7 @@ export default class {
162
170
 
163
171
  // Throw a 'no response' error and set a timeout to revert this after 2 seconds
164
172
  setTimeout(() => {
165
- this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSpeed)
173
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, fanSpeedToHkPercent(this.cacheSpeed, this.speedSteps))
166
174
  }, 2000)
167
175
  throw new this.hapErr(-70402)
168
176
  }
@@ -320,10 +328,11 @@ export default class {
320
328
  case '3101': {
321
329
  // Fan speed
322
330
  const newSpeed = Number.parseInt(getTwoItemPosition(hexParts, 4), 16)
323
- if (newSpeed >= 1 && newSpeed <= 12 && this.cacheSpeed !== newSpeed) {
331
+ if (newSpeed >= 1 && newSpeed <= this.speedSteps && this.cacheSpeed !== newSpeed) {
324
332
  this.cacheSpeed = newSpeed
325
- this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSpeed)
326
- this.accessory.log(`${platformLang.curSpeed} [${this.cacheSpeed}]`)
333
+ const hkSpeed = fanSpeedToHkPercent(newSpeed, this.speedSteps)
334
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, hkSpeed)
335
+ this.accessory.log(`${platformLang.curSpeed} [${newSpeed} -> ${hkSpeed}%]`)
327
336
  }
328
337
  break
329
338
  }
@@ -1,7 +1,9 @@
1
1
  import {
2
2
  base64ToHex,
3
+ fanSpeedToHkPercent,
3
4
  getTwoItemPosition,
4
5
  hexToTwoItems,
6
+ hkPercentToFanSpeed,
5
7
  parseError,
6
8
  } from '../utils/functions.js'
7
9
  import platformLang from '../utils/lang-en.js'
@@ -30,6 +32,8 @@ export default class {
30
32
  9: 'MwUAAwAAAAAAAAAAAAAAAAAAADU=', // auto mode
31
33
  }
32
34
  this.autoSpeed = 9
35
+ // Real speed steps, excluding auto - HomeKit percentages map onto these
36
+ this.speedSteps = 8
33
37
 
34
38
  // Remove any old original Fan services
35
39
  if (this.accessory.getService(this.hapServ.Fan)) {
@@ -57,7 +61,7 @@ export default class {
57
61
  this.service
58
62
  .getCharacteristic(this.hapChar.RotationSpeed)
59
63
  .setProps({
60
- maxValue: this.autoSpeed,
64
+ maxValue: 100,
61
65
  minStep: 1,
62
66
  minValue: 0,
63
67
  unit: 'unitless',
@@ -115,6 +119,9 @@ export default class {
115
119
  return
116
120
  }
117
121
 
122
+ // HomeKit sends a percentage - map it onto this model's real speed steps
123
+ value = hkPercentToFanSpeed(value, this.speedSteps)
124
+
118
125
  // Don't continue if the new value is the same as before
119
126
  if (this.cacheSpeed === value) {
120
127
  return
@@ -142,7 +149,7 @@ export default class {
142
149
 
143
150
  // Throw a 'no response' error and set a timeout to revert this after 2 seconds
144
151
  setTimeout(() => {
145
- this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSpeed)
152
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, fanSpeedToHkPercent(this.cacheSpeed, this.speedSteps, this.autoSpeed))
146
153
  }, 2000)
147
154
  throw new this.hapErr(-70402)
148
155
  }
@@ -199,8 +206,9 @@ export default class {
199
206
  }
200
207
  if (Number.isFinite(newSpeed) && newSpeed >= 1 && newSpeed <= this.autoSpeed && this.cacheSpeed !== newSpeed) {
201
208
  this.cacheSpeed = newSpeed
202
- this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSpeed)
203
- this.accessory.log(`${platformLang.curSpeed} [${this.cacheSpeed}]`)
209
+ const hkSpeed = fanSpeedToHkPercent(newSpeed, this.speedSteps, this.autoSpeed)
210
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, hkSpeed)
211
+ this.accessory.log(`${platformLang.curSpeed} [${newSpeed} -> ${hkSpeed}%]`)
204
212
  }
205
213
  }
206
214
 
@@ -237,10 +245,11 @@ export default class {
237
245
  case '0501': {
238
246
  // Fan speed
239
247
  const newSpeed = Number.parseInt(getTwoItemPosition(hexParts, 4), 16)
240
- if (newSpeed >= 1 && newSpeed <= 8 && this.cacheSpeed !== newSpeed) {
248
+ if (newSpeed >= 1 && newSpeed <= this.speedSteps && this.cacheSpeed !== newSpeed) {
241
249
  this.cacheSpeed = newSpeed
242
- this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSpeed)
243
- this.accessory.log(`${platformLang.curSpeed} [${this.cacheSpeed}]`)
250
+ const hkSpeed = fanSpeedToHkPercent(newSpeed, this.speedSteps, this.autoSpeed)
251
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, hkSpeed)
252
+ this.accessory.log(`${platformLang.curSpeed} [${newSpeed} -> ${hkSpeed}%]`)
244
253
  }
245
254
  break
246
255
  }
@@ -248,7 +257,8 @@ export default class {
248
257
  // Mode change (auto = 0x03)
249
258
  if (getTwoItemPosition(hexParts, 4) === '03' && this.cacheSpeed !== this.autoSpeed) {
250
259
  this.cacheSpeed = this.autoSpeed
251
- this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSpeed)
260
+ // Cache auto so we can tell it apart from "user asked for max speed"
261
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, 100)
252
262
  this.accessory.log(`${platformLang.curSpeed} [auto]`)
253
263
  }
254
264
  break
@@ -1,7 +1,9 @@
1
1
  import {
2
2
  base64ToHex,
3
+ fanSpeedToHkPercent,
3
4
  getTwoItemPosition,
4
5
  hexToTwoItems,
6
+ hkPercentToFanSpeed,
5
7
  parseError,
6
8
  } from '../utils/functions.js'
7
9
  import platformLang from '../utils/lang-en.js'
@@ -86,6 +88,8 @@ export default class {
86
88
  9: 'MwUAAwAAAAAAAAAAAAAAAAAAADU=', // auto mode
87
89
  }
88
90
  this.autoSpeed = 9
91
+ // Real speed steps, excluding auto - HomeKit percentages map onto these
92
+ this.speedSteps = 8
89
93
 
90
94
  // Remove any old original Fan services
91
95
  if (this.accessory.getService(this.hapServ.Fan)) {
@@ -113,7 +117,7 @@ export default class {
113
117
  this.service
114
118
  .getCharacteristic(this.hapChar.RotationSpeed)
115
119
  .setProps({
116
- maxValue: this.autoSpeed,
120
+ maxValue: 100,
117
121
  minStep: 1,
118
122
  minValue: 0,
119
123
  unit: 'unitless',
@@ -171,6 +175,9 @@ export default class {
171
175
  return
172
176
  }
173
177
 
178
+ // HomeKit sends a percentage - map it onto this model's real speed steps
179
+ value = hkPercentToFanSpeed(value, this.speedSteps)
180
+
174
181
  // Don't continue if the new value is the same as before
175
182
  if (this.cacheSpeed === value) {
176
183
  return
@@ -198,7 +205,7 @@ export default class {
198
205
 
199
206
  // Throw a 'no response' error and set a timeout to revert this after 2 seconds
200
207
  setTimeout(() => {
201
- this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSpeed)
208
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, fanSpeedToHkPercent(this.cacheSpeed, this.speedSteps, this.autoSpeed))
202
209
  }, 2000)
203
210
  throw new this.hapErr(-70402)
204
211
  }
@@ -255,8 +262,9 @@ export default class {
255
262
  }
256
263
  if (Number.isFinite(newSpeed) && newSpeed >= 1 && newSpeed <= this.autoSpeed && this.cacheSpeed !== newSpeed) {
257
264
  this.cacheSpeed = newSpeed
258
- this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSpeed)
259
- this.accessory.log(`${platformLang.curSpeed} [${this.cacheSpeed}]`)
265
+ const hkSpeed = fanSpeedToHkPercent(newSpeed, this.speedSteps, this.autoSpeed)
266
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, hkSpeed)
267
+ this.accessory.log(`${platformLang.curSpeed} [${newSpeed} -> ${hkSpeed}%]`)
260
268
  }
261
269
  }
262
270
 
@@ -293,10 +301,11 @@ export default class {
293
301
  case '0501': {
294
302
  // Fan speed
295
303
  const newSpeed = Number.parseInt(getTwoItemPosition(hexParts, 4), 16)
296
- if (newSpeed >= 1 && newSpeed <= 8 && this.cacheSpeed !== newSpeed) {
304
+ if (newSpeed >= 1 && newSpeed <= this.speedSteps && this.cacheSpeed !== newSpeed) {
297
305
  this.cacheSpeed = newSpeed
298
- this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSpeed)
299
- this.accessory.log(`${platformLang.curSpeed} [${this.cacheSpeed}]`)
306
+ const hkSpeed = fanSpeedToHkPercent(newSpeed, this.speedSteps, this.autoSpeed)
307
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, hkSpeed)
308
+ this.accessory.log(`${platformLang.curSpeed} [${newSpeed} -> ${hkSpeed}%]`)
300
309
  }
301
310
  break
302
311
  }
@@ -304,7 +313,8 @@ export default class {
304
313
  // Mode change (auto = 0x03)
305
314
  if (getTwoItemPosition(hexParts, 4) === '03' && this.cacheSpeed !== this.autoSpeed) {
306
315
  this.cacheSpeed = this.autoSpeed
307
- this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSpeed)
316
+ // Cache auto so we can tell it apart from "user asked for max speed"
317
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, 100)
308
318
  this.accessory.log(`${platformLang.curSpeed} [auto]`)
309
319
  }
310
320
  break
@@ -1,7 +1,9 @@
1
1
  import {
2
2
  base64ToHex,
3
+ fanSpeedToHkPercent,
3
4
  getTwoItemPosition,
4
5
  hexToTwoItems,
6
+ hkPercentToFanSpeed,
5
7
  parseError,
6
8
  } from '../utils/functions.js'
7
9
  import platformLang from '../utils/lang-en.js'
@@ -30,6 +32,8 @@ export default class {
30
32
  9: 'MwUAAwAAAAAAAAAAAAAAAAAAADU=', // auto mode
31
33
  }
32
34
  this.autoSpeed = 9
35
+ // Real speed steps, excluding auto - HomeKit percentages map onto these
36
+ this.speedSteps = 8
33
37
 
34
38
  // Remove any old original Fan services
35
39
  if (this.accessory.getService(this.hapServ.Fan)) {
@@ -57,7 +61,7 @@ export default class {
57
61
  this.service
58
62
  .getCharacteristic(this.hapChar.RotationSpeed)
59
63
  .setProps({
60
- maxValue: this.autoSpeed,
64
+ maxValue: 100,
61
65
  minStep: 1,
62
66
  minValue: 0,
63
67
  unit: 'unitless',
@@ -115,6 +119,9 @@ export default class {
115
119
  return
116
120
  }
117
121
 
122
+ // HomeKit sends a percentage - map it onto this model's real speed steps
123
+ value = hkPercentToFanSpeed(value, this.speedSteps)
124
+
118
125
  // Don't continue if the new value is the same as before
119
126
  if (this.cacheSpeed === value) {
120
127
  return
@@ -142,7 +149,7 @@ export default class {
142
149
 
143
150
  // Throw a 'no response' error and set a timeout to revert this after 2 seconds
144
151
  setTimeout(() => {
145
- this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSpeed)
152
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, fanSpeedToHkPercent(this.cacheSpeed, this.speedSteps, this.autoSpeed))
146
153
  }, 2000)
147
154
  throw new this.hapErr(-70402)
148
155
  }
@@ -199,8 +206,9 @@ export default class {
199
206
  }
200
207
  if (Number.isFinite(newSpeed) && newSpeed >= 1 && newSpeed <= this.autoSpeed && this.cacheSpeed !== newSpeed) {
201
208
  this.cacheSpeed = newSpeed
202
- this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSpeed)
203
- this.accessory.log(`${platformLang.curSpeed} [${this.cacheSpeed}]`)
209
+ const hkSpeed = fanSpeedToHkPercent(newSpeed, this.speedSteps, this.autoSpeed)
210
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, hkSpeed)
211
+ this.accessory.log(`${platformLang.curSpeed} [${newSpeed} -> ${hkSpeed}%]`)
204
212
  }
205
213
  }
206
214
 
@@ -237,10 +245,11 @@ export default class {
237
245
  case '0501': {
238
246
  // Fan speed
239
247
  const newSpeed = Number.parseInt(getTwoItemPosition(hexParts, 4), 16)
240
- if (newSpeed >= 1 && newSpeed <= 8 && this.cacheSpeed !== newSpeed) {
248
+ if (newSpeed >= 1 && newSpeed <= this.speedSteps && this.cacheSpeed !== newSpeed) {
241
249
  this.cacheSpeed = newSpeed
242
- this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSpeed)
243
- this.accessory.log(`${platformLang.curSpeed} [${this.cacheSpeed}]`)
250
+ const hkSpeed = fanSpeedToHkPercent(newSpeed, this.speedSteps, this.autoSpeed)
251
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, hkSpeed)
252
+ this.accessory.log(`${platformLang.curSpeed} [${newSpeed} -> ${hkSpeed}%]`)
244
253
  }
245
254
  break
246
255
  }
@@ -248,7 +257,8 @@ export default class {
248
257
  // Mode change (auto = 0x03)
249
258
  if (getTwoItemPosition(hexParts, 4) === '03' && this.cacheSpeed !== this.autoSpeed) {
250
259
  this.cacheSpeed = this.autoSpeed
251
- this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSpeed)
260
+ // Cache auto so we can tell it apart from "user asked for max speed"
261
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, 100)
252
262
  this.accessory.log(`${platformLang.curSpeed} [auto]`)
253
263
  }
254
264
  break
@@ -1,11 +1,13 @@
1
1
  import { hs2rgb, rgb2hs } from '../utils/colour.js'
2
2
  import {
3
3
  base64ToHex,
4
+ fanSpeedToHkPercent,
4
5
  generateCodeFromHexValues,
5
6
  generateRandomString,
6
7
  getTwoItemPosition,
7
8
  hexToDecimal,
8
9
  hexToTwoItems,
10
+ hkPercentToFanSpeed,
9
11
  parseError,
10
12
  sleep,
11
13
  } from '../utils/functions.js'
@@ -43,6 +45,8 @@ export default class {
43
45
  13: 'MwUAAgAAAAAAAAAAAAAAAAAAADQ=', // auto mode
44
46
  }
45
47
  this.autoSpeed = 13
48
+ // Real speed steps, excluding auto - HomeKit percentages map onto these
49
+ this.speedSteps = 12
46
50
 
47
51
  // Remove any old original Fan services
48
52
  if (this.accessory.getService(this.hapServ.Fan)) {
@@ -70,7 +74,7 @@ export default class {
70
74
  this.service
71
75
  .getCharacteristic(this.hapChar.RotationSpeed)
72
76
  .setProps({
73
- maxValue: this.autoSpeed,
77
+ maxValue: 100,
74
78
  minStep: 1,
75
79
  minValue: 0,
76
80
  unit: 'unitless', // This is actually from HAP for Bluetooth LE Specification, but fits
@@ -157,8 +161,16 @@ export default class {
157
161
 
158
162
  async internalSpeedUpdate(value) {
159
163
  try {
164
+ // Don't continue if the fan is being turned off
165
+ if (value === 0) {
166
+ return
167
+ }
168
+
169
+ // HomeKit sends a percentage - map it onto this model's real speed steps
170
+ value = hkPercentToFanSpeed(value, this.speedSteps)
171
+
160
172
  // Don't continue if the new value is the same as before
161
- if (this.cacheSpeed === value || value === 0) {
173
+ if (this.cacheSpeed === value) {
162
174
  return
163
175
  }
164
176
 
@@ -182,7 +194,7 @@ export default class {
182
194
 
183
195
  // Throw a 'no response' error and set a timeout to revert this after 2 seconds
184
196
  setTimeout(() => {
185
- this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSpeed)
197
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, fanSpeedToHkPercent(this.cacheSpeed, this.speedSteps, this.autoSpeed))
186
198
  }, 2000)
187
199
  throw new this.hapErr(-70402)
188
200
  }
@@ -380,8 +392,9 @@ export default class {
380
392
  }
381
393
  if (Number.isFinite(newSpeed) && newSpeed >= 1 && newSpeed <= this.autoSpeed && this.cacheSpeed !== newSpeed) {
382
394
  this.cacheSpeed = newSpeed
383
- this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSpeed)
384
- this.accessory.log(`${platformLang.curSpeed} [${this.cacheSpeed}]`)
395
+ const hkSpeed = fanSpeedToHkPercent(newSpeed, this.speedSteps, this.autoSpeed)
396
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, hkSpeed)
397
+ this.accessory.log(`${platformLang.curSpeed} [${newSpeed} -> ${hkSpeed}%]`)
385
398
  }
386
399
  }
387
400
 
@@ -430,7 +443,8 @@ export default class {
430
443
  // Mode change (auto = 0x02)
431
444
  if (getTwoItemPosition(hexParts, 4) === '02' && this.cacheSpeed !== this.autoSpeed) {
432
445
  this.cacheSpeed = this.autoSpeed
433
- this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSpeed)
446
+ // Cache auto so we can tell it apart from "user asked for max speed"
447
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, 100)
434
448
  this.accessory.log(`${platformLang.curSpeed} [auto]`)
435
449
  }
436
450
  break
@@ -1,7 +1,9 @@
1
1
  import {
2
2
  base64ToHex,
3
+ fanSpeedToHkPercent,
3
4
  getTwoItemPosition,
4
5
  hexToTwoItems,
6
+ hkPercentToFanSpeed,
5
7
  parseError,
6
8
  } from '../utils/functions.js'
7
9
  import platformLang from '../utils/lang-en.js'
@@ -30,6 +32,8 @@ export default class {
30
32
  9: 'MwUAAwAAAAAAAAAAAAAAAAAAADU=', // auto mode
31
33
  }
32
34
  this.autoSpeed = 9
35
+ // Real speed steps, excluding auto - HomeKit percentages map onto these
36
+ this.speedSteps = 8
33
37
 
34
38
  // Remove any old original Fan services
35
39
  if (this.accessory.getService(this.hapServ.Fan)) {
@@ -57,7 +61,7 @@ export default class {
57
61
  this.service
58
62
  .getCharacteristic(this.hapChar.RotationSpeed)
59
63
  .setProps({
60
- maxValue: this.autoSpeed,
64
+ maxValue: 100,
61
65
  minStep: 1,
62
66
  minValue: 0,
63
67
  unit: 'unitless',
@@ -115,6 +119,9 @@ export default class {
115
119
  return
116
120
  }
117
121
 
122
+ // HomeKit sends a percentage - map it onto this model's real speed steps
123
+ value = hkPercentToFanSpeed(value, this.speedSteps)
124
+
118
125
  // Don't continue if the new value is the same as before
119
126
  if (this.cacheSpeed === value) {
120
127
  return
@@ -142,7 +149,7 @@ export default class {
142
149
 
143
150
  // Throw a 'no response' error and set a timeout to revert this after 2 seconds
144
151
  setTimeout(() => {
145
- this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSpeed)
152
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, fanSpeedToHkPercent(this.cacheSpeed, this.speedSteps, this.autoSpeed))
146
153
  }, 2000)
147
154
  throw new this.hapErr(-70402)
148
155
  }
@@ -199,8 +206,9 @@ export default class {
199
206
  }
200
207
  if (Number.isFinite(newSpeed) && newSpeed >= 1 && newSpeed <= this.autoSpeed && this.cacheSpeed !== newSpeed) {
201
208
  this.cacheSpeed = newSpeed
202
- this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSpeed)
203
- this.accessory.log(`${platformLang.curSpeed} [${this.cacheSpeed}]`)
209
+ const hkSpeed = fanSpeedToHkPercent(newSpeed, this.speedSteps, this.autoSpeed)
210
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, hkSpeed)
211
+ this.accessory.log(`${platformLang.curSpeed} [${newSpeed} -> ${hkSpeed}%]`)
204
212
  }
205
213
  }
206
214
 
@@ -237,10 +245,11 @@ export default class {
237
245
  case '0501': {
238
246
  // Fan speed
239
247
  const newSpeed = Number.parseInt(getTwoItemPosition(hexParts, 4), 16)
240
- if (newSpeed >= 1 && newSpeed <= 8 && this.cacheSpeed !== newSpeed) {
248
+ if (newSpeed >= 1 && newSpeed <= this.speedSteps && this.cacheSpeed !== newSpeed) {
241
249
  this.cacheSpeed = newSpeed
242
- this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSpeed)
243
- this.accessory.log(`${platformLang.curSpeed} [${this.cacheSpeed}]`)
250
+ const hkSpeed = fanSpeedToHkPercent(newSpeed, this.speedSteps, this.autoSpeed)
251
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, hkSpeed)
252
+ this.accessory.log(`${platformLang.curSpeed} [${newSpeed} -> ${hkSpeed}%]`)
244
253
  }
245
254
  break
246
255
  }
@@ -248,7 +257,8 @@ export default class {
248
257
  // Mode change (auto = 0x03)
249
258
  if (getTwoItemPosition(hexParts, 4) === '03' && this.cacheSpeed !== this.autoSpeed) {
250
259
  this.cacheSpeed = this.autoSpeed
251
- this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSpeed)
260
+ // Cache auto so we can tell it apart from "user asked for max speed"
261
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, 100)
252
262
  this.accessory.log(`${platformLang.curSpeed} [auto]`)
253
263
  }
254
264
  break
@@ -1,11 +1,13 @@
1
1
  import { hs2rgb, rgb2hs } from '../utils/colour.js'
2
2
  import {
3
3
  base64ToHex,
4
+ fanSpeedToHkPercent,
4
5
  generateCodeFromHexValues,
5
6
  generateRandomString,
6
7
  getTwoItemPosition,
7
8
  hexToDecimal,
8
9
  hexToTwoItems,
10
+ hkPercentToFanSpeed,
9
11
  parseError,
10
12
  sleep,
11
13
  } from '../utils/functions.js'
@@ -43,6 +45,8 @@ export default class {
43
45
  13: 'MwUAAgAAAAAAAAAAAAAAAAAAADQ=', // auto mode
44
46
  }
45
47
  this.autoSpeed = 13
48
+ // Real speed steps, excluding auto - HomeKit percentages map onto these
49
+ this.speedSteps = 12
46
50
 
47
51
  // Remove any old original Fan services
48
52
  if (this.accessory.getService(this.hapServ.Fan)) {
@@ -70,7 +74,7 @@ export default class {
70
74
  this.service
71
75
  .getCharacteristic(this.hapChar.RotationSpeed)
72
76
  .setProps({
73
- maxValue: this.autoSpeed,
77
+ maxValue: 100,
74
78
  minStep: 1,
75
79
  minValue: 0,
76
80
  unit: 'unitless', // This is actually from HAP for Bluetooth LE Specification, but fits
@@ -157,8 +161,16 @@ export default class {
157
161
 
158
162
  async internalSpeedUpdate(value) {
159
163
  try {
164
+ // Don't continue if the fan is being turned off
165
+ if (value === 0) {
166
+ return
167
+ }
168
+
169
+ // HomeKit sends a percentage - map it onto this model's real speed steps
170
+ value = hkPercentToFanSpeed(value, this.speedSteps)
171
+
160
172
  // Don't continue if the new value is the same as before
161
- if (this.cacheSpeed === value || value === 0) {
173
+ if (this.cacheSpeed === value) {
162
174
  return
163
175
  }
164
176
 
@@ -182,7 +194,7 @@ export default class {
182
194
 
183
195
  // Throw a 'no response' error and set a timeout to revert this after 2 seconds
184
196
  setTimeout(() => {
185
- this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSpeed)
197
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, fanSpeedToHkPercent(this.cacheSpeed, this.speedSteps, this.autoSpeed))
186
198
  }, 2000)
187
199
  throw new this.hapErr(-70402)
188
200
  }
@@ -380,8 +392,9 @@ export default class {
380
392
  }
381
393
  if (Number.isFinite(newSpeed) && newSpeed >= 1 && newSpeed <= this.autoSpeed && this.cacheSpeed !== newSpeed) {
382
394
  this.cacheSpeed = newSpeed
383
- this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSpeed)
384
- this.accessory.log(`${platformLang.curSpeed} [${this.cacheSpeed}]`)
395
+ const hkSpeed = fanSpeedToHkPercent(newSpeed, this.speedSteps, this.autoSpeed)
396
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, hkSpeed)
397
+ this.accessory.log(`${platformLang.curSpeed} [${newSpeed} -> ${hkSpeed}%]`)
385
398
  }
386
399
  }
387
400
 
@@ -430,7 +443,8 @@ export default class {
430
443
  // Mode change (auto = 0x02)
431
444
  if (getTwoItemPosition(hexParts, 4) === '02' && this.cacheSpeed !== this.autoSpeed) {
432
445
  this.cacheSpeed = this.autoSpeed
433
- this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSpeed)
446
+ // Cache auto so we can tell it apart from "user asked for max speed"
447
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, 100)
434
448
  this.accessory.log(`${platformLang.curSpeed} [auto]`)
435
449
  }
436
450
  break