Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
f4aff26
Implemented charge windows instead of just picking random low price s…
fredli74 Feb 6, 2026
f0cabe5
logic.ts trailing whitespace
fredli74 Feb 6, 2026
6ee0dd7
Update server/logic.ts
fredli74 Feb 6, 2026
d77163b
Reuse now instead of Date.now()
fredli74 Feb 6, 2026
62ca0a6
Incorrectly removed current timeslot from price_data SQL
fredli74 Feb 6, 2026
2f49ce8
Inline score calculation for remainders.
fredli74 Feb 6, 2026
56f2dbf
Track requested charge levels in status messages
fredli74 Feb 6, 2026
dfdbcbc
Refactor soft intents: type definition and status function
fredli74 Feb 6, 2026
289a128
Disable ESLint for v-html in alerts
fredli74 Feb 6, 2026
edee93d
Still do SoftIntents during manual charge, because of trip scheduling
fredli74 Feb 7, 2026
cd6e37c
Fix backoff logic to always run at least once if we have a targetMs <…
fredli74 Feb 7, 2026
a05c30e
Fix: Apply fill plan only if windows exist and time sufficient
fredli74 Feb 7, 2026
9970b35
Tesla-agent would set charge limit (wantedSoc) too low and risk the v…
fredli74 Feb 7, 2026
abaa689
Improve logging
fredli74 Feb 8, 2026
0ba6c3c
Refactor charge scheduling to use dynamic programming instead of spli…
fredli74 Feb 12, 2026
d6249df
Refactor vehicle logging to use dedicated logger
fredli74 Feb 12, 2026
eff4b93
Increase warmup penalty to 15 minutes and add scheduling window logging.
fredli74 Feb 13, 2026
e792fc0
Fix existingPrecon check
fredli74 Feb 13, 2026
2469fd9
Add error logging for vehicleWork failures
fredli74 Feb 16, 2026
8ec1159
Add exponential backoff for telemetry config on 403 errors
fredli74 Feb 17, 2026
e20c353
Allow over max price for charging start windows
fredli74 Feb 20, 2026
f283d3f
Fix spelling of 'Telemetry' in logs and strings
fredli74 Feb 20, 2026
303905c
Move dotenv config and use serverURL in proxy target
fredli74 Feb 20, 2026
b7c3b2e
Add module temperature min/max telemetry support
fredli74 Feb 20, 2026
66d84c1
Add split charge control for vehicle settings
fredli74 Feb 20, 2026
c2d4356
Fix split charge label and warmup penalty calculation
fredli74 Feb 20, 2026
7d4afca
Fix: We did not respect the autoHvac setting
fredli74 Feb 23, 2026
47b15e8
tesla: redact secrets in logs
fredli74 Mar 11, 2026
5640500
Fix splitCharge default and saving state handling.
fredli74 Mar 17, 2026
3cd204f
Fix soft intent scheduling by keeping per-intent deadlines
fredli74 Apr 3, 2026
6f93098
Keep active charge legs sticky near completion
fredli74 Apr 3, 2026
98dee8f
Audit Tesla schedules against live vehicle state
fredli74 Apr 3, 2026
55ab6e1
Await queued Tesla work on telemetry scope backoff
fredli74 Apr 3, 2026
6cb27e4
Log updateVehicle against verified vehicle UUID
fredli74 Apr 3, 2026
2bf6ad5
Compile error because of missing import
fredli74 Apr 3, 2026
2b65953
Scope vehicle location save cleanup to each request
fredli74 Apr 3, 2026
94a38f8
Refactor charge intent handling with price planning constants
fredli74 Apr 4, 2026
3184af2
Refactor scheduling logic for step-based optimization
fredli74 Apr 10, 2026
4936876
Fix splitCharge type and location key in templates
fredli74 Apr 10, 2026
8930668
Add planStartIndex to track charge plan start position
fredli74 Apr 21, 2026
1337dcb
Replace deepmerge with ticket-based save state tracking
fredli74 Apr 21, 2026
240bff8
Refactor gap penalty logic for interruptions
fredli74 Apr 21, 2026
654fd5d
Fix stepMs for splitCharge.Always case
fredli74 Apr 21, 2026
fcb1d3c
Add disallowGaps check to block gap-containing compositions.
fredli74 Apr 21, 2026
16ab507
Refactor scheduling logic to prioritize tariff fidelity and phase tra…
fredli74 Apr 22, 2026
3617fc9
Add detailed logging for scheduling steps and node management.
fredli74 Apr 22, 2026
2dd33b0
Extract location settings logic into helper method
fredli74 Apr 22, 2026
cac8348
Replace string-based schedule comparison with structural equality che…
fredli74 Apr 22, 2026
8ead037
Refactor charging plan to track warmup debt and energy cost.
fredli74 Apr 22, 2026
c3fddbb
Replace compareStopTimes with numeric comparison in sort
fredli74 Apr 22, 2026
c0f3761
Fix form validation check and add else block for saving state.
fredli74 Apr 22, 2026
26458af
Refactor log formatting into helper function
fredli74 Apr 22, 2026
c767ffe
Fix: Sort schedules by start time instead of stop time
fredli74 Apr 22, 2026
770145f
Sort intents by start time instead of stop time.
fredli74 Apr 22, 2026
ea2929b
Enhance schedule management and emergency wake-up logic
fredli74 Apr 22, 2026
c24027f
Update manual schedule departure threshold to 3km for clearer detection.
fredli74 Apr 22, 2026
44b1f35
Remove setSmartStatusFromIntent and integrate status into charge plan
fredli74 Apr 22, 2026
ae599de
Log VIN always, include UUID when available
fredli74 Apr 22, 2026
2c474af
Move Tesla scheduling methods to public utilities.
fredli74 Apr 22, 2026
c2b0854
Add Node.js tests for Tesla agent logic
fredli74 Apr 22, 2026
40e31e0
Add trusted HTML comments to v-html usages
fredli74 Apr 22, 2026
df20a04
Replace splitCharge string with enum in resolvers and schema
fredli74 Apr 22, 2026
562a1fe
Refactor charge schedule quantum and adjust blocker logic for tariff …
fredli74 Apr 22, 2026
79979ac
Fix emergency wake-up time parsing and log formatting
fredli74 Apr 23, 2026
f49c50e
Fix: Skip schedule reconciliation if live schedules are unknown
fredli74 Apr 23, 2026
7acb956
Add schedule sync issue indicators and backend support
fredli74 Apr 23, 2026
868024c
Simplify schedule sync issue handling by removing reason and streamli…
fredli74 Apr 23, 2026
060e327
Update sync status colors and drift message wording
fredli74 Apr 23, 2026
7c89cc2
Move icon class to tooltip activator
fredli74 Apr 23, 2026
c0de92a
Use issueKind instead of hardcoded 'drift
fredli74 Apr 24, 2026
0eb6648
Update schedule tooltip and add SOC limit checks
fredli74 Apr 24, 2026
8e21af4
Fix schedule sync classification by checking start time only.
fredli74 Apr 27, 2026
919cd96
Add teslaWantedSocLimit function and test
fredli74 Apr 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ jobs:
cache: 'npm'
- run: npm ci
- run: npm run build
- run: npm run test:node
6 changes: 6 additions & 0 deletions app/src/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,13 @@
tile
prominent
>
<!-- Intentionally rendered as trusted app/server-provided HTML. Do not pass user-supplied content here. -->
<!-- eslint-disable-next-line vue/no-v-html -->
<span v-html="error.message"></span>
Comment thread
fredli74 marked this conversation as resolved.
</v-alert>
<v-alert v-model="warning.show" dismissible type="warning" tile>
<!-- Intentionally rendered as trusted app/server-provided HTML. Do not pass user-supplied content here. -->
<!-- eslint-disable-next-line vue/no-v-html -->
<span v-html="warning.message"></span>
Comment thread
fredli74 marked this conversation as resolved.
Comment thread
fredli74 marked this conversation as resolved.
</v-alert>
Comment thread
fredli74 marked this conversation as resolved.
<v-alert
Expand All @@ -85,6 +89,8 @@
tile
@input="closedInfo"
>
<!-- Intentionally rendered as trusted app/server-provided HTML. Do not pass user-supplied content here. -->
<!-- eslint-disable-next-line vue/no-v-html -->
<span v-html="info.message"></span>
Comment thread
fredli74 marked this conversation as resolved.
Comment thread
fredli74 marked this conversation as resolved.
Comment on lines 77 to 94
</v-alert>
</v-flex>
Expand Down
34 changes: 24 additions & 10 deletions app/src/components/edit-location.vue
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ import gql from "graphql-tag";

import EditVehicleLocationSettings from "@app/components/edit-vehicle-location-settings.vue";
import RemoveDialog from "@app/components/remove-dialog.vue";
import deepmerge from "deepmerge";
import equal from "fast-deep-equal";
import { GQLLocation, GQLPriceList } from "@shared/sc-schema.js";
import { UpdateLocationParams } from "@shared/sc-client.js";
Expand Down Expand Up @@ -129,17 +128,26 @@ export default class EditLocation extends Vue {

debounceTimer?: any;
touchedFields: any = {};
clearSaving: any = {};
saveTicketSeq = 0;
saveTickets: Record<string, number> = {};
async save(field: string) {
delete this.clearSaving[field];
const fieldTicket = ++this.saveTicketSeq;
this.saveTickets[field] = fieldTicket;
this.$set(this.saving, field, true);

if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}
this.debounceTimer = setTimeout(async () => {
const form: any = this.$refs.form;
if (form.validate && form.validate()) {
const fieldsInRequest = Object.entries(this.saving)
.filter(([, value]) => value)
.map(([key]) => key);
const requestTickets: Record<string, number> = {};
for (const key of fieldsInRequest) {
requestTickets[key] = this.saveTickets[key] || 0;
}
if (!form.validate || form.validate()) {
const update: UpdateLocationParams = {
id: this.location.id,
};
Expand All @@ -153,12 +161,18 @@ export default class EditLocation extends Vue {
delete update.providerData;
}

this.clearSaving = deepmerge(this.clearSaving, this.saving);

await this.$scClient.updateLocation(update);

for (const [key, value] of Object.entries(this.clearSaving)) {
if (value) {
try {
await this.$scClient.updateLocation(update);
} finally {
for (const key of fieldsInRequest) {
if (this.saveTickets[key] === requestTickets[key]) {
this.$set(this.saving, key, false);
}
}
}
} else {
for (const key of fieldsInRequest) {
if (this.saveTickets[key] === requestTickets[key]) {
this.$set(this.saving, key, false);
}
}
Expand Down
78 changes: 62 additions & 16 deletions app/src/components/edit-vehicle-location-settings.vue
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
<template>
<v-form ref="form">
<v-row>
<v-col cols="12" sm="5" md="6" class="mt-2">
<v-col cols="12" sm="4" md="5" class="mt-2">
<v-list-item-title>{{ name }}</v-list-item-title>
<v-list-item-subtitle
class="font-light overline caption secondary--text text--lighten-2"
>
({{ settings.locationID }})
</v-list-item-subtitle>
</v-col>
<v-spacer />
<v-col cols="6" sm="3" md="3">
<v-col cols="6" sm="3" md="2">
<v-text-field
v-model="directLevel"
:rules="[directLevelRules]"
Expand All @@ -33,7 +32,25 @@
</template>
</v-text-field>
</v-col>
<v-col cols="6" sm="4" md="3">
<v-col cols="6" sm="3" md="2">
<v-select
v-model="splitCharge"
:items="splitChargeList"
label="Split charge window"
placeholder=" "
:loading="saving.splitCharge"
>
<template #append-outer>
<v-tooltip bottom max-width="18rem">
<template #activator="{ on }">
<v-icon v-on="on">mdi-help-circle-outline</v-icon>
</template>
Control if charging is allowed to split into multiple windows.
</v-tooltip>
</template>
</v-select>
</v-col>
<v-col cols="12" sm="4" md="3">
<v-combobox
v-model="goal"
:items="goalCBList"
Expand Down Expand Up @@ -62,7 +79,6 @@
v-model="focus"
active-class="selected-charge"
color="primary"
label="hej"
mandatory
>
<v-btn small>Low Cost</v-btn>
Expand All @@ -77,9 +93,8 @@

<script lang="ts">
import { Component, Vue, Prop } from "vue-property-decorator";
import deepmerge from "deepmerge";
import { GQLVehicle, GQLVehicleLocationSetting } from "@shared/sc-schema.js";
import { SmartChargeGoal } from "@shared/sc-types.js";
import { SmartChargeGoal, SplitCharge } from "@shared/sc-types.js";
import { UpdateVehicleParams } from "@shared/sc-client.js";

@Component({})
Expand All @@ -90,18 +105,25 @@ export default class EditVehicle extends Vue {

saving!: { [key: string]: boolean };
goalCBList!: { text: string; value: string }[];
splitChargeList!: { text: string; value: string }[];
data() {
return {
saving: {
directLevel: false,
goal: false,
splitCharge: false,
},
goalCBList: [
{ text: "Low cost", value: SmartChargeGoal.Low },
{ text: "Balanced", value: SmartChargeGoal.Balanced },
{ text: "Full charge", value: SmartChargeGoal.Full },
{ text: "Custom", value: "%" },
],
splitChargeList: [
{ text: "Never", value: SplitCharge.Never },
{ text: "Auto", value: SplitCharge.Auto },
{ text: "Always", value: SplitCharge.Always },
],
Comment on lines 96 to +126
};
}

Expand Down Expand Up @@ -153,19 +175,36 @@ export default class EditVehicle extends Vue {
this.save("goal");
}

get splitCharge(): string {
return this.settings.splitCharge || SplitCharge.Auto;
}
Comment on lines +178 to +180
set splitCharge(value: string) {
this.settings.splitCharge = value;
this.save("splitCharge");
}

debounceTimer?: any;
touchedFields: any = {};
clearSaving: any = {};
saveTicketSeq = 0;
saveTickets: Record<string, number> = {};
async save(field: string) {
delete this.clearSaving[field];
const fieldTicket = ++this.saveTicketSeq;
this.saveTickets[field] = fieldTicket;
this.$set(this.saving, field, true);

if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}
this.debounceTimer = setTimeout(async () => {
const form: any = this.$refs.form;
if (form.validate && form.validate()) {
const fieldsInRequest = Object.entries(this.saving)
.filter(([, value]) => value)
.map(([key]) => key);
const requestTickets: Record<string, number> = {};
for (const key of fieldsInRequest) {
requestTickets[key] = this.saveTickets[key] || 0;
}
if (!form.validate || form.validate()) {
const goal = this.settings.goal as any;
const update: UpdateVehicleParams = {
id: this.vehicle.id,
Expand All @@ -174,16 +213,23 @@ export default class EditVehicle extends Vue {
locationID: this.settings.locationID,
directLevel: this.settings.directLevel,
goal: goal.value || goal,
splitCharge: this.settings.splitCharge || SplitCharge.Auto,
} as GQLVehicleLocationSetting,
],
};

this.clearSaving = deepmerge(this.clearSaving, this.saving);

await this.$scClient.updateVehicle(update);

for (const [key, value] of Object.entries(this.clearSaving)) {
if (value) {
try {
await this.$scClient.updateVehicle(update);
} finally {
for (const key of fieldsInRequest) {
if (this.saveTickets[key] === requestTickets[key]) {
this.$set(this.saving, key, false);
}
}
}
} else {
for (const key of fieldsInRequest) {
if (this.saveTickets[key] === requestTickets[key]) {
this.$set(this.saving, key, false);
}
}
Expand Down
54 changes: 37 additions & 17 deletions app/src/components/edit-vehicle.vue
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@

<EditVehicleLocationSettings
v-for="l in locationSettings()"
:key="l.settings.location"
:key="l.settings.locationID"
:name="l.name"
:settings="l.settings"
:vehicle="vehicle"
Expand Down Expand Up @@ -94,7 +94,6 @@
import { Component, Vue, Prop } from "vue-property-decorator";
import EditVehicleLocationSettings from "@app/components/edit-vehicle-location-settings.vue";
import RemoveDialog from "@app/components/remove-dialog.vue";
import deepmerge from "deepmerge";
import equal from "fast-deep-equal";
import {
GQLVehicle,
Expand Down Expand Up @@ -141,18 +140,24 @@ export default class EditVehicle extends Vue {
return true;
}

getOrCreateLocationSettings(locationID: string): GQLVehicleLocationSetting {
if (!this.vehicle.locationSettings) {
this.$set(this.vehicle, "locationSettings", []);
}
const existing = this.vehicle.locationSettings.find((f) => f.locationID === locationID);
if (existing) return existing;
const created = DefaultVehicleLocationSettings(locationID);
this.vehicle.locationSettings.push(created);
return created;
}

locationSettings(): any[] {
return (
(this.locations &&
this.locations
.filter((l) => l.ownerID === this.vehicle.ownerID)
.map((l) => {
const settings: GQLVehicleLocationSetting =
(this.vehicle.locationSettings &&
this.vehicle.locationSettings.find(
(f) => f.locationID === l.id
)) ||
DefaultVehicleLocationSettings(l.id);
const settings = this.getOrCreateLocationSettings(l.id);
return {
name: l.name,
settings,
Expand Down Expand Up @@ -216,17 +221,26 @@ export default class EditVehicle extends Vue {

debounceTimer?: any;
touchedFields: any = {};
clearSaving: any = {};
saveTicketSeq = 0;
saveTickets: Record<string, number> = {};
async save(field: string) {
delete this.clearSaving[field];
const fieldTicket = ++this.saveTicketSeq;
this.saveTickets[field] = fieldTicket;
this.$set(this.saving, field, true);

if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}
this.debounceTimer = setTimeout(async () => {
const form: any = this.$refs.form;
if (form.validate && form.validate()) {
const fieldsInRequest = Object.entries(this.saving)
.filter(([, value]) => value)
.map(([key]) => key);
const requestTickets: Record<string, number> = {};
for (const key of fieldsInRequest) {
requestTickets[key] = this.saveTickets[key] || 0;
}
if (!form.validate || form.validate()) {
const update: UpdateVehicleParams = {
id: this.vehicle.id,
providerData: {},
Expand All @@ -251,12 +265,18 @@ export default class EditVehicle extends Vue {
delete update.providerData;
}

this.clearSaving = deepmerge(this.clearSaving, this.saving);

await this.$scClient.updateVehicle(update);

for (const [key, value] of Object.entries(this.clearSaving)) {
if (value) {
try {
await this.$scClient.updateVehicle(update);
} finally {
for (const key of fieldsInRequest) {
if (this.saveTickets[key] === requestTickets[key]) {
this.$set(this.saving, key, false);
}
}
}
} else {
for (const key of fieldsInRequest) {
if (this.saveTickets[key] === requestTickets[key]) {
this.$set(this.saving, key, false);
}
}
Expand Down
Loading
Loading