A different take on designing a Lovelace UI

I have searched and tested, the problem is browser, backdrop-filter. Try other browser. I use chrome, when I resize to “phone screen” then it blurs, otherwise not.

hey, can you share your config? :slight_smile: thanks in advance!

Hi, everything is on my repo :wink: ngocjohn (Viet Ngoc) · GitHub

hello, when i turn my tv off, im allways getting this error.

Anyone can help?

Thanks in advance
Capture

onthe base template i have:`

base:
  template:
    - settings
    - tilt
    - extra_styles
  variables:
    state_on: >
      [[[ return ['on', 'home', 'cool', 'fan_only', 'playing', 'unlocked'].indexOf(!entity || entity.state) !== -1; ]]]
    state: >
      [[[ return !entity || entity.state; ]]]
    entity_id: >
      [[[ return !entity || entity.entity_id; ]]]
    entity_picture: >
      [[[ return !entity || entity.attributes.entity_picture; ]]]
    timeout: >
      [[[ return !entity || Date.now() - Date.parse(entity.last_changed); ]]]
    is_youtube: >
      [[[
        let is_youtube = entity?.attributes?.app_id === 'com.google.ios.youtube',
            sensor = this?._config?.triggers_update,
            media_title = entity?.attributes?.media_title,
            watching_title = states[sensor]?.attributes?.title;
        if (is_youtube && media_title === watching_title) {
            return true;
        }
      ]]]
  aspect_ratio: 1/1
  show_state: true
  show_icon: false
  state_display: >
    [[[
      const stateDict = {
        'on': variables.translate_on,
        'off': variables.translate_off,
        'cool': variables.translate_cool,
        'fan_only': variables.translate_fan_only,
      };
      if (variables.state === true) return variables.translate_unknown;
      return stateDict[variables.state];
    ]]]
  tap_action:
    ui_sound_tablet: |
      [[[
        let screensaver = states[variables.entity_tablet] === undefined ||
            states[variables.entity_tablet].state;

        if (variables.state === 'off' && screensaver === 'off') {
            hass.callService('media_player', 'play_media', {
                entity_id: variables.entity_browser_mod,
                media_content_id: '/local/sound/on.m4a',
                media_content_type: 'music'
            });
        }
        if (variables.state_on && screensaver === 'off') {
            hass.callService('media_player', 'play_media', {
                entity_id: variables.entity_browser_mod,
                media_content_id: '/local/sound/off.m4a',
                media_content_type: 'music'
            });
        }
      ]]]
    card_bounce: |
      [[[
        // add animation
        if (this.getElementsByTagName("style").length === 0) {

            // phone condition
            let mq = window.matchMedia('(max-width: 800px)').matches;

            let style = document.createElement('style');

            style.innerHTML = `
                @keyframes card_bounce {
                    0%   { transform: scale(1); }
                    10%  { transform: scale(${ mq ? '0.92' : '0.94' }); }
                    25%  { transform: scale(1); }
                    30%  { transform: scale(${ mq ? '0.96' : '0.98' }); }
                    100% { transform: scale(1); }
                }
            `;

            this.appendChild(style);
        }

        // duration
        let duration = 800;

        // animate
        this.style.animation = `card_bounce ${duration}ms cubic-bezier(0.22, 1, 0.36, 1)`;

        // reset
        window.setTimeout(() => { this.style.animation = "none"; }, duration + 100)
      ]]]
    action: toggle
    haptic: medium
  double_tap_action:
    haptic: success
  hold_action:
    action: block
  styles:
    grid:
      - grid-template-areas: |
          "icon  circle"
          "n     n"
          "s     s"
      - grid-template-columns: repeat(2, 1fr)
      - grid-template-rows: auto repeat(2, min-content)
      - gap: 1.3%
      - align-items: start
      - will-change: transform
    name:
      - justify-self: start
      - line-height: 121%
    state:
      - justify-self: start
      - line-height: 115%
    card:
      - border-radius: var(--button-card-border-radius)
      - border-width: 0
      - -webkit-tap-highlight-color: rgba(0,0,0,0)
      - transition: none
      - --mdc-ripple-color: >
          [[[
            return variables.state_on
                ? 'rgb(0, 0, 0)'
                : '#97989c';
          ]]]
      - color: >
          [[[
            return variables.state_on
                ? '#4b5254'
                : '#97989c';
          ]]]
      - background-color: >
          [[[
            return variables.state_on
                ? 'rgba(255, 255, 255, 0.85)'
                : 'rgba(115, 115, 115, 0.25)';
          ]]]

and int the button:

`          - type: custom:button-card
            entity: media_player.lg_webos_smart_tv
            name: Tv
            state_display: >
              [[[
                if (variables.state === 'playing') {
                    return 'Playing';
                }
                if (variables.state === true) {
                    return variables.translate_unknown;
                }
              ]]]
  #          double_tap_action: !include popup/sala_tv.yaml
            template:
              - base
              - icon_tv`

Will you share your “gallery” function when no media played? Thx

sure. Install gallery_image over hacs.
ui-lovelace.yaml

     #################################################
     #                                               #
     #                     MEDIA                     #
     #                                               #
     #################################################

      - type: grid
        title: Medien
        view_layout:
          grid-area: media
        columns: 1
        cards:

          - type: custom:swipe-card
            parameters:
              touchStartPreventDefault: false
              roundLengths: true
              speed: 550
              spaceBetween: 40
              threshold: 5
              autoHeight: false
              initialSlide: 0
              centeredSlides: true
              slidesPerView: auto
#              effect: cube              loop: false
              preventClicksPropagation: true
              preventClicks: true
              pagination:
                type: bullets
            cards:

              - type: horizontal-stack
                cards:

                  - type: conditional
                    conditions:
                      - entity: select.conditional_media
                        state: gallery
                    card: 
                      type: custom:gallery-card
                      entities:
                        - sensor.gallery_images
                      menu_alignment: hidden
                      slideshow_timer: 5
                      card_mod:
                        style: |
                          ha-card {
                            border-radius: calc(var(--custom-button-card-border-radius) / 2);  /* card - rounded corners */
                            aspect-ratio: 1/1;  /* card - square */
                          }
                          figure {
                            margin: 0px !important;  /* remove card margins to line up with rest of dashboard */
                          }
                          figcaption {
                            display: none;  /* hide image caption */
                          }
                          img {
                            object-fit: cover !important;  /* fill the whole card */
                            aspect-ratio: 1/1;  /* needed for object-fit */
                          }
                          .btn {
                            top: 50% !important;  /* center buttons */
                          }
                          .modal-content {
                            aspect-ratio: unset;  /* undo image aspect-ratio when clicked */
                          }

                  - type: conditional
                    conditions:
                      - entity: select.conditional_media
                        state: TV
                    card:
                      type: custom:button-card
                      entity: media_player.tv_wohnzimmer_2
                      template: [conditional_media, progress_bar]

configuration.yaml

  - platform: files
    folder: /config/www/img
    filter: '**/*.jpg'
    name: gallery_images
    sort: date
    recursive: True

tv_media.yaml

template:
  - select:
      - name: conditional_media
        state: >
          {% set recently_added = 'gallery' %}
          {% set recently_added_backup = 'Recently Added (Offline)' %}
          {% set paused_timeout_minutes = 15 %}
          {% set media_players = [
            states.media_player.tv_wohnzimmer_2,
            states.media_player.tv_schlafzimmer_2,
            states.media_player.vardagsrum,
            states.media_player.lea,
            states.sensor.ps5_101_activity,
            states.media_player.wohnzimmer_oben,
            states.media_player.schlafzimmer,
            states.media_player.buro,
            states.media_player.maja,
            states.media_player.kuche,
            states.media_player.fernseher_maja_2 ] %}

          {% macro media(state) %}
          {% set state = media_players | selectattr('state','eq',state) | list %}
          {% set last_changed = recently_added if state | length == 0 else state | map(attribute='last_changed') | list | max %}
            {{ state | selectattr('last_changed','eq', last_changed) | map(attribute='name') | list | join }}
          {% endmacro %}

          {% set recently_added = recently_added_backup if is_state('sensor.recently_added_offline','Active') else recently_added %}

          {% set playing = media_players | selectattr('state','eq','playing') | list %}
          {% set timeout_playing = False if on | length == 0 else
            (as_timestamp(now()) - as_timestamp(on | map(attribute='last_changed') | list | max)) < paused_timeout_minutes * 60 %}
                
          {% set paused = media_players | selectattr('state','eq','paused') | list %}
          {% set timeout_paused = False if paused | length == 0 else
            (as_timestamp(now()) - as_timestamp(paused | map(attribute='last_changed') | list | max)) < paused_timeout_minutes * 60 %}

          {% if playing %}
            {{ media('playing') if timeout_playing else media('paused') if timeout_paused else media('playing') }}
          {% elif paused %}
            {{ media('paused') if timeout_paused else recently_added }}
          {% else %}
            {{ recently_added }}
          {% endif %}
        options: >
          {% set recently_added = ['gallery'] %}
          {% set recently_added_backup = ['Recently Added (Offline)'] %}
          {% set media_players = [
            states.media_player.tv_wohnzimmer_2,
            states.media_player.vardagsrum,
            states.media_player.lea,
            states.media_player.tv_schlafzimmer_2,
            states.sensor.ps5_101_activity,
            states.media_player.wohnzimmer_oben,
            states.media_player.schlafzimmer,
            states.media_player.buro,
            states.media_player.maja,
            states.media_player.kuche,
            states.media_player.fernseher_maja_2 ] %}
          {{ recently_added + recently_added_backup + media_players | map(attribute='name') | list }}
        select_option:
          service: select.select_option
          target:
            entity_id: select.conditional_media
          data:
            option: >
              {{ option }}

I hope that I don´t forget something

1 Like

How do you get that green glow around the buttons?
Thanks

Its just some box-shadow styling, based on state, that I added to the button card template that I use for those particular button cards:

card:
      - border-radius: var(--button-card-border-radius)
      - border-width: 0
      - -webkit-tap-highlight-color: rgba(0,0,0,0)
      - transition: none
      - --mdc-ripple-color: >
          [[[
            return variables.state_on
                ? 'rgb(0, 0, 0)'
                : '#97989c';
          ]]]
      - color: >
          [[[
            return variables.state_on
                ? '#4b5254'
                : '#97989c';
          ]]]
      - background-color: >
          [[[
            return variables.state_on
                ? 'rgba(255, 255, 255, 0.85)'
                : 'rgba(115, 115, 115, 0.25)';
          ]]]
      - box-shadow: >
          [[[
            return variables.state_on
                ? '0px 0px 10px 3px red'
                : '0px 0px 10px 3px green';
          ]]]
1 Like

This is great!
Unfortunately, I’m unable to make it work in several grids at the same time; only the first one is showing the values.
E.g., below an example of my dashboard where I have one grid per room and I’d like to have the temperature and humidity in each title. However, only the first one shows. Is this expected or is there any way to make it work like this?


The goal is to eliminate those 4 cards and avoid having to swipe whenever I need to turn on a light using the wallpanel

1 Like

Hi,

I adapt the code template from VietNgoc to the following:
wz3

Font Size and Icon Size can adapt to your needs.
Now you can show on all grifs: Temperature, Humidity, Position of a thermostat valve (or your lux sensor) and motion.

For example:

                      tempsensor: sensor.buero2_temperature
                      humidsensor: sensor.buero_position
                      positionSensor: sensor.buero_position
                      motionSensor: binary_sensor.bewegungsmelder_treppe
                      fontSize: 0.8rem
                      motionIconSize: 15px

when there is no sensor defined then nothing will be shown. I hope that works.

change_grid_title:
  state_display: >
    [[[
      function findFirstH1Above(element) {
        let currentElement = element;
        while (currentElement) {
          if (currentElement.shadowRoot) {
            const shadowH1 = currentElement.shadowRoot.querySelector('h1');
            if (shadowH1) return shadowH1;
          }
          const lightH1 = currentElement.querySelector('h1');
          if (lightH1) return lightH1;
          currentElement = currentElement.getRootNode().host;
          if (!currentElement) break;
        }
        return null;
      }
    
      let parentElement = this.getRootNode().host;
      let headerTitle = findFirstH1Above(parentElement);
    
      if (headerTitle && !headerTitle.dataset.processed) {
        const tempSensor = parseFloat(states[variables.tempsensor]?.state);
        const humidSensor = parseFloat(states[variables.humidsensor]?.state);
        const positionSensor = parseFloat(states[variables.positionSensor]?.state);
        const motionState = states[variables.motionSensor]?.state;
        const motionIcon = motionState === 'on' ? 'mdi:motion-sensor' : 'mdi:motion-sensor-off';
        const fontSize = variables.fontSize || '14px';
        const motionIconSize = variables.motionIconSize || '20px';
        let currentTitle = headerTitle.innerText;
        headerTitle.style.display = 'flex';
        headerTitle.style.width = '100%';
        headerTitle.style.justifyContent = 'space-between';
        headerTitle.style.alignItems = 'center';
        headerTitle.style.paddingInline = 'inherit';
        const formattedTemp = !isNaN(tempSensor) ? 
            (tempSensor % 1 === 0 ? tempSensor.toFixed(0) : tempSensor.toFixed(1)).replace('.', ',') + '°C' : '';
        const formattedHumid = !isNaN(humidSensor) ? humidSensor.toFixed(0) + '%' : '';
        const formattedPosition = !isNaN(positionSensor) ? positionSensor.toFixed(0) + '%' : '';
        let sensorValues = [];
        if (formattedTemp) sensorValues.push(formattedTemp);
        if (formattedHumid) sensorValues.push(formattedHumid);
        if (formattedPosition) sensorValues.push(formattedPosition);
        const sensorDisplay = sensorValues.join(' / ');
        const motionIconHTML = states[variables.motionSensor] 
          ? `<ha-icon icon="${motionIcon}" style="margin-left: 5px; --mdc-icon-size: ${motionIconSize};"></ha-icon>`
          : '';
        headerTitle.innerHTML = `
          <div>${currentTitle}</div>
          <div style="font-size: ${fontSize}; display: flex; align-items: center;">
            ${sensorDisplay}
            ${motionIconHTML}
          </div>
        `;
        headerTitle.dataset.processed = true;
      }
    ]]]


best, Michael

1 Like

Hi,

I have an issue with padding value of the grid header and I don´t find where I can change it without installing car-mod. For faster page visivility on older tablets I would like to use it without card-mod:

1h

The green marked padding are should be smaller.

Thanks,
Michael

I think there is no other way to interfere with css without card-mod. Because this is a default lovelace grid card from HA, not custom

thanks for your answer. that’s a pity. Is there any chance to change the source code of lovelace or whereever these .card-header element comes from. Even under the risk that I need to do it again after any update.

Does anyone have an idea how to scale the sidebar relative to the right side? Now the sidebar is too big and that is at the expense of the rest of the display.

hello i want to install @VietNgoc dashboard but i getting this problem please help me thanks you

do you mind sharing the code for glow effect on your dishwasher button?
thanks.

Hi @arifroni I have it set for hover on button, but you can use it normally as box-shadow inset style…

1 Like

If anyone is interested. I’m trying to edit the person button by adding a visualization of the charge of the person’s iPhone in the top right circle. So using the percentage charge as it’s done for the lights but without showing the percentage, so just a visual cue for each person.
I’ve tried many things but nothing seems to stick.

In the person template, I’ve added two variables:

  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: >
        [[[
          return entity.attributes.battery_level || 0;
        ]]]
      charging: >
        [[[
          return entity.attributes.charging || false;
        ]]]

Which are defined as attributes in my created combined.person entity.

And this is my circle template:

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: 1.5
      - --c-stroke-width-dragging: 4
      - --c-font-color: '#97989c'
      - --c-font-size: 17px
      - --c-unit-font-size: 10.5px
      - --c-font-weight: 700
      - --c-letter-spacing: -0.02rem
    custom_fields:
      circle:
        - display: initial
        - width: 90%
        - 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 = entity.entity_id.split('.')[0],
              state = variables.state_on,
              input = variables.circle_input || ' ',
              unit = variables.circle_input_unit || ' ';

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

          let circle = (state, input, unit, batteryLevel, charging) => {
            let color = batteryLevel > 20 ? '#00ff00' : (batteryLevel > 10 ? 'orange' : 'red');
            let dashoffset = c - (batteryLevel / 100 * c);            
            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}" stroke="gray" stroke-opacity="50%" stroke-width="2" fill="none" />
                ${batteryLevel !== undefined ? `
                  <circle cx="25" cy="25" r="${r}" stroke="${color}" stroke-dashoffset="${dashoffset}" stroke-dasharray="${c}" stroke-linecap="round" stroke-width="2" fill="none" style="transform: rotate(-90deg); transform-origin: 50% 50%;" />
                  ${charging ? `<circle cx="25" cy="25" r="${r}" stroke="blue" stroke-width="2" fill="none" style="opacity: 0.5;" />` : ''}
                ` : ''}
                <text id="circle_value" x="50%" y="54%" font-size="var(--c-font-size)" text-anchor="middle" alignment-baseline="middle" dominant-baseline="middle">${input}${tspan}${unit}</tspan></text>
              </svg>

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

          /* * * * * * * * * * * * * * * * * *
           *                                 *
           *             LIGHT               *
           *                                 *
           * * * * * * * * * * * * * * * * * */

          if (domain === 'light' && state) {

                // 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('light', 'turn_on', {
                                    entity_id: entity.entity_id,
                                    brightness_pct: this.value
                                });
                            }
                        }
                    }
                }, 0);
                return circle(state, input, unit, batteryLevel, charging);
            }


          /* * * * * * * * * * * * * * * * * *
           *                                 *
           *             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 batteryLevel = state_attr(entity.entity_id, 'battery_level') || 0;
            let charging = state_attr(entity.entity_id, 'charging') || false;
            
            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, batteryLevel, charging);
          }

          /* * * * * * * * * * * * * * * * * *
           *                                 *
           *            CLIMATE              *
           *                                 *
           * * * * * * * * * * * * * * * * * */

          else if (domain === 'climate') {
            return circle(state, input, unit, batteryLevel, charging);
          }
          
          /* * * * * * * * * * * * * * * * * *
           *                                 *
           *             PIHOLE              *
           *                                 *
           * * * * * * * * * * * * * * * * * */

          else if (domain === 'pihole') {
            return circle(state, input, unit, batteryLevel, charging);
          }

          /* * * * * * * * * * * * * * * * * *
           *                                 *
           *           KIDSHEATER            *
           *                                 *
           * * * * * * * * * * * * * * * * * */

          else if (domain === 'kidsheater') {
            return circle(state, input, unit, batteryLevel, charging);
          }

          /* * * * * * * * * * * * * * * * * *
           *                                 *
           *             AWAIR               *
           *                                 *
           * * * * * * * * * * * * * * * * * */

          else if (domain === 'awair') {
            return circle(state, input, unit, batteryLevel, charging);
          }

          /* * * * * * * * * * * * * * * * * *
           *                                 *
           *             OTHER               *
           *                                 *
           * * * * * * * * * * * * * * * * * */

          else if (variables.state_on) {
            return circle(state, input, unit, batteryLevel, charging);
          }
        }
      ]]]

I can’t figure out why it’s not working.

On a side note, too bad that this thread has slowed down. It was nice seeing new ideas pop up here and there.

@chezpaul2 All default settings from Matt … You’re probably overcomplicating…

card:
  type: custom:button-card
  entity: person.viet_ngoc
  name: Viet Ngoc
  template:
    - circle
    - person
  variables:
    circle_input: >
      [[[
        let battery = Math.round(states['sensor.viet_ngoc_battery_level'].state);
        if (battery) return battery;
      ]]]
    circle_input_unit: ' '

update: And if you want to get really fancy, create a new template with base and circle.

template:

battery_circle:
  template:
    - base
    - circle
  variables:
    battery: ' '
    circle_unit: ' '
  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;
          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;
                }
              </style>
              <circle cx="25" cy="25" r="${radius}" stroke="${color}" stroke-width="3" fill="none" stroke-linecap="round"/>
              <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>
            </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
    circle_unit: ' %'
2 Likes

or you can add any other item you like over another template, nearly everything is possible:
01

phone:
  styles:
    custom_fields:
      battery_status:
        - position: absolute
        - right: -5%
        - left: 58%
        - top: 39%
        - font-size: calc(var(--button-card-font-size) * 0.25 * var(--card-phone))
        - transition: top 250ms ease-out
      battery_charge:
        - position: absolute
        - right: -5%
        - left: 58%
        - top: 49%
        - font-size: calc(var(--button-card-font-size) * 0.25 * var(--card-phone))
      wifi_status:
        - position: absolute
        - right: -5%
        - left: 58%
        - top: 60%
        - font-size: calc(var(--button-card-font-size) * 0.25 * var(--card-phone))
#      proximity:
#        - position: absolute
#        - right: -5%
#        - left: 58%
#        - top: 67%
#        - font-size: calc(var(--button-card-font-size) * 0.25 * var(--card-phone))
  custom_fields:
    battery_status: |
      [[[
        var isHome = states[variables.person].state === 'home';
        var battery_level = states[variables.phone_level].state;
        var battery_state = states[variables.phone_state].state;
        var color = isHome ? (battery_level < 15 ? '#FF0000' : '#3182b7') : '#808080';
        var blink = (battery_state === 'charging' && isHome) ? 'blink-charging 5s linear infinite' : 'none';
        var battery_icon = 'mdi:battery';
        if (battery_state === 'charging') {
          battery_icon = 'mdi:battery-charging';
        } else if (battery_level <= 10) battery_icon = 'mdi:battery-outline';
        else if (battery_level <= 20) battery_icon = 'mdi:battery-20';
        else if (battery_level <= 30) battery_icon = 'mdi:battery-30';
        else if (battery_level <= 40) battery_icon = 'mdi:battery-40';
        else if (battery_level <= 50) battery_icon = 'mdi:battery-50';
        else if (battery_level <= 60) battery_icon = 'mdi:battery-60';
        else if (battery_level <= 70) battery_icon = 'mdi:battery-70';
        else if (battery_level <= 80) battery_icon = 'mdi:battery-80';
        else if (battery_level <= 90) battery_icon = 'mdi:battery-90';
        else if (battery_level >= 100) battery_icon = 'mdi:battery';
        return `<ha-icon
                  icon="${battery_icon}"
                  style="width:25%; color: ${color}; animation: ${blink};"></ha-icon>
                <span style="color: ${color};">${battery_level}%</span>`;
      ]]]
    wifi_status: |
      [[[
        var isHome = states[variables.person].state === 'home';
        var wifi_connected = states[variables.wifi].state === 'on';
        var wifi_connection_name = wifi_connected ? states[variables.wifi_connection].state : '';
        var wifi_color = isHome ? '#3182b7' : '#808080';
        return wifi_connected ? `<ha-icon icon="mdi:wifi" style="width:25%; color: ${wifi_color};"></ha-icon><span style="margin-left: 5px; color: ${wifi_color};">${wifi_connection_name}</span>` : '';
      ]]]
    battery_charge: |
      [[[
        var battery_charge = states[variables.battery_charge].state;
        var battery_level = states[variables.phone_level].state;
        var charge_icon = 'mdi:battery';
        var charge_status = '';
        var charge_color = '#3182b7'; // Standardfarbe (Hellblau)
        if (battery_level == 100) {
          charge_status = 'Voll';
          charge_color = '#3182b7'; // Blau für 100% Akkustand
        } else if (battery_charge === 'charging') {
          charge_icon = 'mdi:battery-charging';
          charge_status = 'Laden';
          charge_color = '#27C950'; // Grün für Laden
        } else if (battery_charge === 'discharging') {
          charge_icon = 'mdi:battery';
          charge_status = 'Entlädt';
          charge_color = '#FFA500'; // Orange für Entladen
        } else {
          charge_status = battery_charge;
        }
        return `<ha-icon icon="${charge_icon}" style="width:25%; color: ${charge_color};"></ha-icon><span style="margin-left: 5px; color: ${charge_color};">${charge_status}</span>`;
      ]]]
#    proximity: |
#      [[[
#        var isHome = states[variables.person].state === 'home';
#        const distance = parseFloat(states[variables.proximity].state).toFixed(1);
#        var color = isHome ? '#3182b7' : '#808080';
#        return `<ha-icon
#                  icon="mdi:map-marker-distance"
#                  style="width:25%; color: ${color};">
#                </ha-icon> <span style="color: ${color};">${distance} km</span>`
#      ]]]
styles:
  - "@keyframes blink-charging":
      0%, 100% { opacity: 1; }
      50% { opacity: 0; }

                    variables:
                      phone_level: sensor.sh_s41p_battery_level
#                      battery_status: sensor.sh_s41p_battery_level
                      wifi_connection: sensor.sh_s41p_wifi_connection

1 Like