iobroker.lovelace 4.1.9 → 4.1.11

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
@@ -473,6 +473,13 @@ After that checkout modified version in `./build` folder. Then.
473
473
  PLACEHOLDER for the next version:
474
474
  ### **WORK IN PROGRESS**
475
475
  -->
476
+ ### 4.1.11 (2024-11-20)
477
+ * (Garfonso) convert string state values to numbers, where necessary.
478
+
479
+ ### 4.1.10 (2024-05-23)
480
+ * (Garfonso) device icons work again.
481
+ * (Garfonso) default user sometimes was not found in system.
482
+
476
483
  ### 4.1.9 (2024-04-26)
477
484
  * (Garfonso) add support for new service call structure.
478
485
  * (Garfonso) add support for delivering files from other adapters, for example, local cover images.
@@ -486,12 +493,6 @@ After that checkout modified version in `./build` folder. Then.
486
493
  * (Garfonso) remove exessive logging
487
494
  * (Garfonso) improve fix for crash again.
488
495
 
489
- ### 4.1.5 (2024-03-05)
490
- * (Garfonso) fixed: possible crashes during startup
491
-
492
- ### 4.1.4 (2024-02-10)
493
- * (Garfonso) improved fix: lamp icons now turn gray on switch off.
494
-
495
496
  ## License
496
497
 
497
498
  Copyright 2019-2024, bluefox <dogafox@gmail.com>
@@ -657,6 +657,7 @@
657
657
  "type": "checkbox",
658
658
  "label": "labelHasTime",
659
659
  "hidden": "data.entity !== 'input_datetime'",
660
+ "default": false,
660
661
  "onChange": {
661
662
  "alsoDependsOn": ["entity"],
662
663
  "calculateFunc": "data.entity === 'input_datetime' ? false : undefined",
@@ -671,6 +672,7 @@
671
672
  "type": "checkbox",
672
673
  "label": "labelHasDate",
673
674
  "hidden": "data.entity !== 'input_datetime'",
675
+ "default": true,
674
676
  "onChange": {
675
677
  "alsoDependsOn": ["entity"],
676
678
  "calculateFunc": "data.entity === 'input_datetime' ? true : undefined",
@@ -47,139 +47,12 @@ Dazu können die folgenden YAML Codebeispiele zum eigenen Theme hinzugefügt wer
47
47
  }
48
48
  ```
49
49
 
50
- ### 3. Titelleiste vollständig verbergen
50
+ ### 4. Titelleiste vollständig verbergen
51
51
 
52
52
  Ab Version 4.0.1 gibt es die Möglichkeit den State `lovelace.0.instances.hideHeader` auf `true` zu setzen, was
53
53
  (nach einem reload) die Titelleiste bei allen Browsern entfernt. Der State existiert auch für alle Instanzen. Er kann
54
54
  also auch pro Browser gesetzt werden.
55
55
 
56
- ### Zu beachten:
57
- * Die Erweiterung [Cardmod](https://github.com/thomasloven/lovelace-card-mod/tree/master) muss installiert sein.
58
- * Der Code muss Teil des Themes sein (*Lovelace Instanz -> Einstellungen -> Themen*), also z.B. so:
59
-
60
- <details>
61
- <summary>Beispiel Theme</summary>
62
-
63
- ```yaml
64
- synthwave:
65
- # text
66
- primary-text-color: '#fff'
67
- secondary-text-color: '#ffffffca'
68
- text-primary-color: '#f4eee4'
69
- disabled-text-color: '#bdbdbd'
70
-
71
- # main interface colors
72
- primary-color: '#f92aad'
73
- primary-background-color: '#2a2139'
74
- dark-primary-color: '#f92aad'
75
- light-primary-color: '#241b2f'
76
- accent-color: '#f92aad'
77
- divider-color: '#49549539'
78
- paper-dialog-button-color: '#fff'
79
- switch-unchecked-button-color: '#fff'
80
- # iron-icon-fill-color: '#fff'
81
- yellow: '#ffcc00'
82
- green: '#72f1b8cc'
83
-
84
- ###
85
-
86
- # background and sidebar
87
- card-background-color: '#34294fe6'
88
- app-header-background-color: 'var(--primary-background-color)'
89
- paper-card-background-color: 'var(--card-background-color)'
90
- secondary-background-color: 'var(--light-primary-color)' # behind the cards on state
91
- ha-card-border-radius: '8px'
92
-
93
- # sidebar menu
94
- sidebar-text-color: 'var(--secondary-text-color)'
95
- # sidebar-background-color: 'var(--paper-listbox-background-color)' # backward compatible with existing themes
96
- sidebar-icon-color: 'var(--secondary-text-color)'
97
- sidebar-selected-text-color: 'var(--primary-text-color)'
98
- sidebar-selected-icon-color: 'var(--primary-text-color)'
99
-
100
- # mwc - for some reason it's buttons
101
- mdc-theme-primary: 'var(--dark-primary-color)'
102
- mdc-theme-secondary: 'var(--dark-primary-color)'
103
-
104
- # shadows
105
- ha-card-box-shadow: '0'
106
-
107
- # icons
108
- paper-item-icon-color: 'var(--secondary-text-color)' # Off
109
- paper-item-icon-active-color: 'var(--yellow)' # On
110
-
111
- # switches
112
- toggle-button-color: 'var(--primary-color)'
113
- # --toggle-button-unchecked-color: 'var(--accent-color)'
114
- paper-toggle-button-checked-button-color: 'var(--primary-text-color)' # Knob On
115
- paper-toggle-button-checked-bar-color: 'var(--dark-primary-color)' # Background On
116
- switch-checked-color: 'var(--dark-primary-color)' # Background On
117
- paper-toggle-button-unchecked-button-color: 'var(--primary-text-color)' # Knob Off
118
- paper-toggle-button-unchecked-bar-color: 'var(--disabled-text-color)' # Background Off
119
-
120
- # Sliders
121
- slider-color: 'var(--primary-color)'
122
- slider-secondary-color: 'var(--light-primary-color)'
123
- slider-bar-color: 'var(--disabled-text-color)'
124
- paper-slider-knob-color: 'var(--accent-color)'
125
- paper-slider-knob-start-color: 'var(--accent-color)'
126
- paper-slider-pin-color: 'var(--accent-color)'
127
- paper-slider-active-color: 'var(--dark-primary-color)'
128
- # paper-slider-container-color: 'linear-gradient(var(--primary-background-color), var(--secondary-background-color)) no-repeat'
129
- paper-slider-secondary-color: 'var(--secondary-background-color)'
130
- paper-slider-disabled-active-color: 'var(--disabled-text-color)'
131
- paper-slider-disabled-secondary-color: 'var(--disabled-text-color)'
132
- switch-unchecked-track-color: 'var(--primary-text-color)'
133
-
134
- # radio buttons
135
- paper-radio-button-checked-color: 'var(--paper-toggle-button-checked-button-color)'
136
- paper-radio-button-unchecked-color: 'var(--paper-toggle-button-unchecked-button-color)'
137
-
138
- # other
139
- state-icon-color: 'var(--green)'
140
- table-row-background-color: 'var(--divider-color)'
141
- table-row-alternative-background-color: 'var(--light-primary-color)'
142
-
143
- ###
144
-
145
- # UI
146
- paper-card-header-color: 'var(--text-primary-color)' # Title in settings
147
-
148
- # Left Menu
149
- paper-listbox-background-color: 'var(--light-primary-color)' # Background
150
- sidebar-background-color: 'var(--light-primary-color)'
151
-
152
- # bar-card compatibility
153
- # https://github.com/custom-cards/bar-card
154
- custom-bar-card-color: 'var(--accent-color)'
155
-
156
- # fix dropdown background
157
- material-background-color: 'var(--light-primary-color)'
158
-
159
- # Scrollbar
160
- scrollbar-thumb-color: 'var(--divider-color)'
161
-
162
- # simple-thermostat buttons
163
- # https://github.com/nervetattoo/simple-thermostat
164
- st-mode-background: 'var(--primary-background-color)'
165
- st-mode-active-background: 'var(--dark-primary-color)'
166
-
167
- card-mod-theme: synthwave
168
- card-mod-root: |
169
- mwc-icon-button[label] {
170
- display: none;
171
- }
172
- ha-icon-button[slot="actionItems"] {
173
- display: none;
174
- }
175
-
176
-
177
-
178
- ```
179
-
180
- </details>
181
-
182
-
183
56
  ## Mini-Media-Card mit Text2Speech (TTS) und Musik-Shortcuts
184
57
 
185
58
  Die Mini-Media-Card unterstützt für Smarte-Lautsprecher (Echo, Google Home, ...) eine Text to Speech (TTS) Eingabe. Zusätzlich kann
package/io-package.json CHANGED
@@ -1,8 +1,34 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "lovelace",
4
- "version": "4.1.9",
4
+ "version": "4.1.11",
5
5
  "news": {
6
+ "4.1.11": {
7
+ "en": "convert string state values to numbers, where necessary.",
8
+ "de": "string-zustandswerte gegebenenfalls in zahlen umwandeln.",
9
+ "ru": "конвертировать значения состояния строки в числа, когда это необходимо.",
10
+ "pt": "converter valores de estado de cadeia para números, quando necessário.",
11
+ "nl": "converteer string state waarden naar getallen, waar nodig.",
12
+ "fr": "convertir les valeurs d'état de chaîne en nombres, si nécessaire.",
13
+ "it": "convertire i valori di stato della stringa in numeri, ove necessario.",
14
+ "es": "convertir valores de estado de cadena a números, cuando sea necesario.",
15
+ "pl": "w razie potrzeby przekonwertować wartości stanu string do liczb.",
16
+ "uk": "перетворюйте значення стану рядків до чисел, де необхідно.",
17
+ "zh-cn": "必要时将字符串状态值转换为数字."
18
+ },
19
+ "4.1.10": {
20
+ "en": "device icons work again (if authorization is required).\ndefault user sometimes was not found in system.",
21
+ "de": "gerätesymbole funktionieren wieder (wenn authorisierung notwendig).\nder eingestellte standard-benutzer wurde manchmal nicht im system gefunden.",
22
+ "ru": "иконки устройства снова работают.\nпользователь по умолчанию иногда не был найден в системе.",
23
+ "pt": "ícones do dispositivo funcionam novamente.\nusuário padrão às vezes não foi encontrado no sistema.",
24
+ "nl": "apparaatpictogrammen werken opnieuw.\nde standaardgebruiker werd soms niet gevonden in het systeem.",
25
+ "fr": "les icônes du périphérique fonctionnent à nouveau.\nl'utilisateur par défaut n'a parfois pas été trouvé dans le système.",
26
+ "it": "le icone del dispositivo funzionano di nuovo.\nutente predefinito a volte non è stato trovato nel sistema.",
27
+ "es": "los iconos del dispositivo funcionan de nuevo.\nusuario predeterminado a veces no se encontró en el sistema.",
28
+ "pl": "ikony urządzenia działają ponownie.\ndomyślny użytkownik czasami nie został znaleziony w systemie.",
29
+ "uk": "знову працюють іконки пристрою.\nне знайдено типовий користувач.",
30
+ "zh-cn": "设备图标再次工作.\n默认用户有时在系统中找不到 ."
31
+ },
6
32
  "4.1.9": {
7
33
  "en": "add support for new service call structure.\nadd support for delivering files from other adapters, for example, local cover images.\ncleaned up service descriptions.",
8
34
  "de": "unterstützung für neue service call-struktur hinzufügen.\nhinzufügen von unterstützung für die bereitstellung von dateien von anderen adaptern, zum beispiel lokale cover-bilder.\ngereinigte servicebeschreibungen.",
@@ -49,14 +75,6 @@
49
75
  "pl": "stałe: możliwe awarie podczas uruchamiania",
50
76
  "uk": "виправлено: можливі аварії під час запуску",
51
77
  "zh-cn": "固定: 启动时可能发生的崩溃"
52
- },
53
- "4.1.4": {
54
- "en": "improved fix: lamp icons now turn gray on switch off.",
55
- "de": "verbesserter fix: lampensymbole werden nun grau beim ausschalten."
56
- },
57
- "4.1.3": {
58
- "en": "prevent warning for browser_mod/recall_id service call\nfix: lamp icons now turn gray on switch off.\nfix: notifications via sendTo work again.",
59
- "de": "Verhindere warnung für browser_mod/recall_id service call\nfix: Lampensymbole werden nun grau beim ausschalten.\nfix: Benachrichtigung mittels sendTo funktionieren wieder."
60
78
  }
61
79
  },
62
80
  "titleLang": {
@@ -103,7 +121,6 @@
103
121
  "platform": "Javascript/Node.js",
104
122
  "tier": 3,
105
123
  "icon": "lovelace.png",
106
- "main": "main.js",
107
124
  "enabled": true,
108
125
  "extIcon": "https://raw.githubusercontent.com/ioBroker/ioBroker.lovelace/master/admin/lovelace.png",
109
126
  "readme": "https://github.com/ioBroker/ioBroker.lovelace/blob/master/README.md",
@@ -111,7 +128,9 @@
111
128
  "mode": "daemon",
112
129
  "type": "visualization",
113
130
  "compact": true,
114
- "materialize": true,
131
+ "adminUI": {
132
+ "config": "materialize"
133
+ },
115
134
  "supportCustoms": true,
116
135
  "messagebox": true,
117
136
  "connectionType": "local",
@@ -308,6 +327,18 @@
308
327
  "def": false
309
328
  }
310
329
  },
330
+ {
331
+ "_id": "info.configUpdateProcessed",
332
+ "type": "state",
333
+ "common": {
334
+ "name": "If a config update was processed",
335
+ "type": "boolean",
336
+ "read": true,
337
+ "write": false,
338
+ "role": "state",
339
+ "def": false
340
+ }
341
+ },
311
342
  {
312
343
  "_id": "info.readyForClients",
313
344
  "type": "state",
@@ -149,6 +149,13 @@ function _lightAdvancedAddColorTemperature(states, objects, entity) {
149
149
  entity.attributes.max_color_temp_kelvin = max;
150
150
  }
151
151
 
152
+ if (attribute.convert_to_mired) {
153
+ entity.attributes.max_mireds = tempObj?.common?.max || 1e6 / entity.attributes.min_color_temp_kelvin;
154
+ entity.attributes.min_mireds = tempObj?.common?.min || 1e6 / entity.attributes.max_color_temp_kelvin;
155
+ } else {
156
+ entity.attributes.max_mireds = 1e6 / entity.attributes.min_color_temp_kelvin;
157
+ entity.attributes.min_mireds = 1e6 / entity.attributes.max_color_temp_kelvin;
158
+ }
152
159
  entity.attributes.supported_color_modes.push(COLOR_TEMP);
153
160
  }
154
161
  }
@@ -312,7 +312,8 @@ function processCommon(name, room, func, obj, entityType, entity_id) {
312
312
  type: getEntityType(entityType, entity_id, obj),
313
313
  room: room,
314
314
  func: func,
315
- ids: [obj._id]
315
+ ids: [obj._id],
316
+ stateType: obj.common?.type
316
317
  }
317
318
  };
318
319
 
@@ -43,8 +43,13 @@ class BrowserModModule {
43
43
  } else {
44
44
  await this.adapter.setObjectNotExistsAsync(ioBrokerDeviceId, {
45
45
  type: 'device',
46
- common: {name: browserId},
47
- native: {instance: browserId}
46
+ common: {
47
+ name: browserId,
48
+ statusStates: {
49
+ onlineId: ioBrokerDeviceId + '.online'
50
+ }
51
+ },
52
+ native: {instance: browserId},
48
53
  });
49
54
  this.adapter.log.info('New browser_mod instance ' + browserId);
50
55
  }
@@ -221,6 +221,7 @@ class HistoryModule {
221
221
  constructor(options) {
222
222
  this.adapter = options.adapter;
223
223
  this.entityData = options.entityData;
224
+ this.personModule = options.personModule;
224
225
  }
225
226
 
226
227
  async processRequest(req, res) {
@@ -233,7 +234,7 @@ class HistoryModule {
233
234
  entities.push(entity || id);
234
235
  }
235
236
 
236
- const newResult = await getHistory(this.adapter, entities, new Date(req.params.start).getTime(), new Date(req.query.end_time).getTime(), req.query.noAttributes, req._user || this.adapter.config.defaultUser);
237
+ const newResult = await getHistory(this.adapter, entities, new Date(req.params.start).getTime(), new Date(req.query.end_time).getTime(), req.query.noAttributes, this.personModule.getUserIDFromName(req._user));
237
238
  const oldResult = [];
238
239
  for (const [entity_id, states] of Object.entries(newResult)) {
239
240
  const entityResult = [];
@@ -307,7 +308,7 @@ class HistoryModule {
307
308
  entities.push(entity || id);
308
309
  }
309
310
 
310
- const historyData = await getHistory(this.adapter, entities, parameters.startTime, Date.now(), parameters.noAttributes, this.adapter.config.defaultUser);
311
+ const historyData = await getHistory(this.adapter, entities, parameters.startTime, Date.now(), parameters.noAttributes, this.personModule.getUserIDFromName(ws.__auth?.username));
311
312
  sendHistoryResponse(ws, message.id, historyData, parameters);
312
313
  return true;
313
314
  }
@@ -28,24 +28,36 @@ class PersonModule {
28
28
  * @returns {string}
29
29
  */
30
30
  getUserIDFromName(name) {
31
+ if (!this.adapter.config.auth || name === this.adapter.config.defaultUser) {
32
+ return this.adapter.config.defaultUser;
33
+ }
34
+
35
+ if (typeof name !== 'string') { //prevent error if no user supplied.
36
+ throw new Error('Username supplied is not a string, can not find id: ' + name);
37
+ }
38
+
31
39
  for (const userObj of Object.values(this.usersCache)) {
32
40
  if (userObj.name === name) {
33
41
  return userObj.iobId;
34
42
  }
35
43
  }
36
44
 
37
- //fallback, default user "admin" is misleading, if user renamed admin.. hm. :-/
38
- if (this.adapter.config.auth && name === 'admin') {
39
- return 'system.user.admin';
40
- }
41
-
42
45
  this.adapter.log.warn(`Could not get user id for ${name} - Trying with username` + JSON.stringify(this.usersCache['system.user.' + name.toLowerCase()]));
43
46
  return 'system.user.' + name.toLowerCase(); //hack and not correct since js-controller 3.2
44
47
  }
45
48
 
49
+ /**
50
+ * Get username for id. Needed for default user -> name conversion, for example.
51
+ * @param {string} id
52
+ * @returns {string | undefined}
53
+ */
54
+ getUserNameFromID(id) {
55
+ return this.usersCache[id]?.name;
56
+ }
57
+
46
58
  onObjectChange(id, obj) {
47
59
  if (id.startsWith('system.user.')) {
48
- if (obj && obj.common && obj.common.enabled()) {
60
+ if (obj && obj.common && obj.common.enabled) {
49
61
  this.usersCache[id] = {
50
62
  iobId: obj._id,
51
63
  name: obj.common.name || '',
@@ -62,6 +74,9 @@ class PersonModule {
62
74
 
63
75
  async init() {
64
76
  const userObjects = await this.adapter.getForeignObjectsAsync('system.user.*', 'user');
77
+ if (!this.adapter.config.defaultUser.startsWith('system.user.')) { //augment default user to be full id here once.
78
+ this.adapter.config.defaultUser = `system.user.${this.adapter.config.defaultUser}`;
79
+ }
65
80
  let defaultUserObject = null;
66
81
  for (const [id, obj] of Object.entries(userObjects)) {
67
82
  if (obj.common && obj.common.enabled) { //only show enabled persons?
@@ -72,7 +87,7 @@ class PersonModule {
72
87
  picture: obj.common.icon,
73
88
  description: obj.common.desc
74
89
  };
75
- if (obj.common.name === this.adapter.config.defaultUser) {
90
+ if (id === this.adapter.config.defaultUser) {
76
91
  defaultUserObject = obj;
77
92
  }
78
93
  }
@@ -80,16 +95,14 @@ class PersonModule {
80
95
 
81
96
  //default user only relevant for !auth.
82
97
  if (!defaultUserObject) {
83
- //Ok, we did not find defaultUser object. Might be renamed admin?
84
- defaultUserObject = userObjects['system.user.admin'];
85
- this.adapter.config.defaultUser = defaultUserObject.common.name;
86
- if (this.adapter.config.defaultUser !== 'admin' && !this.adapter.config.auth) {
87
- //Activating auth will hide this setting. Not sure what to do then... In general: all users will be able to read everything in this case.
88
- this.adapter.log.warn(`Could not find default user ${this.adapter.config.defaultUser} - Using admin - Please update your configuration.`);
98
+ if (!this.adapter.config.auth) {
99
+ const configuredUserNotFound = `Could not find default user ${this.adapter.config.defaultUser}. Please update your configuration.`;
100
+ this.adapter.log.error(configuredUserNotFound);
101
+
102
+ throw new Error(configuredUserNotFound);
89
103
  }
90
104
  }
91
105
 
92
-
93
106
  await this.adapter.subscribeObjectsAsync('system.user.*');
94
107
  }
95
108
  }
@@ -1,5 +1,6 @@
1
1
  const {WebSocket} = require('ws');
2
2
  const {updateTimestamps, processCommon} = require('../entities/utils');
3
+ const crypto = require('crypto');
3
4
 
4
5
  const TodoItemStatus = {
5
6
  NeedsAction: 'needs_action',
@@ -122,7 +123,7 @@ class TodoModule {
122
123
  const todoList = await this._getTodoList(entity);
123
124
  todoList.items.push({
124
125
  name: message.name,
125
- uid: Math.floor(Math.random() * Date.now()),
126
+ uid: crypto.randomUUID(),
126
127
  status: TodoItemStatus.NeedsAction,
127
128
  due: null,
128
129
  description: null
@@ -220,7 +221,7 @@ class TodoModule {
220
221
  const todoList = await this._getTodoList(entity);
221
222
  const item = {
222
223
  summary: data.service_data.item,
223
- uid: Math.floor(Math.random() * Date.now()),
224
+ uid: crypto.randomUUID(),
224
225
  status: TodoItemStatus.NeedsAction,
225
226
  due: null,
226
227
  description: null
package/lib/server.js CHANGED
@@ -165,10 +165,6 @@ class WebServer {
165
165
  adapter: this.adapter,
166
166
  objects: this._objectData.objects
167
167
  }),
168
- history: new HistoryModule({
169
- adapter: this.adapter,
170
- entityData: entityData
171
- }),
172
168
  conversation: new ConversationModule({
173
169
  adapter: this.adapter,
174
170
  sendResponse: this._sendResponse,
@@ -198,6 +194,11 @@ class WebServer {
198
194
  adapter: this.adapter
199
195
  })
200
196
  };
197
+ this._modules.history = new HistoryModule({
198
+ adapter: this.adapter,
199
+ entityData: entityData,
200
+ personModule: this._modules.person
201
+ });
201
202
 
202
203
  this.converter = {
203
204
  [Types.socket]: converterSwitch.processSocket,
@@ -276,6 +277,13 @@ class WebServer {
276
277
  }
277
278
  this.adapter.setState('info.readyForClients', true, true);
278
279
  this.log.debug('Initialization done.');
280
+ }).catch((err) => {
281
+ this.log.error(`Initialization error: ${err}`);
282
+ if (typeof this.adapter.terminate === 'function') {
283
+ this.adapter.terminate(utils.EXIT_CODES.INVALID_ADAPTER_CONFIG);
284
+ } else {
285
+ process.exit(utils.EXIT_CODES.INVALID_ADAPTER_CONFIG);
286
+ }
279
287
  });
280
288
  }
281
289
 
@@ -297,6 +305,7 @@ class WebServer {
297
305
  }
298
306
  await this._getAllStates();
299
307
  await this._manageSubscribesFromConfig();
308
+ this.log.debug('entitiesUpdated for startup.');
300
309
  await this.adapter.setStateAsync('info.entitiesUpdated', true, true);
301
310
  }
302
311
 
@@ -525,7 +534,7 @@ class WebServer {
525
534
  }
526
535
 
527
536
  async _processSingleCall(ws, data, entity_id) {
528
- const user = this._modules.person.getUserIDFromName(ws.__auth.username || this.config.defaultUser);
537
+ const user = this._modules.person.getUserIDFromName(ws.__auth?.username);
529
538
 
530
539
  const entity = entityData.entityId2Entity[entity_id];
531
540
  const id = entity.context.STATE.setId;
@@ -548,7 +557,7 @@ class WebServer {
548
557
  } else if (data.service === 'volume_set') {
549
558
  this.log.debug('volume_set ' + id);
550
559
 
551
- this.adapter.setForeignState(id, data.service_data.value, false, {user}, () =>
560
+ this.adapter.setForeignState(id, Number(data.service_data.value), false, {user}, () =>
552
561
  this._sendResponse(ws, data.id));
553
562
  } else if (data.service === 'trigger' || data.service === 'turn_on' || data.service === 'unlock' || data.service === 'press') {
554
563
  this.log.debug(`${data.service} ${id}`);
@@ -567,7 +576,7 @@ class WebServer {
567
576
  if (entity.context.ATTRIBUTES) {
568
577
  const attr = entity.context.ATTRIBUTES.find(attr => attr.attribute === 'temperature');
569
578
  if (attr) {
570
- return this.adapter.setForeignState(attr.setId, data.service_data.temperature, false, {user}, () =>
579
+ return this.adapter.setForeignState(attr.setId, Number(data.service_data.temperature), false, {user}, () =>
571
580
  this._sendResponse(ws, data.id));
572
581
  }
573
582
  }
@@ -577,6 +586,7 @@ class WebServer {
577
586
  } else if (data.service === 'set_operation_mode') {
578
587
  this.log.debug(`set_operation_mode ${data.service_data.operation_mode}`);
579
588
 
589
+ //TODO: just sending false here probably is wrong. The call is supported only be Waterheater entity. So... not really used, right now?
580
590
  this.adapter.setForeignState(id, false, false, {user}, () =>
581
591
  this._sendResponse(ws, data.id));
582
592
  } else if (data.service === 'set_page') {
@@ -614,6 +624,9 @@ class WebServer {
614
624
  val = data.service_data[data.service.substring(4)]; //fallback if undefined.
615
625
  }
616
626
  }
627
+ if (entity.context.stateType === 'number') {
628
+ val = Number(val);
629
+ }
617
630
 
618
631
  this.adapter.setForeignState(id, val, false, {user}, () =>
619
632
  this._sendResponse(ws, data.id));
@@ -696,7 +709,7 @@ class WebServer {
696
709
  entity.state = 'unknown';
697
710
  if (entity.context.STATE && entity.context.STATE.getId) {
698
711
  try {
699
- const user = this._modules.person.getUserIDFromName(this.config.defaultUser); // TODO: why is this always defaultUser?
712
+ const user = this.config.defaultUser;
700
713
  const state = await this.adapter.getForeignStateAsync(entity.context.STATE.getId, {user});
701
714
  if (state) {
702
715
  await this.onStateChange(entity.context.STATE.getId, state);
@@ -1495,7 +1508,7 @@ class WebServer {
1495
1508
  file = file.substring(0, pos);
1496
1509
  }
1497
1510
  try {
1498
- const user = this._modules.person.getUserIDFromName(this.config.defaultUser); // TODO: why is this always default user?
1511
+ const user = this._modules.person.getUserIDFromName(req._user);
1499
1512
  let data;
1500
1513
  if (file.startsWith('/lovelace/')) {
1501
1514
  file = file.replace('/lovelace/', '');
@@ -1519,7 +1532,7 @@ class WebServer {
1519
1532
  file = file.substring(0, pos);
1520
1533
  }
1521
1534
  try {
1522
- const user = this._modules.person.getUserIDFromName(this.config.defaultUser); // TODO: why is this always default user?
1535
+ const user = this._modules.person.getUserIDFromName(req._user);
1523
1536
  const data = await this.adapter.readFileAsync(this.adapter.namespace, file.replace('/local/custom_ui/', '/cards/'), {user});
1524
1537
  const pos = req.url.lastIndexOf('.');
1525
1538
  res.setHeader('content-type', (mime.getType || mime.lookup).call(data.mimeType, file.substring(pos + 1).toLowerCase()));
@@ -1715,6 +1728,7 @@ class WebServer {
1715
1728
 
1716
1729
  // on http://localhost:3000/auth/authorize?response_type=code&client_id=http%3A%2F%2Flocalhost%3A3000%2F&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Flovelace%3Fauth_callback%3D1&state=eyJoYXNzVXJsIjoiaHR0cDovL2xvY2FsaG9zdDozMDAwIiwiY2xpZW50SWQiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAvIn0%3D
1717
1730
  this._app.get('/auth/authorize', (req, res) => {
1731
+ this.log.debug('PRO-debug: auth/authorize - started.');
1718
1732
  res.setHeader('location', `/frontend_es5/authorize.html${req.url.replace(/^\//, '').replace(/^auth\/authorize/, '')}`);
1719
1733
  res.status(302).send();
1720
1734
  });
@@ -1725,6 +1739,7 @@ class WebServer {
1725
1739
 
1726
1740
  this._app.post('/auth/login_flow', (req, res) => {
1727
1741
  console.log(`/auth/login_flow${JSON.stringify(req.query)}${JSON.stringify(req.body)}`);
1742
+ this.log.debug('PRO-debug: /auth/login_flow');
1728
1743
 
1729
1744
  generateRandomToken((_err, token) => {
1730
1745
  this._auth_flows[token] = {ts: Date.now()};
@@ -1864,15 +1879,17 @@ class WebServer {
1864
1879
  }
1865
1880
  }
1866
1881
  }
1867
- } else {
1868
- req._user = this.config.defaultUser;
1869
1882
  }
1870
1883
 
1884
+ //if no auth or page does not require auth, use default user:
1885
+ //invalid token run into return above and close connection.
1886
+ req._user = req._user || this.config.defaultUser;
1887
+
1871
1888
  next();
1872
1889
  }
1873
1890
  });
1874
1891
 
1875
- //handle local images that are content of some states:
1892
+ //handle local images that are content of some states:
1876
1893
  this._app.use(async (req, res, next) => {
1877
1894
  if (this._requestableFiles.includes(req.url)) {
1878
1895
  if (!req._user) { //sadly frontend does not send auth info with most request.... :-/
@@ -1925,7 +1942,7 @@ class WebServer {
1925
1942
  if (obj && obj.common.type === 'file') {
1926
1943
  contentType = (mime.getType || mime.lookup).call(mime, fileName[0]);
1927
1944
  }
1928
- const user = this._modules.person.getUserIDFromName(this.config.defaultUser);
1945
+ const user = this._modules.person.getUserIDFromName(req._user);
1929
1946
  const data = await this.adapter.getBinaryStateAsync(fileName[0], {user});
1930
1947
  if (data !== null && obj !== undefined) {
1931
1948
  if (data && typeof data === 'object' && data.val !== undefined && data.ack !== undefined) {
@@ -1994,7 +2011,7 @@ class WebServer {
1994
2011
  return res.status(404).json({error: 'Start or end misformated'});
1995
2012
  }
1996
2013
 
1997
- const user = this._modules.person.getUserIDFromName(this.config.defaultUser);
2014
+ const user = this._modules.person.getUserIDFromName(req._user);
1998
2015
  try {
1999
2016
  const state = await this.adapter.getForeignStateAsync(entity.context.STATE.getId, {user});
2000
2017
  if (state && state.val) {
@@ -2076,10 +2093,10 @@ class WebServer {
2076
2093
  }
2077
2094
 
2078
2095
  async _getCurrentUser(ws) {
2079
- const user = this._modules.person.getUserIDFromName(ws.__auth.username || this.config.defaultUser);
2096
+ const user = this._modules.person.getUserIDFromName(ws.__auth.username);
2080
2097
  const userObj = {
2081
2098
  id: user,
2082
- name: ws.__auth.username || this.config.defaultUser,
2099
+ name: ws.__auth.username || this._modules.person.getUserNameFromID(this.config.defaultUser),
2083
2100
  is_owner: user === 'system.user.admin',
2084
2101
  is_admin: user === 'system.user.admin',
2085
2102
  credentials: [{auth_provider_type: 'iobroker', auth_provider_id: null}],
@@ -2165,7 +2182,7 @@ class WebServer {
2165
2182
  throw new Error('no entity found');
2166
2183
  } else {
2167
2184
  let id;
2168
- let userName = this.config.defaultUser;
2185
+ let userName; // will be ignored in case of no authentication enabled.
2169
2186
  if (this.config.auth !== false && (token || access_token)) {
2170
2187
  if (access_token) {
2171
2188
  const now = Date.now();
@@ -2275,6 +2292,7 @@ class WebServer {
2275
2292
 
2276
2293
  ws.on('error', e => {
2277
2294
  console.error(`Error: ${e}`);
2295
+ this.log.debug('PRO-debug: ws error: ' + e + ' - ' + e.stack);
2278
2296
  clearInterval(testTimer);
2279
2297
  testTimer = null;
2280
2298
  });
@@ -2287,9 +2305,20 @@ class WebServer {
2287
2305
 
2288
2306
  //connection is up, let's add a simple event
2289
2307
  ws.on('message', async message => {
2308
+ this.log.debug('PRO-debug: ws message received');
2309
+ if (typeof message !== 'string') {
2310
+ //try to convert to string here?
2311
+ if (message instanceof Buffer) {
2312
+ this.log.debug('PRO-debug: ws message is buffer');
2313
+ message = message.toString('utf8');
2314
+ }
2315
+ }
2316
+
2290
2317
  try {
2291
2318
  message = JSON.parse(message);
2292
2319
  } catch (e) {
2320
+ this.log.debug(`Could not parse message: ${message} with type ${typeof message}, got error ${e} with stack ${e.stack}`);
2321
+ this.log.debug('PRO-debug stringified message: ' + JSON.stringify(message));
2293
2322
  console.error(`Cannot parse message: ${message}`);
2294
2323
  return;
2295
2324
  }
@@ -2528,6 +2557,7 @@ class WebServer {
2528
2557
 
2529
2558
  ws.on('close', () => {
2530
2559
  this.log.debug('Connection closed');
2560
+ this.log.debug('PRO-debug: ws close');
2531
2561
  ws._subscribes = null;
2532
2562
  ws.__templates = null;
2533
2563
  clearInterval(testTimer);
@@ -2669,7 +2699,8 @@ class WebServer {
2669
2699
  if (id === `${this.adapter.namespace}.configuration`) {
2670
2700
  this._lovelaceConfig = obj.native;
2671
2701
  await this._manageSubscribesFromConfig();
2672
- await this.adapter.setStateAsync('info.entitiesUpdated', true, true);
2702
+ this.log.debug(`configUpdateProcessed for config.`);
2703
+ await this.adapter.setStateAsync('info.configUpdateProcessed', true, true);
2673
2704
  } else if (id === 'system.config') {
2674
2705
  if (obj && obj.common) {
2675
2706
  this.lang = obj.common.language || this.lang || 'en';
@@ -2677,6 +2708,7 @@ class WebServer {
2677
2708
  this.systemConfig = obj.common;
2678
2709
  this._updateConstantEntities();
2679
2710
  this.log.debug(`${id} -> config updated, constant entities updated.`);
2711
+ this.log.debug('entitiesUpdated for system.config.');
2680
2712
  await this.adapter.setStateAsync('info.entitiesUpdated', true, true);
2681
2713
  }
2682
2714
  } else {
@@ -2753,6 +2785,7 @@ class WebServer {
2753
2785
  for (const id of idsWithUpdate) {
2754
2786
  await this.onStateChange(id, null, true);
2755
2787
  }
2788
+ this.log.debug('entitiesUpdated for object changes.');
2756
2789
  await this.adapter.setStateAsync('info.entitiesUpdated', true, true);
2757
2790
  this.log.debug('Had changes, updated states and notified entitiesUpdated state.');
2758
2791
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.lovelace",
3
- "version": "4.1.9",
3
+ "version": "4.1.11",
4
4
  "description": "With this adapter you can build visualization for ioBroker with Home Assistant Lovelace UI",
5
5
  "author": {
6
6
  "name": "bluefox",
@@ -24,9 +24,9 @@
24
24
  "dependencies": {
25
25
  "@iobroker/adapter-core": "^3.1.4",
26
26
  "@iobroker/webserver": "^1.0.3",
27
- "axios": "^1.6.8",
28
- "body-parser": "^1.20.2",
29
- "express": "^4.19.2",
27
+ "axios": "^1.7.7",
28
+ "body-parser": "^1.20.3",
29
+ "express": "^4.21.1",
30
30
  "iobroker.type-detector": "^3.0.5",
31
31
  "js-yaml": "^4.1.0",
32
32
  "jstimezonedetect": "^1.0.7",
@@ -35,7 +35,7 @@
35
35
  "nyc": "^15.1.0",
36
36
  "pinyin": "^3.1.0",
37
37
  "translit-rus-eng": "^1.0.8",
38
- "ws": "^8.16.0"
38
+ "ws": "^8.18.0"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@alcalzone/release-script": "^3.7.0",
@@ -55,7 +55,7 @@
55
55
  "chai": "^4.4.1",
56
56
  "chai-as-promised": "^7.1.1",
57
57
  "eslint": "^8.57.0",
58
- "gulp": "^4.0.2",
58
+ "gulp": "^5.0.0",
59
59
  "mocha": "^10.4.0",
60
60
  "proxyquire": "^2.1.3",
61
61
  "sinon": "^17.0.1",