A different take on designing a Lovelace UI

I need some help with addind a red/warm spinning fan when my AC is on heat mode in stead of cooling mode. (blue)

This is the original code:

icon_fan2:
  styles:
    custom_fields:
      icon:
        - width: 75%
        - margin-left: -3%
  custom_fields:
    icon: >
      [[[
        let path = `
          <circle cx="25" cy="25" r="6.6"/>
          <path d="M31.9 30.4c-.5.6-1.1 1.1-1.7 1.5-1.4 1.1-3.2 1.7-5.2 1.7-2.3 0-4.5-.9-6-2.4-.9 1.1-1.6 2.3-2.3 3.2l-4.9 5.4c-1.8 2.7.3 5.6 2.5 7 3.9 2.4 9.8 3.1 14.1 1.9 4.6-1.3 7.9-4.7 7.4-9.7-.2-3.4-1.9-6-3.9-8.6zM17 28.3c-.4-1-.6-2.1-.6-3.3a8.7 8.7 0 0 1 6.4-8.4l-1.6-3.5L19 6.2c-1.5-2.8-5-2.5-7.3-1.2-4 2.2-7.5 6.9-8.7 11.3-1.2 4.6.2 9.2 4.7 11.3 3.1 1.3 6.1 1.2 9.3.7zm26.9-17.6c-3.3-3.4-8-4.6-12.1-1.8-2.8 1.8-4.2 4.6-5.5 7.5 4.2.6 7.4 4.2 7.4 8.6 0 .9-.1 1.7-.4 2.5 1.3.2 2.8.3 3.8.4 2.3.4 4.7 1.3 7.1 1.7 3.2.3 4.7-3 4.8-5.6.3-4.6-1.9-10.1-5.1-13.3z"/>
        `,
        style = `
          <svg viewBox="0 0 50 50">
            <style>
              @keyframes rotate {
                0% {
                  visibility: visible;
                  transform: rotate(0deg) translateZ(0);
                }
                100% {
                  transform: rotate(1080deg) translateZ(0);
                }
              }
              .start {
                animation: rotate 2.8s ease-in;
                transform-origin: center;
                fill: #5daeea;
                visibility: hidden;
                will-change: transform;
              }
              .on {
                animation: rotate 1.8s linear infinite;
                transform-origin: center;
                fill: #5daeea;
                animation-delay: 2.8s;
                visibility: hidden;
                will-change: transform;
              }
              .end {
                animation: rotate 2.8s;
                transform-origin: center;
                fill: #9ca2a5;
                animation-timing-function: cubic-bezier(0.61, 1, 0.88, 1);
                will-change: transform;
              }
              .start_timeout {
                animation: rotate 1.8s linear infinite;
                transform-origin: center;
                fill: #5daeea;
                visibility: hidden;
                will-change: transform;
              }
              .end_timeout {
                fill: #9ca2a5;
              }
            </style>
        `;
        if (variables.state_on && variables.timeout < 2000) {
          return `${style}<g class="start">${path}</g><g class="on">${path}</g></svg>`;
        }
        if (variables.state === 'off' && variables.timeout < 2000) {
          return `${style}<g class="end">${path}</g></svg>`;
        }
        if (variables.state_on && variables.timeout > 2000) {
          return `${style}<g class="start_timeout">${path}</g></svg>`;
        } else {
          return `${style}<g class="end_timeout">${path}</g></svg>`;
        }
      ]]]

And this is what i tried but is not working, any help would be higly appreciated:

icon_fan2:
  styles:
    custom_fields:
      icon:
        - width: 75%
        - margin-left: -3%
  custom_fields:
    icon: >
      [[[
        let path = `
          <circle cx="25" cy="25" r="6.6"/>
          <path d="M31.9 30.4c-.5.6-1.1 1.1-1.7 1.5-1.4 1.1-3.2 1.7-5.2 1.7-2.3 0-4.5-.9-6-2.4-.9 1.1-1.6 2.3-2.3 3.2l-4.9 5.4c-1.8 2.7.3 5.6 2.5 7 3.9 2.4 9.8 3.1 14.1 1.9 4.6-1.3 7.9-4.7 7.4-9.7-.2-3.4-1.9-6-3.9-8.6zM17 28.3c-.4-1-.6-2.1-.6-3.3a8.7 8.7 0 0 1 6.4-8.4l-1.6-3.5L19 6.2c-1.5-2.8-5-2.5-7.3-1.2-4 2.2-7.5 6.9-8.7 11.3-1.2 4.6.2 9.2 4.7 11.3 3.1 1.3 6.1 1.2 9.3.7zm26.9-17.6c-3.3-3.4-8-4.6-12.1-1.8-2.8 1.8-4.2 4.6-5.5 7.5 4.2.6 7.4 4.2 7.4 8.6 0 .9-.1 1.7-.4 2.5 1.3.2 2.8.3 3.8.4 2.3.4 4.7 1.3 7.1 1.7 3.2.3 4.7-3 4.8-5.6.3-4.6-1.9-10.1-5.1-13.3z"/>
        `,
        style = `
          <svg viewBox="0 0 50 50">
            <style>
              @keyframes rotate {
                0% {
                  visibility: visible;
                  transform: rotate(0deg) translateZ(0);
                }
                100% {
                  transform: rotate(1080deg) translateZ(0);
                }
              }
              .start {
                animation: rotate 2.8s ease-in;
                transform-origin: center;
                fill: #5daeea;
                visibility: hidden;
                will-change: transform;
              }
              .startheat {
                animation: rotate 2.8s ease-in;
                transform-origin: center;
                fill: #991c1c;
                visibility: hidden;
                will-change: transform;
              }              
              .on {
                animation: rotate 1.8s linear infinite;
                transform-origin: center;
                fill: #5daeea;
                animation-delay: 2.8s;
                visibility: hidden;
                will-change: transform;
              }
              .heat {
                animation: rotate 1.8s linear infinite;
                transform-origin: center;
                fill: #991c1c;
                animation-delay: 2.8s;
                visibility: hidden;
                will-change: transform;
              }              
              .end {
                animation: rotate 2.8s;
                transform-origin: center;
                fill: #9ca2a5;
                animation-timing-function: cubic-bezier(0.61, 1, 0.88, 1);
                will-change: transform;
              }
              .start_timeout {
                animation: rotate 1.8s linear infinite;
                transform-origin: center;
                fill: #5daeea;
                visibility: hidden;
                will-change: transform;
              }
              .end_timeout {
                fill: #9ca2a5;
              }
            </style>
        `;
        if (variables.state_on && variables.timeout < 2000) {
          return `${style}<g class="start">${path}</g><g class="on">${path}</g></svg>`;
        }
        if (variables.state === 'heat' && variables.timeout < 2000) {
            state = 'heat';
        }              
        if (variables.state_heat && variables.timeout < 2000) {
          return `${style}<g class="startheat">${path}</g><g class="heat">${path}</g></svg>`;
        }
        if (variables.state === 'off' && variables.timeout < 2000) {
          return `${style}<g class="end">${path}</g></svg>`;
        }
        if (variables.state_on && variables.timeout > 2000) {
          return `${style}<g class="start_timeout">${path}</g></svg>`;
        } else {
          return `${style}<g class="end_timeout">${path}</g></svg>`;
        }
      ]]]

I’ll definately buy someone a beer if this can be fixed :wink:

svg style part, pick your color…
fill: #5daeea;

I want to keep using blue when state is ‘on’ or cooling… and red color when state is heat.
And also that the button itself have the white “on” color

This know indeed, but i want to change the color of the spinning fan when the AC is heating in stead of cooling on the iconfan which mattias made

You can replace the hex color code with a conditional ternary operator. Assuming the entity of the button card is your HVAC, you could use something like this

icon_fan2:
  styles:
    custom_fields:
      icon:
        - width: 75%
        - margin-left: -3%
  custom_fields:
    icon: >
      [[[
        let path = `
          <circle cx="25" cy="25" r="6.6"/>
          <path d="M31.9 30.4c-.5.6-1.1 1.1-1.7 1.5-1.4 1.1-3.2 1.7-5.2 1.7-2.3 0-4.5-.9-6-2.4-.9 1.1-1.6 2.3-2.3 3.2l-4.9 5.4c-1.8 2.7.3 5.6 2.5 7 3.9 2.4 9.8 3.1 14.1 1.9 4.6-1.3 7.9-4.7 7.4-9.7-.2-3.4-1.9-6-3.9-8.6zM17 28.3c-.4-1-.6-2.1-.6-3.3a8.7 8.7 0 0 1 6.4-8.4l-1.6-3.5L19 6.2c-1.5-2.8-5-2.5-7.3-1.2-4 2.2-7.5 6.9-8.7 11.3-1.2 4.6.2 9.2 4.7 11.3 3.1 1.3 6.1 1.2 9.3.7zm26.9-17.6c-3.3-3.4-8-4.6-12.1-1.8-2.8 1.8-4.2 4.6-5.5 7.5 4.2.6 7.4 4.2 7.4 8.6 0 .9-.1 1.7-.4 2.5 1.3.2 2.8.3 3.8.4 2.3.4 4.7 1.3 7.1 1.7 3.2.3 4.7-3 4.8-5.6.3-4.6-1.9-10.1-5.1-13.3z"/>
        `,
        style = `
          <svg viewBox="0 0 50 50">
            <style>
              @keyframes rotate {
                0% {
                  visibility: visible;
                  transform: rotate(0deg) translateZ(0);
                }
                100% {
                  transform: rotate(1080deg) translateZ(0);
                }
              }
              .start {
                animation: rotate 2.8s ease-in;
                transform-origin: center;
                fill: ${variables.state === 'heat' ? '#991c1c' : '#5daeea'};
                visibility: hidden;
                will-change: transform;
              }
              .on {
                animation: rotate 1.8s linear infinite;
                transform-origin: center;
                fill: ${variables.state === 'heat' ? '#991c1c' : '#5daeea'};
                animation-delay: 2.8s;
                visibility: hidden;
                will-change: transform;
              }
              .end {
                animation: rotate 2.8s;
                transform-origin: center;
                fill: #9ca2a5;
                animation-timing-function: cubic-bezier(0.61, 1, 0.88, 1);
                will-change: transform;
              }
              .start_timeout {
                animation: rotate 1.8s linear infinite;
                transform-origin: center;
                fill: ${variables.state === 'heat' ? '#991c1c' : '#5daeea'};
                visibility: hidden;
                will-change: transform;
              }
              .end_timeout {
                fill: #9ca2a5;
              }
            </style>
        `;
        if (variables.state_on && variables.timeout < 2000) {
          return `${style}<g class="start">${path}</g><g class="on">${path}</g></svg>`;
        }
        if (variables.state === 'off' && variables.timeout < 2000) {
          return `${style}<g class="end">${path}</g></svg>`;
        }
        if (variables.state_on && variables.timeout > 2000) {
          return `${style}<g class="start_timeout">${path}</g></svg>`;
        } else {
          return `${style}<g class="end_timeout">${path}</g></svg>`;
        }
      ]]]

Edit: for the card to show as white/on add ‘heat’ to the state_on array in button_card_templates/base.yaml

base:
  template:
    - settings
    - tilt
    - extra_styles
  variables:
    state_on: >
      [[[ return ['on', 'home', 'cool', 'heat', 'fan_only', 'playing', 'unlocked'].indexOf(!entity || entity.state) !== -1; ]]]

I appreciate your help!

The base addition is working, now when the state is ‘heat’ the button is ON. so thats the first step.
now when i use your edited code for the spinning fan the button is disappeared.
i feel we are getting closer! did you manage to get this working?

I think I was responding to someone else. I am not sure I can help you with your icon issue. I don’t use Mattias’s HVAC card. I have created my own using the Simple Thermostat card.

Where is your electric pole icon coming from?

I had it working on my actual fan, but it failed with my HVAC for some reason. Let’s try a different approach.

icon_fan2:
  styles:
    custom_fields:
      icon:
        - width: 75%
        - margin-left: -3%
        - fill: >
            [[[ 
              return variables.state === 'cool' || variables.state === 'fan_only' 
              ? '#5daeea' 
              : variables.state === 'heat'
                ? '#991c1c'
                : '#9da0a2'; 
            ]]]
  custom_fields:
    icon: >
      [[[
        let path = `
          <circle cx="25" cy="25" r="6.6"/>
          <path d="M31.9 30.4c-.5.6-1.1 1.1-1.7 1.5-1.4 1.1-3.2 1.7-5.2 1.7-2.3 0-4.5-.9-6-2.4-.9 1.1-1.6 2.3-2.3 3.2l-4.9 5.4c-1.8 2.7.3 5.6 2.5 7 3.9 2.4 9.8 3.1 14.1 1.9 4.6-1.3 7.9-4.7 7.4-9.7-.2-3.4-1.9-6-3.9-8.6zM17 28.3c-.4-1-.6-2.1-.6-3.3a8.7 8.7 0 0 1 6.4-8.4l-1.6-3.5L19 6.2c-1.5-2.8-5-2.5-7.3-1.2-4 2.2-7.5 6.9-8.7 11.3-1.2 4.6.2 9.2 4.7 11.3 3.1 1.3 6.1 1.2 9.3.7zm26.9-17.6c-3.3-3.4-8-4.6-12.1-1.8-2.8 1.8-4.2 4.6-5.5 7.5 4.2.6 7.4 4.2 7.4 8.6 0 .9-.1 1.7-.4 2.5 1.3.2 2.8.3 3.8.4 2.3.4 4.7 1.3 7.1 1.7 3.2.3 4.7-3 4.8-5.6.3-4.6-1.9-10.1-5.1-13.3z"/>
        `,
        style = `
          <svg viewBox="0 0 50 50">
            <style>
              @keyframes rotate {
                0% {
                  visibility: visible;
                  transform: rotate(0deg) translateZ(0);
                }
                100% {
                  transform: rotate(1080deg) translateZ(0);
                }
              }
              .start {
                animation: rotate 2.8s ease-in;
                transform-origin: center;
                visibility: hidden;
                will-change: transform;
              }
              .on {
                animation: rotate 1.8s linear infinite;
                transform-origin: center;
                animation-delay: 2.8s;
                visibility: hidden;
                will-change: transform;
              }
              .end {
                animation: rotate 2.8s;
                transform-origin: center;
                animation-timing-function: cubic-bezier(0.61, 1, 0.88, 1);
                will-change: transform;
              }
              .start_timeout {
                animation: rotate 1.8s linear infinite;
                transform-origin: center;
                visibility: hidden;
                will-change: transform;
              }
              .end_timeout {
              }
            </style>
        `;
        if (variables.state_on && variables.timeout < 2000) {
          return `${style}<g class="start">${path}</g><g class="on">${path}</g></svg>`;
        }
        if (variables.state === 'off' && variables.timeout < 2000) {
          return `${style}<g class="end">${path}</g></svg>`;
        }
        if (variables.state_on && variables.timeout > 2000) {
          return `${style}<g class="start_timeout">${path}</g></svg>`;
        } else {
          return `${style}<g class="end_timeout">${path}</g></svg>`;
        }
      ]]]

This takes the fill out of the CSS and adds the fill ternary operator to the icon custom fields.
image

Edit: the previous code would work too if I didn’t forget to wrap the hex values in single quotes… whoops. Edited the previous post for clarity.

1 Like

Hi,
This place is a treasure trove! Thanks to all contributors.
I am trying to move image inside card, or card on dash.
It needs to be dynamic, driven by sensor value.
Can this be achieved?
TIA

Has anybody achieved to make the circle slider work with cover blinds instead of lights? The circle gets displayed, also the value of the current_position attribute (a number from 0 to 100). But I can’t get the slider to work an set the position using the slider. :frowning:

This is the code I am using:

cover_circle:
  styles:
    card:
      - --c-stroke-color-on: '#b0b0b0'
      - --c-stroke-color-off: none
      - --c-fill-color-on: none
      - --c-fill-color-off: rgba(255,255,255,0.04)
      - --c-stroke-width: 2.3
      - --c-stroke-width-dragging: 4
      - --c-font-color: '#97989c'
      - --c-font-size: 14px
      - --c-unit-font-size: 10.5px
      - --c-font-weight: 700
      - --c-letter-spacing: -0.02rem
    custom_fields:
      circle:
        - display: initial
        - width: 88%
        - margin: -3% 2% 0 0
        - justify-self: end
        - opacity: 1
  custom_fields:
    circle: >
      [[[
        if (entity) {
            let r = 22.1,
                c = r * 2 * Math.PI,
                tspan = '<tspan dx=".2" dy="-.4">',
                domain = "cover",
                state = entity.state === "open" || entity.state === "closed",
                input = entity.attributes.current_position || ' ',
                unit = '%';

           /* * * * * * * * * * * * * * * * * *
            *                                 *
            *             CIRCLE              *
            *                                 *
            * * * * * * * * * * * * * * * * * */

            let circle = (state, input, unit) => {
                return `
                  <svg viewBox="0 0 50 50">
                    <style>
                      circle {
                        transform: rotate(-90deg);
                        transform-origin: 50% 50%;
                        stroke-dasharray: ${c};
                        stroke-dashoffset: ${typeof input === 'number' && c - input / 100 * c};
                        stroke-width: var(--c-stroke-width);
                        stroke: ${state ? 'var(--c-stroke-color-on)' : 'var(--c-stroke-color-off)'};
                        fill: ${state ? 'var(--c-fill-color-on)' : 'var(--c-fill-color-off)'};
                      }
                      text {
                        font-size: var(--c-font-size);
                        font-weight: var(--c-font-weight);
                        letter-spacing: var(--c-letter-spacing);
                        fill: var(--c-font-color);
                      }
                      tspan {
                        font-size: var(--c-unit-font-size);
                      }
                      #circle_value, tspan {
                        text-anchor: middle;
                        dominant-baseline: central;
                      }
                    </style>
                    <circle id="circle_stroke" cx="25" cy="25" r="${r}"/>
                    <text id="circle_value" x="50%" y="52%">${input}${tspan}${unit}</tspan></text>
                  </svg>

                 
                      <input id="circle_slider" type="range" min="0" max="100" value="${input}">
                  
                `;
            }

           /* * * * * * * * * * * * * * * * * *
            *                                 *
            *              Cover              *
            *                                 *
            * * * * * * * * * * * * * * * * * */

            if (domain === 'cover') {

                // wait 0ms for shadow dom
                setTimeout(() => {

                    // then get elements
                    let elt = this.shadowRoot,
                        circle_slider = elt.getElementById('circle_slider'),
                        circle_value = elt.getElementById('circle_value'),
                        circle_stroke = elt.getElementById('circle_stroke');

                    // approximate position of thumb relative to circle
                    circle_slider.style.top = `${(circle_slider.value - 50) / 1.66 - 1}%`;

                    // debug position
                    let debug = false;
                    if (debug) circle_slider.style.opacity = 0.3;

                    // pass each event to handler
                    ['click', 'input', 'mousedown', 'mouseup', 'touchstart', 'touchend'].forEach((event) => {
                        circle_slider.addEventListener(event, handler, { passive: true })
                    });

                    function handler(event) {

                        // "this" refers to slider
                        if (event.target === this) {

                            // bypass button-card tap_action
                            event.stopPropagation();

                            // update circle_value
                            circle_value.innerHTML = `${this.value}${tspan}${unit}</tspan>`;

                            // update stroke
                            circle_stroke.style.strokeDashoffset = c - this.value / 100 * c;
                            circle_stroke.style.strokeWidth = 'var(--c-stroke-width-dragging)';
                            
                            // set cursor while dragging
                            if (event.type === 'mousedown' || event.type === 'input') {
                                this.style.cursor = 'grabbing';
                            } else {
                                this.style.cursor = 'grab';
                            }

                            // reset stroke width if value doesn't change
                            if (input == this.value && (event.type === 'click' || event.type === 'touchend'))
                                circle_stroke.style.strokeWidth = 'var(--c-stroke-width)';

                            // on release
                            if (event.type === 'mouseup' || event.type === 'touchend') {

                                // display loader if brightness is 0
                                if (circle_slider.value == 0 && elt.getElementById('loader')) {
                                    elt.getElementById('loader').style.display = 'initial';
                                    elt.getElementById('circle').style.display = 'none';
                                }

                                // set brightness
                                hass.callService('cover', 'set_position', {
                                    entity_id: entity.entity_id,
                                    position: this.value
                                });
                            }
                        }
                    }
                }, 0);

                return circle(state, input, unit);
            }

        }
      ]]]

Any help is appreciated!

I haven’t done it personally, since I don’t have any position covers, but I have done something similar with my HVAC. I believe you’ll have to add your template name to this line so the circle CSS is added to your card (assuming the code you shared is a button card template) in button_card_templates/extra_styles.yaml.

I would think something like this:

${this._config.template.includes('light') || this._config.template.includes('cover_circle') ? `

and of course, you’ll need to include the extra_styles template in your card.

1 Like

Man, you are truly a legend! This is working smooth!
Thank you very much buddy, i appreciate your help on this one!
i would like to buy you a beer!

That did the trick! Thank you so much!! My respect! :slight_smile:
I completely missed the extra styles template and only focused on the code from the circle button template.

Now there are only a few little things left to fix, like there is no value shown when the cover blinds are closed completely, but I guess I can figure this out.

Again thank you very much! Replay appreciate your help, time and effort @D34DC3N73R

Hi buddy, do you want to share me your roller blinds icon please?

No problem! Glad it’s working for you. Please donate to your local animal shelter as a show of thanks if you feel the need.

2 Likes

you may need to add a circle input variable to your button card. For instance, this is what I use in my HVAC:

...
  variables:
    circle_input: >
      [[[
        if (entity) {
            return entity.state === 'cool' || entity.state === 'heat'
                ? Math.round(entity.attributes.temperature)
                : Math.round(entity.attributes.current_temperature);
        }
      ]]]
    circle_input_unit: '°F'
1 Like

I have issue with animation…
It only works when pressing on button.
can anyone spot my mistake?

type: custom:button-card
entity: sensor.humidor_humidity
show_entity_picture: true
entity_picture: /local/img/smoke.png
name: test
tap_action: none
haptic: medium
variables:
  circle_input: |
    [[[
      let progress = states['sensor.dishwasher_progress'];
      if (progress) return parseInt(progress.state);
    ]]]
  circle_input_unit: '%'
styles:
  entity_picture:
    - width: 100%
    - height: 100%
    - animation:
        - smoke1 0.5s linear infinite;
        - smoke2 2s linear infinite;
        - smoke3 1.5s linear infinite;
extra_styles: |
  @keyframes smoke1 {
    0% {
      filter: blur(0px);
      transform: translateY(0px) scale(-1, 1);
      opacity: 0;
    }
    
                
    25% {
      filter: blur(3px);
      transform: translateY(-10px) scale(-1, 1.05);
      opacity: 0.5;
    }

    
                
    50% {
      filter: blur(5px);
      transform: translateY(-20px) scale(-1, 1.1);
      opacity: 1;
    }
                
    75% {
      filter: blur(5px);
      transform: translateY(-30px) scale(-1, 1.15);
      opacity: 0.5;
    }
                
    100% {
      filter: blur(7px);
      transform: translateY(-40px) scale(-1, 1.2);
      opacity: 0;
    }
  }
  @keyframes smoke2 {
    0% {
      filter: blur(0px);
      transform: translateY(0px) scale(1);
      opacity: 0;
    }
    
    25% {
      filter: blur(3px);
      transform: translateY(-10px) scale(1.05);
      opacity: 0.5;
    }
    
    50% {
      filter: blur(5px);
      transform: translateY(-20px) scale(1.1);
      opacity: 1;
    }
    
    75% {
      filter: blur(5px);
      transform: translateY(-30px) scale(1.15);
      opacity: 0.5;
    }
   
    100% {
      filter: blur(7px);
      transform: translateY(-40px) scale(1.2);
      opacity: 0;
    }
  }

  @keyframes smoke3 {
    0% {
      filter: blur(0px);
      transform: translateY(0px) scale(1);
      opacity: 0;
    }
    
    25% {
      filter: blur(3px);
      transform: translateY(-20px) scale(1.05);
      opacity: 0.5;
    }
    
    50% {
      filter: blur(5px);
      transform: translateY(-40px) scale(1.1);
      opacity: 1;
    }
    
    75% {
      filter: blur(5px);
      transform: translateY(-60px) scale(1.15);
      opacity: 0.5;
    }
    
    100% {
      filter: blur(7px);
      transform: translateY(-80px) scale(1.2);
      opacity: 0;
    }
  }

Does some one know?

@Emad131 what are the different states you get from the nest protect called?
and what color goes with what state?