@switchbot/homebridge-switchbot 3.5.0-beta.28 → 3.5.0-beta.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/CHANGELOG.md +1 -0
  2. package/config.schema.json +67 -1
  3. package/dist/device/blindtilt.d.ts.map +1 -1
  4. package/dist/device/blindtilt.js +8 -3
  5. package/dist/device/blindtilt.js.map +1 -1
  6. package/dist/device/bot.d.ts.map +1 -1
  7. package/dist/device/bot.js +8 -3
  8. package/dist/device/bot.js.map +1 -1
  9. package/dist/device/ceilinglight.d.ts.map +1 -1
  10. package/dist/device/ceilinglight.js +31 -5
  11. package/dist/device/ceilinglight.js.map +1 -1
  12. package/dist/device/colorbulb.d.ts.map +1 -1
  13. package/dist/device/colorbulb.js +23 -4
  14. package/dist/device/colorbulb.js.map +1 -1
  15. package/dist/device/contact.d.ts.map +1 -1
  16. package/dist/device/contact.js +7 -2
  17. package/dist/device/contact.js.map +1 -1
  18. package/dist/device/curtain.d.ts.map +1 -1
  19. package/dist/device/curtain.js +7 -2
  20. package/dist/device/curtain.js.map +1 -1
  21. package/dist/device/device.d.ts.map +1 -1
  22. package/dist/device/device.js +13 -3
  23. package/dist/device/device.js.map +1 -1
  24. package/dist/device/fan.d.ts +51 -0
  25. package/dist/device/fan.d.ts.map +1 -0
  26. package/dist/device/fan.js +499 -0
  27. package/dist/device/fan.js.map +1 -0
  28. package/dist/device/hub.d.ts.map +1 -1
  29. package/dist/device/hub.js +32 -24
  30. package/dist/device/hub.js.map +1 -1
  31. package/dist/device/humidifier.d.ts.map +1 -1
  32. package/dist/device/humidifier.js +7 -2
  33. package/dist/device/humidifier.js.map +1 -1
  34. package/dist/device/iosensor.d.ts.map +1 -1
  35. package/dist/device/iosensor.js +25 -17
  36. package/dist/device/iosensor.js.map +1 -1
  37. package/dist/device/lightstrip.d.ts.map +1 -1
  38. package/dist/device/lightstrip.js +41 -4
  39. package/dist/device/lightstrip.js.map +1 -1
  40. package/dist/device/lock.d.ts.map +1 -1
  41. package/dist/device/lock.js +7 -2
  42. package/dist/device/lock.js.map +1 -1
  43. package/dist/device/meter.d.ts.map +1 -1
  44. package/dist/device/meter.js +25 -17
  45. package/dist/device/meter.js.map +1 -1
  46. package/dist/device/meterplus.d.ts.map +1 -1
  47. package/dist/device/meterplus.js +7 -2
  48. package/dist/device/meterplus.js.map +1 -1
  49. package/dist/device/motion.d.ts.map +1 -1
  50. package/dist/device/motion.js +7 -2
  51. package/dist/device/motion.js.map +1 -1
  52. package/dist/device/plug.d.ts.map +1 -1
  53. package/dist/device/plug.js +7 -2
  54. package/dist/device/plug.js.map +1 -1
  55. package/dist/device/robotvacuumcleaner.d.ts.map +1 -1
  56. package/dist/device/robotvacuumcleaner.js +8 -3
  57. package/dist/device/robotvacuumcleaner.js.map +1 -1
  58. package/dist/device/waterdetector.d.ts.map +1 -1
  59. package/dist/device/waterdetector.js +14 -7
  60. package/dist/device/waterdetector.js.map +1 -1
  61. package/dist/irdevice/airconditioner.d.ts.map +1 -1
  62. package/dist/irdevice/airconditioner.js +2 -1
  63. package/dist/irdevice/airconditioner.js.map +1 -1
  64. package/dist/irdevice/airpurifier.d.ts.map +1 -1
  65. package/dist/irdevice/airpurifier.js +2 -1
  66. package/dist/irdevice/airpurifier.js.map +1 -1
  67. package/dist/irdevice/camera.d.ts.map +1 -1
  68. package/dist/irdevice/camera.js +2 -1
  69. package/dist/irdevice/camera.js.map +1 -1
  70. package/dist/irdevice/fan.d.ts +1 -1
  71. package/dist/irdevice/fan.d.ts.map +1 -1
  72. package/dist/irdevice/fan.js +3 -2
  73. package/dist/irdevice/fan.js.map +1 -1
  74. package/dist/irdevice/irdevice.js.map +1 -1
  75. package/dist/irdevice/light.d.ts.map +1 -1
  76. package/dist/irdevice/light.js +2 -1
  77. package/dist/irdevice/light.js.map +1 -1
  78. package/dist/irdevice/other.d.ts.map +1 -1
  79. package/dist/irdevice/other.js +2 -1
  80. package/dist/irdevice/other.js.map +1 -1
  81. package/dist/irdevice/tv.d.ts.map +1 -1
  82. package/dist/irdevice/tv.js +2 -1
  83. package/dist/irdevice/tv.js.map +1 -1
  84. package/dist/irdevice/vacuumcleaner.d.ts.map +1 -1
  85. package/dist/irdevice/vacuumcleaner.js +2 -1
  86. package/dist/irdevice/vacuumcleaner.js.map +1 -1
  87. package/dist/irdevice/waterheater.d.ts.map +1 -1
  88. package/dist/irdevice/waterheater.js +2 -1
  89. package/dist/irdevice/waterheater.js.map +1 -1
  90. package/dist/platform.d.ts +2 -1
  91. package/dist/platform.d.ts.map +1 -1
  92. package/dist/platform.js +68 -5
  93. package/dist/platform.js.map +1 -1
  94. package/dist/settings.d.ts +22 -4
  95. package/dist/settings.d.ts.map +1 -1
  96. package/dist/utils.d.ts +10 -5
  97. package/dist/utils.d.ts.map +1 -1
  98. package/dist/utils.js +23 -9
  99. package/dist/utils.js.map +1 -1
  100. package/package.json +3 -3
  101. package/src/device/blindtilt.ts +8 -3
  102. package/src/device/bot.ts +8 -3
  103. package/src/device/ceilinglight.ts +31 -5
  104. package/src/device/colorbulb.ts +25 -6
  105. package/src/device/contact.ts +9 -4
  106. package/src/device/curtain.ts +21 -16
  107. package/src/device/device.ts +13 -3
  108. package/src/device/fan.ts +548 -0
  109. package/src/device/hub.ts +30 -22
  110. package/src/device/humidifier.ts +8 -3
  111. package/src/device/iosensor.ts +27 -19
  112. package/src/device/lightstrip.ts +40 -6
  113. package/src/device/lock.ts +12 -7
  114. package/src/device/meter.ts +29 -21
  115. package/src/device/meterplus.ts +11 -6
  116. package/src/device/motion.ts +8 -3
  117. package/src/device/plug.ts +8 -3
  118. package/src/device/robotvacuumcleaner.ts +13 -8
  119. package/src/device/waterdetector.ts +14 -7
  120. package/src/irdevice/airconditioner.ts +4 -3
  121. package/src/irdevice/airpurifier.ts +4 -3
  122. package/src/irdevice/camera.ts +3 -2
  123. package/src/irdevice/fan.ts +3 -2
  124. package/src/irdevice/irdevice.ts +1 -1
  125. package/src/irdevice/light.ts +2 -1
  126. package/src/irdevice/other.ts +46 -45
  127. package/src/irdevice/tv.ts +2 -1
  128. package/src/irdevice/vacuumcleaner.ts +2 -1
  129. package/src/irdevice/waterheater.ts +2 -1
  130. package/src/platform.ts +71 -5
  131. package/src/settings.ts +34 -9
  132. package/src/utils.ts +24 -9
@@ -0,0 +1,548 @@
1
+ /* eslint-disable max-len */
2
+ /* Copyright(C) 2021-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved.
3
+ *
4
+ * plug.ts: @switchbot/homebridge-switchbot.
5
+ */
6
+ import { request } from 'undici';
7
+ import { deviceBase } from './device.js';
8
+ import { SwitchBotPlatform } from '../platform.js';
9
+ import { Subject, debounceTime, interval, skipWhile, take, tap } from 'rxjs';
10
+ import { CharacteristicValue, PlatformAccessory, Service } from 'homebridge';
11
+ import { device, devicesConfig, serviceData, deviceStatus, Devices } from '../settings.js';
12
+
13
+ export class Fan extends deviceBase {
14
+ // Services
15
+ private Fan: {
16
+ Service: Service;
17
+ Active: CharacteristicValue;
18
+ SwingMode: CharacteristicValue;
19
+ RotationSpeed: CharacteristicValue;
20
+ };
21
+
22
+ private Battery: {
23
+ Service: Service;
24
+ BatteryLevel: CharacteristicValue;
25
+ StatusLowBattery: CharacteristicValue;
26
+ ChargingState: CharacteristicValue;
27
+ };
28
+
29
+ // Updates
30
+ plugUpdateInProgress!: boolean;
31
+ doPlugUpdate!: Subject<void>;
32
+
33
+ constructor(
34
+ readonly platform: SwitchBotPlatform,
35
+ accessory: PlatformAccessory,
36
+ device: device & devicesConfig,
37
+ ) {
38
+ super(platform, accessory, device);
39
+ // this is subject we use to track when we need to POST changes to the SwitchBot API
40
+ this.doPlugUpdate = new Subject();
41
+ this.plugUpdateInProgress = false;
42
+
43
+ // Initialize Fan property
44
+ this.Fan = {
45
+ Service: accessory.getService(this.hap.Service.Fanv2)!,
46
+ Active: accessory.context.Active || this.hap.Characteristic.Active.INACTIVE,
47
+ SwingMode: accessory.context.SwingMode || this.hap.Characteristic.SwingMode.SWING_DISABLED,
48
+ RotationSpeed: accessory.context.RotationSpeed || 0,
49
+ };
50
+
51
+ // Initialize Battery property
52
+ this.Battery = {
53
+ Service: accessory.getService(this.hap.Service.Battery)!,
54
+ BatteryLevel: accessory.context.BatteryLevel || 100,
55
+ StatusLowBattery: accessory.context.StatusLowBattery || this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL,
56
+ ChargingState: accessory.context.ChargingState || this.hap.Characteristic.ChargingState.NOT_CHARGING,
57
+ };
58
+
59
+ // Retrieve initial values and updateHomekit
60
+ this.refreshStatus();
61
+
62
+ // get the Fan service if it exists, otherwise create a new Fanv2 service
63
+ // you can create multiple services for each accessory
64
+ const FanService = `${accessory.displayName} ${device.deviceType}`;
65
+ (this.Fan.Service = accessory.getService(this.hap.Service.Fanv2)
66
+ || accessory.addService(this.hap.Service.Fanv2)), FanService;
67
+
68
+ this.Fan.Service.setCharacteristic(this.hap.Characteristic.Name, accessory.displayName);
69
+ // each service must implement at-minimum the "required characteristics" for the given service type
70
+ // see https://developers.homebridge.io/#/service/Fanv2
71
+
72
+ // create handlers for required characteristics
73
+ this.Fan.Service.getCharacteristic(this.hap.Characteristic.Active).onSet(this.ActiveSet.bind(this));
74
+ // create handlers for required characteristics
75
+ this.Fan.Service.getCharacteristic(this.hap.Characteristic.RotationSpeed).onSet(this.RotationSpeedSet.bind(this));
76
+ // create handlers for required characteristics
77
+ this.Fan.Service.getCharacteristic(this.hap.Characteristic.SwingMode).onSet(this.SwingModeSet.bind(this));
78
+
79
+ // Update Homekit
80
+ this.updateHomeKitCharacteristics();
81
+
82
+ // Start an update interval
83
+ interval(this.deviceRefreshRate * 1000)
84
+ .pipe(skipWhile(() => this.plugUpdateInProgress))
85
+ .subscribe(async () => {
86
+ await this.refreshStatus();
87
+ });
88
+
89
+ //regisiter webhook event handler
90
+ if (device.webhook) {
91
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} is listening webhook.`);
92
+ this.platform.webhookEventHandler[this.device.deviceId] = async (context) => {
93
+ try {
94
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} received Webhook: ${JSON.stringify(context)}`);
95
+ const { version, battery, powerState, oscillation, chargingStatus, fanSpeed } = context;
96
+ const { Active, SwingMode, RotationSpeed } = this.Fan;
97
+ const { BatteryLevel, ChargingState } = this.Battery;
98
+ const { FirmwareRevision } = this.accessory.context;
99
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ` +
100
+ '(version, battery, powerState, oscillation, chargingStatus, fanSpeed) = '
101
+ + `Webhook:(${version}, ${battery}, ${powerState}, ${oscillation}, ${chargingStatus}, ${fanSpeed}), `
102
+ + `current:(${FirmwareRevision}, ${BatteryLevel}, ${Active}, ${SwingMode}, ${ChargingState}, ${RotationSpeed})`);
103
+
104
+ // Active
105
+ this.Fan.Active = powerState === 'ON' ? this.hap.Characteristic.Active.ACTIVE : this.hap.Characteristic.Active.INACTIVE;
106
+
107
+ // SwingMode
108
+ this.Fan.SwingMode = oscillation === 'on' ?
109
+ this.hap.Characteristic.SwingMode.SWING_ENABLED : this.hap.Characteristic.SwingMode.SWING_DISABLED;
110
+
111
+ // RotationSpeed
112
+ this.Fan.RotationSpeed = fanSpeed;
113
+
114
+ // ChargingState
115
+ this.Battery.ChargingState = chargingStatus === 'charging' ?
116
+ this.hap.Characteristic.ChargingState.CHARGING : this.hap.Characteristic.ChargingState.NOT_CHARGING;
117
+
118
+ // BatteryLevel
119
+ this.Battery.BatteryLevel = battery;
120
+
121
+ // FirmwareRevision
122
+ this.accessory.context.FirmwareRevision = version;
123
+ this.accessory
124
+ .getService(this.hap.Service.AccessoryInformation)!
125
+ .setCharacteristic(this.hap.Characteristic.FirmwareRevision, this.accessory.context.FirmwareRevision)
126
+ .getCharacteristic(this.hap.Characteristic.FirmwareRevision)
127
+ .updateValue(this.accessory.context.FirmwareRevision);
128
+ this.updateHomeKitCharacteristics();
129
+ } catch (e: any) {
130
+ this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} `
131
+ + `failed to handle webhook. Received: ${JSON.stringify(context)} Error: ${e}`);
132
+ }
133
+ };
134
+ }
135
+
136
+ // Watch for Plug change events
137
+ // We put in a debounce of 100ms so we don't make duplicate calls
138
+ this.doPlugUpdate
139
+ .pipe(
140
+ tap(() => {
141
+ this.plugUpdateInProgress = true;
142
+ }),
143
+ debounceTime(this.platform.config.options!.pushRate! * 1000),
144
+ )
145
+ .subscribe(async () => {
146
+ try {
147
+ await this.pushChanges();
148
+ } catch (e: any) {
149
+ this.apiError(e);
150
+ this.errorLog(
151
+ `${this.device.deviceType}: ${this.accessory.displayName} failed pushChanges with ${this.device.connectionType} Connection,` +
152
+ ` Error Message: ${JSON.stringify(e.message)}`,
153
+ );
154
+ }
155
+ this.plugUpdateInProgress = false;
156
+ });
157
+ }
158
+
159
+ async BLEparseStatus(serviceData: serviceData): Promise<void> {
160
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEparseStatus`);
161
+
162
+ // State
163
+ switch (serviceData.state) {
164
+ case 'on':
165
+ this.Fan.Active = true;
166
+ break;
167
+ default:
168
+ this.Fan.Active = false;
169
+ }
170
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.Fan.Active}`);
171
+ }
172
+
173
+ async openAPIparseStatus(deviceStatus: deviceStatus) {
174
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIparseStatus`);
175
+
176
+ // Active
177
+ this.Fan.Active = deviceStatus.body.power === 'on' ? this.hap.Characteristic.Active.ACTIVE : this.hap.Characteristic.Active.INACTIVE;
178
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Active: ${this.Fan.Active}`);
179
+
180
+ // SwingMode
181
+ this.Fan.SwingMode = deviceStatus.body.oscillation === 'on' ?
182
+ this.hap.Characteristic.SwingMode.SWING_ENABLED : this.hap.Characteristic.SwingMode.SWING_DISABLED;
183
+
184
+ // RotationSpeed
185
+ this.Fan.RotationSpeed = deviceStatus.body.fanSpeed;
186
+
187
+ // ChargingState
188
+ this.Battery.ChargingState = deviceStatus.body.chargingStatus === 'charging' ?
189
+ this.hap.Characteristic.ChargingState.CHARGING : this.hap.Characteristic.ChargingState.NOT_CHARGING;
190
+
191
+ // BatteryLevel
192
+ this.Battery.BatteryLevel = Number(deviceStatus.body.battery);
193
+ if (this.Battery.BatteryLevel < 10) {
194
+ this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW;
195
+ } else {
196
+ this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL;
197
+ }
198
+ if (Number.isNaN(this.Battery.BatteryLevel)) {
199
+ this.Battery.BatteryLevel = 100;
200
+ }
201
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.Battery.BatteryLevel},`
202
+ + ` StatusLowBattery: ${this.Battery.StatusLowBattery}`);
203
+
204
+ // FirmwareRevision
205
+ this.accessory.context.FirmwareRevision = deviceStatus.body.version;
206
+ this.accessory
207
+ .getService(this.hap.Service.AccessoryInformation)!
208
+ .setCharacteristic(this.hap.Characteristic.FirmwareRevision, this.accessory.context.FirmwareRevision)
209
+ .getCharacteristic(this.hap.Characteristic.FirmwareRevision)
210
+ .updateValue(this.accessory.context.FirmwareRevision);
211
+ }
212
+
213
+
214
+
215
+ /**
216
+ * Asks the SwitchBot API for the latest device information
217
+ */
218
+ async refreshStatus(): Promise<void> {
219
+ if (!this.device.enableCloudService && this.OpenAPI) {
220
+ this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} refreshStatus enableCloudService: ${this.device.enableCloudService}`);
221
+ } else if (this.BLE) {
222
+ await this.BLERefreshStatus();
223
+ } else if (this.OpenAPI && this.platform.config.credentials?.token) {
224
+ await this.openAPIRefreshStatus();
225
+ } else {
226
+ await this.offlineOff();
227
+ this.debugWarnLog(
228
+ `${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` +
229
+ ` ${this.device.connectionType}, refreshStatus will not happen.`,
230
+ );
231
+ }
232
+ }
233
+
234
+ async BLERefreshStatus(): Promise<void> {
235
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLERefreshStatus`);
236
+ const switchbot = await this.platform.connectBLE();
237
+ // Convert to BLE Address
238
+ this.device.bleMac = this.device
239
+ .deviceId!.match(/.{1,2}/g)!
240
+ .join(':')
241
+ .toLowerCase();
242
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLE Address: ${this.device.bleMac}`);
243
+ this.getCustomBLEAddress(switchbot);
244
+ // Start to monitor advertisement packets
245
+ (async () => {
246
+ // Start to monitor advertisement packets
247
+ await switchbot.startScan({ model: this.device.bleModel, id: this.device.bleMac });
248
+ // Set an event handler
249
+ switchbot.onadvertisement = (ad: any) => {
250
+ if (this.device.bleMac === ad.address && ad.model === this.device.bleModel) {
251
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ${JSON.stringify(ad, null, ' ')}`);
252
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} address: ${ad.address}, model: ${ad.model}`);
253
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`);
254
+ } else {
255
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`);
256
+ }
257
+ };
258
+ // Wait 10 seconds
259
+ await switchbot.wait(this.scanDuration * 1000);
260
+ // Stop to monitor
261
+ await switchbot.stopScan();
262
+ // Update HomeKit
263
+ await this.BLEparseStatus(switchbot.onadvertisement.serviceData);
264
+ await this.updateHomeKitCharacteristics();
265
+ })();
266
+ if (switchbot === undefined) {
267
+ await this.BLERefreshConnection(switchbot);
268
+ }
269
+ }
270
+
271
+ async openAPIRefreshStatus(): Promise<void> {
272
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIRefreshStatus`);
273
+ try {
274
+ const { body, statusCode } = await this.platform.retryRequest(this.deviceMaxRetries, this.deviceDelayBetweenRetries,
275
+ `${Devices}/${this.device.deviceId}/status`, { headers: this.platform.generateHeaders() });
276
+ this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} statusCode: ${statusCode}`);
277
+ const deviceStatus: any = await body.json();
278
+ this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus: ${JSON.stringify(deviceStatus)}`);
279
+ this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus statusCode: ${deviceStatus.statusCode}`);
280
+ if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) {
281
+ this.debugSuccessLog(`${this.device.deviceType}: ${this.accessory.displayName} `
282
+ + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`);
283
+ this.openAPIparseStatus(deviceStatus);
284
+ this.updateHomeKitCharacteristics();
285
+ } else {
286
+ this.statusCode(statusCode);
287
+ this.statusCode(deviceStatus.statusCode);
288
+ }
289
+ } catch (e: any) {
290
+ this.apiError(e);
291
+ this.errorLog(
292
+ `${this.device.deviceType}: ${this.accessory.displayName} failed openAPIRefreshStatus with ${this.device.connectionType}` +
293
+ ` Connection, Error Message: ${JSON.stringify(e.message)}`,
294
+ );
295
+ }
296
+ }
297
+
298
+ /**
299
+ * Pushes the requested changes to the SwitchBot API
300
+ * deviceType commandType Command command parameter Description
301
+ * Battery Circulator Fan - "command" "turnOff" "default" = set to OFF state
302
+ * Battery Circulator Fan - "command" "turnOn" "default" = set to ON state
303
+ * Battery Circulator Fan - "command" "setNightLightMode" "off, 1, or 2" = off, turn off nightlight, (1, bright) (2, dim)
304
+ * Battery Circulator Fan - "command" "setWindMode" "direct, natural, sleep, or baby" = Set fan mode. direct: direct mode. natural: natural mode. sleep: sleep mode. baby: ultra quiet mode
305
+ * Battery Circulator Fan - "command" "setWindSpeed" "{1-100} e.g. 10" = Set fan speed.1~100
306
+ */
307
+
308
+ async pushChanges(): Promise<void> {
309
+ if (!this.device.enableCloudService && this.OpenAPI) {
310
+ this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} pushChanges enableCloudService: ${this.device.enableCloudService}`);
311
+ } else if (this.BLE) {
312
+ await this.BLEpushChanges();
313
+ } else if (this.OpenAPI && this.platform.config.credentials?.token) {
314
+ await this.openAPIpushChanges();
315
+ } else {
316
+ await this.offlineOff();
317
+ this.debugWarnLog(
318
+ `${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + ` ${this.device.connectionType}, pushChanges will not happen.`,
319
+ );
320
+ }
321
+ // Refresh the status from the API
322
+ interval(15000)
323
+ .pipe(skipWhile(() => this.plugUpdateInProgress))
324
+ .pipe(take(1))
325
+ .subscribe(async () => {
326
+ await this.refreshStatus();
327
+ });
328
+ }
329
+
330
+ async BLEpushChanges(): Promise<void> {
331
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEpushChanges`);
332
+ if (this.Fan.Active !== this.accessory.context.Active) {
333
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEpushChanges`
334
+ + ` On: ${this.Fan.Active} OnCached: ${this.accessory.context.Active}`);
335
+ const switchbot = await this.platform.connectBLE();
336
+ // Convert to BLE Address
337
+ this.device.bleMac = this.device
338
+ .deviceId!.match(/.{1,2}/g)!
339
+ .join(':')
340
+ .toLowerCase();
341
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLE Address: ${this.device.bleMac}`);
342
+ switchbot
343
+ .discover({
344
+ model: this.device.bleModel,
345
+ id: this.device.bleMac,
346
+ })
347
+ .then(async (device_list: any) => {
348
+ this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.Fan.Active}`);
349
+ return await this.retryBLE({
350
+ max: await this.maxRetryBLE(),
351
+ fn: async () => {
352
+ if (this.Fan.Active) {
353
+ return await device_list[0].turnOn({ id: this.device.bleMac });
354
+ } else {
355
+ return await device_list[0].turnOff({ id: this.device.bleMac });
356
+ }
357
+ },
358
+ });
359
+ })
360
+ .then(() => {
361
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Done.`);
362
+ this.successLog(`${this.device.deviceType}: ${this.accessory.displayName} `
363
+ + `Active: ${this.Fan.Active} sent over BLE, sent successfully`);
364
+ this.Fan.Active = false;
365
+ })
366
+ .catch(async (e: any) => {
367
+ this.apiError(e);
368
+ this.errorLog(
369
+ `${this.device.deviceType}: ${this.accessory.displayName} failed BLEpushChanges with ${this.device.connectionType}` +
370
+ ` Connection, Error Message: ${JSON.stringify(e.message)}`,
371
+ );
372
+ await this.BLEPushConnection();
373
+ });
374
+ } else {
375
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No BLEpushChanges,`
376
+ + ` Active: ${this.Fan.Active}, ActiveCached: ${this.accessory.context.Active}`);
377
+ }
378
+ }
379
+
380
+ async openAPIpushChanges() {
381
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIpushChanges`);
382
+ if (this.Fan.Active !== this.accessory.context.Active) {
383
+ let command = '';
384
+ if (this.Fan.Active) {
385
+ command = 'turnOn';
386
+ } else {
387
+ command = 'turnOff';
388
+ }
389
+ const bodyChange = JSON.stringify({
390
+ command: `${command}`,
391
+ parameter: 'default',
392
+ commandType: 'command',
393
+ });
394
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Sending request to SwitchBot API, body: ${bodyChange},`);
395
+ try {
396
+ const { body, statusCode } = await request(`${Devices}/${this.device.deviceId}/commands`, {
397
+ body: bodyChange,
398
+ method: 'POST',
399
+ headers: this.platform.generateHeaders(),
400
+ });
401
+ this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} statusCode: ${statusCode}`);
402
+ const deviceStatus: any = await body.json();
403
+ this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus: ${JSON.stringify(deviceStatus)}`);
404
+ this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus body: ${JSON.stringify(deviceStatus.body)}`);
405
+ this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus statusCode: ${deviceStatus.statusCode}`);
406
+ if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) {
407
+ this.debugErrorLog(`${this.device.deviceType}: ${this.accessory.displayName} `
408
+ + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`);
409
+ this.successLog(`${this.device.deviceType}: ${this.accessory.displayName} `
410
+ + `request to SwitchBot API, body: ${deviceStatus} sent successfully`);
411
+ } else {
412
+ this.statusCode(statusCode);
413
+ this.statusCode(deviceStatus.statusCode);
414
+ }
415
+ } catch (e: any) {
416
+ this.apiError(e);
417
+ this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed openAPIpushChanges with ${this.device.connectionType}`
418
+ + ` Connection, Error Message: ${JSON.stringify(e.message)}`);
419
+ }
420
+ } else {
421
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No openAPIpushChanges.`
422
+ + `On: ${this.Fan.Active}, ActiveCached: ${this.accessory.context.Active}`);
423
+ }
424
+ }
425
+
426
+ /**
427
+ * Handle requests to set the value of the "On" characteristic
428
+ */
429
+ async ActiveSet(value: CharacteristicValue): Promise<void> {
430
+ if (this.Fan.Active === this.accessory.context.Active) {
431
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No Changes, Set Active: ${value}`);
432
+ } else {
433
+ this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set Active: ${value}`);
434
+ }
435
+
436
+ this.Fan.Active = value;
437
+ this.doPlugUpdate.next();
438
+ }
439
+
440
+ /**
441
+ * Handle requests to set the value of the "On" characteristic
442
+ */
443
+ async RotationSpeedSet(value: CharacteristicValue): Promise<void> {
444
+ if (this.Fan.RotationSpeed === this.accessory.context.RotationSpeed) {
445
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No Changes, Set RotationSpeed: ${value}`);
446
+ } else {
447
+ this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set RotationSpeed: ${value}`);
448
+ }
449
+
450
+ this.Fan.RotationSpeed = value;
451
+ this.doPlugUpdate.next();
452
+ }
453
+
454
+ /**
455
+ * Handle requests to set the value of the "On" characteristic
456
+ */
457
+ async SwingModeSet(value: CharacteristicValue): Promise<void> {
458
+ if (this.Fan.SwingMode === this.accessory.context.SwingMode) {
459
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No Changes, Set SwingMode: ${value}`);
460
+ } else {
461
+ this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set SwingMode: ${value}`);
462
+ }
463
+
464
+ this.Fan.SwingMode = value;
465
+ this.doPlugUpdate.next();
466
+ }
467
+
468
+ async updateHomeKitCharacteristics(): Promise<void> {
469
+ // Active
470
+ if (this.Fan.Active === undefined) {
471
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Active: ${this.Fan.Active}`);
472
+ } else {
473
+ this.accessory.context.Active = this.Fan.Active;
474
+ this.Fan.Service.updateCharacteristic(this.hap.Characteristic.Active, this.Fan.Active);
475
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic Active: ${this.Fan.Active}`);
476
+ }
477
+ // RotationSpeed
478
+ if(this.Fan.RotationSpeed === undefined) {
479
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} RotationSpeed: ${this.Fan.RotationSpeed}`);
480
+ } else {
481
+ this.accessory.context.RotationSpeed = this.Fan.RotationSpeed;
482
+ this.Fan.Service.updateCharacteristic(this.hap.Characteristic.RotationSpeed, this.Fan.RotationSpeed);
483
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic RotationSpeed: ${this.Fan.RotationSpeed}`);
484
+ }
485
+ // SwingMode
486
+ if(this.Fan.SwingMode === undefined) {
487
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} SwingMode: ${this.Fan.SwingMode}`);
488
+ } else {
489
+ this.accessory.context.SwingMode = this.Fan.SwingMode;
490
+ this.Fan.Service.updateCharacteristic(this.hap.Characteristic.SwingMode, this.Fan.SwingMode);
491
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic SwingMode: ${this.Fan.SwingMode}`);
492
+ }
493
+ // BateryLevel
494
+ if(this.Battery.BatteryLevel === undefined) {
495
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.Battery.BatteryLevel}`);
496
+ } else {
497
+ this.accessory.context.BatteryLevel = this.Battery.BatteryLevel;
498
+ this.Battery.Service.updateCharacteristic(this.hap.Characteristic.BatteryLevel, this.Battery.BatteryLevel);
499
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic BatteryLevel: ${this.Battery.BatteryLevel}`);
500
+ }
501
+ // ChargingState
502
+ if(this.Battery.ChargingState === undefined) {
503
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ChargingState: ${this.Battery.ChargingState}`);
504
+ } else {
505
+ this.accessory.context.ChargingState = this.Battery.ChargingState;
506
+ this.Battery.Service.updateCharacteristic(this.hap.Characteristic.ChargingState, this.Battery.ChargingState);
507
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic ChargingState: ${this.Battery.ChargingState}`);
508
+ }
509
+ // StatusLowBattery
510
+ if(this.Battery.StatusLowBattery === undefined) {
511
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} StatusLowBattery: ${this.Battery.StatusLowBattery}`);
512
+ } else {
513
+ this.accessory.context.StatusLowBattery = this.Battery.StatusLowBattery;
514
+ this.Battery.Service.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, this.Battery.StatusLowBattery);
515
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic StatusLowBattery: ${this.Battery.StatusLowBattery}`);
516
+ }
517
+ }
518
+
519
+ async BLEPushConnection() {
520
+ if (this.platform.config.credentials?.token && this.device.connectionType === 'BLE/OpenAPI') {
521
+ this.warnLog(`${this.device.deviceType}: ${this.accessory.displayName} Using OpenAPI Connection to Push Changes`);
522
+ await this.openAPIpushChanges();
523
+ }
524
+ }
525
+
526
+ async BLERefreshConnection(switchbot: any): Promise<void> {
527
+ this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} wasn't able to establish BLE Connection, node-switchbot:`
528
+ + ` ${JSON.stringify(switchbot)}`);
529
+ if (this.platform.config.credentials?.token && this.device.connectionType === 'BLE/OpenAPI') {
530
+ this.warnLog(`${this.device.deviceType}: ${this.accessory.displayName} Using OpenAPI Connection to Refresh Status`);
531
+ await this.openAPIRefreshStatus();
532
+ }
533
+ }
534
+
535
+ async offlineOff(): Promise<void> {
536
+ if (this.device.offline) {
537
+ this.Fan.Service.updateCharacteristic(this.hap.Characteristic.Active, this.hap.Characteristic.Active.INACTIVE);
538
+ this.Fan.Service.updateCharacteristic(this.hap.Characteristic.RotationSpeed, 0);
539
+ this.Fan.Service.updateCharacteristic(this.hap.Characteristic.SwingMode, this.hap.Characteristic.SwingMode.SWING_DISABLED);
540
+ }
541
+ }
542
+
543
+ async apiError(e: any): Promise<void> {
544
+ this.Fan.Service.updateCharacteristic(this.hap.Characteristic.Active, e);
545
+ this.Fan.Service.updateCharacteristic(this.hap.Characteristic.RotationSpeed, e);
546
+ this.Fan.Service.updateCharacteristic(this.hap.Characteristic.SwingMode, e);
547
+ }
548
+ }
package/src/device/hub.ts CHANGED
@@ -7,6 +7,7 @@ import { deviceBase } from './device.js';
7
7
  import { SwitchBotPlatform } from '../platform.js';
8
8
  import { Devices, device, deviceStatus, devicesConfig } from '../settings.js';
9
9
  import { CharacteristicValue, PlatformAccessory, Service, Units } from 'homebridge';
10
+ import { convertUnits } from '../utils.js';
10
11
 
11
12
  export class Hub extends deviceBase {
12
13
  // Services
@@ -145,34 +146,36 @@ export class Hub extends deviceBase {
145
146
  });
146
147
 
147
148
  //regisiter webhook event handler
148
- if (this.device.webhook) {
149
- this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} is listening webhook.`);
149
+ if (device.webhook) {
150
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} is listening webhook.`);
150
151
  this.platform.webhookEventHandler[this.device.deviceId] = async (context) => {
151
152
  try {
152
153
  this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} received Webhook: ${JSON.stringify(context)}`);
153
- if (context.scale === 'CELSIUS') {
154
- const { temperature, humidity, lightLevel } = context;
155
- const { CurrentTemperature } = this.TemperatureSensor || { CurrentTemperature: undefined };
156
- const { CurrentAmbientLightLevel } = this.LightSensor || { CurrentAmbientLightLevel: undefined };
157
- const { CurrentRelativeHumidity } = this.HumiditySensor || { CurrentRelativeHumidity: undefined };
158
- this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ` +
159
- '(temperature, humidity, lightLevel) = ' +
160
- `Webhook:(${temperature}, ${humidity}, ${lightLevel}), ` +
154
+ const { temperature, humidity, lightLevel } = context;
155
+ const { CurrentTemperature } = this.TemperatureSensor || { CurrentTemperature: undefined };
156
+ const { CurrentAmbientLightLevel } = this.LightSensor || { CurrentAmbientLightLevel: undefined };
157
+ const { CurrentRelativeHumidity } = this.HumiditySensor || { CurrentRelativeHumidity: undefined };
158
+ if (context.scale !== 'CELCIUS' && device.hub?.convertUnitTo === undefined) {
159
+ this.warnLog(`${this.device.deviceType}: ${this.accessory.displayName} received a non-CELCIUS Webhook scale: `
160
+ + `${context.scale}, Use the *convertUnitsTo* config under Hub settings, if displaying incorrectly in HomeKit.`);
161
+ }
162
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ` +
163
+ '(scale, temperature, humidity, lightLevel) = ' +
164
+ `Webhook:(${context.scale}, ${convertUnits(temperature, context.scale, device.hub?.convertUnitTo)}, ${humidity}, ${lightLevel}), ` +
161
165
  `current:(${CurrentTemperature}, ${CurrentRelativeHumidity}, ${CurrentAmbientLightLevel})`);
162
- if (!this.device.hub?.hide_humidity) {
166
+ if (!this.device.hub?.hide_humidity) {
163
167
  this.HumiditySensor!.CurrentRelativeHumidity = humidity;
164
- }
165
- if (!this.device.hub?.hide_temperature) {
166
- this.TemperatureSensor!.CurrentTemperature = temperature;
167
- }
168
- if (!this.device.hub?.hide_lightsensor) {
169
- const set_minLux = await this.minLux();
170
- const set_maxLux = await this.maxLux();
171
- const spaceBetweenLevels = 19;
172
- await this.getLightLevel(lightLevel, set_minLux, set_maxLux, spaceBetweenLevels);
173
- }
174
- this.updateHomeKitCharacteristics();
175
168
  }
169
+ if (!this.device.hub?.hide_temperature) {
170
+ this.TemperatureSensor!.CurrentTemperature = convertUnits(temperature, context.scale, device.hub?.convertUnitTo);
171
+ }
172
+ if (!this.device.hub?.hide_lightsensor) {
173
+ const set_minLux = await this.minLux();
174
+ const set_maxLux = await this.maxLux();
175
+ const spaceBetweenLevels = 19;
176
+ await this.getLightLevel(lightLevel, set_minLux, set_maxLux, spaceBetweenLevels);
177
+ }
178
+ this.updateHomeKitCharacteristics();
176
179
  } catch (e: any) {
177
180
  this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} `
178
181
  + `failed to handle webhook. Received: ${JSON.stringify(context)} Error: ${e}`);
@@ -215,6 +218,11 @@ export class Hub extends deviceBase {
215
218
 
216
219
  // FirmwareRevision
217
220
  this.accessory.context.FirmwareRevision = deviceStatus.body.version;
221
+ this.accessory
222
+ .getService(this.hap.Service.AccessoryInformation)!
223
+ .setCharacteristic(this.hap.Characteristic.FirmwareRevision, this.accessory.context.FirmwareRevision)
224
+ .getCharacteristic(this.hap.Characteristic.FirmwareRevision)
225
+ .updateValue(this.accessory.context.FirmwareRevision);
218
226
  }
219
227
 
220
228
  async refreshStatus(): Promise<void> {
@@ -143,8 +143,8 @@ export class Humidifier extends deviceBase {
143
143
  });
144
144
 
145
145
  //regisiter webhook event handler
146
- if (this.device.webhook) {
147
- this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} is listening webhook.`);
146
+ if (device.webhook) {
147
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} is listening webhook.`);
148
148
  this.platform.webhookEventHandler[this.device.deviceId] = async (context) => {
149
149
  try {
150
150
  this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} received Webhook: ${JSON.stringify(context)}`);
@@ -270,6 +270,11 @@ export class Humidifier extends deviceBase {
270
270
 
271
271
  // FirmwareRevision
272
272
  this.accessory.context.FirmwareRevision = deviceStatus.body.version;
273
+ this.accessory
274
+ .getService(this.hap.Service.AccessoryInformation)!
275
+ .setCharacteristic(this.hap.Characteristic.FirmwareRevision, this.accessory.context.FirmwareRevision)
276
+ .getCharacteristic(this.hap.Characteristic.FirmwareRevision)
277
+ .updateValue(this.accessory.context.FirmwareRevision);
273
278
  }
274
279
 
275
280
  async refreshStatus(): Promise<void> {
@@ -670,7 +675,7 @@ export class Humidifier extends deviceBase {
670
675
  } else {
671
676
  this.TemperatureSensor!.Service.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, this.TemperatureSensor!.CurrentTemperature);
672
677
  this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic`
673
- + ` CurrentTemperature: ${this.TemperatureSensor!.CurrentTemperature}`);
678
+ + ` CurrentTemperature: ${this.TemperatureSensor!.CurrentTemperature}`);
674
679
  this.accessory.context.CurrentTemperature = this.TemperatureSensor!.CurrentTemperature;
675
680
  }
676
681
  }