diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 83095c6..2f4a81c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,33 @@ Changelog ========= +Version 7.2.1 (2025-12-25) +========================== + +Added +----- +- **CLI Command**: New ``device-info`` command to retrieve basic device information from REST API + + .. code-block:: bash + + # Get basic device info (DeviceInfo model) + python3 -m nwp500.cli device-info + python3 -m nwp500.cli device-info --raw + +- **InstallType Enum**: New ``InstallType`` enum for device installation classification + + - ``InstallType.RESIDENTIAL`` = "R" - Residential use + - ``InstallType.COMMERCIAL`` = "C" - Commercial use + - Used in ``DeviceInfo.install_type`` field with automatic validation + +- **String Enum Validator**: New ``str_enum_validator()`` converter for string-based enums + +Changed +------- +- **DeviceInfo Model**: ``install_type`` field now uses ``InstallType`` enum instead of plain string +- **CLI Documentation**: Clarified distinction between ``info`` (DeviceFeature via MQTT) and ``device-info`` (DeviceInfo via REST API) commands + + Version 7.2.0 (2025-12-23) ========================== diff --git a/README.rst b/README.rst index e297bfe..5aa732a 100644 --- a/README.rst +++ b/README.rst @@ -104,9 +104,12 @@ The library includes a command line interface for monitoring and controlling you # Get current device status python3 -m nwp500.cli status - # Get device information and firmware + # Get device information and firmware (via MQTT - DeviceFeature) python3 -m nwp500.cli info + # Get basic device info from REST API (DeviceInfo) + python3 -m nwp500.cli device-info + # Get controller serial number python3 -m nwp500.cli serial diff --git a/src/nwp500/__init__.py b/src/nwp500/__init__.py index 0397781..afa9eef 100644 --- a/src/nwp500/__init__.py +++ b/src/nwp500/__init__.py @@ -57,6 +57,7 @@ ErrorCode, FilterChange, HeatSource, + InstallType, OnOffFlag, Operation, RecirculationMode, @@ -161,6 +162,7 @@ "ErrorCode", "FilterChange", "HeatSource", + "InstallType", "OnOffFlag", "Operation", "RecirculationMode", diff --git a/src/nwp500/cli/__init__.py b/src/nwp500/cli/__init__.py index 0e8659e..72b3205 100644 --- a/src/nwp500/cli/__init__.py +++ b/src/nwp500/cli/__init__.py @@ -4,6 +4,7 @@ from .handlers import ( handle_device_info_request, handle_get_controller_serial_request, + handle_get_device_info_rest, handle_get_energy_request, handle_get_reservations_request, handle_get_tou_request, @@ -28,6 +29,7 @@ # Command handlers "handle_device_info_request", "handle_get_controller_serial_request", + "handle_get_device_info_rest", "handle_get_energy_request", "handle_get_reservations_request", "handle_get_tou_request", diff --git a/src/nwp500/cli/__main__.py b/src/nwp500/cli/__main__.py index 998755d..db3400a 100644 --- a/src/nwp500/cli/__main__.py +++ b/src/nwp500/cli/__main__.py @@ -149,6 +149,25 @@ async def info(mqtt: NavienMqttClient, device: Any, raw: bool) -> None: await handlers.handle_device_info_request(mqtt, device, raw) +@cli.command() # type: ignore[attr-defined] +@click.option("--raw", is_flag=True, help="Output raw JSON response") +@async_command +async def device_info( + mqtt: NavienMqttClient, + device: Any, + raw: bool, +) -> None: + """Show basic device info from REST API (DeviceInfo model).""" + ctx = click.get_current_context() + api = None + if ctx and hasattr(ctx, "obj") and ctx.obj is not None: + api = ctx.obj.get("api") + if api: + await handlers.handle_get_device_info_rest(api, device, raw) + else: + _logger.error("API client not available") + + @cli.command() # type: ignore[attr-defined] @click.option("--raw", is_flag=True, help="Output raw JSON response") @async_command diff --git a/src/nwp500/cli/handlers.py b/src/nwp500/cli/handlers.py index c37d61a..366b4c0 100644 --- a/src/nwp500/cli/handlers.py +++ b/src/nwp500/cli/handlers.py @@ -11,6 +11,7 @@ DeviceFeature, DeviceStatus, EnergyUsageResponse, + NavienAPIClient, NavienMqttClient, ) from nwp500.exceptions import ( @@ -347,6 +348,38 @@ def raw_callback(topic: str, message: dict[str, Any]) -> None: _logger.error("Timed out updating reservations.") +async def handle_get_device_info_rest( + api_client: NavienAPIClient, device: Device, raw: bool = False +) -> None: + """Get device info from REST API (minimal DeviceInfo fields).""" + try: + device_info_obj = await api_client.get_device_info( + mac_address=device.device_info.mac_address, + additional_value=device.device_info.additional_value, + ) + if raw: + print_json(device_info_obj.model_dump()) + else: + # Print simple formatted output + info = device_info_obj.device_info + + install_type_str = info.install_type if info.install_type else "N/A" + print("\n=== Device Info (REST API) ===\n") + print(f"Device Name: {info.device_name}") + mac_display = ( + redact_serial(info.mac_address) if info.mac_address else "N/A" + ) + print(f"MAC Address: {mac_display}") + print(f"Device Type: {info.device_type}") + print(f"Home Seq: {info.home_seq}") + print(f"Connected: {info.connected}") + print(f"Install Type: {install_type_str}") + print(f"Additional Value: {info.additional_value or 'N/A'}") + print() + except Exception as e: + _logger.error(f"Error fetching device info: {e}") + + async def handle_get_tou_request( mqtt: NavienMqttClient, device: Device, api_client: Any ) -> None: diff --git a/src/nwp500/converters.py b/src/nwp500/converters.py index 48f10c7..f094815 100644 --- a/src/nwp500/converters.py +++ b/src/nwp500/converters.py @@ -15,6 +15,8 @@ "device_bool_from_python", "tou_override_to_python", "div_10", + "enum_validator", + "str_enum_validator", ] @@ -131,3 +133,33 @@ def validate(value: Any) -> Any: return enum_class(int(value)) return validate + + +def str_enum_validator(enum_class: type[Any]) -> Callable[[Any], Any]: + """Create a validator for converting string to str-based Enum. + + Args: + enum_class: The str Enum class to validate against. + + Returns: + A validator function compatible with Pydantic BeforeValidator. + + Example: + >>> from enum import Enum + >>> class Status(str, Enum): + ... ACTIVE = "A" + ... INACTIVE = "I" + >>> validator = str_enum_validator(Status) + >>> validator("A") + + """ + + def validate(value: Any) -> Any: + """Validate and convert value to enum.""" + if isinstance(value, enum_class): + return value + if isinstance(value, str): + return enum_class(value) + return enum_class(str(value)) + + return validate diff --git a/src/nwp500/enums.py b/src/nwp500/enums.py index 512c899..b9c4429 100644 --- a/src/nwp500/enums.py +++ b/src/nwp500/enums.py @@ -7,7 +7,7 @@ See docs/protocol/quick_reference.rst for comprehensive protocol details. """ -from enum import IntEnum +from enum import Enum, IntEnum # ============================================================================ # Status Value Enumerations @@ -239,6 +239,17 @@ class VolumeCode(IntEnum): VOLUME_80 = 3 # NWP500-80: 80-gallon (302.8 liters) tank capacity +class InstallType(str, Enum): + """Installation type classification. + + Indicates whether the device is installed for residential or commercial use. + This affects warranty terms and service requirements. + """ + + RESIDENTIAL = "R" # Residential use + COMMERCIAL = "C" # Commercial use + + class UnitType(IntEnum): """Navien device/unit model types.""" @@ -434,6 +445,11 @@ class FirmwareType(IntEnum): VolumeCode.VOLUME_80: "80 gallons", } +INSTALL_TYPE_TEXT = { + InstallType.RESIDENTIAL: "Residential", + InstallType.COMMERCIAL: "Commercial", +} + # ============================================================================ # Error Code Enumerations