Skip to content

Instantly share code, notes, and snippets.

@thomasloven
Last active November 15, 2024 15:44
Show Gist options
  • Save thomasloven/1de8c62d691e754f95b023105fe4b74b to your computer and use it in GitHub Desktop.
Save thomasloven/1de8c62d691e754f95b023105fe4b74b to your computer and use it in GitHub Desktop.
Simplest custom card
// 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
*/
// 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);
// 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);
// 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);
// 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.
@elmar-hinz
Copy link

elmar-hinz commented Jun 3, 2023

// 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 make hass a public reactive property.

@karwosts
Copy link

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