Use 0.98 jinja template subscriptions. Also style entity rows and glance entities

This commit is contained in:
Thomas Lovén 2019-08-27 23:33:32 +02:00
parent 60a6d80116
commit 27ebb26c26
4 changed files with 357 additions and 210 deletions

View File

@ -59,7 +59,7 @@ entities:
![smallcaps](https://user-images.githubusercontent.com/1299821/59151624-9a1d7680-8a36-11e9-9b2d-598c80ff2aa1.png) ![smallcaps](https://user-images.githubusercontent.com/1299821/59151624-9a1d7680-8a36-11e9-9b2d-598c80ff2aa1.png)
You can also use [templates](https://github.com/thomasloven/hass-config/wiki/Mod-plugin-templates) to change the styles dynamically. You can also use [templating](https://www.home-assistant.io/docs/configuration/templating/) to change the styles dynamically.
**Example** \ **Example** \
Make an `entity-button` card green when the light is on Make an `entity-button` card green when the light is on
@ -69,7 +69,7 @@ type: entity-button
entity: light.bed_light entity: light.bed_light
style: | style: |
ha-card { ha-card {
background: [[ if(light.bed_light == "on", "green", "") ]]; background: {% if is_state('light.bed_light', 'on') %} green {% endif %};
} }
``` ```
@ -100,9 +100,43 @@ style: |
## More examples ## More examples
More examples are available [here](https://github.com/thomasloven/lovelace-card-mod/blob/master/src/example.yaml). More examples are available [here](https://github.com/thomasloven/lovelace-card-mod/blob/master/src/example.yaml).
![more](https://user-images.githubusercontent.com/1299821/59151861-9f7cc000-8a3a-11e9-90d2-ff192c4c10a7.gif) ![more](https://user-images.githubusercontent.com/1299821/63809565-eb951d80-c922-11e9-8630-697befb3c95f.png)
## Styling entity and glance cards
To make things easier, rows in `entities` cards and buttons in `glance` cards can be styled individually.
For those, the styles will be applied to the shadowRoot of the element, so a good starting point (rather than `ha-card`) would be `:host`:
```yaml
type: entities
entities:
- light.bed_light
- entity: light.kitchen_lights
style: |
:host {
color: red;
}
- entity: input_number.value
style: |
:host {
--paper-item-icon-color:
{% if states(config.entity)|int < 50 %}
blue
{% else %}
red
{% endif %}
;
```
## Templating
Jinja templates have access to a few special variables. Those are:
- `config` - an object containing the card, entity row or glance button configuration
- `user` - the username of the currently logged in user
- `browser` - the deviceID of the current browser (see [browser_mod](https://github.com/thomasloven/hass-browser_mod).
- `hash` - the hash part of the current URL.
## Advanced usage ## Advanced usage
When exploring the cards using the element inspector, you might run into something called a `shadow-root` and notice that you can't apply styles to anything inside that. When exploring the cards using the element inspector, you might run into something called a `shadow-root` and notice that you can't apply styles to anything inside that.
@ -147,6 +181,29 @@ entity: alarm_control_panel.alarm
# FAQ # FAQ
### How do I convert my old card-modder configuration to card-mod?
For cards, you just have to wrap everything in `ha-card {}` and format it as CSS.
So, you go from:
```yaml
style:
"--ha-card-background": rgba(200, 0, 0, 0.5)
color: white
```
to
```yaml
style: |
ha-card {
--ha-card-background: rgba(200, 0, 0, 0.5);
color: white
}
```
For rows in an entities card, you replace `ha-card` with `:host` as described above.
Note that some cards can't be modded with card-mod. See below.
### Why won't this work for some cards? ### Why won't this work for some cards?
The cards this doesn't work for often are not really *cards* at all, but change how other cards work. Examples include: `conditional`, `entity-filter`, `horizontal-stack` and `vertical-stack` as well as some custom cards, like `layout-card`, `auto-entities` and `state-switch` among others. The cards this doesn't work for often are not really *cards* at all, but change how other cards work. Examples include: `conditional`, `entity-filter`, `horizontal-stack` and `vertical-stack` as well as some custom cards, like `layout-card`, `auto-entities` and `state-switch` among others.

File diff suppressed because one or more lines are too long

View File

@ -1,15 +1,13 @@
title: card-mod title: card-mod
cards: cards:
# Style the card, but use special styles for the header
- type: entities - type: entities
title: Default title: Simple configuration
entities:
- light.bed_light
- light.ceiling_lights
- light.kitchen_lights
- type: entities
style: | style: |
ha-card { ha-card {
color: blue;
}
.card-header {
color: red; color: red;
} }
entities: entities:
@ -17,41 +15,55 @@ cards:
- light.ceiling_lights - light.ceiling_lights
- light.kitchen_lights - light.kitchen_lights
- type: glance # Style an entities card row separately
title: Glance card - type: entities
title: Styled rows
entities:
- light.bed_light
- entity: light.ceiling_lights
style: |
:host {
color: green;
}
- type: section
label: A section label
style: |
:host {
--primary-color: red;
}
- entity: light.kitchen_lights
# Styles that update to state changes using jinja2 templating
- type: entities
title: Dynamic styles
style: | style: |
ha-card { ha-card {
font-variant: small-caps; background: url(http://place{{ states("input_select.background") }}.com/g/600/400);
} --primary-text-color: rgb(250,250,250);
.card-header { --paper-listbox-background-color: black;
font-size: 16px; text-shadow: 1px 1px 0 #000;
} }
entities: entities:
- input_select.background
- light.bed_light - light.bed_light
- light.ceiling_lights - light.ceiling_lights
- light.kitchen_lights - light.kitchen_lights
# Using the "config" special variable for templates
- type: entity-button - type: entity-button
entity: light.bed_light entity: light.bed_light
style: | style: |
ha-card { ha-card {
background: [[ if(light.bed_light == "on", "green", "") ]]; background:
} {% if is_state(config.entity, 'on') %}
green
- type: entity-button {% endif %}
entity: light.bed_light ;
style: |
@keyframes blink {
50% {
background: red;
}
}
ha-card {
animation: blink 2s linear infinite;
}
# Advanced css selectors can be used
- type: entities - type: entities
title: Styled title: Advanced CSS rules
style: | style: |
ha-card { ha-card {
background: rgba(0,50,0,0.5); background: rgba(0,50,0,0.5);
@ -66,29 +78,48 @@ cards:
- light.kitchen_lights - light.kitchen_lights
- light.bed_light - light.bed_light
# Cards can be made transparent by removing background and box-shadow
- type: entities - type: entities
title: Dynamic styles title: Transparent card
show_header_toggle: false
style: | style: |
ha-card { ha-card {
background: url(http://place[[ input_select.background.state ]].com/g/600/400); background: none;
--primary-text-color: rgb(250,250,250); box-shadow: none;
color: [[ if(light.bed_light == "on", "rgb(250,250,250)", "red") ]]; }
--paper-listbox-background-color: black; entities:
text-shadow: 1px 1px 0 #000; - type: custom:text-divider-row
text: blah
# Entities in glance cards can also be styled individually
- type: glance
title: Glance card
style: |
ha-card {
font-variant: small-caps;
}
.card-header {
font-size: 16px;
} }
entities: entities:
- input_select.background
- light.bed_light - light.bed_light
- light.ceiling_lights - entity: light.ceiling_lights
style: |
:host {
font-variant: none;
}
- light.kitchen_lights - light.kitchen_lights
# Adjustable picture elements position
# This could be much more useful with, say, an x and y value taken from sensor values
# that e.g. contain your robot vacuum cleaner location...
- type: vertical-stack - type: vertical-stack
cards: cards:
- type: picture-elements - type: picture-elements
style: | style: |
ha-card { ha-card {
--top: [[ input_number.y_pos.state ]]%; --top: {{ states("input_number.y_pos") }}%;
--left: [[ input_number.x_pos.state ]]%; --left: {{ states("input_number.x_pos") }}%;
} }
title: Dynamic styling of elements title: Dynamic styling of elements
image: "http://placekitten.com/g/800/600" image: "http://placekitten.com/g/800/600"
@ -103,6 +134,7 @@ cards:
- input_number.x_pos - input_number.x_pos
- input_number.y_pos - input_number.y_pos
# Some animations
- type: horizontal-stack - type: horizontal-stack
cards: cards:
- type: entity-button - type: entity-button
@ -114,7 +146,7 @@ cards:
} }
@keyframes blink { @keyframes blink {
50% { 50% {
filter: invert(100%); background: red;
} }
} }
- type: entity-button - type: entity-button
@ -160,100 +192,7 @@ cards:
} }
} }
# Keyframes are inherited into picture-elements elements as well...
- type: horizontal-stack
cards:
- type: entity-button
entity: light.bed_light
name: swing
style: |
ha-card {
animation: swing 1s linear infinite alternate;
}
@keyframes swing {
0% {
-webkit-transform: rotate(5deg);
transform: rotate(5deg);
}
100% {
-webkit-transform: rotate(-5deg);
transform: rotate(-5deg);
}
}
- type: entity-button
entity: light.bed_light
name: swell
style: |
ha-card {
animation: pulse 3s linear infinite alternate;
}
@keyframes blinka {
50% { background: white; }
100% { background: blue; }
}
@keyframes pulse {
0% {
-webkit-transform: scaleX(1);
transform: scaleX(1);
}
50% {
-webkit-transform: scale3d(1.15,1.15,1.15);
transform: scale3d(1.15,1.15,1.15);
}
100% {
-webkit-transform: scaleX(1);
transform: scaleX(1);
}
- type: entity-button
entity: light.bed_light
name: flip
style: |
ha-card {
animation: flip 5s linear infinite
}
@keyframes blinka {
50% { background: white; }
100% { background: red; }
}
@keyframes flip {
0% {
-webkit-animation-timing-function: ease-out;
-webkit-transform: perspective(400px) scaleX(1) translateZ(0) rotateY(-1turn);
animation-timing-function: ease-out;
transform: perspective(400px) scaleX(1) translateZ(0) rotateY(-1turn)
}
40% {
-webkit-animation-timing-function: ease-out;
-webkit-transform: perspective(400px) scaleX(1) translateZ(150px) rotateY(-190deg);
animation-timing-function: ease-out;
transform: perspective(400px) scaleX(1) translateZ(150px) rotateY(-190deg)
}
50% {
-webkit-animation-timing-function: ease-in;
-webkit-transform: perspective(400px) scaleX(1) translateZ(150px) rotateY(-170deg);
animation-timing-function: ease-in;
transform: perspective(400px) scaleX(1) translateZ(150px) rotateY(-170deg)
}
80% {
-webkit-animation-timing-function: ease-in;
-webkit-transform: perspective(400px) scale3d(.95,.95,.95) translateZ(0) rotateY(0deg);
animation-timing-function: ease-in;
transform: perspective(400px) scale3d(.95,.95,.95) translateZ(0) rotateY(0deg)
}
to {
-webkit-animation-timing-function: ease-in;
-webkit-transform: perspective(400px) scaleX(1) translateZ(0) rotateY(0deg);
animation-timing-function: ease-in;
transform: perspective(400px) scaleX(1) translateZ(0) rotateY(0deg)
}
}
- type: picture-elements - type: picture-elements
style: | style: |
@keyframes dvd { @keyframes dvd {
@ -288,6 +227,9 @@ cards:
animation: dvd 10s linear infinite animation: dvd 10s linear infinite
left: 0% left: 0%
top: 50% top: 50%
# An example of going into shadowroots of stuff
# Not for the faint of heart
- type: alarm-panel - type: alarm-panel
card_icon: mdi:bell card_icon: mdi:bell
name: Alarm Panel name: Alarm Panel
@ -311,3 +253,44 @@ cards:
--mdc-theme-primary: green; --mdc-theme-primary: green;
} }
entity: alarm_control_panel.alarm entity: alarm_control_panel.alarm
# By placing a stack in an entities card as a row, it can be modded as well.
- type: entities
style: |
ha-card {
--ha-card-background: red;
box-shadow: none;
background: none;
}
.card-content {
padding: 0
}
entities:
- type: custom:hui-horizontal-stack-card
cards:
- type: entities
entities:
- light.bed_light
- light.kitchen_lights
- light.ceiling_lights
- type: glance
entities:
- light.bed_light
- light.kitchen_lights
- light.ceiling_lights
# Automatically styled entity rows
# Makes use of config variable
- type: custom:auto-entities
filter:
include:
- domain: light
options:
style: |
:host {
color: {% if is_state(config.entity, 'on') %} green {% endif %};
}
card:
type: entities
title: Auto-entities

View File

@ -1,6 +1,78 @@
import {html, css} from "/card-tools/lit-element.js"; import {html, css} from "/card-tools/lit-element.js";
import {fireEvent} from "/card-tools/event.js"; import {fireEvent} from "/card-tools/event.js";
import {parseTemplate} from "/card-tools/templates.js"; import {subscribeRenderTemplate} from "/card-tools/templates.js";
const applyStyle = async function(root, style, params, debug) {
const debugPrint = function(str){
if(!debug) return;
if(typeof str === "string")
console.log(' '.repeat(2*(debug-1)) + str);
else
console.log(str);
}
if(!root || !style) return;
if(root.updateComplete)
await root.updateComplete;
if(typeof style === "string") {
const oldStyleEl = root.querySelector(".card-mod-style");
if(oldStyleEl)
{
// Remove old styles and cancel template subscriptions
if(oldStyleEl.cancelSubscription)
{
await oldStyleEl.cancelSubscription;
}
root.removeChild(oldStyleEl)
}
// Add new style tag to the root element
const styleEl = document.createElement('style');
styleEl.classList = "card-mod-style";
styleEl.cancelSubscription = subscribeRenderTemplate(
null,
(result) => {
styleEl.innerHTML = result;
},
{
template: style,
variables: params.variables,
entity_ids: params.entity_ids,
}
);
root.appendChild(styleEl);
debugPrint("Applied styles to:");
debugPrint(root);
} else {
Object.keys(style).forEach((k) => {
if(k === ".") {
debugPrint(`Stepping into root of ${root.tagName}`)
return applyStyle(root, style[k], params, debug?debug+1:0);
} else if(k === "$") {
debugPrint(`Stepping into ShadowRoot of ${root.tagName}`)
return applyStyle(root.shadowRoot, style[k], params, debug?debug+1:0);
} else {
debugPrint(`Searching for: "${k}". ${root.querySelectorAll(k).length} matches.`);
root.querySelectorAll(`${k}`).forEach((el) => {
debugPrint(`Stepping into ${el.tagName}`)
applyStyle(el, style[k], params, debug?debug+1:0);
});
return;
}
});
}
}
const find_entity_ids = function(data) {
if(typeof(data) === "string") {
return data.includes("config.entity");
}
return Object.values(data).some(find_entity_ids);
}
customElements.whenDefined('ha-card').then(() => { customElements.whenDefined('ha-card').then(() => {
const HaCard = customElements.get('ha-card'); const HaCard = customElements.get('ha-card');
@ -19,63 +91,6 @@ customElements.whenDefined('ha-card').then(() => {
return null; return null;
}; };
const applyStyle = function(root, style, debug) {
const debugPrint = function(str){
if(!debug) return;
if(typeof str === "string")
console.log(' '.repeat(2*(debug-1)) + str);
else
console.log(str);
}
if(!root || !style) return;
if(typeof style === "string") {
// Remove old styles if we're updating
if(root.querySelector(".card-mod-style"))
root.removeChild(root.querySelector(".card-mod-style"));
// Add new style tag to the root element
const styleEl = document.createElement('style');
styleEl.classList = "card-mod-style";
styleEl.innerHTML = parseTemplate(style);
root.appendChild(styleEl);
if(debug) {
debugPrint("Applied styles to:")
console.log(root);
}
} else {
Object.keys(style).forEach((k) => {
if(k === ".") {
debugPrint(`Stepping into root of ${root.tagName}`)
return applyStyle(root, style[k], debug?debug+1:0);
} else if(k === "$") {
debugPrint(`Stepping into ShadowRoot of ${root.tagName}`)
return applyStyle(root.shadowRoot, style[k], debug?debug+1:0);
} else {
debugPrint(`Searching for: "${k}". ${root.querySelectorAll(k).length} matches.`);
root.querySelectorAll(`${k}`).forEach((el) => {
debugPrint(`Stepping into ${el.tagName}`)
applyStyle(el, style[k], debug?debug+1:0);
});
return;
}
});
}
}
HaCard.prototype.updated = function(_) {
// Apply styles after updates, if specified
const config = findConfig(this);
if(config && config.style) {
if(config.debug_cardmod)
console.log("--- APPLYING STYLES START ---");
applyStyle(this, config.style, config.debug_cardmod ? 1 : 0);
if(config.debug_cardmod)
console.log("--- APPLYING STYLES END ---");
}
}
HaCard.prototype.firstUpdated = function() { HaCard.prototype.firstUpdated = function() {
// Move the header inside the slot instead of in the shadowDOM // Move the header inside the slot instead of in the shadowDOM
@ -85,22 +100,114 @@ customElements.whenDefined('ha-card').then(() => {
{ {
this.insertBefore(header, this.children[0]); this.insertBefore(header, this.children[0]);
} }
// Listen for changes to hass or the location and update
document.querySelector("home-assistant").provideHass(this); const config = findConfig(this);
window.addEventListener("location-changed", () => this._requestUpdate()); if(!config || !config.style) return;
let entity_ids = config.entity_ids;
if(!entity_ids && find_entity_ids(config.style))
entity_ids = [config.entity];
const apply = () => {
applyStyle(this, config.style, {
variables: {config},
entity_ids
}, !!config.debug_cardmod);
}
apply();
window.addEventListener("location-changed", () => apply());
}
fireEvent('ll-rebuild', {});
});
customElements.whenDefined('hui-entities-card').then(() => {
const EntitiesCard = customElements.get('hui-entities-card');
const oldRenderEntity = EntitiesCard.prototype.renderEntity;
EntitiesCard.prototype.renderEntity = function(config) {
const retval = oldRenderEntity.bind(this)(config);
if(!config.style) return retval;
if(!retval.values) return retval;
const row = retval.values[0];
if(!row || !row.updateComplete) return retval;
let entity_ids = config.entity_ids;
if (!entity_ids && find_entity_ids(config.style))
entity_ids = [config.entity];
const apply = () => {
applyStyle(row.shadowRoot, config.style, {
variables: {config},
entity_ids
}, !!config.debug_cardmod);
}
row.updateComplete.then(apply);
window.addEventListener("location-changed", apply);
return retval;
}
fireEvent('ll-rebuild', {});
});
customElements.whenDefined('hui-glance-card').then(() => {
const GlanceCard = customElements.get('hui-glance-card');
GlanceCard.prototype.firstUpdated = function () {
const entities = this.shadowRoot.querySelectorAll("ha-card div.entity");
entities.forEach((e) => {
const root = e.attachShadow({mode: "open"});
[...e.children].forEach((el) => root.appendChild(el));
const styletag = document.createElement("style");
root.appendChild(styletag);
styletag.innerHTML = `
:host {
box-sizing: border-box;
padding: 0 4px;
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
margin-bottom: 12px;
width: var(--glance-column-width, 20%);
}
div {
width: 100%;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.name {
min-height: var(--paper-font-body1_-_line-height, 20px);
}
state-badge {
margin: 8px 0;
}
`;
const config = e.entityConf;
if(!config.style) return;
let entity_ids = config.entity_ids;
if (!entity_ids && find_entity_ids(config.style))
entity_ids = [config.entity];
const apply = () => {
applyStyle(root, config.style, {
variables: {config},
entity_ids
}, !!config.debug_cardmod);
}
apply();
window.addEventListener("location-changed", apply);
});
} }
Object.defineProperty(HaCard.prototype, 'hass', {
get() {
return this._hass;
},
set(value) {
if(value !== this._hass) {
const oldval = this._hass;
this._hass = value;
this._requestUpdate('hass', oldval);
}
},
});
fireEvent('ll-rebuild', {}); fireEvent('ll-rebuild', {});
}); });