diff --git a/public/locales/de/common.json b/public/locales/de/common.json
index 49b691ce..d7b86d92 100644
--- a/public/locales/de/common.json
+++ b/public/locales/de/common.json
@@ -51,6 +51,13 @@
"series": "Series",
"episodes": "Episodes",
"songs": "Songs"
+ },
+ "evcc": {
+ "pvPower": "Erzeugung",
+ "batterySoc": "Batterie",
+ "gridpower": "Netz",
+ "homepower": "Verbrauch",
+ "chargePower": "Ladepunkt"
},
"tautulli": {
"playing": "Spielen",
diff --git a/public/locales/en/common.json b/public/locales/en/common.json
index 9dd10d04..5dbe25a8 100755
--- a/public/locales/en/common.json
+++ b/public/locales/en/common.json
@@ -91,6 +91,13 @@
"series": "Series",
"episodes": "Episodes",
"songs": "Songs"
+ },
+ "evcc": {
+ "pvPower": "Production",
+ "batterySoc": "Battery",
+ "gridpower": "Grid",
+ "homepower": "Consumption",
+ "chargePower": "Charger"
},
"flood": {
"download": "Download",
diff --git a/src/widgets/components.js b/src/widgets/components.js
index f8828f3b..002ac7eb 100644
--- a/src/widgets/components.js
+++ b/src/widgets/components.js
@@ -16,6 +16,7 @@ const components = {
docker: dynamic(() => import("./docker/component")),
kubernetes: dynamic(() => import("./kubernetes/component")),
emby: dynamic(() => import("./emby/component")),
+ evcc: dynamic(() => import("./evcc/component")),
fileflows: dynamic(() => import("./fileflows/component")),
flood: dynamic(() => import("./flood/component")),
freshrss: dynamic(() => import("./freshrss/component")),
diff --git a/src/widgets/evcc/component.jsx b/src/widgets/evcc/component.jsx
new file mode 100644
index 00000000..cb5270ad
--- /dev/null
+++ b/src/widgets/evcc/component.jsx
@@ -0,0 +1,39 @@
+import { useTranslation } from "next-i18next";
+
+import Container from "components/services/widget/container";
+import Block from "components/services/widget/block";
+import useWidgetAPI from "utils/proxy/use-widget-api";
+
+export default function Component({ service }) {
+ const { t } = useTranslation();
+
+ const { widget } = service;
+ const { data: resultData, error: resultError } = useWidgetAPI(widget, "result");
+
+
+ if (resultError) {
+ return ;
+ }
+
+ if (!resultData) {
+ return (
+ ,
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/widgets/evcc/proxy.js b/src/widgets/evcc/proxy.js
new file mode 100644
index 00000000..8aad741d
--- /dev/null
+++ b/src/widgets/evcc/proxy.js
@@ -0,0 +1,89 @@
+import { httpProxy } from "utils/proxy/http";
+import getServiceWidget from "utils/config/service-helpers";
+import createLogger from "utils/logger";
+
+const logger = createLogger("homeassistantProxyHandler");
+
+const defaultQueries = [
+ {
+ template: "{{ states.person|selectattr('state','equalto','home')|list|length }} / {{ states.person|list|length }}",
+ label: "homeassistant.people_home"
+ },
+ {
+ template: "{{ states.light|selectattr('state','equalto','on')|list|length }} / {{ states.light|list|length }}",
+ label: "homeassistant.lights_on"
+ },
+ {
+ template: "{{ states.switch|selectattr('state','equalto','on')|list|length }} / {{ states.switch|list|length }}",
+ label: "homeassistant.switches_on"
+ }
+];
+
+function formatOutput(output, data) {
+ return output.replace(/\{.*?\}/g,
+ (match) => match.replace(/\{|\}/g, "").split(".").reduce((o, p) => o ? o[p] : "", data) ?? "");
+}
+
+async function getQuery(query, { url, key }) {
+ const headers = { Authorization: `Bearer ${key}` };
+ const { state, template, label, value } = query;
+ if (state) {
+ return {
+ result: await httpProxy(new URL(`${url}/api/states/${state}`), {
+ headers,
+ method: "GET"
+ }),
+ output: (data) => {
+ const jsonData = JSON.parse(data);
+ return {
+ label: formatOutput(label ?? "{attributes.friendly_name}", jsonData),
+ value: formatOutput(value ?? "{state} {attributes.unit_of_measurement}", jsonData)
+ };
+ }
+ };
+ }
+ if (template) {
+ return {
+ result: await httpProxy(new URL(`${url}/api/template`), {
+ headers,
+ method: "POST",
+ body: JSON.stringify({ template })
+ }),
+ output: (data) => ({ label, value: data.toString() })
+ };
+ }
+ return { result: [500, null, { error: { message: `invalid query ${JSON.stringify(query)}` } }] };
+}
+
+export default async function evccProxyHandler(req, res) {
+ const { group, service } = req.query;
+
+ if (!group || !service) {
+ logger.debug("Invalid or missing service '%s' or group '%s'", service, group);
+ return res.status(400).json({ error: "Invalid proxy service type" });
+ }
+
+ const widget = await getServiceWidget(group, service);
+ if (!widget) {
+ logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
+ return res.status(400).json({ error: "Invalid proxy service type" });
+ }
+
+ let queries = defaultQueries;
+ if (!widget.fields && widget.custom) {
+ queries = widget.custom.slice(0, 4);
+ }
+
+ const results = await Promise.all(queries.map(q => getQuery(q, widget)));
+
+ const err = results.find(r => r.result[2]?.error);
+ if (err) {
+ const [status, , data] = err.result;
+ return res.status(status).send(data);
+ }
+
+ return res.status(200).send(results.map(r => {
+ const [status, , data] = r.result;
+ return status === 200 ? r.output(data) : { label: status, value: data.toString() };
+ }));
+}
diff --git a/src/widgets/evcc/widget.js b/src/widgets/evcc/widget.js
new file mode 100644
index 00000000..f3b22417
--- /dev/null
+++ b/src/widgets/evcc/widget.js
@@ -0,0 +1,14 @@
+import genericProxyHandler from "utils/proxy/handlers/generic";
+
+const widget = {
+ api: "{url}/api/{endpoint}",
+ proxyHandler: genericProxyHandler,
+
+ mappings: {
+ result: {
+ endpoint: "state",
+ }
+ },
+};
+
+export default widget;
\ No newline at end of file
diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js
index 9e155383..40d57abe 100644
--- a/src/widgets/widgets.js
+++ b/src/widgets/widgets.js
@@ -11,6 +11,7 @@ import deluge from "./deluge/widget";
import diskstation from "./diskstation/widget";
import downloadstation from "./downloadstation/widget";
import emby from "./emby/widget";
+import evcc from "./evcc/widget";
import fileflows from "./fileflows/widget";
import flood from "./flood/widget";
import freshrss from "./freshrss/widget";
@@ -92,6 +93,7 @@ const widgets = {
diskstation,
downloadstation,
emby,
+ evcc,
fileflows,
flood,
freshrss,