homebridge-tuya-without-developer-account 1.0.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 (97) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/LICENSE +20 -0
  3. package/PUBLISHING.md +80 -0
  4. package/README.md +233 -0
  5. package/SUPPORTED_DEVICES.md +206 -0
  6. package/config.schema.json +131 -0
  7. package/dist/cloud/api/TuyaHACloudAPI.js +286 -0
  8. package/dist/cloud/api/TuyaHASharingMQ.js +114 -0
  9. package/dist/cloud/device/TuyaDevice.js +50 -0
  10. package/dist/cloud/device/TuyaHADeviceManager.js +355 -0
  11. package/dist/index.js +7 -0
  12. package/dist/platform.js +397 -0
  13. package/dist/settings.js +18 -0
  14. package/dist/shared/AccessoryFactory.js +276 -0
  15. package/dist/shared/accessories/AccessoryFactory.js +305 -0
  16. package/dist/shared/accessories/AirConditionerAccessory.js +307 -0
  17. package/dist/shared/accessories/AirPurifierAccessory.js +90 -0
  18. package/dist/shared/accessories/AirQualitySensorAccessory.js +30 -0
  19. package/dist/shared/accessories/BaseAccessory.js +406 -0
  20. package/dist/shared/accessories/BlindsAccessory.js +199 -0
  21. package/dist/shared/accessories/CameraAccessory.js +121 -0
  22. package/dist/shared/accessories/CarbonDioxideSensorAccessory.js +52 -0
  23. package/dist/shared/accessories/CarbonMonoxideSensorAccessory.js +52 -0
  24. package/dist/shared/accessories/ContactSensorAccessory.js +30 -0
  25. package/dist/shared/accessories/DehumidifierAccessory.js +68 -0
  26. package/dist/shared/accessories/DiffuserAccessory.js +55 -0
  27. package/dist/shared/accessories/DimmerAccessory.js +94 -0
  28. package/dist/shared/accessories/DoorbellAccessory.js +91 -0
  29. package/dist/shared/accessories/ExtractionHoodAccessory.js +120 -0
  30. package/dist/shared/accessories/FanAccessory.js +129 -0
  31. package/dist/shared/accessories/GarageDoorAccessory.js +69 -0
  32. package/dist/shared/accessories/HeaterAccessory.js +102 -0
  33. package/dist/shared/accessories/HeaterAccessory_old.js +96 -0
  34. package/dist/shared/accessories/HumanPresenceSensorAccessory.js +20 -0
  35. package/dist/shared/accessories/HumidifierAccessory.js +137 -0
  36. package/dist/shared/accessories/IRAirConditionerAccessory.js +278 -0
  37. package/dist/shared/accessories/IRControlHubAccessory.js +49 -0
  38. package/dist/shared/accessories/IRControlHubSubAccessory.js +52 -0
  39. package/dist/shared/accessories/IRGenericAccessory.js +49 -0
  40. package/dist/shared/accessories/LeakSensorAccessory.js +36 -0
  41. package/dist/shared/accessories/LightAccessory.js +36 -0
  42. package/dist/shared/accessories/LightSensorAccessory.js +32 -0
  43. package/dist/shared/accessories/LocationWeatherAccessory.js +72 -0
  44. package/dist/shared/accessories/LockAccessory.js +56 -0
  45. package/dist/shared/accessories/MotionSensorAccessory.js +20 -0
  46. package/dist/shared/accessories/OutletAccessory.js +23 -0
  47. package/dist/shared/accessories/PetFeederAccessory.js +139 -0
  48. package/dist/shared/accessories/SceneAccessory.js +32 -0
  49. package/dist/shared/accessories/SceneSwitchAccessory.js +44 -0
  50. package/dist/shared/accessories/SecuritySystemAccessory.js +30 -0
  51. package/dist/shared/accessories/SmokeSensorAccessory.js +35 -0
  52. package/dist/shared/accessories/SwitchAccessory.js +148 -0
  53. package/dist/shared/accessories/TemperatureHumiditySensorAccessory.js +24 -0
  54. package/dist/shared/accessories/ThermostatAccessory.js +192 -0
  55. package/dist/shared/accessories/TowerRackAccessory.js +157 -0
  56. package/dist/shared/accessories/ValveAccessory.js +45 -0
  57. package/dist/shared/accessories/VibrationSensorAccessory.js +46 -0
  58. package/dist/shared/accessories/WeatherStationAccessory.js +58 -0
  59. package/dist/shared/accessories/WetBulbGlobeTemperatureAccessory.js +23 -0
  60. package/dist/shared/accessories/WhiteNoiseLightAccessory.js +59 -0
  61. package/dist/shared/accessories/WindowAccessory.js +14 -0
  62. package/dist/shared/accessories/WindowCoveringAccessory.js +156 -0
  63. package/dist/shared/accessories/WirelessSwitchAccessory.js +42 -0
  64. package/dist/shared/accessories/characteristic/Active.js +22 -0
  65. package/dist/shared/accessories/characteristic/AirQuality.js +74 -0
  66. package/dist/shared/accessories/characteristic/CurrentRelativeHumidity.js +23 -0
  67. package/dist/shared/accessories/characteristic/CurrentTemperature.js +23 -0
  68. package/dist/shared/accessories/characteristic/CurrentWeather.js +49 -0
  69. package/dist/shared/accessories/characteristic/CurrentWeatherByOpenMeteo.js +49 -0
  70. package/dist/shared/accessories/characteristic/CurrentWetBulbGlobeTemperature.js +48 -0
  71. package/dist/shared/accessories/characteristic/EnergyUsage.js +98 -0
  72. package/dist/shared/accessories/characteristic/Light.js +268 -0
  73. package/dist/shared/accessories/characteristic/LightSensor.js +23 -0
  74. package/dist/shared/accessories/characteristic/LockPhysicalControls.js +21 -0
  75. package/dist/shared/accessories/characteristic/MotionDetected.js +22 -0
  76. package/dist/shared/accessories/characteristic/Name.js +15 -0
  77. package/dist/shared/accessories/characteristic/OccupancyDetected.js +19 -0
  78. package/dist/shared/accessories/characteristic/On.js +25 -0
  79. package/dist/shared/accessories/characteristic/OutletInUse.js +14 -0
  80. package/dist/shared/accessories/characteristic/ProgrammableSwitchEvent.js +89 -0
  81. package/dist/shared/accessories/characteristic/RelativeHumidityDehumidifierThreshold.js +28 -0
  82. package/dist/shared/accessories/characteristic/RotationSpeed.js +78 -0
  83. package/dist/shared/accessories/characteristic/SecuritySystemState.js +74 -0
  84. package/dist/shared/accessories/characteristic/SwingMode.js +21 -0
  85. package/dist/shared/accessories/characteristic/TargetTemperature.js +29 -0
  86. package/dist/shared/accessories/characteristic/TemperatureDisplayUnits.js +25 -0
  87. package/dist/shared/util/ConfigHash.js +79 -0
  88. package/dist/shared/util/FfmpegStreamingProcess.js +126 -0
  89. package/dist/shared/util/InfraredTool.js +392 -0
  90. package/dist/shared/util/Logger.js +42 -0
  91. package/dist/shared/util/TuyaRecordingDelegate.js +22 -0
  92. package/dist/shared/util/TuyaStreamDelegate.js +329 -0
  93. package/dist/shared/util/color.js +23 -0
  94. package/dist/shared/util/util.js +135 -0
  95. package/homebridge-ui/public/index.html +329 -0
  96. package/homebridge-ui/server.js +224 -0
  97. package/package.json +61 -0
@@ -0,0 +1,406 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const util_1 = require("../util/util");
4
+ const Logger_1 = require("../util/Logger");
5
+ const util_2 = require("../util/util");
6
+ const MANUFACTURER = 'Tuya Inc.';
7
+ const SCHEMA_CODE = {
8
+ BATTERY_STATE: ['battery_state'],
9
+ BATTERY_PERCENT: ['battery_percentage', 'residual_electricity', 'wireless_electricity', 'va_battery', 'battery'],
10
+ BATTERY_CHARGING: ['charge_state'],
11
+ };
12
+ /**
13
+ * Homebridge Accessory Categories Documentation:
14
+ * https://developers.homebridge.io/#/categories
15
+ * Tuya Standard Instruction Set Documentation:
16
+ * https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq
17
+ */
18
+ class BaseAccessory {
19
+ get deviceManager() {
20
+ // Return whichever manager owns this device (local takes priority in hybrid mode)
21
+ const deviceID = this.accessory.context.deviceID;
22
+ if (deviceID && this.platform.localDeviceManager?.getDevice(deviceID)) {
23
+ return this.platform.localDeviceManager;
24
+ }
25
+ return this.platform.deviceManager;
26
+ }
27
+ get deviceSource() {
28
+ // Determine which source this device comes from for override lookup
29
+ const deviceID = this.accessory.context.deviceID;
30
+ if (deviceID && this.platform.localDeviceManager?.getDevice(deviceID)) {
31
+ return 'local';
32
+ }
33
+ if (deviceID && this.platform.deviceManager?.getDevice(deviceID)) {
34
+ return 'cloud';
35
+ }
36
+ return undefined;
37
+ }
38
+ get device() {
39
+ // Try local device manager first (for local-only or hybrid modes)
40
+ const localDevice = this.platform.localDeviceManager?.getDevice(this.accessory.context.deviceID);
41
+ if (localDevice) {
42
+ return localDevice;
43
+ }
44
+ // Fall back to cloud device manager
45
+ return this.platform.deviceManager?.getDevice(this.accessory.context.deviceID);
46
+ }
47
+ get log() {
48
+ if (!this.cachedLog) {
49
+ const deviceName = this.device?.name ?? this.device?.id ?? this.accessory.context.deviceID ?? 'Unknown Device';
50
+ this.cachedLog = new Logger_1.PrefixLogger(this.platform.log, deviceName, this.platform.options.debug && ((this.platform.options.debugLevel ?? '').length > 0
51
+ ? this.platform.options.debugLevel?.includes(this.device?.id ?? '')
52
+ : true));
53
+ }
54
+ return this.cachedLog;
55
+ }
56
+ constructor(platform, accessory) {
57
+ this.platform = platform;
58
+ this.accessory = accessory;
59
+ this.Service = this.platform.api.hap.Service;
60
+ this.Characteristic = this.platform.api.hap.Characteristic;
61
+ this.initialized = false;
62
+ this.sendQueue = new Map();
63
+ this.debounceSendCommands = (0, util_2.debounce)(async () => {
64
+ const commands = [...this.sendQueue.values()];
65
+ if (commands.length === 0) {
66
+ return;
67
+ }
68
+ if (!this.device || !this.deviceManager) {
69
+ this.log.warn('Device manager or device not available, cannot send commands.');
70
+ this.sendQueue.clear();
71
+ return;
72
+ }
73
+ try {
74
+ await this.deviceManager.sendCommands(this.device.id, commands);
75
+ }
76
+ catch (error) {
77
+ if (this.platform.deviceManager && this.deviceManager === this.platform.localDeviceManager) {
78
+ const deviceName = this.device?.name || this.device?.id || 'Unknown Device';
79
+ this.log.warn(`[${deviceName}] Local debounced send failed, falling back to cloud: ${error instanceof Error ? error.message : error}`);
80
+ try {
81
+ await this.platform.deviceManager.sendCommands(this.device.id, commands);
82
+ }
83
+ catch (cloudError) {
84
+ this.log.warn(`[${deviceName}] Cloud fallback failed: ${cloudError instanceof Error ? cloudError.message : cloudError}`);
85
+ }
86
+ }
87
+ else {
88
+ this.log.warn(`Debounced send failed: ${error instanceof Error ? error.message : error}`);
89
+ }
90
+ }
91
+ finally {
92
+ this.sendQueue.clear();
93
+ }
94
+ }, 100);
95
+ this.addAccessoryInfoService();
96
+ this.addBatteryService();
97
+ }
98
+ addAccessoryInfoService() {
99
+ const service = this.accessory.getService(this.Service.AccessoryInformation)
100
+ || this.accessory.addService(this.Service.AccessoryInformation);
101
+ if (!this.device) {
102
+ // Use fallback values if device is not available yet
103
+ const safeName = (0, util_1.sanitizeName)(this.accessory.displayName) ?? 'Tuya Device';
104
+ service
105
+ .setCharacteristic(this.Characteristic.Manufacturer, MANUFACTURER)
106
+ .setCharacteristic(this.Characteristic.Name, safeName)
107
+ .setCharacteristic(this.Characteristic.ConfiguredName, safeName);
108
+ return;
109
+ }
110
+ const safeName = (0, util_1.sanitizeName)(this.device.name) ?? (this.device.id || 'Tuya Device');
111
+ service
112
+ .setCharacteristic(this.Characteristic.Manufacturer, MANUFACTURER)
113
+ .setCharacteristic(this.Characteristic.Model, this.device.model || this.device.product_name || this.device.product_id)
114
+ .setCharacteristic(this.Characteristic.Name, safeName)
115
+ .setCharacteristic(this.Characteristic.ConfiguredName, safeName);
116
+ const serialNumber = typeof this.device.uuid === 'string' ? this.device.uuid.trim() : '';
117
+ if (serialNumber.length > 1) {
118
+ service.setCharacteristic(this.Characteristic.SerialNumber, serialNumber);
119
+ }
120
+ else {
121
+ this.log.warn(`Skipping invalid SerialNumber for accessory ${safeName}`);
122
+ }
123
+ }
124
+ addBatteryService() {
125
+ const percentSchema = this.getSchema(...SCHEMA_CODE.BATTERY_PERCENT);
126
+ if (!percentSchema) {
127
+ return;
128
+ }
129
+ const { BATTERY_LEVEL_NORMAL, BATTERY_LEVEL_LOW } = this.Characteristic.StatusLowBattery;
130
+ const service = this.accessory.getService(this.Service.Battery)
131
+ || this.accessory.addService(this.Service.Battery);
132
+ const stateSchema = this.getSchema(...SCHEMA_CODE.BATTERY_STATE);
133
+ if (stateSchema || percentSchema) {
134
+ service.getCharacteristic(this.Characteristic.StatusLowBattery)
135
+ .onGet(() => {
136
+ if (stateSchema) {
137
+ const status = this.getStatus(stateSchema.code);
138
+ if (!status) {
139
+ return BATTERY_LEVEL_NORMAL;
140
+ }
141
+ return (status.value === 'low') ? BATTERY_LEVEL_LOW : BATTERY_LEVEL_NORMAL;
142
+ }
143
+ // fallback
144
+ const status = this.getStatus(percentSchema.code);
145
+ if (!status) {
146
+ return BATTERY_LEVEL_NORMAL;
147
+ }
148
+ return (status.value <= 20) ? BATTERY_LEVEL_LOW : BATTERY_LEVEL_NORMAL;
149
+ });
150
+ }
151
+ const property = percentSchema.property;
152
+ const multiple = Math.pow(10, property ? property.scale : 0);
153
+ service.getCharacteristic(this.Characteristic.BatteryLevel)
154
+ .onGet(() => {
155
+ const status = this.getStatus(percentSchema.code);
156
+ if (!status) {
157
+ return 0;
158
+ }
159
+ return (0, util_1.limit)(status.value / multiple, 0, 100);
160
+ });
161
+ const chargingSchema = this.getSchema(...SCHEMA_CODE.BATTERY_CHARGING);
162
+ if (chargingSchema) {
163
+ const { NOT_CHARGING, CHARGING } = this.Characteristic.ChargingState;
164
+ service.getCharacteristic(this.Characteristic.ChargingState)
165
+ .onGet(() => {
166
+ const status = this.getStatus(chargingSchema.code);
167
+ if (!status) {
168
+ return NOT_CHARGING;
169
+ }
170
+ return status.value ? CHARGING : NOT_CHARGING;
171
+ });
172
+ }
173
+ }
174
+ configureStatusActive() {
175
+ for (const service of this.accessory.services) {
176
+ if (!service.testCharacteristic(this.Characteristic.StatusActive)) { // silence warning
177
+ service.addOptionalCharacteristic(this.Characteristic.StatusActive);
178
+ }
179
+ service.getCharacteristic(this.Characteristic.StatusActive)
180
+ .onGet(() => this.device?.online ?? true);
181
+ }
182
+ }
183
+ async updateAllValues() {
184
+ for (const service of this.accessory.services) {
185
+ for (const characteristic of service.characteristics) {
186
+ if (characteristic.UUID === this.Characteristic.ProgrammableSwitchEvent.UUID) {
187
+ continue;
188
+ }
189
+ let newValue = characteristic.value;
190
+ const getHandler = characteristic['getHandler'];
191
+ if (getHandler) {
192
+ try {
193
+ newValue = await getHandler();
194
+ }
195
+ catch (error) {
196
+ // TODO: why `characteristic.updateValue(HapStatusError)` not working?
197
+ // newValue = error as Error;
198
+ continue;
199
+ }
200
+ }
201
+ if (characteristic.value !== newValue && !(newValue instanceof Error)) {
202
+ this.log.debug('[%s/%s/%s] Update value: %o => %o', service.constructor.name, service.subtype, characteristic.constructor.name, characteristic.value, newValue);
203
+ }
204
+ characteristic.updateValue(newValue);
205
+ }
206
+ }
207
+ }
208
+ checkOnlineStatus() {
209
+ if (this.device?.online === false) {
210
+ const { HapStatusError, HAPStatus } = this.platform.api.hap;
211
+ throw new HapStatusError(-70402 /* HAPStatus.SERVICE_COMMUNICATION_FAILURE */);
212
+ }
213
+ }
214
+ getSchema(...codes) {
215
+ if (!this.device) {
216
+ return undefined;
217
+ }
218
+ for (const code of codes) {
219
+ const schema = this.device.schema.find(schema => {
220
+ // ignore case
221
+ return schema.code.toLowerCase() === code.toLowerCase();
222
+ });
223
+ if (schema) {
224
+ return schema;
225
+ }
226
+ }
227
+ return undefined;
228
+ }
229
+ getStatus(code) {
230
+ if (!this.device) {
231
+ return undefined;
232
+ }
233
+ return this.device.status.find(status => status.code === code);
234
+ }
235
+ async sendCommands(commands, debounce = false) {
236
+ if (commands.length === 0) {
237
+ return;
238
+ }
239
+ if (!this.device || !this.deviceManager) {
240
+ this.log.warn('Device manager or device not available, cannot send commands.');
241
+ return;
242
+ }
243
+ commands = commands.filter((status) => status.code && status.value !== undefined);
244
+ if (this.device.online === false) {
245
+ this.log.warn('Device is offline, skip send command.');
246
+ this.updateAllValues();
247
+ const { HapStatusError, HAPStatus } = this.platform.api.hap;
248
+ throw new HapStatusError(-70402 /* HAPStatus.SERVICE_COMMUNICATION_FAILURE */);
249
+ return;
250
+ }
251
+ // Update cache immediately
252
+ for (const newStatus of commands) {
253
+ const oldStatus = this.device.status.find(_status => _status.code === newStatus.code);
254
+ if (oldStatus) {
255
+ oldStatus.value = newStatus.value;
256
+ }
257
+ }
258
+ if (debounce === false) {
259
+ try {
260
+ return await this.deviceManager.sendCommands(this.device.id, commands);
261
+ }
262
+ catch (error) {
263
+ if (this.platform.deviceManager && this.deviceManager === this.platform.localDeviceManager) {
264
+ const deviceName = this.device?.name || this.device?.id || 'Unknown Device';
265
+ this.log.warn(`[${deviceName}] Local send failed, falling back to cloud: ${error instanceof Error ? error.message : error}`);
266
+ return await this.platform.deviceManager.sendCommands(this.device.id, commands);
267
+ }
268
+ throw error;
269
+ }
270
+ }
271
+ for (const newStatus of commands) {
272
+ // Update send queue
273
+ this.sendQueue.set(newStatus.code, newStatus);
274
+ }
275
+ this.debounceSendCommands();
276
+ }
277
+ checkRequirements() {
278
+ if (!this.device) {
279
+ return false;
280
+ }
281
+ let result = true;
282
+ for (const codes of this.requiredSchema()) {
283
+ const schema = this.getSchema(...codes);
284
+ if (schema) {
285
+ continue;
286
+ }
287
+ this.log.warn('Product Category: %s', this.device.category);
288
+ this.log.warn('Missing one of the required schema: %s', codes);
289
+ this.log.warn('Please switch device control mode to "DP Instruction", and set `deviceOverrides` manually.');
290
+ this.log.warn('Detail information: https://github.com/homebridge-plugins/homebridge-tuya#faq');
291
+ result = false;
292
+ }
293
+ if (!result) {
294
+ this.log.warn('Existing schema: %o', this.device.schema);
295
+ }
296
+ return result;
297
+ }
298
+ requiredSchema() {
299
+ return [];
300
+ }
301
+ configureServices() {
302
+ //
303
+ }
304
+ async onDeviceInfoUpdate(info) {
305
+ this.updateAllValues();
306
+ }
307
+ async onDeviceStatusUpdate(status) {
308
+ this.updateAllValues();
309
+ }
310
+ }
311
+ // Overriding getSchema, getStatus, sendCommands
312
+ class OverridedBaseAccessory extends BaseAccessory {
313
+ constructor() {
314
+ super(...arguments);
315
+ this.eval = (script, device, value) => eval(script);
316
+ }
317
+ getOverridedSchema(code) {
318
+ if (!this.device) {
319
+ return undefined;
320
+ }
321
+ const schemaConfig = this.platform.getDeviceSchemaConfig(this.device, code, this.deviceSource);
322
+ if (!schemaConfig) {
323
+ return undefined;
324
+ }
325
+ const oldSchema = this.device.schema.find(schema => {
326
+ // ignore case
327
+ return schema.code.toLowerCase() === schemaConfig.code.toLowerCase();
328
+ });
329
+ if (!oldSchema) {
330
+ return undefined;
331
+ }
332
+ const schema = {
333
+ code,
334
+ mode: oldSchema.mode,
335
+ type: schemaConfig.type || oldSchema.type,
336
+ property: schemaConfig.property || oldSchema.property,
337
+ _hidden: schemaConfig.hidden,
338
+ };
339
+ if (!(0, util_2.deepEqual)(oldSchema, schema)) {
340
+ this.log.debug('Override schema %o => %o', oldSchema, schema);
341
+ }
342
+ return schema;
343
+ }
344
+ getSchema(...codes) {
345
+ for (const code of codes) {
346
+ const schema = this.getOverridedSchema(code) || super.getSchema(code);
347
+ if (!schema) {
348
+ continue;
349
+ }
350
+ if (schema['_hidden']) {
351
+ return undefined;
352
+ }
353
+ return schema;
354
+ }
355
+ return undefined;
356
+ }
357
+ getOverridedStatus(code) {
358
+ if (!this.device) {
359
+ return undefined;
360
+ }
361
+ const schemaConfig = this.platform.getDeviceSchemaConfig(this.device, code, this.deviceSource);
362
+ if (!schemaConfig) {
363
+ return undefined;
364
+ }
365
+ const oldStatus = super.getStatus(schemaConfig.code);
366
+ if (!oldStatus) {
367
+ return undefined;
368
+ }
369
+ const status = { code: schemaConfig.newCode || schemaConfig.code, value: oldStatus.value };
370
+ if (schemaConfig.onGet) {
371
+ status.value = this.eval(schemaConfig.onGet, this.device, oldStatus.value);
372
+ }
373
+ if (!(0, util_2.deepEqual)(oldStatus, status)) {
374
+ this.log.debug('Override status %o => %o', oldStatus, status);
375
+ }
376
+ return status;
377
+ }
378
+ getStatus(code) {
379
+ return this.getOverridedStatus(code) || super.getStatus(code);
380
+ }
381
+ async sendCommands(commands, debounce) {
382
+ if (!this.device) {
383
+ await super.sendCommands(commands, debounce);
384
+ return;
385
+ }
386
+ // convert to original commands
387
+ for (const command of commands) {
388
+ const schemaConfig = this.platform.getDeviceSchemaConfig(this.device, command.code, this.deviceSource);
389
+ if (!schemaConfig) {
390
+ continue;
391
+ }
392
+ const oldCommand = { code: schemaConfig.code, value: command.value };
393
+ if (schemaConfig.onSet) {
394
+ oldCommand.value = this.eval(schemaConfig.onSet, this.device, command.value);
395
+ }
396
+ if (!(0, util_2.deepEqual)(oldCommand, command)) {
397
+ this.log.debug('Override command %o => %o', command, oldCommand);
398
+ command.code = oldCommand.code;
399
+ command.value = oldCommand.value;
400
+ }
401
+ }
402
+ await super.sendCommands(commands, debounce);
403
+ }
404
+ }
405
+ exports.default = OverridedBaseAccessory;
406
+ //# sourceMappingURL=BaseAccessory.js.map
@@ -0,0 +1,199 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const util_1 = require("../util/util");
7
+ const BaseAccessory_1 = __importDefault(require("./BaseAccessory"));
8
+ const SCHEMA_CODE = {
9
+ CONTROL: ['control', 'mach_operate'],
10
+ CURRENT_POSITION: ['percent_state'],
11
+ TARGET_POSITION: ['percent_control', 'position'],
12
+ POSITION: ['position'],
13
+ };
14
+ /**
15
+ * BlindsAccessory – handles roller motor shades and blinds.
16
+ * Supports position control with tracking and state management.
17
+ *
18
+ * Categories: 'mg' (blinds), 'mgmt' (motorized blinds)
19
+ */
20
+ class BlindsAccessory extends BaseAccessory_1.default {
21
+ requiredSchema() {
22
+ return [SCHEMA_CODE.CONTROL];
23
+ }
24
+ configureServices() {
25
+ this.configureCurrentPosition();
26
+ this.configurePositionState();
27
+ this.configureTargetPosition();
28
+ }
29
+ /**
30
+ * Configure CurrentPosition characteristic.
31
+ * Read-only value showing actual blind position (0-100%).
32
+ */
33
+ configureCurrentPosition() {
34
+ const currentSchema = this.getSchema(...SCHEMA_CODE.CURRENT_POSITION);
35
+ const targetSchema = this.getSchema(...SCHEMA_CODE.TARGET_POSITION) ||
36
+ this.getSchema(...SCHEMA_CODE.POSITION);
37
+ const service = this.accessory.getService(this.Service.WindowCovering) ||
38
+ this.accessory.addService(this.Service.WindowCovering);
39
+ service.getCharacteristic(this.Characteristic.CurrentPosition)
40
+ .onGet(() => {
41
+ // Prefer current position schema if available
42
+ if (currentSchema) {
43
+ const status = this.getStatus(currentSchema.code);
44
+ return (0, util_1.limit)(status.value, 0, 100);
45
+ }
46
+ // Fall back to target position schema
47
+ if (targetSchema) {
48
+ const status = this.getStatus(targetSchema.code);
49
+ return (0, util_1.limit)(status.value, 0, 100);
50
+ }
51
+ // Fall back to control command status (open/close/stop)
52
+ const controlSchema = this.getSchema(...SCHEMA_CODE.CONTROL);
53
+ if (controlSchema) {
54
+ const status = this.getStatus(controlSchema.code);
55
+ return this.controlValueToPosition(status.value);
56
+ }
57
+ return 50; // Default to middle position
58
+ });
59
+ }
60
+ /**
61
+ * Configure PositionState characteristic.
62
+ * Indicates if blinds are going up (INCREASING), down (DECREASING), or stopped.
63
+ */
64
+ configurePositionState() {
65
+ const currentSchema = this.getSchema(...SCHEMA_CODE.CURRENT_POSITION);
66
+ const targetSchema = this.getSchema(...SCHEMA_CODE.TARGET_POSITION) ||
67
+ this.getSchema(...SCHEMA_CODE.POSITION);
68
+ const { DECREASING, INCREASING, STOPPED } = this.Characteristic.PositionState;
69
+ const service = this.accessory.getService(this.Service.WindowCovering) ||
70
+ this.accessory.addService(this.Service.WindowCovering);
71
+ service.getCharacteristic(this.Characteristic.PositionState)
72
+ .onGet(() => {
73
+ // If we don't have both current and target, assume stopped
74
+ if (!currentSchema || !targetSchema) {
75
+ return STOPPED;
76
+ }
77
+ const currentStatus = this.getStatus(currentSchema.code);
78
+ const targetStatus = this.getStatus(targetSchema.code);
79
+ const currentPos = currentStatus.value;
80
+ const targetPos = targetStatus.value;
81
+ if (targetPos > currentPos) {
82
+ return INCREASING; // Moving up/open
83
+ }
84
+ else if (targetPos < currentPos) {
85
+ return DECREASING; // Moving down/close
86
+ }
87
+ else {
88
+ return STOPPED; // At target position
89
+ }
90
+ });
91
+ }
92
+ /**
93
+ * Configure TargetPosition characteristic.
94
+ * Allows user to set desired blind position (0-100%).
95
+ */
96
+ configureTargetPosition() {
97
+ const controlSchema = this.getSchema(...SCHEMA_CODE.CONTROL);
98
+ const targetSchema = this.getSchema(...SCHEMA_CODE.TARGET_POSITION) ||
99
+ this.getSchema(...SCHEMA_CODE.POSITION);
100
+ if (!controlSchema && !targetSchema) {
101
+ this.log.warn('No target position schema available for blinds control');
102
+ return;
103
+ }
104
+ const service = this.accessory.getService(this.Service.WindowCovering) ||
105
+ this.accessory.addService(this.Service.WindowCovering);
106
+ service.getCharacteristic(this.Characteristic.TargetPosition)
107
+ .onGet(() => {
108
+ // If target position schema exists, use it
109
+ if (targetSchema) {
110
+ const status = this.getStatus(targetSchema.code);
111
+ return (0, util_1.limit)(status.value, 0, 100);
112
+ }
113
+ // Otherwise, use control schema (open/close/stop)
114
+ if (controlSchema) {
115
+ const status = this.getStatus(controlSchema.code);
116
+ return this.controlValueToPosition(status.value);
117
+ }
118
+ return this.targetPosition ?? 50;
119
+ })
120
+ .onSet(async (value) => {
121
+ const targetPos = value;
122
+ this.targetPosition = targetPos;
123
+ // Clear any pending reset timer
124
+ if (this.positionResetTimer) {
125
+ clearTimeout(this.positionResetTimer);
126
+ this.positionResetTimer = undefined;
127
+ }
128
+ // If we have a percent_control schema, use it directly
129
+ if (targetSchema && targetSchema.code !== 'control' && targetSchema.code !== 'mach_operate') {
130
+ await this.sendCommands([{ code: targetSchema.code, value: targetPos }], true);
131
+ }
132
+ else if (controlSchema) {
133
+ // Otherwise, use the control schema (open/close/stop)
134
+ const controlValue = this.positionToControlValue(targetPos);
135
+ await this.sendCommands([{ code: controlSchema.code, value: controlValue }], true);
136
+ // Schedule idle reset after 30 seconds if device doesn't report position
137
+ // This prevents the blinds from continuously moving
138
+ this.positionResetTimer = setTimeout(() => {
139
+ this._resetToIdle();
140
+ }, 30 * 1000);
141
+ }
142
+ });
143
+ }
144
+ /**
145
+ * Convert HomeKit position value (0-100) to Tuya control value (open/close/stop).
146
+ */
147
+ positionToControlValue(position) {
148
+ if (position >= 95) {
149
+ return 'open'; // or 'ZZ' for some devices
150
+ }
151
+ else if (position <= 5) {
152
+ return 'close'; // or 'FZ' for some devices
153
+ }
154
+ else {
155
+ return 'stop'; // or 'STOP' for some devices
156
+ }
157
+ }
158
+ /**
159
+ * Convert Tuya control value (open/close/stop) to HomeKit position (0-100).
160
+ */
161
+ controlValueToPosition(value) {
162
+ const lowerValue = value.toLowerCase();
163
+ if (lowerValue === 'open' || lowerValue === 'zz') {
164
+ return 100;
165
+ }
166
+ else if (lowerValue === 'close' || lowerValue === 'fz') {
167
+ return 0;
168
+ }
169
+ else if (lowerValue === 'stop' || lowerValue === 'stopped') {
170
+ return 50;
171
+ }
172
+ return 50; // Default to middle
173
+ }
174
+ /**
175
+ * Reset control to idle state after position movement completes.
176
+ */
177
+ _resetToIdle() {
178
+ const controlSchema = this.getSchema(...SCHEMA_CODE.CONTROL);
179
+ if (controlSchema) {
180
+ this.sendCommands([{ code: controlSchema.code, value: 'stop' }]);
181
+ }
182
+ this.positionResetTimer = undefined;
183
+ }
184
+ /**
185
+ * Handle device status updates from cloud/local.
186
+ */
187
+ async onDeviceStatusUpdate(status) {
188
+ super.onDeviceStatusUpdate(status);
189
+ // If we receive a position update, clear the reset timer
190
+ const positionUpdate = status.find(s => SCHEMA_CODE.CURRENT_POSITION.includes(s.code) ||
191
+ (SCHEMA_CODE.TARGET_POSITION.includes(s.code) && s.code !== 'control'));
192
+ if (positionUpdate && this.positionResetTimer) {
193
+ clearTimeout(this.positionResetTimer);
194
+ this.positionResetTimer = undefined;
195
+ }
196
+ }
197
+ }
198
+ exports.default = BlindsAccessory;
199
+ //# sourceMappingURL=BlindsAccessory.js.map