Dopsal jsem postup pro základní oživení Hass a doplňků. Pokud by něco bylo nepochopitelné dovysvětlím, popřípadě se doplní nějaké obrázky. Příště se zaměřím na oživení chytrého relé. Dlouhým procházení jsem dospěl k názoru, že je ideální řešení v podobě ESP8266. V případě Wemos je omezení dodělávka napájení a relé. Jako ideální hardware jsem našel Sonoff Basic, který se přehraje nějakým vlastním řešením z prostředí Arduino IDE. Tady je kód sonoff-arduino
html.h
Nebo sáhnout po nějakém hotovém řešení např. Tasmota. Tady je výhoda rychlý rozvoj, stále možnost si Tasmotu přiohnout. Díky tomuto řešení máme k dispozici 10A Relé, s komunikací MQTT a s možností rozšíření o pousty senzorů s cenovkou do 100Kč. Takže jako hlavní nosný prvek bude právě Sonoff Basic.
Kód:
// 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 <OneWire.h> // Dohledate ve správci knihoven
#include <DallasTemperature.h> // Dohledate ve správci knihoven
#include "html.h" // HTML kod ovladaci stranky
// Cisla pinu LED, spinace a teplomeru
#define GPIO_LED 13
#define GPIO_TEPLOMER 14
#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;
// Nastartovani sbernice 1-Wire a teplomeru DS18B20
OneWire oneWire(GPIO_TEPLOMER);
DallasTemperature teplomer(&oneWire);
// Pomocne promenne pro synchronizaci casu
const char ntp_server[] = "us.pool.ntp.org";
const uint8_t zona = 1;
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;
// 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) {
Serial.println("Entered config mode");
Serial.println(WiFi.softAPIP());
//if you used auto generated SSID, print it
Serial.println(wmp->getConfigPortalSSID());
//entered config mode, make led toggle faster
// 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);
// 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(4);
zacatek_hodina = EEPROM.read(0);
zacatek_minuta = EEPROM.read(1);
konec_hodina = EEPROM.read(2);
konec_minuta = EEPROM.read(3);
WiFiManager wifiManager;
// Nastartuj teplomer DS18B20
teplomer.begin();
// Nastartuj WiFiManager, ktery se postara o pripojeni k Wi-Fi
wifiManager.setAPCallback(zacatekKonfigurace);
wifiManager.setSaveConfigCallback(konecKonfigurace);
// IP parametry konfiguracni Wi-Fi site
wifiManager.setAPStaticIPConfig(IPAddress(192, 168, 0, 1), IPAddress(192, 168, 0, 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 (!wifiManager.autoConnect("WiFiLampa")) {
Serial.println("failed to connect and hit timeout");
// 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 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, \"teplota\":#teplota}";
char zacatek[6];
char konec[6];
char cas[9];
teplomer.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("#teplota", String(teplomer.getTempCByIndex(0), 2));
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;
}
}
// Funcke pro ziskani aktualniho casu z NTP serveru skrze UDP protokol
time_t ziskejNtpCas()
{
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 + zona * 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();
}
Kód:
// 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>WiFiLampa</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);
$("#teplota").html(data.teplota);
$("#zacatek").val(data.zacatek);
$("#konec").val(data.konec);
// Podle stavu rele vykresli adekvatni tlacitko
if(data.stav == 1){
$("#rozsvitit").hide();
$("#stav").html(" svítí");
$("#stav").css("color", "green");
}
else{
$("#zhasnout").hide();
$("#stav").html(" je zhasnutá");
$("#stav").css("color", "red");
}
}
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(" svítí");
$("#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 zhasnutá");
$("#stav").css("color", "red");
}
else{
console.error("Chyba: " + data.odpoved);
}
});
});
});
</script>
</head>
<body>
<h1>WiFiLampa<span id="stav"></span></h1>
<p>
<a id="rozsvitit" href="#">Rozsvítit</a>
<a id="zhasnout" href="#">Zhasnout</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>, teplota: <span id="teplota"></span> °C, volná paměť: <span id="ram"></span> kB
</p>
</body>
</html>
)html";