diff --git a/examples/smart_home/src/smart_home/lights/server.py b/examples/smart_home/src/smart_home/lights/server.py index ce0eabcc8..e9946ac49 100644 --- a/examples/smart_home/src/smart_home/lights/server.py +++ b/examples/smart_home/src/smart_home/lights/server.py @@ -101,36 +101,95 @@ def list_groups() -> list[str]: @lights_mcp.tool() -def list_scenes() -> list[str]: - """Lists the names of all available Hue scenes.""" +def list_scenes() -> dict[str, list[str]] | list[str]: + """Lists Hue scenes, grouped by the light group they belong to. + + Returns: + dict[str, list[str]]: A dictionary mapping group names to a list of scene names within that group. + list[str]: An error message list if the bridge connection fails or an error occurs. + """ if not (bridge := _get_bridge()): return ["Error: Bridge not connected"] try: - # phue2 get_scene() returns a dict {id: {details}} including name - scenes = bridge.get_scene() - return [scene_details["name"] for scene_details in scenes.values()] + scenes_data = bridge.get_scene() # Returns dict {scene_id: {details...}} + groups_data = bridge.get_group() # Returns dict {group_id: {details...}} + + # Create a lookup for group name by group ID + group_id_to_name = {gid: ginfo["name"] for gid, ginfo in groups_data.items()} + + scenes_by_group: dict[str, list[str]] = {} + for scene_id, scene_details in scenes_data.items(): + scene_name = scene_details.get("name") + # Scenes might be associated with a group via 'group' key or lights + # Using 'group' key if available is more direct for group scenes + group_id = scene_details.get("group") + if scene_name and group_id and group_id in group_id_to_name: + group_name = group_id_to_name[group_id] + if group_name not in scenes_by_group: + scenes_by_group[group_name] = [] + # Avoid duplicate scene names within a group listing (though unlikely) + if scene_name not in scenes_by_group[group_name]: + scenes_by_group[group_name].append(scene_name) + + # Sort scenes within each group for consistent output + for group_name in scenes_by_group: + scenes_by_group[group_name].sort() + + return scenes_by_group except (PhueException, Exception) as e: - return [f"Error listing scenes: {e}"] + # Return error as list to match other list-returning tools on error + return [f"Error listing scenes by group: {e}"] @lights_mcp.tool() def activate_scene(group_name: str, scene_name: str) -> dict[str, Any]: - """Activates a specific scene within a specified light group.""" + """Activates a specific scene within a specified light group, verifying the scene belongs to the group.""" if not (bridge := _get_bridge()): return {"error": "Bridge not connected", "success": False} try: - # Note: phue2 run_scene uses group_name and scene_name directly + # 1. Find the target group ID + groups_data = bridge.get_group() + target_group_id = None + for gid, ginfo in groups_data.items(): + if ginfo.get("name") == group_name: + target_group_id = gid + break + if not target_group_id: + return {"error": f"Group '{group_name}' not found", "success": False} + + # 2. Find the target scene and check its group association + scenes_data = bridge.get_scene() + scene_found = False + scene_in_correct_group = False + for sid, sinfo in scenes_data.items(): + if sinfo.get("name") == scene_name: + scene_found = True + # Check if this scene is associated with the target group ID + if sinfo.get("group") == target_group_id: + scene_in_correct_group = True + break # Found the scene in the correct group + + if not scene_found: + return {"error": f"Scene '{scene_name}' not found", "success": False} + + if not scene_in_correct_group: + return { + "error": f"Scene '{scene_name}' does not belong to group '{group_name}'", + "success": False, + } + + # 3. Activate the scene (now that we've verified it) result = bridge.run_scene(group_name=group_name, scene_name=scene_name) - # run_scene returns True on success, we'll make the response richer + if result: return { "group": group_name, "activated_scene": scene_name, "success": True, - "phue2_result": result, # Include the raw True/False + "phue2_result": result, } else: - # This case might indicate the scene/group exists but activation failed + # This case might indicate the scene/group exists but activation failed internally return { "group": group_name, "scene": scene_name, @@ -139,7 +198,7 @@ def activate_scene(group_name: str, scene_name: str) -> dict[str, Any]: } except (KeyError, PhueException, Exception) as e: - # KeyError likely means group or scene name is wrong + # Handle potential errors during bridge communication or data parsing return handle_phue_error(f"{group_name}/{scene_name}", "activate_scene", e) @@ -193,3 +252,41 @@ def set_group_attributes(group_name: str, attributes: HueAttributes) -> dict[str } except (KeyError, PhueException, ValueError, Exception) as e: return handle_phue_error(group_name, "set_group_attributes", e) + + +@lights_mcp.tool() +def list_lights_by_group() -> dict[str, list[str]] | list[str]: + """Lists Hue lights, grouped by the room/group they belong to. + + Returns: + dict[str, list[str]]: A dictionary mapping group names to a list of light names within that group. + list[str]: An error message list if the bridge connection fails or an error occurs. + """ + if not (bridge := _get_bridge()): + return ["Error: Bridge not connected"] + try: + groups_data = bridge.get_group() # dict {group_id: {details}} + lights_data = bridge.get_light_objects("id") # dict {light_id: {details}} + + lights_by_group: dict[str, list[str]] = {} + for group_id, group_details in groups_data.items(): + group_name = group_details.get("name") + light_ids = group_details.get("lights", []) + if group_name and light_ids: + light_names = [] + for light_id in light_ids: + # phue uses string IDs for lights in group, but int IDs in get_light_objects + light_id_int = int(light_id) + if light_id_int in lights_data: + light_name = lights_data[light_id_int].name + if light_name: + light_names.append(light_name) + if light_names: + light_names.sort() # Keep light list sorted + lights_by_group[group_name] = light_names + + return lights_by_group + + except (PhueException, Exception) as e: + # Return error as list + return [f"Error listing lights by group: {e}"]