diff --git a/apps/docs/docs/widgets/smart-home-entity-state/index.mdx b/apps/docs/docs/widgets/smart-home-entity-state/index.mdx index d101d6376..0c991ccc6 100644 --- a/apps/docs/docs/widgets/smart-home-entity-state/index.mdx +++ b/apps/docs/docs/widgets/smart-home-entity-state/index.mdx @@ -43,6 +43,7 @@ import temperature from './img/temperature.png'; ### Interactions - Toggle entity state by clicking on the entity (only when clickable enabled). +- After a successful toggle, the displayed state refreshes immediately. ### Adding the widget @@ -64,4 +65,4 @@ import retrieveEntityId from './img/retrieve-entity-id.png';
homeassistant retrieve-entity-id -
\ No newline at end of file + diff --git a/apps/docs/docs/widgets/smart-home-execute-automation/index.mdx b/apps/docs/docs/widgets/smart-home-execute-automation/index.mdx index 522c113ca..ce73df9d3 100644 --- a/apps/docs/docs/widgets/smart-home-execute-automation/index.mdx +++ b/apps/docs/docs/widgets/smart-home-execute-automation/index.mdx @@ -47,4 +47,7 @@ import withDisplayname from './img/with-displayname.png'; ### Configuration + +Use the Home Assistant entity ID (for example, `automation.turn_on_lights`) in the **Automation ID** field, not the automation's internal ID. + diff --git a/packages/api/src/router/widgets/smart-home.ts b/packages/api/src/router/widgets/smart-home.ts index c37468752..6efa16c9c 100644 --- a/packages/api/src/router/widgets/smart-home.ts +++ b/packages/api/src/router/widgets/smart-home.ts @@ -1,3 +1,4 @@ +import { TRPCError } from "@trpc/server"; import { z } from "zod/v4"; import { getIntegrationKindsByCategory } from "@homarr/definitions"; @@ -41,6 +42,13 @@ export const smartHomeRouter = createTRPCRouter({ const client = await createIntegrationAsync(integration); const success = await client.triggerToggleAsync(input.entityId); + if (!success) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Home Assistant failed to toggle the entity", + }); + } + return success; }), executeAutomation: protectedProcedure @@ -55,6 +63,13 @@ export const smartHomeRouter = createTRPCRouter({ .input(z.object({ automationId: z.string() })) .mutation(async ({ ctx: { integration }, input }) => { const client = await createIntegrationAsync(integration); - await client.triggerAutomationAsync(input.automationId); + const success = await client.triggerAutomationAsync(input.automationId); + + if (!success) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Home Assistant failed to trigger the automation", + }); + } }), }); diff --git a/packages/integrations/src/homeassistant/homeassistant-integration.ts b/packages/integrations/src/homeassistant/homeassistant-integration.ts index 6da82df46..15c4ece7f 100644 --- a/packages/integrations/src/homeassistant/homeassistant-integration.ts +++ b/packages/integrations/src/homeassistant/homeassistant-integration.ts @@ -134,7 +134,10 @@ export class HomeAssistantIntegration extends Integration implements ISmartHomeI */ private async postAsync(path: `/api/${string}`, body: Record) { return await fetchWithTrustedCertificatesAsync(this.url(path), { - headers: this.getAuthHeaders(), + headers: { + ...this.getAuthHeaders(), + "Content-Type": "application/json", + }, body: JSON.stringify(body), method: "POST", }); diff --git a/packages/integrations/test/home-assistant-unit.spec.ts b/packages/integrations/test/home-assistant-unit.spec.ts new file mode 100644 index 000000000..4887a95a4 --- /dev/null +++ b/packages/integrations/test/home-assistant-unit.spec.ts @@ -0,0 +1,48 @@ +// @vitest-environment node + +import { beforeEach, describe, expect, test, vi } from "vitest"; + +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; + +import { HomeAssistantIntegration } from "../src"; + +vi.mock("@homarr/core/infrastructure/http", () => ({ + fetchWithTrustedCertificatesAsync: vi.fn(), +})); + +const mockFetch = vi.mocked(fetchWithTrustedCertificatesAsync); +const integration = new HomeAssistantIntegration({ + id: "home-assistant", + name: "Home Assistant", + url: "http://home-assistant.local:8123", + externalUrl: null, + decryptedSecrets: [{ kind: "apiKey", value: "secret" }], +}); + +describe("HomeAssistantIntegration actions", () => { + beforeEach(() => { + mockFetch.mockReset(); + mockFetch.mockResolvedValue({ ok: true } as never); + }); + + test.each([ + ["toggle", () => integration.triggerToggleAsync("light.desk"), "/api/services/homeassistant/toggle", "light.desk"], + [ + "automation", + () => integration.triggerAutomationAsync("automation.turn_on_lights"), + "/api/services/automation/trigger", + "automation.turn_on_lights", + ], + ])("sends the %s service call as JSON", async (_action, trigger, path, entityId) => { + await expect(trigger()).resolves.toBe(true); + + expect(mockFetch).toHaveBeenCalledWith(new URL(`http://home-assistant.local:8123${path}`), { + headers: { + Authorization: "Bearer secret", + "Content-Type": "application/json", + }, + body: JSON.stringify({ entity_id: entityId }), + method: "POST", + }); + }); +}); diff --git a/packages/request-handler/src/smart-home-entity-state.ts b/packages/request-handler/src/smart-home-entity-state.ts index 3c8851522..503690a37 100644 --- a/packages/request-handler/src/smart-home-entity-state.ts +++ b/packages/request-handler/src/smart-home-entity-state.ts @@ -18,4 +18,6 @@ export const smartHomeEntityStateRequestHandler = createIntegrationRequestHandle return result.data.state; }, + // The widget already polls every 30 seconds and refetches after toggles. + cacheTtlMs: 0, });