@homebridge-plugins/homebridge-govee 11.32.2-beta.10 → 11.32.2-beta.12
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 +4 -0
- package/lib/device/appliance-basic.js +94 -7
- package/lib/device/heater1a.js +10 -2
- package/lib/utils/capabilities.js +220 -0
- package/lib/utils/device-capabilities.js +1 -1
- package/lib/utils/functions.js +9 -0
- package/package.json +1 -1
- package/lib/utils/appliance-capabilities.js +0 -45
package/CHANGELOG.md
CHANGED
|
@@ -21,6 +21,10 @@ All notable changes to `@homebridge-plugins/homebridge-govee` will be documented
|
|
|
21
21
|
- feat: give every appliance type a starting handler, so a new model works for on and off and asks for the rest
|
|
22
22
|
- refactor: point models that behave identically at one shared handler instead of copies of the same file
|
|
23
23
|
- fix: stop shipping the test files to npm, since they are only needed in the repository
|
|
24
|
+
- fix: refuse to send a bluetooth command too long to fit in a packet, rather than sending a longer one the device quietly ignores
|
|
25
|
+
- test: record what every supported model turns into, so a change can be shown to have altered nothing
|
|
26
|
+
- feat: work out what an appliance can do from what the device itself reports, so a model nobody has written down still gets the right controls
|
|
27
|
+
- feat: offer oscillation and the child lock on any appliance that reports having them, driven through govee's api rather than needing that model's command codes
|
|
24
28
|
|
|
25
29
|
## v11.32.1 (2026-08-02)
|
|
26
30
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { describeSources, resolveControls } from '../utils/capabilities.js'
|
|
1
2
|
import { base64ToHex, getTwoItemPosition, hexToTwoItems, parseError } from '../utils/functions.js'
|
|
2
3
|
import platformLang from '../utils/lang-en.js'
|
|
3
4
|
import { logUnknownData, reportUnsupportedControl } from '../utils/report-unknown.js'
|
|
@@ -31,13 +32,26 @@ class ApplianceBasic {
|
|
|
31
32
|
this.accessory = accessory
|
|
32
33
|
this.config = config
|
|
33
34
|
|
|
35
|
+
// Work out what this particular device can do, rather than what its model
|
|
36
|
+
// generally can - for a model nobody has written down, what the device
|
|
37
|
+
// itself reported is all there is to go on
|
|
38
|
+
const resolved = resolveControls({
|
|
39
|
+
model: accessory.context.gvModel,
|
|
40
|
+
family: config.family,
|
|
41
|
+
firmware: accessory.context.firmware,
|
|
42
|
+
openApiCapabilities: accessory.context.openApiCapabilities,
|
|
43
|
+
deviceConf: platform.deviceConf?.[accessory.context.gvDeviceId],
|
|
44
|
+
})
|
|
45
|
+
this.controls = resolved.controls
|
|
46
|
+
this.cacheToggles = {}
|
|
47
|
+
this.discoveredControls = []
|
|
48
|
+
|
|
34
49
|
// Some appliances can have a richer tile, but only if the device reports
|
|
35
50
|
// the reading that tile needs - HomeKit's humidifier, for example, has to
|
|
36
51
|
// show a current humidity, and plenty of Govee humidifiers never send one.
|
|
37
|
-
// Where
|
|
38
|
-
//
|
|
39
|
-
const hasSensor = config.
|
|
40
|
-
&& !!accessory.context.openApiCapabilities?.[config.richCapability]
|
|
52
|
+
// Where the device says it has that sensor, use the better tile; otherwise
|
|
53
|
+
// fall back to the plain one, which needs nothing extra.
|
|
54
|
+
const hasSensor = config.richControl && this.controls[config.richControl]
|
|
41
55
|
const serviceName = (config.richService && hasSensor) ? config.richService : config.service
|
|
42
56
|
this.usingRichService = serviceName === config.richService
|
|
43
57
|
|
|
@@ -94,10 +108,78 @@ class ApplianceBasic {
|
|
|
94
108
|
this.cacheVariable = this.service.getCharacteristic(this.variableChar).value
|
|
95
109
|
}
|
|
96
110
|
|
|
111
|
+
// A control the device told us about can be driven through Govee's own api
|
|
112
|
+
// by name, without knowing this model's command bytes. So unlike the
|
|
113
|
+
// settings above, these work rather than asking to be reported.
|
|
114
|
+
this.addDiscoveredToggle('swing', 'SwingMode', 'oscillationToggle')
|
|
115
|
+
this.addDiscoveredToggle('lock', 'LockPhysicalControls', 'lockToggle')
|
|
116
|
+
|
|
97
117
|
// Output the customised options to the log
|
|
98
118
|
const opts = JSON.stringify({})
|
|
99
119
|
platform.log('[%s] %s %s.', accessory.displayName, platformLang.devInitOpts, opts)
|
|
100
|
-
this.accessory.
|
|
120
|
+
this.accessory.logDebug(`controls: ${describeSources(resolved.sources)}`)
|
|
121
|
+
|
|
122
|
+
const working = ['on and off', ...this.discoveredControls]
|
|
123
|
+
this.accessory.log(`${working.join(', ')} ${working.length > 1 ? 'are' : 'is'} supported for this model, the ${config.variableLabel || 'other settings'} are not known yet`)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Offer a control the device said it has, driven through Govee's api by the
|
|
128
|
+
* name the device gave it.
|
|
129
|
+
*
|
|
130
|
+
* This is the one place a model nobody has tested gets something that really
|
|
131
|
+
* works beyond power: the api takes the capability name, so the command bytes
|
|
132
|
+
* for this model never have to be known.
|
|
133
|
+
*/
|
|
134
|
+
addDiscoveredToggle(control, characteristic, instance) {
|
|
135
|
+
const char = this.hapChar[characteristic]
|
|
136
|
+
|
|
137
|
+
// The device has to have named this capability itself. Knowing the model
|
|
138
|
+
// usually has a turning head is not enough - the api is driven by the name
|
|
139
|
+
// the device reported, so without it there is nothing to send, and the
|
|
140
|
+
// switch would fail every time it was touched.
|
|
141
|
+
const reported = !!this.accessory.context.openApiCapabilities?.[instance]
|
|
142
|
+
|
|
143
|
+
if (!reported || !this.controls[control] || !char) {
|
|
144
|
+
if (this.service.testCharacteristic?.(char)) {
|
|
145
|
+
this.service.removeCharacteristic(this.service.getCharacteristic(char))
|
|
146
|
+
}
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
this.service
|
|
151
|
+
.getCharacteristic(char)
|
|
152
|
+
.onSet(async value => this.internalToggleUpdate(control, char, instance, value))
|
|
153
|
+
this.cacheToggles[control] = this.service.getCharacteristic(char).value
|
|
154
|
+
|
|
155
|
+
this.discoveredControls.push(control === 'swing' ? 'oscillation' : 'the child lock')
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async internalToggleUpdate(control, characteristic, instance, value) {
|
|
159
|
+
const previous = this.cacheToggles[control]
|
|
160
|
+
try {
|
|
161
|
+
if (previous === value) {
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
await this.platform.sendDeviceUpdate(this.accessory, {
|
|
166
|
+
cmd: 'openApi',
|
|
167
|
+
openApi: {
|
|
168
|
+
instance,
|
|
169
|
+
capabilityType: 'devices.capabilities.toggle',
|
|
170
|
+
value: value ? 1 : 0,
|
|
171
|
+
},
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
this.cacheToggles[control] = value
|
|
175
|
+
this.accessory.log(`current ${control} [${value ? 'on' : 'off'}]`)
|
|
176
|
+
} catch (err) {
|
|
177
|
+
this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`)
|
|
178
|
+
setTimeout(() => {
|
|
179
|
+
this.service.updateCharacteristic(characteristic, previous)
|
|
180
|
+
}, 2000)
|
|
181
|
+
throw new this.hapErr(-70402)
|
|
182
|
+
}
|
|
101
183
|
}
|
|
102
184
|
|
|
103
185
|
async internalStateUpdate(value) {
|
|
@@ -229,6 +311,7 @@ export function makeBasicAppliance(config) {
|
|
|
229
311
|
|
|
230
312
|
// Fan-shaped appliances: a speed slider that is not worked out yet
|
|
231
313
|
export const deviceFanBasic = makeBasicAppliance({
|
|
314
|
+
family: 'fan',
|
|
232
315
|
service: 'Fanv2',
|
|
233
316
|
variableChar: 'RotationSpeed',
|
|
234
317
|
variableLabel: 'fan speed',
|
|
@@ -236,6 +319,7 @@ export const deviceFanBasic = makeBasicAppliance({
|
|
|
236
319
|
|
|
237
320
|
// Ice makers reuse the fan tile, with the slider standing in for ice size
|
|
238
321
|
export const deviceIceMakerBasic = makeBasicAppliance({
|
|
322
|
+
family: 'iceMaker',
|
|
239
323
|
service: 'Fanv2',
|
|
240
324
|
variableChar: 'RotationSpeed',
|
|
241
325
|
variableLabel: 'ice size',
|
|
@@ -245,11 +329,12 @@ export const deviceIceMakerBasic = makeBasicAppliance({
|
|
|
245
329
|
// has to show a current humidity and many Govee humidifiers never report one.
|
|
246
330
|
// A model that does report it gets the proper humidifier tile instead.
|
|
247
331
|
export const deviceHumidifierBasic = makeBasicAppliance({
|
|
332
|
+
family: 'humidifier',
|
|
248
333
|
service: 'Fan',
|
|
249
334
|
variableChar: 'RotationSpeed',
|
|
250
335
|
variableLabel: 'mist level',
|
|
251
336
|
richService: 'HumidifierDehumidifier',
|
|
252
|
-
|
|
337
|
+
richControl: 'humiditySensor',
|
|
253
338
|
richVariableChar: 'RelativeHumidityHumidifierThreshold',
|
|
254
339
|
richReadingChar: 'CurrentRelativeHumidity',
|
|
255
340
|
currentStateChar: 'CurrentHumidifierDehumidifierState',
|
|
@@ -258,11 +343,12 @@ export const deviceHumidifierBasic = makeBasicAppliance({
|
|
|
258
343
|
})
|
|
259
344
|
|
|
260
345
|
export const deviceDehumidifierBasic = makeBasicAppliance({
|
|
346
|
+
family: 'dehumidifier',
|
|
261
347
|
service: 'Fan',
|
|
262
348
|
variableChar: 'RotationSpeed',
|
|
263
349
|
variableLabel: 'fan speed',
|
|
264
350
|
richService: 'HumidifierDehumidifier',
|
|
265
|
-
|
|
351
|
+
richControl: 'humiditySensor',
|
|
266
352
|
richVariableChar: 'RelativeHumidityDehumidifierThreshold',
|
|
267
353
|
richReadingChar: 'CurrentRelativeHumidity',
|
|
268
354
|
currentStateChar: 'CurrentHumidifierDehumidifierState',
|
|
@@ -271,6 +357,7 @@ export const deviceDehumidifierBasic = makeBasicAppliance({
|
|
|
271
357
|
})
|
|
272
358
|
|
|
273
359
|
export const devicePurifierBasic = makeBasicAppliance({
|
|
360
|
+
family: 'purifier',
|
|
274
361
|
service: 'AirPurifier',
|
|
275
362
|
variableChar: 'RotationSpeed',
|
|
276
363
|
variableLabel: 'fan speed',
|
package/lib/device/heater1a.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { describeSources, resolveControls } from '../utils/capabilities.js'
|
|
2
2
|
import {
|
|
3
3
|
base64ToHex,
|
|
4
4
|
farToCen,
|
|
@@ -94,7 +94,15 @@ export default class {
|
|
|
94
94
|
|
|
95
95
|
// Only offer the controls this model actually has, so a heater without a
|
|
96
96
|
// turning head does not get an oscillation switch that does nothing
|
|
97
|
-
|
|
97
|
+
const resolved = resolveControls({
|
|
98
|
+
model: this.accessory.context.gvModel,
|
|
99
|
+
family: 'heater1',
|
|
100
|
+
firmware: this.accessory.context.firmware,
|
|
101
|
+
openApiCapabilities: this.accessory.context.openApiCapabilities,
|
|
102
|
+
deviceConf: platform.deviceConf?.[this.accessory.context.gvDeviceId],
|
|
103
|
+
})
|
|
104
|
+
this.features = resolved.controls
|
|
105
|
+
this.accessory.logDebug(`controls: ${describeSources(resolved.sources)}`)
|
|
98
106
|
|
|
99
107
|
// Add the set handler to the heater swing mode characteristic (for oscillation)
|
|
100
108
|
if (this.features.swing) {
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a device can do, worked out from several sources rather than one table.
|
|
3
|
+
*
|
|
4
|
+
* Writing out all six hundred models by hand is a job nobody finishes, and it
|
|
5
|
+
* throws away the two things we already have: Govee's own API says what each
|
|
6
|
+
* device can do, and some behaviour depends on firmware rather than model. So
|
|
7
|
+
* instead of a table this asks a series of layers and takes the first answer:
|
|
8
|
+
*
|
|
9
|
+
* 1. user config an explicit setting always wins
|
|
10
|
+
* 2. firmware this model, at or above this firmware version
|
|
11
|
+
* 3. model written down because someone worked it out
|
|
12
|
+
* 4. discovered what this particular device reported it can do
|
|
13
|
+
* 5. family what this kind of appliance usually has
|
|
14
|
+
*
|
|
15
|
+
* Most models never need an entry of their own. They are answered by what the
|
|
16
|
+
* device reported plus the family defaults, which is why this scales to models
|
|
17
|
+
* nobody has written down.
|
|
18
|
+
*
|
|
19
|
+
* ⚠️ Family defaults are deliberately generous. A control this plugin has
|
|
20
|
+
* offered for years may be in use by someone, and taking it away to tidy up
|
|
21
|
+
* would break a working setup. Only a model entry - written after someone
|
|
22
|
+
* confirmed it - is allowed to say a device does not have something.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { compareVersions } from './device-capabilities.js'
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* What each kind of appliance usually offers.
|
|
29
|
+
*
|
|
30
|
+
* These are the fallback, so they describe the group rather than any one model.
|
|
31
|
+
* Where a group has historically been given a control, it stays here even if
|
|
32
|
+
* some members turn out not to have it - see the warning above.
|
|
33
|
+
*/
|
|
34
|
+
const familyControls = {
|
|
35
|
+
heater1: { swing: true, lock: true, light: false },
|
|
36
|
+
heater2: { swing: true, lock: true, light: true },
|
|
37
|
+
fan: { swing: true, lock: false, light: false },
|
|
38
|
+
humidifier: { swing: false, lock: false, light: false },
|
|
39
|
+
dehumidifier: { swing: false, lock: false, light: false },
|
|
40
|
+
purifier: { swing: false, lock: false, light: false },
|
|
41
|
+
iceMaker: { swing: false, lock: false, light: false },
|
|
42
|
+
diffuser: { swing: false, lock: false, light: false },
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const emptyControls = {
|
|
46
|
+
swing: false,
|
|
47
|
+
lock: false,
|
|
48
|
+
light: false,
|
|
49
|
+
humiditySensor: false,
|
|
50
|
+
temperatureSensor: false,
|
|
51
|
+
modes: false,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Controls worked out for a particular model.
|
|
56
|
+
*
|
|
57
|
+
* Only add a model here once its controls are actually known. An entry is
|
|
58
|
+
* trusted over what the device reports, and it is the only thing allowed to
|
|
59
|
+
* take a control away.
|
|
60
|
+
*/
|
|
61
|
+
const modelControls = {
|
|
62
|
+
// Heaters. These match what Govee's own app offers for each model.
|
|
63
|
+
// Added new, so there is nobody whose setup could be broken by them
|
|
64
|
+
H7137: { swing: false, lock: false, light: false },
|
|
65
|
+
H713E: { swing: false, lock: false, light: false },
|
|
66
|
+
|
|
67
|
+
// Diffusers are power only, which is what the plugin already offers
|
|
68
|
+
H7161: { swing: false, lock: false, light: false },
|
|
69
|
+
H7162: { swing: false, lock: false, light: false },
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Controls a model is believed NOT to have, which are still being offered.
|
|
74
|
+
*
|
|
75
|
+
* These are recorded rather than applied. Each has been offered for a long
|
|
76
|
+
* time, so an owner may be relying on it, and taking it away to match a
|
|
77
|
+
* capability list would break a working setup for the sake of tidiness. The
|
|
78
|
+
* evidence is written down here so nobody has to work it out twice, and so
|
|
79
|
+
* there is a list to check the next time an owner of one of these gets in
|
|
80
|
+
* touch.
|
|
81
|
+
*
|
|
82
|
+
* Move an entry into `modelControls` once an owner confirms the control really
|
|
83
|
+
* does nothing.
|
|
84
|
+
*/
|
|
85
|
+
const unconfirmedMissing = {
|
|
86
|
+
H713A: { swing: false },
|
|
87
|
+
H713B: { swing: false },
|
|
88
|
+
H713C: { swing: false },
|
|
89
|
+
H7135: { swing: false, light: false },
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Controls that only appear at or above a firmware version. Same shape as the
|
|
94
|
+
* protocol overrides next door: a list per model, oldest rule first.
|
|
95
|
+
*/
|
|
96
|
+
const firmwareControls = {}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* What Govee's API calls a capability, and which control it means to us.
|
|
100
|
+
*
|
|
101
|
+
* These names come back per device, so they say what this particular unit can
|
|
102
|
+
* do rather than what its model generally can. That makes them the only source
|
|
103
|
+
* here that works for a model nobody has written down.
|
|
104
|
+
*/
|
|
105
|
+
const discoveryMap = {
|
|
106
|
+
oscillationToggle: 'swing',
|
|
107
|
+
lockToggle: 'lock',
|
|
108
|
+
nightlightToggle: 'light',
|
|
109
|
+
mainLightToggle: 'light',
|
|
110
|
+
backgroundLightToggle: 'light',
|
|
111
|
+
sensorHumidity: 'humiditySensor',
|
|
112
|
+
sensorTemperature: 'temperatureSensor',
|
|
113
|
+
workMode: 'modes',
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Turn the capability list a device reported into controls.
|
|
118
|
+
*
|
|
119
|
+
* Only ever adds. A capability the device did not mention is left undecided so
|
|
120
|
+
* a later layer can answer, because the API not listing something is not proof
|
|
121
|
+
* the device cannot do it.
|
|
122
|
+
*/
|
|
123
|
+
function fromDiscovery(openApiCapabilities) {
|
|
124
|
+
const discovered = {}
|
|
125
|
+
Object.keys(openApiCapabilities || {}).forEach((instance) => {
|
|
126
|
+
const control = discoveryMap[instance]
|
|
127
|
+
if (control) {
|
|
128
|
+
discovered[control] = true
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
return discovered
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Config settings that name a control directly, so an owner can turn something
|
|
136
|
+
* off that their device does not really have without waiting for a release.
|
|
137
|
+
*/
|
|
138
|
+
function fromConfig(deviceConf) {
|
|
139
|
+
const chosen = {}
|
|
140
|
+
if (deviceConf?.hideLight === true) {
|
|
141
|
+
chosen.light = false
|
|
142
|
+
}
|
|
143
|
+
if (deviceConf?.hideSwing === true) {
|
|
144
|
+
chosen.swing = false
|
|
145
|
+
}
|
|
146
|
+
return chosen
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function firmwareLayer(model, firmware) {
|
|
150
|
+
if (!firmware) {
|
|
151
|
+
return {}
|
|
152
|
+
}
|
|
153
|
+
const applied = {}
|
|
154
|
+
const rules = [...(firmwareControls[model] || [])]
|
|
155
|
+
.sort((a, b) => compareVersions(a.minVersion, b.minVersion))
|
|
156
|
+
rules.forEach((rule) => {
|
|
157
|
+
if (compareVersions(firmware, rule.minVersion) >= 0) {
|
|
158
|
+
Object.assign(applied, rule.controls)
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
return applied
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Work out what a device can do.
|
|
166
|
+
*
|
|
167
|
+
* @param {object} context describing the device
|
|
168
|
+
* @param {string} context.model the govee model, eg 'H7130'
|
|
169
|
+
* @param {string} [context.family] which set of defaults to fall back on
|
|
170
|
+
* @param {string} [context.firmware] the firmware version the device reported
|
|
171
|
+
* @param {object} [context.openApiCapabilities] what the api said it can do
|
|
172
|
+
* @param {object} [context.deviceConf] this device's settings
|
|
173
|
+
* @returns {{ controls: object, sources: object }} the controls, and which
|
|
174
|
+
* layer decided each one - the second is for the log, so an unexpected answer
|
|
175
|
+
* can be traced without guessing
|
|
176
|
+
*/
|
|
177
|
+
export function resolveControls(context = {}) {
|
|
178
|
+
const {
|
|
179
|
+
model,
|
|
180
|
+
family,
|
|
181
|
+
firmware,
|
|
182
|
+
openApiCapabilities,
|
|
183
|
+
deviceConf,
|
|
184
|
+
} = context
|
|
185
|
+
|
|
186
|
+
// Lowest priority first, so a later layer overwrites an earlier one
|
|
187
|
+
const layers = [
|
|
188
|
+
['family', familyControls[family] || {}],
|
|
189
|
+
['discovered', fromDiscovery(openApiCapabilities)],
|
|
190
|
+
['model', modelControls[model] || {}],
|
|
191
|
+
['firmware', firmwareLayer(model, firmware)],
|
|
192
|
+
['config', fromConfig(deviceConf)],
|
|
193
|
+
]
|
|
194
|
+
|
|
195
|
+
const controls = { ...emptyControls }
|
|
196
|
+
const sources = {}
|
|
197
|
+
|
|
198
|
+
layers.forEach(([name, values]) => {
|
|
199
|
+
Object.entries(values).forEach(([control, value]) => {
|
|
200
|
+
controls[control] = value
|
|
201
|
+
sources[control] = name
|
|
202
|
+
})
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
return { controls, sources }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* A one-line summary of where each answer came from, for the debug log. Reading
|
|
210
|
+
* "swing from family" tells you nobody has confirmed it, which is a different
|
|
211
|
+
* situation from "swing from model".
|
|
212
|
+
*/
|
|
213
|
+
export function describeSources(sources) {
|
|
214
|
+
return Object.entries(sources)
|
|
215
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
216
|
+
.map(([control, source]) => `${control} from ${source}`)
|
|
217
|
+
.join(', ') || 'nothing known'
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export { familyControls, modelControls, unconfirmedMissing }
|
|
@@ -80,7 +80,7 @@ const firmwareOverrides = {
|
|
|
80
80
|
* newer than `b`, negative if older, and zero if they match. Missing parts
|
|
81
81
|
* count as zero, so "1.03" and "1.03.00" are equal.
|
|
82
82
|
*/
|
|
83
|
-
function compareVersions(a, b) {
|
|
83
|
+
export function compareVersions(a, b) {
|
|
84
84
|
const aParts = String(a).split('.')
|
|
85
85
|
const bParts = String(b).split('.')
|
|
86
86
|
for (let i = 0; i < Math.max(aParts.length, bParts.length); i += 1) {
|
package/lib/utils/functions.js
CHANGED
|
@@ -22,6 +22,15 @@ const farToCen = temp => Math.round(((temp - 32) * 5) / 9)
|
|
|
22
22
|
*/
|
|
23
23
|
function generateCodeFromHexValues(hexValues, returnAsHexBuffer = false) {
|
|
24
24
|
const cmdSection = Buffer.from(hexValues.flat())
|
|
25
|
+
|
|
26
|
+
// More than 19 bytes leaves no room for the checksum. Without this the
|
|
27
|
+
// padding length goes negative, which produces an empty pad rather than an
|
|
28
|
+
// error, and the result is a packet longer than 20 bytes that the device
|
|
29
|
+
// will not understand - with nothing said about why
|
|
30
|
+
if (cmdSection.length > 19) {
|
|
31
|
+
throw new Error(`Command is ${cmdSection.length} bytes, which is longer than a packet allows`)
|
|
32
|
+
}
|
|
33
|
+
|
|
25
34
|
const padSection = Buffer.from(Array.from({ length: 19 - cmdSection.length }).fill(0))
|
|
26
35
|
const noXSection = Buffer.concat([cmdSection, padSection])
|
|
27
36
|
let checksum = 0
|
package/package.json
CHANGED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* What each appliance model can actually do.
|
|
3
|
-
*
|
|
4
|
-
* Appliance handlers are shared between models, which historically meant every
|
|
5
|
-
* model in a group was given every control the group could offer - so a heater
|
|
6
|
-
* without a turning head still got an oscillation switch that did nothing. This
|
|
7
|
-
* map lets a shared handler offer only the controls a model really has, without
|
|
8
|
-
* needing a separate file per model.
|
|
9
|
-
*
|
|
10
|
-
* A model that is not listed keeps the handler's full set of controls, so
|
|
11
|
-
* adding an entry is opt-in and nothing changes for a model until it is
|
|
12
|
-
* described here.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Heaters.
|
|
17
|
-
*
|
|
18
|
-
* ⚠️ Several models already in the plugin are believed not to have oscillation
|
|
19
|
-
* (the H713A, H713B, H713C and H7135), but they have been offered it for a long
|
|
20
|
-
* time. They are deliberately NOT restricted here: if any owner is using that
|
|
21
|
-
* control today, taking it away would break a working setup for the sake of
|
|
22
|
-
* tidiness. Restrict them only once an owner has confirmed the control does
|
|
23
|
-
* nothing. New models are added with their real capabilities from the start,
|
|
24
|
-
* where there is nobody to break.
|
|
25
|
-
*/
|
|
26
|
-
const heaterFeatures = {
|
|
27
|
-
// Power and mode only - no turning head, no night light
|
|
28
|
-
H7137: { swing: false, lock: false },
|
|
29
|
-
H713E: { swing: false, lock: false },
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const heaterDefaults = {
|
|
33
|
-
swing: true,
|
|
34
|
-
lock: true,
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Get the controls a heater model should offer.
|
|
39
|
-
*
|
|
40
|
-
* @param {string} model the govee model, eg 'H7130'
|
|
41
|
-
* @returns {{ swing: boolean, lock: boolean }} the controls to offer
|
|
42
|
-
*/
|
|
43
|
-
export function getHeaterFeatures(model) {
|
|
44
|
-
return { ...heaterDefaults, ...(heaterFeatures[model] || {}) }
|
|
45
|
-
}
|