diff --git a/app/django_wol/celery.py b/app/django_wol/celery.py index bed70158..f55ec86c 100644 --- a/app/django_wol/celery.py +++ b/app/django_wol/celery.py @@ -11,6 +11,10 @@ app.conf.beat_schedule = { "ping_devices_5s": { "task": "wol.tasks.status", "schedule": 5 + }, + "scheduled_wakes_1s": { + "task": "wol.tasks.scheduled_wakes", + "schedule": 1 } } diff --git a/app/wol/admin.py b/app/wol/admin.py index d71ec372..a929965b 100644 --- a/app/wol/admin.py +++ b/app/wol/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from .models import Device +from wol.models import Device class DeviceAdmin(admin.ModelAdmin): diff --git a/app/wol/consumers.py b/app/wol/consumers.py index a0e65b79..9ca5f5f2 100644 --- a/app/wol/consumers.py +++ b/app/wol/consumers.py @@ -1,12 +1,13 @@ -import ipaddress import json - -import wakeonlan +from datetime import datetime, tzinfo from channels.db import database_sync_to_async from channels.generic.websocket import AsyncWebsocketConsumer from django.core import serializers +from django.utils.dateparse import parse_datetime +from django.utils.timezone import make_aware -from .models import Device, Websocket +from wol.models import Device, Websocket +from wol.wake import wake class WSConsumer(AsyncWebsocketConsumer): @@ -36,20 +37,56 @@ class WSConsumer(AsyncWebsocketConsumer): ) async def receive(self, text_data=None, bytes_data=None): - dev = await self.get_json_from_device_id(text_data) + received = json.loads(text_data) - subnet = ipaddress.ip_network( - f"{dev['fields']['ip']}/{dev['fields']['netmask']}", strict=False).broadcast_address - wakeonlan.send_magic_packet(dev['fields']["mac"], ip_address=str(subnet)) + if received["message"] == "wake": + dev = await self.get_json_from_device_id(received["id"]) + wake(dev["fields"]["mac"], dev["fields"] + ["ip"], dev["fields"]["netmask"]) - await self.channel_layer.group_send( - "wol", { - "type": "send_group", - "message": { - "wake": dev + await self.channel_layer.group_send( + "wol", { + "type": "send_group", + "message": { + "wake": { + "id": dev["pk"], + "name": dev["fields"]["name"] + } + } } - } - ) + ) + elif received["message"] == "add_schedule": + if not received["datetime"]: + return + d = make_aware(parse_datetime(received["datetime"])) + print(d.isoformat()) + await self.add_schedule(received["id"], d) + await self.channel_layer.group_send( + "wol", { + "type": "send_group", + "message": { + "add_schedule": { + "id": received["id"], + "name": received["name"], + "datetime": str(d.isoformat()) + } + } + } + ) + elif received["message"] == "delete_schedule": + await self.delete_schedule(received["id"]) + await self.channel_layer.group_send( + "wol", { + "type": "send_group", + "message": { + "delete_schedule": { + "id": received["id"], + "name": received["name"] + } + } + } + ) + async def send_status(self, event): await self.send(event["status"]) @@ -77,3 +114,15 @@ class WSConsumer(AsyncWebsocketConsumer): def get_json_from_device_id(self, id): dev = Device.objects.filter(id=id) return serializers.serialize("python", dev)[0] + + @database_sync_to_async + def add_schedule(self, id, datetime): + dev = Device.objects.filter(id=id).get() + dev.scheduled_wake = datetime + dev.save() + + @database_sync_to_async + def delete_schedule(self, id): + dev = Device.objects.filter(id=id).get() + dev.scheduled_wake = None + dev.save() diff --git a/app/wol/migrations/0007_device_scheduled_wake.py b/app/wol/migrations/0007_device_scheduled_wake.py new file mode 100644 index 00000000..4835656b --- /dev/null +++ b/app/wol/migrations/0007_device_scheduled_wake.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.7 on 2021-09-23 16:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('wol', '0006_websocket'), + ] + + operations = [ + migrations.AddField( + model_name='device', + name='scheduled_wake', + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/app/wol/models.py b/app/wol/models.py index acdc1623..26c2b99e 100644 --- a/app/wol/models.py +++ b/app/wol/models.py @@ -7,6 +7,7 @@ class Device(models.Model): ip = models.GenericIPAddressField() mac = models.CharField(max_length=17) netmask = models.CharField(max_length=15, default="255.255.255.0", blank=False, null=False) + scheduled_wake = models.DateTimeField(blank=True, null=True) class Websocket(models.Model): visitors = models.PositiveSmallIntegerField(blank=False, null=False, default=0) diff --git a/app/wol/routing.py b/app/wol/routing.py index 3927f9f4..bdc89a69 100644 --- a/app/wol/routing.py +++ b/app/wol/routing.py @@ -1,7 +1,7 @@ from django.urls import path -from . consumers import WSConsumer +from wol.consumers import WSConsumer ws_urlpatterns = [ path("wol/", WSConsumer.as_asgi()) -] \ No newline at end of file +] diff --git a/app/wol/static/css/style.css b/app/wol/static/css/style.css index ed47a4cd..fbd06ba0 100644 --- a/app/wol/static/css/style.css +++ b/app/wol/static/css/style.css @@ -49,6 +49,7 @@ .button.is-static { border-color: transparent; + color: inherit; } .notification { @@ -61,6 +62,10 @@ box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.3); } +.modal-card, .modal-content { + margin: 0 auto; +} + @media (prefers-color-scheme: dark) { .box { background-color: var(--box-bg); @@ -69,7 +74,11 @@ .button.is-static { background-color: #dbdbdb; color: #363636; - } + } + + .modal-card-body { + background-color: black; + } } @keyframes green-pulse { diff --git a/app/wol/static/js/main.js b/app/wol/static/js/main.js index 5afa690b..432217be 100644 --- a/app/wol/static/js/main.js +++ b/app/wol/static/js/main.js @@ -88,6 +88,25 @@ class BulmaNotification { let notif; window.onload = () => { notif = new BulmaNotification(); + var now = new Date(); + var utcString = now.toISOString().substring(0, 19); + var year = now.getFullYear(); + var month = now.getMonth() + 1; + var day = now.getDate(); + var hour = now.getHours(); + var minute = now.getMinutes(); + var second = now.getSeconds(); + var localDatetime = year + "-" + + (month < 10 ? "0" + month.toString() : month) + "-" + + (day < 10 ? "0" + day.toString() : day) + "T" + + (hour < 10 ? "0" + hour.toString() : hour) + ":" + + (minute < 10 ? "0" + minute.toString() : minute) + + utcString.substring(16, 19); + var datetimeFields = document.querySelectorAll('[id*=-input]'); + for (let index = 0; index < datetimeFields.length; index++) { + const element = datetimeFields[index]; + element.value = localDatetime; + } }; // @@ -100,7 +119,8 @@ function setDeviceUp(device) { var statusDot = document.getElementById(device.id + "-dot"); var statusPorts = document.getElementById(device.id + "-ports"); var wakeButton = document.getElementById(device.id + "-btn-wake"); - + var scheduleModalButton = document.getElementById(device.id + "-btn-schedule"); + // check if device was down before if (statusDot.classList.contains("dot-down")) { notif.show("Device now up!", device.name + " is now up.", "is-success", 5000); @@ -141,6 +161,8 @@ function setDeviceUp(device) { // set wake btn wakeButton.classList.remove("is-loading"); wakeButton.disabled = true; + // set schedule button + scheduleModalButton.disabled = false; } function setDeviceDown(device) { @@ -149,12 +171,13 @@ function setDeviceDown(device) { var statusDot = document.getElementById(device.id + "-dot"); var statusPorts = document.getElementById(device.id + "-ports"); var wakeButton = document.getElementById(device.id + "-btn-wake"); + var scheduleModalButton = document.getElementById(device.id + "-btn-schedule"); // check if device was up before if (statusDot.classList.contains("dot-up")) { notif.show("Device now down!", device.name + " is now down.", "is-danger", 5000); } - + // clear current animation statusDot.style.animation = "none"; statusDot.offsetWidth; @@ -177,13 +200,36 @@ function setDeviceDown(device) { statusPorts.innerHTML = ''; // set wake btn wakeButton.disabled = false; - wakeButton.addEventListener("click", function() { - setDeviceWake(device.id); - }); + // set schedule button + scheduleModalButton.disabled = false; } -function setDeviceWake(id) { - socket.send(id); +function wakeDevice(id) { + socket.send(JSON.stringify({ + "message": "wake", + "id": id + })); +} + +function addSchedule(id, name, datetime) { + if (!(datetime)) { + return; + } + console.log(datetime); + socket.send(JSON.stringify({ + "message": "add_schedule", + "id": id, + "name": name, + "datetime": datetime + })); +} + +function deleteSchedule(id, name) { + socket.send(JSON.stringify({ + "message": "delete_schedule", + "id": id, + "name": name + })); } // @@ -195,22 +241,6 @@ socket.onmessage = function (event) { var message = JSON.parse(event.data); console.log(message); - // set visitors element - if ("visitors" in message) { - if (message.visitors == 1) { - document.getElementById("visitors").innerHTML = message.visitors + ' visitor'; - } else { - document.getElementById("visitors").innerHTML = message.visitors + ' visitors'; - notif.show("Visitors updated", "There are currently " + message.visitors + " visitors", "is-info", 50000); - } - } - - // set wake button - if ("wake" in message) { - document.getElementById(message.wake.pk + "-btn-wake").classList.add("is-loading"); - notif.show("Wake started", message.wake.fields.name + " has been started.", "is-info", 5000); - } - // set devices up or down if ("device" in message) { if (message.device.up == true) { @@ -219,7 +249,42 @@ socket.onmessage = function (event) { setDeviceDown(message.device); } } + + // set visitors element + if ("visitors" in message) { + if (message.visitors == 1) { + document.getElementById("visitors").innerHTML = message.visitors + ' visitor'; + } else { + document.getElementById("visitors").innerHTML = message.visitors + ' visitors'; + notif.show("Visitors updated", "There are currently " + message.visitors + " visitors", "is-info", 5000); + } + } + + // set wake by client + if ("wake" in message) { + document.getElementById(message.wake.id + "-btn-wake").classList.add("is-loading"); + notif.show("Wake started", message.wake.name + " has been started.", "is-info", 5000); + } + + // set wake by schedule + if ("wake_schedule" in message) { + document.getElementById(message.wake_schedule.id + "-btn-wake").classList.add("is-loading"); + document.getElementById(message.wake_schedule.id + "-schedule-notice").innerHTML = ""; + notif.show("Scheduled wake started", message.wake_schedule.name + " has been started.", "is-info", 5000); + } + + // add schedule + if ("add_schedule" in message) { + document.getElementById(message.add_schedule.id + "-schedule-notice").innerHTML = `

Scheduled wake set:
${message.add_schedule.datetime}

`; + notif.show("Schedule added", "A wake up event has been scheduled for " + message.add_schedule.name, "is-info", 5000); + } + + // delete schedule + if ("delete_schedule" in message) { + document.getElementById(message.delete_schedule.id + "-schedule-notice").innerHTML = ""; + notif.show("Schedule deleted", "A wake up event has been deleted for " + message.delete_schedule.name, "is-info", 5000); + } } socket.onclose = function (event) { notif.show("Connection closed", "Websocket connection has closed", "is-danger", 5000); -} \ No newline at end of file +} diff --git a/app/wol/static/js/modal.js b/app/wol/static/js/modal.js new file mode 100644 index 00000000..f26012b1 --- /dev/null +++ b/app/wol/static/js/modal.js @@ -0,0 +1,57 @@ +'use strict'; + +document.addEventListener('DOMContentLoaded', function () { + // Modals + + var rootEl = document.documentElement; + var $modals = getAll('.modal'); + var $modalButtons = getAll('.modal-button'); + var $modalCloses = getAll('.modal-background, .modal-close, .modal-card-head .delete, .modal-card-foot .button'); + + if ($modalButtons.length > 0) { + $modalButtons.forEach(function ($el) { + $el.addEventListener('click', function () { + var target = $el.dataset.target; + openModal(target); + }); + }); + } + + if ($modalCloses.length > 0) { + $modalCloses.forEach(function ($el) { + $el.addEventListener('click', function () { + closeModals(); + }); + }); + } + + function openModal(target) { + var $target = document.getElementById(target); + rootEl.classList.add('is-clipped'); + $target.classList.add('is-active'); + } + + function closeModals() { + rootEl.classList.remove('is-clipped'); + $modals.forEach(function ($el) { + $el.classList.remove('is-active'); + }); + } + + document.addEventListener('keydown', function (event) { + var e = event || window.event; + + if (e.keyCode === 27) { + closeModals(); + closeDropdowns(); + } + }); + + // Utils + + function getAll(selector) { + var parent = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : document; + + return Array.prototype.slice.call(parent.querySelectorAll(selector), 0); + } +}) \ No newline at end of file diff --git a/app/wol/tasks.py b/app/wol/tasks.py index 3e4f7ad0..a0491b8d 100644 --- a/app/wol/tasks.py +++ b/app/wol/tasks.py @@ -6,8 +6,11 @@ import threading from asgiref.sync import async_to_sync from celery import shared_task from channels.layers import get_channel_layer +from django.core import serializers +from django.utils import timezone -from .models import Device, Websocket +from wol.models import Device, Websocket +from wol.wake import wake channel_layer = get_channel_layer() @@ -65,3 +68,28 @@ def status(): d = WolDevice() t = threading.Thread(target=d.start, args=(dev,)) t.start() + + +@shared_task +def scheduled_wakes(): + if Websocket.objects.first().visitors == 0: + return + + devices = Device.objects.all() + + for dev in devices: + if dev.scheduled_wake and dev.scheduled_wake <= timezone.now(): + wake(dev.mac, dev.ip, dev.netmask) + dev.scheduled_wake = None + dev.save() + async_to_sync(channel_layer.group_send)( + "wol", { + "type": "send_group", + "message": { + "wake_schedule": { + "id": dev.id, + "name": dev.name + } + } + } + ) diff --git a/app/wol/templates/wol/index.html b/app/wol/templates/wol/index.html index a09c8191..4174ed1e 100644 --- a/app/wol/templates/wol/index.html +++ b/app/wol/templates/wol/index.html @@ -15,7 +15,7 @@ - +
@@ -94,22 +94,55 @@

Actions

-
-

- -

+
+ +
+
+ {% if dev.scheduled_wake %} +

+ Scheduled wake set:
{{ dev.scheduled_wake|date:"c" }} +

+ {% endif %} +
+

+ {% endfor %} @@ -117,6 +150,7 @@ diff --git a/app/wol/urls.py b/app/wol/urls.py index 2d9cdc04..4c45ab7a 100644 --- a/app/wol/urls.py +++ b/app/wol/urls.py @@ -1,6 +1,6 @@ from django.urls import path -from . import views +from wol import views urlpatterns = [ path("", views.index, name="index") diff --git a/app/wol/views.py b/app/wol/views.py index 38256832..a1ae5cd4 100644 --- a/app/wol/views.py +++ b/app/wol/views.py @@ -1,6 +1,6 @@ from django.shortcuts import render -from .models import Device, Websocket +from wol.models import Device, Websocket def index(request): diff --git a/app/wol/wake.py b/app/wol/wake.py new file mode 100644 index 00000000..ce804e0e --- /dev/null +++ b/app/wol/wake.py @@ -0,0 +1,9 @@ +import ipaddress + +import wakeonlan + + +def wake(mac, ip, netmask): + subnet = ipaddress.ip_network( + f"{ip}/{netmask}", strict=False).broadcast_address + wakeonlan.send_magic_packet(mac, ip_address=str(subnet))