homebridge-nest-accfactory 0.3.0 → 0.3.2
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 +31 -0
- package/README.md +31 -25
- package/config.schema.json +46 -22
- package/dist/HomeKitDevice.js +523 -281
- package/dist/HomeKitHistory.js +357 -341
- package/dist/config.js +69 -87
- package/dist/consts.js +160 -0
- package/dist/devices.js +40 -48
- package/dist/ffmpeg.js +297 -0
- package/dist/index.js +3 -3
- package/dist/nexustalk.js +182 -149
- package/dist/plugins/camera.js +1164 -933
- package/dist/plugins/doorbell.js +26 -32
- package/dist/plugins/floodlight.js +11 -24
- package/dist/plugins/heatlink.js +411 -5
- package/dist/plugins/lock.js +309 -0
- package/dist/plugins/protect.js +240 -71
- package/dist/plugins/tempsensor.js +159 -35
- package/dist/plugins/thermostat.js +891 -455
- package/dist/plugins/weather.js +128 -33
- package/dist/protobuf/nest/services/apigateway.proto +1 -1
- package/dist/protobuf/nestlabs/gateway/v2.proto +1 -1
- package/dist/protobuf/root.proto +1 -0
- package/dist/rtpmuxer.js +186 -0
- package/dist/streamer.js +490 -248
- package/dist/system.js +1741 -2868
- package/dist/utils.js +327 -0
- package/dist/webrtc.js +358 -229
- package/package.json +19 -16
package/dist/config.js
CHANGED
|
@@ -1,23 +1,19 @@
|
|
|
1
1
|
// Configuration validation and processing
|
|
2
2
|
// Part of homebridge-nest-accfactory
|
|
3
3
|
//
|
|
4
|
-
// Code version 2025.06.
|
|
4
|
+
// Code version 2025.06.30
|
|
5
5
|
// Mark Hulskamp
|
|
6
6
|
'use strict';
|
|
7
7
|
|
|
8
8
|
// Define nodejs module requirements
|
|
9
|
-
import fs from 'fs';
|
|
10
9
|
import path from 'node:path';
|
|
11
10
|
import crypto from 'node:crypto';
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
|
|
12
|
+
// Import our modules
|
|
13
|
+
import FFmpeg from './ffmpeg.js';
|
|
14
14
|
|
|
15
15
|
// Define constants
|
|
16
|
-
|
|
17
|
-
const AccountType = {
|
|
18
|
-
Nest: 'Nest',
|
|
19
|
-
Google: 'Google',
|
|
20
|
-
};
|
|
16
|
+
import { FFMPEG_VERSION, ACCOUNT_TYPE } from './consts.js';
|
|
21
17
|
|
|
22
18
|
function processConfig(config, log) {
|
|
23
19
|
let options = (config.options = typeof config?.options === 'object' ? config.options : {});
|
|
@@ -40,93 +36,79 @@ function processConfig(config, log) {
|
|
|
40
36
|
options.maxStreams = isNaN(config.options?.maxStreams) === false ? Number(config.options.maxStreams) : 2;
|
|
41
37
|
|
|
42
38
|
// Check if a ffmpeg binary exist via a specific path in configuration OR /usr/local/bin
|
|
43
|
-
options.ffmpeg = {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
options.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
options.ffmpeg.libopus = false;
|
|
57
|
-
options.ffmpeg.libx264 = false;
|
|
58
|
-
options.ffmpeg.libfdk_aac = false;
|
|
59
|
-
|
|
60
|
-
if (fs.existsSync(options.ffmpeg.binary) === false) {
|
|
61
|
-
// If we flag ffmpegPath as undefined, no video streaming/record support enabled for camers/doorbells
|
|
62
|
-
log?.warn?.('Specified ffmpeg binary "%s" was not found', options.ffmpeg.binary);
|
|
63
|
-
log?.warn?.('Stream video/recording from camera/doorbells will be unavailable');
|
|
64
|
-
options.ffmpeg.binary = undefined;
|
|
39
|
+
options.ffmpeg = {
|
|
40
|
+
binary: undefined,
|
|
41
|
+
valid: false,
|
|
42
|
+
debug: config.options?.ffmpegDebug === true,
|
|
43
|
+
hwaccel: false,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
let ffmpegPath =
|
|
47
|
+
typeof config.options?.ffmpegPath === 'string' && config.options.ffmpegPath !== '' ? config.options.ffmpegPath : '/usr/local/bin';
|
|
48
|
+
|
|
49
|
+
let resolvedPath = path.resolve(ffmpegPath);
|
|
50
|
+
if (resolvedPath.endsWith('/ffmpeg') === false) {
|
|
51
|
+
resolvedPath += '/ffmpeg';
|
|
65
52
|
}
|
|
66
53
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
54
|
+
// Create FFmpeg probe
|
|
55
|
+
let ffmpeg = new FFmpeg(resolvedPath, log);
|
|
56
|
+
|
|
57
|
+
if (typeof ffmpeg.version !== 'string') {
|
|
58
|
+
log?.warn?.('ffmpeg binary "%s" not found or not executable, camera/doorbell streaming will be unavailable', ffmpeg.binary);
|
|
59
|
+
} else {
|
|
60
|
+
// Proceed with compatibility checks
|
|
61
|
+
options.ffmpeg.valid = ffmpeg.hasMinimumSupport({
|
|
62
|
+
version: FFMPEG_VERSION,
|
|
63
|
+
encoders: ['libx264', 'libfdk_aac', 'libopus'],
|
|
64
|
+
decoders: ['libspeex'],
|
|
70
65
|
});
|
|
66
|
+
if (options.ffmpeg.valid === false) {
|
|
67
|
+
log?.warn?.('ffmpeg binary "%s" does not meet the minimum support requirements', ffmpeg.binary);
|
|
71
68
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
// Determine what libraries ffmpeg is compiled with
|
|
76
|
-
options.ffmpeg.version = stdout.match(/(?:ffmpeg version:(\d+)\.)?(?:(\d+)\.)?(?:(\d+)\.\d+)(.*?)/gim)?.[0];
|
|
77
|
-
options.ffmpeg.libspeex = stdout.includes('--enable-libspeex') === true;
|
|
78
|
-
options.ffmpeg.libopus = stdout.includes('--enable-libopus') === true;
|
|
79
|
-
options.ffmpeg.libx264 = stdout.includes('--enable-libx264') === true;
|
|
80
|
-
options.ffmpeg.libfdk_aac = stdout.includes('--enable-libfdk-aac') === true;
|
|
81
|
-
|
|
82
|
-
let versionTooOld =
|
|
83
|
-
options.ffmpeg.version?.localeCompare(FFMPEGVERSION, undefined, {
|
|
69
|
+
if (
|
|
70
|
+
ffmpeg.version?.localeCompare(FFMPEG_VERSION, undefined, {
|
|
84
71
|
numeric: true,
|
|
85
72
|
sensitivity: 'case',
|
|
86
73
|
caseFirst: 'upper',
|
|
87
|
-
}) === -1
|
|
74
|
+
}) === -1
|
|
75
|
+
) {
|
|
76
|
+
log?.warn?.('Minimum binary version is "%s", however the installed version is "%s"', FFMPEG_VERSION, ffmpeg.version);
|
|
77
|
+
log?.warn?.('Stream video/recording from camera/doorbells will be unavailable');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if ((ffmpeg.features?.decoders || []).includes('libspeex') === false) {
|
|
81
|
+
log?.warn?.('Missing speex decoder in ffmpeg, talkback on certain camera/doorbells will be unavailable');
|
|
82
|
+
}
|
|
88
83
|
|
|
89
84
|
if (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
options.ffmpeg.libopus === false ||
|
|
93
|
-
options.ffmpeg.libx264 === false ||
|
|
94
|
-
options.ffmpeg.libfdk_aac === false
|
|
85
|
+
(ffmpeg.features?.encoders || []).includes('libfdk_aac') === false &&
|
|
86
|
+
(ffmpeg.features?.encoders || []).includes('libopus') === false
|
|
95
87
|
) {
|
|
96
|
-
log?.warn?.('
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
log?.warn?.('Missing libfdk_aac and libopus in ffmpeg binary, audio from camera/doorbells will be unavailable');
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (options.ffmpeg.libx264 && !options.ffmpeg.libfdk_aac) {
|
|
113
|
-
log?.warn?.('Missing libfdk_aac in ffmpeg binary, audio from camera/doorbells will be unavailable');
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (options.ffmpeg.libx264 && options.ffmpeg.libfdk_aac && !options.ffmpeg.libopus) {
|
|
117
|
-
log?.warn?.('Missing libopus in ffmpeg binary, audio (including talkback) from certain camera/doorbells will be unavailable');
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (!options.ffmpeg.libx264) {
|
|
121
|
-
log?.warn?.('Missing libx264 in ffmpeg binary, stream video/recording from camera/doorbells will be unavailable');
|
|
122
|
-
options.ffmpeg.binary = undefined; // No ffmpeg since we do not have all the required libraries
|
|
123
|
-
}
|
|
88
|
+
log?.warn?.('Missing fdk_aac and opus encoders in ffmpeg, audio from camera/doorbells will be unavailable');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if ((ffmpeg.features?.encoders || []).includes('libfdk_aac') === false) {
|
|
92
|
+
log?.warn?.('Missing fdk_aac encoder in ffmpeg, audio from camera/doorbells will be unavailable');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if ((ffmpeg.features?.encoders || []).includes('libopus') === false) {
|
|
96
|
+
log?.warn?.('Missing opus encoder in ffmpeg, talkback on certain camera/doorbells will be unavailable');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if ((ffmpeg.features?.encoders || []).includes('libx264') === false) {
|
|
100
|
+
log?.warn?.('Missing libx264 encoder in ffmpeg, stream video/recording from camera/doorbells will be unavailable');
|
|
124
101
|
}
|
|
125
102
|
}
|
|
126
|
-
}
|
|
127
103
|
|
|
128
|
-
|
|
129
|
-
|
|
104
|
+
if (options.ffmpeg.valid === true) {
|
|
105
|
+
log?.success?.('Found valid ffmpeg binary in %s', ffmpeg.binary);
|
|
106
|
+
options.ffmpeg.binary = ffmpeg.binary;
|
|
107
|
+
options.ffmpeg.hwaccel = ffmpeg.supportsHardwareH264 === true;
|
|
108
|
+
if (ffmpeg.supportsHardwareH264 === true) {
|
|
109
|
+
log?.debug?.('Hardware H264 encoding available via "%s"', ffmpeg.hardwareH264Codec);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
130
112
|
}
|
|
131
113
|
|
|
132
114
|
// Process per device configuration(s)
|
|
@@ -135,7 +117,7 @@ function processConfig(config, log) {
|
|
|
135
117
|
}
|
|
136
118
|
|
|
137
119
|
if (config?.devices !== undefined && Array.isArray(config.devices) === false) {
|
|
138
|
-
// If the devices section is a JSON
|
|
120
|
+
// If the devices section is a JSON object keyed by the devices serial number, convert to devices array object
|
|
139
121
|
let newDeviceArray = [];
|
|
140
122
|
for (const [serialNumber, props] of Object.entries(config.devices)) {
|
|
141
123
|
newDeviceArray.push({
|
|
@@ -167,7 +149,7 @@ function buildConnections(config) {
|
|
|
167
149
|
let fieldTest = section?.fieldTest === true;
|
|
168
150
|
connections[crypto.randomUUID()] = {
|
|
169
151
|
name: key,
|
|
170
|
-
type:
|
|
152
|
+
type: ACCOUNT_TYPE.NEST,
|
|
171
153
|
authorised: false,
|
|
172
154
|
access_token: section.access_token,
|
|
173
155
|
fieldTest,
|
|
@@ -187,7 +169,7 @@ function buildConnections(config) {
|
|
|
187
169
|
let fieldTest = section?.fieldTest === true;
|
|
188
170
|
connections[crypto.randomUUID()] = {
|
|
189
171
|
name: key,
|
|
190
|
-
type:
|
|
172
|
+
type: ACCOUNT_TYPE.GOOGLE,
|
|
191
173
|
authorised: false,
|
|
192
174
|
issuetoken: section.issuetoken,
|
|
193
175
|
cookie: section.cookie,
|
|
@@ -204,4 +186,4 @@ function buildConnections(config) {
|
|
|
204
186
|
}
|
|
205
187
|
|
|
206
188
|
// Define exports
|
|
207
|
-
export {
|
|
189
|
+
export { processConfig, buildConnections };
|
package/dist/consts.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// Common defines
|
|
2
|
+
// Part of homebridge-nest-accfactory
|
|
3
|
+
//
|
|
4
|
+
// Code version 2025.08.07
|
|
5
|
+
// Mark Hulskamp
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
// Define nodejs module requirements
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import url from 'node:url';
|
|
11
|
+
|
|
12
|
+
// Define constants
|
|
13
|
+
export const TIMERS = {
|
|
14
|
+
ALERTS: 2000, // Camera alert polling interval (ms)
|
|
15
|
+
ZONES: 30000, // Camera zone polling interval (ms)
|
|
16
|
+
WEATHER: 300000, // Weather data refresh interval (ms)
|
|
17
|
+
NEST_API: 10000, // Nest API request timeout (ms)
|
|
18
|
+
TALKBACK_AUDIO: 1000, // Audio talkback timeout (ms)
|
|
19
|
+
SNAPSHOT: 30000, // Timeout for retaining snapshot image timeout (ms)
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const USER_AGENT = 'Nest/5.82.2 (iOScom.nestlabs.jasper.release) os=18.5'; // User Agent string
|
|
23
|
+
|
|
24
|
+
export const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); // Make a defined for JS __dirname
|
|
25
|
+
|
|
26
|
+
export const DATA_SOURCE = {
|
|
27
|
+
NEST: 'Nest', // From the Nest API
|
|
28
|
+
GOOGLE: 'Google', // From the Protobuf/Google API
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const DAYS_OF_WEEK_FULL = ['MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY', 'SUNDAY'];
|
|
32
|
+
|
|
33
|
+
export const DAYS_OF_WEEK_SHORT = ['MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', 'SUN'];
|
|
34
|
+
|
|
35
|
+
export const PROTOBUF_RESOURCES = {
|
|
36
|
+
THERMOSTAT: [
|
|
37
|
+
'nest.resource.NestAmber1DisplayResource',
|
|
38
|
+
'nest.resource.NestAmber2DisplayResource',
|
|
39
|
+
'nest.resource.NestLearningThermostat1Resource',
|
|
40
|
+
'nest.resource.NestLearningThermostat2Resource',
|
|
41
|
+
'nest.resource.NestLearningThermostat3Resource',
|
|
42
|
+
'nest.resource.NestAgateDisplayResource',
|
|
43
|
+
'nest.resource.NestOnyxResource',
|
|
44
|
+
'google.resource.GoogleZirconium1Resource',
|
|
45
|
+
'google.resource.GoogleBismuth1Resource',
|
|
46
|
+
],
|
|
47
|
+
HEATLINK: ['nest.resource.NestAgateHeatlinkResource'],
|
|
48
|
+
KRYPTONITE: ['nest.resource.NestKryptoniteResource'],
|
|
49
|
+
LOCK: ['yale.resource.LinusLockResource'],
|
|
50
|
+
PROTECT: [
|
|
51
|
+
'nest.resource.NestProtect1LinePoweredResource',
|
|
52
|
+
'nest.resource.NestProtect1BatteryPoweredResource',
|
|
53
|
+
'nest.resource.NestProtect2LinePoweredResource',
|
|
54
|
+
'nest.resource.NestProtect2BatteryPoweredResource',
|
|
55
|
+
'nest.resource.NestProtect2Resource',
|
|
56
|
+
],
|
|
57
|
+
CAMERA: [
|
|
58
|
+
'google.resource.GreenQuartzResource',
|
|
59
|
+
'google.resource.SpencerResource',
|
|
60
|
+
'google.resource.VenusResource',
|
|
61
|
+
'nest.resource.NestCamIndoorResource',
|
|
62
|
+
'nest.resource.NestCamIQResource',
|
|
63
|
+
'nest.resource.NestCamIQOutdoorResource',
|
|
64
|
+
'nest.resource.NestCamOutdoorResource',
|
|
65
|
+
'nest.resource.NestHelloResource',
|
|
66
|
+
'google.resource.GoogleNewmanResource',
|
|
67
|
+
],
|
|
68
|
+
DOORBELL: ['nest.resource.NestHelloResource', 'google.resource.GreenQuartzResource', 'google.resource.VenusResource'],
|
|
69
|
+
FLOODLIGHT: ['google.resource.NeonQuartzResource', 'google.resource.AzizResource'],
|
|
70
|
+
CONNECT: ['nest.resource.NestConnectResource'],
|
|
71
|
+
DETECT: ['nest.resource.NestDetectResource'],
|
|
72
|
+
GUARD: ['nest.resource.NestHelloResource'],
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const NEST_API_BUCKETS = [
|
|
76
|
+
'buckets',
|
|
77
|
+
'delayed_topaz',
|
|
78
|
+
'demand_response',
|
|
79
|
+
'device',
|
|
80
|
+
'device_alert_dialog',
|
|
81
|
+
'geofence_info',
|
|
82
|
+
'kryptonite',
|
|
83
|
+
'link',
|
|
84
|
+
'message',
|
|
85
|
+
'message_center',
|
|
86
|
+
'metadata',
|
|
87
|
+
'occupancy',
|
|
88
|
+
'quartz',
|
|
89
|
+
'safety',
|
|
90
|
+
'rcs_settings',
|
|
91
|
+
'safety_summary',
|
|
92
|
+
'schedule',
|
|
93
|
+
'shared',
|
|
94
|
+
'structure',
|
|
95
|
+
'structure_metadata',
|
|
96
|
+
'topaz',
|
|
97
|
+
'topaz_resource',
|
|
98
|
+
'track',
|
|
99
|
+
'trip',
|
|
100
|
+
'tuneups',
|
|
101
|
+
'user',
|
|
102
|
+
'user_settings',
|
|
103
|
+
'where',
|
|
104
|
+
'widget_track',
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
export const DEVICE_TYPE = {
|
|
108
|
+
THERMOSTAT: 'Thermostat',
|
|
109
|
+
TEMPSENSOR: 'TemperatureSensor',
|
|
110
|
+
PROTECT: 'Protect',
|
|
111
|
+
CAMERA: 'Camera',
|
|
112
|
+
DOORBELL: 'Doorbell',
|
|
113
|
+
FLOODLIGHT: 'FloodlightCamera',
|
|
114
|
+
WEATHER: 'Weather',
|
|
115
|
+
HEATLINK: 'Heatlink',
|
|
116
|
+
LOCK: 'Lock',
|
|
117
|
+
ALARM: 'Alarm',
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export const FFMPEG_VERSION = '6.0.0';
|
|
121
|
+
|
|
122
|
+
export const ACCOUNT_TYPE = {
|
|
123
|
+
NEST: 'Nest',
|
|
124
|
+
GOOGLE: 'Google',
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export const LOW_BATTERY_LEVEL = 10; // Low battery level percentage
|
|
128
|
+
|
|
129
|
+
export const THERMOSTAT_MIN_TEMPERATURE = 9; // Minimum temperature for Nest Thermostat
|
|
130
|
+
|
|
131
|
+
export const THERMOSTAT_MAX_TEMPERATURE = 32; // Maximum temperature for Nest Thermostat
|
|
132
|
+
|
|
133
|
+
export const HOTWATER_MIN_TEMPERATURE = 30; // Minimum temperature for hotwater heating
|
|
134
|
+
|
|
135
|
+
export const HOTWATER_MAX_TEMPERATURE = 70; // Maximum temperature for hotwater heating
|
|
136
|
+
|
|
137
|
+
export const HOTWATER_BOOST_TIMES = [1800, 3600, 7200]; // Valid hotwater boost times
|
|
138
|
+
|
|
139
|
+
export const RESOURCE_PATH = './res';
|
|
140
|
+
export const RESOURCE_IMAGES = {
|
|
141
|
+
CAMERA_OFFLINE: 'Nest_camera_offline.jpg',
|
|
142
|
+
CAMERA_OFF: 'Nest_camera_off.jpg',
|
|
143
|
+
CAMERA_TRANSFER: 'Nest_camera_transfer.jpg',
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export const RESOURCE_FRAMES = {
|
|
147
|
+
CAMERA_OFFLINE: 'Nest_camera_offline.h264',
|
|
148
|
+
CAMERA_OFF: 'Nest_camera_off.h264',
|
|
149
|
+
CAMERA_TRANSFER: 'Nest_camera_transfer.h264',
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export const LOG_LEVELS = {
|
|
153
|
+
INFO: 'info',
|
|
154
|
+
SUCCESS: 'success',
|
|
155
|
+
WARN: 'warn',
|
|
156
|
+
ERROR: 'error',
|
|
157
|
+
DEBUG: 'debug',
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
export const NESTLABS_MAC_PREFIX = '18B430';
|
package/dist/devices.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Device support loader
|
|
2
2
|
// Part of homebridge-nest-accfactory
|
|
3
3
|
//
|
|
4
|
-
// Code version 2025.
|
|
4
|
+
// Code version 2025.07.25
|
|
5
5
|
// Mark Hulskamp
|
|
6
6
|
'use strict';
|
|
7
7
|
|
|
@@ -14,62 +14,50 @@ import url from 'node:url';
|
|
|
14
14
|
import HomeKitDevice from './HomeKitDevice.js';
|
|
15
15
|
|
|
16
16
|
// Define constants
|
|
17
|
-
|
|
18
|
-
const DeviceType = Object.freeze({
|
|
19
|
-
THERMOSTAT: 'Thermostat',
|
|
20
|
-
TEMPSENSOR: 'TemperatureSensor',
|
|
21
|
-
SMOKESENSOR: 'Protect',
|
|
22
|
-
CAMERA: 'Camera',
|
|
23
|
-
DOORBELL: 'Doorbell',
|
|
24
|
-
FLOODLIGHT: 'FloodlightCamera',
|
|
25
|
-
WEATHER: 'Weather',
|
|
26
|
-
HEATLINK: 'Heatlink',
|
|
27
|
-
LOCK: 'Lock',
|
|
28
|
-
ALARM: 'Alarm',
|
|
29
|
-
});
|
|
17
|
+
import { __dirname, DEVICE_TYPE } from './consts.js';
|
|
30
18
|
|
|
31
19
|
async function loadDeviceModules(log, pluginDir = '') {
|
|
32
20
|
let baseDir = path.join(__dirname, pluginDir);
|
|
33
21
|
let deviceMap = new Map();
|
|
34
22
|
let files = (await fs.readdir(baseDir)).sort();
|
|
35
23
|
|
|
24
|
+
log?.debug?.('Using base module v%s', HomeKitDevice.VERSION);
|
|
25
|
+
|
|
36
26
|
for (const file of files) {
|
|
37
27
|
if (file.endsWith('.js') === false) {
|
|
38
28
|
continue;
|
|
39
29
|
}
|
|
40
30
|
|
|
41
31
|
try {
|
|
42
|
-
let
|
|
43
|
-
let
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
32
|
+
let modulePath = url.pathToFileURL(path.join(baseDir, file)).href;
|
|
33
|
+
let module = await import(modulePath);
|
|
34
|
+
let chosenClass = undefined;
|
|
35
|
+
|
|
36
|
+
// First pass: find a valid subclass of HomeKitDevice
|
|
37
|
+
for (const exported of Object.values(module)) {
|
|
38
|
+
if (
|
|
39
|
+
typeof exported === 'function' &&
|
|
40
|
+
HomeKitDevice.prototype.isPrototypeOf(exported.prototype) &&
|
|
41
|
+
typeof exported.TYPE === 'string' &&
|
|
42
|
+
typeof exported.VERSION === 'string'
|
|
43
|
+
) {
|
|
44
|
+
chosenClass = exported;
|
|
45
|
+
break; // first valid one wins (like your original)
|
|
48
46
|
}
|
|
47
|
+
}
|
|
49
48
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (proto === HomeKitDevice) {
|
|
53
|
-
if (
|
|
54
|
-
typeof exported.TYPE !== 'string' ||
|
|
55
|
-
exported.TYPE === '' ||
|
|
56
|
-
typeof exported.VERSION !== 'string' ||
|
|
57
|
-
exported.VERSION === ''
|
|
58
|
-
) {
|
|
59
|
-
log?.warn?.('Skipping device module %s (missing TYPE or VERSION)', file);
|
|
60
|
-
break;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (deviceMap.has(exported.TYPE) === false) {
|
|
64
|
-
deviceMap.set(exported.TYPE, exported);
|
|
65
|
-
log?.info?.('Loaded device module "%s" (v%s)', exported.TYPE, exported.VERSION);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
break;
|
|
69
|
-
}
|
|
49
|
+
if (chosenClass && deviceMap.has(chosenClass.TYPE) === false) {
|
|
50
|
+
let entry = { class: chosenClass };
|
|
70
51
|
|
|
71
|
-
|
|
52
|
+
// Add additional named functions (like processRawData)
|
|
53
|
+
for (const [key, value] of Object.entries(module)) {
|
|
54
|
+
if (typeof value === 'function' && value !== chosenClass) {
|
|
55
|
+
entry[key] = value;
|
|
56
|
+
}
|
|
72
57
|
}
|
|
58
|
+
|
|
59
|
+
deviceMap.set(chosenClass.TYPE, entry);
|
|
60
|
+
log?.info?.('Loaded %s module v%s', chosenClass.TYPE, chosenClass.VERSION);
|
|
73
61
|
}
|
|
74
62
|
} catch (error) {
|
|
75
63
|
log?.warn?.('Failed to load device support file "%s": %s', file, error.message);
|
|
@@ -82,27 +70,31 @@ async function loadDeviceModules(log, pluginDir = '') {
|
|
|
82
70
|
function getDeviceHKCategory(type) {
|
|
83
71
|
let category = 1; // Categories.OTHER
|
|
84
72
|
|
|
85
|
-
if (type ===
|
|
73
|
+
if (type === DEVICE_TYPE.LOCK) {
|
|
86
74
|
category = 6; // Categories.DOOR_LOCK
|
|
87
75
|
}
|
|
88
76
|
|
|
89
|
-
if (type ===
|
|
77
|
+
if (type === DEVICE_TYPE.HEATLINK) {
|
|
78
|
+
category = 8; // Categories.SWITCH
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (type === DEVICE_TYPE.THERMOSTAT) {
|
|
90
82
|
category = 9; // Categories.THERMOSTAT
|
|
91
83
|
}
|
|
92
84
|
|
|
93
|
-
if (type ===
|
|
85
|
+
if (type === DEVICE_TYPE.TEMPSENSOR || type === DEVICE_TYPE.PROTECT || type === DEVICE_TYPE.WEATHER) {
|
|
94
86
|
category = 10; // Categories.SENSOR
|
|
95
87
|
}
|
|
96
88
|
|
|
97
|
-
if (type ===
|
|
89
|
+
if (type === DEVICE_TYPE.ALARM) {
|
|
98
90
|
category = 11; // Categories.SECURITY_SYSTEM
|
|
99
91
|
}
|
|
100
92
|
|
|
101
|
-
if (type ===
|
|
93
|
+
if (type === DEVICE_TYPE.CAMERA || type === DEVICE_TYPE.FLOODLIGHT) {
|
|
102
94
|
category = 17; // Categories.IP_CAMERA
|
|
103
95
|
}
|
|
104
96
|
|
|
105
|
-
if (type ===
|
|
97
|
+
if (type === DEVICE_TYPE.DOORBELL) {
|
|
106
98
|
category = 18; // Categories.VIDEO_DOORBELL
|
|
107
99
|
}
|
|
108
100
|
|
|
@@ -110,4 +102,4 @@ function getDeviceHKCategory(type) {
|
|
|
110
102
|
}
|
|
111
103
|
|
|
112
104
|
// Define exports
|
|
113
|
-
export {
|
|
105
|
+
export { loadDeviceModules, getDeviceHKCategory };
|