Mini Weather Station Display - ESPHome (updated for 3.2" touchscreen) with BOM Forecast

This is my ESPHome based weather station display, it’s main function is to display the details of the wind, but it also displays other values from my Ecowitt weather station.

When I bought the weather station I went cheap and got the wifi only version (i.e. no display). This is fine if I want to fire up HA and have a look, but I wanted something for the kitchen counter top.

This is my solution:

I have yet to decide on a case, but decided to share as it’s finished as far as software is concerned.

The hardware comprises of:

  • 2.2" TFT display with ili9341 controller
  • ESP32 Devkit V1
  • Micro PIR

From the top down, the display shows:

  • Current time
  • Current outside temperature
  • Compass rose with wind direction and strength indicator
  • Wind trend lines
  • Named wind direction and speed
  • Wind gust as reported by the weather station - “G” (last 20 sec max)
  • Trend display maximum - “TM” (this is the speed that the longest trend line represents)
  • Rainfall since midnight
  • Current air pressure

The display will go to sleep if not trigger by the motion sensor for 5 mins.

The software also attempts to prevent burn-in of the display.

The code is below. It is reasonably well commented - if anyone doesn’t get something please ask.

This is a fairly simple project, the main things of note are the calculations for displaying the direction marker and the code for storing and displaying trend lines.

esphome:
  name: wind
  # Set inital value for idle timer
  on_boot:
    priority: -100
    then:
      - lambda: |-
          id(idle_time) = id(my_time).now().timestamp;
    
esp32:
  board: esp32dev
  framework:
    type: arduino

# Enable logging
logger:

# Enable Home Assistant API
api:

spi:
  clk_pin: GPIO18
  mosi_pin: GPIO23
  miso_pin: GPIO19

ota:
  password: "8aebd6eb0427929de523046d6773d66e"

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Wind Fallback Hotspot"
    password: "sh1mZkRvphaJ"

captive_portal:

# All sensors sourced from HA - Ecowitt weather station sensors
text_sensor:
  - platform: homeassistant
    name: "Current Temp"
    entity_id: sensor.weather_station_temperature
    id: temp

  - platform: homeassistant
    name: "Current Wind Dir"
    entity_id: sensor.wind_direction
    id: wind_dir

  - platform: homeassistant
    name: "Current Wind Speed"
    entity_id: sensor.wind_speed
    id: wind_speed

  - platform: homeassistant
    name: "Named Wind Dir"
    entity_id: sensor.named_wind_direction
    id: named_dir
    
  - platform: homeassistant
    name: "Rain since 12am"
    entity_id: sensor.rain_accumulation
    id: total_rain

  - platform: homeassistant
    name: "Pressure MSL hPa"
    entity_id: sensor.atmospheric_pressure_msl
    id: pressure
    
  - platform: homeassistant
    name: "Wind gust"
    entity_id: sensor.wind_gust
    id: gust

# Stores timestamp of last motion detect    
globals:
  - id: idle_time
    type: long

font:
  - file: "fonts/calibri.ttf"
    id: calibri_20
    size: 20

  - file: "fonts/calibri.ttf"
    id: calibri_25
    size: 25
    
  - file: "fonts/calibri.ttf"
    id: calibri_40
    size: 40
    
time:
  - platform: homeassistant
    id: my_time

    on_time:
      # Every 1 minute check if no presence trigger for 5 min
      - seconds: 0
        minutes: /1
        then:
          - if:
              # more than 5 min with no motion? Exercise display then turn off backlight
              condition:
                lambda: |-
                  return id(my_time).now().timestamp-id(idle_time) > 300;
              then:
                - display.page.show: page2
                - delay: 1s
                - display.page.show: page3
                - delay: 1s
                - display.page.show: page4
                - delay: 1s
                - display.page.show: page5
                - light.turn_off: back_light
                - delay: 1s
                - display.page.show: page1

# PIR on GPIO33
binary_sensor:
  - platform: gpio
    pin: GPIO33
    id: pir
    device_class: motion
    # turn on display on motion detect   
    # store timestamp of last detection event
    on_press:
      then:
        - lambda: |-
            id(idle_time) = id(my_time).now().timestamp;
        - display.page.show: page1
        - light.turn_on: back_light
    on_release:
      then:
        - lambda: |-
            id(idle_time) = id(my_time).now().timestamp;

# backlight pin as PWM GPIO32
output:
  - platform: ledc
    pin: 32
    id: gpio_32_backlight_pwm

# define as light
light:
  - platform: monochromatic
    output: gpio_32_backlight_pwm
    name: "ILI9341 Display Backlight"
    id: back_light
    restore_mode: ALWAYS_ON
    
# 2.2" TFT display 320x240, portrait orientation
display:
  - platform: ili9341
    rotation: 0
    model: TFT 2.4
    dc_pin: GPIO21
    cs_pin: GPIO22
    led_pin: GPIO32
    reset_pin: GPIO17
    
    # Page 1 - main display.  Other pages or for anti burn-in
    pages:
      - id: page1
        lambda: |-
          // Colours - used for visiual indication of wind strength
          auto red = Color(255, 0, 0);
          auto green = Color(0, 255, 0);
          auto light_blue = Color(135, 237, 232);
          auto orange = Color(255, 170, 43);
          auto white = Color(255, 255, 255);
          auto purple = Color(97, 15, 219);
          auto grey = Color(100, 135, 135);
          // history arrays for trend display
          static float hist_wind[30];
          static int hist_dir[30];
          static int index=0;
          static int prev_index=29;
          // Convert wind direction to int, speed to float
          int dir = atoi(id(wind_dir).state.c_str());
          float speed = atof(id(wind_speed).state.c_str());
          // Calculate xy position to plot wind indicator
          int x = 120 + (90 * (cos((dir-90)*PI/180)));
          int y = 150 + (90 * (sin((dir-90)*PI/180)));
          // Store in array
          if (hist_wind[prev_index]!=speed || hist_dir[prev_index]!=dir) {
            hist_wind[index] = speed;
            hist_dir[index] = dir;
            index += 1;
            prev_index += 1;
            if (index==30) { index = 0; }
            if (prev_index==30) { prev_index = 0; }
          }
          // Trend maximum for last 30
          float max = hist_wind[0];
          for (size_t i = 0; i < 30; ++i) {
            if (hist_wind[i] > max) {
              max = hist_wind[i];
            }
          }
          // Weather data
          it.strftime(5, 10, id(calibri_25), TextAlign::TOP_LEFT, "%H:%M", id(my_time).now());
          it.printf(235, 10, id(calibri_25), TextAlign::TOP_RIGHT, "%s °C", id(temp).state.c_str());
          it.printf(5, 290, id(calibri_20), TextAlign::BOTTOM_LEFT, "G %s km/h", id(gust).state.c_str());
          it.printf(5, 310, id(calibri_20), TextAlign::BOTTOM_LEFT, "TM %2.1f km/h", max);
          it.printf(235, 290, id(calibri_20), TextAlign::BOTTOM_RIGHT, "%s mm", id(total_rain).state.c_str());
          it.printf(235, 310, id(calibri_20), TextAlign::BOTTOM_RIGHT, "%s hPa", id(pressure).state.c_str());
          // Display trend data
          for (size_t i = 0; i < 30; ++i) {
            if (hist_wind[i]>0) {
              int a = 120 + ((90-(70*hist_wind[i]/max)) * (cos((hist_dir[i]-90)*PI/180)));
              int b = 150 + ((90-(70*hist_wind[i]/max)) * (sin((hist_dir[i]-90)*PI/180)));
              int c = 120 + (90 * (cos((hist_dir[i]-90)*PI/180)));
              int d = 150 + (90 * (sin((hist_dir[i]-90)*PI/180)));
              it.line(a, b, c, d, grey);
            }
          }
          // Compass rose
          it.circle(120, 150, 90);
          it.print(120, 50, id(calibri_20), red, TextAlign::BOTTOM_CENTER, "N");
          it.print(120, 250, id(calibri_20), red, TextAlign::TOP_CENTER, "S");
          it.print(20, 150, id(calibri_20), red, TextAlign::CENTER_RIGHT, "W");
          it.print(220, 150, id(calibri_20), red, TextAlign::CENTER_LEFT, "E");
          // Wind pointer
          if (speed<=11) { it.filled_circle(x, y, 7, light_blue); }
            else {
          if (speed>11 && speed<=30) { it.filled_circle(x, y, 7, green); } 
            else {
          if (speed>30 && speed<=60) { it.filled_circle(x, y, 7, orange); } 
            else {
          if (speed>60) { it.filled_circle(x, y, 7, red); } 
            } } }
          // Wind details
          it.printf(120, 130, id(calibri_25), TextAlign::BOTTOM_CENTER, "%s", id(named_dir).state.c_str());
          if (speed<10) { it.printf(120, 175, id(calibri_40), TextAlign::BOTTOM_CENTER, "%2.1f", speed); }
            else { it.printf(120, 175, id(calibri_40), TextAlign::BOTTOM_CENTER, "%2.0f", round(speed));   
          }
          it.print(120, 175, id(calibri_20), TextAlign::TOP_CENTER, "km/h");
          
      - id: page2
        lambda: |-
          // fill screen red
          auto red = Color(255, 0, 0);
          it.fill(red);
          
      - id: page3
        lambda: |-
          // Fill screen green
          auto green = Color(0, 255, 0);
          it.fill(green);
          
      - id: page4
        lambda: |-
          // Fill screen white
          auto white = Color(255, 255, 255);
          it.fill(white);
          
      - id: page5
        lambda: |-
          it.fill(COLOR_OFF);
7 Likes

This is really cool, I think im going to give it a shot!

Small change made - purely optional.

As above the trend display can whittle itself down to a couple of lines it is is calm. To prevent this you can ignore any zero values for wind speed when recording in the trend array.

Change line 188 from:

          if (hist_wind[prev_index]!=speed || hist_dir[prev_index]!=dir) {

to:

          if ((hist_wind[prev_index]!=speed || hist_dir[prev_index]!=dir) && speed!=0) {

Adding an update for this post - now implemented with a 3.2 inch screen (still ili9341) and a xpt2046 touchscreen.

This has been mounted on Dustin Watts adapter board for ili9488 3.5 inch screen, which is pin compatible. Dustin designed this for the FreeTouchDeck software - it also is compatible with openHASP. See GitHub - DustinWatts/ESP32_TFT_Combiner: A PCB making it easy to combine an ESP32 and a TFT Touchsceen. for pinouts and design files and you can buy them from PCBWay ESP32 TFT Combiner V1 - Share Project - PCBWay

The touch screen allows you to switch pages - I use this to display BOM (Australia) weather forecast data - with a bit of work it could use any forecast data.

Reasons for sharing:

Hopefully this will again be of use to someone.

Current weather:

6 day forecast:

Today and tomorrow summary:

esphome:
  name: weather
  # Set inital value for idle timer
  on_boot:
    priority: -100
    then:
      - lambda: |-
          id(u1_idle_time) = id(u1_my_time).now().timestamp;
    
esp32:
  board: esp32dev
  framework:
    type: arduino

# Enable logging
logger:

# Enable Home Assistant API
api:

spi:
  clk_pin: GPIO18
  mosi_pin: GPIO23
  miso_pin: GPIO19

ota:
  password: "8aebd6eb0427929de523046d6773d66e"

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Wind Fallback Hotspot"
    password: "sh1mZkRvphaJ"

captive_portal:

# touchscreen - default calibration as screen is just one big button.
xpt2046:
  id: u1_touchscreen
  cs_pin: 21
  irq_pin: 27
  update_interval: 50ms
  report_interval: 1s
  threshold: 400
  dimension_x: 240
  dimension_y: 320
  calibration_x_min: 280
  calibration_x_max: 3860
  calibration_y_min: 3860
  calibration_y_max: 280
  swap_x_y: false

# All sensors sourced from HA - Ecowitt weather station sensors
# BOM weather forecast sensors
text_sensor:
  - platform: homeassistant
    name: "Current Temp"
    entity_id: sensor.weather_station_temperature
    id: u1_temp

  - platform: homeassistant
    name: "Current Wind Dir"
    entity_id: sensor.wind_direction
    id: u1_wind_dir

  - platform: homeassistant
    name: "Current Wind Speed"
    entity_id: sensor.wind_speed
    id: u1_wind_speed

  - platform: homeassistant
    name: "Named Wind Dir"
    entity_id: sensor.named_wind_direction
    id: u1_named_dir
    
  - platform: homeassistant
    name: "Rain since 12am"
    entity_id: sensor.rain_accumulation
    id: u1_total_rain

  - platform: homeassistant
    name: "Pressure MSL hPa"
    entity_id: sensor.atmospheric_pressure_msl
    id: u1_pressure
    
  - platform: homeassistant
    name: "Wind gust"
    entity_id: sensor.wind_gust
    id: u1_gust

  - platform: homeassistant
    id: u1_forecast0
    entity_id: sensor.rutherglen_extended_text_0

  - platform: homeassistant
    id: u1_forecast1e
    entity_id: sensor.rutherglen_extended_text_1
    
  - platform: homeassistant
    id: u1_forecast1
    entity_id: sensor.rutherglen_short_text_1
    
  - platform: homeassistant
    id: u1_forecast2
    entity_id: sensor.rutherglen_short_text_2
    
  - platform: homeassistant
    id: u1_forecast3
    entity_id: sensor.rutherglen_short_text_3
    
  - platform: homeassistant
    id: u1_forecast4
    entity_id: sensor.rutherglen_short_text_4
    
  - platform: homeassistant
    id: u1_forecast5
    entity_id: sensor.rutherglen_short_text_5

  - platform: homeassistant
    id: u1_forecast6
    entity_id: sensor.rutherglen_short_text_6
    
  - platform: homeassistant
    id: u1_icon0
    entity_id: sensor.rutherglen_icon_descriptor_0
    
  - platform: homeassistant
    id: u1_icon1
    entity_id: sensor.rutherglen_icon_descriptor_1
    
  - platform: homeassistant
    id: u1_icon2
    entity_id: sensor.rutherglen_icon_descriptor_2
    
  - platform: homeassistant
    id: u1_icon3
    entity_id: sensor.rutherglen_icon_descriptor_3
    
  - platform: homeassistant
    id: u1_icon4
    entity_id: sensor.rutherglen_icon_descriptor_4
    
  - platform: homeassistant
    id: u1_icon5
    entity_id: sensor.rutherglen_icon_descriptor_5

  - platform: homeassistant
    id: u1_icon6
    entity_id: sensor.rutherglen_icon_descriptor_6
    
  - platform: homeassistant
    id: u1_rainrange0
    entity_id: sensor.rutherglen_rain_amount_range_0

  - platform: homeassistant
    id: u1_rainrange1
    entity_id: sensor.rutherglen_rain_amount_range_1
  
  - platform: homeassistant
    id: u1_rainrange2
    entity_id: sensor.rutherglen_rain_amount_range_2
  
  - platform: homeassistant
    id: u1_rainrange3
    entity_id: sensor.rutherglen_rain_amount_range_3
  
  - platform: homeassistant
    id: u1_rainrange4
    entity_id: sensor.rutherglen_rain_amount_range_4
  
  - platform: homeassistant
    id: u1_rainrange5
    entity_id: sensor.rutherglen_rain_amount_range_5
  
  - platform: homeassistant
    id: u1_rainrange6
    entity_id: sensor.rutherglen_rain_amount_range_6

  - platform: homeassistant
    id: u1_rainchance0
    entity_id: sensor.rutherglen_rain_chance_0

  - platform: homeassistant
    id: u1_rainchance1
    entity_id: sensor.rutherglen_rain_chance_1
    
  - platform: homeassistant
    id: u1_rainchance2
    entity_id: sensor.rutherglen_rain_chance_2
    
  - platform: homeassistant
    id: u1_rainchance3
    entity_id: sensor.rutherglen_rain_chance_3
    
  - platform: homeassistant
    id: u1_rainchance4
    entity_id: sensor.rutherglen_rain_chance_4
    
  - platform: homeassistant
    id: u1_rainchance5
    entity_id: sensor.rutherglen_rain_chance_5
    
  - platform: homeassistant
    id: u1_rainchance6
    entity_id: sensor.rutherglen_rain_chance_6
    
  - platform: homeassistant
    id: u1_min0
    entity_id: sensor.rutherglen_temp_min_0
    
  - platform: homeassistant
    id: u1_min1
    entity_id: sensor.rutherglen_temp_min_1
    
  - platform: homeassistant
    id: u1_min2
    entity_id: sensor.rutherglen_temp_min_2
    
  - platform: homeassistant
    id: u1_min3
    entity_id: sensor.rutherglen_temp_min_3
    
  - platform: homeassistant
    id: u1_min4
    entity_id: sensor.rutherglen_temp_min_4
    
  - platform: homeassistant
    id: u1_min5
    entity_id: sensor.rutherglen_temp_min_5
    
  - platform: homeassistant
    id: u1_min6
    entity_id: sensor.rutherglen_temp_min_6
    
  - platform: homeassistant
    id: u1_max0
    entity_id: sensor.rutherglen_temp_max_0
    
  - platform: homeassistant
    id: u1_max1
    entity_id: sensor.rutherglen_temp_max_1
    
  - platform: homeassistant
    id: u1_max2
    entity_id: sensor.rutherglen_temp_max_2
    
  - platform: homeassistant
    id: u1_max3
    entity_id: sensor.rutherglen_temp_max_3
    
  - platform: homeassistant
    id: u1_max4
    entity_id: sensor.rutherglen_temp_max_4
    
  - platform: homeassistant
    id: u1_max5
    entity_id: sensor.rutherglen_temp_max_5
    
  - platform: homeassistant
    id: u1_max6
    entity_id: sensor.rutherglen_temp_max_6
    
# Stores timestamp of last motion detect    
globals:
  - id: u1_idle_time
    type: long

font:
  # Forecast text
  - file: "fonts/calibri.ttf"
    id: calibri_12
    size: 12

  # Weather display detail text
  - file: "fonts/calibri.ttf"
    id: calibri_20
    size: 20
    
  # weather icons from:
  # https://github.com/erikflowers/weather-icons/blob/bb80982bf1f43f2d57f9dd753e7413bf88beb9ed/font/weathericons-regular-webfont.ttf
  # Install font on your pc/mac and edit with Word to make these
  # easier to understand and troubleshoot
  - file: "fonts/weathericons-regular-webfont.ttf"
    id: weather_22
    size: 22
    glyphs: [
      "", # clear
      "", # cloudy
      "", # cyclone
      "", # dust
      "", # fog
      "", # frost
      "", # haze
      "", # heavy_shower
      "", # heavy_shower_night
      "", # light_rain
      "", # light_shower
      "", # light_shower_night
      "", # mostly_sunny
      "", # partly_cloudy
      "", # partly_cloudy_night
      "", # rain
      "", # shower
      "", # shower_night
      "", # snow
      "", # storm
      "", # sunny
      "", # tropical_cyclone
      "" # wind
      ]

  # weather display time and temp
  - file: "fonts/calibri.ttf"
    id: calibri_25
    size: 25
    
  # weather display wind speed
  - file: "fonts/calibri.ttf"
    id: calibri_40
    size: 40
    
time:
  - platform: homeassistant
    id: u1_my_time

    on_time:
      # Every 1 minute check if no presence trigger for 5 min
      - seconds: 0
        minutes: /1
        then:
          - if:
              # more than 5 min with no motion? Exercise display then turn off backlight
              condition:
                lambda: |-
                  return id(u1_my_time).now().timestamp-id(u1_idle_time) > 300;
              then:
                - display.page.show: u1_page2
                - delay: 1s
                - display.page.show: u1_page3
                - delay: 1s
                - display.page.show: u1_page4
                - delay: 1s
                - display.page.show: u1_page5
                - light.turn_off: u1_back_light
                - delay: 1s
                - display.page.show: u1_page1

# Sun required for sun elevation sensor
sun:
  latitude: -36.051961
  longitude: 146.458654

# State is negative after sunset and before sunrise  
sensor:
  platform: sun
  id: sun_elevation
  type: elevation
  
# PIR on GPIO33
binary_sensor:

  - platform: gpio
    pin: GPIO33
    id: u1_pir
    device_class: motion
    # turn on display on motion detect   
    # store timestamp of last detection event
   on_press:
      then:
        - lambda: |-
            id(u1_idle_time) = id(u1_my_time).now().timestamp;
        - display.page.show: u1_page1
        - light.turn_on: u1_back_light
    on_release:
      then:
        - lambda: |-
            id(u1_idle_time) = id(u1_my_time).now().timestamp;
            
  # Screen is one big button
  - platform: xpt2046
    xpt2046_id: u1_touchscreen
    id: u1_touch_key0
    x_min: 0
    x_max: 240
    y_min: 0
    y_max: 320
    on_press:
      then:
        # if backlight on, then swap screen page.
        # if backlight off - wake up and display page 1
        - lambda: |-
            id(u1_idle_time) = id(u1_my_time).now().timestamp;
        - if:
            condition:
              light.is_on: u1_back_light
            then:
              if: 
                condition:
                  display.is_displaying_page: u1_page1
                then:
                  display.page.show: u1_page6
                else:
                  if:
                    condition:
                      display.is_displaying_page: u1_page6
                    then:
                      display.page.show: u1_page7
                    else:
                      if:
                        condition:
                          display.is_displaying_page: u1_page7
                        then:
                          display.page.show: u1_page1
            else:
              - display.page.show: u1_page1
              - light.turn_on: u1_back_light      

# backlight pin as PWM GPIO32
output:
  - platform: ledc
    pin: 32
    id: u1_gpio_32_backlight_pwm

# define as light
light:
  - platform: monochromatic
    output: u1_gpio_32_backlight_pwm
    name: "ILI9341 Display Backlight"
    id: u1_back_light
    restore_mode: ALWAYS_ON
    
# 2.4" or 3.2" TFT display 320x240, portrait orientation
display:
  - platform: ili9341
    id: u1_display
    rotation: 0
    model: TFT 2.4 # works for 3.2
    dc_pin: GPIO02
    cs_pin: GPIO15
    led_pin: GPIO32
    reset_pin: GPIO04
    
    # Page 1 - main display.  Pages 2-5 are for anti burn-in
    pages:
      - id: u1_page1
        lambda: |-
          // Colours - used for visiual indication of wind strength
          auto red = Color(255, 0, 0);
          auto green = Color(0, 255, 0);
          auto light_blue = Color(135, 237, 232);
          auto orange = Color(255, 170, 43);
          auto white = Color(255, 255, 255);
          auto purple = Color(97, 15, 219);
          auto grey = Color(100, 135, 135);
          // history arrays for trend display
          static float hist_wind[30];
          static int hist_dir[30];
          static int index=0;
          static int prev_index=29;
          // Convert wind direction to int, speed to float
          int dir = atoi(id(u1_wind_dir).state.c_str());
          float speed = atof(id(u1_wind_speed).state.c_str());
          // Calculate xy position to plot wind indicator
          int x = 120 + (90 * (cos((dir-90)*PI/180)));
          int y = 150 + (90 * (sin((dir-90)*PI/180)));
          // Store in array
          if ((hist_wind[prev_index]!=speed || hist_dir[prev_index]!=dir) && speed!=0) {
            hist_wind[index] = speed;
            hist_dir[index] = dir;
            index += 1;
            prev_index += 1;
            if (index==30) { index = 0; }
            if (prev_index==30) { prev_index = 0; }
          }
          // Trend maximum for last 30
          float max = hist_wind[0];
          for (size_t i = 0; i < 30; ++i) {
            if (hist_wind[i] > max) {
              max = hist_wind[i];
            }
          }
          // Weather data
          it.strftime(5, 10, id(calibri_25), TextAlign::TOP_LEFT, "%H:%M", id(u1_my_time).now());
          it.printf(235, 10, id(calibri_25), TextAlign::TOP_RIGHT, "%s °C", id(u1_temp).state.c_str());
          it.printf(5, 290, id(calibri_20), TextAlign::BOTTOM_LEFT, "G %s km/h", id(u1_gust).state.c_str());
          it.printf(5, 310, id(calibri_20), TextAlign::BOTTOM_LEFT, "TM %2.1f km/h", max);
          it.printf(235, 290, id(calibri_20), TextAlign::BOTTOM_RIGHT, "%s mm", id(u1_total_rain).state.c_str());
          it.printf(235, 310, id(calibri_20), TextAlign::BOTTOM_RIGHT, "%s hPa", id(u1_pressure).state.c_str());
          // Display trend data
          for (size_t i = 0; i < 30; ++i) {
            if (hist_wind[i]>0) {
              int a = 120 + ((90-(70*hist_wind[i]/max)) * (cos((hist_dir[i]-90)*PI/180)));
              int b = 150 + ((90-(70*hist_wind[i]/max)) * (sin((hist_dir[i]-90)*PI/180)));
              int c = 120 + (90 * (cos((hist_dir[i]-90)*PI/180)));
              int d = 150 + (90 * (sin((hist_dir[i]-90)*PI/180)));
              it.line(a, b, c, d, grey);
            }
          }
          // Compass rose
          it.circle(120, 150, 90);
          it.print(120, 50, id(calibri_20), red, TextAlign::BOTTOM_CENTER, "N");
          it.print(120, 250, id(calibri_20), red, TextAlign::TOP_CENTER, "S");
          it.print(20, 150, id(calibri_20), red, TextAlign::CENTER_RIGHT, "W");
          it.print(220, 150, id(calibri_20), red, TextAlign::CENTER_LEFT, "E");
          // Wind pointer
          if (speed<=11) { it.filled_circle(x, y, 7, light_blue); }
            else {
          if (speed>11 && speed<=30) { it.filled_circle(x, y, 7, green); } 
            else {
          if (speed>30 && speed<=60) { it.filled_circle(x, y, 7, orange); } 
            else {
          if (speed>60) { it.filled_circle(x, y, 7, red); } 
            } } }
          // Wind details
          it.printf(120, 130, id(calibri_25), TextAlign::BOTTOM_CENTER, "%s", id(u1_named_dir).state.c_str());
          if (speed<10) { it.printf(120, 175, id(calibri_40), TextAlign::BOTTOM_CENTER, "%2.1f", speed); }
            else { it.printf(120, 175, id(calibri_40), TextAlign::BOTTOM_CENTER, "%2.0f", round(speed));   
          }
          it.print(120, 175, id(calibri_20), TextAlign::TOP_CENTER, "km/h");
          
      - id: u1_page2
        lambda: |-
          // fill screen red
          auto red = Color(255, 0, 0);
          it.fill(red);
          
      - id: u1_page3
        lambda: |-
          // Fill screen green
          auto green = Color(0, 255, 0);
          it.fill(green);
          
      - id: u1_page4
        lambda: |-
          // Fill screen white
          auto white = Color(255, 255, 255);
          it.fill(white);
          
      - id: u1_page5
        lambda: |-
          it.fill(COLOR_OFF);

    # Page 6 - Forecast data          
      - id: u1_page6
        lambda: |-
          // list of condition for forecast icons
          auto light_blue = Color(135, 237, 232);
          std::string conditions[7] = { id(u1_icon0).state.c_str(), 
                                        id(u1_icon1).state.c_str(), 
                                        id(u1_icon2).state.c_str(), 
                                        id(u1_icon3).state.c_str(), 
                                        id(u1_icon4).state.c_str(), 
                                        id(u1_icon5).state.c_str(), 
                                        id(u1_icon6).state.c_str() };
          // days of week for forecast text
          std::string weekday[14] = { "Today", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri" };
          int wd = id(u1_my_time).now().day_of_week;
          std::string forecast[7] = { "", 
                                      (weekday[wd+1] + " - " + id(u1_min1).state + "°/" + id(u1_max1).state + "°  " + id(u1_forecast1).state 
                                      + " " + id(u1_rainchance1).state +"% chance of " + id(u1_rainrange1).state +"mm ").c_str(), 
                                      (weekday[wd+2] + " - " + id(u1_min2).state + "°/" + id(u1_max2).state + "°  " + id(u1_forecast2).state 
                                      + " " + id(u1_rainchance2).state +"% chance of " + id(u1_rainrange2).state +"mm ").c_str(), 
                                      (weekday[wd+3] + " - " + id(u1_min3).state + "°/" + id(u1_max3).state + "°  " + id(u1_forecast3).state 
                                      + " " + id(u1_rainchance3).state +"% chance of " + id(u1_rainrange3).state +"mm ").c_str(),
                                      (weekday[wd+4] + " - " + id(u1_min4).state + "°/" + id(u1_max4).state + "°  " + id(u1_forecast4).state 
                                      + " " + id(u1_rainchance4).state +"% chance of " + id(u1_rainrange4).state +"mm ").c_str(),
                                      (weekday[wd+5] + " - " + id(u1_min5).state + "°/" + id(u1_max5).state + "°  " + id(u1_forecast5).state 
                                      + " " + id(u1_rainchance5).state +"% chance of " + id(u1_rainrange5).state +"mm ").c_str(),
                                      (weekday[wd+6] + " - " + id(u1_min6).state + "°/" + id(u1_max6).state + "°  " + id(u1_forecast6).state 
                                      + " " + id(u1_rainchance6).state +"% chance of " + id(u1_rainrange6).state +"mm ").c_str()};
          // map conditions to icons
          std::map<std::string, std::string> weather_state { 
            { "clear", "" }, 
            { "cloudy", "" }, 
            { "cyclone", "" }, 
            { "dust", "" }, 
            { "dusty", "" }, 
            { "fog", "" }, 
            { "frost", "" }, 
            { "haze", "" }, 
            { "hazy", "" }, 
            { "heavy_shower", "" }, 
            { "heavy_showers", "" }, 
            { "light_rain", "" }, 
            { "light_shower", "" }, 
            { "light_showers", "" }, 
            { "mostly_sunny", "" }, 
            { "partly_cloudy", "" }, 
            { "rain", "" }, 
            { "shower", "" }, 
            { "showers", "" }, 
            { "snow", "" }, 
            { "storm", "" }, 
            { "storms", "" }, 
            { "sunny", "" }, 
            { "tropical_cyclone", "" }, 
            { "wind", "" },
            { "windy", "" }
          };
          // map conditions to icons after sunset
          std::map<std::string, std::string> weather_state_night { 
            { "clear", "" }, 
            { "cloudy", "" }, 
            { "cyclone", "" }, 
            { "dust", "" }, 
            { "dusty", "" }, 
            { "fog", "" }, 
            { "frost", "" }, 
            { "haze", "" }, 
            { "hazy", "" }, 
            { "heavy_shower", "" }, 
            { "heavy_showers", "" }, 
            { "light_rain", "" }, 
            { "light_shower", "" }, 
            { "light_showers", "" }, 
            { "mostly_sunny", "" }, 
            { "partly_cloudy", "" }, 
            { "rain", "" }, 
            { "shower", "" }, 
            { "showers", "" }, 
            { "snow", "" }, 
            { "storm", "" }, 
            { "storms", "" }, 
            { "sunny", "" }, 
            { "tropical_cyclone", "" }, 
            { "wind", "" },
            { "windy", "" }
          };
          // select icon from approriate map (daytime / nighttime)
          if (id(sun_elevation).state <= 0) {
            it.printf(5, 2, id(weather_22), "%s", (weather_state_night[conditions[0]]).c_str());
          } else {
            it.printf(5, 2, id(weather_22), "%s", (weather_state[conditions[0]]).c_str());
          }
          // print today's forecast
          std::string f0 = "";
          if (id(u1_min0).state == "unknown") {
            f0 = ("Remainder of today - " + id(u1_max0).state + "° " + id(u1_forecast0).state) + " ";
          } else {
            f0 = ("Today - " + id(u1_min0).state + "°/" + id(u1_max0).state + "° " + id(u1_forecast0).state) + " ";
          }
          // Wrap at last space before 36 characters
          // don't wrap last line
          std::string line = "";
          int pos = 0;
          int end = 35;
          for (int i = 1; i <= 4; i++) {
            end = f0.find_last_of(" ", end);
            line = f0.substr(pos, end-pos);
            pos = end + 1;
            end = end + 36;
            it.printf(40, ((i-1)*13+9), id(calibri_12), "%s", line.c_str());
          }
          it.printf(40, ((4)*13+9), id(calibri_12), "%s", f0.substr(pos, 999).c_str());
          // daily short forecasts
          for (int i = 1; i < 7; i++) {
            it.line(0, (i*38+43), 240, (i*38+43), light_blue);
            it.printf(5, (i*38+45), id(weather_22), "%s", (weather_state[conditions[i]]).c_str());
            line = "";
            pos = 0;
            end = 35;            
            // Wrap at last space before 36 characters
            for (int j = 1; j <= 2; j++) {
              end = forecast[i].find_last_of(" ", end);
              line = forecast[i].substr(pos, end-pos);
              pos = end + 1;
              end = end + 36;
              it.printf(40, (i*38+51+(j-1)*13), id(calibri_12), "%s", line.c_str());
            }
          }

    # Page 7 - Extended 2 day forecast data  
      - id: u1_page7
        lambda: |-
          // list of condition for forecast icons
          auto light_blue = Color(135, 237, 232);
          std::string conditions[7] = { id(u1_icon0).state.c_str(), 
                                        id(u1_icon1).state.c_str(), 
                                        id(u1_icon2).state.c_str(), 
                                        id(u1_icon3).state.c_str(), 
                                        id(u1_icon4).state.c_str(), 
                                        id(u1_icon5).state.c_str(), 
                                        id(u1_icon6).state.c_str() };
          // days of week for forecast text
          std::string weekday[14] = { "Today", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri" };
          int wd = id(u1_my_time).now().day_of_week;
          // map conditions to icons
          std::map<std::string, std::string> weather_state { 
            { "clear", "" }, 
            { "cloudy", "" }, 
            { "cyclone", "" }, 
            { "dust", "" }, 
            { "dusty", "" }, 
            { "fog", "" }, 
            { "frost", "" }, 
            { "haze", "" }, 
            { "hazy", "" }, 
            { "heavy_shower", "" }, 
            { "heavy_showers", "" }, 
            { "light_rain", "" }, 
            { "light_shower", "" }, 
            { "light_showers", "" }, 
            { "mostly_sunny", "" }, 
            { "partly_cloudy", "" }, 
            { "rain", "" }, 
            { "shower", "" }, 
            { "showers", "" }, 
            { "snow", "" }, 
            { "storm", "" }, 
            { "storms", "" }, 
            { "sunny", "" }, 
            { "tropical_cyclone", "" }, 
            { "wind", "" },
            { "windy", "" }
          };
          // map conditions to icons after sunset
          std::map<std::string, std::string> weather_state_night { 
            { "clear", "" }, 
            { "cloudy", "" }, 
            { "cyclone", "" }, 
            { "dust", "" }, 
            { "dusty", "" }, 
            { "fog", "" }, 
            { "frost", "" }, 
            { "haze", "" }, 
            { "hazy", "" }, 
            { "heavy_shower", "" }, 
            { "heavy_showers", "" }, 
            { "light_rain", "" }, 
            { "light_shower", "" }, 
            { "light_showers", "" }, 
            { "mostly_sunny", "" }, 
            { "partly_cloudy", "" }, 
            { "rain", "" }, 
            { "shower", "" }, 
            { "showers", "" }, 
            { "snow", "" }, 
            { "storm", "" }, 
            { "storms", "" }, 
            { "sunny", "" }, 
            { "tropical_cyclone", "" }, 
            { "wind", "" },
            { "windy", "" }
          };
          // select icon from approriate map (daytime / nighttime)
          if (id(sun_elevation).state <= 0) {
            it.printf(5, 2, id(weather_22), "%s", (weather_state_night[conditions[0]]).c_str());
          } else {
            it.printf(5, 2, id(weather_22), "%s", (weather_state[conditions[0]]).c_str());
          }
          // print today's forecast
          std::string f0 = "";
          if (id(u1_min0).state == "unknown") {
            f0 = ("Remainder of today - " + id(u1_max0).state + "° " + id(u1_forecast0).state) + " " + id(u1_rainchance0).state +"% chance of " + id(u1_rainrange0).state +"mm ";
          } else {
            f0 = ("Today - " + id(u1_min0).state + "°/" + id(u1_max0).state + "° " + id(u1_forecast0).state) + " " + id(u1_rainchance0).state +"% chance of " + id(u1_rainrange0).state +"mm ";
          }
          // Wrap at last space before 35 characters
          // don't wrap last line
          std::string line = "";
          int pos = 0;
          int end = 35;
          for (int i = 1; i <= 10; i++) {
            end = f0.find_last_of(" ", end);
            line = f0.substr(pos, end-pos);
            pos = end + 1;
            end = end + 35;
            it.printf(40, ((i-1)*13+10), id(calibri_12), "%s", line.c_str());
          }
          it.printf(40, ((10)*13+10), id(calibri_12), "%s", f0.substr(pos, 999).c_str());
          it.line(0, 160, 240, 160, light_blue);
          // print tomorrow's forecast
          it.printf(5, 172, id(weather_22), "%s", (weather_state[conditions[1]]).c_str());
          f0 = ("Tomorrow - " + id(u1_min1).state + "°/" + id(u1_max1).state + "° " + id(u1_forecast1e).state) + " " + id(u1_rainchance0).state +"% chance of " + id(u1_rainrange0).state +"mm ";
          // Wrap at last space before 35 characters
          // don't wrap last line
          line = "";
          pos = 0;
          end = 35;
          for (int i = 1; i <= 10; i++) {
            end = f0.find_last_of(" ", end);
            line = f0.substr(pos, end-pos);
            pos = end + 1;
            end = end + 35;
            it.printf(40, ((i-1)*13+170), id(calibri_12), "%s", line.c_str());
          }
          it.printf(40, ((10)*13+170), id(calibri_12), "%s", f0.substr(pos, 999).c_str());

Hello!

First, let me tell you just how great this is. I love it! I love to see people making this kind of stuff at home, rather than buying it someplace.

My question though is this; I am making something far less demanding than this, but your project is the closest thing that implements something I am trying to accomplish. Essentially I am running a program on a Heltec esp32 wifi kit with an SSD1306_i2c 128x64 display. I am feeding info to it from some Home Assistant text sensors, but I require a “word wrap” function. I can’t for the life of me figure it out, nor have I been able to find anything in days of searching the web. As I said, your project comes close, as it has integrated word-wrapping, but in all my cut-n-paste programming attempts, I can’t for the life of me get it to work.

Can you help with this?

text_sensor:

  - platform: homeassistant
    name: "Message"
    id: message
    entity_id: input_text.message
    icon: "mdi:message-text"

Then I want to wrap the text here:

display:

  - platform: ssd1306_i2c
    model: "SSD1306 128x64"
    reset_pin: 16
    address: 0x3C
    id: my_display
      - id: page1
        lambda: |-
          it.printf(0, 2,  id(font4), "%s", id(message).state.c_str());
    pages:

Ideally, automatically centred horizontally and limited to 10 characters per line.

Can you help???

Hmm - 10 characters per line? That’s possible but a bit limiting. I will have a look at my code and see if I can come up with something that will do the job, but maybe words that are longer than 10 chars will have to overflow the edges of the display.

Hey there! Thanks for your reply.

There wouldn’t actually be very many words on it, generally about three. Its a field for setting reminders.

I think this will do what you want - but I haven’t tested so it could be a bug ridden nightmare :slight_smile:

          std::string s = "";
          s = id(sensor).state) + " ";
          // Wrap at last space up to and including 10 characters
          std::string line = "";
          int pos = 0;
          int end = 9;
          int found = 0;
          int dpw = 120; // display width in pixels
          int lsp = 13; // line spacing in pixels
          int fln = 9;  // first line pos in pixels
          // i <=4 means we get 4 lines from initial string
          for (int i = 1; i <= 4; i++) {
            // start by looking for last space - looking no more than 10 chars from begininning of last word
            found = s.find_last_of(" ", end);
            // if we didn't find space - find the next after (word is longer than 10 chars)
            if (found == -1) {
              found = s.find_first_of(" ", end);
            }
            line = s.substr(pos, found-pos);
            pos = found + 1;
            end = found + 10;
            it.printf(dpw/2, ((i-1)*lsp+fln), id(calibri_12), TextAlign::TOP_CENTER, "%s", line.c_str());
          }

Hey there again Daryl!

So I wasn’t as bug ridden as you thought, I got one error after plugging my own info in - there was a closed bracket ‘)’ too many in the code. After that was taken out, it worked fine. I made a few small changes, but it should do what I need. I have noticed that it sometimes repeats the last line, depending on how many characters is in it after how many spaces. I think I can work with this though. Thank you so much!

I have attached the modified code for your, and anyone else’s interest.

        lambda: |-
          std::string s = "";
          s = id(message).state + " ";
          // Wrap at last space up to and including 10 characters
          std::string line = "";
          int pos = 0;
          int end = 9;
          int found = 0;
          int dpw = 128; // display width in pixels
          int lsp = 18; // line spacing in pixels
          int fln = 7;  // first line pos in pixels // 9
          // i <=4 means we get 4 lines from initial string
          for (int i = 1; i <= 4; i++) {
            // start by looking for last space - looking no more than 10 chars from begininning of last word
            found = s.find_last_of(" ", end);
            // if we didn't find space - find the next after (word is longer than 10 chars)
            if (found == -1) {
              found = s.find_first_of(" ", end);
            }
            line = s.substr(pos, found-pos);
            pos = found + 1;
            end = found + 10;
            it.printf(dpw/2, ((i-1)*lsp+fln), id(font4), TextAlign::TOP_CENTER, "%s", line.c_str());
          }
1 Like

Yeah reading through it, it doesn’t handle strings that break up into shorter than 4 lines…

I did a pen & paper test of the algorithm - but all my test cases were based on strings that parsed out to be 4 lines or more.

You probably need to do something like:

            if (found == -1) {
              found = s.find_first_of(" ", end);
              if (found == -1) {
                found = 999;
              }
            }

Or similar processing that handles finding the end of the string before 4 lines.

Thanks. I’ll try and fiddle around with that one when I can get in front of my computer and the device i’m flashing again.
I’ll let you know how it goes.