Init commit
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
HA_URL=http://192.168.1.141:8123
|
||||
HA_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiI1N2M1MmY4OGViNDI0MmEzOWM4ZGVkYzY5NjA0ZjAyYiIsImlhdCI6MTc5MTI3NDY5OSwiZXhwIjoyMTA2NjM0Njk5fQ.JHFxqul7PxyoC8X9FgPYnEGYuuFycDw_Af4EOTytG7c
|
||||
HA_ALLOWED_DOMAINS=light,switch,cover,climate,sensor,binary_sensor,media_player,calendar
|
||||
@@ -0,0 +1,3 @@
|
||||
HA_URL=http://homeassistant.local:8123
|
||||
HA_TOKEN=CHANGE_ME
|
||||
HA_ALLOWED_DOMAINS=light,switch,cover,climate,sensor,binary_sensor,media_player,calendar
|
||||
@@ -0,0 +1,3 @@
|
||||
# MCP_home_assistant
|
||||
|
||||
Copier `.env.example` vers `.env`, puis créer le venv et installer `requirements.txt`.
|
||||
@@ -0,0 +1,245 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import requests
|
||||
from dotenv import load_dotenv
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent
|
||||
load_dotenv(BASE_DIR / ".env")
|
||||
|
||||
HA_URL = os.getenv("HA_URL", "http://192.168.1.141:8123").rstrip("/")
|
||||
HA_TOKEN = os.getenv("HA_TOKEN")
|
||||
|
||||
if not HA_TOKEN:
|
||||
raise RuntimeError("HA_TOKEN manquant dans /opt/hermes/ha-mcp/.env")
|
||||
|
||||
HEADERS = {
|
||||
"Authorization": f"Bearer {HA_TOKEN}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
mcp = FastMCP("home-assistant")
|
||||
|
||||
|
||||
def ha_get(path: str) -> Any:
|
||||
response = requests.get(
|
||||
f"{HA_URL}{path}",
|
||||
headers=HEADERS,
|
||||
timeout=10,
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
|
||||
def ha_post(path: str, payload: Optional[Dict[str, Any]] = None) -> Any:
|
||||
response = requests.post(
|
||||
f"{HA_URL}{path}",
|
||||
headers=HEADERS,
|
||||
json=payload or {},
|
||||
timeout=10,
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
if response.text:
|
||||
return response.json()
|
||||
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def ha_check_api() -> Any:
|
||||
"""Check if Home Assistant API is reachable."""
|
||||
return ha_get("/api/")
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def ha_list_entities(domain: Optional[str] = None) -> Any:
|
||||
"""
|
||||
List Home Assistant entities.
|
||||
|
||||
Optional domain examples:
|
||||
- light
|
||||
- switch
|
||||
- sensor
|
||||
- binary_sensor
|
||||
- automation
|
||||
- script
|
||||
"""
|
||||
states = ha_get("/api/states")
|
||||
|
||||
if domain:
|
||||
prefix = f"{domain}."
|
||||
states = [
|
||||
entity for entity in states
|
||||
if entity.get("entity_id", "").startswith(prefix)
|
||||
]
|
||||
|
||||
return [
|
||||
{
|
||||
"entity_id": entity.get("entity_id"),
|
||||
"state": entity.get("state"),
|
||||
"friendly_name": entity.get("attributes", {}).get("friendly_name"),
|
||||
}
|
||||
for entity in states
|
||||
]
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def ha_search_entities(search: str) -> Any:
|
||||
"""
|
||||
Search Home Assistant entities by entity_id or friendly name.
|
||||
"""
|
||||
search_lower = search.lower()
|
||||
states = ha_get("/api/states")
|
||||
|
||||
results = []
|
||||
|
||||
for entity in states:
|
||||
entity_id = entity.get("entity_id", "")
|
||||
friendly_name = entity.get("attributes", {}).get("friendly_name", "")
|
||||
|
||||
if (
|
||||
search_lower in entity_id.lower()
|
||||
or search_lower in friendly_name.lower()
|
||||
):
|
||||
results.append(
|
||||
{
|
||||
"entity_id": entity_id,
|
||||
"state": entity.get("state"),
|
||||
"friendly_name": friendly_name,
|
||||
}
|
||||
)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def ha_get_state(entity_id: str) -> Any:
|
||||
"""Get the current state of a Home Assistant entity."""
|
||||
return ha_get(f"/api/states/{entity_id}")
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def ha_turn_on(entity_id: str) -> Any:
|
||||
"""
|
||||
Turn on a Home Assistant entity.
|
||||
|
||||
Works with domains such as:
|
||||
- light
|
||||
- switch
|
||||
- fan
|
||||
- input_boolean
|
||||
"""
|
||||
domain = entity_id.split(".")[0]
|
||||
return ha_post(
|
||||
f"/api/services/{domain}/turn_on",
|
||||
{"entity_id": entity_id},
|
||||
)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def ha_turn_off(entity_id: str) -> Any:
|
||||
"""
|
||||
Turn off a Home Assistant entity.
|
||||
|
||||
Works with domains such as:
|
||||
- light
|
||||
- switch
|
||||
- fan
|
||||
- input_boolean
|
||||
"""
|
||||
domain = entity_id.split(".")[0]
|
||||
return ha_post(
|
||||
f"/api/services/{domain}/turn_off",
|
||||
{"entity_id": entity_id},
|
||||
)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def ha_toggle(entity_id: str) -> Any:
|
||||
"""Toggle a Home Assistant entity."""
|
||||
domain = entity_id.split(".")[0]
|
||||
return ha_post(
|
||||
f"/api/services/{domain}/toggle",
|
||||
{"entity_id": entity_id},
|
||||
)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def ha_call_service(
|
||||
domain: str,
|
||||
service: str,
|
||||
data: Dict[str, Any],
|
||||
) -> Any:
|
||||
"""
|
||||
Call any Home Assistant service.
|
||||
|
||||
Example:
|
||||
domain = "light"
|
||||
service = "turn_on"
|
||||
data = {
|
||||
"entity_id": "light.salon",
|
||||
"brightness_pct": 50
|
||||
}
|
||||
"""
|
||||
return ha_post(
|
||||
f"/api/services/{domain}/{service}",
|
||||
data,
|
||||
)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def ha_run_script(script_entity_id: str) -> Any:
|
||||
"""Run a Home Assistant script entity."""
|
||||
return ha_post(
|
||||
"/api/services/script/turn_on",
|
||||
{"entity_id": script_entity_id},
|
||||
)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def ha_trigger_automation(
|
||||
automation_entity_id: str,
|
||||
skip_condition: bool = True,
|
||||
) -> Any:
|
||||
"""
|
||||
Trigger a Home Assistant automation manually.
|
||||
|
||||
By default, conditions are skipped.
|
||||
"""
|
||||
return ha_post(
|
||||
"/api/services/automation/trigger",
|
||||
{
|
||||
"entity_id": automation_entity_id,
|
||||
"skip_condition": skip_condition,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def ha_restart_home_assistant() -> Any:
|
||||
"""
|
||||
Restart Home Assistant.
|
||||
|
||||
Use carefully.
|
||||
"""
|
||||
return ha_post("/api/services/homeassistant/restart")
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def ha_reload_automations() -> Any:
|
||||
"""Reload Home Assistant automations."""
|
||||
return ha_post("/api/services/automation/reload")
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def ha_reload_scripts() -> Any:
|
||||
"""Reload Home Assistant scripts."""
|
||||
return ha_post("/api/services/script/reload")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run()
|
||||
@@ -0,0 +1,20 @@
|
||||
import os, requests
|
||||
from dotenv import load_dotenv
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
load_dotenv(); mcp=FastMCP("Alfred Home Assistant")
|
||||
BASE=os.getenv("HA_URL","").rstrip("/"); TOKEN=os.getenv("HA_TOKEN","")
|
||||
def headers(): return {"Authorization":f"Bearer {TOKEN}","Content-Type":"application/json"}
|
||||
@mcp.tool()
|
||||
def get_state(entity_id:str):
|
||||
"""Read one Home Assistant entity state exposed for Alfred."""
|
||||
r=requests.get(f"{BASE}/api/states/{entity_id}",headers=headers(),timeout=15); r.raise_for_status(); return r.json()
|
||||
@mcp.tool()
|
||||
def call_service(domain:str, service:str, entity_id:str, data:dict|None=None):
|
||||
"""Call an allowed HA service. Prefer entities explicitly exposed to Assist."""
|
||||
allowed=set(x.strip() for x in os.getenv("HA_ALLOWED_DOMAINS","light,switch,cover,climate,media_player,calendar").split(','))
|
||||
if domain not in allowed: raise ValueError("Domaine non autorisé")
|
||||
payload={"entity_id":entity_id, **(data or {})}; r=requests.post(f"{BASE}/api/services/{domain}/{service}",headers=headers(),json=payload,timeout=20); r.raise_for_status(); return r.json()
|
||||
@mcp.tool()
|
||||
def list_states(domain:str=""):
|
||||
r=requests.get(f"{BASE}/api/states",headers=headers(),timeout=20);r.raise_for_status();items=r.json();return [x for x in items if not domain or x['entity_id'].startswith(domain+'.')]
|
||||
if __name__=='__main__': mcp.run()
|
||||
@@ -0,0 +1,3 @@
|
||||
mcp>=1.12
|
||||
requests>=2.32
|
||||
python-dotenv>=1.0
|
||||
Reference in New Issue
Block a user