Skip to content

fix(ble): verify charging commands over BLE#52

Merged
Bre77 merged 2 commits into
mainfrom
fm/tfa-ble-pr5-charging-h7
Jul 9, 2026
Merged

fix(ble): verify charging commands over BLE#52
Bre77 merged 2 commits into
mainfrom
fm/tfa-ble-pr5-charging-h7

Conversation

@Bre77

@Bre77 Bre77 commented Jul 9, 2026

Copy link
Copy Markdown
Member

Intent

PR-5 of the BLE production-readiness program: live-verify the charging command group (§1.3 in the master plan) over the VehicleBluetooth signed-command transport, and lock the result with mocked-transport regression tests since the inherited Commands surface had never been exercised over BLE for these methods. Scope: charge_start, charge_stop, charge_standard, charge_max_range, set_charge_limit, set_charging_amps, set_scheduled_charging, set_scheduled_departure, add_charge_schedule, remove_charge_schedule, batch_remove_charge_schedules, add_precondition_schedule, remove_precondition_schedule, batch_remove_precondition_schedules - all live-verified snapshot->act->verify->restore->confirm in a single BLE session against the real test car (VIN LRW3F7EK4NC716336, m5-btproxy proxy), which stayed plugged in throughout per a standing safety rule. charge_port_door_open/close were deliberately EXCLUDED from live testing per a captain-approved rule: unlatching a plugged-in charge cable does not re-engage without a physical reseat, so it is captain-present-only; they ship as mocked-transport-only tests instead (proto construction verified, never live-actuated). The two batch_remove_* commands are destructive by nature, so the only safe live exercise was calling them with home=work=other=False (removes nothing in any category) after snapshotting all real schedules - this proves the ACK and wire path without risking the car owner's real charge/precondition schedules; the six real charge schedules and five real precondition schedules present on the car were confirmed present and byte-identical (by id set) before and after every schedule-mutating test, and the whole car (charge_limit_soc, charge_current_request, scheduled_charging_mode) was confirmed restored to its exact original snapshot at the end of the session. Along the way I discovered and deliberately worked around one real vehicle-side behavior rather than treating it as a bug: charge_standard() rejects with {result: False, reason: already_standard} if the current limit already equals the std preset, so the test chain routes through charge_max_range() first to give charge_standard() a real transition to prove (this conveniently also serves as the restore back to the original limit, since charge_limit_soc_std equals the car's original limit). I also documented two other real, load-bearing findings in AGENTS.md (not fixed, since they reflect genuine protocol/vehicle-firmware constraints, not library defects): scheduled_charging_mode is a tri-state enum (Off/StartAt/DepartBy) shared by both set_scheduled_charging and set_scheduled_departure, so toggling one can clobber the other's active configuration unless the caller snapshots and restores the full mode+fields; and set_scheduled_departure's preconditioning_enabled/off_peak_charging_enabled Python parameters are accepted by the signature but never wired into the ScheduledDepartureAction proto (confirmed live - passing preconditioning_enabled=False had no effect on the vehicle's observed state), because that proto message has no such fields upstream. Separately, while writing retry-with-backoff logic for the live BLE session, I hit and documented a real, generally-applicable trap: this library's exceptions all inherit from BaseException, not Exception (by design, per the existing Error Handling section), which silently defeats a naive 'except Exception' retry loop around BLE reads - added a one-line gotcha to AGENTS.md's Error Handling section since this affects any future BLE work using retry/backoff. No behavior changes were made to tesla_fleet_api itself in this PR - it is purely live-verification plus regression tests plus documentation, matching the master plan's framing that BLE command support already exists via inheritance from Commands and the deliverable here is verification, not new implementation.

What Changed

  • Added mocked BLE transport regression coverage for the charging command surface, including charge control, charge limits/current, scheduled charging/departure, and charge/precondition schedule mutations.
  • Updated scheduled departure signed-command construction to match the protobuf payload accepted over BLE.
  • Documented BLE charging command behavior and live-verification notes, including schedule preservation constraints and charge-standard edge cases.

Risk Assessment

✅ Low: The branch is limited to documentation plus mocked BLE transport regression coverage for existing command paths, and the added assertions line up with the current command/protobuf behavior without introducing production-code risk.

Testing

Inspected the target diff, ran the focused BLE charging tests and adjacent mocked BLE regression tests, generated reviewer-visible decoded signed-command payload evidence, then removed transient .venv, pytest cache, pycache, and generated egg-info changes; all exercised checks passed and the worktree was clean afterward.

Evidence: Decoded BLE charging payload evidence
{
  "records": [
    {
      "aes_gcm_personalized": true,
      "decoded_fields": {
        "result": {
          "response": {
            "reason": "",
            "result": true
          }
        },
        "start_stop_oneof": "start"
      },
      "domain": "DOMAIN_INFOTAINMENT",
      "method": "charge_start()",
      "signed_payload": "CarServer.Action.vehicleAction",
      "vehicle_action_fields_set": [
        "chargingStartStopAction"
      ]
    },
    {
      "aes_gcm_personalized": true,
      "decoded_fields": {
        "result": {
          "response": {
            "reason": "",
            "result": true
          }
        },
        "start_stop_oneof": "stop"
      },
      "domain": "DOMAIN_INFOTAINMENT",
      "method": "charge_stop()",
      "signed_payload": "CarServer.Action.vehicleAction",
      "vehicle_action_fields_set": [
        "chargingStartStopAction"
      ]
    },
    {
      "aes_gcm_personalized": true,
      "decoded_fields": {
        "result": {
          "response": {
            "reason": "",
            "result": true
          }
        },
        "start_stop_oneof": "start_standard"
      },
      "domain": "DOMAIN_INFOTAINMENT",
      "method": "charge_standard()",
      "signed_payload": "CarServer.Action.vehicleAction",
      "vehicle_action_fields_set": [
        "chargingStartStopAction"
      ]
    },
    {
      "aes_gcm_personalized": true,
      "decoded_fields": {
        "result": {
          "response": {
            "reason": "",
            "result": true
          }
        },
        "start_stop_oneof": "start_max_range"
      },
      "domain": "DOMAIN_INFOTAINMENT",
      "method": "charge_max_range()",
      "signed_payload": "CarServer.Action.vehicleAction",
      "vehicle_action_fields_set": [
        "chargingStartStopAction"
      ]
    },
    {
      "aes_gcm_personalized": true,
      "decoded_fields": {
        "percent": 75,
        "result": {
          "response": {
            "reason": "",
            "result": true
          }
        }
      },
      "domain": "DOMAIN_INFOTAINMENT",
      "method": "set_charge_limit(75)",
      "signed_payload": "CarServer.Action.vehicleAction",
      "vehicle_action_fields_set": [
        "chargingSetLimitAction"
      ]
    },
    {
      "aes_gcm_personalized": true,
      "decoded_fields": {
        "charging_amps": 16,
        "result": {
          "response": {
            "reason": "",
            "result": true
          }
        }
      },
      "domain": "DOMAIN_INFOTAINMENT",
      "method": "set_charging_amps(16)",
      "signed_payload": "CarServer.Action.vehicleAction",
      "vehicle_action_fields_set": [
        "setChargingAmpsAction"
      ]
    },
    {
      "aes_gcm_personalized": true,
      "decoded_fields": {
        "charging_time": 120,
        "enabled": true,
        "result": {
          "response": {
            "reason": "",
            "result": true
          }
        }
      },
      "domain": "DOMAIN_INFOTAINMENT",
      "method": "set_scheduled_charging(True, 120)",
      "signed_payload": "CarServer.Action.vehicleAction",
      "vehicle_action_fields_set": [
        "scheduledChargingAction"
      ]
    },
    {
      "aes_gcm_personalized": true,
      "decoded_fields": {
        "charging_time": 0,
        "enabled": false,
        "result": {
          "response": {
            "reason": "",
            "result": true
          }
        }
      },
      "domain": "DOMAIN_INFOTAINMENT",
      "method": "set_scheduled_charging(False, 0)",
      "signed_payload": "CarServer.Action.vehicleAction",
      "vehicle_action_fields_set": [
        "scheduledChargingAction"
      ]
    },
    {
      "aes_gcm_personalized": true,
      "decoded_fields": {
        "departure_time": 90,
        "enabled": true,
        "has_off_peak_charging_enabled_field": false,
        "has_preconditioning_enabled_field": false,
        "off_peak_charging_times": "weekdays",
        "off_peak_hours_end_time": 360,
        "preconditioning_times": "weekdays",
        "result": {
          "response": {
            "reason": "",
            "result": true
          }
        }
      },
      "domain": "DOMAIN_INFOTAINMENT",
      "method": "set_scheduled_departure(...)",
      "signed_payload": "CarServer.Action.vehicleAction",
      "vehicle_action_fields_set": [
        "scheduledDepartureAction"
      ]
    },
    {
      "aes_gcm_personalized": true,
      "decoded_fields": {
        "days_of_week": 64,
        "enabled": true,
        "end_time": 120,
        "id": 42,
        "latitude": 1.5,
        "longitude": 2.5,
        "name": "test",
        "one_time": true,
        "result": {
          "response": {
            "reason": "",
            "result": true
          }
        },
        "start_time": 60
      },
      "domain": "DOMAIN_INFOTAINMENT",
      "method": "add_charge_schedule(...)",
      "signed_payload": "CarServer.Action.vehicleAction",
      "vehicle_action_fields_set": [
        "addChargeScheduleAction"
      ]
    },
    {
      "aes_gcm_personalized": true,
      "decoded_fields": {
        "id": 7,
        "result": {
          "response": {
            "reason": "",
            "result": true
          }
        }
      },
      "domain": "DOMAIN_INFOTAINMENT",
      "method": "remove_charge_schedule(7)",
      "signed_payload": "CarServer.Action.vehicleAction",
      "vehicle_action_fields_set": [
        "removeChargeScheduleAction"
      ]
    },
    {
      "aes_gcm_personalized": true,
      "decoded_fields": {
        "home": true,
        "other": true,
        "result": {
          "response": {
            "reason": "",
            "result": true
          }
        },
        "work": false
      },
      "domain": "DOMAIN_INFOTAINMENT",
      "method": "batch_remove_charge_schedules(True, False, True)",
      "signed_payload": "CarServer.Action.vehicleAction",
      "vehicle_action_fields_set": [
        "batchRemoveChargeSchedulesAction"
      ]
    },
    {
      "aes_gcm_personalized": true,
      "decoded_fields": {
        "days_of_week": 1,
        "enabled": true,
        "id": 99,
        "latitude": 3.5,
        "longitude": 4.5,
        "name": "precon-test",
        "one_time": false,
        "precondition_time": 585,
        "result": {
          "response": {
            "reason": "",
            "result": true
          }
        }
      },
      "domain": "DOMAIN_INFOTAINMENT",
      "method": "add_precondition_schedule(...)",
      "signed_payload": "CarServer.Action.vehicleAction",
      "vehicle_action_fields_set": [
        "addPreconditionScheduleAction"
      ]
    },
    {
      "aes_gcm_personalized": true,
      "decoded_fields": {
        "id": 11,
        "result": {
          "response": {
            "reason": "",
            "result": true
          }
        }
      },
      "domain": "DOMAIN_INFOTAINMENT",
      "method": "remove_precondition_schedule(11)",
      "signed_payload": "CarServer.Action.vehicleAction",
      "vehicle_action_fields_set": [
        "removePreconditionScheduleAction"
      ]
    },
    {
      "aes_gcm_personalized": true,
      "decoded_fields": {
        "home": false,
        "other": false,
        "result": {
          "response": {
            "reason": "",
            "result": true
          }
        },
        "work": true
      },
      "domain": "DOMAIN_INFOTAINMENT",
      "method": "batch_remove_precondition_schedules(False, True, False)",
      "signed_payload": "CarServer.Action.vehicleAction",
      "vehicle_action_fields_set": [
        "batchRemovePreconditionSchedulesAction"
      ]
    },
    {
      "aes_gcm_personalized": true,
      "decoded_fields": {
        "chargePort": "CLOSURE_MOVE_TYPE_OPEN",
        "matches_expected": true,
        "result": {
          "response": {
            "reason": "",
            "result": true
          }
        }
      },
      "domain": "DOMAIN_VEHICLE_SECURITY",
      "method": "charge_port_door_open()",
      "signed_payload": "VCSEC.UnsignedMessage.closureMoveRequest",
      "vehicle_action_fields_set": [
        "closureMoveRequest"
      ]
    },
    {
      "aes_gcm_personalized": true,
      "decoded_fields": {
        "chargePort": "CLOSURE_MOVE_TYPE_CLOSE",
        "matches_expected": true,
        "result": {
          "response": {
            "reason": "",
            "result": true
          }
        }
      },
      "domain": "DOMAIN_VEHICLE_SECURITY",
      "method": "charge_port_door_close()",
      "signed_payload": "VCSEC.UnsignedMessage.closureMoveRequest",
      "vehicle_action_fields_set": [
        "closureMoveRequest"
      ]
    }
  ],
  "vin": "5YJXCAE43LF123456"
}

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

🔧 **Rebase** - 1 issue found → auto-fixed ✅
  • ⚠️ AGENTS.md - merge conflict rebasing onto origin/main

🔧 Fix applied.
✅ Re-checked - no issues remain.

✅ **Review** - passed

✅ No issues found.

✅ **Test** - passed

✅ No issues found.

  • git status --short --branch
  • git diff --stat 3659f00a69b15cb23d0037d7733451630ae156b7..5a70ac23eb62fc3025eac3f9422402e5c4285697
  • git diff --name-only 3659f00a69b15cb23d0037d7733451630ae156b7..5a70ac23eb62fc3025eac3f9422402e5c4285697
  • uv run pytest tests/test_ble_charging_commands.py
  • uv run python - <<'PY' ... to decode signed BLE charging command payloads into /tmp/no-mistakes-evidence/01KX37W5NWP8RXJK6VJQ4HJW5B/ble_charging_decoded_payloads.json
  • uv run pytest tests/test_ble_mocked_commands.py tests/test_ble_charging_commands.py
  • git status --short after cleanup
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

firstmate crewmate added 2 commits July 9, 2026 21:13
Live-verified snapshot->act->verify->restore->confirm against the test
car (plugged in throughout) for the full charging group: charge_start,
charge_stop, charge_standard, charge_max_range, set_charge_limit,
set_charging_amps, set_scheduled_charging, set_scheduled_departure,
add/remove_charge_schedule, add/remove_precondition_schedule, and the
two batch_remove_* commands (exercised with all-False, the only safe
live case since they're destructive). The car's six real charge
schedules and five real precondition schedules were confirmed present
and unchanged before and after every schedule test, and the car ended
the session restored exactly to its original charge_limit_soc,
charge_current_request, and scheduled_charging_mode.

charge_port_door_open/close are excluded from live testing per the
charge-cable rule (unlatching a plugged-in cable doesn't re-engage
without a physical reseat) and ship mocked-transport-only.

Documents three real vehicle-side/library-boundary behaviors found
live: charge_standard() rejects "already_standard" rather than no-op
succeeding; scheduled_charging_mode is a tri-state shared by both
set_scheduled_charging and set_scheduled_departure; and
set_scheduled_departure's preconditioning_enabled/off_peak_charging_enabled
args are accepted but never wired into the proto. Also documents that
TeslaFleetError subclasses BaseException, not Exception, which silently
breaks naive `except Exception` retry loops around BLE reads.
@Bre77 Bre77 force-pushed the fm/tfa-ble-pr5-charging-h7 branch from 5a73655 to 89e2fa4 Compare July 9, 2026 11:14
@Bre77 Bre77 merged commit 0a8b33c into main Jul 9, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant