Added web interface and threading task
This commit is contained in:
parent
d58be164f8
commit
8a4c8c9f41
57
src/main.py
57
src/main.py
@ -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,9 +48,10 @@ 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:
|
||||||
try:
|
try:
|
||||||
@ -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:])
|
||||||
|
|
||||||
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__":
|
if __name__ == "__main__":
|
||||||
main()
|
# Add threading
|
||||||
|
pull_data()
|
||||||
|
app.run(debug=True)
|
397
src/templates/index.html
Normal file
397
src/templates/index.html
Normal 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>
|
Loading…
x
Reference in New Issue
Block a user