Exceptions¶
New in v5.0: Complete exception architecture with enterprise-grade error handling.
The nwp500 library provides a structured exception hierarchy for error handling. All custom exceptions inherit from a base class and provide structured error information.
Exception Hierarchy¶
All library exceptions inherit from Nwp500Error:
Nwp500Error (base)
├── AuthenticationError
│ ├── InvalidCredentialsError
│ ├── TokenExpiredError
│ └── TokenRefreshError
├── APIError
├── MqttError
│ ├── MqttConnectionError
│ ├── MqttNotConnectedError
│ ├── MqttPublishError
│ ├── MqttSubscriptionError
│ └── MqttCredentialsError
├── ValidationError
│ ├── ParameterValidationError
│ └── RangeValidationError
└── DeviceError
├── DeviceNotFoundError
├── DeviceOfflineError
├── DeviceOperationError
└── DeviceCapabilityError
Base Exception¶
Nwp500Error¶
- class Nwp500Error(message, *, error_code=None, details=None, retriable=False)¶
Base exception for all nwp500 library errors.
All custom exceptions in the library inherit from this base class, allowing consumers to catch all library-specific errors with a single handler.
- Parameters:
Attributes:
message(str) - Human-readable error messageerror_code(str or None) - Machine-readable error codedetails(dict) - Additional contextretriable(bool) - Whether operation can be retried
Methods:
to_dict()- Serialize exception for logging/monitoring
Example - Catching all library errors:
from nwp500 import NavienMqttClient, Nwp500Error try: mqtt = NavienMqttClient(auth) await mqtt.connect() await mqtt.request_device_status(device) except Nwp500Error as e: # Catches all library exceptions print(f"Library error: {e}") # Check if retriable if e.retriable: print("This operation can be retried") # Log structured data logger.error("Operation failed", extra=e.to_dict())
Authentication Exceptions¶
AuthenticationError¶
- class AuthenticationError(message, status_code=None, response=None, **kwargs)¶
Base exception for authentication-related errors.
- Parameters:
Attributes:
message(str) - Error messagestatus_code(int or None) - HTTP status coderesponse(dict or None) - Full API response
InvalidCredentialsError¶
- class InvalidCredentialsError¶
Raised when email/password combination is incorrect.
Subclass of
AuthenticationError. Typically indicates a 401 Unauthorized response from the API.Example:
from nwp500 import NavienAuthClient, InvalidCredentialsError try: async with NavienAuthClient(email, password) as auth: pass except InvalidCredentialsError as e: print(f"Invalid credentials: {e}") print("Please check your email and password") # Prompt user to re-enter credentials
TokenExpiredError¶
- class TokenExpiredError¶
Raised when an authentication token has expired.
Subclass of
AuthenticationError. Tokens have a limited lifetime and must be refreshed periodically.
TokenRefreshError¶
- class TokenRefreshError¶
Raised when token refresh operation fails.
Subclass of
AuthenticationError. Occurs when refresh token is invalid or expired, requiring full re-authentication.Example:
from nwp500 import NavienAuthClient, TokenRefreshError try: await auth.ensure_valid_token() except TokenRefreshError as e: print(f"Token refresh failed: {e}") print("Re-authenticating with fresh credentials") await auth.sign_in(email, password)
API Exceptions¶
APIError¶
- class APIError(message, code=None, response=None, **kwargs)¶
Raised when REST API returns an error response.
- Parameters:
Common HTTP codes:
400 - Bad request (invalid parameters)
401 - Unauthorized (authentication failed)
404 - Not found (device or resource missing)
429 - Rate limited (too many requests)
500 - Server error (Navien API issue)
503 - Service unavailable (API down)
Example:
from nwp500 import NavienAPIClient, APIError try: device = await api.get_device_info("invalid_mac") except APIError as e: print(f"API error: {e.message}") if e.code == 404: print("Device not found") elif e.code == 401: print("Authentication failed") elif e.code >= 500: print("Server error - try again later")
MQTT Exceptions¶
MqttError¶
- class MqttError¶
Base exception for MQTT operations.
All MQTT-related errors inherit from this base class, allowing consumers to handle all MQTT issues with a single exception handler.
MqttConnectionError¶
- class MqttConnectionError¶
Connection establishment or maintenance failed.
Raised when the MQTT connection to AWS IoT Core cannot be established or when an existing connection fails. May be due to network issues, invalid credentials, or AWS service problems.
Example:
from nwp500 import NavienMqttClient, MqttConnectionError try: mqtt = NavienMqttClient(auth) await mqtt.connect() except MqttConnectionError as e: print(f"Connection failed: {e}") print("Check network connectivity and AWS credentials")
MqttNotConnectedError¶
- class MqttNotConnectedError¶
Operation requires active MQTT connection.
Raised when attempting MQTT operations (publish, subscribe, etc.) without an established connection. Call
connect()before performing operations.Example:
from nwp500 import NavienMqttClient, MqttNotConnectedError mqtt = NavienMqttClient(auth) try: await mqtt.request_device_status(device) except MqttNotConnectedError: # Not connected - establish connection first await mqtt.connect() await mqtt.request_device_status(device)
MqttPublishError¶
- class MqttPublishError¶
Failed to publish message to MQTT broker.
Raised when a message cannot be published to an MQTT topic. This may occur during connection interruptions or when the broker rejects the message.
Often includes
retriable=Trueflag for intelligent retry strategies.Example with retry:
from nwp500 import MqttPublishError import asyncio async def publish_with_retry(mqtt, topic, payload, max_retries=3): for attempt in range(max_retries): try: await mqtt.publish(topic, payload) return # Success except MqttPublishError as e: if e.retriable and attempt < max_retries - 1: wait_time = 2 ** attempt # Exponential backoff print(f"Retry in {wait_time}s...") await asyncio.sleep(wait_time) else: raise # Not retriable or max retries reached
MqttSubscriptionError¶
- class MqttSubscriptionError¶
Failed to subscribe to MQTT topic.
Raised when subscription to an MQTT topic fails. This may occur if the connection is interrupted or if the client lacks permissions for the topic.
MqttCredentialsError¶
- class MqttCredentialsError¶
AWS credentials invalid or expired.
Raised when AWS IoT credentials are missing, invalid, or expired. Re-authentication may be required to obtain fresh credentials.
Example:
from nwp500 import NavienMqttClient, MqttCredentialsError try: mqtt = NavienMqttClient(auth) except MqttCredentialsError as e: print(f"Credentials error: {e}") print("Re-authenticating to get fresh AWS credentials") await auth.sign_in(email, password)
Validation Exceptions¶
ValidationError¶
- class ValidationError¶
Base exception for validation failures.
Raised when input parameters or data fail validation checks.
ParameterValidationError¶
- class ParameterValidationError(message, parameter=None, value=None, **kwargs)¶
Invalid parameter value provided.
Raised when a parameter value is invalid for reasons other than being out of range (e.g., wrong type, invalid format).
- Parameters:
parameter (str or None) – Name of the invalid parameter
value (Any) – The invalid value provided
RangeValidationError¶
- class RangeValidationError(message, field=None, value=None, min_value=None, max_value=None, **kwargs)¶
Value outside acceptable range.
Raised when a numeric value is outside its valid range.
- Parameters:
field (str or None) – Name of the field
value (Any) – The invalid value provided
min_value (Any) – Minimum acceptable value
max_value (Any) – Maximum acceptable value
Example:
from nwp500 import NavienMqttClient, RangeValidationError try: await mqtt.set_dhw_temperature(device, 200.0) except RangeValidationError as e: print(f"Invalid {e.field}: {e.value}") print(f"Valid range: {e.min_value} to {e.max_value}") # Output: Invalid temperature_f: 200.0 # Valid range: 95 to 150
Device Exceptions¶
DeviceError¶
- class DeviceError¶
Base exception for device operations.
All device-related errors inherit from this base class.
DeviceNotFoundError¶
- class DeviceNotFoundError¶
Requested device not found.
Raised when a device cannot be found in the user’s device list or when attempting to access a non-existent device.
DeviceOfflineError¶
- class DeviceOfflineError¶
Device is offline or unreachable.
Raised when a device is offline and cannot respond to commands or status requests. The device may be powered off, disconnected from the network, or experiencing connectivity issues.
DeviceOperationError¶
- class DeviceOperationError¶
Device operation failed.
Raised when a device operation (mode change, temperature setting, etc.) fails. This may occur due to invalid commands, device restrictions, or device-side errors.
DeviceCapabilityError¶
- class DeviceCapabilityError(feature, message=None, **kwargs)¶
Device doesn’t support a required controllable feature.
Raised when attempting to execute a command on a device that doesn’t support the feature. This is raised by control commands decorated with
@requires_capabilitywhen the device doesn’t have the necessary capability.- Parameters:
Attributes:
feature(str) - Name of the unsupported featuremessage(str) - Human-readable error message
Example:
from nwp500 import NavienMqttClient, DeviceCapabilityError mqtt = NavienMqttClient(auth) await mqtt.connect() # Request device info first await mqtt.subscribe_device_feature(device, lambda f: None) await mqtt.request_device_info(device) try: # This raises DeviceCapabilityError if device doesn't support recirculation await mqtt.set_recirculation_mode(device, 1) except DeviceCapabilityError as e: print(f"Feature not supported: {e.feature}") print(f"Error: {e}")
Supported Controllable Features:
power_use- Device power on/off controldhw_use- DHW mode changesdhw_temperature_setting_use- DHW temperature controlholiday_use- Vacation/away modeprogram_reservation_use- Reservations and TOU schedulingrecirculation_use- Recirculation pump controlrecirc_reservation_use- Recirculation scheduling
Checking Capabilities Before Control:
from nwp500.device_capabilities import DeviceCapabilityChecker # Check if device supports a feature if DeviceCapabilityChecker.supports("recirculation_use", device_features): await mqtt.set_recirculation_mode(device, 1) else: print("Device doesn't support recirculation")
Viewing All Available Controls:
from nwp500.device_capabilities import DeviceCapabilityChecker controls = DeviceCapabilityChecker.get_available_controls(device_features) for feature, supported in controls.items(): status = "✓" if supported else "✗" print(f"{status} {feature}")
Error Handling Patterns¶
Pattern 1: Specific Exception Handling¶
Handle specific exception types for granular control:
from nwp500 import (
NavienAuthClient,
NavienMqttClient,
InvalidCredentialsError,
MqttNotConnectedError,
RangeValidationError,
)
async def robust_operation():
try:
async with NavienAuthClient(email, password) as auth:
mqtt = NavienMqttClient(auth)
await mqtt.connect()
await mqtt.set_dhw_temperature(device, 120.0)
except InvalidCredentialsError:
print("Invalid credentials - check email/password")
except MqttNotConnectedError:
print("MQTT not connected - device may be offline")
except RangeValidationError as e:
print(f"Invalid {e.field}: {e.value}")
print(f"Valid range: {e.min_value} to {e.max_value}")
Pattern 2: Category-Based Handling¶
Catch exception categories (Auth, MQTT, Validation):
from nwp500 import (
AuthenticationError,
MqttError,
ValidationError,
Nwp500Error,
)
try:
# Operations
pass
except AuthenticationError as e:
print(f"Authentication failed: {e}")
# Re-authenticate
except MqttError as e:
print(f"MQTT error: {e}")
# Check connection
except ValidationError as e:
print(f"Invalid input: {e}")
# Fix parameters
Pattern 3: Retry Logic with retriable Flag¶
Implement intelligent retry strategies:
from nwp500 import MqttPublishError
import asyncio
async def operation_with_retry(max_retries=3):
for attempt in range(max_retries):
try:
await mqtt.publish(topic, payload)
return # Success
except MqttPublishError as e:
if e.retriable and attempt < max_retries - 1:
wait_time = 2 ** attempt # Exponential backoff
print(f"Attempt {attempt + 1} failed, retrying in {wait_time}s")
await asyncio.sleep(wait_time)
else:
print(f"Operation failed: {e}")
raise
Pattern 4: Device Capability Checking¶
Handle capability errors for device control commands:
from nwp500 import NavienMqttClient, DeviceCapabilityError
from nwp500.device_capabilities import DeviceCapabilityChecker
async def control_with_capability_check():
mqtt = NavienMqttClient(auth)
await mqtt.connect()
# Request device info first
await mqtt.subscribe_device_feature(device, lambda f: None)
await mqtt.request_device_info(device)
# Option 1: Try control and catch capability error
try:
await mqtt.set_recirculation_mode(device, 1)
except DeviceCapabilityError as e:
print(f"Device doesn't support: {e.feature}")
# Fallback to alternative command
# Option 2: Check capability before attempting
if DeviceCapabilityChecker.supports("recirculation_use", device_features):
await mqtt.set_recirculation_mode(device, 1)
else:
print("Recirculation not supported")
# Option 3: View all available controls
controls = DeviceCapabilityChecker.get_available_controls(device_features)
for feature, supported in controls.items():
if supported:
print(f"✓ {feature} supported")
Pattern 5: Structured Logging¶
Use to_dict() for structured error logging:
import logging
from nwp500 import Nwp500Error
logger = logging.getLogger(__name__)
try:
await mqtt.request_device_status(device)
except Nwp500Error as e:
# Log structured error data
logger.error("Operation failed", extra=e.to_dict())
# Output includes: error_type, message, error_code, details, retriable
Pattern 5: Catch-All with Base Exception¶
Catch all library exceptions with Nwp500Error:
from nwp500 import Nwp500Error
try:
# Any library operation
await mqtt.connect()
await mqtt.request_device_status(device)
except Nwp500Error as e:
# All nwp500 exceptions inherit from Nwp500Error
print(f"Library error: {e}")
# Check if retriable
if e.retriable:
print("This operation can be retried")
# Log for debugging
logger.error("Operation failed", extra=e.to_dict())
Exception Chaining¶
New in v5.0: All exception wrapping preserves the original exception chain.
When the library wraps exceptions (e.g., wrapping aiohttp.ClientError in
AuthenticationError), the original exception is preserved using Python’s
raise ... from syntax.
Example - Inspecting exception chains:
from nwp500 import AuthenticationError
import aiohttp
try:
async with NavienAuthClient(email, password) as auth:
pass
except AuthenticationError as e:
print(f"Authentication error: {e}")
# Check for original cause
if e.__cause__:
print(f"Original error: {e.__cause__}")
print(f"Original type: {type(e.__cause__).__name__}")
# Was it a network error?
if isinstance(e.__cause__, aiohttp.ClientError):
print("Network connectivity issue")
This preserves full stack traces for debugging in production.
Best Practices¶
Catch specific exceptions first, then general:
try: await mqtt.connect() except MqttNotConnectedError: # Handle specific case pass except MqttError: # Handle general MQTT errors pass except Nwp500Error: # Handle any library error pass
Use exception attributes for user-friendly messages:
try: await mqtt.set_dhw_temperature(device, 200.0) except RangeValidationError as e: # Show helpful message print(f"Temperature must be between {e.min_value}°F and {e.max_value}°F")
Check retriable flag before retrying:
try: await mqtt.publish(topic, payload) except MqttPublishError as e: if e.retriable: # Safe to retry await asyncio.sleep(1) await mqtt.publish(topic, payload) else: # Don't retry raise
Use to_dict() for monitoring/logging:
try: await operation() except Nwp500Error as e: # Send structured data to monitoring system monitoring.record_exception(e.to_dict())
Always cleanup resources:
mqtt = NavienMqttClient(auth) try: await mqtt.connect() # Operations except Nwp500Error as e: print(f"Error: {e}") finally: await mqtt.disconnect()
Migration from v4.x¶
If upgrading from v4.x, update your exception handling:
Before (v4.x):
try:
await mqtt.request_device_status(device)
except RuntimeError as e:
if "Not connected" in str(e):
await mqtt.connect()
After (v5.0+):
from nwp500 import MqttNotConnectedError
try:
await mqtt.request_device_status(device)
except MqttNotConnectedError:
await mqtt.connect()
await mqtt.request_device_status(device)
See the CHANGELOG.rst for complete migration guide with more examples.