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());