openHASP: An MQTT driven Touchscreen / Scene controller

Hi! I'm the creator of openHASP, an opensource firmware for Home-Automation SwitchPlates.

openHASP can be flashed to various ESP32 devices to create a GUI front-end which uses MQTT to send & receive commands and events. The firmware only contains a basic UI to setup the initial WiFi connection because users are expected to upload their own page layouts next.

Pages and objects can be defined using the jsonl (JSON lines) format and the parameters are interpreted on the fly. This allows users a great degree of flexibility in designing a personal layout.

One page definition might look something like this:
image

{"page":1,"comment":"---------- Page 1 ----------"}
{"id":1,"obj":"btn","x":10,"y":45,"w":220,"h":55,"toggle":true,"text":"Touch me \uE96B","text_font":32}
{"id":2,"obj":"checkbox","x":10,"y":100,"w":220,"h":55,"text":" My Checkbox"}
{"id":3,"obj":"label","x":10,"y":10,"w":220,"h":30,"text":"\uE75A My Label","align":1,"padh":50}
{"id":4,"obj":"switch","x":125,"y":220,"w":100,"h":55,"radius":40,"radius2":40}
{"id":5,"obj":"led","x":22,"y":220,"w":55,"h":55,"val":255}
{"id":6,"obj":"dropdown","x":10,"y":160,"w":130,"options":"\uE40A Apples\n\uE40A Oranges\n\uE40A Bananas"}
{"id":7,"obj":"spinner","x":160,"y":140,"w":70,"h":70}

All objects have standardized event handlers that communicate the state over MQTT, making it easy to integrate the device in a Home-Automation system like nodeRED, Home Assistant or openHAB. Objects can also be grouped with local GPIO’s (such as relays or a dimmer) to interact with lights and switches.

There are pre-compiled binaries for a dozen of supported hardware devices and configuration files for many more in the platformio project on Github…

Here is a recent review video of openHASP running on the Lanbon L8 in-wall switch:

5 Likes

I haven't watched the video, but it looks good.

Nice. Just wish there was a version that would integrate with ESPHome which I now use on all of my ESP devices.

This comes up from time to time and I actually did a proof-of-concept of ESPhome running LVGL. It requires writing custom components for ESPhome, which is a whole other topic and area of expertise. It's a different approach/project altogether.

The current display components for ESPhome are rather basic. But Tasmota is doing cool LVGL stuff now too.

1 Like

I don't have a large enough screen on anything to make that worth while really. What I've done for my M5stack Basic is write some bespoke outputs that let me more easily create simple block screen layouts. The Basic is not touch screen, it just has 3 buttons.

So I have a common.h with things like this in it:

#ifdef FONT_SIZE_22
  //auto PRINT_FONT = id(print_font);
  //#define ICON_FONT id(icon_font)
  const int FONT_SIZE = 22;
  const int HEADER_HT = FONT_SIZE+4; // Header height
  const int FOOTER_HT = HEADER_HT; // Footer height
  const int LINE_SIZE = FONT_SIZE+2; // fontsize + 2
  const int LINE_1 = HEADER_HT+1; // header + 1
  const int LINE_2 = LINE_1 + FONT_SIZE + 2; // Previous line + 2
  const int LINE_3 = LINE_2 + FONT_SIZE + 2; // Previous line + 2
  const int LINE_4 = LINE_3 + FONT_SIZE + 2; // Previous line + 2
  const int LINE_5 = LINE_4 + FONT_SIZE + 2; // Previous line + 2
  const int LINE_6 = LINE_5 + FONT_SIZE + 2; // Previous line + 2
  const int TEXT_COL_LEFT = 1;
  const int TEXT_BLOCK_CENTER = 10 * FONT_SIZE;
#endif

And then a number of "page" yaml files like this:

- id: page1
  # Initial page. Header (Pg, status, time), Footer (buttons)
  lambda: |-

    static int num_executions = 0;
    num_executions += 1;
    
    /* @see lib\common.h for constant definitions such as lines and columns */

    /* ---- Header ---- */
    auto nowtime = id(sntp_time).now();
    auto wifiStatus = WiFi.status();
    it.filled_rectangle(0,0, it.get_width(), HEADER_HT, id(COLOR_CSS_DEEPSKYBLUE));
    if (WiFi.status() == WL_CONNECTED) {
      it.print(TEXT_BLOCK_1, HEADER_Y, id(icon_font), id(COLOR_CSS_BLACK), TextAlign::LEFT, "");
    } else {
      it.print(TEXT_BLOCK_1, HEADER_Y, id(icon_font), id(COLOR_CSS_RED), TextAlign::LEFT, "");
    }
    if (nowtime.is_valid()) {
      it.strftime(it.get_width()-1, HEADER_Y, id(print_font), id(COLOR_CSS_BLACK), TextAlign::TOP_RIGHT, "%Y-%m-%d %H:%M", nowtime);
    } else {
      it.print(it.get_width()-1, HEADER_Y, id(print_font), id(COLOR_CSS_BLACK), TextAlign::TOP_RIGHT, "----/--/-- --:--");
    }

    /* ---- Footer ---- */
    it.filled_rectangle(0,it.get_height()-FOOTER_HT, it.get_width(), FOOTER_HT, id(COLOR_CSS_LIGHTBLUE));
    

    /* ---- Main ---- */
    //it.printf(TEXT_BLOCK_1, LINE_1, id(print_font), id(COLOR_CSS_WHITE), TextAlign::LEFT, "%s", me.c_str());

    // Here
    double t_float = id(temperature).state;
    double h_float = id(humidity).state;
    it.print(TEXT_BLOCK_1, LINE_1, id(print_font), id(COLOR_CSS_WHITE), TextAlign::TOP_LEFT, "Here");
    it.print(TEXT_BLOCK_1+6, LINE_2, id(icon_font), id(COLOR_CSS_WHITE), TextAlign::TOP_CENTER, "");
    it.print(TEXT_BLOCK_1+6, LINE_3, id(icon_font), id(COLOR_CSS_WHITE), TextAlign::TOP_CENTER, "");
    it.printf(TEXT_BLOCK_1+LINE_SIZE, LINE_2, id(print_font), t_float>22.0?id(COLOR_CSS_RED):id(COLOR_CSS_WHITE), TextAlign::LEFT, "%.1f°C", t_float);
    it.printf(TEXT_BLOCK_1+LINE_SIZE, LINE_3, id(print_font), h_float>70?id(COLOR_CSS_RED):id(COLOR_CSS_WHITE), TextAlign::LEFT, "%.1f%%", h_float);

    // Landing (D1M04)
    t_float = strToFloat( id(landing_temperature).state );
    h_float = strToFloat( id(landing_humidity).state );
    it.print(TEXT_BLOCK_2, LINE_1, id(print_font), id(COLOR_CSS_WHITE), TextAlign::TOP_LEFT, "Landing");
    it.print(TEXT_BLOCK_2+6, LINE_2, id(icon_font), id(COLOR_CSS_WHITE), TextAlign::TOP_CENTER, "");
    it.print(TEXT_BLOCK_2+6, LINE_3, id(icon_font), id(COLOR_CSS_WHITE), TextAlign::TOP_CENTER, "");
    if (t_float == 0.00) {
      it.print(TEXT_BLOCK_2+LINE_SIZE, LINE_2, id(print_font), id(COLOR_CSS_LINEN), TextAlign::LEFT, "--.-°C");
    } else {
      it.printf(TEXT_BLOCK_2+LINE_SIZE, LINE_2, id(print_font), t_float>22.0?id(COLOR_CSS_RED):id(COLOR_CSS_WHITE), TextAlign::LEFT, "%.1f°C", t_float);      
    }
    if (h_float == 0.00) {
      it.print(TEXT_BLOCK_2+LINE_SIZE, LINE_3, id(print_font), id(COLOR_CSS_LINEN), TextAlign::LEFT, "--.-%");
    } else {
      it.printf(TEXT_BLOCK_2+LINE_SIZE, LINE_3, id(print_font), h_float>70?id(COLOR_CSS_RED):id(COLOR_CSS_WHITE), TextAlign::LEFT, "%.1f%%", h_float);
    }
    

    // Bathroom
    t_float = strToFloat( id(bathroom_temperature).state ); 
    h_float = strToFloat( id(bathroom_humidity).state );
    it.print(TEXT_BLOCK_3, LINE_1, id(print_font), id(COLOR_CSS_WHITE), TextAlign::TOP_LEFT, "Bathroom");
    it.print(TEXT_BLOCK_3+6, LINE_2, id(icon_font), id(COLOR_CSS_WHITE), TextAlign::TOP_CENTER, "");
    it.print(TEXT_BLOCK_3+6, LINE_3, id(icon_font), id(COLOR_CSS_WHITE), TextAlign::TOP_CENTER, "");
    if (t_float == 0.00) {
      it.print(TEXT_BLOCK_3+LINE_SIZE, LINE_2, id(print_font), id(COLOR_CSS_LINEN), TextAlign::LEFT, "--.-°C");
    } else {
      it.printf(TEXT_BLOCK_3+LINE_SIZE, LINE_2, id(print_font), t_float>22.0?id(COLOR_CSS_RED):id(COLOR_CSS_WHITE), TextAlign::LEFT, "%.1f°C", t_float);      
    }
    if (h_float == 0.00) {
      it.print(TEXT_BLOCK_3+LINE_SIZE, LINE_3, id(print_font), id(COLOR_CSS_LINEN), TextAlign::LEFT, "--.-%");
    } else {
      it.printf(TEXT_BLOCK_3+LINE_SIZE, LINE_3, id(print_font), h_float>70?id(COLOR_CSS_RED):id(COLOR_CSS_WHITE), TextAlign::LEFT, "%.1f%%", h_float);
    }

    // Front Hall
    t_float = strToFloat( id(front_hall_temperature).state ); 
    //h_float = strToFloat( id(front_hall_humidity).state );
    it.print(TEXT_BLOCK_1, LINE_4, id(print_font), id(COLOR_CSS_WHITE), TextAlign::TOP_LEFT, "F Hall");
    it.print(TEXT_BLOCK_1+6, LINE_5, id(icon_font), id(COLOR_CSS_WHITE), TextAlign::TOP_CENTER, "");
    it.print(TEXT_BLOCK_1+6, LINE_6, id(icon_font), id(COLOR_CSS_WHITE), TextAlign::TOP_CENTER, "");
    if (t_float == 0.00) {
      it.print(TEXT_BLOCK_1+LINE_SIZE, LINE_5, id(print_font), id(COLOR_CSS_LINEN), TextAlign::LEFT, "--.-°C");
    } else {
      it.printf(TEXT_BLOCK_1+LINE_SIZE, LINE_5, id(print_font), t_float>22.0?id(COLOR_CSS_RED):id(COLOR_CSS_WHITE), TextAlign::LEFT, "%.1f°C", t_float);      
    }
    it.print(TEXT_BLOCK_1+LINE_SIZE, LINE_6, id(print_font), id(COLOR_CSS_LINEN), TextAlign::LEFT, "--.-%");

    // Rear Hall
    t_float = strToFloat( id(rear_hall_temperature).state ); 
    h_float = strToFloat( id(rear_hall_humidity).state );
    it.print(TEXT_BLOCK_2, LINE_4, id(print_font), id(COLOR_CSS_WHITE), TextAlign::TOP_LEFT, "R Hall");
    it.print(TEXT_BLOCK_2+6, LINE_5, id(icon_font), id(COLOR_CSS_WHITE), TextAlign::TOP_CENTER, "");
    it.print(TEXT_BLOCK_2+6, LINE_6, id(icon_font), id(COLOR_CSS_WHITE), TextAlign::TOP_CENTER, "");
    if (t_float == 0.00) {
      it.print(TEXT_BLOCK_2+LINE_SIZE, LINE_5, id(print_font), id(COLOR_CSS_LINEN), TextAlign::LEFT, "--.-°C");
    } else {
      it.printf(TEXT_BLOCK_2+LINE_SIZE, LINE_5, id(print_font), t_float>22.0?id(COLOR_CSS_RED):id(COLOR_CSS_WHITE), TextAlign::LEFT, "%.1f°C", t_float);      
    }
    if (h_float == 0.00) {
      it.print(TEXT_BLOCK_2+LINE_SIZE, LINE_6, id(print_font), id(COLOR_CSS_LINEN), TextAlign::LEFT, "--.-%");
    } else {
      it.printf(TEXT_BLOCK_2+LINE_SIZE, LINE_6, id(print_font), h_float>70?id(COLOR_CSS_RED):id(COLOR_CSS_WHITE), TextAlign::LEFT, "%.1f%%", h_float);
    }

    // Kitchen
    t_float = strToFloat( id(kitchen_temperature).state ); 
    //h_float = strToFloat( id(kitchen_humidity).state );
    it.print(TEXT_BLOCK_3, LINE_4, id(print_font), id(COLOR_CSS_WHITE), TextAlign::TOP_LEFT, "Kitchen");
    it.print(TEXT_BLOCK_3+6, LINE_5, id(icon_font), id(COLOR_CSS_WHITE), TextAlign::TOP_CENTER, "");
    it.print(TEXT_BLOCK_3+6, LINE_6, id(icon_font), id(COLOR_CSS_WHITE), TextAlign::TOP_CENTER, "");
    if (t_float == 0.00) {
      it.print(TEXT_BLOCK_3+LINE_SIZE, LINE_5, id(print_font), id(COLOR_CSS_LINEN), TextAlign::LEFT, "--.-°C");
    } else {
      it.printf(TEXT_BLOCK_3+LINE_SIZE, LINE_5, id(print_font), t_float>22.0?id(COLOR_CSS_RED):id(COLOR_CSS_WHITE), TextAlign::LEFT, "%.1f°C", t_float);      
    }
    it.print(TEXT_BLOCK_3+LINE_SIZE, LINE_6, id(print_font), id(COLOR_CSS_LINEN), TextAlign::LEFT, "--.-%");

So I can get things to line up pretty easily. Nothing like as nice as your touchscreen setup though of course.

Thanks for sharing. I don't doubt similar things can be achieved with ESPhome. Of course, you can use whichever firmware works in your situation.

The openHASP concept is that you can create, change, move or delete any UI element on the screen on-the-fly using a simple JSON format over MQTT. Users don't need to recompile and upload new firmware when the layout changes. Besides text labels, you can use 20 common UI elements to quickly create interactive screens.

2 Likes

Thanks for that introduction, @fvanroie. I just ordered a Lanbon L8 switch from Amazon UK, which should arrive a Wednesday!!

Hi @fvanroie

I have one of these 320-320-4-inch wireless-tag devices (ESP32-S2-WROVER based) would you know if (and how) this firmware could be used for this?

@Steve-Mcl We are indeed testing the WT-86-32-3ZW1 in the development branch. Feel free to try it and join the discussion on Github.

I finally managed to flash OpenHASP onto my Lanbon L8. I checked the demo pages and love the possibilities to build screens with so many options. I can control the relays and the moodlight of the Lanbon switch. Up to 12 pages can easily be filled with various GUI elements and either selected via the navigation on the bottom (or some other place you prefer) and also via swipe.

plate1
plate2

see my adapted demo page. Top left there is a toggle button, on the right a touch button. The three bulbs control the three relays via MQTT (so there is no direct control, everything is managed from Node-RED).
Time and temperature are also published by Node-RED.

@fvanroie, have you plans to support a color change for the LED object? This is something I use quite often in my Web GUI.

2 Likes

Nice @stefan24! That's looking good.

Try setting p5b5.bg_color=orange and p5b5.shadow_color=orange. The bg_color and shadow_color properties control the LED color.

... and additionally the border_color, otherwise it seems not to be perfect orange.
Got it, but not obvious.
Thank you for the quick reply!

1 Like

I’m happy to announce the release of openHASP 0.6.3. Please check out the release notes for the notable changes. It includes quite a list of improvements, new features and bug fixes.

We can now also be found on www.openhasp.com and you can install the firmware using the openHASP Web Installer.

1 Like

Great development! I'm going to experiment openHASP on my AZ-Touch plate. Also, nice video - but being that such a "dense" matter, I really believe you should speak MUCH slower...