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/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. 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.