From 4aad35b6deee7809238ca2fbb2d0d0d51c6c025d Mon Sep 17 00:00:00 2001 From: Emmanuel Levijarvi Date: Wed, 4 Feb 2026 10:10:37 -0800 Subject: [PATCH 1/2] Fix: Incorrect unit scaling for energy capacity fields (off by factor of 10) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add mul_10 helper function to converters.py for multiplying by 10.0 - Apply BeforeValidator to total_energy_capacity and available_energy_capacity fields - Device reports energy in 10Wh units, library now correctly converts to Wh - Add comprehensive tests for mul_10 converter - Addresses issue #70 where fully heated tank reported 1.4 kWh instead of 14 kWh Physics verification: 65-gallon tank heated from 53°F to 141°F requires ~14 kWh, not 1.4 kWh as previously calculated. Co-authored-by: eman <19387+eman@users.noreply.github.com> --- src/nwp500/converters.py | 24 +++++++++++ src/nwp500/models.py | 6 ++- tests/test_model_converters.py | 73 ++++++++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+), 2 deletions(-) diff --git a/src/nwp500/converters.py b/src/nwp500/converters.py index d2a88e8..8e53b16 100644 --- a/src/nwp500/converters.py +++ b/src/nwp500/converters.py @@ -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", @@ -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. diff --git a/src/nwp500/models.py b/src/nwp500/models.py index 2c480d8..804e91f 100644 --- a/src/nwp500/models.py +++ b/src/nwp500/models.py @@ -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, @@ -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) ] @@ -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" diff --git a/tests/test_model_converters.py b/tests/test_model_converters.py index 4eb5f5b..cfb940e 100644 --- a/tests/test_model_converters.py +++ b/tests/test_model_converters.py @@ -15,6 +15,7 @@ device_bool_to_python, div_10, enum_validator, + mul_10, tou_override_to_python, ) from nwp500.enums import DhwOperationSetting, OnOffFlag @@ -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. From bd0c9c6b4d95b470001fd17ec45acff04a7e6b12 Mon Sep 17 00:00:00 2001 From: Emmanuel Levijarvi Date: Wed, 4 Feb 2026 10:15:42 -0800 Subject: [PATCH 2/2] Fix linting: Replace str, Enum with StrEnum for InstallType Resolves ruff UP042 warning about class inheriting from both str and Enum. Updated to use StrEnum from enum module which is the modern approach. --- src/nwp500/enums.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/nwp500/enums.py b/src/nwp500/enums.py index b9c4429..ae72667 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 Enum, IntEnum +from enum import IntEnum, StrEnum # ============================================================================ # Status Value Enumerations @@ -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.