diff --git a/.devcontainer/configuration.yaml b/.devcontainer/configuration.yaml index 96d5bb5..4fcfce6 100644 --- a/.devcontainer/configuration.yaml +++ b/.devcontainer/configuration.yaml @@ -1,6 +1,9 @@ default_config: lovelace: mode: yaml + resources: + - url: http://127.0.0.1:5000/thermostat-dark-card.js + type: module demo: climate: - platform: generic_thermostat diff --git a/.devcontainer/ui-lovelace.yaml b/.devcontainer/ui-lovelace.yaml index 5eb3fca..899fdd0 100644 --- a/.devcontainer/ui-lovelace.yaml +++ b/.devcontainer/ui-lovelace.yaml @@ -1,6 +1,3 @@ -resources: - - url: http://127.0.0.1:5000/thermostat-dark-card.js - type: module views: - cards: - type: custom:thermostat-dark-card diff --git a/rollup.config.dev.js b/rollup.config.dev.js index ba6db58..7561424 100644 --- a/rollup.config.dev.js +++ b/rollup.config.dev.js @@ -13,7 +13,7 @@ export default { }, plugins: [ resolve(), - typescript(), + typescript({ sourceMap: true }), json(), babel({ exclude: "node_modules/**", diff --git a/src/const.ts b/src/const.ts index fb545ba..a154a19 100644 --- a/src/const.ts +++ b/src/const.ts @@ -1,6 +1,7 @@ export const HVAC_HEATING = 'heating'; export const HVAC_COOLING = 'cooling'; export const HVAC_IDLE = 'idle'; +export const HVAC_OFF = 'off'; export const PRESET_ECO = 'eco'; export const PRESET_AWAY = 'away'; diff --git a/src/thermostat-dark-card.ts b/src/thermostat-dark-card.ts index b91459d..1a10de0 100644 --- a/src/thermostat-dark-card.ts +++ b/src/thermostat-dark-card.ts @@ -10,13 +10,18 @@ import { PropertyValues, internalProperty, } from 'lit-element'; -import { HomeAssistant, hasConfigOrEntityChanged, LovelaceCardEditor, getLovelace } from 'custom-card-helpers'; // This is a community maintained npm module with common helper functions/types +import { + HomeAssistant, + hasConfigOrEntityChanged, + LovelaceCardEditor, + getLovelace +} from 'custom-card-helpers'; // This is a community maintained npm module with common helper functions/types import './editor'; import { ThermostatUserInterface } from './user-interface'; import type { ThermostatDarkCardConfig } from './types'; -import { HVAC_HEATING, HVAC_COOLING, HVAC_IDLE, GREEN_LEAF_MODES } from './const'; +import { HVAC_HEATING, HVAC_COOLING, HVAC_IDLE, HVAC_OFF, GREEN_LEAF_MODES, } from './const'; import { localize } from './localize/localize'; // This puts your card into the UI card picker dialog @@ -29,7 +34,7 @@ import { localize } from './localize/localize'; @customElement('thermostat-dark-card') export class ThermostatDarkCard extends ThermostatUserInterface { - @property({ attribute: false }) private _hass!: HomeAssistant; + @property({ attribute: false }) public _hass!: HomeAssistant; public static async getConfigElement(): Promise { return document.createElement('thermostat-dark-card-editor'); @@ -55,10 +60,9 @@ export class ThermostatDarkCard extends ThermostatUserInterface { } else hvacState = entity.attributes['hvac_action'] || entity.state; if (![HVAC_IDLE, HVAC_HEATING, HVAC_COOLING].includes(hvacState)) { - hvacState = config.hvac.states[hvacState] || HVAC_IDLE + hvacState = config.hvac.states[hvacState] || HVAC_OFF } - let awayState = entity.attributes[config.away.attribute]; //let awayState = 'on'; if (config.away.sensor.sensor) { @@ -159,6 +163,12 @@ export class ThermostatDarkCard extends ThermostatUserInterface { ${this.container}`; } + private _handleHold(): void { + //const config = this._config; + //if (!config) return; + //handleClick(this, this._hass!, this._evalActions(config, 'hold_action'), true, false); + } + private _controlSetPoints(): void { if (this.dual) { if ( @@ -201,13 +211,31 @@ export class ThermostatDarkCard extends ThermostatUserInterface { --thermostat-path-active-color: rgba(255, 255, 255, 0.8); --thermostat-path-active-color-large: rgba(255, 255, 255, 1); --thermostat-text-color: white; + --thermostat-toggle-color: grey; + --thermostat-toggle-off-color: darkgrey; + --thermostat-toggle-on-color: lightgrey; } .dial.has-thermo .dial__ico__leaf { visibility: hidden; } + .dial.hide-toggle .dial__ico__power { + display: none; + } .dial .dial__shape { transition: fill 0.5s; } + .dial__ico__power{ + fill: var(--thermostat-toggle-color); + cursor: pointer; + pointer-events: bounding-box; + } + .dial.dial--state--off .dial__ico__power{ + fill: var(--thermostat-toggle-off-color); + } + .dial.dial--state--heating .dial__ico__power, + .dial.dial--state--cooling .dial__ico__power{ + fill: var(--thermostat-toggle-on-color); + } .dial__ico__leaf { fill: #13eb13; opacity: 0; @@ -237,6 +265,7 @@ export class ThermostatDarkCard extends ThermostatUserInterface { transition: opacity 0.5s; } .dial__temperatureControl { + display: none; fill: white; opacity: 0; transition: opacity 0.2s; @@ -247,6 +276,9 @@ export class ThermostatDarkCard extends ThermostatUserInterface { .dial--edit .dial__editableIndicator { opacity: 1; } + .dial--edit .dial__temperatureControl { + display: block; + } .dial--state--off .dial__shape { fill: var(--thermostat-off-fill); } diff --git a/src/user-interface.ts b/src/user-interface.ts index f9ea97b..736693b 100644 --- a/src/user-interface.ts +++ b/src/user-interface.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/camelcase */ import { LitElement, internalProperty } from 'lit-element'; -import { HVAC_HEATING, HVAC_COOLING, HVAC_IDLE } from './const'; +import { HomeAssistant, fireEvent } from 'custom-card-helpers'; +import { HVAC_HEATING, HVAC_COOLING, HVAC_IDLE, HVAC_OFF } from './const'; class SvgUtil { // Rotate a cartesian point about given origin by X degrees static rotatePoint(point, angle, origin): Array { @@ -111,12 +112,17 @@ export class ThermostatUserInterface extends LitElement { @internalProperty() private _ticks!: Array; @internalProperty() private _controls!: Array; @internalProperty() private _root!: SVGElement; + @internalProperty() private _toggle!: SVGElement; @internalProperty() private minValue!: number; @internalProperty() private maxValue!: number; @internalProperty() private _timeoutHandler!: number; @internalProperty() private _hvacState!: string; @internalProperty() private _away!: boolean; @internalProperty() private _savedOptions: any; + @internalProperty() + public _hass!: HomeAssistant; + + private _touchTimeout: any; public get hvacState(): string { return this._hvacState; @@ -173,6 +179,7 @@ export class ThermostatUserInterface extends LitElement { config.chevron_size = Number(config.chevron_size); config.pending = Number(config.pending); config.idle_zone = Number(config.idle_zone); + this._touchTimeout = 0; this._config = config; // need certain options for updates this._ticks = []; // need for dynamic tick updates this._controls = []; // need for managing highlight and clicks @@ -183,11 +190,13 @@ export class ThermostatUserInterface extends LitElement { if (config.name) this._container.appendChild(this._buildTitle(config.name)); this._container.appendChild(style); const root = this._buildCore(config.diameter); + const toggle = this._buildPowerIcon(config.radius); root.appendChild(this._buildDial(config.radius)); root.appendChild(this._buildTicks(config.numTicks)); root.appendChild(this._buildRing(config.radius)); root.appendChild(this._buildLeaf(config.radius)); root.appendChild(this._buildThermoIcon(config.radius)); + root.appendChild(toggle); root.appendChild(this._buildDialSlot(1)); root.appendChild(this._buildDialSlot(2)); root.appendChild(this._buildDialSlot(3)); @@ -205,11 +214,48 @@ export class ThermostatUserInterface extends LitElement { this._container.appendChild(root); this._root = root; + this._toggle = toggle; this._buildControls(config.radius); if (this._savedOptions) { this.updateState(this._savedOptions); } + this._root.addEventListener('click', () => this._enableControls()); + this._root.addEventListener('touchstart', (e) => this._handleTouchStart(e, this)); + this._root.addEventListener('touchend', () => this._handleTouchEnd()); + this._root.addEventListener('touchcancel', (e) => this._handleTouchCancel(e)); + this._root.addEventListener('contextmenu', (e) => this._handleMoreInfo(e, this)); + this._toggle.addEventListener('click', (e) => this._handleToggle(e)) + } + + private _handleTouchCancel(e: TouchEvent): void { + e.preventDefault(); + window.clearTimeout(this._touchTimeout); + } + + private _handleTouchStart(e: TouchEvent, t: ThermostatUserInterface): void { + this._touchTimeout = setTimeout( + this._handleMoreInfo, 2*1000, e, t + ) + } + + private _handleTouchEnd(): void { + window.clearTimeout(this._touchTimeout); + } + + private _handleMoreInfo(e: MouseEvent, t: ThermostatUserInterface): void { + if (e) e.preventDefault(); + fireEvent(t, "hass-more-info", { + entityId: t._config!.entity, + }); + } + + private _handleToggle(e: MouseEvent) { + e.stopPropagation(); + const serviceCall = this._hvacState !== HVAC_OFF ? "turn_off" : "turn_on"; + this._hass!.callService("climate", serviceCall, { + entity_id: this._config!.entity + }); } _configDial(): void { @@ -415,6 +461,7 @@ export class ThermostatUserInterface extends LitElement { if (this._timeoutHandler) clearTimeout(this._timeoutHandler); this._updateEdit(true); this._updateClass('has-thermo', true); + this._updateClass('hide-toggle', true); this._updateText('target', this.temperature.target); this._updateText('low', this.temperature.low); this._updateText('high', this.temperature.high); @@ -422,17 +469,18 @@ export class ThermostatUserInterface extends LitElement { this._updateText('ambient', this._ambient); this._updateEdit(false); this._updateClass('has-thermo', false); + this._updateClass('hide-toggle', false); this._inControl = false; this._updateClass('in_control', this._inControl); config.control(); }, config.pending * 1000); } - _toggle(): boolean { - const config = this._config; - config.toggle(); - return false; - } + // _toggle(): boolean { + // const config = this._config; + // config.toggle(); + // return false; + // } _updateClass(className, flag): void { this.setSvgClass(this._root, className, flag); @@ -663,6 +711,23 @@ export class ThermostatUserInterface extends LitElement { }); } + private _buildPowerIcon(radius: number): SVGElement { + const width = 24; + const scale = 2.3; + const scaledWidth = width * scale; + const powerDef = 'M16.56,5.44L15.11,6.89C16.84,7.94 18,9.83 18,12A6,6 0 0,1 12,18A6,6 0 0,1 6,12C6,9.83 7.16,7.94 8.88,6.88L7.44,5.44C5.36,6.88 4,9.28 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12C20,9.28 18.64,6.88 16.56,5.44M13,3H11V13H13'; + const translate = [radius - (scaledWidth / 2), radius * 1.6]; + const color = this._hvacState == HVAC_OFF ? 'grey' : 'white' + return this, this.createSVGElement( + 'path', { + class: 'dial__ico__power', + fill: color, + d: powerDef, + transform: 'translate('+ translate[0] +',' + translate[1] +') scale('+ scale + ')', + } + ) + } + private _buildDialSlot(index: number): SVGElement { return this.createSVGElement('text', { class: 'dial__lbl dial__lbl--ring',