Source code for nwp500.mqtt.client

"""
MQTT Client for Navien Smart Control.

This module provides an MQTT client for real-time communication with Navien
devices using AWS IoT Core. It handles connection, subscriptions, and message
publishing for device control and monitoring.

The client uses WebSocket connections with AWS credentials obtained from
the authentication flow.
"""

import asyncio
import concurrent.futures
import logging
import uuid
from collections.abc import Callable
from typing import TYPE_CHECKING, Any

from awscrt.exceptions import AwsCrtError

from ..auth import NavienAuthClient
from ..events import EventEmitter
from ..exceptions import (
    AuthenticationError,
    MqttConnectionError,
    MqttCredentialsError,
    MqttNotConnectedError,
    MqttPublishError,
    TokenRefreshError,
)
from ..mqtt_events import (
    ConnectionInterruptedEvent,
    ConnectionResumedEvent,
    MqttClientEvents,
)
from ..unit_system import UnitSystemType
from ._control_commands import DeviceControlCommandsMixin
from ._device_subscriptions import DeviceSubscriptionsMixin
from .command_queue import MqttCommandQueue
from .connection import MqttConnection
from .control import MqttDeviceController
from .diagnostics import MqttDiagnosticsCollector
from .periodic import MqttPeriodicRequestManager
from .reconnection import MqttReconnectionHandler
from .subscriptions import MqttSubscriptionManager
from .types import MqttConnectionHandle, QoS
from .utils import (
    MqttConnectionConfig,
    PeriodicRequestType,
)

if TYPE_CHECKING:
    from ..models import (
        Device,
        DeviceFeature,
    )

__author__ = "Emmanuel Levijarvi"
__copyright__ = "Emmanuel Levijarvi"
__license__ = "MIT"

_logger = logging.getLogger(__name__)


def _log_scheduled_coroutine_result(
    future: concurrent.futures.Future[Any],
) -> None:
    """Surface exceptions from scheduled coroutines.

    Attached to futures returned by ``run_coroutine_threadsafe``.
    Without this callback the returned future is discarded and any
    exception (e.g. resubscribe failure after a clean-session resume) is
    silently swallowed.
    """
    if future.cancelled():
        return
    exc = future.exception()
    if exc is not None:
        # Pass an explicit (type, value, traceback) tuple: a bare
        # exception instance is treated by logging as a truthy flag,
        # which would log the *current* exception context (empty in a
        # done-callback) instead of the coroutine's traceback.
        _logger.error(
            "Scheduled coroutine failed: %s",
            exc,
            exc_info=(type(exc), exc, exc.__traceback__),
        )