iobroker.lovelace 4.1.0 → 4.1.1

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,12 @@ After that checkout modified version in `./build` folder. Then.
473
473
  PLACEHOLDER for next version:
474
474
  ### **WORK IN PROGRESS**
475
475
  -->
476
+ ### 4.1.1 (2024-01-02)
477
+ * (Garfonso) changed: determining user id
478
+ * (Garfosno) changed: history attributes handling
479
+ * (Garfonso) added: handle browser_mod/recall_id service call.
480
+ * (Garfonso) changed: all states are strings (fixes #483)
481
+
476
482
  ### 4.1.0 (2023-12-18)
477
483
  * (Garfons) add an option to show users on login screen (off by default)
478
484
 
@@ -488,12 +494,9 @@ After that checkout modified version in `./build` folder. Then.
488
494
  * (Garfonso) fix: login & authorization
489
495
  * (Garfonso) added: user images & names on login screen
490
496
 
491
- ### 4.0.9 (2023-12-12)
492
- * (Garfonso) fixed: timestamp in legacy history data
493
-
494
497
  ## License
495
498
 
496
- Copyright 2019-2023, bluefox <dogafox@gmail.com>
499
+ Copyright 2019-2024, bluefox <dogafox@gmail.com>
497
500
 
498
501
  Licensed under the Apache License, Version 2.0 (the "License");
499
502
  you may not use this file except in compliance with the License.
package/io-package.json CHANGED
@@ -1,8 +1,12 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "lovelace",
4
- "version": "4.1.0",
4
+ "version": "4.1.1",
5
5
  "news": {
6
+ "4.1.1": {
7
+ "en": "changed: determining user id\nchanged: history attributes handling\nadded: handle browser_mod/recall_id service call.\nchanged: all states are strings!",
8
+ "de": "geändert: bestimmung der benutzer-id\ngeändert: historie für attribute\nhinzugefügt: handle browser_mod/recall_id service call.\ngeändert: alle zustände sind strings!"
9
+ },
6
10
  "4.1.0": {
7
11
  "en": "add an option to show users on login screen (off by default)",
8
12
  "de": "Neu Option um Benutzer auf dem Login-Bildschirm anzuzeigen"
@@ -44,19 +48,6 @@
44
48
  "4.0.8": {
45
49
  "en": "re-add legacy history for custom cards",
46
50
  "de": "re-add die alten history methoden für benutzerdefinierte karten"
47
- },
48
- "4.0.7": {
49
- "en": "history should be working again.",
50
- "de": "history sollte wieder funktionieren.",
51
- "ru": "история должна работать снова.",
52
- "pt": "a história deve estar a trabalhar de novo.",
53
- "nl": "de geschiedenis zou weer moeten werken.",
54
- "fr": "l'histoire devrait retravailler.",
55
- "it": "la storia dovrebbe funzionare di nuovo.",
56
- "es": "la historia debe estar trabajando de nuevo.",
57
- "pl": "historia powinna być kontynuowana.",
58
- "uk": "історія повинна працювати знову.",
59
- "zh-cn": "历史应该再次发挥作用。."
60
51
  }
61
52
  },
62
53
  "title": "Visualization with Lovelace-UI",
@@ -237,11 +237,15 @@ function fillClimateEntityFromStates(states, objects, entity, iobType) {
237
237
 
238
238
  //controls hvac_mode which can be 'off' but not 'on', so translate 'on' to heat / cool depending on type.
239
239
  if (states.state || states.stateRead) {
240
+ if (!entity.attributes.hvac_modes) {
241
+ entity.attributes.hvac_modes = [];
242
+ }
243
+ entity.attributes.hvac_modes.push('off');
240
244
  if (!states.hvac_mode) {
241
245
  if (iobType === typeDetector.Types.airCondition) {
242
- entity.attributes.hvac_modes = ['off', 'cool'];
246
+ entity.attributes.hvac_modes.push('cool');
243
247
  } else {
244
- entity.attributes.hvac_modes = ['off', 'heat'];
248
+ entity.attributes.hvac_modes.push('heat');
245
249
  }
246
250
  }
247
251
  entity.context.STATE.getParser = function (entity, attr, state) {
@@ -48,8 +48,6 @@ exports.numericDeviceClasses = [
48
48
  'distance'
49
49
  ];
50
50
 
51
- const numericDeviceClassesIob = exports.numericDeviceClasses.concat(['timestamp']);
52
-
53
51
  exports.iobState2EntityState = function (entity, val, attribute) {
54
52
  let type = entity.context.type;
55
53
  const pos = type.lastIndexOf('.');
@@ -61,14 +59,17 @@ exports.iobState2EntityState = function (entity, val, attribute) {
61
59
  return val ? 'on' : 'off';
62
60
  } else if (type === 'binary_sensor') {
63
61
  return val ? 'on' : 'off';
64
- } else if (typeof val === 'number' && entity.attributes && (numericDeviceClassesIob.includes(entity.attributes.device_class) || entity.attributes.unit_of_measurement)) {
65
- return val; //do not convert timestamps or values that have a unit of measurement..
66
- } else if (typeof val === 'number' && entity.attributes && ['date'].includes(entity.attributes.device_class)) {
67
- const dateStr = new Date(val).toDateString(); //convert to date string
62
+ }else if (typeof val === 'number' && entity.attributes && ['date', 'timestamp'].includes(entity.attributes.device_class)) {
63
+ //convert to date string
64
+ const date = new Date(val);
65
+ let dateStr = date.toDateString();
66
+ if (attribute === 'timestamp') {
67
+ dateStr = date.toISOString();
68
+ }
68
69
  return dateStr === 'Invalid Date' ? 'unknown' : dateStr;
69
70
  } else if (type === 'lock') {
70
71
  return val ? 'unlocked' : 'locked';
71
- } else if (typeof val === 'boolean' && type !== 'media_player') {
72
+ } else if (typeof val === 'boolean' && type !== 'media_player' && !attribute) { //attributes can have true/false.
72
73
  return val ? 'on' : 'off';
73
74
  } else if (typeof val === 'number' && entity.context.STATE.map2lovelace) {
74
75
  return entity.context.STATE.map2lovelace[val] || val;
@@ -344,6 +344,7 @@ class BrowserModModule {
344
344
  instance: message.browserID,
345
345
  ws
346
346
  };
347
+ ws.browserID = message.browserID; //store browserID in ws object to handle recall service call later.
347
348
 
348
349
  ws.send(JSON.stringify([{id: message.id, type: 'result', success: true, result: null}, {
349
350
  id: message.id, type: 'event', event: {
@@ -399,6 +400,8 @@ class BrowserModModule {
399
400
  this.adapter.log.debug('Updated browser_mod settings: ' + message.key + ' to ' + message.value);
400
401
  }
401
402
  //think about making this permanent somehow? -> but only if we find a way to allow browser_mod_settings panel.
403
+ } else if (method === 'recall_id') {
404
+ ws.send(JSON.stringify({id: message.id, type: 'result', success: true, result: ws.browserID}));
402
405
  } else {
403
406
  this.adapter.log.warn('Unknown browser_mod method: ' + JSON.stringify(message));
404
407
  }
@@ -67,7 +67,7 @@ async function getHistory(adapter, entities, start, end, noAttributes, user) {
67
67
  }
68
68
  let found = false;
69
69
  let best = null;
70
- let bestDiff = 1000;
70
+ let bestDiff = 10000;
71
71
  const results = attributesResult[attribute.attribute].result;
72
72
  for (const result of results) {
73
73
  if (result.val !== null) {
@@ -81,10 +81,10 @@ async function getHistory(adapter, entities, start, end, noAttributes, user) {
81
81
  }
82
82
  if (found) {
83
83
  attributeValues[attribute.attribute] = typeof attribute.historyParser === 'function' ? attribute.historyParser(id, best.val) : best.val;
84
- best.used = true;
85
- } else {
84
+ best.used = true; //will be checked later, all "unused" attributes will be added without state later.
85
+ } /*else { //let's try to leave attribute empty, for now, if no value in history that is closer than 10 seconds.
86
86
  attributeValues[attribute.attribute] = entity.attributes[attribute.attribute]; //use current value as default if none found.
87
- }
87
+ }*/
88
88
  }
89
89
  }
90
90
  for (const key of Object.keys(entity.attributes)) {
@@ -103,7 +103,7 @@ async function getHistory(adapter, entities, start, end, noAttributes, user) {
103
103
  s: typeof entity.context.STATE.historyParser === 'function' ?
104
104
  entity.context.STATE.historyParser(id, e.val).toString() :
105
105
  iobState2EntityState(entity, e.val),
106
- a: noAttributes ? undefined : getAttributeValues(e, attributesResult, attributesUsed),
106
+ a: noAttributes ? {} : getAttributeValues(e, attributesResult, attributesUsed),
107
107
  lc: 1,
108
108
  lu: 1
109
109
  };
@@ -113,6 +113,8 @@ async function getHistory(adapter, entities, start, end, noAttributes, user) {
113
113
  //add unused attribute values:
114
114
  if (!noAttributes && entity.context.ATTRIBUTES) {
115
115
  for (const attribute of entity.context.ATTRIBUTES) {
116
+ //find other attributes for this type. Will use this attribute as "state" for the getAttributeValues function.
117
+ // so we don't want to find this attribute again. Will add all matching attributes anyway. So in later runs this attribute will be "empty", i.e. all used.
116
118
  attributesUsed.push(attribute.attribute);
117
119
  const results = attributesResult[attribute.attribute].result;
118
120
  for (const result of results) {
@@ -125,7 +127,7 @@ async function getHistory(adapter, entities, start, end, noAttributes, user) {
125
127
  //state: null,
126
128
  lc: 1,
127
129
  lu: 1,
128
- a: noAttributes ? undefined : getAttributeValues(result, attributesResult, attributesUsed, attributeValues)
130
+ a: noAttributes ? {} : getAttributeValues(result, attributesResult, attributesUsed, attributeValues)
129
131
  };
130
132
  updateTimestamps(data, result, true);
131
133
  historyPerEntity.push(data);
@@ -137,6 +139,7 @@ async function getHistory(adapter, entities, start, end, noAttributes, user) {
137
139
  if (historyPerEntity.length === 0) {
138
140
  historyPerEntity.push({
139
141
  s: entity.state,
142
+ a: {},
140
143
  lu: start / 1000
141
144
  });
142
145
  }
@@ -177,12 +180,12 @@ function sendHistoryResponse(ws, id, historyData, parameters) {
177
180
  state.s = 'unknown';
178
181
  }
179
182
 
180
- if (parameters.noAttributes) {
183
+ /*if (parameters.noAttributes) {
181
184
  delete state.a;
182
185
  }
183
186
  if (parameters.minimalResponse) {
184
187
  delete state.lc;
185
- }
188
+ }*/
186
189
  }
187
190
  }
188
191
 
@@ -275,7 +278,7 @@ class HistoryModule {
275
278
  id: Number(message.id)
276
279
  };
277
280
 
278
- // add subscribe here.
281
+ // add subscription here.
279
282
  ws._subscribes.history = ws._subscribes.history || [];
280
283
  ws._subscribes.history.push(parameters);
281
284
  } else if (message.type === 'history/history_during_period') {
@@ -22,6 +22,27 @@ class PersonModule {
22
22
  return result;
23
23
  }
24
24
 
25
+ /**
26
+ * Get the user id from the username
27
+ * @param {string} name
28
+ * @returns {string}
29
+ */
30
+ getUserIDFromName(name) {
31
+ for (const userObj of Object.values(this.usersCache)) {
32
+ if (userObj.name === name) {
33
+ return userObj.iobId;
34
+ }
35
+ }
36
+
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
+ this.adapter.log.warn(`Could not get user id for ${name} - Trying with username` + JSON.stringify(this.usersCache['system.user.' + name.toLowerCase()]));
43
+ return 'system.user.' + name.toLowerCase(); //hack and not correct since js-controller 3.2
44
+ }
45
+
25
46
  onObjectChange(id, obj) {
26
47
  if (id.startsWith('system.user.')) {
27
48
  if (obj && obj.common && obj.common.enabled()) {
@@ -41,6 +62,7 @@ class PersonModule {
41
62
 
42
63
  async init() {
43
64
  const userObjects = await this.adapter.getForeignObjectsAsync('system.user.*', 'user');
65
+ let defaultUserObject = null;
44
66
  for (const [id, obj] of Object.entries(userObjects)) {
45
67
  if (obj.common && obj.common.enabled) { //only show enabled persons?
46
68
  this.usersCache[id] = {
@@ -50,8 +72,24 @@ class PersonModule {
50
72
  picture: obj.common.icon,
51
73
  description: obj.common.desc
52
74
  };
75
+ if (obj.common.name === this.adapter.config.defaultUser) {
76
+ defaultUserObject = obj;
77
+ }
78
+ }
79
+ }
80
+
81
+ //default user only relevant for !auth.
82
+ 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.`);
53
89
  }
54
90
  }
91
+
92
+
55
93
  await this.adapter.subscribeObjectsAsync('system.user.*');
56
94
  }
57
95
  }
@@ -181,6 +181,9 @@ class TodoModule {
181
181
  if (state && state.val) {
182
182
  try {
183
183
  todoList.items = JSON.parse(state.val);
184
+ if (!todoList.items || !Array.isArray(todoList.items)) {
185
+ todoList.items = [];
186
+ }
184
187
  //convert legacy items:
185
188
  for (const item of todoList.items) {
186
189
  if (item.name && !item.summary) {
package/lib/server.js CHANGED
@@ -518,7 +518,7 @@ class WebServer {
518
518
  }
519
519
 
520
520
  async _processSingleCall(ws, data, entity_id) {
521
- const user = await this._getUserId(ws.__auth.username || this.config.defaultUser);
521
+ const user = this._modules.person.getUserIDFromName(ws.__auth.username || this.config.defaultUser);
522
522
 
523
523
  const entity = entityData.entityId2Entity[entity_id];
524
524
  const id = entity.context.STATE.setId;
@@ -681,7 +681,7 @@ class WebServer {
681
681
  entity.state = 'unknown';
682
682
  if (entity.context.STATE && entity.context.STATE.getId) {
683
683
  try {
684
- const user = await this._getUserId(this.config.defaultUser); //TODO: why is this always defaultUser?
684
+ const user = this._modules.person.getUserIDFromName(this.config.defaultUser); //TODO: why is this always defaultUser?
685
685
  const state = await this.adapter.getForeignStateAsync(entity.context.STATE.getId, {user});
686
686
  if (state) {
687
687
  await this.onStateChange(entity.context.STATE.getId, state);
@@ -1477,7 +1477,7 @@ class WebServer {
1477
1477
  file = file.substring(0, pos);
1478
1478
  }
1479
1479
  try {
1480
- const user = await this._getUserId(this.config.defaultUser); //TODO: why is this always default user?
1480
+ const user = this._modules.person.getUserIDFromName(this.config.defaultUser); //TODO: why is this always default user?
1481
1481
  let data;
1482
1482
  if (file.startsWith('/lovelace/')) {
1483
1483
  file = file.replace('/lovelace/', '');
@@ -1501,7 +1501,7 @@ class WebServer {
1501
1501
  file = file.substring(0, pos);
1502
1502
  }
1503
1503
  try {
1504
- const user = await this._getUserId(this.config.defaultUser); //TODO: why is this always default user?
1504
+ const user = this._modules.person.getUserIDFromName(this.config.defaultUser); //TODO: why is this always default user?
1505
1505
  const data = await this.adapter.readFileAsync(this.adapter.namespace, file.replace('/local/custom_ui/', '/cards/'), {user});
1506
1506
  const pos = req.url.lastIndexOf('.');
1507
1507
  res.setHeader('content-type', (mime.getType || mime.lookup).call(data.mimeType, file.substring(pos + 1).toLowerCase()));
@@ -1844,7 +1844,7 @@ class WebServer {
1844
1844
  }
1845
1845
 
1846
1846
  try {
1847
- const user = await this._getUserId(this.config.defaultUser); //TODO: why is this always default user?
1847
+ const user = this._modules.person.getUserIDFromName(this.config.defaultUser); //TODO: why is this always default user?
1848
1848
  const image = await this.adapter.readFileAsync(id, url, {user});
1849
1849
  if (image.file === null || image.file === undefined) {
1850
1850
  throw new Error('File empty');
@@ -1868,7 +1868,7 @@ class WebServer {
1868
1868
  if (obj && obj.common.type === 'file') {
1869
1869
  contentType = (mime.getType || mime.lookup).call(mime, fileName[0]);
1870
1870
  }
1871
- const user = await this._getUserId(this.config.defaultUser);
1871
+ const user = this._modules.person.getUserIDFromName(this.config.defaultUser);
1872
1872
  const data = await this.adapter.getBinaryStateAsync(fileName[0], {user});
1873
1873
  if (data !== null && obj !== undefined) {
1874
1874
  if (data && typeof data === 'object' && data.val !== undefined && data.ack !== undefined) {
@@ -1937,7 +1937,7 @@ class WebServer {
1937
1937
  return res.status(404).json({error: 'Start or end misformated'});
1938
1938
  }
1939
1939
 
1940
- const user = await this._getUserId(this.config.defaultUser);
1940
+ const user = this._modules.person.getUserIDFromName(this.config.defaultUser);
1941
1941
  try {
1942
1942
  const state = await this.adapter.getForeignStateAsync(entity.context.STATE.getId, {user});
1943
1943
  if (state && state.val) {
@@ -2018,29 +2018,8 @@ class WebServer {
2018
2018
  });
2019
2019
  }
2020
2020
 
2021
- async _getUserId(user) {
2022
- let userId = this._userNamesToIds[user];
2023
- if (userId) {
2024
- return userId;
2025
- }
2026
-
2027
- if (typeof this.adapter.getUserID === 'function') {
2028
- try {
2029
- userId = await this.adapter.getUserID(user);
2030
- this._userNamesToIds[user] = userId;
2031
- } catch (err) {
2032
- this.log.warn(`Could not get user id for ${user} - ${err}`);
2033
- }
2034
- }
2035
- if (!userId) {
2036
- this.log.warn(`Could not get user id for ${user} - Trying with username`);
2037
- userId = 'system.user.' + user.toLowerCase(); //hack and not correct since js-controller 3.2
2038
- }
2039
- return userId;
2040
- }
2041
-
2042
2021
  async _getCurrentUser(ws) {
2043
- const user = await this._getUserId(ws.__auth.username || this.config.defaultUser);
2022
+ const user = this._modules.person.getUserIDFromName(ws.__auth.username || this.config.defaultUser);
2044
2023
  const userObj = {
2045
2024
  id: user,
2046
2025
  name: ws.__auth.username || this.config.defaultUser,
@@ -2159,7 +2138,7 @@ class WebServer {
2159
2138
  }
2160
2139
  if (id) {
2161
2140
  try {
2162
- const user = await this._getUserId(userName);
2141
+ const user = this._modules.person.getUserIDFromName(userName);
2163
2142
  const state = await this.adapter.getForeignStateAsync(id, {user});
2164
2143
  if (state && state.val && typeof state.val === 'string') {
2165
2144
  const val = state.val.split('?')[0] || '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.lovelace",
3
- "version": "4.1.0",
3
+ "version": "4.1.1",
4
4
  "description": "With this adapter you can build visualization for ioBroker with Home Assistant Lovelace UI",
5
5
  "author": {
6
6
  "name": "bluefox",
@@ -20,7 +20,7 @@
20
20
  },
21
21
  "dependencies": {
22
22
  "@iobroker/adapter-core": "^3.0.4",
23
- "axios": "^1.6.2",
23
+ "axios": "^1.6.3",
24
24
  "body-parser": "^1.20.2",
25
25
  "express": "^4.18.2",
26
26
  "iobroker.type-detector": "^3.0.5",
@@ -31,7 +31,7 @@
31
31
  "nyc": "^15.1.0",
32
32
  "pinyin": "^3.1.0",
33
33
  "translit-rus-eng": "^1.0.8",
34
- "ws": "^8.15.1"
34
+ "ws": "^8.16.0"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@alcalzone/release-script": "^3.7.0",
@@ -44,13 +44,13 @@
44
44
  "@types/chai-as-promised": "^7.1.8",
45
45
  "@types/gulp": "^4.0.17",
46
46
  "@types/mocha": "^10.0.6",
47
- "@types/node": "^16.18.68 < 17",
47
+ "@types/node": "^16.18.69 < 17",
48
48
  "@types/proxyquire": "^1.3.31",
49
49
  "@types/sinon": "^17.0.2",
50
50
  "@types/sinon-chai": "^3.2.12",
51
51
  "chai": "^4.3.10",
52
52
  "chai-as-promised": "^7.1.1",
53
- "eslint": "^8.55.0",
53
+ "eslint": "^8.56.0",
54
54
  "gulp": "^4.0.2",
55
55
  "mocha": "^10.2.0",
56
56
  "proxyquire": "^2.1.3",