Skip to content

Commit

Permalink
feat(timers): Allow manual execution of stored timers
Browse files Browse the repository at this point in the history
  • Loading branch information
Hypfer committed Apr 23, 2023
1 parent 7327236 commit a5f920e
Show file tree
Hide file tree
Showing 8 changed files with 152 additions and 10 deletions.
14 changes: 7 additions & 7 deletions backend/lib/Valetudo.js
Expand Up @@ -84,25 +84,25 @@ class Valetudo {
valetudoHelper: this.valetudoHelper
});

this.scheduler = new Scheduler({
config: this.config,
robot: this.robot,
ntpClient: this.ntpClient
});

this.webserver = new Webserver({
config: this.config,
robot: this.robot,
mqttController: this.mqttController,
networkAdvertisementManager: this.networkAdvertisementManager,
ntpClient: this.ntpClient,
updater: this.updater,
scheduler: this.scheduler,
valetudoEventStore: this.valetudoEventStore,
valetudoHelper: this.valetudoHelper
});


this.scheduler = new Scheduler({
config: this.config,
robot: this.robot,
ntpClient: this.ntpClient
});


this.networkConnectionStabilizer = new NetworkConnectionStabilizer({
config: this.config
});
Expand Down
23 changes: 23 additions & 0 deletions backend/lib/webserver/TimerRouter.js
@@ -1,6 +1,7 @@
const BasicControlCapability = require("../core/capabilities/BasicControlCapability");
const express = require("express");
const FanSpeedControlCapability = require("../core/capabilities/FanSpeedControlCapability");
const Logger = require("../Logger");
const MapSegmentationCapability = require("../core/capabilities/MapSegmentationCapability");
const OperationModeControlCapability = require("../core/capabilities/OperationModeControlCapability");
const ValetudoTimer = require("../entities/core/ValetudoTimer");
Expand All @@ -12,6 +13,7 @@ class TimerRouter {
* @param {object} options
* @param {import("../Configuration")} options.config
* @param {import("../core/ValetudoRobot")} options.robot
* @param {import("../scheduler/Scheduler")} options.scheduler
* @param {*} options.validator
*/
constructor(options) {
Expand All @@ -20,6 +22,7 @@ class TimerRouter {
this.config = options.config;
this.robot = options.robot;
this.validator = options.validator;
this.scheduler = options.scheduler;

this.initRoutes();
}
Expand Down Expand Up @@ -146,6 +149,26 @@ class TimerRouter {
}
});

this.router.put("/:id/action", this.validator, (req, res) => {
const storedTimers = this.config.get("timers");

if (storedTimers[req.params.id]) {
if (req.body.action === "execute_now") {
this.scheduler.executeTimer(storedTimers[req.params.id]).then(() => {
res.sendStatus(200);
}).catch((e) => {
Logger.error("Error while manually executing timer " + req.params.id, e);

res.sendStatus(500);
});
} else {
res.sendStatus(400);
}
} else {
res.sendStatus(404);
}
});

this.router.delete("/:id", (req, res) => {
const timers = this.config.get("timers");

Expand Down
3 changes: 2 additions & 1 deletion backend/lib/webserver/WebServer.js
Expand Up @@ -37,6 +37,7 @@ class WebServer {
* @param {import("../NetworkAdvertisementManager")} options.networkAdvertisementManager
* @param {import("../NTPClient")} options.ntpClient
* @param {import("../updater/Updater")} options.updater
* @param {import("../scheduler/Scheduler")} options.scheduler
* @param {import("../ValetudoEventStore")} options.valetudoEventStore
* @param {import("../Configuration")} options.config
* @param {import("../utils/ValetudoHelper")} options.valetudoHelper
Expand Down Expand Up @@ -132,7 +133,7 @@ class WebServer {

this.app.use("/api/v2/ntpclient/", new NTPClientRouter({config: this.config, ntpClient: options.ntpClient, validator: this.validator}).getRouter());

this.app.use("/api/v2/timers/", new TimerRouter({config: this.config, robot: this.robot, validator: this.validator}).getRouter());
this.app.use("/api/v2/timers/", new TimerRouter({config: this.config, robot: this.robot, validator: this.validator, scheduler: options.scheduler}).getRouter());

this.app.use("/api/v2/system/", new SystemRouter({}).getRouter());

Expand Down
47 changes: 47 additions & 0 deletions backend/lib/webserver/doc/TimerRouter.openapi.json
Expand Up @@ -174,6 +174,53 @@
}
}
},
"/api/v2/timers/{id}/action": {
"put": {
"tags": [
"Timers"
],
"summary": "Execute an action on an existing timer",
"parameters": [
{
"in": "path",
"name": "id",
"required": true,
"description": "Timer UUID",
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": [
"execute_now"
]
}
}
}
}
}
},
"responses": {
"200": {
"$ref": "#/components/responses/200"
},
"400": {
"$ref": "#/components/responses/400"
},
"404": {
"$ref": "#/components/responses/404"
}
}
}
},
"/api/v2/timers/properties": {
"get": {
"tags": [
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/api/client.ts
Expand Up @@ -599,6 +599,16 @@ export const sendTimerUpdate = async (timerData: Timer): Promise<void> => {
});
};

export const sendTimerAction = async (timerId: string, timerAction: "execute_now"): Promise<void> => {
await valetudoAPI
.put(`/timers/${timerId}/action`, {action: timerAction})
.then(({ status }) => {
if (status !== 200) {
throw new Error("Could not send timer action");
}
});
};

export const fetchTimerProperties = async (): Promise<TimerProperties> => {
return valetudoAPI
.get<TimerProperties>("/timers/properties")
Expand Down
11 changes: 11 additions & 0 deletions frontend/src/api/hooks.ts
Expand Up @@ -105,6 +105,7 @@ import {
fetchValetudoCustomizations,
sendValetudoCustomizations,
fetchConsumableProperties,
sendTimerAction,
} from "./client";
import {
PresetSelectionState,
Expand Down Expand Up @@ -782,6 +783,16 @@ export const useTimerModificationMutation = () => {
);
};

export const useTimerActionMutation = () => {
return useValetudoFetchingMutation(
useOnSettingsChangeError("Timer"),
CacheKey.Timers,
(params: {timerId: string, timerAction: "execute_now"}) => {
return sendTimerAction(params.timerId, params.timerAction).then(fetchTimerInformation);
}
);
};

export const useTimerDeletionMutation = () => {
return useValetudoFetchingMutation(
useOnSettingsChangeError("Timer"),
Expand Down
46 changes: 45 additions & 1 deletion frontend/src/valetudo/timers/TimerCard.tsx
Expand Up @@ -15,7 +15,7 @@ import {
IconButton,
Typography,
} from "@mui/material";
import { Delete as DeleteIcon, Edit as EditIcon } from "@mui/icons-material";
import { Delete as DeleteIcon, Edit as EditIcon, PlayArrow as ExecNowIcon } from "@mui/icons-material";
import React, { FunctionComponent } from "react";
import {Timer, TimerProperties, ValetudoTimerActionType} from "../../api";
import TimerEditDialog from "./TimerEditDialog";
Expand Down Expand Up @@ -57,6 +57,7 @@ type TimerCardProps = {
timerProperties: TimerProperties;
onSave: (newProps: Timer) => void;
onDelete: () => void;
onExecNow: () => void;
};

export const timerActionLabels: Record<ValetudoTimerActionType, string> = {
Expand All @@ -69,9 +70,11 @@ const TimerCard: FunctionComponent<TimerCardProps> = ({
timerProperties,
onSave,
onDelete,
onExecNow
}): JSX.Element => {
const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false);
const [editDialogOpen, setEditDialogOpen] = React.useState(false);
const [execNowDialogOpen, setExecNowDialogOpen] = React.useState(false);
const timerInLocalTime = React.useMemo(() => {
return convertTimer(timer, new Date().getTimezoneOffset() * -1);
}, [timer]);
Expand Down Expand Up @@ -173,6 +176,14 @@ const TimerCard: FunctionComponent<TimerCardProps> = ({
>
<EditIcon />
</IconButton>
<IconButton
onClick={() => {
setExecNowDialogOpen(true);
}}
color="success"
>
<ExecNowIcon />
</IconButton>
</Grid>
</Grid>

Expand Down Expand Up @@ -229,6 +240,39 @@ const TimerCard: FunctionComponent<TimerCardProps> = ({
}}
timerProperties={timerProperties}
/>

<Dialog
open={execNowDialogOpen}
onClose={() => {
setExecNowDialogOpen(false);
}}
>
<DialogTitle>Execute timer?</DialogTitle>
<DialogContent>
<DialogContentText>
Do you really want to execute this timer right now?
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
onClick={() => {
setExecNowDialogOpen(false);
}}
>
No
</Button>
<Button
onClick={() => {
setExecNowDialogOpen(false);
onExecNow();
}}
autoFocus
>
Yes
</Button>
</DialogActions>
</Dialog>

</CardContent>
</Card>
);
Expand Down
8 changes: 7 additions & 1 deletion frontend/src/valetudo/timers/Timers.tsx
Expand Up @@ -10,6 +10,7 @@ import {
Timer,
TimerInformation,
TimerProperties,
useTimerActionMutation,
useTimerCreationMutation,
useTimerDeletionMutation,
useTimerInfoQuery,
Expand Down Expand Up @@ -55,6 +56,7 @@ const Timers = (): JSX.Element => {
const { mutate: createTimer } = useTimerCreationMutation();
const { mutate: modifyTimer } = useTimerModificationMutation();
const { mutate: deleteTimer } = useTimerDeletionMutation();
const { mutate: execTimerAction } = useTimerActionMutation();

const [addTimerDialogOpen, setAddTimerDialogOpen] = React.useState(false);
const [helpDialogOpen, setHelpDialogOpen] = React.useState(false);
Expand All @@ -73,19 +75,23 @@ const Timers = (): JSX.Element => {
const onSave = (timer: Timer) => {
modifyTimer(convertTimer(timer, new Date().getTimezoneOffset()));
};
const onExecNow = () => {
execTimerAction({timerId: id, timerAction: "execute_now"});
};

return (
<Grid item key={id}>
<TimerCard
onDelete={onDelete}
onSave={onSave}
onExecNow={onExecNow}
timerProperties={timerPropertiesData as TimerProperties}
timer={timer}
/>
</Grid>
);
});
}, [modifyTimer, deleteTimer, timerPropertiesData, timerData]);
}, [modifyTimer, deleteTimer, execTimerAction, timerPropertiesData, timerData]);

const addTimer = React.useCallback(() => {
if (!timerPropertiesData) {
Expand Down

0 comments on commit a5f920e

Please sign in to comment.