Compare commits
3 Commits
88a24565b4
...
1.0.3
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d46f9c52c | |||
| c90a05fcfd | |||
| abdca20a66 |
35
.drone.yml
35
.drone.yml
@@ -46,6 +46,11 @@ steps:
|
||||
path: /usr/local/cargo/registry
|
||||
- name: web_app
|
||||
path: /tmp/web_build
|
||||
- name: releases
|
||||
path: /tmp/releases
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
depends_on:
|
||||
- backend_check
|
||||
- web_build
|
||||
@@ -54,13 +59,41 @@ steps:
|
||||
- mv /tmp/web_build/dist static
|
||||
- cargo build --release
|
||||
- ls -lah target/release/central_backend
|
||||
- mv target/release/central_backend /tmp/releases/central_backend
|
||||
|
||||
# Build ESP32 program
|
||||
- name: esp32_compile
|
||||
image: espressif/idf:v5.5.1
|
||||
volumes:
|
||||
- name: releases
|
||||
path: /tmp/releases
|
||||
commands:
|
||||
- cd esp32_device
|
||||
- /opt/esp/entrypoint.sh idf.py build
|
||||
- ls -lah build/main.bin
|
||||
- cp build/main.bin /tmp/releases/wt32-eth01.bin
|
||||
|
||||
# Auto-release to Gitea
|
||||
- name: gitea_release
|
||||
image: plugins/gitea-release
|
||||
depends_on:
|
||||
- backend_compile
|
||||
- esp32_compile
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
volumes:
|
||||
- name: releases
|
||||
path: /tmp/releases
|
||||
environment:
|
||||
PLUGIN_API_KEY:
|
||||
from_secret: GITEA_API_KEY # needs permission write:repository
|
||||
settings:
|
||||
base_url: https://gitea.communiquons.org
|
||||
files:
|
||||
- /tmp/releases/central_backend
|
||||
- /tmp/releases/wt32-eth01.bin
|
||||
checksum: sha512
|
||||
|
||||
|
||||
volumes:
|
||||
@@ -68,3 +101,5 @@ volumes:
|
||||
temp: {}
|
||||
- name: web_app
|
||||
temp: {}
|
||||
- name: releases
|
||||
temp: {}
|
||||
|
||||
2
central_backend/Cargo.lock
generated
2
central_backend/Cargo.lock
generated
@@ -640,7 +640,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "central_backend"
|
||||
version = "1.0.2"
|
||||
version = "1.0.3"
|
||||
dependencies = [
|
||||
"actix",
|
||||
"actix-cors",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "central_backend"
|
||||
version = "1.0.2"
|
||||
version = "1.0.3"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.0.2
|
||||
1.0.3
|
||||
Reference in New Issue
Block a user