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
- Microsoft 365 Integration
- Google Workspace Integration
- Room Booking Platform Integrations
- Room Panel Display Design
- Lobby Directory Displays
- Occupancy Sensing Integration
- Wayfinding Integration
- Multi-Tenant and Enterprise Configurations
- Error Handling and Resilience
- Security Considerations
- Frequently Asked Questions
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
- Navigate to Azure Portal → Azure Active Directory → App registrations
- Click "New registration"
- 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
- Go to Certificates & secrets
- Click "New client secret"
- Set expiration (recommended: 24 months)
- 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
- Go to Google Cloud Console → IAM & Admin → Service Accounts
- Create service account: "signage-calendar-reader"
- Grant role: "No roles required" (we'll use domain-wide delegation)
- Create JSON key and download
Step 2: Enable Domain-Wide Delegation
- Edit service account → Enable "Domain-wide Delegation"
- Note the Client ID
- In Google Admin Console → Security → API Controls → Domain-wide delegation
- Add new API client:
- Client ID: (from service account)
- OAuth scopes:
https://www.googleapis.com/auth/calendar.readonlyhttps://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
- Current Status (largest, most prominent)
- Available (green) / Occupied (red) / Starting Soon (yellow)
- Current/Next Meeting
- Meeting title, organizer, time
- Upcoming Schedule
- Next 2-3 meetings
- Quick Actions
- Book now, extend meeting, end early
Status Color Coding
| Status | Color | Hex Code | Meaning |
|---|---|---|---|
| Available | Green | #22C55E | Room is free, can be booked |
| Occupied | Red | #EF4444 | Meeting in progress |
| Starting Soon | Yellow/Amber | #F59E0B | Meeting starts within 15 min |
| Ending Soon | Orange | #F97316 | Current meeting ending in 5 min |
| All Day | Purple | #8B5CF6 | Reserved for entire day |
| Offline | Gray | #6B7280 | No 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 Type | Detection Method | Best For | Limitations |
|---|---|---|---|
| PIR (Passive Infrared) | Heat/motion | Basic presence detection | Can't count people, false negatives when still |
| mmWave Radar | Micro-movements | Accurate presence, works through glass | Higher cost |
| Camera + AI | Visual analysis | People counting, dwell time | Privacy concerns, higher processing needs |
| Desk Sensors | Pressure/heat | Hot-desking, individual workstations | Per-desk installation required |
| Badge Readers | NFC/RFID | Entry/exit tracking, authentication | Requires active badge tap |
| WiFi/BLE | Device proximity | Approximate presence | Dependent 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.