Family Calendar From Old IPad

by fweijers in Living > Life Hacks

23 Views, 0 Favorites, 0 Comments

Family Calendar From Old IPad

FamCalWithiPad4.jpg

Found my old iPad 4 lying around and decided to turn it into a family calendar

Supplies

iPad 4

Google account

Google script

Dakboard account

Introduction: Old Ipad

I found this old iPad 4, it still worked like a charm and some of the apps still launched.

Unfortunately not a single app could be installed from the App store because of the outdated iOS version. Even installed apps could not be updated. This fine tablet actually seemed to be quite useless...

  1. Also, the Calendar app could not retrieve data from a google calendar anymore because of outdated security settings.
  2. Also the Safari app could not display the apple or google calendar anymore.

But: Safari was still capable of displaying some internet pages...

DakBoard and Google Calendars

Since Safari was capable of displaying some internet pages, I tried DAKboard: a kiosk display,

https://dakboard.com/pricing

It worked!

So I signed up for a free plan, and the old iPad displayed this DAKboard calendar nice and well.


My family members all use their google account as their digital calendars.

DAKboard free plan only allows 2 calendars to be displayed ... so I needed a solution for this.

Google Script to Merge Calendars

I created a google account called something like "family" and shared all my family members to that family calendar.

Do this in google calendar for each member: under settings -> Shared with -> add your "family" account.


At this point, the newly created family account is capable to display the agenda of each family member.

But the family calender does not contain all events from all family members calendars, it only displays them. DAKboard was still empty at this point.

I needed to merge all calendars into the family calendar.


To merge all calendars, I use script.google.com

As the family user, create a google script to merge all calendars into one.



Google Script

This is the functionality of my google script, I built it with a little help from chatgpt.

Functional Overview

What this script does:

  1. 🗓 Combines multiple Google Calendars into a single shared “family” calendar.
  2. 🔁 Two-way sync direction: It copies from multiple source calendars and from an ICS feed (this are the garbage collection dates from an open calendar of the municipal waste processing company, when to put the bins in the street to get them emptied :-) ) into one target calendar.
  3. 🧩 Keeps full event details: Title, description, and location are preserved.
  4. 🧹 Automatically removes deleted events: If a source or ICS event is gone, it’s deleted from the target too.
  5. Performance-friendly: Adds a short delay (200 ms) between event creations for API throttling.
  6. Time-limited sync: Only synchronizes events within the next 6 months (configurable).
  7. 📅 ICS feed support: Can import events from public .ics URLs (e.g. garbage collection schedules).
  8. 🧼 De-duplication: Prevents duplicate events in the target calendar.
  9. 📋 Logging: Every sync step is logged in the Apps Script console for troubleshooting.


How to use:

  1. Open Google Apps Script
  2. Create a new project and paste this code.
  3. Update the TARGET_CALENDAR_ID, SOURCE_CALENDARS, and ICS_FEEDS to match your setup.
  4. Authorize the script the first time you run it.
  5. (Optional) Add a time-based trigger (e.g. daily) to automate synchronization.


This is my anonymized script:


/**

* Calendar Sync Script - v9 (Anonymized)

*

* Features:

* - Keeps full titles from source calendars

* - ICS feed: only category name in event title

* - Syncs up to 6 months ahead

* - 200ms throttle between event creations

* - Removes deleted events from both sources and ICS feeds

*/


const TARGET_CALENDAR_ID = "familycalendar@example.com"; // target (merged) calendar

const SOURCE_CALENDARS = [

"person1@example.com",

"person2@example.com",

"person3@example.com",

"person4@example.com",

"calendar@example.com",

];


const ICS_FEEDS = [

{

url: "https://example.com/path/to/icalfeed.ics",

sourceId: "ICS_WASTE_CALENDAR",

},

];


const MAX_MONTHS_AHEAD = 6;

const THROTTLE_MS = 200;


function syncCalendars() {

Logger.log("=== CalendarSync v9: Started ===");


const targetCalendar = CalendarApp.getCalendarById(TARGET_CALENDAR_ID);

if (!targetCalendar) {

Logger.log("❌ Target calendar not found: " + TARGET_CALENDAR_ID);

return;

}


const today = new Date();

today.setHours(0,0,0,0);

const future = new Date(today);

future.setMonth(future.getMonth() + MAX_MONTHS_AHEAD);


const validEventKeys = new Set();


// --- Sync source calendars ---

for (const sourceId of SOURCE_CALENDARS) {

try {

const src = CalendarApp.getCalendarById(sourceId);

if (!src) {

Logger.log(`⚠️ Source calendar not found: ${sourceId}`);

continue;

}

const events = src.getEvents(today, future);

Logger.log(`[CalendarSync] ${sourceId}: ${events.length} events found`);

for (const e of events) {

const key = syncEvent(e, targetCalendar);

if (key) validEventKeys.add(key);

}

} catch (err) {

Logger.log(`⚠️ Error in source calendar ${sourceId}: ${err.message}`);

}

}


// --- Sync ICS feeds ---

for (const feed of ICS_FEEDS) {

try {

const keysFromICS = syncICSFeed(feed.url, targetCalendar);

keysFromICS.forEach(k => validEventKeys.add(k));

} catch (err) {

Logger.log(`[CalendarSync] ⚠️ Error in ICS feed ${feed.url}: ${err.message}`);

}

}


// --- Cleanup target calendar ---

cleanupTargetCalendar(targetCalendar, validEventKeys, today, future);


Logger.log("=== CalendarSync v9: Completed ===");

}


// --- Copy one event from a source calendar to target ---

function syncEvent(sourceEvent, targetCalendar) {

const title = (sourceEvent.getTitle() || "").trim();

if (!title) return null;


const start = sourceEvent.getStartTime();

const end = sourceEvent.getEndTime();

const key = `SRC_${title}_${start.getTime()}`;


const existing = targetCalendar.getEvents(start, end, {search: title});

const duplicate = existing.some(e => e.getTitle() === title && e.getStartTime().getTime() === start.getTime());


if (!duplicate) {

targetCalendar.createEvent(title, start, end, {

description: sourceEvent.getDescription() || "",

location: sourceEvent.getLocation() || ""

});

Logger.log(`[CalendarSync] ➕ New event: ${title}`);

Utilities.sleep(THROTTLE_MS);

}


return key;

}


// --- Import events from an ICS feed ---

function syncICSFeed(url, targetCalendar) {

Logger.log(`[CalendarSync] Processing ICS feed: ${url}`);

const response = UrlFetchApp.fetch(url, { muteHttpExceptions: true });

const icsText = response.getContentText();

const events = parseICSEvents(icsText);


const keys = [];

for (const ev of events) {

if (!ev.title || !ev.start) continue;


const cleanTitle = ev.title.split(" ")[0].trim();

const key = `ICS_${cleanTitle}_${ev.start.getTime()}`;


const existing = targetCalendar.getEvents(ev.start, ev.end, { search: cleanTitle });

const duplicate = existing.some(e => e.getTitle() === cleanTitle && e.getStartTime().getTime() === ev.start.getTime());


if (!duplicate) {

targetCalendar.createEvent(cleanTitle, ev.start, ev.end, { description: "", location: "" });

Logger.log(`[CalendarSync] ➕ ICS event: ${cleanTitle} (${ev.start.toDateString()})`);

Utilities.sleep(THROTTLE_MS);

}


keys.push(key);

}


return keys;

}


// --- Parse .ICS text into event objects ---

function parseICSEvents(icsText) {

const events = [];

const lines = icsText.split(/\r?\n/);

let ev = null;

for (const line of lines) {

if (line === "BEGIN:VEVENT") ev = {};

else if (line === "END:VEVENT") {

if (ev?.title && ev?.start) events.push(ev);

ev = null;

} else if (ev) {

if (line.startsWith("SUMMARY:")) ev.title = line.substring(8).trim();

else if (line.startsWith("DTSTART")) ev.start = parseICSTime(line);

else if (line.startsWith("DTEND")) ev.end = parseICSTime(line);

}

}

return events;

}


// --- Parse an ICS datetime line ---

function parseICSTime(line) {

const m = line.match(/:(\d{8}T\d{6})/);

if (!m) return null;

const s = m[1];

return new Date(

s.substring(0, 4),

parseInt(s.substring(4, 6)) - 1,

s.substring(6, 8),

s.substring(9, 11),

s.substring(11, 13),

s.substring(13, 15)

);

}


// --- Remove obsolete events from target calendar ---

function cleanupTargetCalendar(targetCalendar, validEventKeys, start, end) {

const targetEvents = targetCalendar.getEvents(start, end);

let removed = 0;

for (const ev of targetEvents) {

const idSRC = `SRC_${ev.getTitle()}_${ev.getStartTime().getTime()}`;

const idICS = `ICS_${ev.getTitle()}_${ev.getStartTime().getTime()}`;

if (!validEventKeys.has(idSRC) && !validEventKeys.has(idICS)) {

try {

ev.deleteEvent();

removed++;

Logger.log(`[CalendarSync] 🗑️ Removed: ${ev.getTitle()}`);

Utilities.sleep(THROTTLE_MS);

} catch (err) {

Logger.log(`[CalendarSync] ⚠️ Error removing ${ev.getTitle()}: ${err.message}`);

}

}

}

Logger.log(`[CalendarSync] Cleanup done – ${removed} events removed`);

}

Using the Calendar

The google script is time triggered, you can find this in de sctipt.google environment.

The DAKboard agenda is refreshed twice per 24h, which is enough for this family calendar.

Some tips to display the calendar nicely:


Add DAKboard as a Full-Screen Web App (Home Screen)

This method removes the browser bars and gives a “native app” look.

  1. Open Safari on your iPhone or iPad.
  2. Go to your DAKboard URL, e.g.
  3. https://dakboard.com/app?p=your_dashboard_id
  4. Tap the Share icon (square with an arrow pointing up).
  5. Scroll down and tap “Add to Home Screen.”
  6. Give it a name, e.g. “DAKboard.”
  7. Tap Add.

Now a DAKboard icon appears on your home screen.

When you launch it from there, it will:

  1. Open full screen (no Safari bars).
  2. Stay in that mode even if you lock/unlock the device.

💡 Tip: Prevent screen sleep (optional)

If you want your DAKboard display to stay on permanently (for example on a wall-mounted iPad):

  1. Go to Settings → Display & Brightness → Auto-Lock.
  2. Set it to Never.
  3. Plug in the device so it stays powered.