Dnes Vám představím velice povedený kousek čínské elektroniky, který si díky troše snažení můžete přeprogramovat k obrazu svému. Navíc jde o funkční celek, takže stačí jen upravit desku s plošným spojem, přeprogramovat elektroniku a máte krásný kousek Wi-Fi spínače na 220V, a to vše za velmi příjemnou cenu.
POZOR
Ač se celé zařízení dá rozebrat a testovat a programovat s bezpečným napětím, přesto se jedná o elektroniku připravenou na připojení na napětí 220V. Zapojení a provozování přístroje s takovým napětím vyžaduje znalost elektrikářských předpisů. Pokud takovými znalostmi nedisponujete, měli byste před připojením upraveného přístroje nechat zapojení zkontrolovat elektrikářem. Elektrické spotřebiče po špatné upravě mohou i zabíjet, proto to neberte na lehkou váhu!
Již před letošní zimou jsem přemýšlel, jak co nejjednodušeji pomocí prostředí Arduina naprogramovat spínání spotřebičů na 220V pomocí Wi-Fi. Složit podobnou elektroniku už přece jen chce pár součástek. Moduly ESP8266, které doma běžně používám, totiž mají nízké napětí i malý výkon na to, aby mohly spínat relátko, a tak bylo potřeba nejen naprogramovat samotný program do čipu, ale také složit veškerá spínání a relátka a to vše ještě pěkně pospolu uložit do nějaké krabičky, kde by to, pokud možno, nějaký ten čas odolalo. Nakonec jsem na to nenašel čas, a tak jsem ovládání pomocí ESP modulu odložil.
Přitom existuje krásný kus čínské elektroniky, který již všechno v sobě má a stačí jej jen zapojit a používat, pokud však stejně jako já nechcete být závislí na aplikacích výrobce a jeho rozhraní, dá se tento kus elektroniky v celku jednoduše přeprogramovat. Krásou celého systému je nejen jednoduchost, se kterou lze celý systém přeprogramovat k obrazu svému, ale také cena celého zařízení nesoucí jméno sonoff basic pohybující se okolo 6 dolarů.
Na zařízení jsem narazil na stránkách portálu živě.cz v seriálu o programování elektroniky. Hned jsem si tedy relátko objednal na webu www.itead.cc. Jakmile dorazil balíček z Číny, pustil jsem se do oživení plánů, které jsem před zimou opustil. Na jmenovaných stránkách portálu Živě.cz je krom postupu a popisu zapojení také zveřejněn zdrojový kód. Z něj jsem z velké části vycházel, ovšem trochu jsem jej poupravil, aby správně fungoval.
Nejdříve však bylo nutné k relátku připájet kontakty, aby jej bylo možné spojit pomocí USB převodníku a nahrát na něj upravený kód. I když je relátko připojeno na 220V, má vlastní logiku na 3.3V a po připojení USB převodníku lze programovat bez nutnosti připojení velkého napětí. Celá elektronika krom spínacího relátka se totiž dá vcelku jednoduše napájet právě z připojeného převodníku pomocí USB.
Po připojení k USB portu s připojeným USB převodníkem je nutné nastavit prostředí Arduino na správnou vývojovou desku, a to „Generic ESP8285“ a další potřebné údaje. Chvíli mi trvalo, než jsem narazil na správné nastavení, které u mého počítače fungovalo
Jak jsem již psal, vycházel jsem z úpravy zdrojového kódu zveřejněného na portále Živě.cz který jsem upravil dle vlastních potřeb. Relátko se rozbliká, zobrazí vlastní Wi-Fi, pomocí které jej můžete připojit do své bezdrátové sítě. Poté, co se připojí, si stáhne pomocí funkce TimeLib z časového serveru aktuální čas, ten se poté využívá pro automatické vypínání a spínání relátka a tento čas si sama elektronika jednou za hodinu zaktualizuje pomocí českého NTP serveru.
Autor článku poté připojil do elektroniky obvod pro měření teploty vně relátka, nicméně já se rozhodl, že volný výstup z GP014 zatím nechám neobsazen. Integrované tlačítko, pomocí kterého se nahrává kód do paměti, jsem se rozhodl využít pro ruční spínání, pokud by bylo potřeba relátko sepnout či vypnout a neměl bych po ruce zrovna telefon.
Při testování programu zveřejněného na jmenovaném portálu jsem také zjistil, že autor nebral při psaní programu v potaz letní čas, a tak je během letního času čas na relátku špatně. Podle dostupných informací se totiž NTP servery nestarají o letní nebo zimní časové nastavení, ale vždy vracejí aktuální čas v zimním a je tedy na programu nebo příjemci NTP nastavení, aby k časové zóně v letním období přidali +1. Po dlouhém přemýšlení a studiu možného řešení jsem se nakonec rozhodl, že v paměti relátka vyčlením jednu adresu a zde budu manuálně z webového rozhraní zapínat nebo vypínat letní čas tak, aby byl vždy aktuální. Je to sice polovičaté řešení, nicméně plně plní funkci a navíc pomocí adresy API se dá později zautomatizovat
V případě změny času by se prostě zavolala adresa spínače a čas se nastavil. Popřípadě také může celé relátko v pořádku fungovat i se špatným časem :-).
Taky jsem přeprogramoval diodu, aby v případě, že relátko sepne, svítila a v případě, že je vypnuté, aby zhasla. Tak je jednoduše na první pohled vidět, jestli je relé zrovna vypnuto, nebo sepnuto. Jen poznamenám, že jakmile připojíte převodník, musíte podržet tlačítko GPI0 0, abyste mohly nahrát nový program.
Spotřeba zařízení
Protože je sonoff většinou 24 hodin denně zapojený v elektrické síti nastává otázka kolik si vlastně relátko připojené na Wifi spotřebuje energie. Aktuální konfigurace si z elektrické sítě veme 1.4W při rozpojeném relátku a 1.9W při sepnutí. Samozdřejmě měřeno bez jakékoliv zátěže kteou muže spínat.
Zde je program
// Knihovny pro praci s WiFi
#include <ESP8266WiFi.h>
#include <DNSServer.h>
#include <ESP8266WebServer.h>
#include <WiFiManager.h> // Dohledate ve spravci knihoven
#include <Ticker.h>
#include <EEPROM.h>
#include <TimeLib.h> // https://github.com/PaulStoffregen/Time
#include <WiFiUdp.h>
#include "html.h" // HTML kod ovladaci stranky
// Cisla pinu LED, spinace a tlačítka
#define GPIO_LED 13
#define GPIO_TL 0
#define GPIO_RELE 12
// Webovy server pojede na standardnim portu 80
ESP8266WebServer server(80);
// Objekt ticker z vestavene knihovny se postara o blikani LED
Ticker ticker;
int signalstate = 1;
int signalstatelast = 1;
// Pomocne promenne pro synchronizaci casu
const char ntp_server[] = "tik.cesnet.cz";
uint8_t zona = 1 ;
uint8_t zonanew ;
const uint8_t port = 8888;
uint8_t ntp_packet[48];
WiFiUDP Udp;
// Kontrolni promenna pro spinani podle casoveho planu
uint64_t kontrola_spinace = 0;
// Hodiny a minuty spinani podle casoveho planu
uint8_t zacatek_hodina, zacatek_minuta, konec_hodina, konec_minuta, letni_cas;
// Funkce tick rozsviti/zhasne LED
void tick() {
digitalWrite(GPIO_LED, !digitalRead(GPIO_LED));
}
// Pokud se cip nemuze pripojit k Wi-Fi a spustil vlastni konfiguracni AP, rozblikej LED
void zacatekKonfigurace(WiFiManager *wmp) {
// kazdych 200 ms zavolej funkci tick
ticker.attach(0.2, tick);
}
// Jakmile rezim konfigurace Wi-Fi skonci, ukonci blikani
// LED na Sonoff Basic ma opacnou logiku
// Pri LOW sviti a pri HIGH je zhasnuta
void konecKonfigurace() {
ticker.detach();
digitalWrite(GPIO_LED, HIGH);
}
// Halvni funkce, ktera se zpracuje po startu
void setup() {
// Nastaveni smeru pinu GPIO na zapis
pinMode(GPIO_LED, OUTPUT);
pinMode(GPIO_RELE, OUTPUT);
pinMode(GPIO_TL, INPUT);
// Zhasni LED
digitalWrite(GPIO_LED, HIGH);
Serial.begin(115200);
// Precti prvni 4 B z trvale pameti,
// ve ktere jsou ulozene casy automatickeho spinani a vypinani
EEPROM.begin(5);
zacatek_hodina = EEPROM.read(0);
zacatek_minuta = EEPROM.read(1);
konec_hodina = EEPROM.read(2);
konec_minuta = EEPROM.read(3);
letni_cas = EEPROM.read(4);
// Nastartuj WiFiManager, ktery se postara o pripojeni k Wi-Fi
WiFiManager wm;
wm.setAPCallback(zacatekKonfigurace);
wm.setSaveConfigCallback(konecKonfigurace);
// IP parametry konfiguracni Wi-Fi site
wm.setAPStaticIPConfig(IPAddress(192, 168, 100, 1), IPAddress(192, 168, 100, 1), IPAddress(255, 255, 255, 0));
// Pripoj se k Wi-Fi
// Pokud zatim zadnou nemas v pameti, nebo je mimo dosah,
// spust vlastni AP, ke kteremu se muze uzivatel pripojit a nastavit novou Wi-Fi
if (!wm.autoConnect("WiFiSonoff","Sonoff18")) {
// Pokud se neco pokazi a nelze se pripojit, restartuj cip a zacni znovu
ESP.reset();
delay(1000);
}
else {
Serial.print("Pripojen jako: ");
Serial.println(WiFi.localIP());
}
// Ted uz jsem pripojeny k Wi-Fi, takze mohu pokracovat v programu
// Pokud uzivatel zada do prohlizece IP adresu spinace,
// posli mu HTML kod ulozeny v souboru html.h.
// Behem HTTP komunikace zaroven sviti LED (problikne)
// HTML kod se nacita primo z flashove pameti cipu a nezatezuje RAM
server.on("/", []() {
digitalWrite(GPIO_LED, LOW);
server.send_P(200, "text/html", html);
digitalWrite(GPIO_LED, HIGH);
});
// Server zaroven reaguje na nekolik HTTP dotazu ve formatu:
// http://ipadresa/api?PARAMETR=HODNOTA
// Pro nastaveni automatickeho spinani a vypinani tedy staci zavolat:
// http://ipadresa/api?zacatek=HH:MM&konec=HH:MM
server.on("/api", []() {
digitalWrite(GPIO_LED, LOW);
if (server.hasArg("zacatek") && server.hasArg("konec")) {
if ((server.arg("zacatek") != NULL) && (server.arg("konec") != NULL)) {
zacatek_hodina = server.arg("zacatek").substring(0, 2).toInt();
zacatek_minuta = server.arg("zacatek").substring(3, 5).toInt();
konec_hodina = server.arg("konec").substring(0, 2).toInt();
konec_minuta = server.arg("konec").substring(3, 5).toInt();
EEPROM.write(0, zacatek_hodina);
EEPROM.write(1, zacatek_minuta);
EEPROM.write(2, konec_hodina);
EEPROM.write(3, konec_minuta);
EEPROM.commit();
server.send(200, "application/json", "{\"odpoved\":\"1\"}");
}
else {
server.send(200, "application/json", "{\"odpoved\":\"0\"}");
}
}
// Pro sepnuti rele (zapnuti/vypnuti svetla):
// http://ipadresa/api?stav=1 (nebo 0)
else if (server.hasArg("stav")) {
if (server.arg("stav") != NULL) {
uint8_t stav = server.arg("stav").toInt();
if (stav == 1) {
Serial.println("Zapinam rele");
digitalWrite(GPIO_RELE, HIGH);
server.send(200, "application/json", "{\"odpoved\":\"1\"}");
}
else {
Serial.println("Vypinam rele");
digitalWrite(GPIO_RELE, LOW);
server.send(200, "application/json", "{\"odpoved\":\"0\"}");
}
}
else {
server.send(200, "application/json", "{\"odpoved\":\"-1\"}");
}
}
// Pro nastaveni letniho casu
// http://ipadresa/api?letnicas=1 (nebo 0)
else if (server.hasArg("letnicas")) {
if (server.arg("letnicas") != NULL) {
uint8_t stav = server.arg("letnicas").toInt();
if (stav == 1) {
Serial.println("Zapinam letni cas");
EEPROM.write(4, 1);
EEPROM.commit();
// aby se zona projevila bez nutnosti restartovat obvod
setSyncProvider(ziskejNtpCas);
// server.send(200, "application/json", "{\"odpoved\":\"1\"}");
// přesmeruj na hlavni stránku aby se rovnouo aktualizoval datum na stránce
server.sendHeader("Location", String("/"), true);
server.send ( 302, "text/plain", "");
}
else {
Serial.println("Vypinam letni cas");
EEPROM.write(4, 0);
EEPROM.commit();
// aby se zona obnovila bez nutnosti restartovat obvod
setSyncProvider(ziskejNtpCas);
// server.send(200, "application/json", "{\"odpoved\":\"0\"}");
// přesmeruj na hlavni stránku aby se rovnouo aktualizoval datum na stránce
server.sendHeader("Location", String("/"), true);
server.send ( 302, "text/plain", "");
}
}
else {
server.send(200, "application/json", "{\"odpoved\":\"-1\"}");
}
}
// Pro stazeni udaju (teplota, stav, cas na cipu, volna RAM) v JSON:
// http://ipadresa/api?data=
else if (server.hasArg("data")) {
String data = "{\"odpoved\":\"1\", \"zacatek\":\"#zacatek\", \"konec\":\"#konec\", \"stav\":\"#stav\", \"cas\":\"#cas\", \"ram\":\"#ram\", \"letni_cas\":\"#letni_cas\"}";
char zacatek[6];
char konec[6];
char cas[9];
// sensors.requestTemperatures();
sprintf(zacatek, "%02d:%02d", zacatek_hodina, zacatek_minuta);
sprintf(konec, "%02d:%02d", konec_hodina, konec_minuta);
sprintf(cas, "%02d:%02d:%02d", hour(), minute(), second());
// Nahrad hodnoty v AJAX sablone vyse
// S tridou Arduino String zachazet s velkou rozvahou, pouziva dynamickou alokaci
// Na cipech s malickou RAM ji mohou pri spatnem designu rychle zaplnit
// Viz pamet typu heap, dynamicka alokace a riziko fragmentace RAM
// https://www.gribblelab.org/CBootCamp/7_Memory_Stack_vs_Heap.html
data.replace("#stav", String(digitalRead(GPIO_RELE)));
data.replace("#zacatek", String(zacatek));
data.replace("#konec", String(konec));
data.replace("#cas", String(cas));
data.replace("#ram", String((ESP.getFreeHeap() / 1000.0f), 2));
data.replace("#letni_cas", String(letni_cas));
server.send(200, "application/json", data);
}
else {
server.send(200, "application/json", "{\"odpoved\":\"0\"}");
}
digitalWrite(GPIO_LED, HIGH);
});
// Nastartovani UDP (synchronizace casu pomoci NTP serveru)
Udp.begin(port);
// Knihovna pro praci s casem bude kazdou hodinu
// volat funkci, ktera bude stahovat cerstvy cas z NTP serveru
setSyncProvider(ziskejNtpCas);
setSyncInterval(3600);
// Nastartovani HTTP serveru
server.begin();
}
// Smycka loop se opakuje stale dokola
void loop() {
// Zpracuj pozadavky HTTP klientu
server.handleClient();
// Jednou za minutu zkontroluj, jestli aktualni
// cas neodpovida hodnotam pro automaticke sepnuti/vypnuti rele
if (millis() > kontrola_spinace) {
if ((zacatek_hodina == hour()) && (zacatek_minuta == minute())) {
digitalWrite(GPIO_RELE, HIGH);
Serial.println("Spinam rele podle casoveho planu");
}
if ((konec_hodina == hour()) && (konec_minuta == minute())) {
digitalWrite(GPIO_RELE, LOW);
Serial.println("Vypinam rele podle casoveho planu");
}
kontrola_spinace = millis() + 6e4;
}
if (digitalRead(GPIO_RELE) == LOW) {
// zde je opačná logika pokud je relé rozpojeno je nutné poslat HIGH aby dioda zhasla
digitalWrite(GPIO_LED, HIGH);
}else{
digitalWrite(GPIO_LED, LOW);
}
// přečteme signál z tlačítka
signalstate = digitalRead(GPIO_TL);
if (signalstate != signalstatelast) {
if (signalstate == LOW){
if (digitalRead(GPIO_RELE) == LOW) {
digitalWrite(GPIO_RELE, HIGH);
Serial.println("zapinam rele tlacitkem ");
}else{
digitalWrite(GPIO_RELE, LOW);
Serial.println("vypinam rele tlacitkem");
}
delay(50);
}
}
signalstatelast = signalstate;
}
// Funcke pro ziskani aktualniho casu z NTP serveru skrze UDP protokol
time_t ziskejNtpCas()
{
letni_cas = EEPROM.read(4);
zonanew = zona + letni_cas;
Serial.println("aktualizuji cas");
IPAddress ntp_server_ip;
while (Udp.parsePacket() > 0);
WiFi.hostByName(ntp_server, ntp_server_ip);
odesliNtpPacket(ntp_server_ip);
uint32_t start = millis();
while (millis() - start < 1500) {
int size = Udp.parsePacket();
if (size >= 48) {
Udp.read(ntp_packet, 48);
unsigned long sekundy; // sekundy od roku 1900
sekundy = (unsigned long)ntp_packet[40] << 24;
sekundy |= (unsigned long)ntp_packet[41] << 16;
sekundy |= (unsigned long)ntp_packet[42] << 8;
sekundy |= (unsigned long)ntp_packet[43];
// Vrati pocet sekund a pripocita casovou zonu
return sekundy - 2208988800UL + zonanew * SECS_PER_HOUR;
}
}
// Pokud se dotaz nepodaril, vrat 0
return 0;
}
// Funcke pro odeslani UDP paketu/framu na NTP server
void odesliNtpPacket(IPAddress &adresa) {
memset(ntp_packet, 0, 48);
ntp_packet[0] = 0b11100011;
ntp_packet[1] = 0;
ntp_packet[2] = 6;
ntp_packet[3] = 0xEC;
ntp_packet[12] = 49;
ntp_packet[13] = 0x4E;
ntp_packet[14] = 49;
ntp_packet[15] = 52;
Udp.beginPacket(adresa, 123);
Udp.write(ntp_packet, 48);
Udp.endPacket();
}
Soubor s HTML kódem
// Pro prevod ceskych znaku v HTML kodu
// do zakladniho ASCII jsem pouzil prevod
// do formatu HEX NCR na webu:
// https://r12a.github.io/app-conversion/
static const char PROGMEM html[] = R"html(
<!DOCTYPE html>
<html lang="cs">
<head>
<title>WiFiRele</title>
<link href="https://fonts.googleapis.com/css?family=Comfortaa&subset=latin-ext" rel="stylesheet">
<script
src="https://code.jquery.com/jquery-3.3.1.min.js"
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
crossorigin="anonymous">
</script>
<style>
body{
font-family: "Comfortaa", cursive;
line-height: 150%;
margin: 50px;
text-align: center;
}
a{
display: inline-block;
width: 100px;
padding: 5px;
border: 1px solid steelblue;
font-weight: bold;
}
a:link, a:visited, a:active{
color: steelblue;
background: white;
text-decoration: none;
}
a:hover{
color: white;
background: steelblue;
text-decoration: none;
}
</style>
<script>
// Tato funkce se zpracuje pote, co se stahne a vykresli cely HTML kod
$(function(){
// Na zacatku si pres AJAX stahni JSON s informacemi
$.get("/api?data=1", function(data){
if(data.odpoved == 1){
console.log(data);
// Aktualizuj podle stazenych dat prvky na strance
$("#cas").html(data.cas);
$("#ram").html(data.ram);
$("#letni_cas").html(data.letni_cas);
$("#zacatek").val(data.zacatek);
$("#konec").val(data.konec);
// Podle stavu rele vykresli adekvatni tlacitko
if(data.stav == 1){
$("#rozsvitit").hide();
$("#stav").html(" sepnuto");
$("#stav").css("color", "green");
}
else{
$("#zhasnout").hide();
$("#stav").html(" je vypnuto");
$("#stav").css("color", "red");
}
// Podle stavu letniho casu vykresli adekvatni tlacitko
if(data.letni_cas == 1){
$("#zapni_letni").hide();
}
else{
$("#vypni_letni").hide();
}
}
else{
console.error("Chyba: " + data.odpoved);
}
});
// Pokud klepnu na tlacitko nastaveni casu, odesli AJAXem nove casy automatickeho spinani
$("#nastavit").click(function(){
$.get("/api?zacatek=" + $("#zacatek").val() + "&konec=" + $("#konec").val(), function(data){
if(data.odpoved == 1){
console.log("Zmena casu automatickeho spinani a vypinani");
}
else{
console.error("Chyba: " + data.odpoved);
}
});
});
// Po klepnuti na odkaz pro rozsviceni odesli AJAXem prikaz k rozsviceni
$("#rozsvitit").click(function(){
$.get("/api?stav=1", function(data){
if(data.odpoved == 1){
console.log("Rele sepnuto!");
$("#rozsvitit").hide();
$("#zhasnout").show();
$("#stav").html(" zapnuto");
$("#stav").css("color", "green");
}
else{
console.error("Chyba: " + data.odpoved);
}
});
});
// Po klepnuti na odkaz pro zhasnuti odesli AJAXem prikaz ke zhasnuti
$("#zhasnout").click(function(){
$.get("/api?stav=0", function(data){
if(data.odpoved == 0){
console.log("Rele vypnuto!");
$("#rozsvitit").show();
$("#zhasnout").hide();
$("#stav").html(" je vypnuto");
$("#stav").css("color", "red");
}
else{
console.error("Chyba: " + data.odpoved);
}
});
});
});
</script>
</head>
<body>
<h1>Relé<span id="stav"></span></h1>
<p>
<a id="rozsvitit" href="#">Zapnout</a>
<a id="zhasnout" href="#">Vypnout</a>
</p>
<p>
Nastavit čas automatického spínání a vypínání
</p>
<p>
Zapnout v <input id="zacatek" type="time" /> a vypnout v
<input id="konec" type="time" />
<input id="nastavit" type="button" value="Nastavit cas" />
</p>
<p>
Aktuální čas: <span id="cas"></span>
<a id="zapni_letni" href="/api?letnicas=1">Nastav letní čas</a>
<a id="vypni_letni" href="/api?letnicas=0">Nastav zimní čas</a><br>
, volná paměť: <span id="ram"></span> kB
</p>
</body>
</html>
)html";
Mohlo by Vás zajímat
Zdrojový kód v zip souboru můžete stáhnout zde.
Článek z porátlu Živě.cz ze kterého jsem bral inspiraci i převážnou část kódu
Itead.cz stránky ze kterých jsem modul obědnal
Další články o programování z Arduina prostředí na těchto stránkách
Miloš_1dePp
2019-03-03 19:35:04Tento článek mě zaujal. Zkoušel jsem tedy provést jen kompilaci a zasekl jsem se zde: #include „html.h“ // HTML kod ovladaci stranky kde může být problém ?
Stanimir
2019-04-25 19:29:10Taky jsem se zasekl na stejném místě, po dalším studování jsem na to přišel. V Arduino IDE se otevře nový projekt kam se nahraje program, potom se najede na šipku vpravo a vybere se nová záložka která se pojmenuje html.h a vloží se do ní soubor s html kódem. Snad ti to pomůže.
Ijacek.007
2019-04-30 20:18:39Omlouvám se nějak jsem na ten komentář zapoměl odpovědět. :-(
Ijacek.007
2019-11-21 18:57:08Pro všechny co se zaselky na onem HTML souboru jsem přidal na stánku odkaz
na stažení celého zipu po rozbalení lze rovnou otevřít v prostředí
arduina.
najdete jej i zde https://blog.ijacek007.cz/sonoff_v1.4.zip
Zdeněk_I0RJa
2021-01-23 09:46:40obrý den, mám doma více zařízení od SONOFF a všechny zařízení mají v nastavení možnost „stav po zapnutí“ ale bohužel ifan 03 tuto možnost nenabízí. Můžete mi poradit . Jelikož při výpadku elektriky a opětovném zapnutí dojde k rozsvícení žárovky a to nechci. Je třeba možnost tento povel dohrát a pokud ano jak?
Vložit komentář
* - vyžadované údaje. RSS kanál s komentáři