├── .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 | [](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 | [](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 | [](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 | [](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 | [](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 | [](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 | [](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 | [](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 | [](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 | [](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 | [](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 | [](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 | [](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 | [](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 | [](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 | [](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 | [](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 | [](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 | [](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 | [](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 | [](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 | [](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 | [](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 | [](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 | [](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 | [](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 | [](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 | [](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 | [](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 | [](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 | [](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 | [](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 | [](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 | [](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 | [](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 | [](https://github.com/artem-sedykh/mini-climate-card/releases/latest)
2 | [](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 | [](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 | [](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 | [](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 | 
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 | 
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 | 
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 |
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 |
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 | this.handleChangingTargetTemperature(e)}">
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 | [](https://github.com/artem-sedykh/mini-climate-card/releases/latest)
4 | [](https://travis-ci.com/artem-sedykh/mini-climate-card)
5 | [](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 |
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 | 
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 |
--------------------------------------------------------------------------------