itender/src/iTender.ts
2023-02-06 23:20:35 +01:00

340 lines
12 KiB
TypeScript

import {iTenderStatus} from "./iTenderStatus";
import Container from "./database/Container";
import {IContainer} from "./database/IContainer";
import Drink from "./database/Drink";
import {IDrink} from "./database/IDrink";
import debug from "debug";
import {WebSocketHandler} from "./WebSocketHandler";
import {IJob} from "./database/IJob";
import {Utils} from "./Utils";
import {WebSocketPayload} from "./WebSocketPayload";
import {WebSocketEvent} from "./WebSocketEvent";
import Job from "./database/Job";
import {IIngredient} from "./database/IIngredient";
import Ingredient from "./database/Ingredient";
import {RejectReason} from "./RejectReason";
import axios from "axios";
import {Mixer} from "./Mixer";
const log = debug("itender:station");
const mixLog = debug("itender:mixer");
/**
* The main class of the itender, here a located all main features of the system, like starting pumps, firing events and stuff
*/
export class iTender {
/**
* How many seconds it takes to fill 100ml
* @private
*/
static secondsPer100ml: number = 59.993; // 35.3335
/**
* Sensitivity-Factor of the hx711 scales
* Calculated by (Value /100) Value needs to be measured by hx711 when a known weight is on the sensor,
* in this example its 100g, so divide it by 100
*/
static sensitivityFactor: number = 1.0;
/**
* Current internal status of itender
* @private
*/
private static _status: iTenderStatus = iTenderStatus.STARTING;
/**
* Returns the internal status of itender app
*/
static get status(): iTenderStatus {
return this._status;
}
/**
* Current internal connection-status boolean
* @private
*/
private static _internetConnection: boolean = false;
/**
* Returns true if internet connection is active
*/
static get internetConnection(): boolean {
return this._internetConnection;
}
/**
* Drinks in cache
* @private
*/
private static _drinks: IDrink[];
/**
* Retrieve all drinks in cache
*/
static get drinks(): IDrink[] {
return this._drinks;
}
/**
* Sets the current itender status and sends it to the client
* @param status
*/
static setStatus(status: iTenderStatus) {
this._status = status;
if (WebSocketHandler.ws && WebSocketHandler.ws.readyState == 1)
WebSocketHandler.sendStatus().then().catch(console.error);
log("Status is now " + status);
}
/**
* This method is fired if the user likes to mix a drink
* It starts to calculate the ingredients and amounts of each ingredient
* also calculates the amount of time, the drink will need to be done
* @param data
*/
static onReceiveFill(data: { drink: IDrink, amounts?: { ingredient: String, amount: number }[], amount?: number }): Promise<IJob> {
return new Promise(async (resolve, reject) => {
mixLog("Receiving fill");
let drink = await Drink.findById(data.drink).populate("ingredients.type");
if (!drink) {
reject();
return;
}
const job = new Job();
console.debug(data.drink, data.amount);
let amounts: { ingredient: IIngredient, amount: number, container?: IContainer }[] = [];
job.completeAmount = 0;
if (data.amounts) {
for (let x of data.amounts) {
let ingredient = await Ingredient.findById(x.ingredient);
if (!ingredient) continue;
amounts.push({ingredient: ingredient, amount: x.amount});
job.completeAmount += x.amount;
}
} else if (data.amount) {
let sum = 0;
for (let x of (drink.ingredients as { type: IIngredient, amount: number }[])) {
sum += x.amount;
}
let factor = sum / data.amount;
for (let x of (drink.ingredients as { type: IIngredient, amount: number }[])) {
amounts.push({ingredient: x.type, amount: x.amount / factor})
job.completeAmount += x.amount / factor;
}
} else {
for (let x of (drink.ingredients as { type: IIngredient, amount: number }[])) {
amounts.push({ingredient: x.type, amount: x.amount});
job.completeAmount += x.amount;
}
}
job.estimatedTime = 0;
let tolerance = 3;
for (let x of amounts) {
let c = await Container.findOne({$and: [{enabled: true}, {filled: {$gt: x.amount + tolerance}}, {content: x.ingredient}]});
if (!c) {
mixLog("Not enough ingredients!");
reject(RejectReason.NOT_ENOUGH_INGREDIENTS);
return;
}
x.container = c;
let estimated = iTender.secondsPer100ml / 100 * x.amount;
if (job.estimatedTime < estimated)
job.estimatedTime = estimated;
}
job.drink = drink
job.amounts = amounts as { ingredient: IIngredient, amount: number, container: IContainer }[];
if (job.estimatedTime < 0.5) {
job.estimatedTime = 1;
}
await job.save()
resolve(job);
await Mixer.startFill(job);
});
}
/**
* Refresh the drinks to the local variable
* Check which drinks can be done, based on the current container ingredients
*/
static refreshDrinks(): Promise<void> {
log("Refreshing drinks...");
return new Promise(async resolve => {
this._drinks = [];
for (let d of (await Drink.find().populate("ingredients.type"))) {
let drinkAccept = true;
for (let i of d.ingredients) {
let c = await Container.findOne({content: i.type});
if (!c) {
drinkAccept = false;
break;
}
}
if (drinkAccept) {
this._drinks.push(d);
}
}
resolve();
log("Drinks refreshed!");
await WebSocketHandler.sendDrinks();
});
}
/**
* Automatic checkup (all 30 seconds)
* When no current job is ongoing, skip
* if current job is active, but the start time was more than 2mins ago, the job gets canceled
*/
public static async autoCheckup() {
setInterval(async () => {
log("Auto Checkup");
if (!Mixer.currentJob)
return;
// Check if startedTime plus 2 mins smaller than now
if (Mixer.currentJob.startedAt.getTime() + 1000 * 60 * 2 <= Date.now()) {
// Job can be declared as stuck!
await Mixer.cancelFill();
}
}, 1000 * 30);
}
/**
* Checks the internet connection
*/
public static async checkNetwork() {
this._internetConnection = await Utils.checkInternet();
}
/**
* Refrehs drinks from the cloud (https://itender.iif.li)
*/
static refreshFromServer(): Promise<void> {
return new Promise(async (resolve, reject) => {
iTender.setStatus(iTenderStatus.DOWNLOADING)
log("Refreshing drinks from server...");
try {
const requestIngredients = await axios.get("https://itender.iif.li/api/ingredients");
let serverIngredients = requestIngredients.data as IIngredient[];
if (serverIngredients.length == 0) {
log("Got 0 ingredients from the server... aborting.");
throw new Error("Got 0 ingredients from the server, invalid");
}
log("Got " + serverIngredients.length + " ingredients from server");
let localIngredients = await Ingredient.find();
for (let local of localIngredients) {
let found = false;
for (let remote of serverIngredients) {
if (local.name == remote.name) {
found = true;
break;
}
}
if (!found) {
await Ingredient.deleteOne({"_id": local._id});
for (let c of (await Container.find({content: local._id}))) {
c.content = undefined;
c.save();
}
}
}
for (let remote of serverIngredients) {
let ingredient = await Ingredient.findById(remote._id);
if (!ingredient)
ingredient = new Ingredient();
ingredient._id = remote._id;
ingredient.name = remote.name;
await ingredient.save();
}
const requestDrinks = await axios.get("https://itender.iif.li/api/drinks");
let serverDrinks = requestDrinks.data as IDrink[];
if (serverDrinks.length == 0) {
log("Got 0 drinks from the server... aborting.");
throw new Error("Got 0 drinks from the server, invalid");
}
log("Got " + serverDrinks.length + " drinks from server");
let localDrinks = await Drink.find();
for (let local of localDrinks) {
let found = false;
for (let remote of serverDrinks) {
if (local.name == remote.name) {
found = true;
break;
}
}
if (!found) {
Utils.deleteImage(local._id);
await Drink.deleteOne({"_id": local._id});
}
}
for (let remote of serverDrinks) {
let drink = await Drink.findById(remote._id);
if (!drink)
drink = new Drink();
drink._id = remote._id;
drink.name = remote.name;
drink.ingredients = remote.ingredients;
await drink.save();
// Download thumbnail
if (!Utils.checkForImage(remote._id)) {
let url = "https://itender.iif.li/images/" + remote._id + ".png";
try {
await Utils.downloadImage(url, drink._id);
log("Drink " + remote.name + "'s Thumbnail downloaded");
} catch (e) {
log("Drink " + remote.name + " failed to download thumbnail! (" + url + ") | " + e);
}
}
}
} catch (e) {
console.error("Could not refresh drinks " + e);
await WebSocketHandler.send(new WebSocketPayload(WebSocketEvent.ERROR, "Beim Aktualisieren der Getränke ist ein Netzwerk-Fehler aufgetreten.<br>Bitte später erneut versuchen!"));
}
iTender.setStatus(iTenderStatus.READY);
resolve();
await iTender.refreshDrinks();
});
}
}