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