feat: add homeassistant integration (#578)
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import { decryptSecret } from "@homarr/common";
|
||||
import { and, eq, inArray } from "@homarr/db";
|
||||
import { integrations } from "@homarr/db/schema/sqlite";
|
||||
import type { IntegrationKind } from "@homarr/definitions";
|
||||
import { z } from "@homarr/validation";
|
||||
|
||||
import { decryptSecret } from "../router/integration";
|
||||
import { publicProcedure } from "../trpc";
|
||||
|
||||
export const createOneIntegrationMiddleware = <TKind extends IntegrationKind>(...kinds: TKind[]) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import crypto from "crypto";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import { decryptSecret, encryptSecret } from "@homarr/common";
|
||||
import type { Database } from "@homarr/db";
|
||||
import { and, createId, eq } from "@homarr/db";
|
||||
import { integrations, integrationSecrets } from "@homarr/db/schema/sqlite";
|
||||
@@ -207,27 +207,6 @@ export const integrationRouter = createTRPCRouter({
|
||||
}),
|
||||
});
|
||||
|
||||
const algorithm = "aes-256-cbc"; //Using AES encryption
|
||||
const key = Buffer.from("1d71cceced68159ba59a277d056a66173613052cbeeccbfbd15ab1c909455a4d", "hex"); // TODO: generate with const data = crypto.randomBytes(32).toString('hex')
|
||||
|
||||
export function encryptSecret(text: string): `${string}.${string}` {
|
||||
const initializationVector = crypto.randomBytes(16);
|
||||
const cipher = crypto.createCipheriv(algorithm, Buffer.from(key), initializationVector);
|
||||
let encrypted = cipher.update(text);
|
||||
encrypted = Buffer.concat([encrypted, cipher.final()]);
|
||||
return `${encrypted.toString("hex")}.${initializationVector.toString("hex")}`;
|
||||
}
|
||||
|
||||
export function decryptSecret(value: `${string}.${string}`) {
|
||||
const [data, dataIv] = value.split(".") as [string, string];
|
||||
const initializationVector = Buffer.from(dataIv, "hex");
|
||||
const encryptedText = Buffer.from(data, "hex");
|
||||
const decipher = crypto.createDecipheriv(algorithm, Buffer.from(key), initializationVector);
|
||||
let decrypted = decipher.update(encryptedText);
|
||||
decrypted = Buffer.concat([decrypted, decipher.final()]);
|
||||
return decrypted.toString();
|
||||
}
|
||||
|
||||
interface UpdateSecretInput {
|
||||
integrationId: string;
|
||||
value: string;
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { Session } from "@homarr/auth";
|
||||
import { encryptSecret } from "@homarr/common";
|
||||
import { createId } from "@homarr/db";
|
||||
import { integrations, integrationSecrets } from "@homarr/db/schema/sqlite";
|
||||
import { createDb } from "@homarr/db/test";
|
||||
|
||||
import type { RouterInputs } from "../..";
|
||||
import { encryptSecret, integrationRouter } from "../integration";
|
||||
import { integrationRouter } from "../integration";
|
||||
import { expectToBeDefined } from "./helper";
|
||||
|
||||
// Mock the auth module to return an empty session
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createTRPCRouter } from "../../trpc";
|
||||
import { appRouter } from "./app";
|
||||
import { dnsHoleRouter } from "./dns-hole";
|
||||
import { notebookRouter } from "./notebook";
|
||||
import { smartHomeRouter } from "./smart-home";
|
||||
import { weatherRouter } from "./weather";
|
||||
|
||||
export const widgetRouter = createTRPCRouter({
|
||||
@@ -9,4 +10,5 @@ export const widgetRouter = createTRPCRouter({
|
||||
weather: weatherRouter,
|
||||
app: appRouter,
|
||||
dnsHole: dnsHoleRouter,
|
||||
smartHome: smartHomeRouter,
|
||||
});
|
||||
|
||||
38
packages/api/src/router/widgets/smart-home.ts
Normal file
38
packages/api/src/router/widgets/smart-home.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { observable } from "@trpc/server/observable";
|
||||
|
||||
import { HomeAssistantIntegration } from "@homarr/integrations";
|
||||
import { homeAssistantEntityState } from "@homarr/redis";
|
||||
import { z } from "@homarr/validation";
|
||||
|
||||
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
|
||||
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||
|
||||
export const smartHomeRouter = createTRPCRouter({
|
||||
subscribeEntityState: publicProcedure.input(z.object({ entityId: z.string() })).subscription(({ input }) => {
|
||||
return observable<{
|
||||
entityId: string;
|
||||
state: string;
|
||||
}>((emit) => {
|
||||
homeAssistantEntityState.subscribe((message) => {
|
||||
if (message.entityId !== input.entityId) {
|
||||
return;
|
||||
}
|
||||
emit.next(message);
|
||||
});
|
||||
});
|
||||
}),
|
||||
switchEntity: publicProcedure
|
||||
.unstable_concat(createOneIntegrationMiddleware("homeAssistant"))
|
||||
.input(z.object({ entityId: z.string() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const client = new HomeAssistantIntegration(ctx.integration);
|
||||
return await client.triggerToggleAsync(input.entityId);
|
||||
}),
|
||||
executeAutomation: publicProcedure
|
||||
.unstable_concat(createOneIntegrationMiddleware("homeAssistant"))
|
||||
.input(z.object({ automationId: z.string() }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const client = new HomeAssistantIntegration(ctx.integration);
|
||||
await client.triggerAutomationAsync(input.automationId);
|
||||
}),
|
||||
});
|
||||
Reference in New Issue
Block a user