├── .eslintrc ├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .nvmrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── activating.md ├── context-aware.md ├── features.md ├── installing.md └── templates.md ├── gulp └── tasks │ ├── build.js │ ├── clean.js │ ├── compress.js │ └── default.js ├── gulpfile.js ├── home-assistant-polymer ├── package.json ├── polymer.json ├── script └── fix_date.sh ├── scripts-dbg-es5.js ├── scripts-dbg.js ├── scripts-es5.js.LICENSE ├── scripts-es5.js.map ├── scripts.js.LICENSE ├── scripts.js.map ├── src ├── elements │ ├── cui-base-element.js │ ├── dynamic-element.js │ ├── dynamic-with-extra.js │ ├── ha-config-custom-ui.js │ ├── ha-themed-slider.js │ ├── state-card-custom-ui.js │ ├── state-card-with-slider.js │ └── state-card-without-slider.js ├── entrypoints │ └── scripts.js ├── mixins │ └── events-mixin.js ├── state-card-custom-ui.html └── utils │ ├── hass-attribute-util.js │ ├── hooks.js │ └── version.js ├── state-card-custom-ui-dbg-es5.html ├── state-card-custom-ui-dbg-es5.html.gz ├── state-card-custom-ui-dbg.html ├── state-card-custom-ui-dbg.html.gz ├── state-card-custom-ui-es5.html ├── state-card-custom-ui-es5.html.gz ├── state-card-custom-ui.html ├── state-card-custom-ui.html.gz ├── update.sh ├── webpack.config.js └── yarn.lock /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base", 3 | "globals": { 4 | "__DEV__": false, 5 | "__DEMO__": false, 6 | "Polymer": true, 7 | }, 8 | "env": { 9 | "browser": true 10 | }, 11 | "rules": { 12 | "global-require": 0, 13 | "class-methods-use-this": 0, 14 | "no-underscore-dangle": 0, 15 | "no-plusplus": [ 2, {"allowForLoopAfterthoughts": true }], 16 | "no-bitwise": 0, 17 | "no-param-reassign": ["error", { "props": false }], 18 | "function-paren-newline": 0, 19 | "import/no-extraneous-dependencies": ["error", {"devDependencies": ["gulp/**", "webpack.config.js"]}], 20 | "import/extensions": ["error", "ignorePackages"] 21 | }, 22 | "plugins": [ 23 | "import" 24 | ], 25 | } 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Checklist 2 | - [ ] I'm running the latest version of CustomUI ([Update guide](https://github.com/andrey-git/home-assistant-custom-ui/blob/master/docs/installing.md#updating)) or using a specific [release](https://github.com/andrey-git/home-assistant-custom-ui/releases) that is not marked as "Broken". 3 | - [ ] I tried to force-refresh (Ctrl+Shift+R / Ctrl+F5) the browser 4 | - [ ] (Optional, but recommended) I'm using Chrome or tried to reproduce the feature on Chrome. 5 | 6 | **Browser + Version:** 7 | 8 | 12 | **CustomUI version:** 13 | 14 | **Home Assistant release (`hass --version`):** 15 | 16 | **Problem-relevant `configuration.yaml` entries:** 17 | ```yaml 18 | 19 | ``` 20 | 21 | **Problem-relevant Home Assistant log entries:** 22 | ``` 23 | 24 | ``` 25 | 26 | 27 | 36 | **Any errors from browser Javascript console:** 37 | ``` 38 | 39 | ``` 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | bower_components/ 3 | build/ 4 | build-dbg/ 5 | # IDE 6 | .idea/ 7 | # Locally installed version 8 | www/ 9 | custom_components/ 10 | panels/ 11 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 6.2 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Changelog 2 | 3 | #### 2019-05-18 4 | * Hotfix for HA 0.94+ 5 | 6 | #### 2019-03-24 7 | * Fix for `icon-color` for HA 0.88+ 8 | * Fox config panel for HA 0.90+ 9 | 10 | #### 2019-01-13 11 | * Hotfix for `extra_badge` for HA 0.85+ 12 | 13 | #### 2018-12-17 14 | * Hotfix for HA 0.84.1+ 15 | 16 | #### 2018-08-31 17 | * Fix for icon_color not being applied on HA >0.77 (by Jérôme) 18 | * This version requires HA 0.77+ 19 | 20 | #### 2018-08-06 21 | * Fix CustomUI attributes not being hidden in more-info 22 | 23 | #### 2018-07-13 24 | * Hotfix for HA 0.73+ 25 | 26 | #### 2018-06-25 27 | * Entities with `hide_in_default_view: false` will no longer be hidden from 28 | default view even if they are part of another view with 29 | `hide_in_default_view: true`. 30 | * Add `icon_color`, `state`, and `_stateDisplay` to the list of hidden attributes. 31 | 32 | #### 2018-06-02 33 | * Hotfix for HA 0.71+ 34 | * Restore handling custom attributes in form and more-info. 35 | 36 | #### 2018-05-28 37 | * New feature: [`control_element`](docs/features.md#custom-controls) 38 | * Fix theming on Firefix/Edge. 39 | 40 | #### 2018-05-21 41 | * Hotfix release for HA 0.70+ 42 | 43 | #### 2018-04-29 44 | * Add support for `action_name` attribute that can be used to change the displayed action on scene and script cards. 45 | * Make confirmable controls protect slider on 2nd line 46 | 47 | #### 2018-04-27 48 | * Add support for `icon_color` attribute to force-set icon color of any entity. 49 | 50 | #### 2018-03-30 51 | * Prevent template from being applied twice. Fixes #110 52 | * Use new system to display version info. 53 | * Fix customui config panel. Fixes #122 54 | * Prevent more-info from comming up when touching slider or lock. 55 | 56 | #### 2018-02-16 57 | Bugfixes: 58 | * On Firefox badges sometimes didn't use themes. 59 | * Update badges-in-state-card default margin to 0. 60 | * `blacklist_states` didn't work with values like 0, empty string. 61 | * Slider multiplied the value of template light by 10. 62 | 63 | #### 2018-01-26 64 | * Support for theming top-of-the-page badges. 65 | * Climate state-card can now get a temperature-controlling slide like light and cover cards. 66 | * Hotfix for HA 0.62 67 | 68 | #### 2018-01-17 69 | * Fix race condition that could prevent HA from starting 70 | 71 | #### 2018-01-14 : Broken release 72 | * Support hiding groups/entities from the default view 73 | 74 | #### 2018-01-12 75 | * Allow setting margin on slider via `--ha-themed-slider-margin`. 76 | * Allow changing state-card badge margins via `--ha-badges-card-margin`. Fixes #78 77 | * `extra_badge`s can now be individually themed. Fixes #77 78 | * Allow changing the size and number of UI columns (requires `customizer`) 79 | 80 | #### 2017-12-27 81 | * Hotfix for HA 0.61 82 | * Change size of badges in state card to 85% to match the badges in the top section. 83 | * Added --ha-badges-card-width and --ha-badges-card-text-align variables to allow theming badges in state card. 84 | * `extra_data_template` can now be an array to display several rows of data. 85 | * Align lock icon better for cover domain. 86 | 87 | #### 2017-12-15 88 | * Fix bug in template computation. 89 | 90 | #### 2017-12-14 : Broken release 91 | * New feature: [Templates](docs/templates.md) are now processed when states are fetched, so you can now template any state or attribute, nit just in state cards. For example: 92 | * Modify group members. 93 | * Make your own translation of states. 94 | * Improvements: 95 | * `confirm_controls` will now protect the whole state card and not just the toggle control. Fixes #39 96 | * Context-aware and Device-aware attributes can now be defined as regular expression. Fixes #55 97 | * [extra_badge](docs/features.md#add-badge-to-the-state-card) can now be a list if you want to put more than 1 badge into a state card. 98 | * Bugfix: Improve detection of whether slider should be hidden in `hide-slider` mode. Fixes #15 99 | 100 | #### 2017-11-29 : Breaking Change 101 | * File names changed into `state-card-custom-ui.html` and `state-card-custom-ui-es5.html`. Either update [customizer](https://github.com/andrey-git/home-assistant-customizer) or see updated instructions in [Activating](docs/activating.md) section. 102 | 103 | *Note: Update to at least 20171117 required for HA 0.58+* 104 | 105 | #### 2017-11-17 106 | * Compatibility fix with HA 0.58 107 | 108 | *Note: Update to at least 20170910 required for HA 0.53+* 109 | 110 | #### 2017-10-19 111 | * Add `_stateDisplay` template attribute to allow setting final visual state. 112 | 113 | #### 2017-10-18 114 | * Fix context-aware group names 115 | 116 | #### 2017-09-27 : Breaking Change 117 | * Entity state and attributes can now be overridden by templates. See [Templates](docs/templates.md) 118 | * *Breaking change*: `theme_template` attribute has been removed. Used templates to tweak `themes` attribute. 119 | Note that `extra_data_template` behavior didn't change. 120 | 121 | #### 2017-09-18 122 | * Delay initialization until states are loaded. 123 | * Give better error for broken templates. 124 | 125 | #### 2017-09-10 126 | * Fix some Polymer2-related bugs. 127 | 128 | #### 2017-09-09 : Broken 129 | * Compress code better and transpile it for ES5: Now it should support all browsers supported by Home Assistant. 130 | 131 | #### 2017-09-03 : Broken 132 | * Use badges without using group-in-group 133 | * Per-entity theming improvements (Thanks @ahofelt for reference implementation): 134 | * paper-card-background-color will now properly set per-entity background. 135 | * Label-related variable will now properly affect labels. 136 | * Support for using your own custom UI state cards with this CustomUI framework. 137 | 138 | 139 | #### 2017-08-30 : Config panel is broken 140 | * Support CustomUI attributes in customization config UI introduced in Home Assistant 0.53 141 | * Add CustomUI config subpanel. 142 | * Customizer support for loading local (on Home Assistant machine) or hosted on Github CustomUI. 143 | 144 | #### 2017-08-05 (minor change) 145 | * Improve performance by recalculating only after attachment. 146 | * Visual fix for Polymer2 147 | 148 | #### 2017-08-02 149 | * *Breaking Change* `extra_data_template` format changed. 150 | * Added `theme_template` attribute to make conditional entity-theming easy. 151 | 152 | #### 2017-07-29 153 | New features 154 | * New (optional) CustomUI panel. 155 | * New (optional) [customizer](https://github.com/andrey-git/home-assistant-customizer) custom component. 156 | * Register CustomUI panel above. 157 | * Hide CustomUI attribute in `more-info` (Requires HA 0.50+) 158 | * Hide arbitrary attributes in `more-info` (Requires HA 0.50+) 159 | * Dynamic customization. 160 | * New `update.sh` script that will keep CustomUI up to date. 161 | * Group names can now be context-aware. 162 | * `extra_data_template` can now look into another entity's state. 163 | * CustomUI version is now displayed in `dev-info` panel. 164 | * Attributes can now be device-aware in addition to being context-aware. Device name is set via CustomUI panel. 165 | * Per-entity theming (Requires HA 0.50+) 166 | * Confirmable controls. 167 | 168 | Bugfixes: 169 | * Customization now works for *unavailable* entities #24 (Requires HA 0.50+) 170 | 171 | #### 2017-07-06 172 | * Fix bug introduced on 20170701 which caused slider to do nothing. 173 | 174 | #### 2017-07-02 : BROKEN 175 | * `show_last_changed` is now supported on almost all state cards. 176 | 177 | #### 2017-07-01 : BROKEN 178 | * Now features are also supported on all "plain" and "toggle" cards in addition to light and cover cards. 179 | * *Breaking Change* Now all attributes can be context-aware, not just `friendly_name` and `hidden`. 180 | 181 | #### 2017-05-22 182 | * Extend support for context-aware names to views (in addition to groups) 183 | * Fix bug where the slider would sometimes show as 0 upon start. 184 | 185 | #### 2017-05-21 186 | * Add support for context-aware hide. 187 | * Add support for badges in state cards. 188 | 189 | #### 2017-05-12 190 | * Now to use the feature download a single minified `state-card-custom-ui.html` file. Unminified sources for development are under `src/` 191 | * Add support for context-aware entity names. This feature is supported for all domains, not just `light` and `cover`. 192 | * Add 'no-slider' mode in case you want to use light/cover custom-ui without the slider. 193 | 194 | #### 2017-05-11 195 | * Fix cover position range to be 0..100 by default (was 0..255 like light) 196 | * Switch back to using core `state-info` component instead of `custom-state-info`. 197 | Now `extra_data_template` feature requires HA 0.43+ 198 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 andrey-git 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 | # Custom UI elements for [Home Assistant](https://home-assistant.io) 2 | 3 | TODO: Add screenshots 4 | 5 | ## Notes 6 | **CustomUI 2019-05-18 required for HA 0.94+** 7 | 8 | **CustomUI 2018-12-17 required for HA 0.84.1+** 9 | 10 | **CustomUI 2018-08-06 is the last version to work on HA 0.76 and earlier** 11 | 12 | Please fill the [usage poll](https://docs.google.com/forms/d/e/1FAIpQLSdjgb4iu5aDyvFB6ch9KJpRn25I0wLL7NLyTIhcWCzU3KM1-w/viewform?usp=send_form) 13 | 14 | ## Installing 15 | See [installing](docs/installing.md) 16 | 17 | ## Activating 18 | See [activating](docs/activating.md) 19 | 20 | ## Features 21 | See [features](docs/features.md) 22 | 23 | ## Support 24 | Please ask questions and post feature requests in the [forum](https://community.home-assistant.io/t/customui-discussion-thread/48694). Post bugreports here on github in [issues](https://github.com/andrey-git/home-assistant-custom-ui/issues/) 25 | 26 | ## Changelog 27 | 28 | #### 2019-05-18 29 | * Hotfix for HA 0.94+ 30 | 31 | #### 2019-03-24 32 | * Fix for `icon-color` for HA 0.88+ 33 | * Fox config panel for HA 0.90+ 34 | 35 | #### 2019-01-13 36 | * Hotfix for `extra_badge` for HA 0.85+ 37 | 38 | 39 | [Full Changelog](CHANGELOG.md) 40 | -------------------------------------------------------------------------------- /docs/activating.md: -------------------------------------------------------------------------------- 1 | # Activating 2 | 3 | ## For Home Assistant 0.53+ 4 | 5 | #### 1. Tell Home Assistant to load relevant files. 6 | (**Only one** of 1.1 - 1.8) 7 | 8 | 1.1 Using [customizer](https://github.com/andrey-git/home-assistant-customizer/) for [local install](installing.md#local-install) 9 | ```yaml 10 | customizer: 11 | custom_ui: local 12 | ``` 13 | 14 | 1.2 Manually for [local install](installing.md#local-install) 15 | 16 | HA 0.53-0.58 17 | ```yaml 18 | frontend: 19 | extra_html_url: 20 | - /local/custom_ui/state-card-custom-ui.html 21 | ``` 22 | HA 0.59+ 23 | ```yaml 24 | frontend: 25 | extra_html_url: 26 | - /local/custom_ui/state-card-custom-ui.html 27 | extra_html_url_es5: 28 | - /local/custom_ui/state-card-custom-ui-es5.html 29 | 30 | ``` 31 | 1.3 Using [customizer](https://github.com/andrey-git/home-assistant-customizer/) for [hosted](installing.md#hosted-use-053) use of head version 32 | ```yaml 33 | customizer: 34 | custom_ui: hosted 35 | ``` 36 | 37 | 1.4 Manually for [hosted](installing.md#hosted-use-053) use of head version 38 | 39 | HA 0.53-0.58 40 | ```yaml 41 | frontend: 42 | extra_html_url: 43 | - https://raw.githubusercontent.com/andrey-git/home-assistant-custom-ui/master/state-card-custom-ui.html 44 | ``` 45 | HA 0.59+ 46 | ```yaml 47 | frontend: 48 | extra_html_url: 49 | - https://raw.githubusercontent.com/andrey-git/home-assistant-custom-ui/master/state-card-custom-ui.html 50 | extra_html_url_es5: 51 | - https://raw.githubusercontent.com/andrey-git/home-assistant-custom-ui/master/state-card-custom-ui-es5.html 52 | ``` 53 | 54 | 1.5 Using [customizer](https://github.com/andrey-git/home-assistant-customizer/) for a specific release 55 | ```yaml 56 | customizer: 57 | custom_ui: 20170830 58 | ``` 59 | 60 | 1.6 Manually for [hosted](installing.md#hosted-use-053) use of a specific release 61 | 62 | HA 0.53-0.58 63 | ```yaml 64 | frontend: 65 | extra_html_url: 66 | - https://github.com/andrey-git/home-assistant-custom-ui/releases/download/20170830/state-card-custom-ui.html 67 | ``` 68 | HA 0.59+ 69 | ```yaml 70 | frontend: 71 | extra_html_url: 72 | - https://github.com/andrey-git/home-assistant-custom-ui/releases/download/20171129/state-card-custom-ui.html 73 | extra_html_url_es5: 74 | - https://github.com/andrey-git/home-assistant-custom-ui/releases/download/20171129/state-card-custom-ui-es5.html 75 | ``` 76 | 77 | 1.7 Using [customizer](https://github.com/andrey-git/home-assistant-customizer/) for debug head version. (Only use for reporting readable Javascript errors!) 78 | ```yaml 79 | customizer: 80 | custom_ui: debug 81 | ``` 82 | 83 | 1.8 Manually for debug head version. (Only use for reporting readable JavaScript errors!) 84 | 85 | HA 0.53-0.58 86 | ```yaml 87 | frontend: 88 | extra_html_url: 89 | - https://raw.githubusercontent.com/andrey-git/home-assistant-custom-ui/master/state-card-custom-ui-dbg.html 90 | ``` 91 | HA 0.59+ 92 | ```yaml 93 | frontend: 94 | extra_html_url: 95 | - https://github.com/andrey-git/home-assistant-custom-ui/releases/download/20171129/state-card-custom-ui-dbg.html 96 | extra_html_url_es5: 97 | - https://github.com/andrey-git/home-assistant-custom-ui/releases/download/20171129/state-card-custom-ui-dbg-es5.html 98 | ``` 99 | 100 | #### 2. Tell Home Assistant to use CustomUI for state cards. 101 | 102 | In the `customize:` section of `configuration.yaml` put `custom_ui_state_card: state-card-custom-ui` for the relevant entities / domains. 103 | 104 | To use CustomUI for light and cover domain: 105 | ```yaml 106 | homeassistant: 107 | customize_glob: 108 | light.*: 109 | custom_ui_state_card: state-card-custom-ui 110 | cover.*: 111 | custom_ui_state_card: state-card-custom-ui 112 | ``` 113 | 114 | Note that yaml keys can't start with an asterix. Use quotes in that case: 115 | ```yaml 116 | customize_glob: 117 | "*.*": 118 | custom_ui_state_card: state-card-custom-ui 119 | ``` 120 | 121 | 122 | ## For Home Assistant up to 0.52 123 | In the `customize:` section of `configuration.yaml` put `custom_ui_state_card: custom-ui` for the relevant entities / domains. 124 | 125 | For example: 126 | ```yaml 127 | homeassistant: 128 | customize_glob: 129 | light.*: 130 | custom_ui_state_card: custom-ui 131 | cover.*: 132 | custom_ui_state_card: custom-ui 133 | ``` 134 | 135 | Note that yaml keys can't start with an asterisk. Use quotes in that case: 136 | ```yaml 137 | customize_glob: 138 | "*.*": 139 | custom_ui_state_card: custom-ui 140 | ``` 141 | -------------------------------------------------------------------------------- /docs/context-aware.md: -------------------------------------------------------------------------------- 1 | # Context / Device aware attributes 2 | ![context_aware](https://cloud.githubusercontent.com/assets/5478779/26284053/45fbc000-3e3b-11e7-8d4a-56ef0d5e6c60.png) 3 | 4 | ## Context-aware 5 | You can use context-aware attributes to give different names for the same entity in different groups or views. 6 | For example if you have a *Yard Light* and a *Yard Sensor* in a group named *Yard*, you could name the entities as *Light* and *Sensor* in the group only by using context-aware `friendly_name` attribute. This will also work in views (`view: yes` groups). In order to rename an entity in the default view, use `default_view` view name (even if you didn't define such a view). 7 | 8 | Example: 9 | ```yaml 10 | homeassistant: 11 | customize_glob: 12 | "*.*": 13 | custom_ui_state_card: state-card-custom-ui 14 | light.yard_light: 15 | friendly_name: Yard Light 16 | group: 17 | group.yard: 18 | friendly_name: Light 19 | sensor.yard_sensor: 20 | friendly_name: Yard Sensor 21 | group: 22 | group.yard: 23 | friendly_name: Sensor 24 | 25 | group: 26 | yard: 27 | entities: 28 | - light.yard_light 29 | - sensor.yard_sensor 30 | ``` 31 | 32 | ## Device-aware attributes 33 | You can also change attributes per device. For example: 34 | ```yaml 35 | homeassistant: 36 | customize_glob: 37 | "*.*": 38 | custom_ui_state_card: state-card-custom-ui 39 | device_tracker.joe_phone: 40 | friendly_name: Joe phone 41 | device: 42 | joe_mobile: 43 | friendly_name: My phone 44 | ``` 45 | The name of each device is set in CustomUI section of `config` pane. 46 | 47 | ![Section in Configuration panel](https://user-images.githubusercontent.com/5478779/33785533-1fa54a6c-dc6e-11e7-94e5-9e20071b1da9.png) 48 | 49 | If both context-aware and device-aware attributes are specified - device-aware will be applied first and then context-aware, possibly overriding device-aware attributes. Those can also be nested: 50 | ```yaml 51 | homeassistant: 52 | customize_glob: 53 | "*.*": 54 | custom_ui_state_card: state-card-custom-ui 55 | device_tracker.joe_phone: 56 | friendly_name: Joe phone 57 | group: 58 | group.phones: 59 | friendly_name: Joe 60 | device: 61 | joe_mobile: 62 | friendly_name: My phone 63 | group: 64 | phones: 65 | friendly_name: My 66 | 67 | group: 68 | phones: 69 | entities: 70 | - device_tracker.joe_phone 71 | ``` 72 | 73 | ### Using Regular Expressions 74 | The name of the group / device can be a [Regular Expression](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions). For example: 75 | ```yaml 76 | homeassistant: 77 | customize: 78 | device_tracker.joe_phone: 79 | friendly_name: Joe phone 80 | group: 81 | # This will catch both group.phones_boys and group.phones_girls groups 82 | group.phone.*: 83 | friendly_name: Joe 84 | ``` 85 | Note that this is a Regular Expression syntax, not Glob syntax used by `customize-glob`. 86 | 87 | ### Context-aware hide 88 | In case you want a device to be a member of a group but not *show* in the group - use context-aware `hidden` attribute. 89 | Unlike the regular `hidden: true` which hides the device in all views, context-aware `hidden: true` will hide the devices in specified groups only. 90 | ```yaml 91 | homeassistant: 92 | customize: 93 | ... 94 | light.yard_light: 95 | group: 96 | group.yard: 97 | hidden: true 98 | 99 | group: 100 | yard: 101 | entities: 102 | - light.yard_light 103 | ... 104 | ``` 105 | 106 | ### Other uses 107 | Context or Device aware attributes also work for custom attributes, like `hide_control`, `show_last_changed`, and others. 108 | -------------------------------------------------------------------------------- /docs/features.md: -------------------------------------------------------------------------------- 1 | # CustomUI Features 2 | 3 | * [Customizer component](#customizer-component) 4 | * [CustomUI panel](#customui-panel) 5 | * [Global Features](#global-features) 6 | + [Hiding entities from default view tab (HA 0.62+)](#hiding-entities-from-default-view-tab) 7 | + [Template attributes [New in 20170927]](#template-attributes) 8 | + [Icon Color](#icon-color) 9 | + [Action name](#action-name) 10 | * [Features available for all domains](#features-available-for-all-domains) 11 | + [Context-aware attributes](#context-aware-attributes) 12 | + [Badges in state cards](#badges-in-state-cards) 13 | + [Per entity theming (Requires HA 0.50+)](#per-entity-theming) 14 | + [Secondary custom UI](#secondary-customui) 15 | * [Features available for almost all domains](#features-available-for-almost-all-domains) 16 | + [You can always show the last-changed text](#you-can-always-show-the-last-changed-text) 17 | * [Features available for light, cover, climate, "plain", and "toggle" cards](#features-available-for-light-cover-climate-plain-and-toggle-cards) 18 | - [You can hide the control altogether](#you-can-hide-the-control-altogether) 19 | - [You can add extra data below the entity name](#you-can-add-extra-data-below-the-entity-name) 20 | - [Add badge to the state card](#add-badge-to-the-state-card) 21 | - [Confirmable controls](#confirmable-controls) 22 | - [Custom controls](#custom-controls) 23 | * [Features available for light and cover domains only](#features-available-for-light-and-cover-domains-only) 24 | - [If there is not enough horizontal space the mode is set by `state_card_mode` parameter](#if-there-is-not-enough-horizontal-space-the-mode-is-set-by-state_card_mode-parameter) 25 | - [If the slider got moved to a new line it will be 200 px wide.](#if-the-slider-got-moved-to-a-new-line-it-will-be-200-px-wide) 26 | - [The slider behavior is controlled by `slider_theme` dictionary.](#the-slider-behavior-is-controlled-by-slider_theme-dictionary) 27 | * [Complete example](#complete-example) 28 | 29 | 30 | ## Customizer component 31 | See instruction in a dedicated repository: [https://github.com/andrey-git/home-assistant-customizer](https://github.com/andrey-git/home-assistant-customizer/) 32 | Provides the following features: 33 | * Load CustomUI files (HA 0.53+) 34 | * Register CustomUI panel (HA 0.52 and below). 35 | * Hide CustomUI attributes in `more-info` (HA 0.50 - 0.52) 36 | * Hide arbitrary attributes in `more-info` (Requires HA 0.50+) 37 | * Dynamic customization. 38 | * Set the width and number of UI columns (Requires CustomUI 20180112+) 39 | 40 | 41 | ## CustomUI panel 42 | ![Section in Configuration panel](https://user-images.githubusercontent.com/5478779/33785533-1fa54a6c-dc6e-11e7-94e5-9e20071b1da9.png) 43 | Use it to set device name. 44 | 45 | In HA 0.53+ is added automatically to configuration panel. 46 | 47 | ## Global Features 48 | Those features do not require setting `custom_ui_state_card: state-card-custom-ui` 49 | on any entity. Just [loading](activating.md) is enough. 50 | 51 | ### Hiding entities from default view tab 52 | Requires HA 0.62+ 53 | 54 | If you don't have a `default_view` view defined you still hide some entities from the Home tab by setting `hide_in_default_view: true` attribute. 55 | 56 | If applied to a View (a group with `view: true`) `hide_in_default_view` will hide everything under that view (not just the view group itself). 57 | 58 | Apply `hide_in_default_view: false` to prevent a specific entity from being hidden even if it is part of another view with `hide_in_default_view: true` 59 | 60 | 61 | ### Template attributes 62 | You can set entity's attributes or state using JavaScript templates. See [Templates](templates.md) for more info. 63 | 64 | For example to show "Active" instead of "on" for binary sensor: 65 | ```yaml 66 | homeassistant: 67 | customize: 68 | binary_sensor.my_sensor: 69 | templates: 70 |        state: if (state === 'on') return 'Active'; else return state; 71 | ``` 72 | This is very powerful feature that can do a lot of cool things, but it could also have performance implications. 73 | 74 | ### Icon Color 75 | Starting from version 2018-04-27 you can set an icon color by specifying `icon_color` 76 | attribute which was removed from core in HA 0.66 77 | 78 | For example: 79 | ```yaml 80 | homeassistant: 81 | customize: 82 | light.bed_light: 83 | icon_color: green 84 | ``` 85 | 86 | The color could take any CSS color value. For example: `#FFACAC`, `red`, `rgba(10, 20, 30, 0.5)` etc. 87 | 88 | Note that the color will be applied as-is and it won't be affected by the `brightness` attribute.h 89 | 90 | ### Action name 91 | Starting from version 2018-04-29 you change the displayed action name by specifying `action_name` attribute. 92 | 93 | Action name is the button used to activate scenes and to execute non-interruptible scripts. 94 | 95 | For example: 96 | ```yaml 97 | homeassistant: 98 | customize: 99 | scene.dark: 100 | action_name: Darken 101 | ``` 102 | 103 | ## Features available for all domains 104 | 105 | ### Context-aware attributes 106 | ![context_aware](https://cloud.githubusercontent.com/assets/5478779/26284053/45fbc000-3e3b-11e7-8d4a-56ef0d5e6c60.png) 107 | 108 | You can use context-aware attributes to give different names/attributes for the same entity in different groups, views, or devices. 109 | See [context-aware.md](context-aware.md) 110 | 111 | ### Badges in state cards 112 | ![badges](https://cloud.githubusercontent.com/assets/5478779/26284132/b4a2dbe6-3e3c-11e7-9bb5-0441d30342bf.png) 113 | 114 | If you like badges, you can now put them in the state cards. This also works for domains that are usually not used as a badge. Lights for example. 115 | There are 4 ways to put badges in a state card. 116 | 117 | 1) Turn a single state card into a badge. Adjacent badges will clamp together to a single line. 118 | 119 | ```yaml 120 | homeassistant: 121 | customize_glob: 122 | "*.*": 123 | custom_ui_state_card: state-card-custom-ui 124 | sensor.door_sensor: 125 | state_card_mode: badges 126 | sensor.yard_sensor: 127 | state_card_mode: badges 128 | 129 | group: 130 | my_group: 131 | entities: 132 | - sensor.door_sensor 133 | - sensor.yard_sensor 134 | ``` 135 | 136 | 2) Create a dedicated group of devices you want to display as badges and apply `state_card_mode: badges` to it. Note that this group must be in another group. The example below will show 2 sensors as badges in outer_group's card. 137 | 138 | ```yaml 139 | homeassistant: 140 | customize_glob: 141 | "*.*": 142 | custom_ui_state_card: state-card-custom-ui 143 | group.inner_group: 144 | state_card_mode: badges 145 | 146 | group: 147 | inner_group: 148 | entities: 149 | - sensor.door_sensor 150 | - sensor.yard_sensor 151 | outer_group: 152 | entities: 153 | - group.inner_group 154 | *all other devices of outer_group* 155 | ``` 156 | 157 | 3) If you already have a group, *part* of which you want to display as badges *inside another group* - use `badges_list` to filter badge wannabe entities. In the previous example, if you wanted to show only `sensor.door_sensor` as a badge in outer_group: 158 | ```yaml 159 | ... 160 | group.inner_group: 161 | state_card_mode: badges 162 | badges_list: 163 | - sensor.door_sensor 164 | group: 165 | inner_group: 166 | ... 167 | outer_group: 168 | ... 169 | ... 170 | ``` 171 | 172 | 4) Creating a dedicated group has a downside that the group will also show in the UI as whole in the default_view. To prevent that, you can make the group include itself. In the following example `inner_group` and `outer_group` are the same group: 173 | ```yaml 174 | homeassistant: 175 | customize_glob: 176 | "*.*": 177 | custom_ui_state_card: state-card-custom-ui 178 | group.my_group: 179 | state_card_mode: badges 180 | 181 | group: 182 | my_group: 183 | entities: 184 | - sensor.door_sensor 185 | - sensor.yard_sensor 186 | - group.my_group 187 | *all other devices of outer_group* 188 | ``` 189 | If you use this example as-is you will notice that all of your devices in the group appear both as regular state cards and as badges. To limit badges to the door/yard sensors only use `badges_list` from Example 2. To hide door/yard sensor cards (but leave them as badges) use the [Context-aware hide attribute](#context-aware-attributes) feature. 190 | Full example: 191 | ```yaml 192 | homeassistant: 193 | customize_glob: 194 | "*.*": 195 | custom_ui_state_card: state-card-custom-ui 196 | group.my_group: 197 | state_card_mode: badges 198 | badges_list: 199 | - sensor.door_sensor 200 | - sensor.yard_sensor 201 | sensor.door_sensor: 202 | group: 203 | group.my_group: 204 | hidden: true 205 | sensor.yard_sensor: 206 | group: 207 | group.my_group: 208 | hidden: true 209 | 210 | group: 211 | my_group: 212 | entities: 213 | - sensor.door_sensor 214 | - sensor.yard_sensor 215 | - light.mylight 216 | - group.my_group 217 | ``` 218 | 219 | ### Per entity theming 220 | Requires HA 0.50+ 221 | 222 | ![entity_themed](https://user-images.githubusercontent.com/5478779/28746280-0839b3f2-74c4-11e7-9478-bb197f9fd005.png) 223 | 224 | You can select per-entity theme from the list of defined [themes](https://home-assistant.io/components/frontend/) 225 | ```yaml 226 | frontend: 227 | themes: 228 | green_example: 229 | paper-toggle-button-checked-button-color: green 230 | light.yard: 231 | theme: green_example 232 | ``` 233 | 234 | Starting from version 2018-01-26 per-entity theming can also affect top-of-the-page 235 | badges. 236 | 237 | ### Secondary customUI 238 | ![secondary_custom_ui](https://user-images.githubusercontent.com/5478779/30005196-a8d8bd2a-90e5-11e7-9f4c-a787a1227076.png) 239 | 240 | If you would like to use your own [state-card-custom-alarm.html](https://community.home-assistant.io/t/custom-ui-with-buttons-fan-control/13808/46) for `alarm_control_panel` but still enjoy framework features of CustomUI, like theming, you can use `state_card_custom_ui_secondary`: 241 | ```yaml 242 | homeassistant: 243 | customize_glob: 244 | "*.*": 245 | custom_ui_state_card: state-card-custom-ui 246 | alarm_control_panel.alarm: 247 | state_card_custom_ui_secondary: state-card-custom_alarm 248 | ``` 249 | 250 | ## Features available for almost all domains. 251 | 252 | The following is supported for all state cards except `configurator` 253 | 254 | #### You can always show the last-changed text 255 | ![show_last_changed](https://cloud.githubusercontent.com/assets/5478779/24838935/37b90bf8-1d5a-11e7-9e28-970740ba2fa8.png) 256 | 257 | Use `show_last_changed: true` 258 | 259 | Note that if you use the [extra_data_template](#you-can-add-extra-data-below-the-entity-name-requires-ha-043) below it will take precedence over `show_last_changed` 260 | 261 | ## Features available for light, cover, climate, "plain", and "toggle" cards. 262 | 263 | The next features are available for 4 types of cards: 264 | * Light 265 | * Cover 266 | * Climate 267 | * "Plain" i.e. card with icon, name, and state. 268 | * "Toggle" i.e. card with icon, name, and toggle. 269 | 270 | #### You can hide the control altogether 271 | ![hide_control](https://cloud.githubusercontent.com/assets/5478779/24772031/8a7d546e-1b18-11e7-935a-4360eeb9ebc8.png) 272 | 273 | Use `hide_control: true` to hide the control (toggle / cover buttons) altogether. 274 | 275 | #### You can add extra data below the entity name 276 | ![extra_data](https://cloud.githubusercontent.com/assets/5478779/24772032/8a7e90e0-1b18-11e7-9b3e-e36b56ef2417.png) 277 | 278 | Use `extra_data_template` to add extra data below the entity name. The format is a [Templates](templates.md). 279 | For example to show power consumption from the `power_consumption` attribute use: 280 | ```yaml 281 | extra_data_template: ${attributes.power_consumption}W 282 | ``` 283 | Use can add several lines by using an array: 284 | ```yaml 285 | extra_data_template: 286 | - ${attributes.power_consumption}W 287 | - ${attributes.temperature}C 288 | ``` 289 | 290 | #### Add badge to the state card 291 | ![extra_badge](https://cloud.githubusercontent.com/assets/5478779/24772030/8a7cc4ea-1b18-11e7-9313-f7654ffb0c71.png) 292 | 293 | Instead of using a gray text below the entity name you can add a sensor-like badge. There are two ways to do that: 294 | 1) Specify a real sensor by entity ID: 295 | ```yaml 296 | extra_badge: 297 | entity_id: sensor.my_sensor 298 | ``` 299 | 2) Make a fake sensor from entity's attribute: 300 | ```yaml 301 | extra_badge: 302 | attribute: power_consumption 303 | unit: W 304 | ``` 305 | 306 | If you use the first format, i.e. a real sensor, you can use context-aware theming 307 | on it. 308 | 309 | In both cases you can specify a blacklist of badge "states", when you don't want to see the badge. 310 | ```yaml 311 | extra_badge: 312 | entity_id: sensor.my_sensor 313 | blacklist_states: 0 314 | ``` 315 | 316 | You can also provide a list of badges: 317 | ```yaml 318 | extra_badge: 319 | - entity_id: sensor.my_sensor1 320 | blacklist_states: 0 321 | - entity_id: sensor.my_sensor2 322 | blacklist_states: 'z' 323 | ``` 324 | 325 | #### Confirmable controls 326 | ![confirmable](https://user-images.githubusercontent.com/5478779/28746903-6abd4be2-74ce-11e7-94d9-77423894c423.png) 327 | 328 | Sometimes you don't want to flip a switch by mistake. 329 | 330 | Use `confirm_controls_show_lock` to block the control and show a transparent lock icon over it. Tapping on the lock will open it for 5 seconds allowing to use the control. If you would like to prevent accidental flip without the visual lock hint, use `confirm_controls` instead. 331 | 332 | #### Custom controls 333 | 334 | You can replace the default control with another control by using `control_element` attribute. 335 | 336 | For example: 337 | ```yaml 338 | switch.my: 339 | control_element: my-custom-switch-element 340 | ``` 341 | 342 | You can also use `control_element: ''` to show the state, like the "plain" card does. 343 | 344 | ## Features available for light and cover domains only 345 | 346 | If there is enough space the card will have icon+name on the left, optional slider in the middle and toggle on the right: 347 | 348 | ![cover](https://cloud.githubusercontent.com/assets/5478779/23921980/4eab7978-0909-11e7-8058-ad17a52d93c3.png) 349 | 350 | ![wide](https://cloud.githubusercontent.com/assets/5478779/23335593/e344048e-fbc0-11e6-81fd-85466a6b98b2.png) 351 | 352 | #### If there is not enough horizontal space the mode is set by `state_card_mode` parameter 353 | ![medium](https://cloud.githubusercontent.com/assets/5478779/23335594/e909eee2-fbc0-11e6-8429-8648b89d6d13.png) ![narrow](https://cloud.githubusercontent.com/assets/5478779/23335595/eceaa92a-fbc0-11e6-9dff-018585f60ff0.png) 354 | 355 | | `state_card_mode` value | description | 356 | | --- | --- | 357 | | break-slider-toggle | Move the slider and the toggle together to a second line. | 358 | | single-line | Never use more than one line. Shrink the name and the slider. | 359 | | break-slider | Move slider to second line. Leave toggle on the first line. | 360 | | hide-slider | Hide the slider. | 361 | | no-slider (default) | Never show the slider even if there is enough space. | 362 | 363 | #### If the slider got moved to a new line it will be 200 px wide. 364 | Use `stretch_slider` attribute to make it stretch to all available space. 365 | 366 | #### The slider behavior is controlled by `slider_theme` dictionary. 367 | In that dictionary the following optional fields are available: 368 | 369 | | field | default | description | 370 | | --- | --- | --- | 371 | | min | 0 | Minimum slider value | 372 | | max | 255 for light, 100 for cover | Maximum slider value | 373 | | pin | False | Display numeric value when moving the slider | 374 | | off_when_min | True | Whether to turn the light *off* when moving the slider to the minimum value if that value is not 0 | 375 | | report_when_not_changed | True | Whether to send the light-controlling command if the slider was returned to the initial position. I.e. you moved the slider and then changed your mind | 376 | 377 | ## Complete example 378 | ```yaml 379 | homeassistant: 380 | customize: 381 | light.bedroom: 382 | custom_ui_state_card: state-card-custom-ui 383 | state_card_mode: break-slider 384 | stretch_slider: true 385 | extra_data_template: "${attributes.power_consumption !== 0 ? (attributes.power_consumption + 'W') : ''}" 386 | hide_control: false 387 | show_last_changed: false 388 | theme: happy 389 | confirm_controls_show_lock: true 390 | slider_theme: 391 | min: 10 392 | max: 200 393 | pin: true 394 | off_when_min: false 395 | report_when_not_changed: false 396 | extra_badge: 397 | entity_id: sensor.my_sensor # Will take precedence over attribute and unit below. 398 | attribute: power_consumption 399 | unit: W 400 | blacklist_states: 0 401 | 402 | frontend: 403 | extra_html_url: 404 | - /local/custom_ui/state-card-custom-ui.html 405 | ``` 406 | -------------------------------------------------------------------------------- /docs/installing.md: -------------------------------------------------------------------------------- 1 | # Installing CustomUI 2 | 3 | ## Hosted use (HA 0.53+) 4 | 5 | Instead of installing CustomUI files on you Home Assistant machine you can have your browser fetch them directly from GitHub servers. In this case you don't need to download any files. 6 | 7 | Pros: 8 | * Easier to set up. 9 | * You can choose to either use the 'latest' release or a specific release. 10 | * You automatically get bug-fixes if you use 'latest'. 11 | 12 | Cons: 13 | * Requires Internet connection. 14 | * The hoster of the files (GitHub) can track how often you open Home Assistant. 15 | * You automatically get breaking changes if you use 'latest'. 16 | 17 | ## Local install 18 | 19 | ### Automatic install. 20 | 21 | 1) Download [update.sh](../update.sh) to your homeassistant config dir. (For example `/home/homeassistant/.homeassistant/`) 22 | You can do so by running 23 | ```bash 24 | curl -o update.sh "https://raw.githubusercontent.com/andrey-git/home-assistant-custom-ui/master/update.sh?raw=true" 25 | ``` 26 | from that dir. 27 | 28 | 2) Make `update.sh` executable by running 29 | ```bash 30 | $ chmod u+x update.sh 31 | ``` 32 | 3) Run it: 33 | ```bash 34 | $ ./update.sh 35 | ``` 36 | 37 | The script updates itself and downloads CustomUI main code into `www/custom_ui` and Customizer component into `custom_components`. The script will prompt you about creating any of those dirs if they don't exist. 38 | 39 | ### Updating 40 | Run 41 | ```bash 42 | $ ./update.sh 43 | ``` 44 | again. It will update everything telling you which files changed. 45 | 46 | ### Manual install 47 | 48 | Place [state-card-custom-ui.html](../state-card-custom-ui.html?raw=true) and [state-card-custom-ui.html.gz](../state-card-custom-ui.html.gz?raw=true) in `~/.homeassistant/www/custom_ui/` dir to install the main code. 49 | 50 | Additionally on HA 0.59 and later place [state-card-custom-ui-es5.html](../state-card-custom-ui-es5.html?raw=true) and [state-card-custom-ui-es5.html.gz](../state-card-custom-ui-es5.html.gz?raw=true) in `~/.homeassistant/www/custom_ui/` dir to install the es5 version. 51 | 52 | (HA 0.52 and earlier only) Place [ha-panel-custom-ui.html](../ha-panel-custom-ui.html?raw=true) and [ha-panel-custom-ui.html.gz](../ha-panel-custom-ui.html.gz?raw=true) in `~/.homeassistant/panels/` dir to install CustomUI configuration panel. 53 | 54 | Place all files from [https://github.com/andrey-git/home-assistant-customizer/tree/master/customizer](https://github.com/andrey-git/home-assistant-customizer/tree/master/customizer) into `~/.homeassistant/custom_components/customizer` 55 | -------------------------------------------------------------------------------- /docs/templates.md: -------------------------------------------------------------------------------- 1 | # Templates 2 | 3 | The `templates` attributes allow you to inject your own expressions and code using JavaScript code or template literals in order to override entity attributes and state. Within the code / template literals, you have full access to the entity's state object, which allows you to access other properties such as last_changed, attributes.friendly_name, etc. The full set of objects available is shown below. 4 | 5 | Additionally, `extra_data_template` attribute is always evaluated as a template without using `templates` attributes for it. 6 | 7 | In order for a template to return "nothing", for example so that `extra_data_template` won't take space or allow showing `show_last_changed` - return `null` from your template. Note that `null` can only be returned using code format, as template literal format always returns a string. 8 | 9 | **Note that those are JavaScript templates evaluated in your browser, not Jinja2 templates which are evaluated server-side and use a different syntax.** 10 | 11 | ### Available variables 12 | #### Global variables 13 | Those variables allow one entity to set state / attribute based on another entity or some other "external" thing. Using those hurts UI performance, so use them only if you indeed want an entity to depend on something external. 14 | 15 | `hass` - the hass object 16 | 17 | `entities` - a map object from `entity_id` to state objects. 18 | 19 | #### Local Variables 20 | `entity` - the state object for the current entity. 21 | Note that if you are using device or context aware attributes then the object you get is already modified. 22 | 23 | `attributes` attributes map of the current entity. Shortcut for `entity.attributes`. 24 | 25 | `attribute` the original (non-template) value of the attribute being calculated by a template. When calculating `state`, `_stateDisplay`, or `extra_data_template` it will be undefined. 26 | 27 | `state` the original (non-template) state of the entity. 28 | 29 | ### Special attributes 30 | There are two special template attributes: 31 | * `state` will override the entity state. Note that logic that applies visual state still applies. For example non-`off` motion sensor is displayed as `detected`. Changing the `state` template wouldn't prevent this logic from running. 32 | * `_stateDisplay` sets the final display string and prevents any visual state logic from running. 33 | 34 | ### Formats 35 | You can provide the template in two different formats. 36 | * If the template contains the word `return` it will be treated as raw JavaScript code. 37 | * Otherwise it will be treated as JavaScript template literal that returns a string. 38 | 39 | ### Examples 40 | 41 | ```yaml 42 | homeassistant: 43 | customize: 44 | ... 45 | light.bedroom: 46 | templates: 47 | theme: > 48 | if (attributes.power_consumption > 10) return 'red'; else return 'default'; 49 | extra_data_template: > 50 | "${attributes.power_consumption !== 0 ? (attributes.power_consumption + 'W') : ''}" 51 | light.kitchen: 52 | templates: 53 | # Uses global variable - might be slow. 54 | theme: > 55 | if (entities['light.bedroom'].state === 'on') return 'red'; else return 'default'; 56 | extra_data_template: > 57 | if (entities['light.bedroom'].attributes.brightness > 30) return 'Yes'; else return null; 58 | ``` 59 | 60 | #### Display brightness (even if it is 0%) 61 | ```yaml 62 | homeassistant: 63 | customize: 64 | light.my_light: 65 | extra_data_template: ${Math.round(attributes.brightness || 0) / 2.55}% 66 | ``` 67 | 68 | #### Display brightness only if it exists and is non-zero: 69 | ```yaml 70 | homeassistant: 71 | customize: 72 | light.my_light: 73 | extra_data_template: > 74 | if (attributes.brightness) return Math.round(attributes.brightness / 2.55) + '%'; else return null; 75 | ``` 76 | 77 | #### Change 'on' state to 'activated': 78 | ```yaml 79 | homeassistant: 80 | customize: 81 | binary_sensor.my_sensor: 82 | templates: 83 | state: if (state === 'on') return 'activated'; else return state; 84 | binary_sensor.motion_sensor: 85 | templates: 86 | # null will make the regular state-based logic to run. 87 | _stateDisplay: if (state === 'on') return 'activated'; else return null; 88 | ``` 89 | 90 | #### Make a group that contains all `on` entities. 91 | *Note that the group itself must be preexisting, CustomUI can't create a new group.* 92 | 93 | *Note this doesn't change group membership that the backend sees, so calling services on this group would only affect the original members.* 94 | ```yaml 95 | homeassistant: 96 | customize: 97 | group.on_devices: 98 | templates: 99 | entity_id: return Object.keys(entities).filter(key => entities[key].state === 'on') 100 | ``` 101 | -------------------------------------------------------------------------------- /gulp/tasks/build.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const rename = require('gulp-rename'); 3 | const inlinesource = require('gulp-inline-source'); 4 | const replace = require('gulp-batch-replace'); 5 | 6 | function build(minify, transpile) { 7 | const toReplace = [ 8 | ['./entrypoints/scripts.js', minify ? `../build/scripts${transpile ? '-es5' : ''}.js` : `scripts-dbg${transpile ? '-es5' : ''}.js`], 9 | ]; 10 | let stream = gulp.src('src/state-card-custom-ui.html') 11 | .pipe(replace(toReplace)); 12 | if (minify) { 13 | stream = stream.pipe(inlinesource({ compress: false })); 14 | } 15 | return stream 16 | .pipe(rename({ basename: `state-card-custom-ui${minify ? '' : '-dbg'}${transpile ? '-es5' : ''}` })) 17 | .pipe(gulp.dest('build/')); 18 | } 19 | 20 | 21 | gulp.task('build-minify', build.bind(null, true, false)); 22 | gulp.task('build-dbg', build.bind(null, false, false)); 23 | gulp.task('build-minify-es5', build.bind(null, true, true)); 24 | gulp.task('build-dbg-es5', build.bind(null, false, true)); 25 | gulp.task('build', gulp.series('build-minify', 'build-dbg', 'build-minify-es5', 'build-dbg-es5')); 26 | -------------------------------------------------------------------------------- /gulp/tasks/clean.js: -------------------------------------------------------------------------------- 1 | const del = require('del'); 2 | const gulp = require('gulp'); 3 | 4 | gulp.task('clean', () => del(['build'])); 5 | -------------------------------------------------------------------------------- /gulp/tasks/compress.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const zopfli = require('gulp-zopfli-green'); 3 | 4 | gulp.task('compress', () => 5 | gulp.src('state-card-custom-ui*.html') 6 | .pipe(zopfli()) 7 | .pipe(gulp.dest('./'))); 8 | -------------------------------------------------------------------------------- /gulp/tasks/default.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | 3 | gulp.task('default', gulp.series(['build'])); 4 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var requireDir = require('require-dir'); 2 | 3 | requireDir('./gulp/tasks/'); 4 | -------------------------------------------------------------------------------- /home-assistant-polymer: -------------------------------------------------------------------------------- 1 | ../home-assistant-polymer -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "home-assistant-custom-ui", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "dependencies": { 7 | "@babel/preset-typescript": "^7.1.0", 8 | "@polymer/polymer": "^3.0.2", 9 | "del": "^3.0.0", 10 | "eslint": "^4.13.0", 11 | "eslint-config-airbnb-base": "^12.1.0", 12 | "gulp": "^4", 13 | "gulp-rename": "^1.2.2", 14 | "gulp-zopfli-green": "^3.0.0", 15 | "lit-element": "^2.1.0", 16 | "require-dir": "^0.3.2", 17 | "typescript": "^3.2.2", 18 | "webpack": "^4.8.3" 19 | }, 20 | "scripts": { 21 | "init": "yarn install", 22 | "test": "eslint src gulp webpack.config.js && polymer lint", 23 | "webpack": "webpack", 24 | "gulp": "gulp", 25 | "gzip": "yarn run gulp compress", 26 | "build": "yarn run webpack && yarn run gulp && cp build/*.{html,map,LICENSE} . && cp build/*dbg*.js . && yarn run gzip", 27 | "deploy": "yarn run test && yarn run build && cp *.{js,html,map,LICENSE,gz} ~/.homeassistant/www/custom_ui/", 28 | "deploy_dev": "rm -rf ~/.homeassistant/www/custom_ui/* && yarn run deploy && mv ~/.homeassistant/www/custom_ui/state-card-custom-ui-dbg-es5.html.gz ~/.homeassistant/www/custom_ui/state-card-custom-ui-es5.html.gz && mv ~/.homeassistant/www/custom_ui/state-card-custom-ui-dbg.html.gz ~/.homeassistant/www/custom_ui/state-card-custom-ui.html.gz", 29 | "fix_date": "script/fix_date.sh", 30 | "release": "yarn run test && yarn run fix_date && yarn run deploy" 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "git+https://github.com/andrey-git/home-assistant-custom-ui.git" 35 | }, 36 | "author": "", 37 | "license": "MIT", 38 | "bugs": { 39 | "url": "https://github.com/andrey-git/home-assistant-custom-ui/issues" 40 | }, 41 | "homepage": "https://github.com/andrey-git/home-assistant-custom-ui#readme", 42 | "devDependencies": { 43 | "@babel/core": "^7.2.2", 44 | "@babel/preset-env": "^7.2.3", 45 | "babel-loader": "^8.0.5", 46 | "eslint-plugin-import": "^2.12.0", 47 | "gulp-batch-replace": "^0.0.0", 48 | "gulp-inline-source": "^3.1.0", 49 | "polymer-cli": "^1.7.2", 50 | "uglifyjs-webpack-plugin": "^1.2.5", 51 | "webpack-cli": "^2.1.3" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /polymer.json: -------------------------------------------------------------------------------- 1 | { 2 | "entrypoint": "src/state-card-custom-ui.html", 3 | "shell": "src/entrypoints/scripts.js", 4 | "sources": [ 5 | "src/**/*" 6 | ], 7 | "lint": { 8 | "ignoreWarnings": ["could-not-resolve-reference", "could-not-load"], 9 | "rules": ["polymer-3"] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /script/fix_date.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | sed -i -r "s/(export default ')[^']+/\1`date +%Y%m%d`/" src/utils/version.js 3 | -------------------------------------------------------------------------------- /scripts-es5.js.LICENSE: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 4 | * This code may only be used under the BSD style license found at 5 | * http://polymer.github.io/LICENSE.txt 6 | * The complete set of authors may be found at 7 | * http://polymer.github.io/AUTHORS.txt 8 | * The complete set of contributors may be found at 9 | * http://polymer.github.io/CONTRIBUTORS.txt 10 | * Code distributed by Google as part of the polymer project is also 11 | * subject to an additional IP rights grant found at 12 | * http://polymer.github.io/PATENTS.txt 13 | */ 14 | 15 | /** 16 | * @license 17 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 18 | * This code may only be used under the BSD style license found at 19 | * http://polymer.github.io/LICENSE.txt 20 | * The complete set of authors may be found at 21 | * http://polymer.github.io/AUTHORS.txt 22 | * The complete set of contributors may be found at 23 | * http://polymer.github.io/CONTRIBUTORS.txt 24 | * Code distributed by Google as part of the polymer project is also 25 | * subject to an additional IP rights grant found at 26 | * http://polymer.github.io/PATENTS.txt 27 | */ 28 | 29 | /** 30 | * @license 31 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 32 | * This code may only be used under the BSD style license found at 33 | * http://polymer.github.io/LICENSE.txt 34 | * The complete set of authors may be found at 35 | * http://polymer.github.io/AUTHORS.txt 36 | * The complete set of contributors may be found at 37 | * http://polymer.github.io/CONTRIBUTORS.txt 38 | * Code distributed by Google as part of the polymer project is also 39 | * subject to an additional IP rights grant found at 40 | * http://polymer.github.io/PATENTS.txt 41 | */ 42 | 43 | /** 44 | * @license 45 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 46 | * This code may only be used under the BSD style license found at 47 | * http://polymer.github.io/LICENSE.txt 48 | * The complete set of authors may be found at 49 | * http://polymer.github.io/AUTHORS.txt 50 | * The complete set of contributors may be found at 51 | * http://polymer.github.io/CONTRIBUTORS.txt 52 | * Code distributed by Google as part of the polymer project is also 53 | * subject to an additional IP rights grant found at 54 | * http://polymer.github.io/PATENTS.txt 55 | */ 56 | 57 | /** 58 | * @license 59 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 60 | * This code may only be used under the BSD style license found at 61 | * http://polymer.github.io/LICENSE.txt 62 | * The complete set of authors may be found at 63 | * http://polymer.github.io/AUTHORS.txt 64 | * The complete set of contributors may be found at 65 | * http://polymer.github.io/CONTRIBUTORS.txt 66 | * Code distributed by Google as part of the polymer project is also 67 | * subject to an additional IP rights grant found at 68 | * http://polymer.github.io/PATENTS.txt 69 | */ 70 | 71 | /** 72 | * @license 73 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 74 | * This code may only be used under the BSD style license found at 75 | * http://polymer.github.io/LICENSE.txt 76 | * The complete set of authors may be found at 77 | * http://polymer.github.io/AUTHORS.txt 78 | * The complete set of contributors may be found at 79 | * http://polymer.github.io/CONTRIBUTORS.txt 80 | * Code distributed by Google as part of the polymer project is also 81 | * subject to an additional IP rights grant found at 82 | * http://polymer.github.io/PATENTS.txt 83 | */ 84 | 85 | /** 86 | * @license 87 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 88 | * This code may only be used under the BSD style license found at 89 | * http://polymer.github.io/LICENSE.txt 90 | * The complete set of authors may be found at 91 | * http://polymer.github.io/AUTHORS.txt 92 | * The complete set of contributors may be found at 93 | * http://polymer.github.io/CONTRIBUTORS.txt 94 | * Code distributed by Google as part of the polymer project is also 95 | * subject to an additional IP rights grant found at 96 | * http://polymer.github.io/PATENTS.txt 97 | */ 98 | 99 | /** 100 | * @license 101 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 102 | * This code may only be used under the BSD style license found at 103 | * http://polymer.github.io/LICENSE.txt 104 | * The complete set of authors may be found at 105 | * http://polymer.github.io/AUTHORS.txt 106 | * The complete set of contributors may be found at 107 | * http://polymer.github.io/CONTRIBUTORS.txt 108 | * Code distributed by Google as part of the polymer project is also 109 | * subject to an additional IP rights grant found at 110 | * http://polymer.github.io/PATENTS.txt 111 | */ 112 | 113 | /** 114 | * @license 115 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 116 | * This code may only be used under the BSD style license found at 117 | * http://polymer.github.io/LICENSE.txt 118 | * The complete set of authors may be found at 119 | * http://polymer.github.io/AUTHORS.txt 120 | * The complete set of contributors may be found at 121 | * http://polymer.github.io/CONTRIBUTORS.txt 122 | * Code distributed by Google as part of the polymer project is also 123 | * subject to an additional IP rights grant found at 124 | * http://polymer.github.io/PATENTS.txt 125 | */ 126 | 127 | /** 128 | * @license 129 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 130 | * This code may only be used under the BSD style license found at 131 | * http://polymer.github.io/LICENSE.txt 132 | * The complete set of authors may be found at 133 | * http://polymer.github.io/AUTHORS.txt 134 | * The complete set of contributors may be found at 135 | * http://polymer.github.io/CONTRIBUTORS.txt 136 | * Code distributed by Google as part of the polymer project is also 137 | * subject to an additional IP rights grant found at 138 | * http://polymer.github.io/PATENTS.txt 139 | */ 140 | 141 | /** 142 | * @license 143 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 144 | * This code may only be used under the BSD style license found at 145 | * http://polymer.github.io/LICENSE.txt 146 | * The complete set of authors may be found at 147 | * http://polymer.github.io/AUTHORS.txt 148 | * The complete set of contributors may be found at 149 | * http://polymer.github.io/CONTRIBUTORS.txt 150 | * Code distributed by Google as part of the polymer project is also 151 | * subject to an additional IP rights grant found at 152 | * http://polymer.github.io/PATENTS.txt 153 | */ 154 | 155 | /** 156 | @license 157 | Copyright (c) 2019 The Polymer Project Authors. All rights reserved. 158 | This code may only be used under the BSD style license found at 159 | http://polymer.github.io/LICENSE.txt The complete set of authors may be found at 160 | http://polymer.github.io/AUTHORS.txt The complete set of contributors may be 161 | found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as 162 | part of the polymer project is also subject to an additional IP rights grant 163 | found at http://polymer.github.io/PATENTS.txt 164 | */ 165 | 166 | /** 167 | * @license 168 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 169 | * This code may only be used under the BSD style license found at 170 | * http://polymer.github.io/LICENSE.txt 171 | * The complete set of authors may be found at 172 | * http://polymer.github.io/AUTHORS.txt 173 | * The complete set of contributors may be found at 174 | * http://polymer.github.io/CONTRIBUTORS.txt 175 | * Code distributed by Google as part of the polymer project is also 176 | * subject to an additional IP rights grant found at 177 | * http://polymer.github.io/PATENTS.txt 178 | */ 179 | 180 | /** 181 | * @license 182 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 183 | * This code may only be used under the BSD style license found at 184 | * http://polymer.github.io/LICENSE.txt 185 | * The complete set of authors may be found at 186 | * http://polymer.github.io/AUTHORS.txt 187 | * The complete set of contributors may be found at 188 | * http://polymer.github.io/CONTRIBUTORS.txt 189 | * Code distributed by Google as part of the polymer project is also 190 | * subject to an additional IP rights grant found at 191 | * http://polymer.github.io/PATENTS.txt 192 | */ 193 | 194 | /** 195 | @license 196 | Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 197 | This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt 198 | The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt 199 | The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt 200 | Code distributed by Google as part of the polymer project is also 201 | subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt 202 | */ 203 | 204 | /** 205 | @license 206 | Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 207 | This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt 208 | The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt 209 | The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt 210 | Code distributed by Google as part of the polymer project is also 211 | subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt 212 | */ 213 | -------------------------------------------------------------------------------- /scripts.js.LICENSE: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 4 | * This code may only be used under the BSD style license found at 5 | * http://polymer.github.io/LICENSE.txt 6 | * The complete set of authors may be found at 7 | * http://polymer.github.io/AUTHORS.txt 8 | * The complete set of contributors may be found at 9 | * http://polymer.github.io/CONTRIBUTORS.txt 10 | * Code distributed by Google as part of the polymer project is also 11 | * subject to an additional IP rights grant found at 12 | * http://polymer.github.io/PATENTS.txt 13 | */ 14 | 15 | /** 16 | * @license 17 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 18 | * This code may only be used under the BSD style license found at 19 | * http://polymer.github.io/LICENSE.txt 20 | * The complete set of authors may be found at 21 | * http://polymer.github.io/AUTHORS.txt 22 | * The complete set of contributors may be found at 23 | * http://polymer.github.io/CONTRIBUTORS.txt 24 | * Code distributed by Google as part of the polymer project is also 25 | * subject to an additional IP rights grant found at 26 | * http://polymer.github.io/PATENTS.txt 27 | */ 28 | 29 | /** 30 | * @license 31 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 32 | * This code may only be used under the BSD style license found at 33 | * http://polymer.github.io/LICENSE.txt 34 | * The complete set of authors may be found at 35 | * http://polymer.github.io/AUTHORS.txt 36 | * The complete set of contributors may be found at 37 | * http://polymer.github.io/CONTRIBUTORS.txt 38 | * Code distributed by Google as part of the polymer project is also 39 | * subject to an additional IP rights grant found at 40 | * http://polymer.github.io/PATENTS.txt 41 | */ 42 | 43 | /** 44 | * @license 45 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 46 | * This code may only be used under the BSD style license found at 47 | * http://polymer.github.io/LICENSE.txt 48 | * The complete set of authors may be found at 49 | * http://polymer.github.io/AUTHORS.txt 50 | * The complete set of contributors may be found at 51 | * http://polymer.github.io/CONTRIBUTORS.txt 52 | * Code distributed by Google as part of the polymer project is also 53 | * subject to an additional IP rights grant found at 54 | * http://polymer.github.io/PATENTS.txt 55 | */ 56 | 57 | /** 58 | * @license 59 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 60 | * This code may only be used under the BSD style license found at 61 | * http://polymer.github.io/LICENSE.txt 62 | * The complete set of authors may be found at 63 | * http://polymer.github.io/AUTHORS.txt 64 | * The complete set of contributors may be found at 65 | * http://polymer.github.io/CONTRIBUTORS.txt 66 | * Code distributed by Google as part of the polymer project is also 67 | * subject to an additional IP rights grant found at 68 | * http://polymer.github.io/PATENTS.txt 69 | */ 70 | 71 | /** 72 | * @license 73 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 74 | * This code may only be used under the BSD style license found at 75 | * http://polymer.github.io/LICENSE.txt 76 | * The complete set of authors may be found at 77 | * http://polymer.github.io/AUTHORS.txt 78 | * The complete set of contributors may be found at 79 | * http://polymer.github.io/CONTRIBUTORS.txt 80 | * Code distributed by Google as part of the polymer project is also 81 | * subject to an additional IP rights grant found at 82 | * http://polymer.github.io/PATENTS.txt 83 | */ 84 | 85 | /** 86 | * @license 87 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 88 | * This code may only be used under the BSD style license found at 89 | * http://polymer.github.io/LICENSE.txt 90 | * The complete set of authors may be found at 91 | * http://polymer.github.io/AUTHORS.txt 92 | * The complete set of contributors may be found at 93 | * http://polymer.github.io/CONTRIBUTORS.txt 94 | * Code distributed by Google as part of the polymer project is also 95 | * subject to an additional IP rights grant found at 96 | * http://polymer.github.io/PATENTS.txt 97 | */ 98 | 99 | /** 100 | * @license 101 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 102 | * This code may only be used under the BSD style license found at 103 | * http://polymer.github.io/LICENSE.txt 104 | * The complete set of authors may be found at 105 | * http://polymer.github.io/AUTHORS.txt 106 | * The complete set of contributors may be found at 107 | * http://polymer.github.io/CONTRIBUTORS.txt 108 | * Code distributed by Google as part of the polymer project is also 109 | * subject to an additional IP rights grant found at 110 | * http://polymer.github.io/PATENTS.txt 111 | */ 112 | 113 | /** 114 | * @license 115 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 116 | * This code may only be used under the BSD style license found at 117 | * http://polymer.github.io/LICENSE.txt 118 | * The complete set of authors may be found at 119 | * http://polymer.github.io/AUTHORS.txt 120 | * The complete set of contributors may be found at 121 | * http://polymer.github.io/CONTRIBUTORS.txt 122 | * Code distributed by Google as part of the polymer project is also 123 | * subject to an additional IP rights grant found at 124 | * http://polymer.github.io/PATENTS.txt 125 | */ 126 | 127 | /** 128 | * @license 129 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 130 | * This code may only be used under the BSD style license found at 131 | * http://polymer.github.io/LICENSE.txt 132 | * The complete set of authors may be found at 133 | * http://polymer.github.io/AUTHORS.txt 134 | * The complete set of contributors may be found at 135 | * http://polymer.github.io/CONTRIBUTORS.txt 136 | * Code distributed by Google as part of the polymer project is also 137 | * subject to an additional IP rights grant found at 138 | * http://polymer.github.io/PATENTS.txt 139 | */ 140 | 141 | /** 142 | @license 143 | Copyright (c) 2019 The Polymer Project Authors. All rights reserved. 144 | This code may only be used under the BSD style license found at 145 | http://polymer.github.io/LICENSE.txt The complete set of authors may be found at 146 | http://polymer.github.io/AUTHORS.txt The complete set of contributors may be 147 | found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as 148 | part of the polymer project is also subject to an additional IP rights grant 149 | found at http://polymer.github.io/PATENTS.txt 150 | */ 151 | 152 | /** 153 | * @license 154 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 155 | * This code may only be used under the BSD style license found at 156 | * http://polymer.github.io/LICENSE.txt 157 | * The complete set of authors may be found at 158 | * http://polymer.github.io/AUTHORS.txt 159 | * The complete set of contributors may be found at 160 | * http://polymer.github.io/CONTRIBUTORS.txt 161 | * Code distributed by Google as part of the polymer project is also 162 | * subject to an additional IP rights grant found at 163 | * http://polymer.github.io/PATENTS.txt 164 | */ 165 | 166 | /** 167 | @license 168 | Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 169 | This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt 170 | The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt 171 | The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt 172 | Code distributed by Google as part of the polymer project is also 173 | subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt 174 | */ 175 | 176 | /** 177 | @license 178 | Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 179 | This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt 180 | The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt 181 | The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt 182 | Code distributed by Google as part of the polymer project is also 183 | subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt 184 | */ 185 | -------------------------------------------------------------------------------- /src/elements/cui-base-element.js: -------------------------------------------------------------------------------- 1 | import '../utils/hooks.js'; 2 | 3 | export default (superClass) => { 4 | /** 5 | * @extends HTMLElement 6 | */ 7 | class CuiBaseElement extends superClass { 8 | static get properties() { 9 | return { 10 | hass: Object, 11 | inDialog: { 12 | type: Boolean, 13 | value: false, 14 | }, 15 | stateObj: Object, 16 | controlElement: String, 17 | extra: { 18 | type: Array, 19 | computed: 'computeExtra(hass, stateObj)', 20 | }, 21 | }; 22 | } 23 | 24 | computeExtra(hass, stateObj) { 25 | let extras = stateObj.attributes.extra_data_template; 26 | if (extras) { 27 | if (!Array.isArray(extras)) { 28 | extras = [extras]; 29 | } 30 | return extras.map(extra => window.customUI.computeTemplate( 31 | extra, 32 | hass, 33 | hass.states, 34 | stateObj, 35 | stateObj.attributes, 36 | /* attribute= */ undefined, 37 | stateObj.state, 38 | )).filter(result => result !== null); 39 | } 40 | return []; 41 | } 42 | 43 | showLastChanged(stateObj, inDialog, extra) { 44 | if (inDialog) return true; 45 | if (extra.length) return false; 46 | return !!stateObj.attributes.show_last_changed; 47 | } 48 | 49 | hasExtra(extra) { 50 | return extra.length > 0; 51 | } 52 | } 53 | return CuiBaseElement; 54 | }; 55 | -------------------------------------------------------------------------------- /src/elements/dynamic-element.js: -------------------------------------------------------------------------------- 1 | import dynamicContentUpdater from '../../home-assistant-polymer/src/common/dom/dynamic_content_updater.ts'; 2 | 3 | function loadCustomUI() { 4 | /** 5 | * @extends HTMLElement 6 | */ 7 | class DynamicElement extends Polymer.Element { 8 | static get properties() { 9 | return { 10 | hass: Object, 11 | stateObj: Object, 12 | elementName: String, 13 | 14 | inDialog: { 15 | type: Boolean, 16 | value: false, 17 | }, 18 | }; 19 | } 20 | 21 | static get observers() { 22 | return [ 23 | 'observerFunc(hass, stateObj, elementName, inDialog)', 24 | ]; 25 | } 26 | 27 | observerFunc(hass, stateObj, elementName, inDialog) { 28 | dynamicContentUpdater( 29 | this, 30 | elementName ? elementName.toUpperCase() : 'DIV', 31 | { hass, stateObj, inDialog }); 32 | } 33 | } 34 | customElements.define('dynamic-element', DynamicElement); 35 | } 36 | if (Polymer && Polymer.Element && customElements.get('home-assistant')) { 37 | loadCustomUI(); 38 | } else { 39 | customElements.whenDefined('home-assistant').then(() => loadCustomUI()); 40 | } 41 | -------------------------------------------------------------------------------- /src/elements/dynamic-with-extra.js: -------------------------------------------------------------------------------- 1 | import { html } from '@polymer/polymer/lib/utils/html-tag.js'; 2 | import applyThemesOnElement from '../../home-assistant-polymer/src/common/dom/apply_themes_on_element.ts'; 3 | 4 | import './dynamic-element.js'; 5 | import '../utils/hooks.js'; 6 | 7 | customElements.whenDefined('state-card-display').then(() => { 8 | /** 9 | * @extends HTMLElement 10 | */ 11 | class DynamicWithExtra extends customElements.get('state-card-display') { 12 | static get template() { 13 | return html` 14 | 15 | 61 |
62 | 69 | 91 |
92 | `; 93 | } 94 | 95 | static get properties() { 96 | return { 97 | hass: Object, 98 | inDialog: { 99 | type: Boolean, 100 | value: false, 101 | }, 102 | stateObj: Object, 103 | controlElement: String, 104 | extraObj: { 105 | type: Array, 106 | computed: 'computeExtra(hass, stateObj, _attached)', 107 | }, 108 | _attached: Boolean, 109 | extraObjVisible: { 110 | type: Boolean, 111 | computed: 'computeExtraVisible(extraObj, inDialog)', 112 | }, 113 | }; 114 | } 115 | 116 | connectedCallback() { 117 | super.connectedCallback(); 118 | this._attached = true; 119 | } 120 | 121 | disconnectedCallback() { 122 | this._isAttached = false; 123 | super.disconnectedCallback(); 124 | } 125 | 126 | computeExtra(hass, stateObj, attached) { 127 | if (!stateObj.attributes.extra_badge || !attached) return []; 128 | let extraBadges = stateObj.attributes.extra_badge; 129 | if (!Array.isArray(extraBadges)) { 130 | extraBadges = [extraBadges]; 131 | } 132 | return extraBadges.map((extraBadge) => { 133 | let result = null; 134 | if (extraBadge.entity_id && hass.states[extraBadge.entity_id]) { 135 | result = Object.assign({}, window.customUI.maybeChangeObject( 136 | this, hass.states[extraBadge.entity_id], this.inDialog, 137 | /* allowHidden= */false)); 138 | } else if (extraBadge.attribute && 139 | stateObj.attributes[extraBadge.attribute] !== undefined) { 140 | result = { 141 | state: String(stateObj.attributes[extraBadge.attribute]), 142 | entity_id: 'none.none', 143 | attributes: { unit_of_measurement: extraBadge.unit }, 144 | }; 145 | } 146 | if (!result) return null; 147 | let blacklist = extraBadge.blacklist_states; 148 | if (blacklist !== undefined) { 149 | if (!Array.isArray(blacklist)) { 150 | blacklist = [blacklist]; 151 | } 152 | if (blacklist.some(v => RegExp(v).test(result.state.toString()))) { 153 | return null; 154 | } 155 | } 156 | result._entityDisplay = ''; 157 | result.attributes = Object.assign({}, { friendly_name: '' }); 158 | return result; 159 | }).filter(extraBadge => extraBadge != null); 160 | } 161 | 162 | computeExtraVisible(extraObj, inDialog) { 163 | if (inDialog || !extraObj) return false; 164 | return extraObj.length !== 0; 165 | } 166 | 167 | extraClass(extraObjVisible) { 168 | return extraObjVisible ? 'extra' : ''; 169 | } 170 | 171 | _showControl(inDialog, stateObj) { 172 | if (inDialog) return true; 173 | return !stateObj.attributes.hide_control; 174 | } 175 | 176 | computeStateDisplay(stateObj) { 177 | // haLocalize removed in 0.61 178 | return super.computeStateDisplay(this.haLocalize || this.localize, stateObj); 179 | } 180 | 181 | isConfirmControls(stateObj) { 182 | return stateObj.attributes.confirm_controls || 183 | stateObj.attributes.confirm_controls_show_lock; 184 | } 185 | 186 | clickHandler(e) { 187 | this.root.querySelector('#overlay').style.pointerEvents = 'none'; 188 | const lock = this.root.querySelector('#lock'); 189 | if (lock) { 190 | lock.icon = 'mdi:lock-open-outline'; 191 | lock.style.opacity = '0.1'; 192 | } 193 | window.setTimeout(() => { 194 | this.root.querySelector('#overlay').style.pointerEvents = ''; 195 | if (lock) { 196 | lock.icon = 'mdi:lock-outline'; 197 | lock.style.opacity = ''; 198 | } 199 | }, 5000); 200 | e.stopPropagation(); 201 | } 202 | 203 | applyThemes(hass, element, stateObj) { 204 | const themeName = stateObj.attributes.theme || 'default'; 205 | applyThemesOnElement( 206 | element, hass.themes || { default_theme: 'default', themes: {} }, themeName); 207 | } 208 | 209 | extraDomChanged() { 210 | this.root.querySelectorAll('ha-state-label-badge') 211 | .forEach((elem) => { 212 | this.applyThemes(this.hass, elem, elem.state); 213 | }); 214 | } 215 | } 216 | customElements.define('dynamic-with-extra', DynamicWithExtra); 217 | }); 218 | -------------------------------------------------------------------------------- /src/elements/ha-config-custom-ui.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html } from 'lit-element'; 2 | import '../utils/hooks.js'; 3 | 4 | /** 5 | * @extends HTMLElement 6 | */ 7 | class HaConfigCustomUi extends LitElement { 8 | render() { 9 | return html` 10 | 11 | 12 | 13 | 14 | 18 |
Custom UI settings
19 |
20 |
21 | 22 | 23 | 24 |
25 | Set device name so that you can reference it in per-device settings 26 | 30 |
31 |
32 |
33 |
34 | `; 35 | } 36 | 37 | static get properties() { 38 | return { 39 | isWide: { 40 | type: Boolean, 41 | attribute: 'is-wide', 42 | }, 43 | 44 | name: { 45 | type: String, 46 | reflect: true, 47 | observer: 'nameChanged', 48 | }, 49 | }; 50 | } 51 | 52 | attributeChangedCallback(name, oldval, newval) { 53 | if (name === 'name') { 54 | this.nameChanged(newval); 55 | } 56 | super.attributeChangedCallback(name, oldval, newval); 57 | } 58 | 59 | connectedCallback() { 60 | super.connectedCallback(); 61 | this.name = window.customUI.getName(); 62 | } 63 | 64 | nameChanged(name) { 65 | window.customUI.setName(name); 66 | } 67 | 68 | _backHandler() { 69 | window.history.back(); 70 | const event = new CustomEvent('location-changed'); 71 | this.dispatchEvent(event); 72 | } 73 | } 74 | customElements.define('ha-config-custom-ui', HaConfigCustomUi); 75 | -------------------------------------------------------------------------------- /src/elements/ha-themed-slider.js: -------------------------------------------------------------------------------- 1 | import { html } from '@polymer/polymer/lib/utils/html-tag.js'; 2 | 3 | function loadCustomUI() { 4 | /** 5 | * @extends HTMLElement 6 | */ 7 | class HaThemedSlider extends Polymer.Element { 8 | static get template() { 9 | return html` 10 | 29 | 30 | 37 | 38 | `; 39 | } 40 | 41 | ready() { 42 | super.ready(); 43 | this.disableOffWhenMin = !this._computeAttribute(this.theme, 'off_when_min', !this.disableOffWhenMin); 44 | this.computeEnabledThemedReportWhenNotChanged(this.theme, this.disableReportWhenNotChanged); 45 | } 46 | 47 | connectedCallback() { 48 | super.connectedCallback(); 49 | this.$.slider._keyBindings = this.$.slider._keyBindings || {}; 50 | } 51 | 52 | static get properties() { 53 | return { 54 | min: { 55 | type: Number, 56 | value: 0, 57 | }, 58 | max: { 59 | type: Number, 60 | value: 100, 61 | }, 62 | pin: { 63 | type: Boolean, 64 | value: false, 65 | }, 66 | isOn: { 67 | type: Boolean, 68 | value: false, 69 | }, 70 | disableOffWhenMin: { 71 | type: Boolean, 72 | value: false, 73 | notify: true, 74 | }, 75 | disableReportWhenNotChanged: { 76 | type: Boolean, 77 | value: false, 78 | }, 79 | 80 | theme: Object, 81 | value: { 82 | type: Number, 83 | notify: true, 84 | }, 85 | _themedMin: { 86 | type: Number, 87 | computed: '_computeAttribute(theme, "min", min)', 88 | }, 89 | }; 90 | } 91 | 92 | static get observers() { 93 | return [ 94 | 'computeEnabledThemedReportWhenNotChanged(theme, disableReportWhenNotChanged)', 95 | ]; 96 | } 97 | 98 | computeEnabledThemedReportWhenNotChanged(theme, disableReportWhenNotChanged) { 99 | this._enabledThemedReportWhenNotChanged = this._computeAttribute( 100 | theme, 'report_when_not_changed', !disableReportWhenNotChanged); 101 | } 102 | 103 | _computeAttribute(theme, attr, def) { 104 | if (theme) { 105 | if (attr in theme) { 106 | return theme[attr]; 107 | } 108 | } 109 | return def; 110 | } 111 | 112 | computeClass(theme, isOn, themedMin) { 113 | let result = ''; 114 | if (isOn) { 115 | result += 'is-on '; 116 | } 117 | if (this._computeAttribute(theme, 'off_when_min', !this.disableOffWhenMin) || themedMin === 0) { 118 | // If offWhenMin is enabled don't customize. 119 | return ''; 120 | } 121 | return `${result}disable-off-when-min`; 122 | } 123 | 124 | valueChanged(ev) { 125 | if (!this._enabledThemedReportWhenNotChanged && this.value === ev.target.value) { 126 | ev.stopPropagation(); 127 | return; 128 | } 129 | this.value = ev.target.value; 130 | } 131 | } 132 | customElements.define('ha-themed-slider', HaThemedSlider); 133 | } 134 | if (Polymer && Polymer.Element && customElements.get('home-assistant')) { 135 | loadCustomUI(); 136 | } else { 137 | customElements.whenDefined('home-assistant').then(() => loadCustomUI()); 138 | } 139 | -------------------------------------------------------------------------------- /src/elements/state-card-custom-ui.js: -------------------------------------------------------------------------------- 1 | import applyThemesOnElement from '../../home-assistant-polymer/src/common/dom/apply_themes_on_element.ts'; 2 | import computeStateDomain from '../../home-assistant-polymer/src/common/entity/compute_state_domain.ts'; 3 | import dynamicContentUpdater from '../../home-assistant-polymer/src/common/dom/dynamic_content_updater.ts'; 4 | import stateCardType from '../../home-assistant-polymer/src/common/entity/state_card_type.ts'; 5 | 6 | import '../utils/hooks.js'; 7 | import './state-card-with-slider.js'; 8 | import './state-card-without-slider.js'; 9 | 10 | function loadCustomUI() { 11 | const SHOW_LAST_CHANGED_BLACKLISTED_CARDS = ['configurator']; 12 | const DOMAIN_TO_SLIDER_SUPPORT = { 13 | light: 1, // SUPPORT_BRIGHTNESS 14 | cover: 4, // SUPPORT_SET_POSITION 15 | climate: 1, // SUPPORT_TARGET_TEMPERATURE 16 | }; 17 | const TYPE_TO_CONTROL = { 18 | toggle: 'ha-entity-toggle', 19 | display: '', 20 | cover: 'ha-cover-controls', 21 | }; 22 | 23 | /** 24 | * @extends HTMLElement 25 | */ 26 | class StateCardCustomUi extends Polymer.Element { 27 | static get properties() { 28 | return { 29 | hass: Object, 30 | 31 | inDialog: { 32 | type: Boolean, 33 | value: false, 34 | }, 35 | 36 | stateObj: Object, 37 | }; 38 | } 39 | 40 | static get observers() { 41 | return [ 42 | 'inputChanged(hass, inDialog, stateObj)', 43 | ]; 44 | } 45 | 46 | connectedCallback() { 47 | super.connectedCallback(); 48 | const container = this.parentNode.parentNode; 49 | if (container.tagName === 'DIV' && 50 | (container.classList.contains('state') || container.classList.contains('child-card'))) { 51 | this._container = container; 52 | 53 | // Since this doesn't actually change the background - no need to clear it. 54 | container.style.setProperty( 55 | 'background-color', 'var(--paper-card-background-color, inherit)'); 56 | 57 | // Polyfill 'updateStyles'. 58 | if (!container.updateStyles) { 59 | container.updateStyles = (styles) => { 60 | Object.keys(styles).forEach((key) => { 61 | container.style.setProperty(key, styles[key]); 62 | }); 63 | }; 64 | } 65 | } 66 | this._isAttached = true; 67 | this.inputChanged(this.hass, this.inDialog, this.stateObj); 68 | } 69 | 70 | disconnectedCallback() { 71 | this._isAttached = false; 72 | if (this._container) { 73 | this._container.updateStyles({ display: '', margin: '', padding: '' }); 74 | applyThemesOnElement( 75 | this._container, this.hass.themes || { default_theme: 'default', themes: {} }, 'default'); 76 | this._container = null; 77 | } 78 | super.disconnectedCallback(); 79 | } 80 | 81 | badgeMode(hass, stateObj, domain) { 82 | const states = []; 83 | if (domain === 'group') { 84 | stateObj.attributes.entity_id.forEach((id) => { 85 | const state = hass.states[id]; 86 | if (!state) { 87 | /* eslint-disable no-console */ 88 | console.warn(`Unknown ID ${id} in group ${stateObj.entity_id}`); 89 | /* eslint-enable no-console */ 90 | return; 91 | } 92 | if (!stateObj.attributes.badges_list || 93 | stateObj.attributes.badges_list.includes(state.entity_id)) { 94 | states.push(window.customUI.maybeChangeObject( 95 | this, state, false /* inDialog */, false /* allowHidden */)); 96 | } 97 | }); 98 | } else { 99 | states.push(stateObj); 100 | if (this._container) { 101 | this._container.style.display = 'inline-block'; 102 | const params = { display: 'inline-block' }; 103 | if (this._container.classList.contains('state')) { 104 | params.margin = 'var(--ha-badges-card-margin, 0)'; 105 | } 106 | this.updateStyles(params); 107 | } 108 | } 109 | dynamicContentUpdater( 110 | this, 111 | 'HA-BADGES-CARD', 112 | { hass, states }); 113 | if (this._container) { 114 | this._container.updateStyles({ 115 | width: 'var(--ha-badges-card-width, initial)', 116 | 'text-align': 'var(--ha-badges-card-text-align, initial)', 117 | }); 118 | } 119 | this.lastChild.style.fontSize = '85%'; 120 | 121 | // Since this variable only affects badges mode - no need to clean it up. 122 | this.style.setProperty('--ha-state-label-badge-margin-bottom', '0'); 123 | } 124 | 125 | cleanBadgeStyle() { 126 | if (this._container) { 127 | this._container.updateStyles({ 128 | display: '', 129 | width: '', 130 | 'text-align': '', 131 | }); 132 | } 133 | this.updateStyles({ display: '', margin: '' }); 134 | } 135 | 136 | applyThemes(hass, modifiedObj) { 137 | let themeTarget = this; 138 | let themeName = 'default'; 139 | if (this._container) { 140 | themeTarget = this._container; 141 | } 142 | if (modifiedObj.attributes.theme) { 143 | themeName = modifiedObj.attributes.theme; 144 | } 145 | applyThemesOnElement( 146 | themeTarget, hass.themes || { default_theme: 'default', themes: {} }, themeName); 147 | } 148 | 149 | maybeHideEntity(modifiedObj) { 150 | if (!modifiedObj) { 151 | if (this.lastChild) { 152 | this.removeChild(this.lastChild); 153 | } 154 | if (this._container) { 155 | this._container.updateStyles({ margin: '0', padding: '0' }); 156 | } 157 | return true; 158 | } 159 | if (this._container) { 160 | this._container.updateStyles({ margin: '', padding: '' }); 161 | } 162 | return false; 163 | } 164 | 165 | sliderEligible_(domain, obj, inDialog) { 166 | if (inDialog) return false; 167 | return DOMAIN_TO_SLIDER_SUPPORT[domain] && 168 | (DOMAIN_TO_SLIDER_SUPPORT[domain] & obj.attributes.supported_features) && 169 | obj.attributes.state_card_mode && obj.attributes.state_card_mode !== 'no-slider'; 170 | } 171 | 172 | inputChanged(hass, inDialog, stateObj) { 173 | if (!stateObj || !hass || !this._isAttached) return; 174 | const domain = computeStateDomain(stateObj); 175 | const modifiedObj = window.customUI.maybeChangeObject( 176 | this, stateObj, inDialog, true /* allowHidden */); 177 | 178 | if (this.maybeHideEntity(modifiedObj)) return; 179 | 180 | this.applyThemes(hass, modifiedObj); 181 | 182 | if (!inDialog && modifiedObj.attributes.state_card_mode === 'badges') { 183 | this.badgeMode(hass, modifiedObj, domain); 184 | } else { 185 | this.regularMode_(hass, inDialog, modifiedObj, domain); 186 | } 187 | } 188 | 189 | regularMode_(hass, inDialog, stateObj, domain) { 190 | this.cleanBadgeStyle(); 191 | 192 | const params = { 193 | hass, 194 | stateObj, 195 | inDialog, 196 | }; 197 | const originalStateCardType = stateCardType(hass, stateObj); 198 | let customStateCardType; 199 | const secondaryStateCardType = stateObj.attributes.state_card_custom_ui_secondary; 200 | 201 | if (domain === 'light' && this.sliderEligible_(domain, stateObj, inDialog)) { 202 | Object.assign(params, { 203 | controlElement: 'ha-entity-toggle', 204 | serviceMin: 'turn_off', 205 | serviceMax: 'turn_on', 206 | valueName: 'brightness', 207 | domain, 208 | }); 209 | customStateCardType = 'state-card-with-slider'; 210 | } else if (domain === 'cover' && this.sliderEligible_(domain, stateObj, inDialog)) { 211 | Object.assign(params, { 212 | controlElement: 'ha-cover-controls', 213 | max: 100, 214 | serviceMin: 'close_cover', 215 | serviceMax: 'set_cover_position', 216 | setValueName: 'position', 217 | valueName: 'current_position', 218 | nameOn: 'open', 219 | domain, 220 | }); 221 | customStateCardType = 'state-card-with-slider'; 222 | } else if (domain === 'climate' && this.sliderEligible_(domain, stateObj, inDialog)) { 223 | Object.assign(params, { 224 | controlElement: 'ha-climate-state', 225 | min: stateObj.attributes.min_temp || -100, 226 | max: stateObj.attributes.max_temp || 200, 227 | serviceMin: 'set_temperature', 228 | serviceMax: 'set_temperature', 229 | valueName: 'temperature', 230 | nameOn: '', 231 | domain, 232 | }); 233 | customStateCardType = 'state-card-with-slider'; 234 | } else if (TYPE_TO_CONTROL[originalStateCardType] !== undefined) { 235 | params.controlElement = TYPE_TO_CONTROL[originalStateCardType]; 236 | customStateCardType = 'state-card-without-slider'; 237 | } else if (stateObj.attributes.show_last_changed && 238 | !SHOW_LAST_CHANGED_BLACKLISTED_CARDS.includes(originalStateCardType)) { 239 | params.inDialog = true; 240 | } 241 | if (stateObj.state === 'unavailable') { 242 | params.controlElement = ''; 243 | } 244 | if (stateObj.attributes.control_element !== undefined) { 245 | params.controlElement = stateObj.attributes.control_element; 246 | } 247 | 248 | dynamicContentUpdater( 249 | this, 250 | (secondaryStateCardType || customStateCardType || `STATE-CARD-${originalStateCardType}`).toUpperCase(), 251 | params); 252 | } 253 | } 254 | customElements.define('state-card-custom-ui', StateCardCustomUi); 255 | } 256 | if (Polymer && Polymer.Element && customElements.get('home-assistant')) { 257 | loadCustomUI(); 258 | } else { 259 | customElements.whenDefined('home-assistant').then(() => loadCustomUI()); 260 | } 261 | -------------------------------------------------------------------------------- /src/elements/state-card-with-slider.js: -------------------------------------------------------------------------------- 1 | import { html } from '@polymer/polymer/lib/utils/html-tag.js'; 2 | import CuiBaseElement from './cui-base-element.js'; 3 | import './dynamic-with-extra.js'; 4 | import './ha-themed-slider.js'; 5 | 6 | function loadCustomUI() { 7 | /** 8 | * @extends HTMLElement 9 | */ 10 | class StateCardWithSlider extends CuiBaseElement(Polymer.Element) { 11 | static get template() { 12 | return html` 13 | 14 | 54 | 55 |
56 |
57 | 64 | 67 | 68 | 71 |
72 | 90 |
91 | `; 92 | } 93 | 94 | static get properties() { 95 | return { 96 | domain: String, 97 | serviceMin: String, 98 | serviceMax: String, 99 | valueName: String, 100 | setValueName: String, 101 | nameOn: { type: String, value: 'on' }, 102 | min: { type: Number, value: 0 }, 103 | max: { type: Number, value: 255 }, 104 | 105 | sliderValue: { 106 | type: Number, 107 | value: 0, 108 | }, 109 | disableOffWhenMin: Boolean, 110 | mode: String, 111 | stretchSlider: { 112 | type: Boolean, 113 | value: false, 114 | }, 115 | breakSlider: { 116 | type: Boolean, 117 | value: false, 118 | }, 119 | hideSlider: { 120 | type: Boolean, 121 | value: false, 122 | }, 123 | lineTooLong: { 124 | type: Boolean, 125 | value: false, 126 | }, 127 | minLineBreak: Number, 128 | maxLineBreak: Number, 129 | showSlider: { 130 | type: Number, 131 | computed: '_showSlider(inDialog, stateObj, hideSlider)', 132 | }, 133 | }; 134 | } 135 | 136 | ready() { 137 | super.ready(); 138 | this._onIronResize = this._onIronResize.bind(this); 139 | } 140 | 141 | connectedCallback() { 142 | super.connectedCallback(); 143 | this._isConnected = true; 144 | window.addEventListener('resize', this._onIronResize); 145 | this._waitForLayout(); 146 | } 147 | 148 | disconnectedCallback() { 149 | window.removeEventListener('resize', this._onIronResize); 150 | this._isConnected = false; 151 | super.disconnectedCallback(); 152 | } 153 | 154 | static get observers() { 155 | return [ 156 | 'stateObjChanged(stateObj, nameOn, valueName)', 157 | ]; 158 | } 159 | 160 | _waitForLayout() { 161 | if (!this._isConnected) return; 162 | this._setMode(); 163 | if (this._frameId) return; 164 | this.readyToCompute = false; 165 | this._frameId = window.requestAnimationFrame(() => { 166 | this._frameId = null; 167 | this.readyToCompute = true; 168 | this._onIronResize(); 169 | }); 170 | } 171 | 172 | _setMode() { 173 | const obj = { 174 | hideSlider: this.mode === 'hide-slider' && this.lineTooLong, 175 | breakSlider: 176 | (this.mode === 'break-slider' || this.mode === 'hide-slider') && 177 | this.lineTooLong, 178 | }; 179 | if (!this.showSlider) { 180 | obj.breakSlider = true; 181 | } 182 | this.setProperties(obj); 183 | } 184 | 185 | _onIronResize() { 186 | if (!this.readyToCompute) return; 187 | if (this.mode === 'no-slider') { 188 | this.setProperties({ 189 | hideSlider: true, 190 | breakSlider: true, 191 | }); 192 | return; 193 | } 194 | const prevBreakSlider = this.breakSlider; 195 | const prevHideSlider = this.hideSlider; 196 | this.setProperties({ 197 | lineTooLong: false, 198 | hideSlider: false, 199 | breakSlider: false, 200 | }); 201 | const { container } = this.$; 202 | const containerWidth = container.clientWidth; 203 | if (containerWidth === 0) return; 204 | if (containerWidth <= this.minLineBreak) { 205 | this.lineTooLong = true; 206 | } else if (containerWidth >= this.maxLineBreak) { 207 | this.lineTooLong = false; 208 | } else { 209 | if (prevHideSlider && this.mode === 'hide-slider') { 210 | // We need to unhide the slider in order to recalculate height. 211 | this._waitForLayout(); 212 | return; 213 | } 214 | const containerHeight = container.clientHeight; 215 | const stateHeight = this.root.querySelector('.state-info').clientHeight; 216 | this.lineTooLong = containerHeight > stateHeight * 1.5; 217 | if (this.lineTooLong) { 218 | this.minLineBreak = containerWidth; 219 | } else if (!prevBreakSlider) { 220 | this.maxLineBreak = containerWidth; 221 | } 222 | } 223 | this._setMode(); 224 | } 225 | 226 | _computeWrapClass(mode, stretchSlider, lineTooLong, inDialog) { 227 | if (inDialog) { 228 | return ''; 229 | } 230 | if (mode === 'single-line') { 231 | return 'nowrap'; 232 | } 233 | if (stretchSlider && lineTooLong) { 234 | return 'stretch wrap'; 235 | } 236 | return 'wrap'; 237 | } 238 | 239 | _showSlider(inDialog, stateObj, hideSlider) { 240 | if (inDialog || hideSlider) { 241 | return false; 242 | } 243 | return true; 244 | } 245 | 246 | sliderChanged(ev) { 247 | const value = parseInt(ev.target.value, 10); 248 | const param = { entity_id: this.stateObj.entity_id }; 249 | if (Number.isNaN(value)) return; 250 | let target = this.root.querySelector('#slider'); 251 | if (ev.target !== target) { 252 | // No Shadow DOM - we have access to original target. 253 | ({ target } = ev); 254 | } else if (ev.path) { 255 | [target] = ev.path; 256 | } else if (ev.composedPath) { 257 | [target] = ev.composedPath(); 258 | } 259 | if (value === 0 || (value <= target.min && !this.disableOffWhenMin)) { 260 | this.hass.callService(this.domain, this.serviceMin, param); 261 | } else { 262 | param[this.setValueName || this.valueName] = value; 263 | this.hass.callService(this.domain, this.serviceMax, param); 264 | } 265 | } 266 | 267 | stateObjChanged(stateObj, nameOn, valueName) { 268 | const obj = { 269 | sliderValue: this.isOn(stateObj, nameOn) ? stateObj.attributes[valueName] : 0, 270 | }; 271 | if (stateObj) { 272 | Object.assign(obj, { 273 | minLineBreak: 0, 274 | maxLineBreak: 999, 275 | hideSlider: false, 276 | breakSlider: false, 277 | lineTooLong: false, 278 | mode: stateObj.attributes.state_card_mode, 279 | stretchSlider: !!stateObj.attributes.stretch_slider, 280 | }); 281 | } 282 | this.setProperties(obj); 283 | if (stateObj) { 284 | this._waitForLayout(); 285 | } 286 | } 287 | 288 | isOn(stateObj, nameOn) { 289 | return stateObj && (!nameOn || stateObj.state === nameOn); 290 | } 291 | 292 | stopPropagation(ev) { 293 | ev.stopPropagation(); 294 | } 295 | } 296 | customElements.define('state-card-with-slider', StateCardWithSlider); 297 | } 298 | if (Polymer && Polymer.Element && customElements.get('home-assistant')) { 299 | loadCustomUI(); 300 | } else { 301 | customElements.whenDefined('home-assistant').then(() => loadCustomUI()); 302 | } 303 | -------------------------------------------------------------------------------- /src/elements/state-card-without-slider.js: -------------------------------------------------------------------------------- 1 | import { html } from '@polymer/polymer/lib/utils/html-tag.js'; 2 | import CuiBaseElement from './cui-base-element.js'; 3 | import './dynamic-with-extra.js'; 4 | 5 | function loadCustomUI() { 6 | /** 7 | * @extends HTMLElement 8 | */ 9 | class StateCardWithoutSlider extends CuiBaseElement(Polymer.Element) { 10 | static get template() { 11 | return html` 12 | 13 | 18 | 19 |
20 | 26 | 29 | 30 | 35 | 36 |
37 | `; 38 | } 39 | } 40 | customElements.define('state-card-without-slider', StateCardWithoutSlider); 41 | } 42 | if (Polymer && Polymer.Element && customElements.get('home-assistant')) { 43 | loadCustomUI(); 44 | } else { 45 | customElements.whenDefined('home-assistant').then(() => loadCustomUI()); 46 | } 47 | -------------------------------------------------------------------------------- /src/entrypoints/scripts.js: -------------------------------------------------------------------------------- 1 | import '../elements/state-card-custom-ui.js'; 2 | -------------------------------------------------------------------------------- /src/mixins/events-mixin.js: -------------------------------------------------------------------------------- 1 | // Polymer legacy event helpers used courtesy of the Polymer project. 2 | // 3 | // Copyright (c) 2017 The Polymer Authors. All rights reserved. 4 | // 5 | // Redistribution and use in source and binary forms, with or without 6 | // modification, are permitted provided that the following conditions are 7 | // met: 8 | // 9 | // * Redistributions of source code must retain the above copyright 10 | // notice, this list of conditions and the following disclaimer. 11 | // * Redistributions in binary form must reproduce the above 12 | // copyright notice, this list of conditions and the following disclaimer 13 | // in the documentation and/or other materials provided with the 14 | // distribution. 15 | // * Neither the name of Google Inc. nor the names of its 16 | // contributors may be used to endorse or promote products derived from 17 | // this software without specific prior written permission. 18 | // 19 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | export default (superClass) => { 32 | /** 33 | * @extends HTMLElement 34 | */ 35 | class EventsMixin extends superClass { 36 | /** 37 | * Dispatches a custom event with an optional detail value. 38 | * 39 | * @param {string} type Name of event type. 40 | * @param {*=} detail Detail value containing event-specific 41 | * payload. 42 | * @param {{ bubbles: (boolean|undefined), 43 | cancelable: (boolean|undefined), 44 | composed: (boolean|undefined) }=} 45 | * options Object specifying options. These may include: 46 | * `bubbles` (boolean, defaults to `true`), 47 | * `cancelable` (boolean, defaults to false), and 48 | * `node` on which to fire the event (HTMLElement, defaults to `this`). 49 | * @return {Event} The new event that was fired. 50 | */ 51 | fire(type, detail = {}, options = {}) { 52 | const event = new Event(type, { 53 | bubbles: options.bubbles === undefined ? true : options.bubbles, 54 | cancelable: Boolean(options.cancelable), 55 | composed: options.composed === undefined ? true : options.composed, 56 | }); 57 | event.detail = detail; 58 | const node = options.node || this; 59 | node.dispatchEvent(event); 60 | return event; 61 | } 62 | } 63 | return EventsMixin; 64 | }; 65 | -------------------------------------------------------------------------------- /src/state-card-custom-ui.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/utils/hass-attribute-util.js: -------------------------------------------------------------------------------- 1 | import hassAttributesUtil from '../../home-assistant-polymer/src/util/hass-attributes-util.js'; 2 | 3 | window.hassAttributeUtil = window.hassAttributeUtil || {}; 4 | const SUPPORTED_SLIDER_MODES = [ 5 | 'single-line', 'break-slider', 'break-slider-toggle', 'hide-slider', 'no-slider', 6 | ]; 7 | 8 | const customUiAttributes = { 9 | group: undefined, 10 | device: undefined, 11 | templates: undefined, 12 | state: undefined, 13 | _stateDisplay: undefined, 14 | control_element: { type: 'string' }, 15 | state_card_mode: { 16 | type: 'array', 17 | options: { 18 | light: SUPPORTED_SLIDER_MODES.concat('badges'), 19 | cover: SUPPORTED_SLIDER_MODES.concat('badges'), 20 | climate: SUPPORTED_SLIDER_MODES.concat('badges'), 21 | '*': ['badges'], 22 | }, 23 | }, 24 | state_card_custom_ui_secondary: { type: 'string' }, 25 | badges_list: { type: 'json' }, 26 | show_last_changed: { type: 'boolean' }, 27 | hide_control: { type: 'boolean' }, 28 | extra_data_template: { type: 'string' }, 29 | extra_badge: { type: 'json' }, 30 | stretch_slider: { type: 'boolean' }, 31 | slider_theme: { type: 'json' }, 32 | theme: { type: 'string' }, 33 | confirm_controls: { type: 'boolean' }, 34 | confirm_controls_show_lock: { type: 'boolean' }, 35 | hide_in_default_view: { type: 'boolean' }, 36 | icon_color: { type: 'string' }, 37 | }; 38 | window.hassAttributeUtil.LOGIC_STATE_ATTRIBUTES = hassAttributesUtil.LOGIC_STATE_ATTRIBUTES; 39 | window.hassAttributeUtil.UNKNOWN_TYPE = hassAttributesUtil.UNKNOWN_TYPE; 40 | Object.assign(window.hassAttributeUtil.LOGIC_STATE_ATTRIBUTES, customUiAttributes); 41 | -------------------------------------------------------------------------------- /src/utils/hooks.js: -------------------------------------------------------------------------------- 1 | import applyThemesOnElement from '../../home-assistant-polymer/src/common/dom/apply_themes_on_element.ts'; 2 | import computeStateDomain from '../../home-assistant-polymer/src/common/entity/compute_state_domain.ts'; 3 | import getViewEntities from '../../home-assistant-polymer/src/common/entity/get_view_entities.ts'; 4 | 5 | import '../elements/ha-config-custom-ui.js'; 6 | import VERSION from './version.js'; 7 | import './hass-attribute-util.js'; 8 | 9 | window.customUI = window.customUI || { 10 | SUPPORTED_SLIDER_MODES: [ 11 | 'single-line', 'break-slider', 'break-slider-toggle', 'hide-slider', 'no-slider', 12 | ], 13 | 14 | domHost(elem) { 15 | if (elem === document) return null; 16 | const root = elem.getRootNode(); 17 | return (root instanceof DocumentFragment) ? /** @type {ShadowRoot} */ (root).host : root; 18 | }, 19 | 20 | lightOrShadow(elem, selector) { 21 | return elem.shadowRoot ? 22 | elem.shadowRoot.querySelector(selector) : 23 | elem.querySelector(selector); 24 | }, 25 | 26 | getElementHierarchy(root, hierarchy) { 27 | if (root === null) return null; 28 | const elem = hierarchy.shift(); 29 | if (elem) { 30 | return window.customUI.getElementHierarchy( 31 | window.customUI.lightOrShadow(root, elem), hierarchy); 32 | } 33 | return root; 34 | }, 35 | 36 | getContext(elem) { 37 | if (elem._context === undefined) { 38 | elem._context = []; 39 | for (let element = (elem.tagName === 'HA-ENTITIES-CARD' ? window.customUI.domHost(elem) : elem); 40 | element; element = window.customUI.domHost(element)) { 41 | switch (element.tagName) { 42 | case 'HA-ENTITIES-CARD': 43 | if (element.groupEntity) { 44 | elem._context.push(element.groupEntity.entity_id); 45 | } else if (element.groupEntity === false && element.states && element.states.length) { 46 | elem._context.push(`group.${computeStateDomain(element.states[0])}`); 47 | } 48 | break; 49 | case 'MORE-INFO-GROUP': 50 | case 'STATE-CARD-CONTENT': 51 | if (element.stateObj) { 52 | elem._context.push(element.stateObj.entity_id); 53 | } 54 | break; 55 | case 'HA-CARDS': 56 | elem._context.push(element.getAttribute('data-view') || 'default_view'); 57 | break; 58 | // no default 59 | } 60 | } 61 | elem._context.reverse(); 62 | } 63 | return elem._context; 64 | }, 65 | 66 | findMatch(key, options) { 67 | if (!options) return null; 68 | if (options[key]) return key; 69 | return Object.keys(options).find(option => key.match(`^${option}$`)); 70 | }, 71 | 72 | maybeChangeObjectByDevice(stateObj) { 73 | const name = window.customUI.getName(); 74 | if (!name) return stateObj; 75 | const match = this.findMatch(name, stateObj.attributes.device); 76 | if (!match) return stateObj; 77 | const attributes = Object.assign({}, stateObj.attributes.device[match]); 78 | 79 | if (!Object.keys(attributes).length) return stateObj; 80 | return window.customUI.applyAttributes(stateObj, attributes); 81 | }, 82 | 83 | maybeChangeObjectByGroup(elem, stateObj) { 84 | const context = window.customUI.getContext(elem); 85 | if (!context) return stateObj; 86 | 87 | if (!stateObj.attributes.group) { 88 | return stateObj; 89 | } 90 | const attributes = {}; 91 | context.forEach((c) => { 92 | const match = this.findMatch(c, stateObj.attributes.group); 93 | if (stateObj.attributes.group[match]) { 94 | Object.assign(attributes, stateObj.attributes.group[match]); 95 | } 96 | }); 97 | 98 | if (!Object.keys(attributes).length) return stateObj; 99 | 100 | return window.customUI.applyAttributes(stateObj, attributes); 101 | }, 102 | 103 | _setKeep(obj, value) { 104 | if (obj._cui_keep === undefined) { 105 | obj._cui_keep = value; 106 | } else { 107 | obj._cui_keep = obj._cui_keep && value; 108 | } 109 | }, 110 | 111 | maybeApplyTemplateAttributes(hass, states, stateObj, attributes) { 112 | if (!attributes.templates) { 113 | window.customUI._setKeep(stateObj, true); 114 | return stateObj; 115 | } 116 | const newAttributes = {}; 117 | let hasGlobal = false; 118 | let hasChanges = false; 119 | Object.keys(attributes.templates).forEach((key) => { 120 | const template = attributes.templates[key]; 121 | if (template.match(/\b(entities|hass)\b/)) { 122 | hasGlobal = true; 123 | } 124 | const value = window.customUI.computeTemplate( 125 | template, hass, states, stateObj, attributes, 126 | (stateObj.untemplated_attributes && stateObj.untemplated_attributes[key]) || 127 | attributes[key], 128 | stateObj.untemplated_state || stateObj.state); 129 | // In case of null don't set the value. 130 | if (value === null) return; 131 | newAttributes[key] = value; 132 | if (key === 'state') { 133 | if (value !== stateObj.state) { 134 | hasChanges = true; 135 | } 136 | } else if (key === '_stateDisplay') { 137 | if (value !== stateObj._stateDisplay) { 138 | hasChanges = true; 139 | } 140 | } else if (value !== attributes[key]) { 141 | hasChanges = true; 142 | } 143 | }); 144 | window.customUI._setKeep(stateObj, !hasGlobal); 145 | if (!hasChanges) { 146 | return stateObj; 147 | } 148 | if (stateObj.attributes === attributes) { 149 | // We are operating on real attributes. Replace them. 150 | const result = window.customUI.applyAttributes(stateObj, newAttributes); 151 | if (Object.prototype.hasOwnProperty.call(newAttributes, 'state')) { 152 | if (newAttributes.state !== null) { 153 | result.state = String(newAttributes.state); 154 | result.untemplated_state = stateObj.state; 155 | } 156 | } 157 | if (Object.prototype.hasOwnProperty.call(newAttributes, '_stateDisplay')) { 158 | result._stateDisplay = newAttributes._stateDisplay; 159 | result.untemplated_stateDisplay = stateObj._stateDisplay; 160 | } 161 | window.customUI._setKeep(result, !hasGlobal); 162 | return result; 163 | } 164 | // Operating on context-aware attributes. Return shallow copy of object. 165 | return Object.assign({}, stateObj); 166 | }, 167 | 168 | maybeApplyTemplates(hass, states, stateObj) { 169 | const newResult = window.customUI.maybeApplyTemplateAttributes( 170 | hass, states, stateObj, stateObj.attributes); 171 | let hasChanges = (newResult !== stateObj); 172 | 173 | function checkAttributes(obj) { 174 | if (!obj) return; 175 | Object.values(obj).forEach((attributes) => { 176 | const result = window.customUI.maybeApplyTemplateAttributes( 177 | hass, states, newResult, attributes); 178 | hasChanges |= (result !== newResult); 179 | }); 180 | checkAttributes(obj.device); 181 | checkAttributes(obj.group); 182 | } 183 | 184 | checkAttributes(stateObj.attributes.device); 185 | checkAttributes(stateObj.attributes.group); 186 | if (newResult !== stateObj) return newResult; 187 | if (hasChanges) { 188 | return Object.assign({}, stateObj); 189 | } 190 | return stateObj; 191 | }, 192 | 193 | applyAttributes(stateObj, attributes) { 194 | return { 195 | entity_id: stateObj.entity_id, 196 | state: stateObj.state, 197 | attributes: Object.assign({}, stateObj.attributes, attributes), 198 | untemplated_attributes: stateObj.attributes, 199 | last_changed: stateObj.last_changed, 200 | }; 201 | }, 202 | 203 | maybeChangeObject(elem, stateObj, inDialog, allowHidden) { 204 | if (inDialog) return stateObj; 205 | let obj = window.customUI.maybeChangeObjectByDevice(stateObj); 206 | obj = window.customUI.maybeChangeObjectByGroup(elem, obj); 207 | obj = window.customUI.maybeApplyTemplateAttributes( 208 | elem.hass, elem.hass.states, obj, obj.attributes); 209 | 210 | if (obj !== stateObj && obj.attributes.hidden && allowHidden) { 211 | return null; 212 | } 213 | return obj; 214 | }, 215 | 216 | fixGroupTitles() { 217 | const homeAssistantMain = window.customUI.getElementHierarchy(document, [ 218 | 'home-assistant', 219 | 'home-assistant-main']); 220 | if (homeAssistantMain === null) { 221 | // DOM not ready. Wait 1 second. 222 | window.setTimeout(window.customUI.fixGroupTitles, 1000); 223 | return; 224 | } 225 | 226 | const haCards = window.customUI.getElementHierarchy(homeAssistantMain, [ 227 | 'partial-cards', 228 | 'ha-cards[view-visible]']); 229 | if (haCards === null) return; 230 | const main = window.customUI.lightOrShadow(haCards, '.main') || haCards.$.main; 231 | const cards = main.querySelectorAll('ha-entities-card'); 232 | cards.forEach((card) => { 233 | if (card.groupEntity) { 234 | const obj = window.customUI.maybeChangeObject( 235 | card, 236 | card.groupEntity, 237 | false /* inDialog */, 238 | false /* allowHidden */); 239 | if (obj !== card.groupEntity && obj.attributes.friendly_name) { 240 | const nameElem = window.customUI.lightOrShadow(card, '.name'); 241 | nameElem.textContent = obj.attributes.friendly_name; 242 | } 243 | } 244 | }); 245 | }, 246 | 247 | controlColumns(columns) { 248 | const partialCards = window.customUI.getElementHierarchy(document, [ 249 | 'home-assistant', 250 | 'home-assistant-main', 251 | 'partial-cards']); 252 | if (partialCards === null) { 253 | // DOM not ready. Wait 1 second. 254 | window.setTimeout( 255 | window.customUI.controlColumns.bind(null, columns), 256 | 1000); 257 | return; 258 | } 259 | // Function renamed from handleWindowChange to _updateColumns on 3.7.18 260 | const f = partialCards.handleWindowChange || partialCards._updateColumns; 261 | partialCards.mqls.forEach((mql) => { 262 | mql.removeListener(f); 263 | }); 264 | partialCards.mqls = columns.map((width) => { 265 | const mql = window.matchMedia(`(min-width: ${width}px)`); 266 | mql.addListener(f); 267 | return mql; 268 | }); 269 | f(); 270 | }, 271 | 272 | useCustomizer() { 273 | const main = window.customUI.lightOrShadow(document, 'home-assistant'); 274 | const customizer = main.hass.states['customizer.customizer']; 275 | if (!customizer) return; 276 | if (customizer.attributes.columns) { 277 | window.customUI.controlColumns(customizer.attributes.columns); 278 | } 279 | if (customizer.attributes.hide_attributes) { 280 | if (window.hassAttributeUtil && window.hassAttributeUtil.LOGIC_STATE_ATTRIBUTES) { 281 | customizer.attributes.hide_attributes.forEach((attr) => { 282 | if (!Object.prototype.hasOwnProperty.call( 283 | window.hassAttributeUtil.LOGIC_STATE_ATTRIBUTES, attr)) { 284 | window.hassAttributeUtil.LOGIC_STATE_ATTRIBUTES[attr] = undefined; 285 | } 286 | }); 287 | } 288 | } 289 | }, 290 | 291 | updateConfigPanel() { 292 | if (!window.location.pathname.startsWith('/config')) return; 293 | const haPanelConfig = window.customUI.getElementHierarchy(document, [ 294 | 'home-assistant', 295 | 'home-assistant-main', 296 | 'partial-panel-resolver', 297 | 'ha-panel-config']); 298 | if (!haPanelConfig) { 299 | // DOM not ready. Wait 100ms. 300 | window.setTimeout(window.customUI.updateConfigPanel, 100); 301 | return; 302 | } 303 | const haConfigNavigation = window.customUI.getElementHierarchy(haPanelConfig, [ 304 | 'ha-config-dashboard', 305 | 'ha-config-navigation']); 306 | if (haConfigNavigation) { 307 | // HaConfigNavigation started using localize on 21.01.2018 308 | if (haConfigNavigation.localize && !haConfigNavigation.cuiPatch) { 309 | haConfigNavigation.cuiPatch = true; 310 | haConfigNavigation._originalComputeLoaded = haConfigNavigation._computeLoaded; 311 | haConfigNavigation._originalComputeCaption = haConfigNavigation._computeCaption; 312 | haConfigNavigation._originalComputeDescription = haConfigNavigation._computeDescription; 313 | haConfigNavigation._computeLoaded = (hass, page) => 314 | page === 'customui' || haConfigNavigation._originalComputeLoaded(hass, page); 315 | haConfigNavigation._computeCaption = (page, localize) => 316 | (page === 'customui' ? 'Custom UI' : haConfigNavigation._originalComputeCaption(page, localize)); 317 | haConfigNavigation._computeDescription = (page, localize) => 318 | (page === 'customui' ? 'SetUI tweaks' : haConfigNavigation._originalComputeDescription(page, localize)); 319 | } 320 | if (!haConfigNavigation.pages.some(conf => conf === 'customui' || conf.domain === 'customui')) { 321 | haConfigNavigation.push('pages', haConfigNavigation.localize ? 'customui' : { 322 | domain: 'customui', 323 | caption: 'Custom UI', 324 | description: 'Set UI tweaks.', 325 | loaded: true, 326 | }); 327 | } 328 | } 329 | const getHaConfigCustomUi = () => { 330 | const haConfigCustomUi = document.createElement('ha-config-custom-ui'); 331 | haConfigCustomUi.isWide = haPanelConfig.isWide; 332 | haConfigCustomUi.setAttribute('page-name', 'customui'); 333 | return haConfigCustomUi; 334 | }; 335 | 336 | const ironPages = window.customUI.lightOrShadow(haPanelConfig, 'iron-pages'); 337 | if (ironPages) { 338 | if (ironPages.lastElementChild.tagName !== 'HA-CONFIG-CUSTOM-UI') { 339 | const haConfigCustomUi = getHaConfigCustomUi(); 340 | ironPages.appendChild(haConfigCustomUi); 341 | ironPages.addEventListener('iron-items-changed', () => { 342 | if (window.location.pathname.startsWith('/config/customui')) { 343 | ironPages.select('customui'); 344 | } 345 | }); 346 | } 347 | } else if (haPanelConfig.shadowRoot) { 348 | const root = haPanelConfig.shadowRoot || haPanelConfig; 349 | if (root.lastElementChild.tagName !== 'HA-CONFIG-CUSTOM-UI') { 350 | const haConfigCustomUi = getHaConfigCustomUi(); 351 | root.appendChild(haConfigCustomUi); 352 | } 353 | const visible = window.location.pathname.startsWith('/config/customui'); 354 | root.lastElementChild.style.display = visible ? '' : 'none'; 355 | } else if (haPanelConfig.routerOptions && haPanelConfig.routerOptions.routes) { 356 | if (!haPanelConfig.routerOptions.routes.customui) { 357 | haPanelConfig.routerOptions.routes.customui = { 358 | tag: 'ha-config-custom-ui', 359 | load: () => Promise.resolve(), 360 | }; 361 | // CustomUI panel is the entrypoint, so we need to reload the page. 362 | if (window.location.pathname.startsWith('/config/customui')) { 363 | haPanelConfig.update(new Map([['route', undefined]])); 364 | } 365 | } 366 | } 367 | }, 368 | 369 | installStatesHook() { 370 | customElements.whenDefined('home-assistant').then(() => { 371 | const homeAssistant = customElements.get('home-assistant'); 372 | if (!homeAssistant || !homeAssistant.prototype._updateHass) return; 373 | const originalUpdate = homeAssistant.prototype._updateHass; 374 | homeAssistant.prototype._updateHass = function update(obj) { 375 | // Use named function to preserve 'this'. 376 | const { hass } = this; 377 | if (obj.states) { 378 | Object.keys(obj.states).forEach((key) => { 379 | const entity = obj.states[key]; 380 | if (entity._cui_keep) return; 381 | const newEntity = window.customUI.maybeApplyTemplates(hass, obj.states, entity); 382 | if (hass.states && entity !== hass.states[key]) { 383 | // New state arrived. Put modified state in. 384 | obj.states[key] = newEntity; 385 | } else if (entity !== newEntity) { 386 | // It's the same state but contents changed due to other state changes. 387 | obj.states[key] = newEntity; 388 | } 389 | }); 390 | } 391 | originalUpdate.call(this, obj); 392 | if (obj.themes && hass._themeWaiters) { 393 | hass._themeWaiters.forEach(waiter => waiter.stateChanged(waiter.state)); 394 | hass._themeWaiters = undefined; 395 | } 396 | }; 397 | const main = window.customUI.lightOrShadow(document, 'home-assistant'); 398 | if (main.hass && main.hass.states) { 399 | main._updateHass({ states: main.hass.states }); 400 | } 401 | }); 402 | }, 403 | 404 | installPartialCards() { 405 | customElements.whenDefined('partial-cards').then(() => { 406 | const partialCards = customElements.get('partial-cards'); 407 | if (!partialCards || !partialCards.prototype._defaultViewFilter) return; 408 | partialCards.prototype._defaultViewFilter = (hass, entityId) => { 409 | if (hass.states[entityId].attributes.hidden) return false; 410 | const excludes = {}; 411 | Object.values(hass.states).forEach((entity) => { 412 | if (entity.attributes && entity.attributes.hide_in_default_view) { 413 | const excludeEntityId = entity.entity_id; 414 | if (excludes[excludeEntityId]) return; 415 | excludes[excludeEntityId] = entity; 416 | if (entity.attributes.view) { 417 | const viewEntities = getViewEntities(hass.states, entity); 418 | Object.keys(viewEntities) 419 | .filter( 420 | id => viewEntities[id].attributes.hide_in_default_view !== false) 421 | .forEach((id) => { 422 | excludes[id] = viewEntities[id]; 423 | }); 424 | } 425 | } 426 | }); 427 | return !excludes[entityId]; 428 | }; 429 | }); 430 | }, 431 | 432 | // Allows changing the 'Execute' / 'Activate' text on script/scene cards. 433 | installActionName(elementName) { 434 | customElements.whenDefined(elementName).then(() => { 435 | const klass = customElements.get(elementName); 436 | if (!klass || !klass.prototype) return; 437 | Object.defineProperty(klass.prototype, 'localize', { 438 | get() { 439 | function customLocalize(v) { 440 | if (this.stateObj && this.stateObj.attributes && 441 | this.stateObj.attributes.action_name) { 442 | return this.stateObj.attributes.action_name; 443 | } 444 | return this.__data.localize(v); 445 | } 446 | return customLocalize; 447 | }, 448 | set() {}, 449 | }); 450 | }); 451 | }, 452 | 453 | // Allows theming "regular" top badges. 454 | installHaStateLabelBadge() { 455 | customElements.whenDefined('ha-state-label-badge').then(() => { 456 | const haStateLabelBadge = customElements.get('ha-state-label-badge'); 457 | if (!haStateLabelBadge || !haStateLabelBadge.prototype.stateChanged) return; 458 | // Use named function to preserve 'this'. 459 | haStateLabelBadge.prototype.stateChanged = function update(stateObj) { 460 | // TODO: Call window.customUI.maybeChangeObject 461 | if (stateObj.attributes.theme) { 462 | if (this.hass.themes === null) { 463 | this.hass._themeWaiters = this.hass._themeWaiters || []; 464 | this.hass._themeWaiters.push(this); 465 | } else { 466 | applyThemesOnElement( 467 | this, 468 | this.hass.themes || { default_theme: 'default', themes: {} }, 469 | stateObj.attributes.theme || 'default'); 470 | } 471 | } 472 | this.updateStyles(); 473 | if (this.startInterval) { 474 | // Added on 19.1.2018 475 | this.startInterval(stateObj); 476 | } 477 | }; 478 | }); 479 | }, 480 | 481 | installStateBadge() { 482 | customElements.whenDefined('state-badge').then(() => { 483 | const stateBadge = customElements.get('state-badge'); 484 | if (!stateBadge) return; 485 | if (stateBadge.prototype._updateIconAppearance) { 486 | const originalUpdateIconAppearance = stateBadge.prototype._updateIconAppearance; 487 | // Use named function to preserve 'this'. 488 | stateBadge.prototype._updateIconAppearance = function customUpdateIconAppearance(stateObj) { 489 | if (stateObj.attributes.icon_color && !stateObj.attributes.entity_picture) { 490 | this.style.backgroundImage = ''; 491 | Object.assign(this.$.icon.style, { 492 | color: stateObj.attributes.icon_color, 493 | filter: '', 494 | }); 495 | } else { 496 | originalUpdateIconAppearance.call(this, stateObj); 497 | } 498 | }; 499 | } else if (stateBadge.prototype.updated) { 500 | const originalUpdated = stateBadge.prototype.updated; 501 | // Use named function to preserve 'this'. 502 | stateBadge.prototype.updated = function customUpdated(changedProps) { 503 | if (!changedProps.has('stateObj')) return; 504 | const { stateObj } = this; 505 | if (stateObj.attributes.icon_color && !stateObj.attributes.entity_picture) { 506 | this.style.backgroundImage = ''; 507 | Object.assign(this._icon.style, { 508 | color: stateObj.attributes.icon_color, 509 | filter: '', 510 | }); 511 | } else { 512 | originalUpdated.call(this, changedProps); 513 | } 514 | }; 515 | } 516 | }); 517 | }, 518 | 519 | installHaAttributes() { 520 | customElements.whenDefined('ha-attributes').then(() => { 521 | const haAttributes = customElements.get('ha-attributes'); 522 | if (!haAttributes || !haAttributes.prototype.computeFiltersArray || 523 | !window.hassAttributeUtil) return; 524 | // Use named function to preserve 'this'. 525 | haAttributes.prototype.computeFiltersArray = 526 | function customComputeFiltersArray(extraFilters) { 527 | return Object.keys(window.hassAttributeUtil.LOGIC_STATE_ATTRIBUTES).concat( 528 | extraFilters ? extraFilters.split(',') : []); 529 | }; 530 | }); 531 | }, 532 | 533 | installHaFormCustomize() { 534 | if (!window.location.pathname.startsWith('/config')) return; 535 | customElements.whenDefined('ha-form-customize').then(() => { 536 | const haFormCustomize = customElements.get('ha-form-customize'); 537 | if (!haFormCustomize) { 538 | // DOM not ready. Wait 100ms. 539 | window.setTimeout(window.customUI.installHaFormCustomize, 100); 540 | return; 541 | } 542 | if (window.customUI.haFormCustomizeInitDone) return; 543 | window.customUI.haFormCustomizeInitDone = true; 544 | 545 | if (!window.hassAttributeUtil) return; 546 | if (haFormCustomize.prototype._computeSingleAttribute) { 547 | // Use named function to preserve 'this'. 548 | haFormCustomize.prototype._computeSingleAttribute = 549 | function customComputeSingleAttribute(key, value, secondary) { 550 | const config = window.hassAttributeUtil.LOGIC_STATE_ATTRIBUTES[key] 551 | || { type: window.hassAttributeUtil.UNKNOWN_TYPE }; 552 | return this._initOpenObject(key, config.type === 'json' ? JSON.stringify(value) : value, secondary, config); 553 | }; 554 | } 555 | if (haFormCustomize.prototype.getNewAttributesOptions) { 556 | // Use named function to preserve 'this'. 557 | haFormCustomize.prototype.getNewAttributesOptions = 558 | function customgetNewAttributesOptions( 559 | localAttributes, globalAttributes, existingAttributes, newAttributes) { 560 | const knownKeys = 561 | Object.keys(window.hassAttributeUtil.LOGIC_STATE_ATTRIBUTES) 562 | .filter((key) => { 563 | const conf = window.hassAttributeUtil.LOGIC_STATE_ATTRIBUTES[key]; 564 | return conf && (!conf.domains || !this.entity || 565 | conf.domains.includes(computeStateDomain(this.entity))); 566 | }) 567 | .filter(this.filterFromAttributes(localAttributes)) 568 | .filter(this.filterFromAttributes(globalAttributes)) 569 | .filter(this.filterFromAttributes(existingAttributes)) 570 | .filter(this.filterFromAttributes(newAttributes)); 571 | return knownKeys.sort().concat('Other'); 572 | }; 573 | } 574 | }); 575 | }, 576 | 577 | installClassHooks() { 578 | if (window.customUI.classInitDone) return; 579 | window.customUI.classInitDone = true; 580 | window.customUI.installPartialCards(); 581 | window.customUI.installStatesHook(); 582 | window.customUI.installHaStateLabelBadge(); 583 | window.customUI.installStateBadge(); 584 | window.customUI.installHaAttributes(); 585 | window.customUI.installActionName('state-card-scene'); 586 | window.customUI.installActionName('state-card-script'); 587 | }, 588 | 589 | init() { 590 | if (window.customUI.initDone) return; 591 | window.customUI.installClassHooks(); 592 | const main = window.customUI.lightOrShadow(document, 'home-assistant'); 593 | if (!main.hass || !main.hass.states) { 594 | // Connection wasn't made yet. Try in 1 second. 595 | window.setTimeout(window.customUI.init, 1000); 596 | return; 597 | } 598 | window.customUI.initDone = true; 599 | 600 | window.customUI.useCustomizer(); 601 | 602 | window.customUI.runHooks(); 603 | window.addEventListener('location-changed', window.setTimeout.bind(null, window.customUI.runHooks, 100)); 604 | /* eslint-disable no-console */ 605 | console.log(`Loaded CustomUI ${VERSION}`); 606 | /* eslint-enable no-console */ 607 | if (!window.CUSTOM_UI_LIST) { 608 | window.CUSTOM_UI_LIST = []; 609 | } 610 | window.CUSTOM_UI_LIST.push({ 611 | name: 'CustomUI', 612 | version: VERSION, 613 | url: 'https://github.com/andrey-git/home-assistant-custom-ui', 614 | }); 615 | }, 616 | 617 | runHooks() { 618 | window.customUI.fixGroupTitles(); 619 | window.customUI.updateConfigPanel(); 620 | window.customUI.installHaFormCustomize(); 621 | }, 622 | 623 | getName() { 624 | return window.localStorage.getItem('ha-device-name') || ''; 625 | }, 626 | 627 | setName(name) { 628 | window.localStorage.setItem('ha-device-name', name || ''); 629 | }, 630 | 631 | computeTemplate(template, hass, entities, entity, attributes, attribute, state) { 632 | const functionBody = (template.indexOf('return') >= 0) ? template : `return \`${template}\`;`; 633 | try { 634 | /* eslint-disable no-new-func */ 635 | const func = new Function( 636 | 'hass', 'entities', 'entity', 'attributes', 'attribute', 'state', functionBody); 637 | /* eslint-enable no-new-func */ 638 | return func(hass, entities, entity, attributes, attribute, state); 639 | } catch (e) { 640 | /* eslint-disable no-console */ 641 | if ((e instanceof SyntaxError) || e instanceof ReferenceError) { 642 | console.warn(`${e.name}: ${e.message} in template ${functionBody}`); 643 | return null; 644 | } 645 | /* eslint-enable no-console */ 646 | throw e; 647 | } 648 | }, 649 | }; 650 | window.customUI.init(); 651 | -------------------------------------------------------------------------------- /src/utils/version.js: -------------------------------------------------------------------------------- 1 | export default '20190518'; 2 | -------------------------------------------------------------------------------- /state-card-custom-ui-dbg-es5.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /state-card-custom-ui-dbg-es5.html.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrey-git/home-assistant-custom-ui/5274c9b1e51d8a4ba409cbf510d286472d42c328/state-card-custom-ui-dbg-es5.html.gz -------------------------------------------------------------------------------- /state-card-custom-ui-dbg.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /state-card-custom-ui-dbg.html.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrey-git/home-assistant-custom-ui/5274c9b1e51d8a4ba409cbf510d286472d42c328/state-card-custom-ui-dbg.html.gz -------------------------------------------------------------------------------- /state-card-custom-ui-es5.html.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrey-git/home-assistant-custom-ui/5274c9b1e51d8a4ba409cbf510d286472d42c328/state-card-custom-ui-es5.html.gz -------------------------------------------------------------------------------- /state-card-custom-ui.html: -------------------------------------------------------------------------------- 1 | 242 | -------------------------------------------------------------------------------- /state-card-custom-ui.html.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrey-git/home-assistant-custom-ui/5274c9b1e51d8a4ba409cbf510d286472d42c328/state-card-custom-ui.html.gz -------------------------------------------------------------------------------- /update.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function get_file { 4 | DOWNLOAD_PATH=${2}?raw=true 5 | FILE_NAME=$1 6 | if [ "${FILE_NAME:0:1}" = "/" ]; then 7 | SAVE_PATH=$FILE_NAME 8 | else 9 | SAVE_PATH=$3$FILE_NAME 10 | fi 11 | TMP_NAME=${1}.tmp 12 | echo "Getting $1" 13 | # wget $DOWNLOAD_PATH -q -O $TMP_NAME 14 | curl -s -q -L -o $TMP_NAME $DOWNLOAD_PATH 15 | rv=$? 16 | if [ $rv != 0 ]; then 17 | rm $TMP_NAME 18 | echo "Download failed with error $rv" 19 | exit 20 | fi 21 | diff ${SAVE_PATH} $TMP_NAME &>/dev/null 22 | if [ $? == 0 ]; then 23 | echo "File up to date." 24 | rm $TMP_NAME 25 | return 0 26 | else 27 | mv $TMP_NAME ${SAVE_PATH} 28 | if [ $1 == $0 ]; then 29 | chmod u+x $0 30 | echo "Restarting" 31 | $0 32 | exit $? 33 | else 34 | return 1 35 | fi 36 | fi 37 | } 38 | 39 | function get_file_and_gz { 40 | get_file $1 $2 $3 41 | r1=$? 42 | get_file ${1}.gz ${2}.gz $3 43 | r2=$? 44 | if (( $r1 != 0 || $r2 != 0 )); then 45 | return 1 46 | fi 47 | return 0 48 | } 49 | 50 | function check_dir { 51 | if [ ! -d $1 ]; then 52 | read -p "$1 dir not found. Create? (y/n): [n] " r 53 | r=${r:-n} 54 | if [[ $r == 'y' || $r == 'Y' ]]; then 55 | mkdir -p $1 56 | else 57 | exit 58 | fi 59 | fi 60 | } 61 | 62 | if [ ! -f configuration.yaml ]; then 63 | echo "There is no configuration.yaml in current dir. 'update.sh' should run from Homeassistant config dir" 64 | read -p "Are you sure you want to continue? (y/n): [n] " r 65 | r=${r:-n} 66 | if [[ $r == 'n' || $r == 'N' ]]; then 67 | exit 68 | fi 69 | fi 70 | 71 | get_file $0 https://github.com/andrey-git/home-assistant-custom-ui/blob/master/update.sh ./ 72 | 73 | 74 | check_dir "www/custom_ui" 75 | 76 | 77 | get_file scripts.js.map https://github.com/andrey-git/home-assistant-custom-ui/blob/master/scripts.js.map www/custom_ui/ 78 | get_file scripts.js.LICENSE https://github.com/andrey-git/home-assistant-custom-ui/blob/master/scripts.js.LICENSE www/custom_ui/ 79 | get_file scripts-es5.js.map https://github.com/andrey-git/home-assistant-custom-ui/blob/master/scripts-es5.js.map www/custom_ui/ 80 | get_file scripts-es5.js.LICENSE https://github.com/andrey-git/home-assistant-custom-ui/blob/master/scripts-es5.js.LICENSE www/custom_ui/ 81 | get_file_and_gz state-card-custom-ui-es5.html https://github.com/andrey-git/home-assistant-custom-ui/blob/master/state-card-custom-ui-es5.html www/custom_ui/ 82 | get_file_and_gz state-card-custom-ui.html https://github.com/andrey-git/home-assistant-custom-ui/blob/master/state-card-custom-ui.html www/custom_ui/ 83 | 84 | 85 | if [ $? != 0 ]; then 86 | echo "Updated to Custom UI `grep -o -e '"[0-9][0-9][0-9]*"' www/custom_ui/state-card-custom-ui.html`" 87 | fi 88 | 89 | 90 | check_dir "custom_components/customizer" 91 | 92 | get_file __init__.py https://github.com/andrey-git/home-assistant-customizer/blob/master/customizer/__init__.py custom_components/customizer/ 93 | get_file services.yaml https://github.com/andrey-git/home-assistant-customizer/blob/master/customizer/services.yaml custom_components/customizer/ 94 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); 3 | 4 | function createConfig(es5, prod) { 5 | const buildPath = 'build'; 6 | 7 | const entry = { 8 | scripts: './src/entrypoints/scripts.js', 9 | }; 10 | 11 | const babelOptions = {}; 12 | 13 | babelOptions.presets = 14 | [ 15 | es5 && [ 16 | require('@babel/preset-env').default, 17 | { modules: false }, 18 | ], 19 | require('@babel/preset-typescript').default, 20 | ].filter(Boolean); 21 | 22 | const plugins = prod ? 23 | [new UglifyJsPlugin({ 24 | extractComments: true, 25 | sourceMap: true, 26 | uglifyOptions: { 27 | // Disabling because it broke output 28 | mangle: false, 29 | }, 30 | }), 31 | ] : []; 32 | 33 | return { 34 | mode: prod ? 'production' : 'development', 35 | devtool: prod ? 'source-map ' : 'inline-source-map', 36 | entry, 37 | module: { 38 | rules: [ 39 | { 40 | test: /\.js$|\.ts$/, 41 | use: { 42 | loader: 'babel-loader', 43 | options: babelOptions, 44 | }, 45 | }, 46 | ], 47 | }, 48 | plugins, 49 | resolve: { 50 | extensions: ['.ts', '.js', '.json'], 51 | }, 52 | output: { 53 | filename: `[name]${prod ? '' : '-dbg'}${es5 ? '-es5' : ''}.js`, 54 | path: path.resolve(__dirname, buildPath), 55 | }, 56 | }; 57 | } 58 | 59 | const configs = [ 60 | createConfig(/* es5= */true, /* prod= */true), 61 | createConfig(/* es5= */false, /* prod= */true), 62 | createConfig(/* es5= */true, /* prod= */false), 63 | createConfig(/* es5= */false, /* prod= */false), 64 | ]; 65 | module.exports = configs; 66 | --------------------------------------------------------------------------------