From 4bdaea61caad19a6798a5c2e4f4ac867c22c7711 Mon Sep 17 00:00:00 2001 From: mdvanes <4253562+mdvanes@users.noreply.github.com> Date: Mon, 15 Apr 2024 12:59:47 +0200 Subject: [PATCH 01/18] fix: temperature chart range --- .../src/energyusage/energyusage.controller.ts | 51 ++++++++++--------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/apps/server/src/energyusage/energyusage.controller.ts b/apps/server/src/energyusage/energyusage.controller.ts index aa8474e5..79ea709c 100644 --- a/apps/server/src/energyusage/energyusage.controller.ts +++ b/apps/server/src/energyusage/energyusage.controller.ts @@ -40,47 +40,37 @@ const strToConfigs = (sensorConfig: string): SensorConfig[] => { }; const temperatureResponseToEntry = - (temperatureSensors: SensorConfig[], index: number) => + (temperatureSensors: SensorConfig[], date: string) => (response: GotTempResponse, temperatureIndex: number) => { const sensor = temperatureSensors[temperatureIndex]; - const temperatureEntry = response.result[index]; - if (!temperatureEntry) { - return [ - sensor.name, - { - avg: undefined, - high: undefined, - low: undefined, - }, - ]; - } - if (temperatureEntry.d !== temperatureEntry.d) { - throw new Error("days do not match"); - } + const temperatureEntry = response.result.find((r) => r.d === date); return [ sensor.name, { - avg: temperatureEntry.ta, - high: temperatureEntry.te, - low: temperatureEntry.tm, + avg: temperatureEntry?.ta ?? "", + high: temperatureEntry?.te ?? "", + low: temperatureEntry?.tm ?? "", }, ]; }; const sensorResultsToAggregated = ( + gasEntries: GasUsageItem[], temperatureSensors: SensorConfig[], temperatureResponses: GotTempResponse[] ) => - (gasEntry: GasUsageItem, index: number) => { + (date: string) => { const temperatureEntries = temperatureResponses.map( - temperatureResponseToEntry(temperatureSensors, index) + temperatureResponseToEntry(temperatureSensors, date) ); + const gasEntry = gasEntries.find((r) => r.d === date); const result: EnergyUsageGasItem = { - counter: gasEntry.c, - used: gasEntry.v, - day: gasEntry.d, + day: date, + counter: gasEntry?.c ?? "", + used: gasEntry?.v ?? "", + temp: Object.fromEntries(temperatureEntries), }; return result; @@ -133,10 +123,23 @@ export class EnergyUsageController { temperaturePromises ); + const now = Date.now(); + const NR_OF_DAYS = 31; + const lastMonth = new Date(now - 1000 * 60 * 60 * 24 * NR_OF_DAYS); + const dateRange = new Array(NR_OF_DAYS) + .fill(0) + .map((n, index) => + new Date( + lastMonth.getTime() + 1000 * 60 * 60 * 24 * (index + 1) + ) + .toISOString() + .slice(0, 10) + ); const aggregated: EnergyUsageGetGasUsageResponse = { ...gasCounterResponse, - result: gasCounterResponse.result.map( + result: dateRange.map( sensorResultsToAggregated( + gasCounterResponse.result, temperatureSensors, temperatureResponses ) From 3c41170b157b63ce9afc36ca0467951916d51a7d Mon Sep 17 00:00:00 2001 From: mdvanes <4253562+mdvanes@users.noreply.github.com> Date: Mon, 15 Apr 2024 13:08:19 +0200 Subject: [PATCH 02/18] chore: show homesec items --- .../Components/Molecules/HomeSec/HomeSec.tsx | 21 ++++++ .../Components/Pages/Dashboard/Dashboard.tsx | 2 + apps/client/src/Reducers/index.ts | 2 + apps/client/src/Services/homesecApi.ts | 18 +++++ apps/client/src/store.ts | 2 + apps/server/.env.example | 5 +- apps/server/src/app/app.module.ts | 2 + apps/server/src/homesec/homesec.controller.ts | 68 +++++++++++++++++++ libs/types/src/index.ts | 1 + libs/types/src/lib/homesec.types.ts | 57 ++++++++++++++++ 10 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 apps/client/src/Components/Molecules/HomeSec/HomeSec.tsx create mode 100644 apps/client/src/Services/homesecApi.ts create mode 100644 apps/server/src/homesec/homesec.controller.ts create mode 100644 libs/types/src/lib/homesec.types.ts diff --git a/apps/client/src/Components/Molecules/HomeSec/HomeSec.tsx b/apps/client/src/Components/Molecules/HomeSec/HomeSec.tsx new file mode 100644 index 00000000..8904bbad --- /dev/null +++ b/apps/client/src/Components/Molecules/HomeSec/HomeSec.tsx @@ -0,0 +1,21 @@ +import { FC } from "react"; +import { useGetHomesecDeviceListQuery } from "../../../Services/homesecApi"; + +export const HomeSec: FC = () => { + // TODO handle error/loading + const { data: devices } = useGetHomesecDeviceListQuery(undefined); + + console.log(devices); + + return ( +
+ {devices?.senrows.map((sensor) => ( +
+ {sensor.name}: {sensor.status} +
+ ))} +
+ ); +}; + +export default HomeSec; diff --git a/apps/client/src/Components/Pages/Dashboard/Dashboard.tsx b/apps/client/src/Components/Pages/Dashboard/Dashboard.tsx index 1f0e89ce..341f7628 100644 --- a/apps/client/src/Components/Pages/Dashboard/Dashboard.tsx +++ b/apps/client/src/Components/Pages/Dashboard/Dashboard.tsx @@ -15,6 +15,7 @@ import StreamContainer from "../../Molecules/StreamContainer/StreamContainer"; import SwitchBarList from "../../Molecules/SwitchBarList/SwitchBarList"; import UrlToMusic from "../../Molecules/UrlToMusic/UrlToMusic"; import VideoStream from "../../Molecules/VideoStream/VideoStream"; +import HomeSec from "../../Molecules/HomeSec/HomeSec"; import Docker from "../Docker/Docker"; const useStyles = makeStyles()((theme) => ({ @@ -43,6 +44,7 @@ const Dashboard: FC = () => { + diff --git a/apps/client/src/Reducers/index.ts b/apps/client/src/Reducers/index.ts index e51f174d..7d8c8fea 100644 --- a/apps/client/src/Reducers/index.ts +++ b/apps/client/src/Reducers/index.ts @@ -10,6 +10,7 @@ import { dataloraApi } from "../Services/dataloraApi"; import { dockerListApi } from "../Services/dockerListApi"; import { downloadListApi } from "../Services/downloadListApi"; import { energyUsageApi } from "../Services/energyUsageApi"; +import { homesecApi } from "../Services/homesecApi"; import { jukeboxApi } from "../Services/jukeboxApi"; import { monitApi } from "../Services/monitApi"; import { nextupApi } from "../Services/nextupApi"; @@ -30,6 +31,7 @@ const rootReducer = combineReducers({ [dockerListApi.reducerPath]: dockerListApi.reducer, [downloadListApi.reducerPath]: downloadListApi.reducer, [energyUsageApi.reducerPath]: energyUsageApi.reducer, + [homesecApi.reducerPath]: homesecApi.reducer, [jukeboxApi.reducerPath]: jukeboxApi.reducer, [monitApi.reducerPath]: monitApi.reducer, [nextupApi.reducerPath]: nextupApi.reducer, diff --git a/apps/client/src/Services/homesecApi.ts b/apps/client/src/Services/homesecApi.ts new file mode 100644 index 00000000..e32a1d0e --- /dev/null +++ b/apps/client/src/Services/homesecApi.ts @@ -0,0 +1,18 @@ +import { HomesecDevicesResponse } from "@homeremote/types"; +import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/dist/query/react"; +import { willAddCredentials } from "../devUtils"; + +export const homesecApi = createApi({ + reducerPath: "homesecApi", + baseQuery: fetchBaseQuery({ + baseUrl: `${process.env.NX_BASE_URL}/api/homesec`, + credentials: willAddCredentials(), + }), + endpoints: (builder) => ({ + getHomesecDeviceList: builder.query({ + query: () => "/devices", + }), + }), +}); + +export const { useGetHomesecDeviceListQuery } = homesecApi; diff --git a/apps/client/src/store.ts b/apps/client/src/store.ts index b5fb8252..bfcbb4ed 100644 --- a/apps/client/src/store.ts +++ b/apps/client/src/store.ts @@ -8,6 +8,7 @@ import { dataloraApi } from "./Services/dataloraApi"; import { dockerListApi } from "./Services/dockerListApi"; import { downloadListApi } from "./Services/downloadListApi"; import { energyUsageApi } from "./Services/energyUsageApi"; +import { homesecApi } from "./Services/homesecApi"; import { jukeboxApi } from "./Services/jukeboxApi"; import { monitApi } from "./Services/monitApi"; import { nextupApi } from "./Services/nextupApi"; @@ -26,6 +27,7 @@ export const store = configureStore({ dockerListApi.middleware, downloadListApi.middleware, energyUsageApi.middleware, + homesecApi.middleware, jukeboxApi.middleware, monitApi.middleware, nextupApi.middleware, diff --git a/apps/server/.env.example b/apps/server/.env.example index 98411fae..b5488853 100644 --- a/apps/server/.env.example +++ b/apps/server/.env.example @@ -27,4 +27,7 @@ NEXTUP_USER_ID=123 CARTWIN_VIN=123 CARTWIN_VCC_API_KEY=123 VIDEO_STREAM_HASH=123 -VIDEO_STREAM_URL=http://localhost:52998/ \ No newline at end of file +VIDEO_STREAM_URL=http://localhost:52998/ +HOMESEC_BASE_URL=http://192.168.0.123/ +HOMESEC_USERNAME=user +HOMESEC_PASSWORD=pass \ No newline at end of file diff --git a/apps/server/src/app/app.module.ts b/apps/server/src/app/app.module.ts index 69f5be24..1dec97d2 100644 --- a/apps/server/src/app/app.module.ts +++ b/apps/server/src/app/app.module.ts @@ -8,6 +8,7 @@ import { DataloraController } from "../datalora/datalora.controller"; import { DockerlistController } from "../dockerlist/dockerlist.controller"; import { DownloadlistController } from "../downloadlist/downloadlist.controller"; import { EnergyUsageController } from "../energyusage/energyusage.controller"; +import { HomesecController } from "../homesec/homesec.controller"; import { JukeboxController } from "../jukebox/jukebox.controller"; import { LoginController } from "../login/login.controller"; import { LogoutController } from "../logout/logout.controller"; @@ -52,6 +53,7 @@ import { AppService } from "./app.service"; DockerlistController, DownloadlistController, EnergyUsageController, + HomesecController, JukeboxController, LoginController, LogoutController, diff --git a/apps/server/src/homesec/homesec.controller.ts b/apps/server/src/homesec/homesec.controller.ts new file mode 100644 index 00000000..f6afb0ff --- /dev/null +++ b/apps/server/src/homesec/homesec.controller.ts @@ -0,0 +1,68 @@ +import { + Controller, + Get, + HttpException, + HttpStatus, + Logger, + Request, + UseGuards, +} from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; +import { JwtAuthGuard } from "../auth/jwt-auth.guard"; +import { AuthenticatedRequest } from "../login/LoginRequest.types"; +import got from "got"; +import { + HomesecDevicesResponse, + HomesecPanelResponse, +} from "@homeremote/types"; + +@Controller("api/homesec") +export class HomesecController { + private readonly logger: Logger; + private readonly baseUrl: string; + private readonly username: string; + private readonly password: string; + + constructor(private configService: ConfigService) { + this.logger = new Logger(HomesecController.name); + this.baseUrl = this.configService.get("HOMESEC_BASE_URL") || ""; + this.username = + this.configService.get("HOMESEC_USERNAME") || ""; + this.password = + this.configService.get("HOMESEC_PASSWORD") || ""; + } + + @UseGuards(JwtAuthGuard) + @Get("devices") + async getDevices( + @Request() req: AuthenticatedRequest + ): Promise { + this.logger.verbose(`[${req.user.name}] GET to /api/homesec/devices`); + + try { + const url = `${this.baseUrl}/action/deviceListGet`; + + const response1: HomesecPanelResponse = await got( + `${this.baseUrl}/action/panelCondGet`, + { + username: this.username, + password: this.password, + } + ).json(); + this.logger.log(response1); + + const response: HomesecDevicesResponse = await got(url, { + username: this.username, + password: this.password, + }).json(); + // TODO this.logger.log(response); + return response; + } catch (err) { + this.logger.error(err); + throw new HttpException( + "failed to receive downstream data", + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } +} diff --git a/libs/types/src/index.ts b/libs/types/src/index.ts index 33c9a510..85ea9af7 100644 --- a/libs/types/src/index.ts +++ b/libs/types/src/index.ts @@ -3,6 +3,7 @@ export * from "./lib/datalora.types"; export * from "./lib/dockerlist.types"; export * from "./lib/downloadlist.types"; export * from "./lib/energyusage.types"; +export * from "./lib/homesec.types"; export * from "./lib/jukebox.types"; export * from "./lib/monit.types"; export * from "./lib/nextup.types"; diff --git a/libs/types/src/lib/homesec.types.ts b/libs/types/src/lib/homesec.types.ts new file mode 100644 index 00000000..c41608eb --- /dev/null +++ b/libs/types/src/lib/homesec.types.ts @@ -0,0 +1,57 @@ +interface SensorRow { + area: number; + zone: number; + type: number; + type_f: + | "Door Contact" + | "Smoke Detector" + | "Keypad" + | "IR" + | "Remote Controller"; + name: string; + cond: ""; + cond_ok: "0" | "1"; + battery: ""; + battery_ok: "0" | "1"; + tamper: ""; + tamper_ok: "0" | "1"; + bypass: "No" | "Yes"; + temp_bypass: "0" | "1"; + rssi: string; // "Strong, 9"; + status: "" | "Door Close" | "Door Open"; + id: string; + su: number; +} + +export interface HomesecDevicesResponse { + senrows: SensorRow[]; +} + +export interface HomesecPanelResponse { + updates: { + mode_a1: "Disarm"; + mode_a2: "Disarm"; + battery_ok: "1"; + battery: "Normal"; + tamper_ok: "1"; + tamper: "N/A"; + interference_ok: "1"; + interference: "Normal"; + ac_activation_ok: "1"; + ac_activation: "Normal"; + sys_in_inst: "System in maintenance"; + rssi: "1"; + sig_gsm_ok: "1"; + sig_gsm: "N/A"; + }; + forms: { + pcondform1: { + mode: "0"; + f_arm: "0"; + }; + pcondform2: { + mode: "0"; + f_arm: "0"; + }; + }; +} From 2df4cb7019cc66a44cd762b58c77ae82bb15174e Mon Sep 17 00:00:00 2001 From: mdvanes <4253562+mdvanes@users.noreply.github.com> Date: Mon, 15 Apr 2024 13:34:03 +0200 Subject: [PATCH 03/18] chore: show homesec status and devices --- .../Components/Molecules/HomeSec/HomeSec.tsx | 11 ++--- apps/client/src/Services/homesecApi.ts | 8 ++-- apps/server/src/homesec/homesec.controller.ts | 45 ++++++++++++++----- libs/types/src/lib/homesec.types.ts | 20 +++++++-- 4 files changed, 59 insertions(+), 25 deletions(-) diff --git a/apps/client/src/Components/Molecules/HomeSec/HomeSec.tsx b/apps/client/src/Components/Molecules/HomeSec/HomeSec.tsx index 8904bbad..cc99860f 100644 --- a/apps/client/src/Components/Molecules/HomeSec/HomeSec.tsx +++ b/apps/client/src/Components/Molecules/HomeSec/HomeSec.tsx @@ -1,17 +1,18 @@ import { FC } from "react"; -import { useGetHomesecDeviceListQuery } from "../../../Services/homesecApi"; +import { useGetHomesecStatusQuery } from "../../../Services/homesecApi"; export const HomeSec: FC = () => { // TODO handle error/loading - const { data: devices } = useGetHomesecDeviceListQuery(undefined); + const { data } = useGetHomesecStatusQuery(undefined); - console.log(devices); + console.log(data?.status); return (
- {devices?.senrows.map((sensor) => ( + {data?.status} + {data?.devices?.map((sensor) => (
- {sensor.name}: {sensor.status} + {sensor.name}: {sensor.status} {sensor.type_f} {sensor.rssi}
))}
diff --git a/apps/client/src/Services/homesecApi.ts b/apps/client/src/Services/homesecApi.ts index e32a1d0e..1007f279 100644 --- a/apps/client/src/Services/homesecApi.ts +++ b/apps/client/src/Services/homesecApi.ts @@ -1,4 +1,4 @@ -import { HomesecDevicesResponse } from "@homeremote/types"; +import { HomesecStatusResponse } from "@homeremote/types"; import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/dist/query/react"; import { willAddCredentials } from "../devUtils"; @@ -9,10 +9,10 @@ export const homesecApi = createApi({ credentials: willAddCredentials(), }), endpoints: (builder) => ({ - getHomesecDeviceList: builder.query({ - query: () => "/devices", + getHomesecStatus: builder.query({ + query: () => "/status", }), }), }); -export const { useGetHomesecDeviceListQuery } = homesecApi; +export const { useGetHomesecStatusQuery } = homesecApi; diff --git a/apps/server/src/homesec/homesec.controller.ts b/apps/server/src/homesec/homesec.controller.ts index f6afb0ff..88a7600f 100644 --- a/apps/server/src/homesec/homesec.controller.ts +++ b/apps/server/src/homesec/homesec.controller.ts @@ -14,6 +14,7 @@ import got from "got"; import { HomesecDevicesResponse, HomesecPanelResponse, + HomesecStatusResponse, } from "@homeremote/types"; @Controller("api/homesec") @@ -33,30 +34,50 @@ export class HomesecController { } @UseGuards(JwtAuthGuard) - @Get("devices") + @Get("status") async getDevices( @Request() req: AuthenticatedRequest - ): Promise { + ): Promise { this.logger.verbose(`[${req.user.name}] GET to /api/homesec/devices`); try { - const url = `${this.baseUrl}/action/deviceListGet`; - - const response1: HomesecPanelResponse = await got( + const panelResponse: HomesecPanelResponse = await got( `${this.baseUrl}/action/panelCondGet`, { username: this.username, password: this.password, } ).json(); - this.logger.log(response1); + this.logger.log(panelResponse); - const response: HomesecDevicesResponse = await got(url, { - username: this.username, - password: this.password, - }).json(); - // TODO this.logger.log(response); - return response; + try { + const devicesResponse: HomesecDevicesResponse = await got( + `${this.baseUrl}/action/deviceListGet`, + { + username: this.username, + password: this.password, + } + ).json(); + this.logger.log(devicesResponse); + return { + status: panelResponse.updates.mode_a1, + devices: devicesResponse.senrows.map( + ({ id, name, type_f, status, rssi }) => ({ + id, + name, + type_f, + status, + rssi, + }) + ), + }; + } catch (err) { + this.logger.error("deviceListGet:", err); + return { + status: panelResponse.updates.mode_a1, + devices: [], + }; + } } catch (err) { this.logger.error(err); throw new HttpException( diff --git a/libs/types/src/lib/homesec.types.ts b/libs/types/src/lib/homesec.types.ts index c41608eb..ae8f7617 100644 --- a/libs/types/src/lib/homesec.types.ts +++ b/libs/types/src/lib/homesec.types.ts @@ -27,10 +27,17 @@ export interface HomesecDevicesResponse { senrows: SensorRow[]; } +type Modes = "Disarm" | "Home Arm 1"; + +export enum PcondformModes { + Disarm = "0", + HomeArm = "2", +} + export interface HomesecPanelResponse { updates: { - mode_a1: "Disarm"; - mode_a2: "Disarm"; + mode_a1: Modes; + mode_a2: Modes; battery_ok: "1"; battery: "Normal"; tamper_ok: "1"; @@ -46,12 +53,17 @@ export interface HomesecPanelResponse { }; forms: { pcondform1: { - mode: "0"; + mode: PcondformModes; f_arm: "0"; }; pcondform2: { - mode: "0"; + mode: PcondformModes; f_arm: "0"; }; }; } + +export interface HomesecStatusResponse { + status: Modes; + devices: Pick[]; +} From 8c0ee981c49d3ddbbf5f43b2f523de1fd265f5a6 Mon Sep 17 00:00:00 2001 From: mdvanes <4253562+mdvanes@users.noreply.github.com> Date: Mon, 15 Apr 2024 14:18:19 +0200 Subject: [PATCH 04/18] chore: restyle homesec --- .../Components/Molecules/HomeSec/HomeSec.tsx | 92 +++++++++++++++++-- .../HomeSec/SimpleHomeSecListItem.tsx | 34 +++++++ apps/server/src/homesec/homesec.controller.ts | 5 +- libs/types/src/lib/homesec.types.ts | 14 +-- 4 files changed, 127 insertions(+), 18 deletions(-) create mode 100644 apps/client/src/Components/Molecules/HomeSec/SimpleHomeSecListItem.tsx diff --git a/apps/client/src/Components/Molecules/HomeSec/HomeSec.tsx b/apps/client/src/Components/Molecules/HomeSec/HomeSec.tsx index cc99860f..2302c237 100644 --- a/apps/client/src/Components/Molecules/HomeSec/HomeSec.tsx +++ b/apps/client/src/Components/Molecules/HomeSec/HomeSec.tsx @@ -1,21 +1,93 @@ import { FC } from "react"; import { useGetHomesecStatusQuery } from "../../../Services/homesecApi"; +import { + List, + ListItem, + ListItemAvatar, + ListItemButton, + ListItemText, + Paper, + Icon, +} from "@mui/material"; +import { HomesecStatusResponse, TypeF } from "@homeremote/types"; +import LoadingDot from "../LoadingDot/LoadingDot"; +import SimpleHomeSecListItem from "./SimpleHomeSecListItem"; -export const HomeSec: FC = () => { - // TODO handle error/loading - const { data } = useGetHomesecStatusQuery(undefined); +const statusClass: Record = { + Error: "black", + Disarm: "green", + "Home Arm 1": "yellow", +}; - console.log(data?.status); +const typeIcon: Record = { + "Door Contact": "sensor_door", + "Smoke Detector": "smoking_rooms", + Keypad: "keyboard", + IR: "animation", + "Remote Controller": "settings_remote", +}; + +export const HomeSec: FC = () => { + const { data, isLoading, isFetching, isError, refetch } = + useGetHomesecStatusQuery(undefined); return ( -
- {data?.status} + + + {isError ? ( + refetch()} + /> + ) : ( + "" + )} + {!data?.devices || data?.devices.length === 0 ? ( + refetch()} + /> + ) : ( + "" + )} {data?.devices?.map((sensor) => ( -
- {sensor.name}: {sensor.status} {sensor.type_f} {sensor.rssi} -
+ + + + {typeIcon[sensor.type_f]} + + +
{sensor.name}
+
+
{sensor.status}
+
{sensor.rssi}
+
+
+ } + /> + + ))} - + ); }; diff --git a/apps/client/src/Components/Molecules/HomeSec/SimpleHomeSecListItem.tsx b/apps/client/src/Components/Molecules/HomeSec/SimpleHomeSecListItem.tsx new file mode 100644 index 00000000..c56e7346 --- /dev/null +++ b/apps/client/src/Components/Molecules/HomeSec/SimpleHomeSecListItem.tsx @@ -0,0 +1,34 @@ +import RestartAltIcon from "@mui/icons-material/RestartAlt"; +import { + IconButton, + ListItem, + ListItemAvatar, + ListItemButton, + ListItemText, +} from "@mui/material"; +import { FC } from "react"; + +const SimpleHomeSecListItem: FC<{ title: string; onClick: () => void }> = ({ + title, + onClick, +}) => { + return ( + + + + + {title} + + + + + } + /> + + + ); +}; + +export default SimpleHomeSecListItem; diff --git a/apps/server/src/homesec/homesec.controller.ts b/apps/server/src/homesec/homesec.controller.ts index 88a7600f..6ed6b825 100644 --- a/apps/server/src/homesec/homesec.controller.ts +++ b/apps/server/src/homesec/homesec.controller.ts @@ -16,6 +16,7 @@ import { HomesecPanelResponse, HomesecStatusResponse, } from "@homeremote/types"; +import { wait } from "../util/wait"; @Controller("api/homesec") export class HomesecController { @@ -48,9 +49,9 @@ export class HomesecController { password: this.password, } ).json(); - this.logger.log(panelResponse); try { + await wait(5000); const devicesResponse: HomesecDevicesResponse = await got( `${this.baseUrl}/action/deviceListGet`, { @@ -58,7 +59,7 @@ export class HomesecController { password: this.password, } ).json(); - this.logger.log(devicesResponse); + return { status: panelResponse.updates.mode_a1, devices: devicesResponse.senrows.map( diff --git a/libs/types/src/lib/homesec.types.ts b/libs/types/src/lib/homesec.types.ts index ae8f7617..635bd1f3 100644 --- a/libs/types/src/lib/homesec.types.ts +++ b/libs/types/src/lib/homesec.types.ts @@ -1,13 +1,15 @@ +export type TypeF = + | "Door Contact" + | "Smoke Detector" + | "Keypad" + | "IR" + | "Remote Controller"; + interface SensorRow { area: number; zone: number; type: number; - type_f: - | "Door Contact" - | "Smoke Detector" - | "Keypad" - | "IR" - | "Remote Controller"; + type_f: TypeF; name: string; cond: ""; cond_ok: "0" | "1"; From a4df566a9e3989378d1507fdd9a80379c64971b9 Mon Sep 17 00:00:00 2001 From: mdvanes <4253562+mdvanes@users.noreply.github.com> Date: Mon, 15 Apr 2024 16:36:01 +0200 Subject: [PATCH 05/18] chore: add tests for homesec --- .../Molecules/HomeSec/HomeSec.test.tsx | 38 ++++++ .../Components/Molecules/HomeSec/HomeSec.tsx | 2 +- .../src/homesec/homesec.controller.spec.ts | 117 ++++++++++++++++++ apps/server/src/homesec/homesec.controller.ts | 7 +- 4 files changed, 160 insertions(+), 4 deletions(-) create mode 100644 apps/client/src/Components/Molecules/HomeSec/HomeSec.test.tsx create mode 100644 apps/server/src/homesec/homesec.controller.spec.ts diff --git a/apps/client/src/Components/Molecules/HomeSec/HomeSec.test.tsx b/apps/client/src/Components/Molecules/HomeSec/HomeSec.test.tsx new file mode 100644 index 00000000..809027ca --- /dev/null +++ b/apps/client/src/Components/Molecules/HomeSec/HomeSec.test.tsx @@ -0,0 +1,38 @@ +import { render, screen } from "@testing-library/react"; +import { jukeboxApi } from "../../../Services/jukeboxApi"; +import { MockStoreProvider } from "../../../testHelpers"; +import { FC, ReactNode } from "react"; +import HomeSec from "./HomeSec"; +import fetchMock, { enableFetchMocks } from "jest-fetch-mock"; +import { HomesecStatusResponse, PlaylistsResponse } from "@homeremote/types"; +import { homesecApi } from "../../../Services/homesecApi"; + +enableFetchMocks(); + +const Wrapper: FC<{ children: ReactNode }> = ({ children }) => { + return ( + {children} + ); +}; + +describe("HomeSec", () => { + it("shows status and list of devices", async () => { + const mockStatusResponse: HomesecStatusResponse = { + status: "Disarm", + devices: [ + { + id: "1", + name: "Front door", + status: "Door Close", + rssi: "Strong, 9", + type_f: "Door Contact", + }, + ], + }; + fetchMock.mockResponse(JSON.stringify(mockStatusResponse)); + render(, { wrapper: Wrapper }); + + await screen.findByText("sensor_door"); + expect(screen.getByText("Front door")).toBeVisible(); + }); +}); diff --git a/apps/client/src/Components/Molecules/HomeSec/HomeSec.tsx b/apps/client/src/Components/Molecules/HomeSec/HomeSec.tsx index 2302c237..491af252 100644 --- a/apps/client/src/Components/Molecules/HomeSec/HomeSec.tsx +++ b/apps/client/src/Components/Molecules/HomeSec/HomeSec.tsx @@ -58,7 +58,7 @@ export const HomeSec: FC = () => { "" )} {data?.devices?.map((sensor) => ( - + {typeIcon[sensor.type_f]} diff --git a/apps/server/src/homesec/homesec.controller.spec.ts b/apps/server/src/homesec/homesec.controller.spec.ts new file mode 100644 index 00000000..bd1e5f73 --- /dev/null +++ b/apps/server/src/homesec/homesec.controller.spec.ts @@ -0,0 +1,117 @@ +import { ConfigService } from "@nestjs/config"; +import { Test, TestingModule } from "@nestjs/testing"; +import got, { CancelableRequest } from "got"; +import { mocked } from "jest-mock"; +import { AuthenticatedRequest } from "../login/LoginRequest.types"; +import { HomesecController } from "./homesec.controller"; + +jest.mock("got"); +const mockGot = mocked(got); + +const mockAuthenticatedRequest = { + user: { name: "someuser", id: 1 }, +} as AuthenticatedRequest; + +describe("HomeSec Controller", () => { + let controller: HomesecController; + let configService: ConfigService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [HomesecController], + providers: [ + { provide: ConfigService, useValue: { get: jest.fn() } }, + ], + }).compile(); + + configService = module.get(ConfigService); + controller = module.get(HomesecController); + + jest.spyOn(configService, "get").mockImplementation((envName) => { + if (envName === "HOMESEC_BASE_URL") { + return "url"; + } + if (envName === "HOMESEC_USERNAME") { + return "user"; + } + if (envName === "HOMESEC_PASSWORD") { + return "pass"; + } + }); + }); + + afterAll(() => { + mockGot.mockRestore(); + }); + + it("returns devices and panel status on /GET", async () => { + mockGot + .mockReturnValueOnce({ + json: () => + Promise.resolve({ + updates: { + mode_a1: "Disarm", + }, + }), + } as CancelableRequest) + .mockReturnValue({ + json: () => + Promise.resolve({ + senrows: [{ id: "123" }], + }), + } as CancelableRequest); + + const response = await controller.getStatus(mockAuthenticatedRequest); + expect(response).toEqual({ + status: "Disarm", + devices: [ + { + id: "123", + name: undefined, + rssi: undefined, + status: undefined, + type_f: undefined, + }, + ], + }); + expect(mockGot).toBeCalledTimes(2); + expect(mockGot).toBeCalledWith("/action/panelCondGet", { + password: "", + username: "", + }); + expect(mockGot).toBeCalledWith("/action/deviceListGet", { + password: "", + username: "", + }); + }, 10000); + + it("throws error on /GET panel failure", async () => { + mockGot.mockReturnValue({ + json: () => Promise.reject("some error"), + } as CancelableRequest); + await expect( + controller.getStatus(mockAuthenticatedRequest) + ).rejects.toThrow("failed to receive downstream data"); + }); + + // TODO this should work, but throws out of the inner catch. This does not happen runtime. + it.skip("returns [] on /GET devices failure", async () => { + mockGot + .mockReturnValueOnce({ + json: () => + Promise.resolve({ + updates: { + mode_a1: "Disarm", + }, + }), + } as CancelableRequest) + .mockReturnValue({ + json: () => Promise.resolve({}), + } as CancelableRequest); + controller.getStatus(mockAuthenticatedRequest); + + const response = await controller.getStatus(mockAuthenticatedRequest); + expect(response).toEqual({ status: "Disarm", devices: [] }); + expect(mockGot).toBeCalledTimes(2); + }, 10000); +}); diff --git a/apps/server/src/homesec/homesec.controller.ts b/apps/server/src/homesec/homesec.controller.ts index 6ed6b825..948b9330 100644 --- a/apps/server/src/homesec/homesec.controller.ts +++ b/apps/server/src/homesec/homesec.controller.ts @@ -36,7 +36,7 @@ export class HomesecController { @UseGuards(JwtAuthGuard) @Get("status") - async getDevices( + async getStatus( @Request() req: AuthenticatedRequest ): Promise { this.logger.verbose(`[${req.user.name}] GET to /api/homesec/devices`); @@ -49,6 +49,7 @@ export class HomesecController { password: this.password, } ).json(); + const status = panelResponse.updates.mode_a1; try { await wait(5000); @@ -61,7 +62,7 @@ export class HomesecController { ).json(); return { - status: panelResponse.updates.mode_a1, + status, devices: devicesResponse.senrows.map( ({ id, name, type_f, status, rssi }) => ({ id, @@ -75,7 +76,7 @@ export class HomesecController { } catch (err) { this.logger.error("deviceListGet:", err); return { - status: panelResponse.updates.mode_a1, + status, devices: [], }; } From c01ce111d7ce73b2b78a9214dac1e2c6a171435a Mon Sep 17 00:00:00 2001 From: mdvanes <4253562+mdvanes@users.noreply.github.com> Date: Mon, 15 Apr 2024 16:37:50 +0200 Subject: [PATCH 06/18] clean up --- .../src/Components/Molecules/HomeSec/HomeSec.test.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/apps/client/src/Components/Molecules/HomeSec/HomeSec.test.tsx b/apps/client/src/Components/Molecules/HomeSec/HomeSec.test.tsx index 809027ca..83e18ea3 100644 --- a/apps/client/src/Components/Molecules/HomeSec/HomeSec.test.tsx +++ b/apps/client/src/Components/Molecules/HomeSec/HomeSec.test.tsx @@ -1,11 +1,10 @@ +import { HomesecStatusResponse } from "@homeremote/types"; import { render, screen } from "@testing-library/react"; -import { jukeboxApi } from "../../../Services/jukeboxApi"; -import { MockStoreProvider } from "../../../testHelpers"; -import { FC, ReactNode } from "react"; -import HomeSec from "./HomeSec"; import fetchMock, { enableFetchMocks } from "jest-fetch-mock"; -import { HomesecStatusResponse, PlaylistsResponse } from "@homeremote/types"; +import { FC, ReactNode } from "react"; import { homesecApi } from "../../../Services/homesecApi"; +import { MockStoreProvider } from "../../../testHelpers"; +import HomeSec from "./HomeSec"; enableFetchMocks(); From ded8cb0054fcfda58e79495d1d45e05323457083 Mon Sep 17 00:00:00 2001 From: mdvanes <4253562+mdvanes@users.noreply.github.com> Date: Mon, 15 Apr 2024 19:42:18 +0200 Subject: [PATCH 07/18] chore: normalize error retry banners --- .../Molecules/DockerList/DockerList.tsx | 17 ++- .../Molecules/DownloadList/DownloadList.tsx | 28 +++-- .../Molecules/ErrorRetry/ErrorRetry.tsx | 52 ++++++++ .../Components/Molecules/HomeSec/HomeSec.tsx | 118 ++++++++++-------- 4 files changed, 139 insertions(+), 76 deletions(-) create mode 100644 apps/client/src/Components/Molecules/ErrorRetry/ErrorRetry.tsx diff --git a/apps/client/src/Components/Molecules/DockerList/DockerList.tsx b/apps/client/src/Components/Molecules/DockerList/DockerList.tsx index b4871091..666bda17 100644 --- a/apps/client/src/Components/Molecules/DockerList/DockerList.tsx +++ b/apps/client/src/Components/Molecules/DockerList/DockerList.tsx @@ -1,4 +1,4 @@ -import { Alert, Box, Grid } from "@mui/material"; +import { Box, Grid } from "@mui/material"; import { Stack } from "@mui/system"; import { FC, useEffect, useState } from "react"; import { @@ -8,6 +8,7 @@ import { import { getErrorMessage } from "../../../Utils/getErrorMessage"; import CardExpandBar from "../CardExpandBar/CardExpandBar"; import DockerInfo from "../DockerInfo/DockerInfo"; +import ErrorRetry from "../ErrorRetry/ErrorRetry"; import LoadingDot from "../LoadingDot/LoadingDot"; import ContainerDot from "./ContainerDot"; @@ -22,14 +23,12 @@ interface DockerListProps { const DockerList: FC = ({ onError }) => { const [isOpen, setIsOpen] = useState(false); const [isSkippingBecauseError, setIsSkippingBecauseError] = useState(false); - const { data, isLoading, isFetching, error } = useGetDockerListQuery( - undefined, - { + const { data, isLoading, isFetching, error, refetch } = + useGetDockerListQuery(undefined, { pollingInterval: isSkippingBecauseError ? undefined : UPDATE_INTERVAL_MS, - } - ); + }); useEffect(() => { if (error) { @@ -40,9 +39,9 @@ const DockerList: FC = ({ onError }) => { if (error) { return ( - - {getErrorMessage(error)} - + refetch()}> + DockerList could not load + ); } diff --git a/apps/client/src/Components/Molecules/DownloadList/DownloadList.tsx b/apps/client/src/Components/Molecules/DownloadList/DownloadList.tsx index 82bfea2b..f85fc7bf 100644 --- a/apps/client/src/Components/Molecules/DownloadList/DownloadList.tsx +++ b/apps/client/src/Components/Molecules/DownloadList/DownloadList.tsx @@ -1,11 +1,13 @@ +import { List, Paper } from "@mui/material"; import { FC, useEffect, useState } from "react"; -import { Alert, List, Paper } from "@mui/material"; -import { useAppDispatch } from "../../../store"; -import DownloadListItem from "./DownloadListItem"; -import { logError } from "../LogCard/logSlice"; import { useGetDownloadListQuery } from "../../../Services/downloadListApi"; +import { getErrorMessage } from "../../../Utils/getErrorMessage"; +import { useAppDispatch } from "../../../store"; import CardExpandBar from "../CardExpandBar/CardExpandBar"; +import ErrorRetry from "../ErrorRetry/ErrorRetry"; import LoadingDot from "../LoadingDot/LoadingDot"; +import { logError } from "../LogCard/logSlice"; +import DownloadListItem from "./DownloadListItem"; const UPDATE_INTERVAL_MS = 30000; @@ -14,20 +16,20 @@ const DownloadList: FC = () => { const [isSkippingBecauseError, setIsSkippingBecauseError] = useState(false); const dispatch = useAppDispatch(); - const { data, error, isLoading, isFetching } = useGetDownloadListQuery( - undefined, - { + const { data, error, isLoading, isFetching, refetch } = + useGetDownloadListQuery(undefined, { pollingInterval: isSkippingBecauseError ? undefined : UPDATE_INTERVAL_MS, - } - ); + }); const [listItems, setListItems] = useState([]); useEffect(() => { if (error) { setIsSkippingBecauseError(true); - dispatch(logError("GetDownloadList failed")); + dispatch( + logError(`GetDownloadList failed: ${getErrorMessage(error)}`) + ); } }, [dispatch, error]); @@ -50,9 +52,9 @@ const DownloadList: FC = () => { {error && ( - - There is an error, data may be stale - + refetch()}> + DL could not load + )} {listItems} void; +} + +export const ErrorRetry: FC = ({ + marginate = false, + children, + retry, +}) => { + return ( + + +
{children}
+ + + + + +
+
+ ); +}; + +export default ErrorRetry; diff --git a/apps/client/src/Components/Molecules/HomeSec/HomeSec.tsx b/apps/client/src/Components/Molecules/HomeSec/HomeSec.tsx index 491af252..273a2241 100644 --- a/apps/client/src/Components/Molecules/HomeSec/HomeSec.tsx +++ b/apps/client/src/Components/Molecules/HomeSec/HomeSec.tsx @@ -1,15 +1,17 @@ -import { FC } from "react"; -import { useGetHomesecStatusQuery } from "../../../Services/homesecApi"; +import { HomesecStatusResponse, TypeF } from "@homeremote/types"; import { + Icon, List, ListItem, ListItemAvatar, ListItemButton, ListItemText, Paper, - Icon, + Tooltip, } from "@mui/material"; -import { HomesecStatusResponse, TypeF } from "@homeremote/types"; +import { FC } from "react"; +import { useGetHomesecStatusQuery } from "../../../Services/homesecApi"; +import ErrorRetry from "../ErrorRetry/ErrorRetry"; import LoadingDot from "../LoadingDot/LoadingDot"; import SimpleHomeSecListItem from "./SimpleHomeSecListItem"; @@ -31,63 +33,71 @@ export const HomeSec: FC = () => { const { data, isLoading, isFetching, isError, refetch } = useGetHomesecStatusQuery(undefined); + const hasNoDevices = + !isError && + !isLoading && + !isFetching && + (!data?.devices || data?.devices.length === 0); + return ( - - - {isError ? ( - refetch()} - /> - ) : ( - "" - )} - {!data?.devices || data?.devices.length === 0 ? ( - refetch()} - /> - ) : ( - "" - )} - {data?.devices?.map((sensor) => ( - - - - {typeIcon[sensor.type_f]} - - -
{sensor.name}
+ + + + {isError && ( + refetch()}> + HomeSec could not load + + )} + {hasNoDevices && ( + refetch()} + /> + )} + {data?.devices?.map((sensor) => ( + + + + {typeIcon[sensor.type_f]} + + -
{sensor.status}
-
{sensor.rssi}
+
{sensor.name}
+
+
{sensor.status}
+
{sensor.rssi}
+
- - } - /> -
-
- ))} -
+ } + /> +
+
+ ))} +
+ ); }; From 8a2698563e457f4a9e15671ff36b5132ea1ba0bb Mon Sep 17 00:00:00 2001 From: mdvanes <4253562+mdvanes@users.noreply.github.com> Date: Mon, 15 Apr 2024 20:08:35 +0200 Subject: [PATCH 08/18] chore: refactor loading dot --- .../Components/Molecules/HomeSec/HomeSec.tsx | 29 ++++++++++++++++--- .../Molecules/LoadingDot/LoadingDot.tsx | 13 +++++---- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/apps/client/src/Components/Molecules/HomeSec/HomeSec.tsx b/apps/client/src/Components/Molecules/HomeSec/HomeSec.tsx index 273a2241..c95920e7 100644 --- a/apps/client/src/Components/Molecules/HomeSec/HomeSec.tsx +++ b/apps/client/src/Components/Molecules/HomeSec/HomeSec.tsx @@ -9,10 +9,13 @@ import { Paper, Tooltip, } from "@mui/material"; -import { FC } from "react"; +import { FC, useEffect, useState } from "react"; import { useGetHomesecStatusQuery } from "../../../Services/homesecApi"; +import { getErrorMessage } from "../../../Utils/getErrorMessage"; +import { useAppDispatch } from "../../../store"; import ErrorRetry from "../ErrorRetry/ErrorRetry"; import LoadingDot from "../LoadingDot/LoadingDot"; +import { logError } from "../LogCard/logSlice"; import SimpleHomeSecListItem from "./SimpleHomeSecListItem"; const statusClass: Record = { @@ -29,9 +32,24 @@ const typeIcon: Record = { "Remote Controller": "settings_remote", }; +const UPDATE_INTERVAL_MS = 120000; + export const HomeSec: FC = () => { - const { data, isLoading, isFetching, isError, refetch } = - useGetHomesecStatusQuery(undefined); + const [isSkippingBecauseError, setIsSkippingBecauseError] = useState(false); + const dispatch = useAppDispatch(); + const { data, isLoading, isFetching, isError, error, refetch } = + useGetHomesecStatusQuery(undefined, { + pollingInterval: isSkippingBecauseError + ? undefined + : UPDATE_INTERVAL_MS, + }); + + useEffect(() => { + if (error) { + setIsSkippingBecauseError(true); + dispatch(logError(`HomeSec failed: ${getErrorMessage(error)}`)); + } + }, [dispatch, error]); const hasNoDevices = !isError && @@ -49,7 +67,10 @@ export const HomeSec: FC = () => { borderColor: statusClass[data?.status ?? "Error"], }} > - + {isError && ( refetch()}> HomeSec could not load diff --git a/apps/client/src/Components/Molecules/LoadingDot/LoadingDot.tsx b/apps/client/src/Components/Molecules/LoadingDot/LoadingDot.tsx index fecca696..80597a88 100644 --- a/apps/client/src/Components/Molecules/LoadingDot/LoadingDot.tsx +++ b/apps/client/src/Components/Molecules/LoadingDot/LoadingDot.tsx @@ -3,10 +3,11 @@ import { FC, useEffect, useState } from "react"; const SLOW_UPDATE_MS = 1000; // if the response takes longer than 1000ms, it is considered slow and the full progress bar is shown -const LoadingDot: FC<{ isLoading: boolean; noMargin?: boolean }> = ({ - isLoading, - noMargin = false, -}) => { +const LoadingDot: FC<{ + isLoading: boolean; + noMargin?: boolean; + slowUpdateMs?: number; +}> = ({ isLoading, noMargin = false, slowUpdateMs = SLOW_UPDATE_MS }) => { const [isSlow, setIsSlow] = useState(false); useEffect(() => { @@ -16,14 +17,14 @@ const LoadingDot: FC<{ isLoading: boolean; noMargin?: boolean }> = ({ setIsSlow(false); timer = setTimeout(() => { setIsSlow(true); - }, SLOW_UPDATE_MS); + }, slowUpdateMs); } return () => { if (timer) { clearTimeout(timer); } }; - }, [isLoading]); + }, [isLoading, slowUpdateMs]); return ( Date: Mon, 15 Apr 2024 20:28:30 +0200 Subject: [PATCH 09/18] chore: fix validate --- .github/workflows/validateAndBuild.yml | 6 +++--- .../src/Components/Pages/Dashboard/Dashboard.test.tsx | 1 + .../Pages/Dashboard/__snapshots__/Dashboard.test.tsx.snap | 1 + 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/validateAndBuild.yml b/.github/workflows/validateAndBuild.yml index 9d731786..135acd5e 100644 --- a/.github/workflows/validateAndBuild.yml +++ b/.github/workflows/validateAndBuild.yml @@ -9,13 +9,13 @@ jobs: strategy: matrix: - node-version: [16.x] + node-version: [20.x] steps: - name: Checkout ๐Ÿ›Ž๏ธ - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - name: Install and Build components ๐Ÿ”ง diff --git a/apps/client/src/Components/Pages/Dashboard/Dashboard.test.tsx b/apps/client/src/Components/Pages/Dashboard/Dashboard.test.tsx index 8dc7a66d..d58010d6 100644 --- a/apps/client/src/Components/Pages/Dashboard/Dashboard.test.tsx +++ b/apps/client/src/Components/Pages/Dashboard/Dashboard.test.tsx @@ -30,6 +30,7 @@ jest.mock( () => "mock-previously-played-card" ); jest.mock("../../Molecules/CarTwin/CarTwinCard", () => "mock-cartwin-card"); +jest.mock("../../Molecules/HomeSec/HomeSec", () => "mock-homesec"); describe("Dashboard page", () => { it("contains all the control components", () => { diff --git a/apps/client/src/Components/Pages/Dashboard/__snapshots__/Dashboard.test.tsx.snap b/apps/client/src/Components/Pages/Dashboard/__snapshots__/Dashboard.test.tsx.snap index 165fd2d9..5c0a5293 100644 --- a/apps/client/src/Components/Pages/Dashboard/__snapshots__/Dashboard.test.tsx.snap +++ b/apps/client/src/Components/Pages/Dashboard/__snapshots__/Dashboard.test.tsx.snap @@ -15,6 +15,7 @@ exports[`Dashboard page contains all the control components 1`] = ` + From 5932ec53f4d94d797ca0ada906db5b13c972b959 Mon Sep 17 00:00:00 2001 From: mdvanes <4253562+mdvanes@users.noreply.github.com> Date: Tue, 16 Apr 2024 10:36:20 +0200 Subject: [PATCH 10/18] refactor: Monit error logging --- .../src/Components/Molecules/Monit/Monit.tsx | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/apps/client/src/Components/Molecules/Monit/Monit.tsx b/apps/client/src/Components/Molecules/Monit/Monit.tsx index e3bb88b9..fe35c3ce 100644 --- a/apps/client/src/Components/Molecules/Monit/Monit.tsx +++ b/apps/client/src/Components/Molecules/Monit/Monit.tsx @@ -1,23 +1,39 @@ -import { Alert, Card, CardContent } from "@mui/material"; -import { FC } from "react"; +import { Card, CardContent } from "@mui/material"; +import { FC, useEffect, useState } from "react"; import { useGetMonitStatusQuery } from "../../../Services/monitApi"; import { getErrorMessage } from "../../../Utils/getErrorMessage"; +import { useAppDispatch } from "../../../store"; +import ErrorRetry from "../ErrorRetry/ErrorRetry"; import LoadingDot from "../LoadingDot/LoadingDot"; +import { logError } from "../LogCard/logSlice"; import MonitInstance from "./MonitInstance"; // Monit only updates once per minute on the backend const UPDATE_INTERVAL_MS = 60 * 1000; const Monit: FC = () => { - const { data, isLoading, isFetching, error } = useGetMonitStatusQuery( - undefined, - { - pollingInterval: UPDATE_INTERVAL_MS, + const [isSkippingBecauseError, setIsSkippingBecauseError] = useState(false); + const dispatch = useAppDispatch(); + const { data, isLoading, isFetching, error, refetch } = + useGetMonitStatusQuery(undefined, { + pollingInterval: isSkippingBecauseError + ? undefined + : UPDATE_INTERVAL_MS, + }); + + useEffect(() => { + if (error) { + setIsSkippingBecauseError(true); + dispatch(logError(`Monit failed: ${getErrorMessage(error)}`)); } - ); + }, [dispatch, error]); if (error) { - return {getErrorMessage(error)}; + return ( + refetch()}> + Monit could not load + + ); } return ( From 0f44b64c861027614a9b29c82927bf651c4329bc Mon Sep 17 00:00:00 2001 From: mdvanes <4253562+mdvanes@users.noreply.github.com> Date: Tue, 16 Apr 2024 10:59:08 +0200 Subject: [PATCH 11/18] chore: collapse non doors in homesec --- .../Components/Molecules/HomeSec/HomeSec.tsx | 38 +++++++++++++++++-- .../HomeSec/SimpleHomeSecListItem.tsx | 9 ++++- libs/types/src/lib/homesec.types.ts | 5 ++- 3 files changed, 47 insertions(+), 5 deletions(-) diff --git a/apps/client/src/Components/Molecules/HomeSec/HomeSec.tsx b/apps/client/src/Components/Molecules/HomeSec/HomeSec.tsx index c95920e7..86696a1a 100644 --- a/apps/client/src/Components/Molecules/HomeSec/HomeSec.tsx +++ b/apps/client/src/Components/Molecules/HomeSec/HomeSec.tsx @@ -13,6 +13,7 @@ import { FC, useEffect, useState } from "react"; import { useGetHomesecStatusQuery } from "../../../Services/homesecApi"; import { getErrorMessage } from "../../../Utils/getErrorMessage"; import { useAppDispatch } from "../../../store"; +import CardExpandBar from "../CardExpandBar/CardExpandBar"; import ErrorRetry from "../ErrorRetry/ErrorRetry"; import LoadingDot from "../LoadingDot/LoadingDot"; import { logError } from "../LogCard/logSlice"; @@ -35,6 +36,7 @@ const typeIcon: Record = { const UPDATE_INTERVAL_MS = 120000; export const HomeSec: FC = () => { + const [isOpen, setIsOpen] = useState(false); const [isSkippingBecauseError, setIsSkippingBecauseError] = useState(false); const dispatch = useAppDispatch(); const { data, isLoading, isFetching, isError, error, refetch } = @@ -57,6 +59,13 @@ export const HomeSec: FC = () => { !isFetching && (!data?.devices || data?.devices.length === 0); + const devices = data?.devices ?? []; + + // By default, only show doors + const shownDevices = isOpen + ? devices + : devices.filter(({ type_f }) => type_f === "Door Contact"); + return ( { onClick={() => refetch()} /> )} - {data?.devices?.map((sensor) => ( + {shownDevices.map((sensor) => ( { > - {typeIcon[sensor.type_f]} + + {typeIcon[sensor.type_f]} + { gap: "16px", }} > -
{sensor.status}
+
+ {sensor.status} + + {sensor.cond_ok === "1" ? ( + + check_circle_outline + + ) : ( + + error_outline + + )} +
{sensor.rssi}
@@ -117,6 +140,15 @@ export const HomeSec: FC = () => {
))} + {devices.length > 0 && ( + + )}
); diff --git a/apps/client/src/Components/Molecules/HomeSec/SimpleHomeSecListItem.tsx b/apps/client/src/Components/Molecules/HomeSec/SimpleHomeSecListItem.tsx index c56e7346..f04c0900 100644 --- a/apps/client/src/Components/Molecules/HomeSec/SimpleHomeSecListItem.tsx +++ b/apps/client/src/Components/Molecules/HomeSec/SimpleHomeSecListItem.tsx @@ -20,7 +20,14 @@ const SimpleHomeSecListItem: FC<{ title: string; onClick: () => void }> = ({ primary={ <> {title} - + diff --git a/libs/types/src/lib/homesec.types.ts b/libs/types/src/lib/homesec.types.ts index 635bd1f3..9e8408fd 100644 --- a/libs/types/src/lib/homesec.types.ts +++ b/libs/types/src/lib/homesec.types.ts @@ -67,5 +67,8 @@ export interface HomesecPanelResponse { export interface HomesecStatusResponse { status: Modes; - devices: Pick[]; + devices: Pick< + SensorRow, + "id" | "name" | "type_f" | "status" | "rssi" | "cond_ok" + >[]; } From aff8f6ecac1e506d0e89530b6fb42928d4360a5b Mon Sep 17 00:00:00 2001 From: mdvanes <4253562+mdvanes@users.noreply.github.com> Date: Tue, 16 Apr 2024 11:16:48 +0200 Subject: [PATCH 12/18] chore: try splitting build --- .github/workflows/validateAndBuild.yml | 4 +++- .../Components/Molecules/HomeSec/HomeSec.tsx | 21 ++++++++++++++----- apps/server/src/homesec/homesec.controller.ts | 3 ++- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/.github/workflows/validateAndBuild.yml b/.github/workflows/validateAndBuild.yml index 135acd5e..195bb037 100644 --- a/.github/workflows/validateAndBuild.yml +++ b/.github/workflows/validateAndBuild.yml @@ -18,11 +18,13 @@ jobs: uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - - name: Install and Build components ๐Ÿ”ง + - name: Install dependencies ๐Ÿ”ง run: | npm i --legacy-peer-deps # Workaround for missing binary npm i @swc/core-linux-x64-gnu --legacy-peer-deps + - name: Build components ๐Ÿ”ง + run: | npm run build --if-present env: CI: false # true -> fails on warning diff --git a/apps/client/src/Components/Molecules/HomeSec/HomeSec.tsx b/apps/client/src/Components/Molecules/HomeSec/HomeSec.tsx index 86696a1a..a252c1a9 100644 --- a/apps/client/src/Components/Molecules/HomeSec/HomeSec.tsx +++ b/apps/client/src/Components/Molecules/HomeSec/HomeSec.tsx @@ -116,18 +116,29 @@ export const HomeSec: FC = () => {
+
{sensor.status}
- {sensor.status} - {sensor.cond_ok === "1" ? ( - + check_circle_outline ) : ( - + error_outline )} diff --git a/apps/server/src/homesec/homesec.controller.ts b/apps/server/src/homesec/homesec.controller.ts index 948b9330..b327e42e 100644 --- a/apps/server/src/homesec/homesec.controller.ts +++ b/apps/server/src/homesec/homesec.controller.ts @@ -64,12 +64,13 @@ export class HomesecController { return { status, devices: devicesResponse.senrows.map( - ({ id, name, type_f, status, rssi }) => ({ + ({ id, name, type_f, status, rssi, cond_ok }) => ({ id, name, type_f, status, rssi, + cond_ok, }) ), }; From 363d5141d4d1f7410a20833fd490caa1066458b2 Mon Sep 17 00:00:00 2001 From: mdvanes <4253562+mdvanes@users.noreply.github.com> Date: Tue, 16 Apr 2024 11:26:29 +0200 Subject: [PATCH 13/18] chore: split build into steps --- .github/workflows/validateAndBuild.yml | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/.github/workflows/validateAndBuild.yml b/.github/workflows/validateAndBuild.yml index 195bb037..f18164cf 100644 --- a/.github/workflows/validateAndBuild.yml +++ b/.github/workflows/validateAndBuild.yml @@ -18,17 +18,26 @@ jobs: uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - - name: Install dependencies ๐Ÿ”ง + - name: Install dependencies โš™๏ธ run: | npm i --legacy-peer-deps # Workaround for missing binary npm i @swc/core-linux-x64-gnu --legacy-peer-deps - - name: Build components ๐Ÿ”ง + - name: Typecheck ๐Ÿค™ + run: | + npm run typecheck --if-present + - name: Lint ๐Ÿ‘Œ + run: | + npm run lint --if-present + - name: Test ๐Ÿงช run: | - npm run build --if-present + npm run test:ci --if-present env: CI: false # true -> fails on warning - - name: Build docker image + - name: Build components ๐Ÿ”ง + run: | + npm run build --ignore-scripts --if-present + - name: Build docker image ๐Ÿ’ฟ if: ${{ github.ref == 'main' }} run: | docker build . -t mdworld/homeremote:latest \ No newline at end of file From 1efe1f019dd163080d1715bcdc78af360596ac26 Mon Sep 17 00:00:00 2001 From: mdvanes <4253562+mdvanes@users.noreply.github.com> Date: Tue, 16 Apr 2024 11:30:38 +0200 Subject: [PATCH 14/18] chore: update publish script --- .github/workflows/publish.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0c8f1451..2f911a62 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -10,20 +10,20 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [16.x] + node-version: [20.x] steps: - name: Check out the repo ๐Ÿ›Ž๏ธ - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Log in to Docker Hub - uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 + uses: docker/metadata-action@v5 with: images: mdworld/homeremote @@ -40,7 +40,7 @@ jobs: - name: Build and push Docker image # if: ${{ github.ref == 'main' }} - uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc + uses: docker/build-push-action@v5 with: context: . push: true From dd5924e09bfc244bdc7a4a1bddb0228f5b3bb362 Mon Sep 17 00:00:00 2001 From: mdvanes <4253562+mdvanes@users.noreply.github.com> Date: Tue, 16 Apr 2024 11:33:50 +0200 Subject: [PATCH 15/18] chore: add writeGitInfo to build --- .github/workflows/validateAndBuild.yml | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/.github/workflows/validateAndBuild.yml b/.github/workflows/validateAndBuild.yml index f18164cf..c98ab7de 100644 --- a/.github/workflows/validateAndBuild.yml +++ b/.github/workflows/validateAndBuild.yml @@ -23,20 +23,18 @@ jobs: npm i --legacy-peer-deps # Workaround for missing binary npm i @swc/core-linux-x64-gnu --legacy-peer-deps + - name: writeGitInfo โœ๏ธ + run: npm run writeGitInfo --if-present - name: Typecheck ๐Ÿค™ - run: | - npm run typecheck --if-present + run: npm run typecheck --if-present - name: Lint ๐Ÿ‘Œ - run: | - npm run lint --if-present + run: npm run lint --if-present - name: Test ๐Ÿงช - run: | - npm run test:ci --if-present + run: npm run test:ci --if-present env: CI: false # true -> fails on warning - name: Build components ๐Ÿ”ง - run: | - npm run build --ignore-scripts --if-present + run: npm run build --ignore-scripts --if-present - name: Build docker image ๐Ÿ’ฟ if: ${{ github.ref == 'main' }} run: | From 8f1db075579596178637ecaae789253772a905d6 Mon Sep 17 00:00:00 2001 From: mdvanes <4253562+mdvanes@users.noreply.github.com> Date: Tue, 16 Apr 2024 11:52:10 +0200 Subject: [PATCH 16/18] fix test --- apps/client/src/Components/Molecules/HomeSec/HomeSec.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/client/src/Components/Molecules/HomeSec/HomeSec.test.tsx b/apps/client/src/Components/Molecules/HomeSec/HomeSec.test.tsx index 83e18ea3..eb03ad13 100644 --- a/apps/client/src/Components/Molecules/HomeSec/HomeSec.test.tsx +++ b/apps/client/src/Components/Molecules/HomeSec/HomeSec.test.tsx @@ -25,6 +25,7 @@ describe("HomeSec", () => { status: "Door Close", rssi: "Strong, 9", type_f: "Door Contact", + cond_ok: "1", }, ], }; From e4b1372aebdd69e3655de51bff75b3869e9e4c73 Mon Sep 17 00:00:00 2001 From: mdvanes <4253562+mdvanes@users.noreply.github.com> Date: Tue, 16 Apr 2024 12:19:43 +0200 Subject: [PATCH 17/18] chore: show docker compose project --- .../Molecules/DockerInfo/DockerInfo.tsx | 10 ++++-- .../Molecules/DockerList/ContainerDot.tsx | 2 +- .../Molecules/DockerList/DockerList.tsx | 6 ++-- apps/client/src/Services/dockerListApi.ts | 13 +------- .../src/dockerlist/dockerlist.controller.ts | 32 +++++++++++++------ libs/types/src/lib/dockerlist.types.ts | 3 ++ 6 files changed, 36 insertions(+), 30 deletions(-) diff --git a/apps/client/src/Components/Molecules/DockerInfo/DockerInfo.tsx b/apps/client/src/Components/Molecules/DockerInfo/DockerInfo.tsx index db8fa3c6..bc3af90f 100644 --- a/apps/client/src/Components/Molecules/DockerInfo/DockerInfo.tsx +++ b/apps/client/src/Components/Molecules/DockerInfo/DockerInfo.tsx @@ -1,3 +1,4 @@ +import { DockerContainerInfo } from "@homeremote/types"; import { Alert, Button, @@ -10,7 +11,6 @@ import { } from "@mui/material"; import { FC, useState } from "react"; import { - DockerContainerInfo, useStartDockerMutation, useStopDockerMutation, } from "../../../Services/dockerListApi"; @@ -18,7 +18,7 @@ import { const DockerInfo: FC<{ info: DockerContainerInfo }> = ({ info }) => { const [startDocker] = useStartDockerMutation(); const [stopDocker] = useStopDockerMutation(); - const { Names, Status, Id, State } = info; + const { Names, Status, Id, State, Labels } = info; const isUp = Status.indexOf("Up") === 0; const toggleContainer = () => { @@ -50,7 +50,11 @@ const DockerInfo: FC<{ info: DockerContainerInfo }> = ({ info }) => { cursor: "pointer", }} > - {name} | {Status} + {name}{" "} + {Labels["com.docker.compose.project"] + ? `(${Labels["com.docker.compose.project"]})` + : ""}{" "} + | {Status} = ({ info }) => ( ({ Id, Names, State, Status }); + Labels, +}: DockerContainerInfo): DockerContainerInfo => ({ + Id, + Names, + State, + Status, + Labels: { + "com.docker.compose.project": Labels["com.docker.compose.project"], + }, +}); // Using Docker Engine API: curl --unix-socket /var/run/docker.sock http://v1.24/containers/json?all=true // These urls also work: http://localhost/v1.24/containers/json?all=true or v1.24/containers/json?all=true @@ -44,6 +53,9 @@ export class DockerlistController { const result = await got(`${ROOT_URL}/json?all=true`, { socketPath: SOCKET_PATH, }).json(); + this.logger.verbose( + result.map((r) => r.Labels["com.docker.compose.project"]) + ); return { status: "received", containers: result.map(pickAndMapContainerProps), diff --git a/libs/types/src/lib/dockerlist.types.ts b/libs/types/src/lib/dockerlist.types.ts index 247891ad..ec58a37c 100644 --- a/libs/types/src/lib/dockerlist.types.ts +++ b/libs/types/src/lib/dockerlist.types.ts @@ -3,6 +3,9 @@ export interface DockerContainerInfo { Names: string[]; State: string; Status: string; + Labels: { + "com.docker.compose.project": string | null; + }; } export type AllResponse = DockerContainerInfo[]; From 09061f7ee18e99dfd36d4a0520bb4a89872c219b Mon Sep 17 00:00:00 2001 From: mdvanes <4253562+mdvanes@users.noreply.github.com> Date: Tue, 16 Apr 2024 14:53:25 +0200 Subject: [PATCH 18/18] fix: test --- apps/client/src/Components/Pages/Docker/Docker.test.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/client/src/Components/Pages/Docker/Docker.test.tsx b/apps/client/src/Components/Pages/Docker/Docker.test.tsx index 7bcad46a..194ba49b 100644 --- a/apps/client/src/Components/Pages/Docker/Docker.test.tsx +++ b/apps/client/src/Components/Pages/Docker/Docker.test.tsx @@ -24,12 +24,18 @@ const mockDockerListResponse: DockerListResponse = { Names: ["/some-name"], State: "running", Status: "Up 5 days", + Labels: { + "com.docker.compose.project": null, + }, }, { Id: "124", Names: ["/some-stopped"], State: "stopped", Status: "Exited 5 days ago", + Labels: { + "com.docker.compose.project": null, + }, }, ], };