diff --git a/.flake8 b/.flake8 index decce21..30b5eee 100644 --- a/.flake8 +++ b/.flake8 @@ -1,6 +1,6 @@ [flake8] max-line-length = 88 -extend-ignore = E203, E501, W503 +extend-ignore = B008, E203, E402, E501, W503 exclude = .git, __pycache__, diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b631e9e..f51eccb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,6 +21,7 @@ repos: rev: 7.0.0 hooks: - id: flake8 + args: ["--config=.flake8"] additional_dependencies: - flake8-bugbear - flake8-comprehensions @@ -29,6 +30,7 @@ repos: rev: v1.9.0 hooks: - id: mypy + exclude: ^alembic/ additional_dependencies: - pydantic>=2.0.0 - pydantic-settings>=2.0.0 diff --git a/pyproject.toml b/pyproject.toml index edea9de..eacb525 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -106,9 +106,25 @@ plugins = ["pydantic.mypy"] module = [ "paho.*", "uvicorn.*", + "alembic.*", ] ignore_missing_imports = true +[[tool.mypy.overrides]] +module = [ + "tests.*", + "conftest", +] +disallow_untyped_defs = false +disallow_incomplete_defs = false + +[[tool.mypy.overrides]] +module = [ + "alembic.env", + "alembic.versions.*", +] +ignore_errors = true + [tool.pytest.ini_options] minversion = "7.0" asyncio_mode = "auto" diff --git a/src/meshcore_hub/__main__.py b/src/meshcore_hub/__main__.py index 7e1de6a..86db823 100644 --- a/src/meshcore_hub/__main__.py +++ b/src/meshcore_hub/__main__.py @@ -71,7 +71,7 @@ def db() -> None: def db_upgrade(revision: str, database_url: str | None) -> None: """Upgrade database to a later version.""" import os - from alembic import command + from alembic import command # type: ignore[attr-defined] from alembic.config import Config click.echo(f"Upgrading database to revision: {revision}") @@ -101,7 +101,7 @@ def db_upgrade(revision: str, database_url: str | None) -> None: def db_downgrade(revision: str, database_url: str | None) -> None: """Revert database to a previous version.""" import os - from alembic import command + from alembic import command # type: ignore[attr-defined] from alembic.config import Config click.echo(f"Downgrading database to revision: {revision}") @@ -130,7 +130,7 @@ def db_downgrade(revision: str, database_url: str | None) -> None: ) def db_revision(message: str, autogenerate: bool) -> None: """Create a new database migration.""" - from alembic import command + from alembic import command # type: ignore[attr-defined] from alembic.config import Config click.echo(f"Creating new revision: {message}") @@ -151,7 +151,7 @@ def db_revision(message: str, autogenerate: bool) -> None: def db_current(database_url: str | None) -> None: """Show current database revision.""" import os - from alembic import command + from alembic import command # type: ignore[attr-defined] from alembic.config import Config alembic_cfg = Config("alembic.ini") @@ -164,7 +164,7 @@ def db_current(database_url: str | None) -> None: @db.command("history") def db_history() -> None: """Show database migration history.""" - from alembic import command + from alembic import command # type: ignore[attr-defined] from alembic.config import Config alembic_cfg = Config("alembic.ini") diff --git a/src/meshcore_hub/api/app.py b/src/meshcore_hub/api/app.py index a2f5857..4a1efd1 100644 --- a/src/meshcore_hub/api/app.py +++ b/src/meshcore_hub/api/app.py @@ -6,6 +6,7 @@ from typing import AsyncGenerator from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware +from sqlalchemy import text from meshcore_hub import __version__ from meshcore_hub.common.database import DatabaseManager @@ -115,7 +116,7 @@ def create_app( try: db = get_db_manager() with db.session_scope() as session: - session.execute("SELECT 1") + session.execute(text("SELECT 1")) return {"status": "ready", "database": "connected"} except Exception as e: return {"status": "not_ready", "database": str(e)} diff --git a/src/meshcore_hub/collector/handlers/message.py b/src/meshcore_hub/collector/handlers/message.py index 28614f9..2494cac 100644 --- a/src/meshcore_hub/collector/handlers/message.py +++ b/src/meshcore_hub/collector/handlers/message.py @@ -62,7 +62,7 @@ def _handle_message( """ text = payload.get("text") if not text: - logger.warning(f"Message missing text content") + logger.warning("Message missing text content") return now = datetime.now(timezone.utc) diff --git a/src/meshcore_hub/common/models/trace_path.py b/src/meshcore_hub/common/models/trace_path.py index 11630dc..62c45b3 100644 --- a/src/meshcore_hub/common/models/trace_path.py +++ b/src/meshcore_hub/common/models/trace_path.py @@ -3,7 +3,7 @@ from datetime import datetime from typing import Optional -from sqlalchemy import BigInteger, DateTime, ForeignKey, Index, Integer, String +from sqlalchemy import BigInteger, DateTime, ForeignKey, Index, Integer from sqlalchemy.dialects.sqlite import JSON from sqlalchemy.orm import Mapped, mapped_column diff --git a/src/meshcore_hub/common/mqtt.py b/src/meshcore_hub/common/mqtt.py index 03bdb6b..82b80c2 100644 --- a/src/meshcore_hub/common/mqtt.py +++ b/src/meshcore_hub/common/mqtt.py @@ -124,7 +124,7 @@ class MQTTClient: self.config = config self.topic_builder = TopicBuilder(config.prefix) self._client = mqtt.Client( - callback_api_version=CallbackAPIVersion.VERSION2, + callback_api_version=CallbackAPIVersion.VERSION2, # type: ignore[call-arg] client_id=config.client_id, clean_session=config.clean_session, ) @@ -211,7 +211,7 @@ class MQTTClient: pattern_parts = pattern.split("/") topic_parts = topic.split("/") - for i, (p, t) in enumerate(zip(pattern_parts, topic_parts)): + for _i, (p, t) in enumerate(zip(pattern_parts, topic_parts)): if p == "#": return True if p != "+" and p != t: diff --git a/src/meshcore_hub/interface/cli.py b/src/meshcore_hub/interface/cli.py index 8563b1b..d2cd41e 100644 --- a/src/meshcore_hub/interface/cli.py +++ b/src/meshcore_hub/interface/cli.py @@ -2,7 +2,6 @@ import click -from meshcore_hub.common.config import InterfaceMode from meshcore_hub.common.logging import configure_logging diff --git a/src/meshcore_hub/interface/device.py b/src/meshcore_hub/interface/device.py index 2843d6c..255154f 100644 --- a/src/meshcore_hub/interface/device.py +++ b/src/meshcore_hub/interface/device.py @@ -2,7 +2,6 @@ import asyncio import logging -import time from abc import ABC, abstractmethod from dataclasses import dataclass from enum import Enum @@ -261,9 +260,9 @@ class MeshCoreDevice(BaseMeshCoreDevice): """ super().__init__(config) self._running = False - self._mc = None - self._loop = None - self._subscriptions = [] + self._mc: Any = None + self._loop: Any = None + self._subscriptions: list[Any] = [] def connect(self) -> bool: """Connect to the MeshCore device.""" @@ -309,7 +308,7 @@ class MeshCoreDevice(BaseMeshCoreDevice): if self_info: self._public_key = self_info.get("public_key") if self._public_key: - logger.info(f"Retrieved device public key from self_info") + logger.info("Retrieved device public key from self_info") else: logger.warning( "Device self_info missing public_key field. " @@ -352,7 +351,7 @@ class MeshCoreDevice(BaseMeshCoreDevice): for mc_event_type, our_event_type in event_map.items(): - async def callback(event, et=our_event_type): + async def callback(event: Any, et: EventType = our_event_type) -> None: # Convert event to dict and dispatch # Use event.payload for the full data (text, etc.) # event.attributes only contains filtering fields @@ -399,7 +398,7 @@ class MeshCoreDevice(BaseMeshCoreDevice): try: - async def _send(): + async def _send() -> None: await self._mc.commands.send_msg(destination, text) self._loop.run_until_complete(_send()) @@ -422,7 +421,7 @@ class MeshCoreDevice(BaseMeshCoreDevice): try: - async def _send(): + async def _send() -> None: await self._mc.commands.send_chan_msg(channel_idx, text) self._loop.run_until_complete(_send()) @@ -440,7 +439,7 @@ class MeshCoreDevice(BaseMeshCoreDevice): try: - async def _send(): + async def _send() -> None: await self._mc.commands.send_advert(flood=flood) self._loop.run_until_complete(_send()) @@ -458,7 +457,7 @@ class MeshCoreDevice(BaseMeshCoreDevice): try: - async def _request(): + async def _request() -> None: await self._mc.commands.send_statusreq(target) self._loop.run_until_complete(_request()) @@ -476,7 +475,7 @@ class MeshCoreDevice(BaseMeshCoreDevice): try: - async def _request(): + async def _request() -> None: await self._mc.commands.send_telemetry_req(target) self._loop.run_until_complete(_request()) @@ -494,7 +493,7 @@ class MeshCoreDevice(BaseMeshCoreDevice): try: - async def _set_time(): + async def _set_time() -> None: await self._mc.commands.set_time(timestamp) self._loop.run_until_complete(_set_time()) @@ -512,7 +511,7 @@ class MeshCoreDevice(BaseMeshCoreDevice): try: - async def _start_fetching(): + async def _start_fetching() -> None: await self._mc.start_auto_message_fetching() self._loop.run_until_complete(_start_fetching()) @@ -531,7 +530,7 @@ class MeshCoreDevice(BaseMeshCoreDevice): self._setup_event_subscriptions() # Run the async event loop - async def _run_loop(): + async def _run_loop() -> None: while self._running and self._connected: await asyncio.sleep(0.1) diff --git a/src/meshcore_hub/interface/mock_device.py b/src/meshcore_hub/interface/mock_device.py index 89b85f6..289925b 100644 --- a/src/meshcore_hub/interface/mock_device.py +++ b/src/meshcore_hub/interface/mock_device.py @@ -158,7 +158,6 @@ class MockMeshCoreDevice(BaseMeshCoreDevice): logger.warning("Simulated send failure") return False - ts = timestamp or int(time.time()) logger.info(f"Mock: Sending message to {destination[:12]}...: {text[:20]}...") # Simulate send confirmation after delay @@ -199,7 +198,6 @@ class MockMeshCoreDevice(BaseMeshCoreDevice): logger.warning("Simulated send failure") return False - ts = timestamp or int(time.time()) logger.info(f"Mock: Sending message to channel {channel_idx}: {text[:20]}...") return True diff --git a/src/meshcore_hub/interface/receiver.py b/src/meshcore_hub/interface/receiver.py index 78d2281..4adcca6 100644 --- a/src/meshcore_hub/interface/receiver.py +++ b/src/meshcore_hub/interface/receiver.py @@ -15,7 +15,6 @@ from typing import Any, Optional from meshcore_hub.common.mqtt import MQTTClient, MQTTConfig from meshcore_hub.interface.device import ( BaseMeshCoreDevice, - DeviceConfig, EventType, create_device, ) diff --git a/src/meshcore_hub/interface/sender.py b/src/meshcore_hub/interface/sender.py index 5c08a1e..14a7e4c 100644 --- a/src/meshcore_hub/interface/sender.py +++ b/src/meshcore_hub/interface/sender.py @@ -6,7 +6,6 @@ In SENDER mode, the interface: 3. Executes received commands on the device """ -import json import logging import signal import threading @@ -16,7 +15,6 @@ from typing import Any, Optional from meshcore_hub.common.mqtt import MQTTClient, MQTTConfig from meshcore_hub.interface.device import ( BaseMeshCoreDevice, - DeviceConfig, create_device, ) diff --git a/src/meshcore_hub/web/app.py b/src/meshcore_hub/web/app.py index db9a1b9..3deaecc 100644 --- a/src/meshcore_hub/web/app.py +++ b/src/meshcore_hub/web/app.py @@ -7,7 +7,6 @@ from typing import AsyncGenerator import httpx from fastapi import FastAPI, Request -from fastapi.responses import HTMLResponse from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates @@ -132,7 +131,8 @@ def create_app( def get_templates(request: Request) -> Jinja2Templates: """Get templates from app state.""" - return request.app.state.templates + templates: Jinja2Templates = request.app.state.templates + return templates def get_network_context(request: Request) -> dict: diff --git a/src/meshcore_hub/web/routes/members.py b/src/meshcore_hub/web/routes/members.py index f730a05..60d9870 100644 --- a/src/meshcore_hub/web/routes/members.py +++ b/src/meshcore_hub/web/routes/members.py @@ -13,7 +13,7 @@ logger = logging.getLogger(__name__) router = APIRouter() -def load_members(members_file: str | None) -> list[dict]: +def load_members(members_file: str | None) -> list[dict[str, str]]: """Load members from JSON file. Args: @@ -32,9 +32,11 @@ def load_members(members_file: str | None) -> list[dict]: data = json.load(f) # Handle both list and dict with "members" key if isinstance(data, list): - return data + return list(data) elif isinstance(data, dict) and "members" in data: - return data["members"] + members = data["members"] + if isinstance(members, list): + return list(members) else: logger.warning(f"Members file not found: {members_file}") except Exception as e: diff --git a/src/meshcore_hub/web/routes/messages.py b/src/meshcore_hub/web/routes/messages.py index a0ade5e..1a11d2e 100644 --- a/src/meshcore_hub/web/routes/messages.py +++ b/src/meshcore_hub/web/routes/messages.py @@ -29,7 +29,7 @@ async def messages_list( offset = (page - 1) * limit # Build query params - params = {"limit": limit, "offset": offset} + params: dict[str, int | str] = {"limit": limit, "offset": offset} if message_type: params["message_type"] = message_type if channel_idx is not None: diff --git a/src/meshcore_hub/web/routes/nodes.py b/src/meshcore_hub/web/routes/nodes.py index e9f7bbb..5810733 100644 --- a/src/meshcore_hub/web/routes/nodes.py +++ b/src/meshcore_hub/web/routes/nodes.py @@ -28,7 +28,7 @@ async def nodes_list( offset = (page - 1) * limit # Build query params - params = {"limit": limit, "offset": offset} + params: dict[str, int | str] = {"limit": limit, "offset": offset} if search: params["search"] = search if adv_type: diff --git a/tests/test_api/test_advertisements.py b/tests/test_api/test_advertisements.py index e5c2387..6e2763c 100644 --- a/tests/test_api/test_advertisements.py +++ b/tests/test_api/test_advertisements.py @@ -1,7 +1,5 @@ """Tests for advertisement API routes.""" -import pytest - class TestListAdvertisements: """Tests for GET /advertisements endpoint.""" diff --git a/tests/test_api/test_auth.py b/tests/test_api/test_auth.py index b084090..580b804 100644 --- a/tests/test_api/test_auth.py +++ b/tests/test_api/test_auth.py @@ -1,7 +1,5 @@ """Tests for API authentication.""" -import pytest - class TestAuthenticationFlow: """Tests for authentication behavior.""" diff --git a/tests/test_api/test_commands.py b/tests/test_api/test_commands.py index ecfd0c1..f0d5b0b 100644 --- a/tests/test_api/test_commands.py +++ b/tests/test_api/test_commands.py @@ -1,7 +1,5 @@ """Tests for command API routes.""" -import pytest - class TestSendMessage: """Tests for POST /commands/send-message endpoint.""" diff --git a/tests/test_api/test_dashboard.py b/tests/test_api/test_dashboard.py index 7d04408..0d5ff54 100644 --- a/tests/test_api/test_dashboard.py +++ b/tests/test_api/test_dashboard.py @@ -1,7 +1,5 @@ """Tests for dashboard API routes.""" -import pytest - class TestDashboardStats: """Tests for GET /dashboard/stats endpoint.""" diff --git a/tests/test_api/test_messages.py b/tests/test_api/test_messages.py index 3d3b878..f7b1425 100644 --- a/tests/test_api/test_messages.py +++ b/tests/test_api/test_messages.py @@ -1,7 +1,5 @@ """Tests for message API routes.""" -import pytest - class TestListMessages: """Tests for GET /messages endpoint.""" diff --git a/tests/test_api/test_nodes.py b/tests/test_api/test_nodes.py index 1804f90..ad932da 100644 --- a/tests/test_api/test_nodes.py +++ b/tests/test_api/test_nodes.py @@ -1,7 +1,5 @@ """Tests for node API routes.""" -import pytest - class TestListNodes: """Tests for GET /nodes endpoint.""" diff --git a/tests/test_api/test_telemetry.py b/tests/test_api/test_telemetry.py index b303024..ff7c802 100644 --- a/tests/test_api/test_telemetry.py +++ b/tests/test_api/test_telemetry.py @@ -1,7 +1,5 @@ """Tests for telemetry API routes.""" -import pytest - class TestListTelemetry: """Tests for GET /telemetry endpoint.""" diff --git a/tests/test_api/test_trace_paths.py b/tests/test_api/test_trace_paths.py index 7ceaa2e..0bdde36 100644 --- a/tests/test_api/test_trace_paths.py +++ b/tests/test_api/test_trace_paths.py @@ -1,7 +1,5 @@ """Tests for trace path API routes.""" -import pytest - class TestListTracePaths: """Tests for GET /trace-paths endpoint.""" diff --git a/tests/test_collector/conftest.py b/tests/test_collector/conftest.py index c8c7592..7026b0a 100644 --- a/tests/test_collector/conftest.py +++ b/tests/test_collector/conftest.py @@ -1,11 +1,8 @@ """Fixtures for collector component tests.""" import pytest -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker from meshcore_hub.common.database import DatabaseManager -from meshcore_hub.common.models import Base @pytest.fixture diff --git a/tests/test_collector/test_handlers/test_advertisement.py b/tests/test_collector/test_handlers/test_advertisement.py index c4a7d50..6370396 100644 --- a/tests/test_collector/test_handlers/test_advertisement.py +++ b/tests/test_collector/test_handlers/test_advertisement.py @@ -1,6 +1,5 @@ """Tests for advertisement handler.""" -import pytest from sqlalchemy import select from meshcore_hub.common.models import Advertisement, Node diff --git a/tests/test_collector/test_handlers/test_message.py b/tests/test_collector/test_handlers/test_message.py index fdf32d7..20d5b50 100644 --- a/tests/test_collector/test_handlers/test_message.py +++ b/tests/test_collector/test_handlers/test_message.py @@ -1,6 +1,5 @@ """Tests for message handlers.""" -import pytest from sqlalchemy import select from meshcore_hub.common.models import Message, Node diff --git a/tests/test_collector/test_handlers/test_telemetry.py b/tests/test_collector/test_handlers/test_telemetry.py index 17f89bd..60816b9 100644 --- a/tests/test_collector/test_handlers/test_telemetry.py +++ b/tests/test_collector/test_handlers/test_telemetry.py @@ -1,6 +1,5 @@ """Tests for telemetry handler.""" -import pytest from sqlalchemy import select from meshcore_hub.common.models import Node, Telemetry diff --git a/tests/test_common/test_models.py b/tests/test_common/test_models.py index a79d504..f1385ae 100644 --- a/tests/test_common/test_models.py +++ b/tests/test_common/test_models.py @@ -155,6 +155,7 @@ class TestTelemetryModel: db_session.commit() assert telemetry.id is not None + assert telemetry.parsed_data is not None assert telemetry.parsed_data["temperature"] == 22.5 @@ -175,4 +176,5 @@ class TestEventLogModel: assert event.id is not None assert event.event_type == "BATTERY" + assert event.payload is not None assert event.payload["battery_percentage"] == 75 diff --git a/tests/test_interface/conftest.py b/tests/test_interface/conftest.py index 96b4532..c3b5611 100644 --- a/tests/test_interface/conftest.py +++ b/tests/test_interface/conftest.py @@ -1,8 +1,10 @@ """Fixtures for interface component tests.""" +from collections.abc import Generator + import pytest -from meshcore_hub.interface.device import DeviceConfig, EventType +from meshcore_hub.interface.device import DeviceConfig from meshcore_hub.interface.mock_device import MockDeviceConfig, MockMeshCoreDevice @@ -27,7 +29,9 @@ def mock_device_config() -> MockDeviceConfig: @pytest.fixture -def mock_device(device_config, mock_device_config) -> MockMeshCoreDevice: +def mock_device( + device_config: DeviceConfig, mock_device_config: MockDeviceConfig +) -> Generator[MockMeshCoreDevice, None, None]: """Create a mock device instance for testing.""" device = MockMeshCoreDevice(device_config, mock_device_config) yield device diff --git a/tests/test_interface/test_device.py b/tests/test_interface/test_device.py index 73e0894..83cf900 100644 --- a/tests/test_interface/test_device.py +++ b/tests/test_interface/test_device.py @@ -1,7 +1,5 @@ """Tests for device abstraction.""" -import pytest - from meshcore_hub.interface.device import ( DeviceConfig, EventType, diff --git a/tests/test_interface/test_mock_device.py b/tests/test_interface/test_mock_device.py index 5c05361..89e5062 100644 --- a/tests/test_interface/test_mock_device.py +++ b/tests/test_interface/test_mock_device.py @@ -1,8 +1,6 @@ """Tests for mock device implementation.""" -import pytest import time -import threading from meshcore_hub.interface.device import EventType from meshcore_hub.interface.mock_device import ( diff --git a/tests/test_interface/test_receiver.py b/tests/test_interface/test_receiver.py index fc8e023..ff0d376 100644 --- a/tests/test_interface/test_receiver.py +++ b/tests/test_interface/test_receiver.py @@ -3,8 +3,7 @@ import pytest from unittest.mock import MagicMock, patch -from meshcore_hub.interface.device import DeviceConfig, EventType -from meshcore_hub.interface.mock_device import MockDeviceConfig, MockMeshCoreDevice +from meshcore_hub.interface.device import EventType from meshcore_hub.interface.receiver import Receiver, create_receiver @@ -69,7 +68,7 @@ class TestCreateReceiver: def test_creates_receiver_with_mock_device(self): """Test creating receiver with mock device.""" - with patch("meshcore_hub.interface.receiver.MQTTClient") as MockMQTT: + with patch("meshcore_hub.interface.receiver.MQTTClient"): receiver = create_receiver(mock=True) assert receiver is not None @@ -78,8 +77,8 @@ class TestCreateReceiver: def test_creates_receiver_with_custom_mqtt_config(self): """Test creating receiver with custom MQTT configuration.""" - with patch("meshcore_hub.interface.receiver.MQTTClient") as MockMQTT: - receiver = create_receiver( + with patch("meshcore_hub.interface.receiver.MQTTClient") as mock_mqtt: + create_receiver( mock=True, mqtt_host="mqtt.example.com", mqtt_port=8883, @@ -87,8 +86,8 @@ class TestCreateReceiver: ) # Verify MQTT client was created with correct config - MockMQTT.assert_called_once() - config = MockMQTT.call_args[0][0] + mock_mqtt.assert_called_once() + config = mock_mqtt.call_args[0][0] assert config.host == "mqtt.example.com" assert config.port == 8883 assert config.prefix == "custom" diff --git a/tests/test_interface/test_sender.py b/tests/test_interface/test_sender.py index 7d86d74..08044ec 100644 --- a/tests/test_interface/test_sender.py +++ b/tests/test_interface/test_sender.py @@ -3,8 +3,6 @@ import pytest from unittest.mock import MagicMock, patch -from meshcore_hub.interface.device import DeviceConfig, EventType -from meshcore_hub.interface.mock_device import MockDeviceConfig, MockMeshCoreDevice from meshcore_hub.interface.sender import Sender, create_sender @@ -107,7 +105,7 @@ class TestCreateSender: def test_creates_sender_with_mock_device(self): """Test creating sender with mock device.""" - with patch("meshcore_hub.interface.sender.MQTTClient") as MockMQTT: + with patch("meshcore_hub.interface.sender.MQTTClient"): sender = create_sender(mock=True) assert sender is not None @@ -116,16 +114,16 @@ class TestCreateSender: def test_creates_sender_with_custom_mqtt_config(self): """Test creating sender with custom MQTT configuration.""" - with patch("meshcore_hub.interface.sender.MQTTClient") as MockMQTT: - sender = create_sender( + with patch("meshcore_hub.interface.sender.MQTTClient") as mock_mqtt: + create_sender( mock=True, mqtt_host="mqtt.example.com", mqtt_port=8883, mqtt_prefix="custom", ) - MockMQTT.assert_called_once() - config = MockMQTT.call_args[0][0] + mock_mqtt.assert_called_once() + config = mock_mqtt.call_args[0][0] assert config.host == "mqtt.example.com" assert config.port == 8883 assert config.prefix == "custom"