-
+ data-bs-toggle="tooltip"
+ >
+
+
-
+
{{ name }}
-
+
{{ name }}
+
diff --git a/assets/js/mixins/icon.js b/assets/js/mixins/icon.js
new file mode 100644
index 0000000000..8b997cab24
--- /dev/null
+++ b/assets/js/mixins/icon.js
@@ -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 };
+ },
+ },
+};
diff --git a/cmd/setup.go b/cmd/setup.go
index edc4752469..1447020049 100644
--- a/cmd/setup.go
+++ b/cmd/setup.go
@@ -295,7 +295,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
diff --git a/i18n/de.toml b/i18n/de.toml
index ae46813769..5383391b01 100644
--- a/i18n/de.toml
+++ b/i18n/de.toml
@@ -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."
diff --git a/i18n/en.toml b/i18n/en.toml
index 64126f8c27..e67191163e 100644
--- a/i18n/en.toml
+++ b/i18n/en.toml
@@ -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"
diff --git a/tests/vehicle-error.evcc.yaml b/tests/vehicle-error.evcc.yaml
new file mode 100755
index 0000000000..bcce474b8c
--- /dev/null
+++ b/tests/vehicle-error.evcc.yaml
@@ -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
diff --git a/tests/vehicle-error.spec.js b/tests/vehicle-error.spec.js
new file mode 100644
index 0000000000..e329d3190c
--- /dev/null
+++ b/tests/vehicle-error.spec.js
@@ -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();
+ });
+});
diff --git a/vehicle/wrapper.go b/vehicle/wrapper.go
index d92eb0a03f..6727185b26 100644
--- a/vehicle/wrapper.go
+++ b/vehicle/wrapper.go
@@ -11,11 +11,13 @@ import (
// Wrapper wraps an api.Vehicle to capture initialization errors
type Wrapper struct {
embed
- err error
+ typ string
+ config map[string]interface{}
+ err error
}
// NewWrapper creates an offline Vehicle wrapper
-func NewWrapper(name string, other map[string]interface{}, err error) api.Vehicle {
+func NewWrapper(name string, typ string, other map[string]interface{}, err error) api.Vehicle {
var cc struct {
embed `mapstructure:",squash"`
Other map[string]interface{} `mapstructure:",remain"`
@@ -30,11 +32,13 @@ func NewWrapper(name string, other map[string]interface{}, err error) api.Vehicl
}
v := &Wrapper{
- embed: cc.embed,
- err: fmt.Errorf("vehicle not available: %w", err),
+ embed: cc.embed,
+ typ: typ,
+ config: cc.Other,
+ err: fmt.Errorf("vehicle not available: %w", err),
}
- v.Features_ = append(v.Features_, api.Offline)
+ v.Features_ = append(v.Features_, api.Offline, api.Retryable)
v.SetTitle(cc.Title_)
return v
@@ -45,11 +49,16 @@ func (v *Wrapper) Error() string {
return v.err.Error()
}
+// Error returns the initialization error
+func (v *Wrapper) Config() (string, map[string]interface{}) {
+ return v.typ, v.config
+}
+
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)