Skip to content

Commit

Permalink
refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
mdvanes committed Sep 13, 2024
1 parent e961a05 commit 0c34a02
Show file tree
Hide file tree
Showing 12 changed files with 185 additions and 116 deletions.
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -11,13 +11,13 @@ interface HaSwitchBarProps {
}

const HaSwitchBar: FC<HaSwitchBarProps> = ({ 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());
};
Expand All @@ -28,17 +28,17 @@ const HaSwitchBar: FC<HaSwitchBarProps> = ({ haSwitch }) => {
leftButton={
<SwitchBarInnerButton
isReadOnly={false}
clickAction={toggle("On")}
clickAction={toggle("on")}
icon="radio_button_checked"
isActive={haSwitch.status === "On"}
isActive={haSwitch.status === "on"}
/>
}
rightButton={
<SwitchBarInnerButton
isReadOnly={false}
clickAction={toggle("Off")}
clickAction={toggle("off")}
icon="radio_button_unchecked"
isActive={haSwitch.status === "Off"}
isActive={haSwitch.status === "off"}
/>
}
icon={false}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ClimateSensorsListItemProps> = ({
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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);

Expand All @@ -49,6 +49,7 @@ export const SwitchesCard: FC = () => {
{switches.map((item) => (
<SwitchesListItem key={item.entity_id} item={item} />
))}
{/* TODO extract to own card */}
<ClimateSensorsListItem sensors={climateSensors} />
</List>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<SwitchesListItemProps> = ({ 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) {
Expand All @@ -44,25 +44,17 @@ export const SwitchesListItem: FC<SwitchesListItemProps> = ({ item }) => {
return (
<ListItem disableGutters disablePadding>
<SwitchesListItemButton
onClick={setState("On")}
onClick={setState("on")}
selected={item.state === "on"}
>
<RadioButtonCheckedIcon />
</SwitchesListItemButton>
<ListItemText
sx={{ flex: 1, paddingX: 1 }}
primary={item.attributes?.friendly_name}
// secondary={
// <>
// {item.state}
// {/* {ParentIndexNumber}x{IndexNumber}{" "}
// <strong>{SeriesName} </strong>
// {ProductionYear && ` (${ProductionYear}) `} */}
// </>
// }
/>
<SwitchesListItemButton
onClick={setState("Off")}
onClick={setState("off")}
selected={item.state === "off"}
>
<RadioButtonUncheckedIcon />
Expand Down
8 changes: 4 additions & 4 deletions apps/client/src/Components/Molecules/SwitchesCard/utils.ts
Original file line number Diff line number Diff line change
@@ -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 ?? "";
Expand Down
4 changes: 3 additions & 1 deletion apps/server/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -65,8 +66,9 @@ import { AppService } from "./app.service";
PwToHashController,
ScheduleController,
ServiceLinksController,
StatusController,
SmartEntitiesController,
StacksController,
StatusController,
SwitchesController,
UrltomusicController,
VideoStreamController,
Expand Down
138 changes: 138 additions & 0 deletions apps/server/src/smart-entities/smart-entities.controller.ts
Original file line number Diff line number Diff line change
@@ -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<string>("HOMEASSISTANT_BASE_URL") || "",
token: this.configService.get<string>("HOMEASSISTANT_TOKEN") || "",
entityId:
this.configService.get<string>("HOMEASSISTANT_SWITCHES_ID") ||
"",
};
}

async fetchHa(path: string): Promise<unknown> {
const response = await fetch(`${this.haApiConfig.baseUrl}${path}`, {
headers: {
Authorization: `Bearer ${this.haApiConfig.token}`,
},
});
return response.json();
}

async fetchHaEntityState(
entityId: string
): Promise<SmartEntitiesTypes.GetSmartEntitiesResponse> {
return this.fetchHa(`/api/states/${entityId}`);
}

@UseGuards(JwtAuthGuard)
@Get()
async getSmartEntities(
@Request() req: AuthenticatedRequest
): Promise<SmartEntitiesTypes.GetSmartEntitiesResponse> {
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<SmartEntitiesTypes.UpdateSmartEntityResponse> {
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
);
}
}
}
Loading

0 comments on commit 0c34a02

Please sign in to comment.