Person Cards - Show Off Yours

Thank you for sharing this awesome card!

Hello @albummi :slight_smile:

Is it possible to display places ? From maps, like this :

      - zone.work
      - zone.sport
      - zone.home

Thanks :slight_smile:

1 Like

it’s ok for me :slight_smile:

Hi :slight_smile:

How does it works ?

When the person is “not_home” show City name

  • value: not_home
    name: |
    [[[
    return 🗺️ ${states[variables.person].attributes.city};
    ]]]

We have to create every city ?

Thanks

@RodgerDodger would you be able to share the find my phone script and the phone mode switcher? Thank you a tons!

Finally happy enough to show mine.

It’s heavily based on some of the cards in this topic but with some changes.

The profile picture changes based on a calendar so for different holidays we have a different profile picture i.e. Halloween or Christmas etc…
Also when it’s someone’s birthday the profilepicture changes.

Then the icon changes based on location zones.

Against all expection chatgpt was a great help with this but it took some time to get all the errors out of it that it kept creating.

Also it takes the person.name entity and uses that for everything so you don’t have to specify entities and paths everywhere.

EDIT
Adjusted an error that slipped in.

type: custom:button-card
entity: person.johan
aspect_ratio: 1/1
name: Person
show_entity_picture: true
show_name: false
tap_action:
  action: navigate
  navigation_path: "[[[ return `/familie-dashboard/${entity.entity_id.split('.')[1]}`; ]]]"
double_tap_action:
  action: navigate
  navigation_path: /familie-dashboard/kaart
hold_action:
  action: none
styles:
  card:
    - background: none
    - border-width: 0px
    - border-radius: 1%
    - padding: 1%
    - font-size: 10px
    - text-shadow: none
    - text-transform: capitalize
    - justify-self: end
    - align-self: middle
  grid:
    - grid-template-areas: "\"icon status\""
  name:
    - font-size: 15px
    - align-self: middle
    - justify-self: start
    - padding-bottom: 10px
  custom_fields:
    icon:
      - clip-path: circle()
      - width: 90%
      - pointer-events: none
      - display: grid
      - border: 12px solid
      - border-color: "#73C6B6"
      - border-radius: 300px
      - margin: 0
      - justify-self: end
      - opacity: 1
    status:
      - align-self: start
      - justify-self: end
      - margin-top: 5px
      - margin-left: "-90px"
      - width: 80px
custom_fields:
  icon: |
    [[[
      const personEntity = entity.entity_id.split('.')[1];
      const basePath = `/local/profielfoto/${personEntity}`;
      const defaultImage = `${personEntity}.jpg`;
      const d = new Date();
      const month = d.getMonth();
      const day = d.getDate();

      const verjaardag = { month: 3, day: 18 };

      function isVerjaardag() {
        return verjaardag.month === month && verjaardag.day === day;
      }

      let image = defaultImage;

      if (isVerjaardag()) {
        image = 'jarig.jpg';
      } else {
        const feestdag = states['input_text.huidige_feestdag'].state;
        if (feestdag && feestdag !== 'geen_feestdag') {
          let feestdagNaam = feestdag.replace(/\s+/g, '_').toLowerCase();
          image = `${feestdagNaam}.jpg`;
        }
      }

      return `<img src="${basePath}/${image}" width="100%" onerror="this.onerror=null;this.src='${basePath}/${defaultImage}';">`;
    ]]]
  status: |
    [[[
      const personEntity = entity.entity_id;
      const state = states[personEntity].state;
      const icons = {
        'not_home': { icon: 'mdi:home-export-outline', color: '#F4A261' },
        'Something': { icon: 'mdi:home-flood', color: '#F4A261' },
        'home': { icon: 'mdi:home', color: '#4CAF50' },
        'Werk': { icon: 'mdi:store', color: '#A78BFA' },
        'Otherthing': { icon: 'mdi:pool', color: '#A78BFA' },
        'School': { icon: 'mdi:school', color: '#A78BFA' },
        'Sport': { icon: 'mdi:swim', color: '#A78BFA' },
        'unknown': { icon: 'mdi:crosshairs-question', color: '#F87171' }
      };
      const config = icons[state] || { icon: 'mdi:home-heart', color: '#F4A261' };
      return `<ha-icon icon="${config.icon}" style="color: ${config.color};"></ha-icon>`;
    ]]]
2 Likes

It’s posted above, here’s a direct link:

I have constructed my own person card with the help of Google Gemini. This worked reasonably well (with some trial & error). I guess, some of the coding is overly complicated… but, Tbh, I would not have been able to do this fairly complex programming on my own…

What still doesn’t work: zone-specific avatar pictures, on_tap action for the different items (i.e. show a map if I click on the avatar picture). So, any help is appreciated!

type: custom:button-card
variables:
  person_entity: person.mario_schubert
  battery_level_sensor: sensor.mario_smartphone_battery_level
  battery_charging_sensor: binary_sensor.mario_smartphone_is_charging
  activity_sensor: sensor.mario_smartphone_detected_activity
  ringer_mode_sensor: sensor.mario_smartphone_ringer_mode
  travel_time_sensor: sensor.mario_reisezeit_waze
  distance_sensor: sensor.home_entfernung_mario
  steps_sensor: sensor.mario_smartphone_steps
custom_fields:
  avatar_img: |
    [[[
      const personState = states[variables.person_entity];
      let avatarUrl = '/local/pictures/person/mario/avatar.jpg'; // Standard-Avatar

      async function getAvatarUrl() {
        if (personState && personState.attributes && personState.attributes.zone_id) {
          const zoneIdLower = personState.attributes.zone_id.toLowerCase();
          const zoneSpecificAvatar = `/local/pictures/person/mario/${zoneIdLower}.jpg`;

          const checkFileExists = (url) => {
            return fetch(url, { method: 'HEAD', cache: 'no-cache' })
              .then(response => response.ok)
              .catch(() => false);
          };

          const exists = await checkFileExists(zoneSpecificAvatar);
          if (exists) {
            return zoneSpecificAvatar;
          }
        }
        return '/local/pictures/person/mario/avatar.jpg';
      }

      getAvatarUrl().then(url => {
        const imgElement = this.shadowRoot ? this.shadowRoot.querySelector('#avatar-image') : null;
        if (imgElement) {
          imgElement.src = url;
        }
      });

      return `<img id="avatar-image" src="${avatarUrl}" style="width: 90px; height: 90px;
                  border-radius: 12px; object-fit: cover;" />`;
    ]]]
  batterie: |
    [[[
      const level = states[variables.battery_level_sensor]?.state;
      const charging = states[variables.battery_charging_sensor]?.state === 'on';
      let icon = 'mdi:battery-unknown';
      let iconColor = 'var(--primary-text-color)';

      if (level !== undefined) {
        const levelInt = parseInt(level);
        if (charging) {
          if (levelInt >= 90) icon = 'mdi:battery-charging-90';
          else if (levelInt >= 80) icon = 'mdi:battery-charging-80';
          else if (levelInt >= 70) icon = 'mdi:battery-charging-70';
          else if (levelInt >= 60) icon = 'mdi:battery-charging-60';
          else if (levelInt >= 50) icon = 'mdi:battery-charging-50';
          else if (levelInt >= 40) icon = 'mdi:battery-charging-40';
          else if (levelInt >= 30) icon = 'mdi:battery-charging-30';
          else if (levelInt >= 20) icon = 'mdi:battery-charging-20';
          else if (levelInt >= 10) icon = 'mdi:battery-charging-10';
          else icon = 'mdi:battery-charging-outline';
        } else {
          if (levelInt >= 90) icon = 'mdi:battery-90';
          else if (levelInt >= 80) icon = 'mdi:battery-80';
          else if (levelInt >= 70) icon = 'mdi:battery-70';
          else if (levelInt >= 60) icon = 'mdi:battery-60';
          else if (levelInt >= 50) icon = 'mdi:battery-50';
          else if (levelInt >= 40) icon = 'mdi:battery-40';
          else if (levelInt >= 30) icon = 'mdi:battery-30';
          else if (levelInt >= 20) icon = 'mdi:battery-20';
          else if (levelInt >= 10) icon = 'mdi:battery-10';
          else icon = 'mdi:battery-outline';

          if (levelInt >= 90) iconColor = 'darkgreen';
          else if (levelInt >= 30) iconColor = 'orange';
          else if (levelInt < 20) iconColor = 'red';
        }
        return `<span style="display:flex; align-items:center; font-size: 0.9em; background-color: rgba(0, 0, 0, 0.05); padding: 2px 8px; border-radius: 8px; cursor: pointer;">
                      <ha-icon icon="${icon}" style="width: 1.2em; height: 1.2em; margin-right: 5px; color: ${iconColor}; vertical-align: middle;"></ha-icon>${level}%
                    </span>`;
      }
      return '';
    ]]]
  aktivität: |
    [[[
      const act = states[variables.activity_sensor]?.state;
      let icon = 'mdi:human-male';
      let iconColor = 'darkblue';
      let text = act;
      if (act === 'still') {
        icon = 'mdi:meditation';
        iconColor = 'darkblue';
        text = 'unbewegt';
      } else if (act === 'walking') {
        icon = 'mdi:walk';
        iconColor = 'mediumseagreen';
        text = 'gehend';
      } else if (act === 'running') {
        icon = 'mdi:run';
        iconColor = 'darkred';
        text = 'laufend';
      } else if (act === 'in vehicle') {
        icon = 'mdi:car';
        iconColor = 'dimgray';
        text = 'im Auto';
      } else if (act === 'on bicycle') {
        icon = 'mdi:bicycle';
        iconColor = 'darkorange';
        text = 'Rad fahrend';
      }
      return act
        ? `<span style="display:flex; align-items:center; font-size: 0.9em; background-color: rgba(0, 0, 0, 0.05); padding: 2px 8px; border-radius: 8px; cursor: pointer;">
                      <ha-icon icon="${icon}" style="width: 1.2em; height: 1.2em; margin-right: 5px; color: ${iconColor}; vertical-align: middle;"></ha-icon>${text}
                    </span>` : '';
    ]]]
  klingelmodus: |
    [[[
      const ringerState = states[variables.ringer_mode_sensor]?.state;
      let icon = 'mdi:bell';
      let iconColor = 'lightblue';
      let text = '';
      if (ringerState === 'silent') {
        icon = 'mdi:bell-off';
        iconColor = 'lightcoral';
        text = 'leise';
      } else if (ringerState === 'vibrate') {
        icon = 'mdi:vibrate';
        iconColor = 'lightsalmon';
        text = 'vibration';
      } else {
        text = 'laut';
      }
      return ringerState
        ? `<span style="display:flex; align-items:center; font-size: 0.9em; background-color: rgba(0, 0, 0, 0.05); padding: 2px 8px; border-radius: 8px; cursor: pointer;">
                      <ha-icon icon="${icon}" style="width: 1.2em; height: 1.2em; margin-right: 5px; color: ${iconColor}; vertical-align: middle;"></ha-icon>${text}
                    </span>` : '';
    ]]]
  schritte: |
    [[[
      const steps = states[variables.steps_sensor]?.state;
      let icon = 'mdi:foot-print';
      let sicon = 'mdi:progress-question';
      let iconColor = 'mediumpurple';
      const stepCount = steps === undefined ? 0 : steps;
      if (parseInt(stepCount) <= 500) sicon = 'mdi:emoticon-sad-outline';
      else if (parseInt(stepCount) <= 1000) sicon = 'mdi:star-outline';
      else if (parseInt(stepCount) <= 5000) sicon = 'mdi:star-half-full';
      else if (parseInt(stepCount) <= 7000) sicon = 'mdi:star';
      else if (parseInt(stepCount) > 7000) sicon = 'mdi:star-shooting';
      return `<span style="display:flex; align-items:center; font-size: 0.9em; background-color: rgba(0, 0, 0, 0.05); padding: 2px 8px; border-radius: 8px; cursor: pointer;">
                    <ha-icon icon="${icon}" style="width: 1.2em; height: 1.2em; margin-right: 5px; color: ${iconColor}; vertical-align: middle;"></ha-icon> ${stepCount} <ha-icon icon="${sicon}" style="width: 1.2em; height: 1.2em; margin-left: 5px; color: gray; vertical-align: middle;"></ha-icon>
                  </span>`;
    ]]]
  position: |
    [[[
      const personState = states[variables.person_entity];
      let icon = 'mdi:map-marker';
      let iconColor = 'lightblue';
      let travelDistanceInfo = '';
      const awayIcon = 'mdi:home-export-outline';

      if (personState && personState.state) {
        if (personState.state === 'home') {
          const homeZone = states['zone.home'];
          if (homeZone && homeZone.attributes && homeZone.attributes.icon) {
            icon = homeZone.attributes.icon;
          }
        } else if (personState.attributes.zone_id) {
          const zoneState = states[`zone.${personState.attributes.zone_id}`];
          if (zoneState && zoneState.attributes && zoneState.attributes.icon) {
            icon = zoneState.attributes.icon;
          } else {
            icon = awayIcon;
          }
        } else if (personState.state === 'not_home') {
          icon = awayIcon;
          iconColor = 'dimgray';
        } else if (personState.state === 'Arbeit') {
          icon = 'mdi:hospital-building';
          iconColor = 'lightblue';
        } else if (personState.state === 'im Garten') {
          icon = 'mdi:tree';
          iconColor = 'forestgreen';
        }
      }

      const travelTimeValue = states[variables.travel_time_sensor]?.state;
      const distanceValue = states[variables.distance_sensor]?.state;

      let travelTimeIcon = travelTimeValue ? `<ha-icon icon="mdi:car-clock" style="width: 1em; height: 1em; margin-right: 2px; color: dimgray; vertical-align: 0px;"></ha-icon>` : '';
      let distanceIcon = distanceValue ? `<ha-icon icon="mdi:home-import-outline" style="width: 1em; height: 1em; margin-right: 2px; color: dimgray; vertical-align: 0px;"></ha-icon>` : '';

      let travelDistanceText = '';
      if (travelTimeValue && distanceValue) {
        travelDistanceText = `<span style="margin-left:10px; color: dimgray;">(${distanceIcon}${(parseFloat(distanceValue) / 1).toFixed(1)} km &nbsp; | &nbsp; ${travelTimeIcon}${travelTimeValue} min)</span>`;
      } else if (travelTimeValue) {
        travelDistanceText = `<span style="margin-left:10px;color: dimgray;">(${travelTimeIcon}${travelTimeValue} min</span>)`;
      } else if (distanceValue) {
        travelDistanceText = `<span style="margin-left:10px;color: dimgray;">(${distanceIcon}${(parseFloat(distanceValue) / 1).toFixed(1)} km)</span>`;
      }

      let locationText = personState?.state === 'not_home' ? 'unterwegs' : personState?.state || 'unterwegs';
      locationText += travelDistanceText;

      return `<span style="display:flex; align-items:center; font-size: 0.9em; background-color: rgba(0, 0, 0, 0.05); padding: 2px 8px; border-radius: 8px; cursor: pointer;">
                    <ha-icon icon="${icon}" style="width: 1.2em; height: 1.2em; margin-right: 5px; color: ${iconColor}; vertical-align: middle;"></ha-icon>${locationText}
                  </span>`;
    ]]]
  last_changed: |
    [[[
      const lastChanged = states[variables.person_entity]?.last_changed;
      if (lastChanged) {
        const now = new Date();
        const changed = new Date(lastChanged);
        const diff = now.getTime() - changed.getTime();
        const minutes = Math.floor(diff / (1000 * 60));
        const hours = Math.floor(minutes / 60);
        const remainingMinutes = minutes % 60;
        let timeString = '';

        if (hours > 0) {
          timeString = `${hours} Stunde${hours > 1 ? 'n' : ''} und ${remainingMinutes} Minute${remainingMinutes !== 1 ? 'n' : ''}`;
        } else if (minutes > 0) {
          timeString = `${minutes} Minute${minutes !== 1 ? 'n' : ''}`;
        } else {
          timeString = 'gerade eben';
        }

        return `<span style="font-size: 0.8em; color: var(--secondary-text-color);">Letzte Änderung vor <span style="font-weight: bold; color: dimgray;">${timeString}</span>.</span>`;
      }
      return '';
    ]]]
styles:
  card:
    - position: relative
    - height: 140px
    - padding: 12px
    - font-size: 1.2em;
  custom_fields:
    avatar_img:
      - position: absolute
      - top: 10px
      - left: 10px
      - width: 90px
      - height: 90px
      - border-radius: 12px
    batterie:
      - position: absolute
      - top: 15px
      - left: 110px
    aktivität:
      - position: absolute
      - top: 45px
      - left: 110px
    klingelmodus:
      - position: absolute
      - top: 15px
      - left: 230px
    schritte:
      - position: absolute
      - top: 45px
      - left: 230px
    position:
      - position: absolute
      - top: 75px
      - left: 110px
    last_changed:
      - font-size: 0.8em;
      - position: absolute
      - bottom: 10px
      - right: 14px
card_mod:
  style: |
    ha-card {
      border-radius: 12px;
      box-shadow: var(--ha-card-box-shadow);
      overflow: visible;
    }
tap_action:
  action: navigate
  navigation_path: /map?entity_id=person.mario_schubert

2 Likes

Mhkay, I have now completely and manually overhauled the card.

It bugged me out that I was not able to define seperate on_tap actions for the various bits of information. As far as I understood, the button-card only allows one such action for the whole button. Therefore, I have divided the one button with all information into vertical and horizontal stacks of custom buttons for each bit of information. The on_tap action opens the details-page of each entity.
However, I don’t like that the custom-data always aligns to the right of the button. I had to shrink the button-container to counter that… but this way, the grid gets destroyed… any Ideas on how to align the button content to the left?

For the daily steps counter, I now make use of the utility-meter integration that calculates the daily steps and resets the counter every night. Because my smartphone only counts the steps since it’s last reboot:

I added this to my configuration.yaml:

utility_meter:
  mario_daily_steps:
    source: sensor.mario_smartphone_steps_sensor
    cycle: daily

Also, I have removed some AI-generated gibberish code regarding zone-specific Avatars and the automatic fetching of the zone-icon… these parts unfortunately still don’t work… any ideas??

type: custom:vertical-stack-in-card
cards:
  - type: horizontal-stack
    cards:
      - type: custom:button-card
        name: ""
        custom_fields:
          avatar: |
            [[[ 
              const personState = states["person.mario_schubert"];
              let zoneId = personState.state.toLowerCase();
              let StandardAvatar = personState.attributes.entity_picture;
              const avatarUrl = zoneId 
                ? `/local/pictures/person/mario/${zoneId}.jpg` 
                : StandardAvatar;
              return `<img src="${avatarUrl}" onerror="this.onerror=null;this.src='${StandardAvatar}'" style="width:90px; height:90px; border-radius:12px; object-fit:cover;">`;
            ]]]
        styles:
          card:
            - width: 90px
            - height: 90px
            - background: transparent
            - box-shadow: none
            - border: none
        tap_action:
          action: more-info
          entity: person.mario_schubert
        card_mod:
          style: |
            ha-card {
              border-radius: 5px;
              background: transparent;
              box-shadow: none;
              border: none;
            }
      - type: vertical-stack
        cards:
          - type: horizontal-stack
            cards:
              - type: custom:button-card
                name: ""
                custom_fields:
                  battery: |
                    [[[ 
                      const level = states["sensor.mario_smartphone_battery_level"]?.state;
                      const charging = states["binary_sensor.mario_smartphone_is_charging"]?.state === 'on';
                      let icon = 'mdi:battery-unknown';
                      let iconColor = 'var(--primary-text-color)';
                      if (level !== undefined) {
                        const levelInt = parseInt(level);
                        if (charging) {
                          if (levelInt >= 90) icon = 'mdi:battery-charging-90';
                          else if (levelInt >= 80) icon = 'mdi:battery-charging-80';
                          else if (levelInt >= 70) icon = 'mdi:battery-charging-70';
                          else if (levelInt >= 60) icon = 'mdi:battery-charging-60';
                          else if (levelInt >= 50) icon = 'mdi:battery-charging-50';
                          else if (levelInt >= 40) icon = 'mdi:battery-charging-40';
                          else if (levelInt >= 30) icon = 'mdi:battery-charging-30';
                          else if (levelInt >= 20) icon = 'mdi:battery-charging-20';
                          else if (levelInt >= 10) icon = 'mdi:battery-charging-10';
                          else icon = 'mdi:battery-charging-outline';
                        } else {
                          if (levelInt >= 90) icon = 'mdi:battery-90';
                          else if (levelInt >= 80) icon = 'mdi:battery-80';
                          else if (levelInt >= 70) icon = 'mdi:battery-70';
                          else if (levelInt >= 60) icon = 'mdi:battery-60';
                          else if (levelInt >= 50) icon = 'mdi:battery-50';
                          else if (levelInt >= 40) icon = 'mdi:battery-40';
                          else if (levelInt >= 30) icon = 'mdi:battery-30';
                          else if (levelInt >= 20) icon = 'mdi:battery-20';
                          else if (levelInt >= 10) icon = 'mdi:battery-10';
                          else icon = 'mdi:battery-outline';
                          if (levelInt >= 90) iconColor = 'darkgreen';
                          else if (levelInt >= 40) iconColor = 'green';
                          else if (levelInt >= 20) iconColor = 'orange';
                          else if (levelInt < 20) iconColor = 'red';
                        }
                        return `<span style="font-size:0.85em;"><ha-icon icon="${icon}" style="width:1.3em; height:1.3em; color:${iconColor};"></ha-icon> ${level}%</span>`;
                      }
                      return '';
                    ]]]
                tap_action:
                  action: more-info
                  entity: sensor.mario_smartphone_battery_level
                styles:
                  card:
                    - box-shadow: none
                    - background: rgba(0,0,0,0.05)
                    - padding: 0px 6px 3px
                    - border: none
                    - width: auto
              - type: custom:button-card
                name: ""
                custom_fields:
                  ringer: |
                    [[[ 
                      const ringerState = states["sensor.mario_smartphone_ringer_mode"]?.state;
                      let icon = 'mdi:bell';
                      let iconColor = 'lightblue';
                      let text = '';
                      if (ringerState === 'silent') {
                        icon = 'mdi:bell-off';
                        iconColor = 'lightcoral';
                        text = 'Leise';
                      } else if (ringerState === 'vibrate') {
                        icon = 'mdi:vibrate';
                        iconColor = 'lightsalmon';
                        text = 'Vibration';
                      } else { text = 'Laut'; }
                      return `<span style="font-size:0.85em;"><ha-icon icon="${icon}" style="width:1.3em; height:1.3em; color:${iconColor};"></ha-icon> ${text}</span>`;
                    ]]]
                tap_action:
                  action: more-info
                  entity: sensor.mario_smartphone_ringer_mode
                styles:
                  card:
                    - background: rgba(0,0,0,0.05)
                    - box-shadow: none
                    - border: none
                    - padding: 0px 6px 3px
                    - width: auto
          - type: horizontal-stack
            cards:
              - type: custom:button-card
                name: ""
                custom_fields:
                  activity: |
                    [[[ 
                      const act = states["sensor.mario_smartphone_detected_activity"]?.state;
                      let icon = 'mdi:human-male';
                      let iconColor = 'darkblue';
                      let text = act;
                      if (act === 'still') {
                        icon = 'mdi:meditation';
                        text = 'unbewegt';
                      } else if (act === 'walking') {
                        icon = 'mdi:walk';
                        iconColor = 'mediumseagreen';
                        text = 'gehend';
                      } else if (act === 'running') {
                        icon = 'mdi:run';
                        iconColor = 'darkred';
                        text = 'laufend';
                      } else if (act === 'in_vehicle') {
                        icon = 'mdi:car';
                        iconColor = 'dimgray';
                        text = 'im Auto';
                      } else if (act === 'on_bicycle') {
                        icon = 'mdi:bicycle';
                        iconColor = 'darkorange';
                        text = 'Rad fahrend';
                      }
                      return `<span style="font-size:0.85em;"><ha-icon icon="${icon}" style="width:1.3em; height:1.3em; color:${iconColor};"></ha-icon> ${text}</span>`;
                    ]]]
                tap_action:
                  action: more-info
                  entity: sensor.mario_smartphone_detected_activity
                styles:
                  card:
                    - background: rgba(0,0,0,0.05)
                    - box-shadow: none
                    - border: none
                    - padding: 0px 6px 3px
                    - text-align: left
                    - width: auto
              - type: custom:button-card
                name: ""
                custom_fields:
                  steps: |
                    [[[ 
                      const steps = states["sensor.mario_daily_steps"]?.state;
                      let icon = 'mdi:foot-print';
                      let sicon = 'mdi:progress-question';
                      let iconColor = 'mediumpurple';
                      const stepCount = steps === undefined ? 0 : steps;
                      if (parseInt(stepCount) <= 500) sicon = 'mdi:emoticon-sad-outline';
                      else if (parseInt(stepCount) <= 1000) sicon = 'mdi:star-outline';
                      else if (parseInt(stepCount) <= 5000) sicon = 'mdi:star-half-full';
                      else if (parseInt(stepCount) <= 7000) sicon = 'mdi:star';
                      else if (parseInt(stepCount) > 7000) sicon = 'mdi:star-shooting';
                      return `<span style="font-size:0.85em;"><ha-icon icon="${icon}" style="width:1.3em; height:1.3em; color:${iconColor};"></ha-icon> ${stepCount} <ha-icon icon="${sicon}" style="width:1.3em; height:1.3em; color:gray;"></ha-icon></span>`;
                    ]]]
                tap_action:
                  action: more-info
                  entity: sensor.mario_daily_steps
                styles:
                  card:
                    - background: rgba(0,0,0,0.05)
                    - box-shadow: none
                    - border: none
                    - padding: 0px 6px 3px
                    - width: auto
          - type: custom:button-card
            name: ""
            custom_fields:
              localization: |
                [[[ 
                  const personState = states["person.mario_schubert"];
                  let zoneIcon = 'mdi:map-marker';
                  let zoneIconColor = 'lightblue';
                  let zoneName = personState.state;
                  const rawZoneId = personState.state.toLowerCase();
                  const zone = states["zone.${rawZoneId}"];
                    if (zone?.icon) {
                      zoneIcon = zone.icon || zoneIcon;
                      zoneName = zone.friendly_name || personState.state;
                    }
                  if (personState.state === 'not_home') {
                    zoneIcon = 'mdi:home-export-outline';
                    zoneIconColor = 'dimgray';
                    zoneName = 'unterwegs';
                  }
                  const travelTimeValue = states["sensor.mario_reisezeit_waze"]?.state;
                  const distanceValue = states["sensor.home_entfernung_mario"]?.state;
                  let travelTimeIcon = travelTimeValue 
                    ? `<ha-icon icon="mdi:car" style="width:1em; height:1em; color:dimgray;"></ha-icon> ` 
                    : '';
                  let distanceIcon = distanceValue 
                    ? `<ha-icon icon="mdi:home-import-outline" style="width:1em; height:1em; color:dimgray;"></ha-icon>` 
                    : '';
                  let travelDistanceText = '';
                  travelDistanceText = ` &nbsp; <span style="font-size:0.8em; color:dimgray;">${distanceIcon}${(parseFloat(distanceValue)).toFixed(1)} km &nbsp; | &nbsp; ${travelTimeIcon}${travelTimeValue} min</span>`;
                  return `<span style="font-size:0.85em;"><ha-icon icon="${zoneIcon}" style="width:1.3em; height:1.3em; color:${zoneIconColor};"></ha-icon> ${zoneName}${travelDistanceText}</span>`;
                ]]]
            tap_action:
              action: more-info
              entity: person.mario_schubert
            styles:
              card:
                - background: rgba(0,0,0,0.05)
                - box-shadow: none
                - border: none
                - padding: 0px 10px 3px 4px
                - width: auto
          - type: custom:button-card
            name: ""
            custom_fields:
              localization: |
                [[[ 
                  const personState = states["person.mario_schubert"];
                  const lastChanged = personState.last_changed;
                  let lastChangedText = '';
                  if (lastChanged) {
                    const now = new Date();
                    const changed = new Date(lastChanged);
                    const diff = now.getTime() - changed.getTime();
                    const minutes = Math.floor(diff / 60000);
                    const hours = Math.floor(minutes / 60);
                    const remainingMinutes = minutes % 60;
                    if (hours > 0) {
                      lastChangedText = `Letzte Änderung <b>vor ${hours} Stunde${hours > 1 ? 'n' : ''}</b> und <b>${remainingMinutes} Minute${remainingMinutes !== 1 ? 'n' : ''}</b>.`;
                    } else if (minutes > 0) {
                      lastChangedText = `Letzte Änderung <b>vor ${minutes} Minute${minutes !== 1 ? 'n' : ''}</b>.`;
                    } else {
                      lastChangedText = `Letzte Änderung <b>gerade eben</b>.`;
                    }
                  }
                  return `<span style="padding-left:30px; font-size:0.75em; color: var(--secondary-text-color); display:block; text-align:left;">${lastChangedText}</span>`;
                ]]]
            tap_action:
              action: more-info
              entity: person.mario_schubert
            styles:
              card:
                - box-shadow: none
                - border: none
                - padding: 0px
                - font-size: 0.9em
card_mod:
  style: |
    ha-card {
      padding:10px;
    }

So, I have found a way to automatically retrieve the icon from the current zone by browsing all the zone-entities and match their friendly-name against the person-state… This is the code:

      const personState = states["person.xxxxxx"];
      let zoneIcon = 'mdi:map-marker';
      let zoneIconColor = 'lightblue';
      let zoneName = personState.state;

    // Durchlaufe alle Entities, die als Zone definiert sind (entity_id beginnt mit "zone.")
    Object.keys(states).forEach(entityId => {
      if (entityId.startsWith("zone.")) {
        const zone = states[entityId];
        if (zone.attributes && zone.attributes.friendly_name === zoneName) {
          zoneIcon = zone.attributes.icon || zoneIcon;
        }
      }
    });

      if (personState.state === 'not_home') {
        zoneIcon = 'mdi:home-export-outline';
        zoneIconColor = 'dimgray';
        zoneName = 'unterwegs';
      }

Embed code is live for Ventusky btw.

Hi Everyone!

I wanted to share the 1,000,000,000th iteration of my custom “Person Card” that I am using.
I am using the HTML-Jinja2-Template custom card because I find it easier to work in CSS/HTML than I do in the custom button card or picture elements card, and it is much more flexible.

The card pulls in a bunch of info from the mobile app integration (location, battery, WiFi, bluetooth, steps, etc.) and is pretty responsive to different display sizes, so it works great on both desktop and mobile, at least for me.

Obviously, you would need to replace all the entities below, both in the entities: and in the HTML code.

Disclaimer: I built this with the assistance of generative AI (ChatGPT and Github CoPilot). There may be unused and/or redundant code and it may not work for you at all.

Screenshots


Requirements:

  • Card: HTML-Jinja2-Template-card
  • Integration: Mobile App
  • Optional-ish (if you tweak the code to remove the parts that use these)
    • Custom stack-in-card – for stacking cards nicely
    • card-mod – for some visual tweaks
    • Google Travel Time Integration for distance from home
    • For the Find Phone button (because I couldn’t figure out to do it with the HTML template card)
      Custom button-card - For the Find Phone button
      Custom restriction-card - if you want to lock the Find Phone button so that it requires 2 clicks, useful on mobile to prevent accidental activations
      – a script that calls some service to locate your device. I use Home Assistant’s Mobile App to unmute / set volume then play a notification sound

Theme

Code

type: custom:stack-in-card
mode: vertical
cards:
  - type: grid
    square: false
    columns: 1
    cards:
      - type: custom:html-template-card
        title: ""
        ignore_line_breaks: true
        entities:
          - person.joel
          - sensor.joels_location
          - binary_sensor.joels_phone_is_charging
          - sensor.joels_phone_battery_level
          - binary_sensor.joels_phone_wifi_state
          - sensor.joels_phone_wifi_connection
          - binary_sensor.joels_phone_bluetooth_state
          - sensor.joels_phone_bluetooth_connection
          - binary_sensor.joels_phone_mobile_data
          - binary_sensor.joels_phone_android_auto
          - sensor.home_joel_distance
          - sensor.joels_phone_do_not_disturb_sensor
          - sensor.joels_phone_next_alarm
          - sensor.joels_phone_steps
          - script.find_joels_phone
        content: |
          <style>
             ha-card {
              padding-bottom: 0px !important;
            }
            .device-card {
              display: grid;
              grid-template-columns: repeat(2, 1fr);
              grid-template-rows: auto auto auto auto auto auto;
              gap: 0.25em 0.25em;
              padding: 0;
              width: 100%;
              background: none;
              background-size: cover;
              font-family: Roboto, sans-serif;
              color: white;
              box-sizing: border-box;
              padding: 0px !important;
            }

            .profile {
              grid-column: 1 / -1;
              display: flex;
              flex-direction: column;
              align-items: center;
              gap: 0.1em;
              margin-bottom: 0;
              min-width: 0;
              cursor: pointer;
            }

            .profile-pic {
              width: 20vw;
              max-width: 100px;
              height: auto;
              aspect-ratio: 1 / 1;
              border-radius: 50%;
              border: solid 4px white; /* fallback */
              box-shadow: 0 0 5px rgba(0,0,0,0.5);
              background-image: url('/local/ha.png');
              background-size: cover;
              background-position: center;
              transition: border-color 0.3s ease;
            }

            .profile-pic[data-location="home"] {
              border-color: #4caf50;
            }
            .profile-pic[data-location="not_home"] {
              border-color: #ff9800;
            }
            .profile-pic[data-location="Work"] {
              border-color: #f44336;
            }

            .name {
              font-weight: bold;
              font-size: clamp(1rem, 2vw, 1.2em);
              text-shadow: 0 0 2px black;
              min-width: 0;
            }

            .location {
              font-size: clamp(0.8rem, 1.5vw, 1em);
              text-shadow: 0 0 2px black;
              min-width: 0;
              text-transform: capitalize;
            }

            .sensor-item {
              display: flex;
              align-items: center;
              gap: 0.25em;
              font-size: 1em;
              background: none;
              padding: 0.2em 0.2em;
              margin: 0;
              border-radius: 6px;
              cursor: pointer;
              transition: background 0.3s;
              text-shadow: 0 0 1px black;
              box-sizing: border-box;
              text-transform: capitalize;
              min-width: 0;
              overflow: hidden;
            }

            .sensor-item ha-icon {
              flex-shrink: 0;
              width: 24px;
              height: 24px;
            }

            .sensor-item span {
              white-space: nowrap;
              overflow: hidden;
              text-overflow: ellipsis;
              min-width: 0;
            }

            .sensor-item:hover {
              background: rgba(255, 255, 255, 0.1);
            }

            ha-icon {
              color: white;
            }

            .sensor-item[data-state="on"] ha-icon {
              color: #4caf50;
            }
            .charging-indicator {
              display: none;
            }
            
            .charging-indicator[data-charging="on"] {
              display: inline;
            }
            .wifi-on {
              display: none;
            }
            .wifi-off {
              display: inline;
            }
            .wifi-on[data-wifi="on"] {
              display: inline;
            }
            .wifi-off[data-wifi="on"] {
              display: none;
            }
            
            .battery-high {
              color: #4caf50 !important;
            }
            
            .battery-medium {
              color: #ff9800 !important;
            }
            
            .battery-low {
              color: #f44336 !important;
            }
            
            .dnd-on {
              color: #f44336 !important;
            }
            
            .alarm-on {
              color: #4caf50 !important;
            }

            .locate-btn {
              grid-column: 1 / -1;
              background: rgba(255, 255, 255, 0.1);
              border: 1px solid rgba(255, 255, 255, 0.3);
              border-radius: 8px;
              padding: 0.5em 1em;
              font-size: 1em;
              font-weight: bold;
              text-align: center;
              cursor: pointer;
              transition: background 0.3s;
              text-shadow: 0 0 2px black;
            }

            .locate-btn:hover {
              background: rgba(255, 255, 255, 0.2);
            }
          </style>


          <div class="device-card">
            <div class="profile" onclick="this.dispatchEvent(new CustomEvent('hass-more-info', {bubbles:true, composed:true, detail:{entityId:'person.joel'}}))" data-location="{{ states('person.joel') }}">
              <div class="profile-pic" data-location="{{ states('person.joel') }}"></div>
              <div class="name">Person 1 - {{ states('person.joel').replace('_', ' ') | title }}</div>
              <div class="location" onclick="event.stopPropagation(); this.dispatchEvent(new CustomEvent('hass-more-info', {bubbles:true, composed:true, detail:{entityId:'sensor.joels_location'}}))">{{ states('sensor.joels_location').replace('_', ' ') }}</div>
            </div>

            <div class="sensor-item" data-state="{{ states('binary_sensor.joels_phone_is_charging') }}">
              {% set battery_level = states('sensor.joels_phone_battery_level') %}
              {% if battery_level not in ['unknown', 'unavailable', 'none'] and battery_level | int >= 80 %}
                <ha-icon icon="mdi:battery" class="battery-high" onclick="this.dispatchEvent(new CustomEvent('hass-more-info', {bubbles:true, composed:true, detail:{entityId:'binary_sensor.joels_phone_is_charging'}}))"></ha-icon>
              {% elif battery_level not in ['unknown', 'unavailable', 'none'] and battery_level | int >= 21 %}
                <ha-icon icon="mdi:battery" class="battery-medium" onclick="this.dispatchEvent(new CustomEvent('hass-more-info', {bubbles:true, composed:true, detail:{entityId:'binary_sensor.joels_phone_is_charging'}}))"></ha-icon>
              {% else %}
                <ha-icon icon="mdi:battery" class="battery-low" onclick="this.dispatchEvent(new CustomEvent('hass-more-info', {bubbles:true, composed:true, detail:{entityId:'binary_sensor.joels_phone_is_charging'}}))"></ha-icon>
              {% endif %}
              <span onclick="this.dispatchEvent(new CustomEvent('hass-more-info', {bubbles:true, composed:true, detail:{entityId:'sensor.joels_phone_battery_level'}}))">
                {{ states('sensor.joels_phone_battery_level') }}%
                <span class="charging-indicator" data-charging="{{ states('binary_sensor.joels_phone_is_charging') }}"> Charging</span>
              </span>
            </div>
            
            <div class="sensor-item" data-state="{{ states('binary_sensor.joels_phone_bluetooth_state') }}">
              <ha-icon icon="mdi:bluetooth" onclick="this.dispatchEvent(new CustomEvent('hass-more-info', {bubbles:true, composed:true, detail:{entityId:'binary_sensor.joels_phone_bluetooth_state'}}))"></ha-icon>
              <span onclick="this.dispatchEvent(new CustomEvent('hass-more-info', {bubbles:true, composed:true, detail:{entityId:'sensor.joels_phone_bluetooth_connection'}}))">
                {% if states('binary_sensor.joels_phone_bluetooth_state') == 'on' %}
                  {% set devices = state_attr('sensor.joels_phone_bluetooth_connection', 'connected_paired_devices') %}
                  {% set connection_count = states('sensor.joels_phone_bluetooth_connection') %}
                  {% if (devices is none or devices | length == 0) or connection_count == '0' %}
                    On
                  {% else %}
                    {{ devices | map('regex_replace', '.*\\((.*)\\)', '\\1') | map('truncate', 8, True) | join(', ') }}
                  {% endif %}
                {% else %}
                  Off
                {% endif %}
              </span>
            </div>
            
            <div class="sensor-item" data-state="{{ states('binary_sensor.joels_phone_wifi_state') }}">
              <ha-icon icon="mdi:wifi" onclick="this.dispatchEvent(new CustomEvent('hass-more-info', {bubbles:true, composed:true, detail:{entityId:'binary_sensor.joels_phone_wifi_state'}}))"></ha-icon>
              <span onclick="this.dispatchEvent(new CustomEvent('hass-more-info', {bubbles:true, composed:true, detail:{entityId:'sensor.joels_phone_wifi_connection'}}))">
                {% if states('binary_sensor.joels_phone_wifi_state') == 'on' %}
                  {% if states('sensor.joels_phone_wifi_connection') == '<not connected>' %}
                    On
                  {% else %}
                    {{ states('sensor.joels_phone_wifi_connection') }}
                  {% endif %}
                {% else %}
                  Off
                {% endif %}
              </span>
            </div>
            <div class="sensor-item" data-state="{{ states('binary_sensor.joels_phone_mobile_data') }}" onclick="this.dispatchEvent(new CustomEvent('hass-more-info', {bubbles:true, composed:true, detail:{entityId:'binary_sensor.joels_phone_mobile_data'}}))">
              <ha-icon icon="mdi:signal"></ha-icon>
              <span>{{ states('binary_sensor.joels_phone_mobile_data') }}</span>
            </div>
            <div class="sensor-item" data-state="{{ states('sensor.joels_phone_next_alarm') }}" onclick="this.dispatchEvent(new CustomEvent('hass-more-info', {bubbles:true, composed:true, detail:{entityId:'sensor.joels_phone_next_alarm'}}))">
              {% if states('sensor.joels_phone_next_alarm') not in ['unavailable', 'unknown', 'none'] %}
                <ha-icon icon="mdi:alarm" class="alarm-on"></ha-icon>
              {% else %}
                <ha-icon icon="mdi:alarm"></ha-icon>
              {% endif %}
              <span>
                {% if states('sensor.joels_phone_next_alarm') not in ['unavailable', 'unknown', 'none'] %}
                  {{ as_timestamp(states('sensor.joels_phone_next_alarm')) | timestamp_custom('%a %-I:%M %p') }}
                {% else %}
                  {{ states('sensor.joels_phone_next_alarm') }}
                {% endif %}
              </span>
            </div>
            <div class="sensor-item" data-state="{{ states('sensor.joels_phone_do_not_disturb_sensor') }}" onclick="this.dispatchEvent(new CustomEvent('hass-more-info', {bubbles:true, composed:true, detail:{entityId:'sensor.joels_phone_do_not_disturb_sensor'}}))">
              {% if states('sensor.joels_phone_do_not_disturb_sensor') == 'on' or states('sensor.joels_phone_do_not_disturb_sensor') == 'priority_only' %}
                <ha-icon icon="mdi:minus-circle" class="dnd-on"></ha-icon>
                <span class="dnd-on">
                  {% if states('sensor.joels_phone_do_not_disturb_sensor') == 'priority_only' %}
                    Priority Only
                  {% else %}
                    {{ states('sensor.joels_phone_do_not_disturb_sensor') }}
                  {% endif %}
                </span>
              {% else %}
                <ha-icon icon="mdi:minus-circle"></ha-icon>
                <span>{{ states('sensor.joels_phone_do_not_disturb_sensor') }}</span>
              {% endif %}
            </div>
            <div class="sensor-item" data-state="{{ states('sensor.home_joel_distance') }}" onclick="this.dispatchEvent(new CustomEvent('hass-more-info', {bubbles:true, composed:true, detail:{entityId:'sensor.home_joel_distance'}}))">
              <ha-icon icon="mdi:map-marker-distance"></ha-icon>
              <span>
                {% if states('sensor.home_joel_distance') not in ['unknown', 'unavailable', 'none'] %}
                  {{ states('sensor.home_joel_distance') | float | round(0) }} Miles From Home
                {% else %}
                  {{ states('sensor.home_joel_distance') }}
                {% endif %}
              </span>
            </div>
            <div class="sensor-item" data-state="{{ states('sensor.joels_phone_steps') }}" onclick="this.dispatchEvent(new CustomEvent('hass-more-info', {bubbles:true, composed:true, detail:{entityId:'sensor.joels_phone_steps'}}))">
              <ha-icon icon="mdi:walk"></ha-icon>
              <span>
                {% if states('sensor.joels_phone_steps') not in ['unknown', 'unavailable', 'none'] %}
                  {{ states('sensor.joels_phone_steps') | float | round(0) }} Steps
                {% else %}
                  {{ states('sensor.joels_phone_steps') }}
                {% endif %}
              </span>
            </div>
          </div>
  - type: grid
    square: false
    columns: 1
    cards:
      - type: custom:restriction-card
        css_variables:
          "--restriction-lock-row-margin-top": "-8px"
        row: true
        restrictions: null
        card:
          type: custom:button-card
          card_mod:
            style: |
              ha-card {
                background: transparent !important;
                box-shadow: none !important;
                padding: 0px 2px 6px 2px !important;
                margin: 0px 2px 0px 2px !important;
              }
          color: auto
          name: Find Phone
          styles:
            card:
              - margin: 0px
              - padding: 0px
          tap_action:
            action: call-service
            service: script.find_joels_phone
            confirmation:
              text: Are you sure you want to locate Person 1's phone?
square: false
columns: 1

3 Likes

Hello, and thank you for this wonderful sharing that I’m trying to create!
Could you please tell me where your avatars come from?

My person card implementation. Thanks for the inspiration

7 Likes

Can you share the code?

Would also love the code too, looks great!

Here is the template to add to the top of your dashboard yaml when using the raw configuration editor. Make sure you have installed the custom-button-card from HACS first. Other integrations are proximity, companion app (for phone sensors) a configured home zone, and spotify. Most information is optional. Add the variable but leave it blank - it should still render without throwing an error.

Template code for dashboard
button_card_templates:
  person_card:
    variables:
      var_person_entity: Person Entity
      var_device_tracker: Location tracking device
      var_phone_device_type: Enter iphone or android
      var_iphone_wifi_ssid: (iPhone only) phone SSID sensor
      var_phone_battery_state: Phone battery state sensor
      var_phone_battery_level: Phone battery level sensor
      var_phone_ringer_mode: (Android Only) Phone ringer mode sensor
      var_phone_ringer_volume: (Android Only) Phone ringer volume level
      var_geocoded_location: Entity to provide exact addresses
      var_spotify: spotify entity. Leave blank if not used
      var_wifi_connection: (Android Only) Phone wifi connection name sensor
      var_step_counter: Step counter entity
      var_home_proximity: Proximity to home distance sensor
      var_activity: Activity detection sensor
      var_travel_direction: Proximity to home travel direction sensor
      var_map_api_key: geoapify.com map api key
    name: null
    show_name: false
    show_state: false
    show_icon: false
    show_entity_picture: false
    entity: '[[[ return variables.var_person_entity ]]]'
    aspect_ratio: 1/1
    triggers_update:
      - '[[[ return variables.var_activity ]]]'
      - '[[[ return variables.var_spotify ]]]'
    tap_action:
      action: none
    styles:
      card:
        - padding: 0% 0% 0% 0%
      custom_fields:
        photo:
          - position: absolute
          - top: 8%
          - left: 4%
          - width: 38%
          - height: 38%
          - border: 7px solid var(--primary-color);
          - border-radius: 50%
        map_area:
          - position: absolute
          - background-color: var(--primary-color)
          - background: |
              [[[
                
                if ( states[variables.var_device_tracker]) {
                  return `url('https://maps.geoapify.com/v1/staticmap?style=osm-bright&width=600&height=230&center=lonlat:${states[variables.var_device_tracker].attributes.longitude},${states[variables.var_device_tracker].attributes.latitude}&zoom=14.0&marker=lonlat:${states[variables.var_device_tracker].attributes.longitude},${states[variables.var_device_tracker].attributes.latitude};type:material;color:%23ff3421;icontype:awesome&scaleFactor=2&apiKey=${variables.var_map_api_key}')`;
                }
              ]]]
          - background-repeat: no-repeat
          - background-size: cover;
          - width: 100%
          - height: 35%
          - top: 0%
          - left: 0%
          - font-size: 8px
        zone:
          - position: absolute
          - justify-self: start
          - font-weight: bold
          - font-size: 100%
          - top: 27%
          - left: 53%
          - width: 60%
          - height: 100%
          - text-align: left
          - color: black
          - font-size: 100%
        geolocation:
          - position: absolute
          - justify-self: start
          - font-size: 80%
          - top: 36%
          - left: 53%
          - width: 50%
          - text-align: left
          - color: var(--primary-text-color)
        travel:
          - position: absolute
          - justify-self: start
          - font-size: 80%
          - top: 58%
          - left: 60%
          - width: 50%
          - text-align: left
          - color: var(--accent-color)
        proximity:
          - position: absolute
          - justify-self: start
          - font-size: 90%
          - top: 53%
          - left: 53%
          - width: 50%
          - text-align: left
          - color: var(--primary-text-color)
        battery:
          - position: absolute
          - justify-self: start
          - font-size: 90%
          - top: 53%
          - left: 10%
          - width: 50%
          - text-align: left
          - color: |
              [[[                 
                if ( states[variables.var_phone_battery_level] )  {
                  if (states[variables.var_phone_battery_level].state <= 15 )
                    return `red`;
                  if (states[variables.var_phone_battery_level].state >= 16 && states[variables.var_phone_battery_level].state < 45)
                    return `orange`;
                  else
                    return `green`;
                }
              ]]]
        ringer:
          - position: absolute
          - justify-self: start
          - font-size: 90%
          - top: 80%
          - left: 10%
          - width: 50%
          - text-align: left
          - color: |
              [[[
                  if ( states[variables.var_phone_ringer_mode] ) {
                    if (states[variables.var_phone_ringer_mode].state == "silent")
                      return `red`;
                    else  
                      return `var(--primary-text-color)`;
                  }
              ]]]
        steps:
          - position: absolute
          - justify-self: start
          - font-size: 90%
          - top: 66%
          - left: 53%
          - width: 50%
          - text-align: left
          - color: var(--primary-text-color)
        wifi:
          - position: absolute
          - justify-self: start
          - font-size: 90%
          - top: 66%
          - left: 10%
          - width: 50%
          - text-align: left
          - color: |
              [[[
                if (variables.var_phone_device_type == 'iphone') {
                  if (states[variables.var_iphone_wifi_ssid]) {
                    if (states[variables.var_iphone_wifi_ssid].state == 'Not Connected')  
                      return `lightgrey`;
                    else 
                      return `var(--primary-text-color)`;
                  }
                } else {
                  if (states[variables.var_wifi_connection]) {
                    if (states[variables.var_wifi_connection].state == '<not connected>')  
                      return `lightgrey`;
                    else 
                      return `var(--primary-text-color)`;
                  }
                }
              ]]]
        media_playing:
          - position: absolute
          - display: flex
          - align-items: center
          - font-size: 80%
          - top: 89%
          - left: 0%
          - width: 95%
          - height: 8%
          - color: |
              [[[ 
                return `var(--primary-text-color)`;
              ]]]
          - visibility: |
              [[[
                if ( (states[variables.var_spotify] )   && ( states[variables.var_spotify].state == "playing" ) )
                  return `visible`;
                else
                  return  `hidden`;
              ]]]
          - background-color: |
              [[[
                  return `var(--primary-color)`;
              ]]]
        media_image:
          - position: absolute
          - justify-self: start
          - top: 82%
          - left: 80%
          - width: 17%
          - text-align: left
          - color: |
              [[[
                return `var(--primary-text-color)`;
              ]]]
          - visibility: |
              [[[
                if ( ( states[variables.var_spotify] )   && (states[variables.var_spotify].state == "playing" ) )
                  return `visible`;
                else
                  return  `hidden`;
              ]]]
        activity:
          - position: absolute
          - font-size: 80%
          - background: white
          - display: flex
          - align-items: center
          - top: 38%
          - left: 34%
          - width: 10%
          - height: 10%
          - text-align: center
          - color: black
          - visibility: |
              [[[
                if ( states[variables.var_activity] ) {
                  let var_allowed = 'in_vehicle automotive on_bicycle cycling running still stationary walking on_foot';
                  let var_activity_state = states[variables.var_activity].state;
                  
                  if ( var_allowed.toLowerCase().includes(var_activity_state.toLowerCase()) )
                    return `visible`;
                  else
                    return `hidden`;
                }
              ]]]
          - border: 4px solid var(--accent-color);
          - border-radius: 50%
    custom_fields:
      map_area: ''
      photo: |
        [[[
          return `<img style="width: 100%;height: 100%; object-fit: contain;" src='${entity.attributes.entity_picture}' >`
        ]]]  
      zone: |
        [[[
          if (`${entity.state}` == 'not_home') { 
          return `<ha-icon icon="mdi:home-export-outline"
            style="width: 10%; height: 10%";>
            </ha-icon><span> Away</span>`;
          } 
          if (`${entity.state}` =='home') { 
          return `<ha-icon 
            icon="mdi:home"
            style="width: 10%; height: 10%;">
            </ha-icon><span> ${entity.state}</span>`;
          } else {
          return `<ha-icon 
            icon="mdi:map-marker-radius"
            style="width: 10%; height: 10%;">
            </ha-icon><span> ${entity.state}</span>`;
          }
        ]]]
      geolocation: |
        [[[
          
          if ( states[variables.var_geocoded_location] ) {
            if ( variables.var_phone_device_type == 'iphone')
              return `
                <span> 
                  ${states[variables.var_geocoded_location].attributes['Sub Thoroughfare']}
                  ${states[variables.var_geocoded_location].attributes.Thoroughfare} <br />
                  ${states[variables.var_geocoded_location].attributes.Locality}, 
                  ${states[variables.var_geocoded_location].attributes['Administrative Area']}
                  ${states[variables.var_geocoded_location].attributes['Postal Code']}
                </span>`;
            else
              return `
                <span> 
                  ${states[variables.var_geocoded_location].attributes.sub_thoroughfare}
                  ${states[variables.var_geocoded_location].attributes.thoroughfare} <br />
                  ${states[variables.var_geocoded_location].attributes.locality}, 
                  ${states[variables.var_geocoded_location].attributes.administrative_area}
                  ${states[variables.var_geocoded_location].attributes.postal_code}
                </span>`;
          }
        ]]]
      travel: |
        [[[
          if ( states[variables.var_travel_direction] && states[variables.var_travel_direction].state == 'towards' ) {
           return `
              <span> 
                Heading Home
              </span>`;
          } else {
            return ` `
          }
           
        ]]]
      battery: |
        [[[
          if (states[variables.var_phone_battery_state]) {
            if (states[variables.var_phone_battery_state].state.toLowerCase() =='charging') { 
              return `<ha-icon icon="mdi:battery-charging" style="width: 12%; height: 12%; ">
              </ha-icon> 
              <span style="color: var(--text-color-sensor);">
               ${states[variables.var_phone_battery_level].state}%
                <span style="font-size: 80%;" >
                  phone charging
                </span>
              </span>`;
            } else {
              if (states[variables.var_phone_battery_level].state < 11) {
                return `<ha-icon icon="mdi:battery-10" style="width: 12%; height: 12%;"> </ha-icon>
                  <span style="color: var(--text-color-sensor);">
                  ${states[variables.var_phone_battery_level].state}%
                    <span style="font-size: 80%;" >
                      phone battery
                    </span>
                  </span>`;}
              if (states[variables.var_phone_battery_level].state >=10 && states[variables.var_phone_battery_level].state < 20)
                return `<ha-icon icon="mdi:battery-20" style="width: 12%; height: 12%;"> </ha-icon>
                  <span style="color: var(--text-color-sensor);">
                  ${states[variables.var_phone_battery_level].state}%
                    <span style="font-size: 80%;" >
                      phone battery
                    </span>
                  </span>`;
              if (states[variables.var_phone_battery_level].state >= 20 && states[variables.var_phone_battery_level].state < 30)
                return `<ha-icon icon="mdi:battery-30" style="width: 12%; height: 12%;"> </ha-icon>
                  <span style="color: var(--text-color-sensor);">
                  ${states[variables.var_phone_battery_level].state}%
                    <span style="font-size: 80%;" >
                      phone battery
                    </span>
                  </span>`;
              if (states[variables.var_phone_battery_level].state >= 30 && states[variables.var_phone_battery_level].state < 40)
                return `<ha-icon icon="mdi:battery-40" style="width: 12%; height: 12%;"> </ha-icon>
                  <span style="color: var(--text-color-sensor);">
                    ${states[variables.var_phone_battery_level].state}%
                    <span style="font-size: 80%;" >
                      phone battery
                    </span>
                  </span>`;
              if (states[variables.var_phone_battery_level].state >= 40 && states[variables.var_phone_battery_level].state < 50)
                return `<ha-icon icon="mdi:battery-50" style="width: 12%; height: 12%;"> </ha-icon>
                  <span style="color: var(--text-color-sensor);">
                  ${states[variables.var_phone_battery_level].state}%
                    <span style="font-size: 80%;" >
                      phone battery
                    </span>
                  </span>`;
              if (states[variables.var_phone_battery_level].state >= 50 && states[variables.var_phone_battery_level].state < 60)
                return `<ha-icon icon="mdi:battery-60" style="width: 12%; height: 12%;"> </ha-icon>
                  <span style="color: var(--text-color-sensor);">
                  ${states[variables.var_phone_battery_level].state}%
                    <span style="font-size: 80%;" >
                      phone battery
                    </span>
                  </span>`;
              if (states[variables.var_phone_battery_level].state >= 60 && states[variables.var_phone_battery_level].state < 70)
                return `<ha-icon icon="mdi:battery-70" style="width: 12%; height: 12%;"> </ha-icon>
                  <span style="color: var(--text-color-sensor);">
                  ${states[variables.var_phone_battery_level].state}%
                    <span style="font-size: 80%;" >
                      phone battery
                    </span>
                  </span>`;
                if (states[variables.var_phone_battery_level].state >= 70 && states[variables.var_phone_battery_level].state < 80)
                return `<ha-icon icon="mdi:battery-80" style="width: 12%; height: 12%;"> </ha-icon>
                  <span style="color: var(--text-color-sensor);">
                  ${states[variables.var_phone_battery_level].state}%
                    <span style="font-size: 80%;" >
                      phone battery
                    </span>
                  </span>`;
                if (states[variables.var_phone_battery_level].state >= 80 && states[variables.var_phone_battery_level].state < 90)
                return `<ha-icon icon="mdi:battery-90" style="width: 12%; height: 12%;"> </ha-icon>
                  <span style="color: var(--text-color-sensor);">
                  ${states[variables.var_phone_battery_level].state}%
                    <span style="font-size: 80%;" >
                      phone battery
                    </span>
                  </span>`;
              if (states[variables.var_phone_battery_level].state >= 90 && states[variables.var_phone_battery_level].state <= 100)
                return `<ha-icon icon="mdi:battery" style="width: 12%; height: 12%;"> </ha-icon>
                  <span style="color: var(--text-color-sensor);">
                  ${states[variables.var_phone_battery_level].state}%
                    <span style="font-size: 80%;" >
                      phone battery
                    </span>
                  </span>`;
            }
          }
        ]]]
      ringer: |
        [[[

          if (variables.var_phone_device_type == 'iphone') {
            return ` `
          } else { 
            if ( states[variables.var_phone_ringer_mode] ) {
              if (states[variables.var_phone_ringer_mode].state == "silent")
                return `<ha-icon
                  icon="mdi:volume-off"
                  style="width: 12%; height: 12%;">
                  </ha-icon>                
                  <span style="font-size: 80%;" >
                    ringer silent
                  </span>`
              if (states[variables.var_phone_ringer_mode].state == "vibrate") 
                return `<ha-icon icon="mdi:volume-vibrate"
                  style="width: 12%; height: 12%;">
                  </ha-icon>
                  <span style="font-size: 80%;" >
                    ringer vibrate
                  </span>`
              else 
                return `<ha-icon
                  icon="mdi:volume-high"
                  style="width: 12%; height: 12%;">
                  </ha-icon>
                  <span> ${states[variables.var_phone_ringer_volume].state}
                    <span style="font-size: 80%;" >
                      ringer level
                    </span>
                  </span>`
            }
          }
            
        ]]]
      steps: |
        [[[
          if ( states[variables.var_step_counter] ) {
            return `<ha-icon
              icon="mdi:walk"
              style="width: 12%; height: 12%;">
              </ha-icon> 
              <span>${Math.round(states[variables.var_step_counter].state).toFixed(0)} <span style="font-size: 80%;" >steps today</span></span>`
          }
        ]]]
      wifi: |
        [[[

          if (variables.var_phone_device_type == 'iphone') {
            if ( states[variables.var_iphone_wifi_ssid] ) {
              if ( states[variables.var_iphone_wifi_ssid].state == 'Not Connected') 
                 return `<ha-icon
                  icon="mdi:wifi-off"
                  style="width: 12%; height: 12%;">
                  </ha-icon>
                    <span style="font-size: 80%;" >Disconnected</span>`;
              else
                 return `<ha-icon
                  icon="mdi:wifi"
                  style="width: 12%; height: 12%;">
                  </ha-icon>
                    <span style="font-size: 80%;" > ${states[variables.var_iphone_wifi_ssid].state} </span>`;
          }
          } else {
            if ( states[variables.var_wifi_connection] ) {
              if ( states[variables.var_wifi_connection].state == '<not connected>' ) 
                return `<ha-icon
                icon="mdi:wifi-off"
                style="width: 12%; height: 12%;">
                </ha-icon>
                <span style="font-size: 80%;" >Disconnected</span>`;
              else 
                return `<ha-icon
                icon="mdi:wifi"
                style="width: 12%; height: 12%;">
                </ha-icon>
                <span style="font-size: 80%;" >${states[variables.var_wifi_connection].state}</span>`;
            }
          }
        ]]]
      media_playing: |
        [[[
            if ( (states[variables.var_spotify])  && (states[variables.var_spotify].state == "playing" )) {
              return `<marquee> <ha-icon
              icon="mdi:music"
              style="width: 20px; height: 20px;"></ha-icon
              <span> ${states[variables.var_spotify].attributes.media_title}
              - ${states[variables.var_spotify].attributes.media_artist} </marquee>`;
            } else {
              return ` `;
            }
        ]]]
      media_image: |
        [[[
            if ((states[variables.var_spotify])   && (states[variables.var_spotify].state == "playing" )) {
              return `<img style="width: 100%;height: 100%; object-fit: contain;" src='${states[variables.var_spotify].attributes.entity_picture}' >`;
            } else {
              return ` `;
            }
        ]]]
      proximity: |
        [[[
          if ( states[variables.var_home_proximity] ) {
          return `<ha-icon
            icon="mdi:map-marker-distance"
            style="width: 12%; height: 12%;">
            </ha-icon> 
            <span> 
               ${Math.round(states[variables.var_home_proximity].state).toFixed(0)}
            <span style="font-size: 80%;" >miles from home</span></span>`
          }
        ]]]
      activity: |
        [[[
          if ( states[variables.var_activity] ) {
            let var_activity_state = states[variables.var_activity].state.toLowerCase();

            if ( var_activity_state == 'in_vehicle' || var_activity_state == 'automotive')
              return `<ha-icon icon="mdi:car" style="width: 70%; height: 70%;"></ha-icon>`;
            else if ( var_activity_state == 'on_bicycle' || var_activity_state == 'cycling')
                return `<ha-icon icon="mdi:bicycle" style="width: 70%; height: 70%;"></ha-icon>`;
            else if ( var_activity_state.toLowerCase() == 'running')
                return `<ha-icon icon="mdi:run-fast" style="width: 70%; height: 70%;"></ha-icon>`;
            else if ( var_activity_state == 'still' || var_activity_state == 'stationary')
                return `<ha-icon icon="mdi:sofa" style="width: 70%; height: 70%;"></ha-icon>`;
            else if ( var_activity_state.toLowerCase() == 'walking' || var_activity_state == 'on_foot') {
                return `<ha-icon icon="mdi:shoe-print" style="width: 70%; height: 70%;"></ha-icon>`;
            } else {      
                return `<ha-icon icon="mdi:map-marker-question-outline" style="width: 70%; height: 70%;"></ha-icon>`;
            }
          }
        ]]]

Here is the yaml to create the card. Replace the variables with your entities that match the required settings. If using an iPhone, make sure to set variable var_device_type: to iphone…

Card Yaml
type: custom:button-card
template: person_card
   var_person_entity: Person Entity
   var_device_tracker: Location tracking device
   var_phone_device_type: Enter iphone or android
   var_iphone_wifi_ssid: (iPhone only) phone SSID sensor
   var_phone_battery_state: Phone battery state sensor
   var_phone_battery_level: Phone battery level sensor
   var_phone_ringer_mode: (Android Only) Phone ringer mode sensor
   var_phone_ringer_volume: (Android Only) Phone ringer volume level
   var_geocoded_location: Entity to provide exact addresses
   var_spotify: spotify entity. Leave blank if not used
   var_wifi_connection: (Android Only) Phone wifi connection name sensor
   var_step_counter: Step counter entity
   var_home_proximity: Proximity to home distance sensor
   var_activity: Activity detection sensor
   var_travel_direction: Proximity to home travel direction sensor
   var_map_api_key: api key from geoapify.Register to first, its free 

Sorry took a while to answer. I was working on one function to show if the person is headed towards home.

Also here is an example card configuration that give my current card

Arcade Bliss Card Config Example
type: custom:button-card
template: person_card
variables:
  var_person_entity: person.arcadebliss
  var_device_tracker: device_tracker.arcadeblisss_talkie_talkie
  var_phone_battery_state: sensor.pixel_8_pro_battery_state
  var_phone_battery_level: sensor.pixel_8_pro_battery_level
  var_phone_ringer_mode: sensor.pixel_8_pro_ringer_mode
  var_phone_ringer_volume: sensor.pixel_8_pro_volume_level_ringer
  var_geocoded_location: sensor.pixel_8_pro_geocoded_location
  var_spotify: media_player.spotify_arcadebliss
  var_wifi_state: binary_sensor.pixel_8_pro_wifi_state
  var_wifi_connection: sensor.pixel_8_pro_wifi_connection
  var_step_counter: sensor.pixel_8_pro_daily_steps
  var_home_proximity: sensor.proximity_home_arcadebliss_distance
  var_activity: sensor.pixel_8_pro_detected_activity
  var_travel_direction: sensor.proximity_home_arcadebliss_direction_of_travel
  var_map_api_key: 1a6a9cbce0fa3fc8ab8537971284caea


4 Likes

Interesting one I found

1 Like

1 Like

+1 for code request