From 18cd1f961f3fc6670af4e5722fcb7bd6f5925536 Mon Sep 17 00:00:00 2001 From: Dennis Vesterlund Date: Mon, 6 May 2024 20:00:13 +0200 Subject: [PATCH] feat: home assistant entity generic toggle (#2015) Added support for generic toggle on Home Assistant entities that support it. For entities that does not support toggle the feature will silently fail. This way matches how Home Assistant handles it. --- .../en/modules/smart-home/entity-state.json | 9 ++++-- .../api/routers/smart-home/entity-state.ts | 30 +++++++++++++++++ .../server/sdk/homeassistant/HomeAssistant.ts | 24 ++++++++++++++ .../entity-state/entity-state.widget.tsx | 32 ++++++++++++++----- 4 files changed, 85 insertions(+), 10 deletions(-) diff --git a/public/locales/en/modules/smart-home/entity-state.json b/public/locales/en/modules/smart-home/entity-state.json index f7eacbbb2..99048e629 100644 --- a/public/locales/en/modules/smart-home/entity-state.json +++ b/public/locales/en/modules/smart-home/entity-state.json @@ -22,8 +22,13 @@ }, "displayFriendlyName": { "label": "Display friendly name", - "info": "Display friendly name from Home Assistant instead instead of display name" + "info": "Display friendly name from Home Assistant instead instead of display name." + }, + "genericToggle": { + "label": "Entity toggle", + "info": "Perform a generic Home Assistant toggle action on entity when clicked." } } } -} \ No newline at end of file +} + diff --git a/src/server/api/routers/smart-home/entity-state.ts b/src/server/api/routers/smart-home/entity-state.ts index fdc204f16..e9d974f3a 100644 --- a/src/server/api/routers/smart-home/entity-state.ts +++ b/src/server/api/routers/smart-home/entity-state.ts @@ -88,6 +88,36 @@ export const smartHomeEntityStateRouter = createTRPCRouter({ } } + return false; + }), + triggerToggle: protectedProcedure + .input(z.object({ + widgetId: z.string(), + configName: z.string() + })).mutation(async ({ input }) => { + const config = getConfig(input.configName); + const widget = config.widgets.find(widget => widget.id === input.widgetId) as ISmartHomeEntityStateWidget | null; + + if (!widget) { + Consola.error(`Referenced widget ${input.widgetId} does not exist on backend.`); + throw new TRPCError({ + code: 'CONFLICT', + message: 'Referenced widget does not exist on backend', + }); + } + + const instances = config.apps.filter((app) => app.integration?.type == 'homeAssistant'); + + for (const instance of instances) { + const url = new URL(instance.url); + const client = HomeAssistantSingleton.getOrSet(url, findAppProperty(instance, 'apiKey')); + const state = await client.triggerToggle(widget.properties.entityId); + + if (state) { + return true; + } + } + return false; }), }); diff --git a/src/tools/server/sdk/homeassistant/HomeAssistant.ts b/src/tools/server/sdk/homeassistant/HomeAssistant.ts index 1a317772c..2f443edd6 100644 --- a/src/tools/server/sdk/homeassistant/HomeAssistant.ts +++ b/src/tools/server/sdk/homeassistant/HomeAssistant.ts @@ -56,4 +56,28 @@ export class HomeAssistant { 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 triggerToggle(entityId: string) { + try { + const response = await fetch(appendPath(this.basePath, `/services/homeassistant/toggle`), { + headers: { + 'Authorization': `Bearer ${this.token}`, + }, + body: JSON.stringify({ + 'entity_id': entityId, + }), + method: 'POST' + }); + return response.ok; + } catch (err) { + Consola.error(`Failed to fetch from '${this.basePath}': ${err}`); + return false; + } + } } diff --git a/src/widgets/smart-home/entity-state/entity-state.widget.tsx b/src/widgets/smart-home/entity-state/entity-state.widget.tsx index e448fd235..f2d3c8993 100644 --- a/src/widgets/smart-home/entity-state/entity-state.widget.tsx +++ b/src/widgets/smart-home/entity-state/entity-state.widget.tsx @@ -21,6 +21,11 @@ const definition = defineWidget({ defaultValue: false, info: true, }, + genericToggle: { + type: 'switch', + defaultValue: false, + info: true, + }, automationId: { type: 'text', info: true, @@ -82,15 +87,26 @@ function EntityStateTile({ widget }: SmartHomeEntityStateWidgetProps) { }, }); - const handleClick = async () => { - if (!widget.properties.automationId) { - return; - } + const { mutateAsync: mutateTriggerToggleSync } = api.smartHomeEntityState.triggerToggle.useMutation({ + onSuccess: () => { + void utils.smartHomeEntityState.invalidate(); + }, + }); - await mutateTriggerAutomationAsync({ - configName: configName as string, - widgetId: widget.id, - }); + const handleClick = async () => { + if (widget.properties.genericToggle) { + await mutateTriggerToggleSync({ + configName: configName as string, + widgetId: widget.id, + }); + } + + if (widget.properties.automationId) { + await mutateTriggerAutomationAsync({ + configName: configName as string, + widgetId: widget.id, + }); + } }; let dataComponent = null;