Last active
November 15, 2024 15:44
-
-
Save thomasloven/1de8c62d691e754f95b023105fe4b74b to your computer and use it in GitHub Desktop.
Simplest custom card
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Simplest possible custom card | |
// Does nothing. Doesn't look like anything | |
class MyCustomCard extends HTMLElement { | |
setConfig(config) { | |
// The config object contains the configuration specified by the user in ui-lovelace.yaml | |
// for your card. | |
// It will minimally contain: | |
// config.type = "custom:my-custom-card" | |
// `setConfig` MUST be defined - and is in fact the only function that must be. | |
// It doesn't need to actually DO anything, though. | |
// Note that setConfig will ALWAYS be called at the start of the lifetime of the card | |
// BEFORE the `hass` object is first provided. | |
// It MAY be called several times during the lifetime of the card, e.g. if the configuration | |
// of the card is changed. | |
} | |
set hass(hass) { | |
// Whenever anything updates in Home Assistant, the hass object is updated | |
// and passed out to every card. If you want to react to state changes, this is where | |
// you do it. If not, you can just ommit this setter entirely. | |
// Note that if you do NOT have a `set hass(hass)` in your class, you can access the hass | |
// object through `this.hass`. But if you DO have it, you need to save the hass object | |
// manually, thusly: | |
this._hass = hass; | |
} | |
getCardSize() { | |
// The height of your card. Home Assistant uses this to automatically | |
// distribute all cards over the available columns. | |
// This is actually optional. If not present, the cardHeight is assumed to be 1. | |
return 1; | |
} | |
} | |
// This registers the card class as a custom element that can be included in lovelace by | |
// type: custom:my-custom-card | |
customElements.define('my-custom-card', MyCustomCard); | |
/* To use this card: | |
- Put this file in <config>/www/my-custom-card.js | |
- Add "/local/my-custom-card.js" to your lovelace resources | |
- Refresh your browser | |
- Add a new lovelace card and set its configuration to: | |
type: custom:my-custom-card | |
*/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// A custom card that actually looks like something | |
// Not like something good, mind you, but *something* at least. | |
class MyCustomCard2 extends HTMLElement { | |
setConfig(config) { | |
this._config = config; | |
// Example configuration: | |
// | |
// type: custom:my-custom-card2 | |
// entity: light.bed_light | |
// | |
if(!config.entity) { | |
// If no entity was specified, this will display a red error card with the message below | |
throw new Error('You need to define an entity'); | |
} | |
// Make sure this only runs once | |
if(!this.setupComplete) { | |
// A ha-card element should be the base of all cards | |
// Best practice, and makes themes and stuff work | |
const card = document.createElement("ha-card"); | |
// At this point, we don't necessarily know anything about the current state | |
// of anything, but we can set up the general structure of the card. | |
this.nameDiv = document.createElement("div"); | |
card.appendChild(this.nameDiv); | |
this.button = document.createElement("button"); | |
this.button.addEventListener("click", () => this.buttonClicked()); | |
card.appendChild(this.button); | |
this.appendChild(card); | |
this.setupComplete = true; | |
} | |
} | |
set hass(hass) { | |
this._hass = hass; | |
// Update the card in case anything has changed | |
if(!this._config) return; // Can't assume setConfig is called before hass is set | |
const stateObj = hass.states[this._config.entity]; | |
if(!stateObj) return; // This could be handled more gracefully | |
this.nameDiv.innerHTML = stateObj.attributes.friendly_name || stateObj.entity_id; | |
this.button.innerHTML = stateObj.state === "on" ? "Turn off" : "Turn on"; | |
} | |
buttonClicked() { | |
const stateObj = this._hass.states[this._config.entity]; | |
// Sanity checking is left as an exercise for the reader | |
const service = stateObj.state === "on" ? "turn_off" : "turn_on"; | |
// Turn on or off the light | |
// https://developers.home-assistant.io/docs/frontend_data#hasscallservicedomain-service-data | |
this._hass.callService("light", service, {entity_id: this._config.entity}); | |
} | |
} | |
customElements.define('my-custom-card2', MyCustomCard2); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// A version of the same custom card which uses Lit (https://lit.dev) like the core lovelace cards do. | |
// Lit is imported from a CDN here, but it can also be bundled with your card with webpack or rollup or the like | |
import { html, LitElement } from "https://unpkg.com/lit?module"; | |
class MyCustomCard3 extends LitElement { | |
// This will make parts of the card rerender when this.hass or this._config is changed. | |
// this.hass is updated by Home Assistant whenever anything happens in your system. | |
static get properties() { | |
return { | |
hass: {}, | |
_config: {}, | |
}; | |
} | |
setConfig(config) { | |
this._config = config; | |
} | |
// The render() function of a LitElement returns the HTML of your card, and any time one or the | |
// properties defined above are updated, the correct parts of the rendered html are magically | |
// replaced with the new values. Check https://lit.dev for more info. | |
render() { | |
if (!this.hass || !this._config) { | |
return html``; | |
} | |
const stateObj = this.hass.states[this._config.entity]; | |
if (!stateObj) { | |
return html` <ha-card>Unknown entity: ${this._config.entity}</ha-card> `; | |
} | |
// @click below is also LitElement magic | |
return html` | |
<ha-card> | |
<div>${stateObj.attributes.friendly_name || stateObj.entity_id}</div> | |
<button @click=${this.buttonClicked}> | |
Turn ${stateObj.state === "on" ? "off" : "on"} | |
</button> | |
</ha-card> | |
`; | |
} | |
buttonClicked() { | |
const stateObj = this.hass.states[this._config.entity]; | |
const service = stateObj.state === "on" ? "turn_off" : "turn_on"; | |
this.hass.callService("light", service, { entity_id: this._config.entity }); | |
} | |
} | |
customElements.define("my-custom-card3", MyCustomCard3); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// A version of the same custom card that shows up in the card picker dialog and also has a graphical editor | |
// This also used Lit because it's easy, but everything can be done without that too. | |
import { html, LitElement } from "https://unpkg.com/lit?module"; | |
// First we need to make some changes to the custom card class | |
class MyCustomCard4 extends LitElement { | |
static getConfigElement() { | |
// Create and return an editor element | |
return document.createElement("my-custom-card-editor"); | |
} | |
static getStubConfig() { | |
// Return a minimal configuration that will result in a working card configuration | |
return { entity: "" }; | |
} | |
// The rest of MyCustomCard4 is exactly like MyCustomCard3 above | |
// ... | |
// | |
} | |
customElements.define("my-custom-card4", MyCustomCard4); | |
// Next we add our card to the list of custom cards for the card picker | |
window.customCards = window.customCards || []; // Create the list if it doesn't exist. Careful not to overwrite it | |
window.customCards.push({ | |
type: "my-custom-card4", | |
name: "My Custom Card", | |
description: "A cool custom card", | |
}); | |
// Finally we create and register the editor itself | |
class MyCustomCardEditor extends LitElement { | |
static get properties() { | |
return { | |
hass: {}, | |
_config: {}, | |
}; | |
} | |
// setConfig works the same way as for the card itself | |
setConfig(config) { | |
this._config = config; | |
} | |
// This function is called when the input element of the editor loses focus | |
entityChanged(ev) { | |
// We make a copy of the current config so we don't accidentally overwrite anything too early | |
const _config = Object.assign({}, this._config); | |
// Then we update the entity value with what we just got from the input field | |
_config.entity = ev.target.value; | |
// And finally write back the updated configuration all at once | |
this._config = _config; | |
// A config-changed event will tell lovelace we have made changed to the configuration | |
// this make sure the changes are saved correctly later and will update the preview | |
const event = new CustomEvent("config-changed", { | |
detail: { config: _config }, | |
bubbles: true, | |
composed: true, | |
}); | |
this.dispatchEvent(event); | |
} | |
render() { | |
if (!this.hass || !this._config) { | |
return html``; | |
} | |
// @focusout below will call entityChanged when the input field loses focus (e.g. the user tabs away or clicks outside of it) | |
return html` | |
Entity: | |
<input | |
.value=${this._config.entity} | |
@focusout=${this.entityChanged} | |
></input> | |
`; | |
} | |
} | |
customElements.define("my-custom-card-editor", MyCustomCardEditor); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// If you don't want to rely on external resources, you will have to "bundle" the lit module into your card. | |
// One way to do this is using Browserify (https://browserify.org/) with the esmify plugin (https://github.com/mattdesl/esmify) | |
// This requires you to have npm installed. | |
// > npm install --save-dev browserify esmify | |
// > npm install lit browser-resolve | |
// Then you replace the import statement in the beginning of the file with | |
import { html, LitElement } from "lit"; | |
// and finally bundle your file with | |
// > browserify -p esmify my-custom-card-4-bundled.js > my-custom-card.js | |
// This should give you a file my-custom-card.js which you can load as a resource. |
May be useful to include that the object you push to window.customCards
can include a documentationURL
key, which will generate a help link to that URL in the frontend card editor.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
// this.hass is updated by Home Assistant whenever anything happens in your system.
Right. I did check this with with a log statement. It renders, when I trigger a different entity.
Upon each update of any entity the card gets rendered. In 99% it's completely unrelated. And yes the official documentation suggests to do it this way. All cards get continuously re-rendered for no reason if they follow this approach.
I suggest a different approach. Put the relevant parts into single reactive states (That is Lit' terminology for internal reactive properties). Use
set hass(hass)
to set them up, but never makehass
a public reactive property.