Compare commits

...

6 commits

Author SHA1 Message Date
Michael Geers
bbb7d704f6
Revert "PV: reserve only min power for higher-priority loadpoints starting up" (#32948)
Some checks failed
Release / Validate Tag (push) Has been cancelled
Release / call-build-workflow (push) Has been cancelled
Release / Publish Docker :release (push) Has been cancelled
Release / Github & APT (push) Has been cancelled
Release / Demo (push) Has been cancelled
Release / Hassio Addon (push) Has been cancelled
Release / Delete Release Branch (push) Has been cancelled
2026-08-19 09:37:21 +02:00
Michael Geers
d284a2f9af
Vehicle: don't instantiate disabled vehicles (#32943) 2026-08-18 11:21:08 +02:00
Michael Geers
96fb302147
chore: add ship-go rollup with mdns re-announcement leak fix (#32941) 2026-08-18 09:51:14 +02:00
Michael Geers
eb836e9ec8
Tesla Powerwall Fleet: fix template not selectable in GUI (#32936) 2026-08-18 08:50:29 +02:00
Michael Geers
52c0cfaa5d
Loadpoint: fix config error for disabled loadpoints (#32923) 2026-08-17 15:25:12 +02:00
Michael Geers
d1b1ded0e6
chore: fix release workflow (#32878)
Some checks failed
Release / Validate Tag (push) Has been cancelled
Release / call-build-workflow (push) Has been cancelled
Release / Publish Docker :release (push) Has been cancelled
Release / Github & APT (push) Has been cancelled
Release / Demo (push) Has been cancelled
Release / Hassio Addon (push) Has been cancelled
Release / Delete Release Branch (push) Has been cancelled
2026-08-16 11:24:40 +02:00
14 changed files with 116 additions and 64 deletions

View file

@ -157,18 +157,15 @@ jobs:
TITLE: ${{ steps.pr.outputs.title }} TITLE: ${{ steps.pr.outputs.title }}
ACTOR: ${{ github.event.comment.user.login }} ACTOR: ${{ github.event.comment.user.login }}
run: | run: |
# the release line the branch was cut from, 0.313 for release/0.313.1
line=$(echo "${TARGET#release/}" | cut -d. -f1,2)
# the pull request is authored by the deploy token, so assign the # the pull request is authored by the deploy token, so assign the
# requester. the body stays empty, squashing it would carry the text # requester. the body stays empty, squashing it would carry the text
# into the release branch as the commit description. # into the release branch as the commit description.
# the marker trails the title so the changelog filters and groups still # the title stays untouched so the changelog filters and groups still
# see the original prefix, goreleaser strips it from the release notes # see the original prefix, the backport label identifies the pull request
url=$(gh pr create \ url=$(gh pr create \
--base "$TARGET" \ --base "$TARGET" \
--head "$BRANCH" \ --head "$BRANCH" \
--title "$TITLE [$line backport]" \ --title "$TITLE" \
--label backport \ --label backport \
--assignee "$ACTOR" \ --assignee "$ACTOR" \
--body "") --body "")

View file

@ -71,8 +71,6 @@ snapshot:
changelog: changelog:
sort: asc sort: asc
# drop the marker the backport command appends to the pull request title
format: '{{ .SHA }} {{ reReplaceAll ` \[\d+\.\d+ backport\]` .Message "" }}'
filters: filters:
exclude: exclude:
- "^chore" - "^chore"

View file

@ -538,9 +538,14 @@ func configureVehicles(static []config.Named, names ...string) error {
} }
} }
instance, err := vehicleInstance(cc) // disabled vehicles are not instantiated
if err != nil { var instance api.Vehicle
return fmt.Errorf("cannot create vehicle '%s': %w", cc.Name, err) if !conf.Disable {
var err error
instance, err = vehicleInstance(cc)
if err != nil {
return fmt.Errorf("cannot create vehicle '%s': %w", cc.Name, err)
}
} }
mu.Lock() mu.Lock()

View file

@ -253,7 +253,8 @@ func NewLoadpointFromConfig(log *util.Logger, settings settings.Settings, collec
} }
lp.defaultVehicle = dev.Instance() lp.defaultVehicle = dev.Instance()
if lp.defaultVehicle == nil { if lp.defaultVehicle == nil {
return lp, errors.New("missing default vehicle instance") // disabled vehicle
lp.log.DEBUG.Printf("default vehicle '%s' is disabled", lp.VehicleRef)
} }
} }
@ -1061,20 +1062,15 @@ func (lp *Loadpoint) charging() bool {
func (lp *Loadpoint) PvChargeStarting() bool { func (lp *Loadpoint) PvChargeStarting() bool {
lp.RLock() lp.RLock()
enabled := lp.enabled enabled := lp.enabled
pvTimer := lp.pvTimer pvTimerRunning := !lp.pvTimer.IsZero()
lp.RUnlock() lp.RUnlock()
if lp.GetMode() != api.ModePV || !lp.connected() || lp.chargeGoalReached(enabled) { if lp.GetMode() != api.ModePV || !lp.connected() || lp.chargeGoalReached(enabled) {
return false return false
} }
if enabled || pvTimer.IsZero() { // enable timer running (not yet enabled)
return false return !enabled && pvTimerRunning
}
// a timer restarting on every surplus dip never starts the loadpoint, hence
// only claim surplus once it has survived half of the enable delay (#32778)
return lp.clock.Since(pvTimer) >= lp.GetEnableDelay()/2
} }
// chargeGoalReached reports whether the loadpoint will not draw more: enabled // chargeGoalReached reports whether the loadpoint will not draw more: enabled

View file

@ -12,7 +12,9 @@ import (
"github.com/evcc-io/evcc/core/soc" "github.com/evcc-io/evcc/core/soc"
"github.com/evcc-io/evcc/messenger" "github.com/evcc-io/evcc/messenger"
"github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/config"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock" "go.uber.org/mock/gomock"
) )
@ -882,3 +884,26 @@ func TestBatteryBoostHold(t *testing.T) {
// which is what keeps the sitePower priority adjustment applied to the loadpoint // which is what keeps the sitePower priority adjustment applied to the loadpoint
assert.NotEqual(t, boostDisabled, lp.GetBatteryBoost(), "hold is active") assert.NotEqual(t, boostDisabled, lp.GetBatteryBoost(), "hold is active")
} }
// default vehicle referencing a disabled vehicle must not fail loadpoint creation
func TestNewLoadpointFromConfigDisabledVehicle(t *testing.T) {
config.Reset()
t.Cleanup(config.Reset)
ctrl := gomock.NewController(t)
// disabled vehicle: device registered without instance
var vehicle api.Vehicle
require.NoError(t, config.Vehicles().Add(config.NewStaticDevice(config.Named{Name: "vehicle"}, vehicle)))
require.NoError(t, config.Chargers().Add(config.NewStaticDevice(config.Named{Name: "charger"}, api.Charger(api.NewMockCharger(ctrl)))))
lp, err := NewLoadpointFromConfig(util.NewLogger("foo"), nil, nil, map[string]any{
"charger": "charger",
"vehicle": "vehicle",
})
require.NoError(t, err)
require.Nil(t, lp.defaultVehicle)
// disabled vehicle is filtered from instances
require.Empty(t, config.Instances(config.Vehicles().Devices()))
}

View file

@ -1127,9 +1127,7 @@ func (site *Site) reservedPVPower(lp updater) float64 {
continue continue
} }
if other.EffectivePriority() > prio && other.PvChargeStarting() { if other.EffectivePriority() > prio && other.PvChargeStarting() {
// min power is what the loadpoint needs to start, reserving its max reserved += other.EffectiveMaxPower()
// power would starve this loadpoint beyond that (#32778)
reserved += other.EffectiveMinPower()
} }
} }

View file

@ -10,10 +10,8 @@ import (
"github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util"
) )
const testEnableDelay = time.Minute
func newPVLoadpoint(prio int, mode api.ChargeMode, status api.ChargeStatus, enabled bool, timer time.Time) *Loadpoint { func newPVLoadpoint(prio int, mode api.ChargeMode, status api.ChargeStatus, enabled bool, timer time.Time) *Loadpoint {
lp := &Loadpoint{ return &Loadpoint{
log: util.NewLogger("lp"), log: util.NewLogger("lp"),
clock: clock.NewMock(), clock: clock.NewMock(),
minCurrent: minA, minCurrent: minA,
@ -25,21 +23,13 @@ func newPVLoadpoint(prio int, mode api.ChargeMode, status api.ChargeStatus, enab
pvTimer: timer, pvTimer: timer,
priority: prio, priority: prio,
} }
lp.Enable.Delay = testEnableDelay
return lp
}
// pvTimerStarted returns a timer start that has been running for the given duration
func pvTimerStarted(running time.Duration) time.Time {
return clock.NewMock().Now().Add(-running)
} }
func TestPvChargeStarting(t *testing.T) { func TestPvChargeStarting(t *testing.T) {
now := clock.NewMock().Now() now := clock.NewMock().Now()
settled := pvTimerStarted(testEnableDelay / 2)
// enable timer running but car already full (soc at default 100% limit): not starting up // enable timer running but car already full (soc at default 100% limit): not starting up
enablePendingFull := newPVLoadpoint(0, api.ModePV, api.StatusB, false, settled) enablePendingFull := newPVLoadpoint(0, api.ModePV, api.StatusB, false, now)
enablePendingFull.vehicleSoc = 100 enablePendingFull.vehicleSoc = 100
tc := []struct { tc := []struct {
@ -47,13 +37,12 @@ func TestPvChargeStarting(t *testing.T) {
lp *Loadpoint lp *Loadpoint
starting bool starting bool
}{ }{
{"enable timer running", newPVLoadpoint(0, api.ModePV, api.StatusB, false, settled), true}, {"enable timer running", newPVLoadpoint(0, api.ModePV, api.StatusB, false, now), true},
{"enable timer just restarted", newPVLoadpoint(0, api.ModePV, api.StatusB, false, now), false},
{"enabled not charging", newPVLoadpoint(0, api.ModePV, api.StatusB, true, time.Time{}), false}, {"enabled not charging", newPVLoadpoint(0, api.ModePV, api.StatusB, true, time.Time{}), false},
{"enabled and charging", newPVLoadpoint(0, api.ModePV, api.StatusC, true, time.Time{}), false}, {"enabled and charging", newPVLoadpoint(0, api.ModePV, api.StatusC, true, time.Time{}), false},
{"disabled idle", newPVLoadpoint(0, api.ModePV, api.StatusB, false, time.Time{}), false}, {"disabled idle", newPVLoadpoint(0, api.ModePV, api.StatusB, false, time.Time{}), false},
{"disconnected", newPVLoadpoint(0, api.ModePV, api.StatusA, false, settled), false}, {"disconnected", newPVLoadpoint(0, api.ModePV, api.StatusA, false, now), false},
{"not pv mode", newPVLoadpoint(0, api.ModeNow, api.StatusB, false, settled), false}, {"not pv mode", newPVLoadpoint(0, api.ModeNow, api.StatusB, false, now), false},
{"enable pending but car full", enablePendingFull, false}, {"enable pending but car full", enablePendingFull, false},
} }
@ -68,7 +57,7 @@ func TestReservedPVPower(t *testing.T) {
Voltage = 230 Voltage = 230
// higher-priority loadpoint (prio 1) starting up // higher-priority loadpoint (prio 1) starting up
high := newPVLoadpoint(1, api.ModePV, api.StatusB, false, pvTimerStarted(testEnableDelay/2)) high := newPVLoadpoint(1, api.ModePV, api.StatusB, false, clock.NewMock().Now())
// lower-priority loadpoint (prio 0) in PV mode // lower-priority loadpoint (prio 0) in PV mode
low := newPVLoadpoint(0, api.ModePV, api.StatusB, false, time.Time{}) low := newPVLoadpoint(0, api.ModePV, api.StatusB, false, time.Time{})
@ -77,20 +66,10 @@ func TestReservedPVPower(t *testing.T) {
loadpoints: []*Loadpoint{high, low}, loadpoints: []*Loadpoint{high, low},
} }
// low reserves the power high needs to start, not its max power (#32778) // low reserves high's anticipated max power while high is starting up
if got, want := site.reservedPVPower(low), high.EffectiveMinPower(); got != want { if got, want := site.reservedPVPower(low), high.EffectiveMaxPower(); got != want {
t.Errorf("low: want %.0f, got %.0f", want, got) t.Errorf("low: want %.0f, got %.0f", want, got)
} }
if high.EffectiveMinPower() == high.EffectiveMaxPower() {
t.Fatal("test requires min and max power to differ")
}
// a timer restarting on every surplus dip must not reserve at all (#32778)
high.pvTimer = clock.NewMock().Now()
if got := site.reservedPVPower(low); got != 0 {
t.Errorf("low while high timer restarts: want 0, got %.0f", got)
}
high.pvTimer = pvTimerStarted(testEnableDelay / 2)
// high (top priority) reserves nothing // high (top priority) reserves nothing
if got := site.reservedPVPower(high); got != 0 { if got := site.reservedPVPower(high); got != 0 {

View file

@ -104,14 +104,7 @@ type vehicles struct {
} }
func (vv *vehicles) Instances() []api.Vehicle { func (vv *vehicles) Instances() []api.Vehicle {
devs := config.Vehicles().Devices() return config.Instances(config.Vehicles().Devices())
res := make([]api.Vehicle, 0, len(devs))
for _, dev := range devs {
res = append(res, dev.Instance())
}
return res
} }
func (vv *vehicles) Settings() []vehicle.API { func (vv *vehicles) Settings() []vehicle.API {
@ -119,6 +112,10 @@ func (vv *vehicles) Settings() []vehicle.API {
res := make([]vehicle.API, 0, len(devs)) res := make([]vehicle.API, 0, len(devs))
for _, dev := range devs { for _, dev := range devs {
// skip disabled vehicles
if dev.Instance() == nil {
continue
}
res = append(res, vehicle.Adapter(vv.log, dev)) res = append(res, vehicle.Adapter(vv.log, dev))
} }

2
go.mod
View file

@ -269,3 +269,5 @@ replace github.com/lorenzodonini/ocpp-go => github.com/evcc-io/ocpp-go v0.0.0-20
replace github.com/enbility/spine-go => github.com/andig/spine-go v0.7.1-0.20260804073026-f9a8d8e8fd18 replace github.com/enbility/spine-go => github.com/andig/spine-go v0.7.1-0.20260804073026-f9a8d8e8fd18
replace github.com/enbility/eebus-go => github.com/andig/eebus-go v0.0.0-20260725155950-e735091ff165 replace github.com/enbility/eebus-go => github.com/andig/eebus-go v0.0.0-20260725155950-e735091ff165
replace github.com/enbility/ship-go => github.com/andig/ship-go v0.6.1-0.20260818072438-bade5ec9fbd7

4
go.sum
View file

@ -30,6 +30,8 @@ github.com/andig/gosunspec v0.0.0-20260705113727-6d585e133512 h1:1y8dS4GaBB9WUu/
github.com/andig/gosunspec v0.0.0-20260705113727-6d585e133512/go.mod h1:c6P6szcR+ROkqZruOR4f6qbDKFjZX6OitPpj+yJ/r8k= github.com/andig/gosunspec v0.0.0-20260705113727-6d585e133512/go.mod h1:c6P6szcR+ROkqZruOR4f6qbDKFjZX6OitPpj+yJ/r8k=
github.com/andig/mbserver v0.0.0-20230310211055-1d29cbb5820e h1:m/NTP3JWpR7M0ljLxiQU4fzR25jjhe1LDtxLMNcoNJQ= github.com/andig/mbserver v0.0.0-20230310211055-1d29cbb5820e h1:m/NTP3JWpR7M0ljLxiQU4fzR25jjhe1LDtxLMNcoNJQ=
github.com/andig/mbserver v0.0.0-20230310211055-1d29cbb5820e/go.mod h1:4VtYzTm//oUipwvO3yh0g/udTE7pYJM+U/kyAuFDsgM= github.com/andig/mbserver v0.0.0-20230310211055-1d29cbb5820e/go.mod h1:4VtYzTm//oUipwvO3yh0g/udTE7pYJM+U/kyAuFDsgM=
github.com/andig/ship-go v0.6.1-0.20260818072438-bade5ec9fbd7 h1:YXXxRvtVSTLATXSvMYBtf8PBNeVRBu7WzxfL4PCqN+8=
github.com/andig/ship-go v0.6.1-0.20260818072438-bade5ec9fbd7/go.mod h1:EvRFx83phCwPQrOuWw2CX5t0c6mcFVzIk6gQEz4Lijg=
github.com/andig/spine-go v0.7.1-0.20260804073026-f9a8d8e8fd18 h1:FVKqjwJJLo8vV47jXt1lNN4EVJXCyV3LSTNFt9eeimU= github.com/andig/spine-go v0.7.1-0.20260804073026-f9a8d8e8fd18 h1:FVKqjwJJLo8vV47jXt1lNN4EVJXCyV3LSTNFt9eeimU=
github.com/andig/spine-go v0.7.1-0.20260804073026-f9a8d8e8fd18/go.mod h1:ddWZU5BQGyzRGF20Z7rUKFFzbCp+cfu9mZgYtOMW/fM= github.com/andig/spine-go v0.7.1-0.20260804073026-f9a8d8e8fd18/go.mod h1:ddWZU5BQGyzRGF20Z7rUKFFzbCp+cfu9mZgYtOMW/fM=
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
@ -140,8 +142,6 @@ github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2I
github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU= github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU=
github.com/enbility/go-avahi v0.0.0-20240909195612-d5de6b280d7a h1:foChWb8lhzqa6lWDRs6COYMdp649YlUirFP8GqoT0JQ= github.com/enbility/go-avahi v0.0.0-20240909195612-d5de6b280d7a h1:foChWb8lhzqa6lWDRs6COYMdp649YlUirFP8GqoT0JQ=
github.com/enbility/go-avahi v0.0.0-20240909195612-d5de6b280d7a/go.mod h1:H64mhYcAQUGUUnVqMdZQf93kPecH4M79xwH95Lddt3U= github.com/enbility/go-avahi v0.0.0-20240909195612-d5de6b280d7a/go.mod h1:H64mhYcAQUGUUnVqMdZQf93kPecH4M79xwH95Lddt3U=
github.com/enbility/ship-go v0.6.1-0.20260720110450-0aa90f64ac76 h1:gDrV6vBG3luAgsyX4sXYShITT41CDKXrtyluLeRmglE=
github.com/enbility/ship-go v0.6.1-0.20260720110450-0aa90f64ac76/go.mod h1:EvRFx83phCwPQrOuWw2CX5t0c6mcFVzIk6gQEz4Lijg=
github.com/enbility/zeroconf/v2 v2.0.0-20240920094356-be1cae74fda6 h1:XOYvxKtT1oxT37w/5oEiRLuPbm9FuJPt3fiYhX0h8Po= github.com/enbility/zeroconf/v2 v2.0.0-20240920094356-be1cae74fda6 h1:XOYvxKtT1oxT37w/5oEiRLuPbm9FuJPt3fiYhX0h8Po=
github.com/enbility/zeroconf/v2 v2.0.0-20240920094356-be1cae74fda6/go.mod h1:BszP9qFV14mPXgyIREbgIdQtWxbAj3OKqvK02HihMoM= github.com/enbility/zeroconf/v2 v2.0.0-20240920094356-be1cae74fda6/go.mod h1:BszP9qFV14mPXgyIREbgIdQtWxbAj3OKqvK02HihMoM=
github.com/evcc-io/modbus v0.0.0-20250501165638-8b6f1fbdb7ea h1:F6eyC8V8wvc3ranlsR4coAls+OPBkVvxyX0a2VqdlPc= github.com/evcc-io/modbus v0.0.0-20250501165638-8b6f1fbdb7ea h1:F6eyC8V8wvc3ranlsR4coAls+OPBkVvxyX0a2VqdlPc=

View file

@ -13,6 +13,7 @@ import (
"github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/config" "github.com/evcc-io/evcc/util/config"
"github.com/evcc-io/evcc/util/templates" "github.com/evcc-io/evcc/util/templates"
"github.com/go-viper/mapstructure/v2"
"github.com/gorilla/mux" "github.com/gorilla/mux"
) )
@ -119,8 +120,9 @@ func loadpointConfig(dev config.Device[loadpoint.API]) (loadpointFullConfig, err
return loadpointFullConfig{}, err return loadpointFullConfig{}, err
} }
// non-strict: runtime settings (mode, ...) are persisted alongside the config
var static loadpoint.StaticConfig var static loadpoint.StaticConfig
if err := util.DecodeOther(staticMap, &static); err != nil { if err := mapstructure.Decode(staticMap, &static); err != nil {
return loadpointFullConfig{}, err return loadpointFullConfig{}, err
} }

View file

@ -25,6 +25,7 @@ func TestLoadpointConfigDisabledNilInstance(t *testing.T) {
"charger": "wallbox", "charger": "wallbox",
"meter": "lp-meter", "meter": "lp-meter",
"title": "Garage", "title": "Garage",
"mode": "pv", // runtime setting persisted by the settings adapter
}, },
} }

View file

@ -19,6 +19,8 @@ requirements:
The [myteslamate.com](https://www.myteslamate.com/tesla-api-application-registration/) guide explains the process and generates a free Access and Refresh Token. The [myteslamate.com](https://www.myteslamate.com/tesla-api-application-registration/) guide explains the process and generates a free Access and Refresh Token.
With this token pair and your Client ID created in the Tesla Developer Account, evcc can directly communicate with the Tesla Fleet API. With this token pair and your Client ID created in the Tesla Developer Account, evcc can directly communicate with the Tesla Fleet API.
params: params:
- name: usage
choice: ["battery"]
- name: host - name: host
- name: password - name: password
required: true required: true

View file

@ -59,6 +59,21 @@ test.describe("disable / enable", async () => {
await page.reload(); await page.reload();
await createLoadpoint(page, "Garage"); await createLoadpoint(page, "Garage");
// restart to bring both loadpoints live
await restart();
await page.reload();
// switch charge mode on dashboard; runtime setting is persisted next to the loadpoint config
await page.goto("/");
const modeButton = page
.getByTestId("loadpoint")
.nth(0)
.getByTestId("mode")
.getByRole("button", { name: "Fast" });
await modeButton.click();
await expect(modeButton).toHaveClass(/active/);
await page.goto("/#/config");
const target = page.getByTestId("loadpoint").nth(0); const target = page.getByTestId("loadpoint").nth(0);
await toggleLoadpointDisable(page, 0, "Disable"); await toggleLoadpointDisable(page, 0, "Disable");
@ -66,10 +81,11 @@ test.describe("disable / enable", async () => {
// card shows disabled state // card shows disabled state
await expect(disabledBadge(target)).toBeVisible(); await expect(disabledBadge(target)).toBeVisible();
// restart, no fatal // restart, no fatal, both loadpoints still listed
await restart(); await restart();
await page.reload(); await page.reload();
await expectNoFatal(page); await expectNoFatal(page);
await expect(page.getByTestId("loadpoint")).toHaveCount(2);
await expect(disabledBadge(target)).toBeVisible(); await expect(disabledBadge(target)).toBeVisible();
// re-enable by clicking the disabled card // re-enable by clicking the disabled card
@ -223,6 +239,10 @@ test.describe("disable / enable", async () => {
await start(); await start();
await page.goto("/#/config"); await page.goto("/#/config");
// loadpoint so the vehicle shows up on the dashboard
await createLoadpoint(page, "Carport");
await page.reload();
// add user-defined vehicle // add user-defined vehicle
await page.getByTestId("add-vehicle").click(); await page.getByTestId("add-vehicle").click();
const modal = page.getByTestId("vehicle-modal"); const modal = page.getByTestId("vehicle-modal");
@ -261,6 +281,23 @@ soc:
await expectNoFatal(page); await expectNoFatal(page);
await expect(disabledBadge(vehicleCard)).toBeVisible(); await expect(disabledBadge(vehicleCard)).toBeVisible();
// disabled vehicle is hidden from main ui
await page.goto("/");
await expect(page.getByTestId("loadpoint")).toBeVisible();
await expect(
page.getByTestId("change-vehicle").getByRole("option", { name: "blue Honda" })
).toHaveCount(0);
// no vehicles entry in more menu
const moreTab = page.getByTestId("tab-more");
await moreTab.click();
await expect(moreTab.getByRole("button", { name: "User Interface" })).toBeVisible();
await expect(moreTab.getByRole("button", { name: "Vehicles" })).not.toBeVisible();
// close more menu
await moreTab.click();
await page.goto("/#/config");
// re-enable by clicking the disabled card // re-enable by clicking the disabled card
await disabledBadge(vehicleCard).click(); await disabledBadge(vehicleCard).click();
await expect(disabledBadge(vehicleCard)).toHaveCount(0); await expect(disabledBadge(vehicleCard)).toHaveCount(0);
@ -270,6 +307,19 @@ soc:
await page.reload(); await page.reload();
await expectNoFatal(page); await expectNoFatal(page);
await expect(vehicleCard).toContainText("blue Honda"); await expect(vehicleCard).toContainText("blue Honda");
// vehicle is selectable on the dashboard again
await page.goto("/");
await expect(
page.getByTestId("change-vehicle").getByRole("option", { name: "blue Honda" })
).toHaveCount(1);
// vehicle is back in the more menu vehicles modal
await moreTab.click();
await moreTab.getByRole("button", { name: "Vehicles" }).click();
const settingsModal = page.getByTestId("vehicle-settings-modal");
await expectModalVisible(settingsModal);
await expect(settingsModal.getByRole("group", { name: "blue Honda" })).toBeVisible();
}); });
}); });