diff --git a/circuit-laundry-notifier/circuit_scraper.py b/circuit-laundry-notifier/circuit_scraper.py index 86cafdb..6b265fb 100644 --- a/circuit-laundry-notifier/circuit_scraper.py +++ b/circuit-laundry-notifier/circuit_scraper.py @@ -4,6 +4,7 @@ from typing import * import requests from bs4 import BeautifulSoup +from cachetools import cached, TTLCache class ScraperError(ValueError): @@ -52,6 +53,7 @@ class CircuitScraper: return CircuitScraper._base_url + f"/?site={site_id}" @staticmethod + @cached(cache=TTLCache(maxsize=64, ttl=30)) def get_site_machine_states(site_id: str) -> List[Machine]: site_url = CircuitScraper._get_site_url(site_id) @@ -120,3 +122,13 @@ class CircuitScraper: machines.append(machine) 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 diff --git a/circuit-laundry-notifier/static/main.js b/circuit-laundry-notifier/static/main.js new file mode 100644 index 0000000..fa9aa7e --- /dev/null +++ b/circuit-laundry-notifier/static/main.js @@ -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 = {} \ No newline at end of file diff --git a/circuit-laundry-notifier/static/selectMachine.js b/circuit-laundry-notifier/static/selectMachine.js new file mode 100644 index 0000000..3261c07 --- /dev/null +++ b/circuit-laundry-notifier/static/selectMachine.js @@ -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) + }) +} \ No newline at end of file diff --git a/circuit-laundry-notifier/static/selectSite.js b/circuit-laundry-notifier/static/selectSite.js new file mode 100644 index 0000000..59e0336 --- /dev/null +++ b/circuit-laundry-notifier/static/selectSite.js @@ -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) + }) + }) +} \ No newline at end of file diff --git a/circuit-laundry-notifier/static/siteSelect.js b/circuit-laundry-notifier/static/siteSelect.js deleted file mode 100644 index 62c3a17..0000000 --- a/circuit-laundry-notifier/static/siteSelect.js +++ /dev/null @@ -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() - }) -} \ No newline at end of file diff --git a/circuit-laundry-notifier/static/submit.js b/circuit-laundry-notifier/static/submit.js new file mode 100644 index 0000000..7f0121a --- /dev/null +++ b/circuit-laundry-notifier/static/submit.js @@ -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 ${responseBody["ntfy_topic"]}`) + 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) + }) + }) +} \ No newline at end of file diff --git a/circuit-laundry-notifier/static/util.js b/circuit-laundry-notifier/static/util.js deleted file mode 100644 index 8f04e17..0000000 --- a/circuit-laundry-notifier/static/util.js +++ /dev/null @@ -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() -} \ No newline at end of file diff --git a/circuit-laundry-notifier/templates/index.html b/circuit-laundry-notifier/templates/index.html index 300be6e..ae2e7d8 100644 --- a/circuit-laundry-notifier/templates/index.html +++ b/circuit-laundry-notifier/templates/index.html @@ -9,12 +9,6 @@ - -

CircuitBodge

@@ -36,14 +30,30 @@
+ + - - + + + + diff --git a/circuit-laundry-notifier/web.py b/circuit-laundry-notifier/web.py index e4330bc..f5589df 100644 --- a/circuit-laundry-notifier/web.py +++ b/circuit-laundry-notifier/web.py @@ -5,8 +5,15 @@ from threading import Lock from typing import * 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: @@ -82,11 +89,54 @@ def _api_register_watcher(site_id: str) -> Union[any, Tuple[any, int]]: if machine_number is None: 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() ws = WatchState(site_id, ntfy_topic, machine_number) jobs.append(ws) 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 @@ -142,8 +192,20 @@ def _api_run_watcher() -> Union[any, Tuple[any, int]]: continue if target_machine.state == MachineState.Completed: - # TODO: Notify ntfy - print("DONE " + str(ws)) + requests.post( + 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) continue diff --git a/poetry.lock b/poetry.lock index 23aeaba..b4ef544 100644 --- a/poetry.lock +++ b/poetry.lock @@ -13,6 +13,14 @@ soupsieve = ">1.2" html5lib = ["html5lib"] lxml = ["lxml"] +[[package]] +name = "cachetools" +version = "5.2.0" +description = "Extensible memoizing collections and decorators" +category = "main" +optional = false +python-versions = "~=3.7" + [[package]] name = "certifi" version = "2022.9.24" @@ -192,10 +200,11 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "flake8 [metadata] lock-version = "1.1" python-versions = "^3.8" -content-hash = "28f817d195ff20de983a656a216fbdaac203af52239ca31ed0bf9676eee92bb6" +content-hash = "00993ecc54f6327f8e58e6b91ce934b966e97c8db4bdb2c7a973c4101b20cb1c" [metadata.files] beautifulsoup4 = [] +cachetools = [] certifi = [] charset-normalizer = [] click = [] diff --git a/pyproject.toml b/pyproject.toml index 739ad06..bf4ac0e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,7 @@ python = "^3.8" requests = "^2.28.1" beautifulsoup4 = "^4.11.1" Flask = "^2.2.2" +cachetools = "^5.2.0" [tool.poetry.dev-dependencies]