From 0c34a02bb4986040e013e6c3619bedfed42fd542 Mon Sep 17 00:00:00 2001 From: mdvanes <4253562+mdvanes@users.noreply.github.com> Date: Fri, 13 Sep 2024 15:22:35 +0200 Subject: [PATCH] refactor --- .../Molecules/SwitchBarList/HaSwitchBar.tsx | 16 +- .../SwitchesCard/ClimateSensorsListItem.tsx | 4 +- .../Molecules/SwitchesCard/SwitchesCard.tsx | 9 +- .../SwitchesCard/SwitchesListItem.tsx | 26 ++-- .../Molecules/SwitchesCard/utils.ts | 8 +- apps/server/src/app/app.module.ts | 4 +- .../smart-entities.controller.ts | 138 ++++++++++++++++++ .../src/switches/switches.controller.ts | 68 +-------- .../definitions/external/homeAssistant.yml | 2 +- libs/types/src/index.ts | 1 + libs/types/src/lib/smart-entities.types.ts | 12 ++ libs/types/src/lib/switches.types.ts | 13 -- 12 files changed, 185 insertions(+), 116 deletions(-) create mode 100644 apps/server/src/smart-entities/smart-entities.controller.ts create mode 100644 libs/types/src/lib/smart-entities.types.ts diff --git a/apps/client/src/Components/Molecules/SwitchBarList/HaSwitchBar.tsx b/apps/client/src/Components/Molecules/SwitchBarList/HaSwitchBar.tsx index 70276d60..5f8bf9f4 100644 --- a/apps/client/src/Components/Molecules/SwitchBarList/HaSwitchBar.tsx +++ b/apps/client/src/Components/Molecules/SwitchBarList/HaSwitchBar.tsx @@ -1,6 +1,6 @@ import { HomeRemoteHaSwitch } from "@homeremote/types"; import { FC } from "react"; -import { useUpdateHaSwitchMutation } from "../../../Services/generated/switchesApi"; +import { useUpdateSmartEntityMutation } from "../../../Services/generated/smartEntitiesApi"; import { useAppDispatch } from "../../../store"; import SwitchBar from "./SwitchBar"; import SwitchBarInnerButton from "./SwitchBarInnerButton"; @@ -11,13 +11,13 @@ interface HaSwitchBarProps { } const HaSwitchBar: FC = ({ haSwitch }) => { - const [updateHaSwitch] = useUpdateHaSwitchMutation(); + const [updateHaSwitch] = useUpdateSmartEntityMutation(); const dispatch = useAppDispatch(); - const toggle = (nextState: "On" | "Off") => async () => { + const toggle = (nextState: "on" | "off") => async () => { await updateHaSwitch({ entityId: haSwitch.idx, - body: { state: nextState }, + updateSmartEntityBody: { state: nextState }, }); dispatch(getSwitches()); }; @@ -28,17 +28,17 @@ const HaSwitchBar: FC = ({ haSwitch }) => { leftButton={ } rightButton={ } icon={false} diff --git a/apps/client/src/Components/Molecules/SwitchesCard/ClimateSensorsListItem.tsx b/apps/client/src/Components/Molecules/SwitchesCard/ClimateSensorsListItem.tsx index 7425c54e..c5d9c75f 100644 --- a/apps/client/src/Components/Molecules/SwitchesCard/ClimateSensorsListItem.tsx +++ b/apps/client/src/Components/Molecules/SwitchesCard/ClimateSensorsListItem.tsx @@ -2,10 +2,10 @@ import DeviceThermostatIcon from "@mui/icons-material/DeviceThermostat"; import WaterIcon from "@mui/icons-material/Water"; import { ListItemIcon, ListItemText, Stack } from "@mui/material"; import { FC } from "react"; -import { Switch } from "../../../Services/generated/switchesApi"; +import { State } from "../../../Services/generated/smartEntitiesApi"; interface ClimateSensorsListItemProps { - sensors: Switch[]; + sensors: State[]; } export const ClimateSensorsListItem: FC = ({ diff --git a/apps/client/src/Components/Molecules/SwitchesCard/SwitchesCard.tsx b/apps/client/src/Components/Molecules/SwitchesCard/SwitchesCard.tsx index 7be57325..1b4716bf 100644 --- a/apps/client/src/Components/Molecules/SwitchesCard/SwitchesCard.tsx +++ b/apps/client/src/Components/Molecules/SwitchesCard/SwitchesCard.tsx @@ -1,6 +1,6 @@ import { List, Paper } from "@mui/material"; import { FC, useEffect, useState } from "react"; -import { useGetSwitchesQuery } from "../../../Services/generated/switchesApi"; +import { useGetSmartEntitiesQuery } from "../../../Services/generated/smartEntitiesApi"; import { getErrorMessage } from "../../../Utils/getErrorMessage"; import { useAppDispatch } from "../../../store"; import ErrorRetry from "../ErrorRetry/ErrorRetry"; @@ -17,15 +17,15 @@ export const SwitchesCard: FC = () => { const [isSkippingBecauseError, setIsSkippingBecauseError] = useState(false); const { data, error, isError, isFetching, isLoading, refetch } = - useGetSwitchesQuery(undefined, { + useGetSmartEntitiesQuery(undefined, { pollingInterval: isSkippingBecauseError ? undefined : UPDATE_INTERVAL_MS, }); - const switches = (data?.switches ?? []).filter(isSwitch); + const switches = (data?.entities ?? []).filter(isSwitch); - const climateSensors = (data?.switches ?? []) + const climateSensors = (data?.entities ?? []) .filter(isClimateSensor) .toSorted(sortClimateSensors); @@ -49,6 +49,7 @@ export const SwitchesCard: FC = () => { {switches.map((item) => ( ))} + {/* TODO extract to own card */} ); diff --git a/apps/client/src/Components/Molecules/SwitchesCard/SwitchesListItem.tsx b/apps/client/src/Components/Molecules/SwitchesCard/SwitchesListItem.tsx index 03c5fe0e..47078e6a 100644 --- a/apps/client/src/Components/Molecules/SwitchesCard/SwitchesListItem.tsx +++ b/apps/client/src/Components/Molecules/SwitchesCard/SwitchesListItem.tsx @@ -6,28 +6,28 @@ import { ListItem, ListItemText } from "@mui/material"; import { SerializedError } from "@reduxjs/toolkit"; import { FC } from "react"; import { - Switch, - useUpdateHaSwitchMutation, -} from "../../../Services/generated/switchesApi"; + State, + useUpdateSmartEntityMutation, +} from "../../../Services/generated/smartEntitiesApi"; import { getErrorMessage } from "../../../Utils/getErrorMessage"; import { useAppDispatch } from "../../../store"; import { logError } from "../LogCard/logSlice"; import { SwitchesListItemButton } from "./SwitchesListItemButton"; interface SwitchesListItemProps { - item: Switch; + item: State; } export const SwitchesListItem: FC = ({ item }) => { const dispatch = useAppDispatch(); - const [updateSwitch] = useUpdateHaSwitchMutation(); + const [updateSwitch] = useUpdateSmartEntityMutation(); - const setState = (state: "On" | "Off") => async () => { + const setState = (state: "on" | "off") => async () => { try { if (item.entity_id) { await updateSwitch({ entityId: item.entity_id, - body: { state }, + updateSmartEntityBody: { state }, }); } } catch (error) { @@ -44,7 +44,7 @@ export const SwitchesListItem: FC = ({ item }) => { return ( @@ -52,17 +52,9 @@ export const SwitchesListItem: FC = ({ item }) => { - // {item.state} - // {/* {ParentIndexNumber}x{IndexNumber}{" "} - // {SeriesName} - // {ProductionYear && ` (${ProductionYear}) `} */} - // - // } /> diff --git a/apps/client/src/Components/Molecules/SwitchesCard/utils.ts b/apps/client/src/Components/Molecules/SwitchesCard/utils.ts index 2fc5a6bd..42344552 100644 --- a/apps/client/src/Components/Molecules/SwitchesCard/utils.ts +++ b/apps/client/src/Components/Molecules/SwitchesCard/utils.ts @@ -1,13 +1,13 @@ -import { Switch } from "../../../Services/generated/switchesApi"; +import { State } from "../../../Services/generated/smartEntitiesApi"; // device_class is undefined for switches -export const isSwitch = (s: Switch) => +export const isSwitch = (s: State) => typeof s?.attributes?.device_class === "undefined"; -export const isClimateSensor = (s: Switch) => +export const isClimateSensor = (s: State) => ["temperature", "humidity"].includes(s?.attributes?.device_class ?? ""); -export const sortClimateSensors = (a: Switch, b: Switch) => { +export const sortClimateSensors = (a: State, b: State) => { const aName = a.attributes?.friendly_name ?? ""; const bName = b.attributes?.friendly_name ?? ""; const aClass = a.attributes?.device_class ?? ""; diff --git a/apps/server/src/app/app.module.ts b/apps/server/src/app/app.module.ts index 056f2a2d..c2b4e5e2 100644 --- a/apps/server/src/app/app.module.ts +++ b/apps/server/src/app/app.module.ts @@ -19,6 +19,7 @@ import { ProfileController } from "../profile/profile.controller"; import { PwToHashController } from "../pw-to-hash/pw-to-hash.controller"; import { ScheduleController } from "../schedule/schedule.controller"; import { ServiceLinksController } from "../service-links/service-links.controller"; +import { SmartEntitiesController } from "../smart-entities/smart-entities.controller"; import { StacksController } from "../stacks/stacks.controller"; import { StatusController } from "../status/status.controller"; import { SwitchesController } from "../switches/switches.controller"; @@ -65,8 +66,9 @@ import { AppService } from "./app.service"; PwToHashController, ScheduleController, ServiceLinksController, - StatusController, + SmartEntitiesController, StacksController, + StatusController, SwitchesController, UrltomusicController, VideoStreamController, diff --git a/apps/server/src/smart-entities/smart-entities.controller.ts b/apps/server/src/smart-entities/smart-entities.controller.ts new file mode 100644 index 00000000..bf36c22b --- /dev/null +++ b/apps/server/src/smart-entities/smart-entities.controller.ts @@ -0,0 +1,138 @@ +import { + GetHaStatesResponse, + State, + type SmartEntitiesTypes, +} from "@homeremote/types"; +import { + Body, + Controller, + Get, + HttpException, + HttpStatus, + Logger, + Param, + Post, + Request, + UseGuards, +} from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; +import { JwtAuthGuard } from "../auth/jwt-auth.guard"; +import { AuthenticatedRequest } from "../login/LoginRequest.types"; + +@Controller("api/smart-entities") +export class SmartEntitiesController { + private readonly logger: Logger; + + private readonly haApiConfig: { + baseUrl: string; + token: string; + entityId: string; + }; + + constructor(private configService: ConfigService) { + this.logger = new Logger(SmartEntitiesController.name); + + this.haApiConfig = { + baseUrl: + this.configService.get("HOMEASSISTANT_BASE_URL") || "", + token: this.configService.get("HOMEASSISTANT_TOKEN") || "", + entityId: + this.configService.get("HOMEASSISTANT_SWITCHES_ID") || + "", + }; + } + + async fetchHa(path: string): Promise { + const response = await fetch(`${this.haApiConfig.baseUrl}${path}`, { + headers: { + Authorization: `Bearer ${this.haApiConfig.token}`, + }, + }); + return response.json(); + } + + async fetchHaEntityState( + entityId: string + ): Promise { + return this.fetchHa(`/api/states/${entityId}`); + } + + @UseGuards(JwtAuthGuard) + @Get() + async getSmartEntities( + @Request() req: AuthenticatedRequest + ): Promise { + this.logger.verbose( + `[${req.user.name}] GET to /smart-entities for entityId ${this.haApiConfig.entityId}` + ); + + try { + const switchIds: GetHaStatesResponse = await this.fetchHa( + `/api/states/${this.haApiConfig.entityId}` + ); + console.log("switchIds", switchIds); + + if ("message" in switchIds && switchIds.message) { + throw new Error(switchIds.message); + } + + if ("attributes" in switchIds) { + const allHaStates = (await this.fetchHa( + `/api/states` + )) as State[]; + const switchStates = allHaStates.filter((state) => + switchIds.attributes.entity_id.includes(state.entity_id) + ); + + return { + switches: switchStates, + } as SmartEntitiesTypes.GetSmartEntitiesResponse; + } + + throw new Error("no attributes property"); + } catch (err) { + this.logger.error(`[${req.user.name}] ${err}`); + throw new HttpException( + "failed to receive downstream data", + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } + + @UseGuards(JwtAuthGuard) + @Post("/:entityId") + async updateSmartEntity( + @Param("entityId") entityId: string, + @Body() args: SmartEntitiesTypes.UpdateSmartEntityArgs, + @Request() req: AuthenticatedRequest + ): Promise { + this.logger.verbose( + `[${req.user.name}] POST to /smart-entities/${entityId} with state: ${args.state}` + ); + + try { + const [entityType] = entityId.split("."); + const pathType = entityType === "light" ? "light" : "switch"; + // TODO use helper and add Arg/Response types + await fetch( + `${ + this.haApiConfig.baseUrl + }/api/services/${pathType}/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/apps/server/src/switches/switches.controller.ts b/apps/server/src/switches/switches.controller.ts index 571346af..35679042 100644 --- a/apps/server/src/switches/switches.controller.ts +++ b/apps/server/src/switches/switches.controller.ts @@ -2,13 +2,9 @@ import { DomoticzStatus, DomoticzType, GetHaStatesResponse, - GetSwitchesResponse, HomeRemoteHaSwitch, HomeRemoteSwitch, - State, SwitchesResponse, - UpdateHaSwitchArgs, - UpdateHaSwitchResponse, } from "@homeremote/types"; import { Body, @@ -132,13 +128,6 @@ interface UpdateSwitchMessage { type: string; } -// const haEntityToSwitch = (haEntity: GetHaStatesResponse): Switch => ({ -// ...haEntity, -// state: ["on", "off"].includes(haEntity.state) -// ? (haEntity.state as "on" | "off") -// : undefined, -// }); - @Controller("api/switches") export class SwitchesController { private readonly logger: Logger; @@ -223,59 +212,6 @@ export class SwitchesController { } }; - async fetchHa(path: string): Promise { - const response = await fetch(`${this.haApiConfig.baseUrl}${path}`, { - headers: { - Authorization: `Bearer ${this.haApiConfig.token}`, - }, - }); - return response.json(); - } - - async fetchHaEntityState(entityId: string): Promise { - return this.fetchHa(`/api/states/${entityId}`); - } - - @UseGuards(JwtAuthGuard) - @Get("/ha") - async getSwitchesHa( - @Request() req: AuthenticatedRequest - ): Promise { - this.logger.verbose( - `[${req.user.name}] GET to /api/switches/ha for entityId ${this.haApiConfig.entityId}` - ); - - try { - const switchIds: GetHaStatesResponse = await this.fetchHa( - `/api/states/${this.haApiConfig.entityId}` - ); - console.log("switchIds", switchIds); - - if ("message" in switchIds && switchIds.message) { - throw new Error(switchIds.message); - } - - if ("attributes" in switchIds) { - const allHaStates = (await this.fetchHa( - `/api/states` - )) as State[]; - const switchStates = allHaStates.filter((state) => - switchIds.attributes.entity_id.includes(state.entity_id) - ); - - return { switches: switchStates } as GetSwitchesResponse; - } - - throw new Error("no attributes property"); - } catch (err) { - this.logger.error(`[${req.user.name}] ${err}`); - throw new HttpException( - "failed to receive downstream data", - HttpStatus.INTERNAL_SERVER_ERROR - ); - } - } - @UseGuards(JwtAuthGuard) @Get() /** @deprecated */ @@ -361,9 +297,9 @@ export class SwitchesController { @Post("/ha/:entityId") async updateHaSwitch( @Param("entityId") entityId: string, - @Body() args: UpdateHaSwitchArgs, + @Body() args: any, @Request() req: AuthenticatedRequest - ): Promise { + ): Promise { this.logger.verbose( `[${req.user.name}] Call to /switch/ha/${entityId} state: ${args.state}` ); diff --git a/libs/types/definitions/external/homeAssistant.yml b/libs/types/definitions/external/homeAssistant.yml index 547d5128..6d55365b 100644 --- a/libs/types/definitions/external/homeAssistant.yml +++ b/libs/types/definitions/external/homeAssistant.yml @@ -63,7 +63,7 @@ paths: type: string responses: "200": - description: HaStates + description: State content: application/json: schema: diff --git a/libs/types/src/index.ts b/libs/types/src/index.ts index 152eeb82..18ecfd85 100644 --- a/libs/types/src/index.ts +++ b/libs/types/src/index.ts @@ -14,6 +14,7 @@ export * from "./lib/nextup.types"; export * from "./lib/nowplaying.types"; export * from "./lib/schedule.types"; export * from "./lib/servicelinks.types"; +export * as SmartEntitiesTypes from "./lib/smart-entities.types"; export * from "./lib/stacks.types"; export * from "./lib/switches.types"; export * from "./lib/urltomusic.types"; diff --git a/libs/types/src/lib/smart-entities.types.ts b/libs/types/src/lib/smart-entities.types.ts new file mode 100644 index 00000000..a82f0c11 --- /dev/null +++ b/libs/types/src/lib/smart-entities.types.ts @@ -0,0 +1,12 @@ +import { components, operations } from "./generated/smartEntities"; + +export type UpdateSmartEntityArgs = + operations["updateSmartEntity"]["requestBody"]["content"]["application/json"]; + +export type UpdateSmartEntityResponse = + operations["updateSmartEntity"]["responses"]["200"]["content"]["application/json"]; + +export type GetSmartEntitiesResponse = + operations["getSmartEntities"]["responses"]["200"]["content"]["application/json"]; + +export type State = components["schemas"]["State"]; diff --git a/libs/types/src/lib/switches.types.ts b/libs/types/src/lib/switches.types.ts index 6a5f75fa..a1c6acf4 100644 --- a/libs/types/src/lib/switches.types.ts +++ b/libs/types/src/lib/switches.types.ts @@ -1,5 +1,3 @@ -import { components, operations } from "./generated/switches"; - export const DomoticzTypeOptions = { Dimmer: "Dimmer", Group: "Group", @@ -41,14 +39,3 @@ 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"]; - -export type GetSwitchesResponse = - operations["getSwitches"]["responses"]["200"]["content"]["application/json"]; - -export type Switch = components["schemas"]["Switch"];