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)
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 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).
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 acknowledged qdelimiter$ = "|" 'separates messages in the retryq time2live = 5 'sent-message unacknowledged lifetime in seconds 'msgID$ = "" 'unique msg ID consists of send date+time + time2live - also acts as msg 'expire' time flag userled = 0 led1pin = 13: led1off = 1: pin.mode led1pin, output: pin(led1pin) = led1off relayoff = 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 low button1pin = 0: pin.mode button1pin, input, pullup button2pin = 9: pin.mode button2pin, input, pullup button3pin = 10: pin.mode button3pin, input, pullup button4pin = 14: pin.mode button4pin, input, pullup interrupt button1pin, b1pressed interrupt button2pin, b2pressed interrupt button3pin, b3pressed interrupt button4pin, b4pressed indcol$ = "green" userledoff$ = "Gainsboro": userledon$ = "DeepSkyBlue" blinks = 10 'blink default number of blinks, can be over-ridden by sending "nodename blink number_of_blinks" gosub paint onhtmlchange changed onhtmlreload paint timer0 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$ wait paint: cls autorefresh 1500 a$ = 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>| endif if 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;") endif if 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>| endif a$ = a$ + "<br>ShowSettings:" + checkbox$(showsettings) if showsettings = 1 then a$=a$+", ShowTitle:"+checkbox$(showtitle)+", ShowID:"+checkbox$(showid)+", Buttons:"+checkbox$(showbuttons)+", All buttons:"+checkbox$(showall) endif a$ = 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$ = "" return changed: ch$ = HtmlEventVar$ if instr(ch$,"show") = 1 then gosub paint if instr(ch$,"blinks") = 1 then refresh data$ = str$(blinks) gosub blink endif return heartbeat: if showall = 1 then userled = 1 - userled if userled = 1 then html CSSID$("userled", "fill: " + userledon$ ) else html CSSID$("userled", "fill: " + userledoff$ ) endif pin(led1pin) = 1 - led1off pause 70 pin(led1pin) = led1off return udpRX: 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 'ACK return ACK: msg$ = "": getdata msg$, RXmsg$, " ", 1 wlog "Ack recvd for " + msg$ pos = word.find(retryq$,msg$,qdelimiter$) if pos > 0 then retryq$ = word.delete$(retryq$,pos,qdelimiter$) return RETRY: if word.count(retryq$, qdelimiter$) > 0 then if 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 endif endif return sub SendQ(sendmsg$) sendmsg$ = sendmsg$ + " ID=" + str$(dateunix(date$) + timeunix(time$) + time2live, "%10d", 1) retryq$ = retryq$ + sendmsg$ + qdelimiter$ udp.write netip$ + "255", udpport, sendmsg$ end sub sub Send(sendmsg$) udp.write netip$ + "255", udpport, sendmsg$ end sub sub GetData(ret$, v$, sep$, pos) 'extracts everything from the msg after the Instruction and puts into data$ (thanks cicciocb) local i, p, q p = 1 for i = 1 to pos p = instr(p + 1, v$, sep$) if p > 0 then p = p + len(sep$) next i if p = 0 then ret$ = "" else q = instr(p+1, v$, sep$) if q = 0 then q = 999 ret$ = mid$(v$, p) end if end sub sub 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 if end sub blink: if data$ <> "" then blinks = val(data$) ledstate = pin(led1pin) pin(led1pin) = led1off pause 200 for count = 1 to blinks if led1off = 1 then pin(led1pin) = 0 else pin(led1pin) = 1 pause 800 pin(led1pin) = led1off pause 200 next count pause 2000 pin(led1pin) = ledstate 'Restore LED state to its previous state return blinkip: ledstate = pin(led1pin) blinkon = 150 blinkoff = 300 blinkpause = 1000 blinkgap = 1400 pin(led1pin) = led1off pause blinkpause for 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 if next pos pause blinkgap pin(led1pin) = ledstate return REPLY: udp.reply "Reply from " + Nodename$ return AllOn: 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 return AllOff: 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 return R1on: if pin(relay1pin) = relayoff then pin(relay1pin) = not relayoff: html cssid$("ind1", "background:red;"): refresh return R2on: if pin(relay2pin) = relayoff then pin(relay2pin) = not relayoff: html cssid$("ind2", "background:red;"): refresh return R3on: if pin(relay3pin) = relayoff then pin(relay3pin) = not relayoff: html cssid$("ind3", "background:red;"): refresh return R4on: if pin(relay4pin) = relayoff then pin(relay4pin) = not relayoff: html cssid$("ind4", "background:red;"): refresh return R1off: if pin(relay1pin) <> relayoff then pin(relay1pin) = relayoff: html cssid$("ind1", "background:green;"): refresh return R2off: if pin(relay2pin) <> relayoff then pin(relay2pin) = relayoff: html cssid$("ind2", "background:green;"): refresh return R3off: if pin(relay3pin) <> relayoff then pin(relay3pin) = relayoff: html cssid$("ind3", "background:green;"): refresh return R4off: if pin(relay4pin) <> relayoff then pin(relay4pin) = relayoff: html cssid$("ind4", "background:green;"): refresh return R1toggle: if pin(relay1pin) = relayoff then gosub R1on else gosub R1off return R2toggle: if pin(relay2pin) = relayoff then gosub R2on else gosub R2off return R3toggle: if pin(relay3pin) = relayoff then gosub R3on else gosub R3off return R4toggle: if pin(relay4pin) = relayoff then gosub R4on else gosub R4off return b1pressed: 'wlog "1" if pin(button1pin) = buttonoff then gosub R1toggle return b2pressed: 'wlog "2" if pin(button2pin) = buttonoff then gosub R2toggle return b3pressed: 'wlog "3" if pin(button3pin) = buttonoff then gosub R3toggle return b4pressed: 'wlog "4" if pin(button4pin) = buttonoff then gosub R4toggle return END '-------------------- End --------------------- |