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