diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 83c06b6c0e..3fc314b843 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -47,7 +47,7 @@ body: label: Log details render: text description: > - Show evcc log output by running with evcc --log debug. The evcc service **MUST** be stopped before running this command. + 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. - type: dropdown validations: diff --git a/.github/workflows/default.yml b/.github/workflows/default.yml index d783620729..de7f2441ea 100644 --- a/.github/workflows/default.yml +++ b/.github/workflows/default.yml @@ -134,11 +134,9 @@ jobs: - run: mkdir dist && touch dist/empty - name: Lint - uses: golangci/golangci-lint-action@v4 + uses: golangci/golangci-lint-action@v5 with: version: latest - skip-pkg-cache: true - skip-build-cache: true args: --out-format=colored-line-number --timeout 5m ui: @@ -179,11 +177,6 @@ jobs: integration: name: Integration runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - shardIndex: [1, 2] - shardTotal: [2] steps: - uses: actions/checkout@v4 @@ -226,7 +219,7 @@ jobs: run: npx playwright install --with-deps chromium - name: Run tests - run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} + run: npx playwright test # - name: Run tests # uses: docker://mcr.microsoft.com/playwright:v1.34.3-jammy @@ -236,6 +229,6 @@ jobs: - uses: actions/upload-artifact@v4 if: ${{ !cancelled() }} with: - name: playwright-report-${{ matrix.shardIndex }} + name: playwright-report path: playwright-report/ retention-days: 14 diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 97d81fc69e..d929665501 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -27,7 +27,7 @@ jobs: run: make install docs - name: Deploy to docs repo - uses: peaceiris/actions-gh-pages@v3 + uses: peaceiris/actions-gh-pages@v4 with: personal_token: ${{ secrets.GH_TOKEN }} publish_dir: ./templates/docs @@ -36,4 +36,4 @@ jobs: destination_dir: ${{ github.event_name == 'release' && 'templates/release' || github.event_name == 'schedule' && 'templates/nightly' || 'templates/unknown_trigger' }} allow_empty_commit: false commit_message: ${{ github.event_name == 'release' && 'Templates update for release' || github.event_name == 'schedule' && 'Templates update for nightly' || 'Templates update unknown trigger' }} - if: ${{ success() }} + if: success() diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index da93ba98b5..07536caa32 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -28,7 +28,7 @@ jobs: run: rm templates/evcc.io/.gitignore - name: Deploy to evcc.io repo - uses: peaceiris/actions-gh-pages@v3 + uses: peaceiris/actions-gh-pages@v4 with: personal_token: ${{ secrets.GH_TOKEN }} publish_dir: ./templates/evcc.io/ @@ -37,4 +37,4 @@ jobs: destination_dir: data allow_empty_commit: false commit_message: Brand data update - if: ${{ success() }} + if: success() diff --git a/.gitignore b/.gitignore index 4b8c120c44..81e4c508cf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -__debug_bin +__debug_bin* .vscode .cache .DS_store diff --git a/.golangci.yml b/.golangci.yml index 6369a2f2d0..ee6d9976f1 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -16,7 +16,6 @@ linters: enable: - dogsled - durationcheck - - exportloopref - gci - gofmt - goimports @@ -40,6 +39,7 @@ linters: # fixme # - bodyclose # - exhaustive + # - exportloopref # - gocritic # - godot # - gomoddirectives diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..e8ddf5e15a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,121 @@ +## Contributing + +### Developing + +#### Development environment + +Developing evcc requires [Go][1] 1.22 and [Node][2] 18. 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. + +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. You can manually reformat your code by running: + +```sh +make lint +make lint-ui +``` + +#### Changing device templates + +evcc supports a massive amount of different devices. To keep our documentation and website in sync with the latest software the core project (this repo) generates meta-data that's pushed to the `docs` and `evcc.io` repository. Make sure to update this meta-data every time you make changes to a templates. + +```sh +make docs +``` + +If you miss one of the above steps Gitub Actions will likely trigger a **Porcelain** error. + +### Building from source + +Install prerequisites (once): + +```sh +make install-ui +make install +``` + +Build and run: + +```sh +make +./evcc +``` + +Open UI at http://127.0.0.1:7070 + +To run without creating the `evcc` binary use: + + go run ./... + +#### Cross Compiling + +To compile a version for an ARM device like a Raspberry Pi set GO command variables as needed, eg: + +```sh +GOOS=linux GOARCH=arm GOARM=6 make +``` + +#### Publishing docker images + +```sh +make docker DOCKER_IMAGE=my/docker DOCKER_TAG=0815 +``` + +### Debugging in VS Code + +#### evcc Core + +To debug a local evcc build in VS Code, add the following entry to your `launch.json`. +You can adjust the referred configuration as needed to e.g. use your live configuration. + +```json +{ + "name": "Launch evcc local build with demo config", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}", + "args": ["-c", "${workspaceFolder}/cmd/demo.yaml"], + "cwd": "${workspaceFolder}", +}, +``` + +#### UI + +For frontend development start the Vue toolchain in dev-mode. Open http://127.0.0.1:7071/ to get to the live reloading development server. It pulls its data from port 7070 (see above). + +```sh +npm install +npm run dev +``` + +#### Integration testing + +We use Playwright for end-to-end integration tests. They start a local evcc instance with different configuration yamls and prefilled databases. To run them, you have to do a local build first. + +```sh +make ui build +npm run playwright +``` + +#### Simulating device state + +Since we don't want to run tests against 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 speed up testing. + +``` +make ui build +./evcc --config tests/simulator.evcc.yaml +``` + +### Adding or modifying translations + +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. + +[![Languages](https://hosted.weblate.org/widgets/evcc/-/evcc/multi-auto.svg)](https://hosted.weblate.org/engage/evcc/) + +[1]: https://go.dev +[2]: https://nodejs.org/ diff --git a/README.md b/README.md index 5e63a3bac0..3150a206b8 100644 --- a/README.md +++ b/README.md @@ -34,94 +34,17 @@ evcc is an extensible EV Charge Controller and home energy management system. Fe - logging using [InfluxDB](https://www.influxdata.com) and [Grafana](https://grafana.com/grafana/) - granular charge power control down to mA steps with supported chargers (labeled by e.g. smartWB as [OLC](https://board.evse-wifi.de/viewtopic.php?f=16&t=187)) - REST and MQTT [APIs](https://docs.evcc.io/docs/reference/api) for integration with home automation systems -- Add-ons for [HomeAssistant](https://github.com/evcc-io/evcc-hassio-addon) and [OpenHAB](https://www.openhab.org/addons/bindings/evcc) (not maintained by the evcc core team) +- Add-ons for [Home Assistant](https://github.com/evcc-io/evcc-hassio-addon) and [OpenHAB](https://www.openhab.org/addons/bindings/evcc) (not maintained by the evcc core team) ## Getting Started You'll find everything you need in our [documentation](https://docs.evcc.io/). -## Contribute +## Contributing -To build evcc from source, [Go][1] 1.22 and [Node][2] 18 are required. - -Build and run go backend. The UI becomes available at http://127.0.0.1:7070/ - -```sh -make install-ui -make ui -make install -make -./evcc -``` - -### Cross Compile - -To compile a version for an ARM device like a Raspberry Pi set GO command variables as needed, eg: - -```sh -GOOS=linux GOARCH=arm GOARM=6 make -``` - -### UI development - -For frontend development start the Vue toolchain in dev-mode. Open http://127.0.0.1:7071/ to get to the livelreloading development server. It pulls its data from port 7070 (see above). - -```sh -npm install -npm run dev -``` - -### Integration tests - -We use Playwright for end-to-end integration tests. They start a local evcc instance with different configuration yamls and prefilled databases. To run them, you have to do a local build first. - -```sh -make ui build -npm run playwright -``` - -#### Simulating device state - -Since we don't want to run tests against 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 speed 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 [Vetur](https://marketplace.visualstudio.com/items?itemName=octref.vetur) extension. You can manually reformat your code by running: - -```sh -make lint -make lint-ui -``` - -### Changing templates - -evcc supports a massive amount of different devices. To keep our documentation and website in sync with the latest software the core project (this repo) generates meta-data that's pushed to the `docs` and `evcc.io` repository. Make sure to update this meta-data every time you make changes to a templates. - -```sh -make docs -``` - -If you miss one of the above steps Gitub Actions will likely trigger a **Porcelain** error. - -### Adding or modifying translations - -evcc already includes many translations for the UI. Weblate Hosted is used to maintain all languages. Feel free to add more languages or verify and edit existing translations. Weblate will automatically push all modifications on a regular base to the evcc repository. +Technical details on how to contribute, how to add translations and how to build evcc from source can be found [here](CONTRIBUTING.md). [![Weblate Hosted](https://hosted.weblate.org/widgets/evcc/-/evcc/287x66-grey.png)](https://hosted.weblate.org/engage/evcc/) -[![Languages](https://hosted.weblate.org/widgets/evcc/-/evcc/multi-auto.svg)](https://hosted.weblate.org/engage/evcc/) - -https://hosted.weblate.org/projects/evcc/evcc/ ## Sponsorship @@ -133,6 +56,3 @@ Maintaining evcc consumes time and effort. With the vast amount of different dev While evcc is open source, we would also like to encourage vendors to provide open source hardware devices, public documentation and support open source projects like ours that provide additional value to otherwise closed hardware. Where this is not the case, evcc requires "sponsor token" to finance ongoing development and support of evcc. The personal sponsor token requires a [Github Sponsorship](https://github.com/sponsors/evcc-io) and can be requested at [sponsor.evcc.io](https://sponsor.evcc.io/). - -[1]: https://go.dev -[2]: https://nodejs.org/ diff --git a/api/api.go b/api/api.go index 77500df088..cc6a9ea7ff 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,Vehicle,ChargeRater,Battery,Tariff,BatteryController +//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 // Meter provides total active power in W type Meter interface { @@ -202,3 +202,32 @@ type FeatureDescriber interface { type CsvWriter interface { WriteCsv(context.Context, io.Writer) error } + +// CircuitMeasurements is the measurements a circuit or load must deliver +type CircuitMeasurements interface { + GetChargePower() float64 + GetMaxPhaseCurrent() float64 +} + +// CircuitLoad represents a loadpoint attached to a circuit +type CircuitLoad interface { + CircuitMeasurements + GetCircuit() Circuit +} + +// Circuit defines the load control domain +type Circuit interface { + CircuitMeasurements + GetTitle() string + SetTitle(string) + GetParent() Circuit + RegisterChild(child Circuit) + HasMeter() bool + GetMaxPower() float64 + GetMaxCurrent() float64 + SetMaxPower(float64) + SetMaxCurrent(float64) + Update([]CircuitLoad) error + ValidateCurrent(old, new float64) float64 + ValidatePower(old, new float64) float64 +} diff --git a/api/feature.go b/api/feature.go index 44b5240c5a..15d7e200f4 100644 --- a/api/feature.go +++ b/api/feature.go @@ -10,5 +10,4 @@ const ( IntegratedDevice Heating Retryable - WelcomeCharge ) diff --git a/api/feature_enumer.go b/api/feature_enumer.go index 736284087b..858b77ac91 100644 --- a/api/feature_enumer.go +++ b/api/feature_enumer.go @@ -7,11 +7,11 @@ import ( "strings" ) -const _FeatureName = "OfflineCoarseCurrentIntegratedDeviceHeatingRetryableWelcomeCharge" +const _FeatureName = "OfflineCoarseCurrentIntegratedDeviceHeatingRetryable" -var _FeatureIndex = [...]uint8{0, 7, 20, 36, 43, 52, 65} +var _FeatureIndex = [...]uint8{0, 7, 20, 36, 43, 52} -const _FeatureLowerName = "offlinecoarsecurrentintegrateddeviceheatingretryablewelcomecharge" +const _FeatureLowerName = "offlinecoarsecurrentintegrateddeviceheatingretryable" func (i Feature) String() string { i -= 1 @@ -30,10 +30,9 @@ func _FeatureNoOp() { _ = x[IntegratedDevice-(3)] _ = x[Heating-(4)] _ = x[Retryable-(5)] - _ = x[WelcomeCharge-(6)] } -var _FeatureValues = []Feature{Offline, CoarseCurrent, IntegratedDevice, Heating, Retryable, WelcomeCharge} +var _FeatureValues = []Feature{Offline, CoarseCurrent, IntegratedDevice, Heating, Retryable} var _FeatureNameToValueMap = map[string]Feature{ _FeatureName[0:7]: Offline, @@ -46,8 +45,6 @@ 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{ @@ -56,7 +53,6 @@ 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/mock.go b/api/mock.go index c3f79d5258..9b81c990ad 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,Vehicle,ChargeRater,Battery,Tariff,BatteryController) +// Source: github.com/evcc-io/evcc/api (interfaces: Charger,ChargeState,CurrentLimiter,PhaseSwitcher,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,Vehicle,ChargeRater,Battery,Tariff,BatteryController +// 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 // // Package api is a generated GoMock package. @@ -324,6 +324,46 @@ func (mr *MockMeterEnergyMockRecorder) TotalEnergy() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TotalEnergy", reflect.TypeOf((*MockMeterEnergy)(nil).TotalEnergy)) } +// MockPhaseCurrents is a mock of PhaseCurrents interface. +type MockPhaseCurrents struct { + ctrl *gomock.Controller + recorder *MockPhaseCurrentsMockRecorder +} + +// MockPhaseCurrentsMockRecorder is the mock recorder for MockPhaseCurrents. +type MockPhaseCurrentsMockRecorder struct { + mock *MockPhaseCurrents +} + +// NewMockPhaseCurrents creates a new mock instance. +func NewMockPhaseCurrents(ctrl *gomock.Controller) *MockPhaseCurrents { + mock := &MockPhaseCurrents{ctrl: ctrl} + mock.recorder = &MockPhaseCurrentsMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPhaseCurrents) EXPECT() *MockPhaseCurrentsMockRecorder { + return m.recorder +} + +// Currents mocks base method. +func (m *MockPhaseCurrents) Currents() (float64, float64, float64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Currents") + ret0, _ := ret[0].(float64) + ret1, _ := ret[1].(float64) + ret2, _ := ret[2].(float64) + ret3, _ := ret[3].(error) + return ret0, ret1, ret2, ret3 +} + +// Currents indicates an expected call of Currents. +func (mr *MockPhaseCurrentsMockRecorder) Currents() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Currents", reflect.TypeOf((*MockPhaseCurrents)(nil).Currents)) +} + // MockVehicle is a mock of Vehicle interface. type MockVehicle struct { ctrl *gomock.Controller @@ -636,3 +676,214 @@ func (mr *MockBatteryControllerMockRecorder) SetBatteryMode(arg0 any) *gomock.Ca mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetBatteryMode", reflect.TypeOf((*MockBatteryController)(nil).SetBatteryMode), arg0) } + +// MockCircuit is a mock of Circuit interface. +type MockCircuit struct { + ctrl *gomock.Controller + recorder *MockCircuitMockRecorder +} + +// MockCircuitMockRecorder is the mock recorder for MockCircuit. +type MockCircuitMockRecorder struct { + mock *MockCircuit +} + +// NewMockCircuit creates a new mock instance. +func NewMockCircuit(ctrl *gomock.Controller) *MockCircuit { + mock := &MockCircuit{ctrl: ctrl} + mock.recorder = &MockCircuitMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockCircuit) EXPECT() *MockCircuitMockRecorder { + return m.recorder +} + +// GetChargePower mocks base method. +func (m *MockCircuit) GetChargePower() float64 { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetChargePower") + ret0, _ := ret[0].(float64) + return ret0 +} + +// GetChargePower indicates an expected call of GetChargePower. +func (mr *MockCircuitMockRecorder) GetChargePower() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChargePower", reflect.TypeOf((*MockCircuit)(nil).GetChargePower)) +} + +// GetMaxCurrent mocks base method. +func (m *MockCircuit) GetMaxCurrent() float64 { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetMaxCurrent") + ret0, _ := ret[0].(float64) + return ret0 +} + +// GetMaxCurrent indicates an expected call of GetMaxCurrent. +func (mr *MockCircuitMockRecorder) GetMaxCurrent() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMaxCurrent", reflect.TypeOf((*MockCircuit)(nil).GetMaxCurrent)) +} + +// GetMaxPhaseCurrent mocks base method. +func (m *MockCircuit) GetMaxPhaseCurrent() float64 { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetMaxPhaseCurrent") + ret0, _ := ret[0].(float64) + return ret0 +} + +// GetMaxPhaseCurrent indicates an expected call of GetMaxPhaseCurrent. +func (mr *MockCircuitMockRecorder) GetMaxPhaseCurrent() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMaxPhaseCurrent", reflect.TypeOf((*MockCircuit)(nil).GetMaxPhaseCurrent)) +} + +// GetMaxPower mocks base method. +func (m *MockCircuit) GetMaxPower() float64 { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetMaxPower") + ret0, _ := ret[0].(float64) + return ret0 +} + +// GetMaxPower indicates an expected call of GetMaxPower. +func (mr *MockCircuitMockRecorder) GetMaxPower() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMaxPower", reflect.TypeOf((*MockCircuit)(nil).GetMaxPower)) +} + +// GetParent mocks base method. +func (m *MockCircuit) GetParent() Circuit { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetParent") + ret0, _ := ret[0].(Circuit) + return ret0 +} + +// GetParent indicates an expected call of GetParent. +func (mr *MockCircuitMockRecorder) GetParent() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetParent", reflect.TypeOf((*MockCircuit)(nil).GetParent)) +} + +// GetTitle mocks base method. +func (m *MockCircuit) GetTitle() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetTitle") + ret0, _ := ret[0].(string) + return ret0 +} + +// GetTitle indicates an expected call of GetTitle. +func (mr *MockCircuitMockRecorder) GetTitle() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTitle", reflect.TypeOf((*MockCircuit)(nil).GetTitle)) +} + +// HasMeter mocks base method. +func (m *MockCircuit) HasMeter() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HasMeter") + ret0, _ := ret[0].(bool) + return ret0 +} + +// HasMeter indicates an expected call of HasMeter. +func (mr *MockCircuitMockRecorder) HasMeter() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasMeter", reflect.TypeOf((*MockCircuit)(nil).HasMeter)) +} + +// RegisterChild mocks base method. +func (m *MockCircuit) RegisterChild(arg0 Circuit) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "RegisterChild", arg0) +} + +// RegisterChild indicates an expected call of RegisterChild. +func (mr *MockCircuitMockRecorder) RegisterChild(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegisterChild", reflect.TypeOf((*MockCircuit)(nil).RegisterChild), arg0) +} + +// SetMaxCurrent mocks base method. +func (m *MockCircuit) SetMaxCurrent(arg0 float64) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetMaxCurrent", arg0) +} + +// SetMaxCurrent indicates an expected call of SetMaxCurrent. +func (mr *MockCircuitMockRecorder) SetMaxCurrent(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetMaxCurrent", reflect.TypeOf((*MockCircuit)(nil).SetMaxCurrent), arg0) +} + +// SetMaxPower mocks base method. +func (m *MockCircuit) SetMaxPower(arg0 float64) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetMaxPower", arg0) +} + +// SetMaxPower indicates an expected call of SetMaxPower. +func (mr *MockCircuitMockRecorder) SetMaxPower(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetMaxPower", reflect.TypeOf((*MockCircuit)(nil).SetMaxPower), arg0) +} + +// SetTitle mocks base method. +func (m *MockCircuit) SetTitle(arg0 string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetTitle", arg0) +} + +// SetTitle indicates an expected call of SetTitle. +func (mr *MockCircuitMockRecorder) SetTitle(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetTitle", reflect.TypeOf((*MockCircuit)(nil).SetTitle), arg0) +} + +// Update mocks base method. +func (m *MockCircuit) Update(arg0 []CircuitLoad) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// Update indicates an expected call of Update. +func (mr *MockCircuitMockRecorder) Update(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockCircuit)(nil).Update), arg0) +} + +// ValidateCurrent mocks base method. +func (m *MockCircuit) ValidateCurrent(arg0, arg1 float64) float64 { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ValidateCurrent", arg0, arg1) + ret0, _ := ret[0].(float64) + return ret0 +} + +// ValidateCurrent indicates an expected call of ValidateCurrent. +func (mr *MockCircuitMockRecorder) ValidateCurrent(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateCurrent", reflect.TypeOf((*MockCircuit)(nil).ValidateCurrent), arg0, arg1) +} + +// ValidatePower mocks base method. +func (m *MockCircuit) ValidatePower(arg0, arg1 float64) float64 { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ValidatePower", arg0, arg1) + ret0, _ := ret[0].(float64) + return ret0 +} + +// ValidatePower indicates an expected call of ValidatePower. +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) +} diff --git a/api/proto/pb/victron.pb.go b/api/proto/pb/victron.pb.go new file mode 100644 index 0000000000..fa4d17af67 --- /dev/null +++ b/api/proto/pb/victron.pb.go @@ -0,0 +1,249 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.34.1 +// protoc v5.26.1 +// source: proto/victron.proto + +package pb + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type VictronRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ProductId string `protobuf:"bytes,1,opt,name=productId,proto3" json:"productId,omitempty"` + VrmId string `protobuf:"bytes,2,opt,name=vrmId,proto3" json:"vrmId,omitempty"` + Serial string `protobuf:"bytes,3,opt,name=serial,proto3" json:"serial,omitempty"` + Board string `protobuf:"bytes,4,opt,name=board,proto3" json:"board,omitempty"` +} + +func (x *VictronRequest) Reset() { + *x = VictronRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_proto_victron_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *VictronRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VictronRequest) ProtoMessage() {} + +func (x *VictronRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_victron_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VictronRequest.ProtoReflect.Descriptor instead. +func (*VictronRequest) Descriptor() ([]byte, []int) { + return file_proto_victron_proto_rawDescGZIP(), []int{0} +} + +func (x *VictronRequest) GetProductId() string { + if x != nil { + return x.ProductId + } + return "" +} + +func (x *VictronRequest) GetVrmId() string { + if x != nil { + return x.VrmId + } + return "" +} + +func (x *VictronRequest) GetSerial() string { + if x != nil { + return x.Serial + } + return "" +} + +func (x *VictronRequest) GetBoard() string { + if x != nil { + return x.Board + } + return "" +} + +type VictronReply struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Authorized bool `protobuf:"varint,1,opt,name=authorized,proto3" json:"authorized,omitempty"` + Subject string `protobuf:"bytes,2,opt,name=subject,proto3" json:"subject,omitempty"` +} + +func (x *VictronReply) Reset() { + *x = VictronReply{} + if protoimpl.UnsafeEnabled { + mi := &file_proto_victron_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *VictronReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VictronReply) ProtoMessage() {} + +func (x *VictronReply) ProtoReflect() protoreflect.Message { + mi := &file_proto_victron_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VictronReply.ProtoReflect.Descriptor instead. +func (*VictronReply) Descriptor() ([]byte, []int) { + return file_proto_victron_proto_rawDescGZIP(), []int{1} +} + +func (x *VictronReply) GetAuthorized() bool { + if x != nil { + return x.Authorized + } + return false +} + +func (x *VictronReply) GetSubject() string { + if x != nil { + return x.Subject + } + return "" +} + +var File_proto_victron_proto protoreflect.FileDescriptor + +var file_proto_victron_proto_rawDesc = []byte{ + 0x0a, 0x13, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x76, 0x69, 0x63, 0x74, 0x72, 0x6f, 0x6e, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x72, 0x0a, 0x0e, 0x56, 0x69, 0x63, 0x74, 0x72, 0x6f, 0x6e, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x70, 0x72, 0x6f, 0x64, 0x75, + 0x63, 0x74, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x64, + 0x75, 0x63, 0x74, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x72, 0x6d, 0x49, 0x64, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x72, 0x6d, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, + 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x65, 0x72, + 0x69, 0x61, 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x22, 0x48, 0x0a, 0x0c, 0x56, 0x69, 0x63, + 0x74, 0x72, 0x6f, 0x6e, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x61, 0x75, 0x74, + 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x61, + 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x62, + 0x6a, 0x65, 0x63, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x75, 0x62, 0x6a, + 0x65, 0x63, 0x74, 0x32, 0x3c, 0x0a, 0x07, 0x56, 0x69, 0x63, 0x74, 0x72, 0x6f, 0x6e, 0x12, 0x31, + 0x0a, 0x0d, 0x49, 0x73, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x12, + 0x0f, 0x2e, 0x56, 0x69, 0x63, 0x74, 0x72, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x0d, 0x2e, 0x56, 0x69, 0x63, 0x74, 0x72, 0x6f, 0x6e, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, + 0x00, 0x42, 0x0a, 0x5a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_proto_victron_proto_rawDescOnce sync.Once + file_proto_victron_proto_rawDescData = file_proto_victron_proto_rawDesc +) + +func file_proto_victron_proto_rawDescGZIP() []byte { + file_proto_victron_proto_rawDescOnce.Do(func() { + file_proto_victron_proto_rawDescData = protoimpl.X.CompressGZIP(file_proto_victron_proto_rawDescData) + }) + return file_proto_victron_proto_rawDescData +} + +var file_proto_victron_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_proto_victron_proto_goTypes = []interface{}{ + (*VictronRequest)(nil), // 0: VictronRequest + (*VictronReply)(nil), // 1: VictronReply +} +var file_proto_victron_proto_depIdxs = []int32{ + 0, // 0: Victron.IsValidDevice:input_type -> VictronRequest + 1, // 1: Victron.IsValidDevice:output_type -> VictronReply + 1, // [1:2] is the sub-list for method output_type + 0, // [0:1] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_proto_victron_proto_init() } +func file_proto_victron_proto_init() { + if File_proto_victron_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_proto_victron_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*VictronRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proto_victron_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*VictronReply); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_proto_victron_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_proto_victron_proto_goTypes, + DependencyIndexes: file_proto_victron_proto_depIdxs, + MessageInfos: file_proto_victron_proto_msgTypes, + }.Build() + File_proto_victron_proto = out.File + file_proto_victron_proto_rawDesc = nil + file_proto_victron_proto_goTypes = nil + file_proto_victron_proto_depIdxs = nil +} diff --git a/api/proto/pb/victron_grpc.pb.go b/api/proto/pb/victron_grpc.pb.go new file mode 100644 index 0000000000..c93af94445 --- /dev/null +++ b/api/proto/pb/victron_grpc.pb.go @@ -0,0 +1,109 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.3.0 +// - protoc v5.26.1 +// source: proto/victron.proto + +package pb + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +const ( + Victron_IsValidDevice_FullMethodName = "/Victron/IsValidDevice" +) + +// VictronClient is the client API for Victron service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type VictronClient interface { + IsValidDevice(ctx context.Context, in *VictronRequest, opts ...grpc.CallOption) (*VictronReply, error) +} + +type victronClient struct { + cc grpc.ClientConnInterface +} + +func NewVictronClient(cc grpc.ClientConnInterface) VictronClient { + return &victronClient{cc} +} + +func (c *victronClient) IsValidDevice(ctx context.Context, in *VictronRequest, opts ...grpc.CallOption) (*VictronReply, error) { + out := new(VictronReply) + err := c.cc.Invoke(ctx, Victron_IsValidDevice_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// VictronServer is the server API for Victron service. +// All implementations must embed UnimplementedVictronServer +// for forward compatibility +type VictronServer interface { + IsValidDevice(context.Context, *VictronRequest) (*VictronReply, error) + mustEmbedUnimplementedVictronServer() +} + +// UnimplementedVictronServer must be embedded to have forward compatible implementations. +type UnimplementedVictronServer struct { +} + +func (UnimplementedVictronServer) IsValidDevice(context.Context, *VictronRequest) (*VictronReply, error) { + return nil, status.Errorf(codes.Unimplemented, "method IsValidDevice not implemented") +} +func (UnimplementedVictronServer) mustEmbedUnimplementedVictronServer() {} + +// UnsafeVictronServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to VictronServer will +// result in compilation errors. +type UnsafeVictronServer interface { + mustEmbedUnimplementedVictronServer() +} + +func RegisterVictronServer(s grpc.ServiceRegistrar, srv VictronServer) { + s.RegisterService(&Victron_ServiceDesc, srv) +} + +func _Victron_IsValidDevice_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(VictronRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(VictronServer).IsValidDevice(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Victron_IsValidDevice_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(VictronServer).IsValidDevice(ctx, req.(*VictronRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// Victron_ServiceDesc is the grpc.ServiceDesc for Victron service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Victron_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "Victron", + HandlerType: (*VictronServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "IsValidDevice", + Handler: _Victron_IsValidDevice_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "proto/victron.proto", +} diff --git a/api/proto/victron.proto b/api/proto/victron.proto new file mode 100644 index 0000000000..9c6614c67c --- /dev/null +++ b/api/proto/victron.proto @@ -0,0 +1,21 @@ +syntax = "proto3"; + +// protoc proto/victron.proto --go_out=. --go-grpc_out=. + +option go_package = "proto/pb"; + +service Victron { + rpc IsValidDevice (VictronRequest) returns (VictronReply) {} +} + +message VictronRequest { + string productId = 1; + string vrmId = 2; + string serial = 3; + string board = 4; +} + +message VictronReply { + bool authorized = 1; + string subject = 2; +} diff --git a/assets/js/components/BatterySettingsModal.vue b/assets/js/components/BatterySettingsModal.vue index 2140db998e..9fa382ba66 100644 --- a/assets/js/components/BatterySettingsModal.vue +++ b/assets/js/components/BatterySettingsModal.vue @@ -436,7 +436,7 @@ export default { return { transform: `scale(${scale})` }; }, fmtSoc(soc) { - return `${Math.round(soc)}%`; + return this.fmtPercentage(soc); }, async changeDischargeControl(e) { try { diff --git a/assets/js/components/ChargingPlan.vue b/assets/js/components/ChargingPlan.vue index 7fb993577e..3e4060a3b7 100644 --- a/assets/js/components/ChargingPlan.vue +++ b/assets/js/components/ChargingPlan.vue @@ -176,7 +176,7 @@ export default { return this.targetChargeEnabled || this.minSocEnabled; }, minSocLabel: function () { - return `${Math.round(this.minSoc)}%`; + return this.fmtPercentage(this.minSoc); }, modalId: function () { return `chargingPlanModal_${this.id}`; @@ -204,7 +204,7 @@ export default { }, targetSocLabel: function () { if (this.socBasedPlanning) { - return `${Math.round(this.effectivePlanSoc)}%`; + return this.fmtPercentage(this.effectivePlanSoc); } return fmtEnergy( this.planEnergy, diff --git a/assets/js/components/ChargingPlanArrival.vue b/assets/js/components/ChargingPlanArrival.vue index a42f09e5c7..a94270a998 100644 --- a/assets/js/components/ChargingPlanArrival.vue +++ b/assets/js/components/ChargingPlanArrival.vue @@ -20,7 +20,11 @@ - {{ $t("main.loadpointSettings.minSoc.description", [selectedMinSoc || "x"]) }} + {{ + $t("main.loadpointSettings.minSoc.description", [ + selectedMinSoc ? fmtPercentage(selectedMinSoc) : "x", + ]) + }}
diff --git a/assets/js/components/ChargingPlanSettingsEntry.vue b/assets/js/components/ChargingPlanSettingsEntry.vue index df877066a6..50bfd04de2 100644 --- a/assets/js/components/ChargingPlanSettingsEntry.vue +++ b/assets/js/components/ChargingPlanSettingsEntry.vue @@ -176,6 +176,7 @@ export default { this.capacity || 100, this.socPerKwh, this.fmtKWh, + this.fmtPercentage, "-" ); // remove the first entry (0) diff --git a/assets/js/components/ChargingPlanWarnings.vue b/assets/js/components/ChargingPlanWarnings.vue index bdb0cbc9f6..48899ba245 100644 --- a/assets/js/components/ChargingPlanWarnings.vue +++ b/assets/js/components/ChargingPlanWarnings.vue @@ -129,7 +129,7 @@ export default { }, methods: { fmtSoc(soc) { - return `${Math.round(soc)}%`; + return this.fmtPercentage(soc); }, }, }; diff --git a/assets/js/components/ChargingSessionModal.vue b/assets/js/components/ChargingSessionModal.vue index 6721878ac0..d5f8c2d466 100644 --- a/assets/js/components/ChargingSessionModal.vue +++ b/assets/js/components/ChargingSessionModal.vue @@ -79,9 +79,8 @@ {{ $t("sessions.solar") }} - {{ fmtNumber(session.solarPercentage, 1) }}% ({{ - fmtKWh(solarEnergy, solarEnergy >= 1e3) - }}) + {{ fmtPercentage(session.solarPercentage, 1) }} + ({{ fmtKWh(solarEnergy, solarEnergy >= 1e3) }}) diff --git a/assets/js/components/Config/DeviceTags.vue b/assets/js/components/Config/DeviceTags.vue index bc1ea75847..594621eca4 100644 --- a/assets/js/components/Config/DeviceTags.vue +++ b/assets/js/components/Config/DeviceTags.vue @@ -41,7 +41,8 @@ export default { case "chargedEnergy": return this.fmtKWh(value * 1e3); case "soc": - return `${this.fmtNumber(value, 1)}%`; + case "socLimit": + return this.fmtPercentage(value, 1); case "odometer": case "range": return `${this.fmtNumber(value, 0)} km`; @@ -53,8 +54,6 @@ export default { return value.map((v) => this.fmtKw(v)).join(", "); case "chargeStatus": return value; - case "socLimit": - return `${this.fmtNumber(value)}%`; } return value; }, diff --git a/assets/js/components/Config/MeterModal.vue b/assets/js/components/Config/MeterModal.vue index f2990d9686..6af8d7b894 100644 --- a/assets/js/components/Config/MeterModal.vue +++ b/assets/js/components/Config/MeterModal.vue @@ -235,9 +235,19 @@ export default { modbusCapabilities() { return this.modbus?.Choice || []; }, + modbusDefaults() { + const { ID, Comset, Baudrate, Port } = this.modbus || {}; + return { + id: ID, + comset: Comset, + baudrate: Baudrate, + port: Port, + }; + }, apiData() { return { template: this.templateName, + ...this.modbusDefaults, ...this.values, usage: this.meterType, }; diff --git a/assets/js/components/Energyflow/BatteryIcon.vue b/assets/js/components/Energyflow/BatteryIcon.vue index c3199d8c45..c216399e87 100644 --- a/assets/js/components/Energyflow/BatteryIcon.vue +++ b/assets/js/components/Energyflow/BatteryIcon.vue @@ -1,13 +1,16 @@ diff --git a/assets/js/components/Energyflow/Energyflow.vue b/assets/js/components/Energyflow/Energyflow.vue index 857044bf9d..5d7ef22cf5 100644 --- a/assets/js/components/Energyflow/Energyflow.vue +++ b/assets/js/components/Energyflow/Energyflow.vue @@ -288,7 +288,7 @@ export default { return this.pv.map(({ power }) => this.fmtKw(power, this.powerInKw)); }, batteryFmt() { - return (soc) => `${Math.round(soc)}%`; + return (soc) => this.fmtPercentage(soc, 0); }, co2Available() { return this.smartCostType === CO2_TYPE; diff --git a/assets/js/components/Energyflow/Visualization.vue b/assets/js/components/Energyflow/Visualization.vue index a5f90fab64..1622861e67 100644 --- a/assets/js/components/Energyflow/Visualization.vue +++ b/assets/js/components/Energyflow/Visualization.vue @@ -202,7 +202,7 @@ export default { return { value, hideIcon: this.hideLabelIcon(value, minWidth), - style: { width: this.widthTotal(value) }, + style: { "flex-basis": this.widthTotal(value) }, [position]: true, }; }, @@ -269,7 +269,7 @@ html.dark .grid-import { overflow: hidden; } .visualization--ready :deep(.label-bar) { - transition-property: width, opacity; + transition-property: flex-basis, opacity; transition-duration: var(--evcc-transition-medium), var(--evcc-transition-fast); transition-timing-function: linear, ease; } diff --git a/assets/js/components/LimitEnergySelect.vue b/assets/js/components/LimitEnergySelect.vue index 27a1a3d8a9..1cad71da2a 100644 --- a/assets/js/components/LimitEnergySelect.vue +++ b/assets/js/components/LimitEnergySelect.vue @@ -57,6 +57,7 @@ export default { this.capacity || 100, this.socPerKwh, this.fmtKWh, + this.fmtPercentage, this.$t("main.targetEnergy.noLimit") ); }, @@ -75,7 +76,7 @@ export default { return fmtEnergy(value, this.step, this.fmtKWh, this.$t("main.targetEnergy.noLimit")); }, fmtSoc: function (value) { - return `+${Math.round(value)}%`; + return `+${this.fmtPercentage(value)}`; }, }, }; diff --git a/assets/js/components/LimitSocSelect.vue b/assets/js/components/LimitSocSelect.vue index 753bf111a0..8ce4183af7 100644 --- a/assets/js/components/LimitSocSelect.vue +++ b/assets/js/components/LimitSocSelect.vue @@ -67,7 +67,7 @@ export default { return null; }, formatSoc: function (value) { - return this.heating ? this.fmtTemperature(value) : `${Math.round(value)}%`; + return this.heating ? this.fmtTemperature(value) : this.fmtPercentage(value); }, formatKm: function (value) { return `${this.fmtNumber(value, 0)} ${distanceUnit()}`; diff --git a/assets/js/components/Loadpoint.vue b/assets/js/components/Loadpoint.vue index e8374af872..ab9ef1d024 100644 --- a/assets/js/components/Loadpoint.vue +++ b/assets/js/components/Loadpoint.vue @@ -182,7 +182,7 @@ export default { phaseRemaining: Number, pvRemaining: Number, pvAction: String, - smartCostLimit: Number, + smartCostLimit: { type: Number, default: 0 }, smartCostType: String, smartCostActive: Boolean, tariffGrid: Number, diff --git a/assets/js/components/LoadpointSessionInfo.vue b/assets/js/components/LoadpointSessionInfo.vue index 7c904e3ea5..46d6448e99 100644 --- a/assets/js/components/LoadpointSessionInfo.vue +++ b/assets/js/components/LoadpointSessionInfo.vue @@ -117,7 +117,7 @@ export default { return this.valueSm !== undefined; }, solarFormatted() { - return `${this.fmtNumber(this.sessionSolarPercentage, 1)}%`; + return this.fmtPercentage(this.sessionSolarPercentage, 1); }, priceFormatted() { return `${this.fmtMoney(this.sessionPrice, this.currency)} ${this.fmtCurrencySymbol( diff --git a/assets/js/components/Loadpoints.vue b/assets/js/components/Loadpoints.vue index 341e0a7858..648f0e87ba 100644 --- a/assets/js/components/Loadpoints.vue +++ b/assets/js/components/Loadpoints.vue @@ -63,9 +63,7 @@ export default { props: { loadpoints: Array, vehicles: Array, - smartCostLimit: Number, smartCostType: String, - smartCostActive: Boolean, tariffGrid: Number, tariffCo2: Number, currency: String, diff --git a/assets/js/components/LoginModal.vue b/assets/js/components/LoginModal.vue index e3ccb1b782..83a67be638 100644 --- a/assets/js/components/LoginModal.vue +++ b/assets/js/components/LoginModal.vue @@ -26,6 +26,15 @@

{{ $t("loginModal.error") }}{{ error }}

+ + {{ $t("loginModal.iframeHint") }} +