A different take on designing a Lovelace UI

Very cool. thanks guys. I’ll look into both. I’m trying to keep the time spent in a zone inside the circle. But have the circle show the percentage. I should be able to figure it out now. Thanks

Screenshot 2024-10-01 at 8.52.24 AM
Hmmm. I’m almost there. But for some reason, the units (timed presence) inside the circle won’t show.

Dashboard:

        cards:
          - type: custom:button-card
            entity: sensor.paul_combined
            name: Paul
            triggers_update:
              - sensor.paul_last_changed
            tap_action: !include popup/home_paul.yaml
            template:
              - person
            variables:
              battery_level: sensor.pauls_iphone_12_battery_level
              battery_charge: sensor.pauls_iphone_battery_status

person.yaml:

person:
  template:
    - base
    - circle
  state_display: >
    [[[
      if (entity) {
        const stateperson = entity.state === 'home' ? variables.translate_home : variables.state === 'not_home' ? variables.translate_not_home : variables.state;
        return `<div style="white-space: nowrap; overflow: hidden; text-overflow: clip; width: 100%;" scrollamount="3">${stateperson}</div>`
      }
      return variables.translate_unknown;
    ]]]
  triggers_update: sensor.time
  tap_action:
    action: none
  variables:
      circle_input: >
        [[[
          if (entity && entity.last_changed) {
            const lastChanged = new Date(entity.last_changed);
            const now = new Date();
            const diff = Math.floor((now - lastChanged) / 1000); // difference in seconds
            if (diff < 60) return `${diff}s`;
            if (diff < 3600) return `${Math.floor(diff / 60)}m`;
            if (diff < 86400) return `${Math.floor(diff / 3600)}h`;
            return `${Math.floor(diff / 86400)}d`;
          }
          return '';
        ]]]
      battery_level: ' '
      battery_charge: ' '
  styles:
    custom_fields:
      icon:
        - clip-path: circle()
        - width: 82%
        - pointer-events: none
        - display: grid
      steps:
        - position: absolute
        - right: 5%
        - bottom: 5%
        - font-size: 20px
        - font-weight: 500
        - color: '#4b5254'
      charge_icon:
        - position: absolute
        - right: 20%
        - left: 58%
        - top: 70%
  custom_fields:
    icon: >
      [[[
        return entity && entity.attributes.entity_picture
            ? `<img src="${entity.attributes.entity_picture}" width="100%">`
            : null;
      ]]]
    steps: >
      [[[
        return entity && entity.attributes.steps
          ? `${entity.attributes.steps}`
          : '';
      ]]]
    circle: >
      [[[
          let inputs = states[variables.battery_level].state,
            radius = 22.1,
            circumference = radius * 2 * Math.PI;
          var color = "rgba(48, 128, 181, 0.8)";
          if (inputs <= 10) {
            color = "#FDD60F";
          } else if (inputs <= 25) {
            color = "rgba(48, 128, 181, 0.8)";
          }
          else {
            color = "#27C950";
          }
          return `
            <svg viewBox="0 0 50 50">
              <style>
                circle {
                  transform: rotate(-90deg);
                  transform-origin: 50% 50%;
                  stroke-dasharray: ${circumference};
                  stroke-dashoffset: ${circumference - inputs / 100 * circumference};
                }
              </style>
              <circle cx="25" cy="25" r="${radius}" stroke="${color}" stroke-width="2" fill="none" stroke-linecap="round"/>
            </svg>
          `;
      ]]]
    charge_icon: >
      [[[
              const state = states[variables.battery_charge].state;
              const lightbulbIcon = `<ha-icon icon="mdi:flash" style="width: 20px; height: 20px; color: red;"></ha-icon>`;
              const blinkingAnimation = '@keyframes blink { 50% { opacity: 0; } }';           
              if (state === 'Charging') {
                return `
                  ${lightbulbIcon}
                  <style>
                    ${blinkingAnimation}
                    [lightbulb] {
                      animation: blink 1s infinite;
                    }
                  </style>
                `;
              } else {
                return " ";
              }
        ]]]

My circle.yaml file is the same as the original with this code still in there:

    /* * * * * * * * * * * * * * * * * *
            *                                 *
            *             PERSON              *
            *                                 *
            * * * * * * * * * * * * * * * * * */

            else if (domain === 'person') {
                // Time calculation logic
                let time = c => {
                    let s = (c / 1e3),
                        m = (c / 6e4),
                        h = (c / 36e5),
                        d = (c / 864e5);
                    return s < 60
                        ? parseInt(s) + 's'
                        : m < 60 ? parseInt(m) + 'm'
                        : h < 24 ? parseInt(h) + 'h'
                        : parseInt(d) + 'd';
                };
                let input = states[variables.retain] === undefined || states[variables.retain].state === 'unavailable'
                        ? time(Date.now() - Date.parse(entity.last_changed))
                        : time(Date.now() - Date.parse(states[variables.retain].state)),
                    unit = ' ';
                return circle(state, input, unit);
            }

I do realize that I took out this line:

<text x="50%" y="54%" fill="#8d8e90" font-size="14" text-anchor="middle" alignment-baseline="middle" dominant-baseline="middle">${input}<tspan font-size="10">${unit}</tspan></text>

But it was because I didn’t want the battery level percentage inside the circle.
I still want the time spent in said zone.
I can’t figure out what’s wrong.

EDIT: Or is it that I have to now define the circle_input within the person.yaml?

Now I’m not sure what you need at all. Do you want to have time in the circle, but a stroke for battery level?

You don’t use the ‘person’ domain for the main entity, so the input for the circle is using battery state. Add another variable like ‘person_retain’ with sensor ‘sensor.paul_last_changed’, further in template add variable for display the text in circle…

updated template with added retain variable and battery charging state icon

battery_circle:
  template:
    - base
    - circle
  variables:
    battery: ' '
    circle_unit: ' '
    person_retain: ' '
    battery_status: ' '
  state_display: >
    [[[
      if (entity) {
          return variables.state === 'home'
              ? variables.translate_home
              : variables.state === 'not_home'
                  ? variables.translate_not_home
                  : variables.state;
      }
      return variables.translate_unknown;
    ]]]
  triggers_update: sensor.time
  custom_fields:
    icon: >
      [[[
        return entity && variables.entity_picture
            ? `<img src="${variables.entity_picture}" width="100%">`
            : null;
      ]]]
    circle: >
      [[[
          let input = states[variables.battery].state,
            radius = 20.5,
            circumference = radius * 2 * Math.PI;
          let unit = variables.circle_unit;
          let time = c => {
              let s = (c/1e3),
                  m = (c/6e4),
                  h = (c/36e5),
                  d = (c/864e5);
              return s < 60
                  ? parseInt(s) + 's'
                  : m < 60 ? parseInt(m) + 'm'
                  : h < 24 ? parseInt(h) + 'h'
                  : parseInt(d) + 'd';
          };
          let person_retain = states[variables.person_retain] !== undefined || states[variables.person_retain].state !== 'unavailable'
                        ? time(Date.now() - Date.parse(states[variables.person_retain].state))
                        : time(Date.now() - Date.parse(entity.last_changed));
          var color = "rgba(48, 128, 181, 0.8)";
          if (input <= 20) {
            color = "#FDD60F";
          } else if (input <= 40) {
            color = "rgba(48, 128, 181, 0.8)";
          }
          else {
            color = "#27C950";
          }
          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;
                }
                .flash-icon {
                  font-size: 14px;
                  display: ${states[variables.battery_status].state === 'charging' ? 'block' : 'none'};
                  animation: blink 1s linear infinite;
                }
                @keyframes blink {
                  50% {
                    opacity: 0;
                  }
                }
              </style>
              <circle cx="25" cy="25" r="${radius}" stroke="${color}" stroke-width="3" fill="none" stroke-linecap="round"/>
              <text x="50%" y="15%" class="flash-icon" text-anchor="middle" alignment-baseline="middle">⚡</text>
              <text x="50%" y="54%" fill="#8d8e90" font-size="14" text-anchor="middle" alignment-baseline="middle" dominant-baseline="middle">${person_retain}<tspan font-size="10">${unit}</tspan></text>
            </svg>
          `;
      ]]]
  styles:
    custom_fields:
      icon:
        - clip-path: circle()
        - width: 82%
        - pointer-events: none
        - display: grid
        - filter: >
            [[[
                return variables.state === 'not_home'
                  ? `grayscale(1)`
                  : null;
            ]]]

button

  card:
    type: custom:button-card
    entity: person.viet_ngoc
    name: Viet Ngoc
    template:
      - battery_circle
    variables:
      battery: sensor.viet_ngoc_battery_level
      person_retain: sensor.viet_ngoc_last_changed
      battery_status: sensor.roidmi_v60_029d_charging_state

@chezpaul2
2024-10-01 20.05.43
Or a stroke as a border for the icon…
CleanShot 2024-10-01 at 21.02.51@2x

2 Likes

Yes, that was it. Thanks a ton.

I added a charging icon as a flash above the circle, it only shows when the state is charging

Yes I just saw that. I’ll use it too. :wink:
Thanks a ton


It looks like my person_retain is “undefined”.
It used to work fine.
person_retain: sensor.paul_last_changed
That’s created by the mqtt integration right?
I’m guessing that’s why you can’t see it in the dev>states page.

Can you share your updated config?

Sure. Thanks
person.yaml:

person:
  template:
    - base
    - circle
  state_display: >
    [[[
      if (entity) {
          return variables.state === 'home'
              ? variables.translate_home
              : variables.state === 'not_home'
                  ? variables.translate_not_home
                  : variables.state;
      }
      return variables.translate_unknown;
    ]]]
  triggers_update: sensor.time
  tap_action:
    action: none
  variables:
    battery_level: ' '
    battery_status: ' '
    person_retain: ' '
    circle_unit: ' '
  styles:
    custom_fields:
      icon:
        - clip-path: circle()
        - width: 82%
        - pointer-events: none
        - display: grid
      steps:
        - position: absolute
        - right: 2%
        - bottom: 8%
        - font-size: 20px
        - font-weight: 500
        - color: '#4b5254'
  custom_fields:
    icon: >
      [[[
        return entity && entity.attributes.entity_picture
            ? `<img src="${entity.attributes.entity_picture}" width="100%">`
            : null;
      ]]]
    steps: >
      [[[
        return entity && entity.attributes.steps
          ? `${entity.attributes.steps}`
          : '';
      ]]]
    circle: >
      [[[
          let input = states[variables.battery_level].state,
            radius = 22.1,
            circumference = radius * 2 * Math.PI;
          let unit = variables.circle_unit;
          let time = c => {
              let s = (c/1e3),
                  m = (c/6e4),
                  h = (c/36e5),
                  d = (c/864e5);
              return s < 60
                  ? parseInt(s) + 's'
                  : m < 60 ? parseInt(m) + 'm'
                  : h < 24 ? parseInt(h) + 'h'
                  : parseInt(d) + 'd';
          };
          let person_retain = states[variables.person_retain] !== undefined || states[variables.person_retain].state !== 'unavailable'
                        ? time(Date.now() - Date.parse(states[variables.person_retain].state))
                        : time(Date.now() - Date.parse(entity.last_changed));
          var color = "rgba(48, 128, 181, 0.8)";
          if (input <= 10) {
            color = "#FDD60F";
          } else if (input <= 20) {
            color = "rgba(48, 128, 181, 0.8)";
          }
          else {
            color = "#27C950";
          }
          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;
                }
                .flash-icon {
                  font-size: 14px;
                  display: ${states[variables.battery_status].state === 'charging' ? 'block' : 'none'};
                  animation: blink 1s linear infinite;
                }
                @keyframes blink {
                  50% {
                    opacity: 0;
                  }
                }
              </style>
              <circle cx="25" cy="25" r="${radius}" stroke="${color}" stroke-width="2" fill="none" stroke-linecap="round"/>
              <text x="50%" y="15%" class="flash-icon" text-anchor="middle" alignment-baseline="middle">⚡</text>
              <text x="50%" y="54%" fill="#8d8e90" font-size="14" text-anchor="middle" alignment-baseline="middle" dominant-baseline="middle">${person_retain}<tspan font-size="10">${unit}</tspan></text>
            </svg>
          `;
      ]]]

card:

        cards:
          - type: custom:button-card
            entity: sensor.paul_combined
            name: Paul
            triggers_update:
              - sensor.paul_last_changed
            tap_action: !include popup/home_paul.yaml
            template:
              - person
            variables:
              battery_level: sensor.pauls_iphone_12_battery_level
              battery_status: sensor.pauls_iphone_battery_status
              person_retain: sensor.paul_last_changed

Probably the sensor is unavailable, try restarting HA… Sensor should have a state like timestamp

Hmmm. Strange…

This is where it’s created right?

#Person Persistance
  - platform: mqtt
    name: paul_last_changed
    state_topic: homeassistant/persistence/paul
    value_template: >
        {{ value | replace(' ', 'T') }}

I would look at how you have created the sensor, yes it is a mqqt sensor, including automation to update the sensor…

alias: person_home_change
description: ""
trigger:
  - platform: state
    entity_id:
      - person.viet_ngoc
    from:
      - home
      - not_home
    to:
      - home
      - not_home
action:
  - data:
      topic: |
        homeassistant/persistence/{{ trigger.to_state.name | lower }}
      payload: |
        {{ now() }}
      retain: true
    action: mqtt.publish
mode: parallel


Yeah, I have all those defined in my HA. I mean last_changed has always been working for years. It’s always been there. It’s just that now, trying to use the variable: person_retain that doesn’t seem to work.
And for some reason, sensor.paul_last_changed has never existed in my dev>states page.
I now remember already trying to find it and not being able to but because the person button always showed the time accordingly to what was really happening, I never gave it a second thought. (I thought it was an mqtt thing to not show the sensor.)

Try to recreate the sensor in a new format, without platforms… but directly for mqtt, like this…
and after that either restart HA or in dev tools trigger mqtt to reload config.

mqtt:
  sensor: 
    - name: 'paul_last_changed'
.
.


For debug, you can manually change the state for person. This will trigger the update mqtt sensor…

Seems to be working…

But still no last_changed

I just noted that my *** PERSON *** section of circle is different than yours.
This is mine:

           /* * * * * * * * * * * * * * * * * *
            *                                 *
            *             PERSON              *
            *                                 *
            * * * * * * * * * * * * * * * * * */

            else if (domain === 'person') {
                let time = c => {
                    let s = (c/1e3),
                        m = (c/6e4),
                        h = (c/36e5),
                        d = (c/864e5);
                    return s < 60
                        ? parseInt(s) + 's'
                        : m < 60 ? parseInt(m) + 'm'
                        : h < 24 ? parseInt(h) + 'h'
                        : parseInt(d) + 'd';
                };
                let input = states[variables.retain] === undefined || states[variables.retain].state === 'unavailable'
                        ? time(Date.now() - Date.parse(entity.last_changed))
                        : time(Date.now() - Date.parse(states[variables.retain].state)),
                    unit = ' ';
                return circle(state, input, unit);
            }

My template does not use the script from the circle base template. The problem is that your sensor is unavailable. You can create a test template sensor and use it in a variable for person retain. And remove the triggers_update in you config.

triggers_update:
              - sensor.paul_last_changed

test sensor template… don’t forget to reload yaml via dev tools after.

template:
  # SENSORS
  - sensor:
      - unique_id: person_last_changed
        name: 'Test person last changed'
        state: >-
          {{ states.person.viet_ngoc.last_changed | replace(" ", "T") }}

you get this sensor with the available state…

Finally figure out what it was. My mqtt.yaml file was not being called up! Pfff!
Fixed it. Now the buttons work, of course.
Thank you so much for taking the time to help me out.

I encourage anyone to go and use John’s cards that he has created for HA.
The Vehicle one is AMAZING!
The Lunar phase one is also really cool.

Come to think of it, John, you need to make a really nice person card. :wink:

Anyway, you went way beyond the call. thanks again.

2 Likes

Or a beautiful Music Assistant card like this one? (not yet released) :wink:

@chezpaul2 I’m working on a custom card for HA that lets you browse and manage your movie library, working with Kodi integration. You can search for movies in your local collection or pull up results from TMDB. For movie detail, it has its own popup with trailer, description etc…
You can control your media player directly from the card, sending movies to any device in real time using websocket. It all happens smoothly without any delays. Right now, it’s just for my personal use, so it is not published anywhere yet. :sweat_smile:

You can see how it looks here :point_down:

https://dropover.cloud/097e97

movie-lib

I also use Music Assistant, though I don’t have much experience with how addons work in Home Assistant. However, I’ve created my own card similar to this one, but for my music library. It currently works with YouTube Music and Spotify, offering similar features to Music Assistant, including full media player control.

3 Likes

Wow @VietNgoc. That looks real, real nice. Everything you do looks so professional.
I use your car card everyday, 4 to 5 times a day. Love it. Ho by the way, the range progress bar doesn’t seem to show at all. Whatever I put in there, nothing shows on the card. Not sure why.
But everything else works like a charm.

The movie library thing you working on looks amazing. I would love to use it. I can beta test if you want. haha. But I use plex as my movie server and I have mostly French movies. Haha…

I’ve been trying since the beginning of using HA to do a radio card for my wife so she can listen to French radio while at home on any speaker but it’s harder to do than I thought. Well I mean to do well, with a nice UI. Music assistant looks promising.

I do like what you have on your Dashboard on the movie section. the automatic swiping of movies with their rating etc. I tried to copy it from your GitHub but I felled miserably. I also think you’ve changed your HA yaml file but not the screenshots. :wink:
But I thought it was for all about to be released movies and not the ones in your Kodi. I thought that would be cool to see what’s coming out.
I did use kodi a while back for a long time but find plex to be a little nicer on the eye and less work to keep it up and running.And for some reason, I don’t like the no mouse attitude of Kodi. :joy:

I wouldn’t mind tryin your music assistant card though. :grimacing: