@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.
- package/CHANGELOG.md +1768 -0
- package/config.schema.json +239 -12
- package/dist/device/airpurifier.d.ts +45 -0
- package/dist/device/airpurifier.d.ts.map +1 -0
- package/dist/device/airpurifier.js +520 -0
- package/dist/device/airpurifier.js.map +1 -0
- package/dist/device/device.d.ts.map +1 -1
- package/dist/device/device.js +36 -0
- package/dist/device/device.js.map +1 -1
- package/dist/device/hub.d.ts.map +1 -1
- package/dist/device/hub.js +2 -0
- package/dist/device/hub.js.map +1 -1
- package/dist/platform.d.ts +1 -0
- package/dist/platform.d.ts.map +1 -1
- package/dist/platform.js +66 -0
- package/dist/platform.js.map +1 -1
- package/dist/settings.d.ts +5 -2
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +1 -0
- package/dist/settings.js.map +1 -1
- package/docs/variables/default.html +1 -1
- package/package.json +1 -1
- package/src/device/airpurifier.ts +561 -0
- package/src/device/device.ts +36 -0
- package/src/device/hub.ts +4 -0
- package/src/platform.ts +68 -0
- package/src/settings.ts +6 -2
|
@@ -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
|
+
}
|
package/src/device/device.ts
CHANGED
|
@@ -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
|