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 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
- - `info.connection` — instance shows when MotionEye is unreachable
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.info.*`)
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
- | `info.connection` | boolean | MotionEye reachable |
56
- | `info.camerasOnline` | number | Enabled cameras found in MotionEye |
57
- | `info.lastSync` | text | Last status poll timestamp |
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
- "common": {
3
- "name": "motioneye",
4
- "version": "0.0.1",
5
- "news": {
6
- "0.0.1": {
7
- "en": "Initial development release adapter scaffold and MotionEye API client",
8
- "de": "Erste Entwicklungsversion Adapter-Grundgerüst und MotionEye-API-Client",
9
- "ru": "Первоначальный выпуск разработки каркас адаптера и клиент MotionEye API",
10
- "pt": "Lançamento inicial de desenvolvimento — estrutura do adaptador e cliente MotionEye API",
11
- "nl": "Eerste ontwikkelingsrelease adapter scaffold en MotionEye API-client",
12
- "fr": "Première version de développement — structure d'adaptateur et client API MotionEye",
13
- "it": "Rilascio iniziale di sviluppo scaffold dell'adattatore e client API MotionEye",
14
- "es": "Lanzamiento inicial de desarrollo andamiaje del adaptador y cliente API MotionEye",
15
- "pl": "Początkowe wydanie deweloperskie szkielet adaptera i klient API MotionEye",
16
- "uk": "Початковий випуск розробки каркас адаптера та клієнт MotionEye API",
17
- "zh-cn": "初始开发版本 适配器脚手架和 MotionEye API 客户端"
18
- }
19
- },
20
- "titleLang": {
21
- "en": "MotionEye",
22
- "de": "MotionEye",
23
- "ru": "MotionEye",
24
- "pt": "MotionEye",
25
- "nl": "MotionEye",
26
- "fr": "MotionEye",
27
- "it": "MotionEye",
28
- "es": "MotionEye",
29
- "pl": "MotionEye",
30
- "uk": "MotionEye",
31
- "zh-cn": "MotionEye"
32
- },
33
- "desc": {
34
- "en": "Connect MotionEye cameras to ioBroker for motion detection, snapshots, and live streams",
35
- "de": "MotionEye-Kameras mit ioBroker verbinden für Bewegungserkennung, Snapshots und Livestreams",
36
- "ru": "Подключите камеры MotionEye к ioBroker для обнаружения движения, снимков и прямых трансляций",
37
- "pt": "Conecte câmeras MotionEye ao ioBroker para detecção de movimento, snapshots e transmissões ao vivo",
38
- "nl": "Verbind MotionEye-camera's met ioBroker voor bewegingsdetectie, snapshots en livestreams",
39
- "fr": "Connectez les caméras MotionEye à ioBroker pour la détection de mouvement, les instantanés et les flux en direct",
40
- "it": "Collega le telecamere MotionEye a ioBroker per il rilevamento del movimento, gli snapshot e gli streaming live",
41
- "es": "Conecte cámaras MotionEye a ioBroker para detección de movimiento, instantáneas y transmisiones en vivo",
42
- "pl": "Połącz kamery MotionEye z ioBroker w celu wykrywania ruchu, migawek i transmisji na żywo",
43
- "uk": "Підключіть камери MotionEye до ioBroker для виявлення руху, знімків і прямих трансляцій",
44
- "zh-cn": "MotionEye 摄像头连接到 ioBroker,实现运动检测、快照和直播"
45
- },
46
- "authors": [
47
- "skvarel <skvarel@inventwo.com>"
48
- ],
49
- "keywords": [
50
- "motioneye",
51
- "motion",
52
- "camera",
53
- "surveillance"
54
- ],
55
- "licenseInformation": {
56
- "type": "free",
57
- "license": "MIT"
58
- },
59
- "platform": "Javascript/Node.js",
60
- "icon": "motioneye.svg",
61
- "enabled": true,
62
- "extIcon": "https://raw.githubusercontent.com/inventwo/ioBroker.motioneye/main/admin/motioneye.svg",
63
- "readme": "https://github.com/inventwo/ioBroker.motioneye/blob/main/README.md",
64
- "loglevel": "info",
65
- "tier": 3,
66
- "mode": "daemon",
67
- "type": "hardware",
68
- "compact": true,
69
- "connectionType": "local",
70
- "dataSource": "poll",
71
- "adminUI": {
72
- "config": "json"
73
- },
74
- "dependencies": [
75
- {
76
- "js-controller": ">=6.0.11"
77
- }
78
- ],
79
- "globalDependencies": [
80
- {
81
- "admin": ">=7.6.20"
82
- }
83
- ]
84
- },
85
- "native": {
86
- "motionHost": "",
87
- "motionPort": 7999,
88
- "motionEyePort": 8765,
89
- "motionEyeUser": "admin",
90
- "motionEyePassword": "",
91
- "useMotionEyeConfig": true,
92
- "webhookPort": 8090,
93
- "webhookBind": "0.0.0.0",
94
- "webhookHost": "",
95
- "motionResetMs": 15000,
96
- "statusPollIntervalSec": 300,
97
- "requestTimeoutMs": 10000,
98
- "disableStreamOnStart": true,
99
- "applyMediaSettingsOnStart": true,
100
- "streamAutoOffMs": 120000,
101
- "streamStartDelayMs": 3000,
102
- "streamReadyTimeoutMs": 45000,
103
- "streamRetryMs": 2000,
104
- "streamSiblingRelinkTimeoutMs": 60000,
105
- "defaultMode": "off",
106
- "cameras": []
107
- },
108
- "objects": [],
109
- "instanceObjects": [
110
- {
111
- "_id": "",
112
- "type": "meta",
113
- "common": {
114
- "name": {
115
- "en": "MotionEye cameras and adapter states",
116
- "de": "MotionEye-Kameras und Adapter-Zustände",
117
- "ru": "Камеры MotionEye и состояния адаптера",
118
- "pt": "Câmeras MotionEye e estados do adaptador",
119
- "nl": "MotionEye-camera's en adapterstatussen",
120
- "fr": "Caméras MotionEye et états de l'adaptateur",
121
- "it": "Telecamere MotionEye e stati dell'adattatore",
122
- "es": "Cámaras MotionEye y estados del adaptador",
123
- "pl": "Kamery MotionEye i stany adaptera",
124
- "uk": "Камери MotionEye та стани адаптера",
125
- "zh-cn": "MotionEye 摄像头和适配器状态"
126
- },
127
- "type": "meta.folder"
128
- },
129
- "native": {}
130
- },
131
- {
132
- "_id": "info",
133
- "type": "meta",
134
- "common": {
135
- "name": {
136
- "en": "MotionEye adapter information",
137
- "de": "MotionEye-Adapter-Informationen",
138
- "ru": "Информация об адаптере MotionEye",
139
- "pt": "Informações do adaptador MotionEye",
140
- "nl": "MotionEye-adapterinformatie",
141
- "fr": "Informations sur l'adaptateur MotionEye",
142
- "it": "Informazioni sull'adattatore MotionEye",
143
- "es": "Información del adaptador MotionEye",
144
- "pl": "Informacje o adapterze MotionEye",
145
- "uk": "Інформація про адаптер MotionEye",
146
- "zh-cn": "MotionEye 适配器信息"
147
- },
148
- "type": "meta.folder"
149
- },
150
- "native": {}
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
+ };
@@ -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
  };
@@ -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
- await this.setObjectNotExistsAsync('info.connection', {
206
- type: 'state',
207
- common: {
208
- name: 'MotionEye reachable',
209
- type: 'boolean',
210
- role: 'indicator.connected',
211
- read: true,
212
- write: false,
213
- def: false,
214
- },
215
- native: {},
216
- });
217
- await this.setObjectNotExistsAsync('info.camerasOnline', {
218
- type: 'state',
219
- common: {
220
- name: 'Cameras online',
221
- type: 'number',
222
- role: 'value',
223
- read: true,
224
- write: false,
225
- def: 0,
226
- },
227
- native: {},
228
- });
229
- await this.setObjectNotExistsAsync('info.lastSync', {
230
- type: 'state',
231
- common: {
232
- name: 'Last MotionEye sync',
233
- type: 'string',
234
- role: 'text',
235
- read: true,
236
- write: false,
237
- def: '',
238
- },
239
- native: {},
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('info.connection', connected, true);
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('info.connection', true, true);
580
+ await this.setStateAsync(`${INFO_PREFIX}.connection`, true, true);
541
581
  } catch (error) {
542
- await this.setStateAsync('info.connection', false, true);
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('info.camerasOnline', online, true);
580
- await this.setStateAsync('info.lastSync', new Date().toISOString(), true);
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
- "name": "iobroker.motioneye",
3
- "version": "0.0.1",
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
- }
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
  }