diff --git a/gallery/src/data/entity.js b/gallery/src/data/entity.js index b2b90a50ca..d705322d81 100644 --- a/gallery/src/data/entity.js +++ b/gallery/src/data/entity.js @@ -55,7 +55,9 @@ export class LightEntity extends Entity { if (service === "turn_on") { // eslint-disable-next-line - const { brightness, hs_color } = data; + let { brightness, hs_color, brightness_pct } = data; + // eslint-disable-next-line + brightness = (255 * brightness_pct) / 100; this.update( "on", Object.assign(this.attributes, { diff --git a/gallery/src/demos/demo-hui-light-card.js b/gallery/src/demos/demo-hui-light-card.js new file mode 100644 index 0000000000..8f5b2cca28 --- /dev/null +++ b/gallery/src/demos/demo-hui-light-card.js @@ -0,0 +1,48 @@ +import { html } from "@polymer/polymer/lib/utils/html-tag.js"; +import { PolymerElement } from "@polymer/polymer/polymer-element.js"; + +import getEntity from "../data/entity.js"; +import provideHass from "../data/provide_hass.js"; +import "../components/demo-cards.js"; + +const ENTITIES = [ + getEntity("light", "bed_light", "on", { + friendly_name: "Bed Light", + brightness: 130, + }), +]; + +const CONFIGS = [ + { + heading: "Basic example", + config: ` +- type: light + entity: light.bed_light + `, + }, +]; + +class DemoLightEntity extends PolymerElement { + static get template() { + return html` + + `; + } + + static get properties() { + return { + _configs: { + type: Object, + value: CONFIGS, + }, + }; + } + + ready() { + super.ready(); + const hass = provideHass(this.$.demos); + hass.addEntities(ENTITIES); + } +} + +customElements.define("demo-hui-light-card", DemoLightEntity); diff --git a/src/panels/lovelace/cards/hui-light-card.ts b/src/panels/lovelace/cards/hui-light-card.ts new file mode 100644 index 0000000000..83058920c9 --- /dev/null +++ b/src/panels/lovelace/cards/hui-light-card.ts @@ -0,0 +1,318 @@ +import { + html, + LitElement, + PropertyValues, + PropertyDeclarations, +} from "@polymer/lit-element"; +import { fireEvent } from "../../../common/dom/fire_event.js"; +import { styleMap } from "lit-html/directives/styleMap.js"; +import computeStateName from "../../../common/entity/compute_state_name.js"; +import stateIcon from "../../../common/entity/state_icon.js"; +import { jQuery } from "../../../resources/jquery"; + +import "../../../components/ha-card.js"; +import "../../../components/ha-icon.js"; +import { roundSliderStyle } from "../../../resources/jquery.roundslider"; + +import { HomeAssistant, LightEntity } from "../../../types.js"; +import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin"; +import { LovelaceCard, LovelaceConfig } from "../types.js"; +import { longPress } from "../common/directives/long-press-directive"; +import { TemplateResult } from "lit-html"; + +const lightConfig = { + radius: 80, + step: 1, + circleShape: "pie", + startAngle: 315, + width: 5, + min: 1, + max: 100, + sliderType: "min-range", + lineCap: "round", + handleSize: "+12", + showTooltip: false, +}; + +interface Config extends LovelaceConfig { + entity: string; + name?: string; +} + +export class HuiLightCard extends hassLocalizeLitMixin(LitElement) + implements LovelaceCard { + public hass?: HomeAssistant; + private _config?: Config; + private _brightnessTimout?: number; + + static get properties(): PropertyDeclarations { + return { + hass: {}, + _config: {}, + }; + } + + public getCardSize(): number { + return 2; + } + + public setConfig(config: Config): void { + if (!config.entity || config.entity.split(".")[0] !== "light") { + throw new Error("Specify an entity from within the light domain."); + } + + this._config = config; + } + + protected render(): TemplateResult { + if (!this.hass || !this._config) { + return html``; + } + + const stateObj = this.hass.states[this._config!.entity] as LightEntity; + + return html` + ${this.renderStyle()} + + ${ + !stateObj + ? html` +
Entity not available: ${ + this._config.entity + }
` + : html` +
+
+
+ +
+
${this._config.name || + computeStateName(stateObj)}
+
+
+ ` + } + +
+ `; + } + + protected shouldUpdate(changedProps: PropertyValues): boolean { + if (changedProps.get("hass")) { + return ( + (changedProps.get("hass") as any).states[this._config!.entity] !== + this.hass!.states[this._config!.entity] + ); + } + return (changedProps as unknown) as boolean; + } + + protected firstUpdated(): void { + const brightness = this.hass!.states[this._config!.entity].attributes + .brightness; + jQuery("#light", this.shadowRoot).roundSlider({ + ...lightConfig, + change: (value) => this._setBrightness(value), + drag: (value) => this._dragEvent(value), + start: () => this._showBrightness(), + stop: () => this._hideBrightness(), + }); + this.shadowRoot!.querySelector(".brightness")!.innerHTML = + (Math.round((brightness / 254) * 100) || 0) + "%"; + } + + protected updated(): void { + const attrs = this.hass!.states[this._config!.entity].attributes; + + jQuery("#light", this.shadowRoot).roundSlider({ + value: Math.round((attrs.brightness / 254) * 100) || 0, + }); + } + + private renderStyle(): TemplateResult { + return html` + ${roundSliderStyle} + + `; + } + + private _dragEvent(e: any): void { + this.shadowRoot!.querySelector(".brightness")!.innerHTML = e.value + "%"; + } + + private _showBrightness(): void { + clearTimeout(this._brightnessTimout); + this.shadowRoot!.querySelector(".brightness")!.classList.add( + "show_brightness" + ); + } + + private _hideBrightness(): void { + this._brightnessTimout = window.setTimeout(() => { + this.shadowRoot!.querySelector(".brightness")!.classList.remove( + "show_brightness" + ); + }, 500); + } + + private _setBrightness(e: any): void { + this.hass!.callService("light", "turn_on", { + entity_id: this._config!.entity, + brightness_pct: e.value, + }); + } + + private _computeBrightness(stateObj: LightEntity): string { + if (!stateObj.attributes.brightness) { + return ""; + } + const brightness = stateObj.attributes.brightness; + return `brightness(${(brightness + 245) / 5}%)`; + } + + private _computeColor(stateObj: LightEntity): string { + if (!stateObj.attributes.hs_color) { + return ""; + } + const [hue, sat] = stateObj.attributes.hs_color; + if (sat <= 10) { + return ""; + } + return `hsl(${hue}, 100%, ${100 - sat / 2}%)`; + } + + private _handleClick(hold: boolean): void { + const entityId = this._config!.entity; + + if (hold) { + fireEvent(this, "hass-more-info", { + entityId, + }); + return; + } + + this.hass!.callService("light", "toggle", { + entity_id: entityId, + }); + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-light-card": HuiLightCard; + } +} + +customElements.define("hui-light-card", HuiLightCard); diff --git a/src/panels/lovelace/common/create-card-element.js b/src/panels/lovelace/common/create-card-element.js index 3759bc814c..92db6f4a6f 100644 --- a/src/panels/lovelace/common/create-card-element.js +++ b/src/panels/lovelace/common/create-card-element.js @@ -10,6 +10,7 @@ import "../cards/hui-glance-card.ts"; import "../cards/hui-history-graph-card.js"; import "../cards/hui-horizontal-stack-card.ts"; import "../cards/hui-iframe-card.ts"; +import "../cards/hui-light-card"; import "../cards/hui-map-card.js"; import "../cards/hui-markdown-card.ts"; import "../cards/hui-media-control-card.js"; @@ -38,6 +39,7 @@ const CARD_TYPES = new Set([ "history-graph", "horizontal-stack", "iframe", + "light", "map", "markdown", "media-control", diff --git a/src/types.ts b/src/types.ts index 359425876a..6cc016d2ee 100644 --- a/src/types.ts +++ b/src/types.ts @@ -111,3 +111,13 @@ export type ClimateEntity = HassEntityBase & { aux_heat?: "on" | "off"; }; }; + +export type LightEntity = HassEntityBase & { + attributes: HassEntityAttributeBase & { + min_mireds: number; + max_mireds: number; + friendly_name: string; + brightness: number; + hs_color: number[]; + }; +};