DIY Family Calendar (Skylight)
Introduction
My wife has been recently bombarded in social media with ads for smart home calendars and was ready to spend over $300 on one. Before giving her green light I asked to for a chance to research them and after comparing various options, I realized that most offered similar functionality but differed significantly in price.
Most importantly, I didnt see any outstanding feature that I couldnt implement somehow in our Home Assistant setup and decided to try to tackle it before giving in to the purchase.
The Plan
I looked at vids and documentation of Skylight, Cozyla and Hearth Display and noted functionality and asked my wife for feedback of the features, which where musts, and which were nice to haves. Also, she preferred the aesthetics and UI of Skylight, which I used as inspiration for this project.
Previously, I experimented with building smart mirrors and wall dashboards. These worked but never got full WAF which is why my wife was so drawn to commercial smart calendars. I had an unused mini PC collecting dust (maybe could even use a Raspberry Pi). The only essential hardware purchase was a touchscreen monitor because that was a non-negotiable feature.
Why DIY Instead of Buying in my case?
Choosing the DIY route with Home Assistant provided several benefits:
- Seamless Smart Home Integration – Works with Home Assistant and other smart devices.
- Repurposing Old Hardware – No need for expensive proprietary devices.
- Lower Cost – I only needed a ~$100 touchscreen monitor.
- No Recurring Fees – Avoids monthly or yearly subscriptions.
- No Vendor Lock-in – No risk of obsolescence if a company shuts down.
- Customization & Expandability – Modify and add features anytime.
- Single App Management – Everything remains within Home Assistant.
- Flexible Placement – Can be moved around the house instead of a fixed wall mount. Including the “must” countertop option.
Features & Requirements
The final DIY smart calendar had the following features:
- Touchscreen interface for easy interaction.
- Countertop-friendly design for accessibility.
- Family-wide and individual event views.
- Event filtering by family member.
- Two-way calendar syncing with external platforms.
- Direct event creation from the touchscreen.
- Aesthetic inspiration from the Skylight calendar.
- Future Expansion Possibilities:
- Task and chore tracking.
- To-do and shopping lists.
- Smart home controls.
- Camera integration.
Hardware Selection
For the touchscreen, I found a 15-inch HP touchscreen on Woot. I considered generic brands ($50-$80) but chose this model due to its:
- Built-in speaker
- Webcam
- Microphone
These extras enable potential upgrades (maybe some are undoable though) such as:
- Barcode scanning for grocery tracking.
- Face recognition and presence detection.
- Voice control.
- General audio output.
BIIIIG DISCLAIMER
This project was a personal DIY build. I am not an expert in Home Assistant, UI/UX design, or hardware development. Expect some hacky solutions, and there’s room for improvement!
How I Built It
The core system relies on Home Assistant helpers, custom cards, and integrations, including:
- Browser Mod
- Week Planner Card
- Bubble Cards
- Grocy (for tasks, products, and lists)
- config-template-card**
Step-by-Step Implementation
1. Setting Up Family Profiles
Each family member was assigned a person entity in Home Assistant, allowing:
- Profile picture assignment.
- Individualized access control.
2. Creating & Syncing Calendars
Each family member received a dedicated Google Calendar:
- Individual calendars for each person.
- A shared “All Family” calendar.
- Birthday and holiday calendars.
These were shared with my wife and me, allowing direct edits from Google Calendar. Integration into Home Assistant used the Google Calendar integration with read/write access, generating entities like:
calendar.familymember1
calendar.familymember2
calendar.birthdays
calendar.holidays_in_united_states
3. Implementing Calendar Filters
Since the Week Planner Card doesn’t support hourly views, I implemented filtering via regular expressions (regex). Each entity could display all events (.*
) or no events (^$
).
For each family member, I created an input_text helper ([person]_calendar_filter
) to dynamically control event visibility.
Then for each calendar, we need to create a script to show/hide events based on regex for a calendar:
alias: [PERSON] Calendar Visible Filter
description: "Shows or hides events for [PERSON] Calendar"
sequence:
- data:
entity_id: input_text.ander_calendar_filter
value: |
{% if is_state('input_text.ander_calendar_filter', '.*') %}
^$
{% else %}
.*
{% endif %}
action: input_text.set_value
4. Calendar Scripts
With the filters created we can focus on scripts for the functionality:
alias: Add Google Calendar Event
description: Create an event based on All Day or Timed selection
sequence:
# Check if it’s an all-day event or not
- choose:
- conditions:
- condition: state
entity_id: input_boolean.calendar_all_day_event
state: "off"
### NOT an all-day event ####
sequence:
- action: google.create_event
target:
entity_id: calendar.{{ states('input_select.calendar_select') | lower }}
data:
summary: "{{ states('input_text.calendar_event_title') }}"
description: "{{ states('input_text.calendar_event_description') }}"
start_date_time: "{{ states('input_datetime.calendar_event_start') }}"
end_date_time: "{{ states('input_datetime.calendar_event_end') }}"
# 3-second delay before closing pop-up
- delay:
hours: 0
minutes: 0
seconds: 3
- action: browser_mod.navigate
data:
path: /dashboard-skylight/calendar
deviceID:
- THIS
# Add a persistent notification that an event has been added (optional)
- action: persistent_notification.create
data:
title: Calendar Entry Added
message: >-
Added Calendar Entry for {{ states('input_select.calendar_select') }}
({{ states('input_text.calendar_event_title') }})
### YES, it IS an all-day event ####
- conditions:
- condition: state
entity_id: input_boolean.calendar_all_day_event
state: "on"
sequence:
- action: google.create_event
target:
entity_id: calendar.{{ states('input_select.calendar_select') | lower }}
data:
summary: "{{ states('input_text.calendar_event_title') }}"
description: "{{ states('input_text.calendar_event_description') }}"
start_date: >
{{ (states('input_datetime.calendar_day_event_start') |
as_datetime).strftime('%Y-%m-%d') }}
end_date: >
{% set end_date = (states('input_datetime.calendar_day_event_end') | as_datetime) %}
{% set end_date = end_date + timedelta(days=1) %}
{{ end_date.strftime('%Y-%m-%d') }}
# Similar delay to close popup and notification
- delay:
hours: 0
minutes: 0
seconds: 3
- action: browser_mod.navigate
data:
path: /dashboard-skylight/calendar
deviceID:
- THIS
- action: persistent_notification.create
data:
title: All Day Calendar Entry Added
message: >-
Added ALL DAY Calendar Entry for {{ states('input_select.calendar_select') }}
({{ states('input_text.calendar_event_title') }})
mode: single
5. Actual Dashboard
Note: I know I took some shortcuts on the layout and grid sizes, so feel free to change as needed for your own situation.
Here is the code used, broken down so you can integrate as you want.
Top Section
As mentioned, the requirement was to show time, weather, and forecast. This is pretty straightforward with just a few modifications on the font type/format/size and card backgrounds.
- type: grid
cards:
- type: custom:better-moment-card
parentStyle: |
line-height:normal;
moment:
- parentStyle: |
font-size:1em; text-align:center; margin-top:5px;
templateRaw: |
{{moment format=cccc}}
- parentStyle: |
font-size:1.5em; text-align:center; margin-top:5px;
templateRaw: |
{{moment format=LLLL dd, yyyy}}
- parentStyle: |
font-size:4em; text-align:center; font-weight:400;
templateRaw: |
{{moment format=HH:mm}}
card_mod:
style: |
ha-card {
background: transparent !important;
box-shadow: none !important;
border: none !important;
}
- type: custom:weather-card
entity: weather.[MY_WEATHER_ENTITY}
current: true
details: true
forecast: false
- type: weather-forecast
show_current: false
show_forecast: true
entity: weather.[MY_WEATHER_ENTITY]
forecast_type: daily
name: Weather Forecast
card_mod:
style: |
ha-card {
background: transparent !important;
box-shadow: none !important;
border: none !important;
}
Calendar Header
To show the name/header of the calendar as well as the buttons to filter calendars, add events, and change the calendar view
- type: vertical-stack
cards:
- type: markdown
content: <font color="Black" size="6">The Skylight Calendar</font>
#Buttons
- type: horizontal-stack
cards:
- type: custom:bubble-card
card_type: button
button_type: state
entity: person.[PERSON]
scrolling_effect: false
show_icon: true
show_name: true
show_state: false
tap_action:
action: perform-action
perform_action: script.[PERSON]_calendar_visible_filter
target: {}
button_action:
tap_action:
action: perform-action
perform_action: script.[PERSON]_calendar_visible_filter
target: {}
styles: |
.bubble-button-background {
opacity: 1 !important;
background-color: ${hass.states['input_text.[PERSON]_calendar_filter'].state === '.*' ? 'light-grey' : '#[HEX_COLOR]'} !important;}
}
#NOTE: change the hex color as needed.
#NOTE 2: REPEAT THIS FOR EVERY PERSON/CALENDAR YOU WANT TO HAVE
#ADD EVENT BUTTON
- type: custom:bubble-card
card_type: button
button_type: name
card_layout: large
name: Add Event
icon: mdi:calendar-plus
tap_action:
action: navigate
navigation_path: "#addcalendarevent"
button_action:
tap_action:
action: navigate
navigation_path: "#addcalendarevent"
styles: |
* {
font-size: 1.05em !important;
}
ha-card {
--bubble-main-background-color: #393745 !important;
width: 300px;
}
.bubble-icon {
--mdc-icon-size: 30px !important;
color: snow !important;
opacity: 1;
}
.bubble-icon-container {
background: #393745 !important;
display: flex;
}
.bubble-name {
color: snow !important;
opacity: 1;
display: flex;
line-height: 18px;
flex-direction: row;
justify-content: center;
flex-grow: 1;
margin: 0 40px 0 0;
pointer-events: none;
position: relative;
overflow: hidden;
}
# SELECT VIEW
- type: custom:bubble-card
card_type: select
entity: input_select.calendar_view
show_name: true
show_state: true
name: Select View
show_last_changed: false
show_attribute: false
Calendar Section
This uses the week-planner-card with some UI mods to make it look like skylight
#Note: Uses config template card to reduce code size
- type: custom:config-template-card
entities:
- input_text.[PERSON]_calendar_filter
…
…
- input_text.birthdays_calendar_filter
- input_select.calendar_view
variables:
PERSONCAL: states['input_text.[PERSON]_calendar_filter']?.state
PERSON2CAL: states['input_text.[PERSON2]_calendar_filter']?.state
…
…
BIRCAL: states['input_text.birthdays_calendar_filter']?.state
#Other variables
VIEW: states['input_select.calendar_view']?.state
# DAYS
DAYS: |
(() => {
const calendarView = states['input_select.calendar_view']?.state;
if (calendarView === 'Today') return 1;
if (calendarView === 'Tomorrow') return 2;
// Check for Month, Biweek, or Week based on screen width
if (calendarView === 'Week') return 7;
if (calendarView === 'Biweek') return 14;
if (calendarView === 'Month') return 28;
if (calendarView === 'Bimonth') return 56;
// Default fallback to 7 if no condition matches
return 7;
})()
#CALENDAR!
card:
type: custom:week-planner-card
calendars:
- entity: calendar.[PERSON]
name: [PERSON}
color: "#HEX_COLOR"
filter: ${ PERSONCAL }
…
#REPEAT FOR EACH PERSON/CALENDAR
…
- entity: calendar.birthdays
name: Birthdays
color: "#OTHER_HEX_COLOR"
filter: ${ BIRCAL }
days: ${ DAYS }
startingDayOffset: 0
hideWeekend: false
noCardBackground: false
compact: false
weather:
showCondition: true
showTemperature: true
showLowTemperature: true
useTwiceDaily: false
entity: weather.[MY_WEATHER_ENTITY]
locale: en
showLocation: true
hidePastEvents: false
hideDaysWithoutEvents: false
hideTodayWithoutEvents: false
combineSimilarEvents: true
showLegend: false
legendToggle: false
texts:
monday: Mon
tuesday: Tue
wednesday: Wed
thursday: Thu
friday: Fri
saturday: Sat
sunday: Sun
yesterday: ""
today: ""
tomorrow: ""
card_mod:
style: |
ha-card {
.event.past {
opacity: .2;
background-color: gray !important;
}
.time {
color: #333333 !important;
font-size: 0.8em !important;
}
.event {
color: #333333 !important;
line-height: 16px !important;
background-color: var(--border-color) !important;
border-radius: 10px !important;
max-height: 80px !important;
overflow: hidden !important;
font-size: 1.1em !important;
}
.none {
background-color: transparent !important;
}
.today .number {
border-radius: 5px;
background-color: orange !important;
padding-left: 4px;
padding-right: 4px;
}
.day .date .text {
font-size: 1em !important;
font-weight: bold !important;
}
.day .date .number {
font-weight: bold !important;
font-size: 3em !important;
}
.day {
--background-color: red;
border: solid 1px whitesmoke;
padding: 0.2%;
width: 13% !important;
}
}
Add Event PopUp
This uses the week-planner-card with some UI mods to make it look like skylight
- type: vertical-stack
cards:
- type: custom:bubble-card
card_type: pop-up
hash: "#addcalendarevent"
button_type: name
name: Add Calendar Event
icon: mdi:calendar-plus
scrolling_effect: false
show_icon: true
show_name: true
styles: |
.bubble-button-card-container {
background:
${hass.states['input_select.calendar_select'].state ==
'PERSON' ? '#HEX_COLOR
: hass.states['input_select.calendar_select'].state ==
'PERSON2' ? '#HEX_COLOR2'
: hass.states['input_select.calendar_select'].state ==
'PERSON3' ? '#HEX_COLOR3'
.. repeat for each…
: hass.states['input_select.calendar_select'].state ==
'Birthdays' ? '#OTHER_HEX_COLOR'
: 'red'} !important;
}
- type: vertical-stack
cards:
- type: entities
entities:
- entity: input_select.calendar_select
- entity: input_text.calendar_event_title
name: Event Title
- entity: input_text.calendar_event_description
name: Event Description
- entity: input_boolean.calendar_all_day_event
name: All Day Event
title: Add Calendar Event
state_color: false
- type: conditional
conditions:
- entity: input_boolean.calendar_all_day_event
state: "off"
card:
type: entities
entities:
- entity: input_datetime.calendar_event_start
name: Start Time
- entity: input_datetime.calendar_event_end
name: End Time
- type: conditional
conditions:
- entity: input_boolean.calendar_all_day_event
state: "on"
card:
type: entities
entities:
- entity: input_datetime.calendar_day_event_start
name: Event Start Date
- entity: input_datetime.calendar_day_event_end
name: Event End Date
- type: custom:button-card
name: Add Event to Calendar
tap_action:
action: call-service
service: script.add_google_calendar_event
styles:
card:
- background-color: |
[[[
if (states['input_select.calendar_select'].state == 'PERSON')
return "#HEX_COLOR";
if (states['input_select.calendar_select'].state == 'PERSON2')
return "#HEX_COLOR2";
… repeat for each calendar….
if (states['input_select.calendar_select'].state == 'Birthdays')
return "#33a02c";
return "gray";
]]]
Next Steps
I’ve also implemented task and chore tracking, but I still need to write up the details. I’ll update this post soon to cover that, along with to-do lists, shopping lists, and more smart home integrations.
I’ve also created a couple dashboards specific for this device to be able to control lights in a floorplan view cctv cameras and weather.
I’ll append these to this post accordingly when I have these writeups ready.
Current Dashboard View
Let me know if you have any questions or suggestions!