@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.
- package/CHANGELOG.md +31 -0
- package/config.schema.json +1 -1
- package/lib/connection/ble.js +26 -1
- package/lib/connection/openapi.js +18 -0
- package/lib/device/appliance-basic.js +377 -0
- package/lib/device/dehumidifier-H7150.js +9 -3
- package/lib/device/diffuser-H7161.js +7 -1
- package/lib/device/fan-H1370.js +7 -1
- package/lib/device/fan-H7100.js +36 -4
- package/lib/device/fan-H7105.js +7 -1
- package/lib/device/fan-H7107.js +7 -1
- package/lib/device/heater1a.js +52 -12
- package/lib/device/heater1b.js +14 -3
- package/lib/device/heater2.js +7 -1
- package/lib/device/humidifier-H7140.js +7 -1
- package/lib/device/humidifier-H7141.js +7 -1
- package/lib/device/humidifier-H7142.js +7 -1
- package/lib/device/humidifier-H7143.js +7 -1
- package/lib/device/humidifier-H7160.js +7 -1
- package/lib/device/ice-maker-H7172.js +19 -7
- package/lib/device/index.js +44 -51
- package/lib/device/kettle.js +7 -1
- package/lib/device/outlet-single.js +7 -1
- package/lib/device/purifier-H7120.js +14 -25
- package/lib/device/purifier-H7122.js +7 -1
- package/lib/device/purifier-H7123.js +9 -3
- package/lib/device/purifier-H7126.js +7 -1
- package/lib/device/{sensor-button.js → sensor-basic.js} +14 -2
- package/lib/device/sensor-presence.js +7 -1
- package/lib/device/sensor-thermo4.js +13 -2
- package/lib/device/switch-double.js +34 -15
- package/lib/device/switch-triple.js +36 -17
- package/lib/device/template.js +15 -2
- package/lib/platform.js +83 -799
- package/lib/utils/capabilities.js +220 -0
- package/lib/utils/command-builder.js +6 -4
- package/lib/utils/config-parser.js +280 -0
- package/lib/utils/constants.js +120 -8
- package/lib/utils/decode.js +4 -1
- package/lib/utils/device-capabilities.js +77 -3
- package/lib/utils/device-context.js +179 -0
- package/lib/utils/device-merge.js +251 -0
- package/lib/utils/dispatch.js +159 -0
- package/lib/utils/functions.js +9 -0
- package/lib/utils/lang-en.js +0 -2
- package/lib/utils/report-unknown.js +156 -0
- package/package.json +1 -1
- package/lib/device/dehumidifier-H7151.js +0 -189
- package/lib/device/dehumidifier-H7152.js +0 -180
- package/lib/device/diffuser-H7162.js +0 -123
- package/lib/device/fan-H1310.js +0 -284
- package/lib/device/fan-H7101.js +0 -340
- package/lib/device/fan-H7102.js +0 -284
- package/lib/device/fan-H7106.js +0 -284
- package/lib/device/fan-H7111.js +0 -345
- package/lib/device/humidifier-H7145.js +0 -187
- package/lib/device/humidifier-H7147.js +0 -187
- package/lib/device/humidifier-H7148.js +0 -187
- package/lib/device/humidifier-H7149.js +0 -187
- package/lib/device/humidifier-H714E.js +0 -187
- package/lib/device/ice-maker-H717D.js +0 -218
- package/lib/device/ice-maker-H8120.js +0 -218
- package/lib/device/outlet-double.js +0 -188
- package/lib/device/outlet-triple.js +0 -160
- package/lib/device/purifier-H7121.js +0 -406
- package/lib/device/purifier-H7124.js +0 -504
- package/lib/device/purifier-H7127.js +0 -359
- package/lib/device/purifier-H7128.js +0 -359
- package/lib/device/purifier-H7129.js +0 -359
- package/lib/device/purifier-H712C.js +0 -359
- package/lib/device/sensor-contact.js +0 -22
- package/lib/utils/ble-timeouts.test.js +0 -44
- package/lib/utils/functions.test.js +0 -292
|
@@ -1,292 +0,0 @@
|
|
|
1
|
-
import { Buffer } from 'node:buffer'
|
|
2
|
-
|
|
3
|
-
import { describe, expect, it } from 'vitest'
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
base64ToHex,
|
|
7
|
-
cenToFar,
|
|
8
|
-
describeControlFailure,
|
|
9
|
-
fanSpeedToHkPercent,
|
|
10
|
-
farToCen,
|
|
11
|
-
generateCodeFromHexValues,
|
|
12
|
-
getTwoItemPosition,
|
|
13
|
-
hasProperty,
|
|
14
|
-
hexToBase64,
|
|
15
|
-
hexToDecimal,
|
|
16
|
-
hexToTwoItems,
|
|
17
|
-
hkPercentToFanSpeed,
|
|
18
|
-
nearestHalf,
|
|
19
|
-
parseDeviceId,
|
|
20
|
-
parseError,
|
|
21
|
-
updateFailureError,
|
|
22
|
-
} from './functions.js'
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* The fan speed mapping is the one worth pinning hardest. Before #1310/#1326
|
|
26
|
-
* the plugin sent the raw HomeKit percentage as a speed, so a slider at 100%
|
|
27
|
-
* asked an 8-step fan for speed 100, and the Home app displayed speeds like
|
|
28
|
-
* "7%". These tests cover the round trip on both step counts we ship.
|
|
29
|
-
*/
|
|
30
|
-
describe('fan speed mapping', () => {
|
|
31
|
-
it('maps the top of the slider to the fastest speed', () => {
|
|
32
|
-
expect(hkPercentToFanSpeed(100, 8)).toBe(8)
|
|
33
|
-
expect(hkPercentToFanSpeed(100, 12)).toBe(12)
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
it('never returns speed 0, which would mean "off" to the device', () => {
|
|
37
|
-
// HomeKit sends 0 when the fan is switched off. Speed 0 is not a valid
|
|
38
|
-
// speed, so the mapping clamps to the slowest instead.
|
|
39
|
-
expect(hkPercentToFanSpeed(0, 8)).toBe(1)
|
|
40
|
-
expect(hkPercentToFanSpeed(1, 8)).toBe(1)
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
it('never exceeds the number of steps the model has', () => {
|
|
44
|
-
expect(hkPercentToFanSpeed(150, 8)).toBe(8)
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
it('round trips every speed on an 8 step model', () => {
|
|
48
|
-
for (let speed = 1; speed <= 8; speed++) {
|
|
49
|
-
expect(hkPercentToFanSpeed(fanSpeedToHkPercent(speed, 8), 8)).toBe(speed)
|
|
50
|
-
}
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
it('round trips every speed on a 12 step model', () => {
|
|
54
|
-
for (let speed = 1; speed <= 12; speed++) {
|
|
55
|
-
expect(hkPercentToFanSpeed(fanSpeedToHkPercent(speed, 12), 12)).toBe(speed)
|
|
56
|
-
}
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
it('reports a stopped fan as 0 percent', () => {
|
|
60
|
-
expect(fanSpeedToHkPercent(0, 8)).toBe(0)
|
|
61
|
-
expect(fanSpeedToHkPercent(undefined, 8)).toBe(0)
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
it('shows auto mode at the top of the slider', () => {
|
|
65
|
-
// Chosen so the two paths disagree: without the auto case, speed 5 of 8
|
|
66
|
-
// would map to 63%. An auto speed that happens to sit above the step count
|
|
67
|
-
// would return 100 either way and would not prove the branch runs.
|
|
68
|
-
expect(fanSpeedToHkPercent(5, 8, 5)).toBe(100)
|
|
69
|
-
expect(fanSpeedToHkPercent(5, 8, undefined)).toBe(63)
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
it('does not treat auto as special when the model has no auto speed', () => {
|
|
73
|
-
expect(fanSpeedToHkPercent(4, 8, undefined)).toBe(50)
|
|
74
|
-
})
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
describe('parseDeviceId', () => {
|
|
78
|
-
it('uppercases and strips punctuation the api does not use', () => {
|
|
79
|
-
expect(parseDeviceId('ab:cd:ef')).toBe('AB:CD:EF')
|
|
80
|
-
expect(parseDeviceId('ab cd-ef')).toBe('ABCDEF')
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
it('keeps the separators govee device ids actually contain', () => {
|
|
84
|
-
// Colons and underscores are meaningful in a Govee device id, so the
|
|
85
|
-
// cleaner must not strip them along with the rest.
|
|
86
|
-
expect(parseDeviceId('AB:CD_EF')).toBe('AB:CD_EF')
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
it('accepts a non-string id without throwing', () => {
|
|
90
|
-
expect(parseDeviceId(123456)).toBe('123456')
|
|
91
|
-
})
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
describe('hex helpers', () => {
|
|
95
|
-
it('splits a hex string into byte pairs', () => {
|
|
96
|
-
expect(hexToTwoItems('aabbcc')).toEqual(['aa', 'bb', 'cc'])
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
it('reads a byte by its 1-based position, as the device handlers do', () => {
|
|
100
|
-
// Callers index from 1 because they are reading a protocol spec, not an
|
|
101
|
-
// array. Getting this off by one silently misreads every payload.
|
|
102
|
-
const parts = hexToTwoItems('aa1b0164')
|
|
103
|
-
expect(getTwoItemPosition(parts, 1)).toBe('aa')
|
|
104
|
-
expect(getTwoItemPosition(parts, 2)).toBe('1b')
|
|
105
|
-
expect(getTwoItemPosition(parts, 4)).toBe('64')
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
it('converts hex to decimal', () => {
|
|
109
|
-
expect(hexToDecimal('64')).toBe(100)
|
|
110
|
-
expect(hexToDecimal('ff')).toBe(255)
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
it('round trips base64 and hex', () => {
|
|
114
|
-
const hex = '330501000000000000000000000000000000000037'
|
|
115
|
-
expect(base64ToHex(hexToBase64(hex))).toBe(hex)
|
|
116
|
-
})
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
describe('generateCodeFromHexValues', () => {
|
|
120
|
-
it('builds a 20 byte packet', () => {
|
|
121
|
-
// Every Govee BLE/IoT command is exactly 20 bytes: 19 of payload plus a
|
|
122
|
-
// trailing XOR checksum.
|
|
123
|
-
const packet = Buffer.from(generateCodeFromHexValues(['0x33', '0x05', '0x01']), 'base64')
|
|
124
|
-
expect(packet).toHaveLength(20)
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
it('ends with an xor checksum over the first 19 bytes', () => {
|
|
128
|
-
const packet = Buffer.from(generateCodeFromHexValues(['0x33', '0x05', '0x01']), 'base64')
|
|
129
|
-
let expected = 0
|
|
130
|
-
for (let i = 0; i < 19; i++) {
|
|
131
|
-
expected ^= packet[i]
|
|
132
|
-
}
|
|
133
|
-
expect(packet[19]).toBe(expected)
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
it('pads short commands with zeroes rather than leaving them short', () => {
|
|
137
|
-
const packet = Buffer.from(generateCodeFromHexValues(['0x33', '0x01']), 'base64')
|
|
138
|
-
expect(packet.subarray(2, 19).every(b => b === 0)).toBe(true)
|
|
139
|
-
})
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
describe('temperature conversion', () => {
|
|
143
|
-
it('converts celsius to fahrenheit to one decimal place', () => {
|
|
144
|
-
expect(cenToFar(0)).toBe(32)
|
|
145
|
-
expect(cenToFar(100)).toBe(212)
|
|
146
|
-
expect(cenToFar(21.5)).toBe(70.7)
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
it('converts fahrenheit to whole celsius', () => {
|
|
150
|
-
expect(farToCen(32)).toBe(0)
|
|
151
|
-
expect(farToCen(212)).toBe(100)
|
|
152
|
-
})
|
|
153
|
-
|
|
154
|
-
it('rounds to the nearest half', () => {
|
|
155
|
-
expect(nearestHalf(21.24)).toBe(21)
|
|
156
|
-
expect(nearestHalf(21.26)).toBe(21.5)
|
|
157
|
-
expect(nearestHalf(21.75)).toBe(22)
|
|
158
|
-
})
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
describe('misc helpers', () => {
|
|
162
|
-
it('detects own properties only', () => {
|
|
163
|
-
expect(hasProperty({ a: 1 }, 'a')).toBe(true)
|
|
164
|
-
expect(hasProperty({}, 'toString')).toBe(false)
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
it('appends the first stack frame to an error message', () => {
|
|
168
|
-
const err = new Error('boom')
|
|
169
|
-
err.stack = 'Error: boom\n at thing (/a.js:1:1)'
|
|
170
|
-
expect(parseError(err)).toContain('boom')
|
|
171
|
-
expect(parseError(err)).toContain('at thing')
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
it('can be told to hide the stack for expected errors', () => {
|
|
175
|
-
// Some errors are routine (a device being offline) and their stack is
|
|
176
|
-
// noise in the log, so callers pass them in hideStack.
|
|
177
|
-
const err = new Error('offline')
|
|
178
|
-
err.stack = 'Error: offline\n at thing (/a.js:1:1)'
|
|
179
|
-
expect(parseError(err, ['offline'])).toBe('offline')
|
|
180
|
-
})
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Govee can accept a control request and still refuse the command, reporting it
|
|
185
|
-
* only inside `capability.state`. That shape used to read as success, so the
|
|
186
|
-
* device ignored the command while HomeKit kept showing the new state (#1324).
|
|
187
|
-
*/
|
|
188
|
-
describe('describeControlFailure', () => {
|
|
189
|
-
it('returns govee\'s own wording for the offline water timer in #1324', () => {
|
|
190
|
-
const response = {
|
|
191
|
-
requestId: 'test-control-turn-on',
|
|
192
|
-
msg: 'Device is offline. Please check the Wi-Fi connection.',
|
|
193
|
-
code: 400,
|
|
194
|
-
capability: {
|
|
195
|
-
type: 'devices.capabilities.on_off',
|
|
196
|
-
instance: 'powerSwitch',
|
|
197
|
-
state: {
|
|
198
|
-
status: 'failure',
|
|
199
|
-
errorCode: 400,
|
|
200
|
-
errorMsg: 'Device is offline. Please check the Wi-Fi connection.',
|
|
201
|
-
},
|
|
202
|
-
value: 1,
|
|
203
|
-
},
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
expect(describeControlFailure(response)).toBe('Device is offline. Please check the Wi-Fi connection.')
|
|
207
|
-
})
|
|
208
|
-
|
|
209
|
-
it('catches a refusal even when the response is 200 overall', () => {
|
|
210
|
-
// The case the old code missed entirely: nothing at the top level says
|
|
211
|
-
// anything went wrong.
|
|
212
|
-
const response = {
|
|
213
|
-
code: 200,
|
|
214
|
-
msg: 'success',
|
|
215
|
-
capability: { state: { status: 'failure', errorMsg: 'Device is busy' } },
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
expect(describeControlFailure(response)).toBe('Device is busy')
|
|
219
|
-
})
|
|
220
|
-
|
|
221
|
-
it('falls back to the top-level message when no errorMsg is given', () => {
|
|
222
|
-
const response = {
|
|
223
|
-
msg: 'Device is offline',
|
|
224
|
-
capability: { state: { status: 'failure' } },
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
expect(describeControlFailure(response)).toBe('Device is offline')
|
|
228
|
-
})
|
|
229
|
-
|
|
230
|
-
it('still describes a refusal that carries no wording at all', () => {
|
|
231
|
-
expect(describeControlFailure({ capability: { state: { status: 'failure' } } }))
|
|
232
|
-
.toBe('the device rejected the command')
|
|
233
|
-
})
|
|
234
|
-
|
|
235
|
-
it('returns null when the command was accepted', () => {
|
|
236
|
-
const accepted = {
|
|
237
|
-
code: 200,
|
|
238
|
-
msg: 'success',
|
|
239
|
-
capability: { state: { status: 'success' } },
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
expect(describeControlFailure(accepted)).toBeNull()
|
|
243
|
-
})
|
|
244
|
-
|
|
245
|
-
it('returns null for responses with no capability state', () => {
|
|
246
|
-
expect(describeControlFailure({ code: 200, msg: 'success' })).toBeNull()
|
|
247
|
-
expect(describeControlFailure({})).toBeNull()
|
|
248
|
-
expect(describeControlFailure(undefined)).toBeNull()
|
|
249
|
-
})
|
|
250
|
-
})
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* The bug (#1324): when a command had a Bluetooth fallback, every earlier
|
|
254
|
-
* failure was thrown away and replaced with "no connection method available".
|
|
255
|
-
* That is worse than vague — it is untrue, and its link sends the user off to
|
|
256
|
-
* configure connection methods when the real answer was that Govee had said
|
|
257
|
-
* the device was offline.
|
|
258
|
-
*/
|
|
259
|
-
describe('updateFailureError', () => {
|
|
260
|
-
it('reports govee\'s own refusal rather than the generic message', () => {
|
|
261
|
-
const refusal = new Error('Device is offline. Please check the Wi-Fi connection.')
|
|
262
|
-
const chosen = updateFailureError(refusal, 'no connection method available')
|
|
263
|
-
|
|
264
|
-
expect(chosen.message).toBe('Device is offline. Please check the Wi-Fi connection.')
|
|
265
|
-
})
|
|
266
|
-
|
|
267
|
-
it('passes the original error through, so upstream flags survive', () => {
|
|
268
|
-
const refusal = new Error('Device is offline. Please check the Wi-Fi connection.')
|
|
269
|
-
refusal.deviceRefusal = true
|
|
270
|
-
|
|
271
|
-
const chosen = updateFailureError(refusal, 'no connection method available')
|
|
272
|
-
|
|
273
|
-
// Same object, not a copy - platform.js reads `deviceRefusal` off it to
|
|
274
|
-
// decide whether to print a stack line.
|
|
275
|
-
expect(chosen).toBe(refusal)
|
|
276
|
-
expect(chosen.deviceRefusal).toBe(true)
|
|
277
|
-
})
|
|
278
|
-
|
|
279
|
-
it('falls back to the generic message only when nothing was tried', () => {
|
|
280
|
-
expect(updateFailureError(undefined, 'no connection method available').message)
|
|
281
|
-
.toBe('no connection method available')
|
|
282
|
-
expect(updateFailureError(null, 'no connection method available').message)
|
|
283
|
-
.toBe('no connection method available')
|
|
284
|
-
})
|
|
285
|
-
|
|
286
|
-
it('keeps a bluetooth timeout rather than masking it', () => {
|
|
287
|
-
const bleFailure = new Error('BLE update failed: Connection timeout')
|
|
288
|
-
|
|
289
|
-
expect(updateFailureError(bleFailure, 'no connection method available').message)
|
|
290
|
-
.toBe('BLE update failed: Connection timeout')
|
|
291
|
-
})
|
|
292
|
-
})
|