A different take on designing a Lovelace UI

hi, I have circle info to display “x/y” (where x is number of people home and y is the total of people).

might be a good start for what you are trying to do

this is in 2 parts this will fill in the circle based on the % of people home

  variables:
    circle_input:  >
      [[[
        return entity === undefined || Math.round(states['zone.home'].state / 3 * 100 ) ;
      ]]]

and this puts the value in the circle,

I can see an issue when that 10/10 might be to big and you will need to play with the font size, or just display x not x/y when all are on but that will not work if you have a group with more than 10 switches.

 custom_fields:
    circle: >
      [[[
          let input = variables.circle_input,
            radius = 20.5,
            circumference = radius * 2 * Math.PI;
          let inner_text = states['zone.home'].state
          return `
            <svg viewBox="0 0 50 50">
              <style>
                circle {
                  transform: rotate(-90deg);
                  transform-origin: 50% 50%;
                  stroke-dasharray: ${circumference};
                  stroke-dashoffset: ${circumference - input / 100 * circumference};
                }
                tspan {
                  font-size: 10px;
                }
              </style>
              <circle cx="25" cy="25" r="${radius}" stroke="#b2b2b2" stroke-width="1.5" fill="none" />
              <text x="50%" y="54%" fill="#8d8e90" font-size="14" text-anchor="middle" alignment-baseline="middle" dominant-baseline="middle">${inner_text}/3</text>
            </svg>
          `;
      ]]]

you could also use this template to get the inner text for the circle input

{{ expand('group.office_monitors') | selectattr('state','eq','on')|map(attribute='name') | list | count }} / {{ expand('group.office_monitors') | count }}

Full Home Card

- type: custom:button-card
  entity: input_select.state_home
  name: Home
  variables:
    circle_input:  >
      [[[
        return entity === undefined || Math.round(states['zone.home'].state / 3 * 100 ) ;
      ]]]
  hold_action:
    action: more-info
  tap_action:
    action: call-service
    service: input_select.select_next
    service_data:
      entity_id: input_select.state_home
  double_tap_action:
    action: call-service
    service: input_select.select_previous
    service_data:
      entity_id: input_select.state_home
  custom_fields:
    circle: >
      [[[
          let input = variables.circle_input,
            radius = 20.5,
            circumference = radius * 2 * Math.PI;
          let inner_text = states['zone.home'].state
          return `
            <svg viewBox="0 0 50 50">
              <style>
                circle {
                  transform: rotate(-90deg);
                  transform-origin: 50% 50%;
                  stroke-dasharray: ${circumference};
                  stroke-dashoffset: ${circumference - input / 100 * circumference};
                }
                tspan {
                  font-size: 10px;
                }
              </style>
              <circle cx="25" cy="25" r="${radius}" stroke="#b2b2b2" stroke-width="1.5" fill="none" />
              <text x="50%" y="54%" fill="#8d8e90" font-size="14" text-anchor="middle" alignment-baseline="middle" dominant-baseline="middle">${inner_text}/3</text>
            </svg>
          `;
      ]]]
  template:
    - base
    - icon_home
    - circle

if this is for light switches it might be better to look into switch as x Switch as X - Home Assistant

1 Like

Great work!

Is it possible to add this dashboard next to my current dashboard? If i follow the install instruction i loose all my current lovelace dashboards. I want to make this primary and but want to keep the other ones.

Thank you, this saves me a ton of head scratching!

It’s by far not perfect and I didn’t do it myself!

lovelace-ui:

type: custom:button-card
entity: weather.pirateweather
name: Berlin
template:
  - base
  - widget_weather
variables:
  temp_min: sensor.pirateweather_overnight_low_temperature_0d
  temp_max: sensor.pirateweather_daytime_high_temperature_0d
  precip: sensor.template_sensor_pirateweather_precip_probability_round

Button-card templates:

  widgets:
    aspect_ratio: 1
    variables: null
    show_icon: false
    show_state: true
    show_name: true
    tap_action:
      animation_card: |
        [[[
          const animation_speed_ms = 900;
          const animation = `card_bounce ${animation_speed_ms}ms cubic-bezier(0.22, 1, 0.36, 1)`;
          this.shadowRoot.getElementById("card").style.animation = animation;
          window.setTimeout(() => {
            this.shadowRoot.getElementById("card").style.animation = "none";
          }, animation_speed_ms)
        ]]]
    styles:
      card:
        - background-color: rgba(0,0,0,0.4)
        - font-color: rgb(255,255,255,0.9)
        - text-shadow: 0px 0px 1px black
        - letter-spacing: 0.05vw
      name:
        - color: rgb(255,255,255,0.6)
    extra_styles: |
      #name, #state {
        font-size: 1.1vw;
        letter-spacing: 0.05vw;
      }
      /* portrait */
      @media screen and (max-width: 1200px) {
        #name, #state {
          font-size: 2vw;
          letter-spacing: 0.05vw;
        }
      }
      /* phone */
      @media screen and (max-width: 800px) {
        #name, #state {
          font-size: 5.1vw;
          letter-spacing: 0.12vw;
        }
      }
      @keyframes card_bounce {
        0% {
          transform: scale(1);
        }
        15% {
          transform: scale(0.9);
        }
        25% {
          transform: scale(1);
        }
        30% {
          transform: scale(0.98);
        }
        100% {
          transform: scale(1);
        }
      }

and

  widget_weather:
    variables:
      temp_min: ''
      temp_max: ''
      precip: ''
    aspect_ratio: 1/1
    show_icon: false
    show_entity_picture: true
    show_name: true
    show_state: true
    show_label: true
    tap_action:
      action: more-info
    styles:
      grid:
        - grid-template-areas: |
            "n"        
            "temp"
            "i"
            "s"
            "l"
        - grid-template-columns: 1fr
        - grid-template-rows: min-content repeat(2, 1fr) repeat(2, min-content)
        - gap: 0%
      card:
        - color: rgba(255, 255, 255, 0.8)
        - background: |
            [[[
              if (states['sun.sun'].state == 'below_horizon'){
                return 'linear-gradient(to top, rgba(53,59,83,1) 0%, rgba(10,14,34,1) 100%)';
              } else
                return 'linear-gradient(to top, rgba(123,168,197,1) 0%, rgba(61,132,176,1) 100%)';
            ]]]                        
      name:
        - place-self: start
        - text-transform: uppercase
        - font-weight: 500
      img_cell:
        - justify-content: start
      icon:
        - width: 20%
      label:
        - place-self: start
        - margin-left: '-5px'
      custom_fields:
        temp:
          - place-self: start
    custom_fields:
      temp: |
        [[[ return entity.attributes.temperature + "°"; ]]]    
    label: |
      [[[
          if (window.navigator.userAgent.match(/iPhone/i)) {
            return         `<ha-icon
            icon="mdi:arrow-up-thin"
            style="width: 15px; height: 15px; margin-right: -6px;">
            </ha-icon><span> ${states[variables.temp_max].state}°</span>
                  <ha-icon
            icon="mdi:arrow-down-thin"
            style="width: 15px; height: 15px; margin-right: -6px;">
            </ha-icon><span> ${states[variables.temp_min].state}° </span>       
          `;
                }
          else {
                    return `<ha-icon
            icon="mdi:arrow-up-thin"
            style="width: 15px; height: 15px; margin-right: -5px;">
            </ha-icon><span> ${states[variables.temp_max].state}°</span>
                  <ha-icon
            icon="mdi:arrow-down-thin"
            style="width: 15px; height: 15px; margin-right: -5px; margin-left: -5px;">
            </ha-icon><span> ${states[variables.temp_min].state}° </span>       
                  <ha-icon
            icon="mdi:weather-pouring"
            style="width: 14px; height: 14px; margin-right: -2px; margin-left: -1px;">
            </ha-icon><span> ${states[variables.precip].state}%</span>
          `}
      ]]]
    entity_picture: |
      [[[
        return entity && entity.state
          ? `/local/animated-weather-icons/${entity.state}.svg`
          : '?';
      ]]]
    extra_styles: |
      [[[
        return `
          #name {
            font-size: 1vw;
          }
          #temp {
            font-size: 1.9vw;
          }
          #state {
            font-size: 0.60vw;
          }  
          #label {
            font-size: 0.60vw;
          }
          /* portrait */
          @media screen and (max-width: 1200px) {
            #name {
              font-size: 1.3vw;
            }
            #temp {
              font-size: 4.5vw;
            }
            #state {
              font-size: 1.1vw;
            }  
            #label {
              font-size: 1.1vw;
            }
          }
          /* phone */
          @media screen and (max-width: 800px) {
            #name {
              font-size: 3vw;
            }              
            #temp {
              font-size: 2.5vw;
            }    
            #state {
              font-size: 1.8vw;
            }
            #label {
              font-size: 2.25vw;
            }
                   
          }
          @keyframes card_bounce {
            0% {
              transform: scale(1);
            }
            15% {
              transform: scale(0.9);
            }
            25% {
              transform: scale(1);
            }
            30% {
              transform: scale(0.98);
            }
            100% {
              transform: scale(1);
            }
          }
        `
      ]]]
1 Like

Very long time since I’ve been here.
I have updated to the build from a week or so back.
A lot of work getting everything to work together again, but did a lot of nice fixes for my own environment.
However this is the first time I have enabled “tilt”
And I am noticing with the tilt enabled the cards seem a lot more fuzzy/blurry, especially noticeable on text

image image

With tilt enabled (left) and disabled (right)
I don’t see this fuzziness for anyone else’s setup, is this something anyone has encountered before?

In the “inspect” view of the browser the following line seems to cause the fuzziness

Transform: perspective(800px) rotateX(0deg) rotateY(0deg) scale3d(1, 1, 1)

Which is a part of:

<ha-card id="card" class="button-card-main type-custom-button-card" style="border-radius: var(--button-card-border-radius); --mdc-ripple-color: #97989c; color: rgb(151, 152, 156); background-color: rgba(115, 115, 115, 0.25); --c-stroke-color-on: #b0b0b0; --c-stroke-color-off: #313638; --c-fill-color-on: none; --c-fill-color-off: rgba(255,255,255,0.04); --c-stroke-width-on: 2.3; --c-stroke-width-off: 1.5; --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; position: absolute; transform: perspective(800px) rotateX(0deg) rotateY(0deg) scale3d(1, 1, 1); will-change: transform;">

And a second question, I am positive I have seen the question and answer somewhere in the thread, but I cannot find it anywhere, even after searching and going to the thread.
But, when swiping on a swiper card, I am not able to change a dimmer light level by clicking and holding on the percentage circle
It works outside of the swiper card, and as I said the solution is out there but I cannot seem to find it, if anyone knows, please let me know

Yeah, but barely noticeable on my end, not like yours… Probably how your browser renders 3d transform?

Try some parameters https://swiperjs.com/swiper-api

type: custom:swipe-card
parameters:
  touchStartPreventDefault: false

Thanks Mattias, I will try some different browsers, and otherwise just disable the effect, I have lived without it thus far so I should be able to live without it permanently :stuck_out_tongue:

The swipe-card issue was instantly solved with your suggestion, thank you so much.

And also: Thanks a LOT for updating this project, improving it every step.
It has already come a long way, the code is a lot clearer, I have learned so much and I really love this project.
even though I still don’t have a tablet on my wall to put this on :wink:
Can’t wait to see what type of improvements you think of next!

1 Like

yes, as long as you enable the theme at a dashboard level not a system level

I try to set a picture as a background in a button-card, could anyone give me a hint how to do this?

And the new ps5-mqtt integration had now an activity-sensor, where I can see the game I play at the moment. Is it possible to show this in the PS5-Card isntead of the icon?

@Mattias_Persson

hi

understand i this code correct? the tilt is only alowed on this devices?

because on my Windows Laptop the Tilt is gone

 if (window.navigator.userAgent.match(/Macintosh; Intel Mac OS X/i)) {

@htpc2308

Yeap, you are correct.
You can replace the /Macintosh; Intel Mac OS X/i with your own useragent if you like.
Or if you want to make it more broad replace .match with .includes
For instance to make the tilt work with ANY firefox browser version:
if (window.navigator.userAgent.includes("Firefox")) {
Could even make it more broad by using the whole if/else as an exclude by switching the true and false
for instance, if you want to make the tilt only NOT work on firefox you can use:

if (window.navigator.userAgent.includes("Firefox")) {
        return false;
    }
    return true;

Play with the setting to get it to work on the device you want it.
For me, using it on android causes major slowdowns, so I only want it to show on Firefox
Since on mobile I always use the Home Assistant app anyway

2 Likes

Add something like this to the button-card-template of the card and play with the settings to achieve the result you want

styles:
  card:
    - align-self: middle
    - background-color: none
    - background-position: center center
    - background-repeat: no-repeat
    - background-image: image.png

Can you change the start/end of the marquee effect? Now the text is scrolling to the left side of the card true the icon and img_cell. Will it possible to start just where the name is and scroll to the right side.

marquee

I don’t know if you solved it already, but I solved the problem by adding the theme again. This caused problems due to the changes in extra_styles.yaml.

@venealis
I’ve run it on a Windows server for a while, but just recently migrated it back to a Synology NAS.
The Windows server used HASS.Agent. And now I’m just using the Synology integration.

Here’s the current popup (plex.yaml called with tap_action)

action: fire-dom-event
browser_mod:
  command: popup
  title: Plex
  style:
    .: |
      :host .main-title {
        pointer-events: none;
      }
    hui-vertical-stack-card:
      $hui-conditional-card>hui-vertical-stack-card$: |
        hui-horizontal-stack-card {
          padding: 0em 2em 1.5em 2em;
        }
      $hui-conditional-card>hui-vertical-stack-card$hui-horizontal-stack-card$: |
        #root {
          justify-content: space-evenly;
        }
        
  card:
    type: vertical-stack
    cards:

      ### ON ###
      - type: conditional
        conditions:
          - entity: binary_sensor.ping_rackstation
            state: 'on'
        card:
          type: vertical-stack
          cards:
            - type: entities
              state_color: true
              card_mod:
                class: content
              entities:

                - entity: switch.wol_plex
                  name: Power state
                  secondary_info: last-changed
                  icon: mdi:power

                - type: custom:bar-card
                  width: 55%
                  height: 2em
                  decimal: 0
                  unit_of_measurement: '%'
                  positions: &pos
                    icon: outside
                    indicator: 'off'
                    name: outside
                  severity: &sev
                    - color: '#303435'
                      from: 0
                      to: 89
                    - color: '#6d2525'
                      from: 90
                      to: 100
                  entity_row: true
                  entities:

                    - entity: sensor.rackstation_cpu_utilization_total
                      name: CPU load
                      tap_action:
                        action: call-service
                        service: homeassistant.update_entity
                        service_data:
                          entity_id: sensor.rackstation_cpu_utilization_total

                    - entity: sensor.rackstation_memory_usage_real
                      name: RAM usage
                      tap_action:
                        action: call-service
                        service: homeassistant.update_entity
                        service_data:
                          entity_id: sensor.rackstation_memory_usage_real


                - entity: sensor.plex_issues
                  name: Health

                - entity: sensor.plex_sagaflix
                  name: Activity
                  icon: mdi:progress-upload

                - entity: sensor.rackstation_last_boot
                  name: Last boot

                - entity: sensor.sagaflix_library_movies
                  name: Movies

                - entity: sensor.sagaflix_library_tv_shows
                  name: TV Shows


            - type: horizontal-stack
              cards:
              - type: custom:button-card
                name: Sleep
                icon: mdi:power-sleep
                tap_action:
                  action: call-service
                  service: switch.toggle
                  service_data:
                    entity_id: switch.hass_custom_sleep_cmd
                template: icon_name

              - type: custom:button-card
                name: Refresh
                icon: mdi:cog-refresh
                tap_action:
                  action: call-service
                  service: switch.toggle
                  service_data:
                    entity_id: switch.hass_custom_hass_restart
                template: icon_name

              - type: custom:button-card
                name: Reboot
                icon: mdi:restart
                tap_action:
                  action: call-service
                  service: switch.toggle
                  service_data:
                    entity_id: switch.hass_pc_restart
                template: icon_name


      ### OFF ###
      - type: conditional
        conditions:
          - entity: binary_sensor.ping_rackstation
            state: 'off'
        card:
          type: vertical-stack
          cards:
            - type: entities
              state_color: true
              show_header_toggle: false
              card_mod:
                class: content
              entities:

                - entity: switch.wol_plex
                  name: Plex
                  secondary_info: last-changed

            - type: horizontal-stack
              cards:
                - type: custom:button-card
                  name: Power On
                  icon: mdi:power
                  tap_action:
                    action: call-service
                    service: switch.turn_on
                    service_data:
                      entity_id: switch.wol_plex
                  template: icon_name

Appreciate it thanks

I made an attempt at getting the circle value in a more generic way. but im going to give up for now.

the issue I have is the number of switches on is not up to date, it always displays 1, but ill post the code so you have more of a base to work from. this was added to the template code you posted above

  variables:
      circle_input:  >
        [[[
          let on = 0, id = Boolean(entity.attributes.entity_id)
            ? [entity.entity_id].concat(entity.attributes.entity_id)
            : [entity.entity_id];
          for (let i = 1; i < id.length; i++) {
            if(states[id[i]].state == 'on') on++
          }
          return entity === undefined || Math.round(on / (id.length -1) * 100 ) ;

        ]]]
  custom_fields:
    circle: >
      [[[
          let input = variables.circle_input,
            radius = 20.5,
            circumference = radius * 2 * Math.PI;
          let on = 0, id = Boolean(entity.attributes.entity_id)
            ? [entity.entity_id].concat(entity.attributes.entity_id)
            : [entity.entity_id];
          for (let i = 1; i < id.length; i++) {
            if(states[id[i]].state == 'on') on++
          }

          let inner_text = `${on} / ${(id.length -1)}`
          return `
            <svg viewBox="0 0 50 50">
              <style>
                circle {
                  transform: rotate(-90deg);
                  transform-origin: 50% 50%;
                  stroke-dasharray: ${circumference};
                  stroke-dashoffset: ${circumference - input / 100 * circumference};
                }
                tspan {
                  font-size: 10px;
                }
              </style>
              <circle cx="25" cy="25" r="${radius}" stroke="#b2b2b2" stroke-width="1.5" fill="none" />
              <text x="50%" y="54%" fill="#8d8e90" font-size="14" text-anchor="middle" alignment-baseline="middle" dominant-baseline="middle">${inner_text}</text>
            </svg>
          `;
      ]]]

so I have been working on integrating my AC that was just a input_select for fan level and is now using the generic_thermostat, but I having an odd issue tithe the icon_fan2, on the left is the new card and on the right is the old card.

I found that the issue was when the icon was used along with the circle,

Screen Shot 2022-08-28 at 1.43.28 pm

Fix

I needed to apply a better css selector in the icon’s css so it would over ride the css from the circle, I did try important but that didn’t work.

  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;
                }
                .start > circle {
                  fill: #5daeea;
                }
                .on {
                  animation: rotate 1.8s linear infinite;
                  transform-origin: center;
                  fill: #5daeea;
                  animation-delay: 2.8s;
                  visibility: hidden;
                  will-change: transform;
                }
                .on > circle {
                  fill: #5daeea;
                }
                .end {
                  animation: rotate 2.8s;
                  transform-origin: center;
                  fill: #9ca2a5;
                  animation-timing-function: cubic-bezier(0.61, 1, 0.88, 1);
                  will-change: transform;
                }
                .end > circle {
                  fill: #9ca2a5;
                }
                .start_timeout {
                  animation: rotate 1.8s linear infinite;
                  transform-origin: center;
                  fill: #5daeea;
                  visibility: hidden;
                  will-change: transform;
                }
                .start_timeout > circle {
                  fill: #5daeea;
                }
                .end_timeout {
                  fill: #9ca2a5;
                }
                .end_timeout > circle {
                  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>`;
          }
        ]]]
        
1 Like

Hi! I feel like I’m missing something stupid. The sidebar isn’t coming up even though I haven’t done any edits to it yet. Any ideas? Thanks!

Got the same error yesterday, while I forgot to close a bracket inside a if-condition.