Can set relay forced state from UI
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
@@ -1,10 +1,19 @@
|
|||||||
import { APIClient } from "./ApiClient";
|
import { APIClient } from "./ApiClient";
|
||||||
import { Device, DeviceRelay } from "./DeviceApi";
|
import { Device, DeviceRelay } from "./DeviceApi";
|
||||||
|
|
||||||
|
export type RelayForcedState =
|
||||||
|
| { type: "None" }
|
||||||
|
| { type: "Off" | "On"; until: number };
|
||||||
|
|
||||||
|
export type SetRelayForcedState =
|
||||||
|
| { type: "None" }
|
||||||
|
| { type: "Off" | "On"; for_secs: number };
|
||||||
|
|
||||||
export interface RelayStatus {
|
export interface RelayStatus {
|
||||||
id: string;
|
id: string;
|
||||||
on: boolean;
|
on: boolean;
|
||||||
for: number;
|
for: number;
|
||||||
|
forced_state: RelayForcedState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RelaysStatus = Map<string, RelayStatus>;
|
export type RelaysStatus = Map<string, RelayStatus>;
|
||||||
@@ -48,6 +57,20 @@ export class RelayApi {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set relay forced state
|
||||||
|
*/
|
||||||
|
static async SetForcedState(
|
||||||
|
relay: DeviceRelay,
|
||||||
|
forced: SetRelayForcedState
|
||||||
|
): Promise<void> {
|
||||||
|
await APIClient.exec({
|
||||||
|
method: "PUT",
|
||||||
|
uri: `/relay/${relay.id}/forced_state`,
|
||||||
|
jsonData: forced,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a relay configuration
|
* Delete a relay configuration
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogContentText,
|
||||||
|
DialogTitle,
|
||||||
|
TextField,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { DeviceRelay } from "../api/DeviceApi";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export function SelectForcedStateDurationDialog(p: {
|
||||||
|
relay: DeviceRelay;
|
||||||
|
forcedState: string;
|
||||||
|
onCancel: () => void;
|
||||||
|
onSubmit: (duration: number) => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const [duration, setDuration] = React.useState(60);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open onClose={p.onCancel}>
|
||||||
|
<DialogTitle>Set forced relay state</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText>
|
||||||
|
Please specify the number of minutes the relay <i>{p.relay.name}</i>{" "}
|
||||||
|
will remain in forced state <i>{p.forcedState}</i>:
|
||||||
|
</DialogContentText>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label="Duration (min)"
|
||||||
|
variant="standard"
|
||||||
|
value={Math.floor(duration / 60)}
|
||||||
|
onChange={(e) => {
|
||||||
|
const val = Number.parseInt(e.target.value);
|
||||||
|
setDuration((Number.isNaN(val) ? 1 : val) * 60);
|
||||||
|
}}
|
||||||
|
fullWidth
|
||||||
|
style={{ marginTop: "5px" }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p>Equivalent in seconds: {duration} secs</p>
|
||||||
|
<p>Equivalent in hours: {duration / 3600} hours</p>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={p.onCancel}>Cancel</Button>
|
||||||
|
<Button onClick={() => p.onSubmit(duration)} autoFocus>
|
||||||
|
Start timer
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -10,16 +10,16 @@ import {
|
|||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Device, DeviceRelay } from "../../api/DeviceApi";
|
import { Device, DeviceRelay } from "../../api/DeviceApi";
|
||||||
|
import { RelayApi, RelayStatus } from "../../api/RelayApi";
|
||||||
import { EditDeviceRelaysDialog } from "../../dialogs/EditDeviceRelaysDialog";
|
import { EditDeviceRelaysDialog } from "../../dialogs/EditDeviceRelaysDialog";
|
||||||
import { DeviceRouteCard } from "./DeviceRouteCard";
|
import { useAlert } from "../../hooks/context_providers/AlertDialogProvider";
|
||||||
import { useConfirm } from "../../hooks/context_providers/ConfirmDialogProvider";
|
import { useConfirm } from "../../hooks/context_providers/ConfirmDialogProvider";
|
||||||
import { useLoadingMessage } from "../../hooks/context_providers/LoadingMessageProvider";
|
import { useLoadingMessage } from "../../hooks/context_providers/LoadingMessageProvider";
|
||||||
import { RelayApi, RelayStatus } from "../../api/RelayApi";
|
|
||||||
import { useSnackbar } from "../../hooks/context_providers/SnackbarProvider";
|
import { useSnackbar } from "../../hooks/context_providers/SnackbarProvider";
|
||||||
import { useAlert } from "../../hooks/context_providers/AlertDialogProvider";
|
|
||||||
import { AsyncWidget } from "../../widgets/AsyncWidget";
|
import { AsyncWidget } from "../../widgets/AsyncWidget";
|
||||||
import { TimeWidget } from "../../widgets/TimeWidget";
|
|
||||||
import { BoolText } from "../../widgets/BoolText";
|
import { BoolText } from "../../widgets/BoolText";
|
||||||
|
import { TimeWidget } from "../../widgets/TimeWidget";
|
||||||
|
import { DeviceRouteCard } from "./DeviceRouteCard";
|
||||||
|
|
||||||
export function DeviceRelays(p: {
|
export function DeviceRelays(p: {
|
||||||
device: Device;
|
device: Device;
|
||||||
@@ -145,7 +145,8 @@ function RelayEntryStatus(
|
|||||||
errMsg="Failed to load relay status!"
|
errMsg="Failed to load relay status!"
|
||||||
build={() => (
|
build={() => (
|
||||||
<>
|
<>
|
||||||
<BoolText val={state!.on} positive="ON" negative="OFF" /> for{" "}
|
<BoolText val={state!.on} positive="ON" negative="OFF" />{" "}
|
||||||
|
{state?.forced_state.type !== "None" && <b>Forced</b>} for{" "}
|
||||||
<TimeWidget diff time={state!.for} />
|
<TimeWidget diff time={state!.for} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -16,12 +16,13 @@ import React from "react";
|
|||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { Device, DeviceApi, DeviceRelay, DeviceURL } from "../api/DeviceApi";
|
import { Device, DeviceApi, DeviceRelay, DeviceURL } from "../api/DeviceApi";
|
||||||
import { RelayApi, RelaysStatus } from "../api/RelayApi";
|
import { RelayApi, RelaysStatus } from "../api/RelayApi";
|
||||||
|
import { ServerApi } from "../api/ServerApi";
|
||||||
import { AsyncWidget } from "../widgets/AsyncWidget";
|
import { AsyncWidget } from "../widgets/AsyncWidget";
|
||||||
import { BoolText } from "../widgets/BoolText";
|
import { BoolText } from "../widgets/BoolText";
|
||||||
|
import { CopyToClipboard } from "../widgets/CopyToClipboard";
|
||||||
|
import { RelayForcedState } from "../widgets/RelayForcedState";
|
||||||
import { SolarEnergyRouteContainer } from "../widgets/SolarEnergyRouteContainer";
|
import { SolarEnergyRouteContainer } from "../widgets/SolarEnergyRouteContainer";
|
||||||
import { TimeWidget } from "../widgets/TimeWidget";
|
import { TimeWidget } from "../widgets/TimeWidget";
|
||||||
import { CopyToClipboard } from "../widgets/CopyToClipboard";
|
|
||||||
import { ServerApi } from "../api/ServerApi";
|
|
||||||
|
|
||||||
export function RelaysListRoute(p: {
|
export function RelaysListRoute(p: {
|
||||||
homeWidget?: boolean;
|
homeWidget?: boolean;
|
||||||
@@ -104,6 +105,7 @@ function RelaysList(p: {
|
|||||||
<TableCell>Priority</TableCell>
|
<TableCell>Priority</TableCell>
|
||||||
<TableCell>Consumption</TableCell>
|
<TableCell>Consumption</TableCell>
|
||||||
<TableCell>Status</TableCell>
|
<TableCell>Status</TableCell>
|
||||||
|
<TableCell>Forced state</TableCell>
|
||||||
<TableCell></TableCell>
|
<TableCell></TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
@@ -129,6 +131,13 @@ function RelaysList(p: {
|
|||||||
/>{" "}
|
/>{" "}
|
||||||
for <TimeWidget diff time={p.status.get(row.id)!.for} />
|
for <TimeWidget diff time={p.status.get(row.id)!.for} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<RelayForcedState
|
||||||
|
relay={row}
|
||||||
|
state={p.status.get(row.id)!}
|
||||||
|
onUpdated={p.onReload}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Tooltip title="Copy legacy api status">
|
<Tooltip title="Copy legacy api status">
|
||||||
<CopyToClipboard
|
<CopyToClipboard
|
||||||
|
|||||||
79
central_frontend/src/widgets/RelayForcedState.tsx
Normal file
79
central_frontend/src/widgets/RelayForcedState.tsx
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { MenuItem, Select, SelectChangeEvent } from "@mui/material";
|
||||||
|
import { DeviceRelay } from "../api/DeviceApi";
|
||||||
|
import { RelayApi, RelayStatus, SetRelayForcedState } from "../api/RelayApi";
|
||||||
|
import { TimeWidget } from "./TimeWidget";
|
||||||
|
import { useLoadingMessage } from "../hooks/context_providers/LoadingMessageProvider";
|
||||||
|
import { useAlert } from "../hooks/context_providers/AlertDialogProvider";
|
||||||
|
import { useSnackbar } from "../hooks/context_providers/SnackbarProvider";
|
||||||
|
import React from "react";
|
||||||
|
import { SelectForcedStateDurationDialog } from "../dialogs/SelectForcedStateDurationDialog";
|
||||||
|
|
||||||
|
export function RelayForcedState(p: {
|
||||||
|
relay: DeviceRelay;
|
||||||
|
state: RelayStatus;
|
||||||
|
onUpdated: () => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const loadingMessage = useLoadingMessage();
|
||||||
|
const alert = useAlert();
|
||||||
|
const snackbar = useSnackbar();
|
||||||
|
|
||||||
|
const [futureStateType, setFutureStateType] = React.useState<
|
||||||
|
string | undefined
|
||||||
|
>();
|
||||||
|
|
||||||
|
const handleChange = (event: SelectChangeEvent) => {
|
||||||
|
if (event.target.value == "None") {
|
||||||
|
submitChange({ type: "None" });
|
||||||
|
} else {
|
||||||
|
setFutureStateType(event.target.value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitChange = async (state: SetRelayForcedState) => {
|
||||||
|
try {
|
||||||
|
loadingMessage.show("Setting forced state...");
|
||||||
|
await RelayApi.SetForcedState(p.relay, state);
|
||||||
|
p.onUpdated();
|
||||||
|
snackbar("Forced state successfully updated!");
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Failed to set relay forced state! ${e}`);
|
||||||
|
alert(`Failed to set loading state for relay! ${e}`);
|
||||||
|
} finally {
|
||||||
|
loadingMessage.hide();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Select
|
||||||
|
value={p.state.forced_state.type}
|
||||||
|
onChange={handleChange}
|
||||||
|
size="small"
|
||||||
|
variant="standard"
|
||||||
|
>
|
||||||
|
<MenuItem value={"None"}>None</MenuItem>
|
||||||
|
<MenuItem value={"Off"}>Off</MenuItem>
|
||||||
|
<MenuItem value={"On"}>On</MenuItem>
|
||||||
|
</Select>
|
||||||
|
{p.state.forced_state.type !== "None" && (
|
||||||
|
<>
|
||||||
|
<TimeWidget future time={p.state.forced_state.until} /> left
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{futureStateType !== undefined && (
|
||||||
|
<SelectForcedStateDurationDialog
|
||||||
|
{...p}
|
||||||
|
forcedState={futureStateType}
|
||||||
|
onCancel={() => setFutureStateType(undefined)}
|
||||||
|
onSubmit={(d) =>
|
||||||
|
submitChange({
|
||||||
|
type: futureStateType as any,
|
||||||
|
for_secs: d,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -51,13 +51,14 @@ export function timeDiff(a: number, b: number): string {
|
|||||||
return `${diffYears} years`;
|
return `${diffYears} years`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function timeDiffFromNow(t: number): string {
|
export function timeDiffFromNow(t: number, future?: boolean): string {
|
||||||
return timeDiff(t, time());
|
return future ? timeDiff(time(), t) : timeDiff(t, time());
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TimeWidget(p: {
|
export function TimeWidget(p: {
|
||||||
time?: number;
|
time?: number;
|
||||||
diff?: boolean;
|
diff?: boolean;
|
||||||
|
future?: boolean;
|
||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
if (!p.time) return <></>;
|
if (!p.time) return <></>;
|
||||||
return (
|
return (
|
||||||
@@ -65,7 +66,9 @@ export function TimeWidget(p: {
|
|||||||
title={formatDate(p.diff ? new Date().getTime() / 1000 - p.time : p.time)}
|
title={formatDate(p.diff ? new Date().getTime() / 1000 - p.time : p.time)}
|
||||||
arrow
|
arrow
|
||||||
>
|
>
|
||||||
<span>{p.diff ? timeDiff(0, p.time) : timeDiffFromNow(p.time)}</span>
|
<span>
|
||||||
|
{p.diff ? timeDiff(0, p.time) : timeDiffFromNow(p.time, p.future)}
|
||||||
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user