Skip to main content
Glama
smartthings.py6.02 kB
from __future__ import annotations import logging import os from typing import Any, Dict, List, Optional import httpx # Configure logging logger = logging.getLogger(__name__) ST_BASE_DEFAULT = "https://api.smartthings.com/v1" class SmartThingsError(RuntimeError): """Custom exception for SmartThings API errors.""" pass class SmartThingsClient: def __init__(self, token: str | None = None, base_url: str | None = None, timeout: float = 15.0): self.token = token or os.environ.get("SMARTTHINGS_PAT") if not self.token: raise SmartThingsError("SMARTTHINGS_PAT is required (env var)") self.base_url = (base_url or os.environ.get("SMARTTHINGS_BASE_URL") or ST_BASE_DEFAULT).rstrip("/") logger.info(f"Initializing SmartThings client with base URL: {self.base_url}") self._client = httpx.AsyncClient(timeout=timeout, headers={ "Authorization": f"Bearer {self.token}", "Content-Type": "application/json", "Accept": "application/json", }) async def close(self): """Close the HTTP client and clean up resources.""" logger.info("Closing SmartThings HTTP client") await self._client.aclose() async def _get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: """Make a GET request to the SmartThings API.""" url = f"{self.base_url}{path}" logger.debug(f"GET {url} with params: {params}") r = await self._client.get(url, params=params) if r.status_code >= 400: error_msg = f"GET {url} -> {r.status_code}: {r.text}" logger.error(error_msg) raise SmartThingsError(error_msg) logger.debug(f"GET {url} -> {r.status_code}") return r.json() async def _post(self, path: str, json: Dict[str, Any]) -> Dict[str, Any]: """Make a POST request to the SmartThings API.""" url = f"{self.base_url}{path}" logger.debug(f"POST {url} with body: {json}") r = await self._client.post(url, json=json) if r.status_code >= 400: error_msg = f"POST {url} -> {r.status_code}: {r.text}" logger.error(error_msg) raise SmartThingsError(error_msg) logger.debug(f"POST {url} -> {r.status_code}") return r.json() async def locations(self) -> List[Dict[str, Any]]: """Get all locations for this account.""" logger.info("Fetching locations") data = await self._get("/locations") return data.get("items", []) async def rooms(self, location_id: str) -> List[Dict[str, Any]]: """Get all rooms in a location.""" logger.info(f"Fetching rooms for location: {location_id}") data = await self._get(f"/locations/{location_id}/rooms") return data.get("items", []) async def devices(self, *, location_id: Optional[str] = None) -> List[Dict[str, Any]]: """Get all devices, optionally filtered by location.""" logger.info(f"Fetching devices (location_id={location_id})") params = {"locationId": location_id} if location_id else None data = await self._get("/devices", params=params) return data.get("items", []) async def get_device(self, device_id: str) -> Dict[str, Any]: """Get a single device by ID.""" logger.info(f"Fetching device: {device_id}") return await self._get(f"/devices/{device_id}") async def device_status(self, device_id: str) -> Dict[str, Any]: """Get the current status of a device.""" logger.info(f"Fetching status for device: {device_id}") return await self._get(f"/devices/{device_id}/status") async def device_health(self, device_id: str) -> Dict[str, Any]: """Get the health status of a device.""" logger.info(f"Fetching health for device: {device_id}") try: return await self._get(f"/devices/{device_id}/health") except SmartThingsError as e: logger.warning(f"Could not fetch health for device {device_id}: {e}") return {"state": "unknown"} async def send_commands(self, device_id: str, commands: List[Dict[str, Any]]) -> Dict[str, Any]: """Send commands to a device.""" logger.info(f"Sending commands to device {device_id}: {commands}") body = {"commands": commands} return await self._post(f"/devices/{device_id}/commands", json=body) def flatten_status(status: Dict[str, Any]) -> Dict[str, Any]: """ Flatten nested status dictionary into a simple key-value structure. Converts: {"main": {"switch": {"switch": {"value": "on"}}}} To: {"main.switch.switch": "on"} """ flat: Dict[str, Any] = {} for component, caps in (status or {}).items(): for cap, attrs in (caps or {}).items(): for attr, val in (attrs or {}).items(): if isinstance(val, dict) and "value" in val: flat[f"{component}.{cap}.{attr}"] = val["value"] else: flat[f"{component}.{cap}.{attr}"] = val return flat async def find_device_by_name(client: SmartThingsClient, name: str, *, location_id: Optional[str] = None): """ Search for a device by name (case-insensitive substring match). Args: client: The SmartThings client instance. name: The name or label to search for. location_id: Optional location ID to narrow the search. Returns: The first matching device or None. """ q = (name or "").lower() logger.info(f"Searching for device with name containing: '{q}'") devices = await client.devices(location_id=location_id) for d in devices: label = (d.get("label") or d.get("name") or "").lower() if q == label or q in label: logger.info(f"Found matching device: {d.get('label')} ({d.get('deviceId')})") return d logger.warning(f"No device found matching name: '{name}'") return None

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/bjornhovd/Samsung-SmartThings-MCP'

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