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 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.2",
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 devicesByMaster = {};
155
- for (const device of deviceInfos) {
154
+ const deviceInfosByMaster = {};
155
+ for (const deviceInfo of deviceInfos) {
156
156
  // Definition dynamisch zuweisen
157
- device.definition = this.deviceDefinitions.find(d => d.deviceDesignation === device.deviceDesignation);
158
- if (!device.definition) {
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 ${device.deviceName} (${device.deviceDesignation}) – no definition found`,
163
+ `Skipping device ${deviceInfo.deviceName} (${deviceInfo.deviceDesignation}) – no definition found`,
161
164
  );
162
165
  continue;
163
166
  }
164
167
 
165
- const masterId = device.masterDeviceID || device.deviceID;
166
- if (!devicesByMaster[masterId]) {
167
- devicesByMaster[masterId] = [];
168
+ const masterId = deviceInfo.masterDeviceID || deviceInfo.deviceID;
169
+ if (!deviceInfosByMaster[masterId]) {
170
+ deviceInfosByMaster[masterId] = [];
168
171
  }
169
- devicesByMaster[masterId].push(device);
172
+ deviceInfosByMaster[masterId].push(deviceInfo);
170
173
  }
171
174
 
172
175
  // 2️⃣ MasterDevices anlegen
173
- for (const [masterId, childDevices] of Object.entries(devicesByMaster)) {
174
- const masterName = (childDevices[0].masterDeviceName || childDevices[0].deviceName).replace(
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 child of childDevices) {
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(child.definition)}`,
187
+ `Getting device capabilites from definition: ${JSON.stringify(childDeviceInfo.definition)}`,
185
188
  );
186
- const capabilities = getDeviceCapabilities(child.definition);
189
+ const capabilities = getDeviceCapabilities(childDeviceInfo.definition);
187
190
 
188
191
  if (!capabilities.shouldBeCreated) {
189
192
  this.adapter.log.debug(
190
- `Skipping device ${child.deviceName} (${child.deviceDesignation}) – no definable states found`,
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 ${child.deviceID}: kind=${child.definition.deviceType.kind}, capabilities=${JSON.stringify(capabilities)}`,
198
+ `Device ${childDeviceInfo.deviceID}: kind=${childDeviceInfo.definition.deviceType.kind}, capabilities=${JSON.stringify(capabilities)}`,
196
199
  );
197
200
  }
198
201
 
199
- const schellenbergDevice = await this.deviceManager.createDevice({
200
- id: child.deviceID,
201
- name: child.deviceName.replace(/\${|}/g, ""),
202
- deviceType: child.deviceTypClient ?? child.definition.deviceType?.kind ?? "unknown",
203
- designation: child.deviceDesignation,
204
- definition: child.definition,
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(schellenbergDevice);
209
- this.adapter.log.info(`Device created: ${child.deviceName} (ID: ${child.deviceID})`);
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
- if (SensorKinds[kind]) {
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
- enumStateConfig = EnumStateKinds[enumKind] ?? EnumStateKinds.genericEnum;
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
- step: deviceType.step ?? sensorConfig.defaultStep,
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
- Object.assign(device, deviceInfo);
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.definition = null;
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
- // Dynamisch aus device.definition
21
- const capabilities = getDeviceCapabilities(this.definition);
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: JSON.parse(`"${rawName.replace(/\\\\/g, "\\")}"`),
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
- for (const stateDef of this.definition.deviceType.switchingValues) {
135
- const stateId = stateDef.name
136
- .replace(/\${|}/g, "") // ${On} → On
137
- .replace(this.adapter.FORBIDDEN_CHARS, "")
138
- .toLowerCase();
140
+ if (!this.normalizedDefinition) {
141
+ this.adapter.log.warn(`Device ${this.id}: no normalized definition available`);
142
+ return;
143
+ }
139
144
 
140
- await this.adapter.setObjectNotExistsAsync(`${statePrefix}${stateId}`, {
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: stateId,
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: "number",
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: Instanz von DeviceValue
259
+ // value: Instance of DeviceValue
255
260
  if (value.deviceID !== this.id) {
256
- // Diese Instanz betrifft nicht dieses Device
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-Device unter einem Master-Device
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
- // Einzelnes Device
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.definition
273
- const capabilities = getDeviceCapabilities(this.definition);
277
+ // Dynamisch aus device.normalizedDefinition
278
+ const capabilities = getDeviceCapabilities(this.normalizedDefinition);
274
279
 
275
280
  if (capabilities.hasSwitchingValues) {
276
- // SwitchingValues ignorieren, liefern keine Updates zurück
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 von SchellenbergDevice
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: JSON.parse(`"${rawName.replace(/\\\\/g, "\\")}"`) },
18
+ common: { name: decodeUnicodeString(this.name) },
19
19
  native: {},
20
20
  });
21
21
 
22
- // Child Devices anlegen
22
+ // Create child devices
23
23
  for (const child of this.childDevices) {
24
- await child.CreateAndSave(masterPrefix); // MasterPrefix übergeben
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.smartfriends",
3
- "version": "1.3.2",
3
+ "version": "1.3.3",
4
4
  "description": "smartfriends",
5
5
  "author": {
6
6
  "name": "Black-Thunder",