@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.
Files changed (78) hide show
  1. package/CHANGELOG.md +1937 -0
  2. package/LICENSE +21 -0
  3. package/README.md +72 -0
  4. package/config.schema.json +1727 -0
  5. package/eslint.config.js +49 -0
  6. package/lib/connection/aws.js +174 -0
  7. package/lib/connection/ble.js +208 -0
  8. package/lib/connection/cert/AmazonRootCA1.pem +20 -0
  9. package/lib/connection/http.js +240 -0
  10. package/lib/connection/lan.js +284 -0
  11. package/lib/device/cooler-single.js +300 -0
  12. package/lib/device/dehumidifier-H7150.js +182 -0
  13. package/lib/device/dehumidifier-H7151.js +157 -0
  14. package/lib/device/diffuser-H7161.js +117 -0
  15. package/lib/device/diffuser-H7162.js +117 -0
  16. package/lib/device/fan-H7100.js +274 -0
  17. package/lib/device/fan-H7101.js +330 -0
  18. package/lib/device/fan-H7102.js +274 -0
  19. package/lib/device/fan-H7105.js +503 -0
  20. package/lib/device/fan-H7106.js +274 -0
  21. package/lib/device/fan-H7111.js +335 -0
  22. package/lib/device/heater-single.js +300 -0
  23. package/lib/device/heater1a.js +353 -0
  24. package/lib/device/heater1b.js +616 -0
  25. package/lib/device/heater2.js +838 -0
  26. package/lib/device/humidifier-H7140.js +224 -0
  27. package/lib/device/humidifier-H7141.js +257 -0
  28. package/lib/device/humidifier-H7142.js +522 -0
  29. package/lib/device/humidifier-H7143.js +157 -0
  30. package/lib/device/humidifier-H7148.js +157 -0
  31. package/lib/device/humidifier-H7160.js +446 -0
  32. package/lib/device/ice-maker-H7162.js +46 -0
  33. package/lib/device/index.js +105 -0
  34. package/lib/device/kettle.js +269 -0
  35. package/lib/device/light-switch.js +86 -0
  36. package/lib/device/light.js +617 -0
  37. package/lib/device/outlet-double.js +121 -0
  38. package/lib/device/outlet-single.js +172 -0
  39. package/lib/device/outlet-triple.js +160 -0
  40. package/lib/device/purifier-H7120.js +336 -0
  41. package/lib/device/purifier-H7121.js +336 -0
  42. package/lib/device/purifier-H7122.js +449 -0
  43. package/lib/device/purifier-H7123.js +411 -0
  44. package/lib/device/purifier-H7124.js +411 -0
  45. package/lib/device/purifier-H7126.js +296 -0
  46. package/lib/device/purifier-H7127.js +296 -0
  47. package/lib/device/purifier-H712C.js +296 -0
  48. package/lib/device/purifier-single.js +119 -0
  49. package/lib/device/sensor-button.js +22 -0
  50. package/lib/device/sensor-contact.js +22 -0
  51. package/lib/device/sensor-leak.js +87 -0
  52. package/lib/device/sensor-monitor.js +190 -0
  53. package/lib/device/sensor-presence.js +53 -0
  54. package/lib/device/sensor-thermo.js +144 -0
  55. package/lib/device/sensor-thermo4.js +55 -0
  56. package/lib/device/switch-double.js +121 -0
  57. package/lib/device/switch-single.js +95 -0
  58. package/lib/device/switch-triple.js +160 -0
  59. package/lib/device/tap-single.js +108 -0
  60. package/lib/device/template.js +43 -0
  61. package/lib/device/tv-single.js +84 -0
  62. package/lib/device/valve-single.js +155 -0
  63. package/lib/fakegato/LICENSE +21 -0
  64. package/lib/fakegato/fakegato-history.js +814 -0
  65. package/lib/fakegato/fakegato-storage.js +108 -0
  66. package/lib/fakegato/fakegato-timer.js +125 -0
  67. package/lib/fakegato/uuid.js +27 -0
  68. package/lib/homebridge-ui/public/index.html +433 -0
  69. package/lib/homebridge-ui/server.js +10 -0
  70. package/lib/index.js +8 -0
  71. package/lib/platform.js +1967 -0
  72. package/lib/utils/colour.js +564 -0
  73. package/lib/utils/constants.js +579 -0
  74. package/lib/utils/custom-chars.js +225 -0
  75. package/lib/utils/eve-chars.js +68 -0
  76. package/lib/utils/functions.js +117 -0
  77. package/lib/utils/lang-en.js +131 -0
  78. 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
+ }