Advertisement
Guest User

Untitled

a guest
Jul 19th, 2019
107
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 11.81 KB | None | 0 0
  1. /** some helper functions, mmmh, am I the only one needing those? Am I doing something wrong? */
  2. // typical [[1,2,3], [6,7,8]] to [[1, 6], [2, 7], [3, 8]] converter
  3. transpose = m => m[0].map((x, i) => m.map(x => x[i]));
  4.  
  5. // single items -> Array with item with length == 1
  6. listify = obj => ((obj instanceof Array) ? obj : [obj]);
  7.  
  8. // simply return the args, which were passed, mmh not needed anymore here...
  9. //pipe = (...args) => args
  10.  
  11. // a map function, which splits args to multiple vars, python-like
  12. //mmap =
  13.  
  14. // omg, js is still very broken, trouble comparing strings? 80s? plain-C? wtf!
  15. var compare = function(a, b) {
  16. if (typeof a == "string")
  17. return a.localeCompare(b);
  18. else if (typeof b == "string")
  19. return -1 * b.localeCompare(a);
  20. else
  21. return a - b;
  22. }
  23.  
  24. /** flex-table data representation and keeper */
  25. class DataTable {
  26. constructor(cfg) {
  27. this.cols = cfg.columns;
  28. this.cfg = cfg;
  29.  
  30. this.col_ids = this.cols.map(col => col.prop || col.attr || col.attr_as_list);
  31. this.headers = this.cols.filter(col => !col.hidden).map((col, idx) =>
  32. col.name || this.col_ids[idx]);
  33.  
  34. this.rows = [];
  35. }
  36.  
  37. add(...rows) {
  38. this.rows.push(...rows.map(row => row.render_data(this.cols)));
  39. }
  40.  
  41. clear_rows() {
  42. this.rows = [];
  43. }
  44.  
  45. get_rows() {
  46. // sorting is allowed asc/desc for one column
  47. if (this.cfg.sort_by) {
  48. let sort_col = this.cfg.sort_by;
  49. let sort_dir = 1;
  50. if (sort_col) {
  51. if (["-", "+"].includes(sort_col.slice(-1))) {
  52. // "-" => descending, "+" => ascending
  53. sort_dir = ((sort_col.slice(-1)) == "-") ? -1 : +1;
  54. sort_col = sort_col.slice(0, -1);
  55. }
  56. }
  57.  
  58. // determine col-by-idx to be sorted with...
  59. var sort_idx = this.cols.findIndex((col) =>
  60. ["id", "attr", "prop", "attr_as_list"].some(attr =>
  61. attr in col && sort_col == col[attr]));
  62.  
  63. // if applicable sort according to config
  64. if (sort_idx > -1)
  65. this.rows.sort((x, y) => sort_dir * compare(
  66. x.data[sort_idx] && x.data[sort_idx].content,
  67. y.data[sort_idx] && y.data[sort_idx].content));
  68. else
  69. console.error(`config.sort_by: ${this.cfg.sort_by}, but column not found!`);
  70. }
  71. // mark rows to be hidden due to 'strict' property
  72. this.rows = this.rows.filter(row => !row.hidden);
  73.  
  74. // truncate shown rows to 'max rows', if configured
  75. if ("max_rows" in this.cfg && this.cfg.max_rows > -1)
  76. this.rows = this.rows.slice(0, this.cfg.max_rows);
  77.  
  78. return this.rows;
  79. }
  80. }
  81.  
  82. /** One level down, data representation for each row (including all cells) */
  83. class DataRow {
  84. constructor(entity, strict, raw_data=null) {
  85. this.entity = entity;
  86. this.hidden = false;
  87. this.strict = strict;
  88. this.raw_data = raw_data;
  89. this.data = null;
  90. this.has_multiple = false;
  91. }
  92.  
  93. get_raw_data(col_cfgs) {
  94. this.raw_data = col_cfgs.map((col) => {
  95.  
  96. // collect the "raw" data from the requested source(s)
  97. if ("attr" in col) {
  98. return ((col.attr in this.entity.attributes) ?
  99. this.entity.attributes[col.attr] : null);
  100.  
  101. } else if ("prop" in col) {
  102. // 'object_id' and 'name' not working -> make them work:
  103. if (col.prop == "object_id") {
  104. return this.entity.entity_id.split(".").slice(1).join(".");
  105.  
  106. // 'name' automagically resolves to most verbose name
  107. } else if (col.prop == "name") {
  108. if ("friendly_name" in this.entity.attributes)
  109. return this.entity.attributes.friendly_name;
  110. else if ("name" in this.entity)
  111. return this.entity.name;
  112. else if ("name" in this.entity.attributes)
  113. return this.entity.attributes.name;
  114. else
  115. return this.entity.entity_id;
  116.  
  117. // other state properties seem to work as expected...
  118. } else
  119. return ((col.prop in this.entity) ? this.entity[col.prop] : null);
  120.  
  121. } else if ("attr_as_list" in col) {
  122. this.has_multiple = true;
  123. return this.entity.attributes[col.attr_as_list];
  124.  
  125. } else
  126. console.error(`no selector found for col: ${col.name} - skipping...`);
  127. return null;
  128. });
  129. }
  130.  
  131. render_data(col_cfgs) {
  132. // apply passed "modify" configuration setting by using eval()
  133. // assuming the data is available inside the function as "x"
  134. this.data = this.raw_data.map((raw, idx) => {
  135. if (raw === "undefined" || typeof raw === "undefined" || raw === null)
  136. return ((this.strict) ? null : "n/a");
  137.  
  138. // finally, put it all together
  139. let x = raw;
  140. let cfg = col_cfgs[idx];
  141. return new Object({
  142. content: (cfg.modify) ? eval(cfg.modify) : x,
  143. pre: cfg.prefix || "",
  144. suf: cfg.suffix || "",
  145. css: cfg.align || "left",
  146. hide: cfg.hidden
  147. });
  148. });
  149. this.hidden = this.data.some(data => (data === null));
  150. return this;
  151. };
  152. }
  153.  
  154.  
  155. /** The HTMLElement, which is used as a base for the Lovelace custom card */
  156. class FlexTableCard extends HTMLElement {
  157. constructor() {
  158. super();
  159. this.attachShadow({
  160. mode: 'open'
  161. });
  162. this.card_height = 1;
  163. this.tbl = null;
  164. }
  165.  
  166. _getRegEx(pats, invert=false) {
  167. // compile and convert wildcardish-regex to real RegExp
  168. const real_pats = pats.map((pat) => pat.replace(/\*/g, '.*'));
  169. const merged = real_pats.map((pat) => `(${pat})`).join("|");
  170. if (invert)
  171. return new RegExp(`^(?:(?!${merged}).)*$`, 'gi');
  172. else
  173. return new RegExp(`^${merged}$`, 'gi');
  174. }
  175.  
  176. _getEntities(hass, incl, excl) {
  177. // apply inclusion regex
  178. const incl_re = listify(incl).map(pat => this._getRegEx([pat]));
  179. // make sure to respect the incl-implied order: no (incl-)regex-stiching, collect
  180. // results for each include and finally reduce to a single list of state-keys
  181. let keys = incl_re.map((regex) =>
  182. Object.keys(hass.states).filter(e_id => e_id.match(regex))).
  183. reduce((out, item) => out.concat(item), []);
  184. if (excl) {
  185. // apply exclusion, if applicable (here order is not affecting the output(s))
  186. const excl_re = this._getRegEx(listify(excl), true);
  187. keys = keys.filter(e_id => e_id.match(excl_re));
  188. }
  189. return keys.map(key => hass.states[key]);
  190. }
  191.  
  192. setConfig(config) {
  193. // get & keep card-config and hass-interface
  194. const root = this.shadowRoot;
  195. if (root.lastChild)
  196. root.removeChild(root.lastChild);
  197.  
  198. const cfg = Object.assign({}, config);
  199.  
  200. // assemble html
  201. const card = document.createElement('ha-card');
  202. card.header = cfg.title;
  203. const content = document.createElement('div');
  204. const style = document.createElement('style');
  205.  
  206. this.tbl = new DataTable(cfg);
  207.  
  208. // some css style
  209. style.textContent = `
  210. table { width: 100%; padding: 16px; }
  211. thead th { text-align: left; }
  212. tr td, th { padding-left: 0.5em; padding-right: 0.5em; }
  213. tr td.left, th.left { text-align: left; }
  214. tr td.center, th.center { text-align: center; }
  215. tr td.right, th.right { text-align: right; }
  216. tbody tr:nth-child(odd) { background-color: var(--paper-card-background-color); }
  217. tbody tr:nth-child(even) { background-color: var(--secondary-background-color); }
  218. `;
  219. // table skeleton, body identified with: 'flextbl'
  220. content.innerHTML = `
  221. <table>
  222. <thead>
  223. <tr>${this.tbl.headers.map(
  224. (name, idx) => `<th class="${cfg.columns[idx].align || 'left'}">${name}</th>`)
  225. .join("")}</tr>
  226. </thead>
  227. <tbody id='flextbl'></tbody>
  228. </table>
  229. `;
  230. // push css-style & table as content into the card's DOM tree
  231. card.appendChild(style);
  232. card.appendChild(content);
  233. // append card to _root_ node...
  234. root.appendChild(card);
  235. this._config = cfg;
  236. }
  237.  
  238. _updateContent(element, rows) {
  239. // callback for updating the cell-contents
  240. element.innerHTML = rows.map((row) =>
  241. `<tr id="entity_row_${row.entity.entity_id}">${row.data.map(
  242. (cell) => ((!cell.hide) ?
  243. `<td class="${cell.css}">${cell.pre}${cell.content}${cell.suf}</td>` : "")
  244. ).join("")}</tr>`).join("");
  245.  
  246. // if configured, set clickable row to show entity popup-dialog
  247. rows.forEach(row => {
  248. const elem = this.shadowRoot.getElementById(`entity_row_${row.entity.entity_id}`);
  249. // bind click()-handler to row (if configured)
  250. elem.onclick = (this.tbl.cfg.clickable) ? (function(clk_ev) {
  251. // create and fire 'details-view' signal
  252. let ev = new Event("hass-more-info", {
  253. bubbles: true, cancelable: false, composed: true
  254. });
  255. ev.detail = { entityId: row.entity.entity_id };
  256. this.dispatchEvent(ev);
  257. }) : null;
  258. });
  259. }
  260.  
  261. set hass(hass) {
  262. const config = this._config;
  263. const root = this.shadowRoot;
  264.  
  265. // get "data sources / origins" i.e, entities
  266. let entities = hass.callWS({
  267. type: "zha/devices"
  268. }).then(
  269. (devices) => {
  270. // `raw_rows` to be filled with data here, due to 'attr_as_list' it is possible to have
  271. // multiple data `raw_rows` acquired into one cell(.raw_data), so re-iterate all rows
  272. // to---if applicable---spawn new DataRow objects for these accordingly
  273. let raw_rows = devices.map(e => new DataRow({attributes: e}, config.strict));
  274. raw_rows.forEach(e => e.get_raw_data(config.columns))
  275.  
  276. // now add() the raw_data rows to the DataTable
  277. this.tbl.clear_rows();
  278. raw_rows.forEach(row_obj => {
  279. if (!row_obj.has_multiple)
  280. this.tbl.add(row_obj);
  281. else
  282. this.tbl.add(...transpose(row_obj.raw_data).map(new_raw_data =>
  283. new DataRow(row_obj.entity, row_obj.strict, new_raw_data)));
  284. });
  285.  
  286. // finally set card height and insert card
  287. this._setCardSize(this.tbl.rows.length);
  288. // all preprocessing / rendering will be done here inside DataTable::get_rows()
  289. this._updateContent(root.getElementById('flextbl'), this.tbl.get_rows());
  290. }
  291. );
  292.  
  293.  
  294. }
  295.  
  296. _setCardSize(num_rows) {
  297. this.card_height = parseInt(num_rows * 0.5);
  298. }
  299.  
  300. getCardSize() {
  301. return this.card_height;
  302. }
  303. }
  304.  
  305. customElements.define('flex-table-card', FlexTableCard);
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement