import { deepEqual } from 'fast-equals'; declare var PUBLIC_URL: string; console.log("Url is " + PUBLIC_URL); document.body.innerHTML = require('./main.html') as string; import { TimeView } from "./timeview"; import { PlantViews } from "./plant"; import { NetworkConfigView } from "./network"; import { NightLampView } from "./nightlightview"; import { TankConfigView } from "./tankview"; import { SubmitView } from "./submitView"; import { ProgressView } from "./progress"; import { OTAView } from "./ota"; import { BatteryView } from "./batteryview"; import { FileView } from './fileview'; import { LogView } from './log'; import {HardwareConfigView} from "./hardware"; import { BackupHeader, BatteryState, GetTime, LogArray, LogLocalisation, Moistures, NightLampCommand, PlantControllerConfig, SetTime, SSIDList, TankInfo, TestPump, VersionInfo, FileList } from "./api"; export class Controller { loadTankInfo() : Promise { return fetch(PUBLIC_URL + "/tank") .then(response => response.json()) .then(json => json as TankInfo) .then(tankinfo => { controller.tankView.setTankInfo(tankinfo) }) .catch(error => { console.log(error); }); } loadLogLocaleConfig() { return fetch(PUBLIC_URL + "/log_localization") .then(response => response.json()) .then(json => json as LogLocalisation) .then(loglocale => { controller.logView.setLogLocalisation(loglocale) }) .catch(error => { console.log(error); }); } loadLog() { return fetch(PUBLIC_URL + "/log") .then(response => response.json()) .then(json => json as LogArray) .then(logs => { controller.logView.setLog(logs) }) .catch(error => { console.log(error); }); } getBackupInfo() : Promise { return fetch(PUBLIC_URL + "/backup_info") .then(response => response.json()) .then(json => json as BackupHeader) .then(header => { controller.submitView.setBackupInfo(header) }) .catch(error => { console.log(error); }); } populateTimezones(): Promise { return fetch(PUBLIC_URL+'/timezones') .then(response => response.json()) .then(json => json as string[]) .then(timezones => { controller.timeView.timezones(timezones) }) .catch(error => console.error('Error fetching timezones:', error)); } updateFileList() : Promise { return fetch(PUBLIC_URL + "/files") .then(response => response.json()) .then(json => json as FileList) .then(filelist => { controller.fileview.setFileList(filelist, PUBLIC_URL) }) .catch(error => { console.log(error); }); } uploadFile(file: File, name:string) { var current = 0; var max = 100; controller.progressview.addProgress("file_upload", (current / max) * 100, "Uploading File " + name + "(" + current + "/" + max + ")") var ajax = new XMLHttpRequest(); ajax.upload.addEventListener("progress", event => { current = event.loaded / 1000; max = event.total / 1000; controller.progressview.addProgress("file_upload", (current / max) * 100, "Uploading File " + name + "(" + current + "/" + max + ")") }, false); ajax.addEventListener("load", () => { controller.progressview.removeProgress("file_upload") controller.updateFileList() }, false); ajax.addEventListener("error", () => { alert("Error upload") controller.progressview.removeProgress("file_upload") controller.updateFileList() }, false); ajax.addEventListener("abort", () => { alert("abort upload") controller.progressview.removeProgress("file_upload") controller.updateFileList() }, false); ajax.open("POST", PUBLIC_URL + "/file?filename="+name); ajax.send(file); } deleteFile(name:string) { controller.progressview.addIndeterminate("file_delete", "Deleting " + name); var ajax = new XMLHttpRequest(); ajax.open("DELETE", PUBLIC_URL + "/file?filename="+name); ajax.send(); ajax.addEventListener("error", () => { controller.progressview.removeProgress("file_delete") alert("Error delete") controller.updateFileList() }, false); ajax.addEventListener("abort", () => { controller.progressview.removeProgress("file_delete") alert("Error upload") controller.updateFileList() }, false); ajax.addEventListener("load", () => { controller.progressview.removeProgress("file_delete") controller.updateFileList() }, false); controller.updateFileList() } updateRTCData() : Promise { return fetch(PUBLIC_URL + "/time") .then(response => response.json()) .then(json => json as GetTime) .then(time => { controller.timeView.update(time.native, time.rtc) }) .catch(error => { controller.timeView.update("n/a", "n/a") console.log(error); }); } updateBatteryData(): Promise { return fetch(PUBLIC_URL + "/battery") .then(response => response.json()) .then(json => json as BatteryState) .then(battery => { controller.batteryView.update(battery) }) .catch(error => { controller.batteryView.update(null) console.log(error); }) } uploadNewFirmware(file: File) { var current = 0; var max = 100; controller.progressview.addProgress("ota_upload", (current / max) * 100, "Uploading firmeware (" + current + "/" + max + ")") var ajax = new XMLHttpRequest(); ajax.upload.addEventListener("progress", event => { current = event.loaded / 1000; max = event.total / 1000; controller.progressview.addProgress("ota_upload", (current / max) * 100, "Uploading firmeware (" + current + "/" + max + ")") }, false); ajax.addEventListener("load", () => { controller.progressview.removeProgress("ota_upload") controller.reboot(); }, false); ajax.addEventListener("error", () => { alert("Error ota") controller.progressview.removeProgress("ota_upload") }, false); ajax.addEventListener("abort", () => { alert("abort ota") controller.progressview.removeProgress("ota_upload") }, false); ajax.open("POST", PUBLIC_URL + "/ota"); ajax.send(file); } version() : Promise { controller.progressview.addIndeterminate("version", "Getting buildVersion") return fetch(PUBLIC_URL + "/version") .then(response => response.json()) .then(json => json as VersionInfo) .then(versionInfo => { controller.progressview.removeProgress("version") controller.firmWareView.setVersion(versionInfo); }) } getBackupConfig() { controller.progressview.addIndeterminate("get_backup_config", "Downloading Backup") fetch(PUBLIC_URL + "/get_backup_config") .then(response => response.text()) .then(loaded => { controller.progressview.removeProgress("get_backup_config") controller.submitView.setBackupJson(loaded); }) } async downloadConfig(): Promise { controller.progressview.addIndeterminate("get_config", "Downloading Config") const response = await fetch(PUBLIC_URL + "/get_config"); const loaded = await response.json(); var currentConfig = loaded as PlantControllerConfig; controller.setInitialConfig(currentConfig); controller.setConfig(currentConfig); //sync json view initially controller.configChanged(); controller.progressview.removeProgress("get_config"); } setInitialConfig(currentConfig: PlantControllerConfig) { this.initialConfig = currentConfig } uploadConfig(json: string, statusCallback: (status: string) => void) { controller.progressview.addIndeterminate("set_config", "Uploading Config") fetch(PUBLIC_URL + "/set_config", { method: "POST", body: json, }) .then(response => response.text()) .then(text => statusCallback(text)) controller.progressview.removeProgress("set_config") //load from remote to be clean controller.downloadConfig() } backupConfig(json: string, statusCallback: (status: string) => void) { controller.progressview.addIndeterminate("backup_config", "Backingup Config") fetch(PUBLIC_URL + "/backup_config", { method: "POST", body: json, }) .then(response => response.text()) .then(text => statusCallback(text)) controller.progressview.removeProgress("backup_config") } syncRTCFromBrowser() { controller.progressview.addIndeterminate("write_rtc", "Writing RTC") var value: SetTime = { time: new Date().toISOString() } var pretty = JSON.stringify(value, undefined, 1); fetch(PUBLIC_URL + "/time", { method: "POST", body: pretty }).then( _ => controller.progressview.removeProgress("write_rtc") ) } configChanged() { const current = controller.getConfig(); var pretty = JSON.stringify(current, undefined, 0); controller.submitView.setJson(pretty); if (deepEqual(current, controller.initialConfig)) { document.title = "PlantCtrl" } else { document.title = "*PlantCtrl" } } selfTest(){ fetch(PUBLIC_URL + "/boardtest", { method: "POST" }) } testNightLamp(active: boolean){ var body: NightLampCommand = { active: active } var pretty = JSON.stringify(body, undefined, 1); fetch(PUBLIC_URL + "/lamptest", { method: "POST", body: pretty }) } testPlant(plantId: number) { let counter = 0 let limit = 30 controller.progressview.addProgress("test_pump", counter / limit * 100, "Testing pump " + (plantId + 1) + " for " + (limit - counter) + "s") let timerId: string | number | NodeJS.Timeout | undefined function updateProgress() { counter++; controller.progressview.addProgress("test_pump", counter / limit * 100, "Testing pump " + (plantId + 1) + " for " + (limit - counter) + "s") timerId = setTimeout(updateProgress, 1000); } timerId = setTimeout(updateProgress, 1000); var body: TestPump = { pump: plantId } var pretty = JSON.stringify(body, undefined, 1); fetch(PUBLIC_URL + "/pumptest", { method: "POST", body: pretty }) .then(response => response.text()) .then( _ => { clearTimeout(timerId); controller.progressview.removeProgress("test_pump"); } ) } getConfig(): PlantControllerConfig { return { hardware: controller.hardwareView.getConfig(), network: controller.networkView.getConfig(), tank: controller.tankView.getConfig(), night_lamp: controller.nightLampView.getConfig(), plants: controller.plantViews.getConfig(), timezone: controller.timeView.getTimeZone() } } scanWifi() { let counter = 0 let limit = 5 controller.progressview.addProgress("scan_ssid", counter / limit * 100, "Scanning for SSIDs for " + (limit - counter) + "s") let timerId: string | number | NodeJS.Timeout | undefined function updateProgress() { counter++; controller.progressview.addProgress("scan_ssid", counter / limit * 100, "Scanning for SSIDs for " + (limit - counter) + "s") timerId = setTimeout(updateProgress, 1000); } timerId = setTimeout(updateProgress, 1000); var ajax = new XMLHttpRequest(); ajax.responseType = 'json'; ajax.onreadystatechange = () => { if (ajax.readyState === 4) { clearTimeout(timerId); controller.progressview.removeProgress("scan_ssid"); this.networkView.setScanResult(ajax.response as SSIDList) } }; ajax.onerror = (evt) => { clearTimeout(timerId); controller.progressview.removeProgress("scan_ssid"); alert("Failed to start see console") } ajax.open("POST", PUBLIC_URL + "/wifiscan"); ajax.send(); } setConfig(current: PlantControllerConfig) { this.tankView.setConfig(current.tank); this.networkView.setConfig(current.network); this.nightLampView.setConfig(current.night_lamp); this.plantViews.setConfig(current.plants); this.timeView.setTimeZone(current.timezone); this.hardwareView.setConfig(current.hardware); } measure_moisture() { let counter = 0 let limit = 2 controller.progressview.addProgress("measure_moisture", counter / limit * 100, "Measure Moisture " + (limit - counter) + "s") let timerId: string | number | NodeJS.Timeout | undefined function updateProgress() { counter++; controller.progressview.addProgress("measure_moisture", counter / limit * 100, "Measure Moisture " + (limit - counter) + "s") timerId = setTimeout(updateProgress, 1000); } timerId = setTimeout(updateProgress, 1000); fetch(PUBLIC_URL + "/moisture") .then(response => response.json()) .then(json => json as Moistures) .then(time => { controller.plantViews.update(time.moisture_a, time.moisture_b) clearTimeout(timerId); controller.progressview.removeProgress("measure_moisture"); }) .catch(error => { clearTimeout(timerId); controller.progressview.removeProgress("measure_moisture"); console.log(error); }); } exit() { fetch(PUBLIC_URL + "/exit", { method: "POST", }) controller.progressview.addIndeterminate("rebooting", "Returned to normal mode, you can close this site now") } waitForReboot() { console.log("Check if controller online again") fetch(PUBLIC_URL + "/version", { method: "GET", signal: AbortSignal.timeout(5000) }).then(response => { if (response.status != 200){ console.log("Not reached yet, retrying") setTimeout(controller.waitForReboot, 1000) } else { console.log("Reached controller, reloading") controller.progressview.addIndeterminate("rebooting", "Reached Controller, reloading") setTimeout(function(){ window.location.reload() }, 2000); } }) .catch(err => { console.log("Not reached yet, retrying") setTimeout(controller.waitForReboot, 1000) }) } reboot() { fetch(PUBLIC_URL + "/reboot", { method: "POST", }) controller.progressview.addIndeterminate("rebooting", "Rebooting") setTimeout(this.waitForReboot, 1000) } initialConfig: PlantControllerConfig | null = null readonly rebootBtn: HTMLButtonElement readonly exitBtn: HTMLButtonElement readonly timeView: TimeView; readonly plantViews: PlantViews; readonly networkView: NetworkConfigView; readonly hardwareView: HardwareConfigView; readonly tankView: TankConfigView; readonly nightLampView: NightLampView; readonly submitView: SubmitView; readonly firmWareView: OTAView; readonly progressview: ProgressView; readonly batteryView: BatteryView; readonly fileview: FileView; readonly logView: LogView constructor() { this.timeView = new TimeView(this) this.plantViews = new PlantViews(this) this.networkView = new NetworkConfigView(this, PUBLIC_URL) this.tankView = new TankConfigView(this) this.batteryView = new BatteryView(this) this.nightLampView = new NightLampView(this) this.submitView = new SubmitView(this) this.firmWareView = new OTAView(this) this.progressview = new ProgressView(this) this.fileview = new FileView(this) this.logView = new LogView(this) this.hardwareView = new HardwareConfigView(this) this.rebootBtn = document.getElementById("reboot") as HTMLButtonElement this.rebootBtn.onclick = () => { controller.reboot(); } this.exitBtn = document.getElementById("exit") as HTMLButtonElement this.exitBtn.onclick = () => { controller.exit(); } } selftest() { } } const controller = new Controller(); controller.progressview.removeProgress("rebooting"); const tasks = [ { task: controller.populateTimezones, displayString: "Populating Timezones" }, { task: controller.updateRTCData, displayString: "Updating RTC Data" }, { task: controller.updateBatteryData, displayString: "Updating Battery Data" }, { task: controller.downloadConfig, displayString: "Downloading Configuration" }, { task: controller.version, displayString: "Fetching Version Information" }, { task: controller.updateFileList, displayString: "Updating File List" }, { task: controller.getBackupInfo, displayString: "Fetching Backup Information" }, { task: controller.loadLogLocaleConfig, displayString: "Loading Log Localization Config" }, { task: controller.loadTankInfo, displayString: "Loading Tank Information" }, ]; async function executeTasksSequentially() { let current = 0; for (const { task, displayString } of tasks) { current++; let ratio = current / tasks.length; controller.progressview.addProgress("initial", ratio * 100, displayString); try { await task(); } catch (error) { console.error(`Error executing task '${displayString}':`, error); // Optionally, you can decide whether to continue or break on errors break; } } } executeTasksSequentially().then(r => { controller.progressview.removeProgress("initial") }); controller.progressview.removeProgress("rebooting"); window.addEventListener("beforeunload", (event) => { const currentConfig = controller.getConfig(); // Check if the current state differs from the initial configuration if (!deepEqual(currentConfig, controller.initialConfig)) { const confirmationMessage = "You have unsaved changes. Are you sure you want to leave this page?"; // Standard behavior for displaying the confirmation dialog event.preventDefault(); event.returnValue = confirmationMessage; // This will trigger the browser's default dialog return confirmationMessage; } });