diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 9beeeb2..f8d45fb 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -74,9 +74,9 @@ Report the results of these checks in your final summary, including: ### After Completing a Task Document validation results: -- ✅ **Linting**: All checks passed -- ✅ **Type checking**: No errors found -- ✅ **Tests**: X/X passed (or "N/A - no existing tests for this feature") +- **Linting**: All checks passed +- **Type checking**: No errors found +- **Tests**: X/X passed (or "N/A - no existing tests for this feature") ## Patterns & Conventions - **Async context managers** for authentication: `async with NavienAuthClient(email, password) as auth_client:` @@ -156,8 +156,8 @@ Removed ``` ## Final Results **Starting point:** X errors - **Ending point:** 0 errors ✅ - **Tests:** All passing ✓ + **Ending point:** 0 errors + **Tests:** All passing ## What Was Fixed - Module 1 - Brief description (N errors) diff --git a/.gitignore b/.gitignore index f393443..5bf0e4f 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,4 @@ venv*/ .python-version .secrets reference/* +resources/* diff --git a/docs/development/history.rst b/docs/development/history.rst index ae4f1b1..12cc5a2 100644 --- a/docs/development/history.rst +++ b/docs/development/history.rst @@ -23,24 +23,22 @@ providing: Current Status -------------- -**Production Ready** ✅ - The library is feature-complete with: -- ✅ Complete authentication (AWS Cognito + JWT) -- ✅ REST API client (all endpoints) -- ✅ MQTT client with real-time communication -- ✅ Event emitter pattern (Phase 1 complete) -- ✅ Automatic reconnection with exponential backoff -- ✅ Command queue for disconnection handling -- ✅ Device control (power, temperature, modes) -- ✅ Real-time monitoring (status, features, energy) -- ✅ Historical energy usage data (daily breakdown) -- ✅ Thread-safe event emission from MQTT callbacks -- ✅ Comprehensive documentation -- ✅ Working examples for all features -- ✅ Unit tests with good coverage -- ✅ Python 3.9+ with modern type hints +- Complete authentication (AWS Cognito + JWT) +- REST API client (all endpoints) +- MQTT client with real-time communication +- Event emitter pattern (Phase 1 complete) +- Automatic reconnection with exponential backoff +- Command queue for disconnection handling +- Device control (power, temperature, modes) +- Real-time monitoring (status, features, energy) +- Historical energy usage data (daily breakdown) +- Thread-safe event emission from MQTT callbacks +- Comprehensive documentation +- Working examples for all features +- Unit tests with good coverage +- Python 3.9+ with modern type hints Implementation Milestones ------------------------- @@ -116,7 +114,7 @@ query example Energy Data API (October 7, 2025) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -**Status:** ✅ Implemented +**Status:** Implemented Complete energy monitoring capabilities including historical data: @@ -175,18 +173,18 @@ Testing & Verification All components have been tested with real Navien NWP500 devices: -**Authentication:** ✅ Verified with production API - Sign-in flow +**Authentication:** Verified with production API - Sign-in flow working - Token refresh working - AWS credentials properly obtained -**REST API:** ✅ All endpoints tested - Device listing working - Device +**REST API:** All endpoints tested - Device listing working - Device info retrieval working - Firmware info working -**MQTT Client:** ✅ Real-time communication verified - WebSocket +**MQTT Client:** Real-time communication verified - WebSocket connection established - Commands sent and acknowledged - Status messages received and parsed - Device control working (power, temperature, mode) -**Test Coverage:** ✅ Comprehensive - Unit tests for data models - +**Test Coverage:** Comprehensive - Unit tests for data models - Integration tests with real API - Interactive examples for all features Architecture Decisions @@ -282,7 +280,7 @@ MQTT callbacks run in separate threads (e.g., 'Dummy-1') created by AWS IoT SDK. Command Queue Implementation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -**Status:** ✅ Implemented +**Status:** Implemented Automatic command queuing for reliable communication during network interruptions: @@ -321,18 +319,6 @@ Modernized codebase to use Python 3.9+ native type hints (PEP 585): - Python version classifiers added (3.9-3.13) - ruff target-version updated to py39 -Future Enhancements -------------------- - -Potential areas for future development: - -1. **Event System Phase 2:** Event filtering with lambda conditions, event middleware, event buffering and replay -2. **Event System Phase 3:** Event namespacing with wildcards (``device.*``), event history and time-travel debugging, performance metrics and monitoring -3. **Multiple Devices:** Efficient handling of multiple simultaneous device connections -4. **Configuration Validation:** Validate settings against device capabilities -5. **Command Priority Queue:** Different priority levels for different command types -6. **Queue Persistence:** Save queue to disk for recovery after restart - References ---------- diff --git a/docs/guides/advanced_features_explained.rst b/docs/guides/advanced_features_explained.rst new file mode 100644 index 0000000..e6634ed --- /dev/null +++ b/docs/guides/advanced_features_explained.rst @@ -0,0 +1,495 @@ +Advanced Features Explained: Weather-Responsive Heating, Demand Response, and Tank Stratification +================================================================================================== + +This document provides comprehensive technical documentation for three advanced NWP500 features. + +Overview of Advanced Features +----------------------------- + +The NWP500 heat pump water heater implements sophisticated algorithms for grid integration, environmental responsiveness, and efficiency optimization: + +1. **Weather-Responsive Heating** - Adjusts heating strategy based on ambient temperature conditions +2. **Demand Response Integration** - Responds to grid signals for demand/response events (CTA-2045) +3. **Tank Stratification Optimization** - Uses dual temperature sensors for enhanced heating efficiency + +Weather-Responsive Heating +========================== + +Feature Overview +^^^^^^^^^^^^^^^^ + +The device continuously monitors ambient air temperature to optimize heat pump performance and adjust heating strategies. This enables the system to maintain comfort while adapting to seasonal conditions automatically. + +Technical Implementation +^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Data Sources**: + +- ``ambientTemperature`` (decicelsius_to_f): Heat pump outlet air temperature measurement +- ``outsideTemperature`` (raw integer): Weather data temperature from cloud API/device configuration +- ``evaporatorTemperature`` (decicelsius_to_f): Evaporator coil temperature during heat pump operation + + +How It Works +^^^^^^^^^^^^ + +**Temperature Thresholds and Heating Adjustments**: + +1. **High Ambient Temperature (>70°F / 21°C)** + - Heat pump COP (Coefficient of Performance) is high + - Device prioritizes heat pump operation over electric heating + - Lower superheat targets for efficient operation + - Reduced compressor activation frequency + +2. **Moderate Ambient Temperature (50-70°F / 10-21°C)** + - Balanced hybrid approach + - Heat pump and electric elements coordinate + - Optimal range for most climates + - Device operates with default efficiency settings + +3. **Cold Ambient Temperature (<50°F / 10°C)** + - Heat pump efficiency decreases significantly + - Device pre-charges tank before peak demand periods + - Electric heating elements engage more frequently + - At freezing (32°F / 0°C), COP drops 40-50% from optimal + +4. **Extreme Cold (<20°F / -7°C)** + - Heat pump operation becomes inefficient + - Device may default to electric-only mode during these periods + - Freeze protection mechanisms activate automatically + - Recovery time increases significantly + +**Algorithm Parameters**: + +The device maintains internal target superheat values that adjust based on ambient conditions. Superheat represents the temperature difference between evaporator outlet and compressor suction: + +.. code-block:: text + + Ideal superheat target: 10-20°F (5.5-11°C) + + Ambient 90°F: Target = 12°F (easier to achieve) + Ambient 60°F: Target = 15°F (standard) + Ambient 30°F: Target = 18°F (challenging, may not be achievable) + +**Compressor Control Adjustments**: + +- **High Ambient**: Lower ON/OFF temperature setpoints, reduced cycle frequency +- **Low Ambient**: Higher ON/OFF temperature setpoints, increased cycle frequency +- **Recovery Override**: Pre-charging during known demand periods (morning peak) + +Practical Applications +^^^^^^^^^^^^^^^^^^^^^^^ + +**Morning Peak Scenario (40°F Ambient)**: + +1. Device detects low ambient temperature overnight +2. If reservation calls for 140°F by 7 AM, device may start pre-charging at 5 AM +3. Uses both heat pump and electric elements (hybrid mode) +4. Reaches 140°F with hybrid approach, avoiding delay + +**Cold Spell Scenario (20°F Ambient)**: + +1. Device measures 20°F ambient, knows COP is ~1.8 +2. Switches to electric-only mode if heating needed +3. Avoids inefficient heat pump cycles +4. Reduces overall energy consumption despite higher per-BTU cost + +**Seasonal Optimization (Summer 90°F)**: + +1. Device sees high ambient temperature +2. Enables heat pump operation even for small heating demand +3. Operates compressor at lower speeds for precise temperature control +4. Achieves 3.5+ COP (for every 1 kW electrical, 3.5 kW of heat) + +Integration with MQTT Status Message +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``outsideTemperature`` field is transmitted in the device status update. Python clients can monitor this field: + +.. code-block:: python + + # From device status updates + status = await mqtt_client.get_status() + + # Access ambient temperature data + outdoor_temp = status.outside_temperature # Raw integer value + measured_ambient = status.ambient_temperature # Heat pump inlet measurement + evaporator_temp = status.evaporator_temperature # Coil temperature + +Demand Response Integration (CTA-2045) +====================================== + +Feature Overview +^^^^^^^^^^^^^^^^ + +The NWP500 supports demand response signals per the CTA-2045 (Consumer Technology Association) standard, enabling integration with smart grid programs and demand response events. + +**CTA-2045 Standard**: + +A protocol that allows utilities to send control signals to networked devices (like water heaters) to manage demand during peak periods or grid stress conditions. + +Technical Implementation +^^^^^^^^^^^^^^^^^^^^^^^^^ +DR Event Status Field +^^^^^^^^^^^^^^^^^^^^^ + +**Field**: ``drEventStatus`` (bitfield) + +**Type**: Integer (bitfield, each bit represents a different DR signal) + +**Values**: +- ``0``: No active DR events +- Non-zero: One or more DR signals active (specific bits depend on utility implementation) + +**Typical Signal Meanings**: + +.. list-table:: + :header-rows: 1 + :widths: 15 15 30 40 + + * - Signal + - Typical Cost + - Expected Duration + - Device Response + * - Shed (Bit 0) + - Very High + - 30-60 minutes + - Stop heating, reduce temperature + * - Reduce (Bit 1) + - High + - 1-4 hours + - Reduce heating, use heat pump only + * - Normal (Bit 2) + - Moderate + - Continuous + - Standard operation + * - Pre-charge (Bit 3) + - Low + - 1-2 hours + - Pre-heat tank before event + * - Emergency (Bit 4) + - Critical + - Minutes to hours + - Immediate halt/shutdown + +**Example DR Event Sequence**: + +.. code-block:: text + + Time Event drEventStatus Device Action + ---- ----- -------------- ---------------- + 2:00 PM Grid operator predicts peak (Normal operation) + 2:30 PM Pre-charge signal issued 0b00001000 Start heating now + 3:00 PM Peak period begins 0b00000010 Stop heating, reduce + 3:30 PM Peak continues 0b00000010 Heat pump only (low power) + 4:00 PM Peak period ends 0b00000001 Recover tank charge + 4:30 PM Normal operation restored 0b00000000 Resume standard schedule + +DR Override Status Field +^^^^^^^^^^^^^^^^^^^^^^^^ + +**Field**: ``drOverrideStatus`` (integer flag) + +**Purpose**: Tracks user-initiated overrides of demand response commands + +**Values**: +- ``0``: No override active, device responding to DR commands +- Non-zero: Override active for specified period (typically up to 72 hours) + +**User Override Scenario**: + +1. Grid issues "shed" command (stop all heating) +2. Device would halt heating for 1 hour +3. User needs hot water for emergency task +4. User presses "Override" in mobile app +5. Device allows heating for next 30 minutes (or configured duration) +6. ``drOverrideStatus`` set to non-zero, indicating override active +7. After override period expires, device returns to DR command compliance + +Implementation in Device Firmware +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Decision Tree** (inferred from status fields): + +.. code-block:: text + + IF drOverrideStatus != 0: + Allow all heating operations + Decrement override timer + ELSE IF drEventStatus != 0: + Determine signal type from drEventStatus bits + Apply corresponding power reduction + Adjust setpoints or compressor behavior + ELSE: + Execute normal reservation/TOU/mode schedule + +**Practical Grid Integration Benefits**: + +1. **Peak Shaving**: Reduce demand during 3-7 PM peak periods, saving 20-30% during those hours +2. **Rate Optimization**: Auto-respond to time-of-use pricing signals +3. **Grid Stability**: Participate in demand response events, earn utility incentives +4. **Cost Reduction**: Shift heating to low-price periods automatically + +Utility Integration Requirements +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To use demand response with your NWP500: + + +Tank Temperature Sensors +^^^^^^^^^^^^^^^^^^^^^^^^ + +**Upper Tank Sensor** (``tankUpperTemperature``) + +- **Location**: Near tank top, typically 12-18" below top +- **Measurement**: ``decicelsius_to_f`` conversion (tenths of Celsius to Fahrenheit) +- **Typical Range**: 110-160°F (43-71°C) +- **Purpose**: Indicates hot water availability for immediate draw +- **Control Target**: Used to trigger upper electric heating element and upper heat pump stage + +**Lower Tank Sensor** (``tankLowerTemperature``) + +- **Location**: Near tank bottom, typically 6-12" above lowest point +- **Measurement**: ``decicelsius_to_f`` conversion (tenths of Celsius to Fahrenheit) +- **Typical Range**: 95-155°F (35-68°C) +- **Purpose**: Monitors bulk tank heating progress +- **Control Target**: Used to trigger lower electric heating element and lower heat pump stage + +Tank Stratification Explained +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**What Is Stratification?** + +In a vertical tank, naturally occurring density differences create layers: + +.. code-block:: text + + 155°F (68°C) ┌─────────────┐ ← Upper sensor (HOT) + │ Hot zone │ + │ (stratif) │ Recently heated water + 120°F (49°C) ├─────────────┤ ← Dividing line (thermocline) + │ Warm zone │ Transitional temperature + │ │ + 95°F (35°C) ├─────────────┤ ← Lower sensor (COOL) + │ Cool zone │ Being heated by compressor + └─────────────┘ + +**Why Stratification Matters**: + +1. **Efficiency Benefit**: Thermostat setpoints work on upper sensor only until recovery needed +2. **Recovery Speed**: Lower element heating doesn't start until really needed (stratification maintained) +3. **Cost Savings**: Avoids unnecessary full-tank heating; only heats lower section when depleted +4. **User Comfort**: Upper zone always available at target temperature for draw + +Practical Stratification Scenarios +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Scenario 1: Excellent Stratification (Efficient)** + +.. code-block:: text + + Time Upper Temp Lower Temp Differential Status + ---- ---------- ---------- ----------- -------- + 9:00 AM 140°F 110°F 30°F Good stratification + 9:15 AM 138°F 110°F 28°F Still good (light draw) + 10:00 AM 140°F 112°F 28°F Heat pump maintains lower + + → Device operates efficiently: upper element/HP just maintains top, lower recovers slowly + → User gets hot water from top layer without full-tank heating + +**Scenario 2: Poor Stratification (Inefficient)** + +.. code-block:: text + + Time Upper Temp Lower Temp Differential Status + ---- ---------- ---------- ----------- -------- + 3:00 PM 100°F 98°F 2°F Bad stratification + 3:30 PM 102°F 100°F 2°F Tank too uniform + 4:00 PM 95°F 94°F 1°F Almost no difference + + → Device detects poor stratification + → Triggers full tank heating (both elements active) + → Inefficient: heats entire volume instead of targeted zones + → Recovery slower due to element capacity + +**Scenario 3: Failed Sensor or Mixing Issue** + +.. code-block:: text + + Time Upper Temp Lower Temp Differential Status + ---- ---------- ---------- ----------- -------- + 10:00 AM 155°F 160°F -5°F ERROR: Lower hotter than upper! + + → Impossible condition: lower can't be hotter than upper + → Indicates failed sensor or severe mixing/circulation issue + → Device may alert or switch to safety mode + +Device Control Strategy Based on Stratification +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Two-Stage Heating with Stratification**: + +.. list-table:: + :header-rows: 1 + :widths: 15 20 20 45 + + * - Condition + - Upper Element + - Lower Element + - Device Action + * - Upper <110°F, Lower <90°F + - OFF + - ON (primary) + - Heat entire tank from bottom; creates stratification + * - Upper 110-130°F, Lower <90°F + - OFF + - ON + - Maintain stratification: let upper stay ready, heat lower + * - Upper >130°F, Lower >120°F + - OFF + - OFF + - Both satisfied, coast on heat retention + * - Upper <100°F, Lower >120°F + - ON (priority) + - OFF + - Restore top zone quickly (likely hot water draw) + * - Upper ~Upper set, Lower <100°F + - ON + - ON + - Full recovery needed; both elements heating + +**Stratification Efficiency Gains**: + +- **Upper heating only**: 15-25% less energy vs. full tank heating +- **Lower heating only**: 20-30% longer recovery time but 40-60% lower cost per cycle +- **Optimal**: ~25-30°F differential maximizes recovery time vs. efficiency tradeoff + +Heat Pump Integration with Stratification +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The two-stage control extends to heat pump operation: + +- **Upper Heat Pump**: Activates when upper sensor drops below setpoint (quick, efficient recovery) +- **Lower Heat Pump**: Activates when lower sensor needs charging (low COP but maintains heating) + +Modern control systems may use "superheat modulation" where: + +- Heat pump adjusts compressor speed based on stratification degree +- Tighter superheat (more efficient) when stratification good +- Looser superheat (safer operation) when stratification poor + +Monitoring Stratification from Python +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: python + + from nwp500.mqtt_client import NavienMQTTClient + from nwp500.models import DeviceStatus + + async def monitor_stratification(mqtt_client: NavienMQTTClient, device_id: str): + """Monitor tank stratification quality""" + + status = await mqtt_client.get_status(device_id) + + upper_temp = status.tank_upper_temperature # float in °F + lower_temp = status.tank_lower_temperature # float in °F + + stratification_delta = upper_temp - lower_temp + + if stratification_delta < 5: + print(f"WARNING: Poor stratification (Δ={stratification_delta}°F)") + print(" → Full tank heating required") + print(" → Efficiency reduced, recovery slower") + elif stratification_delta > 25: + print(f"GOOD: Excellent stratification (Δ={stratification_delta}°F)") + print(" → Efficient targeted heating") + print(" → Quick hot water availability") + else: + print(f"INFO: Normal stratification (Δ={stratification_delta}°F)") + print(" → Balanced efficiency and recovery") + + return { + 'upper_temp': upper_temp, + 'lower_temp': lower_temp, + 'stratification_delta': stratification_delta, + 'quality': 'excellent' if stratification_delta > 25 else 'poor' if stratification_delta < 5 else 'normal' + } + +Factors Affecting Stratification +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Positive Factors** (Preserve Stratification): + +1. **Tank Insulation Quality**: Well-insulated tanks maintain temperature differences longer +2. **Slow Heating**: Gentle heating from bottom maintains distinct layers +3. **Low Draw Velocity**: Slow water draws don't turbulently mix layers +4. **Minimal Circulation**: Recirculation pumps can destroy stratification if running +5. **Vertical Tank Orientation**: Tall narrow tanks maintain stratification better than squat tanks + +**Negative Factors** (Degrade Stratification): + - Morning peak hour starting (6-7 AM) + - Reservation calls for 140°F + + Device Decision: + 1. Weather-responsive: 25°F ambient → COP low, expect user needs + 2. Tank stratification: Delta only 5°F → full-tank heating needed + 3. Demand response: Reduce signal → lower compressor priority + + Action Taken: + - Electric lower element activated (ignores DR, local override) + - Heat pump compressor disabled (responds to DR reduce signal) + - Target: Warm tank from bottom, allow sufficient top recovery + - Result: 140°F achieved in 45 min (slower due to DR, but cold ambient expected) + +Formula Confirmation +==================== + +**Formula**: + +The temperature conversion formula is: + +.. code-block:: text + + Formula: displayed_value = raw_value + 20 + + Application Evidence: + - Application handled by NaviLink + - Implementation in device status messages + - Fields: dhwTemperature, tankUpperTemperature, tankLowerTemperature, etc. + - Conversion: Applied uniformly to all add_20 type fields in device status + - Raw value range: 0-130 (representing -4°F to 150°F) + - Display range: 20-150°F + +**Related Documentation**: + +See :doc:`../protocol/data_conversions` for complete field conversion reference and formula applications. + +Summary and Recommendations +============================ + +**Weather-Responsive Heating**: +- Automatically adapts heat pump efficiency based on ambient conditions +- Enables pre-charging for predictable demand peaks +- Monitors ambient via ``outsideTemperature`` field in device status +- Integrate ambient data into recovery time predictions + +**Demand Response Integration**: +- Enables grid-aware operation and potential utility incentive payments +- Monitor ``drEventStatus`` and ``drOverrideStatus`` fields +- User can override DR events temporarily (up to 72 hours typical) +- Integrate DR status into user notifications and UI displays + +**Tank Stratification Optimization**: +- Dual sensors enable smart two-stage heating +- Monitor stratification delta (upper - lower) for efficiency insights +- Target 20-30°F delta for optimal efficiency +- Alert users when stratification poor (indicates maintenance need) +- Use stratification data for predictive recovery time estimation + +See Also +-------- + +* :doc:`../protocol/data_conversions` - Temperature field conversions (add_20, decicelsius_to_f) +* :doc:`../protocol/device_status` - Complete device status field reference +* :doc:`scheduling_features` - Reservation and TOU integration points +* :doc:`../python_api/models` - DeviceStatus model field definitions diff --git a/docs/guides/auto_recovery.rst b/docs/guides/auto_recovery.rst index b560b61..71872f5 100644 --- a/docs/guides/auto_recovery.rst +++ b/docs/guides/auto_recovery.rst @@ -131,8 +131,8 @@ Refresh authentication tokens before retrying (handles token expiry). **Cons:** More complex, need to manage client lifecycle -Strategy 4: Exponential Backoff (Production-Ready) ⭐ RECOMMENDED -================================================================= +Strategy 4: Exponential Backoff ⭐ RECOMMENDED +================================================ Use exponential backoff between recovery attempts with token refresh. @@ -216,7 +216,7 @@ Use exponential backoff between recovery attempts with token refresh. # Reset on success self.recovery_attempt = 0 - print("✅ Recovery successful!") + print("Recovery successful!") except Exception as e: print(f"Recovery failed: {e}") @@ -332,7 +332,7 @@ See ``examples/simple_auto_recovery.py`` for a complete implementation. logger.info(f"Recovery attempt {recovery_attempt}/{max_recovery_attempts}") logger.info(f"Waiting {delay:.0f} seconds before retry") - logger.info("✅ Recovery successful!") + logger.info("Recovery successful!") Examples ======== @@ -385,9 +385,9 @@ The logs will show: INFO: Waiting 60 seconds before recovery... INFO: Refreshing authentication tokens... INFO: Recreating MQTT client... - INFO: ✅ Connected: navien-client-abc123 + INFO: Connected: navien-client-abc123 INFO: Subscriptions restored - INFO: ✅ Recovery successful! + INFO: Recovery successful! When to Use Each Strategy ========================== @@ -446,11 +446,11 @@ Conclusion For production use, **use Strategy 4 (Exponential Backoff)** via the ``ResilientMqttClient`` wrapper provided in ``examples/simple_auto_recovery.py``. It handles: -* ✅ Automatic recovery from permanent failures -* ✅ Exponential backoff to prevent server overload -* ✅ Token refresh for long-running connections -* ✅ Clean client recreation -* ✅ Subscription restoration -* ✅ Configurable limits and delays +* Automatic recovery from permanent failures +* Exponential backoff to prevent server overload +* Token refresh for long-running connections +* Clean client recreation +* Subscription restoration +* Configurable limits and delays This ensures your application stays connected even during extended network outages. diff --git a/docs/guides/event_system.rst b/docs/guides/event_system.rst index 7bf3df4..6ee4e05 100644 --- a/docs/guides/event_system.rst +++ b/docs/guides/event_system.rst @@ -445,11 +445,11 @@ Best Practices .. code-block:: python - # ✓ Fast handler + # GOOD: Fast handler def on_status(status): asyncio.create_task(process_status(status)) - # ✗ Slow handler (blocks event loop) + # BAD: Slow handler (blocks event loop) def on_status(status): time.sleep(5) # BAD process_status(status) diff --git a/docs/guides/scheduling_features.rst b/docs/guides/scheduling_features.rst new file mode 100644 index 0000000..9ac7000 --- /dev/null +++ b/docs/guides/scheduling_features.rst @@ -0,0 +1,401 @@ +Advanced Scheduling Guide +========================= + +This guide documents advanced scheduling capabilities of the NWP500 and clarifies the interaction between different scheduling systems. + +Current Scheduling Systems +-------------------------- + +The NWP500 supports four independent scheduling systems that work together: + +1. **Reservations** (Scheduled Programs) +2. **Time of Use (TOU)** (Price-Based Scheduling) +3. **Vacation Mode** (Automatic Suspension) +4. **Anti-Legionella** (Periodic Maintenance) + +Understanding How They Interact +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +These systems operate with different priorities and interaction rules: + +.. list-table:: + :header-rows: 1 + :widths: 20 20 20 20 20 + + * - System + - Trigger Type + - Scope + - Priority + - Override Behavior + * - Reservations + - Time-based (daily/weekly) + - Mode/Temperature changes + - Medium + - TOU and Vacation suspend reservations + * - TOU + - Time + Price periods + - Heating behavior optimization + - Low-Medium + - Vacation suspends TOU; Reservations override + * - Vacation + - Duration-based + - Complete suspension with maintenance ops + - Highest (blocks heating) + - Overrides all; only anti-legionella and freeze protection run + * - Anti-Legionella + - Periodic cycle + - Temperature boost + - Highest (mandatory maintenance) + - Runs even during vacation; interrupts other modes + +Reservations (Scheduled Programs) - Detailed Reference +------------------------------------------------------ + +Reservations allow you to change the device's operating mode and temperature at specific times of day. + +Capabilities and Limitations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Supported**: +- Weekly patterns (Monday-Sunday, any combination) +- Multiple entries (up to ~16 entries, device-dependent) +- Two-second time precision +- Mode changes (Heat Pump, Electric, Energy Saver, High Demand) +- Temperature setpoint changes (95-150°F) +- Per-entry enable/disable + +**Not Supported (Currently)**: +- Monthly patterns (e.g., "first Tuesday of month") +- Holiday calendars +- Relative times (e.g., "2 hours before sunset") +- Weather-based triggers +- Usage-based thresholds + +Reservation Entry Structure +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Each reservation entry controls one scheduled action: + +.. code-block:: python + + { + "enable": 1, # 1=enabled, 2=disabled + "week": 62, # Bitfield: bit 0=Sunday, bit 1=Monday, etc. + # 62 = 0b111110 = Monday-Friday + "hour": 6, # 0-23 (24-hour format) + "min": 30, # 0-59 + "mode": 3, # 1=Heat Pump, 2=Electric, 3=Energy Saver, 4=High Demand + "param": 120 # Temperature offset (raw value, add 20 to display) + # 120 raw = 140°F display + } + +**Week Bitfield Encoding**: + +The ``week`` field uses 7 bits for days of week: + +.. code-block:: text + + Bit Position: 0 1 2 3 4 5 6 + Day: Sun Mon Tue Wed Thu Fri Sat + Bit Value: 1 2 4 8 16 32 64 + + Examples: + - Monday-Friday (work week): 2+4+8+16+32 = 62 (0b111110) + - Weekends only: 1+64 = 65 (0b1000001) + - Every day: 127 (0b1111111) + - Mon/Wed/Fri only: 2+8+32 = 42 (0b101010) + +**Temperature Parameter Encoding**: + +The ``param`` field stores temperature with an offset of 20°F: + +.. code-block:: text + + Display Temperature → Raw Parameter Value + 95°F → 75 (95-20) + 120°F → 100 (120-20) + 140°F → 120 (140-20) + 150°F → 130 (150-20) + +**Mode Selection Strategy**: + +- **Heat Pump (1)**: Lowest cost, slowest recovery, best for off-peak periods or overnight +- **Energy Saver (3)**: Default hybrid mode, balanced efficiency/recovery, recommended for all-day use +- **High Demand (4)**: Faster recovery, higher cost, useful for scheduled peak demand times (e.g., morning showers) +- **Electric (2)**: Emergency only, very high cost, fastest recovery, maximum 72-hour operation limit + +Example Use Cases +^^^^^^^^^^^^^^^^^ + +**Scenario 1: Morning Peak Demand** + +Heat water to high temperature before morning showers: + +.. code-block:: python + + # 6:30 AM weekdays: switch to High Demand mode at 140°F + morning_peak = { + "enable": 1, + "week": 62, # Monday-Friday + "hour": 6, + "min": 30, + "mode": 4, # High Demand + "param": 120 # 140°F + } + +**Scenario 2: Work Hours Energy Saving** + +During work hours (when nobody home), reduce heating: + +.. code-block:: python + + # 9:00 AM weekdays: switch to Heat Pump only + work_hours_eco = { + "enable": 1, + "week": 62, # Monday-Friday + "hour": 9, + "min": 0, + "mode": 1, # Heat Pump (most efficient) + "param": 100 # 120°F (lower setpoint) + } + +**Scenario 3: Evening Preparation** + +Restore comfort before evening return: + +.. code-block:: python + + # 5:00 PM weekdays: switch back to Energy Saver at 140°F + evening_prep = { + "enable": 1, + "week": 62, # Monday-Friday + "hour": 17, + "min": 0, + "mode": 3, # Energy Saver (balanced) + "param": 120 # 140°F + } + +**Scenario 4: Weekend Comfort** + +Maintain comfort throughout weekend: + +.. code-block:: python + + # 8:00 AM weekends: switch to High Demand at 150°F + weekend_morning = { + "enable": 1, + "week": 65, # Saturday + Sunday + "hour": 8, + "min": 0, + "mode": 4, # High Demand + "param": 130 # 150°F (maximum) + } + +Time of Use (TOU) Scheduling - Advanced Details +----------------------------------------------- + +TOU scheduling is more complex than reservations, allowing price-aware heating optimization. + +How TOU Works +^^^^^^^^^^^^^ + +1. Device receives multiple time periods, each with a price range (min/max) +2. During low-price periods: Device uses heat pump only (or less aggressive heating) +3. During high-price periods: Device reduces heating or switches to lower efficiency to save electricity +4. During peak periods: Device may pre-charge tank before peak to minimize peak-time heating + +TOU Period Structure +^^^^^^^^^^^^^^^^^^^^ + +Each TOU period defines a time window with price information: + +.. code-block:: python + + { + "season": 448, # Bitfield for months (bit 0=Jan, ..., bit 11=Dec) + # 448 = 0b111000000 = June, July, August (summer) + "week": 62, # Bitfield for weekdays (same as reservations) + # 62 = Monday-Friday + "startHour": 9, # 0-23 + "startMinute": 0, # 0-59 + "endHour": 17, # 0-23 + "endMinute": 0, # 0-59 + "priceMin": 10, # Minimum price (encoded, typically cents) + "priceMax": 25, # Maximum price (encoded, typically cents) + "decimalPoint": 2 # Price decimal places (2 = price is priceMin/100) + } + +**Season Bitfield Encoding**: + +Months are encoded as bits (similar to days): + +.. code-block:: text + + Bit Position: 0 1 2 3 4 5 6 7 8 9 10 11 + Month: Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec + Bit Value: 1 2 4 8 16 32 64 128 256 512 1024 2048 + + Examples: + - Summer (Jun-Aug): 64+128+256 = 448 (0b111000000) + - Winter (Dec-Feb): 1+2+2048 = 2051 (0b100000000011) + - Year-round: 4095 (0b111111111111) + +**Price Encoding**: + +Prices are encoded as integers with separate decimal point indicator: + +.. code-block:: python + + # Example: Encode $0.12/kWh with decimal_point=2 + priceMin = 12 # Represents $0.12 when decimal_point=2 + + # Example: Encode $0.125/kWh with decimal_point=3 + priceMin = 125 # Represents $0.125 when decimal_point=3 + +Maximum 16 TOU Periods +^^^^^^^^^^^^^^^^^^^^^^ + +The device supports up to 16 different price periods. Design your schedule to fit: + +- **Simple**: 3-4 periods (off-peak, shoulder, on-peak) +- **Moderate**: 6-8 periods (split by season and weekday/weekend) +- **Complex**: 12-16 periods (full tariff with seasonal and weekday variations) + +Example: 3-Period Summer Schedule +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: python + + # Summer (Jun-Jul-Aug), 3-period schedule + + # Off-peak: 9 PM - 9 AM weekdays + off_peak_summer = { + "season": 448, # Jun, Jul, Aug + "week": 62, # Mon-Fri + "startHour": 21, # 9 PM + "startMinute": 0, + "endHour": 9, # 9 AM next day (wraps) + "endMinute": 0, + "priceMin": 8, # $0.08/kWh + "priceMax": 10, # $0.10/kWh + "decimalPoint": 2 + } + + # Shoulder: 9 AM - 2 PM weekdays + shoulder_summer = { + "season": 448, + "week": 62, + "startHour": 9, + "startMinute": 0, + "endHour": 14, # 2 PM + "endMinute": 0, + "priceMin": 12, # $0.12/kWh + "priceMax": 18, # $0.18/kWh + "decimalPoint": 2 + } + + # Peak: 2 PM - 9 PM weekdays + peak_summer = { + "season": 448, + "week": 62, + "startHour": 14, # 2 PM + "startMinute": 0, + "endHour": 21, # 9 PM + "endMinute": 0, + "priceMin": 20, # $0.20/kWh + "priceMax": 35, # $0.35/kWh + "decimalPoint": 2 + } + +Vacation Mode - Extended Use Details +------------------------------------ + +Vacation mode suspends heating for up to 99 days while maintaining critical functions. + +Vacation Behavior +^^^^^^^^^^^^^^^^^ + +When vacation mode is active: + +1. **Heating SUSPENDED**: No heat pump or electric heating cycles +2. **Freeze Protection**: Still active - if temperature drops below 43°F, electric heating activates briefly +3. **Anti-Legionella**: Still runs on schedule - disinfection cycles continue +4. **Automatic Resumption**: Heating automatically resumes 9 hours before vacation end date +5. **All Other Schedules**: Reservations and TOU are suspended during vacation + +Vacation Duration Calculation +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: text + + Duration: 0-99 days + - 0 days = Vacation mode disabled (resume heating immediately) + - 1 day = Heat resumes ~24 hours from now + - 7 days = Vacation until next week, resume ~7 days from now + - 14 days = Two-week vacation + - 99 days = Approximately 3 months (maximum) + +When to Use Vacation Mode +^^^^^^^^^^^^^^^^^^^^^^^^^ + +- Extended absences (weeklong trips or longer) +- Seasonal properties (winterized/unopened for season) +- Emergency situations requiring complete shutdown +- Energy conservation for long maintenance periods + +**NOT Recommended For**: +- Weekend trips (use reservations instead) +- Work-day absences (use Energy Saver + TOU instead) +- Daily night-time suspension (use reservations with Heat Pump mode) + +Anti-Legionella Cycles - Maintenance Details +-------------------------------------------- + +Anti-legionella feature periodically heats water to 158°F (70°C) for disinfection. + +Mandatory Operation +^^^^^^^^^^^^^^^^^^^ + +Anti-legionella cycles run even when: +- Vacation mode is active +- Device is in standby +- User has requested low-power operation + +Period Configuration +^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 25 75 + + * - Period (days) + - Purpose + * - 1-3 (not typical) + - Rare: high-contamination risk environments + * - 7 + - Standard: high-risk installations or hardwater areas + * - 14 + - Common: residential with typical water quality + * - 30 + - Relaxed: commercial buildings with annual testing + * - 90 + - Minimal: well-maintained commercial systems with water treatment + +Default: 14 days + +Legionella Risk Factors +^^^^^^^^^^^^^^^^^^^^^^^ + +Anti-legionella becomes more critical in: +- Hard water areas (mineral deposits harbor bacteria) +- Systems left unused for days (stagnant water) +- Warm climates (25-45°C ideal for legionella growth) +- Recirculation systems (warm water in pipes) + +See Also +-------- + +* :doc:`reservations` - Quick start for reservation setup +* :doc:`time_of_use` - TOU pricing details and OpenEI integration +* :doc:`../protocol/data_conversions` - Understanding temperature and power fields +* :doc:`auto_recovery` - Handling temporary connectivity issues diff --git a/docs/index.rst b/docs/index.rst index d925185..d2a701e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -119,6 +119,7 @@ Documentation Index protocol/rest_api protocol/mqtt_protocol protocol/device_status + protocol/data_conversions protocol/device_features protocol/error_codes protocol/firmware_tracking @@ -128,6 +129,7 @@ Documentation Index :caption: User Guides guides/reservations + guides/scheduling_features guides/energy_monitoring guides/time_of_use guides/event_system diff --git a/docs/protocol/data_conversions.rst b/docs/protocol/data_conversions.rst new file mode 100644 index 0000000..9888942 --- /dev/null +++ b/docs/protocol/data_conversions.rst @@ -0,0 +1,636 @@ +Data Conversions and Units Reference +==================================== + +This document provides comprehensive details on all data conversions applied to device status messages, field units, and the meaning of various data structures. + +Overview of Conversion Types +---------------------------- + +The NWP500 device encodes data in a compact binary format. The Python client automatically converts these raw values to user-friendly representations using the following conversion strategies: + +Raw Encoding Strategies +^^^^^^^^^^^^^^^^^^^^^^^ + +The device uses several encoding schemes to minimize transmission overhead: + +1. **Offset Encoding** (add_20) + - Applied to most temperature fields + - Formula: ``displayed_value = raw_value + 20`` + - Purpose: Negative temperatures stored as positive integers + - Range: Typically -4°F (-20°C) to 149°F (65°C) + - Example: Raw 100 → 120°F display value + +2. **Tenths Encoding** (div_10) + - Applied to decimal precision values + - Formula: ``displayed_value = raw_value / 10.0`` + - Purpose: Preserve decimal precision in integer storage + - Common for flow rates and differential temperatures + - Example: Raw 125 → 12.5 GPM + +3. **Decicelsius to Fahrenheit** (decicelsius_to_f) + - Applied to refrigerant and evaporator temperatures + - Formula: ``displayed_value = (raw_value / 10) * 9/5 + 32`` + - Purpose: Convert Celsius tenths to Fahrenheit + - Example: Raw 250 (25°C) → 77°F + +4. **Boolean Encoding** (device_bool) + - Applied to all status flags + - Formula: ``displayed_value = (raw_value == 2)`` + - Encoding: 1 or 0 = False, 2 = True + - Purpose: Compact boolean representation + +5. **Enumeration Encoding** (enum) + - Applied to mode and state fields + - Direct value-to-enum mapping + - Example: 0=Standby, 32=Heat Pump, 64=Hybrid Efficiency, 96=Hybrid Boost + +Temperature Fields Reference +---------------------------- + +All temperature fields in this section are shown with their applied conversions. Stored values are in °F unless otherwise specified. + +DHW (Domestic Hot Water) Temperatures +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 25 10 15 50 + + * - Field + - Conversion + - Display Unit + - Description + * - ``dhwTemperature`` + - add_20 + - °F + - **Current outlet temperature** of hot water being delivered to fixtures. Real-time measurement. Typically 90-150°F. + * - ``dhwTemperature2`` + - add_20 + - °F + - **Secondary DHW temperature sensor** reading (redundancy/averaging). May differ slightly from primary sensor during temperature transitions. + * - ``dhwTemperatureSetting`` + - add_20 + - °F + - **User-configured target temperature** for DHW delivery. Adjustable range: 95-150°F. Default: 120°F. This is the setpoint users configure in the app. + * - ``currentInletTemperature`` + - div_10 + - °F + - **Cold water inlet temperature** to the water heater. Affects heating performance and recovery time. Typically 40-80°F depending on season and location. + * - ``dhwTargetTemperatureSetting`` + - add_20 + - °F + - **Duplicate of dhwTemperatureSetting** for legacy API compatibility. + +Tank Temperature Sensors +^^^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 25 10 15 50 + + * - Field + - Conversion + - Display Unit + - Description + * - ``tankUpperTemperature`` + - decicelsius_to_f + - °F + - **Upper tank sensor temperature**. Indicates stratification - hot water at top for quick delivery. Typically hottest point in tank. + * - ``tankLowerTemperature`` + - decicelsius_to_f + - °F + - **Lower tank sensor temperature**. Indicates bulk tank temperature and heating progress. Typically cooler than upper sensor. + +**Tank Temperature Stratification**: Well-insulated tanks maintain significant temperature differences between upper (hot, recently drawn from) and lower (cooler, being heated) regions. The device uses this stratification to optimize heating efficiency. + +Refrigerant Circuit Temperatures +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +These temperatures monitor the heat pump refrigerant circuit health and performance. Understanding these helps diagnose efficiency issues: + +.. list-table:: + :header-rows: 1 + :widths: 25 10 15 50 + + * - Field + - Conversion + - Display Unit + - Description + * - ``dischargeTemperature`` + - decicelsius_to_f + - °F + - **Compressor discharge temperature**. Temperature of refrigerant exiting the compressor. Typically 120-180°F. High values indicate high system pressure; low values indicate efficiency issues. + * - ``suctionTemperature`` + - decicelsius_to_f + - °F + - **Compressor suction temperature**. Temperature of refrigerant entering the compressor. Typically 40-60°F. Affects superheat calculation. + * - ``evaporatorTemperature`` + - decicelsius_to_f + - °F + - **Evaporator coil temperature**. Where heat is extracted from ambient air. Typically 20-50°F. Lower outdoor air temperature reduces evaporator efficiency. + * - ``ambientTemperature`` + - decicelsius_to_f + - °F + - **Ambient air temperature** measured at heat pump inlet. Directly affects system performance. At freezing (32°F), heat pump efficiency drops significantly. + * - ``targetSuperHeat`` + - decicelsius_to_f + - °F + - **Target superheat setpoint**. Desired temperature difference between suction and evaporator ensuring complete refrigerant vaporization. Typically 10-20°F. + * - ``currentSuperHeat`` + - decicelsius_to_f + - °F + - **Measured superheat value**. Actual temperature difference. Deviation from target indicates EEV (Electronic Expansion Valve) control issues. + +**Refrigerant Circuit Diagnostics**: +- If ``currentSuperHeat >> targetSuperHeat``: EEV may be stuck open (undercharge symptoms) +- If ``currentSuperHeat << targetSuperHeat``: EEV may be stuck closed (overcharge symptoms) +- If ``dischargeTemperature`` extremely high (>200°F): System may be in bypass protection +- If ``ambientTemperature`` below 32°F: Heat pump COP (efficiency) significantly reduced + +Heating Element Control Temperatures +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Electric heating elements are controlled via thermostat ranges. Two sensors (upper and lower tank) allow two-stage heating: + +.. list-table:: + :header-rows: 1 + :widths: 30 10 15 45 + + * - Field + - Conversion + - Display Unit + - Description + * - ``heUpperOnTempSetting`` + - add_20 + - °F + - **Upper element ON threshold**. Upper tank temp must fall below this to activate upper heating element. + * - ``heUpperOffTempSetting`` + - add_20 + - °F + - **Upper element OFF threshold**. Upper tank temp rises above this to deactivate upper element (hysteresis). + * - ``heLowerOnTempSetting`` + - add_20 + - °F + - **Lower element ON threshold**. Lower tank temp must fall below this to activate lower element. + * - ``heLowerOffTempSetting`` + - add_20 + - °F + - **Lower element OFF threshold**. Lower tank temp rises above this to deactivate lower element. + * - ``heUpperOnDiffTempSetting`` + - div_10 + - °F + - **Upper element differential** (ON-OFF difference). Hysteresis width to prevent rapid cycling. Typically 2-5°F. + * - ``heUpperOffDiffTempSetting`` + - div_10 + - °F + - **Upper element differential** variation (advanced tuning). May vary based on mode. + * - ``heLowerOnDiffTempSetting`` + - div_10 + - °F + - **Lower element differential** (ON-OFF difference). + * - ``heLowerOffDiffTempSetting`` + - div_10 + - °F + - **Lower element differential** variation. + * - ``heatMinOpTemperature`` + - add_20 + - °F + - **Minimum operating temperature** for heating elements. Safety threshold - elements won't activate if inlet water is below this (prevents pump cavitation). + +Heat Pump Control Temperatures +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Heat pump stages are similarly controlled via thermostat ranges: + +.. list-table:: + :header-rows: 1 + :widths: 30 10 15 45 + + * - Field + - Conversion + - Display Unit + - Description + * - ``hpUpperOnTempSetting`` + - add_20 + - °F + - **Upper heat pump ON**. Upper tank falls below this to activate heat pump for upper tank heating. + * - ``hpUpperOffTempSetting`` + - add_20 + - °F + - **Upper heat pump OFF**. Upper tank rises above this to stop upper tank heat pump operation. + * - ``hpLowerOnTempSetting`` + - add_20 + - °F + - **Lower heat pump ON**. Lower tank falls below this to activate heat pump for lower tank heating. + * - ``hpLowerOffTempSetting`` + - add_20 + - °F + - **Lower heat pump OFF**. Lower tank rises above this to stop lower tank heat pump operation. + * - ``hpUpperOnDiffTempSetting`` + - div_10 + - °F + - **Heat pump upper differential** (ON-OFF hysteresis). Prevents rapid cycling. + * - ``hpUpperOffDiffTempSetting`` + - div_10 + - °F + - **Heat pump upper differential** variation. + * - ``hpLowerOnDiffTempSetting`` + - div_10 + - °F + - **Heat pump lower differential**. + * - ``hpLowerOffDiffTempSetting`` + - div_10 + - °F + - **Heat pump lower differential** variation. + +Freeze Protection Temperatures +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 30 10 15 45 + + * - Field + - Conversion + - Display Unit + - Description + * - ``freezeProtectionUse`` + - device_bool + - Boolean + - **Freeze protection enabled flag**. When True, triggers anti-freeze operation below threshold. + * - ``freezeProtectionTemperature`` + - add_20 + - °F + - **Freeze protection temperature setpoint**. Default 43°F (6°C). If any tank sensor drops below this, electric heating activates. + * - ``freezeProtectionTempMin`` + - add_20 + - °F + - **Minimum freeze protection temperature limit**. + * - ``freezeProtectionTempMax`` + - add_20 + - °F + - **Maximum freeze protection temperature limit**. + +Recirculation System Temperatures +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +For systems with recirculation pumps (optional feature): + +.. list-table:: + :header-rows: 1 + :widths: 25 10 15 50 + + * - Field + - Conversion + - Display Unit + - Description + * - ``recircTemperature`` + - add_20 + - °F + - **Recirculation loop current temperature**. Temperature of water being circulated back to tank. + * - ``recircFaucetTemperature`` + - add_20 + - °F + - **Recirculation faucet outlet temperature**. How hot water is at the furthest fixture during recirculation. + * - ``recircTempSetting`` + - add_20 + - °F + - **Recirculation target temperature**. What temperature to maintain in the recirculation line. + +Flow Rate Fields +---------------- + +.. list-table:: + :header-rows: 1 + :widths: 25 10 15 50 + + * - Field + - Conversion + - Display Unit + - Description + * - ``currentDhwFlowRate`` + - div_10 + - GPM + - **Current hot water flow rate** at outlet. Measured in real-time. Typical range: 0-5 GPM for fixtures. 0 when no water being drawn. + * - ``recircDhwFlowRate`` + - div_10 + - GPM + - **Recirculation loop flow rate**. Circulation pump speed. Typical range: 1-3 GPM to maintain temperature without excessive energy use. + * - ``cumulatedDhwFlowRate`` + - None (direct value) + - gallons + - **Total hot water delivered since installation**. Cumulative counter - never decreases. Useful for usage tracking and diagnostics. + +Power and Energy Fields +----------------------- + +.. list-table:: + :header-rows: 1 + :widths: 25 10 15 50 + + * - Field + - Conversion + - Display Unit + - Description + * - ``currentInstPower`` + - None (direct value) + - W + - **Instantaneous power consumption**. Real-time measurement. Does **NOT** include electric heating element power draw. Heat pump only. + * - ``totalEnergyCapacity`` + - None (direct value) + - Wh + - **Tank energy capacity** at full charge. Theoretical maximum heat content. Useful for recovery time estimation. + * - ``availableEnergyCapacity`` + - None (direct value) + - Wh + - **Available energy in tank right now**. Indicates how much hot water capacity remains before next heating cycle. Lower value = lower DHW charge percentage. + +.. note:: + ``currentInstPower`` excludes electric heating element power. If the heater is actively heating with electric elements, the actual power draw will be higher (typically +3755W @ 208V or +5000W @ 240V). + +System Status and Performance Fields +------------------------------------ + +.. list-table:: + :header-rows: 1 + :widths: 25 10 15 50 + + * - Field + - Conversion + - Display Unit + - Description + * - ``dhwChargePer`` + - None (direct value) + - % + - **DHW tank charge percentage** (0-100%). Indicates usable hot water availability. Decreases as hot water is drawn; increases during heating cycles. + * - ``currentHeatUse`` + - device_bool + - Boolean + - **Currently heating flag**. True when any heat source (heat pump or element) is active. + * - ``heatUpperUse`` + - device_bool + - Boolean + - **Upper electric element active flag**. True when upper heating element currently drawing power. + * - ``heatLowerUse`` + - device_bool + - Boolean + - **Lower electric element active flag**. True when lower heating element currently drawing power. + * - ``compUse`` + - device_bool + - Boolean + - **Heat pump compressor running flag**. True when heat pump is actively compressing refrigerant. + * - ``eevUse`` + - device_bool + - Boolean + - **Electronic Expansion Valve (EEV) active flag**. True when EEV is modulating refrigerant flow. Usually correlates with ``compUse``. + * - ``evaFanUse`` + - device_bool + - Boolean + - **Evaporator fan running flag**. True when fan drawing ambient air through evaporator coil. + +Safety and Diagnostic Fields +---------------------------- + +.. list-table:: + :header-rows: 1 + :widths: 25 10 15 50 + + * - Field + - Conversion + - Display Unit + - Description + * - ``ecoUse`` + - device_bool + - Boolean + - **ECO (Energy Cut Off) flag**. True when high-temperature safety limit triggered. Automatically resets when tank cools below limit. + * - ``scaldUse`` + - device_bool + - Boolean + - **Scald protection warning flag**. True when water temperature reaches potentially hazardous levels (typically >130°F). Advisory only. + * - ``freezeProtectionUse`` + - device_bool + - Boolean + - **Freeze protection active flag**. True when freeze protection heating cycle is running. + * - ``airFilterAlarmUse`` + - device_bool + - Boolean + - **Air filter maintenance reminder enabled flag**. True when air filter alarm feature is active. + * - ``airFilterAlarmPeriod`` + - None (direct value) + - hours + - **Air filter service interval**. Default 1000 hours. Maintenance reminder triggers after this interval. + * - ``airFilterAlarmElapsed`` + - None (direct value) + - hours + - **Hours since last air filter service reset**. Resets to 0 when maintenance is performed. + * - ``cumulatedOpTimeEvaFan`` + - None (direct value) + - hours + - **Total evaporator fan runtime since installation**. Diagnostic indicator of system usage and fan wear. + * - ``wtrOvrSensorUse`` + - device_bool + - Boolean + - **Water overflow/leak sensor active flag**. True when leak detected in condensate pan or water connections. Triggers error E799. + * - ``conOvrSensorUse`` + - device_bool + - Boolean + - **Condensate overflow sensor active flag**. True when condensate pan overflow detected. + * - ``shutOffValveUse`` + - device_bool + - Boolean + - **Shut-off valve status flag**. True when valve is in normal operating position. + * - ``antiLegionellaUse`` + - device_bool + - Boolean + - **Anti-legionella function enabled flag**. True when feature is active. + * - ``antiLegionellaOperationBusy`` + - device_bool + - Boolean + - **Anti-legionella cycle in progress flag**. True during periodic high-temperature disinfection cycle. + * - ``antiLegionellaPeriod`` + - None (direct value) + - days + - **Anti-legionella execution interval**. Period between automatic disinfection cycles. + +Vacation and Scheduling Fields +------------------------------ + +.. list-table:: + :header-rows: 1 + :widths: 25 10 15 50 + + * - Field + - Conversion + - Display Unit + - Description + * - ``vacationDaySetting`` + - None (direct value) + - days + - **User-configured vacation duration** (0-99 days). When vacation mode activated, heating suspends for this period. Resumption scheduled 9 hours before end. + * - ``vacationDayElapsed`` + - None (direct value) + - days + - **Days elapsed in current vacation mode**. Increments daily from 0 to vacationDaySetting. Reaches max then heating resumes. + * - ``programReservationUse`` + - device_bool + - Boolean + - **Scheduled program (reservation) enabled flag**. True when any reservation schedule is active and affecting operation. + * - ``recircReservationUse`` + - device_bool + - Boolean + - **Recirculation schedule enabled flag**. True when recirculation pump has active schedule. + * - ``touStatus`` + - None (direct value) + - See values below + - **Time-of-Use (TOU) schedule status**. 0=inactive, 1=active. Controls heating based on electricity rate periods. + * - ``drEventStatus`` + - None (direct value) + - Bitfield + - **Demand Response event status** (CTA-2045). Each bit represents a DR signal. 0=no active events, non-zero=DR commands active. + * - ``drOverrideStatus`` + - None (direct value) + - See explanation + - **User override of Demand Response** (up to 72 hours). 0=no override. Non-zero value indicates override active. + * - ``touOverrideStatus`` + - None (direct value) + - See explanation + - **User temporary override of TOU schedule**. Similar to DR override - user can override schedule temporarily. + +Network and Diagnostic Fields +----------------------------- + +.. list-table:: + :header-rows: 1 + :widths: 25 10 15 50 + + * - Field + - Conversion + - Display Unit + - Description + * - ``wifiRssi`` + - None (direct value) + - dBm + - **WiFi signal strength** (Received Signal Strength Indicator). Typical range: -30 (excellent) to -90 (poor). Signal below -70 may cause reliability issues. + * - ``outsideTemperature`` + - None (direct value) + - °F + - **Outdoor ambient temperature** (from weather data, not device-measured). Used by device for algorithm optimization. May differ from device-measured ambient temperature. + * - ``errorCode`` + - None (direct value) + - Error code + - **Primary error code** if device fault detected. 0=no error. See ERROR_CODES.rst for complete reference. + * - ``subErrorCode`` + - None (direct value) + - Error code + - **Secondary error code** providing additional fault details. Paired with errorCode for diagnostics. + +Advanced Technical Fields +------------------------- + +.. list-table:: + :header-rows: 1 + :widths: 25 10 15 50 + + * - Field + - Conversion + - Display Unit + - Description + * - ``targetFanRpm`` + - None (direct value) + - RPM + - **Evaporator fan target speed**. Set by control algorithm based on heating demand. Typical range 0-3000 RPM. + * - ``currentFanRpm`` + - None (direct value) + - RPM + - **Actual measured evaporator fan speed**. May differ slightly from target due to motor inertia and load. + * - ``fanPwm`` + - None (direct value) + - % + - **Fan PWM (Pulse Width Modulation) duty cycle** (0-100). Direct control signal to fan motor. Higher = faster. + * - ``eevStep`` + - None (direct value) + - steps + - **EEV stepper motor position** (0-255 typical). Position within its range controls refrigerant flow. 0=wide open, 255=fully closed. + * - ``mixingRate`` + - None (direct value) + - % + - **Mixing valve position** (0-100%). For systems with mixing valves: controls proportion of tank water vs. inlet water for scald prevention. + * - ``currentStatenum`` + - None (direct value) + - State ID + - **Internal device state machine ID**. For diagnostics/advanced troubleshooting. Indicates which logic state device currently executing. + * - ``smartDiagnostic`` + - None (direct value) + - Diagnostic code + - **Smart diagnostic status** for system health monitoring. Non-zero value indicates diagnostic conditions detected. + * - ``faultStatus1`` / ``faultStatus2`` + - None (bitfield) + - Bitfield + - **Hardware fault status registers**. Each bit represents a specific hardware fault condition. See DEVICE_FEATURES.rst for bit definitions. + +Configuration Fields +-------------------- + +These fields reflect device settings (as opposed to real-time measurements): + +.. list-table:: + :header-rows: 1 + :widths: 25 10 15 50 + + * - Field + - Conversion + - Display Unit + - Description + * - ``temperatureType`` + - None (direct value) + - Enum + - **Temperature display unit setting** (1=Celsius, 2=Fahrenheit). Reflects user's app setting. + * - ``tempFormulaType`` + - None (direct value) + - Enum + - **Temperature conversion formula type**. Advanced: used for non-standard sensor calibrations. + * - ``errorBuzzerUse`` + - device_bool + - Boolean + - **Error buzzer enabled flag**. When True, device beeps on errors. + * - ``didReload`` + - device_bool + - Boolean + - **Recent reload/restart flag**. True indicates device recently rebooted (e.g., after power loss or update). + * - ``command`` + - None (direct value) + - Command ID + - **Last command that triggered this status**. For tracking which command most recently caused status update. + +Temperature Unit Notes +---------------------- + +* **Fahrenheit** conversions assume target display is °F as configured in the device +* **Celsius calculations** can be derived by reversing the conversions: + + - From ``add_20`` fields: ``celsius = (fahrenheit - 20) * 5/9`` + - From ``decicelsius_to_f`` fields: ``celsius = (fahrenheit - 32) * 5/9`` + - From ``div_10`` fields: ``celsius = value_celsius / 10.0`` + +* **Sensor Accuracy**: Typically ±2°F for tank sensors, ±3°F for refrigerant sensors +* **Conversion Rounding**: Python automatically handles floating-point precision; most displayed values are accurate to 0.1°F + +Practical Applications of Conversions +------------------------------------- + +Understanding these conversions helps with: + +1. **Energy Monitoring**: Combine ``totalEnergyCapacity``, ``availableEnergyCapacity``, and ``currentInstPower`` to estimate recovery times +2. **Efficiency Analysis**: Compare ``ambientTemperature`` against current COP (Coefficient of Performance) to verify expected efficiency +3. **Fault Diagnosis**: Monitor ``dischargeTemperature`` and ``currentSuperHeat`` for refrigerant circuit health +4. **Maintenance Scheduling**: Track ``airFilterAlarmElapsed`` and ``cumulatedOpTimeEvaFan`` for preventative maintenance +5. **User Experience**: Use ``dhwChargePer`` to show users remaining hot water in tank; correlate with ``currentInstPower`` to show recovery ETA + +See Also +-------- + +* :doc:`device_status` - Complete status message structure and field definitions +* :doc:`error_codes` - Error code reference for fault diagnosis +* :doc:`../guides/energy_monitoring` - Using energy data for optimization +* :doc:`../guides/time_of_use` - TOU scheduling and rate optimization +* :doc:`../guides/advanced_features_explained` - Weather-responsive heating, demand response, and tank stratification diff --git a/docs/protocol/device_features.rst b/docs/protocol/device_features.rst index 49b7ebd..2538947 100644 --- a/docs/protocol/device_features.rst +++ b/docs/protocol/device_features.rst @@ -47,7 +47,7 @@ The DeviceFeature data contains comprehensive device capabilities, configuration * - ``wifiSwVersion`` - int - None - - WiFi module firmware version - handles NaviLink app connectivity and cloud communication + - WiFi module firmware version - handles WiFi app connectivity and cloud communication - None * - ``controllerSwCode`` - int @@ -255,7 +255,7 @@ The device feature data corresponds to these official NWP500 specifications: * ECO (Energy Cut Off) high-limit safety switch **Smart Features & Connectivity** - * NaviLink WiFi app connectivity + * WiFi app connectivity * Self-diagnostic system with error codes * CTA-2045 Demand Response module support * Anti-Legionella periodic disinfection (1-30 day intervals) @@ -278,7 +278,7 @@ The device returns three separate firmware components for comprehensive system i * Status indicator control and user feedback **WiFi Module (``wifiSwVersion``, ``wifiSwCode``)** - * NaviLink cloud connectivity and app communication + * Cloud connectivity and app communication * Wireless network management and security * Remote monitoring and control capabilities diff --git a/docs/python_api/api_client.rst b/docs/python_api/api_client.rst index 68d5b69..ffd4ac3 100644 --- a/docs/python_api/api_client.rst +++ b/docs/python_api/api_client.rst @@ -192,10 +192,10 @@ get_firmware_info() print(f" Current code: {fw.cur_sw_code}") if fw.downloaded_version: - print(f" ⚠️ Update available: {fw.downloaded_version}") + print(f" [WARNING] Update available: {fw.downloaded_version}") print(f" Download code: {fw.downloaded_sw_code}") else: - print(f" ✓ Up to date") + print(f" [OK] Up to date") # Get firmware for specific device fw_info = await api.get_firmware_info(mac_address="04786332fca0") @@ -358,12 +358,12 @@ Example 2: Firmware Check print(f" Current: {fw.cur_version} (code: {fw.cur_sw_code})") if fw.downloaded_version: - print(f" ⚠️ UPDATE AVAILABLE") + print(f" [WARNING] UPDATE AVAILABLE") print(f" Version: {fw.downloaded_version}") print(f" Code: {fw.downloaded_sw_code}") updates_available += 1 else: - print(f" ✓ Up to date") + print(f" [OK] Up to date") if updates_available: print(f"\n{updates_available} device(s) have updates available") @@ -473,7 +473,7 @@ Best Practices .. code-block:: python - # ✓ Correct usage + # [OK] Correct usage async with NavienAuthClient() as auth: # API: Discover devices api = NavienAPIClient(auth) diff --git a/docs/python_api/auth_client.rst b/docs/python_api/auth_client.rst index a3049d2..7233af2 100644 --- a/docs/python_api/auth_client.rst +++ b/docs/python_api/auth_client.rst @@ -559,7 +559,7 @@ Best Practices .. code-block:: python - # ✓ Correct + # [OK] Correct async with NavienAuthClient(email, password) as auth: # operations diff --git a/docs/python_api/cli.rst b/docs/python_api/cli.rst index baeb070..658c014 100644 --- a/docs/python_api/cli.rst +++ b/docs/python_api/cli.rst @@ -119,9 +119,9 @@ Real-time continuous monitoring of device status. Energy: 85.5% Components: - ✓ Heat Pump Running - ✗ Upper Heater - ✗ Lower Heater + ENABLED: Heat Pump Running + DISABLED: Upper Heater + DISABLED: Lower Heater [12:35:01] Temperature changed: 139.0°F @@ -188,13 +188,13 @@ number. Temperature Range: 100°F - 150°F Supported Features: - ✓ Energy Monitoring - ✓ Anti-Legionella - ✓ Reservations - ✓ Heat Pump Mode - ✓ Electric Mode - ✓ Energy Saver Mode - ✓ High Demand Mode + ENABLED: Energy Monitoring + ENABLED: Anti-Legionella + ENABLED: Reservations + ENABLED: Heat Pump Mode + ENABLED: Electric Mode + ENABLED: Energy Saver Mode + ENABLED: High Demand Mode --get-controller-serial ^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/python_api/constants.rst b/docs/python_api/constants.rst index 5db3915..e41c28a 100644 --- a/docs/python_api/constants.rst +++ b/docs/python_api/constants.rst @@ -322,7 +322,7 @@ Best Practices .. code-block:: python - # ✓ Clear and type-safe + # [OK] Clear and type-safe from nwp500.constants import CommandCode request.command = CommandCode.STATUS_REQUEST @@ -333,7 +333,7 @@ Best Practices .. code-block:: python - # ✓ Preferred - client handles command codes + # [OK] Preferred - client handles command codes await mqtt.request_device_status(device) # ✗ Manual - only for advanced use cases diff --git a/docs/python_api/events.rst b/docs/python_api/events.rst index 30ecfe6..f645f78 100644 --- a/docs/python_api/events.rst +++ b/docs/python_api/events.rst @@ -268,11 +268,11 @@ Example 3: Temperature Alerts def check_temp(status): if status.dhwTemperature < 110: - print("⚠️ WARNING: Temperature below 110°F") + print("WARNING: Temperature below 110°F") send_alert("Low water temperature") if status.dhwTemperature > 145: - print("⚠️ WARNING: Temperature above 145°F") + print("WARNING: Temperature above 145°F") send_alert("High water temperature") mqtt.on('status_received', check_temp) @@ -317,11 +317,11 @@ Best Practices .. code-block:: python - # ✓ Register first + # GOOD: Register first mqtt.on('status_received', handler) await mqtt.connect() - # ✗ May miss early events + # BAD: May miss early events await mqtt.connect() mqtt.on('status_received', handler) diff --git a/docs/python_api/models.rst b/docs/python_api/models.rst index eb790c4..25735c6 100644 --- a/docs/python_api/models.rst +++ b/docs/python_api/models.rst @@ -207,7 +207,7 @@ Device identification and connection information. print(f"Type: {info.device_type}") if info.connected == 2: - print("Status: Online ✓") + print("Status: Online [OK]") else: print("Status: Offline ✗") @@ -267,9 +267,9 @@ Firmware version information. print(f" Current: {fw.cur_version} (code: {fw.cur_sw_code})") if fw.downloaded_version: - print(f" ⚠️ Update available: {fw.downloaded_version}") + print(f" [WARNING] Update available: {fw.downloaded_version}") else: - print(f" ✓ Up to date") + print(f" [OK] Up to date") Status Models ============= @@ -469,19 +469,19 @@ Device capabilities, features, and firmware information. print(f"\nSupported Features:") if feature.energyUsageUse: - print(" ✓ Energy monitoring") + print(" [OK] Energy monitoring") if feature.antiLegionellaSettingUse: - print(" ✓ Anti-Legionella") + print(" [OK] Anti-Legionella") if feature.programReservationUse: - print(" ✓ Reservations") + print(" [OK] Reservations") if feature.heatpumpUse: - print(" ✓ Heat pump mode") + print(" [OK] Heat pump mode") if feature.electricUse: - print(" ✓ Electric mode") + print(" [OK] Electric mode") if feature.energySaverUse: - print(" ✓ Energy Saver mode") + print(" [OK] Energy Saver mode") if feature.highDemandUse: - print(" ✓ High Demand mode") + print(" [OK] High Demand mode") Energy Models ============= @@ -668,7 +668,7 @@ Best Practices .. code-block:: python - # ✓ Type-safe + # [OK] Type-safe from nwp500 import DhwOperationSetting await mqtt.set_dhw_mode(device, DhwOperationSetting.ENERGY_SAVER.value) diff --git a/docs/python_api/mqtt_client.rst b/docs/python_api/mqtt_client.rst index cbdd950..aa03982 100644 --- a/docs/python_api/mqtt_client.rst +++ b/docs/python_api/mqtt_client.rst @@ -1002,11 +1002,11 @@ Best Practices .. code-block:: python - # ✓ Correct order + # CORRECT order await mqtt.subscribe_device_status(device, on_status) await mqtt.request_device_status(device) - # ✗ Wrong - response will be missed + # WRONG - response will be missed await mqtt.request_device_status(device) await mqtt.subscribe_device_status(device, on_status) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index cb50e23..cc574ce 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -164,18 +164,18 @@ Send control commands to change device settings: # Turn on the device await mqtt.set_power(device, power_on=True) - print("✓ Device powered on") + print("Device powered on") # Set to Energy Saver mode await mqtt.set_dhw_mode( device, mode_id=DhwOperationSetting.ENERGY_SAVER.value ) - print("✓ Set to Energy Saver mode") + print("Set to Energy Saver mode") # Set temperature to 120°F await mqtt.set_dhw_temperature(device, temperature=120) - print("✓ Temperature set to 120°F") + print("Temperature set to 120°F") await asyncio.sleep(2) await mqtt.disconnect() diff --git a/examples/README.md b/examples/README.md index 7a0fc82..2963399 100644 --- a/examples/README.md +++ b/examples/README.md @@ -139,7 +139,7 @@ asyncio.run(main()) When running any example with valid credentials, you should see output similar to: ``` -✅ Authenticated as: John Doe +[SUCCESS] Authenticated as: John Doe 📧 Email: your_email@example.com 🔑 Token expires at: 2024-01-15 14:30:00 ``` diff --git a/examples/api_client_example.py b/examples/api_client_example.py index 2c72877..4b51945 100644 --- a/examples/api_client_example.py +++ b/examples/api_client_example.py @@ -53,7 +53,7 @@ async def example_basic_usage(): # Create auth client and authenticate async with NavienAuthClient(email, password) as auth_client: # Already authenticated! - print("✅ Authenticated successfully\n") + print("[SUCCESS] Authenticated successfully\n") # Create API client with authenticated auth_client client = NavienAPIClient(auth_client=auth_client) @@ -62,9 +62,9 @@ async def example_basic_usage(): print("📱 Retrieving devices...") try: devices = await asyncio.wait_for(client.list_devices(), timeout=30.0) - print(f"✅ Found {len(devices)} device(s)\n") + print(f"[SUCCESS] Found {len(devices)} device(s)\n") except asyncio.TimeoutError: - print("❌ Request timed out while retrieving devices") + print("[ERROR] Request timed out while retrieving devices") print(" The API server may be slow or unresponsive.") return 1 @@ -118,7 +118,7 @@ def mask_location(_, __): ) print( - f"✅ Detailed info for: {detailed_info.device_info.device_name}" + f"[SUCCESS] Detailed info for: {detailed_info.device_info.device_name}" ) if detailed_info.device_info.install_type: print( @@ -128,7 +128,9 @@ def mask_location(_, __): print(" Coordinates: (available, not shown for privacy)") print() except asyncio.TimeoutError: - print("⚠️ Request timed out - API may be slow or unresponsive") + print( + "[WARNING] Request timed out - API may be slow or unresponsive" + ) print(" Continuing with other requests...") print() @@ -138,29 +140,31 @@ def mask_location(_, __): firmware_list = await asyncio.wait_for( client.get_firmware_info(mac, additional), timeout=30.0 ) - print(f"✅ Found {len(firmware_list)} firmware components") + print(f"[SUCCESS] Found {len(firmware_list)} firmware components") for fw in firmware_list: print(f" SW Code: {fw.cur_sw_code}, Version: {fw.cur_version}") print() except asyncio.TimeoutError: - print("⚠️ Request timed out - API may be slow or unresponsive") + print( + "[WARNING] Request timed out - API may be slow or unresponsive" + ) print() print("=" * 70) - print("✅ Example completed successfully!") + print("[SUCCESS] Example completed successfully!") print("=" * 70) return 0 except AuthenticationError as e: - print(f"\n❌ Authentication failed: {e.message}") + print(f"\n[ERROR] Authentication failed: {e.message}") print("\nPlease set environment variables:") print(" export NAVIEN_EMAIL='your_email@example.com'") print(" export NAVIEN_PASSWORD='your_password'") return 1 except APIError as e: - print(f"\n❌ API error: {e.message}") + print(f"\n[ERROR] API error: {e.message}") if e.code: print(f" Error code: {e.code}") return 1 @@ -190,7 +194,7 @@ async def example_convenience_function(): api_client = NavienAPIClient(auth_client=auth_client) devices = await api_client.list_devices() - print(f"✅ Found {len(devices)} device(s):\n") + print(f"[SUCCESS] Found {len(devices)} device(s):\n") try: from examples.mask import mask_any, mask_location # type: ignore @@ -214,7 +218,7 @@ def mask_location(_, __): return 0 except Exception as e: - print(f"❌ Error: {str(e)}") + print(f"[ERROR] Error: {str(e)}") return 1 @@ -240,7 +244,7 @@ async def example_error_handling(): # Try to get info for non-existent device await client.get_device_info("invalid_mac_address", "invalid") except APIError as e: - print("✅ Caught APIError as expected:") + print("[SUCCESS] Caught APIError as expected:") print(f" Message: {e.message}") print(f" Code: {e.code}") print() @@ -249,10 +253,10 @@ async def example_error_handling(): print("Example 2: Authentication check") print("-" * 70) if client.is_authenticated: - print("✅ Client is authenticated") + print("[SUCCESS] Client is authenticated") print(f" User: {client.user_email}") else: - print("❌ Client is not authenticated") + print("[ERROR] Client is not authenticated") print() diff --git a/examples/auth_constructor_example.py b/examples/auth_constructor_example.py index 1fb67c0..c07865b 100644 --- a/examples/auth_constructor_example.py +++ b/examples/auth_constructor_example.py @@ -23,7 +23,7 @@ async def main(): # Pass credentials to constructor - authentication happens automatically async with NavienAuthClient(email, password) as auth_client: # Already authenticated! No need to call sign_in() - print(f"✅ Authenticated as: {auth_client.current_user.full_name}") + print(f"[SUCCESS] Authenticated as: {auth_client.current_user.full_name}") print(f"📧 Email: {auth_client.user_email}") print(f"🔑 Token expires at: {auth_client.current_tokens.expires_at}") diff --git a/examples/authenticate.py b/examples/authenticate.py index d77d02d..d2e9bcf 100755 --- a/examples/authenticate.py +++ b/examples/authenticate.py @@ -48,7 +48,7 @@ async def main(): response = client._auth_response # Display user information - print("\n✅ Authentication successful!") + print("\n[SUCCESS] Authentication successful!") print("\nUser Information:") print(f" Name: {response.user_info.full_name}") print(f" Status: {response.user_info.user_status}") @@ -68,7 +68,9 @@ async def main(): print("\nCorrect Authorization Headers:") auth_headers = client.get_auth_headers() print(f" authorization: {auth_headers['authorization'][:50]}...") - print("\n⚠️ IMPORTANT: Use lowercase 'authorization' with raw token") + print( + "\n[WARNING] IMPORTANT: Use lowercase 'authorization' with raw token" + ) print(" (no 'Bearer ' prefix). Standard Bearer format will NOT work!") print("\n Correct: {'authorization': 'eyJraWQi...'}") print(" Wrong: {'Authorization': 'Bearer eyJraWQi...'}") @@ -82,20 +84,20 @@ async def main(): ) except InvalidCredentialsError as e: - print(f"\n❌ Invalid credentials: {e.message}") + print(f"\n[ERROR] Invalid credentials: {e.message}") print("\nPlease set environment variables:") print(" export NAVIEN_EMAIL='your_email@example.com'") print(" export NAVIEN_PASSWORD='your_password'") return 1 except AuthenticationError as e: - print(f"\n❌ Authentication failed: {e.message}") + print(f"\n[ERROR] Authentication failed: {e.message}") if e.code: print(f"Error code: {e.code}") return 1 except Exception as e: - print(f"\n❌ Unexpected error: {str(e)}") + print(f"\n[ERROR] Unexpected error: {str(e)}") import traceback traceback.print_exc() diff --git a/examples/auto_recovery_example.py b/examples/auto_recovery_example.py index 13b648d..e6e25b0 100644 --- a/examples/auto_recovery_example.py +++ b/examples/auto_recovery_example.py @@ -278,13 +278,13 @@ async def on_reconnection_failed(attempts): # ============================================================================ -# STRATEGY 4: Exponential Backoff Retry (Production-Ready) +# STRATEGY 4: Exponential Backoff Retry # ============================================================================ async def strategy_exponential_backoff(auth_client, device): """ - Production-ready strategy: Use exponential backoff for recovery attempts. + Robust strategy: Use exponential backoff for recovery attempts. - This is the most robust strategy for production use. It: + This is an effective strategy for production use. It: - Uses exponential backoff between recovery attempts - Refreshes tokens periodically - Recreates the client cleanly @@ -355,7 +355,7 @@ async def on_reconnection_failed(attempts): mqtt_client.on("reconnection_failed", on_reconnection_failed) await mqtt_client.connect() - logger.info(f"✅ Recovered! Connected: {mqtt_client.client_id}") + logger.info(f"[SUCCESS] Recovered! Connected: {mqtt_client.client_id}") # Re-subscribe await mqtt_client.subscribe_device_status(device, lambda s: None) @@ -440,9 +440,9 @@ async def main(): try: asyncio.run(main()) except KeyboardInterrupt: - print("\n\n⚠️ Interrupted by user") + print("\n\n[WARNING] Interrupted by user") except Exception as e: - print(f"\n❌ Error: {e}") + print(f"\n[ERROR] Error: {e}") import traceback traceback.print_exc() diff --git a/examples/combined_callbacks.py b/examples/combined_callbacks.py index 76cf26a..2b41c58 100644 --- a/examples/combined_callbacks.py +++ b/examples/combined_callbacks.py @@ -42,7 +42,7 @@ async def main(): if not email or not password: print( - "❌ Error: Please set NAVIEN_EMAIL and NAVIEN_PASSWORD environment variables" + "[ERROR] Error: Please set NAVIEN_EMAIL and NAVIEN_PASSWORD environment variables" ) return 1 @@ -53,7 +53,7 @@ async def main(): try: async with NavienAuthClient(email, password) as auth_client: - print(f"✅ Authenticated as: {auth_client.current_user.full_name}") + print(f"[SUCCESS] Authenticated as: {auth_client.current_user.full_name}") print() api_client = NavienAPIClient( @@ -62,7 +62,7 @@ async def main(): devices = await api_client.list_devices() if not devices: - print("❌ No devices found") + print("[ERROR] No devices found") return 1 device = devices[0] @@ -76,7 +76,7 @@ async def main(): try: await mqtt_client.connect() - print("✅ Connected to MQTT") + print("[SUCCESS] Connected to MQTT") print() counts = {"status": 0, "feature": 0} @@ -120,7 +120,7 @@ def on_feature(feature: DeviceFeature): # Now subscribe to typed callbacks await mqtt_client.subscribe_device_status(device, on_status) await mqtt_client.subscribe_device_feature(device, on_feature) - print("✅ Subscribed to both callbacks") + print("[SUCCESS] Subscribed to both callbacks") print() # Request both types of data @@ -132,7 +132,7 @@ def on_feature(feature: DeviceFeature): await asyncio.sleep(2) await mqtt_client.request_device_status(device) - print("✅ Requests sent") + print("[SUCCESS] Requests sent") print() # Wait for responses @@ -147,10 +147,10 @@ def on_feature(feature: DeviceFeature): print("=" * 70) await mqtt_client.disconnect() - print("\n✅ Disconnected") + print("\n[SUCCESS] Disconnected") except Exception as e: - print(f"❌ Error: {e}") + print(f"[ERROR] Error: {e}") if mqtt_client.is_connected: await mqtt_client.disconnect() return 1 @@ -158,7 +158,7 @@ def on_feature(feature: DeviceFeature): return 0 except Exception as e: - print(f"❌ Error: {e}") + print(f"[ERROR] Error: {e}") import traceback traceback.print_exc() diff --git a/examples/command_queue_demo.py b/examples/command_queue_demo.py index dd2e97e..7c99bdc 100644 --- a/examples/command_queue_demo.py +++ b/examples/command_queue_demo.py @@ -37,7 +37,9 @@ async def command_queue_demo(): password = os.getenv("NAVIEN_PASSWORD") if not email or not password: - print("❌ Error: Set NAVIEN_EMAIL and NAVIEN_PASSWORD environment variables") + print( + "[ERROR] Error: Set NAVIEN_EMAIL and NAVIEN_PASSWORD environment variables" + ) return False print("Command Queue Demonstration") @@ -47,7 +49,9 @@ async def command_queue_demo(): # Step 1: Authenticate print("\n1. Authenticating with Navien API...") async with NavienAuthClient(email, password) as auth_client: - print(f" ✅ Authenticated as: {auth_client.current_user.full_name}") + print( + f" [SUCCESS] Authenticated as: {auth_client.current_user.full_name}" + ) # Get devices from nwp500.api_client import NavienAPIClient @@ -56,11 +60,11 @@ async def command_queue_demo(): devices = await api_client.list_devices() if not devices: - print(" ❌ No devices found") + print(" [ERROR] No devices found") return False device = devices[0] - print(f" ✅ Found device: {device.device_info.device_name}") + print(f" [SUCCESS] Found device: {device.device_info.device_name}") # Step 2: Create MQTT client with command queue enabled print("\n2. Creating MQTT client with command queue...") @@ -77,12 +81,12 @@ async def command_queue_demo(): # Register event handlers def on_interrupted(error): - print(f" ⚠️ Connection interrupted: {error}") - print(f" 📝 Queued commands: {mqtt_client.queued_commands_count}") + print(f" [WARNING] Connection interrupted: {error}") + print(f" [NOTE] Queued commands: {mqtt_client.queued_commands_count}") def on_resumed(return_code, session_present): - print(" ✅ Connection resumed!") - print(f" 📝 Queued commands: {mqtt_client.queued_commands_count}") + print(" [SUCCESS] Connection resumed!") + print(f" [NOTE] Queued commands: {mqtt_client.queued_commands_count}") mqtt_client.on("connection_interrupted", on_interrupted) mqtt_client.on("connection_resumed", on_resumed) @@ -90,7 +94,7 @@ def on_resumed(return_code, session_present): # Step 3: Connect print("\n3. Connecting to AWS IoT...") await mqtt_client.connect() - print(f" ✅ Connected! Client ID: {mqtt_client.client_id}") + print(f" [SUCCESS] Connected! Client ID: {mqtt_client.client_id}") # Step 4: Subscribe to device print("\n4. Subscribing to device messages...") @@ -102,13 +106,13 @@ def on_message(topic, message): received_messages.append(message) await mqtt_client.subscribe_device(device, on_message) - print(" ✅ Subscribed to device") + print(" [SUCCESS] Subscribed to device") # Step 5: Test normal operation print("\n5. Testing normal operation (connected)...") print(" Sending status request...") await mqtt_client.request_device_status(device) - print(" ✅ Command sent successfully") + print(" [SUCCESS] Command sent successfully") await asyncio.sleep(2) # Step 6: Simulate disconnection and queue commands @@ -119,7 +123,7 @@ def on_message(topic, message): # Manually disconnect await mqtt_client.disconnect() - print(" ✅ Disconnected") + print(" [SUCCESS] Disconnected") # Try sending commands while disconnected - they should be queued print("\n7. Sending commands while disconnected (will be queued)...") @@ -138,19 +142,19 @@ def on_message(topic, message): await mqtt_client.set_dhw_temperature_display(device, 130) print(f" Queue size: {mqtt_client.queued_commands_count}") - print(f" ✅ Queued {mqtt_client.queued_commands_count} command(s)") + print(f" [SUCCESS] Queued {mqtt_client.queued_commands_count} command(s)") # Step 8: Reconnect and watch commands get sent print("\n8. Reconnecting...") await mqtt_client.connect() - print(" ✅ Reconnected!") + print(" [SUCCESS] Reconnected!") # Give time for queued commands to be sent print(" Waiting for queued commands to be sent...") await asyncio.sleep(3) print( - f" ✅ Queue processed! Remaining: {mqtt_client.queued_commands_count}" + f" [SUCCESS] Queue processed! Remaining: {mqtt_client.queued_commands_count}" ) # Step 9: Test queue limits @@ -165,7 +169,7 @@ def on_message(topic, message): print( f" Queue size: {mqtt_client.queued_commands_count} (max: {config.max_queued_commands})" ) - print(" ✅ Queue properly limited (oldest commands dropped)") + print(" [SUCCESS] Queue properly limited (oldest commands dropped)") # Clear queue cleared = mqtt_client.clear_command_queue() @@ -180,10 +184,10 @@ def on_message(topic, message): # Cleanup print("\n11. Disconnecting...") await mqtt_client.disconnect() - print(" ✅ Disconnected cleanly") + print(" [SUCCESS] Disconnected cleanly") print("\n" + "=" * 60) - print("✅ Command Queue Demo Complete!") + print("[SUCCESS] Command Queue Demo Complete!") print("\nKey Features Demonstrated:") print(" • Commands queued when disconnected") print(" • Automatic sending on reconnection") @@ -194,7 +198,7 @@ def on_message(topic, message): return True except Exception as e: - print(f"\n❌ Error: {e}") + print(f"\n[ERROR] Error: {e}") import traceback traceback.print_exc() diff --git a/examples/device_feature_callback.py b/examples/device_feature_callback.py index afc7123..dd39416 100644 --- a/examples/device_feature_callback.py +++ b/examples/device_feature_callback.py @@ -53,7 +53,7 @@ async def main(): if not email or not password: print( - "❌ Error: Please set NAVIEN_EMAIL and NAVIEN_PASSWORD environment variables" + "[ERROR] Error: Please set NAVIEN_EMAIL and NAVIEN_PASSWORD environment variables" ) print("\nExample:") print(" export NAVIEN_EMAIL='your_email@example.com'") @@ -69,7 +69,7 @@ async def main(): # Step 1: Authenticate and get AWS credentials print("Step 1: Authenticating with Navien API...") async with NavienAuthClient(email, password) as auth_client: - print(f"✅ Authenticated as: {auth_client.current_user.full_name}") + print(f"[SUCCESS] Authenticated as: {auth_client.current_user.full_name}") print() # Step 2: Get device list @@ -80,14 +80,14 @@ async def main(): devices = await api_client.list_devices() if not devices: - print("❌ Error: No devices found in your account") + print("[ERROR] Error: No devices found in your account") return 1 device = devices[0] device_id = device.device_info.mac_address device_type = device.device_info.device_type - print(f"✅ Using device: {device.device_info.device_name}") + print(f"[SUCCESS] Using device: {device.device_info.device_name}") print(f" MAC Address: {mask_mac(device_id)}") print() @@ -97,7 +97,7 @@ async def main(): try: await mqtt_client.connect() - print("✅ Connected to AWS IoT Core") + print("[SUCCESS] Connected to AWS IoT Core") print() # Step 4: Subscribe to device feature with automatic parsing @@ -224,7 +224,7 @@ def on_device_feature(feature: DeviceFeature): # Subscribe with automatic parsing await mqtt_client.subscribe_device_feature(device, on_device_feature) - print("✅ Subscribed to device features with automatic parsing") + print("[SUCCESS] Subscribed to device features with automatic parsing") print() # Step 5: Request device info to get feature data @@ -233,7 +233,7 @@ def on_device_feature(feature: DeviceFeature): await asyncio.sleep(1) await mqtt_client.request_device_info(device) - print("✅ Device info request sent") + print("[SUCCESS] Device info request sent") print() # Wait for feature message @@ -242,7 +242,7 @@ def on_device_feature(feature: DeviceFeature): try: await asyncio.sleep(10) except KeyboardInterrupt: - print("\n⚠️ Interrupted by user") + print("\n[WARNING] Interrupted by user") print() print( @@ -253,7 +253,7 @@ def on_device_feature(feature: DeviceFeature): # Disconnect print("Step 6: Disconnecting from AWS IoT...") await mqtt_client.disconnect() - print("✅ Disconnected successfully") + print("[SUCCESS] Disconnected successfully") except Exception: import logging @@ -267,12 +267,12 @@ def on_device_feature(feature: DeviceFeature): print() print("=" * 70) - print("✅ Device Feature Callback Example Completed Successfully!") + print("[SUCCESS] Device Feature Callback Example Completed Successfully!") print("=" * 70) return 0 except AuthenticationError as e: - print(f"\n❌ Authentication failed: {e.message}") + print(f"\n[ERROR] Authentication failed: {e.message}") if e.code: print(f" Error code: {e.code}") return 1 diff --git a/examples/device_status_callback.py b/examples/device_status_callback.py index 393d109..2634f27 100755 --- a/examples/device_status_callback.py +++ b/examples/device_status_callback.py @@ -59,7 +59,7 @@ async def main(): if not email or not password: print( - "❌ Error: Please set NAVIEN_EMAIL and NAVIEN_PASSWORD environment variables" + "[ERROR] Error: Please set NAVIEN_EMAIL and NAVIEN_PASSWORD environment variables" ) print("\nExample:") print(" export NAVIEN_EMAIL='your_email@example.com'") @@ -75,7 +75,7 @@ async def main(): # Step 1: Authenticate and get AWS credentials print("Step 1: Authenticating with Navien API...") async with NavienAuthClient(email, password) as auth_client: - print(f"✅ Authenticated as: {auth_client.current_user.full_name}") + print(f"[SUCCESS] Authenticated as: {auth_client.current_user.full_name}") print() # Step 2: Get device list @@ -86,14 +86,14 @@ async def main(): devices = await api_client.list_devices() if not devices: - print("❌ Error: No devices found in your account") + print("[ERROR] Error: No devices found in your account") return 1 device = devices[0] device_id = device.device_info.mac_address device_type = device.device_info.device_type - print(f"✅ Using device: {device.device_info.device_name}") + print(f"[SUCCESS] Using device: {device.device_info.device_name}") print(f" MAC Address: {mask_mac(device_id)}") print() @@ -103,7 +103,7 @@ async def main(): try: await mqtt_client.connect() - print("✅ Connected to AWS IoT Core") + print("[SUCCESS] Connected to AWS IoT Core") print() # Step 4: Subscribe to device status with automatic parsing @@ -195,7 +195,7 @@ def on_device_status(status: DeviceStatus): # Then subscribe with automatic parsing await mqtt_client.subscribe_device_status(device, on_device_status) - print("✅ Subscribed to device messages and status parsing") + print("[SUCCESS] Subscribed to device messages and status parsing") print() # Step 5: Request device status @@ -204,7 +204,7 @@ def on_device_status(status: DeviceStatus): await asyncio.sleep(1) await mqtt_client.request_device_status(device) - print("✅ Status request sent") + print("[SUCCESS] Status request sent") print() # Wait for status updates @@ -213,7 +213,7 @@ def on_device_status(status: DeviceStatus): try: await asyncio.sleep(20) except KeyboardInterrupt: - print("\n⚠️ Interrupted by user") + print("\n[WARNING] Interrupted by user") print() print("📊 Summary:") @@ -224,7 +224,7 @@ def on_device_status(status: DeviceStatus): # Disconnect print("Step 6: Disconnecting from AWS IoT...") await mqtt_client.disconnect() - print("✅ Disconnected successfully") + print("[SUCCESS] Disconnected successfully") except Exception: import logging @@ -238,12 +238,12 @@ def on_device_status(status: DeviceStatus): print() print("=" * 70) - print("✅ Device Status Callback Example Completed Successfully!") + print("[SUCCESS] Device Status Callback Example Completed Successfully!") print("=" * 70) return 0 except AuthenticationError as e: - print(f"\n❌ Authentication failed: {e.message}") + print(f"\n[ERROR] Authentication failed: {e.message}") if e.code: print(f" Error code: {e.code}") return 1 diff --git a/examples/device_status_callback_debug.py b/examples/device_status_callback_debug.py index b5af78a..9eb67a2 100644 --- a/examples/device_status_callback_debug.py +++ b/examples/device_status_callback_debug.py @@ -45,7 +45,7 @@ async def main(): if not email or not password: print( - "❌ Error: Please set NAVIEN_EMAIL and NAVIEN_PASSWORD environment variables" + "[ERROR] Error: Please set NAVIEN_EMAIL and NAVIEN_PASSWORD environment variables" ) return 1 @@ -58,7 +58,7 @@ async def main(): # Step 1: Authenticate and get AWS credentials print("Step 1: Authenticating with Navien API...") async with NavienAuthClient(email, password) as auth_client: - print(f"✅ Authenticated as: {auth_client.current_user.full_name}") + print(f"[SUCCESS] Authenticated as: {auth_client.current_user.full_name}") print() # Step 2: Get device list @@ -69,7 +69,7 @@ async def main(): devices = await api_client.list_devices() if not devices: - print("❌ Error: No devices found in your account") + print("[ERROR] Error: No devices found in your account") return 1 device = devices[0] @@ -83,7 +83,7 @@ async def main(): def mask_any(_): return "[REDACTED]" - print(f"✅ Using device: {device.device_info.device_name}") + print(f"[SUCCESS] Using device: {device.device_info.device_name}") print(f" MAC Address: {mask_mac(device_id)}") print(f" Device Type: {mask_any(device_type)}") print() @@ -94,7 +94,7 @@ def mask_any(_): try: await mqtt_client.connect() - print("✅ Connected to AWS IoT Core") + print("[SUCCESS] Connected to AWS IoT Core") print() # Step 4: Subscribe with BOTH raw and parsed callbacks @@ -113,7 +113,7 @@ def raw_message_handler(topic: str, message: dict): print(f" Response keys: {list(message['response'].keys())}") if "status" in message["response"]: - print(" ✅ Contains STATUS data") + print(" [SUCCESS] Contains STATUS data") status_keys = list(message["response"]["status"].keys())[ :10 ] @@ -128,7 +128,9 @@ def raw_message_handler(topic: str, message: dict): def on_device_status(status: DeviceStatus): """Parsed status callback.""" message_count["status"] += 1 - print(f"\n✅ PARSED Status Update #{message_count['status']}") + print( + f"\n[SUCCESS] PARSED Status Update #{message_count['status']}" + ) print(f" DHW Temperature: {status.dhwTemperature:.1f}°F") print(f" Operation Mode: {status.operationMode.name}") print(f" Compressor: {status.compUse}") @@ -136,12 +138,12 @@ def on_device_status(status: DeviceStatus): # Subscribe with raw handler first print("Subscribing to raw messages...") await mqtt_client.subscribe_device(device, raw_message_handler) - print("✅ Subscribed to raw messages") + print("[SUCCESS] Subscribed to raw messages") # Also subscribe with parsed handler print("Subscribing to parsed status...") await mqtt_client.subscribe_device_status(device, on_device_status) - print("✅ Subscribed to parsed status") + print("[SUCCESS] Subscribed to parsed status") print() # Step 5: Request device status @@ -150,7 +152,7 @@ def on_device_status(status: DeviceStatus): await asyncio.sleep(1) await mqtt_client.request_device_status(device) - print("✅ Status request sent") + print("[SUCCESS] Status request sent") print() # Wait for status updates @@ -159,7 +161,7 @@ def on_device_status(status: DeviceStatus): try: await asyncio.sleep(20) except KeyboardInterrupt: - print("\n⚠️ Interrupted by user") + print("\n[WARNING] Interrupted by user") print() print("📊 Summary:") @@ -170,7 +172,7 @@ def on_device_status(status: DeviceStatus): # Disconnect print("Step 6: Disconnecting from AWS IoT...") await mqtt_client.disconnect() - print("✅ Disconnected successfully") + print("[SUCCESS] Disconnected successfully") except Exception: import logging @@ -184,12 +186,12 @@ def on_device_status(status: DeviceStatus): print() print("=" * 70) - print("✅ Debug Example Completed!") + print("[SUCCESS] Debug Example Completed!") print("=" * 70) return 0 except AuthenticationError as e: - print(f"\n❌ Authentication failed: {e.message}") + print(f"\n[ERROR] Authentication failed: {e.message}") if e.code: print(f" Error code: {e.code}") return 1 diff --git a/examples/energy_usage_example.py b/examples/energy_usage_example.py index 5634416..e8aface 100755 --- a/examples/energy_usage_example.py +++ b/examples/energy_usage_example.py @@ -70,13 +70,13 @@ def on_energy_usage(energy: EnergyUsageResponse): emoji = "🌟" elif hp_pct > 60: efficiency_rating = "Good" - emoji = "✅" + emoji = "[SUCCESS]" elif hp_pct > 40: efficiency_rating = "Fair" - emoji = "⚠️" + emoji = "[WARNING]" else: efficiency_rating = "Poor" - emoji = "⚠️" + emoji = "[WARNING]" print(f"⚡ EFFICIENCY RATING: {emoji} {efficiency_rating}") print(" (Higher heat pump usage = better efficiency)") @@ -107,7 +107,7 @@ def on_energy_usage(energy: EnergyUsageResponse): # Create API client and authenticate print("Authenticating...") async with NavienAuthClient(email, password) as auth_client: - print("✓ Authenticated") + print("[OK] Authenticated") # Create API client with authenticated auth_client api_client = NavienAPIClient(auth_client=auth_client) @@ -121,18 +121,18 @@ def on_energy_usage(energy: EnergyUsageResponse): device = devices[0] # Avoid logging sensitive info such as MAC address. - print(f"✓ Device detected: {device.device_info.device_name}") + print(f"[OK] Device detected: {device.device_info.device_name}") # Connect to MQTT print("\nConnecting to MQTT...") mqtt_client = NavienMqttClient(auth_client) await mqtt_client.connect() - print("✓ Connected to MQTT") + print("[OK] Connected to MQTT") # Subscribe to energy usage responses print("\nSubscribing to energy usage data...") await mqtt_client.subscribe_energy_usage(device, on_energy_usage) - print("✓ Subscribed to energy usage responses") + print("[OK] Subscribed to energy usage responses") # Request energy usage for current month now = datetime.now() @@ -143,7 +143,7 @@ def on_energy_usage(energy: EnergyUsageResponse): await mqtt_client.request_energy_usage( device, year=current_year, months=[current_month] ) - print("✓ Request sent") + print("[OK] Request sent") # Wait for response print("\nWaiting for energy data (up to 30 seconds)...") @@ -152,7 +152,7 @@ def on_energy_usage(energy: EnergyUsageResponse): # Cleanup print("\nDisconnecting...") await mqtt_client.disconnect() - print("✓ Disconnected") + print("[OK] Disconnected") if __name__ == "__main__": diff --git a/examples/event_emitter_demo.py b/examples/event_emitter_demo.py index 545d64a..9508f98 100644 --- a/examples/event_emitter_demo.py +++ b/examples/event_emitter_demo.py @@ -41,7 +41,7 @@ def log_temperature(old_temp: float, new_temp: float): def alert_on_high_temp(old_temp: float, new_temp: float): """Alert handler for high temperatures.""" if new_temp > 145: - print(f"⚠️ [Alert] HIGH TEMPERATURE: {new_temp}°F!") + print(f"[WARNING] [Alert] HIGH TEMPERATURE: {new_temp}°F!") async def save_temperature_to_db(old_temp: float, new_temp: float): @@ -81,14 +81,14 @@ def on_heating_stopped(status: DeviceStatus): # Example 4: Error handlers def on_error_detected(error_code: str, status: DeviceStatus): """Handler for error detection.""" - print(f"❌ [Error] ERROR DETECTED: {error_code}") + print(f"[ERROR] [Error] ERROR DETECTED: {error_code}") print(f" Temperature: {status.dhwTemperature}°F") print(f" Mode: {status.operationMode}") def on_error_cleared(error_code: str): """Handler for error cleared.""" - print(f"✅ [Error] ERROR CLEARED: {error_code}") + print(f"[SUCCESS] [Error] ERROR CLEARED: {error_code}") # Example 5: Connection state handlers @@ -110,7 +110,9 @@ async def main(): password = os.getenv("NAVIEN_PASSWORD") if not email or not password: - print("❌ Error: Set NAVIEN_EMAIL and NAVIEN_PASSWORD environment variables") + print( + "[ERROR] Error: Set NAVIEN_EMAIL and NAVIEN_PASSWORD environment variables" + ) return False print("=" * 70) @@ -122,7 +124,9 @@ async def main(): # Step 1: Authenticate print("1. Authenticating...") async with NavienAuthClient(email, password) as auth_client: - print(f" ✅ Authenticated as: {auth_client.current_user.full_name}") + print( + f" [SUCCESS] Authenticated as: {auth_client.current_user.full_name}" + ) print() # Get devices @@ -130,17 +134,17 @@ async def main(): devices = await api_client.list_devices() if not devices: - print(" ❌ No devices found") + print(" [ERROR] No devices found") return False device = devices[0] - print(f" ✅ Device: {device.device_info.device_name}") + print(f" [SUCCESS] Device: {device.device_info.device_name}") print() # Step 2: Create MQTT client (inherits EventEmitter) print("2. Creating MQTT client with event emitter...") mqtt_client = NavienMqttClient(auth_client) - print(" ✅ Client created") + print(" [SUCCESS] Client created") print() # Step 3: Register event listeners BEFORE connecting @@ -150,34 +154,34 @@ async def main(): mqtt_client.on("temperature_changed", log_temperature) mqtt_client.on("temperature_changed", alert_on_high_temp) mqtt_client.on("temperature_changed", save_temperature_to_db) - print(" ✅ Registered 3 temperature change handlers") + print(" [SUCCESS] Registered 3 temperature change handlers") # Mode change - multiple handlers mqtt_client.on("mode_changed", log_mode_change) mqtt_client.on("mode_changed", optimize_on_mode_change) - print(" ✅ Registered 2 mode change handlers") + print(" [SUCCESS] Registered 2 mode change handlers") # Power state changes mqtt_client.on("heating_started", on_heating_started) mqtt_client.on("heating_stopped", on_heating_stopped) - print(" ✅ Registered heating start/stop handlers") + print(" [SUCCESS] Registered heating start/stop handlers") # Error handling mqtt_client.on("error_detected", on_error_detected) mqtt_client.on("error_cleared", on_error_cleared) - print(" ✅ Registered error handlers") + print(" [SUCCESS] Registered error handlers") # Connection state mqtt_client.on("connection_interrupted", on_connection_interrupted) mqtt_client.on("connection_resumed", on_connection_resumed) - print(" ✅ Registered connection handlers") + print(" [SUCCESS] Registered connection handlers") # One-time listener example mqtt_client.once( "status_received", lambda s: print(f" 🎉 First status received: {s.dhwTemperature}°F"), ) - print(" ✅ Registered one-time status handler") + print(" [SUCCESS] Registered one-time status handler") print() # Show listener counts @@ -197,19 +201,19 @@ async def main(): # Step 4: Connect and subscribe print("5. Connecting to MQTT...") await mqtt_client.connect() - print(" ✅ Connected!") + print(" [SUCCESS] Connected!") print() print("6. Subscribing to device status...") # We pass a dummy callback since we're using events await mqtt_client.subscribe_device_status(device, lambda s: None) - print(" ✅ Subscribed - events will now be emitted") + print(" [SUCCESS] Subscribed - events will now be emitted") print() # Step 5: Request initial status print("7. Requesting initial status...") await mqtt_client.request_device_status(device) - print(" ✅ Request sent") + print(" [SUCCESS] Request sent") print() # Step 6: Monitor for changes @@ -259,11 +263,11 @@ async def main(): # Step 9: Cleanup print("11. Disconnecting...") await mqtt_client.disconnect() - print(" ✅ Disconnected cleanly") + print(" [SUCCESS] Disconnected cleanly") print() print("=" * 70) - print("✅ Event Emitter Demo Complete!") + print("[SUCCESS] Event Emitter Demo Complete!") print() print("Key Features Demonstrated:") print(" • Multiple listeners per event") @@ -277,7 +281,7 @@ async def main(): return True except Exception as e: - print(f"\n❌ Error: {e}") + print(f"\n[ERROR] Error: {e}") import traceback traceback.print_exc() diff --git a/examples/exception_handling_example.py b/examples/exception_handling_example.py index b1abf90..2a0a2ec 100755 --- a/examples/exception_handling_example.py +++ b/examples/exception_handling_example.py @@ -59,16 +59,16 @@ async def example_authentication_errors(): async with NavienAuthClient("invalid@example.com", "wrong_password") as _: pass except InvalidCredentialsError as e: - print(f"✓ Caught InvalidCredentialsError: {e}") + print(f"[OK] Caught InvalidCredentialsError: {e}") print(f" Status code: {e.status_code}") print(" Can check credentials and retry") except TokenRefreshError as e: - print(f"✓ Caught TokenRefreshError: {e}") + print(f"[OK] Caught TokenRefreshError: {e}") print(" Need to re-authenticate with fresh credentials") except AuthenticationError as e: - print(f"✓ Caught AuthenticationError: {e}") + print(f"[OK] Caught AuthenticationError: {e}") print(" General authentication failure") # Show structured error data @@ -86,7 +86,7 @@ async def example_mqtt_errors(): password = os.getenv("NAVIEN_PASSWORD", "your_password") if email == "your_email@example.com": - print("⚠️ Set NAVIEN_EMAIL and NAVIEN_PASSWORD to run this example") + print("[WARNING] Set NAVIEN_EMAIL and NAVIEN_PASSWORD to run this example") return try: @@ -109,22 +109,22 @@ async def example_mqtt_errors(): try: await mqtt.request_device_status(device) except MqttNotConnectedError as e: - print(f"✓ Caught MqttNotConnectedError: {e}") + print(f"[OK] Caught MqttNotConnectedError: {e}") print(" Can reconnect and retry the operation") except MqttConnectionError as e: - print(f"✓ Caught MqttConnectionError: {e}") + print(f"[OK] Caught MqttConnectionError: {e}") print(f" Error code: {e.error_code}") print(" Network or AWS IoT connection issue") except MqttPublishError as e: - print(f"✓ Caught MqttPublishError: {e}") + print(f"[OK] Caught MqttPublishError: {e}") if e.retriable: - print(" ✓ This error is retriable!") + print(" [OK] This error is retriable!") print(" Can implement exponential backoff retry") except MqttError as e: - print(f"✓ Caught MqttError (base class): {e}") + print(f"[OK] Caught MqttError (base class): {e}") print(" Catches all MQTT-related errors") @@ -138,7 +138,7 @@ async def example_validation_errors(): password = os.getenv("NAVIEN_PASSWORD", "your_password") if email == "your_email@example.com": - print("⚠️ Set NAVIEN_EMAIL and NAVIEN_PASSWORD to run this example") + print("[WARNING] Set NAVIEN_EMAIL and NAVIEN_PASSWORD to run this example") return try: @@ -158,14 +158,14 @@ async def example_validation_errors(): try: await mqtt.set_dhw_mode(device, mode_id=5, vacation_days=50) except RangeValidationError as e: - print(f"✓ Caught RangeValidationError: {e}") + print(f"[OK] Caught RangeValidationError: {e}") print(f" Field: {e.field}") print(f" Invalid value: {e.value}") print(f" Valid range: {e.min_value} to {e.max_value}") print(" Can show user-friendly error message!") except ValidationError as e: - print(f"✓ Caught ValidationError (base class): {e}") + print(f"[OK] Caught ValidationError (base class): {e}") await mqtt.disconnect() @@ -197,7 +197,7 @@ async def operation_with_retry(max_retries=3): if e.retriable and attempt < max_retries - 1: wait_time = 2**attempt # Exponential backoff print( - f" ✓ Retriable error: {e.error_code}, " + f" [OK] Retriable error: {e.error_code}, " f"retrying in {wait_time}s..." ) await asyncio.sleep(wait_time) @@ -259,17 +259,17 @@ async def example_catch_all_library_errors(): raise MqttNotConnectedError("Not connected") except Nwp500Error as e: - print(f"✓ Caught Nwp500Error (base for all library errors): {e}") + print(f"[OK] Caught Nwp500Error (base for all library errors): {e}") print(f" Error type: {type(e).__name__}") print(" Can catch all library exceptions with single handler") # Check specific error type if isinstance(e, MqttError): - print(" ✓ This is an MQTT error") + print(" [OK] This is an MQTT error") elif isinstance(e, AuthenticationError): - print(" ✓ This is an authentication error") + print(" [OK] This is an authentication error") elif isinstance(e, ValidationError): - print(" ✓ This is a validation error") + print(" [OK] This is a validation error") async def example_exception_chaining(): @@ -288,7 +288,7 @@ async def example_exception_chaining(): raise AuthenticationError("Network error during sign-in") from e except AuthenticationError as e: - print(f"✓ Caught AuthenticationError: {e}") + print(f"[OK] Caught AuthenticationError: {e}") print(f" Original cause: {e.__cause__}") print(f" Original cause type: {type(e.__cause__).__name__}") print("\nFull exception chain is preserved for debugging!") @@ -310,7 +310,7 @@ async def main(): await example_exception_chaining() print("\n" + "=" * 70) - print("✅ All examples completed!") + print("[SUCCESS] All examples completed!") print("=" * 70) print("\nKey Takeaways:") print(" 1. Use specific exception types for better error handling") diff --git a/examples/improved_auth_pattern.py b/examples/improved_auth_pattern.py index 033681e..5c64898 100644 --- a/examples/improved_auth_pattern.py +++ b/examples/improved_auth_pattern.py @@ -25,7 +25,7 @@ async def main(): # Authenticate once and use the auth_client everywhere async with NavienAuthClient(email, password) as auth_client: # Already authenticated! - print(f"✅ Authenticated as: {auth_client.user_email}") + print(f"[SUCCESS] Authenticated as: {auth_client.user_email}") # Step 2: Create API client and get device api_client = NavienAPIClient(auth_client=auth_client) @@ -35,12 +35,12 @@ async def main(): print("No devices found") return - print(f"✅ Found device: {device.device_info.device_name}") + print(f"[SUCCESS] Found device: {device.device_info.device_name}") # Step 3: Create MQTT client using the same auth_client mqtt = NavienMqttClient(auth_client) await mqtt.connect() - print(f"✅ MQTT Connected: {mqtt.client_id}") + print(f"[SUCCESS] MQTT Connected: {mqtt.client_id}") # Step 4: Monitor device status def on_status(status): @@ -57,7 +57,7 @@ def on_status(status): await asyncio.sleep(10) await mqtt.disconnect() - print("\n✅ Disconnected") + print("\n[SUCCESS] Disconnected") if __name__ == "__main__": diff --git a/examples/mqtt_client_example.py b/examples/mqtt_client_example.py index 06d7ea5..e0d070e 100755 --- a/examples/mqtt_client_example.py +++ b/examples/mqtt_client_example.py @@ -54,7 +54,7 @@ async def main(): if not email or not password: print( - "❌ Error: Please set NAVIEN_EMAIL and NAVIEN_PASSWORD environment variables" + "[ERROR] Error: Please set NAVIEN_EMAIL and NAVIEN_PASSWORD environment variables" ) print("\nExample:") print(" export NAVIEN_EMAIL='your_email@example.com'") @@ -70,15 +70,15 @@ async def main(): # Step 1: Authenticate and get AWS credentials print("Step 1: Authenticating with Navien API...") async with NavienAuthClient(email, password) as auth_client: - print(f"✅ Authenticated as: {auth_client.current_user.full_name}") + print(f"[SUCCESS] Authenticated as: {auth_client.current_user.full_name}") # Check if we have AWS credentials for MQTT if not auth_client.current_tokens.access_key_id: - print("❌ Error: No AWS credentials in authentication response") + print("[ERROR] Error: No AWS credentials in authentication response") print(" MQTT communication requires AWS IoT credentials") return 1 - print("✅ AWS IoT credentials obtained") + print("[SUCCESS] AWS IoT credentials obtained") print( f" Access Key ID: {auth_client.current_tokens.access_key_id[:15]}..." ) @@ -97,11 +97,11 @@ async def main(): devices = await api_client.list_devices() if not devices: - print("❌ Error: No devices found in your account") + print("[ERROR] Error: No devices found in your account") print(" Please register a device first") return 1 - print(f"✅ Found {len(devices)} device(s):") + print(f"[SUCCESS] Found {len(devices)} device(s):") for i, device in enumerate(devices): print(f" {i + 1}. {device.device_info.device_name} (MAC: **MASKED**)") print( @@ -121,7 +121,7 @@ async def main(): def mask_any(_): # pragma: no cover - fallback return "[REDACTED]" - print(f"✅ Using device: {device.device_info.device_name}") + print(f"[SUCCESS] Using device: {device.device_info.device_name}") print(f" MAC Address: {mask_mac(device_id)}") print(f" Device Type: {mask_any(device_type)}") print() @@ -132,7 +132,7 @@ def mask_any(_): # pragma: no cover - fallback try: await mqtt_client.connect() - print("✅ Connected to AWS IoT Core") + print("[SUCCESS] Connected to AWS IoT Core") print(f" Client ID: {mqtt_client.client_id}") print(f" Session ID: {mqtt_client.session_id}") print() @@ -172,7 +172,7 @@ def on_device_feature(feature: DeviceFeature): # Subscribe with typed parsing await mqtt_client.subscribe_device_status(device, on_device_status) await mqtt_client.subscribe_device_feature(device, on_device_feature) - print("✅ Subscribed to device messages with typed parsing") + print("[SUCCESS] Subscribed to device messages with typed parsing") print() # Step 5: Send commands and monitor responses @@ -201,7 +201,7 @@ def on_device_feature(feature: DeviceFeature): try: await asyncio.sleep(15) except KeyboardInterrupt: - print("\n⚠️ Interrupted by user") + print("\n[WARNING] Interrupted by user") print() print(f"📊 Summary: Received {message_count['count']} message(s)") @@ -210,7 +210,7 @@ def on_device_feature(feature: DeviceFeature): # Step 6: Disconnect print("Step 6: Disconnecting from AWS IoT...") await mqtt_client.disconnect() - print("✅ Disconnected successfully") + print("[SUCCESS] Disconnected successfully") except Exception: import logging @@ -224,12 +224,12 @@ def on_device_feature(feature: DeviceFeature): print() print("=" * 70) - print("✅ MQTT Client Example Completed Successfully!") + print("[SUCCESS] MQTT Client Example Completed Successfully!") print("=" * 70) return 0 except AuthenticationError as e: - print(f"\n❌ Authentication failed: {e.message}") + print(f"\n[ERROR] Authentication failed: {e.message}") if e.code: print(f" Error code: {e.code}") return 1 diff --git a/examples/periodic_requests.py b/examples/periodic_requests.py index 93fc35a..00d31f1 100755 --- a/examples/periodic_requests.py +++ b/examples/periodic_requests.py @@ -190,9 +190,9 @@ async def monitor_with_dots(seconds: int, interval: int = 5): request_type=PeriodicRequestType.DEVICE_STATUS, period_seconds=20, ) - print("✓ Periodic requests started") + print("[OK] Periodic requests started") else: - print("✓ Periodic requests not started (disabled by config)") + print("[OK] Periodic requests not started (disabled by config)") print("Waiting 15 seconds (should see no new automatic requests)...") await asyncio.sleep(15) diff --git a/examples/reconnection_demo.py b/examples/reconnection_demo.py index f339b95..5055747 100644 --- a/examples/reconnection_demo.py +++ b/examples/reconnection_demo.py @@ -38,7 +38,7 @@ async def main(): # Authenticate async with NavienAuthClient(email, password) as auth_client: - print(f"✅ Authenticated as: {auth_client.current_user.full_name}") + print(f"[SUCCESS] Authenticated as: {auth_client.current_user.full_name}") # Get device api_client = NavienAPIClient(auth_client=auth_client) @@ -48,7 +48,7 @@ async def main(): print("No devices found") return - print(f"✅ Found device: {device.device_info.device_name}") + print(f"[SUCCESS] Found device: {device.device_info.device_name}") # Configure MQTT with custom reconnection settings config = MqttConnectionConfig( @@ -67,11 +67,11 @@ async def main(): # Register event handlers def on_interrupted(error): - print(f"\n⚠️ Connection interrupted: {error}") + print(f"\n[WARNING] Connection interrupted: {error}") print(" Automatic reconnection will begin...") def on_resumed(return_code, session_present): - print("\n✅ Connection resumed!") + print("\n[SUCCESS] Connection resumed!") print(f" Return code: {return_code}") print(f" Session present: {session_present}") @@ -80,7 +80,7 @@ def on_resumed(return_code, session_present): # Connect await mqtt_client.connect() - print(f"✅ MQTT Connected: {mqtt_client.client_id}") + print(f"[SUCCESS] MQTT Connected: {mqtt_client.client_id}") # Subscribe to device status status_count = 0 @@ -132,16 +132,16 @@ def on_status(status): # Disconnect await mqtt_client.disconnect() - print("\n✅ Disconnected") + print("\n[SUCCESS] Disconnected") if __name__ == "__main__": try: asyncio.run(main()) except KeyboardInterrupt: - print("\n\n⚠️ Interrupted by user") + print("\n\n[WARNING] Interrupted by user") except Exception as e: - print(f"\n❌ Error: {e}") + print(f"\n[ERROR] Error: {e}") import traceback traceback.print_exc() diff --git a/examples/simple_auto_recovery.py b/examples/simple_auto_recovery.py index db75562..8e8b0d8 100644 --- a/examples/simple_auto_recovery.py +++ b/examples/simple_auto_recovery.py @@ -67,7 +67,7 @@ async def connect(self, device, status_callback=None): # Create and connect MQTT client await self._create_client() - logger.info(f"✅ Connected: {self.mqtt_client.client_id}") + logger.info(f"[SUCCESS] Connected: {self.mqtt_client.client_id}") async def _create_client(self): """Create MQTT client with recovery handler.""" @@ -147,7 +147,7 @@ async def _handle_reconnection_failed(self, attempts): # Reset recovery counter on success self.recovery_attempt = 0 - logger.info("✅ Recovery successful!") + logger.info("[SUCCESS] Recovery successful!") except Exception as e: logger.error(f"Recovery attempt failed: {e}") @@ -249,9 +249,9 @@ def on_status(status): try: asyncio.run(main()) except KeyboardInterrupt: - print("\n\n⚠️ Interrupted by user") + print("\n\n[WARNING] Interrupted by user") except Exception as e: - print(f"\n❌ Error: {e}") + print(f"\n[ERROR] Error: {e}") import traceback traceback.print_exc() diff --git a/examples/test_api_client.py b/examples/test_api_client.py index 6804fba..82f17bd 100755 --- a/examples/test_api_client.py +++ b/examples/test_api_client.py @@ -34,7 +34,9 @@ async def test_api_client(): password = os.getenv("NAVIEN_PASSWORD") if not email or not password: - print("❌ Please set NAVIEN_EMAIL and NAVIEN_PASSWORD environment variables") + print( + "[ERROR] Please set NAVIEN_EMAIL and NAVIEN_PASSWORD environment variables" + ) return 1 print("=" * 70) @@ -47,7 +49,7 @@ async def test_api_client(): # Test 1: Authentication print("Test 1: Authentication") print("-" * 70) - print(f"✅ Authenticated as: {email}") + print(f"[SUCCESS] Authenticated as: {email}") print() # Create API client with authenticated auth_client @@ -59,7 +61,7 @@ async def test_api_client(): print("Test 2: List Devices") print("-" * 70) devices = await client.list_devices() - print(f"✅ Found {len(devices)} device(s)") + print(f"[SUCCESS] Found {len(devices)} device(s)") # Helper to mask MAC addresses for safe printing def _mask_mac(mac: str) -> str: @@ -95,7 +97,9 @@ def mask_location(_, __): print() if not devices: - print("⚠️ No devices found. Cannot test device-specific endpoints.") + print( + "[WARNING] No devices found. Cannot test device-specific endpoints." + ) return 0 # Use first device for remaining tests @@ -108,7 +112,7 @@ def mask_location(_, __): print("-" * 70) device_info = await client.get_device_info(mac, additional) print( - f"✅ Retrieved detailed info for: {device_info.device_info.device_name}" + f"[SUCCESS] Retrieved detailed info for: {device_info.device_info.device_name}" ) print(f" MAC: {device_info.device_info.mac_address}") print(f" Type: {device_info.device_info.device_type}") @@ -123,14 +127,16 @@ def mask_location(_, __): print("-" * 70) try: firmware_list = await client.get_firmware_info(mac, additional) - print(f"✅ Retrieved firmware info: {len(firmware_list)} firmware(s)") + print( + f"[SUCCESS] Retrieved firmware info: {len(firmware_list)} firmware(s)" + ) for fw in firmware_list: print(f" Current SW Code: {fw.cur_sw_code}") print(f" Current Version: {fw.cur_version}") if fw.downloaded_version: print(f" Downloaded Version: {fw.downloaded_version}") except APIError as e: - print(f"⚠️ Firmware info not available: {e.message}") + print(f"[WARNING] Firmware info not available: {e.message}") print() # Test 5: Get TOU Info (if applicable) @@ -139,10 +145,10 @@ def mask_location(_, __): try: # Note: controller_id may need to be obtained from device data # This might fail if TOU is not configured - print("⚠️ TOU info requires controller_id - skipping for now") + print("[WARNING] TOU info requires controller_id - skipping for now") print(" (This endpoint requires device-specific configuration)") except Exception as e: - print(f"⚠️ TOU info error: {e}") + print(f"[WARNING] TOU info error: {e}") print() # Test 6: Convenience Method @@ -150,15 +156,17 @@ def mask_location(_, __): print("-" * 70) first_device = await client.get_first_device() if first_device: - print(f"✅ Get first device: {first_device.device_info.device_name}") + print( + f"[SUCCESS] Get first device: {first_device.device_info.device_name}" + ) else: - print("⚠️ No devices available") + print("[WARNING] No devices available") print() # Test 7: Data Model Verification print("Test 7: Data Model Verification") print("-" * 70) - print("✅ DeviceInfo model:") + print("[SUCCESS] DeviceInfo model:") print(f" - home_seq: {type(test_device.device_info.home_seq).__name__}") print( f" - mac_address: {type(test_device.device_info.mac_address).__name__}" @@ -168,7 +176,7 @@ def mask_location(_, __): ) print(f" - connected: {type(test_device.device_info.connected).__name__}") - print("✅ Location model:") + print("[SUCCESS] Location model:") print(f" - state: {type(test_device.location.state).__name__}") print(f" - city: {type(test_device.location.city).__name__}") if test_device.location.latitude: @@ -182,34 +190,34 @@ def mask_location(_, __): # Try to get info for non-existent device await client.get_device_info("invalid_mac", "invalid") except APIError as e: - print(f"✅ APIError caught correctly: {e.message[:50]}...") + print(f"[SUCCESS] APIError caught correctly: {e.message[:50]}...") except Exception as e: - print(f"⚠️ Unexpected error type: {type(e).__name__}") + print(f"[WARNING] Unexpected error type: {type(e).__name__}") print() print("=" * 70) - print("✅ All API client tests completed successfully!") + print("[SUCCESS] All API client tests completed successfully!") print("=" * 70) print() print("Summary:") - print(" ✅ Authentication working") - print(" ✅ Device listing working") - print(" ✅ Device info retrieval working") - print(" ✅ Data models parsing correctly") - print(" ✅ Error handling functional") + print(" [SUCCESS] Authentication working") + print(" [SUCCESS] Device listing working") + print(" [SUCCESS] Device info retrieval working") + print(" [SUCCESS] Data models parsing correctly") + print(" [SUCCESS] Error handling functional") print() return 0 except AuthenticationError as e: - print(f"❌ Authentication error: {e.message}") + print(f"[ERROR] Authentication error: {e.message}") return 1 except APIError as e: - print(f"❌ API error: {e.message}") + print(f"[ERROR] API error: {e.message}") if e.code: print(f" Code: {e.code}") return 1 except Exception as e: - print(f"❌ Unexpected error: {str(e)}") + print(f"[ERROR] Unexpected error: {str(e)}") import traceback traceback.print_exc() @@ -223,7 +231,9 @@ async def test_convenience_function(): password = os.getenv("NAVIEN_PASSWORD") if not email or not password: - print("❌ Please set NAVIEN_EMAIL and NAVIEN_PASSWORD environment variables") + print( + "[ERROR] Please set NAVIEN_EMAIL and NAVIEN_PASSWORD environment variables" + ) return 1 print() @@ -239,7 +249,7 @@ async def test_convenience_function(): async with NavienAuthClient(email, password) as auth_client: api_client = NavienAPIClient(auth_client=auth_client) devices = await api_client.list_devices() - print(f"✅ get_devices() returned {len(devices)} device(s)") + print(f"[SUCCESS] get_devices() returned {len(devices)} device(s)") for idx, _ in enumerate(devices, start=1): # Do not log sensitive data like device name or MAC address @@ -247,7 +257,7 @@ async def test_convenience_function(): return 0 except Exception as e: - print(f"❌ Error: {str(e)}") + print(f"[ERROR] Error: {str(e)}") return 1 diff --git a/examples/test_mqtt_connection.py b/examples/test_mqtt_connection.py index 7a93fa3..72dfe6a 100755 --- a/examples/test_mqtt_connection.py +++ b/examples/test_mqtt_connection.py @@ -34,7 +34,9 @@ async def test_mqtt_connection(): password = os.getenv("NAVIEN_PASSWORD") if not email or not password: - print("❌ Error: Set NAVIEN_EMAIL and NAVIEN_PASSWORD environment variables") + print( + "[ERROR] Error: Set NAVIEN_EMAIL and NAVIEN_PASSWORD environment variables" + ) return False print("Testing MQTT Connection to AWS IoT Core") @@ -44,53 +46,55 @@ async def test_mqtt_connection(): # Step 1: Authenticate print("\n1. Authenticating with Navien API...") async with NavienAuthClient(email, password) as auth_client: - print(f" ✅ Authenticated as: {auth_client.current_user.full_name}") + print( + f" [SUCCESS] Authenticated as: {auth_client.current_user.full_name}" + ) # Verify AWS credentials tokens = auth_client.current_tokens if not tokens.access_key_id or not tokens.secret_key: - print(" ❌ No AWS credentials in response") + print(" [ERROR] No AWS credentials in response") return False - print(f" ✅ AWS Access Key ID: {tokens.access_key_id[:15]}...") + print(f" [SUCCESS] AWS Access Key ID: {tokens.access_key_id[:15]}...") print( - f" ✅ AWS Session Token: {'Present' if tokens.session_token else 'None'}" + f" [SUCCESS] AWS Session Token: {'Present' if tokens.session_token else 'None'}" ) # Step 2: Create MQTT client print("\n2. Creating MQTT client...") mqtt_client = NavienMqttClient(auth_client) - print(f" ✅ Client ID: {mqtt_client.client_id}") + print(f" [SUCCESS] Client ID: {mqtt_client.client_id}") # Step 3: Connect print("\n3. Connecting to AWS IoT via WebSocket...") await mqtt_client.connect() - print(" ✅ Connected successfully!") - print(f" ✅ Is connected: {mqtt_client.is_connected}") + print(" [SUCCESS] Connected successfully!") + print(f" [SUCCESS] Is connected: {mqtt_client.is_connected}") # Step 4: Simple verification - wait a moment print("\n4. Verifying connection stability...") await asyncio.sleep(2) if mqtt_client.is_connected: - print(" ✅ Connection is stable") + print(" [SUCCESS] Connection is stable") else: - print(" ❌ Connection lost") + print(" [ERROR] Connection lost") return False # Step 5: Disconnect print("\n5. Disconnecting...") await mqtt_client.disconnect() - print(" ✅ Disconnected successfully") - print(f" ✅ Is connected: {mqtt_client.is_connected}") + print(" [SUCCESS] Disconnected successfully") + print(f" [SUCCESS] Is connected: {mqtt_client.is_connected}") print("\n" + "=" * 60) - print("✅ MQTT Connection Test PASSED") + print("[SUCCESS] MQTT Connection Test PASSED") print("=" * 60) return True except Exception as e: - print(f"\n❌ Test FAILED: {e}") + print(f"\n[ERROR] Test FAILED: {e}") import traceback traceback.print_exc() diff --git a/examples/test_mqtt_messaging.py b/examples/test_mqtt_messaging.py index 17bad08..bce82d9 100644 --- a/examples/test_mqtt_messaging.py +++ b/examples/test_mqtt_messaging.py @@ -36,7 +36,9 @@ async def test_mqtt_messaging(): password = os.getenv("NAVIEN_PASSWORD") if not email or not password: - print("❌ Error: Set NAVIEN_EMAIL and NAVIEN_PASSWORD environment variables") + print( + "[ERROR] Error: Set NAVIEN_EMAIL and NAVIEN_PASSWORD environment variables" + ) return False print("=" * 80) @@ -65,13 +67,13 @@ def message_handler(topic: str, message: dict): # Step 1: Authenticate print("Step 1: Authenticating...") async with NavienAuthClient(email, password) as auth_client: - print(f"✅ Authenticated as: {auth_client.current_user.full_name}") + print(f"[SUCCESS] Authenticated as: {auth_client.current_user.full_name}") if not auth_client.current_tokens.access_key_id: - print("❌ No AWS credentials available") + print("[ERROR] No AWS credentials available") return False - print("✅ AWS credentials obtained") + print("[SUCCESS] AWS credentials obtained") print() # Step 2: Get device info @@ -82,7 +84,7 @@ def message_handler(topic: str, message: dict): devices = await api_client.list_devices() if not devices: - print("❌ No devices found") + print("[ERROR] No devices found") return False device = devices[0] @@ -105,7 +107,7 @@ def mask_mac(addr: str) -> str: # Always redact to avoid leaking sensitive data return "[REDACTED_MAC]" - print(f"✅ Found device: {device.device_info.device_name}") + print(f"[SUCCESS] Found device: {device.device_info.device_name}") print(f" MAC Address: {mask_mac(device_id)}") print(f" Device Type: {mask_any(device_type)}") print(f" Additional Value: {additional_value}") @@ -117,7 +119,7 @@ def mask_mac(addr: str) -> str: mqtt_client = NavienMqttClient(auth_client) await mqtt_client.connect() - print("✅ Connected to AWS IoT") + print("[SUCCESS] Connected to AWS IoT") print(f" Client ID: {mqtt_client.client_id}") print(f" Session ID: {mqtt_client.session_id}") print() @@ -147,7 +149,9 @@ def mask_mac_in_topic(topic: str, mac_addr: str) -> str: for topic in topics: try: await mqtt_client.subscribe(topic, message_handler) - print(f" ✅ Subscribed to: {mask_mac_in_topic(topic, device_id)}") + print( + f" [SUCCESS] Subscribed to: {mask_mac_in_topic(topic, device_id)}" + ) except Exception: # Avoid printing exception contents which may contain sensitive identifiers try: @@ -159,7 +163,7 @@ def mask_any(_): return "[REDACTED]" print( - f" ⚠️ Failed to subscribe to topic. Device type: {mask_any(device_type)}" + f" [WARNING] Failed to subscribe to topic. Device type: {mask_any(device_type)}" ) logging.debug( "Subscribe failure for device_type=%s; topic name redacted for privacy", @@ -179,9 +183,9 @@ def mask_any(_): ) try: await mqtt_client.signal_app_connection(device) - print(" ✅ Sent") + print(" [SUCCESS] Sent") except Exception as e: - print(f" ❌ Error: {e}") + print(f" [ERROR] Error: {e}") await asyncio.sleep(3) # Command 2: Request device info @@ -190,9 +194,9 @@ def mask_any(_): ) try: await mqtt_client.request_device_info(device) - print(" ✅ Sent") + print(" [SUCCESS] Sent") except Exception as e: - print(f" ❌ Error: {e}") + print(f" [ERROR] Error: {e}") await asyncio.sleep(5) # Command 3: Request device status @@ -201,9 +205,9 @@ def mask_any(_): ) try: await mqtt_client.request_device_status(device) - print(" ✅ Sent") + print(" [SUCCESS] Sent") except Exception as e: - print(f" ❌ Error: {e}") + print(f" [ERROR] Error: {e}") await asyncio.sleep(5) # Step 6: Wait for responses with status updates @@ -229,7 +233,7 @@ def mask_any(_): print() if messages_received: - print("✅ SUCCESS: Device responded to commands!") + print("[SUCCESS] SUCCESS: Device responded to commands!") print() print("Messages received:") for i, msg_data in enumerate(messages_received, 1): @@ -255,7 +259,7 @@ def mask_any(_): print(" Type: Other Response") print(f" Keys: {list(response.keys())}") else: - print("❌ FAILURE: No messages received from device") + print("[ERROR] FAILURE: No messages received from device") print() print("Possible causes:") print("1. Device is offline or not connected to network") @@ -274,12 +278,12 @@ def mask_any(_): # Step 8: Disconnect await mqtt_client.disconnect() - print("✅ Disconnected from AWS IoT") + print("[SUCCESS] Disconnected from AWS IoT") return len(messages_received) > 0 except Exception as e: - print(f"\n❌ Test failed with error: {e}") + print(f"\n[ERROR] Test failed with error: {e}") import traceback traceback.print_exc() diff --git a/examples/token_restoration_example.py b/examples/token_restoration_example.py index 6815858..67e21b2 100644 --- a/examples/token_restoration_example.py +++ b/examples/token_restoration_example.py @@ -50,7 +50,7 @@ async def save_tokens_example(): if not tokens: raise RuntimeError("Failed to obtain tokens") - logger.info("✓ Authentication successful") + logger.info("[OK] Authentication successful") logger.info(f"Token expires at: {tokens.expires_at}") # Serialize tokens to dictionary @@ -60,7 +60,7 @@ async def save_tokens_example(): with open(TOKEN_FILE, "w") as f: json.dump(token_data, f, indent=2) - logger.info(f"✓ Tokens saved to {TOKEN_FILE}") + logger.info(f"[OK] Tokens saved to {TOKEN_FILE}") logger.info("You can now use --restore to skip authentication on future runs") @@ -99,7 +99,7 @@ async def restore_tokens_example(): elif stored_tokens.are_aws_credentials_expired: logger.warning("⚠ AWS credentials expired, will re-authenticate...") else: - logger.info("✓ Stored tokens are still valid") + logger.info("[OK] Stored tokens are still valid") # Use stored tokens to initialize client async with NavienAuthClient( @@ -109,7 +109,7 @@ async def restore_tokens_example(): if not tokens: raise RuntimeError("Failed to restore authentication") - logger.info("✓ Successfully authenticated using stored tokens") + logger.info("[OK] Successfully authenticated using stored tokens") logger.info(f"Current token expires at: {tokens.expires_at}") # If tokens were refreshed, save them @@ -118,7 +118,7 @@ async def restore_tokens_example(): token_data = tokens.to_dict() with open(TOKEN_FILE, "w") as f: json.dump(token_data, f, indent=2) - logger.info(f"✓ Updated tokens saved to {TOKEN_FILE}") + logger.info(f"[OK] Updated tokens saved to {TOKEN_FILE}") async def main(): diff --git a/scripts/README.md b/scripts/README.md index d0253ee..484da3a 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -162,25 +162,25 @@ This project uses `setuptools_scm` which: ### What NOT to Do -❌ **Never edit the version in `setup.cfg`'s `[pyscaffold]` section!** +[ERROR] **Never edit the version in `setup.cfg`'s `[pyscaffold]` section!** - That field is the PyScaffold tool version (4.6), not the package version - Changing it to 4.7 was the bug that caused the version jump from 3.1.4 to 4.7 -❌ **Never add `__version__` to source code** +[ERROR] **Never add `__version__` to source code** - Version is derived from git tags, not hardcoded -❌ **Never create tags manually without validation** +[ERROR] **Never create tags manually without validation** - Use `make version-bump` which validates version progression ### What TO Do -✅ Use `make version-bump BUMP=` to create new versions +[SUCCESS] Use `make version-bump BUMP=` to create new versions -✅ Run `make validate-version` before releases +[SUCCESS] Run `make validate-version` before releases -✅ Let `setuptools_scm` derive versions from git tags +[SUCCESS] Let `setuptools_scm` derive versions from git tags -✅ Follow semantic versioning: +[SUCCESS] Follow semantic versioning: - **Patch** (X.Y.Z+1): Bug fixes, no API changes - **Minor** (X.Y+1.0): New features, backward compatible - **Major** (X+1.0.0): Breaking changes diff --git a/scripts/bump_version.py b/scripts/bump_version.py index eb356df..707e0f6 100755 --- a/scripts/bump_version.py +++ b/scripts/bump_version.py @@ -161,7 +161,7 @@ def create_tag(version: str, message: str = None) -> None: else: run_git_command(["tag", "-a", tag_name, "-m", f"Release version {version}"]) - print(f"✓ Created tag: {tag_name}") + print(f"[OK] Created tag: {tag_name}") def main() -> None: @@ -216,7 +216,7 @@ def main() -> None: # Create the tag create_tag(new_version) - print("\n✓ Version bump complete!") + print("\n[OK] Version bump complete!") print("\nNext steps:") print(f" 1. Push the tag: git push origin v{new_version}") print(" 2. Build release: make build") diff --git a/scripts/format.py b/scripts/format.py index a19af91..75ebb13 100644 --- a/scripts/format.py +++ b/scripts/format.py @@ -16,19 +16,19 @@ def run_command(cmd, description): try: result = subprocess.run(cmd, check=True, capture_output=True, text=True) - print(f"✓ {description} - COMPLETED") + print(f"[OK] {description} - COMPLETED") if result.stdout.strip(): print(f"Output: {result.stdout}") return True except subprocess.CalledProcessError as e: - print(f"❌ {description} - FAILED") + print(f"[ERROR] {description} - FAILED") if e.stdout: print(f"STDOUT:\n{e.stdout}") if e.stderr: print(f"STDERR:\n{e.stderr}") return False except FileNotFoundError: - print(f"❌ {description} - FAILED (ruff not found)") + print(f"[ERROR] {description} - FAILED (ruff not found)") print("Install ruff with: python3 -m pip install ruff>=0.1.0") return False @@ -39,7 +39,7 @@ def main(): project_root = Path(__file__).parent.parent os.chdir(project_root) - print("🚀 Running local formatting (mirroring tox format environment)") + print("[START] Running local formatting (mirroring tox format environment)") print(f"Working directory: {project_root}") # Define the same commands used in tox.ini format environment @@ -67,7 +67,7 @@ def main(): print("Your code is now formatted consistently with CI requirements.") return 0 else: - print("❌ Some formatting operations FAILED!") + print("[ERROR] Some formatting operations FAILED!") print("Check the output above for details.") return 1 diff --git a/scripts/lint.py b/scripts/lint.py index f2e8cb5..cbc3c03 100644 --- a/scripts/lint.py +++ b/scripts/lint.py @@ -16,19 +16,19 @@ def run_command(cmd, description): try: result = subprocess.run(cmd, check=True, capture_output=True, text=True) - print(f"✓ {description} - PASSED") + print(f"[OK] {description} - PASSED") if result.stdout.strip(): print(f"Output: {result.stdout}") return True except subprocess.CalledProcessError as e: - print(f"❌ {description} - FAILED") + print(f"[ERROR] {description} - FAILED") if e.stdout: print(f"STDOUT:\n{e.stdout}") if e.stderr: print(f"STDERR:\n{e.stderr}") return False except FileNotFoundError: - print(f"❌ {description} - FAILED (ruff not found)") + print(f"[ERROR] {description} - FAILED (ruff not found)") print("Install ruff with: python3 -m pip install ruff>=0.1.0") return False @@ -39,7 +39,7 @@ def main(): project_root = Path(__file__).parent.parent os.chdir(project_root) - print("🚀 Running local linting (mirroring CI environment)") + print("[START] Running local linting (mirroring CI environment)") print(f"Working directory: {project_root}") # Define the same commands used in tox.ini @@ -67,7 +67,7 @@ def main(): print("Your code matches the CI environment requirements.") return 0 else: - print("❌ Some linting checks FAILED!") + print("[ERROR] Some linting checks FAILED!") print("Run the following commands to fix issues:") print(" python3 -m ruff check --fix src/ tests/ examples/") print(" python3 -m ruff format src/ tests/ examples/") diff --git a/scripts/setup-dev.py b/scripts/setup-dev.py index c9b205f..8fe7952 100644 --- a/scripts/setup-dev.py +++ b/scripts/setup-dev.py @@ -16,19 +16,19 @@ def run_command(cmd, description): try: result = subprocess.run(cmd, check=True, capture_output=True, text=True) - print(f"✓ {description} - SUCCESS") + print(f"[OK] {description} - SUCCESS") if result.stdout.strip(): print(f"Output: {result.stdout}") return True except subprocess.CalledProcessError as e: - print(f"❌ {description} - FAILED") + print(f"[ERROR] {description} - FAILED") if e.stdout: print(f"STDOUT:\n{e.stdout}") if e.stderr: print(f"STDERR:\n{e.stderr}") return False except FileNotFoundError: - print(f"❌ {description} - FAILED (command not found)") + print(f"[ERROR] {description} - FAILED (command not found)") return False def main(): @@ -38,7 +38,7 @@ def main(): project_root = Path(__file__).parent.parent os.chdir(project_root) - print("🚀 Setting up development environment") + print("[START] Setting up development environment") print(f"Working directory: {project_root}") # Install ruff for linting (matches CI requirement) @@ -70,7 +70,7 @@ def main(): print(" python3 scripts/format.py") return 0 else: - print("❌ Development environment setup FAILED!") + print("[ERROR] Development environment setup FAILED!") print("Check the output above for details.") return 1 diff --git a/scripts/validate_version.py b/scripts/validate_version.py index 5f15449..2bc45da 100755 --- a/scripts/validate_version.py +++ b/scripts/validate_version.py @@ -18,97 +18,127 @@ def check_pyscaffold_version() -> bool: """Check that the PyScaffold version in setup.cfg hasn't been modified.""" setup_cfg = Path("setup.cfg") - + if not setup_cfg.exists(): print("Error: setup.cfg not found", file=sys.stderr) return False - + content = setup_cfg.read_text() - + # Look for the [pyscaffold] section pyscaffold_section = re.search( r"\[pyscaffold\].*?^version\s*=\s*(.+?)$", content, - re.MULTILINE | re.DOTALL + re.MULTILINE | re.DOTALL, ) - + if not pyscaffold_section: - print("Warning: [pyscaffold] version not found in setup.cfg", file=sys.stderr) + print( + "Warning: [pyscaffold] version not found in setup.cfg", + file=sys.stderr, + ) return True # Not a failure, just unexpected - + version = pyscaffold_section.group(1).strip() - + # PyScaffold version should be 4.6 (the version that created this project) if version != "4.6": - print(f"❌ ERROR: setup.cfg [pyscaffold] version has been modified to {version}", file=sys.stderr) + print( + f"❌ setup.cfg [pyscaffold] version has been modified to {version}", + file=sys.stderr, + ) print("", file=sys.stderr) - print("The [pyscaffold] version field should always be 4.6", file=sys.stderr) - print("This is the PyScaffold TOOL version, NOT the package version!", file=sys.stderr) + print( + "The [pyscaffold] version field should always be 4.6", + file=sys.stderr, + ) + print( + "This is the PyScaffold TOOL version, NOT the package version!", + file=sys.stderr, + ) print("", file=sys.stderr) - print("Package version is managed by setuptools_scm from git tags.", file=sys.stderr) - print("Use 'make version-bump BUMP=patch' to create new versions.", file=sys.stderr) + print( + "Package version is managed by setuptools_scm from git tags.", + file=sys.stderr, + ) + print( + "Use 'make version-bump BUMP=patch' to create new versions.", + file=sys.stderr, + ) print("", file=sys.stderr) - print(f"To fix: Change version back to 4.6 in setup.cfg [pyscaffold] section", file=sys.stderr) + print( + "To fix: Change version back to 4.6 in [pyscaffold] section", + file=sys.stderr, + ) return False - + return True def check_hardcoded_versions() -> bool: """Check for hardcoded version strings in source code.""" src_dir = Path("src/nwp500") - + if not src_dir.exists(): print("Error: src/nwp500 directory not found", file=sys.stderr) return False - + # Version patterns to look for (excluding valid patterns) version_pattern = re.compile( r'__version__\s*=\s*["\'](\d+\.\d+\.\d+)["\']|' r'version\s*=\s*["\'](\d+\.\d+\.\d+)["\']' ) - + found_issues = [] - + for py_file in src_dir.rglob("*.py"): # Skip __init__.py which might import version from setuptools_scm if py_file.name == "__init__.py": continue - + content = py_file.read_text() matches = version_pattern.finditer(content) - + for match in matches: found_issues.append((py_file, match.group(0))) - + if found_issues: - print("❌ ERROR: Hardcoded version strings found:", file=sys.stderr) + print("❌ Hardcoded version strings found:", file=sys.stderr) for file_path, version_string in found_issues: print(f" {file_path}: {version_string}", file=sys.stderr) print("", file=sys.stderr) - print("Version should be derived from git tags via setuptools_scm.", file=sys.stderr) - print("Remove hardcoded version strings from source code.", file=sys.stderr) + print( + "Version should be derived from git tags via setuptools_scm.", + file=sys.stderr, + ) + print( + "Remove hardcoded version strings from source code.", + file=sys.stderr, + ) return False - + return True def check_setup_py() -> bool: """Verify setup.py uses setuptools_scm correctly.""" setup_py = Path("setup.py") - + if not setup_py.exists(): print("Warning: setup.py not found", file=sys.stderr) return True - + content = setup_py.read_text() - + if "use_scm_version" not in content: - print("❌ ERROR: setup.py does not use setuptools_scm", file=sys.stderr) + print("❌ setup.py does not use setuptools_scm", file=sys.stderr) print("", file=sys.stderr) - print("setup.py should contain: setup(use_scm_version={...})", file=sys.stderr) + print( + "setup.py should contain: setup(use_scm_version={...})", + file=sys.stderr, + ) return False - + return True @@ -116,30 +146,30 @@ def main() -> int: """Main entry point.""" print("Running version validation checks...") print("") - + all_checks = [ ("PyScaffold version", check_pyscaffold_version), ("Hardcoded versions", check_hardcoded_versions), ("setup.py configuration", check_setup_py), ] - + results = [] for check_name, check_func in all_checks: print(f"Checking {check_name}...", end=" ") result = check_func() results.append(result) if result: - print("✓") + print("[OK]") else: print("✗") - + print("") - + if all(results): - print("✓ All version validation checks passed!") + print("[OK] All version validation checks passed!") return 0 else: - print("❌ Version validation failed!") + print("[ERROR] Version validation failed!") print("") print("Common fixes:") print(" - Revert setup.cfg [pyscaffold] version to 4.6") diff --git a/src/nwp500/auth.py b/src/nwp500/auth.py index f654f5d..a71f39b 100644 --- a/src/nwp500/auth.py +++ b/src/nwp500/auth.py @@ -19,6 +19,7 @@ import aiohttp +from . import __version__ from .config import API_BASE_URL, REFRESH_ENDPOINT, SIGN_IN_ENDPOINT from .exceptions import ( AuthenticationError, @@ -675,7 +676,7 @@ def get_auth_headers(self) -> dict[str, str]: This is different from standard Bearer token authentication. """ headers = { - "User-Agent": "NaviLink-Python/1.0.0", + "User-Agent": f"nwp500-python/{__version__}", "Content-Type": "application/json", } diff --git a/src/nwp500/mqtt_connection.py b/src/nwp500/mqtt_connection.py index ad1e515..bafd51f 100644 --- a/src/nwp500/mqtt_connection.py +++ b/src/nwp500/mqtt_connection.py @@ -374,6 +374,20 @@ async def publish( "in background" ) raise + except AwsCrtError as e: + # Handle connection destruction during publish + # This can happen when AWS IoT Core disconnects (e.g., 24-hour + # timeout) + error_name = getattr(e, "name", None) + if error_name == "AWS_ERROR_MQTT_CONNECTION_DESTROYED": + _logger.warning( + f"MQTT connection destroyed during publish to '{topic}'. " + "This can occur during AWS-initiated disconnections. " + "Reconnection will be attempted automatically." + ) + # Mark as disconnected so reconnection handler can take over + self._connected = False + raise _logger.debug(f"Published to '{topic}' with packet_id {packet_id}") return int(packet_id) diff --git a/src/nwp500/mqtt_periodic.py b/src/nwp500/mqtt_periodic.py index 7926048..87b6c11 100644 --- a/src/nwp500/mqtt_periodic.py +++ b/src/nwp500/mqtt_periodic.py @@ -187,13 +187,15 @@ async def periodic_request() -> None: ) break except (AwsCrtError, RuntimeError) as e: - # Handle clean session cancellation gracefully (expected - # during reconnection) - # Safely check exception name attribute + # Handle known MQTT errors gracefully + error_name = ( + getattr(e, "name", None) + if isinstance(e, AwsCrtError) + else None + ) + if ( - isinstance(e, AwsCrtError) - and hasattr(e, "name") - and e.name + error_name == "AWS_ERROR_MQTT_CANCELLED_FOR_CLEAN_SESSION" ): _logger.debug( @@ -203,6 +205,15 @@ async def periodic_request() -> None: request_type.value, redacted_device_id, ) + elif error_name == "AWS_ERROR_MQTT_CONNECTION_DESTROYED": + _logger.warning( + "MQTT connection destroyed during %s request " + "for %s. This can occur during AWS-initiated " + "disconnections (e.g., 24-hour timeout). " + "Reconnection will be attempted automatically.", + request_type.value, + redacted_device_id, + ) else: _logger.error( "Error in periodic %s request for %s: %s",