second commit

This commit is contained in:
denis
2026-03-10 13:10:32 +01:00
parent b5cc0106c6
commit 3aac7ff9b9
101 changed files with 22046 additions and 1 deletions
View File
+42
View File
@@ -0,0 +1,42 @@
from django.utils.translation import gettext_lazy as _
from django.contrib import admin
from django_celery_beat.admin import PeriodicTaskAdmin, PeriodicTask
from .models import (Device, DeviceType)
class DeviceAdmin(admin.ModelAdmin):
list_display = ('label', 'uuid', 'image_tag', 'device_type__code', 'index', 'task', 'lifetime', 'task_d', 'active')
list_filter = ('device_type', 'active')
search_fields = ('label',)
readonly_fields = ('uuid',)
def task_e(self, instance):
if instance.task:
return instance.task.enabled
task_e.short_description = _('Activé')
def task_d(self, instance):
if instance.task:
return instance.task.description
task_d.short_description = _('Description')
def has_add_permission(self, request, obj=None):
return False
class DeviceTypeAdmin(admin.ModelAdmin):
list_display = ('name', 'code',)
search_fields = ('name', 'code')
class CustomPeriodicTaskAdmin(PeriodicTaskAdmin):
list_display = ('name', 'task', 'enabled', 'description', 'scheduler', 'interval',
'start_time', 'last_run_at', 'expires', 'one_off', 'total_run_count')
search_fields = ('name', 'task')
admin.site.unregister(PeriodicTask)
admin.site.register(PeriodicTask, CustomPeriodicTaskAdmin)
admin.site.register(DeviceType, DeviceTypeAdmin)
admin.site.register(Device, DeviceAdmin)
+5
View File
@@ -0,0 +1,5 @@
from django.apps import AppConfig
class RelayConfig(AppConfig):
name = 'relay'
+43
View File
@@ -0,0 +1,43 @@
#
#from django.utils.translation import gettext_lazy as _
#from channels.layers import get_channel_layer
#from django.conf import settings
#import asyncio
#from asgiref.sync import sync_to_async
import json, logging
from channels.generic.websocket import AsyncWebsocketConsumer
#from modules import utils
#from .models import Relay
from .process import redisDB
logger = logging.getLogger(__name__)
class RelayConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.uuid = self.scope["url_route"]["kwargs"].get("uuid")
self.this_group = f"relay_{self.uuid}"
await self.channel_layer.group_add(self.this_group, self.channel_name)
await self.accept()
#logger.info(f"==== relay {self.uuid} is connected to {self.this_group} group")
async def disconnect(self, close_code):
await self.channel_layer.group_discard(self.this_group, self.channel_name)
logger.info( f"Disconnect from {self.this_group}")
# routage channels => type: relay.message
async def relay_message(self, event):
await self.send(text_data=json.dumps(event["text"]))
## Receive message from WebSocket
async def receive(self, text_data):
data = json.loads(text_data)
msg_type = data.get("type")
if msg_type == "relay":
redisDB.publish(f"{msg_type}_{self.uuid}", json.dumps(data))
+115
View File
@@ -0,0 +1,115 @@
import json
from datetime import timedelta
from django.utils.translation import gettext_lazy as _
from django_resized import ResizedImageField
from django_celery_beat.models import PeriodicTask
from django.dispatch import receiver
from django.utils.text import slugify
from django.utils.safestring import mark_safe
from django.db.models.signals import post_save
from django.db import models
from modules import utils
class DeviceType(models.Model):
name = models.CharField(_("Nom du capteur"), max_length=64, null=True)
code = models.SlugField(_("Type de capteur"), help_text=_("Max 16 caractères"), unique=True, max_length=16, null=True, blank=True)
class Meta:
ordering = ['name']
verbose_name = _("Type de capteur")
verbose_name_plural = _("Types de capteurs")
def save(self, *args, **kwargs):
self.code = slugify(self.code)
super().save(*args, **kwargs)
def __str__(self):
return f'{self.name}'
class Device(models.Model):
label = models.CharField(_("Capteur"), max_length=100, null=True)
uuid = models.SlugField(_("Uuid"), help_text=_('Uuid iot clef unique'), unique=True, max_length=32, null=True, blank=True)
device_type = models.ForeignKey(DeviceType, verbose_name=_("Type de capteur"), on_delete=models.CASCADE, null=True, blank=True, )
index = models.SmallIntegerField(_("Index"), help_text=_("Numéro d'index"), default=0)
pin_active = models.BooleanField(_("Broche"), help_text=_("Broche active à l'initialisation"), default=True)
image = ResizedImageField(_("Image"), help_text=_("Image du capteur"), size=[320, 240], upload_to='devices/', null=True, blank=True, default = 'devices/vanne.png')
task = models.ForeignKey(
PeriodicTask,
verbose_name=_("Tâche périodique"),
help_text=_("Tâche de démarrage périodique de ce capteur (optionnel)"),
on_delete=models.CASCADE, null=True, blank=False
)
lifetime = models.DurationField(_("Durée"), help_text=_("Durée de l'évènement. Jours hh:mm:ss format"), blank=True, null=True, default=timedelta(hours=1, minutes=15))
heartbeat = models.IntegerField (_("Battement"), help_text=_("Interrogation du capteur en secondes"), default=1)
active = models.BooleanField(_("Actif"), default=True)
def image_tag(self):
return mark_safe(f'<img src="{self.image.url}" style="width: 45px; height:45px;">')
image_tag.short_description = _("Image")
image_tag.allow_tags = True
def to_register(self):
return dict(
dev_type=self.device_type.code if self.device_type else 'None',
index=self.index,
key=self.uuid,
label=self.label,
status=0,
active=1 if self.pin_active else 0,
)
class Meta:
ordering = ['index', ]
verbose_name = _("Capteur")
verbose_name_plural = _("Capteurs")
@classmethod
def all_devices(cls):
return Device.objects.filter(active=True).all().order_by('index')
@classmethod
def all_devices_json(cls):
devices = Device.objects.filter(active=True).all().order_by('index')
buf = []
for dev in devices:
buf.append({
"uuid": dev.uuid,
"index": dev.index,
})
return json.dumps(buf)
def save(self, *args, **kwargs):
if not self.uuid:
self.uuid = utils.get_device_uuid(12)
super().save(*args, **kwargs)
def __str__(self):
return f'{self.label}'
@receiver(post_save, sender=Device)
def create_device(sender, instance, created, **kwargs):
name = f'{instance.uuid}'
t = PeriodicTask.objects.filter(name__exact=name).first()
if not t:
p = PeriodicTask.objects.create(
name=name,
task='relay status',
description=_('Pas encore programmé'),
interval_id=1,
args=json.dumps([instance.uuid])
)
instance.task_id = p.id
instance.save()
else:
PeriodicTask.objects.filter(id=instance.task.id).update(
name=name,
args=json.dumps([instance.uuid])
)
instance.task.save()
+169
View File
@@ -0,0 +1,169 @@
'''
Created on 23 janv. 2026
@author: denis
'''
# process.py
#from django.utils.translation import gettext_lazy as _
import os
os.environ['BOARD_DEVICE']="RPI"
import json
from threading import Thread, Event
from asgiref.sync import async_to_sync #, sync_to_async
from channels.layers import get_channel_layer
from django.conf import settings
from django.db import transaction
from celery import Task
from celery.exceptions import Ignore
from celery.utils.log import get_task_logger
from redis import Redis
from modules import gpiolib as gpio, devices as dev
from . import models
logger = get_task_logger(__name__)
redisDB = Redis(host=settings.REDIS_HOST, port=settings.REDIS_PORT, db=0, decode_responses=True)
class RelayManager:
def __init__(self):
self.shift_out_register = gpio.ShiftOutRegister74HC595(
latch_pin=settings.CTL_LATCH_PIN,
clock_pin=settings.CTL_CLOCK_PIN,
data_pin=settings.CTL_DATA_PIN,
number=settings.CTL_NUMBER,
)
def create_relay(self, device, publish=None):
return dev.Relay(self.shift_out_register, device.to_register(), publish=publish)
relay_manager = RelayManager()
class RelayProcess(Task):
def __init__(self, uuid):
super().__init__()
self.channel_layer = get_channel_layer()
self.uuid = uuid
self.group = f'relay_{uuid}'
self.stop_event = Event()
self.heartbeat = 10
self.relay = None
self.device = None
def __call__(self, uuid, *args, **kwargs):
self.uuid = uuid
return self.start(*args, **kwargs)
def redis_pub(self, state, **msg):
async_to_sync(self.channel_layer.group_send)(
self.group, {
"type": 'relay.message',
"text": { 'topic': state, **msg }
}
)
self.mqtt_pub(state, **msg)
def mqtt_pub(self, state, **payload):
topic = f"relay/{self.uuid}/{state}"
payload.pop("uuid", None)
redisDB.publish('mqtt_pub', json.dumps({"type": "mqtt", "topic": topic, "payload": payload}))
#logger.info(f"mqtt_pub {topic}: {payload}")
def start(self, *args, **kwargs):
try:
with transaction.atomic():
self.device = models.Device.objects.select_for_update().get(uuid=self.uuid)
self.stop_event.clear()
self.start_services()
except Exception as e:
logger.error(f"Relay error {self.uuid}: {e}")
raise Ignore()
def stop(self):
try:
self.redis_pub('state', stop="done")
self.stop_event.set()
logger.info(f"Relay {self.uuid} stopped.")
Event().wait(1.0)
finally:
self.stop_event.set()
gpio.IO_CLEANUP()
def start_services(self):
Thread(target=self._listen_to_redis, daemon=True).start()
Thread(target=self._relay_process, daemon=True).start()
def _listen_to_redis(self):
try:
logger.info(f"RelayProcess {self.group}: listen via redisDB")
pubsub = redisDB.pubsub()
pubsub.subscribe(f"{self.group}", "relay_broadcast", "mqtt_message")
try:
for message in pubsub.listen():
#logger.info(f"{message}")
if self.stop_event.is_set():
break
cmd = json.loads(str(message.get('data')))
if not isinstance(cmd, dict):
continue
if cmd["type"]=="relay" or cmd["type"]=="mqtt":
topic = cmd["topic"]
if topic == "start":
duration = cmd.get("value", 0)
self.relay.relay_start(duration=duration)
elif topic == "stop":
self.relay.relay_stop()
elif topic in ["status", "init"]:
self.relay.relay_status()
elif topic == "halt":
self.relay.relay_halt()
elif topic == "restart":
self.relay.relay_restart()
except Exception as e:
logger.error(f'listen_to_redis: {e}')
finally:
pubsub.unsubscribe()
pubsub.close()
def _relay_process(self):
try:
logger.info(f"RelayProcess Start {self.uuid}")
self.relay = relay_manager.create_relay(self.device, publish=self.redis_pub)
self.relay.relay_stop()
while not self.stop_event.is_set():
try:
Event().wait(self.device.heartbeat)
self.relay.relay_status()
except Exception as e:
logger.error(f'====> {e}')
Event().wait(1.0)
except Exception as e:
logger.error(f"Relay {self.uuid}: {e}")
finally:
logger.info(f"End of relay process: {self.uuid}")
self.redis_pub('state', relay="stop")
+10
View File
@@ -0,0 +1,10 @@
#
# routing.py
from django.urls import re_path
from . import consumers
websocket_urlpatterns = [
re_path(r"ws/relay/(?P<uuid>[^/]+)/?$", consumers.RelayConsumer.as_asgi()),
]
@@ -0,0 +1,24 @@
#relay-grid {
display: grid;
grid-template-columns: repeat(var(--grid-columns, 3), 1fr);
gap: 0.51em;
width: 100%;
height: 100%;
padding: 0.5em;
align-items: stretch;
justify-items: stretch;
}
.relay-box {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
min-width: 320px;
}
.relay-meta {
width: 100%;
flex: 0 0 auto;
padding: .5em;
}
@@ -0,0 +1,186 @@
class RelayOverlay {
constructor(parent, relay) {
this.parent = parent;
this.relay = relay;
this.uuid = relay.uuid;
this.sockets = parent.sockets;
this.init_controls();
}
init_controls() {
this.duration_box = sId('duration-'+this.uuid);
this.start_box = sId('start-'+this.uuid);
this.stop_box = sId('stop-'+this.uuid);
this.flow_box = sId('flow-'+this.uuid);
this.ts_box = sId('ts-'+this.uuid);
this.rate_box = sId('rate-'+this.uuid);
this.remaining_box = sId('remaining-'+this.uuid);
this.duration_value = parseInt(this.duration_box.value);
this.relay_state = 0;
this.flow = 0.00;
this.start_box.onclick = () => {
this.start();
};
this.stop_box.onclick = () => {
this.stop();
};
this.duration_box.onchange = () => {
this.duration_value = parseInt(this.duration_box.value);
}
}
update(payload) {
if (payload.topic === "status") {
this.relay_state = payload.relay_state;
this.flow = parseFloat(payload.flow.toFixed(2));
this.rate = parseFloat(payload.rate.toFixed(2));
this.start_box.disabled = this.relay_state === 1;
this.stop_box.disabled = this.relay_state === 0;
if (payload.remaining>0) {
this.remaining_box.textContent = `${payload.remaining} s restante(s)`;
} else {
this.remaining_box.textContent = "";
}
this.ts_box.textContent = new Date(payload.ts * 1000).toLocaleTimeString();
this.duration_box.value = this.duration_value;
this.flow_box.textContent = this.flow;
this.rate_box.textContent = this.rate;
}
}
start() { this._send({ type: 'relay', topic: "start", value: this.duration_value }); }
stop() { this._send({ type: 'relay', topic: "stop" }); }
status() { this._send({ type: 'relay', topic: "status" }); }
halt() { this._send({ type: 'relay', topic: "halt" }); }
restart() { this._send({ type: 'relay', topic: "restart" }); }
_send(message) { this.sockets[this.uuid].send(message); }
}
class RelayManager {
constructor(container, relay) {
this.container = container;
this.relay = relay;
this.overlays = {};
this.sockets = {};
this.init_controls();
}
init_controls() {
this.all_relay_state = 0;
this.startall_box = sId('start-all');
this.stopall_box = sId('stop-all');
this.flow_box = sId('flow-all');
this.startall_box.onclick = () => {
this.all_relay_state = 1;
this._broadcast({ type: 'relay-multi', action: "start" });
};
this.stopall_box.onclick = () => {
this.all_relay_state = 0;
this._broadcast({ type: 'relay-multi', action: "stop" });
};
this.update_button();
}
async start() {
for (const relay of this.relay) {
const overlay = new RelayOverlay(this, relay);
this.overlays[relay.uuid] = overlay;
}
}
registerSocket(uuid, socket) {
this.sockets[uuid] = socket;
}
update_button() {
this.startall_box.disabled = this.all_relay_state === 1;
this.stopall_box.disabled = this.all_relay_state === 0;
}
flow_update() {
let total_flow = 0.00;
for (const relay of this.relay) {
const overlay = this.overlays[relay.uuid];
if (overlay) {
total_flow = total_flow + overlay.flow;
}
}
this.flow_box.textContent = total_flow.toFixed(2);
}
update(uuid, payload) {
const overlay = this.overlays[uuid];
if (overlay) {
overlay.update(payload);
}
}
_broadcast(message) {
if (message.type === 'relay-multi') {
const action = message.action;
this.update_button();
for (const relay of this.relay) {
const overlay = this.overlays[relay.uuid];
overlay[action]();
}
}
}
}
class MetadataSocket {
constructor(url, uuid) {
this.url = url;
this.uuid = uuid;
this.ws = null;
this.manager = null;
this.reconnectDelay = 1000;
this.shouldReconnect = true;
this.reconnect = false;
}
setOverlayManager(manager) {
this.manager = manager;
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
this.manager.update(this.uuid, data);
this.manager.flow_update();
};
this.ws.onopen = (event) => {
const overlay = this.manager.overlays[this.uuid];
if (overlay && !this.reconnect)
overlay['status']();
this.reconnect = false;
};
this.ws.onclose = () => {
console.warn(`WebSocket closed ${this.uuid}`);
if (this.shouldReconnect) {
this.reconnect = true;
setTimeout(() => {
console.log("Reconnect WebSocket...");
this.connect();
}, this.reconnectDelay);
}
};
}
send(obj) {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(obj));
}
}
}
@@ -0,0 +1,31 @@
const cpu_used = sId('cpu-used');
const shm_used = sId('shm-used');
const mem_used = sId('mem-used');
const disk_used = sId('disk-used');
const ramdisk_used = sId('ramdisk-used');
let autoTimer = null;
async function fetchStats() {
try {
const r = await fetch(stats_endpoint, { credentials: 'same-origin' });
if (!r.ok) throw new Error('HTTP ' + r.status);
const j = await r.json();
//console.log(j);
const cpu_percent = j.cpu_info.cpu_percent+'%'; cpu_used.style.setProperty("--cpu-used", cpu_percent); cpu_used.title=`Cpu: ${cpu_percent}`;
const shm_length = j.shm.length; shm_used.style.setProperty("--shm_used", shm_length); shm_used.title= `Shm: ${shm_length}`;
const virtual_memory = j.memory_info.virtual_memory.percent+'%'; mem_used.style.setProperty("--mem-used", virtual_memory); mem_used.title=`Mem: ${virtual_memory}`;
const root_percent = j.disk_info.root.percent+'%'; disk_used.style.setProperty("--disk-used", root_percent); disk_used.title=`Disk: ${root_percent}`;
let ramdisk_percent = "0%";
if (! j.ramdisk_info) ramdisk_percent = j.ramdisk_info.percent+'%';
ramdisk_used.style.setProperty("--ramdisk-used", ramdisk_percent); ramdisk_used.title=`Ramdisk: ${ramdisk_percent}`;
} catch (e) {
console.log('Error: ' + e.message);
}
}
function auto_fetch_start() { fetchStats(); autoTimer = setInterval(fetchStats, 5000);}
function auto_fetch_stop() { clearInterval(autoTimer); autoTimer = null; }
auto_fetch_start();
+64
View File
@@ -0,0 +1,64 @@
# tasks.py
from celery import shared_task
from celery.utils.log import get_task_logger
from .process import RelayProcess
from .models import Device
logger = get_task_logger(__name__)
class RelayTaskManager:
def __init__(self):
self.active_relay = {}
def start_relay(self, uuid):
if uuid not in self.active_relay:
relay = RelayProcess(uuid)
relay.start()
self.active_relay[uuid] = relay
else:
logger.info(f"Relay {uuid} already running!...")
def stop_relay(self, uuid):
if uuid in self.active_relay:
self.active_relay[uuid].stop()
del self.active_relay[uuid]
def stop_all_relay(self):
for _, relay in self.active_relay.items():
relay.stop()
self.active_relay.clear()
def start_all_relay(self):
all_relays = Device.all_devices()
for relay in all_relays:
self.start_relay(relay.uuid)
task_manager = RelayTaskManager()
@shared_task(bind=True)
def relay_start(self, uuid):
task_manager.start_relay(uuid)
return f"Relai {uuid} démarré."
@shared_task(bind=True)
def relay_stop(self, uuid):
task_manager.stop_relay(uuid)
return f"Relai {uuid} arrêté."
@shared_task(bind=True)
def relay_stop_all(self):
task_manager.stop_all_relay()
return "Touts les Relais arrêtées."
@shared_task(bind=True)
def relay_start_all(self):
task_manager.start_all_relay()
return "Touts les Relais en cours de démarrage."
@@ -0,0 +1,84 @@
{% extends 'base.html' %}
{% load i18n home_tags %}
{% block header %}
<div class="w3-bar w3-dark-light">
<a href="#" class="w3-bar-item w3-mobile w3-btn w3-hover-opacity w3-xlarge w3-text-yellow" onclick="toggleSidebar()" title="{% trans 'Barre de côté' %}">
<i class="fa-solid fa-right-left"></i>
</a>
<a href="#" class="w3-bar-item w3-mobile w3-btn w3-hover-opacity w3-right w3-xlarge w3-text-green" onclick="goFullscreen()" title="{% trans 'Ecran plein' %}">
<i class="fa-solid fa-maximize"></i>
</a>
</div>
{% endblock %}
{% block sidebar %}
<div class="w3-dark-xlight w3-border-bottom">
<div class="w3-large w3-bold"><a href="/" class="w3-btn w3-hover-opacity w100 w3-left-align">{{ app_title }}</a></div>
<div class="w3-padding">
<span class="w3-padding">{{ request.user.username }}</span>
<form method="post" action="{% url 'logout' %}" class="w3-right">
{% csrf_token %}
<button class="w3-bar-item w3-btn w3-hover-opacity" type="submit" title="{% trans 'Déconnexion' %}">
<i class="fa-solid fa-right-from-bracket"></i>
</button>
</form>
</div>
</div>
{% block system %}
<div class="w3-light-blue w3-padding-large">
<div id="shm-used" class="w3-green w3-round" style="height:0.5em; width: var(--shm-used, 0%);" title="{% trans 'Mémoire partagée' %}"></div>
<div id="ramdisk-used" class="w3-purple w3-round" style="height:0.5em;width: var(--ramdisk-used, 0%); margin: 0.35em 0" title="{% trans 'Disque RAM' %}"></div>
<div id="mem-used" class="w3-deep-orange w3-round" style="height:0.5em;width: var(--mem-used, 0%); margin: 0.35em 0" title="{% trans 'Mémoire RAM' %}"></div>
<div id="cpu-used" class="w3-indigo w3-round" style="height:0.5em;width: var(--cpu-used, 0%); margin: 0.35em 0" title="{% trans 'Charge CPU' %}"></div>
<div id="disk-used" class="w3-amber w3-round" style="height:0.5em;width: var(--disk-used, 0%); margin: 0.35em 0" title="{% trans 'Disque /' %}"></div>
</div>
{% endblock %}
{% block columns %}
<div class="w3-dark-light">
<div class=" w3-padding w3-border-bottom">
<span>{% trans "Grille" %}</span><br>
<span>
{% x_range 2 6 as cols %}
{% for c in cols %}
<button id="swap-{{ c }}" class="w3-btn w3-badge w3-padding-small w3-sand w3-tiny" onclick="setGridColumns({{ c }})">{{ c }}</button>
{% endfor %}
</span>
</div>
</div>
<script>
function updateActiveButton(n) {
document.querySelectorAll('[id^="swap-"]').forEach(btn => {
btn.classList.remove('w3-blue', 'w3-sand'); btn.classList.add('w3-sand');
});
const active = sId('swap-'+ n);
if (active) { active.classList.remove('w3-sand'); active.classList.add('w3-blue');
}
}
</script>
{% endblock %}
{% block sidebar_list %}
<div class="w3-dark-light w3-padding-small">
<a href="{% url 'relay:dashboard' %}" class="w3-bar-item w3-btn w3-hover-opacity">
<i class="fa-solid fa-table-cells w3-text-green"></i> {% trans "Tableau de bord" %}
</a>
{% block sidebar_command %}{% endblock %}
<a href="{% url 'relay:admin' %}" class="w3-bar-item w3-btn w3-hover-opacity w3-margin-top-6">
<i class="fa-solid fa-gear w3-text-pink"></i> {% trans "Administration base de données" %}
</a>
<a href="{% url 'relay:supervisor' %}" class="w3-bar-item w3-btn w3-hover-opacity">
<i class="fa-solid fa-gears w3-text-purple"></i> {% trans "Supervisor" %}
</a>
</div>
{% endblock %}
{% endblock %}
{% block content %}{% endblock %}
{% block js_footer %}
{{ block.super }}
<script>
const stats_endpoint = "{% url 'relay:api_stats' %}";
</script>
<script src="/static/relay/js/stats.js"></script>
{% endblock %}
@@ -0,0 +1,100 @@
{% extends 'relay/base.html' %}
{% load i18n home_tags %}
{% block styles %}
{{ block.super }}
<link href="/static/relay/css/relay.css" rel="stylesheet">
{% endblock %}
{% block sidebar_command %}
<div class="w3-row w3-border w3-margin-top">
<div class="w3-third w3-center">
<button id="start-all" class="w3-btn w3-blue w3-round w3-margin-2 w3-hover-opacity w3-block" title="{% trans "Tout démarrer" %}">
{% trans "Marche" %}
</button>
<button id="stop-all" class="w3-btn w3-pink w3-round w3-margin-2 w3-hover-opacity w3-block w3-margin-top" title="{% trans "Tout arrêter" %}">
{% trans "Arrêt" %}
</button>
</div>
<div class="w3-twothird">
<div class="w3-center">{% trans "Débit total" %}</div>
<div class="w3-dark-xlight w3-border w3-padding w3-margin w3-right-align">
<div id="flow-all" class="w3-wide w3-bold">0.00</div><div>litres</div>
</div>
</div>
</div>
{% endblock %}
{% block content %}
<div id="relay-grid" class="w3-black">
{% for dev in devices %}
<div id="box-{{ dev.uuid }}" class="relay-box w3-border w3-round w3-round-large">
<div class="w3-row-padding">
<div class="w3-col s4 w3-center">
<div class="w3-padding-small">{{ dev.label }}</div>
<img src="{{ dev.image.url }}" class="w3-circle" style="width:64px;">
<select id="duration-{{ dev.uuid }}">
<option value="0" selected>{% trans 'Sans délai' %}</option>
<option value="30">30 s</option>
<option value="60">1 mn</option>
<option value="120">2 mn</option>
<option value="300">5 mn</option>
<option value="600">10 mn</option>
</select>
</div>
<div class="w3-col s3">
<div id="ts-{{ dev.uuid }}" class="w3-small w3-padding-small"></div>
<button id="start-{{ dev.uuid }}" class="w3-btn w3-blue w3-round w3-margin-2 w3-hover-opacity w3-block w3-padding-small">
{% trans "Marche" %}
</button>
<button id="stop-{{ dev.uuid }}" class="w3-btn w3-pink w3-round w3-margin-2 w3-hover-opacity w3-block w3-padding-small">
{% trans "Arrêt" %}
</button>
</div>
<div class="w3-col s5">
<div class="w3-padding-small">{% trans "Débit" %}: <span id="rate-{{ dev.uuid }}" class="w3-small"></span>&nbsp;l/mn</div>
<div class="w3-dark-xlight w3-border w3-padding w3-right-align w100">
<div id="flow-{{ dev.uuid }}" class="w3-wide w3-bold"></div><div>litres</div>
</div>
<div id="remaining-{{ dev.uuid }}" class="w3-small"></div>
</div>
</div>
</div>
{% endfor %}
</div>
{% endblock %}
{% block js_footer %}
{{ block.super }}
<script src="/static/relay/js/relay.js"></script>
<script>
const container = sId("relay-grid");
const ws_route = "{{ ws_route }}";
const columns = "{{ columns }}";
const devices = {{ relay|safe }};
function setGridColumns(col) {
container.style.setProperty("--grid-columns", col);
updateActiveButton(col);
}
// ---- Point d'entrée ----
(async () => {
const manager = new RelayManager(container, devices);
await manager.start();
const protocol = location.protocol === "https:" ? "wss" : "ws";
for (const uuid in manager.overlays) {
const wsUrl = `${protocol}://${location.host}/${ws_route}/${uuid}`;
const socket = new MetadataSocket(wsUrl, uuid);
socket.setOverlayManager(manager);
socket.connect();
manager.registerSocket(uuid, socket);
}
setGridColumns(columns);
})();
</script>
{% endblock %}
@@ -0,0 +1,6 @@
{% extends 'relay/base.html' %}
{% block columns %}{% endblock %}
{% block content %}
<iframe width="100%" src="{{ link }}" style="border:none; height:90vh;display:block;"></iframe>
{% endblock %}
+3
View File
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
+14
View File
@@ -0,0 +1,14 @@
from django.urls import path
from . import views
app_name = "relay"
urlpatterns = [
path('dashboard/', views.dashboard_view, name='dashboard'),
path('relay/admin/', views.admin_view, name='admin'),
path('relay/supervisor/', views.supervisor_view, name='supervisor'),
path('api/stats/', views.stats_view, name='api_stats'),
]
+53
View File
@@ -0,0 +1,53 @@
from django.shortcuts import render
from django.http import JsonResponse
from django.views.decorators.http import require_GET
from django.contrib.auth.decorators import login_required
from django.conf import settings
from modules.system_stats import get_cached_stats, start_background_updater
from . import models
start_background_updater()
@require_GET
def stats_view(request):
"""
Retourne tout le cache (shm, cpu_info, memory_info, disk_info, updated_at)
"""
try:
data = get_cached_stats()
return JsonResponse(data, safe=False)
except Exception as e:
return JsonResponse({"error": str(e)}, status=500)
def global_context(request, **ctx):
return dict(
app_title=settings.APP_TITLE,
app_sub_title=settings.APP_SUB_TITLE,
**ctx
)
@login_required
def dashboard_view(request):
ctx = dict(
devices=models.Device.all_devices(),
relay=models.Device.all_devices_json(),
ws_route=settings.RELAY_WEBSOCKET_ROUTE,
columns=3,
)
return render(request, "relay/dashboard.html", context=global_context(request, **ctx))
@login_required
def admin_view(request):
return render(request, "relay/iframe.html", context=global_context(request, link='/admin/'))
@login_required
def supervisor_view(request):
return render(request, "relay/iframe.html", context=global_context(request, link='http://relay.local:9001/'))