Skip to content

Commit 579e912

Browse files
committed
updates for plugin v11
1 parent 28c9082 commit 579e912

File tree

12 files changed

+1068
-820
lines changed

12 files changed

+1068
-820
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ jobs:
99
strategy:
1010
matrix:
1111
# the Node.js versions to build on
12-
node-version: [18.x, 20.x]
12+
node-version: [20.x, 22.x]
1313

1414
steps:
1515
- uses: actions/checkout@v4

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,23 @@ This project tries to adhere to [Semantic Versioning](http://semver.org/). In pr
88
- `MINOR` version when a new device type is added, or when a new feature is added that is backwards-compatible
99
- `PATCH` version when backwards-compatible bug fixes are implemented
1010

11+
## v11.0.0 (2025-05-18)
12+
13+
⚠️ This plugin no longer officially supports Node 18. Please update to Node 20 or 22.
14+
15+
### Added
16+
17+
- support light models `H600B` and `H7093`
18+
- support fan model `H7107`
19+
20+
### Changed
21+
22+
- updated dependencies
23+
24+
### Removed
25+
26+
- remove official support for node 18
27+
1128
## v10.19.0 (2025-04-19)
1229

1330
### Added

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ Homebridge plugin to integrate Govee devices into HomeKit
2828
### Prerequisites
2929

3030
- To use this plugin, you will need to already have:
31-
- [Node](https://nodejs.org): latest version of `v18`, `v20` or `v22` - any other major version is not supported.
32-
- [Homebridge](https://homebridge.io): `v1.6` - refer to link for more information and installation instructions.
31+
- [Node](https://nodejs.org): latest version of `v20` or `v22` - any other major version is not supported.
32+
- [Homebridge](https://homebridge.io): `v1.6` or above - refer to link for more information and installation instructions.
3333
- For bluetooth connectivity, it may be necessary to install extra packages on your system, see [Bluetooth Control](https://github.com/homebridge-plugins/homebridge-govee/wiki/Bluetooth-Control). Bluetooth works best when using a Raspberry Pi, not been tested on Windows, and Mac devices are unsupported.
3434

3535
### Setup

lib/device/fan-H7107.js

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
import {
2+
base64ToHex,
3+
generateCodeFromHexValues,
4+
getTwoItemPosition,
5+
hexToTwoItems,
6+
parseError,
7+
} from '../utils/functions.js'
8+
import platformLang from '../utils/lang-en.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+
// Codes etc
22+
this.speedCodes = {
23+
11: 'MwUBAQAAAAAAAAAAAAAAAAAAADY=',
24+
22: 'MwUBAgAAAAAAAAAAAAAAAAAAADU=',
25+
33: 'MwUBAwAAAAAAAAAAAAAAAAAAADQ=',
26+
44: 'MwUBBAAAAAAAAAAAAAAAAAAAADM=',
27+
55: 'MwUBBQAAAAAAAAAAAAAAAAAAADI=',
28+
66: 'MwUBBgAAAAAAAAAAAAAAAAAAADE=',
29+
77: 'MwUBBwAAAAAAAAAAAAAAAAAAADA=',
30+
88: 'MwUBCAAAAAAAAAAAAAAAAAAAAD8=',
31+
}
32+
33+
// Remove any old original Fan services
34+
if (this.accessory.getService(this.hapServ.Fan)) {
35+
this.accessory.removeService(this.accessory.getService(this.hapServ.Fan))
36+
}
37+
38+
// Add the fan service for the fan if it doesn't already exist
39+
this.service = this.accessory.getService(this.hapServ.Fanv2) || this.accessory.addService(this.hapServ.Fanv2)
40+
41+
// Add the set handler to the fan on/off characteristic
42+
this.service
43+
.getCharacteristic(this.hapChar.Active)
44+
.onSet(async value => this.internalStateUpdate(value))
45+
this.cacheState = this.service.getCharacteristic(this.hapChar.Active).value ? 'on' : 'off'
46+
47+
// Add the set handler to the fan rotation speed characteristic
48+
this.service
49+
.getCharacteristic(this.hapChar.RotationSpeed)
50+
.setProps({
51+
minStep: 11,
52+
minValue: 0,
53+
validValues: [0, 11, 22, 33, 44, 55, 66, 77, 88, 99],
54+
})
55+
.onSet(async value => this.internalSpeedUpdate(value))
56+
this.cacheSpeed = this.service.getCharacteristic(this.hapChar.RotationSpeed).value
57+
this.cacheMode = this.cacheSpeed === 99 ? 'auto' : 'manual'
58+
59+
// Add the set handler to the fan swing mode
60+
this.service
61+
.getCharacteristic(this.hapChar.SwingMode)
62+
.onSet(async value => this.internalSwingUpdate(value))
63+
this.cacheSwing = this.service.getCharacteristic(this.hapChar.SwingMode).value === 1 ? 'on' : 'off'
64+
65+
// Output the customised options to the log
66+
const opts = JSON.stringify({})
67+
platform.log('[%s] %s %s.', accessory.displayName, platformLang.devInitOpts, opts)
68+
}
69+
70+
async internalStateUpdate(value) {
71+
try {
72+
const newValue = value ? 'on' : 'off'
73+
74+
// Don't continue if the new value is the same as before
75+
if (this.cacheState === newValue) {
76+
return
77+
}
78+
79+
// Send the request to the platform sender function
80+
await this.platform.sendDeviceUpdate(this.accessory, {
81+
cmd: 'ptReal',
82+
value: value ? 'MwEBAAAAAAAAAAAAAAAAAAAAADM=' : 'MwEAAAAAAAAAAAAAAAAAAAAAADI=',
83+
})
84+
85+
// Cache the new state and log if appropriate
86+
if (this.cacheState !== newValue) {
87+
this.cacheState = newValue
88+
this.accessory.log(`${platformLang.curState} [${newValue}]`)
89+
}
90+
} catch (err) {
91+
// Catch any errors during the process
92+
this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`)
93+
94+
// Throw a 'no response' error and set a timeout to revert this after 2 seconds
95+
setTimeout(() => {
96+
this.service.updateCharacteristic(this.hapChar.Active, this.cacheState === 'on' ? 1 : 0)
97+
}, 2000)
98+
throw new this.hapErr(-70402)
99+
}
100+
}
101+
102+
async internalSpeedUpdate(value) {
103+
try {
104+
// Don't continue if the value is lower than 11
105+
if (value < 11) {
106+
return
107+
}
108+
let newMode = value === 99 ? 'auto' : 'manual'
109+
110+
// Don't continue if the new value is the same as before
111+
if (this.cacheSpeed === value) {
112+
return
113+
}
114+
115+
// Don't continue if trying to access auto mode but there is no sensor attached
116+
let codeToSend
117+
if (newMode === 'auto') {
118+
if (!this.accessory.context.sensorAttached || !this.cacheAutoCode) {
119+
this.accessory.logWarn('auto mode not supported without a linked sensor')
120+
codeToSend = this.speedCodes[88]
121+
newMode = 'manual'
122+
value = 88
123+
} else {
124+
codeToSend = this.cacheAutoCode
125+
}
126+
} else {
127+
codeToSend = this.speedCodes[value]
128+
}
129+
130+
await this.platform.sendDeviceUpdate(this.accessory, {
131+
cmd: 'ptReal',
132+
value: codeToSend,
133+
})
134+
135+
// Cache the new state and log if appropriate
136+
if (this.cacheMode !== newMode) {
137+
this.cacheMode = newMode
138+
this.accessory.log(`${platformLang.curMode} [${this.cacheMode}]`)
139+
}
140+
if (this.cacheSpeed !== value) {
141+
this.cacheSpeed = value
142+
this.accessory.log(`${platformLang.curSpeed} [${value}%]`)
143+
}
144+
} catch (err) {
145+
// Catch any errors during the process
146+
this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`)
147+
148+
// Throw a 'no response' error and set a timeout to revert this after 2 seconds
149+
setTimeout(() => {
150+
this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSpeed)
151+
}, 2000)
152+
throw new this.hapErr(-70402)
153+
}
154+
}
155+
156+
async internalSwingUpdate(value) {
157+
try {
158+
const newValue = value ? 'on' : 'off'
159+
// Don't continue if the new value is the same as before
160+
if (this.cacheSwing === value) {
161+
return
162+
}
163+
164+
await this.platform.sendDeviceUpdate(this.accessory, {
165+
cmd: 'ptReal',
166+
value: value ? 'Mx8BAQAAAAAAAAAAAAAAAAAAACw=' : 'Mx8BAAAAAAAAAAAAAAAAAAAAAC0=',
167+
})
168+
169+
// Cache the new state and log if appropriate
170+
if (this.cacheSwing !== newValue) {
171+
this.cacheSwing = newValue
172+
this.accessory.log(`${platformLang.curSwing} [${newValue}]`)
173+
}
174+
} catch (err) {
175+
// Catch any errors during the process
176+
this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`)
177+
178+
// Throw a 'no response' error and set a timeout to revert this after 2 seconds
179+
setTimeout(() => {
180+
this.service.updateCharacteristic(this.hapChar.SwingMode, this.cacheSwing === 'on' ? 1 : 0)
181+
}, 2000)
182+
throw new this.hapErr(-70402)
183+
}
184+
}
185+
186+
externalUpdate(params) {
187+
// Update the active characteristic
188+
if (params.state && params.state !== this.cacheState) {
189+
this.cacheState = params.state
190+
this.service.updateCharacteristic(this.hapChar.Active, this.cacheState === 'on' ? 1 : 0)
191+
this.accessory.log(`${platformLang.curState} [${this.cacheState}]`)
192+
}
193+
194+
// Check for some other scene/mode change
195+
(params.commands || []).forEach((command) => {
196+
const hexString = base64ToHex(command)
197+
const hexParts = hexToTwoItems(hexString)
198+
199+
// Return now if not a device query update code
200+
if (getTwoItemPosition(hexParts, 1) !== 'aa') {
201+
return
202+
}
203+
204+
if (getTwoItemPosition(hexParts, 2) === '08') {
205+
// Sensor Attached?
206+
const dev = hexString.substring(4, hexString.length - 24)
207+
this.accessory.context.sensorAttached = dev !== '000000000000'
208+
return
209+
}
210+
211+
const deviceFunction = `${getTwoItemPosition(hexParts, 2)}${getTwoItemPosition(hexParts, 3)}`
212+
213+
switch (deviceFunction) {
214+
case '0501': {
215+
// Fan speed
216+
const newSpeed = getTwoItemPosition(hexParts, 4)
217+
const newSpeedInt = Number.parseInt(newSpeed, 10) * 11
218+
const newMode = 'manual'
219+
if (this.cacheMode !== newMode) {
220+
this.cacheMode = newMode
221+
this.accessory.log(`${platformLang.curMode} [${this.cacheMode}]`)
222+
}
223+
if (this.cacheSpeed !== newSpeedInt) {
224+
this.cacheSpeed = newSpeedInt
225+
this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSpeed)
226+
this.accessory.log(`${platformLang.curSpeed} [${this.cacheSpeed}%]`)
227+
}
228+
break
229+
}
230+
case '0500': {
231+
// Auto mode on/off
232+
// Maybe this relates to
233+
// (Guess) Fixed Speed: 1
234+
// Custom: 2
235+
// Auto: 3
236+
// Sleep: 5
237+
// Nature: 6
238+
// Turbo: 7
239+
const newMode = getTwoItemPosition(hexParts, 4) === '03' ? 'auto' : 'manual'
240+
if (this.cacheMode !== newMode) {
241+
this.cacheMode = newMode
242+
this.accessory.log(`${platformLang.curMode} [${this.cacheMode}]`)
243+
244+
if (this.cacheMode === 'auto' && this.cacheSpeed !== 99) {
245+
this.cacheSpeed = 99
246+
this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSpeed)
247+
this.accessory.log(`${platformLang.curSpeed} [${this.cacheSpeed}%]`)
248+
}
249+
}
250+
break
251+
}
252+
case '0503': {
253+
// Auto mode, we need to keep this code to send it back to the device
254+
const code = hexToTwoItems(`33${hexString.substring(2, hexString.length - 2)}`)
255+
this.cacheAutoCode = generateCodeFromHexValues(code.map(p => Number.parseInt(p, 16)))
256+
break
257+
}
258+
case '1f01': {
259+
// Swing Mode
260+
const newSwing = getTwoItemPosition(hexParts, 4) === '01' ? 'on' : 'off'
261+
if (this.cacheSwing !== newSwing) {
262+
this.cacheSwing = newSwing
263+
this.service.updateCharacteristic(this.hapChar.SwingMode, this.cacheSwing === 'on' ? 1 : 0)
264+
this.accessory.log(`${platformLang.curSwing} [${this.cacheSwing}]`)
265+
}
266+
break
267+
}
268+
default:
269+
this.accessory.logDebugWarn(`${platformLang.newScene}: [${command}] [${hexString}]`)
270+
break
271+
}
272+
})
273+
}
274+
}

lib/device/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import deviceFanH7101 from './fan-H7101.js'
88
import deviceFanH7102 from './fan-H7102.js'
99
import deviceFanH7105 from './fan-H7105.js'
1010
import deviceFanH7106 from './fan-H7106.js'
11+
import deviceFanH7107 from './fan-H7107.js'
1112
import deviceFanH7111 from './fan-H7111.js'
1213
import deviceHeater1A from './heater1a.js'
1314
import deviceHeater1B from './heater1b.js'
@@ -67,6 +68,7 @@ export default {
6768
deviceFanH7102,
6869
deviceFanH7105,
6970
deviceFanH7106,
71+
deviceFanH7107,
7072
deviceFanH7111,
7173
deviceHeaterSingle,
7274
deviceHeater1A,

0 commit comments

Comments
 (0)