Skip to main content
Glama
pedrof
by pedrof
hue_client.py11 kB
""" Philips Hue Bridge client wrapper. This module provides an async wrapper around the aiohue library for interacting with Philips Hue Bridge API. """ from typing import Dict, List, Optional, Any import logging try: from aiohue.v2 import HueBridgeV2 except ImportError: HueBridgeV2 = None logger = logging.getLogger(__name__) # Brightness value constants BRIGHTNESS_MIN_HUE_API = 0 BRIGHTNESS_MAX_HUE_API = 254 BRIGHTNESS_MIN_PERCENT = 0.0 BRIGHTNESS_MAX_PERCENT = 100.0 # Color temperature constants (in mireds) COLOR_TEMP_MIN_MIREDS = 153 COLOR_TEMP_MAX_MIREDS = 500 # CIE xy color space constants CIE_XY_MIN = 0.0 CIE_XY_MAX = 1.0 # Resource type identifier DEVICE_RESOURCE_TYPE = "device" def _convert_brightness_to_percentage(brightness: int) -> float: """ Convert Hue API brightness value (0-254) to percentage (0-100). Args: brightness: Brightness value in Hue API format (0-254) Returns: Brightness as percentage (0.0-100.0) """ return (brightness / BRIGHTNESS_MAX_HUE_API) * BRIGHTNESS_MAX_PERCENT def _is_device_resource(resource_type: str) -> bool: """ Check if a resource type identifier represents a device. Args: resource_type: The resource type string to check Returns: True if the resource is a device type """ return resource_type == DEVICE_RESOURCE_TYPE class HueClient: """Async client for interacting with Philips Hue Bridge.""" def __init__(self, bridge_ip: str, api_key: str): """ Initialize Hue client. Args: bridge_ip: IP address of the Hue Bridge api_key: API key/username for authentication """ if HueBridgeV2 is None: raise ImportError( "aiohue library not installed. " "Install it with: pip install aiohue" ) self.bridge_ip = bridge_ip self.api_key = api_key self.bridge: Optional[HueBridgeV2] = None self._connected = False async def connect(self) -> None: """Connect to the Hue Bridge.""" try: self.bridge = HueBridgeV2(self.bridge_ip, self.api_key) await self.bridge.initialize() self._connected = True logger.info(f"Connected to Hue Bridge at {self.bridge_ip}") except Exception as e: logger.error(f"Failed to connect to Hue Bridge: {e}") raise async def disconnect(self) -> None: """Disconnect from the Hue Bridge.""" if self.bridge: await self.bridge.close() self._connected = False logger.info("Disconnected from Hue Bridge") def is_connected(self) -> bool: """Check if connected to the bridge.""" return self._connected async def get_lights(self) -> Dict[str, Any]: """ Get all lights from the bridge. Returns: Dictionary of light resources """ if not self.bridge: raise RuntimeError("Not connected to bridge") lights = {} for light in self.bridge.lights: # Get device name device = self.bridge.devices.get(light.owner.rid) name = device.metadata.name if device and device.metadata else f"Light {light.id[:8]}" lights[light.id] = { "id": light.id, "name": name, "on": light.on.on if light.on else False, "brightness": light.dimming.brightness if light.dimming else None, "color_temp": light.color_temperature.mirek if (light.color_temperature and light.color_temperature.mirek_valid) else None, "color": { "xy": [light.color.xy.x, light.color.xy.y] if light.color and light.color.xy else None, }, "reachable": True, # v2 API doesn't have reachable status } return lights async def get_light(self, light_id: str) -> Optional[Dict[str, Any]]: """ Get a specific light by ID. Args: light_id: The light's unique identifier Returns: Light data dictionary or None if not found """ lights = await self.get_lights() return lights.get(light_id) async def set_light_state( self, light_id: str, on: Optional[bool] = None, brightness: Optional[int] = None, color_temp: Optional[int] = None, xy: Optional[List[float]] = None, transition_time: Optional[float] = None, ) -> bool: """ Set the state of a light. Args: light_id: The light's unique identifier on: Turn light on (True) or off (False) brightness: Brightness level (0-254) color_temp: Color temperature in mireds (153-500) xy: Color in CIE xy format [x, y] transition_time: Transition time in seconds Returns: True if successful """ if not self.bridge: raise RuntimeError("Not connected to bridge") # Verify light exists light = self.bridge.lights.get(light_id) if not light: raise ValueError(f"Light {light_id} not found") # Use the specific methods provided by aiohue LightsController # These are the ONLY correct ways to control lights in aiohue v2 API # DO NOT use update() or pass kwargs - it will fail! if on is not None: # Use dedicated turn_on/turn_off methods if on: await self.bridge.lights.turn_on(light_id) else: await self.bridge.lights.turn_off(light_id) if brightness is not None: # aiohue v2 uses 0-100 percentage for brightness, not 0-254 brightness_percentage = _convert_brightness_to_percentage(brightness) await self.bridge.lights.set_brightness(light_id, brightness_percentage) if color_temp is not None: # Set color temperature in mireds (153-500) await self.bridge.lights.set_color_temperature(light_id, color_temp) if xy is not None: # aiohue expects x and y as separate positional parameters await self.bridge.lights.set_color(light_id, xy[0], xy[1]) # Note: transition_time is not directly supported by all methods in aiohue # It would need to be implemented differently if required return True async def get_groups(self) -> Dict[str, Any]: """ Get all groups/rooms from the bridge. Returns: Dictionary of group resources """ if not self.bridge: raise RuntimeError("Not connected to bridge") groups = {} for group in self.bridge.groups: # Only include Room objects (not GroupedLight) if hasattr(group, 'metadata') and group.metadata: groups[group.id] = { "id": group.id, "name": group.metadata.name, "type": group.type.value if hasattr(group, 'type') else "room", "lights": [svc.rid for svc in group.children] if hasattr(group, 'children') else [], } return groups async def get_scenes(self) -> Dict[str, Any]: """ Get all scenes from the bridge. Returns: Dictionary of scene resources """ if not self.bridge: raise RuntimeError("Not connected to bridge") scenes = {} for scene in self.bridge.scenes: scenes[scene.id] = { "id": scene.id, "name": scene.metadata.name if scene.metadata else f"Scene {scene.id[:8]}", "group": scene.group.rid if hasattr(scene, 'group') else None, } return scenes async def activate_scene(self, scene_id: str) -> bool: """ Activate a scene. Args: scene_id: The scene's unique identifier Returns: True if successful """ if not self.bridge: raise RuntimeError("Not connected to bridge") scene = self.bridge.scenes.get(scene_id) if not scene: raise ValueError(f"Scene {scene_id} not found") await self.bridge.scenes.recall(scene_id) return True def _collect_light_ids_from_group(self, group) -> List[str]: """ Collect all light IDs from a group's devices. In Hue API v2, groups contain device references in their 'children' attribute. This method finds all lights owned by those devices. Args: group: The group resource object Returns: List of light IDs belonging to the group Raises: ValueError: If group has no children or no lights found """ if not hasattr(group, 'children'): raise ValueError(f"Group {group.id} has no children") light_ids = [] for child in group.children: # Each child is a ResourceIdentifier - check if it points to a device if _is_device_resource(child.rtype.value): # Find all lights owned by this device for light in self.bridge.lights: if hasattr(light, 'owner') and light.owner.rid == child.rid: light_ids.append(light.id) if not light_ids: raise ValueError(f"No lights found in group {group.id}") return light_ids async def set_group_state( self, group_id: str, on: Optional[bool] = None, brightness: Optional[int] = None, color_temp: Optional[int] = None, xy: Optional[List[float]] = None, ) -> bool: """ Set the state of all lights in a group. Args: group_id: The group's unique identifier on: Turn lights on (True) or off (False) brightness: Brightness level (0-254) color_temp: Color temperature in mireds (153-500) xy: Color in CIE xy format [x, y] Returns: True if successful """ if not self.bridge: raise RuntimeError("Not connected to bridge") group = self.bridge.groups.get(group_id) if not group: raise ValueError(f"Group {group_id} not found") # Collect all light IDs from the group's devices light_ids = self._collect_light_ids_from_group(group) # Apply state to each light in the group # Note: This could be optimized by using asyncio.gather for parallel execution for light_id in light_ids: await self.set_light_state( light_id=light_id, on=on, brightness=brightness, color_temp=color_temp, xy=xy, ) return True

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/pedrof/hue-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server