From 8c68689c1d06cc655f6cca94a1867d4f7cadbe96 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Thu, 17 Aug 2023 19:28:06 +0200 Subject: [PATCH] Fix resetOnDisconnect overrides vehicle settings (BC) (#9370) --- README.md | 15 + core/loadpoint.go | 6 - core/loadpoint_vehicle_test.go | 4 - package-lock.json | 187 +++++++++++ package.json | 4 +- tests/evcc.js | 1 + .../persist-target-minsoc-connected.evcc.yaml | 58 ---- ...rsist-target-minsoc-disconnected.evcc.yaml | 58 ---- tests/persist-target-minsoc.spec.js | 73 ++++- tests/simulator.evcc.yaml | 99 ++++++ tests/simulator.js | 28 ++ tests/simulator/api.js | 37 +++ tests/simulator/index.html | 15 + tests/simulator/src/Simulator.vue | 297 ++++++++++++++++++ tests/simulator/src/main.js | 6 + tests/simulator/vite.config.js | 8 + 16 files changed, 752 insertions(+), 144 deletions(-) delete mode 100755 tests/persist-target-minsoc-connected.evcc.yaml delete mode 100755 tests/persist-target-minsoc-disconnected.evcc.yaml create mode 100755 tests/simulator.evcc.yaml create mode 100644 tests/simulator.js create mode 100644 tests/simulator/api.js create mode 100644 tests/simulator/index.html create mode 100644 tests/simulator/src/Simulator.vue create mode 100644 tests/simulator/src/main.js create mode 100644 tests/simulator/vite.config.js diff --git a/README.md b/README.md index 1aca7472f..078929081 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,21 @@ make ui build npm run playwright ``` +#### Simulating device state + +Since we dont want to run tests agains real devices or cloud services we've build a simple simulator that lets you emulated meters, vehicles and loadpoints. The simulators web interface runs on http://localhost:7072. + +``` +npm run simulator +``` + +Run an evcc instance that uses simulator data. This configuration runs with a very high refresh interval to seed up testing. + +``` +make ui build +./evcc --config tests/simulator.evcc.yaml +``` + ### Code formatting We use linters (golangci-lint, Prettier) to keep a coherent source code formatting. It's recommended to use the format-on-save feature of your editor. For VSCode use the [Go](https://marketplace.visualstudio.com/items?itemName=golang.Go), [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) and [Veture](https://marketplace.visualstudio.com/items?itemName=octref.vetur) extension. You can manually reformat your code by running: diff --git a/core/loadpoint.go b/core/loadpoint.go index 7b4e2306b..bf506b4c9 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -551,12 +551,6 @@ func (lp *Loadpoint) applyAction(actionCfg api.ActionConfig) { if max := actionCfg.MaxCurrent; max != nil && *max <= *lp.onDisconnect.MaxCurrent { lp.SetMaxCurrent(*max) } - if actionCfg.MinSoc != nil { - lp.SetMinSoc(*actionCfg.MinSoc) - } - if actionCfg.TargetSoc != nil { - lp.SetTargetSoc(*actionCfg.TargetSoc) - } if actionCfg.Priority != nil { lp.SetPriority(*actionCfg.Priority) } diff --git a/core/loadpoint_vehicle_test.go b/core/loadpoint_vehicle_test.go index 2ee2ba562..33b240bfd 100644 --- a/core/loadpoint_vehicle_test.go +++ b/core/loadpoint_vehicle_test.go @@ -211,8 +211,6 @@ func TestDefaultVehicle(t *testing.T) { lp.ResetOnDisconnect = true lp.evVehicleDisconnectHandler() assert.Equal(t, mode, lp.GetMode(), "mode") - assert.Equal(t, minsoc, lp.GetMinSoc(), "minsoc") - assert.Equal(t, targetsoc, lp.GetTargetSoc(), "targetsoc") assert.Equal(t, lp.onDisconnect, onDisconnect, "ondisconnect must remain untouched") // set non-default vehicle during disconnect - should be default on connect @@ -243,8 +241,6 @@ func TestApplyVehicleDefaults(t *testing.T) { assert.Equal(t, *conf.Mode, lp.Mode) assert.Equal(t, *conf.MinCurrent, lp.MinCurrent) assert.Equal(t, *conf.MaxCurrent, lp.MaxCurrent) - assert.Equal(t, *conf.MinSoc, lp.Soc.min) - assert.Equal(t, *conf.TargetSoc, lp.Soc.target) } // onIdentified config diff --git a/package-lock.json b/package-lock.json index 852c27d28..5d9c4f80d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@vue/compiler-sfc": "^3.2.30", "@vue/test-utils": "^2.2.7", "axios": "^1.2.1", + "body-parser": "^1.20.2", "bootstrap": "^5.2.2", "canvas-confetti": "^1.4.0", "countup.js": "^2.3.2", @@ -3137,6 +3138,64 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/body-parser": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/body-parser/node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -3241,6 +3300,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -3500,6 +3567,14 @@ "upper-case": "^2.0.2" } }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", @@ -3710,6 +3785,23 @@ "node": ">=0.4.0" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, "node_modules/diacritics": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/diacritics/-/diacritics-1.3.0.tgz", @@ -5120,6 +5212,29 @@ "node": ">=12" } }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/http-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", @@ -5886,6 +6001,14 @@ "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==" }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -6531,6 +6654,20 @@ "node": ">=6" } }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", @@ -6555,6 +6692,31 @@ } ] }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -7003,6 +7165,11 @@ "upper-case-first": "^2.0.2" } }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -7390,6 +7557,14 @@ "node": ">=8.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, "node_modules/toml-eslint-parser": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/toml-eslint-parser/-/toml-eslint-parser-0.4.0.tgz", @@ -7502,6 +7677,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typed-array-length": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", diff --git a/package.json b/package.json index 8aaf8cc84..f14e2cc11 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "lint": "prettier assets './**/*.{yml,yaml}' --write && eslint assets/**/*.js assets/**/*.vue --fix", "dev": "vite", "histoire": "histoire dev", - "playwright": "playwright test --ui" + "playwright": "playwright test --ui", + "simulator": "vite tests/simulator" }, "main": "index.js", "dependencies": { @@ -22,6 +23,7 @@ "@vue/compiler-sfc": "^3.2.30", "@vue/test-utils": "^2.2.7", "axios": "^1.2.1", + "body-parser": "^1.20.2", "bootstrap": "^5.2.2", "canvas-confetti": "^1.4.0", "countup.js": "^2.3.2", diff --git a/tests/evcc.js b/tests/evcc.js index d767fce53..2f00bce18 100644 --- a/tests/evcc.js +++ b/tests/evcc.js @@ -42,6 +42,7 @@ async function _start(config) { console.log("starting evcc", { config }); const instance = exec(`EVCC_DATABASE_DSN=${DB_PATH} ${BINARY} --config tests/${config}`); instance.stdout.pipe(process.stdout); + instance.stderr.pipe(process.stderr); instance.on("exit", (code) => { if (code !== 0) { throw new Error("evcc terminated", code); diff --git a/tests/persist-target-minsoc-connected.evcc.yaml b/tests/persist-target-minsoc-connected.evcc.yaml deleted file mode 100755 index 3a235bbfb..000000000 --- a/tests/persist-target-minsoc-connected.evcc.yaml +++ /dev/null @@ -1,58 +0,0 @@ -site: - title: Hello World - meters: - grid: grid - -meters: - - name: grid - type: custom - power: - source: js - script: | - 1000 - -loadpoints: - - title: Carport - charger: charger - mode: pv - vehicle: golf - -chargers: - - name: charger - type: custom - enable: - source: js - script: - enabled: - source: js - script: | - false - status: - source: js - script: | - "B" - maxcurrent: - source: js - script: - -vehicles: - - name: golf - title: blauer e-Golf - type: custom - soc: - source: js - vm: shared - script: | - 35 - capacity: 20 - - name: honda - type: template - template: offline - title: grüner Honda e - capacity: 28.5 - -tariffs: - currency: EUR - grid: - type: fixed - price: 0.20 diff --git a/tests/persist-target-minsoc-disconnected.evcc.yaml b/tests/persist-target-minsoc-disconnected.evcc.yaml deleted file mode 100755 index 9e988d136..000000000 --- a/tests/persist-target-minsoc-disconnected.evcc.yaml +++ /dev/null @@ -1,58 +0,0 @@ -site: - title: Hello World - meters: - grid: grid - -meters: - - name: grid - type: custom - power: - source: js - script: | - 1000 - -loadpoints: - - title: Carport - charger: charger - mode: pv - vehicle: golf - -chargers: - - name: charger - type: custom - enable: - source: js - script: - enabled: - source: js - script: | - false - status: - source: js - script: | - "A" - maxcurrent: - source: js - script: - -vehicles: - - name: golf - title: blauer e-Golf - type: custom - soc: - source: js - vm: shared - script: | - 35 - capacity: 20 - - name: honda - type: template - template: offline - title: grüner Honda e - capacity: 28.5 - -tariffs: - currency: EUR - grid: - type: fixed - price: 0.20 diff --git a/tests/persist-target-minsoc.spec.js b/tests/persist-target-minsoc.spec.js index 627311a9d..5ed1b0ebb 100644 --- a/tests/persist-target-minsoc.spec.js +++ b/tests/persist-target-minsoc.spec.js @@ -1,8 +1,25 @@ const { test, expect } = require("@playwright/test"); const { start, stop, restart } = require("./evcc"); +const { startSimulator, stopSimulator, SIMULATOR_URL } = require("./simulator"); -const CONFIG_CONNECTED = "persist-target-minsoc-connected.evcc.yaml"; -const CONFIG_DISCONNECTED = "persist-target-minsoc-disconnected.evcc.yaml"; +const CONFIG = "simulator.evcc.yaml"; + +test.beforeAll(async () => { + await startSimulator(); +}); +test.afterAll(async () => { + await stopSimulator(); +}); + +test.beforeEach(async ({ page }) => { + await start(CONFIG); + + await page.goto(SIMULATOR_URL); + await page.getByLabel("Grid Power").fill("500"); + await page.getByTestId("vehicle0").getByLabel("SoC").fill("20"); + await page.getByTestId("loadpoint0").getByText("B (connected)").click(); + await page.getByRole("button", { name: "Apply changes" }).click(); +}); test.afterEach(async () => { await stop(); @@ -10,39 +27,66 @@ test.afterEach(async () => { test.describe("targetSoc", async () => { test("survives a restart", async ({ page }) => { - await start(CONFIG_CONNECTED); await page.goto("/"); await expect(page.getByTestId("target-soc-value")).toHaveText("100%"); await page.getByTestId("target-soc").getByRole("combobox").selectOption("50%"); await expect(page.getByTestId("target-soc-value")).toHaveText("50%"); - await restart(CONFIG_CONNECTED); + await restart(CONFIG); await page.reload(); await expect(page.getByTestId("target-soc-value")).toHaveText("50%"); }); test("can be set even if vehicle isn't connected yet", async ({ page }) => { - await start(CONFIG_DISCONNECTED); - await page.goto("/"); + await page.goto(SIMULATOR_URL); + await page.getByTestId("loadpoint0").getByText("A (disconnected)").click(); + await page.getByRole("button", { name: "Apply changes" }).click(); + await page.goto("/"); await expect(page.getByTestId("vehicle-title")).toContainText("blauer e-Golf"); await expect(page.getByTestId("vehicle-status")).toHaveText("Disconnected."); await expect(page.getByTestId("target-soc-value")).toHaveText("100%"); await page.getByTestId("target-soc").getByRole("combobox").selectOption("50%"); await expect(page.getByTestId("target-soc-value")).toHaveText("50%"); - await restart(CONFIG_CONNECTED); - await page.reload(); + await page.goto(SIMULATOR_URL); + await page.getByTestId("loadpoint0").getByText("B (connected)").click(); + await page.getByRole("button", { name: "Apply changes" }).click(); + await page.goto("/"); + await expect(page.getByTestId("target-soc-value")).toHaveText("50%"); + }); + + test("target soc should be preserved when vehicle gets disconnected", async ({ page }) => { + await page.goto("/"); + await expect(page.getByTestId("target-soc-value")).toHaveText("100%"); + await page.getByTestId("target-soc").getByRole("combobox").selectOption("50%"); + await expect(page.getByTestId("target-soc-value")).toHaveText("50%"); + + // disconnect + await page.goto(SIMULATOR_URL); + await page.getByTestId("loadpoint0").getByText("A (disconnected)").click(); + await page.getByRole("button", { name: "Apply changes" }).click(); + + await page.goto("/"); + await expect(page.getByTestId("vehicle-status")).toHaveText("Disconnected."); + await expect(page.getByTestId("target-soc-value")).toHaveText("50%"); + + // connect + await page.goto(SIMULATOR_URL); + await page.getByTestId("loadpoint0").getByText("B (connected)").click(); + await page.getByRole("button", { name: "Apply changes" }).click(); + + await page.goto("/"); + await expect(page.getByTestId("vehicle-status")).toHaveText("Connected."); await expect(page.getByTestId("target-soc-value")).toHaveText("50%"); }); }); test.describe("targetEnergy", async () => { test("survives a restart", async ({ page }) => { - await start(CONFIG_CONNECTED); await page.goto("/"); await page.getByRole("button", { name: "blauer e-Golf" }).click(); @@ -52,7 +96,7 @@ test.describe("targetEnergy", async () => { await page.getByTestId("target-energy").getByRole("combobox").selectOption("10 kWh (+35%)"); await expect(page.getByTestId("target-energy-value")).toHaveText("10 kWh"); - await restart(CONFIG_CONNECTED); + await restart(CONFIG); await page.reload(); await page.getByRole("button", { name: "blauer e-Golf" }).click(); @@ -64,7 +108,6 @@ test.describe("targetEnergy", async () => { test.describe("minSoc", async () => { test("survives a restart", async ({ page }) => { - await start(CONFIG_CONNECTED); await page.goto("/"); await page.getByTestId("charging-plan").getByRole("button", { name: "none" }).click(); @@ -74,7 +117,7 @@ test.describe("minSoc", async () => { await page.getByRole("combobox", { name: "Min. charge %" }).selectOption("20%"); await expect(page.getByText("charged to 20% in solar mode")).toBeVisible(); - await restart(CONFIG_CONNECTED); + await restart(CONFIG); await page.reload(); await page.getByTestId("charging-plan").getByRole("button", { name: "none" }).click(); @@ -83,7 +126,6 @@ test.describe("minSoc", async () => { }); test("show minsoc instead of plan when minsoc is active", async ({ page }) => { - await start(CONFIG_CONNECTED); await page.goto("/"); await expect(page.getByTestId("charging-plan")).toContainText("Plan"); @@ -103,7 +145,6 @@ test.describe("minSoc", async () => { }); test("disabled for offline vehicles", async ({ page }) => { - await start(CONFIG_CONNECTED); await page.goto("/"); // switch to offline vehicle @@ -116,7 +157,6 @@ test.describe("minSoc", async () => { }); test("disabled for guest vehicles", async ({ page }) => { - await start(CONFIG_CONNECTED); await page.goto("/"); // switch to offline vehicle @@ -131,7 +171,6 @@ test.describe("minSoc", async () => { test.describe("targetTime", async () => { test("survives a restart", async ({ page }) => { - await start(CONFIG_CONNECTED); await page.goto("/"); await page.getByTestId("target-soc").getByRole("combobox").selectOption("90%"); @@ -145,7 +184,7 @@ test.describe("targetTime", async () => { "tomorrow 9:30 AM" ); - await restart(CONFIG_CONNECTED); + await restart(CONFIG); await page.reload(); await expect(page.getByTestId("vehicle-status")).toContainText("Target charging starts at"); diff --git a/tests/simulator.evcc.yaml b/tests/simulator.evcc.yaml new file mode 100755 index 000000000..c9fb45da3 --- /dev/null +++ b/tests/simulator.evcc.yaml @@ -0,0 +1,99 @@ +interval: 0.25s + +site: + title: evcc Simulator + meters: + grid: grid + pvs: + - pv + batteries: + - battery + +meters: + - name: grid + type: custom + power: + source: http + uri: http://localhost:7072/api/state + jq: .site.grid.power + - name: pv + type: custom + power: + source: http + uri: http://localhost:7072/api/state + jq: .site.pv.power + - name: battery + type: custom + power: + source: http + uri: http://localhost:7072/api/state + jq: .site.battery.power + soc: + source: http + uri: http://localhost:7072/api/state + jq: .site.battery.soc + - name: lp0meter + type: custom + power: + source: http + uri: http://localhost:7072/api/state + jq: .loadpoints[0].power + energy: + source: http + uri: http://localhost:7072/api/state + jq: .loadpoints[0].energy + +loadpoints: + - title: Carport + charger: charger + meter: lp0meter + mode: pv + soc: + estimate: false + vehicle: golf + resetOnDisconnect: true + +chargers: + - name: charger + type: custom + enable: + source: js + script: | + false + enabled: + source: http + uri: http://localhost:7072/api/state + jq: .loadpoints[0].enabled + status: + source: http + uri: http://localhost:7072/api/state + jq: .loadpoints[0].status + maxcurrent: + source: js + script: | + 16 + +vehicles: + - name: golf + title: blauer e-Golf + type: custom + soc: + source: http + uri: http://localhost:7072/api/state + jq: .vehicles[0].soc + range: + source: http + uri: http://localhost:7072/api/state + jq: .vehicles[0].range + capacity: 20 + - name: honda + type: template + template: offline + title: grüner Honda e + capacity: 28.5 + +tariffs: + currency: EUR + grid: + type: fixed + price: 0.40 diff --git a/tests/simulator.js b/tests/simulator.js new file mode 100644 index 000000000..394b39b40 --- /dev/null +++ b/tests/simulator.js @@ -0,0 +1,28 @@ +import waitOn from "wait-on"; +import axios from "axios"; +import { exec } from "child_process"; + +export const SIMULATOR_URL = "http://localhost:7072/"; +const HEALTH_URL = SIMULATOR_URL + "api/state"; +const SHUTDOWN_URL = SIMULATOR_URL + "api/shutdown"; + +export async function startSimulator() { + console.log("starting simulator"); + const instance = exec("npm run simulator"); + console.log("exec end"); + instance.stdout.pipe(process.stdout); + instance.stderr.pipe(process.stderr); + + instance.on("exit", (code) => { + if (code !== 0) { + throw new Error("simulator terminated", code); + } + }); + console.log("waiton"); + await waitOn({ resources: [HEALTH_URL], log: true }); +} + +export async function stopSimulator() { + console.log("shutting down simulator"); + await axios.post(SHUTDOWN_URL); +} diff --git a/tests/simulator/api.js b/tests/simulator/api.js new file mode 100644 index 000000000..0506ecaf3 --- /dev/null +++ b/tests/simulator/api.js @@ -0,0 +1,37 @@ +import bodyParser from "body-parser"; + +let state = { + site: { + grid: { power: 0 }, + pv: { power: 0 }, + battery: { power: 0, soc: 0 }, + }, + loadpoints: [{ power: 0, energy: 0, enabled: false, status: "A" }], + vehicles: [{ soc: 0, range: 0 }], +}; + +export default () => ({ + name: "api", + enforce: "pre", + configureServer(server) { + console.log("configureServer"); + return () => { + server.middlewares.use(bodyParser.json()); + server.middlewares.use((req, res, next) => { + if (req.method === "POST" && req.originalUrl === "/api/state") { + console.log("POST /api/state", req.body); + state = req.body; + res.end(); + } else if (req.method === "POST" && req.originalUrl === "/api/shutdown") { + console.log("POST /api/shutdown", req.body); + res.end(); + process.exit(); + } else if (req.originalUrl === "/api/state") { + res.end(JSON.stringify(state)); + } else { + next(); + } + }); + }; + }, +}); diff --git a/tests/simulator/index.html b/tests/simulator/index.html new file mode 100644 index 000000000..53fc1ee17 --- /dev/null +++ b/tests/simulator/index.html @@ -0,0 +1,15 @@ + + + + + + + evcc Simulator + + + +
+ + + + \ No newline at end of file diff --git a/tests/simulator/src/Simulator.vue b/tests/simulator/src/Simulator.vue new file mode 100644 index 000000000..ac63d6067 --- /dev/null +++ b/tests/simulator/src/Simulator.vue @@ -0,0 +1,297 @@ + + + + + diff --git a/tests/simulator/src/main.js b/tests/simulator/src/main.js new file mode 100644 index 000000000..d08ba0c3f --- /dev/null +++ b/tests/simulator/src/main.js @@ -0,0 +1,6 @@ +import "bootstrap/dist/css/bootstrap.min.css"; +import "../../../assets/css/app.css"; +import { createApp } from "vue"; +import Simulator from "./Simulator.vue"; + +createApp(Simulator).mount("#app"); diff --git a/tests/simulator/vite.config.js b/tests/simulator/vite.config.js new file mode 100644 index 000000000..1de22886a --- /dev/null +++ b/tests/simulator/vite.config.js @@ -0,0 +1,8 @@ +import { defineConfig } from "vite"; +import vue from "@vitejs/plugin-vue"; +import api from "./api"; + +export default defineConfig({ + plugins: [vue(), api()], + server: { port: 7072, host: true }, +});