diff --git a/apps/tasks/package.json b/apps/tasks/package.json index 231850eaf..612d7544d 100644 --- a/apps/tasks/package.json +++ b/apps/tasks/package.json @@ -27,6 +27,8 @@ "@homarr/ping": "workspace:^0.1.0", "@homarr/redis": "workspace:^0.1.0", "@homarr/server-settings": "workspace:^0.1.0", + "@homarr/integrations": "workspace:^0.1.0", + "@homarr/widgets": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0", "@homarr/analytics": "workspace:^0.1.0", "dotenv": "^16.4.5", diff --git a/apps/tasks/src/jobs.ts b/apps/tasks/src/jobs.ts index 5f62247dd..457a8fc14 100644 --- a/apps/tasks/src/jobs.ts +++ b/apps/tasks/src/jobs.ts @@ -1,4 +1,5 @@ import { iconsUpdaterJob } from "~/jobs/icons-updater"; +import { smartHomeEntityStateJob } from "~/jobs/integrations/home-assistant"; import { analyticsJob } from "./jobs/analytics"; import { pingJob } from "./jobs/ping"; import { queuesJob } from "./jobs/queue"; @@ -9,6 +10,7 @@ export const jobs = createJobGroup({ analytics: analyticsJob, iconsUpdater: iconsUpdaterJob, ping: pingJob, + smartHomeEntityState: smartHomeEntityStateJob, // This job is used to process queues. queues: queuesJob, diff --git a/apps/tasks/src/jobs/integrations/home-assistant.ts b/apps/tasks/src/jobs/integrations/home-assistant.ts new file mode 100644 index 000000000..2b96715a9 --- /dev/null +++ b/apps/tasks/src/jobs/integrations/home-assistant.ts @@ -0,0 +1,64 @@ +import SuperJSON from "superjson"; + +import { decryptSecret } from "@homarr/common"; +import { db, eq } from "@homarr/db"; +import { items } from "@homarr/db/schema/sqlite"; +import { HomeAssistantIntegration } from "@homarr/integrations"; +import { logger } from "@homarr/log"; +import { homeAssistantEntityState } from "@homarr/redis"; +import type { WidgetComponentProps } from "@homarr/widgets"; + +import { EVERY_MINUTE } from "~/lib/cron-job/constants"; +import { createCronJob } from "~/lib/cron-job/creator"; + +export const smartHomeEntityStateJob = createCronJob(EVERY_MINUTE).withCallback(async () => { + const itemsForIntegration = await db.query.items.findMany({ + where: eq(items.kind, "smartHome-entityState"), + with: { + integrations: { + with: { + integration: { + with: { + secrets: { + columns: { + kind: true, + value: true, + }, + }, + }, + }, + }, + }, + }, + }); + + for (const itemForIntegration of itemsForIntegration) { + const integration = itemForIntegration.integrations[0]?.integration; + if (!integration) { + continue; + } + + const options = SuperJSON.parse["options"]>( + itemForIntegration.options, + ); + + const homeAssistant = new HomeAssistantIntegration({ + ...integration, + decryptedSecrets: integration.secrets.map((secret) => ({ + ...secret, + value: decryptSecret(secret.value), + })), + }); + const state = await homeAssistant.getEntityStateAsync(options.entityId); + + if (!state.success) { + logger.error("Unable to fetch data from Home Assistant"); + continue; + } + + await homeAssistantEntityState.publishAsync({ + entityId: options.entityId, + state: state.data.state, + }); + } +}); diff --git a/packages/api/package.json b/packages/api/package.json index e17d26c4f..843278b72 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -27,7 +27,6 @@ "@homarr/log": "workspace:^", "@homarr/ping": "workspace:^0.1.0", "@homarr/redis": "workspace:^0.1.0", - "@homarr/tasks": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0", "@homarr/server-settings": "workspace:^0.1.0", "@trpc/client": "next", diff --git a/packages/api/src/middlewares/integration.ts b/packages/api/src/middlewares/integration.ts index 8f23903fc..9895f50ad 100644 --- a/packages/api/src/middlewares/integration.ts +++ b/packages/api/src/middlewares/integration.ts @@ -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 = (...kinds: TKind[]) => { diff --git a/packages/api/src/router/integration.ts b/packages/api/src/router/integration.ts index 92fb2fcb9..c7b333673 100644 --- a/packages/api/src/router/integration.ts +++ b/packages/api/src/router/integration.ts @@ -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; diff --git a/packages/api/src/router/test/integration.spec.ts b/packages/api/src/router/test/integration.spec.ts index 9b5b7f6d7..5bbbcc4b4 100644 --- a/packages/api/src/router/test/integration.spec.ts +++ b/packages/api/src/router/test/integration.spec.ts @@ -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 diff --git a/packages/api/src/router/widgets/index.ts b/packages/api/src/router/widgets/index.ts index 903f43542..7ece7bd62 100644 --- a/packages/api/src/router/widgets/index.ts +++ b/packages/api/src/router/widgets/index.ts @@ -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, }); diff --git a/packages/api/src/router/widgets/smart-home.ts b/packages/api/src/router/widgets/smart-home.ts new file mode 100644 index 000000000..fa3ffb415 --- /dev/null +++ b/packages/api/src/router/widgets/smart-home.ts @@ -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); + }), +}); diff --git a/packages/common/src/encryption.ts b/packages/common/src/encryption.ts new file mode 100644 index 000000000..70dee629a --- /dev/null +++ b/packages/common/src/encryption.ts @@ -0,0 +1,22 @@ +import crypto from "crypto"; + +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(); +} diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index d0bedb0d6..0b6d4cb65 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -4,5 +4,7 @@ export * from "./cookie"; export * from "./array"; export * from "./stopwatch"; export * from "./hooks"; +export * from "./url"; export * from "./number"; export * from "./error"; +export * from "./encryption"; diff --git a/packages/common/src/number.ts b/packages/common/src/number.ts index 5341e5004..24738850d 100644 --- a/packages/common/src/number.ts +++ b/packages/common/src/number.ts @@ -15,3 +15,7 @@ export const formatNumber = (value: number, decimalPlaces: number) => { } return value.toFixed(decimalPlaces); }; + +export const randomInt = (min: number, max: number) => { + return Math.floor(Math.random() * (max - min + 1) + min); +}; diff --git a/packages/common/src/url.ts b/packages/common/src/url.ts new file mode 100644 index 000000000..f99fe099c --- /dev/null +++ b/packages/common/src/url.ts @@ -0,0 +1,5 @@ +export const appendPath = (url: URL | string, path: string) => { + const newUrl = new URL(url); + newUrl.pathname += path; + return newUrl; +}; diff --git a/packages/definitions/src/integration.ts b/packages/definitions/src/integration.ts index e8e153e01..7564a6ae8 100644 --- a/packages/definitions/src/integration.ts +++ b/packages/definitions/src/integration.ts @@ -137,4 +137,5 @@ export type IntegrationCategory = | "mediaSearch" | "mediaRequest" | "downloadClient" - | "useNetClient"; + | "useNetClient" + | "smartHomeServer"; diff --git a/packages/definitions/src/widget.ts b/packages/definitions/src/widget.ts index 6c50ba23b..d36b93687 100644 --- a/packages/definitions/src/widget.ts +++ b/packages/definitions/src/widget.ts @@ -1,2 +1,12 @@ -export const widgetKinds = ["clock", "weather", "app", "iframe", "video", "notebook", "dnsHoleSummary"] as const; +export const widgetKinds = [ + "clock", + "weather", + "app", + "iframe", + "video", + "notebook", + "dnsHoleSummary", + "smartHome-entityState", + "smartHome-executeAutomation", +] as const; export type WidgetKind = (typeof widgetKinds)[number]; diff --git a/packages/integrations/package.json b/packages/integrations/package.json index 057f79ed5..0bcbaee28 100644 --- a/packages/integrations/package.json +++ b/packages/integrations/package.json @@ -30,7 +30,9 @@ }, "dependencies": { "@homarr/definitions": "workspace:^0.1.0", - "@homarr/validation": "workspace:^0.1.0" + "@homarr/log": "workspace:^0.1.0", + "@homarr/validation": "workspace:^0.1.0", + "@homarr/common": "workspace:^0.1.0" }, "prettier": "@homarr/prettier-config" } diff --git a/packages/integrations/src/homeassistant/homeassistant-integration.ts b/packages/integrations/src/homeassistant/homeassistant-integration.ts new file mode 100644 index 000000000..267b3b957 --- /dev/null +++ b/packages/integrations/src/homeassistant/homeassistant-integration.ts @@ -0,0 +1,73 @@ +import { appendPath } from "@homarr/common"; +import { logger } from "@homarr/log"; + +import { Integration } from "../base/integration"; +import { entityStateSchema } from "./homeassistant-types"; + +export class HomeAssistantIntegration extends Integration { + async getEntityStateAsync(entityId: string) { + try { + const response = await fetch(appendPath(this.integration.url, `/states/${entityId}`), { + headers: { + Authorization: `Bearer ${this.getSecretValue("apiKey")}`, + }, + }); + const body = (await response.json()) as unknown; + if (!response.ok) { + logger.warn(`Response did not indicate success`); + return { + error: "Response did not indicate success", + }; + } + return entityStateSchema.safeParseAsync(body); + } catch (err) { + logger.error(`Failed to fetch from ${this.integration.url}: ${err as string}`); + return { + success: false as const, + error: err, + }; + } + } + + async triggerAutomationAsync(entityId: string) { + try { + const response = await fetch(appendPath(this.integration.url, "/services/automation/trigger"), { + headers: { + Authorization: `Bearer ${this.getSecretValue("apiKey")}`, + }, + body: JSON.stringify({ + entity_id: entityId, + }), + method: "POST", + }); + return response.ok; + } catch (err) { + logger.error(`Failed to fetch from '${this.integration.url}': ${err as string}`); + return false; + } + } + + /** + * Triggers a toggle action for a specific entity. + * + * @param entityId - The ID of the entity to toggle. + * @returns A boolean indicating whether the toggle action was successful. + */ + async triggerToggleAsync(entityId: string) { + try { + const response = await fetch(appendPath(this.integration.url, "/services/homeassistant/toggle"), { + headers: { + Authorization: `Bearer ${this.getSecretValue("apiKey")}`, + }, + body: JSON.stringify({ + entity_id: entityId, + }), + method: "POST", + }); + return response.ok; + } catch (err) { + logger.error(`Failed to fetch from '${this.integration.url}': ${err as string}`); + return false; + } + } +} diff --git a/packages/integrations/src/homeassistant/homeassistant-types.ts b/packages/integrations/src/homeassistant/homeassistant-types.ts new file mode 100644 index 000000000..7015b5e23 --- /dev/null +++ b/packages/integrations/src/homeassistant/homeassistant-types.ts @@ -0,0 +1,13 @@ +import { z } from "@homarr/validation"; + +export const entityStateSchema = z.object({ + attributes: z.record( + z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(z.union([z.string(), z.number()]))]), + ), + entity_id: z.string(), + last_changed: z.string().pipe(z.coerce.date()), + last_updated: z.string().pipe(z.coerce.date()), + state: z.string(), +}); + +export type EntityState = z.infer; diff --git a/packages/integrations/src/index.ts b/packages/integrations/src/index.ts index 4740f40bd..1804d2124 100644 --- a/packages/integrations/src/index.ts +++ b/packages/integrations/src/index.ts @@ -1 +1,2 @@ export { PiHoleIntegration } from "./pi-hole/pi-hole-integration"; +export { HomeAssistantIntegration } from "./homeassistant/homeassistant-integration"; diff --git a/packages/redis/src/index.ts b/packages/redis/src/index.ts index 164d9a273..78300c3cf 100644 --- a/packages/redis/src/index.ts +++ b/packages/redis/src/index.ts @@ -7,6 +7,12 @@ export const pingChannel = createSubPubChannel<{ url: string; statusCode: number "ping", ); export const pingUrlChannel = createListChannel("ping-url"); + +export const homeAssistantEntityState = createSubPubChannel<{ + entityId: string; + state: string; +}>("home-assistant/entity-state"); + export const queueChannel = createQueueChannel<{ name: string; executionDate: Date; diff --git a/packages/translation/src/lang/en.ts b/packages/translation/src/lang/en.ts index 661d05093..61251cd61 100644 --- a/packages/translation/src/lang/en.ts +++ b/packages/translation/src/lang/en.ts @@ -818,6 +818,36 @@ export default { noBrowerSupport: "Your Browser does not support iframes. Please update your browser.", }, }, + "smartHome-entityState": { + name: "Entity State", + description: "Display the state of an entity and toggle it optionally", + option: { + entityId: { + label: "Entity ID", + }, + displayName: { + label: "Display-name", + }, + entityUnit: { + label: "Entity Unit", + }, + clickable: { + label: "Clickable", + }, + }, + }, + "smartHome-executeAutomation": { + name: "Execute Automation", + description: "Trigger an automation with one click", + option: { + displayName: { + label: "Display name", + }, + automationId: { + label: "Automation ID", + }, + }, + }, weather: { name: "Weather", description: "Displays the current weather information of a set location.", diff --git a/packages/widgets/package.json b/packages/widgets/package.json index e97317dfd..a88e1c515 100644 --- a/packages/widgets/package.json +++ b/packages/widgets/package.json @@ -44,6 +44,7 @@ "@homarr/translation": "workspace:^0.1.0", "@homarr/ui": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0", + "@mantine/hooks": "^7.10.1", "@tiptap/extension-color": "2.4.0", "@tiptap/extension-highlight": "2.4.0", "@tiptap/extension-image": "2.4.0", diff --git a/packages/widgets/src/index.tsx b/packages/widgets/src/index.tsx index 98c071668..26c27533b 100644 --- a/packages/widgets/src/index.tsx +++ b/packages/widgets/src/index.tsx @@ -12,6 +12,8 @@ import * as dnsHoleSummary from "./dns-hole/summary"; import * as iframe from "./iframe"; import type { WidgetImportRecord } from "./import"; import * as notebook from "./notebook"; +import * as smartHomeEntityState from "./smart-home/entity-state"; +import * as smartHomeExecuteAutomation from "./smart-home/execute-automation"; import * as video from "./video"; import * as weather from "./weather"; @@ -29,10 +31,13 @@ export const widgetImports = { iframe, video, dnsHoleSummary, + "smartHome-entityState": smartHomeEntityState, + "smartHome-executeAutomation": smartHomeExecuteAutomation, } satisfies WidgetImportRecord; export type WidgetImports = typeof widgetImports; export type WidgetImportKey = keyof WidgetImports; +export type { WidgetComponentProps }; const loadedComponents = new Map>>(); diff --git a/packages/widgets/src/smart-home/entity-state/component.tsx b/packages/widgets/src/smart-home/entity-state/component.tsx new file mode 100644 index 000000000..c7e590b71 --- /dev/null +++ b/packages/widgets/src/smart-home/entity-state/component.tsx @@ -0,0 +1,76 @@ +"use client"; + +import React, { useState } from "react"; +import { Center, Stack, Text, UnstyledButton } from "@mantine/core"; + +import { clientApi } from "@homarr/api/client"; + +import type { WidgetComponentProps } from "../../definition"; + +export default function SmartHomeEntityStateWidget({ + options, + integrationIds, + isEditMode, +}: WidgetComponentProps<"smartHome-entityState">) { + const [lastState, setLastState] = useState<{ + entityId: string; + state: string; + }>(); + + const utils = clientApi.useUtils(); + + clientApi.widget.smartHome.subscribeEntityState.useSubscription( + { + entityId: options.entityId, + }, + { + onData(data) { + setLastState(data); + }, + }, + ); + + const { mutate } = clientApi.widget.smartHome.switchEntity.useMutation({ + onSettled: () => { + void utils.widget.smartHome.invalidate(); + }, + }); + + const attribute = options.entityUnit.length > 0 ? " " + options.entityUnit : ""; + + const handleClick = React.useCallback(() => { + if (isEditMode) { + return; + } + + if (!options.clickable) { + return; + } + + mutate({ + entityId: options.entityId, + integrationId: integrationIds[0] ?? "", + }); + }, []); + + return ( + +
+ + + {options.displayName} + + + {lastState?.state} + {attribute} + + +
+
+ ); +} diff --git a/packages/widgets/src/smart-home/entity-state/index.ts b/packages/widgets/src/smart-home/entity-state/index.ts new file mode 100644 index 000000000..300188e5f --- /dev/null +++ b/packages/widgets/src/smart-home/entity-state/index.ts @@ -0,0 +1,19 @@ +import { IconBinaryTree } from "@tabler/icons-react"; + +import { createWidgetDefinition } from "../../definition"; +import { optionsBuilder } from "../../options"; + +export const { definition, componentLoader } = createWidgetDefinition("smartHome-entityState", { + icon: IconBinaryTree, + options: optionsBuilder.from((factory) => ({ + entityId: factory.text({ + defaultValue: "sun.sun", + }), + displayName: factory.text({ + defaultValue: "Sun", + }), + entityUnit: factory.text(), + clickable: factory.switch(), + })), + supportedIntegrations: ["homeAssistant"], +}).withDynamicImport(() => import("./component")); diff --git a/packages/widgets/src/smart-home/execute-automation/component.tsx b/packages/widgets/src/smart-home/execute-automation/component.tsx new file mode 100644 index 000000000..f05417e60 --- /dev/null +++ b/packages/widgets/src/smart-home/execute-automation/component.tsx @@ -0,0 +1,55 @@ +import React from "react"; +import { ActionIcon, Center, LoadingOverlay, Overlay, Stack, Text, UnstyledButton } from "@mantine/core"; +import { useDisclosure, useTimeout } from "@mantine/hooks"; +import { IconAutomation, IconCheck } from "@tabler/icons-react"; + +import { clientApi } from "@homarr/api/client"; + +import type { WidgetComponentProps } from "../../definition"; + +export default function SmartHomeTriggerAutomationWidget({ + options, + integrationIds, + isEditMode, +}: WidgetComponentProps<"smartHome-executeAutomation">) { + const [isShowSuccess, { open: showSuccess, close: closeSuccess }] = useDisclosure(); + const { start } = useTimeout(() => { + closeSuccess(); + }, 1000); + + const { mutateAsync, isPending } = clientApi.widget.smartHome.executeAutomation.useMutation({ + onSuccess: () => { + showSuccess(); + start(); + }, + }); + const handleClick = React.useCallback(async () => { + if (isEditMode) { + return; + } + await mutateAsync({ + automationId: options.automationId, + integrationId: integrationIds[0] ?? "", + }); + }, [isEditMode]); + return ( + + {isShowSuccess && ( + +
+ + + +
+
+ )} + +
+ + + {options.displayName} + +
+
+ ); +} diff --git a/packages/widgets/src/smart-home/execute-automation/index.ts b/packages/widgets/src/smart-home/execute-automation/index.ts new file mode 100644 index 000000000..a1996e9fe --- /dev/null +++ b/packages/widgets/src/smart-home/execute-automation/index.ts @@ -0,0 +1,13 @@ +import { IconBinaryTree } from "@tabler/icons-react"; + +import { createWidgetDefinition } from "../../definition"; +import { optionsBuilder } from "../../options"; + +export const { definition, componentLoader } = createWidgetDefinition("smartHome-executeAutomation", { + icon: IconBinaryTree, + options: optionsBuilder.from((factory) => ({ + displayName: factory.text(), + automationId: factory.text(), + })), + supportedIntegrations: ["homeAssistant"], +}).withDynamicImport(() => import("./component")); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3735f7829..615eb8799 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -262,6 +262,9 @@ importers: '@homarr/icons': specifier: workspace:^0.1.0 version: link:../../packages/icons + '@homarr/integrations': + specifier: workspace:^0.1.0 + version: link:../../packages/integrations '@homarr/log': specifier: workspace:^ version: link:../../packages/log @@ -277,6 +280,9 @@ importers: '@homarr/validation': specifier: workspace:^0.1.0 version: link:../../packages/validation + '@homarr/widgets': + specifier: workspace:^0.1.0 + version: link:../../packages/widgets dotenv: specifier: ^16.4.5 version: 16.4.5 @@ -439,9 +445,6 @@ importers: '@homarr/server-settings': specifier: workspace:^0.1.0 version: link:../server-settings - '@homarr/tasks': - specifier: workspace:^0.1.0 - version: link:../../apps/tasks '@homarr/validation': specifier: workspace:^0.1.0 version: link:../validation @@ -688,9 +691,15 @@ importers: packages/integrations: dependencies: + '@homarr/common': + specifier: workspace:^0.1.0 + version: link:../common '@homarr/definitions': specifier: workspace:^0.1.0 version: link:../definitions + '@homarr/log': + specifier: workspace:^0.1.0 + version: link:../log '@homarr/validation': specifier: workspace:^0.1.0 version: link:../validation @@ -1007,6 +1016,9 @@ importers: '@homarr/validation': specifier: workspace:^0.1.0 version: link:../validation + '@mantine/hooks': + specifier: ^7.10.1 + version: 7.10.1(react@18.3.1) '@tiptap/extension-color': specifier: 2.4.0 version: 2.4.0(@tiptap/core@2.4.0(@tiptap/pm@2.2.4))(@tiptap/extension-text-style@2.4.0(@tiptap/core@2.4.0(@tiptap/pm@2.2.4)))