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 { 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 {
|
||||
id: string;
|
||||
on: boolean;
|
||||
for: number;
|
||||
forced_state: RelayForcedState;
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
|
||||
@@ -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";
|
||||
import React from "react";
|
||||
import { Device, DeviceRelay } from "../../api/DeviceApi";
|
||||
import { RelayApi, RelayStatus } from "../../api/RelayApi";
|
||||
import { EditDeviceRelaysDialog } from "../../dialogs/EditDeviceRelaysDialog";
|
||||
import { DeviceRouteCard } from "./DeviceRouteCard";
|
||||
import { useAlert } from "../../hooks/context_providers/AlertDialogProvider";
|
||||
import { useConfirm } from "../../hooks/context_providers/ConfirmDialogProvider";
|
||||
import { useLoadingMessage } from "../../hooks/context_providers/LoadingMessageProvider";
|
||||
import { RelayApi, RelayStatus } from "../../api/RelayApi";
|
||||
import { useSnackbar } from "../../hooks/context_providers/SnackbarProvider";
|
||||
import { useAlert } from "../../hooks/context_providers/AlertDialogProvider";
|
||||
import { AsyncWidget } from "../../widgets/AsyncWidget";
|
||||
import { TimeWidget } from "../../widgets/TimeWidget";
|
||||
import { BoolText } from "../../widgets/BoolText";
|
||||
import { TimeWidget } from "../../widgets/TimeWidget";
|
||||
import { DeviceRouteCard } from "./DeviceRouteCard";
|
||||
|
||||
export function DeviceRelays(p: {
|
||||
device: Device;
|
||||
@@ -145,7 +145,8 @@ function RelayEntryStatus(
|
||||
errMsg="Failed to load relay status!"
|
||||
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} />
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -16,12 +16,13 @@ import React from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Device, DeviceApi, DeviceRelay, DeviceURL } from "../api/DeviceApi";
|
||||
import { RelayApi, RelaysStatus } from "../api/RelayApi";
|
||||
import { ServerApi } from "../api/ServerApi";
|
||||
import { AsyncWidget } from "../widgets/AsyncWidget";
|
||||
import { BoolText } from "../widgets/BoolText";
|
||||
import { CopyToClipboard } from "../widgets/CopyToClipboard";
|
||||
import { RelayForcedState } from "../widgets/RelayForcedState";
|
||||
import { SolarEnergyRouteContainer } from "../widgets/SolarEnergyRouteContainer";
|
||||
import { TimeWidget } from "../widgets/TimeWidget";
|
||||
import { CopyToClipboard } from "../widgets/CopyToClipboard";
|
||||
import { ServerApi } from "../api/ServerApi";
|
||||
|
||||
export function RelaysListRoute(p: {
|
||||
homeWidget?: boolean;
|
||||
@@ -104,6 +105,7 @@ function RelaysList(p: {
|
||||
<TableCell>Priority</TableCell>
|
||||
<TableCell>Consumption</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Forced state</TableCell>
|
||||
<TableCell></TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
@@ -129,6 +131,13 @@ function RelaysList(p: {
|
||||
/>{" "}
|
||||
for <TimeWidget diff time={p.status.get(row.id)!.for} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<RelayForcedState
|
||||
relay={row}
|
||||
state={p.status.get(row.id)!}
|
||||
onUpdated={p.onReload}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Tooltip title="Copy legacy api status">
|
||||
<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`;
|
||||
}
|
||||
|
||||
export function timeDiffFromNow(t: number): string {
|
||||
return timeDiff(t, time());
|
||||
export function timeDiffFromNow(t: number, future?: boolean): string {
|
||||
return future ? timeDiff(time(), t) : timeDiff(t, time());
|
||||
}
|
||||
|
||||
export function TimeWidget(p: {
|
||||
time?: number;
|
||||
diff?: boolean;
|
||||
future?: boolean;
|
||||
}): React.ReactElement {
|
||||
if (!p.time) return <></>;
|
||||
return (
|
||||
@@ -65,7 +66,9 @@ export function TimeWidget(p: {
|
||||
title={formatDate(p.diff ? new Date().getTime() / 1000 - p.time : p.time)}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user