@switchbot/homebridge-switchbot 4.3.8 → 4.3.10

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
+ }
@@ -213,6 +213,10 @@ export abstract class deviceBase {
213
213
  deviceSpecificConfig = device as lockConfig
214
214
  break
215
215
  case 'Hub 2':
216
+ case 'Hub Mini 2':
217
+ deviceSpecificConfig = device as hubConfig
218
+ break
219
+ case 'Hub 3':
216
220
  deviceSpecificConfig = device as hubConfig
217
221
  break
218
222
  default:
@@ -444,6 +448,13 @@ export abstract class deviceBase {
444
448
  bleModelName: SwitchBotBLEModelName.Hub2,
445
449
  bleModelFriendlyName: SwitchBotBLEModelFriendlyName.Hub2,
446
450
  },
451
+ 'Hub Mini 2': {
452
+ // Hub Mini 2 uses Hub2 model mapping as it shares the same API interface and capabilities
453
+ model: SwitchBotModel.Hub2,
454
+ bleModel: SwitchBotBLEModel.Hub2,
455
+ bleModelName: SwitchBotBLEModelName.Hub2,
456
+ bleModelFriendlyName: SwitchBotBLEModelFriendlyName.Hub2,
457
+ },
447
458
  'Hub 3': {
448
459
  model: SwitchBotModel.Hub3,
449
460
  bleModel: SwitchBotBLEModel.Hub3,
@@ -684,6 +695,42 @@ export abstract class deviceBase {
684
695
  bleModelName: SwitchBotBLEModelName.Unknown,
685
696
  bleModelFriendlyName: SwitchBotBLEModelFriendlyName.Unknown,
686
697
  },
698
+ 'Air Purifier': {
699
+ model: SwitchBotModel.AirPurifier,
700
+ bleModel: SwitchBotBLEModel.AirPurifier,
701
+ bleModelName: SwitchBotBLEModelName.AirPurifier,
702
+ bleModelFriendlyName: SwitchBotBLEModelFriendlyName.AirPurifier,
703
+ },
704
+ 'Air Purifier Table': {
705
+ model: SwitchBotModel.AirPurifierTable,
706
+ bleModel: SwitchBotBLEModel.AirPurifierTable,
707
+ bleModelName: SwitchBotBLEModelName.AirPurifierTable,
708
+ bleModelFriendlyName: SwitchBotBLEModelFriendlyName.AirPurifierTable,
709
+ },
710
+ 'Air Purifier VOC': {
711
+ model: SwitchBotModel.AirPurifier,
712
+ bleModel: SwitchBotBLEModel.AirPurifier,
713
+ bleModelName: SwitchBotBLEModelName.AirPurifier,
714
+ bleModelFriendlyName: SwitchBotBLEModelFriendlyName.AirPurifier,
715
+ },
716
+ 'Air Purifier Table VOC': {
717
+ model: SwitchBotModel.AirPurifierTable,
718
+ bleModel: SwitchBotBLEModel.AirPurifierTable,
719
+ bleModelName: SwitchBotBLEModelName.AirPurifierTable,
720
+ bleModelFriendlyName: SwitchBotBLEModelFriendlyName.AirPurifierTable,
721
+ },
722
+ 'Air Purifier PM2.5': {
723
+ model: SwitchBotModel.AirPurifier,
724
+ bleModel: SwitchBotBLEModel.AirPurifier,
725
+ bleModelName: SwitchBotBLEModelName.AirPurifier,
726
+ bleModelFriendlyName: SwitchBotBLEModelFriendlyName.AirPurifierPM2_5,
727
+ },
728
+ 'Air Purifier Table PM2.5': {
729
+ model: SwitchBotModel.AirPurifierTable,
730
+ bleModel: SwitchBotBLEModel.AirPurifierTable,
731
+ bleModelName: SwitchBotBLEModelName.AirPurifierTable,
732
+ bleModelFriendlyName: SwitchBotBLEModelFriendlyName.AirPurifierTablePM2_5,
733
+ },
687
734
  }
688
735
  const defaultDevice = {
689
736
  model: SwitchBotModel.Unknown,