diff --git a/software/src/modules/eco/eco.cpp b/software/src/modules/eco/eco.cpp new file mode 100644 index 000000000..3fc62b33a --- /dev/null +++ b/software/src/modules/eco/eco.cpp @@ -0,0 +1,99 @@ +/* esp32-firmware + * Copyright (C) 2024 Olaf Lüke + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ +#include "eco.h" + +#include +#include + +#include "event_log_prefix.h" +#include "module_dependencies.h" +#include "build.h" + + +void Eco::pre_setup() +{ + this->trace_buffer_index = logger.alloc_trace_buffer("eco", 1 << 20); + + config = ConfigRoot{Config::Object({ + {"charge_plan_active", Config::Bool(false)}, + {"mode_after_charge_plan", Config::Uint(3, 0, 3)}, + {"charge_below_active", Config::Bool(false)}, + {"charge_below", Config::Int32(0)}, // in ct + {"block_above_active", Config::Bool(false)}, + {"block_above", Config::Int32(20)} // in ct + }), [this](Config &update, ConfigSource source) -> String { + task_scheduler.scheduleOnce([this]() { + this->update(); + }); + return ""; + }}; + + charge_plan = Config::Object({ + {"enabled",Config::Bool(false)}, + {"day", Config::Uint(0, 0, 2)}, + {"time", Config::Int(8*60)}, // localtime in minutes since 00:00 + {"hours", Config::Uint(4, 1, 48)} + }); + charge_plan_update = charge_plan; + + state = Config::Object({ + {"last_charge_plan_save", Config::Uint(0)}, + }); +} + +void Eco::setup() +{ + api.restorePersistentConfig("eco/config", &config); + // TODO: Set user defined default charge_plan? + + initialized = true; +} + +void Eco::register_urls() +{ + api.addPersistentConfig("eco/config", &config); + api.addState("eco/state", &state); + + api.addState("eco/charge_plan", &charge_plan); + api.addCommand("eco/charge_plan_update", &charge_plan_update, {}, [this](String &/*errmsg*/) { + charge_plan = charge_plan_update; + state.get("last_charge_plan_save")->updateUint(rtc.timestamp_minutes()); + update(); + }, false); + + task_scheduler.scheduleWallClock([this]() { + this->update(); + }, 1_m, 0_ms, true); +} + +void Eco::update() +{ + current_decision = Decision::Normal; +} + +Eco::Decision Eco::get_decision() +{ + if (!config.get("charge_planning_active")->asBool() && + !config.get("charge_below_active")->asBool() && + !config.get("block_above_active")->asBool()) { + return Decision::Normal; + } + + return current_decision; +} \ No newline at end of file diff --git a/software/src/modules/eco/eco.h b/software/src/modules/eco/eco.h new file mode 100644 index 000000000..919365cfc --- /dev/null +++ b/software/src/modules/eco/eco.h @@ -0,0 +1,52 @@ +/* esp32-firmware + * Copyright (C) 2024 Olaf Lüke + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#pragma once + +#include "module.h" +#include "config.h" + + +class Eco final : public IModule +{ +private: + void update(); + + ConfigRoot config; + ConfigRoot charge_plan; + ConfigRoot charge_plan_update; + ConfigRoot state; + + size_t trace_buffer_index; + +public: + enum class Decision : uint8_t { + Normal = 0, + Fast = 1, + Block = 2 + }; + + Eco(){} + void pre_setup() override; + void setup() override; + void register_urls() override; + Decision get_decision(); + + Decision current_decision = Decision::Normal; +}; diff --git a/software/src/modules/eco/module.ini b/software/src/modules/eco/module.ini new file mode 100644 index 000000000..081312c47 --- /dev/null +++ b/software/src/modules/eco/module.ini @@ -0,0 +1,7 @@ +[Dependencies] +Requires = Task Scheduler + Event Log + API + Day Ahead Prices + Solar Forecast + Rtc diff --git a/software/web/src/modules/eco/api.ts b/software/web/src/modules/eco/api.ts new file mode 100644 index 000000000..473f9c9f4 --- /dev/null +++ b/software/web/src/modules/eco/api.ts @@ -0,0 +1,19 @@ +export interface config { + charge_plan_active: boolean; + mode_after_charge_plan: number; + charge_below_active: boolean; + charge_below: number; + block_above_active: boolean; + block_above: number; +} + +export interface charge_plan { + enabled: boolean; + day: number; + time: number; + hours: number; +} + +export interface state { + last_charge_plan_save: number; +} diff --git a/software/web/src/modules/eco/main.tsx b/software/web/src/modules/eco/main.tsx new file mode 100644 index 000000000..36c38a8a4 --- /dev/null +++ b/software/web/src/modules/eco/main.tsx @@ -0,0 +1,257 @@ +/* esp32-firmware + * Copyright (C) 2024 Olaf Lüke + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +import * as util from "../../ts/util"; +import * as API from "../../ts/api"; +import { h, Fragment, createRef, Component, RefObject } from "preact"; +import { __ } from "../../ts/translation"; +import { SwitchableInputNumber } from "../../ts/components/switchable_input_number"; +import { ConfigComponent } from "../../ts/components/config_component"; +import { ConfigForm } from "../../ts/components/config_form"; +import { SubPage } from "../../ts/components/sub_page"; +import { NavbarItem } from "../../ts/components/navbar_item"; +import { Thermometer } from "react-feather"; +import { StatusSection } from "../../ts/components/status_section"; +import { FormRow } from "../../ts/components/form_row"; +import { is_solar_forecast_enabled } from "../solar_forecast/main"; +import { is_day_ahead_prices_enabled } from "../day_ahead_prices/main"; +import { Switch } from "../../ts/components/switch"; +import { InputSelect } from "../../ts/components/input_select"; +import { InputNumber } from "../../ts/components/input_number"; +import { InputTime } from "../../ts/components/input_time"; +import { Button } from "react-bootstrap"; + +export function EcoNavbar() { + return ( + + + + + } + hidden={false} + /> + ); +} + +type EcoConfig = API.getType["eco/config"]; + +interface EcoState { + eco_state: API.getType["eco/state"]; + charge_plan: API.getType["eco/charge_plan"]; +} + +export class Eco extends ConfigComponent<'eco/config', {status_ref?: RefObject}, EcoState> { + constructor() { + super('eco/config', + __("eco.script.save_failed")); + } + + render(props: {}, state: EcoState & EcoConfig) { + if (!util.render_allowed()) + return ; + + const solar_forecast_enabled = is_solar_forecast_enabled(); + const day_ahead_prices_enabled = is_day_ahead_prices_enabled(); + + return ( + + + + + + + this.setState({mode_after_charge_plan: parseInt(v)})} + /> + + + + + + + + + + ); + } +} + + +interface EcoStatusState { + state: API.getType["eco/state"]; + charge_plan: API.getType["eco/charge_plan"]; +} + +export class EcoStatus extends Component<{}, EcoStatusState> { + timeout: number = undefined; + + constructor() { + super(); + + util.addApiEventListener('eco/state', () => { + this.setState({state: API.get('eco/state')}) + }); + + util.addApiEventListener('eco/charge_plan', () => { + if(this.state.charge_plan === undefined) { + this.setState({charge_plan: API.get('eco/charge_plan')}) + } + }); + } + + get_date_from_minutes(minutes: number) { + const h = Math.floor(minutes / 60); + const m = minutes - h * 60; + return new Date(0, 0, 1, h, m); + } + + get_minutes_from_date(date: Date) { + return date.getMinutes() + date.getHours()*60; + } + + update_charge_plan(charge_plan: API.getType["eco/charge_plan"]) { + if(this.timeout !== undefined) { + clearTimeout(this.timeout); + } + + console.log(charge_plan); + + this.timeout = setTimeout(() => API.save("eco/charge_plan", charge_plan), 1000); + } + + render(props: {}, state: EcoStatusState) { + if (!util.render_allowed()) { + return + } + + let charge_plan_text = () => { + let day = "bis Heute um"; + if (state.charge_plan.day === 1) { + day = "bis Morgen um"; + } else if (state.charge_plan.day === 2) { + day = "täglich bis"; + } + + const active = state.charge_plan.enabled ? "aktiv" : "nicht aktiv"; + const time = this.get_date_from_minutes(state.charge_plan.time).toLocaleTimeString([], {hour: '2-digit', minute: '2-digit'}); + return `Aktueller Ladeplan: Nutze die günstigsten ${state.charge_plan.hours} Stunden ${day} ${time} Uhr. Der Ladeplan ist ${active}.`; + }; + + return + +
+
+
Tag
+ this.setState({charge_plan: {...state.charge_plan, day: parseInt(v)}}, () => this.update_charge_plan({...state.charge_plan, day: parseInt(v)}))} + /> +
+
+
+
+
Uhrzeit
+ this.setState({charge_plan: {...state.charge_plan, time: this.get_minutes_from_date(d)}}, () => this.update_charge_plan({...state.charge_plan, time: this.get_minutes_from_date(d)}))} + /> +
+
+
+
+
Ladedauer
+ this.setState({charge_plan: {...state.charge_plan, hours: v}}, () => this.update_charge_plan({...state.charge_plan, hours: v}))} + min={1} + max={48} + /> +
+
+
+ +
+
+
; + } +} + +export function init() { +} diff --git a/software/web/src/modules/eco/post.scss b/software/web/src/modules/eco/post.scss new file mode 100644 index 000000000..8e834ef55 --- /dev/null +++ b/software/web/src/modules/eco/post.scss @@ -0,0 +1,49 @@ +.eco-fixed-size { + min-width: 110px; +} + +@media (max-width: 768px) { + .eco-input-group-append { + margin-left: 0px; + } + + .eco-input-group-prepend { + margin-right: 0px; + } + + .input-group-prepend:not(.eco-input-group-append) .input-group-text { + border-bottom-left-radius: 0; + } + + .input-group-prepend.eco-input-group-append .input-group-text { + border-top-left-radius: 0; + } + + .input-group-prepend:not(.eco-input-group-append) + input { + border-bottom-right-radius: 0; + } + + .input-group-prepend.eco-input-group-append + input { + border-top-right-radius: 0; + } +} + +@media (min-width: 768px) { + .eco-input-group-append { + margin-left: -1px; + } + + .eco-input-group-append .input-group-text { + border-bottom-left-radius: 0; + border-top-left-radius: 0; + } + + .eco-input-group-prepend { + margin-right: -1px; + } + + .eco-input-group-prepend .input-group-text { + border-bottom-right-radius: 0; + border-top-right-radius: 0; + } +} diff --git a/software/web/src/modules/eco/translation_de.tsx b/software/web/src/modules/eco/translation_de.tsx new file mode 100644 index 000000000..b477624ee --- /dev/null +++ b/software/web/src/modules/eco/translation_de.tsx @@ -0,0 +1,19 @@ +/** @jsxImportSource preact */ +import { h } from "preact"; +let x = { + "eco": { + "status": {}, + "navbar": { + "eco": "Eco-Modus" + }, + "content": { + "eco": "Eco-Modus", + "active": "Aktiv", + "inactive": "Inaktiv" + }, + "script": { + "save_failed": "Speichern der Eco-Einstellungen fehlgeschlagen", + "reboot_content_changed": "Eco-Einstellungen" + } + } +} diff --git a/software/web/src/modules/eco/translation_en.tsx b/software/web/src/modules/eco/translation_en.tsx new file mode 100644 index 000000000..6128d31b7 --- /dev/null +++ b/software/web/src/modules/eco/translation_en.tsx @@ -0,0 +1,19 @@ +/** @jsxImportSource preact */ +import { h } from "preact"; +let x = { + "eco": { + "status": {}, + "navbar": { + "eco": "Eco Mode" + }, + "content": { + "eco": "Eco Mode", + "active": "Active", + "inactive": "Inactive" + }, + "script": { + "save_failed": "Failed to save the eco settings", + "reboot_content_changed": "eco settings" + } + } +}