From 1abe9aa11ce48bac3f27f0c86a015f24f0cef7a2 Mon Sep 17 00:00:00 2001 From: fineemb Date: Wed, 12 Feb 2020 18:30:43 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A7=A3=E5=86=B3HACS=E5=AE=89=E8=A3=85?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 8 +- dist/main.js | 142 -------- dist/styles.js | 336 ------------------ dist/thermostat_card.lib.js | 661 ------------------------------------ 4 files changed, 4 insertions(+), 1143 deletions(-) delete mode 100644 dist/main.js delete mode 100644 dist/styles.js delete mode 100644 dist/thermostat_card.lib.js diff --git a/README.md b/README.md index c4c80c5..f4833c2 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ * @Description : * @Date : 2020-02-03 12:52:45 * @LastEditors : fineemb - * @LastEditTime : 2020-02-03 13:59:13 + * @LastEditTime : 2020-02-12 18:30:09 --> # Lovelace Thermostat Card @@ -25,12 +25,12 @@ A simple thermostat implemented in CSS and SVG based on this.openProp()); - this._container.appendChild(this._load_icon('','')); - this.c_body = document.createElement('div'); - this.c_body.className = 'c_body'; - const root = this._buildCore(config.diameter); - root.appendChild(this._buildDial(config.radius)); - root.appendChild(this._buildTicks(config.num_ticks)); - root.appendChild(this._buildRing(config.radius)); - root.appendChild(this._buildThermoIcon(config.radius)); - root.appendChild(this._buildDialSlot(1)); - root.appendChild(this._buildDialSlot(2)); - root.appendChild(this._buildDialSlot(3)); - - root.appendChild(this._buildText(config.radius, 'ambient', 0)); - root.appendChild(this._buildText(config.radius, 'target', 0)); - root.appendChild(this._buildText(config.radius, 'low', -config.radius / 2.5)); - root.appendChild(this._buildText(config.radius, 'high', config.radius / 3)); - root.appendChild(this._buildChevrons(config.radius, 0, 'low', 0.7, -config.radius / 2.5)); - root.appendChild(this._buildChevrons(config.radius, 0, 'high', 0.7, config.radius / 3)); - root.appendChild(this._buildChevrons(config.radius, 0, 'target', 1, 0)); - root.appendChild(this._buildChevrons(config.radius, 180, 'low', 0.7, -config.radius / 2.5)); - root.appendChild(this._buildChevrons(config.radius, 180, 'high', 0.7, config.radius / 3)); - root.appendChild(this._buildChevrons(config.radius, 180, 'target', 1, 0)); - - this.c_body.appendChild(root); - this._container.appendChild(this.c_body); - this._root = root; - this._buildControls(config.radius); - this._root.addEventListener('click', () => this._enableControls()); - this._container.appendChild(this._buildDialog()); - this._main_icon.addEventListener('click', () => this._openDialog()); - this._modes_dialog.addEventListener('click', () => this._hideDialog()); - } - - updateState(options,hass) { - - const config = this._config; - const away = options.away || false; - this.entity = options.entity; - this.min_value = options.min_value; - this.max_value = options.max_value; - this.hvac_state = options.hvac_state; - this.preset_mode = options.preset_mode; - this.hvac_modes = options.hvac_modes; - this.temperature = { - low: options.target_temperature_low, - high: options.target_temperature_high, - target: options.target_temperature, - ambient: options.ambient_temperature, - } - - this._updateClass('has_dual', this.dual); - let tick_label, from, to; - const tick_indexes = []; - const ambient_index = SvgUtil.restrictToRange(Math.round((this.ambient - this.min_value) / (this.max_value - this.min_value) * config.num_ticks), 0, config.num_ticks - 1); - const target_index = SvgUtil.restrictToRange(Math.round((this._target - this.min_value) / (this.max_value - this.min_value) * config.num_ticks), 0, config.num_ticks - 1); - const high_index = SvgUtil.restrictToRange(Math.round((this._high - this.min_value) / (this.max_value - this.min_value) * config.num_ticks), 0, config.num_ticks - 1); - const low_index = SvgUtil.restrictToRange(Math.round((this._low - this.min_value) / (this.max_value - this.min_value) * config.num_ticks), 0, config.num_ticks - 1); - - if (!this.dual) { - tick_label = [this._target, this.ambient].sort(); - this._updateTemperatureSlot(tick_label[0], -8, `temperature_slot_1`); - this._updateTemperatureSlot(tick_label[1], 8, `temperature_slot_2`); - - switch (this.hvac_state) { - case 'dry': - this._load_icon(this.hvac_state, 'water-percent'); - break; - case 'fan_only': - this._load_icon(this.hvac_state, 'fan'); - break; - case 'cool': - this._load_icon(this.hvac_state, 'snowflake'); - - if (target_index <= ambient_index) { - from = target_index; - to = ambient_index; - } - break; - case 'heat': - this._load_icon(this.hvac_state, 'fire'); - - if (target_index >= ambient_index) { - from = ambient_index; - to = target_index; - } - break; - case 'auto': - this._load_icon(this.hvac_state, 'atom'); - - if (target_index >= ambient_index) { - from = ambient_index; - to = target_index; - } - break; - case 'off': - this._load_icon(this.hvac_state, 'power'); - break; - default: - this._load_icon('more', 'dots-horizontal'); - } - } else { - tick_label = [this._low, this._high, this.ambient].sort(); - this._updateTemperatureSlot(null, 0, `temperature_slot_1`); - this._updateTemperatureSlot(null, 0, `temperature_slot_2`); - this._updateTemperatureSlot(null, 0, `temperature_slot_3`); - - switch (this.hvac_state) { - case 'cool': - if (high_index < ambient_index) { - from = high_index; - to = ambient_index; - this._updateTemperatureSlot(this.ambient, 8, `temperature_slot_3`); - this._updateTemperatureSlot(this._high, -8, `temperature_slot_2`); - } - break; - case 'heat': - if (low_index > ambient_index) { - from = ambient_index; - to = low_index; - this._updateTemperatureSlot(this.ambient, -8, `temperature_slot_1`); - this._updateTemperatureSlot(this._low, 8, `temperature_slot_2`); - } - break; - case 'off': - if (high_index < ambient_index) { - from = high_index; - to = ambient_index; - this._updateTemperatureSlot(this.ambient, 8, `temperature_slot_3`); - this._updateTemperatureSlot(this._high, -8, `temperature_slot_2`); - } - if (low_index > ambient_index) { - from = ambient_index; - to = low_index; - this._updateTemperatureSlot(this.ambient, -8, `temperature_slot_1`); - this._updateTemperatureSlot(this._low, 8, `temperature_slot_2`); - } - break; - default: - } - } - - tick_label.forEach(item => tick_indexes.push(SvgUtil.restrictToRange(Math.round((item - this.min_value) / (this.max_value - this.min_value) * config.num_ticks), 0, config.num_ticks - 1))); - this._updateTicks(from, to, tick_indexes, this.hvac_state); - this._updateColor(this.hvac_state, this.preset_mode); - this._updateText('ambient', this.ambient); - this._updateEdit(false); - this._updateDialog(this.hvac_modes,hass); - } - - _temperatureControlClicked(index) { - const config = this._config; - let chevron; - this._root.querySelectorAll('path.dial__chevron').forEach(el => SvgUtil.setClass(el, 'pressed', false)); - if (this.in_control) { - if (this.dual) { - switch (index) { - case 0: - // clicked top left - chevron = this._root.querySelectorAll('path.dial__chevron--low')[1]; - this._low = this._low + config.step; - if ((this._low + config.idle_zone) >= this._high) this._low = this._high - config.idle_zone; - break; - case 1: - // clicked top right - chevron = this._root.querySelectorAll('path.dial__chevron--high')[1]; - this._high = this._high + config.step; - if (this._high > this.max_value) this._high = this.max_value; - break; - case 2: - // clicked bottom right - chevron = this._root.querySelectorAll('path.dial__chevron--high')[0]; - this._high = this._high - config.step; - if ((this._high - config.idle_zone) <= this._low) this._high = this._low + config.idle_zone; - break; - case 3: - // clicked bottom left - chevron = this._root.querySelectorAll('path.dial__chevron--low')[0]; - this._low = this._low - config.step; - if (this._low < this.min_value) this._low = this.min_value; - break; - } - SvgUtil.setClass(chevron, 'pressed', true); - setTimeout(() => SvgUtil.setClass(chevron, 'pressed', false), 200); - if (config.highlight_tap) - SvgUtil.setClass(this._controls[index], 'control-visible', true); - } - else { - if (index < 2) { - // clicked top - chevron = this._root.querySelectorAll('path.dial__chevron--target')[1]; - this._target = this._target + config.step; - if (this._target > this.max_value) this._target = this.max_value; - if (config.highlight_tap) { - SvgUtil.setClass(this._controls[0], 'control-visible', true); - SvgUtil.setClass(this._controls[1], 'control-visible', true); - } - } else { - // clicked bottom - chevron = this._root.querySelectorAll('path.dial__chevron--target')[0]; - this._target = this._target - config.step; - if (this._target < this.min_value) this._target = this.min_value; - if (config.highlight_tap) { - SvgUtil.setClass(this._controls[2], 'control-visible', true); - SvgUtil.setClass(this._controls[3], 'control-visible', true); - } - } - SvgUtil.setClass(chevron, 'pressed', true); - setTimeout(() => SvgUtil.setClass(chevron, 'pressed', false), 200); - } - if (config.highlight_tap) { - setTimeout(() => { - SvgUtil.setClass(this._controls[0], 'control-visible', false); - SvgUtil.setClass(this._controls[1], 'control-visible', false); - SvgUtil.setClass(this._controls[2], 'control-visible', false); - SvgUtil.setClass(this._controls[3], 'control-visible', false); - }, 200); - } - } else { - this._enableControls(); - } - } - - _updateEdit(show_edit) { - SvgUtil.setClass(this._root, 'dial--edit', show_edit); - } - - _enableControls() { - const config = this._config; - this._in_control = true; - this._updateClass('in_control', this.in_control); - if (this._timeoutHandler) clearTimeout(this._timeoutHandler); - this._updateEdit(true); - //this._updateClass('has-thermo', true); - this._updateText('target', this.temperature.target); - this._updateText('low', this.temperature.low); - this._updateText('high', this.temperature.high); - this._timeoutHandler = setTimeout(() => { - this._updateText('ambient', this.ambient); - this._updateEdit(false); - //this._updateClass('has-thermo', false); - this._in_control = false; - this._updateClass('in_control', this.in_control); - config.control(); - }, config.pending * 1000); - } - - _updateClass(class_name, flag) { - SvgUtil.setClass(this._root, class_name, flag); - } - - _updateText(id, value) { - const lblTarget = this._root.querySelector(`#${id}`).querySelectorAll('tspan'); - const text = Math.floor(value); - if (value) { - lblTarget[0].textContent = text; - if (value % 1 != 0) { - lblTarget[1].textContent = Math.round(value % 1 * 10); - } else { - lblTarget[1].textContent = ''; - } - } - - if (this.in_control && id == 'target' && this.dual) { - lblTarget[0].textContent = 'ยท'; - } - } - - _updateTemperatureSlot(value, offset, slot) { - - const config = this._config; - const lblSlot1 = this._root.querySelector(`#${slot}`) - lblSlot1.textContent = value != null ? SvgUtil.superscript(value) : ''; - - const peggedValue = SvgUtil.restrictToRange(value, this.min_value, this.max_value); - const position = [config.radius, config.ticks_outer_radius - (config.ticks_outer_radius - config.ticks_inner_radius) / 2]; - let degs = config.tick_degrees * (peggedValue - this.min_value) / (this.max_value - this.min_value) - config.offset_degrees + offset; - const pos = SvgUtil.rotatePoint(position, degs, [config.radius, config.radius]); - SvgUtil.attributes(lblSlot1, { - x: pos[0], - y: pos[1] - }); - } - - _updateColor(state, preset_mode) { - - if(preset_mode === undefined) - return; - if(state != 'off' && preset_mode.toLowerCase() == 'idle') - state = 'idle' - - this._root.classList.forEach(c => { - if (c.indexOf('dial--state--') != -1) - this._root.classList.remove(c); - }); - this._root.classList.add('dial--state--' + state); - } - - _updateTicks(from, to, large_ticks, hvac_state) { - const config = this._config; - - const tickPoints = [ - [config.radius - 1, config.ticks_outer_radius], - [config.radius + 1, config.ticks_outer_radius], - [config.radius + 1, config.ticks_inner_radius], - [config.radius - 1, config.ticks_inner_radius] - ]; - const tickPointsLarge = [ - [config.radius - 1.5, config.ticks_outer_radius], - [config.radius + 1.5, config.ticks_outer_radius], - [config.radius + 1.5, config.ticks_inner_radius + 20], - [config.radius - 1.5, config.ticks_inner_radius + 20] - ]; - - this._ticks.forEach((tick, index) => { - let isLarge = false; - let isActive = (index >= from && index <= to) ? 'active '+hvac_state : ''; - large_ticks.forEach(i => isLarge = isLarge || (index == i)); - if (isLarge) isActive += ' large'; - const theta = config.tick_degrees / config.num_ticks; - SvgUtil.attributes(tick, { - d: SvgUtil.pointsToPath(SvgUtil.rotatePoints(isLarge ? tickPointsLarge : tickPoints, index * theta - config.offset_degrees, [config.radius, config.radius])), - class: isActive - }); - }); - } - _updateDialog(modes,hass){ - this._modes_dialog.innerHTML = ""; - for(var i=0;i` - d.addEventListener('click', (e) => this._setMode(e,mode,hass)); - //this._modes[i].push(d); - this._modes_dialog.appendChild(d) - } - } - _buildCore(diameter) { - return SvgUtil.createSVGElement('svg', { - width: '100%', - height: '100%', - viewBox: '0 0 ' + diameter + ' ' + diameter, - class: 'dial' - }) - } - - openProp() { - this._config.propWin(this.entity.entity_id) - } - _openDialog(){ - this._modes_dialog.className = "dialog modes"; - } - _hideDialog(){ - this._modes_dialog.className = "dialog modes hide"; - } - _setMode(e, mode,hass){ - console.log(mode); - let config = this._config; - if (this._timeoutHandlerMode) clearTimeout(this._timeoutHandlerMode); - hass.callService('climate', 'set_hvac_mode', { - entity_id: this._config.entity, - hvac_mode: mode, - }); - this._modes_dialog.className = "dialog modes "+mode+" pending"; - this._timeoutHandlerMode = setTimeout(() => { - this._modes_dialog.className = "dialog modes hide"; - }, config.pending * 1000); - e.stopPropagation(); - } - - _buildTitle(title) { - this._ic = document.createElement('div'); - this._ic.className = "prop"; - this._ic.innerHTML = ` - - `; - this._container.innerHTML = ` -
-
${title}
-
- `; - return this._ic; - } - _load_icon(state, ic_name){ - - let ic_dot = 'dot_r' - if(ic_name == ''){ - ic_dot = 'dot_h' - } - - this._main_icon.innerHTML = ` -
-
-
-
- `; - return this._main_icon; - } - _buildDialog(){ - this._modes_dialog.className = "dialog modes hide"; - return this._modes_dialog; - } - // build black dial - _buildDial(radius) { - return SvgUtil.createSVGElement('circle', { - cx: radius, - cy: radius, - r: radius, - class: 'dial__shape' - }) - } - // build circle around - _buildRing(radius) { - return SvgUtil.createSVGElement('path', { - d: SvgUtil.donutPath(radius, radius, radius - 4, radius - 8), - class: 'dial__editableIndicator', - }) - } - - _buildTicks(num_ticks) { - const tick_element = SvgUtil.createSVGElement('g', { - class: 'dial__ticks' - }); - for (let i = 0; i < num_ticks; i++) { - const tick = SvgUtil.createSVGElement('path', {}) - this._ticks.push(tick); - tick_element.appendChild(tick); - } - return tick_element; - } - - _buildChevrons(radius, rotation, id, scale, offset) { - const config = this._config; - const translation = rotation > 0 ? -1 : 1; - const width = config.chevron_size; - const chevron_def = ["M", 0, 0, "L", width / 2, width * 0.3, "L", width, 0].map((x) => isNaN(x) ? x : x * scale).join(' '); - const translate = [radius - width / 2 * scale * translation + offset, radius + 70 * scale * 1.1 * translation]; - const chevron = SvgUtil.createSVGElement('path', { - class: `dial__chevron dial__chevron--${id}`, - d: chevron_def, - transform: `translate(${translate[0]},${translate[1]}) rotate(${rotation})` - }); - return chevron; - } - - _buildThermoIcon(radius) { - const thermoScale = radius / 3 / 100; - const thermoDef = 'M 37.999 38.261 V 7 c 0 -3.859 -3.141 -7 -7 -7 s -7 3.141 -7 7 v 31.261 c -3.545 2.547 -5.421 6.769 -4.919 11.151 c 0.629 5.482 5.066 9.903 10.551 10.512 c 0.447 0.05 0.895 0.074 1.339 0.074 c 2.956 0 5.824 -1.08 8.03 -3.055 c 2.542 -2.275 3.999 -5.535 3.999 -8.943 C 42.999 44.118 41.14 40.518 37.999 38.261 Z M 37.666 55.453 c -2.146 1.921 -4.929 2.8 -7.814 2.482 c -4.566 -0.506 -8.261 -4.187 -8.785 -8.752 c -0.436 -3.808 1.28 -7.471 4.479 -9.56 l 0.453 -0.296 V 38 h 1 c 0.553 0 1 -0.447 1 -1 s -0.447 -1 -1 -1 h -1 v -3 h 1 c 0.553 0 1 -0.447 1 -1 s -0.447 -1 -1 -1 h -1 v -3 h 1 c 0.553 0 1 -0.447 1 -1 s -0.447 -1 -1 -1 h -1 v -3 h 1 c 0.553 0 1 -0.447 1 -1 s -0.447 -1 -1 -1 h -1 v -3 h 1 c 0.553 0 1 -0.447 1 -1 s -0.447 -1 -1 -1 h -1 v -3 h 1 c 0.553 0 1 -0.447 1 -1 s -0.447 -1 -1 -1 h -1 V 8 h 1 c 0.553 0 1 -0.447 1 -1 s -0.447 -1 -1 -1 H 26.1 c 0.465 -2.279 2.484 -4 4.899 -4 c 2.757 0 5 2.243 5 5 v 1 h -1 c -0.553 0 -1 0.447 -1 1 s 0.447 1 1 1 h 1 v 3 h -1 c -0.553 0 -1 0.447 -1 1 s 0.447 1 1 1 h 1 v 3 h -1 c -0.553 0 -1 0.447 -1 1 s 0.447 1 1 1 h 1 v 3 h -1 c -0.553 0 -1 0.447 -1 1 s 0.447 1 1 1 h 1 v 3 h -1 c -0.553 0 -1 0.447 -1 1 s 0.447 1 1 1 h 1 v 3 h -1 c -0.553 0 -1 0.447 -1 1 s 0.447 1 1 1 h 1 v 4.329 l 0.453 0.296 c 2.848 1.857 4.547 4.988 4.547 8.375 C 40.999 50.841 39.784 53.557 37.666 55.453 Z'.split(' ').map((x) => isNaN(x) ? x : x * thermoScale).join(' '); - const translate = [radius - (thermoScale * 100 * 0.3), radius * 1.65] - return SvgUtil.createSVGElement('path', { - class: 'dial__ico__thermo', - d: thermoDef, - transform: 'translate(' + translate[0] + ',' + translate[1] + ')' - }); - } - - _buildDialSlot(index) { - return SvgUtil.createSVGElement('text', { - class: 'dial__lbl dial__lbl--ring', - id: `temperature_slot_${index}` - }) - } - - _buildText(radius, name, offset) { - const target = SvgUtil.createSVGElement('text', { - x: radius + offset, - y: radius, - class: `dial__lbl dial__lbl--${name}`, - id: name - }); - const text = SvgUtil.createSVGElement('tspan', { - }); - // hack - if (name == 'target' || name == 'ambient') offset += 20; - const superscript = SvgUtil.createSVGElement('tspan', { - x: radius + radius / 3.1 + offset, - y: radius - radius / 6, - class: `dial__lbl--super--${name}` - }); - target.appendChild(text); - target.appendChild(superscript); - return target; - } - - _buildControls(radius) { - let startAngle = 270; - let loop = 4; - for (let index = 0; index < loop; index++) { - const angle = 360 / loop; - const sector = SvgUtil.anglesToSectors(radius, startAngle, angle); - const controlsDef = 'M' + sector.L + ',' + sector.L + ' L' + sector.L + ',0 A' + sector.L + ',' + sector.L + ' 1 0,1 ' + sector.X + ', ' + sector.Y + ' z'; - const path = SvgUtil.createSVGElement('path', { - class: 'dial__temperatureControl', - fill: 'blue', - d: controlsDef, - transform: 'rotate(' + sector.R + ', ' + sector.L + ', ' + sector.L + ')' - }); - this._controls.push(path); - path.addEventListener('click', () => this._temperatureControlClicked(index)); - this._root.appendChild(path); - startAngle = startAngle + angle; - } - } - -} - -class SvgUtil { - static createSVGElement(tag, attributes) { - const element = document.createElementNS('http://www.w3.org/2000/svg', tag); - this.attributes(element, attributes) - return element; - } - static attributes(element, attrs) { - for (let i in attrs) { - element.setAttribute(i, attrs[i]); - } - } - // Rotate a cartesian point about given origin by X degrees - static rotatePoint(point, angle, origin) { - const radians = angle * Math.PI / 180; - const x = point[0] - origin[0]; - const y = point[1] - origin[1]; - const x1 = x * Math.cos(radians) - y * Math.sin(radians) + origin[0]; - const y1 = x * Math.sin(radians) + y * Math.cos(radians) + origin[1]; - return [x1, y1]; - } - // Rotate an array of cartesian points about a given origin by X degrees - static rotatePoints(points, angle, origin) { - return points.map((point) => this.rotatePoint(point, angle, origin)); - } - // Given an array of points, return an SVG path string representing the shape they define - static pointsToPath(points) { - return points.map((point, iPoint) => (iPoint > 0 ? 'L' : 'M') + point[0] + ' ' + point[1]).join(' ') + 'Z'; - } - static circleToPath(cx, cy, r) { - return [ - "M", cx, ",", cy, - "m", 0 - r, ",", 0, - "a", r, ",", r, 0, 1, ",", 0, r * 2, ",", 0, - "a", r, ",", r, 0, 1, ",", 0, 0 - r * 2, ",", 0, - "z" - ].join(' ').replace(/\s,\s/g, ","); - } - static donutPath(cx, cy, rOuter, rInner) { - return this.circleToPath(cx, cy, rOuter) + " " + this.circleToPath(cx, cy, rInner); - } - - static superscript(n) { - - if ((n - Math.floor(n)) !== 0) - n = Number(n).toFixed(1);; - const x = `${n}${n == 0 ? '' : ''}`; - return x; - } - - // Restrict a number to a min + max range - static restrictToRange(val, min, max) { - if (val < min) return min; - if (val > max) return max; - return val; - } - static setClass(el, className, state) { - - - el.classList[state ? 'add' : 'remove'](className); - } - - static anglesToSectors(radius, startAngle, angle) { - let aRad = 0 // Angle in Rad - let z = 0 // Size z - let x = 0 // Side x - let X = 0 // SVG X coordinate - let Y = 0 // SVG Y coordinate - const aCalc = (angle > 180) ? 360 - angle : angle; - aRad = aCalc * Math.PI / 180; - z = Math.sqrt(2 * radius * radius - (2 * radius * radius * Math.cos(aRad))); - if (aCalc <= 90) { - x = radius * Math.sin(aRad); - } - else { - x = radius * Math.sin((180 - aCalc) * Math.PI / 180); - } - Y = Math.sqrt(z * z - x * x); - if (angle <= 180) { - X = radius + x; - } - else { - X = radius - x; - } - return { - L: radius, - X: X, - Y: Y, - R: startAngle - } - } -} \ No newline at end of file