Add support for PCF8574 I2C port expander

- for Multi Relay usermod.
This commit is contained in:
Blaz Kristan 2023-05-24 23:40:23 +02:00
parent 56a3bff42e
commit b2df7981a9
2 changed files with 684 additions and 524 deletions

View File

@ -1,6 +1,9 @@
# Multi Relay
This usermod-v2 modification allows the connection of multiple relays, each with individual delay and on/off mode.
Usermod supports PCF8574 I2C port expander to reduce GPIO use.
PCF8574 supports 8 outputs and each output corresponds to a relay in WLED (relay 0 = port 0, etc). I you are using more than 8 relays with multiple PCF8574 make sure their addresses are set conscutively (e.g. 0x20 and 0x21). You can set address of first expander in settings.
(**NOTE:** Will require Wire library and global I2C pins defined.)
## HTTP API
All responses are returned in JSON format.
@ -81,13 +84,15 @@ void registerUsermods()
Usermod can be configured via the Usermods settings page.
* `enabled` - enable/disable usermod
* `use-PCF8574` - use PCF8574 port expander instead of GPIO pins
* `first-PCF8574` - I2C address of first expander (WARNING: enter *decimal* value)
* `broadcast`- time in seconds between MQTT relay-state broadcasts
* `HA-discovery`- enable Home Assistant auto discovery
* `pin` - ESP GPIO pin the relay is connected to (can be configured at compile time `-D MULTI_RELAY_PINS=xx,xx,...`)
* `delay-s` - delay in seconds after on/off command is received
* `active-high` - assign high/low activation of relay (can be used to reverse relay states)
* `external` - if enabled, WLED does not control relay, it can only be triggered by an external command (MQTT, HTTP, JSON or button)
* `button` - button (from LED Settings) that controls this relay
* `broadcast`- time in seconds between MQTT relay-state broadcasts
* `HA-discovery`- enable Home Assistant auto discovery
If there is no MultiRelay section, just save current configuration and re-open Usermods settings page.
@ -100,3 +105,6 @@ Have fun - @blazoncek
2021-11
* Added information about dynamic configuration options
* Added button support.
2023-05
* Added support for PCF8574 I2C port expander (multiple)

View File

@ -4,6 +4,11 @@
#ifndef MULTI_RELAY_MAX_RELAYS
#define MULTI_RELAY_MAX_RELAYS 4
#else
#if MULTI_RELAY_MAX_RELAYS>16
#undef MULTI_RELAY_MAX_RELAYS
#define MULTI_RELAY_MAX_RELAYS 16
#endif
#endif
#ifndef MULTI_RELAY_PINS
@ -15,21 +20,29 @@
#define ON true
#define OFF false
#ifndef PCF8574_ADDRESS
#define PCF8574_ADDRESS 0x20 // some may start at 0x38
#endif
/*
* This usermod handles multiple relay outputs.
* These outputs complement built-in relay output in a way that the activation can be delayed.
* They can also activate/deactivate in reverse logic independently.
*
* Written and maintained by @blazoncek
*/
typedef struct relay_t {
int8_t pin;
bool active;
bool mode;
bool state;
bool external;
struct { // reduces memory footprint
bool active : 1;
bool mode : 1;
bool state : 1;
bool external : 1;
int8_t button : 4;
};
uint16_t delay;
int8_t button;
} Relay;
@ -48,7 +61,8 @@ class MultiRelay : public Usermod {
bool enabled = false; // needs to be configured (no default config)
// status of initialisation
bool initDone = false;
bool usePcf8574 = false;
uint8_t addrPcf8574 = PCF8574_ADDRESS;
bool HAautodiscovery = false;
uint16_t periodicBroadcastSec = 60;
@ -64,8 +78,127 @@ class MultiRelay : public Usermod {
static const char _button[];
static const char _broadcast[];
static const char _HAautodiscovery[];
static const char _pcf8574[];
static const char _pcfAddress[];
void publishMqtt(int relay) {
void handleOffTimer();
void InitHtmlAPIHandle();
int getValue(String data, char separator, int index);
uint8_t getActiveRelayCount();
byte IOexpanderWrite(byte address, byte _data);
byte IOexpanderRead(int address);
void publishMqtt(int relay);
#ifndef WLED_DISABLE_MQTT
void publishHomeAssistantAutodiscovery();
#endif
public:
/**
* constructor
*/
MultiRelay();
/**
* desctructor
*/
~MultiRelay() {}
/**
* Enable/Disable the usermod
*/
inline void enable(bool enable) { enabled = enable; }
/**
* Get usermod enabled/disabled state
*/
inline bool isEnabled() { return enabled; }
/**
* getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!).
* This could be used in the future for the system to determine whether your usermod is installed.
*/
inline uint16_t getId() { return USERMOD_ID_MULTI_RELAY; }
/**
* switch relay on/off
*/
void switchRelay(uint8_t relay, bool mode);
/**
* toggle relay
*/
inline void toggleRelay(uint8_t relay) {
switchRelay(relay, !_relay[relay].state);
}
/**
* setup() is called once at boot. WiFi is not yet connected at this point.
* You can use it to initialize variables, sensors or similar.
*/
void setup();
/**
* connected() is called every time the WiFi is (re)connected
* Use it to initialize network interfaces
*/
inline void connected() { InitHtmlAPIHandle(); }
/**
* loop() is called continuously. Here you can check for events, read sensors, etc.
*/
void loop();
#ifndef WLED_DISABLE_MQTT
bool onMqttMessage(char* topic, char* payload);
void onMqttConnect(bool sessionPresent);
#endif
/**
* handleButton() can be used to override default button behaviour. Returning true
* will prevent button working in a default way.
* Replicating button.cpp
*/
bool handleButton(uint8_t b);
/**
* addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API.
*/
void addToJsonInfo(JsonObject &root);
/**
* addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object).
* Values in the state object may be modified by connected clients
*/
void addToJsonState(JsonObject &root);
/**
* readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object).
* Values in the state object may be modified by connected clients
*/
void readFromJsonState(JsonObject &root);
/**
* provide the changeable values
*/
void addToConfig(JsonObject &root);
void appendConfigData();
/**
* restore the changeable values
* readFromConfig() is called before setup() to populate properties from values stored in cfg.json
*
* The function should return true if configuration was successfully loaded or false if there was no configuration.
*/
bool readFromConfig(JsonObject &root);
};
// class implementetion
void MultiRelay::publishMqtt(int relay) {
#ifndef WLED_DISABLE_MQTT
//Check if MQTT Connected, otherwise it will crash the 8266
if (WLED_MQTT_CONNECTED){
@ -74,12 +207,12 @@ class MultiRelay : public Usermod {
mqtt->publish(subuf, 0, false, _relay[relay].state ? "on" : "off");
}
#endif
}
}
/**
/**
* switch off the strip if the delay has elapsed
*/
void handleOffTimer() {
void MultiRelay::handleOffTimer() {
unsigned long now = millis();
bool activeRelays = false;
for (int i=0; i<MULTI_RELAY_MAX_RELAYS; i++) {
@ -93,15 +226,15 @@ class MultiRelay : public Usermod {
}
if (!activeRelays) _switchTimerStart = 0;
if (periodicBroadcastSec && now - lastBroadcast > (periodicBroadcastSec*1000)) lastBroadcast = now;
}
}
/**
/**
* HTTP API handler
* borrowed from:
* https://github.com/gsieben/WLED/blob/master/usermods/GeoGab-Relays/usermod_GeoGab.h
*/
#define GEOGABVERSION "0.1.3"
void InitHtmlAPIHandle() { // https://github.com/me-no-dev/ESPAsyncWebServer
#define GEOGABVERSION "0.1.3"
void MultiRelay::InitHtmlAPIHandle() { // https://github.com/me-no-dev/ESPAsyncWebServer
DEBUG_PRINTLN(F("Relays: Initialize HTML API"));
server.on("/relays", HTTP_GET, [this](AsyncWebServerRequest *request) {
@ -161,9 +294,9 @@ class MultiRelay : public Usermod {
janswer += F("\"}");
request->send(200, "application/json", janswer);
});
}
}
int getValue(String data, char separator, int index) {
int MultiRelay::getValue(String data, char separator, int index) {
int found = 0;
int strIndex[] = {0, -1};
int maxIndex = data.length()-1;
@ -176,13 +309,29 @@ class MultiRelay : public Usermod {
}
}
return found>index ? data.substring(strIndex[0], strIndex[1]).toInt() : -1;
}
}
public:
/**
* constructor
*/
MultiRelay() {
//Write a byte to the IO expander
byte MultiRelay::IOexpanderWrite(byte address, byte _data ) {
Wire.beginTransmission(addrPcf8574 + address);
Wire.write(_data);
return Wire.endTransmission();
}
//Read a byte from the IO expander
byte MultiRelay::IOexpanderRead(int address) {
byte _data = 0;
Wire.requestFrom(addrPcf8574 + address, 1);
if (Wire.available()) {
_data = Wire.read();
}
return _data;
}
// public methods
MultiRelay::MultiRelay() {
const int8_t defPins[] = {MULTI_RELAY_PINS};
for (size_t i=0; i<MULTI_RELAY_MAX_RELAYS; i++) {
_relay[i].pin = i<sizeof(defPins) ? defPins[i] : -1;
@ -193,54 +342,45 @@ class MultiRelay : public Usermod {
_relay[i].external = false;
_relay[i].button = -1;
}
}
/**
* desctructor
*/
~MultiRelay() {}
}
/**
* Enable/Disable the usermod
*/
inline void enable(bool enable) { enabled = enable; }
/**
* Get usermod enabled/disabled state
*/
inline bool isEnabled() { return enabled; }
/**
/**
* switch relay on/off
*/
void switchRelay(uint8_t relay, bool mode) {
if (relay>=MULTI_RELAY_MAX_RELAYS || _relay[relay].pin<0) return;
void MultiRelay::switchRelay(uint8_t relay, bool mode) {
if (relay>=MULTI_RELAY_MAX_RELAYS || (_relay[relay].pin<0 && !usePcf8574)) return;
_relay[relay].state = mode;
if (usePcf8574) {
byte expander = relay/8;
uint16_t state = 0;
for (int i=0; i<MULTI_RELAY_MAX_RELAYS; i++) state |= (uint16_t)_relay[i].state << i; // fill relay states for all pins
state = (mode ? !_relay[relay].mode : _relay[relay].mode) ? state | (1<<relay) : state & ~(1<<relay); // take into account invert mode
IOexpanderWrite(expander, state>>(8*expander));
DEBUG_PRINT(F("PCF8574 Writing to ")); DEBUG_PRINT(addrPcf8574 + expander); DEBUG_PRINT(F(" with data ")); DEBUG_PRINTLN(state>>(8*expander));
} else {
pinMode(_relay[relay].pin, OUTPUT);
digitalWrite(_relay[relay].pin, mode ? !_relay[relay].mode : _relay[relay].mode);
}
publishMqtt(relay);
}
}
/**
* toggle relay
*/
inline void toggleRelay(uint8_t relay) {
switchRelay(relay, !_relay[relay].state);
}
uint8_t getActiveRelayCount() {
uint8_t MultiRelay::getActiveRelayCount() {
uint8_t count = 0;
if (usePcf8574) return MULTI_RELAY_MAX_RELAYS; // we don't know how many there are
for (int i=0; i<MULTI_RELAY_MAX_RELAYS; i++) if (_relay[i].pin>=0) count++;
return count;
}
}
//Functions called by WLED
//Functions called by WLED
#ifndef WLED_DISABLE_MQTT
/**
/**
* handling of MQTT message
* topic only contains stripped topic (part after /wled/MAC)
* topic should look like: /relay/X/command; where X is relay number, 0 based
*/
bool onMqttMessage(char* topic, char* payload) {
bool MultiRelay::onMqttMessage(char* topic, char* payload) {
if (strlen(topic) > 8 && strncmp_P(topic, PSTR("/relay/"), 7) == 0 && strncmp_P(topic+8, PSTR("/command"), 8) == 0) {
uint8_t relay = strtoul(topic+7, NULL, 10);
if (relay<MULTI_RELAY_MAX_RELAYS) {
@ -258,12 +398,12 @@ class MultiRelay : public Usermod {
}
}
return false;
}
}
/**
/**
* subscribe to MQTT topic for controlling relays
*/
void onMqttConnect(bool sessionPresent) {
void MultiRelay::onMqttConnect(bool sessionPresent) {
//(re)subscribe to required topics
char subuf[64];
if (mqttDeviceTopic[0] != 0) {
@ -276,9 +416,9 @@ class MultiRelay : public Usermod {
publishMqtt(i); //publish current state
}
}
}
}
void publishHomeAssistantAutodiscovery() {
void MultiRelay::publishHomeAssistantAutodiscovery() {
for (int i = 0; i < MULTI_RELAY_MAX_RELAYS; i++) {
char uid[24], json_str[1024], buf[128];
size_t payload_size;
@ -315,15 +455,28 @@ class MultiRelay : public Usermod {
sprintf_P(buf, PSTR("homeassistant/switch/%s/config"), uid);
mqtt->publish(buf, 0, true, json_str, payload_size);
}
}
}
#endif
/**
/**
* setup() is called once at boot. WiFi is not yet connected at this point.
* You can use it to initialize variables, sensors or similar.
*/
void setup() {
void MultiRelay::setup() {
// pins retrieved from cfg.json (readFromConfig()) prior to running setup()
// if we want PCF8574 expander I2C pins need to be valid
if (i2c_sda == i2c_scl && i2c_sda == -1) usePcf8574 = false;
if (usePcf8574) {
uint16_t state = 0;
for (int i=0; i<MULTI_RELAY_MAX_RELAYS; i++) state |= (uint16_t)(_relay[i].external ? (_relay[i].mode ? !_relay[i].state : _relay[i].state) : (_relay[i].mode ? !offMode : offMode)) << i; // fill relay states for all pins
for (int i=0; i<MULTI_RELAY_MAX_RELAYS; i += 8) {
byte expander = i/8;
IOexpanderWrite(expander, state>>(8*expander)); // init expander (set all outputs)
delay(1);
}
DEBUG_PRINTLN(F("PCF8574(s) inited."));
} else {
for (int i=0; i<MULTI_RELAY_MAX_RELAYS; i++) {
if (_relay[i].pin<0) continue;
if (!pinManager.allocatePin(_relay[i].pin,true, PinOwner::UM_MultiRelay)) {
@ -334,22 +487,15 @@ class MultiRelay : public Usermod {
_relay[i].active = false;
}
}
}
_oldMode = offMode;
initDone = true;
}
}
/**
* connected() is called every time the WiFi is (re)connected
* Use it to initialize network interfaces
*/
void connected() {
InitHtmlAPIHandle();
}
/**
/**
* loop() is called continuously. Here you can check for events, read sensors, etc.
*/
void loop() {
void MultiRelay::loop() {
yield();
if (!enabled || strip.isUpdating()) return;
@ -362,19 +508,19 @@ class MultiRelay : public Usermod {
_oldMode = offMode;
_switchTimerStart = millis();
for (int i=0; i<MULTI_RELAY_MAX_RELAYS; i++) {
if (_relay[i].pin>=0 && !_relay[i].external) _relay[i].active = true;
if ((_relay[i].pin>=0 || usePcf8574) && !_relay[i].external) _relay[i].active = true;
}
}
handleOffTimer();
}
}
/**
/**
* handleButton() can be used to override default button behaviour. Returning true
* will prevent button working in a default way.
* Replicating button.cpp
*/
bool handleButton(uint8_t b) {
bool MultiRelay::handleButton(uint8_t b) {
yield();
if (!enabled
|| buttonType[b] == BTN_TYPE_NONE
@ -461,12 +607,12 @@ class MultiRelay : public Usermod {
}
}
return handled;
}
}
/**
/**
* addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API.
*/
void addToJsonInfo(JsonObject &root) {
void MultiRelay::addToJsonInfo(JsonObject &root) {
if (enabled) {
JsonObject user = root["u"];
if (user.isNull())
@ -478,7 +624,7 @@ class MultiRelay : public Usermod {
String uiDomString;
for (int i=0; i<MULTI_RELAY_MAX_RELAYS; i++) {
if (_relay[i].pin<0 || !_relay[i].external) continue;
if ((_relay[i].pin<0 && !usePcf8574) || !_relay[i].external) continue;
uiDomString = F("Relay "); uiDomString += i;
JsonArray infoArr = user.createNestedArray(uiDomString); // timer value
@ -497,13 +643,13 @@ class MultiRelay : public Usermod {
infoArr.add(uiDomString);
}
}
}
}
/**
/**
* addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object).
* Values in the state object may be modified by connected clients
*/
void addToJsonState(JsonObject &root) {
void MultiRelay::addToJsonState(JsonObject &root) {
if (!initDone || !enabled) return; // prevent crash on boot applyPreset()
JsonObject multiRelay = root[FPSTR(_name)];
if (multiRelay.isNull()) {
@ -521,13 +667,13 @@ class MultiRelay : public Usermod {
multiRelay[FPSTR(_relay_str)] = 0;
multiRelay[F("state")] = _relay[0].state;
#endif
}
}
/**
/**
* readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object).
* Values in the state object may be modified by connected clients
*/
void readFromJsonState(JsonObject &root) {
void MultiRelay::readFromJsonState(JsonObject &root) {
if (!initDone || !enabled) return; // prevent crash on boot applyPreset()
JsonObject usermod = root[FPSTR(_name)];
if (!usermod.isNull()) {
@ -552,16 +698,19 @@ class MultiRelay : public Usermod {
}
}
}
}
}
/**
/**
* provide the changeable values
*/
void addToConfig(JsonObject &root) {
void MultiRelay::addToConfig(JsonObject &root) {
JsonObject top = root.createNestedObject(FPSTR(_name));
top[FPSTR(_enabled)] = enabled;
top[FPSTR(_pcf8574)] = usePcf8574;
top[FPSTR(_pcfAddress)] = addrPcf8574;
top[FPSTR(_broadcast)] = periodicBroadcastSec;
top[FPSTR(_HAautodiscovery)] = HAautodiscovery;
for (int i=0; i<MULTI_RELAY_MAX_RELAYS; i++) {
String parName = FPSTR(_relay_str); parName += '-'; parName += i;
JsonObject relay = top.createNestedObject(parName);
@ -571,17 +720,22 @@ class MultiRelay : public Usermod {
relay[FPSTR(_external)] = _relay[i].external;
relay[FPSTR(_button)] = _relay[i].button;
}
top[FPSTR(_HAautodiscovery)] = HAautodiscovery;
DEBUG_PRINTLN(F("MultiRelay config saved."));
}
}
/**
void MultiRelay::appendConfigData() {
oappend(SET_F("addInfo('MultiRelay:first-PCF8574',1,'<i>(not hex!)</i>','address');"));
oappend(SET_F("addInfo('MultiRelay:broadcast-sec',1,'(MQTT message)');"));
oappend(SET_F("addInfo('MultiRelay:relay-0:pin',1,'(use -1 for PCF8574)');"));
}
/**
* restore the changeable values
* readFromConfig() is called before setup() to populate properties from values stored in cfg.json
*
* The function should return true if configuration was successfully loaded or false if there was no configuration.
*/
bool readFromConfig(JsonObject &root) {
bool MultiRelay::readFromConfig(JsonObject &root) {
int8_t oldPin[MULTI_RELAY_MAX_RELAYS];
JsonObject top = root[FPSTR(_name)];
@ -591,7 +745,13 @@ class MultiRelay : public Usermod {
return false;
}
//bool configComplete = !top.isNull();
//configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled);
enabled = top[FPSTR(_enabled)] | enabled;
usePcf8574 = top[FPSTR(_pcf8574)] | usePcf8574;
addrPcf8574 = top[FPSTR(_pcfAddress)] | addrPcf8574;
// if I2C is not globally initialised just ignore
if (i2c_sda == i2c_scl && i2c_sda == -1) usePcf8574 = false;
periodicBroadcastSec = top[FPSTR(_broadcast)] | periodicBroadcastSec;
periodicBroadcastSec = min(900,max(0,(int)periodicBroadcastSec));
HAautodiscovery = top[FPSTR(_HAautodiscovery)] | HAautodiscovery;
@ -640,18 +800,8 @@ class MultiRelay : public Usermod {
DEBUG_PRINTLN(F(" config (re)loaded."));
}
// use "return !top["newestParameter"].isNull();" when updating Usermod with new features
return !top[FPSTR(_HAautodiscovery)].isNull();
}
/**
* getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!).
* This could be used in the future for the system to determine whether your usermod is installed.
*/
uint16_t getId()
{
return USERMOD_ID_MULTI_RELAY;
}
};
return !top[FPSTR(_pcf8574)].isNull();
}
// strings to reduce flash memory usage (used more than twice)
const char MultiRelay::_name[] PROGMEM = "MultiRelay";
@ -663,3 +813,5 @@ const char MultiRelay::_external[] PROGMEM = "external";
const char MultiRelay::_button[] PROGMEM = "button";
const char MultiRelay::_broadcast[] PROGMEM = "broadcast-sec";
const char MultiRelay::_HAautodiscovery[] PROGMEM = "HA-autodiscovery";
const char MultiRelay::_pcf8574[] PROGMEM = "use-PCF8574";
const char MultiRelay::_pcfAddress[] PROGMEM = "first-PCF8574";