feat: Home Assistant integration — WebSocket connection, automation conditions, UI

Add full Home Assistant integration via WebSocket API:
- HARuntime: persistent WebSocket client with auth, auto-reconnect, entity state cache
- HAManager: ref-counted runtime pool (like WeatherManager)
- HomeAssistantCondition: new automation trigger type matching entity states
- REST API: CRUD for HA sources + /test, /entities, /status endpoints
- /api/v1/system/integrations-status: combined MQTT + HA dashboard indicators
- Frontend: HA Sources tab in Streams, condition type in automation editor
- Modal editor with host, token, SSL, entity filters
- websockets>=13.0 dependency added
This commit is contained in:
2026-03-27 22:42:48 +03:00
parent f3d07fc47f
commit 2153dde4b7
26 changed files with 1912 additions and 119 deletions

View File

@@ -0,0 +1,97 @@
"""Home Assistant source schemas (CRUD + test + entities)."""
from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel, Field
class HomeAssistantSourceCreate(BaseModel):
"""Request to create a Home Assistant source."""
name: str = Field(description="Source name", min_length=1, max_length=100)
host: str = Field(description="HA host:port (e.g. '192.168.1.100:8123')", min_length=1)
token: str = Field(description="Long-Lived Access Token", min_length=1)
use_ssl: bool = Field(default=False, description="Use wss:// instead of ws://")
entity_filters: List[str] = Field(
default_factory=list, description="Entity ID filter patterns (e.g. ['sensor.*'])"
)
description: Optional[str] = Field(None, description="Optional description", max_length=500)
tags: List[str] = Field(default_factory=list, description="User-defined tags")
class HomeAssistantSourceUpdate(BaseModel):
"""Request to update a Home Assistant source."""
name: Optional[str] = Field(None, description="Source name", min_length=1, max_length=100)
host: Optional[str] = Field(None, description="HA host:port", min_length=1)
token: Optional[str] = Field(None, description="Long-Lived Access Token", min_length=1)
use_ssl: Optional[bool] = Field(None, description="Use wss://")
entity_filters: Optional[List[str]] = Field(None, description="Entity ID filter patterns")
description: Optional[str] = Field(None, description="Optional description", max_length=500)
tags: Optional[List[str]] = None
class HomeAssistantSourceResponse(BaseModel):
"""Home Assistant source response."""
id: str = Field(description="Source ID")
name: str = Field(description="Source name")
host: str = Field(description="HA host:port")
use_ssl: bool = Field(description="Whether SSL is enabled")
entity_filters: List[str] = Field(default_factory=list, description="Entity filter patterns")
connected: bool = Field(default=False, description="Whether the WebSocket connection is active")
entity_count: int = Field(default=0, description="Number of cached entities")
description: Optional[str] = Field(None, description="Description")
tags: List[str] = Field(default_factory=list, description="User-defined tags")
created_at: datetime = Field(description="Creation timestamp")
updated_at: datetime = Field(description="Last update timestamp")
class HomeAssistantSourceListResponse(BaseModel):
"""List of Home Assistant sources."""
sources: List[HomeAssistantSourceResponse] = Field(description="List of HA sources")
count: int = Field(description="Number of sources")
class HomeAssistantEntityResponse(BaseModel):
"""A single HA entity."""
entity_id: str = Field(description="Entity ID (e.g. 'sensor.temperature')")
state: str = Field(description="Current state value")
friendly_name: str = Field(description="Human-readable name")
domain: str = Field(description="Entity domain (e.g. 'sensor', 'binary_sensor')")
class HomeAssistantEntityListResponse(BaseModel):
"""List of entities from a HA instance."""
entities: List[HomeAssistantEntityResponse] = Field(description="List of entities")
count: int = Field(description="Number of entities")
class HomeAssistantTestResponse(BaseModel):
"""Connection test result."""
success: bool = Field(description="Whether connection and auth succeeded")
ha_version: Optional[str] = Field(None, description="Home Assistant version")
entity_count: int = Field(default=0, description="Number of entities found")
error: Optional[str] = Field(None, description="Error message if connection failed")
class HomeAssistantConnectionStatus(BaseModel):
"""Connection status for dashboard indicators."""
source_id: str
name: str
connected: bool
entity_count: int
class HomeAssistantStatusResponse(BaseModel):
"""Overall HA integration status for dashboard."""
connections: List[HomeAssistantConnectionStatus]
total_sources: int
connected_count: int