Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion gallery/src/data/entity.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand Down
48 changes: 48 additions & 0 deletions gallery/src/demos/demo-hui-light-card.js
Original file line number Diff line number Diff line change
@@ -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`
<demo-cards id='demos' configs="[[_configs]]"></demo-cards>
`;
}

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);
318 changes: 318 additions & 0 deletions src/panels/lovelace/cards/hui-light-card.ts
Original file line number Diff line number Diff line change
@@ -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()}
<ha-card>
${
!stateObj
? html`
<div class="not-found">Entity not available: ${
this._config.entity
}</div>`
: html`
<div id="light"></div>
<div id="tooltip">
<div class="icon-state">
<ha-icon
data-state="${stateObj.state}"
.icon="${stateIcon(stateObj)}"
style="${styleMap({
filter: this._computeBrightness(stateObj),
color: this._computeColor(stateObj),
})}"
@ha-click="${() => this._handleClick(false)}"
@ha-hold="${() => this._handleClick(true)}"
.longPress="${longPress()}"
></ha-icon>
<div
class="brightness"
@ha-click="${() => this._handleClick(false)}"
@ha-hold="${() => this._handleClick(true)}"
.longPress="${longPress()}"
></div>
<div class="name">${this._config.name ||
computeStateName(stateObj)}</div>
</div>
</div>
`
}

</ha-card>
`;
}

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}
<style>
:host {
display: block;
}
ha-card {
position: relative;
overflow: hidden;
}
#tooltip {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 100%;
text-align: center;
z-index: 15;
}
.icon-state {
display: block;
margin: auto;
width: 100%;
height: 100%;
transform: translate(0,25%);
}
#light {
margin: 0 auto;
padding-top: 16px;
padding-bottom: 16px;
}
#light .rs-bar.rs-transition.rs-first, .rs-bar.rs-transition.rs-second{
z-index: 20 !important;
}
#light .rs-range-color {
background-color: var(--primary-color);
}
#light .rs-path-color {
background-color: #d6d6d6;
}
#light .rs-handle {
background-color: #FFF;
padding: 7px;
border: 2px solid #d6d6d6;
}
#light .rs-handle.rs-focus {
border-color:var(--primary-color);
}
#light .rs-handle:after {
border-color: var(--primary-color);
background-color: var(--primary-color);
}
#light .rs-border {
border-color: transparent;
}
ha-icon {
margin: auto;
width: 76px;
height: 76px;
color: var(--paper-item-icon-color, #44739e);
cursor: pointer;
}
ha-icon[data-state=on] {
color: var(--paper-item-icon-active-color, #FDD835);
}
ha-icon[data-state=unavailable] {
color: var(--state-icon-unavailable-color);
}
.name {
padding-top: 40px;
font-size: 1.2rem;
}
.brightness {
font-size: 1.2rem;
position: absolute;
margin: 0 auto;
left: 50%;
top: 10%;
transform: translate(-50%);
opacity: 0;
transition: opacity .5s ease-in-out;
-moz-transition: opacity .5s ease-in-out;
-webkit-transition: opacity .5s ease-in-out;
cursor: pointer;
color: white;
text-shadow:
-1px -1px 0 #000,
1px -1px 0 #000,
-1px 1px 0 #000,
1px 1px 0 #000;
}
.show_brightness {
opacity: 1;
}
.not-found {
flex: 1;
background-color: yellow;
padding: 8px;
}
</style>
`;
}

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,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now I know why the gallery is flaky. We don't have support for brightness_pct in our demo hass for the gallery.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How can I add that?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where did you look?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

});
}

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);
2 changes: 2 additions & 0 deletions src/panels/lovelace/common/create-card-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -38,6 +39,7 @@ const CARD_TYPES = new Set([
"history-graph",
"horizontal-stack",
"iframe",
"light",
"map",
"markdown",
"media-control",
Expand Down
Loading