homebridge-nest-accfactory 0.0.4-a

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 (88) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/LICENSE +176 -0
  3. package/README.md +121 -0
  4. package/config.schema.json +107 -0
  5. package/dist/HomeKitDevice.js +441 -0
  6. package/dist/HomeKitHistory.js +2835 -0
  7. package/dist/camera.js +1276 -0
  8. package/dist/doorbell.js +122 -0
  9. package/dist/index.js +35 -0
  10. package/dist/nexustalk.js +741 -0
  11. package/dist/protect.js +240 -0
  12. package/dist/protobuf/google/rpc/status.proto +91 -0
  13. package/dist/protobuf/google/rpc/stream_body.proto +26 -0
  14. package/dist/protobuf/google/trait/product/camera.proto +53 -0
  15. package/dist/protobuf/googlehome/foyer.proto +208 -0
  16. package/dist/protobuf/nest/messages.proto +8 -0
  17. package/dist/protobuf/nest/services/apigateway.proto +107 -0
  18. package/dist/protobuf/nest/trait/audio.proto +7 -0
  19. package/dist/protobuf/nest/trait/cellular.proto +313 -0
  20. package/dist/protobuf/nest/trait/debug.proto +37 -0
  21. package/dist/protobuf/nest/trait/detector.proto +41 -0
  22. package/dist/protobuf/nest/trait/diagnostics.proto +87 -0
  23. package/dist/protobuf/nest/trait/firmware.proto +221 -0
  24. package/dist/protobuf/nest/trait/guest.proto +105 -0
  25. package/dist/protobuf/nest/trait/history.proto +345 -0
  26. package/dist/protobuf/nest/trait/humanlibrary.proto +19 -0
  27. package/dist/protobuf/nest/trait/hvac.proto +1353 -0
  28. package/dist/protobuf/nest/trait/input.proto +29 -0
  29. package/dist/protobuf/nest/trait/lighting.proto +61 -0
  30. package/dist/protobuf/nest/trait/located.proto +193 -0
  31. package/dist/protobuf/nest/trait/media.proto +68 -0
  32. package/dist/protobuf/nest/trait/network.proto +352 -0
  33. package/dist/protobuf/nest/trait/occupancy.proto +373 -0
  34. package/dist/protobuf/nest/trait/olive.proto +15 -0
  35. package/dist/protobuf/nest/trait/pairing.proto +85 -0
  36. package/dist/protobuf/nest/trait/product/camera.proto +283 -0
  37. package/dist/protobuf/nest/trait/product/detect.proto +67 -0
  38. package/dist/protobuf/nest/trait/product/doorbell.proto +18 -0
  39. package/dist/protobuf/nest/trait/product/guard.proto +59 -0
  40. package/dist/protobuf/nest/trait/product/protect.proto +344 -0
  41. package/dist/protobuf/nest/trait/promonitoring.proto +14 -0
  42. package/dist/protobuf/nest/trait/resourcedirectory.proto +32 -0
  43. package/dist/protobuf/nest/trait/safety.proto +119 -0
  44. package/dist/protobuf/nest/trait/security.proto +516 -0
  45. package/dist/protobuf/nest/trait/selftest.proto +78 -0
  46. package/dist/protobuf/nest/trait/sensor.proto +291 -0
  47. package/dist/protobuf/nest/trait/service.proto +46 -0
  48. package/dist/protobuf/nest/trait/structure.proto +85 -0
  49. package/dist/protobuf/nest/trait/system.proto +51 -0
  50. package/dist/protobuf/nest/trait/test.proto +15 -0
  51. package/dist/protobuf/nest/trait/ui.proto +65 -0
  52. package/dist/protobuf/nest/trait/user.proto +98 -0
  53. package/dist/protobuf/nest/trait/voiceassistant.proto +30 -0
  54. package/dist/protobuf/nestlabs/eventingapi/v1.proto +83 -0
  55. package/dist/protobuf/nestlabs/gateway/v1.proto +273 -0
  56. package/dist/protobuf/nestlabs/gateway/v2.proto +96 -0
  57. package/dist/protobuf/nestlabs/history/v1.proto +73 -0
  58. package/dist/protobuf/root.proto +64 -0
  59. package/dist/protobuf/wdl-event-importance.proto +11 -0
  60. package/dist/protobuf/wdl.proto +450 -0
  61. package/dist/protobuf/weave/common.proto +144 -0
  62. package/dist/protobuf/weave/trait/audio.proto +12 -0
  63. package/dist/protobuf/weave/trait/auth.proto +22 -0
  64. package/dist/protobuf/weave/trait/description.proto +32 -0
  65. package/dist/protobuf/weave/trait/heartbeat.proto +38 -0
  66. package/dist/protobuf/weave/trait/locale.proto +20 -0
  67. package/dist/protobuf/weave/trait/network.proto +24 -0
  68. package/dist/protobuf/weave/trait/pairing.proto +8 -0
  69. package/dist/protobuf/weave/trait/peerdevices.proto +18 -0
  70. package/dist/protobuf/weave/trait/power.proto +86 -0
  71. package/dist/protobuf/weave/trait/schedule.proto +76 -0
  72. package/dist/protobuf/weave/trait/security.proto +343 -0
  73. package/dist/protobuf/weave/trait/telemetry/tunnel.proto +37 -0
  74. package/dist/protobuf/weave/trait/time.proto +16 -0
  75. package/dist/res/Nest_camera_connecting.h264 +0 -0
  76. package/dist/res/Nest_camera_connecting.jpg +0 -0
  77. package/dist/res/Nest_camera_off.h264 +0 -0
  78. package/dist/res/Nest_camera_off.jpg +0 -0
  79. package/dist/res/Nest_camera_offline.h264 +0 -0
  80. package/dist/res/Nest_camera_offline.jpg +0 -0
  81. package/dist/res/Nest_camera_transfer.jpg +0 -0
  82. package/dist/streamer.js +344 -0
  83. package/dist/system.js +3112 -0
  84. package/dist/tempsensor.js +99 -0
  85. package/dist/thermostat.js +1026 -0
  86. package/dist/weather.js +205 -0
  87. package/dist/webrtc.js +55 -0
  88. package/package.json +66 -0
@@ -0,0 +1,344 @@
1
+ // streamer
2
+ // Part of homebridge-nest-accfactory
3
+ //
4
+ // This is the base class for all Camera/Doorbell streaming
5
+ //
6
+ // Buffers a single audio/video stream which allows multiple HomeKit devices to connect to the single stream
7
+ // for live viewing and/or recording
8
+ //
9
+ // The following functions should be overriden in your class which extends this
10
+ //
11
+ // streamer.connect(host)
12
+ // streamer.close(stopStreamFirst)
13
+ // streamer.talkingAudio(talkingData)
14
+ // streamer.update(deviceData) <- call super after
15
+ //
16
+ // Code version 6/9/2024
17
+ // Mark Hulskamp
18
+ 'use strict';
19
+
20
+ // Define nodejs module requirements
21
+ import { Buffer } from 'node:buffer';
22
+ import { setInterval, setTimeout, clearTimeout } from 'node:timers';
23
+ import fs from 'fs';
24
+ import path from 'node:path';
25
+ import { fileURLToPath } from 'node:url';
26
+
27
+ // Define constants
28
+ const CAMERAOFFLINEH264FILE = 'Nest_camera_offline.h264'; // Camera offline H264 frame file
29
+ const CAMERAOFFH264FILE = 'Nest_camera_off.h264'; // Camera off H264 frame file
30
+
31
+ const H264NALStartcode = Buffer.from([0x00, 0x00, 0x00, 0x01]);
32
+ const AACAudioSilence = Buffer.from([0x21, 0x10, 0x01, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
33
+
34
+ const __dirname = path.dirname(fileURLToPath(import.meta.url)); // Make a defined for JS __dirname
35
+
36
+ // Streamer object
37
+ export default class Streamer {
38
+ cameraOfflineFrame = undefined;
39
+ cameraVideoOffFrame = undefined;
40
+ videoEnabled = undefined;
41
+ audioEnabled = undefined;
42
+ online = undefined;
43
+ host = ''; // Host to connect to or connected too
44
+ socket = null; // TCP socket object
45
+
46
+ // Internal data only for this class
47
+ #outputTimer = undefined; // Timer for non-blocking loop to stream output data
48
+ #outputs = {}; // Output streams ie: buffer, live, record
49
+
50
+ constructor(deviceData, options) {
51
+ // Setup logger object if passed as option
52
+ if (
53
+ typeof options?.log?.info === 'function' &&
54
+ typeof options?.log?.success === 'function' &&
55
+ typeof options?.log?.warn === 'function' &&
56
+ typeof options?.log?.error === 'function' &&
57
+ typeof options?.log?.debug === 'function'
58
+ ) {
59
+ this.log = options.log;
60
+ }
61
+
62
+ // Store data we need from the device data passed it
63
+ this.online = deviceData?.online === true;
64
+ this.videoEnabled = deviceData?.streaming_enabled === true;
65
+ this.audioEnabled = deviceData?.audio_enabled === true;
66
+
67
+ // Setup location for *.h264 frame files. This can be overriden by a passed in option
68
+ let resourcePath = path.resolve(__dirname + '/res'); // Default location for *.h264 files
69
+ if (
70
+ typeof options?.resourcePath === 'string' &&
71
+ options.resourcePath !== '' &&
72
+ fs.existsSync(path.resolve(options.resourcePath)) === true
73
+ ) {
74
+ resourcePath = path.resolve(options.resourcePath);
75
+ }
76
+
77
+ // load buffer for camera offline image in .h264 frame
78
+ if (fs.existsSync(path.resolve(resourcePath + '/' + CAMERAOFFLINEH264FILE)) === true) {
79
+ this.cameraOfflineFrame = fs.readFileSync(path.resolve(resourcePath + '/' + CAMERAOFFLINEH264FILE));
80
+ // remove any H264 NALU from beginning of any video data. We do this as they are added later when output by our ffmpeg router
81
+ if (this.cameraOfflineFrame.indexOf(H264NALStartcode) === 0) {
82
+ this.cameraOfflineFrame = this.cameraOfflineFrame.subarray(H264NALStartcode.length);
83
+ }
84
+ }
85
+
86
+ // load buffer for camera stream off image in .h264 frame
87
+ if (fs.existsSync(path.resolve(resourcePath + '/' + CAMERAOFFH264FILE)) === true) {
88
+ this.cameraVideoOffFrame = fs.readFileSync(path.resolve(resourcePath + '/' + CAMERAOFFH264FILE));
89
+ // remove any H264 NALU from beginning of any video data. We do this as they are added later when output by our ffmpeg router
90
+ if (this.cameraVideoOffFrame.indexOf(H264NALStartcode) === 0) {
91
+ this.cameraVideoOffFrame = this.cameraVideoOffFrame.subarray(H264NALStartcode.length);
92
+ }
93
+ }
94
+
95
+ // Start a non-blocking loop for output to the various streams which connect to our streamer object
96
+ // This process will also handle the rolling-buffer size we require
97
+ // Record streams will always start from the beginning of the buffer (tail)
98
+ // Live streams will always start from the end of the buffer (head)
99
+ let lastTimeVideo = Date.now();
100
+ this.#outputTimer = setInterval(() => {
101
+ let dateNow = Date.now();
102
+ let outputVideoFrame = dateNow > lastTimeVideo + 90000 / 30; // 30 or 15 fps?
103
+ Object.values(this.#outputs).forEach((output) => {
104
+ // Monitor for camera going offline and/or video enabled/disabled
105
+ // We'll insert the appropriate video frame into the stream
106
+ if (this.online === false && this.cameraOfflineFrame !== undefined && outputVideoFrame === true) {
107
+ // Camera is offline so feed in our custom h264 frame and AAC silence
108
+ output.buffer.push({ type: 'video', time: dateNow, data: this.cameraOfflineFrame });
109
+ output.buffer.push({ type: 'audio', time: dateNow, data: AACAudioSilence });
110
+ lastTimeVideo = dateNow;
111
+ }
112
+ if (this.online === true && this.videoEnabled === false && this.cameraVideoOffFrame !== undefined && outputVideoFrame === true) {
113
+ // Camera video is turned off so feed in our custom h264 frame and AAC silence
114
+ output.buffer.push({ type: 'video', time: dateNow, data: this.cameraVideoOffFrame });
115
+ output.buffer.push({ type: 'audio', time: dateNow, data: AACAudioSilence });
116
+ lastTimeVideo = dateNow;
117
+ }
118
+
119
+ // Keep our 'main' rolling buffer under a certain size
120
+ // Live/record buffers will always reduce in length in the next section
121
+ // <---- maybe make this time based x time since first packet in buffer?
122
+ if (output.type === 'buffer' && output.buffer.length > 1250) {
123
+ output.buffer.shift();
124
+ }
125
+
126
+ // Output the packet data to any streams 'live' or 'recording' streams
127
+ if (output.type === 'live' || output.type === 'record') {
128
+ let packet = output.buffer.shift();
129
+ if (packet?.type === 'video' && typeof output?.video?.write === 'function') {
130
+ // H264 NAL Units '0001' are required to be added to beginning of any video data we output
131
+ // If this is missing, add on beginning of data packet
132
+ if (packet.data.indexOf(H264NALStartcode) !== 0) {
133
+ packet.data = Buffer.concat([H264NALStartcode, packet.data]);
134
+ }
135
+ output.video.write(packet.data);
136
+ }
137
+ if (packet?.type === 'audio' && typeof output?.audio?.write === 'function') {
138
+ output.audio.write(packet.data);
139
+ }
140
+ }
141
+ });
142
+ }, 0);
143
+ }
144
+
145
+ // Class functions
146
+ isBuffering() {
147
+ return this.#outputs?.buffer !== undefined;
148
+ }
149
+
150
+ startBuffering() {
151
+ if (this.#outputs?.buffer === undefined) {
152
+ // No active buffer session, start connection to streamer
153
+ if (this.socket === null && typeof this.host === 'string' && this.host !== '') {
154
+ if (typeof this.connect === 'function') {
155
+ this.connect(this.host);
156
+ this?.log?.debug && this.log.debug('Started buffering from "%s"', this.host);
157
+ }
158
+ }
159
+
160
+ this.#outputs.buffer = {
161
+ type: 'buffer',
162
+ buffer: [],
163
+ };
164
+ }
165
+ }
166
+
167
+ startLiveStream(sessionID, videoStream, audioStream, talkbackStream) {
168
+ // Setup error catching for video/audio/talkback streams
169
+ if (videoStream !== null && typeof videoStream === 'object') {
170
+ videoStream.on('error', () => {
171
+ // EPIPE errors??
172
+ });
173
+ }
174
+
175
+ if (audioStream !== null && typeof audioStream === 'object') {
176
+ audioStream.on('error', () => {
177
+ // EPIPE errors??
178
+ });
179
+ }
180
+
181
+ if (talkbackStream !== null && typeof talkbackStream === 'object') {
182
+ let talkbackTimeout = undefined;
183
+
184
+ talkbackStream.on('error', () => {
185
+ // EPIPE errors??
186
+ });
187
+
188
+ talkbackStream.on('data', (data) => {
189
+ // Received audio data to send onto camera/doorbell for output
190
+ if (typeof this.talkingAudio === 'function') {
191
+ this.talkingAudio(data);
192
+
193
+ talkbackTimeout = clearTimeout(talkbackTimeout);
194
+ talkbackTimeout = setTimeout(() => {
195
+ // no audio received in 500ms, so mark end of stream
196
+ this.talkingAudio(Buffer.alloc(0));
197
+ }, 500);
198
+ }
199
+ });
200
+ }
201
+
202
+ if (this.socket === null && typeof this.host === 'string' && this.host !== '') {
203
+ // We do not have an active socket connection, so startup connection to host
204
+ if (typeof this.connect === 'function') {
205
+ this.connect(this.host);
206
+ }
207
+ }
208
+
209
+ // Add video/audio streams for our output loop to handle outputting to
210
+ this.#outputs[sessionID] = {
211
+ type: 'live',
212
+ video: videoStream,
213
+ audio: audioStream,
214
+ talk: talkbackStream,
215
+ buffer: [],
216
+ };
217
+
218
+ // finally, we've started live stream
219
+ this?.log?.debug &&
220
+ this.log.debug(
221
+ 'Started live stream from "%s" %s and sesssion id of "%s"',
222
+ this.host,
223
+ talkbackStream !== null && typeof talkbackStream === 'object' ? 'with two-way audio' : '',
224
+ sessionID,
225
+ );
226
+ }
227
+
228
+ startRecordStream(sessionID, videoStream, audioStream) {
229
+ // Setup error catching for video/audio streams
230
+ if (videoStream !== null && typeof videoStream === 'object') {
231
+ videoStream.on('error', () => {
232
+ // EPIPE errors??
233
+ });
234
+ }
235
+
236
+ if (audioStream !== null && typeof audioStream === 'object') {
237
+ audioStream.on('error', () => {
238
+ // EPIPE errors??
239
+ });
240
+ }
241
+
242
+ if (this.socket === null && typeof this.host === 'string' && this.host !== '') {
243
+ // We do not have an active socket connection, so startup connection to host
244
+ if (typeof this.connect === 'function') {
245
+ this.connect(this.host);
246
+ }
247
+ }
248
+
249
+ // Add video/audio streams for our output loop to handle outputting to
250
+ this.#outputs[sessionID] = {
251
+ type: 'record',
252
+ video: videoStream,
253
+ audio: audioStream,
254
+ // eslint-disable-next-line no-undef
255
+ buffer: this.#outputs?.buffer?.buffer !== undefined ? structuredClone(this.#outputs.buffer.buffer) : [],
256
+ };
257
+
258
+ // Finally we've started the recording stream
259
+ this?.log?.debug && this.log.debug('Started recording stream from "%s" with sesison id of "%s"', this.host, sessionID);
260
+ }
261
+
262
+ stopRecordStream(sessionID) {
263
+ // Request to stop a recording stream
264
+ if (typeof this.#outputs[sessionID] === 'object') {
265
+ this?.log?.debug && this.log.debug('Stopped recording stream from "%s"', this.host);
266
+ delete this.#outputs[sessionID];
267
+ }
268
+
269
+ // If we have no more output streams active, we'll close the connection to host
270
+ if (Object.keys(this.#outputs).length === 0 && typeof this.close === 'function') {
271
+ this.close(true);
272
+ }
273
+ }
274
+
275
+ stopLiveStream(sessionID) {
276
+ // Request to stop an active live stream
277
+ if (typeof this.#outputs[sessionID] === 'object') {
278
+ this?.log?.debug && this.log.debug('Stopped live stream from "%s"', this.host);
279
+ delete this.#outputs[sessionID];
280
+ }
281
+
282
+ // If we have no more output streams active, we'll close the connection to host
283
+ if (Object.keys(this.#outputs).length === 0 && typeof this.close === 'function') {
284
+ this.close(true);
285
+ }
286
+ }
287
+
288
+ stopBuffering() {
289
+ if (this.#outputs?.buffer !== undefined) {
290
+ this?.log?.debug && this.log.debug('Stopped buffering from "%s"', this.host);
291
+ delete this.#outputs.buffer;
292
+ }
293
+
294
+ // If we have no more output streams active, we'll close the connection to host
295
+ if (Object.keys(this.#outputs).length === 0 && typeof this.close === 'function') {
296
+ this.close(true);
297
+ }
298
+ }
299
+
300
+ update(deviceData) {
301
+ if (typeof deviceData !== 'object') {
302
+ return;
303
+ }
304
+
305
+ this.online = deviceData?.online === true;
306
+ this.videoEnabled = deviceData?.streaming_enabled === true;
307
+ this.audioEnabled = deviceData?.audio_enabled === true;
308
+
309
+ if (this.host !== deviceData.streaming_host) {
310
+ this.host = deviceData.streaming_host;
311
+ this?.log?.debug && this.log.debug('New streaming host has requested a new host "%s" for connection', this.host);
312
+ }
313
+
314
+ if (this.online !== deviceData.online || this.videoEnabled !== deviceData.streaming_enabled) {
315
+ // Online status or streaming status has changed has changed
316
+ this.online = deviceData?.online === true;
317
+ this.videoEnabled = deviceData?.streaming_enabled === true;
318
+ if ((this.online === false || this.videoEnabled === false) && typeof this.close === 'function') {
319
+ this.close(true); // as offline or streaming not enabled, close socket
320
+ }
321
+ if (this.online === true && this.videoEnabled === true && typeof this.connect === 'function') {
322
+ this.connect(this.host); // Connect to host for stream
323
+ }
324
+ }
325
+ }
326
+
327
+ addToOutput(type, time, data) {
328
+ if (typeof type !== 'string' || type === '' || typeof time !== 'number' || time === 0) {
329
+ return;
330
+ }
331
+
332
+ Object.values(this.#outputs).forEach((output) => {
333
+ output.buffer.push({
334
+ type: type,
335
+ time: time,
336
+ data: data,
337
+ });
338
+ });
339
+ }
340
+
341
+ haveOutputs() {
342
+ return Object.keys(this.#outputs).length > 0;
343
+ }
344
+ }