@homebridge-plugins/homebridge-govee 11.32.0-beta.2 → 11.32.0-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.
@@ -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'
@@ -91,6 +93,8 @@ export default class {
91
93
  9: 'MwUAAwAAAAAAAAAAAAAAAAAAADU=', // auto mode
92
94
  }
93
95
  this.autoSpeed = 9
96
+ // Real speed steps, excluding auto - HomeKit percentages map onto these
97
+ this.speedSteps = 8
94
98
 
95
99
  // Remove any old original Fan services
96
100
  if (this.accessory.getService(this.hapServ.Fan)) {
@@ -176,12 +180,8 @@ export default class {
176
180
  return
177
181
  }
178
182
 
179
- // HomeKit sends percentages
180
- if (value >= 100) {
181
- value = this.autoSpeed
182
- } else if (value > 8) {
183
- value = Math.max(1, Math.min(8, Math.round(value * 8 / 100)))
184
- }
183
+ // HomeKit sends a percentage - map it onto this model's real speed steps
184
+ value = hkPercentToFanSpeed(value, this.speedSteps)
185
185
 
186
186
  // Don't continue if the new value is the same as before
187
187
  if (this.cacheSpeed === value) {
@@ -210,7 +210,7 @@ export default class {
210
210
 
211
211
  // Throw a 'no response' error and set a timeout to revert this after 2 seconds
212
212
  setTimeout(() => {
213
- this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSpeed)
213
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, fanSpeedToHkPercent(this.cacheSpeed, this.speedSteps, this.autoSpeed))
214
214
  }, 2000)
215
215
  throw new this.hapErr(-70402)
216
216
  }
@@ -265,14 +265,11 @@ export default class {
265
265
  } else {
266
266
  newSpeed = params.workMode.modeValue
267
267
  }
268
- if (Number.isFinite(newSpeed) && newSpeed >= 1 && newSpeed <= this.autoSpeed) {
269
- const hkSpeed = newSpeed === this.autoSpeed ? 100 : Math.round((newSpeed / 8) * 100)
270
-
271
- if (this.cacheSpeed !== hkSpeed) {
272
- this.cacheSpeed = hkSpeed
273
- this.service.updateCharacteristic(this.hapChar.RotationSpeed, hkSpeed)
274
- this.accessory.log(`${platformLang.curSpeed} [${newSpeed} -> ${hkSpeed}%]`)
275
- }
268
+ if (Number.isFinite(newSpeed) && newSpeed >= 1 && newSpeed <= this.autoSpeed && this.cacheSpeed !== newSpeed) {
269
+ this.cacheSpeed = newSpeed
270
+ const hkSpeed = fanSpeedToHkPercent(newSpeed, this.speedSteps, this.autoSpeed)
271
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, hkSpeed)
272
+ this.accessory.log(`${platformLang.curSpeed} [${newSpeed} -> ${hkSpeed}%]`)
276
273
  }
277
274
  }
278
275
 
@@ -309,10 +306,9 @@ export default class {
309
306
  case '0501': {
310
307
  // Fan speed
311
308
  const newSpeed = Number.parseInt(getTwoItemPosition(hexParts, 4), 16)
312
- if (newSpeed >= 1 && newSpeed <= 8) {
313
- const hkSpeed = Math.round((newSpeed / 8) * 100)
314
-
315
- this.cacheSpeed = hkSpeed
309
+ if (newSpeed >= 1 && newSpeed <= this.speedSteps && this.cacheSpeed !== newSpeed) {
310
+ this.cacheSpeed = newSpeed
311
+ const hkSpeed = fanSpeedToHkPercent(newSpeed, this.speedSteps, this.autoSpeed)
316
312
  this.service.updateCharacteristic(this.hapChar.RotationSpeed, hkSpeed)
317
313
  this.accessory.log(`${platformLang.curSpeed} [${newSpeed} -> ${hkSpeed}%]`)
318
314
  }
@@ -321,7 +317,8 @@ export default class {
321
317
  case '0500': {
322
318
  // Mode change (auto = 0x03)
323
319
  if (getTwoItemPosition(hexParts, 4) === '03' && this.cacheSpeed !== this.autoSpeed) {
324
- this.cacheSpeed = 100
320
+ // Cache auto so we can tell it apart from "user asked for max speed"
321
+ this.cacheSpeed = this.autoSpeed
325
322
  this.service.updateCharacteristic(this.hapChar.RotationSpeed, 100)
326
323
  this.accessory.log(`${platformLang.curSpeed} [auto -> 100%]`)
327
324
  }
@@ -1,6 +1,10 @@
1
- import { parseError } from '../utils/functions.js'
1
+ import { base64ToHex, generateCodeFromHexValues, parseError } from '../utils/functions.js'
2
2
  import platformLang from '../utils/lang-en.js'
3
3
 
4
+ // Models here with a night light, reported as status code `aa 1b 01 [on] [level]`
5
+ // and set with the matching `33 1b 01 [on] [level]` (H5089, #1323)
6
+ const nightLightModels = ['H5089']
7
+
4
8
  export default class {
5
9
  constructor(platform, accessory) {
6
10
  // Set up variables from the platform
@@ -20,6 +24,14 @@ export default class {
20
24
  this.accessory.removeService(this.accessory.getService('Switch 2'))
21
25
  }
22
26
 
27
+ // A model that used to be treated as a single switch or outlet leaves an
28
+ // unnumbered service behind, which would sit next to the numbered pair as
29
+ // a tile that no longer does anything (#1323)
30
+ this.accessory.services
31
+ .filter(service => !service.subtype
32
+ && [this.hapServ.Switch.UUID, this.hapServ.Outlet.UUID].includes(service.UUID))
33
+ .forEach(service => this.accessory.removeService(service))
34
+
23
35
  // Add the outlet services if they don't already exist
24
36
  this.service1 = this.accessory.getService('Outlet 1')
25
37
  || this.accessory.addService(this.hapServ.Outlet, 'Outlet 1', 'outlet1')
@@ -53,18 +65,39 @@ export default class {
53
65
  })
54
66
  this.service2.cacheState = this.service2.getCharacteristic(this.hapChar.On).value ? 'on' : 'off'
55
67
 
68
+ // Add a night light service for the models that have one
69
+ if (nightLightModels.includes(this.accessory.context.gvModel)) {
70
+ this.lightService = this.accessory.getService('Night Light')
71
+ || this.accessory.addService(this.hapServ.Lightbulb, 'Night Light', 'nightlight')
72
+ if (!this.lightService.testCharacteristic(this.hapChar.ConfiguredName)) {
73
+ this.lightService.addCharacteristic(this.hapChar.ConfiguredName)
74
+ this.lightService.updateCharacteristic(this.hapChar.ConfiguredName, 'Night Light')
75
+ }
76
+ this.lightService.getCharacteristic(this.hapChar.On).onSet(async (value) => {
77
+ await this.internalLightStateUpdate(value)
78
+ })
79
+ this.cacheLightState = this.lightService.getCharacteristic(this.hapChar.On).value ? 'on' : 'off'
80
+ this.lightService.getCharacteristic(this.hapChar.Brightness).onSet(async (value) => {
81
+ await this.internalLightBrightnessUpdate(value)
82
+ })
83
+ this.cacheLightBright = this.lightService.getCharacteristic(this.hapChar.Brightness).value || 100
84
+ } else if (this.accessory.getService('Night Light')) {
85
+ this.accessory.removeService(this.accessory.getService('Night Light'))
86
+ }
87
+
56
88
  // Output the customised options to the log
57
89
  const opts = JSON.stringify({
58
90
  showAs: 'outlet',
59
91
  })
60
92
  platform.log('[%s] %s %s.', accessory.displayName, platformLang.devInitOpts, opts)
61
93
 
94
+ // The value is (which outlets to change << 4) | (which of those to turn on)
62
95
  // 51 turns BOTH ON
63
96
  // 48 turns BOTH OFF
64
- // 34 turns outlet 1 ON
65
- // 32 turns outlet 1 OFF
66
- // 17 turns outlet 2 ON
67
- // 16 turns outlet 2 OFF
97
+ // 17 turns outlet 1 ON
98
+ // 16 turns outlet 1 OFF
99
+ // 34 turns outlet 2 ON
100
+ // 32 turns outlet 2 OFF
68
101
  }
69
102
 
70
103
  async internalStateUpdate(service, value) {
@@ -99,23 +132,99 @@ export default class {
99
132
  }
100
133
  }
101
134
 
102
- externalUpdate(params) {
103
- if (!Array.isArray(params.state)) {
104
- return
135
+ async internalLightStateUpdate(value) {
136
+ try {
137
+ const newValue = value ? 'on' : 'off'
138
+ if (this.cacheLightState === newValue) {
139
+ return
140
+ }
141
+
142
+ // Keep the current level when toggling, so turning the light on does not
143
+ // also change how bright it is
144
+ await this.platform.sendDeviceUpdate(this.accessory, {
145
+ cmd: 'ptReal',
146
+ value: generateCodeFromHexValues([0x33, 0x1B, 0x01, value ? 0x01 : 0x00, this.cacheLightBright]),
147
+ })
148
+
149
+ this.cacheLightState = newValue
150
+ this.accessory.log(`[Night Light] ${platformLang.curState} [${newValue}]`)
151
+ } catch (err) {
152
+ this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`)
153
+ setTimeout(() => {
154
+ this.lightService.updateCharacteristic(this.hapChar.On, this.cacheLightState === 'on')
155
+ }, 2000)
156
+ throw new this.hapErr(-70402)
105
157
  }
158
+ }
106
159
 
107
- if (params.state[0] !== this.service1.cacheState) {
108
- this.service1.cacheState = params.state[0]
109
- this.service1.updateCharacteristic(this.hapChar.On, this.service1.cacheState === 'on')
160
+ async internalLightBrightnessUpdate(value) {
161
+ try {
162
+ if (value === 0 || this.cacheLightBright === value) {
163
+ return
164
+ }
165
+
166
+ // Setting a level also turns the light on, matching how HomeKit sends
167
+ // brightness changes alongside an on command
168
+ await this.platform.sendDeviceUpdate(this.accessory, {
169
+ cmd: 'ptReal',
170
+ value: generateCodeFromHexValues([0x33, 0x1B, 0x01, 0x01, value]),
171
+ })
110
172
 
111
- this.accessory.log(`[${this.service1.getCharacteristic(this.hapChar.ConfiguredName).value}] ${platformLang.curState} [${this.service1.cacheState}]`)
173
+ this.cacheLightBright = value
174
+ this.accessory.log(`[Night Light] ${platformLang.curBright} [${value}%]`)
175
+ } catch (err) {
176
+ this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`)
177
+ setTimeout(() => {
178
+ this.lightService.updateCharacteristic(this.hapChar.Brightness, this.cacheLightBright)
179
+ }, 2000)
180
+ throw new this.hapErr(-70402)
112
181
  }
182
+ }
183
+
184
+ externalUpdate(params) {
185
+ if (Array.isArray(params.state)) {
186
+ if (params.state[0] !== this.service1.cacheState) {
187
+ this.service1.cacheState = params.state[0]
188
+ this.service1.updateCharacteristic(this.hapChar.On, this.service1.cacheState === 'on')
189
+
190
+ this.accessory.log(`[${this.service1.getCharacteristic(this.hapChar.ConfiguredName).value}] ${platformLang.curState} [${this.service1.cacheState}]`)
191
+ }
113
192
 
114
- if (params.state[1] !== this.service2.cacheState) {
115
- this.service2.cacheState = params.state[1]
116
- this.service2.updateCharacteristic(this.hapChar.On, this.service2.cacheState === 'on')
193
+ if (params.state[1] !== this.service2.cacheState) {
194
+ this.service2.cacheState = params.state[1]
195
+ this.service2.updateCharacteristic(this.hapChar.On, this.service2.cacheState === 'on')
117
196
 
118
- this.accessory.log(`[${this.service2.getCharacteristic(this.hapChar.ConfiguredName).value}] ${platformLang.curState} [${this.service2.cacheState}]`)
197
+ this.accessory.log(`[${this.service2.getCharacteristic(this.hapChar.ConfiguredName).value}] ${platformLang.curState} [${this.service2.cacheState}]`)
198
+ }
199
+ }
200
+
201
+ // The night light reports as `aa 1b 01 [on] [level]` (#1323)
202
+ if (this.lightService && Array.isArray(params.commands)) {
203
+ params.commands.forEach((command) => {
204
+ let hexString
205
+ try {
206
+ hexString = base64ToHex(command)
207
+ } catch {
208
+ return
209
+ }
210
+ if (!hexString.startsWith('aa1b01')) {
211
+ return
212
+ }
213
+
214
+ const newState = hexString.substring(6, 8) === '01' ? 'on' : 'off'
215
+ if (newState !== this.cacheLightState) {
216
+ this.cacheLightState = newState
217
+ this.lightService.updateCharacteristic(this.hapChar.On, newState === 'on')
218
+ this.accessory.log(`[Night Light] ${platformLang.curState} [${newState}]`)
219
+ }
220
+
221
+ const newBright = Number.parseInt(hexString.substring(8, 10), 16)
222
+ if (newBright >= 1 && newBright <= 100 && newBright !== this.cacheLightBright) {
223
+ this.cacheLightBright = newBright
224
+ this.lightService.updateCharacteristic(this.hapChar.Brightness, newBright)
225
+ this.accessory.log(`[Night Light] ${platformLang.curBright} [${newBright}%]`)
226
+ }
227
+ })
119
228
  }
120
229
  }
121
230
  }
@@ -1,6 +1,10 @@
1
- import { parseError } from '../utils/functions.js'
1
+ import { base64ToHex, generateCodeFromHexValues, parseError } from '../utils/functions.js'
2
2
  import platformLang from '../utils/lang-en.js'
3
3
 
4
+ // Models here with a night light, reported as status code `aa 1b 01 [on] [level]`
5
+ // and set with the matching `33 1b 01 [on] [level]` (H5089, #1323)
6
+ const nightLightModels = ['H5089']
7
+
4
8
  export default class {
5
9
  constructor(platform, accessory) {
6
10
  // Set up variables from the platform
@@ -20,6 +24,14 @@ export default class {
20
24
  this.accessory.removeService(this.accessory.getService('Outlet 2'))
21
25
  }
22
26
 
27
+ // A model that used to be treated as a single switch or outlet leaves an
28
+ // unnumbered service behind, which would sit next to the numbered pair as
29
+ // a tile that no longer does anything (#1323)
30
+ this.accessory.services
31
+ .filter(service => !service.subtype
32
+ && [this.hapServ.Switch.UUID, this.hapServ.Outlet.UUID].includes(service.UUID))
33
+ .forEach(service => this.accessory.removeService(service))
34
+
23
35
  // Add the switch services if they don't already exist
24
36
  this.service1 = this.accessory.getService('Switch 1')
25
37
  || this.accessory.addService(this.hapServ.Switch, 'Switch 1', 'switch1')
@@ -53,18 +65,39 @@ export default class {
53
65
  })
54
66
  this.service2.cacheState = this.service2.getCharacteristic(this.hapChar.On).value ? 'on' : 'off'
55
67
 
68
+ // Add a night light service for the models that have one
69
+ if (nightLightModels.includes(this.accessory.context.gvModel)) {
70
+ this.lightService = this.accessory.getService('Night Light')
71
+ || this.accessory.addService(this.hapServ.Lightbulb, 'Night Light', 'nightlight')
72
+ if (!this.lightService.testCharacteristic(this.hapChar.ConfiguredName)) {
73
+ this.lightService.addCharacteristic(this.hapChar.ConfiguredName)
74
+ this.lightService.updateCharacteristic(this.hapChar.ConfiguredName, 'Night Light')
75
+ }
76
+ this.lightService.getCharacteristic(this.hapChar.On).onSet(async (value) => {
77
+ await this.internalLightStateUpdate(value)
78
+ })
79
+ this.cacheLightState = this.lightService.getCharacteristic(this.hapChar.On).value ? 'on' : 'off'
80
+ this.lightService.getCharacteristic(this.hapChar.Brightness).onSet(async (value) => {
81
+ await this.internalLightBrightnessUpdate(value)
82
+ })
83
+ this.cacheLightBright = this.lightService.getCharacteristic(this.hapChar.Brightness).value || 100
84
+ } else if (this.accessory.getService('Night Light')) {
85
+ this.accessory.removeService(this.accessory.getService('Night Light'))
86
+ }
87
+
56
88
  // Output the customised options to the log
57
89
  const opts = JSON.stringify({
58
90
  showAs: 'switch',
59
91
  })
60
92
  platform.log('[%s] %s %s.', accessory.displayName, platformLang.devInitOpts, opts)
61
93
 
94
+ // The value is (which outlets to change << 4) | (which of those to turn on)
62
95
  // 51 turns BOTH ON
63
96
  // 48 turns BOTH OFF
64
- // 34 turns outlet 1 ON
65
- // 32 turns outlet 1 OFF
66
- // 17 turns outlet 2 ON
67
- // 16 turns outlet 2 OFF
97
+ // 17 turns outlet 1 ON
98
+ // 16 turns outlet 1 OFF
99
+ // 34 turns outlet 2 ON
100
+ // 32 turns outlet 2 OFF
68
101
  }
69
102
 
70
103
  async internalStateUpdate(service, value) {
@@ -99,23 +132,99 @@ export default class {
99
132
  }
100
133
  }
101
134
 
102
- externalUpdate(params) {
103
- if (!Array.isArray(params.state)) {
104
- return
135
+ async internalLightStateUpdate(value) {
136
+ try {
137
+ const newValue = value ? 'on' : 'off'
138
+ if (this.cacheLightState === newValue) {
139
+ return
140
+ }
141
+
142
+ // Keep the current level when toggling, so turning the light on does not
143
+ // also change how bright it is
144
+ await this.platform.sendDeviceUpdate(this.accessory, {
145
+ cmd: 'ptReal',
146
+ value: generateCodeFromHexValues([0x33, 0x1B, 0x01, value ? 0x01 : 0x00, this.cacheLightBright]),
147
+ })
148
+
149
+ this.cacheLightState = newValue
150
+ this.accessory.log(`[Night Light] ${platformLang.curState} [${newValue}]`)
151
+ } catch (err) {
152
+ this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`)
153
+ setTimeout(() => {
154
+ this.lightService.updateCharacteristic(this.hapChar.On, this.cacheLightState === 'on')
155
+ }, 2000)
156
+ throw new this.hapErr(-70402)
105
157
  }
158
+ }
106
159
 
107
- if (params.state[0] !== this.service1.cacheState) {
108
- this.service1.cacheState = params.state[0]
109
- this.service1.updateCharacteristic(this.hapChar.On, this.service1.cacheState === 'on')
160
+ async internalLightBrightnessUpdate(value) {
161
+ try {
162
+ if (value === 0 || this.cacheLightBright === value) {
163
+ return
164
+ }
165
+
166
+ // Setting a level also turns the light on, matching how HomeKit sends
167
+ // brightness changes alongside an on command
168
+ await this.platform.sendDeviceUpdate(this.accessory, {
169
+ cmd: 'ptReal',
170
+ value: generateCodeFromHexValues([0x33, 0x1B, 0x01, 0x01, value]),
171
+ })
110
172
 
111
- this.accessory.log(`[${this.service1.getCharacteristic(this.hapChar.ConfiguredName).value}] ${platformLang.curState} [${this.service1.cacheState}]`)
173
+ this.cacheLightBright = value
174
+ this.accessory.log(`[Night Light] ${platformLang.curBright} [${value}%]`)
175
+ } catch (err) {
176
+ this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`)
177
+ setTimeout(() => {
178
+ this.lightService.updateCharacteristic(this.hapChar.Brightness, this.cacheLightBright)
179
+ }, 2000)
180
+ throw new this.hapErr(-70402)
112
181
  }
182
+ }
183
+
184
+ externalUpdate(params) {
185
+ if (Array.isArray(params.state)) {
186
+ if (params.state[0] !== this.service1.cacheState) {
187
+ this.service1.cacheState = params.state[0]
188
+ this.service1.updateCharacteristic(this.hapChar.On, this.service1.cacheState === 'on')
189
+
190
+ this.accessory.log(`[${this.service1.getCharacteristic(this.hapChar.ConfiguredName).value}] ${platformLang.curState} [${this.service1.cacheState}]`)
191
+ }
113
192
 
114
- if (params.state[1] !== this.service2.cacheState) {
115
- this.service2.cacheState = params.state[1]
116
- this.service2.updateCharacteristic(this.hapChar.On, this.service2.cacheState === 'on')
193
+ if (params.state[1] !== this.service2.cacheState) {
194
+ this.service2.cacheState = params.state[1]
195
+ this.service2.updateCharacteristic(this.hapChar.On, this.service2.cacheState === 'on')
117
196
 
118
- this.accessory.log(`[${this.service2.getCharacteristic(this.hapChar.ConfiguredName).value}] ${platformLang.curState} [${this.service2.cacheState}]`)
197
+ this.accessory.log(`[${this.service2.getCharacteristic(this.hapChar.ConfiguredName).value}] ${platformLang.curState} [${this.service2.cacheState}]`)
198
+ }
199
+ }
200
+
201
+ // The night light reports as `aa 1b 01 [on] [level]` (#1323)
202
+ if (this.lightService && Array.isArray(params.commands)) {
203
+ params.commands.forEach((command) => {
204
+ let hexString
205
+ try {
206
+ hexString = base64ToHex(command)
207
+ } catch {
208
+ return
209
+ }
210
+ if (!hexString.startsWith('aa1b01')) {
211
+ return
212
+ }
213
+
214
+ const newState = hexString.substring(6, 8) === '01' ? 'on' : 'off'
215
+ if (newState !== this.cacheLightState) {
216
+ this.cacheLightState = newState
217
+ this.lightService.updateCharacteristic(this.hapChar.On, newState === 'on')
218
+ this.accessory.log(`[Night Light] ${platformLang.curState} [${newState}]`)
219
+ }
220
+
221
+ const newBright = Number.parseInt(hexString.substring(8, 10), 16)
222
+ if (newBright >= 1 && newBright <= 100 && newBright !== this.cacheLightBright) {
223
+ this.cacheLightBright = newBright
224
+ this.lightService.updateCharacteristic(this.hapChar.Brightness, newBright)
225
+ this.accessory.log(`[Night Light] ${platformLang.curBright} [${newBright}%]`)
226
+ }
227
+ })
119
228
  }
120
229
  }
121
230
  }