Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 52 additions & 2 deletions HeishaMon/HeishaMon.ino
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,7 @@ void mqtt_reconnect()
if (mqttReconnects == 1) { //only resend all data on first connect to mqtt so a data bomb like and bad mqtt server will not cause a reconnect bomb everytime
if (heishamonSettings.use_1wire) resetlastalldatatime_dallas(); //resend all 1wire values to mqtt
resetlastalldatatime(); //resend all heatpump values to mqtt
publishGPIOStates(mqtt_client, heishamonSettings.gpioSettings, heishamonSettings.mqtt_topic_base, true, heishamonSettings.opentherm, heishamonSettings.listenonly); //publish all GPIO states on first connect
}
//use this to receive valid heishamon raw data from other heishamon to debug this OT code
//#define RAWDEBUG
Expand Down Expand Up @@ -1027,7 +1028,7 @@ void mqtt_callback(char* topic, byte* payload, unsigned int length) {
mqttOTCallback(topic_otcommand, msg);
} else if (strncmp(topic_command, mqtt_topic_gpio, strlen(mqtt_topic_gpio)) == 0) {
char* topic_gpiocommand = topic_command + strlen(mqtt_topic_gpio) + 1; //strip the gpio subtopic from the topic
mqttGPIOCallback(topic_gpiocommand, msg);
mqttGPIOCallback(topic_gpiocommand, msg, heishamonSettings.gpioSettings, heishamonSettings.opentherm, heishamonSettings.listenonly);
} else if (strncmp(topic_command, mqtt_topic_1wire, strlen(mqtt_topic_1wire)) == 0) { //this is a 1wire address topic, restore its retained value at boot
char* topic_1wire_address = topic_command + strlen(mqtt_topic_1wire) + 1; //strip the "1wire/" from the topic to get the sensor address
if ((strchr(topic_1wire_address, '/') == NULL) && (length > 0)) { //only handle the address topic itself (not .../alias) and skip empty (cleared) retained messages
Expand Down Expand Up @@ -1110,6 +1111,8 @@ int8_t webserver_cb(struct webserver_t *client, void *dat) {
client->route = 81; //for Apple devices
} else if (strcmp_P((char *)dat, PSTR("/factoryreset")) == 0) {
client->route = 90;
} else if (strcmp_P((char *)dat, PSTR("/gpio")) == 0) {
client->route = 191;
} else if (strcmp_P((char *)dat, PSTR("/command")) == 0) {
if ((client->userdata = malloc(1)) == NULL) {
loggingSerial.printf(PSTR("Out of memory %s:#%d\n"), __FUNCTION__, __LINE__);
Expand Down Expand Up @@ -1246,6 +1249,34 @@ int8_t webserver_cb(struct webserver_t *client, void *dat) {
case 110: {
return cacheSettings(client, args);
} break;
case 191: {
// GPIO web API: args "pin" (physical GPIO pin number) and "value" (0/1/on/off/true/false)
// Store the physical pin number in userdata between arg callbacks
if (strcmp_P((char *)args->name, PSTR("pin")) == 0) {
char pin_str[8];
snprintf(pin_str, sizeof(pin_str), "%.*s", args->len, args->value);
int phys_pin = atoi(pin_str);
client->userdata = (void*)(intptr_t)phys_pin;
} else if (strcmp_P((char *)args->name, PSTR("value")) == 0) {
int phys_pin = (int)(intptr_t)client->userdata;
char val_str[8];
snprintf(val_str, sizeof(val_str), "%.*s", args->len, args->value);
for (int i = 0; i < NUMGPIO; i++) {
if ((int)heishamonSettings.gpioSettings.gpioPin[i] == phys_pin && heishamonSettings.gpioSettings.gpioMode[i] == OUTPUT) {
char topic[16];
if (i < NUMGPIO_RELAY) {
snprintf_P(topic, sizeof(topic), i == 0 ? PSTR("relay/one") : PSTR("relay/two"));
} else {
snprintf_P(topic, sizeof(topic), PSTR("extra/%d"), i - NUMGPIO_RELAY + 1);
}
mqttGPIOCallback(topic, val_str, heishamonSettings.gpioSettings, heishamonSettings.opentherm, heishamonSettings.listenonly);
break;
}
}
client->userdata = NULL;
}
return 0;
} break;
case 150: {
if (Update.isRunning() && (!Update.hasError())) {
if ((strcmp((char *)args->name, "md5") == 0) && (args->len > 0)) {
Expand Down Expand Up @@ -1371,6 +1402,7 @@ int8_t webserver_cb(struct webserver_t *client, void *dat) {
} break;
case 110: {
int ret = saveSettings(client, &heishamonSettings);
setupGPIO(heishamonSettings.gpioSettings, heishamonSettings.opentherm, heishamonSettings.listenonly); //re-apply GPIO pin modes if they changed
#ifdef ESP8266
if ((!heishamonSettings.opentherm) && (heishamonSettings.listenonly)) {
//make sure we disable TX to heatpump-RX using the mosfet so this line is floating and will not disturb cz-taw1
Expand Down Expand Up @@ -1466,6 +1498,16 @@ int8_t webserver_cb(struct webserver_t *client, void *dat) {
case 180: {
if (heishamonSettings.use_1wire) rescanDallasSensors(log_message, heishamonSettings.dallasResolution);
} break;
case 191: {
if (client->content == 0) {
webserver_send(client, 200, (char *)"application/json", 0);
return 0;
} else if (client->content == 1) {
gpioJsonOutput(client, heishamonSettings.gpioSettings, heishamonSettings.opentherm, heishamonSettings.listenonly);
return 0;
}
return -1;
} break;
default: {
webserver_send(client, 301, (char *)"text/plain", 0);
} break;
Expand Down Expand Up @@ -1639,7 +1681,7 @@ void switchSerial() {
proxySerial.flush();
#endif

setupGPIO(heishamonSettings.gpioSettings); //switch extra GPIOs to configured mode
setupGPIO(heishamonSettings.gpioSettings, heishamonSettings.opentherm, heishamonSettings.listenonly); //switch extra GPIOs to configured mode

//mosfet output enable
pinMode(ENABLEPIN, OUTPUT);
Expand Down Expand Up @@ -2015,7 +2057,13 @@ void loop() {
checkBootButton();

//webserver function
// Diagnostic: a slow synchronous HTTP pass delays MQTT, OpenTherm and heat pump processing.
const uint32_t webStart = millis();
webserver_loop();
const uint32_t webElapsed = millis() - webStart;
if (webElapsed >= 100) {
loggingSerial.printf("DIAG WEB loop=%lu ms\n", (unsigned long)webElapsed);
}

// check wifi
check_wifi();
Expand Down Expand Up @@ -2058,6 +2106,8 @@ void loop() {

if (heishamonSettings.use_s0) s0Loop(mqtt_client, log_message, heishamonSettings.mqtt_topic_base, heishamonSettings.s0Settings);

publishGPIOStates(mqtt_client, heishamonSettings.gpioSettings, heishamonSettings.mqtt_topic_base, false, heishamonSettings.opentherm, heishamonSettings.listenonly);

#ifdef ESP8266
//this only runs on ESP8266, the ESP32 does this in vTask
if ((!sending) && (!heishamonSettings.listenonly) && (heishamonSettings.optionalPCB) && ((unsigned long)(millis() - lastOptionalPCBRunTime) > OPTIONALPCBQUERYTIME) ) {
Expand Down
134 changes: 127 additions & 7 deletions HeishaMon/gpio.cpp
Original file line number Diff line number Diff line change
@@ -1,27 +1,147 @@
#include "gpio.h"
#include <PubSubClient.h>
#include "HeishaOT.h"
#include "src/common/progmem.h"
#include "src/common/stricmp.h"
#include "src/common/webserver.h"
const char* mqtt_topic_gpio PROGMEM = "gpio";

void log_message(char* string);

void setupGPIO(gpioSettingsStruct gpioSettings) {
for (int i = 0 ; i < NUMGPIO ; i++) {
static bool toGPIOValue(char* value) {
return (stricmp((char*)"true", value) == 0) || (stricmp((char*)"on", value) == 0) ||
(stricmp((char*)"enable", value) == 0) || (String(value).toInt() == 1);
}

// Opentherm hardwires inOTPin/outOTPin for its own TX/RX (on ESP8266 these are pins 1 and 3,
// which otherwise double as extra GPIOs) - never let the extra-GPIO code touch them while it's enabled.
static bool isOpenthermPin(unsigned int pin, bool openthermEnabled) {
return openthermEnabled && (pin == inOTPin || pin == outOTPin);
}

// On the small (ESP8266) board, listen-only mode skips digitalWrite(ENABLEPIN, HIGH) in switchSerial()
// so the shared level shifter/mosfets that the extra GPIO pins run through are never powered - using
// them would be pointless (or float) in that mode, so block them entirely rather than silently no-op.
bool extraGPIOBlockedByListenOnly(bool listenOnly) {
#if defined(ESP8266)
return listenOnly;
#else
return false;
#endif
}

// A pin is fully unavailable (no pinMode/digitalWrite, no digitalRead, no reporting anywhere) when
// either Opentherm has claimed it, or listen-only mode leaves its level shifter unpowered.
static bool isGpioUnavailable(unsigned int pin, int pinIndex, bool openthermEnabled, bool listenOnly) {
if (isOpenthermPin(pin, openthermEnabled)) return true;
if (pinIndex >= NUMGPIO_RELAY && extraGPIOBlockedByListenOnly(listenOnly)) return true;
return false;
}

void setupGPIO(gpioSettingsStruct &gpioSettings, bool openthermEnabled, bool listenOnly) {
// Apply user-configurable modes to the non-relay GPIO pins (relays occupy the first NUMGPIO_RELAY slots)
for (int i = 0; i < NUMGPIO_USER; i++) {
int pinIdx = NUMGPIO_RELAY + i;
if (isGpioUnavailable(gpioSettings.gpioPin[pinIdx], pinIdx, openthermEnabled, listenOnly)) continue;
switch (gpioSettings.gpioUserMode[i]) {
case GPIO_MODE_INPUT: gpioSettings.gpioMode[pinIdx] = INPUT; break;
case GPIO_MODE_OUTPUT: gpioSettings.gpioMode[pinIdx] = OUTPUT; break;
default: gpioSettings.gpioMode[pinIdx] = INPUT_PULLUP; break;
}
}
for (int i = 0; i < NUMGPIO; i++) {
if (isGpioUnavailable(gpioSettings.gpioPin[i], i, openthermEnabled, listenOnly)) continue;
pinMode(gpioSettings.gpioPin[i], gpioSettings.gpioMode[i]);
}
}

void mqttGPIOCallback(char* topic, char* value) {
void mqttGPIOCallback(char* topic, char* value, gpioSettingsStruct &gpioSettings, bool openthermEnabled, bool listenOnly) {
char log_msg[256];
sprintf_P(log_msg, PSTR("GPIO: MQTT message received on subtopic '%s' value '%s'"), topic, value);
log_message(log_msg);
#ifdef ESP32
if (strcmp_P(topic, PSTR("relay/one")) == 0) {
if (strcmp_P(PSTR("relay/one"), topic) == 0) {
log_message(_F("GPIO: MQTT message received 'relay/one'"));
digitalWrite(relayOnePin,((stricmp((char*)"true", value) == 0) || (stricmp((char*)"on", value) == 0) || (stricmp((char*)"enable", value) == 0)|| (String(value).toInt() == 1 )));
} else if (strcmp_P(topic,PSTR("relay/two")) == 0) {
digitalWrite(relayOnePin, toGPIOValue(value));
return;
} else if (strcmp_P(PSTR("relay/two"), topic) == 0) {
log_message(_F("GPIO: MQTT message received 'relay/two'"));
digitalWrite(relayTwoPin,((stricmp((char*)"true", value) == 0) || (stricmp((char*)"on", value) == 0) || (stricmp((char*)"enable", value) == 0)|| (String(value).toInt() == 1 )));
digitalWrite(relayTwoPin, toGPIOValue(value));
return;
}
#endif
// Handle user-configurable output GPIOs: topic "extra/N" (1-indexed)
if (strncmp_P(topic, PSTR("extra/"), 6) == 0) {
int idx = atoi(topic + 6) - 1; // convert 1-indexed to 0-indexed
if (idx >= 0 && idx < NUMGPIO_USER) {
int pinIdx = NUMGPIO_RELAY + idx;
// Only pins actually in OUTPUT mode are ever candidates for a real write. Pins left in
// INPUT/INPUT_PULLUP (which includes every opentherm/listen-only-blocked pin, since setupGPIO()
// never promotes them to OUTPUT) commonly receive this callback too - not as a write command,
// but as the self-echo of publishGPIOStates() reporting their own read state back over MQTT.
// Checking mode first (before the opentherm/listen-only reasoning) avoids logging a misleading
// "ignoring write" message for what is actually just a harmless state echo, not a blocked switch.
if (gpioSettings.gpioMode[pinIdx] == OUTPUT) {
if (isOpenthermPin(gpioSettings.gpioPin[pinIdx], openthermEnabled)) {
log_message(_F("GPIO: ignoring extra GPIO write, pin reserved by Opentherm"));
return;
}
if (extraGPIOBlockedByListenOnly(listenOnly)) {
log_message(_F("GPIO: ignoring extra GPIO write, listen-only mode leaves the level shifter unpowered"));
return;
}
char log_msg[64];
snprintf_P(log_msg, sizeof(log_msg), PSTR("GPIO: set extra/%d (pin %d) = %s"), idx + 1, gpioSettings.gpioPin[pinIdx], value);
log_message(log_msg);
digitalWrite(gpioSettings.gpioPin[pinIdx], toGPIOValue(value));
}
}
}
}

void publishGPIOStates(PubSubClient &mqtt_client, gpioSettingsStruct &gpioSettings, char* mqtt_topic_base, bool publishAll, bool openthermEnabled, bool listenOnly) {
static int lastState[NUMGPIO];
static bool initialized = false;
if (!initialized) {
for (int i = 0; i < NUMGPIO; i++) lastState[i] = -1;
initialized = true;
}
char topic[256];
char wsmsg[64];
bool mqttConnected = mqtt_client.connected();
for (int i = 0; i < NUMGPIO; i++) {
// Unavailable pins (Opentherm-claimed, or listen-only leaving the level shifter unpowered) are not
// read at all - no digitalRead, no MQTT publish, no websocket push. They're not meaningful telemetry
// (Opentherm's own bit-banging would just get echoed back over MQTT to mqttGPIOCallback) and the
// frontend GPIO tab hides/removes them entirely, so there's nothing to report them to.
if (isGpioUnavailable(gpioSettings.gpioPin[i], i, openthermEnabled, listenOnly)) continue;
int state = digitalRead(gpioSettings.gpioPin[i]);
if (publishAll || state != lastState[i]) {
lastState[i] = state;
if (i >= NUMGPIO_RELAY && mqttConnected) {
snprintf_P(topic, sizeof(topic), PSTR("%s/gpio/extra/%d"), mqtt_topic_base, i - NUMGPIO_RELAY + 1);
mqtt_client.publish(topic, state ? "1" : "0", true);
}
int len = snprintf_P(wsmsg, sizeof(wsmsg), PSTR("{\"data\": {\"gpio\": {\"pin\": %d, \"state\": %d}}}"),
gpioSettings.gpioPin[i], state);
websocket_write_all(wsmsg, len);
}
}
}

void gpioJsonOutput(struct webserver_t *client, gpioSettingsStruct &gpioSettings, bool openthermEnabled, bool listenOnly) {
char buf[64];
webserver_send_content_P(client, PSTR("["), 1);
bool first = true;
for (int i = 0; i < NUMGPIO; i++) {
if (isGpioUnavailable(gpioSettings.gpioPin[i], i, openthermEnabled, listenOnly)) continue;
int state = digitalRead(gpioSettings.gpioPin[i]);
int mode = (i < NUMGPIO_RELAY) ? GPIO_MODE_OUTPUT : gpioSettings.gpioUserMode[i - NUMGPIO_RELAY];
int len = snprintf_P(buf, sizeof(buf), PSTR("%s{\"pin\":%d,\"mode\":%d,\"state\":%d}"),
first ? "" : ",", gpioSettings.gpioPin[i], mode, state);
webserver_send_content(client, buf, len);
first = false;
}
webserver_send_content_P(client, PSTR("]"), 1);
}

24 changes: 20 additions & 4 deletions HeishaMon/gpio.h
Original file line number Diff line number Diff line change
@@ -1,29 +1,45 @@
#if defined(ESP8266)
#define NUMGPIO 3
#define NUMGPIO_USER 3 // all GPIOs are user-configurable on ESP8266
#define NUMGPIO_RELAY 0 // no fixed relays on ESP8266
#include <ESP8266WiFi.h>
#include <ESP8266WiFiGratuitous.h>
#elif defined(ESP32)
#define NUMGPIO 7
#define NUMGPIO_USER 5 // last 5 are user-configurable
#define NUMGPIO_RELAY 2 // first 2 (pins 21,47) are fixed relays
#include <WiFi.h>
#include <ESPmDNS.h>
#include <Update.h>
#define relayOnePin 21
#define relayTwoPin 47
#endif

// User GPIO mode values (stored in settings)
#define GPIO_MODE_INPUT_PULLUP 0
#define GPIO_MODE_INPUT 1
#define GPIO_MODE_OUTPUT 2

extern const char* mqtt_topic_gpio;

struct gpioSettingsStruct {
#if defined(ESP8266)
unsigned int gpioPin[NUMGPIO] = {1, 3, 16};
unsigned int gpioMode[NUMGPIO] = {INPUT_PULLUP, INPUT_PULLUP, INPUT_PULLUP};
#elif defined(ESP32)
unsigned int gpioPin[NUMGPIO] = {33, 34, 35, 36, 37, 21, 47};
unsigned int gpioMode[NUMGPIO] = {INPUT_PULLUP, INPUT_PULLUP, INPUT_PULLUP, INPUT_PULLUP, INPUT_PULLUP, OUTPUT, OUTPUT};
unsigned int gpioPin[NUMGPIO] = {21, 47, 33, 34, 35, 36, 37};
unsigned int gpioMode[NUMGPIO] = {OUTPUT, OUTPUT, INPUT_PULLUP, INPUT_PULLUP, INPUT_PULLUP, INPUT_PULLUP, INPUT_PULLUP};
#endif
// User-configurable modes for the non-relay GPIOs (0=INPUT_PULLUP, 1=INPUT, 2=OUTPUT)
unsigned int gpioUserMode[NUMGPIO_USER] = {}; // all default to 0 (INPUT_PULLUP)
};

void setupGPIO(gpioSettingsStruct gpioSettings);
void mqttGPIOCallback(char* topic, char* value);
class PubSubClient;
struct webserver_t;
void setupGPIO(gpioSettingsStruct &gpioSettings, bool openthermEnabled, bool listenOnly);
void mqttGPIOCallback(char* topic, char* value, gpioSettingsStruct &gpioSettings, bool openthermEnabled, bool listenOnly);
void publishGPIOStates(PubSubClient &mqtt_client, gpioSettingsStruct &gpioSettings, char* mqtt_topic_base, bool publishAll, bool openthermEnabled, bool listenOnly);
void gpioJsonOutput(struct webserver_t *client, gpioSettingsStruct &gpioSettings, bool openthermEnabled, bool listenOnly);
bool extraGPIOBlockedByListenOnly(bool listenOnly); // true when the whole GPIO tab/panel should be hidden (ESP8266 in listen-only mode)


Loading
Loading