Feature: Add Homebox widget (#3095)

---------

Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
pull/3101/head
Christian DeLuca 2 months ago committed by GitHub
parent b5258c5200
commit 2d5f93668a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,23 @@
---
title: Homebox
description: Homebox Widget Configuration
---
Learn more about [Homebox](https://github.com/hay-kot/homebox).
Uses the same username and password used to login from the web.
The `totalValue` field will attempt to format using the currency you have configured in Homebox.
Allowed fields: `["items", "totalWithWarranty", "locations", "labels", "users", "totalValue"]`.
If more than 4 fields are provided, only the first 4 are displayed.
```yaml
widget:
type: homebox
url: http://homebox.host.or.ip:port
username: username
password: password
fields: ["items", "locations", "totalValue"] # optional - default fields shown
```

@ -66,6 +66,7 @@ nav:
- widgets/services/hdhomerun.md
- widgets/services/healthchecks.md
- widgets/services/homeassistant.md
- widgets/services/homebox.md
- widgets/services/homebridge.md
- widgets/services/iframe.md
- widgets/services/immich.md

@ -863,5 +863,13 @@
"users": "Users",
"recipes": "Recipes",
"keywords": "Keywords"
},
"homebox": {
"items": "Items",
"totalWithWarranty": "With Warranty",
"locations": "Locations",
"labels": "Labels",
"users": "Users",
"totalValue": "Total Value"
}
}

@ -40,6 +40,7 @@ const components = {
hdhomerun: dynamic(() => import("./hdhomerun/component")),
peanut: dynamic(() => import("./peanut/component")),
homeassistant: dynamic(() => import("./homeassistant/component")),
homebox: dynamic(() => import("./homebox/component")),
homebridge: dynamic(() => import("./homebridge/component")),
healthchecks: dynamic(() => import("./healthchecks/component")),
immich: dynamic(() => import("./immich/component")),

@ -0,0 +1,58 @@
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 const homeboxDefaultFields = ["items", "locations", "totalValue"];
export default function Component({ service }) {
const { t } = useTranslation();
const { widget } = service;
const { data: homeboxData, error: homeboxError } = useWidgetAPI(widget);
if (homeboxError) {
return <Container service={service} error={homeboxError} />;
}
// Default fields
if (!widget.fields?.length > 0) {
widget.fields = homeboxDefaultFields;
}
const MAX_ALLOWED_FIELDS = 4;
// Limits max number of displayed fields
if (widget.fields?.length > MAX_ALLOWED_FIELDS) {
widget.fields = widget.fields.slice(0, MAX_ALLOWED_FIELDS);
}
if (!homeboxData) {
return (
<Container service={service}>
<Block label="homebox.items" />
<Block label="homebox.totalWithWarranty" />
<Block label="homebox.locations" />
<Block label="homebox.labels" />
<Block label="homebox.users" />
<Block label="homebox.totalValue" />
</Container>
);
}
return (
<Container service={service}>
<Block label="homebox.items" value={t("common.number", { value: homeboxData.items })} />
<Block label="homebox.totalWithWarranty" value={t("common.number", { value: homeboxData.totalWithWarranty })} />
<Block label="homebox.locations" value={t("common.number", { value: homeboxData.locations })} />
<Block label="homebox.labels" value={t("common.number", { value: homeboxData.labels })} />
<Block label="homebox.users" value={t("common.number", { value: homeboxData.users })} />
<Block
label="homebox.totalValue"
value={t("common.number", {
value: homeboxData.totalValue,
style: "currency",
currency: `${homeboxData.currencyCode}`,
})}
/>
</Container>
);
}

@ -0,0 +1,103 @@
import cache from "memory-cache";
import { formatApiCall } from "utils/proxy/api-helpers";
import { httpProxy } from "utils/proxy/http";
import getServiceWidget from "utils/config/service-helpers";
import createLogger from "utils/logger";
const proxyName = "homeboxProxyHandler";
const sessionTokenCacheKey = `${proxyName}__sessionToken`;
const logger = createLogger(proxyName);
async function login(widget, service) {
logger.debug("Homebox is rejecting the request, logging in.");
const loginUrl = new URL(`${widget.url}/api/v1/users/login`).toString();
const loginBody = `username=${encodeURIComponent(widget.username)}&password=${encodeURIComponent(widget.password)}`;
const loginParams = {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: loginBody,
};
const [, , data] = await httpProxy(loginUrl, loginParams);
try {
const { token, expiresAt } = JSON.parse(data.toString());
const expiresAtDate = new Date(expiresAt).getTime();
cache.put(`${sessionTokenCacheKey}.${service}`, token, expiresAtDate - Date.now());
return { token };
} catch (e) {
logger.error("Unable to login to Homebox API: %s", e);
}
return { token: false };
}
async function apiCall(widget, endpoint, service) {
const key = `${sessionTokenCacheKey}.${service}`;
const url = new URL(formatApiCall("{url}/api/v1/{endpoint}", { endpoint, ...widget }));
const headers = {
"Content-Type": "application/json",
Authorization: `${cache.get(key)}`,
};
const params = { method: "GET", headers };
let [status, contentType, data, responseHeaders] = await httpProxy(url, params);
if (status === 401 || status === 403) {
logger.debug("Homebox API rejected the request, attempting to obtain new access token");
const { token } = await login(widget, service);
headers.Authorization = `${token}`;
// retry request with new token
[status, contentType, data, responseHeaders] = await httpProxy(url, params);
if (status !== 200) {
logger.error("HTTP %d logging in to Homebox, data: %s", status, data);
return { status, contentType, data: null, responseHeaders };
}
}
if (status !== 200) {
logger.error("HTTP %d getting data from Homebox, data: %s", status, data);
return { status, contentType, data: null, responseHeaders };
}
return { status, contentType, data: JSON.parse(data.toString()), responseHeaders };
}
export default async function homeboxProxyHandler(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" });
}
if (!cache.get(`${sessionTokenCacheKey}.${service}`)) {
await login(widget, service);
}
// Get stats for the main blocks
const { data: groupStats } = await apiCall(widget, "groups/statistics", service);
// Get group info for currency
const { data: groupData } = await apiCall(widget, "groups", service);
return res.status(200).send({
items: groupStats?.totalItems,
locations: groupStats?.totalLocations,
labels: groupStats?.totalLabels,
totalWithWarranty: groupStats?.totalWithWarranty,
totalValue: groupStats?.totalItemPrice,
users: groupStats?.totalUsers,
currencyCode: groupData?.currency,
});
}

@ -0,0 +1,7 @@
import homeboxProxyHandler from "./proxy";
const widget = {
proxyHandler: homeboxProxyHandler,
};
export default widget;

@ -33,6 +33,7 @@ import gotify from "./gotify/widget";
import grafana from "./grafana/widget";
import hdhomerun from "./hdhomerun/widget";
import homeassistant from "./homeassistant/widget";
import homebox from "./homebox/widget";
import homebridge from "./homebridge/widget";
import healthchecks from "./healthchecks/widget";
import immich from "./immich/widget";
@ -145,6 +146,7 @@ const widgets = {
grafana,
hdhomerun,
homeassistant,
homebox,
homebridge,
healthchecks,
ical: calendar,

Loading…
Cancel
Save