as flask without docker
This commit is contained in:
+4
-2
@@ -1,6 +1,8 @@
|
|||||||
HUE_BRIDGE_HOST=192.168.1.2
|
HUE_BRIDGE_HOST=192.168.1.2
|
||||||
HUE_APPLICATION_KEY=replace-with-your-hue-application-key
|
HUE_APPLICATION_KEY=130987023498cb710109387401983427
|
||||||
HUE_VERIFY_TLS=false
|
HUE_VERIFY_TLS=false
|
||||||
COLLECT_INTERVAL_SECONDS=30
|
COLLECT_INTERVAL_SECONDS=30
|
||||||
LOG_LEVEL=INFO
|
LOG_LEVEL=INFO
|
||||||
PUBLISH_ADDRESS=0.0.0.0
|
LISTEN_HOST=0.0.0.0
|
||||||
|
LISTEN_PORT=8000
|
||||||
|
DATA_DIR=data
|
||||||
|
|||||||
-11
@@ -1,11 +0,0 @@
|
|||||||
FROM debian:bookworm-slim
|
|
||||||
|
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get install -y --no-install-recommends python3 python3-requests python3-rrdtool fonts-dejavu-core ca-certificates \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
COPY hue_collector ./hue_collector
|
|
||||||
|
|
||||||
EXPOSE 8000
|
|
||||||
CMD ["python3", "-m", "hue_collector"]
|
|
||||||
@@ -1,24 +1,29 @@
|
|||||||
# Lightweight Hue history
|
# Hue RRD Flask dashboard
|
||||||
|
|
||||||
A small service for Raspberry Pi that reads the Philips Hue v2 API, stores history in
|
A lightweight Flask application for Raspberry Pi that reads the Philips Hue v2 API,
|
||||||
fixed-size [RRDtool](https://oss.oetiker.ch/rrdtool/) databases, and serves its own web
|
stores history in fixed-size RRDtool databases, and serves a local web dashboard.
|
||||||
dashboard. It does not require Grafana, VictoriaMetrics, Node.js, or a browser-side
|
|
||||||
charting library.
|
|
||||||
|
|
||||||
The dashboard contains:
|
The dashboard contains:
|
||||||
|
|
||||||
- One graph for every room and zone. Unavailable, off, and on lights form a stacked
|
- One graph for every room and zone. Unavailable, off, and on lights form a stacked
|
||||||
percentage area, with average intensity of shining lights drawn as a line.
|
percentage area, with average intensity of shining lights drawn as a line.
|
||||||
- Temperature and illuminance graphs for every available Hue sensor.
|
- Temperature and illuminance graphs for every Hue sensor.
|
||||||
- 6-hour, 24-hour, 7-day, and 30-day time ranges.
|
- 6-hour, 24-hour, 7-day, and 30-day time ranges.
|
||||||
|
|
||||||
RRD storage is bounded automatically: raw samples are retained for 7 days,
|
RRD storage remains bounded automatically: raw samples are retained for 7 days,
|
||||||
approximately five-minute averages for 90 days, and hourly averages for two years.
|
approximately five-minute averages for 90 days, and hourly averages for two years.
|
||||||
|
|
||||||
## Configure Hue
|
## Install on Raspberry Pi OS or Debian
|
||||||
|
|
||||||
Find the bridge IP in the Hue app under **Settings → My Hue system → System
|
Install the native RRDtool binding and Python dependencies:
|
||||||
information**. Then create an application key while physically near the bridge:
|
|
||||||
|
```bash
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install python3 python3-flask python3-requests python3-rrdtool fonts-dejavu-core
|
||||||
|
```
|
||||||
|
|
||||||
|
Create the Hue application key if you do not have one yet. Press the bridge link
|
||||||
|
button immediately before running:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -k -X POST https://BRIDGE_IP/api \
|
curl -k -X POST https://BRIDGE_IP/api \
|
||||||
@@ -26,52 +31,72 @@ curl -k -X POST https://BRIDGE_IP/api \
|
|||||||
-d '{"devicetype":"local-hue-collector"}'
|
-d '{"devicetype":"local-hue-collector"}'
|
||||||
```
|
```
|
||||||
|
|
||||||
Press the bridge link button immediately before running the command. The returned
|
The returned `username` is the application key.
|
||||||
`username` is the application key.
|
|
||||||
|
|
||||||
## Run with Docker Compose
|
Configure and run the Flask application from the project directory:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
# Edit .env with the bridge IP and application key.
|
# Edit .env with the bridge IP and application key.
|
||||||
docker compose up --build -d --remove-orphans
|
set -a
|
||||||
```
|
. ./.env
|
||||||
|
set +a
|
||||||
Open `http://PI_ADDRESS:8000`. RRD data persists in the `hue-rrd-data` Docker volume.
|
|
||||||
The `--remove-orphans` option removes containers from the previous
|
|
||||||
VictoriaMetrics/Grafana version; their old named volumes are not deleted.
|
|
||||||
|
|
||||||
By default, port 8000 binds to all interfaces. To bind only to the Pi's LAN interface,
|
|
||||||
put its exact address in `.env`, for example:
|
|
||||||
|
|
||||||
```env
|
|
||||||
PUBLISH_ADDRESS=192.168.178.42
|
|
||||||
```
|
|
||||||
|
|
||||||
## Run directly on Raspberry Pi OS/Debian
|
|
||||||
|
|
||||||
This avoids Docker overhead entirely:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo apt update
|
|
||||||
sudo apt install python3 python3-requests python3-rrdtool fonts-dejavu-core
|
|
||||||
mkdir -p "$HOME/.local/share/hue-collector"
|
|
||||||
export HUE_BRIDGE_HOST=192.168.178.2
|
|
||||||
export HUE_APPLICATION_KEY=your-key
|
|
||||||
export DATA_DIR="$HOME/.local/share/hue-collector"
|
|
||||||
python3 -m hue_collector
|
python3 -m hue_collector
|
||||||
```
|
```
|
||||||
|
|
||||||
The page is served on port 8000. For continuous operation, run it with systemd or use
|
Open `http://PI_ADDRESS:8000`. The RRD files are written to `DATA_DIR`, which defaults
|
||||||
the Compose setup with `restart: unless-stopped`.
|
to the project's `data/` directory.
|
||||||
|
|
||||||
TLS verification is disabled by default because Hue bridges normally use a
|
## Run continuously with systemd
|
||||||
self-signed certificate. Set `HUE_VERIFY_TLS=true` if the bridge has a trusted
|
|
||||||
certificate. The health check is available at `/healthz`.
|
Install the project and environment file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo install -d /opt/hue-collector
|
||||||
|
sudo cp -r hue_collector /opt/hue-collector/
|
||||||
|
sudo cp .env /etc/hue-collector
|
||||||
|
sudo chmod 600 /etc/hue-collector
|
||||||
|
sudo cp hue-collector.service /etc/systemd/system/
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable --now hue-collector
|
||||||
|
```
|
||||||
|
|
||||||
|
The included unit uses a restricted dynamic service account and stores RRD files in
|
||||||
|
`/var/lib/hue-collector`. Inspect it with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl status hue-collector
|
||||||
|
journalctl -u hue-collector -f
|
||||||
|
```
|
||||||
|
|
||||||
|
After updating the application files, restart it with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl restart hue-collector
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
- `HUE_BRIDGE_HOST`: Hue bridge IP or hostname; required.
|
||||||
|
- `HUE_APPLICATION_KEY`: Hue v2 application key; required.
|
||||||
|
- `HUE_VERIFY_TLS`: verify the bridge certificate; defaults to `false` because Hue
|
||||||
|
bridges normally use a self-signed certificate.
|
||||||
|
- `COLLECT_INTERVAL_SECONDS`: sampling interval; defaults to 30 seconds. Changing it
|
||||||
|
after RRD files have been created does not change those existing files' step.
|
||||||
|
- `LISTEN_HOST`: Flask bind address; defaults to `0.0.0.0` for LAN access.
|
||||||
|
- `LISTEN_PORT`: dashboard port; defaults to `8000`.
|
||||||
|
- `DATA_DIR`: RRD directory; defaults to `data`.
|
||||||
|
|
||||||
|
The application is intended for a trusted home network and does not provide
|
||||||
|
authentication or TLS. Restrict access with the Pi firewall if the network is not
|
||||||
|
trusted.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
The collector tests do not require RRDtool:
|
On a Debian-based machine, install `python3-rrdtool` through apt. Flask and Requests
|
||||||
|
can alternatively be installed from `requirements.txt`. When using a virtual
|
||||||
|
environment, create it with `--system-site-packages` so it can see the apt-installed
|
||||||
|
RRDtool module.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 -m unittest discover -s tests
|
python3 -m unittest discover -s tests
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
services:
|
|
||||||
hue-collector:
|
|
||||||
build: .
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
HUE_BRIDGE_HOST: ${HUE_BRIDGE_HOST:-}
|
|
||||||
HUE_APPLICATION_KEY: ${HUE_APPLICATION_KEY:-}
|
|
||||||
HUE_VERIFY_TLS: ${HUE_VERIFY_TLS:-false}
|
|
||||||
COLLECT_INTERVAL_SECONDS: ${COLLECT_INTERVAL_SECONDS:-30}
|
|
||||||
LOG_LEVEL: ${LOG_LEVEL:-INFO}
|
|
||||||
DATA_DIR: /data
|
|
||||||
volumes:
|
|
||||||
- hue-rrd-data:/data
|
|
||||||
ports:
|
|
||||||
- "${PUBLISH_ADDRESS:-0.0.0.0}:8000:8000"
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/healthz')"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 3
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
hue-rrd-data:
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Philips Hue RRD collector and dashboard
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
DynamicUser=yes
|
||||||
|
StateDirectory=hue-collector
|
||||||
|
WorkingDirectory=/srv/service_hue_collector
|
||||||
|
EnvironmentFile=/srv/service_hue_collector
|
||||||
|
Environment=DATA_DIR=/var/lib/hue-collector
|
||||||
|
ExecStart=/var/lib/hue-collector/.venv/bin/python3 -m hue_collector
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
PrivateTmp=yes
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=yes
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
+61
-79
@@ -1,17 +1,35 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import html
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
from io import BytesIO
|
||||||
from urllib.parse import parse_qs, urlencode, urlparse
|
|
||||||
|
from flask import Flask, Response, abort, render_template_string, request, send_file, url_for
|
||||||
|
|
||||||
from .collector import Collector, HueClient, Settings
|
from .collector import Collector, HueClient, Settings
|
||||||
from .rrd_store import RRDStore
|
from .rrd_store import RRDStore
|
||||||
|
|
||||||
LOG = logging.getLogger("hue-collector")
|
LOG = logging.getLogger("hue-collector")
|
||||||
|
PERIODS = ("6h", "24h", "7d", "30d")
|
||||||
|
|
||||||
|
PAGE = '''<!doctype html><html lang="en"><head><meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1"><meta http-equiv="refresh" content="60">
|
||||||
|
<title>Hue history</title><style>
|
||||||
|
:root{color-scheme:dark;font-family:system-ui,sans-serif}body{margin:auto;max-width:1200px;padding:1rem;background:#0b1220;color:#e5e7eb}
|
||||||
|
header{display:flex;gap:1rem;align-items:center;justify-content:space-between;flex-wrap:wrap}nav a{padding:.45rem .7rem;color:#93c5fd;text-decoration:none}
|
||||||
|
nav a.active{background:#1d4ed8;color:white;border-radius:.35rem}.status,p{color:#9ca3af}section{background:#111827;border:1px solid #374151;border-radius:.6rem;padding:.75rem;margin:1rem 0}
|
||||||
|
h1,h2{margin:.25rem 0 .5rem}h2{font-size:1.1rem}p{margin:.25rem 0}.error{color:#fca5a5}img{display:block;width:100%;height:auto}
|
||||||
|
</style></head><body><header><div><h1>Philips Hue</h1><div class="status">{{ status }}{% if error %} — <span class="error">{{ error }}</span>{% endif %}</div></div>
|
||||||
|
<nav>{% for value in periods %}<a class="{{ 'active' if value == period else '' }}" href="{{ url_for('dashboard', range=value) }}">{{ value }}</a>{% endfor %}</nav></header>
|
||||||
|
<h1>Rooms and zones</h1>{% for item in groups %}<section><h2>{{ item.type|title }}: {{ item.name }}</h2>
|
||||||
|
<img loading="lazy" src="{{ url_for('graph', kind='group', item_id=item.id, range=period) }}" alt="Light history"></section>
|
||||||
|
{% else %}<p>No groups collected yet.</p>{% endfor %}
|
||||||
|
<h1>Sensors</h1>{% for item in sensors %}<section><h2>{{ item.name }}</h2>
|
||||||
|
<p>{{ item.room or item.zones or 'No room' }} · {% if item.value is none %}unavailable{% else %}{{ '%.1f'|format(item.value) }} {{ item.unit }}{% endif %}</p>
|
||||||
|
<img loading="lazy" src="{{ url_for('graph', kind='sensor', item_id=item.id, range=period) }}" alt="Sensor history"></section>
|
||||||
|
{% else %}<p>No sensors collected yet.</p>{% endfor %}</body></html>'''
|
||||||
|
|
||||||
|
|
||||||
class State:
|
class State:
|
||||||
@@ -37,93 +55,57 @@ def collection_loop(collector: Collector, store: RRDStore, state: State, interva
|
|||||||
time.sleep(max(1, interval - (time.monotonic() - started)))
|
time.sleep(max(1, interval - (time.monotonic() - started)))
|
||||||
|
|
||||||
|
|
||||||
def page(state: State, period: str) -> bytes:
|
def create_app(settings: Settings | None = None, *, start_collector: bool = True) -> Flask:
|
||||||
with state.lock:
|
settings = settings or Settings.from_env()
|
||||||
snapshot = state.snapshot
|
app = Flask(__name__)
|
||||||
last_success = state.last_success
|
state = State()
|
||||||
error = state.error
|
store = RRDStore(settings.data_dir, settings.interval)
|
||||||
status = f"Last update: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(last_success))}" if last_success else "Waiting for first collection"
|
app.extensions["hue_state"] = state
|
||||||
if error:
|
app.extensions["hue_rrd_store"] = store
|
||||||
status += f" — Error: {html.escape(error)}"
|
|
||||||
controls = " ".join(
|
|
||||||
f'<a class="{("active" if value == period else "")}" href="/?range={value}">{value}</a>'
|
|
||||||
for value in ("6h", "24h", "7d", "30d")
|
|
||||||
)
|
|
||||||
group_cards = []
|
|
||||||
for item in sorted(snapshot["groups"], key=lambda value: (value["type"], value["name"].lower())):
|
|
||||||
query = urlencode({"kind": "group", "id": item["id"], "range": period})
|
|
||||||
group_cards.append(
|
|
||||||
f'<section><h2>{html.escape(item["type"].title())}: {html.escape(item["name"])}</h2>'
|
|
||||||
f'<img loading="lazy" src="/graph?{query}" alt="Light history"></section>'
|
|
||||||
)
|
|
||||||
sensor_cards = []
|
|
||||||
for item in sorted(snapshot["sensors"], key=lambda value: (value["type"], value["name"].lower())):
|
|
||||||
query = urlencode({"kind": "sensor", "id": item["id"], "range": period})
|
|
||||||
value = "unavailable" if item["value"] is None else f'{item["value"]:.1f} {item["unit"]}'
|
|
||||||
location = item["room"] or item["zones"] or "No room"
|
|
||||||
sensor_cards.append(
|
|
||||||
f'<section><h2>{html.escape(item["name"])}</h2><p>{html.escape(location)} · {html.escape(value)}</p>'
|
|
||||||
f'<img loading="lazy" src="/graph?{query}" alt="Sensor history"></section>'
|
|
||||||
)
|
|
||||||
document = f'''<!doctype html><html lang="en"><head><meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1"><meta http-equiv="refresh" content="60">
|
|
||||||
<title>Hue history</title><style>
|
|
||||||
:root{{color-scheme:dark;font-family:system-ui,sans-serif}}body{{margin:auto;max-width:1200px;padding:1rem;background:#0b1220;color:#e5e7eb}}
|
|
||||||
header{{display:flex;gap:1rem;align-items:center;justify-content:space-between;flex-wrap:wrap}}nav a{{padding:.45rem .7rem;color:#93c5fd;text-decoration:none}}
|
|
||||||
nav a.active{{background:#1d4ed8;color:white;border-radius:.35rem}}.status,p{{color:#9ca3af}}section{{background:#111827;border:1px solid #374151;border-radius:.6rem;padding:.75rem;margin:1rem 0}}
|
|
||||||
h1,h2{{margin:.25rem 0 .5rem}}h2{{font-size:1.1rem}}p{{margin:.25rem 0}}img{{display:block;width:100%;height:auto}}
|
|
||||||
</style></head><body><header><div><h1>Philips Hue</h1><div class="status">{status}</div></div><nav>{controls}</nav></header>
|
|
||||||
<h1>Rooms and zones</h1>{''.join(group_cards) or '<p>No groups collected yet.</p>'}
|
|
||||||
<h1>Sensors</h1>{''.join(sensor_cards) or '<p>No sensors collected yet.</p>'}</body></html>'''
|
|
||||||
return document.encode()
|
|
||||||
|
|
||||||
|
if start_collector:
|
||||||
|
collector = Collector(HueClient(settings))
|
||||||
|
threading.Thread(target=collection_loop, args=(collector, store, state, settings.interval), daemon=True).start()
|
||||||
|
|
||||||
def handler_for(state: State, store: RRDStore):
|
@app.get("/")
|
||||||
class Handler(BaseHTTPRequestHandler):
|
def dashboard():
|
||||||
def do_GET(self):
|
period = request.args.get("range", "24h")
|
||||||
parsed = urlparse(self.path)
|
if period not in PERIODS:
|
||||||
query = parse_qs(parsed.query)
|
period = "24h"
|
||||||
if parsed.path == "/":
|
|
||||||
body, status, content_type = page(state, query.get("range", ["24h"])[0]), 200, "text/html; charset=utf-8"
|
|
||||||
elif parsed.path == "/healthz":
|
|
||||||
with state.lock:
|
with state.lock:
|
||||||
healthy = state.last_success is not None
|
groups = sorted(state.snapshot["groups"], key=lambda item: (item["type"], item["name"].lower()))
|
||||||
body, status, content_type = (b"ok\n", 200, "text/plain") if healthy else (b"not ready\n", 503, "text/plain")
|
sensors = sorted(state.snapshot["sensors"], key=lambda item: (item["type"], item["name"].lower()))
|
||||||
elif parsed.path == "/graph":
|
last_success, error = state.last_success, state.error
|
||||||
try:
|
status = time.strftime("Last update: %Y-%m-%d %H:%M:%S", time.localtime(last_success)) if last_success else "Waiting for first collection"
|
||||||
kind, item_id = query["kind"][0], query["id"][0]
|
return render_template_string(PAGE, groups=groups, sensors=sensors, status=status, error=error, periods=PERIODS, period=period)
|
||||||
|
|
||||||
|
@app.get("/graph/<kind>/<item_id>.png")
|
||||||
|
def graph(kind: str, item_id: str):
|
||||||
if kind not in {"group", "sensor"}:
|
if kind not in {"group", "sensor"}:
|
||||||
raise ValueError("unknown graph kind")
|
abort(404)
|
||||||
with state.lock:
|
with state.lock:
|
||||||
collection = state.snapshot["groups" if kind == "group" else "sensors"]
|
collection = state.snapshot["groups" if kind == "group" else "sensors"]
|
||||||
item = next(value for value in collection if value["id"] == item_id)
|
item = next((value for value in collection if value["id"] == item_id), None)
|
||||||
body = store.graph(kind, item, query.get("range", ["24h"])[0])
|
if item is None:
|
||||||
status, content_type = 200, "image/png"
|
abort(404)
|
||||||
except (KeyError, ValueError, StopIteration, FileNotFoundError) as exc:
|
try:
|
||||||
body, status, content_type = f"graph not found: {exc}\n".encode(), 404, "text/plain"
|
image = store.graph(kind, item, request.args.get("range", "24h"))
|
||||||
except Exception as exc:
|
except (ValueError, FileNotFoundError):
|
||||||
LOG.exception("Graph rendering failed")
|
abort(404)
|
||||||
body, status, content_type = f"graph error: {exc}\n".encode(), 500, "text/plain"
|
return send_file(BytesIO(image), mimetype="image/png", max_age=30)
|
||||||
else:
|
|
||||||
body, status, content_type = b"not found\n", 404, "text/plain"
|
|
||||||
self.send_response(status)
|
|
||||||
self.send_header("Content-Type", content_type)
|
|
||||||
self.send_header("Content-Length", str(len(body)))
|
|
||||||
self.end_headers()
|
|
||||||
self.wfile.write(body)
|
|
||||||
|
|
||||||
def log_message(self, fmt, *args):
|
@app.get("/healthz")
|
||||||
LOG.debug(fmt, *args)
|
def health():
|
||||||
|
with state.lock:
|
||||||
|
healthy = state.last_success is not None
|
||||||
|
return Response("ok\n" if healthy else "not ready\n", status=200 if healthy else 503, mimetype="text/plain")
|
||||||
|
|
||||||
return Handler
|
return app
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
logging.basicConfig(level=os.environ.get("LOG_LEVEL", "INFO"), format="%(asctime)s %(levelname)s %(message)s")
|
logging.basicConfig(level=os.environ.get("LOG_LEVEL", "INFO"), format="%(asctime)s %(levelname)s %(message)s")
|
||||||
settings = Settings.from_env()
|
settings = Settings.from_env()
|
||||||
state = State()
|
app = create_app(settings)
|
||||||
store = RRDStore(settings.data_dir, settings.interval)
|
|
||||||
collector = Collector(HueClient(settings))
|
|
||||||
threading.Thread(target=collection_loop, args=(collector, store, state, settings.interval), daemon=True).start()
|
|
||||||
LOG.info("Serving Hue dashboard on http://%s:%s", settings.listen_host, settings.listen_port)
|
LOG.info("Serving Hue dashboard on http://%s:%s", settings.listen_host, settings.listen_port)
|
||||||
ThreadingHTTPServer((settings.listen_host, settings.listen_port), handler_for(state, store)).serve_forever()
|
app.run(host=settings.listen_host, port=settings.listen_port, debug=False, use_reloader=False)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class Settings:
|
|||||||
interval: int = 30
|
interval: int = 30
|
||||||
listen_host: str = "0.0.0.0"
|
listen_host: str = "0.0.0.0"
|
||||||
listen_port: int = 8000
|
listen_port: int = 8000
|
||||||
data_dir: str = "/data"
|
data_dir: str = "data"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_env(cls) -> "Settings":
|
def from_env(cls) -> "Settings":
|
||||||
@@ -31,7 +31,7 @@ class Settings:
|
|||||||
interval=int(os.environ.get("COLLECT_INTERVAL_SECONDS", "30")),
|
interval=int(os.environ.get("COLLECT_INTERVAL_SECONDS", "30")),
|
||||||
listen_host=os.environ.get("LISTEN_HOST", "0.0.0.0"),
|
listen_host=os.environ.get("LISTEN_HOST", "0.0.0.0"),
|
||||||
listen_port=int(os.environ.get("LISTEN_PORT", "8000")),
|
listen_port=int(os.environ.get("LISTEN_PORT", "8000")),
|
||||||
data_dir=os.environ.get("DATA_DIR", "/data"),
|
data_dir=os.environ.get("DATA_DIR", "data"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Install the native RRDtool binding with: apt install python3-rrdtool
|
||||||
|
Flask>=2.2,<4
|
||||||
|
requests>=2,<3
|
||||||
Reference in New Issue
Block a user