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);
};