@switchbot/homebridge-switchbot 3.5.0-beta.9 → 3.5.1-beta.0

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 (177) hide show
  1. package/.vscode/settings.json +4 -1
  2. package/CHANGELOG.md +4 -1
  3. package/config.schema.json +164 -22
  4. package/dist/device/blindtilt.d.ts +21 -96
  5. package/dist/device/blindtilt.d.ts.map +1 -1
  6. package/dist/device/blindtilt.js +457 -866
  7. package/dist/device/blindtilt.js.map +1 -1
  8. package/dist/device/bot.d.ts +27 -72
  9. package/dist/device/bot.d.ts.map +1 -1
  10. package/dist/device/bot.js +851 -913
  11. package/dist/device/bot.js.map +1 -1
  12. package/dist/device/ceilinglight.d.ts +15 -69
  13. package/dist/device/ceilinglight.d.ts.map +1 -1
  14. package/dist/device/ceilinglight.js +316 -742
  15. package/dist/device/ceilinglight.js.map +1 -1
  16. package/dist/device/colorbulb.d.ts +15 -76
  17. package/dist/device/colorbulb.d.ts.map +1 -1
  18. package/dist/device/colorbulb.js +380 -833
  19. package/dist/device/colorbulb.js.map +1 -1
  20. package/dist/device/contact.d.ts +14 -66
  21. package/dist/device/contact.d.ts.map +1 -1
  22. package/dist/device/contact.js +267 -602
  23. package/dist/device/contact.js.map +1 -1
  24. package/dist/device/curtain.d.ts +18 -89
  25. package/dist/device/curtain.d.ts.map +1 -1
  26. package/dist/device/curtain.js +469 -897
  27. package/dist/device/curtain.js.map +1 -1
  28. package/dist/device/device.d.ts +76 -0
  29. package/dist/device/device.d.ts.map +1 -0
  30. package/dist/device/device.js +846 -0
  31. package/dist/device/device.js.map +1 -0
  32. package/dist/device/fan.d.ts +63 -0
  33. package/dist/device/fan.d.ts.map +1 -0
  34. package/dist/device/fan.js +645 -0
  35. package/dist/device/fan.js.map +1 -0
  36. package/dist/device/hub.d.ts +19 -58
  37. package/dist/device/hub.d.ts.map +1 -1
  38. package/dist/device/hub.js +304 -640
  39. package/dist/device/hub.js.map +1 -1
  40. package/dist/device/humidifier.d.ts +11 -66
  41. package/dist/device/humidifier.d.ts.map +1 -1
  42. package/dist/device/humidifier.js +300 -710
  43. package/dist/device/humidifier.js.map +1 -1
  44. package/dist/device/iosensor.d.ts +12 -64
  45. package/dist/device/iosensor.d.ts.map +1 -1
  46. package/dist/device/iosensor.js +225 -643
  47. package/dist/device/iosensor.js.map +1 -1
  48. package/dist/device/lightstrip.d.ts +12 -70
  49. package/dist/device/lightstrip.d.ts.map +1 -1
  50. package/dist/device/lightstrip.js +371 -742
  51. package/dist/device/lightstrip.js.map +1 -1
  52. package/dist/device/lock.d.ts +17 -63
  53. package/dist/device/lock.d.ts.map +1 -1
  54. package/dist/device/lock.js +309 -642
  55. package/dist/device/lock.js.map +1 -1
  56. package/dist/device/meter.d.ts +15 -64
  57. package/dist/device/meter.d.ts.map +1 -1
  58. package/dist/device/meter.js +223 -638
  59. package/dist/device/meter.js.map +1 -1
  60. package/dist/device/meterplus.d.ts +15 -65
  61. package/dist/device/meterplus.d.ts.map +1 -1
  62. package/dist/device/meterplus.js +216 -644
  63. package/dist/device/meterplus.js.map +1 -1
  64. package/dist/device/motion.d.ts +12 -69
  65. package/dist/device/motion.d.ts.map +1 -1
  66. package/dist/device/motion.js +209 -588
  67. package/dist/device/motion.js.map +1 -1
  68. package/dist/device/plug.d.ts +10 -57
  69. package/dist/device/plug.d.ts.map +1 -1
  70. package/dist/device/plug.js +141 -532
  71. package/dist/device/plug.js.map +1 -1
  72. package/dist/device/robotvacuumcleaner.d.ts +13 -63
  73. package/dist/device/robotvacuumcleaner.d.ts.map +1 -1
  74. package/dist/device/robotvacuumcleaner.js +252 -609
  75. package/dist/device/robotvacuumcleaner.js.map +1 -1
  76. package/dist/device/waterdetector.d.ts +11 -63
  77. package/dist/device/waterdetector.d.ts.map +1 -1
  78. package/dist/device/waterdetector.js +179 -521
  79. package/dist/device/waterdetector.js.map +1 -1
  80. package/dist/homebridge-ui/public/index.html +22 -16
  81. package/dist/homebridge-ui/server.js +1 -1
  82. package/dist/homebridge-ui/server.js.map +1 -1
  83. package/dist/index.d.ts +1 -1
  84. package/dist/index.d.ts.map +1 -1
  85. package/dist/index.js +2 -2
  86. package/dist/index.js.map +1 -1
  87. package/dist/irdevice/airconditioner.d.ts +10 -50
  88. package/dist/irdevice/airconditioner.d.ts.map +1 -1
  89. package/dist/irdevice/airconditioner.js +337 -482
  90. package/dist/irdevice/airconditioner.js.map +1 -1
  91. package/dist/irdevice/airpurifier.d.ts +8 -39
  92. package/dist/irdevice/airpurifier.d.ts.map +1 -1
  93. package/dist/irdevice/airpurifier.js +108 -333
  94. package/dist/irdevice/airpurifier.js.map +1 -1
  95. package/dist/irdevice/camera.d.ts +7 -36
  96. package/dist/irdevice/camera.d.ts.map +1 -1
  97. package/dist/irdevice/camera.js +47 -299
  98. package/dist/irdevice/camera.js.map +1 -1
  99. package/dist/irdevice/fan.d.ts +7 -43
  100. package/dist/irdevice/fan.d.ts.map +1 -1
  101. package/dist/irdevice/fan.js +95 -364
  102. package/dist/irdevice/fan.js.map +1 -1
  103. package/dist/irdevice/irdevice.d.ts +46 -0
  104. package/dist/irdevice/irdevice.d.ts.map +1 -0
  105. package/dist/irdevice/irdevice.js +393 -0
  106. package/dist/irdevice/irdevice.js.map +1 -0
  107. package/dist/irdevice/light.d.ts +12 -45
  108. package/dist/irdevice/light.d.ts.map +1 -1
  109. package/dist/irdevice/light.js +121 -376
  110. package/dist/irdevice/light.js.map +1 -1
  111. package/dist/irdevice/other.d.ts +19 -48
  112. package/dist/irdevice/other.d.ts.map +1 -1
  113. package/dist/irdevice/other.js +441 -584
  114. package/dist/irdevice/other.js.map +1 -1
  115. package/dist/irdevice/tv.d.ts +8 -40
  116. package/dist/irdevice/tv.d.ts.map +1 -1
  117. package/dist/irdevice/tv.js +94 -353
  118. package/dist/irdevice/tv.js.map +1 -1
  119. package/dist/irdevice/vacuumcleaner.d.ts +7 -36
  120. package/dist/irdevice/vacuumcleaner.d.ts.map +1 -1
  121. package/dist/irdevice/vacuumcleaner.js +48 -299
  122. package/dist/irdevice/vacuumcleaner.js.map +1 -1
  123. package/dist/irdevice/waterheater.d.ts +7 -36
  124. package/dist/irdevice/waterheater.d.ts.map +1 -1
  125. package/dist/irdevice/waterheater.js +50 -305
  126. package/dist/irdevice/waterheater.js.map +1 -1
  127. package/dist/platform.d.ts +22 -8
  128. package/dist/platform.d.ts.map +1 -1
  129. package/dist/platform.js +342 -191
  130. package/dist/platform.js.map +1 -1
  131. package/dist/settings.d.ts +66 -18
  132. package/dist/settings.d.ts.map +1 -1
  133. package/dist/settings.js +16 -552
  134. package/dist/settings.js.map +1 -1
  135. package/dist/utils.d.ts +95 -0
  136. package/dist/utils.d.ts.map +1 -1
  137. package/dist/utils.js +657 -1
  138. package/dist/utils.js.map +1 -1
  139. package/eslint.config.js +96 -0
  140. package/package.json +20 -16
  141. package/src/custom.d.ts +2 -1
  142. package/src/device/blindtilt.ts +488 -937
  143. package/src/device/bot.ts +907 -1016
  144. package/src/device/ceilinglight.ts +330 -798
  145. package/src/device/colorbulb.ts +394 -904
  146. package/src/device/contact.ts +298 -637
  147. package/src/device/curtain.ts +496 -954
  148. package/src/device/device.ts +872 -0
  149. package/src/device/fan.ts +692 -0
  150. package/src/device/hub.ts +338 -671
  151. package/src/device/humidifier.ts +330 -777
  152. package/src/device/iosensor.ts +258 -671
  153. package/src/device/lightstrip.ts +394 -809
  154. package/src/device/lock.ts +345 -700
  155. package/src/device/meter.ts +256 -666
  156. package/src/device/meterplus.ts +248 -672
  157. package/src/device/motion.ts +234 -622
  158. package/src/device/plug.ts +152 -572
  159. package/src/device/robotvacuumcleaner.ts +273 -653
  160. package/src/device/waterdetector.ts +198 -549
  161. package/src/homebridge-ui/public/index.html +22 -16
  162. package/src/homebridge-ui/server.ts +1 -1
  163. package/src/index.ts +4 -3
  164. package/src/irdevice/airconditioner.ts +368 -540
  165. package/src/irdevice/airpurifier.ts +134 -369
  166. package/src/irdevice/camera.ts +60 -324
  167. package/src/irdevice/fan.ts +111 -392
  168. package/src/irdevice/irdevice.ts +406 -0
  169. package/src/irdevice/light.ts +159 -425
  170. package/src/irdevice/other.ts +509 -661
  171. package/src/irdevice/tv.ts +123 -388
  172. package/src/irdevice/vacuumcleaner.ts +61 -324
  173. package/src/irdevice/waterheater.ts +63 -333
  174. package/src/platform.ts +395 -259
  175. package/src/settings.ts +120 -610
  176. package/src/utils.ts +661 -1
  177. package/src/device/waterdetector.json +0 -11
@@ -0,0 +1,692 @@
1
+ /* Copyright(C) 2021-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved.
2
+ *
3
+ * plug.ts: @switchbot/homebridge-switchbot.
4
+ */
5
+ import { deviceBase } from './device.js';
6
+ import { Subject, debounceTime, interval, skipWhile, take, tap } from 'rxjs';
7
+
8
+ import type { SwitchBotPlatform } from '../platform.js';
9
+ import type { CharacteristicValue, PlatformAccessory, Service } from 'homebridge';
10
+ import type { device, devicesConfig, serviceData, deviceStatus } from '../settings.js';
11
+
12
+ export class Fan extends deviceBase {
13
+ // Services
14
+ private Fan: {
15
+ Name: CharacteristicValue;
16
+ Service: Service;
17
+ Active: CharacteristicValue;
18
+ SwingMode: CharacteristicValue;
19
+ RotationSpeed: CharacteristicValue;
20
+ };
21
+
22
+ private Battery: {
23
+ Name: CharacteristicValue;
24
+ Service: Service;
25
+ BatteryLevel: CharacteristicValue;
26
+ StatusLowBattery: CharacteristicValue;
27
+ ChargingState: CharacteristicValue;
28
+ };
29
+
30
+ private LightBulb: {
31
+ Name: CharacteristicValue;
32
+ Service: Service;
33
+ On: CharacteristicValue;
34
+ Brightness: CharacteristicValue;
35
+ };
36
+
37
+ // Updates
38
+ fanUpdateInProgress!: boolean;
39
+ doFanUpdate!: Subject<void>;
40
+
41
+ constructor(
42
+ readonly platform: SwitchBotPlatform,
43
+ accessory: PlatformAccessory,
44
+ device: device & devicesConfig,
45
+ ) {
46
+ super(platform, accessory, device);
47
+ // this is subject we use to track when we need to POST changes to the SwitchBot API
48
+ this.doFanUpdate = new Subject();
49
+ this.fanUpdateInProgress = false;
50
+
51
+ // Initialize Fan Service
52
+ accessory.context.Fan = accessory.context.Fan ?? {};
53
+ this.Fan = {
54
+ Name: accessory.context.Fan.Name ?? accessory.displayName,
55
+ Service: accessory.getService(this.hap.Service.Fanv2) ?? accessory.addService(this.hap.Service.Fanv2) as Service,
56
+ Active: accessory.context.Active ?? this.hap.Characteristic.Active.INACTIVE,
57
+ SwingMode: accessory.context.SwingMode ?? this.hap.Characteristic.SwingMode.SWING_DISABLED,
58
+ RotationSpeed: accessory.context.RotationSpeed ?? 0,
59
+ };
60
+ accessory.context.Fan = this.Fan as object;
61
+
62
+ // Initialize Fan Service
63
+ this.Fan.Service
64
+ .setCharacteristic(this.hap.Characteristic.Name, this.Fan.Name)
65
+ .getCharacteristic(this.hap.Characteristic.Active)
66
+ .onGet(() => {
67
+ return this.Fan.Active;
68
+ })
69
+ .onSet(this.ActiveSet.bind(this));
70
+
71
+ // Initialize Fan RotationSpeed Characteristic
72
+ this.Fan.Service
73
+ .getCharacteristic(this.hap.Characteristic.RotationSpeed)
74
+ .onGet(() => {
75
+ return this.Fan.RotationSpeed;
76
+ })
77
+ .onSet(this.RotationSpeedSet.bind(this));
78
+
79
+ // Initialize Fan SwingMode Characteristic
80
+ this.Fan.Service.getCharacteristic(this.hap.Characteristic.SwingMode)
81
+ .onGet(() => {
82
+ return this.Fan.SwingMode;
83
+ })
84
+ .onSet(this.SwingModeSet.bind(this));
85
+
86
+ // Initialize Battery Service
87
+ accessory.context.Battery = accessory.context.Battery ?? {};
88
+ this.Battery = {
89
+ Name: accessory.context.Battery.Name ?? accessory.displayName,
90
+ Service: accessory.getService(this.hap.Service.Battery) ?? accessory.addService(this.hap.Service.Battery) as Service,
91
+ BatteryLevel: accessory.context.BatteryLevel ?? 100,
92
+ StatusLowBattery: accessory.context.StatusLowBattery ?? this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL,
93
+ ChargingState: accessory.context.ChargingState ?? this.hap.Characteristic.ChargingState.NOT_CHARGING,
94
+ };
95
+ accessory.context.Battery = this.Battery as object;
96
+
97
+ // Initialize Battery Service
98
+ this.Battery.Service
99
+ .setCharacteristic(this.hap.Characteristic.Name, this.Battery.Name)
100
+ .getCharacteristic(this.hap.Characteristic.BatteryLevel)
101
+ .onGet(() => {
102
+ return this.Battery.BatteryLevel;
103
+ });
104
+
105
+ // Initialize Battery ChargingState Characteristic
106
+ this.Battery.Service
107
+ .getCharacteristic(this.hap.Characteristic.ChargingState)
108
+ .onGet(() => {
109
+ return this.Battery.ChargingState;
110
+ });
111
+
112
+ // Initialize Battery StatusLowBattery Characteristic
113
+ this.Battery.Service
114
+ .getCharacteristic(this.hap.Characteristic.StatusLowBattery)
115
+ .onGet(() => {
116
+ return this.Battery.StatusLowBattery;
117
+ });
118
+
119
+ // Initialize LightBulb Service
120
+ accessory.context.LightBulb = accessory.context.LightBulb ?? {};
121
+ this.LightBulb = {
122
+ Name: accessory.context.LightBulb.Name ?? accessory.displayName,
123
+ Service: accessory.getService(this.hap.Service.Lightbulb) ?? accessory.addService(this.hap.Service.Lightbulb) as Service,
124
+ On: accessory.context.On ?? false,
125
+ Brightness: accessory.context.Brightness ?? 0,
126
+ };
127
+ accessory.context.LightBulb = this.LightBulb as object;
128
+
129
+ // Initialize LightBulb Characteristics
130
+ this.LightBulb.Service
131
+ .setCharacteristic(this.hap.Characteristic.Name, this.LightBulb.Name)
132
+ .getCharacteristic(this.hap.Characteristic.On)
133
+ .onGet(() => {
134
+ return this.LightBulb.On;
135
+ })
136
+ .onSet(this.OnSet.bind(this));
137
+
138
+ // Initialize LightBulb Brightness Characteristic
139
+ this.LightBulb.Service
140
+ .getCharacteristic(this.hap.Characteristic.Brightness)
141
+ .onGet(() => {
142
+ return this.LightBulb.Brightness;
143
+ })
144
+ .onSet(this.BrightnessSet.bind(this));
145
+
146
+ // Retrieve initial values and updateHomekit
147
+ this.refreshStatus();
148
+
149
+ // Update Homekit
150
+ this.updateHomeKitCharacteristics();
151
+
152
+ // Start an update interval
153
+ interval(this.deviceRefreshRate * 1000)
154
+ .pipe(skipWhile(() => this.fanUpdateInProgress))
155
+ .subscribe(async () => {
156
+ await this.refreshStatus();
157
+ });
158
+
159
+ //regisiter webhook event handler
160
+ this.registerWebhook(accessory, device);
161
+
162
+ // Watch for Plug change events
163
+ // We put in a debounce of 100ms so we don't make duplicate calls
164
+ this.doFanUpdate
165
+ .pipe(
166
+ tap(() => {
167
+ this.fanUpdateInProgress = true;
168
+ }),
169
+ debounceTime(this.devicePushRate * 1000),
170
+ )
171
+ .subscribe(async () => {
172
+ try {
173
+ await this.pushChanges();
174
+ } catch (e: any) {
175
+ this.apiError(e);
176
+ this.errorLog(`${device.deviceType}: ${accessory.displayName} failed pushChanges with ${device.connectionType} Connection,`
177
+ + ` Error Message: ${JSON.stringify(e.message)}`);
178
+ }
179
+ this.fanUpdateInProgress = false;
180
+ });
181
+ }
182
+
183
+ async BLEparseStatus(serviceData: serviceData): Promise<void> {
184
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEparseStatus`);
185
+
186
+ // State
187
+ switch (serviceData.state) {
188
+ case 'on':
189
+ this.Fan.Active = true;
190
+ break;
191
+ default:
192
+ this.Fan.Active = false;
193
+ }
194
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.Fan.Active}`);
195
+ }
196
+
197
+ async openAPIparseStatus(deviceStatus: deviceStatus) {
198
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIparseStatus`);
199
+
200
+ // Active
201
+ this.Fan.Active = deviceStatus.body.power === 'on' ? this.hap.Characteristic.Active.ACTIVE : this.hap.Characteristic.Active.INACTIVE;
202
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Active: ${this.Fan.Active}`);
203
+
204
+ // SwingMode
205
+ this.Fan.SwingMode = deviceStatus.body.oscillation === 'on' ?
206
+ this.hap.Characteristic.SwingMode.SWING_ENABLED : this.hap.Characteristic.SwingMode.SWING_DISABLED;
207
+
208
+ // RotationSpeed
209
+ this.Fan.RotationSpeed = deviceStatus.body.fanSpeed;
210
+
211
+ // ChargingState
212
+ this.Battery.ChargingState = deviceStatus.body.chargingStatus === 'charging' ?
213
+ this.hap.Characteristic.ChargingState.CHARGING : this.hap.Characteristic.ChargingState.NOT_CHARGING;
214
+
215
+ // BatteryLevel
216
+ this.Battery.BatteryLevel = Number(deviceStatus.body.battery);
217
+ if (this.Battery.BatteryLevel < 10) {
218
+ this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW;
219
+ } else {
220
+ this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL;
221
+ }
222
+ if (Number.isNaN(this.Battery.BatteryLevel)) {
223
+ this.Battery.BatteryLevel = 100;
224
+ }
225
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.Battery.BatteryLevel},`
226
+ + ` StatusLowBattery: ${this.Battery.StatusLowBattery}`);
227
+
228
+ // Firmware Version
229
+ const version = deviceStatus.body.version?.toString();
230
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Firmware Version: ${version?.replace(/^V|-.*$/g, '')}`);
231
+ if (deviceStatus.body.version) {
232
+ const deviceVersion = version?.replace(/^V|-.*$/g, '') ?? '0.0.0';
233
+ this.accessory
234
+ .getService(this.hap.Service.AccessoryInformation)!
235
+ .setCharacteristic(this.hap.Characteristic.HardwareRevision, deviceVersion)
236
+ .setCharacteristic(this.hap.Characteristic.FirmwareRevision, deviceVersion)
237
+ .getCharacteristic(this.hap.Characteristic.FirmwareRevision)
238
+ .updateValue(deviceVersion);
239
+ this.accessory.context.deviceVersion = deviceVersion;
240
+ this.debugSuccessLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceVersion: ${this.accessory.context.deviceVersion}`);
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Asks the SwitchBot API for the latest device information
246
+ */
247
+ async refreshStatus(): Promise<void> {
248
+ if (!this.device.enableCloudService && this.OpenAPI) {
249
+ this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} refreshStatus enableCloudService: ${this.device.enableCloudService}`);
250
+ } else if (this.BLE) {
251
+ await this.BLERefreshStatus();
252
+ } else if (this.OpenAPI && this.platform.config.credentials?.token) {
253
+ await this.openAPIRefreshStatus();
254
+ } else {
255
+ await this.offlineOff();
256
+ this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} Connection Type:`
257
+ + ` ${this.device.connectionType}, refreshStatus will not happen.`);
258
+ }
259
+ }
260
+
261
+ async BLERefreshStatus(): Promise<void> {
262
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLERefreshStatus`);
263
+ const switchbot = await this.switchbotBLE();
264
+
265
+ if (switchbot === undefined) {
266
+ await this.BLERefreshConnection(switchbot);
267
+ } else {
268
+ // Start to monitor advertisement packets
269
+ (async () => {
270
+ // Start to monitor advertisement packets
271
+ const serviceData: serviceData = await this.monitorAdvertisementPackets(switchbot);
272
+ // Update HomeKit
273
+ if (serviceData.model !== '' && serviceData.modelName !== '') {
274
+ await this.BLEparseStatus(serviceData);
275
+ await this.updateHomeKitCharacteristics();
276
+ } else {
277
+ this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed to get serviceData, serviceData: ${serviceData}`);
278
+ await this.BLERefreshConnection(switchbot);
279
+ }
280
+ })();
281
+ }
282
+ }
283
+
284
+ async openAPIRefreshStatus(): Promise<void> {
285
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIRefreshStatus`);
286
+ try {
287
+ const { body, statusCode } = await this.deviceRefreshStatus();
288
+ const deviceStatus: any = await body.json();
289
+ await this.refreshStatusCodes(statusCode, deviceStatus);;
290
+ if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) {
291
+ await this.successfulRefreshStatus(statusCode, deviceStatus);
292
+ await this.openAPIparseStatus(deviceStatus);
293
+ await this.updateHomeKitCharacteristics();
294
+ } else {
295
+ await this.statusCodes(statusCode, deviceStatus);
296
+ }
297
+ } catch (e: any) {
298
+ this.apiError(e);
299
+ this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed openAPIRefreshStatus with ${this.device.connectionType}`
300
+ + ` Connection, Error Message: ${JSON.stringify(e.message)}`);
301
+ }
302
+ }
303
+
304
+ async registerWebhook(accessory: PlatformAccessory, device: device & devicesConfig) {
305
+ if (device.webhook) {
306
+ this.debugLog(`${device.deviceType}: ${accessory.displayName} is listening webhook.`);
307
+ this.platform.webhookEventHandler[device.deviceId] = async (context) => {
308
+ try {
309
+ this.debugLog(`${device.deviceType}: ${accessory.displayName} received Webhook: ${JSON.stringify(context)}`);
310
+ const { version, battery, powerState, oscillation, chargingStatus, fanSpeed } = context;
311
+ const { Active, SwingMode, RotationSpeed } = this.Fan;
312
+ const { BatteryLevel, ChargingState } = this.Battery;
313
+ const { FirmwareRevision } = accessory.context;
314
+ this.debugLog(`${device.deviceType}: ${accessory.displayName} (version, battery, powerState, oscillation, chargingStatus, fanSpeed) = `
315
+ + `Webhook:(${version}, ${battery}, ${powerState}, ${oscillation}, ${chargingStatus}, ${fanSpeed}), `
316
+ + `current:(${FirmwareRevision}, ${BatteryLevel}, ${Active}, ${SwingMode}, ${ChargingState}, ${RotationSpeed})`);
317
+
318
+ // Active
319
+ this.Fan.Active = powerState === 'ON' ? this.hap.Characteristic.Active.ACTIVE : this.hap.Characteristic.Active.INACTIVE;
320
+
321
+ // SwingMode
322
+ this.Fan.SwingMode = oscillation === 'on' ?
323
+ this.hap.Characteristic.SwingMode.SWING_ENABLED : this.hap.Characteristic.SwingMode.SWING_DISABLED;
324
+
325
+ // RotationSpeed
326
+ this.Fan.RotationSpeed = fanSpeed;
327
+
328
+ // ChargingState
329
+ this.Battery.ChargingState = chargingStatus === 'charging' ?
330
+ this.hap.Characteristic.ChargingState.CHARGING : this.hap.Characteristic.ChargingState.NOT_CHARGING;
331
+
332
+ // BatteryLevel
333
+ this.Battery.BatteryLevel = battery;
334
+
335
+ // Firmware Version
336
+ if (version) {
337
+ accessory.context.version = version;
338
+ accessory
339
+ .getService(this.hap.Service.AccessoryInformation)!
340
+ .setCharacteristic(this.hap.Characteristic.FirmwareRevision, accessory.context.version)
341
+ .getCharacteristic(this.hap.Characteristic.FirmwareRevision)
342
+ .updateValue(accessory.context.version);
343
+ }
344
+ this.updateHomeKitCharacteristics();
345
+ } catch (e: any) {
346
+ this.errorLog(`${device.deviceType}: ${accessory.displayName} failed to handle webhook. Received: ${JSON.stringify(context)} Error: ${e}`);
347
+ }
348
+ };
349
+ } else {
350
+ this.debugLog(`${device.deviceType}: ${accessory.displayName} is not listening webhook.`);
351
+ }
352
+ }
353
+
354
+ /**
355
+ * Pushes the requested changes to the SwitchBot API
356
+ * commandType command parameter Description
357
+ * "command" "turnOff" "default" = set to OFF state
358
+ * "command" "turnOn" "default" = set to ON state
359
+ * "command" "setNightLightMode" "off, 1, or 2" = off, turn off nightlight, (1, bright) (2, dim)
360
+ * "command" "setWindMode" "direct, natural, sleep, or baby" = Set fan mode
361
+ * "command" "setWindSpeed" "{1-100} e.g. 10" = Set fan speed 1~100
362
+ */
363
+
364
+ async pushChanges(): Promise<void> {
365
+ if (!this.device.enableCloudService && this.OpenAPI) {
366
+ this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} pushChanges enableCloudService: ${this.device.enableCloudService}`);
367
+ } else if (this.BLE) {
368
+ await this.BLEpushChanges();
369
+ } else if (this.OpenAPI && this.platform.config.credentials?.token) {
370
+ await this.openAPIpushChanges();
371
+ } else {
372
+ await this.offlineOff();
373
+ this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} Connection Type:`
374
+ + ` ${this.device.connectionType}, pushChanges will not happen.`);
375
+ }
376
+ // Refresh the status from the API
377
+ interval(15000)
378
+ .pipe(skipWhile(() => this.fanUpdateInProgress))
379
+ .pipe(take(1))
380
+ .subscribe(async () => {
381
+ await this.refreshStatus();
382
+ });
383
+ }
384
+
385
+ async BLEpushChanges(): Promise<void> {
386
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEpushChanges`);
387
+ if (this.Fan.Active !== this.accessory.context.Active) {
388
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEpushChanges`
389
+ + ` On: ${this.Fan.Active} OnCached: ${this.accessory.context.Active}`);
390
+ const switchbot = await this.platform.connectBLE();
391
+ // Convert to BLE Address
392
+ await this.convertBLEAddress();
393
+ switchbot
394
+ .discover({ model: this.device.bleModel, id: this.device.bleMac })
395
+ .then(async (device_list: any) => {
396
+ this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.Fan.Active}`);
397
+ return await this.retryBLE({
398
+ max: await this.maxRetryBLE(),
399
+ fn: async () => {
400
+ if (this.Fan.Active) {
401
+ return await device_list[0].turnOn({ id: this.device.bleMac });
402
+ } else {
403
+ return await device_list[0].turnOff({ id: this.device.bleMac });
404
+ }
405
+ },
406
+ });
407
+ })
408
+ .then(() => {
409
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Done.`);
410
+ this.successLog(`${this.device.deviceType}: ${this.accessory.displayName} `
411
+ + `Active: ${this.Fan.Active} sent over BLE, sent successfully`);
412
+ this.Fan.Active = false;
413
+ })
414
+ .catch(async (e: any) => {
415
+ this.apiError(e);
416
+ this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed BLEpushChanges with ${this.device.connectionType}`
417
+ + ` Connection, Error Message: ${JSON.stringify(e.message)}`);
418
+ await this.BLEPushConnection();
419
+ });
420
+ } else {
421
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No BLEpushChanges,`
422
+ + ` Active: ${this.Fan.Active}, ActiveCached: ${this.accessory.context.Active}`);
423
+ }
424
+ }
425
+
426
+ async openAPIpushChanges() {
427
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIpushChanges`);
428
+ if (this.Fan.Active !== this.accessory.context.Active) {
429
+ let command = '';
430
+ if (this.Fan.Active) {
431
+ command = 'turnOn';
432
+ } else {
433
+ command = 'turnOff';
434
+ }
435
+ const bodyChange = JSON.stringify({
436
+ command: `${command}`,
437
+ parameter: 'default',
438
+ commandType: 'command',
439
+ });
440
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Sending request to SwitchBot API, body: ${bodyChange},`);
441
+ try {
442
+ const { body, statusCode } = await this.pushChangeRequest(bodyChange);
443
+ const deviceStatus: any = await body.json();
444
+ await this.pushStatusCodes(statusCode, deviceStatus);
445
+ if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) {
446
+ await this.successfulPushChange(statusCode, deviceStatus, bodyChange);
447
+ await this.updateHomeKitCharacteristics();
448
+ } else {
449
+ await this.statusCode(statusCode);
450
+ await this.statusCode(deviceStatus.statusCode);
451
+ }
452
+ } catch (e: any) {
453
+ this.apiError(e);
454
+ this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed openAPIpushChanges with ${this.device.connectionType}`
455
+ + ` Connection, Error Message: ${JSON.stringify(e.message)}`);
456
+ }
457
+ } else {
458
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No openAPIpushChanges.`
459
+ + `On: ${this.Fan.Active}, ActiveCached: ${this.accessory.context.Active}`);
460
+ }
461
+ // Push RotationSpeed Update
462
+ if (this.Fan.Active) {
463
+ await this.pushRotationSpeedChanges();
464
+ }
465
+ // Push SwingMode Update
466
+ if (this.Fan.Active) {
467
+ await this.pushSwingModeChanges();
468
+ }
469
+ }
470
+
471
+ async pushRotationSpeedChanges(): Promise<void> {
472
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} pushRotationSpeedChanges`);
473
+ if (this.Fan.SwingMode !== this.accessory.context.SwingMode) {
474
+ const bodyChange = JSON.stringify({
475
+ command: 'setWindSpeed',
476
+ parameter: `${this.Fan.RotationSpeed}`,
477
+ commandType: 'command',
478
+ });
479
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Sending request to SwitchBot API, body: ${bodyChange},`);
480
+ try {
481
+ const { body, statusCode } = await this.pushChangeRequest(bodyChange);
482
+ const deviceStatus: any = await body.json();
483
+ await this.pushStatusCodes(statusCode, deviceStatus);
484
+ if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) {
485
+ await this.successfulPushChange(statusCode, deviceStatus, bodyChange);
486
+ await this.updateHomeKitCharacteristics();
487
+ } else {
488
+ await this.statusCode(statusCode);
489
+ await this.statusCode(deviceStatus.statusCode);
490
+ }
491
+ } catch (e: any) {
492
+ this.apiError(e);
493
+ this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed pushRotationSpeedChanges with ${this.device.connectionType}`
494
+ + ` Connection, Error Message: ${JSON.stringify(e.message)}`);
495
+ }
496
+ } else {
497
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No Changes, pushRotationSpeedChanges: ${this.Fan.RotationSpeed}, `
498
+ + `pushRotationSpeedChangesCached: ${this.accessory.context.RotationSpeed}`);
499
+ }
500
+ }
501
+
502
+ async pushSwingModeChanges(): Promise<void> {
503
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} pushSwingModeChanges`);
504
+ if (this.Fan.SwingMode !== this.accessory.context.SwingMode) {
505
+ let parameter = '';
506
+ if (this.Fan.SwingMode === this.hap.Characteristic.SwingMode.SWING_ENABLED) {
507
+ parameter = 'on';
508
+ } else {
509
+ parameter = 'off';
510
+ }
511
+ const bodyChange = JSON.stringify({
512
+ command: 'setOscillation',
513
+ parameter: `${parameter}`,
514
+ commandType: 'command',
515
+ });
516
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Sending request to SwitchBot API, body: ${bodyChange},`);
517
+ try {
518
+ const { body, statusCode } = await this.pushChangeRequest(bodyChange);
519
+ const deviceStatus: any = await body.json();
520
+ await this.pushStatusCodes(statusCode, deviceStatus);
521
+ if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) {
522
+ await this.successfulPushChange(statusCode, deviceStatus, bodyChange);
523
+ await this.updateHomeKitCharacteristics();
524
+ } else {
525
+ await this.statusCode(statusCode);
526
+ await this.statusCode(deviceStatus.statusCode);
527
+ }
528
+ } catch (e: any) {
529
+ this.apiError(e);
530
+ this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed pushSwingModeChanges with ${this.device.connectionType}`
531
+ + ` Connection, Error Message: ${JSON.stringify(e.message)}`);
532
+ }
533
+ } else {
534
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No Changes, SwingMode: ${this.Fan.SwingMode}, `
535
+ + `SwingModeCached: ${this.accessory.context.SwingMode}`);
536
+ }
537
+ }
538
+
539
+ /**
540
+ * Handle requests to set the value of the "On" characteristic
541
+ */
542
+ async ActiveSet(value: CharacteristicValue): Promise<void> {
543
+ if (this.Fan.Active === this.accessory.context.Active) {
544
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No Changes, Set Active: ${value}`);
545
+ } else {
546
+ this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set Active: ${value}`);
547
+ }
548
+
549
+ this.Fan.Active = value;
550
+ this.doFanUpdate.next();
551
+ }
552
+
553
+ /**
554
+ * Handle requests to set the value of the "On" characteristic
555
+ */
556
+ async RotationSpeedSet(value: CharacteristicValue): Promise<void> {
557
+ if (this.Fan.RotationSpeed === this.accessory.context.RotationSpeed) {
558
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No Changes, Set RotationSpeed: ${value}`);
559
+ } else {
560
+ this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set RotationSpeed: ${value}`);
561
+ }
562
+
563
+ this.Fan.RotationSpeed = value;
564
+ this.doFanUpdate.next();
565
+ }
566
+
567
+ /**
568
+ * Handle requests to set the value of the "On" characteristic
569
+ */
570
+ async SwingModeSet(value: CharacteristicValue): Promise<void> {
571
+ if (this.Fan.SwingMode === this.accessory.context.SwingMode) {
572
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No Changes, Set SwingMode: ${value}`);
573
+ } else {
574
+ this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set SwingMode: ${value}`);
575
+ }
576
+
577
+ this.Fan.SwingMode = value;
578
+ this.doFanUpdate.next();
579
+ }
580
+
581
+ /**
582
+ * Handle requests to set the value of the "On" characteristic
583
+ */
584
+ async OnSet(value: CharacteristicValue): Promise<void> {
585
+ if (this.LightBulb.On === this.accessory.context.On) {
586
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No Changes, Set On: ${value}`);
587
+ } else {
588
+ this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set On: ${value}`);
589
+ }
590
+
591
+ this.LightBulb.On = value;
592
+ this.doFanUpdate.next();
593
+ }
594
+
595
+ /**
596
+ * Handle requests to set the value of the "Brightness" characteristic
597
+ */
598
+ async BrightnessSet(value: CharacteristicValue): Promise<void> {
599
+ if (this.LightBulb.Brightness === this.accessory.context.Brightness) {
600
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No Changes, Set Brightness: ${value}`);
601
+ } else if (this.LightBulb.On) {
602
+ this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set Brightness: ${value}`);
603
+ } else {
604
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Set Brightness: ${value}`);
605
+ }
606
+
607
+ this.LightBulb.Brightness = value;
608
+ this.doFanUpdate.next();
609
+ }
610
+
611
+ async updateHomeKitCharacteristics(): Promise<void> {
612
+ // Active
613
+ if (this.Fan.Active === undefined) {
614
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Active: ${this.Fan.Active}`);
615
+ } else {
616
+ this.accessory.context.Active = this.Fan.Active;
617
+ this.Fan.Service.updateCharacteristic(this.hap.Characteristic.Active, this.Fan.Active);
618
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic Active: ${this.Fan.Active}`);
619
+ }
620
+ // RotationSpeed
621
+ if (this.Fan.RotationSpeed === undefined) {
622
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} RotationSpeed: ${this.Fan.RotationSpeed}`);
623
+ } else {
624
+ this.accessory.context.RotationSpeed = this.Fan.RotationSpeed;
625
+ this.Fan.Service.updateCharacteristic(this.hap.Characteristic.RotationSpeed, this.Fan.RotationSpeed);
626
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic RotationSpeed: ${this.Fan.RotationSpeed}`);
627
+ }
628
+ // SwingMode
629
+ if (this.Fan.SwingMode === undefined) {
630
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} SwingMode: ${this.Fan.SwingMode}`);
631
+ } else {
632
+ this.accessory.context.SwingMode = this.Fan.SwingMode;
633
+ this.Fan.Service.updateCharacteristic(this.hap.Characteristic.SwingMode, this.Fan.SwingMode);
634
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic SwingMode: ${this.Fan.SwingMode}`);
635
+ }
636
+ // BateryLevel
637
+ if (this.Battery.BatteryLevel === undefined) {
638
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.Battery.BatteryLevel}`);
639
+ } else {
640
+ this.accessory.context.BatteryLevel = this.Battery.BatteryLevel;
641
+ this.Battery.Service.updateCharacteristic(this.hap.Characteristic.BatteryLevel, this.Battery.BatteryLevel);
642
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic BatteryLevel: ${this.Battery.BatteryLevel}`);
643
+ }
644
+ // ChargingState
645
+ if (this.Battery.ChargingState === undefined) {
646
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ChargingState: ${this.Battery.ChargingState}`);
647
+ } else {
648
+ this.accessory.context.ChargingState = this.Battery.ChargingState;
649
+ this.Battery.Service.updateCharacteristic(this.hap.Characteristic.ChargingState, this.Battery.ChargingState);
650
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic ChargingState: ${this.Battery.ChargingState}`);
651
+ }
652
+ // StatusLowBattery
653
+ if (this.Battery.StatusLowBattery === undefined) {
654
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} StatusLowBattery: ${this.Battery.StatusLowBattery}`);
655
+ } else {
656
+ this.accessory.context.StatusLowBattery = this.Battery.StatusLowBattery;
657
+ this.Battery.Service.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, this.Battery.StatusLowBattery);
658
+ this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic`
659
+ + ` StatusLowBattery: ${this.Battery.StatusLowBattery}`);
660
+ }
661
+ }
662
+
663
+ async BLEPushConnection() {
664
+ if (this.platform.config.credentials?.token && this.device.connectionType === 'BLE/OpenAPI') {
665
+ this.warnLog(`${this.device.deviceType}: ${this.accessory.displayName} Using OpenAPI Connection to Push Changes`);
666
+ await this.openAPIpushChanges();
667
+ }
668
+ }
669
+
670
+ async BLERefreshConnection(switchbot: any): Promise<void> {
671
+ this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} wasn't able to establish BLE Connection, node-switchbot:`
672
+ + ` ${JSON.stringify(switchbot)}`);
673
+ if (this.platform.config.credentials?.token && this.device.connectionType === 'BLE/OpenAPI') {
674
+ this.warnLog(`${this.device.deviceType}: ${this.accessory.displayName} Using OpenAPI Connection to Refresh Status`);
675
+ await this.openAPIRefreshStatus();
676
+ }
677
+ }
678
+
679
+ async offlineOff(): Promise<void> {
680
+ if (this.device.offline) {
681
+ this.Fan.Service.updateCharacteristic(this.hap.Characteristic.Active, this.hap.Characteristic.Active.INACTIVE);
682
+ this.Fan.Service.updateCharacteristic(this.hap.Characteristic.RotationSpeed, 0);
683
+ this.Fan.Service.updateCharacteristic(this.hap.Characteristic.SwingMode, this.hap.Characteristic.SwingMode.SWING_DISABLED);
684
+ }
685
+ }
686
+
687
+ async apiError(e: any): Promise<void> {
688
+ this.Fan.Service.updateCharacteristic(this.hap.Characteristic.Active, e);
689
+ this.Fan.Service.updateCharacteristic(this.hap.Characteristic.RotationSpeed, e);
690
+ this.Fan.Service.updateCharacteristic(this.hap.Characteristic.SwingMode, e);
691
+ }
692
+ }