iobroker.motioneye 0.1.2 → 0.2.1
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/README.md +14 -8
- package/admin/i18n/de.json +22 -0
- package/admin/i18n/en.json +22 -0
- package/admin/i18n/es.json +22 -0
- package/admin/i18n/fr.json +22 -0
- package/admin/i18n/it.json +22 -0
- package/admin/i18n/nl.json +22 -0
- package/admin/i18n/pl.json +22 -0
- package/admin/i18n/pt.json +22 -0
- package/admin/i18n/ru.json +22 -0
- package/admin/i18n/uk.json +22 -0
- package/admin/i18n/zh-cn.json +22 -0
- package/admin/jsonConfig.json +195 -4
- package/io-package.json +34 -1
- package/lib/cameraDiscovery.js +130 -0
- package/lib/cameraDiscovery.test.js +76 -0
- package/lib/cameraRegistry.js +3 -0
- package/lib/mediaStorage.js +62 -0
- package/lib/mediaStorage.test.js +31 -0
- package/lib/motionEyeApi.js +5 -3
- package/lib/streamManager.js +6 -9
- package/main.js +99 -3
- package/package.json +3 -3
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { expect } = require('chai');
|
|
4
|
+
const {
|
|
5
|
+
extractMediaFolderFromMotionEyeConfig,
|
|
6
|
+
mapMotionEyeCamera,
|
|
7
|
+
mergeMotionEyeCameras,
|
|
8
|
+
parseLoadCamerasMessage,
|
|
9
|
+
} = require('./cameraDiscovery');
|
|
10
|
+
|
|
11
|
+
describe('cameraDiscovery', () => {
|
|
12
|
+
it('extractMediaFolderFromMotionEyeConfig should read custom folder names', () => {
|
|
13
|
+
expect(
|
|
14
|
+
extractMediaFolderFromMotionEyeConfig({
|
|
15
|
+
root_directory: '/var/lib/motioneye/Bambu',
|
|
16
|
+
}),
|
|
17
|
+
).to.equal('Bambu');
|
|
18
|
+
expect(
|
|
19
|
+
extractMediaFolderFromMotionEyeConfig({
|
|
20
|
+
root_directory: '/var/lib/motioneye/Camera8',
|
|
21
|
+
}),
|
|
22
|
+
).to.equal('');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('mapMotionEyeCamera should build admin row defaults', () => {
|
|
26
|
+
expect(
|
|
27
|
+
mapMotionEyeCamera(
|
|
28
|
+
{
|
|
29
|
+
id: 3,
|
|
30
|
+
name: 'Auffahrt',
|
|
31
|
+
root_directory: '/var/lib/motioneye/Auffahrt',
|
|
32
|
+
},
|
|
33
|
+
'still',
|
|
34
|
+
),
|
|
35
|
+
).to.deep.equal({
|
|
36
|
+
name: 'Auffahrt',
|
|
37
|
+
motionEyeId: 3,
|
|
38
|
+
id: '',
|
|
39
|
+
mediaFolder: 'Auffahrt',
|
|
40
|
+
defaultMode: 'still',
|
|
41
|
+
enabled: true,
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('parseLoadCamerasMessage should decode camerasPayload from admin jsonData', () => {
|
|
46
|
+
const cameras = [{ name: 'A', motionEyeId: 1 }];
|
|
47
|
+
const payload = parseLoadCamerasMessage({
|
|
48
|
+
motionHost: '192.168.1.1',
|
|
49
|
+
camerasPayload: encodeURIComponent(JSON.stringify(cameras)),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
expect(payload.motionHost).to.equal('192.168.1.1');
|
|
53
|
+
expect(payload.cameras).to.deep.equal(cameras);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('mergeMotionEyeCameras should keep existing rows and append missing ids', () => {
|
|
57
|
+
const result = mergeMotionEyeCameras(
|
|
58
|
+
[{ name: 'Carport', motionEyeId: 1, enabled: true, defaultMode: 'off' }],
|
|
59
|
+
[
|
|
60
|
+
{ id: 1, name: 'Carport ME' },
|
|
61
|
+
{ id: 2, name: 'Garten' },
|
|
62
|
+
],
|
|
63
|
+
'sharp',
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
expect(result.added).to.equal(1);
|
|
67
|
+
expect(result.cameras).to.have.length(2);
|
|
68
|
+
expect(result.cameras[0].name).to.equal('Carport');
|
|
69
|
+
expect(result.cameras[1]).to.deep.include({
|
|
70
|
+
name: 'Garten',
|
|
71
|
+
motionEyeId: 2,
|
|
72
|
+
defaultMode: 'sharp',
|
|
73
|
+
enabled: true,
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
});
|
package/lib/cameraRegistry.js
CHANGED
|
@@ -31,6 +31,7 @@ function slugifyId(value) {
|
|
|
31
31
|
* @property {number} motionEyeId
|
|
32
32
|
* @property {boolean} [enabled]
|
|
33
33
|
* @property {string} [defaultMode]
|
|
34
|
+
* @property {string} [mediaFolder] Folder name under /var/lib/motioneye
|
|
34
35
|
*/
|
|
35
36
|
|
|
36
37
|
/**
|
|
@@ -41,6 +42,7 @@ function slugifyId(value) {
|
|
|
41
42
|
* @property {number} motionEyeId
|
|
42
43
|
* @property {boolean} enabled
|
|
43
44
|
* @property {string} defaultMode
|
|
45
|
+
* @property {string} mediaFolder Sanitized folder name or empty
|
|
44
46
|
*/
|
|
45
47
|
|
|
46
48
|
/**
|
|
@@ -98,6 +100,7 @@ function resolveCameras(cameras, fallbackDefaultMode = 'off') {
|
|
|
98
100
|
motionEyeId: Number(entry.motionEyeId),
|
|
99
101
|
enabled: entry.enabled !== false,
|
|
100
102
|
defaultMode: entry.defaultMode || fallbackDefaultMode,
|
|
103
|
+
mediaFolder: String(entry.mediaFolder || '').trim(),
|
|
101
104
|
});
|
|
102
105
|
}
|
|
103
106
|
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/** Default MotionEye media base path (see motioneye.conf media_path). */
|
|
4
|
+
const MOTIONEYE_MEDIA_BASE = '/var/lib/motioneye';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Sanitize a single folder name segment (no path separators).
|
|
8
|
+
* @param {unknown} value
|
|
9
|
+
* @returns {string}
|
|
10
|
+
*/
|
|
11
|
+
function sanitizeMediaFolderName(value) {
|
|
12
|
+
const trimmed = String(value == null ? '' : value).trim();
|
|
13
|
+
if (!trimmed) {
|
|
14
|
+
return '';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (/[/\\]/.test(trimmed) || trimmed.includes('..')) {
|
|
18
|
+
return '';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return trimmed
|
|
22
|
+
.replace(/[^\wäöüÄÖÜß .-]/g, '_')
|
|
23
|
+
.replace(/\s+/g, ' ')
|
|
24
|
+
.trim();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {unknown} folderName
|
|
29
|
+
* @returns {string} Full root_directory path or empty when invalid/unset
|
|
30
|
+
*/
|
|
31
|
+
function buildMediaRootDirectory(folderName) {
|
|
32
|
+
const folder = sanitizeMediaFolderName(folderName);
|
|
33
|
+
if (!folder) {
|
|
34
|
+
return '';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return `${MOTIONEYE_MEDIA_BASE}/${folder}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* MotionEye config patch for custom file storage path.
|
|
42
|
+
* @param {unknown} folderName
|
|
43
|
+
* @returns {Record<string, unknown>}
|
|
44
|
+
*/
|
|
45
|
+
function buildStoragePatch(folderName) {
|
|
46
|
+
const rootDirectory = buildMediaRootDirectory(folderName);
|
|
47
|
+
if (!rootDirectory) {
|
|
48
|
+
return {};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
storage_device: 'custom-path',
|
|
53
|
+
root_directory: rootDirectory,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = {
|
|
58
|
+
MOTIONEYE_MEDIA_BASE,
|
|
59
|
+
sanitizeMediaFolderName,
|
|
60
|
+
buildMediaRootDirectory,
|
|
61
|
+
buildStoragePatch,
|
|
62
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { expect } = require('chai');
|
|
4
|
+
const {
|
|
5
|
+
MOTIONEYE_MEDIA_BASE,
|
|
6
|
+
sanitizeMediaFolderName,
|
|
7
|
+
buildMediaRootDirectory,
|
|
8
|
+
buildStoragePatch,
|
|
9
|
+
} = require('./mediaStorage');
|
|
10
|
+
|
|
11
|
+
describe('mediaStorage', () => {
|
|
12
|
+
it('sanitizeMediaFolderName should reject path separators', () => {
|
|
13
|
+
expect(sanitizeMediaFolderName('Bambu')).to.equal('Bambu');
|
|
14
|
+
expect(sanitizeMediaFolderName('Innenhof I')).to.equal('Innenhof I');
|
|
15
|
+
expect(sanitizeMediaFolderName('/var/lib/foo')).to.equal('');
|
|
16
|
+
expect(sanitizeMediaFolderName('..')).to.equal('');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('buildMediaRootDirectory should prepend MotionEye base path', () => {
|
|
20
|
+
expect(buildMediaRootDirectory('Bambu')).to.equal(`${MOTIONEYE_MEDIA_BASE}/Bambu`);
|
|
21
|
+
expect(buildMediaRootDirectory('')).to.equal('');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('buildStoragePatch should return custom-path config', () => {
|
|
25
|
+
expect(buildStoragePatch('Carport')).to.deep.equal({
|
|
26
|
+
storage_device: 'custom-path',
|
|
27
|
+
root_directory: `${MOTIONEYE_MEDIA_BASE}/Carport`,
|
|
28
|
+
});
|
|
29
|
+
expect(buildStoragePatch('')).to.deep.equal({});
|
|
30
|
+
});
|
|
31
|
+
});
|
package/lib/motionEyeApi.js
CHANGED
|
@@ -9,7 +9,7 @@ const SIGNATURE_REGEX = new RegExp('[^a-zA-Z0-9/?_.=&{}\\[\\]":, -]', 'g');
|
|
|
9
9
|
const LIST_CACHE_MS = 15000;
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* @param {string|undefined} serverHeader
|
|
12
|
+
* @param {string | string[] | undefined} serverHeader
|
|
13
13
|
* @returns {string}
|
|
14
14
|
*/
|
|
15
15
|
function parseMotionEyeServerHeader(serverHeader) {
|
|
@@ -17,7 +17,8 @@ function parseMotionEyeServerHeader(serverHeader) {
|
|
|
17
17
|
return '';
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
const
|
|
20
|
+
const header = Array.isArray(serverHeader) ? serverHeader[0] : serverHeader;
|
|
21
|
+
const match = String(header).match(/^motionEye\/(.+)$/i);
|
|
21
22
|
return match ? match[1].trim() : '';
|
|
22
23
|
}
|
|
23
24
|
|
|
@@ -152,6 +153,7 @@ function buildAuthPath(path, method, body, username, signKey) {
|
|
|
152
153
|
* @property {number} status
|
|
153
154
|
* @property {unknown} data
|
|
154
155
|
* @property {string} body
|
|
156
|
+
* @property {import('node:http').IncomingHttpHeaders} [headers]
|
|
155
157
|
*/
|
|
156
158
|
|
|
157
159
|
/**
|
|
@@ -174,7 +176,7 @@ function createMotionEyeApi(options) {
|
|
|
174
176
|
* @param {string} path
|
|
175
177
|
* @param {string} [method]
|
|
176
178
|
* @param {string|null} [body]
|
|
177
|
-
* @returns {Promise<{ status: number, body: string }>}
|
|
179
|
+
* @returns {Promise<{ status: number, body: string, headers: import('node:http').IncomingHttpHeaders }>}
|
|
178
180
|
*/
|
|
179
181
|
function httpRequest(path, method = 'GET', body = null) {
|
|
180
182
|
return new Promise((resolve, reject) => {
|
package/lib/streamManager.js
CHANGED
|
@@ -84,10 +84,6 @@ function checkStreamPort(motionHost, port) {
|
|
|
84
84
|
});
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
function sleep(ms) {
|
|
88
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
89
|
-
}
|
|
90
|
-
|
|
91
87
|
/**
|
|
92
88
|
* @typedef {object} StreamManagerDeps
|
|
93
89
|
* @property {string} motionHost
|
|
@@ -104,6 +100,7 @@ function sleep(ms) {
|
|
|
104
100
|
* @property {(level: string, message: string) => void} log
|
|
105
101
|
* @property {(fn: () => void, ms: number) => unknown} setTimeoutFn
|
|
106
102
|
* @property {(id: unknown) => void} clearTimeoutFn
|
|
103
|
+
* @property {(ms: number) => Promise<void>} delayFn
|
|
107
104
|
* @property {() => Map<string, import('./cameraRegistry').ResolvedCamera>} getCamerasByChannel
|
|
108
105
|
* @property {() => boolean} isUnloading
|
|
109
106
|
*/
|
|
@@ -209,7 +206,7 @@ function createStreamManager(deps) {
|
|
|
209
206
|
*/
|
|
210
207
|
async function publishStreamHtmlWhenReady(camera) {
|
|
211
208
|
const started = Date.now();
|
|
212
|
-
await
|
|
209
|
+
await deps.delayFn(deps.streamStartDelayMs);
|
|
213
210
|
|
|
214
211
|
while (Date.now() - started < deps.streamReadyTimeoutMs) {
|
|
215
212
|
if (deps.isUnloading() || !(await isStreamEnabled(camera))) {
|
|
@@ -221,7 +218,7 @@ function createStreamManager(deps) {
|
|
|
221
218
|
uiConfig = await getStreamUiConfig(camera);
|
|
222
219
|
} catch (error) {
|
|
223
220
|
deps.log('debug', `Stream port query ${camera.name}: ${error.message}`);
|
|
224
|
-
await
|
|
221
|
+
await deps.delayFn(deps.streamRetryMs);
|
|
225
222
|
continue;
|
|
226
223
|
}
|
|
227
224
|
|
|
@@ -234,7 +231,7 @@ function createStreamManager(deps) {
|
|
|
234
231
|
}
|
|
235
232
|
|
|
236
233
|
await setStreamLoadingHtml(camera);
|
|
237
|
-
await
|
|
234
|
+
await deps.delayFn(deps.streamRetryMs);
|
|
238
235
|
}
|
|
239
236
|
|
|
240
237
|
if (await isStreamEnabled(camera)) {
|
|
@@ -289,7 +286,7 @@ function createStreamManager(deps) {
|
|
|
289
286
|
/** @type {Record<string, boolean>} */
|
|
290
287
|
const relinked = {};
|
|
291
288
|
|
|
292
|
-
await
|
|
289
|
+
await deps.delayFn(deps.streamStartDelayMs);
|
|
293
290
|
|
|
294
291
|
while (Date.now() - started < deps.streamSiblingRelinkTimeoutMs) {
|
|
295
292
|
if (deps.isUnloading() || relinkRunIds[runKey] !== runId) {
|
|
@@ -328,7 +325,7 @@ function createStreamManager(deps) {
|
|
|
328
325
|
return;
|
|
329
326
|
}
|
|
330
327
|
|
|
331
|
-
await
|
|
328
|
+
await deps.delayFn(deps.streamRetryMs);
|
|
332
329
|
}
|
|
333
330
|
}
|
|
334
331
|
|
package/main.js
CHANGED
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
const utils = require('@iobroker/adapter-core');
|
|
8
8
|
const { createMotionEyeApi } = require('./lib/motionEyeApi');
|
|
9
9
|
const { createMotionApi } = require('./lib/motionApi');
|
|
10
|
+
const { buildStoragePatch } = require('./lib/mediaStorage');
|
|
10
11
|
const { INFO_STATE_LABELS } = require('./lib/infoLabels');
|
|
12
|
+
const { mergeMotionEyeCameras, parseLoadCamerasMessage } = require('./lib/cameraDiscovery');
|
|
11
13
|
const { resolveCameras, buildWebhookUrl } = require('./lib/cameraRegistry');
|
|
12
14
|
const {
|
|
13
15
|
normalizeMode,
|
|
@@ -35,6 +37,12 @@ class Motioneye extends utils.Adapter {
|
|
|
35
37
|
});
|
|
36
38
|
this.on('ready', this.onReady.bind(this));
|
|
37
39
|
this.on('stateChange', this.onStateChange.bind(this));
|
|
40
|
+
this.on('message', obj => {
|
|
41
|
+
this.onMessage(obj).catch(error => {
|
|
42
|
+
this.log.error(`onMessage failed: ${error.stack || error.message || error}`);
|
|
43
|
+
this.replyToMessage(obj, { error: String(error.message || error) });
|
|
44
|
+
});
|
|
45
|
+
});
|
|
38
46
|
this.on('unload', this.onUnload.bind(this));
|
|
39
47
|
|
|
40
48
|
this.motionEyeApi = undefined;
|
|
@@ -93,6 +101,7 @@ class Motioneye extends utils.Adapter {
|
|
|
93
101
|
setState: (id, val, ack) => this.setStateAsync(id, val, ack),
|
|
94
102
|
log: (level, message) => this.log[level](message),
|
|
95
103
|
setTimeoutFn: (fn, ms) => this.setTimeout(fn, ms),
|
|
104
|
+
delayFn: ms => this.delay(ms),
|
|
96
105
|
clearTimeoutFn: id => {
|
|
97
106
|
// @ts-expect-error adapter-core branded Timeout id from setTimeout
|
|
98
107
|
this.clearTimeout(id);
|
|
@@ -209,7 +218,7 @@ class Motioneye extends utils.Adapter {
|
|
|
209
218
|
}
|
|
210
219
|
|
|
211
220
|
async ensureInfoStates() {
|
|
212
|
-
for (const [stateId,
|
|
221
|
+
for (const [stateId, labels] of Object.entries(INFO_STATE_LABELS)) {
|
|
213
222
|
const type = stateId === 'camerasOnline' ? 'number' : 'string';
|
|
214
223
|
const role =
|
|
215
224
|
stateId === 'connection' ? 'indicator.connected' : stateId === 'camerasOnline' ? 'value' : 'text';
|
|
@@ -217,7 +226,7 @@ class Motioneye extends utils.Adapter {
|
|
|
217
226
|
await this.setObjectNotExistsAsync(`${INFO_PREFIX}.${stateId}`, {
|
|
218
227
|
type: 'state',
|
|
219
228
|
common: {
|
|
220
|
-
name,
|
|
229
|
+
name: /** @type {ioBroker.StringOrTranslated} */ (labels),
|
|
221
230
|
type: stateId === 'connection' ? 'boolean' : type,
|
|
222
231
|
role,
|
|
223
232
|
read: true,
|
|
@@ -411,7 +420,7 @@ class Motioneye extends utils.Adapter {
|
|
|
411
420
|
{
|
|
412
421
|
id: 'streamUrl',
|
|
413
422
|
common: {
|
|
414
|
-
name: `${camera.name} stream HTML
|
|
423
|
+
name: `${camera.name} stream HTML`,
|
|
415
424
|
type: 'string',
|
|
416
425
|
role: 'text',
|
|
417
426
|
read: true,
|
|
@@ -462,6 +471,16 @@ class Motioneye extends utils.Adapter {
|
|
|
462
471
|
});
|
|
463
472
|
}
|
|
464
473
|
|
|
474
|
+
const streamUrlId = `${channelId}.streamUrl`;
|
|
475
|
+
const streamUrlName = `${camera.name} stream HTML`;
|
|
476
|
+
const streamUrlObject = await this.getObjectAsync(streamUrlId);
|
|
477
|
+
const currentStreamUrlName = streamUrlObject?.common?.name ? String(streamUrlObject.common.name) : '';
|
|
478
|
+
if (currentStreamUrlName && /\(inventwo\)|inventwo HTML/i.test(currentStreamUrlName)) {
|
|
479
|
+
await this.extendObjectAsync(streamUrlId, {
|
|
480
|
+
common: { name: streamUrlName },
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
|
|
465
484
|
const webhookUrl = this.getWebhookUrl(camera);
|
|
466
485
|
await this.setStateAsync(`${channelId}.webhookUrl`, webhookUrl, true);
|
|
467
486
|
await this.setStateAsync(`${channelId}.motionEyeId`, camera.motionEyeId, true);
|
|
@@ -534,6 +553,15 @@ class Motioneye extends utils.Adapter {
|
|
|
534
553
|
patch.video_streaming = false;
|
|
535
554
|
}
|
|
536
555
|
|
|
556
|
+
const storagePatch = buildStoragePatch(camera.mediaFolder);
|
|
557
|
+
if (camera.mediaFolder && !storagePatch.root_directory) {
|
|
558
|
+
this.log.warn(
|
|
559
|
+
`Invalid media folder for ${camera.name}: "${camera.mediaFolder}" — skipped (use a single folder name without slashes)`,
|
|
560
|
+
);
|
|
561
|
+
} else {
|
|
562
|
+
Object.assign(patch, storagePatch);
|
|
563
|
+
}
|
|
564
|
+
|
|
537
565
|
const result = await this.motionEyeApi.saveCameraConfig(camera.motionEyeId, patch);
|
|
538
566
|
await this.setStateAsync(`${channelId}.mode`, mode, true);
|
|
539
567
|
await this.setStateAsync(`${channelId}.status`, `Mode=${MODE_LABELS[mode]}`, true);
|
|
@@ -754,6 +782,74 @@ class Motioneye extends utils.Adapter {
|
|
|
754
782
|
}
|
|
755
783
|
}
|
|
756
784
|
|
|
785
|
+
/**
|
|
786
|
+
* @param {ioBroker.Message} obj
|
|
787
|
+
* @param {Record<string, unknown>} response
|
|
788
|
+
*/
|
|
789
|
+
replyToMessage(obj, response) {
|
|
790
|
+
if (!obj?.callback || !obj.from || !obj.command) {
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
this.sendTo(obj.from, obj.command, response, obj.callback);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* @param {ioBroker.Message} obj
|
|
799
|
+
*/
|
|
800
|
+
async onMessage(obj) {
|
|
801
|
+
if (!obj || typeof obj.command !== 'string') {
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
if (obj.command === 'loadCameras') {
|
|
806
|
+
await this.handleLoadCameras(obj);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* @param {ioBroker.Message} obj
|
|
812
|
+
*/
|
|
813
|
+
async handleLoadCameras(obj) {
|
|
814
|
+
const payload = parseLoadCamerasMessage(obj.message);
|
|
815
|
+
|
|
816
|
+
const motionHost = String(payload.motionHost || this.config.motionHost || '').trim();
|
|
817
|
+
const motionEyePort = Number(payload.motionEyePort ?? this.config.motionEyePort) || 8765;
|
|
818
|
+
const motionEyeUser = String(payload.motionEyeUser ?? this.config.motionEyeUser ?? 'admin');
|
|
819
|
+
const motionEyePassword = String(this.config.motionEyePassword ?? '');
|
|
820
|
+
const requestTimeoutMs = Number(payload.requestTimeoutMs ?? this.config.requestTimeoutMs) || 45000;
|
|
821
|
+
const defaultMode = String(payload.defaultMode || this.config.defaultMode || 'off');
|
|
822
|
+
const existingCameras = Array.isArray(payload.cameras) ? payload.cameras : this.config.cameras || [];
|
|
823
|
+
|
|
824
|
+
try {
|
|
825
|
+
if (!motionHost) {
|
|
826
|
+
throw new Error('MotionEye host is required');
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
const api = createMotionEyeApi({
|
|
830
|
+
host: motionHost,
|
|
831
|
+
motionEyePort,
|
|
832
|
+
username: motionEyeUser,
|
|
833
|
+
password: motionEyePassword,
|
|
834
|
+
requestTimeoutMs,
|
|
835
|
+
listCacheMs: 0,
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
const motionEyeList = await api.getCameraList();
|
|
839
|
+
const { cameras, added } = mergeMotionEyeCameras(existingCameras, motionEyeList, defaultMode);
|
|
840
|
+
|
|
841
|
+
this.log.info(`Loaded ${motionEyeList.length} camera(s) from MotionEye, added ${added} new row(s)`);
|
|
842
|
+
|
|
843
|
+
this.replyToMessage(obj, {
|
|
844
|
+
native: { cameras },
|
|
845
|
+
result: added > 0 ? 'added' : 'none',
|
|
846
|
+
});
|
|
847
|
+
} catch (error) {
|
|
848
|
+
this.log.error(`loadCameras failed: ${error.message}`);
|
|
849
|
+
this.replyToMessage(obj, { error: error.message });
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
757
853
|
/**
|
|
758
854
|
* @param {() => void} callback
|
|
759
855
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iobroker.motioneye",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Connect MotionEye cameras to ioBroker for motion detection, snapshots, and live streams",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "skvarel",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"node": ">= 22"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@iobroker/adapter-core": "^3.
|
|
32
|
+
"@iobroker/adapter-core": "^3.4.1"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@alcalzone/release-script": "^5.2.1",
|
|
@@ -74,6 +74,6 @@
|
|
|
74
74
|
},
|
|
75
75
|
"readmeFilename": "README.md",
|
|
76
76
|
"overrides": {
|
|
77
|
-
"@iobroker/adapter-core": "^3.
|
|
77
|
+
"@iobroker/adapter-core": "^3.4.1"
|
|
78
78
|
}
|
|
79
79
|
}
|