├── LICENSE ├── README.md ├── hacs.json └── love-lock-card.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 CyrisXD 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Notice 2 | This is currently not being worked on for more features but I am happy to accept any pull requests. 3 | 4 | # Lovelace Love-Lock-Card 5 | Custom Lovelace card to password protect or lock entire Home Assistant cards. Based upon vertical-stack-in-card. 6 | This is not a bulletproof method and is just an implementation of [client-side security](https://i.redd.it/1iavece0fp111.jpg). But could be useful against children or flatmates poking where they shouldn't. 7 | 8 | 9 | #### Please ⭐️ this repo if you find it useful 10 | 11 | 12 | # Example 13 | There are 3 types of locks. Locks are activated when clicking on any card. 14 | 15 | ***Left*** - Password Protected. 16 | 17 | ***Middle*** - Timeout to fade. 18 | 19 | ***Right*** - Confirm Unlock 20 | 21 | 22 | ![example](https://i.imgur.com/k35TSKw.gif) 23 | 24 | ## Options 25 | 26 | | Name | Type | Default | Description 27 | | ---- | ---- | ------- | ----------- 28 | | type | string | **Required** | `custom:love-lock-card` 29 | | cards | list | **Required** | List of cards 30 | | title | string | **Optional** | Card title 31 | | popup | string | **Optional** | password, confirm, timeout 32 | | password | string | **Required** | Only required with popup:password 33 | 34 | ## Installation 35 | 36 | ### Now available in HACS 37 | 38 | ![HACS](https://i.imgur.com/1xNjAuC.jpg) 39 | 40 | ### Manual Install 41 | 42 | 1. Install the `love-lock-card` card by copying `love-lock-card.js` to `/www/love-lock-card.js` 43 | 44 | 2. Link `love-lock-card` inside your `ui-lovelace.yaml` 45 | 46 | ```yaml 47 | resources: 48 | - url: /local/love-lock-card.js 49 | type: js 50 | ``` 51 | 52 | 3. Add a custom card in your `ui-lovelace.yaml` 53 | 54 | **Password Example** 55 | 56 | ```yaml 57 | type: 'custom:love-lock-card' 58 | title: Lounge 59 | popup: password 60 | password: 1234 61 | cards: 62 | - entity: light.hue_white_lamp_1 63 | name: Lounge Lamp 64 | type: light 65 | ``` 66 | 67 | **Confirm Example** 68 | 69 | ```yaml 70 | type: 'custom:love-lock-card' 71 | title: Lounge 72 | popup: confirm 73 | cards: 74 | - entity: light.hue_white_lamp_1 75 | name: Lounge Lamp 76 | type: light 77 | ``` 78 | 79 | **Timeout Example** 80 | 81 | ```yaml 82 | type: 'custom:love-lock-card' 83 | title: Lounge 84 | popup: timeout 85 | cards: 86 | - entity: light.hue_white_lamp_1 87 | name: Lounge Lamp 88 | type: light 89 | ``` 90 | 91 | # Credits 92 | Idea comes from [Thomasloven's lovelace-toggle-lock-entity-row](https://github.com/thomasloven/lovelace-toggle-lock-entity-row) 93 | 94 | Based on [vertical-stack-in-card](https://github.com/custom-cards/vertical-stack-in-card/blob/master/README.md) 95 | -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Lovelace Lock Card", 3 | "render_readme": true 4 | } 5 | -------------------------------------------------------------------------------- /love-lock-card.js: -------------------------------------------------------------------------------- 1 | class LoveLockCard extends HTMLElement { 2 | constructor() { 3 | super(); 4 | // Make use of shadowRoot to avoid conflicts when reusing 5 | this.attachShadow({ mode: "open" }); 6 | } 7 | 8 | setConfig(config) { 9 | if (!config || !config.cards || !Array.isArray(config.cards)) { 10 | throw new Error("Card config incorrect"); 11 | } 12 | 13 | // Check if password specified 14 | if (config.popup == "password" && !config.password) { 15 | throw new Error( 16 | "Type: Password Selected. You need to specify a password" 17 | ); 18 | } 19 | 20 | this.style.boxShadow = 21 | "var(--ha-card-box-shadow, 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2))"; 22 | this.style.borderRadius = "var(--ha-card-border-radius, 2px)"; 23 | this.style.background = "var(--paper-card-background-color)"; 24 | this.style.display = "block"; 25 | // this.style.position = "relative" 26 | 27 | const root = this.shadowRoot; 28 | while (root.hasChildNodes()) { 29 | root.removeChild(root.lastChild); 30 | } 31 | 32 | // Wrap main card 33 | const wrapper = document.createElement("div"); 34 | wrapper.setAttribute("style", "position:relative"); 35 | root.appendChild(wrapper); 36 | 37 | // Password 38 | var password = '"' + btoa(config.password) + '"'; 39 | 40 | // Cover styles 41 | const coverShow = 42 | '"position:absolute; top:0; left:0; width:100%; height: 100%; z-index:1000; transition: 1s opacity;"'; 43 | const coverHide = '"display:none; transition: 1s opacity;"'; 44 | 45 | // Password Script 46 | var passwordScript = ` 47 | var element = this; 48 | var pass = prompt("Please enter your password"); 49 | if (btoa(pass) !== ${password}) { 50 | alert("Invalid Password"); 51 | } else { 52 | element.setAttribute("style", ${coverHide}); 53 | } 54 | setTimeout(function(){ 55 | element.setAttribute("style", ${coverShow}); 56 | }, 10000) 57 | `; 58 | 59 | // Confirm Script 60 | var confirmScript = ` 61 | var element = this; 62 | var confirmpopup = confirm("Are you sure you want to unlock?"); 63 | if (confirmpopup == true) { 64 | this.setAttribute("style", ${coverHide}); 65 | } 66 | setTimeout(function(){ 67 | element.setAttribute("style", ${coverShow}); 68 | }, 10000) 69 | `; 70 | 71 | // Timeout Script 72 | var timeoutScript = ` 73 | var element = this; 74 | element.style.opacity = '0'; 75 | setTimeout(function(){ 76 | element.setAttribute("style", ${coverHide}); 77 | }, 1000) 78 | setTimeout(function(){ 79 | element.style.opacity = '1'; 80 | element.setAttribute("style", ${coverShow}); 81 | }, 10000) 82 | 83 | `; 84 | 85 | // Cover the wrapper with lock 86 | const cover = document.createElement("div"); 87 | cover.setAttribute( 88 | "style", 89 | "position:absolute; top:0; left:0; width:100%; height: 100%; z-index:1000; transition: 1s opacity;" 90 | ); 91 | 92 | // Determine which lock/script to use 93 | if (config.popup == "password") { 94 | cover.setAttribute("onclick", passwordScript); 95 | } else if (config.popup == "confirm") { 96 | cover.setAttribute("onclick", confirmScript); 97 | } else if (config.popup == "timeout") { 98 | cover.setAttribute("onclick", timeoutScript); 99 | } 100 | 101 | // Lock Icon 102 | const lockicon = document.createElement("ha-icon"); 103 | lockicon.setAttribute("icon", "mdi:lock-outline"); 104 | lockicon.setAttribute("style", "position:absolute; top: 10px; right:7px;"); 105 | cover.appendChild(lockicon); 106 | 107 | this._refCards = []; 108 | 109 | if (config.title) { 110 | const title = document.createElement("div"); 111 | title.className = "header"; 112 | title.style = 113 | "font-family: var(--paper-font-headline_-_font-family); -webkit-font-smoothing: var(--paper-font-headline_-_-webkit-font-smoothing); font-size: var(--paper-font-headline_-_font-size); font-weight: var(--paper-font-headline_-_font-weight); letter-spacing: var(--paper-font-headline_-_letter-spacing); line-height: var(--paper-font-headline_-_line-height);text-rendering: var(--paper-font-common-expensive-kerning_-_text-rendering);opacity: var(--dark-primary-opacity);padding: 24px 16px 0px 16px"; 114 | const title_text = document.createTextNode(config.title); 115 | title.appendChild(title_text); 116 | wrapper.appendChild(title); 117 | } 118 | 119 | const _createThing = (tag, config) => { 120 | const element = document.createElement(tag); 121 | 122 | try { 123 | element.setConfig(config); 124 | } catch (err) { 125 | console.error(tag, err); 126 | // return _createError(err.message, config); 127 | } 128 | return element; 129 | }; 130 | 131 | const _createError = (error, config) => { 132 | return _createThing("hui-error-card", { 133 | type: "error", 134 | error, 135 | config 136 | }); 137 | }; 138 | 139 | const _fireEvent = (ev, detail, entity = null) => { 140 | ev = new Event(ev, { 141 | bubbles: true, 142 | cancelable: false, 143 | composed: true 144 | }); 145 | 146 | ev.detail = detail || {}; 147 | 148 | if (entity) { 149 | entity.dispatchEvent(ev); 150 | } else { 151 | document 152 | .querySelector("home-assistant") 153 | .shadowRoot.querySelector("home-assistant-main") 154 | .shadowRoot.querySelector("app-drawer-layout partial-panel-resolver") 155 | .shadowRoot.querySelector("ha-panel-lovelace") 156 | .shadowRoot.querySelector("hui-root") 157 | .shadowRoot.querySelector("ha-app-layout #view") 158 | .firstElementChild.dispatchEvent(ev); 159 | } 160 | }; 161 | 162 | config.cards.forEach(item => { 163 | let tag = item.type; 164 | 165 | if (tag.startsWith("divider")) { 166 | tag = `hui-divider-row`; 167 | } else if (tag.startsWith("custom:")) { 168 | tag = tag.substr("custom:".length); 169 | } else { 170 | tag = `hui-${tag}-card`; 171 | } 172 | 173 | if (customElements.get(tag)) { 174 | const element = _createThing(tag, item); 175 | 176 | wrapper.appendChild(element); 177 | 178 | // Only add cover if specified 179 | if (config.popup) { 180 | wrapper.appendChild(cover); 181 | } 182 | 183 | this._refCards.push(element); 184 | } else { 185 | // If element doesn't exist (yet) create an error 186 | const element = _createError( 187 | `Custom element doesn't exist: ${tag}.`, 188 | item 189 | ); 190 | element.style.display = "None"; 191 | 192 | const time = setTimeout(() => { 193 | element.style.display = ""; 194 | }, 2000); 195 | 196 | // Remove error if element is defined later 197 | customElements.whenDefined(tag).then(() => { 198 | clearTimeout(time); 199 | _fireEvent("ll-rebuild", {}, element); 200 | }); 201 | 202 | root.appendChild(element); 203 | this._refCards.push(element); 204 | } 205 | }); 206 | } 207 | 208 | set hass(hass) { 209 | if (this._refCards) { 210 | this._refCards.forEach(card => { 211 | card.hass = hass; 212 | }); 213 | } 214 | } 215 | 216 | connectedCallback() { 217 | this._refCards.forEach(element => { 218 | let fn = () => { 219 | this._card(element); 220 | }; 221 | 222 | if (element.updateComplete) { 223 | element.updateComplete.then(fn); 224 | } else { 225 | fn(); 226 | } 227 | }); 228 | } 229 | 230 | _card(element) { 231 | if (element.shadowRoot) { 232 | if (!element.shadowRoot.querySelector("ha-card")) { 233 | let searchEles = element.shadowRoot.getElementById("root"); 234 | if (!searchEles) { 235 | searchEles = element.shadowRoot.getElementById("card"); 236 | } 237 | if (!searchEles) return; 238 | searchEles = searchEles.childNodes; 239 | 240 | for (let i = 0; i < searchEles.length; i++) { 241 | if (searchEles[i].style !== undefined) { 242 | searchEles[i].style.margin = "0px"; 243 | } 244 | this._card(searchEles[i]); 245 | } 246 | } else { 247 | element.shadowRoot.querySelector("ha-card").style.boxShadow = "none"; 248 | } 249 | } else { 250 | if ( 251 | typeof element.querySelector === "function" && 252 | element.querySelector("ha-card") 253 | ) { 254 | element.querySelector("ha-card").style.boxShadow = "none"; 255 | } 256 | let searchEles = element.childNodes; 257 | for (let i = 0; i < searchEles.length; i++) { 258 | if (searchEles[i] && searchEles[i].style) { 259 | searchEles[i].style.margin = "0px"; 260 | } 261 | this._card(searchEles[i]); 262 | } 263 | } 264 | } 265 | 266 | getCardSize() { 267 | let totalSize = 0; 268 | this._refCards.forEach(element => { 269 | totalSize += 270 | typeof element.getCardSize === "function" ? element.getCardSize() : 1; 271 | }); 272 | return totalSize; 273 | } 274 | } 275 | 276 | customElements.define("love-lock-card", LoveLockCard); 277 | --------------------------------------------------------------------------------