├── .github ├── FUNDING.yml └── workflows │ └── build.yaml ├── hacs.json ├── example1.png ├── LICENSE ├── README.md └── kiosk-mode.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [maykar] 2 | -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Kiosk Mode", 3 | "render_readme": true 4 | } -------------------------------------------------------------------------------- /example1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maykar/kiosk-mode/HEAD/example1.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Ryan Meek 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 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build on release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Set version number 13 | run: | 14 | sed -i 's|*DEV|${{ github.ref }}|g' kiosk-mode.js 15 | sed -i 's|refs/tags/||' kiosk-mode.js 16 | - name: Install deps 17 | run: npm install uglify-js browserify @babel/core @babel/cli @babel/preset-env @babel/plugin-transform-runtime 18 | - name: Run Babel 19 | run: npx babel kiosk-mode.js --out-file kiosk-mode.js --presets @babel/preset-env --plugins @babel/plugin-transform-runtime 20 | - name: Run Browserify 21 | run: npx browserify kiosk-mode.js -o kiosk-mode.js 22 | - name: Run Uglify 23 | run: npx uglify-js kiosk-mode.js -b beautify=false,max_line_len=150 --compress --mangle toplevel --output kiosk-mode.js 24 | - name: Upload release asset 25 | uses: actions/upload-release-asset@v1 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | with: 29 | upload_url: ${{ github.event.release.upload_url }} 30 | asset_path: ./kiosk-mode.js 31 | asset_name: kiosk-mode.js 32 | asset_content_type: text/javascript 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kiosk-mode 2 | 3 | [![hacs_badge](https://img.shields.io/badge/HACS-Default-yellow.svg)](https://github.com/custom-components/hacs) [![hacs_badge](https://img.shields.io/badge/Buy-Me%20a%20Coffee-critical)](https://www.buymeacoffee.com/FgwNR2l) 4 | 5 | Hides the header and/or sidebar drawer in [Home Assistant](https://www.home-assistant.io/) 6 | 7 | ![image](example1.png) 8 | 9 | # Installation 10 | 11 | *If you previously used [custom-header](https://github.com/maykar/custom-header) you need to uninstall it from [HACS](https://hacs.xyz/)*
12 | 13 | **Follow only one of two installation methods below, HACS or Manually:** 14 | 15 |
16 | Installation and tracking with HACS 17 |
18 | 19 | * In the "Frontend" section of [HACS](https://github.com/hacs/integration) hit the plus icon in the bottom right 20 | * Search for `Kiosk Mode` and install it 21 | * If using YAML mode or if HACS doesn't automatically add it you'll need to add the resource below 22 | 23 | YAML mode users will add it to their [configuration.yaml](https://www.home-assistant.io/lovelace/dashboards-and-views/#adding-more-dashboards-with-yaml) file. 24 | Non-YAML mode, or Storage Mode, users can find resources in their sidebar under `"Configuration" > "Lovelace Dashboards" > "Resources"` 25 | 26 | ```yaml 27 | resources: 28 | - url: /hacsfiles/kiosk-mode/kiosk-mode.js 29 | type: module 30 | ``` 31 |
32 |
33 | 34 |
35 | Manual installation 36 |
37 | 38 | * Download [kiosk-mode.js](https://github.com/matt8707/kiosk-mode/releases/latest) from the latest release and place it in your `www` folder 39 | * Add the resource below 40 | 41 | YAML mode users add it to their [configuration.yaml](https://www.home-assistant.io/lovelace/dashboards-and-views/#adding-more-dashboards-with-yaml) file. 42 | Non-YAML mode, or Storage Mode, users can find resources in their sidebar under `"Configuration" > "Lovelace Dashboards" > "Resources"` 43 | 44 | ```yaml 45 | resources: 46 | # You'll need to update the version number at the end of the url after every update. 47 | - url: /local/kiosk-mode.js?v=1.2.1 48 | type: module 49 | ``` 50 |
51 |
52 | 53 | *If you have trouble installing please [read this guide](https://github.com/thomasloven/hass-config/wiki/Lovelace-Plugins)* 54 | 55 | ## Important Info 56 | 57 | * If you need to disable Kiosk-Mode temporarily add `?disable_km` to the end of your URL. 58 | * Config is placed in the root of your Lovelace config: `kiosk_mode:` should not be indented & is per dashboard. 59 | * If you want the same settings on other dashboards you'll need to repeat the config on those dashboards as well. 60 | * Refresh page after config changes. 61 | 62 | ## Config Options 63 | 64 | | Config Option | Type | Default | Description | 65 | |:---------------|:---------------|:---------------|:----------| 66 | |`kiosk:`| Boolean | false | Hides both the header and sidebar. 67 | |`hide_header:` | Boolean | false | Hides only the header. 68 | |`hide_sidebar:` | Boolean | false | Hides only the sidebar. Disables swipe to open. 69 | |`hide_menubutton:` | Boolean | false | Hides only the sidebar menu icon. Does not disable swipe to open. 70 | |`hide_overflow:` | Boolean | false | Hides the top right menu. 71 | |`ignore_entity_settings:` | Boolean | false | Useful for [conditional configs](#conditional-lovelace-config) and will cause `entity_settings` to be ignored. 72 | |`ignore_mobile_settings:` | Boolean | false | Useful for [conditional configs](#conditional-lovelace-config) and will cause `mobile_settings` to be ignored. 73 | 74 | ## Simple config example 75 | 76 | ``` 77 | kiosk_mode: 78 | hide_header: true 79 | 80 | views: 81 | ``` 82 | *Note: `views:` is added in the example above to show where `kiosk_mode:` should be placed in your Lovelace config*

83 | 84 | ## Conditional Lovelace Config 85 | Contitional configs take priority and if a condition matches all other config options/methods are ignored. 86 | These use the same options as above, but placed under one of the following user/entity conditions:

87 | 88 | ### admin_settings: 89 | Sets the config for every admin user.
90 | *Overwritten by user_settings, mobile_settings, and entity_settings ( unless one of the ignore options is used ).*
91 | 92 | ``` 93 | kiosk_mode: 94 | admin_settings: 95 | hide_header: true 96 | ``` 97 |
98 | 99 | ### non_admin_settings: 100 | Sets the config for every regular user.
101 | *Overwritten by user_settings, mobile_settings, and entity_settings ( unless one of the ignore options is used ).*
102 | 103 | ``` 104 | kiosk_mode: 105 | non_admin_settings: 106 | hide_header: true 107 | ignore_entity_settings: true 108 | ``` 109 |
110 | 111 | ### user_settings: 112 | Sets the config for specific users. **This uses a user's name, not their username (if they're different)**.
113 | *Overwritten by mobile_settings, and entity_settings ( unless one of the ignore options is used ).*
114 | 115 | ``` 116 | kiosk_mode: 117 | user_settings: 118 | - users: 119 | - "ryan meek" 120 | - "maykar" 121 | hide_sidebar: true 122 | - users: 123 | - "the wife" 124 | kiosk: true 125 | ignore_entity_settings: true 126 | ``` 127 |
128 | 129 | ### mobile_settings: 130 | Sets the config for mobile devices. The default breakpoint is 812px, which can be changed by setting the `custom_width` option.
131 | *Overwritten by entity_settings, unless `ignore_entity_settings` is used, can be ignored with `ignore_mobile_settings`.*
132 | 133 | ``` 134 | kiosk_mode: 135 | mobile_settings: 136 | hide_header: true 137 | ignore_entity_settings: true 138 | custom_width: 768 139 | ``` 140 |
141 | 142 | ### entity_settings: 143 | Dynamically change config on any entity's state. Under `entity:` list the entity followed by the state that will enable the config below. For more complex logic use this with a template sensor.
144 | *Takes priority over all other config settings unless they use `ignore_entity_settings`.*

145 | 146 | *Any condition that doesn't match will then fall back to previous configurations if another "false" entity condition hasn't also been set (see the 2nd example).* 147 | ``` 148 | kiosk_mode: 149 | entity_settings: 150 | - entity: 151 | input_boolean.hide_sidebar: 'on' 152 | hide_sidebar: true 153 | - entity: 154 | sensor.hide_header: 'on' 155 | hide_header: true 156 | - entity: 157 | input_text.kiosk: 'true' 158 | kiosk: true 159 | ``` 160 | 161 | ``` 162 | kiosk_mode: 163 | entity_settings: 164 | # hide_sidebar has both true and false conditions to be a true override. 165 | - entity: 166 | input_boolean.hide_sidebar: 'on' 167 | hide_sidebar: true 168 | - entity: 169 | input_boolean.hide_sidebar: 'off' 170 | hide_sidebar: false 171 | ``` 172 |
173 | 174 | ## Query Strings 175 | Add a query string such as `?kiosk` to the end of your URL: 176 | 177 | ``` 178 | https://hass:8123/lovelace/default_view?kiosk 179 | ``` 180 | 181 | The query string options are: 182 | 183 | * `?kiosk` to hide both header and sidebar 184 | * `?hide_header` to hide only the header 185 | * `?hide_sidebar` to hide only the sidebar 186 | * `?hide_overflow` to hide the top right menu 187 | * `?hide_menubutton` to hide sidebar menu button 188 | 189 | ## Query String Caching 190 | 191 | You save settings in a devices cache by using the cache keyword once on the device.
This will also make it so the options work on all views and dashboards. 192 | 193 | Example: `?hide_header&cache` makes all views & dashboards hide the header.
194 | This works for all query strings except for the utility strings listed below. 195 | 196 | **Utility Query Strings** 197 | 198 | * `?clear_km_cache` will clear all cached preferences 199 | * `?disable_km` will temporarily disable any modifications 200 |
201 | 202 | ### Related 203 | 204 | * [Fully Kiosk Browser](https://www.fully-kiosk.com/) - Great for wall mounted tablets 205 | * [Applicationize](https://applicationize.me/) - Convert web apps into desktop apps 206 | * [KTibow/fullscreen-card](https://github.com/KTibow/fullscreen-card) - Make your Home Assistant browser fullscreen 207 |
208 | 209 | ### Credit 210 | This was originally based on and inspired by [ciotlosm's kiosk mode gist](https://gist.github.com/ciotlosm/1f09b330aa5bd5ea87b59f33609cc931) and [corrafig's fork](https://gist.github.com/corrafig/c8288df960e7f59e82c12d14de26fde8) of the same gist. 211 | 212 | Big thank you to [matt8707](https://github.com/matt8707) for starting this project, allowing me to rewrite it, and transfering ownership. 213 | 214 | Many thanks to [KTibow](https://github.com/KTibow) as well, for the github release action and support. 215 | -------------------------------------------------------------------------------- /kiosk-mode.js: -------------------------------------------------------------------------------- 1 | class KioskMode { 2 | constructor() { 3 | window.kioskModeEntities = {}; 4 | if (this.queryString("clear_km_cache")) this.setCache(["kmHeader", "kmSidebar", "kmOverflow", "kmMenuButton"], "false"); 5 | this.ha = document.querySelector("home-assistant"); 6 | this.main = this.ha.shadowRoot.querySelector("home-assistant-main").shadowRoot; 7 | this.user = this.ha.hass.user; 8 | this.llAttempts = 0; 9 | this.run(); 10 | this.entityWatch(); 11 | new MutationObserver(this.watchDashboards).observe(this.main.querySelector("partial-panel-resolver"), { 12 | childList: true, 13 | }); 14 | } 15 | 16 | run(lovelace = this.main.querySelector("ha-panel-lovelace")) { 17 | if (this.queryString("disable_km") || !lovelace) return; 18 | this.getConfig(lovelace); 19 | } 20 | 21 | getConfig(lovelace) { 22 | this.llAttempts++; 23 | try { 24 | const llConfig = lovelace.lovelace.config; 25 | const config = llConfig.kiosk_mode || {}; 26 | this.processConfig(lovelace, config); 27 | } catch (e) { 28 | if (this.llAttempts < 200) { 29 | setTimeout(() => this.getConfig(lovelace), 50); 30 | } else { 31 | console.log("Lovelace config not found, continuing with default configuration."); 32 | console.log(e); 33 | this.processConfig(lovelace, {}); 34 | } 35 | } 36 | } 37 | 38 | processConfig(lovelace, config) { 39 | const dash = this.ha.hass.panelUrl; 40 | if (!window.kioskModeEntities[dash]) window.kioskModeEntities[dash] = []; 41 | this.hideHeader = this.hideSidebar = this.hideOverflow = this.ignoreEntity = this.ignoreMobile = false; 42 | 43 | // Retrieve localStorage values & query string options. 44 | const queryStringsSet = 45 | this.cached(["kmHeader", "kmSidebar", "kmOverflow", "kmMenuButton"]) || this.queryString(["kiosk", "hide_sidebar", "hide_header", "hide_overflow", "hide_menubutton"]); 46 | if (queryStringsSet) { 47 | this.hideHeader = this.cached("kmHeader") || this.queryString(["kiosk", "hide_header"]); 48 | this.hideSidebar = this.cached("kmSidebar") || this.queryString(["kiosk", "hide_sidebar"]); 49 | this.hideOverflow = this.cached("kmOverflow") || this.queryString(["kiosk", "hide_overflow"]); 50 | this.hideMenuButton = this.cached("kmMenuButton") || this.queryString(["kiosk", "hide_menubutton"]); 51 | } 52 | 53 | // Use config values only if config strings and cache aren't used. 54 | this.hideHeader = queryStringsSet ? this.hideHeader : config.kiosk || config.hide_header; 55 | this.hideSidebar = queryStringsSet ? this.hideSidebar : config.kiosk || config.hide_sidebar; 56 | this.hideOverflow = queryStringsSet ? this.hideOverflow : config.kiosk || config.hide_overflow; 57 | this.hideMenuButton = queryStringsSet ? this.hideMenuButton : config.kiosk || config.hide_menubutton; 58 | 59 | const adminConfig = this.user.is_admin ? config.admin_settings : config.non_admin_settings; 60 | if (adminConfig) this.setOptions(adminConfig); 61 | 62 | if (config.user_settings) { 63 | for (let conf of this.array(config.user_settings)) { 64 | if (this.array(conf.users).some((x) => x.toLowerCase() == this.user.name.toLowerCase())) this.setOptions(conf); 65 | } 66 | } 67 | 68 | const mobileConfig = this.ignoreMobile ? null : config.mobile_settings; 69 | if (mobileConfig) { 70 | const mobileWidth = mobileConfig.custom_width ? mobileConfig.custom_width : 812; 71 | if (window.innerWidth <= mobileWidth) this.setOptions(mobileConfig); 72 | } 73 | 74 | const entityConfig = this.ignoreEntity ? null : config.entity_settings; 75 | if (entityConfig) { 76 | for (let conf of entityConfig) { 77 | const entity = Object.keys(conf.entity)[0]; 78 | if (!window.kioskModeEntities[dash].includes(entity)) window.kioskModeEntities[dash].push(entity); 79 | if (this.ha.hass.states[entity].state == conf.entity[entity]) { 80 | if ("hide_header" in conf) this.hideHeader = conf.hide_header; 81 | if ("hide_sidebar" in conf) this.hideSidebar = conf.hide_sidebar; 82 | if ("hide_overflow" in conf) this.hideOverflow = conf.hide_overflow; 83 | if ("hide_menubutton" in conf) this.hideMenuButton = conf.hide_menubutton; 84 | if ("kiosk" in conf) this.hideHeader = this.hideSidebar = conf.kiosk; 85 | } 86 | } 87 | } 88 | 89 | this.insertStyles(lovelace); 90 | } 91 | 92 | insertStyles(lovelace) { 93 | const huiRoot = lovelace.shadowRoot.querySelector("hui-root").shadowRoot; 94 | const drawerLayout = this.main.querySelector("app-drawer-layout"); 95 | const appToolbar = huiRoot.querySelector("app-toolbar"); 96 | const overflowStyle = "ha-button-menu{display:none !important;}"; 97 | const headerStyle = "#view{min-height:100vh !important;--header-height:0;}app-header{display:none;}"; 98 | 99 | if (this.hideHeader || this.hideOverflow) { 100 | this.addStyle(`${this.hideHeader ? headerStyle : ""}${this.hideOverflow ? overflowStyle : ""}`, huiRoot); 101 | if (this.queryString("cache")) { 102 | if (this.hideHeader) this.setCache("kmHeader", "true"); 103 | if (this.hideOverflow) this.setCache("kmOverflow", "true"); 104 | } 105 | } else { 106 | this.removeStyle(huiRoot); 107 | } 108 | 109 | if (this.hideSidebar) { 110 | this.addStyle(":host{--app-drawer-width:0 !important;}#drawer{display:none;}", drawerLayout); 111 | this.addStyle("ha-menu-button{display:none !important;}", appToolbar); 112 | if (this.queryString("cache")) this.setCache("kmSidebar", "true"); 113 | } else { 114 | this.removeStyle([appToolbar, drawerLayout]); 115 | } 116 | 117 | if (this.hideMenuButton) { 118 | this.addStyle("ha-menu-button{display:none !important;}", appToolbar); 119 | if (this.queryString("cache")) this.setCache("kmMenuButton", "true"); 120 | } else { 121 | this.removeStyle(appToolbar); 122 | } 123 | 124 | // Resize window to "refresh" view. 125 | window.dispatchEvent(new Event("resize")); 126 | 127 | this.llAttempts = 0; 128 | } 129 | 130 | // Run on dashboard change. 131 | watchDashboards(mutations) { 132 | mutations.forEach(({ addedNodes }) => { 133 | for (let node of addedNodes) if (node.localName == "ha-panel-lovelace") window.KioskMode.run(node); 134 | }); 135 | } 136 | 137 | // Run on entity change. 138 | async entityWatch() { 139 | (await window.hassConnection).conn.subscribeMessage((e) => this.entityWatchCallback(e), { 140 | type: "subscribe_events", 141 | event_type: "state_changed", 142 | }); 143 | } 144 | 145 | entityWatchCallback(event) { 146 | const entities = window.kioskModeEntities[this.ha.hass.panelUrl] || []; 147 | if ( 148 | entities.length && 149 | event.event_type == "state_changed" && 150 | entities.includes(event.data.entity_id) && 151 | (!event.data.old_state || event.data.new_state.state != event.data.old_state.state) 152 | ) { 153 | this.run(); 154 | } 155 | } 156 | 157 | setOptions(config) { 158 | this.hideHeader = config.kiosk || config.hide_header; 159 | this.hideSidebar = config.kiosk || config.hide_sidebar; 160 | this.hideOverflow = config.kiosk || config.hide_overflow; 161 | this.hideMenuButton = config.kiosk || config.hide_menubutton; 162 | this.ignoreEntity = config.ignore_entity_settings; 163 | this.ignoreMobile = config.ignore_mobile_settings; 164 | } 165 | 166 | // Convert to array. 167 | array(x) { 168 | return Array.isArray(x) ? x : [x]; 169 | } 170 | 171 | // Return true if keyword is found in query strings. 172 | queryString(keywords) { 173 | return this.array(keywords).some((x) => window.location.search.includes(x)); 174 | } 175 | 176 | // Set localStorage item. 177 | setCache(k, v) { 178 | this.array(k).forEach((x) => window.localStorage.setItem(x, v)); 179 | } 180 | 181 | // Retrieve localStorage item as bool. 182 | cached(key) { 183 | return this.array(key).some((x) => window.localStorage.getItem(x) == "true"); 184 | } 185 | 186 | styleExists(elem) { 187 | return elem.querySelector(`#kiosk_mode_${elem.localName}`); 188 | } 189 | 190 | addStyle(css, elem) { 191 | if (!this.styleExists(elem)) { 192 | const style = document.createElement("style"); 193 | style.setAttribute("id", `kiosk_mode_${elem.localName}`); 194 | style.innerHTML = css; 195 | elem.appendChild(style); 196 | } 197 | } 198 | 199 | removeStyle(elements) { 200 | this.array(elements).forEach((elem) => { 201 | if (this.styleExists(elem)) elem.querySelector(`#kiosk_mode_${elem.localName}`).remove(); 202 | }); 203 | } 204 | } 205 | 206 | // Overly complicated console tag. 207 | const conInfo = { header: "%c≡ kiosk-mode".padEnd(27), ver: "%cversion *DEV " }; 208 | const br = "%c\n"; 209 | const maxLen = Math.max(...Object.values(conInfo).map((el) => el.length)); 210 | for (const [key] of Object.entries(conInfo)) { 211 | if (conInfo[key].length <= maxLen) conInfo[key] = conInfo[key].padEnd(maxLen); 212 | if (key == "header") conInfo[key] = `${conInfo[key].slice(0, -1)}⋮ `; 213 | } 214 | const header = 215 | "display:inline-block;border-width:1px 1px 0 1px;border-style:solid;border-color:#424242;color:white;background:#03a9f4;font-size:12px;padding:4px 4.5px 5px 6px;"; 216 | const info = "border-width:0px 1px 1px 1px;padding:7px;background:white;color:#424242;line-height:0.7;"; 217 | console.info(conInfo.header + br + conInfo.ver, header, "", `${header} ${info}`); 218 | 219 | // Initial Run 220 | Promise.resolve(customElements.whenDefined("hui-view")).then(() => { 221 | window.KioskMode = new KioskMode(); 222 | }); 223 | --------------------------------------------------------------------------------