My dashboard with responsive grid, custom calendar and cat card 🐈

Hi y’all,

after years of tinkering with Home Assistant I’m finally happy enough with my main dashboard to show it off here. I’m gonna focus on the three bits I think are most interesting for other users.

Responsive grid lay-out


(sorry for bad quality gif, had to resize it to be displayed here)

For this I made a fork of the existing custom layout-card that makes it easier to implement a css grid by adding support for grid-areas and the option to define grids for 3 breakpoints. I posted the fork on github, along with an example grid-layout, but I don’t plan on maintaining this fork. I opened a PR for an inclusion of this feature on the original card.

Full explanations are in the grid section of the readme on the Github repo, but this snippet gives you an idea of how easy it is to define a responsive layout this way (this example is not for my dashboard btw):

gridrows: auto auto auto
gridcols: 25% 25% 50%
gridareas: | 
  'card1 card4 card6' 
  'card2 xxxxx card7' 
  'card3 card5 card7' 
gridcols_medium: 50% 50%
gridareas_medium: | 
  'card1 card4' 
  'card2 xxxxx' 
  'card3 card5'
  'card6 card6' 
  'card7 card7' 
gridcols_small: 100%   
gridareas_small: | 
  'card1' 
  'card2'
  'card3'
  'card4'
  'card5'
  'card6' 
  'card7'
cards:
  - type: markdown
    gridarea: card1
    content: >
            # Card 1
  - ...

Cat card

Card for cat statistics :wink:
Inspired by Isabella Alström’s config

cat-button-card

This is a custom button card with 3 counters and a switch.
The litter-box icon changes color dynamically based on the number of visits and starts blinking when you really really should not put off cleaning the damn thing any longer. A long press on the card resets the litter box counter.
The 3 counters are linked to xiaomi aquara door/window sensors that are attached to the flaps of litter-box and cat-door.

card code:

type: custom:button-card
entity: switch.schakelaar_fonteintje
show_name: false
show_state: false
show_icon: false
show_units: false
hold_action:
  action: call-service
  confirmation:
    text: "Reset kattenbakcounter. Ben je zeker?"
  service: counter.reset
  service_data: 
    entity_id: counter.counter_kattenbak
custom_fields:
  pic: '[[[ return `<img src="/local/images/Juul_venster.jpg"/>` ]]]'
  in: >
      [[[
        return `<ha-icon icon="mdi:airplane-landing" style="width: 52px; height: 52px; color: #fafafa;"></ha-icon><div><span>${states['counter.counter_kattenluik_in'].state}</span></div>` 
      ]]]
  out: >
    [[[
      return `<ha-icon icon="mdi:airplane-takeoff" style="width: 52px; height: 52px; color: #fafafa;"></ha-icon><div><span>${states['counter.counter_kattenluik_uit'].state}</span></div>` 
    ]]]
  poop: >
    [[[
      return `<ha-icon icon="mdi:emoticon-poop"           style="width: 52px; height: 52px; color: var(--poopcolor); animation: var(--blink);"></ha-icon><div><span style="color: var(--poopcolor);">${states['counter.counter_kattenbak'].state}</span></div>`
    ]]]
  water: >
    [[[
      let word;
      if (states['switch.schakelaar_fonteintje'].state === "on") {
        word = "Aan";
        return `<img src="/local/images/icons8-fountain-on.png" style="width: 52px; height: 52px; margin-bottom: -4px;"/><div><span>${word}</span></div>`;
      }; 
      if (states['switch.schakelaar_fonteintje'].state === "off"){
        word = "Uit";
        return `<img src="/local/images/icons8-fountain-off-white.png" style="width: 52px; height: 52px; margin-bottom: -4px;"/><div><span>${word}</span></div>`;
      };
    ]]]
  spacer: >
    [[[
      return `<ha-icon icon="mdi:exit-to-app" style="width: 52px; height: 52px; color: rgba(0, 0, 0, 0)"></ha-icon><div>&nbsp;</div>`
    ]]]
styles:
  card:
    - background-color: rgba(0, 0, 0, 0.0)
    - color: rgba(255, 255, 255, 0.82)
    - margin-top: 0
    - font-family: SF UI Text Regular
  grid:
    - grid-template-areas: '"pic in out poop spacer water"' 
    - grid-template-columns: 30% 15% 15% 15% auto min-content
    - grid-template-rows: 1fr
    - width: 100%
  pic:
    - align-self: middle
  custom_fields:
    poop:
      - --poopcolor: '[[[ 
          if (states["counter.counter_kattenbak"].state <= 3) return "#fafafa"; 
          if (states["counter.counter_kattenbak"].state > 3 && states["counter.counter_kattenbak"].state <= 7) return "#fc8210"; 
          if (states["counter.counter_kattenbak"].state > 7) return "#ff0000"; 
        ]]]'
      - --blink: '[[[if (states["counter.counter_kattenbak"].state > 7) return "blink 2.5s infinite"; ]]]'
      - align-self: end
      - justify-self: end
      - text-align: middle
      - font-size: 2em
      - background-color: rgba(0, 0, 0, 0.0)
      - padding-top: .3em
    in:
      - color: "#fafafa"
      - align-self: end
      - justify-self: start
      - background-color: rgba(0, 0, 0, 0.0)
      - font-size: 2em
      - padding-top: .3em
    out:
      - color: "#fafafa"
      - background-color: rgba(0, 0, 0, 0.0)
      - font-size: 2em
      - align-self: end
      - justify-self: start
      - padding-top: .3em
    spacer:
      - background-color: rgba(0, 0, 0, 0.0)
      - align-self: end
      - justify-self: auto
      - font-size: 2em
      - padding-top: .3em
    water:
      - color: "#fafafa"
      - justify-self: end
      - align-self: end
      - --statecolor: '[[[if (states["switch.schakelaar_fonteintje"].state === "on") return "#3674ea"; else return "#fafafa"; ]]]'
      - font-size: 1.75em
      - background-color: rgba(0, 0, 0, 0.0)
      - padding-top: .3em
      - padding-right: 1em
      - font-family: 'SF UI Text Medium'
style: |
  img {
    border-radius: 50%;
    width: 120px;
  }

Custom calendar

This card is a custom button card as well. I really like this card type as it allows you to use html and javascript and write your own custom elements. It can really be made to look like anything you want. In this case the card doesn’t even act like a button.

The look btw was inspired by this codepen, whose code I simplified a bit.

The whole “Us” card is a stack-in card containing custom button cards. This is the code for the person/calendar card:

- type: custom:button-card
  name: Kris
  show_icon: false 
  show_name: false
  custom_fields:
    person: '[[[ return `<img src="/local/images/profielpic-kris.jpg"/>` ]]]'
    calendar: >
      [[[
      let calSnippet = '';
        
      for (let i = 0; i < states["sensor.agenda_kris"].state; i++) {
        if (i > 3) {};
        if (i <= 3 ) {
        let start_month = states["sensor.agenda_kris"].attributes.data[i].start_month;
        let start_day = states["sensor.agenda_kris"].attributes.data[i].start_day;
        let start_time = states["sensor.agenda_kris"].attributes.data[i].start_time;
        let end_month = states["sensor.agenda_kris"].attributes.data[i].end_month;
        let end_day = states["sensor.agenda_kris"].attributes.data[i].end_day;
        let end_time = states["sensor.agenda_kris"].attributes.data[i].end_time;
        let time = start_time;
        if (start_day !== end_day) {time = "meerdaags, tot " + end_day + " " + end_month};
        if (end_day === start_day +1 && start_time === end_time) {time = "ganse dag"};
        let event = states["sensor.agenda_kris"].attributes.data[i].summary;
        let location = states["sensor.agenda_kris"].attributes.data[i].location;
        if (location === "" || location === undefined) {location = "-"}
      
      calSnippet += 
        `<table><tr>
            <td class="date month">${start_month}</td><td class="event"><div class="event-title">${event}</div></td>
          </tr>
          <tr>
            <td class="date day">${start_day}</td><td class="event"><span class="location"><ha-icon class="icon" icon="mdi:map-marker"></ha-icon>${location}</span><span class="time"><ha-icon class="icon" icon="mdi:clock-outline"></ha-icon>${time}</span></td>
          </tr></table>`
          }
      }
      
      let taskSnippet = "";
        for (var i=0; i < states["sensor.grocy_tasks"].state; i++) {
          if (states['sensor.grocy_tasks'].attributes.data[i].user === "Kris")
          {
            let taskName = states['sensor.grocy_tasks'].attributes.data[i].name;
            let taskDate = states['sensor.grocy_tasks'].attributes.data[i].due_date;
            taskSnippet += 
            `<table><tr>
              <td class="date"><img class="task-icon" src="/local/images/icons8-to-do-48.png"/></td><td class="task event-title">${taskName}</td>
            </tr>
            <tr>
              <td class="date"></td><td class="task"><span class="time"><ha-icon class="icon" icon="mdi:clock-outline"></ha-icon>${taskDate}</span></td>
            </tr></table>`;
          }
        }
        
      calSnippet += taskSnippet;
        
      return calSnippet;
      ]]]
  styles:
    card:
      - background-color: rgba(0, 0, 0, 0.0)
      - color: rgba(255, 255, 255, 0.82)
      - margin-top: 1em
    grid:
      - grid-template-areas: '"person" "calendar"' 
      - grid-template-columns: 1fr
      - grid-template-rows: 1fr min-content
    person:
      - align-self: middle
  style: |
    img {
      border-radius: 50%;
      width: 120px;
      margin-bottom: 1em;
    }
    .icon {
      margin-right: 5%;
      text-align: center;
      float: left;
      width: 16px;
    }
    table {
      margin-left: 10px;
      box-sizing: border-box;
      border-spacing: 0;
      margin-bottom: 1.25em;
      width: 100%;
    }
    td {
      white-space: -o-pre-wrap; 
      word-wrap: break-word;
      white-space: pre-wrap; 
      white-space: -moz-pre-wrap; 
      white-space: -pre-wrap; 
    }
    .date {
      border-right: 2px solid #dc4225;
      width: 20%;
      text-align: center;
    }
    .event, .task {
      padding-left: 10px;
      width: 80%;
    }
    .month {
      text-transform: uppercase;
      vertical-align: bottom;
    }
    .day {
      font-size: 1.8em;
      vertical-align: top;
    }
    .event-title {
      color: rgba(255, 255, 255, 0.82);
      margin-top: 0;
      font-size: 1.1em;
      font-weight: 400;
      text-align: left;
      font-family: 'SF UI Text Semibold';
      vertical-align: top;
      word-wrap: break-word;
      overflow-wrap: break-word;
    }
    .time, .location {
      display: block;
      text-align: left;
      font-size: 0.9em;
      padding-top: 5px;
      font-family: SF UI Text Regular;
    }
    .time {
      padding-bottom: 1em;
    }
    .task-icon {
      width: 30px;
      border-radius: 0;
      margin-bottom: 0;
    }

It relies on calendar data provided by a custom sensor. The standard calendar integration and especially the caldav integration weren’t working for me so I made a NodeJS script that queries my caldav-provider and posts the data to the Home Assistant state machine in this format:

state: 3
data:
  - startDate: '2020-09-26T10:00:00.000Z'
    endDate: '2020-09-26T17:00:00.000Z'
    summary: Lunch with friends
    year: '20'
    start_month: sep.
    start_day: 26
    start_time: '12:00'
    end_month: sep.
    end_day: 26
    end_time: '19:00'
  - startDate: '2020-10-03T16:00:00.000Z'
    endDate: '2020-10-03T17:30:00.000Z'
    summary: Game night 
    location: Some street - Some city
    year: '20'
    start_month: okt.
    start_day: 3
    start_time: '18:00'
    end_month: okt.
    end_day: 3
    end_time: '19:30'
  - startDate: '2020-10-09T19:30:00.000Z'
    endDate: '2020-10-11T21:00:00.000Z'
    summary: City tripping to Helsinki
    location: Helsinki, Finland
    year: '20'
    start_month: okt.
    start_day: 9
    start_time: '21:30'
    end_month: okt.
    end_day: 12
    end_time: '23:00'

The state equals the number of future events in the calendar, the data-attribute is populated with the actual event information.

The script is constantly running in the background via my api-consumer addon. The script itself is very custom and only useful if by chance you’re using GMX too, it scrapes the calendar data as their caldav service isn’t really working properly too.

At the end of the calendar events I also added data from Grocy (tasks), this too uses a custom script at the moment, but should/could be refactored as the latest version of the Grocy integration has finally added support for tasks. I still need to include an action too were a (long) press on a task would mark the task as completed.

Hope you find some useful ideas here,
cheers

10 Likes

Hi Kris,

I’ve tried your fork as I was maintaining 2 Lovelace dashboards (one for mobile and one for desktop usages), while the responsiveness did some weird stuff in the orignal layout card. This feature is great as with a little bit of fiddling around I now merged them into a single one! Waiting for this PR to be in the original card. Many thanks!

Cool, glad you like it. I didn’t want to go down that route of having to define multiple dashboards or using state-switch to achieve this, hence the quick hack of the layout-card.

Wat voor stofzuiger heb je? Ik heb een blaupunkt maar krijg hem niet connected.

Hello, you need to add this requirement :

And this requirement need to add this requirement :

:exploding_head: :exploding_head:

Cheers