Version 2.0!

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

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
node_modules/

21
LICENSE.txt Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Thomas Lovén
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

315
README.md
View File

@ -1,44 +1,51 @@
card-tools version 0.4 card-tools version 2
========== ====================
This is a collection of tools to simplify creating custom cards for [Home Assistant](https://home-assistant.io) This is a collection of tools to simplify creating custom cards for [Home Assistant](https://home-assistant.io)
# IMPORTANT
`card-tools` v. 0.4 and any plugins that require it works only with Home Assistant 0.87 or later.
## Installation instructions ## Installation instructions
If you see "Can't find card-tools. [...]" in your Home Assistant UI, follow these instructions. If you see "Can't find card-tools. [...]" in your Home Assistant UI, follow these instructions.
To install `card-tools` follow [this guide](https://github.com/thomasloven/hass-config/wiki/Lovelace-Plugins). To install `card-tools` follow [this guide](https://github.com/thomasloven/hass-config/wiki/Lovelace-Plugins).
The recommended type of this plugin is: `js` The recommended type of this plugin is: `module`
### For [custom\_updater](https://github.com/custom-components/custom_updater)
```yaml ```yaml
resources: resources:
- url: /customcards/github/thomasloven/card-tools.js?track=true url: /local/card-tools.js
type: js type: module
``` ```
## User instructions ## User instructions
That's all. You don't need to do anything else. That's all. You don't need to do anything else.
## Developer instructions ---
---
---
*BREAKING CHANGES IN VERSION 0.3* Please read changelog below ## Card Developer Instructions
`card-tools` defines a global object `cardTools` which contains some helpful functions and stuff. There are two ways you can get access to the card-tools functions.
To make sure `card-tools` is installed, add the following line to the start of the `setConfig()` function of your custom card: 1. If you are using npm and a packager:
To make sure `card-tools` is loaded before your plugin, wait for `customElements.whenDefined("card-tools")` to resolve. Add the package as a dependency
Example: ```bash
```js > npm install thomasloven/lovelace-card-tools
```
And then import the parts you want
```javascript
import { LitElement } from "card-tools/src/lit-element";
```
2. Have your users add `card-tools.js` to their lovelace resources and access the functions through the `card-tools` customElement:
```javascript
customElements.whenDefined('card-tools').then(() => { customElements.whenDefined('card-tools').then(() => {
var cardTools = customElements.get('card-tools'); var cardTools = customElements.get('card-tools');
// YOUR CODE GOES IN HERE // YOUR CODE GOES IN HERE
@ -68,49 +75,29 @@ setTimeout(() => {
The `setTimeout` block at the end will make your element display an error message if `card-tools` is not found. Make sure the element name is the same in both `customElements.define()` calls. The `setTimeout` block at the end will make your element display an error message if `card-tools` is not found. Make sure the element name is the same in both `customElements.define()` calls.
The following functions are defined:
| Name | v >= | Description | ## Functions
| --- | --- | --- |
| `cardTools.version` | 0.4 | Current `card-tools` version |
| `cardTools.checkVersion(v)` | 0.1 | Check that the current `card-tools` version is at least `v` |
| `cardTools.hass` | 0.4 | Returns A `hass` state object. Useful for plugins that are *not* custom cards. If you need it, you'll know it |
| `cardTool.fireEvent(event, detail)` | 0.1 | Fire lovelace event `event` with options `detail` |
| `cardTools.LitElement` | 0.4 | A reference to the LitElement base class. |
| `cardTools.LitHtml` | 0.4 | A reference to the litHtml template function (requires Home Assistant 0.84 or later) |
| `cardTools.createCard(config)` | 0.1 | Creates and sets up a lovelace card based on `config` |
| `cardTools.createElement(config)` | 0.1 | Creates and sets up a `picture-elements` element based on `config` |
| `cardTools.createEntityRow(config)` | 0.1 | Creates and sets up an `entities` row based on `config` |
| `cardTools.deviceID` | 0.4 | Kind of unique and kind of persistent identifier for the browser viewing the page |
| `cardTools.moreInfo(entity)` | 0.1 | Brings up the `more-info` dialog for the specified `entity` id |
| `cardTools.longPress(element)` | 0.1 | Bind `element` to the long-press handler of lovelace |
| `cardTools.hasTemplate(text)` | 0.2 | Check if `text` contains a simple state template |
| `cardTools.parseTemplate(text, [data])` | 0.2 | Parse a simple state template and return results |
| `cardTools.args(script)` | 0.3 | Returns URL parameters of the script from `resources:` |
| `cardTools.localize(key)` | 0.3 | Returns translations of certains strings to the users language |
| `cardTools.lovelace`| 0.4 | A reference to a structure containing some information about the users lovelace configuration |
| `cardTools.popup(title, message, large)` | 0.4 | Open a popup window (simmilar to the more-info dialog) |
| `cardTools.closePopUp()` | 0.4 | Closes a popup window or more-info dialog |
| `cardTools.logger(message, script)` | 0.4 | Write a debug message to the browser console |
> Another way to use the `card-tools` is to just copy the function you want, and paste it into your card. It requires a bit of more work, but may be more user friendly. ### `card-tools/src/deviceID`
#### `deviceID`
This can be used to uniquely identify the device connected to Lovelace. Or actually, the device-browser combination.
It generates a random number, and stores it in the browsers local storage. That means it will stay around for quite a while.
It's kind of hard to explain, but as an example I use this to identify the browser for [browser_mod](https://github.com/thomasloven/hass-browser_mod).
I'm sure this can have lots of more uses.
The device ID is stored in localstorage with a key called `lovelace-player-device-id` (for historical reasons).
### version and checkVersion ### `card-tools/src/event`
#### `fireEvent(ev, detail)` / `cardTools.fireEvent(...)
This is mainly used as a helper for some other functions of `cardTools`, but it could be useful to fire a lovelace event sometime, such as `"config-refresh"` perhaps? Explore!
Those functions are just there to make sure the user has the right version of `card-tools`. I may add more functions later, and then you can make sure that those are supported by the version the user has.
I recommend adding a check as soon as possible, such as in the `setConfig()` function of a custom card/element/entity row.
```js
setConfig(config) {
cardTools.checkVersion(0.1);
...
```
> For information: I plan to increase the version number when I add functions. Not for bug fixes.
### hass
### `card-tools/src/hass`
#### `hass()` / `cardTools.hass`
This is provided for plugins that *aren't* cards, elements or entity rows. For those three kinds, the hass object is kindly provided to you by the whatever loads your element, but if you wish to write something that doesn't have a representation in the DOM, this can give you access to all of Home Assistants power anyway. This is provided for plugins that *aren't* cards, elements or entity rows. For those three kinds, the hass object is kindly provided to you by the whatever loads your element, but if you wish to write something that doesn't have a representation in the DOM, this can give you access to all of Home Assistants power anyway.
```js ```js
@ -118,172 +105,118 @@ This is provided for plugins that *aren't* cards, elements or entity rows. For t
greeting.innerHTML = `Hi there, ${cardTools.hass.user.name}`; greeting.innerHTML = `Hi there, ${cardTools.hass.user.name}`;
cardTools.hass.connection.subscribeEvents((event) => {console.log("A service was called in Home Assistant")}, 'call-service'); cardTools.hass.connection.subscribeEvents((event) => {console.log("A service was called in Home Assistant")}, 'call-service');
``` ```
**Note that this is called as a function if imported, but is a direct property of the cardTools element.**
### lovelace ### `lovelace()` / `cardTools.lovelace`
This object contains information about the users lovelace configuration. As a bonus `lovelace().current_view` contains the index of the currently displayed view.
**Note that this is called as a function if imported, but is a direct property of the cardTools element.**
This object contains information about the users lovelace configuration. As a bonus `cardTools.lovelace.current_view` contains the index of the currently displayed view. ### `lovelace_view()` / `cardTools.lovelace_view()`
Return a reference to the lovelace view object.
### fireEvent ### `provideHass(element)` / `cardTools.provideHass(...)`
Will make sure `element.hass` is set and updated whenever the `hass` object is updated (i.e. when any entity state changes).
This is mainly used as a helper for some other functions of `cardTools`, but it could be useful to fire a lovelace event sometime, such as `"config-refresh"` perhaps? Explore! ### `load_lovelace()`
Evaluating this function will load in the lovelace interface and all customElements of it, such as `ha-card`.
This is not provided in the `card-tools` element, because that wouldn't make sense.
### LitElement, LitHtml and LitCSS
Currently, the Home Assistant frontend is being converted to LitElement based elements, rather than Polymer based, since those are faster and easier to use. If you wish to make your element LitElement based, those may help. ### `card-tools/src/lit-element`
#### `LitElement` / `cardTools.LitElement`
The [lit-element](https://lit-element.polymer-project.org/) base class. Using this could save you a few bytes.
### createCard, createElement, createEntityRow #### `html` / `cardTools.LitHtml`
#### `css` / `cardTools.LitCSS`
The `html` and `css` functions from lit-element.
### `card-tools/src/action`
#### `bindActionHandler(element, options)` / `cardTools.longpress(...)
This binds `element` to the action-handler of lovelace, which manages different special click/tap actions such as long-press or double click.
Once bound, the element will receive `action` events whenever something happens.
```javascript
render() {
return html`
<div
id="my-clickable-div"
@action=${(ev) => console.log("I was clicked, or something", ev)
>
Double-tap me!
</div>`;
}
firstUpdated() {
bindActionHandler(this.shadowRoot.querySelector("#my-clickable-div"), {hasHold: true, hasDoubleClick: true});
}
```
### `card-tools/src/lovelace-element.js`
#### `createCard(config)` / `cardTools.createCard(...)`
#### `createElement(config)` / `cardTools.createElement(...)`
#### `createEntityRow(config)` / `cardTools.createEntityRow(...)`
Currently, custom elements can be used in three places in Lovelace; as cards, as elements in a `picture-elements` card or as rows in an `entities` card. Currently, custom elements can be used in three places in Lovelace; as cards, as elements in a `picture-elements` card or as rows in an `entities` card.
Those functions creates a card, element or row safely and cleanly from a config object. They handle custom elements and automatically picks the most suitable row for an entity. In short, it's mainly based on - and works very similar to - how Lovelace handles those things natively. Those functions creates a card, element or row safely and cleanly from a config object. They handle custom elements and automatically picks the most suitable row for an entity. In short, it's mainly based on - and works very similar to - how Lovelace handles those things natively.
```js ```javascript
const myElement = cardTools.createElement({ const myElement = createElement({
type: "state-icon", type: "state-icon",
entity: "light.bed_light", entity: "light.bed_light",
hold_action: {action: "toggle"}, hold_action: {action: "toggle"},
}); });
``` ```
> There's also a `cardTools.createThing(thing, config)` which is a helper function for those three. You'll probably never need to access it directly, but it might be good to know that it's there... ### `card-tools/src/card-maker`
Importing this file (or adding `card-tools.js` the lovelace resources) will define three new customElements, `card-maker`, `element-maker` and `entity-row-maker` which acts as wrappers for cards, elements and entity rows respectively. Very useful for e.g. cards which contain other cards:
### deviceID ```javascript
This can be used to uniquely identify the device connected to Lovelace. Or actually, the device-browser combination.
It generates a random number, and stores it in the browsers local storage. That means it will stay around for quite a while.
It's kind of hard to explain, but as an example I use this to make browsers usable as media players in [lovelace-player](https://githb.com/thomasloven/lovelace-player). In short, a `media`-`deviceID` pair is sent to every browser currently viewing the lovelace UI, but only if the `deviceId` matches `cardTools.deviceID()` is the `media` played. That way, I can make a sound play only on my ipad, even if I have the same page open on my computer.
I'm sure this can have lots of uses.
The device ID is stored in a key called `lovelace-player-device-id` (for historical reasons).
### moreInfo
This can be used to open the more-info dialog for an entity.
```js
render() { render() {
return cardTools.LitHtml` return html`
<paper-button <ha-card>
@click="${cardTools.moreInfo("light.bed_light");}" <card-maker
> .config=${this.card1_config}
Click me! .hass=${this.hass}
</paper-button> ></card-maker>
`; <card-maker
.config=${this.card2_config}
.hass=${this.hass}
></card-maker>
</ha-card>`;
} }
``` ```
### longpress ### `card-tools/src/more-info`
#### `moreInfo(entity, large=false)` / `cardTools.moreInfo(...)`
Pops open the more-info dialog for `entity`.
Some elements in Lovelace can perform two different actions when they are clicked and clicked-and-held. This lets you give this capability to any element. ### `card-tools/src/popup`
#### `popUp(title, card, large=false, style=null, fullscreen=false)` / `cardTools.popUp(...)`
Opens up a dialog similar to the more-info dialog, but with the contents replaces by the lovelace card defined by `card`.
Once an element has been bound to longpress, it will be able to receive `ha-click` and `ha-hold` events. #### `closePopUp()` / `cardTools.closePopUp()`
Closes a popUp or more-info dialog.
```js ### `card-tools/src/templates`
render() { #### `parseTemplate(hass, str, variables)` / `cardTools.parseTemplate(hass, str, variables)`
return cardTools.LitHtml` Renders and returns a jinja2 template in the backend.
<paper-button Only works if the currently logged in user is in the admin group.
@click="${cardTools.moreInfo("light.bed_light");}"
@ha-click="${console.log('I was clicked')}"
@ha-hold="${console.log('I was held')}"
>
Click or hold me!
</paper-button>
`;
}
firstUpdated() { #### `subscribeRenderTemplate(conn, onChange, params)` / `cardTools.subscribeRenderTemplate(...)`
cardTools.longpress(this.shadowRoot.querySelector('paper-button')); Renders the jinja2 templates in `parameters.template` in the backend and sends the results to `onChange` whenever anything changes.
} Returns a function for canceling the subscription.
```
### hasTemplate and parseTemplate ### `card-tools/src/old-templates`
Relates to my [Mod Plugin Templates](https://github.com/thomasloven/hass-config/wiki/Mod-plugin-templates) which are rendered entirely in the frontend
`cardTools.parseTemplate` lets you parse a user specified template like `[[ light.bed_light.state ]]` and return the result. #### `hasOldTemplate(text)`
Check if there is a template in `text`.
Two things are important: #### `parseOldTemplate(text, data)` / `cardTools.parseTemplate(text, data)`
Parses a template and returns the result.
- Template must start with `[[<space>` and end with `<space>]]`
- This is not in any way the same kind of template as used in the Home Assistant configuration
The templates are parsed by reading one step at a time from the `hass.states` object.
Thus, the first part must be an entity with domain and id, e.g. `light.bed_light`, `media_player.bedroom` etc.
Next is one of:
- `entity_id`
- `state`
- `attributes.<attribute>`
- `last_changed`
- `last_updated`
The function replaces any template found in the supplied string with the requested value, or an error message on failure.
The optional argument `data` can be an object containing extra data for templates. In a template `{key}` will be evaluated to `data[key]`.
`cardTools.hasTemplate` just checks if a string contains a simple state template.
### args
Lovelace plugins are imported by placing a script URL in the `resources` section of `ui-lovelace.yaml` or the Raw editor. This URL can be followed by query parameters.
```
resources:
- url: /local/my-plugin.js?height=5&flag&width=10&
type: js
```
If called from `my-plugin.js` `cardTools.args()` will return the javascript object `{height: 5, flag: undefined, width: 10}`.
If called from a callback function, `cardTools.args` requires the parameter `script` in order to determine the current script. It should have the value of `document.currentScript`, but must be defined outside of the callback scope.
### localize
Returns the translation of certain strings (defined by string keys) to the users language.
Examples of keys:
- `"state.light.on"`
- `"state.binary_sensor.garage_door.off"`
- `"domain.fan"`
- `"attribute.weather.humidity"`
More can be found by exploring `cardTools.hass().resources`.
### popup
This function opens a dialog similar to the more-info dialog but with the title and message specified. Set `large` to `true` to attempt to open a wider dialog
### closePopUp
This function closes a popup or more-info dialog.
### logger
This function allows the user to enable a debug mode by adding `?debug` to the `url:` in their `resources` when importing your card. Messages printed with `cardTools.logger()` will only be shown if the debug mode is active.
The `script` parameter is required if `cardTools.logger` is called from within a callback function. See the description of `cardTools.args` for more information.
## Changelog
*0.2*
- Added `parseTemplate()` function
*0.3*
- `LitElement` renamed to `litElement`
- `cardTools.litElement()`, `cardTools.litHtml` and `cardtools.deviceID()` are now functions
- Updated recommendation for how to check if `card-tools` exists
- Added `hasTemplate()` to documentation
- Added `args()` function
- Added `localize()` function
*0.4*
- `cardTools.LitElement` reintroduced. It is not a function
- `cardTools.LitHtml` introduced. It is not a function
- `cardTools.v()` removed and replaced with `cardTools.version` (kind of breaking, but I don't think anyone uses it...)
- `cardTools.deviceID()` removed and replaced with `cardTools.deviceID` (kind of breaking, but I don't think anyone uses it...)
- `cardTools.hass()` deprecated and replaced with `cardTools.hass`
- `cardTools.LitCSS` added
- `cardTools.lovelace` added
- `cardTools.popup()` added
- `cardTools.closePopUp()` added
- `cardTools.logger()` added
- Added `script` parameter to `cardTools.args`
--- ---
<a href="https://www.buymeacoffee.com/uqD6KHCdJ" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/white_img.png" alt="Buy Me A Coffee" style="height: auto !important;width: auto !important;" ></a> <a href="https://www.buymeacoffee.com/uqD6KHCdJ" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/white_img.png" alt="Buy Me A Coffee" style="height: auto !important;width: auto !important;" ></a>

File diff suppressed because one or more lines are too long

3
hacs.json Normal file
View File

@ -0,0 +1,3 @@
{
"name": "card-tools"
}

15
info.md Normal file
View File

@ -0,0 +1,15 @@
card-tools
==========
This is a collection of tools to simplify creating custom cards for [Home Assistant](https://home-assistant.io)
If you see "Can't find card-tools. [...]" in your Home Assistant UI, this is what you want.
## Usage instructions
1: Install card-tools
2: Done
## Card Developer Instructions
See repository on github

4066
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

20
package.json Normal file
View File

@ -0,0 +1,20 @@
{
"name": "card-tools",
"private": true,
"version": "2.0.0",
"description": "Lovelace Card Tools",
"scripts": {
"build": "webpack",
"watch": "webpack --watch --mode=development"
},
"repository": {
"type": "git",
"url": "github.com:thomasloven/card-tools"
},
"author": "Thomas Lovén",
"license": "MIT",
"devDependencies": {
"webpack": "^4.41.2",
"webpack-cli": "^3.3.10"
}
}

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,
}
);
};

10
webpack.config.js Normal file
View File

@ -0,0 +1,10 @@
const path = require('path');
module.exports = {
entry: './src/main.js',
mode: 'production',
output: {
filename: 'card-tools.js',
path: path.resolve(__dirname)
}
};