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:
41
src/mqtt_home/emqx_api.py
Normal file
41
src/mqtt_home/emqx_api.py
Normal 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
70
src/mqtt_home/schemas.py
Normal 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
55
tests/test_emqx_api.py
Normal 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
|
||||||
Reference in New Issue
Block a user