@homebridge-plugins/homebridge-govee 11.33.0 → 11.33.2-beta.0
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 +15 -0
- package/lib/connection/ble.js +6 -4
- package/lib/device/fan-H7105.js +31 -22
- package/lib/device/kettle.js +35 -1
- package/lib/utils/report-unknown.js +33 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@homebridge-plugins/homebridge-govee` will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## v11.33.2 (Pending Release)
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- fix: implement the H7105 oscillation toggle, which threw a not-implemented error (#1339)
|
|
10
|
+
|
|
11
|
+
## v11.33.1 (2026-08-04)
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- fix: stop the kettle reporting its startup status as unrecognised scenes (#1337)
|
|
16
|
+
- fix: stop reporting a bluetooth broadcast that carries no data to read (#1338)
|
|
17
|
+
- fix: report an unrecognised code once per kind, not once per value it carries (#1338)
|
|
18
|
+
- chore(deps): dependency updates
|
|
19
|
+
|
|
5
20
|
## v11.33.0 (2026-08-03)
|
|
6
21
|
|
|
7
22
|
### Changed
|
package/lib/connection/ble.js
CHANGED
|
@@ -129,10 +129,12 @@ export default class BLEConnection {
|
|
|
129
129
|
// model and an owner has no other way to get hold of it.
|
|
130
130
|
if (!isValidPeripheral(peripheral)) {
|
|
131
131
|
const unknownName = advertisement?.localName
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
132
|
+
// A device sends more than one kind of advertisement, and the ones
|
|
133
|
+
// carrying only its name have no manufacturer data at all. There is
|
|
134
|
+
// nothing in those to decode and nothing to be learnt from reporting
|
|
135
|
+
// them, so a working sensor was warning about itself (#1338)
|
|
136
|
+
if (unknownName && unknownName.toLowerCase().startsWith('govee') && advertisement.manufacturerData) {
|
|
137
|
+
const raw = advertisement.manufacturerData.toString('hex')
|
|
136
138
|
|
|
137
139
|
// A device rebroadcasts constantly, so say this once and then stay
|
|
138
140
|
// quiet about it for the rest of the session. This has to come first:
|
package/lib/device/fan-H7105.js
CHANGED
|
@@ -187,33 +187,42 @@ export default class extends GoveeDevice {
|
|
|
187
187
|
|
|
188
188
|
async internalSwingUpdate(value) {
|
|
189
189
|
try {
|
|
190
|
-
|
|
191
|
-
if
|
|
190
|
+
const newValue = value ? 'on' : 'off'
|
|
191
|
+
// Don't continue if the new value is the same as before. The comparison
|
|
192
|
+
// has to be against the mapped string - cacheSwing holds 'on'/'off' and
|
|
193
|
+
// value arrives as a boolean, so comparing them directly never matched
|
|
194
|
+
if (this.cacheSwing === newValue) {
|
|
192
195
|
return
|
|
193
196
|
}
|
|
194
197
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
198
|
+
// Same oscillation shape as the H7107, which is the only other fan that
|
|
199
|
+
// reports a swing RANGE rather than a plain on/off. Turning ON carries
|
|
200
|
+
// over the range bytes from the last report seen; turning OFF must not,
|
|
201
|
+
// because a parameterised off command was answered with a zero-tailed
|
|
202
|
+
// frame while the fan kept oscillating (#1334, #1339)
|
|
203
|
+
const tailBytes = value && this.cacheSwingCode
|
|
204
|
+
? this.cacheSwingCode.slice(6, 14).match(/.{1,2}/g).map(byte => Number.parseInt(byte, 16))
|
|
205
|
+
: []
|
|
206
|
+
await this.platform.sendDeviceUpdate(this.accessory, {
|
|
207
|
+
cmd: 'ptReal',
|
|
208
|
+
value: generateCodeFromHexValues([0x33, 0x1D, value ? 0x01 : 0x00, ...tailBytes]),
|
|
209
|
+
openApi: this.accessory.context.openApiCapabilities?.oscillationToggle
|
|
210
|
+
? { instance: 'oscillationToggle', capabilityType: 'devices.capabilities.toggle', value: value ? 1 : 0 }
|
|
211
|
+
: undefined,
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
// The fan's own oscillation state travels as multiSync traffic, so send
|
|
215
|
+
// that shape too. Whichever the model ignores is harmless
|
|
216
|
+
await this.platform.sendDeviceUpdate(this.accessory, {
|
|
217
|
+
cmd: 'multiSync',
|
|
218
|
+
value: generateCodeFromHexValues([0x3A, 0x1D, value ? 0x01 : 0x00, ...tailBytes]),
|
|
219
|
+
})
|
|
211
220
|
|
|
212
221
|
// Cache the new state and log if appropriate
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
222
|
+
if (this.cacheSwing !== newValue) {
|
|
223
|
+
this.cacheSwing = newValue
|
|
224
|
+
this.accessory.log(`${platformLang.curSwing} [${newValue}]`)
|
|
225
|
+
}
|
|
217
226
|
} catch (err) {
|
|
218
227
|
this.failUpdate(err, () => {
|
|
219
228
|
this.service.updateCharacteristic(this.hapChar.SwingMode, this.cacheSwing === 'on' ? 1 : 0)
|
package/lib/device/kettle.js
CHANGED
|
@@ -29,6 +29,9 @@ export default class extends GoveeDevice {
|
|
|
29
29
|
// Set up variables from the accessory
|
|
30
30
|
const deviceConf = platform.deviceConf[accessory.context.gvDeviceId] || {}
|
|
31
31
|
|
|
32
|
+
// What each preset was last reported as, so a repeat is not logged twice
|
|
33
|
+
this.reportedPresets = new Map()
|
|
34
|
+
|
|
32
35
|
this.codes = {
|
|
33
36
|
greenTea: 'MwUAAgAAAAAAAAAAAAAAAAAAADQ=',
|
|
34
37
|
oolongTea: 'MwUAAwAAAAAAAAAAAAAAAAAAADU=',
|
|
@@ -251,6 +254,27 @@ export default class extends GoveeDevice {
|
|
|
251
254
|
|
|
252
255
|
const deviceFunction = `${getTwoItemPosition(hexParts, 2)}${getTwoItemPosition(hexParts, 3)}`
|
|
253
256
|
|
|
257
|
+
// On startup the kettle lists the temperature each of its presets is set
|
|
258
|
+
// to, one frame per preset: aa 05 <slot> <degrees F>. That is a different
|
|
259
|
+
// thing from aa 05 00 <mode> below, which is a mode change - the slot
|
|
260
|
+
// byte is what tells them apart. Nothing here needs the values, so they
|
|
261
|
+
// are logged for reference rather than reported as unrecognised (#1337)
|
|
262
|
+
if (getTwoItemPosition(hexParts, 2) === '05' && getTwoItemPosition(hexParts, 3) !== '00') {
|
|
263
|
+
const slot = Number.parseInt(getTwoItemPosition(hexParts, 3), 16)
|
|
264
|
+
const tempInF = Number.parseInt(getTwoItemPosition(hexParts, 4), 16)
|
|
265
|
+
const line = `preset ${slot} is set to ${tempInF}°F [${Math.round(((tempInF - 32) * 5) / 9)}°C]`
|
|
266
|
+
// Worth seeing in a normal log, but only once each. The kettle sends
|
|
267
|
+
// these when it connects, and if a firmware ever repeats them on a
|
|
268
|
+
// timer this keeps five lines from becoming five every few minutes
|
|
269
|
+
if (this.reportedPresets.get(slot) === line) {
|
|
270
|
+
this.accessory.logDebug(line)
|
|
271
|
+
} else {
|
|
272
|
+
this.reportedPresets.set(slot, line)
|
|
273
|
+
this.accessory.log(line)
|
|
274
|
+
}
|
|
275
|
+
return
|
|
276
|
+
}
|
|
277
|
+
|
|
254
278
|
switch (deviceFunction) {
|
|
255
279
|
case '0500': { // current mode
|
|
256
280
|
// switch to green tea_: aa050002000000000000000000000000000000ad
|
|
@@ -287,9 +311,19 @@ export default class extends GoveeDevice {
|
|
|
287
311
|
case '2301': { // scheduled start on
|
|
288
312
|
break
|
|
289
313
|
}
|
|
314
|
+
// Also sent on startup, alongside the preset list above. What each one
|
|
315
|
+
// carries has not been worked out, and nothing here needs them, so they
|
|
316
|
+
// are known-and-ignored rather than reported every time (#1337)
|
|
317
|
+
case '1701': // a second form of the on-base report
|
|
318
|
+
case '1f06':
|
|
319
|
+
case '1f08':
|
|
320
|
+
case '1900': {
|
|
321
|
+
this.accessory.logDebug(`startup status [${deviceFunction}]: ${hexString}`)
|
|
322
|
+
break
|
|
323
|
+
}
|
|
290
324
|
default:
|
|
291
325
|
logUnknownData(this.accessory, {
|
|
292
|
-
kind: '
|
|
326
|
+
kind: 'status',
|
|
293
327
|
source: params.source,
|
|
294
328
|
raw: command,
|
|
295
329
|
hex: hexString,
|
|
@@ -14,10 +14,33 @@ import { base64ToHex } from './functions.js'
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
// Remembers what has already been reported, so a code the device repeats every
|
|
17
|
-
// few seconds is only shouted about once. Keyed by device, kind and
|
|
18
|
-
//
|
|
17
|
+
// few seconds is only shouted about once. Keyed by device, kind and the shape
|
|
18
|
+
// of the frame rather than its whole payload - see frameShape below.
|
|
19
19
|
const alreadyReported = new Set()
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* What kind of message a frame is, ignoring the values it carries.
|
|
23
|
+
*
|
|
24
|
+
* A frame is `<prefix> <command> <sub-command> <data...>`, and the first three
|
|
25
|
+
* bytes are what the device handlers themselves switch on to decide what a
|
|
26
|
+
* frame means. Keying on those means one report per kind of message.
|
|
27
|
+
*
|
|
28
|
+
* Keying on the whole payload instead looks equivalent but is not: a frame
|
|
29
|
+
* carrying a reading or a counter differs every time it is sent, so each one
|
|
30
|
+
* counts as new and gets reported again. One fan sent the same message every
|
|
31
|
+
* twenty minutes with a changing value, which worked out at around seventy
|
|
32
|
+
* warnings a day for a single message type, burying the frames worth seeing
|
|
33
|
+
* (#1338).
|
|
34
|
+
*
|
|
35
|
+
* @param {string} [hex] the frame as hex, where there is one
|
|
36
|
+
* @returns {string|undefined} the first three bytes, or undefined if not a frame
|
|
37
|
+
*/
|
|
38
|
+
function frameShape(hex) {
|
|
39
|
+
return typeof hex === 'string' && /^[0-9a-f]{6}/i.test(hex)
|
|
40
|
+
? hex.slice(0, 6).toLowerCase()
|
|
41
|
+
: undefined
|
|
42
|
+
}
|
|
43
|
+
|
|
21
44
|
/**
|
|
22
45
|
* Reset the record of what has been reported. Only needed by tests, and when
|
|
23
46
|
* the platform restarts and wants a fresh session.
|
|
@@ -142,10 +165,14 @@ export function logUnknownData(accessory, {
|
|
|
142
165
|
})
|
|
143
166
|
|
|
144
167
|
const line = `unrecognised ${kind} - please include this line if you report it: ${JSON.stringify(report)}`
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
//
|
|
148
|
-
//
|
|
168
|
+
// Only the callers that pass `hex` are handing over a real device frame. The
|
|
169
|
+
// rest report things like a settings object, and base64ToHex will happily
|
|
170
|
+
// turn any string into hex that looks like one - two different messages can
|
|
171
|
+
// then share a first three bytes and wrongly count as the same shape.
|
|
172
|
+
const fingerprint = `${context.gvDeviceId || accessory.displayName}|${kind}|${frameShape(hex) ?? report.raw}`
|
|
173
|
+
|
|
174
|
+
// Say it once loudly enough to be noticed, then keep quiet about that shape
|
|
175
|
+
// for the rest of the session.
|
|
149
176
|
if (alreadyReported.has(fingerprint)) {
|
|
150
177
|
accessory.logDebug?.(line)
|
|
151
178
|
return
|
package/package.json
CHANGED