wip
This commit is contained in:
0
selfdrive/thermald/__init__.py
Normal file
0
selfdrive/thermald/__init__.py
Normal file
@@ -1,6 +1,5 @@
|
||||
import time
|
||||
import threading
|
||||
from typing import Optional
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
@@ -38,12 +37,14 @@ class PowerMonitoring:
|
||||
self.car_battery_capacity_uWh = max((CAR_BATTERY_CAPACITY_uWh / 10), int(car_battery_capacity_uWh))
|
||||
|
||||
# FrogPilot variables
|
||||
device_shutdown_setting = self.params.get_int("DeviceShutdown")
|
||||
device_management = self.params.get_bool("DeviceManagement")
|
||||
device_shutdown_setting = self.params.get_int("DeviceShutdown") if device_management else 33
|
||||
# If the toggle is set for < 1 hour, configure by 15 minute increments
|
||||
self.device_shutdown_time = (device_shutdown_setting - 3) * 3600 if device_shutdown_setting >= 4 else device_shutdown_setting * (60 * 15)
|
||||
self.low_voltage_shutdown = self.params.get_float("LowVoltageShutdown") if device_management else VBATT_PAUSE_CHARGING
|
||||
|
||||
# Calculation tick
|
||||
def calculate(self, voltage: Optional[int], ignition: bool):
|
||||
def calculate(self, voltage: int | None, ignition: bool):
|
||||
try:
|
||||
now = time.monotonic()
|
||||
|
||||
@@ -113,14 +114,14 @@ class PowerMonitoring:
|
||||
return int(self.car_battery_capacity_uWh)
|
||||
|
||||
# See if we need to shutdown
|
||||
def should_shutdown(self, ignition: bool, in_car: bool, offroad_timestamp: Optional[float], started_seen: bool):
|
||||
def should_shutdown(self, ignition: bool, in_car: bool, offroad_timestamp: float | None, started_seen: bool):
|
||||
if offroad_timestamp is None:
|
||||
return False
|
||||
|
||||
now = time.monotonic()
|
||||
should_shutdown = False
|
||||
offroad_time = (now - offroad_timestamp)
|
||||
low_voltage_shutdown = (self.car_voltage_mV < (VBATT_PAUSE_CHARGING * 1e3) and
|
||||
low_voltage_shutdown = (self.car_voltage_mV < (self.low_voltage_shutdown * 1e3) and
|
||||
offroad_time > VOLTAGE_SHUTDOWN_MIN_OFFROAD_TIME_S)
|
||||
should_shutdown |= offroad_time > self.device_shutdown_time
|
||||
should_shutdown |= low_voltage_shutdown
|
||||
@@ -128,7 +129,7 @@ class PowerMonitoring:
|
||||
should_shutdown &= not ignition
|
||||
should_shutdown &= (not self.params.get_bool("DisablePowerDown"))
|
||||
should_shutdown &= in_car
|
||||
should_shutdown &= offroad_time > DELAY_SHUTDOWN_TIME_S if self.device_shutdown_time else True # If "Instant" is selected for the timer, shutdown immediately
|
||||
should_shutdown &= offroad_time > DELAY_SHUTDOWN_TIME_S
|
||||
should_shutdown |= self.params.get_bool("ForcePowerDown")
|
||||
should_shutdown &= started_seen or (now > MIN_ON_TIME_S)
|
||||
return should_shutdown
|
||||
|
||||
0
selfdrive/thermald/tests/__init__.py
Normal file
0
selfdrive/thermald/tests/__init__.py
Normal file
56
selfdrive/thermald/tests/test_fan_controller.py
Normal file
56
selfdrive/thermald/tests/test_fan_controller.py
Normal file
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env python3
|
||||
import unittest
|
||||
from unittest.mock import Mock, patch
|
||||
from parameterized import parameterized
|
||||
|
||||
from openpilot.selfdrive.thermald.fan_controller import TiciFanController
|
||||
|
||||
ALL_CONTROLLERS = [(TiciFanController,)]
|
||||
|
||||
def patched_controller(controller_class):
|
||||
with patch("os.system", new=Mock()):
|
||||
return controller_class()
|
||||
|
||||
class TestFanController(unittest.TestCase):
|
||||
def wind_up(self, controller, ignition=True):
|
||||
for _ in range(1000):
|
||||
controller.update(100, ignition)
|
||||
|
||||
def wind_down(self, controller, ignition=False):
|
||||
for _ in range(1000):
|
||||
controller.update(10, ignition)
|
||||
|
||||
@parameterized.expand(ALL_CONTROLLERS)
|
||||
def test_hot_onroad(self, controller_class):
|
||||
controller = patched_controller(controller_class)
|
||||
self.wind_up(controller)
|
||||
self.assertGreaterEqual(controller.update(100, True), 70)
|
||||
|
||||
@parameterized.expand(ALL_CONTROLLERS)
|
||||
def test_offroad_limits(self, controller_class):
|
||||
controller = patched_controller(controller_class)
|
||||
self.wind_up(controller)
|
||||
self.assertLessEqual(controller.update(100, False), 30)
|
||||
|
||||
@parameterized.expand(ALL_CONTROLLERS)
|
||||
def test_no_fan_wear(self, controller_class):
|
||||
controller = patched_controller(controller_class)
|
||||
self.wind_down(controller)
|
||||
self.assertEqual(controller.update(10, False), 0)
|
||||
|
||||
@parameterized.expand(ALL_CONTROLLERS)
|
||||
def test_limited(self, controller_class):
|
||||
controller = patched_controller(controller_class)
|
||||
self.wind_up(controller, True)
|
||||
self.assertEqual(controller.update(100, True), 100)
|
||||
|
||||
@parameterized.expand(ALL_CONTROLLERS)
|
||||
def test_windup_speed(self, controller_class):
|
||||
controller = patched_controller(controller_class)
|
||||
self.wind_down(controller, True)
|
||||
for _ in range(10):
|
||||
controller.update(90, True)
|
||||
self.assertGreaterEqual(controller.update(90, True), 60)
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
200
selfdrive/thermald/tests/test_power_monitoring.py
Normal file
200
selfdrive/thermald/tests/test_power_monitoring.py
Normal file
@@ -0,0 +1,200 @@
|
||||
#!/usr/bin/env python3
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.thermald.power_monitoring import PowerMonitoring, CAR_BATTERY_CAPACITY_uWh, \
|
||||
CAR_CHARGING_RATE_W, VBATT_PAUSE_CHARGING, DELAY_SHUTDOWN_TIME_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(name, value, constant=False):
|
||||
if constant:
|
||||
return patch(f"openpilot.selfdrive.thermald.power_monitoring.{name}", value)
|
||||
return patch(f"openpilot.selfdrive.thermald.power_monitoring.{name}", return_value=value)
|
||||
|
||||
|
||||
@patch("time.monotonic", new=mock_time_monotonic)
|
||||
class TestPowerMonitoring(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.params = Params()
|
||||
|
||||
# Test to see that it doesn't do anything when pandaState is None
|
||||
def test_pandaState_present(self):
|
||||
pm = PowerMonitoring()
|
||||
for _ in range(10):
|
||||
pm.calculate(None, None)
|
||||
self.assertEqual(pm.get_power_used(), 0)
|
||||
self.assertEqual(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)
|
||||
self.assertEqual(pm.get_power_used(), 0)
|
||||
|
||||
# Test to see that it integrates with discharging battery
|
||||
def test_offroad_integration_discharging(self):
|
||||
POWER_DRAW = 4
|
||||
with pm_patch("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)
|
||||
self.assertLess(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):
|
||||
POWER_DRAW = 4
|
||||
with pm_patch("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)
|
||||
self.assertLess(abs(pm.get_car_battery_capacity() - expected_capacity), 10)
|
||||
|
||||
# Test to check positive integration upper limit
|
||||
def test_car_battery_integration_upper_limit(self):
|
||||
POWER_DRAW = 4
|
||||
with pm_patch("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)
|
||||
self.assertLess(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):
|
||||
POWER_DRAW = 4
|
||||
with pm_patch("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)
|
||||
self.assertLess(abs(pm.get_car_battery_capacity() - expected_capacity), 10)
|
||||
|
||||
# Test to check negative integration lower limit
|
||||
def test_car_battery_integration_lower_limit(self):
|
||||
POWER_DRAW = 4
|
||||
with pm_patch("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)
|
||||
self.assertLess(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):
|
||||
MOCKED_MAX_OFFROAD_TIME = 3600
|
||||
POWER_DRAW = 0 # To stop shutting down for other reasons
|
||||
with pm_patch("MAX_TIME_OFFROAD_S", MOCKED_MAX_OFFROAD_TIME, constant=True), pm_patch("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:
|
||||
self.assertFalse(pm.should_shutdown(ignition, True, start_time, False))
|
||||
self.assertTrue(pm.should_shutdown(ignition, True, start_time, False))
|
||||
|
||||
def test_car_voltage(self):
|
||||
POWER_DRAW = 0 # To stop shutting down for other reasons
|
||||
TEST_TIME = 350
|
||||
VOLTAGE_SHUTDOWN_MIN_OFFROAD_TIME_S = 50
|
||||
with pm_patch("VOLTAGE_SHUTDOWN_MIN_OFFROAD_TIME_S", VOLTAGE_SHUTDOWN_MIN_OFFROAD_TIME_S, constant=True), \
|
||||
pm_patch("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:
|
||||
self.assertEqual(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))
|
||||
self.assertTrue(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):
|
||||
POWER_DRAW = 0 # To stop shutting down for other reasons
|
||||
TEST_TIME = 100
|
||||
self.params.put_bool("DisablePowerDown", True)
|
||||
with pm_patch("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:
|
||||
self.assertFalse(pm.should_shutdown(ignition, True, ssb, False))
|
||||
self.assertFalse(pm.should_shutdown(ignition, True, ssb, False))
|
||||
|
||||
# Test to check policy of not stopping charging when ignition
|
||||
def test_ignition(self):
|
||||
POWER_DRAW = 0 # To stop shutting down for other reasons
|
||||
TEST_TIME = 100
|
||||
with pm_patch("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:
|
||||
self.assertFalse(pm.should_shutdown(ignition, True, ssb, False))
|
||||
self.assertFalse(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):
|
||||
POWER_DRAW = 0 # To stop shutting down for other reasons
|
||||
TEST_TIME = 100
|
||||
with pm_patch("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:
|
||||
self.assertFalse(pm.should_shutdown(ignition, False, ssb, False))
|
||||
self.assertFalse(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:
|
||||
self.assertFalse(pm.should_shutdown(ignition, in_car,
|
||||
offroad_timestamp,
|
||||
started_seen),
|
||||
f"Should not shutdown before {DELAY_SHUTDOWN_TIME_S} seconds offroad time")
|
||||
self.assertTrue(pm.should_shutdown(ignition, in_car,
|
||||
offroad_timestamp,
|
||||
started_seen),
|
||||
f"Should shutdown after {DELAY_SHUTDOWN_TIME_S} seconds offroad time")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -6,7 +6,6 @@ import threading
|
||||
import time
|
||||
from collections import OrderedDict, namedtuple
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional, Tuple
|
||||
|
||||
import psutil
|
||||
|
||||
@@ -50,9 +49,9 @@ THERMAL_BANDS = OrderedDict({
|
||||
# Override to highest thermal band when offroad and above this temp
|
||||
OFFROAD_DANGER_TEMP = 75
|
||||
|
||||
prev_offroad_states: Dict[str, Tuple[bool, Optional[str]]] = {}
|
||||
prev_offroad_states: dict[str, tuple[bool, str | None]] = {}
|
||||
|
||||
tz_by_type: Optional[Dict[str, int]] = None
|
||||
tz_by_type: dict[str, int] | None = None
|
||||
def populate_tz_by_type():
|
||||
global tz_by_type
|
||||
tz_by_type = {}
|
||||
@@ -87,7 +86,7 @@ def read_thermal(thermal_config):
|
||||
return dat
|
||||
|
||||
|
||||
def set_offroad_alert_if_changed(offroad_alert: str, show_alert: bool, extra_text: Optional[str]=None):
|
||||
def set_offroad_alert_if_changed(offroad_alert: str, show_alert: bool, extra_text: str | None=None):
|
||||
if prev_offroad_states.get(offroad_alert, None) == (show_alert, extra_text):
|
||||
return
|
||||
prev_offroad_states[offroad_alert] = (show_alert, extra_text)
|
||||
@@ -169,16 +168,16 @@ def thermald_thread(end_event, hw_queue) -> None:
|
||||
|
||||
count = 0
|
||||
|
||||
onroad_conditions: Dict[str, bool] = {
|
||||
onroad_conditions: dict[str, bool] = {
|
||||
"ignition": False,
|
||||
}
|
||||
startup_conditions: Dict[str, bool] = {}
|
||||
startup_conditions_prev: Dict[str, bool] = {}
|
||||
startup_conditions: dict[str, bool] = {}
|
||||
startup_conditions_prev: dict[str, bool] = {}
|
||||
|
||||
off_ts: Optional[float] = None
|
||||
started_ts: Optional[float] = None
|
||||
off_ts: float | None = None
|
||||
started_ts: float | None = None
|
||||
started_seen = False
|
||||
startup_blocked_ts: Optional[float] = None
|
||||
startup_blocked_ts: float | None = None
|
||||
thermal_status = ThermalStatus.yellow
|
||||
|
||||
last_hw_state = HardwareState(
|
||||
@@ -205,6 +204,10 @@ def thermald_thread(end_event, hw_queue) -> None:
|
||||
|
||||
fan_controller = None
|
||||
|
||||
# FrogPilot variables
|
||||
device_management = params.get_bool("DeviceManagement")
|
||||
offline_mode = device_management and params.get_bool("OfflineMode")
|
||||
|
||||
while not end_event.is_set():
|
||||
sm.update(PANDA_STATES_TIMEOUT)
|
||||
|
||||
@@ -217,6 +220,7 @@ def thermald_thread(end_event, hw_queue) -> None:
|
||||
peripheral_panda_present = peripheralState.pandaType != log.PandaState.PandaType.unknown
|
||||
|
||||
msg = read_thermal(thermal_config)
|
||||
msg.deviceState.deviceType = HARDWARE.get_device_type()
|
||||
|
||||
if sm.updated['pandaStates'] and len(pandaStates) > 0:
|
||||
|
||||
@@ -297,12 +301,9 @@ def thermald_thread(end_event, hw_queue) -> None:
|
||||
elif current_band.max_temp is not None and all_comp_temp > current_band.max_temp:
|
||||
thermal_status = list(THERMAL_BANDS.keys())[band_idx + 1]
|
||||
|
||||
if params.get_bool("FireTheBabysitter") and params.get_bool("MuteOverheated"):
|
||||
thermal_status = ThermalStatus.green
|
||||
|
||||
# **** starting logic ****
|
||||
|
||||
startup_conditions["up_to_date"] = (params.get("OfflineMode") and params.get("FireTheBabysitter")) or params.get("Offroad_ConnectivityNeeded") is None or params.get_bool("DisableUpdates") or params.get_bool("SnoozeUpdate")
|
||||
startup_conditions["up_to_date"] = params.get("Offroad_ConnectivityNeeded") is None or params.get_bool("DisableUpdates") or params.get_bool("SnoozeUpdate") or offline_mode
|
||||
startup_conditions["not_uninstalling"] = not params.get_bool("DoUninstall")
|
||||
startup_conditions["accepted_terms"] = params.get("HasAcceptedTerms") == terms_version
|
||||
|
||||
|
||||
Reference in New Issue
Block a user