homebridge-nest-accfactory 0.2.2 → 0.2.3
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 +4 -0
- package/README.md +10 -5
- package/dist/HomeKitDevice.js +19 -60
- package/dist/HomeKitHistory.js +63 -67
- package/dist/camera.js +81 -57
- package/dist/doorbell.js +2 -2
- package/dist/index.js +1 -1
- package/dist/nexustalk.js +4 -4
- package/dist/protect.js +15 -43
- package/dist/streamer.js +8 -7
- package/dist/system.js +73 -29
- package/dist/thermostat.js +6 -18
- package/dist/webrtc.js +31 -8
- package/package.json +11 -10
package/dist/system.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Nest System communications
|
|
2
2
|
// Part of homebridge-nest-accfactory
|
|
3
3
|
//
|
|
4
|
-
// Code version
|
|
4
|
+
// Code version 2024/12/01
|
|
5
5
|
// Mark Hulskamp
|
|
6
6
|
'use strict';
|
|
7
7
|
|
|
@@ -78,7 +78,7 @@ export default class NestAccfactory {
|
|
|
78
78
|
|
|
79
79
|
// Perform validation on the configuration passed into us and set defaults if not present
|
|
80
80
|
|
|
81
|
-
// Build our accounts connection object. Allows us to have multiple diffent
|
|
81
|
+
// Build our accounts connection object. Allows us to have multiple diffent account connections under the one accessory
|
|
82
82
|
Object.keys(this.config).forEach((key) => {
|
|
83
83
|
if (this.config[key]?.access_token !== undefined && this.config[key].access_token !== '') {
|
|
84
84
|
// Nest account connection, assign a random UUID for each connection
|
|
@@ -105,7 +105,7 @@ export default class NestAccfactory {
|
|
|
105
105
|
authorised: false,
|
|
106
106
|
issuetoken: this.config[key].issuetoken,
|
|
107
107
|
cookie: this.config[key].cookie,
|
|
108
|
-
fieldTest:
|
|
108
|
+
fieldTest: this.config[key]?.fieldTest === true,
|
|
109
109
|
referer: this.config[key]?.fieldTest === true ? 'home.ft.nest.com' : 'home.nest.com',
|
|
110
110
|
restAPIHost: this.config[key]?.fieldTest === true ? 'home.ft.nest.com' : 'home.nest.com',
|
|
111
111
|
cameraAPIHost: this.config[key]?.fieldTest === true ? 'camera.home.ft.nest.com' : 'camera.home.nest.com',
|
|
@@ -124,10 +124,10 @@ export default class NestAccfactory {
|
|
|
124
124
|
this.config.options = {};
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
this.config.options.eveHistory =
|
|
127
|
+
this.config.options.eveHistory = this.config.options?.eveHistory === true;
|
|
128
128
|
this.config.options.elevation = isNaN(this.config.options?.elevation) === false ? Number(this.config.options.elevation) : 0;
|
|
129
|
-
this.config.options.weather =
|
|
130
|
-
this.config.options.hksv =
|
|
129
|
+
this.config.options.weather = this.config.options?.weather === true;
|
|
130
|
+
this.config.options.hksv = this.config.options?.hksv === true;
|
|
131
131
|
|
|
132
132
|
// Get configuration for max number of concurrent 'live view' streams. For HomeKit Secure Video, this will always be 1
|
|
133
133
|
this.config.options.maxStreams =
|
|
@@ -140,6 +140,7 @@ export default class NestAccfactory {
|
|
|
140
140
|
// Check if a ffmpeg binary exists in current path OR the specific path via configuration
|
|
141
141
|
// If using HomeBridge, the default path will be where the Homebridge user folder is, otherwise the current directory
|
|
142
142
|
this.config.options.ffmpeg = {};
|
|
143
|
+
this.config.options.ffmpeg.debug = this.config.options?.ffmpegDebug === true;
|
|
143
144
|
this.config.options.ffmpeg.binary = path.resolve(
|
|
144
145
|
typeof this.config.options?.ffmpegPath === 'string' && this.config.options.ffmpegPath !== ''
|
|
145
146
|
? this.config.options.ffmpegPath
|
|
@@ -367,7 +368,7 @@ export default class NestAccfactory {
|
|
|
367
368
|
this.#connections[connectionUUID].token = googleToken;
|
|
368
369
|
this.#connections[connectionUUID].cameraAPI = {
|
|
369
370
|
key: 'Authorization',
|
|
370
|
-
value: 'Basic ',
|
|
371
|
+
value: 'Basic ', // NOTE: extra space required
|
|
371
372
|
token: googleToken,
|
|
372
373
|
oauth2: googleOAuth2Token,
|
|
373
374
|
};
|
|
@@ -392,7 +393,7 @@ export default class NestAccfactory {
|
|
|
392
393
|
this.#connections[connectionUUID].authorised = false;
|
|
393
394
|
this?.log?.debug &&
|
|
394
395
|
this.log.debug(
|
|
395
|
-
'Failed to connect using credential details for connection uuid "%s". A periodic retry event will be triggered',
|
|
396
|
+
'Failed to connect to gateway using credential details for connection uuid "%s". A periodic retry event will be triggered',
|
|
396
397
|
connectionUUID,
|
|
397
398
|
);
|
|
398
399
|
this?.log?.error && this.log.error('Authorisation failed using Google account');
|
|
@@ -897,11 +898,15 @@ export default class NestAccfactory {
|
|
|
897
898
|
// Tidy up tracked devices since this one is removed
|
|
898
899
|
if (this.#trackedDevices[this.#rawData?.[resource.resourceId]?.value?.device_identity?.serialNumber] !== undefined) {
|
|
899
900
|
// Remove any active running timers we have for this device
|
|
900
|
-
|
|
901
|
-
this.#trackedDevices[this.#rawData[resource.resourceId].value.device_identity.serialNumber]?.timers
|
|
902
|
-
)
|
|
903
|
-
|
|
904
|
-
|
|
901
|
+
if (
|
|
902
|
+
this.#trackedDevices[this.#rawData[resource.resourceId].value.device_identity.serialNumber]?.timers !== undefined
|
|
903
|
+
) {
|
|
904
|
+
Object.values(
|
|
905
|
+
this.#trackedDevices[this.#rawData[resource.resourceId].value.device_identity.serialNumber]?.timers,
|
|
906
|
+
).forEach((timers) => {
|
|
907
|
+
clearInterval(timers);
|
|
908
|
+
});
|
|
909
|
+
}
|
|
905
910
|
|
|
906
911
|
// Send removed notice onto HomeKit device for it to process
|
|
907
912
|
this.#eventEmitter.emit(
|
|
@@ -911,7 +916,7 @@ export default class NestAccfactory {
|
|
|
911
916
|
);
|
|
912
917
|
|
|
913
918
|
// Finally, remove from tracked devices
|
|
914
|
-
delete this.#trackedDevices[this.#rawData
|
|
919
|
+
delete this.#trackedDevices[this.#rawData[resource.resourceId].value.device_identity.serialNumber];
|
|
915
920
|
}
|
|
916
921
|
delete this.#rawData[resource.resourceId];
|
|
917
922
|
}
|
|
@@ -1299,7 +1304,7 @@ export default class NestAccfactory {
|
|
|
1299
1304
|
}
|
|
1300
1305
|
}
|
|
1301
1306
|
|
|
1302
|
-
// Finally,
|
|
1307
|
+
// Finally, if device is not excluded, send updated data to device for it to process
|
|
1303
1308
|
if (deviceData.excluded === false && this.#trackedDevices?.[deviceData?.serialNumber] !== undefined) {
|
|
1304
1309
|
if (
|
|
1305
1310
|
this.#rawData[deviceData?.nest_google_uuid]?.source !== undefined &&
|
|
@@ -1397,6 +1402,16 @@ export default class NestAccfactory {
|
|
|
1397
1402
|
data.hkPairingCode = this.config.devices[data.serialNumber].hkPairingCode;
|
|
1398
1403
|
}
|
|
1399
1404
|
|
|
1405
|
+
// If we have a hkPairingCode defined, we need to generate a hkUsername also
|
|
1406
|
+
if (data.hkPairingCode !== undefined) {
|
|
1407
|
+
// Use a Nest Labs prefix for first 6 digits, followed by a CRC24 based off serial number for last 6 digits.
|
|
1408
|
+
data.hkUsername = ('18B430' + crc24(data.serialNumber.toUpperCase()))
|
|
1409
|
+
.toString('hex')
|
|
1410
|
+
.split(/(..)/)
|
|
1411
|
+
.filter((s) => s)
|
|
1412
|
+
.join(':');
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1400
1415
|
processed = data;
|
|
1401
1416
|
// eslint-disable-next-line no-unused-vars
|
|
1402
1417
|
} catch (error) {
|
|
@@ -1732,23 +1747,45 @@ export default class NestAccfactory {
|
|
|
1732
1747
|
);
|
|
1733
1748
|
|
|
1734
1749
|
// Work out current mode. ie: off, cool, heat, range and get temperature low (heat) and high (cool)
|
|
1735
|
-
RESTTypeData.hvac_mode =
|
|
1736
|
-
|
|
1737
|
-
|
|
1750
|
+
RESTTypeData.hvac_mode =
|
|
1751
|
+
this.#rawData?.['shared.' + value.value.serial_number]?.value?.target_temperature_type !== undefined
|
|
1752
|
+
? this.#rawData?.['shared.' + value.value.serial_number].value.target_temperature_type
|
|
1753
|
+
: 'off';
|
|
1754
|
+
RESTTypeData.target_temperature =
|
|
1755
|
+
isNaN(this.#rawData?.['shared.' + value.value.serial_number]?.value?.target_temperature) === false
|
|
1756
|
+
? Number(this.#rawData['shared.' + value.value.serial_number].value.target_temperature)
|
|
1757
|
+
: 0.0;
|
|
1758
|
+
RESTTypeData.target_temperature_low =
|
|
1759
|
+
isNaN(this.#rawData?.['shared.' + value.value.serial_number]?.value?.target_temperature_low) === false
|
|
1760
|
+
? Number(this.#rawData['shared.' + value.value.serial_number].value.target_temperature_low)
|
|
1761
|
+
: 0.0;
|
|
1762
|
+
RESTTypeData.target_temperature_high =
|
|
1763
|
+
isNaN(this.#rawData?.['shared.' + value.value.serial_number]?.value?.target_temperature_high) === false
|
|
1764
|
+
? Number(this.#rawData['shared.' + value.value.serial_number].value.target_temperature_high)
|
|
1765
|
+
: 0.0;
|
|
1738
1766
|
if (this.#rawData?.['shared.' + value.value.serial_number]?.value?.target_temperature_type.toUpperCase() === 'COOL') {
|
|
1739
1767
|
// Target temperature is the cooling point
|
|
1740
|
-
RESTTypeData.target_temperature =
|
|
1768
|
+
RESTTypeData.target_temperature =
|
|
1769
|
+
isNaN(this.#rawData?.['shared.' + value.value.serial_number]?.value?.target_temperature_high) === false
|
|
1770
|
+
? Number(this.#rawData['shared.' + value.value.serial_number].value.target_temperature_high)
|
|
1771
|
+
: 0.0;
|
|
1741
1772
|
}
|
|
1742
1773
|
if (this.#rawData?.['shared.' + value.value.serial_number]?.value?.target_temperature_type.toUpperCase() === 'HEAT') {
|
|
1743
1774
|
// Target temperature is the heating point
|
|
1744
|
-
RESTTypeData.target_temperature =
|
|
1775
|
+
RESTTypeData.target_temperature =
|
|
1776
|
+
isNaN(this.#rawData?.['shared.' + value.value.serial_number]?.value?.target_temperature_low) === false
|
|
1777
|
+
? Number(this.#rawData['shared.' + value.value.serial_number].value.target_temperature_low)
|
|
1778
|
+
: 0.0;
|
|
1745
1779
|
}
|
|
1746
1780
|
if (this.#rawData?.['shared.' + value.value.serial_number]?.value?.target_temperature_type.toUpperCase() === 'RANGE') {
|
|
1747
1781
|
// Target temperature is in between the heating and cooling point
|
|
1748
1782
|
RESTTypeData.target_temperature =
|
|
1749
|
-
(this.#rawData['shared.' + value.value.serial_number]
|
|
1750
|
-
|
|
1751
|
-
|
|
1783
|
+
isNaN(this.#rawData?.['shared.' + value.value.serial_number]?.value?.target_temperature_low) === false &&
|
|
1784
|
+
isNaN(this.#rawData?.['shared.' + value.value.serial_number]?.value?.target_temperature_high) === false
|
|
1785
|
+
? (Number(this.#rawData['shared.' + value.value.serial_number].value.target_temperature_low) +
|
|
1786
|
+
Number(this.#rawData['shared.' + value.value.serial_number].value.target_temperature_high)) *
|
|
1787
|
+
0.5
|
|
1788
|
+
: 0.0;
|
|
1752
1789
|
}
|
|
1753
1790
|
|
|
1754
1791
|
// Work out if eco mode is active and adjust temperature low/high and target
|
|
@@ -2104,7 +2141,8 @@ export default class NestAccfactory {
|
|
|
2104
2141
|
try {
|
|
2105
2142
|
if (
|
|
2106
2143
|
value?.source === NestAccfactory.DataSource.PROTOBUF &&
|
|
2107
|
-
value.value?.streaming_protocol?.supportedProtocols
|
|
2144
|
+
Array.isArray(value.value?.streaming_protocol?.supportedProtocols) === true &&
|
|
2145
|
+
value.value.streaming_protocol.supportedProtocols.includes('PROTOCOL_WEBRTC') === true &&
|
|
2108
2146
|
(value.value?.configuration_done?.deviceReady === true ||
|
|
2109
2147
|
value.value?.camera_migration_status?.state?.where === 'MIGRATED_TO_GOOGLE_HOME')
|
|
2110
2148
|
) {
|
|
@@ -2289,7 +2327,12 @@ export default class NestAccfactory {
|
|
|
2289
2327
|
: 120;
|
|
2290
2328
|
tempDevice.chimeSwitch = this.config?.devices?.[tempDevice.serialNumber]?.chimeSwitch === true; // Control 'indoor' chime by switch
|
|
2291
2329
|
tempDevice.localAccess = this.config?.devices?.[tempDevice.serialNumber]?.localAccess === true; // Local network video streaming rather than from cloud from camera/doorbells
|
|
2292
|
-
|
|
2330
|
+
// eslint-disable-next-line no-undef
|
|
2331
|
+
tempDevice.ffmpeg = structuredClone(this.config.options.ffmpeg); // ffmpeg details, path, libraries. No ffmpeg = undefined
|
|
2332
|
+
if (this.config?.devices?.[tempDevice.serialNumber]?.ffmpegDebug !== undefined) {
|
|
2333
|
+
// Device specific ffmpeg debugging
|
|
2334
|
+
tempDevice.ffmpeg.debug = this.config?.devices?.[tempDevice.serialNumber]?.ffmpegDebug === true;
|
|
2335
|
+
}
|
|
2293
2336
|
tempDevice.maxStreams =
|
|
2294
2337
|
isNaN(this.config.options?.maxStreams) === false
|
|
2295
2338
|
? Number(this.config.options.maxStreams)
|
|
@@ -2414,7 +2457,7 @@ export default class NestAccfactory {
|
|
|
2414
2457
|
}
|
|
2415
2458
|
|
|
2416
2459
|
let nest_google_uuid = values.uuid; // Nest/Google structure uuid for this get request
|
|
2417
|
-
let connectionUuid = this.#rawData[values.uuid].connection; // Associated connection uuid for the
|
|
2460
|
+
let connectionUuid = this.#rawData[values.uuid].connection; // Associated connection uuid for the device
|
|
2418
2461
|
|
|
2419
2462
|
if (this.#protobufRoot !== null && this.#rawData?.[nest_google_uuid]?.source === NestAccfactory.DataSource.PROTOBUF) {
|
|
2420
2463
|
let updatedTraits = [];
|
|
@@ -2756,7 +2799,7 @@ export default class NestAccfactory {
|
|
|
2756
2799
|
subscribeJSONData.objects.push({ object_key: nest_google_uuid, op: 'MERGE', value: { [key]: value } });
|
|
2757
2800
|
}
|
|
2758
2801
|
|
|
2759
|
-
// Some elements when setting thermostat data are located in a different object
|
|
2802
|
+
// Some elements when setting thermostat data are located in a different object location than with the device object
|
|
2760
2803
|
// Handle this scenario below
|
|
2761
2804
|
if (nest_google_uuid.startsWith('device.') === true) {
|
|
2762
2805
|
let RESTStructureUUID = nest_google_uuid;
|
|
@@ -2773,6 +2816,7 @@ export default class NestAccfactory {
|
|
|
2773
2816
|
(key === 'target_temperature_high' && isNaN(value) === false)
|
|
2774
2817
|
) {
|
|
2775
2818
|
RESTStructureUUID = 'shared.' + nest_google_uuid.split('.')[1];
|
|
2819
|
+
subscribeJSONData.objects.push({ object_key: RESTStructureUUID, op: 'MERGE', value: { target_change_pending: true } });
|
|
2776
2820
|
}
|
|
2777
2821
|
|
|
2778
2822
|
if (key === 'fan_state' && typeof value === 'boolean') {
|
|
@@ -3039,9 +3083,9 @@ export default class NestAccfactory {
|
|
|
3039
3083
|
},
|
|
3040
3084
|
encodedData,
|
|
3041
3085
|
)
|
|
3042
|
-
.then((response) => response.
|
|
3086
|
+
.then((response) => response.arrayBuffer())
|
|
3043
3087
|
.then((data) => {
|
|
3044
|
-
commandResponse = TraitMapResponse.decode(data).toJSON();
|
|
3088
|
+
commandResponse = TraitMapResponse.decode(Buffer.from(data)).toJSON();
|
|
3045
3089
|
})
|
|
3046
3090
|
.catch((error) => {
|
|
3047
3091
|
this?.log?.debug && this.log.debug('Protobuf gateway service command failed with error. Error was "%s"', error?.code);
|
package/dist/thermostat.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Nest Thermostat
|
|
2
2
|
// Part of homebridge-nest-accfactory
|
|
3
3
|
//
|
|
4
|
-
// Code version
|
|
4
|
+
// Code version 15/10/2024
|
|
5
5
|
// Mark Hulskamp
|
|
6
6
|
'use strict';
|
|
7
7
|
|
|
@@ -185,9 +185,6 @@ export default class NestThermostat extends HomeKitDevice {
|
|
|
185
185
|
if (this.occupancyService === undefined) {
|
|
186
186
|
this.occupancyService = this.accessory.addService(this.hap.Service.OccupancySensor, '', 1);
|
|
187
187
|
}
|
|
188
|
-
if (this.occupancyService.testCharacteristic(this.hap.Characteristic.StatusFault) === false) {
|
|
189
|
-
this.occupancyService.addCharacteristic(this.hap.Characteristic.StatusFault);
|
|
190
|
-
}
|
|
191
188
|
this.thermostatService.addLinkedService(this.occupancyService);
|
|
192
189
|
|
|
193
190
|
// Setup battery service if not already present on the accessory
|
|
@@ -599,12 +596,13 @@ export default class NestThermostat extends HomeKitDevice {
|
|
|
599
596
|
|
|
600
597
|
this.thermostatService.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, deviceData.current_temperature);
|
|
601
598
|
|
|
599
|
+
// If thermostat isn't online or removed from base, report in HomeKit
|
|
602
600
|
this.thermostatService.updateCharacteristic(
|
|
603
601
|
this.hap.Characteristic.StatusFault,
|
|
604
602
|
deviceData.online === true && deviceData.removed_from_base === false
|
|
605
603
|
? this.hap.Characteristic.StatusFault.NO_FAULT
|
|
606
604
|
: this.hap.Characteristic.StatusFault.GENERAL_FAULT,
|
|
607
|
-
);
|
|
605
|
+
);
|
|
608
606
|
|
|
609
607
|
this.thermostatService.updateCharacteristic(
|
|
610
608
|
this.hap.Characteristic.LockPhysicalControls,
|
|
@@ -624,6 +622,8 @@ export default class NestThermostat extends HomeKitDevice {
|
|
|
624
622
|
}
|
|
625
623
|
|
|
626
624
|
// Using a temperature sensor as active temperature?
|
|
625
|
+
// Probably not the best way for HomeKit, but works ;-)
|
|
626
|
+
// Maybe a custom characteristic would be better?
|
|
627
627
|
this.thermostatService.updateCharacteristic(this.hap.Characteristic.StatusActive, deviceData.active_rcs_sensor === '');
|
|
628
628
|
|
|
629
629
|
// Update battery status
|
|
@@ -648,22 +648,10 @@ export default class NestThermostat extends HomeKitDevice {
|
|
|
648
648
|
? this.hap.Characteristic.OccupancyDetected.OCCUPANCY_DETECTED
|
|
649
649
|
: this.hap.Characteristic.OccupancyDetected.OCCUPANCY_NOT_DETECTED,
|
|
650
650
|
);
|
|
651
|
-
this.occupancyService.updateCharacteristic(
|
|
652
|
-
this.hap.Characteristic.StatusFault,
|
|
653
|
-
deviceData.online === true && deviceData.removed_from_base === false
|
|
654
|
-
? this.hap.Characteristic.StatusFault.NO_FAULT
|
|
655
|
-
: this.hap.Characteristic.StatusFault.GENERAL_FAULT,
|
|
656
|
-
); // If Nest isn't online or removed from base, report in HomeKit
|
|
657
651
|
|
|
658
652
|
// Update seperate humidity sensor if configured todo so
|
|
659
653
|
if (this.humidityService !== undefined) {
|
|
660
654
|
this.humidityService.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, deviceData.current_humidity);
|
|
661
|
-
this.humidityService.updateCharacteristic(
|
|
662
|
-
this.hap.Characteristic.StatusFault,
|
|
663
|
-
deviceData.online === true && deviceData.removed_from_base === false
|
|
664
|
-
? this.hap.Characteristic.StatusFault.NO_FAULT
|
|
665
|
-
: this.hap.Characteristic.StatusFault.GENERAL_FAULT,
|
|
666
|
-
); // If Nest isn't online or removed from base, report in HomeKit
|
|
667
655
|
}
|
|
668
656
|
|
|
669
657
|
// Update humity on thermostat
|
|
@@ -957,7 +945,7 @@ export default class NestThermostat extends HomeKitDevice {
|
|
|
957
945
|
if (this.thermostatService !== undefined && typeof this.historyService?.addHistory === 'function') {
|
|
958
946
|
let tempEntry = this.historyService.lastHistory(this.thermostatService);
|
|
959
947
|
if (
|
|
960
|
-
tempEntry ===
|
|
948
|
+
tempEntry === undefined ||
|
|
961
949
|
(typeof tempEntry === 'object' && tempEntry.status !== historyEntry.status) ||
|
|
962
950
|
tempEntry.temperature !== deviceData.current_temperature ||
|
|
963
951
|
JSON.stringify(tempEntry.target) !== JSON.stringify(historyEntry.target) ||
|
package/dist/webrtc.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
// Handles connection and data from Google WebRTC systems
|
|
5
5
|
//
|
|
6
|
-
// Code version
|
|
6
|
+
// Code version 15/11/2024
|
|
7
7
|
// Mark Hulskamp
|
|
8
8
|
'use strict';
|
|
9
9
|
|
|
@@ -15,7 +15,7 @@ import werift from 'werift';
|
|
|
15
15
|
import EventEmitter from 'node:events';
|
|
16
16
|
import http2 from 'node:http2';
|
|
17
17
|
import { Buffer } from 'node:buffer';
|
|
18
|
-
import { setInterval, clearInterval } from 'node:timers';
|
|
18
|
+
import { setInterval, clearInterval, setTimeout, clearTimeout } from 'node:timers';
|
|
19
19
|
import fs from 'node:fs';
|
|
20
20
|
import path from 'node:path';
|
|
21
21
|
import crypto from 'node:crypto';
|
|
@@ -48,6 +48,7 @@ export default class WebRTC extends Streamer {
|
|
|
48
48
|
token = undefined; // oauth2 token
|
|
49
49
|
localAccess = false; // Do we try direct local access to the camera or via Google Home first
|
|
50
50
|
extendTimer = undefined; // Stream extend timer
|
|
51
|
+
stalledTimer = undefined; // Timer object for no received data
|
|
51
52
|
pingTimer = undefined; // Google Hopme Foyer periodic ping
|
|
52
53
|
blankAudio = AACMONO48000BLANK;
|
|
53
54
|
video = {}; // Video stream details once connected
|
|
@@ -92,7 +93,9 @@ export default class WebRTC extends Streamer {
|
|
|
92
93
|
// Class functions
|
|
93
94
|
async connect() {
|
|
94
95
|
clearInterval(this.extendTimer);
|
|
96
|
+
clearTimeout(this.stalledTimer);
|
|
95
97
|
this.extendTimer = undefined;
|
|
98
|
+
this.stalledTimer = undefined;
|
|
96
99
|
this.#id = undefined;
|
|
97
100
|
|
|
98
101
|
if (this.#googleHomeDeviceUUID === undefined) {
|
|
@@ -166,7 +169,7 @@ export default class WebRTC extends Streamer {
|
|
|
166
169
|
{ type: 'nack', parameter: 'pli' },
|
|
167
170
|
{ type: 'goog-remb' },
|
|
168
171
|
],
|
|
169
|
-
parameters: 'level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=
|
|
172
|
+
parameters: 'level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f',
|
|
170
173
|
payloadType: RTP_VIDEO_PAYLOAD_TYPE,
|
|
171
174
|
}),
|
|
172
175
|
],
|
|
@@ -275,9 +278,11 @@ export default class WebRTC extends Streamer {
|
|
|
275
278
|
});
|
|
276
279
|
|
|
277
280
|
if (homeFoyerResponse?.data?.[0]?.streamExtensionStatus !== 'STATUS_STREAM_EXTENDED') {
|
|
278
|
-
this?.log?.debug && this.log.debug('Error
|
|
281
|
+
this?.log?.debug && this.log.debug('Error occurred while requested stream extension for uuid "%s"', this.uuid);
|
|
279
282
|
|
|
280
|
-
|
|
283
|
+
if (typeof this.#peerConnection?.close === 'function') {
|
|
284
|
+
await this.#peerConnection.close();
|
|
285
|
+
}
|
|
281
286
|
}
|
|
282
287
|
}
|
|
283
288
|
}, EXTENDINTERVAL);
|
|
@@ -317,7 +322,9 @@ export default class WebRTC extends Streamer {
|
|
|
317
322
|
}
|
|
318
323
|
|
|
319
324
|
clearInterval(this.extendTimer);
|
|
325
|
+
clearInterval(this.stalledTimer);
|
|
320
326
|
this.extendTimer = undefined;
|
|
327
|
+
this.stalledTimer = undefined;
|
|
321
328
|
this.#id = undefined;
|
|
322
329
|
this.#googleHomeFoyer = undefined;
|
|
323
330
|
this.#peerConnection = undefined;
|
|
@@ -365,7 +372,7 @@ export default class WebRTC extends Streamer {
|
|
|
365
372
|
|
|
366
373
|
if (homeFoyerResponse?.status !== 0) {
|
|
367
374
|
this.audio.talking = undefined;
|
|
368
|
-
this?.log?.debug && this.log.debug('Error
|
|
375
|
+
this?.log?.debug && this.log.debug('Error occurred while requesting talkback to start for uuid "%s"', this.uuid);
|
|
369
376
|
}
|
|
370
377
|
if (homeFoyerResponse?.status === 0) {
|
|
371
378
|
this.audio.talking = true;
|
|
@@ -380,7 +387,7 @@ export default class WebRTC extends Streamer {
|
|
|
380
387
|
rtpHeader.marker = true;
|
|
381
388
|
rtpHeader.payloadOffset = RTP_PACKET_HEADER_SIZE;
|
|
382
389
|
rtpHeader.payloadType = this.audio.id; // As the camera is send/recv, we use the same payload type id as the incoming audio
|
|
383
|
-
rtpHeader.timestamp = Date.now() & 0xffffffff; // Think the time
|
|
390
|
+
rtpHeader.timestamp = Date.now() & 0xffffffff; // Think the time stamp difference should be 960ms per audio packet?
|
|
384
391
|
rtpHeader.sequenceNumber = this.audio.talkSquenceNumber++ & 0xffff;
|
|
385
392
|
let rtpPacket = new werift.RtpPacket(rtpHeader, talkingData);
|
|
386
393
|
this.#audioTransceiver.sender.sendRtp(rtpPacket.serialize());
|
|
@@ -397,7 +404,7 @@ export default class WebRTC extends Streamer {
|
|
|
397
404
|
command: 'COMMAND_STOP',
|
|
398
405
|
});
|
|
399
406
|
if (homeFoyerResponse?.status !== 0) {
|
|
400
|
-
this?.log?.debug && this.log.debug('Error
|
|
407
|
+
this?.log?.debug && this.log.debug('Error occurred while requesting talkback to stop for uuid "%s"', this.uuid);
|
|
401
408
|
}
|
|
402
409
|
if (homeFoyerResponse?.status === 0) {
|
|
403
410
|
this?.log?.debug && this.log.debug('Talking ended on uuid "%s"', this.uuid);
|
|
@@ -439,6 +446,22 @@ export default class WebRTC extends Streamer {
|
|
|
439
446
|
return;
|
|
440
447
|
}
|
|
441
448
|
|
|
449
|
+
// Setup up a timeout to monitor for no packets recieved in a certain period
|
|
450
|
+
// If its trigger, we'll attempt to restart the stream and/or connection
|
|
451
|
+
clearTimeout(this.stalledTimer);
|
|
452
|
+
this.stalledTimer = setTimeout(async () => {
|
|
453
|
+
this?.log?.debug &&
|
|
454
|
+
this.log.debug(
|
|
455
|
+
'We have not received any data from webrtc in the past "%s" seconds for uuid "%s". Attempting restart',
|
|
456
|
+
10,
|
|
457
|
+
this.uuid,
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
if (typeof this.#peerConnection?.close === 'function') {
|
|
461
|
+
await this.#peerConnection.close();
|
|
462
|
+
}
|
|
463
|
+
}, 10000);
|
|
464
|
+
|
|
442
465
|
if (weriftRtpPacket.header.payloadType !== undefined && weriftRtpPacket.header.payloadType === this.video?.id) {
|
|
443
466
|
// Process video RTP packets. Need to re-assemble the H264 NALUs into a single H264 frame we can output
|
|
444
467
|
if (weriftRtpPacket.header.padding === false) {
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-nest-accfactory",
|
|
3
|
-
"displayName": "
|
|
3
|
+
"displayName": "Nest Accfactory",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.2.
|
|
5
|
+
"version": "0.2.3",
|
|
6
6
|
"description": "Homebridge support for Nest/Google devices including HomeKit Secure Video (HKSV) support for doorbells and cameras",
|
|
7
7
|
"author": "n0rt0nthec4t",
|
|
8
8
|
"license": "Apache-2.0",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"bugs": {
|
|
15
15
|
"url": "https://github.com/n0rt0nthec4t/homebridge-nest-accfactory/issues"
|
|
16
16
|
},
|
|
17
|
+
"funding": "https://github.com/n0rt0nthec4t/homebridge-nest-accfactory?sponsor=1",
|
|
17
18
|
"keywords": [
|
|
18
19
|
"homekit",
|
|
19
20
|
"homebridge-plugin",
|
|
@@ -46,24 +47,24 @@
|
|
|
46
47
|
"clean": "rimraf ./dist*",
|
|
47
48
|
"format": "prettier --write src/*.js src/**/*.js",
|
|
48
49
|
"lint": "eslint src/*.js src/**/*.js --max-warnings=20",
|
|
49
|
-
"build": "npm run clean && copyfiles -u 1 src/*.js dist && copyfiles -u 1 src/res/*.h264 dist && copyfiles -u 1 src/res/*.jpg dist && copyfiles -u 1 'src/protobuf/**/*.proto' dist",
|
|
50
|
+
"build": "npm run clean && copyfiles -u 1 src/*.js dist && copyfiles -u 2 src/HomeKitDevice/*.js dist && copyfiles -u 2 src/HomeKitHistory/*.js dist && copyfiles -u 1 src/res/*.h264 dist && copyfiles -u 1 src/res/*.jpg dist && copyfiles -u 1 'src/protobuf/**/*.proto' dist",
|
|
50
51
|
"prepublishOnly": "npm run lint && npm run build"
|
|
51
52
|
},
|
|
52
53
|
"devDependencies": {
|
|
53
|
-
"@eslint/js": "^9.
|
|
54
|
-
"@stylistic/eslint-plugin": "^2.
|
|
55
|
-
"@types/node": "^22.
|
|
56
|
-
"@typescript-eslint/parser": "^8.
|
|
54
|
+
"@eslint/js": "^9.16.0",
|
|
55
|
+
"@stylistic/eslint-plugin": "^2.11.0",
|
|
56
|
+
"@types/node": "^22.10.1",
|
|
57
|
+
"@typescript-eslint/parser": "^8.17.0",
|
|
57
58
|
"homebridge": "^2.0.0-beta.0",
|
|
58
59
|
"copyfiles": "^2.4.1",
|
|
59
|
-
"eslint": "^9.
|
|
60
|
-
"prettier": "^3.
|
|
60
|
+
"eslint": "^9.16.0",
|
|
61
|
+
"prettier": "^3.4.2",
|
|
61
62
|
"prettier-eslint": "^16.3.0",
|
|
62
63
|
"rimraf": "^6.0.1"
|
|
63
64
|
},
|
|
64
65
|
"dependencies": {
|
|
65
66
|
"protobufjs": "^7.4.0",
|
|
66
67
|
"ws": "^8.18.0",
|
|
67
|
-
"werift": "^0.20.
|
|
68
|
+
"werift": "^0.20.1"
|
|
68
69
|
}
|
|
69
70
|
}
|