mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-02-18 23:33:58 +08:00
# Conflicts: # .github/workflows/selfdrive_tests.yaml # common/params.h # common/params_keys.h # common/params_pyx.pyx # docs/CARS.md # opendbc_repo # panda # selfdrive/car/tests/test_models.py # selfdrive/pandad/pandad.cc # selfdrive/pandad/pandad.h # selfdrive/selfdrived/selfdrived.py # selfdrive/ui/translations/main_ar.ts # selfdrive/ui/translations/main_de.ts # selfdrive/ui/translations/main_es.ts # selfdrive/ui/translations/main_fr.ts # selfdrive/ui/translations/main_ja.ts # selfdrive/ui/translations/main_ko.ts # selfdrive/ui/translations/main_pt-BR.ts # selfdrive/ui/translations/main_th.ts # selfdrive/ui/translations/main_tr.ts # selfdrive/ui/translations/main_zh-CHS.ts # selfdrive/ui/translations/main_zh-CHT.ts # system/athena/athenad.py # system/athena/manage_athenad.py # system/manager/manager.py # system/sentry.py # uv.lock Sync: `commaai/opendbc:master` into `sunnypilot/opendbc:master` Sync: `commaai/panda:master` into `sunnypilot/panda:master`
235 lines
9.4 KiB
Python
235 lines
9.4 KiB
Python
import pytest
|
||
|
||
from openpilot.common.params import Params
|
||
from openpilot.system.hardware.power_monitoring import PowerMonitoring, CAR_BATTERY_CAPACITY_uWh, \
|
||
CAR_CHARGING_RATE_W, VBATT_PAUSE_CHARGING, DELAY_SHUTDOWN_TIME_S, MAX_TIME_OFFROAD_S
|
||
|
||
# Create fake time
|
||
ssb = 0.
|
||
def mock_time_monotonic():
|
||
global ssb
|
||
ssb += 1.
|
||
return ssb
|
||
|
||
TEST_DURATION_S = 50
|
||
GOOD_VOLTAGE = 12 * 1e3
|
||
VOLTAGE_BELOW_PAUSE_CHARGING = (VBATT_PAUSE_CHARGING - 1) * 1e3
|
||
|
||
def pm_patch(mocker, name, value, constant=False):
|
||
if constant:
|
||
mocker.patch(f"openpilot.system.hardware.power_monitoring.{name}", value)
|
||
else:
|
||
mocker.patch(f"openpilot.system.hardware.power_monitoring.{name}", return_value=value)
|
||
|
||
|
||
@pytest.fixture(autouse=True)
|
||
def mock_time(mocker):
|
||
mocker.patch("time.monotonic", mock_time_monotonic)
|
||
|
||
|
||
class TestPowerMonitoring:
|
||
def setup_method(self):
|
||
self.params = Params()
|
||
|
||
# Test to see that it doesn't do anything when pandaState is None
|
||
def test_panda_state_present(self):
|
||
pm = PowerMonitoring()
|
||
for _ in range(10):
|
||
pm.calculate(None, None)
|
||
assert pm.get_power_used() == 0
|
||
assert pm.get_car_battery_capacity() == (CAR_BATTERY_CAPACITY_uWh / 10)
|
||
|
||
# Test to see that it doesn't integrate offroad when ignition is True
|
||
def test_offroad_ignition(self):
|
||
pm = PowerMonitoring()
|
||
for _ in range(10):
|
||
pm.calculate(GOOD_VOLTAGE, True)
|
||
assert pm.get_power_used() == 0
|
||
|
||
# Test to see that it integrates with discharging battery
|
||
def test_offroad_integration_discharging(self, mocker):
|
||
POWER_DRAW = 4
|
||
pm_patch(mocker, "HARDWARE.get_current_power_draw", POWER_DRAW)
|
||
pm = PowerMonitoring()
|
||
for _ in range(TEST_DURATION_S + 1):
|
||
pm.calculate(GOOD_VOLTAGE, False)
|
||
expected_power_usage = ((TEST_DURATION_S/3600) * POWER_DRAW * 1e6)
|
||
assert abs(pm.get_power_used() - expected_power_usage) < 10
|
||
|
||
# Test to check positive integration of car_battery_capacity
|
||
def test_car_battery_integration_onroad(self, mocker):
|
||
POWER_DRAW = 4
|
||
pm_patch(mocker, "HARDWARE.get_current_power_draw", POWER_DRAW)
|
||
pm = PowerMonitoring()
|
||
pm.car_battery_capacity_uWh = 0
|
||
for _ in range(TEST_DURATION_S + 1):
|
||
pm.calculate(GOOD_VOLTAGE, True)
|
||
expected_capacity = ((TEST_DURATION_S/3600) * CAR_CHARGING_RATE_W * 1e6)
|
||
assert abs(pm.get_car_battery_capacity() - expected_capacity) < 10
|
||
|
||
# Test to check positive integration upper limit
|
||
def test_car_battery_integration_upper_limit(self, mocker):
|
||
POWER_DRAW = 4
|
||
pm_patch(mocker, "HARDWARE.get_current_power_draw", POWER_DRAW)
|
||
pm = PowerMonitoring()
|
||
pm.car_battery_capacity_uWh = CAR_BATTERY_CAPACITY_uWh - 1000
|
||
for _ in range(TEST_DURATION_S + 1):
|
||
pm.calculate(GOOD_VOLTAGE, True)
|
||
estimated_capacity = CAR_BATTERY_CAPACITY_uWh + (CAR_CHARGING_RATE_W / 3600 * 1e6)
|
||
assert abs(pm.get_car_battery_capacity() - estimated_capacity) < 10
|
||
|
||
# Test to check negative integration of car_battery_capacity
|
||
def test_car_battery_integration_offroad(self, mocker):
|
||
POWER_DRAW = 4
|
||
pm_patch(mocker, "HARDWARE.get_current_power_draw", POWER_DRAW)
|
||
pm = PowerMonitoring()
|
||
pm.car_battery_capacity_uWh = CAR_BATTERY_CAPACITY_uWh
|
||
for _ in range(TEST_DURATION_S + 1):
|
||
pm.calculate(GOOD_VOLTAGE, False)
|
||
expected_capacity = CAR_BATTERY_CAPACITY_uWh - ((TEST_DURATION_S/3600) * POWER_DRAW * 1e6)
|
||
assert abs(pm.get_car_battery_capacity() - expected_capacity) < 10
|
||
|
||
# Test to check negative integration lower limit
|
||
def test_car_battery_integration_lower_limit(self, mocker):
|
||
POWER_DRAW = 4
|
||
pm_patch(mocker, "HARDWARE.get_current_power_draw", POWER_DRAW)
|
||
pm = PowerMonitoring()
|
||
pm.car_battery_capacity_uWh = 1000
|
||
for _ in range(TEST_DURATION_S + 1):
|
||
pm.calculate(GOOD_VOLTAGE, False)
|
||
estimated_capacity = 0 - ((1/3600) * POWER_DRAW * 1e6)
|
||
assert abs(pm.get_car_battery_capacity() - estimated_capacity) < 10
|
||
|
||
# Test to check policy of stopping charging after MAX_TIME_OFFROAD_S
|
||
def test_max_time_offroad(self, mocker):
|
||
MOCKED_MAX_OFFROAD_TIME = 3600
|
||
POWER_DRAW = 0 # To stop shutting down for other reasons
|
||
pm_patch(mocker, "MAX_TIME_OFFROAD_S", MOCKED_MAX_OFFROAD_TIME, constant=True)
|
||
pm_patch(mocker, "HARDWARE.get_current_power_draw", POWER_DRAW)
|
||
pm = PowerMonitoring()
|
||
pm.car_battery_capacity_uWh = CAR_BATTERY_CAPACITY_uWh
|
||
start_time = ssb
|
||
ignition = False
|
||
while ssb <= start_time + MOCKED_MAX_OFFROAD_TIME:
|
||
pm.calculate(GOOD_VOLTAGE, ignition)
|
||
if (ssb - start_time) % 1000 == 0 and ssb < start_time + MOCKED_MAX_OFFROAD_TIME:
|
||
assert not pm.should_shutdown(ignition, True, start_time, False)
|
||
assert pm.should_shutdown(ignition, True, start_time, False)
|
||
|
||
def test_car_voltage(self, mocker):
|
||
POWER_DRAW = 0 # To stop shutting down for other reasons
|
||
TEST_TIME = 350
|
||
VOLTAGE_SHUTDOWN_MIN_OFFROAD_TIME_S = 50
|
||
pm_patch(mocker, "VOLTAGE_SHUTDOWN_MIN_OFFROAD_TIME_S", VOLTAGE_SHUTDOWN_MIN_OFFROAD_TIME_S, constant=True)
|
||
pm_patch(mocker, "HARDWARE.get_current_power_draw", POWER_DRAW)
|
||
pm = PowerMonitoring()
|
||
pm.car_battery_capacity_uWh = CAR_BATTERY_CAPACITY_uWh
|
||
ignition = False
|
||
start_time = ssb
|
||
for i in range(TEST_TIME):
|
||
pm.calculate(VOLTAGE_BELOW_PAUSE_CHARGING, ignition)
|
||
if i % 10 == 0:
|
||
assert pm.should_shutdown(ignition, True, start_time, True) == \
|
||
(pm.car_voltage_mV < VBATT_PAUSE_CHARGING * 1e3 and \
|
||
(ssb - start_time) > VOLTAGE_SHUTDOWN_MIN_OFFROAD_TIME_S and \
|
||
(ssb - start_time) > DELAY_SHUTDOWN_TIME_S)
|
||
assert pm.should_shutdown(ignition, True, start_time, True)
|
||
|
||
# Test to check policy of not stopping charging when DisablePowerDown is set
|
||
def test_disable_power_down(self, mocker):
|
||
POWER_DRAW = 0 # To stop shutting down for other reasons
|
||
TEST_TIME = 100
|
||
self.params.put_bool("DisablePowerDown", True)
|
||
pm_patch(mocker, "HARDWARE.get_current_power_draw", POWER_DRAW)
|
||
pm = PowerMonitoring()
|
||
pm.car_battery_capacity_uWh = CAR_BATTERY_CAPACITY_uWh
|
||
ignition = False
|
||
for i in range(TEST_TIME):
|
||
pm.calculate(VOLTAGE_BELOW_PAUSE_CHARGING, ignition)
|
||
if i % 10 == 0:
|
||
assert not pm.should_shutdown(ignition, True, ssb, False)
|
||
assert not pm.should_shutdown(ignition, True, ssb, False)
|
||
|
||
# Test to check policy of not stopping charging when ignition
|
||
def test_ignition(self, mocker):
|
||
POWER_DRAW = 0 # To stop shutting down for other reasons
|
||
TEST_TIME = 100
|
||
pm_patch(mocker, "HARDWARE.get_current_power_draw", POWER_DRAW)
|
||
pm = PowerMonitoring()
|
||
pm.car_battery_capacity_uWh = CAR_BATTERY_CAPACITY_uWh
|
||
ignition = True
|
||
for i in range(TEST_TIME):
|
||
pm.calculate(VOLTAGE_BELOW_PAUSE_CHARGING, ignition)
|
||
if i % 10 == 0:
|
||
assert not pm.should_shutdown(ignition, True, ssb, False)
|
||
assert not pm.should_shutdown(ignition, True, ssb, False)
|
||
|
||
# Test to check policy of not stopping charging when harness is not connected
|
||
def test_harness_connection(self, mocker):
|
||
POWER_DRAW = 0 # To stop shutting down for other reasons
|
||
TEST_TIME = 100
|
||
pm_patch(mocker, "HARDWARE.get_current_power_draw", POWER_DRAW)
|
||
pm = PowerMonitoring()
|
||
pm.car_battery_capacity_uWh = CAR_BATTERY_CAPACITY_uWh
|
||
|
||
ignition = False
|
||
for i in range(TEST_TIME):
|
||
pm.calculate(VOLTAGE_BELOW_PAUSE_CHARGING, ignition)
|
||
if i % 10 == 0:
|
||
assert not pm.should_shutdown(ignition, False, ssb, False)
|
||
assert not pm.should_shutdown(ignition, False, ssb, False)
|
||
|
||
def test_delay_shutdown_time(self):
|
||
pm = PowerMonitoring()
|
||
pm.car_battery_capacity_uWh = 0
|
||
ignition = False
|
||
in_car = True
|
||
offroad_timestamp = ssb
|
||
started_seen = True
|
||
pm.calculate(VOLTAGE_BELOW_PAUSE_CHARGING, ignition)
|
||
|
||
while ssb < offroad_timestamp + DELAY_SHUTDOWN_TIME_S:
|
||
assert not pm.should_shutdown(ignition, in_car,
|
||
offroad_timestamp,
|
||
started_seen), \
|
||
f"Should not shutdown before {DELAY_SHUTDOWN_TIME_S} seconds offroad time"
|
||
assert pm.should_shutdown(ignition, in_car,
|
||
offroad_timestamp,
|
||
started_seen), \
|
||
f"Should shutdown after {DELAY_SHUTDOWN_TIME_S} seconds offroad time"
|
||
|
||
@pytest.mark.parametrize(
|
||
"max_time_offroad, offroad_time_min, expected_result",
|
||
[
|
||
# No max time set – fallback to default (30 hours)
|
||
(None, 0, False),
|
||
(None, MAX_TIME_OFFROAD_S + 1, True), # exceeds 30h (1800+ mins)
|
||
|
||
# Valid max time values (in minutes)
|
||
(60, 59, False), # under limit
|
||
(60, 120, True), # over limit
|
||
(10, 8, False), # under limit
|
||
(10, 11, True), # over limit
|
||
|
||
# Edge case: max time is zero → no limit enforced
|
||
(0, 0, False),
|
||
(0, 400, False),
|
||
|
||
# Invalid max time formats or negative values → fallback to 30 hours
|
||
(-100, 100, False), # should fallback to 30h
|
||
(-1, MAX_TIME_OFFROAD_S + 1, True), # should fallback to 30h, and exceed it
|
||
]
|
||
)
|
||
def test_max_time_offroad_exceeded(self, max_time_offroad, offroad_time_min, expected_result):
|
||
# Set the parameter if provided
|
||
if max_time_offroad is not None:
|
||
self.params.put("MaxTimeOffroad", max_time_offroad)
|
||
|
||
# Convert offroad time from minutes to seconds
|
||
offroad_time_s = offroad_time_min * 60
|
||
|
||
pm = PowerMonitoring()
|
||
result = pm.max_time_offroad_exceeded(offroad_time_s)
|
||
|
||
assert result == expected_result
|