@homebridge-plugins/homebridge-govee 10.12.1
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 +1937 -0
- package/LICENSE +21 -0
- package/README.md +72 -0
- package/config.schema.json +1727 -0
- package/eslint.config.js +49 -0
- package/lib/connection/aws.js +174 -0
- package/lib/connection/ble.js +208 -0
- package/lib/connection/cert/AmazonRootCA1.pem +20 -0
- package/lib/connection/http.js +240 -0
- package/lib/connection/lan.js +284 -0
- package/lib/device/cooler-single.js +300 -0
- package/lib/device/dehumidifier-H7150.js +182 -0
- package/lib/device/dehumidifier-H7151.js +157 -0
- package/lib/device/diffuser-H7161.js +117 -0
- package/lib/device/diffuser-H7162.js +117 -0
- package/lib/device/fan-H7100.js +274 -0
- package/lib/device/fan-H7101.js +330 -0
- package/lib/device/fan-H7102.js +274 -0
- package/lib/device/fan-H7105.js +503 -0
- package/lib/device/fan-H7106.js +274 -0
- package/lib/device/fan-H7111.js +335 -0
- package/lib/device/heater-single.js +300 -0
- package/lib/device/heater1a.js +353 -0
- package/lib/device/heater1b.js +616 -0
- package/lib/device/heater2.js +838 -0
- package/lib/device/humidifier-H7140.js +224 -0
- package/lib/device/humidifier-H7141.js +257 -0
- package/lib/device/humidifier-H7142.js +522 -0
- package/lib/device/humidifier-H7143.js +157 -0
- package/lib/device/humidifier-H7148.js +157 -0
- package/lib/device/humidifier-H7160.js +446 -0
- package/lib/device/ice-maker-H7162.js +46 -0
- package/lib/device/index.js +105 -0
- package/lib/device/kettle.js +269 -0
- package/lib/device/light-switch.js +86 -0
- package/lib/device/light.js +617 -0
- package/lib/device/outlet-double.js +121 -0
- package/lib/device/outlet-single.js +172 -0
- package/lib/device/outlet-triple.js +160 -0
- package/lib/device/purifier-H7120.js +336 -0
- package/lib/device/purifier-H7121.js +336 -0
- package/lib/device/purifier-H7122.js +449 -0
- package/lib/device/purifier-H7123.js +411 -0
- package/lib/device/purifier-H7124.js +411 -0
- package/lib/device/purifier-H7126.js +296 -0
- package/lib/device/purifier-H7127.js +296 -0
- package/lib/device/purifier-H712C.js +296 -0
- package/lib/device/purifier-single.js +119 -0
- package/lib/device/sensor-button.js +22 -0
- package/lib/device/sensor-contact.js +22 -0
- package/lib/device/sensor-leak.js +87 -0
- package/lib/device/sensor-monitor.js +190 -0
- package/lib/device/sensor-presence.js +53 -0
- package/lib/device/sensor-thermo.js +144 -0
- package/lib/device/sensor-thermo4.js +55 -0
- package/lib/device/switch-double.js +121 -0
- package/lib/device/switch-single.js +95 -0
- package/lib/device/switch-triple.js +160 -0
- package/lib/device/tap-single.js +108 -0
- package/lib/device/template.js +43 -0
- package/lib/device/tv-single.js +84 -0
- package/lib/device/valve-single.js +155 -0
- package/lib/fakegato/LICENSE +21 -0
- package/lib/fakegato/fakegato-history.js +814 -0
- package/lib/fakegato/fakegato-storage.js +108 -0
- package/lib/fakegato/fakegato-timer.js +125 -0
- package/lib/fakegato/uuid.js +27 -0
- package/lib/homebridge-ui/public/index.html +433 -0
- package/lib/homebridge-ui/server.js +10 -0
- package/lib/index.js +8 -0
- package/lib/platform.js +1967 -0
- package/lib/utils/colour.js +564 -0
- package/lib/utils/constants.js +579 -0
- package/lib/utils/custom-chars.js +225 -0
- package/lib/utils/eve-chars.js +68 -0
- package/lib/utils/functions.js +117 -0
- package/lib/utils/lang-en.js +131 -0
- package/package.json +75 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import dgram from 'node:dgram'
|
|
2
|
+
|
|
3
|
+
import { parseError } from '../utils/functions.js'
|
|
4
|
+
import platformLang from '../utils/lang-en.js'
|
|
5
|
+
|
|
6
|
+
const commands = { scan: 'scan', deviceStatus: 'devStatus' }
|
|
7
|
+
const multicastIp = '239.255.255.250'
|
|
8
|
+
const scanCommandPort = 4001
|
|
9
|
+
const receiverPort = 4002
|
|
10
|
+
const devicePort = 4003
|
|
11
|
+
const getDevicesScanTimeoutMs = 2000
|
|
12
|
+
|
|
13
|
+
/*
|
|
14
|
+
This class handles LAN discovery and communication with Govee devices.
|
|
15
|
+
|
|
16
|
+
The documentation can be fount at https://app-h5.govee.com/user-manual/wlan-guide.
|
|
17
|
+
|
|
18
|
+
The connection is UDP based and uses multicast to discover devices on the network.
|
|
19
|
+
- The discovery flow is as follows:
|
|
20
|
+
┌──────┐ ┌───────────┐
|
|
21
|
+
│Client│ │GoveeDevice│
|
|
22
|
+
└──┬───┘ └─────┬─────┘
|
|
23
|
+
│ Request Scan │
|
|
24
|
+
│ ──────────────────>│
|
|
25
|
+
│ │ ╔═══════════════════════════════════════╗
|
|
26
|
+
│ │ ║Group address of 239.255.255.250:4001 ░║
|
|
27
|
+
│ │ ╚═══════════════════════════════════════╝
|
|
28
|
+
│ Response Scan │
|
|
29
|
+
│ <─ ─ ─ ─ ─ ─ ─ ─ ─ │
|
|
30
|
+
│ │
|
|
31
|
+
╔════════════════════════════════════╗│ │
|
|
32
|
+
║Response will be sent to port 4002 ░║│ │
|
|
33
|
+
╚════════════════════════════════════╝┴───┐ ┌─────┴─────┐
|
|
34
|
+
│Client│ │GoveeDevice│
|
|
35
|
+
└──────┘ └───────────┘
|
|
36
|
+
|
|
37
|
+
1. On devices that have `LAN control` turned on, the device will join the multicast address `239.255.255.250` and listen for information sent to port `4001` of the multicast.
|
|
38
|
+
2. The client (sender) will send a scan request to that group address, on port 4001.
|
|
39
|
+
3. Each Govee device will send to the server (receiver) on port `4002` a send response scan message.
|
|
40
|
+
|
|
41
|
+
After the discovery process, we keep track of the devices found and keep polling for new devices every 5 seconds.
|
|
42
|
+
|
|
43
|
+
- The communication flow is as follows:
|
|
44
|
+
┌──────┐ ┌───────────┐
|
|
45
|
+
│Client│ │GoveeDevice│
|
|
46
|
+
└──┬───┘ └─────┬─────┘
|
|
47
|
+
│ Control Command │
|
|
48
|
+
│ ──────────────────────────────────────────────>
|
|
49
|
+
│ │
|
|
50
|
+
│ │ ╔════════════════════════╗
|
|
51
|
+
│ │ ║Device IP on port 4003 ░║
|
|
52
|
+
│ │ ╚════════════════════════╝
|
|
53
|
+
│ Device Status (only for Device Status Command)│
|
|
54
|
+
│ <─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│
|
|
55
|
+
│ │
|
|
56
|
+
╔════════════════════════════════════╗│ │
|
|
57
|
+
║Response will be sent to port 4002 ░║│ │
|
|
58
|
+
╚════════════════════════════════════╝┴───┐ ┌─────┴─────┐
|
|
59
|
+
│Client│ │GoveeDevice│
|
|
60
|
+
└──────┘ └───────────┘
|
|
61
|
+
1. Once the IP of the device is known, we will send a control command to the device on port `4003`.
|
|
62
|
+
2. The device will respond with the device status only if Device Status Command is sent on port `4002`.
|
|
63
|
+
*/
|
|
64
|
+
export default class {
|
|
65
|
+
constructor(platform) {
|
|
66
|
+
this.log = platform.log
|
|
67
|
+
this.config = platform.config
|
|
68
|
+
|
|
69
|
+
// Keeps track of all devices that are found on the network
|
|
70
|
+
this.lanDevices = []
|
|
71
|
+
|
|
72
|
+
// Add any devices that have a custom IP address in the config
|
|
73
|
+
Object.keys(platform.deviceConf).forEach((device) => {
|
|
74
|
+
if (platform.deviceConf[device].customIPAddress) {
|
|
75
|
+
this.lanDevices.push({
|
|
76
|
+
ip: platform.deviceConf[device].customIPAddress,
|
|
77
|
+
device,
|
|
78
|
+
isPendingDiscovery: true,
|
|
79
|
+
isManual: true,
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
// Create a UDP socket to listen for messages sent by Govee devices in a multicast group
|
|
85
|
+
this.receiver = dgram.createSocket('udp4')
|
|
86
|
+
|
|
87
|
+
// Create a UDP socket to send messages to Govee devices from host
|
|
88
|
+
this.sender = dgram.createSocket('udp4')
|
|
89
|
+
|
|
90
|
+
this.latestDeviceScanTimestamp = Date.now()
|
|
91
|
+
|
|
92
|
+
this.connectionPromise = new Promise((resolve, reject) => {
|
|
93
|
+
// Handle messages received
|
|
94
|
+
this.receiver.on('message', (msg, rinfo) => {
|
|
95
|
+
const strMessage = msg.toString()
|
|
96
|
+
try {
|
|
97
|
+
const message = JSON.parse(strMessage)
|
|
98
|
+
const command = message.msg.cmd
|
|
99
|
+
|
|
100
|
+
switch (command) {
|
|
101
|
+
case commands.scan: {
|
|
102
|
+
// Handle scan responses sent by devices registered in the multicast group
|
|
103
|
+
this.latestDeviceScanTimestamp = Date.now()
|
|
104
|
+
const deviceData = message.msg.data
|
|
105
|
+
|
|
106
|
+
const existingIndex = this.lanDevices.findIndex(value => value.device === deviceData.device)
|
|
107
|
+
|
|
108
|
+
if (existingIndex === -1) {
|
|
109
|
+
this.log.debug(
|
|
110
|
+
'[LAN] %s [isNew=true,isManual=false] [%s] [%s].',
|
|
111
|
+
platformLang.lanFoundDevice,
|
|
112
|
+
strMessage,
|
|
113
|
+
JSON.stringify(rinfo),
|
|
114
|
+
)
|
|
115
|
+
this.lanDevices.push(deviceData)
|
|
116
|
+
|
|
117
|
+
platform.receiveUpdateLAN(deviceData.device, {}, deviceData.ip)
|
|
118
|
+
} else if (this.lanDevices[existingIndex].isPendingDiscovery) {
|
|
119
|
+
// This device was added in the constructor as a manual device
|
|
120
|
+
// Now we have discovered this device, replace the manual info with the received info
|
|
121
|
+
this.lanDevices[existingIndex] = {
|
|
122
|
+
...deviceData,
|
|
123
|
+
isManual: true,
|
|
124
|
+
}
|
|
125
|
+
this.log.debug(
|
|
126
|
+
'[LAN] %s [isNew=true,isManual=true] [%s] [%s].',
|
|
127
|
+
platformLang.lanFoundDevice,
|
|
128
|
+
strMessage,
|
|
129
|
+
JSON.stringify(rinfo),
|
|
130
|
+
)
|
|
131
|
+
platform.receiveUpdateLAN(deviceData.device, {}, deviceData.ip)
|
|
132
|
+
} else {
|
|
133
|
+
this.log.debug(
|
|
134
|
+
'[LAN] %s [isNew=false] [%s] [%s].',
|
|
135
|
+
platformLang.lanFoundDevice,
|
|
136
|
+
strMessage,
|
|
137
|
+
JSON.stringify(rinfo),
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
break
|
|
141
|
+
}
|
|
142
|
+
case commands.deviceStatus: {
|
|
143
|
+
// Handle device status responses sent by devices
|
|
144
|
+
const deviceAddress = rinfo.address
|
|
145
|
+
|
|
146
|
+
const foundDeviceId = this.lanDevices.find(value => value.ip === deviceAddress)
|
|
147
|
+
|
|
148
|
+
if (foundDeviceId) {
|
|
149
|
+
// Send the update to the receiver function
|
|
150
|
+
platform.receiveUpdateLAN(foundDeviceId.device, message.msg.data, deviceAddress)
|
|
151
|
+
} else {
|
|
152
|
+
this.log.warn('[LAN] %s [%s].', platformLang.lanUnkDevice, deviceAddress)
|
|
153
|
+
}
|
|
154
|
+
break
|
|
155
|
+
}
|
|
156
|
+
default:
|
|
157
|
+
break
|
|
158
|
+
}
|
|
159
|
+
} catch (err) {
|
|
160
|
+
this.log('[LAN] %s [%s] [%s].', platformLang.lanParseError, strMessage, parseError(err))
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
// Handle errors
|
|
165
|
+
this.receiver.on('error', (err) => {
|
|
166
|
+
this.log.warn('[LAN] server error: %s.', parseError(err))
|
|
167
|
+
reject(err)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
// Handle started listening for messages
|
|
171
|
+
this.receiver.on('listening', () => {
|
|
172
|
+
const { address, port } = this.receiver.address()
|
|
173
|
+
this.log.debug('[LAN] %s %s:%s.', platformLang.lanServerStarted, address, port)
|
|
174
|
+
resolve()
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
this.receiver.bind(receiverPort, () => {
|
|
178
|
+
this.receiver.addMembership(multicastIp, '0.0.0.0')
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
this.sender.bind()
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Send a request that asks for all devices in the multicast group
|
|
186
|
+
sendScanCommand() {
|
|
187
|
+
const scanCommand = JSON.stringify({ msg: { cmd: commands.scan, data: { account_topic: 'reserve' } } })
|
|
188
|
+
this.log.debug('[LAN] scanning for devices over LAN...')
|
|
189
|
+
this.sender.send(scanCommand, scanCommandPort, multicastIp)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Get all available LAN devices
|
|
193
|
+
async getDevices() {
|
|
194
|
+
return new Promise((resolve) => {
|
|
195
|
+
this.connectionPromise.then(() => {
|
|
196
|
+
this.sendScanCommand()
|
|
197
|
+
|
|
198
|
+
// Since there is no list of devices to be gathered, we will send a scan request and wait until
|
|
199
|
+
// there are no more devices announcing themselves in the multicast group.
|
|
200
|
+
const checkPeriod = setInterval(() => {
|
|
201
|
+
const diff = Date.now() - this.latestDeviceScanTimestamp
|
|
202
|
+
if (diff >= getDevicesScanTimeoutMs) {
|
|
203
|
+
clearInterval(checkPeriod)
|
|
204
|
+
resolve(this.lanDevices)
|
|
205
|
+
}
|
|
206
|
+
}, 100)
|
|
207
|
+
}, () => {
|
|
208
|
+
resolve([])
|
|
209
|
+
})
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Send a request to a device to ask for its current state
|
|
214
|
+
async sendDeviceStateRequest(device) {
|
|
215
|
+
const stateCommand = JSON.stringify({ msg: { cmd: commands.deviceStatus, data: {} } })
|
|
216
|
+
return new Promise((resolve, reject) => {
|
|
217
|
+
this.sender.send(stateCommand, devicePort, device.ip, (err) => {
|
|
218
|
+
if (err) {
|
|
219
|
+
reject(err)
|
|
220
|
+
}
|
|
221
|
+
resolve()
|
|
222
|
+
})
|
|
223
|
+
})
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// This is called by the platform on sending a device update via LAN
|
|
227
|
+
async updateDevice(accessory, params) {
|
|
228
|
+
const updatedParams = { msg: params }
|
|
229
|
+
|
|
230
|
+
accessory.logDebug(`[LAN] ${platformLang.sendingUpdate} [${JSON.stringify(updatedParams)}]`)
|
|
231
|
+
|
|
232
|
+
const foundDeviceId = this.lanDevices.findIndex(value => value.device === accessory.context.gvDeviceId)
|
|
233
|
+
|
|
234
|
+
if (foundDeviceId === -1) {
|
|
235
|
+
throw new Error(platformLang.lanDevNotFound)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const foundDevice = this.lanDevices[foundDeviceId]
|
|
239
|
+
|
|
240
|
+
return new Promise((resolve, reject) => {
|
|
241
|
+
const command = JSON.stringify(updatedParams)
|
|
242
|
+
|
|
243
|
+
this.sender.send(command, devicePort, foundDevice.ip, async (err) => {
|
|
244
|
+
if (err) {
|
|
245
|
+
// We can assume the device is offline or not available anymore, so we will remove it from the devices list
|
|
246
|
+
// We should only do this if it is not a device that the user has configured manually with an IP
|
|
247
|
+
if (!foundDevice.isManual) {
|
|
248
|
+
this.lanDevices.splice(foundDeviceId, 1)
|
|
249
|
+
accessory.logDebugWarn(`[LAN] ${platformLang.lanDevRemoved}`)
|
|
250
|
+
}
|
|
251
|
+
reject(err)
|
|
252
|
+
} else {
|
|
253
|
+
accessory.logDebug(`[LAN] ${platformLang.lanCmdSent} ${foundDevice.ip}`)
|
|
254
|
+
resolve()
|
|
255
|
+
}
|
|
256
|
+
})
|
|
257
|
+
})
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
startDevicesPolling() {
|
|
261
|
+
this.devicesPolling = setInterval(() => {
|
|
262
|
+
this.sendScanCommand()
|
|
263
|
+
}, this.config.lanScanInterval * 1000)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
startStatusPolling() {
|
|
267
|
+
this.statusPolling = setInterval(async () => {
|
|
268
|
+
this.lanDevices.forEach(async (device) => {
|
|
269
|
+
try {
|
|
270
|
+
await this.sendDeviceStateRequest(device)
|
|
271
|
+
} catch (err) {
|
|
272
|
+
this.log.warn('[%s] [LAN] %s %s.', device.device, platformLang.lanReqError, parseError(err))
|
|
273
|
+
}
|
|
274
|
+
})
|
|
275
|
+
}, this.config.lanRefreshTime * 1000)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
close() {
|
|
279
|
+
clearInterval(this.devicesPolling)
|
|
280
|
+
clearInterval(this.statusPolling)
|
|
281
|
+
this.receiver.close()
|
|
282
|
+
this.sender.close()
|
|
283
|
+
}
|
|
284
|
+
}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { generateRandomString, hasProperty, 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
|
+
this.temperatureSource = accessory.context.temperatureSource;
|
|
15
|
+
|
|
16
|
+
// Remove any old services from simulations
|
|
17
|
+
['AirPurifier', 'Lightbulb', 'Outlet', 'Switch', 'Valve'].forEach((service) => {
|
|
18
|
+
if (this.accessory.getService(this.hapServ[service])) {
|
|
19
|
+
this.accessory.removeService(this.accessory.getService(this.hapServ[service]))
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
// Set up the accessory with default target temp when added the first time
|
|
24
|
+
if (!hasProperty(this.accessory.context, 'cacheTarget')) {
|
|
25
|
+
this.accessory.context.cacheTarget = 20
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Check to make sure user has not switched from cooler to heater
|
|
29
|
+
if (this.accessory.context.cacheType !== 'cooler') {
|
|
30
|
+
// Remove and re-setup as a HeaterCooler
|
|
31
|
+
if (this.accessory.getService(this.hapServ.HeaterCooler)) {
|
|
32
|
+
this.accessory.removeService(this.accessory.getService(this.hapServ.HeaterCooler))
|
|
33
|
+
}
|
|
34
|
+
this.accessory.context.cacheType = 'cooler'
|
|
35
|
+
this.accessory.context.cacheTarget = 20
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Add the heater service if it doesn't already exist
|
|
39
|
+
this.service = this.accessory.getService(this.hapServ.HeaterCooler)
|
|
40
|
+
|| this.accessory.addService(this.hapServ.HeaterCooler)
|
|
41
|
+
|
|
42
|
+
// Set custom properties of the current temperature characteristic
|
|
43
|
+
this.service.getCharacteristic(this.hapChar.CurrentTemperature).setProps({
|
|
44
|
+
minStep: 0.1,
|
|
45
|
+
})
|
|
46
|
+
this.cacheTemp = this.service.getCharacteristic(this.hapChar.CurrentTemperature).value
|
|
47
|
+
|
|
48
|
+
// Add the set handler to the heater active characteristic
|
|
49
|
+
this.service
|
|
50
|
+
.getCharacteristic(this.hapChar.Active)
|
|
51
|
+
.onSet(async value => this.internalStateUpdate(value))
|
|
52
|
+
|
|
53
|
+
// Add options to the target state characteristic
|
|
54
|
+
this.service.getCharacteristic(this.hapChar.TargetHeaterCoolerState).setProps({
|
|
55
|
+
minValue: 0,
|
|
56
|
+
maxValue: 0,
|
|
57
|
+
validValues: [0],
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
// Add the set handler to the target temperature characteristic
|
|
61
|
+
this.service
|
|
62
|
+
.getCharacteristic(this.hapChar.CoolingThresholdTemperature)
|
|
63
|
+
.updateValue(this.accessory.context.cacheTarget)
|
|
64
|
+
.setProps({ minStep: 0.5 })
|
|
65
|
+
.onSet(async value => this.internalTargetTempUpdate(value))
|
|
66
|
+
|
|
67
|
+
// Initialise these caches now since they aren't determined by the initial externalUpdate()
|
|
68
|
+
this.cacheState = this.service.getCharacteristic(this.hapChar.Active).value === 1 ? 'on' : 'off'
|
|
69
|
+
this.cacheCool = this.cacheState === 'on'
|
|
70
|
+
&& this.service.getCharacteristic(this.hapChar.CurrentHeaterCoolerState).value === 3
|
|
71
|
+
? 'on'
|
|
72
|
+
: 'off'
|
|
73
|
+
|
|
74
|
+
// Pass the accessory to Fakegato to set up with Eve
|
|
75
|
+
this.accessory.eveService = new platform.eveService('custom', this.accessory, {
|
|
76
|
+
log: () => {},
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
// Set up an interval to get regular temperature updates
|
|
80
|
+
setTimeout(() => {
|
|
81
|
+
this.getTemperature()
|
|
82
|
+
this.intervalPoll = setInterval(() => this.getTemperature(), 120000)
|
|
83
|
+
}, 5000)
|
|
84
|
+
|
|
85
|
+
// Stop the intervals on Homebridge shutdown
|
|
86
|
+
platform.api.on('shutdown', () => {
|
|
87
|
+
clearInterval(this.intervalPoll)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// Output the customised options to the log
|
|
91
|
+
const opts = JSON.stringify({
|
|
92
|
+
showAs: 'cooler',
|
|
93
|
+
temperatureSource: this.temperatureSource,
|
|
94
|
+
})
|
|
95
|
+
platform.log('[%s] %s %s.', accessory.displayName, platformLang.devInitOpts, opts)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async internalStateUpdate(value) {
|
|
99
|
+
try {
|
|
100
|
+
let newState
|
|
101
|
+
let newCool
|
|
102
|
+
let newValue
|
|
103
|
+
if (value === 0) {
|
|
104
|
+
newValue = 'off'
|
|
105
|
+
newState = 'off'
|
|
106
|
+
newCool = 'off'
|
|
107
|
+
} else if (this.cacheTemp > this.accessory.context.cacheTarget) {
|
|
108
|
+
newValue = 'on'
|
|
109
|
+
newState = 'on'
|
|
110
|
+
newCool = 'on'
|
|
111
|
+
} else {
|
|
112
|
+
newValue = 'off'
|
|
113
|
+
newState = 'on'
|
|
114
|
+
newCool = 'off'
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Only send the update if either:
|
|
118
|
+
// * The new value (state) is OFF and the cacheCool was ON
|
|
119
|
+
// * The new value (state) is ON and newCool is 'on'
|
|
120
|
+
if ((value === 0 && this.cacheCool === 'on') || (value === 1 && newCool === 'on')) {
|
|
121
|
+
// Set up a one-minute timeout for the plugin to ignore incoming updates
|
|
122
|
+
const timerKey = generateRandomString(5)
|
|
123
|
+
this.updateTimeout = timerKey
|
|
124
|
+
setTimeout(() => {
|
|
125
|
+
if (this.updateTimeout === timerKey) {
|
|
126
|
+
this.updateTimeout = false
|
|
127
|
+
}
|
|
128
|
+
}, 60000)
|
|
129
|
+
|
|
130
|
+
// Send the request to the platform sender function
|
|
131
|
+
await this.platform.sendDeviceUpdate(this.accessory, {
|
|
132
|
+
cmd: 'stateOutlet',
|
|
133
|
+
value: newValue,
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Cache and log
|
|
138
|
+
if (newState !== this.cacheState) {
|
|
139
|
+
this.cacheState = newState
|
|
140
|
+
this.accessory.log(`${platformLang.curState} [${this.cacheState}]`)
|
|
141
|
+
}
|
|
142
|
+
if (newCool !== this.cacheCool) {
|
|
143
|
+
this.cacheCool = newCool
|
|
144
|
+
this.accessory.log(`${platformLang.curCool} [${this.cacheCool}]`)
|
|
145
|
+
}
|
|
146
|
+
const newOnState = this.cacheCool === 'on' ? 3 : 1
|
|
147
|
+
this.service.updateCharacteristic(
|
|
148
|
+
this.hapChar.CurrentHeaterCoolerState,
|
|
149
|
+
value === 1 ? newOnState : 0,
|
|
150
|
+
)
|
|
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.Active, this.cacheState === 'on' ? 1 : 0)
|
|
158
|
+
}, 2000)
|
|
159
|
+
throw new this.hapErr(-70402)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async internalTargetTempUpdate(value) {
|
|
164
|
+
try {
|
|
165
|
+
// Don't continue if the new value is the same as before
|
|
166
|
+
if (value === this.accessory.context.cacheTarget) {
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
this.accessory.context.cacheTarget = value
|
|
170
|
+
this.accessory.log(`${platformLang.curTarg} [${value}°C]`)
|
|
171
|
+
if (this.cacheState === 'off') {
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Check to see if we need to turn on or off
|
|
176
|
+
let newValue
|
|
177
|
+
let newCool
|
|
178
|
+
if (this.cacheTemp > value) {
|
|
179
|
+
newValue = 'on'
|
|
180
|
+
newCool = 'on'
|
|
181
|
+
} else {
|
|
182
|
+
newValue = 'off'
|
|
183
|
+
newCool = 'off'
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Don't continue if no change needed to device state
|
|
187
|
+
if (newCool === this.cacheCool) {
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Set up a one-minute timeout for the plugin to ignore incoming updates
|
|
192
|
+
const timerKey = generateRandomString(5)
|
|
193
|
+
this.updateTimeout = timerKey
|
|
194
|
+
setTimeout(() => {
|
|
195
|
+
if (this.updateTimeout === timerKey) {
|
|
196
|
+
this.updateTimeout = false
|
|
197
|
+
}
|
|
198
|
+
}, 60000)
|
|
199
|
+
|
|
200
|
+
// Send the request to the platform sender function
|
|
201
|
+
await this.platform.sendDeviceUpdate(this.accessory, {
|
|
202
|
+
cmd: 'stateOutlet',
|
|
203
|
+
value: newValue,
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
// Cache and log
|
|
207
|
+
this.cacheCool = newCool
|
|
208
|
+
this.accessory.log(`${platformLang.curCool} [${this.cacheCool}]`)
|
|
209
|
+
this.service.updateCharacteristic(
|
|
210
|
+
this.hapChar.CurrentHeaterCoolerState,
|
|
211
|
+
this.cacheCool === 'on' ? 3 : 1,
|
|
212
|
+
)
|
|
213
|
+
} catch (err) {
|
|
214
|
+
// Catch any errors during the process
|
|
215
|
+
this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`)
|
|
216
|
+
|
|
217
|
+
// Throw a 'no response' error and set a timeout to revert this after 2 seconds
|
|
218
|
+
setTimeout(() => {
|
|
219
|
+
this.service.updateCharacteristic(
|
|
220
|
+
this.hapChar.CoolingThresholdTemperature,
|
|
221
|
+
this.accessory.context.cacheTarget,
|
|
222
|
+
)
|
|
223
|
+
}, 2000)
|
|
224
|
+
throw new this.hapErr(-70402)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async internalCurrentTempUpdate() {
|
|
229
|
+
try {
|
|
230
|
+
// Don't continue if the device is off
|
|
231
|
+
if (this.cacheState === 'off') {
|
|
232
|
+
return
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Check to see if we need to turn on or off
|
|
236
|
+
let newValue
|
|
237
|
+
let newCool
|
|
238
|
+
if (this.cacheTemp > this.accessory.context.cacheTarget) {
|
|
239
|
+
newValue = 'on'
|
|
240
|
+
newCool = 'on'
|
|
241
|
+
} else {
|
|
242
|
+
newValue = 'off'
|
|
243
|
+
newCool = 'off'
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Don't continue if no change needed to device state
|
|
247
|
+
if (newCool === this.cacheCool) {
|
|
248
|
+
return
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Set up a one-minute timeout for the plugin to ignore incoming updates
|
|
252
|
+
const timerKey = generateRandomString(5)
|
|
253
|
+
this.updateTimeout = timerKey
|
|
254
|
+
setTimeout(() => {
|
|
255
|
+
if (this.updateTimeout === timerKey) {
|
|
256
|
+
this.updateTimeout = false
|
|
257
|
+
}
|
|
258
|
+
}, 60000)
|
|
259
|
+
|
|
260
|
+
// Send the request to the platform sender function
|
|
261
|
+
await this.platform.sendDeviceUpdate(this.accessory, {
|
|
262
|
+
cmd: 'stateOutlet',
|
|
263
|
+
value: newValue,
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
// Log and cache
|
|
267
|
+
this.cacheCool = newCool
|
|
268
|
+
this.accessory.log(`${platformLang.curCool} [${this.cacheCool}]`)
|
|
269
|
+
this.service.updateCharacteristic(
|
|
270
|
+
this.hapChar.CurrentHeaterCoolerState,
|
|
271
|
+
this.cacheCool === 'on' ? 3 : 1,
|
|
272
|
+
)
|
|
273
|
+
} catch (err) {
|
|
274
|
+
// Catch any errors during the process
|
|
275
|
+
this.accessory.logWarn(parseError(err))
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async getTemperature() {
|
|
280
|
+
try {
|
|
281
|
+
// Skip polling if the storage hasn't initialised properly
|
|
282
|
+
if (!this.platform.storageClientData) {
|
|
283
|
+
return
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const newTemp = await this.platform.storageData.getItem(`${this.temperatureSource}_temp`)
|
|
287
|
+
if (newTemp && newTemp !== this.cacheTemp) {
|
|
288
|
+
this.cacheTemp = newTemp
|
|
289
|
+
this.service.updateCharacteristic(this.hapChar.CurrentTemperature, this.cacheTemp)
|
|
290
|
+
this.accessory.eveService.addEntry({ temp: this.cacheTemp })
|
|
291
|
+
this.accessory.log(`${platformLang.curTemp} [${this.cacheTemp}°C]`)
|
|
292
|
+
await this.internalCurrentTempUpdate()
|
|
293
|
+
}
|
|
294
|
+
} catch (err) {
|
|
295
|
+
this.accessory.logWarn(parseError(err))
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
externalUpdate() {}
|
|
300
|
+
}
|