Created
December 24, 2022 04:32
-
-
Save philharlow/a550d96cad82b4fe8b28a447527dc632 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <ESPHASDevice.h> | |
//////////////////////////////////////////////////////////// | |
// HTML | |
//////////////////////////////////////////////////////////// | |
/* | |
const char index_html[] PROGMEM = R"rawliteral( | |
<!DOCTYPE HTML><html> | |
<head> | |
<meta name="viewport" content="width=device-width, initial-scale=1"> | |
<style> | |
html { | |
font-family: Arial; | |
display: inline-block; | |
margin: 0px auto; | |
text-align: center; | |
} | |
h2 { font-size: 3.0rem; } | |
p { font-size: 3.0rem; } | |
.units { font-size: 1.2rem; } | |
.dht-labels{ | |
font-size: 1.5rem; | |
vertical-align:middle; | |
padding-bottom: 15px; | |
} | |
</style> | |
</head> | |
<body> | |
<h2>ESP8266 DHT Server</h2> | |
<p> | |
<span class="dht-labels">Temperature</span> | |
<span id="temperature">%TEMPERATURE%</span> | |
<sup class="units">°C</sup> | |
</p> | |
<p> | |
<span class="dht-labels">Humidity</span> | |
<span id="humidity">%HUMIDITY%</span> | |
<sup class="units">%</sup> | |
</p> | |
</body> | |
<script> | |
setInterval(function ( ) { | |
var xhttp = new XMLHttpRequest(); | |
xhttp.onreadystatechange = function() { | |
if (this.readyState == 4 && this.status == 200) { | |
document.getElementById("temperature").innerHTML = this.responseText; | |
} | |
}; | |
xhttp.open("GET", "/temperature", true); | |
xhttp.send(); | |
}, 10000 ) ; | |
setInterval(function ( ) { | |
var xhttp = new XMLHttpRequest(); | |
xhttp.onreadystatechange = function() { | |
if (this.readyState == 4 && this.status == 200) { | |
document.getElementById("humidity").innerHTML = this.responseText; | |
} | |
}; | |
xhttp.open("GET", "/humidity", true); | |
xhttp.send(); | |
}, 10000 ) ; | |
</script> | |
</html>)rawliteral"; | |
*/ | |
const char* fwHost = "****.local"; | |
const char* fwHostFingerprint = "********"; | |
ESPHASDevice* instance; | |
EEPROMInt* ESPHASDevice::AddSettingInt(String name, int defaultVal) { | |
EEPROMInt* val = new EEPROMInt(name, defaultVal); | |
EEPROMSetting* lastSetting = (EEPROMSetting*)NameSetting; | |
while (lastSetting->Next != nullptr) lastSetting = lastSetting->Next; | |
lastSetting->Next = (EEPROMSetting*)val; | |
return val; | |
} | |
EEPROMFloat* ESPHASDevice::AddSettingFloat(String name, float defaultVal) { | |
EEPROMFloat* val = new EEPROMFloat(name, defaultVal); | |
EEPROMSetting* lastSetting = (EEPROMSetting*)NameSetting; | |
while (lastSetting->Next != nullptr) lastSetting = lastSetting->Next; | |
lastSetting->Next = (EEPROMSetting*)val; | |
return val; | |
} | |
EEPROMString* ESPHASDevice::AddSettingString(String name, char defaultVal[60]) { | |
EEPROMString* val = new EEPROMString(name, defaultVal); | |
EEPROMSetting* lastSetting = (EEPROMSetting*)NameSetting; | |
while (lastSetting->Next != nullptr) lastSetting = lastSetting->Next; | |
lastSetting->Next = (EEPROMSetting*)val; | |
return val; | |
} | |
EEPROMBool* ESPHASDevice::AddSettingBool(String name, bool defaultVal) { | |
EEPROMBool* val = new EEPROMBool(name, defaultVal); | |
EEPROMSetting* lastSetting = (EEPROMSetting*)NameSetting; | |
while (lastSetting->Next != nullptr) lastSetting = lastSetting->Next; | |
lastSetting->Next = (EEPROMSetting*)val; | |
return val; | |
} | |
bool TimerDefinition::loop(unsigned long now) { | |
unsigned long elapsed = now - start; | |
if (elapsed >= ms) { | |
callback(ESPHASDevice::millis64()); | |
start += ms; | |
//elapsed = now - start; | |
return true; | |
} | |
return false; | |
} | |
MQTTBinarySensor* ESPHASDevice::AddBinarySensor(String name) { | |
MQTTBinarySensor* entity = new MQTTBinarySensor(name); | |
AddEntity(entity); | |
return entity; | |
} | |
MQTTNumberSensor* ESPHASDevice::AddNumberSensor(String name) { | |
MQTTNumberSensor* entity = new MQTTNumberSensor(name); | |
AddEntity(entity); | |
return entity; | |
} | |
MQTTSwitch* ESPHASDevice::AddSwitch(String name, void (*callback)(bool on)) { | |
MQTTSwitch* entity = new MQTTSwitch(name); | |
AddEntity(entity); | |
entity->HandleCommand = [](String topic, String payload) { | |
MQTTSwitch* entity = instance->GetSwitch(topic); | |
if (entity) entity->SetOn(payload == "ON"); | |
}; | |
entity->HandleOn = callback; | |
return entity; | |
} | |
void ESPHASDevice::AddEntity(MQTTEntity* entity) { | |
if (FirstEntity) { | |
MQTTEntity* lastEntity = FirstEntity; | |
while (lastEntity->Next != nullptr) lastEntity = lastEntity->Next; | |
lastEntity->Next = entity; | |
} else { | |
FirstEntity = entity; | |
} | |
} | |
MQTTSwitch* ESPHASDevice::GetSwitch(String commandTopic) { | |
MQTTEntity* nextEntity = FirstEntity; | |
while (nextEntity != nullptr) { | |
if (nextEntity->Type == "switch") { | |
MQTTSwitch* switchEntity = (MQTTSwitch*)nextEntity; | |
if (switchEntity->CommandTopic == commandTopic) | |
return switchEntity; | |
} | |
nextEntity = nextEntity->Next; | |
} | |
return nullptr; | |
} | |
void MQTTEntity::Publish(String topic, String payload) { | |
instance->publish(topic, payload); | |
} | |
void MQTTEntity::Subscribe(String topic, void (*callback)(String topic2, String payload)) { | |
instance->subscribe(topic, callback); | |
} | |
/* | |
template<typename T> | |
T* ESPHASDevice::AddSetting(T* setting) { | |
EEPROMSetting* s = (EEPROMSetting*)NameSetting; | |
while (s->Next != nullptr) s->Next; | |
s->Next = (EEPROMSetting*)setting; | |
return setting; | |
} | |
*/ | |
int EPROM_MEMORY_SIZE = 512; | |
void ESPHASDevice::SaveSettings() { | |
Serial.println("Saving settings"); | |
EEPROM.begin(EPROM_MEMORY_SIZE); | |
EEPROM.put<uint8_t>(0, eepromVersion); | |
EEPROMSetting* s = (EEPROMSetting*)NameSetting; | |
int index = 1; | |
while (s != nullptr) { | |
Sprintln("Writing setting: " + String(index)); | |
s->EEPROMPut(index); | |
index += s->eepromSize; | |
s = s->Next; | |
} | |
EEPROM.commit(); | |
EEPROM.end(); | |
} | |
void ESPHASDevice::loadSettings() { | |
uint8_t settingsVer = 0; | |
EEPROM.begin(EPROM_MEMORY_SIZE); | |
EEPROM.get(0, settingsVer ); | |
EEPROM.end(); | |
/*Serial.print("versionAsInt: "); | |
Serial.println(eepromVersion); | |
Serial.print("settingsVer: "); | |
Serial.println(settingsVer);*/ | |
if (settingsVer == eepromVersion) { | |
Serial.println("Same version! loading settings"); | |
EEPROM.begin(EPROM_MEMORY_SIZE); | |
Sprintln("Looping settings"); | |
EEPROMSetting* s = (EEPROMSetting*)NameSetting; | |
int index = 1; | |
while (s != nullptr) { | |
Sprintln("Read at: " + String(index)); | |
s->EEPROMGet(index); | |
index += s->eepromSize; | |
s = s->Next; | |
} | |
EEPROM.end(); | |
Sprintln("finished loop"); | |
deviceName = String(NameSetting->Value); | |
Serial.println(deviceName); | |
} else { | |
Serial.println("different version! saving settings"); | |
SaveSettings(); | |
} | |
} | |
String ESPHASDevice::callURL(String server, int port, String url) | |
{ | |
HTTPClient httpClient; | |
httpClient.begin(server, port, url); | |
int httpCode = httpClient.GET(); | |
if(httpCode == 200) | |
{ | |
String payload = httpClient.getString(); | |
Serial.println("callURL("+server+":"+port+"/"+url+") received: " + payload); | |
return payload; | |
} | |
else if (httpCode == 0) | |
Serial.print("callURL("+server+":"+port+"/"+url+") failed, no connection or no HTTP server."); | |
return ""; | |
} | |
//////////////////////////////////////////////////////////// | |
// HTTP Handling | |
//////////////////////////////////////////////////////////// | |
void handleNotFound() | |
{ | |
ESP8266WebServer* httpServer = instance->httpServer; | |
String msg = "File Not Found<br/><br/>"; | |
msg += "<b>URI:</b> " + httpServer->uri() + "</br>"; | |
msg += "<b>Method:</b> "; | |
msg += (httpServer->method() == HTTP_GET)?"GET":"POST"; | |
msg += "<br/><b>Arguments:</b> "; | |
msg += httpServer->args(); | |
msg += "<br/>"; | |
for (uint8_t i=0; i<httpServer->args(); i++) | |
msg += " " + httpServer->argName(i) + ": " + httpServer->arg(i) + "<br/>"; | |
msg += "<br/><b>Type:</b> "; | |
msg += instance->deviceType; | |
msg += "<br/><b>Name:</b> "; | |
msg += instance->deviceName; | |
msg += "<br/><b>Version:</b> "; | |
msg += instance->firmwareVersion; | |
msg += "<br/><br/><b><a href='/update'>Update</a></b><br/>"; | |
msg += "<br/><b>Subscriptions:</b><br>"; | |
for (int i=0; i<instance->subscriptionCount; i++) { | |
String topic = instance->subscriptions[i].topic; | |
msg += topic + "<br/>"; | |
} | |
msg += "<br/><b>EEPROM:</b><br>"; | |
EEPROMSetting* s = (EEPROMSetting*)instance->NameSetting; | |
while (s != nullptr) { | |
msg += s->Name + " = " + s->GetDisplayValue() + "<br/>"; | |
s = s->Next; | |
} | |
msg += "<br/><b>Entities:</b><br>"; | |
MQTTEntity* nextEntity = instance->FirstEntity; | |
while (nextEntity != nullptr) { | |
msg += nextEntity->Name + " - " + nextEntity->GetStatusPayload() + "<br/>"; | |
nextEntity = nextEntity->Next; | |
} | |
msg += "<hr/><b>Custom:</b><br>"; | |
if (instance->onPageNotFound) | |
msg += instance->onPageNotFound(); | |
httpServer->send(404, "text/html", msg); | |
} | |
//////////////////////////////////////////////////////////// | |
// Setup | |
//////////////////////////////////////////////////////////// | |
ESPHASDevice::ESPHASDevice() { | |
Serial.begin(115200); | |
httpServer = new ESP8266WebServer(80); | |
httpUpdater = new ESP8266HTTPUpdateServer(); | |
httpUpdater->setup(httpServer); | |
char name[60] = ""; | |
WiFi.macAddress().toCharArray(name, 60); | |
Sprintln("copnstructor()"); | |
NameSetting = new EEPROMString("name", name); | |
Sprintln((int)NameSetting); | |
inited = true; | |
} | |
void ESPHASDevice::setup_wifi() { | |
delay(10); | |
// We start by connecting to a WiFi network | |
Serial.println(); | |
Serial.print("Connecting to "); | |
Serial.println(ssid); | |
WiFi.hostname(pubSubClientId); | |
WiFi.mode(WIFI_STA); | |
WiFi.begin(ssid, password); | |
while (WiFi.status() != WL_CONNECTED) { | |
delay(500); | |
Serial.print("."); | |
} | |
randomSeed(micros()); | |
Serial.println(""); | |
Serial.println("WiFi connected"); | |
Serial.println("IP address: "); | |
Serial.println(WiFi.localIP()); | |
} | |
void ESPHASDevice::registerMQTT() { | |
DynamicJsonDocument doc(1024); | |
doc["type"] = deviceType; | |
doc["name"] = deviceName; | |
doc["mac"] = WiFi.macAddress(); | |
doc["ip"] = WiFi.localIP().toString(); | |
doc["version"] = firmwareVersion; | |
String payload; | |
serializeJson(doc, payload); | |
publish(deviceType + "/register", payload); | |
//Serial.print("registered: "); | |
Sprintln(subscriptionCount); | |
for (int i=0; i<subscriptionCount; i++) { | |
String topic = subscriptions[i].topic; | |
//Serial.print("Subscribing to: "); | |
Sprintln(topic); | |
pubSubClient->subscribe(topic.c_str(), subscriptions[i].qos); | |
} | |
if (FirstEntity) | |
subscribe("homeassistant/status", [](String topic, String payload) { | |
if (payload == "online") | |
instance->RefreshMQTTEntities(); | |
}); | |
RefreshMQTTEntities(); | |
} | |
void ESPHASDevice::RefreshMQTTEntities() { | |
MQTTEntity* nextEntity = FirstEntity; | |
while (nextEntity != nullptr) { | |
nextEntity->onMQTTConnected(); | |
nextEntity = nextEntity->Next; | |
} | |
} | |
void ESPHASDevice::createTimer(unsigned long ms, bool repeats, void (*callback)(uint64_t now)) { | |
Serial.println("Adding timer. ms: " + String(ms)); | |
TimerDefinition* newTimer = new TimerDefinition(ms, repeats, callback); | |
if (timers == nullptr) | |
timers = newTimer; | |
else { | |
TimerDefinition* lastTimer = timers; | |
while (lastTimer != nullptr && lastTimer->next != nullptr) lastTimer = lastTimer->next; | |
lastTimer->next = newTimer; | |
} | |
} | |
void ESPHASDevice::timerLoop() { | |
unsigned long now = millis(); | |
TimerDefinition* def = timers; | |
TimerDefinition* last = timers; | |
while (def != nullptr) { | |
if (def->loop(now) && def->repeats == false) { | |
last->next = def->next; | |
delete(def); | |
def = last->next; | |
} else { | |
last = def; | |
def = def->next; | |
} | |
} | |
} | |
void ESPHASDevice::subscribe(String topic, void (*callback)(String topic2, String payload), int qos) { | |
//Serial.println("Adding subscription to: " + topic); | |
subscriptions[subscriptionCount++] = MQTTSubscription(topic, callback, qos); | |
if (pubSubClient->connected()) | |
{ | |
//Serial.print("Subscribing to: "); | |
Sprintln(topic); | |
pubSubClient->subscribe(topic.c_str(), qos); | |
} | |
} | |
void ESPHASDevice::handleMQTT(String topic, String payload) { | |
Serial.print("Handling sub for: "); | |
Serial.println(topic); | |
for (int i=0; i<subscriptionCount; i++) { | |
String subTopic = subscriptions[i].topic; | |
// TODO: check for wildcards | |
if (topic == subTopic) { | |
if (subscriptions[i].callback) | |
subscriptions[i].callback(topic, payload); | |
} | |
} | |
} | |
void ESPHASDevice::publish(String topic, String payload, bool retained) { | |
pubSubClient->publish_P(topic.c_str(), (uint8_t*)payload.c_str(), payload.length(), retained); | |
} | |
void mqttCallback(char* topic, uint8_t* payloadBytes, unsigned int length) { | |
payloadBytes[length] = '\0'; // Make payload a string by NULL terminating it. | |
String payloadStr = String((char *)payloadBytes); | |
String topicStr = String(topic); | |
instance->handleMQTT(topicStr, payloadStr); | |
} | |
void handleUpdateSettings(String topic, String payload) { | |
instance->setSettingsJson(payload); | |
} | |
// hack | |
void ESPHASDevice::setSettingsJson(String json) { | |
Sprintln("updateSettingsJson()"); | |
DynamicJsonDocument doc(512); | |
deserializeJson(doc, json); | |
EEPROMSetting* s = (EEPROMSetting*)NameSetting; | |
while (s != nullptr) { | |
s->UpdateFromJson(doc); | |
s = s->Next; | |
} | |
SaveSettings(); | |
// hack | |
sendSettingsJson(); | |
if (onSettingsChanged) | |
onSettingsChanged(); | |
} | |
void handleGetSettings(String topic, String payload) { | |
instance->sendSettingsJson(); | |
} | |
void ESPHASDevice::sendSettingsJson() { | |
Sprintln("sendSettingsJson()"); | |
DynamicJsonDocument doc(512); | |
EEPROMSetting* s = (EEPROMSetting*)NameSetting; | |
while (s != nullptr) { | |
s->WriteJson(doc); | |
s = s->Next; | |
} | |
String payload; | |
serializeJson(doc, payload); | |
Serial.println(payload); | |
publish(WiFi.macAddress() + "/settings", payload); | |
} | |
void handleResetSettings(String topic, String payload) { | |
instance->resetSettings(); | |
} | |
void ESPHASDevice::resetSettings() { | |
Sprintln("resetSettingsJson()"); | |
// Skip resetting the name | |
EEPROMSetting* s = (EEPROMSetting*)NameSetting->Next; | |
while (s != nullptr) { | |
s->Reset(); | |
s = s->Next; | |
} | |
SaveSettings(); | |
// hack | |
sendSettingsJson(); | |
} | |
void handleLoadSettings(String topic, String payload) { | |
instance->loadSettings(); | |
} | |
void handleTopics(String topic, String payload) { | |
instance->sendTopics(); | |
} | |
void ESPHASDevice::sendTopics() { | |
Sprintln("sendTopics()"); | |
const size_t CAPACITY = JSON_ARRAY_SIZE(4*maxSubscriptions); | |
// allocate the memory for the document | |
StaticJsonDocument<CAPACITY> doc; | |
JsonArray array = doc.to<JsonArray>(); | |
for (int i=0; i<subscriptionCount; i++) { | |
subscriptions[i].WriteTopic(array); | |
} | |
String payload; | |
serializeJson(doc, payload); | |
Serial.println(payload); | |
publish(WiFi.macAddress() + "/topics", payload); | |
} | |
void reboot(String topic, String payload) { | |
//ESP.restart(); | |
// let the watchdog reboot us | |
while (true) {} | |
} | |
void update(String topic, String payload) { | |
String url = "http://" + String(fwHost) + "/firmware/" + instance->deviceType + ".ino.bin";// payload; | |
if (payload.length() > 5) | |
url = "http://" + String(fwHost) + payload; | |
Serial.print("connecting to "); | |
Serial.println(url); | |
// will reboot here if succesful | |
t_httpUpdate_return ret = ESPhttpUpdate.update(url, "", fwHostFingerprint); | |
switch(ret) { | |
case HTTP_UPDATE_FAILED: | |
Serial.printf("HTTP_UPDATE_FAILD Error (%d): %s", ESPhttpUpdate.getLastError(), ESPhttpUpdate.getLastErrorString().c_str()); | |
Serial.println(""); | |
break; | |
case HTTP_UPDATE_NO_UPDATES: | |
Serial.println("HTTP_UPDATE_NO_UPDATES"); | |
break; | |
} | |
Serial.println("Updating failed"); | |
Serial.println(ret); | |
} | |
void ESPHASDevice::init(String type, String version, String eepromV) { | |
instance = this; | |
deviceType = type; | |
pubSubClientId = deviceType + "-" + WiFi.macAddress(); | |
firmwareVersion = version; | |
eepromVersion = 0; | |
for (int i=0; i<eepromV.length(); i++) eepromVersion += (int)eepromV[i]; | |
EEPROMSetting* s = (EEPROMSetting*)NameSetting; | |
while (s != nullptr) { | |
for (int i=0; i<s->Name.length(); i++) eepromVersion += (int)s->Name[i]; | |
s = s->Next; | |
} | |
Serial.begin(115200); | |
//Serial.print("firmware: "); | |
Sprintln(firmwareVersion); | |
loadSettings(); | |
setup_wifi(); | |
pubSubClient = new PubSubClient(espClient); | |
pubSubClient->setServer(mqtt_server_address, 1883); | |
pubSubClient->setCallback(mqttCallback); | |
//httpServer->on("/", handleRoot); | |
httpServer->onNotFound(handleNotFound); | |
httpServer->begin(); | |
subscribe(WiFi.macAddress() + "/setSettings", handleUpdateSettings); | |
subscribe(WiFi.macAddress() + "/getSettings", handleGetSettings); | |
subscribe(WiFi.macAddress() + "/resetSettings", handleResetSettings); | |
subscribe(WiFi.macAddress() + "/getTopics", handleTopics); | |
subscribe(WiFi.macAddress() + "/reboot", reboot); | |
subscribe(WiFi.macAddress() + "/update", update); | |
subscribe(deviceType + "/update", update); | |
Sprintln("init done()"); | |
} | |
//////////////////////////////////////////////////////////// | |
// reconnect | |
//////////////////////////////////////////////////////////// | |
bool wasConnected = true; | |
unsigned long lastMqttAttempt = millis(); | |
void ESPHASDevice::connectMQTT() { | |
unsigned long now = millis(); | |
unsigned long elapsed = now - lastMqttAttempt; | |
if (elapsed >= 10000) { | |
Serial.print("Attempting MQTT connection..."); | |
if (onMQTTConnecting) | |
onMQTTConnecting(); | |
// Create a random client ID | |
lastMqttAttempt = now; | |
// Attempt to connect | |
if (pubSubClient->connect(pubSubClientId.c_str(), "mqttm", "mqttm")) { | |
Serial.println("connected"); | |
registerMQTT(); | |
wasConnected = true; | |
if (onMQTTConnected) | |
onMQTTConnected(); | |
} | |
else { | |
Serial.print("failed, rc="); | |
Serial.print(pubSubClient->state()); | |
Serial.println(" try again in 10 seconds"); | |
if (onMQTTDisconnected) | |
onMQTTDisconnected(wasConnected); | |
wasConnected = false; | |
} | |
} | |
} | |
//////////////////////////////////////////////////////////// | |
// Loop | |
//////////////////////////////////////////////////////////// | |
void ESPHASDevice::loop() | |
{ | |
if (!pubSubClient->connected()) { | |
if (CanDoBlockingWork) | |
connectMQTT(); | |
} | |
pubSubClient->loop(); | |
httpServer->handleClient(); | |
timerLoop(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment