├── .gitignore ├── LICENSE.md ├── MQTT.md ├── README.md ├── arduino-multi-relay.ino ├── config.h.sample └── images └── node-red-flow.png /.gitignore: -------------------------------------------------------------------------------- 1 | *.hex 2 | config.h 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | License 2 | 3 | Copyright (c) 2019 lkankowski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software, including without limitation the rights to use, copy, modify, 8 | merge, publish, distribute, (but NOT sell) copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to the 10 | following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MQTT.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | Connecting Arduino directly to HomeSystem application in some cases isn't enough. 3 | One of the drawback is that only one application can be connected over serial interface. 4 | There are some hacks, like `socat`, but why don't use MQTT gateway? 5 | For me it is excelent choice, and also gives me opportunity to test another home application 6 | at the same time when Domoticz. 7 | 8 | 9 | # Requirements / assumptions 10 | * Arduino board connected through UART (Serial/USB) - "/dev/ttyACM0" 11 | * Docker 12 | * Mosquitto 13 | * Domoticz (optional) 14 | I've test all commands as root using `sudo bash` after login - sorry for a bad practise :) 15 | 16 | 17 | # Installation 18 | ## Node-Red on Docker 19 | If you already have Node-Red, just skip to next step 20 | 21 | Create directory as user with uid 1000 (usually "pi"): 22 | ``` 23 | sudo mkdir /docker/nodered 24 | sudo chown 1000:1000 /docker/nodered 25 | ``` 26 | Now run the container (can take some time): 27 | ``` 28 | docker run -dit -p 1880:1880 -v /docker/nodered:/data --group-add dialout --device=/dev/ttyACM0 --name nodered nodered/node-red 29 | #CTRL+C to leave docker - Node-Red will keep running in background 30 | ``` 31 | More about Node-Red in Docker - https://nodered.org/docs/getting-started/docker 32 | 33 | 34 | ## Node-Red modules 35 | Enter into docker container: 36 | ``` 37 | docker exec -it nodered /bin/bash 38 | ``` 39 | and install required Node-Red modules: 40 | ``` 41 | npm install --no-optional node-red-contrib-mysensors 42 | npm install --no-optional node-red-node-serialport 43 | exit 44 | ``` 45 | When installing outside docker, options "-g" and "--unsafe-perm" can be helpfull. 46 | 47 | Optionaly you can create image, so you can faster recreate container: 48 | ``` 49 | docker commit nodered my-node-red 50 | ``` 51 | And from now you can re-create container using: 52 | ``` 53 | docker stop nodered 54 | docker rm nodered 55 | docker run -dit -p 1880:1880 -v /docker/nodered:/data --group-add dialout --device=/dev/ttyACM0 --restart always --name nodered my-node-red 56 | #CTRL+C to leave docker - Node-Red will keep running in background 57 | ``` 58 | 59 | 60 | ## MQTT Gateway on Node-Red 61 | Create flow like this: 62 | ![MySensors MQTT Gateway Flow](images/node-red-flow.png) 63 | 64 | or import flow like this: 65 | ``` 66 | [{"id":"72d44000.f28408","type":"tab","label":"MQTT/Serial Gateway","disabled":false,"info":""},{"id":"95ef7ed6.7fb9e","type":"serial in","z":"72d44000.f28408","name":"From Lights","serial":"b5ac5c96.bd3248","x":90,"y":140,"wires":[["fecbb4a9.a5ea68"]]},{"id":"2fa5780d.136bf8","type":"serial out","z":"72d44000.f28408","name":"To Lights","serial":"b5ac5c96.bd3248","x":700,"y":460,"wires":[]},{"id":"5c13e55a.e2cfec","type":"mqtt out","z":"72d44000.f28408","name":"To Domoticz MQTT","topic":"","qos":"","retain":"","broker":"bbe453f9.77c5f","x":730,"y":220,"wires":[]},{"id":"d22c4b6.f01e938","type":"mqtt in","z":"72d44000.f28408","name":"","topic":"domoticz/out/MyMQTT/#","qos":"2","datatype":"auto","broker":"bbe453f9.77c5f","x":130,"y":360,"wires":[["f8a3c82a.95084"]]},{"id":"f8a3c82a.95084","type":"mysdecode","z":"72d44000.f28408","database":"","name":"","mqtt":true,"enrich":false,"x":440,"y":360,"wires":[["66725363.829d5c"]]},{"id":"fecbb4a9.a5ea68","type":"mysdecode","z":"72d44000.f28408","database":"","name":"","mqtt":false,"enrich":false,"x":350,"y":140,"wires":[["322780f7.dea218"]]},{"id":"322780f7.dea218","type":"mysencode","z":"72d44000.f28408","name":"","mqtt":true,"mqtttopic":"domoticz/in/MyMQTT","x":410,"y":220,"wires":[["5c13e55a.e2cfec"]]},{"id":"66725363.829d5c","type":"mysencode","z":"72d44000.f28408","name":"","mqtt":false,"mqtttopic":"","x":430,"y":460,"wires":[["2fa5780d.136bf8"]]},{"id":"b5ac5c96.bd3248","type":"serial-port","z":"","serialport":"/dev/ttyACM0","serialbaud":"115200","databits":"8","parity":"none","stopbits":"1","waitfor":"","dtr":"none","rts":"none","cts":"none","dsr":"none","newline":"\\n","bin":"false","out":"char","addchar":"\\n","responsetimeout":"10000"},{"id":"bbe453f9.77c5f","type":"mqtt-broker","z":"","name":"Mosquitto","broker":"192.168.1.111","port":"1883","clientid":"","usetls":false,"compatmode":false,"keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","closeTopic":"","closeQos":"0","closePayload":"","willTopic":"","willQos":"0","willPayload":""}] 67 | ``` 68 | Configurations: 69 | * Serial port parameters: 115200-8N1, add character to output messages "\n" 70 | * MQTT broker: just put IP and credentials 71 | 72 | 73 | ## Domoticz 74 | Make database backup first! 75 | 76 | Go to Configuration >> Hardware and select "MySensors Gateway USB". 77 | Change it to "MySensors Gateway with MQTT interface", provide Mosquitto broker parameters (IP, portand credentials) and click update button. 78 | 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Check new version: https://github.com/lkankowski/arduino-multi-relay2. 2 | 3 | This version is not maintained anymore. 4 | 5 | # About 6 | Arduino program to handle relays and control them using switches. 7 | Single pair of relay and switch are configured in single line. 8 | Buttons has debouncing and support for mono- and bi-stable switches. 9 | There is support for multiple switches for single relay and virtual switches for devices only accessible from Home App. 10 | 11 | I left configuration variable _myRelayButtons[]_ which is used at my house, so before use you HAVE TO change it. 12 | 13 | # Configuration 14 | ## First time only 15 | Copy file "config.h.sample" into "config.h". On any future code update, you won't have to copy configuration to a newer version. 16 | 17 | ## Main config file "config.h" 18 | ``` 19 | RelayButton myRelayButtons[] = { 20 | {sensor ID, relay pin, button pin, relay options, button type, relay description} 21 | }; 22 | ``` 23 | Params description: 24 | * sensor ID - sensor ID reported on MySensor Gateway (ie. Domoticz/Home Assistant), must be unique (can be -1 when it is not reported MySensor Gateway) 25 | * relay pin - pin connected to the relay. Expander is supported (see details inseparate section) 26 | * button pin - pin connected to the button. There can be multiple buttons to the same relay. "<0" for virtual buttons (only available in MySensor Gateway, ie. Domoticz/Home Assistant). There is no support for Expander because of de-bouncing. 27 | * relay options - combined with '|' operator: 28 | * RELAY_TRIGGER_LOW or RELAY_TRIGGER_HIGH - trigger level, required 29 | * RELAY_STARTUP_ON or RELAY_STARTUP_OFF - startup state, optional 30 | * RELAY_IMPULSE - optional, relay is turned on only for short period of time (defined in constant RELAY_IMPULSE_INTERVAL, 250ms by default), ignored for DING_DONG and REED_SWITCH buttons 31 | * button type: 32 | * MONO_STABLE - GND connected to the button pin 33 | * BI_STABLE - state change from LOW to HIGH and HIGH to LOW, used with mechanical and touch buttons 34 | * DING_DONG - doorbell button, relay is triggered only when button is pushed (experimental, not tested) 35 | * REED_SWITCH - door/window sensor, oposite to DING_DONG - state LOW when door/window is closed, HIGH when opened 36 | * relay description - reported on MySensor Gateway, can help identify device on initial configuration in Home Automation App (ie. Domoticz/Home Assistant), can be empty ("") 37 | 38 | In my case, I have a documentation of whole electricity of my house in Google Calc, and I have a formula to generate this configuration. 39 | 40 | ## Mono-stable buttons 41 | For mono-stable buttons you can define in "config.h" if relay is triggered when you push button: 42 | ``` 43 | const uint8_t MONO_STABLE_TRIGGER = LOW; 44 | ``` 45 | or when button is released: 46 | ``` 47 | const uint8_t MONO_STABLE_TRIGGER = HIGH; 48 | ``` 49 | 50 | 51 | # Debugging 52 | In a sketch file: 53 | ``` 54 | #define MY_DEBUG 55 | ``` 56 | Just comment this on production. 57 | 58 | # Sample configurations 59 | ## Simple mono-stable switch and relay pair 60 | Relay is connected to pin 12, button is connected to pin 11, relay is triggered to ON using LOW state, button is mono-stable 61 | ``` 62 | RelayButton myRelayButtons[] = { 63 | {1, 12, 11, RELAY_TRIGGER_LOW, MONO_STABLE, ""} 64 | }; 65 | ``` 66 | 67 | ## Simple bi-stable switch and relay pair 68 | Relay is connected to pin 12, button is connected to pin 11, relay is triggered to ON using HIGH state, button is bi-stable 69 | ``` 70 | RelayButton myRelayButtons[] = { 71 | {1, 12, 11, RELAY_TRIGGER_HIGH, BI_STABLE, ""} 72 | }; 73 | ``` 74 | 75 | ## Simple two mono-stable switch for one relay 76 | Relay is connected to pin 12, buttons are connected to pins 11 and 10, relay is triggered to ON using HIGH state, buttons are mono-stable 77 | ``` 78 | RelayButton myRelayButtons[] = { 79 | {1, 12, 11, RELAY_TRIGGER_HIGH, MONO_STABLE, ""} 80 | {2, 12, 10, RELAY_TRIGGER_HIGH, MONO_STABLE, ""} 81 | }; 82 | ``` 83 | 84 | ## Mono-stable button with relay that starts turned on 85 | ``` 86 | RelayButton myRelayButtons[] = { 87 | {1, 12, 11, RELAY_TRIGGER_HIGH | RELAY_STARTUP_ON, MONO_STABLE, ""} 88 | }; 89 | ``` 90 | 91 | ## Two similar doorbell configuration 92 | ``` 93 | RelayButton myRelayButtons[] = { 94 | {1, 12, 11, RELAY_TRIGGER_HIGH | RELAY_IMPULSE, MONO_STABLE, ""} 95 | }; 96 | ``` 97 | is equivalent to: 98 | ``` 99 | RelayButton myRelayButtons[] = { 100 | {1, 12, 11, RELAY_TRIGGER_HIGH, DING_DONG, ""} 101 | }; 102 | ``` 103 | Difference is that the first example will do only 250ms impulse (turn on - wait ~250ms - turn off), and the second will turn on the relay as long, as button is pushed. 104 | 105 | ## Door or window sensor 106 | ``` 107 | RelayButton myRelayButtons[] = { 108 | {1, 12, 11, RELAY_TRIGGER_HIGH, REED_SWITCH, ""} 109 | }; 110 | ``` 111 | 112 | 113 | # Expander 114 | Only one expander library at a time is supported. 115 | 116 | ## PCF8574 117 | To use expander PCF8574 you have to install library (https://github.com/skywodd/pcf8574_arduino_library) 118 | Basic information about expander and library you can find here - https://youtu.be/JNmVREucfyc (PL, library in description) 119 | 120 | And uncomment expander library: 121 | ``` 122 | #include "PCF8574.h" 123 | ``` 124 | 125 | ## MCP23017 126 | https://github.com/adafruit/Adafruit-MCP23017-Arduino-Library 127 | 128 | Uncomment expander library: 129 | ``` 130 | #include "Adafruit_MCP23017.h" 131 | ``` 132 | 133 | ## Configuration 134 | 135 | Configure all expanders id: 136 | I.e. only one PCF8574 expander with id = 0x20: 137 | ``` 138 | uint8_t expanderAddresses[] = {0x20}; 139 | ``` 140 | I.e. only one MCP23017 expander with id = 0: 141 | ``` 142 | uint8_t expanderAddresses[] = {0}; 143 | ``` 144 | From now you can use expander pins in configuration _myRelayButtons[]_. To recognize expander pin, numbers start from 0x0100 and have special meaning: 145 | * first byte - expander number (starts from 1) 146 | * second byte - pin number 147 | In example - "0x0100" means pin 0 on first expander 148 | 149 | To simplify using expanders, there is "E(a,b)" macro: 150 | * a - expander number (starts from 0) 151 | * b - pin on expander [0-f] 152 | ``` 153 | E(0,0) - first pin on first expander 154 | ``` 155 | 156 | -------------------------------------------------------------------------------- /arduino-multi-relay.ino: -------------------------------------------------------------------------------- 1 | // Enable debug prints to serial monitor 2 | //#define MY_DEBUG 3 | 4 | #define MY_GATEWAY_SERIAL 5 | 6 | #include 7 | #include 8 | //#include "PCF8574.h" 9 | //#include "Adafruit_MCP23017.h" 10 | 11 | #if defined(PCF8574_H) || defined(_Adafruit_MCP23017_H_) 12 | #define USE_EXPANDER 13 | #include // Required for I2C communication 14 | uint8_t expanderAddresses[] = {0x20}; 15 | const int numberOfExpanders = sizeof(expanderAddresses); 16 | #if defined(PCF8574_H) 17 | PCF8574 expander[numberOfExpanders]; 18 | #elif defined(_Adafruit_MCP23017_H_) 19 | Adafruit_MCP23017 expander[numberOfExpanders]; 20 | #endif 21 | #define E(expanderNo, ExpanderPin) (((expanderNo+1)<<8) | (ExpanderPin)) 22 | #endif 23 | 24 | // No Button Constant 25 | #define NOB -1 26 | #define MULTI_RELAY_VERSION 9 27 | #define RELAY_STATE_STORAGE 1 28 | 29 | const uint8_t RELAY_TRIGGER_LOW = 0; 30 | const uint8_t RELAY_TRIGGER_HIGH = 1; 31 | const uint8_t RELAY_STARTUP_ON = 2; 32 | const uint8_t RELAY_STARTUP_OFF = 4; 33 | const uint8_t RELAY_IMPULSE = 8; 34 | const uint8_t RELAY_STARTUP_MASK = RELAY_STARTUP_ON | RELAY_STARTUP_OFF; 35 | 36 | enum ButtonType { 37 | MONO_STABLE = 0, 38 | BI_STABLE = 1, 39 | DING_DONG = 2, // HIGH state immediatly after push, LOW state after release 40 | REED_SWITCH = 3 // magnetic sensor for door or window, LOW - closed, HIGH - opened 41 | }; 42 | 43 | typedef struct { 44 | int sensorId; 45 | int relay; 46 | int button; 47 | uint8_t relayOptions; 48 | ButtonType buttonType; 49 | const char * relayDescription; 50 | } RelayButton; 51 | 52 | // Configuration in separate file 53 | #include "config.h" 54 | 55 | const int numberOfRelayButtons = sizeof(myRelayButtons) / sizeof(RelayButton); 56 | 57 | typedef struct { 58 | int firstButton; 59 | int nextButton; 60 | } RelayMultiButtons; 61 | 62 | RelayMultiButtons relayMultiButtons[numberOfRelayButtons]; 63 | uint8_t myRelayState[numberOfRelayButtons]; // 0 - OFF, 1 - ON 64 | unsigned long myRelayImpulseStart[numberOfRelayButtons]; 65 | int impulsePending = 0; 66 | 67 | // MySensors - Sending Data 68 | // To send data you have to create a MyMessage container to hold the information. 69 | MyMessage msgs[numberOfRelayButtons]; 70 | 71 | Bounce myButtonDebouncer[numberOfRelayButtons]; 72 | 73 | //Function Declaration 74 | uint8_t loadRelayState(int relayNum, uint8_t forceEeprom = 0); 75 | void saveRelayState(int relayNum, uint8_t state, uint8_t useEeprom); 76 | void saveRelayState(int relayNum, uint8_t state); 77 | void changeRelayState(int relayNum, uint8_t relayState); 78 | 79 | 80 | 81 | // MySensors - This will execute before MySensors starts up 82 | void before() { 83 | Serial.begin(115200); 84 | 85 | #ifdef USE_EXPANDER 86 | /* Start I2C bus and PCF8574 instance */ 87 | for(int i = 0; i < numberOfExpanders; i++) { 88 | expander[i].begin(expanderAddresses[i]); 89 | } 90 | #endif 91 | 92 | // initialize multiple buttons list structure 93 | for (int i = 0; i < numberOfRelayButtons; i++) { 94 | relayMultiButtons[i].firstButton = -1; 95 | relayMultiButtons[i].nextButton = -1; 96 | } 97 | // find multiple buttons for the same relay (uni-directional list) 98 | for (int i = 0; i < numberOfRelayButtons-1; i++) { 99 | if (relayMultiButtons[i].firstButton == -1) { 100 | int prevRelayButton = i; 101 | for (int j = i+1; j < numberOfRelayButtons; j++) { 102 | if (myRelayButtons[i].relay == myRelayButtons[j].relay) { 103 | relayMultiButtons[prevRelayButton].firstButton = i; 104 | relayMultiButtons[prevRelayButton].nextButton = j; 105 | relayMultiButtons[j].firstButton = i; 106 | prevRelayButton = j; 107 | } 108 | } 109 | } 110 | } 111 | 112 | // if version has changed, reset state of all relays 113 | int versionChangeResetState = (MULTI_RELAY_VERSION == loadState(0) ) ? 0 : 1; 114 | 115 | for (int i = 0; i < numberOfRelayButtons; i++) { 116 | // if this relay has multiple buttons, load only first 117 | if (relayMultiButtons[i].firstButton == -1 || relayMultiButtons[i].firstButton == i) { 118 | // Then set relay pins in output mode 119 | #ifdef USE_EXPANDER 120 | if ( myRelayButtons[i].relay & 0xff00 ) { 121 | // EXPANDER 122 | int expanderNo = (myRelayButtons[i].relay >> 8) - 1; 123 | int expanderPin = myRelayButtons[i].relay & 0xff; 124 | expander[expanderNo].pinMode(expanderPin, OUTPUT); 125 | } else { 126 | #endif 127 | pinMode(myRelayButtons[i].relay, OUTPUT); 128 | #ifdef USE_EXPANDER 129 | } 130 | #endif 131 | 132 | uint8_t isTurnedOn = 0; 133 | 134 | if (myRelayButtons[i].relayOptions & RELAY_STARTUP_ON) { 135 | isTurnedOn = 1; 136 | } else if (myRelayButtons[i].relayOptions & RELAY_STARTUP_OFF) { 137 | } else { 138 | // Set relay to last known state (using eeprom storage) 139 | isTurnedOn = loadRelayState(i, 1); // 1 - true, 0 - false 140 | if (versionChangeResetState && isTurnedOn) { 141 | saveRelayState(i, 0, 1); 142 | isTurnedOn = 0; 143 | } 144 | } 145 | 146 | changeRelayState(i, isTurnedOn); 147 | myRelayState[i] = isTurnedOn; 148 | } 149 | myRelayImpulseStart[i] = 0; 150 | } 151 | if (versionChangeResetState) { 152 | // version has changed, so store new version in eeporom 153 | saveState(0, MULTI_RELAY_VERSION); 154 | } 155 | } // before() 156 | 157 | // executed AFTER mysensors has been initialised 158 | void setup() { 159 | for(int i = 0; i < numberOfRelayButtons; i++) { 160 | if (myRelayButtons[i].button >= 0) { 161 | // No Expander support for buttons (de-bouncing) 162 | pinMode(myRelayButtons[i].button, INPUT_PULLUP); // HIGH state when button is not pushed 163 | } 164 | } 165 | // Send state to MySensor Gateway 166 | for(int i = 0; i < numberOfRelayButtons; i++) { 167 | // if this relay has multiple buttons, send only first 168 | if (relayMultiButtons[i].firstButton == -1 || relayMultiButtons[i].firstButton == i) { 169 | msgs[i] = MyMessage(myRelayButtons[i].sensorId, V_STATUS); 170 | uint8_t relayState; 171 | if (myRelayButtons[i].relayOptions & RELAY_STARTUP_ON) { 172 | relayState = 1; 173 | } else if (myRelayButtons[i].relayOptions & RELAY_STARTUP_OFF) { 174 | relayState = 0; 175 | } else { 176 | relayState = loadRelayState(i); 177 | } 178 | send(msgs[i].set(relayState)); // send current state 179 | } 180 | } 181 | // Setup buttons 182 | for(int i = 0; i < numberOfRelayButtons; i++) { 183 | if (myRelayButtons[i].button >= 0) { 184 | // setup debouncer 185 | //myButtonDebouncer[i] = Bounce(); 186 | myButtonDebouncer[i].attach(myRelayButtons[i].button); 187 | myButtonDebouncer[i].interval(50); 188 | } 189 | } 190 | } 191 | 192 | void loop() { 193 | for(int i = 0; i < numberOfRelayButtons; i++) { 194 | if (myRelayButtons[i].button >= 0 && myButtonDebouncer[i].update()) { 195 | int buttonState = myButtonDebouncer[i].read(); 196 | #ifdef MY_DEBUG 197 | Serial.print("# Button "); 198 | Serial.print(i); 199 | Serial.print(" changed to: "); 200 | Serial.println(buttonState); 201 | #endif 202 | 203 | int relayNum = (relayMultiButtons[i].firstButton == -1) ? i : relayMultiButtons[i].firstButton; 204 | 205 | if (myRelayButtons[i].buttonType == DING_DONG) { 206 | if (buttonState == LOW) { // button pressed 207 | changeRelayState(relayNum, 1); 208 | send(msgs[relayNum].set(1)); 209 | } else { // button released 210 | changeRelayState(relayNum, 0); 211 | send(msgs[relayNum].set(0)); 212 | } 213 | } else if (myRelayButtons[i].buttonType == REED_SWITCH) { 214 | if (buttonState == LOW) { // door/window closed 215 | changeRelayState(relayNum, 0); 216 | send(msgs[relayNum].set(0)); 217 | } else { // door/window opened 218 | changeRelayState(relayNum, 1); 219 | send(msgs[relayNum].set(1)); 220 | } 221 | } else if (myRelayButtons[i].buttonType == BI_STABLE || buttonState == MONO_STABLE_TRIGGER) { 222 | // If button type is BI_STABLE, any change will toggle relay state 223 | // For MONO_STABLE, relay is triggered based on MONO_STABLE_TRIGGER 224 | uint8_t isTurnedOn = ! loadRelayState(relayNum); // 1 - true, 0 - false 225 | changeRelayState(relayNum, isTurnedOn); 226 | send(msgs[relayNum].set(isTurnedOn)); 227 | saveRelayState(relayNum, isTurnedOn); 228 | if ((myRelayButtons[relayNum].relayOptions & RELAY_IMPULSE) && isTurnedOn) { 229 | myRelayImpulseStart[relayNum] = millis(); 230 | impulsePending++; 231 | } 232 | } 233 | } 234 | } 235 | 236 | if (impulsePending) { 237 | unsigned long currentMillis = millis(); 238 | for(int i = 0; i < numberOfRelayButtons; i++) { 239 | // Ignore multi-buttons 240 | if (relayMultiButtons[i].firstButton == -1 || relayMultiButtons[i].firstButton == i) { 241 | if ((myRelayButtons[i].relayOptions & RELAY_IMPULSE) && (myRelayImpulseStart[i] > 0)) { 242 | // the "|| (currentMillis < myRelayImpulseStart[i])" is for "millis()" overflow protection 243 | if ((currentMillis > myRelayImpulseStart[i]+RELAY_IMPULSE_INTERVAL) || (currentMillis < myRelayImpulseStart[i])) { 244 | if (myRelayState[i] == 1) { 245 | changeRelayState(i, 0); 246 | saveRelayState(i, 0); 247 | } 248 | send(msgs[i].set(0)); // send current state 249 | myRelayImpulseStart[i] = 0; 250 | impulsePending--; 251 | } 252 | } 253 | } 254 | } 255 | if (impulsePending < 0) impulsePending = 0; 256 | } 257 | } 258 | 259 | 260 | 261 | // MySensors - Presentation 262 | // Your sensor must first present itself to the controller. 263 | // The presentation is a hint to allow controller prepare for the sensor data that eventually will come. 264 | // Executed after "before()" and before "setup()" in: _begin (MySensorsCore.cpp) > gatewayTransportInit() > presentNode() 265 | void presentation() { 266 | // Send the sketch version information to the gateway and Controller 267 | sendSketchInfo("Multi Relay", "1.3"); 268 | 269 | // Register every relay as separate sensor 270 | for (int i = 0; i < numberOfRelayButtons; i++) { 271 | // if this relay has multiple buttons, register only first 272 | if (relayMultiButtons[i].firstButton == -1 || relayMultiButtons[i].firstButton == i) { 273 | // Register all sensors to gw (they will be created as child devices) 274 | // void present(uint8_t childSensorId, uint8_t sensorType, const char *description, bool ack); 275 | // childSensorId - The unique child id you want to choose for the sensor connected to this Arduino. Range 0-254. 276 | // sensorType - The sensor type you want to create. 277 | // description An optional textual description of the attached sensor. 278 | // ack - Set this to true if you want destination node to send ack back to this node. Default is not to request any ack. 279 | present(myRelayButtons[i].sensorId, S_BINARY, myRelayButtons[i].relayDescription); 280 | } 281 | } 282 | } 283 | 284 | 285 | // MySensors - Handling incoming messages 286 | // Nodes that expects incoming data, such as an actuator or repeating nodes, 287 | // must implement the receive() - function to handle the incoming messages. 288 | // Do not sleep a node where you expect incoming data or you will lose messages. 289 | void receive(const MyMessage &message) { 290 | // We only expect one type of message from controller. But we better check anyway. 291 | if (message.type == V_STATUS) { 292 | uint8_t isTurnedOn = message.getBool(); // 1 - true, 0 - false 293 | int relayNum = getRelayNum(message.sensor); 294 | if (relayNum == -1) return; 295 | changeRelayState(relayNum, isTurnedOn); 296 | if ((myRelayButtons[relayNum].relayOptions & RELAY_IMPULSE) && isTurnedOn) { 297 | myRelayImpulseStart[relayNum] = millis(); 298 | impulsePending++; 299 | } 300 | // Store state in eeprom if changed 301 | if (loadRelayState(relayNum) != isTurnedOn) { 302 | saveRelayState(relayNum, isTurnedOn); 303 | } 304 | send(msgs[relayNum].set(isTurnedOn)); // support for OPTIMISTIC=FALSE (Home Asistant) 305 | #ifdef MY_DEBUG 306 | // Write some debug info 307 | Serial.print("# Incoming change for sensor: " + relayNum); 308 | Serial.println(", New status: " + isTurnedOn); 309 | #endif 310 | } 311 | } 312 | 313 | uint8_t loadRelayState(int relayNum, uint8_t forceEeprom) { 314 | uint8_t relayState; 315 | if (forceEeprom) { 316 | relayState = loadState(RELAY_STATE_STORAGE + relayNum); 317 | } else { 318 | relayState = myRelayState[relayNum]; 319 | } 320 | #ifdef MY_DEBUG 321 | Serial.print("# loadRelayState: "); 322 | Serial.print(relayNum); 323 | if (forceEeprom) { 324 | Serial.print("(byte "); 325 | Serial.print(RELAY_STATE_STORAGE + relayNum); 326 | Serial.print(")"); 327 | } 328 | Serial.print(" = "); 329 | Serial.println(relayState); 330 | #endif 331 | return(relayState); 332 | } 333 | 334 | void saveRelayState(int relayNum, uint8_t state, uint8_t useEeprom) { 335 | 336 | int mainRelayNum = (relayMultiButtons[relayNum].firstButton == -1) ? relayNum : relayMultiButtons[relayNum].firstButton; 337 | 338 | myRelayState[mainRelayNum] = state; 339 | if (useEeprom && (relayNum == mainRelayNum)) { 340 | saveState(RELAY_STATE_STORAGE + mainRelayNum, state); 341 | } 342 | 343 | int nextButton = mainRelayNum; 344 | // update all buttons 345 | while ((nextButton = relayMultiButtons[nextButton].nextButton) != -1) { 346 | myRelayState[nextButton] = state; 347 | }; 348 | } 349 | 350 | void saveRelayState(int relayNum, uint8_t state) { 351 | uint8_t useEeprom = ((myRelayButtons[relayNum].relayOptions & RELAY_STARTUP_MASK) == 0); 352 | saveRelayState(relayNum, state, useEeprom); 353 | } 354 | 355 | void changeRelayState(int relayNum, uint8_t relayState) { 356 | 357 | uint8_t relayTrigger = myRelayButtons[relayNum].relayOptions & RELAY_TRIGGER_HIGH; 358 | uint8_t digitalOutState = relayState ? relayTrigger : ! relayTrigger; 359 | 360 | #ifdef USE_EXPANDER 361 | if ( myRelayButtons[relayNum].relay & 0xff00 ) { 362 | int expanderNo = (myRelayButtons[relayNum].relay >> 8) - 1; 363 | int expanderPin = myRelayButtons[relayNum].relay & 0xff; 364 | expander[expanderNo].digitalWrite(expanderPin, digitalOutState); 365 | } else { 366 | #endif 367 | digitalWrite(myRelayButtons[relayNum].relay, digitalOutState); 368 | #ifdef USE_EXPANDER 369 | } 370 | #endif 371 | } 372 | 373 | int getRelayNum(int sensorId) { 374 | for (int i = 0; i < numberOfRelayButtons; i++) { 375 | if (myRelayButtons[i].sensorId == sensorId) return(i); 376 | } 377 | return(-1); 378 | } 379 | -------------------------------------------------------------------------------- /config.h.sample: -------------------------------------------------------------------------------- 1 | // For mono-stable buttons it will trigger state: LOW - when button is pressed, HIGH - when button is released 2 | const uint8_t MONO_STABLE_TRIGGER = LOW; 3 | 4 | // time interval for RELAY_IMPULSE type relay, ignored when button type is DING_DONG or REED_SWITCH 5 | unsigned long RELAY_IMPULSE_INTERVAL = 250; 6 | 7 | 8 | // Row params: sensor ID - sensor ID reported on MySensor Gateway 9 | // relay pin - Expander supported 10 | // button pin - <0 for virtual buttons (only available in MySensor Gateway); no support for Expander 11 | // relay options - [RELAY_TRIGGER_LOW|RELAY_TRIGGER_HIGH] {RELAY_STARTUP_ON|RELAY_STARTUP_OFF} {RELAY_IMPULSE} 12 | // button type - [MONO_STABLE|BI_STABLE|DING_DONG|REED_SWITCH] 13 | // relay description - reported on MySensor Gateway, can help identify device on initial configuration in Home Automation App, can be empty ("") 14 | RelayButton myRelayButtons[] = { 15 | {0, 4, A0, RELAY_TRIGGER_LOW, MONO_STABLE, "Ł2 - kinkiet"}, // WŁ: Ł2 16 | {1, 19, A1, RELAY_TRIGGER_LOW, BI_STABLE, "Salon 2"}, // WŁ: Salon prawy 17 | {2, 18, A2, RELAY_TRIGGER_LOW, BI_STABLE, "Salon 1"}, // WŁ: Salon lewy 18 | {3, 14, A3, RELAY_TRIGGER_LOW | RELAY_STARTUP_OFF, BI_STABLE, "Halogen - Taras"}, // WŁ: Taras 19 | {4, 25, A4, RELAY_TRIGGER_LOW, BI_STABLE, "Kuchnia"}, // WŁ: Kuchnia lewy 20 | {5, 26, A5, RELAY_TRIGGER_LOW, BI_STABLE, "Kuchnia - Kinkiet"}, // WŁ: Kuchnia prawy 21 | {6, 31, A6, RELAY_TRIGGER_LOW, BI_STABLE, "Jadalnia 2"}, // WŁ: Hall I/Jadalnia prawy 22 | {17, 22, A7, RELAY_TRIGGER_LOW, BI_STABLE, "Ł1 - Kinkiet"}, // WŁ: Hall I/Ł1 prawy 23 | {8, 34, A8, RELAY_TRIGGER_LOW, MONO_STABLE, "Garaż"}, // WŁ: Kotłownia/Garaż 24 | {8, 34, A9, RELAY_TRIGGER_LOW, MONO_STABLE, "Garaż"}, // WŁ: Garaż 25 | {10, 3, A10, RELAY_TRIGGER_LOW | RELAY_STARTUP_ON, BI_STABLE, "Halogen - wejście"}, // WŁ: Drzwi wejściowe 26 | {12, 32, A12, RELAY_TRIGGER_LOW, BI_STABLE, "Hall 1"}, // WŁ: Hall I/Jadalnia lewy 27 | {12, 32, A13, RELAY_TRIGGER_LOW, BI_STABLE, "Hall 1"}, // WŁ: Hall I/Wiatrołap 28 | {14, 35, A14, RELAY_TRIGGER_LOW, BI_STABLE, "Wiatrołap"}, // WŁ: Wiatrołap/Hall I 29 | {15, 24, A15, RELAY_TRIGGER_LOW, MONO_STABLE, "Kotłownia"}, // WŁ: Kotłownia/Hall I 30 | {16, 27, 53, RELAY_TRIGGER_LOW, BI_STABLE, "Ł1 - Taśma LED"}, // WŁ: Hall I/Ł1 środek 31 | {17, 22, 52, RELAY_TRIGGER_LOW, MONO_STABLE, "Ł1 - Kinkiet"}, // WŁ: Ł1 32 | {18, 23, 51, RELAY_TRIGGER_LOW, BI_STABLE, "Ł1"}, // WŁ: Hall I/Ł1 lewy 33 | {19, 8, 50, RELAY_TRIGGER_LOW, BI_STABLE, "Klatka Schodowa"}, // WŁ: Hall I/Schody prawy 34 | {12, 32, 49, RELAY_TRIGGER_LOW, BI_STABLE, "Hall 1"}, // WŁ: Hall I/Schody lewy 35 | {21, 29, 48, RELAY_TRIGGER_LOW, BI_STABLE, "Gabinet"}, // WŁ: Gabinet 36 | {22, 9, 47, RELAY_TRIGGER_LOW, BI_STABLE, "Hall 2"}, // WŁ: Hall II/Schody prawy 37 | {19, 8, 46, RELAY_TRIGGER_LOW, BI_STABLE, "Klatka Schodowa"}, // WŁ: Hall II/Schody lewy 38 | {24, 11, 45, RELAY_TRIGGER_LOW, BI_STABLE, "Garderoba"}, // WŁ: Garderoba 39 | {26, 7, 43, RELAY_TRIGGER_LOW, BI_STABLE, "Pok. nad kuchnią 1"}, // WŁ: Pok. nad kuchnią 1 40 | {27, 10, 42, RELAY_TRIGGER_LOW, BI_STABLE, "Pok. nad salonem 2"}, // WŁ: Pok. nad salonem 2 41 | {29, 5, 40, RELAY_TRIGGER_LOW, BI_STABLE, "Ł2"}, // WŁ: Hall II/Ł2 lewy 42 | {30, 6, 39, RELAY_TRIGGER_LOW, BI_STABLE, "Ł2 - Taśma LED"}, // WŁ: Hall II/Ł2 prawy 43 | {22, 9, 38, RELAY_TRIGGER_LOW, BI_STABLE, "Hall 2"}, // WŁ: Hall II/Sypialnia 44 | {32, 12, 37, RELAY_TRIGGER_LOW, BI_STABLE, "Sypialnia 2"}, // WŁ: Sypialnia 2 45 | {34, 28, -1, RELAY_TRIGGER_LOW | RELAY_STARTUP_ON, MONO_STABLE, "Halogen - Garaż"}, // WŁ: Virtual Button 1 46 | {35, 30, -2, RELAY_TRIGGER_LOW | RELAY_STARTUP_OFF, MONO_STABLE, "Ł1 - Wentylator"}, // WŁ: Virtual Button 2 47 | {36, 15, -3, RELAY_TRIGGER_LOW | RELAY_STARTUP_OFF, MONO_STABLE, "Halogen - wschód"}, // WŁ: Virtual Button 3 48 | {40, 2, -7, RELAY_TRIGGER_LOW | RELAY_STARTUP_OFF, MONO_STABLE, "Ł2 - wentylator"}, // WŁ: Virtual Button 7 49 | }; 50 | -------------------------------------------------------------------------------- /images/node-red-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lkankowski/arduino-multi-relay/a9221e6cde249fee6580c5019780c2a9a16e6176/images/node-red-flow.png --------------------------------------------------------------------------------