Version 2.0!

This commit is contained in:
2019-11-05 00:28:13 +01:00
parent bff8a650bc
commit fd2e449001
21 changed files with 4875 additions and 619 deletions

11
src/action.js Normal file
View File

@@ -0,0 +1,11 @@
export function bindActionHandler(element, options={}) {
customElements.whenDefined("long-press").then(() => {
const longpress = document.body.querySelector("long-press");
longpress.bind(element);
});
customElements.whenDefined("action-handler").then(() => {
const actionHandler = document.body.querySelector("action-handler");
actionHandler.bind(element, options);
});
return element;
}

63
src/card-maker.js Normal file
View File

@@ -0,0 +1,63 @@
import { LitElement, html } from "./lit-element.js";
import { createCard, createEntityRow, createElement } from "./lovelace-element.js";
import { provideHass } from "./hass.js";
class ThingMaker extends LitElement {
static get properties() {
return {
'hass': {},
'config': {},
'noHass': {type: Boolean },
};
}
setConfig(config) {
this._config = config;
if(!this.el)
this.el = this.create(config);
else
this.el.setConfig(config);
if(this._hass) this.el.hass = this._hass;
if(this.noHass) provideHass(this);
}
set config(config) {
this.setConfig(config);
}
set hass(hass) {
this._hass = hass;
if(this.el) this.el.hass = hass;
}
createRenderRoot() {
return this;
}
render() {
return html`${this.el}`;
}
}
if(!customElements.get("card-maker")) {
class CardMaker extends ThingMaker {
create(config) {
return createCard(config);
}
}
customElements.define("card-maker", CardMaker);
}
if(!customElements.get("element-maker")) {
class ElementMaker extends ThingMaker {
create(config) {
return createElement(config);
}
}
customElements.define("element-maker", ElementMaker);
}
if(!customElements.get("entity-row-maker")) {
class EntityRowMaker extends ThingMaker {
create(config) {
return createEntityRow(config);
}
}
customElements.define("entity-row-maker", EntityRowMaker);
}

15
src/deviceID.js Normal file
View File

@@ -0,0 +1,15 @@
function _deviceID() {
const ID_STORAGE_KEY = 'lovelace-player-device-id';
if(window['fully'] && typeof fully.getDeviceId === "function")
return fully.getDeviceId();
if(!localStorage[ID_STORAGE_KEY])
{
const s4 = () => {
return Math.floor((1+Math.random())*100000).toString(16).substring(1);
}
localStorage[ID_STORAGE_KEY] = `${s4()}${s4()}-${s4()}${s4()}`;
}
return localStorage[ID_STORAGE_KEY];
};
export let deviceID = _deviceID();

25
src/event.js Normal file
View File

@@ -0,0 +1,25 @@
export function fireEvent(ev, detail, entity=null) {
ev = new Event(ev, {
bubbles: true,
cancelable: false,
composed: true,
});
ev.detail = detail || {};
if(entity) {
entity.dispatchEvent(ev);
} else {
var root = document.querySelector("home-assistant");
root = root && root.shadowRoot;
root = root && root.querySelector("home-assistant-main");
root = root && root.shadowRoot;
root = root && root.querySelector("app-drawer-layout partial-panel-resolver");
root = root && root.shadowRoot || root;
root = root && root.querySelector("ha-panel-lovelace");
root = root && root.shadowRoot;
root = root && root.querySelector("hui-root");
root = root && root.shadowRoot;
root = root && root.querySelector("ha-app-layout #view");
root = root && root.firstElementChild;
if (root) root.dispatchEvent(ev);
}
}

58
src/hass.js Normal file
View File

@@ -0,0 +1,58 @@
export function hass() {
return document.querySelector('home-assistant').hass
};
export function provideHass(element) {
return document.querySelector("home-assistant").provideHass(element);
}
export function lovelace() {
var root = document.querySelector("home-assistant");
root = root && root.shadowRoot;
root = root && root.querySelector("home-assistant-main");
root = root && root.shadowRoot;
root = root && root.querySelector("app-drawer-layout partial-panel-resolver");
root = root && root.shadowRoot || root;
root = root && root.querySelector("ha-panel-lovelace")
root = root && root.shadowRoot;
root = root && root.querySelector("hui-root")
if (root) {
var ll = root.lovelace
ll.current_view = root.___curView;
return ll;
}
return null;
}
export function lovelace_view() {
var root = document.querySelector("home-assistant");
root = root && root.shadowRoot;
root = root && root.querySelector("home-assistant-main");
root = root && root.shadowRoot;
root = root && root.querySelector("app-drawer-layout partial-panel-resolver");
root = root && root.shadowRoot || root;
root = root && root.querySelector("ha-panel-lovelace");
root = root && root.shadowRoot;
root = root && root.querySelector("hui-root");
root = root && root.shadowRoot;
root = root && root.querySelector("ha-app-layout #view");
root = root && root.firstElementChild;
return root;
}
export function load_lovelace() {
if(customElements.get("hui-view")) return true;
const res = document.createElement("partial-panel-resolver");
res.hass = hass();
res.route = {path: "/lovelace/"};
// res._updateRoutes();
try {
document.querySelector("home-assistant").appendChild(res).catch((error) => {});
} catch (error) {
document.querySelector("home-assistant").removeChild(res);
}
if(customElements.get("hui-view")) return true;
return false;
}

5
src/lit-element.js Normal file
View File

@@ -0,0 +1,5 @@
export const LitElement = customElements.get('home-assistant-main') ? Object.getPrototypeOf(customElements.get('home-assistant-main')) : Object.getPrototypeOf(customElements.get('hui-view'));
export const html = LitElement.prototype.html;
export const css = LitElement.prototype.css;

114
src/lovelace-element.js Normal file
View File

@@ -0,0 +1,114 @@
import { fireEvent } from "./event.js";
export const CUSTOM_TYPE_PREFIX = "custom:";
export const DOMAINS_HIDE_MORE_INFO = [
"input_number",
"input_select",
"input_text",
"scene",
"weblink",
];
function errorElement(error, config) {
const el = document.createElement("hui-error-card");
el.setConfig({
type: "error",
error,
config,
});
return el;
}
function _createElement(tag, config) {
const el = document.createElement(tag);
try {
el.setConfig(config);
} catch (err) {
return errorElement(err, config);
}
return el;
}
function createLovelaceElement(thing, config) {
if(!config || typeof config !== "object" || !config.type)
return errorElement(`No ${thing} type configured`, config);
let tag = config.type;
if(tag.startsWith(CUSTOM_TYPE_PREFIX))
tag = tag.substr(CUSTOM_TYPE_PREFIX.length);
else
tag = `hui-${tag}-${thing}`;
if(customElements.get(tag))
return _createElement(tag, config);
const el = errorElement(`Custom element doesn't exist: ${tag}.`, config);
el.style.display = "None";
const timer = setTimeout(() => {
el.style.display = "";
}, 2000);
customElements.whenDefined(tag).then(() => {
clearTimeout(timer);
fireEvent("ll-rebuild", {}, el);
});
return el;
}
export function createCard(config) {
return createLovelaceElement('card', config);
}
export function createElement(config) {
return createLovelaceElement('element', config);
}
export function createEntityRow(config) {
const SPECIAL_TYPES = new Set([
"call-service",
"divider",
"section",
"weblink",
]);
const DEFAULT_ROWS = {
alert: "toggle",
automation: "toggle",
climate: "climate",
cover: "cover",
fan: "toggle",
group: "group",
input_boolean: "toggle",
input_number: "input-number",
input_select: "input-select",
input_text: "input-text",
light: "toggle",
lock: "lock",
media_player: "media-player",
remote: "toggle",
scene: "scene",
script: "script",
sensor: "sensor",
timer: "timer",
switch: "toggle",
vacuum: "toggle",
water_heater: "climate",
input_datetime: "input-datetime",
};
if(!config)
return errorElement("Invalid configuration given.", config);
if(typeof config === "string")
config = {entity: config};
if(typeof config !== "object" || (!config.entity && !config.type))
return errorElement("Invalid configuration given.", config);
const type = config.type || "default";
if(SPECIAL_TYPES.has(type) || type.startsWith(CUSTOM_TYPE_PREFIX))
return createLovelaceElement('row', config);
const domain = config.entity.split(".", 1)[0];
Object.assign(config, {type: DEFAULT_ROWS[domain] || "text"});
return createLovelaceElement('entity-row', config);
}

62
src/main.js Normal file
View File

@@ -0,0 +1,62 @@
import "./card-maker.js"
import { deviceID } from "./deviceID.js";
import { fireEvent } from "./event.js";
import { hass, provideHass, lovelace, lovelace_view } from "./hass.js";
import { LitElement, html, css } from "./lit-element.js";
import { bindActionHandler } from "./action.js";
import { createCard, createElement, createEntityRow } from "./lovelace-element.js";
import { moreInfo } from "./more-info.js";
import { popUp, closePopUp } from "./popup.js";
import { parseTemplate, subscribeRenderTemplate } from "./templates.js";
import { hasOldTemplate, parseOldTemplate } from "./old-templates.js";
class CardTools {
static checkVersion(v) {
}
static get deviceID() { return deviceID; }
static get fireEvent() { return fireEvent; }
static get hass() { return hass(); }
static get lovelace() { return lovelace(); }
static get lovelace_view() { return lovelace_view; }
static get provideHass() { return provideHass; }
static get LitElement() { return LitElement; }
static get LitHtml() { return html; }
static get LitCSS() { return css; }
static get longpress() { return bindActionHandler; }
static get createCard() { return createCard; }
static get createElement() { return createElement; }
static get createEntityRow() { return createEntityRow; }
static get moreInfo() { return moreInfo; }
static get popUp() { return popUp; }
static get closePopUp() { return closePopUp; }
static get hasTemplate() { return hasOldTemplate; }
static parseTemplate(hass, str, specialData = {}) {
if (typeof(hass) === "string")
return parseOldTemplate(hass, str);
return parseTemplate(hass, str, specialData);
}
static get subscribeRenderTemplate() { return subscribeRenderTemplate; }
}
if(!customElements.get("card-tools")) {
customElements.define("card-tools", CardTools);
window.cardTools = customElements.get('card-tools');
console.info(`%cCARD-TOOLS 2 IS INSTALLED
%cDeviceID: ${customElements.get('card-tools').deviceID}`,
"color: green; font-weight: bold",
"");
}

8
src/more-info.js Normal file
View File

@@ -0,0 +1,8 @@
import { fireEvent } from "./event.js";
export function moreInfo(entity, large=false) {
fireEvent("hass-more-info", {entityId: entity}, document.querySelector("home-assistant"));
const el = document.querySelector("home-assistant")._moreInfoEl;
el.large = large;
return el;
}

116
src/old-templates.js Normal file
View File

@@ -0,0 +1,116 @@
import {hass} from './hass.js';
import {deviceID} from './deviceID.js';
export function hasOldTemplate(text) {
return /\[\[\s+.*\s+\]\]/.test(text);
}
function parseTemplateString(str, specialData = {}) {
if(typeof(str) !== "string") return text;
const FUNCTION = /^[a-zA-Z0-9_]+\(.*\)$/;
const EXPR = /([^=<>!]+)\s*(==|!=|<|>|<=|>=)\s*([^=<>!]+)/;
const SPECIAL = /^\{.+\}$/;
const STRING = /^"[^"]*"|'[^']*'$/;
if(typeof(specialData) === "string") specialData = {};
specialData = Object.assign({
user: hass().user.name,
browser: deviceID,
hash: location.hash.substr(1) || ' ',
}, specialData);
const _parse_function = (str) => {
let args = [str.substr(0, str.indexOf('(')).trim()]
str = str.substr(str.indexOf('(')+1);
while(str) {
let index = 0;
let parens = 0;
let quote = false;
while(str[index]) {
let c = str[index++];
if(c === quote && index > 1 && str[index-2] !== "\\")
quote = false;
else if(`"'`.includes(c))
quote = c;
if(quote) continue;
if(c === '(')
parens = parens + 1;
else if(c === ')') {
parens = parens - 1;
continue
}
if(parens > 0) continue;
if(",)".includes(c)) break;
}
args.push(str.substr(0, index-1).trim());
str = str.substr(index);
}
return args;
};
const _parse_special = (str) => {
str = str.substr(1, str.length - 2);
return specialData[str] || `{${str}}`;
};
const _parse_entity = (str) => {
str = str.split(".");
let v;
if(str[0].match(SPECIAL)) {
v = _parse_special(str.shift());
v = hass().states[v] || v;
} else {
v = hass().states[`${str.shift()}.${str.shift()}`];
if(!str.length) return v['state'];
}
str.forEach(item => v=v[item]);
return v;
}
const _eval_expr = (str) => {
str = EXPR.exec(str);
if(str === null) return false;
const lhs = parseTemplateString(str[1]);
const rhs = parseTemplateString(str[3]);
var expr = ''
if(parseFloat(lhs) != lhs)
expr = `"${lhs}" ${str[2]} "${rhs}"`;
else
expr = `${parseFloat(lhs)} ${str[2]} ${parseFloat(rhs)}`
return eval(expr);
}
const _eval_function = (args) => {
if(args[0] === "if") {
if(_eval_expr(args[1]))
return parseTemplateString(args[2]);
return parseTemplateString(args[3]);
}
}
try {
str = str.trim();
if(str.match(STRING))
return str.substr(1, str.length - 2);
if(str.match(SPECIAL))
return _parse_special(str);
if(str.match(FUNCTION))
return _eval_function(_parse_function(str));
if(str.includes("."))
return _parse_entity(str);
return str;
} catch (err) {
return `[[ Template matching failed: ${str} ]]`;
}
}
export function parseOldTemplate(text, data = {}) {
if(typeof(text) !== "string") return text;
// Note: .*? is javascript regex syntax for NON-greedy matching
var RE_template = /\[\[\s(.*?)\s\]\]/g;
text = text.replace(RE_template, (str, p1, offset, s) => parseTemplateString(p1, data));
return text;
}

94
src/popup.js Normal file
View File

@@ -0,0 +1,94 @@
import { hass, provideHass } from "./hass.js";
import { fireEvent } from "./event.js";
import { createCard } from "./lovelace-element.js";
import { moreInfo } from "./more-info.js";
import "./card-maker.js"
export function closePopUp() {
const moreInfoEl = document.querySelector("home-assistant") && document.querySelector("home-assistant")._moreInfoEl;
if(moreInfoEl)
moreInfoEl.close();
}
export function popUp(title, card, large=false, style=null, fullscreen=false) {
// Force _moreInfoEl to be loaded
fireEvent("hass-more-info", {entityId: null});
const moreInfoEl = document.querySelector("home-assistant")._moreInfoEl;
// Close and reopen to clear any previous styling
// Necessary for popups from popups
moreInfoEl.close();
moreInfoEl.open();
const wrapper = document.createElement("div");
wrapper.innerHTML = `
<style>
app-toolbar {
color: var(--more-info-header-color);
background-color: var(--more-info-header-background);
}
.scrollable {
overflow: auto;
max-width: 100% !important;
}
</style>
${fullscreen
? ``
: `
<app-toolbar>
<paper-icon-button
icon="hass:close"
dialog-dismiss=""
></paper-icon-button>
<div class="main-title" main-title="">
${title}
</div>
</app-toolbar>
`
}
<div class="scrollable">
<card-maker nohass>
</card-maker>
</div>
`;
const scroll = wrapper.querySelector(".scrollable");
const content = scroll.querySelector("card-maker");
content.config = card;
moreInfoEl.sizingTarget = scroll;
moreInfoEl.large = large;
moreInfoEl._page = "none"; // Display nothing by default
moreInfoEl.shadowRoot.appendChild(wrapper);
let oldStyle = {};
if(style) {
moreInfoEl.resetFit(); // Reset positioning to enable setting it via css
for (var k in style) {
oldStyle[k] = moreInfoEl.style[k];
moreInfoEl.style.setProperty(k, style[k]);
}
}
moreInfoEl._dialogOpenChanged = function(newVal) {
if (!newVal) {
if(this.stateObj)
this.fire("hass-more-info", {entityId: null});
if (this.shadowRoot == wrapper.parentNode) {
this._page = null;
this.shadowRoot.removeChild(wrapper);
if(style) {
moreInfoEl.resetFit();
for (var k in oldStyle)
if (oldStyle[k])
moreInfoEl.style.setProperty(k, oldStyle[k]);
else
moreInfoEl.style.removeProperty(k);
}
}
}
}
return moreInfoEl;
}

43
src/templates.js Normal file
View File

@@ -0,0 +1,43 @@
import {hass} from './hass.js';
import {deviceID} from './deviceID.js';
export async function parseTemplate(hass, str, specialData = {}) {
if (!hass) hass = hass();
if (typeof(specialData === "string")) specialData = {};
specialData = Object.assign({
user: hass.user.name,
browser: deviceID,
hash: location.hash.substr(1) || ' ',
},
specialData);
for (var k in specialData) {
var re = new RegExp(`\\{${k}\\}`, "g");
str = str.replace(re, specialData[k]);
}
return hass.callApi("POST", "template", {template: str});
};
export function subscribeRenderTemplate(conn, onChange, params) {
// params = {template, entity_ids, variables}
if(!conn)
conn = hass().connection;
let variables = {
user: hass().user.name,
browser: deviceID,
hash: location.hash.substr(1) || ' ',
...params.variables,
};
let template = params.template;
let entity_ids = params.entity_ids;
return conn.subscribeMessage(
(msg) => onChange(msg.result),
{ type: "render_template",
template,
variables,
entity_ids,
}
);
};