+ {{ $t("forecast.modalTitle") }} +
+nothing to see here
+diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml
index 6617d79aa0..6dd2566e0c 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yaml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yaml
@@ -39,9 +39,9 @@ body:
render: yaml
description: >
Show evcc configuration file evcc.yaml
-
+
Please make sure your report does NOT contain **passwords**, **sponsor token** or other **credentials**!
-
+
To quickly dump a redacted configuration without secrets, you can use the `evcc dump --cfg` command.
- type: textarea
@@ -69,6 +69,15 @@ body:
- macOS
- other
+ - type: checkboxes
+ id: external
+ attributes:
+ label: External automation
+ description: Make sure the observed issue is caused by evcc and not by external automation
+ options:
+ - label: I have made sure that no external automation like HomeAssistant or Node-RED is active or accessing any of the mentioned devices when this issue occurs.
+ required: true
+
- type: checkboxes
id: nightly
attributes:
diff --git a/.github/workflows/default.yml b/.github/workflows/default.yml
index 5ebdada5f8..19a02f7851 100644
--- a/.github/workflows/default.yml
+++ b/.github/workflows/default.yml
@@ -1,8 +1,5 @@
name: Default
-env:
- GO_VERSION: ^1.23
-
on:
push:
branches:
@@ -13,14 +10,13 @@ on:
jobs:
clean:
name: Clean
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
- go-version: ${{ env.GO_VERSION }}
cache: false
id: go
@@ -58,7 +54,6 @@ jobs:
- uses: actions/setup-go@v5
with:
- go-version: ${{ env.GO_VERSION }}
cache: false
id: go
@@ -91,7 +86,6 @@ jobs:
- uses: actions/setup-go@v5
with:
- go-version: ${{ env.GO_VERSION }}
cache: false
id: go
@@ -117,7 +111,6 @@ jobs:
- uses: actions/setup-go@v5
with:
- go-version: ${{ env.GO_VERSION }}
cache: false # avoid cache thrashing
id: go
@@ -183,7 +176,6 @@ jobs:
- uses: actions/setup-go@v5
with:
- go-version: ${{ env.GO_VERSION }}
cache: false
id: go
@@ -208,14 +200,12 @@ jobs:
- name: Build Go
run: make build
+ - name: Install Playwright
+ run: npx playwright install --with-deps chromium
+
- name: Run tests
run: npx playwright test
- # - name: Run tests
- # uses: docker://mcr.microsoft.com/playwright:v1.34.3-jammy
- # with:
- # args: npx playwright test
-
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml
index 033fa69796..96a0f6d063 100644
--- a/.github/workflows/documentation.yml
+++ b/.github/workflows/documentation.yml
@@ -1,8 +1,5 @@
name: Deploy updated templates
-env:
- GO_VERSION: ^1.23
-
on:
schedule:
- cron: "0 2 * * *" # same time as nightly is triggered
@@ -19,8 +16,6 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- with:
- go-version: ${{ env.GO_VERSION }}
id: go
- name: Build docs
diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml
index f1985d8f6f..b91a932929 100644
--- a/.github/workflows/nightly.yml
+++ b/.github/workflows/nightly.yml
@@ -1,8 +1,5 @@
name: Nightly Build
-env:
- GO_VERSION: ^1.23
-
on:
schedule: # runs on the default branch: master
- cron: "0 2 * * *" # run at 2 AM UTC
@@ -62,16 +59,59 @@ jobs:
- name: Setup Buildx
uses: docker/setup-buildx-action@v3
+ - name: Define tags
+ id: meta
+ uses: docker/metadata-action@v5
+ with:
+ images: evcc/evcc
+ tags: |
+ type=raw,value=nightly
+ type=raw,value=nightly.{{date 'YYYYMMDD'}}-{{sha}}
+
- name: Publish
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64,linux/arm/v6
push: true
- build-args: |
- TESLA_CLIENT_ID=${{ secrets.TESLA_CLIENT_ID }}
- tags: |
- evcc/evcc:nightly
+ tags: ${{ steps.meta.outputs.tags }}
+
+ - name: Delete old nightly.* tags
+ run: |
+ old_tags=$(curl -s "https://hub.docker.com/v2/repositories/evcc/evcc/tags/?page_size=100" | jq -r '.results | map(select(.name | startswith("nightly."))) | sort_by(.last_updated) | reverse | .[1:] | .[].name')
+ for tag in $old_tags; do
+ echo "Deleting tag: $tag"
+ curl -s -H "Authorization: Bearer ${{ secrets.DOCKER_PASS }}" -X DELETE "https://hub.docker.com/v2/repositories/evcc/evcc/tags/$tag/"
+ done
+
+ hassio:
+ name: Hassio Addon :nightly
+ needs:
+ - docker
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@master
+ with:
+ repository: evcc-io/hassio-addon
+ token: ${{ secrets.GH_TOKEN }}
+ path: ./hassio
+
+ - name: Update version
+ run: |
+ current_date=$(date +%Y%m%d)
+ short_sha=$(echo "${{ github.sha }}" | cut -c 1-7)
+ sed -i -e "s/version:.*/version: nightly.${current_date}-${short_sha}/" ./hassio/evcc-nightly/config.yaml
+
+ - name: Push
+ run: |
+ cd ./hassio
+ git add .
+ git config user.name github-actions
+ git config user.email github-actions@github.com
+ git commit -am "Mirror evcc nightly release"
+ git push
apt:
name: Publish APT nightly
@@ -85,8 +125,6 @@ jobs:
fetch-depth: 0
- uses: actions/setup-go@v5
- with:
- go-version: ${{ env.GO_VERSION }}
id: go
- name: Patch ASN1
@@ -106,7 +144,6 @@ jobs:
args: --snapshot -f .goreleaser-nightly.yml --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- TESLA_CLIENT_ID: ${{ secrets.TESLA_CLIENT_ID }}
- uses: actions/setup-python@v5
with:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index bb7662f8d3..a61e05a165 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -1,8 +1,5 @@
name: Release
-env:
- GO_VERSION: ^1.23
-
on:
push:
tags:
@@ -48,7 +45,6 @@ jobs:
push: true
build-args: |
RELEASE=1
- TESLA_CLIENT_ID=${{ secrets.TESLA_CLIENT_ID }}
tags: ${{ steps.meta.outputs.tags }}
apt:
@@ -64,8 +60,6 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
- with:
- go-version: ${{ env.GO_VERSION }}
id: go
- name: Patch ASN1
@@ -100,7 +94,6 @@ jobs:
env:
# use GH_TOKEN for access to evcc-io/homebrew-tap
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
- TESLA_CLIENT_ID: ${{ secrets.TESLA_CLIENT_ID }}
- uses: actions/setup-python@v5
with:
diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml
index 22350181b0..e2cedc92d7 100644
--- a/.github/workflows/website.yml
+++ b/.github/workflows/website.yml
@@ -1,8 +1,5 @@
name: Deploy data to website
-env:
- GO_VERSION: ^1.23
-
on:
release:
types: [created]
@@ -17,8 +14,6 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- with:
- go-version: ${{ env.GO_VERSION }}
id: go
- name: Build docs
diff --git a/.gitignore b/.gitignore
index 0e9fef08c6..f9a1e6bddd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
__debug_bin*
.vscode
+!.vscode/extensions.json
.cache
.DS_store
*.gz
diff --git a/.golangci.yml b/.golangci.yml
index 573d971b88..3999f00c82 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -1,5 +1,5 @@
run:
- go: "1.23"
+ go: "1.24"
issues:
exclude:
diff --git a/.goreleaser-nightly.yml b/.goreleaser-nightly.yml
index bba2360ceb..52fb556871 100644
--- a/.goreleaser-nightly.yml
+++ b/.goreleaser-nightly.yml
@@ -11,7 +11,7 @@ builds:
- -trimpath
- -tags=release
ldflags:
- - -X github.com/evcc-io/evcc/server.Version={{ .Tag }} -X github.com/evcc-io/evcc/server.Commit={{ .ShortCommit }} -X github.com/evcc-io/evcc/vehicle/tesla.TESLA_CLIENT_ID={{ .Env.TESLA_CLIENT_ID }} -s -w
+ - -X github.com/evcc-io/evcc/server.Version={{ .Tag }} -X github.com/evcc-io/evcc/server.Commit={{ .ShortCommit }} -s -w
env:
- CGO_ENABLED=0
goos:
@@ -33,14 +33,6 @@ builds:
flags:
- -trimpath
- -tags=release,timetzdata
- - goos: darwin
- goarch: arm64
- ldflags:
- - -X github.com/evcc-io/evcc/server.Version={{ .Tag }} -X github.com/evcc-io/evcc/server.Commit={{ .ShortCommit }} -X github.com/evcc-io/evcc/vehicle/tesla.TESLA_CLIENT_ID={{ .Env.TESLA_CLIENT_ID }} -s -w -B gobuildid
- - goos: darwin
- goarch: amd64
- ldflags:
- - -X github.com/evcc-io/evcc/server.Version={{ .Tag }} -X github.com/evcc-io/evcc/server.Commit={{ .ShortCommit }} -X github.com/evcc-io/evcc/vehicle/tesla.TESLA_CLIENT_ID={{ .Env.TESLA_CLIENT_ID }} -s -w -B gobuildid
archives:
- builds:
diff --git a/.goreleaser.yml b/.goreleaser.yml
index 344981cac9..bf54ece2b8 100644
--- a/.goreleaser.yml
+++ b/.goreleaser.yml
@@ -14,7 +14,7 @@ builds:
- -trimpath
- -tags=release
ldflags:
- - -X github.com/evcc-io/evcc/server.Version={{ .Version }} -X github.com/evcc-io/evcc/vehicle/tesla.TESLA_CLIENT_ID={{ .Env.TESLA_CLIENT_ID }} -s -w
+ - -X github.com/evcc-io/evcc/server.Version={{ .Version }} -s -w
env:
- CGO_ENABLED=0
goos:
@@ -38,14 +38,6 @@ builds:
flags:
- -trimpath
- -tags=release,timetzdata
- - goos: darwin
- goarch: arm64
- ldflags:
- - -X github.com/evcc-io/evcc/server.Version={{ .Version }} -X github.com/evcc-io/evcc/vehicle/tesla.TESLA_CLIENT_ID={{ .Env.TESLA_CLIENT_ID }} -s -w -B gobuildid
- - goos: darwin
- goarch: amd64
- ldflags:
- - -X github.com/evcc-io/evcc/server.Version={{ .Version }} -X github.com/evcc-io/evcc/vehicle/tesla.TESLA_CLIENT_ID={{ .Env.TESLA_CLIENT_ID }} -s -w -B gobuildid
env:
- CGO_ENABLED=0
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 0000000000..dc9cabb64f
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,8 @@
+{
+ "recommendations": [
+ "Vue.volar",
+ "vitest.explorer",
+ "golang.go",
+ "esbenp.prettier-vscode",
+ ]
+}
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index a4c9406c7b..4cfd19763c 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -4,7 +4,7 @@
#### Development environment
-Developing evcc requires [Go][1] 1.23 and [Node][2] 22. We recommend VSCode with the [Go](https://marketplace.visualstudio.com/items?itemName=golang.Go), [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) and [Vetur](https://marketplace.visualstudio.com/items?itemName=octref.vetur) extensions.
+Developing evcc requires [Go][1] and [Node][2]. We recommend VSCode with the [Go](https://marketplace.visualstudio.com/items?itemName=golang.Go), [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) and [Vetur](https://marketplace.visualstudio.com/items?itemName=octref.vetur) extensions.
Alternatively, if you use VS Code and [devcontainers](https://code.visualstudio.com/docs/devcontainers/containers), you can use the "Dev containers: Clone repository in container volume" action. This will create a devcontainer with the required toolchain and install the prerequisites as explained below. Wait until the startup log says "Done. Press any key to close the terminal." and check for any errors.
diff --git a/Dockerfile b/Dockerfile
index f366ef6287..5fb53332f3 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -20,7 +20,7 @@ RUN make ui
# STEP 2 build executable binary
-FROM --platform=$BUILDPLATFORM golang:1.23-alpine AS builder
+FROM --platform=$BUILDPLATFORM golang:1.24-alpine AS builder
# Install git + SSL ca certificates.
# Git is required for fetching the dependencies.
@@ -39,7 +39,6 @@ RUN go mod download
# install tools
COPY Makefile .
-COPY tools.go .
COPY cmd/decorate/ cmd/decorate/
COPY api/ api/
RUN make install
@@ -58,9 +57,6 @@ ARG TARGETARCH
ARG TARGETVARIANT
ARG GOARM=${TARGETVARIANT#v}
-ARG TESLA_CLIENT_ID
-ENV TESLA_CLIENT_ID=${TESLA_CLIENT_ID}
-
RUN RELEASE=${RELEASE} GOOS=${TARGETOS} GOARCH=${TARGETARCH} GOARM=${GOARM} make build
diff --git a/Makefile b/Makefile
index 832337dd0f..751a92661f 100644
--- a/Makefile
+++ b/Makefile
@@ -9,8 +9,7 @@ endif
VERSION := $(if $(TAG_NAME),$(TAG_NAME),$(SHA))
BUILD_DATE := $(shell date -u '+%Y-%m-%d_%H:%M:%S')
BUILD_TAGS := -tags=release
-TESLA_CLIENT_ID := ${TESLA_CLIENT_ID}
-LD_FLAGS := -X github.com/evcc-io/evcc/server.Version=$(VERSION) -X github.com/evcc-io/evcc/server.Commit=$(COMMIT) -X github.com/evcc-io/evcc/vehicle/tesla.TESLA_CLIENT_ID=$(TESLA_CLIENT_ID) -s -w
+LD_FLAGS := -X github.com/evcc-io/evcc/server.Version=$(VERSION) -X github.com/evcc-io/evcc/server.Commit=$(COMMIT) -s -w
BUILD_ARGS := -trimpath -ldflags='$(LD_FLAGS)'
# docker
@@ -19,8 +18,9 @@ DOCKER_TAG := testing
PLATFORM := linux/amd64,linux/arm64,linux/arm/v6
# gokrazy image
-IMAGE_FILE := evcc_$(TAG_NAME).image
-IMAGE_OPTIONS := -hostname evcc -http_port 8080 github.com/gokrazy/serial-busybox github.com/gokrazy/breakglass github.com/gokrazy/mkfs github.com/gokrazy/wifi github.com/evcc-io/evcc
+GOK_DIR := packaging/gokrazy
+GOK := gok -i evcc --parent_dir $(GOK_DIR)
+IMAGE_FILE := evcc_$(TAG_NAME).img
# deb
PACKAGES = ./release
@@ -37,7 +37,7 @@ clean::
rm -rf dist/
install::
- go install $$(go list -e -f '{{join .Imports " "}}' tools.go)
+ go install tool
install-ui::
npm ci
@@ -84,15 +84,15 @@ release::
docker::
@echo Version: $(VERSION) $(SHA) $(BUILD_DATE)
- docker buildx build --platform $(PLATFORM) --tag $(DOCKER_IMAGE):$(DOCKER_TAG) --build-arg TESLA_CLIENT_ID=$(TESLA_CLIENT_ID) --push .
+ docker buildx build --platform $(PLATFORM) --tag $(DOCKER_IMAGE):$(DOCKER_TAG) --push .
publish-nightly::
@echo Version: $(VERSION) $(SHA) $(BUILD_DATE)
- docker buildx build --platform $(PLATFORM) --tag $(DOCKER_IMAGE):nightly --build-arg TESLA_CLIENT_ID=$(TESLA_CLIENT_ID) --push .
+ docker buildx build --platform $(PLATFORM) --tag $(DOCKER_IMAGE):nightly --push .
publish-release::
@echo Version: $(VERSION) $(SHA) $(BUILD_DATE)
- docker buildx build --platform $(PLATFORM) --tag $(DOCKER_IMAGE):latest --tag $(DOCKER_IMAGE):$(VERSION) --build-arg TESLA_CLIENT_ID=$(TESLA_CLIENT_ID) --build-arg RELEASE=1 --push .
+ docker buildx build --platform $(PLATFORM) --tag $(DOCKER_IMAGE):latest --tag $(DOCKER_IMAGE):$(VERSION) --build-arg RELEASE=1 --push .
apt-nightly::
$(foreach file, $(wildcard $(PACKAGES)/*.deb), \
@@ -104,25 +104,27 @@ apt-release::
cloudsmith push deb evcc/stable/any-distro/any-version $(file); \
)
-# gokrazy image
-gokrazy::
- go install github.com/gokrazy/tools/cmd/gokr-packer@main
- mkdir -p flags/github.com/gokrazy/breakglass
- echo "-forward=private-network" > flags/github.com/gokrazy/breakglass/flags.txt
- mkdir -p env/github.com/evcc-io/evcc
- echo "EVCC_NETWORK_PORT=80" > env/github.com/evcc-io/evcc/env.txt
- echo "EVCC_DATABASE_DSN=/perm/evcc.db" >> env/github.com/evcc-io/evcc/env.txt
- mkdir -p buildflags/github.com/evcc-io/evcc
- echo "$(BUILD_TAGS),gokrazy" > buildflags/github.com/evcc-io/evcc/buildflags.txt
- echo "-ldflags=$(LD_FLAGS)" >> buildflags/github.com/evcc-io/evcc/buildflags.txt
- gokr-packer -hostname evcc -http_port 8080 -overwrite=$(IMAGE_FILE) -target_storage_bytes=1258299392 $(IMAGE_OPTIONS)
+# gokrazy
+gok::
+ which gok || go install github.com/gokrazy/tools/cmd/gok@main
+ # https://stackoverflow.com/questions/1250079/how-to-escape-single-quotes-within-single-quoted-strings
+ sed 's!"GoBuildFlags": null!"GoBuildFlags": ["$(BUILD_TAGS) -trimpath -ldflags='"'"'$(LD_FLAGS)'"'"'"]!g' $(GOK_DIR)/config.tmpl.json > $(GOK_DIR)/evcc/config.json
+ ${GOK} add .
+ # ${GOK} add tailscale.com/cmd/tailscaled
+ # ${GOK} add tailscale.com/cmd/tailscale
+
+# build image
+gok-image:: gok
+ ${GOK} overwrite --full=$(IMAGE_FILE) --target_storage_bytes=1258299392
# gzip -f $(IMAGE_FILE)
-gokrazy-run::
- MACHINE=arm64 IMAGE_FILE=$(IMAGE_FILE) ./packaging/gokrazy/run.sh
+# run qemu
+gok-vm:: gok
+ ${GOK} vm run --netdev user,id=net0,hostfwd=tcp::8080-:80,hostfwd=tcp::8022-:22,hostfwd=tcp::8888-:8080
-gokrazy-update::
- gokr-packer -update yes $(IMAGE_OPTIONS)
+# update instance
+gok-update::
+ ${GOK} update yes
soc::
@echo Version: $(VERSION) $(SHA) $(BUILD_DATE)
diff --git a/api/api.go b/api/api.go
index 9a01f7de1c..605de89590 100644
--- a/api/api.go
+++ b/api/api.go
@@ -7,7 +7,7 @@ import (
"time"
)
-//go:generate mockgen -package api -destination mock.go github.com/evcc-io/evcc/api Charger,ChargeState,CurrentLimiter,CurrentGetter,PhaseSwitcher,PhaseGetter,Identifier,Meter,MeterEnergy,PhaseCurrents,Vehicle,ChargeRater,Battery,Tariff,BatteryController,Circuit
+//go:generate go tool mockgen -package api -destination mock.go github.com/evcc-io/evcc/api Charger,ChargeState,CurrentLimiter,CurrentGetter,PhaseSwitcher,PhaseGetter,Identifier,Meter,MeterEnergy,PhaseCurrents,Vehicle,ChargeRater,Battery,Tariff,BatteryController,Circuit
// Meter provides total active power in W
type Meter interface {
@@ -44,8 +44,8 @@ type BatteryCapacity interface {
Capacity() float64
}
-// MaxACPower provides max AC power in W
-type MaxACPower interface {
+// MaxACPowerGetter provides max AC power in W
+type MaxACPowerGetter interface {
MaxACPower() float64
}
@@ -120,7 +120,8 @@ type Authorizer interface {
Authorize(key string) error
}
-// PhaseDescriber returns the number of available phases
+// PhaseDescriber returns the number of physically connected phases
+// Used for vehicles and to limit switch sockets to 1p only
type PhaseDescriber interface {
Phases() int
}
diff --git a/api/batterymode.go b/api/batterymode.go
index 01bd617038..4ca00b3f12 100644
--- a/api/batterymode.go
+++ b/api/batterymode.go
@@ -3,7 +3,7 @@ package api
// BatteryMode is the home battery operation mode. Valid values are normal, locked and charge
type BatteryMode int
-//go:generate enumer -type BatteryMode -trimprefix Battery -transform=lower
+//go:generate go tool enumer -type BatteryMode -trimprefix Battery -transform=lower
const (
BatteryUnknown BatteryMode = iota
BatteryNormal
diff --git a/api/error.go b/api/error.go
index ce4d78accd..672dddd3f3 100644
--- a/api/error.go
+++ b/api/error.go
@@ -9,10 +9,13 @@ var ErrNotAvailable = errors.New("not available")
var ErrMustRetry = errors.New("must retry")
// ErrSponsorRequired indicates that a sponsor token is required
-var ErrSponsorRequired = errors.New("sponsorship required, see https://github.com/evcc-io/evcc#sponsorship")
+var ErrSponsorRequired = errors.New("sponsorship required, see https://docs.evcc.io/docs/sponsorship")
// ErrMissingCredentials indicates that user/password are missing
-var ErrMissingCredentials = errors.New("missing credentials")
+var ErrMissingCredentials = errors.New("missing user/password credentials")
+
+// ErrMissingToken indicates that access/refresh tokens are missing
+var ErrMissingToken = errors.New("missing token credentials")
// ErrOutdated indicates that result is outdated
var ErrOutdated = errors.New("outdated")
diff --git a/api/feature.go b/api/feature.go
index 44b5240c5a..4909652305 100644
--- a/api/feature.go
+++ b/api/feature.go
@@ -2,7 +2,7 @@ package api
type Feature int
-//go:generate enumer -type Feature -text
+//go:generate go tool enumer -type Feature -text
const (
_ Feature = iota
Offline
diff --git a/api/globalconfig/types.go b/api/globalconfig/types.go
index 8e9b6e30fb..03365666da 100644
--- a/api/globalconfig/types.go
+++ b/api/globalconfig/types.go
@@ -7,7 +7,7 @@ import (
"time"
"github.com/evcc-io/evcc/api"
- "github.com/evcc-io/evcc/provider/mqtt"
+ "github.com/evcc-io/evcc/plugin/mqtt"
"github.com/evcc-io/evcc/push"
"github.com/evcc-io/evcc/server/eebus"
"github.com/evcc-io/evcc/util/config"
@@ -38,7 +38,7 @@ type All struct {
Vehicles []config.Named
Tariffs Tariffs
Site map[string]interface{}
- Loadpoints []map[string]interface{}
+ Loadpoints []config.Named
Circuits []config.Named
}
@@ -54,8 +54,8 @@ type Go struct {
type ModbusProxy struct {
Port int
- ReadOnly string
- modbus.Settings `mapstructure:",squash"`
+ ReadOnly string `yaml:",omitempty" json:",omitempty"`
+ modbus.Settings `mapstructure:",squash" yaml:",inline,omitempty" json:",omitempty"`
}
var _ api.Redactor = (*Hems)(nil)
@@ -134,12 +134,17 @@ type Messaging struct {
Services []config.Typed
}
+func (c Messaging) Configured() bool {
+ return len(c.Services) > 0 || len(c.Events) > 0
+}
+
type Tariffs struct {
Currency string
Grid config.Typed
FeedIn config.Typed
Co2 config.Typed
Planner config.Typed
+ Solar []config.Typed
}
type Network struct {
diff --git a/api/rates.go b/api/rates.go
index 14ccfb1085..59891322c1 100644
--- a/api/rates.go
+++ b/api/rates.go
@@ -1,7 +1,7 @@
package api
import (
- "fmt"
+ "encoding/json"
"slices"
"time"
)
@@ -13,8 +13,8 @@ type Rate struct {
Price float64 `json:"price"`
}
-// IsEmpty returns is the rate is the zero value
-func (r Rate) IsEmpty() bool {
+// IsZero returns is the rate is the zero value
+func (r Rate) IsZero() bool {
return r.Start.IsZero() && r.End.IsZero() && r.Price == 0
}
@@ -22,23 +22,32 @@ func (r Rate) IsEmpty() bool {
type Rates []Rate
// Sort rates by start time
-func (r Rates) Sort() {
- slices.SortStableFunc(r, func(i, j Rate) int {
+func (rr Rates) Sort() {
+ slices.SortStableFunc(rr, func(i, j Rate) int {
return i.Start.Compare(j.Start)
})
}
-// Current returns the rates current rate or error
-func (r Rates) Current(now time.Time) (Rate, error) {
- for _, rr := range r {
- if !rr.Start.After(now) && rr.End.After(now) {
- return rr, nil
+// At returns the rate for given timestamp or error.
+// Rates MUST be sorted by start time.
+func (rr Rates) At(ts time.Time) (Rate, error) {
+ if i, ok := slices.BinarySearchFunc(rr, ts, func(r Rate, ts time.Time) int {
+ switch {
+ case ts.Before(r.Start):
+ return +1
+ case !ts.Before(r.End):
+ return -1
+ default:
+ return 0
}
+ }); ok {
+ return rr[i], nil
}
- if len(r) == 0 {
- return Rate{}, fmt.Errorf("no matching rate for: %s", now.Local().Format(time.RFC3339))
- }
- return Rate{}, fmt.Errorf("no matching rate for: %s, %d rates (%s to %s)",
- now.Local().Format(time.RFC3339), len(r), r[0].Start.Local().Format(time.RFC3339), r[len(r)-1].End.Local().Format(time.RFC3339))
+ return Rate{}, ErrNotAvailable
+}
+
+// MarshalMQTT implements server.MQTTMarshaler
+func (r Rates) MarshalMQTT() ([]byte, error) {
+ return json.Marshal(r)
}
diff --git a/api/rates_test.go b/api/rates_test.go
new file mode 100644
index 0000000000..97baa13138
--- /dev/null
+++ b/api/rates_test.go
@@ -0,0 +1,38 @@
+package api
+
+import (
+ "testing"
+ "time"
+
+ "github.com/benbjohnson/clock"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestRates(t *testing.T) {
+ clock := clock.NewMock()
+ rate := func(start int, val float64) Rate {
+ return Rate{
+ Start: clock.Now().Add(time.Duration(start) * time.Hour),
+ End: clock.Now().Add(time.Duration(start+1) * time.Hour),
+ Price: val,
+ }
+ }
+
+ rr := Rates{rate(1, 1), rate(2, 2), rate(3, 3), rate(4, 4)}
+
+ _, err := rr.At(clock.Now())
+ assert.Error(t, err)
+
+ for i := 1; i <= 4; i++ {
+ r, err := rr.At(clock.Now().Add(time.Duration(i) * time.Hour))
+ assert.NoError(t, err)
+ assert.Equal(t, float64(i), r.Price)
+
+ r, err = rr.At(clock.Now().Add(time.Duration(i)*time.Hour + 30*time.Minute))
+ assert.NoError(t, err)
+ assert.Equal(t, float64(i), r.Price)
+ }
+
+ _, err = rr.At(clock.Now().Add(5 * time.Hour))
+ assert.Error(t, err)
+}
diff --git a/api/reason.go b/api/reason.go
index 8d3439c556..4d8900ec39 100644
--- a/api/reason.go
+++ b/api/reason.go
@@ -2,7 +2,7 @@ package api
type Reason int
-//go:generate enumer -type Reason -trimprefix Reason -transform=lower
+//go:generate go tool enumer -type Reason -trimprefix Reason -transform=lower
const (
ReasonUnknown Reason = iota
ReasonWaitingForAuthorization
diff --git a/api/tariff.go b/api/tariff.go
index dcb5f6dbff..ca6063b684 100644
--- a/api/tariff.go
+++ b/api/tariff.go
@@ -1,6 +1,7 @@
package api
-//go:generate enumer -type TariffType -trimprefix TariffType -transform=lower -text
+//go:generate go tool enumer -type TariffType -trimprefix TariffType -transform=lower -text
+//go:generate go tool enumer -type TariffUsage -trimprefix TariffUsage -transform=lower
type TariffType int
@@ -10,4 +11,16 @@ const (
TariffTypePriceDynamic
TariffTypePriceForecast
TariffTypeCo2
+ TariffTypeSolar
+)
+
+type TariffUsage int
+
+const (
+ _ TariffUsage = iota
+ TariffUsageCo2
+ TariffUsageFeedIn
+ TariffUsageGrid
+ TariffUsagePlanner
+ TariffUsageSolar
)
diff --git a/api/tarifftype_enumer.go b/api/tarifftype_enumer.go
index b44f8cd7be..12272d9bc8 100644
--- a/api/tarifftype_enumer.go
+++ b/api/tarifftype_enumer.go
@@ -7,11 +7,11 @@ import (
"strings"
)
-const _TariffTypeName = "pricestaticpricedynamicpriceforecastco2"
+const _TariffTypeName = "pricestaticpricedynamicpriceforecastco2solar"
-var _TariffTypeIndex = [...]uint8{0, 11, 23, 36, 39}
+var _TariffTypeIndex = [...]uint8{0, 11, 23, 36, 39, 44}
-const _TariffTypeLowerName = "pricestaticpricedynamicpriceforecastco2"
+const _TariffTypeLowerName = "pricestaticpricedynamicpriceforecastco2solar"
func (i TariffType) String() string {
i -= 1
@@ -29,9 +29,10 @@ func _TariffTypeNoOp() {
_ = x[TariffTypePriceDynamic-(2)]
_ = x[TariffTypePriceForecast-(3)]
_ = x[TariffTypeCo2-(4)]
+ _ = x[TariffTypeSolar-(5)]
}
-var _TariffTypeValues = []TariffType{TariffTypePriceStatic, TariffTypePriceDynamic, TariffTypePriceForecast, TariffTypeCo2}
+var _TariffTypeValues = []TariffType{TariffTypePriceStatic, TariffTypePriceDynamic, TariffTypePriceForecast, TariffTypeCo2, TariffTypeSolar}
var _TariffTypeNameToValueMap = map[string]TariffType{
_TariffTypeName[0:11]: TariffTypePriceStatic,
@@ -42,6 +43,8 @@ var _TariffTypeNameToValueMap = map[string]TariffType{
_TariffTypeLowerName[23:36]: TariffTypePriceForecast,
_TariffTypeName[36:39]: TariffTypeCo2,
_TariffTypeLowerName[36:39]: TariffTypeCo2,
+ _TariffTypeName[39:44]: TariffTypeSolar,
+ _TariffTypeLowerName[39:44]: TariffTypeSolar,
}
var _TariffTypeNames = []string{
@@ -49,6 +52,7 @@ var _TariffTypeNames = []string{
_TariffTypeName[11:23],
_TariffTypeName[23:36],
_TariffTypeName[36:39],
+ _TariffTypeName[39:44],
}
// TariffTypeString retrieves an enum value from the enum constants string name.
diff --git a/api/tariffusage_enumer.go b/api/tariffusage_enumer.go
new file mode 100644
index 0000000000..2eadfda902
--- /dev/null
+++ b/api/tariffusage_enumer.go
@@ -0,0 +1,91 @@
+// Code generated by "enumer -type TariffUsage -trimprefix TariffUsage -transform=lower"; DO NOT EDIT.
+
+package api
+
+import (
+ "fmt"
+ "strings"
+)
+
+const _TariffUsageName = "co2feedingridplannersolar"
+
+var _TariffUsageIndex = [...]uint8{0, 3, 9, 13, 20, 25}
+
+const _TariffUsageLowerName = "co2feedingridplannersolar"
+
+func (i TariffUsage) String() string {
+ i -= 1
+ if i < 0 || i >= TariffUsage(len(_TariffUsageIndex)-1) {
+ return fmt.Sprintf("TariffUsage(%d)", i+1)
+ }
+ return _TariffUsageName[_TariffUsageIndex[i]:_TariffUsageIndex[i+1]]
+}
+
+// An "invalid array index" compiler error signifies that the constant values have changed.
+// Re-run the stringer command to generate them again.
+func _TariffUsageNoOp() {
+ var x [1]struct{}
+ _ = x[TariffUsageCo2-(1)]
+ _ = x[TariffUsageFeedIn-(2)]
+ _ = x[TariffUsageGrid-(3)]
+ _ = x[TariffUsagePlanner-(4)]
+ _ = x[TariffUsageSolar-(5)]
+}
+
+var _TariffUsageValues = []TariffUsage{TariffUsageCo2, TariffUsageFeedIn, TariffUsageGrid, TariffUsagePlanner, TariffUsageSolar}
+
+var _TariffUsageNameToValueMap = map[string]TariffUsage{
+ _TariffUsageName[0:3]: TariffUsageCo2,
+ _TariffUsageLowerName[0:3]: TariffUsageCo2,
+ _TariffUsageName[3:9]: TariffUsageFeedIn,
+ _TariffUsageLowerName[3:9]: TariffUsageFeedIn,
+ _TariffUsageName[9:13]: TariffUsageGrid,
+ _TariffUsageLowerName[9:13]: TariffUsageGrid,
+ _TariffUsageName[13:20]: TariffUsagePlanner,
+ _TariffUsageLowerName[13:20]: TariffUsagePlanner,
+ _TariffUsageName[20:25]: TariffUsageSolar,
+ _TariffUsageLowerName[20:25]: TariffUsageSolar,
+}
+
+var _TariffUsageNames = []string{
+ _TariffUsageName[0:3],
+ _TariffUsageName[3:9],
+ _TariffUsageName[9:13],
+ _TariffUsageName[13:20],
+ _TariffUsageName[20:25],
+}
+
+// TariffUsageString retrieves an enum value from the enum constants string name.
+// Throws an error if the param is not part of the enum.
+func TariffUsageString(s string) (TariffUsage, error) {
+ if val, ok := _TariffUsageNameToValueMap[s]; ok {
+ return val, nil
+ }
+
+ if val, ok := _TariffUsageNameToValueMap[strings.ToLower(s)]; ok {
+ return val, nil
+ }
+ return 0, fmt.Errorf("%s does not belong to TariffUsage values", s)
+}
+
+// TariffUsageValues returns all values of the enum
+func TariffUsageValues() []TariffUsage {
+ return _TariffUsageValues
+}
+
+// TariffUsageStrings returns a slice of all String values of the enum
+func TariffUsageStrings() []string {
+ strs := make([]string, len(_TariffUsageNames))
+ copy(strs, _TariffUsageNames)
+ return strs
+}
+
+// IsATariffUsage returns "true" if the value is listed in the enum definition. "false" otherwise
+func (i TariffUsage) IsATariffUsage() bool {
+ for _, v := range _TariffUsageValues {
+ if i == v {
+ return true
+ }
+ }
+ return false
+}
diff --git a/assets/css/app.css b/assets/css/app.css
index 1a83bf9de1..5e26d9bad6 100644
--- a/assets/css/app.css
+++ b/assets/css/app.css
@@ -25,8 +25,9 @@
--evcc-green: #baffcb;
--evcc-dark-green: #0fde41ff;
--evcc-darker-green: #0ba631ff;
+ --evcc-darker-green-rgb: 11, 166, 49;
--evcc-darkest-green: #076f20ff;
- --evcc-darkest-green-rgb: 11, 166, 49;
+ --evcc-darkest-green-rgb: 7, 111, 32;
--evcc-yellow: #faf000;
--evcc-dark-yellow: #bbb400;
--evcc-orange: #ff9000;
@@ -46,6 +47,8 @@
--evcc-pv: var(--evcc-dark-green);
--evcc-battery: var(--evcc-darker-green);
--evcc-export: var(--evcc-yellow);
+ --evcc-price: #ff912fff;
+ --evcc-co2: #00916eff;
--evcc-background: var(--bs-gray-bright);
--evcc-box: var(--bs-white);
--evcc-box-border: var(--bs-gray-brighter);
@@ -53,7 +56,7 @@
--evcc-gray: var(--bs-gray-medium);
--evcc-gray-50: #93949e80;
--evcc-gray-25: #93949e40;
-
+ --evcc-gray-10: #93949e20;
--evcc-accent1: var(--evcc-dark-yellow);
--evcc-accent2: var(--evcc-darker-green);
--evcc-accent3: var(--evcc-darkest-green);
@@ -164,6 +167,14 @@ h5 {
color: var(--bs-primary) !important;
}
+.text-price {
+ color: var(--evcc-price) !important;
+}
+
+.text-co2 {
+ color: var(--evcc-co2) !important;
+}
+
a {
color: var(--bs-primary);
}
@@ -269,10 +280,16 @@ a:hover {
--bs-btn-hover-border-color: var(--bs-gray-medium);
}
-.btn-link:hover {
+.btn-link:hover,
+.btn-link:focus {
color: var(--bs-primary);
}
+.btn-link:focus {
+ outline: var(--bs-focus-ring-width) solid var(--bs-focus-ring-color);
+ outline-width: var(--bs-focus-ring-width);
+}
+
.dark .btn-outline-secondary {
--bs-btn-color: var(--bs-gray-bright);
--bs-btn-border-color: var(--bs-gray-bright);
@@ -364,6 +381,16 @@ a:hover {
transition-timing-function: linear;
}
+.modal.fade-left .modal-dialog {
+ transform: translate(-50px, 0);
+}
+.modal.fade-right .modal-dialog {
+ transform: translate(50px, 0);
+}
+.modal.show .modal-dialog {
+ transform: none;
+}
+
.modal-header {
padding: 0 0 1rem 0;
border: none;
@@ -517,8 +544,13 @@ input::-webkit-datetime-edit {
.form-control {
color: var(--evcc-default-text);
}
-.form-control:disabled {
- color: var(--bs-gray-dark);
+.form-control:disabled,
+.form-control:read-only,
+.dark .form-control:disabled,
+.dark .form-control:read-only {
+ background-color: var(--evcc-gray-10);
+ color: var(--bs-default-text);
+ opacity: 1;
}
.dark .form-control {
@@ -633,6 +665,12 @@ html.app .modal-dialog {
color: var(--evcc-darkest-green);
}
+.hyphenate {
+ overflow-wrap: break-word;
+ word-wrap: break-word;
+ hyphens: auto;
+}
+
input::-webkit-date-and-time-value {
text-align: left;
}
diff --git a/assets/js/auth.js b/assets/js/auth.js
index b5faed53e1..befa07ad3c 100644
--- a/assets/js/auth.js
+++ b/assets/js/auth.js
@@ -7,7 +7,8 @@ import { isSystemError } from "./utils/fatal";
const auth = reactive({
configured: true,
loggedIn: null, // true / false / null (unknown)
- nextUrl: null,
+ nextUrl: null, // url to navigate to after login
+ nextModal: null, // modal instance to show after login
});
export async function updateAuthStatus() {
@@ -64,8 +65,15 @@ export function getAndClearNextUrl() {
return nextUrl;
}
-export function openLoginModal(nextUrl = null) {
+export function getAndClearNextModal() {
+ const nextModal = auth.nextModal;
+ auth.nextModal = null;
+ return nextModal;
+}
+
+export function openLoginModal(nextUrl = null, nextModal = null) {
auth.nextUrl = nextUrl;
+ auth.nextModal = nextModal;
const modal = Modal.getOrCreateInstance(document.getElementById("loginModal"));
modal.show();
}
diff --git a/assets/js/colors.js b/assets/js/colors.js
index 2e536a66f8..25f2be0766 100644
--- a/assets/js/colors.js
+++ b/assets/js/colors.js
@@ -15,9 +15,10 @@ const colors = reactive({
grid: null,
co2PerKWh: null,
pricePerKWh: null,
- price: "#FF912FFF",
- co2: "#00916EFF",
+ price: null,
+ co2: null,
background: null,
+ light: null,
selfPalette: ["#0FDE41FF", "#FFBD2FFF", "#FD6158FF", "#03C1EFFF", "#0F662DFF", "#FF922EFF"],
palette: [
"#03C1EFFF",
@@ -35,6 +36,18 @@ const colors = reactive({
],
});
+export const dimColor = (color) => {
+ return color.toLowerCase().replace(/ff$/, "20");
+};
+
+export const lighterColor = (color) => {
+ return color.toLowerCase().replace(/ff$/, "aa");
+};
+
+export const fullColor = (color) => {
+ return color.toLowerCase().replace(/20$/, "ff");
+};
+
function updateCssColors() {
const style = window.getComputedStyle(document.documentElement);
colors.text = style.getPropertyValue("--evcc-default-text");
@@ -42,22 +55,21 @@ function updateCssColors() {
colors.border = style.getPropertyValue("--bs-border-color-translucent");
colors.self = style.getPropertyValue("--evcc-self");
colors.grid = style.getPropertyValue("--evcc-grid");
+ colors.price = style.getPropertyValue("--evcc-price");
+ colors.co2 = style.getPropertyValue("--evcc-co2");
colors.background = style.getPropertyValue("--evcc-background");
colors.pricePerKWh = style.getPropertyValue("--bs-gray-medium");
colors.co2PerKWh = style.getPropertyValue("--bs-gray-medium");
+ colors.light = style.getPropertyValue("--bs-gray-light");
}
// update colors on theme change
const darkModeMatcher = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)");
-darkModeMatcher?.addEventListener("change", updateCssColors);
+if (darkModeMatcher && darkModeMatcher.addEventListener) {
+ darkModeMatcher.addEventListener("change", updateCssColors);
+}
+// initialize colors
updateCssColors();
-
-export const dimColor = (color) => {
- return color.toLowerCase().replace(/ff$/, "20");
-};
-
-export const fullColor = (color) => {
- return color.toLowerCase().replace(/20$/, "ff");
-};
+window.requestAnimationFrame(updateCssColors);
export default colors;
diff --git a/assets/js/components/BatterySettingsModal.vue b/assets/js/components/BatterySettingsModal.vue
index 5727e0122c..bc82bb390e 100644
--- a/assets/js/components/BatterySettingsModal.vue
+++ b/assets/js/components/BatterySettingsModal.vue
@@ -7,7 +7,7 @@
@open="modalVisible"
@closed="modalInvisible"
>
-