Le problème
Quand on utilise un agent conversationnel LLM (Ollama, OpenAI, etc.) avec Home Assistant, il faut contrôler précisément quelles entités sont exposées pour ne pas saturer la fenêtre de contexte du LLM. Avec 300+ entités exposées par défaut, le modèle dump la liste d’entités au lieu de répondre.
L’UI de HA ne permet de basculer les entités qu’une par une dans Paramètres > Assistants vocaux > Exposer. Pas de gestion en masse, pas d’API REST, et aucun moyen documenté de le faire programmatiquement.
La solution : API WebSocket
Home Assistant possède des commandes WebSocket non documentées pour gérer les entités exposées. Je les ai trouvées dans le code source de HA core :
Lister les entités exposées
{
"id": 1,
"type": "homeassistant/expose_entity/list"
}
Retourne toutes les entités avec leur statut d’exposition par assistant (conversation, cloud.alexa, cloud.google_assistant) :
{
"id": 1,
"type": "result",
"success": true,
"result": {
"exposed_entities": {
"light.living_room": {
"conversation": true,
"cloud.alexa": false,
"cloud.google_assistant": false
}
}
}
}
Exposer ou retirer des entités
{
"id": 2,
"type": "homeassistant/expose_entity",
"assistants": ["conversation"],
"entity_ids": ["light.living_room", "sensor.temperature"],
"should_expose": true
}
Points clés :
- Effet immédiat — pas besoin de redémarrer HA
- Opérations en masse — passer plusieurs entity IDs en un seul appel
- Par assistant — contrôle séparé pour
conversation,cloud.alexa,cloud.google_assistant
Script Python
J’ai écrit un script qui gère ça automatiquement. Il lit les entités cibles depuis un fichier YAML et synchronise l’état d’exposition :
"""
Gère les entités exposées aux assistants vocaux HA via l'API WebSocket.
Utilise les commandes WebSocket non documentées :
- homeassistant/expose_entity/list - lister les entités exposées par assistant
- homeassistant/expose_entity - exposer/retirer des entités
Usage:
python ha_expose.py --list-only # lister les entités actuellement exposées
python ha_expose.py --dry-run # voir ce qui changerait
python ha_expose.py # appliquer les entités depuis le YAML
Prérequis: pip install websockets pyyaml
Variable d'env: HA_TOKEN (long-lived access token)
"""
import argparse
import asyncio
import json
import os
import sys
from pathlib import Path
import websockets
import yaml
DEFAULT_URL = "ws://homeassistant.local:8123/api/websocket"
msg_id = 0
def next_id():
global msg_id
msg_id += 1
return msg_id
def load_entities(path):
"""Charge les entity IDs cibles depuis un fichier YAML."""
with open(path) as f:
data = yaml.safe_load(f)
entities = []
for category in data.values():
if isinstance(category, list):
entities.extend(category)
return entities
async def send_and_receive(ws, payload):
"""Envoie un message WebSocket et attend la réponse correspondante."""
await ws.send(json.dumps(payload))
while True:
resp = json.loads(await ws.recv())
if resp.get("id") == payload.get("id"):
return resp
async def main():
parser = argparse.ArgumentParser(description="Manage HA exposed entities via WebSocket API")
parser.add_argument("--url", default=os.environ.get("HA_WS_URL", DEFAULT_URL))
parser.add_argument("--token", default=os.environ.get("HA_TOKEN"))
parser.add_argument("--entities", type=Path, required=True, help="YAML file with target entities")
parser.add_argument("--assistant", default="conversation")
parser.add_argument("--list-only", action="store_true", help="Only list currently exposed")
parser.add_argument("--dry-run", action="store_true", help="Show what would change")
args = parser.parse_args()
if not args.token:
print("ERROR: Set HA_TOKEN env var or use --token"); sys.exit(1)
async with websockets.connect(args.url) as ws:
# Authenticate
msg = json.loads(await ws.recv())
assert msg["type"] == "auth_required"
await ws.send(json.dumps({"type": "auth", "access_token": args.token}))
msg = json.loads(await ws.recv())
assert msg["type"] == "auth_ok", f"Auth failed: {msg}"
# List current state
resp = await send_and_receive(ws, {"id": next_id(), "type": "homeassistant/expose_entity/list"})
currently_exposed = [
eid for eid, assistants in resp["result"]["exposed_entities"].items()
if assistants.get(args.assistant) is True
]
print(f"Currently exposed to {args.assistant}: {len(currently_exposed)}")
if args.list_only:
for eid in sorted(currently_exposed):
print(f" {eid}")
return
# Load targets and compute diff
target = load_entities(args.entities)
to_unexpose = set(currently_exposed) - set(target)
to_expose = set(target) - set(currently_exposed)
print(f"Target: {len(target)} | To expose: {len(to_expose)} | To unexpose: {len(to_unexpose)}")
if args.dry_run:
for eid in sorted(to_expose): print(f" + {eid}")
for eid in sorted(to_unexpose): print(f" - {eid}")
return
# Apply
if to_unexpose:
resp = await send_and_receive(ws, {
"id": next_id(), "type": "homeassistant/expose_entity",
"assistants": [args.assistant], "entity_ids": list(to_unexpose), "should_expose": False,
})
print(f"Unexposed {len(to_unexpose)} OK" if resp.get("success") else f"ERROR: {resp}")
if to_expose:
resp = await send_and_receive(ws, {
"id": next_id(), "type": "homeassistant/expose_entity",
"assistants": [args.assistant], "entity_ids": list(to_expose), "should_expose": True,
})
print(f"Exposed {len(to_expose)} OK" if resp.get("success") else f"ERROR: {resp}")
# Verify
resp = await send_and_receive(ws, {"id": next_id(), "type": "homeassistant/expose_entity/list"})
final = [eid for eid, a in resp["result"]["exposed_entities"].items() if a.get(args.assistant) is True]
print(f"Final count: {len(final)} {'SUCCESS' if len(final) == len(target) else 'MISMATCH'}")
if __name__ == "__main__":
asyncio.run(main())
Fichier YAML des entités
# entities.yaml - adapter à votre installation
temperature:
- sensor.living_room_temperature
- sensor.bedroom_temperature
- sensor.outdoor_temperature
climate:
- climate.living_room
- climate.bedroom
lights:
- light.all_lights
- light.living_room
- light.bedroom
Mon cas d’usage
Je fais tourner Ollama (qwen2.5:7b sur RTX 5070 Ti) comme agent conversationnel pour Home Assistant Voice PE. Avec 397 entités exposées, la fenêtre de contexte de 8K était complètement saturée. Après réduction à 94 entités soigneusement sélectionnées avec ce script, tout fonctionne parfaitement :
- Température, climatisation, lumières, volets, capteurs de sécurité, météo, piscine, énergie
- Groupes pour le contrôle en masse (« éteins toutes les lumières »)
- Entités individuelles pour le contrôle précis (« allume la lumière de la chambre »)
Ce que j’ai appris
- Ne PAS modifier
.storage/homeassistant.exposed_entitiesdirectement — HA garde l’état en mémoire et écrase le fichier à l’arrêt - L’API WebSocket est le seul moyen programmatique fiable de gérer les entités exposées
- Les changements prennent effet immédiatement — pas de redémarrage nécessaire
- Avec « Préférer la gestion locale des commandes » activé dans le pipeline, les intents simples (état, toggle) sont gérés par Assist natif, les requêtes complexes passent par Ollama
Ce qui manque dans HA core
- Pas d’endpoint REST API pour expose/unexpose (uniquement WebSocket)
- Pas d’UI de gestion en masse (toggle un par un uniquement)
- Pas de documentation de ces commandes WebSocket
- Pas de moyen de définir les entités exposées en YAML dans la configuration
Si d’autres ont rencontré ce problème, je suis curieux de savoir comment vous l’avez résolu !