├── .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 | [](https://github.com/custom-components/hacs) [](https://www.buymeacoffee.com/FgwNR2l)
4 |
5 | Hides the header and/or sidebar drawer in [Home Assistant](https://www.home-assistant.io/)
6 |
7 | 
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 |
--------------------------------------------------------------------------------