iobroker.motioneye 0.0.1 → 0.1.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/README.md +11 -5
- package/io-package.json +166 -153
- package/lib/infoLabels.js +74 -0
- package/lib/motionEyeApi.js +76 -1
- package/lib/motionEyeApi.test.js +26 -1
- package/main.js +82 -41
- package/package.json +77 -77
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ Connect MotionEye cameras to ioBroker for motion detection, snapshots, and live
|
|
|
27
27
|
- Dynamic channels under `motioneye.0.<Name>.*`
|
|
28
28
|
- Built-in webhook server — no simple-api dependency
|
|
29
29
|
- MotionEye Config API sync for modes and webhook URLs
|
|
30
|
-
- `
|
|
30
|
+
- `_info.connection` — instance shows when MotionEye is unreachable
|
|
31
31
|
- Stream sibling relink after VIS re-render (multi-camera dashboards)
|
|
32
32
|
|
|
33
33
|
## Data Points
|
|
@@ -48,13 +48,17 @@ Connect MotionEye cameras to ioBroker for motion detection, snapshots, and live
|
|
|
48
48
|
| `motionEyeId` | value | yes | no | MotionEye camera ID |
|
|
49
49
|
| `motionEyeName` | text | yes | no | Original name in MotionEye |
|
|
50
50
|
|
|
51
|
-
### Instance (`motioneye.0.
|
|
51
|
+
### Instance (`motioneye.0._info.*`)
|
|
52
|
+
|
|
53
|
+
The `_info` folder sorts above camera channels in the object tree.
|
|
52
54
|
|
|
53
55
|
| State | Type | Description |
|
|
54
56
|
|-------|------|-------------|
|
|
55
|
-
| `
|
|
56
|
-
| `
|
|
57
|
-
| `
|
|
57
|
+
| `_info.connection` | boolean | MotionEye reachable |
|
|
58
|
+
| `_info.camerasOnline` | number | Enabled cameras found in MotionEye |
|
|
59
|
+
| `_info.lastSync` | text | Last status poll timestamp |
|
|
60
|
+
| `_info.motionEyeVersion` | text | MotionEye server version |
|
|
61
|
+
| `_info.motionVersion` | text | Motion daemon version |
|
|
58
62
|
|
|
59
63
|
## Installation
|
|
60
64
|
|
|
@@ -99,6 +103,8 @@ If you like our work and would like to support us, we appreciate any donation.
|
|
|
99
103
|
<!--
|
|
100
104
|
### **WORK IN PROGRESS**
|
|
101
105
|
-->
|
|
106
|
+
### 0.1.0 (2026-06-21)
|
|
107
|
+
- (skvarel) Added states for motionEyeVersion and motionVersion
|
|
102
108
|
|
|
103
109
|
### 0.0.1 (2026-06-21)
|
|
104
110
|
- (skvarel) Initial development release
|
package/io-package.json
CHANGED
|
@@ -1,153 +1,166 @@
|
|
|
1
|
-
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
1
|
+
{
|
|
2
|
+
"common": {
|
|
3
|
+
"name": "motioneye",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"news": {
|
|
6
|
+
"0.1.0": {
|
|
7
|
+
"en": "Added states for motionEyeVersion and motionVersion",
|
|
8
|
+
"de": "Zustände für motionEyeVersion und motionVersion hinzugefügt.",
|
|
9
|
+
"ru": "Добавлены состояния для motionEyeVersion и motionVersion.",
|
|
10
|
+
"pt": "Adicionados estados para motionEyeVersion e motionVersion.",
|
|
11
|
+
"nl": "Statuswaarden toegevoegd voor motionEyeVersion en motionVersion",
|
|
12
|
+
"fr": "Ajout d'états pour motionEyeVersion et motionVersion",
|
|
13
|
+
"it": "Aggiunti gli stati per motionEyeVersion e motionVersion",
|
|
14
|
+
"es": "Se agregaron estados para motionEyeVersion y motionVersion.",
|
|
15
|
+
"pl": "Dodano stany dla motionEyeVersion i motionVersion",
|
|
16
|
+
"uk": "Додано стани для motionEyeVersion та motionVersion",
|
|
17
|
+
"zh-cn": "为 motionEyeVersion 和 motionVersion 添加了状态"
|
|
18
|
+
},
|
|
19
|
+
"0.0.1": {
|
|
20
|
+
"en": "Initial development release — adapter scaffold and MotionEye API client",
|
|
21
|
+
"de": "Erste Entwicklungsversion — Adapter-Grundgerüst und MotionEye-API-Client",
|
|
22
|
+
"ru": "Первоначальный выпуск разработки — каркас адаптера и клиент MotionEye API",
|
|
23
|
+
"pt": "Lançamento inicial de desenvolvimento — estrutura do adaptador e cliente MotionEye API",
|
|
24
|
+
"nl": "Eerste ontwikkelingsrelease — adapter scaffold en MotionEye API-client",
|
|
25
|
+
"fr": "Première version de développement — structure d'adaptateur et client API MotionEye",
|
|
26
|
+
"it": "Rilascio iniziale di sviluppo — scaffold dell'adattatore e client API MotionEye",
|
|
27
|
+
"es": "Lanzamiento inicial de desarrollo — andamiaje del adaptador y cliente API MotionEye",
|
|
28
|
+
"pl": "Początkowe wydanie deweloperskie — szkielet adaptera i klient API MotionEye",
|
|
29
|
+
"uk": "Початковий випуск розробки — каркас адаптера та клієнт MotionEye API",
|
|
30
|
+
"zh-cn": "初始开发版本 — 适配器脚手架和 MotionEye API 客户端"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"titleLang": {
|
|
34
|
+
"en": "MotionEye",
|
|
35
|
+
"de": "MotionEye",
|
|
36
|
+
"ru": "MotionEye",
|
|
37
|
+
"pt": "MotionEye",
|
|
38
|
+
"nl": "MotionEye",
|
|
39
|
+
"fr": "MotionEye",
|
|
40
|
+
"it": "MotionEye",
|
|
41
|
+
"es": "MotionEye",
|
|
42
|
+
"pl": "MotionEye",
|
|
43
|
+
"uk": "MotionEye",
|
|
44
|
+
"zh-cn": "MotionEye"
|
|
45
|
+
},
|
|
46
|
+
"desc": {
|
|
47
|
+
"en": "Connect MotionEye cameras to ioBroker for motion detection, snapshots, and live streams",
|
|
48
|
+
"de": "MotionEye-Kameras mit ioBroker verbinden für Bewegungserkennung, Snapshots und Livestreams",
|
|
49
|
+
"ru": "Подключите камеры MotionEye к ioBroker для обнаружения движения, снимков и прямых трансляций",
|
|
50
|
+
"pt": "Conecte câmeras MotionEye ao ioBroker para detecção de movimento, snapshots e transmissões ao vivo",
|
|
51
|
+
"nl": "Verbind MotionEye-camera's met ioBroker voor bewegingsdetectie, snapshots en livestreams",
|
|
52
|
+
"fr": "Connectez les caméras MotionEye à ioBroker pour la détection de mouvement, les instantanés et les flux en direct",
|
|
53
|
+
"it": "Collega le telecamere MotionEye a ioBroker per il rilevamento del movimento, gli snapshot e gli streaming live",
|
|
54
|
+
"es": "Conecte cámaras MotionEye a ioBroker para detección de movimiento, instantáneas y transmisiones en vivo",
|
|
55
|
+
"pl": "Połącz kamery MotionEye z ioBroker w celu wykrywania ruchu, migawek i transmisji na żywo",
|
|
56
|
+
"uk": "Підключіть камери MotionEye до ioBroker для виявлення руху, знімків і прямих трансляцій",
|
|
57
|
+
"zh-cn": "将 MotionEye 摄像头连接到 ioBroker,实现运动检测、快照和直播"
|
|
58
|
+
},
|
|
59
|
+
"authors": [
|
|
60
|
+
"skvarel <skvarel@inventwo.com>"
|
|
61
|
+
],
|
|
62
|
+
"keywords": [
|
|
63
|
+
"motioneye",
|
|
64
|
+
"motion",
|
|
65
|
+
"camera",
|
|
66
|
+
"surveillance"
|
|
67
|
+
],
|
|
68
|
+
"licenseInformation": {
|
|
69
|
+
"type": "free",
|
|
70
|
+
"license": "MIT"
|
|
71
|
+
},
|
|
72
|
+
"platform": "Javascript/Node.js",
|
|
73
|
+
"icon": "motioneye.svg",
|
|
74
|
+
"enabled": true,
|
|
75
|
+
"extIcon": "https://raw.githubusercontent.com/inventwo/ioBroker.motioneye/main/admin/motioneye.svg",
|
|
76
|
+
"readme": "https://github.com/inventwo/ioBroker.motioneye/blob/main/README.md",
|
|
77
|
+
"loglevel": "info",
|
|
78
|
+
"tier": 3,
|
|
79
|
+
"mode": "daemon",
|
|
80
|
+
"type": "hardware",
|
|
81
|
+
"compact": true,
|
|
82
|
+
"connectionType": "local",
|
|
83
|
+
"dataSource": "poll",
|
|
84
|
+
"adminUI": {
|
|
85
|
+
"config": "json"
|
|
86
|
+
},
|
|
87
|
+
"dependencies": [
|
|
88
|
+
{
|
|
89
|
+
"js-controller": ">=6.0.11"
|
|
90
|
+
}
|
|
91
|
+
],
|
|
92
|
+
"globalDependencies": [
|
|
93
|
+
{
|
|
94
|
+
"admin": ">=7.6.20"
|
|
95
|
+
}
|
|
96
|
+
]
|
|
97
|
+
},
|
|
98
|
+
"native": {
|
|
99
|
+
"motionHost": "",
|
|
100
|
+
"motionPort": 7999,
|
|
101
|
+
"motionEyePort": 8765,
|
|
102
|
+
"motionEyeUser": "admin",
|
|
103
|
+
"motionEyePassword": "",
|
|
104
|
+
"useMotionEyeConfig": true,
|
|
105
|
+
"webhookPort": 8090,
|
|
106
|
+
"webhookBind": "0.0.0.0",
|
|
107
|
+
"webhookHost": "",
|
|
108
|
+
"motionResetMs": 15000,
|
|
109
|
+
"statusPollIntervalSec": 300,
|
|
110
|
+
"requestTimeoutMs": 10000,
|
|
111
|
+
"disableStreamOnStart": true,
|
|
112
|
+
"applyMediaSettingsOnStart": true,
|
|
113
|
+
"streamAutoOffMs": 120000,
|
|
114
|
+
"streamStartDelayMs": 3000,
|
|
115
|
+
"streamReadyTimeoutMs": 45000,
|
|
116
|
+
"streamRetryMs": 2000,
|
|
117
|
+
"streamSiblingRelinkTimeoutMs": 60000,
|
|
118
|
+
"defaultMode": "off",
|
|
119
|
+
"cameras": []
|
|
120
|
+
},
|
|
121
|
+
"objects": [],
|
|
122
|
+
"instanceObjects": [
|
|
123
|
+
{
|
|
124
|
+
"_id": "",
|
|
125
|
+
"type": "meta",
|
|
126
|
+
"common": {
|
|
127
|
+
"name": {
|
|
128
|
+
"en": "MotionEye cameras and adapter states",
|
|
129
|
+
"de": "MotionEye-Kameras und Adapter-Zustände",
|
|
130
|
+
"ru": "Камеры MotionEye и состояния адаптера",
|
|
131
|
+
"pt": "Câmeras MotionEye e estados do adaptador",
|
|
132
|
+
"nl": "MotionEye-camera's en adapterstatussen",
|
|
133
|
+
"fr": "Caméras MotionEye et états de l'adaptateur",
|
|
134
|
+
"it": "Telecamere MotionEye e stati dell'adattatore",
|
|
135
|
+
"es": "Cámaras MotionEye y estados del adaptador",
|
|
136
|
+
"pl": "Kamery MotionEye i stany adaptera",
|
|
137
|
+
"uk": "Камери MotionEye та стани адаптера",
|
|
138
|
+
"zh-cn": "MotionEye 摄像头和适配器状态"
|
|
139
|
+
},
|
|
140
|
+
"type": "meta.folder"
|
|
141
|
+
},
|
|
142
|
+
"native": {}
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
"_id": "_info",
|
|
146
|
+
"type": "meta",
|
|
147
|
+
"common": {
|
|
148
|
+
"name": {
|
|
149
|
+
"en": "MotionEye adapter information",
|
|
150
|
+
"de": "MotionEye-Adapter-Informationen",
|
|
151
|
+
"ru": "Информация об адаптере MotionEye",
|
|
152
|
+
"pt": "Informações do adaptador MotionEye",
|
|
153
|
+
"nl": "MotionEye-adapterinformatie",
|
|
154
|
+
"fr": "Informations sur l'adaptateur MotionEye",
|
|
155
|
+
"it": "Informazioni sull'adattatore MotionEye",
|
|
156
|
+
"es": "Información del adaptador MotionEye",
|
|
157
|
+
"pl": "Informacje o adapterze MotionEye",
|
|
158
|
+
"uk": "Інформація про адаптер MotionEye",
|
|
159
|
+
"zh-cn": "MotionEye 适配器信息"
|
|
160
|
+
},
|
|
161
|
+
"type": "meta.folder"
|
|
162
|
+
},
|
|
163
|
+
"native": {}
|
|
164
|
+
}
|
|
165
|
+
]
|
|
166
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/** @type {Record<string, Record<string, string>>} */
|
|
4
|
+
const INFO_STATE_LABELS = {
|
|
5
|
+
connection: {
|
|
6
|
+
en: 'MotionEye reachable',
|
|
7
|
+
de: 'MotionEye erreichbar',
|
|
8
|
+
ru: 'MotionEye доступен',
|
|
9
|
+
pt: 'MotionEye acessível',
|
|
10
|
+
nl: 'MotionEye bereikbaar',
|
|
11
|
+
fr: 'MotionEye accessible',
|
|
12
|
+
it: 'MotionEye raggiungibile',
|
|
13
|
+
es: 'MotionEye accesible',
|
|
14
|
+
pl: 'MotionEye osiągalny',
|
|
15
|
+
uk: 'MotionEye доступний',
|
|
16
|
+
'zh-cn': 'MotionEye 可访问',
|
|
17
|
+
},
|
|
18
|
+
camerasOnline: {
|
|
19
|
+
en: 'Cameras online',
|
|
20
|
+
de: 'Kameras online',
|
|
21
|
+
ru: 'Камер онлайн',
|
|
22
|
+
pt: 'Câmeras online',
|
|
23
|
+
nl: "Camera's online",
|
|
24
|
+
fr: 'Caméras en ligne',
|
|
25
|
+
it: 'Telecamere online',
|
|
26
|
+
es: 'Cámaras en línea',
|
|
27
|
+
pl: 'Kamery online',
|
|
28
|
+
uk: 'Камер онлайн',
|
|
29
|
+
'zh-cn': '在线摄像头数',
|
|
30
|
+
},
|
|
31
|
+
lastSync: {
|
|
32
|
+
en: 'Last MotionEye sync',
|
|
33
|
+
de: 'Letzte MotionEye-Synchronisation',
|
|
34
|
+
ru: 'Последняя синхронизация MotionEye',
|
|
35
|
+
pt: 'Última sincronização MotionEye',
|
|
36
|
+
nl: 'Laatste MotionEye-synchronisatie',
|
|
37
|
+
fr: 'Dernière synchronisation MotionEye',
|
|
38
|
+
it: 'Ultima sincronizzazione MotionEye',
|
|
39
|
+
es: 'Última sincronización MotionEye',
|
|
40
|
+
pl: 'Ostatnia synchronizacja MotionEye',
|
|
41
|
+
uk: 'Остання синхронізація MotionEye',
|
|
42
|
+
'zh-cn': '上次 MotionEye 同步',
|
|
43
|
+
},
|
|
44
|
+
motionEyeVersion: {
|
|
45
|
+
en: 'MotionEye version',
|
|
46
|
+
de: 'MotionEye-Version',
|
|
47
|
+
ru: 'Версия MotionEye',
|
|
48
|
+
pt: 'Versão MotionEye',
|
|
49
|
+
nl: 'MotionEye-versie',
|
|
50
|
+
fr: 'Version MotionEye',
|
|
51
|
+
it: 'Versione MotionEye',
|
|
52
|
+
es: 'Versión MotionEye',
|
|
53
|
+
pl: 'Wersja MotionEye',
|
|
54
|
+
uk: 'Версія MotionEye',
|
|
55
|
+
'zh-cn': 'MotionEye 版本',
|
|
56
|
+
},
|
|
57
|
+
motionVersion: {
|
|
58
|
+
en: 'Motion daemon version',
|
|
59
|
+
de: 'Motion-Daemon-Version',
|
|
60
|
+
ru: 'Версия демона Motion',
|
|
61
|
+
pt: 'Versão do daemon Motion',
|
|
62
|
+
nl: 'Motion-daemonversie',
|
|
63
|
+
fr: 'Version du démon Motion',
|
|
64
|
+
it: 'Versione demone Motion',
|
|
65
|
+
es: 'Versión del demonio Motion',
|
|
66
|
+
pl: 'Wersja demona Motion',
|
|
67
|
+
uk: 'Версія демона Motion',
|
|
68
|
+
'zh-cn': 'Motion 守护进程版本',
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
module.exports = {
|
|
73
|
+
INFO_STATE_LABELS,
|
|
74
|
+
};
|
package/lib/motionEyeApi.js
CHANGED
|
@@ -8,6 +8,54 @@ const SIGNATURE_REGEX = new RegExp('[^a-zA-Z0-9/?_.=&{}\\[\\]":, -]', 'g');
|
|
|
8
8
|
/** Default cache TTL for camera list (ms). */
|
|
9
9
|
const LIST_CACHE_MS = 15000;
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* @param {string|undefined} serverHeader
|
|
13
|
+
* @returns {string}
|
|
14
|
+
*/
|
|
15
|
+
function parseMotionEyeServerHeader(serverHeader) {
|
|
16
|
+
if (!serverHeader) {
|
|
17
|
+
return '';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const match = String(serverHeader).match(/^motionEye\/(.+)$/i);
|
|
21
|
+
return match ? match[1].trim() : '';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Parse MotionEye /version HTML body.
|
|
26
|
+
*
|
|
27
|
+
* @param {string} html
|
|
28
|
+
* @returns {{ motionEyeVersion: string, motionVersion: string, hostname: string, osVersion: string }}
|
|
29
|
+
*/
|
|
30
|
+
function parseVersionPage(html) {
|
|
31
|
+
const result = {
|
|
32
|
+
motionEyeVersion: '',
|
|
33
|
+
motionVersion: '',
|
|
34
|
+
hostname: '',
|
|
35
|
+
osVersion: '',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
if (!html) {
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const patterns = {
|
|
43
|
+
motionEyeVersion: /version\s*=\s*"([^"]*)"/,
|
|
44
|
+
motionVersion: /motion_version\s*=\s*"([^"]*)"/,
|
|
45
|
+
hostname: /hostname\s*=\s*"([^"]*)"/,
|
|
46
|
+
osVersion: /os_version\s*=\s*"([^"]*)"/,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
for (const [key, pattern] of Object.entries(patterns)) {
|
|
50
|
+
const match = html.match(pattern);
|
|
51
|
+
if (match) {
|
|
52
|
+
result[key] = match[1];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
|
|
11
59
|
/**
|
|
12
60
|
* @param {string} value
|
|
13
61
|
* @returns {string}
|
|
@@ -120,6 +168,7 @@ function createMotionEyeApi(options) {
|
|
|
120
168
|
const signKey = motionEyeSignKey(password);
|
|
121
169
|
|
|
122
170
|
let listCache = null;
|
|
171
|
+
let lastMotionEyeVersion = '';
|
|
123
172
|
|
|
124
173
|
/**
|
|
125
174
|
* @param {string} path
|
|
@@ -155,6 +204,7 @@ function createMotionEyeApi(options) {
|
|
|
155
204
|
resolve({
|
|
156
205
|
status: res.statusCode || 0,
|
|
157
206
|
body: responseBody.trim(),
|
|
207
|
+
headers: res.headers,
|
|
158
208
|
});
|
|
159
209
|
});
|
|
160
210
|
});
|
|
@@ -203,7 +253,7 @@ function createMotionEyeApi(options) {
|
|
|
203
253
|
throw new Error(String(message));
|
|
204
254
|
}
|
|
205
255
|
|
|
206
|
-
return { status: result.status, data, body: result.body };
|
|
256
|
+
return { status: result.status, data, body: result.body, headers: result.headers };
|
|
207
257
|
}
|
|
208
258
|
|
|
209
259
|
/**
|
|
@@ -216,6 +266,8 @@ function createMotionEyeApi(options) {
|
|
|
216
266
|
}
|
|
217
267
|
|
|
218
268
|
const result = await call('/config/list', 'GET');
|
|
269
|
+
lastMotionEyeVersion = parseMotionEyeServerHeader(result.headers?.server);
|
|
270
|
+
|
|
219
271
|
if (!result.data || typeof result.data !== 'object' || result.data === null || !('cameras' in result.data)) {
|
|
220
272
|
throw new Error('config/list: no cameras found');
|
|
221
273
|
}
|
|
@@ -263,11 +315,32 @@ function createMotionEyeApi(options) {
|
|
|
263
315
|
return { changed: true, data: result.data };
|
|
264
316
|
}
|
|
265
317
|
|
|
318
|
+
/**
|
|
319
|
+
* @returns {Promise<{ motionEyeVersion: string, motionVersion: string, hostname: string, osVersion: string }>}
|
|
320
|
+
*/
|
|
321
|
+
async function getServerVersions() {
|
|
322
|
+
const authPath = buildAuthPath('/version', 'GET', null, username, signKey);
|
|
323
|
+
const result = await httpRequest(authPath, 'GET');
|
|
324
|
+
const fromPage = parseVersionPage(result.body);
|
|
325
|
+
const fromHeader = parseMotionEyeServerHeader(result.headers?.server);
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
motionEyeVersion: fromPage.motionEyeVersion || fromHeader || lastMotionEyeVersion,
|
|
329
|
+
motionVersion: fromPage.motionVersion,
|
|
330
|
+
hostname: fromPage.hostname,
|
|
331
|
+
osVersion: fromPage.osVersion,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
266
335
|
return {
|
|
267
336
|
call,
|
|
268
337
|
getCameraList,
|
|
269
338
|
getCameraConfig,
|
|
270
339
|
saveCameraConfig,
|
|
340
|
+
getServerVersions,
|
|
341
|
+
getLastMotionEyeVersion() {
|
|
342
|
+
return lastMotionEyeVersion;
|
|
343
|
+
},
|
|
271
344
|
invalidateCache() {
|
|
272
345
|
listCache = null;
|
|
273
346
|
},
|
|
@@ -280,6 +353,8 @@ module.exports = {
|
|
|
280
353
|
computeSignature,
|
|
281
354
|
motionEyeSignKey,
|
|
282
355
|
buildAuthPath,
|
|
356
|
+
parseMotionEyeServerHeader,
|
|
357
|
+
parseVersionPage,
|
|
283
358
|
createMotionEyeApi,
|
|
284
359
|
LIST_CACHE_MS,
|
|
285
360
|
};
|
package/lib/motionEyeApi.test.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const crypto = require('node:crypto');
|
|
4
4
|
const { expect } = require('chai');
|
|
5
|
-
const { quoteParam, computeSignature, motionEyeSignKey, buildAuthPath } = require('./motionEyeApi');
|
|
5
|
+
const { quoteParam, computeSignature, motionEyeSignKey, buildAuthPath, parseMotionEyeServerHeader, parseVersionPage } = require('./motionEyeApi');
|
|
6
6
|
|
|
7
7
|
describe('motionEyeApi signature', () => {
|
|
8
8
|
it('quoteParam should encode special characters', () => {
|
|
@@ -60,3 +60,28 @@ describe('motionEyeApi signature', () => {
|
|
|
60
60
|
expect(authPath).to.include('/config/list?foo=bar&_username=admin&_signature=');
|
|
61
61
|
});
|
|
62
62
|
});
|
|
63
|
+
|
|
64
|
+
describe('motionEyeApi version parsing', () => {
|
|
65
|
+
it('parseMotionEyeServerHeader should extract version from Server header', () => {
|
|
66
|
+
expect(parseMotionEyeServerHeader('motionEye/0.44.0')).to.equal('0.44.0');
|
|
67
|
+
expect(parseMotionEyeServerHeader('MotionEye/1.2.3')).to.equal('1.2.3');
|
|
68
|
+
expect(parseMotionEyeServerHeader('nginx')).to.equal('');
|
|
69
|
+
expect(parseMotionEyeServerHeader('')).to.equal('');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('parseVersionPage should parse MotionEye /version HTML body', () => {
|
|
73
|
+
const html = [
|
|
74
|
+
'hostname = "motioneye-pi"',
|
|
75
|
+
'version = "0.44.0"',
|
|
76
|
+
'motion_version = "4.5.1"',
|
|
77
|
+
'os_version = "Raspbian GNU/Linux 12"',
|
|
78
|
+
].join('\n');
|
|
79
|
+
|
|
80
|
+
expect(parseVersionPage(html)).to.deep.equal({
|
|
81
|
+
motionEyeVersion: '0.44.0',
|
|
82
|
+
motionVersion: '4.5.1',
|
|
83
|
+
hostname: 'motioneye-pi',
|
|
84
|
+
osVersion: 'Raspbian GNU/Linux 12',
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
});
|
package/main.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
const utils = require('@iobroker/adapter-core');
|
|
8
8
|
const { createMotionEyeApi } = require('./lib/motionEyeApi');
|
|
9
9
|
const { createMotionApi } = require('./lib/motionApi');
|
|
10
|
+
const { INFO_STATE_LABELS } = require('./lib/infoLabels');
|
|
10
11
|
const { resolveCameras, buildWebhookUrl } = require('./lib/cameraRegistry');
|
|
11
12
|
const {
|
|
12
13
|
normalizeMode,
|
|
@@ -18,6 +19,10 @@ const {
|
|
|
18
19
|
const { createWebhookServer } = require('./lib/webhookServer');
|
|
19
20
|
const { createStreamManager } = require('./lib/streamManager');
|
|
20
21
|
|
|
22
|
+
/** Info states live under `_info` so the folder sorts before camera channels. */
|
|
23
|
+
const INFO_PREFIX = '_info';
|
|
24
|
+
const LEGACY_INFO_STATES = ['connection', 'camerasOnline', 'lastSync', 'motionEyeVersion', 'motionVersion'];
|
|
25
|
+
|
|
21
26
|
class Motioneye extends utils.Adapter {
|
|
22
27
|
/**
|
|
23
28
|
* @param {Partial<utils.AdapterOptions>} [options] - Adapter options
|
|
@@ -43,6 +48,7 @@ class Motioneye extends utils.Adapter {
|
|
|
43
48
|
this.camerasByChannel = new Map();
|
|
44
49
|
this.webhookHost = '';
|
|
45
50
|
this._unloading = false;
|
|
51
|
+
this._serverVersionsFetched = false;
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
/**
|
|
@@ -202,42 +208,75 @@ class Motioneye extends utils.Adapter {
|
|
|
202
208
|
}
|
|
203
209
|
|
|
204
210
|
async ensureInfoStates() {
|
|
205
|
-
|
|
206
|
-
type: '
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
211
|
+
for (const [stateId, name] of Object.entries(INFO_STATE_LABELS)) {
|
|
212
|
+
const type = stateId === 'camerasOnline' ? 'number' : 'string';
|
|
213
|
+
const role =
|
|
214
|
+
stateId === 'connection' ? 'indicator.connected' : stateId === 'camerasOnline' ? 'value' : 'text';
|
|
215
|
+
|
|
216
|
+
await this.setObjectNotExistsAsync(`${INFO_PREFIX}.${stateId}`, {
|
|
217
|
+
type: 'state',
|
|
218
|
+
common: {
|
|
219
|
+
name,
|
|
220
|
+
type: stateId === 'connection' ? 'boolean' : type,
|
|
221
|
+
role,
|
|
222
|
+
read: true,
|
|
223
|
+
write: false,
|
|
224
|
+
def: stateId === 'connection' ? false : stateId === 'camerasOnline' ? 0 : '',
|
|
225
|
+
},
|
|
226
|
+
native: {},
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
await this.migrateLegacyInfoChannel();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async migrateLegacyInfoChannel() {
|
|
234
|
+
for (const stateId of LEGACY_INFO_STATES) {
|
|
235
|
+
const legacyId = `info.${stateId}`;
|
|
236
|
+
const newId = `${INFO_PREFIX}.${stateId}`;
|
|
237
|
+
const legacyObject = await this.getObjectAsync(legacyId);
|
|
238
|
+
if (!legacyObject) {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const legacyState = await this.getStateAsync(legacyId);
|
|
243
|
+
if (legacyState) {
|
|
244
|
+
await this.setStateAsync(newId, legacyState.val, true);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
await this.delObjectAsync(legacyId);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const legacyFolder = await this.getObjectAsync('info');
|
|
251
|
+
if (legacyFolder) {
|
|
252
|
+
await this.delObjectAsync('info');
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async updateServerVersionStates() {
|
|
257
|
+
if (!this.motionEyeApi) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const motionEyeVersion = this.motionEyeApi.getLastMotionEyeVersion();
|
|
262
|
+
if (motionEyeVersion) {
|
|
263
|
+
await this.setStateAsync(`${INFO_PREFIX}.motionEyeVersion`, motionEyeVersion, true);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (this._serverVersionsFetched) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
const versions = await this.motionEyeApi.getServerVersions();
|
|
272
|
+
if (versions.motionEyeVersion) {
|
|
273
|
+
await this.setStateAsync(`${INFO_PREFIX}.motionEyeVersion`, versions.motionEyeVersion, true);
|
|
274
|
+
}
|
|
275
|
+
await this.setStateAsync(`${INFO_PREFIX}.motionVersion`, versions.motionVersion || '', true);
|
|
276
|
+
this._serverVersionsFetched = true;
|
|
277
|
+
} catch (error) {
|
|
278
|
+
this.log.debug(`Could not read MotionEye /version page: ${error.message}`);
|
|
279
|
+
}
|
|
241
280
|
}
|
|
242
281
|
|
|
243
282
|
syncCameraRegistry() {
|
|
@@ -442,7 +481,7 @@ class Motioneye extends utils.Adapter {
|
|
|
442
481
|
this.log.warn(`MotionEye not reachable at startup: ${error.message}`);
|
|
443
482
|
}
|
|
444
483
|
|
|
445
|
-
await this.setStateAsync(
|
|
484
|
+
await this.setStateAsync(`${INFO_PREFIX}.connection`, connected, true);
|
|
446
485
|
|
|
447
486
|
for (const camera of this.camerasById.values()) {
|
|
448
487
|
try {
|
|
@@ -455,6 +494,7 @@ class Motioneye extends utils.Adapter {
|
|
|
455
494
|
}
|
|
456
495
|
|
|
457
496
|
if (connected) {
|
|
497
|
+
await this.updateServerVersionStates();
|
|
458
498
|
await this.pollMotionEye();
|
|
459
499
|
}
|
|
460
500
|
}
|
|
@@ -537,9 +577,9 @@ class Motioneye extends utils.Adapter {
|
|
|
537
577
|
let cameras;
|
|
538
578
|
try {
|
|
539
579
|
cameras = await this.motionEyeApi.getCameraList();
|
|
540
|
-
await this.setStateAsync(
|
|
580
|
+
await this.setStateAsync(`${INFO_PREFIX}.connection`, true, true);
|
|
541
581
|
} catch (error) {
|
|
542
|
-
await this.setStateAsync(
|
|
582
|
+
await this.setStateAsync(`${INFO_PREFIX}.connection`, false, true);
|
|
543
583
|
throw error;
|
|
544
584
|
}
|
|
545
585
|
|
|
@@ -576,8 +616,9 @@ class Motioneye extends utils.Adapter {
|
|
|
576
616
|
}
|
|
577
617
|
}
|
|
578
618
|
|
|
579
|
-
await this.setStateAsync(
|
|
580
|
-
await this.setStateAsync(
|
|
619
|
+
await this.setStateAsync(`${INFO_PREFIX}.camerasOnline`, online, true);
|
|
620
|
+
await this.setStateAsync(`${INFO_PREFIX}.lastSync`, new Date().toISOString(), true);
|
|
621
|
+
await this.updateServerVersionStates();
|
|
581
622
|
}
|
|
582
623
|
|
|
583
624
|
async startWebhookServer() {
|
package/package.json
CHANGED
|
@@ -1,79 +1,79 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
2
|
+
"name": "iobroker.motioneye",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Connect MotionEye cameras to ioBroker for motion detection, snapshots, and live streams",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "skvarel",
|
|
7
|
+
"email": "skvarel@inventwo.com"
|
|
8
|
+
},
|
|
9
|
+
"contributors": [
|
|
10
|
+
{
|
|
11
|
+
"name": "skvarel"
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/inventwo/ioBroker.motioneye",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"keywords": [
|
|
17
|
+
"motioneye",
|
|
18
|
+
"motion",
|
|
19
|
+
"camera",
|
|
20
|
+
"surveillance",
|
|
21
|
+
"ioBroker",
|
|
22
|
+
"adapter"
|
|
23
|
+
],
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/inventwo/ioBroker.motioneye.git"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">= 22"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@iobroker/adapter-core": "^3.3.2"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@alcalzone/release-script": "^5.2.1",
|
|
36
|
+
"@alcalzone/release-script-plugin-iobroker": "^5.2.0",
|
|
37
|
+
"@alcalzone/release-script-plugin-license": "^5.2.0",
|
|
38
|
+
"@alcalzone/release-script-plugin-manual-review": "^5.2.0",
|
|
39
|
+
"@iobroker/adapter-dev": "^1.5.0",
|
|
40
|
+
"@iobroker/dev-server": "^0.8.0",
|
|
41
|
+
"@iobroker/eslint-config": "^2.3.4",
|
|
42
|
+
"@iobroker/testing": "^5.2.2",
|
|
43
|
+
"@tsconfig/node22": "^22.0.5",
|
|
44
|
+
"@types/iobroker": "npm:@iobroker/types@^7.1.2",
|
|
45
|
+
"@types/node": "^22.19.19",
|
|
46
|
+
"typescript": "~6.0.3"
|
|
47
|
+
},
|
|
48
|
+
"main": "main.js",
|
|
49
|
+
"files": [
|
|
50
|
+
"admin{,/!(src)/**}/!(tsconfig|tsconfig.*|.eslintrc).{json,json5}",
|
|
51
|
+
"admin{,/!(src)/**}/*.{html,css,png,svg,jpg,js}",
|
|
52
|
+
"lib/",
|
|
53
|
+
"io-package.json",
|
|
54
|
+
"LICENSE",
|
|
55
|
+
"main.js"
|
|
56
|
+
],
|
|
57
|
+
"scripts": {
|
|
58
|
+
"release-patch": "release-script patch --yes",
|
|
59
|
+
"release-minor": "release-script minor --yes",
|
|
60
|
+
"release-major": "release-script major --yes",
|
|
61
|
+
"test:js": "mocha --config test/mocharc.custom.json \"{!(node_modules|test)/**/*.test.js,*.test.js,test/**/test!(PackageFiles|Startup).js}\"",
|
|
62
|
+
"test:package": "mocha test/package --exit",
|
|
63
|
+
"test:integration": "mocha test/integration --exit",
|
|
64
|
+
"test": "npm run test:js && npm run test:package",
|
|
65
|
+
"check": "tsc --noEmit -p tsconfig.check.json",
|
|
66
|
+
"lint": "eslint -c eslint.config.mjs .",
|
|
67
|
+
"lint-fix": "eslint -c eslint.config.mjs . --fix",
|
|
68
|
+
"translate": "translate-adapter",
|
|
69
|
+
"dev-server:start": "dev-server watch",
|
|
70
|
+
"dev-server:stop": "powershell -NonInteractive -Command \"Get-NetTCPConnection -LocalPort 8091,26436,24436,20436 -EA SilentlyContinue | Select-Object -ExpandProperty OwningProcess -Unique | ForEach-Object { Stop-Process -Id $_ -Force -EA SilentlyContinue }; Write-Host 'dev-server stopped'\""
|
|
71
|
+
},
|
|
72
|
+
"bugs": {
|
|
73
|
+
"url": "https://github.com/inventwo/ioBroker.motioneye/issues"
|
|
74
|
+
},
|
|
75
|
+
"readmeFilename": "README.md",
|
|
76
|
+
"overrides": {
|
|
77
|
+
"@iobroker/adapter-core": "^3.3.2"
|
|
78
|
+
}
|
|
79
79
|
}
|