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.
- package/CHANGELOG.md +10 -0
- package/LICENSE +20 -0
- package/PUBLISHING.md +80 -0
- package/README.md +233 -0
- package/SUPPORTED_DEVICES.md +206 -0
- package/config.schema.json +131 -0
- package/dist/cloud/api/TuyaHACloudAPI.js +286 -0
- package/dist/cloud/api/TuyaHASharingMQ.js +114 -0
- package/dist/cloud/device/TuyaDevice.js +50 -0
- package/dist/cloud/device/TuyaHADeviceManager.js +355 -0
- package/dist/index.js +7 -0
- package/dist/platform.js +397 -0
- package/dist/settings.js +18 -0
- package/dist/shared/AccessoryFactory.js +276 -0
- package/dist/shared/accessories/AccessoryFactory.js +305 -0
- package/dist/shared/accessories/AirConditionerAccessory.js +307 -0
- package/dist/shared/accessories/AirPurifierAccessory.js +90 -0
- package/dist/shared/accessories/AirQualitySensorAccessory.js +30 -0
- package/dist/shared/accessories/BaseAccessory.js +406 -0
- package/dist/shared/accessories/BlindsAccessory.js +199 -0
- package/dist/shared/accessories/CameraAccessory.js +121 -0
- package/dist/shared/accessories/CarbonDioxideSensorAccessory.js +52 -0
- package/dist/shared/accessories/CarbonMonoxideSensorAccessory.js +52 -0
- package/dist/shared/accessories/ContactSensorAccessory.js +30 -0
- package/dist/shared/accessories/DehumidifierAccessory.js +68 -0
- package/dist/shared/accessories/DiffuserAccessory.js +55 -0
- package/dist/shared/accessories/DimmerAccessory.js +94 -0
- package/dist/shared/accessories/DoorbellAccessory.js +91 -0
- package/dist/shared/accessories/ExtractionHoodAccessory.js +120 -0
- package/dist/shared/accessories/FanAccessory.js +129 -0
- package/dist/shared/accessories/GarageDoorAccessory.js +69 -0
- package/dist/shared/accessories/HeaterAccessory.js +102 -0
- package/dist/shared/accessories/HeaterAccessory_old.js +96 -0
- package/dist/shared/accessories/HumanPresenceSensorAccessory.js +20 -0
- package/dist/shared/accessories/HumidifierAccessory.js +137 -0
- package/dist/shared/accessories/IRAirConditionerAccessory.js +278 -0
- package/dist/shared/accessories/IRControlHubAccessory.js +49 -0
- package/dist/shared/accessories/IRControlHubSubAccessory.js +52 -0
- package/dist/shared/accessories/IRGenericAccessory.js +49 -0
- package/dist/shared/accessories/LeakSensorAccessory.js +36 -0
- package/dist/shared/accessories/LightAccessory.js +36 -0
- package/dist/shared/accessories/LightSensorAccessory.js +32 -0
- package/dist/shared/accessories/LocationWeatherAccessory.js +72 -0
- package/dist/shared/accessories/LockAccessory.js +56 -0
- package/dist/shared/accessories/MotionSensorAccessory.js +20 -0
- package/dist/shared/accessories/OutletAccessory.js +23 -0
- package/dist/shared/accessories/PetFeederAccessory.js +139 -0
- package/dist/shared/accessories/SceneAccessory.js +32 -0
- package/dist/shared/accessories/SceneSwitchAccessory.js +44 -0
- package/dist/shared/accessories/SecuritySystemAccessory.js +30 -0
- package/dist/shared/accessories/SmokeSensorAccessory.js +35 -0
- package/dist/shared/accessories/SwitchAccessory.js +148 -0
- package/dist/shared/accessories/TemperatureHumiditySensorAccessory.js +24 -0
- package/dist/shared/accessories/ThermostatAccessory.js +192 -0
- package/dist/shared/accessories/TowerRackAccessory.js +157 -0
- package/dist/shared/accessories/ValveAccessory.js +45 -0
- package/dist/shared/accessories/VibrationSensorAccessory.js +46 -0
- package/dist/shared/accessories/WeatherStationAccessory.js +58 -0
- package/dist/shared/accessories/WetBulbGlobeTemperatureAccessory.js +23 -0
- package/dist/shared/accessories/WhiteNoiseLightAccessory.js +59 -0
- package/dist/shared/accessories/WindowAccessory.js +14 -0
- package/dist/shared/accessories/WindowCoveringAccessory.js +156 -0
- package/dist/shared/accessories/WirelessSwitchAccessory.js +42 -0
- package/dist/shared/accessories/characteristic/Active.js +22 -0
- package/dist/shared/accessories/characteristic/AirQuality.js +74 -0
- package/dist/shared/accessories/characteristic/CurrentRelativeHumidity.js +23 -0
- package/dist/shared/accessories/characteristic/CurrentTemperature.js +23 -0
- package/dist/shared/accessories/characteristic/CurrentWeather.js +49 -0
- package/dist/shared/accessories/characteristic/CurrentWeatherByOpenMeteo.js +49 -0
- package/dist/shared/accessories/characteristic/CurrentWetBulbGlobeTemperature.js +48 -0
- package/dist/shared/accessories/characteristic/EnergyUsage.js +98 -0
- package/dist/shared/accessories/characteristic/Light.js +268 -0
- package/dist/shared/accessories/characteristic/LightSensor.js +23 -0
- package/dist/shared/accessories/characteristic/LockPhysicalControls.js +21 -0
- package/dist/shared/accessories/characteristic/MotionDetected.js +22 -0
- package/dist/shared/accessories/characteristic/Name.js +15 -0
- package/dist/shared/accessories/characteristic/OccupancyDetected.js +19 -0
- package/dist/shared/accessories/characteristic/On.js +25 -0
- package/dist/shared/accessories/characteristic/OutletInUse.js +14 -0
- package/dist/shared/accessories/characteristic/ProgrammableSwitchEvent.js +89 -0
- package/dist/shared/accessories/characteristic/RelativeHumidityDehumidifierThreshold.js +28 -0
- package/dist/shared/accessories/characteristic/RotationSpeed.js +78 -0
- package/dist/shared/accessories/characteristic/SecuritySystemState.js +74 -0
- package/dist/shared/accessories/characteristic/SwingMode.js +21 -0
- package/dist/shared/accessories/characteristic/TargetTemperature.js +29 -0
- package/dist/shared/accessories/characteristic/TemperatureDisplayUnits.js +25 -0
- package/dist/shared/util/ConfigHash.js +79 -0
- package/dist/shared/util/FfmpegStreamingProcess.js +126 -0
- package/dist/shared/util/InfraredTool.js +392 -0
- package/dist/shared/util/Logger.js +42 -0
- package/dist/shared/util/TuyaRecordingDelegate.js +22 -0
- package/dist/shared/util/TuyaStreamDelegate.js +329 -0
- package/dist/shared/util/color.js +23 -0
- package/dist/shared/util/util.js +135 -0
- package/homebridge-ui/public/index.html +329 -0
- package/homebridge-ui/server.js +224 -0
- 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
|