esp32-timed-switch/esp32-timed-switch.ino

491 lines
21 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "esp_system.h"
#include "sntp.h"
#include "time.h"
#include <WiFi.h>
#include <WiFiUdp.h>
#include <Preferences.h>
#include "scheduler.h"
#include "scheduler_prefs.h"
// #include "wifi_prefs.h"
// --------------------------------------------
// Init const and global objects
// --------------------------------------------
const char* html_app="<html><head><title>ESP32 timed Switch</title><script src=\"https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.8.3/angular.js\"></script><link rel=\"manifest\" href=\"manifest.json\" /><meta charset=\"UTF-8\"></head><body><h1>Scheduler</h1><script>class Schedule {constructor(scheduleArray) {if (scheduleArray && scheduleArray.length === 24) {this.schedule = scheduleArray.slice();} else {this.schedule = new Array(24).fill(0x000);}}loadFromArrayBuffer = function(buffer) {var view = new DataView(buffer);for (var i = 0; i < 24; i++) {this.schedule[i] = view.getUint16(i * 2);}};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.');}const bitPosition = 11 - segment;if (state) {this.schedule[hour] |= (1 << bitPosition);} else {this.schedule[hour] &= ~(1 << bitPosition);}}serializeForDisplay() {return this.schedule.map(hour => {if (hour === 0xFFF) {return { displayValue: ' ', class: 'on' };} else if (hour === 0x00) {return { displayValue: ' ', class: 'off' };} else {return { displayValue: ' ', class: 'partial' };}});}prepareEditMatrix() {const editMatrix = [];for (let hour = 0; hour < 24; hour++) {const hourSegments = [];for (let segment = 0; segment < 12; segment++) {hourSegments.push({value: (this.schedule[hour] & (1 << segment)) !== 0 ? 1 : 0});}editMatrix.push(hourSegments.reverse());}return editMatrix;}updateFromEditMatrix(editMatrix) {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() {var buffer = new ArrayBuffer(24 * 2);var view = new DataView(buffer);for (var i = 0; i < 24; i++) {view.setUint16( i*2, this.schedule[i]);}return buffer;};}var app = angular.module('plugSchedulerApp', []);app.value('api_host', window.location.host);app.factory(\"data\", function(){var sharedData = { api_host: window.location.host};return {getSharedData: function () {return sharedData;}};});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) {var ctrl = this;ctrl.editMode = false;$scope.data = data.getSharedData();ctrl.$onInit = function() {ctrl.scheduleObj = new Schedule();ctrl.getSchedule();};ctrl.getSchedule = function() {if (ctrl.plugId === undefined) {return;}var url = `http://${$scope.data.api_host}/api/schedule/${ctrl.plugId}`;$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;ctrl.editMatrix = ctrl.scheduleObj.prepareEditMatrix();};ctrl.saveSchedule = function() {ctrl.editMode = false;ctrl.scheduleObj.updateFromEditMatrix(ctrl.editMatrix);ctrl.updateSchedule();};ctrl.updateSchedule = function() {var url = `http://${$scope.data.api_host}/api/schedule/${ctrl.plugId}`;var serializedSchedule = ctrl.scheduleObj.serializeToArrayBuffer();$http.post(url, serializedSchedule, {headers: {'Content-Type': 'application/octet-stream'},responseType: 'arraybuffer',transformRequest: []}).then(function(response) {console.log(\"successfully updated\");ctrl.displaySchedule = ctrl.scheduleObj.serializeForDisplay();}, function(error) {alert('Error updating schedule:', error);});};ctrl.checkAll = function(hourIndex) {ctrl.editMatrix[hourIndex].forEach(segment => {segment.value = 1;});};ctrl.uncheckAll = function(hourIndex) {ctrl.editMatrix[hourIndex].forEach(segment => {segment.value = 0;});};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]);};}]);app.component('plugScheduleWidget', {bindings: {plugId: '<'},template: `<div class=\"plug-schedule-widget\"><h3>Plug {{ $ctrl.plugId }}</h3><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><button class=\"modify\" ng-click=\"$ctrl.modifySchedule()\">Modify</button></div><div ng-if=\"$ctrl.editMode\"><table><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><tr ng-repeat=\"hourSegments in $ctrl.editMatrix track by $index\"><td class=\"hours\">{{ $index }}</td><td class=\"cell\" ng-repeat=\"segment in hourSegments track by $index\"><input type=\"checkbox\" ng-model=\"segment.value\" ng-true-value=\"1\" ng-false-value=\"0\"></td><td><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></td></tr></table><button ng-click=\"$ctrl.saveSchedule()\">Save Schedule</button></div></div>`,controller: 'PlugScheduleController'});</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>--></div><style>* {font-family: Calibri, Candara, Segoe, Segoe UI, Optima, Arial, sans-serif;padding: 0px;border-spacing: 0px;border: transparent;text-align: center;color: #aaa;font-weight: normal;font-size: 20pt;}body {margin: auto;background: #333340;}button {background: #696983;border-radius: 6px;box-shadow: 1px 1px 3px rgba(0,0,0,0.6);color: eee;font-size: 0.8em;margin-left: 4px;padding: 14px;vertical-align: top;}button.modify {margin: 4px;}button.modifiers {width: 40px;}button:hover {background: #9f9fc4;box-shadow: inset 1px 1px 3px rgba(0,0,0,0.6);}.hour-cell {display: inline-block;width: 25px;height: 50px;margin-top: 4px;}.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;}.off {background-color: #eee;}.on {background-color: #8e8;}.partial {background-color: #bdc;}table {margin: auto;}</style></body></html>";
// PREFERENCES
Preferences preferences;
#define RW_MODE false
#define RO_MODE true
// WIFI
// const char* ssid = "Wokwi-GUEST";
// const char* password = "";
const uint8_t wifi_loop_max = 10;
// NTP
bool sntp_initialized = false;
const char* ntpServer1 = "pool.ntp.org";
const char* ntpServer2 = "time.nist.gov";
const long gmtOffset_sec = 2 * 3600; // GMT + 2
const int daylightOffset_sec = 0;
// TIMER
hw_timer_t*timer_main = NULL;
uint32_t timer_interval = 3000000;
uint16_t timer_interval_in_secs = timer_interval / 1000000;
;
// SCHEDULER
bool brun_scheduler = true;
uint8_t current_hour = HOUR_DEFAULT;
uint8_t current_minute = MINUTE_DEFAULT;
uint8_t current_second = 0;
uint32_t current_ts = 0;
uint32_t next_event_ts = 0;
int scheduler_1[24] ; // = {A60,A60,A60,A60,A60,A60,A60,A60,A60,A60,A60,A60,A60,A60,A60,A60,A60,A60,A60,A60,A60,A60,A60,A60};
int scheduler_2[24] ; // = {A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00};
int scheduler_3[24] ; // = {A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00};
int scheduler_4[24] ; // = {A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00};
int scheduler_5[24] ; // = {A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00};
int scheduler_6[24] ; // = {A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00};
int scheduler_7[24] ; // = {A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00};
int scheduler_8[24] ; // = {A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00,A00};
int* scheduler_list[] = {
scheduler_1,
scheduler_2,
scheduler_3,
scheduler_4,
scheduler_5,
scheduler_6,
scheduler_7,
scheduler_8,
};
// PINS
unsigned int PIN_1 = 12;
unsigned int PIN_2 = 14;
unsigned int PIN_3 = 27;
unsigned int PIN_4 = 26;
unsigned int PIN_5 = 20;
unsigned int PIN_6 = 33;
unsigned int PIN_7 = 32;
unsigned int PIN_8 = 35;
unsigned int pin_list[8] = {
PIN_1,
PIN_2,
PIN_3,
PIN_4,
PIN_5,
PIN_6,
PIN_7,
PIN_8
};
// HTTP
WiFiClient client;
WiFiServer http_server(3000);
String request;
// --------------------------------------------
// Local functions
// --------------------------------------------
// Time helper
uint32_t get_timestamp( uint8_t hour, uint8_t minute, uint8_t second){
return ( 3600 * hour + 60 * minute + second) % 86400;
}
bool event_keep_waiting( uint32_t current_ts, uint32_t event_ts ){
// basic case
// ts: 11h 41m 11s
// ev: 11h 44m 0s
// ts < ev => TRUE
if ( current_ts < event_ts ){
return true;
}
// if time looped at midnight
// ts: 23h 57m 21s
// ev: 0h 1m 0s
// ts > ev => TRUE (for high to very low values )
if( (current_ts - event_ts) > 600 && event_ts < 300 ) {
return true;
}
// ts: 11h 44m 11s
// ev: 11h 44m 0s
// ts > ev => FALSE
return false;
}
// NTP Callback
void timeavailable(struct timeval *t)
{
Serial.println("NTP::Got time adjustment from NTP!");
sntp_initialized = true;
return;
}
// TIMER Callback
void IRAM_ATTR onTimer(){
brun_scheduler = true;
}
void run_scheduler(){
// Serial.println("onTimer::run");
// Get the current time via NTP, or downgrade
if ( sntp_initialized ){
struct tm timeinfo;
time_t now;
time(&now);
localtime_r(&now, &timeinfo);
current_hour = timeinfo.tm_hour;
current_minute = timeinfo.tm_min;
current_second = timeinfo.tm_sec;
// If no NTP clock
}else{
// Serial.println("onTimer::NO NTP");
current_second += timer_interval_in_secs;
if(current_second >= 60) {
current_second = 0;
current_minute += 1;
if(current_minute >= 60) {
current_minute = 0;
current_hour += 1;
if(current_hour >= 24) {
current_hour = 0;
}
}
}
}
// If not expected to run exit
current_ts = get_timestamp( current_hour, current_minute, current_second );
// Serial.printf("onTimer::check event %d %d\n", current_ts, next_event_ts);
if( event_keep_waiting( current_ts, next_event_ts ) ){
return;
}
// Set the next target
next_event_ts = get_timestamp( current_hour, current_minute + EVENT_INC_MINUTE, current_second );
Serial.println("Reconfigure the Relays!");
// Run the relays reconfiguration
for ( int i = 0; i < 8; i++ ) {
// Get a pointer to the current array
int* scheduler = scheduler_list[i];
unsigned int pin = pin_list[i];
// Set the expected status
uint8_t minutes_by_5 = (current_minute / 5);
int hourly_12_values = scheduler[current_hour];
// Serial.printf("Check hour(%d) >> minutes(%d x 5)\n", hourly_12_values, minutes_by_5);
char buffer[40];
if (( hourly_12_values >> minutes_by_5 ) & 1) {
digitalWrite(pin, HIGH);
sprintf (buffer, "Pin %d is up ", pin);
} else {
digitalWrite(pin, LOW);
sprintf (buffer, "Pin %d is down ", pin);
}
Serial.println(buffer); // Get the expected status
}
Serial.println("Reconfiguration over");
return;
}
// HTTP
const char* getPreferenceName( int plug_id){
switch (plug_id) {
case 0:
return RELAY_1_SCHEDULE;
break;
case 1:
return RELAY_2_SCHEDULE;
break;
case 2:
return RELAY_3_SCHEDULE;
break;
case 3:
return RELAY_4_SCHEDULE;
break;
case 4:
return RELAY_5_SCHEDULE;
break;
case 5:
return RELAY_6_SCHEDULE;
break;
case 6:
return RELAY_7_SCHEDULE;
break;
case 7:
return RELAY_8_SCHEDULE;
break;
}
}
void saveSchedule( int plug_id, uint16_t data[24]){
const char* preference_name = getPreferenceName( plug_id );
preferences.putBytes(preference_name, data, 48);
Serial.print("saving size:");
Serial.print(sizeof(data));
Serial.print(" to ");
Serial.println(preference_name);
uint16_t buf[24];
preferences.getBytes(preference_name, buf, 48);
for ( int iterator=0; iterator < 24; iterator++){
Serial.print(iterator);
Serial.print(" : ");
Serial.print(data[iterator]);
Serial.print(" / ");
Serial.println(buf[iterator]);
}
delay(2000);
}
void sendSchedule(WiFiClient &client, int plug_id) {
uint16_t buf[24];
const char* preference_name = getPreferenceName( plug_id );
Serial.print("sending ");
Serial.println(preference_name);
preferences.getBytes(preference_name, buf, 48);
for ( int iterator=0; iterator < 24; iterator++){
Serial.print(iterator);
Serial.print(" : ");
Serial.println(buf[iterator]);
}
Serial.println("---------------------------");
client.write((const uint8_t*)buf, 24 * sizeof(uint16_t));
}
// --------------------------------------------
// ARDUINO Functions
// --------------------------------------------
void setup(){
// HARDWARE
Serial.begin(115200);
delay(1000);
Serial.println("Starting.");
pinMode(PIN_1, OUTPUT);
pinMode(PIN_2, OUTPUT);
pinMode(PIN_3, OUTPUT);
pinMode(PIN_4, OUTPUT);
pinMode(PIN_5, OUTPUT);
pinMode(PIN_6, OUTPUT);
pinMode(PIN_7, OUTPUT);
pinMode(PIN_8, OUTPUT);
// PREFERENCES
Serial.println("Setup::PREFERENCES");
preferences.begin(PREF_NAMESPACE, RW_MODE);
// preferences.clear();
bool pref_init = preferences.isKey("test");
if (true || pref_init == false) {
Serial.println("preferences not exist");
preferences.putBool("test", true);
saveSchedule(1,scheduler_default_deactivate);
saveSchedule(2,scheduler_default_deactivate);
saveSchedule(3,scheduler_default_deactivate);
saveSchedule(4,scheduler_default_deactivate);
/*
saveSchedule(5,scheduler_default_deactivate);
saveSchedule(6,scheduler_default_deactivate);
saveSchedule(7,scheduler_default_deactivate);
saveSchedule(8,scheduler_default_deactivate);
*/
}
preferences.getBytes(RELAY_1_SCHEDULE, scheduler_1, preferences.getBytesLength(RELAY_1_SCHEDULE));
preferences.getBytes(RELAY_2_SCHEDULE, scheduler_2, preferences.getBytesLength(RELAY_2_SCHEDULE));
preferences.getBytes(RELAY_3_SCHEDULE, scheduler_2, preferences.getBytesLength(RELAY_3_SCHEDULE));
preferences.getBytes(RELAY_4_SCHEDULE, scheduler_2, preferences.getBytesLength(RELAY_4_SCHEDULE));
preferences.getBytes(RELAY_5_SCHEDULE, scheduler_2, preferences.getBytesLength(RELAY_5_SCHEDULE));
preferences.getBytes(RELAY_6_SCHEDULE, scheduler_2, preferences.getBytesLength(RELAY_6_SCHEDULE));
preferences.getBytes(RELAY_7_SCHEDULE, scheduler_2, preferences.getBytesLength(RELAY_7_SCHEDULE));
preferences.getBytes(RELAY_8_SCHEDULE, scheduler_2, preferences.getBytesLength(RELAY_8_SCHEDULE));
preferences.end();
Serial.println("Setup::PREFERENCES::end");
// WIFI
Serial.println("Setup::WIFI");
Serial.printf("Connecting to %s ", ssid);
WiFi.begin(ssid, password);
uint8_t wifi_loop_count = 0;
while (WiFi.status() != WL_CONNECTED or wifi_loop_count < wifi_loop_max ) {
delay(1000);
wifi_loop_count++;
Serial.print(".");
}
// TIMER
Serial.println("Setup::Timer");
timer_main = timerBegin(0, 80, true);
timerAttachInterrupt(timer_main, &onTimer, true);
timerAlarmWrite(timer_main, timer_interval, true);
timerAlarmEnable(timer_main);
// NTP
Serial.println("Setup::NTP");
sntp_set_time_sync_notification_cb(timeavailable);
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer1, ntpServer2);
delay(5000);
// SCHEDULER
Serial.println("Setup::SCHEDULER");
next_event_ts = get_timestamp( HOUR_DEFAULT, MINUTE_DEFAULT, 10 );
// HTTP
Serial.println("Setup::HTTP");
http_server.begin();
Serial.print("Connect to IP Address: ");
Serial.print("http://");
Serial.println(WiFi.localIP());
// Todo
}
void loop(){
if( brun_scheduler == true ){
brun_scheduler = false;
run_scheduler();
}
client = http_server.available();
if (!client) {
// Serial.println("Server not available... skip...");
return;
}
String request = client.readStringUntil('\r');
Serial.print("request: ");
Serial.println(request);
// GET request
if (request.indexOf("GET /api/schedule/") >= 0) {
int startIdIndex = 18;
int endIdIndex = 19;
String plug_idStr = request.substring(startIdIndex, endIdIndex);
int plug_id = plug_idStr.toInt() - 1; // Adjust for 0 index
Serial.print("plug_id " );
Serial.println(plug_id);
if (plug_id >= 0 && plug_id < 8) {
client.println("HTTP/1.1 200 OK");
client.println("Content-Type: text/html");
client.println("Connection: close");
client.println();
sendSchedule(client, plug_id);
} else {
client.println("HTTP/1.1 404 Not Found");
client.println("Connection: close");
client.println();
}
// GET homepage
} else if (request.indexOf("GET /") >= 0) {
client.println("HTTP/1.1 200 OK");
client.println("Content-Type: text/html");
client.println("Connection: close");
client.println();
client.println(html_app);
} else {
// POST request
int slashIndex = request.indexOf("POST /api/schedule/");
if (slashIndex != -1) {
Serial.print("slashIndex: ");
Serial.println(slashIndex);
int nextSlashIndex = slashIndex + 19;
// Extract the plug ID
String plug_idStr = request.substring(nextSlashIndex, nextSlashIndex + 1);
Serial.print("plug_idStr ");
Serial.println(plug_idStr);
int plug_id = plug_idStr.toInt() - 1;
// Force a new line
client.readStringUntil('\n');
// Read the next line which should have the POST body/content
String header ;
while ( header = client.readStringUntil('\n') ){
Serial.print("header: '");
Serial.print(header);
Serial.print(" / ");
Serial.print(header.length());
Serial.println("'");
if( header.length() < 2 ){
Serial.println("parsed headers");
break;
}
}
Serial.println("Done reading. Going for binary.");
char post_body[48];
client.readBytes(post_body, 48);
Serial.print("Read post_body: ");
Serial.println(post_body);
// Update the schedule for the specified plug
if (plug_id >= 0 && plug_id < 8) {
uint16_t byte_value[24];
for (int i=0; i < 24; i++) {
byte_value[i] = (char) post_body[i*2] * 256 + post_body[(i*2)+1];
Serial.print("Byte ");
Serial.print(i);
Serial.print(" : ");
Serial.print(byte_value[i]);
Serial.println("");
}
saveSchedule(plug_id, byte_value );
Serial.println("Schedule updated for plug " + plug_idStr);
} else {
Serial.println("Invalid plug ID");
}
client.println("HTTP/1.1 200 OK");
client.println("Content-Type: text/plain");
client.println("Connection: close");
client.println();
}
}
client.stop();
}