"""Tests for battery voltage to percentage conversion.""" import pytest from meshmon.battery import VOLTAGE_TABLE, voltage_to_percentage class TestVoltageToPercentage: """Test battery voltage to percentage conversion.""" # ========================================================================== # Boundary conditions # ========================================================================== @pytest.mark.parametrize( "voltage,expected", [ (4.20, 100.0), # Exact maximum (4.21, 100.0), # Above maximum (clamped) (4.50, 100.0), # Well above maximum (5.00, 100.0), # Way above maximum (3.00, 0.0), # Exact minimum (2.99, 0.0), # Below minimum (clamped) (2.50, 0.0), # Well below minimum (0.00, 0.0), # Zero voltage (-1.0, 0.0), # Negative (impossible but should handle) ], ) def test_boundary_values(self, voltage: float, expected: float): """Test values at and beyond the voltage table boundaries.""" assert voltage_to_percentage(voltage) == expected # ========================================================================== # Table lookup exact values # ========================================================================== @pytest.mark.parametrize("voltage,expected", VOLTAGE_TABLE) def test_exact_table_values(self, voltage: float, expected: float): """Test that exact table values return correct percentages.""" result = voltage_to_percentage(voltage) assert result == expected, f"Expected {expected}% at {voltage}V, got {result}%" # ========================================================================== # Interpolation tests # ========================================================================== @pytest.mark.parametrize( "voltage,expected_range", [ (4.13, (90.0, 100.0)), # Between 4.20 and 4.06 (4.02, (80.0, 90.0)), # Between 4.06 and 3.98 (3.95, (70.0, 80.0)), # Between 3.98 and 3.92 (3.50, (0.0, 10.0)), # Between 3.45 and 3.68 (3.72, (10.0, 20.0)), # Between 3.68 and 3.74 ], ) def test_interpolation_ranges( self, voltage: float, expected_range: tuple[float, float] ): """Test that interpolated values fall within expected ranges.""" result = voltage_to_percentage(voltage) assert expected_range[0] <= result <= expected_range[1], ( f"At {voltage}V, expected {expected_range[0]}-{expected_range[1]}%, " f"got {result}%" ) def test_midpoint_interpolation(self): """Midpoint between two table entries should give midpoint percentage.""" # Use the first two table entries (4.20V=100%, 4.06V=90%) v_high, p_high = 4.20, 100 v_low, p_low = 4.06, 90 midpoint_v = (v_high + v_low) / 2 # 4.13V midpoint_p = (p_high + p_low) / 2 # 95% result = voltage_to_percentage(midpoint_v) # Allow small floating point tolerance assert abs(result - midpoint_p) < 0.01, ( f"Midpoint voltage {midpoint_v}V should give ~{midpoint_p}%, " f"got {result}%" ) def test_interpolation_is_linear(self): """Verify linear interpolation between adjacent table points.""" # Test linearity between 3.82V (50%) and 3.87V (60%) v1, p1 = 3.82, 50 v2, p2 = 3.87, 60 # Test at 25%, 50%, and 75% between the points for fraction in [0.25, 0.50, 0.75]: test_voltage = v1 + fraction * (v2 - v1) expected_pct = p1 + fraction * (p2 - p1) result = voltage_to_percentage(test_voltage) assert abs(result - expected_pct) < 0.01, ( f"At {test_voltage}V ({fraction*100}% between {v1}V and {v2}V), " f"expected {expected_pct}%, got {result}%" ) # ========================================================================== # Monotonicity test # ========================================================================== def test_percentage_is_monotonic(self): """Battery percentage should decrease monotonically as voltage drops.""" voltages = [v / 100 for v in range(420, 299, -1)] # 4.20 down to 3.00 percentages = [voltage_to_percentage(v) for v in voltages] for i in range(1, len(percentages)): assert percentages[i] <= percentages[i - 1], ( f"Monotonicity violation: at {voltages[i]}V got {percentages[i]}%, " f"but at {voltages[i-1]}V got {percentages[i-1]}%" ) # ========================================================================== # Type handling # ========================================================================== def test_integer_voltage_input(self): """Function should handle integer input.""" # Integer 4 should be treated as 4.0V result = voltage_to_percentage(4) assert isinstance(result, float) assert 80.0 <= result <= 100.0 # 4.0V is between 3.98V (80%) and 4.06V (90%) class TestVoltageTable: """Test the VOLTAGE_TABLE constant.""" def test_table_is_sorted_descending(self): """Voltage table should be sorted in descending order by voltage.""" voltages = [v for v, _ in VOLTAGE_TABLE] assert voltages == sorted(voltages, reverse=True), ( "VOLTAGE_TABLE should be sorted by voltage in descending order" ) def test_table_has_expected_endpoints(self): """Table should cover the full 18650 range.""" voltages = [v for v, _ in VOLTAGE_TABLE] percentages = [p for _, p in VOLTAGE_TABLE] assert voltages[0] == 4.20, "Table should start at 4.20V (100%)" assert voltages[-1] == 3.00, "Table should end at 3.00V (0%)" assert percentages[0] == 100, "First entry should be 100%" assert percentages[-1] == 0, "Last entry should be 0%" def test_table_has_reasonable_entries(self): """Table should have enough entries for smooth interpolation.""" assert len(VOLTAGE_TABLE) >= 10, "Table should have at least 10 entries" def test_percentages_are_descending(self): """Percentages should decrease as voltage decreases.""" percentages = [p for _, p in VOLTAGE_TABLE] assert percentages == sorted(percentages, reverse=True), ( "Percentages should be in descending order" )