The Schedule module provides comprehensive functionality for managing device schedules across multiple nodes in the ESP RainMaker system. It allows users to create, edit, enable, disable, and delete schedules that can control multiple devices automatically at specified times with repeat patterns using the CDF (Central Data Framework) and SDK.
Schedules are implemented completely on the node side as a "service", with the cloud backend acting as a gateway between nodes and clients like phone apps.
Scheduling
This guide explains how the Schedule module works, focusing on the major operations and how CDF APIs are used throughout the system.
User Flow Overview
The Schedule module has four main user flows:
- Create New Schedule: Schedules → CreateSchedule → Select Device → Configure Device Parameters → Set Time & Repeat Days
- Edit Existing Schedule: Schedules → CreateSchedule (with existing data) → Modify → Save
- Enable/Disable Schedule: Schedules → Toggle Schedule Status
- Delete Schedule: Schedules → CreateSchedule (edit mode) → Delete
Architecture Overview
Components Structure
(schedule)/
├── _layout.tsx # Navigation and context setup
├── Schedules.tsx # Main schedule list screen
├── CreateSchedule.tsx # Create/edit schedule screen
├── ScheduleDeviceSelection.tsx # Choose devices for schedule
└── ScheduleDeviceParamsSelection.tsx # Configure device parameters
1. Schedules List (Schedules.tsx)
This is the main screen where users see all their schedules and can manage them.
// Get CDF stores
const { store } = useCDF();
const { scheduleStore, nodeStore, groupStore } = store;
// Get schedule list from CDF
const { scheduleList } = scheduleStore;
// Fetch and sync schedules data from all nodes in the currently active home
const fetchSchedules = async () => {
const currentHome = groupStore?._groupsByID[groupStore?.currentHomeId];
await scheduleStore.syncSchedulesFromNodes(currentHome.nodes || []);
};
Schedule Operations
// Enable/disable a schedule
const handleScheduleToggle = async (scheduleId: string, enabled: boolean) => {
const selectedSchedule = scheduleList.find((schedule) => schedule.id === scheduleId);
if (enabled) {
await selectedSchedule.enable(); // CDF handles communication
} else {
await selectedSchedule.disable(); // CDF handles communication
}
};
// Edit a schedule
const handleScheduleEdit = async (scheduleId: string) => {
const selectedSchedule = scheduleList.find((schedule) => schedule.id === scheduleId);
setScheduleInfo(selectedSchedule); // Load schedule into context
router.push("/(schedule)/CreateSchedule");
};
// Delete a schedule
const handleScheduleDelete = async (scheduleId: string) => {
const selectedSchedule = scheduleList.find((schedule) => schedule.id === scheduleId);
await selectedSchedule.remove(); // CDF removes from all nodes
};
What this does:
- Lists all schedules from CDF schedule store
- Handles schedule enable/disable, editing, and deletion
- Uses CDF methods like
schedule.enable(),schedule.disable(),schedule.remove(),schedule.edit()
2. Create New Schedule Flow
Step 1: CreateSchedule.tsx (Empty State)
// Get CDF stores
const { store } = useCDF();
const { scheduleStore } = store;
// Initialize empty schedule
const [state, setState] = useState({
scheduleName: "",
scheduleId: generateRandomId(),
isEditing: false,
enabled: true,
triggers: [],
actions: {},
nodes: [],
});
Step 2: ScheduleDeviceSelection.tsx
// Get nodes from CDF store
const { store } = useCDF();
const nodeList = store?.nodeStore?.nodeList as ESPRMNode[];
// Filter nodes that support Schedules service
const scheduleNodes = nodeList.filter((node) =>
node.nodeConfig?.services?.some(
(service) => service.type === ESPRM_SCHEDULES_SERVICE
)
);
// Extract devices from each node
scheduleNodes.forEach((node) => {
const devices = node.nodeConfig?.devices ?? [];
const schedule = node.nodeConfig?.services?.find(
(service) => service.type === ESPRM_SCHEDULES_SERVICE
)?.params[0] as any;
// Check if node has reached max schedules
const isMaxScheduleReached =
schedule && schedule.bounds?.max && schedule.bounds.max == schedule.value.length;
devices.forEach((device) => {
allDevices.push({
node: deepClone(node),
device: deepClone(device),
isSelected: false, // New schedule, no existing actions
isMaxScheduleReached,
});
});
});
Step 3: ScheduleDeviceParamsSelection.tsx
// Get device from CDF node store
const { nodeId, params } = useMemo(() => {
const nodeId = state.selectedDevice?.nodeId;
if (!nodeId) return {};
const node = store.nodeStore.nodesByID[nodeId];
if (!node || !node.nodeConfig) return {};
const device = node.nodeConfig.devices.find(
(device) => device.name === state.selectedDevice?.deviceName
);
if (!device) return {};
// Filter and process device parameters
const filteredParams = device.params?.filter(
(param) =>
param.type !== ESPRM_NAME_PARAM_TYPE &&
param.uiType !== ESPRM_UI_HIDDEN_PARAM_TYPE
);
// Map parameters with schedule action values
const params = filteredParams?.map((param) => ({
...param,
value: getActionValue(nodeId, device.name, param.name) || param.value,
}));
return { selectedDevice: device, nodeId, params };
}, [state.selectedDevice]);
Step 4: Set Time & Repeat Days
// Configure schedule triggers (time and repeat pattern)
const handleTimeChange = (time: { hours: number; minutes: number }) => {
const minutesFromMidnight = time.hours * 60 + time.minutes;
setTriggers([{ m: minutesFromMidnight, d: selectedDaysBitmap }]);
};
const handleDayToggle = (dayIndex: number) => {
const newDaysBitmap = selectedDaysBitmap ^ (1 << dayIndex);
setSelectedDaysBitmap(newDaysBitmap);
setTriggers([{ m: minutesFromMidnight, d: newDaysBitmap }]);
};
Step 5: Save Schedule
const handleSave = async () => {
setLoading((prev) => ({ ...prev, save: true }));
try {
// Prepare schedule data
const scheduleData = {
id: state.scheduleId,
name: state.scheduleName,
description: "",
nodes: Object.keys(state.actions), // Node IDs involved
action: state.actions || {}, // Device actions
triggers: state.triggers, // Time and repeat pattern
enabled: state.enabled,
validity: state.validity,
info: state.info,
flags: state.flags,
};
// Create new schedule using CDF
await scheduleStore.createSchedule(scheduleData as any);
toast.showSuccess(t("scheduleManagement.scheduleCreatedSuccessfully"));
resetState();
router.dismissTo("/(schedule)/Schedules");
} catch (error) {
console.error("Error saving schedule:", error);
toast.showError(t("schedule.errors.scheduleCreationFailed"));
} finally {
setLoading((prev) => ({ ...prev, save: false }));
}
};
3. Edit Existing Schedule Flow
Flow: Schedules → CreateSchedule (with existing data) → Modify → Save
Step 1: Load Existing Schedule Data
// When editing, schedule data is already loaded in context
useEffect(() => {
if (state.isEditing && state.scheduleId) {
// Schedule data is already in context from Schedules.tsx
// No need to fetch from CDF again
}
}, [state.isEditing, state.scheduleId]);
Step 2: Modify Schedule Data
// User can modify existing schedule data
// Changes are tracked in schedule context
const handleScheduleNameChange = (name: string) => {
setScheduleName(name);
};
const handleTimeChange = (time: { hours: number; minutes: number }) => {
const minutesFromMidnight = time.hours * 60 + time.minutes;
setTriggers([{ m: minutesFromMidnight, d: selectedDaysBitmap }]);
};
const handleDeviceActionChange = (
nodeId: string,
deviceName: string,
paramName: string,
value: any
) => {
setActionValue(nodeId, deviceName, paramName, value);
};
Step 3: Save Changes
const handleSave = async () => {
setLoading((prev) => ({ ...prev, save: true }));
try {
// Update existing schedule using CDF
await scheduleStore.schedulesByID[state.scheduleId]?.edit({
name: state.scheduleName,
action: state.actions,
triggers: state.triggers,
});
toast.showSuccess(t("scheduleManagement.scheduleUpdatedSuccessfully"));
resetState();
router.dismissTo("/(schedule)/Schedules");
} catch (error) {
console.error("Error updating schedule:", error);
toast.showError(t("schedule.errors.scheduleUpdateFailed"));
} finally {
setLoading((prev) => ({ ...prev, save: false }));
}
};
4. Schedule Operations (Enable/Disable & Delete)
Enable/Disable Schedule Operation
// Toggle schedule from Schedules.tsx
const handleScheduleToggle = async (scheduleId: string, enabled: boolean) => {
try {
const selectedSchedule = scheduleStore.schedulesByID[scheduleId];
if (selectedSchedule) {
if (enabled) {
await selectedSchedule.enable(); // CDF handles communication
} else {
await selectedSchedule.disable(); // CDF handles communication
}
toast.showSuccess(
enabled
? "Schedule enabled successfully"
: "Schedule disabled successfully"
);
}
} catch (error) {
console.error("Error toggling schedule:", error);
toast.showError("Failed to toggle schedule");
}
};
What this does:
- Enables or disables schedule execution
- Uses CDF's
schedule.enable()andschedule.disable()methods - Automatically updates schedule status across all nodes
Delete Schedule Operation
// Delete button only visible when editing existing schedule
const handleDelete = async () => {
setLoading((prev) => ({ ...prev, delete: true }));
try {
// Use CDF to remove schedule from all nodes
await scheduleStore.schedulesByID[state.scheduleId]?.remove();
toast.showSuccess(t("scheduleManagement.scheduleDeletedSuccessfully"));
resetState();
router.dismissTo("/(schedule)/Schedules");
} catch (error) {
console.error("Error deleting schedule:", error);
toast.showError(t("scheduleManagement.scheduleDeletionFailed"));
} finally {
setLoading((prev) => ({ ...prev, delete: false }));
}
};
Key Points:
- Delete button only appears when editing an existing schedule
- Uses CDF's
schedule.remove()method - Automatically removes schedule from all involved nodes
5. Out-of-Sync Device Handling
The schedule module includes sophisticated out-of-sync detection and synchronization:
// Check if node is out of sync
const checkNodeOutOfSync = (nodeId: string) => {
const node = store.nodeStore.nodesByID[nodeId];
const schedule = scheduleStore.schedulesByID[state.scheduleId];
// Compare current device state with stored schedule actions
const isOutOfSync = /* comparison logic */;
return {
isOutOfSync: boolean,
details: {
action: Record<string, any>,
name: string,
triggers: ScheduleTrigger[],
flags: number,
},
};
};
// Sync out-of-sync devices
const handleScheduleSync = async () => {
setLoading((prev) => ({ ...prev, sync: true }));
try {
// Prepare full schedule data for synchronization
const scheduleData = {
id: state.scheduleId,
name: state.scheduleName,
description: "",
nodes: state.nodes.map((node) => node.id),
action: state.actions,
triggers: state.triggers,
enabled: state.enabled,
validity: state.validity,
info: state.info,
flags: state.flags,
};
// Sync schedule data to out-of-sync nodes using edit()
const resp = await scheduleStore.schedulesByID[state.scheduleId]?.edit(
scheduleData as any
) as any;
// Check response status from all nodes
if (resp && resp.some((resp: any) => resp.status !== SUCESS)) {
toast.showError("Some devices failed to update");
return false;
} else {
toast.showSuccess("Schedule synchronized successfully");
// Clear out-of-sync metadata on success
dispatch({ type: "SET_OUT_OF_SYNC_META", payload: {} });
return true;
}
} catch (error) {
console.error("Error syncing schedule:", error);
toast.showError("Failed to sync schedule");
return false;
} finally {
setLoading((prev) => ({ ...prev, sync: false }));
}
};
What this does:
- Detects when device state differs from stored schedule actions
- Provides visual indication of out-of-sync status
- Uses
schedule.edit()method with full schedule data to synchronize - Checks response status from all nodes to ensure successful sync
- Clears out-of-sync metadata on successful synchronization
- Allows manual synchronization to restore consistency
CDF API Usage Summary
1. Schedule Store Operations
// Get all schedules
const { scheduleList } = scheduleStore;
// Create new schedule
await scheduleStore.createSchedule(scheduleData);
// Edit existing schedule
await scheduleStore.schedulesByID[scheduleId].edit(updates);
// Delete schedule
await scheduleStore.schedulesByID[scheduleId].remove();
// Enable schedule
await schedule.enable();
// Disable schedule
await schedule.disable();
// Sync schedules from nodes
await scheduleStore.syncSchedulesFromNodes(nodes);
2. Schedule Data Structure
// Schedule Trigger Format
interface ScheduleTrigger {
m: number; // Minutes from midnight (0-1439)
d: number; // Day bitmap (bit 0 = Sunday, bit 1 = Monday, etc.)
}
// Schedule Action Format
interface ScheduleAction {
[nodeId: string]: {
[deviceName: string]: {
[paramName: string]: any; // Parameter value
};
};
}
Data Flow Summary
Create New Schedule Flow:
- Schedules.tsx → User clicks "Create Schedule"
- CreateSchedule.tsx → User enters schedule name
- ScheduleDeviceSelection.tsx → User selects devices from CDF node store
- ScheduleDeviceParamsSelection.tsx → User configures parameters
- CreateSchedule.tsx → User sets time and repeat days
- CreateSchedule.tsx → Save schedule using CDF
scheduleStore.createSchedule()
Edit Existing Schedule Flow:
- Schedules.tsx → User clicks "Edit" on existing schedule
- CreateSchedule.tsx → Schedule data loaded from context
- User modifies → Schedule name, time, repeat days, device actions, etc.
- Save changes → Using CDF
scheduleStore.schedulesByID[scheduleId].edit()
Enable/Disable & Delete Operations:
- Enable/Disable: Available from schedule list, uses CDF
schedule.enable()/schedule.disable() - Delete: Only available in edit mode, uses CDF
schedule.remove()
Key Takeaways
- Four Main Flows: Create (5 steps), Edit (3 steps), Enable/Disable, Delete
- CDF is the Backbone: All operations use CDF stores
- Context Management: Schedule context manages temporary state
- Delete Visibility: Delete button only shows when editing
- Async Operations: All CDF operations are async with proper error handling
- Data Flow: Schedules → CreateSchedule → Device Selection → Parameter Configuration → Time/Repeat Setup → Save
- Out-of-Sync Handling: Sophisticated detection and synchronization for device state consistency