diff --git a/assets/js/components/Config/OptimizerModal.vue b/assets/js/components/Config/OptimizerModal.vue new file mode 100644 index 000000000..a5a84aa56 --- /dev/null +++ b/assets/js/components/Config/OptimizerModal.vue @@ -0,0 +1,68 @@ + + + diff --git a/assets/js/components/MaterialIcon/Optimizer.vue b/assets/js/components/MaterialIcon/Optimizer.vue new file mode 100644 index 000000000..b2a2e45a5 --- /dev/null +++ b/assets/js/components/MaterialIcon/Optimizer.vue @@ -0,0 +1,18 @@ + + + diff --git a/assets/js/types/evcc.ts b/assets/js/types/evcc.ts index 8da356ec0..d53863682 100644 --- a/assets/js/types/evcc.ts +++ b/assets/js/types/evcc.ts @@ -112,6 +112,7 @@ export interface State { config?: string; database?: string; ocpp?: Ocpp; + optimizer?: boolean; } export interface ConfigStatus { diff --git a/assets/js/views/Config.vue b/assets/js/views/Config.vue index b435e3f55..affb87be4 100644 --- a/assets/js/views/Config.vue +++ b/assets/js/views/Config.vue @@ -321,6 +321,19 @@ + + + +

{{ $t("config.section.services") }}

@@ -402,6 +415,7 @@ + @@ -461,6 +475,8 @@ import MqttIcon from "../components/MaterialIcon/Mqtt.vue"; import MqttModal from "../components/Config/MqttModal.vue"; import NetworkModal from "../components/Config/NetworkModal.vue"; import NotificationIcon from "../components/MaterialIcon/Notification.vue"; +import OptimizerIcon from "../components/MaterialIcon/Optimizer.vue"; +import OptimizerModal from "../components/Config/OptimizerModal.vue"; import restart, { performRestart } from "../restart"; import SponsorModal from "../components/Config/SponsorModal.vue"; import store from "../store"; @@ -541,6 +557,8 @@ export default defineComponent({ MqttModal, NetworkModal, NotificationIcon, + OptimizerIcon, + OptimizerModal, SponsorModal, TariffsLegacyModal, TariffCard, @@ -763,6 +781,10 @@ export default defineComponent({ eebus() { return store.state?.eebus; }, + optimizerTags(): DeviceTags { + if (!store.state?.optimizer) return { configured: { value: false } }; + return { configured: { value: true } }; + }, modbusproxyTags(): DeviceTags { const config = store.state?.modbusproxy || []; if (config.length > 0) { diff --git a/cmd/root.go b/cmd/root.go index 283cb0173..a1d10adab 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -392,6 +392,7 @@ func runRoot(cmd *cobra.Command, args []string) { valueChan <- util.Param{Key: keys.System, Val: util.System()} valueChan <- util.Param{Key: keys.Timezone, Val: time.Now().Format("MST -07:00")} valueChan <- util.Param{Key: keys.Experimental, Val: isExperimental()} + valueChan <- util.Param{Key: keys.Optimizer, Val: isOptimizer()} // run shutdown functions on stop var once sync.Once diff --git a/cmd/setup.go b/cmd/setup.go index c186355b6..e1574da8b 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -1315,3 +1315,9 @@ func isExperimental() bool { b, _ := settings.Bool(keys.Experimental) return b } + +// isOptimizer returns if optimizer is enabled +func isOptimizer() bool { + b, _ := settings.Bool(keys.Optimizer) + return b +} diff --git a/core/keys/global.go b/core/keys/global.go index 99420e193..f8fa5eba1 100644 --- a/core/keys/global.go +++ b/core/keys/global.go @@ -28,6 +28,7 @@ const ( SetupRequired = "setupRequired" // initial setup is required (lp = 0), fresh installation Plant = "plant" Telemetry = "telemetry" + Optimizer = "optimizer" DemoMode = "demoMode" AuthDisabled = "authDisabled" AuthProviders = "authProviders" diff --git a/core/site.go b/core/site.go index 2e6d66c2b..0d9ceb0eb 100644 --- a/core/site.go +++ b/core/site.go @@ -767,13 +767,19 @@ func (site *Site) updateMeters() error { return err } - if sponsor.IsAuthorized() { + if sponsor.IsAuthorized() && optimizerEnabled() { go site.optimizerUpdateAsync() } return nil } +func optimizerEnabled() bool { + exp, _ := settings.Bool(keys.Experimental) + opt, _ := settings.Bool(keys.Optimizer) + return exp && opt +} + func (site *Site) updateHomeConsumption(homePower float64) { site.householdEnergy.AddPower(homePower) diff --git a/core/site_optimizer.go b/core/site_optimizer.go index f19c72fb7..b79659067 100644 --- a/core/site_optimizer.go +++ b/core/site_optimizer.go @@ -54,6 +54,8 @@ func (r optimizerResult) MarshalBytes() ([]byte, error) { type batteryType string const ( + OPTIMIZER_URI = "https://optimizer.evcc.io" + batteryTypeLoadpoint batteryType = "loadpoint" batteryTypeVehicle batteryType = "vehicle" batteryTypeBattery batteryType = "battery" @@ -127,11 +129,6 @@ func (site *Site) optimizerUpdateAsync() { } func (site *Site) optimizerUpdate(battery []types.Measurement) error { - uri := os.Getenv("OPTIMIZER_URI") - if uri == "" { - return nil - } - solarTariff := site.GetTariff(api.TariffUsageSolar) solar := currentRates(solarTariff) @@ -237,6 +234,7 @@ func (site *Site) optimizerUpdate(battery []types.Measurement) error { httpClient := request.NewClient(site.log) httpClient.Timeout = 30 * time.Second + uri := lo.CoalesceOrEmpty(os.Getenv("OPTIMIZER_URI"), OPTIMIZER_URI) apiClient, err := optimizer.NewClientWithResponses(uri, optimizer.WithHTTPClient(httpClient)) if err != nil { return err diff --git a/i18n/de.json b/i18n/de.json index 844c0de65..4e34cb620 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -599,6 +599,11 @@ "url": "Server-URL", "urlHelp": "Kopiere diese URL in die Konfiguration deiner Wallbox. Details findest du im Handbuch des Herstellers. Die Wallbox sollte automatisch ihre eindeutige Kennung (Station-ID) an die URL anhängen. In seltenen Fällen musst du die Kennung manuell angeben. Beispiel: `{url}`" }, + "optimizer": { + "description": "Analysiert Solarprognose, Strompreise und deinen typischen Verbrauch, um Batterie- und Ladestrategie zu optimieren. Daten werden zur Berechnung an den evcc Optimierungsdienst übertragen.", + "enable": "Optimizer aktivieren", + "title": "Optimizer" + }, "options": { "boolean": { "no": "nein", diff --git a/i18n/en.json b/i18n/en.json index bfd017351..f87439e35 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -599,6 +599,11 @@ "url": "Server URL", "urlHelp": "Copy this URL into your charger's configuration. Check the manufacturer's manual for details. The charger is expected to automatically append its unique identifier (station ID) to the url. In rare cases, you may need to manually specify the identifier. Example: `{url}`" }, + "optimizer": { + "description": "Analyzes solar forecast, electricity prices, and your consumption patterns to optimize battery and charging strategy. Data is sent to the evcc optimization service for processing.", + "enable": "Enable Optimizer", + "title": "Optimizer" + }, "options": { "boolean": { "no": "no", diff --git a/server/http.go b/server/http.go index aa353b752..4279605d1 100644 --- a/server/http.go +++ b/server/http.go @@ -295,6 +295,7 @@ func (s *HTTPd) RegisterSystemHandler(site *core.Site, pub publisher, cache *uti "updatesponsortoken": {"POST", "/sponsortoken", updateSponsortokenHandler(pub)}, "deletesponsortoken": {"DELETE", "/sponsortoken", deleteSponsorTokenHandler(pub)}, "experimental": {"POST", "/experimental/{value:[01truefalse]+}", boolHandler(setExperimental(pub), getExperimental)}, + "optimizer": {"POST", "/optimizer/{value:[01truefalse]+}", boolHandler(setOptimizer(pub), getOptimizer)}, } // yaml handlers diff --git a/server/http_config_site_other_handler.go b/server/http_config_site_other_handler.go index b59efff52..8127b9b14 100644 --- a/server/http_config_site_other_handler.go +++ b/server/http_config_site_other_handler.go @@ -10,6 +10,19 @@ import ( "github.com/evcc-io/evcc/util/sponsor" ) +func setOptimizer(pub publisher) func(bool) error { + return func(b bool) error { + settings.SetBool(keys.Optimizer, b) + pub(keys.Optimizer, b) + return nil + } +} + +func getOptimizer() bool { + b, _ := settings.Bool(keys.Optimizer) + return b +} + func setExperimental(pub publisher) func(bool) error { return func(b bool) error { settings.SetBool(keys.Experimental, b)