@homebridge-plugins/homebridge-ewelink 12.6.2 → 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 +19 -2
- package/CLAUDE.md +144 -0
- package/README.md +3 -5
- package/lib/connection/lan.js +3 -0
- package/lib/device/air-conditioner.js +512 -0
- package/lib/device/index.js +4 -0
- package/lib/device/virtual.js +270 -0
- package/lib/platform.js +20 -0
- package/lib/utils/constants.js +14 -6
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +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
|
+
|
|
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
|
|
18
|
+
|
|
19
|
+
- dependency updates
|
|
20
|
+
- added new devices
|
|
4
21
|
|
|
5
22
|
## v12.6.2 (2025-07-24)
|
|
6
23
|
|
|
7
|
-
###
|
|
24
|
+
### Changes
|
|
8
25
|
|
|
9
26
|
- dependency updates
|
|
10
27
|
|
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
### Development
|
|
8
|
+
- **Lint code**: `npm run lint`
|
|
9
|
+
- **Fix lint issues**: `npm run lint:fix`
|
|
10
|
+
|
|
11
|
+
## Architecture
|
|
12
|
+
|
|
13
|
+
### Core Structure
|
|
14
|
+
This is a Homebridge plugin that integrates eWeLink devices into HomeKit. The main entry point is `lib/index.js` which registers the platform with Homebridge.
|
|
15
|
+
|
|
16
|
+
### Key Components
|
|
17
|
+
|
|
18
|
+
**Platform Layer** (`lib/platform.js`)
|
|
19
|
+
- Main platform class that coordinates all plugin operations
|
|
20
|
+
- Manages device discovery, initialization, and lifecycle
|
|
21
|
+
- Handles configuration and storage via node-persist
|
|
22
|
+
- Implements request queue with p-queue for rate limiting
|
|
23
|
+
- Routes devices to appropriate handlers based on UIID and configuration
|
|
24
|
+
|
|
25
|
+
**Connection Layer** (`lib/connection/`)
|
|
26
|
+
- `api.js`: Internal HTTP API server for local control
|
|
27
|
+
- `http.js`: eWeLink cloud API client
|
|
28
|
+
- `lan.js`: Local network device control
|
|
29
|
+
- `ws.js`: WebSocket client for real-time updates
|
|
30
|
+
|
|
31
|
+
**Device Layer** (`lib/device/`)
|
|
32
|
+
- Individual device type implementations (switches, lights, sensors, etc.)
|
|
33
|
+
- Each device type extends base functionality with specific HomeKit characteristics
|
|
34
|
+
- Supports various Sonoff/eWeLink device models
|
|
35
|
+
- Device types include:
|
|
36
|
+
- Single/Multi switches with optional power monitoring
|
|
37
|
+
- RGB/CCT/Dimmable lights
|
|
38
|
+
- Sensors (ambient, contact, motion, water)
|
|
39
|
+
- Thermostats, fans, curtains
|
|
40
|
+
- RF Bridge devices
|
|
41
|
+
- Zigbee devices (via bridge)
|
|
42
|
+
- Groups and simulations
|
|
43
|
+
|
|
44
|
+
**Utilities** (`lib/utils/`)
|
|
45
|
+
- `constants.js`: Device configurations and UIID mappings
|
|
46
|
+
- `functions.js`: Shared utility functions
|
|
47
|
+
- `custom-chars.js`: Custom HomeKit characteristics
|
|
48
|
+
- `eve-chars.js`: Eve Home app specific characteristics
|
|
49
|
+
- Language files for internationalization
|
|
50
|
+
|
|
51
|
+
### Device Identification (UIID)
|
|
52
|
+
Every eWeLink device has a UIID (unique interface ID) that determines its capabilities:
|
|
53
|
+
|
|
54
|
+
**Key UIID Categories** (defined in `lib/utils/constants.js`):
|
|
55
|
+
- `switchSingle`: [1, 6, 14, 24, 27, etc.] - Basic on/off switches
|
|
56
|
+
- `switchSinglePower`: [5, 32] - Switches with power monitoring
|
|
57
|
+
- `switchMulti`: [2, 3, 4, 7, 8, 9, etc.] - Multi-channel switches
|
|
58
|
+
- `lightRGB`: [22] - RGB color lights
|
|
59
|
+
- `lightCCT`: [103] - Color temperature lights
|
|
60
|
+
- `lightRGBCCT`: [33, 59, 104, etc.] - Full color + temperature lights
|
|
61
|
+
- `lightDimmer`: [36, 44, 57] - Dimmable lights
|
|
62
|
+
- `sensorContact`: [102, 154] - Door/window sensors
|
|
63
|
+
- `sensorAmbient`: [15, 181] - Temperature/humidity sensors
|
|
64
|
+
- `thermostat`: [127] - Smart thermostats
|
|
65
|
+
- `rfBridge`: [28, 98] - RF 433MHz bridges
|
|
66
|
+
- `zbBridge`: [66, 128, 168] - Zigbee bridges
|
|
67
|
+
- Zigbee prefixed devices (1000-7000 range)
|
|
68
|
+
|
|
69
|
+
**Device Routing Logic** (in `lib/platform.js:initialiseDevice()`):
|
|
70
|
+
1. Checks device UIID against category arrays in constants
|
|
71
|
+
2. Considers user configuration (showAs property for simulations)
|
|
72
|
+
3. Creates appropriate device handler instance
|
|
73
|
+
4. Special handling for:
|
|
74
|
+
- Power monitoring devices (different decimal places)
|
|
75
|
+
- Multi-channel devices (individual switches/outlets)
|
|
76
|
+
- Bridge devices (RF/Zigbee sub-devices)
|
|
77
|
+
- Simulated accessories (garage doors, valves, etc.)
|
|
78
|
+
|
|
79
|
+
### Communication Flow
|
|
80
|
+
1. Plugin authenticates with eWeLink cloud to fetch device list
|
|
81
|
+
2. Attempts local (LAN) control first for supported devices
|
|
82
|
+
3. Falls back to cloud control via HTTP/WebSocket if local fails
|
|
83
|
+
4. Maintains real-time sync through WebSocket connection
|
|
84
|
+
5. Exposes optional internal API for external control
|
|
85
|
+
|
|
86
|
+
### Command & Payload Structure
|
|
87
|
+
|
|
88
|
+
**HTTP Connection** (`lib/connection/http.js`)
|
|
89
|
+
- **Login**: POST to `/v2/user/login`
|
|
90
|
+
- Request: `{email/phoneNumber, password, countryCode}`
|
|
91
|
+
- Response: Returns `at` (access token) and `apikey`
|
|
92
|
+
- Uses HMAC-SHA256 signature with app secret
|
|
93
|
+
- **Get Homes**: GET to `/v2/family`
|
|
94
|
+
- Headers: Bearer token authentication
|
|
95
|
+
- **Get Devices**: GET to `/v2/device` and `/v2/group`
|
|
96
|
+
- Fetches device list per home with full params
|
|
97
|
+
|
|
98
|
+
**WebSocket Connection** (`lib/connection/ws.js`)
|
|
99
|
+
- **Authentication**: Action `userOnline`
|
|
100
|
+
- Payload: `{action: 'userOnline', apikey, appid, at, nonce, sequence, ts, version: 8}`
|
|
101
|
+
- **Device Update**: Action `update`
|
|
102
|
+
- Payload: `{action: 'update', apikey, deviceid, params, sequence, ts: 0, userAgent: 'app'}`
|
|
103
|
+
- Params vary by device type (see below)
|
|
104
|
+
- **Query State**: Action `query`
|
|
105
|
+
- Payload: `{action: 'query', apikey, deviceid, params: [], sequence}`
|
|
106
|
+
- **Heartbeat**: Sends `'ping'` at intervals to keep connection alive
|
|
107
|
+
|
|
108
|
+
**LAN Connection** (`lib/connection/lan.js`)
|
|
109
|
+
- Uses mDNS discovery for `_ewelink._tcp.local` services
|
|
110
|
+
- **Encryption**: AES-128-CBC with MD5 hashed device key
|
|
111
|
+
- **Update Request**: POST to `http://{device_ip}:8081/zeroconf/{suffix}`
|
|
112
|
+
- Suffixes: `switch`, `switches`, `dimmable`, `light`, `fan`, `transmit`, `location`, `monitor`
|
|
113
|
+
- Encrypted payload with IV
|
|
114
|
+
- Data structure: `{data, deviceid, encrypt: true, iv, selfApikey: '123', sequence}`
|
|
115
|
+
- **DNS Monitoring**: Listens for TXT records with encrypted device updates
|
|
116
|
+
|
|
117
|
+
**Common Device Parameters**
|
|
118
|
+
- **Single Switch**: `{switch: 'on'/'off'}`
|
|
119
|
+
- **Multi Switch**: `{switches: [{outlet: 0-3, switch: 'on'/'off'}]}`
|
|
120
|
+
- **SCM Devices**: Uses switches array even for single switch (UIID 77/78/81/107/112/138/160)
|
|
121
|
+
- **Dimmable**: `{brightness: 0-100, mode: 0}`
|
|
122
|
+
- **RGB Light**: `{ltype: 'color'/'white', color: {br, r, g, b}, white: {br, ct}}`
|
|
123
|
+
- **Power Monitor**: `{uiActive: 120}` to request power updates
|
|
124
|
+
- **Curtain/Motor**: `{location: 0-100}` for position
|
|
125
|
+
- **RF Bridge**: `{cmd: 'transmit', rfChl: channel}`
|
|
126
|
+
- **TH10/16**: `{switch, mainSwitch, deviceType: 'normal'}`
|
|
127
|
+
- **Fan (iFan)**: `{light: 'on'/'off', fan: 'on'/'off', speed: 1-3}`
|
|
128
|
+
|
|
129
|
+
**Update Priority**
|
|
130
|
+
1. Try LAN if device supports it (check `constants.devices.lan` array)
|
|
131
|
+
2. Fall back to WebSocket if LAN fails
|
|
132
|
+
3. Queue updates with 250ms intervals to prevent rate limiting
|
|
133
|
+
4. Power monitoring devices poll every 120 seconds
|
|
134
|
+
|
|
135
|
+
### State Management
|
|
136
|
+
- Device states cached using node-persist
|
|
137
|
+
- Queue system (p-queue) prevents API rate limiting
|
|
138
|
+
- Fakegato history service for Eve Home app integration
|
|
139
|
+
|
|
140
|
+
## Important Notes
|
|
141
|
+
- ES modules with Node.js 20/22/24 support required
|
|
142
|
+
- Uses @antfu/eslint-config for code style
|
|
143
|
+
- No test suite - manual testing required
|
|
144
|
+
- Verified Homebridge plugin following official standards
|
package/README.md
CHANGED
|
@@ -8,11 +8,9 @@
|
|
|
8
8
|
Homebridge plugin to integrate eWeLink devices into HomeKit
|
|
9
9
|
|
|
10
10
|
[](https://www.npmjs.com/package/@homebridge-plugins/homebridge-ewelink)
|
|
11
|
-
[](https://github.com/homebridge-plugins/homebridge-ewelink/wiki/Beta-Version)
|
|
12
|
-
|
|
13
|
-
[](https://plugins.hoobs.org/plugin/homebridge-ewelink)
|
|
15
|
-
|
|
11
|
+
[](https://github.com/homebridge-plugins/homebridge-ewelink/wiki/Beta-Version)<br>
|
|
12
|
+
[](https://github.com/homebridge/homebridge/wiki/Verified-Plugins)
|
|
13
|
+
[](https://plugins.hoobs.org/plugin/homebridge-ewelink)<br>
|
|
16
14
|
[](https://www.npmjs.com/package/@homebridge-plugins/homebridge-ewelink)
|
|
17
15
|
[](https://discord.com/channels/432663330281226270/742733745743855627)
|
|
18
16
|
|
package/lib/connection/lan.js
CHANGED
|
@@ -242,6 +242,9 @@ export default class {
|
|
|
242
242
|
} else if (json.params.uiActive && deviceInfo.uiid === 32) {
|
|
243
243
|
params.uiActive = json.params.uiActive
|
|
244
244
|
suffix = 'monitor'
|
|
245
|
+
} else if (json.params.uiActive && [126, 165].includes(deviceInfo.uiid)) {
|
|
246
|
+
// DualR3 uses statistics endpoint in LAN mode
|
|
247
|
+
suffix = 'statistics'
|
|
245
248
|
} else if (json.params.cmd) {
|
|
246
249
|
params.cmd = 'transmit'
|
|
247
250
|
params.rfChl = json.params.rfChl
|
|
@@ -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
|
+
}
|
package/lib/device/index.js
CHANGED
|
@@ -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)
|
package/lib/utils/constants.js
CHANGED
|
@@ -284,8 +284,8 @@ export default {
|
|
|
284
284
|
211,
|
|
285
285
|
212,
|
|
286
286
|
],
|
|
287
|
-
switchSingle: [1, 6, 14, 24, 27, 226, 1009, 1256, 7004, 7005, 7010],
|
|
288
|
-
switchSCM: [77, 78, 81, 107, 112, 138, 160, 191, 209],
|
|
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, 264],
|
|
289
289
|
switchSCMPower: [182, 190],
|
|
290
290
|
switchSinglePower: [5, 32],
|
|
291
291
|
switchMulti: [
|
|
@@ -312,23 +312,26 @@ export default {
|
|
|
312
312
|
210,
|
|
313
313
|
211,
|
|
314
314
|
212,
|
|
315
|
+
262,
|
|
315
316
|
2256,
|
|
316
317
|
3256,
|
|
317
318
|
4256,
|
|
318
319
|
],
|
|
319
|
-
switchMultiPower: [126, 165],
|
|
320
|
+
switchMultiPower: [126, 165, 262],
|
|
320
321
|
switchMate: [177],
|
|
321
322
|
switchMan: [174],
|
|
322
323
|
lightDimmer: [36, 44, 57],
|
|
323
324
|
lightRGB: [22],
|
|
324
325
|
lightCCT: [103],
|
|
325
326
|
lightRGBCCT: [33, 59, 104, 135, 136, 137, 173],
|
|
326
|
-
curtain: [11, 67],
|
|
327
|
+
curtain: [11, 67, 91, 258],
|
|
327
328
|
sensorContact: [102, 154],
|
|
328
329
|
sensorAmbient: [15, 181],
|
|
329
330
|
sensorTempHumi: [18],
|
|
330
331
|
thermostat: [127],
|
|
332
|
+
airConditioner: [151],
|
|
331
333
|
panel: [133, 195],
|
|
334
|
+
virtual: [265],
|
|
332
335
|
fan: [34],
|
|
333
336
|
humidifier: [19],
|
|
334
337
|
diffuser: [25],
|
|
@@ -344,7 +347,7 @@ export default {
|
|
|
344
347
|
zbSensorMotion: [2026, 7002],
|
|
345
348
|
zbSensorOccupancy: [7016],
|
|
346
349
|
zbSensorContact: [3026, 7003],
|
|
347
|
-
zbSensorWater: [4026],
|
|
350
|
+
zbSensorWater: [4026, 7019],
|
|
348
351
|
zbSensorSmoke: [5026],
|
|
349
352
|
zbThermostat: [7017],
|
|
350
353
|
zbWaterValve: [7027],
|
|
@@ -468,6 +471,7 @@ export default {
|
|
|
468
471
|
83: 3, // "GSM_SOCKET_3_NO_FLOW"
|
|
469
472
|
84: 4, // "GSM_SOCKET_4_NO_FLOW"
|
|
470
473
|
87: 1, // "EWELINK_IOT_CAMERA" (GK-200MP2B)
|
|
474
|
+
91: 1, // "ROLLING_DOOR" (Cover/Curtain device)
|
|
471
475
|
98: 1, // "DOORBELL_RFBRIDGE"
|
|
472
476
|
102: 1, // "DOOR_MAGNETIC" (OPL-DMA, DW2)
|
|
473
477
|
103: 1, // "WOTEWODE_TEM_LIGHT" (B02-F)
|
|
@@ -506,8 +510,11 @@ export default {
|
|
|
506
510
|
210: 2, // Sonoff Ultimate Switch T5-2C-86
|
|
507
511
|
211: 3, // Sonoff Ultimate Switch T5-3C-86
|
|
508
512
|
212: 4, // Sonoff Ultimate Switch T5-4C-86
|
|
513
|
+
225: 1, // CK-BL602-PCSW-01 (Single switch with power monitoring)
|
|
509
514
|
226: 1, // https://github.com/homebridge-plugins/homebridge-ewelink/issues/636
|
|
510
515
|
243: 1, // Zigbee Bridge Ultra
|
|
516
|
+
258: 1, // Additional Cover device
|
|
517
|
+
262: 4, // CK-BL602-SWP1-02 (4-channel switch with power monitoring)
|
|
511
518
|
1000: 1, // "zigbee_ON_OFF_SWITCH_1000" (button device)
|
|
512
519
|
1009: 1, // "" (Some sort of single switch device)
|
|
513
520
|
1256: 1, // "ZIGBEE_SINGLE_SWITCH"
|
|
@@ -535,7 +542,9 @@ export default {
|
|
|
535
542
|
7014: 1, // some sensor https://github.com/homebridge-plugins/homebridge-ewelink/issues/494
|
|
536
543
|
7016: 1, // https://sonoff.tech/product/gateway-and-sensors/snzb-06p/
|
|
537
544
|
7017: 1, // https://github.com/homebridge-plugins/homebridge-ewelink/issues/518
|
|
545
|
+
7019: 1, // Zigbee Water Sensor
|
|
538
546
|
7027: 1, // Zigbee Smart Water Valve
|
|
547
|
+
7032: 1, // Zigbee Power Monitoring Switch
|
|
539
548
|
},
|
|
540
549
|
|
|
541
550
|
cannotSupportDevices: {
|
|
@@ -582,7 +591,6 @@ export default {
|
|
|
582
591
|
88: 0, // "YK_INFRARED"
|
|
583
592
|
89: 0, // "SMART_OPEN_MACHINE"
|
|
584
593
|
90: 0, // "GSM_RFBridge"
|
|
585
|
-
91: 0, // "ROLLING_DOOR_91"
|
|
586
594
|
93: 0, // "HTHD_AIR_CLEANER"
|
|
587
595
|
94: 0, // "YIAN_ELECTRIC_PROTECT"
|
|
588
596
|
109: 0, // "YK_INFRARED_2"
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"displayName": "Homebridge eWeLink",
|
|
4
4
|
"alias": "eWeLink",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"version": "12.
|
|
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.
|
|
65
|
-
"axios": "^1.
|
|
64
|
+
"@homebridge/plugin-ui-utils": "^2.1.2",
|
|
65
|
+
"axios": "^1.13.2",
|
|
66
66
|
"node-persist": "^4.0.4",
|
|
67
|
-
"p-queue": "^
|
|
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": "^4.
|
|
72
|
+
"@antfu/eslint-config": "^6.4.2"
|
|
73
73
|
}
|
|
74
74
|
}
|