Skip to main content
Glama
alexa-dynamic.ts12.4 kB
import { AccountInfoSchema, EndpointsDiscoverySchema, PhoenixDevicesSchema, SmartHomeFavoritesSchema, } from "@/schemas/alexa"; import type { Env } from "@/types/alexa"; import { buildAlexaHeaders } from "@/utils/alexa"; // Cache for dynamic values to avoid repeated API calls const cache = new Map<string, { value: any; timestamp: number }>(); const CACHE_TTL = 5 * 60 * 1000; // 5 minutes function getCached<T>(key: string): T | null { const cached = cache.get(key); if (cached && Date.now() - cached.timestamp < CACHE_TTL) { return cached.value as T; } return null; } function setCache(key: string, value: any) { cache.set(key, { value, timestamp: Date.now() }); } // Fetch account information dynamically export async function getAccountInfo(env: Env) { const cacheKey = "account_info"; const cached = getCached<{ customerId: string; profiles: any[] }>(cacheKey); if (cached) return cached; const response = await fetch("https://alexa-comms-mobile-service.amazon.com/accounts", { method: "GET", headers: buildAlexaHeaders(env), }); if (!response.ok) { throw new Error(`Failed to fetch account info: ${response.status}`); } const data = await response.json(); const validatedData = AccountInfoSchema.parse(data); // Get the first account (should be the signed-in user) const primaryAccount = validatedData.find((account) => account.signedInUser) || validatedData[0]; if (!primaryAccount) { throw new Error("No account found in response"); } const accountInfo = { customerId: primaryAccount.directedId, profiles: [], // Not available in this simpler endpoint }; setCache(cacheKey, accountInfo); return accountInfo; } // Fetch all Alexa devices/endpoints export async function getAlexaEndpoints(env: Env) { const cacheKey = "alexa_endpoints"; const cached = getCached<any[]>(cacheKey); if (cached) return cached; const response = await fetch("https://alexa.amazon.com/api/smarthome/v2/endpoints", { method: "POST", headers: buildAlexaHeaders(env, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache", }), body: JSON.stringify({ endpointContexts: ["GROUP"] }), }); if (!response.ok) { throw new Error(`Failed to fetch endpoints: ${response.status}`); } const data = await response.json(); const validatedData = EndpointsDiscoverySchema.parse(data); setCache(cacheKey, validatedData.endpoints); return validatedData.endpoints; } // Fetch registered devices export async function getAlexaDevices(env: Env) { const cacheKey = "alexa_devices"; const cached = getCached<any[]>(cacheKey); if (cached) return cached; const response = await fetch("https://alexa.amazon.com/api/devices-v2/device?cached=true", { method: "GET", headers: buildAlexaHeaders(env, { Accept: "application/json; charset=utf-8", "Cache-Control": "no-cache", }), }); if (!response.ok) { throw new Error(`Failed to fetch devices: ${response.status}`); } const data = await response.json(); const validatedData = PhoenixDevicesSchema.parse(data); setCache(cacheKey, validatedData.devices); return validatedData.devices; } // Get primary Echo device for announcements export async function getPrimaryEchoDevice(env: Env) { const devices = await getAlexaDevices(env); // Look for Echo devices (ECHO family) const echoDevices = devices.filter( (device: any) => device.deviceFamily === "ECHO" && device.online, ); if (echoDevices.length === 0) { throw new Error("No online Echo devices found"); } // Return the first available Echo device return echoDevices[0]; } // Get primary smart home device for media queries export async function getPrimaryMediaDevice(env: Env) { const devices = await getAlexaDevices(env); // Look for devices that support media capabilities const mediaDevices = devices.filter( (device: any) => device.capabilities?.includes("AUDIO_PLAYER") && device.online, ); if (mediaDevices.length === 0) { // Fallback to any online device const onlineDevices = devices.filter((device: any) => device.online); if (onlineDevices.length === 0) { throw new Error("No online devices found"); } return onlineDevices[0]; } return mediaDevices[0]; } // Get favorites (main smart home devices) - more reliable than endpoints export async function getSmartHomeFavorites(env: Env) { const cacheKey = "smart_home_favorites"; const cached = getCached<any[]>(cacheKey); if (cached) return cached; const query = ` fragment FavoriteMetadata on Favorite { resource { id __typename } favoriteFriendlyName displayInfo { displayCategories { primary { isCustomerSpecified isDiscovered value sources __typename } all { isCustomerSpecified isDiscovered value sources __typename } __typename } __typename } alternateIdentifiers { legacyIdentifiers { chrsIdentifier { entityId __typename } dmsIdentifier { deviceSerialNumber { type value { text __typename } __typename } deviceType { type value { text __typename } __typename } __typename } __typename } __typename } type rank active variant __typename } query ListFavoritesForHomeChannel($requestedTypes: [String!]) { favorites(listFavoritesInput: {requestedTypes: $requestedTypes}) { favorites { ...FavoriteMetadata __typename } __typename } } `; const response = await fetch("https://alexa.amazon.com/nexus/v1/graphql", { method: "POST", headers: buildAlexaHeaders(env, { "Content-Type": "application/json", "X-Amzn-Marketplace-Id": "ATVPDKIKX0DER", "X-Amzn-Client": "AlexaApp", "X-Amzn-Os-Name": "android", }), body: JSON.stringify({ operationName: "ListFavoritesForHomeChannel", variables: { requestedTypes: [ "AEA", "ALEXA_LIST", "AWAY_LIGHTING", "DEVICE_SHORTCUT", "DTG", "ENDPOINT", "SHORTCUT", "STATIC_ENTERTAINMENT", ], }, query, }), }); if (!response.ok) { throw new Error(`Failed to fetch favorites: ${response.status}`); } const data = await response.json(); const validatedData = SmartHomeFavoritesSchema.parse(data); const favorites = validatedData.data.favorites.favorites; setCache(cacheKey, favorites); return favorites; } // Find smart home entities (lights, sensors, etc.) using favorites API export async function getSmartHomeEntities(env: Env) { const favorites = await getSmartHomeFavorites(env); // Filter for active smart home devices const smartHomeDevices = favorites.filter((favorite: any) => { return favorite.active && favorite.type === "ENDPOINT"; }); return smartHomeDevices; } // Get the primary light entity dynamically (first available light) export async function getPrimaryLight(env: Env) { const smartHomeDevices = await getSmartHomeEntities(env); // Look for devices that are lights const lightDevices = smartHomeDevices.filter((device: any) => { const primaryCategory = device.displayInfo?.displayCategories?.primary?.value; return primaryCategory === "LIGHT"; }); if (lightDevices.length === 0) { throw new Error("No smart home light devices found"); } // Return the first light device return lightDevices[0]; } // Get all device entity IDs for state requests export async function getAllDeviceEntityIds(env: Env) { const [endpoints, devices] = await Promise.all([getAlexaEndpoints(env), getAlexaDevices(env)]); const entityIds: Array<{ entityId: string; entityType: string }> = []; // Add Echo devices for temperature/illuminance sensors devices.forEach((device: any) => { if (device.online) { // Add bridge entities for Echo devices entityIds.push({ entityId: `AlexaBridge_${device.serialNumber}@${device.deviceType}_${device.serialNumber}`, entityType: "APPLIANCE", }); } }); // Add smart home endpoints endpoints.forEach((endpoint: any) => { const entityId = endpoint.identifier?.entityId || endpoint.serialNumber; if (entityId) { entityIds.push({ entityId, entityType: "APPLIANCE", }); } }); return entityIds; } // Get customer smart home endpoints using GraphQL export async function getCustomerSmartHomeEndpoints(env: Env) { const cacheKey = "customer_smart_home_endpoints"; const cached = getCached<any[]>(cacheKey); if (cached) return cached; const query = ` query CustomerSmartHome { endpoints( endpointsQueryParams: { paginationParams: { disablePagination: true } } ) { items { endpointId id friendlyName displayCategories { all { value } primary { value } } legacyIdentifiers { chrsIdentifier { entityId } dmsIdentifier { deviceType { type value { text } } deviceSerialNumber { type value { text } } } } legacyAppliance { applianceId applianceTypes friendlyName entityId mergedApplianceIds capabilities } } } } `; const response = await fetch("https://alexa.amazon.com/nexus/v1/graphql", { method: "POST", headers: buildAlexaHeaders(env, { "Content-Type": "application/json", "X-Amzn-Marketplace-Id": "ATVPDKIKX0DER", "X-Amzn-Client": "AlexaApp", "X-Amzn-Os-Name": "android", }), body: JSON.stringify({ query }), }); if (!response.ok) { throw new Error(`Failed to fetch customer smart home endpoints: ${response.status}`); } const data = (await response.json()) as any; const endpoints = data.data?.endpoints?.items || []; setCache(cacheKey, endpoints); return endpoints; } // Get Echo device entity ID for sensors (temperature, illuminance) export async function getEchoDeviceEntityId(env: Env): Promise<string> { const endpoints = await getCustomerSmartHomeEndpoints(env); // Find Echo device (ALEXA_VOICE_ENABLED category) const echoDevice = endpoints.find((endpoint: any) => { const primaryCategory = endpoint.displayCategories?.primary?.value; return primaryCategory === "ALEXA_VOICE_ENABLED"; }); if (!echoDevice) { throw new Error("No Echo device found for sensors"); } // For sensor data, we need the entity ID, not appliance ID return echoDevice.legacyIdentifiers?.chrsIdentifier?.entityId || echoDevice.entityId; } // Get light appliance ID for state requests (different from entity ID) export async function getLightApplianceId(env: Env): Promise<string> { const endpoints = await getCustomerSmartHomeEndpoints(env); // Find light device const lightDevice = endpoints.find((endpoint: any) => { const primaryCategory = endpoint.displayCategories?.primary?.value; return primaryCategory === "LIGHT"; }); if (!lightDevice) { throw new Error("No light device found"); } // Return the appliance ID from legacyAppliance return lightDevice.legacyAppliance?.applianceId; } // Helper to extract entity ID from smart home device export function extractEntityId(device: any): string { // For favorites API response, entity ID is in alternateIdentifiers if (device.alternateIdentifiers?.legacyIdentifiers?.chrsIdentifier?.entityId) { return device.alternateIdentifiers.legacyIdentifiers.chrsIdentifier.entityId; } // For other API responses, check identifier if (device.identifier?.entityId) { return device.identifier.entityId; } // Extract from resource.id if available (favorites format) if (device.resource?.id?.includes("endpoint.")) { return device.resource.id.replace("amzn1.alexa.endpoint.", ""); } // Fallback to serial number return device.serialNumber || device.resource?.id; } // Helper to build endpoint ID from entity ID export function buildEndpointId(entityId: string): string { if (entityId.startsWith("amzn1.alexa.endpoint.")) { return entityId; } return `amzn1.alexa.endpoint.${entityId}`; }

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/guitarbeat/alexa-mcp-server'

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