@homebridge-plugins/homebridge-govee 11.32.2-beta.13 → 11.32.2-beta.14

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 CHANGED
@@ -28,6 +28,8 @@ All notable changes to `@homebridge-plugins/homebridge-govee` will be documented
28
28
  - refactor: point three more models at a shared handler instead of a near-copy of the same file, keeping each one's behaviour exactly as it was
29
29
  - fix: give the H7124 the air quality handling the H7123 already had, so a poor reading is named properly and stops being logged over and over (#1261)
30
30
  - refactor: point the H7151 and H7111 at the handlers they were byte-for-byte copies of
31
+ - refactor: build the ice maker size codes from the model rather than writing both orderings out by hand
32
+ - refactor: build the switch and outlet tiles of a multi-socket device from one handler instead of two copies of it
31
33
 
32
34
  ## v11.32.1 (2026-08-02)
33
35
 
@@ -1,8 +1,9 @@
1
- import { base64ToHex, getTwoItemPosition, hexToTwoItems, parseError } from '../utils/functions.js'
1
+ import { getDeviceCapabilities } from '../utils/device-capabilities.js'
2
+ import { base64ToHex, generateCodeFromHexValues, getTwoItemPosition, hexToTwoItems, parseError } from '../utils/functions.js'
2
3
  import platformLang from '../utils/lang-en.js'
3
4
  import { logUnknownData } from '../utils/report-unknown.js'
4
5
 
5
- export default class {
6
+ export default class GoveeIceMaker {
6
7
  constructor(platform, accessory) {
7
8
  // Set up variables from the platform
8
9
  this.hapChar = platform.api.hap.Characteristic
@@ -14,10 +15,15 @@ export default class {
14
15
  this.accessory = accessory
15
16
 
16
17
  // Ice size codes: small=1, medium=2, large=3 (matching OpenAPI workMode values)
18
+ // Small, medium and large are all `33 05 <size>`. Which byte means which
19
+ // size runs the opposite way on the older models, so the order comes from
20
+ // the model rather than being written out twice
21
+ const ascending = getDeviceCapabilities(accessory.context.gvModel).iceSizeAscending
22
+ const sizeBytes = ascending ? [0x01, 0x02, 0x03] : [0x03, 0x02, 0x01]
17
23
  this.sizeCodes = {
18
- 1: 'MwUDAAAAAAAAAAAAAAAAAAAAADU=', // small
19
- 2: 'MwUCAAAAAAAAAAAAAAAAAAAAADQ=', // medium
20
- 3: 'MwUBAAAAAAAAAAAAAAAAAAAAADc=', // large
24
+ 1: generateCodeFromHexValues([0x33, 0x05, sizeBytes[0]]), // small
25
+ 2: generateCodeFromHexValues([0x33, 0x05, sizeBytes[1]]), // medium
26
+ 3: generateCodeFromHexValues([0x33, 0x05, sizeBytes[2]]), // large
21
27
  }
22
28
 
23
29
  this.sizeLabels = {
@@ -204,7 +210,7 @@ export default class {
204
210
  }
205
211
  case 'aa05': {
206
212
  // Ice size report. The payload encoding is deliberately NOT decoded here:
207
- // sizeCodes runs small=0x03..large=0x01 while the OpenAPI path sends
213
+ // the size byte order differs by model while the OpenAPI path sends
208
214
  // workMode un-inverted, and which is authoritative is unresolved (#1329).
209
215
  // Claiming a mapping here could silently swap sizes, so this case exists
210
216
  // only to stop a routine status report being logged as a new scene code.
@@ -24,13 +24,10 @@ import deviceHumidifierH7142 from './humidifier-H7142.js'
24
24
  import deviceHumidifierH7143 from './humidifier-H7143.js'
25
25
  import deviceHumidifierH7160 from './humidifier-H7160.js'
26
26
  import deviceIceMakerH7172 from './ice-maker-H7172.js'
27
- import deviceIceMakerH8121 from './ice-maker-H8121.js'
28
27
  import deviceKettle from './kettle.js'
29
28
  import deviceLightSwitch from './light-switch.js'
30
29
  import deviceLight from './light.js'
31
- import deviceOutletDouble from './outlet-double.js'
32
30
  import deviceOutletSingle from './outlet-single.js'
33
- import deviceOutletTriple from './outlet-triple.js'
34
31
  import devicePurifierH7120 from './purifier-H7120.js'
35
32
  import devicePurifierH7122 from './purifier-H7122.js'
36
33
  import devicePurifierH7123 from './purifier-H7123.js'
@@ -44,9 +41,9 @@ import deviceSensorPresence from './sensor-presence.js'
44
41
  import deviceSensorThermo4 from './sensor-thermo4.js'
45
42
  import deviceSensorThermoSwitch from './sensor-thermo-switch.js'
46
43
  import deviceSensorThermo from './sensor-thermo.js'
47
- import deviceSwitchDouble from './switch-double.js'
44
+ import deviceSwitchDouble, { asOutlet as deviceOutletDouble } from './switch-double.js'
48
45
  import deviceSwitchSingle from './switch-single.js'
49
- import deviceSwitchTriple from './switch-triple.js'
46
+ import deviceSwitchTriple, { asOutlet as deviceOutletTriple } from './switch-triple.js'
50
47
  import deviceTapSingle from './tap-single.js'
51
48
  import deviceTemplate from './template.js'
52
49
  import deviceTVSingle from './tv-single.js'
@@ -90,8 +87,8 @@ export default {
90
87
  deviceIceMakerH7172,
91
88
  deviceIceMakerH717D: deviceIceMakerH7172, // identical to the H7172
92
89
  deviceIceMakerH8120: deviceIceMakerH7172, // identical to the H7172
93
- deviceIceMakerH8121,
94
- deviceIceMakerH8122: deviceIceMakerH8121, // identical to the H8121
90
+ deviceIceMakerH8121: deviceIceMakerH7172, // same handler; its size byte order comes from the model
91
+ deviceIceMakerH8122: deviceIceMakerH7172, // identical to the H8121
95
92
  deviceKettle,
96
93
  deviceLight,
97
94
  deviceLightSwitch,
@@ -8,8 +8,8 @@ import platformLang from '../utils/lang-en.js'
8
8
  // outright rather than accepted and silently dropped (#1323).
9
9
  const nightLightModels = ['H5089']
10
10
 
11
- export default class {
12
- constructor(platform, accessory) {
11
+ class MultiSwitchDouble {
12
+ constructor(platform, accessory, config) {
13
13
  // Set up variables from the platform
14
14
  this.hapChar = platform.api.hap.Characteristic
15
15
  this.hapErr = platform.api.hap.HapStatusError
@@ -19,12 +19,13 @@ export default class {
19
19
  // Set up variables from the accessory
20
20
  this.accessory = accessory
21
21
 
22
- // Remove outlet services if they exist
23
- if (this.accessory.getService('Outlet 1')) {
24
- this.accessory.removeService(this.accessory.getService('Outlet 1'))
22
+ // Drop the tiles of the other kind, left behind if this device has been
23
+ // shown as the other one before
24
+ if (this.accessory.getService(`${config.other} 1`)) {
25
+ this.accessory.removeService(this.accessory.getService(`${config.other} 1`))
25
26
  }
26
- if (this.accessory.getService('Outlet 2')) {
27
- this.accessory.removeService(this.accessory.getService('Outlet 2'))
27
+ if (this.accessory.getService(`${config.other} 2`)) {
28
+ this.accessory.removeService(this.accessory.getService(`${config.other} 2`))
28
29
  }
29
30
 
30
31
  // A model that used to be treated as a single switch or outlet leaves an
@@ -35,15 +36,15 @@ export default class {
35
36
  && [this.hapServ.Switch.UUID, this.hapServ.Outlet.UUID].includes(service.UUID))
36
37
  .forEach(service => this.accessory.removeService(service))
37
38
 
38
- // Add the switch services if they don't already exist
39
- this.service1 = this.accessory.getService('Switch 1')
40
- || this.accessory.addService(this.hapServ.Switch, 'Switch 1', 'switch1')
41
- this.service2 = this.accessory.getService('Switch 2')
42
- || this.accessory.addService(this.hapServ.Switch, 'Switch 2', 'switch2')
39
+ // Add this device's tiles if they are not there already
40
+ this.service1 = this.accessory.getService(`${config.label} 1`)
41
+ || this.accessory.addService(this.hapServ[config.label], `${config.label} 1`, `${config.subtype}1`)
42
+ this.service2 = this.accessory.getService(`${config.label} 2`)
43
+ || this.accessory.addService(this.hapServ[config.label], `${config.label} 2`, `${config.subtype}2`)
43
44
 
44
45
  if (!this.service1.testCharacteristic(this.hapChar.ConfiguredName)) {
45
46
  this.service1.addCharacteristic(this.hapChar.ConfiguredName)
46
- this.service1.updateCharacteristic(this.hapChar.ConfiguredName, 'Switch 1')
47
+ this.service1.updateCharacteristic(this.hapChar.ConfiguredName, `${config.label} 1`)
47
48
  }
48
49
  if (!this.service1.testCharacteristic(this.hapChar.ServiceLabelIndex)) {
49
50
  this.service1.addCharacteristic(this.hapChar.ServiceLabelIndex)
@@ -51,7 +52,7 @@ export default class {
51
52
  }
52
53
  if (!this.service2.testCharacteristic(this.hapChar.ConfiguredName)) {
53
54
  this.service2.addCharacteristic(this.hapChar.ConfiguredName)
54
- this.service2.updateCharacteristic(this.hapChar.ConfiguredName, 'Switch 2')
55
+ this.service2.updateCharacteristic(this.hapChar.ConfiguredName, `${config.label} 2`)
55
56
  }
56
57
  if (!this.service2.testCharacteristic(this.hapChar.ServiceLabelIndex)) {
57
58
  this.service2.addCharacteristic(this.hapChar.ServiceLabelIndex)
@@ -86,7 +87,7 @@ export default class {
86
87
 
87
88
  // Output the customised options to the log
88
89
  const opts = JSON.stringify({
89
- showAs: 'switch',
90
+ showAs: config.showAs,
90
91
  })
91
92
  platform.log('[%s] %s %s.', accessory.displayName, platformLang.devInitOpts, opts)
92
93
 
@@ -186,3 +187,21 @@ export default class {
186
187
  }
187
188
  }
188
189
  }
190
+
191
+ /**
192
+ * A double switch and a double outlet are the same device with a different tile
193
+ * in the Home app, chosen by the owner's `showAs` setting. Only the service
194
+ * type, the tile names and the logged option differ, so they share this and are
195
+ * built from it rather than from a copy of it.
196
+ */
197
+ function make(config) {
198
+ return class extends MultiSwitchDouble {
199
+ constructor(platform, accessory) {
200
+ super(platform, accessory, config)
201
+ }
202
+ }
203
+ }
204
+
205
+ export default make({ label: 'Switch', subtype: 'switch', other: 'Outlet', showAs: 'switch' })
206
+
207
+ export const asOutlet = make({ label: 'Outlet', subtype: 'outlet', other: 'Switch', showAs: 'outlet' })
@@ -1,8 +1,8 @@
1
1
  import { parseError } from '../utils/functions.js'
2
2
  import platformLang from '../utils/lang-en.js'
3
3
 
4
- export default class {
5
- constructor(platform, accessory) {
4
+ class MultiSwitchTriple {
5
+ constructor(platform, accessory, config) {
6
6
  // Set up variables from the platform
7
7
  this.hapChar = platform.api.hap.Characteristic
8
8
  this.hapErr = platform.api.hap.HapStatusError
@@ -12,24 +12,25 @@ export default class {
12
12
  // Set up variables from the accessory
13
13
  this.accessory = accessory
14
14
 
15
- // Remove outlet services if they exist
16
- if (this.accessory.getService('Outlet 1')) {
17
- this.accessory.removeService(this.accessory.getService('Outlet 1'))
15
+ // Drop the tiles of the other kind, left behind if this device has been
16
+ // shown as the other one before
17
+ if (this.accessory.getService(`${config.other} 1`)) {
18
+ this.accessory.removeService(this.accessory.getService(`${config.other} 1`))
18
19
  }
19
- if (this.accessory.getService('Outlet 2')) {
20
- this.accessory.removeService(this.accessory.getService('Outlet 2'))
20
+ if (this.accessory.getService(`${config.other} 2`)) {
21
+ this.accessory.removeService(this.accessory.getService(`${config.other} 2`))
21
22
  }
22
- if (this.accessory.getService('Outlet 3')) {
23
- this.accessory.removeService(this.accessory.getService('Outlet 3'))
23
+ if (this.accessory.getService(`${config.other} 3`)) {
24
+ this.accessory.removeService(this.accessory.getService(`${config.other} 3`))
24
25
  }
25
26
 
26
- // Add the switch services if they don't already exist
27
- this.service1 = this.accessory.getService('Switch 1')
28
- || this.accessory.addService(this.hapServ.Switch, 'Switch 1', 'switch1')
29
- this.service2 = this.accessory.getService('Switch 2')
30
- || this.accessory.addService(this.hapServ.Switch, 'Switch 2', 'switch2')
31
- this.service3 = this.accessory.getService('Switch 3')
32
- || this.accessory.addService(this.hapServ.Switch, 'Switch 3', 'switch3')
27
+ // Add this device's tiles if they are not there already
28
+ this.service1 = this.accessory.getService(`${config.label} 1`)
29
+ || this.accessory.addService(this.hapServ[config.label], `${config.label} 1`, `${config.subtype}1`)
30
+ this.service2 = this.accessory.getService(`${config.label} 2`)
31
+ || this.accessory.addService(this.hapServ[config.label], `${config.label} 2`, `${config.subtype}2`)
32
+ this.service3 = this.accessory.getService(`${config.label} 3`)
33
+ || this.accessory.addService(this.hapServ[config.label], `${config.label} 3`, `${config.subtype}3`)
33
34
 
34
35
  // Add the set handler to the switch on/off characteristic
35
36
  this.service1.getCharacteristic(this.hapChar.On).onSet(async (value) => {
@@ -47,7 +48,7 @@ export default class {
47
48
 
48
49
  // Output the customised options to the log
49
50
  const opts = JSON.stringify({
50
- showAs: 'switch',
51
+ showAs: config.showAs,
51
52
  })
52
53
  platform.log('[%s] %s %s.', accessory.displayName, platformLang.devInitOpts, opts)
53
54
 
@@ -158,3 +159,21 @@ export default class {
158
159
  }
159
160
  }
160
161
  }
162
+
163
+ /**
164
+ * A triple switch and a triple outlet are the same device with a different tile
165
+ * in the Home app, chosen by the owner's `showAs` setting. Only the service
166
+ * type, the tile names and the logged option differ, so they share this and are
167
+ * built from it rather than from a copy of it.
168
+ */
169
+ function make(config) {
170
+ return class extends MultiSwitchTriple {
171
+ constructor(platform, accessory) {
172
+ super(platform, accessory, config)
173
+ }
174
+ }
175
+ }
176
+
177
+ export default make({ label: 'Switch', subtype: 'switch', other: 'Outlet', showAs: 'switch' })
178
+
179
+ export const asOutlet = make({ label: 'Outlet', subtype: 'outlet', other: 'Switch', showAs: 'outlet' })
@@ -41,6 +41,12 @@ const modelOverrides = {
41
41
  H1310: { readsSwingStatus: false },
42
42
  R1310: { readsSwingStatus: false },
43
43
 
44
+ // Later ice makers number their ice sizes the natural way round. The older
45
+ // ones run the other way, small being the highest byte, so the two families
46
+ // share a handler and differ only here
47
+ H8121: { iceSizeAscending: true },
48
+ H8122: { iceSizeAscending: true },
49
+
44
50
  // AWS outlet uses 17/16 for on/off instead of 1/0
45
51
  H5080: { awsPowerOn: 17, awsPowerOff: 16 },
46
52
  H5083: { awsPowerOn: 17, awsPowerOff: 16 },
@@ -109,6 +115,7 @@ const defaults = {
109
115
  awsStatusCmdVersion: 2,
110
116
  awsBrightnessNoScale: false,
111
117
  readsSwingStatus: true,
118
+ iceSizeAscending: false,
112
119
  }
113
120
 
114
121
  export function getDeviceCapabilities(model, firmware) {
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@homebridge-plugins/homebridge-govee",
3
3
  "alias": "Govee",
4
4
  "type": "module",
5
- "version": "11.32.2-beta.13",
5
+ "version": "11.32.2-beta.14",
6
6
  "description": "Homebridge plugin to integrate Govee devices into HomeKit.",
7
7
  "author": {
8
8
  "name": "bwp91",
@@ -1,228 +0,0 @@
1
- import { base64ToHex, getTwoItemPosition, hexToTwoItems, parseError } from '../utils/functions.js'
2
- import platformLang from '../utils/lang-en.js'
3
- import { logUnknownData } from '../utils/report-unknown.js'
4
-
5
- export default class GoveeIceMakerH8121 {
6
- constructor(platform, accessory) {
7
- // Set up variables from the platform
8
- this.hapChar = platform.api.hap.Characteristic
9
- this.hapErr = platform.api.hap.HapStatusError
10
- this.hapServ = platform.api.hap.Service
11
- this.platform = platform
12
-
13
- // Set up variables from the accessory
14
- this.accessory = accessory
15
-
16
- // Ice size codes: small=1, medium=2, large=3, matching the OpenAPI workMode
17
- // values. Unlike the H8120, this generation's BLE size byte runs in the SAME
18
- // direction as workMode (small=0x05 01 .. large=0x05 03), so the two paths
19
- // agree here and no inversion is needed.
20
- this.sizeCodes = {
21
- 1: 'MwUBAAAAAAAAAAAAAAAAAAAAADc=', // small -> 33 05 01
22
- 2: 'MwUCAAAAAAAAAAAAAAAAAAAAADQ=', // medium -> 33 05 02
23
- 3: 'MwUDAAAAAAAAAAAAAAAAAAAAADU=', // large -> 33 05 03
24
- }
25
-
26
- this.sizeLabels = {
27
- 1: 'small',
28
- 2: 'medium',
29
- 3: 'large',
30
- }
31
-
32
- // Remove old switch service if migrating
33
- if (this.accessory.getService(this.hapServ.Switch)) {
34
- this.accessory.removeService(this.accessory.getService(this.hapServ.Switch))
35
- }
36
-
37
- // Add the fan service for ice size control
38
- this.service = this.accessory.getService(this.hapServ.Fanv2)
39
- || this.accessory.addService(this.hapServ.Fanv2)
40
-
41
- // Add the set handler to the on/off characteristic
42
- this.service.getCharacteristic(this.hapChar.Active).onSet(async (value) => {
43
- await this.internalStateUpdate(value)
44
- })
45
- this.cacheState = this.service.getCharacteristic(this.hapChar.Active).value === 1 ? 'on' : 'off'
46
-
47
- // Add the set handler to the rotation speed characteristic (ice size)
48
- this.service
49
- .getCharacteristic(this.hapChar.RotationSpeed)
50
- .setProps({
51
- maxValue: 3,
52
- minStep: 1,
53
- minValue: 0,
54
- unit: 'unitless',
55
- })
56
- .onSet(async value => this.internalSizeUpdate(value))
57
- this.cacheSize = this.service.getCharacteristic(this.hapChar.RotationSpeed).value || 2
58
-
59
- // Keep the characteristic in step with the cache. Without this an accessory
60
- // that has never had a size set reports Active=1 with RotationSpeed=0, which
61
- // is contradictory and which the Home app renders as "0".
62
- if (this.service.getCharacteristic(this.hapChar.RotationSpeed).value !== this.cacheSize) {
63
- this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSize)
64
- }
65
-
66
- // Output the customised options to the log
67
- const opts = JSON.stringify({})
68
- platform.log('[%s] %s %s.', accessory.displayName, platformLang.devInitOpts, opts)
69
- }
70
-
71
- async internalStateUpdate(value) {
72
- try {
73
- const newState = value ? 'on' : 'off'
74
-
75
- // Don't continue if the new value is the same as before
76
- if (newState === this.cacheState) {
77
- return
78
- }
79
-
80
- if (value) {
81
- // Turn on with current cached ice size
82
- await this.platform.sendDeviceUpdate(this.accessory, {
83
- cmd: 'ptReal',
84
- value: this.sizeCodes[this.cacheSize],
85
- openApi: this.accessory.context.openApiCapabilities?.workMode
86
- ? { instance: 'workMode', capabilityType: 'devices.capabilities.work_mode', value: { workMode: this.cacheSize, modeValue: 0 } }
87
- : undefined,
88
- })
89
- } else {
90
- // Turn off. The OpenAPI workMode=0 path is authoritative; the BLE frame
91
- // mirrors the H8120 ice-maker-off command (33 19 00) as a best effort
92
- // and is pending confirmation from an owner of either model.
93
- await this.platform.sendDeviceUpdate(this.accessory, {
94
- cmd: 'ptReal',
95
- value: 'MxkAAAAAAAAAAAAAAAAAAAAAACo=',
96
- openApi: this.accessory.context.openApiCapabilities?.workMode
97
- ? { instance: 'workMode', capabilityType: 'devices.capabilities.work_mode', value: { workMode: 0, modeValue: 0 } }
98
- : undefined,
99
- })
100
- }
101
-
102
- // Cache the new state and log if appropriate
103
- if (this.cacheState !== newState) {
104
- this.cacheState = newState
105
- this.accessory.log(`${platformLang.curState} [${this.cacheState}]`)
106
- }
107
- } catch (err) {
108
- // Catch any errors during the process
109
- this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`)
110
-
111
- // Throw a 'no response' error and set a timeout to revert this after 2 seconds
112
- setTimeout(() => {
113
- this.service.updateCharacteristic(this.hapChar.Active, this.cacheState === 'on' ? 1 : 0)
114
- }, 2000)
115
- throw new this.hapErr(-70402)
116
- }
117
- }
118
-
119
- async internalSizeUpdate(value) {
120
- try {
121
- // 0 is not a valid ice size. Put the slider back, otherwise HomeKit keeps 0
122
- // while the device carries on at cacheSize - and that 0 is what gets written
123
- // to the accessory cache, so it returns on the next restart. Deferred for the
124
- // same reason as the error reverts below: an immediate update would be
125
- // overwritten by the value HomeKit is currently writing.
126
- if (value === 0) {
127
- setTimeout(() => {
128
- this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSize)
129
- }, 1000)
130
- return
131
- }
132
-
133
- // Don't continue if the new value is the same as before
134
- if (value === this.cacheSize) {
135
- return
136
- }
137
-
138
- await this.platform.sendDeviceUpdate(this.accessory, {
139
- cmd: 'ptReal',
140
- value: this.sizeCodes[value],
141
- openApi: this.accessory.context.openApiCapabilities?.workMode
142
- ? { instance: 'workMode', capabilityType: 'devices.capabilities.work_mode', value: { workMode: value, modeValue: 0 } }
143
- : undefined,
144
- })
145
-
146
- // Cache the new state and log if appropriate
147
- this.cacheSize = value
148
- this.cacheState = 'on'
149
- this.service.updateCharacteristic(this.hapChar.Active, 1)
150
- this.accessory.log(`${platformLang.curSpeed} [${this.sizeLabels[value]}]`)
151
- } catch (err) {
152
- // Catch any errors during the process
153
- this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`)
154
-
155
- // Throw a 'no response' error and set a timeout to revert this after 2 seconds
156
- setTimeout(() => {
157
- this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSize)
158
- }, 2000)
159
- throw new this.hapErr(-70402)
160
- }
161
- }
162
-
163
- externalUpdate(params) {
164
- // Handle OpenAPI workMode
165
- if (params.workMode) {
166
- const mode = params.workMode.workMode
167
- if (mode > 0 && mode <= 3) {
168
- if (this.cacheState !== 'on') {
169
- this.cacheState = 'on'
170
- this.service.updateCharacteristic(this.hapChar.Active, 1)
171
- this.accessory.log(`${platformLang.curState} [on]`)
172
- }
173
- if (this.cacheSize !== mode) {
174
- this.cacheSize = mode
175
- this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSize)
176
- this.accessory.log(`${platformLang.curSpeed} [${this.sizeLabels[this.cacheSize]}]`)
177
- }
178
- } else if (mode === 0) {
179
- if (this.cacheState !== 'off') {
180
- this.cacheState = 'off'
181
- this.service.updateCharacteristic(this.hapChar.Active, 0)
182
- this.accessory.log(`${platformLang.curState} [off]`)
183
- }
184
- }
185
- }
186
-
187
- // Check for some other scene/mode change
188
- (params.commands || []).forEach((command) => {
189
- const hexString = base64ToHex(command)
190
- const hexParts = hexToTwoItems(hexString)
191
-
192
- // Return now if not a device query update code
193
- if (getTwoItemPosition(hexParts, 1) !== 'aa') {
194
- return
195
- }
196
-
197
- const deviceFunction = `${getTwoItemPosition(hexParts, 1)}${getTwoItemPosition(hexParts, 2)}`
198
-
199
- switch (deviceFunction) {
200
- case 'aa19': {
201
- // On/Off
202
- const newState = getTwoItemPosition(hexParts, 3) === '01' ? 'on' : 'off'
203
- if (this.cacheState !== newState) {
204
- this.cacheState = newState
205
- this.service.updateCharacteristic(this.hapChar.Active, this.cacheState === 'on' ? 1 : 0)
206
- this.accessory.log(`${platformLang.curState} [${this.cacheState}]`)
207
- }
208
- break
209
- }
210
- case 'aa05': {
211
- // Ice size report. Left undecoded for now: the OpenAPI workMode report
212
- // above is the authoritative source of the current size, so decoding
213
- // the BLE echo would only risk a mismatch. This case exists to stop a
214
- // routine status report being logged as an unrecognised scene code.
215
- break
216
- }
217
- default:
218
- logUnknownData(this.accessory, {
219
- kind: 'scene',
220
- source: params.source,
221
- raw: command,
222
- hex: hexString,
223
- })
224
- break
225
- }
226
- })
227
- }
228
- }
@@ -1,188 +0,0 @@
1
- import { base64ToHex, parseError } from '../utils/functions.js'
2
- import platformLang from '../utils/lang-en.js'
3
-
4
- // Models that expose a night light. The H5089 reports its light as
5
- // `aa 1b 01 [on] [level]` and that reading is correct, so the tile shows the
6
- // real state. No command shape tried moves it - neither mirroring its own
7
- // status code nor the `33 18` the purifiers use - so control is refused
8
- // outright rather than accepted and silently dropped (#1323).
9
- const nightLightModels = ['H5089']
10
-
11
- export default class {
12
- constructor(platform, accessory) {
13
- // Set up variables from the platform
14
- this.hapChar = platform.api.hap.Characteristic
15
- this.hapErr = platform.api.hap.HapStatusError
16
- this.hapServ = platform.api.hap.Service
17
- this.platform = platform
18
-
19
- // Set up variables from the accessory
20
- this.accessory = accessory
21
-
22
- // Remove switch services if they exist
23
- if (this.accessory.getService('Switch 1')) {
24
- this.accessory.removeService(this.accessory.getService('Switch 1'))
25
- }
26
- if (this.accessory.getService('Switch 2')) {
27
- this.accessory.removeService(this.accessory.getService('Switch 2'))
28
- }
29
-
30
- // A model that used to be treated as a single switch or outlet leaves an
31
- // unnumbered service behind, which would sit next to the numbered pair as
32
- // a tile that no longer does anything (#1323)
33
- this.accessory.services
34
- .filter(service => !service.subtype
35
- && [this.hapServ.Switch.UUID, this.hapServ.Outlet.UUID].includes(service.UUID))
36
- .forEach(service => this.accessory.removeService(service))
37
-
38
- // Add the outlet services if they don't already exist
39
- this.service1 = this.accessory.getService('Outlet 1')
40
- || this.accessory.addService(this.hapServ.Outlet, 'Outlet 1', 'outlet1')
41
- this.service2 = this.accessory.getService('Outlet 2')
42
- || this.accessory.addService(this.hapServ.Outlet, 'Outlet 2', 'outlet2')
43
-
44
- if (!this.service1.testCharacteristic(this.hapChar.ConfiguredName)) {
45
- this.service1.addCharacteristic(this.hapChar.ConfiguredName)
46
- this.service1.updateCharacteristic(this.hapChar.ConfiguredName, 'Outlet 1')
47
- }
48
- if (!this.service1.testCharacteristic(this.hapChar.ServiceLabelIndex)) {
49
- this.service1.addCharacteristic(this.hapChar.ServiceLabelIndex)
50
- this.service1.updateCharacteristic(this.hapChar.ServiceLabelIndex, 1)
51
- }
52
- if (!this.service2.testCharacteristic(this.hapChar.ConfiguredName)) {
53
- this.service2.addCharacteristic(this.hapChar.ConfiguredName)
54
- this.service2.updateCharacteristic(this.hapChar.ConfiguredName, 'Outlet 2')
55
- }
56
- if (!this.service2.testCharacteristic(this.hapChar.ServiceLabelIndex)) {
57
- this.service2.addCharacteristic(this.hapChar.ServiceLabelIndex)
58
- this.service2.updateCharacteristic(this.hapChar.ServiceLabelIndex, 2)
59
- }
60
-
61
- // Add the set handler to the switch on/off characteristic
62
- this.service1.getCharacteristic(this.hapChar.On).onSet(async (value) => {
63
- await this.internalStateUpdate(this.service1, value ? 17 : 16)
64
- })
65
- this.service1.cacheState = this.service1.getCharacteristic(this.hapChar.On).value ? 'on' : 'off'
66
- this.service2.getCharacteristic(this.hapChar.On).onSet(async (value) => {
67
- await this.internalStateUpdate(this.service2, value ? 34 : 32)
68
- })
69
- this.service2.cacheState = this.service2.getCharacteristic(this.hapChar.On).value ? 'on' : 'off'
70
-
71
- // Add a night light service for the models that have one
72
- if (nightLightModels.includes(this.accessory.context.gvModel)) {
73
- this.lightService = this.accessory.getService('Night Light')
74
- || this.accessory.addService(this.hapServ.Lightbulb, 'Night Light', 'nightlight')
75
- if (!this.lightService.testCharacteristic(this.hapChar.ConfiguredName)) {
76
- this.lightService.addCharacteristic(this.hapChar.ConfiguredName)
77
- this.lightService.updateCharacteristic(this.hapChar.ConfiguredName, 'Night Light')
78
- }
79
- this.lightService.getCharacteristic(this.hapChar.On).onSet(() => this.refuseLightControl())
80
- this.cacheLightState = this.lightService.getCharacteristic(this.hapChar.On).value ? 'on' : 'off'
81
- this.lightService.getCharacteristic(this.hapChar.Brightness).onSet(() => this.refuseLightControl())
82
- this.cacheLightBright = this.lightService.getCharacteristic(this.hapChar.Brightness).value || 100
83
- } else if (this.accessory.getService('Night Light')) {
84
- this.accessory.removeService(this.accessory.getService('Night Light'))
85
- }
86
-
87
- // Output the customised options to the log
88
- const opts = JSON.stringify({
89
- showAs: 'outlet',
90
- })
91
- platform.log('[%s] %s %s.', accessory.displayName, platformLang.devInitOpts, opts)
92
-
93
- // The value is (which outlets to change << 4) | (which of those to turn on)
94
- // 51 turns BOTH ON
95
- // 48 turns BOTH OFF
96
- // 17 turns outlet 1 ON
97
- // 16 turns outlet 1 OFF
98
- // 34 turns outlet 2 ON
99
- // 32 turns outlet 2 OFF
100
- }
101
-
102
- async internalStateUpdate(service, value) {
103
- try {
104
- const newValue = value % 16 === 0 ? 'off' : 'on'
105
-
106
- // Don't continue if the new value is the same as before
107
- if (service.cacheState === newValue) {
108
- return
109
- }
110
-
111
- // Send the request to the platform sender function
112
- await this.platform.sendDeviceUpdate(this.accessory, {
113
- cmd: 'stateDual',
114
- value,
115
- })
116
-
117
- // Cache the new state and log if appropriate
118
- if (service.cacheState !== newValue) {
119
- service.cacheState = newValue
120
- this.accessory.log(`[${service.getCharacteristic(this.hapChar.ConfiguredName).value}] ${platformLang.curState} [${newValue}]`)
121
- }
122
- } catch (err) {
123
- // Catch any errors during the process
124
- this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`)
125
-
126
- // Throw a 'no response' error and set a timeout to revert this after 2 seconds
127
- setTimeout(() => {
128
- service.updateCharacteristic(this.hapChar.On, service.cacheState === 'on')
129
- }, 2000)
130
- throw new this.hapErr(-70402)
131
- }
132
- }
133
-
134
- // The night light reading is correct, but every command shape tried has been
135
- // ignored by the plug, so refuse the write. HomeKit reverts the tile rather
136
- // than showing a state the plug was never given (#1323)
137
- refuseLightControl() {
138
- this.accessory.logWarn(`[Night Light] ${platformLang.devNotUpdated} this device does not accept night light commands`)
139
- throw new this.hapErr(-70402)
140
- }
141
-
142
- externalUpdate(params) {
143
- if (Array.isArray(params.state)) {
144
- if (params.state[0] !== this.service1.cacheState) {
145
- this.service1.cacheState = params.state[0]
146
- this.service1.updateCharacteristic(this.hapChar.On, this.service1.cacheState === 'on')
147
-
148
- this.accessory.log(`[${this.service1.getCharacteristic(this.hapChar.ConfiguredName).value}] ${platformLang.curState} [${this.service1.cacheState}]`)
149
- }
150
-
151
- if (params.state[1] !== this.service2.cacheState) {
152
- this.service2.cacheState = params.state[1]
153
- this.service2.updateCharacteristic(this.hapChar.On, this.service2.cacheState === 'on')
154
-
155
- this.accessory.log(`[${this.service2.getCharacteristic(this.hapChar.ConfiguredName).value}] ${platformLang.curState} [${this.service2.cacheState}]`)
156
- }
157
- }
158
-
159
- // The night light reports as `aa 1b 01 [on] [level]` (#1323)
160
- if (this.lightService && Array.isArray(params.commands)) {
161
- params.commands.forEach((command) => {
162
- let hexString
163
- try {
164
- hexString = base64ToHex(command)
165
- } catch {
166
- return
167
- }
168
- if (!hexString.startsWith('aa1b01')) {
169
- return
170
- }
171
-
172
- const newState = hexString.substring(6, 8) === '01' ? 'on' : 'off'
173
- if (newState !== this.cacheLightState) {
174
- this.cacheLightState = newState
175
- this.lightService.updateCharacteristic(this.hapChar.On, newState === 'on')
176
- this.accessory.log(`[Night Light] ${platformLang.curState} [${newState}]`)
177
- }
178
-
179
- const newBright = Number.parseInt(hexString.substring(8, 10), 16)
180
- if (newBright >= 1 && newBright <= 100 && newBright !== this.cacheLightBright) {
181
- this.cacheLightBright = newBright
182
- this.lightService.updateCharacteristic(this.hapChar.Brightness, newBright)
183
- this.accessory.log(`[Night Light] ${platformLang.curBright} [${newBright}%]`)
184
- }
185
- })
186
- }
187
- }
188
- }
@@ -1,160 +0,0 @@
1
- import { parseError } from '../utils/functions.js'
2
- import platformLang from '../utils/lang-en.js'
3
-
4
- export default class {
5
- constructor(platform, accessory) {
6
- // Set up variables from the platform
7
- this.hapChar = platform.api.hap.Characteristic
8
- this.hapErr = platform.api.hap.HapStatusError
9
- this.hapServ = platform.api.hap.Service
10
- this.platform = platform
11
-
12
- // Set up variables from the accessory
13
- this.accessory = accessory
14
-
15
- // Remove switch services if they exist
16
- if (this.accessory.getService('Switch 1')) {
17
- this.accessory.removeService(this.accessory.getService('Switch 1'))
18
- }
19
- if (this.accessory.getService('Switch 2')) {
20
- this.accessory.removeService(this.accessory.getService('Switch 2'))
21
- }
22
- if (this.accessory.getService('Switch 3')) {
23
- this.accessory.removeService(this.accessory.getService('Switch 3'))
24
- }
25
-
26
- // Add the outlet services if they don't already exist
27
- this.service1 = this.accessory.getService('Outlet 1')
28
- || this.accessory.addService(this.hapServ.Outlet, 'Outlet 1', 'outlet1')
29
- this.service2 = this.accessory.getService('Outlet 2')
30
- || this.accessory.addService(this.hapServ.Outlet, 'Outlet 2', 'outlet2')
31
- this.service3 = this.accessory.getService('Outlet 3')
32
- || this.accessory.addService(this.hapServ.Outlet, 'Outlet 3', 'outlet3')
33
-
34
- // Add the set handler to the switch on/off characteristic
35
- this.service1.getCharacteristic(this.hapChar.On).onSet(async (value) => {
36
- await this.internalStateUpdate(this.service1, value ? 17 : 16)
37
- })
38
- this.service1.cacheState = this.service1.getCharacteristic(this.hapChar.On).value ? 'on' : 'off'
39
- this.service2.getCharacteristic(this.hapChar.On).onSet(async (value) => {
40
- await this.internalStateUpdate(this.service2, value ? 34 : 32)
41
- })
42
- this.service2.cacheState = this.service2.getCharacteristic(this.hapChar.On).value ? 'on' : 'off'
43
- this.service3.getCharacteristic(this.hapChar.On).onSet(async (value) => {
44
- await this.internalStateUpdate(this.service3, value ? 68 : 64)
45
- })
46
- this.service3.cacheState = this.service3.getCharacteristic(this.hapChar.On).value ? 'on' : 'off'
47
-
48
- // Output the customised options to the log
49
- const opts = JSON.stringify({
50
- showAs: 'outlet',
51
- })
52
- platform.log('[%s] %s %s.', accessory.displayName, platformLang.devInitOpts, opts)
53
-
54
- // 119 turns BOTH ON
55
- // 112 turns BOTH OFF
56
-
57
- // 68 turns outlet 3 ON
58
- // 64 turns outlet 3 OFF
59
-
60
- // 34 turns outlet 2 ON
61
- // 32 turns outlet 2 OFF
62
-
63
- // 17 turns outlet 1 ON
64
- // 16 turns outlet 1 OFF
65
- }
66
-
67
- async internalStateUpdate(service, value) {
68
- try {
69
- const newValue = value % 16 === 0 ? 'off' : 'on'
70
-
71
- // Don't continue if the new value is the same as before
72
- if (service.cacheState === newValue) {
73
- return
74
- }
75
-
76
- // Send the request to the platform sender function
77
- await this.platform.sendDeviceUpdate(this.accessory, {
78
- cmd: 'stateDual',
79
- value,
80
- })
81
-
82
- // Cache the new state and log if appropriate
83
- service.cacheState = newValue
84
- this.accessory.log(`[${service.displayName}] ${platformLang.curState} [${newValue}]`)
85
- } catch (err) {
86
- // Catch any errors during the process
87
- this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`)
88
-
89
- // Throw a 'no response' error and set a timeout to revert this after 2 seconds
90
- setTimeout(() => {
91
- service.updateCharacteristic(this.hapChar.On, service.cacheState === 'on')
92
- }, 2000)
93
- throw new this.hapErr(-70402)
94
- }
95
- }
96
-
97
- externalUpdate(params) {
98
- // Check to see if the provided state is different from the cached state
99
- if (!params.stateDual) {
100
- return
101
- }
102
-
103
- if ([112, 119].includes(params.stateDual)) {
104
- // 112 -> both off
105
- // 119 -> both on
106
- const newState = params.stateDual === 119 ? 'on' : 'off'
107
- if (newState !== this.service1.cacheState) {
108
- this.service1.cacheState = newState
109
- this.service1.updateCharacteristic(this.hapChar.On, this.service1.cacheState === 'on')
110
-
111
- // Log the change
112
- this.accessory.log(`[${this.service1.displayName}] ${platformLang.curState} [${this.service1.cacheState}]`)
113
- }
114
- if (newState !== this.service2.cacheState) {
115
- this.service2.cacheState = newState
116
- this.service2.updateCharacteristic(this.hapChar.On, this.service2.cacheState === 'on')
117
-
118
- // Log the change
119
- this.accessory.log(`[${this.service2.displayName}] ${platformLang.curState} [${this.service2.cacheState}]`)
120
- }
121
- if (newState !== this.service3.cacheState) {
122
- this.service3.cacheState = newState
123
- this.service3.updateCharacteristic(this.hapChar.On, this.service3.cacheState === 'on')
124
-
125
- // Log the change
126
- this.accessory.log(`[${this.service3.displayName}] ${platformLang.curState} [${this.service3.cacheState}]`)
127
- }
128
- }
129
- if ([16, 17].includes(params.stateDual)) {
130
- const newState = params.stateDual === 17 ? 'on' : 'off'
131
- if (newState !== this.service1.cacheState) {
132
- this.service1.cacheState = newState
133
- this.service1.updateCharacteristic(this.hapChar.On, this.service1.cacheState === 'on')
134
-
135
- // Log the change
136
- this.accessory.log(`[${this.service1.displayName}] ${platformLang.curState} [${this.service1.cacheState}]`)
137
- }
138
- }
139
- if ([32, 34].includes(params.stateDual)) {
140
- const newState = params.stateDual === 34 ? 'on' : 'off'
141
- if (newState !== this.service2.cacheState) {
142
- this.service2.cacheState = newState
143
- this.service2.updateCharacteristic(this.hapChar.On, this.service2.cacheState === 'on')
144
-
145
- // Log the change
146
- this.accessory.log(`[${this.service2.displayName}] ${platformLang.curState} [${this.service2.cacheState}]`)
147
- }
148
- }
149
- if ([64, 68].includes(params.stateDual)) {
150
- const newState = params.stateDual === 68 ? 'on' : 'off'
151
- if (newState !== this.service3.cacheState) {
152
- this.service3.cacheState = newState
153
- this.service3.updateCharacteristic(this.hapChar.On, this.service3.cacheState === 'on')
154
-
155
- // Log the change
156
- this.accessory.log(`[${this.service3.displayName}] ${platformLang.curState} [${this.service3.cacheState}]`)
157
- }
158
- }
159
- }
160
- }