iobroker.smartfriends 1.2.0 → 1.3.0-alpha.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
@@ -33,6 +33,11 @@ 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.0-alpha.0 (2026-01-10)
37
+
38
+ - (Black-Thunder) Support for further device types was added
39
+ - (Black-Thunder) Umlauts in device names are now correctly parsed
40
+
36
41
  ### 1.2.0 (2026-01-09)
37
42
 
38
43
  - (Black-Thunder) Timeout for initial device request was increased
package/io-package.json CHANGED
@@ -1,8 +1,21 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "smartfriends",
4
- "version": "1.2.0",
4
+ "version": "1.3.0-alpha.0",
5
5
  "news": {
6
+ "1.3.0-alpha.0": {
7
+ "en": "Support for further device types was added\nUmlauts in device names are now correctly parsed",
8
+ "de": "Unterstützung für weitere Gerätetypen wurde hinzugefügt\nUmlaute in Gerätenamen werden nun korrekt dargestellt",
9
+ "ru": "Добавлена поддержка дополнительных типов устройств\nУмлауты в названиях устройств теперь правильно разобраны",
10
+ "pt": "Foi adicionado suporte para outros tipos de dispositivos\nUmlauts em nomes de dispositivos agora são corretamente analisados",
11
+ "nl": "Ondersteuning voor andere apparaattypes is toegevoegd\nUmlauts in apparaatnamen zijn nu correct ontleed",
12
+ "fr": "Un soutien pour d'autres types d'appareils a été ajouté\nUmlauts dans les noms de périphériques sont maintenant correctement analysés",
13
+ "it": "È stato aggiunto il supporto per ulteriori tipi di dispositivo\nI nomi dei dispositivi sono ora correttamente analizzati",
14
+ "es": "Se agregó soporte para nuevos tipos de dispositivos\nUmlauts en los nombres de los dispositivos ahora se analizan correctamente",
15
+ "pl": "Dodano wsparcie dla innych typów urządzeń\nKlauty w nazwach urządzeń są teraz poprawnie przefiltrowane",
16
+ "uk": "Додана підтримка подальших типів пристроїв\nУмлаути в назвах пристроїв тепер правильно припаровані",
17
+ "zh-cn": "添加了对更多设备类型的支持\n设备名称中的 Umlaut 现已正确解析"
18
+ },
6
19
  "1.2.0": {
7
20
  "en": "Timeout for initial device request was increased\nDevices without defined device type are ignored\nRefactored device handling and added support for further device types",
8
21
  "de": "Timeout für erste Geräteanfrage wurde erhöht\nGeräte ohne definierten Gerätetyp werden ignoriert\nRefactored Device Handling und zusätzliche Unterstützung für weitere Gerätetypen",
@@ -80,19 +93,6 @@
80
93
  "pl": "Przekształcona obsługa urządzenia: stany dynamiczne, usunięty biały typ, zgrupowane urządzenia pod master ID",
81
94
  "uk": "Рефакторний пристрій обробки: динамічні стани, видалений тип білий список, вбудовані пристрої під магістр ID",
82
95
  "zh-cn": "重构设备处理: 动态状态, 删除类型白名单, 主 ID 下分组设备"
83
- },
84
- "1.0.1": {
85
- "en": "Increased robustness when communicating with the gateway\nAdded new option to ignore certificate errors",
86
- "de": "Erhöhte Robustheit bei der Kommunikation mit dem Gateway\nNeue Option hinzugefügt, um Zertifikatsfehler zu ignorieren",
87
- "ru": "Повышенная надежность при общении с шлюзом\nДобавлена новая возможность игнорировать ошибки сертификата",
88
- "pt": "Maior robustez ao se comunicar com o gateway\nAdicionada nova opção para ignorar erros de certificado",
89
- "nl": "Verhoogde robuustheid bij het communiceren met de gateway\nNieuwe optie toegevoegd om certificaatfouten te negeren",
90
- "fr": "Une robustesse accrue lors de la communication avec la passerelle\nAjout d'une nouvelle option pour ignorer les erreurs de certificat",
91
- "it": "Maggiore robustezza quando si comunica con il gateway\nAggiunta nuova opzione per ignorare gli errori del certificato",
92
- "es": "Mayor robustez al comunicarse con la puerta de entrada\nNueva opción para ignorar errores de certificado",
93
- "pl": "Większa odporność podczas komunikacji z bramą\nDodano nową opcję do ignorowania błędów certyfikatu",
94
- "uk": "Підвищена надійність при спілкуванні з шлюзом\nДодано новий варіант ігнорувати помилки сертифіката",
95
- "zh-cn": "与网关沟通时的强度提高\n添加新选项以忽略证书错误"
96
96
  }
97
97
  },
98
98
  "titleLang": {
@@ -29,13 +29,21 @@ const LevelKinds = {
29
29
  */
30
30
  const SensorKinds = {
31
31
  thermometer: {
32
- role: "level.temperature",
32
+ role: "value.temperature",
33
33
  desc: "Measured temperature",
34
34
  unit: "°C",
35
35
  defaultMin: -90,
36
36
  defaultMax: 60,
37
37
  defaultStep: 0.5,
38
38
  },
39
+ luminanceDetector: {
40
+ role: "value.brightness ",
41
+ desc: "Measured brightness",
42
+ unit: "lux",
43
+ defaultMin: 0,
44
+ defaultMax: 150000,
45
+ defaultStep: 1,
46
+ },
39
47
  volume: {
40
48
  role: "value.rain",
41
49
  desc: "Measured rain volume",
@@ -44,8 +52,18 @@ const SensorKinds = {
44
52
  defaultMax: 350,
45
53
  defaultStep: 1,
46
54
  },
47
- // generic sensor kind for different types of weather sensors (e.g. atmospheric pressure, wind speed, etc.)
48
- weather: {
55
+ batteryLevel: {
56
+ role: "value.battery",
57
+ desc: "Battery level",
58
+ unit: "%",
59
+ defaultMin: 0,
60
+ defaultMax: 100,
61
+ defaultStep: 1,
62
+ },
63
+ // some devices are defined as "kind": "default", so this is a fallback
64
+ // generic sensor kind for different types of sensors (e.g. atmospheric pressure, wind speed, etc.)
65
+ genericSensor: {
66
+ kind: "measuredValue",
49
67
  role: "value",
50
68
  desc: "Measured value",
51
69
  unit: "",
@@ -54,17 +72,50 @@ const SensorKinds = {
54
72
 
55
73
  /**
56
74
  * Declarative registry for device kinds that provide
57
- * an alarm information
75
+ * an enumerated information
58
76
  */
59
- const AlarmKinds = {
77
+ const EnumStateKinds = {
60
78
  failureStatus: {
61
79
  role: "value",
62
80
  desc: "Error code of the device",
63
81
  },
82
+ smokeDetector: {
83
+ role: "sensor.alarm.fire",
84
+ desc: "Smoke detector state",
85
+ },
86
+ signal: {
87
+ kind: "rssi", // rename to make the purpose of the state clearer
88
+ role: "value",
89
+ desc: "Signal strength (RSSI)",
90
+ },
91
+ floodDetector: {
92
+ role: "sensor.alarm.flood",
93
+ desc: "Flood detector state",
94
+ },
95
+ genericEnum: {
96
+ role: "value",
97
+ desc: "Enumerated state",
98
+ },
99
+ };
100
+
101
+ /**
102
+ * Declarative registry for device kinds that provide thermostat controls
103
+ */
104
+ const ThermostatKinds = {
105
+ thermostat: {
106
+ kind: "targetTemperature", // rename to make the purpose of the state clearer
107
+ role: "level.temperature",
108
+ desc: "Target temperature",
109
+ unit: "°C",
110
+ defaultMin: 5,
111
+ defaultMax: 28,
112
+ defaultStep: 0.5,
113
+ },
64
114
  };
65
115
 
66
116
  module.exports = {
67
- AlarmKinds,
68
117
  LevelKinds,
69
118
  SensorKinds,
119
+ EnumStateKinds,
120
+ ThermostatKinds,
70
121
  };
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
 
3
- const { LevelKinds, SensorKinds, AlarmKinds } = require("./DeviceCapabilityRegistry");
3
+ const { resolveEnumKind } = require("./EnumKindResolver");
4
+ const { LevelKinds, SensorKinds, EnumStateKinds, ThermostatKinds } = require("./DeviceCapabilityRegistry");
4
5
 
5
6
  /**
6
7
  * Resolves capabilities from a SmartFriends device definition
@@ -10,76 +11,104 @@ function getDeviceCapabilities(definition) {
10
11
  return {
11
12
  shouldBeCreated: false,
12
13
  hasWritableStates: false,
13
- hasSwitchingValues: false,
14
- hasLevel: false,
15
- hasSensor: false,
16
- hasAlarm: false,
17
14
  configObj: null,
18
15
  };
19
16
  }
20
17
 
21
- const deviceType = definition?.deviceType ?? {};
18
+ const deviceType = definition.deviceType;
22
19
  const kind = deviceType.kind ?? "unknown";
23
- const deviceDesignation = definition?.deviceDesignation?.replace(/\${|}/g, "") ?? kind;
24
20
  const model = deviceType.model ?? "unknown";
21
+ const type = deviceType.type;
22
+ const name = (definition.deviceDesignation ?? kind).replace(/\${|}/g, "");
23
+ const configObj = { kind, name };
25
24
 
25
+ // SwitchingValues → always win
26
26
  const switchingValues = Array.isArray(deviceType.switchingValues) ? deviceType.switchingValues : [];
27
-
28
27
  const hasSwitchingValues = switchingValues.length > 0;
29
- let configObj = {
30
- kind,
31
- name: deviceDesignation,
32
- };
33
28
 
29
+ // Level actuators
34
30
  const levelConfig = LevelKinds[kind] ?? null;
35
- const sensorConfig = (SensorKinds[kind] && model == "analog") ?? null; // only sensor type "analog" contains measurable values
36
- const alarmConfig = AlarmKinds[kind] ?? null;
31
+
32
+ // Thermostat
33
+ const thermostatConfig = type === "actuator" && model === "analog" ? (ThermostatKinds[kind] ?? null) : null;
34
+
35
+ // Analog sensors
36
+ let sensorConfig = null;
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
+ }
45
+ }
46
+
47
+ // Enum sensors (textOptions)
48
+ let enumStateConfig = null;
49
+ if (
50
+ type === "sensor" &&
51
+ model === "text" &&
52
+ Array.isArray(deviceType.textOptions) &&
53
+ deviceType.textOptions.some(opt => opt && typeof opt === "object" && "name" in opt)
54
+ ) {
55
+ const enumKind = resolveEnumKind(definition, deviceType);
56
+ enumStateConfig = EnumStateKinds[enumKind] ?? EnumStateKinds.genericEnum;
57
+ }
37
58
 
38
59
  if (levelConfig) {
39
- configObj = {
40
- ...configObj,
60
+ Object.assign(configObj, {
61
+ kind: levelConfig.kind ?? configObj.kind,
41
62
  role: levelConfig.role,
42
63
  desc: levelConfig.desc,
43
64
  unit: levelConfig.unit,
44
65
  min: deviceType.min ?? levelConfig.defaultMin,
45
66
  max: deviceType.max ?? levelConfig.defaultMax,
46
67
  step: deviceType.step ?? levelConfig.defaultStep,
47
- };
68
+ });
69
+ } else if (thermostatConfig) {
70
+ Object.assign(configObj, {
71
+ kind: thermostatConfig.kind ?? configObj.kind,
72
+ role: thermostatConfig.role,
73
+ desc: thermostatConfig.desc,
74
+ unit: thermostatConfig.unit,
75
+ min: deviceType.min ?? thermostatConfig.defaultMin,
76
+ max: deviceType.max ?? thermostatConfig.defaultMax,
77
+ step: deviceType.step ?? thermostatConfig.defaultStep,
78
+ });
48
79
  } else if (sensorConfig) {
49
- configObj = {
50
- ...configObj,
80
+ Object.assign(configObj, {
81
+ kind: sensorConfig.kind ?? configObj.kind,
51
82
  role: sensorConfig.role,
52
83
  desc: sensorConfig.desc,
53
84
  unit: sensorConfig.unit,
54
85
  min: deviceType.min ?? sensorConfig.defaultMin,
55
86
  max: deviceType.max ?? sensorConfig.defaultMax,
56
87
  step: deviceType.step ?? sensorConfig.defaultStep,
57
- };
58
- } else if (alarmConfig) {
59
- configObj = {
60
- ...configObj,
61
- role: alarmConfig.role,
62
- desc: alarmConfig.desc,
63
- };
64
-
65
- // map "textOptions" (if present) into configObj as enumerated states
66
- if (deviceType.textOptions && Array.isArray(deviceType.textOptions) && deviceType.textOptions.length > 0) {
67
- configObj.states = deviceType.textOptions.reduce((acc, opt) => {
68
- acc[opt.value] = `${opt.state} (${opt.name.replace(/\${|}/g, "")})`;
88
+ });
89
+ } else if (enumStateConfig) {
90
+ Object.assign(configObj, {
91
+ kind: enumStateConfig.kind ?? configObj.kind,
92
+ role: enumStateConfig.role,
93
+ desc: enumStateConfig.desc,
94
+ states: deviceType.textOptions.reduce((acc, opt) => {
95
+ acc[opt.value] = opt.name.replace(/\${|}/g, "");
69
96
  return acc;
70
- }, {});
71
- }
97
+ }, {}),
98
+ });
72
99
  }
73
100
 
74
101
  return {
75
102
  // state creation decision
76
- shouldBeCreated: hasSwitchingValues || !!levelConfig || !!sensorConfig || !!alarmConfig,
77
- hasWritableStates: hasSwitchingValues || !!levelConfig,
103
+ shouldBeCreated:
104
+ hasSwitchingValues || !!levelConfig || !!thermostatConfig || !!sensorConfig || !!enumStateConfig,
105
+ hasWritableStates: hasSwitchingValues || !!levelConfig || !!thermostatConfig,
78
106
  // metadata for builders
79
107
  hasSwitchingValues,
80
108
  hasLevel: !!levelConfig,
109
+ hasThermostat: !!thermostatConfig,
81
110
  hasSensor: !!sensorConfig,
82
- hasAlarm: !!alarmConfig,
111
+ hasEnumState: !!enumStateConfig,
83
112
  // config object for state creation
84
113
  configObj: configObj,
85
114
  };
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Resolves semantic enum type for devices with
5
+ * model=text + textOptions
6
+ */
7
+ function resolveEnumKind(definition, deviceType) {
8
+ const designation = definition.deviceDesignation ?? "";
9
+ const kind = definition.kind ?? "";
10
+ const options = deviceType.textOptions ?? [];
11
+
12
+ // RSSI / signal strength
13
+ if (designation.includes("Rssi")) {
14
+ return "signal";
15
+ }
16
+
17
+ // Failure status
18
+ if (kind.includes("failureStatus")) {
19
+ return "failureStatus";
20
+ }
21
+
22
+ // Smoke detector
23
+ if (options.some(o => /smoke/i.test(o.valueState ?? ""))) {
24
+ return "smokeDetector";
25
+ }
26
+
27
+ // Flood / water
28
+ if (options.some(o => /water|flood/i.test(o.valueState ?? ""))) {
29
+ return "floodDetector";
30
+ }
31
+
32
+ return "genericEnum";
33
+ }
34
+
35
+ module.exports = {
36
+ resolveEnumKind,
37
+ };
@@ -26,10 +26,12 @@ class SchellenbergDevice {
26
26
 
27
27
  devicePrefix = devicePrefix.replace(this.adapter.FORBIDDEN_CHARS, "");
28
28
 
29
+ // Umlauts are Unicode-escaped and need to be parsed here
30
+ const rawName = this.name.replace(/\${|}/g, "");
29
31
  await this.adapter.setObjectNotExistsAsync(devicePrefix, {
30
32
  type: "channel",
31
33
  common: {
32
- name: this.name,
34
+ name: JSON.parse(`"${rawName.replace(/\\\\/g, "\\")}"`),
33
35
  },
34
36
  native: {},
35
37
  });
@@ -92,8 +94,8 @@ class SchellenbergDevice {
92
94
  await this.createSensorState(infoPrefix, capabilities.configObj);
93
95
  }
94
96
 
95
- if (capabilities.hasAlarm) {
96
- await this.createAlarmState(infoPrefix, capabilities.configObj);
97
+ if (capabilities.hasEnumState) {
98
+ await this.createEnumState(infoPrefix, capabilities.configObj);
97
99
  }
98
100
  //#endregion
99
101
 
@@ -118,6 +120,10 @@ class SchellenbergDevice {
118
120
  if (capabilities.hasLevel) {
119
121
  await this.createLevelState(controlPrefix, capabilities.configObj);
120
122
  }
123
+
124
+ if (capabilities.hasThermostat) {
125
+ await this.createThermostatState(controlPrefix, capabilities.configObj);
126
+ }
121
127
  }
122
128
  //#endregion
123
129
 
@@ -168,6 +174,25 @@ class SchellenbergDevice {
168
174
  });
169
175
  }
170
176
 
177
+ async createThermostatState(statePrefix, thermostatConfigObj) {
178
+ await this.adapter.setObjectNotExistsAsync(`${statePrefix}${thermostatConfigObj.kind}`, {
179
+ type: "state",
180
+ common: {
181
+ name: thermostatConfigObj.kind,
182
+ desc: thermostatConfigObj.desc,
183
+ type: "number",
184
+ role: thermostatConfigObj.role,
185
+ read: true,
186
+ write: true,
187
+ min: thermostatConfigObj.min,
188
+ max: thermostatConfigObj.max,
189
+ step: thermostatConfigObj.step,
190
+ unit: thermostatConfigObj.unit,
191
+ },
192
+ native: {},
193
+ });
194
+ }
195
+
171
196
  async createSensorState(statePrefix, sensorConfigObj) {
172
197
  await this.adapter.setObjectNotExistsAsync(`${statePrefix}${sensorConfigObj.kind}`, {
173
198
  type: "state",
@@ -188,17 +213,17 @@ class SchellenbergDevice {
188
213
  });
189
214
  }
190
215
 
191
- async createAlarmState(statePrefix, alarmConfigObj) {
192
- await this.adapter.setObjectNotExistsAsync(`${statePrefix}${alarmConfigObj.kind}`, {
216
+ async createEnumState(statePrefix, enumConfigObj) {
217
+ await this.adapter.setObjectNotExistsAsync(`${statePrefix}${enumConfigObj.kind}`, {
193
218
  type: "state",
194
219
  common: {
195
- name: alarmConfigObj.kind,
196
- desc: alarmConfigObj.desc,
220
+ name: enumConfigObj.kind,
221
+ desc: enumConfigObj.desc,
197
222
  type: "number",
198
- role: alarmConfigObj.role,
223
+ role: enumConfigObj.role,
199
224
  read: true,
200
225
  write: false,
201
- states: alarmConfigObj.states,
226
+ states: enumConfigObj.states,
202
227
  },
203
228
  native: {},
204
229
  });
@@ -253,15 +278,11 @@ class SchellenbergDevice {
253
278
  return;
254
279
  }
255
280
 
256
- if (capabilities.hasLevel) {
281
+ if (capabilities.hasLevel || capabilities.hasThermostat) {
257
282
  await this.setDeviceStateValue(controlPrefix, capabilities.configObj, value);
258
283
  }
259
284
 
260
- if (capabilities.hasSensor) {
261
- await this.setDeviceStateValue(infoPrefix, capabilities.configObj, value);
262
- }
263
-
264
- if (capabilities.hasAlarm) {
285
+ if (capabilities.hasSensor || capabilities.hasEnumState) {
265
286
  await this.setDeviceStateValue(infoPrefix, capabilities.configObj, value);
266
287
  }
267
288
  }
@@ -10,9 +10,12 @@ class SchellenbergMasterDevice {
10
10
 
11
11
  async createMasterFolder() {
12
12
  const masterPrefix = `${commonDefines.AdapterDatapointIDs.Devices}.${this.id.replace(this.adapter.FORBIDDEN_CHARS, "")}`;
13
+
14
+ // Umlauts are Unicode-escaped and need to be parsed here
15
+ const rawName = this.name.replace(/\${|}/g, "");
13
16
  await this.adapter.setObjectNotExistsAsync(masterPrefix, {
14
17
  type: "channel",
15
- common: { name: this.name },
18
+ common: { name: JSON.parse(`"${rawName.replace(/\\\\/g, "\\")}"`) },
16
19
  native: {},
17
20
  });
18
21
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.smartfriends",
3
- "version": "1.2.0",
3
+ "version": "1.3.0-alpha.0",
4
4
  "description": "smartfriends",
5
5
  "author": {
6
6
  "name": "Black-Thunder",