SHARE
TWEET

Untitled

a guest Jul 19th, 2019 64 Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  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);
RAW Paste Data
We use cookies for various purposes including analytics. By continuing to use Pastebin, you agree to our use of cookies as described in the Cookies Policy. OK, I Understand
 
Top