833 lines
27 KiB
C
833 lines
27 KiB
C
#pragma once
|
||
#include <Arduino.h>
|
||
|
||
const char index_html[] PROGMEM = R"rawliteral(
|
||
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>UPS Dashboard</title>
|
||
<link rel="icon" href="favicon.ico">
|
||
<style>
|
||
body {
|
||
font-family: Arial, sans-serif;
|
||
padding: 20px;
|
||
margin-top: -20px;
|
||
background-color: #121212; /* Dark background */
|
||
color: #e0e0e0; /* Light text color */
|
||
}
|
||
hr {
|
||
margin-bottom: 20px;
|
||
border-color: #333; /* Darker border color */
|
||
}
|
||
h1 {
|
||
font-size: 20px;
|
||
font-weight: bold;
|
||
margin-bottom: 10px;
|
||
color: #ffffff; /* White text for headings */
|
||
}
|
||
.form-group {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 15px;
|
||
}
|
||
button {
|
||
background-color: #333; /* Dark button background */
|
||
color: #e0e0e0; /* Light text color */
|
||
border: none;
|
||
padding: 5px 10px;
|
||
cursor: pointer;
|
||
border-radius: 4px;
|
||
}
|
||
button:hover {
|
||
background-color: #444; /* Slightly lighter on hover */
|
||
}
|
||
button.disabled {
|
||
background-color: #0046a3; /* Gray color for disabled button */
|
||
color: #a0a0a0;
|
||
cursor: default;
|
||
pointer-events: none; /* Prevents hover effect */
|
||
transition: none; /* Removes transition effect on hover */
|
||
}
|
||
|
||
button[disabled] {
|
||
background-color: #333; /* Gray color for disabled button */
|
||
color: #555;
|
||
cursor: default;
|
||
pointer-events: none; /* Prevents hover effect */
|
||
transition: none; /* Removes transition effect on hover */
|
||
}
|
||
button[disabled]:hover {
|
||
background-color: #333;
|
||
}
|
||
|
||
label {
|
||
color: #e0e0e0; /* Light text color for labels */
|
||
}
|
||
|
||
/* for styled on off using checkbox */
|
||
.toggle-switch {
|
||
position: relative;
|
||
display: inline-block;
|
||
width: 48px;
|
||
height: 24px;
|
||
}
|
||
.toggle-switch input {
|
||
opacity: 0;
|
||
width: 0;
|
||
height: 0;
|
||
}
|
||
.slider {
|
||
position: absolute;
|
||
cursor: pointer;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background-color: #ccc;
|
||
transition: .2s;
|
||
border-radius: 24px;
|
||
}
|
||
.slider:before {
|
||
position: absolute;
|
||
content: "";
|
||
height: 17px;
|
||
width: 17px;
|
||
left: 4px;
|
||
bottom: 4px;
|
||
background-color: white;
|
||
transition: .2s;
|
||
border-radius: 50%;
|
||
}
|
||
input:checked + .slider {
|
||
background-color: #0078d7;
|
||
}
|
||
input:checked + .slider:before {
|
||
transform: translateX(22px);
|
||
}
|
||
/* Disabled state */
|
||
input:disabled + .slider {
|
||
background-color: #e6e6e6;
|
||
cursor: default;
|
||
pointer-events: none; /* Prevents hover effect */
|
||
}
|
||
input:disabled:checked + .slider {
|
||
background-color: #a0d1f6;
|
||
}
|
||
input:disabled + .slider:before {
|
||
background-color: #f5f5f5;
|
||
}
|
||
/* Optional: Add focus style */
|
||
input:focus + .slider {
|
||
box-shadow: 0 0 1px #0078d7;
|
||
}
|
||
|
||
/* Selec/dropdown style */
|
||
select {
|
||
/* width: calc(100% - 22px); */
|
||
background-color: #333333;
|
||
border: none;
|
||
color: #ffffff;
|
||
font-size: 16px;
|
||
border-radius: 5px;
|
||
box-sizing: border-box;
|
||
margin-right: 5px;
|
||
}
|
||
|
||
/* Login Overlay Styles */
|
||
#login-overlay {
|
||
position: fixed;
|
||
top: 0; left: 0; right: 0; bottom: 0;
|
||
background-color: rgba(18, 18, 18, 0.95);
|
||
z-index: 9999;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.login-box {
|
||
background-color: #242424;
|
||
padding: 30px;
|
||
border-radius: 8px;
|
||
box-shadow: 0 4px 15px rgba(0,0,0,0.5);
|
||
text-align: center;
|
||
width: 300px;
|
||
}
|
||
.login-box input {
|
||
padding: 10px;
|
||
margin-top: 10px;
|
||
margin-bottom: 20px;
|
||
width: 80%;
|
||
border: 1px solid #444;
|
||
background-color: #121212;
|
||
color: #fff;
|
||
border-radius: 4px;
|
||
}
|
||
.login-box h2 {
|
||
margin-top: 0;
|
||
color: #e0e0e0;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<!-- Login Overlay -->
|
||
<div id="login-overlay">
|
||
<div class="login-box" onkeydown="if(event.key==='Enter')attemptLogin()">
|
||
<h2>UPS Authentication</h2>
|
||
<input type="text" id="login-username" placeholder="Username (admin)" value="admin">
|
||
<input type="password" id="login-password" placeholder="Password">
|
||
<br>
|
||
<button onclick="attemptLogin()" style="width: 80%; padding: 10px;">Login</button>
|
||
<p id="login-error" style="color: #ff5555; display: none; margin-top: 15px; font-size: 14px;">Invalid username or password</p>
|
||
</div>
|
||
</div>
|
||
|
||
<h1>
|
||
<label>Smart Network Controlled UPS V1.1</label>
|
||
<!-- ⭕Hollow Red Circle⚫Black Circle⚪White Circle · 🟢Green Circle🔵Blue Circle🟡Yellow Circle🟠Orange Circle🟤Brown Circle 🔴Red Circle 🟣 Purple Circle -->
|
||
<label id="connect-state-label" title="Unavailable"> ⚫</label>
|
||
<button id="logoutBtn" onclick="logout()" style="position: absolute; right: 20px; top: 20px; padding: 4px 12px; font-size: 12px;">Logout</button>
|
||
</h1>
|
||
|
||
<hr>
|
||
<h1>Power Control:</h1>
|
||
|
||
<div class="form-group">
|
||
<label>Auto ON</label>
|
||
<!-- <button id="toggleAutoBtn"
|
||
onclick="toggleAutoOn()"
|
||
style="position: absolute;
|
||
left: 140px;width:50px;">OFF</button> -->
|
||
<select id="auto-mode" style="position: absolute; left: 150px;width: 150px;">
|
||
<option value="0">Disabled</option>
|
||
<option value="1">Line Powered</option>
|
||
<option value="2">Persistent on Line</option>
|
||
</select>
|
||
</div>
|
||
<!-- <div class="form-group">
|
||
<label>State</label>
|
||
<button id="switchBtn"
|
||
onclick="toggleSwitch()"
|
||
style="position: absolute;
|
||
left: 140px;width:50px;">OFF</button>
|
||
</div> -->
|
||
|
||
<div class="form-group">
|
||
<label>Power</label>
|
||
<label class="toggle-switch" style="position: absolute; left: 150px;">
|
||
<input type="checkbox" id="switchToggle" disabled >
|
||
<span class="slider"></span>
|
||
</label>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>Shutdown Cast</label>
|
||
<select id="shutcast-mode" style="position: absolute; left: 150px;width: 150px;">
|
||
<option value="0">Disabled</option>
|
||
<option value="1">On Battery</option>
|
||
<option value="2">≤75% Battery</option>
|
||
<option value="3">≤50% Battery</option>
|
||
<option value="4">≤25% Battery</option>
|
||
<option value="5">Battery Critical</option>
|
||
</select>
|
||
<label id="shutdown-cast-label" style="position: absolute; left: 310px;width: 50px;"> </label>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<div class="form-group">
|
||
<div class="container">
|
||
<label style="position: absolute; align-items: center;" title="RestAPI: htttp://<IP>/state?shutdown=<seconds>" >Shutdown</label>
|
||
<label id="shutdown-countdown" style="position: absolute; left: 150px; width: 150px;">999</label>
|
||
</div>
|
||
|
||
<div class="container" id="shutdown-form" style="position: absolute; left: 150px; align-items: center;">
|
||
<input type="number" id="shut-timer-input" placeholder="0-999" value="120" min="10" max="999" step="1" style="position: absolute; left: 0px; width: 50px; align-items: center;" maxlength="3">
|
||
<label style="position: absolute; left: 65px; width: 50px; align-items: center;">sec(s)</label>
|
||
<button id="shutdownBtn"
|
||
onclick="shutdownTriggered()"
|
||
style="position: absolute;
|
||
left: 120px;width:64px; align-items:center; padding: 0px 0px; height: 22px; margin: -1px -0px">START</button>
|
||
</div>
|
||
|
||
<!-- <div class="container" id="shutdown-countdown" style="position: absolute; left: 150px; align-items: center;">
|
||
<label style="position: absolute; left: 0px; width: 50px;"></label>
|
||
</div> -->
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<hr>
|
||
<h1>Charge Control:</h1>
|
||
|
||
<div class="form-group">
|
||
<label>Charge Mode</label>
|
||
<select id="charge-mode" style="position: absolute; left: 150px;width: 150px;">
|
||
<option value="0">Always Charge</option>
|
||
<option value="1">90-100%</option>
|
||
<option value="2">85-100%</option>
|
||
<option value="3">80-100%</option>
|
||
<option value="4">90-95%</option>
|
||
<option value="5">85-95%</option>
|
||
<option value="6">80-95%</option>
|
||
<option value="7">85-90%</option>
|
||
<option value="8">80-90%</option>
|
||
<option value="9">80-85%</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label title="Automatic Voltage Reducer">AVR</label>
|
||
<label class="toggle-switch" style="position: absolute; left: 150px;">
|
||
<input type="checkbox" id="switchToggleAutoReducer" disabled >
|
||
<span class="slider"></span>
|
||
</label>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label title="Overvoltage Protection">OVP</label>
|
||
<label class="toggle-switch" style="position: absolute; left: 150px;">
|
||
<input type="checkbox" id="switchToggleOVP" disabled >
|
||
<span class="slider"></span>
|
||
</label>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label title="Full Charge on Power Lost">FCoPL</label>
|
||
<select id="fcopl-mode" style="position: absolute; left: 150px;width: 150px;">
|
||
<option value="0">Disabled</option>
|
||
<option value="1">Always</option>
|
||
<option value="2">At Battery ≤95%</option>
|
||
<option value="3">At Battery ≤90%</option>
|
||
<option value="4">At Battery ≤85%</option>
|
||
<option value="5">At Battery ≤80%</option>
|
||
</select>
|
||
<button id="resetFCoPLBtn"
|
||
onclick="resetFCoPLTriggered()"
|
||
style="position: absolute;
|
||
left: 310px;width:64px; align-items:center; padding: 0px 0px; height: 22px; margin: -1px -0px">Reset</button>
|
||
</div>
|
||
|
||
|
||
<hr>
|
||
<h1>Monitor:</h1>
|
||
<div class="form-group">
|
||
<label>Status</label>
|
||
<label id="status-label"
|
||
style="position: absolute;
|
||
left: 150px;width:400px;"
|
||
>-</label>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Power Source</label>
|
||
<label id="power-label"
|
||
style="position: absolute;
|
||
left: 150px;width:400px;"
|
||
>-</label>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Battery Voltage</label>
|
||
<label id="input-label"
|
||
style="position: absolute;
|
||
left: 150px;width:100px;"
|
||
>-</label>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Battery Level</label>
|
||
<label id="batt-label"
|
||
style="position: absolute;
|
||
left: 150px;width:400px;"
|
||
>⬛⬛⬛⬛</label>
|
||
</div>
|
||
|
||
<script>
|
||
// ⬛ 🟥 (red square), 🟦 (blue square), 🟩 (green square), 🟨 (yellow square), 🟧 (orange square), and 🟫 (brown square)
|
||
// ◼️ Black Medium Square: A medium-sized black square.
|
||
// ⬛ Black Large Square: A large black square.
|
||
// ◾ Black Medium-Small Square: A smaller black square.
|
||
// ▪️ Black Small Square: The smallest size of a black square.
|
||
// 🔲 Black Square Button: A square version of the radio button emoji
|
||
|
||
var labelStatus = document.getElementById('status-label');
|
||
|
||
var labelPower = document.getElementById('power-label');
|
||
var labelInput = document.getElementById('input-label');
|
||
var labelBatt = document.getElementById('batt-label');
|
||
|
||
var dropdownAutoOnSelect = document.getElementById('auto-mode');
|
||
var toggleSwitch = document.getElementById('switchToggle');
|
||
|
||
var shutTimerInput = document.getElementById('shut-timer-input');
|
||
var shutdownForm = document.getElementById('shutdown-form');
|
||
var shutTimer = document.getElementById('shutdown-countdown');
|
||
|
||
var dropdownShutLevelSelect = document.getElementById('shutcast-mode');
|
||
var shutFlagLabel = document.getElementById('shutdown-cast-label');
|
||
|
||
var connectStateLabel = document.getElementById('connect-state-label');
|
||
|
||
var dropdownChargeModeSelect = document.getElementById('charge-mode');
|
||
var dropdownFullChargeOnPowerLostModeSelect = document.getElementById('fcopl-mode');
|
||
var btnResetFCOPL = document.getElementById('resetFCoPLBtn');
|
||
var toggleAutomaticVoltageReducer = document.getElementById('switchToggleAutoReducer');
|
||
var toggleOvervoltageProtection = document.getElementById('switchToggleOVP');
|
||
|
||
var lastConnectTick = 0;
|
||
|
||
var autoOnMode = 0;
|
||
var stateTarget = false;
|
||
var stateValue = false;
|
||
var lineFrom = 0; //0 = None, 1 = AC, 2 = Battery
|
||
|
||
var onChange = false;
|
||
var checkRefresh;
|
||
|
||
var battLevel = 0;
|
||
var battMaxLevel = 0;
|
||
var battPercentage = 0;
|
||
var isChargingOrBlink = false;
|
||
var chargeLevel = 0;
|
||
var isBattCritical = false;
|
||
var chargeTickTock = false;
|
||
|
||
var timedShutdown = false;
|
||
var timedShutRemaining = 0;
|
||
|
||
var everReceiveStat = false;
|
||
|
||
var shutdownSuggestionLevel = 0;
|
||
//var shutdownSuggestionLevelPrev = 0;
|
||
var shutdownCastFlag = false;
|
||
|
||
var chargeStatus = 0;
|
||
var chargingState = 0;
|
||
var chargeMode = 0;
|
||
var chargeAutoVoltageReducer = false;
|
||
var chargeOvervoltageProtection = false;
|
||
var fullChargeOnPowerLostMode = 0;
|
||
var fullChargeOnPowerLostState = false;
|
||
|
||
var targetChargeMode = 0;
|
||
var targetFCOPLMode = 0;
|
||
var targetOvervoltageProtection = false;
|
||
var targetAutomaticVoltageReducer = false;
|
||
|
||
var authHeader = "";
|
||
|
||
function attemptLogin() {
|
||
var usr = document.getElementById('login-username').value;
|
||
var pwd = document.getElementById('login-password').value;
|
||
authHeader = "Basic " + btoa(usr + ":" + pwd);
|
||
document.getElementById('login-error').style.display = 'none';
|
||
SendState("", 0);
|
||
}
|
||
|
||
function logout() {
|
||
authHeader = "";
|
||
document.getElementById('login-password').value = "";
|
||
document.getElementById('login-error').style.display = 'none';
|
||
document.getElementById('login-overlay').style.display = 'flex';
|
||
fetch('/logout', { method: "GET" }).catch(function(){});
|
||
}
|
||
|
||
function repeatChar(char, times) {
|
||
return char.repeat(times);
|
||
}
|
||
|
||
// Disable all elements in shutdown-form
|
||
function disableShutdownForm() {
|
||
const form = document.getElementById('shutdown-form');
|
||
const elements = form.querySelectorAll('input, button, select, textarea');
|
||
elements.forEach(element => {
|
||
element.disabled = true;
|
||
});
|
||
//const btn = document.getElementById('shutdownBtn');
|
||
//btn.disabled = true;
|
||
}
|
||
|
||
// Enable all elements in shutdown-form
|
||
function enableShutdownForm() {
|
||
const form = document.getElementById('shutdown-form');
|
||
const elements = form.querySelectorAll('input, button, select, textarea');
|
||
elements.forEach(element => {
|
||
element.disabled = false;
|
||
});
|
||
//const btn = document.getElementById('shutdownBtn');
|
||
//btn.disabled = false;
|
||
}
|
||
|
||
dropdownShutLevelSelect.addEventListener('change', function() {
|
||
let targetShutdownLvMode = parseInt(this.value);
|
||
let s = 'shutdown suggest cast: ' + targetShutdownLvMode;
|
||
console.log(s);
|
||
if (targetShutdownLvMode!==shutdownSuggestionLevel){
|
||
this.disabled = true;
|
||
SendState('shutlv', targetShutdownLvMode);
|
||
}
|
||
});
|
||
|
||
dropdownAutoOnSelect.addEventListener('change', function() {
|
||
let targetAutoMode = parseInt(this.value);
|
||
let s = 'auto on mode: ' + targetAutoMode;
|
||
console.log(s);
|
||
if (targetAutoMode!==autoOnMode){
|
||
this.disabled = true;
|
||
SendState('auto', targetAutoMode);
|
||
}
|
||
});
|
||
|
||
toggleSwitch.addEventListener('change', function() {
|
||
let targetState = toggleSwitch.checked;
|
||
let s = 'Toggle switched: ' + (targetState?'ON':'OFF');
|
||
console.log(s);
|
||
if (targetState!==stateValue){
|
||
//Disabled Button
|
||
toggleSwitch.disabled = true;
|
||
SendState('on', targetState?'1':'0');
|
||
}
|
||
});
|
||
|
||
function shutdownTriggered(){
|
||
//shutdownForm.style.display = 'none';
|
||
let t = parseInt(shutTimerInput.value);
|
||
console.log('Send timed shutdown: ' + t);
|
||
if (stateValue){
|
||
//url: htttp://<IP>/state?shutdown=<seconds>
|
||
SendState('shutdown', t);
|
||
}
|
||
}
|
||
|
||
function ConnectionRefresh(){
|
||
lastConnectTick = performance.now();
|
||
connectStateLabel.textContent = ' 🟢';
|
||
connectStateLabel.title = 'Connected';
|
||
}
|
||
|
||
dropdownChargeModeSelect.addEventListener('change', function() {
|
||
targetChargeMode = parseInt(this.value);
|
||
let s = 'charge mode: ' + targetChargeMode;
|
||
console.log(s);
|
||
if (targetChargeMode!==chargeMode){
|
||
this.disabled = true;
|
||
SendState('cm', targetChargeMode);
|
||
}
|
||
});
|
||
|
||
dropdownFullChargeOnPowerLostModeSelect.addEventListener('change', function() {
|
||
targetFCOPLMode = parseInt(this.value);
|
||
let s = 'FCOPL mode: ' + targetFCOPLMode;
|
||
console.log(s);
|
||
if (targetFCOPLMode!==fullChargeOnPowerLostMode){
|
||
this.disabled = true;
|
||
SendState('coil', targetFCOPLMode);
|
||
}
|
||
});
|
||
|
||
function resetFCoPLTriggered(){
|
||
btnResetFCOPL.disabled = true;
|
||
if (fullChargeOnPowerLostState){
|
||
SendState('rofc', 1);
|
||
}
|
||
}
|
||
|
||
toggleAutomaticVoltageReducer.addEventListener('change', function() {
|
||
targetAutomaticVoltageReducer = toggleAutomaticVoltageReducer.checked;
|
||
let s = 'AVR switched: ' + (targetAutomaticVoltageReducer?'ON':'OFF');
|
||
console.log(s);
|
||
if (targetAutomaticVoltageReducer!==chargeAutoVoltageReducer){
|
||
//Disabled Button
|
||
toggleAutomaticVoltageReducer.disabled = true;
|
||
SendState('car', targetAutomaticVoltageReducer?'1':'0');
|
||
}
|
||
});
|
||
|
||
toggleOvervoltageProtection.addEventListener('change', function() {
|
||
targetOvervoltageProtection = toggleOvervoltageProtection.checked;
|
||
let s = 'OVP switched: ' + (targetOvervoltageProtection?'ON':'OFF');
|
||
console.log(s);
|
||
if (targetOvervoltageProtection!==chargeOvervoltageProtection){
|
||
//Disabled Button
|
||
toggleOvervoltageProtection.disabled = true;
|
||
SendState('covp', targetOvervoltageProtection?'1':'0');
|
||
}
|
||
});
|
||
|
||
function SendState(cmd, value) {
|
||
// Clear timeout
|
||
clearTimeout(checkRefresh);
|
||
|
||
if (cmd !== "") {
|
||
onChange = true;
|
||
const url = '/state?' + cmd + '=' + value;
|
||
|
||
console.log("Fetch URL:", url);
|
||
|
||
fetch(url, {
|
||
method: "GET",
|
||
headers: {
|
||
"Content-Type": "application/x-www-form-urlencoded",
|
||
"Authorization": authHeader
|
||
}
|
||
})
|
||
.then(response => {
|
||
if (response.status === 401) {
|
||
document.getElementById('login-overlay').style.display = 'flex';
|
||
if (authHeader !== "") document.getElementById('login-error').style.display = 'block';
|
||
throw new Error("Unauthorized");
|
||
}
|
||
if (!response.ok) throw new Error("Failed to update.");
|
||
return response.text();
|
||
})
|
||
.then(data => {
|
||
document.getElementById('login-overlay').style.display = 'none';
|
||
console.log(`${cmd} updated:`, data);
|
||
onChange = false;
|
||
if (parseInt(data.success)>0) ConnectionRefresh();
|
||
})
|
||
.catch(error => {
|
||
console.error("Error:", error);
|
||
onChange = false;
|
||
});
|
||
|
||
} else {
|
||
if (!onChange) {
|
||
const url = '/state';
|
||
fetch(url, {
|
||
method: "GET",
|
||
headers: {
|
||
"Authorization": authHeader
|
||
}
|
||
})
|
||
.then(response => {
|
||
if (response.status === 401) {
|
||
document.getElementById('login-overlay').style.display = 'flex';
|
||
if (authHeader !== "") document.getElementById('login-error').style.display = 'block';
|
||
throw new Error("Unauthorized");
|
||
}
|
||
if (!response.ok) throw new Error("Failed to fetch data.");
|
||
return response.json(); // Already a JSON object, no need for JSON.parse()
|
||
})
|
||
.then(data => {
|
||
document.getElementById('login-overlay').style.display = 'none';
|
||
//console.log("Fetched Data:", data);
|
||
if (parseInt(data.success)>0) ConnectionRefresh();
|
||
if (onChange) return;
|
||
//sync display
|
||
if (data.success=="2"){
|
||
autoOnMode = parseInt(data.auto);
|
||
let targetAutoMode = parseInt(dropdownAutoOnSelect.value);
|
||
if (targetAutoMode!=autoOnMode){
|
||
dropdownAutoOnSelect.value = autoOnMode.toString();
|
||
}
|
||
if (dropdownAutoOnSelect.disabled){
|
||
dropdownAutoOnSelect.disabled = false;
|
||
}
|
||
|
||
//toggleAutoBtn.textContent = autoOn?'ON':'OFF';
|
||
stateTarget = parseInt(data.on) > 0;
|
||
stateValue = parseInt(data.state) > 0;
|
||
if (stateTarget==stateValue){
|
||
toggleSwitch.checked = stateTarget;
|
||
toggleSwitch.disabled = false;
|
||
}
|
||
//switchBtn.textContent = stateOn?'ON':'OFF';
|
||
|
||
lineFrom = parseInt(data.line);
|
||
isChargingOrBlink = parseInt(data.battBlink) > 0;
|
||
chargeLevel = parseInt(data.chargeLv); //battery level at charges
|
||
isBattCritical = parseInt(data.battCritical) > 0;
|
||
|
||
//Version 1.1
|
||
battPercentage = parseInt(data.battP);
|
||
chargeStatus = parseInt(data.chgError);
|
||
chargingState = parseInt(data.charging);
|
||
chargeMode = parseInt(data.chgMode);
|
||
chargeAutoVoltageReducer = parseInt(data.chgReducer) > 0;
|
||
chargeOvervoltageProtection = parseInt(data.chgOVP) > 0;
|
||
fullChargeOnPowerLostMode = parseInt(data.chgFullTrig);
|
||
fullChargeOnPowerLostState = parseInt(data.chgOFC) > 0;
|
||
|
||
//let s = lineFrom==0?'OFF':(lineFrom==1 || isChargingOrBlink)?'⚡️ AC':'🔋 Battery';
|
||
let s = lineFrom==0?'OFF':(lineFrom==1 || isChargingOrBlink)?'⚡️ Line':'🔋 Battery';
|
||
|
||
if (chargingState>0){
|
||
if (chargeStatus==0){ //normal state
|
||
if (lineFrom==1 && chargingState==2) s += " (Charging)";
|
||
if (lineFrom==1 && chargingState==1) s += " (Reduced Charging)";
|
||
}else{
|
||
//charging while error (retry test)
|
||
if (lineFrom==1 && chargingState>0) s += " (Charging)";
|
||
}
|
||
}
|
||
|
||
if (lineFrom==2 && isBattCritical) s += " (Critical)";
|
||
labelPower.textContent = s;
|
||
labelInput.textContent = data.v + " V";
|
||
battLevel = parseInt(data.batt);
|
||
battMaxLevel = parseInt(data.battMax);
|
||
|
||
|
||
//display V1.1
|
||
//charge mode:
|
||
if (targetChargeMode!=chargeMode){
|
||
dropdownChargeModeSelect.value = chargeMode.toString();
|
||
}
|
||
if (dropdownChargeModeSelect.disabled){
|
||
dropdownChargeModeSelect.disabled = false;
|
||
}
|
||
//fcopl:
|
||
if (targetFCOPLMode!=fullChargeOnPowerLostMode){
|
||
dropdownFullChargeOnPowerLostModeSelect.value = fullChargeOnPowerLostMode.toString();
|
||
}
|
||
if (dropdownFullChargeOnPowerLostModeSelect.disabled){
|
||
dropdownFullChargeOnPowerLostModeSelect.disabled = false;
|
||
}
|
||
//cvopl reset button:
|
||
if (fullChargeOnPowerLostState){
|
||
if (btnResetFCOPL.disabled || btnResetFCOPL.style.display !== "inline-block"){
|
||
btnResetFCOPL.disabled = false;
|
||
btnResetFCOPL.style.display = "inline-block";
|
||
}
|
||
}else{
|
||
if (!btnResetFCOPL.disabled || btnResetFCOPL.style.visibility !== "none"){
|
||
btnResetFCOPL.disabled = true;
|
||
btnResetFCOPL.style.display = "none";
|
||
}
|
||
}
|
||
//AVR:
|
||
//if (toggleAutomaticVoltageReducer.checked!=chargeAutoVoltageReducer){
|
||
if (toggleAutomaticVoltageReducer.disabled){
|
||
toggleAutomaticVoltageReducer.checked = chargeAutoVoltageReducer;
|
||
toggleAutomaticVoltageReducer.disabled = false;
|
||
}
|
||
//}
|
||
//OVP:
|
||
if (toggleOvervoltageProtection.disabled){
|
||
toggleOvervoltageProtection.checked = chargeOvervoltageProtection;
|
||
toggleOvervoltageProtection.disabled = false;
|
||
}
|
||
|
||
|
||
if (stateTarget==stateValue){
|
||
if (stateValue){
|
||
enableShutdownForm();
|
||
}else{
|
||
disableShutdownForm();
|
||
}
|
||
}else{
|
||
disableShutdownForm();
|
||
}
|
||
|
||
timedShutdown = parseInt(data.timedShutdown) > 0;
|
||
if (timedShutdown){
|
||
timedShutRemaining = parseInt(data.shutdownRemain);
|
||
if (timedShutRemaining==0){
|
||
shutTimer.textContent = 'TURNING OFF...';
|
||
}else{
|
||
shutTimer.textContent = timedShutRemaining.toString();
|
||
}
|
||
shutdownForm.style.display = 'none';
|
||
}else{
|
||
timedShutRemaining = 0;
|
||
shutTimer.textContent = '';
|
||
shutdownForm.style.display = 'block';
|
||
}
|
||
|
||
if (!everReceiveStat){
|
||
shutTimerInput.value = parseInt(data.shutLastTimer);
|
||
}
|
||
everReceiveStat = true;
|
||
|
||
|
||
//shutdown suggestion level
|
||
let shutLv = parseInt(data.shutdownSuggestMode);
|
||
if (shutdownSuggestionLevel!=shutLv){
|
||
shutdownSuggestionLevel = shutLv;
|
||
dropdownShutLevelSelect.value = shutLv.toString();
|
||
}
|
||
if (dropdownShutLevelSelect.disabled){
|
||
dropdownShutLevelSelect.disabled = false;
|
||
}
|
||
|
||
shutdownCastFlag = parseInt(data.shutdownSuggest) > 0;
|
||
shutFlagLabel.textContent = shutdownCastFlag?'✔️':' ';
|
||
|
||
if (chargeStatus==0){
|
||
labelStatus.textContent = "Normal";
|
||
}else if (chargeStatus==1){
|
||
labelStatus.textContent = "⚠️ Battery Not Detected" + (chargingState==0?"":" (Retry charging...)");
|
||
}else if (chargeStatus==2){
|
||
labelStatus.textContent = "⛔ Battery Shorted" + (chargingState==0?"":" (Retry charging..)");
|
||
}else if (chargeStatus==4){
|
||
labelStatus.textContent = "⚠️ Charge Overvoltage" + (chargingState==0?"":" (Retry charging...)");
|
||
}
|
||
}
|
||
})
|
||
.catch(error => console.error("Error:", error));
|
||
}
|
||
}
|
||
}
|
||
|
||
//charging independent animation
|
||
function BattDisplayRoutine(){
|
||
|
||
//connection state update when timeout
|
||
if (performance.now() - lastConnectTick >= 3000){
|
||
connectStateLabel.textContent = ' ⚫';
|
||
connectStateLabel.title = 'Unavailable';
|
||
}
|
||
|
||
if (!isChargingOrBlink){
|
||
if (isBattCritical){
|
||
chargeTickTock = !chargeTickTock;
|
||
let bar = chargeTickTock?0:1;
|
||
if (chargeTickTock){
|
||
labelBatt.textContent = repeatChar('🟧', bar) + repeatChar('⬛', battMaxLevel-bar) + " " + battPercentage + "%";
|
||
}else{
|
||
labelBatt.textContent = repeatChar('🟧', bar) + repeatChar('⬛', battMaxLevel-bar) + " " + battPercentage + "%";
|
||
}
|
||
}else{
|
||
chargeTickTock = false;
|
||
if (battMaxLevel==0){
|
||
labelBatt.textContent = '⬛⬛⬛⬛';//'Unknown';
|
||
}else{
|
||
labelBatt.textContent = repeatChar('🟩', battLevel) + repeatChar('⬛', battMaxLevel-battLevel) + " " + battPercentage + "%";
|
||
}
|
||
}
|
||
}else{
|
||
chargeTickTock = !chargeTickTock;
|
||
let bar = chargeTickTock?Math.min(chargeLevel+1, battMaxLevel):Math.min(chargeLevel, battMaxLevel);
|
||
if (chargeTickTock){
|
||
labelBatt.textContent = repeatChar('🟩', bar) + repeatChar('⬛', battMaxLevel-bar) + " " + battPercentage + "%";
|
||
}else{
|
||
labelBatt.textContent = repeatChar('🟩', bar) + repeatChar('⬛', battMaxLevel-bar) + " " + battPercentage + "%";
|
||
}
|
||
}
|
||
}
|
||
|
||
function syncState(){
|
||
//checkOnlineState();
|
||
if (onChange){
|
||
clearTimeout(checkRefresh);
|
||
return;
|
||
}
|
||
checkRefresh = setTimeout(function() {
|
||
SendState("", 0);
|
||
}, 1000);
|
||
}
|
||
|
||
disableShutdownForm();
|
||
|
||
// Set an interval to sync display
|
||
setInterval(syncState, 500);
|
||
|
||
SendState("", 0);
|
||
syncState();
|
||
|
||
setInterval(BattDisplayRoutine, 1000);
|
||
|
||
</script>
|
||
</body>
|
||
</html>
|
||
)rawliteral";
|