2 Commits

Author SHA1 Message Date
e0f0067e89 Get state of relay on device page 2024-09-25 21:56:54 +02:00
3c2fa18d9a Display relays status 2024-09-25 19:35:39 +02:00
8 changed files with 157 additions and 10 deletions

View File

@@ -360,3 +360,34 @@ impl Handler<GetDevicesState> for EnergyActor {
.collect()
}
}
#[derive(serde::Serialize)]
pub struct ResRelayState {
pub id: DeviceRelayID,
on: bool,
r#for: usize,
}
/// Get the state of all relays
#[derive(Message)]
#[rtype(result = "Vec<ResRelayState>")]
pub struct GetAllRelaysState;
impl Handler<GetAllRelaysState> for EnergyActor {
type Result = Vec<ResRelayState>;
fn handle(&mut self, _msg: GetAllRelaysState, _ctx: &mut Context<Self>) -> Self::Result {
let mut list = vec![];
for d in &self.devices.relays_list() {
let state = self.engine.relay_state(d.id);
list.push(ResRelayState {
id: d.id,
on: state.is_on(),
r#for: state.state_for(),
})
}
list
}
}

View File

@@ -39,6 +39,10 @@ impl RelayState {
fn is_off(&self) -> bool {
!self.on
}
pub fn state_for(&self) -> usize {
(time_secs() - self.since as u64) as usize
}
}
type RelaysState = HashMap<DeviceRelayID, RelayState>;

View File

@@ -185,6 +185,14 @@ pub async fn secure_server(energy_actor: EnergyActorAddr) -> anyhow::Result<()>
"/web_api/relay/{id}",
web::delete().to(relays_controller::delete),
)
.route(
"/web_api/relays/status",
web::get().to(relays_controller::status_all),
)
.route(
"/web_api/relay/{id}/status",
web::get().to(relays_controller::status_single),
)
// Devices API
.route(
"/devices_api/utils/time",

View File

@@ -93,3 +93,20 @@ pub async fn delete(actor: WebEnergyActor, path: web::Path<RelayIDInPath>) -> Ht
Ok(HttpResponse::Accepted().finish())
}
/// Get the status of all relays
pub async fn status_all(actor: WebEnergyActor) -> HttpResult {
let list = actor.send(energy_actor::GetAllRelaysState).await?;
Ok(HttpResponse::Ok().json(list))
}
/// Get the state of a single relay
pub async fn status_single(actor: WebEnergyActor, path: web::Path<RelayIDInPath>) -> HttpResult {
let list = actor.send(energy_actor::GetAllRelaysState).await?;
let Some(state) = list.into_iter().find(|r| r.id == path.id) else {
return Ok(HttpResponse::NotFound().json("Relay not found!"));
};
Ok(HttpResponse::Ok().json(state))
}

View File

@@ -1,6 +1,14 @@
import { APIClient } from "./ApiClient";
import { Device, DeviceRelay } from "./DeviceApi";
export interface RelayStatus {
id: string;
on: boolean;
for: number;
}
export type RelaysStatus = Map<string, RelayStatus>;
export class RelayApi {
/**
* Get the full list of relays
@@ -49,4 +57,34 @@ export class RelayApi {
uri: `/relay/${relay.id}`,
});
}
/**
* Get the status of all relays
*/
static async GetRelaysStatus(): Promise<RelaysStatus> {
const data: any[] = (
await APIClient.exec({
method: "GET",
uri: `/relays/status`,
})
).data;
const map = new Map();
for (let r of data) {
map.set(r.id, r);
}
return map;
}
/**
* Get the status of a single relay
*/
static async SingleStatus(relay: DeviceRelay): Promise<RelayStatus> {
return (
await APIClient.exec({
method: "GET",
uri: `/relay/${relay.id}/state`,
})
).data;
}
}

View File

@@ -14,9 +14,12 @@ import { EditDeviceRelaysDialog } from "../../dialogs/EditDeviceRelaysDialog";
import { DeviceRouteCard } from "./DeviceRouteCard";
import { useConfirm } from "../../hooks/context_providers/ConfirmDialogProvider";
import { useLoadingMessage } from "../../hooks/context_providers/LoadingMessageProvider";
import { RelayApi } from "../../api/RelayApi";
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";
export function DeviceRelays(p: {
device: Device;
@@ -115,10 +118,35 @@ export function DeviceRelays(p: {
</>
}
>
<ListItemText primary={r.name} secondary={"TODO: status"} />
<ListItemText
primary={r.name}
secondary={<RelayEntryStatus relay={r} />}
/>
</ListItem>
))}
</DeviceRouteCard>
</>
);
}
function RelayEntryStatus(p: { relay: DeviceRelay }): React.ReactElement {
const [state, setState] = React.useState<RelayStatus | undefined>();
const load = async () => {
setState(await RelayApi.SingleStatus(p.relay));
};
return (
<AsyncWidget
loadKey={p.relay.id}
load={load}
errMsg="Failed to load relay status!"
build={() => (
<>
<BoolText val={state!.on} positive="ON" negative="OFF" /> for{" "}
<TimeWidget diff time={state!.for} />
</>
)}
/>
);
}

View File

@@ -12,17 +12,21 @@ import {
} from "@mui/material";
import React from "react";
import { DeviceRelay } from "../api/DeviceApi";
import { RelayApi } from "../api/RelayApi";
import { RelayApi, RelaysStatus } from "../api/RelayApi";
import { AsyncWidget } from "../widgets/AsyncWidget";
import { BoolText } from "../widgets/BoolText";
import { SolarEnergyRouteContainer } from "../widgets/SolarEnergyRouteContainer";
import { TimeWidget } from "../widgets/TimeWidget";
export function RelaysListRoute(): React.ReactElement {
const loadKey = React.useRef(1);
const [list, setList] = React.useState<DeviceRelay[] | undefined>();
const [status, setStatus] = React.useState<RelaysStatus | undefined>();
const load = async () => {
setList(await RelayApi.GetList());
setStatus(await RelayApi.GetRelaysStatus());
list?.sort((a, b) => b.priority - a.priority);
};
@@ -48,7 +52,9 @@ export function RelaysListRoute(): React.ReactElement {
ready={!!list}
errMsg="Failed to load the list of relays!"
load={load}
build={() => <RelaysList onReload={reload} list={list!} />}
build={() => (
<RelaysList onReload={reload} list={list!} status={status!} />
)}
/>
</SolarEnergyRouteContainer>
);
@@ -56,6 +62,7 @@ export function RelaysListRoute(): React.ReactElement {
function RelaysList(p: {
list: DeviceRelay[];
status: RelaysStatus;
onReload: () => void;
}): React.ReactElement {
return (
@@ -78,15 +85,18 @@ function RelaysList(p: {
>
<TableCell>{row.name}</TableCell>
<TableCell>
{row.enabled ? (
<span style={{ color: "green" }}>YES</span>
) : (
<span style={{ color: "red" }}>NO</span>
)}
<BoolText val={row.enabled} positive="YES" negative="NO" />
</TableCell>
<TableCell>{row.priority}</TableCell>
<TableCell>{row.consumption}</TableCell>
<TableCell>TODO</TableCell>
<TableCell>
<BoolText
val={p.status.get(row.id)!.on}
positive="ON"
negative="OFF"
/>{" "}
for <TimeWidget diff time={p.status.get(row.id)!.for} />
</TableCell>
</TableRow>
))}
</TableBody>

View File

@@ -0,0 +1,11 @@
export function BoolText(p: {
val: boolean;
positive: string;
negative: string;
}): React.ReactElement {
return p.val ? (
<span style={{ color: "green" }}>{p.positive}</span>
) : (
<span style={{ color: "red" }}>{p.negative}</span>
);
}