diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 3fc314b843..988900e2e2 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -48,6 +48,7 @@ body: render: text description: > Show evcc log output of the issue, see https://docs.evcc.io/en/docs/faq#how-do-i-create-a-log-file-for-error-analysis for instructions. + In case of issues with physical devices like chargers, meters or vehicles, make sure that the log file has level `trace` enabled for the device. - type: dropdown validations: diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8ac6b8c498..9a0107604f 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,3 +4,5 @@ updates: directory: "/" schedule: interval: "monthly" + labels: + - "infrastructure" diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 2204104f4d..91a33a5583 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -63,7 +63,7 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Publish - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 with: context: . platforms: linux/amd64,linux/arm64,linux/arm/v6 @@ -103,7 +103,7 @@ jobs: key: ${{ runner.os }}-${{ github.sha }}-dist - name: Create nightly build - uses: goreleaser/goreleaser-action@v5 + uses: goreleaser/goreleaser-action@v6 with: version: latest args: --snapshot -f .goreleaser-nightly.yml --clean diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 532e47bb66..27cfb478d6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -41,7 +41,7 @@ jobs: evcc/evcc - name: Publish - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 with: context: . platforms: linux/amd64,linux/arm64,linux/arm/v6 @@ -96,7 +96,7 @@ jobs: # run: make image-rootfs - name: Create Github Release - uses: goreleaser/goreleaser-action@v5 + uses: goreleaser/goreleaser-action@v6 with: version: latest args: release --clean diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml index 988724aeb4..a4d89a39d0 100644 --- a/.github/workflows/stale.yaml +++ b/.github/workflows/stale.yaml @@ -4,6 +4,8 @@ on: workflow_dispatch: schedule: - cron: "0 0 * * *" + issue_comment: + types: [created] jobs: stale: @@ -12,7 +14,7 @@ jobs: - uses: actions/stale@v9 id: stale with: - days-before-stale: 20 + days-before-stale: 7 days-before-close: 5 exempt-issue-labels: "pinned,security,backlog,bug" exempt-pr-labels: "pinned,security,backlog,bug" diff --git a/.goreleaser.yml b/.goreleaser.yml index 3be533dcd3..9fa16857ef 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,3 +1,5 @@ +version: 2 + dist: release release: github: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e8ddf5e15a..706a19f615 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -115,6 +115,13 @@ make ui build evcc already includes many translations for the UI. We're using [Weblate](https://hosted.weblate.org/projects/evcc/evcc/) to maintain translations. Feel free to add more languages or verify and edit existing translations. Weblate will automatically push all modifications to the evcc repository where they get reviewed and merged. +If you find a text that is not yet translatable in [Weblate](https://hosted.weblate.org/projects/evcc/evcc/), you can help us by making it translatable. To do this, you can simply find the missing translation text in the code and apply similar changes as in these two Pull Requests: + +- [UI: Add missing translation for Error during startup](https://github.com/evcc-io/evcc/pull/14695) +- [Translation: kein Plan, keine Grenze](https://github.com/evcc-io/evcc/pull/7461/) + +Note: To ensure the build succeeds after creating new translations, make sure to include your new translations in both the [de.toml](i18n/de.toml) and [en.toml](i18n/en.toml) files. + [![Languages](https://hosted.weblate.org/widgets/evcc/-/evcc/multi-auto.svg)](https://hosted.weblate.org/engage/evcc/) [1]: https://go.dev diff --git a/Dockerfile b/Dockerfile index e029d25ccd..1bc6303a21 100644 --- a/Dockerfile +++ b/Dockerfile @@ -84,13 +84,15 @@ COPY packaging/docker/bin/* /app/ # mDNS EXPOSE 5353/udp +# EEBus +EXPOSE 4712/tcp # UI and /api EXPOSE 7070/tcp # KEBA charger EXPOSE 7090/udp # OCPP charger EXPOSE 8887/tcp -# GoodWe Wifi Inverter +# Modbus UDP EXPOSE 8899/udp # SMA Energy Manager EXPOSE 9522/udp diff --git a/api/api.go b/api/api.go index cc6a9ea7ff..c7dcafb399 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,PhaseSwitcher,Identifier,Meter,MeterEnergy,PhaseCurrents,Vehicle,ChargeRater,Battery,Tariff,BatteryController,Circuit +//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 // Meter provides total active power in W type Meter interface { @@ -222,6 +222,7 @@ type Circuit interface { SetTitle(string) GetParent() Circuit RegisterChild(child Circuit) + Wrap(parent Circuit) error HasMeter() bool GetMaxPower() float64 GetMaxCurrent() float64 diff --git a/api/feature.go b/api/feature.go index 15d7e200f4..44b5240c5a 100644 --- a/api/feature.go +++ b/api/feature.go @@ -10,4 +10,5 @@ const ( IntegratedDevice Heating Retryable + WelcomeCharge ) diff --git a/api/feature_enumer.go b/api/feature_enumer.go index 858b77ac91..736284087b 100644 --- a/api/feature_enumer.go +++ b/api/feature_enumer.go @@ -7,11 +7,11 @@ import ( "strings" ) -const _FeatureName = "OfflineCoarseCurrentIntegratedDeviceHeatingRetryable" +const _FeatureName = "OfflineCoarseCurrentIntegratedDeviceHeatingRetryableWelcomeCharge" -var _FeatureIndex = [...]uint8{0, 7, 20, 36, 43, 52} +var _FeatureIndex = [...]uint8{0, 7, 20, 36, 43, 52, 65} -const _FeatureLowerName = "offlinecoarsecurrentintegrateddeviceheatingretryable" +const _FeatureLowerName = "offlinecoarsecurrentintegrateddeviceheatingretryablewelcomecharge" func (i Feature) String() string { i -= 1 @@ -30,9 +30,10 @@ func _FeatureNoOp() { _ = x[IntegratedDevice-(3)] _ = x[Heating-(4)] _ = x[Retryable-(5)] + _ = x[WelcomeCharge-(6)] } -var _FeatureValues = []Feature{Offline, CoarseCurrent, IntegratedDevice, Heating, Retryable} +var _FeatureValues = []Feature{Offline, CoarseCurrent, IntegratedDevice, Heating, Retryable, WelcomeCharge} var _FeatureNameToValueMap = map[string]Feature{ _FeatureName[0:7]: Offline, @@ -45,6 +46,8 @@ var _FeatureNameToValueMap = map[string]Feature{ _FeatureLowerName[36:43]: Heating, _FeatureName[43:52]: Retryable, _FeatureLowerName[43:52]: Retryable, + _FeatureName[52:65]: WelcomeCharge, + _FeatureLowerName[52:65]: WelcomeCharge, } var _FeatureNames = []string{ @@ -53,6 +56,7 @@ var _FeatureNames = []string{ _FeatureName[20:36], _FeatureName[36:43], _FeatureName[43:52], + _FeatureName[52:65], } // FeatureString retrieves an enum value from the enum constants string name. diff --git a/api/globalconfig/types.go b/api/globalconfig/types.go index a69765674e..aaac7b0ea7 100644 --- a/api/globalconfig/types.go +++ b/api/globalconfig/types.go @@ -6,9 +6,9 @@ import ( "strconv" "time" - "github.com/evcc-io/evcc/charger/eebus" "github.com/evcc-io/evcc/provider/mqtt" "github.com/evcc-io/evcc/push" + "github.com/evcc-io/evcc/server/eebus" "github.com/evcc-io/evcc/util/config" "github.com/evcc-io/evcc/util/modbus" ) diff --git a/api/mock.go b/api/mock.go index 9b81c990ad..983cfa8d52 100644 --- a/api/mock.go +++ b/api/mock.go @@ -1,9 +1,9 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/evcc-io/evcc/api (interfaces: Charger,ChargeState,CurrentLimiter,PhaseSwitcher,Identifier,Meter,MeterEnergy,PhaseCurrents,Vehicle,ChargeRater,Battery,Tariff,BatteryController,Circuit) +// Source: github.com/evcc-io/evcc/api (interfaces: Charger,ChargeState,CurrentLimiter,CurrentGetter,PhaseSwitcher,PhaseGetter,Identifier,Meter,MeterEnergy,PhaseCurrents,Vehicle,ChargeRater,Battery,Tariff,BatteryController,Circuit) // // Generated by this command: // -// mockgen -package api -destination mock.go github.com/evcc-io/evcc/api Charger,ChargeState,CurrentLimiter,PhaseSwitcher,Identifier,Meter,MeterEnergy,PhaseCurrents,Vehicle,ChargeRater,Battery,Tariff,BatteryController,Circuit +// 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 // // Package api is a generated GoMock package. @@ -173,6 +173,44 @@ func (mr *MockCurrentLimiterMockRecorder) GetMinMaxCurrent() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMinMaxCurrent", reflect.TypeOf((*MockCurrentLimiter)(nil).GetMinMaxCurrent)) } +// MockCurrentGetter is a mock of CurrentGetter interface. +type MockCurrentGetter struct { + ctrl *gomock.Controller + recorder *MockCurrentGetterMockRecorder +} + +// MockCurrentGetterMockRecorder is the mock recorder for MockCurrentGetter. +type MockCurrentGetterMockRecorder struct { + mock *MockCurrentGetter +} + +// NewMockCurrentGetter creates a new mock instance. +func NewMockCurrentGetter(ctrl *gomock.Controller) *MockCurrentGetter { + mock := &MockCurrentGetter{ctrl: ctrl} + mock.recorder = &MockCurrentGetterMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockCurrentGetter) EXPECT() *MockCurrentGetterMockRecorder { + return m.recorder +} + +// GetMaxCurrent mocks base method. +func (m *MockCurrentGetter) GetMaxCurrent() (float64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetMaxCurrent") + ret0, _ := ret[0].(float64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetMaxCurrent indicates an expected call of GetMaxCurrent. +func (mr *MockCurrentGetterMockRecorder) GetMaxCurrent() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMaxCurrent", reflect.TypeOf((*MockCurrentGetter)(nil).GetMaxCurrent)) +} + // MockPhaseSwitcher is a mock of PhaseSwitcher interface. type MockPhaseSwitcher struct { ctrl *gomock.Controller @@ -210,6 +248,44 @@ func (mr *MockPhaseSwitcherMockRecorder) Phases1p3p(arg0 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Phases1p3p", reflect.TypeOf((*MockPhaseSwitcher)(nil).Phases1p3p), arg0) } +// MockPhaseGetter is a mock of PhaseGetter interface. +type MockPhaseGetter struct { + ctrl *gomock.Controller + recorder *MockPhaseGetterMockRecorder +} + +// MockPhaseGetterMockRecorder is the mock recorder for MockPhaseGetter. +type MockPhaseGetterMockRecorder struct { + mock *MockPhaseGetter +} + +// NewMockPhaseGetter creates a new mock instance. +func NewMockPhaseGetter(ctrl *gomock.Controller) *MockPhaseGetter { + mock := &MockPhaseGetter{ctrl: ctrl} + mock.recorder = &MockPhaseGetterMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPhaseGetter) EXPECT() *MockPhaseGetterMockRecorder { + return m.recorder +} + +// GetPhases mocks base method. +func (m *MockPhaseGetter) GetPhases() (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPhases") + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetPhases indicates an expected call of GetPhases. +func (mr *MockPhaseGetterMockRecorder) GetPhases() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPhases", reflect.TypeOf((*MockPhaseGetter)(nil).GetPhases)) +} + // MockIdentifier is a mock of Identifier interface. type MockIdentifier struct { ctrl *gomock.Controller @@ -887,3 +963,17 @@ func (mr *MockCircuitMockRecorder) ValidatePower(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidatePower", reflect.TypeOf((*MockCircuit)(nil).ValidatePower), arg0, arg1) } + +// Wrap mocks base method. +func (m *MockCircuit) Wrap(arg0 Circuit) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Wrap", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// Wrap indicates an expected call of Wrap. +func (mr *MockCircuitMockRecorder) Wrap(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Wrap", reflect.TypeOf((*MockCircuit)(nil).Wrap), arg0) +} diff --git a/assets/css/app.css b/assets/css/app.css index f3ae7cee3d..f7880b5e61 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -250,6 +250,10 @@ a:hover { --bs-btn-hover-border-color: var(--bs-gray-medium); } +.btn-link:hover { + color: var(--bs-primary); +} + .dark .btn-outline-secondary { --bs-btn-color: var(--bs-gray-bright); --bs-btn-border-color: var(--bs-gray-bright); diff --git a/assets/js/auth.js b/assets/js/auth.js index f2f94b56c6..c2aaf8fc9f 100644 --- a/assets/js/auth.js +++ b/assets/js/auth.js @@ -1,6 +1,8 @@ import { reactive, watch } from "vue"; import api from "./api"; +import store from "./store"; import Modal from "bootstrap/js/dist/modal"; +import { isSystemError } from "./utils/fatal"; const auth = reactive({ configured: true, @@ -9,6 +11,11 @@ const auth = reactive({ }); export async function updateAuthStatus() { + if (store.state.offline || isSystemError(store.state.fatal)) { + // system not ready, skip auth check + return; + } + try { const res = await api.get("/auth/status", { validateStatus: (code) => [200, 501, 500].includes(code), @@ -72,4 +79,15 @@ watch( } ); +let timeoutId = null; +function debounedUpdateAuthStatus() { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + updateAuthStatus(); + }, 500); +} + +watch(() => store.state.offline, debounedUpdateAuthStatus); +watch(() => store.state.fatal, debounedUpdateAuthStatus); + export default auth; diff --git a/assets/js/components/ChargingPlan.vue b/assets/js/components/ChargingPlan.vue index 3e4060a3b7..cf74008360 100644 --- a/assets/js/components/ChargingPlan.vue +++ b/assets/js/components/ChargingPlan.vue @@ -2,17 +2,14 @@