├── .gitattributes ├── release_notes ├── v1.0.1.md ├── v2.0.1.md ├── v2.2.1.md ├── v2.7.0.md ├── v1.0.2.md ├── v1.2.8.md ├── v2.3.0.md ├── v1.2.4.md ├── v2.0.0.md ├── v2.0.3.md ├── v2.6.1.md ├── v2.6.2.md ├── v2.1.3.md ├── v2.4.3.md ├── v2.4.4.md ├── v2.7.2.md ├── v1.2.7.md ├── v2.2.0.md ├── v2.3.1.md ├── v2.4.5.md ├── v1.2.9.md ├── v2.6.0.md ├── v2.7.1.md ├── v2.1.0.md ├── v2.0.2.md ├── v2.1.1.md ├── v2.4.1.md ├── v2.7.3.md ├── v2.4.0.md ├── v1.0.3.md ├── v1.2.6.md ├── v2.5.0.md ├── v2.1.2.md ├── v2.4.2.md ├── v1.2.10.md ├── v1.2.3.md ├── v1.2.5.md ├── v1.2.2.md ├── v1.1.0.md └── v1.2.1.md ├── images └── preview.png ├── hacs.json ├── src ├── initialize.js ├── utils │ ├── getLabel.js │ ├── utils.js │ ├── buildElementDefinitions.js │ └── handleClick.js ├── components │ ├── mwc │ │ ├── ripple.js │ │ ├── list.js │ │ ├── menu-surface.js │ │ ├── list-item.js │ │ └── menu.js │ ├── mode-menu.js │ ├── buttons.js │ ├── button.js │ ├── dropdown.js │ ├── temperature.js │ ├── secondary-info.js │ ├── target-temperature.js │ ├── indicators.js │ ├── dropdown-base.js │ └── fan-mode-secondary.js ├── sharedStyle.js ├── const.js ├── models │ ├── temperature.js │ ├── indicator.js │ ├── hvac-mode.js │ ├── climate.js │ ├── target-temperature.js │ └── button.js ├── style.js └── main.js ├── .github └── workflows │ ├── hacs.yml │ ├── ci.yml │ └── cd.yml ├── .eslintrc.yml ├── .babelrc ├── rollup-plugins └── ignore.js ├── rollup.config.js ├── info.md ├── LICENSE ├── package.json ├── .gitignore └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | *.js text eol=lf -------------------------------------------------------------------------------- /release_notes/v1.0.1.md: -------------------------------------------------------------------------------- 1 | ## v1.0.1 2 | - Initial release 3 | -------------------------------------------------------------------------------- /images/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artem-sedykh/mini-climate-card/HEAD/images/preview.png -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mini climate card", 3 | "render_readme": false, 4 | "filename": "mini-climate-card-bundle.js" 5 | } 6 | -------------------------------------------------------------------------------- /release_notes/v2.0.1.md: -------------------------------------------------------------------------------- 1 | ## v2.0.1 2 | [![Downloads](https://img.shields.io/github/downloads/artem-sedykh/mini-climate-card/v2.0.1/total.svg)](https://github.com/artem-sedykh/mini-climate-card/releases/tag/v2.0.1) 3 | 4 | ### FIXED 5 | - fix: mobile popup issues by @regevbr 6 | 7 | -------------------------------------------------------------------------------- /release_notes/v2.2.1.md: -------------------------------------------------------------------------------- 1 | ## v2.2.1 2 | [![Downloads](https://img.shields.io/github/downloads/artem-sedykh/mini-climate-card/v2.2.1/total.svg)](https://github.com/artem-sedykh/mini-climate-card/releases/tag/v2.2.1) 3 | 4 | ### Added 5 | - task: move ci to Github Actions by @regevbr 6 | -------------------------------------------------------------------------------- /release_notes/v2.7.0.md: -------------------------------------------------------------------------------- 1 | ## v2.7.0 2 | [![Downloads](https://img.shields.io/github/downloads/artem-sedykh/mini-climate-card/v2.7.0/total.svg)](https://github.com/artem-sedykh/mini-climate-card/releases/tag/v2.7.0) 3 | 4 | ### Fixed 5 | - Update to new lit version #137 by @regevbr 6 | -------------------------------------------------------------------------------- /release_notes/v1.0.2.md: -------------------------------------------------------------------------------- 1 | ## v1.0.2 2 | ### ADDED 3 | - Added group property for display in `entities` container 4 | - Add toggle button configuration 5 | 6 | ### FIXED 7 | - Cannot set property '__init' of undefined [issue](https://github.com/artem-sedykh/mini-climate-card/issues/2) 8 | 9 | -------------------------------------------------------------------------------- /release_notes/v1.2.8.md: -------------------------------------------------------------------------------- 1 | ## v1.2.8 2 | [![Downloads](https://img.shields.io/github/downloads/artem-sedykh/mini-climate-card/v1.2.8/total.svg)](https://github.com/artem-sedykh/mini-climate-card/releases/tag/v1.2.8) 3 | 4 | ### FIXED 5 | - Forced TargetTemperature as Float. #48 by @straccio 6 | -------------------------------------------------------------------------------- /release_notes/v2.3.0.md: -------------------------------------------------------------------------------- 1 | ## v2.3.0 2 | [![Downloads](https://img.shields.io/github/downloads/artem-sedykh/mini-climate-card/v2.3.0/total.svg)](https://github.com/artem-sedykh/mini-climate-card/releases/tag/v2.3.0) 3 | 4 | ### Added 5 | - feature: hide secondary_info option by @regevbr 6 | -------------------------------------------------------------------------------- /release_notes/v1.2.4.md: -------------------------------------------------------------------------------- 1 | ## v1.2.4 2 | [![Downloads](https://img.shields.io/github/downloads/artem-sedykh/mini-climate-card/v1.2.4/total.svg)](https://github.com/artem-sedykh/mini-climate-card/releases/tag/v1.2.4) 3 | 4 | ### FIXED 5 | 6 | - Styles fixed #23(When the toggle_button is hidden) 7 | -------------------------------------------------------------------------------- /release_notes/v2.0.0.md: -------------------------------------------------------------------------------- 1 | ## v2.0.0 2 | [![Downloads](https://img.shields.io/github/downloads/artem-sedykh/mini-climate-card/v2.0.0/total.svg)](https://github.com/artem-sedykh/mini-climate-card/releases/tag/v2.0.0) 3 | 4 | ### FIXED 5 | - fix #66: 2022.3.X Breaks dropdown #67 by @regevbr 6 | 7 | -------------------------------------------------------------------------------- /release_notes/v2.0.3.md: -------------------------------------------------------------------------------- 1 | ## v2.0.3 2 | [![Downloads](https://img.shields.io/github/downloads/artem-sedykh/mini-climate-card/v2.0.3/total.svg)](https://github.com/artem-sedykh/mini-climate-card/releases/tag/v2.0.3) 3 | 4 | ### FIXED 5 | - fix #37: 'style' option has no effect by @regevbr 6 | 7 | -------------------------------------------------------------------------------- /release_notes/v2.6.1.md: -------------------------------------------------------------------------------- 1 | ## v2.6.1 2 | [![Downloads](https://img.shields.io/github/downloads/artem-sedykh/mini-climate-card/v2.6.1/total.svg)](https://github.com/artem-sedykh/mini-climate-card/releases/tag/v2.6.1) 3 | 4 | ### Fixed 5 | - fix: remove border when in group mode #122 by @regevbr 6 | -------------------------------------------------------------------------------- /release_notes/v2.6.2.md: -------------------------------------------------------------------------------- 1 | ## v2.6.2 2 | [![Downloads](https://img.shields.io/github/downloads/artem-sedykh/mini-climate-card/v2.6.2/total.svg)](https://github.com/artem-sedykh/mini-climate-card/releases/tag/v2.6.2) 3 | 4 | ### Fixed 5 | - Dropdown not working in android app #124 by @regevbr 6 | -------------------------------------------------------------------------------- /release_notes/v2.1.3.md: -------------------------------------------------------------------------------- 1 | ## v2.1.3 2 | [![Downloads](https://img.shields.io/github/downloads/artem-sedykh/mini-climate-card/v2.1.3/total.svg)](https://github.com/artem-sedykh/mini-climate-card/releases/tag/v2.1.3) 3 | 4 | ### Fixed 5 | - bug: race condition caused icons not to render by @regevbr 6 | -------------------------------------------------------------------------------- /release_notes/v2.4.3.md: -------------------------------------------------------------------------------- 1 | ## v2.4.3 2 | [![Downloads](https://img.shields.io/github/downloads/artem-sedykh/mini-climate-card/v2.4.3/total.svg)](https://github.com/artem-sedykh/mini-climate-card/releases/tag/v2.4.3) 3 | 4 | ### Fixed 5 | - fix: Icon color wont change with state when on by @regevbr 6 | -------------------------------------------------------------------------------- /release_notes/v2.4.4.md: -------------------------------------------------------------------------------- 1 | ## v2.4.4 2 | [![Downloads](https://img.shields.io/github/downloads/artem-sedykh/mini-climate-card/v2.4.4/total.svg)](https://github.com/artem-sedykh/mini-climate-card/releases/tag/v2.4.4) 3 | 4 | ### Fixed 5 | - fix: frontend border to match new HA styles by @SanchosPancho 6 | -------------------------------------------------------------------------------- /release_notes/v2.7.2.md: -------------------------------------------------------------------------------- 1 | ## v2.7.2 2 | [![Downloads](https://img.shields.io/github/downloads/artem-sedykh/mini-climate-card/v2.7.2/total.svg)](https://github.com/artem-sedykh/mini-climate-card/releases/tag/v2.7.2) 3 | 4 | ### Fixed 5 | - fix: target_temperature unit not working #151 by @regevbr 6 | -------------------------------------------------------------------------------- /src/initialize.js: -------------------------------------------------------------------------------- 1 | import { version } from '../package.json'; 2 | 3 | // eslint-disable-next-line no-console 4 | console.info( 5 | `%c MINI-CLIMATE-CARD %c ${version} `, 6 | 'color: white; background: coral; font-weight: 700;', 7 | 'color: coral; background: white; font-weight: 700;', 8 | ); 9 | -------------------------------------------------------------------------------- /release_notes/v1.2.7.md: -------------------------------------------------------------------------------- 1 | ## v1.2.7 2 | [![Downloads](https://img.shields.io/github/downloads/artem-sedykh/mini-climate-card/v1.2.7/total.svg)](https://github.com/artem-sedykh/mini-climate-card/releases/tag/v1.2.7) 3 | 4 | ### FIXED 5 | - for ha >= 0.113.0 added theme variable `--card-background-color` 6 | -------------------------------------------------------------------------------- /release_notes/v2.2.0.md: -------------------------------------------------------------------------------- 1 | ## v2.2.0 2 | [![Downloads](https://img.shields.io/github/downloads/artem-sedykh/mini-climate-card/v2.2.0/total.svg)](https://github.com/artem-sedykh/mini-climate-card/releases/tag/v2.2.0) 3 | 4 | ### Added 5 | - feature: you can now add the card from the card picker by @regevbr 6 | -------------------------------------------------------------------------------- /release_notes/v2.3.1.md: -------------------------------------------------------------------------------- 1 | ## v2.3.1 2 | [![Downloads](https://img.shields.io/github/downloads/artem-sedykh/mini-climate-card/v2.3.1/total.svg)](https://github.com/artem-sedykh/mini-climate-card/releases/tag/v2.3.1) 3 | 4 | ### Added 5 | - fix: target temperature shows NaN with 0.x step size by @regevbr 6 | -------------------------------------------------------------------------------- /release_notes/v2.4.5.md: -------------------------------------------------------------------------------- 1 | ## v2.4.5 2 | [![Downloads](https://img.shields.io/github/downloads/artem-sedykh/mini-climate-card/v2.4.5/total.svg)](https://github.com/artem-sedykh/mini-climate-card/releases/tag/v2.4.5) 3 | 4 | ### Fixed 5 | - fix: Buttons style not updated on entity state #113 by @regevbr 6 | -------------------------------------------------------------------------------- /release_notes/v1.2.9.md: -------------------------------------------------------------------------------- 1 | ## v1.2.9 2 | [![Downloads](https://img.shields.io/github/downloads/artem-sedykh/mini-climate-card/v1.2.9/total.svg)](https://github.com/artem-sedykh/mini-climate-card/releases/tag/v1.2.9) 3 | 4 | ### FIXED 5 | - Show a placeholder ('-') when no target temperature #50 by @GuyLewin 6 | -------------------------------------------------------------------------------- /release_notes/v2.6.0.md: -------------------------------------------------------------------------------- 1 | ## v2.6.0 2 | [![Downloads](https://img.shields.io/github/downloads/artem-sedykh/mini-climate-card/v2.6.0/total.svg)](https://github.com/artem-sedykh/mini-climate-card/releases/tag/v2.6.0) 3 | 4 | ### Added 5 | - feature: add custom style functionality to indicators value by @regevbr 6 | -------------------------------------------------------------------------------- /release_notes/v2.7.1.md: -------------------------------------------------------------------------------- 1 | ## v2.7.1 2 | [![Downloads](https://img.shields.io/github/downloads/artem-sedykh/mini-climate-card/v2.7.1/total.svg)](https://github.com/artem-sedykh/mini-climate-card/releases/tag/v2.7.1) 3 | 4 | ### Added 5 | - feature: Add ability to override ha-card-box-shadow #122 by @regevbr 6 | -------------------------------------------------------------------------------- /release_notes/v2.1.0.md: -------------------------------------------------------------------------------- 1 | ## v2.1.0 2 | [![Downloads](https://img.shields.io/github/downloads/artem-sedykh/mini-climate-card/v2.1.0/total.svg)](https://github.com/artem-sedykh/mini-climate-card/releases/tag/v2.1.0) 3 | 4 | ### FIXED 5 | - fix #51: Add ability to swap target and actual temperature by @parrel 6 | 7 | -------------------------------------------------------------------------------- /release_notes/v2.0.2.md: -------------------------------------------------------------------------------- 1 | ## v2.0.2 2 | [![Downloads](https://img.shields.io/github/downloads/artem-sedykh/mini-climate-card/v2.0.2/total.svg)](https://github.com/artem-sedykh/mini-climate-card/releases/tag/v2.0.2) 3 | 4 | ### FIXED 5 | - fix #72: selected value in dropdown lists is not highlighted by @regevbr 6 | 7 | -------------------------------------------------------------------------------- /release_notes/v2.1.1.md: -------------------------------------------------------------------------------- 1 | ## v2.1.1 2 | [![Downloads](https://img.shields.io/github/downloads/artem-sedykh/mini-climate-card/v2.1.1/total.svg)](https://github.com/artem-sedykh/mini-climate-card/releases/tag/v2.1.1) 3 | 4 | ### FIXED 5 | - implemented #78: Add last-updated as an option for secondary_info type by @parrel 6 | -------------------------------------------------------------------------------- /release_notes/v2.4.1.md: -------------------------------------------------------------------------------- 1 | ## v2.4.1 2 | [![Downloads](https://img.shields.io/github/downloads/artem-sedykh/mini-climate-card/v2.4.1/total.svg)](https://github.com/artem-sedykh/mini-climate-card/releases/tag/v2.4.1) 3 | 4 | ### Fixed 5 | - fix: fix support for fire-dom-event action to interact with browser mod by @regevbr 6 | -------------------------------------------------------------------------------- /release_notes/v2.7.3.md: -------------------------------------------------------------------------------- 1 | ## v2.7.3 2 | [![Downloads](https://img.shields.io/github/downloads/artem-sedykh/mini-climate-card/v2.7.3/total.svg)](https://github.com/artem-sedykh/mini-climate-card/releases/tag/v2.7.3) 3 | 4 | ### Fixed 5 | - fix: Can't get custom change_action for target_temperature working #165 by @regevbr 6 | -------------------------------------------------------------------------------- /release_notes/v2.4.0.md: -------------------------------------------------------------------------------- 1 | ## v2.4.0 2 | [![Downloads](https://img.shields.io/github/downloads/artem-sedykh/mini-climate-card/v2.4.0/total.svg)](https://github.com/artem-sedykh/mini-climate-card/releases/tag/v2.4.0) 3 | 4 | ### Added 5 | - feature: add support for fire-dom-event action to interact with browser mod by @regevbr 6 | -------------------------------------------------------------------------------- /release_notes/v1.0.3.md: -------------------------------------------------------------------------------- 1 | ## v1.0.3 2 | [![Downloads](https://img.shields.io/github/downloads/artem-sedykh/mini-climate-card/v1.0.3/total.svg)](https://github.com/artem-sedykh/mini-climate-card/releases/tag/v1.0.3) 3 | ### FIXED 4 | - temperature config fixed [issue](https://github.com/artem-sedykh/mini-climate-card/issues/3) 5 | 6 | -------------------------------------------------------------------------------- /release_notes/v1.2.6.md: -------------------------------------------------------------------------------- 1 | ## v1.2.6 2 | [![Downloads](https://img.shields.io/github/downloads/artem-sedykh/mini-climate-card/v1.2.6/total.svg)](https://github.com/artem-sedykh/mini-climate-card/releases/tag/v1.2.6) 3 | 4 | ### FIXED 5 | - variable `ha-card-border-radius` added to styles, for rounding the edges of the card in various themes 6 | -------------------------------------------------------------------------------- /release_notes/v2.5.0.md: -------------------------------------------------------------------------------- 1 | ## v2.5.0 2 | [![Downloads](https://img.shields.io/github/downloads/artem-sedykh/mini-climate-card/v2.5.0/total.svg)](https://github.com/artem-sedykh/mini-climate-card/releases/tag/v2.5.0) 3 | 4 | ### Added 5 | - feature: add hide functionality to indicators by @regevbr and @adi90x 6 | - feature: hide can now be a function by @regevbr 7 | -------------------------------------------------------------------------------- /release_notes/v2.1.2.md: -------------------------------------------------------------------------------- 1 | ## v2.1.2 2 | [![Downloads](https://img.shields.io/github/downloads/artem-sedykh/mini-climate-card/v2.1.2/total.svg)](https://github.com/artem-sedykh/mini-climate-card/releases/tag/v2.1.2) 3 | 4 | ### FIXED 5 | - implemented #39: Ability to hide current temperature by @mishnz 6 | 7 | ### Changed 8 | - updated Lit dependencies by @regevbr 9 | -------------------------------------------------------------------------------- /release_notes/v2.4.2.md: -------------------------------------------------------------------------------- 1 | ## v2.4.2 2 | [![Downloads](https://img.shields.io/github/downloads/artem-sedykh/mini-climate-card/v2.4.2/total.svg)](https://github.com/artem-sedykh/mini-climate-card/releases/tag/v2.4.2) 3 | 4 | ### Fixed 5 | - fix: Border when embedded since 2022.11.0 #106 by @regevbr 6 | - fix: Icon color wont change with state when on #108 by @regevbr 7 | -------------------------------------------------------------------------------- /.github/workflows/hacs.yml: -------------------------------------------------------------------------------- 1 | name: Hacs Validation 2 | 3 | on: 4 | push: 5 | branches: ['*'] 6 | 7 | jobs: 8 | validate: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout code 12 | uses: actions/checkout@v3 13 | - name: HACS validation 14 | uses: hacs/action@main 15 | with: 16 | category: plugin 17 | -------------------------------------------------------------------------------- /release_notes/v1.2.10.md: -------------------------------------------------------------------------------- 1 | ## v1.2.10 2 | [![Downloads](https://img.shields.io/github/downloads/artem-sedykh/mini-climate-card/v1.2.10/total.svg)](https://github.com/artem-sedykh/mini-climate-card/releases/tag/v1.2.10) 3 | 4 | ### FIXED 5 | - fix icons not showing in new HA version #53 by @regevbr 6 | 7 | ### CHNGED 8 | - add heat-cool, supported by Nest Thermostat #52 by @GuyLewin 9 | 10 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | extends: airbnb-base 2 | rules: 3 | no-else-return: 0 4 | no-underscore-dangle: 0 5 | nonblock-statement-body-position: 0 6 | curly: 0 7 | no-return-assign: 0 8 | consistent-return: 0 9 | no-mixed-operators: 0 10 | class-methods-use-this: 0 11 | no-nested-ternary: 0 12 | globals: 13 | window: true 14 | Event: true 15 | customElements: true 16 | CustomEvent: true 17 | -------------------------------------------------------------------------------- /release_notes/v1.2.3.md: -------------------------------------------------------------------------------- 1 | ## v1.2.3 2 | [![Downloads](https://img.shields.io/github/downloads/artem-sedykh/mini-climate-card/v1.2.3/total.svg)](https://github.com/artem-sedykh/mini-climate-card/releases/tag/v1.2.3) 3 | 4 | ### ADDED 5 | 6 | - added the ability to hide hvac_mode #15 7 | ```yaml 8 | - type: custom:mini-climate 9 | entity: climate.dahatsu 10 | name: Кондиционер 11 | hvac_mode: 12 | hide: on 13 | ``` 14 | -------------------------------------------------------------------------------- /src/utils/getLabel.js: -------------------------------------------------------------------------------- 1 | const getLabel = (hass, labels, fallback = 'unknown') => { 2 | const lang = hass.selectedLanguage || hass.language; 3 | const resources = hass.resources[lang]; 4 | if (!resources) 5 | return fallback; 6 | 7 | for (let i = 0; i < labels.length; i += 1) { 8 | const label = labels[i]; 9 | if (resources[label]) 10 | return resources[label]; 11 | } 12 | 13 | return fallback; 14 | }; 15 | 16 | export default getLabel; 17 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "modules": false, 7 | "targets": { 8 | "esmodules": true 9 | } 10 | } 11 | ], 12 | ["minify"] 13 | ], 14 | "comments": false, 15 | "plugins": [ 16 | [ 17 | "@babel/plugin-proposal-decorators", 18 | { "legacy": true } 19 | ], 20 | [ 21 | "@babel/plugin-proposal-class-properties", 22 | { "loose": true } 23 | ], 24 | ["@babel/plugin-transform-template-literals"], 25 | ["iife-wrap"] 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /release_notes/v1.2.5.md: -------------------------------------------------------------------------------- 1 | ## v1.2.5 2 | [![Downloads](https://img.shields.io/github/downloads/artem-sedykh/mini-climate-card/v1.2.5/total.svg)](https://github.com/artem-sedykh/mini-climate-card/releases/tag/v1.2.5) 3 | 4 | ### ADDED 5 | 6 | - sort order for dropdown 7 | 8 | ```yaml 9 | - type: custom:mini-climate 10 | entity: climate.dahatsu 11 | name: Кондиционер 12 | hvac_mode: 13 | source: 14 | 'off': 15 | name: 'off' 16 | order: 4 17 | heat: 18 | name: heat 19 | order: 2 20 | cool: 21 | name: cool 22 | order: 1 23 | ``` 24 | -------------------------------------------------------------------------------- /rollup-plugins/ignore.js: -------------------------------------------------------------------------------- 1 | export default function (userOptions = {}) { 2 | // Files need to be absolute paths. 3 | // This only works if the file has no exports 4 | // and only is imported for its side effects 5 | const files = userOptions.files || []; 6 | 7 | if (files.length === 0) { 8 | return { 9 | name: 'ignore', 10 | }; 11 | } 12 | 13 | return { 14 | name: 'ignore', 15 | 16 | load(id) { 17 | return files.some(toIgnorePath => id.startsWith(toIgnorePath)) 18 | ? { 19 | code: '', 20 | } 21 | : null; 22 | }, 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /src/components/mwc/ripple.js: -------------------------------------------------------------------------------- 1 | import { RippleBase } from '@material/mwc-ripple/mwc-ripple-base'; 2 | import { styles as rippleStyles } from '@material/mwc-ripple/mwc-ripple.css'; 3 | import { ScopedRegistryHost } from '@lit-labs/scoped-registry-mixin'; 4 | import buildElementDefinitions from '../../utils/buildElementDefinitions'; 5 | 6 | export default class ClimateRipple extends ScopedRegistryHost(RippleBase) { 7 | static get defineId() { return 'mwc-ripple'; } 8 | 9 | static get elementDefinitions() { 10 | return buildElementDefinitions([], ClimateRipple); 11 | } 12 | 13 | static get styles() { 14 | return rippleStyles; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/components/mwc/list.js: -------------------------------------------------------------------------------- 1 | import { ScopedRegistryHost } from '@lit-labs/scoped-registry-mixin'; 2 | import { ListBase } from '@material/mwc-list/mwc-list-base'; 3 | import { styles as listStyles } from '@material/mwc-list/mwc-list.css'; 4 | import ClimateListItem from './list-item'; 5 | import buildElementDefinitions from '../../utils/buildElementDefinitions'; 6 | 7 | export default class ClimateList extends ScopedRegistryHost(ListBase) { 8 | static get defineId() { return 'mwc-list'; } 9 | 10 | static get elementDefinitions() { 11 | return buildElementDefinitions([ClimateListItem], ClimateList); 12 | } 13 | 14 | static get styles() { 15 | return listStyles; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/sharedStyle.js: -------------------------------------------------------------------------------- 1 | import { css } from 'lit'; 2 | 3 | const sharedStyle = css` 4 | .ellipsis { 5 | overflow: hidden; 6 | text-overflow: ellipsis; 7 | white-space: nowrap; 8 | } 9 | .label { 10 | margin: 0 8px; 11 | } 12 | ha-icon { 13 | width: calc(var(--mc-unit) * .6); 14 | height: calc(var(--mc-unit) * .6); 15 | } 16 | ha-icon-button { 17 | color: var(--mc-button-color); 18 | transition: color .25s; 19 | } 20 | ha-icon-button[color] { 21 | color: var(--mc-icon-active-color) !important; 22 | opacity: 1 !important; 23 | } 24 | ha-icon-button[inactive] { 25 | opacity: .5; 26 | } 27 | `; 28 | 29 | export default sharedStyle; 30 | -------------------------------------------------------------------------------- /src/components/mwc/menu-surface.js: -------------------------------------------------------------------------------- 1 | import { MenuSurfaceBase } from '@material/mwc-menu/mwc-menu-surface-base'; 2 | import { styles as menuSurfaceStyles } from '@material/mwc-menu/mwc-menu-surface.css'; 3 | import { ScopedRegistryHost } from '@lit-labs/scoped-registry-mixin'; 4 | import buildElementDefinitions from '../../utils/buildElementDefinitions'; 5 | 6 | export default class ClimateMenuSurface extends ScopedRegistryHost(MenuSurfaceBase) { 7 | static get defineId() { return 'mwc-menu-surface'; } 8 | 9 | static get elementDefinitions() { 10 | return buildElementDefinitions([], ClimateMenuSurface); 11 | } 12 | 13 | static get styles() { 14 | return menuSurfaceStyles; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from 'rollup-plugin-node-resolve'; 2 | import json from '@rollup/plugin-json'; 3 | import ignore from './rollup-plugins/ignore'; 4 | 5 | export default { 6 | input: 'src/main.js', 7 | output: { 8 | file: 'dist/mini-climate-card-bundle.js', 9 | format: 'umd', 10 | name: 'MiniClimate', 11 | }, 12 | plugins: [ 13 | resolve(), 14 | json(), 15 | ignore({ 16 | files: [ 17 | '@material/mwc-menu/mwc-menu-surface.js', 18 | '@material/mwc-ripple/mwc-ripple.js', 19 | '@material/mwc-list/mwc-list.js', 20 | '@material/mwc-list/mwc-list-item.js', 21 | ].map(file => require.resolve(file)), 22 | }), 23 | ], 24 | }; 25 | -------------------------------------------------------------------------------- /src/components/mwc/list-item.js: -------------------------------------------------------------------------------- 1 | import { ScopedRegistryHost } from '@lit-labs/scoped-registry-mixin'; 2 | import { ListItemBase } from '@material/mwc-list/mwc-list-item-base'; 3 | import { styles as listItemStyles } from '@material/mwc-list/mwc-list-item.css'; 4 | import ClimateRipple from './ripple'; 5 | import buildElementDefinitions from '../../utils/buildElementDefinitions'; 6 | 7 | export default class ClimateListItem extends ScopedRegistryHost(ListItemBase) { 8 | static get defineId() { return 'mwc-list-item'; } 9 | 10 | static get elementDefinitions() { 11 | return buildElementDefinitions([ClimateRipple], ClimateListItem); 12 | } 13 | 14 | static get styles() { 15 | return listItemStyles; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /info.md: -------------------------------------------------------------------------------- 1 | [![Last Version](https://img.shields.io/github/package-json/v/artem-sedykh/mini-climate-card?label.svg=release)](https://github.com/artem-sedykh/mini-climate-card/releases/latest) 2 | [![buymeacoffee_badge](https://img.shields.io/badge/Donate-buymeacoffe-ff813f?style=flat)](https://www.buymeacoffee.com/anavrin72) 3 | 4 | A minimalistic yet customizable climate card for [Home Assistant](https://github.com/home-assistant/home-assistant) Lovelace UI. 5 | 6 |

7 | 8 |

9 | 10 | **Check the [repository](https://github.com/artem-sedykh/mini-climate-card) for card options & example configurations** 11 | -------------------------------------------------------------------------------- /src/components/mwc/menu.js: -------------------------------------------------------------------------------- 1 | import { ScopedRegistryHost } from '@lit-labs/scoped-registry-mixin'; 2 | import { MenuBase } from '@material/mwc-menu/mwc-menu-base'; 3 | import { styles as menuStyles } from '@material/mwc-menu/mwc-menu.css'; 4 | import ClimateMenuSurface from './menu-surface'; 5 | import ClimateList from './list'; 6 | import buildElementDefinitions from '../../utils/buildElementDefinitions'; 7 | 8 | export default class ClimateMenu extends ScopedRegistryHost(MenuBase) { 9 | static get defineId() { return 'mwc-menu'; } 10 | 11 | static get elementDefinitions() { 12 | return buildElementDefinitions([ClimateMenuSurface, ClimateList], ClimateMenu); 13 | } 14 | 15 | static get styles() { 16 | return menuStyles; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/const.js: -------------------------------------------------------------------------------- 1 | 2 | const ICON = { 3 | DEFAULT: 'mdi:air-conditioner', 4 | FAN: 'mdi:fan', 5 | OFF: 'mdi:power', 6 | HEAT: 'mdi:weather-sunny', 7 | AUTO: 'mdi:cached', 8 | COOL: 'mdi:snowflake', 9 | HEAT_COOL: 'mdi:sun-snowflake', 10 | DRY: 'mdi:water', 11 | FAN_ONLY: 'mdi:fan', 12 | TOGGLE: 'mdi:dots-horizontal', 13 | UP: 'mdi:chevron-up', 14 | DOWN: 'mdi:chevron-down', 15 | }; 16 | 17 | export default ICON; 18 | export const STATES_OFF = ['closed', 'locked', 'off']; 19 | export const UNAVAILABLE = 'unavailable'; 20 | export const UNKNOWN = 'unknown'; 21 | export const UNAVAILABLE_STATES = [UNAVAILABLE, UNKNOWN]; 22 | export const ACTION_TIMEOUT = 2000; 23 | export const TAP_ACTIONS = ['more-info', 'navigate', 'call-service', 'url', 'fire-dom-event']; 24 | export const NO_TARGET_TEMPERATURE = '-'; 25 | -------------------------------------------------------------------------------- /release_notes/v1.2.2.md: -------------------------------------------------------------------------------- 1 | ## v1.2.2 2 | [![Downloads](https://img.shields.io/github/downloads/artem-sedykh/mini-climate-card/v1.2.2/total.svg)](https://github.com/artem-sedykh/mini-climate-card/releases/tag/v1.2.2) 3 | 4 | ### ADDED 5 | 6 | - Ability to set a fixed number of decimal places for temperature 7 | ```yaml 8 | - type: custom:mini-climate 9 | entity: climate.dahatsu 10 | name: Кондиционер 11 | temperature: 12 | fixed: 1 13 | ``` 14 | rounding can be used 15 | ```yaml 16 | - type: custom:mini-climate 17 | entity: climate.dahatsu 18 | name: Кондиционер 19 | temperature: 20 | round: 1 21 | ``` 22 | difference between round and fixed 23 | 24 | `21.123 round: 1 => 21.1` 25 | `21.123 round: 2 => 21.12` 26 | `21 round: 1 => 21` 27 | 28 | `21.123 fixed: 1 => 21.1` 29 | `21 fixed: 1 => 21.0` 30 | `21 fixed: 2 => 21.00` 31 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | push: 5 | branches: ['*'] 6 | pull_request: 7 | branches: ['*'] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v3 15 | - name: Node 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: 18.x 19 | - name: Cahce dependencies 20 | uses: actions/cache@v3 21 | with: 22 | path: ~/.npm # npm cache files are stored in `~/.npm` on Linux/macOS 23 | key: ${{ runner.OS }}-node-18.x-${{ hashFiles('**/package-lock.json') }} 24 | restore-keys: | 25 | ${{ runner.OS }}-node-18.x 26 | ${{ runner.OS }}- 27 | - name: Install dependencies 28 | run: | 29 | npm install 30 | - name: Lint and build 31 | run: | 32 | npm run lint 33 | npm run rollup 34 | npm run babel 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Artem Sedykh 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 | -------------------------------------------------------------------------------- /src/utils/utils.js: -------------------------------------------------------------------------------- 1 | import { STATES_OFF, UNAVAILABLE_STATES } from '../const'; 2 | 3 | const toggleState = (state) => { 4 | if (!state) 5 | return state; 6 | 7 | if (!STATES_OFF.includes(state) && !UNAVAILABLE_STATES.includes(state)) 8 | return 'off'; 9 | 10 | if (STATES_OFF.includes(state) && !UNAVAILABLE_STATES.includes(state)) 11 | return 'on'; 12 | 13 | return state; 14 | }; 15 | 16 | const getEntityValue = (entity, config) => { 17 | if (!entity) 18 | return undefined; 19 | 20 | if (!config) 21 | return entity.state; 22 | 23 | if (config.attribute && entity.attributes) 24 | return entity.attributes[config.attribute]; 25 | 26 | return entity.state; 27 | }; 28 | 29 | const round = (value, decimals) => Number(`${Math.round(Number(`${value}e${decimals}`))}e-${decimals}`); 30 | 31 | const compileTemplate = (template, context) => { 32 | try { 33 | // eslint-disable-next-line no-new-func 34 | return (new Function('', `return ${template}`)).call(context || {}); 35 | } catch (e) { 36 | throw new Error(`\n[COMPILE ERROR]: [${e.toString()}]\n[SOURCE]: ${template}\n`); 37 | } 38 | }; 39 | 40 | export { 41 | round, 42 | compileTemplate, 43 | getEntityValue, 44 | toggleState, 45 | }; 46 | -------------------------------------------------------------------------------- /src/utils/buildElementDefinitions.js: -------------------------------------------------------------------------------- 1 | const buildElementDefinitions = (elements, constructor) => { 2 | const promises = []; 3 | const definitions = elements.reduce( 4 | (aggregate, element) => { 5 | if (typeof element === 'string') { 6 | const clazz = customElements.get(element); 7 | if (clazz) { 8 | // eslint-disable-next-line no-param-reassign 9 | aggregate[element] = clazz; 10 | } else { 11 | promises.push(customElements.whenDefined(element).then(() => { 12 | if (constructor.registry.get(element) === undefined) { 13 | constructor.registry.define(element, customElements.get(element)); 14 | } 15 | })); 16 | } 17 | } else { 18 | // eslint-disable-next-line no-param-reassign 19 | aggregate[element.defineId] = element; 20 | } 21 | return aggregate; 22 | }, {}, 23 | ); 24 | // eslint-disable-next-line no-param-reassign 25 | constructor.elementDefinitionsLoaded = promises.length === 0; 26 | if (!constructor.elementDefinitionsLoaded) { 27 | Promise.all(promises) 28 | .then(() => { 29 | // eslint-disable-next-line no-param-reassign 30 | constructor.elementDefinitionsLoaded = true; 31 | }); 32 | } 33 | return definitions; 34 | }; 35 | 36 | export default buildElementDefinitions; 37 | -------------------------------------------------------------------------------- /src/utils/handleClick.js: -------------------------------------------------------------------------------- 1 | export default (node, hass, config, entityId) => { 2 | let e; 3 | if (!config) 4 | return; 5 | 6 | // eslint-disable-next-line default-case 7 | switch (config.action) { 8 | case 'more-info': { 9 | e = new Event('hass-more-info', { composed: true }); 10 | e.detail = { 11 | entityId: config.entity || entityId, 12 | }; 13 | node.dispatchEvent(e); 14 | break; 15 | } 16 | case 'navigate': { 17 | if (!config.navigation_path) return; 18 | window.history.pushState(null, '', config.navigation_path); 19 | e = new Event('location-changed', { composed: true }); 20 | e.detail = { replace: false }; 21 | window.dispatchEvent(e); 22 | break; 23 | } 24 | case 'call-service': { 25 | if (!config.service) return; 26 | const [domain, service] = config.service.split('.', 2); 27 | const serviceData = { ...config.service_data }; 28 | hass.callService(domain, service, serviceData); 29 | break; 30 | } 31 | case 'fire-dom-event': { 32 | e = new Event('ll-custom', { composed: true, bubbles: true }); 33 | e.detail = { ...config }; 34 | node.dispatchEvent(e); 35 | break; 36 | } 37 | case 'url': { 38 | if (!config.url) return; 39 | window.location.href = config.url; 40 | } 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mini-climate-card", 3 | "version": "v2.7.3", 4 | "description": "a/c card for Home Assistant Lovelace UI", 5 | "keywords": [ 6 | "home-assistant", 7 | "homeassistant", 8 | "hass", 9 | "automation", 10 | "lovelace", 11 | "climate", 12 | "custom-cards" 13 | ], 14 | "main": "src/main.js", 15 | "module": "src/main.js", 16 | "repository": "git@github.com:artem-sedykh/mini-climate-card.git", 17 | "author": "Artem Sedykh ", 18 | "license": "MIT", 19 | "dependencies": { 20 | "@lit-labs/scoped-registry-mixin": "^1.0.3", 21 | "@material/mwc-list": "^0.27.0", 22 | "@material/mwc-menu": "^0.27.0", 23 | "@material/mwc-ripple": "^0.27.0", 24 | "lit": "^3.1.0", 25 | "resize-observer-polyfill": "^1.5.1" 26 | }, 27 | "devDependencies": { 28 | "@babel/cli": "^7.4.3", 29 | "@babel/core": "^7.4.3", 30 | "@babel/plugin-proposal-class-properties": "^7.3.3", 31 | "@babel/plugin-proposal-decorators": "^7.3.0", 32 | "@babel/plugin-transform-template-literals": "^7.2.0", 33 | "@babel/preset-env": "^7.3.1", 34 | "@rollup/plugin-json": "^4.0.3", 35 | "babel-plugin-iife-wrap": "^1.1.0", 36 | "babel-preset-minify": "^0.5.0", 37 | "eslint": "^5.16.0", 38 | "eslint-config-airbnb-base": "^13.1.0", 39 | "eslint-plugin-import": "^2.26.0", 40 | "rollup": "^2.10.5", 41 | "rollup-plugin-node-resolve": "^3.4.0" 42 | }, 43 | "scripts": { 44 | "build": "npm run lint && npm run rollup && npm run babel", 45 | "rollup": "rollup -c", 46 | "babel": "babel dist/mini-climate-card-bundle.js --out-file dist/mini-climate-card-bundle.js", 47 | "lint": "eslint src/* --ext .js", 48 | "lint:fix": "eslint src/* --ext .js --fix", 49 | "watch": "rollup -c --watch" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Deployment 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | 8 | jobs: 9 | cd: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v3 14 | - name: Node 15 | uses: actions/setup-node@v3 16 | with: 17 | node-version: 18.x 18 | - name: Cahce dependencies 19 | uses: actions/cache@v3 20 | with: 21 | path: ~/.npm # npm cache files are stored in `~/.npm` on Linux/macOS 22 | key: ${{ runner.OS }}-node-18.x-${{ hashFiles('**/package-lock.json') }} 23 | restore-keys: | 24 | ${{ runner.OS }}-node-18.x 25 | ${{ runner.OS }}- 26 | - name: Install dependencies 27 | run: | 28 | npm install 29 | - name: Lint and build 30 | run: | 31 | npm run lint 32 | npm run rollup 33 | npm run babel 34 | - name: Set the release version 35 | shell: bash 36 | run: echo "RELEASE_VERSION=${GITHUB_REF:11}" >> $GITHUB_ENV 37 | - name: Set the release body 38 | id: release 39 | shell: bash 40 | run: | 41 | r=$(cat release_notes/v${{ env.RELEASE_VERSION }}.md) 42 | r="${r//'%'/'%25'}" # Multiline escape sequences for % 43 | r="${r//$'\n'/'%0A'}" # Multiline escape sequences for '\n' 44 | r="${r//$'\r'/'%0D'}" # Multiline escape sequences for '\r' 45 | echo "::set-output name=RELEASE_BODY::$r" 46 | - name: Upload the release file 47 | uses: svenstaro/upload-release-action@2.6.1 48 | with: 49 | file: dist/* 50 | file_glob: true 51 | overwrite: true 52 | promote: true 53 | body: | 54 | ${{ steps.release.outputs.RELEASE_BODY }} 55 | -------------------------------------------------------------------------------- /release_notes/v1.1.0.md: -------------------------------------------------------------------------------- 1 | ## v1.1.0 2 | [![Downloads](https://img.shields.io/github/downloads/artem-sedykh/mini-climate-card/v1.1.0/total.svg)](https://github.com/artem-sedykh/mini-climate-card/releases/tag/v1.1.0) 3 | ### FIXED 4 | - Smoothness of change of the set temperature. 5 | 6 | ### CHANGED 7 | - Automatic calculation max-width entity name #6 8 | 9 | ### ADDED 10 | - When you click on the button, it instantly changes its state, but if the actual state has not changed after `action_timeout` 11 | it returns to the previous state, default value `2000 milliseconds` 12 | ```yaml 13 | - type: custom:mini-climate 14 | entity: climate.dahatsu 15 | buttons: 16 | turbo: 17 | icon: mdi:weather-hurricane 18 | topic: 'dahatsu/turbo/set' 19 | state: 20 | attribute: turbo 21 | mapper: "(state, entity) => state ? 'on': 'off'" 22 | action_timeout: 2000 23 | toggle_action: > 24 | (state) => this.call_service('mqtt', 'publish', { payload: this.toggle_state(state), topic: this.topic, retain: false, qos: 1 }) 25 | 26 | ``` 27 | - indicator `tap_action` configuration, see readme with detailed configuration #5 28 | ```yaml 29 | - type: custom:mini-climate 30 | entity: climate.dahatsu 31 | indicators: 32 | humidity: 33 | icon: mdi:water 34 | unit: '%' 35 | source: 36 | entity: sensor.humidity 37 | tap_action: more-info 38 | # or 39 | # tap_action: 40 | # action: more-info 41 | ``` 42 | - Added configuration for secondary_info supported types `[last-changed, fan-mode, hvac-mode]`, default type `fan-mode` 43 | ```yaml 44 | - type: custom:mini-climate 45 | entity: climate.dahatsu 46 | secondary_info: last-changed 47 | 48 | - type: custom:mini-climate 49 | entity: climate.dahatsu 50 | secondary_info: 51 | type: fan-mode 52 | icon: 'mdi:fan' 53 | 54 | - type: custom:mini-climate 55 | entity: climate.dahatsu 56 | secondary_info: hvac-mode 57 | ``` 58 | -------------------------------------------------------------------------------- /src/models/temperature.js: -------------------------------------------------------------------------------- 1 | import { compileTemplate, getEntityValue, round } from '../utils/utils'; 2 | 3 | export default class TemperatureObject { 4 | constructor(temperatureEntity, targetTemperatureEntity, config, climate) { 5 | this.climate = climate || {}; 6 | this.temperatureEntity = temperatureEntity || {}; 7 | this.targetTemperatureEntity = targetTemperatureEntity || {}; 8 | this.config = config; 9 | if (this.config.hide_current_temperature) { 10 | if (typeof this.config.hide_current_temperature === 'boolean') { 11 | this.shouldHideCurrentTemperature = () => true; 12 | } else { 13 | this.shouldHideCurrentTemperature = compileTemplate(this.config.hide_current_temperature); 14 | } 15 | } else { 16 | this.shouldHideCurrentTemperature = () => false; 17 | } 18 | } 19 | 20 | get unit() { 21 | return this.config.temperature.unit || this.config.target_temperature.unit || '°C'; 22 | } 23 | 24 | get step() { 25 | const entity = this.targetTemperatureEntity; 26 | 27 | if ('step' in this.config.target_temperature) 28 | return this.config.target_temperature.step; 29 | 30 | if (entity && entity.attributes && entity.attributes.target_temp_step) 31 | return entity.attributes.target_temp_step; 32 | 33 | return 1; 34 | } 35 | 36 | get value() { 37 | const value = this.rawValue; 38 | 39 | if (value !== undefined) { 40 | if ('fixed' in this.config.temperature) 41 | return parseFloat(value.toString()).toFixed(this.config.temperature.fixed); 42 | 43 | if ('round' in this.config.temperature) 44 | return round(value, this.config.temperature.round); 45 | } 46 | 47 | return value; 48 | } 49 | 50 | get rawValue() { 51 | return getEntityValue(this.temperatureEntity, this.config.temperature.source); 52 | } 53 | 54 | get hide() { 55 | return this.shouldHideCurrentTemperature(this.value, this.temperatureEntity, 56 | this.targetTemperatureEntity, this.climate.entity, this.climate.mode); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/models/indicator.js: -------------------------------------------------------------------------------- 1 | import { getEntityValue, round } from '../utils/utils'; 2 | 3 | export default class IndicatorObject { 4 | constructor(entity, config, climate, hass) { 5 | this.config = config || {}; 6 | this.entity = entity || {}; 7 | this.climate = climate || {}; 8 | this._hass = hass || {}; 9 | } 10 | 11 | get id() { 12 | return this.config.id; 13 | } 14 | 15 | get hass() { 16 | return this._hass; 17 | } 18 | 19 | get originalValue() { 20 | return getEntityValue(this.entity, this.config.source); 21 | } 22 | 23 | get value() { 24 | let value = this.originalValue; 25 | 26 | if (this.config.functions.mapper) { 27 | value = this.config.functions.mapper(value, this.entity, 28 | this.climate.entity, this.climate.mode); 29 | } 30 | 31 | if ('round' in this.config && Number.isNaN(value) === false) 32 | value = round(value, this.config.round); 33 | 34 | return value; 35 | } 36 | 37 | get unit() { 38 | return this.config.unit; 39 | } 40 | 41 | get icon() { 42 | if (this.config.functions.icon && this.config.functions.icon.template) { 43 | return this.config.functions.icon.template(this.value, this.entity, 44 | this.climate.entity, this.climate.mode); 45 | } else if (this.config.icon && typeof this.config.icon === 'string') { 46 | return this.config.icon; 47 | } 48 | 49 | return ''; 50 | } 51 | 52 | get iconStyle() { 53 | if (this.config.functions.icon && this.config.functions.icon.style) 54 | return this.config.functions.icon.style(this.value, this.entity, 55 | this.climate.entity, this.climate.mode) || {}; 56 | 57 | return {}; 58 | } 59 | 60 | get valueStyle() { 61 | if (this.config.functions.value && this.config.functions.value.style) 62 | return this.config.functions.value.style(this.value, this.entity, 63 | this.climate.entity, this.climate.mode) || {}; 64 | 65 | return {}; 66 | } 67 | 68 | get hide() { 69 | if (this.config.functions.hide) { 70 | return this.config.functions.hide(this.value, this.entity, 71 | this.climate.entity, this.climate.mode); 72 | } 73 | 74 | return false; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | .idea/ 106 | dist/ 107 | /icons.txt 108 | -------------------------------------------------------------------------------- /src/components/mode-menu.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css } from 'lit'; 2 | import { ScopedRegistryHost } from '@lit-labs/scoped-registry-mixin'; 3 | import ICON from '../const'; 4 | import ClimateDropdownBase from './dropdown-base'; 5 | import buildElementDefinitions from '../utils/buildElementDefinitions'; 6 | 7 | export default class ClimateModeMenu extends ScopedRegistryHost(LitElement) { 8 | static get defineId() { return 'mc-mode-menu'; } 9 | 10 | static get elementDefinitions() { 11 | return buildElementDefinitions([ClimateDropdownBase], ClimateModeMenu); 12 | } 13 | 14 | constructor() { 15 | super(); 16 | this.mode = {}; 17 | } 18 | 19 | static get properties() { 20 | return { 21 | mode: { type: Object }, 22 | }; 23 | } 24 | 25 | get calcIcon() { 26 | if (this.selected) { 27 | if (this.selected.icon) 28 | return this.selected.icon; 29 | 30 | if (this.selected.id !== undefined && this.selected.id !== null) { 31 | const id = this.selected.id.toString().toUpperCase(); 32 | 33 | if (id in ICON) 34 | return ICON[id]; 35 | } 36 | } 37 | 38 | return ''; 39 | } 40 | 41 | get selected() { 42 | return this.mode.source.find(i => i.id === this.mode.state) || {}; 43 | } 44 | 45 | get sources() { 46 | return this.mode.source 47 | .filter(s => !s.hide) 48 | .map(s => ({ name: s.name, id: s.id, type: 'source' })); 49 | } 50 | 51 | handleChange(e) { 52 | e.stopPropagation(); 53 | const selected = e.detail.id; 54 | this.mode.handleChange(selected); 55 | } 56 | 57 | render() { 58 | if (!ClimateModeMenu.elementDefinitionsLoaded) { 59 | return html``; 60 | } 61 | 62 | return html` 63 | 70 | 71 | `; 72 | } 73 | 74 | static get styles() { 75 | return css` 76 | :host { 77 | min-width: calc(var(--mc-unit) * .85); 78 | --mc-dropdown-unit: calc(var(--mc-unit) * .75); 79 | --paper-item-min-height: var(--mc-unit); 80 | } 81 | `; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /release_notes/v1.2.1.md: -------------------------------------------------------------------------------- 1 | ## v1.2.1 2 | [![Downloads](https://img.shields.io/github/downloads/artem-sedykh/mini-climate-card/v1.2.1/total.svg)](https://github.com/artem-sedykh/mini-climate-card/releases/tag/v1.2.1) 3 | 4 | ### ADDED 5 | - Add new secondary_info type: `hvac-action` #9 6 | By default, translations from [ha frontend](https://github.com/home-assistant/frontend/blob/master/translations/frontend/en.json#L33) 7 | ```yaml 8 | - type: custom:mini-climate 9 | entity: climate.dahatsu 10 | secondary_info: 11 | type: hvac-action 12 | ``` 13 | but you can customize your translations 14 | ```yaml 15 | - type: custom:mini-climate 16 | entity: climate.dahatsu 17 | secondary_info: 18 | type: hvac-action 19 | source: 20 | cooling: Охлаждение 21 | ``` 22 | You can set your own icon for each hvac-action 23 | ```yaml 24 | - type: custom:mini-climate 25 | entity: climate.dahatsu 26 | secondary_info: 27 | type: hvac-action 28 | source: 29 | cooling: 30 | icon: 'mdi:snowflake' 31 | name: Охлаждение 32 | ``` 33 | You can set your own icon for each hvac-action 34 | ```yaml 35 | - type: custom:mini-climate 36 | entity: climate.dahatsu 37 | secondary_info: 38 | type: hvac-action 39 | source: 40 | cooling: 41 | icon: 'mdi:snowflake' 42 | name: Охлаждение 43 | ``` 44 | Or you can use one permanent icon 45 | ```yaml 46 | - type: custom:mini-climate 47 | entity: climate.dahatsu 48 | secondary_info: 49 | type: hvac-action 50 | icon: 'mdi:cached' 51 | ``` 52 | 53 | - Add new secondary_info type: `fan-mode-dropdown` #10 54 | 55 | ```yaml 56 | - type: custom:mini-climate 57 | entity: climate.dahatsu 58 | secondary_info: fan-mode-dropdown 59 | ``` 60 | ![image](https://user-images.githubusercontent.com/861063/84180244-d80d0a80-aa8f-11ea-8275-f4e3db85fd31.png) 61 | 62 | - Added the ability to make buttons on the main screen 63 | 64 | ```yaml 65 | - type: custom:mini-climate 66 | entity: climate.dahatsu 67 | fan_mode: 68 | location: main 69 | ``` 70 | ![image](https://user-images.githubusercontent.com/861063/84198175-d5201300-aaab-11ea-94fb-bf9fde5aa2b1.png) 71 | 72 | This rule also applies to buttons. 73 | ```yaml 74 | - type: custom:mini-climate 75 | entity: climate.dahatsu 76 | buttons: 77 | custom_button: 78 | location: main 79 | ``` 80 | ![image](https://user-images.githubusercontent.com/861063/84198845-d4d44780-aaac-11ea-8590-4eff94457593.png) 81 | 82 | > With these settings, the data may not fit on the mobile version! 83 | -------------------------------------------------------------------------------- /src/components/buttons.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css } from 'lit'; 2 | import { ScopedRegistryHost } from '@lit-labs/scoped-registry-mixin'; 3 | import sharedStyle from '../sharedStyle'; 4 | import ClimateButton from './button'; 5 | import ClimateDropDown from './dropdown'; 6 | import buildElementDefinitions from '../utils/buildElementDefinitions'; 7 | 8 | export default class ClimateButtons extends ScopedRegistryHost(LitElement) { 9 | static get defineId() { return 'mc-buttons'; } 10 | 11 | static get elementDefinitions() { 12 | return buildElementDefinitions([ClimateDropDown, ClimateButton], ClimateButtons); 13 | } 14 | 15 | static get properties() { 16 | return { 17 | buttons: {}, 18 | }; 19 | } 20 | 21 | renderButton(button) { 22 | if (button.isUnavailable) 23 | return ''; 24 | 25 | return html` 26 | 29 | 30 | `; 31 | } 32 | 33 | renderDropdown(dropdown) { 34 | return html` 35 | 37 | 38 | `; 39 | } 40 | 41 | renderInternal(button) { 42 | if (button.type === 'dropdown') 43 | return this.renderDropdown(button); 44 | 45 | return this.renderButton(button); 46 | } 47 | 48 | render() { 49 | if (!ClimateButtons.elementDefinitionsLoaded) { 50 | return html``; 51 | } 52 | 53 | const context = this; 54 | return html`${Object.entries(this.buttons) 55 | .map(b => b[1]) 56 | .filter(b => b.location !== 'main' && !b.hide) 57 | .sort((a, b) => ((a.order > b.order) ? 1 : ((b.order > a.order) ? -1 : 0))) 58 | .map(button => context.renderInternal(button))}`; 59 | } 60 | 61 | static get styles() { 62 | return [ 63 | sharedStyle, 64 | css` 65 | :host { 66 | position: relative; 67 | box-sizing: border-box; 68 | margin: 0; 69 | overflow: hidden; 70 | transition: background .5s; 71 | --paper-item-min-height: var(--mc-unit); 72 | --mc-dropdown-unit: var(--mc-unit); 73 | --mdc-icon-button-size: calc(var(--mc-unit)); 74 | } 75 | :host([color]) { 76 | background: var(--mc-active-color); 77 | transition: background .25s; 78 | opacity: 1; 79 | } 80 | :host([disabled]) { 81 | opacity: .25; 82 | pointer-events: none; 83 | } 84 | mc-button { 85 | width: calc(var(--mc-unit)); 86 | height: calc(var(--mc-unit)); 87 | } 88 | `]; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/components/button.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css } from 'lit'; 2 | import { styleMap } from 'lit/directives/style-map'; 3 | import { ScopedRegistryHost } from '@lit-labs/scoped-registry-mixin'; 4 | import sharedStyle from '../sharedStyle'; 5 | import buildElementDefinitions from '../utils/buildElementDefinitions'; 6 | 7 | export default class ClimateButton extends ScopedRegistryHost(LitElement) { 8 | static get defineId() { return 'mc-button'; } 9 | 10 | static get elementDefinitions() { 11 | return buildElementDefinitions([ 12 | 'ha-icon', 13 | 'ha-icon-button', 14 | ], ClimateButton); 15 | } 16 | 17 | constructor() { 18 | super(); 19 | this._isOn = false; 20 | this.timer = undefined; 21 | } 22 | 23 | static get properties() { 24 | return { 25 | button: { type: Object }, 26 | }; 27 | } 28 | 29 | handleToggle(e) { 30 | e.stopPropagation(); 31 | const { entity } = this.button; 32 | 33 | this._isOn = !this._isOn; 34 | this.button.handleToggle(); 35 | 36 | if (this.timer) 37 | clearTimeout(this.timer); 38 | 39 | this.timer = setTimeout(async () => { 40 | if (this.button.entity === entity) { 41 | this._isOn = this.button.isOn; 42 | this.requestUpdate('_isOn'); 43 | } 44 | }, this.button.actionTimeout); 45 | this.requestUpdate('_isOn'); 46 | } 47 | 48 | render() { 49 | if (!ClimateButton.elementDefinitionsLoaded) { 50 | return html``; 51 | } 52 | return html` 53 | this.handleToggle(e)} 57 | ?disabled="${this.button.disabled || this.button.isUnavailable}" 58 | ?color=${this._isOn}> 59 | 60 | 61 | `; 62 | } 63 | 64 | updated(changedProps) { 65 | if (changedProps.has('button')) { 66 | this._isOn = this.button.isOn; 67 | 68 | if (this.timer) 69 | clearTimeout(this.timer); 70 | 71 | this.requestUpdate('_isOn'); 72 | } 73 | } 74 | 75 | static get styles() { 76 | return [ 77 | sharedStyle, 78 | css` 79 | :host { 80 | position: relative; 81 | box-sizing: border-box; 82 | margin: 0; 83 | overflow: hidden; 84 | transition: background .5s; 85 | } 86 | :host([color]) { 87 | background: var(--mc-active-color); 88 | transition: background .25s; 89 | opacity: 1; 90 | } 91 | :host([disabled]) { 92 | opacity: .25; 93 | pointer-events: none; 94 | } 95 | `]; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/models/hvac-mode.js: -------------------------------------------------------------------------------- 1 | import { getEntityValue } from '../utils/utils'; 2 | 3 | export default class HvacModeObject { 4 | constructor(entity, config, climate) { 5 | this.config = config || {}; 6 | this.entity = entity || {}; 7 | this.climate = climate || {}; 8 | } 9 | 10 | get hide() { 11 | if (this.config.functions.hide) { 12 | return this.config.functions.hide(this.state, this.entity, 13 | this.climate.entity, this.climate.mode); 14 | } 15 | 16 | return false; 17 | } 18 | 19 | get originalState() { 20 | return getEntityValue(this.entity, this.config.state); 21 | } 22 | 23 | get state() { 24 | let state = this.originalState; 25 | 26 | if (this.config.functions.state && this.config.functions.state.mapper) { 27 | state = this.config.functions.state.mapper(state, this.entity, 28 | this.climate.entity); 29 | } 30 | 31 | return state; 32 | } 33 | 34 | isActive(state) { 35 | if (this.config.functions.active) { 36 | return this.config.functions.active(state, this.entity, this.climate.entity); 37 | } 38 | 39 | return false; 40 | } 41 | 42 | get disabled() { 43 | if (this.config.functions.disabled) { 44 | return this.config.functions.disabled(this.state, this.entity, this.climate.entity); 45 | } 46 | 47 | return false; 48 | } 49 | 50 | get style() { 51 | if (this.config.functions.style) { 52 | return this.config.functions.style(this.state, this.entity, this.climate.entity) || {}; 53 | } 54 | 55 | return {}; 56 | } 57 | 58 | get source() { 59 | const { functions } = this.config; 60 | let source = Object.entries(this.config.source || {}) 61 | .filter(([key]) => key !== '__filter') 62 | .map(([key, value]) => { 63 | if (typeof value === 'object') { 64 | return { id: key, ...value || {} }; 65 | } 66 | return { id: key, name: value }; 67 | }); 68 | 69 | if (source.some(s => 'order' in s)) 70 | source = source.sort((a, b) => ((a.order > b.order) ? 1 : ((b.order > a.order) ? -1 : 0))); 71 | 72 | if (functions.source && functions.source.filter) { 73 | return functions.source.filter(source, this.state, this.entity, this.climate.entity); 74 | } 75 | 76 | return source; 77 | } 78 | 79 | get selected() { 80 | const { state } = this; 81 | if (state === undefined || state === null) 82 | return undefined; 83 | 84 | return this.source.find(s => s.id === state.toString()); 85 | } 86 | 87 | handleChange(selected) { 88 | if (this.config.functions.change_action) { 89 | return this.config.functions.change_action(selected, this.entity, this.climate.entity); 90 | } 91 | 92 | return undefined; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/components/dropdown.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css } from 'lit'; 2 | import { ScopedRegistryHost } from '@lit-labs/scoped-registry-mixin'; 3 | import sharedStyle from '../sharedStyle'; 4 | import ClimateDropdownBase from './dropdown-base'; 5 | import buildElementDefinitions from '../utils/buildElementDefinitions'; 6 | 7 | export default class ClimateDropDown extends ScopedRegistryHost(LitElement) { 8 | static get defineId() { return 'mc-dropdown'; } 9 | 10 | static get elementDefinitions() { 11 | return buildElementDefinitions([ClimateDropdownBase], ClimateDropDown); 12 | } 13 | 14 | constructor() { 15 | super(); 16 | this.dropdown = {}; 17 | this.timer = undefined; 18 | this._state = undefined; 19 | } 20 | 21 | static get properties() { 22 | return { 23 | dropdown: { type: Object }, 24 | }; 25 | } 26 | 27 | handleChange(e) { 28 | e.stopPropagation(); 29 | 30 | const selected = e.detail.id; 31 | const { entity } = this.dropdown; 32 | this._state = selected; 33 | 34 | this.dropdown.handleChange(selected); 35 | 36 | if (this.timer) 37 | clearTimeout(this.timer); 38 | 39 | this.timer = setTimeout(async () => { 40 | if (this.dropdown.entity === entity) { 41 | this._state = (this.dropdown.state !== undefined && this.dropdown.state !== null) 42 | ? this.dropdown.state.toString() : ''; 43 | 44 | this.requestUpdate('_state'); 45 | } 46 | }, this.dropdown.actionTimeout); 47 | 48 | this.requestUpdate('_state'); 49 | } 50 | 51 | render() { 52 | if (!ClimateDropDown.elementDefinitionsLoaded) { 53 | return html``; 54 | } 55 | 56 | return html` 57 | this.handleChange(e)} 60 | .items=${this.dropdown.source} 61 | .icon=${this.dropdown.icon} 62 | .disabled="${this.dropdown.disabled}" 63 | .active=${this.dropdown.isActive(this._state)} 64 | .selected=${this._state}> 65 | 66 | `; 67 | } 68 | 69 | updated(changedProps) { 70 | if (changedProps.has('dropdown')) { 71 | this._state = (this.dropdown.state !== undefined && this.dropdown.state !== null) 72 | ? this.dropdown.state.toString() : ''; 73 | 74 | if (this.timer) 75 | clearTimeout(this.timer); 76 | 77 | this.requestUpdate('_state'); 78 | } 79 | } 80 | 81 | static get styles() { 82 | return [ 83 | sharedStyle, 84 | css` 85 | :host { 86 | position: relative; 87 | box-sizing: border-box; 88 | margin: 0; 89 | overflow: hidden; 90 | transition: background .5s; 91 | } 92 | :host([color]) { 93 | background: var(--mc-active-color); 94 | transition: background .25s; 95 | opacity: 1; 96 | } 97 | :host([disabled]) { 98 | opacity: .25; 99 | pointer-events: none; 100 | } 101 | `]; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/components/temperature.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css } from 'lit'; 2 | import { ScopedRegistryHost } from '@lit-labs/scoped-registry-mixin'; 3 | import buildElementDefinitions from '../utils/buildElementDefinitions'; 4 | import { NO_TARGET_TEMPERATURE } from '../const'; 5 | 6 | export default class ClimateTemperature extends ScopedRegistryHost(LitElement) { 7 | static get defineId() { return 'mc-temperature'; } 8 | 9 | static get elementDefinitions() { 10 | return buildElementDefinitions([], ClimateTemperature); 11 | } 12 | 13 | static get properties() { 14 | return { 15 | temperature: Object, 16 | changing: Boolean, 17 | target: Number, 18 | swapTemperatures: Boolean, 19 | }; 20 | } 21 | 22 | get targetStr() { 23 | const targetStr = this.target.toString(); 24 | const targetNum = parseFloat(targetStr); 25 | if (Number.isNaN(targetNum) || targetStr === NO_TARGET_TEMPERATURE) { 26 | return NO_TARGET_TEMPERATURE; 27 | } 28 | const parts = this.temperature.step.toString().split('.'); 29 | return parts[1] 30 | ? targetNum.toFixed(parts[1].length) 31 | : targetStr; 32 | } 33 | 34 | renderTemperature() { 35 | if (this.temperature.value === undefined || this.temperature.hide) 36 | return ''; 37 | 38 | if (this.swapTemperatures) { 39 | return html` 40 | ${this.temperature.value} 41 | /`; 42 | } 43 | 44 | return html` 45 | / 46 | ${this.temperature.value}`; 47 | } 48 | 49 | render() { 50 | if (!ClimateTemperature.elementDefinitionsLoaded) { 51 | return html``; 52 | } 53 | 54 | if (!this.temperature) { 55 | return html``; 56 | } 57 | 58 | const cls = this.changing ? 'changing' : ''; 59 | const { unit } = this.temperature; 60 | if (this.swapTemperatures) { 61 | return html` 62 |
63 | ${this.renderTemperature()} 64 | ${this.targetStr} 65 | ${unit} 66 |
`; 67 | } 68 | 69 | return html` 70 |
71 | ${this.targetStr} 72 | ${this.renderTemperature()} 73 | ${unit} 74 |
75 | `; 76 | } 77 | 78 | static get styles() { 79 | return css` 80 | .state { 81 | margin-top:calc(var(--mc-unit) * .15); 82 | } 83 | .state__value { 84 | font-weight: var(--mc-info-font-weight); 85 | line-height: calc(var(--mc-unit) * .475); 86 | font-size: calc(var(--mc-unit) * .475); 87 | } 88 | .state__uom { 89 | font-size: calc(var(--mc-unit) * 0.35); 90 | font-weight: var(--mc-name-font-weight); 91 | opacity: 0.6; 92 | line-height: calc(var(--mc-unit) * 0.475); 93 | } 94 | .ellipsis { 95 | overflow: hidden; 96 | text-overflow: ellipsis; 97 | white-space: nowrap; 98 | } 99 | .changing { 100 | color: var(--mc-accent-color); 101 | } 102 | `; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/components/secondary-info.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css } from 'lit'; 2 | import { ScopedRegistryHost } from '@lit-labs/scoped-registry-mixin'; 3 | import ClimateFanModeSecondary from './fan-mode-secondary'; 4 | import sharedStyle from '../sharedStyle'; 5 | import buildElementDefinitions from '../utils/buildElementDefinitions'; 6 | 7 | export default class ClimateSecondaryInfo extends ScopedRegistryHost(LitElement) { 8 | static get defineId() { return 'mc-secondary-info'; } 9 | 10 | static get elementDefinitions() { 11 | return buildElementDefinitions([ 12 | 'ha-icon', 13 | 'ha-relative-time', 14 | ClimateFanModeSecondary, 15 | ], ClimateSecondaryInfo); 16 | } 17 | 18 | constructor() { 19 | super(); 20 | this.fanMode = {}; 21 | this.hvacMode = {}; 22 | this.config = {}; 23 | this.climate = {}; 24 | } 25 | 26 | static get properties() { 27 | return { 28 | fanMode: { type: Object }, 29 | config: { type: Object }, 30 | hvacMode: { type: Object }, 31 | climate: { type: Object }, 32 | }; 33 | } 34 | 35 | renderHvacAction() { 36 | const action = this.climate.hvacAction; 37 | if (!action) 38 | return ''; 39 | 40 | const icon = action.icon ? action.icon : this.config.secondary_info.icon; 41 | const cls = icon ? '' : 'gray'; 42 | 43 | return html` 44 | ${icon ? html`` : ''} 45 | ${action.name} 46 | `; 47 | } 48 | 49 | renderHvacMode() { 50 | const { hvacMode } = this; 51 | const mode = hvacMode.selected || {}; 52 | const icon = mode.icon ? mode.icon : this.config.secondary_info.icon; 53 | 54 | return html` 55 | ${icon ? html`` : ''} 56 | ${mode.name} 57 | `; 58 | } 59 | 60 | render() { 61 | if (!ClimateSecondaryInfo.elementDefinitionsLoaded) { 62 | return html``; 63 | } 64 | 65 | const { type } = this.config.secondary_info; 66 | 67 | switch (type) { 68 | case 'hvac-mode': 69 | return this.renderHvacMode(); 70 | case 'hvac-action': 71 | return this.renderHvacAction(); 72 | case 'last-changed': 73 | return html``; 74 | case 'last-updated': 75 | return html``; 76 | default: 77 | return html``; 78 | } 79 | } 80 | 81 | static get styles() { 82 | return [ 83 | sharedStyle, 84 | css` 85 | ha-relative-time, .gray { 86 | color: #727272; 87 | } 88 | .name { 89 | font-size: calc(var(--mc-unit) * .35); 90 | font-weight: var(--mc-info-font-weight); 91 | line-height: calc(var(--mc-unit) * .5); 92 | vertical-align: middle; 93 | display: inline-block; 94 | } 95 | .icon { 96 | color: var(--mc-icon-color); 97 | height: calc(var(--mc-unit) * .475); 98 | width: calc(var(--mc-unit) * .5); 99 | min-width: calc(var(--mc-unit) * .5); 100 | --mdc-icon-size: calc(var(--mc-unit) * 0.5); 101 | } 102 | `]; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/components/target-temperature.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css } from 'lit'; 2 | import { ScopedRegistryHost } from '@lit-labs/scoped-registry-mixin'; 3 | import buildElementDefinitions from '../utils/buildElementDefinitions'; 4 | 5 | export default class ClimateTargetTemperature extends ScopedRegistryHost(LitElement) { 6 | static get defineId() { return 'mc-target-temperature'; } 7 | 8 | static get elementDefinitions() { 9 | return buildElementDefinitions([ 10 | 'ha-icon', 11 | 'ha-icon-button', 12 | ], ClimateTargetTemperature); 13 | } 14 | 15 | constructor() { 16 | super(); 17 | this.timeout = 800; 18 | } 19 | 20 | static get properties() { 21 | return { 22 | targetTemperature: { type: Object }, 23 | }; 24 | } 25 | 26 | increment(e) { 27 | e.stopPropagation(); 28 | const changed = this.targetTemperature.increment(); 29 | 30 | if (changed) { 31 | this.temp_last_changed = Date.now(); 32 | this.targetTemperatureChanged(); 33 | } 34 | } 35 | 36 | decrement(e) { 37 | e.stopPropagation(); 38 | 39 | const changed = this.targetTemperature.decrement(); 40 | 41 | if (changed) { 42 | this.temp_last_changed = Date.now(); 43 | this.targetTemperatureChanged(); 44 | } 45 | } 46 | 47 | sendChangeEvent(changing) { 48 | const data = { detail: { changing } }; 49 | const event = new CustomEvent('changing', data); 50 | this.dispatchEvent(event); 51 | } 52 | 53 | targetTemperatureChanged() { 54 | if (!this.temp_last_changed) 55 | return; 56 | 57 | this.sendChangeEvent(true); 58 | 59 | window.setTimeout(() => { 60 | const now = Date.now(); 61 | if (now - this.temp_last_changed >= this.timeout) { 62 | const { value } = this.targetTemperature; 63 | try { 64 | this.targetTemperature.update(value); 65 | } finally { 66 | this.sendChangeEvent(false); 67 | this.temp_last_changed = null; 68 | } 69 | } 70 | }, this.timeout + 10); 71 | } 72 | 73 | render() { 74 | if (!ClimateTargetTemperature.elementDefinitionsLoaded) { 75 | return html``; 76 | } 77 | if (!this.targetTemperature) 78 | return ''; 79 | 80 | return html` 81 |
82 | this.increment(e)}> 85 | 86 | 87 | this.decrement(e)}> 90 | 91 | 92 |
93 | `; 94 | } 95 | 96 | static get styles() { 97 | return css` 98 | .controls-wrap { 99 | display: flex; 100 | flex-direction: column; 101 | height: 100%; 102 | --ha-icon-display: flex; 103 | } 104 | .temp { 105 | width: calc(var(--mc-unit) * .75); 106 | height: calc(var(--mc-unit) * .75); 107 | --mdc-icon-button-size: calc(var(--mc-unit) * .75); 108 | color: var(--mc-icon-color); 109 | } 110 | .temp.--up { 111 | margin-top: -2px; 112 | } 113 | .temp.--down { 114 | margin-top: -2px; 115 | } 116 | .temp.--down { 117 | margin-top: auto; 118 | } 119 | `; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/models/climate.js: -------------------------------------------------------------------------------- 1 | import getLabel from '../utils/getLabel'; 2 | import ICON, { STATES_OFF, UNAVAILABLE_STATES } from '../const'; 3 | 4 | export default class ClimateObject { 5 | constructor(hass, config, entity) { 6 | this.hass = hass || {}; 7 | this.config = config || {}; 8 | this.entity = entity || {}; 9 | this.state = entity.state; 10 | this.attr = { 11 | friendly_name: '', 12 | temperature: 16, 13 | current_temperature: 24, 14 | fan_mode: '', 15 | hvac_modes: [], 16 | target_temp_step: undefined, 17 | min_temp: undefined, 18 | max_temp: undefined, 19 | hvac_action: '', 20 | fan_modes: [], 21 | ...entity.attributes || {}, 22 | }; 23 | } 24 | 25 | get lastChanged() { 26 | return this.entity.last_changed; 27 | } 28 | 29 | get lastUpdated() { 30 | return this.entity.last_updated; 31 | } 32 | 33 | get hvacAction() { 34 | const source = (this.config.secondary_info && this.config.secondary_info.source) || {}; 35 | const action = this.attr.hvac_action; 36 | let item = { id: action }; 37 | const labelPrefix = 'state_attributes.climate.hvac_action'; 38 | item.name = getLabel(this.hass, [`${labelPrefix}.${action}`], action); 39 | 40 | if (action in source) { 41 | if (typeof source[action] === 'string') 42 | item.name = source[action]; 43 | else 44 | item = { ...item, ...source[action] }; 45 | } 46 | 47 | return item; 48 | } 49 | 50 | get mode() { 51 | return this._hvac_mode; 52 | } 53 | 54 | set mode(value) { 55 | this._hvac_mode = value; 56 | } 57 | 58 | get defaultHvacModes() { 59 | const hvacModes = this.attr.hvac_modes; 60 | const source = []; 61 | 62 | for (let i = 0; i < hvacModes.length; i += 1) { 63 | const hvacMode = hvacModes[i]; 64 | const labels = [`state.climate.${hvacMode}`, `component.climate.state._.${hvacMode}`]; 65 | const item = { id: hvacMode, name: getLabel(this.hass, labels, hvacMode) }; 66 | const iconId = hvacMode.toString().toUpperCase(); 67 | if (iconId in ICON) 68 | item.icon = ICON[iconId]; 69 | 70 | source.push(item); 71 | } 72 | return source; 73 | } 74 | 75 | get defaultFanModes() { 76 | const fanModes = this.attr.fan_modes; 77 | const source = {}; 78 | const labelPrefix = 'state_attributes.climate.fan_mode'; 79 | 80 | for (let i = 0; i < fanModes.length; i += 1) { 81 | const mode = fanModes[i]; 82 | source[mode] = getLabel(this.hass, [`${labelPrefix}.${mode}`], mode); 83 | } 84 | return source; 85 | } 86 | 87 | get id() { 88 | return this.entity.entity_id; 89 | } 90 | 91 | get icon() { 92 | return this.attr.icon; 93 | } 94 | 95 | get name() { 96 | return this.attr.friendly_name || ''; 97 | } 98 | 99 | get isOff() { 100 | return this.entity !== undefined 101 | && STATES_OFF.includes(this.state) 102 | && !UNAVAILABLE_STATES.includes(this.state); 103 | } 104 | 105 | get isActive() { 106 | return (this.isOff === false && this.isUnavailable === false) || false; 107 | } 108 | 109 | get isUnavailable() { 110 | return this.entity === undefined || UNAVAILABLE_STATES.includes(this.state); 111 | } 112 | 113 | get isOn() { 114 | return this.entity !== undefined 115 | && !STATES_OFF.includes(this.state) 116 | && !UNAVAILABLE_STATES.includes(this.state); 117 | } 118 | 119 | callService(domain, service, inOptions) { 120 | return this.hass.callService(domain, service, { 121 | entity_id: this.config.entity, 122 | ...inOptions, 123 | }); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/models/target-temperature.js: -------------------------------------------------------------------------------- 1 | import { getEntityValue } from '../utils/utils'; 2 | import { NO_TARGET_TEMPERATURE } from '../const'; 3 | 4 | export default class TargetTemperatureObject { 5 | constructor(entity, config, hass) { 6 | this.entity = entity || {}; 7 | this.config = config; 8 | this._hass = hass; 9 | 10 | this.min = this.getMin(); 11 | this.max = this.getMax(); 12 | this.step = this.getStep(); 13 | } 14 | 15 | get hass() { 16 | return this._hass; 17 | } 18 | 19 | get icons() { 20 | return this.config.target_temperature.icons; 21 | } 22 | 23 | getStep() { 24 | if ('step' in this.config.target_temperature) 25 | return parseFloat(this.config.target_temperature.step); 26 | 27 | if (this.entity && this.entity.attributes && this.entity.attributes.target_temp_step) 28 | return parseFloat(this.entity.attributes.target_temp_step); 29 | 30 | return 1.0; 31 | } 32 | 33 | getMin() { 34 | if ('min' in this.config.target_temperature) 35 | return parseFloat(this.config.target_temperature.min); 36 | 37 | if (this.entity && this.entity.attributes && this.entity.attributes.min_temp) 38 | return parseFloat(this.entity.attributes.min_temp); 39 | 40 | return 16.0; 41 | } 42 | 43 | getMax() { 44 | if ('max' in this.config.target_temperature) 45 | return parseFloat(this.config.target_temperature.max); 46 | 47 | if (this.entity && this.entity.attributes && this.entity.attributes.max_temp) 48 | return parseFloat(this.entity.attributes.max_temp); 49 | 50 | return 30.0; 51 | } 52 | 53 | _floatOrPlaceholder(value) { 54 | if (Number.isNaN(value)) { 55 | return NO_TARGET_TEMPERATURE; 56 | } 57 | return value; 58 | } 59 | 60 | get value() { 61 | if (this._targetTemperature !== undefined) 62 | return this._floatOrPlaceholder(parseFloat(this._targetTemperature)); 63 | 64 | const newValue = getEntityValue(this.entity, this.config.target_temperature.source); 65 | return this._floatOrPlaceholder(parseFloat(newValue)); 66 | } 67 | 68 | set value(value) { 69 | this._targetTemperature = parseFloat(value); 70 | } 71 | 72 | increment() { 73 | const oldValue = this.value; 74 | 75 | if (oldValue === NO_TARGET_TEMPERATURE) { 76 | return false; 77 | } 78 | 79 | const newVal = this._round(this.value + this.step); 80 | 81 | if (newVal <= this.max) { 82 | if (newVal <= this.min) { 83 | this.value = this.min; 84 | } else { 85 | this.value = newVal; 86 | } 87 | } else { 88 | this.value = this.max; 89 | } 90 | 91 | return oldValue !== this.value; 92 | } 93 | 94 | decrement() { 95 | const oldValue = this.value; 96 | 97 | if (oldValue === NO_TARGET_TEMPERATURE) { 98 | return false; 99 | } 100 | 101 | const newVal = this._round(this.value - this.step); 102 | 103 | if (newVal >= this.min) { 104 | this.value = newVal; 105 | } else { 106 | this.value = this.min; 107 | } 108 | 109 | return oldValue !== this.value; 110 | } 111 | 112 | _round(val) { 113 | const s = this.step.toString().split('.'); 114 | return s[1] ? parseFloat(val.toFixed(s[1].length)) : Math.round(val); 115 | } 116 | 117 | update(value) { 118 | if (this.config.target_temperature.functions.change_action) { 119 | const climateEntity = this.hass.states[this.config.entity]; 120 | return this.config.target_temperature.functions.change_action(value, this.entity, 121 | climateEntity); 122 | } 123 | 124 | return this.hass.callService('climate', 'set_temperature', { entity_id: this.entity.entity_id, temperature: value }); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/components/indicators.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css } from 'lit'; 2 | 3 | import { styleMap } from 'lit/directives/style-map'; 4 | import { ScopedRegistryHost } from '@lit-labs/scoped-registry-mixin'; 5 | import handleClick from '../utils/handleClick'; 6 | import { TAP_ACTIONS } from '../const'; 7 | import buildElementDefinitions from '../utils/buildElementDefinitions'; 8 | 9 | export default class ClimateIndicators extends ScopedRegistryHost(LitElement) { 10 | static get defineId() { return 'mc-indicators'; } 11 | 12 | static get elementDefinitions() { 13 | return buildElementDefinitions(['ha-icon'], ClimateIndicators); 14 | } 15 | 16 | static get properties() { 17 | return { 18 | indicators: { type: Object }, 19 | }; 20 | } 21 | 22 | handlePopup(e, indicator) { 23 | e.stopPropagation(); 24 | handleClick(this, indicator.hass, indicator.config.tap_action, indicator.entity.entity_id); 25 | } 26 | 27 | renderIcon(indicator) { 28 | const { icon } = indicator; 29 | 30 | if (!icon) 31 | return ''; 32 | 33 | return html``; 34 | } 35 | 36 | renderUnit(indicator) { 37 | if (!indicator.unit) 38 | return ''; 39 | 40 | return html`${indicator.unit}`; 41 | } 42 | 43 | renderIndicator(indicator) { 44 | if (!indicator) 45 | return ''; 46 | const action = indicator.config && indicator.config.tap_action 47 | && indicator.config.tap_action.action; 48 | const cls = action && TAP_ACTIONS.includes(action) ? 'pointer' : ''; 49 | 50 | return html` 51 |
this.handlePopup(e, indicator)}> 52 | ${this.renderIcon(indicator)} 53 | ${indicator.value} 54 | ${this.renderUnit(indicator)} 55 |
56 | `; 57 | } 58 | 59 | render() { 60 | if (!ClimateIndicators.elementDefinitionsLoaded) { 61 | return html``; 62 | } 63 | 64 | const indicatorsToShow = Object.entries(this.indicators) 65 | .map(entry => entry[1]) 66 | .filter(indicator => !indicator.hide); 67 | 68 | const context = this; 69 | 70 | return html` 71 |
72 | ${indicatorsToShow.map(i => context.renderIndicator(i))} 73 |
74 | `; 75 | } 76 | 77 | static get styles() { 78 | return css` 79 | :host { 80 | position: relative; 81 | box-sizing: border-box; 82 | font-size: calc(var(--mc-unit) * .35); 83 | line-height: calc(var(--mc-unit) * .35); 84 | } 85 | .mc-indicators__container { 86 | display: flex; 87 | flex-wrap: wrap; 88 | margin-right: calc(var(--mc-unit) * .075); 89 | } 90 | .state { 91 | position: relative; 92 | display: flex; 93 | flex-wrap: nowrap; 94 | margin-right: calc(var(--mc-unit) * .1); 95 | } 96 | .pointer { 97 | cursor: pointer 98 | } 99 | .state__value_icon { 100 | height: calc(var(--mc-unit) * .475); 101 | width: calc(var(--mc-unit) * .5); 102 | color: var(--mc-icon-color); 103 | --mdc-icon-size: calc(var(--mc-unit) * 0.5); 104 | } 105 | .state__value { 106 | margin: 0 1px; 107 | font-weight: var(--mc-info-font-weight); 108 | line-height: calc(var(--mc-unit) * .475); 109 | } 110 | .state__uom { 111 | font-size: calc(var(--mc-unit) * .275); 112 | line-height: calc(var(--mc-unit) * .525); 113 | margin-left: 1px; 114 | height: calc(var(--mc-unit) * .475); 115 | opacity: 0.8; 116 | } 117 | `; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/components/dropdown-base.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css } from 'lit'; 2 | import { styleMap } from 'lit/directives/style-map'; 3 | 4 | import { ScopedRegistryHost } from '@lit-labs/scoped-registry-mixin'; 5 | import sharedStyle from '../sharedStyle'; 6 | import ClimateMenu from './mwc/menu'; 7 | import ClimateListItem from './mwc/list-item'; 8 | import buildElementDefinitions from '../utils/buildElementDefinitions'; 9 | 10 | export default class ClimateDropdownBase extends ScopedRegistryHost(LitElement) { 11 | static get defineId() { return 'mc-dropdown-base'; } 12 | 13 | static get elementDefinitions() { 14 | return buildElementDefinitions([ 15 | 'ha-icon', 16 | 'ha-icon-button', 17 | ClimateMenu, 18 | ClimateListItem, 19 | ], ClimateDropdownBase); 20 | } 21 | 22 | static get properties() { 23 | return { 24 | items: [], 25 | label: String, 26 | selected: String, 27 | icon: String, 28 | active: Boolean, 29 | disabled: Boolean, 30 | iconStyle: { type: Object }, 31 | }; 32 | } 33 | 34 | constructor() { 35 | super(); 36 | this.iconStyle = {}; 37 | } 38 | 39 | get selectedId() { 40 | return this.items.map(item => item.id).indexOf(this.selected); 41 | } 42 | 43 | onChange(e) { 44 | const { index } = e.detail; 45 | if (index !== this.selectedId && this.items[index]) { 46 | this.dispatchEvent(new CustomEvent('change', { 47 | detail: this.items[index], 48 | })); 49 | e.detail.index = -1; 50 | } 51 | } 52 | 53 | handleClick() { 54 | const menu = this.shadowRoot.querySelector('#menu'); 55 | menu.anchor = this.shadowRoot.querySelector('#button'); 56 | menu.show(); 57 | } 58 | 59 | render() { 60 | if (!ClimateDropdownBase.elementDefinitionsLoaded) { 61 | return html``; 62 | } 63 | return html` 64 |
65 | 71 | 72 | 73 | 79 | ${this.items.map(item => html` 80 | 81 | ${item.name} 82 | `)} 83 | 84 |
85 | `; 86 | } 87 | 88 | static get styles() { 89 | return [ 90 | sharedStyle, 91 | css` 92 | :host { 93 | position: relative; 94 | overflow: hidden; 95 | } 96 | .mc-dropdown 97 | :host([disabled]) { 98 | opacity: .25; 99 | pointer-events: none; 100 | } 101 | :host([faded]) { 102 | opacity: .75; 103 | } 104 | .mc-dropdown { 105 | padding: 0; 106 | } 107 | ha-icon-button[disabled] { 108 | opacity: .25; 109 | pointer-events: none; 110 | } 111 | .mc-dropdown__button.icon { 112 | margin: 0; 113 | } 114 | ha-icon-button { 115 | width: calc(var(--mc-dropdown-unit)); 116 | height: calc(var(--mc-dropdown-unit)); 117 | --mdc-icon-button-size: calc(var(--mc-dropdown-unit)); 118 | } 119 | mwc-item > *:nth-child(2) { 120 | margin-left: 4px; 121 | } 122 | .mc-dropdown[focused] ha-icon-button { 123 | color: var(--mc-accent-color); 124 | } 125 | .mc-dropdown[focused] ha-icon-button[focused] { 126 | color: var(--mc-text-color); 127 | transform: rotate(0deg); 128 | } 129 | `, 130 | ]; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/models/button.js: -------------------------------------------------------------------------------- 1 | import { getEntityValue } from '../utils/utils'; 2 | import { ACTION_TIMEOUT, STATES_OFF, UNAVAILABLE_STATES } from '../const'; 3 | 4 | export default class ButtonObject { 5 | constructor(entity, config, climate, hass) { 6 | this.config = config || {}; 7 | this.entity = entity || {}; 8 | this.climate = climate || {}; 9 | this._hass = hass || {}; 10 | } 11 | 12 | get id() { 13 | return this.config.id; 14 | } 15 | 16 | get location() { 17 | return this.config.location || 'bottom'; 18 | } 19 | 20 | get hass() { 21 | return this._hass; 22 | } 23 | 24 | get type() { 25 | return this.config.type; 26 | } 27 | 28 | get order() { 29 | return this.config.order; 30 | } 31 | 32 | get hide() { 33 | if (this.config.functions.hide) { 34 | return this.config.functions.hide(this.state, this.entity, 35 | this.climate.entity, this.climate.mode); 36 | } 37 | 38 | return false; 39 | } 40 | 41 | get icon() { 42 | return this.config.icon; 43 | } 44 | 45 | get originalState() { 46 | return getEntityValue(this.entity, this.config.state); 47 | } 48 | 49 | get state() { 50 | let state = this.originalState; 51 | 52 | if (this.config.functions.state && this.config.functions.state.mapper) { 53 | state = this.config.functions.state.mapper(state, this.entity, 54 | this.climate.entity, this.climate.mode); 55 | } 56 | 57 | return state; 58 | } 59 | 60 | isActive(state) { 61 | if (this.config.functions.active) { 62 | return this.config.functions.active(state, this.entity, 63 | this.climate.entity, this.climate.mode); 64 | } 65 | 66 | return false; 67 | } 68 | 69 | get isUnavailable() { 70 | return this.entity === undefined || UNAVAILABLE_STATES.includes(this.state); 71 | } 72 | 73 | get isOn() { 74 | return this.entity !== undefined 75 | && !STATES_OFF.includes(this.state) 76 | && !UNAVAILABLE_STATES.includes(this.state); 77 | } 78 | 79 | get disabled() { 80 | if (this.config.functions.disabled) { 81 | return this.config.functions.disabled(this.state, this.entity, 82 | this.climate.entity, this.climate.mode); 83 | } 84 | 85 | return false; 86 | } 87 | 88 | get style() { 89 | if (this.config.functions.style) { 90 | return this.config.functions.style(this.state, this.entity, 91 | this.climate.entity, this.climate.mode) || {}; 92 | } 93 | 94 | return {}; 95 | } 96 | 97 | get source() { 98 | const { functions } = this.config; 99 | let source = Object.entries(this.config.source || {}) 100 | .filter(([key]) => key !== '__filter') 101 | .map(([key, value]) => { 102 | if (typeof value === 'object') { 103 | return { id: key, ...value || {} }; 104 | } 105 | return { id: key, name: value }; 106 | }); 107 | 108 | if (source.some(s => 'order' in s)) 109 | source = source.sort((a, b) => ((a.order > b.order) ? 1 : ((b.order > a.order) ? -1 : 0))); 110 | 111 | if (functions.source && functions.source.filter) { 112 | return functions.source.filter(source, this.state, this.entity, 113 | this.climate.entity, this.climate.mode); 114 | } 115 | 116 | return source; 117 | } 118 | 119 | get selected() { 120 | const { state } = this; 121 | if (state === undefined || state === null) 122 | return undefined; 123 | 124 | return this.source.find(s => s.id === state.toString()); 125 | } 126 | 127 | get actionTimeout() { 128 | if ('action_timeout' in this.config) 129 | return this.config.action_timeout; 130 | 131 | return ACTION_TIMEOUT; 132 | } 133 | 134 | handleToggle() { 135 | if (this.config.functions.toggle_action) { 136 | return this.config.functions.toggle_action(this.state, this.entity, 137 | this.climate.entity, this.climate.mode); 138 | } 139 | 140 | return this.climate.callService('switch', 'toggle', { entity_id: this.entity.entity_id }); 141 | } 142 | 143 | handleChange(selected) { 144 | if (this.config.functions.change_action) { 145 | return this.config.functions.change_action(selected, this.state, this.entity, 146 | this.climate.entity, this.climate.mode); 147 | } 148 | 149 | return undefined; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/components/fan-mode-secondary.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css } from 'lit'; 2 | import { ScopedRegistryHost } from '@lit-labs/scoped-registry-mixin'; 3 | import sharedStyle from '../sharedStyle'; 4 | import ClimateMenu from './mwc/menu'; 5 | import ClimateListItem from './mwc/list-item'; 6 | import buildElementDefinitions from '../utils/buildElementDefinitions'; 7 | 8 | export default class ClimateFanModeSecondary extends ScopedRegistryHost(LitElement) { 9 | static get defineId() { return 'mc-fan-mode-secondary'; } 10 | 11 | static get elementDefinitions() { 12 | return buildElementDefinitions([ 13 | 'ha-icon', 14 | ClimateMenu, 15 | ClimateListItem, 16 | ], ClimateFanModeSecondary); 17 | } 18 | 19 | constructor() { 20 | super(); 21 | this.fanMode = {}; 22 | this.config = {}; 23 | this.timer = undefined; 24 | this._selected = {}; 25 | this.source = {}; 26 | } 27 | 28 | static get properties() { 29 | return { 30 | fanMode: { type: Object }, 31 | config: { type: Object }, 32 | }; 33 | } 34 | 35 | get selectedIndex() { 36 | return this.fanMode.source.map(item => item.id).indexOf(this._selected.id); 37 | } 38 | 39 | handleChange(e) { 40 | const { index } = e.detail; 41 | 42 | if (index === this.selectedIndex || !this.fanMode.source[index]) 43 | return; 44 | 45 | clearTimeout(this.timer); 46 | 47 | const selected = this.fanMode.source[index]; 48 | const { entity } = this.fanMode; 49 | const oldSelected = this._selected; 50 | this._selected = selected; 51 | 52 | this.timer = setTimeout(async () => { 53 | if (this.fanMode.entity === entity) { 54 | this._selected = oldSelected; 55 | this.requestUpdate('_selected'); 56 | } 57 | }, this.fanMode.actionTimeout); 58 | 59 | this.fanMode.handleChange(selected.id); 60 | 61 | this.requestUpdate('_selected'); 62 | } 63 | 64 | renderFanMode() { 65 | const label = this._selected ? this._selected.name : this.fanMode.state; 66 | const icon = this.config.secondary_info.icon ? this.config.secondary_info.icon 67 | : this.fanMode.icon; 68 | 69 | return html` 70 | 71 | ${label} 72 | `; 73 | } 74 | 75 | handleClick() { 76 | const menu = this.shadowRoot.querySelector('#menu'); 77 | menu.anchor = this.shadowRoot.querySelector('#button'); 78 | menu.show(); 79 | } 80 | 81 | renderFanModeDropdown() { 82 | return html` 83 |
84 | 89 | ${this.renderFanMode()} 90 | 91 | 97 | ${this.fanMode.source.map(item => html` 98 | 99 | ${item.name} 100 | `)} 101 | 102 |
103 | `; 104 | } 105 | 106 | render() { 107 | if (!ClimateFanModeSecondary.elementDefinitionsLoaded) { 108 | return html``; 109 | } 110 | 111 | const { type } = this.config.secondary_info; 112 | 113 | if (type === 'fan-mode-dropdown') { 114 | return this.renderFanModeDropdown(); 115 | } 116 | 117 | return this.renderFanMode(); 118 | } 119 | 120 | updated(changedProps) { 121 | if (changedProps.has('fanMode')) { 122 | clearTimeout(this.timer); 123 | this._selected = this.fanMode.selected; 124 | this.requestUpdate('_selected'); 125 | } 126 | } 127 | 128 | static get styles() { 129 | return [ 130 | sharedStyle, 131 | css` 132 | .mc-dropdown { 133 | padding: 0; 134 | } 135 | .name { 136 | font-size: calc(var(--mc-unit) * .35); 137 | font-weight: var(--mc-info-font-weight); 138 | line-height: calc(var(--mc-unit) * .5); 139 | vertical-align: middle; 140 | display: inline-block; 141 | } 142 | .icon { 143 | color: var(--mc-icon-color); 144 | height: calc(var(--mc-unit) * .475); 145 | width: calc(var(--mc-unit) * .5); 146 | min-width: calc(var(--mc-unit) * .5); 147 | --mdc-icon-size: calc(var(--mc-unit) * 0.5); 148 | } 149 | `]; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/style.js: -------------------------------------------------------------------------------- 1 | import { css } from 'lit'; 2 | 3 | const style = css` 4 | :host { 5 | overflow: visible !important; 6 | display: block; 7 | --mc-scale: var(--mini-climate-scale, 1); 8 | --mc-unit: calc(var(--mc-scale) * 40px); 9 | --mc-name-font-weight: var(--mini-climate-name-font-weight, 400); 10 | --mc-info-font-weight: var(--mini-climate-info-font-weight, 300); 11 | --mc-entity-info-left-offset: 8px; 12 | --mc-accent-color: var(--mini-climate-accent-color, var(--accent-color, #f39c12)); 13 | --mc-text-color: var(--mini-climate-base-color, var(--primary-text-color, #000)); 14 | --mc-active-color: var(--mc-accent-color); 15 | --mc-button-color: var(--mini-climate-button-color, var(--paper-item-icon-color, #44739e)); 16 | --mc-icon-color: 17 | var(--mini-climate-icon-color, 18 | var(--mini-climate-base-color, 19 | var(--paper-item-icon-color, #44739e))); 20 | --mc-icon-active-color: var(--state-binary_sensor-active-color, #ffc107); 21 | --mc-info-opacity: 1; 22 | --mc-bg-opacity: var(--mini-climate-background-opacity, 1); 23 | color: var(--mc-text-color); 24 | --mc-dropdown-unit: calc(var(--mc-unit) * .75); 25 | --paper-item-min-height: var(--mc-unit); 26 | --mdc-icon-button-size: calc(var(--mc-unit) * 0.75); 27 | } 28 | ha-card.--group { 29 | box-shadow: none; 30 | } 31 | ha-card.--bg { 32 | --mc-info-opacity: .75; 33 | } 34 | ha-card { 35 | cursor: default; 36 | display: flex; 37 | background: transparent; 38 | overflow: visible; 39 | padding: 0; 40 | position: relative; 41 | color: inherit; 42 | font-size: calc(var(--mc-unit) * 0.35); 43 | border: none; 44 | } 45 | ha-card:before { 46 | content: ''; 47 | padding-top: 0px; 48 | transition: padding-top .5s cubic-bezier(.21,.61,.35,1); 49 | will-change: padding-top; 50 | } 51 | header { 52 | display: none; 53 | } 54 | .mc__bg { 55 | background: var(--ha-card-background, var(--card-background-color, var(--paper-card-background-color, white))); 56 | position: absolute; 57 | top: 0; right: 0; bottom: 0; left: 0; 58 | overflow: hidden; 59 | -webkit-transform: translateZ(0); 60 | transform: translateZ(0); 61 | opacity: var(--mc-bg-opacity); 62 | box-shadow: var(--mini-climate-card-box-shadow, var(--ha-card-box-shadow, none)); 63 | box-sizing: border-box; 64 | border-radius: var(--ha-card-border-radius, 12px); 65 | border-width: var(--ha-card-border-width, 1px); 66 | border-style: solid; 67 | border-color: var(--ha-card-border-color, var(--divider-color, #e0e0e0) ); 68 | } 69 | ha-card.--group .mc__bg { 70 | background: none; 71 | border: none; 72 | } 73 | .mc-climate { 74 | align-self: flex-end; 75 | box-sizing: border-box; 76 | position: relative; 77 | padding: 16px 16px 0px 16px; 78 | transition: padding .25s ease-out; 79 | width: 100%; 80 | will-change: padding; 81 | } 82 | .flex { 83 | display: flex; 84 | display: -ms-flexbox; 85 | display: -webkit-flex; 86 | flex-direction: row; 87 | } 88 | .mc-climate__core { 89 | position: relative; 90 | padding-right: 5px; 91 | } 92 | .entity__info { 93 | user-select: none; 94 | margin-left: var(--mc-entity-info-left-offset); 95 | flex: 1; 96 | min-width: 0; 97 | white-space: nowrap; 98 | } 99 | .entity__icon { 100 | color: var(--mc-icon-color); 101 | white-space: nowrap; 102 | } 103 | .entity__icon[color] { 104 | color: var(--mc-icon-active-color); 105 | } 106 | .entity__icon { 107 | animation: fade-in .25s ease-out; 108 | background-position: center center; 109 | background-repeat: no-repeat; 110 | background-size: cover; 111 | border-radius: 100%; 112 | height: var(--mc-unit); 113 | width: var(--mc-unit); 114 | min-width: var(--mc-unit); 115 | line-height: var(--mc-unit); 116 | margin-right: calc(var(--mc-unit) / 5); 117 | position: relative; 118 | text-align: center; 119 | will-change: border-color; 120 | transition: border-color .25s ease-out; 121 | } 122 | .entity__info__name { 123 | overflow: hidden; 124 | text-overflow: ellipsis; 125 | white-space: nowrap; 126 | line-height: calc(var(--mc-unit) / 2); 127 | color: var(--mc-text-color); 128 | font-weight: var(--mc-name-font-weight); 129 | } 130 | .entity__secondary_info { 131 | margin-top: -2px; 132 | } 133 | ha-card.--initial .mc-climate { 134 | padding: 16px 16px 5px 16px; 135 | } 136 | ha-card.--unavailable .mc-climate { 137 | padding: 16px; 138 | } 139 | ha-card.--group .mc-climate { 140 | padding: 8px 0px 0px 0px; 141 | } 142 | .toggle-button { 143 | width: calc(var(--mc-unit) * .75); 144 | height: calc(var(--mc-unit) * .75); 145 | --mdc-icon-button-size: calc(var(--mc-unit) * .75); 146 | color: var(--mc-icon-color); 147 | margin-left: auto; 148 | margin-top: calc(var(--mc-unit) * -.125); 149 | margin-right: calc(var(--mc-unit) * .05); 150 | --ha-icon-display: flex; 151 | } 152 | .toggle-button.open { 153 | transform: rotate(180deg); 154 | color: var(--mc-active-color); 155 | } 156 | .wrap { 157 | display: flex; 158 | flex-direction: row; 159 | } 160 | .entity__controls { 161 | margin-left: auto; 162 | display: flex; 163 | white-space: nowrap; 164 | margin-top: calc(var(--mc-unit) * -.25); 165 | } 166 | .ctl-wrap { 167 | display: flex; 168 | flex-direction: row; 169 | margin-left: auto; 170 | margin-top: auto; 171 | margin-bottom: 0; 172 | --ha-icon-display: flex; 173 | } 174 | .bottom { 175 | margin-top: calc(var(--mc-unit) * .05); 176 | height: calc(var(--mc-unit) * .625);; 177 | } 178 | .entity__info__name_wrap { 179 | margin-right: 10px; 180 | max-width: calc(calc(var(--mc-card-width) - 191.3px) / 1.43); 181 | min-width: calc(var(--mc-unit) * 2.5); 182 | cursor: pointer; 183 | height: var(--mc-unit); 184 | } 185 | mc-buttons { 186 | width: 100%; 187 | justify-content: space-evenly; 188 | display: flex; 189 | --ha-icon-display: flex; 190 | } 191 | mc-temperature { 192 | min-width: 0; 193 | } 194 | .--unavailable .ctl-wrap { 195 | margin-left: auto; 196 | margin-top: auto; 197 | margin-bottom: auto; 198 | } 199 | .--unavailable .entity__info { 200 | margin-top: auto; 201 | margin-bottom: auto; 202 | } 203 | .mc-toggle_content { 204 | margin-top: calc(var(--mc-unit) * .05); 205 | } 206 | .ctl-wrap mc-dropdown, .ctl-wrap mc-button { 207 | min-width: calc(var(--mc-unit) * .75); 208 | margin-right: 3px; 209 | } 210 | .ctl-wrap mc-button { 211 | width: calc(var(--mc-unit) * 0.75); 212 | height: calc(var(--mc-unit) * 0.75); 213 | } 214 | `; 215 | 216 | export default style; 217 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { html, LitElement } from 'lit'; 2 | import ResizeObserver from 'resize-observer-polyfill'; 3 | import { classMap } from 'lit/directives/class-map'; 4 | import { styleMap } from 'lit/directives/style-map'; 5 | import { ScopedRegistryHost } from '@lit-labs/scoped-registry-mixin'; 6 | import style from './style'; 7 | import sharedStyle from './sharedStyle'; 8 | import handleClick from './utils/handleClick'; 9 | import getLabel from './utils/getLabel'; 10 | import './initialize'; 11 | 12 | import { compileTemplate, toggleState } from './utils/utils'; 13 | import TemperatureObject from './models/temperature'; 14 | import TargetTemperatureObject from './models/target-temperature'; 15 | import ButtonObject from './models/button'; 16 | import IndicatorObject from './models/indicator'; 17 | import ClimateObject from './models/climate'; 18 | import HvacModeObject from './models/hvac-mode'; 19 | import ICON from './const'; 20 | import ClimateTemperature from './components/temperature'; 21 | import ClimateTargetTemperature from './components/target-temperature'; 22 | import ClimateModeMenu from './components/mode-menu'; 23 | import ClimateIndicators from './components/indicators'; 24 | import ClimateDropDown from './components/dropdown'; 25 | import ClimateButtons from './components/buttons'; 26 | import ClimateButton from './components/button'; 27 | import ClimateSecondaryInfo from './components/secondary-info'; 28 | import buildElementDefinitions from './utils/buildElementDefinitions'; 29 | 30 | class MiniClimate extends ScopedRegistryHost(LitElement) { 31 | static get elementDefinitions() { 32 | return buildElementDefinitions([ 33 | 'ha-card', 34 | 'ha-icon', 35 | 'ha-icon-button', 36 | ClimateButton, 37 | ClimateButtons, 38 | ClimateDropDown, 39 | ClimateIndicators, 40 | ClimateModeMenu, 41 | ClimateSecondaryInfo, 42 | ClimateTargetTemperature, 43 | ClimateTemperature, 44 | ], MiniClimate); 45 | } 46 | 47 | static getStubConfig(hass, unusedEntities, allEntities) { 48 | let entity = unusedEntities.find(eid => eid.split('.')[0] === 'climate'); 49 | if (!entity) { 50 | entity = allEntities.find(eid => eid.split('.')[0] === 'climate'); 51 | } 52 | return { entity }; 53 | } 54 | 55 | constructor() { 56 | super(); 57 | this.initial = true; 58 | this.toggle = false; 59 | this.temperature = {}; 60 | this.targetTemperature = {}; 61 | this.swapTemperatures = false; 62 | this.buttons = {}; 63 | this.indicators = {}; 64 | this.hvacMode = {}; 65 | this.targetTemperatureChanging = false; 66 | this.climate = {}; 67 | this.targetTemperatureValue = 0; 68 | this.width = 0; 69 | } 70 | 71 | static get properties() { 72 | return { 73 | _hass: { type: Object }, 74 | config: { type: Object }, 75 | entity: { type: Object }, 76 | climate: { type: Object }, 77 | initial: { type: Boolean }, 78 | toggle: { type: Boolean }, 79 | }; 80 | } 81 | 82 | static get styles() { 83 | return [ 84 | sharedStyle, 85 | style, 86 | ]; 87 | } 88 | 89 | set hass(hass) { 90 | if (!hass) return; 91 | const entity = hass.states[this.config.entity]; 92 | this._hass = hass; 93 | let force = false; 94 | 95 | if (entity && this.entity !== entity) { 96 | this.entity = entity; 97 | this.climate = new ClimateObject(hass, this.config, entity); 98 | force = true; 99 | } 100 | 101 | this.updateIndicators(force); 102 | this.updateButtons(force); 103 | this.updateTemperature(force); 104 | this.updateTargetTemperature(force); 105 | this.updateHvacMode(force); 106 | 107 | this.climate.mode = this.hvacMode.selected; 108 | } 109 | 110 | get hass() { 111 | return this._hass; 112 | } 113 | 114 | get name() { 115 | return this.config.name || this.climate.name; 116 | } 117 | 118 | updateIndicators(force) { 119 | const indicators = { }; 120 | let changed = false; 121 | 122 | for (let i = 0; i < this.config.indicators.length; i += 1) { 123 | const config = this.config.indicators[i]; 124 | const { id } = config; 125 | 126 | const entityId = config.source.entity || this.climate.id; 127 | const entity = this.hass.states[entityId]; 128 | 129 | if (entity) { 130 | indicators[id] = new IndicatorObject(entity, config, this.climate, this.hass); 131 | } 132 | 133 | if (entity !== (this.indicators[id] && this.indicators[id].entity)) 134 | changed = true; 135 | } 136 | 137 | if (changed || force) 138 | this.indicators = indicators; 139 | } 140 | 141 | updateTemperature(force) { 142 | if (this.targetTemperatureChanging) 143 | return; 144 | 145 | const temperatureEntityId = this.config.temperature.source.entity || this.config.entity; 146 | const temperatureEntity = this.hass.states[temperatureEntityId]; 147 | 148 | const targetTemperatureEntityId = (this.config.target_temperature.source 149 | && this.config.target_temperature.source.entity) || this.config.entity; 150 | 151 | const targetTemperatureEntity = this.hass.states[targetTemperatureEntityId]; 152 | 153 | const temperature = new TemperatureObject(temperatureEntity, targetTemperatureEntity, 154 | this.config, this.climate); 155 | 156 | if (this.temperature.rawValue !== temperature.rawValue 157 | || this.temperature.target !== temperature.target || force) { 158 | this.temperature = temperature; 159 | } 160 | } 161 | 162 | updateTargetTemperature(force) { 163 | if (this.targetTemperatureChanging) 164 | return; 165 | 166 | const entityId = (this.config.target_temperature.source 167 | && this.config.target_temperature.source.entity) || this.config.entity; 168 | 169 | const entity = this.hass.states[entityId]; 170 | 171 | if (this.targetTemperature.entity !== entity || force) { 172 | this.targetTemperature = new TargetTemperatureObject(entity, this.config, this.hass); 173 | this.targetTemperatureValue = this.targetTemperature.value; 174 | } 175 | } 176 | 177 | updateHvacMode(force) { 178 | const config = this.config.hvac_mode; 179 | 180 | const entityId = (config.state && config.state.entity) || this.climate.id; 181 | const entity = this.hass.states[entityId]; 182 | 183 | if ((entity && entity !== (this.hvacMode && this.hvacMode.entity)) || force) { 184 | this.hvacMode = new HvacModeObject(entity, config, this.climate); 185 | } 186 | } 187 | 188 | updateButtons(force) { 189 | const buttons = { }; 190 | let changed = false; 191 | 192 | for (let i = 0; i < this.config.buttons.length; i += 1) { 193 | const config = this.config.buttons[i]; 194 | const { id } = config; 195 | 196 | const entityId = (config.state && config.state.entity) || this.climate.id; 197 | const entity = this.hass.states[entityId]; 198 | 199 | if (entity) { 200 | buttons[id] = new ButtonObject(entity, config, this.climate, this.hass); 201 | } 202 | 203 | if (entity !== (this.buttons[id] && this.buttons[id].entity)) 204 | changed = true; 205 | } 206 | 207 | if (changed || force) { 208 | this.buttons = buttons; 209 | } 210 | } 211 | 212 | getButtonsConfig(config) { 213 | const data = Object.entries(config.buttons || {}); 214 | 215 | const buttons = []; 216 | 217 | for (let i = 0; i < data.length; i += 1) { 218 | const [key, value] = data[i]; 219 | const button = this.getButtonConfig(value, config); 220 | button.id = key; 221 | 222 | if (!('order' in button)) 223 | button.order = i + 1; 224 | 225 | buttons.push(button); 226 | } 227 | 228 | return buttons; 229 | } 230 | 231 | getButtonConfig(value, config) { 232 | const item = { 233 | icon: 'mdi:radiobox-marked', 234 | type: 'button', 235 | toggle_action: undefined, 236 | ...value, 237 | }; 238 | 239 | item.functions = {}; 240 | 241 | const context = { ...value }; 242 | context.call_service = (domain, service, options) => this.hass.callService( 243 | domain, service, options, 244 | ); 245 | context.entity_config = config; 246 | context.toggle_state = toggleState; 247 | 248 | if (item.disabled) { 249 | item.functions.disabled = compileTemplate(item.disabled, context); 250 | } 251 | 252 | if (item.state && item.state.mapper) { 253 | item.functions.state = { mapper: compileTemplate(item.state.mapper, context) }; 254 | } 255 | 256 | if (item.active) { 257 | item.functions.active = compileTemplate(item.active, context); 258 | } 259 | 260 | if (item.source && item.source.__filter) { 261 | item.functions.source = { filter: compileTemplate(item.source.__filter, context) }; 262 | } 263 | 264 | if (item.toggle_action) { 265 | item.functions.toggle_action = compileTemplate(item.toggle_action, context); 266 | } 267 | 268 | if (item.change_action) { 269 | item.functions.change_action = compileTemplate(item.change_action, context); 270 | } 271 | 272 | if (item.style) 273 | item.functions.style = compileTemplate(item.style, context); 274 | 275 | if (item.hide) { 276 | if (typeof item.hide === 'boolean') { 277 | item.functions.hide = () => true; 278 | } else { 279 | item.functions.hide = compileTemplate(item.hide, context); 280 | } 281 | } 282 | 283 | return item; 284 | } 285 | 286 | getFanModeConfig(config) { 287 | let fanModeConfig = { 288 | id: 'fan_mode', 289 | icon: 'mdi:fan', 290 | type: 'dropdown', 291 | order: 0, 292 | state: { attribute: 'fan_mode' }, 293 | change_action: (selected, state, entity) => { 294 | const options = { fan_mode: selected, entity_id: entity.entity_id }; 295 | return this.call_service('climate', 'set_fan_mode', options); 296 | }, 297 | ...config.fan_mode || {}, 298 | }; 299 | 300 | fanModeConfig = this.getButtonConfig(fanModeConfig, config); 301 | const { functions } = fanModeConfig; 302 | 303 | if (!functions.active) 304 | functions.active = () => this.climate.isOn; 305 | 306 | return fanModeConfig; 307 | } 308 | 309 | getIndicatorConfig(key, value, config) { 310 | const item = { 311 | id: key, 312 | source: { enitity: undefined, attribute: undefined, mapper: undefined }, 313 | icon: '', 314 | ...value, 315 | }; 316 | 317 | if (typeof value.tap_action === 'string') 318 | item.tap_action = { action: value.tap_action }; 319 | else 320 | item.tap_action = { action: 'none', ...item.tap_action || {} }; 321 | 322 | item.functions = item.functions || {}; 323 | const context = { ...value }; 324 | context.entity_config = config; 325 | context.toggle_state = toggleState; 326 | 327 | if (item.source.mapper) 328 | item.functions.mapper = compileTemplate(item.source.mapper, context); 329 | 330 | if (typeof item.icon === 'object') { 331 | item.functions.icon = {}; 332 | 333 | if (item.icon.template) 334 | item.functions.icon.template = compileTemplate(item.icon.template, context); 335 | 336 | if (item.icon.style) 337 | item.functions.icon.style = compileTemplate(item.icon.style, context); 338 | } 339 | 340 | if (typeof item.value === 'object') { 341 | item.functions.value = {}; 342 | 343 | if (item.value.style) 344 | item.functions.value.style = compileTemplate(item.value.style, context); 345 | } 346 | 347 | if (item.hide) { 348 | if (typeof item.hide === 'boolean') { 349 | item.functions.hide = () => true; 350 | } else { 351 | item.functions.hide = compileTemplate(item.hide, context); 352 | } 353 | } 354 | 355 | return item; 356 | } 357 | 358 | getSecondaryInfoConfig(config) { 359 | const item = { 360 | ...config, 361 | }; 362 | 363 | item.functions = item.functions || {}; 364 | const context = { ...config }; 365 | 366 | if (item.hide) { 367 | if (typeof item.hide === 'boolean') { 368 | item.functions.hide = () => true; 369 | } else { 370 | item.functions.hide = compileTemplate(item.hide, context); 371 | } 372 | } 373 | 374 | return item; 375 | } 376 | 377 | getToggleConfig(config) { 378 | const item = { 379 | ...config, 380 | }; 381 | 382 | item.functions = item.functions || {}; 383 | const context = { ...config }; 384 | 385 | if (item.hide) { 386 | if (typeof item.hide === 'boolean') { 387 | item.functions.hide = () => true; 388 | } else { 389 | item.functions.hide = compileTemplate(item.hide, context); 390 | } 391 | } 392 | 393 | return item; 394 | } 395 | 396 | getIndicatorsConfig(config) { 397 | return Object.entries(config.indicators || {}) 398 | .map(i => this.getIndicatorConfig(i[0], i[1] || {}, config)); 399 | } 400 | 401 | getTargetTemperatureConfig(config) { 402 | const item = { 403 | source: { entity: undefined, attribute: 'temperature' }, 404 | ...config.target_temperature || {}, 405 | }; 406 | 407 | item.icons = { 408 | up: ICON.UP, 409 | down: ICON.DOWN, 410 | ...item.icons || {}, 411 | }; 412 | 413 | item.functions = {}; 414 | 415 | const context = { ...config.target_temperature || {} }; 416 | context.call_service = (domain, service, options) => this.hass.callService( 417 | domain, service, options, 418 | ); 419 | context.entity_config = config; 420 | context.toggle_state = toggleState; 421 | 422 | if (item.change_action) { 423 | item.functions.change_action = compileTemplate(item.change_action, context); 424 | } 425 | 426 | return item; 427 | } 428 | 429 | getHvacModeConfig(config) { 430 | let mode = { 431 | type: 'dropdown', 432 | change_action: (selected, entity) => { 433 | const options = { hvac_mode: selected, entity_id: entity.entity_id }; 434 | return this.call_service('climate', 'set_hvac_mode', options); 435 | }, 436 | ...config.hvac_mode || {}, 437 | }; 438 | 439 | mode = this.getButtonConfig(mode, this.config); 440 | 441 | const { functions } = mode; 442 | 443 | if (!functions.active) 444 | functions.active = () => this.climate.isOn; 445 | 446 | return mode; 447 | } 448 | 449 | setConfig(config) { 450 | const supportedDomains = ['climate', 'fan']; 451 | 452 | if (!config.entity || supportedDomains.includes(config.entity.split('.')[0]) === false) 453 | throw new Error(`Specify an entity from within domains: [${supportedDomains.join(', ')}].`); 454 | 455 | this.config = { 456 | tap_action: { 457 | action: 'more-info', 458 | navigation_path: '', 459 | url: '', 460 | entity: '', 461 | service: '', 462 | service_data: {}, 463 | }, 464 | ...config, 465 | }; 466 | 467 | this.config.indicators = this.getIndicatorsConfig(config); 468 | 469 | this.config.buttons = this.getButtonsConfig(config); 470 | 471 | this.fanModeConfig = this.getFanModeConfig(config); 472 | 473 | this.config.buttons.push(this.fanModeConfig); 474 | 475 | this.config.target_temperature = this.getTargetTemperatureConfig(config); 476 | 477 | this.config.temperature = { 478 | round: 1, 479 | source: { entity: undefined, attribute: 'current_temperature' }, 480 | ...config.temperature || {}, 481 | }; 482 | 483 | this.config.hvac_mode = this.getHvacModeConfig(this.config); 484 | 485 | this.config.toggle = this.getToggleConfig({ 486 | icon: ICON.TOGGLE, 487 | hide: false, 488 | default: false, 489 | ...config.toggle || {}, 490 | }); 491 | 492 | if (typeof config.secondary_info === 'string') { 493 | this.config.secondary_info = { type: config.secondary_info }; 494 | } else { 495 | this.config.secondary_info = { 496 | type: 'fan_mode', 497 | ...config.secondary_info || {}, 498 | }; 499 | } 500 | this.config.secondary_info = this.getSecondaryInfoConfig(this.config.secondary_info); 501 | 502 | this.toggle = this.config.toggle.default; 503 | 504 | this.swapTemperatures = !!this.config.swap_temperatures; 505 | } 506 | 507 | renderCtlWrap() { 508 | if (this.climate.isUnavailable) { 509 | return html` 510 | 511 | ${getLabel(this.hass, ['state.default.unavailable'], 'Unavailable')} 512 | 513 | `; 514 | } 515 | 516 | const buttons = Object.entries(this.buttons).map(b => b[1]) 517 | .filter(b => b.location === 'main' && !b.hide) 518 | .sort((a, b) => ((a.order > b.order) ? 1 : ((b.order > a.order) ? -1 : 0))); 519 | 520 | return html` 521 | ${buttons.map(button => (button.type === 'dropdown' 522 | ? html`` 523 | : html``))} 524 | ${this.hvacMode.hide 525 | ? '' 526 | : html``} 527 | 532 | 533 | `; 534 | } 535 | 536 | renderEntityControls() { 537 | if (this.climate.isUnavailable) 538 | return ''; 539 | 540 | return html` 541 |
542 | 545 | 546 |
547 | `; 548 | } 549 | 550 | render() { 551 | if (!MiniClimate.elementDefinitionsLoaded) { 552 | return html``; 553 | } 554 | 555 | const handle = this.config.secondary_info.type !== 'fan-mode-dropdown'; 556 | return html` 557 | 560 |
561 |
562 |
563 | ${this.renderIcon()} 564 |
565 |
566 |
this.handlePopup(e, handle)}> 567 | ${this.renderEntityName()} 568 |
569 |
570 | ${this.renderCtlWrap()} 571 |
572 |
573 | ${this.renderBottomPanel()} 574 |
575 | ${this.renderEntityControls()} 576 |
577 | ${this.renderTogglePanel()} 578 |
579 |
580 | `; 581 | } 582 | 583 | handleChangingTargetTemperature(e) { 584 | this.targetTemperatureValue = this.targetTemperature.value; 585 | this.targetTemperatureChanging = e.detail.changing; 586 | this.requestUpdate('targetTemperatureChanging'); 587 | } 588 | 589 | handlePopup(e, handle) { 590 | if (!handle) 591 | return; 592 | 593 | e.stopPropagation(); 594 | handleClick(this, this.hass, this.config.tap_action, this.climate.id); 595 | } 596 | 597 | handleToggle(e) { 598 | e.stopPropagation(); 599 | this.toggle = !this.toggle; 600 | } 601 | 602 | toggleButtonCls() { 603 | return this.toggle ? 'open' : ''; 604 | } 605 | 606 | renderIcon() { 607 | const state = this.climate.isActive; 608 | return html` 609 |
610 | 611 |
`; 612 | } 613 | 614 | renderTogglePanel() { 615 | if (!this.toggle) 616 | return ''; 617 | 618 | return html` 619 |
620 | 622 | 623 |
624 | `; 625 | } 626 | 627 | renderBottomPanel() { 628 | if (this.climate.isUnavailable) 629 | return ''; 630 | 631 | return html` 632 |
633 | 635 | 636 | ${this.renderToggleButton()} 637 |
638 | `; 639 | } 640 | 641 | renderToggleButton() { 642 | if (Object.entries(this.buttons) 643 | .map(entry => entry[1]) 644 | .filter(button => !button.hide && button.location !== 'main') 645 | .length === 0) 646 | return html``; 647 | 648 | if (this.config.toggle.functions.hide 649 | && this.config.toggle.functions.hide(this.climate.entity, this.climate.mode)) { 650 | return html``; 651 | } 652 | 653 | return html` 654 | this.handleToggle(e)}> 657 | 658 | 659 | `; 660 | } 661 | 662 | renderEntityName() { 663 | return html` 664 |
this.handlePopup(e, true)}> 665 | ${this.name} 666 |
667 | ${this.renderSecondaryInfo()} 668 | `; 669 | } 670 | 671 | renderSecondaryInfo() { 672 | if (this.climate.isUnavailable) 673 | return html``; 674 | 675 | if (this.config.secondary_info.functions.hide 676 | && this.config.secondary_info.functions.hide(this.climate.entity, this.climate.mode)) { 677 | return html``; 678 | } 679 | 680 | return html` 681 |
682 | 687 | 688 |
`; 689 | } 690 | 691 | computeIcon() { 692 | return this.config.icon ? this.config.icon : this.climate.icon || ICON.DEFAULT; 693 | } 694 | 695 | computeClasses({ config } = this) { 696 | return classMap({ 697 | '--initial': this.initial, 698 | '--collapse': config.collapse, 699 | '--group': config.group, 700 | '--more-info': config.tap_action !== 'none', 701 | '--inactive': !this.climate.isActive, 702 | '--unavailable': this.climate.isUnavailable, 703 | }); 704 | } 705 | 706 | computeStyles() { 707 | const { scale } = this.config; 708 | 709 | return styleMap({ 710 | ...(scale && { '--mc-unit': `${40 * scale}px` }), 711 | ...{ '--mc-card-width': `${this.width}px` }, 712 | }); 713 | } 714 | 715 | initDefaultFanModeSource() { 716 | const fanMode = this.fanModeConfig; 717 | const entries = Object.entries(fanMode.source || {}).filter(s => s[0] !== '__filter'); 718 | const { entity } = this.climate; 719 | 720 | if (entity && entries.length === 0 && entity.attributes && entity.attributes.fan_modes) { 721 | fanMode.source = { ...this.climate.defaultFanModes, ...fanMode.source || {} }; 722 | } 723 | } 724 | 725 | initDefaultHvacModeSource() { 726 | const hvacMode = this.config.hvac_mode; 727 | const entries = Object.entries(hvacMode.source || {}).filter(s => s[0] !== '__filter'); 728 | const { entity } = this.climate; 729 | 730 | if (entity && entries.length === 0) 731 | hvacMode.source = { ...this.climate.defaultHvacModes, ...hvacMode.source || {} }; 732 | } 733 | 734 | firstUpdated(changedProps) { 735 | super.firstUpdated(changedProps); 736 | 737 | if (changedProps.has('climate')) { 738 | this.initDefaultFanModeSource(); 739 | this.initDefaultHvacModeSource(); 740 | this.requestUpdate('climate'); 741 | } 742 | if (changedProps.has('targetTemperature')) { 743 | this.targetTemperatureValue = this.targetTemperature.value; 744 | this.requestUpdate('targetTemperatureValue'); 745 | } 746 | 747 | const ro = new ResizeObserver((entries) => { 748 | const item = entries.find(e => e.target === this); 749 | if (item && item.contentRect && this.width !== item.contentRect.width) { 750 | this.width = item.contentRect.width; 751 | this.requestUpdate('width'); 752 | } 753 | }); 754 | 755 | ro.observe(this); 756 | } 757 | } 758 | 759 | customElements.define('mini-climate', MiniClimate); 760 | window.customCards = window.customCards || []; 761 | window.customCards.push({ 762 | type: 'mini-climate', 763 | name: 'Mini Climate', 764 | preview: true, 765 | description: 'A custom climate card', 766 | documentationURL: 'https://github.com/artem-sedykh/mini-climate-card', 767 | }); 768 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mini Climate Card 2 | 3 | [![Last Version](https://img.shields.io/github/package-json/v/artem-sedykh/mini-climate-card?label.svg=release)](https://github.com/artem-sedykh/mini-climate-card/releases/latest) 4 | [![Build Status](https://travis-ci.com/artem-sedykh/mini-climate-card.svg?branch=master)](https://travis-ci.com/artem-sedykh/mini-climate-card) 5 | [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg)](https://github.com/hacs/integration) 6 | 7 | A minimalistic yet customizable climate card for [Home Assistant](https://home-assistant.io/) Lovelace UI. 8 | Please ⭐️ this repo if you find it useful 9 | 10 |

11 | card preview 12 |

13 | 14 | ## Notice 15 | v2 is only compatible from version 2022.11 onwards 16 | 17 | ## Install 18 | 19 | *This card is available in [HACS](https://github.com/hacs/integration) (Home Assistant Community Store)* 20 | 21 | ### Simple install 22 | 23 | 1. Download and copy `mini-climate-card-bundle.js` from the [latest release](https://github.com/artem-sedykh/mini-climate-card/releases/latest) into your `config/www` directory. 24 | 25 | 2. Add a reference to `mini-climate-card-bundle.js` inside your `ui-lovelace.yaml`. 26 | 27 | ```yaml 28 | resources: 29 | - url: /local/mini-climate-card-bundle.js?v=2.21 30 | type: module 31 | ``` 32 | 33 | ### CLI install 34 | 35 | 1. Move into your `config/www` directory 36 | 37 | 2. Grab `mini-climate-card-bundle.js` 38 | 39 | ```console 40 | $ wget https://github.com/artem-sedykh/mini-climate-card/releases/download/v2.2.1/mini-climate-card-bundle.js 41 | ``` 42 | 43 | 3. Add a reference to `mini-climate-card-bundle.js` inside your `ui-lovelace.yaml`. 44 | 45 | ```yaml 46 | resources: 47 | - url: /local/mini-climate-card-bundle.js?v=2.2.1 48 | type: module 49 | ``` 50 | 51 | ## Updating 52 | 1. Find your `mini-climate-card-bundle.js` file in `config/www` or wherever you ended up storing it. 53 | 54 | 2. Replace the local file with the latest one attached in the [latest release](https://github.com/artem-sedykh/mini-climate-card/releases/latest). 55 | 56 | 3. Add the new version number to the end of the cards reference url in your `ui-lovelace.yaml` like below. 57 | 58 | ```yaml 59 | resources: 60 | - url: /local/mini-climate-card-bundle.js?v=2.2.1 61 | type: module 62 | ``` 63 | 64 | *You may need to empty the browsers cache if you have problems loading the updated card.* 65 | 66 | ## Using the card 67 | 68 | ### Options 69 | 70 | #### Card options 71 | | Name | Type | Default | Since | Description | 72 | |-------------------------------------------|-------------------------------------|--------------|--------|---------------------------------------------------------------------------------------------------------------| 73 | | type | string | **required** | v1.0.1 | `custom:mini-climate` | 74 | | entity | string | **required** | v1.0.1 | An entity_id from an entity within the `climate` domain | 75 | | name | string | optional | v1.0.1 | Override the entities friendly name | 76 | | group | boolean | optional | v1.0.2 | Removes border, paddings, background color and box-shadow | 77 | | icon | string | optional | v1.0.1 | Specify a custom icon from any of the available mdi icons | 78 | | swap_temperatures | boolean | optional | V2.1.1 | Swap the current and the target temperature in the card | 79 | | hide_current_temperature | boolean | optional | V2.1.2 | Hide the current temperature in the card | 80 | | hide_current_temperature | function | optional | V2.5.0 | Custom hide the current temperature in the card function | 81 | | **toggle** | object | optional | v1.0.2 | Show/hide bottom buttons toggle button | 82 | | toggle: `icon` | string | optional | v1.0.2 | Custom icon, default value `mdi:dots-horizontal` | 83 | | toggle: `hide` | boolean | optional | v1.0.2 | Hide toggle button, default value `False` | 84 | | toggle: `hide` | function | optional | v2.5.0 | Custom hide toggle button function | 85 | | toggle: `default` | boolean | optional | v1.0.2 | Default toggle button state, default value `False` | 86 | | **secondary_info** | object | optional | v1.1.0 | secondary_info config. [secondary info examples](#secondary-info) | 87 | | secondary_info: `type` | string | optional | v1.1.0 | Available types: `last-changed, last-updated (v2.2.0), fan-mode, fan-mode-dropdown, hvac-mode, hvac-action` | 88 | | secondary_info: `icon` | string | optional | v1.1.0 | Icon for types: `fan-mode, fan-mode-dropdown, hvac-mode`, `hvac-action` | 89 | | secondary_info: `hide` | boolean | optional | v2.3.0 | Hide secondary_info, default value `False` | 90 | | secondary_info: `hide` | function | optional | v2.5.0 | Custom hide secondary_info function. | 91 | | secondary_info: `source` | object | optional | v1.2.1 | Source available types: `hvac-action` | 92 | | secondary_info: `source:{item_name}` | object | optional | v1.2.1 | Source item name | 93 | | secondary_info: `source:{item_name}:icon` | object | optional | v1.2.1 | Specify a custom icon from any of the available mdi icons | 94 | | secondary_info: `source:{item_name}:name` | object | optional | v1.2.1 | Display name | 95 | | **temperature** | object | optional | v1.0.1 | Current temperature configuration. [temperature examples](#temperature) | 96 | | temperature: `unit` | string | optional | v1.0.1 | Display unit, default `°C` | 97 | | temperature: `round or fixed` | number | optional | v1.2.2 | Rounding or fixed value, default `round: 1` | 98 | | temperature: `source` | object | optional | v1.0.1 | Data source for target temperature | 99 | | temperature: `source:entity` | string | optional | v1.0.1 | entity_id, default current climate entity_id | 100 | | temperature: `source:attribute` | string | optional | v1.0.1 | Default `current_temperature` | 101 | | **target_temperature** | object | optional | v1.0.1 | Target temperature configuration. [target_temperature examples](#target_temperature) | 102 | | target_temperature: `icons` | object | optional | v1.0.1 | Icons for temperature change buttons | 103 | | target_temperature: `icons:up` | string | optional | v1.0.1 | Up icon, default `mdi:chevron-up` | 104 | | target_temperature: `icons:down` | string | optional | v1.0.1 | Down icon, default `mdi:chevron-down` | 105 | | target_temperature: `unit` | string | optional | v1.0.1 | Display unit, default `°C` | 106 | | target_temperature: `min` | number | optional | v1.0.1 | Minimum temperature, the default value is taken from the attribute `min_temp` of the given entity | 107 | | target_temperature: `max` | number | optional | v1.0.1 | Maximum temperature, the default value is taken from the attribute `max_temp` of the given entity | 108 | | target_temperature: `step` | number | optional | v1.0.1 | Temperature change step, the default value is taken from the attribute `target_temp_step` of the given entity | 109 | | target_temperature: `source` | object | optional | v1.0.1 | Data source for target temperature | 110 | | target_temperature: `source:entity` | string | optional | v1.0.1 | entity_id, default current climate entity_id | 111 | | target_temperature: `source:attribute` | string | optional | v1.0.1 | Default `temperature` | 112 | | target_temperature: `change_action` | function | optional | v1.0.1 | Custom temperature change function | 113 | | **hvac_mode** | object | optional | v1.0.1 | HVAC mode. [hvac_mode examples](#hvac_mode) | 114 | | hvac_mode: `style` | function | optional | v1.0.1 | Custom style | 115 | | hvac_mode: `change_action` | function | optional | v1.0.1 | Custom hvac_mode change function | 116 | | hvac_mode: `state` | object | optional | v1.0.1 | Config to get hvac_mode state | 117 | | hvac_mode: `hide` | boolean | optional | v1.2.3 | Hide hvac_mode, default value `False` | 118 | | hvac_mode: `hide` | function | optional | v2.5.0 | Custom hide hvac_mode function | 119 | | hvac_mode: `state:entity` | string | optional | v1.1.0 | hvac_mode entity_id | 120 | | hvac_mode: `state:attribute` | string | optional | v1.1.0 | hvac_mode attribute | 121 | | hvac_mode: `state:mapper` | function | optional | v1.1.0 | State processing function | 122 | | hvac_mode: `active` | function | optional | v1.1.0 | Active function | 123 | | hvac_mode: `source` | object | optional | v1.0.1 | Data | 124 | | hvac_mode: `source:__filter` | function | optional | v1.1.0 | Filter function | 125 | | hvac_mode: `source:item` | object | optional | v1.0.1 | `item` - mode name e.g. cool, heat, off, etc. | 126 | | hvac_mode: `source:item:icon` | string | optional | v1.0.1 | Specify a custom icon from any of the available mdi icons | 127 | | hvac_mode: `source:item:name` | string | optional | v1.0.1 | Display name | 128 | | hvac_mode: `source:item:hide` | boolean | optional | v2.5.0 | Hide source, default value `False` | 129 | | hvac_mode: `source:item:order` | number | optional | v1.2.5 | Sort order | 130 | | **fan_mode** | object | optional | v1.0.1 | Fan operation for climate device. [fan_mode examples](#fan_mode) | 131 | | fan_mode: `icon` | string | optional | v1.0.1 | Specify a custom icon from any of the available mdi icons | 132 | | fan_mode: `order` | number | optional | v1.0.1 | Sort order, default value `0` | 133 | | fan_mode: `location` | string | optional | v1.0.1 | Allows you to display buttons on the main panel, types `main, bottom`, default `bottom` | 134 | | fan_mode: `hide` | number | optional | v1.0.1 | Hide button, default value `False` | 135 | | fan_mode: `hide` | function | optional | v2.5.0 | Custom hide button function | 136 | | fan_mode: `style` | function | optional | v1.0.1 | Style | 137 | | fan_mode: `disabled` | function | optional | v1.0.1 | Disabled function | 138 | | fan_mode: `active` | function | optional | v1.0.1 | Active | 139 | | fan_mode: `change_action` | function | optional | v1.0.1 | Custom fan_mode change function | 140 | | fan_mode: `state` | object | optional | v1.0.1 | Config to get fan_mode state | 141 | | fan_mode: `state:entity` | string | optional | v1.0.1 | fan_mode entity_id | 142 | | fan_mode: `state:attribute` | string | optional | v1.0.1 | fan_mode attribute, default `fan_mode` | 143 | | fan_mode: `source` | object | optional | v1.0.1 | Source for drop down list | 144 | | fan_mode: `source:item` | string | optional | v1.0.1 | `item` - mode name e.g. auto, low, medium... | 145 | | fan_mode: `source:__filter` | function | optional | v1.0.1 | Source filter | 146 | | **indicators** | object | optional | v1.0.1 | Any indicators, [examples](#indicators) | 147 | | indicators: `name` | object | optional | v1.0.1 | The name of your indicator see [examples](#indicators) | 148 | | indicators: `name:icon` | string | optional | v1.0.1 | Specify a custom icon from any of the available mdi icons | 149 | | indicators: `name:icon` | object | optional | v1.0.1 | Icon object | 150 | | indicators: `name:icon:template` | function | optional | v1.0.1 | Icon template function | 151 | | indicators: `name:icon:style` | function | optional | v1.0.1 | Styles | 152 | | indicators: `name:value` | object | optional | v1.0.1 | Value object | 153 | | indicators: `name:value:style` | function | optional | v1.0.1 | Styles | 154 | | indicators: `name:unit` | string | optional | v1.0.1 | Display unit | 155 | | indicators: `name:unit` | string | optional | v1.0.1 | Display unit | 156 | | indicators: `name:round` | number | optional | v1.0.1 | Rounding number value | 157 | | indicators: `name:hide` | boolean | optional | v2.5.0 | Hide indicator, default value `False` | 158 | | indicators: `name:hide` | function | optional | v2.5.0 | Custom hide indicator function | 159 | | indicators: `name:source` | number | optional | v1.0.1 | Data source | 160 | | indicators: `name:source:entity` | string | optional | v1.0.1 | Indicator entity_id | 161 | | indicators: `name:source:attribute` | string | optional | v1.0.1 | Entity attribute | 162 | | indicators: `name:source:mapper` | function | optional | v1.0.1 | Value processing function | 163 | | indicators: `name:tap_action` | [action object](#tap-action-object) | true | v1.1.0 | Action on click/tap | 164 | | **buttons** | object | optional | v1.0.1 | Any buttons, [example](#buttons) | 165 | | buttons: `name` | object | optional | v1.0.1 | The name of your button see examples | 166 | | buttons: `name:icon` | string | optional | v1.0.1 | Specify a custom icon from any of the available mdi icons | 167 | | buttons: `name:type` | string | optional | v1.0.1 | `dropdown` or `button` default `button` | 168 | | buttons: `name:order` | number | optional | v1.0.1 | Sort order | 169 | | buttons: `name:location` | string | optional | v1.2.1 | Allows you to display buttons on the main panel, types `main, bottom`, default `bottom` | 170 | | buttons: `name:state` | object | optional | v1.0.1 | Config to get button state | 171 | | buttons: `name:state:entity` | string | optional | v1.0.1 | Button entity_id | 172 | | buttons: `name:state:attribute` | string | optional | v1.0.1 | Entity attribute | 173 | | buttons: `name:state:mapper` | function | optional | v1.0.1 | State processing function | 174 | | buttons: `name:disabled` | function | optional | v1.0.1 | Calc disabled button | 175 | | buttons: `name:hide` | boolean | optional | v2.5.0 | Hide button, default value `False` | 176 | | buttons: `name:hide` | function | optional | v2.5.0 | Custom hide button function | 177 | | buttons: `name:active` | function | optional | v1.0.1 | For type `dropdown` | 178 | | buttons: `name:source` | object | optional | v1.0.1 | For type `dropdown` | 179 | | buttons: `name:source:item` | string | optional | v1.0.1 | Source item, format horizontal: horizontal | 180 | | buttons: `name:source:__filter` | function | optional | v1.0.1 | Filter function | 181 | | buttons: `name:change_action` | function | optional | v1.0.1 | For type `dropdown` | 182 | | buttons: `name:toggle_action` | function | optional | v1.0.1 | For type `button` | 183 | | buttons: `name:style` | function | optional | v1.0.1 | Styles | 184 | | tap_action | [action object](#tap-action-object) | true | v1.0.4 | Action on click/tap, [tap_action](#tap-action-example) | 185 | | scale | number | optional | v1.0.1 | UI scale modifier, default is `1` | 186 | 187 | #### temperature 188 | 189 | > Functions available: 190 | 191 | | Name | Type | execution context | arguments | return type | 192 | |----------------------------|----------|-------------------|---------------------------------------------------------|-------------| 193 | | `hide_current_temperature` | function | | value, entity, target_entity, climate_entity, hvac_mode | boolean | 194 | 195 | `value` - temperature value 196 | `entity` - temperature entity 197 | `target_entity` - target temperature entity 198 | `climate_entity` - climate entity 199 | `hvac_mode` - current hvac_mode 200 | 201 | > Configuration example for the temperature: 202 | ```yaml 203 | type: custom:mini-climate 204 | entity: climate.my_ac 205 | hide_current_temperature: > 206 | (value) => value < 20 207 | temperature: 208 | unit: '°C' 209 | round: 1 210 | # use an external temperature sensor 211 | source: 212 | entity: sensor.temperature 213 | ``` 214 | 215 | #### target_temperature 216 | 217 | > Functions available for the target_temperature: 218 | 219 | | Name | Type | execution context | arguments | return type | 220 | |-----------------|----------|---------------------------|-------------------------------|-------------| 221 | | `change_action` | function | target_temperature config | value, entity, climate_entity | promise | 222 | 223 | `value` - target_temperature value 224 | `entity` - target_temperature entity 225 | `climate_entity` - climate entity 226 | 227 | **execution context methods:** 228 | 229 | | Name | arguments | description | return type | 230 | |----------------|---------------------------|---------------------------------------------------------|-------------| 231 | | `toggle_state` | state | toggle state, example: `this.toggle_state('on') => off` | string | 232 | | `call_service` | domain, service, options, | call Home Assistant service | promise | 233 | 234 | > Configuration example for the target_temperature: 235 | ```yaml 236 | type: custom:mini-climate 237 | entity: climate.my_ac 238 | target_temperature: 239 | icons: 240 | up: mdi:chevron-up 241 | down: mdi:chevron-down 242 | unit: '°C' 243 | min: 16 244 | max: 31 245 | step: 0.5 246 | change_action: > 247 | (value, entity) => this.call_service('climate', 'set_temperature', { entity_id: entity.entity_id, temperature: value }) 248 | ``` 249 | 250 | #### hvac_mode 251 | 252 | > Functions available for the hvac_mode: 253 | 254 | | Name | Type | execution context | arguments | return type | 255 | |-------------------|----------|-------------------|---------------------------------------|--------------------------------------| 256 | | `state:mapper` | function | hvac_mode config | state, entity, climate_entity | any | 257 | | `active` | function | hvac_mode config | state, entity, climate_entity | boolean | 258 | | `change_action` | function | hvac_mode config | selected, entity, climate_entity | any | 259 | | `style` | function | hvac_mode config | value, entity, climate_entity | object | 260 | | `source:__filter` | function | hvac_mode config | source, state, entity, climate_entity | object({ id..., name...,... }) array | 261 | | `hide` | function | hvac_mode config | state, entity, climate_entity | boolean | 262 | 263 | `state` - current hvac state 264 | `selected` - selected value 265 | `entity` - hvac entity 266 | `climate_entity` - current climate entity 267 | 268 | **execution context methods:** 269 | 270 | | Name | arguments | description | return type | 271 | |----------------|---------------------------|---------------------------------------------------------|-------------| 272 | | `toggle_state` | state | toggle state, example: `this.toggle_state('on') => off` | string | 273 | | `call_service` | domain, service, options, | call Home Assistant service | promise | 274 | 275 | > Configuration example for the hvac_mode: 276 | ```yaml 277 | type: custom:mini-climate 278 | entity: climate.my_ac 279 | hvac_mode: 280 | style: "(value, entity) => ({ color: 'black !important' })" 281 | hide: > 282 | (state) => state === 'dry' 283 | source: 284 | 'off': 285 | icon: mdi:power 286 | name: 'off' 287 | heat: 288 | icon: mdi:weather-sunny 289 | name: heat 290 | auto: 291 | icon: mdi:cached 292 | name: auto 293 | cool: 294 | icon: mdi:snowflake 295 | name: cool 296 | dry: 297 | icon: mdi:water 298 | name: dry 299 | fan_only: 300 | icon: mdi:fan 301 | name: fan 302 | change_action: > 303 | (selected, entity) => this.call_service('climate', 'set_hvac_mode', { entity_id: entity.entity_id, hvac_mode: selected }) 304 | ``` 305 | 306 | #### fan_mode 307 | 308 | > Functions available for the fan_mode: 309 | 310 | | Name | Type | execution context | arguments | return type | 311 | |-------------------|----------|-------------------|---------------------------------------------------|----------------------------------| 312 | | `state:mapper` | function | button config | state, entity, climate_entity, hvac_mode | any | 313 | | `source:__filter` | function | button config | source, state, entity, climate_entity, hvac_mode | object({ id..., name... }) array | 314 | | `active` | function | button config | value, entity, climate_entity, hvac_mode | boolean | 315 | | `disabled` | function | button config | value, entity, climate_entity, hvac_mode | boolean | 316 | | `style` | function | button config | value, entity, climate_entity, hvac_mode | object | 317 | | `change_action` | function | button config | selected_value, entity, climate_entity, hvac_mode | promise | 318 | | `hide` | function | button config | state, entity, climate_entity, hvac_mode | boolean | 319 | 320 | `state` - current button state value 321 | `entity` - button entity 322 | `climate_entity` - climate entity 323 | `hvac_mode` - current hvac_mode 324 | `source` - dropdown source object array: [ { id: 'id', name: 'name' }, ... ] 325 | `selected_value` - selected dropdown value 326 | 327 | **execution context methods:** 328 | 329 | | Name | arguments | description | return type | 330 | |----------------|---------------------------|---------------------------------------------------------|-------------| 331 | | `toggle_state` | sate | toggle state, example: `this.toggle_state('on') => off` | string | 332 | | `call_service` | domain, service, options, | call Home Assistant service | promise | 333 | 334 | > Configuration example for the fan_mode: 335 | ```yaml 336 | type: custom:mini-climate 337 | entity: climate.my_ac 338 | fan_mode: 339 | hide: > 340 | (state) => state === 'low' 341 | icon: mdi:fan 342 | order: 0 343 | active: (state, entity) => entity.state !== 'off' 344 | source: 345 | auto: auto 346 | low: low 347 | medium: medium 348 | high: high 349 | # filter usage example 350 | __filter: > 351 | (source, state, entity) => entity.attributes 352 | .fan_modes_al.map(fan_mode => source.find(s => s.id === fan_mode)) 353 | .filter(fan_mode=>fan_mode) 354 | change_action: > 355 | (selected, state, entity) => this.call_service('climate', 'set_fan_mode', { entity_id: entity.entity_id, fan_mode: selected }) 356 | ``` 357 | #### Indicators 358 | 359 | > The indicators display additional information on the card, for example, you can display humidity, consumption, etc. 360 | > Adding a simple indicator: 361 | ```yaml 362 | type: custom:mini-climate 363 | entity: climate.my_ac 364 | indicators: 365 | humidity: 366 | icon: mdi:water 367 | unit: '%' 368 | round: 1 369 | source: 370 | entity: sensor.humidity 371 | ``` 372 | 373 | ##### indicator functions 374 | 375 | > Consider configuring an indicator using javascript 376 | > Functions available for the indicator: 377 | 378 | | Name | Type | execution context | arguments | return type | 379 | |-----------------|----------|-------------------|------------------------------------------|-------------| 380 | | `source:mapper` | function | indicator config | value, entity, climate_entity, hvac_mode | any | 381 | | `icon:template` | function | indicator config | value, entity, climate_entity, hvac_mode | string | 382 | | `icon:style` | function | indicator config | value, entity, climate_entity, hvac_mode | object | 383 | | `value:style` | function | indicator config | value, entity, climate_entity, hvac_mode | object | 384 | | `hide` | function | indicator config | value, entity, climate_entity, hvac_mode | boolean | 385 | 386 | `value` - current indicator value 387 | `entity` - indicator entity 388 | `climate_entity` - climate entity 389 | `hvac_mode` - current hvac_mode 390 | 391 | ##### source mapper 392 | 393 | > Using the mapper function, you can change the indicator value: 394 | ```yaml 395 | type: custom:mini-climate 396 | entity: climate.my_ac 397 | indicators: 398 | power: 399 | icon: mdi:power-plug 400 | source: 401 | values: 402 | 'on': 'power is on!' 403 | 'off': 'power is off!' 404 | entity: switch.ac_power 405 | # since the current execution context is an indicator config, we can use this.source.values to get values 406 | mapper: value => this.source.values[value] 407 | # example of using all function arguments 408 | # mapper: > 409 | # (value, entity, climate_entity, hvac_mode) => { 410 | # console.log(value); 411 | # console.log(entity); 412 | # console.log(climate_entity); 413 | # console.log(hvac_mode); 414 | # console.log(this); 415 | # return ... 416 | # } 417 | ``` 418 | 419 | ##### icon template 420 | 421 | > The indicator icon can be calculated dynamically 422 | for example: 423 | ```yaml 424 | type: custom:mini-climate 425 | entity: climate.my_ac 426 | indicators: 427 | humidity: 428 | icon: 429 | template: > 430 | (value) => (value > 30 ? 'mdi:weather-rainy' : 'mdi:water') 431 | unit: '%' 432 | round: 1 433 | source: 434 | entity: sensor.humidity 435 | ``` 436 | 437 | ##### icon style 438 | 439 | > You can also set custom styles. 440 | for example: 441 | ```yaml 442 | type: custom:mini-climate 443 | entity: climate.my_ac 444 | indicators: 445 | humidity: 446 | icon: 447 | template: () => 'mdi:water' 448 | style: > 449 | (value) => (value > 30 ? { color: 'red'} : {}) 450 | unit: '%' 451 | round: 1 452 | source: 453 | entity: sensor.humidity 454 | ``` 455 | 456 | ##### value style 457 | 458 | > You can also set custom styles. 459 | for example: 460 | ```yaml 461 | type: custom:mini-climate 462 | entity: climate.my_ac 463 | indicators: 464 | humidity: 465 | value: 466 | style: > 467 | (value) => (value > 30 ? { color: 'red'} : {}) 468 | unit: '%' 469 | round: 1 470 | source: 471 | entity: sensor.humidity 472 | ``` 473 | 474 | ##### Hide 475 | 476 | > You can also hide based on state. 477 | for example: 478 | ```yaml 479 | type: custom:mini-climate 480 | entity: climate.my_ac 481 | indicators: 482 | humidity: 483 | hide: > 484 | (value) => value < 20 485 | unit: '%' 486 | round: 1 487 | source: 488 | entity: sensor.humidity 489 | ``` 490 | 491 | #### Buttons 492 | 493 | > You can add various buttons, supported types: button and dropdown 494 | 495 | ##### buttons functions 496 | 497 | | Name | Type | execution context | arguments | return type | 498 | |-------------------|----------|-------------------|---------------------------------------------------|----------------------------------| 499 | | `state:mapper` | function | button config | state, entity, climate_entity, hvac_mode | any | 500 | | `source:__filter` | function | button config | source, state, entity, climate_entity, hvac_mode | object({ id..., name... }) array | 501 | | `active` | function | button config | value, entity, climate_entity, hvac_mode | boolean | 502 | | `disabled` | function | button config | value, entity, climate_entity, hvac_mode | boolean | 503 | | `style` | function | button config | value, entity, climate_entity, hvac_mode | object | 504 | | `toggle_action` | function | button config | state, entity, climate_entity, hvac_mode | promise | 505 | | `change_action` | function | button config | selected_value, entity, climate_entity, hvac_mode | promise | 506 | | `hide` | function | button config | state, entity, climate_entity, hvac_mode | boolean | 507 | 508 | `state` - current button state value 509 | `entity` - button entity 510 | `climate_entity` - climate entity 511 | `hvac_mode` - current hvac_mode 512 | `source` - dropdown source object array: [ { id: 'id', name: 'name' }, ... ] 513 | `selected_value` - selected dropdown value 514 | 515 | **execution context methods:** 516 | 517 | | Name | arguments | description | return type | 518 | |----------------|---------------------------|---------------------------------------------------------|-------------| 519 | | `toggle_state` | sate | toggle state, example: `this.toggle_state('on') => off` | string | 520 | | `call_service` | domain, service, options, | call Home Assistant service | promise | 521 | 522 | ##### dropdown 523 | > Consider an example swing_mode configuration: 524 | 525 | ```yaml 526 | type: custom:mini-climate 527 | entity: climate.my_ac 528 | buttons: 529 | swing_mode: 530 | type: dropdown 531 | icon: mdi:approximately-equal 532 | state: 533 | attribute: swing_mode 534 | active: state => state !== 'off' 535 | source: 536 | 'off': Off 537 | horizontal: On 538 | change_action: > 539 | (selected, state, entity) => this.call_service('climate', 'set_swing_mode', { entity_id: entity.entity_id, swing_mode: selected }) 540 | ``` 541 | 542 | ##### button 543 | > Consider the example of adding buttons: 544 | ```yaml 545 | type: custom:mini-climate 546 | entity: climate.my_ac 547 | buttons: 548 | power: 549 | icon: mdi:power-plug 550 | state: 551 | entity: switch.ac_power 552 | # for the button type, if no toggle_action is specified, the switch.toggle method is called 553 | ``` 554 | 555 | ```yaml 556 | type: custom:mini-climate 557 | entity: climate.my_ac 558 | buttons: 559 | turbo: 560 | icon: mdi:weather-hurricane 561 | hide: > 562 | (state, entity) => !entity.attributes.turbo_al 563 | state: 564 | attribute: turbo 565 | mapper: "state => (state ? 'on': 'off')" 566 | disabled: (state, entity) => !entity.attributes.turbo_al 567 | toggle_action: > 568 | (state) => this.call_service('mqtt', 'publish', { payload: this.toggle_state(state), topic: 'my_ac/turbo/set', retain: false, qos: 1 }) 569 | ``` 570 | 571 | #### tap action object 572 | 573 | | Name | Type | Default | Options | Description | 574 | |-----------------|:------:|:-----------:|:-----------------------------------------------------------------------------:|-----------------------------------------------------------------------------------| 575 | | action | string | `more-info` | `more-info` / `navigate` / `call-service` / `fire-dom-event` / `url` / `none` | Action to perform. | 576 | | entity | string | | Any entity id | Override default entity of `more-info`, when `action` is defined as `more-info`. | 577 | | service | string | | Any service | Service to call (e.g. `fan.turn_on`) when `action` is defined as `call-service` | 578 | | service_data | object | | Any service data | Service data to include with the service call. | 579 | | navigation_path | string | | Any path | Path to navigate to (e.g. `/lovelace/0/`) when `action` is defined as `navigate`. | 580 | | url | string | | Any URL | URL to open when `action` is defined as `url`. | 581 | 582 | #### tap action example 583 | ```yaml 584 | # toggle example 585 | # call-service example 586 | type: custom:mini-climate 587 | entity: climate.my_ac 588 | tap_action: 589 | action: call-service 590 | service: climate.set_hvac_mode 591 | service_data: 592 | entity_id: climate.my_ac 593 | hvac_mode: 'off' 594 | 595 | # fire-dom-event + browser mod example 596 | type: custom:mini-climate 597 | entity: climate.my_ac 598 | tap_action: 599 | action: fire-dom-event 600 | browser_mod: 601 | service: browser_mod.popup 602 | data: 603 | title: My title 604 | content: test 605 | 606 | # navigate example 607 | type: custom:mini-climate 608 | entity: climate.my_ac 609 | tap_action: 610 | action: navigate 611 | navigation_path: '/lovelace/4' 612 | 613 | # navigate example 614 | type: custom:mini-climate 615 | entity: climate.my_ac 616 | tap_action: 617 | action: url 618 | url: 'https://www.google.com/' 619 | 620 | # none example 621 | type: custom:mini-climate 622 | entity: climate.my_ac 623 | tap_action: none 624 | 625 | # more-info for custom entity example 626 | type: custom:mini-climate 627 | entity: climate.my_ac 628 | tap_action: 629 | action: more-info 630 | entity: sensor.humidity 631 | ``` 632 | 633 | #### secondary info 634 | 635 | ##### secondary info functions 636 | 637 | | Name | Type | execution context | arguments | return type | 638 | |--------|----------|-----------------------|---------------------------|-------------| 639 | | `hide` | function | secondary info config | climate_entity, hvac_mode | boolean | 640 | 641 | `climate_entity` - climate entity 642 | `hvac_mode` - current hvac_mode 643 | 644 | ```yaml 645 | type: custom:mini-climate 646 | entity: climate.dahatsu 647 | secondary_info: last-changed 648 | 649 | type: custom:mini-climate 650 | entity: climate.dahatsu 651 | secondary_info: 652 | type: fan-mode 653 | icon: 'mdi:fan' 654 | hide: > 655 | (climate_entity) => !climate_entity.attributes.turbo_al 656 | 657 | type: custom:mini-climate 658 | entity: climate.dahatsu 659 | secondary_info: hvac-mode 660 | ``` 661 | 662 | #### toggle 663 | 664 | ##### toggle functions 665 | 666 | | Name | Type | execution context | arguments | return type | 667 | |--------|----------|-------------------|---------------------------|-------------| 668 | | `hide` | function | toggle config | climate_entity, hvac_mode | boolean | 669 | 670 | `climate_entity` - climate entity 671 | `hvac_mode` - current hvac_mode 672 | 673 | ```yaml 674 | type: custom:mini-climate 675 | entity: climate.dahatsu 676 | toggle: 677 | default: true 678 | icon: 'mdi:fan' 679 | hide: > 680 | (climate_entity) => !climate_entity.attributes.turbo_al 681 | ``` 682 | 683 | ##### hvac-action type 684 | 685 | By default, translations from [ha frontend](https://github.com/home-assistant/frontend/blob/master/translations/frontend/en.json#L33) 686 | ```yaml 687 | type: custom:mini-climate 688 | entity: climate.dahatsu 689 | secondary_info: 690 | type: hvac-action 691 | ``` 692 | but you can customize your translations 693 | ```yaml 694 | type: custom:mini-climate 695 | entity: climate.dahatsu 696 | secondary_info: 697 | type: hvac-action 698 | source: 699 | cooling: Охлаждение 700 | ``` 701 | You can set your own icon for each hvac-action 702 | ```yaml 703 | type: custom:mini-climate 704 | entity: climate.dahatsu 705 | secondary_info: 706 | type: hvac-action 707 | source: 708 | cooling: 709 | icon: 'mdi:snowflake' 710 | name: Охлаждение 711 | ``` 712 | You can set your own icon for each hvac-action 713 | ```yaml 714 | type: custom:mini-climate 715 | entity: climate.dahatsu 716 | secondary_info: 717 | type: hvac-action 718 | source: 719 | cooling: 720 | icon: 'mdi:snowflake' 721 | name: Охлаждение 722 | ``` 723 | Or you can use one permanent icon 724 | ```yaml 725 | type: custom:mini-climate 726 | entity: climate.dahatsu 727 | secondary_info: 728 | type: hvac-action 729 | icon: 'mdi:cached' 730 | ``` 731 | 732 | ##### fan-mode-dropdown 733 | 734 | ```yaml 735 | type: custom:mini-climate 736 | entity: climate.dahatsu 737 | secondary_info: fan-mode-dropdown 738 | ``` 739 | ![image](https://user-images.githubusercontent.com/861063/84180244-d80d0a80-aa8f-11ea-8275-f4e3db85fd31.png) 740 | 741 | ### Theme variables 742 | The following variables are available and can be set in your theme to change the appearence of the card. 743 | Can be specified by color name, hexadecimal, rgb, rgba, hsl, hsla, basically anything supported by CSS. 744 | 745 | | name | Default | Description | 746 | |---------------------------------|-----------------------------------------------------------------------|---------------------------------| 747 | | mini-climate-name-font-weight | 400 | Font weight of the entity name | 748 | | mini-climate-info-font-weight | 300 | Font weight of the states | 749 | | mini-climate-icon-color | --mini-humidifier-base-color, var(--paper-item-icon-color, #44739e) | The color for icons | 750 | | mini-climate-button-color | --mini-humidifier-button-color, var(--paper-item-icon-color, #44739e) | The color for buttons icons | 751 | | mini-climate-accent-color | var(--accent-color) | The accent color of UI elements | 752 | | mini-climate-base-color | var(--primary-text-color) & var(--paper-item-icon-color) | The color of base text | 753 | | mini-climate-background-opacity | 1 | Opacity of the background | 754 | | mini-climate-scale | 1 | Scale of the card | 755 | | mini-climate-card-box-shadow | var(--ha-card-box-shadow, none) | The card shadow | 756 | 757 | ## My configuration 758 | 759 | > I originally wrote a plugin for my air conditioner implementation using [esphome](https://github.com/esphome/esphome) 760 | > if interested, you can source [esphome-mqtt-climate](https://github.com/artem-sedykh/esphome-mqtt-climate) 761 | > the following is a configuration example for my air conditioner 762 | 763 | ```yaml 764 | type: custom:mini-climate 765 | entity: climate.dahatsu 766 | name: Кондиционер 767 | fan_mode: 768 | source: 769 | auto: Авто 770 | low: Слабый 771 | medium: Средний 772 | high: Сильный 773 | # for my implementation fan_modes_al is an array of available fan modes of the selected hvac mode 774 | __filter: > 775 | (source, state, entity) => entity.attributes 776 | .fan_modes_al.map(fan_mode => source.find(s => s.id === fan_mode)) 777 | .filter(fan_mode => fan_mode) 778 | buttons: 779 | swing_mode: 780 | type: dropdown 781 | icon: mdi:approximately-equal 782 | state: 783 | attribute: swing_mode 784 | # the drop-down list will remain active until swing_mode is off 785 | active: state => state !== 'off' 786 | source: 787 | 'off': Выкл 788 | horizontal: Вкл 789 | change_action: > 790 | (selected, state, entity) => this.call_service('climate', 'set_swing_mode', { entity_id: entity.entity_id, swing_mode: selected }) 791 | # turbo air conditioning button 792 | turbo: 793 | icon: mdi:weather-hurricane 794 | # control topic 795 | topic: 'dahatsu/turbo/set' 796 | state: 797 | attribute: turbo 798 | # for my device, the turbo attribute returns boolean type, convert it to on or off 799 | mapper: "(state, entity) => state ? 'on': 'off'" 800 | # turbo button is not available for all modes, block it when it is not available 801 | disabled: (state, entity) => !entity.attributes.turbo_al 802 | # when you click on the button, send the event to mqtt 803 | toggle_action: > 804 | (state) => this.call_service('mqtt', 'publish', { payload: this.toggle_state(state), topic: this.topic, retain: false, qos: 1 }) 805 | # eco button configuration is the same as for turbo button 806 | eco: 807 | icon: mdi:leaf 808 | topic: 'dahatsu/eco/set' 809 | state: 810 | attribute: eco 811 | mapper: "(state, entity) => state ? 'on': 'off'" 812 | disabled: (state, entity) => !entity.attributes.eco_al 813 | toggle_action: > 814 | (state) => this.call_service('mqtt', 'publish', { payload: this.toggle_state(state), topic: this.topic, retain: false, qos: 1 }) 815 | # health button configuration is the same as for turbo button 816 | health: 817 | icon: mdi:emoticon-happy-outline 818 | topic: 'dahatsu/health/set' 819 | state: 820 | attribute: health 821 | mapper: "(state, entity) => state ? 'on': 'off'" 822 | disabled: (state, entity) => !entity.attributes.health_al 823 | toggle_action: > 824 | (state) => this.call_service('mqtt', 'publish', { payload: this.toggle_state(state), topic: this.topic, retain: false, qos: 1 }) 825 | # power off button 826 | power_switch: 827 | icon: mdi:power-plug 828 | state: 829 | entity: switch.air_conditioner_kitchen_switch_l1 830 | indicators: 831 | # humidity indicator 832 | humidity: 833 | icon: mdi:water 834 | unit: '%' 835 | round: 1 836 | source: 837 | entity: sensor.sensor_temp_hum_pre_kitchen_humidity 838 | # power consumption indicator 839 | power_consumption: 840 | icon: mdi:flash 841 | unit: 'W' 842 | round: 1 843 | source: 844 | entity: sensor.dahatsu_power 845 | # power indicator 846 | power: 847 | icon: mdi:power-plug 848 | source: 849 | entity: switch.air_conditioner_kitchen_switch_l1 850 | values: 851 | 'on': 'вкл' 852 | 'off': 'выкл' 853 | # localization of values 854 | mapper: value => this.source.values[value] 855 | ``` 856 | 857 | ## Development 858 | *If you plan to contribute back to this repo, please fork & create the PR against the [dev](https://github.com/artem-sedykh/mini-climate-card/tree/dev) branch.* 859 | 860 | **Clone this repository into your `config/www` folder using git.** 861 | 862 | ```console 863 | $ git clone https://github.com/artem-sedykh/mini-climate-card.git 864 | ``` 865 | 866 | **Add a reference to the card in your `ui-lovelace.yaml`.** 867 | 868 | ```yaml 869 | resources: 870 | - url: /local/mini-humidifier/dist/mini-climate-card-bundle.js 871 | type: module 872 | ``` 873 | 874 | ### Instructions 875 | 876 | *Requires `nodejs` & `npm`* 877 | 878 | 1. Move into the `mini-climate-card` repo, checkout the *dev* branch & install dependencies. 879 | ```console 880 | $ cd mini-climate-card-dev && git checkout dev && npm install 881 | ``` 882 | 883 | 2. Make changes to the source 884 | 885 | 3. Build the source by running 886 | ```console 887 | $ npm run build 888 | ``` 889 | 890 | 4. Refresh the browser to see changes 891 | 892 | *Make sure cache is cleared or disabled* 893 | 894 | 5. *(Optional)* Watch the source and automatically rebuild on save 895 | ```console 896 | $ npm run watch 897 | ``` 898 | 899 | *The new `mini-climate-card-bundle.js` will be build and ready inside `/dist`.* 900 | 901 | 902 | ## Getting errors? 903 | Make sure you have `javascript_version: latest` in your `configuration.yaml` under `frontend:`. 904 | 905 | Make sure you have the latest version of `mini-climate-card-bundle.js`. 906 | 907 | If you have issues after updating the card, try clearing your browsers cache or restart Home Assistant. 908 | 909 | If you are getting "Custom element doesn't exist: mini-climate" or running older browsers try replacing `type: module` with `type: js` in your resource reference, like below. 910 | 911 | ```yaml 912 | resources: 913 | - url: ... 914 | type: js 915 | ``` 916 | 917 | ## License 918 | This project is under the MIT license. 919 | --------------------------------------------------------------------------------