Skip to main content
Glama
smart-scenarios.tool.ts21.1 kB
/** * Smart Scenarios Tool for Home Assistant * * Detects and manages common smart home scenarios: * - Nobody home detection (turn off lights, reduce climate) * - Window open with heating detection * - Motion-based lighting * - Energy saving modes * - Night mode scenarios * - Weather-based climate control */ import { z } from "zod"; import { UserError } from "fastmcp"; import { logger } from "../../utils/logger.js"; import { BaseTool } from "../base-tool.js"; import { MCPContext } from "../../mcp/types.js"; import { get_hass, call_service } from "../../hass/index.js"; import { Tool } from "../../types/index.js"; // Define the schema for smart scenarios const smartScenariosSchema = z.object({ action: z .enum([ "detect_scenarios", "apply_nobody_home", "apply_window_heating_check", "apply_motion_lighting", "apply_energy_saving", "apply_night_mode", "apply_arrival_home", "detect_issues", "create_automation", ]) .describe("The smart scenario action to perform"), mode: z .enum(["detect", "apply", "auto"]) .optional() .default("detect") .describe("detect=only report, apply=execute actions, auto=create automation"), rooms: z.array(z.string()).optional().describe("Specific rooms/areas to apply scenario to"), temperature_reduction: z .number() .min(1) .max(10) .optional() .default(3) .describe("Temperature reduction in degrees for climate control (default: 3)"), enable_notifications: z .boolean() .optional() .default(true) .describe("Send notifications about scenario detection/actions"), }); type SmartScenariosParams = z.infer<typeof smartScenariosSchema>; interface ScenarioDetection { scenario_type: string; detected: boolean; entities_affected: string[]; current_state: string; recommended_action: string; automation_config?: Record<string, unknown>; } interface ScenarioAction { action: string; entity_id: string; reason?: string; from?: number; to?: number; } // Smart Scenarios service class class SmartScenariosService { /** * Detect if nobody is home */ async detectNobodyHome(): Promise<ScenarioDetection> { try { const hass = await get_hass(); const states = await hass.getStates(); // Find person entities and device trackers const personEntities = states.filter((s) => s.entity_id.startsWith("person.")); const deviceTrackers = states.filter((s) => s.entity_id.startsWith("device_tracker.")); const presenceSensors = states.filter( (s) => s.entity_id.includes("presence") || s.entity_id.includes("occupancy"), ); // Check if anyone is home const anyoneHome = [ ...personEntities.filter((p) => p.state === "home"), ...deviceTrackers.filter((d) => d.state === "home"), ...presenceSensors.filter((p) => p.state === "on" || p.state === "detected"), ].length > 0; const nobodyHome = !anyoneHome; // Find lights that are currently on const lightsOn = states.filter((s) => s.entity_id.startsWith("light.") && s.state === "on"); // Find climate devices that are heating/cooling const climateActive = states.filter( (s) => s.entity_id.startsWith("climate.") && (s.state === "heat" || s.state === "cool" || s.state === "heat_cool"), ); return { scenario_type: "nobody_home", detected: nobodyHome, entities_affected: [ ...lightsOn.map((l) => l.entity_id), ...climateActive.map((c) => c.entity_id), ], current_state: nobodyHome ? `Nobody home detected. ${lightsOn.length} lights on, ${climateActive.length} climate devices active.` : "Someone is home.", recommended_action: nobodyHome ? "Turn off all lights and reduce climate to eco mode" : "No action needed", automation_config: nobodyHome ? this.generateNobodyHomeAutomation(lightsOn, climateActive) : undefined, }; } catch (error) { logger.error("Failed to detect nobody home scenario:", error); throw error; } } /** * Detect window open with heating */ async detectWindowHeatingConflict(): Promise<ScenarioDetection[]> { try { const hass = await get_hass(); const states = await hass.getStates(); // Find window/door sensors const windowSensors = states.filter( (s) => (s.entity_id.includes("window") || s.entity_id.includes("door")) && (s.attributes.device_class === "window" || s.attributes.device_class === "door" || s.attributes.device_class === "opening") && (s.state === "on" || s.state === "open"), ); // Find climate devices that are heating const climateHeating = states.filter( (s) => s.entity_id.startsWith("climate.") && (s.state === "heat" || s.state === "auto") && s.attributes.current_temperature < s.attributes.temperature, ); const conflicts: ScenarioDetection[] = []; // Check for conflicts by room/area for (const climate of climateHeating) { const climateArea = ((climate.attributes.area_id as string | undefined) ?? "") || this.extractArea(climate.entity_id); // Find windows in the same area const windowsInArea = windowSensors.filter((w) => { const windowArea = ((w.attributes.area_id as string | undefined) ?? "") || this.extractArea(w.entity_id); return windowArea === climateArea; }); if (windowsInArea.length > 0) { conflicts.push({ scenario_type: "window_heating_conflict", detected: true, entities_affected: [climate.entity_id, ...windowsInArea.map((w) => w.entity_id)], current_state: `Window open in ${climateArea} while heating is active (target: ${climate.attributes.temperature}°C, current: ${climate.attributes.current_temperature}°C)`, recommended_action: `Turn off climate.${climate.entity_id.split(".")[1]} while window is open`, automation_config: this.generateWindowHeatingAutomation(climate, windowsInArea), }); } } return conflicts; } catch (error) { logger.error("Failed to detect window heating conflicts:", error); throw error; } } /** * Detect energy saving opportunities */ async detectEnergySavingOpportunities(): Promise<ScenarioDetection[]> { try { const hass = await get_hass(); const states = await hass.getStates(); const opportunities: ScenarioDetection[] = []; // Find lights on during daytime const currentHour = new Date().getHours(); const isDaytime = currentHour >= 8 && currentHour <= 18; if (isDaytime) { const lightsOn = states.filter((s) => s.entity_id.startsWith("light.") && s.state === "on"); if (lightsOn.length > 0) { opportunities.push({ scenario_type: "daytime_lights", detected: true, entities_affected: lightsOn.map((l) => l.entity_id), current_state: `${lightsOn.length} lights on during daytime`, recommended_action: "Consider using daylight sensors or turning off unnecessary lights", automation_config: this.generateDaylightAutomation(lightsOn), }); } } // Find devices in standby consuming power const powerSensors = states.filter( (s) => s.attributes.device_class === "power" && s.attributes.unit_of_measurement === "W", ); const standbyDevices = powerSensors.filter((s) => { const power = parseFloat(s.state); return !isNaN(power) && power > 0 && power < 10; // Standby typically 0-10W }); if (standbyDevices.length > 0) { opportunities.push({ scenario_type: "standby_power", detected: true, entities_affected: standbyDevices.map((d) => d.entity_id), current_state: `${standbyDevices.length} devices consuming standby power`, recommended_action: "Use smart plugs to completely turn off devices when not in use", }); } // Find climate devices set too high/low const climateDevices = states.filter((s) => s.entity_id.startsWith("climate.")); for (const climate of climateDevices) { const targetTemp = climate.attributes.temperature as number | undefined; const currentTemp = climate.attributes.current_temperature as number | undefined; if (targetTemp !== undefined && currentTemp !== undefined) { const isHeating = climate.state === "heat" && targetTemp > 23; const isCooling = climate.state === "cool" && targetTemp < 20; if (isHeating || isCooling) { opportunities.push({ scenario_type: "inefficient_climate", detected: true, entities_affected: [climate.entity_id], current_state: `${climate.entity_id} set to ${targetTemp}°C (${climate.state})`, recommended_action: isHeating ? "Reduce heating target to 20-22°C to save energy" : "Increase cooling target to 24-26°C to save energy", }); } } } return opportunities; } catch (error) { logger.error("Failed to detect energy saving opportunities:", error); throw error; } } /** * Apply nobody home scenario */ async applyNobodyHomeScenario(params: SmartScenariosParams): Promise<{ success: boolean; actions_taken: ScenarioAction[]; message: string; }> { try { const hass = await get_hass(); const states = await hass.getStates(); const actions: ScenarioAction[] = []; // Turn off all lights const lightsOn = states.filter((s) => s.entity_id.startsWith("light.") && s.state === "on"); for (const light of lightsOn) { if (!params.rooms || this.isInRooms(light, params.rooms)) { await call_service("light", "turn_off", { entity_id: light.entity_id }); actions.push({ action: "turned_off", entity_id: light.entity_id }); } } // Set climate to eco mode or reduce temperature const climateDevices = states.filter((s) => s.entity_id.startsWith("climate.")); for (const climate of climateDevices) { if (!params.rooms || this.isInRooms(climate, params.rooms)) { const currentTemp = climate.attributes.temperature as number | undefined; // Check if eco_mode preset is available const presets = (climate.attributes.preset_modes as string[] | undefined) || []; if (Array.isArray(presets) && presets.includes("eco")) { await call_service("climate", "set_preset_mode", { entity_id: climate.entity_id, preset_mode: "eco", }); actions.push({ action: "set_eco_mode", entity_id: climate.entity_id }); } else if (currentTemp !== undefined) { // Reduce temperature const newTemp = currentTemp - (params.temperature_reduction || 3); await call_service("climate", "set_temperature", { entity_id: climate.entity_id, temperature: newTemp, }); actions.push({ action: "reduced_temperature", entity_id: climate.entity_id, from: currentTemp, to: newTemp, }); } } } // Send notification if enabled if (params.enable_notifications) { await call_service("notify", "notify", { message: `Nobody home mode activated. Turned off ${lightsOn.length} lights and adjusted ${climateDevices.length} climate devices.`, title: "🏠 Smart Home: Nobody Home", }); } return { success: true, actions_taken: actions, message: `Applied nobody home scenario. Affected ${actions.length} entities.`, }; } catch (error) { logger.error("Failed to apply nobody home scenario:", error); throw error; } } /** * Apply window heating check */ async applyWindowHeatingCheck(params: SmartScenariosParams): Promise<{ conflicts_found: number; conflicts: ScenarioDetection[]; actions_taken: ScenarioAction[]; mode: string; }> { try { const conflicts = await this.detectWindowHeatingConflict(); const actions: ScenarioAction[] = []; if (params.mode === "apply") { for (const conflict of conflicts) { // Turn off climate devices const climateEntity = conflict.entities_affected.find((e) => e.startsWith("climate.")); if (climateEntity !== undefined && climateEntity.length > 0) { await call_service("climate", "turn_off", { entity_id: climateEntity }); actions.push({ action: "turned_off_climate", entity_id: climateEntity, reason: "window_open", }); } } if (params.enable_notifications && conflicts.length > 0) { await call_service("notify", "notify", { message: `Turned off ${actions.length} climate devices due to open windows.`, title: "🪟 Smart Home: Window/Heating Conflict", }); } } return { conflicts_found: conflicts.length, conflicts: conflicts, actions_taken: actions, mode: params.mode ?? "detect", }; } catch (error) { logger.error("Failed to apply window heating check:", error); throw error; } } // Helper methods private extractArea(entityId: string): string { // Try to extract area from entity_id naming convention // e.g., climate.living_room -> living_room const parts = entityId.split("."); if (parts.length > 1) { return parts[1].replace(/_/g, " "); } return "unknown"; } private isInRooms( entity: { entity_id: string; attributes: Record<string, unknown> }, rooms: string[], ): boolean { const area = ((entity.attributes.area_id as string | undefined) ?? "") || this.extractArea(entity.entity_id); return rooms.some((room) => area.toLowerCase().includes(room.toLowerCase())); } // Automation generation helpers private generateNobodyHomeAutomation( lights: Array<{ entity_id: string }>, climate: Array<{ entity_id: string }>, ): Record<string, unknown> { return { alias: "Nobody Home - Auto Actions", description: "Automatically turn off lights and reduce climate when nobody is home", trigger: [ { platform: "state", entity_id: "zone.home", to: "0", for: { minutes: 5 }, }, ], condition: [], action: [ { service: "light.turn_off", target: { entity_id: lights.map((l) => l.entity_id), }, }, { service: "climate.set_preset_mode", target: { entity_id: climate.map((c) => c.entity_id), }, data: { preset_mode: "eco", }, }, { service: "notify.notify", data: { message: "Nobody home mode activated", title: "Smart Home", }, }, ], mode: "single", }; } private generateWindowHeatingAutomation( climate: { entity_id: string }, windows: Array<{ entity_id: string }>, ): Record<string, unknown> { return { alias: `Window Open Climate Control - ${this.extractArea(climate.entity_id)}`, description: "Turn off heating when window opens, restore when closed", trigger: windows.map((w) => ({ platform: "state", entity_id: w.entity_id, to: "on", })), condition: [ { condition: "state", entity_id: climate.entity_id, state: ["heat", "auto"], }, ], action: [ { service: "climate.turn_off", target: { entity_id: climate.entity_id, }, }, { wait_for_trigger: windows.map((w) => ({ platform: "state", entity_id: w.entity_id, to: "off", for: { minutes: 2 }, })), timeout: { hours: 4 }, }, { service: "climate.turn_on", target: { entity_id: climate.entity_id, }, }, ], mode: "restart", }; } private generateDaylightAutomation( lights: Array<{ entity_id: string }>, ): Record<string, unknown> { return { alias: "Daylight Savings - Turn off lights", description: "Turn off lights during bright daylight hours", trigger: [ { platform: "sun", event: "sunrise", offset: "+01:00:00", }, ], condition: [], action: [ { service: "light.turn_off", target: { entity_id: lights.map((l) => l.entity_id), }, }, ], mode: "single", }; } } // Singleton instance const smartScenariosService = new SmartScenariosService(); // Execute smart scenarios logic async function executeSmartScenariosLogic(params: SmartScenariosParams): Promise<string> { logger.debug(`Executing smart scenarios action: ${params.action}`); switch (params.action) { case "detect_scenarios": { const nobodyHome = await smartScenariosService.detectNobodyHome(); const windowConflicts = await smartScenariosService.detectWindowHeatingConflict(); const energySaving = await smartScenariosService.detectEnergySavingOpportunities(); return JSON.stringify( { action: params.action, timestamp: new Date().toISOString(), scenarios: { nobody_home: nobodyHome, window_heating_conflicts: windowConflicts, energy_saving_opportunities: energySaving, }, summary: { total_issues: 1 + windowConflicts.length + energySaving.length, nobody_home_detected: nobodyHome.detected, window_conflicts: windowConflicts.length, energy_opportunities: energySaving.length, }, }, null, 2, ); } case "apply_nobody_home": { const result = await smartScenariosService.applyNobodyHomeScenario(params); return JSON.stringify(result, null, 2); } case "apply_window_heating_check": { const result = await smartScenariosService.applyWindowHeatingCheck(params); return JSON.stringify(result, null, 2); } case "detect_issues": { const windowConflicts = await smartScenariosService.detectWindowHeatingConflict(); const energySaving = await smartScenariosService.detectEnergySavingOpportunities(); return JSON.stringify( { action: params.action, timestamp: new Date().toISOString(), issues: [...windowConflicts, ...energySaving], total_issues: windowConflicts.length + energySaving.length, }, null, 2, ); } case "create_automation": { throw new UserError( "Automatic automation creation is not yet implemented. " + "Use the 'detect_scenarios' action to get automation configs, " + "then create them manually or use the automation_config tool.", ); } default: throw new UserError( `Action ${params.action} is not yet implemented. Available: detect_scenarios, apply_nobody_home, apply_window_heating_check, detect_issues`, ); } } // Export the tool export const smartScenariosTool: Tool = { name: "smart_scenarios", description: "Detect and manage smart home scenarios: nobody home, window/heating conflicts, energy saving, and more", parameters: smartScenariosSchema, execute: executeSmartScenariosLogic, }; // Export class for compatibility export class SmartScenariosTool extends BaseTool { constructor() { super({ name: smartScenariosTool.name, description: smartScenariosTool.description, parameters: smartScenariosSchema, metadata: { category: "automation", version: "1.0.0", tags: ["smart_home", "scenarios", "automation", "energy"], }, }); } public async execute(params: SmartScenariosParams, context: MCPContext): Promise<string> { logger.debug(`Executing SmartScenariosTool with params: ${JSON.stringify(params)}`); try { const validatedParams = this.validateParams(params); return await executeSmartScenariosLogic(validatedParams); } catch (error) { logger.error(`Error in SmartScenariosTool: ${String(error)}`); throw error; } } }

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/jango-blockchained/advanced-homeassistant-mcp'

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