Deep-dive · Smart Home + Agent Protocols¶
Phase: 5 (smart home) e cross-phase per i protocolli Versione: maggio 2026
1. Home Assistant bridge¶
Setup Docker¶
services:
homeassistant:
image: ghcr.io/home-assistant/home-assistant:stable
network_mode: host
volumes:
- /opt/jarvis/ha-config:/config
- /etc/localtime:/etc/localtime:ro
restart: unless-stopped
privileged: true
HA ascolta su :8123. Per Zigbee/Thread dongle USB: devices: ["/dev/ttyUSB0:/dev/ttyUSB0"].
REST + WebSocket client¶
import json
import aiohttp
HA_URL = "http://homeassistant.local:8123"
HA_TOKEN = "YOUR_LONG_LIVED_TOKEN"
HEADERS = {"Authorization": f"Bearer {HA_TOKEN}", "Content-Type": "application/json"}
async def turn_on_light(entity_id: str, brightness: int = 255) -> dict:
async with aiohttp.ClientSession() as s:
async with s.post(
f"{HA_URL}/api/services/light/turn_on",
headers=HEADERS,
json={"entity_id": entity_id, "brightness": brightness},
) as r:
return await r.json()
async def stream_states():
"""WebSocket subscribe state_changed."""
async with aiohttp.ClientSession() as s:
async with s.ws_connect(f"ws://homeassistant.local:8123/api/websocket") as ws:
assert (await ws.receive_json())["type"] == "auth_required"
await ws.send_json({"type": "auth", "access_token": HA_TOKEN})
assert (await ws.receive_json())["type"] == "auth_ok"
await ws.send_json({
"id": 1, "type": "subscribe_events", "event_type": "state_changed"
})
async for msg in ws:
if msg.type == aiohttp.WSMsgType.TEXT:
data = json.loads(msg.data)
if data.get("type") == "event":
evt = data["event"]["data"]
print(f"[HA] {evt.get('entity_id')} → {evt.get('new_state', {}).get('state')}")
Conversation Agent¶
Espone Jarvis come backend voce HA tramite custom HACS component:
POST /api/conversation/process
{
"text": "Accendi la luce del soggiorno",
"conversation_id": "jarvis-abc",
"language": "it"
}
HACS + Frigate NVR¶
Frigate publishes detection events su frigate/<camera>/events con label, score, snapshot URL. Jarvis sottoscrive per automazioni context-aware.
2. Matter 1.5 (novembre 2025)¶
Standard CSA, supportato da Apple/Google/Amazon/Samsung. v1.5 introduce:
- Cameras WebRTC + audio bidirezionale + zone privacy
- Enhanced Closures (tende, cancelli, garage)
- Energy Management esteso (EV charger, battery storage, solar)
Cluster model¶
| Cluster | ID | Funzione |
|---|---|---|
| OnOff | 0x0006 | On/off |
| LevelControl | 0x0008 | Dimmer |
| ColorControl | 0x0300 | Hue/saturation/temp |
| Thermostat | 0x0201 | Setpoint |
| DoorLock | 0x0101 | Serratura |
| EnergyEvse | 0x0099 | EV charger |
Commissioning flow¶
- Accessory non-commissioned in BLE advertising
- Controller legge QR Matter (o NFC)
- PASE via BLE
- Trasferimento credenziali Wi-Fi/Thread dataset
- CASE completion
python-matter-server¶
import aiohttp
async def commission_device(code: str) -> dict:
async with aiohttp.ClientSession() as s:
async with s.ws_connect("ws://localhost:5580/ws") as ws:
await ws.send_json({
"message_id": "1",
"command": "commission_with_code",
"args": {"code": code, "network_only": False},
})
return await ws.receive_json()
async def discover_nodes() -> list:
async with aiohttp.ClientSession() as s:
async with s.ws_connect("ws://localhost:5580/ws") as ws:
await ws.send_json({"message_id": "2", "command": "get_nodes", "args": {}})
return (await ws.receive_json()).get("result", [])
Thread Border Router 2026¶
- Apple HomePod mini / HomePod 2: Thread 1.4, credential sharing
- Google Nest Hub 2nd gen: Thread 1.3 (upgrade in arrivo)
- HA Yellow / SkyConnect: BR open source via OTBR
Da maggio 2026 Thread 1.4 obbligatorio per nuove certificazioni: BR 1.4 condividono automaticamente le credenziali.
3. Thread + OpenThread¶
ESPHome ≥ 2025.6.0 con ESP32-C6/H2:
# esphome-thread-sensor.yml
esp32:
board: esp32-c6-devkitc-1
framework:
type: esp-idf
openthread:
network_name: "JarvisHome"
channel: 15
output_power: 10 # dBm
sensor:
- platform: bme280
temperature:
name: "Temperatura Soggiorno"
OTBR Docker:
docker run --detach --name otbr \
--sysctl "net.ipv6.conf.all.disable_ipv6=0 net.ipv4.conf.all.forwarding=1 net.ipv6.conf.all.forwarding=1" \
--privileged \
-p 8080:80 -p 8081:8081 \
--device /dev/ttyUSB0:/dev/ttyACM0 \
nrfconnect/otbr:latest \
--radio-url spinel+hdlc+uart:///dev/ttyACM0?uart-baudrate=460800
4. Zigbee 3.0¶
Ampia copertura legacy (3.000+ device su zigbee2mqtt).
| Tool | Ruolo |
|---|---|
| zigpy | Stack Zigbee Python low-level |
| Zigbee2MQTT | Bridge Zigbee → MQTT |
| ZHA | Integrazione nativa HA |
| Coordinator | Chip | Note |
|---|---|---|
| Sonoff ZBDongle-P | CC2652P | Standard de facto |
| Sonoff ZBDongle-MG24 | MGM240P | Zigbee + Thread |
| HA Connect ZBT-2 | EFR32MG24 | Ufficiale HA (nov 2025) |
5. MQTT come event bus¶
Mosquitto 2.x raccomandato per home (footprint 5MB RAM). EMQX per multi-node.
Topic structure Jarvis:
jarvis/{device_id}/{capability}/{action}
# Esempi
jarvis/lamp-living/light/turn_on
jarvis/thermostat-main/temperature/current
jarvis/camera-entrance/motion/detected
jarvis/frigate/camera-front/person/detected
QoS levels: 0 telemetria alta freq · 1 comandi standard · 2 transazioni critiche (lock).
mTLS auth:
listener 8883
cafile /etc/mosquitto/certs/ca.crt
certfile /etc/mosquitto/certs/server.crt
keyfile /etc/mosquitto/certs/server.key
require_certificate true
use_identity_as_username true
import json
import aiomqtt
async def publish_command(device_id: str, capability: str, payload: dict):
async with aiomqtt.Client(
hostname="mosquitto", port=8883,
tls_params=aiomqtt.TLSParameters(
ca_certs="/certs/ca.crt",
certfile="/certs/jarvis-agent.crt",
keyfile="/certs/jarvis-agent.key",
),
) as c:
await c.publish(f"jarvis/{device_id}/{capability}/set",
payload=json.dumps(payload), qos=1)
6. Model Context Protocol (MCP)¶
Anthropic, dic 2024. Spec attuale 2025-11-25. JSON-RPC 2.0 su stdio o HTTP/SSE.
3 primitive:
- Tools: funzioni con schema JSON
- Resources: dati con URI template
- Prompts: template tipizzati
Apple iOS 26.1 (autunno 2026): Siri usa MCP via App Intents.
"""
MCP server per Jarvis home control.
"""
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent, Resource, CallToolResult
import aiohttp
app = Server("jarvis-home-mcp")
HA_URL = "http://homeassistant.local:8123"
HA_HEADERS = {"Authorization": f"Bearer YOUR_TOKEN"}
@app.list_tools()
async def list_tools():
return [
Tool(
name="home_turn_on_light",
description="Accende una luce o gruppo di luci",
inputSchema={
"type": "object",
"properties": {
"entity_id": {"type": "string"},
"brightness_pct": {"type": "integer", "minimum": 1, "maximum": 100},
},
"required": ["entity_id"],
},
),
]
@app.call_tool()
async def call_tool(name: str, arguments: dict) -> CallToolResult:
async with aiohttp.ClientSession(headers=HA_HEADERS) as s:
if name == "home_turn_on_light":
payload = {"entity_id": arguments["entity_id"]}
if "brightness_pct" in arguments:
payload["brightness_pct"] = arguments["brightness_pct"]
async with s.post(f"{HA_URL}/api/services/light/turn_on", json=payload) as r:
result = await r.json()
return CallToolResult(content=[TextContent(type="text", text=str(result))])
7. Agent2Agent (A2A)¶
Google, apr 2025 → Linux Foundation jun 2025. Risolve agent-to-agent.
- Agent Card JSON (capacità, endpoint, auth)
- Task lifecycle (submitted, working, completed, failed)
- LangGraph v1.0 supporto nativo
import httpx
from a2a.client import A2AClient
from a2a.types import SendMessageRequest, MessageSendParams
HOME_AGENT_URL = "http://jarvis-home-agent:8001"
async def delegate_to_home_agent(task: str) -> str:
async with httpx.AsyncClient() as http:
client = A2AClient(http_client=http, url=HOME_AGENT_URL)
request = SendMessageRequest(
params=MessageSendParams(
message={
"role": "user",
"parts": [{"kind": "text", "text": task}],
"messageId": "msg-001",
}
)
)
response = await client.send_message(request)
return response.result.parts[0].text if response.result else ""
MCP vs A2A (complementari)¶
| Dimensione | MCP | A2A |
|---|---|---|
| Relazione | Agent → Tool/Resource | Agent → Agent |
| Paradigma | Client-server sync | Task delegation async |
| Stato | Stateless | Lifecycle tracking |
| Uso Jarvis | Esporre tool HA, file, API | Delegare a sotto-agenti |
8. AG-UI Protocol¶
CopilotKit. Bidirezionale frontend ↔ backend agentico, complementa MCP+A2A su layer rendering UI. Adottato da Google ADK, LangChain, AWS, Microsoft, Mastra, PydanticAI.
// React + AG-UI
import { useCoAgent, useCoAgentStateRender } from "@copilotkit/react-core";
import { CopilotKit } from "@copilotkit/react-core";
import { CopilotChat } from "@copilotkit/react-ui";
function SmartHomeDashboard() {
const { state } = useCoAgent<HomeState>({ name: "jarvis_home_agent" });
useCoAgentStateRender<HomeState>({
name: "jarvis_home_agent",
render: ({ state }) => (
<div className="grid grid-cols-3 gap-4">
{state.rooms.map(r => <RoomCard key={r.id} room={r} />)}
</div>
),
});
return (
<div className="flex">
<CopilotChat instructions="Sei Jarvis..." />
</div>
);
}
export default function App() {
return (
<CopilotKit runtimeUrl="/api/copilotkit">
<SmartHomeDashboard />
</CopilotKit>
);
}
9. KDE Connect (cross-device sync)¶
Open source dal 2013, TCP/UDP discovery + TLS data. Plugin sfruttabili da Jarvis:
| Plugin | Uso Jarvis |
|---|---|
| Notification Sync | Alert smart home → smartphone |
| Remote Input | Debug remoto da mobile |
| Clipboard Sync | Entity ID condivisi |
| Media Control | Player Jarvis remote |
| File Transfer | Log, snapshot Frigate |
| Run Command | Trigger automazioni |
# Notifiche via DBus
import dbus
def send_notification(device_id: str, title: str, body: str):
bus = dbus.SessionBus()
device = bus.get_object(
"org.kde.kdeconnect.daemon",
f"/modules/kdeconnect/devices/{device_id}/notifications"
)
iface = dbus.Interface(device, "org.kde.kdeconnect.device.notifications")
iface.sendNotification(title, body, "jarvis", 0)
10. Authentik OIDC + WebAuthn¶
Authentik 2025.12 (gen 2026): Endpoint Devices fleet, Fleet Connector, WebAuthn Conditional UI, Local Device Login Linux con FIDO2.
services:
authentik-server:
image: ghcr.io/goauthentik/server:2025.12
command: server
environment:
AUTHENTIK_REDIS__HOST: redis
AUTHENTIK_POSTGRESQL__HOST: postgresql
AUTHENTIK_POSTGRESQL__USER: authentik
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY}
ports:
- "9000:9000"
- "9443:9443"
Verifica JWT su ogni device-agent:
import jwt as pyjwt
from functools import lru_cache
AUTHENTIK_URL = "https://auth.jarvis.local"
JWKS_URL = f"{AUTHENTIK_URL}/application/o/jarvis/.well-known/jwks.json"
@lru_cache(maxsize=1)
def jwks_client() -> pyjwt.PyJWKClient:
return pyjwt.PyJWKClient(JWKS_URL)
def verify_jwt(token: str) -> dict:
signing_key = jwks_client().get_signing_key_from_jwt(token)
return pyjwt.decode(
token,
signing_key.key,
algorithms=["RS256", "ES256"],
audience="jarvis-agent",
issuer=f"{AUTHENTIK_URL}/application/o/jarvis/",
options={"require": ["exp", "iat", "sub", "aud"]},
)
Mappa dipendenze¶
open-jarvis
├── MQTT Bus (Mosquitto)
│ ├── Frigate events → jarvis/camera/+/detected
│ ├── Zigbee2MQTT → jarvis/zigbee/+/+
│ └── ESPHome sensors → jarvis/thread/+/+
├── Home Assistant (REST + WebSocket :8123)
│ ├── Matter (python-matter-server)
│ ├── ZHA / Zigbee
│ └── Conversation Agent ← Jarvis LLM core
├── MCP Server (stdio / SSE)
│ ├── Tool: home.turn_on_light
│ └── Resource: home://states
├── A2A Network
│ ├── JarvisHomeAgent
│ ├── JarvisSecurityAgent
│ └── JarvisEnergyAgent
├── AG-UI (React dashboard)
└── Authentik OIDC + WebAuthn
Versioni (mag 2026)¶
| Componente | Versione |
|---|---|
| Home Assistant | 2025.12+ |
| Matter SDK | 1.4.2 (python-matter-server) |
| Matter Spec | 1.5 (nov 2025) |
| Thread Spec | 1.4 |
| ESPHome | 2025.6.0+ |
| MCP Spec | 2025-11-25 |
| A2A Protocol | 1.0 (Linux Foundation) |
| Authentik | 2025.12 |