跳到主要内容

Group

The Group module provides functionality for managing homes and rooms within the ESP RainMaker system. It allows users to create, organize, and manage logical groups.

Groups are implemented as hierarchical structures where homes contain rooms, and rooms contain devices.

This guide explains how the Group module works, focusing on the major operations and how CDF APIs are used throughout the system.

User Flow Overview

The Group module has four main user flows:

  1. Home Management: Create, edit, and manage homes
  2. Room Management: Create, edit, and manage rooms within homes
  3. Device Organization: Assign devices to rooms and control them
  4. Group Sharing: Share homes with other users, transfer ownership, and manage permissions

Architecture Overview

Components Structure

(group)/
├── _layout.tsx # Navigation setup
├── Home.tsx # Main home dashboard
├── HomeManagement.tsx # Home management screen
├── Settings.tsx # Home settings, sharing, and info
├── Rooms.tsx # Room listing and management
├── CreateRoom.tsx # Create/edit room screen
├── CustomizeRoomName.tsx # Room name selection
└── CreateRoomSuccess.tsx # Success confirmation

components/HomeSettings/
├── HomeSharing.tsx # Display shared users and pending requests
└── AddUserModal.tsx # Modal for adding users with sharing options

1. Main Home Dashboard (Home.tsx)

This is the main screen where users see their home, rooms, and devices organized in tabs.

Fetch Group List from CDF store

// Get CDF stores
const { store } = useCDF();
const { groupStore, nodeStore, userStore } = store;

// Get current home ID and groups
const currentHomeId = groupStore.currentHomeId; // String ID
const currentHome = groupStore._groupsByID?.[currentHomeId]; // Get actual group object
const groupList = groupStore.groupList;

// Get devices from nodes
const nodes = nodeStore.nodeList;
const devices = transformNodesToDevices(nodes);

Home Operations

// Create default home if none exists
const createDefaultGroup = async () => {
userStore.user?.createGroup({
name: DEFAULT_HOME_GROUP_NAME,
nodeIds: [],
description: "",
customData: {},
type: GROUP_TYPE_HOME,
mutuallyExclusive: true, // Ensures nodes can only belong to one home at a time
});
};

// Switch between homes
const handleHomeSelect = (home: ESPRMGroup) => {
if (home?.id) {
groupStore.currentHomeId = home.id; // Update store with ID
updateLastSelectedHome(userStore, home.id); // Persist to user preferences
setSelectedHome(home); // Update local UI state
}
};

// Initialize home (runs on screen focus)
const initializeHome = async () => {
// Step 1: Handle initial setup - create default home if none exists
if (groupStore?.groupList.length === 0) {
const unAssignedNodes = getUnassignedNodes(nodeStore?.nodeList, []);
await createDefaultHomeGroup(userStore.user, unAssignedNodes);
}

// Step 2-3: Ensure homes are mutually exclusive and assign unassigned nodes

// Step 4: Determine current home
// Retrieve lastSelectedHomeId from user preferences
const lastSelectedHomeId =
userStore.userInfo?.customData?.lastSelectedHomeId?.value || null;

// Find home by preferredId or fallback to first valid home
const primaryHome = findHomeGroup(groupStore?.groupList, {
preferredId: lastSelectedHomeId,
});

const currentHomeId = primaryHome?.id || groupStore.currentHomeId;
const currentHome = groupStore._groupsByID?.[currentHomeId];

// Set current home ID in group store
groupStore.currentHomeId = currentHome.id;

// Step 5: Persist selection if changed
if (lastSelectedHomeId !== currentHome.id) {
updateLastSelectedHome(userStore, currentHome.id);
}

// Step 6: Update UI state
setSelectedHome({ ...currentHome });
};

// Refresh home data
const onRefresh = async () => {
setRefreshing(true);
try {
const shouldFetchFirstPage = true;
await fetchNodesAndGroups(shouldFetchFirstPage);
initializeHome();
} finally {
setRefreshing(false);
}
};

What this does:

  • Shows home selection banner with all available homes
  • Displays devices organized by rooms in tabs
  • Handles home switching and device refresh
  • Uses CDF methods like fetchNodesAndGroups(), groupStore.syncGroupList()
  • Manages current home via groupStore.currentHomeId (stores ID, not object)
  • Persists home selection to user preferences via updateLastSelectedHome()

Key Implementation Details:

  • currentHomeId is stored as a string ID in groupStore.currentHomeId
  • To get the actual group object: groupStore._groupsByID[currentHomeId]
  • Home selection is persisted to userStore.userInfo.customData.lastSelectedHomeId.value
  • The initializeHome() function handles initialization, node assignment, and home selection logic

2. Home Management

HomeManagement.tsx

// Get homes from CDF store
const { store } = useCDF();
const homes = store?.groupStore?.groupList || [];

// Create new home
const handleAddHome = (newHomeName: string) => {
store?.userStore?.user?.createGroup({
mutuallyExclusive: true,
name: newHomeName,
nodeIds: [],
type: GROUP_TYPE_HOME,
}).then(() => {
toast.showSuccess("Home created successfully");
setShowDialog(false);
});
};

// Refresh home list
const onRefresh = async () => {
await store.groupStore.syncGroupList();
updateHomes();
};

3. Room Management

List Rooms - (Rooms.tsx)

// Get rooms from current home
const home = groupStore?.groupList?.find(
(home) => home.id === (id || groupStore?.currentHomeId)
);

// Fetch rooms from home
const fetchGroup = async () => {
await groupStore?.syncGroupList();
if (home) {
await home.getSubGroups();
const rooms = (home.subGroups as ESPRMGroup[]) || [];
state.rooms = rooms;
}
};

// Handle room operations
const handleEditRoom = (room: ESPRMGroup) => {
router.push({
pathname: "/(group)/CreateRoom",
params: { roomId: room.id, id: state.home?.id },
});
};

const handleDeleteRoom = async (room: ESPRMGroup) => {
await room.delete();
await onRefresh();
toast.showSuccess("Room removed successfully");
};

Create a new Room in selected home (CreateRoom.tsx)

// Get home and room from store
const home = store?.groupStore?.groupList?.find((home) => home.id === id);
const room = home?.subGroups?.find((room) => room.id === roomId);

// Get available devices
const availableDevices = nodeStore?.nodeList
?.filter((node: any) => homeNodes.includes(node.id))
?.filter((node: any) => !existingNodes.includes(node.id))
?.map((node: any) => ({
id: node.id,
name: node.nodeConfig?.devices
.map((device: any) => device.displayName)
.join(", "),
node: node,
}));

// Create new room
const handleSave = () => {
home?.createSubGroup({
name: roomName,
nodeIds: selectedDevices.map((device) => device.id) || [],
customData: {},
type: GROUP_TYPE_ROOM,
mutuallyExclusive: true,
}).then(async (group) => {
toast.showSuccess("Room created successfully");
router.replace({
pathname: "/(group)/CreateRoomSuccess",
params: { id: id },
});
});
};

// Update existing room
const handleUpdate = async () => {
const existingNodes = room?.nodes;
const newNodes = selectedDevices.map((device) => device.id);

const nodesToRemove = existingNodes?.filter(
(node) => !newNodes.includes(node)
) || [];
const nodesToAdd = newNodes.filter(
(node) => !existingNodes?.includes(node)
) || [];

await Promise.allSettled([
room?.updateGroupInfo({ groupName: roomName }),
nodesToAdd.length > 0 && room?.addNodes(nodesToAdd),
nodesToRemove.length > 0 && room?.removeNodes(nodesToRemove),
]);

toast.showSuccess("Room updated successfully");
};

Select Room name (CustomizeRoomName.tsx)

// Predefined room names
const predefinedRooms: RoomType[] = [
{ key: "bedroom", label: t("group.customizeRoomName.roomNames.bedroom") },
{ key: "livingRoom", label: t("group.customizeRoomName.roomNames.livingRoom") },
{ key: "kitchen", label: t("group.customizeRoomName.roomNames.kitchen") },
// ... more room types
];

// Handle room name selection
const handleConfirm = () => {
const finalRoomName = roomName.trim() || selectedRoom;
if (finalRoomName) {
router.dismissTo({
pathname: "/(group)/CreateRoom",
params: { roomName: finalRoomName, id: id, roomId: roomId },
});
}
};

4. Home Settings Operations (Settings.tsx)

Home Management

// Update home name
const handleHomeNameUpdate = async () => {
if (homeName?.length > 0) {
setIsLoading(true);
home?.updateGroupInfo({
groupName: homeName,
}).then((res: HomeUpdateResponse) => {
if (res.status === SUCESS) {
toast.showSuccess("Home name updated successfully");
} else {
toast.showError("Failed to update home name");
}
}).finally(() => {
setIsLoading(false);
});
}
};

// Delete home or leave group
const handleRemoveHome = () => {
setIsLoading(true);

const action = isPrimary ? home?.delete() : home?.leave();
const successMessage = isPrimary
? "Home removed successfully"
: "Left home successfully";

action?.then((res: HomeUpdateResponse) => {
if (res.status === SUCESS) {
toast.showSuccess(successMessage);
router.dismiss(1);
} else {
toast.showError("Operation failed");
}
}).finally(() => {
setIsLoading(false);
});
};

Group Sharing

Group sharing allows primary users to share homes with other users, enabling collaborative home management. The system supports multiple sharing modes including standard sharing, ownership transfer, and role assignment.

User Roles

Primary Users:

  • Can manage sharing (add/remove users)
  • Can create and manage rooms
  • Can edit home name
  • Can delete the home
  • Can transfer ownership

Secondary Users:

  • Can view and control devices
  • Cannot manage sharing
  • Cannot create/edit rooms
  • Cannot edit home name
  • Can only leave the home (not delete)

Get Sharing Information

/**
* Fetches sharing information for the home
* Determines if current user is primary or secondary
* Retrieves list of shared users and pending requests
*
* SDK functions used:
* - ESPRMGroup.getSharingInfo
* - ESPRMUser.getIssuedGroupSharingRequests
*/
const getSharedUsers = async () => {
try {
// Get sharing info from home
const res = await home?.getSharingInfo({
metadata: false,
withSubGroups: false,
withParentGroups: false,
});

if (!res) return;

const currentUsername = norm(store?.userStore?.userInfo?.username);
const primaryUsers = (res.primaryUsers || []).map((u) => ({
...u,
username: norm(u.username),
}));
const secondaryUsers = (res.secondaryUsers || []).map((u) => ({
...u,
username: norm(u.username),
}));

// Check if current user is primary
const isCurrentUserPrimary = primaryUsers.some(
(u) => u.username === currentUsername
);
setIsPrimary(isCurrentUserPrimary);

// If secondary user, show who shared with them
if (!isCurrentUserPrimary && primaryUsers.length > 0) {
setSharedByUser({
id: generateRandomId(),
username: primaryUsers[0].username,
metadata: primaryUsers[0].metadata,
});
setSharedUsers([]);
setPendingUsers([]);
return;
}

// If primary user, get pending requests
if (isCurrentUserPrimary) {
const issuedSharingInfo =
await store?.userStore?.user?.getIssuedGroupSharingRequests();

// Extract all requests from all status categories
let allSharingRequests: any[] = [];
if (issuedSharingInfo?.sharedRequests) {
const sharedRequestsObj = issuedSharingInfo.sharedRequests as any;
allSharingRequests = Object.values(sharedRequestsObj).reduce(
(acc: any[], curr: any) => {
if (Array.isArray(curr)) {
acc.push(...curr);
}
return acc;
},
[]
);
}

// Filter pending requests for this home
const pendingRequests = allSharingRequests
.filter((req: any) => {
const isPending = req.status === "pending";
const isForThisGroup = req.groupIds?.includes(home?.id);
const isNotExpired = !isRequestExpired(req.timestamp);
return isPending && isForThisGroup && isNotExpired;
})
.map((req: any) => ({
id: req.id || generateRandomId(),
username: norm(req.username),
metadata: req.metadata || {},
requestId: req.id,
timestamp: req.timestamp,
remainingDays: getRemainingDays(req.timestamp),
expirationMessage: formatExpirationMessage(req.timestamp, t),
}));

setPendingUsers(sortByExpirationDate(pendingRequests));

// Get accepted users (excluding current user)
const acceptedUsers = [
...primaryUsers.filter((u) => u.username !== currentUsername),
...secondaryUsers.filter((u) => u.username !== currentUsername),
].map((u) => ({
id: generateRandomId(),
username: u.username,
metadata: u.metadata,
}));

setSharedUsers(acceptedUsers);
setSharedByUser(null);
}
} catch (error) {
toast.showError(t("group.errors.errorGettingSharedUsers"));
}
};

Share Home with User

/**
* Adds a user to the home with different sharing options
*
* Sharing modes:
* 1. Standard sharing (makePrimary: true/false)
* 2. Transfer ownership (transfer: true)
* 3. Transfer and assign role (transferAndAssignRole: true)
*
* SDK function used:
* - ESPRMGroup.Share
* - ESPRMGroup.transfer
*/
const handleAddUser = async () => {
setIsAddingUserLoading(true);

try {
if (transferAndAssignRole) {
// Transfer ownership and assign secondary role to self
await home?.transfer({
toUserName: newUserEmail,
assignRoleToSelf: "secondary",
metadata: {},
});
} else if (transfer) {
// Transfer ownership only
await home?.transfer({
toUserName: newUserEmail,
metadata: {},
});
} else {
// Standard sharing
await home?.Share({
toUserName: newUserEmail,
makePrimary: makePrimary, // true = primary, false = secondary
});
}

toast.showSuccess(
transfer || transferAndAssignRole
? t("group.settings.transferRequestedSuccessfully")
: t("group.settings.sharingRequestedSuccessfully")
);
setIsAddingUser(false);
setNewUserEmail("");
setMakePrimary(false);
setTransfer(false);
setTransferAndAssignRole(false);

// Refresh sharing info to show pending users
getSharedUsers();
} catch (err: any) {
switch (err.errorCode) {
case ERROR_CODES_MAP.USER_NOT_FOUND:
toast.showError(t("group.errors.userNotFound"));
break;
case ERROR_CODES_MAP.ADDING_SELF_NOT_ALLOWED:
toast.showError(t("group.errors.addingSelfNotAllowed"));
break;
default:
toast.showError(err.description);
break;
}
} finally {
setIsAddingUserLoading(false);
}
};

Remove User from Home

/**
* Removes an accepted user from the home
* Only primary users can remove other users
*
* SDK function used:
* - ESPRMGroup.removeSharingFor
*/
const handleRemoveUser = (username: string) => {
setRemoveUserLoading(true);
home
?.removeSharingFor(username)
.then(() => {
toast.showSuccess(t("group.settings.sharingRemovedSuccessfully"));
// Refresh sharing info to get updated list
getSharedUsers();
})
.catch((err) => {
toast.showError(err.description);
})
.finally(() => {
setRemoveUserLoading(false);
});
};

Remove Pending Sharing Request

/**
* Removes a pending sharing request
* Cancels a sharing invitation that hasn't been accepted yet
*
* SDK functions used:
* - ESPRMUser.getIssuedGroupSharingRequests
* - ESPRMGroupSharingRequest.remove
*/
const handleRemovePendingUser = async (username: string) => {
try {
setRemoveUserLoading(true);

const issuedSharingInfo =
await store?.userStore?.user?.getIssuedGroupSharingRequests();

let allSharingRequests: any[] = [];
if (issuedSharingInfo?.sharedRequests) {
const sharedRequestsObj = issuedSharingInfo.sharedRequests as any;
allSharingRequests = Object.values(sharedRequestsObj).reduce(
(acc: any[], curr: any) => {
if (Array.isArray(curr)) {
acc.push(...curr);
}
return acc;
},
[]
);
}

// Find pending request for this user and home
const pendingRequest = allSharingRequests.find((req: any) => {
const isMatchingUser = norm(req.username) === norm(username);
const isForThisGroup = req.groupIds?.includes(home?.id);
const isPending = req.status === "pending";
return isMatchingUser && isForThisGroup && isPending;
});

if (pendingRequest) {
await pendingRequest.remove();
toast.showSuccess(t("group.settings.sharingRemovedSuccessfully"));
getSharedUsers();
} else {
throw new Error("Pending request not found");
}
} catch (error) {
toast.showError(t("group.errors.errorRemovingUser"));
} finally {
setRemoveUserLoading(false);
}
};

Sharing UI Components

The sharing functionality uses two main components:

HomeSharing Component:

  • Displays shared users and pending requests
  • Shows "Shared by" for secondary users
  • Provides add/remove user actions
  • Only visible to primary users (except "Shared by" section)

AddUserModal Component:

  • Email input with validation
  • Sharing options:
    • Grant Ownership: Share as primary user (makePrimary: true)
    • Transfer Group: Transfer ownership to another user
    • Transfer and Assign Role: Transfer ownership and assign secondary role to self
  • Mutually exclusive options (only one can be selected)

Sharing Flow

  1. Primary user opens Settings → Views sharing section
  2. Clicks "Add User" → Opens AddUserModal
  3. Enters email and selects sharing mode → Submits sharing request
  4. Request appears in "Pending for Acceptance" → Shows expiration time
  5. Recipient accepts request → User appears in "Shared with" section
  6. Primary user can remove → Removes from shared users or cancels pending request

Key Implementation Details

  • Role Detection: Use getSharingInfo() to determine if current user is primary or secondary
  • Pending Requests: Fetched via getIssuedGroupSharingRequests() and filtered by status and group ID
  • Request Expiration: Pending requests have expiration timestamps that should be checked
  • Email Validation: Must validate email format before sharing
  • Self-Sharing Prevention: System prevents users from adding themselves
  • Refresh Pattern: Always call getSharedUsers() after sharing operations to update UI
  • Error Handling: Handle specific error codes (USER_NOT_FOUND, ADDING_SELF_NOT_ALLOWED)

CDF API Usage Summary

1. Group Store Operations

// Get all groups/homes
const groupList = groupStore.groupList;

// Get current home ID (string)
const currentHomeId = groupStore.currentHomeId;

// Get current home object from ID
const currentHome = groupStore._groupsByID?.[currentHomeId];

// Sync group list
await groupStore.syncGroupList();

// Create new group
await userStore.user.createGroup({
name: groupName,
nodeIds: [],
type: GROUP_TYPE_HOME,
mutuallyExclusive: true
});

2. Group Operations

// Create subgroup (room)
await home.createSubGroup({
name: roomName,
nodeIds: selectedDevices,
type: GROUP_TYPE_ROOM,
mutuallyExclusive: true
});

// Update group info
await group.updateGroupInfo({
groupName: newName
});

// Add nodes to group
await group.addNodes(nodeIds);

// Remove nodes from group
await group.removeNodes(nodeIds);

// Delete group
await group.delete();

// Leave group
await group.leave();

3. Group Sharing Operations

// Get sharing information
const sharingInfo = await home.getSharingInfo({
metadata: false,
withSubGroups: false,
withParentGroups: false,
});
// Returns: { primaryUsers: [], secondaryUsers: [] }

// Share home with user (standard sharing)
await home.Share({
toUserName: "user@example.com",
makePrimary: false, // true = primary user, false = secondary user
});

// Transfer home ownership
await home.transfer({
toUserName: "user@example.com",
metadata: {},
});

// Transfer ownership and assign role to self
await home.transfer({
toUserName: "user@example.com",
assignRoleToSelf: "secondary", // or "primary"
metadata: {},
});

// Remove user from sharing
await home.removeSharingFor(username);

// Get issued sharing requests (for pending requests)
const issuedSharingInfo = await userStore.user.getIssuedGroupSharingRequests();
// Returns: { sharedRequests: { [status]: [...] } }

// Remove pending sharing request
const pendingRequest = /* find request from issuedSharingInfo */;
await pendingRequest.remove();

4. User Store Operations

// Get user info
const userInfo = store.userStore.userInfo;

// Create group
await userStore.user.createGroup(groupConfig);

// Get sharing requests
const sharingInfo = await userStore.user.getIssuedGroupSharingRequests();

Data Flow Summary

Home Management Flow:

  1. Home.tsx → User clicks "Manage Homes"
  2. HomeManagement.tsx → User creates new home using CDF userStore.user.createGroup()
  3. Success → Home created and added to groupStore.groupList

Room Management Flow:

  1. Settings.tsx → User clicks "Room Management"
  2. Rooms.tsx → User clicks "Add Room"
  3. CreateRoom.tsx → User selects devices and enters room name
  4. CustomizeRoomName.tsx → User chooses predefined or custom name
  5. CreateRoom.tsx → Save room using CDF home.createSubGroup()
  6. CreateRoomSuccess.tsx → Success confirmation

Group Sharing Flow:

  1. Settings.tsx → Primary user views sharing section
  2. HomeSharing Component → Displays shared users and pending requests
  3. Add User Button → Opens AddUserModal
  4. AddUserModal → User enters email and selects sharing mode:
    • Standard sharing (primary/secondary role)
    • Transfer ownership
    • Transfer and assign role
  5. CDF API Callhome.Share() or home.transfer() creates sharing request
  6. Pending Request → Appears in "Pending for Acceptance" section
  7. Recipient Accepts → User appears in "Shared with" section
  8. Remove User → Primary user can remove via home.removeSharingFor() or cancel pending request

Key Takeaways for Developers

  1. Three Main Flows: Home Management, Room Management, Group Sharing
  2. CDF is the Backbone: All operations use CDF stores (groupStore, userStore)
  3. Hierarchical Structure: Homes contain Rooms, Rooms contain Devices
  4. Async Operations: All CDF operations are async with proper error handling
  5. Data Flow: Home → Rooms → Devices → Parameters
  6. Sharing System:
    • Primary users can share homes with other users (primary or secondary roles)
    • Supports ownership transfer and role assignment
    • Pending requests expire and must be managed
    • Secondary users have limited permissions (view/control only)
  7. Validation: Home names must be unique and within length limits, emails must be validated for sharing
  8. Refresh Pattern: Use fetchNodesAndGroups() for refreshing data, getSharedUsers() for sharing info
  9. Current Home Management:
    • groupStore.currentHomeId stores the current home ID as a string
    • Access the group object via groupStore._groupsByID[currentHomeId]
    • Home selection is persisted to userStore.userInfo.customData.lastSelectedHomeId.value
    • The initializeHome() function handles initialization and selection logic
  10. Role-Based Access: Use getSharingInfo() to determine user role and show appropriate UI

Common Patterns

Store Access Pattern:

const { store } = useCDF();
const { groupStore, nodeStore, userStore } = store;

Group Operations Pattern:

// Create group/room
const response = await home.createSubGroup({
name: roomName,
nodeIds: selectedDevices,
type: "room",
mutuallyExclusive: true
});

// Update group
const response = await home.updateGroupInfo({
groupName: newName
});

Error Handling Pattern:

try {
const result = await operation();
if (result.status === "success") {
toast.showSuccess(successMessage);
} else {
toast.showError(result.description);
}
} catch (error) {
toast.showError(error.description);
}

Refresh Pattern:

const onRefresh = async () => {
setRefreshing(true);
try {
await store.groupStore.syncGroupList();
await store.nodeStore.syncNodeList();
} catch (error) {
toast.showError(error.description);
} finally {
setRefreshing(false);
}
};

Group Sharing Pattern:

// Get sharing info and determine role
const getSharedUsers = async () => {
const res = await home.getSharingInfo({
metadata: false,
withSubGroups: false,
withParentGroups: false,
});

const currentUsername = norm(userStore.userInfo.username);
const isPrimary = res.primaryUsers.some(
u => norm(u.username) === currentUsername
);

if (isPrimary) {
// Get pending requests
const issuedInfo = await userStore.user.getIssuedGroupSharingRequests();
// Process and display pending/accepted users
} else {
// Show who shared with current user
setSharedByUser(res.primaryUsers[0]);
}
};

// Share home
await home.Share({
toUserName: email,
makePrimary: false,
});

// Remove user
await home.removeSharingFor(username);