├── VERSION
├── hacs.json
├── info.md
├── LICENSE
├── README.md
└── dual-gauge-card.js
/VERSION:
--------------------------------------------------------------------------------
1 | 0.5.2
2 |
--------------------------------------------------------------------------------
/hacs.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Dual gauge card",
3 | "filename": "dual-gauge-card.js",
4 | "content_in_root": true
5 | }
6 |
--------------------------------------------------------------------------------
/info.md:
--------------------------------------------------------------------------------
1 | # Dual gauge card
2 |
3 | Two gauges combined into one.
4 |
5 | ## Features
6 | * Use entities or their attributes
7 | * Change colors depending on values
8 | * Configure both gauges at once and/or separately
9 |
10 |
11 | 
12 |
13 |
14 | See [README.md](https://github.com/custom-cards/dual-gauge-card/blob/master/README.md) for installation and configuration.
15 |
16 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Custom cards for Home Assistant
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Dual gauge card
2 |
3 | > [!IMPORTANT]
4 | > **Looking for new maintainer!**
5 | >
6 | > I can't find the time or inspiration to take care of this project anymore.
7 | > So if you'd like to become the new maintainer of this project please let me know via a Pull request!
8 |
9 |
10 | Two gauges in one, built mostly with CSS.
11 |
12 | [](LICENSE)
13 | [](https://github.com/custom-components/hacs)
14 |
15 |
16 | 
17 |
18 |
19 | Heavily inspired by [ciotlosm's gauge-card](https://github.com/ciotlosm/custom-lovelace/), but completly written
20 | from scratch.
21 |
22 | ## Installation
23 |
24 | Use [HACS](https://github.com/custom-components/hacs) (recommended)
25 | or download [dual-gauge-card.js](https://github.com/custom-cards/dual-gauge-card/raw/master/dual-gauge-card.js) and place it in your www directory.
26 |
27 | In your ui-lovelace.yaml add this:
28 | ```yaml
29 | - url: /community_plugin/dual-gauge-card/dual-gauge-card.js
30 | type: js
31 | ```
32 |
33 | If you don't use HACS please change the url accordingly.
34 |
35 | ## Config
36 |
37 | | Name | Type | Default | Description |
38 | |------------------|--------|---------|--------------------------------------------------|
39 | | title | string | | Common title |
40 | | min | int | 0 | minimum value |
41 | | max | int | 100 | maximum value |
42 | | colors | object | | color config (optional) |
43 | | background_color | string | | background color of the gauges |
44 | | shadeInner | bool | true | shade (darken) colors of the inner gauge by 25% |
45 | | cardwidth | int | 300 | width of the card in pixels (see below) |
46 | | outer | object | | config for the outer gauge |
47 | | inner | object | | config for the inner gauge |
48 | | precision | int | 2 | decimal precision |
49 |
50 | ### gauge config
51 |
52 | Both gauges have the same attributes:
53 |
54 | | Name | Type | Default | Description |
55 | |-----------|--------|---------|------------------------------------------------------------------|
56 | | entity | string | | entity id |
57 | | attribute | string | | use this attribute of the entity instead of its state (optional) |
58 | | label | string | | label for this gauges value (optional) |
59 | | unit | object | | unit to add to the value (optional) |
60 | | min | int | | minimum value |
61 | | max | int | | maximum value |
62 | | colors | object | | color config (optional) |
63 | | precision | int | 2 | decimal precision |
64 |
65 | ### cardwidth
66 |
67 | You may use the config value _cardwidth_ to set the overall width of the card as an absolute value in pixels.
68 | All elements of the gauge are sized relative to this so that the gauge scales to this, _but_ the card is not
69 | responsive for now, i.e. it doesn't resize automatically.
70 |
71 |
72 | ### color config
73 |
74 | Colors can be configured as list of pairs of each a color and a minimum value.
75 |
76 | If a gauges value is greater than or equal to one of those minimum values, the according color
77 | is used for that gauge. If no color is found, the last color in the list is used as a fallback.
78 | To use a single color regardless of the value just use a single list entry with any value to always trigger
79 | the fallback.
80 |
81 | By default, colors for the inner gauge are shaded by 25% (see option _shadeInner_).
82 |
83 | The list is automatically sorted so you don't need to do that in your config - but I recommend it anyways.
84 |
85 | ### common config vs. individual config
86 |
87 | Colors, as well as the min and max values, may be configured once for both gauges or individually for each gauge. Individual values override common values.
88 |
89 | ## Example
90 |
91 | The example on the screenshot is configured like this:
92 | ```
93 | - type: custom:dual-gauge-card
94 | title: Living room
95 | min: -20
96 | max: 40
97 | outer:
98 | entity: climate.living_room
99 | attribute: current_temperature
100 | label: "Current"
101 | unit: "°C"
102 | inner:
103 | entity: climate.living_room
104 | label: "Target"
105 | attribute: temperature
106 | unit: "°C"
107 | colors:
108 | - color: "var(--label-badge-red)"
109 | value: 27.5
110 | - color: "var(--label-badge-green)"
111 | value: 25
112 | - color: "var(--label-badge-yellow)"
113 | value: 18
114 | - color: "var(--label-badge-blue)"
115 | value: 0
116 | - color: "var(--paper-blue-400)"
117 | value: -40
118 | ```
119 |
120 | In this example, the outer gauge has individual min and max values and uses default colors, whereas the inner
121 | gauge has individual colors and uses the common min and max values.
122 | ```
123 | - type: custom:dual-gauge-card
124 | title: Living room
125 | min: -20
126 | max: 40
127 | precision: 2
128 | outer:
129 | entity: climate.living_room
130 | attribute: current_temperature
131 | label: "Current"
132 | unit: "°C"
133 | min: -30
134 | max: 50
135 | inner:
136 | entity: climate.living_room
137 | label: "Target"
138 | attribute: temperature
139 | unit: "°C"
140 | colors:
141 | - color: "var(--label-badge-green)"
142 | value: 25
143 | - color: "var(--label-badge-yellow)"
144 | value: 18
145 | - color: "var(--label-badge-blue)"
146 | value: 0
147 | ```
148 |
149 |
--------------------------------------------------------------------------------
/dual-gauge-card.js:
--------------------------------------------------------------------------------
1 | class DualGaugeCard extends HTMLElement {
2 | set hass(hass) {
3 | this._hass = hass;
4 |
5 | if (!this.card) {
6 | this._createCard();
7 | }
8 |
9 | this._update();
10 | }
11 |
12 | setConfig(config) {
13 | if (!config.inner|| !config.inner.entity) {
14 | throw new Error('You need to define an entity for the inner gauge');
15 | }
16 | if (!config.outer || !config.outer.entity) {
17 | throw new Error('You need to define an entity for the outer gauge');
18 | }
19 | this.config = JSON.parse(JSON.stringify(config));
20 |
21 | if (!this.config.min) {
22 | this.config.min = 0;
23 | }
24 | if (!this.config.max) {
25 | this.config.max = 100;
26 | }
27 |
28 | if (this.config.precision === undefined) {
29 | this.config.precision = 2;
30 | }
31 | if (this.config.inner.precision === undefined) {
32 | this.config.inner.precision = this.config.precision;
33 | }
34 | if (this.config.outer.precision === undefined) {
35 | this.config.outer.precision = this.config.precision;
36 | }
37 |
38 | if (!this.config.inner.min) {
39 | this.config.inner.min = this.config.min;
40 | }
41 | if (!this.config.inner.max) {
42 | this.config.inner.max = this.config.max;
43 | }
44 |
45 | if (!this.config.outer.min) {
46 | this.config.outer.min = this.config.min;
47 | }
48 | if (!this.config.outer.max) {
49 | this.config.outer.max = this.config.max;
50 | }
51 |
52 | if (!this.config.hasOwnProperty('shadeInner')) {
53 | this.config.shadeInner = true
54 | }
55 |
56 | if (!this.config.inner.colors) {
57 | this.config.inner.colors = this.config.colors;
58 | }
59 | if (!this.config.outer.colors) {
60 | this.config.outer.colors = this.config.colors;
61 | }
62 |
63 | if (this.config.inner.colors) {
64 | this.config.inner.colors.sort((a, b) => a.value < b.value ? 1 : -1);
65 | }
66 | if (this.config.outer.colors) {
67 | this.config.outer.colors.sort((a, b) => a.value < b.value ? 1 : -1);
68 | }
69 | }
70 |
71 | _update() {
72 | if (this._hass.states[this.config['inner'].entity] == undefined ||
73 | this._hass.states[this.config['outer'].entity] == undefined) {
74 | console.warn("Undefined entity");
75 | if (this.card) {
76 | this.card.remove();
77 | }
78 |
79 | this.card = document.createElement('ha-card');
80 | if (this.config.header) {
81 | this.card.header = this.config.header;
82 | }
83 |
84 | const content = document.createElement('p');
85 | content.style.background = "#e8e87a";
86 | content.style.padding = "8px";
87 | content.innerHTML = "Error finding these entities:
- " +
88 | this.config['inner'].entity +
89 | "
- " + this.config['outer'].entity;
90 | this.card.appendChild(content);
91 |
92 | this.appendChild(this.card);
93 | return;
94 | } else if (this.card && this.card.firstElementChild.tagName.toLowerCase() == "p") {
95 | this._createCard();
96 | }
97 | this._updateGauge('inner');
98 | this._updateGauge('outer');
99 | }
100 |
101 | _updateGauge(gauge) {
102 | const gaugeConfig = this.config[gauge];
103 | const value = this._getEntityStateValue(this._hass.states[gaugeConfig.entity], gaugeConfig.attribute);
104 | this._setCssVariable(this.nodes.content, gauge + '-angle', this._calculateRotation(value, gaugeConfig));
105 | this.nodes[gauge].value.innerHTML = this._formatValue(value, gaugeConfig);
106 | if (gaugeConfig.label) {
107 | this.nodes[gauge].label.innerHTML = gaugeConfig.label;
108 | }
109 |
110 | const color = this._findColor(value, gaugeConfig);
111 | if (color) {
112 | this._setCssVariable(this.nodes.content, gauge + '-color', color);
113 | }
114 | }
115 |
116 | _showDetails(gauge) {
117 | const event = new Event('hass-more-info', {
118 | bubbles: true,
119 | cancelable: false,
120 | composed: true
121 | });
122 | event.detail = {
123 | entityId: this.config[gauge].entity
124 | };
125 | this.card.dispatchEvent(event);
126 | return event;
127 | }
128 |
129 | _formatValue(value, gaugeConfig) {
130 |
131 | value = parseFloat(value);
132 |
133 | if (gaugeConfig.precision !== undefined) {
134 | value = value.toFixed(gaugeConfig.precision);
135 | }
136 |
137 | if (gaugeConfig.unit) {
138 | value = value.toString() + gaugeConfig.unit;
139 | }
140 |
141 | return value;
142 | }
143 |
144 | _getEntityStateValue(entity, attribute) {
145 | if (!attribute) {
146 | if(isNaN(entity.state)) return "-" ; //check if entity state is NaN
147 | else return entity.state;
148 | }
149 |
150 | // return entity.attributes[attribute];
151 | if(isNaN(entity.attributes[attribute])) return "-" ; //check if entity attribute is NaN
152 | else return entity.attributes[attribute];
153 | }
154 |
155 | _calculateRotation(value, gaugeConfig) {
156 | if(isNaN(value)) return '180deg'; //check if value is NaN
157 | const maxTurnValue = Math.min(Math.max(value, gaugeConfig.min), gaugeConfig.max);
158 | return (180 + (5 * (maxTurnValue - gaugeConfig.min)) / (gaugeConfig.max - gaugeConfig.min) / 10 * 360) + 'deg';
159 | }
160 |
161 | _findColor(value, gaugeConfig) {
162 | if (!gaugeConfig.colors) return;
163 |
164 | var i = 0,
165 | count = gaugeConfig.colors.length - 1;
166 | for (; i < count; i++) {
167 | if (value >= gaugeConfig.colors[i].value) return gaugeConfig.colors[i].color;
168 | }
169 |
170 | return gaugeConfig.colors[count].color;
171 | }
172 |
173 | _createCard() {
174 | if (this.card) {
175 | this.card.remove();
176 | }
177 |
178 | this.card = document.createElement('ha-card');
179 | if (this.config.header) {
180 | this.card.header = this.config.header;
181 | }
182 |
183 | const content = document.createElement('div');
184 | this.card.appendChild(content);
185 |
186 | this.styles = document.createElement('style');
187 | this.card.appendChild(this.styles);
188 |
189 | this.appendChild(this.card);
190 |
191 | content.classList.add('gauge-dual-card');
192 | content.innerHTML = `
193 |