From 759a2ff5925352769b3c19431090433b5cd0b246 Mon Sep 17 00:00:00 2001 From: Niklas Schumacher Date: Sun, 21 Aug 2022 19:50:34 +0200 Subject: [PATCH] Initial release (v0.1.0) --- .gitignore | 4 + LICENSE | 21 ++++++ hacs.json | 6 ++ package.json | 62 ++++++++++++++++ rollup.config.js | 53 +++++++++++++ src/shutter-row.js | 180 +++++++++++++++++++++++++++++++++++++++++++++ src/style.css | 48 ++++++++++++ 7 files changed, 374 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 hacs.json create mode 100644 package.json create mode 100644 rollup.config.js create mode 100644 src/shutter-row.js create mode 100644 src/style.css diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..133209c --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +dist +node_modules +package-lock.json \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..dced36b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Niklas Schumacher + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/hacs.json b/hacs.json new file mode 100644 index 0000000..438f1da --- /dev/null +++ b/hacs.json @@ -0,0 +1,6 @@ +{ + "name": "shutter-row", + "zip_release": true, + "filename": "shutter-row.js", + "render_readme": true +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..39442a5 --- /dev/null +++ b/package.json @@ -0,0 +1,62 @@ +{ + "name": "@berrywhite/lovelace-shutter-row", + "private": true, + "version": "0.1.0", + "description": "Home Assistant Lovelace Shutter Row Card", + "main": "shutter-row.js", + "module": "shutter-row.js", + "type": "module", + "scripts": { + "start": "rollup -c --watch", + "lint": "eslint src", + "lint:fix": "eslint src --fix", + "format": "prettier --write .", + "build": "rollup -c", + "test": "npm run lint && npm run build" + }, + "keywords": [ + "home-assistant", + "homeassistant", + "hass", + "automation", + "lovelace", + "custom-cards", + "shutter", + "row" + ], + "author": "berrywhite", + "license": "BSD-3-Clause", + "dependencies": { + "@rollup/plugin-commonjs": "^22.0.2", + "custom-card-helpers": "^1.9.0", + "lit": "^2.3.0", + "postcss-preset-env": "^7.7.2", + "rollup-plugin-postcss": "^4.0.2", + "rollup-plugin-postcss-lit": "^2.0.0", + "rollup-plugin-serve": "^2.0.1" + }, + "devDependencies": { + "@11ty/eleventy": "^1.0.1", + "@11ty/eleventy-plugin-syntaxhighlight": "^4.0.0", + "@babel/eslint-parser": "^7.17.0", + "@custom-elements-manifest/analyzer": "^0.6.3", + "@open-wc/testing": "^3.1.5", + "@rollup/plugin-commonjs": "^22.0.2", + "@rollup/plugin-json": "^4.1.0", + "@rollup/plugin-node-resolve": "^13.3.0", + "@rollup/plugin-replace": "^4.0.0", + "@web/dev-server": "^0.1.31", + "@web/dev-server-legacy": "^1.0.0", + "@web/test-runner": "^0.13.27", + "@web/test-runner-playwright": "^0.8.8", + "@webcomponents/webcomponentsjs": "^2.6.0", + "eslint": "^8.15.0", + "lit-analyzer": "^0.0.6", + "prettier": "^2.6.2", + "rimraf": "^3.0.2", + "rollup": "^2.77.3", + "rollup-plugin-summary": "^1.4.3", + "rollup-plugin-terser": "^7.0.2" + }, + "customElements": "custom-elements.json" +} diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 0000000..725f744 --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,53 @@ +/* eslint-env node */ +import commonjs from '@rollup/plugin-commonjs'; +import nodeResolve from '@rollup/plugin-node-resolve'; +import json from '@rollup/plugin-json'; +import postcss from 'rollup-plugin-postcss'; +import postcssPresetEnv from 'postcss-preset-env'; +import postcssLit from 'rollup-plugin-postcss-lit'; +import { terser } from 'rollup-plugin-terser'; +import serve from 'rollup-plugin-serve'; + +const IS_DEV = process.env.ROLLUP_WATCH; + +const serverOptions = { + contentBase: ['./dist'], + host: 'localhost', + port: 5000, + allowCrossOrigin: true, + headers: { + 'Access-Control-Allow-Origin': '*', + }, +}; + +export default { + input: 'src/shutter-row.js', + output: { + dir: 'dist', + format: 'es', + }, + plugins: [ + nodeResolve(), + commonjs(), + json(), + postcss({ + plugins: [ + postcssPresetEnv({ + stage: 1, + features: { + 'nesting-rules': true, + }, + }), + ], + extract: false, + }), + postcssLit(), + IS_DEV && serve(serverOptions), + !IS_DEV && + terser({ + output: { + comments: false, + }, + }), + ], +}; \ No newline at end of file diff --git a/src/shutter-row.js b/src/shutter-row.js new file mode 100644 index 0000000..4cdb94f --- /dev/null +++ b/src/shutter-row.js @@ -0,0 +1,180 @@ +import { LitElement, html } from 'lit'; +import { fireEvent } from 'custom-card-helpers'; +import style from './style.css'; + +let PACKAGE_JSON = require('./package.json'); +let HASSIO_CARD_ID = "shutter-row"; +let HASSIO_CARD_NAME = "Shutter row"; +let VERSION = PACKAGE_JSON.version; + + +class ShutterRow extends LitElement { + static get properties() { + return { + hass: Object, + config: Object, + }; + } + + static get styles() { + return style; + } + + getConfig() { + let getConfigAttribute = (attribute, defaultValue) => { + return attribute in this._config ? this._config[attribute] : defaultValue; + } + + return { + type: getConfigAttribute("type", false), + entity: getConfigAttribute("entity", false), + name: getConfigAttribute("name", false), + invert_position: getConfigAttribute("invert_position", false), + }; + } + // On user config update + setConfig(config) { + let getConfigAttribute = (attribute, defaultValue) => { + return attribute in this._config ? this._config[attribute] : defaultValue; + } + if (!config.entity) { + throw new Error('You need to define an entity'); + } + + this._config = config; + this.config = { + type: config.type, + entity: config.entity, + name: getConfigAttribute("name", false), + invert_position: getConfigAttribute("invert_position", false), + } + this.entityId = this.config.entity; + } + + // Lovelace card height + getCardSize() { + return 2; + } + getName() { + if(this.getConfig().name) + return this.getConfig().name; + return this.state.attributes.friendly_name; + } + getPosition() { + return this.state.attributes.current_position; + } + getPositionDisplay() { + if( (this.getConfig().invert_position && this.getPosition() == 100) || + (!this.getConfig().invert_position && this.getPosition() == 0) ) + return this.hass.localize("component.cover.state._.closed"); + if( (this.config.invert_position && this.getPosition() == 0) || + (!this.getConfig().invert_position && this.getPosition() == 100) ) + return this.hass.localize("component.cover.state._.open"); + return `${this.getPosition()} %`; + } + setMeta(force=false) { + // Only on change + if(this.state == this.hass.states[this.entityId] && !force) + return; + this.state = this.hass.states[this.entityId]; + this.stateDisplay = this.state ? this.state.state : 'unavailable'; + } + + render() { + this.setMeta(); + + let config = this.getConfig(); + let position = this.getPosition(); + + let upReached = function() { + if(!config.invert_position && position == 100 || + config.invert_position && position == 0) + return true; + return false; + } + let downReached = function() { + if(config.invert_position && position == 100 || + !config.invert_position && position == 0) + return true; + return false; + } + return html` + +
+
+ + +
+ + ${this.getName()} +
+ + + +
+
+
+ +
+ ${this.getPositionDisplay()} +
+
+
+ `; + } + getElements() { + return { + controls: this.renderRoot.querySelector("div.card-first-row div.controls"), + slider: this.renderRoot.querySelector("div.card-second-row ha-slider"), + } + } + + onUpClick() { + let elements = this.getElements(); + if(this.state.attributes.moving == "UP" || elements.controls.hasAttribute("up-reached")) + return; + this.hass.callService("cover", "open_cover", { + entity_id: this.entityId, + }); + } + + onDownClick() { + let elements = this.getElements(); + if(this.state.attributes.moving == "DOWN" || elements.controls.hasAttribute("down-reached")) + return; + this.hass.callService("cover", "close_cover", { + entity_id: this.entityId, + }); + } + + onStopClick() { + if(this.state.attributes.moving == "STOP") + return; + this.hass.callService("cover", "stop_cover", { + entity_id: this.entityId, + }); + } + + onSliderChange() { + let elements = this.getElements(); + if(elements.slider.value == this.getPosition()) + return; + this.hass.callService("cover", "set_cover_position", { + entity_id: this.entityId, + position: parseInt(elements.slider.value), + }); + } + + moreInfo() { + let entityId = this.getConfig().entity; + fireEvent(this, 'hass-more-info', { + entityId, + }, { + bubbles: false, + composed: true, + }); + } +} + +customElements.define(HASSIO_CARD_ID, ShutterRow); +console.info("%c" + HASSIO_CARD_NAME.toLocaleUpperCase() + " " + VERSION, "color: #ffa500"); \ No newline at end of file diff --git a/src/style.css b/src/style.css new file mode 100644 index 0000000..47f0c2b --- /dev/null +++ b/src/style.css @@ -0,0 +1,48 @@ +.disabled, +div.controls[state="unavailable"] ha-icon { + color: var(--disabled-text-color); + cursor: default; +} +.ghost { + display: none !important; +} +ha-card { + padding: 16px; +} +ha-icon[disabled] { + color: var(--disabled-text-color); + cursor: default; +} +div.card-row { + display: flex; + align-items: center; + height: 40px; +} +div.card-first-row ha-icon { + display: inline-block; + width: 40px; + height: 40px; + line-height: 40px; + text-align: center; + cursor: pointer; +} +div.entity-icon ha-icon { + color: var(--paper-item-icon-color, #44739e); +} +div.card-first-row span.entity-name { + margin-left: 16px; + margin-right: 8px; + cursor: pointer; +} +div.card-first-row div.controls { + margin-left: auto; +} +div.card-second-row ha-slider { + width: 100%; + padding-left: 40px; + box-sizing: border-box; +} +div.card-second-row div.infos { + width: 175px; + text-align: right; +} \ No newline at end of file