diff --git a/README.rst b/README.rst index f672d88..6cb5dc7 100644 --- a/README.rst +++ b/README.rst @@ -7,11 +7,15 @@ Python library for Navien NWP500 Heat Pump Water Heater A Python library for monitoring and controlling the Navien NWP500 Heat Pump Water Heater through the Navilink cloud service. This library provides comprehensive access to device status, temperature control, operation mode management, and real-time monitoring capabilities. +**Documentation:** https://nwp500-python.readthedocs.io/ + +**Source Code:** https://github.com/eman/nwp500-python + Features ======== * **Device Monitoring**: Access real-time status information including temperatures, power consumption, and tank charge level -* **Temperature Control**: Set target water temperature (100-140°F) +* **Temperature Control**: Set target water temperature (90-151°F) * **Operation Mode Control**: Switch between Heat Pump, Energy Saver, High Demand, Electric, and Vacation modes * **Reservation Management**: Schedule automatic temperature and mode changes * **Time of Use (TOU)**: Configure energy pricing schedules for demand response @@ -183,7 +187,7 @@ Operation Modes - 5 - Suspends heating to save energy during extended absences. -**Important:** When you set a mode, you're configuring the ``dhwOperationSetting`` (what mode to use when heating). The device's current operational state is reported in ``operationMode`` (0=Standby, 32=Heat Pump active, 64=Energy Saver active, 96=High Demand active). See the `Device Status Fields documentation `_ for details on this distinction. +**Important:** When you set a mode, you're configuring the ``dhwOperationSetting`` (what mode to use when heating). The device's current operational state is reported in ``operationMode`` (0=Standby, 32=Heat Pump active, 64=Energy Saver active, 96=High Demand active). MQTT Protocol ============= @@ -210,25 +214,10 @@ The library supports low-level MQTT communication with Navien devices: * Reservation information * TOU settings -See the full `MQTT Protocol Documentation`_ for detailed message formats. - Documentation ============= -Comprehensive documentation is available in the ``docs/`` directory: - -* `Device Status Fields`_ - Complete field reference with units and conversions -* `Device Feature Fields`_ - Device capabilities and firmware information reference -* `MQTT Messages`_ - MQTT protocol documentation -* `MQTT Client`_ - MQTT client usage guide -* `Authentication`_ - Authentication module documentation - -.. _MQTT Protocol Documentation: docs/MQTT_MESSAGES.rst -.. _Device Status Fields: docs/DEVICE_STATUS_FIELDS.rst -.. _Device Feature Fields: docs/DEVICE_FEATURE_FIELDS.rst -.. _MQTT Messages: docs/MQTT_MESSAGES.rst -.. _MQTT Client: docs/MQTT_CLIENT.rst -.. _Authentication: docs/AUTHENTICATION.rst +For detailed information on device status fields, MQTT protocol, authentication, and more, see the complete documentation at https://nwp500-python.readthedocs.io/ Data Models =========== @@ -258,7 +247,7 @@ Requirements Development =========== -To set up a development environment, clone the repository and install the required dependencies: +To set up a development environment: .. code-block:: bash @@ -287,14 +276,10 @@ To ensure your local linting matches CI exactly: # Auto-fix and format tox -e format -For detailed linting setup instructions, see `LINTING_SETUP.md `_. - -For comprehensive development guide, see `DEVELOPMENT.md `_. - License ======= -This project is licensed under the MIT License - see the `LICENSE.txt `_ file for details. +This project is licensed under the MIT License. Author ====== diff --git a/docs/guides/reservations.rst b/docs/guides/reservations.rst index 9c133b5..a22f900 100644 --- a/docs/guides/reservations.rst +++ b/docs/guides/reservations.rst @@ -299,6 +299,7 @@ Request the current reservation schedule from the device: import asyncio from typing import Any + from nwp500 import decode_week_bitfield async def read_schedule(): async with NavienAuthClient(email, password) as auth: @@ -327,7 +328,7 @@ Request the current reservation schedule from the device: print(f"Number of entries: {len(entries)}") for idx, entry in enumerate(entries, 1): - days = NavienAPIClient.decode_week_bitfield( + days = decode_week_bitfield( entry.get("week", 0) ) hour = entry.get("hour", 0) @@ -633,7 +634,7 @@ Full working example with error handling and response monitoring: print(f"Active entries: {len(entries)}\n") for idx, entry in enumerate(entries, 1): - days = NavienAPIClient.decode_week_bitfield( + days = decode_week_bitfield( entry["week"] ) print(f"Entry {idx}: {entry['hour']:02d}:" diff --git a/docs/guides/time_of_use.rst b/docs/guides/time_of_use.rst index 0616975..e186df0 100644 --- a/docs/guides/time_of_use.rst +++ b/docs/guides/time_of_use.rst @@ -288,14 +288,13 @@ Building TOU Periods Helper Methods ~~~~~~~~~~~~~~ -The ``NavienAPIClient`` class provides helper methods for building TOU period configurations: +The library provides helper functions for building TOU period configurations: build_tou_period() """""""""""""""""" .. code-block:: python - @staticmethod def build_tou_period( season_months: Union[List[int], range], week_days: List[str], @@ -331,7 +330,6 @@ encode_price() .. code-block:: python - @staticmethod def encode_price(price: float, decimal_point: int = 5) -> int Encodes a floating-point price into an integer for transmission. @@ -340,8 +338,10 @@ Encodes a floating-point price into an integer for transmission. .. code-block:: python + from nwp500 import encode_price + # Encode $0.45000 per kWh - encoded = NavienAPIClient.encode_price(0.45, decimal_point=5) + encoded = encode_price(0.45, decimal_point=5) # Returns: 45000 decode_price() @@ -349,7 +349,6 @@ decode_price() .. code-block:: python - @staticmethod def decode_price(encoded_price: int, decimal_point: int = 5) -> float Decodes an integer price back to floating-point. @@ -358,8 +357,10 @@ Decodes an integer price back to floating-point. .. code-block:: python + from nwp500 import decode_price + # Decode price from device - price = NavienAPIClient.decode_price(45000, decimal_point=5) + price = decode_price(45000, decimal_point=5) # Returns: 0.45 encode_week_bitfield() @@ -367,7 +368,6 @@ encode_week_bitfield() .. code-block:: python - @staticmethod def encode_week_bitfield(day_names: List[str]) -> int Encodes a list of day names into a bitfield. @@ -386,8 +386,10 @@ Encodes a list of day names into a bitfield. .. code-block:: python + from nwp500 import encode_week_bitfield + # Weekdays only - bitfield = NavienAPIClient.encode_week_bitfield([ + bitfield = encode_week_bitfield([ "Monday", "Tuesday", "Wednesday", "Thursday", "Friday" ]) # Returns: 0b0111110 = 62 @@ -397,7 +399,6 @@ decode_week_bitfield() .. code-block:: python - @staticmethod def decode_week_bitfield(bitfield: int) -> List[str] Decodes a bitfield back into a list of day names. @@ -406,8 +407,10 @@ Decodes a bitfield back into a list of day names. .. code-block:: python + from nwp500 import decode_week_bitfield + # Decode weekday bitfield - days = NavienAPIClient.decode_week_bitfield(62) + days = decode_week_bitfield(62) # Returns: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"] Usage Examples @@ -421,7 +424,7 @@ Configure two rate periods - off-peak and peak pricing: .. code-block:: python import asyncio - from nwp500 import NavienAPIClient, NavienAuthClient, NavienMqttClient + from nwp500 import NavienAPIClient, NavienAuthClient, NavienMqttClient, build_tou_period async def configure_simple_tou(): async with NavienAuthClient("user@example.com", "password") as auth_client: @@ -446,7 +449,7 @@ Configure two rate periods - off-peak and peak pricing: controller_serial = feature.controllerSerialNumber # Define off-peak period (midnight to 2 PM, weekdays) - off_peak = NavienAPIClient.build_tou_period( + off_peak = build_tou_period( season_months=range(1, 13), # All months week_days=["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"], start_hour=0, @@ -459,7 +462,7 @@ Configure two rate periods - off-peak and peak pricing: ) # Define peak period (3 PM to 8 PM, weekdays) - peak = NavienAPIClient.build_tou_period( + peak = build_tou_period( season_months=range(1, 13), week_days=["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"], start_hour=15, @@ -502,7 +505,7 @@ Configure different rates for summer and winter: # ... get controller_serial (same as Example 1) ... # Summer off-peak (June-September, all day except 2-8 PM) - summer_off_peak = NavienAPIClient.build_tou_period( + summer_off_peak = build_tou_period( season_months=[6, 7, 8, 9], week_days=["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"], start_hour=0, @@ -515,7 +518,7 @@ Configure different rates for summer and winter: ) # Summer peak (June-September, 2-8 PM) - summer_peak = NavienAPIClient.build_tou_period( + summer_peak = build_tou_period( season_months=[6, 7, 8, 9], week_days=["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"], start_hour=14, @@ -528,7 +531,7 @@ Configure different rates for summer and winter: ) # Winter rates (October-May) - winter_off_peak = NavienAPIClient.build_tou_period( + winter_off_peak = build_tou_period( season_months=[10, 11, 12, 1, 2, 3, 4, 5], week_days=["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"], start_hour=0, @@ -540,7 +543,7 @@ Configure different rates for summer and winter: decimal_point=5 ) - winter_peak = NavienAPIClient.build_tou_period( + winter_peak = build_tou_period( season_months=[10, 11, 12, 1, 2, 3, 4, 5], week_days=["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"], start_hour=17, @@ -571,6 +574,8 @@ Query the device for its current TOU configuration: .. code-block:: python + from nwp500 import decode_week_bitfield, decode_price + async def check_tou_settings(): async with NavienAuthClient("user@example.com", "password") as auth_client: api_client = NavienAPIClient(auth_client=auth_client) @@ -593,12 +598,12 @@ Query the device for its current TOU configuration: print(f"Number of periods: {len(periods)}") for i, period in enumerate(periods, 1): - days = NavienAPIClient.decode_week_bitfield(period.get("week", 0)) - price_min = NavienAPIClient.decode_price( + days = decode_week_bitfield(period.get("week", 0)) + price_min = decode_price( period.get("priceMin", 0), period.get("decimalPoint", 0) ) - price_max = NavienAPIClient.decode_price( + price_max = decode_price( period.get("priceMax", 0), period.get("decimalPoint", 0) ) @@ -657,7 +662,7 @@ data from the OpenEI API and configuring it on your device: import asyncio import aiohttp - from nwp500 import NavienAPIClient, NavienAuthClient, NavienMqttClient + from nwp500 import NavienAPIClient, NavienAuthClient, NavienMqttClient, build_tou_period OPENEI_API_URL = "https://api.openei.org/utility_rates" OPENEI_API_KEY = "DEMO_KEY" # Get your own key at openei.org @@ -734,7 +739,7 @@ data from the OpenEI API and configuring it on your device: # Convert to TOU format weekdays = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"] return [ - NavienAPIClient.build_tou_period( + build_tou_period( season_months=range(1, 13), week_days=weekdays, start_hour=p["start_hour"], diff --git a/docs/python_api/constants.rst b/docs/python_api/constants.rst index a28fff2..5db3915 100644 --- a/docs/python_api/constants.rst +++ b/docs/python_api/constants.rst @@ -278,24 +278,6 @@ Checking Command Types if is_control_command(CommandCode.POWER_ON): print("This is a control command") -Backward Compatibility -====================== - -Legacy constant names are supported for backward compatibility: - -.. code-block:: python - - # Old names (still work) - CMD_STATUS_REQUEST = CommandCode.STATUS_REQUEST - CMD_DEVICE_INFO_REQUEST = CommandCode.DEVICE_INFO_REQUEST - CMD_POWER_ON = CommandCode.POWER_ON - CMD_POWER_OFF = CommandCode.POWER_OFF - # ... etc - - # Prefer new enum-based names - from nwp500.constants import CommandCode - CommandCode.STATUS_REQUEST - Firmware Version Constants =========================== diff --git a/examples/anti_legionella_example.py b/examples/anti_legionella_example.py index 66a1074..9bc5c37 100644 --- a/examples/anti_legionella_example.py +++ b/examples/anti_legionella_example.py @@ -17,11 +17,7 @@ from typing import Any from nwp500 import NavienAPIClient, NavienAuthClient, NavienMqttClient -from nwp500.constants import ( - CMD_ANTI_LEGIONELLA_DISABLE, - CMD_ANTI_LEGIONELLA_ENABLE, - CMD_STATUS_REQUEST, -) +from nwp500.constants import CommandCode def display_anti_legionella_status(status: dict[str, Any], label: str = "") -> None: @@ -119,7 +115,7 @@ def on_status(topic: str, message: dict[str, Any]) -> None: print("STEP 1: Getting initial Anti-Legionella status...") print("=" * 70) status_received.clear() - expected_command = CMD_STATUS_REQUEST + expected_command = CommandCode.STATUS_REQUEST await mqtt_client.request_device_status(device) try: @@ -137,7 +133,7 @@ def on_status(topic: str, message: dict[str, Any]) -> None: print("STEP 2: Enabling Anti-Legionella cycle every 7 days...") print("=" * 70) status_received.clear() - expected_command = CMD_ANTI_LEGIONELLA_ENABLE + expected_command = CommandCode.ANTI_LEGIONELLA_ENABLE await mqtt_client.enable_anti_legionella(device, period_days=7) try: @@ -155,7 +151,7 @@ def on_status(topic: str, message: dict[str, Any]) -> None: print("WARNING: This reduces protection against Legionella bacteria!") print("=" * 70) status_received.clear() - expected_command = CMD_ANTI_LEGIONELLA_DISABLE + expected_command = CommandCode.ANTI_LEGIONELLA_DISABLE await mqtt_client.disable_anti_legionella(device) try: @@ -172,7 +168,7 @@ def on_status(topic: str, message: dict[str, Any]) -> None: print("STEP 4: Re-enabling Anti-Legionella with 14-day cycle...") print("=" * 70) status_received.clear() - expected_command = CMD_ANTI_LEGIONELLA_ENABLE + expected_command = CommandCode.ANTI_LEGIONELLA_ENABLE await mqtt_client.enable_anti_legionella(device, period_days=14) try: diff --git a/examples/exception_handling_example.py b/examples/exception_handling_example.py index 66ba01a..b1abf90 100755 --- a/examples/exception_handling_example.py +++ b/examples/exception_handling_example.py @@ -156,9 +156,7 @@ async def example_validation_errors(): # Try to set invalid vacation days try: - await mqtt.set_dhw_operation_setting( - device, mode_id=5, vacation_days=50 - ) + await mqtt.set_dhw_mode(device, mode_id=5, vacation_days=50) except RangeValidationError as e: print(f"✓ Caught RangeValidationError: {e}") print(f" Field: {e.field}") diff --git a/examples/tou_openei_example.py b/examples/tou_openei_example.py index 7d993dd..e8492b9 100755 --- a/examples/tou_openei_example.py +++ b/examples/tou_openei_example.py @@ -16,7 +16,14 @@ import aiohttp -from nwp500 import NavienAPIClient, NavienAuthClient, NavienMqttClient +from nwp500 import ( + NavienAPIClient, + NavienAuthClient, + NavienMqttClient, + build_tou_period, + decode_price, + decode_week_bitfield, +) # OpenEI API configuration OPENEI_API_URL = "https://api.openei.org/utility_rates" @@ -179,7 +186,7 @@ def convert_openei_to_tou_periods( ] for period in periods: - tou_period = NavienAPIClient.build_tou_period( + tou_period = build_tou_period( season_months=range(1, 13), # All months week_days=weekdays, start_hour=period["start_hour"], @@ -254,8 +261,8 @@ async def main() -> None: print(f"\nConverted to {len(tou_periods)} TOU periods:") for i, period in enumerate(tou_periods, 1): # Decode for display - days = NavienAPIClient.decode_week_bitfield(period["week"]) - price = NavienAPIClient.decode_price(period["priceMin"], period["decimalPoint"]) + days = decode_week_bitfield(period["week"]) + price = decode_price(period["priceMin"], period["decimalPoint"]) print( f" {i}. {period['startHour']:02d}:{period['startMinute']:02d}" f"-{period['endHour']:02d}:{period['endMinute']:02d} " diff --git a/examples/tou_schedule_example.py b/examples/tou_schedule_example.py index a14a0bf..0d91fc5 100644 --- a/examples/tou_schedule_example.py +++ b/examples/tou_schedule_example.py @@ -6,7 +6,7 @@ import sys from typing import Any -from nwp500 import NavienAPIClient, NavienAuthClient, NavienMqttClient +from nwp500 import NavienAPIClient, NavienAuthClient, NavienMqttClient, build_tou_period async def _wait_for_controller_serial(mqtt_client: NavienMqttClient, device) -> str: @@ -57,7 +57,7 @@ async def main() -> None: print("Controller serial number acquired.") # Build two TOU periods as documented in MQTT_MESSAGES.rst - off_peak = NavienAPIClient.build_tou_period( + off_peak = build_tou_period( season_months=range(1, 13), week_days=["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"], start_hour=0, @@ -68,7 +68,7 @@ async def main() -> None: price_max=0.34831, decimal_point=5, ) - peak = NavienAPIClient.build_tou_period( + peak = build_tou_period( season_months=range(1, 13), week_days=["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"], start_hour=15, diff --git a/setup.cfg b/setup.cfg index 1c97adc..394a58b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -87,7 +87,7 @@ dev = # script_name = nwp500.module:function # For example: console_scripts = - nwp-cli = nwp500.cli:run + nwp-cli = nwp500.cli.__main__:run # And any other entry points, for example: # pyscaffold.cli = # awesome = pyscaffoldext.awesome.extension:AwesomeExtension diff --git a/src/nwp500/cli.py b/src/nwp500/cli.py deleted file mode 100644 index 0f5e64f..0000000 --- a/src/nwp500/cli.py +++ /dev/null @@ -1,77 +0,0 @@ -"""Navien Water Heater Control Script - Backward Compatibility Wrapper. - -This module maintains backward compatibility by importing from the -new modular cli package structure. -""" - -# Main entry points -from nwp500.cli.__main__ import ( - async_main, - get_authenticated_client, - main, - parse_args, - run, - setup_logging, -) - -# Command handlers -from nwp500.cli.commands import ( - handle_device_feature_request, - handle_device_info_request, - handle_get_energy_request, - handle_get_reservations_request, - handle_get_tou_request, - handle_power_request, - handle_set_dhw_temp_request, - handle_set_mode_request, - handle_set_tou_enabled_request, - handle_status_raw_request, - handle_status_request, - handle_update_reservations_request, -) - -# Monitoring -from nwp500.cli.monitoring import handle_monitoring - -# Output formatters -from nwp500.cli.output_formatters import ( - _json_default_serializer, - format_json_output, - print_json, - write_status_to_csv, -) - -# Token storage -from nwp500.cli.token_storage import TOKEN_FILE, load_tokens, save_tokens - -__all__ = [ - "async_main", - "get_authenticated_client", - "handle_device_feature_request", - "handle_device_info_request", - "handle_get_energy_request", - "handle_get_reservations_request", - "handle_get_tou_request", - "handle_monitoring", - "handle_power_request", - "handle_set_dhw_temp_request", - "handle_set_mode_request", - "handle_set_tou_enabled_request", - "handle_status_raw_request", - "handle_status_request", - "handle_update_reservations_request", - "_json_default_serializer", - "format_json_output", - "main", - "parse_args", - "print_json", - "run", - "setup_logging", - "TOKEN_FILE", - "load_tokens", - "save_tokens", - "write_status_to_csv", -] - -if __name__ == "__main__": - run() diff --git a/src/nwp500/constants.py b/src/nwp500/constants.py index 686f853..f2497bd 100644 --- a/src/nwp500/constants.py +++ b/src/nwp500/constants.py @@ -55,22 +55,6 @@ class CommandCode(IntEnum): TOU_ENABLE = 33554476 # Enable TOU optimization -# Backward compatibility aliases -# These maintain compatibility with code using the old CMD_* naming convention -CMD_STATUS_REQUEST = CommandCode.STATUS_REQUEST -CMD_DEVICE_INFO_REQUEST = CommandCode.DEVICE_INFO_REQUEST -CMD_POWER_ON = CommandCode.POWER_ON -CMD_POWER_OFF = CommandCode.POWER_OFF -CMD_DHW_MODE = CommandCode.DHW_MODE -CMD_DHW_TEMPERATURE = CommandCode.DHW_TEMPERATURE -CMD_ENERGY_USAGE_QUERY = CommandCode.ENERGY_USAGE_QUERY -CMD_RESERVATION_MANAGEMENT = CommandCode.RESERVATION_MANAGEMENT -CMD_TOU_SETTINGS = CommandCode.TOU_SETTINGS -CMD_ANTI_LEGIONELLA_DISABLE = CommandCode.ANTI_LEGIONELLA_DISABLE -CMD_ANTI_LEGIONELLA_ENABLE = CommandCode.ANTI_LEGIONELLA_ENABLE -CMD_TOU_DISABLE = CommandCode.TOU_DISABLE -CMD_TOU_ENABLE = CommandCode.TOU_ENABLE - # Note for maintainers: # Command codes and expected payload fields are defined in # `docs/MQTT_MESSAGES.rst` under the "Control Messages" section and