homebridge-nest-accfactory 0.2.9 → 0.3.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 +29 -1
- package/README.md +14 -7
- package/config.schema.json +126 -1
- package/dist/HomeKitDevice.js +194 -77
- package/dist/HomeKitHistory.js +1 -1
- package/dist/config.js +207 -0
- package/dist/devices.js +113 -0
- package/dist/index.js +2 -1
- package/dist/nexustalk.js +19 -21
- package/dist/{camera.js → plugins/camera.js} +212 -239
- package/dist/{doorbell.js → plugins/doorbell.js} +32 -30
- package/dist/plugins/floodlight.js +91 -0
- package/dist/plugins/heatlink.js +17 -0
- package/dist/{protect.js → plugins/protect.js} +24 -41
- package/dist/{tempsensor.js → plugins/tempsensor.js} +13 -17
- package/dist/{thermostat.js → plugins/thermostat.js} +424 -381
- package/dist/{weather.js → plugins/weather.js} +26 -60
- package/dist/protobuf/google/trait/product/camera.proto +1 -1
- package/dist/protobuf/googlehome/foyer.proto +0 -46
- package/dist/protobuf/nest/services/apigateway.proto +31 -1
- package/dist/protobuf/nest/trait/firmware.proto +207 -89
- package/dist/protobuf/nest/trait/hvac.proto +1052 -312
- package/dist/protobuf/nest/trait/located.proto +51 -8
- package/dist/protobuf/nest/trait/network.proto +366 -36
- package/dist/protobuf/nest/trait/occupancy.proto +145 -17
- package/dist/protobuf/nest/trait/product/protect.proto +57 -43
- package/dist/protobuf/nest/trait/resourcedirectory.proto +8 -0
- package/dist/protobuf/nest/trait/sensor.proto +7 -1
- package/dist/protobuf/nest/trait/service.proto +3 -1
- package/dist/protobuf/nest/trait/structure.proto +60 -14
- package/dist/protobuf/nest/trait/ui.proto +41 -1
- package/dist/protobuf/nest/trait/user.proto +6 -1
- package/dist/protobuf/nest/trait/voiceassistant.proto +2 -1
- package/dist/protobuf/nestlabs/eventingapi/v1.proto +20 -1
- package/dist/protobuf/root.proto +1 -0
- package/dist/protobuf/wdl.proto +18 -2
- package/dist/protobuf/weave/common.proto +2 -1
- package/dist/protobuf/weave/trait/heartbeat.proto +41 -1
- package/dist/protobuf/weave/trait/power.proto +1 -0
- package/dist/protobuf/weave/trait/security.proto +10 -1
- package/dist/streamer.js +80 -80
- package/dist/system.js +1208 -1245
- package/dist/webrtc.js +28 -23
- package/package.json +12 -12
- package/dist/floodlight.js +0 -97
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// Nest Cameras
|
|
2
2
|
// Part of homebridge-nest-accfactory
|
|
3
3
|
//
|
|
4
|
-
// Code version 2025/03/19
|
|
5
4
|
// Mark Hulskamp
|
|
6
5
|
'use strict';
|
|
7
6
|
|
|
@@ -18,18 +17,26 @@ import path from 'node:path';
|
|
|
18
17
|
import { fileURLToPath } from 'node:url';
|
|
19
18
|
|
|
20
19
|
// Define our modules
|
|
21
|
-
import HomeKitDevice from '
|
|
22
|
-
import NexusTalk from '
|
|
23
|
-
import WebRTC from '
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
20
|
+
import HomeKitDevice from '../HomeKitDevice.js';
|
|
21
|
+
import NexusTalk from '../nexustalk.js';
|
|
22
|
+
import WebRTC from '../webrtc.js';
|
|
23
|
+
|
|
24
|
+
const CAMERARESOURCE = {
|
|
25
|
+
offline: 'Nest_camera_offline.jpg',
|
|
26
|
+
off: 'Nest_camera_off.jpg',
|
|
27
|
+
transfer: 'Nest_camera_transfer.jpg',
|
|
28
|
+
};
|
|
28
29
|
const MP4BOX = 'mp4box'; // MP4 box fragement event for HKSV recording
|
|
29
30
|
const SNAPSHOTCACHETIMEOUT = 30000; // Timeout for retaining snapshot image (in milliseconds)
|
|
31
|
+
const PROTOCOLWEBRTC = 'PROTOCOL_WEBRTC';
|
|
32
|
+
const PROTOCOLNEXUSTALK = 'PROTOCOL_NEXUSTALK';
|
|
33
|
+
const RESOURCEPATH = '../res';
|
|
30
34
|
const __dirname = path.dirname(fileURLToPath(import.meta.url)); // Make a defined for JS __dirname
|
|
31
35
|
|
|
32
36
|
export default class NestCamera extends HomeKitDevice {
|
|
37
|
+
static TYPE = 'Camera';
|
|
38
|
+
static VERSION = '2025.06.12';
|
|
39
|
+
|
|
33
40
|
controller = undefined; // HomeKit Camera/Doorbell controller service
|
|
34
41
|
streamer = undefined; // Streamer object for live/recording stream
|
|
35
42
|
motionServices = undefined; // Object of Camera/Doorbell motion sensor(s)
|
|
@@ -51,27 +58,25 @@ export default class NestCamera extends HomeKitDevice {
|
|
|
51
58
|
constructor(accessory, api, log, eventEmitter, deviceData) {
|
|
52
59
|
super(accessory, api, log, eventEmitter, deviceData);
|
|
53
60
|
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
61
|
+
// Load supporrt image files as required
|
|
62
|
+
const loadImageIfExists = (filename, label) => {
|
|
63
|
+
let buffer = undefined;
|
|
64
|
+
let file = path.resolve(__dirname, RESOURCEPATH, filename);
|
|
65
|
+
if (fs.existsSync(file) === true) {
|
|
66
|
+
buffer = fs.readFileSync(file);
|
|
67
|
+
} else {
|
|
68
|
+
this.log?.warn?.('Failed to load %s image resource for "%s"', label, this.deviceData.description);
|
|
69
|
+
}
|
|
70
|
+
return buffer;
|
|
71
|
+
};
|
|
65
72
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
this.#cameraTransferringImage = fs.readFileSync(imageFile);
|
|
70
|
-
}
|
|
73
|
+
this.#cameraOfflineImage = loadImageIfExists(CAMERARESOURCE.offline, 'offline');
|
|
74
|
+
this.#cameraVideoOffImage = loadImageIfExists(CAMERARESOURCE.off, 'video off');
|
|
75
|
+
this.#cameraTransferringImage = loadImageIfExists(CAMERARESOURCE.transfer, 'transferring');
|
|
71
76
|
}
|
|
72
77
|
|
|
73
78
|
// Class functions
|
|
74
|
-
|
|
79
|
+
setupDevice(hapController = this.hap.CameraController) {
|
|
75
80
|
// Setup motion services
|
|
76
81
|
if (this.motionServices === undefined) {
|
|
77
82
|
this.createCameraMotionServices();
|
|
@@ -92,19 +97,13 @@ export default class NestCamera extends HomeKitDevice {
|
|
|
92
97
|
if (this.operatingModeService === undefined) {
|
|
93
98
|
// Add in operating mode service for a non-hksv camera/doorbell
|
|
94
99
|
// Allow us to change things such as night vision, camera indicator etc within HomeKit for those also :-)
|
|
95
|
-
this.operatingModeService = this.
|
|
96
|
-
if (this.operatingModeService === undefined) {
|
|
97
|
-
this.operatingModeService = this.accessory.addService(this.hap.Service.CameraOperatingMode, '', 1);
|
|
98
|
-
}
|
|
100
|
+
this.operatingModeService = this.addHKService(this.hap.Service.CameraOperatingMode, '', 1);
|
|
99
101
|
}
|
|
100
102
|
|
|
101
|
-
// Setup set
|
|
102
|
-
if (this.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
this.operatingModeService.addOptionalCharacteristic(this.hap.Characteristic.CameraOperatingModeIndicator);
|
|
106
|
-
}
|
|
107
|
-
this.operatingModeService.getCharacteristic(this.hap.Characteristic.CameraOperatingModeIndicator).onSet((value) => {
|
|
103
|
+
// Setup set characteristics
|
|
104
|
+
if (this.deviceData?.has_statusled === true) {
|
|
105
|
+
this.addHKCharacteristic(this.operatingModeService, this.hap.Characteristic.CameraOperatingModeIndicator, {
|
|
106
|
+
onSet: (value) => {
|
|
108
107
|
// 0 = auto, 1 = low, 2 = high
|
|
109
108
|
// We'll use auto mode for led on and low for led off
|
|
110
109
|
if (
|
|
@@ -112,43 +111,33 @@ export default class NestCamera extends HomeKitDevice {
|
|
|
112
111
|
(value === false && this.deviceData.statusled_brightness !== 1)
|
|
113
112
|
) {
|
|
114
113
|
this.set({ uuid: this.deviceData.nest_google_uuid, statusled_brightness: value === true ? 0 : 1 });
|
|
115
|
-
|
|
116
|
-
this.log.info('Recording status LED on "%s" was turned', this.deviceData.description, value === true ? 'on' : 'off');
|
|
117
|
-
}
|
|
114
|
+
this?.log?.info?.('Recording status LED on "%s" was turned', this.deviceData.description, value === true ? 'on' : 'off');
|
|
118
115
|
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
this.operatingModeService.getCharacteristic(this.hap.Characteristic.CameraOperatingModeIndicator).onGet(() => {
|
|
116
|
+
},
|
|
117
|
+
onGet: () => {
|
|
122
118
|
return this.deviceData.statusled_brightness !== 1;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (this.deviceData.has_irled === true) {
|
|
127
|
-
if (this.operatingModeService.testCharacteristic(this.hap.Characteristic.NightVision) === false) {
|
|
128
|
-
this.operatingModeService.addOptionalCharacteristic(this.hap.Characteristic.NightVision);
|
|
129
|
-
}
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
}
|
|
130
122
|
|
|
131
|
-
|
|
123
|
+
if (this.deviceData?.has_irled === true) {
|
|
124
|
+
this.addHKCharacteristic(this.operatingModeService, this.hap.Characteristic.NightVision, {
|
|
125
|
+
onSet: (value) => {
|
|
132
126
|
// only change IRLed status value if different than on-device
|
|
133
127
|
if ((value === false && this.deviceData.irled_enabled === true) || (value === true && this.deviceData.irled_enabled === false)) {
|
|
134
128
|
this.set({ uuid: this.deviceData.nest_google_uuid, irled_enabled: value === true ? 'auto_on' : 'always_off' });
|
|
135
129
|
|
|
136
|
-
|
|
137
|
-
this.log.info('Night vision on "%s" was turned', this.deviceData.description, value === true ? 'on' : 'off');
|
|
138
|
-
}
|
|
130
|
+
this?.log?.info?.('Night vision on "%s" was turned', this.deviceData.description, value === true ? 'on' : 'off');
|
|
139
131
|
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
this.operatingModeService.getCharacteristic(this.hap.Characteristic.NightVision).onGet(() => {
|
|
132
|
+
},
|
|
133
|
+
onGet: () => {
|
|
143
134
|
return this.deviceData.irled_enabled;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (this.operatingModeService.testCharacteristic(this.hap.Characteristic.ManuallyDisabled) === false) {
|
|
148
|
-
this.operatingModeService.addOptionalCharacteristic(this.hap.Characteristic.ManuallyDisabled);
|
|
149
|
-
}
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
}
|
|
150
138
|
|
|
151
|
-
|
|
139
|
+
this.addHKCharacteristic(this.operatingModeService, this.hap.Characteristic.ManuallyDisabled, {
|
|
140
|
+
onSet: (value) => {
|
|
152
141
|
if (value !== this.operatingModeService.getCharacteristic(this.hap.Characteristic.ManuallyDisabled).value) {
|
|
153
142
|
// Make sure only updating status if HomeKit value *actually changes*
|
|
154
143
|
if (
|
|
@@ -157,35 +146,31 @@ export default class NestCamera extends HomeKitDevice {
|
|
|
157
146
|
) {
|
|
158
147
|
// Camera state does not reflect requested state, so fix
|
|
159
148
|
this.set({ uuid: this.deviceData.nest_google_uuid, streaming_enabled: value === false ? true : false });
|
|
160
|
-
|
|
161
|
-
this.log.info('Camera on "%s" was turned', this.deviceData.description, value === false ? 'on' : 'off');
|
|
162
|
-
}
|
|
149
|
+
this?.log?.info?.('Camera on "%s" was turned', this.deviceData.description, value === false ? 'on' : 'off');
|
|
163
150
|
}
|
|
164
151
|
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
this.operatingModeService.getCharacteristic(this.hap.Characteristic.ManuallyDisabled).onGet(() => {
|
|
152
|
+
},
|
|
153
|
+
onGet: () => {
|
|
168
154
|
return this.deviceData.streaming_enabled === false
|
|
169
155
|
? this.hap.Characteristic.ManuallyDisabled.DISABLED
|
|
170
156
|
: this.hap.Characteristic.ManuallyDisabled.ENABLED;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (this.deviceData.has_video_flip === true) {
|
|
174
|
-
if (this.operatingModeService.testCharacteristic(this.hap.Characteristic.ImageRotation) === false) {
|
|
175
|
-
this.operatingModeService.addOptionalCharacteristic(this.hap.Characteristic.ImageRotation);
|
|
176
|
-
}
|
|
157
|
+
},
|
|
158
|
+
});
|
|
177
159
|
|
|
178
|
-
|
|
160
|
+
if (this.deviceData?.has_video_flip === true) {
|
|
161
|
+
this.addHKCharacteristic(this.operatingModeService, this.hap.Characteristic.ImageRotation, {
|
|
162
|
+
onGet: () => {
|
|
179
163
|
return this.deviceData.video_flipped === true ? 180 : 0;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
164
|
+
},
|
|
165
|
+
});
|
|
182
166
|
}
|
|
183
167
|
|
|
184
|
-
if (this.controller?.recordingManagement?.recordingManagementService !== undefined) {
|
|
185
|
-
|
|
186
|
-
this.controller.recordingManagement.recordingManagementService
|
|
187
|
-
|
|
188
|
-
|
|
168
|
+
if (this.controller?.recordingManagement?.recordingManagementService !== undefined && this.deviceData.has_microphone === true) {
|
|
169
|
+
this.addHKCharacteristic(
|
|
170
|
+
this.controller.recordingManagement.recordingManagementService,
|
|
171
|
+
this.hap.Characteristic.RecordingAudioActive,
|
|
172
|
+
{
|
|
173
|
+
onSet: (value) => {
|
|
189
174
|
if (
|
|
190
175
|
(this.deviceData.audio_enabled === true && value === this.hap.Characteristic.RecordingAudioActive.DISABLE) ||
|
|
191
176
|
(this.deviceData.audio_enabled === false && value === this.hap.Characteristic.RecordingAudioActive.ENABLE)
|
|
@@ -194,42 +179,37 @@ export default class NestCamera extends HomeKitDevice {
|
|
|
194
179
|
uuid: this.deviceData.nest_google_uuid,
|
|
195
180
|
audio_enabled: value === this.hap.Characteristic.RecordingAudioActive.ENABLE ? true : false,
|
|
196
181
|
});
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
);
|
|
203
|
-
}
|
|
182
|
+
this?.log?.info?.(
|
|
183
|
+
'Audio recording on "%s" was turned',
|
|
184
|
+
this.deviceData.description,
|
|
185
|
+
value === this.hap.Characteristic.RecordingAudioActive.ENABLE ? 'on' : 'off',
|
|
186
|
+
);
|
|
204
187
|
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
this.controller.recordingManagement.recordingManagementService
|
|
208
|
-
.getCharacteristic(this.hap.Characteristic.RecordingAudioActive)
|
|
209
|
-
.onGet(() => {
|
|
188
|
+
},
|
|
189
|
+
onGet: () => {
|
|
210
190
|
return this.deviceData.audio_enabled === true
|
|
211
191
|
? this.hap.Characteristic.RecordingAudioActive.ENABLE
|
|
212
192
|
: this.hap.Characteristic.RecordingAudioActive.DISABLE;
|
|
213
|
-
}
|
|
214
|
-
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
);
|
|
215
196
|
}
|
|
216
197
|
|
|
217
198
|
if (this.deviceData.migrating === true) {
|
|
218
199
|
// Migration happening between Nest <-> Google Home apps
|
|
219
|
-
this?.log?.warn
|
|
200
|
+
this?.log?.warn?.('Migration between Nest <-> Google Home apps is underway for "%s"', this.deviceData.description);
|
|
220
201
|
}
|
|
221
202
|
|
|
222
203
|
if (
|
|
223
|
-
(this.deviceData.streaming_protocols.includes(
|
|
224
|
-
this.deviceData.streaming_protocols.includes(
|
|
225
|
-
(this.deviceData.streaming_protocols.includes(
|
|
226
|
-
(this.deviceData.streaming_protocols.includes(
|
|
204
|
+
(this.deviceData.streaming_protocols.includes(PROTOCOLWEBRTC) === false &&
|
|
205
|
+
this.deviceData.streaming_protocols.includes(PROTOCOLNEXUSTALK) === false) ||
|
|
206
|
+
(this.deviceData.streaming_protocols.includes(PROTOCOLWEBRTC) === true && WebRTC === undefined) ||
|
|
207
|
+
(this.deviceData.streaming_protocols.includes(PROTOCOLNEXUSTALK) === true && NexusTalk === undefined)
|
|
227
208
|
) {
|
|
228
|
-
this?.log?.error
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
);
|
|
209
|
+
this?.log?.error?.(
|
|
210
|
+
'No suitable streaming protocol is present for "%s". Streaming and recording will be unavailable',
|
|
211
|
+
this.deviceData.description,
|
|
212
|
+
);
|
|
233
213
|
}
|
|
234
214
|
|
|
235
215
|
// Setup linkage to EveHome app if configured todo so
|
|
@@ -243,17 +223,13 @@ export default class NestCamera extends HomeKitDevice {
|
|
|
243
223
|
});
|
|
244
224
|
}
|
|
245
225
|
|
|
246
|
-
//
|
|
247
|
-
let postSetupDetails = [];
|
|
226
|
+
// Extra setup details for output
|
|
248
227
|
this.deviceData.hksv === true &&
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
);
|
|
252
|
-
this.deviceData.localAccess === true && postSetupDetails.push('Local access');
|
|
253
|
-
return postSetupDetails;
|
|
228
|
+
this.postSetupDetail('HomeKit Secure Video support' + (this.streamer?.isBuffering() === true ? ' and recording buffer started' : ''));
|
|
229
|
+
this.deviceData.localAccess === true && this.postSetupDetail('Local access');
|
|
254
230
|
}
|
|
255
231
|
|
|
256
|
-
|
|
232
|
+
removeDevice() {
|
|
257
233
|
// Clean up our camera object since this device is being removed
|
|
258
234
|
clearTimeout(this.motionTimer);
|
|
259
235
|
clearTimeout(this.personTimer);
|
|
@@ -298,11 +274,10 @@ export default class NestCamera extends HomeKitDevice {
|
|
|
298
274
|
// https://github.com/hjdhjd/homebridge-unifi-protect/blob/eee6a4e379272b659baa6c19986d51f5bf2cbbbc/src/protect-ffmpeg-record.ts
|
|
299
275
|
async *handleRecordingStreamRequest(sessionID) {
|
|
300
276
|
if (this.deviceData?.ffmpeg?.binary === undefined) {
|
|
301
|
-
this?.log?.warn
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
);
|
|
277
|
+
this?.log?.warn?.(
|
|
278
|
+
'Received request to start recording for "%s" however we do not have an ffmpeg binary present',
|
|
279
|
+
this.deviceData.description,
|
|
280
|
+
);
|
|
306
281
|
return;
|
|
307
282
|
}
|
|
308
283
|
|
|
@@ -312,16 +287,15 @@ export default class NestCamera extends HomeKitDevice {
|
|
|
312
287
|
) {
|
|
313
288
|
// Should only be recording if motion detected.
|
|
314
289
|
// Sometimes when starting up, HAP-nodeJS or HomeKit triggers this even when motion isn't occuring
|
|
315
|
-
this?.log?.debug
|
|
290
|
+
this?.log?.debug?.('Received request to commence recording for "%s" however we have not detected any motion');
|
|
316
291
|
return;
|
|
317
292
|
}
|
|
318
293
|
|
|
319
294
|
if (this.streamer === undefined) {
|
|
320
|
-
this?.log?.error
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
);
|
|
295
|
+
this?.log?.error?.(
|
|
296
|
+
'Received request to start recording for "%s" however we do not any associated streaming protocol support',
|
|
297
|
+
this.deviceData.description,
|
|
298
|
+
);
|
|
325
299
|
return;
|
|
326
300
|
}
|
|
327
301
|
|
|
@@ -410,12 +384,11 @@ export default class NestCamera extends HomeKitDevice {
|
|
|
410
384
|
// Start our ffmpeg recording process and stream from our streamer
|
|
411
385
|
// video is pipe #1
|
|
412
386
|
// audio is pipe #3 if including audio
|
|
413
|
-
this?.log?.debug
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
);
|
|
387
|
+
this?.log?.debug?.(
|
|
388
|
+
'ffmpeg process for recording stream from "%s" will be called using the following commandline',
|
|
389
|
+
this.deviceData.description,
|
|
390
|
+
commandLine.join(' ').toString(),
|
|
391
|
+
);
|
|
419
392
|
let ffmpegRecording = child_process.spawn(this.deviceData.ffmpeg.binary, commandLine.join(' ').split(' '), {
|
|
420
393
|
env: process.env,
|
|
421
394
|
stdio: ['pipe', 'pipe', 'pipe', 'pipe'],
|
|
@@ -455,8 +428,7 @@ export default class NestCamera extends HomeKitDevice {
|
|
|
455
428
|
|
|
456
429
|
ffmpegRecording.on('exit', (code, signal) => {
|
|
457
430
|
if (signal !== 'SIGKILL' || signal === null) {
|
|
458
|
-
this?.log?.error
|
|
459
|
-
this.log.error('ffmpeg recording process for "%s" stopped unexpectedly. Exit code was "%s"', this.deviceData.description, code);
|
|
431
|
+
this?.log?.error?.('ffmpeg recording process for "%s" stopped unexpectedly. Exit code was "%s"', this.deviceData.description, code);
|
|
460
432
|
}
|
|
461
433
|
|
|
462
434
|
if (this.#hkSessions?.[sessionID] !== undefined) {
|
|
@@ -473,7 +445,7 @@ export default class NestCamera extends HomeKitDevice {
|
|
|
473
445
|
ffmpegRecording.stderr.on('data', (data) => {
|
|
474
446
|
if (data.toString().includes('frame=') === false && this.deviceData?.ffmpeg?.debug === true) {
|
|
475
447
|
// Monitor ffmpeg output
|
|
476
|
-
this?.log?.debug
|
|
448
|
+
this?.log?.debug?.(data.toString());
|
|
477
449
|
}
|
|
478
450
|
});
|
|
479
451
|
|
|
@@ -486,8 +458,7 @@ export default class NestCamera extends HomeKitDevice {
|
|
|
486
458
|
this.#hkSessions[sessionID].eventEmitter = eventEmitter;
|
|
487
459
|
this.#hkSessions[sessionID].ffmpeg = ffmpegRecording; // Store ffmpeg process ID
|
|
488
460
|
|
|
489
|
-
this?.log?.info
|
|
490
|
-
this.log.info('Started recording from "%s" %s', this.deviceData.description, includeAudio === false ? 'without audio' : '');
|
|
461
|
+
this?.log?.info?.('Started recording from "%s" %s', this.deviceData.description, includeAudio === false ? 'without audio' : '');
|
|
491
462
|
|
|
492
463
|
// Loop generating MOOF/MDAT box pairs for HomeKit Secure Video.
|
|
493
464
|
// HAP-NodeJS cancels this async generator function when recording completes also
|
|
@@ -539,14 +510,13 @@ export default class NestCamera extends HomeKitDevice {
|
|
|
539
510
|
|
|
540
511
|
// Log recording finished messages depending on reason
|
|
541
512
|
if (closeReason === this.hap.HDSProtocolSpecificErrorReason.NORMAL) {
|
|
542
|
-
this?.log?.info
|
|
513
|
+
this?.log?.info?.('Completed recording from "%s"', this.deviceData.description);
|
|
543
514
|
} else {
|
|
544
|
-
this?.log?.warn
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
);
|
|
515
|
+
this?.log?.warn?.(
|
|
516
|
+
'Recording from "%s" completed with error. Reason was "%s"',
|
|
517
|
+
this.deviceData.description,
|
|
518
|
+
this.hap.HDSProtocolSpecificErrorReason[closeReason],
|
|
519
|
+
);
|
|
550
520
|
}
|
|
551
521
|
}
|
|
552
522
|
|
|
@@ -555,13 +525,13 @@ export default class NestCamera extends HomeKitDevice {
|
|
|
555
525
|
// Start a buffering stream for this camera/doorbell. Ensures motion captures all video on motion trigger
|
|
556
526
|
// Required due to data delays by on prem Nest to cloud to HomeKit accessory to iCloud etc
|
|
557
527
|
// Make sure have appropriate bandwidth!!!
|
|
558
|
-
this?.log?.info
|
|
528
|
+
this?.log?.info?.('Recording was turned on for "%s"', this.deviceData.description);
|
|
559
529
|
this.streamer.startBuffering();
|
|
560
530
|
}
|
|
561
531
|
|
|
562
532
|
if (enableRecording === false && this.streamer?.isBuffering() === true) {
|
|
563
533
|
this.streamer.stopBuffering();
|
|
564
|
-
this?.log?.warn
|
|
534
|
+
this?.log?.warn?.('Recording was turned off for "%s"', this.deviceData.description);
|
|
565
535
|
}
|
|
566
536
|
}
|
|
567
537
|
|
|
@@ -688,20 +658,18 @@ export default class NestCamera extends HomeKitDevice {
|
|
|
688
658
|
// called when HomeKit asks to start/stop/reconfigure a camera/doorbell live stream
|
|
689
659
|
if (request.type === this.hap.StreamRequestTypes.START && this.streamer === undefined) {
|
|
690
660
|
// We have no streamer object configured, so cannot do live streams!!
|
|
691
|
-
this?.log?.error
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
);
|
|
661
|
+
this?.log?.error?.(
|
|
662
|
+
'Received request to start live video for "%s" however we do not any associated streaming protocol support',
|
|
663
|
+
this.deviceData.description,
|
|
664
|
+
);
|
|
696
665
|
}
|
|
697
666
|
|
|
698
667
|
if (request.type === this.hap.StreamRequestTypes.START && this.deviceData?.ffmpeg?.binary === undefined) {
|
|
699
668
|
// No ffmpeg binary present, so cannot do live streams!!
|
|
700
|
-
this?.log?.warn
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
);
|
|
669
|
+
this?.log?.warn?.(
|
|
670
|
+
'Received request to start live video for "%s" however we do not have an ffmpeg binary present',
|
|
671
|
+
this.deviceData.description,
|
|
672
|
+
);
|
|
705
673
|
}
|
|
706
674
|
|
|
707
675
|
if (
|
|
@@ -802,12 +770,11 @@ export default class NestCamera extends HomeKitDevice {
|
|
|
802
770
|
// Start our ffmpeg streaming process and stream from our streamer
|
|
803
771
|
// video is pipe #1
|
|
804
772
|
// audio is pipe #3 if including audio
|
|
805
|
-
this?.log?.debug
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
);
|
|
773
|
+
this?.log?.debug?.(
|
|
774
|
+
'ffmpeg process for live streaming from "%s" will be called using the following commandline',
|
|
775
|
+
this.deviceData.description,
|
|
776
|
+
commandLine.join(' ').toString(),
|
|
777
|
+
);
|
|
811
778
|
let ffmpegStreaming = child_process.spawn(this.deviceData.ffmpeg.binary, commandLine.join(' ').split(' '), {
|
|
812
779
|
env: process.env,
|
|
813
780
|
stdio: ['pipe', 'pipe', 'pipe', 'pipe'],
|
|
@@ -815,12 +782,11 @@ export default class NestCamera extends HomeKitDevice {
|
|
|
815
782
|
|
|
816
783
|
ffmpegStreaming.on('exit', (code, signal) => {
|
|
817
784
|
if (signal !== 'SIGKILL' || signal === null) {
|
|
818
|
-
this?.log?.error
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
);
|
|
785
|
+
this?.log?.error?.(
|
|
786
|
+
'ffmpeg video/audio live streaming process for "%s" stopped unexpectedly. Exit code was "%s"',
|
|
787
|
+
this.deviceData.description,
|
|
788
|
+
code,
|
|
789
|
+
);
|
|
824
790
|
|
|
825
791
|
// Clean up or streaming request, but calling it again with a 'STOP' reques
|
|
826
792
|
this.handleStreamRequest({ type: this.hap.StreamRequestTypes.STOP, sessionID: request.sessionID }, null);
|
|
@@ -831,7 +797,7 @@ export default class NestCamera extends HomeKitDevice {
|
|
|
831
797
|
ffmpegStreaming.stderr.on('data', (data) => {
|
|
832
798
|
if (data.toString().includes('frame=') === false && this.deviceData?.ffmpeg?.debug === true) {
|
|
833
799
|
// Monitor ffmpeg output
|
|
834
|
-
this?.log?.debug
|
|
800
|
+
this?.log?.debug?.(data.toString());
|
|
835
801
|
}
|
|
836
802
|
});
|
|
837
803
|
|
|
@@ -894,24 +860,22 @@ export default class NestCamera extends HomeKitDevice {
|
|
|
894
860
|
|
|
895
861
|
commandLine.push('-f data pipe:1');
|
|
896
862
|
|
|
897
|
-
this?.log?.debug
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
);
|
|
863
|
+
this?.log?.debug?.(
|
|
864
|
+
'ffmpeg process for talkback on "%s" will be called using the following commandline',
|
|
865
|
+
this.deviceData.description,
|
|
866
|
+
commandLine.join(' ').toString(),
|
|
867
|
+
);
|
|
903
868
|
ffmpegAudioTalkback = child_process.spawn(this.deviceData.ffmpeg.binary, commandLine.join(' ').split(' '), {
|
|
904
869
|
env: process.env,
|
|
905
870
|
});
|
|
906
871
|
|
|
907
872
|
ffmpegAudioTalkback.on('exit', (code, signal) => {
|
|
908
873
|
if (signal !== 'SIGKILL' || signal === null) {
|
|
909
|
-
this?.log?.error
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
);
|
|
874
|
+
this?.log?.error?.(
|
|
875
|
+
'ffmpeg audio talkback streaming process for "%s" stopped unexpectedly. Exit code was "%s"',
|
|
876
|
+
this.deviceData.description,
|
|
877
|
+
code,
|
|
878
|
+
);
|
|
915
879
|
|
|
916
880
|
// Clean up or streaming request, but calling it again with a 'STOP' request
|
|
917
881
|
this.handleStreamRequest({ type: this.hap.StreamRequestTypes.STOP, sessionID: request.sessionID }, null);
|
|
@@ -927,7 +891,7 @@ export default class NestCamera extends HomeKitDevice {
|
|
|
927
891
|
ffmpegAudioTalkback.stderr.on('data', (data) => {
|
|
928
892
|
if (data.toString().includes('frame=') === false && this.deviceData?.ffmpeg?.debug === true) {
|
|
929
893
|
// Monitor ffmpeg output
|
|
930
|
-
this?.log?.debug
|
|
894
|
+
this?.log?.debug?.(data.toString());
|
|
931
895
|
}
|
|
932
896
|
});
|
|
933
897
|
|
|
@@ -971,12 +935,11 @@ export default class NestCamera extends HomeKitDevice {
|
|
|
971
935
|
ffmpegAudioTalkback.stdin.end();
|
|
972
936
|
}
|
|
973
937
|
|
|
974
|
-
this?.log?.info
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
);
|
|
938
|
+
this?.log?.info?.(
|
|
939
|
+
'Live stream started on "%s" %s',
|
|
940
|
+
this.deviceData.description,
|
|
941
|
+
ffmpegAudioTalkback?.stdout ? 'with two-way audio' : '',
|
|
942
|
+
);
|
|
980
943
|
|
|
981
944
|
// Start the appropriate streamer
|
|
982
945
|
this.streamer !== undefined &&
|
|
@@ -1010,11 +973,11 @@ export default class NestCamera extends HomeKitDevice {
|
|
|
1010
973
|
|
|
1011
974
|
delete this.#hkSessions[request.sessionID];
|
|
1012
975
|
|
|
1013
|
-
this?.log?.info
|
|
976
|
+
this?.log?.info?.('Live stream stopped from "%s"', this.deviceData.description);
|
|
1014
977
|
}
|
|
1015
978
|
|
|
1016
979
|
if (request.type === this.hap.StreamRequestTypes.RECONFIGURE && typeof this.#hkSessions[request.sessionID] === 'object') {
|
|
1017
|
-
this?.log?.debug
|
|
980
|
+
this?.log?.debug?.('Unsupported reconfiguration request for live stream on "%s"', this.deviceData.description);
|
|
1018
981
|
}
|
|
1019
982
|
|
|
1020
983
|
if (typeof callback === 'function') {
|
|
@@ -1022,32 +985,32 @@ export default class NestCamera extends HomeKitDevice {
|
|
|
1022
985
|
}
|
|
1023
986
|
}
|
|
1024
987
|
|
|
1025
|
-
|
|
988
|
+
updateDevice(deviceData) {
|
|
1026
989
|
if (typeof deviceData !== 'object' || this.controller === undefined) {
|
|
1027
990
|
return;
|
|
1028
991
|
}
|
|
1029
992
|
|
|
1030
993
|
if (this.deviceData.migrating === false && deviceData.migrating === true) {
|
|
1031
994
|
// Migration happening between Nest <-> Google Home apps. We'll stop any active streams, close the current streaming object
|
|
1032
|
-
this?.log?.warn
|
|
995
|
+
this?.log?.warn?.('Migration between Nest <-> Google Home apps has started for "%s"', deviceData.description);
|
|
1033
996
|
this.streamer !== undefined && this.streamer.stopEverything();
|
|
1034
997
|
this.streamer = undefined;
|
|
1035
998
|
}
|
|
1036
999
|
|
|
1037
1000
|
if (this.deviceData.migrating === true && deviceData.migrating === false) {
|
|
1038
1001
|
// Migration has completed between Nest <-> Google Home apps
|
|
1039
|
-
this?.log?.success
|
|
1002
|
+
this?.log?.success?.('Migration between Nest <-> Google Home apps has completed for "%s"', deviceData.description);
|
|
1040
1003
|
}
|
|
1041
1004
|
|
|
1042
1005
|
// Handle case of changes in streaming protocols OR just finished migration between Nest <-> Google Home apps
|
|
1043
1006
|
if (this.streamer === undefined && deviceData.migrating === false) {
|
|
1044
1007
|
if (JSON.stringify(deviceData.streaming_protocols) !== JSON.stringify(this.deviceData.streaming_protocols)) {
|
|
1045
|
-
this?.log?.warn
|
|
1008
|
+
this?.log?.warn?.('Available streaming protocols have changed for "%s"', deviceData.description);
|
|
1046
1009
|
this.streamer !== undefined && this.streamer.stopEverything();
|
|
1047
1010
|
this.streamer = undefined;
|
|
1048
1011
|
}
|
|
1049
|
-
if (deviceData.streaming_protocols.includes(
|
|
1050
|
-
this?.log?.debug
|
|
1012
|
+
if (deviceData.streaming_protocols.includes(PROTOCOLWEBRTC) === true && WebRTC !== undefined) {
|
|
1013
|
+
this?.log?.debug?.('Using WebRTC streamer for "%s"', deviceData.description);
|
|
1051
1014
|
this.streamer = new WebRTC(deviceData, {
|
|
1052
1015
|
log: this.log,
|
|
1053
1016
|
buffer:
|
|
@@ -1058,8 +1021,8 @@ export default class NestCamera extends HomeKitDevice {
|
|
|
1058
1021
|
});
|
|
1059
1022
|
}
|
|
1060
1023
|
|
|
1061
|
-
if (deviceData.streaming_protocols.includes(
|
|
1062
|
-
this?.log?.debug
|
|
1024
|
+
if (deviceData.streaming_protocols.includes(PROTOCOLNEXUSTALK) === true && NexusTalk !== undefined) {
|
|
1025
|
+
this?.log?.debug?.('Using NexusTalk streamer for "%s"', deviceData.description);
|
|
1063
1026
|
this.streamer = new NexusTalk(deviceData, {
|
|
1064
1027
|
log: this.log,
|
|
1065
1028
|
buffer:
|
|
@@ -1072,22 +1035,26 @@ export default class NestCamera extends HomeKitDevice {
|
|
|
1072
1035
|
}
|
|
1073
1036
|
|
|
1074
1037
|
// Check to see if any activity zones were added for both non-HKSV and HKSV enabled devices
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
) {
|
|
1080
|
-
if (this.
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
tempService.
|
|
1038
|
+
if (
|
|
1039
|
+
Array.isArray(deviceData.activity_zones) === true &&
|
|
1040
|
+
JSON.stringify(deviceData.activity_zones) !== JSON.stringify(this.deviceData.activity_zones)
|
|
1041
|
+
) {
|
|
1042
|
+
deviceData.activity_zones.forEach((zone) => {
|
|
1043
|
+
if (this.deviceData.hksv === false || (this.deviceData.hksv === true && zone.id === 1)) {
|
|
1044
|
+
if (this.motionServices?.[zone.id]?.service === undefined) {
|
|
1045
|
+
// Zone doesn't have an associated motion sensor, so add one
|
|
1046
|
+
let zoneName = zone.id === 1 ? '' : zone.name;
|
|
1047
|
+
let tempService = this.addHKService(this.hap.Service.MotionSensor, zoneName, zone.id);
|
|
1048
|
+
|
|
1049
|
+
this.addHKCharacteristic(tempService, this.hap.Characteristic.Active);
|
|
1050
|
+
tempService.updateCharacteristic(this.hap.Characteristic.Name, zoneName);
|
|
1051
|
+
tempService.updateCharacteristic(this.hap.Characteristic.MotionDetected, false); // No motion initially
|
|
1052
|
+
|
|
1053
|
+
this.motionServices[zone.id] = { service: tempService, timer: undefined };
|
|
1085
1054
|
}
|
|
1086
|
-
tempService.updateCharacteristic(this.hap.Characteristic.MotionDetected, false); // No motion initially
|
|
1087
|
-
this.motionServices[zone.id] = { service: tempService, timer: undefined };
|
|
1088
1055
|
}
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1091
1058
|
|
|
1092
1059
|
// Check to see if any activity zones were removed for both non-HKSV and HKSV enabled devices
|
|
1093
1060
|
// We'll also update the online status of the camera in the motion service here
|
|
@@ -1098,12 +1065,15 @@ export default class NestCamera extends HomeKitDevice {
|
|
|
1098
1065
|
deviceData.online === true ? this.hap.Characteristic.Active.ACTIVE : this.hap.Characteristic.Active.INACTIVE,
|
|
1099
1066
|
);
|
|
1100
1067
|
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1068
|
+
// Handle deleted zones (excluding zone ID 1 for HKSV)
|
|
1069
|
+
if (
|
|
1070
|
+
zoneID !== '1' &&
|
|
1071
|
+
Array.isArray(deviceData.activity_zones) === true &&
|
|
1072
|
+
deviceData.activity_zones.findIndex(({ id }) => id === Number(zoneID)) === -1
|
|
1073
|
+
) {
|
|
1074
|
+
// Motion service we created doesn't appear in zone list anymore, so assume deleted
|
|
1075
|
+
this.accessory.removeService(service.service);
|
|
1076
|
+
delete this.motionServices[zoneID];
|
|
1107
1077
|
}
|
|
1108
1078
|
});
|
|
1109
1079
|
|
|
@@ -1116,7 +1086,7 @@ export default class NestCamera extends HomeKitDevice {
|
|
|
1116
1086
|
: this.hap.Characteristic.ManuallyDisabled.ENABLED,
|
|
1117
1087
|
);
|
|
1118
1088
|
|
|
1119
|
-
if (deviceData
|
|
1089
|
+
if (deviceData?.has_statusled === true) {
|
|
1120
1090
|
// Set camera recording indicator. This cannot be turned off on Nest Cameras/Doorbells
|
|
1121
1091
|
// 0 = auto
|
|
1122
1092
|
// 1 = low
|
|
@@ -1127,12 +1097,12 @@ export default class NestCamera extends HomeKitDevice {
|
|
|
1127
1097
|
);
|
|
1128
1098
|
}
|
|
1129
1099
|
|
|
1130
|
-
if (deviceData
|
|
1100
|
+
if (deviceData?.has_irled === true) {
|
|
1131
1101
|
// Set nightvision status in HomeKit
|
|
1132
1102
|
this.operatingModeService.updateCharacteristic(this.hap.Characteristic.NightVision, deviceData.irled_enabled);
|
|
1133
1103
|
}
|
|
1134
1104
|
|
|
1135
|
-
if (deviceData
|
|
1105
|
+
if (deviceData?.has_video_flip === true) {
|
|
1136
1106
|
// Update image flip status
|
|
1137
1107
|
this.operatingModeService.updateCharacteristic(this.hap.Characteristic.ImageRotation, deviceData.video_flipped === true ? 180 : 0);
|
|
1138
1108
|
}
|
|
@@ -1183,7 +1153,7 @@ export default class NestCamera extends HomeKitDevice {
|
|
|
1183
1153
|
// For a HKSV enabled camera, we will use this to trigger the starting of the HKSV recording if the camera is active
|
|
1184
1154
|
if (event.types.includes('motion') === true) {
|
|
1185
1155
|
if (this.motionTimer === undefined && (this.deviceData.hksv === false || this.streamer === undefined)) {
|
|
1186
|
-
this?.log?.info
|
|
1156
|
+
this?.log?.info?.('Motion detected at "%s"', deviceData.description);
|
|
1187
1157
|
}
|
|
1188
1158
|
|
|
1189
1159
|
event.zone_ids.forEach((zoneID) => {
|
|
@@ -1231,9 +1201,9 @@ export default class NestCamera extends HomeKitDevice {
|
|
|
1231
1201
|
if (event.types.includes('person') === true || event.types.includes('face') === true) {
|
|
1232
1202
|
if (this.personTimer === undefined) {
|
|
1233
1203
|
// We don't have a person cooldown timer running, so we can process the 'person'/'face' event
|
|
1234
|
-
if (this
|
|
1204
|
+
if (this.deviceData.hksv === false || this.streamer === undefined) {
|
|
1235
1205
|
// We'll only log a person detected event if HKSV is disabled
|
|
1236
|
-
this
|
|
1206
|
+
this?.log?.info?.('Person detected at "%s"', deviceData.description);
|
|
1237
1207
|
}
|
|
1238
1208
|
|
|
1239
1209
|
// Cooldown for person being detected
|
|
@@ -1258,26 +1228,29 @@ export default class NestCamera extends HomeKitDevice {
|
|
|
1258
1228
|
// This will help with any 'restored' service Homebridge has done
|
|
1259
1229
|
// And allow for zone changes on the camera/doorbell
|
|
1260
1230
|
this.motionServices = {};
|
|
1261
|
-
this.accessory.services
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1231
|
+
this.accessory.services
|
|
1232
|
+
.filter((service) => service.UUID === this.hap.Service.MotionSensor.UUID)
|
|
1233
|
+
.forEach((service) => this.accessory.removeService(service));
|
|
1234
|
+
|
|
1235
|
+
let zones = Array.isArray(this.deviceData.activity_zones) === true ? this.deviceData.activity_zones : [];
|
|
1266
1236
|
|
|
1267
|
-
if (this.deviceData.has_motion_detection === true &&
|
|
1237
|
+
if (this.deviceData.has_motion_detection === true && zones.length > 0) {
|
|
1268
1238
|
// We have the capability of motion sensing on device, so setup motion sensor(s)
|
|
1269
1239
|
// If we have HKSV video enabled, we'll only create a single motion sensor
|
|
1270
1240
|
// A zone with the ID of 1 is treated as the main motion sensor
|
|
1271
|
-
|
|
1272
|
-
if (this.deviceData.hksv ===
|
|
1273
|
-
|
|
1274
|
-
if (tempService.testCharacteristic(this.hap.Characteristic.Active) === false) {
|
|
1275
|
-
tempService.addCharacteristic(this.hap.Characteristic.Active);
|
|
1276
|
-
}
|
|
1277
|
-
tempService.updateCharacteristic(this.hap.Characteristic.MotionDetected, false); // No motion initially
|
|
1278
|
-
this.motionServices[zone.id] = { service: tempService, timer: undefined };
|
|
1241
|
+
for (let zone of zones) {
|
|
1242
|
+
if (this.deviceData.hksv === true && zone.id !== 1) {
|
|
1243
|
+
continue;
|
|
1279
1244
|
}
|
|
1280
|
-
|
|
1245
|
+
|
|
1246
|
+
let zoneName = zone.id === 1 ? '' : zone.name;
|
|
1247
|
+
let service = this.addHKService(this.hap.Service.MotionSensor, zoneName, zone.id);
|
|
1248
|
+
this.addHKCharacteristic(service, this.hap.Characteristic.Active);
|
|
1249
|
+
service.updateCharacteristic(this.hap.Characteristic.Name, zoneName);
|
|
1250
|
+
service.updateCharacteristic(this.hap.Characteristic.MotionDetected, false); // No motion initially
|
|
1251
|
+
|
|
1252
|
+
this.motionServices[zone.id] = { service, timer: undefined };
|
|
1253
|
+
}
|
|
1281
1254
|
}
|
|
1282
1255
|
}
|
|
1283
1256
|
|