Skip to content

Commit

Permalink
Icon for unreachable online vehicle; better autodetect
Browse files Browse the repository at this point in the history
  • Loading branch information
naltatis committed Mar 13, 2024
1 parent e392399 commit cc5d8d6
Show file tree
Hide file tree
Showing 12 changed files with 201 additions and 21 deletions.
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 };
},
},
};
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();
});
});
2 changes: 1 addition & 1 deletion vehicle/wrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ var _ api.Vehicle = (*Wrapper)(nil)

// SetTitle implements the api.TitleSetter interface
func (v *Wrapper) SetTitle(title string) {
v.Title_ = fmt.Sprintf("%s (unavailable)", title)
v.Title_ = title
}

var _ api.Battery = (*Wrapper)(nil)
Expand Down

0 comments on commit cc5d8d6

Please sign in to comment.