From bb30742b1d82c7cae19a09467047d230b630cdc6 Mon Sep 17 00:00:00 2001 From: Eric McDonald Date: Thu, 14 May 2026 20:15:42 -0700 Subject: [PATCH] Fix crash when all page switches are disabled If you turn off all display page switches in Home Assistant, the AirGradient ONE crashes (reproduced on v5.3.3, likely affects v5.3.4). I traced this to the rotation logic in display_sh1106_multi_page.yaml and display_sh1106_single_page.yaml. When every page switch is off, including display_blank_page, the on_page_change handlers each call display.page.show_next on their own page. This triggers the next page's on_page_change handler, which calls show_next again, and so on. Because there's no base case or enabled page to stop the loop, the call stack grows without bound, triggering a stack overflow. This happens within microseconds and crashes the device. The configuration.md doc says: >disabling all pages will result in the device rebooting but does not explain why. This proposed fix gates show_next calls behind a template binary_sensor (any_non_blank_page_enabled), which is true if and only if any non-blank page switch is on. If the blank page switch is the only switch that's on, the blank page is shown. If the blank page switch is off and every other page switch is also off, the blank page is shown. Applied to display_sh1106_multi_page.yaml and display_sh1106_single_page.yaml. display_ssd1306.yaml is not affected because its on_page_change only has a handler for the boot page, which bounds the chain of possible page skips. configuration.md is updated to reflect that disabling all pages no longer crashes the device, and that setting the blank page switch on its own is sufficient to display a blank page. --- configuration.md | 7 ++-- packages/display_sh1106_multi_page.yaml | 43 +++++++++++++++++++----- packages/display_sh1106_single_page.yaml | 37 +++++++++++++++----- 3 files changed, 65 insertions(+), 22 deletions(-) diff --git a/configuration.md b/configuration.md index 895977a..36dba0b 100644 --- a/configuration.md +++ b/configuration.md @@ -81,7 +81,7 @@ Pairing ESPHome with Home Assistant opens a multitude of opportunities to create ## Disable Display and LED based on time -Enabling the "Blank Page" switch will disable output on the display. It is not necessary to turn off any existing pages, and disabling all pages will result in the device rebooting +Enabling the "Blank Page" switch will disable output on the display. It is not necessary to turn off any existing pages. 1. In Home Assistant, navigate to Settings>Automations and scenes 2. Click "Create Automation" button, then "Create new automation" @@ -90,9 +90,8 @@ Enabling the "Blank Page" switch will disable output on the display. It is not 2. Set to your desired time to turn off the display and/or LED 2. Then do 1. Add Action>Device>Select your AirGradient from the ESPHome Integration - 2. In the Action field, select "Turn on ` Display Blank Page`" - 1. For the base config with only the single page display package, this will set the display to show an empty page - 2. If using the multi_page package, may need to add additional actions to turn off the other enabled pages + 2. In the Action field, select "Turn on ` Display Blank Page`" (if your display package provides this switch) + 1. This will set the display to show an empty page 3. Repeat action for "Turn Off ` LED Strip`" (If applicable) 3. Click the Save button and give it a name, such as "AirGradient Night Mode" 3. Repeat with a new Automation, with the actions reversed (Turn off Display Blank Page and turn on LED Strip), at the desired time with a name such as "AirGradient Night Mode Off" diff --git a/packages/display_sh1106_multi_page.yaml b/packages/display_sh1106_multi_page.yaml index b1fffe5..f6378c2 100644 --- a/packages/display_sh1106_multi_page.yaml +++ b/packages/display_sh1106_multi_page.yaml @@ -18,6 +18,23 @@ font: glyphs: '!"%()+=,-_.:°0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz/µ³' +binary_sensor: + # True if and only if at least one non-blank page switch is on. + # Any new page switches should be added here + - platform: template + id: any_non_blank_page_enabled + internal: True + lambda: |- + return id(display_ag_default_page).state + || id(display_boot_page).state + || id(display_summary_pages).state + || id(display_huge_no_units_page).state + || id(display_air_quality_page).state + || id(display_air_temp_page).state + || id(display_voc_page).state + || id(display_combo_page).state; + + display: - platform: ssd1306_i2c # https://esphome.io/components/display/ssd1306.html @@ -202,11 +219,12 @@ display: - display.page.show_next: oled_display - component.update: oled_display - to: blank - # Skip blank page unless it is turned on and the interval: will only display it then: - if: + # Only change pages if a page switch for a non-blank page is actually enabled; + # acts as a base case to prevent recursive page change loop condition: - switch.is_off: display_blank_page + binary_sensor.is_on: any_non_blank_page_enabled then: - display.page.show_next: oled_display - component.update: oled_display @@ -227,14 +245,14 @@ interval: then: - if: condition: - # If the blank page switch is on, only display the blank page, otherwise, rotate to next page - switch.is_on: display_blank_page + and: + - switch.is_off: display_blank_page + - binary_sensor.is_on: any_non_blank_page_enabled then: - - display.page.show: blank + - display.page.show_next: oled_display - component.update: oled_display else: - # Change page on display - - display.page.show_next: oled_display + - display.page.show: blank - component.update: oled_display @@ -322,8 +340,15 @@ switch: - display.page.show: blank - component.update: oled_display on_turn_off: - - display.page.show_next: oled_display - - component.update: oled_display + - if: + condition: + binary_sensor.is_on: any_non_blank_page_enabled + then: + - display.page.show_next: oled_display + - component.update: oled_display + else: + - display.page.show: blank + - component.update: oled_display number: - platform: template diff --git a/packages/display_sh1106_single_page.yaml b/packages/display_sh1106_single_page.yaml index c0e839d..e5efc49 100644 --- a/packages/display_sh1106_single_page.yaml +++ b/packages/display_sh1106_single_page.yaml @@ -14,6 +14,17 @@ font: bpp: 2 +binary_sensor: + # True if and only if at least one non-blank page switch is on. + # Any new page switches should be added here + - platform: template + id: any_non_blank_page_enabled + internal: True + lambda: |- + return id(display_ag_default_page).state + || id(display_boot_page).state; + + display: - platform: ssd1306_i2c # https://esphome.io/components/display/ssd1306.html @@ -73,11 +84,12 @@ display: - display.page.show_next: oled_display - component.update: oled_display - to: blank - # Skip blank page unless it is turned on and the interval: will only display it then: - if: + # Only change pages if a page switch for a non-blank page is actually enabled; + # acts as a base case to prevent recursive page change loop condition: - switch.is_off: display_blank_page + binary_sensor.is_on: any_non_blank_page_enabled then: - display.page.show_next: oled_display - component.update: oled_display @@ -98,14 +110,14 @@ interval: then: - if: condition: - # If the blank page switch is on, only display the blank page, otherwise, rotate to next page - switch.is_on: display_blank_page + and: + - switch.is_off: display_blank_page + - binary_sensor.is_on: any_non_blank_page_enabled then: - - display.page.show: blank + - display.page.show_next: oled_display - component.update: oled_display else: - # Change page on display - - display.page.show_next: oled_display + - display.page.show: blank - component.update: oled_display @@ -145,8 +157,15 @@ switch: - display.page.show: blank - component.update: oled_display on_turn_off: - - display.page.show_next: oled_display - - component.update: oled_display + - if: + condition: + binary_sensor.is_on: any_non_blank_page_enabled + then: + - display.page.show_next: oled_display + - component.update: oled_display + else: + - display.page.show: blank + - component.update: oled_display number: - platform: template