Added web interface and threading task

This commit is contained in:
Boyan 2024-12-20 18:00:51 +02:00
parent d58be164f8
commit 8a4c8c9f41
2 changed files with 445 additions and 9 deletions

View File

@ -2,9 +2,14 @@ from requests import get
from bs4 import BeautifulSoup
from chardet import detect
import csv
from json import dumps
from json import dumps, load
from flask import Flask, request, jsonify, render_template
from datetime import datetime
import threading
import logging
ROOT = "http://192.168.2.20"
app = Flask(__name__)
# Turns a csv reader into a list of dictionaries(json)
def reader_to_json(reader:csv.DictReader) -> list[dict]:
@ -43,9 +48,10 @@ def get_file(entries:int) -> dict:
raise ValueError("No link found")
# DEBUG: Get file locally
# return local_file("test.csv")
# DEBUG: Local json
# with open("out.json") as i:
# return load(i)
# Saves a json file
def save_file(json:list[dict], filename:str) -> None:
try:
@ -54,11 +60,44 @@ def save_file(json:list[dict], filename:str) -> None:
except Exception as exc:
raise IndexError("Error saving file") from exc
def main():
size = int(input("Entries: "))
file = get_file(size)
@app.route("/", methods=["GET"])
def home():
return render_template("index.html", entries=get_entries(10).json, date=datetime.now())
@app.route("/get", methods=["GET"])
def get_entries(entries:int=None):
if not entries:
entries = request.args.get("entries")
try:
entries = int(entries)
except:
return jsonify({"error":"Invalid entries"})
try:
file = get_file(entries)
return jsonify(file)
except Exception as exc:
return jsonify({"error":str(exc)})
@app.route("/dump", methods=["GET"])
def dump():
with open("out.json") as i:
return i.read()
@app.route("/dumpLast", methods=["GET"])
def dump_last():
with open("out.json") as i:
# Return last 50 entries
return dumps(load(i)[-50:])
save_file(file, "out.json")
def pull_data():
logging.info("Pulling data")
threading.Timer(86400, pull_data).start()
save_file(get_file(50), "out.json")
if __name__ == "__main__":
main()
# Add threading
pull_data()
app.run(debug=True)

397
src/templates/index.html Normal file
View File

@ -0,0 +1,397 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"
/>
<title>Water Monitor</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 2em;
background: #f9f9f9;
color: #333;
}
#header {
display: flex;
justify-content: space-between;
align-items: flex-end;
margin-bottom: 2em;
flex-wrap: wrap;
gap: 1em;
}
#header-left {
display: flex;
flex-direction: column;
}
#header h1 {
font-size: 2em;
color: #333;
margin: 0;
}
#header h2 {
font-size: 1.2em;
font-weight: normal;
display: flex;
align-items: center;
color: #555;
margin: 0.2em 0 0 0;
}
#header h2 i {
margin-right: 0.5em;
color: #666;
}
.search-container {
display: flex;
align-items: center;
gap: 0.5em;
}
.search-container input {
padding: 0.5em;
font-size: 0.9em;
border: 1px solid #ccc;
border-radius: 5px;
}
button {
padding: 0.5em 1em;
font-size: 0.9em;
border: none;
background: #007bff;
color: #fff;
border-radius: 5px;
cursor: pointer;
transition: background 0.2s ease;
}
.search-container button:hover {
background: #0056b3;
}
#records #list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1.5em;
}
.record-container {
background: #fff;
border: 1px solid #ddd;
border-radius: 5px;
padding: 1em;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.record-container:hover {
transform: translateY(-3px);
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
}
.record-header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #eee;
padding-bottom: 0.5em;
margin-bottom: 1em;
}
.record-header .record-title {
font-weight: bold;
font-size: 1.1em;
display: flex;
align-items: center;
}
.record-header .record-title i {
margin-right: 0.5em;
color: #333;
}
.record-header .record-date {
font-size: 0.9em;
color: #666;
display: flex;
align-items: center;
}
.record-header .record-date i {
margin-right: 0.3em;
}
.record-item {
margin: 0.7em 0;
display: flex;
align-items: center;
}
.record-item i {
margin-right: 0.5em;
min-width: 20px;
text-align: center;
color: #007bff;
}
.record-item strong {
margin-right: 0.3em;
}
.alarm {
color: inherit;
}
#load-more-container {
display: flex;
justify-content: center;
margin-top: 2em;
}
#load-more-btn {
padding: 0.8em 1.5em;
border: none;
background: #28a745;
color: #fff;
border-radius: 5px;
font-size: 1em;
cursor: pointer;
transition: background 0.2s ease;
}
#load-more-btn:hover {
background: #218838;
}
@media (max-width: 480px) {
#header h1 {
font-size: 1.5em;
}
#header h2 {
font-size: 1em;
}
}
</style>
</head>
<body>
<section id="header">
<div id="header-left">
<h1>Water Monitor</h1>
<h2><i class="fas fa-calendar-alt"></i> {{ date }}</h2>
</div>
<div id="download">
<button id="all"><i class="fas fa-download"></i> Download</button>
<button id="last50"><i class="fas fa-download"></i> <i class="fa fa-hourglass"></i> Last 50</button>
</div>
<script>
document.getElementById('all').addEventListener('click', async () => {
try {
const res = await fetch('/dump');
const data = await res.json();
const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'water-monitor-data.json';
a.click();
URL.revokeObjectURL(url);
} catch (err) {
console.error('Error downloading data:', err);
}
});
document.getElementById('last50').addEventListener('click', async () => {
try {
const res = await fetch('/dumpLast');
const data = await res.json();
const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'water-monitor-data.json';
a.click();
URL.revokeObjectURL(url);
} catch (err) {
console.error('Error downloading data:', err);
}
});
</script>
<div class="search-container">
<input
type="text"
id="search-input"
placeholder="Search by Record #..."
/>
<button id="search-btn">Search</button>
</div>
</section>
<section id="records">
<div id="list">
{% for item in entries %}
<div class="record-container">
<div class="record-header">
<div class="record-title">
<i class="fas fa-hashtag"></i> {{ item.Record }}
</div>
<div class="record-date">
<i class="fas fa-calendar-alt"></i> {{ item.Date }}
</div>
</div>
<div class="record-item">
<i class="fas fa-clock"></i>
<strong>UTC Time:</strong> {{ item['UTC Time'] }}
</div>
<div class="record-item">
<i class="fas fa-tint"></i>
<strong>PT1:</strong> {{ item.PT1 }}
</div>
<div class="record-item">
<i class="fas fa-tint"></i>
<strong>PT2:</strong> {{ item.PT2 }}
</div>
<div class="record-item">
<i class="fas fa-flask"></i>
<strong>C1:</strong> {{ item.C1 }}
</div>
<div class="record-item">
<i class="fas fa-flask"></i>
<strong>C2:</strong> {{ item.C2 }}
</div>
<div class="record-item">
<i class="fas fa-radiation"></i>
<strong>UVC1:</strong> {{ item.UVC1 }}
</div>
<div class="record-item">
<i
class="fas fa-exclamation-circle alarm"
style="color: {{ 'red' if item.Alarm1 == 'Yes' else 'green' }}"
></i>
<strong>Alarm1:</strong> {{ item.Alarm1 }}
</div>
<div class="record-item">
<i
class="fas {{ 'fa-pause-circle' if item.STATUS == 'Standby' else 'fa-play-circle' }}"
></i>
<strong>Status:</strong> {{ item.STATUS }}
</div>
</div>
{% endfor %}
</div>
<div id="load-more-container">
<button id="load-more-btn">Load More</button>
</div>
</section>
<script>
let allEntries = {{ entries|tojson }};
let displayedEntries = [...allEntries];
let currentCount = displayedEntries.length;
const listContainer = document.getElementById('list');
const searchInput = document.getElementById('search-input');
const searchBtn = document.getElementById('search-btn');
const loadMoreBtn = document.getElementById('load-more-btn');
function renderEntries(entries) {
listContainer.innerHTML = '';
entries.forEach(item => {
const container = document.createElement('div');
container.className = 'record-container';
container.innerHTML = `
<div class="record-header">
<div class="record-title">
<i class="fas fa-hashtag"></i> ${item.Record}
</div>
<div class="record-date">
<i class="fas fa-calendar-alt"></i> ${item.Date}
</div>
</div>
<div class="record-item">
<i class="fas fa-clock"></i>
<strong>UTC Time:</strong> ${item['UTC Time']}
</div>
<div class="record-item">
<i class="fas fa-tint"></i>
<strong>PT1:</strong> ${item.PT1}
</div>
<div class="record-item">
<i class="fas fa-tint"></i>
<strong>PT2:</strong> ${item.PT2}
</div>
<div class="record-item">
<i class="fas fa-flask"></i>
<strong>C1:</strong> ${item.C1}
</div>
<div class="record-item">
<i class="fas fa-flask"></i>
<strong>C2:</strong> ${item.C2}
</div>
<div class="record-item">
<i class="fas fa-radiation"></i>
<strong>UVC1:</strong> ${item.UVC1}
</div>
<div class="record-item">
<i class="fas fa-exclamation-circle" style="color:${item.Alarm1 === 'Yes' ? 'red' : 'green'}"></i>
<strong>Alarm1:</strong> ${item.Alarm1}
</div>
<div class="record-item">
<i class="fas ${item.STATUS === 'Standby' ? 'fa-pause-circle' : 'fa-play-circle'}"></i>
<strong>Status:</strong> ${item.STATUS}
</div>
`;
listContainer.appendChild(container);
});
}
renderEntries(displayedEntries);
searchBtn.addEventListener('click', () => {
const query = searchInput.value.toLowerCase().trim();
if (!query) {
renderEntries(displayedEntries);
return;
}
const filtered = displayedEntries.filter(entry => {
return entry.Record.toLowerCase().includes(query);
});
renderEntries(filtered);
});
loadMoreBtn.addEventListener('click', async () => {
const newCount = currentCount + 10;
try {
const res = await fetch(`/get?entries=${newCount}`);
const data = await res.json();
if (data.error) {
console.error(data.error);
} else {
allEntries = data;
displayedEntries = [...allEntries];
currentCount = displayedEntries.length;
renderEntries(displayedEntries);
}
} catch (err) {
console.error('Error fetching more data:', err);
}
});
</script>
</body>
</html>