@homebridge-plugins/homebridge-ewelink 12.7.0 → 12.7.1-beta.0

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
@@ -1,15 +1,27 @@
1
1
  # Change Log
2
2
 
3
- All notable changes to homebridge-ewelink will be documented in this file.
3
+ All notable changes to `@homebridge-plugins/homebridge-ewelink` will be documented in this file.
4
4
 
5
- ### v12.7.0 (2025-09-19)
5
+ ## v12.8.0 (2025-12-05)
6
+
7
+ ### Changes
8
+
9
+ - feat: add support for Virtual Switch (UIID 264) and Virtual Button (UIID 265)
10
+ - support more devices
11
+ - update readme badges (use `shields.io`)
12
+ - update workflows and npm publish via oidc
13
+ - updated dependencies
14
+
15
+ ## v12.7.0 (2025-09-19)
16
+
17
+ ### Changes
6
18
 
7
19
  - dependency updates
8
20
  - added new devices
9
21
 
10
22
  ## v12.6.2 (2025-07-24)
11
23
 
12
- ### Other Changes
24
+ ### Changes
13
25
 
14
26
  - dependency updates
15
27
 
package/README.md CHANGED
@@ -8,11 +8,9 @@
8
8
  Homebridge plugin to integrate eWeLink devices into HomeKit
9
9
 
10
10
  [![npm](https://img.shields.io/npm/v/@homebridge-plugins/homebridge-ewelink/latest?label=latest)](https://www.npmjs.com/package/@homebridge-plugins/homebridge-ewelink)
11
- [![npm](https://img.shields.io/npm/v/@homebridge-plugins/homebridge-ewelink/beta?label=beta)](https://github.com/homebridge-plugins/homebridge-ewelink/wiki/Beta-Version)
12
-
13
- [![verified-by-homebridge](https://badgen.net/badge/homebridge/verified/purple)](https://github.com/homebridge/homebridge/wiki/Verified-Plugins)
14
- [![hoobs-certified](https://badgen.net/badge/HOOBS/certified/yellow?label=hoobs)](https://plugins.hoobs.org/plugin/homebridge-ewelink)
15
-
11
+ [![npm](https://img.shields.io/npm/v/@homebridge-plugins/homebridge-ewelink/beta?label=beta)](https://github.com/homebridge-plugins/homebridge-ewelink/wiki/Beta-Version)<br>
12
+ [![verified-by-homebridge](https://img.shields.io/badge/homebridge-verified-blueviolet?color=%23491F59&style=flat)](https://github.com/homebridge/homebridge/wiki/Verified-Plugins)
13
+ [![hoobs-certified](https://img.shields.io/badge/hoobs-certified-yellow)](https://plugins.hoobs.org/plugin/homebridge-ewelink)<br>
16
14
  [![npm](https://img.shields.io/npm/dt/@homebridge-plugins/homebridge-ewelink)](https://www.npmjs.com/package/@homebridge-plugins/homebridge-ewelink)
17
15
  [![Discord](https://img.shields.io/discord/432663330281226270?color=728ED5&logo=discord&label=hb-discord)](https://discord.com/channels/432663330281226270/742733745743855627)
18
16
 
@@ -0,0 +1,512 @@
1
+ import platformConsts from '../utils/constants.js'
2
+ import { hasProperty, parseError } from '../utils/functions.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.hapServ = platform.api.hap.Service
9
+ this.hapErr = platform.api.hap.HapStatusError
10
+ this.lang = platform.lang
11
+ this.log = platform.log
12
+ this.platform = platform
13
+
14
+ // Set up variables from the accessory
15
+ this.name = accessory.displayName
16
+ this.accessory = accessory
17
+
18
+ // Initially set the online flag as true (to be then updated as false if necessary)
19
+ this.isOnline = true
20
+
21
+ // Set up custom variables for this device type
22
+ const deviceConf = platform.deviceConf[accessory.context.eweDeviceId] || {}
23
+ this.tempOffset = deviceConf.offset || platformConsts.defaultValues.offset
24
+ this.tempOffsetFactor = deviceConf.offsetFactor
25
+
26
+ // Set the correct logging variables for this accessory
27
+ switch (deviceConf.overrideLogging) {
28
+ case 'standard':
29
+ this.enableLogging = true
30
+ this.enableDebugLogging = false
31
+ break
32
+ case 'debug':
33
+ this.enableLogging = true
34
+ this.enableDebugLogging = true
35
+ break
36
+ case 'disable':
37
+ this.enableLogging = false
38
+ this.enableDebugLogging = false
39
+ break
40
+ default:
41
+ this.enableLogging = !platform.config.disableDeviceLogging
42
+ this.enableDebugLogging = platform.config.debug
43
+ break
44
+ }
45
+
46
+ // Add the heater cooler service if it doesn't already exist
47
+ this.service = this.accessory.getService(this.hapServ.HeaterCooler)
48
+ || this.accessory.addService(this.hapServ.HeaterCooler)
49
+
50
+ // Set custom properties of the current temperature characteristic
51
+ this.service.getCharacteristic(this.hapChar.CurrentTemperature).setProps({
52
+ minStep: 0.1,
53
+ })
54
+ this.cacheTemp = this.service.getCharacteristic(this.hapChar.CurrentTemperature).value
55
+
56
+ // Add the set handler to the active characteristic
57
+ this.service
58
+ .getCharacteristic(this.hapChar.Active)
59
+ .onSet(async value => this.internalActiveUpdate(value))
60
+
61
+ // Add the set handler to the target heater cooler state characteristic
62
+ this.service
63
+ .getCharacteristic(this.hapChar.TargetHeaterCoolerState)
64
+ .setProps({
65
+ minValue: 0,
66
+ maxValue: 2,
67
+ validValues: [0, 1, 2], // Auto, Heat, Cool
68
+ })
69
+ .onSet(async value => this.internalModeUpdate(value))
70
+
71
+ // Add the set handler to the cooling threshold temperature characteristic
72
+ this.service
73
+ .getCharacteristic(this.hapChar.CoolingThresholdTemperature)
74
+ .setProps({
75
+ minValue: 16,
76
+ maxValue: 32,
77
+ minStep: 1,
78
+ })
79
+ .onSet(async value => this.internalTempUpdate(value, 'cool'))
80
+
81
+ // Add the set handler to the heating threshold temperature characteristic
82
+ this.service
83
+ .getCharacteristic(this.hapChar.HeatingThresholdTemperature)
84
+ .setProps({
85
+ minValue: 16,
86
+ maxValue: 32,
87
+ minStep: 1,
88
+ })
89
+ .onSet(async value => this.internalTempUpdate(value, 'heat'))
90
+
91
+ // Add the set handler to the rotation speed characteristic (for fan speed)
92
+ this.service
93
+ .getCharacteristic(this.hapChar.RotationSpeed)
94
+ .setProps({
95
+ minValue: 0,
96
+ maxValue: 100,
97
+ minStep: 25,
98
+ })
99
+ .onSet(async value => this.internalSpeedUpdate(value))
100
+
101
+ // Add the get handlers only if the user hasn't disabled the disableNoResponse setting
102
+ if (!platform.config.disableNoResponse) {
103
+ this.service.getCharacteristic(this.hapChar.Active).onGet(() => {
104
+ if (!this.isOnline) {
105
+ throw new this.hapErr(-70402)
106
+ }
107
+ return this.service.getCharacteristic(this.hapChar.Active).value
108
+ })
109
+ this.service.getCharacteristic(this.hapChar.TargetHeaterCoolerState).onGet(() => {
110
+ if (!this.isOnline) {
111
+ throw new this.hapErr(-70402)
112
+ }
113
+ return this.service.getCharacteristic(this.hapChar.TargetHeaterCoolerState).value
114
+ })
115
+ this.service.getCharacteristic(this.hapChar.CurrentHeaterCoolerState).onGet(() => {
116
+ if (!this.isOnline) {
117
+ throw new this.hapErr(-70402)
118
+ }
119
+ return this.service.getCharacteristic(this.hapChar.CurrentHeaterCoolerState).value
120
+ })
121
+ this.service.getCharacteristic(this.hapChar.CurrentTemperature).onGet(() => {
122
+ if (!this.isOnline) {
123
+ throw new this.hapErr(-70402)
124
+ }
125
+ return this.service.getCharacteristic(this.hapChar.CurrentTemperature).value
126
+ })
127
+ this.service.getCharacteristic(this.hapChar.CoolingThresholdTemperature).onGet(() => {
128
+ if (!this.isOnline) {
129
+ throw new this.hapErr(-70402)
130
+ }
131
+ return this.service.getCharacteristic(this.hapChar.CoolingThresholdTemperature).value
132
+ })
133
+ this.service.getCharacteristic(this.hapChar.HeatingThresholdTemperature).onGet(() => {
134
+ if (!this.isOnline) {
135
+ throw new this.hapErr(-70402)
136
+ }
137
+ return this.service.getCharacteristic(this.hapChar.HeatingThresholdTemperature).value
138
+ })
139
+ this.service.getCharacteristic(this.hapChar.RotationSpeed).onGet(() => {
140
+ if (!this.isOnline) {
141
+ throw new this.hapErr(-70402)
142
+ }
143
+ return this.service.getCharacteristic(this.hapChar.RotationSpeed).value
144
+ })
145
+ }
146
+
147
+ // Initialise these caches now since they aren't determined by the initial externalUpdate()
148
+ this.cacheTarg = this.service.getCharacteristic(this.hapChar.CoolingThresholdTemperature).value
149
+ this.cacheMode = 'cool'
150
+ this.cacheSpeed = 50
151
+
152
+ // Pass the accessory to Fakegato to set up with Eve
153
+ this.accessory.eveService = new platform.eveService('custom', this.accessory, {
154
+ log: () => {},
155
+ })
156
+
157
+ // Output the customised options to the log
158
+ const normalLogging = this.enableLogging ? 'standard' : 'disable'
159
+ const opts = JSON.stringify({
160
+ logging: this.enableDebugLogging ? 'debug' : normalLogging,
161
+ offset: this.tempOffset,
162
+ offsetFactor: this.tempOffsetFactor,
163
+ })
164
+ this.log('[%s] %s %s.', this.name, this.lang.devInitOpts, opts)
165
+ }
166
+
167
+ async internalActiveUpdate(value) {
168
+ try {
169
+ const newValue = value === 1 ? 'on' : 'off'
170
+
171
+ // Don't continue if the value is the same as before
172
+ const prevState = this.service.getCharacteristic(this.hapChar.Active).value === 1 ? 'on' : 'off'
173
+ if (prevState === newValue) {
174
+ return
175
+ }
176
+
177
+ // Send the request to the platform sender function
178
+ await this.platform.sendDeviceUpdate(this.accessory, {
179
+ power: newValue,
180
+ })
181
+
182
+ // Update the current state characteristic
183
+ this.service.updateCharacteristic(
184
+ this.hapChar.CurrentHeaterCoolerState,
185
+ value === 1
186
+ ? (this.cacheMode === 'heat'
187
+ ? this.hapChar.CurrentHeaterCoolerState.HEATING
188
+ : this.hapChar.CurrentHeaterCoolerState.COOLING)
189
+ : this.hapChar.CurrentHeaterCoolerState.INACTIVE,
190
+ )
191
+
192
+ // Cache the new state and log if appropriate
193
+ if (this.enableLogging) {
194
+ this.log('[%s] %s [%s].', this.name, this.lang.curState, newValue)
195
+ }
196
+ } catch (err) {
197
+ // Catch any errors during the process
198
+ const eText = parseError(err)
199
+ this.log.warn('[%s] %s %s.', this.name, this.lang.cantCtl, eText)
200
+
201
+ // Throw a 'no response' error and set a timeout to revert this after 2 seconds
202
+ const prevStateValue = this.service.getCharacteristic(this.hapChar.Active).value
203
+ setTimeout(() => {
204
+ this.service.updateCharacteristic(this.hapChar.Active, prevStateValue)
205
+ }, 2000)
206
+ throw new this.hapErr(-70402)
207
+ }
208
+ }
209
+
210
+ async internalModeUpdate(value) {
211
+ try {
212
+ let newMode
213
+ switch (value) {
214
+ case 0: // Auto
215
+ newMode = 'auto'
216
+ break
217
+ case 1: // Heat
218
+ newMode = 'heat'
219
+ break
220
+ case 2: // Cool
221
+ newMode = 'cool'
222
+ break
223
+ default:
224
+ return
225
+ }
226
+
227
+ // Don't continue if the value is the same as before
228
+ if (this.cacheMode === newMode) {
229
+ return
230
+ }
231
+
232
+ // Send the request to the platform sender function
233
+ await this.platform.sendDeviceUpdate(this.accessory, {
234
+ mode: newMode,
235
+ })
236
+
237
+ // Update the current state characteristic if device is on
238
+ if (this.service.getCharacteristic(this.hapChar.Active).value === 1) {
239
+ this.service.updateCharacteristic(
240
+ this.hapChar.CurrentHeaterCoolerState,
241
+ value === 1
242
+ ? this.hapChar.CurrentHeaterCoolerState.HEATING
243
+ : value === 2
244
+ ? this.hapChar.CurrentHeaterCoolerState.COOLING
245
+ : this.hapChar.CurrentHeaterCoolerState.IDLE,
246
+ )
247
+ }
248
+
249
+ // Cache the new mode and log if appropriate
250
+ this.cacheMode = newMode
251
+ if (this.enableLogging) {
252
+ this.log('[%s] %s [%s].', this.name, this.lang.curMode, newMode)
253
+ }
254
+ } catch (err) {
255
+ // Catch any errors during the process
256
+ const eText = parseError(err)
257
+ this.log.warn('[%s] %s %s.', this.name, this.lang.cantCtl, eText)
258
+
259
+ // Throw a 'no response' error and set a timeout to revert this after 2 seconds
260
+ setTimeout(() => {
261
+ const prevValue = this.cacheMode === 'heat' ? 1 : this.cacheMode === 'cool' ? 2 : 0
262
+ this.service.updateCharacteristic(this.hapChar.TargetHeaterCoolerState, prevValue)
263
+ }, 2000)
264
+ throw new this.hapErr(-70402)
265
+ }
266
+ }
267
+
268
+ async internalTempUpdate(value, mode) {
269
+ try {
270
+ // Only update if this is the active mode or auto mode
271
+ if (this.cacheMode !== mode && this.cacheMode !== 'auto') {
272
+ return
273
+ }
274
+
275
+ // Don't continue if the value is the same as before
276
+ if (this.cacheTarg === value) {
277
+ return
278
+ }
279
+
280
+ // Send the request to the platform sender function
281
+ await this.platform.sendDeviceUpdate(this.accessory, {
282
+ temperature: value,
283
+ })
284
+
285
+ // Cache the new target temperature and log if appropriate
286
+ this.cacheTarg = value
287
+ if (this.enableLogging) {
288
+ this.log('[%s] %s [%s°C].', this.name, this.lang.tarTemp, value)
289
+ }
290
+ } catch (err) {
291
+ // Catch any errors during the process
292
+ const eText = parseError(err)
293
+ this.log.warn('[%s] %s %s.', this.name, this.lang.cantCtl, eText)
294
+
295
+ // Throw a 'no response' error and set a timeout to revert this after 2 seconds
296
+ setTimeout(() => {
297
+ if (mode === 'cool') {
298
+ this.service.updateCharacteristic(this.hapChar.CoolingThresholdTemperature, this.cacheTarg)
299
+ } else {
300
+ this.service.updateCharacteristic(this.hapChar.HeatingThresholdTemperature, this.cacheTarg)
301
+ }
302
+ }, 2000)
303
+ throw new this.hapErr(-70402)
304
+ }
305
+ }
306
+
307
+ async internalSpeedUpdate(value) {
308
+ try {
309
+ // Map HomeKit 0-100 to eWeLink wind speed values
310
+ // Assuming 101 = low, 102 = medium, 103 = high
311
+ let windSpeed
312
+ if (value === 0) {
313
+ windSpeed = 0
314
+ } else if (value <= 33) {
315
+ windSpeed = 101
316
+ } else if (value <= 66) {
317
+ windSpeed = 102
318
+ } else {
319
+ windSpeed = 103
320
+ }
321
+
322
+ // Don't continue if the value is the same as before
323
+ if (this.cacheSpeed === windSpeed) {
324
+ return
325
+ }
326
+
327
+ // Send the request to the platform sender function
328
+ await this.platform.sendDeviceUpdate(this.accessory, {
329
+ wind_speed: windSpeed,
330
+ })
331
+
332
+ // Cache the new speed and log if appropriate
333
+ this.cacheSpeed = windSpeed
334
+ if (this.enableLogging) {
335
+ this.log('[%s] %s [%s].', this.name, this.lang.curSpeed, value)
336
+ }
337
+ } catch (err) {
338
+ // Catch any errors during the process
339
+ const eText = parseError(err)
340
+ this.log.warn('[%s] %s %s.', this.name, this.lang.cantCtl, eText)
341
+
342
+ // Map cached speed back to HomeKit value
343
+ let prevValue = 50
344
+ if (this.cacheSpeed === 101) {
345
+ prevValue = 25
346
+ } else if (this.cacheSpeed === 102) {
347
+ prevValue = 50
348
+ } else if (this.cacheSpeed === 103) {
349
+ prevValue = 75
350
+ }
351
+
352
+ // Throw a 'no response' error and set a timeout to revert this after 2 seconds
353
+ setTimeout(() => {
354
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, prevValue)
355
+ }, 2000)
356
+ throw new this.hapErr(-70402)
357
+ }
358
+ }
359
+
360
+ async externalUpdate(params) {
361
+ try {
362
+ // Check to see if the provided params are valid for this model
363
+ if (
364
+ !hasProperty(params, 'power')
365
+ && !hasProperty(params, 'mode')
366
+ && !hasProperty(params, 'temperature')
367
+ && !hasProperty(params, 'indoor_temperature')
368
+ && !hasProperty(params, 'wind_speed')
369
+ ) {
370
+ throw new Error(this.lang.paramsNotRecd)
371
+ }
372
+
373
+ // Update the active characteristic
374
+ if (hasProperty(params, 'power')) {
375
+ const newState = params.power === 'on' ? 1 : 0
376
+ this.service.updateCharacteristic(this.hapChar.Active, newState)
377
+
378
+ // Update the current state
379
+ this.service.updateCharacteristic(
380
+ this.hapChar.CurrentHeaterCoolerState,
381
+ newState === 1
382
+ ? (this.cacheMode === 'heat'
383
+ ? this.hapChar.CurrentHeaterCoolerState.HEATING
384
+ : this.hapChar.CurrentHeaterCoolerState.COOLING)
385
+ : this.hapChar.CurrentHeaterCoolerState.INACTIVE,
386
+ )
387
+
388
+ // Log the change if appropriate
389
+ if (params.updateSource && this.enableLogging) {
390
+ this.log('[%s] %s [%s].', this.name, this.lang.curState, params.power)
391
+ }
392
+ }
393
+
394
+ // Update the mode characteristic
395
+ if (hasProperty(params, 'mode')) {
396
+ let targetState
397
+ switch (params.mode) {
398
+ case 'auto':
399
+ targetState = 0
400
+ break
401
+ case 'heat':
402
+ targetState = 1
403
+ break
404
+ case 'cool':
405
+ targetState = 2
406
+ break
407
+ default:
408
+ return
409
+ }
410
+
411
+ this.cacheMode = params.mode
412
+ this.service.updateCharacteristic(this.hapChar.TargetHeaterCoolerState, targetState)
413
+
414
+ // Update current state if device is on
415
+ if (this.service.getCharacteristic(this.hapChar.Active).value === 1) {
416
+ this.service.updateCharacteristic(
417
+ this.hapChar.CurrentHeaterCoolerState,
418
+ targetState === 1
419
+ ? this.hapChar.CurrentHeaterCoolerState.HEATING
420
+ : targetState === 2
421
+ ? this.hapChar.CurrentHeaterCoolerState.COOLING
422
+ : this.hapChar.CurrentHeaterCoolerState.IDLE,
423
+ )
424
+ }
425
+
426
+ // Log the change if appropriate
427
+ if (params.updateSource && this.enableLogging) {
428
+ this.log('[%s] %s [%s].', this.name, this.lang.curMode, params.mode)
429
+ }
430
+ }
431
+
432
+ // Update the target temperature
433
+ if (hasProperty(params, 'temperature')) {
434
+ this.cacheTarg = params.temperature
435
+ this.service.updateCharacteristic(this.hapChar.CoolingThresholdTemperature, params.temperature)
436
+ this.service.updateCharacteristic(this.hapChar.HeatingThresholdTemperature, params.temperature)
437
+
438
+ // Log the change if appropriate
439
+ if (params.updateSource && this.enableLogging) {
440
+ this.log('[%s] %s [%s°C].', this.name, this.lang.tarTemp, params.temperature)
441
+ }
442
+ }
443
+
444
+ // Update the current temperature
445
+ if (hasProperty(params, 'indoor_temperature')) {
446
+ let newTemp = Number(params.indoor_temperature)
447
+
448
+ // Apply any configured offset
449
+ if (this.tempOffsetFactor) {
450
+ newTemp *= this.tempOffsetFactor
451
+ }
452
+ newTemp += this.tempOffset
453
+ newTemp = Math.round(newTemp * 10) / 10
454
+
455
+ this.cacheTemp = newTemp
456
+ this.service.updateCharacteristic(this.hapChar.CurrentTemperature, newTemp)
457
+ this.accessory.eveService.addEntry({
458
+ time: Math.round(Date.now() / 1000),
459
+ temp: newTemp,
460
+ })
461
+
462
+ // Log the change if appropriate
463
+ if (params.updateSource && this.enableLogging) {
464
+ this.log('[%s] %s [%s°C].', this.name, this.lang.curTemp, newTemp)
465
+ }
466
+ }
467
+
468
+ // Update the fan speed
469
+ if (hasProperty(params, 'wind_speed')) {
470
+ // Map eWeLink wind speed to HomeKit rotation speed
471
+ let rotationSpeed = 50
472
+ if (params.wind_speed === 0) {
473
+ rotationSpeed = 0
474
+ } else if (params.wind_speed === 101) {
475
+ rotationSpeed = 25
476
+ } else if (params.wind_speed === 102) {
477
+ rotationSpeed = 50
478
+ } else if (params.wind_speed === 103) {
479
+ rotationSpeed = 75
480
+ }
481
+
482
+ this.cacheSpeed = params.wind_speed
483
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, rotationSpeed)
484
+
485
+ // Log the change if appropriate
486
+ if (params.updateSource && this.enableLogging) {
487
+ this.log('[%s] %s [%s].', this.name, this.lang.curSpeed, rotationSpeed)
488
+ }
489
+ }
490
+ } catch (err) {
491
+ // Catch any errors during the process
492
+ const eText = parseError(err)
493
+ this.log.warn('[%s] %s %s.', this.name, this.lang.cantUpd, eText)
494
+ }
495
+ }
496
+
497
+ async currentState() {
498
+ const toReturn = {}
499
+ toReturn.services = ['heater-cooler']
500
+ toReturn.heater_cooler = {
501
+ state: this.service.getCharacteristic(this.hapChar.Active).value === 1 ? 'on' : 'off',
502
+ mode: this.cacheMode,
503
+ target_temp: this.cacheTarg,
504
+ current_temp: this.cacheTemp,
505
+ }
506
+ return toReturn
507
+ }
508
+
509
+ markStatus(isOnline) {
510
+ this.isOnline = isOnline
511
+ }
512
+ }
@@ -1,3 +1,4 @@
1
+ import deviceAirConditioner from './air-conditioner.js'
1
2
  import deviceCurtain from './curtain.js'
2
3
  import deviceDiffuser from './diffuser.js'
3
4
  import deviceFan from './fan.js'
@@ -26,9 +27,11 @@ import deviceSwitchSingleInched from './switch-single-inched.js'
26
27
  import deviceSwitchSingle from './switch-single.js'
27
28
  import deviceTemplate from './template.js'
28
29
  import deviceThermostat from './thermostat.js'
30
+ import deviceVirtual from './virtual.js'
29
31
  import zb from './zigbee/index.js'
30
32
 
31
33
  export default {
34
+ deviceAirConditioner,
32
35
  deviceCurtain,
33
36
  deviceDiffuser,
34
37
  deviceFan,
@@ -56,6 +59,7 @@ export default {
56
59
  deviceSwitchSingleInched,
57
60
  deviceTemplate,
58
61
  deviceThermostat,
62
+ deviceVirtual,
59
63
  sim,
60
64
  zb,
61
65
  }
@@ -0,0 +1,270 @@
1
+ import { hasProperty, parseError } from '../utils/functions.js'
2
+
3
+ export default class {
4
+ constructor(platform, accessory) {
5
+ // Set up variables from the platform
6
+ this.hapChar = platform.api.hap.Characteristic
7
+ this.hapServ = platform.api.hap.Service
8
+ this.hapErr = platform.api.hap.HapStatusError
9
+ this.lang = platform.lang
10
+ this.log = platform.log
11
+ this.platform = platform
12
+
13
+ // Set up variables from the accessory
14
+ this.name = accessory.displayName
15
+ this.accessory = accessory
16
+
17
+ // Initially set the online flag as true (to be then updated as false if necessary)
18
+ this.isOnline = true
19
+
20
+ // Set up custom variables for this device type
21
+ const deviceConf = platform.deviceConf[accessory.context.eweDeviceId] || {}
22
+
23
+ // Set the correct logging variables for this accessory
24
+ switch (deviceConf.overrideLogging) {
25
+ case 'standard':
26
+ this.enableLogging = true
27
+ this.enableDebugLogging = false
28
+ break
29
+ case 'debug':
30
+ this.enableLogging = true
31
+ this.enableDebugLogging = true
32
+ break
33
+ case 'disable':
34
+ this.enableLogging = false
35
+ this.enableDebugLogging = false
36
+ break
37
+ default:
38
+ this.enableLogging = !platform.config.disableDeviceLogging
39
+ this.enableDebugLogging = platform.config.debug
40
+ break
41
+ }
42
+
43
+ // Check device params to determine if it's a button or switch
44
+ this.isButton = hasProperty(accessory.context.eweParams, 'key')
45
+
46
+ if (this.isButton) {
47
+ // Virtual Button - use StatelessProgrammableSwitch
48
+ this.service = this.accessory.getService(this.hapServ.StatelessProgrammableSwitch)
49
+ || this.accessory.addService(this.hapServ.StatelessProgrammableSwitch)
50
+
51
+ // Set up the button with single, double, and long press
52
+ this.service.getCharacteristic(this.hapChar.ProgrammableSwitchEvent).setProps({
53
+ minValue: 0,
54
+ maxValue: 2,
55
+ validValues: [0, 1, 2], // Single, Double, Long press
56
+ })
57
+
58
+ // Remove switch service if it exists from previous setup
59
+ if (this.accessory.getService(this.hapServ.Switch)) {
60
+ this.accessory.removeService(this.accessory.getService(this.hapServ.Switch))
61
+ }
62
+ } else {
63
+ // Virtual Switch - use standard Switch service
64
+ this.service = this.accessory.getService(this.hapServ.Switch)
65
+ || this.accessory.addService(this.hapServ.Switch)
66
+
67
+ // Add the set handler to the switch on/off characteristic
68
+ this.service
69
+ .getCharacteristic(this.hapChar.On)
70
+ .onSet(async value => this.internalStateUpdate(value))
71
+
72
+ // Add the get handler only if the user hasn't disabled the disableNoResponse setting
73
+ if (!platform.config.disableNoResponse) {
74
+ this.service.getCharacteristic(this.hapChar.On).onGet(() => {
75
+ if (!this.isOnline) {
76
+ throw new this.hapErr(-70402)
77
+ }
78
+ return this.service.getCharacteristic(this.hapChar.On).value
79
+ })
80
+ }
81
+
82
+ // Remove button service if it exists from previous setup
83
+ if (this.accessory.getService(this.hapServ.StatelessProgrammableSwitch)) {
84
+ this.accessory.removeService(this.accessory.getService(this.hapServ.StatelessProgrammableSwitch))
85
+ }
86
+ }
87
+
88
+ // Pass the accessory to Fakegato to set up with Eve
89
+ this.accessory.eveService = new platform.eveService('switch', this.accessory, {
90
+ log: () => {},
91
+ })
92
+
93
+ // Output the customised options to the log
94
+ const normalLogging = this.enableLogging ? 'standard' : 'disable'
95
+ const opts = JSON.stringify({
96
+ logging: this.enableDebugLogging ? 'debug' : normalLogging,
97
+ type: this.isButton ? 'button' : 'switch',
98
+ })
99
+ this.log('[%s] %s %s.', this.name, this.lang.devInitOpts, opts)
100
+ }
101
+
102
+ async internalStateUpdate(value) {
103
+ try {
104
+ // This is only for virtual switches, not buttons
105
+ if (this.isButton) {
106
+ return
107
+ }
108
+
109
+ const newValue = value ? 'on' : 'off'
110
+
111
+ // Don't continue if the value is the same as before
112
+ const prevState = this.service.getCharacteristic(this.hapChar.On).value
113
+ if (prevState === value) {
114
+ return
115
+ }
116
+
117
+ // Send the request to the platform sender function
118
+ await this.platform.sendDeviceUpdate(this.accessory, {
119
+ switches: [
120
+ {
121
+ outlet: 0,
122
+ switch: newValue,
123
+ },
124
+ ],
125
+ })
126
+
127
+ // Cache the new state and log if appropriate
128
+ if (this.enableLogging) {
129
+ this.accessory.eveService.addEntry({
130
+ time: Math.round(Date.now() / 1000),
131
+ status: value ? 1 : 0,
132
+ })
133
+ this.log('[%s] %s [%s].', this.name, this.lang.curState, newValue)
134
+ }
135
+ } catch (err) {
136
+ // Catch any errors during the process
137
+ const eText = parseError(err)
138
+ this.log.warn('[%s] %s %s.', this.name, this.lang.cantCtl, eText)
139
+
140
+ // Throw a 'no response' error and set a timeout to revert this after 2 seconds
141
+ setTimeout(() => {
142
+ this.service.updateCharacteristic(this.hapChar.On, this.cacheState === 'on')
143
+ }, 2000)
144
+ throw new this.hapErr(-70402)
145
+ }
146
+ }
147
+
148
+ async externalUpdate(params) {
149
+ try {
150
+ if (this.isButton) {
151
+ // Handle button press events
152
+ if (hasProperty(params, 'key')) {
153
+ // Map key values to HomeKit button events
154
+ let buttonEvent
155
+ switch (params.key) {
156
+ case 0:
157
+ buttonEvent = this.hapChar.ProgrammableSwitchEvent.SINGLE_PRESS
158
+ break
159
+ case 1:
160
+ buttonEvent = this.hapChar.ProgrammableSwitchEvent.DOUBLE_PRESS
161
+ break
162
+ case 2:
163
+ buttonEvent = this.hapChar.ProgrammableSwitchEvent.LONG_PRESS
164
+ break
165
+ default:
166
+ // Unknown key value, default to single press
167
+ buttonEvent = this.hapChar.ProgrammableSwitchEvent.SINGLE_PRESS
168
+ }
169
+
170
+ // Update the button event characteristic
171
+ this.service.updateCharacteristic(this.hapChar.ProgrammableSwitchEvent, buttonEvent)
172
+
173
+ // Log the button press if appropriate
174
+ if (params.updateSource && this.enableLogging) {
175
+ const pressType = params.key === 0 ? 'single' : params.key === 1 ? 'double' : 'long'
176
+ this.log('[%s] %s [%s press].', this.name, this.lang.curState, pressType)
177
+ }
178
+ }
179
+
180
+ // Handle trigger time if provided
181
+ if (hasProperty(params, 'trigTime')) {
182
+ this.lastTriggerTime = params.trigTime
183
+ if (this.enableDebugLogging) {
184
+ this.log('[%s] Button triggered at %s.', this.name, new Date(Number.parseInt(params.trigTime)).toLocaleString())
185
+ }
186
+ }
187
+ } else {
188
+ // Handle virtual switch updates
189
+ if (hasProperty(params, 'switches') && Array.isArray(params.switches)) {
190
+ params.switches.forEach((outlet) => {
191
+ // Check this is the primary outlet
192
+ if (outlet.outlet !== 0) {
193
+ return
194
+ }
195
+
196
+ // Check we have a valid switch value
197
+ if (!hasProperty(outlet, 'switch')) {
198
+ return
199
+ }
200
+
201
+ const newState = outlet.switch === 'on'
202
+ const prevState = this.service.getCharacteristic(this.hapChar.On).value
203
+ if (prevState === newState) {
204
+ return
205
+ }
206
+
207
+ this.service.updateCharacteristic(this.hapChar.On, newState)
208
+ this.cacheState = outlet.switch
209
+
210
+ // Add Eve history entry
211
+ this.accessory.eveService.addEntry({
212
+ time: Math.round(Date.now() / 1000),
213
+ status: newState ? 1 : 0,
214
+ })
215
+
216
+ // Log the change if appropriate
217
+ if (params.updateSource && this.enableLogging) {
218
+ this.log('[%s] %s [%s].', this.name, this.lang.curState, outlet.switch)
219
+ }
220
+ })
221
+ }
222
+
223
+ // Also handle single switch param for compatibility
224
+ if (hasProperty(params, 'switch')) {
225
+ const newState = params.switch === 'on'
226
+ const prevState = this.service.getCharacteristic(this.hapChar.On).value
227
+ if (prevState !== newState) {
228
+ this.service.updateCharacteristic(this.hapChar.On, newState)
229
+ this.cacheState = params.switch
230
+
231
+ // Add Eve history entry
232
+ this.accessory.eveService.addEntry({
233
+ time: Math.round(Date.now() / 1000),
234
+ status: newState ? 1 : 0,
235
+ })
236
+
237
+ // Log the change if appropriate
238
+ if (params.updateSource && this.enableLogging) {
239
+ this.log('[%s] %s [%s].', this.name, this.lang.curState, params.switch)
240
+ }
241
+ }
242
+ }
243
+ }
244
+ } catch (err) {
245
+ // Catch any errors during the process
246
+ const eText = parseError(err)
247
+ this.log.warn('[%s] %s %s.', this.name, this.lang.cantUpd, eText)
248
+ }
249
+ }
250
+
251
+ async currentState() {
252
+ const toReturn = {}
253
+ if (this.isButton) {
254
+ toReturn.services = ['button']
255
+ toReturn.button = {
256
+ last_trigger: this.lastTriggerTime || 'none',
257
+ }
258
+ } else {
259
+ toReturn.services = ['switch']
260
+ toReturn.switch = {
261
+ state: this.cacheState || 'off',
262
+ }
263
+ }
264
+ return toReturn
265
+ }
266
+
267
+ markStatus(isOnline) {
268
+ this.isOnline = isOnline
269
+ }
270
+ }
package/lib/platform.js CHANGED
@@ -1106,6 +1106,26 @@ export default class {
1106
1106
  this.applyAccessoryLogging(accessory)
1107
1107
  accessory.control = new deviceTypes.deviceThermostat(this, accessory)
1108
1108
  /** */
1109
+ } else if (platformConsts.devices.airConditioner.includes(uiid)) {
1110
+ /**
1111
+ ***************
1112
+ AIR CONDITIONERS
1113
+ ****************
1114
+ */
1115
+ accessory = devicesInHB.get(uuid) || this.addAccessory(device, `${device.deviceid}SWX`)
1116
+ this.applyAccessoryLogging(accessory)
1117
+ accessory.control = new deviceTypes.deviceAirConditioner(this, accessory)
1118
+ /** */
1119
+ } else if (platformConsts.devices.virtual.includes(uiid)) {
1120
+ /**
1121
+ ***************
1122
+ VIRTUAL DEVICES
1123
+ ****************
1124
+ */
1125
+ accessory = devicesInHB.get(uuid) || this.addAccessory(device, `${device.deviceid}SWX`)
1126
+ this.applyAccessoryLogging(accessory)
1127
+ accessory.control = new deviceTypes.deviceVirtual(this, accessory)
1128
+ /** */
1109
1129
  } else if (
1110
1130
  device.showAs === 'thermostat'
1111
1131
  && platformConsts.devices.sensorAmbient.includes(uiid)
@@ -285,7 +285,7 @@ export default {
285
285
  212,
286
286
  ],
287
287
  switchSingle: [1, 6, 14, 24, 27, 225, 226, 1009, 1256, 7004, 7005, 7010, 7032],
288
- switchSCM: [77, 78, 81, 107, 112, 138, 160, 191, 209],
288
+ switchSCM: [77, 78, 81, 107, 112, 138, 160, 191, 209, 264],
289
289
  switchSCMPower: [182, 190],
290
290
  switchSinglePower: [5, 32],
291
291
  switchMulti: [
@@ -329,7 +329,9 @@ export default {
329
329
  sensorAmbient: [15, 181],
330
330
  sensorTempHumi: [18],
331
331
  thermostat: [127],
332
+ airConditioner: [151],
332
333
  panel: [133, 195],
334
+ virtual: [265],
333
335
  fan: [34],
334
336
  humidifier: [19],
335
337
  diffuser: [25],
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "displayName": "Homebridge eWeLink",
4
4
  "alias": "eWeLink",
5
5
  "type": "module",
6
- "version": "12.7.0",
6
+ "version": "12.7.1-beta.0",
7
7
  "description": "Homebridge plugin to integrate eWeLink devices into HomeKit.",
8
8
  "author": {
9
9
  "name": "bwp91",
@@ -61,14 +61,14 @@
61
61
  "lint:fix": "npm run lint -- --fix"
62
62
  },
63
63
  "dependencies": {
64
- "@homebridge/plugin-ui-utils": "^2.1.0",
65
- "axios": "^1.12.2",
64
+ "@homebridge/plugin-ui-utils": "^2.1.2",
65
+ "axios": "^1.13.2",
66
66
  "node-persist": "^4.0.4",
67
- "p-queue": "^8.1.1",
67
+ "p-queue": "^9.0.1",
68
68
  "websocket-as-promised": "^3.0.1",
69
69
  "ws": "^8.18.3"
70
70
  },
71
71
  "devDependencies": {
72
- "@antfu/eslint-config": "^5.4.1"
72
+ "@antfu/eslint-config": "^6.4.2"
73
73
  }
74
74
  }