feat: Pydantic schemas and EMQX REST API client

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
walkpan
2026-03-29 21:32:45 +08:00
parent 455ed4ad5e
commit 79b2878b3d
3 changed files with 166 additions and 0 deletions

41
src/mqtt_home/emqx_api.py Normal file
View File

@@ -0,0 +1,41 @@
import httpx
from typing import Any
from mqtt_home.config import Settings
class EmqxApiClient:
def __init__(self, settings: Settings):
self._client = httpx.AsyncClient(
base_url=settings.emqx_api_url,
auth=(settings.emqx_api_key, settings.emqx_api_secret),
timeout=10.0,
)
async def close(self):
await self._client.aclose()
async def get_broker_status(self) -> dict[str, Any]:
resp = await self._client.get("/status")
resp.raise_for_status()
return resp.json().get("data", {})
async def get_metrics(self) -> dict[str, Any]:
resp = await self._client.get("/metrics")
resp.raise_for_status()
return resp.json().get("data", {})
async def get_clients(self, limit: int = 100) -> list[dict[str, Any]]:
resp = await self._client.get("/clients", params={"limit": limit})
resp.raise_for_status()
return resp.json().get("data", [])
async def get_subscriptions(self, limit: int = 100) -> list[dict[str, Any]]:
resp = await self._client.get("/subscriptions", params={"limit": limit})
resp.raise_for_status()
return resp.json().get("data", [])
async def get_topics(self, limit: int = 100) -> list[dict[str, Any]]:
resp = await self._client.get("/topics", params={"limit": limit})
resp.raise_for_status()
return resp.json().get("data", [])

70
src/mqtt_home/schemas.py Normal file
View File

@@ -0,0 +1,70 @@
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, Field
class DeviceCreate(BaseModel):
name: str
type: str = "switch"
protocol: str = "custom"
mqtt_topic: str
command_topic: Optional[str] = None
class DeviceUpdate(BaseModel):
name: Optional[str] = None
type: Optional[str] = None
command_topic: Optional[str] = None
class DeviceCommand(BaseModel):
payload: str
class DeviceResponse(BaseModel):
id: str
name: str
type: str
protocol: str
mqtt_topic: str
command_topic: Optional[str] = None
state: Optional[str] = None
is_online: bool
last_seen: Optional[datetime] = None
created_at: datetime
updated_at: datetime
model_config = {"from_attributes": True}
class DeviceLogResponse(BaseModel):
id: int
device_id: str
direction: str
topic: str
payload: str
timestamp: datetime
model_config = {"from_attributes": True}
class BrokerClient(BaseModel):
clientid: str
username: Optional[str] = None
ip_address: Optional[str] = None
connected: bool
keepalive: int
proto_ver: Optional[int] = None
clean_start: Optional[bool] = None
class BrokerTopic(BaseModel):
topic: str
node: Optional[str] = None
class DashboardStats(BaseModel):
total_devices: int
online_devices: int
offline_devices: int
recent_logs: list[DeviceLogResponse]

55
tests/test_emqx_api.py Normal file
View File

@@ -0,0 +1,55 @@
import pytest
import httpx
from unittest.mock import AsyncMock, patch
from mqtt_home.emqx_api import EmqxApiClient
from mqtt_home.config import Settings
@pytest.fixture
def settings():
return Settings(
mqtt_host="localhost",
emqx_api_url="http://localhost:18083/api/v5",
emqx_api_key="test-key",
emqx_api_secret="test-secret",
database_url="sqlite+aiosqlite:///:memory:",
)
@pytest.fixture
def emqx_client(settings):
return EmqxApiClient(settings)
def _make_response(status_code: int, json_data: dict) -> httpx.Response:
request = httpx.Request("GET", "http://localhost:18083/api/v5/test")
return httpx.Response(status_code, json=json_data, request=request)
async def test_get_broker_status(emqx_client):
with patch.object(emqx_client._client, "get", new_callable=AsyncMock) as mock_get:
mock_get.return_value = _make_response(
200, {"data": {"uptime": 12345, "version": "5.0.0"}}
)
result = await emqx_client.get_broker_status()
assert result["uptime"] == 12345
async def test_get_clients(emqx_client):
with patch.object(emqx_client._client, "get", new_callable=AsyncMock) as mock_get:
mock_get.return_value = _make_response(
200, {"data": [{"clientid": "dev1", "connected": True}]}
)
result = await emqx_client.get_clients()
assert len(result) == 1
assert result[0]["clientid"] == "dev1"
async def test_get_topics(emqx_client):
with patch.object(emqx_client._client, "get", new_callable=AsyncMock) as mock_get:
mock_get.return_value = _make_response(
200, {"data": [{"topic": "home/light"}]}
)
result = await emqx_client.get_topics()
assert len(result) == 1