Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7652eb8c1c | ||
|
|
e761c10f32 |
No files matched your search
@@ -43,6 +43,7 @@ import temperature from './img/temperature.png';
|
|||||||
### Interactions
|
### Interactions
|
||||||
|
|
||||||
- Toggle entity state by clicking on the entity (only when clickable enabled).
|
- Toggle entity state by clicking on the entity (only when clickable enabled).
|
||||||
|
- After a successful toggle, the displayed state refreshes immediately.
|
||||||
|
|
||||||
### Adding the widget
|
### Adding the widget
|
||||||
|
|
||||||
@@ -64,4 +65,4 @@ import retrieveEntityId from './img/retrieve-entity-id.png';
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<img src={retrieveEntityId} alt="homeassistant retrieve-entity-id" width={256 * 2} />
|
<img src={retrieveEntityId} alt="homeassistant retrieve-entity-id" width={256 * 2} />
|
||||||
</div>
|
</div>
|
||||||
@@ -47,4 +47,7 @@ import withDisplayname from './img/with-displayname.png';
|
|||||||
<AddingWidget />
|
<AddingWidget />
|
||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
|
|
||||||
|
Use the Home Assistant entity ID (for example, `automation.turn_on_lights`) in the **Automation ID** field, not the automation's internal ID.
|
||||||
|
|
||||||
<WidgetConfig configuration={smartHomeExecuteAutomationWidget.configuration} />
|
<WidgetConfig configuration={smartHomeExecuteAutomationWidget.configuration} />
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||||
@@ -41,6 +42,13 @@ export const smartHomeRouter = createTRPCRouter({
|
|||||||
const client = await createIntegrationAsync(integration);
|
const client = await createIntegrationAsync(integration);
|
||||||
const success = await client.triggerToggleAsync(input.entityId);
|
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;
|
return success;
|
||||||
}),
|
}),
|
||||||
executeAutomation: protectedProcedure
|
executeAutomation: protectedProcedure
|
||||||
@@ -55,6 +63,13 @@ export const smartHomeRouter = createTRPCRouter({
|
|||||||
.input(z.object({ automationId: z.string() }))
|
.input(z.object({ automationId: z.string() }))
|
||||||
.mutation(async ({ ctx: { integration }, input }) => {
|
.mutation(async ({ ctx: { integration }, input }) => {
|
||||||
const client = await createIntegrationAsync(integration);
|
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",
|
||||||
|
});
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@@ -134,7 +134,10 @@ export class HomeAssistantIntegration extends Integration implements ISmartHomeI
|
|||||||
*/
|
*/
|
||||||
private async postAsync(path: `/api/${string}`, body: Record<string, string>) {
|
private async postAsync(path: `/api/${string}`, body: Record<string, string>) {
|
||||||
return await fetchWithTrustedCertificatesAsync(this.url(path), {
|
return await fetchWithTrustedCertificatesAsync(this.url(path), {
|
||||||
headers: this.getAuthHeaders(),
|
headers: {
|
||||||
|
...this.getAuthHeaders(),
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
method: "POST",
|
method: "POST",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -18,4 +18,6 @@ export const smartHomeEntityStateRequestHandler = createIntegrationRequestHandle
|
|||||||
|
|
||||||
return result.data.state;
|
return result.data.state;
|
||||||
},
|
},
|
||||||
|
// The widget already polls every 30 seconds and refetches after toggles.
|
||||||
|
cacheTtlMs: 0,
|
||||||
});
|
});
|
||||||
Reference in new issue
Block a user