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 bs4 import BeautifulSoup
from chardet import detect from chardet import detect
import csv 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" ROOT = "http://192.168.2.20"
app = Flask(__name__)
# Turns a csv reader into a list of dictionaries(json) # Turns a csv reader into a list of dictionaries(json)
def reader_to_json(reader:csv.DictReader) -> list[dict]: def reader_to_json(reader:csv.DictReader) -> list[dict]:
@ -43,8 +48,9 @@ def get_file(entries:int) -> dict:
raise ValueError("No link found") raise ValueError("No link found")
# DEBUG: Get file locally # DEBUG: Local json
# return local_file("test.csv") # with open("out.json") as i:
# return load(i)
# Saves a json file # Saves a json file
def save_file(json:list[dict], filename:str) -> None: def save_file(json:list[dict], filename:str) -> None:
@ -54,11 +60,44 @@ def save_file(json:list[dict], filename:str) -> None:
except Exception as exc: except Exception as exc:
raise IndexError("Error saving file") from exc raise IndexError("Error saving file") from exc
def main(): @app.route("/", methods=["GET"])
size = int(input("Entries: ")) def home():
file = get_file(size) 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:])
def pull_data():
logging.info("Pulling data")
threading.Timer(86400, pull_data).start()
save_file(get_file(50), "out.json")
save_file(file, "out.json")
if __name__ == "__main__": 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>