Skip to content

Commit

Permalink
feat: unifi controller integration
Browse files Browse the repository at this point in the history
  • Loading branch information
manuel-rw committed Mar 9, 2025
1 parent d9720ac commit b9970e6
Show file tree
Hide file tree
Showing 24 changed files with 874 additions and 3 deletions.
7 changes: 6 additions & 1 deletion apps/nextjs/src/app/[locale]/boards/(content)/_theme.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import type { PropsWithChildren } from "react";
import type { MantineColorsTuple } from "@mantine/core";
import { colorsTuple, createTheme, darken, lighten, MantineProvider } from "@mantine/core";
import { colorsTuple, createTheme, darken, lighten, MantineProvider, rem } from "@mantine/core";

import { useRequiredBoard } from "@homarr/boards/context";
import type { ColorScheme } from "@homarr/definitions";
Expand All @@ -24,6 +24,11 @@ export const BoardMantineProvider = ({
},
primaryColor: "primaryColor",
autoContrast: true,
fontSizes: {
xl2: rem(24),
xl3: rem(28),
xl4: rem(36),
},
});

return (
Expand Down
2 changes: 2 additions & 0 deletions packages/api/src/router/widgets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { mediaRequestsRouter } from "./media-requests";
import { mediaServerRouter } from "./media-server";
import { mediaTranscodingRouter } from "./media-transcoding";
import { minecraftRouter } from "./minecraft";
import { networkControllerRouter } from "./network-controller";
import { notebookRouter } from "./notebook";
import { optionsRouter } from "./options";
import { rssFeedRouter } from "./rssFeed";
Expand All @@ -31,4 +32,5 @@ export const widgetRouter = createTRPCRouter({
mediaTranscoding: mediaTranscodingRouter,
minecraft: minecraftRouter,
options: optionsRouter,
networkController: networkControllerRouter,
});
62 changes: 62 additions & 0 deletions packages/api/src/router/widgets/network-controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { observable } from "@trpc/server/observable";

import type { Modify } from "@homarr/common/types";
import type { Integration } from "@homarr/db/schema";
import type { IntegrationKindByCategory } from "@homarr/definitions";
import { getIntegrationKindsByCategory } from "@homarr/definitions";
import type { NetworkControllerSummary } from "@homarr/integrations/types";
import { networkControllerRequestHandler } from "@homarr/request-handler/network-controller";

import { createManyIntegrationMiddleware } from "../../middlewares/integration";
import { createTRPCRouter, publicProcedure } from "../../trpc";

export const networkControllerRouter = createTRPCRouter({
summary: publicProcedure
.unstable_concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("networkController")))
.query(async ({ ctx }) => {
const results = await Promise.all(
ctx.integrations.map(async (integration) => {
const innerHandler = networkControllerRequestHandler.handler(integration, {});
const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });

return {
integration: {
id: integration.id,
name: integration.name,
kind: integration.kind,
updatedAt: timestamp,
},
summary: data,
};
}),
);
return results;
}),

subscribeToSummary: publicProcedure
.unstable_concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("networkController")))
.subscription(({ ctx }) => {
return observable<{
integration: Modify<Integration, { kind: IntegrationKindByCategory<"networkController"> }>;
summary: NetworkControllerSummary;
}>((emit) => {
const unsubscribes: (() => void)[] = [];
for (const integrationWithSecrets of ctx.integrations) {
const { decryptedSecrets: _, ...integration } = integrationWithSecrets;
const innerHandler = networkControllerRequestHandler.handler(integrationWithSecrets, {});
const unsubscribe = innerHandler.subscribe((summary) => {
emit.next({
integration,
summary,
});
});
unsubscribes.push(unsubscribe);
}
return () => {
unsubscribes.forEach((unsubscribe) => {
unsubscribe();
});
};
});
}),
});
2 changes: 2 additions & 0 deletions packages/cron-jobs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { mediaOrganizerJob } from "./jobs/integrations/media-organizer";
import { mediaRequestListJob, mediaRequestStatsJob } from "./jobs/integrations/media-requests";
import { mediaServerJob } from "./jobs/integrations/media-server";
import { mediaTranscodingJob } from "./jobs/integrations/media-transcoding";
import { networkControllerJob } from "./jobs/integrations/network-controller";
import { minecraftServerStatusJob } from "./jobs/minecraft-server-status";
import { pingJob } from "./jobs/ping";
import { rssFeedsJob } from "./jobs/rss-feeds";
Expand All @@ -34,6 +35,7 @@ export const jobGroup = createCronJobGroup({
updateChecker: updateCheckerJob,
mediaTranscoding: mediaTranscodingJob,
minecraftServerStatus: minecraftServerStatusJob,
networkController: networkControllerJob,
});

export type JobGroupKeys = ReturnType<(typeof jobGroup)["getKeys"]>[number];
14 changes: 14 additions & 0 deletions packages/cron-jobs/src/jobs/integrations/network-controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { EVERY_MINUTE } from "@homarr/cron-jobs-core/expressions";
import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler";
import { networkControllerRequestHandler } from "@homarr/request-handler/network-controller";

import { createCronJob } from "../../lib";

export const networkControllerJob = createCronJob("networkController", EVERY_MINUTE).withCallback(
createRequestIntegrationJobHandler(networkControllerRequestHandler.handler, {
widgetKinds: ["networkControllerSummary"],
getInput: {
networkControllerSummary: () => ({}),
},
}),
);
9 changes: 8 additions & 1 deletion packages/definitions/src/integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,12 @@ export const integrationDefs = {
category: ["calendar"],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/nextcloud.svg",
},
unifiController: {
name: "Unifi Controller",
secretKinds: [["username", "password"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/unifi.png",
category: ["networkController"],
},
} as const satisfies Record<string, integrationDefinition>;

export const integrationKinds = objectKeys(integrationDefs) as AtLeastOneOf<IntegrationKind>;
Expand Down Expand Up @@ -209,4 +215,5 @@ export type IntegrationCategory =
| "indexerManager"
| "healthMonitoring"
| "search"
| "mediaTranscoding";
| "mediaTranscoding"
| "networkController";
2 changes: 2 additions & 0 deletions packages/definitions/src/widget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export const widgetKinds = [
"mediaRequests-requestStats",
"mediaTranscoding",
"minecraftServerStatus",
"networkControllerSummary",
"networkControllerNetworkStatus",
"rssFeed",
"bookmarks",
"indexerManager",
Expand Down
2 changes: 2 additions & 0 deletions packages/integrations/src/base/creator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { createPiHoleIntegrationAsync } from "../pi-hole/pi-hole-integration-fac
import { PlexIntegration } from "../plex/plex-integration";
import { ProwlarrIntegration } from "../prowlarr/prowlarr-integration";
import { ProxmoxIntegration } from "../proxmox/proxmox-integration";
import { UnifiControllerIntegration } from "../unifi-controller/unifi-controller-integration";
import type { Integration, IntegrationInput } from "./integration";

export const createIntegrationAsync = async <TKind extends keyof typeof integrationCreators>(
Expand Down Expand Up @@ -88,6 +89,7 @@ export const integrationCreators = {
proxmox: ProxmoxIntegration,
emby: EmbyIntegration,
nextcloud: NextcloudIntegration,
unifiController: UnifiControllerIntegration,
} satisfies Record<IntegrationKind, IntegrationInstance | [(input: IntegrationInput) => Promise<Integration>]>;

type IntegrationInstanceOfKind<TKind extends keyof typeof integrationCreators> = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { NetworkControllerSummary } from "./network-controller-summary-types";

export interface NetworkControllerSummaryIntegration {
getNetworkSummaryAsync(): Promise<NetworkControllerSummary>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export interface NetworkControllerSummary {
wanStatus: "enabled" | "disabled";

wwwStatus: "enabled" | "disabled";
wwwLatency: number;
wwwPing: number;
wwwUptime: number;

wifiStatus: "enabled" | "disabled";
wifiUsers: number;
wifiGuests: number;

lanStatus: "enabled" | "disabled";
lanUsers: number;
lanGuests: number;

vpnStatus: "enabled" | "disabled";
vpnUsers: number;
}
2 changes: 2 additions & 0 deletions packages/integrations/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
export * from "./calendar-types";
export * from "./interfaces/dns-hole-summary/dns-hole-summary-types";
export * from "./interfaces/network-controller-summary/network-controller-summary-types";
export * from "./interfaces/health-monitoring/healt-monitoring";
export * from "./interfaces/indexer-manager/indexer";
export * from "./interfaces/media-requests/media-request";
export * from "./base/searchable-integration";
export * from "./homeassistant/homeassistant-types";
export * from "./proxmox/proxmox-types";
export * from "./unifi-controller/unifi-controller-types";
22 changes: 22 additions & 0 deletions packages/integrations/src/unifi-controller/error-by-status-code.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { IntegrationTestConnectionError } from "../base/test-connection-error";

export const throwByUnifiControllerResponseStatusCode = (statusCode: number) => {
switch (statusCode) {
case 400:
throw new IntegrationTestConnectionError("badRequest");
case 401:
throw new IntegrationTestConnectionError("unauthorized");
case 403:
throw new IntegrationTestConnectionError("forbidden");
case 404:
throw new IntegrationTestConnectionError("notFound");
case 429:
throw new IntegrationTestConnectionError("tooManyRequests");
case 500:
throw new IntegrationTestConnectionError("internalServerError");
case 503:
throw new IntegrationTestConnectionError("serviceUnavailable");
default:
throw new IntegrationTestConnectionError("commonError");
}
};
Loading

0 comments on commit b9970e6

Please # to comment.