Skip to content

Instantly share code, notes, and snippets.

@philharlow
Created December 24, 2022 04:32
Show Gist options
  • Save philharlow/a550d96cad82b4fe8b28a447527dc632 to your computer and use it in GitHub Desktop.
Save philharlow/a550d96cad82b4fe8b28a447527dc632 to your computer and use it in GitHub Desktop.
#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