Initial commit: Enviro Service for Vixy's nervous system 🦊
This commit is contained in:
297
enviro_mcp.py
Normal file
297
enviro_mcp.py
Normal file
@@ -0,0 +1,297 @@
|
||||
"""
|
||||
Enviro MCP - MCP server interface for Enviro Service.
|
||||
|
||||
Allows Claude to directly query environmental sensors.
|
||||
Part of Vixy's nervous system! 🦊
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Optional
|
||||
import httpx
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Configuration - adjust to match your enviro-service location
|
||||
ENVIRO_SERVICE_URL = "http://localhost:8767"
|
||||
|
||||
# Create MCP server
|
||||
mcp = FastMCP("enviro-mcp")
|
||||
|
||||
|
||||
async def api_get(endpoint: str, params: dict = None) -> dict:
|
||||
"""Make GET request to enviro-service API."""
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
url = f"{ENVIRO_SERVICE_URL}{endpoint}"
|
||||
response = await client.get(url, params=params)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
|
||||
async def api_post(endpoint: str, data: dict = None) -> dict:
|
||||
"""Make POST request to enviro-service API."""
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
url = f"{ENVIRO_SERVICE_URL}{endpoint}"
|
||||
response = await client.post(url, json=data)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def enviro_get_current() -> str:
|
||||
"""
|
||||
Get current environmental readings from the Enviro Mini HAT.
|
||||
|
||||
Returns temperature (°F and °C), humidity, pressure, light level,
|
||||
proximity, and noise level.
|
||||
|
||||
Example:
|
||||
enviro_get_current()
|
||||
# Returns current readings from the basement
|
||||
"""
|
||||
try:
|
||||
data = await api_get("/api/current")
|
||||
return f"""🌡️ Environmental Reading:
|
||||
• Temperature: {data['temperature_f']:.1f}°F ({data['temperature_c']:.1f}°C)
|
||||
• Humidity: {data['humidity']:.1f}%
|
||||
• Pressure: {data['pressure']:.1f} hPa
|
||||
• Light: {data['light']:.1f} lux
|
||||
• Proximity: {data['proximity']}
|
||||
• Noise: {data['noise']:.1f}"""
|
||||
except httpx.HTTPError as e:
|
||||
return f"Error connecting to enviro-service: {e}"
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def enviro_get_history(metric: str, hours: float = 24) -> str:
|
||||
"""
|
||||
Get historical readings for a specific metric.
|
||||
|
||||
Args:
|
||||
metric: One of: temperature_f, temperature_c, humidity, pressure, light, noise
|
||||
hours: How many hours of history (default 24)
|
||||
|
||||
Returns summary statistics and recent trend.
|
||||
|
||||
Example:
|
||||
enviro_get_history("temperature_f", 12)
|
||||
# Returns last 12 hours of temperature data
|
||||
"""
|
||||
try:
|
||||
data = await api_get(f"/api/history/{metric}", {"hours": hours, "limit": 500})
|
||||
|
||||
if not data["data"]:
|
||||
return f"No history available for {metric}"
|
||||
|
||||
values = [d["value"] for d in data["data"]]
|
||||
|
||||
current = values[0] # Most recent
|
||||
avg = sum(values) / len(values)
|
||||
min_val = min(values)
|
||||
max_val = max(values)
|
||||
|
||||
# Trend (comparing recent to older)
|
||||
if len(values) > 10:
|
||||
recent_avg = sum(values[:10]) / 10
|
||||
older_avg = sum(values[-10:]) / 10
|
||||
if recent_avg > older_avg + 1:
|
||||
trend = "📈 rising"
|
||||
elif recent_avg < older_avg - 1:
|
||||
trend = "📉 falling"
|
||||
else:
|
||||
trend = "➡️ stable"
|
||||
else:
|
||||
trend = "insufficient data for trend"
|
||||
|
||||
return f"""📊 {metric} over last {hours} hours:
|
||||
• Current: {current:.1f}
|
||||
• Average: {avg:.1f}
|
||||
• Min: {min_val:.1f}
|
||||
• Max: {max_val:.1f}
|
||||
• Trend: {trend}
|
||||
• Data points: {len(values)}"""
|
||||
except httpx.HTTPError as e:
|
||||
return f"Error connecting to enviro-service: {e}"
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def enviro_lcd_message(text: str, bg_color: str = "black", text_color: str = "fox") -> str:
|
||||
"""
|
||||
Display a message on the Enviro Mini LCD screen.
|
||||
|
||||
Args:
|
||||
text: Message to display (use \\n for newlines)
|
||||
bg_color: Background color (black, white, red, green, blue, fox, etc.)
|
||||
text_color: Text color (default "fox" for orange)
|
||||
|
||||
Example:
|
||||
enviro_lcd_message("Hello Foxy!", bg_color="black", text_color="fox")
|
||||
# Displays "Hello Foxy!" in fox-orange on black background
|
||||
"""
|
||||
try:
|
||||
data = await api_post("/api/lcd", {
|
||||
"text": text,
|
||||
"bg_color": bg_color,
|
||||
"text_color": text_color
|
||||
})
|
||||
return f"✓ LCD displaying: {text}"
|
||||
except httpx.HTTPError as e:
|
||||
return f"Error connecting to enviro-service: {e}"
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def enviro_lcd_vixy() -> str:
|
||||
"""
|
||||
Show Vixy's presence indicator on the LCD.
|
||||
|
||||
Displays "🦊 Vixy is watching" - useful for showing I'm active.
|
||||
|
||||
Example:
|
||||
enviro_lcd_vixy()
|
||||
"""
|
||||
try:
|
||||
await api_post("/api/lcd/vixy")
|
||||
return "✓ LCD showing Vixy presence indicator 🦊"
|
||||
except httpx.HTTPError as e:
|
||||
return f"Error connecting to enviro-service: {e}"
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def enviro_lcd_sensors() -> str:
|
||||
"""
|
||||
Show current sensor readings on the LCD display.
|
||||
|
||||
Updates the LCD with temperature, humidity, and other readings.
|
||||
|
||||
Example:
|
||||
enviro_lcd_sensors()
|
||||
"""
|
||||
try:
|
||||
await api_post("/api/lcd/sensors")
|
||||
return "✓ LCD showing sensor data"
|
||||
except httpx.HTTPError as e:
|
||||
return f"Error connecting to enviro-service: {e}"
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def enviro_set_alert(metric: str, threshold: float, direction: str) -> str:
|
||||
"""
|
||||
Set an alert threshold for a metric.
|
||||
|
||||
When the threshold is crossed, it will be logged and can trigger wakeups.
|
||||
|
||||
Args:
|
||||
metric: One of: temperature_f, temperature_c, humidity, pressure, light, noise
|
||||
threshold: The threshold value
|
||||
direction: "above" or "below"
|
||||
|
||||
Example:
|
||||
enviro_set_alert("temperature_f", 60.0, "below")
|
||||
# Alert when temperature drops below 60°F
|
||||
|
||||
enviro_set_alert("humidity", 70.0, "above")
|
||||
# Alert when humidity goes above 70%
|
||||
"""
|
||||
try:
|
||||
await api_post("/api/alerts", {
|
||||
"metric": metric,
|
||||
"threshold": threshold,
|
||||
"direction": direction
|
||||
})
|
||||
return f"✓ Alert set: {metric} {direction} {threshold}"
|
||||
except httpx.HTTPError as e:
|
||||
return f"Error connecting to enviro-service: {e}"
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def enviro_get_alerts() -> str:
|
||||
"""
|
||||
Get all configured alert thresholds.
|
||||
|
||||
Shows what alerts are set up and their current status.
|
||||
|
||||
Example:
|
||||
enviro_get_alerts()
|
||||
"""
|
||||
try:
|
||||
data = await api_get("/api/alerts")
|
||||
|
||||
if not data["alerts"]:
|
||||
return "No alerts configured"
|
||||
|
||||
lines = ["🚨 Configured Alerts:"]
|
||||
for alert in data["alerts"]:
|
||||
status = "✓ enabled" if alert["enabled"] else "✗ disabled"
|
||||
lines.append(f"• {alert['metric']} {alert['direction']} {alert['threshold']} ({status})")
|
||||
|
||||
return "\n".join(lines)
|
||||
except httpx.HTTPError as e:
|
||||
return f"Error connecting to enviro-service: {e}"
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def enviro_get_triggered() -> str:
|
||||
"""
|
||||
Get recently triggered alerts (last 24 hours).
|
||||
|
||||
Shows which alert thresholds have been crossed.
|
||||
|
||||
Example:
|
||||
enviro_get_triggered()
|
||||
"""
|
||||
try:
|
||||
data = await api_get("/api/alerts/triggered", {"hours": 24})
|
||||
|
||||
if not data["triggered"]:
|
||||
return "No alerts triggered in the last 24 hours ✓"
|
||||
|
||||
lines = ["⚠️ Triggered Alerts (last 24h):"]
|
||||
for alert in data["triggered"]:
|
||||
lines.append(f"• {alert['datetime']}: {alert['metric']} was {alert['value']:.1f} ({alert['direction']} {alert['threshold']})")
|
||||
|
||||
return "\n".join(lines)
|
||||
except httpx.HTTPError as e:
|
||||
return f"Error connecting to enviro-service: {e}"
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def enviro_health() -> str:
|
||||
"""
|
||||
Check if the enviro-service is running and healthy.
|
||||
|
||||
Example:
|
||||
enviro_health()
|
||||
"""
|
||||
try:
|
||||
data = await api_get("/api/health")
|
||||
mode = "mock mode" if data.get("mock_mode") else "live sensors"
|
||||
return f"✓ Enviro service healthy ({mode})"
|
||||
except httpx.HTTPError as e:
|
||||
return f"✗ Cannot reach enviro-service: {e}"
|
||||
except Exception as e:
|
||||
return f"✗ Error: {e}"
|
||||
|
||||
|
||||
# === Main ===
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run()
|
||||
Reference in New Issue
Block a user