Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions assets/js/components/Config/OptimizerModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<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 },
emits: ["changed"],
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}`);
this.$emit("changed");
} catch (err) {
const e = err as AxiosError<{ error: string }>;
this.error = e.response?.data?.error || e.message;
}
},
},
});
</script>
18 changes: 18 additions & 0 deletions assets/js/components/MaterialIcon/Optimizer.vue
Original file line number Diff line number Diff line change
@@ -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>
1 change: 1 addition & 0 deletions assets/js/types/evcc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export interface State {
config?: string;
database?: string;
ocpp?: Ocpp;
optimizer?: boolean;
}

export interface ConfigStatus<C, S> {
Expand Down
22 changes: 22 additions & 0 deletions assets/js/views/Config.vue
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,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>
Expand Down Expand Up @@ -400,6 +413,7 @@
<TariffsLegacyModal @changed="loadDirty" />
<TariffModal :currency="currency" @changed="tariffChanged" />
<TelemetryModal :sponsor="sponsor" :telemetry="telemetry" />
<OptimizerModal @changed="loadDirty" />
<ExperimentalModal :experimental="experimental" />
<TitleModal @changed="loadDirty" />
<ModbusProxyModal :is-sponsor="isSponsor" @changed="loadDirty" />
Expand Down Expand Up @@ -459,6 +473,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";
Expand Down Expand Up @@ -539,6 +555,8 @@ export default defineComponent({
MqttModal,
NetworkModal,
NotificationIcon,
OptimizerIcon,
OptimizerModal,
SponsorModal,
TariffsLegacyModal,
TariffCard,
Expand Down Expand Up @@ -761,6 +779,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) {
Expand Down
1 change: 1 addition & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,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
Expand Down
6 changes: 6 additions & 0 deletions cmd/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -1300,3 +1300,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
}
1 change: 1 addition & 0 deletions core/keys/global.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
8 changes: 7 additions & 1 deletion core/site.go
Original file line number Diff line number Diff line change
Expand Up @@ -766,13 +766,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)

Expand Down
8 changes: 3 additions & 5 deletions core/site_optimizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ var (
type batteryType string

const (
OPTIMIZER_URI = "https://evopt.evcc.io"

batteryTypeLoadpoint batteryType = "loadpoint"
batteryTypeVehicle batteryType = "vehicle"
batteryTypeBattery batteryType = "battery"
Expand Down Expand Up @@ -110,11 +112,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)

Expand Down Expand Up @@ -220,6 +217,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
Expand Down
5 changes: 5 additions & 0 deletions i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,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 einen externen Optimierungsdienst übertragen.",
"enable": "Optimizer aktivieren",
"title": "Optimizer"
},
"options": {
"boolean": {
"no": "nein",
Expand Down
5 changes: 5 additions & 0 deletions i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,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 an external optimization service for processing.",
"enable": "Enable Optimizer",
"title": "Optimizer"
},
"options": {
"boolean": {
"no": "no",
Expand Down
1 change: 1 addition & 0 deletions server/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions server/http_config_site_other_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,20 @@ 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)
setConfigDirty()
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)
Expand Down
Loading