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