Skip to content

Commit

Permalink
Feature: Add Homebox widget (gethomepage#3095)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: shamoon <[email protected]>
  • Loading branch information
cadeluca and shamoon authored Mar 10, 2024
1 parent b5258c5 commit 2d5f936
Show file tree
Hide file tree
Showing 8 changed files with 203 additions and 0 deletions.
23 changes: 23 additions & 0 deletions docs/widgets/services/homebox.md
Original file line number Diff line number Diff line change
@@ -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
```
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions public/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
1 change: 1 addition & 0 deletions src/widgets/components.js
Original file line number Diff line number Diff line change
Expand Up @@ -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")),
Expand Down
58 changes: 58 additions & 0 deletions src/widgets/homebox/component.jsx
Original file line number Diff line number Diff line change
@@ -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>
);
}
103 changes: 103 additions & 0 deletions src/widgets/homebox/proxy.js
Original file line number Diff line number Diff line change
@@ -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,
});
}
7 changes: 7 additions & 0 deletions src/widgets/homebox/widget.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import homeboxProxyHandler from "./proxy";

const widget = {
proxyHandler: homeboxProxyHandler,
};

export default widget;
2 changes: 2 additions & 0 deletions src/widgets/widgets.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -145,6 +146,7 @@ const widgets = {
grafana,
hdhomerun,
homeassistant,
homebox,
homebridge,
healthchecks,
ical: calendar,
Expand Down

0 comments on commit 2d5f936

Please sign in to comment.