Initial commit with translated description

This commit is contained in:
2026-03-29 13:13:35 +08:00
commit 1d2061613e
32 changed files with 4399 additions and 0 deletions

10
tests/__init__.py Normal file
View File

@@ -0,0 +1,10 @@
"""Test package for my-tesla.
We explicitly disable bytecode writing so running tests doesn't create __pycache__
folders inside the repo (keeps the private repo clean + avoids accidental noisy
artifacts).
"""
import sys
sys.dont_write_bytecode = True

View File

@@ -0,0 +1,40 @@
import unittest
import sys
from pathlib import Path
# Allow importing scripts/tesla.py as a module
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "scripts"))
import tesla # noqa: E402
class ChargePortStatusTests(unittest.TestCase):
def test_charge_port_status_json_shape(self):
vehicle = {"display_name": "Test Car", "state": "online"}
data = {
"charge_state": {
"charge_port_door_open": True,
"charge_port_latch": "Engaged",
"conn_charge_cable": "SAE",
"charging_state": "Charging",
# extra fields should be ignored
"battery_level": 50,
},
# drive_state/location must not be pulled into the object
"drive_state": {"latitude": 1.23, "longitude": 4.56},
}
out = tesla._charge_port_status_json(vehicle, data)
self.assertEqual(out["display_name"], "Test Car")
self.assertEqual(out["state"], "online")
self.assertEqual(out["charge_port_door_open"], True)
self.assertEqual(out["charge_port_latch"], "Engaged")
self.assertEqual(out["conn_charge_cable"], "SAE")
self.assertEqual(out["charging_state"], "Charging")
self.assertNotIn("drive_state", out)
self.assertNotIn("latitude", out)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,44 @@
import unittest
from scripts.tesla import _charge_status_json
class TestChargeStatusJson(unittest.TestCase):
def test_charge_status_json_includes_useful_fields(self):
charge = {
'battery_level': 55,
'battery_range': 123.4,
'usable_battery_level': 52,
'charging_state': 'Charging',
'charge_limit_soc': 80,
'time_to_full_charge': 1.5,
'charge_rate': 22,
'charger_power': 7,
'charger_voltage': 240,
'charger_actual_current': 29,
'scheduled_charging_start_time': 60,
'scheduled_charging_mode': 'DepartBy',
'scheduled_charging_pending': True,
'charge_port_door_open': True,
'conn_charge_cable': 'SAEJ1772',
}
out = _charge_status_json(charge)
# Sanity checks: key presence and values pass through.
self.assertEqual(out.get('battery_level'), 55)
self.assertEqual(out.get('usable_battery_level'), 52)
self.assertEqual(out.get('charger_power'), 7)
self.assertEqual(out.get('charger_voltage'), 240)
self.assertEqual(out.get('charger_actual_current'), 29)
self.assertEqual(out.get('charge_port_door_open'), True)
self.assertEqual(out.get('conn_charge_cable'), 'SAEJ1772')
def test_charge_status_json_handles_none(self):
out = _charge_status_json(None)
self.assertIsInstance(out, dict)
self.assertIsNone(out.get('battery_level'))
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,99 @@
import unittest
from unittest import mock
# Import the tesla script as a module
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "scripts"))
import tesla # noqa: E402
class DummyVehicle:
def __init__(self, display_name="Test Car", state="online"):
self._display_name = display_name
self._state = state
self.command_calls = []
def __getitem__(self, k):
if k == "display_name":
return self._display_name
if k == "state":
return self._state
raise KeyError(k)
def get(self, k, default=None):
if k == "display_name":
return self._display_name
if k == "state":
return self._state
return default
def command(self, name, **kwargs):
self.command_calls.append((name, kwargs))
class CmdClimateDefrostTests(unittest.TestCase):
def test_climate_defrost_on_calls_endpoint(self):
vehicle = DummyVehicle()
args = mock.Mock()
args.car = None
args.action = "defrost"
args.value = "on"
args.no_wake = False
args.json = False
args.celsius = False
args.fahrenheit = False
with mock.patch.object(tesla, "get_tesla"), \
mock.patch.object(tesla, "get_vehicle", return_value=vehicle), \
mock.patch.object(tesla, "require_email", return_value="test@example.com"), \
mock.patch.object(tesla, "wake_vehicle") as wake:
tesla.cmd_climate(args)
wake.assert_called_once()
self.assertEqual(vehicle.command_calls, [("SET_PRECONDITIONING_MAX", {"on": True})])
def test_climate_defrost_off_calls_endpoint(self):
vehicle = DummyVehicle()
args = mock.Mock()
args.car = None
args.action = "defrost"
args.value = "off"
args.no_wake = False
args.json = False
args.celsius = False
args.fahrenheit = False
with mock.patch.object(tesla, "get_tesla"), \
mock.patch.object(tesla, "get_vehicle", return_value=vehicle), \
mock.patch.object(tesla, "require_email", return_value="test@example.com"), \
mock.patch.object(tesla, "wake_vehicle") as wake:
tesla.cmd_climate(args)
wake.assert_called_once()
self.assertEqual(vehicle.command_calls, [("SET_PRECONDITIONING_MAX", {"on": False})])
def test_climate_defrost_requires_value(self):
vehicle = DummyVehicle()
args = mock.Mock()
args.car = None
args.action = "defrost"
args.value = None
args.no_wake = False
args.json = False
args.celsius = False
args.fahrenheit = False
with mock.patch.object(tesla, "get_tesla"), \
mock.patch.object(tesla, "get_vehicle", return_value=vehicle), \
mock.patch.object(tesla, "require_email", return_value="test@example.com"), \
mock.patch.object(tesla, "wake_vehicle"):
with self.assertRaises(ValueError):
tesla.cmd_climate(args)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,99 @@
import io
import unittest
from contextlib import redirect_stdout
from unittest import mock
# Import the tesla script as a module
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "scripts"))
import tesla # noqa: E402
class DummyVehicle:
def __init__(self, display_name="Test Car", state="online", data=None):
self._display_name = display_name
self._state = state
self._data = data or {}
def __getitem__(self, k):
if k == "display_name":
return self._display_name
if k == "state":
return self._state
raise KeyError(k)
def get(self, k, default=None):
if k == "display_name":
return self._display_name
if k == "state":
return self._state
return default
def get_vehicle_data(self):
return self._data
class CmdClimateStatusTests(unittest.TestCase):
def test_climate_status_prints_readable_output(self):
data = {
"climate_state": {
"is_climate_on": False,
"inside_temp": 20,
"outside_temp": 10,
"driver_temp_setting": 21,
"passenger_temp_setting": 21,
}
}
vehicle = DummyVehicle(data=data)
args = mock.Mock()
args.car = None
args.action = "status"
args.no_wake = True
args.json = False
with mock.patch.object(tesla, "get_tesla"), \
mock.patch.object(tesla, "get_vehicle", return_value=vehicle), \
mock.patch.object(tesla, "require_email", return_value="test@example.com"), \
mock.patch.object(tesla, "_ensure_online_or_exit"):
buf = io.StringIO()
with redirect_stdout(buf):
tesla.cmd_climate(args)
out = buf.getvalue()
self.assertIn("🚗 Test Car", out)
self.assertIn("Climate: Off", out)
self.assertIn("Inside:", out)
self.assertIn("Outside:", out)
self.assertIn("Setpoint:", out)
def test_climate_status_json_is_json_only(self):
data = {"climate_state": {"is_climate_on": True, "inside_temp": 20}}
vehicle = DummyVehicle(data=data)
args = mock.Mock()
args.car = None
args.action = "status"
args.no_wake = False
args.json = True
with mock.patch.object(tesla, "get_tesla"), \
mock.patch.object(tesla, "get_vehicle", return_value=vehicle), \
mock.patch.object(tesla, "require_email", return_value="test@example.com"), \
mock.patch.object(tesla, "_ensure_online_or_exit"):
buf = io.StringIO()
with redirect_stdout(buf):
tesla.cmd_climate(args)
out = buf.getvalue().strip()
# Should be valid JSON (starts with '{') with no extra human text.
self.assertTrue(out.startswith("{"))
self.assertIn('"inside_temp_c"', out)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,41 @@
import tempfile
import unittest
from types import SimpleNamespace
from unittest import mock
from pathlib import Path
import scripts.tesla as tesla
class CmdMileageRecordTests(unittest.TestCase):
def test_record_skips_when_no_wake_and_asleep(self):
with tempfile.TemporaryDirectory() as td:
db = Path(td) / "mileage.sqlite"
class FakeVehicle(dict):
def get_vehicle_data(self):
raise AssertionError("should not be called when asleep + no-wake")
v = FakeVehicle(display_name="Car", id_s="1", state="asleep")
fake_tesla = SimpleNamespace(vehicle_list=lambda: [v])
args = SimpleNamespace(
action="record",
db=str(db),
no_wake=True,
auto_wake_after_hours=24.0,
json=True,
email=None,
car=None,
)
with mock.patch.object(tesla, "get_tesla", return_value=fake_tesla), \
mock.patch.object(tesla, "require_email", return_value="x@example.com"), \
mock.patch.object(tesla, "wake_vehicle", return_value=False):
# Should not raise
tesla.cmd_mileage(args)
if __name__ == "__main__":
unittest.main()

106
tests/test_cmd_seats.py Normal file
View File

@@ -0,0 +1,106 @@
import io
import unittest
from contextlib import redirect_stdout
from unittest import mock
# Import the tesla script as a module
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "scripts"))
import tesla # noqa: E402
class DummyVehicle:
def __init__(self, display_name="Test Car", state="online", vehicle_data=None):
self._display_name = display_name
self._state = state
self._vehicle_data = vehicle_data or {}
self.command_calls = []
def __getitem__(self, k):
if k == "display_name":
return self._display_name
if k == "state":
return self._state
raise KeyError(k)
def get(self, k, default=None):
if k == "display_name":
return self._display_name
if k == "state":
return self._state
return default
def get_vehicle_data(self):
return self._vehicle_data
def command(self, name, **kwargs):
self.command_calls.append((name, kwargs))
class SeatsTests(unittest.TestCase):
def test_parse_seat_heater_names(self):
self.assertEqual(tesla._parse_seat_heater("driver"), 0)
self.assertEqual(tesla._parse_seat_heater("passenger"), 1)
self.assertEqual(tesla._parse_seat_heater("rear-left"), 2)
self.assertEqual(tesla._parse_seat_heater("rear-center"), 3)
self.assertEqual(tesla._parse_seat_heater("rear-right"), 4)
self.assertEqual(tesla._parse_seat_heater("3rd-left"), 5)
self.assertEqual(tesla._parse_seat_heater("3rd-right"), 6)
self.assertEqual(tesla._parse_seat_heater("0"), 0)
with self.assertRaises(ValueError):
tesla._parse_seat_heater("nope")
def test_seats_status_json_is_json_only(self):
vehicle = DummyVehicle(vehicle_data={
"climate_state": {
"seat_heater_left": 3,
"seat_heater_right": 1,
}
})
args = mock.Mock()
args.car = None
args.action = "status"
args.no_wake = True
args.json = True
with mock.patch.object(tesla, "get_tesla"), \
mock.patch.object(tesla, "get_vehicle", return_value=vehicle), \
mock.patch.object(tesla, "require_email", return_value="test@example.com"), \
mock.patch.object(tesla, "_ensure_online_or_exit"):
buf = io.StringIO()
with redirect_stdout(buf):
tesla.cmd_seats(args)
out = buf.getvalue().strip()
self.assertTrue(out.startswith("{"))
self.assertIn('"seat_heater_left"', out)
self.assertIn('"seat_heater_right"', out)
def test_seats_set_calls_endpoint(self):
vehicle = DummyVehicle()
args = mock.Mock()
args.car = None
args.action = "set"
args.seat = "driver"
args.level = "2"
args.yes = True
with mock.patch.object(tesla, "get_tesla"), \
mock.patch.object(tesla, "get_vehicle", return_value=vehicle), \
mock.patch.object(tesla, "require_email", return_value="test@example.com"), \
mock.patch.object(tesla, "wake_vehicle") as wake:
tesla.cmd_seats(args)
wake.assert_called_once()
self.assertEqual(vehicle.command_calls, [("REMOTE_SEAT_HEATER_REQUEST", {"heater": 0, "level": 2})])
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,73 @@
import io
import unittest
from contextlib import redirect_stdout
from unittest import mock
# Import the tesla script as a module
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "scripts"))
import tesla # noqa: E402
class DummyVehicle:
def __init__(self, display_name="Test Car", state="online", data=None):
self._display_name = display_name
self._state = state
self._data = data or {}
def __getitem__(self, k):
if k == "display_name":
return self._display_name
if k == "state":
return self._state
raise KeyError(k)
def get(self, k, default=None):
if k == "display_name":
return self._display_name
if k == "state":
return self._state
return default
def get_vehicle_data(self):
return self._data
class CmdStatusSummaryTests(unittest.TestCase):
def test_status_summary_prints_summary_and_details(self):
data = {
"charge_state": {"battery_level": 55, "battery_range": 123.4, "charging_state": "Stopped"},
"climate_state": {"inside_temp": 20, "is_climate_on": False},
"vehicle_state": {"locked": True},
}
vehicle = DummyVehicle(data=data)
args = mock.Mock()
args.car = None
args.no_wake = False
args.summary = True
args.json = False
# Patch networky bits.
with mock.patch.object(tesla, "get_tesla"), \
mock.patch.object(tesla, "get_vehicle", return_value=vehicle), \
mock.patch.object(tesla, "require_email", return_value="test@example.com"), \
mock.patch.object(tesla, "_ensure_online_or_exit"):
buf = io.StringIO()
with redirect_stdout(buf):
tesla.cmd_status(args)
out = buf.getvalue()
# Summary line
self.assertIn("🚗 Test Car", out)
self.assertIn("🔋 55%", out)
# Detailed section should still appear
self.assertIn("Battery: 55% (123 mi)", out)
if __name__ == "__main__":
unittest.main()

126
tests/test_cmd_windows.py Normal file
View File

@@ -0,0 +1,126 @@
import unittest
from unittest import mock
# Import the tesla script as a module
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "scripts"))
import tesla # noqa: E402
class DummyVehicle:
def __init__(self, display_name="Test Car", state="online", vehicle_data=None):
self._display_name = display_name
self._state = state
self._vehicle_data = vehicle_data or {}
self.command_calls = []
def __getitem__(self, k):
if k == "display_name":
return self._display_name
if k == "state":
return self._state
raise KeyError(k)
def get(self, k, default=None):
if k == "display_name":
return self._display_name
if k == "state":
return self._state
return default
def get_vehicle_data(self):
return self._vehicle_data
def command(self, name, **kwargs):
self.command_calls.append((name, kwargs))
class CmdWindowsTests(unittest.TestCase):
def test_windows_status_outputs_open_states(self):
vehicle = DummyVehicle(vehicle_data={
'vehicle_state': {
'fd_window': 0,
'fp_window': 1,
'rd_window': 0,
'rp_window': None,
}
})
args = mock.Mock()
args.car = None
args.action = "status"
args.yes = False
args.no_wake = True
args.json = True
with mock.patch.object(tesla, "get_tesla"), \
mock.patch.object(tesla, "get_vehicle", return_value=vehicle), \
mock.patch.object(tesla, "require_email", return_value="test@example.com"), \
mock.patch.object(tesla, "wake_vehicle", return_value=True) as wake, \
mock.patch("builtins.print") as p:
tesla.cmd_windows(args)
# Should not require --yes for status
wake.assert_called_once_with(vehicle, allow_wake=False)
printed = "\n".join(str(call.args[0]) for call in p.call_args_list)
self.assertIn('"front_driver": "Closed"', printed)
self.assertIn('"front_passenger": "Open"', printed)
def test_windows_vent_calls_endpoint(self):
vehicle = DummyVehicle()
args = mock.Mock()
args.car = None
args.action = "vent"
args.yes = True
args.no_wake = False
args.json = False
with mock.patch.object(tesla, "get_tesla"), \
mock.patch.object(tesla, "get_vehicle", return_value=vehicle), \
mock.patch.object(tesla, "require_email", return_value="test@example.com"), \
mock.patch.object(tesla, "wake_vehicle") as wake:
tesla.cmd_windows(args)
wake.assert_called_once()
self.assertEqual(vehicle.command_calls, [("WINDOW_CONTROL", {"command": "vent", "lat": 0, "lon": 0})])
def test_windows_close_calls_endpoint(self):
vehicle = DummyVehicle()
args = mock.Mock()
args.car = None
args.action = "close"
args.yes = True
args.no_wake = False
args.json = False
with mock.patch.object(tesla, "get_tesla"), \
mock.patch.object(tesla, "get_vehicle", return_value=vehicle), \
mock.patch.object(tesla, "require_email", return_value="test@example.com"), \
mock.patch.object(tesla, "wake_vehicle") as wake:
tesla.cmd_windows(args)
wake.assert_called_once()
self.assertEqual(vehicle.command_calls, [("WINDOW_CONTROL", {"command": "close", "lat": 0, "lon": 0})])
def test_windows_unknown_action_raises(self):
vehicle = DummyVehicle()
args = mock.Mock()
args.car = None
args.action = "nope"
args.yes = True
args.no_wake = False
args.json = False
with mock.patch.object(tesla, "get_tesla"), \
mock.patch.object(tesla, "get_vehicle", return_value=vehicle), \
mock.patch.object(tesla, "require_email", return_value="test@example.com"), \
mock.patch.object(tesla, "wake_vehicle"):
with self.assertRaises(ValueError):
tesla.cmd_windows(args)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,32 @@
import os
import stat
import tempfile
import unittest
from pathlib import Path
import scripts.tesla as tesla
class DefaultsPermissionsTests(unittest.TestCase):
def test_save_defaults_sets_0600_permissions_best_effort(self):
# On Windows this may not apply; but this repo targets macOS/Linux.
if os.name != "posix":
self.skipTest("POSIX-only permissions")
with tempfile.TemporaryDirectory() as td:
p = Path(td) / "defaults.json"
# Patch the module-level DEFAULTS_FILE for this test.
old = tesla.DEFAULTS_FILE
try:
tesla.DEFAULTS_FILE = p
tesla.save_defaults({"default_car": "Test"})
mode = stat.S_IMODE(p.stat().st_mode)
self.assertEqual(mode, 0o600)
finally:
tesla.DEFAULTS_FILE = old
if __name__ == "__main__":
unittest.main()

119
tests/test_formatting.py Normal file
View File

@@ -0,0 +1,119 @@
import unittest
import sys
from pathlib import Path
# Allow importing scripts/tesla.py as a module
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "scripts"))
import tesla # noqa: E402
class FormattingTests(unittest.TestCase):
def test_c_to_f(self):
self.assertAlmostEqual(tesla._c_to_f(0), 32)
self.assertAlmostEqual(tesla._c_to_f(20), 68)
def test_fmt_temp_pair(self):
self.assertIsNone(tesla._fmt_temp_pair(None))
self.assertEqual(tesla._fmt_temp_pair(20), "20°C (68°F)")
def test_tire_pressure_formatting(self):
# 2.90 bar is ~42 psi (common on Model 3)
psi = tesla._bar_to_psi(2.90)
self.assertIsNotNone(psi)
self.assertTrue(40 <= psi <= 45)
self.assertEqual(tesla._fmt_tire_pressure(2.9), "2.90 bar (42 psi)")
self.assertIsNone(tesla._fmt_tire_pressure(None))
def test_short_status_contains_expected_bits(self):
vehicle = {"display_name": "Test Car"}
data = {
"charge_state": {
"battery_level": 55,
"battery_range": 123.4,
"charging_state": "Stopped",
},
"climate_state": {"inside_temp": 20, "is_climate_on": False},
"vehicle_state": {"locked": True},
}
out = tesla._short_status(vehicle, data)
self.assertIn("🚗 Test Car", out)
self.assertIn("Locked", out)
self.assertIn("55%", out)
self.assertIn("123 mi", out)
self.assertIn("⚡ Stopped", out)
self.assertIn("68°F", out)
self.assertIn("Off", out)
def test_fmt_minutes_hhmm(self):
self.assertEqual(tesla._fmt_minutes_hhmm(0), "00:00")
self.assertEqual(tesla._fmt_minutes_hhmm(60), "01:00")
self.assertEqual(tesla._fmt_minutes_hhmm(23 * 60 + 59), "23:59")
self.assertIsNone(tesla._fmt_minutes_hhmm(-1))
self.assertIsNone(tesla._fmt_minutes_hhmm("nope"))
def test_parse_hhmm(self):
self.assertEqual(tesla._parse_hhmm("00:00"), 0)
self.assertEqual(tesla._parse_hhmm("01:30"), 90)
self.assertEqual(tesla._parse_hhmm("23:59"), 23 * 60 + 59)
with self.assertRaises(ValueError):
tesla._parse_hhmm("24:00")
with self.assertRaises(ValueError):
tesla._parse_hhmm("12:60")
with self.assertRaises(ValueError):
tesla._parse_hhmm("1230")
def test_report_is_one_screen(self):
vehicle = {"display_name": "Test Car", "state": "online"}
data = {
"charge_state": {
"battery_level": 80,
"usable_battery_level": 78,
"battery_range": 250.2,
"charging_state": "Charging",
"charge_limit_soc": 90,
"time_to_full_charge": 1.5,
"charge_rate": 30,
"scheduled_charging_start_time": 120,
"scheduled_charging_pending": True,
},
"climate_state": {"inside_temp": 21, "outside_temp": 10, "is_climate_on": True},
"vehicle_state": {
"locked": False,
"sentry_mode": True,
"odometer": 12345.6,
"tpms_pressure_fl": 2.9,
"tpms_pressure_fr": 2.9,
"tpms_pressure_rl": 2.8,
"tpms_pressure_rr": 2.8,
},
}
out = tesla._report(vehicle, data)
# Basic structure
self.assertTrue(out.startswith("🚗 Test Car"))
self.assertIn("State: online", out)
self.assertIn("Locked: No", out)
self.assertIn("Sentry: On", out)
self.assertIn("Battery: 80% (250 mi)", out)
self.assertIn("Usable battery: 78%", out)
self.assertIn("Charging: Charging", out)
self.assertIn("Scheduled charging:", out)
self.assertIn("02:00", out)
self.assertIn("Inside:", out)
self.assertIn("Outside:", out)
self.assertIn("Tires (TPMS):", out)
self.assertIn("FL 2.90 bar (42 psi)", out)
self.assertIn("RL 2.80 bar (41 psi)", out)
self.assertIn("Odometer: 12346 mi", out)
def test_round_coord(self):
self.assertEqual(tesla._round_coord(37.123456, 2), 37.12)
self.assertEqual(tesla._round_coord("-122.123456", 2), -122.12)
self.assertIsNone(tesla._round_coord("nope", 2))
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,27 @@
import unittest
from scripts.tesla import _round_coord
class TestLocationRounding(unittest.TestCase):
def test_round_coord_basic(self):
self.assertEqual(_round_coord(37.123456, 2), 37.12)
self.assertEqual(_round_coord(-122.987654, 2), -122.99)
def test_round_coord_digits_range(self):
# Reject overly-precise / invalid digit counts.
self.assertIsNone(_round_coord(1.2345, -1))
self.assertIsNone(_round_coord(1.2345, 7))
# Allow the full supported range.
self.assertEqual(_round_coord(1.2345678, 0), 1.0)
self.assertEqual(_round_coord(1.2345678, 6), 1.234568)
def test_round_coord_invalid_inputs(self):
self.assertIsNone(_round_coord(None, 2))
self.assertIsNone(_round_coord("", 2))
self.assertIsNone(_round_coord(1.23, "nope"))
if __name__ == "__main__":
unittest.main()

46
tests/test_mileage_db.py Normal file
View File

@@ -0,0 +1,46 @@
import os
import tempfile
import unittest
from pathlib import Path
import scripts.tesla as tesla
class MileageDbTests(unittest.TestCase):
def test_init_and_insert(self):
with tempfile.TemporaryDirectory() as td:
db = Path(td) / "mileage.sqlite"
tesla.mileage_init_db(db)
conn = tesla._db_connect(db)
try:
tesla.mileage_insert_point(
conn,
ts_utc=1700000000,
vehicle_id="123",
vehicle_name="Car",
odometer_mi=100.0,
state="online",
source="test",
note=None,
)
conn.commit()
last = tesla.mileage_last_success_ts(conn, "123")
self.assertEqual(last, 1700000000)
finally:
conn.close()
def test_resolve_db_path_env(self):
with tempfile.TemporaryDirectory() as td:
p = str(Path(td) / "x.sqlite")
os.environ["MY_TESLA_MILEAGE_DB"] = p
try:
out = tesla.resolve_mileage_db_path(None)
self.assertEqual(str(out), p)
finally:
os.environ.pop("MY_TESLA_MILEAGE_DB", None)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,59 @@
import sqlite3
import unittest
from unittest.mock import patch
from scripts.tesla import mileage_fetch_points, resolve_since_ts
class TestMileageExportFilter(unittest.TestCase):
def test_resolve_since_ts_prefers_explicit_ts(self):
self.assertEqual(resolve_since_ts(since_ts=123, since_days=7), 123)
def test_resolve_since_ts_since_days(self):
with patch("scripts.tesla.time.time", return_value=1000):
# 1 day = 86400 seconds
self.assertEqual(resolve_since_ts(since_days=1), 1000 - 86400)
def test_mileage_fetch_points_filters(self):
conn = sqlite3.connect(":memory:")
try:
conn.execute(
"""
CREATE TABLE mileage_points (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ts_utc INTEGER NOT NULL,
vehicle_id TEXT,
vehicle_name TEXT,
odometer_mi REAL,
state TEXT,
source TEXT,
note TEXT
);
"""
)
# Insert three points
conn.execute(
"INSERT INTO mileage_points(ts_utc, vehicle_id, vehicle_name, odometer_mi, state, source, note) VALUES (?,?,?,?,?,?,?)",
(10, "v1", "Car", 1.0, "online", "test", None),
)
conn.execute(
"INSERT INTO mileage_points(ts_utc, vehicle_id, vehicle_name, odometer_mi, state, source, note) VALUES (?,?,?,?,?,?,?)",
(20, "v1", "Car", 2.0, "online", "test", None),
)
conn.execute(
"INSERT INTO mileage_points(ts_utc, vehicle_id, vehicle_name, odometer_mi, state, source, note) VALUES (?,?,?,?,?,?,?)",
(30, "v1", "Car", 3.0, "online", "test", None),
)
all_rows = mileage_fetch_points(conn)
self.assertEqual([r[0] for r in all_rows], [10, 20, 30])
filtered = mileage_fetch_points(conn, since_ts=21)
self.assertEqual([r[0] for r in filtered], [30])
finally:
conn.close()
if __name__ == "__main__":
unittest.main()

36
tests/test_no_wake.py Normal file
View File

@@ -0,0 +1,36 @@
import unittest
import sys
from pathlib import Path
from unittest import mock
# Allow importing scripts/tesla.py as a module
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "scripts"))
import tesla # noqa: E402
class DummyVehicle(dict):
def sync_wake_up(self):
raise AssertionError("sync_wake_up should not be called in this test")
class NoWakeTests(unittest.TestCase):
def test_wake_vehicle_allow_wake_false_offline_returns_false(self):
v = DummyVehicle(state="asleep", display_name="Test Car")
self.assertFalse(tesla.wake_vehicle(v, allow_wake=False))
def test_wake_vehicle_online_returns_true(self):
v = DummyVehicle(state="online", display_name="Test Car")
self.assertTrue(tesla.wake_vehicle(v, allow_wake=False))
def test_ensure_online_or_exit_exits_3_when_no_wake(self):
v = DummyVehicle(state="asleep", display_name="Test Car")
with mock.patch.object(tesla, "wake_vehicle", return_value=False):
with self.assertRaises(SystemExit) as ctx:
tesla._ensure_online_or_exit(v, allow_wake=False)
self.assertEqual(ctx.exception.code, 3)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,25 @@
import unittest
from scripts.tesla import _fmt_open
class TestOpeningsFormat(unittest.TestCase):
def test_fmt_open_none(self):
self.assertIsNone(_fmt_open(None))
def test_fmt_open_bool(self):
self.assertEqual(_fmt_open(True), "Open")
self.assertEqual(_fmt_open(False), "Closed")
def test_fmt_open_intish(self):
self.assertEqual(_fmt_open(1), "Open")
self.assertEqual(_fmt_open(0), "Closed")
self.assertEqual(_fmt_open("1"), "Open")
self.assertEqual(_fmt_open("0"), "Closed")
def test_fmt_open_invalid(self):
self.assertIsNone(_fmt_open("maybe"))
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,37 @@
import unittest
import sys
from pathlib import Path
# Allow importing scripts/tesla.py as a module
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "scripts"))
import tesla # noqa: E402
class ReportChargingPowerTests(unittest.TestCase):
def test_report_includes_charging_power_details_when_present(self):
vehicle = {"display_name": "Test Car", "state": "online"}
data = {
"charge_state": {
"battery_level": 50,
"battery_range": 123.4,
"charging_state": "Charging",
"charger_power": 7,
"charger_voltage": 240,
"charger_actual_current": 30,
},
"climate_state": {},
"vehicle_state": {},
}
out = tesla._report(vehicle, data)
self.assertIn("Charging: Charging", out)
self.assertIn("Charging power:", out)
self.assertIn("7 kW", out)
self.assertIn("240V", out)
self.assertIn("30A", out)
if __name__ == "__main__":
unittest.main()

65
tests/test_report_json.py Normal file
View File

@@ -0,0 +1,65 @@
import unittest
import sys
from pathlib import Path
# Allow importing scripts/tesla.py as a module
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "scripts"))
import tesla # noqa: E402
class ReportJsonTests(unittest.TestCase):
def test_report_json_is_sanitized(self):
vehicle = {"display_name": "Test Car", "state": "online"}
data = {
"charge_state": {
"battery_level": 80,
"battery_range": 250.2,
"charging_state": "Charging",
"charge_limit_soc": 90,
"time_to_full_charge": 1.5,
"charge_rate": 30,
"charge_port_door_open": True,
"conn_charge_cable": "SAE",
"scheduled_charging_mode": "Start",
"scheduled_charging_pending": True,
"scheduled_charging_start_time": 60, # 01:00
},
"climate_state": {
"inside_temp": 21,
"outside_temp": 10,
"is_climate_on": True,
"seat_heater_left": 3,
"seat_heater_right": 1,
},
"vehicle_state": {"locked": False, "sentry_mode": True, "odometer": 12345.6},
# This is intentionally present in raw vehicle_data, but should not show up in report JSON.
"drive_state": {"latitude": 37.1234, "longitude": -122.5678},
}
out = tesla._report_json(vehicle, data)
self.assertIn("vehicle", out)
self.assertEqual(out["vehicle"]["display_name"], "Test Car")
# Must not include raw drive_state/location
self.assertNotIn("drive_state", out)
self.assertNotIn("location", out)
self.assertNotIn("latitude", str(out))
self.assertNotIn("longitude", str(out))
# Expected useful bits
self.assertEqual(out["battery"]["level_percent"], 80)
self.assertEqual(out["charging"]["charging_state"], "Charging")
self.assertEqual(out["charging"]["charge_port_door_open"], True)
self.assertEqual(out["charging"]["conn_charge_cable"], "SAE")
self.assertEqual(out["scheduled_charging"]["start_time_hhmm"], "01:00")
self.assertEqual(out["security"]["locked"], False)
# Seat heaters should be present when vehicle reports them
self.assertIn("seat_heaters", out["climate"])
self.assertEqual(out["climate"]["seat_heaters"]["seat_heater_left"], 3)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,46 @@
import unittest
import sys
from pathlib import Path
# Allow importing scripts/tesla.py as a module
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "scripts"))
import tesla # noqa: E402
class ReportOpeningsTests(unittest.TestCase):
def test_report_includes_openings_when_fields_present(self):
vehicle = {"display_name": "Test Car", "state": "online"}
data = {
"charge_state": {"battery_level": 50, "battery_range": 123.4},
"climate_state": {"inside_temp": 20, "outside_temp": 10, "is_climate_on": False},
"vehicle_state": {
"locked": True,
"sentry_mode": False,
"df": 1, # open
"rt": 0, # closed
},
}
out = tesla._report(vehicle, data)
self.assertIn("Openings:", out)
self.assertIn("Driver front door", out)
def test_report_json_openings_omits_when_unknown(self):
vehicle = {"display_name": "Test Car", "state": "online"}
data = {"vehicle_state": {"locked": True}}
out = tesla._report_json(vehicle, data)
self.assertNotIn("openings", out)
def test_report_json_openings_all_closed(self):
vehicle = {"display_name": "Test Car", "state": "online"}
data = {"vehicle_state": {"df": 0, "rt": 0}}
out = tesla._report_json(vehicle, data)
self.assertIn("openings", out)
self.assertEqual(out["openings"]["open"], [])
self.assertEqual(out["openings"]["all_closed"], True)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,34 @@
import unittest
import sys
from pathlib import Path
# Allow importing scripts/tesla.py as a module
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "scripts"))
import tesla # noqa: E402
class ReportPrivacyTests(unittest.TestCase):
def test_report_string_does_not_leak_location_fields(self):
vehicle = {"display_name": "Test Car", "state": "online"}
data = {
"charge_state": {"battery_level": 50, "battery_range": 123.4},
"climate_state": {"inside_temp": 20, "outside_temp": 10, "is_climate_on": False},
"vehicle_state": {"locked": True, "sentry_mode": False},
# Raw vehicle_data can include precise coords; report output must not echo them.
"drive_state": {"latitude": 37.123456, "longitude": -122.987654},
}
out = tesla._report(vehicle, data)
# No location fields or raw coordinate strings should appear.
self.assertNotIn("drive_state", out)
self.assertNotIn("latitude", out)
self.assertNotIn("longitude", out)
self.assertNotIn("37.123", out)
self.assertNotIn("-122.987", out)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,29 @@
import unittest
import sys
from pathlib import Path
# Allow importing scripts/tesla.py as a module
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "scripts"))
import tesla # noqa: E402
class ReportSeatHeatersTests(unittest.TestCase):
def test_report_includes_seat_heaters_when_available(self):
vehicle = {"display_name": "Test Car", "state": "online"}
data = {
"charge_state": {"battery_level": 50, "battery_range": 123.4},
"climate_state": {"is_climate_on": True, "seat_heater_left": 3, "seat_heater_right": 1},
"vehicle_state": {},
}
out = tesla._report(vehicle, data)
self.assertIn("Seat heaters:", out)
# Compact labels (D=driver, P=passenger)
self.assertIn("D 3", out)
self.assertIn("P 1", out)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,30 @@
import unittest
from scripts.tesla import _scheduled_departure_status_json
class TestScheduledDepartureStatus(unittest.TestCase):
def test_scheduled_departure_status_json(self):
charge = {
'scheduled_departure_enabled': True,
'scheduled_departure_time': 7 * 60 + 30,
'preconditioning_enabled': False,
'off_peak_charging_enabled': True,
}
out = _scheduled_departure_status_json(charge)
self.assertEqual(out['scheduled_departure_enabled'], True)
self.assertEqual(out['scheduled_departure_time'], 450)
self.assertEqual(out['scheduled_departure_time_hhmm'], '07:30')
self.assertEqual(out['preconditioning_enabled'], False)
self.assertEqual(out['off_peak_charging_enabled'], True)
def test_scheduled_departure_status_json_missing(self):
out = _scheduled_departure_status_json({})
# Ensure keys exist with None values for stable JSON schemas.
self.assertIn('scheduled_departure_enabled', out)
self.assertIn('scheduled_departure_time', out)
self.assertIn('scheduled_departure_time_hhmm', out)
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,53 @@
import unittest
import sys
from pathlib import Path
# Allow importing scripts/tesla.py as a module
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "scripts"))
import tesla # noqa: E402
class SummaryJsonTests(unittest.TestCase):
def test_summary_json_is_sanitized(self):
vehicle = {"display_name": "Test Car", "state": "online"}
data = {
"charge_state": {
"battery_level": 63,
"battery_range": 199.9,
"usable_battery_level": 61,
"charging_state": "Disconnected",
},
"climate_state": {
"inside_temp": 20,
"is_climate_on": False,
},
"vehicle_state": {"locked": True},
# Present in raw vehicle_data, must not show up in summary JSON.
"drive_state": {"latitude": 37.1234, "longitude": -122.5678},
}
out = tesla._summary_json(vehicle, data)
self.assertIn("vehicle", out)
self.assertEqual(out["vehicle"]["display_name"], "Test Car")
# Must not include raw drive_state/location
self.assertNotIn("drive_state", out)
self.assertNotIn("location", out)
self.assertNotIn("latitude", str(out))
self.assertNotIn("longitude", str(out))
# Expected useful bits
self.assertEqual(out["battery"]["level_percent"], 63)
self.assertEqual(out["battery"]["range_mi"], 199.9)
self.assertEqual(out["battery"]["usable_level_percent"], 61)
self.assertEqual(out["charging"]["charging_state"], "Disconnected")
self.assertEqual(out["security"]["locked"], True)
self.assertIn("summary", out)
self.assertIsInstance(out["summary"], str)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,51 @@
import unittest
from scripts.tesla import _fmt_minutes_hhmm, _parse_hhmm
class TestTimeHelpers(unittest.TestCase):
def test_fmt_minutes_hhmm_basic(self):
self.assertEqual(_fmt_minutes_hhmm(0), "00:00")
self.assertEqual(_fmt_minutes_hhmm(1), "00:01")
self.assertEqual(_fmt_minutes_hhmm(60), "01:00")
self.assertEqual(_fmt_minutes_hhmm(23 * 60 + 59), "23:59")
def test_fmt_minutes_hhmm_wraps_24h(self):
# Tesla sometimes uses minutes-from-midnight; be defensive.
self.assertEqual(_fmt_minutes_hhmm(24 * 60), "00:00")
self.assertEqual(_fmt_minutes_hhmm(25 * 60 + 5), "01:05")
def test_fmt_minutes_hhmm_invalid(self):
self.assertIsNone(_fmt_minutes_hhmm(-1))
self.assertIsNone(_fmt_minutes_hhmm(""))
self.assertIsNone(_fmt_minutes_hhmm(None))
def test_parse_hhmm(self):
self.assertEqual(_parse_hhmm("00:00"), 0)
self.assertEqual(_parse_hhmm("01:05"), 65)
self.assertEqual(_parse_hhmm("23:59"), 23 * 60 + 59)
def test_parse_hhmm_strips(self):
self.assertEqual(_parse_hhmm(" 07:30 "), 7 * 60 + 30)
def test_parse_hhmm_invalid(self):
for bad in [
None,
"",
" ",
"7:30", # must be zero-padded? actually current parser allows; but still has ':' so ok
]:
if bad == "7:30":
# Current implementation allows single-digit hours; keep behavior.
self.assertEqual(_parse_hhmm(bad), 7 * 60 + 30)
else:
with self.assertRaises(ValueError):
_parse_hhmm(bad)
for bad in ["24:00", "00:60", "-1:00", "ab:cd", "123"]:
with self.assertRaises(Exception):
_parse_hhmm(bad)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,71 @@
import unittest
import sys
from pathlib import Path
# Allow importing scripts/tesla.py as a module
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "scripts"))
import tesla # noqa: E402
class VehicleSelectTests(unittest.TestCase):
def setUp(self):
self.vehicles = [
{"display_name": "My Model 3"},
{"display_name": "Road Trip"},
{"display_name": "Model Y"},
]
def test_select_vehicle_default_first(self):
v = tesla._select_vehicle(self.vehicles, None)
self.assertEqual(v["display_name"], "My Model 3")
def test_select_vehicle_exact_case_insensitive(self):
v = tesla._select_vehicle(self.vehicles, "model y")
self.assertEqual(v["display_name"], "Model Y")
def test_select_vehicle_partial_substring(self):
v = tesla._select_vehicle(self.vehicles, "road")
self.assertEqual(v["display_name"], "Road Trip")
def test_select_vehicle_index_1_based(self):
v = tesla._select_vehicle(self.vehicles, "2")
self.assertEqual(v["display_name"], "Road Trip")
def test_select_vehicle_ambiguous_returns_none(self):
vehicles = [
{"display_name": "Alpha"},
{"display_name": "Alphanumeric"},
]
self.assertIsNone(tesla._select_vehicle(vehicles, "alp"))
def test_get_vehicle_ambiguous_error_is_helpful(self):
class FakeTesla:
def vehicle_list(self_inner):
return [
{"display_name": "Alpha"},
{"display_name": "Alphanumeric"},
{"display_name": "Beta"},
]
# Force ambiguity and assert we mention that it's ambiguous + show index hint.
stderr = sys.stderr
try:
from io import StringIO
buf = StringIO()
sys.stderr = buf
with self.assertRaises(SystemExit) as ctx:
tesla.get_vehicle(FakeTesla(), name="alp")
self.assertEqual(ctx.exception.code, 1)
out = buf.getvalue()
self.assertIn("ambiguous", out.lower())
self.assertIn("--car <N>", out)
self.assertIn("Matches:", out)
finally:
sys.stderr = stderr
if __name__ == "__main__":
unittest.main()