Many things
Signed-off-by: AKP <tom@tdpain.net>
This commit is contained in:
parent
8028d82e94
commit
f2f1f9b5e6
11 changed files with 246 additions and 54 deletions
|
@ -4,6 +4,7 @@ from typing import *
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
|
from cachetools import cached, TTLCache
|
||||||
|
|
||||||
|
|
||||||
class ScraperError(ValueError):
|
class ScraperError(ValueError):
|
||||||
|
@ -52,6 +53,7 @@ class CircuitScraper:
|
||||||
return CircuitScraper._base_url + f"/?site={site_id}"
|
return CircuitScraper._base_url + f"/?site={site_id}"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@cached(cache=TTLCache(maxsize=64, ttl=30))
|
||||||
def get_site_machine_states(site_id: str) -> List[Machine]:
|
def get_site_machine_states(site_id: str) -> List[Machine]:
|
||||||
site_url = CircuitScraper._get_site_url(site_id)
|
site_url = CircuitScraper._get_site_url(site_id)
|
||||||
|
|
||||||
|
@ -120,3 +122,13 @@ class CircuitScraper:
|
||||||
machines.append(machine)
|
machines.append(machine)
|
||||||
|
|
||||||
return machines
|
return machines
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_machine(site_id: str, machine_number: str) -> Optional[Machine]:
|
||||||
|
all_machines = CircuitScraper.get_site_machine_states(site_id)
|
||||||
|
res: Optional[Machine] = None
|
||||||
|
for machine in all_machines:
|
||||||
|
if machine.number == machine_number:
|
||||||
|
res = machine
|
||||||
|
break
|
||||||
|
return res
|
||||||
|
|
26
circuit-laundry-notifier/static/main.js
Normal file
26
circuit-laundry-notifier/static/main.js
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
const show = (e) => { e.style.display = "block" }
|
||||||
|
const hide = (e) => { e.style.display = "none" }
|
||||||
|
|
||||||
|
const workingIndicator = document.getElementById("working")
|
||||||
|
|
||||||
|
const successIndicator = document.getElementById("success")
|
||||||
|
const errorIndicator = document.getElementById("error")
|
||||||
|
const errorIndicatorText = document.getElementById("error_text")
|
||||||
|
|
||||||
|
function showError(text) {
|
||||||
|
errorIndicatorText.innerText = text
|
||||||
|
show(errorIndicator)
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSuccessHTML(html) {
|
||||||
|
const elem = document.createElement("p")
|
||||||
|
elem.innerHTML = html
|
||||||
|
showSuccessElement(elem)
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSuccessElement(elem) {
|
||||||
|
successIndicator.appendChild(elem)
|
||||||
|
show(successIndicator)
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = {}
|
19
circuit-laundry-notifier/static/selectMachine.js
Normal file
19
circuit-laundry-notifier/static/selectMachine.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
const selectMachineBlock = document.getElementById("select_machine")
|
||||||
|
const selectMachineSelection = document.getElementById("select_machine__selection")
|
||||||
|
|
||||||
|
function addMachineToSelection(machineObject) {
|
||||||
|
const elem = document.createElement("option")
|
||||||
|
elem.value = machineObject["number"]
|
||||||
|
elem.innerText = `${machineObject["type"]} ${machineObject["number"]}`
|
||||||
|
selectMachineSelection.appendChild(elem)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const nextButton = document.getElementById("select_machine__next_button")
|
||||||
|
|
||||||
|
nextButton.addEventListener("click", () => {
|
||||||
|
state.machine = selectMachineSelection.options[selectMachineSelection.selectedIndex].value
|
||||||
|
hide(selectMachineBlock)
|
||||||
|
show(submitBlock)
|
||||||
|
})
|
||||||
|
}
|
51
circuit-laundry-notifier/static/selectSite.js
Normal file
51
circuit-laundry-notifier/static/selectSite.js
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
const selectSiteBlock = document.getElementById("select_site");
|
||||||
|
|
||||||
|
{
|
||||||
|
const nextButton = document.getElementById("select_site__next_button")
|
||||||
|
const siteIDInput = document.getElementById("select_site__site_id_input")
|
||||||
|
|
||||||
|
nextButton.addEventListener("click", () => {
|
||||||
|
hide(errorIndicator)
|
||||||
|
hide(selectSiteBlock)
|
||||||
|
|
||||||
|
if (siteIDInput.value === "") {
|
||||||
|
showError("Missing site ID")
|
||||||
|
show(selectSiteBlock)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
show(workingIndicator)
|
||||||
|
hide(selectSiteBlock)
|
||||||
|
|
||||||
|
fetch(`/api/v1/machines/${siteIDInput.value.trim()}`)
|
||||||
|
.then(async (response) => {
|
||||||
|
hide(workingIndicator)
|
||||||
|
|
||||||
|
const responseBody = await response.json()
|
||||||
|
if (!response.ok) {
|
||||||
|
showError(responseBody.message)
|
||||||
|
show(selectSiteBlock)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let numberInUse = 0
|
||||||
|
for (let i = 0; i < responseBody.length; i += 1) {
|
||||||
|
const machineObject = responseBody[i]
|
||||||
|
if (machineObject["state"] === "IN_USE") {
|
||||||
|
addMachineToSelection(machineObject)
|
||||||
|
numberInUse += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (numberInUse === 0) {
|
||||||
|
showError("No machines in use.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
state.siteID = siteIDInput.value.trim()
|
||||||
|
|
||||||
|
hide(selectSiteBlock)
|
||||||
|
show(selectMachineBlock)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,17 +0,0 @@
|
||||||
{
|
|
||||||
const block = document.getElementById("select_site");
|
|
||||||
const nextButton = document.getElementById("select_site__next_button")
|
|
||||||
const siteIDInput = document.getElementById("select_site__site_id_input")
|
|
||||||
|
|
||||||
nextButton.addEventListener("click", function () {
|
|
||||||
hideErrorIndicator()
|
|
||||||
|
|
||||||
if (siteIDInput.value === "") {
|
|
||||||
showError("Missing site ID")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
showWorkingIndicator()
|
|
||||||
hideWorkingIndicator()
|
|
||||||
})
|
|
||||||
}
|
|
44
circuit-laundry-notifier/static/submit.js
Normal file
44
circuit-laundry-notifier/static/submit.js
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
const submitBlock = document.getElementById("submit")
|
||||||
|
|
||||||
|
{
|
||||||
|
const nextButton = document.getElementById("submit__next_button")
|
||||||
|
const ntfyTopicInput = document.getElementById("submit__ntfy_topic")
|
||||||
|
|
||||||
|
nextButton.addEventListener("click", () => {
|
||||||
|
hide(errorIndicator)
|
||||||
|
hide(submitBlock)
|
||||||
|
show(workingIndicator)
|
||||||
|
|
||||||
|
const formValues = new FormData()
|
||||||
|
formValues.set("machine_number", state.machine)
|
||||||
|
|
||||||
|
{
|
||||||
|
const v = ntfyTopicInput.value.trim()
|
||||||
|
if (v !== "") {
|
||||||
|
formValues.set("ntfy_topic", ntfyTopicInput.value.trim())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(`/api/v1/machines/${state.siteID}/watch`, {
|
||||||
|
method: "POST",
|
||||||
|
body: formValues,
|
||||||
|
})
|
||||||
|
.then(async (response) => {
|
||||||
|
hide(workingIndicator)
|
||||||
|
|
||||||
|
const responseBody = await response.json()
|
||||||
|
if (!response.ok) {
|
||||||
|
showError(responseBody.message)
|
||||||
|
show(submitBlock)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
showSuccessHTML(`Successfully registered using ntfy topic <code>${responseBody["ntfy_topic"]}</code>`)
|
||||||
|
const anchor = document.createElement("a")
|
||||||
|
anchor.href = `https://ntfy.sh/${responseBody["ntfy_topic"]}`
|
||||||
|
anchor.innerText = "Click here to go to the ntfy topic"
|
||||||
|
anchor.target = "_blank"
|
||||||
|
showSuccessElement(anchor)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,25 +0,0 @@
|
||||||
const workingIndicator = document.getElementById("working")
|
|
||||||
|
|
||||||
function showWorkingIndicator() {
|
|
||||||
workingIndicator.style.display = "block";
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideWorkingIndicator() {
|
|
||||||
workingIndicator.style.display = "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
const errorIndicator = document.getElementById("error")
|
|
||||||
const errorIndicatorText = document.getElementById("error_text")
|
|
||||||
|
|
||||||
function showErrorIndicator() {
|
|
||||||
errorIndicator.style.display = "block"
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideErrorIndicator() {
|
|
||||||
errorIndicator.style.display = "none"
|
|
||||||
}
|
|
||||||
|
|
||||||
function showError(text) {
|
|
||||||
errorIndicatorText.innerText = text
|
|
||||||
showErrorIndicator()
|
|
||||||
}
|
|
|
@ -9,12 +9,6 @@
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<nav class="navbar navbar-dark bg-dark">
|
|
||||||
<div class="container-fluid">
|
|
||||||
<span class="navbar-brand mb-0 h1">CircuitBodge</span>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div class="container pt-4">
|
<div class="container pt-4">
|
||||||
|
|
||||||
<h2>CircuitBodge</h2>
|
<h2>CircuitBodge</h2>
|
||||||
|
@ -36,14 +30,30 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="select_machine" style="display:none;">
|
<div id="select_machine" style="display:none;">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="select_machine__selection" class="form-label">Select a machine</label>
|
||||||
|
<select class="form-select" id="select_machine__selection"></select>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-primary" id="select_machine__next_button">Next</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="submit" style="display:none;">
|
<div id="submit" style="display:none;">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="submit__ntfy_topic" class="form-label">ntfy.sh topic</label>
|
||||||
|
<input type="text" class="form-control" id="submit__ntfy_topic" placeholder="Randomly generated">
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-primary" id="submit__next_button">Next</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="success" style="display:none;">
|
||||||
|
<p id="success__text"></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/util.js"></script>
|
<script src="/static/main.js"></script>
|
||||||
<script src="/static/siteSelect.js"></script>
|
<script src="/static/selectMachine.js"></script>
|
||||||
|
<script src="/static/selectSite.js"></script>
|
||||||
|
<script src="/static/submit.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-OERcA2EqjJCMA+/3y+gxIOqMEjwtxJY7qPCqsdltbNJuaOe923+mo//f6V8Qbsw3" crossorigin="anonymous"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-OERcA2EqjJCMA+/3y+gxIOqMEjwtxJY7qPCqsdltbNJuaOe923+mo//f6V8Qbsw3" crossorigin="anonymous"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|
|
@ -5,8 +5,15 @@ from threading import Lock
|
||||||
from typing import *
|
from typing import *
|
||||||
|
|
||||||
import flask
|
import flask
|
||||||
|
import requests
|
||||||
|
|
||||||
from circuit_scraper import CircuitScraper, Machine, ScraperError, MachineState
|
from circuit_scraper import (
|
||||||
|
CircuitScraper,
|
||||||
|
Machine,
|
||||||
|
ScraperError,
|
||||||
|
MachineState,
|
||||||
|
MachineType,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def new(debug=False) -> flask.Flask:
|
def new(debug=False) -> flask.Flask:
|
||||||
|
@ -82,11 +89,54 @@ def _api_register_watcher(site_id: str) -> Union[any, Tuple[any, int]]:
|
||||||
if machine_number is None:
|
if machine_number is None:
|
||||||
return flask.jsonify({"ok": False, "message": "missing machine_number"}), 400
|
return flask.jsonify({"ok": False, "message": "missing machine_number"}), 400
|
||||||
|
|
||||||
|
machine = CircuitScraper.get_machine(site_id, machine_number)
|
||||||
|
if machine is None:
|
||||||
|
return (
|
||||||
|
flask.jsonify(
|
||||||
|
{
|
||||||
|
"ok": False,
|
||||||
|
"message": "invalid site_id and machine_number combination",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
400,
|
||||||
|
)
|
||||||
|
|
||||||
|
if machine.state != MachineState.InUse:
|
||||||
|
return (
|
||||||
|
flask.jsonify(
|
||||||
|
{
|
||||||
|
"ok": False,
|
||||||
|
"message": "Machine not in use",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
400,
|
||||||
|
)
|
||||||
|
|
||||||
job_lock.acquire()
|
job_lock.acquire()
|
||||||
ws = WatchState(site_id, ntfy_topic, machine_number)
|
ws = WatchState(site_id, ntfy_topic, machine_number)
|
||||||
jobs.append(ws)
|
jobs.append(ws)
|
||||||
job_lock.release()
|
job_lock.release()
|
||||||
|
|
||||||
|
time_remaining_text: str
|
||||||
|
if machine.minutes_remaining < 0:
|
||||||
|
time_remaining_text = (
|
||||||
|
f"It was due to finish {machine.minutes_remaining} minutes ago."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
time_remaining_text = (
|
||||||
|
f"It's due to finish in {machine.minutes_remaining} minutes."
|
||||||
|
)
|
||||||
|
|
||||||
|
requests.post(
|
||||||
|
f"https://ntfy.sh/{ws.ntfy_topic}",
|
||||||
|
data=f"Topic registered to {machine.type.value.lower()} {ws.machine_number} at site {site_id}. "
|
||||||
|
+ time_remaining_text,
|
||||||
|
headers={
|
||||||
|
"Title": "Machine registered",
|
||||||
|
"Tags": "white_check_mark",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
return flask.jsonify(ws), 200
|
return flask.jsonify(ws), 200
|
||||||
|
|
||||||
|
|
||||||
|
@ -142,8 +192,20 @@ def _api_run_watcher() -> Union[any, Tuple[any, int]]:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if target_machine.state == MachineState.Completed:
|
if target_machine.state == MachineState.Completed:
|
||||||
# TODO: Notify ntfy
|
requests.post(
|
||||||
print("DONE " + str(ws))
|
f"https://ntfy.sh/{ws.ntfy_topic}",
|
||||||
|
data=f"{target_machine.type} {target_machine.number} is finished!",
|
||||||
|
headers={
|
||||||
|
"Title": (
|
||||||
|
"Washing"
|
||||||
|
if target_machine.type == MachineType.Washer
|
||||||
|
else "Drying"
|
||||||
|
)
|
||||||
|
+ " completed!",
|
||||||
|
"Priority": "urgent",
|
||||||
|
"Tags": "tada",
|
||||||
|
},
|
||||||
|
)
|
||||||
completed_items.append(i)
|
completed_items.append(i)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
11
poetry.lock
generated
11
poetry.lock
generated
|
@ -13,6 +13,14 @@ soupsieve = ">1.2"
|
||||||
html5lib = ["html5lib"]
|
html5lib = ["html5lib"]
|
||||||
lxml = ["lxml"]
|
lxml = ["lxml"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cachetools"
|
||||||
|
version = "5.2.0"
|
||||||
|
description = "Extensible memoizing collections and decorators"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = "~=3.7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "certifi"
|
name = "certifi"
|
||||||
version = "2022.9.24"
|
version = "2022.9.24"
|
||||||
|
@ -192,10 +200,11 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "flake8
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "1.1"
|
lock-version = "1.1"
|
||||||
python-versions = "^3.8"
|
python-versions = "^3.8"
|
||||||
content-hash = "28f817d195ff20de983a656a216fbdaac203af52239ca31ed0bf9676eee92bb6"
|
content-hash = "00993ecc54f6327f8e58e6b91ce934b966e97c8db4bdb2c7a973c4101b20cb1c"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
beautifulsoup4 = []
|
beautifulsoup4 = []
|
||||||
|
cachetools = []
|
||||||
certifi = []
|
certifi = []
|
||||||
charset-normalizer = []
|
charset-normalizer = []
|
||||||
click = []
|
click = []
|
||||||
|
|
|
@ -10,6 +10,7 @@ python = "^3.8"
|
||||||
requests = "^2.28.1"
|
requests = "^2.28.1"
|
||||||
beautifulsoup4 = "^4.11.1"
|
beautifulsoup4 = "^4.11.1"
|
||||||
Flask = "^2.2.2"
|
Flask = "^2.2.2"
|
||||||
|
cachetools = "^5.2.0"
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
|
|
||||||
|
|
Reference in a new issue