Fork of the espurna firmware for `mhsw` switches
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

414 lines
12 KiB

/*
THINGSPEAK MODULE
Copyright (C) 2019 by Xose Pérez <xose dot perez at gmail dot com>
*/
#if THINGSPEAK_SUPPORT
#if THINGSPEAK_USE_ASYNC
#include <ESPAsyncTCP.h>
#else
#include <ESP8266WiFi.h>
#endif
#define THINGSPEAK_DATA_BUFFER_SIZE 256
const char THINGSPEAK_REQUEST_TEMPLATE[] PROGMEM =
"POST %s HTTP/1.1\r\n"
"Host: %s\r\n"
"User-Agent: ESPurna\r\n"
"Connection: close\r\n"
"Content-Type: application/x-www-form-urlencoded\r\n"
"Content-Length: %d\r\n\r\n";
bool _tspk_enabled = false;
bool _tspk_clear = false;
char * _tspk_queue[THINGSPEAK_FIELDS] = {NULL};
String _tspk_data;
bool _tspk_flush = false;
unsigned long _tspk_last_flush = 0;
unsigned char _tspk_tries = THINGSPEAK_TRIES;
#if THINGSPEAK_USE_ASYNC
AsyncClient * _tspk_client;
bool _tspk_connecting = false;
bool _tspk_connected = false;
#endif
// -----------------------------------------------------------------------------
#if BROKER_SUPPORT
void _tspkBrokerCallback(const unsigned char type, const char * topic, unsigned char id, const char * payload) {
// Process status messages
if (BROKER_MSG_TYPE_STATUS == type) {
tspkEnqueueRelay(id, (char *) payload);
tspkFlush();
}
// Porcess sensor messages
if (BROKER_MSG_TYPE_SENSOR == type) {
//tspkEnqueueMeasurement(id, (char *) payload);
//tspkFlush();
}
}
#endif // BROKER_SUPPORT
#if WEB_SUPPORT
bool _tspkWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
return (strncmp(key, "tspk", 4) == 0);
}
void _tspkWebSocketOnVisible(JsonObject& root) {
root["tspkVisible"] = static_cast<unsigned char>(haveRelaysOrSensors());
}
void _tspkWebSocketOnConnected(JsonObject& root) {
root["tspkEnabled"] = getSetting("tspkEnabled", THINGSPEAK_ENABLED).toInt() == 1;
root["tspkKey"] = getSetting("tspkKey", THINGSPEAK_APIKEY);
root["tspkClear"] = getSetting("tspkClear", THINGSPEAK_CLEAR_CACHE).toInt() == 1;
JsonArray& relays = root.createNestedArray("tspkRelays");
for (byte i=0; i<relayCount(); i++) {
relays.add(getSetting("tspkRelay", i, 0).toInt());
}
#if SENSOR_SUPPORT
_sensorWebSocketMagnitudes(root, "tspk");
#endif
}
#endif
void _tspkConfigure() {
_tspk_clear = getSetting("tspkClear", THINGSPEAK_CLEAR_CACHE).toInt() == 1;
_tspk_enabled = getSetting("tspkEnabled", THINGSPEAK_ENABLED).toInt() == 1;
if (_tspk_enabled && (getSetting("tspkKey", THINGSPEAK_APIKEY).length() == 0)) {
_tspk_enabled = false;
setSetting("tspkEnabled", 0);
}
if (_tspk_enabled && !_tspk_client) _tspkInitClient();
}
#if THINGSPEAK_USE_ASYNC
enum class tspk_state_t : uint8_t {
NONE,
HEADERS,
BODY
};
tspk_state_t _tspk_client_state = tspk_state_t::NONE;
unsigned long _tspk_client_ts = 0;
constexpr const unsigned long THINGSPEAK_CLIENT_TIMEOUT = 5000;
void _tspkInitClient() {
_tspk_client = new AsyncClient();
_tspk_client->onDisconnect([](void * s, AsyncClient * client) {
DEBUG_MSG_P(PSTR("[THINGSPEAK] Disconnected\n"));
_tspk_data = "";
_tspk_client_ts = 0;
_tspk_last_flush = millis();
_tspk_connected = false;
_tspk_connecting = false;
_tspk_client_state = tspk_state_t::NONE;
}, nullptr);
_tspk_client->onTimeout([](void * s, AsyncClient * client, uint32_t time) {
DEBUG_MSG_P(PSTR("[THINGSPEAK] Network timeout after %ums\n"), time);
client->close(true);
}, nullptr);
_tspk_client->onPoll([](void * s, AsyncClient * client) {
uint32_t ts = millis() - _tspk_client_ts;
if (ts > THINGSPEAK_CLIENT_TIMEOUT) {
DEBUG_MSG_P(PSTR("[THINGSPEAK] No response after %ums\n"), ts);
client->close(true);
}
}, nullptr);
_tspk_client->onData([](void * arg, AsyncClient * client, void * response, size_t len) {
char * p = nullptr;
do {
p = nullptr;
switch (_tspk_client_state) {
case tspk_state_t::NONE:
{
p = strnstr(reinterpret_cast<const char *>(response), "HTTP/1.1 200 OK", len);
if (!p) {
client->close(true);
return;
}
_tspk_client_state = tspk_state_t::HEADERS;
continue;
}
case tspk_state_t::HEADERS:
{
p = strnstr(reinterpret_cast<const char *>(response), "\r\n\r\n", len);
if (!p) return;
_tspk_client_state = tspk_state_t::BODY;
}
case tspk_state_t::BODY:
{
if (!p) {
p = strnstr(reinterpret_cast<const char *>(response), "\r\n\r\n", len);
if (!p) return;
}
unsigned int code = (p) ? atoi(&p[4]) : 0;
DEBUG_MSG_P(PSTR("[THINGSPEAK] Response value: %u\n"), code);
if ((0 == code) && _tspk_tries) {
_tspk_flush = true;
DEBUG_MSG_P(PSTR("[THINGSPEAK] Re-enqueuing %u more time(s)\n"), _tspk_tries);
} else {
_tspkClearQueue();
}
client->close(true);
_tspk_client_state = tspk_state_t::NONE;
}
}
} while (_tspk_client_state != tspk_state_t::NONE);
}, nullptr);
_tspk_client->onConnect([](void * arg, AsyncClient * client) {
_tspk_connected = true;
_tspk_connecting = false;
DEBUG_MSG_P(PSTR("[THINGSPEAK] Connected to %s:%u\n"), THINGSPEAK_HOST, THINGSPEAK_PORT);
#if THINGSPEAK_USE_SSL
uint8_t fp[20] = {0};
sslFingerPrintArray(THINGSPEAK_FINGERPRINT, fp);
SSL * ssl = _tspk_client->getSSL();
if (ssl_match_fingerprint(ssl, fp) != SSL_OK) {
DEBUG_MSG_P(PSTR("[THINGSPEAK] Warning: certificate doesn't match\n"));
}
#endif
DEBUG_MSG_P(PSTR("[THINGSPEAK] POST %s?%s\n"), THINGSPEAK_URL, _tspk_data.c_str());
char headers[strlen_P(THINGSPEAK_REQUEST_TEMPLATE) + strlen(THINGSPEAK_URL) + strlen(THINGSPEAK_HOST) + 1];
snprintf_P(headers, sizeof(headers),
THINGSPEAK_REQUEST_TEMPLATE,
THINGSPEAK_URL,
THINGSPEAK_HOST,
_tspk_data.length()
);
client->write(headers);
client->write(_tspk_data.c_str());
}, nullptr);
}
void _tspkPost() {
if (_tspk_connected || _tspk_connecting) return;
_tspk_client_ts = millis();
#if SECURE_CLIENT == SECURE_CLIENT_AXTLS
bool connected = _tspk_client->connect(THINGSPEAK_HOST, THINGSPEAK_PORT, THINGSPEAK_USE_SSL);
#else
bool connected = _tspk_client->connect(THINGSPEAK_HOST, THINGSPEAK_PORT);
#endif
_tspk_connecting = connected;
if (!connected) {
DEBUG_MSG_P(PSTR("[THINGSPEAK] Connection failed\n"));
_tspk_client->close(true);
}
}
#else // THINGSPEAK_USE_ASYNC
void _tspkPost() {
#if THINGSPEAK_USE_SSL
WiFiClientSecure _tspk_client;
#else
WiFiClient _tspk_client;
#endif
if (_tspk_client.connect(THINGSPEAK_HOST, THINGSPEAK_PORT)) {
DEBUG_MSG_P(PSTR("[THINGSPEAK] Connected to %s:%u\n"), THINGSPEAK_HOST, THINGSPEAK_PORT);
if (!_tspk_client.verify(THINGSPEAK_FINGERPRINT, THINGSPEAK_HOST)) {
DEBUG_MSG_P(PSTR("[THINGSPEAK] Warning: certificate doesn't match\n"));
}
DEBUG_MSG_P(PSTR("[THINGSPEAK] POST %s?%s\n"), THINGSPEAK_URL, _tspk_data.c_str());
char headers[strlen_P(THINGSPEAK_REQUEST_TEMPLATE) + strlen(THINGSPEAK_URL) + strlen(THINGSPEAK_HOST) + 1];
snprintf_P(headers, sizeof(headers),
THINGSPEAK_REQUEST_TEMPLATE,
THINGSPEAK_URL,
THINGSPEAK_HOST,
_tspk_data.length()
);
_tspk_client.print(headers);
_tspk_client.print(_tspk_data);
nice_delay(100);
String response = _tspk_client.readString();
int pos = response.indexOf("\r\n\r\n");
unsigned int code = (pos > 0) ? response.substring(pos + 4).toInt() : 0;
DEBUG_MSG_P(PSTR("[THINGSPEAK] Response value: %u\n"), code);
_tspk_client.stop();
_tspk_last_flush = millis();
if ((0 == code) && _tspk_tries) {
_tspk_flush = true;
DEBUG_MSG_P(PSTR("[THINGSPEAK] Re-enqueuing %u more time(s)\n"), _tspk_tries);
} else {
_tspkClearQueue();
}
return;
}
DEBUG_MSG_P(PSTR("[THINGSPEAK] Connection failed\n"));
}
#endif // THINGSPEAK_USE_ASYNC
void _tspkEnqueue(unsigned char index, const char * payload) {
DEBUG_MSG_P(PSTR("[THINGSPEAK] Enqueuing field #%u with value %s\n"), index, payload);
--index;
if (_tspk_queue[index] != NULL) free(_tspk_queue[index]);
_tspk_queue[index] = strdup(payload);
}
void _tspkClearQueue() {
_tspk_tries = THINGSPEAK_TRIES;
if (_tspk_clear) {
for (unsigned char id=0; id<THINGSPEAK_FIELDS; id++) {
if (_tspk_queue[id] != NULL) {
free(_tspk_queue[id]);
_tspk_queue[id] = NULL;
}
}
}
}
void _tspkFlush() {
if (!_tspk_flush) return;
if (millis() - _tspk_last_flush < THINGSPEAK_MIN_INTERVAL) return;
if (_tspk_connected || _tspk_connecting) return;
_tspk_last_flush = millis();
_tspk_flush = false;
_tspk_data.reserve(THINGSPEAK_DATA_BUFFER_SIZE);
// Walk the fields, numbered 1...THINGSPEAK_FIELDS
for (unsigned char id=0; id<THINGSPEAK_FIELDS; id++) {
if (_tspk_queue[id] != NULL) {
if (_tspk_data.length() > 0) _tspk_data.concat("&");
char buf[32] = {0};
snprintf_P(buf, sizeof(buf), PSTR("field%u=%s"), (id + 1), _tspk_queue[id]);
_tspk_data.concat(buf);
}
}
// POST data if any
if (_tspk_data.length()) {
_tspk_data.concat("&api_key=");
_tspk_data.concat(getSetting("tspkKey", THINGSPEAK_APIKEY));
--_tspk_tries;
_tspkPost();
}
}
// -----------------------------------------------------------------------------
bool tspkEnqueueRelay(unsigned char index, char * payload) {
if (!_tspk_enabled) return true;
unsigned char id = getSetting("tspkRelay", index, 0).toInt();
if (id > 0) {
_tspkEnqueue(id, payload);
return true;
}
return false;
}
bool tspkEnqueueMeasurement(unsigned char index, const char * payload) {
if (!_tspk_enabled) return true;
unsigned char id = getSetting("tspkMagnitude", index, 0).toInt();
if (id > 0) {
_tspkEnqueue(id, payload);
return true;
}
return false;
}
void tspkFlush() {
_tspk_flush = true;
}
bool tspkEnabled() {
return _tspk_enabled;
}
void tspkSetup() {
_tspkConfigure();
#if WEB_SUPPORT
wsRegister()
.onVisible(_tspkWebSocketOnVisible)
.onConnected(_tspkWebSocketOnConnected)
.onKeyCheck(_tspkWebSocketOnKeyCheck);
#endif
#if BROKER_SUPPORT
brokerRegister(_tspkBrokerCallback);
#endif
DEBUG_MSG_P(PSTR("[THINGSPEAK] Async %s, SSL %s\n"),
THINGSPEAK_USE_ASYNC ? "ENABLED" : "DISABLED",
THINGSPEAK_USE_SSL ? "ENABLED" : "DISABLED"
);
// Main callbacks
espurnaRegisterLoop(tspkLoop);
espurnaRegisterReload(_tspkConfigure);
}
void tspkLoop() {
if (!_tspk_enabled) return;
if (!wifiConnected() || (WiFi.getMode() != WIFI_STA)) return;
_tspkFlush();
}
#endif