nicer webapp

This commit is contained in:
Alex Janka 2024-01-19 10:28:00 +11:00
parent f1807b2417
commit 502816dbfe
3 changed files with 101 additions and 68 deletions

2
Cargo.lock generated
View file

@ -2567,7 +2567,7 @@ dependencies = [
[[package]] [[package]]
name = "tesla-charge-controller" name = "tesla-charge-controller"
version = "1.0.8" version = "1.0.9"
dependencies = [ dependencies = [
"async-channel", "async-channel",
"chrono", "chrono",

View file

@ -1,6 +1,6 @@
[package] [package]
name = "tesla-charge-controller" name = "tesla-charge-controller"
version = "1.0.8" version = "1.0.9"
edition = "2021" edition = "2021"
license = "MITNFA" license = "MITNFA"
description = "Controls Tesla charge rate based on solar charge data" description = "Controls Tesla charge rate based on solar charge data"

View file

@ -6,7 +6,54 @@
<title>Tesla Charge Control</title> <title>Tesla Charge Control</title>
<link id="favicon" rel="icon" <link id="favicon" rel="icon"
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22></text></svg>"> href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22></text></svg>">
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
background-color: #666;
}
.container {
max-width: 40em;
margin: auto;
padding: 0.5em 2em;
border-radius: 10px;
background-color: white;
}
.loading,
.loading * {
cursor: progress;
}
.selector {
padding: 1em;
background-color: gray;
width: max-content;
border: 0.2em;
border-radius: 6px;
}
label {
/* display: block; */
padding: 0.5em 1em;
margin: 0.5em;
font-weight: bold;
transition: all .2s 0s ease;
border-radius: 4px;
text-align: center;
}
input[type=radio] {
display: none;
}
input[type=radio]:checked+label {
background-color: white;
}
</style>
<script type="text/javascript"> <script type="text/javascript">
const api_url = window.location.protocol + "//" + window.location.hostname + ":" + window.location.port; const api_url = window.location.protocol + "//" + window.location.hostname + ":" + window.location.port;
@ -23,42 +70,51 @@
}); });
function register() { function register() {
return setInterval(refresh, 10000); return setInterval(refresh, 5000);
} }
function flash() { function flash() {
fetch(api_url + "/flash", { method: "POST" }); fetch(api_url + "/flash", { method: "POST" });
} }
var is_automatic_control;
const delay = () => {
return new Promise(resolve => setTimeout(resolve, 1000));
};
function disable_automatic_control() { function disable_automatic_control() {
if (is_automatic_control) {
document.getElementById('control-disabled').checked = true;
document.body.classList.add("loading");
fetch(api_url + "/disable-control", { method: "POST" }) fetch(api_url + "/disable-control", { method: "POST" })
.then((response) => { .then(async (response) => {
let delayres = await delay();
refresh_buttons(); refresh_buttons();
}); });
} }
}
function enable_automatic_control() { function enable_automatic_control() {
if (!is_automatic_control) {
document.getElementById('control-enabled').checked = true;
document.body.classList.add("loading");
fetch(api_url + "/enable-control", { method: "POST" }) fetch(api_url + "/enable-control", { method: "POST" })
.then((response) => { .then(async (response) => {
let delayres = await delay();
refresh_buttons(); refresh_buttons();
}); });
} }
}
function update_control_buttons(data) { function update_control_buttons(data) {
var button_container = document.getElementById("buttons"); document.body.classList.remove("loading");
while (button_container.childElementCount > 0) { button_container.removeChild(button_container.firstChild) } is_automatic_control = data.control_enable;
if (data.control_enable) { if (data.control_enable) {
// control enabled, so show disable button document.getElementById('control-enabled').checked = true;
var button = document.createElement('button');
button.textContent = 'Disable automatic control';
button.addEventListener('click', disable_automatic_control);
button_container.appendChild(button);
} else { } else {
// control disabled, so show enable button document.getElementById('control-disabled').checked = true;
var button = document.createElement('button');
button.textContent = 'Enable automatic control';
button.addEventListener('click', enable_automatic_control);
button_container.appendChild(button);
} }
} }
@ -72,11 +128,11 @@
let favicon = document.getElementById("favicon"); let favicon = document.getElementById("favicon");
favicon.setAttribute("href", "data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>" + "⏳" + "</text></svg>"); favicon.setAttribute("href", "data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>" + "⏳" + "</text></svg>");
refresh_buttons();
fetch(api_url + "/car-state") fetch(api_url + "/car-state")
.then((response) => response.json()) .then((response) => response.json())
.then((json) => update_state(json)); .then((json) => update_state(json));
refresh_buttons();
} }
function update_state(state) { function update_state(state) {
@ -84,46 +140,15 @@
favicon.setAttribute("href", "data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>" + get_emoji(state.charge_state) + "</text></svg>"); favicon.setAttribute("href", "data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>" + get_emoji(state.charge_state) + "</text></svg>");
var info_div = document.getElementById("info"); var info_div = document.getElementById("info");
while (info_div.childElementCount > 0) { info_div.removeChild(info_div.firstChild) } while (info_div.childElementCount > 0) { info_div.removeChild(info_div.firstChild) }
var arr = ["Battery " + state.charge_state.battery_level + "%", "Range: " + (state.charge_state.battery_range * 1.60934).toFixed(1) + "km", "Charging at " + state.charge_state.charge_rate.toFixed(1) + " amps", "Charging until battery at " + state.charge_state.charge_limit_soc + "%",
];
for (line in arr) {
el = document.createElement('p'); el = document.createElement('p');
el.appendChild(document.createTextNode(arr[line]));
info_div.appendChild(el)
}
home = document.createElement('p');
if (state.location_data.home) {
home.appendChild(document.createTextNode("At home"));
} else {
home.appendChild(document.createTextNode("Not home"));
}
info_div.appendChild(home);
el = document.createElement('p');
el.appendChild(document.createTextNode("Full charge state:"));
state_json = document.createElement('pre'); state_json = document.createElement('pre');
state_json.appendChild(document.createTextNode(JSON.stringify(state.charge_state, null, '\t'))); state_json.appendChild(document.createTextNode(JSON.stringify(state, null, '\t')));
el.appendChild(state_json); el.appendChild(state_json);
info_div.appendChild(el); info_div.appendChild(el);
el = document.createElement('p');
el.appendChild(document.createTextNode("Full climate state:"));
state_json = document.createElement('pre');
state_json.appendChild(document.createTextNode(JSON.stringify(state.climate_state, null, '\t')));
el.appendChild(state_json);
info_div.appendChild(el);
} }
function get_emoji(charge_state) { function get_emoji(charge_state) {
@ -136,16 +161,24 @@
</script> </script>
</head> </head>
<body></body> <body>
<div> <div class="container">
<h2> <h3>Automatic control:</h3>
<a href="/grafana">Grafana</a> <div class="selector" id="control-selector">
</h2> <input id="control-enabled" type="radio" name="control" onclick="enable_automatic_control()">
<label for="control-enabled">enabled</label>
<input id="control-disabled" type="radio" name="control" onclick="disable_automatic_control()">
<label for="control-disabled">disabled</label>
</div> </div>
<br>
<button onclick="flash()">flash</button> <button onclick="flash()">flash</button>
<p id="buttons"></p>
<div id="info"></div> <div id="info"></div>
<div>
<h3>
<a href="/grafana">Grafana</a>
</h3>
</div>
</div>
</body>
</html> </html>