diff --git a/.github/actions/build-toolchain/action.yml b/.github/actions/build-toolchain/action.yml index bef7f0dc72..cc113c8754 100644 --- a/.github/actions/build-toolchain/action.yml +++ b/.github/actions/build-toolchain/action.yml @@ -20,7 +20,7 @@ runs: - uses: voidzero-dev/setup-vp@250f29ce396baf5e8f24498e17c0dfdebabc26eb # v1 with: - node-version: "26" + node-version-file: ".node-version" cache: true # Writes are namespaced by cache-scope so a PR build cannot poison the diff --git a/.github/dependabot.yml b/.github/dependabot.yml index abfd0dafcb..38ee3c5f9d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -27,3 +27,9 @@ updates: open-pull-requests-limit: 0 labels: - "infrastructure" + - package-ecosystem: "pip" + directory: "/.github" + schedule: + interval: "monthly" + labels: + - "infrastructure" diff --git a/.github/requirements.txt b/.github/requirements.txt new file mode 100644 index 0000000000..d5dff9d1f9 --- /dev/null +++ b/.github/requirements.txt @@ -0,0 +1 @@ +cloudsmith-cli==1.28.0 diff --git a/.github/upstream-check/watch.json b/.github/upstream-check/watch.json index 5f9d62c7cf..ef98cec392 100644 --- a/.github/upstream-check/watch.json +++ b/.github/upstream-check/watch.json @@ -105,7 +105,7 @@ { "name": "Batterie-Vermessung (Kapazität, Wirkungsgrad)", "files": ["core/site_battery_ident.go"], - "keywords": ["roundtrip", "round trip", "RoundTrip", "usable capacity"] + "keywords": ["roundtrip efficiency", "round trip efficiency", "round-trip efficiency", "RoundTripEfficiency", "roundTripEfficiency", "RoundtripEfficiency", "usable capacity", "UsableCapacity"] }, { "name": "Zweiter Einspeisetarif (EEG) / Energieseite", diff --git a/.github/workflows/claude-code-review-run.yml b/.github/workflows/claude-code-review-run.yml index 01a4bdd32f..f82902d32d 100644 --- a/.github/workflows/claude-code-review-run.yml +++ b/.github/workflows/claude-code-review-run.yml @@ -93,7 +93,7 @@ jobs: # Default checkout (base ref) only — do NOT check out the untrusted PR head. - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 1 @@ -102,7 +102,7 @@ jobs: continue-on-error: true # always report status below, even on failure # pinned: v1 (Claude Code 2.1.216) fails every Bash call with # "bwrap: Can't create file at /home/.mcp.json: Permission denied" - uses: anthropics/claude-code-action@8ce9314fa9a404564fa7e954cd84f25bcba2b829 # v1 @ 2.1.215 + uses: anthropics/claude-code-action@58985842b834ed26087302ba27d07bc24ca8697a # v1 @ 2.1.215 with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} # Provided explicitly so the review can post on fork PRs; also required diff --git a/.github/workflows/claude-issue-agent-run.yml b/.github/workflows/claude-issue-agent-run.yml index f9d7394277..4887b1d502 100644 --- a/.github/workflows/claude-issue-agent-run.yml +++ b/.github/workflows/claude-issue-agent-run.yml @@ -74,7 +74,7 @@ jobs: - name: Run Claude # pinned: v1 (Claude Code 2.1.216) fails every Bash call with # "bwrap: Can't create file at /home/.mcp.json: Permission denied" - uses: anthropics/claude-code-action@8ce9314fa9a404564fa7e954cd84f25bcba2b829 # v1 @ 2.1.215 + uses: anthropics/claude-code-action@58985842b834ed26087302ba27d07bc24ca8697a # v1 @ 2.1.215 with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }} @@ -399,7 +399,8 @@ jobs: if [ "$MODE" = triage ]; then args+=(--draft) fi - if [ "$(gh api "repos/$GITHUB_REPOSITORY/issues/$ISSUE" --jq '.type.name // empty')" = Bug ]; then - args+=(--label bug) - fi + # carry the triaged issue labels over: the PR labeling job skips bot authors + mapfile -t labels < <(gh api "repos/$GITHUB_REPOSITORY/issues/$ISSUE" \ + --jq '[.labels[].name] + (if .type.name == "Bug" then ["bug"] else [] end) | unique | .[]') + for l in "${labels[@]}"; do args+=(--label "$l"); done gh pr create "${args[@]}" diff --git a/.github/workflows/claude-issue-agent.yml b/.github/workflows/claude-issue-agent.yml index 5c3713497f..7cba28aa12 100644 --- a/.github/workflows/claude-issue-agent.yml +++ b/.github/workflows/claude-issue-agent.yml @@ -158,7 +158,7 @@ jobs: - name: Run Claude PR Labeler # pinned: v1 (Claude Code 2.1.216) fails every Bash call with # "bwrap: Can't create file at /home/.mcp.json: Permission denied" - uses: anthropics/claude-code-action@8ce9314fa9a404564fa7e954cd84f25bcba2b829 # v1 @ 2.1.215 + uses: anthropics/claude-code-action@58985842b834ed26087302ba27d07bc24ca8697a # v1 @ 2.1.215 with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/cli-docs.yml b/.github/workflows/cli-docs.yml index 97309b9787..8de0c9fa8a 100644 --- a/.github/workflows/cli-docs.yml +++ b/.github/workflows/cli-docs.yml @@ -26,7 +26,7 @@ jobs: with: go-version-file: go.mod - - uses: voidzero-dev/setup-vp@250f29ce396baf5e8f24498e17c0dfdebabc26eb # v1 + - uses: voidzero-dev/setup-vp@3754dd7dbdb32bd8f6d28b6043de13ad3a75f21f # v1.21.1 with: node-version-file: ".node-version" cache: true diff --git a/.github/workflows/command-pr-build.yml b/.github/workflows/command-pr-build.yml index bf2bf91047..c68bbcf1db 100644 --- a/.github/workflows/command-pr-build.yml +++ b/.github/workflows/command-pr-build.yml @@ -130,7 +130,7 @@ jobs: - name: Setup Buildx id: buildx - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4 + uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4 - name: Restore build cache uses: ./.github/actions/docker-cache diff --git a/.github/workflows/default.yml b/.github/workflows/default.yml index 38f9f2d862..0b4d486ec4 100644 --- a/.github/workflows/default.yml +++ b/.github/workflows/default.yml @@ -57,7 +57,7 @@ jobs: go-version-file: go.mod id: go - - uses: voidzero-dev/setup-vp@250f29ce396baf5e8f24498e17c0dfdebabc26eb # v1 + - uses: voidzero-dev/setup-vp@3754dd7dbdb32bd8f6d28b6043de13ad3a75f21f # v1.21.1 with: node-version-file: ".node-version" cache: true @@ -127,7 +127,7 @@ jobs: with: persist-credentials: false - - uses: voidzero-dev/setup-vp@250f29ce396baf5e8f24498e17c0dfdebabc26eb # v1 + - uses: voidzero-dev/setup-vp@3754dd7dbdb32bd8f6d28b6043de13ad3a75f21f # v1.21.1 with: node-version-file: ".node-version" cache: true @@ -210,7 +210,7 @@ jobs: go-version-file: go.mod id: go - - uses: voidzero-dev/setup-vp@250f29ce396baf5e8f24498e17c0dfdebabc26eb # v1 + - uses: voidzero-dev/setup-vp@3754dd7dbdb32bd8f6d28b6043de13ad3a75f21f # v1.21.1 with: node-version-file: ".node-version" cache: true @@ -332,7 +332,7 @@ jobs: with: persist-credentials: false - - uses: voidzero-dev/setup-vp@250f29ce396baf5e8f24498e17c0dfdebabc26eb # v1 + - uses: voidzero-dev/setup-vp@3754dd7dbdb32bd8f6d28b6043de13ad3a75f21f # v1.21.1 with: node-version-file: ".node-version" cache: true diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index a1009589e4..fe754bbd55 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -55,10 +55,18 @@ jobs: with: persist-credentials: false + - uses: voidzero-dev/setup-vp@250f29ce396baf5e8f24498e17c0dfdebabc26eb # v1 + with: + node-version-file: ".node-version" + cache: true + + - name: Install + run: make install-ui + - name: Prepare OpenAPI spec run: | mkdir -p ./openapi-deploy - npx --yes @redocly/cli@2 bundle ./server/openapi.yaml -o ./openapi-deploy/openapi.yaml + npx redocly bundle ./server/openapi.yaml -o ./openapi-deploy/openapi.yaml - name: Deploy OpenAPI spec to docs repo uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453 # v4 diff --git a/.github/workflows/fork-tests.yml b/.github/workflows/fork-tests.yml index 83c5814df4..c36f7b4072 100644 --- a/.github/workflows/fork-tests.yml +++ b/.github/workflows/fork-tests.yml @@ -55,9 +55,13 @@ jobs: - name: Race detector run: | set -o pipefail - # charger: only the fork's tests, evcc's TestTemplates has an intermittent race of its own + # charger: only the fork's tests, evcc's TestTemplates has an intermittent race of its own. + # core: evcc's enableAutomatic (optimizer_automatic_test.go) writes sponsor.Subject while an + # optimizer run started by an earlier evcc test still reads it; evcc runs no race detector + evccRace='^(TestAutomaticLevels|TestOptimizerGate|TestOptimizerSurplusRegime|TestOptimizerStopFollowsSurplus|TestOptimizerFlexibility|TestOptimizerGateInactive|TestOptimizerGateStale|TestSmartCostLimitUnavailable|TestBatteryModeAutomatic|TestBatteryGridChargeLimitUnavailable)$' { - go test -race -count=1 ./core/lm/... ./core/peak/ ./core/ ./server/ && + go test -race -count=1 ./core/lm/... ./core/peak/ ./server/ && + go test -race -count=1 -skip "$evccRace" ./core/ && go test -race -count=1 -run 'TestSwitchStages|TestSwitchSocketRatedPower' ./charger/ } 2>&1 | tee /tmp/race.log || { grep -E "^(--- FAIL|FAIL|panic:)" /tmp/race.log | head -20 | sed "s/^/::error::/" diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 8c2c1bc251..9d0cdf9850 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -137,7 +137,7 @@ jobs: python-version: 3.12 - name: Install Cloudsmith CLI - run: pip install cloudsmith-cli==1.16.0 + run: pip install -r .github/requirements.txt - name: Publish .deb to Cloudsmith env: diff --git a/.github/workflows/openapi-validate.yml b/.github/workflows/openapi-validate.yml index bac1be56a5..ba135277a6 100644 --- a/.github/workflows/openapi-validate.yml +++ b/.github/workflows/openapi-validate.yml @@ -10,6 +10,7 @@ on: - "server/openapi.yaml" - "server/openapi.state.yaml" - ".github/workflows/openapi-validate.yml" + - "go.mod" # validator version follows go.mod jobs: validate-openapi: @@ -23,4 +24,4 @@ jobs: with: go-version-file: go.mod - name: Validate OpenAPI spec - run: go run github.com/getkin/kin-openapi/cmd/validate@v0.133.0 -ext -- server/openapi.yaml + run: go tool validate -ext -- server/openapi.yaml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2923d776d9..4f15c7b6f5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -156,7 +156,7 @@ jobs: python-version: 3.12 - name: Install Cloudsmith CLI - run: pip install cloudsmith-cli==1.16.0 + run: pip install -r .github/requirements.txt - name: Publish .deb to Cloudsmith env: diff --git a/AGENTS.md b/AGENTS.md index d7fc363040..b98d45d4a3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -209,7 +209,7 @@ Deep documentation on specific subsystems is available in `docs/agents/`. Load w - Use CSS Custom Properties for theming (semantic names: `--evcc-green`, `--evcc-battery`) - No inline `style` attributes; use Bootstrap utility classes or scoped CSS (inline only for truly dynamic values) -- Use existing custom media queries for responsive breakpoints +- Use the custom media queries from `assets/css/breakpoints.css` (`@media (--sm-and-up)`) for responsive breakpoints, never raw px `min-width`/`max-width`. Write mobile-first and override upwards instead of adding max-width queries - Use `$t()` function for all user-facing strings - Update both `i18n/en.json` and `i18n/de.json` for new strings - Use hierarchical namespace: `{section}.{component}.{purpose}` diff --git a/Dockerfile b/Dockerfile index 7aaecbaaef..1d73811514 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # STEP 1 build ui -FROM --platform=$BUILDPLATFORM ghcr.io/voidzero-dev/vite-plus:1.0.0@sha256:8d788678fb9783ba8994c4fe5652af3316bfc414f7ef6ead0136932ee6075b8b AS node +FROM --platform=$BUILDPLATFORM ghcr.io/voidzero-dev/vite-plus:1.1.0@sha256:15dedad6ab1f100c096aaebac6b0fb40ccf1f3dd0dd0169141e2a56588489b60 AS node USER root diff --git a/assets/css/app.css b/assets/css/app.css index 5b502a7353..b8c86a04c9 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -96,6 +96,9 @@ "Noto Color Emoji"; --bs-success: var(--evcc-primary); --bs-success-rgb: var(--bs-primary-rgb); + --evcc-success-bg: oklch(from var(--evcc-darker-green) 96% 0.035 h); + --evcc-success-text: oklch(from var(--evcc-darker-green) 45% 0.11 h); + --evcc-success-text-secondary: oklch(from var(--evcc-darker-green) 52% 0.08 h); --bs-code-color: var(--evcc-darkest-green); --evcc-backdrop: #93949eaa; /* --bs-gray-medium + transparent */ @@ -127,6 +130,9 @@ --bs-border-color-translucent: rgba(255, 255, 255, 0.08); --bs-code-color: var(--evcc-dark-green); --evcc-backdrop: #000000aa; /* black + transparent */ + --evcc-success-bg: oklch(from var(--evcc-darker-green) 30% 0.06 h); + --evcc-success-text: oklch(from var(--evcc-darker-green) 82% 0.17 h); + --evcc-success-text-secondary: oklch(from var(--evcc-darker-green) 80% 0.07 h); --tab-bar-background: var(--evcc-box); color-scheme: dark; } diff --git a/assets/js/components/Battery/BatteryConfigCard.vue b/assets/js/components/Battery/BatteryConfigCard.vue index d4d05a936d..0a5078a35a 100644 --- a/assets/js/components/Battery/BatteryConfigCard.vue +++ b/assets/js/components/Battery/BatteryConfigCard.vue @@ -6,7 +6,9 @@ class="text-primary flex-shrink-0 mt-1" >
-
{{ $t("battery.config.priorityTitle") }}
+
+ {{ $t("battery.config.priorityTitle") }} +
-
{{ $t("battery.config.bufferTitle") }}
+
+ {{ $t("battery.config.bufferTitle") }} +
+ + + + + +

+ @@ -40,17 +54,19 @@ import { defineComponent, type PropType } from "vue"; import formatter from "@/mixins/formatter"; import minuteTicker from "@/mixins/minuteTicker"; import TariffChart from "../Tariff/TariffChart.vue"; +import OptimizerAuto from "../MaterialIcon/OptimizerAuto.vue"; import { SMART_COST_TYPE, type CURRENCY, type Rate, type Slot } from "@/types/evcc"; export default defineComponent({ name: "ChargingPlanPreview", - components: { TariffChart }, + components: { TariffChart, OptimizerAuto }, mixins: [formatter, minuteTicker], props: { duration: Number, power: Number, rates: Array as PropType, plan: Array as PropType, + optimizer: Boolean, smartCostType: String as PropType, targetTime: [Date, null], currency: String as PropType, diff --git a/assets/js/components/Config/AuthProvidersCard.stories.ts b/assets/js/components/Config/AuthProvidersCard.stories.ts new file mode 100644 index 0000000000..c0d2e5e207 --- /dev/null +++ b/assets/js/components/Config/AuthProvidersCard.stories.ts @@ -0,0 +1,41 @@ +import AuthProvidersCard from "./AuthProvidersCard.vue"; +import type { Meta, StoryFn } from "@storybook/vue3"; + +export default { + title: "Config/AuthProvidersCard", + component: AuthProvidersCard, + parameters: { + layout: "centered", + }, +} as Meta; + +// narrow: column width in the config device grid +const Narrow: StoryFn = (args) => ({ + components: { AuthProvidersCard }, + setup() { + return { args }; + }, + template: '
', +}); + +export const Mixed = Narrow.bind({}); +Mixed.args = { + providers: { + "Tesla (Model Y)": { id: "tesla", authenticated: true }, + "Home Assistant (homeassistant.local)": { id: "ha", authenticated: false }, + }, +}; + +export const LongTitles = Narrow.bind({}); +LongTitles.args = { + providers: { + "Home Assistant (https://homeassistant.example.duckdns.org)": { + id: "ha1", + authenticated: true, + }, + "Home Assistant (https://homeassistant.another-very-long-domain.duckdns.org)": { + id: "ha2", + authenticated: false, + }, + }, +}; diff --git a/assets/js/components/Config/AuthProvidersCard.vue b/assets/js/components/Config/AuthProvidersCard.vue index 39a91ae790..879a6a2abb 100644 --- a/assets/js/components/Config/AuthProvidersCard.vue +++ b/assets/js/components/Config/AuthProvidersCard.vue @@ -14,18 +14,20 @@
-
+
- {{ provider.title }} + {{ + provider.title + }}
+
+ +
+ +
+ + +
+ {{ $t(`config.optimizer.automaticLevel.${automatic}.description`) }} +
+
+
+

{{ $t("config.optimizer.info") }}

@@ -41,6 +76,7 @@ import SponsorTokenRequired from "./DeviceModal/SponsorTokenRequired.vue"; import api from "@/api"; import store from "@/store"; import { docsPrefix } from "@/i18n"; +import { OPTIMIZER_AUTOMATIC } from "@/types/evcc"; import type { AxiosError } from "axios"; export default defineComponent({ @@ -52,12 +88,22 @@ export default defineComponent({ data() { return { error: null as string | null, + levels: [OPTIMIZER_AUTOMATIC.BATTERY, OPTIMIZER_AUTOMATIC.FULL], }; }, computed: { enabled(): boolean { return !!store.state?.optimizer; }, + automatic(): OPTIMIZER_AUTOMATIC { + return store.state?.optimizerAutomatic || OPTIMIZER_AUTOMATIC.OFF; + }, + automaticEnabled(): boolean { + return this.automatic !== OPTIMIZER_AUTOMATIC.OFF; + }, + level(): OPTIMIZER_AUTOMATIC { + return this.automaticEnabled ? this.automatic : OPTIMIZER_AUTOMATIC.BATTERY; + }, hasEvopt(): boolean { return !!store.state?.evopt; }, @@ -67,9 +113,20 @@ export default defineComponent({ }, methods: { async change(e: Event) { + await this.post(`config/optimizer/${(e.target as HTMLInputElement).checked}`); + }, + async changeAutomatic(e: Event) { + const { checked } = e.target as HTMLInputElement; + const level = checked ? OPTIMIZER_AUTOMATIC.BATTERY : OPTIMIZER_AUTOMATIC.OFF; + await this.post(`config/optimizerautomatic/${level}`); + }, + async changeLevel(e: Event) { + await this.post(`config/optimizerautomatic/${(e.target as HTMLSelectElement).value}`); + }, + async post(url: string) { try { this.error = null; - await api.post(`config/optimizer/${(e.target as HTMLInputElement).checked}`); + await api.post(url); } catch (err) { const e = err as AxiosError<{ error: string }>; this.error = e.response?.data?.error || e.message; diff --git a/assets/js/components/Config/PropertyEntry.vue b/assets/js/components/Config/PropertyEntry.vue index 3bcccaa63f..28b7393369 100644 --- a/assets/js/components/Config/PropertyEntry.vue +++ b/assets/js/components/Config/PropertyEntry.vue @@ -45,7 +45,7 @@ export default { Description: String, Help: String, Example: String, - Default: [String, Number, Boolean], + Default: { type: [String, Number, Boolean], default: undefined }, Type: String, Unit: String, Mask: Boolean, diff --git a/assets/js/components/Config/PropertyField.vue b/assets/js/components/Config/PropertyField.vue index 3a035cbb72..f5f1f68d75 100644 --- a/assets/js/components/Config/PropertyField.vue +++ b/assets/js/components/Config/PropertyField.vue @@ -186,7 +186,7 @@ export default { property: String, masked: Boolean, placeholder: String, - defaultValue: [String, Number, Boolean], + defaultValue: { type: [String, Number, Boolean], default: undefined }, type: String, unit: String, // transitional: emit ns numbers, remove once all callers accept duration strings (loadpoint follow-up) diff --git a/assets/js/components/Energy/StatCards.vue b/assets/js/components/Energy/StatCards.vue index c56352da2e..0e84b822ee 100644 --- a/assets/js/components/Energy/StatCards.vue +++ b/assets/js/components/Energy/StatCards.vue @@ -1,5 +1,5 @@