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,329 @@
1
+ "use strict";
2
+ /* eslint-disable @typescript-eslint/no-unused-vars */
3
+ /* eslint-disable max-len */
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.TuyaStreamingDelegate = void 0;
6
+ const camera_utils_1 = require("@homebridge/camera-utils");
7
+ const child_process_1 = require("child_process");
8
+ const dgram_1 = require("dgram");
9
+ const FfmpegStreamingProcess_1 = require("./FfmpegStreamingProcess");
10
+ /*
11
+ interface SampleRateEntry {
12
+ type: AudioRecordingCodecType;
13
+ bitrateMode: number;
14
+ samplerate: AudioRecordingSamplerate[];
15
+ audioChannels: number;
16
+ }
17
+ */
18
+ class TuyaStreamingDelegate {
19
+ constructor(camera) {
20
+ this.pendingSessions = {};
21
+ this.ongoingSessions = {};
22
+ this.camera = camera;
23
+ this.hap = camera.platform.api.hap;
24
+ // this.recordingDelegate = new TuyaRecordingDelegate();
25
+ const resolutions = [
26
+ [320, 180, 30],
27
+ [320, 240, 15],
28
+ [320, 240, 30],
29
+ [480, 270, 30],
30
+ [480, 360, 30],
31
+ [640, 360, 30],
32
+ [640, 480, 30],
33
+ [1280, 720, 30],
34
+ [1280, 960, 30],
35
+ [1920, 1080, 30],
36
+ [1600, 1200, 30],
37
+ ];
38
+ const streamingOptions = {
39
+ supportedCryptoSuites: [0 /* SRTPCryptoSuites.AES_CM_128_HMAC_SHA1_80 */],
40
+ video: {
41
+ codec: {
42
+ profiles: [0 /* H264Profile.BASELINE */, 1 /* H264Profile.MAIN */, 2 /* H264Profile.HIGH */],
43
+ levels: [0 /* H264Level.LEVEL3_1 */, 1 /* H264Level.LEVEL3_2 */, 2 /* H264Level.LEVEL4_0 */],
44
+ },
45
+ resolutions: resolutions,
46
+ },
47
+ audio: {
48
+ twoWayAudio: false,
49
+ codecs: [
50
+ {
51
+ type: "AAC-eld" /* AudioStreamingCodecType.AAC_ELD */,
52
+ samplerate: 16 /* AudioStreamingSamplerate.KHZ_16 */,
53
+ },
54
+ ],
55
+ },
56
+ };
57
+ const recordingOptions = {
58
+ overrideEventTriggerOptions: [
59
+ 1 /* EventTriggerOption.MOTION */,
60
+ 2 /* EventTriggerOption.DOORBELL */,
61
+ ],
62
+ prebufferLength: 4 * 1000, // prebufferLength always remains 4s ?
63
+ mediaContainerConfiguration: [
64
+ {
65
+ type: 0 /* MediaContainerType.FRAGMENTED_MP4 */,
66
+ fragmentLength: 4000,
67
+ },
68
+ ],
69
+ video: {
70
+ parameters: {
71
+ profiles: [
72
+ 0 /* H264Profile.BASELINE */,
73
+ 1 /* H264Profile.MAIN */,
74
+ 2 /* H264Profile.HIGH */,
75
+ ],
76
+ levels: [
77
+ 0 /* H264Level.LEVEL3_1 */,
78
+ 1 /* H264Level.LEVEL3_2 */,
79
+ 2 /* H264Level.LEVEL4_0 */,
80
+ ],
81
+ },
82
+ resolutions: resolutions,
83
+ type: 0 /* this.hap.VideoCodecType.H264 */,
84
+ },
85
+ audio: {
86
+ codecs: [
87
+ {
88
+ samplerate: 3 /* this.hap.AudioRecordingSamplerate.KHZ_32 */,
89
+ type: 0 /* this.hap.AudioRecordingCodecType.AAC_LC */,
90
+ },
91
+ ],
92
+ },
93
+ };
94
+ const options = {
95
+ delegate: this,
96
+ streamingOptions: streamingOptions,
97
+ // recording: {
98
+ // options: recordingOptions,
99
+ // delegate: this.recordingDelegate
100
+ // }
101
+ };
102
+ this.controller = new this.hap.CameraController(options);
103
+ }
104
+ stopStream(sessionId) {
105
+ const session = this.ongoingSessions[sessionId];
106
+ if (session) {
107
+ if (session.timeout) {
108
+ clearTimeout(session.timeout);
109
+ }
110
+ try {
111
+ session.socket?.close();
112
+ }
113
+ catch (error) {
114
+ this.camera.log.error(`Error occurred closing socket: ${error}`);
115
+ }
116
+ try {
117
+ session.mainProcess?.stop();
118
+ }
119
+ catch (error) {
120
+ this.camera.log.error(`Error occurred terminating main FFmpeg process: ${error}`);
121
+ }
122
+ try {
123
+ session.returnProcess?.stop();
124
+ }
125
+ catch (error) {
126
+ this.camera.log.error(`Error occurred terminating two-way FFmpeg process: ${error}`);
127
+ }
128
+ delete this.ongoingSessions[sessionId];
129
+ this.camera.log.info('Stopped video stream.');
130
+ }
131
+ }
132
+ forceStopStream(sessionId) {
133
+ this.controller.forceStopStreamingSession(sessionId);
134
+ }
135
+ async handleSnapshotRequest(request, callback) {
136
+ try {
137
+ this.camera.log.debug(`Snapshot requested: ${request.width} x ${request.height}`);
138
+ const snapshot = await this.fetchSnapshot();
139
+ this.camera.log.debug('Sending snapshot');
140
+ callback(undefined, snapshot);
141
+ }
142
+ catch (error) {
143
+ callback(error);
144
+ }
145
+ }
146
+ async prepareStream(request, callback) {
147
+ const videoIncomingPort = await (0, camera_utils_1.reservePorts)({
148
+ count: 1,
149
+ });
150
+ const videoSSRC = this.hap.CameraController.generateSynchronisationSource();
151
+ const audioIncomingPort = await (0, camera_utils_1.reservePorts)({
152
+ count: 1,
153
+ });
154
+ const audioSSRC = this.hap.CameraController.generateSynchronisationSource();
155
+ const sessionInfo = {
156
+ address: request.targetAddress,
157
+ addressVersion: request.addressVersion,
158
+ audioCryptoSuite: request.audio.srtpCryptoSuite,
159
+ audioPort: request.audio.port,
160
+ audioSRTP: Buffer.concat([request.audio.srtp_key, request.audio.srtp_salt]),
161
+ audioSSRC: audioSSRC,
162
+ audioIncomingPort: audioIncomingPort[0],
163
+ videoCryptoSuite: request.video.srtpCryptoSuite,
164
+ videoPort: request.video.port,
165
+ videoSRTP: Buffer.concat([request.video.srtp_key, request.video.srtp_salt]),
166
+ videoSSRC: videoSSRC,
167
+ videoIncomingPort: videoIncomingPort[0],
168
+ };
169
+ const response = {
170
+ video: {
171
+ port: sessionInfo.videoIncomingPort,
172
+ ssrc: videoSSRC,
173
+ srtp_key: request.video.srtp_key,
174
+ srtp_salt: request.video.srtp_salt,
175
+ },
176
+ audio: {
177
+ port: sessionInfo.audioIncomingPort,
178
+ ssrc: audioSSRC,
179
+ srtp_key: request.audio.srtp_key,
180
+ srtp_salt: request.audio.srtp_salt,
181
+ },
182
+ };
183
+ this.pendingSessions[request.sessionID] = sessionInfo;
184
+ callback(undefined, response);
185
+ }
186
+ async handleStreamRequest(request, callback) {
187
+ switch (request.type) {
188
+ case "start" /* this.hap.StreamRequestTypes.START */: {
189
+ this.camera.log.debug(`Start stream requested: ${request.video.width}x${request.video.height}, ${request.video.fps} fps, ${request.video.max_bit_rate} kbps`);
190
+ await this.startStream(request, callback);
191
+ break;
192
+ }
193
+ case "reconfigure" /* this.hap.StreamRequestTypes.RECONFIGURE */: {
194
+ this.camera.log.debug(`Reconfigure stream requested: ${request.video.width}x${request.video.height}, ${request.video.fps} fps, ${request.video.max_bit_rate} kbps (Ignored)`);
195
+ callback();
196
+ break;
197
+ }
198
+ case "stop" /* this.hap.StreamRequestTypes.STOP */: {
199
+ this.camera.log.debug('Stop stream requested');
200
+ this.stopStream(request.sessionID);
201
+ callback();
202
+ break;
203
+ }
204
+ }
205
+ }
206
+ async retrieveDeviceRTSP() {
207
+ const data = await this.camera.deviceManager.api.post(`/v1.0/devices/${this.camera.device.id}/stream/actions/allocate`, {
208
+ type: 'rtsp',
209
+ });
210
+ return data.result.url;
211
+ }
212
+ async startStream(request, callback) {
213
+ const sessionInfo = this.pendingSessions[request.sessionID];
214
+ if (!sessionInfo) {
215
+ this.camera.log.error('Error finding session information.');
216
+ callback(new Error('Error finding session information'));
217
+ }
218
+ const vcodec = 'libx264';
219
+ const mtu = 1316; // request.video.mtu is not used
220
+ const fps = request.video.fps;
221
+ const videoBitrate = request.video.max_bit_rate;
222
+ const rtspUrl = await this.retrieveDeviceRTSP();
223
+ const ffmpegArgs = [
224
+ '-hide_banner',
225
+ '-loglevel', 'verbose',
226
+ '-i', rtspUrl,
227
+ '-an', '-sn', '-dn',
228
+ '-r', fps.toString(),
229
+ '-codec:v', vcodec,
230
+ '-pix_fmt', 'yuv420p',
231
+ '-color_range', 'mpeg',
232
+ '-f', 'rawvideo',
233
+ ];
234
+ const encoderOptions = '-preset ultrafast -tune zerolatency';
235
+ if (encoderOptions) {
236
+ ffmpegArgs.push(...encoderOptions.split(/\s+/));
237
+ }
238
+ if (videoBitrate > 0) {
239
+ ffmpegArgs.push('-b:v', `${videoBitrate}k`);
240
+ }
241
+ // Video Stream
242
+ ffmpegArgs.push('-payload_type', `${request.video.pt}`, '-ssrc', `${sessionInfo.videoSSRC}`, '-f', 'rtp', '-srtp_out_suite', 'AES_CM_128_HMAC_SHA1_80', '-srtp_out_params', sessionInfo.videoSRTP.toString('base64'), `srtp://${sessionInfo.address}:${sessionInfo.videoPort}?rtcpport=${sessionInfo.videoPort}&pkt_size=${mtu}`);
243
+ // Setting up audio
244
+ if (request.audio.codec === "OPUS" /* AudioStreamingCodecType.OPUS */ ||
245
+ request.audio.codec === "AAC-eld" /* AudioStreamingCodecType.AAC_ELD */) {
246
+ ffmpegArgs.push('-vn', '-sn', '-dn');
247
+ if (request.audio.codec === "OPUS" /* AudioStreamingCodecType.OPUS */) {
248
+ ffmpegArgs.push('-acodec', 'libopus', '-application', 'lowdelay');
249
+ }
250
+ else {
251
+ ffmpegArgs.push('-acodec', 'libfdk_aac', '-profile:a', 'aac_eld');
252
+ }
253
+ ffmpegArgs.push('-flags', '+global_header', '-f', 'null', '-ar', `${request.audio.sample_rate}k`, '-b:a', `${request.audio.max_bit_rate}k`, '-ac', `${request.audio.channel}`, '-payload_type', `${request.audio.pt}`, '-ssrc', `${sessionInfo.audioSSRC}`, '-f', 'rtp', '-srtp_out_suite', 'AES_CM_128_HMAC_SHA1_80', '-srtp_out_params', sessionInfo.audioSRTP.toString('base64'), `srtp://${sessionInfo.address}:${sessionInfo.audioPort}?rtcpport=${sessionInfo.audioPort}&pkt_size=188`);
254
+ }
255
+ else {
256
+ this.camera.log.error(`Unsupported audio codec requested: ${request.audio.codec}`);
257
+ }
258
+ ffmpegArgs.push('-progress', 'pipe:1');
259
+ const activeSession = {};
260
+ activeSession.socket = (0, dgram_1.createSocket)(sessionInfo.addressVersion === 'ipv6' ? 'udp6' : 'udp4');
261
+ activeSession.socket.on('error', (err) => {
262
+ this.camera.log.error('Socket error: ' + err.message);
263
+ this.stopStream(request.sessionID);
264
+ });
265
+ activeSession.socket.on('message', () => {
266
+ if (activeSession.timeout) {
267
+ clearTimeout(activeSession.timeout);
268
+ }
269
+ activeSession.timeout = setTimeout(() => {
270
+ this.camera.log.info('Device appears to be inactive. Stopping stream.');
271
+ this.controller.forceStopStreamingSession(request.sessionID);
272
+ this.stopStream(request.sessionID);
273
+ }, request.video.rtcp_interval * 5 * 1000);
274
+ });
275
+ activeSession.socket.bind(sessionInfo.videoIncomingPort);
276
+ activeSession.mainProcess = new FfmpegStreamingProcess_1.FfmpegStreamingProcess(request.sessionID, camera_utils_1.defaultFfmpegPath, ffmpegArgs, this.camera.log, this, callback);
277
+ this.ongoingSessions[request.sessionID] = activeSession;
278
+ delete this.pendingSessions[request.sessionID];
279
+ }
280
+ async fetchSnapshot() {
281
+ if (!this.camera.device.online) {
282
+ this.camera.log.debug('Device is currently offline.');
283
+ throw new Error('Device is currently offline.');
284
+ }
285
+ // TODO: Check if there is a stream already running to fetch snapshot.
286
+ const rtspUrl = await this.retrieveDeviceRTSP();
287
+ const ffmpegArgs = [
288
+ '-i', rtspUrl,
289
+ '-frames:v', '1',
290
+ '-hide_banner',
291
+ '-loglevel',
292
+ 'error',
293
+ '-f',
294
+ 'image2',
295
+ '-',
296
+ ];
297
+ return new Promise((resolve, reject) => {
298
+ this.camera.log.debug(`Running Snapshot command: ${camera_utils_1.defaultFfmpegPath} ${ffmpegArgs.map(value => JSON.stringify(value)).join(' ')}`);
299
+ const ffmpeg = (0, child_process_1.spawn)(camera_utils_1.defaultFfmpegPath, ffmpegArgs.map(x => x.toString()), { env: process.env });
300
+ let errors = [];
301
+ let snapshotBuffer = Buffer.alloc(0);
302
+ ffmpeg.stdout.on('data', (data) => {
303
+ snapshotBuffer = Buffer.concat([snapshotBuffer, data]);
304
+ });
305
+ ffmpeg.on('error', (error) => {
306
+ this.camera.log.error(`FFmpeg process creation failed: ${error.message} - Showing "offline" image instead.`);
307
+ reject('Failed to fetch snapshot.');
308
+ });
309
+ ffmpeg.stderr.on('data', (data) => {
310
+ errors = errors.slice(-5);
311
+ errors.push(data.toString().replace(/(\r\n|\n|\r)/gm, ' '));
312
+ });
313
+ ffmpeg.on('close', () => {
314
+ if (snapshotBuffer.length > 0) {
315
+ resolve(snapshotBuffer);
316
+ }
317
+ else {
318
+ this.camera.log.error('Failed to fetch snapshot. Showing "offline" image instead.');
319
+ if (errors.length > 0) {
320
+ this.camera.log.error(errors.join(' - '));
321
+ }
322
+ reject('Unable to fetch snapshot.');
323
+ }
324
+ });
325
+ });
326
+ }
327
+ }
328
+ exports.TuyaStreamingDelegate = TuyaStreamingDelegate;
329
+ //# sourceMappingURL=TuyaStreamDelegate.js.map
@@ -0,0 +1,23 @@
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
+ exports.kelvinToHSV = kelvinToHSV;
7
+ exports.kelvinToMired = kelvinToMired;
8
+ exports.miredToKelvin = miredToKelvin;
9
+ const color_convert_1 = __importDefault(require("color-convert"));
10
+ const kelvin_to_rgb_1 = __importDefault(require("kelvin-to-rgb"));
11
+ function kelvinToHSV(kevin) {
12
+ const [r, g, b] = (0, kelvin_to_rgb_1.default)(kevin);
13
+ const [h, s, v] = color_convert_1.default.rgb.hsv(r, g, b);
14
+ return { h, s, v };
15
+ }
16
+ // https://en.wikipedia.org/wiki/Mired
17
+ function kelvinToMired(kelvin) {
18
+ return 1e6 / kelvin;
19
+ }
20
+ function miredToKelvin(mired) {
21
+ return 1e6 / mired;
22
+ }
23
+ //# sourceMappingURL=color.js.map
@@ -0,0 +1,135 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.remap = remap;
4
+ exports.limit = limit;
5
+ exports.toHapProperty = toHapProperty;
6
+ exports.sanitizeName = sanitizeName;
7
+ exports.deepEqual = deepEqual;
8
+ exports.debounce = debounce;
9
+ exports.retry = retry;
10
+ exports.generateUUID = generateUUID;
11
+ function remap(value, srcStart, srcEnd, dstStart, dstEnd) {
12
+ const percent = (value - srcStart) / (srcEnd - srcStart);
13
+ const result = percent * (dstEnd - dstStart) + dstStart;
14
+ return result;
15
+ }
16
+ function limit(value, start, end) {
17
+ let result = value;
18
+ result = Math.min(end, result);
19
+ result = Math.max(start, result);
20
+ return result;
21
+ }
22
+ function toHapProperty(property) {
23
+ return Object.entries(property).reduce((hap, [key, value]) => {
24
+ switch (key) {
25
+ case 'min': {
26
+ const multiple = Math.pow(10, property ? property['scale'] : 0);
27
+ hap['minValue'] = Math.max(-273.15, value / multiple);
28
+ break;
29
+ }
30
+ case 'max': {
31
+ const multiple = Math.pow(10, property ? property['scale'] : 0);
32
+ hap['maxValue'] = Math.min(400, value / multiple);
33
+ break;
34
+ }
35
+ case 'step': {
36
+ const multiple = Math.pow(10, property ? property['scale'] : 0);
37
+ hap['minStep'] = Math.max(0.01, value / multiple);
38
+ break;
39
+ }
40
+ case 'range': {
41
+ hap['validValues'] = value;
42
+ break;
43
+ }
44
+ default: {
45
+ hap[key] = value;
46
+ break;
47
+ }
48
+ }
49
+ return hap;
50
+ }, {});
51
+ }
52
+ function sanitizeName(name) {
53
+ if (!name) {
54
+ return undefined;
55
+ }
56
+ const original = name.toString();
57
+ // First trim whitespace to check starting/ending characters
58
+ const trimmed = original.trim();
59
+ // Check if trimmed string starts or ends with non-alphanumeric and reject if so
60
+ if (!/^[\p{L}\p{N}]/u.test(trimmed) || !/[\p{L}\p{N}]$/u.test(trimmed)) {
61
+ return undefined;
62
+ }
63
+ // keep Unicode alphanumeric characters, spaces and apostrophes; replace other chars with space
64
+ // Uses Unicode property escapes so letters and numbers from all scripts are allowed.
65
+ let s = trimmed.replace(/[^\p{L}\p{N}'\s]/gu, ' ');
66
+ // collapse whitespace
67
+ s = s.replace(/\s+/g, ' ').trim();
68
+ // ensure it starts and ends with an alphanumeric (Unicode-aware)
69
+ if (!/^[\p{L}\p{N}].*[\p{L}\p{N}]$/u.test(s)) {
70
+ return undefined;
71
+ }
72
+ return s;
73
+ }
74
+ /**
75
+ * Deep equality check using JSON serialization
76
+ * Works for most use cases (objects, arrays, primitives)
77
+ */
78
+ function deepEqual(a, b) {
79
+ try {
80
+ return JSON.stringify(a) === JSON.stringify(b);
81
+ }
82
+ catch {
83
+ // Fallback for circular references or non-serializable objects
84
+ return a === b;
85
+ }
86
+ }
87
+ /**
88
+ * Debounce function - delays execution until specified milliseconds pass without calls
89
+ * @param fn Function to debounce
90
+ * @param wait Wait time in milliseconds
91
+ * @returns Debounced function
92
+ */
93
+ function debounce(fn, wait) {
94
+ let timeout = null;
95
+ return function debounced(...args) {
96
+ if (timeout) {
97
+ clearTimeout(timeout);
98
+ }
99
+ timeout = setTimeout(() => {
100
+ fn(...args);
101
+ timeout = null;
102
+ }, wait);
103
+ };
104
+ }
105
+ async function retry(fn, options) {
106
+ const { retriesMax = 3, interval = 100, exponential = false, factor = 2, jitter = 0, } = options ?? {};
107
+ let lastError = null;
108
+ let wait = interval;
109
+ for (let attempt = 0; attempt <= retriesMax; attempt++) {
110
+ try {
111
+ return await fn();
112
+ }
113
+ catch (error) {
114
+ lastError = error instanceof Error ? error : new Error(String(error));
115
+ if (attempt < retriesMax) {
116
+ // Calculate wait time
117
+ let waitTime = wait;
118
+ if (jitter > 0) {
119
+ waitTime += Math.random() * jitter;
120
+ }
121
+ await new Promise(resolve => setTimeout(resolve, waitTime));
122
+ // Increase wait for next attempt if exponential
123
+ if (exponential) {
124
+ wait *= factor;
125
+ }
126
+ }
127
+ }
128
+ }
129
+ throw lastError || new Error('Max retries exceeded');
130
+ }
131
+ function generateUUID() {
132
+ const { randomUUID } = require('crypto');
133
+ return randomUUID();
134
+ }
135
+ //# sourceMappingURL=util.js.map