Added web interface and threading task
This commit is contained in:
parent
d58be164f8
commit
8a4c8c9f41
55
src/main.py
55
src/main.py
@ -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,8 +48,9 @@ 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:
|
||||
@ -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:])
|
||||
|
||||
|
||||
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__":
|
||||
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