parent
ecaa0e6068
commit
b399931c4c
15 changed files with 108 additions and 45 deletions
|
|
@ -1,7 +1,6 @@
|
|||
<template>
|
||||
<button
|
||||
class="root d-flex align-items-center justify-content-center"
|
||||
:class="{ attention }"
|
||||
tabindex="0"
|
||||
@click="$emit('click')"
|
||||
>
|
||||
|
|
@ -17,7 +16,6 @@ export default {
|
|||
name: "NewDeviceButton",
|
||||
props: {
|
||||
title: String,
|
||||
attention: Boolean,
|
||||
},
|
||||
emits: ["click"],
|
||||
};
|
||||
|
|
@ -40,25 +38,4 @@ export default {
|
|||
border-color: var(--evcc-default-text);
|
||||
color: var(--evcc-default-text);
|
||||
}
|
||||
.attention {
|
||||
animation: wiggle 3s cubic-bezier(0.36, 0.07, 0.19, 0.97) infinite;
|
||||
}
|
||||
|
||||
@keyframes wiggle {
|
||||
0%,
|
||||
100% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
5%,
|
||||
15% {
|
||||
transform: rotate(-2deg);
|
||||
}
|
||||
10%,
|
||||
20% {
|
||||
transform: rotate(2deg);
|
||||
}
|
||||
25% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -13,3 +13,9 @@ export default {
|
|||
components: { Markdown },
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.alert {
|
||||
margin-bottom: 5rem;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
<template>
|
||||
<div
|
||||
class="energyflow cursor-pointer position-relative"
|
||||
:class="{ 'energyflow--open': detailsOpen }"
|
||||
class="energyflow position-relative"
|
||||
:class="{
|
||||
'energyflow--open': detailsOpen || detailsAlwaysOpen,
|
||||
'cursor-pointer': !detailsAlwaysOpen,
|
||||
}"
|
||||
data-testid="energyflow"
|
||||
@click="toggleDetails"
|
||||
>
|
||||
|
|
@ -445,7 +448,13 @@ export default defineComponent({
|
|||
outPower() {
|
||||
return this.homePower + this.loadpointsPower + this.pvExport + this.batteryCharge;
|
||||
},
|
||||
detailsAlwaysOpen() {
|
||||
return this.loadpoints.length === 0;
|
||||
},
|
||||
detailsHeight() {
|
||||
if (this.detailsAlwaysOpen) {
|
||||
return "auto";
|
||||
}
|
||||
return this.detailsOpen ? this.detailsCompleteHeight + "px" : 0;
|
||||
},
|
||||
batteryFmt() {
|
||||
|
|
@ -545,6 +554,7 @@ export default defineComponent({
|
|||
},
|
||||
mounted() {
|
||||
window.addEventListener("resize", this.updateHeight);
|
||||
|
||||
// height must be calculated in case of initially open details
|
||||
if (settings.energyflowDetails) {
|
||||
this.toggleDetails();
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
<template>
|
||||
<div
|
||||
class="container container--loadpoint px-0 mb-md-2 d-flex flex-column justify-content-center"
|
||||
data-testid="loadpoints"
|
||||
>
|
||||
<div
|
||||
v-if="loadpoints.length > 0"
|
||||
ref="carousel"
|
||||
class="carousel d-lg-flex flex-wrap"
|
||||
:class="[`carousel--${loadpoints.length}`, { 'carousel--fullwidth': fullWidth }]"
|
||||
|
|
@ -186,7 +188,7 @@ export default defineComponent({
|
|||
});
|
||||
</script>
|
||||
<style scoped>
|
||||
.container--loadpoint {
|
||||
.container--loadpoint:not(:empty) {
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<Loadpoints
|
||||
v-else-if="loadpoints.length > 0"
|
||||
v-else
|
||||
class="mt-1 mt-sm-2 flex-grow-1"
|
||||
:loadpoints="orderedVisibleLoadpoints"
|
||||
:vehicles="vehicleList"
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@
|
|||
@site-changed="siteChanged"
|
||||
/>
|
||||
|
||||
<WelcomeBanner v-if="setupRequired" />
|
||||
<h2 class="my-4">{{ $t("config.section.loadpoints") }}</h2>
|
||||
<WelcomeBanner v-if="loadpointsRequired" class="my-4" />
|
||||
<div class="p-0 config-list">
|
||||
<DeviceCard
|
||||
v-for="loadpoint in loadpoints"
|
||||
|
|
@ -48,7 +48,6 @@
|
|||
<NewDeviceButton
|
||||
data-testid="add-loadpoint"
|
||||
:title="$t('config.main.addLoadpoint')"
|
||||
:attention="loadpointsRequired"
|
||||
@click="newLoadpoint"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -571,8 +570,8 @@ export default defineComponent({
|
|||
authProviders() {
|
||||
return store.state?.authProviders;
|
||||
},
|
||||
loadpointsRequired() {
|
||||
return this.loadpoints.length === 0;
|
||||
setupRequired() {
|
||||
return store.state?.setupRequired;
|
||||
},
|
||||
siteTitle() {
|
||||
return this.site?.title;
|
||||
|
|
|
|||
|
|
@ -279,7 +279,7 @@ func runRoot(cmd *cobra.Command, args []string) {
|
|||
// signal devices initialized
|
||||
valueChan <- util.Param{Key: keys.StartupCompleted, Val: true}
|
||||
// show onboarding UI
|
||||
valueChan <- util.Param{Key: keys.SetupRequired, Val: site == nil || len(site.Loadpoints()) == 0}
|
||||
valueChan <- util.Param{Key: keys.SetupRequired, Val: site == nil || !site.IsConfigured()}
|
||||
|
||||
// setup mqtt publisher
|
||||
if err == nil && conf.Mqtt.Broker != "" && conf.Mqtt.Topic != "" {
|
||||
|
|
|
|||
27
core/site.go
27
core/site.go
|
|
@ -957,7 +957,7 @@ func (site *Site) update(lp updater) {
|
|||
|
||||
// prioritize if possible
|
||||
var flexiblePower float64
|
||||
if lp.GetMode() == api.ModePV {
|
||||
if lp != nil && lp.GetMode() == api.ModePV {
|
||||
flexiblePower = site.prioritizer.GetChargePowerFlexibility(lp)
|
||||
}
|
||||
|
||||
|
|
@ -978,10 +978,12 @@ func (site *Site) update(lp updater) {
|
|||
greenShareLoadpoints := site.greenShare(nonChargePower, nonChargePower+totalChargePower)
|
||||
|
||||
// TODO
|
||||
lp.Update(
|
||||
sitePower, max(0, site.batteryPower), consumption, feedin, batteryBuffered, batteryStart,
|
||||
greenShareLoadpoints, site.effectivePrice(greenShareLoadpoints), site.effectiveCo2(greenShareLoadpoints),
|
||||
)
|
||||
if lp != nil {
|
||||
lp.Update(
|
||||
sitePower, max(0, site.batteryPower), consumption, feedin, batteryBuffered, batteryStart,
|
||||
greenShareLoadpoints, site.effectivePrice(greenShareLoadpoints), site.effectiveCo2(greenShareLoadpoints),
|
||||
)
|
||||
}
|
||||
|
||||
site.Health.Update()
|
||||
|
||||
|
|
@ -1097,9 +1099,18 @@ func (site *Site) Prepare(valueChan chan<- util.Param, pushChan chan<- push.Even
|
|||
|
||||
// loopLoadpoints keeps iterating across loadpoints sending the next to the given channel
|
||||
func (site *Site) loopLoadpoints(next chan<- updater) {
|
||||
var logOnce sync.Once
|
||||
|
||||
for {
|
||||
for _, lp := range site.loadpoints {
|
||||
next <- lp
|
||||
if len(site.loadpoints) == 0 {
|
||||
logOnce.Do(func() {
|
||||
site.log.INFO.Println("no loadpoints configured, running in meter-only mode")
|
||||
})
|
||||
next <- nil
|
||||
} else {
|
||||
for _, lp := range site.loadpoints {
|
||||
next <- lp
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1114,7 +1125,7 @@ func (site *Site) Run(stopC chan struct{}, interval time.Duration) {
|
|||
}
|
||||
|
||||
loadpointChan := make(chan updater)
|
||||
if len(site.loadpoints) > 0 {
|
||||
if site.IsConfigured() {
|
||||
go site.loopLoadpoints(loadpointChan)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -137,6 +137,14 @@ func (site *Site) Loadpoints() []loadpoint.API {
|
|||
return lo.Map(site.loadpoints, func(lp *Loadpoint, _ int) loadpoint.API { return lp })
|
||||
}
|
||||
|
||||
func (site *Site) hasMeters() bool {
|
||||
return site.gridMeter != nil || len(site.pvMeters) > 0 || len(site.batteryMeters) > 0 || len(site.auxMeters) > 0 || len(site.extMeters) > 0
|
||||
}
|
||||
|
||||
func (site *Site) IsConfigured() bool {
|
||||
return len(site.loadpoints) > 0 || site.hasMeters()
|
||||
}
|
||||
|
||||
// loadpointsAsCircuitDevices returns the loadpoints as circuit devices
|
||||
func (site *Site) loadpointsAsCircuitDevices() []api.CircuitLoad {
|
||||
return lo.Map(site.loadpoints, func(lp *Loadpoint, _ int) api.CircuitLoad { return lp })
|
||||
|
|
|
|||
|
|
@ -387,7 +387,7 @@
|
|||
"title": "Konfiguration",
|
||||
"unconfigured": "nicht konfiguriert",
|
||||
"vehicles": "Meine Fahrzeuge",
|
||||
"welcomeBannerText": "Lege zunächst eine **Wallbox oder Heizung** an. Falls du noch keine echte Wallbox hast, wähle eine **Demowallbox**.",
|
||||
"welcomeBannerText": "Lege zunächst eine **Wallbox**, **Heizung**, **Netzzähler**, **Solar**, **Batterie** oder **zusätzlichen Zähler** an. Falls du nur testen möchtest, wähle ein **Demogerät**.",
|
||||
"welcomeBannerTitle": "Los geht's mit der Konfiguration!",
|
||||
"yaml": "Geräte aus evcc.yaml sind nicht editierbar."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -387,7 +387,7 @@
|
|||
"title": "Configuration",
|
||||
"unconfigured": "not configured",
|
||||
"vehicles": "My Vehicles",
|
||||
"welcomeBannerText": "Start with creating a **charger or heater**. If you don't have a real charger yet, choose a **demo charger**.",
|
||||
"welcomeBannerText": "Start with creating at least one **charger**, **heater**, **grid**, **solar**, **battery** or **additional meter**. If you're just want to test, pick a **demo device**.",
|
||||
"welcomeBannerTitle": "Let's configure your system!",
|
||||
"yaml": "Devices from evcc.yaml are not editable."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -196,9 +196,14 @@ test.describe("backup and restore", async () => {
|
|||
await expect(page.getByTestId("offline-indicator")).toHaveAttribute("aria-hidden", "false");
|
||||
await restart(undefined, undefined, true);
|
||||
await expect(page.getByTestId("offline-indicator")).toHaveAttribute("aria-hidden", "true");
|
||||
await page.getByRole("link", { name: "Let's start configuration" }).click();
|
||||
|
||||
// verify initial state
|
||||
// redirect to main ui with initial title
|
||||
await expect(
|
||||
page.getByTestId("header").getByRole("heading", { name: initialTitle })
|
||||
).toBeVisible();
|
||||
|
||||
// verify initial state in config ui
|
||||
await page.goto("/#/config");
|
||||
await expect(page.getByTestId("grid")).toBeVisible();
|
||||
await expect(page.getByTestId("generalconfig-title")).toContainText(initialTitle);
|
||||
await stop();
|
||||
|
|
|
|||
42
tests/config-meter-only.spec.ts
Normal file
42
tests/config-meter-only.spec.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
import { start, stop, restart, baseUrl } from "./evcc";
|
||||
import { expectModalHidden, expectModalVisible } from "./utils";
|
||||
|
||||
test.use({ baseURL: baseUrl() });
|
||||
|
||||
test.beforeAll(async () => {
|
||||
await start();
|
||||
});
|
||||
test.afterAll(async () => {
|
||||
await stop();
|
||||
});
|
||||
|
||||
test.describe("meter only", async () => {
|
||||
test("run without loadpoints", async ({ page }) => {
|
||||
await page.goto("/#/config");
|
||||
|
||||
await expect(page.getByTestId("welcome-banner")).toContainText(
|
||||
"Start with creating at least one"
|
||||
);
|
||||
|
||||
await page.getByRole("button", { name: "Add grid meter" }).click();
|
||||
const gridModal = page.getByTestId("meter-modal");
|
||||
await expectModalVisible(gridModal);
|
||||
await gridModal.getByLabel("Manufacturer").selectOption("Demo meter");
|
||||
await gridModal.getByLabel("Power").fill("1000");
|
||||
await gridModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(gridModal);
|
||||
|
||||
await restart();
|
||||
|
||||
await expect(page.getByTestId("welcome-banner")).not.toBeVisible();
|
||||
await expect(page.getByTestId("grid").getByTestId("device-tag-power")).toContainText("1.0 kW");
|
||||
|
||||
await page.getByTestId("home-link").click();
|
||||
|
||||
await expect(page.locator("body")).not.toContainText("Hello aboard!");
|
||||
await expect(page.getByTestId("energyflow")).toBeVisible();
|
||||
await expect(page.getByTestId("energyflow-entry-gridimport")).toContainText("1.0 kW");
|
||||
await expect(page.getByTestId("loadpoints")).toBeEmpty();
|
||||
});
|
||||
});
|
||||
|
|
@ -58,6 +58,9 @@ test.describe("onboarding", async () => {
|
|||
await expectModalVisible(lpModal);
|
||||
await lpModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(lpModal);
|
||||
|
||||
await restart();
|
||||
|
||||
await expect(page.getByTestId("welcome-banner")).not.toBeVisible();
|
||||
|
||||
// create grid meter
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ async function _start(config?: string, flags: string | string[] = []) {
|
|||
steamLog.end();
|
||||
});
|
||||
try {
|
||||
await waitOn({ resources: [baseUrl()], log: LOG_ENABLED, timeout: 50000 });
|
||||
await waitOn({ resources: [baseUrl()], log: LOG_ENABLED, timeout: 90000 });
|
||||
} catch (error) {
|
||||
instance.kill("SIGKILL");
|
||||
console.error(logPrefix(), `evcc startup failed: ${error}`);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue