2023-11-09 19:07:34 +00:00
|
|
|
|
<html>
|
|
|
|
|
<head>
|
|
|
|
|
<title>ESP32 timed Switch</title>
|
|
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.8.3/angular.js"></script>
|
2024-02-26 23:25:12 +00:00
|
|
|
|
<link rel="manifest" href="manifest.json" />
|
|
|
|
|
|
2023-11-09 19:07:34 +00:00
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
<h1>Scheduler</h1>
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
class Schedule {
|
|
|
|
|
|
|
|
|
|
constructor(scheduleArray) {
|
|
|
|
|
|
|
|
|
|
if (scheduleArray && scheduleArray.length === 24) {
|
|
|
|
|
this.schedule = scheduleArray.slice(); // Copy the array to prevent direct mutation
|
|
|
|
|
} else {
|
|
|
|
|
// Initialize with all hours turned off if no valid array is provided
|
|
|
|
|
this.schedule = new Array(24).fill(0x000);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Load a new schedule from an array of 24 integers
|
|
|
|
|
loadFromArrayBuffer = function(buffer) {
|
|
|
|
|
// Load from binary data
|
|
|
|
|
var view = new DataView(buffer);
|
|
|
|
|
for (var i = 0; i < 24; i++) {
|
2024-02-26 23:25:12 +00:00
|
|
|
|
this.schedule[i] = view.getUint16(i * 2);
|
2023-11-09 19:07:34 +00:00
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Update a single 5-minute segment for a single hour
|
|
|
|
|
updateSegment(hour, segment, state) {
|
|
|
|
|
if (hour < 0 || hour >= 24) {
|
|
|
|
|
throw new Error('Hour must be between 0 and 23.');
|
|
|
|
|
}
|
|
|
|
|
if (segment < 0 || segment >= 12) {
|
|
|
|
|
throw new Error('Segment must be between 0 and 11.');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Calculate the bit position from the segment (0 for first segment, 11 for last)
|
|
|
|
|
const bitPosition = 11 - segment;
|
|
|
|
|
if (state) {
|
|
|
|
|
// Turn the segment on by setting the bit at the bit position
|
|
|
|
|
this.schedule[hour] |= (1 << bitPosition);
|
|
|
|
|
} else {
|
|
|
|
|
// Turn the segment off by clearing the bit at the bit position
|
|
|
|
|
this.schedule[hour] &= ~(1 << bitPosition);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
serializeForDisplay() {
|
|
|
|
|
// Converts the schedule array into a display format with the appropriate class for coloring
|
|
|
|
|
return this.schedule.map(hour => {
|
|
|
|
|
if (hour === 0xFFF) { // All segments on
|
|
|
|
|
return { displayValue: ' ', class: 'on' };
|
2024-02-26 23:25:12 +00:00
|
|
|
|
} else if (hour === 0x00) { // All segments off
|
2023-11-09 19:07:34 +00:00
|
|
|
|
return { displayValue: ' ', class: 'off' };
|
|
|
|
|
} else {
|
|
|
|
|
return { displayValue: ' ', class: 'partial' };
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
prepareEditMatrix() {
|
|
|
|
|
// Creates a 24x12 matrix for editing the schedule
|
|
|
|
|
const editMatrix = [];
|
|
|
|
|
for (let hour = 0; hour < 24; hour++) {
|
|
|
|
|
const hourSegments = [];
|
|
|
|
|
for (let segment = 0; segment < 12; segment++) {
|
|
|
|
|
// Check if the segment is on or off
|
|
|
|
|
hourSegments.push({
|
|
|
|
|
value: (this.schedule[hour] & (1 << segment)) !== 0 ? 1 : 0
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
// Reverse the array to start with segment 0 on the left
|
|
|
|
|
editMatrix.push(hourSegments.reverse());
|
|
|
|
|
}
|
|
|
|
|
return editMatrix;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateFromEditMatrix(editMatrix) {
|
|
|
|
|
// Update the schedule array based on the edit matrix
|
|
|
|
|
for (let hour = 0; hour < 24; hour++) {
|
|
|
|
|
let hourValue = 0;
|
|
|
|
|
for (let segment = 0; segment < 12; segment++) {
|
|
|
|
|
if (editMatrix[hour][11 - segment].value) {
|
|
|
|
|
hourValue |= (1 << segment);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
this.schedule[hour] = hourValue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
serializeToArrayBuffer = function() {
|
|
|
|
|
// Convert to binary data
|
2024-02-26 23:25:12 +00:00
|
|
|
|
var buffer = new ArrayBuffer(24 * 2);
|
2023-11-09 19:07:34 +00:00
|
|
|
|
var view = new DataView(buffer);
|
|
|
|
|
for (var i = 0; i < this.schedule.length; i++) {
|
2024-02-26 23:25:12 +00:00
|
|
|
|
view.setUint16(i * 2, this.schedule[i]);
|
2023-11-09 19:07:34 +00:00
|
|
|
|
}
|
2024-02-26 23:25:12 +00:00
|
|
|
|
return view;
|
2023-11-09 19:07:34 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// End of Class
|
|
|
|
|
|
|
|
|
|
// Start of Angular
|
|
|
|
|
|
|
|
|
|
var app = angular.module('plugSchedulerApp', []);
|
2024-02-26 23:25:12 +00:00
|
|
|
|
app.value('api_host', window.location.host);
|
|
|
|
|
app.factory("data", function(){
|
|
|
|
|
var sharedData = { api_host: window.location.host}
|
|
|
|
|
return {
|
|
|
|
|
getSharedData: function () {
|
|
|
|
|
return sharedData;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
});
|
2023-11-09 19:07:34 +00:00
|
|
|
|
|
2024-02-26 23:25:12 +00:00
|
|
|
|
app.controller('ApiHostController', ['$scope', 'api_host', 'data', function( $scope, api_host, data) {
|
|
|
|
|
var ctrl = this;
|
|
|
|
|
$scope.data = data.getSharedData();
|
|
|
|
|
window.ctrl = $scope
|
|
|
|
|
}])
|
|
|
|
|
|
|
|
|
|
app.controller('PlugScheduleController', ['$scope', '$http', 'data', function( $scope, $http, data) {
|
2023-11-09 19:07:34 +00:00
|
|
|
|
var ctrl = this;
|
|
|
|
|
ctrl.editMode = false;
|
2024-02-26 23:25:12 +00:00
|
|
|
|
$scope.data = data.getSharedData();
|
2023-11-09 19:07:34 +00:00
|
|
|
|
|
|
|
|
|
ctrl.$onInit = function() {
|
|
|
|
|
ctrl.scheduleObj = new Schedule(); // Initialize with a default schedule
|
|
|
|
|
ctrl.getSchedule();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
ctrl.getSchedule = function() {
|
|
|
|
|
if (ctrl.plugId === undefined) {
|
2024-02-26 23:25:12 +00:00
|
|
|
|
return;
|
2023-11-09 19:07:34 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fetch the schedule from the server for a given plug ID
|
2024-02-26 23:25:12 +00:00
|
|
|
|
var url = `http://${$scope.data.api_host}/api/schedule/${ctrl.plugId}`
|
2023-11-09 19:07:34 +00:00
|
|
|
|
$http.get(url, { responseType: 'arraybuffer' }).then(function(response) {
|
|
|
|
|
ctrl.scheduleObj.loadFromArrayBuffer(response.data);
|
|
|
|
|
ctrl.displaySchedule = ctrl.scheduleObj.serializeForDisplay();
|
|
|
|
|
}).catch(function(error) {
|
|
|
|
|
console.error('Error fetching schedule:', error);
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
ctrl.modifySchedule = function() {
|
|
|
|
|
ctrl.editMode = true;
|
|
|
|
|
// Prepare a matrix for editing
|
|
|
|
|
ctrl.editMatrix = ctrl.scheduleObj.prepareEditMatrix();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
ctrl.saveSchedule = function() {
|
|
|
|
|
ctrl.editMode = false;
|
|
|
|
|
// Convert the edit matrix back to schedule format
|
|
|
|
|
ctrl.scheduleObj.updateFromEditMatrix(ctrl.editMatrix);
|
|
|
|
|
ctrl.updateSchedule();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Send the updated schedule to the server
|
|
|
|
|
ctrl.updateSchedule = function() {
|
2024-02-26 23:25:12 +00:00
|
|
|
|
var url = `http://${$scope.data.api_host}/api/schedule/${ctrl.plugId}`
|
2023-11-09 19:07:34 +00:00
|
|
|
|
var serializedSchedule = ctrl.scheduleObj.serializeToArrayBuffer();
|
|
|
|
|
|
|
|
|
|
$http.post(url, serializedSchedule, {
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/octet-stream'
|
|
|
|
|
},
|
|
|
|
|
responseType: 'arraybuffer',
|
|
|
|
|
transformRequest: []
|
|
|
|
|
}).then(function(response) {
|
|
|
|
|
// ctrl.getSchedule();
|
|
|
|
|
console.log("successfully updated");
|
|
|
|
|
ctrl.displaySchedule = ctrl.scheduleObj.serializeForDisplay();
|
|
|
|
|
}, function(error) {
|
2024-02-26 23:25:12 +00:00
|
|
|
|
alert('Error updating schedule:', error);
|
2023-11-09 19:07:34 +00:00
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// check all segments for a specific hour
|
|
|
|
|
ctrl.checkAll = function(hourIndex) {
|
|
|
|
|
ctrl.editMatrix[hourIndex].forEach(segment => {
|
|
|
|
|
segment.value = 1;
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// uncheck all segments for a specific hour
|
|
|
|
|
ctrl.uncheckAll = function(hourIndex) {
|
|
|
|
|
ctrl.editMatrix[hourIndex].forEach(segment => {
|
|
|
|
|
segment.value = 0;
|
|
|
|
|
});
|
|
|
|
|
};
|
2024-02-26 23:25:12 +00:00
|
|
|
|
ctrl.invertAll = function(hourIndex) {
|
|
|
|
|
ctrl.editMatrix[hourIndex].forEach(segment => {
|
|
|
|
|
var inverted = Math.abs( segment.value - 1)
|
|
|
|
|
segment.value = inverted;
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
ctrl.checkPattern = function(hourIndex, results) {
|
|
|
|
|
for( index = 0; index < ctrl.editMatrix[hourIndex].length; index++){
|
|
|
|
|
ctrl.editMatrix[hourIndex][index] = { value : results[index] };
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
ctrl.checkHalf = function(hourIndex) {
|
|
|
|
|
ctrl.checkPattern( hourIndex, [1,1,1,1,1,1,0,0,0,0,0,0]);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
ctrl.checkFourth = function(hourIndex) {
|
|
|
|
|
ctrl.checkPattern( hourIndex, [1,1,1,0,0,0,1,1,1,0,0,0]);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
ctrl.checkSixth = function(hourIndex) {
|
|
|
|
|
ctrl.checkPattern( hourIndex, [1,1,0,0,1,1,0,0,1,1,0,0]);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
ctrl.checkTwelveth = function(hourIndex) {
|
|
|
|
|
ctrl.checkPattern( hourIndex, [1,0,1,0,1,0,1,0,1,0,1,0]);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2023-11-09 19:07:34 +00:00
|
|
|
|
}]);
|
|
|
|
|
|
|
|
|
|
app.component('plugScheduleWidget', {
|
|
|
|
|
bindings: {
|
|
|
|
|
plugId: '<'
|
|
|
|
|
},
|
|
|
|
|
template: `
|
|
|
|
|
<div class="plug-schedule-widget">
|
2024-02-26 23:25:12 +00:00
|
|
|
|
<h3>Plug {{ $ctrl.plugId }}</h3>
|
2023-11-09 19:07:34 +00:00
|
|
|
|
<div ng-if="!$ctrl.editMode">
|
|
|
|
|
<div ng-repeat="hour in $ctrl.displaySchedule track by $index" class="hour-cell" ng-class="hour.class">
|
|
|
|
|
{{ hour.displayValue }}
|
|
|
|
|
</div>
|
2024-02-26 23:25:12 +00:00
|
|
|
|
<button class="modify" ng-click="$ctrl.modifySchedule()">Modify</button>
|
2023-11-09 19:07:34 +00:00
|
|
|
|
</div>
|
|
|
|
|
<div ng-if="$ctrl.editMode">
|
|
|
|
|
<table>
|
2024-02-26 23:25:12 +00:00
|
|
|
|
<tr>
|
|
|
|
|
<td class=></td>
|
|
|
|
|
<td class="minutes" ng-repeat="segment in [0,1,2,3,4,5,6,7,8,9,10,11]">{{ segment * 5 }}</td>
|
|
|
|
|
</tr>
|
2023-11-09 19:07:34 +00:00
|
|
|
|
<tr ng-repeat="hourSegments in $ctrl.editMatrix track by $index">
|
2024-02-26 23:25:12 +00:00
|
|
|
|
<td class="hours">{{ $index }}</td>
|
|
|
|
|
<td class="cell" ng-repeat="segment in hourSegments track by $index">
|
2023-11-09 19:07:34 +00:00
|
|
|
|
<input type="checkbox" ng-model="segment.value" ng-true-value="1" ng-false-value="0">
|
|
|
|
|
</td>
|
|
|
|
|
<td>
|
2024-02-26 23:25:12 +00:00
|
|
|
|
<button ng-click="$ctrl.checkAll($index)">1</button>
|
|
|
|
|
<button ng-click="$ctrl.uncheckAll($index)">0</button>
|
|
|
|
|
<button ng-click="$ctrl.checkHalf($index)">½</button>
|
|
|
|
|
<button ng-click="$ctrl.checkFourth($index)">¼</button>
|
|
|
|
|
<button ng-click="$ctrl.checkSixth($index)">⅙</button>
|
|
|
|
|
<button ng-click="$ctrl.checkTwelveth($index)">¹⁄₁₂</button>
|
|
|
|
|
<button ng-click="$ctrl.invertAll($index)">∨</button>
|
2023-11-09 19:07:34 +00:00
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
</table>
|
|
|
|
|
|
|
|
|
|
<button ng-click="$ctrl.saveSchedule()">Save Schedule</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`,
|
|
|
|
|
controller: 'PlugScheduleController'
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// End of Angular
|
|
|
|
|
|
2024-02-26 23:25:12 +00:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<div ng-app="plugSchedulerApp" id="main_container">
|
|
|
|
|
<div ng-controller="PlugScheduleController">
|
|
|
|
|
<plug-schedule-widget plug-id="1"></plug-schedule-widget>
|
|
|
|
|
<plug-schedule-widget plug-id="2"></plug-schedule-widget>
|
|
|
|
|
<plug-schedule-widget plug-id="3"></plug-schedule-widget>
|
|
|
|
|
<plug-schedule-widget plug-id="4"></plug-schedule-widget>
|
|
|
|
|
<plug-schedule-widget plug-id="5"></plug-schedule-widget>
|
|
|
|
|
<plug-schedule-widget plug-id="6"></plug-schedule-widget>
|
|
|
|
|
<plug-schedule-widget plug-id="7"></plug-schedule-widget>
|
|
|
|
|
<plug-schedule-widget plug-id="8"></plug-schedule-widget>
|
|
|
|
|
</div>
|
|
|
|
|
<!--
|
|
|
|
|
<div ng-controller="ApiHostController">
|
|
|
|
|
<h3>
|
|
|
|
|
Change Host
|
|
|
|
|
<input type="text" ng-model="data.api_host" value="{{data.api_host}}" style="margin: 10px;padding: 3px; font-size: 14pt; color: #333340; border-radius: 6px;">
|
|
|
|
|
</h3>
|
|
|
|
|
</div>
|
|
|
|
|
-->
|
2023-11-09 19:07:34 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<style>
|
|
|
|
|
* {
|
2023-11-09 20:34:43 +00:00
|
|
|
|
font-family: Calibri, Candara, Segoe, Segoe UI, Optima, Arial, sans-serif;
|
2023-11-09 19:07:34 +00:00
|
|
|
|
padding: 0px;
|
|
|
|
|
border-spacing: 0px;
|
|
|
|
|
border: transparent;
|
|
|
|
|
text-align: center;
|
2023-11-09 20:34:43 +00:00
|
|
|
|
color: #aaa;
|
2023-11-09 19:07:34 +00:00
|
|
|
|
font-weight: normal;
|
2024-02-26 23:25:12 +00:00
|
|
|
|
font-size: 20pt;
|
2023-11-09 19:07:34 +00:00
|
|
|
|
}
|
|
|
|
|
body {
|
|
|
|
|
margin: auto;
|
2023-11-09 20:34:43 +00:00
|
|
|
|
background: #333340;
|
|
|
|
|
}
|
2023-11-09 19:07:34 +00:00
|
|
|
|
button {
|
|
|
|
|
background: #696983;
|
2024-02-26 23:25:12 +00:00
|
|
|
|
border-radius: 6px;
|
|
|
|
|
box-shadow: 1px 1px 3px rgba(0,0,0,0.6);
|
2023-11-09 19:07:34 +00:00
|
|
|
|
color: eee;
|
2024-02-26 23:25:12 +00:00
|
|
|
|
font-size: 0.8em;
|
|
|
|
|
margin-left: 4px;
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
padding: 14px;
|
|
|
|
|
vertical-align: top;
|
|
|
|
|
}
|
|
|
|
|
button.modify {
|
|
|
|
|
margin: 4px;
|
|
|
|
|
}
|
|
|
|
|
button.modifiers {
|
|
|
|
|
width: 40px;
|
2023-11-09 19:07:34 +00:00
|
|
|
|
}
|
2024-02-26 23:25:12 +00:00
|
|
|
|
|
|
|
|
|
button:hover {
|
|
|
|
|
background: #9f9fc4;
|
|
|
|
|
box-shadow: inset 1px 1px 3px rgba(0,0,0,0.6);
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-09 19:07:34 +00:00
|
|
|
|
.hour-cell {
|
|
|
|
|
display: inline-block;
|
2024-02-26 23:25:12 +00:00
|
|
|
|
width: 25px;
|
|
|
|
|
height: 50px;
|
2023-11-09 19:07:34 +00:00
|
|
|
|
margin-top: 4px;
|
|
|
|
|
}
|
2024-02-26 23:25:12 +00:00
|
|
|
|
.minutes, .hours {
|
|
|
|
|
font-size: 0.6em;
|
|
|
|
|
color: rgba(255, 255, 255, .5);
|
|
|
|
|
padding: 6px;
|
|
|
|
|
}
|
|
|
|
|
table tr td {
|
|
|
|
|
padding: 3px;
|
|
|
|
|
}
|
|
|
|
|
tr:nth-child(2n) {
|
|
|
|
|
box-shadow: inset 1px 1px 4px 1px rgba(0,0,0,0.54);
|
|
|
|
|
background: rgba(0, 0, 0, 0.07);
|
|
|
|
|
}
|
|
|
|
|
tr td.cell {
|
|
|
|
|
border-left: 1px solid rgba(61, 61, 78, .61);
|
|
|
|
|
}
|
|
|
|
|
div#main_container {
|
|
|
|
|
margin: auto;
|
|
|
|
|
}
|
2023-11-09 19:07:34 +00:00
|
|
|
|
|
|
|
|
|
.off {
|
|
|
|
|
background-color: #eee;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.on {
|
|
|
|
|
background-color: #8e8;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.partial {
|
|
|
|
|
background-color: #bdc;
|
|
|
|
|
}
|
|
|
|
|
table {
|
|
|
|
|
margin: auto;
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
</body>
|
|
|
|
|
</html>
|