From c9512a6d26bfd6749909b8bdec15157ec8f8d4a4 Mon Sep 17 00:00:00 2001 From: Fernando Neira Date: Mon, 24 Oct 2022 16:40:49 -0300 Subject: [PATCH 1/7] add homebridge plugin --- public/locales/en/common.json | 8 +++ src/widgets/components.js | 1 + src/widgets/homebridge/component.jsx | 44 ++++++++++++ src/widgets/homebridge/proxy.js | 101 +++++++++++++++++++++++++++ src/widgets/homebridge/widget.js | 14 ++++ src/widgets/widgets.js | 2 + 6 files changed, 170 insertions(+) create mode 100644 src/widgets/homebridge/component.jsx create mode 100644 src/widgets/homebridge/proxy.js create mode 100644 src/widgets/homebridge/widget.js diff --git a/public/locales/en/common.json b/public/locales/en/common.json index f0f6e6c5..d41e30ed 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -284,5 +284,13 @@ "96-night": "Thunderstorm With Hail", "99-day": "Thunderstorm With Hail", "99-night": "Thunderstorm With Hail" + }, + "homebridge": { + "available_update": "System", + "update_available": "Update Available", + "up_to_date": "Up to Date", + "available_homebridge_update": "Plugins", + "plugins_updates_available": "{{quantity}} Available", + "plugins_up_to_date": "Up to Date" } } diff --git a/src/widgets/components.js b/src/widgets/components.js index da6ce362..8f7bfeb9 100644 --- a/src/widgets/components.js +++ b/src/widgets/components.js @@ -9,6 +9,7 @@ const components = { docker: dynamic(() => import("./docker/component")), emby: dynamic(() => import("./emby/component")), gotify: dynamic(() => import("./gotify/component")), + homebridge: dynamic(() => import("./homebridge/component")), jackett: dynamic(() => import("./jackett/component")), jellyfin: dynamic(() => import("./emby/component")), jellyseerr: dynamic(() => import("./jellyseerr/component")), diff --git a/src/widgets/homebridge/component.jsx b/src/widgets/homebridge/component.jsx new file mode 100644 index 00000000..bc6dd87d --- /dev/null +++ b/src/widgets/homebridge/component.jsx @@ -0,0 +1,44 @@ +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: homebridge, error: homebridgeError } = useWidgetAPI(widget, "info"); + + if (homebridgeError || (homebridge && !homebridge.data)) { + return ; + } + + if (!homebridge) { + return ( + + + + + + ); + } + + return ( + + + + + + ); +} diff --git a/src/widgets/homebridge/proxy.js b/src/widgets/homebridge/proxy.js new file mode 100644 index 00000000..7e666b53 --- /dev/null +++ b/src/widgets/homebridge/proxy.js @@ -0,0 +1,101 @@ +import cache from "memory-cache"; + +import { httpProxy } from "utils/proxy/http"; +import { formatApiCall } from "utils/proxy/api-helpers"; +import getServiceWidget from "utils/config/service-helpers"; +import createLogger from "utils/logger"; +import widgets from "widgets/widgets"; + +const proxyName = "homebridgeProxyHandler"; +const sessionTokenCacheKey = `${proxyName}__sessionToken`; +const logger = createLogger(proxyName); + +async function login(widget) { + const endpoint = "auth/login"; + const api = widgets?.[widget.type]?.api + const loginUrl = new URL(formatApiCall(api, { endpoint, ...widget })); + const loginBody = { username: widget.username, password: widget.password }; + const headers = { "Content-Type": "application/json" }; + const [status, contentType, data, responseHeaders] = await httpProxy(loginUrl, { + method: "POST", + body: JSON.stringify(loginBody), + headers, + }); + + const dataParsed = JSON.parse(data.toString()) + + cache.put(sessionTokenCacheKey, dataParsed.access_token); + + return { status, contentType, data: dataParsed, responseHeaders }; +} + +async function apiCall(widget, endpoint) { + const headers = { + "content-type": "application/json", + "Authorization": `Bearer ${cache.get(sessionTokenCacheKey)}`, + } + + const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget })); + const method = "GET"; + + let [status, contentType, data, responseHeaders] = await httpProxy(url, { + method, + headers, + }); + + if (status === 401) { + logger.debug("Homebridge is rejecting the request, but obtaining new session token"); + const { data: loginData } = login(widget); + headers.Authorization = loginData?.auth_token; + + // retry the request, now with the new session token + [status, contentType, data, responseHeaders] = await httpProxy(url, { + method, + headers, + }); + } + + if (status !== 200) { + logger.error("Error getting data from Homebridge: %d. Data: %s", status, data); + } + + return { status, contentType, data: JSON.parse(data.toString()), responseHeaders }; +} + +function formatPluginsResponse(plugins) { + const quantity = plugins?.data.filter(p => p.updateAvailable).length; + return { + updatesAvailable: quantity > 0, + quantity, + } +} + +export default async function homebridgeProxyHandler(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" }); + } + + await login(widget); + + const statusRS = await apiCall(widget, "status/homebridge"); + const versionRS = await apiCall(widget, "status/homebridge-version"); + const pluginsRS = await apiCall(widget, "plugins"); + + return res.status(200).send({ + data: { + status: statusRS?.data?.status, + updateAvailable: versionRS?.data?.updateAvailable, + plugins: formatPluginsResponse(pluginsRS) + } + }); +} diff --git a/src/widgets/homebridge/widget.js b/src/widgets/homebridge/widget.js new file mode 100644 index 00000000..31d4f30e --- /dev/null +++ b/src/widgets/homebridge/widget.js @@ -0,0 +1,14 @@ +import homebridgeProxyHandler from "./proxy"; + +const widget = { + api: "{url}/api/{endpoint}", + proxyHandler: homebridgeProxyHandler, + + mappings: { + info: { + endpoint: "/", + } + }, +}; + +export default widget; diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js index a90bd620..55379e98 100644 --- a/src/widgets/widgets.js +++ b/src/widgets/widgets.js @@ -5,6 +5,7 @@ import changedetectionio from "./changedetectionio/widget"; import coinmarketcap from "./coinmarketcap/widget"; import emby from "./emby/widget"; import gotify from "./gotify/widget"; +import homebridge from "./homebridge/widget"; import jackett from "./jackett/widget"; import jellyseerr from "./jellyseerr/widget"; import lidarr from "./lidarr/widget"; @@ -39,6 +40,7 @@ const widgets = { coinmarketcap, emby, gotify, + homebridge, jackett, jellyfin: emby, jellyseerr, From 5c5b677075f9e2b06554c6e3093d86dd6e1c358c Mon Sep 17 00:00:00 2001 From: Fernando Neira Date: Mon, 24 Oct 2022 18:09:48 -0300 Subject: [PATCH 2/7] improvements --- public/locales/en/common.json | 6 +++--- src/widgets/homebridge/component.jsx | 23 +++++++++++++++-------- src/widgets/homebridge/proxy.js | 22 ++++++++++++++++------ 3 files changed, 34 insertions(+), 17 deletions(-) diff --git a/public/locales/en/common.json b/public/locales/en/common.json index d41e30ed..aa1d6601 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -287,10 +287,10 @@ }, "homebridge": { "available_update": "System", + "updates": "Updates", "update_available": "Update Available", "up_to_date": "Up to Date", - "available_homebridge_update": "Plugins", - "plugins_updates_available": "{{quantity}} Available", - "plugins_up_to_date": "Up to Date" + "child_bridges": "Child Bridges", + "child_bridges_status": "{{ok}}/{{total}}" } } diff --git a/src/widgets/homebridge/component.jsx b/src/widgets/homebridge/component.jsx index bc6dd87d..f8477cfc 100644 --- a/src/widgets/homebridge/component.jsx +++ b/src/widgets/homebridge/component.jsx @@ -19,8 +19,8 @@ export default function Component({ service }) { return ( - - + + ); } @@ -32,13 +32,20 @@ export default function Component({ service }) { value={homebridge.data.status} /> - + {homebridge?.data?.childBridges.quantity > 0 && + } ); } diff --git a/src/widgets/homebridge/proxy.js b/src/widgets/homebridge/proxy.js index 7e666b53..ea4c1654 100644 --- a/src/widgets/homebridge/proxy.js +++ b/src/widgets/homebridge/proxy.js @@ -70,6 +70,14 @@ function formatPluginsResponse(plugins) { } } +function formatChildBridgesResponse(childBridges) { + const quantity = childBridges?.data?.length + return { + quantity, + quantityWithOkStatus: childBridges?.data?.filter(cb => cb.status === "ok").length, + } +} + export default async function homebridgeProxyHandler(req, res) { const { group, service } = req.query; @@ -87,15 +95,17 @@ export default async function homebridgeProxyHandler(req, res) { await login(widget); - const statusRS = await apiCall(widget, "status/homebridge"); - const versionRS = await apiCall(widget, "status/homebridge-version"); - const pluginsRS = await apiCall(widget, "plugins"); + const statusRs = await apiCall(widget, "status/homebridge"); + const versionRs = await apiCall(widget, "status/homebridge-version"); + const childBrigdeRs = await apiCall(widget, "status/homebridge/child-bridges"); + const pluginsRs = await apiCall(widget, "plugins"); return res.status(200).send({ data: { - status: statusRS?.data?.status, - updateAvailable: versionRS?.data?.updateAvailable, - plugins: formatPluginsResponse(pluginsRS) + status: statusRs?.data?.status, + updateAvailable: versionRs?.data?.updateAvailable, + plugins: formatPluginsResponse(pluginsRs), + childBridges: formatChildBridgesResponse(childBrigdeRs), } }); } From b1bf251309211c447b5258a3fd631e919c901530 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+shamoon@users.noreply.github.com> Date: Mon, 24 Oct 2022 14:27:31 -0700 Subject: [PATCH 3/7] Capitalize status =) --- src/widgets/homebridge/component.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/widgets/homebridge/component.jsx b/src/widgets/homebridge/component.jsx index f8477cfc..402fe566 100644 --- a/src/widgets/homebridge/component.jsx +++ b/src/widgets/homebridge/component.jsx @@ -29,7 +29,7 @@ export default function Component({ service }) { Date: Mon, 24 Oct 2022 18:42:55 -0300 Subject: [PATCH 4/7] feature: improvement login api calls --- src/widgets/homebridge/proxy.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/widgets/homebridge/proxy.js b/src/widgets/homebridge/proxy.js index ea4c1654..d4a62865 100644 --- a/src/widgets/homebridge/proxy.js +++ b/src/widgets/homebridge/proxy.js @@ -93,7 +93,9 @@ export default async function homebridgeProxyHandler(req, res) { return res.status(400).json({ error: "Invalid proxy service type" }); } - await login(widget); + if (!cache.get(sessionTokenCacheKey)) { + await login(widget); + } const statusRs = await apiCall(widget, "status/homebridge"); const versionRs = await apiCall(widget, "status/homebridge-version"); From d942e989bd5cd5734b41cca5e63b0359922ca6f1 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+shamoon@users.noreply.github.com> Date: Mon, 24 Oct 2022 14:46:22 -0700 Subject: [PATCH 5/7] Refactor proxy for brevity --- src/widgets/homebridge/component.jsx | 16 ++++++------ src/widgets/homebridge/proxy.js | 39 ++++++++++------------------ 2 files changed, 21 insertions(+), 34 deletions(-) diff --git a/src/widgets/homebridge/component.jsx b/src/widgets/homebridge/component.jsx index 402fe566..807cc49a 100644 --- a/src/widgets/homebridge/component.jsx +++ b/src/widgets/homebridge/component.jsx @@ -9,13 +9,13 @@ export default function Component({ service }) { const { widget } = service; - const { data: homebridge, error: homebridgeError } = useWidgetAPI(widget, "info"); + const { data: homebridgeData, error: homebridgeError } = useWidgetAPI(widget, "info"); - if (homebridgeError || (homebridge && !homebridge.data)) { + if (homebridgeError || homebridgeData?.error) { return ; } - if (!homebridge) { + if (!homebridgeData) { return ( @@ -29,21 +29,21 @@ export default function Component({ service }) { - {homebridge?.data?.childBridges.quantity > 0 && + {homebridgeData?.childBridges?.total > 0 && } diff --git a/src/widgets/homebridge/proxy.js b/src/widgets/homebridge/proxy.js index d4a62865..d154cd43 100644 --- a/src/widgets/homebridge/proxy.js +++ b/src/widgets/homebridge/proxy.js @@ -62,22 +62,6 @@ async function apiCall(widget, endpoint) { return { status, contentType, data: JSON.parse(data.toString()), responseHeaders }; } -function formatPluginsResponse(plugins) { - const quantity = plugins?.data.filter(p => p.updateAvailable).length; - return { - updatesAvailable: quantity > 0, - quantity, - } -} - -function formatChildBridgesResponse(childBridges) { - const quantity = childBridges?.data?.length - return { - quantity, - quantityWithOkStatus: childBridges?.data?.filter(cb => cb.status === "ok").length, - } -} - export default async function homebridgeProxyHandler(req, res) { const { group, service } = req.query; @@ -97,17 +81,20 @@ export default async function homebridgeProxyHandler(req, res) { await login(widget); } - const statusRs = await apiCall(widget, "status/homebridge"); - const versionRs = await apiCall(widget, "status/homebridge-version"); - const childBrigdeRs = await apiCall(widget, "status/homebridge/child-bridges"); - const pluginsRs = await apiCall(widget, "plugins"); + const { data: statusData } = await apiCall(widget, "status/homebridge"); + const { data: versionData } = await apiCall(widget, "status/homebridge-version"); + const { data: childBridgeData } = await apiCall(widget, "status/homebridge/child-bridges"); + const { data: pluginsData } = await apiCall(widget, "plugins"); return res.status(200).send({ - data: { - status: statusRs?.data?.status, - updateAvailable: versionRs?.data?.updateAvailable, - plugins: formatPluginsResponse(pluginsRs), - childBridges: formatChildBridgesResponse(childBrigdeRs), - } + status: statusData?.status, + updateAvailable: versionData?.updateAvailable, + plugins: { + updatesAvailable: pluginsData?.filter(p => p.updateAvailable).length, + }, + childBridges: { + running: childBridgeData?.filter(cb => cb.status === "ok").length, + total: childBridgeData?.length + } }); } From c601094c323ab00259b8a0df78d998e5df36831b Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+shamoon@users.noreply.github.com> Date: Mon, 24 Oct 2022 22:15:16 -0700 Subject: [PATCH 6/7] fix login retry, use token expiration, object deconstruction --- src/widgets/homebridge/proxy.js | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/widgets/homebridge/proxy.js b/src/widgets/homebridge/proxy.js index d154cd43..b05b475c 100644 --- a/src/widgets/homebridge/proxy.js +++ b/src/widgets/homebridge/proxy.js @@ -22,11 +22,15 @@ async function login(widget) { headers, }); - const dataParsed = JSON.parse(data.toString()) - - cache.put(sessionTokenCacheKey, dataParsed.access_token); - - return { status, contentType, data: dataParsed, responseHeaders }; + try { + const { access_token, expires_in } = JSON.parse(data.toString()); + + cache.put(sessionTokenCacheKey, access_token, (expires_in * 1000) - 5 * 60 * 1000); // expires_in (s) - 5m + return { access_token }; + } catch (e) { + logger.error("Unable to login to Homebridge API: %s", e); + return { access_token: false }; + } } async function apiCall(widget, endpoint) { @@ -44,9 +48,9 @@ async function apiCall(widget, endpoint) { }); if (status === 401) { - logger.debug("Homebridge is rejecting the request, but obtaining new session token"); - const { data: loginData } = login(widget); - headers.Authorization = loginData?.auth_token; + logger.debug("Homebridge API rejected the request, attempting to obtain new session token"); + const { access_token } = login(widget); + headers.Authorization = `Bearer ${access_token}`; // retry the request, now with the new session token [status, contentType, data, responseHeaders] = await httpProxy(url, { From e19583b6b09dff6bf5b04d7b939e691007bbe22c Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+shamoon@users.noreply.github.com> Date: Mon, 24 Oct 2022 22:21:07 -0700 Subject: [PATCH 7/7] lint --- src/widgets/homebridge/proxy.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/widgets/homebridge/proxy.js b/src/widgets/homebridge/proxy.js index b05b475c..3f81051f 100644 --- a/src/widgets/homebridge/proxy.js +++ b/src/widgets/homebridge/proxy.js @@ -16,6 +16,7 @@ async function login(widget) { const loginUrl = new URL(formatApiCall(api, { endpoint, ...widget })); const loginBody = { username: widget.username, password: widget.password }; const headers = { "Content-Type": "application/json" }; + // eslint-disable-next-line no-unused-vars const [status, contentType, data, responseHeaders] = await httpProxy(loginUrl, { method: "POST", body: JSON.stringify(loginBody), @@ -23,14 +24,15 @@ async function login(widget) { }); try { - const { access_token, expires_in } = JSON.parse(data.toString()); + const { access_token: accessToken, expires_in: expiresIn } = JSON.parse(data.toString()); - cache.put(sessionTokenCacheKey, access_token, (expires_in * 1000) - 5 * 60 * 1000); // expires_in (s) - 5m - return { access_token }; + cache.put(sessionTokenCacheKey, accessToken, (expiresIn * 1000) - 5 * 60 * 1000); // expiresIn (s) - 5m + return { accessToken }; } catch (e) { logger.error("Unable to login to Homebridge API: %s", e); - return { access_token: false }; } + + return { accessToken: false }; } async function apiCall(widget, endpoint) { @@ -49,8 +51,8 @@ async function apiCall(widget, endpoint) { if (status === 401) { logger.debug("Homebridge API rejected the request, attempting to obtain new session token"); - const { access_token } = login(widget); - headers.Authorization = `Bearer ${access_token}`; + const { accessToken } = login(widget); + headers.Authorization = `Bearer ${accessToken}`; // retry the request, now with the new session token [status, contentType, data, responseHeaders] = await httpProxy(url, {