mirror of
https://github.com/seriousm4x/UpSnap.git
synced 2026-05-13 19:32:04 -04:00
added scheduled wake events #3
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import Device
|
||||
from wol.models import Device
|
||||
|
||||
|
||||
class DeviceAdmin(admin.ModelAdmin):
|
||||
|
||||
@@ -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()
|
||||
|
||||
18
app/wol/migrations/0007_device_scheduled_wake.py
Normal file
18
app/wol/migrations/0007_device_scheduled_wake.py
Normal file
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
]
|
||||
]
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 = '<ul>' + openPorts.join('') + '</ul>';
|
||||
// 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 = `<p>Scheduled wake set:<br>${message.add_schedule.datetime}</p>`;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
57
app/wol/static/js/modal.js
Normal file
57
app/wol/static/js/modal.js
Normal file
@@ -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);
|
||||
}
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<link rel="stylesheet" href="{% static 'css/bulma-notifications.css' %}">
|
||||
</head>
|
||||
|
||||
<body class="px-6">
|
||||
<body class="px-4">
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div class="columns is-vcentered">
|
||||
@@ -94,22 +94,55 @@
|
||||
<h1 class="title">
|
||||
Actions
|
||||
</h1>
|
||||
<div class="field is-grouped">
|
||||
<p class="control">
|
||||
<button class="button is-success" id="{{ dev.id }}-btn-wake" name="wake"
|
||||
aria-label="wake" disabled>
|
||||
<span>Wake</span>
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-play"></i>
|
||||
</span>
|
||||
</button>
|
||||
</p>
|
||||
<div class="buttons">
|
||||
<button class="button is-success" id="{{ dev.id }}-btn-wake" name="wake"
|
||||
aria-label="wake" onclick="wakeDevice({{ dev.id }})" disabled>
|
||||
<span>Wake</span>
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-play"></i>
|
||||
</span>
|
||||
</button>
|
||||
<button id="{{ dev.id }}-btn-schedule" class="button is-light modal-button" data-target="{{ dev.id }}-modal" aria-haspopup="true" disabled>
|
||||
<span class="icon">
|
||||
<i class="fas fa-clock"></i>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="{{ dev.id }}-schedule-notice">
|
||||
{% if dev.scheduled_wake %}
|
||||
<p>
|
||||
Scheduled wake set:<br>{{ dev.scheduled_wake|date:"c" }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<p></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="{{ dev.id }}-modal" class="modal">
|
||||
<div class="modal-background"></div>
|
||||
<div class="modal-content">
|
||||
<div class="modal-card">
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">Schedule wake up for {{ dev.name }}</p>
|
||||
<button class="delete" aria-label="close"></button>
|
||||
</header>
|
||||
<section class="modal-card-body">
|
||||
<label for="{{ dev.id }}-input">Choose date and time:</label>
|
||||
<input type="datetime-local" id="{{ dev.id }}-input">
|
||||
</section>
|
||||
<footer class="modal-card-foot">
|
||||
<button class="button is-success" id="{{ dev.id }}-btn-schedule-save"
|
||||
onclick="addSchedule({{ dev.id }}, '{{ dev.name }}', document.getElementById('{{ dev.id }}-input').value)">Save changes</button>
|
||||
<button class="button is-light-dark">Cancel</button>
|
||||
<button class="button is-danger is-pulled-right" onclick="deleteSchedule({{ dev.id }}, '{{ dev.name }}')">Delete</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
<button class="modal-close is-large" aria-label="close"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</body>
|
||||
@@ -117,6 +150,7 @@
|
||||
<footer>
|
||||
<script src="{% static 'js/jquery-3.6.0.min.js' %}"></script>
|
||||
<script src="{% static 'js/main.js' %}"></script>
|
||||
<script src="{% static 'js/modal.js' %}"></script>
|
||||
</footer>
|
||||
|
||||
</html>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
from wol import views
|
||||
|
||||
urlpatterns = [
|
||||
path("", views.index, name="index")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
from .models import Device, Websocket
|
||||
from wol.models import Device, Websocket
|
||||
|
||||
|
||||
def index(request):
|
||||
|
||||
9
app/wol/wake.py
Normal file
9
app/wol/wake.py
Normal file
@@ -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))
|
||||
Reference in New Issue
Block a user