@homebridge-plugins/homebridge-govee 11.32.2-beta.6 → 11.32.2-beta.7
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 +1 -0
- package/CONTRIBUTING-STRUCTURE.md +66 -0
- package/lib/device/index.js +14 -28
- package/package.json +1 -1
- package/lib/device/diffuser-H7162.js +0 -129
- package/lib/device/fan-H7102.js +0 -290
- package/lib/device/fan-H7106.js +0 -290
- package/lib/device/humidifier-H7145.js +0 -193
- package/lib/device/humidifier-H7147.js +0 -193
- package/lib/device/humidifier-H7148.js +0 -193
- package/lib/device/humidifier-H7149.js +0 -193
- package/lib/device/humidifier-H714E.js +0 -193
- package/lib/device/ice-maker-H717D.js +0 -224
- package/lib/device/ice-maker-H8120.js +0 -224
- package/lib/device/purifier-H7127.js +0 -365
- package/lib/device/purifier-H7128.js +0 -365
- package/lib/device/purifier-H7129.js +0 -365
- package/lib/device/purifier-H712C.js +0 -365
package/CHANGELOG.md
CHANGED
|
@@ -19,6 +19,7 @@ All notable changes to `@homebridge-plugins/homebridge-govee` will be documented
|
|
|
19
19
|
- feat: add 24 more light models, three thermo-hygrometers and a leak detector
|
|
20
20
|
- feat: add the H7103 and H7112 fans and the H5085 plug, with unknown controls saying so instead of guessing
|
|
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
|
+
- refactor: point models that behave identically at one shared handler instead of copies of the same file
|
|
22
23
|
|
|
23
24
|
## v11.32.1 (2026-08-02)
|
|
24
25
|
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# How this plugin is put together, and how to extend it
|
|
2
|
+
|
|
3
|
+
This is the working reference for adding models and functionality. It exists so
|
|
4
|
+
that the next change follows the same shape as the last, whoever makes it.
|
|
5
|
+
|
|
6
|
+
## The three ways a model gets support
|
|
7
|
+
|
|
8
|
+
**1. It shares behaviour with a model already handled — alias it.**
|
|
9
|
+
Nothing new is written. `lib/device/index.js` maps the new model's key to the
|
|
10
|
+
existing handler:
|
|
11
|
+
|
|
12
|
+
```text
|
|
13
|
+
deviceHumidifierH7147: deviceHumidifierH7143,
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
This is the common case. Several Govee models are the same device with a
|
|
17
|
+
different name, and a handler copied per model is a handler that has to be fixed
|
|
18
|
+
per model.
|
|
19
|
+
|
|
20
|
+
**2. It is a new model of a known type, but the details are unknown — let it
|
|
21
|
+
say so.** Add it to the model list in `lib/utils/constants.js` and nothing else.
|
|
22
|
+
The dispatch falls back to `appliance-basic.js`, which offers on and off (that
|
|
23
|
+
command is identical across every Govee appliance) and makes every other control
|
|
24
|
+
report what it needs and then fail, so the owner sees it in the Home app, checks
|
|
25
|
+
the log, and finds a line to paste into an issue.
|
|
26
|
+
|
|
27
|
+
**3. It genuinely behaves differently — write a handler.**
|
|
28
|
+
Only when the controls or their commands really differ.
|
|
29
|
+
|
|
30
|
+
## Where model knowledge lives
|
|
31
|
+
|
|
32
|
+
Per-model differences belong in data, not in a copied file:
|
|
33
|
+
|
|
34
|
+
| File | Holds |
|
|
35
|
+
|---|---|
|
|
36
|
+
| `lib/utils/constants.js` | which models are which type |
|
|
37
|
+
| `lib/utils/device-capabilities.js` | protocol quirks — colour command, brightness scale, and firmware-dependent behaviour |
|
|
38
|
+
| `lib/utils/appliance-capabilities.js` | which controls a model actually has |
|
|
39
|
+
|
|
40
|
+
If a change means copying a file and editing two lines, it belongs in one of
|
|
41
|
+
these instead.
|
|
42
|
+
|
|
43
|
+
## Reporting anything unknown
|
|
44
|
+
|
|
45
|
+
Never log an unrecognised payload by hand. Use `lib/utils/report-unknown.js`:
|
|
46
|
+
|
|
47
|
+
- `logUnknownData` — something arrived that is not understood
|
|
48
|
+
- `reportUnsupportedControl` — the user asked for something not worked out yet
|
|
49
|
+
|
|
50
|
+
Both produce one line carrying the model, firmware, hardware, connection and
|
|
51
|
+
payload, so a single copy-paste is enough to act on. Both warn once and then
|
|
52
|
+
drop to debug, so a device repeating itself does not flood the log.
|
|
53
|
+
|
|
54
|
+
## Things worth knowing before changing appliance code
|
|
55
|
+
|
|
56
|
+
- **The power command is universal.** `33 01 01` on, `33 01 00` off, on every
|
|
57
|
+
appliance. Safe to send for an untested model.
|
|
58
|
+
- **Frames are 20 bytes with an XOR checksum** — byte 20 is the XOR of bytes
|
|
59
|
+
1–19. All 407 shipped appliance commands have been verified against this.
|
|
60
|
+
- **A control that silently does nothing is worse than one that fails.** Failing
|
|
61
|
+
shows up in the Home app and sends the owner to the log.
|
|
62
|
+
- **Do not remove a control a model has offered for a long time** on evidence
|
|
63
|
+
alone. Someone may be using it. Confirm with an owner first.
|
|
64
|
+
- **HomeKit tiles have requirements.** A humidifier must report a current
|
|
65
|
+
humidity, which many Govee humidifiers never send — hence the plain fan tile
|
|
66
|
+
by default, and the richer one only when the device says it has the sensor.
|
package/lib/device/index.js
CHANGED
|
@@ -10,33 +10,23 @@ import deviceDehumidifierH7150 from './dehumidifier-H7150.js'
|
|
|
10
10
|
import deviceDehumidifierH7151 from './dehumidifier-H7151.js'
|
|
11
11
|
import deviceDehumidifierH7152 from './dehumidifier-H7152.js'
|
|
12
12
|
import deviceDiffuserH7161 from './diffuser-H7161.js'
|
|
13
|
-
import deviceDiffuserH7162 from './diffuser-H7162.js'
|
|
14
13
|
import deviceFanH1310 from './fan-H1310.js'
|
|
15
14
|
import deviceFanH1370 from './fan-H1370.js'
|
|
16
15
|
import deviceFanH7100 from './fan-H7100.js'
|
|
17
16
|
import deviceFanH7101 from './fan-H7101.js'
|
|
18
|
-
import deviceFanH7102 from './fan-H7102.js'
|
|
19
17
|
import deviceFanH7105 from './fan-H7105.js'
|
|
20
|
-
import deviceFanH7106 from './fan-H7106.js'
|
|
21
18
|
import deviceFanH7107 from './fan-H7107.js'
|
|
22
19
|
import deviceFanH7111 from './fan-H7111.js'
|
|
23
20
|
import deviceHeater1A from './heater1a.js'
|
|
24
21
|
import deviceHeater1B from './heater1b.js'
|
|
25
22
|
import deviceHeater2 from './heater2.js'
|
|
26
23
|
import deviceHeaterSingle from './heater-single.js'
|
|
27
|
-
import deviceHumidifierH714E from './humidifier-H714E.js'
|
|
28
24
|
import deviceHumidifierH7140 from './humidifier-H7140.js'
|
|
29
25
|
import deviceHumidifierH7141 from './humidifier-H7141.js'
|
|
30
26
|
import deviceHumidifierH7142 from './humidifier-H7142.js'
|
|
31
27
|
import deviceHumidifierH7143 from './humidifier-H7143.js'
|
|
32
|
-
import deviceHumidifierH7145 from './humidifier-H7145.js'
|
|
33
|
-
import deviceHumidifierH7147 from './humidifier-H7147.js'
|
|
34
|
-
import deviceHumidifierH7148 from './humidifier-H7148.js'
|
|
35
|
-
import deviceHumidifierH7149 from './humidifier-H7149.js'
|
|
36
28
|
import deviceHumidifierH7160 from './humidifier-H7160.js'
|
|
37
|
-
import deviceIceMakerH717D from './ice-maker-H717D.js'
|
|
38
29
|
import deviceIceMakerH7172 from './ice-maker-H7172.js'
|
|
39
|
-
import deviceIceMakerH8120 from './ice-maker-H8120.js'
|
|
40
30
|
import deviceIceMakerH8121 from './ice-maker-H8121.js'
|
|
41
31
|
import deviceIceMakerH8122 from './ice-maker-H8122.js'
|
|
42
32
|
import deviceKettle from './kettle.js'
|
|
@@ -45,16 +35,12 @@ import deviceLight from './light.js'
|
|
|
45
35
|
import deviceOutletDouble from './outlet-double.js'
|
|
46
36
|
import deviceOutletSingle from './outlet-single.js'
|
|
47
37
|
import deviceOutletTriple from './outlet-triple.js'
|
|
48
|
-
import devicePurifierH712C from './purifier-H712C.js'
|
|
49
38
|
import devicePurifierH7120 from './purifier-H7120.js'
|
|
50
39
|
import devicePurifierH7121 from './purifier-H7121.js'
|
|
51
40
|
import devicePurifierH7122 from './purifier-H7122.js'
|
|
52
41
|
import devicePurifierH7123 from './purifier-H7123.js'
|
|
53
42
|
import devicePurifierH7124 from './purifier-H7124.js'
|
|
54
43
|
import devicePurifierH7126 from './purifier-H7126.js'
|
|
55
|
-
import devicePurifierH7127 from './purifier-H7127.js'
|
|
56
|
-
import devicePurifierH7128 from './purifier-H7128.js'
|
|
57
|
-
import devicePurifierH7129 from './purifier-H7129.js'
|
|
58
44
|
import devicePurifierSingle from './purifier-single.js'
|
|
59
45
|
import deviceSensorButton from './sensor-button.js'
|
|
60
46
|
import deviceSensorCO2 from './sensor-co2.js'
|
|
@@ -79,17 +65,17 @@ export default {
|
|
|
79
65
|
deviceDehumidifierH7151,
|
|
80
66
|
deviceDehumidifierH7152,
|
|
81
67
|
deviceDiffuserH7161,
|
|
82
|
-
deviceDiffuserH7162,
|
|
68
|
+
deviceDiffuserH7162: deviceDiffuserH7161, // identical to the H7161
|
|
83
69
|
deviceDehumidifierBasic,
|
|
84
70
|
deviceFanBasic,
|
|
85
71
|
deviceFanH1310,
|
|
86
72
|
deviceFanH1370,
|
|
87
73
|
deviceFanR1310: deviceFanH1310, // R1310 is the newer naming of the H1310
|
|
88
74
|
deviceFanH7100,
|
|
75
|
+
deviceFanH7102: deviceFanH7100, // identical to the H7100
|
|
76
|
+
deviceFanH7106: deviceFanH7100, // identical to the H7100
|
|
89
77
|
deviceFanH7101,
|
|
90
|
-
deviceFanH7102,
|
|
91
78
|
deviceFanH7105,
|
|
92
|
-
deviceFanH7106,
|
|
93
79
|
deviceFanH7107,
|
|
94
80
|
deviceFanH7111,
|
|
95
81
|
deviceHeaterSingle,
|
|
@@ -97,20 +83,20 @@ export default {
|
|
|
97
83
|
deviceHeater1B,
|
|
98
84
|
deviceHeater2,
|
|
99
85
|
deviceHumidifierBasic,
|
|
100
|
-
deviceHumidifierH714E,
|
|
101
86
|
deviceHumidifierH7140,
|
|
102
87
|
deviceHumidifierH7141,
|
|
103
88
|
deviceHumidifierH7142,
|
|
104
89
|
deviceHumidifierH7143,
|
|
105
|
-
deviceHumidifierH7145,
|
|
106
|
-
deviceHumidifierH7147,
|
|
107
|
-
deviceHumidifierH7148,
|
|
108
|
-
deviceHumidifierH7149,
|
|
90
|
+
deviceHumidifierH7145: deviceHumidifierH7143, // identical to the H7143
|
|
91
|
+
deviceHumidifierH7147: deviceHumidifierH7143, // identical to the H7143
|
|
92
|
+
deviceHumidifierH7148: deviceHumidifierH7143, // identical to the H7143
|
|
93
|
+
deviceHumidifierH7149: deviceHumidifierH7143, // identical to the H7143
|
|
94
|
+
deviceHumidifierH714E: deviceHumidifierH7143, // identical to the H7143
|
|
109
95
|
deviceHumidifierH7160,
|
|
110
96
|
deviceIceMakerBasic,
|
|
111
97
|
deviceIceMakerH7172,
|
|
112
|
-
deviceIceMakerH717D,
|
|
113
|
-
deviceIceMakerH8120,
|
|
98
|
+
deviceIceMakerH717D: deviceIceMakerH7172, // identical to the H7172
|
|
99
|
+
deviceIceMakerH8120: deviceIceMakerH7172, // identical to the H7172
|
|
114
100
|
deviceIceMakerH8121,
|
|
115
101
|
deviceIceMakerH8122,
|
|
116
102
|
deviceKettle,
|
|
@@ -120,16 +106,16 @@ export default {
|
|
|
120
106
|
deviceOutletSingle,
|
|
121
107
|
deviceOutletTriple,
|
|
122
108
|
devicePurifierBasic,
|
|
123
|
-
devicePurifierH712C,
|
|
124
109
|
devicePurifierH7120,
|
|
125
110
|
devicePurifierH7121,
|
|
126
111
|
devicePurifierH7122,
|
|
127
112
|
devicePurifierH7123,
|
|
128
113
|
devicePurifierH7124,
|
|
129
114
|
devicePurifierH7126,
|
|
130
|
-
devicePurifierH7127,
|
|
131
|
-
devicePurifierH7128,
|
|
132
|
-
devicePurifierH7129,
|
|
115
|
+
devicePurifierH7127: devicePurifierH7126, // identical to the H7126
|
|
116
|
+
devicePurifierH7128: devicePurifierH7126, // identical to the H7126
|
|
117
|
+
devicePurifierH7129: devicePurifierH7126, // identical to the H7126
|
|
118
|
+
devicePurifierH712C: devicePurifierH7126, // identical to the H7126
|
|
133
119
|
devicePurifierSingle,
|
|
134
120
|
deviceSensorButton,
|
|
135
121
|
deviceSensorCO2,
|
package/package.json
CHANGED
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
base64ToHex,
|
|
3
|
-
getTwoItemPosition,
|
|
4
|
-
hexToTwoItems,
|
|
5
|
-
parseError,
|
|
6
|
-
} from '../utils/functions.js'
|
|
7
|
-
import platformLang from '../utils/lang-en.js'
|
|
8
|
-
import { logUnknownData } from '../utils/report-unknown.js'
|
|
9
|
-
|
|
10
|
-
export default class {
|
|
11
|
-
constructor(platform, accessory) {
|
|
12
|
-
// Set up variables from the platform
|
|
13
|
-
this.hapChar = platform.api.hap.Characteristic
|
|
14
|
-
this.hapErr = platform.api.hap.HapStatusError
|
|
15
|
-
this.hapServ = platform.api.hap.Service
|
|
16
|
-
this.platform = platform
|
|
17
|
-
|
|
18
|
-
// Set up variables from the accessory
|
|
19
|
-
this.accessory = accessory
|
|
20
|
-
|
|
21
|
-
// Remove fan service if exists
|
|
22
|
-
if (this.accessory.getService(this.hapServ.Fan)) {
|
|
23
|
-
this.accessory.removeService(this.accessory.getService(this.hapServ.Fan))
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Add the purifier service if it doesn't already exist
|
|
27
|
-
this.service = this.accessory.getService(this.hapServ.AirPurifier)
|
|
28
|
-
|| this.accessory.addService(this.hapServ.AirPurifier)
|
|
29
|
-
|
|
30
|
-
// Add the set handler to the switch on/off characteristic
|
|
31
|
-
this.service.getCharacteristic(this.hapChar.Active).onSet(async (value) => {
|
|
32
|
-
await this.internalStateUpdate(value)
|
|
33
|
-
})
|
|
34
|
-
this.cacheState = this.service.getCharacteristic(this.hapChar.Active).value === 1 ? 'on' : 'off'
|
|
35
|
-
|
|
36
|
-
// Add options to the purifier target state characteristic
|
|
37
|
-
this.service
|
|
38
|
-
.getCharacteristic(this.hapChar.TargetAirPurifierState)
|
|
39
|
-
.updateValue(1)
|
|
40
|
-
.setProps({
|
|
41
|
-
minValue: 1,
|
|
42
|
-
maxValue: 1,
|
|
43
|
-
validValues: [1],
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
// Output the customised options to the log
|
|
47
|
-
const opts = JSON.stringify({})
|
|
48
|
-
platform.log('[%s] %s %s.', accessory.displayName, platformLang.devInitOpts, opts)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async internalStateUpdate(value) {
|
|
52
|
-
try {
|
|
53
|
-
const newValue = value === 1 ? 'on' : 'off'
|
|
54
|
-
|
|
55
|
-
// Don't continue if the new value is the same as before
|
|
56
|
-
if (this.cacheState === newValue) {
|
|
57
|
-
return
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Send the request to the platform sender function
|
|
61
|
-
await this.platform.sendDeviceUpdate(this.accessory, {
|
|
62
|
-
cmd: 'stateHumi',
|
|
63
|
-
value,
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
// Cache the new state and log if appropriate
|
|
67
|
-
if (this.cacheState !== newValue) {
|
|
68
|
-
this.cacheState = newValue
|
|
69
|
-
this.accessory.log(`${platformLang.curState} [${this.cacheState}]`)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
this.service.updateCharacteristic(this.hapChar.CurrentAirPurifierState, value === 1 ? 2 : 0)
|
|
73
|
-
} catch (err) {
|
|
74
|
-
// Catch any errors during the process
|
|
75
|
-
this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`)
|
|
76
|
-
|
|
77
|
-
// Throw a 'no response' error and set a timeout to revert this after 2 seconds
|
|
78
|
-
setTimeout(() => {
|
|
79
|
-
this.service.updateCharacteristic(this.hapChar.Active, this.cacheState === 'on' ? 1 : 0)
|
|
80
|
-
}, 2000)
|
|
81
|
-
throw new this.hapErr(-70402)
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
externalUpdate(params) {
|
|
86
|
-
// Check for an ON/OFF change
|
|
87
|
-
if (params.state && params.state !== this.cacheState) {
|
|
88
|
-
this.cacheState = params.state
|
|
89
|
-
this.service.updateCharacteristic(this.hapChar.Active, this.cacheState === 'on' ? 1 : 0)
|
|
90
|
-
this.service.updateCharacteristic(
|
|
91
|
-
this.hapChar.CurrentAirPurifierState,
|
|
92
|
-
this.cacheState === 'on' ? 2 : 0,
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
// Log the change
|
|
96
|
-
this.accessory.log(`${platformLang.curState} [${this.cacheState}]`)
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Handle OpenAPI workMode
|
|
100
|
-
if (params.workMode) {
|
|
101
|
-
const mode = params.workMode.modeValue
|
|
102
|
-
this.accessory.log(`${platformLang.curMode} [${mode}]`)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Check for some other scene/mode change
|
|
106
|
-
(params.commands || []).forEach((command) => {
|
|
107
|
-
const hexString = base64ToHex(command)
|
|
108
|
-
const hexParts = hexToTwoItems(hexString)
|
|
109
|
-
|
|
110
|
-
// Return now if not a device query update code
|
|
111
|
-
if (getTwoItemPosition(hexParts, 1) !== 'aa') {
|
|
112
|
-
return
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const deviceFunction = `${getTwoItemPosition(hexParts, 1)}${getTwoItemPosition(hexParts, 2)}`
|
|
116
|
-
|
|
117
|
-
switch (deviceFunction) {
|
|
118
|
-
default:
|
|
119
|
-
logUnknownData(this.accessory, {
|
|
120
|
-
kind: 'scene',
|
|
121
|
-
source: params.source,
|
|
122
|
-
raw: command,
|
|
123
|
-
hex: hexString,
|
|
124
|
-
})
|
|
125
|
-
break
|
|
126
|
-
}
|
|
127
|
-
})
|
|
128
|
-
}
|
|
129
|
-
}
|
package/lib/device/fan-H7102.js
DELETED
|
@@ -1,290 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
base64ToHex,
|
|
3
|
-
fanSpeedToHkPercent,
|
|
4
|
-
getTwoItemPosition,
|
|
5
|
-
hexToTwoItems,
|
|
6
|
-
hkPercentToFanSpeed,
|
|
7
|
-
parseError,
|
|
8
|
-
} from '../utils/functions.js'
|
|
9
|
-
import platformLang from '../utils/lang-en.js'
|
|
10
|
-
import { logUnknownData } from '../utils/report-unknown.js'
|
|
11
|
-
|
|
12
|
-
export default class {
|
|
13
|
-
constructor(platform, accessory) {
|
|
14
|
-
// Set up variables from the platform
|
|
15
|
-
this.hapChar = platform.api.hap.Characteristic
|
|
16
|
-
this.hapErr = platform.api.hap.HapStatusError
|
|
17
|
-
this.hapServ = platform.api.hap.Service
|
|
18
|
-
this.platform = platform
|
|
19
|
-
|
|
20
|
-
// Set up variables from the accessory
|
|
21
|
-
this.accessory = accessory
|
|
22
|
-
|
|
23
|
-
// Codes etc
|
|
24
|
-
this.speedCodes = {
|
|
25
|
-
1: 'MwUBAQAAAAAAAAAAAAAAAAAAADY=',
|
|
26
|
-
2: 'MwUBAgAAAAAAAAAAAAAAAAAAADU=',
|
|
27
|
-
3: 'MwUBAwAAAAAAAAAAAAAAAAAAADQ=',
|
|
28
|
-
4: 'MwUBBAAAAAAAAAAAAAAAAAAAADM=',
|
|
29
|
-
5: 'MwUBBQAAAAAAAAAAAAAAAAAAADI=',
|
|
30
|
-
6: 'MwUBBgAAAAAAAAAAAAAAAAAAADE=',
|
|
31
|
-
7: 'MwUBBwAAAAAAAAAAAAAAAAAAADA=',
|
|
32
|
-
8: 'MwUBCAAAAAAAAAAAAAAAAAAAAD8=',
|
|
33
|
-
9: 'MwUAAwAAAAAAAAAAAAAAAAAAADU=', // auto mode
|
|
34
|
-
}
|
|
35
|
-
this.autoSpeed = 9
|
|
36
|
-
// Real speed steps, excluding auto - HomeKit percentages map onto these
|
|
37
|
-
this.speedSteps = 8
|
|
38
|
-
|
|
39
|
-
// Remove any old original Fan services
|
|
40
|
-
if (this.accessory.getService(this.hapServ.Fan)) {
|
|
41
|
-
this.accessory.removeService(this.accessory.getService(this.hapServ.Fan))
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Rebuild the fan service if it was cached under an older speed scale, as
|
|
45
|
-
// HomeKit keeps the old props until the service itself is replaced
|
|
46
|
-
const existingService = this.accessory.getService(this.hapServ.Fanv2)
|
|
47
|
-
if (existingService) {
|
|
48
|
-
const { props } = existingService.getCharacteristic(this.hapChar.RotationSpeed)
|
|
49
|
-
if (props.unit === 'percentage' || props.maxValue !== 100) {
|
|
50
|
-
this.accessory.removeService(existingService)
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Add the fan service for the fan if it doesn't already exist
|
|
55
|
-
this.service = this.accessory.getService(this.hapServ.Fanv2) || this.accessory.addService(this.hapServ.Fanv2)
|
|
56
|
-
|
|
57
|
-
// Add the set handler to the fan on/off characteristic
|
|
58
|
-
this.service
|
|
59
|
-
.getCharacteristic(this.hapChar.Active)
|
|
60
|
-
.onSet(async value => this.internalStateUpdate(value))
|
|
61
|
-
this.cacheState = this.service.getCharacteristic(this.hapChar.Active).value ? 'on' : 'off'
|
|
62
|
-
|
|
63
|
-
// Add the set handler to the fan rotation speed characteristic
|
|
64
|
-
this.service
|
|
65
|
-
.getCharacteristic(this.hapChar.RotationSpeed)
|
|
66
|
-
.setProps({
|
|
67
|
-
maxValue: 100,
|
|
68
|
-
minStep: 1,
|
|
69
|
-
minValue: 0,
|
|
70
|
-
unit: 'unitless',
|
|
71
|
-
})
|
|
72
|
-
.onSet(async value => this.internalSpeedUpdate(value))
|
|
73
|
-
this.cacheSpeed = this.service.getCharacteristic(this.hapChar.RotationSpeed).value
|
|
74
|
-
|
|
75
|
-
// Add the set handler to the fan swing mode
|
|
76
|
-
this.service
|
|
77
|
-
.getCharacteristic(this.hapChar.SwingMode)
|
|
78
|
-
.onSet(async value => this.internalSwingUpdate(value))
|
|
79
|
-
this.cacheSwing = this.service.getCharacteristic(this.hapChar.SwingMode).value === 1 ? 'on' : 'off'
|
|
80
|
-
|
|
81
|
-
// Output the customised options to the log
|
|
82
|
-
const opts = JSON.stringify({})
|
|
83
|
-
platform.log('[%s] %s %s.', accessory.displayName, platformLang.devInitOpts, opts)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
async internalStateUpdate(value) {
|
|
87
|
-
try {
|
|
88
|
-
const newValue = value ? 'on' : 'off'
|
|
89
|
-
|
|
90
|
-
// Don't continue if the new value is the same as before
|
|
91
|
-
if (this.cacheState === newValue) {
|
|
92
|
-
return
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Send the request to the platform sender function
|
|
96
|
-
await this.platform.sendDeviceUpdate(this.accessory, {
|
|
97
|
-
cmd: 'ptReal',
|
|
98
|
-
value: value ? 'MwEBAAAAAAAAAAAAAAAAAAAAADM=' : 'MwEAAAAAAAAAAAAAAAAAAAAAADI=',
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
// Cache the new state and log if appropriate
|
|
102
|
-
if (this.cacheState !== newValue) {
|
|
103
|
-
this.cacheState = newValue
|
|
104
|
-
this.accessory.log(`${platformLang.curState} [${newValue}]`)
|
|
105
|
-
}
|
|
106
|
-
} catch (err) {
|
|
107
|
-
// Catch any errors during the process
|
|
108
|
-
this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`)
|
|
109
|
-
|
|
110
|
-
// Throw a 'no response' error and set a timeout to revert this after 2 seconds
|
|
111
|
-
setTimeout(() => {
|
|
112
|
-
this.service.updateCharacteristic(this.hapChar.Active, this.cacheState === 'on' ? 1 : 0)
|
|
113
|
-
}, 2000)
|
|
114
|
-
throw new this.hapErr(-70402)
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
async internalSpeedUpdate(value) {
|
|
119
|
-
try {
|
|
120
|
-
// Don't continue if the value is 0
|
|
121
|
-
if (value === 0) {
|
|
122
|
-
return
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// HomeKit sends a percentage - map it onto this model's real speed steps
|
|
126
|
-
value = hkPercentToFanSpeed(value, this.speedSteps)
|
|
127
|
-
|
|
128
|
-
// Don't continue if the new value is the same as before
|
|
129
|
-
if (this.cacheSpeed === value) {
|
|
130
|
-
return
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const codeToSend = this.speedCodes[value]
|
|
134
|
-
|
|
135
|
-
const isAuto = value === this.autoSpeed
|
|
136
|
-
await this.platform.sendDeviceUpdate(this.accessory, {
|
|
137
|
-
cmd: 'ptReal',
|
|
138
|
-
value: codeToSend,
|
|
139
|
-
openApi: this.accessory.context.openApiCapabilities?.workMode
|
|
140
|
-
? { instance: 'workMode', capabilityType: 'devices.capabilities.work_mode', value: isAuto ? { workMode: 3 } : { workMode: 1, modeValue: value } }
|
|
141
|
-
: undefined,
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
// Cache the new state and log if appropriate
|
|
145
|
-
if (this.cacheSpeed !== value) {
|
|
146
|
-
this.cacheSpeed = value
|
|
147
|
-
this.accessory.log(`${platformLang.curSpeed} [${value}]`)
|
|
148
|
-
}
|
|
149
|
-
} catch (err) {
|
|
150
|
-
// Catch any errors during the process
|
|
151
|
-
this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`)
|
|
152
|
-
|
|
153
|
-
// Throw a 'no response' error and set a timeout to revert this after 2 seconds
|
|
154
|
-
setTimeout(() => {
|
|
155
|
-
this.service.updateCharacteristic(this.hapChar.RotationSpeed, fanSpeedToHkPercent(this.cacheSpeed, this.speedSteps, this.autoSpeed))
|
|
156
|
-
}, 2000)
|
|
157
|
-
throw new this.hapErr(-70402)
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
async internalSwingUpdate(value) {
|
|
162
|
-
try {
|
|
163
|
-
const newValue = value ? 'on' : 'off'
|
|
164
|
-
// Don't continue if the new value is the same as before
|
|
165
|
-
if (this.cacheSwing === value) {
|
|
166
|
-
return
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
await this.platform.sendDeviceUpdate(this.accessory, {
|
|
170
|
-
cmd: 'ptReal',
|
|
171
|
-
value: value ? 'Mx8BAQAAAAAAAAAAAAAAAAAAACw=' : 'Mx8BAAAAAAAAAAAAAAAAAAAAAC0=',
|
|
172
|
-
openApi: this.accessory.context.openApiCapabilities?.oscillationToggle
|
|
173
|
-
? { instance: 'oscillationToggle', capabilityType: 'devices.capabilities.toggle', value: value ? 1 : 0 }
|
|
174
|
-
: undefined,
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
// Cache the new state and log if appropriate
|
|
178
|
-
if (this.cacheSwing !== newValue) {
|
|
179
|
-
this.cacheSwing = newValue
|
|
180
|
-
this.accessory.log(`${platformLang.curSwing} [${newValue}]`)
|
|
181
|
-
}
|
|
182
|
-
} catch (err) {
|
|
183
|
-
// Catch any errors during the process
|
|
184
|
-
this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`)
|
|
185
|
-
|
|
186
|
-
// Throw a 'no response' error and set a timeout to revert this after 2 seconds
|
|
187
|
-
setTimeout(() => {
|
|
188
|
-
this.service.updateCharacteristic(this.hapChar.SwingMode, this.cacheSwing === 'on' ? 1 : 0)
|
|
189
|
-
}, 2000)
|
|
190
|
-
throw new this.hapErr(-70402)
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
externalUpdate(params) {
|
|
195
|
-
// Update the active characteristic
|
|
196
|
-
if (params.state && params.state !== this.cacheState) {
|
|
197
|
-
this.cacheState = params.state
|
|
198
|
-
this.service.updateCharacteristic(this.hapChar.Active, this.cacheState === 'on' ? 1 : 0)
|
|
199
|
-
this.accessory.log(`${platformLang.curState} [${this.cacheState}]`)
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Handle OpenAPI workMode (speed or auto)
|
|
203
|
-
if (params.workMode) {
|
|
204
|
-
let newSpeed
|
|
205
|
-
if (params.workMode.workMode === 3) {
|
|
206
|
-
newSpeed = this.autoSpeed
|
|
207
|
-
} else {
|
|
208
|
-
newSpeed = params.workMode.modeValue
|
|
209
|
-
}
|
|
210
|
-
if (Number.isFinite(newSpeed) && newSpeed >= 1 && newSpeed <= this.autoSpeed && this.cacheSpeed !== newSpeed) {
|
|
211
|
-
this.cacheSpeed = newSpeed
|
|
212
|
-
const hkSpeed = fanSpeedToHkPercent(newSpeed, this.speedSteps, this.autoSpeed)
|
|
213
|
-
this.service.updateCharacteristic(this.hapChar.RotationSpeed, hkSpeed)
|
|
214
|
-
this.accessory.log(`${platformLang.curSpeed} [${newSpeed} -> ${hkSpeed}%]`)
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Handle OpenAPI toggles
|
|
219
|
-
if (params.toggles?.oscillationToggle !== undefined) {
|
|
220
|
-
const newSwing = params.toggles.oscillationToggle ? 'on' : 'off'
|
|
221
|
-
if (this.cacheSwing !== newSwing) {
|
|
222
|
-
this.cacheSwing = newSwing
|
|
223
|
-
this.service.updateCharacteristic(this.hapChar.SwingMode, this.cacheSwing === 'on' ? 1 : 0)
|
|
224
|
-
this.accessory.log(`${platformLang.curSwing} [${this.cacheSwing}]`)
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Check for some other scene/mode change
|
|
229
|
-
(params.commands || []).forEach((command) => {
|
|
230
|
-
const hexString = base64ToHex(command)
|
|
231
|
-
const hexParts = hexToTwoItems(hexString)
|
|
232
|
-
|
|
233
|
-
// Return now if not a device query update code
|
|
234
|
-
if (getTwoItemPosition(hexParts, 1) !== 'aa') {
|
|
235
|
-
return
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
if (getTwoItemPosition(hexParts, 2) === '08') {
|
|
239
|
-
// Sensor Attached?
|
|
240
|
-
const dev = hexString.substring(4, hexString.length - 24)
|
|
241
|
-
this.accessory.context.sensorAttached = dev !== '000000000000'
|
|
242
|
-
return
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const deviceFunction = `${getTwoItemPosition(hexParts, 2)}${getTwoItemPosition(hexParts, 3)}`
|
|
246
|
-
|
|
247
|
-
switch (deviceFunction) {
|
|
248
|
-
case '0501': {
|
|
249
|
-
// Fan speed
|
|
250
|
-
const newSpeed = Number.parseInt(getTwoItemPosition(hexParts, 4), 16)
|
|
251
|
-
if (newSpeed >= 1 && newSpeed <= this.speedSteps && this.cacheSpeed !== newSpeed) {
|
|
252
|
-
this.cacheSpeed = newSpeed
|
|
253
|
-
const hkSpeed = fanSpeedToHkPercent(newSpeed, this.speedSteps, this.autoSpeed)
|
|
254
|
-
this.service.updateCharacteristic(this.hapChar.RotationSpeed, hkSpeed)
|
|
255
|
-
this.accessory.log(`${platformLang.curSpeed} [${newSpeed} -> ${hkSpeed}%]`)
|
|
256
|
-
}
|
|
257
|
-
break
|
|
258
|
-
}
|
|
259
|
-
case '0500': {
|
|
260
|
-
// Mode change (auto = 0x03)
|
|
261
|
-
if (getTwoItemPosition(hexParts, 4) === '03' && this.cacheSpeed !== this.autoSpeed) {
|
|
262
|
-
this.cacheSpeed = this.autoSpeed
|
|
263
|
-
// Cache auto so we can tell it apart from "user asked for max speed"
|
|
264
|
-
this.service.updateCharacteristic(this.hapChar.RotationSpeed, 100)
|
|
265
|
-
this.accessory.log(`${platformLang.curSpeed} [auto]`)
|
|
266
|
-
}
|
|
267
|
-
break
|
|
268
|
-
}
|
|
269
|
-
case '1f01': {
|
|
270
|
-
// Swing Mode
|
|
271
|
-
const newSwing = getTwoItemPosition(hexParts, 4) === '01' ? 'on' : 'off'
|
|
272
|
-
if (this.cacheSwing !== newSwing) {
|
|
273
|
-
this.cacheSwing = newSwing
|
|
274
|
-
this.service.updateCharacteristic(this.hapChar.SwingMode, this.cacheSwing === 'on' ? 1 : 0)
|
|
275
|
-
this.accessory.log(`${platformLang.curSwing} [${this.cacheSwing}]`)
|
|
276
|
-
}
|
|
277
|
-
break
|
|
278
|
-
}
|
|
279
|
-
default:
|
|
280
|
-
logUnknownData(this.accessory, {
|
|
281
|
-
kind: 'scene',
|
|
282
|
-
source: params.source,
|
|
283
|
-
raw: command,
|
|
284
|
-
hex: hexString,
|
|
285
|
-
})
|
|
286
|
-
break
|
|
287
|
-
}
|
|
288
|
-
})
|
|
289
|
-
}
|
|
290
|
-
}
|