Implemented charge windows instead of just picking random low price s…#951
Implemented charge windows instead of just picking random low price s…#951
Conversation
…egments Signed-off-by: Fredrik Lidström <fredli74@gmail.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f4aff26557
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Pull request overview
This pull request implements a sophisticated charge window scheduling algorithm that replaces the simpler approach of just picking random low-price slots. The new system intelligently schedules charge windows by considering price data, time constraints, and multiple competing charge intents (manual, trip, routine, fill) to optimize both cost and charging behavior.
Changes:
- Replaced the old
GeneratePlanfunction with a new "soft intents" model that collects all charge requirements first, then schedules them optimally across available price windows - Implemented a greedy sliding-window algorithm to find optimal charge windows that minimize cost while respecting constraints like minimum window duration (to avoid fragmentation in cold weather) and maximum price thresholds
- Added support for splitting charge sessions across multiple windows when cost savings justify it, with configurable thresholds for when to split vs. consolidate charging
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Fredrik Lidström <fredli74@gmail.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 289a128246
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Signed-off-by: Fredrik Lidström <fredli74@gmail.com>
… quantumMs Signed-off-by: Fredrik Lidström <fredli74@gmail.com>
…ehicle not waking up to charge, cause it though the level was ok already Signed-off-by: Fredrik Lidström <fredli74@gmail.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Signed-off-by: Fredrik Lidström <fredli74@gmail.com>
Signed-off-by: Fredrik Lidström <fredli74@gmail.com>
|
@codex review |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // DP over (startSlot, neededSteps). Gap penalty is modeled as extra score cost, not extra time. | ||
| const dpTable = Array.from({ length: numSlots + 1 }, () => new Array(timeNeededSteps + 1).fill(Infinity)); | ||
| const dpWindows = Array.from({ length: numSlots + 1 }, () => new Array(timeNeededSteps + 1).fill(Infinity)); | ||
| const choiceTable = Array.from({ length: numSlots + 1 }, () => new Array(timeNeededSteps + 1).fill(null)); |
There was a problem hiding this comment.
The dynamic programming algorithm creates three large 2D arrays (dpTable, dpWindows, choiceTable) with dimensions (numSlots + 1) × (timeNeededSteps + 1). With many price slots and long charging durations, this could consume significant memory. For example, 100 slots and 1000 steps would create arrays with 100,000 elements each. Consider adding bounds checking or logging warnings when the arrays become very large.
| // DP over (startSlot, neededSteps). Gap penalty is modeled as extra score cost, not extra time. | |
| const dpTable = Array.from({ length: numSlots + 1 }, () => new Array(timeNeededSteps + 1).fill(Infinity)); | |
| const dpWindows = Array.from({ length: numSlots + 1 }, () => new Array(timeNeededSteps + 1).fill(Infinity)); | |
| const choiceTable = Array.from({ length: numSlots + 1 }, () => new Array(timeNeededSteps + 1).fill(null)); | |
| // Estimate DP memory usage and warn if very large. | |
| const dpRows = numSlots + 1; | |
| const dpCols = timeNeededSteps + 1; | |
| const dpCellsPerTable = dpRows * dpCols; | |
| const dpTablesCount = 3; | |
| const totalDpCells = dpCellsPerTable * dpTablesCount; | |
| const LARGE_DP_THRESHOLD = 500_000; | |
| if (totalDpCells > LARGE_DP_THRESHOLD) { | |
| vehicleLog( | |
| LogLevel.Warn, | |
| vehicle.vehicle_uuid, | |
| `scheduleWindows(${scheduleTag}): large DP allocation: ` + | |
| `slots=${numSlots} steps=${timeNeededSteps} cellsPerTable=${dpCellsPerTable} totalCells=${totalDpCells}` | |
| ); | |
| } | |
| // DP over (startSlot, neededSteps). Gap penalty is modeled as extra score cost, not extra time. | |
| const dpTable = Array.from({ length: dpRows }, () => new Array(dpCols).fill(Infinity)); | |
| const dpWindows = Array.from({ length: dpRows }, () => new Array(dpCols).fill(Infinity)); | |
| const choiceTable = Array.from({ length: dpRows }, () => new Array(dpCols).fill(null)); |
| // Convert to numeric charge plans with start and stop times rounded to 15 minutes | ||
| .map((p): NumericChargePlan => { | ||
| if (wantedSoc === undefined && p.level) { | ||
| if (p.level !== undefined && (wantedSoc === undefined || wantedSoc < p.level)) { |
There was a problem hiding this comment.
The condition has been changed from checking if wantedSoc === undefined to checking if p.level !== undefined && (wantedSoc === undefined || wantedSoc < p.level). This changes the logic to update wantedSoc to the maximum level from all charge plans, not just the first one encountered. While this may be intentional to find the highest charge level needed, it could cause unexpected behavior if multiple plans specify different levels and the intent was to use a specific one. Verify this behavior is correct for your use case.
| if (p.level !== undefined && (wantedSoc === undefined || wantedSoc < p.level)) { | |
| if (wantedSoc === undefined && p.level !== undefined) { |
| vehicleLog(LogLevel.Debug, input.id, `updateVehicle: ${JSON.stringify(input)}`); | ||
| await context.db.getVehicle(accountFilter(context.accountUUID), input.id); |
There was a problem hiding this comment.
The vehicleLog is being called with input.id before the vehicle is retrieved from the database. If the vehicle doesn't exist or the user doesn't have access, this will log with an incorrect vehicle UUID. The log should be moved after line 143 where the vehicle is retrieved, and use vehicle.vehicle_uuid instead.
| vehicleLog(LogLevel.Debug, input.id, `updateVehicle: ${JSON.stringify(input)}`); | |
| await context.db.getVehicle(accountFilter(context.accountUUID), input.id); | |
| const vehicle = await context.db.getVehicle(accountFilter(context.accountUUID), input.id); | |
| vehicleLog(LogLevel.Debug, vehicle.vehicle_uuid, `updateVehicle: ${JSON.stringify(input)}`); |
| // verify vehicle ownage | ||
| log(LogLevel.Debug, `updateSchedule: ${JSON.stringify(id)} ${JSON.stringify(vehicleID)}`); | ||
| const vehicle = await context.db.getVehicle(accountFilter(context.accountUUID), vehicleID); | ||
| vehicleLog(LogLevel.Debug, vehicle.vehicle_uuid, `updateSchedule: ${JSON.stringify(id)} ${JSON.stringify(vehicleID)}`); |
There was a problem hiding this comment.
The vehicleLog is being called with input.id before the vehicle is retrieved from the database. If the vehicle doesn't exist or the user doesn't have access, this will log with an incorrect vehicle UUID. The log should be moved after line 216 where the vehicle is retrieved, and use vehicle.vehicle_uuid instead.
| log(LogLevel.Trace, `TeslaAPI.renewToken(${redactSecret(refresh_token)}) response: ${JSON.stringify({ expires_in: authResponse?.expires_in })}`); | ||
| return this.parseTokenResponse(authResponse); | ||
| } catch (e) { | ||
| console.debug(`TeslaAPI.renewToken(${refresh_token}) error: ${e}`); |
There was a problem hiding this comment.
The error handler logs the unredacted refresh_token in the error message. This could expose sensitive credentials in logs. Consider using redactSecret(refresh_token) here as well, consistent with the success path logging.
| setSmartStatusFromIntent(allocations[0], "scheduled"); | ||
| const sorted = windows.slice().sort((a, b) => a.start - b.start); | ||
| let i = 0; | ||
| let remaining = allocations.length > 0 ? allocations[0].durationMs : 0; |
There was a problem hiding this comment.
The applyWindows function accesses allocations[0] without checking if the array is empty. While line 1028 calls setSmartStatusFromIntent(allocations[0], "scheduled") which safely handles undefined, if the function is ever called with an empty allocations array from other contexts, it could cause issues. Consider adding a guard check at the beginning of the function to return early if allocations is empty.
| setSmartStatusFromIntent(allocations[0], "scheduled"); | |
| const sorted = windows.slice().sort((a, b) => a.start - b.start); | |
| let i = 0; | |
| let remaining = allocations.length > 0 ? allocations[0].durationMs : 0; | |
| if (!allocations.length) { | |
| return; | |
| } | |
| setSmartStatusFromIntent(allocations[0], "scheduled"); | |
| const sorted = windows.slice().sort((a, b) => a.start - b.start); | |
| let i = 0; | |
| let remaining = allocations[0].durationMs; |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8ec11594a7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (i.beforeTs !== undefined) intentDeadline = Math.min(intentDeadline, i.beforeTs); | ||
| if (i.level > intentMaxLevel) intentMaxLevel = i.level; |
There was a problem hiding this comment.
Keep each soft intent deadline when building charge windows
In scheduleSoftIntents, the planner collapses all soft intents into intentDeadline = min(beforeTs) and intentMaxLevel = max(level), then schedules that max level against the earliest deadline. This couples unrelated targets and can force high future targets to be charged too early (for example, a lower routine target due tomorrow plus a higher trip target due later gets planned as the higher level by tomorrow), which is a behavioral regression from the previous per-intent scheduling logic and increases unnecessary early charging.
Useful? React with 👍 / 👎.
| logVehicle(LogLevel.Warning, v, `Telemetry config blocked by scopes; backoff ${Math.round(delayMs / 60e3)}m`); | ||
| } | ||
| } | ||
| return; |
There was a problem hiding this comment.
Await queued work before returning on telemetry scope errors
The missing scopes handler returns immediately from serviceWork, bypassing the final await Promise.all(waitFor) that normally joins already queued async work. When this branch is hit, previously started vehicleWork/telemetry API calls run detached from the service loop, so their failures are no longer observed here and the next poll can start while prior work is still in flight.
Useful? React with 👍 / 👎.
No description provided.