From 0a908de1e7768d4524170297e5d20bb1e786d824 Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Fri, 26 Sep 2025 17:34:33 +0200 Subject: [PATCH] feat(calendar): add homeassistant support (#4152) --- packages/definitions/src/integration.ts | 2 +- .../homeassistant-integration.ts | 42 +++++++++++++++++-- .../src/homeassistant/homeassistant-types.ts | 24 +++++++++++ 3 files changed, 63 insertions(+), 5 deletions(-) diff --git a/packages/definitions/src/integration.ts b/packages/definitions/src/integration.ts index 1e8269bbc..61abd39fd 100644 --- a/packages/definitions/src/integration.ts +++ b/packages/definitions/src/integration.ts @@ -161,7 +161,7 @@ export const integrationDefs = { name: "Home Assistant", secretKinds: [["apiKey"]], iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/home-assistant.svg", - category: ["smartHomeServer"], + category: ["smartHomeServer", "calendar"], documentationUrl: createDocumentationLink("/docs/integrations/home-assistant"), }, openmediavault: { diff --git a/packages/integrations/src/homeassistant/homeassistant-integration.ts b/packages/integrations/src/homeassistant/homeassistant-integration.ts index 714e24e77..c6a0e1191 100644 --- a/packages/integrations/src/homeassistant/homeassistant-integration.ts +++ b/packages/integrations/src/homeassistant/homeassistant-integration.ts @@ -1,14 +1,19 @@ +import z from "zod"; + import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; +import { ResponseError } from "@homarr/common/server"; import { logger } from "@homarr/log"; import type { IntegrationTestingInput } from "../base/integration"; import { Integration } from "../base/integration"; import { TestConnectionError } from "../base/test-connection/test-connection-error"; import type { TestingResult } from "../base/test-connection/test-connection-service"; +import type { ICalendarIntegration } from "../interfaces/calendar/calendar-integration"; import type { ISmartHomeIntegration } from "../interfaces/smart-home/smart-home-integration"; -import { entityStateSchema } from "./homeassistant-types"; +import type { CalendarEvent } from "../types"; +import { calendarEventSchema, calendarsSchema, entityStateSchema } from "./homeassistant-types"; -export class HomeAssistantIntegration extends Integration implements ISmartHomeIntegration { +export class HomeAssistantIntegration extends Integration implements ISmartHomeIntegration, ICalendarIntegration { public async getEntityStateAsync(entityId: string) { try { const response = await this.getAsync(`/api/states/${entityId}`); @@ -62,6 +67,35 @@ export class HomeAssistantIntegration extends Integration implements ISmartHomeI } } + public async getCalendarEventsAsync(start: Date, end: Date): Promise { + const calendarsResponse = await this.getAsync("/api/calendars"); + if (!calendarsResponse.ok) throw new ResponseError(calendarsResponse); + const calendars = await calendarsSchema.parseAsync(await calendarsResponse.json()); + + return await Promise.all( + calendars.map(async (calendar) => { + const response = await this.getAsync(`/api/calendars/${calendar.entity_id}`, { start, end }); + if (!response.ok) throw new ResponseError(response); + return await z.array(calendarEventSchema).parseAsync(await response.json()); + }), + ).then((events) => + events.flat().map( + (event): CalendarEvent => ({ + title: event.summary, + subTitle: null, + description: event.description, + // If not reseting it to 0 o'clock it uses utc time and therefore shows as 2 o'clock + startDate: "date" in event.start ? new Date(`${event.start.date}T00:00:00`) : new Date(event.start.dateTime), + endDate: "date" in event.end ? new Date(`${event.end.date}T00:00:00`) : new Date(event.end.dateTime), + image: null, + indicatorColor: "#18bcf2", + links: [], + location: event.location, + }), + ), + ); + } + protected async testingAsync(input: IntegrationTestingInput): Promise { const response = await input.fetchAsync(this.url("/api/config"), { headers: this.getAuthHeaders(), @@ -82,8 +116,8 @@ export class HomeAssistantIntegration extends Integration implements ISmartHomeI * @param path full path to the API endpoint * @returns the response from the API */ - private async getAsync(path: `/api/${string}`) { - return await fetchWithTrustedCertificatesAsync(this.url(path), { + private async getAsync(path: `/api/${string}`, queryParams?: Record) { + return await fetchWithTrustedCertificatesAsync(this.url(path, queryParams), { headers: this.getAuthHeaders(), }); } diff --git a/packages/integrations/src/homeassistant/homeassistant-types.ts b/packages/integrations/src/homeassistant/homeassistant-types.ts index da1043b91..cfd0750e6 100644 --- a/packages/integrations/src/homeassistant/homeassistant-types.ts +++ b/packages/integrations/src/homeassistant/homeassistant-types.ts @@ -12,3 +12,27 @@ export const entityStateSchema = z.object({ }); export type EntityState = z.infer; + +export const calendarsSchema = z.array( + z.object({ + name: z.string(), + entity_id: z.string(), + }), +); + +const calendarMomentSchema = z + .object({ + date: z.string(), + }) + .or( + z.object({ + dateTime: z.string(), + }), + ); +export const calendarEventSchema = z.object({ + start: calendarMomentSchema, + end: calendarMomentSchema, + summary: z.string(), + description: z.string().nullable(), + location: z.string().nullable(), +});