iobroker.smartfriends 1.3.2 → 1.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/io-package.json +14 -14
- package/lib/SchellenbergBridge.js +30 -24
- package/lib/devices/DeviceCapabilityRegistry.js +6 -5
- package/lib/devices/DeviceKindCapabilities.js +13 -9
- package/lib/devices/DeviceManager.js +15 -1
- package/lib/devices/SchellenbergDevice.js +26 -21
- package/lib/devices/SchellenbergMasterDevice.js +5 -5
- package/lib/helpers/DeviceDefinitions.js +100 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -33,6 +33,12 @@ The adapter establishes a direct connection to the gateway to control and query
|
|
|
33
33
|
Placeholder for the next version (at the beginning of the line):
|
|
34
34
|
### __WORK IN PROGRESS__
|
|
35
35
|
-->
|
|
36
|
+
### 1.3.3 (2026-01-18)
|
|
37
|
+
|
|
38
|
+
- (Black-Thunder) Special characters in device definitions (e.g. "<>") are now correctly handled
|
|
39
|
+
- (Black-Thunder) Boolean values in device definitions are now correctly handled
|
|
40
|
+
- (Black-Thunder) Numeric sensor values are no longer rounded
|
|
41
|
+
|
|
36
42
|
### 1.3.2 (2026-01-13)
|
|
37
43
|
|
|
38
44
|
- (Black-Thunder) Creation of adapter objects was fixed
|
package/io-package.json
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "smartfriends",
|
|
4
|
-
"version": "1.3.
|
|
4
|
+
"version": "1.3.3",
|
|
5
5
|
"news": {
|
|
6
|
+
"1.3.3": {
|
|
7
|
+
"en": "Special characters in device definitions (e.g. \"<>\") are now correctly handled\nBoolean values in device definitions are now correctly handled\nNumeric sensor values are no longer rounded",
|
|
8
|
+
"de": "Sonderzeichen in Gerätedefinitionen (z.B. \"<>\") werden nun korrekt verarbeitet\nBoolesche Werte in Gerätedefinitionen werden nun korrekt verarbeitet\nNumerische Sensorwerte werden nicht mehr gerundet",
|
|
9
|
+
"ru": "Специальные символы в определениях устройств (например, \"<>\") теперь правильно обработаны\nБулевы значения в определениях устройств теперь правильно обрабатываются\nЧисленные значения датчиков больше не округляются",
|
|
10
|
+
"pt": "Os caracteres especiais nas definições do dispositivo (por exemplo, \"<>\") são agora correctamente manuseados\nOs valores booleanos nas definições do dispositivo são agora correctamente manuseados\nOs valores dos sensores numéricos já não são arredondados",
|
|
11
|
+
"nl": "Speciale tekens in apparaatdefinities (bijv. \"<>\") worden nu correct behandeld\nBooleaanse waarden in apparaatdefinities worden nu correct behandeld\nNumerieke sensorwaarden zijn niet meer afgerond",
|
|
12
|
+
"fr": "Les caractères spéciaux dans les définitions de l'appareil (p. ex. «<>») sont maintenant correctement traités\nLes valeurs booléennes dans les définitions des appareils sont maintenant correctement gérées\nLes valeurs des capteurs numériques ne sont plus arrondies",
|
|
13
|
+
"it": "I caratteri speciali nelle definizioni dei dispositivi (ad esempio \" <>\") sono ora gestiti correttamente\nI valori booleani nelle definizioni dei dispositivi sono ora gestiti correttamente\nI valori del sensore numerico non sono più arrotondati",
|
|
14
|
+
"es": "Los caracteres especiales en las definiciones de dispositivos (por ejemplo, \"directo\") se manejan correctamente\nLos valores booleanos en las definiciones de dispositivos ahora se manejan correctamente\nLos valores numéricos del sensor ya no están redondeados",
|
|
15
|
+
"pl": "Specjalne znaki w definicjach urządzeń (np. \"< >\") są teraz prawidłowo obsługiwane\nWartości boolean w definicjach urządzeń są teraz prawidłowo obsługiwane\nNumeryczne wartości czujników nie są już zaokrąglone",
|
|
16
|
+
"uk": "Спеціальні символи у визначенні пристрою (наприклад, \"<>\") тепер правильно обробляються\nБололеві значення у визначенні пристроїв тепер правильно обробляються\nЧисельні значення датчика більше не закруглені",
|
|
17
|
+
"zh-cn": "设备定义中的特殊字符( 如“ QQ” ) 现在被正确处理\n设备定义中的布尔值现在被正确处理\n数值不再四舍五入"
|
|
18
|
+
},
|
|
6
19
|
"1.3.2": {
|
|
7
20
|
"en": "Creation of adapter objects was fixed",
|
|
8
21
|
"de": "Erstellung von Adapterobjekten wurde korrigiert",
|
|
@@ -80,19 +93,6 @@
|
|
|
80
93
|
"pl": "Większa odporność podczas komunikacji z bramą\nDodano nową opcję do ignorowania błędów certyfikatu",
|
|
81
94
|
"uk": "Підвищена надійність при спілкуванні з шлюзом\nДодано новий варіант ігнорувати помилки сертифіката",
|
|
82
95
|
"zh-cn": "与网关沟通时的强度提高\n添加新选项以忽略证书错误"
|
|
83
|
-
},
|
|
84
|
-
"1.0.0": {
|
|
85
|
-
"en": "initial release",
|
|
86
|
-
"de": "Erstveröffentlichung",
|
|
87
|
-
"ru": "Начальная версия",
|
|
88
|
-
"pt": "lançamento inicial",
|
|
89
|
-
"nl": "Eerste uitgave",
|
|
90
|
-
"fr": "Première version",
|
|
91
|
-
"it": "Versione iniziale",
|
|
92
|
-
"es": "Versión inicial",
|
|
93
|
-
"pl": "Pierwsze wydanie",
|
|
94
|
-
"zh-cn": "首次出版",
|
|
95
|
-
"uk": "початковий випуск"
|
|
96
96
|
}
|
|
97
97
|
},
|
|
98
98
|
"titleLang": {
|
|
@@ -151,62 +151,68 @@ class SchellenbergBridge {
|
|
|
151
151
|
const createdMasters = [];
|
|
152
152
|
|
|
153
153
|
// 1️⃣ Child-Devices nach MasterID gruppieren
|
|
154
|
-
const
|
|
155
|
-
for (const
|
|
154
|
+
const deviceInfosByMaster = {};
|
|
155
|
+
for (const deviceInfo of deviceInfos) {
|
|
156
156
|
// Definition dynamisch zuweisen
|
|
157
|
-
|
|
158
|
-
|
|
157
|
+
deviceInfo.definition = this.deviceDefinitions.find(
|
|
158
|
+
d => d.deviceDesignation === deviceInfo.deviceDesignation,
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
if (!deviceInfo.definition) {
|
|
159
162
|
this.adapter.log.debug(
|
|
160
|
-
`Skipping device ${
|
|
163
|
+
`Skipping device ${deviceInfo.deviceName} (${deviceInfo.deviceDesignation}) – no definition found`,
|
|
161
164
|
);
|
|
162
165
|
continue;
|
|
163
166
|
}
|
|
164
167
|
|
|
165
|
-
const masterId =
|
|
166
|
-
if (!
|
|
167
|
-
|
|
168
|
+
const masterId = deviceInfo.masterDeviceID || deviceInfo.deviceID;
|
|
169
|
+
if (!deviceInfosByMaster[masterId]) {
|
|
170
|
+
deviceInfosByMaster[masterId] = [];
|
|
168
171
|
}
|
|
169
|
-
|
|
172
|
+
deviceInfosByMaster[masterId].push(deviceInfo);
|
|
170
173
|
}
|
|
171
174
|
|
|
172
175
|
// 2️⃣ MasterDevices anlegen
|
|
173
|
-
for (const [masterId,
|
|
174
|
-
const masterName = (
|
|
176
|
+
for (const [masterId, childDeviceInfos] of Object.entries(deviceInfosByMaster)) {
|
|
177
|
+
const masterName = (childDeviceInfos[0].masterDeviceName || childDeviceInfos[0].deviceName).replace(
|
|
175
178
|
/\${|}/g,
|
|
176
179
|
"",
|
|
177
180
|
);
|
|
178
181
|
const masterDevice = new SchellenbergMasterDevice(this.adapter, masterId, masterName, []);
|
|
179
182
|
|
|
180
183
|
// 3️⃣ Child-Devices unter Master anlegen
|
|
181
|
-
for (const
|
|
184
|
+
for (const childDeviceInfo of childDeviceInfos) {
|
|
182
185
|
// Nur Devices mit definierbaren States
|
|
183
186
|
this.adapter.log.debug(
|
|
184
|
-
`Getting device capabilites from definition: ${JSON.stringify(
|
|
187
|
+
`Getting device capabilites from definition: ${JSON.stringify(childDeviceInfo.definition)}`,
|
|
185
188
|
);
|
|
186
|
-
const capabilities = getDeviceCapabilities(
|
|
189
|
+
const capabilities = getDeviceCapabilities(childDeviceInfo.definition);
|
|
187
190
|
|
|
188
191
|
if (!capabilities.shouldBeCreated) {
|
|
189
192
|
this.adapter.log.debug(
|
|
190
|
-
`Skipping device ${
|
|
193
|
+
`Skipping device ${childDeviceInfo.deviceName} (${childDeviceInfo.deviceDesignation}) – no definable states found`,
|
|
191
194
|
);
|
|
192
195
|
continue;
|
|
193
196
|
} else {
|
|
194
197
|
this.adapter.log.debug(
|
|
195
|
-
`Device ${
|
|
198
|
+
`Device ${childDeviceInfo.deviceID}: kind=${childDeviceInfo.definition.deviceType.kind}, capabilities=${JSON.stringify(capabilities)}`,
|
|
196
199
|
);
|
|
197
200
|
}
|
|
198
201
|
|
|
199
|
-
const
|
|
200
|
-
id:
|
|
201
|
-
name:
|
|
202
|
-
deviceType:
|
|
203
|
-
|
|
204
|
-
|
|
202
|
+
const childDevice = await this.deviceManager.createDevice({
|
|
203
|
+
id: childDeviceInfo.deviceID,
|
|
204
|
+
name: childDeviceInfo.deviceName.replace(/\${|}/g, ""),
|
|
205
|
+
deviceType:
|
|
206
|
+
childDeviceInfo.deviceTypClient ?? childDeviceInfo.definition.deviceType?.kind ?? "unknown",
|
|
207
|
+
designation: childDeviceInfo.deviceDesignation,
|
|
208
|
+
definition: childDeviceInfo.definition,
|
|
205
209
|
masterPrefix: `${commonDefines.AdapterDatapointIDs.Devices}.${masterId}`, // States unter Master-Ordner
|
|
206
210
|
});
|
|
207
211
|
|
|
208
|
-
masterDevice.childDevices.push(
|
|
209
|
-
this.adapter.log.info(
|
|
212
|
+
masterDevice.childDevices.push(childDevice);
|
|
213
|
+
this.adapter.log.info(
|
|
214
|
+
`Device created: ${childDeviceInfo.deviceName} (ID: ${childDeviceInfo.deviceID})`,
|
|
215
|
+
);
|
|
210
216
|
}
|
|
211
217
|
|
|
212
218
|
if (masterDevice.childDevices?.length !== 0) {
|
|
@@ -34,15 +34,13 @@ const SensorKinds = {
|
|
|
34
34
|
unit: "°C",
|
|
35
35
|
defaultMin: -90,
|
|
36
36
|
defaultMax: 60,
|
|
37
|
-
defaultStep: 0.5,
|
|
38
37
|
},
|
|
39
38
|
luminanceDetector: {
|
|
40
|
-
role: "value.brightness
|
|
39
|
+
role: "value.brightness",
|
|
41
40
|
desc: "Measured brightness",
|
|
42
41
|
unit: "lux",
|
|
43
42
|
defaultMin: 0,
|
|
44
43
|
defaultMax: 150000,
|
|
45
|
-
defaultStep: 1,
|
|
46
44
|
},
|
|
47
45
|
volume: {
|
|
48
46
|
role: "value.rain",
|
|
@@ -50,7 +48,6 @@ const SensorKinds = {
|
|
|
50
48
|
unit: "mm",
|
|
51
49
|
defaultMin: 0,
|
|
52
50
|
defaultMax: 350,
|
|
53
|
-
defaultStep: 1,
|
|
54
51
|
},
|
|
55
52
|
batteryLevel: {
|
|
56
53
|
role: "value.battery",
|
|
@@ -58,7 +55,6 @@ const SensorKinds = {
|
|
|
58
55
|
unit: "%",
|
|
59
56
|
defaultMin: 0,
|
|
60
57
|
defaultMax: 100,
|
|
61
|
-
defaultStep: 1,
|
|
62
58
|
},
|
|
63
59
|
// some devices are defined as "kind": "default", so this is a fallback
|
|
64
60
|
// generic sensor kind for different types of sensors (e.g. atmospheric pressure, wind speed, etc.)
|
|
@@ -76,23 +72,28 @@ const SensorKinds = {
|
|
|
76
72
|
*/
|
|
77
73
|
const EnumStateKinds = {
|
|
78
74
|
failureStatus: {
|
|
75
|
+
type: "number",
|
|
79
76
|
role: "value",
|
|
80
77
|
desc: "Error code of the device",
|
|
81
78
|
},
|
|
82
79
|
smokeDetector: {
|
|
80
|
+
type: "number",
|
|
83
81
|
role: "sensor.alarm.fire",
|
|
84
82
|
desc: "Smoke detector state",
|
|
85
83
|
},
|
|
86
84
|
signal: {
|
|
87
85
|
kind: "rssi", // rename to make the purpose of the state clearer
|
|
86
|
+
type: "number",
|
|
88
87
|
role: "value",
|
|
89
88
|
desc: "Signal strength (RSSI)",
|
|
90
89
|
},
|
|
91
90
|
floodDetector: {
|
|
91
|
+
type: "number",
|
|
92
92
|
role: "sensor.alarm.flood",
|
|
93
93
|
desc: "Flood detector state",
|
|
94
94
|
},
|
|
95
95
|
genericEnum: {
|
|
96
|
+
type: "number",
|
|
96
97
|
role: "value",
|
|
97
98
|
desc: "Enumerated state",
|
|
98
99
|
},
|
|
@@ -35,13 +35,7 @@ function getDeviceCapabilities(definition) {
|
|
|
35
35
|
// Analog sensors
|
|
36
36
|
let sensorConfig = null;
|
|
37
37
|
if (type === "sensor" && model === "analog") {
|
|
38
|
-
|
|
39
|
-
// Known sensor kind
|
|
40
|
-
sensorConfig = SensorKinds[kind];
|
|
41
|
-
} else if (kind === "default") {
|
|
42
|
-
// Generic fallback fo default kind
|
|
43
|
-
sensorConfig = SensorKinds.genericSensor;
|
|
44
|
-
}
|
|
38
|
+
sensorConfig = SensorKinds[kind] ?? SensorKinds.genericSensor;
|
|
45
39
|
}
|
|
46
40
|
|
|
47
41
|
// Enum sensors (textOptions)
|
|
@@ -53,7 +47,16 @@ function getDeviceCapabilities(definition) {
|
|
|
53
47
|
deviceType.textOptions.some(opt => opt && typeof opt === "object" && "name" in opt)
|
|
54
48
|
) {
|
|
55
49
|
const enumKind = resolveEnumKind(definition, deviceType);
|
|
56
|
-
|
|
50
|
+
const baseEnumConfig = EnumStateKinds[enumKind] ?? EnumStateKinds.genericEnum;
|
|
51
|
+
|
|
52
|
+
const isBooleanEnum =
|
|
53
|
+
deviceType.textOptions.length === 2 && deviceType.textOptions.every(opt => typeof opt.value === "boolean");
|
|
54
|
+
|
|
55
|
+
enumStateConfig = {
|
|
56
|
+
...baseEnumConfig,
|
|
57
|
+
type: isBooleanEnum ? "boolean" : "number",
|
|
58
|
+
kind: baseEnumConfig.kind ?? enumKind,
|
|
59
|
+
};
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
if (levelConfig) {
|
|
@@ -84,10 +87,11 @@ function getDeviceCapabilities(definition) {
|
|
|
84
87
|
unit: sensorConfig.unit,
|
|
85
88
|
min: deviceType.min ?? sensorConfig.defaultMin,
|
|
86
89
|
max: deviceType.max ?? sensorConfig.defaultMax,
|
|
87
|
-
|
|
90
|
+
// leave out step for sensors as this leads to rounded values in ioBroker
|
|
88
91
|
});
|
|
89
92
|
} else if (enumStateConfig) {
|
|
90
93
|
Object.assign(configObj, {
|
|
94
|
+
type: enumStateConfig.type ?? "number",
|
|
91
95
|
kind: enumStateConfig.kind ?? configObj.kind,
|
|
92
96
|
role: enumStateConfig.role,
|
|
93
97
|
desc: enumStateConfig.desc,
|
|
@@ -17,8 +17,22 @@ class DeviceManager {
|
|
|
17
17
|
async createDevice(deviceInfo) {
|
|
18
18
|
try {
|
|
19
19
|
const device = new SchellenbergDevice(this.adapter);
|
|
20
|
-
|
|
20
|
+
|
|
21
|
+
// Set raw data
|
|
22
|
+
device.id = deviceInfo.id;
|
|
23
|
+
device.name = deviceInfo.name;
|
|
24
|
+
device.deviceType = deviceInfo.deviceType;
|
|
25
|
+
device.designation = deviceInfo.designation;
|
|
26
|
+
|
|
27
|
+
// Set normalized definition
|
|
28
|
+
device.SetDefinition(deviceInfo.definition);
|
|
29
|
+
this.log.debug(
|
|
30
|
+
`Normalized device definition for ${deviceInfo.id}: ${JSON.stringify(device.normalizedDefinition)}`,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
// Create and save device states
|
|
21
34
|
await device.CreateAndSave(deviceInfo.masterPrefix);
|
|
35
|
+
|
|
22
36
|
this.devices.set(deviceInfo.id, device);
|
|
23
37
|
return device;
|
|
24
38
|
} catch (error) {
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
const { normalizeDeviceDefinition, decodeUnicodeString } = require("../helpers/DeviceDefinitions");
|
|
3
4
|
const { getDeviceCapabilities } = require("./DeviceKindCapabilities");
|
|
4
5
|
const commonDefines = require("../helpers/CommonDefines");
|
|
5
6
|
|
|
6
7
|
class SchellenbergDevice {
|
|
7
8
|
constructor(adapter) {
|
|
8
9
|
this.adapter = adapter;
|
|
9
|
-
this.
|
|
10
|
+
this.rawDefinition = null;
|
|
11
|
+
this.normalizedDefinition = null;
|
|
10
12
|
|
|
11
13
|
// Info
|
|
12
14
|
this.id = -1;
|
|
@@ -15,10 +17,15 @@ class SchellenbergDevice {
|
|
|
15
17
|
this.designation = "";
|
|
16
18
|
}
|
|
17
19
|
|
|
20
|
+
SetDefinition(definition) {
|
|
21
|
+
this.rawDefinition = definition;
|
|
22
|
+
this.normalizedDefinition = normalizeDeviceDefinition(definition, this.adapter.FORBIDDEN_CHARS) ?? definition;
|
|
23
|
+
}
|
|
24
|
+
|
|
18
25
|
// Creates all necessery states and channels and writes the values into the DB
|
|
19
26
|
async CreateAndSave(masterPrefix) {
|
|
20
|
-
//
|
|
21
|
-
const capabilities = getDeviceCapabilities(this.
|
|
27
|
+
// Dynamically from device.definition
|
|
28
|
+
const capabilities = getDeviceCapabilities(this.normalizedDefinition);
|
|
22
29
|
|
|
23
30
|
let devicePrefix = masterPrefix
|
|
24
31
|
? `${masterPrefix}.${this.id}`
|
|
@@ -27,11 +34,10 @@ class SchellenbergDevice {
|
|
|
27
34
|
devicePrefix = devicePrefix.replace(this.adapter.FORBIDDEN_CHARS, "");
|
|
28
35
|
|
|
29
36
|
// Umlauts are Unicode-escaped and need to be parsed here
|
|
30
|
-
const rawName = this.name.replace(/\${|}/g, "");
|
|
31
37
|
await this.adapter.setObjectNotExistsAsync(devicePrefix, {
|
|
32
38
|
type: "channel",
|
|
33
39
|
common: {
|
|
34
|
-
name:
|
|
40
|
+
name: decodeUnicodeString(this.name),
|
|
35
41
|
},
|
|
36
42
|
native: {},
|
|
37
43
|
});
|
|
@@ -131,16 +137,16 @@ class SchellenbergDevice {
|
|
|
131
137
|
}
|
|
132
138
|
|
|
133
139
|
async createSwitchStates(statePrefix) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
.toLowerCase();
|
|
140
|
+
if (!this.normalizedDefinition) {
|
|
141
|
+
this.adapter.log.warn(`Device ${this.id}: no normalized definition available`);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
139
144
|
|
|
140
|
-
|
|
145
|
+
for (const stateDef of this.normalizedDefinition.deviceType.switchingValues) {
|
|
146
|
+
await this.adapter.setObjectNotExistsAsync(`${statePrefix}${stateDef.name}`, {
|
|
141
147
|
type: "state",
|
|
142
148
|
common: {
|
|
143
|
-
name:
|
|
149
|
+
name: stateDef.name,
|
|
144
150
|
type: "boolean",
|
|
145
151
|
role: "button",
|
|
146
152
|
read: false,
|
|
@@ -205,7 +211,6 @@ class SchellenbergDevice {
|
|
|
205
211
|
write: false,
|
|
206
212
|
min: sensorConfigObj.min,
|
|
207
213
|
max: sensorConfigObj.max,
|
|
208
|
-
step: sensorConfigObj.step,
|
|
209
214
|
unit: sensorConfigObj.unit,
|
|
210
215
|
def: 0,
|
|
211
216
|
},
|
|
@@ -219,7 +224,7 @@ class SchellenbergDevice {
|
|
|
219
224
|
common: {
|
|
220
225
|
name: enumConfigObj.kind,
|
|
221
226
|
desc: enumConfigObj.desc,
|
|
222
|
-
type:
|
|
227
|
+
type: enumConfigObj.type,
|
|
223
228
|
role: enumConfigObj.role,
|
|
224
229
|
read: true,
|
|
225
230
|
write: false,
|
|
@@ -251,29 +256,29 @@ class SchellenbergDevice {
|
|
|
251
256
|
}
|
|
252
257
|
|
|
253
258
|
async updateValue(value) {
|
|
254
|
-
// value:
|
|
259
|
+
// value: Instance of DeviceValue
|
|
255
260
|
if (value.deviceID !== this.id) {
|
|
256
|
-
//
|
|
261
|
+
// This instance does not concern this device
|
|
257
262
|
return;
|
|
258
263
|
}
|
|
259
264
|
|
|
260
265
|
let controlPrefix = "";
|
|
261
266
|
let infoPrefix = "";
|
|
262
267
|
if (value.masterDeviceID != this.id) {
|
|
263
|
-
// Child
|
|
268
|
+
// Child devive under a master device
|
|
264
269
|
controlPrefix = `${commonDefines.AdapterDatapointIDs.Devices}.${value.masterDeviceID}.${this.id}.${commonDefines.AdapterDatapointIDs.Control}.`;
|
|
265
270
|
infoPrefix = `${commonDefines.AdapterDatapointIDs.Devices}.${value.masterDeviceID}.${this.id}.${commonDefines.AdapterDatapointIDs.Info}.`;
|
|
266
271
|
} else {
|
|
267
|
-
//
|
|
272
|
+
// Standalone device
|
|
268
273
|
controlPrefix = `${commonDefines.AdapterDatapointIDs.Devices}.${this.id}.${commonDefines.AdapterDatapointIDs.Control}.`;
|
|
269
274
|
infoPrefix = `${commonDefines.AdapterDatapointIDs.Devices}.${this.id}.${commonDefines.AdapterDatapointIDs.Info}.`;
|
|
270
275
|
}
|
|
271
276
|
|
|
272
|
-
// Dynamisch aus device.
|
|
273
|
-
const capabilities = getDeviceCapabilities(this.
|
|
277
|
+
// Dynamisch aus device.normalizedDefinition
|
|
278
|
+
const capabilities = getDeviceCapabilities(this.normalizedDefinition);
|
|
274
279
|
|
|
275
280
|
if (capabilities.hasSwitchingValues) {
|
|
276
|
-
// SwitchingValues
|
|
281
|
+
// Ignore SwitchingValues as they don't report their state changes
|
|
277
282
|
this.adapter.log.debug(`Device ${this.id}: switchingValue update ignored (value=${value.value})`);
|
|
278
283
|
return;
|
|
279
284
|
}
|
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
const commonDefines = require("../helpers/CommonDefines");
|
|
2
|
+
const { decodeUnicodeString } = require("../helpers/DeviceDefinitions");
|
|
2
3
|
|
|
3
4
|
class SchellenbergMasterDevice {
|
|
4
5
|
constructor(adapter, masterId, name, devices) {
|
|
5
6
|
this.adapter = adapter;
|
|
6
7
|
this.id = masterId;
|
|
7
8
|
this.name = name;
|
|
8
|
-
this.childDevices = devices; // Array
|
|
9
|
+
this.childDevices = devices; // Array of SchellenbergDevice
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
async createMasterFolder() {
|
|
12
13
|
const masterPrefix = `${commonDefines.AdapterDatapointIDs.Devices}.${this.id.replace(this.adapter.FORBIDDEN_CHARS, "")}`;
|
|
13
14
|
|
|
14
15
|
// Umlauts are Unicode-escaped and need to be parsed here
|
|
15
|
-
const rawName = this.name.replace(/\${|}/g, "");
|
|
16
16
|
await this.adapter.setObjectNotExistsAsync(masterPrefix, {
|
|
17
17
|
type: "channel",
|
|
18
|
-
common: { name:
|
|
18
|
+
common: { name: decodeUnicodeString(this.name) },
|
|
19
19
|
native: {},
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
-
//
|
|
22
|
+
// Create child devices
|
|
23
23
|
for (const child of this.childDevices) {
|
|
24
|
-
await child.CreateAndSave(masterPrefix);
|
|
24
|
+
await child.CreateAndSave(masterPrefix);
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const SWITCHING_VALUE_ALIASES = {
|
|
4
|
+
"<>": "toggle",
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export function normalizeDeviceDefinition(definition, forbiddenChars) {
|
|
8
|
+
if (!definition?.deviceType) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const deviceType = definition.deviceType;
|
|
13
|
+
|
|
14
|
+
const normalized = {
|
|
15
|
+
...definition,
|
|
16
|
+
deviceType: {
|
|
17
|
+
...deviceType,
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// --- switchingValues ---
|
|
22
|
+
if (Array.isArray(deviceType.switchingValues)) {
|
|
23
|
+
normalized.deviceType.switchingValues = deviceType.switchingValues
|
|
24
|
+
.map(switchingValue => {
|
|
25
|
+
const safeName = sanitizeIdPart(switchingValue.name ?? switchingValue.value, forbiddenChars);
|
|
26
|
+
if (!safeName) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
...switchingValue,
|
|
32
|
+
name: safeName,
|
|
33
|
+
};
|
|
34
|
+
})
|
|
35
|
+
.filter(Boolean);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// --- enum textOptions ---
|
|
39
|
+
if (Array.isArray(deviceType.textOptions)) {
|
|
40
|
+
normalized.deviceType.textOptions = deviceType.textOptions
|
|
41
|
+
.map(opt => {
|
|
42
|
+
const safeName = sanitizeIdPart(opt.name ?? opt.value, forbiddenChars);
|
|
43
|
+
if (!safeName) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
...opt,
|
|
49
|
+
name: safeName,
|
|
50
|
+
};
|
|
51
|
+
})
|
|
52
|
+
.filter(Boolean);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return normalized;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function sanitizeIdPart(value, forbiddenChars) {
|
|
59
|
+
if (value === undefined || value === null) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let str = String(value);
|
|
64
|
+
|
|
65
|
+
// Alias BEFORE decode
|
|
66
|
+
const preAlias = str.trim();
|
|
67
|
+
if (SWITCHING_VALUE_ALIASES[preAlias]) {
|
|
68
|
+
str = SWITCHING_VALUE_ALIASES[preAlias];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Decode Unicode-escaped strings \uXXXX
|
|
72
|
+
str = decodeUnicodeString(str);
|
|
73
|
+
|
|
74
|
+
// Alias AFTER decode
|
|
75
|
+
const postAlias = str.trim();
|
|
76
|
+
if (SWITCHING_VALUE_ALIASES[postAlias]) {
|
|
77
|
+
str = SWITCHING_VALUE_ALIASES[postAlias];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const sanitized = str
|
|
81
|
+
.toLowerCase()
|
|
82
|
+
.replace(/\${|}/g, "") // ${On} → On
|
|
83
|
+
.replace(forbiddenChars, "");
|
|
84
|
+
|
|
85
|
+
return sanitized.length > 0 ? sanitized : null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function decodeUnicodeString(str) {
|
|
89
|
+
if (typeof str !== "string") {
|
|
90
|
+
return str;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Nothing to decode
|
|
94
|
+
if (!str.includes("\\u")) {
|
|
95
|
+
return str;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Replace literal \uXXXX sequences with real Unicode characters
|
|
99
|
+
return str.replace(/\\u([0-9a-fA-F]{4})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
|
|
100
|
+
}
|