@homebridge-plugins/homebridge-govee 11.32.0-beta.24 → 11.32.0-beta.26

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
@@ -27,6 +27,9 @@ All notable changes to `@homebridge-plugins/homebridge-govee` will be documented
27
27
  - fix: hide the H5089 night light tile until its commands are worked out, rather than showing a control that does nothing (#1323)
28
28
  - feat: add support for the H3001 solar string lights as an rgb light (#1328)
29
29
  - docs(changelog): list every unreleased commit in the pending section
30
+ - fix(fans): rebuild the fan service once when it was cached under the old speed scale, so the top of the slider stops setting the slowest speed (#1326) (@cwhett)
31
+ - fix: show the H5089 night light tile again, but refuse control commands, so it reports the real state instead of quietly ignoring taps (#1323)
32
+ - fix(ice-makers): stop the home app showing an ice size of 0 when one has never been set (#1329) (@cwhett)
30
33
 
31
34
  ## v11.31.0 (2026-07-24)
32
35
 
@@ -40,10 +40,12 @@ export default class {
40
40
  this.accessory.removeService(this.accessory.getService(this.hapServ.Fan))
41
41
  }
42
42
 
43
- // Migrate old %-rotation speed to unitless
43
+ // Rebuild the fan service if it was cached under an older speed scale, as
44
+ // HomeKit keeps the old props until the service itself is replaced
44
45
  const existingService = this.accessory.getService(this.hapServ.Fanv2)
45
46
  if (existingService) {
46
- if (existingService.getCharacteristic(this.hapChar.RotationSpeed).props.unit === 'percentage') {
47
+ const { props } = existingService.getCharacteristic(this.hapChar.RotationSpeed)
48
+ if (props.unit === 'percentage' || props.maxValue !== 100) {
47
49
  this.accessory.removeService(existingService)
48
50
  }
49
51
  }
@@ -34,6 +34,16 @@ export default class {
34
34
  const deviceConf = platform.deviceConf[accessory.context.gvDeviceId]
35
35
  this.hideLight = deviceConf && deviceConf.hideLight
36
36
 
37
+ // Rebuild the fan service if it was cached under an older speed scale, as
38
+ // HomeKit keeps the old props until the service itself is replaced
39
+ const existingService = this.accessory.getService(this.hapServ.Fanv2)
40
+ if (existingService) {
41
+ const { props } = existingService.getCharacteristic(this.hapChar.RotationSpeed)
42
+ if (props.unit === 'percentage' || props.maxValue !== 100) {
43
+ this.accessory.removeService(existingService)
44
+ }
45
+ }
46
+
37
47
  // Add the fan service if it doesn't already exist
38
48
  this.service = this.accessory.getService(this.hapServ.Fanv2) || this.accessory.addService(this.hapServ.Fanv2)
39
49
 
@@ -40,10 +40,12 @@ export default class {
40
40
  this.accessory.removeService(this.accessory.getService(this.hapServ.Fan))
41
41
  }
42
42
 
43
- // Migrate old %-rotation speed to unitless
43
+ // Rebuild the fan service if it was cached under an older speed scale, as
44
+ // HomeKit keeps the old props until the service itself is replaced
44
45
  const existingService = this.accessory.getService(this.hapServ.Fanv2)
45
46
  if (existingService) {
46
- if (existingService.getCharacteristic(this.hapChar.RotationSpeed).props.unit === 'percentage') {
47
+ const { props } = existingService.getCharacteristic(this.hapChar.RotationSpeed)
48
+ if (props.unit === 'percentage' || props.maxValue !== 100) {
47
49
  this.accessory.removeService(existingService)
48
50
  }
49
51
  }
@@ -96,10 +96,12 @@ export default class {
96
96
  this.accessory.removeService(this.accessory.getService(this.hapServ.Fan))
97
97
  }
98
98
 
99
- // Migrate old %-rotation speed to unitless
99
+ // Rebuild the fan service if it was cached under an older speed scale, as
100
+ // HomeKit keeps the old props until the service itself is replaced
100
101
  const existingService = this.accessory.getService(this.hapServ.Fanv2)
101
102
  if (existingService) {
102
- if (existingService.getCharacteristic(this.hapChar.RotationSpeed).props.unit === 'percentage') {
103
+ const { props } = existingService.getCharacteristic(this.hapChar.RotationSpeed)
104
+ if (props.unit === 'percentage' || props.maxValue !== 100) {
103
105
  this.accessory.removeService(existingService)
104
106
  }
105
107
  }
@@ -40,10 +40,12 @@ export default class {
40
40
  this.accessory.removeService(this.accessory.getService(this.hapServ.Fan))
41
41
  }
42
42
 
43
- // Migrate old %-rotation speed to unitless
43
+ // Rebuild the fan service if it was cached under an older speed scale, as
44
+ // HomeKit keeps the old props until the service itself is replaced
44
45
  const existingService = this.accessory.getService(this.hapServ.Fanv2)
45
46
  if (existingService) {
46
- if (existingService.getCharacteristic(this.hapChar.RotationSpeed).props.unit === 'percentage') {
47
+ const { props } = existingService.getCharacteristic(this.hapChar.RotationSpeed)
48
+ if (props.unit === 'percentage' || props.maxValue !== 100) {
47
49
  this.accessory.removeService(existingService)
48
50
  }
49
51
  }
@@ -53,10 +53,12 @@ export default class {
53
53
  this.accessory.removeService(this.accessory.getService(this.hapServ.Fan))
54
54
  }
55
55
 
56
- // Migrate old %-rotation speed to unitless
56
+ // Rebuild the fan service if it was cached under an older speed scale, as
57
+ // HomeKit keeps the old props until the service itself is replaced
57
58
  const existingService = this.accessory.getService(this.hapServ.Fanv2)
58
59
  if (existingService) {
59
- if (existingService.getCharacteristic(this.hapChar.RotationSpeed).props.unit === 'percentage') {
60
+ const { props } = existingService.getCharacteristic(this.hapChar.RotationSpeed)
61
+ if (props.unit === 'percentage' || props.maxValue !== 100) {
60
62
  this.accessory.removeService(existingService)
61
63
  }
62
64
  }
@@ -40,10 +40,12 @@ export default class {
40
40
  this.accessory.removeService(this.accessory.getService(this.hapServ.Fan))
41
41
  }
42
42
 
43
- // Migrate old %-rotation speed to unitless
43
+ // Rebuild the fan service if it was cached under an older speed scale, as
44
+ // HomeKit keeps the old props until the service itself is replaced
44
45
  const existingService = this.accessory.getService(this.hapServ.Fanv2)
45
46
  if (existingService) {
46
- if (existingService.getCharacteristic(this.hapChar.RotationSpeed).props.unit === 'percentage') {
47
+ const { props } = existingService.getCharacteristic(this.hapChar.RotationSpeed)
48
+ if (props.unit === 'percentage' || props.maxValue !== 100) {
47
49
  this.accessory.removeService(existingService)
48
50
  }
49
51
  }
@@ -53,10 +53,12 @@ export default class {
53
53
  this.accessory.removeService(this.accessory.getService(this.hapServ.Fan))
54
54
  }
55
55
 
56
- // Migrate old %-rotation speed to unitless
56
+ // Rebuild the fan service if it was cached under an older speed scale, as
57
+ // HomeKit keeps the old props until the service itself is replaced
57
58
  const existingService = this.accessory.getService(this.hapServ.Fanv2)
58
59
  if (existingService) {
59
- if (existingService.getCharacteristic(this.hapChar.RotationSpeed).props.unit === 'percentage') {
60
+ const { props } = existingService.getCharacteristic(this.hapChar.RotationSpeed)
61
+ if (props.unit === 'percentage' || props.maxValue !== 100) {
60
62
  this.accessory.removeService(existingService)
61
63
  }
62
64
  }
@@ -101,10 +101,12 @@ export default class {
101
101
  this.accessory.removeService(this.accessory.getService(this.hapServ.Fan))
102
102
  }
103
103
 
104
- // Migrate old %-rotation speed to unitless
104
+ // Rebuild the fan service if it was cached under an older speed scale, as
105
+ // HomeKit keeps the old props until the service itself is replaced
105
106
  const existingService = this.accessory.getService(this.hapServ.Fanv2)
106
107
  if (existingService) {
107
- if (existingService.getCharacteristic(this.hapChar.RotationSpeed).props.unit === 'percentage') {
108
+ const { props } = existingService.getCharacteristic(this.hapChar.RotationSpeed)
109
+ if (props.unit === 'percentage' || props.maxValue !== 100) {
108
110
  this.accessory.removeService(existingService)
109
111
  }
110
112
  }
@@ -52,6 +52,13 @@ export default class {
52
52
  .onSet(async value => this.internalSizeUpdate(value))
53
53
  this.cacheSize = this.service.getCharacteristic(this.hapChar.RotationSpeed).value || 2
54
54
 
55
+ // Keep the characteristic in step with the cache. Without this an accessory
56
+ // that has never had a size set reports Active=1 with RotationSpeed=0, which
57
+ // is contradictory and which the Home app renders as "0".
58
+ if (this.service.getCharacteristic(this.hapChar.RotationSpeed).value !== this.cacheSize) {
59
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSize)
60
+ }
61
+
55
62
  // Output the customised options to the log
56
63
  const opts = JSON.stringify({})
57
64
  platform.log('[%s] %s %s.', accessory.displayName, platformLang.devInitOpts, opts)
@@ -105,8 +112,15 @@ export default class {
105
112
 
106
113
  async internalSizeUpdate(value) {
107
114
  try {
108
- // Don't continue if 0
115
+ // 0 is not a valid ice size. Put the slider back, otherwise HomeKit keeps 0
116
+ // while the device carries on at cacheSize - and that 0 is what gets written
117
+ // to the accessory cache, so it returns on the next restart. Deferred for the
118
+ // same reason as the error reverts below: an immediate update would be
119
+ // overwritten by the value HomeKit is currently writing.
109
120
  if (value === 0) {
121
+ setTimeout(() => {
122
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSize)
123
+ }, 1000)
110
124
  return
111
125
  }
112
126
 
@@ -145,8 +159,11 @@ export default class {
145
159
  if (params.workMode) {
146
160
  const mode = params.workMode.workMode
147
161
  if (mode > 0 && mode <= 3) {
148
- this.cacheState = 'on'
149
- this.service.updateCharacteristic(this.hapChar.Active, 1)
162
+ if (this.cacheState !== 'on') {
163
+ this.cacheState = 'on'
164
+ this.service.updateCharacteristic(this.hapChar.Active, 1)
165
+ this.accessory.log(`${platformLang.curState} [on]`)
166
+ }
150
167
  if (this.cacheSize !== mode) {
151
168
  this.cacheSize = mode
152
169
  this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSize)
@@ -184,6 +201,14 @@ export default class {
184
201
  }
185
202
  break
186
203
  }
204
+ case 'aa05': {
205
+ // Ice size report. The payload encoding is deliberately NOT decoded here:
206
+ // sizeCodes runs small=0x03..large=0x01 while the OpenAPI path sends
207
+ // workMode un-inverted, and which is authoritative is unresolved (#1329).
208
+ // Claiming a mapping here could silently swap sizes, so this case exists
209
+ // only to stop a routine status report being logged as a new scene code.
210
+ break
211
+ }
187
212
  default:
188
213
  this.accessory.logWarn(`${platformLang.newScene}: [${command}] [${hexString}]`)
189
214
  break
@@ -52,6 +52,13 @@ export default class {
52
52
  .onSet(async value => this.internalSizeUpdate(value))
53
53
  this.cacheSize = this.service.getCharacteristic(this.hapChar.RotationSpeed).value || 2
54
54
 
55
+ // Keep the characteristic in step with the cache. Without this an accessory
56
+ // that has never had a size set reports Active=1 with RotationSpeed=0, which
57
+ // is contradictory and which the Home app renders as "0".
58
+ if (this.service.getCharacteristic(this.hapChar.RotationSpeed).value !== this.cacheSize) {
59
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSize)
60
+ }
61
+
55
62
  // Output the customised options to the log
56
63
  const opts = JSON.stringify({})
57
64
  platform.log('[%s] %s %s.', accessory.displayName, platformLang.devInitOpts, opts)
@@ -105,8 +112,15 @@ export default class {
105
112
 
106
113
  async internalSizeUpdate(value) {
107
114
  try {
108
- // Don't continue if 0
115
+ // 0 is not a valid ice size. Put the slider back, otherwise HomeKit keeps 0
116
+ // while the device carries on at cacheSize - and that 0 is what gets written
117
+ // to the accessory cache, so it returns on the next restart. Deferred for the
118
+ // same reason as the error reverts below: an immediate update would be
119
+ // overwritten by the value HomeKit is currently writing.
109
120
  if (value === 0) {
121
+ setTimeout(() => {
122
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSize)
123
+ }, 1000)
110
124
  return
111
125
  }
112
126
 
@@ -145,8 +159,11 @@ export default class {
145
159
  if (params.workMode) {
146
160
  const mode = params.workMode.workMode
147
161
  if (mode > 0 && mode <= 3) {
148
- this.cacheState = 'on'
149
- this.service.updateCharacteristic(this.hapChar.Active, 1)
162
+ if (this.cacheState !== 'on') {
163
+ this.cacheState = 'on'
164
+ this.service.updateCharacteristic(this.hapChar.Active, 1)
165
+ this.accessory.log(`${platformLang.curState} [on]`)
166
+ }
150
167
  if (this.cacheSize !== mode) {
151
168
  this.cacheSize = mode
152
169
  this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSize)
@@ -184,6 +201,14 @@ export default class {
184
201
  }
185
202
  break
186
203
  }
204
+ case 'aa05': {
205
+ // Ice size report. The payload encoding is deliberately NOT decoded here:
206
+ // sizeCodes runs small=0x03..large=0x01 while the OpenAPI path sends
207
+ // workMode un-inverted, and which is authoritative is unresolved (#1329).
208
+ // Claiming a mapping here could silently swap sizes, so this case exists
209
+ // only to stop a routine status report being logged as a new scene code.
210
+ break
211
+ }
187
212
  default:
188
213
  this.accessory.logWarn(`${platformLang.newScene}: [${command}] [${hexString}]`)
189
214
  break
@@ -52,6 +52,13 @@ export default class {
52
52
  .onSet(async value => this.internalSizeUpdate(value))
53
53
  this.cacheSize = this.service.getCharacteristic(this.hapChar.RotationSpeed).value || 2
54
54
 
55
+ // Keep the characteristic in step with the cache. Without this an accessory
56
+ // that has never had a size set reports Active=1 with RotationSpeed=0, which
57
+ // is contradictory and which the Home app renders as "0".
58
+ if (this.service.getCharacteristic(this.hapChar.RotationSpeed).value !== this.cacheSize) {
59
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSize)
60
+ }
61
+
55
62
  // Output the customised options to the log
56
63
  const opts = JSON.stringify({})
57
64
  platform.log('[%s] %s %s.', accessory.displayName, platformLang.devInitOpts, opts)
@@ -105,8 +112,15 @@ export default class {
105
112
 
106
113
  async internalSizeUpdate(value) {
107
114
  try {
108
- // Don't continue if 0
115
+ // 0 is not a valid ice size. Put the slider back, otherwise HomeKit keeps 0
116
+ // while the device carries on at cacheSize - and that 0 is what gets written
117
+ // to the accessory cache, so it returns on the next restart. Deferred for the
118
+ // same reason as the error reverts below: an immediate update would be
119
+ // overwritten by the value HomeKit is currently writing.
109
120
  if (value === 0) {
121
+ setTimeout(() => {
122
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSize)
123
+ }, 1000)
110
124
  return
111
125
  }
112
126
 
@@ -145,8 +159,11 @@ export default class {
145
159
  if (params.workMode) {
146
160
  const mode = params.workMode.workMode
147
161
  if (mode > 0 && mode <= 3) {
148
- this.cacheState = 'on'
149
- this.service.updateCharacteristic(this.hapChar.Active, 1)
162
+ if (this.cacheState !== 'on') {
163
+ this.cacheState = 'on'
164
+ this.service.updateCharacteristic(this.hapChar.Active, 1)
165
+ this.accessory.log(`${platformLang.curState} [on]`)
166
+ }
150
167
  if (this.cacheSize !== mode) {
151
168
  this.cacheSize = mode
152
169
  this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSize)
@@ -184,6 +201,14 @@ export default class {
184
201
  }
185
202
  break
186
203
  }
204
+ case 'aa05': {
205
+ // Ice size report. The payload encoding is deliberately NOT decoded here:
206
+ // sizeCodes runs small=0x03..large=0x01 while the OpenAPI path sends
207
+ // workMode un-inverted, and which is authoritative is unresolved (#1329).
208
+ // Claiming a mapping here could silently swap sizes, so this case exists
209
+ // only to stop a routine status report being logged as a new scene code.
210
+ break
211
+ }
187
212
  default:
188
213
  this.accessory.logWarn(`${platformLang.newScene}: [${command}] [${hexString}]`)
189
214
  break
@@ -1,13 +1,12 @@
1
- import { base64ToHex, generateCodeFromHexValues, parseError } from '../utils/functions.js'
1
+ import { base64ToHex, parseError } from '../utils/functions.js'
2
2
  import platformLang from '../utils/lang-en.js'
3
3
 
4
- // Models whose night light can be controlled. The H5089 reports its light as
5
- // `aa 1b 01 [on] [level]` and that reading is correct, but no command shape
6
- // tried so far moves it - neither mirroring its own status code nor the
7
- // `33 18` the purifiers use - so it is left out rather than shipping a tile
8
- // that reports properly and ignores every tap (#1323). Put the model back
9
- // here once a command is known to work.
10
- const nightLightModels = []
4
+ // Models that expose a night light. The H5089 reports its light as
5
+ // `aa 1b 01 [on] [level]` and that reading is correct, so the tile shows the
6
+ // real state. No command shape tried moves it - neither mirroring its own
7
+ // status code nor the `33 18` the purifiers use - so control is refused
8
+ // outright rather than accepted and silently dropped (#1323).
9
+ const nightLightModels = ['H5089']
11
10
 
12
11
  export default class {
13
12
  constructor(platform, accessory) {
@@ -77,13 +76,9 @@ export default class {
77
76
  this.lightService.addCharacteristic(this.hapChar.ConfiguredName)
78
77
  this.lightService.updateCharacteristic(this.hapChar.ConfiguredName, 'Night Light')
79
78
  }
80
- this.lightService.getCharacteristic(this.hapChar.On).onSet(async (value) => {
81
- await this.internalLightStateUpdate(value)
82
- })
79
+ this.lightService.getCharacteristic(this.hapChar.On).onSet(() => this.refuseLightControl())
83
80
  this.cacheLightState = this.lightService.getCharacteristic(this.hapChar.On).value ? 'on' : 'off'
84
- this.lightService.getCharacteristic(this.hapChar.Brightness).onSet(async (value) => {
85
- await this.internalLightBrightnessUpdate(value)
86
- })
81
+ this.lightService.getCharacteristic(this.hapChar.Brightness).onSet(() => this.refuseLightControl())
87
82
  this.cacheLightBright = this.lightService.getCharacteristic(this.hapChar.Brightness).value || 100
88
83
  } else if (this.accessory.getService('Night Light')) {
89
84
  this.accessory.removeService(this.accessory.getService('Night Light'))
@@ -136,75 +131,12 @@ export default class {
136
131
  }
137
132
  }
138
133
 
139
- // Govee does not use one night light command across its range: the purifiers
140
- // that already work here send `33 18 [on] [level]`, while this plug reports
141
- // its light as `aa 1b 01 [on] [level]`. Mirroring the status code was not
142
- // enough on its own, so send both shapes - they express the same thing, so
143
- // whichever the plug ignores costs nothing. Once it is known which one lands
144
- // this drops to that one alone (#1323)
145
- async sendLightCommand(state, level) {
146
- const shapes = [
147
- [0x33, 0x1B, 0x01, state, level],
148
- [0x33, 0x18, state, level],
149
- ]
150
- let lastError
151
- for (const hexValues of shapes) {
152
- try {
153
- await this.platform.sendDeviceUpdate(this.accessory, {
154
- // This plug acks the app's commands as `ptIot` and ignores `ptReal`
155
- cmd: 'ptIot',
156
- value: generateCodeFromHexValues(hexValues),
157
- })
158
- } catch (err) {
159
- lastError = err
160
- }
161
- }
162
- if (lastError) {
163
- throw lastError
164
- }
165
- }
166
-
167
- async internalLightStateUpdate(value) {
168
- try {
169
- const newValue = value ? 'on' : 'off'
170
- if (this.cacheLightState === newValue) {
171
- return
172
- }
173
-
174
- // Keep the current level when toggling, so turning the light on does not
175
- // also change how bright it is
176
- await this.sendLightCommand(value ? 0x01 : 0x00, this.cacheLightBright)
177
-
178
- this.cacheLightState = newValue
179
- this.accessory.log(`[Night Light] ${platformLang.curState} [${newValue}]`)
180
- } catch (err) {
181
- this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`)
182
- setTimeout(() => {
183
- this.lightService.updateCharacteristic(this.hapChar.On, this.cacheLightState === 'on')
184
- }, 2000)
185
- throw new this.hapErr(-70402)
186
- }
187
- }
188
-
189
- async internalLightBrightnessUpdate(value) {
190
- try {
191
- if (value === 0 || this.cacheLightBright === value) {
192
- return
193
- }
194
-
195
- // Setting a level also turns the light on, matching how HomeKit sends
196
- // brightness changes alongside an on command
197
- await this.sendLightCommand(0x01, value)
198
-
199
- this.cacheLightBright = value
200
- this.accessory.log(`[Night Light] ${platformLang.curBright} [${value}%]`)
201
- } catch (err) {
202
- this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`)
203
- setTimeout(() => {
204
- this.lightService.updateCharacteristic(this.hapChar.Brightness, this.cacheLightBright)
205
- }, 2000)
206
- throw new this.hapErr(-70402)
207
- }
134
+ // The night light reading is correct, but every command shape tried has been
135
+ // ignored by the plug, so refuse the write. HomeKit reverts the tile rather
136
+ // than showing a state the plug was never given (#1323)
137
+ refuseLightControl() {
138
+ this.accessory.logWarn(`[Night Light] ${platformLang.devNotUpdated} this device does not accept night light commands`)
139
+ throw new this.hapErr(-70402)
208
140
  }
209
141
 
210
142
  externalUpdate(params) {
@@ -1,13 +1,12 @@
1
- import { base64ToHex, generateCodeFromHexValues, parseError } from '../utils/functions.js'
1
+ import { base64ToHex, parseError } from '../utils/functions.js'
2
2
  import platformLang from '../utils/lang-en.js'
3
3
 
4
- // Models whose night light can be controlled. The H5089 reports its light as
5
- // `aa 1b 01 [on] [level]` and that reading is correct, but no command shape
6
- // tried so far moves it - neither mirroring its own status code nor the
7
- // `33 18` the purifiers use - so it is left out rather than shipping a tile
8
- // that reports properly and ignores every tap (#1323). Put the model back
9
- // here once a command is known to work.
10
- const nightLightModels = []
4
+ // Models that expose a night light. The H5089 reports its light as
5
+ // `aa 1b 01 [on] [level]` and that reading is correct, so the tile shows the
6
+ // real state. No command shape tried moves it - neither mirroring its own
7
+ // status code nor the `33 18` the purifiers use - so control is refused
8
+ // outright rather than accepted and silently dropped (#1323).
9
+ const nightLightModels = ['H5089']
11
10
 
12
11
  export default class {
13
12
  constructor(platform, accessory) {
@@ -77,13 +76,9 @@ export default class {
77
76
  this.lightService.addCharacteristic(this.hapChar.ConfiguredName)
78
77
  this.lightService.updateCharacteristic(this.hapChar.ConfiguredName, 'Night Light')
79
78
  }
80
- this.lightService.getCharacteristic(this.hapChar.On).onSet(async (value) => {
81
- await this.internalLightStateUpdate(value)
82
- })
79
+ this.lightService.getCharacteristic(this.hapChar.On).onSet(() => this.refuseLightControl())
83
80
  this.cacheLightState = this.lightService.getCharacteristic(this.hapChar.On).value ? 'on' : 'off'
84
- this.lightService.getCharacteristic(this.hapChar.Brightness).onSet(async (value) => {
85
- await this.internalLightBrightnessUpdate(value)
86
- })
81
+ this.lightService.getCharacteristic(this.hapChar.Brightness).onSet(() => this.refuseLightControl())
87
82
  this.cacheLightBright = this.lightService.getCharacteristic(this.hapChar.Brightness).value || 100
88
83
  } else if (this.accessory.getService('Night Light')) {
89
84
  this.accessory.removeService(this.accessory.getService('Night Light'))
@@ -136,75 +131,12 @@ export default class {
136
131
  }
137
132
  }
138
133
 
139
- // Govee does not use one night light command across its range: the purifiers
140
- // that already work here send `33 18 [on] [level]`, while this plug reports
141
- // its light as `aa 1b 01 [on] [level]`. Mirroring the status code was not
142
- // enough on its own, so send both shapes - they express the same thing, so
143
- // whichever the plug ignores costs nothing. Once it is known which one lands
144
- // this drops to that one alone (#1323)
145
- async sendLightCommand(state, level) {
146
- const shapes = [
147
- [0x33, 0x1B, 0x01, state, level],
148
- [0x33, 0x18, state, level],
149
- ]
150
- let lastError
151
- for (const hexValues of shapes) {
152
- try {
153
- await this.platform.sendDeviceUpdate(this.accessory, {
154
- // This plug acks the app's commands as `ptIot` and ignores `ptReal`
155
- cmd: 'ptIot',
156
- value: generateCodeFromHexValues(hexValues),
157
- })
158
- } catch (err) {
159
- lastError = err
160
- }
161
- }
162
- if (lastError) {
163
- throw lastError
164
- }
165
- }
166
-
167
- async internalLightStateUpdate(value) {
168
- try {
169
- const newValue = value ? 'on' : 'off'
170
- if (this.cacheLightState === newValue) {
171
- return
172
- }
173
-
174
- // Keep the current level when toggling, so turning the light on does not
175
- // also change how bright it is
176
- await this.sendLightCommand(value ? 0x01 : 0x00, this.cacheLightBright)
177
-
178
- this.cacheLightState = newValue
179
- this.accessory.log(`[Night Light] ${platformLang.curState} [${newValue}]`)
180
- } catch (err) {
181
- this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`)
182
- setTimeout(() => {
183
- this.lightService.updateCharacteristic(this.hapChar.On, this.cacheLightState === 'on')
184
- }, 2000)
185
- throw new this.hapErr(-70402)
186
- }
187
- }
188
-
189
- async internalLightBrightnessUpdate(value) {
190
- try {
191
- if (value === 0 || this.cacheLightBright === value) {
192
- return
193
- }
194
-
195
- // Setting a level also turns the light on, matching how HomeKit sends
196
- // brightness changes alongside an on command
197
- await this.sendLightCommand(0x01, value)
198
-
199
- this.cacheLightBright = value
200
- this.accessory.log(`[Night Light] ${platformLang.curBright} [${value}%]`)
201
- } catch (err) {
202
- this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`)
203
- setTimeout(() => {
204
- this.lightService.updateCharacteristic(this.hapChar.Brightness, this.cacheLightBright)
205
- }, 2000)
206
- throw new this.hapErr(-70402)
207
- }
134
+ // The night light reading is correct, but every command shape tried has been
135
+ // ignored by the plug, so refuse the write. HomeKit reverts the tile rather
136
+ // than showing a state the plug was never given (#1323)
137
+ refuseLightControl() {
138
+ this.accessory.logWarn(`[Night Light] ${platformLang.devNotUpdated} this device does not accept night light commands`)
139
+ throw new this.hapErr(-70402)
208
140
  }
209
141
 
210
142
  externalUpdate(params) {
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@homebridge-plugins/homebridge-govee",
3
3
  "alias": "Govee",
4
4
  "type": "module",
5
- "version": "11.32.0-beta.24",
5
+ "version": "11.32.0-beta.26",
6
6
  "description": "Homebridge plugin to integrate Govee devices into HomeKit.",
7
7
  "author": {
8
8
  "name": "bwp91",