Terminanzeige mit e-Paper am ESP32, MQTT, Node-Red und ESPHome - Teil 2

The following post was sent to us by our reader Eberhard Werminghoff. Have fun reading and tinkering.

in the first part We showed this small series the e-paper display in its variants and connected to the ESP32. We also abstract the structure of the software for the calendar. In this second part we import the flow in Node-Red and design the output on the display. Then we set up ESPHOME and program the ESP32.

  • Import our "flow" according to Node-Red

In Node-Red there is a way to exchange flows with each other. For this purpose, the flows to be divided are saved via an export in a JSON file. I have already done this with myself and set the Export now available here as a download. She has the name "calendar.json". We now want to import them into Node-Red. To do this, we click on the three small lines (Hamburger menu symbol) in Node-Red at the top right and then select the "Import" item. The "Import" window opens. At the same time, the "clipboard" index card has opened. We click on "Select File for Import" and select the "Kalende.json" file in the open window where we had saved it when downloading. At the bottom we have to decide where we want to import in Node-Red. Here we should select "New Flow". And then press the red button "Import". In the riding bar we can now find a rider of the "waste calendar".

The flow has now been imported, but there is still a lack of some small things so that the calendar data can be picked up from Google.

First of all, we have to press "takeover" so that the many blue dots disappear. The message may come:

 "The work area contains some nodes that are not properly set: ..."

But that should not disturb us and confirm the takeover. Now there are still a lot of red triangles on many elements. These elements send information to the MQTT broker (if it weren't for the red triangles) and complain because they don't know a broker.

  • Enter Google Calendar in Node-Red

Node-Red should call up the data from the Google Calendar "Floor cleaning" or "garbage disposal" and represent them in a form readable for other knots. The nodes required for this are not part of the standard range of Node-Red and must be recharged by us. We click again on the "Hamburger menu symbol" at the top right and select "Manage the Palette" with one click. In the now open window, the "Palette" tab has already been activated on the left and shows the two tabs "installed nodes" and "installation" at the top. There we select "Installation" and enter the "Node-Red Contrib-ICAL Event" search field.

Wherever I “installed”, you can see a “installation”. Please press it and have installed the "Ical" range. It can now be found in the left column under "Ical".

Now go to the imported flow “waste calendar” and open the knot there with the name “Waste Plan”. In addition to “Calendar Config”, you will find a pulldown menu in which you select “Add new type‘ I-Cal-Config ’” and edit this new access with the pen.

We switch to the Google calendar. There is a gear on the top right. Behind it are the settings of the existing calendars. Choose your garbage calendar - for me it means "garbage disposal". The small arrow down opens a window in which the data can be entered for the calendar.

There we are particularly interested in the point "Integrate Calendar" and under "Adjust" the "private address in the ICAL format"

Copy this address into the clipboard. Then switch back to Node-Red and Add the address from the clipboard in the node-Red node "Waste Plan" to the field "ICAL/ICS/CALDAV ... “Another.

Only a small part of the address can be seen in the picture above, it is considerably longer.

Now “update”, “ready” and “takeover (deploy)” and the calendar is now available for Node Red ..

A notice: It is urgently needed to use at least version 2.1.2, because otherwise individual of the data fields can be found under other names!

A separate calendar has been set up for the weekly hall cleaning. If you do not set it up, the field will later remain empty in the advertisement behind “Fall cleaning:”. If you have your own application, you can now set up the associated calendar - as described above.

  • Enter MQTT server in Node-Red

We have provided the data from the calendar with the help of Node-Red and must also transmit it to the broker via Node-Red. To do this, it is necessary to enter the access data of the MQTT server (Mosquitto) in Node-Red.

To do this, open any "MQTT OUT" node in Node-Red, which is shown on the side, with a double click. He can have a different lettering, e.g. “weekday/A4” in the “Server” line, see the arrow of the pull-down field. Please click on it. Then click on the "Add new" MQTT-Broker 'type "line. Then click on the pen below and a page opens that looks like this:

Now follow information that you should already know about the installation of the broker. Give a freely selectable name. In addition to "server", enter the IP address of the MQTT server, the remaining information from the "Connection" rider can be found in the previous image. The client ID is automatically assigned, so please do not write it down from the picture.

If you have assigned a username and access fun for access to the MQTT_Server, enter it in the "Security" tab.

The fields behind the "News" tab can remain empty.
Press "Update". Then you will find your broker listed in the field next to "Server". “Finished” and “Takeover (Deploy)” press the facility one after the other.

So that the ugly red triangles on all elements, we have to disappear each "Mqtt out" - element tell which broker he should use. In the pull-down field "Server" there is still:

There we find the name that you have forgiven yourself. Press the "Finished" button. The red triangles change into blue circles. The blue circles also go away through a "takeover (deploy)". The message:

 "The work area contains some nodes that are not properly set: ..."

We already know and ignore them. Again the "takeover (deploy) confirm". After a short duration, a "connected" should be seen among the edited elements - everything is fine.

  • The header and the first overhead line - "from - to"

Here we see the nodes that are connected by lines for the first time. I will explain the nodes used (nodes) here. Finally, there is the JSON file importable in Node-Red for all four functional blocks.

The "Timestamp" node provides the time in milliseconds since January 1, 1970 at the knot output. A damn large number -but it works!

The "Simpletime" node (additional nodes from the Node-Red Contrib simplime range in version 2.10.03) converts the number transmitted by the TIMESTAMP into different date forms. At the output, the year (2022), the month (02), the day (03), the day within the current week (4 for Thursday) and the current calendar week are made available.

Everything that cannot map the large number of nodes is programmable in a functional node, recognizable on this symbol:

The current date in the header on the left is assembled in the functional node 'Current Date'. Here is the content:

 var Day = MSG.mydom;
var month = MSG.mymone;
var year = MSG.myyear;
MSG.Payoad = Day + "."+ month+"."+year
return MSG;

This provides the current date of day for further use at the output.

Who could do something with the date? Right: MQTT - the broker - can use the date and we "publish" (publish) knot it with a "mqtt out". At the same time, we also share the broker where to put the date so that the subscribers to the date also find it. So we share the broker a kind of mailbox where it should go. This mailbox is called "Topic". A message to the broker always includes a "topic" in the "MQTT out" node. Here with us the "MQTT out" nodes get the name of the topic.

A notice: On this occasion, check whether the name of your MQTT server is in the field next to "Server". If not: press the pull-down and select the server. The following also applies here: "ready" and then press "takeover" (top right).

You see that communication ends behind this knot. The data package was delivered, so: "Mission over!".

We treat the other data provided in the same/similar way. Different data also mean different topics. But please with a little structure in the "mailboxes". All data for this project are below the topic "/SmartHome/Display/". We will keep that.

A notice: There is no other way: order must be! At first I simply classified the data under the topic "/". A fact that I later regretted. All topics had to be reorganized and thus changed. Therefore, keep order in the topics right from the start. I currently have about 200 topics, if you don't keep order, you are lost when troubleshooting.

For the line, at the beginning of the week and ends, two separate calendar data must be calculated: start and weekend. The calculation is individual and - as we read above - belongs in a functional node, here: the knot "Week from - to".

 Let dt = new Date();
Let dow = dt.getday();

IF (dow==0)
{
dow=7;
}

Date.prototype.subtractdays = function (D) {
This.set (This.frozen () - (D*24*60*60*1000));
return This;
}

Date.prototype.add -days = function (D) {
This.set (This.frozen () + (D*24*60*60*1000));
return This;
}

Let Day1 = new Date().subtractdays (dow).getdated()+1; // day of the month
IF (Day1<10){Day1 = "0"+Day1;}
Let Month1 = new Date().subtractdays (dow).getmonth()+1; // month of the year
Let year1 = new Date().subtractdays (dow).getfullyear(); // calendar year
var dt1=Day1 + "."+ Month1 + "."+ year1;

Let Day2 = new Date().add -days (7-dow).getdated(); // day of the month
IF (Day2 < 10) {Day2 = "0"+ Day2;}
Let Month2 = new Date().add -days (7-dow).getmonth()+1; // month of the year
Let year2 = new Date().add -days (7-dow).getfullyear(); // calendar year
var dt2=Day2 + "."+ Month2 + "."+ year2;
MSG.Payoad = dt1 + " - " +dt2;
MSG.year1 = dt1;
MSG.year2 = dt2;
return MSG;

Here and in the upcoming lines are always knots with a "ABC“To find on the right. These are text nodes to display certain texts or numbers on the user interface.

For me, the content of the display can also be found in the user interface of Node-Red (see point 3 in the diagram on the bottom right "Display in Node-Red").


  • The second over -lines - corridor cleaning

A notice: It is urgently needed to use at least version 2.1.2, because otherwise individual of the data fields can be found under other names.

If you have not set up a second calendar under 1.1.1, no data can inevitably be transmitted here (see note below: Topic Creativity).

In the "apartment number" node, the transfer of the information determined is prepared and sent to the next node.

At this point is the start of your own creativity: a calendar for the cat toilet, who clears out the dishwasher? and much more It's nice to see that it is not your turn yourself ☺. The data from the comment field of the calendar (here: WHG3) are shown in the two headers of the display.

The duration of the responsibility of 1 week is entered in the Google calendar in the fields "from" and "to".

During the duration entered, responsibility is indicated unchanged in the headers.

  • The actual removal calendar

Our table has four lines with four data fields each. I once depicted the first table line at the top. The other three table lines are structured analogously to this line.

  • Data column 1: Short form of the weekday

The short form of the weekday is shown in the first data column. This is converted from the data field "Dtend; Value = date: 20220210" of the calendar in the functional node for a short form. This conversion is described in the functional node:

 Let n = MSG.total;
 var I=0;
 IF(n>I)
 {
     var dow = MSG.Payoad[I].event;
     dow = new Date(dow).tutorial();
     switch (dow)
    {
         case 0:
         dow = 'So';
         break;
         
         case 1:
         dow = 'Mo';
         break;
         
         case 2:
         dow = 'Di';
         break;
         
         case 3:
         dow = 'Mi';
         break;
         
         case 4:
         dow = 'Do';
         break;
         
         case 5:
         dow = 'Fr';
         break;
         
         case 6:
         dow = 'Sa';
         break;
         
         default:
         dow = '--';
         break;
    }
 
 MSG.Payoad = dow;}
 
 return MSG;

The result is immediately sent to the broker known to us by mentioning the topic. This we also made for each line.

A notice: Variable I must be updated for the other table lines in the square brackets. So: 1.2 and 3 for 2nd to 4th line.

  • Data column 2: Departure date

The removal date is also in the Google calendar in the data field “DTSTTAUT; VALUE = DATE: 20220210" to find. The "waste plan" calendar node into a format

 date: "2022-02-24 00:00 - 2022-02-25 00:00"

converted and handed over to the functional nodes. We need the first 10 jobs (“2022-02-10”) to display the collection date. But since it is a calendar for 2022 anyway, I do without the year and save space. The "Completion" takes over a functional node with the content:

 Let n = MSG.total;
 var I=0;
 IF(n>I)
 {
     var Calday = MSG.Payoad[I].date.substring(8,10);
     var Calmonth = MSG.Payoad[I].date.substring(5,7);
     MSG.Payoad  = Calday + "." + Calmonth + "." ;}
 
 return MSG;

A notice: Variable I must be updated for the other table lines in the square brackets. So: 1.2 and 3 for 2nd to 4th line.

... and now off to the broker ...

  • Data column 3: event

The data available in the ICS file can now be taken over one to one.

 Let n = MSG.total;
 var I=0;
 IF(n>I)
 {
     var event = MSG.Payoad[I].summary;
 
     MSG.Payoad  = event;
 }
 Else
 {
     MSG.Payoad = "no data";
 }
 return MSG;

A notice: Here, too, the variable i must go through the values ​​0 to 3 for lines 1 to 4 depending on the line.

  • Data column 4: actor

This column describes responsibility for an event. I basically have four colorful tons available at home. However, the tons for organic waste and paper have no special responsibility. You get three "-" in the fourth column. So if the Google calendar is empty in the remarks at these tons, "---" is issued.

 Let n = MSG.total;
 var I=0;
 IF(n>I)
 {
     var responsible = MSG.Payoad[I].original event.description;
     IF (responsible == "")
    {
         responsible =" --- ";
    }
     MSG.Payoad = responsible;
 }
 Else
 {
     MSG.Payoad = "---"
 }
 return MSG;

We have described a data line, the other three are also built up. If necessary, the table can be expanded or modified by additional lines/columns.

A notice: Here, too, the variable i must go through the values ​​0 to 3 for lines 1 to 4 depending on the line.

  • The footer

The data of the footer is not determined in Node-Red and sent to the display via MQTT, they are generated in Esphome and also shown from there.

  • The start stop switch for the Deep Sleep of the ESP32

We will mainly operate the ESP in Deep Sleep mode. This has great advantages, especially with battery operation. But we will also import software updates via WLAN, ie "Over the Air" (OTA). And that is exactly where the problem begins: the longer the Deep Sleep Phase takes, it becomes less likely to reach the ESP32 via OTA during a wax phase. The shorter the wax phase is the more difficult; In our project about 10 to 15 seconds.

That is why we created two switches in Node-Red that look as follows:


In the two "mqtt_out" nodes, a "2" must be contained in the "QOS" fields and a "true" are entered in the pulldown field to the right of "Retain". The switches send their condition to the broker via the Topic “/SmartHome/Display/Sleep_Mode” or “/SmartHome/Display/OTA_Mode”.

The "No Deep Sleep" switch only applies after a reset of the ESP. Deep-Sleep cannot be prevented in an existing guard phase. After the end of the opened Deep Sleep phase, the ESP is no longer sent into deep sleep. After reaching the waking state, the "No Deep Sleep" switch must be eliminated.

If the ESP is to be sent back into deep sleep, the "Start Deep Sleep" switch must now be switched on. After reaching the Deepsleep, this switch can also be switched off.

All data for the display is transmitted to the MQTT broker via a "MQTT out" node ("Subscribe").


  • ESPhome

  • Setting up the project

Hopefully the installation worked. I now require a functioning ESPHOME system. You haven't installed yet? Then follow these few steps here!

If everything was installed well, the screen would have to look as follows or similar.

During the installation, a directory was Esphome generated on a drive specified. My directory is on the drive "I:".

There they switch mi

  > I:

In the LW I: Switch to the directory with

 > I: \ CD Esphome

You start a new project named display-4in2.yaml With the input on the prompt and at the same time calls up the Wizard of Esphome:

  I: \ Esphome> Esphome Wizard Display-4in2.yaml

A notice: At this point, Esphomas only accepts small letters, digits and the hyphen.

The system is very polite and now shows:

 I: \ Esphome> Esphome Wizard Display-4in2.yaml
 Hi there!
 I'M The Wizard of Esphome :)
 And I'M here to help you get started with esphome.
 In 4 Steps I'M Going to Guide You Through Creating A Basic Configuration File for Your Custom ESP8266/ESP32 firmware. Yay!
 
 
 
 ============= Step 1 =============
    _____ ____ _____ ______
    / ____/ __ \| __ \| ____|
  | |   | | | | |__) | |__
  | |   | | | | _ /| __|
  | |___| |__| | | \ \| |____
    \_____\____/|_| \_\______|
 
 ===================================
 First up, please choost a name for your node.
 It should be a unique name that that can be used to identify the device later.
 For Example, I like Calling the node in My Living Room Livingroom.
 
 ← [1; 37m (name): ← [0m

In the prompt you enter the name "Display-4in2". If you have not taken into account the note regarding the usable characters above, you will be "rewarded" with an error message.

 ← [1; 37m (name): ← [0mDisplay-4in2
 Great! Your node Is now called "Display-4in2".
 
 
 ============= Step 2 =============
      ______ _____ _____
      | ____|/ ____| __ \\
      | |__ | (___ | |__) |
      | __| \___ \| ___/
      | |____ ____) | |
      |______|_____/|_|
 
 ===================================
 NOW I'D Like to Know What Microcontroller You'Re using so that i can compile firmware for It.
 Are you using an ESP32 OR ESP8266 Platform? (Choose ESP8266 for Sonoff Devices)
 
 Please enter Either ESP32 OR ESP8266.
 ← [1; 37m (ESP32/ESP8266): ← [0m

Now you will be asked if you want to use an ESP8266 or an ESP32. Enter "ESP32". The system's answer is extensive:

 ← [1; 37m (ESP32/ESP8266): ← [0mESP32
 Thanks! You've chosen esp32 as your platform.
 
 Next, i need to know what board you'Re using.
 Please go to http://docs.platformio.org/en/latest/platforms/espressif32.htmL#Boards and Choose a board.
 (Type ESP01_1m for Sonoff Devices)
 
 For example "Nodemcu-32S".
 Options: ALKSESP32, AZ-Delivery-Devkit-V4, BPI-Bit, Briki_abc_ESP32, Briki_mbc-WB_ESP32, D-Duino-32, ESP-WROVER-KIT, ESP32-Devkitlipo, ESP32-EVB, ESP32-POE, ESP32-POE-ISO, ESP32-Pro, ESP320, ESP32DEV, ESP32DOIT-DEVKIT-V1, ESP32Doit-ESPDUino, ESP32ding, ESP32VN-IoT-Uno, Espea32, Especto32, EtherSP3 firebeetle32, fm-devkit, frogboard, healtypi4, heltec_wifi_kit_32, heltec_wifi_kit_32_v2, heltec_wifi_lora_32, heltec_wifi_lora_32_V2, heltec_wireless_stick, heltec_wireless_stick_lite, honeylemon, hornbill32dev, hornbill32minima, imbrios-logsens-v1p1, inex_openkb, intorobot, iotaap_magnolia, iotbusio, iotbusproteus, kits-edu, labplus_mpython, lolin32, lolin32_lite, lolin_d32, lolin_d32_pro, lopy, lopy4, m5stack-atom, m5stack-core-ESP32, M5STACK-Core2, M5stack-Coreink, M5stack-Fire, M5stack-Timer-cam, M5stick-C, Magicbit, MGBOT-DIOTIK32A, MGBOT-DIOTIK32B, MHETESP32DEVKIT, MHETESP32Minikit, Microdu ino-core-esp32, nano32, nina_w10, node32s, nodemcu-32s, nscreen-32, odroid_esp32, onehorse32dev, oroca_edubot, pico32, piranha_esp32, pocket_32, pycom_gpy, qchip, quantum, s_odi_ultra, sensesiot_weizen, sg-o_airMon, sparkfun_lora_gateway_1-channel, Tinypico, TTGO-LORA32-V1, TTGO-LORA32-V2, TTGO-LORA32-V21, TTGO-T-BEAM, TTGO-T-WATCH, TTGO-T1, TTGO-T7-V13-Mini32, TTGO-T7-V14- Mini32, Turta_Iot_node, Vintlabs-Devkit-V1, Wemos_D1_Mini32, Wemosbat, WESP32, Widora-Air, Wifiduino32, Xinabox_cw02
 ← [1; 37m (board): ← [0m

These are the ESP32 boards that are supported by ESPHOME. Here they give "AZ Delivery-Devkit-V4" a:

 ← [1; 37m (board): ← [0mAZ Delivery-Devkit-V4
 Way to go! YoU'Ve Chosen Az-Delivery-Devkit-V4 as your board.
 
 
 
 ============= Step 3 =============
    __         ___ ______ _
    \ \       / (_) ____(_)
    \ \ /\ / / _| |__   _
      \ \/ \/ / | | __| | |
      \ /\ / | | |   | |
        \/ \/   |_|_|   |_|
 
 ===================================
 In this step, i'M Going to Create the Configuration for Wifi.
 
 First, what'S the SSID (The Name) of the Wifi Network Display-4in2 Should Connect to?
 For example "Abraham Linksys".
 ← [1; 37m (SSID): ← [0m

You will now be asked with the SSID for the WLAN data, you can easily edit them later: So carry here "JHKHFKJ“Or your own SSID and are then asked about the right password.

 ← [1; 37m (SSID): ← [0mJHKHFKJ
 Thank you very much! You've just chosen "Jhkhfkj" as your ssid.
 
 Now please state the password of the wifi network so that i can connect to it (Leave empty for no password)
 
 For example "Password42"
 ← [1; 37m (PSK): ← [0m

As a password here, enter "my password" or your own WLAN password:

 ← [1; 37m (PSK): ← [0mmy password
 Perfect! Wifi is now set Up (You can create Static Ips and So On Later).
 
 
 ============= Step 4 =============
        ____ _______
      / __ \__   __|/\\
      | | | | | | / \\
      | | | | | | / /\ \\
      | |__| | | |/ ____ \\
      \____/ |_/_/   \_\\
 
 ===================================
 Almost there! ESPHOME CAN Automatically Upload Custom Firmware Over Wifi (Over The Air) and Integrates Into Home Assistant with a native API.
 This can be Insecure IF you do Not Trust the Wifi Network. Do you want to set a password for COnnecting to this ESP?
 
 Press Enter for no password
 ← [1; 37m (Password): ← [0m

Now it is about updating the software via WLAN "Over the Air (" OTA "). If this transmission is to be secured by an additional password, enter a password or an enter for no password. Choose “Enter”.

 ← [1; 37m (Password): ← [0m
 
 Done! I'Ve Now Written A New Configuration File to Display-4in2.yaml
 
 Next steps:
  > Follow the Rest of the Getting Started Guide:
  > https://esphome.io/guides/geting_started_command_line.html#Adding-Some features
  > To Learn How to Customize Esphome and Install It To Your Device.
 
 I: \ Esphome>

Now there is in the directory I: \ Esphome the file display-4in2.yaml

If you open the file with an editor (e.g. Notepad ++), see the basic structure of the configuration file in Yaml.

 Esphome:
  Surname: Display-4in2
 
 ESP32:
  board: AZ Delivery-Devkit-V4
  framework:
  type: Arduino
 
 # Enable logging
 logger:
 
 # Enable Home Assistant API
 API:
  password: ""
 
 OTA:
  password: ""
 
 wifi:
  SSID: "JHKHFKJ"
  password: "my password"
 
   # Enable Fallback Hotspot (Captive Portal) in Case Wifi Connection Fails
  AP:
  SSID: "Display-4in2 Fallback Hotspot"
  password: "UcffBSF59PF2"
 
 captive_portal:

Important instructions:

  • In Yaml, the moving of lines has a very special meaning (similar to Python). Those who do not pay attention to this will mercilessly get error messages. There are always two spaces - not using the tab.
  • Remarks on the Yaml level begin at a line of line with a double cross (hash, diamond or "#").
  • Remarks at the programming level, i.e. behind "Lambda: |-" Start with "//"
  • After a colon comes always A spaces.

We already specified the most important points in the Wizard, so the important additions:

Logger: If you want to switch off the logger, you can simply delete it or comment with "#". Everything that has been deleted can be added (newly entered) at any time.

There are different levels for logging, we use "info"

API: There is a transition to the Home Assistant program. Since I do not use this software, I have commented on the two lines ..

OTA: Was treated in the wizard

wifi:

SSID: Enter your own SSID here

Password: Here the password set the password to match the SSID

AP: If a connection via WLAN does not come about, an Accesspoint (AP) is automatically opened with the specified access data. When the AP is opened, enter the IP of the AP in your browser. A page opens in which alternative SSID and password can be entered. But you overwrite the data from the above point "WiFi"

Captive_Portal: With a capive portal, a end device can first connect to the mostly unencrypted WLAN that can be reached without access data. In this state, however, the Captive Portal blocks any further access to the network or internet behind it. The device is almost caught in this area, from which the name is derived (Wikipedia). We don't need it, so it will delete it from our configuration.


  • General comments on our Esphome project

  • You see that ESPHOME consists of small modules that can be assigned to diverse properties. There are a total of over 150 modules
  • Each module on the highest level (far left) may only be available once. Everything that has the same properties is one level lower.
  • Modules can be addressed via the ID. Each ID may only be available once.
  • There are three types of sensors:

    1. sensor: numerical content
    2. binary_sensor: binary content (e.g. true, false)
    3. Text_Sensor: Alphanumeric content (e.g. date, time, MQTT content)

The MQTT data received by the ESP32 are in the configuration under:

   #MQTT reception data
  - platform: mqtt_subskribe
  ID: calendar week
  topic: /SmartHome/display/calendar week

be found.

  • font: The character sets to be used must be in a directory fonts Below the folder Esphome be inserted by them. The file format is "TTF". Thus fonts from Windows (e.g. C: \ Windows \ Fonts \) ​​can be followed directly esphome \ fonts \ to be copied. The font size is freely selectable. Since the German language also has umlauts in addition to the normal ASCII signs, a list of the signs to be used is stored in “Glyphs:” for each font.
  • Spi: Since the display does not respond via the SPI interface in our application, only two pins are defined:
    clk_pin:

and the

mosi_pin:

  • display:
    - Platform: Specifies the display used. The available displays are on the website of Esphome to find. The remaining pins are also defined from this:
    CS_PIN:,
    dc_pin:
    Busy_pin:
    reset_pin:

The special features, for example, is in
Model: Are defined.

In spi_id: Is on the ID of Spi: (waveshare_spi). In

Lambda: |- everything that cannot be configured can be "introduced" via C ++ as a code: In our example this is the complete control for the display

    MQTT:
    In the configuration, an incoming message on a certain topic is used in two places to prevent the deep_sleep (prevent) or start (Enter).

      If a message with the content "On" receives in a wax phase on the Topic Deepsleep/OTA_MODE, a renewed Deep Sleep is prevented with the command "Deep_sleep.Prevent: Deep_Sleep_1".
      If a message with the content "On" receives in a wax phase on the Topic Deepsleep/Deep_sleepmode4, a new deep-sleep is started with the "Deep_sleep.enter: deep_sleep_1" command.
      Deep_sleep_1 is the ID of deep_sleep.

      There would still be a lot of special features that are worth mentioning, but would absolutely go beyond the framework here. I can only advise you to try the Internet or the forums if you have any questions.

      • The listing of the entire Esphome configuration

      And now the total configuration to be used.

      ESPHOME supports the outsourcing of access data into so -called “Secrets” (secrets). According to a colon, you can either enter passwords directly, or with the directive ! Secret On an external file called secrets.yaml refer (info). I have this file for you as a template for you Download filed. There you have to replace the XXXXX with your data. The file must then be in the same folder.

       Esphome:
        Surname: Display-4in2
        platform: ESP32
        board: AZ Delivery-Devkit-V4
        on_boot:
        priority: -100
        these:
            - Wait_Until:
        mqtt.connected:
             # Wait for some valid temp data
            - component.update: SNTP_TIME
            - component.update: TPL_DOY
            - component.update: TPL_HOUR_SEC
            - component.update: rssi_value_dsp
             # Give some time to get the rest of the mqtt data
            - delay: 6s
            - component.update: Epaper_Display
       
         
       
       # Enable logging
       logger:
        level: INFO
       
       # Enable Home Assistant API
       # API:
       # Password: "test"
       
       OTA:
        password: "test"
       
       wifi:
        SSID: ! Secret wifi_ssid
        password: ! Secret wifi_password
        Fast_Connect: true
        Manual_ip:
        static_ip: ! Secret wifi_static_esp_board
        gateway: ! Secret wifi_gateway
        subnet: 255.255.255.0
        dns1: ! Secret wifi_dns1
       
         # Enable fallback hotspot (captive portal) in case wifi connection fails
        ap:
        ssid: !secret ap_ssid
        password: !secret ap_password
       
       captive_portal:
       
       time:
        - platform: sntp
        id: sntp_time
        timezone: Europe/Berlin
        servers:
            - 0.pool.ntp.org
            - 1.pool.ntp.org
            - 2.pool.ntp.org
           
           
       mqtt:
        broker: !secret mqtt_ip
        username: !secret mqtt_user
        password: !secret mqtt_password
        id: mqtt_client
        birth_message:
        will_message:
        on_message:
          - topic: /SmartHome/Display/ota_mode
        payload: 'ON'
        then:
              - deep_sleep.prevent: deep_sleep_1
          - topic: /SmartHome/Display/sleep_mode
        payload: 'ON'
        then:
              - deep_sleep.enter: deep_sleep_1
       
       
       deep_sleep:
        id: deep_sleep_1
        run_duration: 20s
        sleep_duration: 3600s
       
       
       binary_sensor:
        - platform: template
        name: "OTA Mode"
        id: ota_mode
        internal: true
        on_state:
            - if:
        condition:
        binary_sensor.is_on: ota_mode
        then:
                  - deep_sleep.prevent: deep_sleep_1
           
       sensor:
         #RSSI based on MAC-Address Waveshare-Board
        - platform: wifi_signal
        name: "WiFi Signal Sensor"
        update_interval: 30s
        id: rssi_value_dsp
           
           
           
       text_sensor:
         # Aktueller Tag im Jahr
        - platform: template
        name: "Tag im Jahr"
        lambda: |-
        auto doy = id(sntp_time).now().strftime("%j");
        return {doy};
        id: tpl_doy
        update_interval: 5s
       
         #Datum und Uhrzeit für die Fußzeile "Daten von"
        - platform: template
        name: "Stunden Minuten"
        lambda: |-
        auto time = id(sntp_time).now().strftime("%H:%M");
        return {time};
        id: tpl_hour_sec
        update_interval: 5s
         
         #MQTT-Empfangsdaten
        - platform: mqtt_subscribe
        id: kalenderwoche
        topic: /SmartHome/Display/Kalenderwoche
        - platform: mqtt_subscribe
        id: datum_akt
        topic: /SmartHome/Display/Datum
        - platform: mqtt_subscribe
        id: wochentag
        topic: /SmartHome/Display/Wochentag
        - platform: mqtt_subscribe
        id: woche_von
        topic: /SmartHome/Display/Woche1
        - platform: mqtt_subscribe
        id: woche_bis
        topic: /SmartHome/Display/Woche2
        - platform: mqtt_subscribe
        id: flurreinigung
        topic: /SmartHome/Display/Flurreinigung
           
           #Daten Zeile 1
        - platform: mqtt_subscribe
        id: wochentag1
        topic: /SmartHome/Display/A1
        - platform: mqtt_subscribe
        id: datum1
        topic: /SmartHome/Display/B1
        - platform: mqtt_subscribe
        id: ereignis1
        topic: /SmartHome/Display/C1
        - platform: mqtt_subscribe
        id: akteur1
        topic: /SmartHome/Display/D1
           
           #Daten Zeile 2
        - platform: mqtt_subscribe
        id: wochentag2
        topic: /SmartHome/Display/A2
        - platform: mqtt_subscribe
        id: datum2
        topic: /SmartHome/Display/B2
        - platform: mqtt_subscribe
        id: ereignis2
        topic: /SmartHome/Display/C2
        - platform: mqtt_subscribe
        id: akteur2
        topic: /SmartHome/Display/D2
       
           #Daten Zeile 3
        - platform: mqtt_subscribe
        id: wochentag3
        topic: /SmartHome/Display/A3
        - platform: mqtt_subscribe
        id: datum3
        topic: /SmartHome/Display/B3
        - platform: mqtt_subscribe
        id: ereignis3
        topic: /SmartHome/Display/C3
        - platform: mqtt_subscribe
        id: akteur3
        topic: /SmartHome/Display/D3
       
           #Daten Zeile 4
        - platform: mqtt_subscribe
        id: wochentag4
        topic: /SmartHome/Display/A4
        - platform: mqtt_subscribe
        id: datum4
        topic: /SmartHome/Display/B4
        - platform: mqtt_subscribe
        id: ereignis4
        topic: /SmartHome/Display/C4
        - platform: mqtt_subscribe
        id: akteur4
        topic: /SmartHome/Display/D4
           
       
           
           
       
       # Verwendete Zeichensätze
       font:
        - file: 'fonts/comic.ttf'
        id: font1
        glyphs:
             ['&', '@', '!', ',', '.', '?', '"', '%', '(', ')', '+', '-', '_', ':', '°', '0',
              '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E',
              'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
              'T', 'U', 'V', 'W', 'X', 'Y', 'Z', ' ', 'a', 'b', 'c', 'd', 'e', 'f',
              'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
              'u', 'v', 'w', 'x', 'y', 'z','å', 'Ä', 'ä', 'Ö', 'ö', 'Ü', 'ü', '/']
        size: 30
        - file: 'fonts/bahnschrift.ttf'
        id: font2
        glyphs:
             ['&', '@', '!', ',', '.', '?', '"', '%', '(', ')', '+', '-', '_', ':', '°', '0',
              '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E',
              'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
              'T', 'U', 'V', 'W', 'X', 'Y', 'Z', ' ', 'a', 'b', 'c', 'd', 'e', 'f',
              'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
              'u', 'v', 'w', 'x', 'y', 'z','å', 'Ä', 'ä', 'Ö', 'ö', 'Ü', 'ü', '/']
        size: 20
        - file: 'fonts/arialbd.ttf'
        id: font3
        glyphs:
             ['&', '@', '!', ',', '.', '?', '"', '%', '(', ')', '+', '-', '_', ':', '°', '0',
              '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E',
              'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
              'T', 'U', 'V', 'W', 'X', 'Y', 'Z', ' ', 'a', 'b', 'c', 'd', 'e', 'f',
              'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
              'u', 'v', 'w', 'x', 'y', 'z','å', 'Ä', 'ä', 'Ö', 'ö', 'Ü', 'ü', '/']
        size: 14
        - file: 'fonts/arial.ttf'
        id: font4
        glyphs:
             ['&', '@', '!', ',', '.', '?', '"', '%', '(', ')', '+', '-', '_', ':', '°', '0',
              '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E',
              'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
              'T', 'U', 'V', 'W', 'X', 'Y', 'Z', ' ', 'a', 'b', 'c', 'd', 'e', 'f',
              'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
              'u', 'v', 'w', 'x', 'y', 'z','å', 'Ä', 'ä', 'Ö', 'ö', 'Ü', 'ü', '/']
        size: 14
       
       #Waveshare display 4.2 inch
       spi:
        clk_pin: GPIO13
        mosi_pin: GPIO14
        id: waveshare_spi
       
       display:
        - platform: waveshare_epaper
        id: epaper_display
        cs_pin: GPIO15
        dc_pin: GPIO27
        busy_pin: GPIO25
        reset_pin: GPIO26
        spi_id: waveshare_spi
        model: 4.20in
        update_interval: never
        lambda: |-
           
        // Kopfzeile
        it.printf(8, 5, id(font3), TextAlign::TOP_LEFT, "%s",id(datum_akt).state.c_str());
        it.printf(200, 5, id(font3), TextAlign::TOP_CENTER, "%s",id(wochentag).state.c_str());
        it.printf(394, 5, id(font3), TextAlign::TOP_RIGHT, "%s / %s", id(kalenderwoche).state.c_str(), id(tpl_doy).state.c_str());
        it.line(8,20,395,20);
             
        // Header der Tabelle
        it.printf(200,50,id(font2), TextAlign::TOP_CENTER, "%s - %s",id(woche_von).state.c_str(),id(woche_bis).state.c_str());
        it.printf(200,80,id(font2), TextAlign::TOP_CENTER, "Flurreinigung: %s" ,id(flurreinigung).state.c_str());
                   
        // Rahmenlinien der Tabelle
        it.line(8, 110, 395, 110);
        it.line(8, 110, 8, 270);
        it.line(395, 110, 395, 270);
        it.line(8, 270, 395, 270);
        it.line(52, 110, 52, 270);
        it.line(130, 110, 130, 270);
        it.line(320, 110, 320, 270);
        it.line(8, 150, 395, 150);
        it.line(8, 190, 395, 190);
        it.line(8, 230, 395, 230);
        // Zeile 1
        it.print(18, 125, id(font2),id(wochentag1).state.c_str());
        it.print(60, 125, id(font2),id(datum1).state.c_str());
        it.print(140, 125, id(font2),id(ereignis1).state.c_str());
        it.print(329, 125, id(font2),id(akteur1).state.c_str());
        // Zeile 2
        it.print(18, 165, id(font2),id(wochentag2).state.c_str());
        it.print(60, 165, id(font2),id(datum2).state.c_str());
        it.print(140, 165, id(font2),id(ereignis2).state.c_str());
        it.print(329, 165, id(font2),id(akteur2).state.c_str());
        // zeile 3
        it.print(18, 205, id(font2),id(wochentag3).state.c_str());
        it.print (60, 205, ID (font2), id (datum3) .state.c_str ());
        it.print (140, 205, ID (font2), id (event3) .state.c_str ());
        it.print (329, 205, ID (font2), id (actor3) .state.c_str ());
        // line 4
        it.print (18, 245, id (font2), id (weekly day4) .state.c_str ());
        it.print (60, 245, ID (font2), id (datum4) .state.c_str ());
        it.print (140, 245, id (font2), id (event4) .state.c_str ());
        it.print (329, 245, ID (font2), id (actor4) .state.c_str ());
        // footer
        iT.Line (8,284,395,284);
        IT.Printf (8,300, ID (font4), Textalign :: Bottom_Left, "RSSI: %.0f dbm", id (rssi_value_dsp). State);
        IT.Printf (200,300, ID (Font4), Textalign :: Bottom_Center, "Version: 1.1");
        IT.Printf (397, 300, ID (font4), textalign :: Bottom_Right,"Data from %s", ID (TPL_HOUR_SEC) .state.c_str ());

      Note on the listing: In some cases, the side width is not sufficient to display the line. It will then be continued on the far left in the next line. Example: line 284 and 285 are one Program line!

      Download of the configuration file

      • Compiling the configuration

      After you have written the configuration and checked all the empty spaces and indentations, Can you with

       I: \ Esphome> Esphome Run Display-4in2.yaml

      Start the compilation. An extensive process that some time (approx. 3min) can take (depending on the computer).

      A notice: The process warns in some places of the use of certain ports, so you can confidently overlook it. They are warnings and no mistakes.

      As long as there are no error messages, is okay. At some point the computer then shows these or similar lines:

       ==============Success] took 161.02 seconds =================================
       ← [32minfo successfully compiled program. ← [0m
       Found multiple options, please choos of one:
        [1] COM13 (Silicon Labs CP210X USB to Uart Bridge (COM13))
        [2] COM3 (HP HS2340 HSPA+ Mobile Broadband Module Device Management (COM3))
        [3] COM4 (HP HS2340 HSPA+ Mobile Broadband Module Device Management (COM4))
        [4] COM5 (HP HS2340 HSPA+ Mobile Broadband Module Modem)
        [5] Over the Air (192.168.2.157)
       (Number):

      • Upload the software to EP32

      Now the computer asks which way to install the software: With the [1] COM13 (number 13 can be different with you) or with [5] Over the air (OTA).

      A notice: The Type of transmission OTA can only be used if the program has already been loaded via the COM interface and contained the part for OTA in the configuration. An OTA part is included, but since it is the first time, we [1] will select the COM interface

       (Number): 1
       ESPTOOL.PY V3.2
       Serial Port Com13
       Connecting .....
       Chip is ESP32-D0WDQ6 (revision 1)
       ...
       ...
       ...
       ...
       ...
       Leaving ...
       Hard Resetting via RTS PIN ...
       ← [32minfo successfully uploaded program. ← [0m
       ← [32minfo Starting Log Output from Com13 with Baud Rate 115200← [0m
       [18:16:18] E [I] [Logger: 214]: Log initialized
       [18:16:18] [I] [App: 029]: Running Through Setup () ...
       [18:16:18] [I] [Wifi: 245]: Wifi Connecting to 'XXXXX'...
       [18:16:20] [I] [Wifi: 502]: Wifi Connected!
       [18:16:20] [I] [MQTT: 175]: Connecting to mqtt ...
       [18:16:26] [W] [MQTT: 260]: MQTT Disconnected: TCP Disconnected.
       [18:16:26] [I] [MQTT: 175]: Connecting to mqtt ...
       [18:16:26] [W] [Wifi: 119]: Wifi Connection Lost ... Reconnecting ...
       [18:16:26] [I] [Wifi: 245]: Wifi Connecting to 'XXXXX'...
       [18:16:28] [I] [Wifi: 502]: Wifi Connected!
       [18:17:26] [I] [MQTT: 175]: Connecting to mqtt ...
       [18:17:26] [I] [MQTT: 215]: MQTT Connected!
       [18:17:26] [I] [Deep_sleep: 040]: Scheduling Deep Sleep to begin in 12000 MS
       [18:17:26] [I] [App: 062]: Setup () Finished Successfully!
       [18:17:26] [I] [App: 102]: Esphome version 2022.1.1 Compiled on Feb8 2022, 18:14:16
       [18:17:38] [I] [Deep_sleep: 103]: Starting Deep Sleep

      After the selection, a number of lines are produced that have an informative character. You can see what happens when. I have not listed all the lines - too much for a blog, hence the lines with the "...".

      The colored lines at the end are controlled by the logger. At 18:17:38 the display went into deep sleep. It will be finished in an hour and lead to a rest. We wait. ------- he woke up !!

       [19:17:06] ETS Jun8 2016 00:22:57
       [19:17:06]
       [19:17:06] RST: 0x5 (deepepsleep_reset), boot: 0x13 (spi_fast_flash_boot)
       [19:17:06] ConfigSip: 0, SpiWP: 0xee
       [19:17:06] CLK_DRV: 0x00, Q_DRV: 0x00, D_DRV: 0x00, CS0_DRV: 0x00, HD_DRV: 0x00, WP_DRV: 0x00
       [19:17:06] Fashion: Dio, Clock Div: 2
       [19:17:06] Load: 0x3fff0018, Len: 4
       [19:17:06] Load: 0x3fff001c, Len: 1044
       [19:17:06] Load: 0x40078000, Len: 10124
       [19:17:06] Load: 0x40080400, Len: 5828
       [19:17:06] Entry 0x400806a8
       [19:17:06][I] [Logger: 214]: Log initialized
       [19:17:06][I] [App: 029]: Running Through Setup () ...
       [19:17:06][I] [Wifi: 245]: Wifi Connecting to 'Buewlan'...
       [19:17:09][I] [Wifi: 502]: Wifi Connected!
       [19:17:09][I] [MQTT: 175]: Connecting to mqtt ...
       [19:17:10][I] [MQTT: 215]: MQTT Connected!
       [19:17:10][I] [deep_sleep: 040]: Scheduling Deep Sleep to begin in 12000 MS
       [19:17:10][I] [App: 062]: Setup () Finished Successfully!
       [19:17:10][I] [App: 102]: Esphome version 2022.1.1 Compiled on Feb8 2022, 18:14:16
       [19:17:22][I] [Deep_sleep: 103]: Starting Deep Sleep

      The deep sleep is over. A reset has woken up the display and provided new data. No warning, everything is fine.

      In theory, it would be possible to increase the duration of the deep sleep to 24 hours. I would not recommend that because the internal clock of the ESP32 does not run exactly and the update time would continue to shift. We already see a shift of 20 seconds here. I recommend deep sleep - depending on the type of calendar - between 1 and 4 hours. If the calendar shown can change during the day, it makes sense for an hour.

      I showed the time in the footer. It indicates the time at which the data was picked up by the broker. The date is on the left in the header and the associated time in the footer on the right. If the data transmission is defective, the time of the last transmission can be read.


      • Errors that have occurred

      The display is not presented completely:

      Deep-Sleep: has a wax phase (run_duration: ) and display: a Update_intervall :. That display: needs a few seconds. to update the display. if display: The update has not yet ended, but the timer for the waking phase has already expired, the system mercilessly goes into the Deep Sleep and does not take care of whether the update has been completed or not. Result: The display has unpleasant fields. This is only remedied by a change in the two above -mentioned timer settings: run_duration: must be chosen so large that the update of display: was safely completed.

      In the current version I have that Update_intervall: on never set, this means that the two timers do not get in the way. I listed this mistake for your other projects because he had busy me for some time.

      The screen remains dark after a reset

      Please check whether the "Start Deep Sleep" switch in Node-Red is "off". If he is up to “a”, a deep sleep is already being initiated within the setup. Then the setup is not properly completed.


      With your current knowledge, you can easily implement your wishes for your own individual calendar. Now I wish you a lot of fun with your display.

      Part 1, part 3


      Eberhard Werminghoff

      DisplaysEsp-32Projekte für anfängerSmart home

      8 comments

      Andreas Wolter

      Andreas Wolter

      @Max: bitte mal versuchen, den Quelltext statt mit der Maus zu markieren und zu kopieren, statt den “Kopieren”-Button zu verwenden.
      Eventuell ist damit das Problem schon gelöst.

      Grüße,
      Andreas Wolter

      Max

      Max

      Hallo zusammen,
      erstmal sehr cooles Projekt.
      Ich habe versuch den Node-Red “Code” zu kopiern und zum laufen zu bringen.
      Leider erscheint bei mir der Fehler: “TypeError: Cannot read property ‘options’ of undefined”
      Der msg.payload ist dabei [empty].
      Ich habe das ganze schon mit dieversen Kalendern ausprobiert.
      Habt ihr einen Tipp?

      Andreas Wolter

      Andreas Wolter

      in Absprache mit Herrn Werminghoff wird es durch die Initialzündung von @Bernhard einen dritten Teil geben. Herr Werminghoff versucht, die Änderungen vorzunehmen und dann zu zeigen, die nötig sind, um einen AZ-Touch MOD verwenden zu können.
      Es gibt einige Stolpersteine, die es noch zu überwinden gilt.

      Grüße,
      Andreas Wolter
      AZ-Delivery Blog

      Eberhard

      Eberhard

      Hallo Gunnar,
      der Schlüssel auf Deine Frage liegt im Node-Red-Knoten für den Kalender. Der heiße “ical” . Also bin ich mir sicher, das es funktioniert. Du benötigt einen Link auf Deinen ical-Kalender, den Du, wie ich es beschrieben habe, eintragen musst.
      Grüße Eberhard

      Gunnar

      Gunnar

      Hallo Andreas,

      herzlichen Dank für deine Ausführung!

      Ich habe zum Thema “Kalender eintragen in Node-Red” eine Frage:
      Hier wird die Übernahme von Daten aus dem Google Kalender beschrieben. Geht das auch mit Daten aus dem Apple Kalender über iCloud? Hierbei handelt es sich auch um ical Daten, die ich schon erfolgreich als externe Kalender in Outlook eingebunden habe.
      Sollte doch dann auch passen, oder ???

      Glück auf!
      Gunnar

      Andreas Wolter

      Andreas Wolter

      @Bernhard: die Umsetzung wäre sicher möglich.
      Dabei müsste man das Projekt zerlegen. Das eine ist die Umsetzung auf dem anderen MicroController. Hier im Beitrag ist es ein ESP32. An der Stelle, wo Sie per Kommandozeile nach ESP32 suchen und das AZ Dev Kit eingeben, müssten Sie ESP8266 eingeben und nach dem D1 Mini Ausschau halten. Dann den korrekten Namen aus der angezeigten Liste eingeben.

      Das Nächste wäre die Ausgabe auf dem Display. Hier wurde ein ePaper Display verwendet. Die Einstellungen dazu finden Sie in der display-4in2.yaml ab Zeile 247. Dort müsste dann die Konfiguration geändert werden, um das TFT Touch Display des AZ Touch MOD anzusprechen.
      Wie man das Display konfiguriert, wird hier beschrieben:
      https://esphome.io/components/display/index.html

      Die Informationen zum Display, das im AZ Touch MOD verwendet wird, stehen hier:
      https://esphome.io/components/display/ili9341.html

      Das wäre zumindest ein erster Ansatz.

      Grüße,
      Andreas Wolter
      AZ-Delivery Blog

      Bernhard

      Bernhard

      Ich habe zu dem Projekt eine Frage, und zwar ließe sich das Projekt auch auf einem AZ-Touch MOD Wandgehäuseset mit 2,8 Zoll Touchscreen für ESP8266 und ESP32 – 1x AZ-Touch und dem ESP8266 umsetzen. Hier hätten wir ein schönes Gehäuse, einen Touchscreen und und und. Nur bin ich nicht Fachmann genug, um das alles zu beurteilen. Es wäre schön, wenn meine Frage vielleicht als Anregung aufgenommen würde.
      Herzlichen Dank für die vielen tollen Projekte.
      Bernhard Schmitz

      Martin Richer

      Martin Richer

      Vielen Dank für den zweiten Teil!!!
      Wird super beim nachbauen 😊

      Leave a comment

      All comments are moderated before being published