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 +7 -6
- package/admin/jsonCustom.json +2 -0
- package/docs/de/ui_tipps.md +1 -128
- package/io-package.json +42 -11
- package/lib/converters/light.js +7 -0
- package/lib/entities/utils.js +2 -1
- package/lib/modules/browser_mod.js +7 -2
- package/lib/modules/history.js +3 -2
- package/lib/modules/person.js +27 -14
- package/lib/modules/todo.js +3 -2
- package/lib/server.js +52 -19
- package/package.json +6 -6
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>
|
package/admin/jsonCustom.json
CHANGED
|
@@ -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",
|
package/docs/de/ui_tipps.md
CHANGED
|
@@ -47,139 +47,12 @@ Dazu können die folgenden YAML Codebeispiele zum eigenen Theme hinzugefügt wer
|
|
|
47
47
|
}
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
###
|
|
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.
|
|
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
|
-
"
|
|
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",
|
package/lib/converters/light.js
CHANGED
|
@@ -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
|
}
|
package/lib/entities/utils.js
CHANGED
|
@@ -43,8 +43,13 @@ class BrowserModModule {
|
|
|
43
43
|
} else {
|
|
44
44
|
await this.adapter.setObjectNotExistsAsync(ioBrokerDeviceId, {
|
|
45
45
|
type: 'device',
|
|
46
|
-
common: {
|
|
47
|
-
|
|
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
|
}
|
package/lib/modules/history.js
CHANGED
|
@@ -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,
|
|
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.
|
|
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
|
}
|
package/lib/modules/person.js
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
}
|
package/lib/modules/todo.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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
|
|
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.
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
28
|
-
"body-parser": "^1.20.
|
|
29
|
-
"express": "^4.
|
|
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.
|
|
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": "^
|
|
58
|
+
"gulp": "^5.0.0",
|
|
59
59
|
"mocha": "^10.4.0",
|
|
60
60
|
"proxyquire": "^2.1.3",
|
|
61
61
|
"sinon": "^17.0.1",
|