Skip to main content

Calendar & Room Booking Integration for Digital Signage

Integrating digital signage with calendar and room booking systems transforms static displays into intelligent workspace management tools. This comprehensive guide covers everything from basic calendar connections to advanced multi-building room booking networks with real-time occupancy sensing.

Integration Architecture Overview

System Components

Calendar-integrated signage systems consist of several interconnected components:

┌─────────────────────────────────────────────────────────────────────┐
│ Calendar Integration Architecture │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Calendar │ │ Booking │ │ Occupancy │ │
│ │ Systems │ │ Platforms │ │ Sensors │ │
│ │ │ │ │ │ │ │
│ │ • Microsoft │ │ • Robin │ │ • PIR Motion │ │
│ │ • Google │ │ • Envoy │ │ • mmWave │ │
│ │ • Apple │ │ • Teem │ │ • Camera AI │ │
│ │ • Exchange │ │ • OfficeRnD │ │ • Desk Sens. │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ └─────────────┬─────┴─────────────┬─────┘ │
│ ▼ ▼ │
│ ┌─────────────────────────────────┐ │
│ │ Integration Middleware │ │
│ │ (API Gateway / Webhook Server) │ │
│ └─────────────┬───────────────────┘ │
│ ▼ │
│ ┌─────────────────────────────────┐ │
│ │ Digital Signage CMS │ │
│ │ (Content Management System) │ │
│ └─────────────┬───────────────────┘ │
│ ▼ │
│ ┌────────────┬─────────────────┬────────────────┐ │
│ ▼ ▼ ▼ ▼ │
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │Room │ │Room │ │Lobby │ │Wayf- │ │
│ │Panel │ │Panel │ │Dir. │ │inding│ │
│ └──────┘ └──────┘ └──────┘ └──────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘

Data Flow Patterns

Push-Based (Webhooks)

  • Calendar system sends updates immediately when changes occur
  • Lowest latency for display updates
  • Requires webhook endpoint infrastructure
  • Best for: Real-time availability displays

Pull-Based (Polling)

  • Signage system requests updates at regular intervals
  • Simpler implementation, no webhook infrastructure needed
  • Higher latency (typically 1-5 minute delays)
  • Best for: General scheduling displays, lobby directories

Hybrid Approach

  • Webhooks for immediate changes
  • Periodic polling as backup/verification
  • Combines reliability with real-time updates
  • Best for: Mission-critical room booking displays

Microsoft 365 Integration

Azure AD Application Registration

Create an Azure AD application for calendar access:

Step 1: Register Application

  1. Navigate to Azure Portal → Azure Active Directory → App registrations
  2. Click "New registration"
  3. Configure application:
    • Name: "Digital Signage Calendar Integration"
    • Supported account types: "Accounts in this organizational directory only"
    • Redirect URI: (leave blank for daemon apps)

Step 2: Configure API Permissions

{
"requiredPermissions": [
{
"api": "Microsoft Graph",
"permissions": [
{
"name": "Calendars.Read",
"type": "Application",
"description": "Read calendars in all mailboxes"
},
{
"name": "Place.Read.All",
"type": "Application",
"description": "Read room and workspace information"
},
{
"name": "User.Read.All",
"type": "Application",
"description": "Read user profiles"
}
]
}
]
}

Step 3: Generate Client Secret

  1. Go to Certificates & secrets
  2. Click "New client secret"
  3. Set expiration (recommended: 24 months)
  4. Important: Copy secret immediately - it won't be shown again

Microsoft Graph API Implementation

Authentication Flow

// OAuth 2.0 Client Credentials Flow
const getAccessToken = async () => {
const tokenEndpoint = `https://login.microsoftonline.com/${TENANT_ID}/oauth2/v2.0/token`;

const params = new URLSearchParams({
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
scope: 'https://graph.microsoft.com/.default',
grant_type: 'client_credentials'
});

const response = await fetch(tokenEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: params
});

const data = await response.json();
return data.access_token;
};

Fetching Room Calendars

// Get all room resources
const getRooms = async (accessToken) => {
const response = await fetch(
'https://graph.microsoft.com/v1.0/places/microsoft.graph.room',
{
headers: { 'Authorization': `Bearer ${accessToken}` }
}
);
return response.json();
};

// Get room schedule
const getRoomSchedule = async (accessToken, roomEmail, startDate, endDate) => {
const response = await fetch(
'https://graph.microsoft.com/v1.0/users/' + roomEmail + '/calendar/calendarView' +
'?startDateTime=' + startDate.toISOString() +
'&endDateTime=' + endDate.toISOString() +
'&$select=subject,organizer,start,end,showAs,location',
{
headers: { 'Authorization': `Bearer ${accessToken}` }
}
);
return response.json();
};

Webhook Subscriptions for Real-Time Updates

// Create subscription for calendar changes
const createSubscription = async (accessToken, roomEmail, webhookUrl) => {
const subscription = {
changeType: 'created,updated,deleted',
notificationUrl: webhookUrl,
resource: `users/${roomEmail}/events`,
expirationDateTime: new Date(Date.now() + 4230 * 60000).toISOString(), // Max 4230 minutes
clientState: generateSecureToken()
};

const response = await fetch(
'https://graph.microsoft.com/v1.0/subscriptions',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(subscription)
}
);
return response.json();
};

Exchange On-Premises Integration

For organizations using Exchange Server on-premises:

EWS (Exchange Web Services) Configuration

<!-- EWS Impersonation Setup -->
<ManagementScope Name="RoomCalendarScope">
<RecipientRestrictionFilter>
RecipientType -eq 'Room'
</RecipientRestrictionFilter>
</ManagementScope>

<ManagementRoleAssignment
Role="ApplicationImpersonation"
User="SignageServiceAccount"
CustomRecipientWriteScope="RoomCalendarScope"
/>

EWS API Call Example

const EWS = require('node-ews');

const ewsConfig = {
username: 'signage-service@company.com',
password: 'ServiceAccountPassword',
host: 'https://mail.company.com',
auth: 'ntlm'
};

const ews = new EWS(ewsConfig);

// Find appointments
const ewsArgs = {
'attributes': {
'Traversal': 'Shallow'
},
'ParentFolderIds': {
'DistinguishedFolderId': {
'attributes': {
'Id': 'calendar'
},
'Mailbox': {
'EmailAddress': 'conference-room-1@company.com'
}
}
},
'CalendarView': {
'attributes': {
'StartDate': startDate.toISOString(),
'EndDate': endDate.toISOString()
}
}
};

const appointments = await ews.run('FindItem', ewsArgs);

Google Workspace Integration

Google Cloud Console Setup

Step 1: Create Service Account

  1. Go to Google Cloud Console → IAM & Admin → Service Accounts
  2. Create service account: "signage-calendar-reader"
  3. Grant role: "No roles required" (we'll use domain-wide delegation)
  4. Create JSON key and download

Step 2: Enable Domain-Wide Delegation

  1. Edit service account → Enable "Domain-wide Delegation"
  2. Note the Client ID
  3. In Google Admin Console → Security → API Controls → Domain-wide delegation
  4. Add new API client:
    • Client ID: (from service account)
    • OAuth scopes:
      • https://www.googleapis.com/auth/calendar.readonly
      • https://www.googleapis.com/auth/admin.directory.resource.calendar.readonly

Google Calendar API Implementation

Service Account Authentication

const { google } = require('googleapis');

const auth = new google.auth.GoogleAuth({
keyFile: 'service-account-key.json',
scopes: [
'https://www.googleapis.com/auth/calendar.readonly',
'https://www.googleapis.com/auth/admin.directory.resource.calendar.readonly'
],
subject: 'admin@company.com' // Domain admin for impersonation
});

const calendar = google.calendar({ version: 'v3', auth });
const admin = google.admin({ version: 'directory_v1', auth });

Fetching Room Resources

// Get all calendar resources (rooms)
const getCalendarResources = async () => {
const response = await admin.resources.calendars.list({
customer: 'my_customer',
maxResults: 500
});

return response.data.items.filter(
resource => resource.resourceType === 'CONFERENCE_ROOM'
);
};

Getting Room Schedule

const getRoomEvents = async (resourceEmail, timeMin, timeMax) => {
const response = await calendar.events.list({
calendarId: resourceEmail,
timeMin: timeMin.toISOString(),
timeMax: timeMax.toISOString(),
singleEvents: true,
orderBy: 'startTime',
fields: 'items(id,summary,organizer,start,end,status,attendees)'
});

return response.data.items;
};

Push Notifications Setup

// Set up push notifications for calendar changes
const watchCalendar = async (resourceEmail, webhookUrl) => {
const response = await calendar.events.watch({
calendarId: resourceEmail,
requestBody: {
id: generateUUID(),
type: 'web_hook',
address: webhookUrl,
token: generateSecureToken(),
expiration: Date.now() + (7 * 24 * 60 * 60 * 1000) // 7 days max
}
});

return response.data;
};

Room Booking Platform Integrations

Robin Integration

Robin provides dedicated workspace management with rich APIs:

API Authentication

const ROBIN_API_BASE = 'https://api.robinpowered.com/v1.0';

const robinClient = axios.create({
baseURL: ROBIN_API_BASE,
headers: {
'Authorization': `Access-Token ${ROBIN_API_KEY}`
}
});

Fetching Space Availability

// Get all spaces with current status
const getSpaces = async (locationId) => {
const response = await robinClient.get(
`/locations/${locationId}/spaces`,
{
params: {
include: 'current_event,next_event'
}
}
);
return response.data.data;
};

// Get space schedule
const getSpaceSchedule = async (spaceId, date) => {
const response = await robinClient.get(
`/spaces/${spaceId}/events`,
{
params: {
after: `${date}T00:00:00`,
before: `${date}T23:59:59`
}
}
);
return response.data.data;
};

Envoy Rooms Integration

const ENVOY_API_BASE = 'https://api.envoy.com';

// OAuth2 authentication
const getEnvoyToken = async () => {
const response = await fetch(`${ENVOY_API_BASE}/oauth2/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: ENVOY_CLIENT_ID,
client_secret: ENVOY_CLIENT_SECRET,
scope: 'rooms.read'
})
});
return response.json();
};

// Get room reservations
const getReservations = async (accessToken, locationId, date) => {
const response = await fetch(
`${ENVOY_API_BASE}/v1/locations/${locationId}/reservations?date=${date}`,
{
headers: { 'Authorization': `Bearer ${accessToken}` }
}
);
return response.json();
};

Teem (iOFFICE) Integration

const TEEM_API_BASE = 'https://app.teem.com/api/v4';

const teemClient = axios.create({
baseURL: TEEM_API_BASE,
headers: {
'Authorization': `Bearer ${TEEM_ACCESS_TOKEN}`,
'X-Teem-Client-Id': TEEM_CLIENT_ID
}
});

// Get room status
const getRoomStatus = async (roomId) => {
const response = await teemClient.get(`/rooms/${roomId}/status`);
return response.data;
};

// Get room calendar
const getRoomCalendar = async (roomId, startDate, endDate) => {
const response = await teemClient.get(`/rooms/${roomId}/calendar`, {
params: {
start: startDate.toISOString(),
end: endDate.toISOString()
}
});
return response.data;
};

Room Panel Display Design

UI/UX Best Practices

Information Hierarchy

  1. Current Status (largest, most prominent)
    • Available (green) / Occupied (red) / Starting Soon (yellow)
  2. Current/Next Meeting
    • Meeting title, organizer, time
  3. Upcoming Schedule
    • Next 2-3 meetings
  4. Quick Actions
    • Book now, extend meeting, end early

Status Color Coding

StatusColorHex CodeMeaning
AvailableGreen#22C55ERoom is free, can be booked
OccupiedRed#EF4444Meeting in progress
Starting SoonYellow/Amber#F59E0BMeeting starts within 15 min
Ending SoonOrange#F97316Current meeting ending in 5 min
All DayPurple#8B5CF6Reserved for entire day
OfflineGray#6B7280No connectivity/data

Responsive Layout Considerations

/* Room panel display layouts */

/* 10" tablet (landscape) - 1280x800 */
.room-panel-large {
display: grid;
grid-template-rows: 120px 1fr 80px;
grid-template-columns: 1fr 300px;
}

/* 7" tablet (landscape) - 1024x600 */
.room-panel-medium {
display: grid;
grid-template-rows: 100px 1fr 60px;
grid-template-columns: 1fr;
}

/* Status indicator sizing */
.status-indicator {
font-size: clamp(24px, 5vw, 48px);
font-weight: 700;
}

.meeting-title {
font-size: clamp(18px, 3vw, 32px);
line-height: 1.2;
}

.time-display {
font-size: clamp(36px, 8vw, 72px);
font-family: 'Roboto Mono', monospace;
}

Room Panel Data Model

interface RoomPanelData {
room: {
id: string;
name: string;
capacity: number;
amenities: string[];
floor: string;
building: string;
};
status: 'available' | 'occupied' | 'starting_soon' | 'ending_soon' | 'offline';
currentMeeting: Meeting | null;
nextMeeting: Meeting | null;
upcomingMeetings: Meeting[];
availableSlots: TimeSlot[];
lastUpdated: Date;
}

interface Meeting {
id: string;
title: string;
organizer: {
name: string;
email: string;
avatar?: string;
};
startTime: Date;
endTime: Date;
attendeeCount: number;
isPrivate: boolean;
checkedIn: boolean;
}

interface TimeSlot {
start: Date;
end: Date;
duration: number; // minutes
}

Touch Interaction Patterns

Ad-Hoc Booking Flow

┌─────────────────────────────────────────────────────────────────────┐
│ Ad-Hoc Booking Flow │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Display │ │ Duration │ │ Confirm │ │
│ │ Shows │───▶│ Selection │───▶│ Booking │ │
│ │ "Book Now" │ │ │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ Duration Options: │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────────┐ │
│ │15min│ │30min│ │45min│ │60min│ │Until │ │
│ │ │ │ │ │ │ │ │ │next mtg │ │
│ └─────┘ └─────┘ └─────┘ └─────┘ └─────────┘ │
│ │
│ Authentication (optional): │
│ • Badge tap (NFC/RFID) │
│ • QR code scan │
│ • PIN entry │
│ • Anonymous booking allowed │
│ │
└─────────────────────────────────────────────────────────────────────┘

Meeting Extension Flow

// Handle meeting extension request
const extendMeeting = async (meetingId, extensionMinutes) => {
// Check if slot is available
const currentMeeting = await getMeeting(meetingId);
const newEndTime = addMinutes(currentMeeting.endTime, extensionMinutes);

const isSlotAvailable = await checkAvailability(
currentMeeting.roomId,
currentMeeting.endTime,
newEndTime
);

if (!isSlotAvailable) {
return {
success: false,
error: 'Next meeting starts too soon',
maxExtension: calculateMaxExtension(currentMeeting)
};
}

// Extend via calendar API
await updateMeetingEndTime(meetingId, newEndTime);

return { success: true, newEndTime };
};

Lobby Directory Displays

Multi-Room Overview Design

Lobby displays showing multiple room statuses:

┌─────────────────────────────────────────────────────────────────────────────┐
│ Conference Room Availability │
│ Floor 3 - Building A │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ BOARDROOM │ │ MEETING ROOM A │ │ MEETING ROOM B │ │
│ │ Capacity: 20 │ │ Capacity: 8 │ │ Capacity: 8 │ │
│ │ │ │ │ │ │ │
│ │ ████ OCCUPIED ████│ │ ████ AVAILABLE ███│ │ █ STARTING SOON ██│ │
│ │ │ │ │ │ │ │
│ │ Q3 Planning │ │ Free until 2:00pm │ │ Team Standup │ │
│ │ John Smith │ │ │ │ Starts in 12 min │ │
│ │ Until 11:30 AM │ │ [BOOK NOW] │ │ │ │
│ └─────────────────────┘ └─────────────────────┘ └─────────────────────┘ │
│ │
│ ┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ HUDDLE SPACE 1 │ │ HUDDLE SPACE 2 │ │ PHONE BOOTH │ │
│ │ Capacity: 4 │ │ Capacity: 4 │ │ Capacity: 1 │ │
│ │ │ │ │ │ │ │
│ │ ████ AVAILABLE ███│ │ ████ OCCUPIED ████│ │ ████ AVAILABLE ███│ │
│ │ │ │ │ │ │ │
│ │ Free all day │ │ 1:1 Meeting │ │ Free until 3:30pm │ │
│ │ │ │ Until 10:45 AM │ │ │ │
│ │ [BOOK NOW] │ │ │ │ [BOOK NOW] │ │
│ └─────────────────────┘ └─────────────────────┘ └─────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Today's Schedule Summary: 12 meetings | 6 rooms booked | 3 avail │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘

Data Aggregation for Directory Display

// Aggregate multiple room statuses
const buildDirectoryData = async (locationId) => {
const rooms = await getRoomsByLocation(locationId);

const roomStatuses = await Promise.all(
rooms.map(async (room) => {
const events = await getRoomEvents(
room.calendarId,
new Date(),
endOfDay(new Date())
);

const currentEvent = events.find(e =>
isWithinInterval(new Date(), { start: e.start, end: e.end })
);

const nextEvent = events.find(e =>
e.start > new Date()
);

return {
room: {
id: room.id,
name: room.name,
capacity: room.capacity,
floor: room.floor,
amenities: room.amenities
},
status: calculateStatus(currentEvent, nextEvent),
currentEvent: currentEvent ? sanitizeEvent(currentEvent) : null,
nextEvent: nextEvent ? sanitizeEvent(nextEvent) : null,
availableUntil: calculateAvailableUntil(events),
todayEventCount: events.length
};
})
);

return {
location: locationId,
timestamp: new Date(),
rooms: roomStatuses,
summary: {
totalRooms: rooms.length,
available: roomStatuses.filter(r => r.status === 'available').length,
occupied: roomStatuses.filter(r => r.status === 'occupied').length,
totalMeetingsToday: roomStatuses.reduce((sum, r) => sum + r.todayEventCount, 0)
}
};
};

Occupancy Sensing Integration

Sensor Types and Use Cases

Sensor TypeDetection MethodBest ForLimitations
PIR (Passive Infrared)Heat/motionBasic presence detectionCan't count people, false negatives when still
mmWave RadarMicro-movementsAccurate presence, works through glassHigher cost
Camera + AIVisual analysisPeople counting, dwell timePrivacy concerns, higher processing needs
Desk SensorsPressure/heatHot-desking, individual workstationsPer-desk installation required
Badge ReadersNFC/RFIDEntry/exit tracking, authenticationRequires active badge tap
WiFi/BLEDevice proximityApproximate presenceDependent on personal devices

Ghost Meeting Detection

Automatically release unoccupied rooms:

// Ghost meeting detection logic
const GHOST_MEETING_THRESHOLD = 10; // minutes
const SENSOR_CONFIDENCE_THRESHOLD = 0.8;

const checkGhostMeeting = async (roomId) => {
const currentMeeting = await getCurrentMeeting(roomId);
if (!currentMeeting) return null;

const meetingStarted = new Date(currentMeeting.startTime);
const now = new Date();
const minutesSinceStart = differenceInMinutes(now, meetingStarted);

if (minutesSinceStart < GHOST_MEETING_THRESHOLD) {
return null; // Too early to determine
}

// Get occupancy data
const sensorData = await getOccupancyData(roomId, {
start: meetingStarted,
end: now
});

const averageOccupancy = calculateAverageOccupancy(sensorData);

if (averageOccupancy.confidence > SENSOR_CONFIDENCE_THRESHOLD &&
averageOccupancy.occupied === false) {
return {
isGhostMeeting: true,
meetingId: currentMeeting.id,
minutesEmpty: minutesSinceStart,
recommendation: 'release'
};
}

return { isGhostMeeting: false };
};

// Auto-release workflow
const handleGhostMeeting = async (roomId) => {
const ghostCheck = await checkGhostMeeting(roomId);

if (ghostCheck?.isGhostMeeting) {
// Option 1: Send notification to organizer
await notifyOrganizer(ghostCheck.meetingId, {
message: 'Your meeting room appears unoccupied. Tap to confirm attendance or the room will be released.',
timeout: 5 // minutes
});

// Option 2: Auto-release after grace period
setTimeout(async () => {
const recheck = await checkGhostMeeting(roomId);
if (recheck?.isGhostMeeting) {
await releaseMeeting(ghostCheck.meetingId);
await logGhostMeetingRelease(ghostCheck);
}
}, 5 * 60 * 1000);
}
};

Check-In Confirmation

Requiring meeting confirmation to retain room:

// Check-in system
const CHECK_IN_WINDOW = {
before: 10, // minutes before meeting
after: 15 // minutes after start (grace period)
};

const generateCheckInStatus = (meeting) => {
const now = new Date();
const meetingStart = new Date(meeting.startTime);
const windowStart = subMinutes(meetingStart, CHECK_IN_WINDOW.before);
const windowEnd = addMinutes(meetingStart, CHECK_IN_WINDOW.after);

if (meeting.checkedIn) {
return { status: 'confirmed', canCheckIn: false };
}

if (now < windowStart) {
return { status: 'too_early', canCheckIn: false, opensIn: differenceInMinutes(windowStart, now) };
}

if (now > windowEnd) {
return { status: 'missed', canCheckIn: false };
}

return {
status: 'pending',
canCheckIn: true,
expiresIn: differenceInMinutes(windowEnd, now)
};
};

// Check-in methods
const checkInMethods = {
// Panel touch
touchCheckIn: async (meetingId, roomId) => {
return confirmCheckIn(meetingId, 'touch', { roomId });
},

// NFC badge tap
badgeCheckIn: async (meetingId, badgeId) => {
const user = await getUserByBadge(badgeId);
const meeting = await getMeeting(meetingId);

if (!isAttendee(user, meeting)) {
throw new Error('User not on meeting invite');
}

return confirmCheckIn(meetingId, 'badge', { userId: user.id });
},

// QR code scan
qrCheckIn: async (meetingId, qrData) => {
const decoded = verifyQRCode(qrData);
return confirmCheckIn(meetingId, 'qr', { token: decoded.token });
}
};

Wayfinding Integration

Dynamic Meeting Room Directions

Integrate room booking data with wayfinding displays:

// Generate wayfinding data for room
const getRoomWayfindingInfo = async (roomId, fromLocation) => {
const room = await getRoom(roomId);
const roomStatus = await getRoomStatus(roomId);

return {
destination: {
name: room.name,
floor: room.floor,
building: room.building,
coordinates: room.mapCoordinates
},
status: roomStatus.status,
currentMeeting: roomStatus.currentMeeting ? {
title: roomStatus.currentMeeting.title,
endsAt: roomStatus.currentMeeting.endTime
} : null,
directions: await calculateRoute(fromLocation, room.mapCoordinates),
estimatedWalkTime: await calculateWalkTime(fromLocation, room.mapCoordinates),
accessibility: {
wheelchairAccessible: room.wheelchairAccessible,
nearestElevator: await findNearestElevator(room.floor),
accessibleRoute: await calculateAccessibleRoute(fromLocation, room.mapCoordinates)
}
};
};

Visitor Meeting Lookup

// Kiosk lookup for visitor meetings
const lookupVisitorMeeting = async (visitorEmail, date = new Date()) => {
// Search all room calendars for meetings with this attendee
const rooms = await getAllRooms();

const matchingMeetings = [];

for (const room of rooms) {
const events = await getRoomEvents(room.calendarId, startOfDay(date), endOfDay(date));

for (const event of events) {
const isAttendee = event.attendees?.some(
a => a.email.toLowerCase() === visitorEmail.toLowerCase()
);

if (isAttendee) {
matchingMeetings.push({
room: room,
meeting: event,
organizer: event.organizer
});
}
}
}

return matchingMeetings.map(m => ({
meetingTitle: m.meeting.summary,
hostName: m.meeting.organizer.displayName,
hostEmail: m.meeting.organizer.email,
startTime: m.meeting.start,
endTime: m.meeting.end,
roomName: m.room.name,
roomLocation: `${m.room.building}, Floor ${m.room.floor}`,
directions: generateDirectionsFromLobby(m.room)
}));
};

Multi-Tenant and Enterprise Configurations

Building-Wide Deployment

// Enterprise configuration structure
const enterpriseConfig = {
organization: {
id: 'org_123',
name: 'Acme Corporation',
timezone: 'America/New_York',
workingHours: { start: '07:00', end: '20:00' }
},

buildings: [
{
id: 'building_hq',
name: 'Headquarters',
address: '123 Main St',
floors: [
{
id: 'floor_3',
name: 'Floor 3',
rooms: [
{
id: 'room_boardroom',
name: 'Executive Boardroom',
calendarId: 'boardroom@company.com',
capacity: 20,
displayDeviceId: 'panel_001',
amenities: ['video_conf', 'whiteboard', 'phone'],
bookingRules: {
requireApproval: true,
maxDuration: 240,
advanceBookingDays: 30,
allowAdHoc: false
}
}
// ... more rooms
]
}
],
lobbyDisplays: [
{
id: 'lobby_main',
location: 'Main Entrance',
showFloors: ['floor_3', 'floor_4'],
displayMode: 'directory'
}
]
}
],

integrations: {
calendar: {
provider: 'microsoft365',
tenantId: 'xxx',
clientId: 'xxx',
// Credentials stored in secure vault
},
occupancy: {
provider: 'verkada',
apiKey: 'xxx'
},
badgeSystem: {
provider: 'hid_origo',
apiEndpoint: 'https://...'
}
},

policies: {
ghostMeetingDetection: true,
ghostMeetingThreshold: 15,
requireCheckIn: true,
checkInGracePeriod: 10,
anonymousBookingAllowed: false,
defaultMeetingDuration: 30
}
};

Multi-Tenant SaaS Configuration

For signage providers serving multiple organizations:

// Tenant isolation model
const tenantManager = {
// Get tenant-specific configuration
getTenantConfig: async (tenantId) => {
const config = await db.tenantConfigs.findOne({ tenantId });
return {
...config,
calendarCredentials: await secretsVault.get(`${tenantId}/calendar`),
};
},

// Process webhook with tenant context
handleWebhook: async (tenantId, webhookData) => {
const config = await tenantManager.getTenantConfig(tenantId);
const processor = createCalendarProcessor(config);

await processor.handleUpdate(webhookData);
},

// Tenant-scoped data access
getRooms: async (tenantId, locationId) => {
return db.rooms.find({
tenantId,
locationId,
isActive: true
});
}
};

Error Handling and Resilience

Offline Operation

Handle connectivity issues gracefully:

// Offline-capable room panel
const offlineStrategy = {
// Cache schedule data locally
cacheSchedule: async (roomId, schedule) => {
await localDB.put('schedule', {
roomId,
schedule,
cachedAt: new Date(),
expiresAt: addHours(new Date(), 24)
});
},

// Serve from cache when offline
getSchedule: async (roomId) => {
try {
// Try online first
const liveSchedule = await fetchSchedule(roomId);
await offlineStrategy.cacheSchedule(roomId, liveSchedule);
return { data: liveSchedule, source: 'live' };
} catch (error) {
// Fall back to cache
const cached = await localDB.get('schedule', roomId);
if (cached && cached.expiresAt > new Date()) {
return {
data: cached.schedule,
source: 'cache',
cachedAt: cached.cachedAt
};
}
throw new Error('No schedule data available');
}
},

// Queue offline bookings for sync
queueOfflineBooking: async (roomId, booking) => {
await localDB.put('pendingBookings', {
id: generateId(),
roomId,
booking,
createdAt: new Date(),
status: 'pending'
});
},

// Sync when back online
syncPendingBookings: async () => {
const pending = await localDB.getAll('pendingBookings', { status: 'pending' });

for (const item of pending) {
try {
await createBooking(item.roomId, item.booking);
await localDB.update('pendingBookings', item.id, { status: 'synced' });
} catch (error) {
if (error.code === 'CONFLICT') {
await localDB.update('pendingBookings', item.id, {
status: 'failed',
error: 'Time slot no longer available'
});
}
}
}
}
};

API Rate Limiting

// Rate limiter for calendar API calls
const rateLimiter = {
microsoft: new RateLimiter({
maxRequests: 10000,
perInterval: 10 * 60 * 1000, // 10 minutes
retryAfter: (error) => {
const retryHeader = error.response?.headers?.['retry-after'];
return retryHeader ? parseInt(retryHeader) * 1000 : 60000;
}
}),

google: new RateLimiter({
maxRequests: 1000000,
perInterval: 100, // per 100 seconds
retryAfter: (error) => {
const retryHeader = error.response?.headers?.['retry-after'];
return retryHeader ? parseInt(retryHeader) * 1000 : 60000;
}
})
};

// Wrapped API call with rate limiting
const makeCalendarRequest = async (provider, requestFn) => {
const limiter = rateLimiter[provider];

await limiter.waitForSlot();

try {
return await requestFn();
} catch (error) {
if (error.status === 429) {
const waitTime = limiter.retryAfter(error);
await sleep(waitTime);
return makeCalendarRequest(provider, requestFn);
}
throw error;
}
};

Security Considerations

Privacy Controls

// Meeting privacy settings
const privacyLevels = {
PUBLIC: {
showTitle: true,
showOrganizer: true,
showAttendees: true,
showDescription: false
},
PRIVATE: {
showTitle: false,
showOrganizer: true,
showAttendees: false,
showDescription: false,
displayAs: 'Private Meeting'
},
CONFIDENTIAL: {
showTitle: false,
showOrganizer: false,
showAttendees: false,
showDescription: false,
displayAs: 'Reserved'
}
};

// Apply privacy rules to display data
const sanitizeEventForDisplay = (event, displayContext) => {
const sensitivity = event.sensitivity || 'normal';
const privacy = privacyLevels[sensitivity.toUpperCase()] || privacyLevels.PUBLIC;

return {
id: event.id,
title: privacy.showTitle ? event.subject : privacy.displayAs,
organizer: privacy.showOrganizer ? {
name: event.organizer.name,
email: null // Never show email on public display
} : null,
startTime: event.start,
endTime: event.end,
attendeeCount: privacy.showAttendees ? event.attendees?.length : null
};
};

API Credential Management

// Secure credential handling
const credentialManager = {
// Store encrypted credentials
store: async (tenantId, provider, credentials) => {
const encrypted = await encrypt(JSON.stringify(credentials), MASTER_KEY);
await secretsDB.put({
key: `${tenantId}/${provider}`,
value: encrypted,
createdAt: new Date(),
rotateAfter: addMonths(new Date(), 6)
});
},

// Retrieve and decrypt
get: async (tenantId, provider) => {
const record = await secretsDB.get(`${tenantId}/${provider}`);
if (!record) throw new Error('Credentials not found');

const decrypted = await decrypt(record.value, MASTER_KEY);
return JSON.parse(decrypted);
},

// Automatic rotation reminder
checkRotation: async () => {
const expiring = await secretsDB.find({
rotateAfter: { $lt: addDays(new Date(), 30) }
});

for (const record of expiring) {
await sendRotationReminder(record.key);
}
}
};

Frequently Asked Questions


Calendar and room booking integrations enable intelligent workspace management through digital signage. For implementation assistance with specific calendar platforms, contact MediaSignage support or explore our integration templates library.