Categories
ESP32 Home Assistant

NFC/Code Schlüsselbox

Es kommt vor, dass man Kleinigkeiten, Schlüssel oder anderes für fremde Personen hinterlegen muss, jedoch nicht vor Ort sein kann. Man kann die Dinge irgendwo verstecken oder hinlegen und hoffen, dass sie nicht von Dritten gefunden werden oder man baut sich eine Box, die mit Code, sowie via NFC geöffnet werden kann.

Ich habe mich für die zweite Variante entschieden, die ich hier vorstelle.

Funktionsweise

Die NFC bzw. Code Schlüsselbox soll sich sowohl durch Nutzung von NFC-Karten, sowie durch Eingabe von Codes öffnen lassen. Die Codes und NFC-Karten sollen im Home Assistant bequem und dynamisch hinterlegt werden können.

Nach einer Bestimmten Zeit wird das Display deaktiviert. Durch Drücken der Taste “0” kann dieses wieder aufgeweckt werden. Es kann dann zwischen Code-Eingbe mit “#”, sowie NFC-Karte mit “*” gewählt werden. Das ist wichtig, da während der ESP32 auf die Daten einer NFC-Karte wartet, keine weiteren Interaktionen verarbeiten kann. Zudem ergibt es energetisch keinen Sinn den NFC Kartenleser dauerhaft aktiv zu lassen. Bei Eingabe eines Code wird dieser mit “#” bestätigt.

Daraufhin prüft der ESP32 die Eingabe oder die NFC-Karte, ob diese berechtigt ist. Dafür holt sich der ESP32 regelmäßig die Daten zu den NFC-Karten, sowie Codes vom HomeAssistant ab.

Nach Verfikation und Korrektheit wird die Tür entriegelt und der Schlüssel oder was auch immer in der Box liegt, kann entnommen werden.

Im HomeAssistant werden bei NFC Karten die UID/MAC eingegeben, sowie bei Codes die Zahlen. Das Format der UID/MAC ist: 00:AA:BB:CC…

Zudem wird der positive Zugriff, sowie auch der Fall des negativen Zugriffs an den HomeAssistant zu Dokumentationszwecken übersandt.

Geschlossen wird die Box durch herzhaftes zudrücken, bis das Schloss einrastet. Der Status, ob die Box offen oder geschlossen ist, kann am Schloss selbst abgegriffen werden, dafür bauen die Hersteller dieser Öffner oft Kontakte ein, die diese Information bereitstellen.

Hardware

Wie bereits viele andere meiner Projekte, startet auch dieses mit einem ESP32 Mikrocontroller. Dazu habe ich mir ein Nummernfeld als Tastatur, einen NFC Code Modul, ein Display, sowie einen Öffner besorgt.

Als NFC Modul verwende ich ein PN532 mit i²c Interface. Für dieses Modul gibt es eine gleichnamige Bibliothek.

Ein Keypad 3 Tasten breit und 4 Tasten hoch ist für dieses Projekt ausreichend. Es gibt vierschiedene Keypads, diese funktionieren meistens ähnlich.

Das Display für die Anzeige ist ein LCD Display mit 2×16 Zeichen und i²c als Schnittstelle. Wichtig ist: Falls das Display nichts anzeigt, hilft es, nach zu lesen, ob das Display mit 3,3V oder 5V versorgt werden muss. Ich habe eines, das 5V benötigt, mit nur 3,3V wird nichts angezeigt.

Als Öffner bzw. Schloss verwende ich ein elektrisches Spind Schloss. Zu finden sind sie meistens mit den Suchbegriff “12v dc elektroschloss”. Wichtig ist, dass man diese Schlösser nur ganz kurz mit Strom versorgen darf, da sie sich sonst in Rauch auflösen. Das steht allerdings in der Regel im Begleitpapier nochmal dabei. Diese Spind Öffner melden oft auch den Status zurück.

Platine

Hier die Platine dazu (Afillate): https://aisler.net/p/VXNKFJSZ

  • ESP32 mit 38 PINs
  • EN-DC -> Brücke um den ESP über den DC-Wandler zu versorgen (im Normalbetrieb) oder über USB, wenn nicht gebrückt wurde, interessant für Debug-Zwecke
  • DC-Wandler : RECOM R-78E50-05
  • MOSFET vom Typ : IRF5210
  • C1, C2, C3 : 10nF
  • D1 : Freilaufdode z.B. 1N 4148 oder 1N 4007
  • L1 : 10 uH
  • R1 : 5.6 kO
  • R2 : 1 kO
  • R3 : 10 kO
  • T1 : BC547C

Die Platine wird mit 12V DC versorgt. Der DC-Wandler erzeugt daraus 5V für den ESP32. Im Falle des Öffnens, wird vom ESP32 zunächst der Transistor T1 geschaltet, der wiederum 12V / 0V am Gate des IRF5210 setzt. Über R3 wird das Gate des IRF5210 immer auf 12V gehalten, bis T1 durchgeschaltet wird und das Gate des IRF auf 0V zieht, dann schaltet der IRF und legt 12V an den Elektroschloss Öffner Kontakt an. Die Diode D1 ist die Freilaufdiode für das Elektroschloss. SW ist der Elektroschloss Schloss Status Kontakt. Das Elektroschloss hat intern einen kleinen Schalter, der Rückmeldet, ob das Schloss geschlossen (1) oder offen (0) ist. Display, NFC Modul, sowie der optionale BME280 werden am i²c Interface links oben angeschlossen. Mein Display musste statt an 3V an 5V angeschlossen werden. Der Rest muss an 3V angeschlossen werden. 5 = +5V | L = SCL | A = SDA | 3 = +3V | G = GND.

Platine mit durchscheinenden Leiterbahnen der Rückseite.

Das Keypad wird unten rechts an den Kontakten 1-7 angeschlossen. Bei meinem Keybad sieht die Kontaktierung wie folgt aus. Das kann je nach Keypad abweichen, bitte selbst prüfen:

Kontaktierung Keypad

Messpunkte

  • M1 : 12V Versorgungsspannung
  • M2 : 5V Spannung nach dem DC-Wandler
  • M3 : Basis des NPN Transistors T1
  • M4 : G32 des ESP32 zur Steuerung des T1
  • M5 : Kollektor des NPN Transistors T1, der mit dem Gate des IRF5210 verbunden ist.
  • M6 : Spannung die am Elektroschloss Öffner Kontakt anliegt.
  • M7 : Elektroschloss Schloss Status Kontakt.

Gehäuse

Platine

Die Platine findet in einer einfachen Box Platz.

Box

Die Box habe ich aus Holz gebaut. An der Tür befindet sich ein Knauf, um sie zu öffnen.

Box von Außen
Von innen kann die Box ohne Werkzeug geöffnet werden.

NFC Modul

Das PN532 Modul wird von hinten in eine 3D gedrucktes gehäuse eingesetzt und dann fixiert. Dazu kann entweder Kleber oder, so wie ich es gemacht habe erhitztes Filament verwendet werden.

Rückseite des NFC Modul Gehäuses. Unten erkennt man noch etwas Doppelklebeband, das erleichtert die temporäte Fixierung zum Anzeichnen und wird vor der finalen Montage entfernt.
Vorderseite des NFC Modul Gehäuses.

Display und Keypad

Display und Keypad werden zusammen in einem Gehäuse platziert. Beim Display handelt es sich , wie bereits oben beschrieben, um eines vom Typ LiquidCrystal 2×16 mit i²c. Das Display wird verschraubt, das Keypad mit Dichtmasse eingeklebt.

Rückeite des Display und Keypad gehäuses. Keypad eingeklebt, Display festgeschraubt.
Damit das alles etwas abgedichtet ist, hab ich auch das Display mit Dichtmasse eingerahmt.

Fotos

Ansicht von innen/hinten, im Durchgang sieht man rechts unten das elektrische Schloss. Rechts neben der Box ist in schwarz das Gehäuse für die Platine.
Ansicht von innen/hinten mit geschlossener Box, sowie eingesetzter und verkabelter Platine. Über und unter der Platine gehen jeweils Kabel durch die Wand für Display, NFC Modul und Keypad.
Ansicht von vorne, komplett fertig.

Video

Code-Eingabe Video – Auf das Bild klicken um das Video zu öffnen. (50MB)
NFC-Chip Video – Auf das Bild klicken, um das Video zu öffnen. (30 MB)

Home Assistant

rest:
  - scan_interval: 60
    resource: http://<ESP32IP>/jsondoaction
    sensor:
     - name: "ESP32 NFC Code Box - Door State"
       value_template: "{{ value_json.doorstate | int }}"
     
     - name: "ESP32 NFC Code Box - Last Failed"
       value_template: "{{ value_json.lastfailed }}"
       
     - name: "ESP32 NFC Code Box - Last Granted"
       value_template: "{{ value_json.lastgranted }}"
     
     - name: "ESP32 NFC Code Box - Free RAM"
       unit_of_measurement: "bytes"
       value_template: "{{ value_json.freeram | int }}"

input_text:
  access_card_001:
    name: "Access Card 001"
  access_card_002:
    name: "Access Card 002"
  access_card_003:
    name: "Access Card 003"
  access_card_004:
    name: "Access Card 004"
  access_card_005:
    name: "Access Card 005"
  access_card_006:
    name: "Access Card 006"
  access_card_007:
    name: "Access Card 007"
  access_code_001:
    name: "Access Code 001"
  access_code_002:
    name: "Access Code 002"
  access_code_003:
    name: "Access Code 003"
  access_code_004:
    name: "Access Code 004"
  access_code_005:
    name: "Access Code 005"
  access_code_006:
    name: "Access Code 006"
  access_code_007:
    name: "Access Code 007"

Code

Im Code müssen einige Anpassungen vorgenommen werden, wie Beispielsweise Authentication Bearer für den HomeAssistant, dessen IP und Port, WLAN-Name und -Passwort, sowie ggf. Namen der Felder im HomeAssistant. Optional kann ein BME280 Sensor an i²c angeschlossen werden, dieser kann mit der Option “enableBme = true” aktiviert werden. Danach stehen die Werte des BME280 auf dem Webserver des ESP32 in JSON Form zur Verfügung.

#include <TaskScheduler.h>

#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>

#include <Wire.h>
#include <PN532_I2C.h>
#include <PN532.h>
//#include <NfcAdapter.h>

#include <Keypad.h>

#include <LiquidCrystal_I2C.h>

// --------- WIFI -----------
#define STASSID    "" // wifi name
#define STAPSK     "" // wifi pw
#define DEVICENAME "ESP32-NFC-Code-Box";

unsigned long previousMillis = 0;
unsigned long interval = 2000;
// --------- END WIFI -------

// --------- INITS -------

const char* ssid = STASSID;
const char* password = STAPSK;
const char* deviceName = DEVICENAME;
WebServer server(80);
StaticJsonDocument<1024> jsonDocument;
char jsonBuffer[1024];

PN532_I2C pn532i2c(Wire);
PN532 nfc(pn532i2c);

int lcdColumns = 16;
int lcdRows = 2;

LiquidCrystal_I2C lcd(0x27, lcdColumns, lcdRows);  

StaticJsonDocument<1024> jsonHADocument;

const String HomeAssistantBearerName = "Authorization";
const String HomeAssistantBearerContent = "Bearer xyzBearerHomeAssistantxyz....";

const String SendApiIotUrl = "http://<homeAssistantIp:Port>/api/states/";

const String AccessCard001Name = "input_text.access_card_001";
const String AccessCard002Name = "input_text.access_card_002";
const String AccessCard003Name = "input_text.access_card_003";
const String AccessCard004Name = "input_text.access_card_004";
const String AccessCard005Name = "input_text.access_card_005";
const String AccessCard006Name = "input_text.access_card_006";
const String AccessCard007Name = "input_text.access_card_007";

const String AccessCode001Name = "input_text.access_code_001";
const String AccessCode002Name = "input_text.access_code_002";
const String AccessCode003Name = "input_text.access_code_003";
const String AccessCode004Name = "input_text.access_code_004";
const String AccessCode005Name = "input_text.access_code_005";
const String AccessCode006Name = "input_text.access_code_006";
const String AccessCode007Name = "input_text.access_code_007";

const String DoorStateName = "sensor.esp32_nfc_code_box_door_state";
const String LastFailedName = "sensor.esp32_nfc_code_box_last_failed";
const String LastGrantedName = "sensor.esp32_nfc_code_box_last_granted";

const byte ROWS = 4; // Four rows
const byte COLS = 3; // Three columns
// Define the Keymap
char keys[ROWS][COLS] = 
{
  {'1','2','3'},
  {'4','5','6'},
  {'7','8','9'},
  {'*','0','#'}
};

byte rowPins[ROWS] = { 25, 17, 18, 27 };
byte colPins[COLS] = { 26, 33, 19 };

// Create the Keypad
Keypad kpd = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS );

bool displayEnabled = true;
unsigned long lastMillsKeyEnter = 0;
unsigned long displayOffInterval = 60000;

// --------- END INITS -------

// --------- SCHEDULER BEGIN -------

void checkFreeRam();
Task scheduleCheckFreeRam(21*1000, TASK_FOREVER, &checkFreeRam);

void wifiReconnectCheck();
Task scheduleWifiReconnectCheck(5*1000, TASK_FOREVER, &wifiReconnectCheck);

void retrieveAccess();
Task scheduleRetrieveAccess(50*1000, TASK_FOREVER, &retrieveAccess);

void getBme();
Task scheduleGetBme(50*1000, TASK_FOREVER, &getBme);

void reportDoorStateChange();
Task scheduleReportDoorStateChange(2*1000, TASK_FOREVER, &reportDoorStateChange);

Scheduler runner;

// --------- SCHEDULER END ---------

// ------- DEFINITIONS ----------
static String bme280State = "State";

static String bme280Enabled = "enabled";
static String bme280Disabled = "disabled";

static String temperatureName = "Temperature";
static String temperatureName2 = "&deg;C";

static String humidityName = "Humidity";
static String humidityName2 = "&percnt;";

static String pressureName = "Pressure";
static String pressureName2 = "hPa";

static String linkColorNormal = "#2321B0";
static String linkColorVisited = "#2321B0";
static String activeMarkerBegin = "<b>&raquo;";
static String activeMarkerEnd = "&laquo;</b>";

// ------- END DEFINITIONS ----------

// ------- PINS ----------
static int morsePin = 2;
static int i2cSdaPin = 21;
static int i2cSclPin = 22;
static int relaisPin = 32;
static int statusPin = 34;
// ------- END PINS ----------

// --------- Variables ---------

static bool enableBme = false;
String bmeTemp = String("");
String bmeHum = String("");
String bmePress = String("");
Adafruit_BME280 bme; // I2C

int freeHeap = 0;

bool stateDoor = false;
bool stateRelais = false;

// Working
String accessCard001 = "";
String accessCard002 = "";
String accessCard003 = "";
String accessCard004 = "";
String accessCard005 = "";
String accessCard006 = "";
String accessCard007 = "";

String accessCode001 = "";
String accessCode002 = "";
String accessCode003 = "";
String accessCode004 = "";
String accessCode005 = "";
String accessCode006 = "";
String accessCode007 = "";

bool needForChange = true;

bool selectCard = false;
bool cardAccess = false;

bool selectCode = false;
bool codeAccess = false;

bool accessDenied = false;

String codeString = "";

String lastFailed = "";
String lastGranted = "";

// --------- END Variables ---------

void setup() 
{
  
  Serial.println("Setup");

  initSerial();
  initWifi();
  initPinModes();
  initSchedules();
  initServer();
  initBme();
  initNfc();
  initKeyBoard();
  initLcd();
  checkFreeRam();

  Serial.println("END Setup");

}

// --------------- LOOP ---------------

void loop() 
{
  server.handleClient();
  runner.execute();
  getKeyBoard();
  writeLcd();
  getNfcCard();
  disableDisplayIfNeeded();
}

// --------------- INITS ---------------

void initSerial()
{
  Serial.begin(115200); 
  while(!Serial){} // Waiting for serial connection
  Serial.println();
}

void initWifi()
{
  Serial.println("WiFi init");
  Serial.print("Wifi: ");
  Serial.println(ssid);
  
  Serial.println("turn wifi off...");
  WiFi.mode(WIFI_OFF);
  delay(200);
  WiFi.mode(WIFI_STA);
  delay(250);
  
  WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE);    
  delay(200); 
  Serial.println("setting hostname");
  WiFi.setHostname(deviceName);
  delay(200); 
  Serial.println("Connecting to WiFi..");
  WiFi.begin(ssid, password);
  delay(200); 

  int iCounter = 0;
  int iMax = 30;
  while (WiFi.status() != WL_CONNECTED && iCounter < iMax)
  {
    digitalWrite(morsePin, LOW);
    delay(500);
    digitalWrite(morsePin, HIGH);
    delay(500);
    Serial.print(".");
    iCounter++;
  }
  
  Serial.println(WiFi.localIP()); 
}

void initPinModes()
{
  Serial.println("PIN MODES init");

  pinMode(morsePin, OUTPUT);
  digitalWrite(morsePin, LOW);

  pinMode(relaisPin, OUTPUT);
  digitalWrite(relaisPin, LOW);

  pinMode(statusPin, INPUT);
}

void initSchedules()
{
  Serial.println("SCHEDULES init");
  
  runner.init();

  runner.addTask(scheduleCheckFreeRam);
  scheduleCheckFreeRam.enable();

  runner.addTask(scheduleWifiReconnectCheck);
  scheduleWifiReconnectCheck.enable();

  runner.addTask(scheduleGetBme);
  scheduleGetBme.enable();

  runner.addTask(scheduleRetrieveAccess);
  scheduleRetrieveAccess.enable();

  runner.addTask(scheduleReportDoorStateChange);
  scheduleReportDoorStateChange.enable();
}

void initServer()
{
  Serial.println("HTTP server init");
  server.on("/", handleConnect);
  server.on("/jsondoaction", jsonDoAct);
  server.on("/jsondoaction", HTTP_POST, jsonDoAct);  
  server.onNotFound(handleConnect);
  server.begin();
  Serial.println("HTTP server started");
}

void initBme()
{
  Serial.println("BME280 init");
  // BME 280
  if (enableBme == true)
  {  
    for(int i = 0; i < 10; i++)
    {
      Serial.println("detecting BME280...");
      delay(500);
      
      if (bme.begin(0x76))
      {
        i = 10;
      }
    }
  }
}

void initNfc()
{
  Serial.println("NFC init");

  nfc.begin();

  // Connected, show version
  uint32_t versiondata = nfc.getFirmwareVersion();
  if (!versiondata)
  {
    Serial.println("PN53x card not found!");
    return;
  }

  nfc.setPassiveActivationRetries(0xFF);

  // configure board to read RFID tags
  nfc.SAMConfig();
}

void initLcd()
{
  Serial.println("LCD init");

  lcd.init();
  // turn on LCD backlight                      
  lcd.backlight();

  // set cursor to first column, first row
  lcd.setCursor(0, 0);
  // print message
  lcd.print("# for code");

  lcd.setCursor(0, 1);
  lcd.print("* for card");

  lastMillsKeyEnter = millis();
}

void initKeyBoard()
{
  Serial.println("KEYBOARD init");
}

// --------------- SCHEDULER ---------------

void checkFreeRam()
{
  freeHeap = ESP.getFreeHeap();

  if (ESP.getFreeHeap() < 60000)
  {
    ESP.restart();
  }

  //Serial.println("Free RAM: " + String(ESP.getFreeHeap()));
}

void wifiReconnectCheck()
{
  unsigned long currentMillis = millis();
  // if WiFi is down, try reconnecting every CHECK_WIFI_TIME seconds
  if ((WiFi.status() != WL_CONNECTED) && (currentMillis - previousMillis >=interval))
  {
    Serial.print(millis());
    Serial.println("Reconnecting to WiFi...");
    WiFi.disconnect();
    WiFi.reconnect();
    previousMillis = currentMillis;
  }
}

void getBme()
{
  if (enableBme == true)
  {
    Serial.println("Refresh BME Vals");
    String zBmeTemp = String(bme.readTemperature());
    String zBmeHum = String(bme.readHumidity());
    String zBmePress = String(bme.readPressure() / 100.0F);

    bmeTemp = zBmeTemp;
    bmeHum = zBmeHum;
    bmePress = zBmePress;
    Serial.println("END - Refresh BME Vals");  
  }
}


void retrieveAccess()
{
  Serial.println("retrieve Access...");

  accessCard001 = getStringHttp(AccessCard001Name);
  //Serial.println("AccessCard001: " + (String)accessCard001);

  accessCard002 = getStringHttp(AccessCard002Name);
  //Serial.println("AccessCard002: " + (String)accessCard002);

  accessCard003 = getStringHttp(AccessCard003Name);
  //Serial.println("AccessCard003: " + (String)accessCard003);

  accessCard004 = getStringHttp(AccessCard004Name);
  //Serial.println("AccessCard004: " + (String)accessCard004);

  accessCard005 = getStringHttp(AccessCard005Name);
  //Serial.println("AccessCard005: " + (String)accessCard005);

  accessCard006 = getStringHttp(AccessCard006Name);
  //Serial.println("AccessCard006: " + (String)accessCard006);

  accessCard007 = getStringHttp(AccessCard007Name);
  //Serial.println("AccessCard007: " + (String)accessCard007);


  accessCode001 = getStringHttp(AccessCode001Name);
  //Serial.println("AccessCode001: " + (String)accessCode001);

  accessCode002 = getStringHttp(AccessCode002Name);
  //Serial.println("AccessCode002: " + (String)accessCode002);

  accessCode003 = getStringHttp(AccessCode003Name);
  //Serial.println("AccessCode003: " + (String)accessCode003);
  
  accessCode004 = getStringHttp(AccessCode004Name);
  //Serial.println("AccessCode004: " + (String)accessCode004);

  accessCode005 = getStringHttp(AccessCode005Name);
  //Serial.println("AccessCode005: " + (String)accessCode005);
  
  accessCode006 = getStringHttp(AccessCode006Name);
  ///Serial.println("AccessCode006: " + (String)accessCode006);

  accessCode007 = getStringHttp(AccessCode007Name);
  //Serial.println("AccessCode007: " + (String)accessCode007);
  
}

void getNfcCard()//void * pvParameters
{
  //while(true)
  //{
  if (selectCard == true)
  {
    Serial.println("getNfcCard...");
    bool success;
    // Buffer to store the UID
    String uidStr = "";
    char uidChar;
    uint8_t uid[] = { 0, 0, 0, 0, 0, 0, 0 };
    // UID size (4 or 7 bytes depending on card type)
    uint8_t uidLength;

    success = nfc.readPassiveTargetID(PN532_MIFARE_ISO14443A, &uid[0], &uidLength, 5000);

    // If the card is detected, print the UID
    if (success)
    {
      Serial.println("Card Detected");
      Serial.print("Size of UID: "); Serial.print(uidLength, DEC);
      Serial.println(" bytes");
      Serial.print("UID: ");

      uidStr = uintMacToHexString(uid, uidLength);

      Serial.println("UID: " + uidStr);

      Serial.println("");
      Serial.println("");

      handleAccess(uidStr, "");
      
      delay(200);
      initNfc();
    }
    else
    {
      // PN532 probably timed out waiting for a card
      // Serial.println("Timed out waiting for a card");
    }

    selectCard = false;

    needForChange = true;
  }
}

void reportDoorStateChange()
{
  int curDoorState = digitalRead(statusPin);
  
  if (curDoorState == 0 && stateDoor == true)
  {
    sendStatusHttp(DoorStateName, "0", false);
  }
  else if (curDoorState == 1 && stateDoor == false)
  {
    sendStatusHttp(DoorStateName, "1", false);
  }

  if (curDoorState == 0)
  {
    stateDoor = false;
  }
  else
  {
    stateDoor = true;
  }
}


void getKeyBoard()
{
  char key = kpd.getKey();
  if(key)  // Check for a valid key.
  {
    if (displayEnabled == false)
    {
      if (key == '0')
      {
        lastMillsKeyEnter = millis();
        needForChange = true;
      }
    }
    else
    {
      lastMillsKeyEnter = millis();
      //Serial.println("selectCard " + String(selectCard));
      //Serial.println("selectCode " + String(selectCode));
      if (selectCard == false && selectCode == false)
      {
        Serial.println(key);
        switch (key)
        {
          case '*':
            //digitalWrite(ledpin, LOW);
            selectCard = true;   
            selectCode = false;
            break;
          case '#':
            //digitalWrite(ledpin, HIGH);
            selectCard = false;
            selectCode = true;
            break;
          //default:
            //Serial.println(key);
        }
      }
      else if (selectCode == true)
      {
        Serial.println(key);

        if (key == '#')
        {
          // check code access
          //codeAccess = checkAccessCode(codeString);
          //selectCode = false;

          handleAccess("", codeString);

          /*if (codeAccess == false)
          {
            accessDenied = true;
          }*/
        }
        else
        {
          if (codeString.length() < 16)
          {
            codeString.concat(key);
          }
          //Serial.println("codeString " + String(codeString));
        }
      }

      needForChange = true;
    }
  }
  else
  {
    //Serial.print(".");
  }
}

void writeLcd()
{
  //Serial.println("LCD write");
  // set cursor to first column, first row

  if (displayEnabled == false)
  {
    return;
  }

  if (needForChange == true)
  {
    lcd.clear();

    if (selectCard == true)  
    {
      lcd.setCursor(0, 0);
      lcd.print("reading card...");

      needForChange = false;
    }
    else if (cardAccess == true)
    {
      lcd.setCursor(0, 0);
      lcd.print("Card:");
      
      lcd.setCursor(0, 1);
      lcd.print("access granted!");

      openGate();

      resetValsToNormal();

      delay(3000);
    }
    else if (selectCode == true)
    {
      lcd.setCursor(0, 0);
      lcd.print("Code + #:");

      lcd.setCursor(0, 1);
      lcd.print(codeString);
      
      needForChange = false;
    }  
    else if(codeAccess == true)   
    {
      lcd.setCursor(0, 0);
      lcd.print("Code:");
      lcd.setCursor(0, 1);
      lcd.print("access granted!");
      
      openGate();

      resetValsToNormal();

      delay(3000);
    } 
    else if (accessDenied)
    {
      lcd.setCursor(0, 1);
      lcd.print("access denied!");

      resetValsToNormal();
      
      delay(3000);
    }
    else
    {
      lcd.setCursor(0, 0);
      // print message
      lcd.print("# for code");

      lcd.setCursor(0, 1);
      lcd.print("* for card");

      needForChange = false;
    }

  }
}

void disableDisplayIfNeeded()
{
  unsigned long curMillis = millis();

  if (displayEnabled == true && lastMillsKeyEnter > curMillis)
  {
    lastMillsKeyEnter = curMillis;
    return;
  }

  if (displayEnabled == false && lastMillsKeyEnter > curMillis)
  {
    lastMillsKeyEnter = 0;
    return;
  }
  
  if ( (lastMillsKeyEnter + displayOffInterval) < curMillis )
  {
    if (displayEnabled == true)
    {
      displayEnabled = false;
      lcd.clear();
      lcd.noBacklight();
      resetValsToNormal();
    }
  }
  else
  {
    if (displayEnabled == false)
    {
      displayEnabled = true;
      needForChange = true;
      lcd.backlight();
    }
  }
}

void resetValsToNormal()
{
  needForChange = true;

  selectCard = false;
  cardAccess = false;

  selectCode = false;
  codeAccess = false;

  accessDenied = false;

  codeString = "";
}

void openGate()
{
  digitalWrite(relaisPin, LOW);
  digitalWrite(relaisPin, HIGH);
  delay(500);
  digitalWrite(relaisPin, LOW);

  sendStatusHttp(LastGrantedName, lastGranted, true);
  delay(1000);
  reportDoorStateChange();
}

// --------------- HANDLE ---------------

void handleAccess(String cardUidIn, String codeIn)
{
  if (checkAccessCard(cardUidIn) == true)
  {
    Serial.println("Access Garanted by Card");
    cardAccess = true;
    selectCard = false;
    selectCode = false;
    needForChange = true;
    lastGranted = "Card:" + cardUidIn;
  }
  else if (checkAccessCode(codeIn) == true)
  {
    Serial.println("Access Garanted by PIN");
    codeAccess = true;
    selectCard = false;
    selectCode = false;
    needForChange = true;
    lastGranted = "Code:" + codeIn;
  }
  else
  {
    Serial.println("Access Denied!");
    accessDenied = true;
    selectCard = false;
    selectCode = false;
    needForChange = true;

    lastFailed = "";

    if (cardUidIn != "")
    {
      lastFailed = "Card:" + cardUidIn;
    }

    if (codeIn != "")
    {
      lastFailed = "Code:" + codeIn;
    }

    sendStatusHttp(LastFailedName, lastFailed, true);
  }
}

bool checkAccessCard(String cardUidIn)
{
  if (cardUidIn.length() > 0)
  {
    if (cardUidIn == accessCard001 ||
        cardUidIn == accessCard002 ||
        cardUidIn == accessCard003 ||
        cardUidIn == accessCard004 ||
        cardUidIn == accessCard005 ||
        cardUidIn == accessCard006 ||
        cardUidIn == accessCard007)
    {
      return true;
    }
  }

  return false;
}

bool checkAccessCode(String codeIn)
{
  if (codeIn.length() > 0)
  {
    if (codeIn == accessCode001 ||
        codeIn == accessCode002 ||
        codeIn == accessCode003 ||
        codeIn == accessCode004 ||
        codeIn == accessCode005 ||
        codeIn == accessCode006 ||
        codeIn == accessCode007)
    {
      return true;
    }
  }

  return false;
}

// --------------- SERVER ---------------

void handleConnect()
{
  Serial.println("Connect");
  server.send(200, "text/html", SendHTML("")); 
}

String SendHTML(String context)
{
  String ptr = "<!DOCTYPE html> <html>\n";
  ptr +="<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, user-scalable=no\">\n";
  ptr +="<title>ESP32 NFC Code Box</title>\n";
  ptr +="<style>html { font-family: Arial; display: inline-block; margin: 0px auto; text-align: center;}\n";
  ptr +="body{margin-top: 50px;} h1 {color: #444444;margin: 50px auto 30px;} h3 {color: #444444;margin-bottom: 50px;}\n";
  ptr +=".button {display: block;width: 80px;background-color: #3498db;border: none;color: white;padding: 13px 30px;text-decoration: none;font-size: 25px;margin: 0px auto 35px;cursor: pointer;border-radius: 4px;}\n";
  ptr +="a, a:active, { color: " + linkColorNormal + "; text-decoration: underline; }\n";
  ptr +="a:visited { color: " + linkColorVisited + "; text-decoration: underline; }\n";
  ptr +="p {font-size: 14px;color: #888;margin-bottom: 10px;}\n";
  ptr +="</style>\n";
  ptr +="</head>\n";
  ptr +="<body>\n";
  ptr +="<h1>ESP32 NFC Code Box</h1>\n";

  String lineBreak = "<br><br>";

  ptr += "";

  ptr += "<br>";

  ptr += lineBreak;

  ptr +="<b>BME 280</b><br>\n";
  ptr += bme280State + ": ";
  if (enableBme == true)
  {
    ptr += bme280Enabled;
    ptr += "<br>";

    ptr += temperatureName + ": ";
    ptr += bmeTemp + temperatureName2 + "<br>";

    ptr += humidityName + ": ";
    ptr += bmeHum + humidityName2 + "<br>";

    ptr += pressureName + ": ";
    ptr += bmePress + pressureName2 + "<br>";
  }
  else
  {
    ptr += bme280Disabled;
    ptr += "<br>";
  }

  ptr += lineBreak;

  ptr += "<b>FreeHeap</b><br>\n";
  ptr += (String)freeHeap + " bytes";

  ptr +="</body>\n";
  ptr +="</html>\n";
  return ptr;
}

void jsonDoAct()
{
  Serial.println("JSON Act");

  if (server.hasArg("plain") == false) 
  {
    //handle error here
  }
  
  String body = server.arg("plain");
  Serial.println(body);
  deserializeJson(jsonDocument, body);

  createStatusJson("");

  server.send(200, "application/json", jsonBuffer); 
}

void createStatusJson(String statusIn) 
{
  if (statusIn == "")
  {
    statusIn = "OK";
  }
  jsonDocument.clear();  
  jsonDocument["state"] = statusIn;

  jsonDocument["enableBme"] = enableBme;
  jsonDocument["doorstate"] = digitalRead(statusPin);
  jsonDocument["lastfailed"] = lastFailed;
  jsonDocument["lastgranted"] = lastGranted;

  if (enableBme == true &&
      bmePress != String("") && 
      bmePress.toDouble() > 0)
  {
    jsonDocument["bmeTemp"] = bmeTemp.toDouble();
    jsonDocument["bmeHum"] = bmeHum.toDouble();
    jsonDocument["bmePress"] = bmePress.toDouble();
  }

  jsonDocument["freeram"] = freeHeap;

  serializeJson(jsonDocument, jsonBuffer);
}

// --------------- HELPER ---------------

String uintMacToHexString(uint8_t values[], uint8_t length)
{
  String retStr = "";
  if (length < 1)
  {
    return retStr;
  }  

  Serial.println("Length: " + String(length));
  
  for (uint8_t i = 0; i < length; i++)
  {
    String converted = "";
    Serial.println(values[i], HEX);
    converted = String(values[i], HEX);
    if (converted.length() < 2)
    {
      converted = (String)"0" + (String)converted;
    }

    retStr += (String)converted;
    
    if (i < length - 1)
    {
      retStr += ":";
    }
  }

  retStr.toUpperCase();
  return retStr;
}

String getStringHttp(String attributeStringIn)
{
  String retStr;

  HTTPClient *http = new HTTPClient();
  http->setReuse(false);
  
  if (http->begin(SendApiIotUrl + attributeStringIn))
  {
    //Serial.print("[HTTP] GET...\n");
    http->setTimeout(5000);
    // start connection and send HTTP header
    http->addHeader("Content-Type", "application/json; charset=UTF-8");
    http->addHeader(HomeAssistantBearerName, HomeAssistantBearerContent); // auth

    //Serial.println("Content: " + sendContent);
    
    int httpCode = http->GET();

    // httpCode will be negative on error
    if (httpCode > 0)
    {
      // file found at server
      if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY)
      {
        //String payload = http->getString();
        //Serial.println(payload);
        retStr = http->getString();
        //Serial.println(retStr);

        deserializeJson(jsonHADocument, retStr);

        if (jsonHADocument.containsKey("state") == true)
        {
          retStr = jsonHADocument["state"].as<String>();
        }
        else
        {
          retStr = "";
        }
      }
    }
    else
    {
      Serial.printf("[HTTP] GET... failed, error: %s\n", http->errorToString(httpCode).c_str());
      retStr = "error: ";
      retStr += http->errorToString(httpCode).c_str();
    }

    http->end();
  }
  else
  {
    //Serial.printf("[HTTP] Unable to connect\n");
    retStr = "";
  }

  delete http;

  return retStr;
}

String sendStatusHttp(String attributeStringIn, String valueIn, bool isString)
{
  String retStr;

  HTTPClient *http = new HTTPClient();
  http->setReuse(false);

  Serial.print("[HTTP] begin...\n");
  Serial.println("[HTTP] URL: " + SendApiIotUrl + attributeStringIn);
  
  if (http->begin(SendApiIotUrl + attributeStringIn))
  {
    Serial.print("[HTTP] POST...\n");
    http->setTimeout(5000);
    // start connection and send HTTP header
    http->addHeader("Content-Type", "application/json; charset=UTF-8");
    http->addHeader(HomeAssistantBearerName, HomeAssistantBearerContent); // auth

    // status Json
    String sendContent = "";
    if (isString == true)
    {
      sendContent = "{\"state\": \"" + valueIn + "\" }";
    }
    else
    {
      sendContent = "{\"state\": " + valueIn + " }";
    }

    Serial.println("Content: " + sendContent);
    
    int httpCode = http->POST(sendContent);

    // httpCode will be negative on error
    if (httpCode > 0)
    {
      Serial.printf("[HTTP] POST... code: %d\n", httpCode);

      // file found at server
      if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY)
      {
        //String payload = http->getString();
        //Serial.println(payload);
        retStr = http->getString();
      }
    }
    else
    {
      Serial.printf("[HTTP] GET... failed, error: %s\n", http->errorToString(httpCode).c_str());
      retStr = "error: ";
      retStr += http->errorToString(httpCode).c_str();
    }

    http->end();
  }
  else
  {
    Serial.printf("[HTTP] Unable to connect\n");
    retStr = "ConErr";
  }

  delete http;

  return retStr;
}

Hinweis

Dieser Artikel dokumentiert lediglich meinen Aufbau. Für den Nachbau, die Nutzung einzelner Komponenten, die Platinen und den gesamten Inhalt wird die Haftung in jeglicher Form ausgeschlossen. Der Aufbau erhebt zudem nicht den Anspruch irgend ein Level von Sicherheit zu gewährleisten.

Categories
ESP32 Home Assistant

ePaper Anzeige

Displays haben viele Anwendungsmöglichkeiten. Sie können wichtige Informationen liefern, als Touch Bedienfeld dienen oder sogar Warnhinweise geben.

Die Auswahl des richtigen Display richtet sich nach den Anforderungen. Für dieses Projekt wird eine dauerhafte Anzeige benötigt, die alle paar Minuten aktualisiert wird. Zudem soll die Anzeige nicht leuchten und dennoch gut lesbar sein. Aus diesem Grund habe ich mich für ein mehrzeiliges ePaper Display entschieden. Klar ist auch, dass das Display mit einem ESP32 kompaitibel sein muss, da ESP32 für mich die Platform der Wahl ist.

Das Display kommt im Gang neben meiner Bürotür zum Einsatz. Im HomeOffice möchte ich anzeigen, ob ich gerade nicht gestört werden möchte, in einer Konferenz bin o.ä..

Display

Das Display hat eine Diagonale von 2,13 Zoll und wird via SPI angesteuert. Ich bevorzuge I²C, das ist jedoch nicht immer möglich. Es kann Schwarz, sowie Rot anzeigen. Der Hintergrund ist weiß.

Platine

Damit das Display fest mit dem ESP32 verbunden werden kann, habe ich eine Platine gestaltet, in die der ESP32 auf der einen Seite, sowie das Display von der anderen Seite eingesteckt wird. Zudem wird die gesamte Platine mit 12V DC versorgt, deshalb ist ein Spannungswandler auf der Platine vorgesehen.

https://aisler.net/p/YIQZJTRL (Werbelink)

Bestückung:

  • DC-Wandler : RECOM R-78E50-05
  • C1, C2, C3: 10nF
  • L1: 10µH
  • Entsprechende Stift-Leisten und Pin-Heads

Gehäuse

Das Gehäuse wird an die Wand geschraubt und ist wie immer 3D gedruckt.

Vorderseite
Rückseite
Rückseite leicht gedreht

Endmontage

Von der Rückseite wird alles im Gehäuse eingebaut.

Display eingebaut. Zu beachten ist, dass die Stiftleiste (gelb) bereits von mir eingelötet wurde. Das Display wird via M3 Schrauben befestigt.
Aufgesteckte Platine mit Stiftleisten und Heads – Ohne ESP32 – Für die Stromversorgung wird an einer Stelle der Wahl ein Loch gebohrt
Detailansicht: Zusammengesteckte Verbindung zwischen Platine und Display
Fertig montiertes Display mit Bespieltext
Fertig montiertes Display mit Bespieltext, seitliche Ansicht

Software

Bei meinem Display handelt es sich vermutlich um das Display “UC8151D”. Dafür muss die GxGDEH0213Z19.h verwendet werden. Ich verwende in der Arduino IDE die Bibliothek “GxEPD”. In der entsprechenden Dokumentation sind andere Display Typen dokumentiert.

Als Schriftart verwende ich die “FreeMonoBold9pt7b”. Entsprechende Imports sind im Quellcode zu finden.

Prinzipiell holt sich der ESP32 alle 40 Sekunden die neuen Werte vom HomeAssistant. Es gibt jedoch auch die Möglichkeit, dass man ein sofortiges Holen der Daten triggern kann. Das Triggern funktioniert über einen HTTP-Server, den der ESP32 öffnet.

Für jede Zeile habe ich einen eigenen String für die Anzeige. Zudem gibt es pro Zeile die Möglichkeit diese in schwarz oder Rot anzeigen zu lassen.

Home Assistant

rest:
  - scan_interval: 60
    resource: http://<IP>/jsondoaction
    sensor:
     - name: "ESP32 Door Kai - Free RAM"
       unit_of_measurement: "bytes"
       value_template: "{{ value_json.freeram | int }}"

input_text:
  door_kai_line_001:
    name: "Door Kai Line 001"
  door_kai_line_002:
    name: "Door Kai Line 002"
  door_kai_line_003:
    name: "Door Kai Line 003"
  door_kai_line_004:
    name: "Door Kai Line 004"
  door_kai_line_005:
    name: "Door Kai Line 005"

input_boolean:
  door_kai_line_001_red:
    name: "Door Kai Line 001 Red"
  door_kai_line_002_red:
    name: "Door Kai Line 002 Red"
  door_kai_line_003_red:
    name: "Door Kai Line 003 Red"
  door_kai_line_004_red:
    name: "Door Kai Line 004 Red"
  door_kai_line_005_red:
    name: "Door Kai Line 005 Red"

Ich habe ein paar Buttons im HomeAssistant erzeugt, hinter die ich Automatisierungen gelegt habe, um schnell einzelne Zeilen zu setzen.

input_button:
  door_kai_update_now:
    name: "Door Kai - Update Now"
  door_kai_auto_praesentation:
    name: "Door Kai - Präsentation - NICHT STÖREN"
  door_kai_auto_arbeit_andere:
    name: "Door Kai - Arbeitszeit" 
  door_kai_auto_arbeit_fokus:
    name: "Door Kai - Fokuszeit"
  door_kai_auto_arbeit_pause:
    name: "Door Kai - Pause"
  door_kai_auto_empty:
    name: "Door Kai - Empty"

Das forcierte Aktualisieren funktioniert wie folgt. Zunächst muss ein REST Command in der Konfiguration erstellt werden:

rest_command:
  door_kai_force_update_cmd:
    url: "http://<IP>/jsondoaction"
    method: POST
    headers:
      accept: "application/json, text/html"
    payload: '{ "update": "1"}'
    content_type:  'application/json; charset=utf-8'

Dieser kann dann wiederum mit einem Trigger auf den Button über eine Automatisierung aufgerufen werden (Beispiel als YAML):

platform: state
entity_id:
  - input_button.door_kai_update_now

… bei Actions dann:

service: rest_command.door_kai_force_update_cmd
data: {}

In einer Automatisierung würde man wie folgt den Trigger setzen (Beispiel als YAML) um über Buttons vorgefertigte Texte zu setzen:

platform: state
entity_id:
  - input_button.door_kai_auto_arbeit_andere

… dann so den Text zu setzen:

service: input_text.set_value
data:
  value: Arbeitszeit
target:
  entity_id: input_text.door_kai_line_002

… so beispielsweise zwei Zeilen auf “rot” setzen:

service: input_boolean.turn_on
data: {}
target:
  entity_id:
    - input_boolean.door_kai_line_005_red
    - input_boolean.door_kai_line_004_red

… eine Zeile auf “nicht rot” bzw. schwarz setzen:

service: input_boolean.turn_off
data: {}
target:
  entity_id:
    - input_boolean.door_kai_line_002_red

Falls benötigt kann hiernach noch der rest command zum forcierten Aktualisieren getriggert werden.

Wichtig zu wissen ist, dass wir keine Texte direkt auf dem ESP32 setzen, sondern immer im HomeAssistant. Der ESP32 holt sich nur periodisch oder wenn es forciert wird, die Texte vom HomeAssistant und gibt diese auf dem Display aus.

Wichtig ist, dass im HomeAssistant ein User angelegt werden sollte, von dem aus ein Authentication Bearer erzeugt wird, über den sich der ESP32 beim Holen der Informationen authentifiziert.

Code

#include <ArduinoJson.h>
#include <HTTPClient.h>
#include <WebServer.h>

#include <TaskScheduler.h>

// display
#include <GxEPD.h>
//#include <GxGDEW0213Z16/GxGDEW0213Z16.h>  // 2.13" b/w/r
#include <GxGDEH0213Z19/GxGDEH0213Z19.h>  // 2.13" b/w/r UC8151D
#include GxEPD_BitmapExamples

// FreeFonts from Adafruit_GFX
//#include <Fonts/FreeMono9pt7b.h>
#include <Fonts/FreeMonoBold9pt7b.h>
//#include <Fonts/FreeMonoBold12pt7b.h>
//#include <Fonts/FreeMonoBold18pt7b.h>
//#include <Fonts/FreeMonoBold24pt7b.h>

#include <GxIO/GxIO_SPI/GxIO_SPI.h>
#include <GxIO/GxIO.h>

GxIO_Class io(SPI, /*CS=5*/ SS, /*DC=*/ 17, /*RST=*/ 16); // arbitrary selection of 17, 16
GxEPD_Class display(io, /*RST=*/ 16, /*BUSY=*/ 4); // arbitrary selection of (16), 4

// --------- WIFI -----------
#define STASSID    "WiFiName"
#define STAPSK     "WiFiPWD"

#define DEVICENAME "ESP32-Door-ePaper-001";

unsigned long previousMillis = 0;
unsigned long interval = 2000;

// --------- END WIFI -------

// --------- INITS -------

const char* ssid = STASSID;
const char* password = STAPSK;
const char* deviceName = DEVICENAME;
WebServer server(80);
StaticJsonDocument<2048> jsonDocument;
char jsonBuffer[2048];

const String HomeAssistantBearerName = "Authorization";
const String HomeAssistantBearerContent = "Bearer <bearer>";

const String SendApiIotUrl = "http://<HaIP>:<HaPort>/api/";

// --------- END INITS -------

// --------- SCHEDULER BEGIN -------

void checkFreeRam();
Task scheduleCheckFreeRam(21*1000, TASK_FOREVER, &checkFreeRam);

void wifiReconnectCheck();
Task scheduleWifiReconnectCheck(5*1000, TASK_FOREVER, &wifiReconnectCheck);

void displayAutoRePaint();
Task scheduleDisplayAutoRePaint(3600*1000, TASK_FOREVER, &displayAutoRePaint);

void getValsFromHA();
Task schedulegetValsFromHA(40*1000, TASK_FOREVER, &getValsFromHA);

Scheduler runner;

// --------- SCHEDULER END ---------

// ------- DEFINITIONS ----------

static String linkColorNormal = "#2321B0";
static String linkColorVisited = "#2321B0";
static String activeMarkerBegin = "<b>&raquo;";
static String activeMarkerEnd = "&laquo;</b>";

// ------- END DEFINITIONS ----------

// ------- PINS ----------
static int morsePin = 2;
static int i2cSdaPin = 21;
static int i2cSclPin = 22;
// ------- END PINS ----------

// --------- Variables ---------

int freeHeap = 0;

bool line1red = false;
bool line2red = false;
bool line3red = false;
bool line4red = false;
bool line5red = false;

String line1 = "";
String line2 = "";
String line3 = "";
String line4 = "";
String line5 = "";

// --------- END Variables ---------


void setup()
{
  initSerial();
  initWifi();
  initPinModes();
  initSchedules();
  initServer();
  initDisplay();
  doPaint();
  checkFreeRam();
}

// --------------- LOOP ---------------

void loop()
{
  server.handleClient();
  runner.execute();
}

// --------------- INITS ---------------

void initSerial()
{
  Serial.begin(115200); 
  while(!Serial){} // Waiting for serial connection
  Serial.println();
}

void initWifi()
{
  Serial.println("WiFi init");
  Serial.print("Wifi: ");
  Serial.println(ssid);
  
  Serial.println("turn wifi off...");
  WiFi.mode(WIFI_OFF);
  delay(10);
  //WiFi.forceSleepBegin();
  delay(200);
  //WiFi.forceSleepWake();
  WiFi.mode(WIFI_STA);
  delay(250);
  
  WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE);    
  delay(200); 
  //WiFi.mode(WIFI_STA);
  Serial.println("setting hostname");
  WiFi.setHostname(deviceName);
  delay(200); 
  Serial.println("Connecting to WiFi..");
  WiFi.begin(ssid, password);
  delay(200); 

  int iCounter = 0;
  int iMax = 30;
  while (WiFi.status() != WL_CONNECTED && iCounter < iMax)
  {
    digitalWrite(morsePin, LOW);
    delay(500);
    digitalWrite(morsePin, HIGH);
    delay(500);
    Serial.print(".");
    iCounter++;
  }
  
  Serial.println(WiFi.localIP()); 
}

void initPinModes()
{
  Serial.println("PIN MODES init");

  pinMode(morsePin, OUTPUT);
  digitalWrite(morsePin, LOW);

}

void initSchedules()
{
  Serial.println("SCHEDULES init");
  
  runner.init();

  runner.addTask(scheduleCheckFreeRam);
  scheduleCheckFreeRam.enable();

  runner.addTask(scheduleWifiReconnectCheck);
  scheduleWifiReconnectCheck.enable();

  runner.addTask(scheduleDisplayAutoRePaint);
  scheduleDisplayAutoRePaint.enable();

  runner.addTask(schedulegetValsFromHA);
  schedulegetValsFromHA.enable();
}

void initDisplay()
{
  display.init(115200);
  display.setRotation(1);
  //showFont(&FreeMono9pt7b);
  //showFont(&FreeMonoBold9pt7b);
}

void initServer()
{
  server.on("/", handleConnect);
  server.on("/jsondoaction", jsonDoAct);
  server.on("/jsondoaction", HTTP_POST, jsonDoAct);  
  server.onNotFound(handleConnect);
  server.begin();
  Serial.println("HTTP server started");
}

// --------------- LOGIC ---------------

void doPaint()
{  
  display.fillScreen(GxEPD_WHITE);
  display.setTextColor(GxEPD_BLACK);
  display.setFont(&FreeMonoBold9pt7b);
  display.setCursor(0, 0);
  display.println();

  if (line1red == true)
  {
    display.setTextColor(GxEPD_RED);
  }
  else
  {
    display.setTextColor(GxEPD_BLACK);
  }

  display.println(line1);

  if (line2red == true)
  {
    display.setTextColor(GxEPD_RED);
  }
  else
  {
    display.setTextColor(GxEPD_BLACK);
  }
  
  display.println(line2);

  if (line3red == true)
  {
    display.setTextColor(GxEPD_RED);
  }
  else
  {
    display.setTextColor(GxEPD_BLACK);
  }
  
  display.println(line3);

  if (line4red == true)
  {
    display.setTextColor(GxEPD_RED);
  }
  else
  {
    display.setTextColor(GxEPD_BLACK);
  }
  
  display.println(line4);

  if (line5red == true)
  {
    display.setTextColor(GxEPD_RED);
  }
  else
  {
    display.setTextColor(GxEPD_BLACK);
  }
  
  display.println(line5);

  display.update();
}

// --------------- SCHEDULER ---------------

void checkFreeRam()
{
  freeHeap = ESP.getFreeHeap();

  if (ESP.getFreeHeap() < 60000)
  {
    ESP.restart();
  }
}

void wifiReconnectCheck()
{
  unsigned long currentMillis = millis();
  // if WiFi is down, try reconnecting every CHECK_WIFI_TIME seconds
  if ((WiFi.status() != WL_CONNECTED) && (currentMillis - previousMillis >=interval))
  {
    Serial.print(millis());
    Serial.println("Reconnecting to WiFi...");
    WiFi.disconnect();
    WiFi.reconnect();
    previousMillis = currentMillis;
  }
}

void displayAutoRePaint()
{
  doPaint();
}

void getValsFromHA()
{
  String responseStr = "";
  String tmpStr = "";  
  bool tmpBool = false;
  bool bSomethingChanged = false;

  // line 1
  responseStr = getRequest(SendApiIotUrl + "states/input_text.door_kai_line_001");
  deserializeJson(jsonDocument, responseStr);
  if (jsonDocument.containsKey("state") == true)
  {
    tmpStr = jsonDocument["state"].as<String>();
    if (line1.compareTo(tmpStr.substring(0,19)) != 0)
    {
      line1 = tmpStr.substring(0,19);
      Serial.println("line1 = " + (String)line1);
      bSomethingChanged = true;
    }
  }

  // line 1 red
  responseStr = getRequest(SendApiIotUrl + "states/input_boolean.door_kai_line_001_red");
  deserializeJson(jsonDocument, responseStr);
  if (jsonDocument.containsKey("state") == true)
  {
    tmpStr = jsonDocument["state"].as<String>();

    if (tmpStr == "on")
    {
      tmpBool = true;
    }
    else
    {
      tmpBool = false;
    }

    if (line1red != tmpBool)
    {
      line1red = tmpBool;
      Serial.println("line1red = " + (String)line1red);
      bSomethingChanged = true;       
    }
  }


  // line 2
  responseStr = getRequest(SendApiIotUrl + "states/input_text.door_kai_line_002");
  deserializeJson(jsonDocument, responseStr);
  if (jsonDocument.containsKey("state") == true)
  {
    tmpStr = jsonDocument["state"].as<String>();
    if (line2.compareTo(tmpStr.substring(0,19)) != 0)
    {
      line2 = tmpStr.substring(0,19);
      Serial.println("line2 = " + (String)line2);
      bSomethingChanged = true;
    }
  }

  // line 2 red
  responseStr = getRequest(SendApiIotUrl + "states/input_boolean.door_kai_line_002_red");
  deserializeJson(jsonDocument, responseStr);
  if (jsonDocument.containsKey("state") == true)
  {
    tmpStr = jsonDocument["state"].as<String>();

    if (tmpStr == "on")
    {
      tmpBool = true;
    }
    else
    {
      tmpBool = false;
    }

    if (line2red != tmpBool)
    {
      line2red = tmpBool;
      Serial.println("line2red = " + (String)line2red);
      bSomethingChanged = true;       
    }
  }

  // line 3
  responseStr = getRequest(SendApiIotUrl + "states/input_text.door_kai_line_003");
  deserializeJson(jsonDocument, responseStr);
  if (jsonDocument.containsKey("state") == true)
  {
    tmpStr = jsonDocument["state"].as<String>();
    if (line3.compareTo(tmpStr.substring(0,19)) != 0)
    {
      line3 = tmpStr.substring(0,19);
      Serial.println("line3 = " + (String)line3);
      bSomethingChanged = true;
    }
  }

  // line 3 red
  responseStr = getRequest(SendApiIotUrl + "states/input_boolean.door_kai_line_003_red");
  deserializeJson(jsonDocument, responseStr);
  if (jsonDocument.containsKey("state") == true)
  {
    tmpStr = jsonDocument["state"].as<String>();

    if (tmpStr == "on")
    {
      tmpBool = true;
    }
    else
    {
      tmpBool = false;
    }

    if (line3red != tmpBool)
    {
      line3red = tmpBool;
      Serial.println("line3red = " + (String)line3red);
      bSomethingChanged = true;       
    }
  }

  // line 4
  responseStr = getRequest(SendApiIotUrl + "states/input_text.door_kai_line_004");
  deserializeJson(jsonDocument, responseStr);
  if (jsonDocument.containsKey("state") == true)
  {
    tmpStr = jsonDocument["state"].as<String>();
    if (line4.compareTo(tmpStr.substring(0,19)) != 0)
    {
      line4 = tmpStr.substring(0,19);
      Serial.println("line4 = " + (String)line4);
      bSomethingChanged = true;
    }
  }

  // line 4 red
  responseStr = getRequest(SendApiIotUrl + "states/input_boolean.door_kai_line_004_red");
  deserializeJson(jsonDocument, responseStr);
  if (jsonDocument.containsKey("state") == true)
  {
    tmpStr = jsonDocument["state"].as<String>();

    if (tmpStr == "on")
    {
      tmpBool = true;
    }
    else
    {
      tmpBool = false;
    }

    if (line4red != tmpBool)
    {
      line4red = tmpBool;
      Serial.println("line4red = " + (String)line4red);
      bSomethingChanged = true;       
    }
  }

  // line 5
  responseStr = getRequest(SendApiIotUrl + "states/input_text.door_kai_line_005");
  deserializeJson(jsonDocument, responseStr);
  if (jsonDocument.containsKey("state") == true)
  {
    tmpStr = jsonDocument["state"].as<String>();
    if (line5.compareTo(tmpStr.substring(0,19)) != 0)
    {
      line5 = tmpStr.substring(0,19);
      Serial.println("line5 = " + (String)line5);
      bSomethingChanged = true;
    }
  }

  // line 5 red
  responseStr = getRequest(SendApiIotUrl + "states/input_boolean.door_kai_line_005_red");
  deserializeJson(jsonDocument, responseStr);
  if (jsonDocument.containsKey("state") == true)
  {
    tmpStr = jsonDocument["state"].as<String>();
    //Serial.println("TmpStr = " + (String)tmpStr);

    if (tmpStr == "on")
    {
      tmpBool = true;
    }
    else
    {
      tmpBool = false;
    }

    if (line5red != tmpBool)
    {
      line5red = tmpBool;
      Serial.println("line5red = " + (String)line5red);
      bSomethingChanged = true;       
    }
  }

  if (bSomethingChanged == true)
  {
    doPaint();
  }
}

// --------------- SERVER ---------------

void handleConnect()
{
  Serial.println("Connect");
  server.send(200, "text/html", SendHTML("")); 
}

String SendHTML(String context)
{
  String ptr = "<!DOCTYPE html> <html>\n";
  ptr +="<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, user-scalable=no\">\n";
  ptr +="<title>ESP32 Door ePaper 001</title>\n";
  ptr +="<style>html { font-family: Arial; display: inline-block; margin: 0px auto; text-align: center;}\n";
  ptr +="body{margin-top: 50px;} h1 {color: #444444;margin: 50px auto 30px;} h3 {color: #444444;margin-bottom: 50px;}\n";
  ptr +=".button {display: block;width: 80px;background-color: #3498db;border: none;color: white;padding: 13px 30px;text-decoration: none;font-size: 25px;margin: 0px auto 35px;cursor: pointer;border-radius: 4px;}\n";
  ptr +="a, a:active, { color: " + linkColorNormal + "; text-decoration: underline; }\n";
  ptr +="a:visited { color: " + linkColorVisited + "; text-decoration: underline; }\n";
  ptr +="p {font-size: 14px;color: #888;margin-bottom: 10px;}\n";
  ptr +="</style>\n";
  ptr +="</head>\n";
  ptr +="<body>\n";
  ptr +="<h1>ESP32 Door ePaper 001</h1>\n";

  String lineBreak = "<br><br>";

  ptr += lineBreak;

  ptr += "<b>Text</b><br>\n";

  if (line1red == true)
  {
    ptr += "Line 1: <font style=\"color:red\">" + line1 + "</font><br>";
  }
  else
  {
    ptr += "Line 1: <font style=\"color:black\">" + line1 + "</font><br>";    
  }

  if (line2red == true)
  {
    ptr += "Line 2: <font style=\"color:red\">" + line2 + "</font><br>";
  }
  else
  {
    ptr += "Line 2: <font style=\"color:black\">" + line2 + "</font><br>";    
  }

  if (line3red == true)
  {
    ptr += "Line 3: <font style=\"color:red\">" + line3 + "</font><br>";
  }
  else
  {
    ptr += "Line 3: <font style=\"color:black\">" + line3 + "</font><br>";    
  }

  if (line4red == true)
  {
    ptr += "Line 4: <font style=\"color:red\">" + line4 + "</font><br>";
  }
  else
  {
    ptr += "Line 4: <font style=\"color:black\">" + line4 + "</font><br>";    
  }

  if (line5red == true)
  {
    ptr += "Line 5: <font style=\"color:red\">" + line5 + "</font><br>";
  }
  else
  {
    ptr += "Line 5: <font style=\"color:black\">" + line5 + "</font><br>";    
  }

  ptr += lineBreak;

  ptr += "<b>FreeHeap</b><br>\n";
  ptr += (String)freeHeap + " bytes";

  ptr +="</body>\n";
  ptr +="</html>\n";
  return ptr;
}

void jsonDoAct()
{
  Serial.println("JSON Act");

  if (server.hasArg("plain") == false) 
  {
    //handle error here
  }
  
  String body = server.arg("plain");
  Serial.println(body);
  deserializeJson(jsonDocument, body);

  if (jsonDocument.containsKey("update") == true)
  {
    int thunderWarningTmp = jsonDocument["update"];
    Serial.println("JSON Act - Force Update");
    
    getValsFromHA();
  }

  createStatusJson("");

  server.send(200, "application/json", jsonBuffer); 
}

void createStatusJson(String statusIn) 
{
  if (statusIn == "")
  {
    statusIn = "OK";
  }
  jsonDocument.clear();  
  jsonDocument["state"] = statusIn;

  jsonDocument["line1"] = line1;
  jsonDocument["line1red"] = line1red;

  jsonDocument["line2"] = line2;
  jsonDocument["line2red"] = line2red;

  jsonDocument["line3"] = line3;
  jsonDocument["line3red"] = line3red;

  jsonDocument["line4"] = line4;
  jsonDocument["line4red"] = line4red;

  jsonDocument["line5"] = line5;
  jsonDocument["line5red"] = line5red;

  jsonDocument["freeram"] = freeHeap;

  serializeJson(jsonDocument, jsonBuffer);
}

// --------------- HELPER ---------------

String getRequest(const String url)
{
  String retStr;

  HTTPClient *https = new HTTPClient();
  https->setReuse(false);

  if (https->begin(url))
  {
    https->setTimeout(10000);
    // start connection and send HTTP header
    https->addHeader("Content-Type", "application/json; charset=UTF-8");
    https->addHeader(HomeAssistantBearerName, HomeAssistantBearerContent); // auth

    int httpCode = https->GET();

    // httpCode will be negative on error
    if (httpCode > 0)
    {
      // file found at server
      if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY)
      {
        retStr = https->getString();  
      }
    }
    else
    {
      Serial.printf("[HTTP] GET... failed, error: %s\n", https->errorToString(httpCode).c_str());
      retStr = "error: ";
      retStr += https->errorToString(httpCode).c_str();
    }

    https->end();
  }
  else
  {
    Serial.printf("[HTTP] Unable to connect\n");
    retStr = "ConErr";
  }

  delete https;
  https = NULL;

  return retStr;
}

Hinweis

Dieser Artikel dokumentiert lediglich meinen Aufbau. Für den Nachbau, die Nutzung einzelner Komponenten, die Platinen und den gesamten Inhalt wird die Haftung in jeglicher Form ausgeschlossen.

Categories
ESP32 Home Assistant

ESP32 LED-Steuerung

Dezente Beleuchtung ist eine schöne Sache. Vor allem, wenn sie sich einfach anpassen und steuern lässt.

Nachdem ich einige kommerzielle Lösungen angeschaut habe, habe ich mich dazu entschieden eine eigene auf Basis eines ESP32 zu entwickeln, die ich in den Home Assistant integrieren kann. Auch weil ich die Steuerung von Farbe, An- sowie Ausschaltzeitpunkt automatisieren wollte.

Funktionsweise

Zunächst muss man sich mit der Steuerung von LED-Bändern beschäftigen, um eine Steuerung entwickeln zu können. LED Bänder bestehen in der Regel aus vielen RGB (Rot-Grün-Blau) LEDs. Je nach Helligkeit der einzelnen Farben, ändert sich die wahrgenommene Farbe. Sind beispielsweise nur Rot und Grün zu je 100% an, ergibt das die Farbe gelb. Kommt blau zu 100% dazu, ergibt sich weiß. Je nach dem zu wie viel Prozent jede Farbe an ist, entsteht eine Mischfarbe. Wichtig ist das Verhältnis der Werte zueinander. Um die LEDs zu dimmen, werden die Verhältnisse der Farben zu einander gleich gehalten, jedoch alle LEDs insgesamt in der Helligkeit reduziert.

Im Gegensatz zu klassischen Glühbirnen, werden LEDs nicht durch das reduzieren der Spannung verdunkelt, sondern durch das “Takten” und die damit verbundene Helligkeitsreduzierung. Nehmen wir eine Sekunde; In dieser Sekunde ist die eine Farbe der LED die gesamte Zeit an, eine andere nur zu 50%. Als Betrachter nimmt man die andere Farbe somit um 50% weniger Hell wahr. Man schaltet jedoch nicht die andere Farbe eine halbe Sekunde ab und diese dann wieder eine halbe Sekunde an, sondern man wählt eine weitaus schnellere, für das Auge nicht wahrnehmbare Frequenz, in der die zweite Farbe an- und ausgeschaltet wird. Die Folge wäre sonst ein unangenehmer und ständiger Farbwechsel. Im Ergebnis ist; Die andere LED zwar in Summe nur 50% der Zeit an, aber der Wechsel zwischen AN und AUS ist so schnell, dass dieser vom menschlichen Auge nicht wahrgenommen wird. Was das menschliche Auge wahrnimmt ist eine geringere Helligkeit der anderen Farbe und damit bildet sich eine entsprechende Mischfarbe.

Ich habe 15m LED Band im Einsatz. Ein Test hat gezeigt, dass wie zu erwarten das in Reihe schalten der drei Bänder dazu führt, dass das letzte LED Band fast nicht mehr leuchtet. Die Bänder müssen also parallel an die Platine angeschlossen werden. Im Parallelbetrieb gemessen, habe ich bei der Farbe weiß, die den höchsten Stromfluss aufweist, zirka 4.5-5 Ampere bei einer Betriebsspannung von 12V gemessen. Das entspricht einer Leistung von zirka 60W. Das heißt der Pluspol über den der Maximalstrom von 5 Ampere fließt und die zugehörigen Komponenten müssen entsprechend dimensioniert werden. Wichtig an dieser Stelle ist: Man kann keine pauschale Aussage über die Stromaufnahme eines 15m LED Bandes treffen, das hängt von vielen Faktoren ab, beispielsweise wie viele LEDs pro Meter installiert sind.

Zudem habe ich festgestellt, dass die LEDs immer unter Spannung stehen. Das heißt es wird nur der Stromabfluss gesteuert, nicht der Zufluss. Das empfinde ich als großes Manko dieses Bandes, da ich eigentlich nicht möchte, dass dauerhaft 12V am Band anliegen.

Hardware

Zum Einsatz kommen ein ESP32, sowie eine selbst entwickelte Platine.

Das LED-Band besteht aus RGB 5050 LEDs.

Ein ESP32 hat den Vorteil, dass er das sogenannte PWM auf vielen PINs unterstützt. Es können zwar nur zehn verschiedene PWM Kanäle genutzt werden, was aber für ein RGB Band völlig ausreichend ist.

PWM bzw. ausgeschrieben Pulse-width modulation ermöglicht es, PINs des ESP32 in einer bestimmten Frequenz unter Bestimmung der Pulsbreite, also wie breit ist der Teil, in dem der PIN an und wie breit ist der Teil, in dem der PIN aus ist, zu setzen.

Das LED Band wird mit einer Spannung von 12V betrieben und benötigt hohe Ströme. Beides kann der ESP32 nicht leisten. Aus diesem Grund habe ich mich dafür entschieden für das Schalten des LED Bandes MOSFETs zu nutzen. MOSFETs haben den großen Vorteil, dass sie im Gegensatz zu Transistoren einen sehr geringen Innenwiderstand bzw. Spannungsabfall aufweisen. Aus diesem Grund fällt sehr wenig Leistung ab und die Wärmeentwicklung hält sich in Grenzen. Wichtig bei der Verwendung von MOSFETs ist, dass sie immer sauber durchgeschaltet oder gesperrt werden, sonst erhöht sich hier Innenwiderstand signifikant, was zwangsläufig zur Zerstörung des MOSFET führt. Je nach Typ sollte an einem MOSFET am Gate entweder 0V oder die Betriebsspannung (der Source) anliegen. Ein Blick auf das Datenblatt des ESP32 enthüllt; Der Mikrocontroller kann maximal 3.3V und Ströme im Milliampere Bereich leisten. Für ein sauberes Sperren bzw. Durchschalten des MOSFET nicht ausreichend. Aus diesem Grund verwende ich ich einen BC547C Transistor als Schalter, um die MOSFETs an zu steuern.

Um den “Abfluss” der Ströme der einzelnen Farben zu steuern verwende ich N-MOSFETs. Um den “Zufluss” bei deaktiviertem Band respektive einer Helligkeit von null ab zu schalten, verwende ich einen P-MOSFET. Warum man nicht eine Art MOSFET für beide Arten der Steuerung verwenden kann, lässt sich in entsprechender Fachliteratur oder im Internet nachlesen.

Da mein verwendeter P-MOSFETs einen signifikant höheren Innenwiderstand (~0,06 Ohm) aufweist, als die verwendeten N-MOSFETS (~0,006 Ohm) und sich daher eine entsprechende Wärmeentwicklung einstellt, habe ich den P-MOSFET mit einem Kühlköper, sowie das Gehäuse mit einem Lüfter ausgestattet. Mit ein Grund für den Lüfter ist, dass es im Inneren des Gartenhauses gerade im Sommer etwas wärmer werden kann und ich vermeiden wollte, dass sich im Gehäuse hohe Temperaturen bei einer sowieso schon hohen Außentemperatur bilden.

Platine

Hier die Platine dazu (Afillate): https://aisler.net/p/ZSWOKSID

Bauteile:

  • Mikrocontroller: ESP32 mit 30 PINs
  • C1, C2, C3 : 10nF
  • LED1, LED2, LED3, LED4 : Standard LED
    (dienen zur optischen Kontrolle)
  • R1, R3, R5, R7 : 1kO
  • R2, R4, R6 : 2.2kO
  • R8 : 2.2kO
  • R9, R10, R11, R12 : 3.3kO
  • R13, R14, R15, R16 : 5.6kO
  • L1 : 10uH
  • T1, T2, T3, T4 : Transistor BC547C
  • 3x MOSFET IRL1004
  • 1x MOSFET IRF5210
  • 1x DC Wandler RECOM R-78E50-05
  • 1x TO-220 Kühlkörper V FI356
  • 2x 7.62mm PCB Screw Terminal Block (3-Pin)

Messpunkte:

  • M1 : Blau
  • M2 : Rot
  • M3 : Grün
  • M4 : Eingangsspannung
  • M5 : On/Off Spannung
  • M6 : 5V DC
  • M7 : Gate Blau
  • M8 : Gate Rot
  • M9 : Gate Grün
  • M10 : Gate On/Off
  • M11 : GND
  • M12 : On/Off Spannung (wie M4)
Verlötete Platine (oben)
Verlötete Platine (seite)

Die Anschlüsse unten auf der Platine 3V3, GND, sowie SCL und SDA (I²C) dienen dazu, damit sich spätere Erweiterungen, wie beispielsweise ein Temperatursensor einfach nachrüsten lassen, falls man das möchte.

Gehäuse

Das Gehäuse ist 3D gedruckt und mit einem 40mm 12V Lüfter versehen. Der Lüfter wird an GND und Vout angeschlossen, damit dieser nur dann läuft, wenn das LED Band angeschaltet ist. Zudem habe ich vor Vin eine 10A Schmelzsicherung installiert, falls ein Kurzschluss z.B. in einem LED-Band auftritt und die Schutzschaltung im Netzteil nicht greifen sollte.

Die seitlichen Löcher im Gehäuse habe ich gebohrt. Der Lüfter bringt Luft ins Gehäuse, die über die Löcher austritt.

Gehäuse offen
Installierte LED-Steuerung, links zu sehen die Verteilung auf die LED-Bänder

Fotos

Home Assistant

Die Schwierigkeit mit Home Assistant liegt darin, dass die LED Farbe von RGB nach Hue und Saturation, sowie umgekehrt umgerechnet werden muss.

rest:
  - scan_interval: 60
    resource: http://<ip>/jsondoaction
    sensor:
     - name: "ESP32 Gartenhaus LED - Hue-Color"
       value_template: "{{ value_json.hue | int }}"
       
     - name: "ESP32 Gartenhaus LED - Saturation-Color"
       value_template: "{{ value_json.saturation | int }}"
       
     - name: "ESP32 Gartenhaus LED - Brightness"
       value_template: "{{ value_json.brightness | int }}"
rest_command:
  gartenhaus_led_set_color:
    url: "http://<ip>/jsondoaction"
    method: POST
    headers:
      accept: "application/json, text/html"
    payload: '{ "hue": {{ h }}, "saturation": {{ s }} }'
    content_type:  'application/json; charset=utf-8'
    
  gartenhaus_led_on:
    url: "http://<ip>/jsondoaction"
    method: POST
    headers:
      accept: "application/json, text/html"
    payload: '{ "ison": "1"}'
    content_type:  'application/json; charset=utf-8'
    
  gartenhaus_led_off:
    url: "http://<ip>/jsondoaction"
    method: POST
    headers:
      accept: "application/json, text/html"
    payload: '{ "ison": "0"}'
    content_type:  'application/json; charset=utf-8'
    
  gartenhaus_led_set_level:
    url: "http://<ip>/jsondoaction"
    method: POST
    headers:
      accept: "application/json, text/html"
    payload: '{ "brightness": {{ brightness }} }'
    content_type:  'application/json; charset=utf-8'
switch:
  - platform: rest
    name: "ESP32 Gartenhaus LED - Enabled"
    resource: http://<ip>/jsondoaction
    method: post
    body_on: "{'ison': '1' }"
    body_off: "{'ison': '0' }"
    is_on_template: "{{ value_json.ison }}"
    headers:
      Content-Type: application/json

Das Farbpicker Control wird hiermit erstellt und steht dann im Oberflächendesigner zur Verfügung:

light:
  - platform: template
    lights:
      gartenhaus_led_light:
        friendly_name: "Gartenhaus Colorpicker"
        color_template: "({{states('sensor.esp32_gartenhaus_led_hue_color') | int}}, {{states('sensor.esp32_gartenhaus_led_saturation_color') | int}})"
        level_template: "{{ states('sensor.esp32_gartenhaus_led_brightness') | int }}"
        set_color:
        - service: rest_command.gartenhaus_led_set_color
          data:
            h: "{{ h }}"
            s: "{{ s }}"
        set_level:
        - service: rest_command.gartenhaus_led_set_level
          data:
            brightness: "{{ brightness }}"
        turn_on:
        - service: rest_command.gartenhaus_led_on
        turn_off:
        - service: rest_command.gartenhaus_led_off
Home Assistant Steuerelement für Farbe und Helligkeit

Software

Der ESP32 meldet proaktiv Änderungen an den Home Assistant. Dafür ist ein sogenanntes Token oder Bearer genannt notwendig, das im Home Assistant generiert werden kann.

#include <ArduinoJson.h>
#include <HTTPClient.h>
#include <WebServer.h>

#include <WiFiClientSecure.h>

#include <TaskScheduler.h>

#include <NTPClient.h>
#include <WiFiUdp.h>

#include <ESP_Color.h>


const String LedEnabledSwitchName = "switch.esp32_gartenhaus_led_enabled";
const String LedHueIntName = "sensor.esp32_gartenhaus_led_hue_color";
const String LedSaturationIntName = "sensor.esp32_gartenhaus_led_saturation_color";
const String LedBrightnessIntName = "sensor.esp32_gartenhaus_led_brightness";

// --------- WIFI -----------

[...]

unsigned long previousMillis = 0;
unsigned long interval = 30000;

// --------- END WIFI -------

// --------- INITS -------

const char* ssid = STASSID;
const char* password = STAPSK;
const char* deviceName = DEVICENAME;
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "de.pool.ntp.org", 0, 0);
WebServer server(80);
StaticJsonDocument<1024> jsonDocument;
char jsonBuffer[1024];

const String HomeAssistantBearerName = "Authorization";
const String HomeAssistantBearerContent = "Bearer xxx";

const String SendApiIotUrl = "http://<homeAssistantIp>:8123/api/states/";



// --------- END INITS -------

// --------- SCHEDULER BEGIN -------

void checkFreeRam();
Task scheduleCheckFreeRam(21*1000, TASK_FOREVER, &checkFreeRam);

void wifiReconnectCheck();
Task scheduleWifiReconnectCheck(8*60*1000, TASK_FOREVER, &wifiReconnectCheck);

void refreshTime();
Task scheduleRefreshTime(10*60*1000, TASK_FOREVER, &refreshTime);

Scheduler runner;

// --------- SCHEDULER END ---------

// ------- DEFINITIONS ----------
static int selfCheckPinDuration = 500;
static String linkColorNormal = "#2321B0";
static String linkColorVisited = "#2321B0";
static String activeMarkerBegin = "<b>&raquo;";
static String activeMarkerEnd = "&laquo;</b>";
// ------- END DEFINITIONS ----------

// ------- PINS ----------
static int morsePin = 2;
static int i2cSdaPin = 21;
static int i2cSclPin = 22;

static int pinOutBlue = 32;
static int pinOutRed = 33;
static int pinOutGreen = 27;
static int pinOutAll = 18;

const int freq = 5000;

const int blueChannel = 2;
const int redChannel = 0;
const int greenChannel = 1;

const int resolution = 8; // 256
// ------- END PINS ----------

// --------- Variables ---------

int freeHeap = 0;

bool isOn = false;
int blueVal = 255;
int redVal = 255;
int greenVal = 255;

int hueVal = 0;
int saturationVal = 0;
int brightnessVal = 100;

// --------- END Variables ---------

void setup() 
{
  // put your setup code here, to run once:
  initSerial();
  initWifi();
  initSchedules();
  initPinModes();
  initRgbPwm();
  buildFromRgb();
  updateLed();
  initTimeClient();
  initServer();
  checkFreeRam();  
}

void loop() 
{
  server.handleClient();
  runner.execute();
}

void initSerial()
{
  Serial.begin(115200); 
  while(!Serial){} // Waiting for serial connection
  Serial.println();
}


void initWifi()
{
  Serial.println("WiFi init");
  Serial.print("Wifi: ");
  Serial.println(ssid);
  //Serial.print("WifiPW: ");
  //Serial.println(password);
  
  Serial.println("turn wifi off...");
  WiFi.mode(WIFI_OFF);
  delay(10);
  //WiFi.forceSleepBegin();
  delay(200);
  //WiFi.forceSleepWake();
  WiFi.mode(WIFI_STA);
  delay(250);
  
  WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE);    
  delay(200); 
  //WiFi.mode(WIFI_STA);
  Serial.println("setting hostname");
  WiFi.setHostname(deviceName);
  delay(200); 
  Serial.println("Connecting to WiFi..");
  WiFi.begin(ssid, password);
  delay(200); 

  int iCounter = 0;
  int iMax = 30;
  while (WiFi.status() != WL_CONNECTED && iCounter < iMax)
  {
    digitalWrite(morsePin, LOW);
    delay(500);
    digitalWrite(morsePin, HIGH);
    delay(500);
    Serial.print(".");
    iCounter++;
  }
  
  Serial.println(WiFi.localIP()); 
}

void initPinModes()
{
  Serial.println("PIN MODES init");

  pinMode(morsePin, OUTPUT);
  digitalWrite(morsePin, LOW);

  pinMode(pinOutAll, OUTPUT);
  digitalWrite(pinOutAll, LOW);
}

void initSchedules()
{
  Serial.println("SCHEDULES init");
  
  runner.init();

  runner.addTask(scheduleCheckFreeRam);
  scheduleCheckFreeRam.enable();

  runner.addTask(scheduleWifiReconnectCheck);
  scheduleWifiReconnectCheck.enable();

  runner.addTask(scheduleRefreshTime);
  scheduleRefreshTime.enable();
}

void initTimeClient()
{
  timeClient.begin();
  delay(1000);
  timeClient.update();
}

void initServer()
{
  server.on("/", handleConnect);
  server.on("/jsondoaction", jsonDoAct);
  server.on("/jsondoaction", HTTP_POST, jsonDoAct);  
  server.onNotFound(handleConnect);
  server.begin();
  Serial.println("HTTP server started");
}

void wifiReconnectCheck()
{
  unsigned long currentMillis = millis();
  // if WiFi is down, try reconnecting every CHECK_WIFI_TIME seconds
  if ((WiFi.status() != WL_CONNECTED) && (currentMillis - previousMillis >=interval))
  {
    Serial.print(millis());
    Serial.println("Reconnecting to WiFi...");
    WiFi.disconnect();
    WiFi.reconnect();
    previousMillis = currentMillis;
  }
}

void refreshTime()
{
  timeClient.update();
}

void checkFreeRam()
{
  freeHeap = ESP.getFreeHeap();

  if (ESP.getFreeHeap() < 60000)
  {
    ESP.restart();
  }
}

// RGB functions

void initRgbPwm()
{
  ledcSetup(blueChannel, freq, resolution);
  ledcSetup(redChannel, freq, resolution);
  ledcSetup(greenChannel, freq, resolution);
  
  // attach the channel to the GPIO to be controlled
  ledcAttachPin(pinOutBlue, blueChannel);
  ledcAttachPin(pinOutRed, redChannel);
  ledcAttachPin(pinOutGreen, greenChannel);
}

void enableFetAll(bool bIn)
{
  if (bIn == true)
  {
    digitalWrite(pinOutAll, HIGH);
  }
  else
  {
    digitalWrite(pinOutAll, LOW);
  }

  isOn = bIn;
  sendStatusChangeSwitch(LedEnabledSwitchName, bIn);

}

void setLedValue(int iChannel, int iVal)
{

  int valToSet = (int) ( ( (float)brightnessVal / (float)255 ) * (float)iVal );

  if (valToSet >= 256)
  {
    valToSet = 255;
  }
  else if (valToSet < 0)
  {
    valToSet = 0;
  }

  ledcWrite(iChannel, 255-valToSet);
}

String convertToHexColor()
{
  String retStr = "";

  byte R = redVal;
  byte G = greenVal;
  byte B = blueVal;

  char hex[7] = {0};
  sprintf(hex,"%02X%02X%02X",R,G,B);

  retStr += String(hex);

  return retStr;
}

void buildFromRgb()
{
  float tmpRed = (float)redVal/(float)255;
  float tmpGreen = (float)greenVal/(float)255;
  float tmpBlue = (float)blueVal/(float)255;

  ESP_Color::Color colTmp = ESP_Color::Color(tmpRed, tmpGreen, tmpBlue);
  auto hsv = colTmp.ToHsv(); 

  float tmpHue = (float)360 * (float)hsv.H;
  float tmpSaturation = (float)100 * (float)hsv.S;

  hueVal = (int)tmpHue;
  saturationVal = (int)tmpSaturation;
}

void buildFromHsv()
{
  ESP_Color::Color colTmp = ESP_Color::Color::FromHsv((float)hueVal/(float)360, (float)saturationVal/(float)100, 1.00f);

  float tmpRed = ((float)255) * (float)colTmp.R;
  float tmpGreen = ((float)255) * (float)colTmp.G;
  float tmpBlue = ((float)255) * (float)colTmp.B;

  redVal = (int) tmpRed;
  greenVal = (int) tmpGreen;
  blueVal = (int) tmpBlue;
}

void updateLed()
{
  setLedValue(blueChannel, blueVal);
  setLedValue(greenChannel, greenVal);
  setLedValue(redChannel, redVal);
}

// HTTP Server

void handleConnect()
{
  Serial.println("Connect");
  server.send(200, "text/html", SendHTML("")); 
}

String SendHTML(String context)
{
  String ptr = "<!DOCTYPE html> <html>\n";
  ptr +="<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, user-scalable=no\">\n";
  ptr +="<title>ESP32 Gartenhaus LED</title>\n";
  ptr +="<style>html { font-family: Arial; display: inline-block; margin: 0px auto; text-align: center;}\n";
  ptr +="body{margin-top: 50px;} h1 {color: #444444;margin: 50px auto 30px;} h3 {color: #444444;margin-bottom: 50px;}\n";
  ptr +=".button {display: block;width: 80px;background-color: #3498db;border: none;color: white;padding: 13px 30px;text-decoration: none;font-size: 25px;margin: 0px auto 35px;cursor: pointer;border-radius: 4px;}\n";
  ptr +="a, a:active, { color: " + linkColorNormal + "; text-decoration: underline; }\n";
  ptr +="a:visited { color: " + linkColorVisited + "; text-decoration: underline; }\n";
  ptr +="p {font-size: 14px;color: #888;margin-bottom: 10px;}\n";
  ptr +="</style>\n";
  ptr +="</head>\n";
  ptr +="<body>\n";
  ptr +="<h1>ESP32 Gartenhaus LED</h1>\n";

  // TODO Webinterface

  String lineBreak = "<br><br>";

  ptr += "";

  ptr +="</body>\n";
  ptr +="</html>\n";
  return ptr;
}

void jsonDoAct()
{
  Serial.println("JSON Act");

  String status = "OK";
  int tmpInput = 0;
  bool tmpOnOff = false;
  bool tmpBuildFromRgb = false;
  bool tmpBuildFromHsv = false;
  bool tmpRefreshBrightness = false;

  if (server.hasArg("plain") == false) 
  {
    //handle error here
  }
  
  String body = server.arg("plain");
  Serial.println(body);
  deserializeJson(jsonDocument, body);

  // continue
  status = "OK";

  // ON OFF
  if (jsonDocument.containsKey("ison") == true)
  {
    tmpInput = jsonDocument["ison"];
    if (tmpInput == 1)
    {
      brightnessVal = 255;
      enableFetAll(true);
    }
    else
    {
      brightnessVal = 0;
      enableFetAll(false);
    }

    tmpOnOff = true;
  }

  // RGB
  if (jsonDocument.containsKey("blue") == true)
  {
    tmpInput = jsonDocument["blue"];
    if (tmpInput >= 0 && tmpInput <= 255)
    {
      blueVal = tmpInput;
      tmpBuildFromRgb = true;
    }
  }
  if (jsonDocument.containsKey("green") == true)
  {
    tmpInput = jsonDocument["green"];
    if (tmpInput >= 0 && tmpInput <= 255)
    {
      greenVal = tmpInput;
      tmpBuildFromRgb = true;
    }
  }
  if (jsonDocument.containsKey("red") == true)
  {
    tmpInput = jsonDocument["red"];
    if (tmpInput >= 0 && tmpInput <= 255)
    {
      redVal = tmpInput;
      tmpBuildFromRgb = true;
    }
  }

  // HSV
  if (jsonDocument.containsKey("hue") == true)
  {
    tmpInput = jsonDocument["hue"];
    if (tmpInput >= 0 && tmpInput <= 360)
    {
      hueVal = tmpInput;
      tmpBuildFromHsv = true;
    }
  }
  if (jsonDocument.containsKey("saturation") == true)
  {
    tmpInput = jsonDocument["saturation"];
    if (tmpInput >= 0 && tmpInput <= 100)
    {
      saturationVal = tmpInput;
      tmpBuildFromHsv = true;
    }
  }

  if (jsonDocument.containsKey("brightness") == true)
  {
    tmpInput = jsonDocument["brightness"];
    if (tmpInput >= 0 && tmpInput <= 255)
    {
      brightnessVal = tmpInput;
      if (brightnessVal == 0)
      {
        enableFetAll(false);
      }
      else
      {
        enableFetAll(true);
      }
      
      tmpRefreshBrightness = true;
      tmpOnOff = true;
    }
  }

  // build
  if (tmpBuildFromRgb == true || tmpBuildFromHsv == true || tmpRefreshBrightness == true || tmpOnOff == true)
  {
    if (tmpBuildFromRgb == true)
    {
      buildFromRgb();
    }
    
    if (tmpBuildFromHsv == true)
    {
      buildFromHsv();
    }

    updateLed();

    updateColorData();
  }

  createStatusJson(status);

  Serial.println(status);

  server.send(200, "application/json", jsonBuffer);
}

// -------------------- JSON HELPER --------------------------

void createStatusJson(String statusIn) 
{
  if (statusIn == "")
  {
    statusIn = "OK";
  }
  
  jsonDocument.clear();  
  jsonDocument["state"] = statusIn;

  jsonDocument["ison"] = isOn;

  jsonDocument["blue"] = blueVal;
  jsonDocument["green"] = greenVal;
  jsonDocument["red"] = redVal;

  jsonDocument["hexcolor"] = convertToHexColor();

  jsonDocument["hue"] = hueVal;
  jsonDocument["saturation"] = saturationVal;
  jsonDocument["brightness"] = brightnessVal;

  jsonDocument["freeram"] = freeHeap;

  serializeJson(jsonDocument, jsonBuffer);
}

// -------------------- HTTP HELPER --------------------------

String boolToSwitch(bool inVal)
{
  if (inVal == true)
  {
    return "on";
  }
  return "off";
}

void sendStatusChangeInt(String attributeStringIn, int valueIn)
{
  Serial.println(sendInputStatusHttp(attributeStringIn, String(valueIn), false));
}

void sendStatusChangeSwitch(String attributeStringIn, bool valueIn)
{
  Serial.println(sendInputStatusHttp(attributeStringIn, boolToSwitch(valueIn), true));
}

void updateColorData()
{
  sendStatusChangeInt(LedHueIntName, hueVal);
  sendStatusChangeInt(LedSaturationIntName, saturationVal);
  sendStatusChangeInt(LedBrightnessIntName, brightnessVal);
}

String sendInputStatusHttp(String attributeStringIn, String valueIn, bool isString)
{
  String retStr;

  HTTPClient *http = new HTTPClient();
  http->setReuse(false);

  Serial.print("[HTTP] begin...\n");
  Serial.println("[HTTP] URL: " + SendApiIotUrl + attributeStringIn);
  
  if (http->begin(SendApiIotUrl + attributeStringIn))
  {
    Serial.print("[HTTP] POST...\n");
    http->setTimeout(5000);
    // start connection and send HTTP header
    http->addHeader("Content-Type", "application/json; charset=UTF-8");
    http->addHeader(HomeAssistantBearerName, HomeAssistantBearerContent); // auth

    // status Json
    String sendContent = "";
    if (isString == true)
    {
      sendContent = "{\"state\": \""+valueIn+"\" }";
    }
    else
    {
      sendContent = "{\"state\": "+valueIn+" }";
    }

    Serial.println("Content: " + sendContent);
    
    int httpCode = http->POST(sendContent);

    // httpCode will be negative on error
    if (httpCode > 0)
    {
      // HTTP header has been send and Server response header has been handled
      Serial.printf("[HTTP] POST... code: %d\n", httpCode);

      // file found at server
      if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY)
      {
        //String payload = http->getString();
        //Serial.println(payload);
        retStr = http->getString();
      }

      //String payload = http->getString();
      //Serial.println(payload);
    }
    else
    {
      Serial.printf("[HTTP] GET... failed, error: %s\n", http->errorToString(httpCode).c_str());
      retStr = "error: ";
      retStr += http->errorToString(httpCode).c_str();
    }

    http->end();
  }
  else
  {
    Serial.printf("[HTTP] Unable to connect\n");
    retStr = "ConErr";
  }

  delete http;
  http = NULL;

  //delay(200);

  return retStr;
}

Hinweis

Dieser Artikel dokumentiert lediglich meinen Aufbau. Für den Nachbau, die Nutzung einzelner Komponenten, die Platinen und den gesamten Inhalt wird die Haftung in jeglicher Form ausgeschlossen.