diff --git a/docs/widgets/services/esphome.md b/docs/widgets/services/esphome.md
new file mode 100644
index 00000000..6038cb61
--- /dev/null
+++ b/docs/widgets/services/esphome.md
@@ -0,0 +1,16 @@
+---
+title: ESPHome
+description: ESPHome Widget Configuration
+---
+
+Learn more about [ESPHome](https://esphome.io/).
+
+Show the number of ESPHome devices based on their state.
+
+Allowed fields: `["total", "online", "offline", "unknown"]`.
+
+```yaml
+widget:
+ type: esphome
+ url: http://esphome.host.or.ip:port
+```
diff --git a/mkdocs.yml b/mkdocs.yml
index 4c574e18..b7f8ec6a 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -49,6 +49,7 @@ nav:
- widgets/services/diskstation.md
- widgets/services/downloadstation.md
- widgets/services/emby.md
+ - widgets/services/esphome.md
- widgets/services/evcc.md
- widgets/services/fileflows.md
- widgets/services/flood.md
diff --git a/public/locales/en/common.json b/public/locales/en/common.json
index f6c1b841..b040e1fd 100644
--- a/public/locales/en/common.json
+++ b/public/locales/en/common.json
@@ -107,6 +107,12 @@
"episodes": "Episodes",
"songs": "Songs"
},
+ "esphome": {
+ "offline": "Offline",
+ "online": "Online",
+ "total": "Total",
+ "unknown": "Unknown"
+ },
"evcc": {
"pv_power": "Production",
"battery_soc": "Battery",
diff --git a/src/widgets/components.js b/src/widgets/components.js
index 74d5fe1f..8b9c277d 100644
--- a/src/widgets/components.js
+++ b/src/widgets/components.js
@@ -23,6 +23,7 @@ const components = {
docker: dynamic(() => import("./docker/component")),
kubernetes: dynamic(() => import("./kubernetes/component")),
emby: dynamic(() => import("./emby/component")),
+ esphome: dynamic(() => import("./esphome/component")),
evcc: dynamic(() => import("./evcc/component")),
fileflows: dynamic(() => import("./fileflows/component")),
flood: dynamic(() => import("./flood/component")),
diff --git a/src/widgets/esphome/component.jsx b/src/widgets/esphome/component.jsx
new file mode 100644
index 00000000..c44352fa
--- /dev/null
+++ b/src/widgets/esphome/component.jsx
@@ -0,0 +1,41 @@
+import { useTranslation } from "next-i18next";
+
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
+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);
+
+ if (resultError) {
+ return ;
+ }
+
+ if (!resultData) {
+ return (
+
+
+
+
+
+
+ );
+ }
+
+ const total = Object.keys(resultData).length;
+ const online = Object.entries(resultData).filter(([, v]) => v === true).length;
+ const offline = Object.entries(resultData).filter(([, v]) => v === false).length;
+ const unknown = Object.entries(resultData).filter(([, v]) => v === null).length;
+
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/src/widgets/esphome/widget.js b/src/widgets/esphome/widget.js
new file mode 100644
index 00000000..c5a87b68
--- /dev/null
+++ b/src/widgets/esphome/widget.js
@@ -0,0 +1,8 @@
+import genericProxyHandler from "utils/proxy/handlers/generic";
+
+const widget = {
+ api: "{url}/ping",
+ proxyHandler: genericProxyHandler,
+};
+
+export default widget;
diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js
index 2eae4ba9..11cc8af9 100644
--- a/src/widgets/widgets.js
+++ b/src/widgets/widgets.js
@@ -17,6 +17,7 @@ import deluge from "./deluge/widget";
import diskstation from "./diskstation/widget";
import downloadstation from "./downloadstation/widget";
import emby from "./emby/widget";
+import esphome from "./esphome/widget";
import evcc from "./evcc/widget";
import fileflows from "./fileflows/widget";
import flood from "./flood/widget";
@@ -127,6 +128,7 @@ const widgets = {
diskstation,
downloadstation,
emby,
+ esphome,
evcc,
fileflows,
flood,