This project was created for a "SONOFF 4CH Rev2 4 Channel Wireless WiFi Smart Switch", costing just under £15 free P&P
It has DIN Rail mounting for eq: a consumer unit, plus 4 corner fixing holes, so fixing should not be a problem.
Mains connection uses 3 groups of spring-loaded terminals, but wires need to be anchored with some sort of strain-relief.
The top cover can be removed with the wires still connected, and there is comfortable room inside for doing some mods such as soldering wires on the underside of the 4 pushbuttons to add extended external pushbuttons for eg: wall switches if wished.
It uses an ESP-8285 which contains internal 1Mb Flash, and allows it to also utilise gpio's 9 and 10.
4 gpio's are used for the 4 relays, 4 gpio's for the 4 push-button switches, and gpio13 as a user LED... plus the flashing header also offers gpio2 (total 10 gpio's)
The 4 LED channel indicators are driven by the relay actuation signals, but the 5v relays are powered from the onboard Mains PSU.
So the relays only operate with Mains applied, which prevents the 'mains In' (which is commoned to all 4 relays) from being driven by eg: 12v instead.
However, there is an unpopulated DC input jack which presumably was originally intended as an alternative low voltage relay supply instead of the onboard mains
Flashing
Follow the instructions at the bottom of the Hints Tips Gotcha's page if you wish to backup the original firmware before flashing with Annex,
The unpopulated programming header requires 5 pins soldering for GND, TX, RX, 3.3V, gpio2 (gpio2 is handy for adding eg: a temperature or light sensor).
Use the Channel 1 gpio0 button to enter flashing mode at power-up, my FTDI 3.3v supplied the Sonoff for flashing and all development... Annex offers the great advantage of programming over Wifii - which does not require a Mains connection - so there is no excuse for electrocuting yourself !
Keep the original device parameters, so use the Toolkit Blue button to read the existing details, then use the Green button to flash just the firmware.
Manually enter flashing mode each time before using the Blue and Green Toolkit buttons, and allow time for Annex to format the empty SPIFFS before rebooting.
Copy and paste the script below and save to whatever name you choose, then add that /path/filename into the Config page autorun field and Save it.
If everything is ok, you should see a blinking blue LED after rebooting.
Script Notes
EasyNet functionality is available if wished, but can just be ignored if not needed.
Commands are: R1on R1off R1toggle R2on R2off R2toggle R3on R3off R3toggle R4on R4off R4toggle Allon Alloff plus Reply BlinkIP Blink
This project has not been included in with the other EasyNet projects because eventually I plan to do another more 'interactive' version.
So don't worry about any script 'shadows' which may not be fully implemented - the demo video and script comments should explain most of what is available.
If the webpage is to be used on mobiles etc then disable the menu bar in Config (and add the script filename into autorun while you're at it).
Note that subsequent webpage re-connections should actually reflect the status of the relays (rather than assume defaults).
Gpio13 'user' LED is used as a blinking heartbeat confidence signal for reassurance that the device has not hung, but it could be used for other things if wished.
Similarly there is a blinking 'confidence' LED on the webpage (which can be disabled if preferred).
Note: Developed on 1.39 beta 1, and does not work on version 1.39 beta 2 because of a firmware bug, but should hopefully be fixed in the next release.
Basic:
title$ = "4-Channel Relay v1.0, by Electroguard"
nodename$ = "" 'Assign a unique node name of your choice (if you forget, it will be called "Node" + its node IP)
groupname$ = "Sonoff\SmartSocket\Relay" 'concatenated group names are searched for a partial match
localIP$ = WORD$(IP$,1)
netIP$ = WORD$(localIP$,1,".") + "." + WORD$(localIP$,2,".") + "." + WORD$(localIP$,3,".") + "."
nodeIP$ = WORD$(localIP$,4,".")
udpport = 5001 'change to suit your own preference, but don't forget to do the same for all nodes
if nodename$ = "" then nodename$ = "Node" + nodeIP$
showsettings = 0
showtitle = 1
showID = 0 '=1 to show local identity info
showbuttons = 1 '=1 to show onscreen system buttons
showall = 1 '=1 to show AllON/AllOff buttons and heartbeat indicator
instructionslist$ = "Reply BlinkIP Blink " 'List of Subdir branches available as remote triggers
instructionslist$ = instructionslist$ + "R1On R1Off R1toggle R2On R2Off R2toggle R3On R3Off R3toggle R4On R4Off R4toggle AllOn AllOff " 'local shared instruction subdirs
instruction$ = "" 'variable to hold incoming instruction
sendmsg$ = "All Reply" 'udp message to send
RXmsg$ = "" 'variable to hold incoming message
data$ = "" 'variable to hold any incoming data after the instruction
queued = 0 '0=broadcast, 1=queued handshake retryq$ = "" 'variable to hold all unexpired messages still waiting to be acknowledgedqdelimiter$ = "|" 'separates messages in the retryqtime2live = 5 'sent-message unacknowledged lifetime in seconds'msgID$ = "" 'unique msg ID consists of send date+time + time2live - also acts as msg 'expire' time flaguserled = 0led1pin = 13: led1off = 1: pin.mode led1pin, output: pin(led1pin) = led1offrelayoff = 0: 'relays are all normally low going active high relay1pin = 12: pin.mode relay1pin, output: pin(relay1pin) = relayoff relay2pin = 5: pin.mode relay2pin, output: pin(relay2pin) = relayoff relay3pin = 4: pin.mode relay3pin, output: pin(relay3pin) = relayoff relay4pin = 15: pin.mode relay4pin, output: pin(relay3pin) = relayoff buttonoff = 1: 'buttons are all normally high going active lowbutton1pin = 0: pin.mode button1pin, input, pullupbutton2pin = 9: pin.mode button2pin, input, pullup button3pin = 10: pin.mode button3pin, input, pullup button4pin = 14: pin.mode button4pin, input, pullup interrupt button1pin, b1pressedinterrupt button2pin, b2pressedinterrupt button3pin, b3pressedinterrupt button4pin, b4pressedindcol$ = "green"userledoff$ = "Gainsboro": userledon$ = "DeepSkyBlue"blinks = 10 'blink default number of blinks, can be over-ridden by sending "nodename blink number_of_blinks"gosub paintonhtmlchange changedonhtmlreload painttimer0 1500, heartbeat timer1 1000, Retry 'periodic timer to keep resending unACKed msgs until they expire udp.begin(udpport)onudp udpRX'wlog "Started: " + time$ + " on " + date$waitpaint:clsautorefresh 1500a$ = a$ + |<br><div id='message' data-var='clicked' onclickx='cmdButton(this)' style='display: table; margin-right:auto;margin-left:auto;text-align:center;'>|if showtitle = 1 then a$ = a$ + title$ + "<br><br>"if showID = 1 then a$ = a$ + |<table align='center'><tr><td>| a$ = a$ + |Node name:</td><td>| + textbox$(nodename$,"tbname") + |</td></tr><tr><td>| a$ = a$ + cssid$("tbname", "color:Darkcyan;font-size:1.2em;width:150px;") a$ = a$ + |local IP:</td><td>| + localIP$ + |</td></tr><tr><td>| a$ = a$ + |UDP port:</td><td>| + textbox$(udpport,"tb40") + |</td></tr></td></tr></table><br><br>|endifif showbuttons = 1 then a$ = a$ + button$("Instant On",r1on) + string$(9," ") + button$("Toggle", r1toggle, "ind1") + string$(9," ") + button$("Instant Off",r1off) + |<br><br>| a$ = a$ + button$("Instant On",r2on) + string$(9," ") + button$("Toggle", r2toggle, "ind2") + string$(9," ") + button$("Instant Off",r2off) + |<br><br>| a$ = a$ + button$("Instant On",r3on) + string$(9," ") + button$("Toggle", r3toggle, "ind3") + string$(9," ") + button$("Instant Off",r3off) + |<br><br>| a$ = a$ + button$("Instant On",r4on) + string$(9," ") + button$("Toggle", r4toggle, "ind4") + string$(9," ") + button$("Instant Off",r4off) + |<br><br>| a$ = a$ + cssid$("ind1", "height:3em; font-size:1.5em; border-radius:.4em; padding:.5em; color:white; background:" + indcol$ + ";") a$ = a$ + cssid$("ind2", "height:3em; font-size:1.5em; border-radius:.4em; padding:.5em; color:white; background:" + indcol$ + ";") a$ = a$ + cssid$("ind3", "height:3em; font-size:1.5em; border-radius:.4em; padding:.5em; color:white; background:" + indcol$ + ";") a$ = a$ + cssid$("ind4", "height:3em; font-size:1.5em; border-radius:.4em; padding:.5em; color:white; background:" + indcol$ + ";")if pin(relay1pin) = relayoff then a$ = a$ + cssid$("ind1", "background:green;") else a$ = a$ + cssid$("ind1", "background:red;") if pin(relay2pin) = relayoff then a$ = a$ + cssid$("ind2", "background:green;") else a$ = a$ + cssid$("ind2", "background:red;") if pin(relay3pin) = relayoff then a$ = a$ + cssid$("ind3", "background:green;") else a$ = a$ + cssid$("ind3", "background:red;") if pin(relay4pin) = relayoff then a$ = a$ + cssid$("ind4", "background:green;") else a$ = a$ + cssid$("ind4", "background:red;") endifif showall = 1 then a$ = a$ + button$("Allt On",Allon) + string$(9," ") a$ = a$ + "<svg height='20' width='30'><circle cx='15' cy='10' r='9' stroke='black' fill='" + userledoff$ + "' id='userled'" + "/></svg>" a$ = a$ + string$(9," ") + button$("All Off",Alloff) + |<br><br>| endifa$ = a$ + "<br>ShowSettings:" + checkbox$(showsettings)if showsettings = 1 then a$=a$+", ShowTitle:"+checkbox$(showtitle)+", ShowID:"+checkbox$(showid)+", Buttons:"+checkbox$(showbuttons)+", All buttons:"+checkbox$(showall)endifa$ = a$ + cssid$("tb30", "width:30; text-align:center; color:teal; background:GhostWhite;")a$ = a$ + cssid$("tb40", "width:40; text-align:center; color:teal; background:GhostWhite;")a$ = a$ + cssid$("tb60", "width:60; text-align:center; color:teal; background:GhostWhite;")a$ = a$ + cssid$("tb80", "width:80; text-align:center; color:teal; background:GhostWhite;")a$ = a$ + |</div>|html a$a$ = ""returnchanged:ch$ = HtmlEventVar$if instr(ch$,"show") = 1 then gosub paintif instr(ch$,"blinks") = 1 then refresh data$ = str$(blinks) gosub blinkendifreturnheartbeat:if showall = 1 then userled = 1 - userled if userled = 1 then html CSSID$("userled", "fill: " + userledon$ ) else html CSSID$("userled", "fill: " + userledoff$ )endifpin(led1pin) = 1 - led1offpause 70pin(led1pin) = led1offreturnudpRX:RXmsg$ = udp.read$if ucase$(word$(RXmsg$,1)) = "ACK" then gosub ACK 'echoed reply from successfully received message, original msg can be removed from queue else target$ = ucase$(word$(RXmsg$,1)) 'Target may be NodeName or GroupName or "ALL" or localIP address if (target$=localIP$) OR (target$=ucase$(nodename$)) OR (instr(ucase$(groupname$),target$)>0) OR (target$="ALL") then instruction$ = trim$(ucase$(word$(RXmsg$,2))) 'Instruction is second word of message data$ = "": getdata data$,RXmsg$," ",2 'extract any data that follows the instruction if word.find(ucase$(instructionslist$),instruction$) > 0 then if (ucase$(instruction$) <> "ACK") and (instr(ucase$(data$),"ID=") > 0) then udp.reply "ACK " + RXmsg$ 'ACKnowledge the incoming msg endif gosub instruction$ 'branch to action the corresponding instruction subroutine else udp.reply RXmsg$ + " INSTRUCTION NOT RECOGNISED" endif 'word.find endif '(target$=localIP$)endif 'ACKreturnACK:msg$ = "": getdata msg$, RXmsg$, " ", 1wlog "Ack recvd for " + msg$pos = word.find(retryq$,msg$,qdelimiter$)if pos > 0 then retryq$ = word.delete$(retryq$,pos,qdelimiter$)returnRETRY:if word.count(retryq$, qdelimiter$) > 0 thenif retryq$ <> "" then wlog "queue=" + retryq$ msg$ = word$(retryq$,1,qdelimiter$) 'grab first unACKed msg in the queue retryq$ = word.delete$(retryq$,1,qdelimiter$) 'chop msg off front of queue expire$ = "" WordParse expire$, msg$, "ID=", " " 'parse out ID= expire time if msg$ <> "" then 'compare expire time to current unix time if dateunix(date$) + timeunix(time$) > val(expire$) then Send "LOG ERROR: Node " + Nodename$ + " FAILED SEND - " + msg$ + " not ACKnowledged" else retryq$ = retryq$ + msg$ + qdelimiter$ udp.write netip$ + "255", udpport, msg$ wlog "retry " + msg$ endif endifendifreturnsub SendQ(sendmsg$) sendmsg$ = sendmsg$ + " ID=" + str$(dateunix(date$) + timeunix(time$) + time2live, "%10d", 1)retryq$ = retryq$ + sendmsg$ + qdelimiter$udp.write netip$ + "255", udpport, sendmsg$end subsub Send(sendmsg$)udp.write netip$ + "255", udpport, sendmsg$end subsub GetData(ret$, v$, sep$, pos) 'extracts everything from the msg after the Instruction and puts into data$ (thanks cicciocb)local i, p, qp = 1for i = 1 to pos p = instr(p + 1, v$, sep$) if p > 0 then p = p + len(sep$)next iif p = 0 then ret$ = ""else q = instr(p+1, v$, sep$) if q = 0 then q = 999 ret$ = mid$(v$, p)end if end subsub WordParse(ret$, full$, search$, sep$) 'extracts value from option=value (thanks cicciocb)local p, b$p = instr(full$, search$)if p <> 0 then b$ = mid$(full$, p + len(search$)) ret$ = word$(b$, 1, sep$)else ret$ = ""end ifend subblink:if data$ <> "" then blinks = val(data$) ledstate = pin(led1pin)pin(led1pin) = led1offpause 200for count = 1 to blinksif led1off = 1 then pin(led1pin) = 0 else pin(led1pin) = 1 pause 800 pin(led1pin) = led1off pause 200next countpause 2000pin(led1pin) = ledstate 'Restore LED state to its previous statereturnblinkip:ledstate = pin(led1pin)blinkon = 150blinkoff = 300blinkpause = 1000blinkgap = 1400pin(led1pin) = led1offpause blinkpausefor pos = 1 to len(localIP$) digitchr$ = mid$(localIP$,pos,1) if digitchr$ = "." then pause blinkgap else if digitchr$ = "0" then digit = 10 else digit = val(digitchr$) for count = 1 to digit if led1off = 0 then pin(led1pin) = 1 else pin(led1pin) = 0 pause blinkon if led1off = 0 then pin(led1pin) = 0 else pin(led1pin) = 1 pause blinkoff next count pause blinkpause end ifnext pospause blinkgappin(led1pin) = ledstatereturnREPLY:udp.reply "Reply from " + Nodename$returnAllOn:if pin(relay1pin) = relayoff then pin(relay1pin) = not relayoff: html cssid$("ind1", "background:red;")if pin(relay2pin) = relayoff then pin(relay2pin) = not relayoff: html cssid$("ind2", "background:red;")if pin(relay3pin) = relayoff then pin(relay3pin) = not relayoff: html cssid$("ind3", "background:red;")if pin(relay4pin) = relayoff then pin(relay4pin) = not relayoff: html cssid$("ind4", "background:red;")refresh returnAllOff:if pin(relay1pin) <> relayoff then pin(relay1pin) = relayoff: html cssid$("ind1", "background:green;")if pin(relay2pin) <> relayoff then pin(relay2pin) = relayoff: html cssid$("ind2", "background:green;")if pin(relay3pin) <> relayoff then pin(relay3pin) = relayoff: html cssid$("ind3", "background:green;")if pin(relay4pin) <> relayoff then pin(relay4pin) = relayoff: html cssid$("ind4", "background:green;")refresh returnR1on:if pin(relay1pin) = relayoff then pin(relay1pin) = not relayoff: html cssid$("ind1", "background:red;"): refresh returnR2on:if pin(relay2pin) = relayoff then pin(relay2pin) = not relayoff: html cssid$("ind2", "background:red;"): refresh returnR3on:if pin(relay3pin) = relayoff then pin(relay3pin) = not relayoff: html cssid$("ind3", "background:red;"): refresh returnR4on:if pin(relay4pin) = relayoff then pin(relay4pin) = not relayoff: html cssid$("ind4", "background:red;"): refresh returnR1off:if pin(relay1pin) <> relayoff then pin(relay1pin) = relayoff: html cssid$("ind1", "background:green;"): refresh returnR2off:if pin(relay2pin) <> relayoff then pin(relay2pin) = relayoff: html cssid$("ind2", "background:green;"): refresh returnR3off:if pin(relay3pin) <> relayoff then pin(relay3pin) = relayoff: html cssid$("ind3", "background:green;"): refresh returnR4off:if pin(relay4pin) <> relayoff then pin(relay4pin) = relayoff: html cssid$("ind4", "background:green;"): refresh returnR1toggle:if pin(relay1pin) = relayoff then gosub R1on else gosub R1offreturnR2toggle:if pin(relay2pin) = relayoff then gosub R2on else gosub R2offreturnR3toggle:if pin(relay3pin) = relayoff then gosub R3on else gosub R3offreturnR4toggle:if pin(relay4pin) = relayoff then gosub R4on else gosub R4offreturnb1pressed:'wlog "1"if pin(button1pin) = buttonoff then gosub R1togglereturnb2pressed:'wlog "2"if pin(button2pin) = buttonoff then gosub R2togglereturnb3pressed:'wlog "3"if pin(button3pin) = buttonoff then gosub R3togglereturnb4pressed:'wlog "4"if pin(button4pin) = buttonoff then gosub R4togglereturnEND '-------------------- End ---------------------