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
Inspired by Isabella Alström’s config
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> </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