diff --git a/CHANGELOG.md b/CHANGELOG.md index 183c01d24..0d9bd1601 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. ## [12.4.0.2] ### Added - Support for multiple MCP23008 as switch/button/relay +- Support for multiple PCF8574 as switch/button/relay ### Breaking Changed - Shelly Pro 4PM using standard MCP23xxx driver and needs one time Auto-Configuration diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c8c2e175b..79bd9a651 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -113,6 +113,7 @@ The latter links can be used for OTA upgrades too like ``OtaUrl https://ota.tasm ## Changelog v12.4.0.2 ### Added - Support for multiple MCP23008/MCP23017/MCP23S17 as switch/button/relay +- Support for multiple PCF8574 as switch/button/relay - NTP time request from gateway [#17984](https://github.com/arendst/Tasmota/issues/17984) ### Breaking Changed diff --git a/tasmota/tasmota_xdrv_driver/xdrv_28_pcf8574.ino b/tasmota/tasmota_xdrv_driver/xdrv_28_pcf8574.ino index a889fe3aa..f07355b1b 100644 --- a/tasmota/tasmota_xdrv_driver/xdrv_28_pcf8574.ino +++ b/tasmota/tasmota_xdrv_driver/xdrv_28_pcf8574.ino @@ -1,7 +1,7 @@ /* xdrv_28_pcf8574.ino - PCF8574 I2C support for Tasmota - Copyright (C) 2021 Stefan Bode + Copyright (C) 2021 Stefan Bode and Theo Arends This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -24,20 +24,60 @@ * * I2C Address: PCF8574 = 0x20 .. 0x27 (0x27 is not supported), * PCF8574A = 0x39 .. 0x3F (0x38 is not supported) + * + * Start address and count can be overriden in user_config_override.h to allow better + * sharing of the I2C address space. Still the covered range must remains valid. + * A count of 0 can be used totaly disable any of the 2 ranges. + * By default, the following addresses are explicitly excluded (as per the docs) : + * - 0x27 and 0x37 are reserved for USE_DISPLAY_LCD in xdsp_01_lcd.ino + * - 0X38 is reserved for other sensors + * If the respective drivers are not used, overrides allows to recover those addresses + * If defined, USE_MCP230xx_ADDR is also always excluded + * + * Mode 1: + * See documentation. + * + * Mode 2: + * Allows easy configuration by using a sequential list of pins configured as Tasmota + * template and handle any input and output as configured GPIOs. + * + * Restrictions: + * - Uses incremental I2C addresses until template pin count reached + * - Max support for 28 switches (input), 32 buttons (input), 32 relays (output) + * + * Supported template fields: + * NAME - Template name + * GPIO - Sequential list of pin 1 and up with configured GPIO function + * Function Code Description + * ------------------- -------- ---------------------------------------- + * None 0 Not used + * Button1..32 B 32..63 Button to Gnd with internal pullup + * Button_n1..32 Bn 64..95 Button to Gnd without internal pullup + * Button_i1..32 Bi 96..127 Button inverted to Vcc with internal pullup + * Button_in1..32 Bin 128..159 Button inverted to Vcc without internal pullup + * Switch1..28 S 160..187 Switch to Gnd with internal pullup + * Switch_n1..28 Sn 192..219 Switch to Gnd without internal pullup + * Relay1..28 R 224..255 Relay + * Relay_i1..28 Ri 256..287 Relay inverted + * Output_Hi Oh 3840 Fixed output high + * Output_lo Ol 3872 Fixed output low + * + * Prepare a template to be loaded either by: + * - a rule like: rule3 on file#pcf8574.dat do {"NAME":"PCF8574 A=Ri8-1, B=B1-8","GPIO":[263,262,261,260,259,258,257,256,32,33,34,35,36,37,38,39]} endon + * - a script like: -y{"NAME":"PCF8574 A=Ri8-1, B=B1-8","GPIO":[263,262,261,260,259,258,257,256,32,33,34,35,36,37,38,39]} + * - file called pcf8574.dat with contents: {"NAME":"PCF8574 A=Ri8-1, B=B1-8","GPIO":[263,262,261,260,259,258,257,256,32,33,34,35,36,37,38,39]} + * + * Inverted relays and buttons Ri8 Ri7 Ri6 Ri5 Ri4 Ri3 Ri2 Ri1 B1 B2 B3 B4 B5 B6 B7 B8 + * {"NAME":"PCF8574 A=Ri8-1, B=B1-8","GPIO":[263,262,261,260,259,258,257,256,32,33,34,35,36,37,38,39]} + * B1 B2 B3 B4 Ri4 Ri3 Ri2 Ri1 B5 B6 B7 B8 Ri8 Ri7 Ri6 Ri5 + * {"NAME":"PCF8574 A=B1-4,Ri4-1, B=B5-8,Ri8-5","GPIO":[32,33,34,35,259,258,257,256,36,37,38,39,263,262,261,260]} + * B1 B2 B3 B4 Ri4 Ri3 Ri2 Ri1 B5 B6 B7 B8 Ri8 Ri7 Ri6 Ri5 + * {"NAME":"PCF8574 A=Bi1-4,Ri4-1, B=Bi5-8,Ri8-5","GPIO":[64,65,66,67,259,258,257,256,68,69,70,71,263,262,261,260]} \*********************************************************************************************/ #define XDRV_28 28 #define XI2C_02 2 // See I2CDEVICES.md -// Start address and count can be overriden in user_config_override.h to allow better -// sharing of the I2C address space. Still the covered range must remains valid. -// A count of 0 can be used totaly disable any of the 2 ranges. -// By default, the following addresses are explicitly excluded (as per the docs) : -// - 0x27 and 0x37 are reserved for USE_DISPLAY_LCD in xdsp_01_lcd.ino -// - 0X38 is reserved for other sensors -// If the respective drivers are not used, overrides allows to recover those addresses -// If defined, USE_MCP230xx_ADDR is also always excluded - // PCF8574 address range from 0x20 to 0x26 #ifndef PCF8574_ADDR1 #define PCF8574_ADDR1 0x20 // PCF8574 @@ -53,6 +93,12 @@ #define PCF8574_ADDR2_COUNT 6 #endif +#define PCF8574_MAX_PINS 64 + +/*********************************************************************************************\ + * PCF8574 support +\*********************************************************************************************/ + // Consitency tests - Checked across the complete range for the PCF8574/PCF8574A to allow override #if (PCF8574_ADDR1 < 0x20) || ((PCF8574_ADDR1 + PCF8574_ADDR1_COUNT - 1) > 0x27) #error PCF8574_ADDR1 and/or PCF8574_ADDR1_COUNT badly overriden. Fix your user_config_override @@ -63,7 +109,9 @@ struct PCF8574 { int error; - uint8_t pin[64]; + uint32_t relay_inverted; + uint32_t button_inverted; + uint16_t pin[PCF8574_MAX_PINS]; uint8_t address[MAX_PCF8574]; uint8_t pin_mask[MAX_PCF8574] = { 0 }; #ifdef USE_PCF8574_MQTTINPUT @@ -71,25 +119,308 @@ struct PCF8574 { #endif uint8_t max_connected_ports = 0; // Max numbers of devices comming from PCF8574 modules uint8_t max_devices = 0; // Max numbers of PCF8574 modules - char stype[9]; - bool type = false; + uint8_t mode; + uint8_t chip; + uint8_t relay_max; + uint8_t relay_offset; + uint8_t button_max; + uint8_t switch_max; + int8_t button_offset; + int8_t switch_offset; + bool interrupt; } Pcf8574; -uint8_t Pcf8574Read(uint8_t idx) -{ +/*********************************************************************************************\ + * PCF8574 - I2C +\*********************************************************************************************/ + +uint8_t Pcf8574Read(uint8_t idx) { Wire.requestFrom(Pcf8574.address[idx],(uint8_t)1); return Wire.read(); } -uint8_t Pcf8574Write(uint8_t idx) -{ +uint8_t Pcf8574Write(uint8_t idx) { Wire.beginTransmission(Pcf8574.address[idx]); Wire.write(Pcf8574.pin_mask[idx]); return Wire.endTransmission(); } -void Pcf8574SwitchRelay(void) -{ +/*********************************************************************************************/ + +uint8_t Pcf8574ReadByte(void) { + Wire.requestFrom(Pcf8574.address[Pcf8574.chip], (uint8_t)1); + return Wire.read(); +} + +uint8_t Pcf8574WriteByte(uint8_t value) { + Wire.beginTransmission(Pcf8574.address[Pcf8574.chip]); + Wire.write(value); + return Wire.endTransmission(); +} + +bool Pcf8574DigitalRead(uint8_t pin) { + // pin 0 - 63 + Pcf8574.chip = pin / 8; + uint8_t bit = pin % 8; + uint8_t value = Pcf8574ReadByte(); + return value & (1 << bit); +} + +void Pcf8574DigitalWrite(uint8_t pin, bool pin_value) { + // pin 0 - 63 + // INPUT or INPUT_PULLUP = Pcf8574DigitalWrite(pin, 1); + // OUTPUT = Pcf8574DigitalWrite(pin, 0); or Pcf8574DigitalWrite(pin, 1); + Pcf8574.chip = pin / 8; + uint8_t bit = pin % 8; + uint8_t reg_value = Pcf8574ReadByte(); + if (pin_value) { + reg_value |= 1 << bit; + } else { + reg_value &= ~(1 << bit); + } + Pcf8574WriteByte(reg_value); +} + +/*********************************************************************************************\ + * PCF8574 Mode 2 - Theo Arends +\*********************************************************************************************/ + +int Pcf8574Pin(uint32_t gpio, uint32_t index = 0); +int Pcf8574Pin(uint32_t gpio, uint32_t index) { + uint16_t real_gpio = gpio << 5; + uint16_t mask = 0xFFE0; + if (index < GPIO_ANY) { + real_gpio += index; + mask = 0xFFFF; + } + for (uint32_t i = 0; i < Pcf8574.max_connected_ports; i++) { + if ((Pcf8574.pin[i] & mask) == real_gpio) { + return i; // Pin number configured for gpio + } + } + return -1; // No pin used for gpio +} + +bool Pcf8574PinUsed(uint32_t gpio, uint32_t index = 0); +bool Pcf8574PinUsed(uint32_t gpio, uint32_t index) { + return (Pcf8574Pin(gpio, index) >= 0); +} + +uint32_t Pcf8574GetPin(uint32_t lpin) { + if (lpin < Pcf8574.max_connected_ports) { + return Pcf8574.pin[lpin]; + } else { + return GPIO_NONE; + } +} + +/*********************************************************************************************/ + +String Pcf8574TemplateLoadFile(void) { + String pcftmplt = ""; +#ifdef USE_UFILESYS + pcftmplt = TfsLoadString("/pcf8574.dat"); +#endif // USE_UFILESYS +#ifdef USE_RULES + if (!pcftmplt.length()) { + pcftmplt = RuleLoadFile("PCF8574.DAT"); + } +#endif // USE_RULES +#ifdef USE_SCRIPT + if (!pcftmplt.length()) { + pcftmplt = ScriptLoadSection(">y"); + } +#endif // USE_SCRIPT + return pcftmplt; +} + +bool Pcf8574LoadTemplate(void) { + String pcftmplt = Pcf8574TemplateLoadFile(); + uint32_t len = pcftmplt.length() +1; + if (len < 7) { return false; } // No PcfTmplt found + + JsonParser parser((char*)pcftmplt.c_str()); + JsonParserObject root = parser.getRootObject(); + if (!root) { return false; } + + // rule3 on file#pcf8574.dat do {"NAME":"PCF8574 A=B1-4,Ri4-1, B=B5-8,Ri8-5","GPIO":[32,33,34,35,259,258,257,256,36,37,38,39,263,262,261,260]} endon + // rule3 on file#pcf8574.dat do {"NAME":"PCF8574 A=Bi1-4,Ri4-1, B=Bi5-8,Ri8-5","GPIO":[64,65,66,67,259,258,257,256,68,69,70,71,263,262,261,260]} endon + JsonParserToken val = root[PSTR(D_JSON_NAME)]; + if (val) { + AddLog(LOG_LEVEL_DEBUG, PSTR("PCF: Template %s"), val.getStr()); + } + JsonParserArray arr = root[PSTR(D_JSON_GPIO)]; + if (arr) { + uint32_t pin = 0; + for (pin; pin < Pcf8574.max_connected_ports; pin++) { // Max number of detected chip pins + JsonParserToken val = arr[pin]; + if (!val) { break; } + uint16_t mpin = val.getUInt(); + if (mpin) { // Above GPIO_NONE + if ((mpin >= AGPIO(GPIO_SWT1)) && (mpin < (AGPIO(GPIO_SWT1) + MAX_SWITCHES_SET))) { + Pcf8574.switch_max++; + Pcf8574DigitalWrite(pin, 1); // INPUT_PULLUP + } + else if ((mpin >= AGPIO(GPIO_SWT1_NP)) && (mpin < (AGPIO(GPIO_SWT1_NP) + MAX_SWITCHES_SET))) { + mpin -= (AGPIO(GPIO_SWT1_NP) - AGPIO(GPIO_SWT1)); + Pcf8574.switch_max++; + Pcf8574DigitalWrite(pin, 1); // INPUT + } + else if ((mpin >= AGPIO(GPIO_KEY1)) && (mpin < (AGPIO(GPIO_KEY1) + MAX_KEYS_SET))) { + Pcf8574.button_max++; + Pcf8574DigitalWrite(pin, 1); // INPUT_PULLUP + } + else if ((mpin >= AGPIO(GPIO_KEY1_NP)) && (mpin < (AGPIO(GPIO_KEY1_NP) + MAX_KEYS_SET))) { + mpin -= (AGPIO(GPIO_KEY1_NP) - AGPIO(GPIO_KEY1)); + Pcf8574.button_max++; + Pcf8574DigitalWrite(pin, 1); // INPUT + } + else if ((mpin >= AGPIO(GPIO_KEY1_INV)) && (mpin < (AGPIO(GPIO_KEY1_INV) + MAX_KEYS_SET))) { + bitSet(Pcf8574.button_inverted, mpin - AGPIO(GPIO_KEY1_INV)); + mpin -= (AGPIO(GPIO_KEY1_INV) - AGPIO(GPIO_KEY1)); + Pcf8574.button_max++; + Pcf8574DigitalWrite(pin, 1); // INPUT_PULLUP + } + else if ((mpin >= AGPIO(GPIO_KEY1_INV_NP)) && (mpin < (AGPIO(GPIO_KEY1_INV_NP) + MAX_KEYS_SET))) { + bitSet(Pcf8574.button_inverted, mpin - AGPIO(GPIO_KEY1_INV_NP)); + mpin -= (AGPIO(GPIO_KEY1_INV_NP) - AGPIO(GPIO_KEY1)); + Pcf8574.button_max++; + Pcf8574DigitalWrite(pin, 1); // INPUT + } + else if ((mpin >= AGPIO(GPIO_REL1)) && (mpin < (AGPIO(GPIO_REL1) + MAX_RELAYS_SET))) { + Pcf8574.relay_max++; + Pcf8574DigitalWrite(pin, 1); // OUTPUT + } + else if ((mpin >= AGPIO(GPIO_REL1_INV)) && (mpin < (AGPIO(GPIO_REL1_INV) + MAX_RELAYS_SET))) { + bitSet(Pcf8574.relay_inverted, mpin - AGPIO(GPIO_REL1_INV)); + mpin -= (AGPIO(GPIO_REL1_INV) - AGPIO(GPIO_REL1)); + Pcf8574.relay_max++; + Pcf8574DigitalWrite(pin, 0); // OUTPUT + } + else if (mpin == AGPIO(GPIO_OUTPUT_HI)) { + Pcf8574DigitalWrite(pin, 1); // OUTPUT + } + else if (mpin == AGPIO(GPIO_OUTPUT_LO)) { + Pcf8574DigitalWrite(pin, 0); // OUTPUT + } + else { mpin = 0; } + Pcf8574.pin[pin] = mpin; + } + if ((Pcf8574.switch_max >= MAX_SWITCHES_SET) || + (Pcf8574.button_max >= MAX_KEYS_SET) || + (Pcf8574.relay_max >= MAX_RELAYS_SET)) { + AddLog(LOG_LEVEL_INFO, PSTR("PCF: Max reached (S%d/B%d/R%d)"), Pcf8574.switch_max, Pcf8574.button_max, Pcf8574.relay_max); + break; + } + } + Pcf8574.max_connected_ports = pin; // Max number of configured pins + } + +// AddLog(LOG_LEVEL_DEBUG, PSTR("PCF: Pins %d, pin %*_V"), Pcf8574.max_connected_ports, Pcf8574.max_connected_ports, (uint8_t*)Pcf8574.pin); + + return true; +} + +uint32_t Pcf8574TemplateGpio(void) { + String pcftmplt = Pcf8574TemplateLoadFile(); + uint32_t len = pcftmplt.length() +1; + if (len < 7) { return 0; } // No PcfTmplt found + + JsonParser parser((char*)pcftmplt.c_str()); + JsonParserObject root = parser.getRootObject(); + if (!root) { return 0; } + + JsonParserArray arr = root[PSTR(D_JSON_GPIO)]; + if (arr.isArray()) { + return arr.size(); // Number of requested pins + } + return 0; +} + +void Pcf8574ServiceInput(void) { + Pcf8574.interrupt = false; + // This works with no interrupt + uint32_t pin_offset = 0; + uint32_t gpio; + for (Pcf8574.chip = 0; Pcf8574.chip < Pcf8574.max_devices; Pcf8574.chip++) { + gpio = Pcf8574ReadByte(); + +// AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("PCF: Chip %d, State %02X"), Pcf8574.chip, gpio); + + uint32_t mask = 1; + for (uint32_t pin = 0; pin < 8; pin++) { + uint32_t state = ((gpio & mask) != 0); + uint32_t lpin = Pcf8574GetPin(pin_offset + pin); // 0 for None, 32 for KEY1, 160 for SWT1, 224 for REL1 + uint32_t index = lpin & 0x001F; // Max 32 buttons or switches + lpin = BGPIO(lpin); // UserSelectablePins number + if (GPIO_KEY1 == lpin) { + ButtonSetVirtualPinState(Pcf8574.button_offset + index, (state != bitRead(Pcf8574.button_inverted, index))); + Pcf8574DigitalWrite(pin_offset + pin, 1); // INPUT and reset interrupt + } + else if (GPIO_SWT1 == lpin) { + SwitchSetVirtualPinState(Pcf8574.switch_offset + index, state); + Pcf8574DigitalWrite(pin_offset + pin, 1); // INPUT and reset interrupt + } + mask <<= 1; + } + pin_offset += 8; + } +} + +void IRAM_ATTR Pcf8574InputIsr(void) { + Pcf8574.interrupt = true; +} + +void Pcf8574Init(void) { + if (Pcf8574.button_max || Pcf8574.switch_max) { + if (PinUsed(GPIO_PCF8574_INT)) { + pinMode(Pin(GPIO_PCF8574_INT), INPUT_PULLUP); + attachInterrupt(Pin(GPIO_PCF8574_INT), Pcf8574InputIsr, CHANGE); + } + } +} + +void Pcf8574Power(void) { + power_t rpower = XdrvMailbox.index >> Pcf8574.relay_offset; + for (uint32_t index = 0; index < Pcf8574.relay_max; index++) { + power_t state = rpower &1; + if (Pcf8574PinUsed(GPIO_REL1, index)) { + uint32_t pin = Pcf8574Pin(GPIO_REL1, index) & 0x3F; // Fix possible overflow over 63 gpios + +// AddLog(LOG_LEVEL_DEBUG, PSTR("PCF: Power pin %d, state %d(%d)"), pin, state, bitRead(Pcf8574.relay_inverted, index)); + + Pcf8574DigitalWrite(pin, bitRead(Pcf8574.relay_inverted, index) ? !state : state); + } + rpower >>= 1; // Select next power + } +} + +bool Pcf8574AddButton(void) { + // XdrvMailbox.index = button/switch index + if (Pcf8574.button_offset < 0) { Pcf8574.button_offset = XdrvMailbox.index; } + uint32_t index = XdrvMailbox.index - Pcf8574.button_offset; + if (index >= Pcf8574.button_max) { return false; } + XdrvMailbox.index = (Pcf8574DigitalRead(Pcf8574Pin(GPIO_KEY1, index)) != bitRead(Pcf8574.button_inverted, index)); + +// AddLog(LOG_LEVEL_DEBUG, PSTR("PCF: AddButton index %d, state %d"), index, XdrvMailbox.index); + + return true; +} + +bool Pcf8574AddSwitch(void) { + // XdrvMailbox.index = button/switch index + if (Pcf8574.switch_offset < 0) { Pcf8574.switch_offset = XdrvMailbox.index; } + uint32_t index = XdrvMailbox.index - Pcf8574.switch_offset; + if (index >= Pcf8574.switch_max) { return false; } + XdrvMailbox.index = Pcf8574DigitalRead(Pcf8574Pin(GPIO_SWT1, index)); + return true; +} + +/*********************************************************************************************\ + * PCF8574 Mode 1 - Stefan Bode +\*********************************************************************************************/ + +void Pcf8574SwitchRelay(void) { for (uint32_t i = 0; i < TasmotaGlobal.devices_present; i++) { uint8_t relay_state = bitRead(XdrvMailbox.index, i); @@ -109,8 +440,7 @@ void Pcf8574SwitchRelay(void) } } -void Pcf8574Init(void) -{ +void Pcf8574ModuleInit(void) { uint8_t pcf8574_address = (PCF8574_ADDR1_COUNT > 0) ? PCF8574_ADDR1 : PCF8574_ADDR2; while ((Pcf8574.max_devices < MAX_PCF8574) && (pcf8574_address < PCF8574_ADDR2 +PCF8574_ADDR2_COUNT)) { @@ -127,16 +457,18 @@ void Pcf8574Init(void) // AddLog(LOG_LEVEL_DEBUG, PSTR("PCF: Probing addr: 0x%x for PCF8574"), pcf8574_address); if (I2cSetDevice(pcf8574_address)) { - Pcf8574.type = true; + Pcf8574.mode = 1; Pcf8574.address[Pcf8574.max_devices] = pcf8574_address; + Pcf8574.max_connected_ports += 8; Pcf8574.max_devices++; - strcpy(Pcf8574.stype, "PCF8574"); + char stype[9]; + strcpy(stype, "PCF8574"); if (pcf8574_address >= PCF8574_ADDR2) { - strcpy(Pcf8574.stype, "PCF8574A"); + strcpy(stype, "PCF8574A"); } - I2cSetActiveFound(pcf8574_address, Pcf8574.stype); + I2cSetActiveFound(pcf8574_address, stype); } pcf8574_address++; @@ -144,43 +476,53 @@ void Pcf8574Init(void) pcf8574_address = PCF8574_ADDR2; } } - if (Pcf8574.type) { - for (uint32_t i = 0; i < sizeof(Pcf8574.pin); i++) { - Pcf8574.pin[i] = 99; - } - UpdateDevicesPresent(-Pcf8574.max_connected_ports); // reset no of devices to avoid duplicate ports on duplicate init. - Pcf8574.max_connected_ports = 0; // reset no of devices to avoid duplicate ports on duplicate init. - for (uint32_t idx = 0; idx < Pcf8574.max_devices; idx++) { // suport up to 8 boards PCF8574 - uint8_t gpio = Pcf8574Read(idx); - // Insure the input pins are actually writen a 1 for proper input operation - Pcf8574.pin_mask[idx] = gpio | ~Settings->pcf8574_config[idx]; - Pcf8574Write(idx); // Write back to the register + if (Pcf8574.mode) { + if (Pcf8574LoadTemplate()) { + Pcf8574.mode = 2; + + Pcf8574.relay_offset = TasmotaGlobal.devices_present; + Pcf8574.relay_max -= UpdateDevicesPresent(Pcf8574.relay_max); + + Pcf8574.button_offset = -1; + Pcf8574.switch_offset = -1; + } else { + Pcf8574.max_connected_ports = 0; // Reset no of devices + for (uint32_t i = 0; i < PCF8574_MAX_PINS; i++) { + Pcf8574.pin[i] = 99; + } + + for (uint32_t idx = 0; idx < Pcf8574.max_devices; idx++) { // suport up to 8 boards PCF8574 + uint8_t gpio = Pcf8574Read(idx); + // Insure the input pins are actually writen a 1 for proper input operation + Pcf8574.pin_mask[idx] = gpio | ~Settings->pcf8574_config[idx]; + Pcf8574Write(idx); // Write back to the register #ifdef USE_PCF8574_MQTTINPUT - Pcf8574.last_input[idx] = gpio & ~Settings->pcf8574_config[idx]; + Pcf8574.last_input[idx] = gpio & ~Settings->pcf8574_config[idx]; #endif // #ifdef USE_PCF8574_MQTTINPUT - //AddLog(LOG_LEVEL_DEBUG, PSTR("PCF: PCF-%d config=0x%02x, gpio=0x%02X"), idx +1, Settings->pcf8574_config[idx], gpio); + //AddLog(LOG_LEVEL_DEBUG, PSTR("PCF: PCF-%d config=0x%02x, gpio=0x%02X"), idx +1, Settings->pcf8574_config[idx], gpio); - for (uint32_t i = 0; i < 8; i++, gpio>>=1) { - uint8_t _result = Settings->pcf8574_config[idx] >> i &1; - //AddLog(LOG_LEVEL_DEBUG, PSTR("PCF: I2C shift i %d: %d. Powerstate: %d, TasmotaGlobal.devices_present: %d"), i,_result, Settings->power>>i&1, TasmotaGlobal.devices_present); - if (_result > 0) { - Pcf8574.pin[TasmotaGlobal.devices_present] = i + 8 * idx; - bitWrite(TasmotaGlobal.rel_inverted, TasmotaGlobal.devices_present, Settings->flag3.pcf8574_ports_inverted); // SetOption81 - Invert all ports on PCF8574 devices - if (!Settings->flag.save_state && !Settings->flag3.no_power_feedback) { // SetOption63 - Don't scan relay power state at restart - #5594 and #5663 - //AddLog(LOG_LEVEL_DEBUG, PSTR("PCF: Set power from from chip state")); - uint8_t power_state = Settings->flag3.pcf8574_ports_inverted ? 1 & ~gpio : 1 & gpio; - bitWrite(TasmotaGlobal.power, TasmotaGlobal.devices_present, power_state); - bitWrite(Settings->power, TasmotaGlobal.devices_present, power_state); + for (uint32_t i = 0; i < 8; i++, gpio>>=1) { + uint8_t _result = Settings->pcf8574_config[idx] >> i &1; + //AddLog(LOG_LEVEL_DEBUG, PSTR("PCF: I2C shift i %d: %d. Powerstate: %d, TasmotaGlobal.devices_present: %d"), i,_result, Settings->power>>i&1, TasmotaGlobal.devices_present); + if (_result > 0) { + Pcf8574.pin[TasmotaGlobal.devices_present] = i + 8 * idx; + bitWrite(TasmotaGlobal.rel_inverted, TasmotaGlobal.devices_present, Settings->flag3.pcf8574_ports_inverted); // SetOption81 - Invert all ports on PCF8574 devices + if (!Settings->flag.save_state && !Settings->flag3.no_power_feedback) { // SetOption63 - Don't scan relay power state at restart - #5594 and #5663 + //AddLog(LOG_LEVEL_DEBUG, PSTR("PCF: Set power from from chip state")); + uint8_t power_state = Settings->flag3.pcf8574_ports_inverted ? 1 & ~gpio : 1 & gpio; + bitWrite(TasmotaGlobal.power, TasmotaGlobal.devices_present, power_state); + bitWrite(Settings->power, TasmotaGlobal.devices_present, power_state); + } + //else AddLog(LOG_LEVEL_DEBUG, PSTR("PCF: DON'T set power from chip state")); + UpdateDevicesPresent(1); + Pcf8574.max_connected_ports++; } - //else AddLog(LOG_LEVEL_DEBUG, PSTR("PCF: DON'T set power from chip state")); - UpdateDevicesPresent(1); - Pcf8574.max_connected_ports++; } } + //AddLog(LOG_LEVEL_DEBUG, PSTR("PCF: Settings->power=0x%08X, TasmotaGlobal.power=0x%08X"), Settings->power, TasmotaGlobal.power); + AddLog(LOG_LEVEL_INFO, PSTR("PCF: Total devices %d, PCF8574 output ports %d"), Pcf8574.max_devices, Pcf8574.max_connected_ports); } - //AddLog(LOG_LEVEL_DEBUG, PSTR("PCF: Settings->power=0x%08X, TasmotaGlobal.power=0x%08X"), Settings->power, TasmotaGlobal.power); - AddLog(LOG_LEVEL_INFO, PSTR("PCF: Total devices %d, PCF8574 output ports %d"), Pcf8574.max_devices, Pcf8574.max_connected_ports); } } @@ -340,25 +682,23 @@ void Pcf8574SaveSettings(void) * Interface \*********************************************************************************************/ -bool Xdrv28(uint32_t function) -{ +bool Xdrv28(uint32_t function) { if (!I2cEnabled(XI2C_02)) { return false; } bool result = false; - if (FUNC_PRE_INIT == function) { - Pcf8574Init(); - } - else if (Pcf8574.type) { + if (FUNC_MODULE_INIT == function) { + Pcf8574ModuleInit(); + } else if (1 == Pcf8574.mode) { switch (function) { - case FUNC_SET_POWER: - Pcf8574SwitchRelay(); - break; #ifdef USE_PCF8574_MQTTINPUT case FUNC_EVERY_50_MSECOND: Pcf8574CheckForInputChange(); break; #endif // #ifdef USE_PCF8574_MQTTINPUT + case FUNC_SET_POWER: + Pcf8574SwitchRelay(); + break; #ifdef USE_PCF8574_SENSOR case FUNC_JSON_APPEND: Pcf8574Show(1); @@ -378,6 +718,32 @@ bool Xdrv28(uint32_t function) #endif // #ifdef USE_PCF8574_DISPLAYINPUT #endif // USE_WEBSERVER } + } else if (2 == Pcf8574.mode) { + switch (function) { + case FUNC_LOOP: + case FUNC_SLEEP_LOOP: + if (!Pcf8574.interrupt) { return false; } + AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("PCF: Interrupt")); + Pcf8574ServiceInput(); + break; + case FUNC_EVERY_100_MSECOND: + if (Pcf8574.button_max || Pcf8574.switch_max) { + Pcf8574ServiceInput(); + } + break; + case FUNC_SET_POWER: + Pcf8574Power(); + break; + case FUNC_INIT: + Pcf8574Init(); + break; + case FUNC_ADD_BUTTON: + result = Pcf8574AddButton(); + break; + case FUNC_ADD_SWITCH: + result = Pcf8574AddSwitch(); + break; + } } return result; } diff --git a/tasmota/tasmota_xdrv_driver/xdrv_28_pcf8574_v1.ino b/tasmota/tasmota_xdrv_driver/xdrv_28_pcf8574_v1.ino new file mode 100644 index 000000000..841da4e70 --- /dev/null +++ b/tasmota/tasmota_xdrv_driver/xdrv_28_pcf8574_v1.ino @@ -0,0 +1,386 @@ +/* + xdrv_28_pcf8574.ino - PCF8574 I2C support for Tasmota + + Copyright (C) 2021 Stefan Bode + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifdef USE_I2C +#ifdef USE_PCF8574_V1 +/*********************************************************************************************\ + * PCF8574 - I2C IO Expander + * + * I2C Address: PCF8574 = 0x20 .. 0x27 (0x27 is not supported), + * PCF8574A = 0x39 .. 0x3F (0x38 is not supported) +\*********************************************************************************************/ + +#define XDRV_28 28 +#define XI2C_02 2 // See I2CDEVICES.md + +// Start address and count can be overriden in user_config_override.h to allow better +// sharing of the I2C address space. Still the covered range must remains valid. +// A count of 0 can be used totaly disable any of the 2 ranges. +// By default, the following addresses are explicitly excluded (as per the docs) : +// - 0x27 and 0x37 are reserved for USE_DISPLAY_LCD in xdsp_01_lcd.ino +// - 0X38 is reserved for other sensors +// If the respective drivers are not used, overrides allows to recover those addresses +// If defined, USE_MCP230xx_ADDR is also always excluded + +// PCF8574 address range from 0x20 to 0x26 +#ifndef PCF8574_ADDR1 +#define PCF8574_ADDR1 0x20 // PCF8574 +#endif +#ifndef PCF8574_ADDR1_COUNT +#define PCF8574_ADDR1_COUNT 7 +#endif +// PCF8574A address range from 0x39 to 0x3E +#ifndef PCF8574_ADDR2 +#define PCF8574_ADDR2 0x39 // PCF8574A +#endif +#ifndef PCF8574_ADDR2_COUNT +#define PCF8574_ADDR2_COUNT 6 +#endif + +// Consitency tests - Checked across the complete range for the PCF8574/PCF8574A to allow override +#if (PCF8574_ADDR1 < 0x20) || ((PCF8574_ADDR1 + PCF8574_ADDR1_COUNT - 1) > 0x27) +#error PCF8574_ADDR1 and/or PCF8574_ADDR1_COUNT badly overriden. Fix your user_config_override +#endif +#if (PCF8574_ADDR2 < 0x38) || ((PCF8574_ADDR2 + PCF8574_ADDR2_COUNT - 1) > 0x3F) +#error PCF8574_ADDR2 and/or PCF8574_ADDR2_COUNT badly overriden. Fix your user_config_override. +#endif + +struct PCF8574 { + int error; + uint8_t pin[64]; + uint8_t address[MAX_PCF8574]; + uint8_t pin_mask[MAX_PCF8574] = { 0 }; +#ifdef USE_PCF8574_MQTTINPUT + uint8_t last_input[MAX_PCF8574] = { 0 }; +#endif + uint8_t max_connected_ports = 0; // Max numbers of devices comming from PCF8574 modules + uint8_t max_devices = 0; // Max numbers of PCF8574 modules + char stype[9]; + bool type = false; +} Pcf8574; + +uint8_t Pcf8574Read(uint8_t idx) +{ + Wire.requestFrom(Pcf8574.address[idx],(uint8_t)1); + return Wire.read(); +} + +uint8_t Pcf8574Write(uint8_t idx) +{ + Wire.beginTransmission(Pcf8574.address[idx]); + Wire.write(Pcf8574.pin_mask[idx]); + return Wire.endTransmission(); +} + +void Pcf8574SwitchRelay(void) +{ + for (uint32_t i = 0; i < TasmotaGlobal.devices_present; i++) { + uint8_t relay_state = bitRead(XdrvMailbox.index, i); + + if (Pcf8574.max_devices > 0 && Pcf8574.pin[i] < 99) { + uint8_t board = Pcf8574.pin[i]>>3; + uint8_t pin = Pcf8574.pin[i]&0x7; + uint8_t oldpinmask = Pcf8574.pin_mask[board]; + uint8_t _val = bitRead(TasmotaGlobal.rel_inverted, i) ? !relay_state : relay_state; + + //AddLog(LOG_LEVEL_DEBUG, PSTR("PCF: SwitchRelay %d=%d => PCF-%d.D%d=%d"), i, relay_state, board +1, pin, _val); + bitWrite(Pcf8574.pin_mask[board], pin, _val); + if (oldpinmask != Pcf8574.pin_mask[board]) { + Pcf8574.error = Pcf8574Write(board); + } + //else AddLog(LOG_LEVEL_DEBUG, PSTR("PCF: SwitchRelay skipped")); + } + } +} + +void Pcf8574Init(void) +{ + uint8_t pcf8574_address = (PCF8574_ADDR1_COUNT > 0) ? PCF8574_ADDR1 : PCF8574_ADDR2; + while ((Pcf8574.max_devices < MAX_PCF8574) && (pcf8574_address < PCF8574_ADDR2 +PCF8574_ADDR2_COUNT)) { + +#if defined(USE_MCP230xx) && defined(USE_MCP230xx_ADDR) + if (USE_MCP230xx_ADDR == pcf8574_address) { + AddLog(LOG_LEVEL_INFO, PSTR("PCF: Address 0x%02x reserved for MCP320xx skipped"), pcf8574_address); + pcf8574_address++; + if ((PCF8574_ADDR1 +PCF8574_ADDR1_COUNT) == pcf8574_address) { // See comment on allowed addresses and overrides + pcf8574_address = PCF8574_ADDR2; + } + } +#endif + + // AddLog(LOG_LEVEL_DEBUG, PSTR("PCF: Probing addr: 0x%x for PCF8574"), pcf8574_address); + + if (I2cSetDevice(pcf8574_address)) { + Pcf8574.type = true; + + Pcf8574.address[Pcf8574.max_devices] = pcf8574_address; + Pcf8574.max_devices++; + + strcpy(Pcf8574.stype, "PCF8574"); + if (pcf8574_address >= PCF8574_ADDR2) { + strcpy(Pcf8574.stype, "PCF8574A"); + } + I2cSetActiveFound(pcf8574_address, Pcf8574.stype); + } + + pcf8574_address++; + if ((PCF8574_ADDR1 +PCF8574_ADDR1_COUNT) == pcf8574_address) { // Support I2C addresses 0x20 to 0x26 and 0x39 to 0x3F + pcf8574_address = PCF8574_ADDR2; + } + } + if (Pcf8574.type) { + for (uint32_t i = 0; i < sizeof(Pcf8574.pin); i++) { + Pcf8574.pin[i] = 99; + } + UpdateDevicesPresent(-Pcf8574.max_connected_ports); // reset no of devices to avoid duplicate ports on duplicate init. + + Pcf8574.max_connected_ports = 0; // reset no of devices to avoid duplicate ports on duplicate init. + for (uint32_t idx = 0; idx < Pcf8574.max_devices; idx++) { // suport up to 8 boards PCF8574 + uint8_t gpio = Pcf8574Read(idx); + // Insure the input pins are actually writen a 1 for proper input operation + Pcf8574.pin_mask[idx] = gpio | ~Settings->pcf8574_config[idx]; + Pcf8574Write(idx); // Write back to the register +#ifdef USE_PCF8574_MQTTINPUT + Pcf8574.last_input[idx] = gpio & ~Settings->pcf8574_config[idx]; +#endif // #ifdef USE_PCF8574_MQTTINPUT + //AddLog(LOG_LEVEL_DEBUG, PSTR("PCF: PCF-%d config=0x%02x, gpio=0x%02X"), idx +1, Settings->pcf8574_config[idx], gpio); + + for (uint32_t i = 0; i < 8; i++, gpio>>=1) { + uint8_t _result = Settings->pcf8574_config[idx] >> i &1; + //AddLog(LOG_LEVEL_DEBUG, PSTR("PCF: I2C shift i %d: %d. Powerstate: %d, TasmotaGlobal.devices_present: %d"), i,_result, Settings->power>>i&1, TasmotaGlobal.devices_present); + if (_result > 0) { + Pcf8574.pin[TasmotaGlobal.devices_present] = i + 8 * idx; + bitWrite(TasmotaGlobal.rel_inverted, TasmotaGlobal.devices_present, Settings->flag3.pcf8574_ports_inverted); // SetOption81 - Invert all ports on PCF8574 devices + if (!Settings->flag.save_state && !Settings->flag3.no_power_feedback) { // SetOption63 - Don't scan relay power state at restart - #5594 and #5663 + //AddLog(LOG_LEVEL_DEBUG, PSTR("PCF: Set power from from chip state")); + uint8_t power_state = Settings->flag3.pcf8574_ports_inverted ? 1 & ~gpio : 1 & gpio; + bitWrite(TasmotaGlobal.power, TasmotaGlobal.devices_present, power_state); + bitWrite(Settings->power, TasmotaGlobal.devices_present, power_state); + } + //else AddLog(LOG_LEVEL_DEBUG, PSTR("PCF: DON'T set power from chip state")); + UpdateDevicesPresent(1); + Pcf8574.max_connected_ports++; + } + } + } + //AddLog(LOG_LEVEL_DEBUG, PSTR("PCF: Settings->power=0x%08X, TasmotaGlobal.power=0x%08X"), Settings->power, TasmotaGlobal.power); + AddLog(LOG_LEVEL_INFO, PSTR("PCF: Total devices %d, PCF8574 output ports %d"), Pcf8574.max_devices, Pcf8574.max_connected_ports); + } +} + +/*********************************************************************************************\ + * Presentation +\*********************************************************************************************/ + +#ifdef USE_WEBSERVER + +#define WEB_HANDLE_PCF8574 "pcf" + +const char HTTP_BTN_MENU_PCF8574[] PROGMEM = + "

"; + +const char HTTP_FORM_I2C_PCF8574_1[] PROGMEM = + "
 " D_PCF8574_PARAMETERS " " + "
" + "


"; + +const char HTTP_FORM_I2C_PCF8574_2[] PROGMEM = + "" D_DEVICE " %d " D_PORT " %d"; + +const char HTTP_SNS_PCF8574_GPIO[] PROGMEM = "{s}PCF8574%c%d D%d{m}%d{e}"; // {s} = , {m} = , {e} = + + +void HandlePcf8574(void) +{ + if (!HttpCheckPriviledgedAccess()) { return; } + + AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_HTTP D_CONFIGURE_PCF8574)); + + if (Webserver->hasArg("save")) { + Pcf8574SaveSettings(); + WebRestart(1); + return; + } + + WSContentStart_P(D_CONFIGURE_PCF8574); + WSContentSendStyle(); + WSContentSend_P(HTTP_FORM_I2C_PCF8574_1, (Settings->flag3.pcf8574_ports_inverted) ? PSTR(" checked") : ""); // SetOption81 - Invert all ports on PCF8574 devices + WSContentSend_P(HTTP_TABLE100); + for (uint32_t idx = 0; idx < Pcf8574.max_devices; idx++) { + for (uint32_t idx2 = 0; idx2 < 8; idx2++) { // 8 ports on PCF8574 + uint8_t helper = 1 << idx2; + WSContentSend_P(HTTP_FORM_I2C_PCF8574_2, + idx +1, idx2, + idx2 + 8*idx, + idx2 + 8*idx, + ((helper & Settings->pcf8574_config[idx]) >> idx2 == 0) ? PSTR(" selected ") : " ", + ((helper & Settings->pcf8574_config[idx]) >> idx2 == 1) ? PSTR(" selected ") : " " + ); + } + } + WSContentSend_P(PSTR("")); + WSContentSend_P(HTTP_FORM_END); + WSContentSpaceButton(BUTTON_CONFIGURATION); + WSContentStop(); +} + +#if defined(USE_PCF8574_SENSOR) || defined(USE_PCF8574_DISPLAYINPUT) +void Pcf8574Show(bool json) +{ +#ifdef USE_PCF8574_SENSOR + if (json) { + for (int idx = 0 ; idx < Pcf8574.max_devices ; idx++) + { + uint8_t gpio = Pcf8574Read(idx); + ResponseAppend_P(PSTR(",\"PCF8574%c%d\":{\"D0\":%i,\"D1\":%i,\"D2\":%i,\"D3\":%i,\"D4\":%i,\"D5\":%i,\"D6\":%i,\"D7\":%i}"), + IndexSeparator(), idx +1, + (gpio>>0)&1,(gpio>>1)&1,(gpio>>2)&1,(gpio>>3)&1,(gpio>>4)&1,(gpio>>5)&1,(gpio>>6)&1,(gpio>>7)&1); + } + } +#endif // #ifdef USE_PCF8574_SENSOR +#if defined(USE_WEBSERVER) && defined(USE_PCF8574_DISPLAYINPUT) + if(!json) { + for (int idx = 0 ; idx < Pcf8574.max_devices ; idx++) + { + uint8_t input_mask = ~Settings->pcf8574_config[idx]; //invert to 1 = input + uint8_t gpio = Pcf8574Read(idx); + for (int pin = 0 ; pin < 8 ; ++pin, input_mask>>=1, gpio>>=1) + { + if (input_mask & 1) + WSContentSend_P(HTTP_SNS_PCF8574_GPIO, IndexSeparator(), idx +1, pin, gpio & 1); + } + } + } +#endif // defined(USE_WEBSERVER) && defined(USE_PCF8574_DISPLAYINPUT) +} +#endif // #if defined(USE_PCF8574_SENSOR) || defined(USE_PCF8574_DISPLAYINPUT) + + +#ifdef USE_PCF8574_MQTTINPUT +void Pcf8574CheckForInputChange(void) +{ + for (int idx = 0 ; idx < Pcf8574.max_devices ; idx++) + { + uint8_t input_mask = ~Settings->pcf8574_config[idx]; //invert to 1 = input + uint8_t input = Pcf8574Read(idx) & input_mask; + uint8_t last_input = Pcf8574.last_input[idx]; + if (input != last_input) { // don't scan bits if no change (EVERY_50_MS !) + for (uint8_t pin = 0 ; pin < 8 ; ++pin) { + if (bitRead(input_mask,pin) && bitRead(input,pin) != bitRead(last_input,pin)) { + ResponseTime_P(PSTR(",\"PCF8574%c%d_INP\":{\"D%i\":%i}}"), IndexSeparator(), idx +1, pin, bitRead(input,pin)); + MqttPublishPrefixTopicRulesProcess_P(RESULT_OR_STAT, PSTR("PCF8574_INP")); + if (Settings->flag3.hass_tele_on_power) { // SetOption59 - Send tele/%topic%/SENSOR in addition to stat/%topic%/RESULT + MqttPublishSensor(); + } + } + Pcf8574.last_input[idx] = input; + } + } + } +} +#endif //#ifdef USE_PCF8574_MQTTINPUT + +void Pcf8574SaveSettings(void) +{ + char stemp[7]; + char tmp[100]; + + //AddLog(LOG_LEVEL_DEBUG, PSTR("PCF: Start working on Save arguements: inverted:%d")), Webserver->hasArg("b1"); + + Settings->flag3.pcf8574_ports_inverted = Webserver->hasArg("b1"); // SetOption81 - Invert all ports on PCF8574 devices + for (byte idx = 0; idx < Pcf8574.max_devices; idx++) { + byte count=0; + byte n = Settings->pcf8574_config[idx]; + while(n!=0) { + n = n&(n-1); + count++; + } + if (count <= TasmotaGlobal.devices_present) { + UpdateDevicesPresent(-count); + } + for (byte i = 0; i < 8; i++) { + snprintf_P(stemp, sizeof(stemp), PSTR("i2cs%d"), i+8*idx); + WebGetArg(stemp, tmp, sizeof(tmp)); + byte _value = (!strlen(tmp)) ? 0 : atoi(tmp); + if (_value) { + Settings->pcf8574_config[idx] = Settings->pcf8574_config[idx] | 1 << i; + UpdateDevicesPresent(1); + Pcf8574.max_connected_ports++; + } else { + Settings->pcf8574_config[idx] = Settings->pcf8574_config[idx] & ~(1 << i ); + } + } + //Settings->pcf8574_config[0] = (!strlen(webServer->arg("i2cs0").c_str())) ? 0 : atoi(webServer->arg("i2cs0").c_str()); + //AddLog(LOG_LEVEL_INFO, PSTR("PCF: I2C Board: %d, Config: %2x")), idx, Settings->pcf8574_config[idx]; + + } +} +#endif // USE_WEBSERVER + +/*********************************************************************************************\ + * Interface +\*********************************************************************************************/ + +bool Xdrv28(uint32_t function) +{ + if (!I2cEnabled(XI2C_02)) { return false; } + + bool result = false; + + if (FUNC_PRE_INIT == function) { + Pcf8574Init(); + } + else if (Pcf8574.type) { + switch (function) { + case FUNC_SET_POWER: + Pcf8574SwitchRelay(); + break; +#ifdef USE_PCF8574_MQTTINPUT + case FUNC_EVERY_50_MSECOND: + Pcf8574CheckForInputChange(); + break; +#endif // #ifdef USE_PCF8574_MQTTINPUT +#ifdef USE_PCF8574_SENSOR + case FUNC_JSON_APPEND: + Pcf8574Show(1); + break; +#endif // #ifdef USE_PCF8574_SENSOR +#ifdef USE_WEBSERVER + case FUNC_WEB_ADD_BUTTON: + WSContentSend_P(HTTP_BTN_MENU_PCF8574); + break; + case FUNC_WEB_ADD_HANDLER: + WebServer_on(PSTR("/" WEB_HANDLE_PCF8574), HandlePcf8574); + break; +#ifdef USE_PCF8574_DISPLAYINPUT + case FUNC_WEB_SENSOR: + Pcf8574Show(0); + break; +#endif // #ifdef USE_PCF8574_DISPLAYINPUT +#endif // USE_WEBSERVER + } + } + return result; +} + +#endif // USE_PCF8574 +#endif // USE_I2C