Optimizer: enable by default (#28213)
This commit is contained in:
parent
f24b670312
commit
b93061bc86
13 changed files with 151 additions and 6 deletions
68
assets/js/components/Config/OptimizerModal.vue
Normal file
68
assets/js/components/Config/OptimizerModal.vue
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
<template>
|
||||
<GenericModal
|
||||
id="optimizerModal"
|
||||
:title="$t('config.optimizer.title')"
|
||||
config-modal-name="optimizer"
|
||||
data-testid="optimizer-modal"
|
||||
>
|
||||
<p>
|
||||
{{ $t("config.optimizer.description") }}
|
||||
<a :href="docsLink" target="_blank">{{ $t("config.general.docsLink") }}</a>
|
||||
</p>
|
||||
<ErrorMessage :error="error" />
|
||||
<div class="form-check form-switch my-3">
|
||||
<input
|
||||
id="optimizerEnabled"
|
||||
:checked="enabled"
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
@change="change"
|
||||
/>
|
||||
<div class="form-check-label">
|
||||
<label for="optimizerEnabled">
|
||||
{{ $t("config.optimizer.enable") }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</GenericModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import GenericModal from "../Helper/GenericModal.vue";
|
||||
import ErrorMessage from "../Helper/ErrorMessage.vue";
|
||||
import api from "@/api";
|
||||
import store from "@/store";
|
||||
import { docsPrefix } from "@/i18n";
|
||||
import type { AxiosError } from "axios";
|
||||
|
||||
export default defineComponent({
|
||||
name: "OptimizerModal",
|
||||
components: { GenericModal, ErrorMessage },
|
||||
data() {
|
||||
return {
|
||||
error: null as string | null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
enabled(): boolean {
|
||||
return !!store.state?.optimizer;
|
||||
},
|
||||
docsLink(): string {
|
||||
return `${docsPrefix()}/docs/features/optimizer`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async change(e: Event) {
|
||||
try {
|
||||
this.error = null;
|
||||
await api.post(`config/optimizer/${(e.target as HTMLInputElement).checked}`);
|
||||
} catch (err) {
|
||||
const e = err as AxiosError<{ error: string }>;
|
||||
this.error = e.response?.data?.error || e.message;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
18
assets/js/components/MaterialIcon/Optimizer.vue
Normal file
18
assets/js/components/MaterialIcon/Optimizer.vue
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<template>
|
||||
<svg :style="svgStyle" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M3.75 19.75q-.325-.325-.325-.75t.325-.75L9.8 12.2q.15-.15.325-.213t.375-.062q.2 0 .375.062t.325.213l3.3 3.3l6.4-7.2q.275-.325.713-.325t.737.3q.275.275.287.663t-.262.687L15.2 17.7q-.15.175-.337.263t-.388.087q-.2 0-.387-.075t-.338-.225L10.5 14.5l-5.25 5.25q-.325.325-.75.325t-.75-.325ZM4 13.3q-.125 0-.25-.063t-.2-.212l-.5-1.075l-1.075-.5q-.15-.075-.213-.2T1.7 11q0-.125.063-.25t.212-.2l1.075-.5l.5-1.075q.075-.15.2-.212T4 8.7q.125 0 .25.063t.2.212l.5 1.075l1.075.5q.275.125.275.45t-.275.45l-1.075.5l-.5 1.075q-.075.15-.2.212T4 13.3Zm11-2q-.125 0-.25-.063t-.2-.212l-.5-1.075l-1.075-.5q-.15-.075-.212-.2T12.7 9q0-.125.063-.25t.212-.2l1.075-.5l.5-1.075q.075-.15.2-.212T15 6.7q.125 0 .25.063t.2.212l.5 1.075l1.075.5q.15.075.213.2T17.3 9q0 .125-.063.25t-.212.2l-1.075.5l-.5 1.075q-.075.15-.2.213T15 11.3Zm-6.5-3q-.125 0-.25-.075T8.05 8L7.4 6.6L6 5.95q-.15-.075-.225-.2T5.7 5.5q0-.125.075-.25T6 5.05l1.4-.65l.65-1.4q.075-.15.2-.225T8.5 2.7q.125 0 .25.075t.2.225l.65 1.4l1.4.65q.15.075.225.2t.075.25q0 .125-.075.25t-.225.2l-1.4.65L8.95 8q-.075.15-.2.225T8.5 8.3Z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import icon from "@/mixins/icon";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Optimizer",
|
||||
mixins: [icon],
|
||||
});
|
||||
</script>
|
||||
|
|
@ -112,6 +112,7 @@ export interface State {
|
|||
config?: string;
|
||||
database?: string;
|
||||
ocpp?: Ocpp;
|
||||
optimizer?: boolean;
|
||||
}
|
||||
|
||||
export interface ConfigStatus<C, S> {
|
||||
|
|
|
|||
|
|
@ -321,6 +321,19 @@
|
|||
<DeviceTags :tags="hemsTags" />
|
||||
</template>
|
||||
</DeviceCard>
|
||||
<DeviceCard
|
||||
v-if="experimental"
|
||||
:title="`${$t('config.optimizer.title')} 🧪`"
|
||||
editable
|
||||
:unconfigured="isUnconfigured(optimizerTags)"
|
||||
data-testid="optimizer"
|
||||
@edit="openModal('optimizer')"
|
||||
>
|
||||
<template #icon><OptimizerIcon /></template>
|
||||
<template #tags>
|
||||
<DeviceTags :tags="optimizerTags" />
|
||||
</template>
|
||||
</DeviceCard>
|
||||
</div>
|
||||
|
||||
<h2 class="my-4 mt-5">{{ $t("config.section.services") }}</h2>
|
||||
|
|
@ -402,6 +415,7 @@
|
|||
<TariffsLegacyModal @changed="loadDirty" />
|
||||
<TariffModal :currency="currency" @changed="tariffChanged" />
|
||||
<TelemetryModal :sponsor="sponsor" :telemetry="telemetry" />
|
||||
<OptimizerModal />
|
||||
<ExperimentalModal :experimental="experimental" />
|
||||
<TitleModal @changed="loadDirty" />
|
||||
<ModbusProxyModal :is-sponsor="isSponsor" @changed="loadDirty" />
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue