diff --git a/src/mqtt_home/emqx_api.py b/src/mqtt_home/emqx_api.py new file mode 100644 index 0000000..1ff8066 --- /dev/null +++ b/src/mqtt_home/emqx_api.py @@ -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", []) diff --git a/src/mqtt_home/schemas.py b/src/mqtt_home/schemas.py new file mode 100644 index 0000000..0c8d7d4 --- /dev/null +++ b/src/mqtt_home/schemas.py @@ -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] diff --git a/tests/test_emqx_api.py b/tests/test_emqx_api.py new file mode 100644 index 0000000..2cf61a0 --- /dev/null +++ b/tests/test_emqx_api.py @@ -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