├── .devcontainer └── devcontainer.json ├── .github └── workflows │ └── codeql.yml ├── README.md ├── examples ├── Hass horseshoe overview 920x693.png ├── flex-horseshoe-card--example-card-1.png ├── flex-horseshoe-card--example-card-12.png ├── flex-horseshoe-card--example-card-4.png └── view-flex-horseshoe-card-examples.yaml ├── flex-horseshoe-card.js ├── hacs.json ├── images └── Hass horseshoe overview 920x693.png └── info.md /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/javascript-node 3 | { 4 | "name": "Node.js", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "mcr.microsoft.com/devcontainers/javascript-node:0-20" 7 | 8 | // Features to add to the dev container. More info: https://containers.dev/features. 9 | // "features": {}, 10 | 11 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 12 | // "forwardPorts": [], 13 | 14 | // Use 'postCreateCommand' to run commands after the container is created. 15 | // "postCreateCommand": "yarn install", 16 | 17 | // Configure tool-specific properties. 18 | // "customizations": {}, 19 | 20 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 21 | // "remoteUser": "root" 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "develop" ] 6 | pull_request: 7 | branches: [ "develop" ] 8 | schedule: 9 | - cron: "16 5 * * 6" 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ javascript ] 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v3 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v2 31 | with: 32 | languages: ${{ matrix.language }} 33 | queries: +security-and-quality 34 | 35 | - name: Autobuild 36 | uses: github/codeql-action/autobuild@v2 37 | 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@v2 40 | with: 41 | category: "/language:${{ matrix.language }}" 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Personal Note, april 2023** 2 | 3 | Getting up to speed again with my custom cards after some difficult years! 4 | 5 | First commit is compatibility for Home Assistant 2023.4. I missed that one in my testcard view. For some reason I fixed the HA version to some 2023.3 version in my docker compose file. And in that case you can `docker compose pull` but nothing is updated... 6 | *** 7 | 8 | [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) 9 | ![GitHub release (latest by date)](https://img.shields.io/github/v/release/AmoebeLabs/flex-horseshoe-card?style=for-the-badge) 10 | ![GitHub Release Date](https://img.shields.io/github/release-date/AmoebeLabs/flex-horseshoe-card?style=for-the-badge) 11 | 12 | # ![](https://tweakers.net/ext/f/D4Fx1OKp6s7Hb21Wzq9JWCJb/full.png) Flexible Horseshoe Card 13 | Flexible looks-like-a-horseshoe card for [Home Assistant](https://github.com/home-assistant/home-assistant) Lovelace UI 14 | 15 | ![](https://tweakers.net/ext/f/3jaSI26J9QxHJa8rTriXFNNO/full.png) 16 | 17 | 18 | *The Lovelace view of the above examples is in the repository in the examples folder. 19 |
So you can see how these layouts are done* 20 | *** 21 | 22 | ### v0.8.0 is the first public release of this card. Be gentle with it! 23 | * * * 24 | 25 | ## Introduction 26 | The flexible horseshoe card can display data from entities and attributes from the sensor and other domains. It displays the current state and for the primary entity it fills the horseshoe with a color depending on the min and max values of the state and the configured color stops and styling. 27 | 28 | The main perk of this card is it's flexibility. It is able to position a number of things where YOU want it using a layout specification for each object you want on the card: 29 | 30 | | Feature | Description | 31 | |---------|-------------| 32 | | **Any** number of **entities** |For each entity, the attribute, units, icon, name, area and tap action can be specified.

*There is currently no limit imposed on the number of entities in this card. I'm using max. 3 entities in the examples, but there is no problem using more.* 33 | | **Any** number of **circles**, **horizontal** and **vertical** **lines** | To function as a divider between values or background for values. 34 | | The **layout** of the card | You can specify each object with a relative position on the card | 35 | | **Animations**, dynamic behaviour | You can specify what happens if an entity changes state like change color, or execute a CSS animation. There are predefined animations. | 36 | | Several ways to **color** the **horseshoe** | From single, fixed color, to a gradient depending on a list of colorstops | 37 | | **Actions** | Handle click actions per entity to for instance switch a light on/off | 38 | 39 | * * * 40 | # ![](https://tweakers.net/ext/f/D4Fx1OKp6s7Hb21Wzq9JWCJb/full.png) Table of contents 41 | - [Some examples](#-some-examples) 42 | - [Install](#-install) 43 | - [Using the card](#-using-the-card) 44 | - [Card Options](#-card-options) 45 | - [Enities Section](#-entities-section) 46 | - [Layout Section](#-layout-section) 47 | - [Horseshoe Section](#-horseshoe-section) 48 | - [Show Section](#-show-section) 49 | - [Card filter Section](#-card-filter-section) 50 | - [Animations Section](#-animations-section) 51 | - [12 reusable Examples](#-examples-section) 52 | - [Design your OWN card](#-design-your-own-card) 53 | - [End notes](#-end-notes) 54 | *** 55 | 56 | # ![](https://tweakers.net/ext/f/D4Fx1OKp6s7Hb21Wzq9JWCJb/full.png) Some examples 57 | 58 | ## Normal, flat UI 59 | Cards in a standard vertical stack / horizontal stack - 2 cards per row - combination. 60 | 61 | ![](https://tweakers.net/ext/f/JRnq6D0rODy48SUOsUcFH1Bb/full.png) 62 | 63 | Legend: 64 | - (3), showing a single attribute from a darksky sensor, a unit (temperature), an area and horizontal line 65 | - (4), showing three attributes from a darksky sensor (temperature, humidity and air pressure), units, two icons, a name and a horizontal line 66 | - (5), showing three sensors from system monitoring (ram used, ram used percentage and ram free), two sensor names ("in use" and "free"), a horizontal line and a vertical line. 67 | - (6), same as (5), bit with different horizontal and vertical line and different fill style for the horseshoe. 68 | 69 | All cards use different styling for filling the horseshoe with a color. 70 | 71 | ## Some extreme, industrial look, 3D UI 72 | Using the same cards as above, but with a predefined set of filters applied. In this case the `card--dropshadow-heavy--sepia90` class filter for the card_filter variable. 73 | 74 | Again, cards in a standard vertical stack / horizontal stack - 2 cards per row - combination. 75 | 76 | ![Another Example](https://tweakers.net/ext/f/xjuaTt3620GPgQyMnrrIIfth/full.png) 77 | ![](https://tweakers.net/ext/f/3wRqCSI3EXdysHVFAwYzqpWl/full.png) 78 | 79 | ## It scales, as it is based on SVG 80 | Using a single card in a row. Card scales to maximum width of the vertical stack card. No changes required for text size, icons, lines and state & attribute values. All thanks to SVG. 81 | 82 | ![](https://tweakers.net/ext/f/JNXii52PVqvVIIKA8wWZjGla/full.png) 83 | 84 | ## Yes, you can interact with it. Switching lights is no problem! 85 | For each entity a `tap_action` can be defined. The default is the known show-more info dialog. This can be changed in executing a service for instance. 86 | 87 | Combined with animations and states, you can alter the appearance of objects. The card containts a list of [predefined animations](#predefined-animations), or you just create your own! 88 | 89 | ![](https://tweakers.net/ext/f/Hk2Lzz2VkPbDUvEQUubBXoJU/full.gif) 90 | 91 | # ![](https://tweakers.net/ext/f/D4Fx1OKp6s7Hb21Wzq9JWCJb/full.png) Install 92 | 93 | ## Install via HACS 94 | [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/custom-components/hacs) 95 | 96 | ## Manual install 97 | 98 | 1. Download and copy `flex-horseshoe-card.js` from github into your `config/www` directory. 99 | 100 | 2. If using the editor UI: Add a reference to `flex-horseshoe-card.js` inside your `ui-lovelace.yaml` or at the top of the *raw config editor UI*. 101 | 3. If using yaml mode, add a reference in the resources.yaml file that is !included in your `ui-lovelac.yaml` file 102 | 103 | ```yaml 104 | resources: 105 | - url: /community_plugin/flex-horseshoe-card/flex-horseshoe-card.js 106 | type: module 107 | ``` 108 | # ![](https://tweakers.net/ext/f/D4Fx1OKp6s7Hb21Wzq9JWCJb/full.png) Using the card 109 | 110 | The preferred method of using this card is by [`decluttering card`](https://github.com/custom-cards/decluttering-card) templates. You define the layout and default options in this template and use the template in your Lovelace config. This config stays clean this way: you only specify the entities, attributes, units and icons which are displayed according to the layout defined in the template. 111 | 112 | The advice will become obvious once you scroll throught the list of card options :smile: 113 | 114 | ## A basic example 115 | This is the card 1 of the examples. It shows the basic definition for the flexible horseshoe card using the darksky sensor with the temperature attribute and its unit and decimals. 116 | 117 | ![](/examples/flex-horseshoe-card--example-card-1.png) 118 | 119 | ```yaml 120 | - type: 'custom:flex-horseshoe-card' 121 | entities: 122 | - entity: weather.dark_sky 123 | attribute: temperature 124 | decimals: 1 125 | unit: '°C' 126 | area: De Maan 127 | show: 128 | horseshoe_style: 'lineargradient' 129 | layout: 130 | states: 131 | # Refers to the first entity in the list, ie index 0 132 | # State value is positioned at (50%,60%) with a large font size 133 | # The size of the units are automatically calculated at 60% of the 134 | # state value font size and shifted upwards. 135 | # The default font color is the theme defined primary-text-color. 136 | - id: 0 137 | entity_index: 0 138 | xpos: 50 139 | ypos: 60 140 | styles: 141 | - font-size: 3.5em; 142 | areas: 143 | # Refers to the first entity in the list, ie index 1 144 | # Area value is positioned at (50%,35%) with font-size 1.5 and 145 | # an opacity of 80%. 146 | # The default font color is the theme defined primary-text-color. 147 | - id: 0 148 | entity_index: 0 149 | xpos: 50 150 | ypos: 35 151 | styles: 152 | - font-size: 1.5em; 153 | - opacity: 0.8; 154 | 155 | # Scale set to -10 to +40 degrees celcius 156 | horseshoe_scale: 157 | min: -10 158 | max: 40 159 | # color stop list with two colors. With the `lineargradient` fill style, only the 160 | # colors are used. The thresholds are ignored with this setting. 161 | color_stops: 162 | 10: 'red' 163 | 18: 'blue' 164 | ``` 165 | 166 | ## Extending the basic example with two more entities and a horizontal line 167 | This is card 4 of the examples. It extends the basic definition of card 1 with two more attributes from the darksky sensor and adds a horizontal line as a divider. We also swap the `area` with the `name` of the first entity. 168 | 169 | ![](/examples/flex-horseshoe-card--example-card-4.png) 170 | 171 | ```yaml 172 | - type: 'custom:flex-horseshoe-card' 173 | entities: 174 | - entity: weather.dark_sky 175 | attribute: temperature 176 | decimals: 1 177 | name: '4: Ut Weer' 178 | unit: '°C' 179 | - entity: weather.dark_sky 180 | attribute: humidity 181 | decimals: 0 182 | unit: '%' 183 | icon: mdi:water-percent 184 | - entity: weather.dark_sky 185 | attribute: pressure 186 | decimals: 0 187 | unit: 'hPa' 188 | icon: mdi:gauge 189 | show: 190 | horseshoe_style: 'lineargradient' 191 | layout: 192 | hlines: 193 | # A horizontal line. Not connected to an entity 194 | - id: 0 195 | xpos: 50 196 | ypos: 42 197 | length: 40 198 | styles: 199 | - stroke: var(--primary-text-color); 200 | - stroke-width: 5; 201 | - stroke-linecap: round; 202 | - opacity: 0.7; 203 | states: 204 | # States 0 refers to the first entity in the list, ie index 0 205 | - id: 0 206 | entity_index: 0 207 | xpos: 50 208 | ypos: 34 209 | styles: 210 | - font-size: 3em; 211 | # States 1 refers to the second entity in the list, ie index 1 212 | - id: 1 213 | entity_index: 1 214 | xpos: 40 215 | ypos: 57 216 | styles: 217 | - text-anchor: start; 218 | - font-size: 1.5em; 219 | # States 2 refers to the third entity in the list, ie index 2 220 | - id: 2 221 | entity_index: 2 222 | xpos: 40 223 | ypos: 72 224 | styles: 225 | - text-anchor: start; 226 | - font-size: 1.5em; 227 | icons: 228 | # Icons 0 refers to the second entity in the list, ie index 1 229 | - id: 0 230 | entity_index: 1 231 | xpos: 37 232 | ypos: 57 233 | align: end 234 | size: 1.3 235 | # Icons 1 refers to the third entity in the list, ie index 2 236 | - id: 1 237 | entity_index: 2 238 | xpos: 37 239 | ypos: 72 240 | align: end 241 | size: 1.3 242 | names: 243 | # Names 0 refers to the first entity in the list, ie index 0 244 | - id: 0 245 | entity_index: 0 246 | xpos: 50 247 | ypos: 95 248 | 249 | # Scale set to -10 to +40 degrees celcius 250 | horseshoe_scale: 251 | min: -10 252 | max: 40 253 | # color stop list with 10 colors defined in the theme. With the `lineargradient` fill style, only the 254 | # first (16:) and last (25:) colors are used. The thresholds are ignored with this setting. 255 | color_stops: 256 | 16: '#FFF6E3' 257 | 17: '#FFE9B9' 258 | 18: '#FFDA8A' 259 | 19: '#FFCB5B' 260 | 20: '#FFBF37' 261 | 21: '#ffb414' 262 | 22: '#FFAD12' 263 | 23: '#FFA40E' 264 | 24: '#FF9C0B' 265 | 25: '#FF8C06' 266 | ``` 267 | 268 | ## Extending the basic example with a lot more options like actions and animations 269 | This is the card 12 of the examples. It displays the wattage (memory sensor is used for this value) and the state of two lights. Both ligts can be switched on and off. The left light uses a predefined animation (yello and zoomout), the right light uses a user defined animation. 270 | 271 | Let's see how that looks :smile: 272 | 273 | ![](/examples/flex-horseshoe-card--example-card-12.png) 274 | 275 | ```yaml 276 | - type: 'custom:flex-horseshoe-card' 277 | entities: 278 | # Abuse the memory_use_percent sensor as the wattage the bulbs use. Just to show the possibilities 279 | - entity: sensor.memory_use_percent 280 | decimals: 0 281 | name: '12: Two Bulbs' 282 | area: Hestia 283 | unit: W 284 | decimals: 0 285 | tap_action: 286 | action: more-info 287 | 288 | # The left light displayed on the card. Index 1 289 | - entity: light.1st_floor_hall_light 290 | name: 'hall' 291 | icon: mdi:lightbulb 292 | tap_action: 293 | action: call-service 294 | service: light.toggle 295 | service_data: { "entity_id" : "light.1st_floor_hall_light" } 296 | 297 | # The right light displayed on the card. Index 2 298 | - entity: light.gledopto 299 | name: 'opto' 300 | icon: mdi:lightbulb 301 | tap_action: 302 | action: call-service 303 | service: light.toggle 304 | service_data: { "entity_id" : "light.gledopto" } 305 | 306 | animations: 307 | # Animations for the second entity, index 1 308 | entity.1: 309 | - state: 'on' 310 | circles: 311 | - animation_id: 11 312 | styles: 313 | - fill: var(--theme-gradient-color-03); 314 | - opacity: 0.9; 315 | - transform-origin: 30% 50%; 316 | - animation: jello 1s ease-in-out both; 317 | icons: 318 | - animation_id: 10 319 | styles: 320 | - fill: black; 321 | - state: 'off' 322 | circles: 323 | - animation_id: 11 324 | reuse: true 325 | styles: 326 | - transform-origin: 30% 50%; 327 | - animation: zoomOut 1s ease-out both; 328 | icons: 329 | - animation_id: 10 330 | styles: 331 | - fill: var(--primary-text-color); 332 | 333 | # Animations for the third entity, index 2 334 | entity.2: 335 | - state: 'on' 336 | circles: 337 | - animation_id: 21 338 | styles: 339 | - fill: var(--theme-gradient-color-03); 340 | - stroke-width: 2; 341 | - stroke: var(--primary-background-color); 342 | - opacity: 0.9; 343 | - stroke-dasharray: 94; 344 | - stroke-dashoffset: 1000; 345 | - animation: stroke 2s ease-out forwards; 346 | 347 | icons: 348 | - animation_id: 20 349 | styles: 350 | - fill: black; 351 | 352 | - state: 'off' 353 | circles: 354 | - animation_id: 21 355 | styles: 356 | - fill: var(--primary-background-color); 357 | - opacity: 0.7; 358 | icons: 359 | - animation_id: 20 360 | styles: 361 | - fill: var(--primary-text-color); 362 | 363 | show: 364 | horseshoe_style: 'fixed' 365 | layout: 366 | states: 367 | - id: 0 368 | entity_index: 0 369 | animation_id: 0 370 | xpos: 50 371 | ypos: 28 372 | uom_font_size: 1.5 373 | styles: 374 | - font-size: 2.5em; 375 | - opacity: 0.9; 376 | names: 377 | - id: 0 378 | animation_id: 0 379 | entity_index: 0 380 | xpos: 50 381 | ypos: 100 382 | styles: 383 | - font-size: 1.2em; 384 | - opacity: 0.7; 385 | - id: 1 386 | animation_id: 1 387 | entity_index: 1 388 | xpos: 30 389 | ypos: 78 390 | styles: 391 | - font-size: 1.2em; 392 | - id: 2 393 | animation_id: 2 394 | entity_index: 2 395 | xpos: 70 396 | ypos: 78 397 | styles: 398 | - font-size: 1.2em; 399 | icons: 400 | - id: 0 401 | animation_id: 10 402 | xpos: 30 403 | ypos: 55 404 | entity_index: 1 405 | icon_size: 3.5 406 | styles: 407 | - color: var(--primary-text-color);; 408 | - id: 1 409 | animation_id: 20 410 | xpos: 70 411 | ypos: 55 412 | entity_index: 2 413 | icon_size: 3.5 414 | styles: 415 | - color: var(--primary-text-color);; 416 | circles: 417 | - animation_id: 3 418 | xpos: 30 419 | ypos: 50 420 | radius: 35 421 | styles: 422 | - fill: var(--primary-background-color); 423 | - animation_id: 11 424 | xpos: 30 425 | ypos: 50 426 | radius: 30 427 | entity_index: 1 428 | 429 | - animation_id: 2 430 | xpos: 70 431 | ypos: 50 432 | radius: 35 433 | styles: 434 | - fill: var(--primary-background-color); 435 | - animation_id: 21 436 | xpos: 70 437 | ypos: 50 438 | radius: 30 439 | entity_index: 2 440 | 441 | horseshoe_scale: 442 | min: 0 443 | max: 100 444 | color: 'var(--primary-background-color)' 445 | horseshoe_state: 446 | color: '#FFDA8A' 447 | color_stops: 448 | 0: '#FFF6E3' 449 | 10: '#FFE9B9' 450 | 20: '#FFDA8A' 451 | 30: '#FFCB5B' 452 | 40: '#FFBF37' 453 | 50: '#ffb414' 454 | 60: '#FFAD12' 455 | 70: '#FFA40E' 456 | 80: '#FF9C0B' 457 | 90: '#FF8C06' 458 | # The @keyframes stroke runs the stroke animation for the second lightbulb, entity light.gledopto 459 | style: | 460 | @keyframes stroke { to { stroke-dashoffset: 0; } } 461 | ``` 462 | 463 | # ![](https://tweakers.net/ext/f/D4Fx1OKp6s7Hb21Wzq9JWCJb/full.png) Card Options 464 | 465 | ## Main Card required, defaulted and pure optional sections 466 | The Card Options are divided into Sections. To give a clear overview of which of the sheer number of sections are required, optional with defaults and optional, the following table is made. 467 | 468 | The [examples section](#-examples-section) shows 12 examples of card definitions, from basic to using all available options! 469 | 470 | Note: The examples will get decluttering templates as an example too, to show how you can better manage and maintain the all the card layouts without loosing overview in the Lovelace views. 471 | 472 | Each section might have it's own required, defaulted and optional properties. 473 | 474 | | Name | Required | Optional /w
defaults | Optional | Since | Description | 475 | |------|:--------:|:---------:|:--------:|-------|-------------| 476 | | type | ![](https://tweakers.net/ext/f/D4Fx1OKp6s7Hb21Wzq9JWCJb/full.png) | | | v0.8.0 | `custom:flex-horseshoe-card`. 477 | | entities | ![](https://tweakers.net/ext/f/D4Fx1OKp6s7Hb21Wzq9JWCJb/full.png) | | | v0.8.0 | One or more sensor entities in a list. See [entities section](#-entities-section) for requirements. 478 | | layout | ![](https://tweakers.net/ext/f/D4Fx1OKp6s7Hb21Wzq9JWCJb/full.png) | | | v0.8.0 | You MUST of course specify where each item is positioned on the card. See [available layout options](#available-layout-options) for requirements. 479 | | horseshoe_scale | ![](https://tweakers.net/ext/f/D4Fx1OKp6s7Hb21Wzq9JWCJb/full.png) | some | | v0.8.0 | Specifies the scale configuration, like min, max, width and color of the scale. See [horseshoe scale](#horseshoe-scale-options) for requirements. 480 | | color_stops | ![](https://tweakers.net/ext/f/D4Fx1OKp6s7Hb21Wzq9JWCJb/full.png) | | | v0.8.0 | Set thresholds for horseshoe gradients and colormapping. See [color stops](#horseshoe-state-options) for requirements. 481 | | | | | | | 482 | | horseshoe_state | | ![](https://tweakers.net/ext/f/D4Fx1OKp6s7Hb21Wzq9JWCJb/full.png) | | v0.8.0 | Specifies the horseshoe width, and fixed color. See [horseshoe state](#horseshoe-state-options) for requirements. 483 | | show | | ![](https://tweakers.net/ext/f/D4Fx1OKp6s7Hb21Wzq9JWCJb/full.png) | | v0.8.0 | Determines what is shown, like the scale and the horseshoe style. See [available show options](#available-show-options) for requirements. 484 | | card_filter | | ![](https://tweakers.net/ext/f/D4Fx1OKp6s7Hb21Wzq9JWCJb/full.png) | | v0.8.0 | 485 | | entities tap_action | | ![](https://tweakers.net/ext/f/D4Fx1OKp6s7Hb21Wzq9JWCJb/full.png) | | v0.8.0 | How to respond to a mouse-click or tap. See [available tap actions](#action-object-optionss) for requirements. 486 | | | | | | | 487 | | animations | | | ![](https://tweakers.net/ext/f/D4Fx1OKp6s7Hb21Wzq9JWCJb/full.png) | v0.8.0 | You can specify animations / dynamic behaviour depending on the state of an entity. Circles, lines and icons can be controlled depending on the state of a given entity. See [available animation options](#available-animation-options) for requirements. 488 | 489 | # ![](https://tweakers.net/ext/f/D4Fx1OKp6s7Hb21Wzq9JWCJb/full.png) Entities section 490 | 491 | ## Available entity options 492 | | Name | Type | Default | Since | Description | 493 | |------|:----:|---------|-------|-------------| 494 | | attribute | string | optional | v0.8.0 | The attribute to be used for the entity. 495 | | unit | string | optional | v0.8.0 | Specifies the entity or attribute unit to be displayed. 496 | | decimals | number | optional | v0.8.0 | Specifies the decimals to format the entity or attribute value. 497 | | name | string | optional | v0.8.0 | Name used for entity or attribute. Overwrites the `friendly_name` attribute. 498 | | area | string | optional | v0.8.0 | Area used for entity or attribute. 499 | | tap_action | [action object](#action-object-options) | optional | v0.8.0 | How to respond to a mouse-click or tap. See [available tap actions](#action-object-optionss) for requirements. 500 | 501 | #### Example 1, displaying an entity: 502 | ```yaml 503 | entities: 504 | - entity: sensor.memory_use_percent 505 | decimals: 0 506 | icon: mdi:memory 507 | name: '5: RAM Usage' 508 | area: Hestia 509 | ``` 510 | 511 | #### Example 2, displaying an attribute: 512 | ```yaml 513 | entities: 514 | - entity: weather.dark_sky 515 | attribute: temperature 516 | units: '°C' 517 | icon: mdi:temperature 518 | decimals: 1 519 | name: 'Temperature' 520 | ``` 521 | 522 | ## Action object options 523 | (changed to be identical to mini graph card) 524 | 525 | | Name | Type | Default | Options | Since | Description | 526 | |------|:----:|---------|---------|-------|-------------| 527 | | action | string | `more-info` | `more-info`, `navigate`, `call-service`, `none` | v0.8.0 |Action to perform 528 | | service | string | none | Any service | v0.8.0 |Service to call (e.g. `media_player.toggle`) when `action` is defined as `call-service` 529 | | service_data | object | none | Any service data | v0.8.0 |Service data to include with the service call (e.g. `entity_id: media_player.office`) 530 | | navigation_path | string | none | Any path | v0.8.0 |Path to navigate to (e.g. `/lovelace/0/`) when `action` is defined as `navigate` 531 | 532 | #### Example 3: a light switch: 533 | ```yaml 534 | entities: 535 | - entity: light.1st_floor_hall_light 536 | name: 'hall' 537 | icon: mdi:lightbulb 538 | tap_action: 539 | action: call-service 540 | service: light.toggle 541 | service_data: { "entity_id" : "light.1st_floor_hall_light" } 542 | ``` 543 | 544 | # ![](https://tweakers.net/ext/f/D4Fx1OKp6s7Hb21Wzq9JWCJb/full.png) Layout section 545 | 546 | ## Available layout options 547 | The layout options determine where the objects are located on the card, and their initial appearance like font, font size, color, width, fill color, stroke color, etc. 548 | 549 | | Name | Type | Default | Since | Description | 550 | |------|:----:|:-------:|-------|-------------| 551 | | Layout object | [layout object](#layout-object-options) | **required** | v0.8.0 | Entity objects:Graphic objects: 552 | 553 | ## Layout object options 554 | 555 | | Name | Type | Default | Options | Since | Description | 556 | |------|:----:|---------|---------|-------|-------------| 557 | | id | number | *not used yet* | | v0.8.0 | Identifies the object. 558 | | xpos | percentage | **required** | percentage 0..100 | v0.8.0 | Relative x-position in card. A value of 50 (%) places the object in the middle of the x-axis 559 | | ypos | percentage | **required** | percentage 0..100 | v0.8.0 | Relative y-position in card. A value of 50 (%) places the object in the middle of the y-axis 560 | | length
*(lines only)* | percentage | **required** | percentage 0.100 | v0.8.0 | Relative length of a line. A value of 50 (%) means the line is half the size of the card's width 561 | | radius
*(circles only)* | pixels | **required** | > 1 / < 200 | v0.8.0 | Specifies the radius of the circle in pixels. 562 | | icon_size
*(icons only)* | em value | **required**| a value of 1 = 12px | v0.8.0 | Specifies the size of the icon in em units. A calculation takes care of positioning the icon 563 | | align
*(icons only)* | position | `middle` | `start`/ `middle`/ `end` | v0.8.0 | Specifies the alignment of the icon relative to the xpos and ypos. Functions idential to the `text-anchor`css property. Used in positioning calculations for the icon. 564 | | entity_index | number | **required** | N/A | v0.8.0 | Refers to the 0-based index in the entity list which the layout is connected to | 565 | | animation_id | number | optional | an Id | v0.8.0 | Identifies an animation in the animations section. It connects this layout object with dynamic behaviour 566 | | styles | list | optional | any valid css entry | v0.8.0 | specify a list of css values to style the object. Must be terminated with a semicolon `;` 567 | 568 | #### Example layout entry 569 | The following layout is a part of card 5. For more complete examples, see the [examples section](#-examples-section) 570 | 571 | ![Another Example](https://tweakers.net/ext/f/xjuaTt3620GPgQyMnrrIIfth/full.png) 572 | 573 | - xpos, ypos and length are **percentages** 574 | - state layout 0 is connected to entity 0, ie the first entity in the entities section 575 | - name layout 0 is also connected to entity 0 576 | 577 | ```yaml 578 | layout: 579 | hlines: 580 | - id: 0 581 | xpos: 50 582 | ypos: 38 583 | length: 40 584 | styles: 585 | - stroke: var(--theme-gradient-color-01); 586 | - stroke-width: 5; 587 | - opacity: 0.9; 588 | - stroke-linecap: round; 589 | vlines: 590 | - id: 0 591 | xpos: 50 592 | ypos: 56 593 | length: 20 594 | styles: 595 | - stroke: white; 596 | - opacity: 0.5; 597 | - stroke-width: 2; 598 | - stroke-linecap: round; 599 | states: 600 | - id: 0 601 | entity_index: 0 602 | xpos: 50 603 | ypos: 30 604 | styles: 605 | - font-size: 3em; 606 | - opacity: 0.9; 607 | names: 608 | - id: 0 609 | entity_index: 0 610 | xpos: 50 611 | ypos: 100 612 | styles: 613 | - font-size: 1.2em; 614 | 615 | ``` 616 | 617 | # ![](https://tweakers.net/ext/f/D4Fx1OKp6s7Hb21Wzq9JWCJb/full.png) Horseshoe section 618 | 619 | ## Horseshoe scale options 620 | | Name | Type | Default | Options | Since | Description | 621 | |------|:----:|---------|---------|-------|-------------| 622 | | min | number | **required** | | v0.8.0 | Minimum value of the scale / horseshoe 623 | | max | number | **required** | | v0.8.0 | Maximum value of the scale / horseshoe 624 | | color | color | `var(--primary-background-color)`|any # or var color| v0.8.0 | Color of the scale and tickmarks, if enabled through `show.scale_tickmarks` option. 625 | | width | pixels | 6 |size in pixels| v0.8.0 | Width of scale 626 | 627 | #### Example: 628 | ```yaml 629 | horseshoe_scale: 630 | min: 0 631 | max: 100 632 | width: 6 633 | color: 'var(--primary-background-color)' 634 | ``` 635 | ## Horseshoe state options 636 | | Name | Type | Default | Options | Since | Description | 637 | |------|:----:|---------|---------|-------|-------------| 638 | | color | color | **required** |any valid color| v0.8.0 | Color of horseshoe if `show.horseshoe_style` = `fixed` 639 | | width | pixels | 12 |size in pixels| v0.8.0 | Width of horseshoe 640 | 641 | #### Example: 642 | ```yaml 643 | horseshoe_state: 644 | width: 12 645 | color: 'var(--theme-gradient-color-01)' 646 | ``` 647 | 648 | ## Horseshoe color stops 649 | | Name | Type | Default | Options | Since | Description | 650 | |------|:----:|---------|---------|-------|-------------| 651 | | color_stops | list | **required** | | v0.8.0 | List of colorstop value and colors. Colors can be specified using: 652 | 653 | #### Example: 654 | Showing a list of colorstop thresholds (0..90) and the colorstop colors, in this case a gradient colorlist from the theme 655 | ```yaml 656 | color_stops: 657 | 0: 'var(--theme-gradient-color-01)' 658 | 10: 'var(--theme-gradient-color-02)' 659 | 20: 'var(--theme-gradient-color-03)' 660 | 30: 'var(--theme-gradient-color-04)' 661 | 40: 'var(--theme-gradient-color-05)' 662 | 50: 'var(--theme-gradient-color-06)' 663 | 60: 'var(--theme-gradient-color-07)' 664 | 70: 'var(--theme-gradient-color-08)' 665 | 80: 'var(--theme-gradient-color-09)' 666 | 90: 'var(--theme-gradient-color-10)' 667 | ``` 668 | ## Horseshoe fill styles 669 | The horseshoe can be filled in different ways. Almost all use the color_stop colors to determine the color of the horseshoe. Not all use the actual color_stop thresholds to determine the color, but just use the color_stop colors. 670 | 671 | The next table describes how the fill styles work: 672 | 673 | | Option | Requires | uses color_stop threshold | Uses entity value | Since | Description | 674 | |--------|----------|-----------------------|-------------------|-------|-------------| 675 | | autominmax | `color_stops` list with at least 2 values | uses `min` and `max` of scale | ![](https://tweakers.net/ext/f/D4Fx1OKp6s7Hb21Wzq9JWCJb/full.png) | v0.8.0 | Autominmax uses the `min` and `max` values to calculate a gradient color using the first and last color in the colorstop list and the actual value of the entity or attribute. 676 | | fixed | `horseshoe_state .color` | | | v0.8.0 | Fills the shoe with a single color 677 | | colorstop | `color_stops` list with at least 2 values | ![](https://tweakers.net/ext/f/D4Fx1OKp6s7Hb21Wzq9JWCJb/full.png) | ![](https://tweakers.net/ext/f/D4Fx1OKp6s7Hb21Wzq9JWCJb/full.png) | v0.8.0 | Fills the shoe with the colorstop color depending on the colorstop value and the value of the state 678 | | colorstopgradient | `color_stops` list with at least 2 values | ![](https://tweakers.net/ext/f/D4Fx1OKp6s7Hb21Wzq9JWCJb/full.png) | ![](https://tweakers.net/ext/f/D4Fx1OKp6s7Hb21Wzq9JWCJb/full.png) | v0.8.0 | Same as `colorstop`, but a gradient is used between colorstops 679 | | lineargradient | `color_stops` list with at least 2 values | | | v0.8.0 | Uses the first and last entry in the `color_stops` list to display a linear gradient. It always shows the full gradient from start to end color, independent of the states value. 680 | 681 | #### The fill style is set in the show section of the card: 682 | ```yaml 683 | show: 684 | horseshoe_style: 'lineargradient' 685 | ``` 686 | 687 | # ![](https://tweakers.net/ext/f/D4Fx1OKp6s7Hb21Wzq9JWCJb/full.png) Animations section 688 | ## Available animation options 689 | Animations are optional, and are driven by state changes of a given entity or attribute. 690 | 691 | | Name | Type | Default | Since | Description | 692 | |------|:----:|:-------:|-------|-------------| 693 | | entity. | string | **required** | v0.8.0 | Entity index (zero based) which triggers the animation. In the form of entity.1 for the SECOND entity in the entity list. If an attribute is specified, the attribute triggers the animation. 694 | | state | string | **required** | v0.8.0 | specifies the state like 'on', or 'off' the animation is meant for 695 | | circles, hlines, vlines, icons | list| **required** | v0.8.0 | list of objects with animations 696 | 697 | ## Available circle, hline, vline, icon animation styles 698 | | Name | Type | Default | Since | Description | 699 | |------|:----:|:-------:|-------|-------------| 700 | | animation_id | number | **required** | v0.8.0 | the unique (for this card) animation_id. Is also referred to by the layout. 701 | | styles | css properties list| **required** | v0.8.0 | list of pure css styles for this object. **MUST** contain a ';' at the end of the line! 702 | | reuse | boolean | `false` | v0.8.0 | Default the previous animation style is cleared. By setting reuse to `true`, the previous animation style is preserved by the new animation. This can be handy if this animation starts where the previous animation left off.
For instance a color: the 'on' state sets the circle to orange. The 'off' state keeps the color, but zooms out. 703 | 704 | ## Predefined animations 705 | | Name | Type | Since | Example definition in the styles section of the animation | 706 | |------|:----:|-------|-------------| 707 | | bounce | attention | v0.8.0 | `styles:`
`- animation: bounce 1s ease-in-out both;`
`- transform-origin: center bottom;` 708 | | flash | attention | v0.8.0 | `styles:`
`- animation: flash 1s ease-in-out both;`
`- transform-origin: center;` 709 | | headShake | attention | v0.8.0 | `styles:`
`- animation: headShake 1s ease-in-out both;`
`- transform-origin: center;` 710 | | heartBeat | attention | v0.8.0 | `styles:`
`- animation: heartBeat 1.3s ease-in-out both;`
`- transform-origin: center;` 711 | | jello | attention | v0.8.0 | `styles:`
`- animation: jello 1s ease-in-out both;`
`- transform-origin: center;` 712 | | pulse | attention | v0.8.0 | `styles:`
`- animation: pulse 1s ease-in-out both;`
`- transform-origin: center;` 713 | | rubberBand | attention | v0.8.0 | `styles:`
`- animation: rubberBand 1s ease-in-out both;`
`- transform-origin: center;` 714 | | shake| attention | v0.8.0 | `styles:`
`- animation: shake 1s ease-in-out both;`
`- transform-origin: center;` 715 | | swing | attention | v0.8.0 | `styles:`
`- animation: swing 1s ease-in-out both;`
`- transform-origin: top center;` 716 | | tada | attention | v0.8.0 | `styles:`
`- animation: tada 1s ease-in-out both;`
`- transform-origin: center;` 717 | | wobble | attention | v0.8.0 | `styles:`
`- animation: wobble 1s ease-in-out both;`
`- transform-origin: center;` 718 | | zoomOut | zooming | v0.8.0 | `styles:`
`- animation: zoomOut 1s ease-out both;`
`- transform-origin: center;` 719 | | zoomIn | zooming | v0.8.0 | `styles:`
`- animation: zoomIn 1s ease-out both;`
`- transform-origin: center;` 720 | 721 | #### Example of animation for card 11: 722 | 723 | ![](https://tweakers.net/ext/f/Hk2Lzz2VkPbDUvEQUubBXoJU/full.gif) 724 | ```yaml 725 | - type: 'custom:flex-horseshoe-card' 726 | entities: 727 | - entity: sensor.memory_use_percent 728 | - entity: light.1st_floor_hall_light 729 | animations: 730 | entity.1: 731 | - state: 'on' 732 | circles: 733 | - animation_id: 10 734 | styles: 735 | - fill: var(--theme-gradient-color-08); 736 | - opacity: 0.9; 737 | - animation: jello 1s ease-in-out both; 738 | - transform-origin: center; 739 | icons: 740 | - animation_id: 0 741 | styles: 742 | - fill: black; 743 | - state: 'off' 744 | circles: 745 | - animation_id: 10 746 | reuse: true 747 | styles: 748 | - transform-origin: center; 749 | - animation: zoomOut 1s ease-out both; 750 | icons: 751 | - animation_id: 0 752 | styles: 753 | - fill: var(--primary-text-color); 754 | ``` 755 | ## User defined animations 756 | You can define your own animations too. 757 | Pick a unique name, add the animation to the style: section of the card, and off you go. 758 | Example Card 12, the bulb named "OPTO" has such a user defined animation: you see something running around if the light is switched on. 759 | 760 | There are at least a few great places for example animations: 761 | - [CSS animations for beginners](https://thoughtbot.com/blog/css-animation-for-beginners) 762 | - [Animate.css](https://daneden.github.io/animate.css/), where the predefined animations come from! 763 | - The interactive site from Ana Travis, [Animista](http://animista.net/). A great site for creating all sorts of animations. 764 | #### Example of Card 12 765 | ```yaml 766 | - type: 'custom:flex-horseshoe-card' 767 | entities: 768 | - entity: sensor.memory_use_percent 769 | - entity: light.1st_floor_hall_light 770 | - entity: light.gledopto 771 | 772 | animations: 773 | entity.2: 774 | - state: 'on' 775 | circles: 776 | - animation_id: 3 777 | styles: 778 | - fill: var(--theme-gradient-color-03); 779 | - stroke-width: 2; 780 | - stroke: var(--primary-background-color); 781 | - opacity: 0.9; 782 | - stroke-dasharray: 94; 783 | - stroke-dashoffset: 1000; 784 | - animation: stroke 2s ease-out forwards; 785 | 786 | icons: 787 | - animation_id: 1 788 | styles: 789 | - fill: black; 790 | 791 | - state: 'off' 792 | circles: 793 | - animation_id: 3 794 | styles: 795 | - fill: var(--primary-background-color); 796 | - opacity: 0.7; 797 | icons: 798 | - animation_id: 1 799 | styles: 800 | - fill: var(--primary-text-color); 801 | # The @keyframes stroke runs the stroke animation for the second lightbulb, entity light.gledopto 802 | style: | 803 | ha-card { 804 | box-shadow: var(--theme-card-box-shadow); 805 | } 806 | @keyframes stroke { to { stroke-dashoffset: 0; } } 807 | ``` 808 | # ![](https://tweakers.net/ext/f/D4Fx1OKp6s7Hb21Wzq9JWCJb/full.png) Show section 809 | 810 | ## Available show options 811 | All options are optional. 812 | 813 | | Name | Default | Parameter | Since |Description | 814 | |------|:-------:|:---------:|-------|-------------| 815 | | scale_tickmarks | true | `true` / `false` | v0.8.0 |Display scale 816 | | horseshoe_style | `autominmax` | `fixed` / `autominmax`/ `colorstop` / `colorstopgradient`/ `lineargradient`| v0.8.0 | Fill style. Most fill styles need the colorstop list to be specified. See [horseshoe fill style list](#horseshoe-fill-styles) for a description. 817 | 818 | # ![](https://tweakers.net/ext/f/D4Fx1OKp6s7Hb21Wzq9JWCJb/full.png) Card Filter section 819 | There are some predefined css filters which you can use to give the full card a different look. Besides the predefined, you can also define you rown using the style: section of the yaml card definition and refer to that class as the card_filter: 820 | 821 | | Name | Default | Parameter | Since |Description | 822 | |------|:-------:|:---------:|-------|-------------| 823 | | card_filter | `card--dropshadow-none` | `card--dropshadow-none`/ `card--dropshadow-medium--opaque--sepia90` / `card--dropshadow-heavy--sepia90` / `card--dropshadow-heavy` / `card--dropshadow-medium--sepia90`/ `card--dropshadow-medium` / `card--dropshadow-light--sepia90` / `card--dropshadow-light` / `card--dropshadow-down-and-distant` | v0.8.0 | List of drop-shadows and sepia colorization using css filters on the full card.

Currently only tested on the darkslategrey / wheat Nyx theme 824 | 825 | # ![](https://tweakers.net/ext/f/D4Fx1OKp6s7Hb21Wzq9JWCJb/full.png) Examples section 826 | 827 | The full view with all 12 examples is in the examples folder of this repository. 828 | 829 | Three examples are included in this readme for easy access. 830 | Most examples use the [Darksy sensor](https://www.home-assistant.io/components/darksky/) or the Home Assistant [system monitor](https://www.home-assistant.io/components/systemmonitor/). 831 | A few use lights, which you have to replace with your own of course. 832 | 833 | Furthermore, theme defined variables are used: 834 | ```yaml 835 | theme-card-box-shadow: 'var(--shadow-elevation-2dp_-_box-shadow)' 836 | ``` 837 | 838 | and: 839 | ```yaml 840 | theme-gradient-color-01: '#FFF6E3' 841 | theme-gradient-color-02: '#FFE9B9' 842 | theme-gradient-color-03: '#FFDA8A' 843 | theme-gradient-color-04: '#FFCB5B' 844 | theme-gradient-color-05: '#FFBF37' 845 | theme-gradient-color-06: '#ffb414' 846 | theme-gradient-color-07: '#FFAD12' 847 | theme-gradient-color-08: '#FFA40E' 848 | theme-gradient-color-09: '#FF9C0B' 849 | theme-gradient-color-10: '#FF8C06' 850 | theme-gradient-color-11: '#FF8305' 851 | ``` 852 | 853 | Define your own, or alter the example cards! 854 | 855 | ![](https://tweakers.net/ext/f/3jaSI26J9QxHJa8rTriXFNNO/full.png) 856 | 857 | # ![](https://tweakers.net/ext/f/D4Fx1OKp6s7Hb21Wzq9JWCJb/full.png) Design your OWN card 858 | I hope you have found enough examples and inspiration to design your own horseshoe layout, with nice colors and functional animations. I just might include some of the community designs in this section :smile: 859 | 860 | # ![](https://tweakers.net/ext/f/D4Fx1OKp6s7Hb21Wzq9JWCJb/full.png) End notes 861 | 862 | ## License 863 | This project is under the MIT license. 864 | 865 | ## Credits 866 | 867 | The making of this card wouldn't be possible without an incredible number of resources I used to find solutions for the things I wanted to do with this card. Credits in random order: 868 | - The Home Assitant dev site with a simple example of creating a lit-element card 869 | - Implementation examples from the community like the mini-graph-card, button-card and gauge-card 870 | - The greatest site for CSS, [css-tricks](https://css-tricks.com/) 871 | - The back-to-school site for HTML, CSS and Javascript: [w3schools](https://www.w3schools.com/) 872 | - Stackoverflow for so many solutions for specific problems 873 | - [Codepen](https://codepen.io/) for so many, many, many small CSS, SVG and HTML examples for things I didn't now how they worked 874 | - [jsfiddle](https://jsfiddle.net/) for so many, many, many small CSS, SVG and HTML examples for things I didn't now how they worked 875 | - [designshack](https://designshack.net) for all sorts of inspirations & designs 876 | - [pinterest](https://nl.pinterest.com/) for color palettes and more 877 | - [CSS animations for beginners](https://thoughtbot.com/blog/css-animation-for-beginners) 878 | - [Animate.css](https://daneden.github.io/animate.css/), where the predefined animations come from! 879 | - The interactive site from Ana Travis, [Animista](http://animista.net/) for all your animations. 880 | - [The Material design palette generator](http://mcg.mbitson.com) which saved me a lot of time. 881 | - An [RGB Color Gradient Maker](http://www.perbang.dk/rgbgradient/) 882 | 883 | ## Personal Note 884 | Many, many, many years ago in the last century I learned to code C and Pascal. To make this card I had to learn the basics of a lot of new things like Javascript, HTML, CSS and the lit-element web component. 885 | The above resources where invaluable to accomplish this. 886 | -------------------------------------------------------------------------------- /examples/Hass horseshoe overview 920x693.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmoebeLabs/flex-horseshoe-card/882ed05e442f42cc354c67088ed773cad0bd28ee/examples/Hass horseshoe overview 920x693.png -------------------------------------------------------------------------------- /examples/flex-horseshoe-card--example-card-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmoebeLabs/flex-horseshoe-card/882ed05e442f42cc354c67088ed773cad0bd28ee/examples/flex-horseshoe-card--example-card-1.png -------------------------------------------------------------------------------- /examples/flex-horseshoe-card--example-card-12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmoebeLabs/flex-horseshoe-card/882ed05e442f42cc354c67088ed773cad0bd28ee/examples/flex-horseshoe-card--example-card-12.png -------------------------------------------------------------------------------- /examples/flex-horseshoe-card--example-card-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmoebeLabs/flex-horseshoe-card/882ed05e442f42cc354c67088ed773cad0bd28ee/examples/flex-horseshoe-card--example-card-4.png -------------------------------------------------------------------------------- /examples/view-flex-horseshoe-card-examples.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # 3 | # View : flex-horseshoe-card-examples (fhce) 4 | # Project : Home Assistant 5 | # Repository: https://github.com/AmoebeLabs/ 6 | # 7 | # Author : Mars @ AmoebeLabs.com 8 | # 9 | # License : CC BY-SA, https://creativecommons.org/licenses/by/4.0/ 10 | # 11 | # ----- 12 | # Description: 13 | # The Flexible Horseshoe Card examples view. 14 | # See: https://github.com/AmoebeLabs/flex-horseshoe-card/tree/master/examples 15 | # 16 | # Refs: 17 | # - https://github.com/AmoebeLabs/flex-horseshoe-card 18 | # 19 | # Them colors are replaced with hardcoded colors: 20 | # - theme-gradient-color-01: '#FFF6E3' 21 | # - theme-gradient-color-02: '#FFE9B9' 22 | # - theme-gradient-color-03: '#FFDA8A' 23 | # - theme-gradient-color-04: '#FFCB5B' 24 | # - theme-gradient-color-05: '#FFBF37' 25 | # - theme-gradient-color-06: '#ffb414' 26 | # - theme-gradient-color-07: '#FFAD12' 27 | # - theme-gradient-color-08: '#FFA40E' 28 | # - theme-gradient-color-09: '#FF9C0B' 29 | # - theme-gradient-color-10: '#FF8C06' 30 | # - theme-gradient-color-11: '#FF8305' 31 | # 32 | ############################################################################### 33 | 34 | title: FHCE 35 | path: fhce 36 | cards: 37 | #------------------------------------------------------------------------------ 38 | #- type: vertical-stack 39 | # cards: 40 | - type: horizontal-stack 41 | cards: 42 | 43 | # Example 1:: 44 | # 45 | ####################################################################### 46 | 47 | - type: 'custom:flex-horseshoe-card' 48 | entities: 49 | - entity: weather.dark_sky 50 | attribute: temperature 51 | decimals: 1 52 | area: De Maan 53 | unit: '°C' 54 | show: 55 | horseshoe_style: 'lineargradient' 56 | layout: 57 | states: 58 | # Refers to the first entity in the list, ie index 0 59 | # State value is positioned at (50%,60%) with a large font size 60 | # The size of the units are automatically calculated at 60% of the 61 | # state value font size and shifted upwards. 62 | # The default font color is the theme defined primary-text-color. 63 | - id: 0 64 | entity_index: 0 65 | xpos: 50 66 | ypos: 60 67 | styles: 68 | - font-size: 3.5em; 69 | areas: 70 | # Refers to the first entity in the list, ie index 1 71 | # Area value is positioned at (50%,35%) with font-size 1.5 and 72 | # an opacity of 80%. 73 | # The default font color is the theme defined primary-text-color. 74 | - id: 0 75 | entity_index: 0 76 | xpos: 50 77 | ypos: 35 78 | styles: 79 | - font-size: 1.5em; 80 | - opacity: 0.8; 81 | 82 | # Scale set to -10 to +40 degrees celcius 83 | horseshoe_scale: 84 | min: -10 85 | max: 40 86 | # color stop list with two colors. With the `lineargradient` fill style, only the 87 | # colors are used. The thresholds are ignored with this setting. 88 | color_stops: 89 | 10: 'red' 90 | 18: 'blue' 91 | 92 | # Example 2:: 93 | # 94 | ####################################################################### 95 | 96 | - type: 'custom:flex-horseshoe-card' 97 | entities: 98 | - entity: weather.dark_sky 99 | attribute: temperature 100 | decimals: 1 101 | name: '2: Ut Weer' 102 | area: De Maan 103 | unit: '°C' 104 | - entity: weather.dark_sky 105 | attribute: humidity 106 | decimals: 0 107 | unit: '%' 108 | - entity: weather.dark_sky 109 | attribute: pressure 110 | decimals: 0 111 | unit: 'hPa' 112 | show: 113 | horseshoe_style: 'lineargradient' 114 | layout: 115 | hlines: 116 | - id: 0 117 | xpos: 50 118 | ypos: 48 119 | length: 40 120 | styles: 121 | - stroke: var(--primary-text-color); 122 | - stroke-width: 2; 123 | - opacity: 0.5; 124 | - id: 1 125 | xpos: 50 126 | ypos: 20 127 | length: 40 128 | styles: 129 | - stroke: var(--primary-text-color); 130 | - stroke-width: 2; 131 | - opacity: 0.5; 132 | circles: 133 | - xpos: 50 134 | ypos: 61 135 | radius: 5 136 | styles: 137 | - fill: var(--primary-text-color); 138 | - opacity: 0.5; 139 | states: 140 | - id: 0 141 | entity_index: 0 142 | xpos: 50 143 | ypos: 40 144 | styles: 145 | - font-size: 3em; 146 | - id: 1 147 | entity_index: 1 148 | xpos: 46 149 | ypos: 64 150 | styles: 151 | - text-anchor: end; 152 | - font-size: 1.6em; 153 | - id: 2 154 | entity_index: 2 155 | xpos: 54 156 | ypos: 64 157 | styles: 158 | - text-anchor: start; 159 | - font-size: 1.6em; 160 | names: 161 | - id: 0 162 | entity_index: 0 163 | xpos: 50 164 | ypos: 100 165 | areas: 166 | - id: 0 167 | entity_index: 0 168 | xpos: 50 169 | ypos: 80 170 | 171 | horseshoe_scale: 172 | min: 10 173 | max: 14 174 | ticksize: 1 175 | color_stops: 176 | 16: '#FFF6E3' 177 | 17: '#FFE9B9' 178 | 18: '#FFDA8A' 179 | 19: '#FFCB5B' 180 | 20: '#FFBF37' 181 | 21: '#ffb414' 182 | 22: '#FFAD12' 183 | 23: '#FFA40E' 184 | 24: '#FF9C0B' 185 | 25: '#FF8C06' 186 | 187 | - type: horizontal-stack 188 | cards: 189 | 190 | # Example 3:: 191 | # 192 | ####################################################################### 193 | 194 | - type: 'custom:flex-horseshoe-card' 195 | entities: 196 | - entity: weather.dark_sky 197 | attribute: temperature 198 | decimals: 1 199 | name: 'Ut Weer' 200 | area: '3: De Maan' 201 | unit: '°C' 202 | show: 203 | horseshoe_style: 'autominmax' 204 | layout: 205 | hlines: 206 | - id: 0 207 | xpos: 50 208 | ypos: 39 209 | length: 40 210 | styles: 211 | - stroke: var(--primary-text-color); 212 | - stroke-width: 2; 213 | - opacity: 0.5; 214 | states: 215 | - id: 0 216 | entity_index: 0 217 | xpos: 50 218 | ypos: 65 219 | styles: 220 | - font-size: 3.5em; 221 | names: 222 | - id: 0 223 | entity_index: 0 224 | xpos: 50 225 | ypos: 30 226 | areas: 227 | - id: 0 228 | entity_index: 0 229 | xpos: 50 230 | ypos: 95 231 | styles: 232 | - font-size: 1.8em; 233 | horseshoe_scale: 234 | min: -10 235 | max: 40 236 | color_stops: 237 | 16: '#FFF6E3' 238 | 17: '#FFE9B9' 239 | 18: '#FFDA8A' 240 | 19: '#FFCB5B' 241 | 20: '#FFBF37' 242 | 21: '#ffb414' 243 | 22: '#FFAD12' 244 | 23: '#FFA40E' 245 | 24: '#FF9C0B' 246 | 25: '#FF8C06' 247 | 248 | # Example 4:: 249 | # 250 | ####################################################################### 251 | 252 | - type: 'custom:flex-horseshoe-card' 253 | entities: 254 | - entity: weather.dark_sky 255 | attribute: temperature 256 | decimals: 1 257 | name: '4: Ut Weer' 258 | area: De Maan 259 | unit: '°C' 260 | - entity: weather.dark_sky 261 | attribute: humidity 262 | decimals: 0 263 | unit: '%' 264 | icon: mdi:water-percent 265 | - entity: weather.dark_sky 266 | attribute: pressure 267 | decimals: 0 268 | unit: 'hPa' 269 | icon: mdi:gauge 270 | show: 271 | horseshoe_style: 'lineargradient' 272 | layout: 273 | hlines: 274 | # A horizontal line. Not connected to an entity 275 | - id: 0 276 | xpos: 50 277 | ypos: 42 278 | length: 40 279 | styles: 280 | - stroke: var(--primary-text-color); 281 | - stroke-width: 5; 282 | - stroke-linecap: round; 283 | - opacity: 0.7; 284 | states: 285 | # States 0 refers to the first entity in the list, ie index 0 286 | - id: 0 287 | entity_index: 0 288 | xpos: 50 289 | ypos: 34 290 | styles: 291 | - font-size: 3em; 292 | # States 1 refers to the second entity in the list, ie index 1 293 | - id: 1 294 | entity_index: 1 295 | xpos: 40 296 | ypos: 57 297 | styles: 298 | - text-anchor: start; 299 | - font-size: 1.5em; 300 | # States 2 refers to the third entity in the list, ie index 2 301 | - id: 2 302 | entity_index: 2 303 | xpos: 40 304 | ypos: 72 305 | styles: 306 | - text-anchor: start; 307 | - font-size: 1.5em; 308 | icons: 309 | # Icons 0 refers to the second entity in the list, ie index 1 310 | - id: 0 311 | entity_index: 1 312 | xpos: 37 313 | ypos: 57 314 | align: end 315 | size: 1.3 316 | # Icons 1 refers to the third entity in the list, ie index 2 317 | - id: 1 318 | entity_index: 2 319 | xpos: 37 320 | ypos: 72 321 | align: end 322 | size: 1.3 323 | names: 324 | # Names 0 refers to the first entity in the list, ie index 0 325 | - id: 0 326 | entity_index: 0 327 | xpos: 50 328 | ypos: 95 329 | 330 | # Scale set to -10 to +40 degrees celcius 331 | horseshoe_scale: 332 | min: -10 333 | max: 40 334 | # color stop list with 10 colors defined in the theme. With the `lineargradient` fill style, only the 335 | # first (16:) and last (25:) colors are used. The thresholds are ignored with this setting. 336 | color_stops: 337 | 16: '#FFF6E3' 338 | 17: '#FFE9B9' 339 | 18: '#FFDA8A' 340 | 19: '#FFCB5B' 341 | 20: '#FFBF37' 342 | 21: '#ffb414' 343 | 22: '#FFAD12' 344 | 23: '#FFA40E' 345 | 24: '#FF9C0B' 346 | 25: '#FF8C06' 347 | 348 | - type: horizontal-stack 349 | cards: 350 | 351 | # Example 5:: 352 | # 353 | ####################################################################### 354 | 355 | - type: 'custom:flex-horseshoe-card' 356 | 357 | entities: 358 | - entity: sensor.memory_use_percent 359 | decimals: 0 360 | icon: mdi:memory 361 | name: '5: RAM Usage' 362 | area: Hestia 363 | - entity: sensor.memory_use 364 | decimals: 0 365 | name: '(In Use)' 366 | - entity: sensor.memory_free 367 | decimals: 0 368 | name: '(free)' 369 | 370 | show: 371 | scale_tickmarks: true 372 | layout: 373 | hlines: 374 | - id: 0 375 | xpos: 50 376 | ypos: 38 377 | length: 40 378 | styles: 379 | - stroke: var(--primary-text-color); 380 | - stroke-width: 5; 381 | - opacity: 0.9; 382 | - stroke-linecap: round; 383 | color: '#FFF6E3' 384 | vlines: 385 | - id: 0 386 | xpos: 50 387 | ypos: 56 388 | length: 20 389 | styles: 390 | - stroke: var(--primary-text-color); 391 | - opacity: 0.5; 392 | - stroke-width: 2; 393 | - stroke-linecap: round; 394 | states: 395 | - id: 0 396 | entity_index: 0 397 | xpos: 50 398 | ypos: 30 399 | styles: 400 | - font-size: 3em; 401 | - opacity: 0.9; 402 | - id: 1 403 | entity_index: 1 404 | xpos: 46 405 | ypos: 54 406 | styles: 407 | - font-size: 1.5em; 408 | - text-anchor: end; 409 | - id: 2 410 | entity_index: 2 411 | xpos: 54 412 | ypos: 54 413 | styles: 414 | - font-size: 1.5em; 415 | - text-anchor: start; 416 | names: 417 | - id: 0 418 | entity_index: 0 419 | xpos: 50 420 | ypos: 100 421 | styles: 422 | - font-size: 1.2em; 423 | - id: 1 424 | entity_index: 1 425 | xpos: 46 426 | ypos: 62 427 | styles: 428 | - font-size: 0.8em; 429 | - text-anchor: end; 430 | - opacity: 0.7; 431 | - id: 2 432 | entity_index: 2 433 | xpos: 54 434 | ypos: 62 435 | styles: 436 | - font-size: 0.8em; 437 | - text-anchor: start; 438 | - opacity: 0.7; 439 | areas: 440 | - id: 0 441 | entity_index: 0 442 | xpos: 50 443 | ypos: 85 444 | styles: 445 | - font-size: 1.2em; 446 | 447 | horseshoe_state: 448 | color: '#FFF6E3' 449 | horseshoe_scale: 450 | min: 0 451 | max: 100 452 | color: 'var(--primary-background-color)' 453 | width: 6 454 | color_stops: 455 | 0: '#FFF6E3' 456 | 10: '#FFE9B9' 457 | 20: '#FFDA8A' 458 | 30: '#FFCB5B' 459 | 40: '#FFBF37' 460 | 50: '#ffb414' 461 | 60: '#FFAD12' 462 | 70: '#FFA40E' 463 | 80: '#FF9C0B' 464 | 90: '#FF8C06' 465 | 466 | # Example 6:: 467 | # 468 | ####################################################################### 469 | 470 | - type: 'custom:flex-horseshoe-card' 471 | 472 | entities: 473 | - entity: sensor.memory_use_percent 474 | decimals: 0 475 | icon: mdi:memory 476 | name: '6: RAM Usage' 477 | area: Hestia 478 | - entity: sensor.memory_use 479 | decimals: 0 480 | name: '(In Use)' 481 | - entity: sensor.memory_free 482 | decimals: 0 483 | name: '(free)' 484 | 485 | show: 486 | scale_tickmarks: true 487 | layout: 488 | hlines: 489 | - id: 0 490 | xpos: 50 491 | ypos: 38 492 | length: 70 493 | styles: 494 | - opacity: 0.2; 495 | vlines: 496 | - id: 0 497 | xpos: 50 498 | ypos: 58 499 | length: 38 500 | styles: 501 | - opacity: 0.2; 502 | states: 503 | - id: 0 504 | entity_index: 0 505 | xpos: 50 506 | ypos: 30 507 | styles: 508 | - font-size: 3em; 509 | - opacity: 0.9; 510 | - id: 1 511 | entity_index: 1 512 | xpos: 46 513 | ypos: 54 514 | styles: 515 | - font-size: 1.6em; 516 | - text-anchor: end; 517 | - id: 2 518 | entity_index: 2 519 | xpos: 54 520 | ypos: 54 521 | styles: 522 | - font-size: 1.6em; 523 | - text-anchor: start; 524 | names: 525 | - id: 0 526 | entity_index: 0 527 | xpos: 50 528 | ypos: 100 529 | styles: 530 | - font-size: 1.3em; 531 | - id: 1 532 | entity_index: 1 533 | xpos: 46 534 | ypos: 62 535 | styles: 536 | - font-size: 0.9em; 537 | - text-anchor: end; 538 | - opacity: 0.7; 539 | - id: 2 540 | entity_index: 2 541 | xpos: 54 542 | ypos: 62 543 | styles: 544 | - font-size: 0.9em; 545 | - text-anchor: start; 546 | - opacity: 0.7; 547 | areas: 548 | - id: 0 549 | entity_index: 0 550 | xpos: 50 551 | ypos: 85 552 | 553 | horseshoe_state: 554 | color: '#FFCB5B' 555 | horseshoe_scale: 556 | min: 0 557 | max: 100 558 | color: 'var(--primary-background-color)' 559 | width: 6 560 | color_stops: 561 | 0: '#FFF6E3' 562 | 10: '#FFE9B9' 563 | 20: '#FFDA8A' 564 | 30: '#FFCB5B' 565 | 40: '#FFBF37' 566 | 50: '#ffb414' 567 | 60: '#FFAD12' 568 | 70: '#FFA40E' 569 | 80: '#FF9C0B' 570 | 90: '#FF8C06' 571 | 572 | - type: horizontal-stack 573 | cards: 574 | 575 | # Example 7:: 576 | # 577 | ####################################################################### 578 | 579 | - type: 'custom:flex-horseshoe-card' 580 | 581 | entities: 582 | - entity: sensor.disk_use 583 | decimals: 1 584 | icon: mdi:harddisk 585 | name: '7: Disk Usage' 586 | area: Hestia 587 | - entity: sensor.disk_use_percent 588 | decimals: 1 589 | - entity: sensor.disk_free 590 | decimals: 1 591 | 592 | scaleTickSize: 50 593 | show: 594 | scale_tickmarks: true 595 | 596 | layout: 597 | icons: 598 | - id: 0 599 | entity_index: 0 600 | xpos: 50 601 | ypos: 20 602 | size: 3 603 | hlines: 604 | - id: 0 605 | xpos: 50 606 | ypos: 48 607 | length: 80 608 | styles: 609 | - opacity: 0.5; 610 | circles: 611 | - id: 0 612 | xpos: 50 613 | ypos: 61 614 | radius: 3 615 | styles: 616 | - fill : var(--primary-text-color); 617 | - opacity: 0.5; 618 | states: 619 | - id: 0 620 | entity_index: 0 621 | xpos: 50 622 | ypos: 40 623 | styles: 624 | - font-size: 3em; 625 | - opacity: 0.9; 626 | - id: 0 627 | entity_index: 1 628 | xpos: 46 629 | ypos: 64 630 | styles: 631 | - font-size: 1.7em; 632 | - text-anchor: end; 633 | - id: 2 634 | entity_index: 2 635 | xpos: 54 636 | ypos: 64 637 | styles: 638 | - font-size: 1.7em; 639 | - text-anchor: start; 640 | names: 641 | - id: 0 642 | entity_index: 0 643 | xpos: 50 644 | ypos: 100 645 | areas: 646 | - id: 0 647 | entity_index: 0 648 | xpos: 50 649 | ypos: 80 650 | horseshoe_scale: 651 | min: 0 652 | max: 215 653 | color_stops: 654 | 0: '#FFF6E3' 655 | 215: '#FF8C06' 656 | 657 | # Example 8:: 658 | # 659 | ####################################################################### 660 | 661 | - type: 'custom:flex-horseshoe-card' 662 | 663 | entities: 664 | - entity: sensor.processor_use 665 | decimals: 0 666 | icon: mdi:memory 667 | name: '8: CPU Load' 668 | area: Hestia 669 | - entity: sensor.load_1m 670 | decimals: 2 671 | unit: '1m' 672 | - entity: sensor.load_5m 673 | decimals: 2 674 | unit: '5m' 675 | 676 | show: 677 | scale_tickmarks: true 678 | layout: 679 | hlines: 680 | - id: 0 681 | xpos: 50 682 | ypos: 38 683 | length: 40 684 | styles: 685 | - stroke: var(--primary-text-color); 686 | - stroke-width: 5; 687 | - opacity: 0.9; 688 | - stroke-linecap: round; 689 | color: '#FFF6E3' 690 | vlines: 691 | - id: 0 692 | xpos: 50 693 | ypos: 56 694 | length: 20 695 | styles: 696 | - stroke: var(--primary-text-color); 697 | - opacity: 0.5; 698 | - stroke-width: 2; 699 | - stroke-linecap: round; 700 | states: 701 | - id: 0 702 | entity_index: 0 703 | xpos: 50 704 | ypos: 30 705 | styles: 706 | - font-size: 3em; 707 | - opacity: 0.9; 708 | - id: 1 709 | entity_index: 1 710 | xpos: 46 711 | ypos: 54 712 | styles: 713 | - font-size: 1.5em; 714 | - text-anchor: end; 715 | - id: 2 716 | entity_index: 2 717 | xpos: 54 718 | ypos: 54 719 | styles: 720 | - font-size: 1.5em; 721 | - text-anchor: start; 722 | names: 723 | - id: 0 724 | entity_index: 0 725 | xpos: 50 726 | ypos: 100 727 | styles: 728 | - font-size: 1.2em; 729 | - id: 1 730 | entity_index: 1 731 | xpos: 46 732 | ypos: 62 733 | styles: 734 | - font-size: 0.8em; 735 | - text-anchor: end; 736 | - opacity: 0.7; 737 | - id: 2 738 | entity_index: 2 739 | xpos: 54 740 | ypos: 62 741 | styles: 742 | - font-size: 0.8em; 743 | - text-anchor: start; 744 | - opacity: 0.7; 745 | areas: 746 | - id: 0 747 | entity_index: 0 748 | xpos: 50 749 | ypos: 85 750 | styles: 751 | - font-size: 1.2em; 752 | 753 | horseshoe_state: 754 | color: '#FFF6E3' 755 | horseshoe_scale: 756 | min: 0 757 | max: 100 758 | color: 'var(--primary-background-color)' 759 | width: 6 760 | color_stops: 761 | 0: '#FFF6E3' 762 | 10: '#FFE9B9' 763 | 20: '#FFDA8A' 764 | 30: '#FFCB5B' 765 | 40: '#FFBF37' 766 | 50: '#ffb414' 767 | 60: '#FFAD12' 768 | 70: '#FFA40E' 769 | 80: '#FF9C0B' 770 | 90: '#FF8C06' 771 | 772 | 773 | - type: horizontal-stack 774 | cards: 775 | 776 | # Example 11:: 777 | # 778 | ####################################################################### 779 | 780 | - type: 'custom:flex-horseshoe-card' 781 | entities: 782 | - entity: sensor.memory_use_percent 783 | decimals: 0 784 | name: '11: One Bulb' 785 | area: Hestia 786 | unit: W 787 | decimals: 0 788 | tap_action: 789 | action: more-info 790 | - entity: light.1st_floor_hall_light 791 | name: 'hall' 792 | icon: mdi:lightbulb 793 | tap_action: 794 | action: call-service 795 | service: light.toggle 796 | service_data: { "entity_id" : "light.1st_floor_hall_light" } 797 | 798 | animations: 799 | entity.1: 800 | - state: 'on' 801 | circles: 802 | - animation_id: 11 803 | styles: 804 | - fill: var(--secondary-text-color); 805 | - opacity: 0.9; 806 | - animation: jello 1s ease-in-out both; 807 | - transform-origin: center; 808 | icons: 809 | - animation_id: 0 810 | styles: 811 | - fill: white; 812 | - state: 'off' 813 | circles: 814 | - animation_id: 11 815 | reuse: true 816 | styles: 817 | - transform-origin: center; 818 | - animation: zoomOut 1s ease-out both; 819 | icons: 820 | - animation_id: 0 821 | styles: 822 | - fill: var(--primary-text-color); 823 | 824 | show: 825 | horseshoe_style: 'lineargradient' 826 | 827 | layout: 828 | states: 829 | - id: 0 830 | entity_index: 0 831 | xpos: 50 832 | ypos: 28 833 | uom_font_size: 1.5 834 | styles: 835 | - font-size: 2.5em; 836 | - opacity: 0.9; 837 | names: 838 | - id: 0 839 | entity_index: 0 840 | xpos: 50 841 | ypos: 100 842 | styles: 843 | - font-size: 1.2em; 844 | - opacity: 0.7; 845 | - id: 1 846 | entity_index: 1 847 | xpos: 50 848 | ypos: 78 849 | styles: 850 | - font-size: 1.5em; 851 | icons: 852 | - id: 0 853 | animation_id: 0 854 | xpos: 50 855 | ypos: 55 856 | entity_index: 1 857 | icon_size: 3.5 858 | styles: 859 | - color: var(--primary-text-color); 860 | circles: 861 | - id: 0 862 | animation_id: 0 863 | xpos: 50 864 | ypos: 50 865 | radius: 35 866 | styles: 867 | - fill: var(--primary-background-color); 868 | - id: 1 869 | animation_id: 11 870 | xpos: 50 871 | ypos: 50 872 | radius: 30 873 | entity_index: 1 874 | styles: 875 | - fill: var(--primary-background-color); 876 | 877 | horseshoe_scale: 878 | min: 0 879 | max: 100 880 | width: 6 881 | color: 'var(--primary-background-color)' 882 | horseshoe_state: 883 | width: 12 884 | color: '#FFF6E3' 885 | color_stops: 886 | 0: '#FFF6E3' 887 | 10: '#FFE9B9' 888 | 20: '#FFDA8A' 889 | 30: '#FFCB5B' 890 | 40: '#FFBF37' 891 | 50: '#ffb414' 892 | 60: '#FFAD12' 893 | 70: '#FFA40E' 894 | 80: '#FF9C0B' 895 | 90: '#FF8C06' 896 | 897 | # Example 12:: 898 | # 899 | ####################################################################### 900 | 901 | - type: 'custom:flex-horseshoe-card' 902 | entities: 903 | # Abuse the memory_use_percent sensor as the wattage the bulbs use. Just to show the possibilities 904 | - entity: sensor.memory_use_percent 905 | decimals: 0 906 | name: '12: Two Bulbs' 907 | area: Hestia 908 | unit: W 909 | decimals: 0 910 | tap_action: 911 | action: more-info 912 | 913 | # The left light displayed on the card. Index 1 914 | - entity: light.1st_floor_hall_light 915 | name: 'hall' 916 | icon: mdi:lightbulb 917 | tap_action: 918 | action: call-service 919 | service: light.toggle 920 | service_data: { "entity_id" : "light.1st_floor_hall_light" } 921 | 922 | # The right light displayed on the card. Index 2 923 | - entity: light.gledopto 924 | name: 'opto' 925 | icon: mdi:lightbulb 926 | tap_action: 927 | action: call-service 928 | service: light.toggle 929 | service_data: { "entity_id" : "light.gledopto" } 930 | 931 | animations: 932 | # Animations for the second entity, index 1 933 | entity.1: 934 | - state: 'on' 935 | circles: 936 | - animation_id: 11 937 | styles: 938 | - fill: var(--secondary-text-color); 939 | - opacity: 0.9; 940 | - transform-origin: 30% 50%; 941 | - animation: jello 1s ease-in-out both; 942 | icons: 943 | - animation_id: 10 944 | styles: 945 | - fill: white; 946 | - state: 'off' 947 | circles: 948 | - animation_id: 11 949 | reuse: true 950 | styles: 951 | - transform-origin: 30% 50%; 952 | - animation: zoomOut 1s ease-out both; 953 | icons: 954 | - animation_id: 10 955 | styles: 956 | - fill: var(--primary-text-color); 957 | 958 | # Animations for the third entity, index 2 959 | entity.2: 960 | - state: 'on' 961 | circles: 962 | - animation_id: 21 963 | styles: 964 | - fill: var(--secondary-text-color); 965 | - stroke-width: 2; 966 | - stroke: var(--primary-background-color); 967 | - opacity: 0.9; 968 | - stroke-dasharray: 94; 969 | - stroke-dashoffset: 1000; 970 | - animation: stroke 2s ease-out forwards; 971 | 972 | icons: 973 | - animation_id: 20 974 | styles: 975 | - fill: white; 976 | 977 | - state: 'off' 978 | circles: 979 | - animation_id: 21 980 | styles: 981 | - fill: var(--primary-background-color); 982 | - opacity: 0.7; 983 | icons: 984 | - animation_id: 20 985 | styles: 986 | - fill: var(--primary-text-color); 987 | 988 | show: 989 | horseshoe_style: 'fixed' 990 | layout: 991 | states: 992 | - id: 0 993 | entity_index: 0 994 | animation_id: 0 995 | xpos: 50 996 | ypos: 28 997 | uom_font_size: 1.5 998 | styles: 999 | - font-size: 2.5em; 1000 | - opacity: 0.9; 1001 | names: 1002 | - id: 0 1003 | animation_id: 0 1004 | entity_index: 0 1005 | xpos: 50 1006 | ypos: 100 1007 | styles: 1008 | - font-size: 1.2em; 1009 | - opacity: 0.7; 1010 | - id: 1 1011 | animation_id: 1 1012 | entity_index: 1 1013 | xpos: 30 1014 | ypos: 78 1015 | styles: 1016 | - font-size: 1.2em; 1017 | - id: 2 1018 | animation_id: 2 1019 | entity_index: 2 1020 | xpos: 70 1021 | ypos: 78 1022 | styles: 1023 | - font-size: 1.2em; 1024 | icons: 1025 | - id: 0 1026 | animation_id: 10 1027 | xpos: 30 1028 | ypos: 55 1029 | entity_index: 1 1030 | icon_size: 3.5 1031 | styles: 1032 | - color: var(--primary-text-color); 1033 | - id: 1 1034 | animation_id: 20 1035 | xpos: 70 1036 | ypos: 55 1037 | entity_index: 2 1038 | icon_size: 3.5 1039 | styles: 1040 | - color: var(--primary-text-color); 1041 | circles: 1042 | - animation_id: 3 1043 | xpos: 30 1044 | ypos: 50 1045 | radius: 35 1046 | styles: 1047 | - fill: var(--primary-background-color); 1048 | - animation_id: 11 1049 | xpos: 30 1050 | ypos: 50 1051 | radius: 30 1052 | entity_index: 1 1053 | 1054 | - animation_id: 2 1055 | xpos: 70 1056 | ypos: 50 1057 | radius: 35 1058 | styles: 1059 | - fill: var(--primary-background-color); 1060 | - animation_id: 21 1061 | xpos: 70 1062 | ypos: 50 1063 | radius: 30 1064 | entity_index: 2 1065 | 1066 | horseshoe_scale: 1067 | min: 0 1068 | max: 100 1069 | color: 'var(--primary-background-color)' 1070 | horseshoe_state: 1071 | color: '#FFDA8A' 1072 | color_stops: 1073 | 0: '#FFF6E3' 1074 | 10: '#FFE9B9' 1075 | 20: '#FFDA8A' 1076 | 30: '#FFCB5B' 1077 | 40: '#FFBF37' 1078 | 50: '#ffb414' 1079 | 60: '#FFAD12' 1080 | 70: '#FFA40E' 1081 | 80: '#FF9C0B' 1082 | 90: '#FF8C06' 1083 | # The @keyframes stroke runs the stroke animation for the second lightbulb, entity light.gledopto 1084 | style: | 1085 | @keyframes stroke { to { stroke-dashoffset: 0; } } 1086 | 1087 | 1088 | 1089 | - type: horizontal-stack 1090 | cards: 1091 | 1092 | # Example 9:: or 5b:: 1093 | # 1094 | ####################################################################### 1095 | 1096 | - type: 'custom:flex-horseshoe-card' 1097 | 1098 | entities: 1099 | - entity: sensor.memory_use_percent 1100 | decimals: 0 1101 | icon: mdi:memory 1102 | name: '5b: RAM Usage' 1103 | area: Hestia 1104 | - entity: sensor.memory_use 1105 | decimals: 0 1106 | name: '(In Use)' 1107 | - entity: sensor.memory_free 1108 | decimals: 0 1109 | name: '(free)' 1110 | 1111 | card_filter: card--dropshadow-heavy--sepia90 1112 | show: 1113 | scale_tickmarks: true 1114 | layout: 1115 | hlines: 1116 | - id: 0 1117 | xpos: 50 1118 | ypos: 38 1119 | length: 40 1120 | styles: 1121 | - stroke: var(--primary-text-color); 1122 | - stroke-width: 5; 1123 | - opacity: 0.9; 1124 | - stroke-linecap: round; 1125 | vlines: 1126 | - id: 0 1127 | xpos: 50 1128 | ypos: 56 1129 | length: 20 1130 | styles: 1131 | - stroke: var(--primary-text-color); 1132 | - opacity: 0.5; 1133 | - stroke-width: 2; 1134 | - stroke-linecap: round; 1135 | states: 1136 | - id: 0 1137 | entity_index: 0 1138 | xpos: 50 1139 | ypos: 30 1140 | styles: 1141 | - font-size: 3em; 1142 | - opacity: 0.9; 1143 | - id: 1 1144 | entity_index: 1 1145 | xpos: 46 1146 | ypos: 54 1147 | styles: 1148 | - font-size: 1.5em; 1149 | - text-anchor: end; 1150 | - id: 2 1151 | entity_index: 2 1152 | xpos: 54 1153 | ypos: 54 1154 | styles: 1155 | - font-size: 1.5em; 1156 | - text-anchor: start; 1157 | names: 1158 | - id: 0 1159 | entity_index: 0 1160 | xpos: 50 1161 | ypos: 100 1162 | styles: 1163 | - font-size: 1.2em; 1164 | - id: 1 1165 | entity_index: 1 1166 | xpos: 46 1167 | ypos: 62 1168 | styles: 1169 | - font-size: 0.8em; 1170 | - text-anchor: end; 1171 | - opacity: 0.7; 1172 | - id: 2 1173 | entity_index: 2 1174 | xpos: 54 1175 | ypos: 62 1176 | styles: 1177 | - font-size: 0.8em; 1178 | - text-anchor: start; 1179 | - opacity: 0.7; 1180 | areas: 1181 | - id: 0 1182 | entity_index: 0 1183 | xpos: 50 1184 | ypos: 85 1185 | styles: 1186 | - font-size: 1.2em; 1187 | 1188 | horseshoe_state: 1189 | color: '#FFF6E3' 1190 | horseshoe_scale: 1191 | min: 0 1192 | max: 100 1193 | color: 'var(--primary-background-color)' 1194 | width: 6 1195 | color_stops: 1196 | 0: '#FFF6E3' 1197 | 10: '#FFE9B9' 1198 | 20: '#FFDA8A' 1199 | 30: '#FFCB5B' 1200 | 40: '#FFBF37' 1201 | 50: '#ffb414' 1202 | 60: '#FFAD12' 1203 | 70: '#FFA40E' 1204 | 80: '#FF9C0B' 1205 | 90: '#FF8C06' 1206 | 1207 | # Example 10:: or 7b:: 1208 | # 1209 | ####################################################################### 1210 | 1211 | - type: 'custom:flex-horseshoe-card' 1212 | 1213 | entities: 1214 | - entity: sensor.disk_use 1215 | decimals: 1 1216 | icon: mdi:harddisk 1217 | name: '7b: Disk Usage' 1218 | area: Hestia 1219 | - entity: sensor.disk_use_percent 1220 | decimals: 1 1221 | - entity: sensor.disk_free 1222 | decimals: 1 1223 | 1224 | show: 1225 | scale_tickmarks: true 1226 | card_filter: card--dropshadow-light--sepia90 1227 | 1228 | layout: 1229 | icons: 1230 | - id: 0 1231 | entity_index: 0 1232 | xpos: 50 1233 | ypos: 20 1234 | size: 3 1235 | hlines: 1236 | - id: 0 1237 | xpos: 50 1238 | ypos: 48 1239 | length: 80 1240 | styles: 1241 | - opacity: 0.5; 1242 | circles: 1243 | - id: 0 1244 | xpos: 50 1245 | ypos: 61 1246 | radius: 3 1247 | styles: 1248 | - fill : var(--primary-text-color); 1249 | - opacity: 0.5; 1250 | states: 1251 | - id: 0 1252 | entity_index: 0 1253 | xpos: 50 1254 | ypos: 40 1255 | styles: 1256 | - font-size: 3em; 1257 | - opacity: 0.9; 1258 | - id: 0 1259 | entity_index: 1 1260 | xpos: 46 1261 | ypos: 64 1262 | styles: 1263 | - font-size: 1.7em; 1264 | - text-anchor: end; 1265 | - id: 2 1266 | entity_index: 2 1267 | xpos: 54 1268 | ypos: 64 1269 | styles: 1270 | - font-size: 1.7em; 1271 | - text-anchor: start; 1272 | names: 1273 | - id: 0 1274 | entity_index: 0 1275 | xpos: 50 1276 | ypos: 100 1277 | areas: 1278 | - id: 0 1279 | entity_index: 0 1280 | xpos: 50 1281 | ypos: 80 1282 | horseshoe_scale: 1283 | min: 0 1284 | max: 215 1285 | ticksize: 50 1286 | 1287 | color_stops: 1288 | 0: '#FFF6E3' 1289 | 215: '#FF8C06' 1290 | -------------------------------------------------------------------------------- /flex-horseshoe-card.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Card : flex-horseshoe-card.js 4 | * Project : Home Assistant 5 | * Repository: https://github.com/AmoebeLabs/ 6 | * 7 | * Author : Mars @ AmoebeLabs.com 8 | * 9 | * License : MIT 10 | * 11 | * ----- 12 | * Description: 13 | * The Flexible Horseshoe Card. 14 | * 15 | * Refs: 16 | * - https://github.com/AmoebeLabs/flex-horseshoe-card 17 | * 18 | ******************************************************************************* 19 | */ 20 | 21 | import { 22 | LitElement, 23 | html, 24 | css, 25 | svg 26 | } from "https://unpkg.com/lit-element@2.0.1/lit-element.js?module"; 27 | 28 | console.info( 29 | `%c FLEX-HORSESHOE-CARD \n%c Version 1.2 `, 30 | 'color: yellow; font-weight: bold; background: black', 31 | 'color: white; font-weight: bold; background: dimgray', 32 | ); 33 | 34 | //++ Consts ++++++++++ 35 | const FONT_SIZE = 12; 36 | const SVG_VIEW_BOX = 200; 37 | 38 | // Donut starts at -220 degrees and is 260 degrees in size. 39 | // zero degrees is at 3 o'clock. 40 | const HORSESHOE_RADIUS_SIZE = 0.45 * SVG_VIEW_BOX; 41 | const TICKMARKS_RADIUS_SIZE = 0.43 * SVG_VIEW_BOX; 42 | const HORSESHOE_PATH_LENGTH = 2 * 260/360 * Math.PI * HORSESHOE_RADIUS_SIZE; 43 | 44 | const DEFAULT_SHOW = { 45 | horseshoe: true, 46 | scale_tickmarks: false, 47 | horseshoe_style: 'fixed', 48 | } 49 | 50 | const DEFAULT_HORSESHOE_SCALE = { 51 | min: 0, 52 | max: 100, 53 | width: 6, 54 | color: 'var(--primary-background-color)', 55 | } 56 | 57 | const DEFAULT_HORSESHOE_STATE = { 58 | width: 12, 59 | color: 'var(--primary-color)', 60 | } 61 | 62 | const DEFAULT_TAP_ACTION = { 63 | action: "more-info" 64 | } 65 | 66 | //-- 67 | 68 | //++ Class ++++++++++ 69 | 70 | class FlexHorseshoeCard extends LitElement { 71 | constructor() { 72 | super(); 73 | 74 | // Get cardId for unique SVG gradient Id 75 | this.cardId = Math.random().toString(36).substr(2, 9); 76 | this.entities = []; 77 | this.entitiesStr = []; 78 | this.attributesStr = []; 79 | this.viewBoxSize = SVG_VIEW_BOX; 80 | this.colorStops = {}; 81 | this.animations = {}; 82 | this.animations.vlines = {}; 83 | this.animations.hlines = {}; 84 | this.animations.circles = {}; 85 | this.animations.icons = {}; 86 | this.animations.names = {}; 87 | this.animations.areas = {}; 88 | this.animations.states = {}; 89 | 90 | this.colorCache = {}; 91 | 92 | // http://jsfiddle.net/jlubean/dL5cLjxt/ 93 | //this.isSafari = !!navigator.userAgent.match(/Version\/[\d\.]+.*Safari/); 94 | // this.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; 95 | 96 | // 2020.11.16 97 | // See: https://javascriptio.com/view/10924/detect-if-device-is-ios 98 | // After iOS 13 you should detect iOS devices like this, since iPad will not be detected as iOS devices 99 | // by old ways (due to new "desktop" options, enabled by default) 100 | 101 | this.isAndroid = !!navigator.userAgent.match(/Android/); 102 | if (!this.isAndroid) { 103 | this.isSafari = !!navigator.userAgent.match(/Version\/[\d\.]+.*Safari/); 104 | this.iOS = (/iPad|iPhone|iPod/.test(navigator.userAgent) || 105 | (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)) && 106 | !window.MSStream; 107 | } 108 | } 109 | 110 | /******************************************************************************* 111 | * Summary. 112 | * Implements the properties method 113 | * 114 | */ 115 | /* 116 | static get properties() { 117 | return { 118 | hass: {}, 119 | config: {}, 120 | states: [], 121 | statesStr: [], 122 | 123 | dashArray: String, 124 | color1_offset: String, 125 | color0: String, 126 | color1: String, 127 | angleCoords: Object 128 | } 129 | } 130 | */ 131 | /******************************************************************************* 132 | * styles() 133 | * 134 | * Summary. 135 | * Returns the static CSS styles for the lit-element 136 | * 137 | * Note: 138 | * - The BEM (http://getbem.com/naming/) naming style for CSS is used 139 | * Of course, if no mistakes are made ;-) 140 | * 141 | */ 142 | static get styles() { 143 | 144 | return css` 145 | :host { 146 | cursor: pointer; 147 | } 148 | 149 | @media (print), (prefers-reduced-motion: reduce) { 150 | .animated { 151 | animation-duration: 1ms !important; 152 | transition-duration: 1ms !important; 153 | animation-iteration-count: 1 !important; 154 | } 155 | } 156 | 157 | @keyframes zoomOut { 158 | from { 159 | opacity: 1; 160 | } 161 | 162 | 50% { 163 | opacity: 0; 164 | transform: scale3d(0.3, 0.3, 0.3); 165 | } 166 | 167 | to { 168 | opacity: 0; 169 | } 170 | } 171 | 172 | @keyframes bounce { 173 | from, 174 | 20%, 175 | 53%, 176 | 80%, 177 | to { 178 | animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); 179 | transform: translate3d(0, 0, 0); 180 | } 181 | 182 | 40%, 183 | 43% { 184 | animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); 185 | transform: translate3d(0, -30px, 0); 186 | } 187 | 188 | 70% { 189 | animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); 190 | transform: translate3d(0, -15px, 0); 191 | } 192 | 193 | 90% { 194 | transform: translate3d(0, -4px, 0); 195 | } 196 | } 197 | 198 | @keyframes flash { 199 | from, 200 | 50%, 201 | to { 202 | opacity: 1; 203 | } 204 | 205 | 25%, 206 | 75% { 207 | opacity: 0; 208 | } 209 | } 210 | 211 | @keyframes headShake { 212 | 0% { 213 | transform: translateX(0); 214 | } 215 | 216 | 6.5% { 217 | transform: translateX(-6px) rotateY(-9deg); 218 | } 219 | 220 | 18.5% { 221 | transform: translateX(5px) rotateY(7deg); 222 | } 223 | 224 | 31.5% { 225 | transform: translateX(-3px) rotateY(-5deg); 226 | } 227 | 228 | 43.5% { 229 | transform: translateX(2px) rotateY(3deg); 230 | } 231 | 232 | 50% { 233 | transform: translateX(0); 234 | } 235 | } 236 | 237 | @keyframes heartBeat { 238 | 0% { 239 | transform: scale(1); 240 | } 241 | 242 | 14% { 243 | transform: scale(1.3); 244 | } 245 | 246 | 28% { 247 | transform: scale(1); 248 | } 249 | 250 | 42% { 251 | transform: scale(1.3); 252 | } 253 | 254 | 70% { 255 | transform: scale(1); 256 | } 257 | } 258 | 259 | @keyframes jello { 260 | from, 261 | 11.1%, 262 | to { 263 | transform: translate3d(0, 0, 0); 264 | } 265 | 266 | 22.2% { 267 | transform: skewX(-12.5deg) skewY(-12.5deg); 268 | } 269 | 270 | 33.3% { 271 | transform: skewX(6.25deg) skewY(6.25deg); 272 | } 273 | 274 | 44.4% { 275 | transform: skewX(-3.125deg) skewY(-3.125deg); 276 | } 277 | 278 | 55.5% { 279 | transform: skewX(1.5625deg) skewY(1.5625deg); 280 | } 281 | 282 | 66.6% { 283 | transform: skewX(-0.78125deg) skewY(-0.78125deg); 284 | } 285 | 286 | 77.7% { 287 | transform: skewX(0.390625deg) skewY(0.390625deg); 288 | } 289 | 290 | 88.8% { 291 | transform: skewX(-0.1953125deg) skewY(-0.1953125deg); 292 | } 293 | } 294 | 295 | @keyframes pulse { 296 | from { 297 | transform: scale3d(1, 1, 1); 298 | } 299 | 300 | 50% { 301 | transform: scale3d(1.05, 1.05, 1.05); 302 | } 303 | 304 | to { 305 | transform: scale3d(1, 1, 1); 306 | } 307 | } 308 | 309 | @keyframes rubberBand { 310 | from { 311 | transform: scale3d(1, 1, 1); 312 | } 313 | 314 | 30% { 315 | transform: scale3d(1.25, 0.75, 1); 316 | } 317 | 318 | 40% { 319 | transform: scale3d(0.75, 1.25, 1); 320 | } 321 | 322 | 50% { 323 | transform: scale3d(1.15, 0.85, 1); 324 | } 325 | 326 | 65% { 327 | transform: scale3d(0.95, 1.05, 1); 328 | } 329 | 330 | 75% { 331 | transform: scale3d(1.05, 0.95, 1); 332 | } 333 | 334 | to { 335 | transform: scale3d(1, 1, 1); 336 | } 337 | } 338 | 339 | @keyframes shake { 340 | from, 341 | to { 342 | transform: translate3d(0, 0, 0); 343 | } 344 | 345 | 10%, 346 | 30%, 347 | 50%, 348 | 70%, 349 | 90% { 350 | transform: translate3d(-10px, 0, 0); 351 | } 352 | 353 | 20%, 354 | 40%, 355 | 60%, 356 | 80% { 357 | transform: translate3d(10px, 0, 0); 358 | } 359 | } 360 | 361 | @keyframes swing { 362 | 20% { 363 | transform: rotate3d(0, 0, 1, 15deg); 364 | } 365 | 366 | 40% { 367 | transform: rotate3d(0, 0, 1, -10deg); 368 | } 369 | 370 | 60% { 371 | transform: rotate3d(0, 0, 1, 5deg); 372 | } 373 | 374 | 80% { 375 | transform: rotate3d(0, 0, 1, -5deg); 376 | } 377 | 378 | to { 379 | transform: rotate3d(0, 0, 1, 0deg); 380 | } 381 | } 382 | 383 | @keyframes tada { 384 | from { 385 | transform: scale3d(1, 1, 1); 386 | } 387 | 10%, 388 | 20% { 389 | transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg); 390 | } 391 | 30%, 392 | 50%, 393 | 70%, 394 | 90% { 395 | transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg); 396 | } 397 | 40%, 398 | 60%, 399 | 80% { 400 | transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg); 401 | } 402 | to { 403 | transform: scale3d(1, 1, 1); 404 | } 405 | } 406 | 407 | 408 | @keyframes wobble { 409 | from { 410 | transform: translate3d(0, 0, 0); 411 | } 412 | 15% { 413 | transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg); 414 | } 415 | 30% { 416 | transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg); 417 | } 418 | 45% { 419 | transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg); 420 | } 421 | 60% { 422 | transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg); 423 | } 424 | 75% { 425 | transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg); 426 | } 427 | to { 428 | transform: translate3d(0, 0, 0); 429 | } 430 | } 431 | 432 | 433 | @media screen and (min-width: 467px) { 434 | :host { 435 | font-size: 12px; 436 | } 437 | } 438 | @media screen and (max-width: 466px) { 439 | :host { 440 | font-size: 12px; 441 | } 442 | } 443 | 444 | :host ha-card { 445 | padding: 10px 10px 0px 10px; 446 | } 447 | 448 | .container { 449 | position: relative; 450 | height: 100%; 451 | display: flex; 452 | flex-direction: column; 453 | } 454 | 455 | .labelContainer { 456 | position: absolute; 457 | top: 0; 458 | left: 0; 459 | width: 100%; 460 | height: 65%; 461 | display: flex; 462 | flex-direction: column; 463 | align-items: center; 464 | justify-content: flex-end; 465 | } 466 | 467 | .ellipsis { 468 | text-overflow: ellipsis; 469 | white-space: nowrap; 470 | overflow: hidden; 471 | } 472 | 473 | .state { 474 | position: relative; 475 | display: flex; 476 | flex-wrap: wrap; 477 | max-width: 100%; 478 | min-width: 0px; 479 | } 480 | 481 | #label { 482 | display: flex; 483 | line-height: 1; 484 | } 485 | 486 | #label.bold { 487 | font-weight: bold; 488 | } 489 | 490 | #label, #name { 491 | margin: 3% 0; 492 | } 493 | 494 | .text { 495 | font-size: 100%; 496 | } 497 | 498 | #name { 499 | font-size: 80%; 500 | font-weight: 300; 501 | } 502 | 503 | .unit { 504 | font-size: 65%; 505 | font-weight: normal; 506 | opacity: 0.6; 507 | line-height: 2em; 508 | vertical-align: bottom; 509 | margin-left: 0.25rem; 510 | } 511 | 512 | .entity__area { 513 | position: absolute; 514 | top: 70%; 515 | font-size: 120%; 516 | opacity: 0.6; 517 | display: flex; 518 | line-height: 1; 519 | align-items: center; 520 | justify-content: center; 521 | width: 100%; 522 | height: 20%; 523 | flex-direction: column; 524 | } 525 | 526 | .nam { 527 | alignment-baseline: central; 528 | fill: var(--primary-text-color); 529 | } 530 | 531 | .state__uom { 532 | font-size: 20px; 533 | opacity: 0.7; 534 | margin: 0; 535 | fill : var(--primary-text-color); 536 | } 537 | 538 | .state__value { 539 | font-size: 3em; 540 | opacity: 1; 541 | fill : var(--primary-text-color); 542 | text-anchor: middle; 543 | } 544 | .entity__name { 545 | text-anchor: middle; 546 | overflow: hidden; 547 | opacity: 0.8; 548 | fill : var(--primary-text-color); 549 | font-size: 1.5em; 550 | text-transform: uppercase; 551 | letter-spacing: 0.1em; 552 | } 553 | 554 | .entity__area { 555 | font-size: 12px; 556 | opacity: 0.7; 557 | overflow: hidden; 558 | fill : var(--primary-text-color); 559 | text-anchor: middle; 560 | text-transform: uppercase; 561 | letter-spacing: 0.1em; 562 | } 563 | 564 | .shadow { 565 | font-size: 30px; 566 | font-weight: 700; 567 | text-anchor: middle; 568 | } 569 | 570 | .card--dropshadow-5 { 571 | filter: drop-shadow(0 1px 0 #ccc) 572 | drop-shadow(0 2px 0 #c9c9c9) 573 | drop-shadow(0 3px 0 #bbb) 574 | drop-shadow(0 4px 0 #b9b9b9) 575 | drop-shadow(0 5px 0 #aaa) 576 | drop-shadow(0 6px 1px rgba(0,0,0,.1)) 577 | drop-shadow(0 0 5px rgba(0,0,0,.1)) 578 | drop-shadow(0 1px 3px rgba(0,0,0,.3)) 579 | drop-shadow(0 3px 5px rgba(0,0,0,.2)) 580 | drop-shadow(0 5px 10px rgba(0,0,0,.25)) 581 | drop-shadow(0 10px 10px rgba(0,0,0,.2)) 582 | drop-shadow(0 20px 20px rgba(0,0,0,.15)); 583 | } 584 | .card--dropshadow-medium--opaque--sepia90 { 585 | filter: drop-shadow(0.0em 0.05em 0px #b2a98f22) 586 | drop-shadow(0.0em 0.07em 0px #b2a98f55) 587 | drop-shadow(0.0em 0.10em 0px #b2a98f88) 588 | drop-shadow(0px 0.6em 0.9em rgba(0,0,0,0.15)) 589 | drop-shadow(0px 1.2em 0.15em rgba(0,0,0,0.1)) 590 | drop-shadow(0px 2.4em 2.5em rgba(0,0,0,0.1)) 591 | sepia(90%); 592 | } 593 | 594 | .card--dropshadow-heavy--sepia90 { 595 | filter: drop-shadow(0.0em 0.05em 0px #b2a98f22) 596 | drop-shadow(0.0em 0.07em 0px #b2a98f55) 597 | drop-shadow(0.0em 0.10em 0px #b2a98f88) 598 | drop-shadow(0px 0.3em 0.45em rgba(0,0,0,0.5)) 599 | drop-shadow(0px 0.6em 0.07em rgba(0,0,0,0.3)) 600 | drop-shadow(0px 1.2em 1.25em rgba(0,0,0,1)) 601 | drop-shadow(0px 1.8em 1.6em rgba(0,0,0,0.1)) 602 | drop-shadow(0px 2.4em 2.0em rgba(0,0,0,0.1)) 603 | drop-shadow(0px 3.0em 2.5em rgba(0,0,0,0.1)) 604 | sepia(90%); 605 | } 606 | 607 | .card--dropshadow-heavy { 608 | filter: drop-shadow(0.0em 0.05em 0px #b2a98f22) 609 | drop-shadow(0.0em 0.07em 0px #b2a98f55) 610 | drop-shadow(0.0em 0.10em 0px #b2a98f88) 611 | drop-shadow(0px 0.3em 0.45em rgba(0,0,0,0.5)) 612 | drop-shadow(0px 0.6em 0.07em rgba(0,0,0,0.3)) 613 | drop-shadow(0px 1.2em 1.25em rgba(0,0,0,1)) 614 | drop-shadow(0px 1.8em 1.6em rgba(0,0,0,0.1)) 615 | drop-shadow(0px 2.4em 2.0em rgba(0,0,0,0.1)) 616 | drop-shadow(0px 3.0em 2.5em rgba(0,0,0,0.1)); 617 | } 618 | 619 | .card--dropshadow-medium--sepia90 { 620 | filter: drop-shadow(0.0em 0.05em 0px #b2a98f) 621 | drop-shadow(0.0em 0.15em 0px #b2a98f) 622 | drop-shadow(0.0em 0.15em 0px #b2a98f) 623 | drop-shadow(0px 0.6em 0.9em rgba(0,0,0,0.15)) 624 | drop-shadow(0px 1.2em 0.15em rgba(0,0,0,0.1)) 625 | drop-shadow(0px 2.4em 2.5em rgba(0,0,0,0.1)) 626 | sepia(90%); 627 | } 628 | 629 | .card--dropshadow-medium { 630 | filter: drop-shadow(0.0em 0.05em 0px #b2a98f) 631 | drop-shadow(0.0em 0.15em 0px #b2a98f) 632 | drop-shadow(0.0em 0.15em 0px #b2a98f) 633 | drop-shadow(0px 0.6em 0.9em rgba(0,0,0,0.15)) 634 | drop-shadow(0px 1.2em 0.15em rgba(0,0,0,0.1)) 635 | drop-shadow(0px 2.4em 2.5em rgba(0,0,0,0.1)); 636 | } 637 | 638 | .card--dropshadow-light--sepia90 { 639 | filter: drop-shadow(0px 0.10em 0px #b2a98f) 640 | drop-shadow(0.1em 0.5em 0.2em rgba(0, 0, 0, .5)) 641 | sepia(90%); 642 | } 643 | 644 | .card--dropshadow-light { 645 | filter: drop-shadow(0px 0.10em 0px #b2a98f) 646 | drop-shadow(0.1em 0.5em 0.2em rgba(0, 0, 0, .5)); 647 | } 648 | 649 | .card--dropshadow-down-and-distant { 650 | filter: drop-shadow(0px 0.05em 0px #b2a98f) 651 | drop-shadow(0px 14px 10px rgba(0,0,0,0.15) 652 | drop-shadow(0px 24px 2px rgba(0,0,0,0.1)) 653 | drop-shadow(0px 34px 30px rgba(0,0,0,0.1)); 654 | } 655 | .card--filter-none { 656 | } 657 | 658 | .horseshoe__svg__group { 659 | transform: translateY(15%); 660 | } 661 | 662 | .line__horizontal { 663 | stroke: var(--primary-text-color); 664 | opacity: 0.3; 665 | stroke-width: 2; 666 | } 667 | 668 | .line__vertical { 669 | stroke: var(--primary-text-color); 670 | opacity: 0.3; 671 | stroke-width: 2; 672 | } 673 | 674 | .svg__dot { 675 | fill: var(--primary-text-color); 676 | opacity: 0.5; 677 | align-self: center; 678 | transform-origin: 50% 50%; 679 | } 680 | 681 | .icon { 682 | align: center; 683 | } 684 | 685 | `; 686 | } 687 | 688 | /******************************************************************************* 689 | * hass() 690 | * 691 | * Summary. 692 | * Updates hass data for the card 693 | * 694 | */ 695 | 696 | set hass(hass) { // This is a safe and fast method // Set ref to hass, use "_"for the name ;-) 697 | this._hass = hass; 698 | 699 | var entityHasChanged = false; 700 | 701 | // Update state strings and check for changes. 702 | // Only if changed, continue and force render 703 | var value; 704 | var index = 0; 705 | var attrSet = false; 706 | var newStateStr; 707 | for (value of this.config.entities) { 708 | this.entities[index] = hass.states[this.config.entities[index].entity]; 709 | 710 | // Get attribute state if specified and available 711 | if (this.config.entities[index].attribute) { 712 | if (this.entities[index].attributes[this.config.entities[index].attribute]) { 713 | newStateStr = this._buildState(this.entities[index].attributes[this.config.entities[index].attribute], this.config.entities[index]); 714 | if (newStateStr != this.attributesStr[index]) { 715 | this.attributesStr[index] = newStateStr; 716 | entityHasChanged = true; 717 | } 718 | attrSet = true; 719 | } 720 | } 721 | if (!attrSet) { 722 | newStateStr = this._buildState(this.entities[index].state, this.config.entities[index]); 723 | if (newStateStr != this.entitiesStr[index]) { 724 | this.entitiesStr[index] = newStateStr; 725 | entityHasChanged = true; 726 | } 727 | } 728 | 729 | index++; 730 | } 731 | 732 | if (!entityHasChanged) { 733 | return; 734 | } 735 | else { 736 | } 737 | 738 | // Use first state or attribute for displaying the horseshoe 739 | 740 | // #TODO: only if state or attribute has changed. 741 | var state = this.entities[0].state; 742 | if ((this.config.entities[0].attribute)) { 743 | if (this.entities[0].attributes[this.config.entities[0].attribute]) { 744 | state = this.entities[0].attributes[this.config.entities[0].attribute]; 745 | } 746 | } 747 | 748 | // Calculate the size of the arc to fill the dasharray with this 749 | // value. It will fill the horseshoe relative to the state and min/max 750 | // values given in the configuration. 751 | 752 | const min = this.config.horseshoe_scale.min || 0; 753 | const max = this.config.horseshoe_scale.max || 100; 754 | const val = Math.min(this._calculateValueBetween(min, max, state), 1); 755 | const score = val * HORSESHOE_PATH_LENGTH; 756 | const total = 10 * HORSESHOE_RADIUS_SIZE; 757 | this.dashArray = `${score} ${total}`; 758 | 759 | // We must draw the horseshoe. Depending on the stroke settings, we draw a fixed color, gradient, autominmax or colorstop 760 | // #TODO: only if state or attribute has changed. 761 | 762 | const strokeStyle = this.config.show.horseshoe_style; 763 | 764 | if (strokeStyle == 'fixed') { 765 | this.stroke_color = this.config.horseshoe_state.color; 766 | this.color0 = this.config.horseshoe_state.color; 767 | this.color1 = this.config.horseshoe_state.color; 768 | this.color1_offset = '0%'; 769 | // We could set the circle attributes, but we do it with a variable as we are using a gradient 770 | // to display the horseshoe circle .. .setAttribute('stroke', stroke); 771 | } 772 | else if (strokeStyle == 'autominmax') { 773 | // Use color0 and color1 for autoranging the color of the horseshoe 774 | const stroke = this._calculateStrokeColor(state, this.colorStopsMinMax, true); 775 | 776 | // We now use a gradient for the horseshoe, using two colors 777 | // Set these colors to the colorstop color... 778 | this.color0 = stroke; 779 | this.color1 = stroke; 780 | this.color1_offset = '0%'; 781 | } 782 | else if (strokeStyle == 'colorstop' || strokeStyle == 'colorstopgradient') { 783 | const stroke = this._calculateStrokeColor(state, this.colorStops, strokeStyle === 'colorstopgradient'); 784 | 785 | // We now use a gradient for the horseshoe, using two colors 786 | // Set these colors to the colorstop color... 787 | this.color0 = stroke; 788 | this.color1 = stroke; 789 | this.color1_offset = '0%'; 790 | } 791 | else if (strokeStyle == 'lineargradient') { 792 | // This has taken a lot of time to get a satisfying result, and it appeared much simpler than anticipated. 793 | // I don't understand it, but for a circle, a gradient from left/right with adjusted stop is enough ?!?!?! 794 | // No calculations to adjust the angle of the gradient, or rotating the gradient itself. 795 | // Weird, but it works. Not a 100% match, but it is good enough for now... 796 | 797 | // According to stackoverflow, these calculations / adjustments would be needed, but it isn't ;-) 798 | // Added from https://stackoverflow.com/questions/9025678/how-to-get-a-rotated-linear-gradient-svg-for-use-as-a-background-image 799 | const angleCoords = {'x1' : '0%', 'y1' : '0%', 'x2': '100%', 'y2' : '0%'}; 800 | this.color1_offset = `${Math.round((1-val)*100)}%`; 801 | 802 | this.angleCoords = angleCoords; 803 | } 804 | 805 | // Check for animations linked to an entity or attribute. 806 | // Set the dynamic animation depending on the state. 807 | // If the card is rendered, the render() functions will take this dynamic animation into account. 808 | // 809 | // #TODO: Determine animation only if specific state or attribute has changed... 810 | 811 | if (this.config.animations) Object.keys(this.config.animations).map(animation => { 812 | const entityIndex = animation.substr(Number(animation.indexOf('.') + 1)); 813 | this.config.animations[animation].map(item => { 814 | // if animation state not equals sensor state, return... Nothing to animate for this state... 815 | if (this.entities[entityIndex].state.toLowerCase() != item.state.toLowerCase()) return; 816 | 817 | if (item.vlines) { 818 | item.vlines.map(item2 => { 819 | if (!this.animations.vlines[item2.animation_id] || !item2.reuse) this.animations.vlines[item2.animation_id] = {}; 820 | this.animations.vlines[item2.animation_id] = Object.assign(this.animations.vlines[item2.animation_id], ...item2.styles); 821 | }) 822 | } 823 | 824 | if (item.hlines) { 825 | item.hlines.map(item2 => { 826 | if (!this.animations.hlines[item2.animation_id] || !item2.reuse) this.animations.hlines[item2.animation_id] = {}; 827 | this.animations.hlines[item2.animation_id] = Object.assign(this.animations.hlines[item2.animation_id], ...item2.styles); 828 | }) 829 | } 830 | 831 | if (item.circles) { 832 | item.circles.map(item2 => { 833 | if (!this.animations.circles[item2.animation_id] || !item2.reuse) this.animations.circles[item2.animation_id] = {}; 834 | this.animations.circles[item2.animation_id] = Object.assign(this.animations.circles[item2.animation_id], ...item2.styles); 835 | }) 836 | } 837 | 838 | if (item.icons) { 839 | item.icons.map(item2 => { 840 | if (!this.animations.icons[item2.animation_id] || !item2.reuse) this.animations.icons[item2.animation_id] = {}; 841 | this.animations.icons[item2.animation_id] = Object.assign(this.animations.icons[item2.animation_id], ...item2.styles); 842 | }) 843 | } 844 | 845 | if (item.states) { 846 | item.states.map(item2 => { 847 | if (!this.animations.states[item2.animation_id] || !item2.reuse) this.animations.states[item2.animation_id] = {}; 848 | this.animations.states[item2.animation_id] = Object.assign(this.animations.states[item2.animation_id], ...item2.styles); 849 | }) 850 | } 851 | 852 | }); 853 | }); 854 | 855 | // For now, always force update to render the card if any of the states or attributes have changed... 856 | // if (entityHasChanged) { this.requestUpdate();} 857 | this.requestUpdate(); 858 | } 859 | 860 | /******************************************************************************* 861 | * setConfig() 862 | * 863 | * Summary. 864 | * Sets/Updates the card configuration. Rarely called if the doc is right 865 | * 866 | */ 867 | 868 | setConfig(config) { 869 | config = JSON.parse(JSON.stringify(config)); 870 | 871 | if (!config.entities) { 872 | throw Error('No entities defined'); 873 | } 874 | if (!config.layout) { 875 | throw Error('No layout defined'); 876 | } 877 | if (!config.horseshoe_scale) { 878 | throw Error('No horseshoe scale defined'); 879 | } else { 880 | if ((!config.horseshoe_scale.min) && (!config.horseshoe_scale.min == 0) || (!config.horseshoe_scale.max)) { 881 | throw Error('No horseshoe min/max for scale defined'); 882 | } 883 | } 884 | if ((!config.color_stops) || (config.color_stops.length < 2)) { 885 | throw Error('No color_stops defined or not at least two colorstops'); 886 | } 887 | 888 | // testing 889 | if (config.entities) { 890 | const newdomain = this._computeDomain(config.entities[0].entity); 891 | if (newdomain != 'sensor') { 892 | // If not a sensor, check if attribute is a number. If so, continue, otherwise Error... 893 | if (config.entities[0].attribute && !isNaN(config.entities[0].attribute)) { 894 | throw Error('First entity or attribute must be a numbered sensorvalue, but is NOT'); 895 | } 896 | } 897 | } 898 | 899 | const newConfig = { 900 | texts: [], 901 | card_filter: 'card--filter-none', 902 | ...config, 903 | show: { ...DEFAULT_SHOW, ...config.show }, 904 | horseshoe_scale: { ...DEFAULT_HORSESHOE_SCALE, ...config.horseshoe_scale }, 905 | horseshoe_state: { ...DEFAULT_HORSESHOE_STATE, ...config.horseshoe_state }, 906 | }; 907 | 908 | for (var entityValue of newConfig.entities) { 909 | if (!entityValue.tap_action) { 910 | entityValue.tap_action = { ...DEFAULT_TAP_ACTION }; 911 | } 912 | } 913 | 914 | let colorStops = {}; 915 | // colorStops[newConfig.horseshoe_scale.min] = newConfig.horseshoe_state.color || '#03a9f4'; 916 | if (newConfig.color_stops) { 917 | Object.keys(newConfig.color_stops).forEach((key) => { 918 | colorStops[key] = newConfig.color_stops[key]; 919 | }); 920 | } 921 | 922 | const sortedStops = Object.keys(colorStops).map(n => Number(n)).sort((a, b) => a - b); 923 | this.colorStops = colorStops; 924 | this.sortedStops = sortedStops; 925 | 926 | // Create a colorStopsMinMax list for autominmax color determination 927 | let colorStopsMinMax = {}; 928 | colorStopsMinMax[newConfig.horseshoe_scale.min] = colorStops[sortedStops[0]]; 929 | colorStopsMinMax[newConfig.horseshoe_scale.max] = colorStops[sortedStops[(sortedStops.length)-1]]; 930 | 931 | this.colorStopsMinMax = colorStopsMinMax; 932 | 933 | // Now set the color0 and color1 for the gradient used in the horseshoe to the colors 934 | // Use default for now!! 935 | this.color0 = colorStops[sortedStops[0]]; 936 | this.color1 = colorStops[sortedStops[(sortedStops.length)-1]]; 937 | 938 | const angleCoords = {'x1' : '0%', 'y1' : '0%', 'x2': '100%', 'y2' : '0%'}; 939 | this.angleCoords = angleCoords; 940 | this.color1_offset = '0%'; 941 | 942 | this.config = newConfig; 943 | } 944 | 945 | /******************************************************************************* 946 | * connectedCallback() 947 | * 948 | * Summary. 949 | * 950 | */ 951 | connectedCallback() { 952 | super.connectedCallback(); 953 | } 954 | 955 | /******************************************************************************* 956 | * disconnectedCallback() 957 | * 958 | * Summary. 959 | * 960 | */ 961 | disconnectedCallback() { 962 | super.disconnectedCallback(); 963 | } 964 | 965 | /******************************************************************************* 966 | * render() 967 | * 968 | * Summary. 969 | * Renders the complete SVG based card according to the specified layout in which 970 | * the user can specify name, area, entities, lines and dots. 971 | * The horseshoe is rendered on the full card. This one can be moved a bit via CSS. 972 | * 973 | */ 974 | 975 | render({ config } = this) { 976 | return html` 977 | this.handlePopup(e, this.entities[0])} 979 | > 980 |
981 | ${this._renderSvg()} 982 |
983 | 984 | 990 |
991 | `; 992 | } 993 | 994 | /******************************************************************************* 995 | * renderTickMarks() 996 | * 997 | * Summary. 998 | * Renders the tick marks on the scale. 999 | * 1000 | */ 1001 | 1002 | _renderTickMarks() { 1003 | const { config, } = this; 1004 | if (!config) return; 1005 | if (!config.show) return; 1006 | if (!config.show.scale_tickmarks) return; 1007 | 1008 | const stroke = config.horseshoe_scale.color ? config.horseshoe_scale.color : 'var(--primary-background-color)'; 1009 | const tickSize = config.horseshoe_scale.ticksize ? config.horseshoe_scale.ticksize 1010 | : (config.horseshoe_scale.max - config.horseshoe_scale.min) / 10; 1011 | 1012 | // fullScale is 260 degrees. Hard coded for now... 1013 | const fullScale = 260; 1014 | const remainder = config.horseshoe_scale.min % tickSize; 1015 | const startTickValue = config.horseshoe_scale.min + (remainder == 0 ? 0 : (tickSize - remainder)); 1016 | const startAngle = ((startTickValue - config.horseshoe_scale.min) / 1017 | (config.horseshoe_scale.max - config.horseshoe_scale.min)) * fullScale; 1018 | var tickSteps = ((config.horseshoe_scale.max - startTickValue) / tickSize); 1019 | 1020 | // new 1021 | var steps = Math.floor(tickSteps); 1022 | const angleStepSize = (fullScale - startAngle) / tickSteps; 1023 | 1024 | // If steps exactly match the max. value/range, add extra step for that max value. 1025 | if ((Math.floor(((steps) * tickSize) + startTickValue)) <= (config.horseshoe_scale.max)) {steps++;} 1026 | 1027 | const radius = config.horseshoe_scale.width ? config.horseshoe_scale.width / 2 : 6/2; 1028 | var angle; 1029 | var scaleItems = []; 1030 | 1031 | // NTS: 1032 | // Value of -230 is weird. Should be -220. Can't find why... 1033 | var i; 1034 | for (i = 0; i < steps; i++) { 1035 | angle = startAngle + ((-230 + (360 - i*angleStepSize)) * Math.PI / 180); 1036 | scaleItems[i] = svg` 1037 | 1040 | `; 1041 | } 1042 | return svg`${scaleItems}`; 1043 | } 1044 | 1045 | /******************************************************************************* 1046 | * _renderSvg() 1047 | * 1048 | * Summary. 1049 | * Renders the SVG 1050 | * 1051 | * NTS: 1052 | * If height and width given for svg it equals the viewbox. The card is not scaled 1053 | * anymore to the full dimensions of the card given by hass/lovelace. 1054 | * Card or svg is also placed default at start of viewport (not box), and can be 1055 | * placed at start, center or end of viewport (Use align-self to center it). 1056 | * 1057 | * 1. If height and width are ommitted, the ha-card/viewport is forced to the x/y 1058 | * aspect ratio of the viewbox, ie 1:1. EXACTLY WHAT WE WANT! 1059 | * 2. If height and width are set to 100%, the viewport (or ha-card) forces the 1060 | * aspect-ratio on the svg. Although GetCardSize is set to 4, it seems the 1061 | * height is forced to 150px, so part of the viewbox/svg is not shown or 1062 | * out of proportion! 1063 | * 3. Setting the height/width also to 200/200 (same as viewbox), the horseshoe is 1064 | * displayed correctly, but doesn't scale to the max space of the ha-card/viewport. 1065 | * It also is displayed at the start of the viewport. For a large horizontal 1066 | * card this is ok, but in other cases, the center position would be better... 1067 | * - use align-self: center on the svg ...or... 1068 | * - use align-items: center on the parent container of the svg. 1069 | * 1070 | */ 1071 | _renderSvg() { 1072 | // For some reason, using a var/const for the viewboxsize doesn't work. 1073 | // Even if the Chrome inspector shows 200 200. So hardcode for now! 1074 | // const { viewBoxSize, } = this; 1075 | 1076 | const cardFilter = this.config.card_filter ? this.config.card_filter : 'card--filter-none'; 1077 | 1078 | return svg` 1079 | 1082 | ${this._renderHorseShoe()} 1083 | 1084 | ${this._renderCircles()} 1085 | ${this._renderHorizontalLines()} 1086 | ${this._renderVerticalLines()} 1087 | ${this._renderIcons()} 1088 | ${this._renderEntityAreas()} 1089 | ${this._renderEntityNames()} 1090 | ${this._renderStates()} 1091 | 1092 | 1093 | `; 1094 | } 1095 | /******************************************************************************* 1096 | * _renderHorseShoe() 1097 | * 1098 | * Summary. 1099 | * Renders the horseshoe group. 1100 | * 1101 | * Description. 1102 | * The horseshoes are rendered in a viewbox of 200x200 (SVG_VIEW_BOX). 1103 | * Both are centered with a radius of 45%, ie 200*0.45 = 90. 1104 | * 1105 | * The foreground horseshoe is always rendered as a gradient with two colors. 1106 | * 1107 | * The horseshoes are rotated 220 degrees and are 2 * 26/36 * Math.PI * r in size 1108 | * There you get your value of 408.4070449 ;-) 1109 | */ 1110 | 1111 | _renderHorseShoe() { 1112 | 1113 | if (!this.config.show.horseshoe) return; 1114 | 1115 | return svg` 1116 | 1117 | 1124 | 1125 | 1133 | 1134 | ${this._renderTickMarks()} 1135 | 1136 | `; 1137 | } 1138 | 1139 | /******************************************************************************* 1140 | * _renderEntityNames() 1141 | * 1142 | * Summary. 1143 | * Renders the given name to the card. If name not given a space is rendered. 1144 | * The location of the name is specified in the layout. 1145 | * 1146 | */ 1147 | 1148 | _renderEntityNames() { 1149 | const { 1150 | layout, 1151 | } = this.config; 1152 | 1153 | if (!layout) return; 1154 | if (!layout.names) return; 1155 | 1156 | const svgItems = layout.names.map(item => { 1157 | 1158 | // compute some styling elements if configured for this name item 1159 | const ENTITY_NAME_STYLES = { 1160 | "font-size": '1.5em;', 1161 | "color": 'var(--primary-text-color);', 1162 | "opacity": '1.0;', 1163 | "text-anchor": 'middle;' 1164 | }; 1165 | 1166 | // Get configuration styles as the default styles 1167 | let configStyle = {...ENTITY_NAME_STYLES}; 1168 | if (item.styles) configStyle = Object.assign(configStyle, ...item.styles); 1169 | 1170 | // Get the runtime styles, caused by states & animation settings 1171 | let stateStyle = {}; 1172 | if (this.animations.names[item.index]) 1173 | stateStyle = Object.assign(stateStyle, this.animations.names[item.index]); 1174 | 1175 | // Merge the two, where the runtime styles may overwrite the statically configured styles 1176 | configStyle = { ...configStyle, ...stateStyle}; 1177 | 1178 | // Convert javascript records to plain text, without "{}" and "," between the styles. 1179 | const configStyleStr = JSON.stringify(configStyle).slice(1, -1).replace(/"/g,"").replace(/,/g,""); 1180 | 1181 | const name = this._buildName(this.entities[item.entity_index], this.config.entities[item.entity_index]); 1182 | 1183 | return svg` 1184 | 1185 | ${name} 1186 | 1187 | `; 1188 | }) 1189 | 1190 | return svg`${svgItems}`; 1191 | } 1192 | 1193 | /******************************************************************************* 1194 | * _renderEntityAreas() 1195 | * 1196 | * Summary. 1197 | * Renders the given area to the card. If area not given a space is rendered. 1198 | * The location of the area is specified in the layout. 1199 | * 1200 | */ 1201 | 1202 | _renderEntityAreas() { 1203 | const { 1204 | layout, 1205 | } = this.config; 1206 | 1207 | if (!layout) return; 1208 | if (!layout.areas) return; 1209 | 1210 | const svgItems = layout.areas.map(item => { 1211 | const AREA_STYLES = { 1212 | "font-size": '1em;', 1213 | "color": 'var(--primary-text-color);', 1214 | "opacity": '1.0;', 1215 | "text-anchor": 'middle;' 1216 | }; 1217 | 1218 | // Get configuration styles as the default styles 1219 | let configStyle = {...AREA_STYLES}; 1220 | if (item.styles) configStyle = Object.assign(configStyle, ...item.styles); 1221 | 1222 | // Get the runtime styles, caused by states & animation settings 1223 | let stateStyle = {}; 1224 | if (this.animations.areas[item.index]) 1225 | stateStyle = Object.assign(stateStyle, this.animations.areas[item.index]); 1226 | 1227 | // Merge the two, where the runtime styles may overwrite the statically configured styles 1228 | configStyle = { ...configStyle, ...stateStyle}; 1229 | 1230 | // Convert javascript records to plain text, without "{}" and "," between the styles. 1231 | const configStyleStr = JSON.stringify(configStyle).slice(1, -1).replace(/"/g,"").replace(/,/g,""); 1232 | 1233 | const area = this._buildArea(this.entities[item.entity_index], this.config.entities[item.entity_index]); 1234 | 1235 | return svg` 1236 | 1237 | ${area} 1238 | 1239 | `; 1240 | }) 1241 | 1242 | return svg`${svgItems}`; 1243 | } 1244 | 1245 | /******************************************************************************* 1246 | * _renderState() 1247 | * 1248 | * Summary. 1249 | * Renders the entity or attribute state of a single item. 1250 | * 1251 | */ 1252 | 1253 | _renderState(item) { 1254 | 1255 | if (!item) return; 1256 | 1257 | // compute x,y or dx,dy positions. Spec none if not specified. 1258 | const x = item.xpos ? item.xpos : ''; 1259 | const y = item.ypos ? item.ypos : ''; 1260 | const dx = item.dx ? item.dx : '0'; 1261 | const dy = item.dy ? item.dy : '0'; 1262 | 1263 | // compute some styling elements if configured for this state item 1264 | const STATE_STYLES = { 1265 | "font-size": '1em;', 1266 | "color": 'var(--primary-text-color);', 1267 | "opacity": '1.0;', 1268 | "text-anchor": 'middle;' 1269 | }; 1270 | 1271 | const UOM_STYLES = { 1272 | "opacity": '0.7;' 1273 | }; 1274 | 1275 | // Get configuration styles as the default styles 1276 | let configStyle = {...STATE_STYLES}; 1277 | if (item.styles) configStyle = Object.assign(configStyle, ...item.styles); 1278 | 1279 | // Get the runtime styles, caused by states & animation settings 1280 | let stateStyle = {}; 1281 | if (this.animations.states[item.index]) 1282 | stateStyle = Object.assign(stateStyle, this.animations.states[item.index]); 1283 | 1284 | // Merge the two, where the runtime styles may overwrite the statically configured styles 1285 | configStyle = { ...configStyle, ...stateStyle}; 1286 | 1287 | // Convert javascript records to plain text, without "{}" and "," between the styles. 1288 | const configStyleStr = JSON.stringify(configStyle).slice(1, -1).replace(/"/g,"").replace(/,/g,""); 1289 | 1290 | // Get font-size of state in configStyle. 1291 | // Split value and px/em; See: https://stackoverflow.com/questions/3370263/separate-integers-and-text-in-a-string 1292 | // For floats and strings: 1293 | // - https://stackoverflow.com/questions/17374893/how-to-extract-floating-numbers-from-strings-in-javascript 1294 | 1295 | // 2019.09.12 1296 | // https://stackoverflow.com/questions/40758143/regular-expression-to-split-double-and-integer-numbers-in-a-string 1297 | // https://regex101.com/r/QYfDtB/1 1298 | // regex \D+|\d*\.?\d+ (met /g van global matches) zou het wel moeten doen. Deze haalt goed de 1.27em; uit elkaar 1299 | // in twee stukken, dus 1.27 en em; 1300 | 1301 | var fsuomStr = configStyle["font-size"]; 1302 | 1303 | var fsuomValue = 0.5; 1304 | var fsuomType = 'em;'; 1305 | const fsuomSplit = fsuomStr.match(/\D+|\d*\.?\d+/g); 1306 | if (fsuomSplit.length == 2) { 1307 | fsuomValue = Number(fsuomSplit[0]) * .6; 1308 | fsuomType = fsuomSplit[1]; 1309 | } 1310 | else console.error('Cannot determine font-size for state', fsuomStr); 1311 | 1312 | fsuomStr = { "font-size": fsuomValue + fsuomType}; 1313 | 1314 | let uomStyle = {...configStyle, ...UOM_STYLES, ...fsuomStr}; 1315 | const uomStyleStr = JSON.stringify(uomStyle).slice(1, -1).replace(/"/g,"").replace(/,/g,""); 1316 | 1317 | const uom = this._buildUom(this.entities[item.entity_index], this.config.entities[item.entity_index]); 1318 | 1319 | const state = (this.config.entities[item.entity_index].attribute && 1320 | this.entities[item.entity_index].attributes[this.config.entities[item.entity_index].attribute]) 1321 | ? this.attributesStr[item.entity_index] 1322 | : this.entitiesStr[item.entity_index]; 1323 | 1324 | if (this._computeDomain(this.entities[item.entity_index].entity_id) == 'sensor') { 1325 | return svg` 1326 | this.handlePopup(e, this.entities[item.entity_index])}> 1327 | 1329 | ${state} 1330 | 1332 | ${uom} 1333 | 1334 | `; 1335 | } else { 1336 | // Not a sensor. Might be any other domain. Unit can only be specified using the units: in the configuration. 1337 | // Still check for using an attribute value for the domain... 1338 | return svg` 1339 | this.handlePopup(e, this.entities[item.entity_index])}> 1340 | 1342 | ${state} 1343 | 1345 | ${uom} 1346 | 1347 | `; 1348 | } 1349 | } 1350 | 1351 | /******************************************************************************* 1352 | * _renderStates() 1353 | * 1354 | * Summary. 1355 | * Renders the states. 1356 | * 1357 | */ 1358 | 1359 | _renderStates() { 1360 | const { 1361 | layout, 1362 | } = this.config; 1363 | 1364 | if (!layout) return; 1365 | if (!layout.states) return; 1366 | 1367 | const svgItems = layout.states.map(item => { 1368 | return svg` 1369 | ${this._renderState(item)} 1370 | `; 1371 | }) 1372 | 1373 | return svg`${svgItems}`; 1374 | } 1375 | 1376 | /******************************************************************************* 1377 | * _renderIcon() 1378 | * 1379 | * Summary. 1380 | * Renders a single icon. 1381 | * 1382 | */ 1383 | 1384 | _renderIcon(item) { 1385 | 1386 | if (!item) return; 1387 | 1388 | item.entity = item.entity ? item.entity : 0; 1389 | 1390 | // get icon size, and calculate the foreignObject position and size. This must match the icon size 1391 | // 1em = FONT_SIZE pixels, so we can calculate the icon size, and x/y positions of the foreignObject 1392 | // the viewport is 200x200, so we can calulate the offset. 1393 | // 1394 | // NOTE: 1395 | // Safari doesn't use the svg viewport for rendering of the foreignObject, but the real clientsize. 1396 | // So positioning an icon doesn't work correctly... 1397 | 1398 | var iconSize = item.icon_size ? item.icon_size : 2; 1399 | var iconPixels = iconSize * FONT_SIZE; 1400 | const x = item.xpos ? item.xpos / 100 : 0.5; 1401 | const y = item.ypos ? item.ypos / 100 : 0.5; 1402 | 1403 | const align = item.align ? item.align : 'center'; 1404 | const adjust = (align == 'center' ? 0.5 : (align == 'start' ? -1 : +1)); 1405 | 1406 | // const parentClientWidth = this.parentElement.clientWidth; 1407 | const clientWidth = this.clientWidth - 20; // hard coded adjust for padding... 1408 | const correction = clientWidth / SVG_VIEW_BOX; 1409 | 1410 | var xpx = (x * SVG_VIEW_BOX); 1411 | var ypx = (y * SVG_VIEW_BOX); 1412 | 1413 | 1414 | if ((this.isSafari) || (this.iOS)) { 1415 | iconSize = iconSize * correction; 1416 | 1417 | xpx = (xpx * correction) - (iconPixels * adjust * correction); 1418 | ypx = (ypx * correction) - (iconPixels * 0.5 * correction) - (iconPixels * 0.25 * correction);// - (iconPixels * 0.25 / 1.86); 1419 | } else { 1420 | // Get x,y in viewbox dimensions and center with half of size of icon. 1421 | // Adjust horizontal for aligning. Can be 1, 0.5 and -1 1422 | // Adjust vertical for half of height... and correct for 0.25em textfont to align. 1423 | xpx = xpx - (iconPixels * adjust); 1424 | ypx = ypx - (iconPixels * 0.5) - (iconPixels * 0.25); 1425 | } 1426 | 1427 | // Get configuration styles as the default styles 1428 | let configStyle = {}; 1429 | if (item.styles) configStyle = Object.assign(configStyle, ...item.styles); 1430 | 1431 | // Get the runtime styles, caused by states & animation settings 1432 | let stateStyle = {}; 1433 | if (this.animations.icons[item.animation_id]) 1434 | stateStyle = Object.assign(stateStyle, this.animations.icons[item.animation_id]); 1435 | 1436 | // Merge the two, where the runtime styles may overwrite the statically configured styles 1437 | configStyle = { ...configStyle, ...stateStyle}; 1438 | 1439 | // Convert javascript records to plain text, without "{}" and "," between the styles. 1440 | const configStyleStr = JSON.stringify(configStyle).slice(1, -1).replace(/"/g,"").replace(/,/g,""); 1441 | 1442 | const icon = this._buildIcon(this.entities[item.entity_index], this.config.entities[item.entity_index]); 1443 | 1444 | return svg` 1445 | this.handlePopup(e, this.entities[item.entity_index])}> 1446 | 1447 | 1448 |
1449 | 1450 |
1451 | 1452 |
1453 | 1454 | `; 1455 | } 1456 | 1457 | /******************************************************************************* 1458 | * _renderIcons() 1459 | * 1460 | * Summary. 1461 | * Renders all the icons in the list. 1462 | * 1463 | */ 1464 | 1465 | _renderIcons() { 1466 | const { 1467 | layout, 1468 | } = this.config; 1469 | 1470 | if (!layout) return; 1471 | if (!layout.icons) return; 1472 | 1473 | const svgItems = layout.icons.map(item => { 1474 | return svg` 1475 | ${this._renderIcon(item)} 1476 | `; 1477 | }) 1478 | 1479 | return svg`${svgItems}`; 1480 | } 1481 | 1482 | /******************************************************************************* 1483 | * _renderHorizontalLines() 1484 | * 1485 | * Summary. 1486 | * Renders the specified lines in the grid. 1487 | * 1488 | */ 1489 | 1490 | _renderHorizontalLines() { 1491 | const { 1492 | layout, 1493 | } = this.config; 1494 | 1495 | if (!layout) return; 1496 | if (!layout.hlines) return; 1497 | 1498 | // compute some styling elements if configured for this state item 1499 | const HLINES_STYLES = { 1500 | "stroke-linecap": 'round;', 1501 | "stroke": 'var(--primary-text-color);', 1502 | "opacity": '1.0;', 1503 | "stroke-width": '2;' 1504 | }; 1505 | 1506 | const svgItems = layout.hlines.map(item => { 1507 | // Get configuration styles as the default styles 1508 | let configStyle = {...HLINES_STYLES}; 1509 | configStyle = Object.assign(configStyle, ...item.styles); 1510 | 1511 | // Get the runtime styles, caused by states & animation settings 1512 | let stateStyle = {}; 1513 | if (this.animations.hlines[item.animation_id]) 1514 | stateStyle = Object.assign(stateStyle, this.animations.hlines[item.animation_id]); 1515 | 1516 | // Merge the two, where the runtime styles may overwrite the statically configured styles 1517 | configStyle = { ...configStyle, ...stateStyle}; 1518 | 1519 | // Convert javascript records to plain text, without "{}" and "," between the styles. 1520 | const configStyleStr = JSON.stringify(configStyle).slice(1, -1).replace(/"/g,"").replace(/,/g,""); 1521 | 1522 | item.entity_index = item.entity_index ? item.entity_index : 0; 1523 | 1524 | return svg` 1525 | this.handlePopup(e, this.entities[item.entity_index])} class="line__horizontal" x1="${item.xpos-item.length/2}%" y1="${item.ypos}%" x2="${item.xpos+item.length/2}%" y2="${item.ypos}%" style="${configStyleStr}"/> 1526 | `; 1527 | }) 1528 | 1529 | return svg`${svgItems}`; 1530 | } 1531 | 1532 | /******************************************************************************* 1533 | * _renderVerticalLines() 1534 | * 1535 | * Summary. 1536 | * Renders the specified lines in the grid. 1537 | * 1538 | */ 1539 | 1540 | _renderVerticalLines() { 1541 | const { 1542 | layout, 1543 | } = this.config; 1544 | 1545 | if (!layout) return; 1546 | if (!layout.vlines) return; 1547 | 1548 | const VLINES_STYLES = { 1549 | "stroke-linecap": 'round;', 1550 | "stroke": 'var(--primary-text-color);', 1551 | "opacity": '1.0;', 1552 | "stroke-width": '2;' 1553 | }; 1554 | 1555 | const svgItems = layout.vlines.map(item => { 1556 | // Get configuration styles as the default styles 1557 | let configStyle = {...VLINES_STYLES}; 1558 | configStyle = Object.assign(configStyle, ...item.styles); 1559 | 1560 | // Get the runtime styles, caused by states & animation settings 1561 | let stateStyle = {}; 1562 | if (this.animations.vlines[item.animation_id]) 1563 | stateStyle = Object.assign(stateStyle, this.animations.vlines[item.animation_id]); 1564 | 1565 | // Merge the two, where the runtime styles may overwrite the statically configured styles 1566 | configStyle = { ...configStyle, ...stateStyle}; 1567 | 1568 | // Convert javascript records to plain text, without "{}" and "," between the styles. 1569 | const configStyleStr = JSON.stringify(configStyle).slice(1, -1).replace(/"/g,"").replace(/,/g,""); 1570 | 1571 | item.entity_index = item.entity_index ? item.entity_index : 0; 1572 | 1573 | return svg` 1574 | this.handlePopup(e, this.entities[item.entity_index])} class="line__vertical" x1="${item.xpos}%" y1="${item.ypos-item.length/2}%" x2="${item.xpos}%" y2="${item.ypos+item.length/2}%" style="${configStyleStr}"/> 1575 | `; 1576 | }) 1577 | 1578 | return svg`${svgItems}`; 1579 | } 1580 | 1581 | /******************************************************************************* 1582 | * _renderCircles() 1583 | * 1584 | * Summary. 1585 | * Renders the specified circles in the grid. 1586 | * 1587 | */ 1588 | 1589 | _renderCircles() { 1590 | const { 1591 | layout, 1592 | } = this.config; 1593 | 1594 | if (!layout) return; 1595 | if (!layout.circles) return; 1596 | 1597 | const svgItems = layout.circles.map(item => { 1598 | // Get configuration styles as the default styles 1599 | let configStyle = {}; 1600 | if (item.styles) configStyle = Object.assign(configStyle, ...item.styles); 1601 | 1602 | // Get the runtime styles, caused by states & animation settings 1603 | let stateStyle = {}; 1604 | if (this.animations.circles[item.animation_id]) 1605 | stateStyle = Object.assign(stateStyle, this.animations.circles[item.animation_id]); 1606 | 1607 | // Merge the two, where the runtime styles may overwrite the statically configured styles 1608 | configStyle = { ...configStyle, ...stateStyle}; 1609 | 1610 | // Convert javascript records to plain text, without "{}" and "," between the styles. 1611 | const configStyleStr = JSON.stringify(configStyle).slice(1, -1).replace(/"/g,"").replace(/,/g,""); 1612 | 1613 | item.entity_index = item.entity_index ? item.entity_index : 0; 1614 | 1615 | return svg` 1616 | this.handlePopup(e, this.entities[item.entity_index])} 1617 | cx="${item.xpos}%" cy="${item.ypos}%" r="${item.radius}" 1618 | style="${configStyleStr}"/> 1619 | `; 1620 | }) 1621 | return svg`${svgItems}`; 1622 | } 1623 | 1624 | /******************************************************************************* 1625 | * _handleClick() 1626 | * 1627 | * Summary. 1628 | * Processes the mouse click of the user and dispatches the event to the 1629 | * configure handler. 1630 | * At this moment, only 'more-info' is used! 1631 | * 1632 | * Credits: 1633 | * All credits to the mini-graph-card for this function. 1634 | * 1635 | */ 1636 | 1637 | _handleClick(node, hass, config, actionConfig, entityId) { 1638 | let e; 1639 | // eslint-disable-next-line default-case 1640 | switch (actionConfig.action) { 1641 | case 'more-info': { 1642 | e = new Event('hass-more-info', { composed: true }); 1643 | e.detail = { entityId }; 1644 | node.dispatchEvent(e); 1645 | break; 1646 | } 1647 | case 'navigate': { 1648 | if (!actionConfig.navigation_path) return; 1649 | window.history.pushState(null, '', actionConfig.navigation_path); 1650 | e = new Event('location-changed', { composed: true }); 1651 | e.detail = { replace: false }; 1652 | window.dispatchEvent(e); 1653 | break; 1654 | } 1655 | case 'call-service': { 1656 | if (!actionConfig.service) return; 1657 | const [domain, service] = actionConfig.service.split('.', 2); 1658 | const serviceData = { ...actionConfig.service_data }; 1659 | hass.callService(domain, service, serviceData); 1660 | } 1661 | } 1662 | } 1663 | 1664 | /******************************************************************************* 1665 | * handlePopup() 1666 | * 1667 | * Summary. 1668 | * Handles the first part of mouse click processing. 1669 | * It stops propagation to the parent and processes the event. 1670 | * 1671 | * The action can be configured per entity. Look-up the entity, and handle the click 1672 | * event for further processing. 1673 | * 1674 | * Credits: 1675 | * Almost all credits to the mini-graph-card for this function. 1676 | * 1677 | */ 1678 | 1679 | handlePopup(e, entity) { 1680 | e.stopPropagation(); 1681 | 1682 | this._handleClick(this, this._hass, this.config, 1683 | this.config.entities[this.config.entities.findIndex( 1684 | function(element, index, array){return element.entity == entity.entity_id})] 1685 | .tap_action, entity.entity_id); 1686 | } 1687 | 1688 | /******************************************************************************* 1689 | * _buildArea() 1690 | * 1691 | * Summary. 1692 | * Builds the Area string. 1693 | * 1694 | */ 1695 | 1696 | _buildArea(entityState, entityConfig) { 1697 | return ( 1698 | entityConfig.area 1699 | || '?' 1700 | ); 1701 | } 1702 | 1703 | /******************************************************************************* 1704 | * _buildName() 1705 | * 1706 | * Summary. 1707 | * Builds the Name string. 1708 | * 1709 | */ 1710 | 1711 | _buildName(entityState, entityConfig) { 1712 | return ( 1713 | entityConfig.name 1714 | || entityState.attributes.friendly_name 1715 | ); 1716 | } 1717 | 1718 | /******************************************************************************* 1719 | * _buildIcon() 1720 | * 1721 | * Summary. 1722 | * Builds the Icon specification name. 1723 | * 1724 | */ 1725 | _buildIcon(entityState, entityConfig) { 1726 | return ( 1727 | entityConfig.icon 1728 | || entityState.attributes.icon 1729 | ); 1730 | } 1731 | 1732 | /******************************************************************************* 1733 | * _buildUom() 1734 | * 1735 | * Summary. 1736 | * Builds the Unit of Measurement string. 1737 | * 1738 | */ 1739 | 1740 | _buildUom(entityState, entityConfig) { 1741 | return ( 1742 | entityConfig.unit 1743 | || entityState.attributes.unit_of_measurement 1744 | || '' 1745 | ); 1746 | } 1747 | 1748 | /******************************************************************************* 1749 | * _buildState() 1750 | * 1751 | * Summary. 1752 | * Builds the State string. 1753 | * If state is not a number, the state is returned AS IS, otherwise the state 1754 | * is build according to the specified number of decimals. 1755 | * 1756 | */ 1757 | 1758 | _buildState(inState, entityConfig) { 1759 | if (isNaN(inState)) 1760 | return inState; 1761 | 1762 | const state = Number(inState); 1763 | 1764 | if (entityConfig.decimals === undefined || Number.isNaN(entityConfig.decimals) || Number.isNaN(state)) 1765 | return Math.round(state * 100) / 100; 1766 | 1767 | const x = 10 ** entityConfig.decimals; 1768 | return (Math.round(state * x) / x).toFixed(entityConfig.decimals); 1769 | } 1770 | 1771 | 1772 | /******************************************************************************* 1773 | * _computeState() 1774 | * 1775 | * Summary. 1776 | * 1777 | */ 1778 | 1779 | _computeState(inState, dec) { 1780 | 1781 | if (isNaN(inState)) 1782 | return inState; 1783 | 1784 | const state = Number(inState); 1785 | 1786 | if (dec === undefined || Number.isNaN(dec) || Number.isNaN(state)) 1787 | return Math.round(state * 100) / 100; 1788 | 1789 | const x = 10 ** dec; 1790 | return (Math.round(state * x) / x).toFixed(dec); 1791 | } 1792 | 1793 | /******************************************************************************* 1794 | * _calculateStrokeColor() 1795 | * 1796 | * Summary. 1797 | * 1798 | */ 1799 | 1800 | _calculateStrokeColor(state, stops, gradient) { 1801 | const sortedStops = Object.keys(stops).map(n => Number(n)).sort((a, b) => a - b); 1802 | let start, end, val; 1803 | const l = sortedStops.length; 1804 | if (state <= sortedStops[0]) { 1805 | return stops[sortedStops[0]]; 1806 | } else if (state >= sortedStops[l - 1]) { 1807 | return stops[sortedStops[l - 1]]; 1808 | } else { 1809 | for (let i = 0; i < l - 1; i++) { 1810 | const s1 = sortedStops[i]; 1811 | const s2 = sortedStops[i + 1]; 1812 | if (state >= s1 && state < s2) { 1813 | [start, end] = [stops[s1], stops[s2]]; 1814 | if (!gradient) { 1815 | return start; 1816 | } 1817 | val = this._calculateValueBetween(s1, s2, state); 1818 | break; 1819 | } 1820 | } 1821 | } 1822 | return this._getGradientValue(start, end, val); 1823 | } 1824 | 1825 | /******************************************************************************* 1826 | * _calculateValueBetween() 1827 | * 1828 | * Summary. 1829 | * Clips the val value between start and end, and returns the between value ;-) 1830 | * 1831 | */ 1832 | 1833 | _calculateValueBetween(start, end, val) { 1834 | return (Math.min(Math.max(val, start), end) - start) / (end - start); 1835 | } 1836 | 1837 | _getLovelacePanel() { 1838 | var root = document.querySelector('home-assistant'); 1839 | root = root && root.shadowRoot; 1840 | root = root && root.querySelector('home-assistant-main'); 1841 | root = root && root.shadowRoot; 1842 | root = root && root.querySelector('app-drawer-layout partial-panel-resolver, ha-drawer partial-panel-resolver'); 1843 | root = (root && root.shadowRoot) || root; 1844 | root = root && root.querySelector('ha-panel-lovelace'); 1845 | if (root) { 1846 | return root; 1847 | } 1848 | return null; 1849 | } 1850 | /******************************************************************************* 1851 | * _getColorVariable() 1852 | * 1853 | * Summary. 1854 | * Get value of CSS color variable, specified as var(--color-value) 1855 | * These variables are defined in the lovelace element so it appears... 1856 | * 1857 | */ 1858 | 1859 | _getColorVariable(inColor) { 1860 | const newColor = inColor.substr(4, inColor.length-5); 1861 | 1862 | if (!this.lovelace) { 1863 | this.lovelace = this._getLovelacePanel(); 1864 | // const root = document.querySelector('home-assistant'); 1865 | // const main = root.shadowRoot.querySelector('home-assistant-main'); 1866 | // const drawer_layout = main.shadowRoot.querySelector('app-drawer-layout'); 1867 | // const pages = drawer_layout.querySelector('partial-panel-resolver'); 1868 | // this.lovelace = pages.querySelector('ha-panel-lovelace'); 1869 | } else { } 1870 | 1871 | const returnColor = window.getComputedStyle(this.lovelace).getPropertyValue(newColor); 1872 | return returnColor; 1873 | } 1874 | 1875 | /******************************************************************************* 1876 | * _getGradientValue() 1877 | * 1878 | * Summary. 1879 | * Get gradient value of color as a result of a color_stop. 1880 | * An RGBA value is calculated, so transparancy is possible... 1881 | * 1882 | * The colors (colorA and colorB) can be specified as: 1883 | * - a css variable, var(--color-value) 1884 | * - a hex value, #fff or #ffffff 1885 | * - an rgb() or rgba() value 1886 | * - a hsl() or hsla() value 1887 | * - a named css color value, such as white. 1888 | * 1889 | */ 1890 | 1891 | _getGradientValue(colorA, colorB, val) { 1892 | 1893 | const resultColorA = this._colorToRGBA(colorA); 1894 | const resultColorB = this._colorToRGBA(colorB); 1895 | 1896 | // We have a rgba() color array from cache or canvas. 1897 | // Calculate color in between, and return #hex value as a result. 1898 | // 1899 | 1900 | const v1 = 1 - val; 1901 | const v2 = val; 1902 | const rDec = Math.floor((resultColorA[0] * v1) + (resultColorB[0] * v2)); 1903 | const gDec = Math.floor((resultColorA[1] * v1) + (resultColorB[1] * v2)); 1904 | const bDec = Math.floor((resultColorA[2] * v1) + (resultColorB[2] * v2)); 1905 | const aDec = Math.floor((resultColorA[3] * v1) + (resultColorB[3] * v2)); 1906 | 1907 | // And convert full RRGGBBAA value to #hex. 1908 | const rHex = this._padZero(rDec.toString(16)); 1909 | const gHex = this._padZero(gDec.toString(16)); 1910 | const bHex = this._padZero(bDec.toString(16)); 1911 | const aHex = this._padZero(aDec.toString(16)); 1912 | return `#${rHex}${gHex}${bHex}${aHex}`; 1913 | } 1914 | _padZero(val) { 1915 | if (val.length < 2) { 1916 | val = `0${val}`; 1917 | } 1918 | return val.substr(0, 2); 1919 | } 1920 | 1921 | _computeDomain(entityId) { 1922 | return entityId.substr(0, entityId.indexOf('.')); 1923 | } 1924 | 1925 | _computeEntity(entityId) { 1926 | return entityId.substr(entityId.indexOf('.') + 1); 1927 | } 1928 | 1929 | /******************************************************************************* 1930 | * _colorToRGBA() 1931 | * 1932 | * Summary. 1933 | * Get RGBA color value of inColor. 1934 | * 1935 | * The inColor can be specified as: 1936 | * - a css variable, var(--color-value) 1937 | * - a hex value, #fff or #ffffff 1938 | * - an rgb() or rgba() value 1939 | * - a hsl() or hsla() value 1940 | * - a named css color value, such as white. 1941 | * 1942 | */ 1943 | 1944 | _colorToRGBA(inColor) { 1945 | // return color if found in colorCache... 1946 | if (inColor in this.colorCache) { 1947 | return this.colorCache[inColor]; 1948 | } 1949 | 1950 | var theColor = inColor; 1951 | // Check for 'var' colors 1952 | let a0 = inColor.substr(0,3); 1953 | if (a0.valueOf() === 'var') { 1954 | theColor = this._getColorVariable(inColor); 1955 | } 1956 | 1957 | // Get color from canvas. This always returns an rgba() value... 1958 | var canvas = document.createElement('canvas'); 1959 | canvas.width = canvas.height = 1; 1960 | var ctx = canvas.getContext('2d'); 1961 | 1962 | ctx.clearRect(0, 0, 1, 1); 1963 | ctx.fillStyle = theColor; 1964 | ctx.fillRect(0, 0, 1, 1); 1965 | const outColor = [ ...ctx.getImageData(0, 0, 1, 1).data ]; 1966 | 1967 | this.colorCache[inColor] = outColor; 1968 | return outColor; 1969 | } 1970 | 1971 | getCardSize() { 1972 | return (4); 1973 | } 1974 | } 1975 | 1976 | customElements.define('flex-horseshoe-card', FlexHorseshoeCard); 1977 | -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Flexible Horseshoe Card for Lovelace", 3 | "content_in_root": true, 4 | "filename": "flex-horseshoe-card.js" 5 | } 6 | -------------------------------------------------------------------------------- /images/Hass horseshoe overview 920x693.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmoebeLabs/flex-horseshoe-card/882ed05e442f42cc354c67088ed773cad0bd28ee/images/Hass horseshoe overview 920x693.png -------------------------------------------------------------------------------- /info.md: -------------------------------------------------------------------------------- 1 | # ![](https://tweakers.net/ext/f/D4Fx1OKp6s7Hb21Wzq9JWCJb/full.png) Flexible Horseshoe Card 2 | Info Flexible looks-like-a-horseshoe card for [Home Assistant](https://github.com/home-assistant/home-assistant) Lovelace UI 3 | 4 | ![](https://tweakers.net/ext/f/3jaSI26J9QxHJa8rTriXFNNO/full.png) 5 | 6 | 7 | *The Lovelace view of the above examples is in the repository in the examples folder. 8 |
So you can see how these layouts are done* 9 | *** 10 | 11 | ## Introduction 12 | The flexible horseshoe card can display data from entities and attributes from the sensor and other domains. It displays the current state and for the primary entity it fills the horseshoe with a color depending on the min and max values of the state and the configured color stops and styling. 13 | 14 | The main perk of this card is it's flexibility. It is able to position a number of things where YOU want it using a layout specification for each object you want on the card: 15 | 16 | | Feature | Description | 17 | |---------|-------------| 18 | | **Any** number of **entities** |For each entity, the attribute, units, icon, name, area and tap action can be specified.

*There is currently no limit imposed on the number of entities in this card. I'm using max. 3 entities in the examples, but there is no problem using more.* 19 | | **Any** number of **circles**, **horizontal** and **vertical** **lines** | To function as a divider between values or background for values. 20 | | The **layout** of the card | You can specify each object with a relative position on the card | 21 | | **Animations**, dynamic behaviour | You can specify what happens if an entity changes state like change color, or execute a CSS animation. There are predefined animations. | 22 | | Several ways to **color** the **horseshoe** | From single, fixed color, to a gradient depending on a list of colorstops | 23 | | **Actions** | Handle click actions per entity to for instance switch a light on/off ![](https://tweakers.net/ext/f/Hk2Lzz2VkPbDUvEQUubBXoJU/full.gif) | 24 | 25 | * * * 26 | --------------------------------------------------------------------------------