mirror of
				https://github.com/karl0ss/homepage.git
				synced 2025-11-04 00:10:57 +00:00 
			
		
		
		
	Merge pull request #442 from Fernando-Neira/widget/homebridge
feature: add homebridge widget
This commit is contained in:
		
						commit
						68631a6e48
					
				@ -284,5 +284,13 @@
 | 
			
		||||
        "96-night": "Thunderstorm With Hail",
 | 
			
		||||
        "99-day": "Thunderstorm With Hail",
 | 
			
		||||
        "99-night": "Thunderstorm With Hail"
 | 
			
		||||
    },
 | 
			
		||||
    "homebridge": {
 | 
			
		||||
        "available_update": "System",
 | 
			
		||||
        "updates": "Updates",
 | 
			
		||||
        "update_available": "Update Available",
 | 
			
		||||
        "up_to_date": "Up to Date",
 | 
			
		||||
        "child_bridges": "Child Bridges",
 | 
			
		||||
        "child_bridges_status": "{{ok}}/{{total}}"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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")),
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										51
									
								
								src/widgets/homebridge/component.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/widgets/homebridge/component.jsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,51 @@
 | 
			
		||||
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: homebridgeData, error: homebridgeError } = useWidgetAPI(widget, "info");
 | 
			
		||||
 | 
			
		||||
  if (homebridgeError || homebridgeData?.error) {
 | 
			
		||||
    return <Container error={t("widget.api_error")} />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!homebridgeData) {
 | 
			
		||||
    return (
 | 
			
		||||
      <Container service={service}>
 | 
			
		||||
        <Block label="widget.status" />
 | 
			
		||||
        <Block label="homebridge.updates" />
 | 
			
		||||
        <Block label="homebridge.child_bridges" />
 | 
			
		||||
      </Container>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Container service={service}>
 | 
			
		||||
      <Block
 | 
			
		||||
        label="widget.status"
 | 
			
		||||
        value={`${homebridgeData.status[0].toUpperCase()}${homebridgeData.status.substr(1)}`}
 | 
			
		||||
      />
 | 
			
		||||
      <Block
 | 
			
		||||
        label="homebridge.updates"
 | 
			
		||||
        value={
 | 
			
		||||
          (homebridgeData.updateAvailable || homebridgeData.plugins?.updatesAvailable)
 | 
			
		||||
            ? t("homebridge.update_available")
 | 
			
		||||
            : t("homebridge.up_to_date")}
 | 
			
		||||
      />
 | 
			
		||||
      {homebridgeData?.childBridges?.total > 0 &&
 | 
			
		||||
        <Block
 | 
			
		||||
          label="homebridge.child_bridges"
 | 
			
		||||
          value={t("homebridge.child_bridges_status", {
 | 
			
		||||
            total: homebridgeData.childBridges.total,
 | 
			
		||||
            ok: homebridgeData.childBridges.running
 | 
			
		||||
          })}
 | 
			
		||||
        />}
 | 
			
		||||
    </Container>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										106
									
								
								src/widgets/homebridge/proxy.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								src/widgets/homebridge/proxy.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,106 @@
 | 
			
		||||
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" };
 | 
			
		||||
  // eslint-disable-next-line no-unused-vars
 | 
			
		||||
  const [status, contentType, data, responseHeaders] = await httpProxy(loginUrl, {
 | 
			
		||||
    method: "POST",
 | 
			
		||||
    body: JSON.stringify(loginBody),
 | 
			
		||||
    headers,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
    const { access_token: accessToken, expires_in: expiresIn } = JSON.parse(data.toString());
 | 
			
		||||
  
 | 
			
		||||
    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 { accessToken: false };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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 API rejected the request, attempting to obtain new session 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, {
 | 
			
		||||
      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 };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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" });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!cache.get(sessionTokenCacheKey)) {
 | 
			
		||||
    await login(widget);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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({
 | 
			
		||||
      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
 | 
			
		||||
      }
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										14
									
								
								src/widgets/homebridge/widget.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/widgets/homebridge/widget.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,14 @@
 | 
			
		||||
import homebridgeProxyHandler from "./proxy";
 | 
			
		||||
 | 
			
		||||
const widget = {
 | 
			
		||||
  api: "{url}/api/{endpoint}",
 | 
			
		||||
  proxyHandler: homebridgeProxyHandler,
 | 
			
		||||
 | 
			
		||||
  mappings: {
 | 
			
		||||
    info: {
 | 
			
		||||
      endpoint: "/",
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default widget;
 | 
			
		||||
@ -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,
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user