Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/nwp500/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"device_bool_from_python",
"tou_override_to_python",
"div_10",
"mul_10",
"enum_validator",
"str_enum_validator",
"half_celsius_to_preferred",
Expand Down Expand Up @@ -123,6 +124,29 @@ def div_10(value: Any) -> float:
return float(value)


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)


def enum_validator(enum_class: type[Any]) -> Callable[[Any], Any]:
"""Create a validator for converting int/value to Enum.

Expand Down
4 changes: 2 additions & 2 deletions src/nwp500/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
See docs/protocol/quick_reference.rst for comprehensive protocol details.
"""

from enum import Enum, IntEnum
from enum import IntEnum, StrEnum

# ============================================================================
# Status Value Enumerations
Expand Down Expand Up @@ -239,7 +239,7 @@ class VolumeCode(IntEnum):
VOLUME_80 = 3 # NWP500-80: 80-gallon (302.8 liters) tank capacity


class InstallType(str, Enum):
class InstallType(StrEnum):
"""Installation type classification.

Indicates whether the device is installed for residential or commercial use.
Expand Down
6 changes: 4 additions & 2 deletions src/nwp500/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
enum_validator,
flow_rate_to_preferred,
half_celsius_to_preferred,
mul_10,
raw_celsius_to_preferred,
tou_override_to_python,
volume_to_preferred,
Expand Down Expand Up @@ -68,6 +69,7 @@
DeviceBool = Annotated[bool, BeforeValidator(device_bool_to_python)]
CapabilityFlag = Annotated[bool, BeforeValidator(device_bool_to_python)]
Div10 = Annotated[float, BeforeValidator(div_10)]
TenWhToWh = Annotated[float, BeforeValidator(mul_10)]
HalfCelsiusToPreferred = Annotated[
float, WrapValidator(half_celsius_to_preferred)
]
Expand Down Expand Up @@ -416,14 +418,14 @@ class DeviceStatus(NavienBaseModel):
"False = device follows TOU schedule normally"
)
)
total_energy_capacity: float = Field(
total_energy_capacity: TenWhToWh = Field(
description="Total energy capacity of the tank in Watt-hours",
json_schema_extra={
"unit_of_measurement": "Wh",
"device_class": "energy",
},
)
available_energy_capacity: float = Field(
available_energy_capacity: TenWhToWh = Field(
description=(
"Available energy capacity - "
"remaining hot water energy available in Watt-hours"
Expand Down
73 changes: 73 additions & 0 deletions tests/test_model_converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
device_bool_to_python,
div_10,
enum_validator,
mul_10,
tou_override_to_python,
)
from nwp500.enums import DhwOperationSetting, OnOffFlag
Expand Down Expand Up @@ -242,6 +243,78 @@ def test_known_values(self, input_value, expected):
assert result == pytest.approx(expected, abs=0.001)


class TestMul10Converter:
"""Test mul_10 converter (multiply by 10).

Used for energy capacity fields where the device reports in 10Wh units,
but we want to store standard Wh.
Only multiplies numeric types (int, float), returns float(value) for others.
"""

def test_zero(self):
"""0 * 10 = 0.0."""
assert mul_10(0) == 0.0

def test_positive_value(self):
"""100 * 10 = 1000.0."""
assert mul_10(100) == 1000.0

def test_negative_value(self):
"""-50 * 10 = -500.0."""
assert mul_10(-50) == -500.0

def test_single_digit(self):
"""5 * 10 = 50.0."""
assert mul_10(5) == 50.0

def test_float_input(self):
"""50.5 * 10 = 505.0."""
assert mul_10(50.5) == 505.0

def test_string_numeric(self):
"""String '100' is converted to float without multiplication."""
# mul_10 converts non-numeric to float but doesn't multiply
result = mul_10("100")
assert result == pytest.approx(100.0)

def test_energy_capacity_example(self):
"""Test with realistic energy capacity values from issue #70."""
# Device reports 1404.0 (10Wh units), should convert to 14040.0 Wh
device_value = 1404.0
expected_wh = 14040.0
assert mul_10(device_value) == expected_wh

def test_large_value(self):
"""1000 * 10 = 10000.0."""
assert mul_10(1000) == 10000.0

def test_very_small_value(self):
"""0.1 * 10 = 1.0."""
assert mul_10(0.1) == 1.0

def test_negative_small_value(self):
"""-0.5 * 10 = -5.0."""
assert mul_10(-0.5) == -5.0

@pytest.mark.parametrize(
"input_value,expected",
[
(0, 0.0),
(10, 100.0),
(50, 500.0),
(100, 1000.0),
(1000, 10000.0),
(-100, -1000.0),
(1.5, 15.0),
(99.9, 999.0),
],
)
def test_known_values(self, input_value, expected):
"""Test known mul_10 conversions for numeric types."""
result = mul_10(input_value)
assert result == pytest.approx(expected, abs=0.001)


class TestEnumValidator:
"""Test enum_validator factory function.

Expand Down