Source code for nwp500.converters

"""Protocol-specific converters for Navien device communication.

This module handles conversion of device-specific data formats to Python types.
The Navien device uses non-standard representations for boolean and numeric
values.

See docs/protocol/quick_reference.rst for comprehensive protocol details.
"""

from __future__ import annotations

from collections.abc import Callable
from typing import Any

__all__ = [
    "device_bool_to_python",
    "device_bool_from_python",
    "tou_override_to_python",
    "div_10",
    "mul_10",
    "enum_validator",
    "str_enum_validator",
]


[docs] def device_bool_to_python(value: Any) -> bool: """Convert device boolean representation to Python bool. Device protocol uses: 1 = OFF/False, 2 = ON/True This design (using 1 and 2 instead of 0 and 1) is likely due to: - 0 being reserved for null/uninitialized state - 1 representing "off" in legacy firmware - 2 representing "on" state Args: value: Device value (typically 1 or 2). Returns: Python boolean (1→False, 2→True). Example: >>> device_bool_to_python(2) True >>> device_bool_to_python(1) False """ return bool(value == 2)
[docs] def device_bool_from_python(value: bool) -> int: """Convert Python bool to device boolean representation. Args: value: Python boolean. Returns: Device value (True→2, False→1). Example: >>> device_bool_from_python(True) 2 >>> device_bool_from_python(False) 1 """ return 2 if value else 1
[docs] def tou_override_to_python(value: Any) -> bool: """Convert TOU override status to Python bool. Device representation: 1 = Override Active, 2 = Override Inactive Args: value: Device TOU override status value. Returns: Python boolean. Example: >>> tou_override_to_python(1) True >>> tou_override_to_python(2) False """ return bool(value == 1)
[docs] def div_10(value: Any) -> float: """Divide numeric value by 10.0. Used for fields that need 0.1 precision conversion. Args: value: Numeric value to divide. Returns: Value divided by 10.0. Example: >>> div_10(150) 15.0 >>> div_10(25.5) 2.55 """ if isinstance(value, (int, float)): return float(value) / 10.0 return float(value) / 10.0
[docs] def mul_10(value: Any) -> float: """Multiply numeric value by 10.0. Used for energy capacity fields where the device reports in 10Wh units, but we want to store standard Wh. Args: value: Numeric value to multiply. Returns: Value multiplied by 10.0. Example: >>> mul_10(150) 1500.0 >>> mul_10(25.5) 255.0 """ if isinstance(value, (int, float)): return float(value) * 10.0 return float(value) * 10.0
[docs] def enum_validator(enum_class: type[Any]) -> Callable[[Any], Any]: """Create a validator for converting int/value to Enum. Args: enum_class: The Enum class to validate against. Returns: A validator function compatible with Pydantic BeforeValidator. Example: >>> from enum import Enum >>> class Color(Enum): ... RED = 1 ... BLUE = 2 >>> validator = enum_validator(Color) >>> validator(1) <Color.RED: 1> """ def validate(value: Any) -> Any: """Validate and convert value to enum.""" if isinstance(value, enum_class): return value if isinstance(value, int): return enum_class(value) return enum_class(int(value)) return validate
[docs] 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") <Status.ACTIVE: '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