Add portion controls and improve recipe display

This commit is contained in:
2026-08-03 15:45:34 -05:00
parent 139eb9bca2
commit f94b08a4e6
10 changed files with 276 additions and 28 deletions
+54 -8
View File
@@ -4,11 +4,12 @@ const express = require("express");
const NodeHelper = require("node_helper");
const QRCode = require("qrcode");
const NextcloudPublicShare = require("./lib/nextcloud-public-share");
const { parseYield, scaleRecipe } = require("./lib/recipe-scaler");
module.exports = NodeHelper.create({
start() {
this.config = { controlPath: "/MMM-NextcloudCookbook/control" };
this.state = { active: false, recipeId: null, recipe: null, error: null };
this.state = { active: false, recipeId: null, recipe: null, portions: null, error: null };
this.recipeCache = { expires: 0, recipes: [] };
this.stateFile = path.join(process.cwd(), "config", "MMM-NextcloudCookbook-state.json");
this.configureClient();
@@ -19,6 +20,12 @@ module.exports = NodeHelper.create({
socketNotificationReceived(notification, payload) {
if (notification !== "CONFIG") return;
this.config = { ...this.config, ...payload };
try {
this.configureClient();
} catch (error) {
this.setError(error);
return;
}
this.stateReady.then(async () => {
this.sendSocketNotification("STATE", this.publicState());
await this.restoreRecipe();
@@ -26,13 +33,17 @@ module.exports = NodeHelper.create({
},
configureClient() {
const shareUrl = process.env.SECRET_NEXTCLOUD_COOKBOOK_SHARE_URL;
const shareUrl = process.env.SECRET_NEXTCLOUD_COOKBOOK_SHARE_URL || this.config.shareUrl;
const password = process.env.SECRET_NEXTCLOUD_COOKBOOK_SHARE_PASSWORD
|| this.config.sharePassword
|| "";
this.client = shareUrl
? new NextcloudPublicShare({
shareUrl,
password: process.env.SECRET_NEXTCLOUD_COOKBOOK_SHARE_PASSWORD || ""
password
})
: null;
this.recipeCache = { expires: 0, recipes: [] };
},
registerRoutes() {
@@ -69,7 +80,30 @@ module.exports = NodeHelper.create({
return response.status(400).json({ error: "recipeId is required" });
}
const recipe = await this.getClient().getRecipe(recipeId);
this.state = { active: true, recipeId, recipe, error: null };
this.state = {
active: true,
recipeId,
recipe,
portions: Math.max(1, Math.round(parseYield(recipe.recipeYield) || 1)),
error: null
};
await this.saveState();
this.sendSocketNotification("STATE", this.publicState());
return response.json(this.publicState());
} catch (error) {
return next(error);
}
});
this.expressApp.post("/MMM-NextcloudCookbook/api/portions", async (request, response, next) => {
try {
const delta = request.body?.delta;
if (!this.state.active || !this.state.recipe) {
return response.status(409).json({ error: "No recipe is active" });
}
if (delta !== -1 && delta !== 1) {
return response.status(400).json({ error: "delta must be -1 or 1" });
}
this.state.portions = Math.max(1, this.state.portions + delta);
await this.saveState();
this.sendSocketNotification("STATE", this.publicState());
return response.json(this.publicState());
@@ -79,7 +113,7 @@ module.exports = NodeHelper.create({
});
this.expressApp.post("/MMM-NextcloudCookbook/api/exit", async (_request, response, next) => {
try {
this.state = { active: false, recipeId: null, recipe: null, error: null };
this.state = { active: false, recipeId: null, recipe: null, portions: null, error: null };
await this.saveState();
this.sendSocketNotification("STATE", this.publicState());
response.json(this.publicState());
@@ -109,7 +143,13 @@ module.exports = NodeHelper.create({
try {
const persisted = JSON.parse(await fs.readFile(this.stateFile, "utf8"));
if (!persisted.active || !persisted.recipeId) return;
this.state = { active: true, recipeId: persisted.recipeId, recipe: null, error: null };
this.state = {
active: true,
recipeId: persisted.recipeId,
recipe: null,
portions: Number.isInteger(persisted.portions) ? persisted.portions : null,
error: null
};
} catch (error) {
if (error.code !== "ENOENT") throw error;
}
@@ -119,6 +159,7 @@ module.exports = NodeHelper.create({
if (!this.state.active || !this.state.recipeId) return;
try {
this.state.recipe = await this.getClient().getRecipe(this.state.recipeId);
this.state.portions ||= Math.max(1, Math.round(parseYield(this.state.recipe.recipeYield) || 1));
this.state.error = null;
} catch (error) {
this.setError(error);
@@ -128,7 +169,11 @@ module.exports = NodeHelper.create({
async saveState() {
const temporary = `${this.stateFile}.tmp`;
await fs.writeFile(temporary, `${JSON.stringify({ active: this.state.active, recipeId: this.state.recipeId })}\n`, {
await fs.writeFile(temporary, `${JSON.stringify({
active: this.state.active,
recipeId: this.state.recipeId,
portions: this.state.portions
})}\n`, {
mode: 0o600
});
await fs.rename(temporary, this.stateFile);
@@ -143,7 +188,8 @@ module.exports = NodeHelper.create({
return {
active: this.state.active,
recipeId: this.state.recipeId,
recipe: this.state.recipe,
recipe: this.state.recipe ? scaleRecipe(this.state.recipe, this.state.portions) : null,
portions: this.state.portions,
error: this.state.error
};
}