app.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. #!/usr/bin/env python3
  2. import os
  3. import tempfile
  4. import threading
  5. import time
  6. from flask import Flask, jsonify, request, abort, render_template_string
  7. app = Flask(__name__)
  8. VALUE_FILE = "/data/pct"
  9. _write_lock = threading.Lock()
  10. # --- Helpers ---
  11. def _clamp_to_range(val: int) -> int:
  12. return max(0, min(99, int(val)))
  13. def _ensure_file_exists():
  14. if not os.path.exists(VALUE_FILE):
  15. _atomic_write("0")
  16. def _read_value() -> int:
  17. with open(VALUE_FILE, "r", encoding="utf-8") as f:
  18. raw = f.read().strip()
  19. if raw == "":
  20. return 0
  21. return _clamp_to_range(int(raw))
  22. def _atomic_write(text: str):
  23. dir_name = os.path.dirname(os.path.abspath(VALUE_FILE)) or "."
  24. os.makedirs(dir_name, exist_ok=True)
  25. fd, tmp_path = tempfile.mkstemp(prefix=".value-", dir=dir_name, text=True)
  26. with open(VALUE_FILE, "w") as tmp:
  27. tmp.write(text)
  28. tmp.flush()
  29. os.fsync(tmp.fileno())
  30. def _get_mtime() -> float:
  31. try:
  32. return os.path.getmtime(VALUE_FILE)
  33. except FileNotFoundError:
  34. return 0.0
  35. def _fmt_time(mtime: float) -> str:
  36. if not mtime:
  37. return "never"
  38. return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(mtime))
  39. # --- Routes ---
  40. @app.route("/")
  41. def index():
  42. _ensure_file_exists()
  43. val = _read_value()
  44. mtime = _get_mtime()
  45. html = """
  46. <!doctype html>
  47. <html>
  48. <head>
  49. <meta charset="utf-8"/>
  50. <title>Astoria Manual Control</title>
  51. <style>
  52. body { font-family: sans-serif; margin: 2em; }
  53. .value { font-size: 64px; margin-bottom: 20px; }
  54. button { font-size: 24px; margin: 6px; padding: 12px 20px; }
  55. </style>
  56. </head>
  57. <body>
  58. <h1>Astoria Manual Control</h1>
  59. <div class="value" id="val">{{value}}</div>
  60. <div id="mtime">Last updated: {{mtime}}</div>
  61. <div>
  62. <button onclick="setVal(0)">0</button>
  63. <button onclick="setVal(7)">7</button>
  64. <button onclick="setVal(33)">33</button>
  65. <button onclick="setVal(99)">99</button>
  66. </div>
  67. <div>
  68. <input type="number" id="num" min="0" max="99" value="{{value}}"/>
  69. <button onclick="saveNum()">Save</button>
  70. </div>
  71. <script>
  72. let lastMtime = {{mtime_num}};
  73. async function waitForChange() {
  74. try {
  75. const resp = await fetch("/wait?since=" + lastMtime);
  76. if (!resp.ok) throw new Error("HTTP " + resp.status);
  77. const data = await resp.json();
  78. document.getElementById("val").textContent = data.value;
  79. document.getElementById("mtime").textContent = "Last updated: " + data.mtime_human;
  80. document.getElementById("num").value = data.value;
  81. lastMtime = data.mtime;
  82. } catch (e) {
  83. console.error("Polling failed:", e);
  84. }
  85. waitForChange(); // restart long poll
  86. }
  87. async function setVal(v) {
  88. await fetch("/set", {
  89. method: "POST",
  90. headers: { "Content-Type": "application/json" },
  91. body: JSON.stringify({ value: v })
  92. });
  93. }
  94. function saveNum() {
  95. const v = parseInt(document.getElementById("num").value);
  96. setVal(v);
  97. }
  98. waitForChange();
  99. </script>
  100. </body>
  101. </html>
  102. """
  103. return render_template_string(html, value=val, mtime=_fmt_time(mtime), mtime_num=mtime)
  104. @app.route("/set", methods=["POST"])
  105. def set_value():
  106. if not request.is_json:
  107. abort(400, "Expected JSON")
  108. body = request.get_json(silent=True) or {}
  109. if "value" not in body:
  110. abort(400, "Missing value")
  111. try:
  112. new_val = _clamp_to_range(int(body["value"]))
  113. except Exception:
  114. abort(400, "Value must be int 0–99")
  115. with _write_lock:
  116. _atomic_write(str(new_val))
  117. mtime = _get_mtime()
  118. return jsonify(value=new_val, mtime=mtime, mtime_human=_fmt_time(mtime))
  119. @app.route("/wait")
  120. def wait_for_change():
  121. """Long poll: wait until file changes after `since` timestamp."""
  122. since = float(request.args.get("since", "0"))
  123. timeout = 30 # max seconds to wait
  124. start = time.time()
  125. while time.time() - start < timeout:
  126. cur_mtime = _get_mtime()
  127. if cur_mtime > since:
  128. val = _read_value()
  129. return jsonify(value=val, mtime=cur_mtime, mtime_human=_fmt_time(cur_mtime))
  130. time.sleep(0.2) # avoid busy loop
  131. # timeout, respond with no change
  132. return jsonify(value=_read_value(), mtime=_get_mtime(), mtime_human=_fmt_time(_get_mtime()))
  133. if __name__ == "__main__":
  134. _ensure_file_exists()
  135. app.run(host="0.0.0.0", port=8080, debug=True)