Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Retryable feature to indicate vehicles that are temporarily offline #12841

Merged
merged 2 commits into from
Mar 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 api/feature.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ const (
CoarseCurrent
IntegratedDevice
Heating
Retryable
)
12 changes: 8 additions & 4 deletions api/feature_enumer.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions assets/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -508,3 +508,10 @@ html.app .modal-dialog {
margin-top: env(safe-area-inset-top);
margin-bottom: env(safe-area-inset-bottom);
}

.btn-neutral {
all: unset;
}
.btn-neutral:hover {
outline: revert;
}
5 changes: 5 additions & 0 deletions assets/js/components/Loadpoint.vue
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,11 @@ export default {
vehicleHasSoc: function () {
return this.vehicleKnown && !this.vehicle?.features?.includes("Offline");
},
vehicleNotReachable: function () {
// online vehicle that was not reachable at startup
const features = this.vehicle?.features || [];
return features.includes("Offline") && features.includes("Retryable");
},
socBasedCharging: function () {
return this.vehicleHasSoc || this.vehicleSoc > 0;
},
Expand Down
17 changes: 17 additions & 0 deletions assets/js/components/MaterialIcon/CloudOffline.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<template>
<svg :style="svgStyle" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M6.5 20q-2.3 0-3.9-1.6T1 14.5q0-1.925 1.188-3.425T5.25 9.15q.075-.2.15-.387t.15-.413L2.1 4.9q-.275-.275-.275-.7t.275-.7q.275-.275.7-.275t.7.275l17 17q.275.275.288.688t-.288.712q-.275.275-.687.288t-.713-.263L17.15 20zm0-2h8.65L7.1 9.95q-.05.275-.075.525T7 11h-.5q-1.45 0-2.475 1.025T3 14.5q0 1.45 1.025 2.475T6.5 18m15.1.75l-1.45-1.4q.425-.35.638-.812T21 15.5q0-1.05-.725-1.775T18.5 13H17v-2q0-2.075-1.463-3.537T12 6q-.675 0-1.3.163t-1.2.512l-1.45-1.45q.875-.6 1.863-.912T12 4q2.925 0 4.963 2.038T19 11q1.725.2 2.863 1.488T23 15.5q0 .975-.375 1.813T21.6 18.75m-6.775-6.725"
></path>
</svg>
</template>

<script>
import icon from "../../mixins/icon";

export default {
name: "CloudOffline",
mixins: [icon],
};
</script>
17 changes: 17 additions & 0 deletions assets/js/components/MaterialIcon/Sync.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<template>
<svg :style="svgStyle" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M6 12.05q0 1.125.425 2.188T7.75 16.2l.25.25V15q0-.425.288-.712T9 14q.425 0 .713.288T10 15v4q0 .425-.288.713T9 20H5q-.425 0-.712-.288T4 19q0-.425.288-.712T5 18h1.75l-.4-.35q-1.3-1.15-1.825-2.625T4 12.05Q4 9.7 5.2 7.787T8.425 4.85q.35-.2.738-.025t.512.575q.125.375-.012.75t-.488.575q-1.45.8-2.312 2.213T6 12.05m12-.1q0-1.125-.425-2.187T16.25 7.8L16 7.55V9q0 .425-.288.713T15 10q-.425 0-.712-.288T14 9V5q0-.425.288-.712T15 4h4q.425 0 .713.288T20 5q0 .425-.288.713T19 6h-1.75l.4.35q1.225 1.225 1.788 2.663T20 11.95q0 2.35-1.2 4.263t-3.225 2.937q-.35.2-.737.025t-.513-.575q-.125-.375.013-.75t.487-.575q1.45-.8 2.313-2.212T18 11.95"
></path>
</svg>
</template>

<script>
import icon from "../../mixins/icon";

export default {
name: "Sync",
mixins: [icon],
};
</script>
1 change: 1 addition & 0 deletions assets/js/components/Vehicle.vue
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ export default {
vehicles: Array,
vehicleSoc: Number,
vehicleTargetSoc: Number,
vehicleNotReachable: Boolean,
},
emits: ["limit-soc-updated", "limit-energy-updated", "change-vehicle", "remove-vehicle"],
data() {
Expand Down
66 changes: 46 additions & 20 deletions assets/js/components/VehicleTitle.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
<template>
<div class="d-flex justify-content-between mb-3 align-items-center" data-testid="vehicle-title">
<h4 class="d-flex align-items-center m-0 flex-grow-1 overflow-hidden">
<shopicon-regular-refresh
<div
v-if="iconType === 'refresh'"
class="me-2 flex-shrink-0 spin"
ref="refresh"
data-bs-toggle="tooltip"
:title="$t('main.vehicle.detectionActive')"
class="me-2 flex-shrink-0 spin"
></shopicon-regular-refresh>
data-bs-toggle="tooltip"
>
<Sync />
</div>
<VehicleIcon
v-else-if="iconType === 'vehicle'"
:name="icon"
Expand All @@ -26,38 +28,59 @@
@change-vehicle="changeVehicle"
@remove-vehicle="removeVehicle"
>
<span class="flex-grow-1 text-truncate vehicle-name">
<span class="flex-grow-1 text-truncate vehicle-name" data-testid="vehicle-name">
{{ name }}
</span>
</VehicleOptions>
<span v-else class="flex-grow-1 text-truncate vehicle-name">
<span v-else class="flex-grow-1 text-truncate vehicle-name" data-testid="vehicle-name">
{{ name }}
</span>
<button
v-if="vehicleNotReachable"
class="ms-2 btn-neutral"
ref="notReachable"
data-bs-toggle="tooltip"
:title="$t('main.vehicle.notReachable')"
type="button"
data-testid="vehicle-not-reachable-icon"
@click="openHelpModal"
>
<CloudOffline class="evcc-gray" />
</button>
</h4>
</div>
</template>

<script>
import "@h2d2/shopicons/es/regular/refresh";
import "@h2d2/shopicons/es/regular/cablecharge";
import Tooltip from "bootstrap/js/dist/tooltip";
import Modal from "bootstrap/js/dist/modal";
import VehicleIcon from "./VehicleIcon";
import VehicleOptions from "./VehicleOptions.vue";
import CloudOffline from "./MaterialIcon/CloudOffline.vue";
import Sync from "./MaterialIcon/Sync.vue";
import collector from "../mixins/collector";

export default {
name: "VehicleTitle",
components: { VehicleOptions, VehicleIcon },
components: { VehicleOptions, VehicleIcon, Sync, CloudOffline },
mixins: [collector],
props: {
connected: Boolean,
id: [String, Number],
vehicleDetectionActive: Boolean,
vehicleNotReachable: Boolean,
icon: String,
vehicleName: String,
vehicles: { type: Array, default: () => [] },
title: String,
},
data: function () {
return {
refreshTooltip: null,
notReachableTooltip: null,
};
},
emits: ["change-vehicle", "remove-vehicle"],
computed: {
iconType() {
Expand Down Expand Up @@ -93,11 +116,11 @@ export default {
},
watch: {
iconType: function () {
this.tooltip();
this.initTooltip();
},
},
mounted: function () {
this.tooltip();
this.initTooltip();
},
methods: {
changeVehicle(name) {
Expand All @@ -106,21 +129,28 @@ export default {
removeVehicle() {
this.$emit("remove-vehicle");
},
tooltip() {
initTooltip() {
this.$nextTick(() => {
this.refreshTooltip?.dispose();
this.notReachableTooltip?.dispose();
if (this.$refs.refresh) {
new Tooltip(this.$refs.refresh);
this.refreshTooltip = new Tooltip(this.$refs.refresh);
}
if (this.$refs.notReachable) {
this.notReachableTooltip = new Tooltip(this.$refs.notReachable);
}
});
},
openHelpModal() {
const modal = Modal.getOrCreateInstance(document.getElementById("helpModal"));
modal.show();
this.initTooltip();
},
},
};
</script>

<style scoped>
.options {
margin-right: -0.75rem;
}
.vehicle-name {
text-decoration-color: var(--evcc-gray);
}
Expand All @@ -130,16 +160,12 @@ export default {
.spin {
animation: rotation 1s infinite cubic-bezier(0.37, 0, 0.63, 1);
}
.spin :deep(svg) {
/* workaround to fix the not perfectly centered shopicon. Remove once its fixed in @h2d2/shopicons */
transform: translateY(-0.7px);
}
@keyframes rotation {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
transform: rotate(-360deg);
}
}
</style>
22 changes: 22 additions & 0 deletions assets/js/mixins/icon.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export default {
props: {
size: {
type: String,
validator: function (value) {
return ["s", "m", "l", "xl"].includes(value);
},
},
},
computed: {
svgStyle() {
const sizes = {
s: "24px",
m: "32px",
l: "48px",
xl: "64px",
};
const size = sizes[this.size] || sizes.s;
return { display: "block", width: size, height: size };
},
},
};
2 changes: 1 addition & 1 deletion cmd/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ func vehicleInstance(cc config.Named) (api.Vehicle, error) {

// wrap non-config vehicle errors to prevent fatals
log.ERROR.Printf("creating vehicle %s failed: %v", cc.Name, err)
instance = vehicle.NewWrapper(cc.Name, cc.Other, err)
instance = vehicle.NewWrapper(cc.Name, cc.Type, cc.Other, err)
}

// ensure vehicle config has title
Expand Down
1 change: 1 addition & 0 deletions i18n/de.toml
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,7 @@ detectionActive = "Fahrzeugerkennung läuft …"
fallbackName = "Fahrzeug"
moreActions = "Weitere Aktionen"
none = "Kein Fahrzeug"
notReachable = "Fahrzeug war nicht erreichbar. Versuche evcc neu zu starten."
targetSoc = "Ladelimit"
temp = "Temperatur"
tempLimit = "Zieltemp."
Expand Down
1 change: 1 addition & 0 deletions i18n/en.toml
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@ detectionActive = "Detecting vehicle…"
fallbackName = "Vehicle"
moreActions = "More actions"
none = "No vehicle"
notReachable = "Vehicle was not reachable. Try restarting evcc."
targetSoc = "Limit"
temp = "Temp."
tempLimit = "Temp. limit"
Expand Down
54 changes: 54 additions & 0 deletions tests/vehicle-error.evcc.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
interval: 0.1s

site:
title: Vehicle Error
meters:
grid: grid

meters:
- name: grid
type: custom
power:
source: js
script: |
1000
- name: charger_meter
type: custom
power:
source: js
script: |
500

loadpoints:
- title: Carport
charger: charger
meter: charger_meter
vehicle: broken_tesla
mode: now

chargers:
- name: charger
type: custom
enable:
source: js
script:
enabled:
source: js
script: |
true
status:
source: js
script: |
"C"
maxcurrent:
source: js
script: |
16

vehicles:
- name: broken_tesla
type: template
template: tesla # not optimal, since real communication with tesla server is happening
title: Broken Tesla
accessToken: A
refreshToken: B
29 changes: 29 additions & 0 deletions tests/vehicle-error.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
const { test, expect } = require("@playwright/test");
const { start, stop } = require("./evcc");

test.beforeAll(async () => {
await start("vehicle-error.evcc.yaml");
});
test.afterAll(async () => {
await stop();
});

test.beforeEach(async ({ page }) => {
await page.goto("/");
});

test.describe("vehicle startup error", async () => {
test("broken vehicle: normal title and 'not reachable' icon", async ({ page }) => {
await expect(page.getByTestId("vehicle-name")).toHaveText("Broken Tesla");
await expect(page.getByTestId("vehicle-not-reachable-icon")).toBeVisible();
});

test("guest vehicle: normal title and no icon", async ({ page }) => {
// switch to offline vehicle
await page.getByRole("button", { name: "Broken Tesla" }).click();
await page.getByRole("button", { name: "Guest vehicle" }).click();

await expect(page.getByTestId("vehicle-name")).toHaveText("Guest vehicle");
await expect(page.getByTestId("vehicle-not-reachable-icon")).not.toBeVisible();
});
});
Loading
Loading