Skip to content

Commit 98810bc

Browse files
committed
add UPS monitoring feature and enhance maintenance page with UPS status display
1 parent 25bf15d commit 98810bc

File tree

6 files changed

+401
-181
lines changed

6 files changed

+401
-181
lines changed

README.md

Lines changed: 81 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
# Cloudflare Worker Error Page
99

1010
This project allows you to deploy a custom error page using a Cloudflare Worker.
11-
With an option for enable maintenance mod, add a banner to a specific or all domain and show a banner when your LTE backup is active
11+
With an option for enable maintenance mod, add a banner to a specific or all domain, show a banner when your LTE backup is active and show a banner when your UPS is on battery
1212

1313
⚠️ For Now only work with Cloudflare tunnel (Zero trust)
1414

@@ -122,9 +122,46 @@ docker run -e CF_ACCOUNT_ID=Your_cloudflare_account_id \
122122
- Click on **Continue to summary** and **Create token**
123123
- SLEEP_SECONDS is how often the container will check the server's IP address.
124124

125-
### 7. Add Auth on your maintenance page
125+
### 7. OPTIONAL Add UPS monitoring via NUT (Network UPS Tools)
126126

127-
TO DO
127+
The Docker container can also monitor your UPS via a NUT server and display a banner when the UPS is running on battery.
128+
129+
- On wrangler.toml set `ENABLE_UPS_BANNER = true`
130+
- Customize the banner message with `TEXT_UPS_BANNER_MESSAGE`
131+
- Your NUT server must be accessible from the Docker container on port 3493 (default)
132+
133+
Add these environment variables to your Docker container:
134+
135+
```bash
136+
docker run -e CF_ACCOUNT_ID=Your_cloudflare_account_id \
137+
-e CF_NAMESPACE_ID=Your_cloudflare_namespace_id \
138+
-e CF_API_TOKEN=Your_cloudflare_api_token \
139+
-e ENABLE_UPS_CHECK=true \
140+
-e NUT_HOST=192.168.1.100 \
141+
-e NUT_UPS_NAME=ups \
142+
-e NUT_PORT=3493 \
143+
-e KV_UPS_KEY=ups-on-battery \
144+
-e SLEEP_SECONDS=60 \
145+
ghcr.io/jamesdadams/cloudflare-worker-error-page:latest
146+
```
147+
148+
| Variable | Description | Default |
149+
|----------|-------------|---------|
150+
| `ENABLE_UPS_CHECK` | Enable UPS monitoring | `false` |
151+
| `NUT_HOST` | IP address of your NUT server | (required) |
152+
| `NUT_UPS_NAME` | Name of the UPS in NUT (e.g. `ups`, `eaton`) | (required) |
153+
| `NUT_PORT` | NUT server port | `3493` |
154+
| `KV_UPS_KEY` | KV key name for UPS status | `ups-on-battery` |
155+
156+
The container queries `ups.status` from the NUT server. When the status contains `OB` (On Battery), the banner is displayed on all pages.
157+
158+
You can also manually toggle UPS mode from the admin panel or via the API:
159+
- `POST /worker/api/toggle-ups-mode` - Toggle UPS battery state
160+
- `POST /worker/api/ups-mode` with body `{ "enabled": true/false }` - Set UPS state
161+
162+
### 8. Add Auth on your maintenance page
163+
164+
TO DO
128165

129166
---
130167

@@ -176,9 +213,10 @@ TO DO
176213

177214
Ce projet vous permet de déployer des pages d'erreur personnalisée à l'aide d'un Cloudflare Worker
178215

179-
- Un mode maintenance,
216+
- Un mode maintenance,
180217
- Ajouter une bannière à un ou plusieurs domaines spécifiques
181218
- Afficher une bannière lorsque votre backup LTE est actif.
219+
- Afficher une bannière lorsque votre onduleur (UPS) fonctionne sur batterie.
182220
- Un bouton pour vous signaler une erreur qui envoie une notification sur Discord
183221

184222
![presentation](images/other/presentation.png)
@@ -197,6 +235,7 @@ Ce projet vous permet de déployer des pages d'erreur personnalisée à l'aide d
197235
- Définissez votre langue (FR ou EN)
198236
- Si vous ne voulez pas avoir la fonctionnalité pour signaler une erreur qui permet d'envoyer un message Discord faite ceci : ```ENABLE_REPORT_ERROR = false```
199237
- Si vous n'avez pas de backup 4g sur votre serveur, faite ceci : ```ENABLE_4G_BANNER = false ```
238+
- Si vous avez un onduleur (UPS) avec un serveur NUT, activez le bandeau UPS : ```ENABLE_UPS_BANNER = true ```
200239
- Normalement, ce n'est pas nécessaire de modifier, mais vous pouvez pour chaque message d'erreur ajouter son code d'erreur pour `TEXT_BOX_ERROR_CODE`, `TEXT_TUNNEL_ERROR_CODE` et `TEXT_CONTAINER_ERROR_CODE`
201240
- Modifiez le texte des différents messages d'erreur si vous le voulez
202241
### 3. Créez un namespace KV
@@ -278,7 +317,44 @@ docker run -e CF_ACCOUNT_ID=Votre_id_compte_cloudflare \
278317
- ![Créer worker](images/backup4g/backup4g_3.png)
279318
- SLEEP_SECONDS définit la fréquence à laquelle le conteneur vérifiera l'adresse IP du serveur.
280319

281-
### 7. Ajoutez une authentification sur votre page de maintenance
320+
### 7. OPTIONNEL : Ajoutez la surveillance de l'onduleur (UPS) via NUT
321+
322+
Le conteneur Docker peut aussi surveiller votre onduleur via un serveur NUT (Network UPS Tools) et afficher un bandeau quand l'UPS fonctionne sur batterie.
323+
324+
- Dans wrangler.toml, mettez `ENABLE_UPS_BANNER = true`
325+
- Personnalisez le message avec `TEXT_UPS_BANNER_MESSAGE`
326+
- Votre serveur NUT doit être accessible depuis le conteneur Docker sur le port 3493 (par défaut)
327+
328+
Ajoutez ces variables d'environnement à votre conteneur Docker :
329+
330+
```bash
331+
docker run -e CF_ACCOUNT_ID=Votre_id_compte_cloudflare \
332+
-e CF_NAMESPACE_ID=Votre_id_namespace_cloudflare \
333+
-e CF_API_TOKEN=Votre_token_api_cloudflare \
334+
-e ENABLE_UPS_CHECK=true \
335+
-e NUT_HOST=192.168.1.100 \
336+
-e NUT_UPS_NAME=ups \
337+
-e NUT_PORT=3493 \
338+
-e KV_UPS_KEY=ups-on-battery \
339+
-e SLEEP_SECONDS=60 \
340+
ghcr.io/jamesdadams/cloudflare-worker-error-page:latest
341+
```
342+
343+
| Variable | Description | Défaut |
344+
|----------|-------------|--------|
345+
| `ENABLE_UPS_CHECK` | Activer la surveillance UPS | `false` |
346+
| `NUT_HOST` | Adresse IP de votre serveur NUT | (requis) |
347+
| `NUT_UPS_NAME` | Nom de l'UPS dans NUT (ex: `ups`, `eaton`) | (requis) |
348+
| `NUT_PORT` | Port du serveur NUT | `3493` |
349+
| `KV_UPS_KEY` | Nom de la clé KV pour le statut UPS | `ups-on-battery` |
350+
351+
Le conteneur interroge `ups.status` depuis le serveur NUT. Quand le statut contient `OB` (On Battery / sur batterie), le bandeau est affiché sur toutes les pages.
352+
353+
Vous pouvez aussi basculer manuellement le mode UPS depuis le panneau d'administration ou via l'API :
354+
- `POST /worker/api/toggle-ups-mode` - Basculer l'état batterie UPS
355+
- `POST /worker/api/ups-mode` avec le body `{ "enabled": true/false }` - Définir l'état UPS
356+
357+
### 8. Ajoutez une authentification sur votre page de maintenance
282358

283359
À FAIRE
284360

docker/check_ip_and_store.py

Lines changed: 85 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import requests
2+
import socket
23
import time
34
import os
45
from datetime import datetime
@@ -10,6 +11,13 @@
1011
KV_4G_KEY = os.environ.get("KV_4G_KEY", "wan-is-4g")
1112
SLEEP_SECONDS = int(os.environ.get("SLEEP_SECONDS", 60))
1213

14+
# UPS monitoring via NUT
15+
ENABLE_UPS_CHECK = os.environ.get("ENABLE_UPS_CHECK", "false").lower() == "true"
16+
NUT_HOST = os.environ.get("NUT_HOST", "")
17+
NUT_PORT = int(os.environ.get("NUT_PORT", 3493))
18+
NUT_UPS_NAME = os.environ.get("NUT_UPS_NAME", "")
19+
KV_UPS_KEY = os.environ.get("KV_UPS_KEY", "ups-on-battery")
20+
1321
def get_wan_ip():
1422
return requests.get("https://api.ipify.org").text.strip()
1523

@@ -32,34 +40,92 @@ def is_mobile_ip(ip):
3240
# ip-api.com returns 'mobile': True if the connection is mobile (4G/5G)
3341
return bool(data.get("mobile", False))
3442

43+
def get_ups_status(host, port, ups_name):
44+
"""Query NUT server for UPS status via raw socket.
45+
Returns the ups.status string (e.g. 'OL', 'OB', 'OB LB') or None on error.
46+
"""
47+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
48+
sock.settimeout(5)
49+
try:
50+
sock.connect((host, port))
51+
cmd = f"GET VAR {ups_name} ups.status\n"
52+
sock.sendall(cmd.encode())
53+
data = sock.recv(1024).decode().strip()
54+
# Expected: VAR <upsname> ups.status "OL"
55+
if data.startswith("VAR") and '"' in data:
56+
status = data.split('"')[1]
57+
return status
58+
return None
59+
finally:
60+
try:
61+
sock.sendall(b"LOGOUT\n")
62+
except Exception:
63+
pass
64+
sock.close()
65+
66+
def is_ups_on_battery(host, port, ups_name):
67+
"""Returns True if the UPS is running on battery (OB in status)."""
68+
status = get_ups_status(host, port, ups_name)
69+
if status is None:
70+
return None
71+
return "OB" in status
72+
3573
def log(msg, level="INFO"):
3674
now = datetime.now().isoformat(timespec="seconds")
3775
print(f"[{now}] [{level}] {msg}")
3876

77+
def check_ip(state):
78+
"""Check WAN IP and 4G status, update KV if changed. Returns updated state."""
79+
ip = get_wan_ip()
80+
log(f"Retrieved IP: {ip} (last: {state['ip']})")
81+
is_4g = is_mobile_ip(ip)
82+
log(f"Is mobile (4G/5G): {is_4g}")
83+
84+
if ip != state["ip"]:
85+
log("IP change detected, updating Cloudflare KV.")
86+
update_cloudflare_kv(KV_IP_KEY, ip)
87+
state["ip"] = ip
88+
89+
if is_4g != state["is_4g"]:
90+
log("Connection type change detected, updating Cloudflare KV.")
91+
update_cloudflare_kv(KV_4G_KEY, str(is_4g).lower())
92+
state["is_4g"] = is_4g
93+
94+
def check_ups(state):
95+
"""Check UPS battery status via NUT, update KV if changed. Returns updated state."""
96+
on_battery = is_ups_on_battery(NUT_HOST, NUT_PORT, NUT_UPS_NAME)
97+
if on_battery is None:
98+
log("Could not read UPS status from NUT server.", level="WARN")
99+
return
100+
101+
log(f"UPS on battery: {on_battery}")
102+
if on_battery != state["ups_on_battery"]:
103+
log("UPS status change detected, updating Cloudflare KV.")
104+
update_cloudflare_kv(KV_UPS_KEY, str(on_battery).lower())
105+
state["ups_on_battery"] = on_battery
106+
39107
def main():
40-
last_ip = None
41-
last_is_4g = None
108+
state = {"ip": None, "is_4g": None, "ups_on_battery": None}
109+
ups_enabled = ENABLE_UPS_CHECK and NUT_HOST and NUT_UPS_NAME
110+
111+
if ENABLE_UPS_CHECK and not ups_enabled:
112+
log("UPS check enabled but NUT_HOST or NUT_UPS_NAME not set, disabling UPS check.", level="WARN")
113+
elif ups_enabled:
114+
log(f"UPS monitoring enabled: {NUT_UPS_NAME}@{NUT_HOST}:{NUT_PORT}")
115+
42116
while True:
43117
try:
44-
log(f"Last IP: {last_ip}")
45-
ip = get_wan_ip()
46-
log(f"Retrieved IP: {ip}")
47-
is_4g = is_mobile_ip(ip)
48-
log(f"Is mobile (4G/5G): {is_4g}")
49-
50-
# Update if IP or connection type changed
51-
if ip != last_ip:
52-
log("IP change detected, updating Cloudflare KV.")
53-
update_cloudflare_kv(KV_IP_KEY, ip)
54-
last_ip = ip
55-
56-
if is_4g != last_is_4g:
57-
log("Connection type change detected, updating Cloudflare KV.")
58-
update_cloudflare_kv(KV_4G_KEY, str(is_4g).lower())
59-
last_is_4g = is_4g
118+
check_ip(state)
60119
except Exception as e:
61120
log(str(e), level="ERROR")
62-
time.sleep(60)
121+
122+
if ups_enabled:
123+
try:
124+
check_ups(state)
125+
except Exception as e:
126+
log(f"UPS check error: {e}", level="ERROR")
127+
128+
time.sleep(SLEEP_SECONDS)
63129

64130
if __name__ == "__main__":
65131
main()

0 commit comments

Comments
 (0)