@switchbot/homebridge-switchbot 4.3.7 → 4.3.9

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.
@@ -0,0 +1,561 @@
1
+ /* Copyright(C) 2021-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved.
2
+ *
3
+ * airpurifier.ts: @switchbot/homebridge-switchbot.
4
+ */
5
+ import type { CharacteristicValue, PlatformAccessory, Service } from 'homebridge'
6
+ import type { airPurifierServiceData, airPurifierStatus, bodyChange, device, SwitchbotDevice } from 'node-switchbot'
7
+ /*
8
+ * For Testing Locally:
9
+ * import { SwitchBotBLEModel, SwitchBotBLEModelName } from '/Users/Shared/GitHub/OpenWonderLabs/node-switchbot/dist/index.js';
10
+ */
11
+ import { SwitchBotBLEModel, SwitchBotBLEModelName } from 'node-switchbot'
12
+ import { debounceTime, interval, skipWhile, Subject, take, tap } from 'rxjs'
13
+
14
+ import type { SwitchBotPlatform } from '../platform.js'
15
+ import type { devicesConfig } from '../settings.js'
16
+ import { formatDeviceIdAsMac } from '../utils.js'
17
+ import { deviceBase } from './device.js'
18
+
19
+ export class AirPurifier extends deviceBase {
20
+ // Services
21
+ private AirPurifier: {
22
+ Name: CharacteristicValue
23
+ Service: Service
24
+ Active: CharacteristicValue
25
+ CurrentAirPurifierState: CharacteristicValue
26
+ TargetAirPurifierState: CharacteristicValue
27
+ RotationSpeed: CharacteristicValue
28
+ LockPhysicalControls: CharacteristicValue
29
+ }
30
+
31
+ private AirQualitySensor?: {
32
+ Name: CharacteristicValue
33
+ Service: Service
34
+ AirQuality: CharacteristicValue
35
+ PM2_5Density: CharacteristicValue
36
+ }
37
+
38
+ // OpenAPI
39
+ deviceStatus!: airPurifierStatus
40
+
41
+ // BLE
42
+ serviceData!: airPurifierServiceData
43
+
44
+ // Updates
45
+ airPurifierUpdateInProgress!: boolean
46
+ doAirPurifierUpdate!: Subject<void>
47
+
48
+ constructor(
49
+ readonly platform: SwitchBotPlatform,
50
+ accessory: PlatformAccessory,
51
+ device: device & devicesConfig,
52
+ ) {
53
+ super(platform, accessory, device)
54
+ // Set category
55
+ accessory.category = this.hap.Categories.AIR_PURIFIER
56
+
57
+ // this is subject we use to track when we need to POST changes to the SwitchBot API
58
+ this.doAirPurifierUpdate = new Subject()
59
+ this.airPurifierUpdateInProgress = false
60
+
61
+ // Initialize AirPurifier Service
62
+ accessory.context.AirPurifier = accessory.context.AirPurifier ?? {}
63
+ this.AirPurifier = {
64
+ Name: accessory.displayName,
65
+ Service: accessory.getService(this.hap.Service.AirPurifier) ?? accessory.addService(this.hap.Service.AirPurifier) as Service,
66
+ Active: accessory.context.Active ?? this.hap.Characteristic.Active.INACTIVE,
67
+ CurrentAirPurifierState: accessory.context.CurrentAirPurifierState ?? this.hap.Characteristic.CurrentAirPurifierState.INACTIVE,
68
+ TargetAirPurifierState: accessory.context.TargetAirPurifierState ?? this.hap.Characteristic.TargetAirPurifierState.AUTO,
69
+ RotationSpeed: accessory.context.RotationSpeed ?? 0,
70
+ LockPhysicalControls: accessory.context.LockPhysicalControls ?? this.hap.Characteristic.LockPhysicalControls.CONTROL_LOCK_DISABLED,
71
+ }
72
+ accessory.context.AirPurifier = this.AirPurifier as object
73
+
74
+ // Initialize AirPurifier Service Characteristics
75
+ this.AirPurifier.Service.setCharacteristic(this.hap.Characteristic.Name, this.AirPurifier.Name).getCharacteristic(this.hap.Characteristic.Active).onGet(() => {
76
+ return this.AirPurifier.Active
77
+ }).onSet(this.ActiveSet.bind(this))
78
+
79
+ // Initialize AirPurifier RotationSpeed Characteristic
80
+ this.AirPurifier.Service.getCharacteristic(this.hap.Characteristic.RotationSpeed).onGet(() => {
81
+ return this.AirPurifier.RotationSpeed
82
+ }).onSet(this.RotationSpeedSet.bind(this))
83
+
84
+ // Initialize AirPurifier CurrentAirPurifierState Characteristic
85
+ this.AirPurifier.Service.getCharacteristic(this.hap.Characteristic.CurrentAirPurifierState).onGet(() => {
86
+ return this.AirPurifier.CurrentAirPurifierState
87
+ })
88
+
89
+ // Initialize AirPurifier TargetAirPurifierState Characteristic
90
+ this.AirPurifier.Service.getCharacteristic(this.hap.Characteristic.TargetAirPurifierState).onGet(() => {
91
+ return this.AirPurifier.TargetAirPurifierState
92
+ }).onSet(this.TargetAirPurifierStateSet.bind(this))
93
+
94
+ // Initialize AirPurifier LockPhysicalControls Characteristic
95
+ this.AirPurifier.Service.getCharacteristic(this.hap.Characteristic.LockPhysicalControls).onGet(() => {
96
+ return this.AirPurifier.LockPhysicalControls
97
+ })
98
+
99
+ // Initialize AirQualitySensor Service (optional, based on device capabilities)
100
+ accessory.context.AirQualitySensor = accessory.context.AirQualitySensor ?? {}
101
+ this.AirQualitySensor = {
102
+ Name: `${accessory.displayName} Air Quality`,
103
+ Service: accessory.getService(this.hap.Service.AirQualitySensor) ?? accessory.addService(this.hap.Service.AirQualitySensor) as Service,
104
+ AirQuality: accessory.context.AirQuality ?? this.hap.Characteristic.AirQuality.UNKNOWN,
105
+ PM2_5Density: accessory.context.PM2_5Density ?? 0,
106
+ }
107
+ accessory.context.AirQualitySensor = this.AirQualitySensor as object
108
+
109
+ // Initialize AirQualitySensor Characteristics
110
+ this.AirQualitySensor.Service.setCharacteristic(this.hap.Characteristic.Name, this.AirQualitySensor.Name).getCharacteristic(this.hap.Characteristic.AirQuality).onGet(() => {
111
+ return this.AirQualitySensor!.AirQuality
112
+ })
113
+
114
+ this.AirQualitySensor.Service.getCharacteristic(this.hap.Characteristic.PM2_5Density).onGet(() => {
115
+ return this.AirQualitySensor!.PM2_5Density
116
+ })
117
+
118
+ // Retrieve initial values and updateHomekit
119
+ try {
120
+ this.debugLog('Retrieve initial values and update Homekit')
121
+ this.refreshStatus()
122
+ } catch (e: any) {
123
+ this.errorLog(`failed to retrieve initial values and update Homekit, Error: ${e.message ?? e}`)
124
+ }
125
+
126
+ // register webhook event handler if enabled
127
+ try {
128
+ this.debugLog('Registering Webhook Event Handler')
129
+ this.registerWebhook()
130
+ } catch (e: any) {
131
+ this.errorLog(`failed to registerWebhook, Error: ${e.message ?? e}`)
132
+ }
133
+
134
+ // register platform BLE event handler if enabled
135
+ try {
136
+ this.debugLog('Registering Platform BLE Event Handler')
137
+ this.registerPlatformBLE()
138
+ } catch (e: any) {
139
+ this.errorLog(`failed to registerPlatformBLE, Error: ${e.message ?? e}`)
140
+ }
141
+
142
+ // Start an update interval
143
+ interval(this.deviceRefreshRate * 1000)
144
+ .pipe(skipWhile(() => this.airPurifierUpdateInProgress))
145
+ .subscribe(async () => {
146
+ await this.refreshStatus()
147
+ })
148
+
149
+ // Watch for Air Purifier change events
150
+ // We put in a debounce of 100ms so we don't make duplicate calls
151
+ this.doAirPurifierUpdate
152
+ .pipe(
153
+ tap(() => {
154
+ this.airPurifierUpdateInProgress = true
155
+ }),
156
+ debounceTime(this.devicePushRate * 1000),
157
+ )
158
+ .subscribe(async () => {
159
+ try {
160
+ await this.pushChanges()
161
+ } catch (e: any) {
162
+ await this.apiError(e)
163
+ this.errorLog(`failed pushChanges with ${this.device.connectionType} Connection, Error Message: ${JSON.stringify(e.message)}`)
164
+ }
165
+ this.airPurifierUpdateInProgress = false
166
+ })
167
+ }
168
+
169
+ async BLEparseStatus(): Promise<void> {
170
+ this.debugLog('BLEparseStatus')
171
+ this.debugLog(`(isOn, mode, child_lock, speed) = BLE:(${this.serviceData.isOn}, ${this.serviceData.mode}, ${this.serviceData.child_lock}, ${this.serviceData.speed}), current:(${this.AirPurifier.Active}, ${this.AirPurifier.TargetAirPurifierState}, ${this.AirPurifier.LockPhysicalControls}, ${this.AirPurifier.RotationSpeed})`)
172
+
173
+ // Active
174
+ this.AirPurifier.Active = this.serviceData.isOn ? this.hap.Characteristic.Active.ACTIVE : this.hap.Characteristic.Active.INACTIVE
175
+ this.debugLog(`Active: ${this.AirPurifier.Active}`)
176
+
177
+ // CurrentAirPurifierState
178
+ if (this.serviceData.isOn) {
179
+ this.AirPurifier.CurrentAirPurifierState = this.hap.Characteristic.CurrentAirPurifierState.PURIFYING_AIR
180
+ } else {
181
+ this.AirPurifier.CurrentAirPurifierState = this.hap.Characteristic.CurrentAirPurifierState.INACTIVE
182
+ }
183
+ this.debugLog(`CurrentAirPurifierState: ${this.AirPurifier.CurrentAirPurifierState}`)
184
+
185
+ // TargetAirPurifierState (mode: 0 = auto, 1 = manual/low, 2 = medium, 3 = high)
186
+ if (this.serviceData.mode === 'auto' || this.serviceData.mode === null) {
187
+ this.AirPurifier.TargetAirPurifierState = this.hap.Characteristic.TargetAirPurifierState.AUTO
188
+ } else {
189
+ this.AirPurifier.TargetAirPurifierState = this.hap.Characteristic.TargetAirPurifierState.MANUAL
190
+ }
191
+ this.debugLog(`TargetAirPurifierState: ${this.AirPurifier.TargetAirPurifierState}`)
192
+
193
+ // RotationSpeed
194
+ this.AirPurifier.RotationSpeed = this.serviceData.speed
195
+ this.debugLog(`RotationSpeed: ${this.AirPurifier.RotationSpeed}`)
196
+
197
+ // LockPhysicalControls
198
+ this.AirPurifier.LockPhysicalControls = this.serviceData.child_lock
199
+ ? this.hap.Characteristic.LockPhysicalControls.CONTROL_LOCK_ENABLED
200
+ : this.hap.Characteristic.LockPhysicalControls.CONTROL_LOCK_DISABLED
201
+ this.debugLog(`LockPhysicalControls: ${this.AirPurifier.LockPhysicalControls}`)
202
+
203
+ // AirQuality (if sensor is enabled)
204
+ if (this.AirQualitySensor && this.serviceData.isAqiValid) {
205
+ const aqiLevel = this.serviceData.aqi_level
206
+ if (aqiLevel === 'excellent') {
207
+ this.AirQualitySensor.AirQuality = this.hap.Characteristic.AirQuality.EXCELLENT
208
+ } else if (aqiLevel === 'good') {
209
+ this.AirQualitySensor.AirQuality = this.hap.Characteristic.AirQuality.GOOD
210
+ } else if (aqiLevel === 'fair') {
211
+ this.AirQualitySensor.AirQuality = this.hap.Characteristic.AirQuality.FAIR
212
+ } else if (aqiLevel === 'poor') {
213
+ this.AirQualitySensor.AirQuality = this.hap.Characteristic.AirQuality.INFERIOR
214
+ } else {
215
+ this.AirQualitySensor.AirQuality = this.hap.Characteristic.AirQuality.POOR
216
+ }
217
+ this.debugLog(`AirQuality: ${this.AirQualitySensor.AirQuality}`)
218
+ }
219
+ }
220
+
221
+ async openAPIparseStatus() {
222
+ this.debugLog('openAPIparseStatus')
223
+ this.debugLog(`(version, power, mode, childLock) = OpenAPI:(${this.deviceStatus.version}, ${this.deviceStatus.power}, ${this.deviceStatus.mode}, ${this.deviceStatus.childLock}), current:(${this.accessory.context.version}, ${this.AirPurifier.Active}, ${this.AirPurifier.TargetAirPurifierState}, ${this.AirPurifier.LockPhysicalControls})`)
224
+
225
+ // Active
226
+ this.AirPurifier.Active = this.deviceStatus.power === 'on' ? this.hap.Characteristic.Active.ACTIVE : this.hap.Characteristic.Active.INACTIVE
227
+ this.debugLog(`Active: ${this.AirPurifier.Active}`)
228
+
229
+ // CurrentAirPurifierState
230
+ if (this.deviceStatus.power === 'on') {
231
+ this.AirPurifier.CurrentAirPurifierState = this.hap.Characteristic.CurrentAirPurifierState.PURIFYING_AIR
232
+ } else {
233
+ this.AirPurifier.CurrentAirPurifierState = this.hap.Characteristic.CurrentAirPurifierState.INACTIVE
234
+ }
235
+ this.debugLog(`CurrentAirPurifierState: ${this.AirPurifier.CurrentAirPurifierState}`)
236
+
237
+ // TargetAirPurifierState (mode: 0 = auto, 1 = low, 2 = medium, 3 = high)
238
+ if (this.deviceStatus.mode === 0) {
239
+ this.AirPurifier.TargetAirPurifierState = this.hap.Characteristic.TargetAirPurifierState.AUTO
240
+ } else {
241
+ this.AirPurifier.TargetAirPurifierState = this.hap.Characteristic.TargetAirPurifierState.MANUAL
242
+ }
243
+ this.debugLog(`TargetAirPurifierState: ${this.AirPurifier.TargetAirPurifierState}`)
244
+
245
+ // RotationSpeed (convert mode to percentage)
246
+ if (this.deviceStatus.mode === 0) {
247
+ this.AirPurifier.RotationSpeed = 0 // Auto mode
248
+ } else {
249
+ this.AirPurifier.RotationSpeed = Math.round((this.deviceStatus.mode / 3) * 100)
250
+ }
251
+ this.debugLog(`RotationSpeed: ${this.AirPurifier.RotationSpeed}`)
252
+
253
+ // LockPhysicalControls
254
+ this.AirPurifier.LockPhysicalControls = this.deviceStatus.childLock === 1
255
+ ? this.hap.Characteristic.LockPhysicalControls.CONTROL_LOCK_ENABLED
256
+ : this.hap.Characteristic.LockPhysicalControls.CONTROL_LOCK_DISABLED
257
+ this.debugLog(`LockPhysicalControls: ${this.AirPurifier.LockPhysicalControls}`)
258
+
259
+ // Firmware Version
260
+ if (this.deviceStatus.version) {
261
+ const version = this.deviceStatus.version as string
262
+ this.debugLog(`Firmware Version: ${version.replace(/^V|-.*$/g, '')}`)
263
+ const deviceVersion = version.replace(/^V|-.*$/g, '') ?? '0.0.0'
264
+ this.accessory
265
+ .getService(this.hap.Service.AccessoryInformation)!
266
+ .setCharacteristic(this.hap.Characteristic.HardwareRevision, deviceVersion)
267
+ .setCharacteristic(this.hap.Characteristic.FirmwareRevision, deviceVersion)
268
+ .getCharacteristic(this.hap.Characteristic.FirmwareRevision)
269
+ .updateValue(deviceVersion)
270
+ this.accessory.context.version = deviceVersion
271
+ this.debugSuccessLog(`version: ${this.accessory.context.version}`)
272
+ }
273
+ }
274
+
275
+ /**
276
+ * Asks the SwitchBot API for the latest device information
277
+ */
278
+ async refreshStatus(): Promise<void> {
279
+ if (!this.device.enableCloudService && this.OpenAPI) {
280
+ this.errorLog(`refreshStatus enableCloudService: ${this.device.enableCloudService}`)
281
+ } else if (this.BLE) {
282
+ await this.BLERefreshStatus()
283
+ } else if (this.OpenAPI && this.platform.config.credentials?.token) {
284
+ await this.openAPIRefreshStatus()
285
+ } else {
286
+ await this.offlineOff()
287
+ this.debugWarnLog(`Connection Type: ${this.device.connectionType}, refreshStatus will not happen.`)
288
+ }
289
+ }
290
+
291
+ async BLERefreshStatus(): Promise<void> {
292
+ this.debugLog('BLERefreshStatus')
293
+ const switchBotBLE = await this.switchbotBLE()
294
+ if (switchBotBLE === undefined) {
295
+ await this.BLERefreshConnection(switchBotBLE)
296
+ } else {
297
+ // Start to monitor advertisement packets
298
+ (async () => {
299
+ // Start to monitor advertisement packets
300
+ const serviceData = await this.monitorAdvertisementPackets(switchBotBLE) as airPurifierServiceData
301
+ // Update HomeKit
302
+ if (serviceData.model === SwitchBotBLEModel.AirPurifier && SwitchBotBLEModelName.AirPurifier) {
303
+ this.serviceData = serviceData
304
+ if (serviceData !== undefined && serviceData !== null) {
305
+ await this.BLEparseStatus()
306
+ await this.updateHomeKitCharacteristics()
307
+ } else {
308
+ this.errorLog(`serviceData is either undefined or null, serviceData: ${JSON.stringify(serviceData)}`)
309
+ await this.BLERefreshConnection(switchBotBLE)
310
+ }
311
+ } else {
312
+ this.errorLog(`failed to get serviceData, serviceData: ${JSON.stringify(serviceData)}`)
313
+ await this.BLERefreshConnection(switchBotBLE)
314
+ }
315
+ })()
316
+ }
317
+ }
318
+
319
+ async registerPlatformBLE(): Promise<void> {
320
+ this.debugLog('registerPlatformBLE')
321
+ if (this.config.options?.BLE && !this.device.disablePlatformBLE) {
322
+ this.debugLog('is listening to Platform BLE.')
323
+ try {
324
+ const formattedDeviceId = formatDeviceIdAsMac(this.device.deviceId)
325
+ this.device.bleMac = formattedDeviceId
326
+ this.debugLog(`bleMac: ${this.device.bleMac}`)
327
+ this.platform.bleEventHandler[this.device.bleMac] = async (context: airPurifierServiceData) => {
328
+ try {
329
+ this.serviceData = context
330
+ if (context !== undefined && context !== null) {
331
+ this.debugLog(`received BLE: ${JSON.stringify(context)}`)
332
+ await this.BLEparseStatus()
333
+ await this.updateHomeKitCharacteristics()
334
+ } else {
335
+ this.errorLog(`context is either undefined or null, context: ${JSON.stringify(context)}`)
336
+ await this.BLERefreshConnection(context)
337
+ }
338
+ } catch (e: any) {
339
+ this.errorLog(`failed to handle BLE. Received: ${JSON.stringify(context)} Error: ${e.message ?? e}`)
340
+ }
341
+ }
342
+ } catch (error) {
343
+ this.errorLog(`failed to format device ID as MAC, Error: ${error}`)
344
+ }
345
+ } else {
346
+ this.debugLog('is not listening to Platform BLE')
347
+ }
348
+ }
349
+
350
+ async openAPIRefreshStatus(): Promise<void> {
351
+ this.debugLog('openAPIRefreshStatus')
352
+ try {
353
+ const response = await this.deviceRefreshStatus()
354
+ const deviceStatus: any = response.body
355
+ this.debugLog(`statusCode: ${deviceStatus.statusCode}, deviceStatus: ${JSON.stringify(deviceStatus)}`)
356
+ if (await this.successfulStatusCodes(deviceStatus)) {
357
+ this.debugSuccessLog(`statusCode: ${deviceStatus.statusCode}, deviceStatus: ${JSON.stringify(deviceStatus)}`)
358
+ this.deviceStatus = deviceStatus.body
359
+ await this.openAPIparseStatus()
360
+ await this.updateHomeKitCharacteristics()
361
+ } else {
362
+ this.debugWarnLog(`statusCode: ${deviceStatus.statusCode}, deviceStatus: ${JSON.stringify(deviceStatus)}`)
363
+ }
364
+ } catch (e: any) {
365
+ await this.apiError(e)
366
+ this.errorLog(`failed openAPIRefreshStatus with ${this.device.connectionType} Connection, Error Message: ${JSON.stringify(e.message)}`)
367
+ }
368
+ }
369
+
370
+ async registerWebhook() {
371
+ if (this.device.webhook) {
372
+ this.debugLog('is listening webhook.')
373
+ this.platform.webhookEventHandler[this.device.deviceId] = async (context: any) => {
374
+ try {
375
+ this.debugLog(`received Webhook: ${JSON.stringify(context)}`)
376
+ // Webhook handling would go here when available
377
+ // For now, just refresh status
378
+ await this.refreshStatus()
379
+ } catch (e: any) {
380
+ this.errorLog(`failed to handle webhook. Received: ${JSON.stringify(context)} Error: ${e.message ?? e}`)
381
+ }
382
+ }
383
+ } else {
384
+ this.debugLog('is not listening webhook.')
385
+ }
386
+ }
387
+
388
+ /**
389
+ * Pushes the requested changes to the SwitchBot API
390
+ * commandType command parameter Description
391
+ * "command" "turnOff" "default" = set to OFF state
392
+ * "command" "turnOn" "default" = set to ON state
393
+ * "command" "setMode" "{0-3}" = 0 for auto, 1 for low, 2 for medium, 3 for high
394
+ */
395
+
396
+ async pushChanges(): Promise<void> {
397
+ if (!this.device.enableCloudService && this.OpenAPI) {
398
+ this.errorLog(`pushChanges enableCloudService: ${this.device.enableCloudService}`)
399
+ } else if (this.BLE) {
400
+ await this.BLEpushChanges()
401
+ } else if (this.OpenAPI && this.platform.config.credentials?.token) {
402
+ await this.openAPIpushChanges()
403
+ } else {
404
+ await this.offlineOff()
405
+ this.debugWarnLog(`Connection Type: ${this.device.connectionType}, pushChanges will not happen.`)
406
+ }
407
+ // Refresh the status from the API
408
+ interval(15000)
409
+ .pipe(skipWhile(() => this.airPurifierUpdateInProgress))
410
+ .pipe(take(1))
411
+ .subscribe(async () => {
412
+ await this.refreshStatus()
413
+ })
414
+ }
415
+
416
+ async BLEpushChanges(): Promise<void> {
417
+ this.debugLog('BLEpushChanges')
418
+ if (this.AirPurifier.Active !== this.accessory.context.Active) {
419
+ this.debugLog(`BLEpushChanges Active: ${this.AirPurifier.Active} ActiveCached: ${this.accessory.context.Active}`)
420
+ const switchBotBLE = await this.platform.connectBLE(this.accessory, this.device)
421
+ try {
422
+ const formattedDeviceId = formatDeviceIdAsMac(this.device.deviceId)
423
+ this.device.bleMac = formattedDeviceId
424
+ this.debugLog(`bleMac: ${this.device.bleMac}`)
425
+ if (switchBotBLE !== false) {
426
+ switchBotBLE
427
+ .discover({ model: this.device.bleModel, id: this.device.bleMac })
428
+ .then(async (device_list: SwitchbotDevice[]) => {
429
+ return await this.retryBLE({
430
+ max: this.maxRetryBLE(),
431
+ fn: async () => {
432
+ if (this.AirPurifier.Active) {
433
+ return await (device_list[0] as any).turnOn()
434
+ } else {
435
+ return await (device_list[0] as any).turnOff()
436
+ }
437
+ },
438
+ })
439
+ })
440
+ .then(async () => {
441
+ this.successLog(`Active: ${this.AirPurifier.Active} sent over SwitchBot BLE, sent successfully`)
442
+ await this.updateHomeKitCharacteristics()
443
+ })
444
+ .catch(async (e: any) => {
445
+ await this.apiError(e)
446
+ this.errorLog(`failed BLEpushChanges with ${this.device.connectionType} Connection, Error Message: ${JSON.stringify(e.message)}`)
447
+ await this.BLEPushConnection()
448
+ })
449
+ } else {
450
+ this.errorLog(`wasn't able to establish BLE Connection, node-switchbot: ${JSON.stringify(switchBotBLE)}`)
451
+ await this.BLEPushConnection()
452
+ }
453
+ } catch (error) {
454
+ this.errorLog(`failed to format device ID as MAC, Error: ${error}`)
455
+ }
456
+ } else {
457
+ this.debugLog(`No changes (BLEpushChanges), Active: ${this.AirPurifier.Active}, ActiveCached: ${this.accessory.context.Active}`)
458
+ }
459
+ }
460
+
461
+ async openAPIpushChanges(): Promise<void> {
462
+ this.debugLog('openAPIpushChanges')
463
+ if (this.AirPurifier.Active !== this.accessory.context.Active) {
464
+ const command = this.AirPurifier.Active ? 'turnOn' : 'turnOff'
465
+ const bodyChange: bodyChange = {
466
+ command: `${command}`,
467
+ parameter: 'default',
468
+ commandType: 'command',
469
+ }
470
+ this.debugLog(`SwitchBot OpenAPI bodyChange: ${JSON.stringify(bodyChange)}`)
471
+ try {
472
+ const response = await this.pushChangeRequest(bodyChange)
473
+ const deviceStatus: any = response.body
474
+ this.debugLog(`statusCode: ${deviceStatus.statusCode}, deviceStatus: ${JSON.stringify(deviceStatus)}`)
475
+ if (await this.successfulStatusCodes(deviceStatus)) {
476
+ this.debugSuccessLog(`statusCode: ${deviceStatus.statusCode}, deviceStatus: ${JSON.stringify(deviceStatus)}`)
477
+ await this.updateHomeKitCharacteristics()
478
+ } else {
479
+ await this.statusCode(deviceStatus.statusCode)
480
+ }
481
+ } catch (e: any) {
482
+ await this.apiError(e)
483
+ this.errorLog(`failed openAPIpushChanges with ${this.device.connectionType} Connection, Error Message: ${JSON.stringify(e.message)}`)
484
+ }
485
+ } else {
486
+ this.debugLog(`No changes (openAPIpushChanges), Active: ${this.AirPurifier.Active}, ActiveCached: ${this.accessory.context.Active}`)
487
+ }
488
+ }
489
+
490
+ async updateHomeKitCharacteristics(): Promise<void> {
491
+ // Active
492
+ await this.updateCharacteristic(this.AirPurifier.Service, this.hap.Characteristic.Active, this.AirPurifier.Active, 'Active')
493
+ // CurrentAirPurifierState
494
+ await this.updateCharacteristic(this.AirPurifier.Service, this.hap.Characteristic.CurrentAirPurifierState, this.AirPurifier.CurrentAirPurifierState, 'CurrentAirPurifierState')
495
+ // TargetAirPurifierState
496
+ await this.updateCharacteristic(this.AirPurifier.Service, this.hap.Characteristic.TargetAirPurifierState, this.AirPurifier.TargetAirPurifierState, 'TargetAirPurifierState')
497
+ // RotationSpeed
498
+ await this.updateCharacteristic(this.AirPurifier.Service, this.hap.Characteristic.RotationSpeed, this.AirPurifier.RotationSpeed, 'RotationSpeed')
499
+ // LockPhysicalControls
500
+ await this.updateCharacteristic(this.AirPurifier.Service, this.hap.Characteristic.LockPhysicalControls, this.AirPurifier.LockPhysicalControls, 'LockPhysicalControls')
501
+ // AirQuality (if sensor is enabled)
502
+ if (this.AirQualitySensor) {
503
+ await this.updateCharacteristic(this.AirQualitySensor.Service, this.hap.Characteristic.AirQuality, this.AirQualitySensor.AirQuality, 'AirQuality')
504
+ await this.updateCharacteristic(this.AirQualitySensor.Service, this.hap.Characteristic.PM2_5Density, this.AirQualitySensor.PM2_5Density, 'PM2_5Density')
505
+ }
506
+ }
507
+
508
+ async ActiveSet(value: CharacteristicValue): Promise<void> {
509
+ this.debugLog(`Active: ${value}`)
510
+ this.AirPurifier.Active = value
511
+ this.doAirPurifierUpdate.next()
512
+ }
513
+
514
+ async RotationSpeedSet(value: CharacteristicValue): Promise<void> {
515
+ this.debugLog(`RotationSpeed: ${value}`)
516
+ this.AirPurifier.RotationSpeed = value
517
+ // Note: Speed changes might need additional API calls depending on the device capabilities
518
+ // For now, we just update the value
519
+ }
520
+
521
+ async TargetAirPurifierStateSet(value: CharacteristicValue): Promise<void> {
522
+ this.debugLog(`TargetAirPurifierState: ${value}`)
523
+ this.AirPurifier.TargetAirPurifierState = value
524
+ // Note: Mode changes might need additional API calls depending on the device capabilities
525
+ // For now, we just update the value
526
+ }
527
+
528
+ async BLEPushConnection() {
529
+ if (this.platform.config.credentials?.token && this.device.connectionType === 'BLE/OpenAPI') {
530
+ this.warnLog('Using OpenAPI Connection to Push Changes')
531
+ await this.openAPIpushChanges()
532
+ }
533
+ }
534
+
535
+ async BLERefreshConnection(switchbot: any): Promise<void> {
536
+ this.errorLog(`wasn't able to establish BLE Connection, node-switchbot: ${switchbot}`)
537
+ if (this.platform.config.credentials?.token && this.device.connectionType === 'BLE/OpenAPI') {
538
+ this.warnLog('Using OpenAPI Connection to Refresh Status')
539
+ await this.openAPIRefreshStatus()
540
+ }
541
+ }
542
+
543
+ async offlineOff(): Promise<void> {
544
+ if (this.device.offline) {
545
+ this.AirPurifier.Active = this.hap.Characteristic.Active.INACTIVE
546
+ this.AirPurifier.CurrentAirPurifierState = this.hap.Characteristic.CurrentAirPurifierState.INACTIVE
547
+ }
548
+ }
549
+
550
+ async apiError(e: any): Promise<void> {
551
+ this.AirPurifier.Service.updateCharacteristic(this.hap.Characteristic.Active, e)
552
+ this.AirPurifier.Service.updateCharacteristic(this.hap.Characteristic.CurrentAirPurifierState, e)
553
+ this.AirPurifier.Service.updateCharacteristic(this.hap.Characteristic.TargetAirPurifierState, e)
554
+ this.AirPurifier.Service.updateCharacteristic(this.hap.Characteristic.RotationSpeed, e)
555
+ this.AirPurifier.Service.updateCharacteristic(this.hap.Characteristic.LockPhysicalControls, e)
556
+ if (this.AirQualitySensor) {
557
+ this.AirQualitySensor.Service.updateCharacteristic(this.hap.Characteristic.AirQuality, e)
558
+ this.AirQualitySensor.Service.updateCharacteristic(this.hap.Characteristic.PM2_5Density, e)
559
+ }
560
+ }
561
+ }
@@ -684,6 +684,42 @@ export abstract class deviceBase {
684
684
  bleModelName: SwitchBotBLEModelName.Unknown,
685
685
  bleModelFriendlyName: SwitchBotBLEModelFriendlyName.Unknown,
686
686
  },
687
+ 'Air Purifier': {
688
+ model: SwitchBotModel.AirPurifier,
689
+ bleModel: SwitchBotBLEModel.AirPurifier,
690
+ bleModelName: SwitchBotBLEModelName.AirPurifier,
691
+ bleModelFriendlyName: SwitchBotBLEModelFriendlyName.AirPurifier,
692
+ },
693
+ 'Air Purifier Table': {
694
+ model: SwitchBotModel.AirPurifierTable,
695
+ bleModel: SwitchBotBLEModel.AirPurifierTable,
696
+ bleModelName: SwitchBotBLEModelName.AirPurifierTable,
697
+ bleModelFriendlyName: SwitchBotBLEModelFriendlyName.AirPurifierTable,
698
+ },
699
+ 'Air Purifier VOC': {
700
+ model: SwitchBotModel.AirPurifier,
701
+ bleModel: SwitchBotBLEModel.AirPurifier,
702
+ bleModelName: SwitchBotBLEModelName.AirPurifier,
703
+ bleModelFriendlyName: SwitchBotBLEModelFriendlyName.AirPurifier,
704
+ },
705
+ 'Air Purifier Table VOC': {
706
+ model: SwitchBotModel.AirPurifierTable,
707
+ bleModel: SwitchBotBLEModel.AirPurifierTable,
708
+ bleModelName: SwitchBotBLEModelName.AirPurifierTable,
709
+ bleModelFriendlyName: SwitchBotBLEModelFriendlyName.AirPurifierTable,
710
+ },
711
+ 'Air Purifier PM2.5': {
712
+ model: SwitchBotModel.AirPurifier,
713
+ bleModel: SwitchBotBLEModel.AirPurifier,
714
+ bleModelName: SwitchBotBLEModelName.AirPurifier,
715
+ bleModelFriendlyName: SwitchBotBLEModelFriendlyName.AirPurifierPM2_5,
716
+ },
717
+ 'Air Purifier Table PM2.5': {
718
+ model: SwitchBotModel.AirPurifierTable,
719
+ bleModel: SwitchBotBLEModel.AirPurifierTable,
720
+ bleModelName: SwitchBotBLEModelName.AirPurifierTable,
721
+ bleModelFriendlyName: SwitchBotBLEModelFriendlyName.AirPurifierTablePM2_5,
722
+ },
687
723
  }
688
724
  const defaultDevice = {
689
725
  model: SwitchBotModel.Unknown,
package/src/device/hub.ts CHANGED
@@ -218,8 +218,12 @@ export class Hub extends deviceBase {
218
218
 
219
219
  async openAPIparseStatus(): Promise<void> {
220
220
  this.debugLog('openAPIparseStatus')
221
+ this.debugLog(`Device Type: ${this.device.deviceType}, Raw deviceStatus: ${JSON.stringify(this.deviceStatus)}`)
222
+
221
223
  // Hub 3 returns sensor data in a nested sensorData object, while Hub 2 returns it at the root level
222
224
  const sensorData: HubSensorData = this.deviceStatus.sensorData ?? this.deviceStatus
225
+ this.debugLog(`Using sensorData from: ${this.deviceStatus.sensorData ? 'nested sensorData object (Hub 3)' : 'root level (Hub 2)'}`)
226
+
223
227
  const temperature = sensorData.temperature
224
228
  const humidity = sensorData.humidity
225
229
  const lightLevel = sensorData.lightLevel