From 002ff5ace2779811590bee9078f533a49ae37575 Mon Sep 17 00:00:00 2001 From: mdvanes <4253562+mdvanes@users.noreply.github.com> Date: Wed, 22 May 2024 19:27:17 +0200 Subject: [PATCH] chore: implement back-end for HA switches --- .../Molecules/SwitchBarList/HaSwitchBar.tsx | 12 +- .../src/Services/generated/switchesApi.ts | 46 ++++++++ .../src/switches/switches.controller.ts | 38 +++++++ libs/types/definitions/internal/switches.yml | 79 +++++++++++++ libs/types/src/lib/generated/switches.ts | 107 ++++++++++++++++++ libs/types/src/lib/switches.types.ts | 8 ++ openapi-config.ts | 6 + redocly.yaml | 6 +- 8 files changed, 299 insertions(+), 3 deletions(-) create mode 100644 apps/client/src/Services/generated/switchesApi.ts create mode 100644 libs/types/definitions/internal/switches.yml create mode 100644 libs/types/src/lib/generated/switches.ts diff --git a/apps/client/src/Components/Molecules/SwitchBarList/HaSwitchBar.tsx b/apps/client/src/Components/Molecules/SwitchBarList/HaSwitchBar.tsx index ba097464..1ad376e4 100644 --- a/apps/client/src/Components/Molecules/SwitchBarList/HaSwitchBar.tsx +++ b/apps/client/src/Components/Molecules/SwitchBarList/HaSwitchBar.tsx @@ -1,5 +1,6 @@ import { HomeRemoteHaSwitch } from "@homeremote/types"; import { FC } from "react"; +import { useUpdateHaSwitchMutation } from "../../../Services/generated/switchesApi"; import SwitchBar from "./SwitchBar"; import SwitchBarInnerButton from "./SwitchBarInnerButton"; @@ -9,6 +10,7 @@ interface HaSwitchBarProps { const HaSwitchBar: FC = ({ haSwitch }) => { // Implement the HaSwitchBar component logic here + const [updateHaSwitch] = useUpdateHaSwitchMutation(); return ( = ({ haSwitch }) => { { - /* */ + updateHaSwitch({ + entityId: haSwitch.idx, + body: { state: "On" }, + }); }} icon="radio_button_checked" isActive={haSwitch.status === "On"} @@ -27,7 +32,10 @@ const HaSwitchBar: FC = ({ haSwitch }) => { { - /* */ + updateHaSwitch({ + entityId: haSwitch.idx, + body: { state: "Off" }, + }); }} icon="radio_button_unchecked" isActive={haSwitch.status === "Off"} diff --git a/apps/client/src/Services/generated/switchesApi.ts b/apps/client/src/Services/generated/switchesApi.ts new file mode 100644 index 00000000..0914617b --- /dev/null +++ b/apps/client/src/Services/generated/switchesApi.ts @@ -0,0 +1,46 @@ +import { emptySplitApi as api } from "../emptyApi"; +export const addTagTypes = [] as const; +const injectedRtkApi = api + .enhanceEndpoints({ + addTagTypes, + }) + .injectEndpoints({ + endpoints: (build) => ({ + updateHaSwitch: build.mutation< + UpdateHaSwitchApiResponse, + UpdateHaSwitchApiArg + >({ + query: (queryArg) => ({ + url: `/api/switches/ha/${queryArg.entityId}`, + method: "POST", + body: queryArg.body, + }), + }), + }), + overrideExisting: false, + }); +export { injectedRtkApi as switchesApi }; +export type UpdateHaSwitchApiResponse = + /** status 200 updateHaSwitch */ UpdateHaSwitchResponse; +export type UpdateHaSwitchApiArg = { + /** Entity ID */ + entityId: string; + body: { + /** Target state, On or Off */ + state?: "On" | "Off"; + }; +}; +export type UpdateHaSwitchResponse = string; +export type ErrorResponse = { + /** Time when error happened */ + timestamp?: string; + /** Code describing the error */ + status?: number; + /** Short error name */ + error?: string; + /** Message explaining the error */ + message?: string; + /** Code of the error */ + code?: number; +}; +export const { useUpdateHaSwitchMutation } = injectedRtkApi; diff --git a/apps/server/src/switches/switches.controller.ts b/apps/server/src/switches/switches.controller.ts index 5fd1bef5..3f2302f4 100644 --- a/apps/server/src/switches/switches.controller.ts +++ b/apps/server/src/switches/switches.controller.ts @@ -5,11 +5,15 @@ import { HomeRemoteHaSwitch, HomeRemoteSwitch, SwitchesResponse, + UpdateHaSwitchArgs, + UpdateHaSwitchResponse, } from "@homeremote/types"; import { Body, Controller, Get, + HttpException, + HttpStatus, Logger, Param, Post, @@ -264,4 +268,38 @@ export class SwitchesController { return { status: "error" }; } } + + @UseGuards(JwtAuthGuard) + @Post("/ha/:entityId") + async updateHaSwitch( + @Param("entityId") entityId: string, + @Body() args: UpdateHaSwitchArgs, + @Request() req: AuthenticatedRequest + ): Promise { + this.logger.verbose( + `[${req.user.name}] Call to /switch/ha/${entityId} state: ${args.state}` + ); + + try { + await fetch( + `${ + this.haApiConfig.baseUrl + }/api/services/light/turn_${args.state.toLowerCase()}`, + { + method: "POST", + headers: { + Authorization: `Bearer ${this.haApiConfig.token}`, + }, + body: JSON.stringify({ entity_id: entityId }), + } + ); + return "received"; + } catch (err) { + this.logger.error(`[${req.user.name}] ${err}`); + throw new HttpException( + "failed to receive downstream data", + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } } diff --git a/libs/types/definitions/internal/switches.yml b/libs/types/definitions/internal/switches.yml new file mode 100644 index 00000000..10b69423 --- /dev/null +++ b/libs/types/definitions/internal/switches.yml @@ -0,0 +1,79 @@ +# npx mock-to-openapi ./libs/types/examples +# https://editor.swagger.io/ + +openapi: 3.0.1 +info: + title: Switches Controller API + description: Switches Controller + version: 1.0.0 +servers: + - url: https://example.com + description: Generated server url +paths: + /api/switches/ha/{entity_id}: + post: + operationId: updateHaSwitch + parameters: + - name: entity_id + in: path + description: Entity ID + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + state: + type: string + description: Target state, On or Off + enum: [On, Off] + responses: + "200": + description: updateHaSwitch + content: + application/json: + schema: + $ref: "#/components/schemas/UpdateHaSwitchResponse" + "400": + description: Bad request. + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + "401": + description: Unauthorized. + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" +components: + schemas: + ErrorResponse: + type: object + properties: + timestamp: + type: string + description: Time when error happened + status: + type: integer + description: Code describing the error + format: int32 + error: + type: string + description: Short error name + message: + type: string + description: Message explaining the error + code: + type: integer + description: Code of the error + format: int32 + description: Error information details + UpdateHaSwitchResponse: + type: string + + \ No newline at end of file diff --git a/libs/types/src/lib/generated/switches.ts b/libs/types/src/lib/generated/switches.ts new file mode 100644 index 00000000..b22a4f6d --- /dev/null +++ b/libs/types/src/lib/generated/switches.ts @@ -0,0 +1,107 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + "/api/switches/ha/{entity_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["updateHaSwitch"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + /** @description Error information details */ + ErrorResponse: { + /** @description Time when error happened */ + timestamp?: string; + /** + * Format: int32 + * @description Code describing the error + */ + status?: number; + /** @description Short error name */ + error?: string; + /** @description Message explaining the error */ + message?: string; + /** + * Format: int32 + * @description Code of the error + */ + code?: number; + }; + UpdateHaSwitchResponse: string; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + updateHaSwitch: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Entity ID */ + entity_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** + * @description Target state, On or Off + * @enum {string} + */ + state?: "On" | "Off"; + }; + }; + }; + responses: { + /** @description updateHaSwitch */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UpdateHaSwitchResponse"]; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; +} diff --git a/libs/types/src/lib/switches.types.ts b/libs/types/src/lib/switches.types.ts index a1c6acf4..325d2bff 100644 --- a/libs/types/src/lib/switches.types.ts +++ b/libs/types/src/lib/switches.types.ts @@ -1,3 +1,5 @@ +import { operations } from "./generated/switches"; + export const DomoticzTypeOptions = { Dimmer: "Dimmer", Group: "Group", @@ -39,3 +41,9 @@ export interface SwitchesResponse { status: "received" | "error"; switches?: Array; } + +export type UpdateHaSwitchArgs = + operations["updateHaSwitch"]["requestBody"]["content"]["application/json"]; + +export type UpdateHaSwitchResponse = + operations["updateHaSwitch"]["responses"]["200"]["content"]["application/json"]; diff --git a/openapi-config.ts b/openapi-config.ts index c3b2c6fd..5696b3d1 100644 --- a/openapi-config.ts +++ b/openapi-config.ts @@ -11,6 +11,12 @@ const config: ConfigFile = { exportName: "energyUsageApi", tag: true, }, + "./apps/client/src/Services/generated/switchesApi.ts": { + apiFile: "./apps/client/src/Services/emptyApi.ts", + schemaFile: "./libs/types/definitions/internal/switches.yml", + exportName: "switchesApi", + tag: true, + }, }, hooks: true, }; diff --git a/redocly.yaml b/redocly.yaml index 952fb0b6..1ad39058 100644 --- a/redocly.yaml +++ b/redocly.yaml @@ -2,11 +2,15 @@ apis: domoticz@v1: root: ./libs/types/definitions/external/domoticz.yml x-openapi-ts: - output: ./libs/types/src/lib/external/generated/domoticz.ts + output: ./libs/types/src/lib/external/generated/domoticz.ts external@v1: root: ./libs/types/definitions/internal/energyUsage.yml x-openapi-ts: output: ./libs/types/src/lib/generated/energyUsage.ts + switches@v1: + root: ./libs/types/definitions/internal/switches.yml + x-openapi-ts: + output: ./libs/types/src/lib/generated/switches.ts homeAssistant@v1: root: ./libs/types/definitions/external/homeAssistant.yml x-openapi-ts: