diff --git a/esphome/esp-bedside-panel.yaml b/esphome/esp-bedside-panel.yaml index 88efacc..0be8cd4 100644 --- a/esphome/esp-bedside-panel.yaml +++ b/esphome/esp-bedside-panel.yaml @@ -6,7 +6,10 @@ # https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-bedside-panel.yaml #:########################################################################################:# # VERSIONS: -# V1.49 2026-07-28 Calibrated battery voltage, added approximate charge level and a subtle Clock-page battery indicator +# V1.52 2026-07-28 Centralised battery publishing and removed continuously evaluated battery template lambdas +# V1.51 2026-07-28 Removed unsupported update_interval from the template USB-power binary sensor +# V1.50 2026-07-28 Reworked calibrated battery monitoring and Clock indicator using delayed interval updates after V1.49 boot rollback +# V1.49 2026-07-28 Added calibrated battery status and Clock indicator; superseded after boot rollback # V1.48 2026-07-28 Added raw onboard battery ADC voltage monitoring on GPIO53 for divider calibration # V1.47 2026-07-20 Widened the alarm volume range while retaining 5-100% controls and the proven 6000 Hz tone # V1.46 2026-07-20 Raised the alarm tone to 6000 Hz, limited volume to 5-100%, and remapped 100% to the previous displayed 30% level @@ -109,11 +112,10 @@ # - Battery-backed RTC: RX8130 # - RTC I2C address: 0x32 # - TEMT6000 ambient-light sensor signal: GPIO20 / ADC1 -# - Battery voltage sense test input: GPIO53 / ADC2 +# - Main battery voltage sense: GPIO53 / ADC2 # - ES8311 audio codec: I2C address 0x18 # - Audio I2S: MCLK GPIO13, BCLK GPIO12, LRCLK GPIO10, DOUT GPIO9 # - Speaker amplifier enable: GPIO11 (active high on this board revision) -# - Main battery voltage sense: GPIO53 / ADC, calibrated multiplier 1.6667 #:########################################################################################:# # OPERATION NOTES: # - This build returns to the proven V0.8a structure. @@ -160,10 +162,11 @@ # - Beep is a short 6000 Hz electronic tone. # - Alarm repeats a quick 6000 Hz beep-beep-beep group followed by a pause. # - Speaker tests generate audio locally and do not require Home Assistant or media files. -# - GPIO53 exposes both raw ADC voltage and calibrated main-battery voltage. -# - Battery percentage is an approximate voltage-based estimate for a single 18650 cell. +# - GPIO53 exposes both raw ADC voltage and calibrated 18650 voltage. +# - Battery percentage is an approximate voltage-based estimate. # - The Clock page shows a subtle battery icon and percentage while on battery. # - When USB power is inferred, the Clock page shows only a full battery icon. +# - Battery calculations and display updates are delayed until after normal startup. #:########################################################################################:# # OFFLINE NOTES: # - LVGL, the clock, touch and backlight operate locally. @@ -182,12 +185,12 @@ substitutions: # Device Naming device_name: "esp-bedside-panel" friendly_name: "ESP Bedside Panel" - description_comment: "Guition JC1060P470C_I_W_Y ESP32-P4 bedside panel with four alarms, calibrated battery monitoring, Clock-page battery status and 24-hour alarm editing. (Layout V1.1)" + description_comment: "Guition JC1060P470C_I_W_Y ESP32-P4 bedside panel with four alarms, calibrated battery monitoring, Clock battery status and 24-hour editing. (Layout V1.1)" device_area: "Bedroom" # Project Naming project_name: "Guition.JC1060P470C_I_W_Y" - project_version: "v1.49" + project_version: "v1.52" # Passwords & Secrets api_key: !secret esp-api_key @@ -208,17 +211,15 @@ substitutions: ambient_light_update_interval: "1s" # Battery Monitoring Settings - # GPIO53 is connected to the board's main battery-sense divider. - # Testing gave 2.369 V at the ADC for approximately 3.95 V at the cell and - # 2.520 V at the ADC for approximately 4.20 V while charging. This supports - # a divider multiplier of 5/3, or 1.6667. + # Testing supports a 5/3 divider multiplier: 2.520 V ADC is approximately + # 4.20 V at the connected single-cell 18650 battery. battery_voltage_pin: "GPIO53" battery_voltage_update_interval: "5s" battery_voltage_multiplier: "1.6667" + battery_status_update_interval: "15s" - # No dedicated VBUS-status GPIO has been confirmed for this board revision. - # USB presence is therefore inferred from a sustained battery-voltage rise, - # a charge-end voltage, or a clear voltage drop when USB is removed. + # No dedicated VBUS-status input has been confirmed. USB presence is + # inferred from sustained voltage rise, charge-end voltage and unplug drop. battery_usb_detect_rise_threshold: "0.006" battery_usb_detect_rise_samples: "2" battery_usb_disconnect_drop_threshold: "0.025" @@ -466,7 +467,6 @@ esphome: - script.execute: refresh_jobs_page - script.execute: refresh_alarm_clock_page - script.execute: refresh_alarm_audio_controls - - script.execute: refresh_clock_battery_indicator #:########################################################################################:# # ESP PLATFORM AND FRAMEWORK: @@ -1127,20 +1127,12 @@ sensor: update_interval: "${update_interval}" #:######################################################################################:# - # MAIN BATTERY MONITORING # + # MAIN BATTERY MONITORING # #:######################################################################################:# # - # GPIO53 is connected to the JC1060P470 battery-sense divider. The raw ADC - # reading remains available for diagnostics. A calibrated copy applies the - # tested 1.6667 multiplier to estimate the actual 18650 terminal voltage. - # - # Percentage is estimated from a piecewise single-cell Li-ion voltage curve. - # It is intentionally approximate because voltage changes with load, charge - # current, cell age and temperature. - # - # No confirmed board pin directly reports USB/VBUS presence. The external - # power state is inferred from sustained voltage rise, charge-end voltage and - # the clear voltage drop normally seen when USB power is removed. + # GPIO53 is connected to the onboard battery-sense divider. The raw ADC + # value remains available for diagnostics. Scaled voltage, percentage and + # estimated USB state are published together by a delayed status interval. - platform: adc id: battery_adc_voltage @@ -1160,138 +1152,28 @@ sensor: send_every: 3 send_first_at: 1 - - platform: copy + # These values are published together by the battery-status interval. + # Keeping their own update intervals disabled avoids duplicated calculations + # and guarantees that voltage, percentage and USB state use the same sample. + - platform: template id: battery_voltage name: "${friendly_name} Battery Voltage" - source_id: battery_adc_voltage + update_interval: never accuracy_decimals: 2 device_class: voltage state_class: measurement unit_of_measurement: "V" icon: mdi:battery - filters: - - multiply: "${battery_voltage_multiplier}" - - on_value: - then: - - lambda: |- - if (isnan(x)) { - return; - } - - const float previous = id(battery_previous_voltage); - bool external_power = id(battery_external_power_present); - - if (!isnan(previous)) { - const float delta = x - previous; - - if (delta <= -${battery_usb_disconnect_drop_threshold}f) { - // A clear downward step is the most dependable indication - // that USB power has just been removed. - external_power = false; - id(battery_voltage_rise_samples) = 0; - id(battery_voltage_stable_samples) = 0; - } else if (delta >= ${battery_usb_detect_rise_threshold}f) { - // Require more than one rising sample so normal ADC noise does - // not make the Clock page claim that USB is connected. - if (id(battery_voltage_rise_samples) < 255) { - id(battery_voltage_rise_samples)++; - } - id(battery_voltage_stable_samples) = 0; - - if ( - id(battery_voltage_rise_samples) >= - ${battery_usb_detect_rise_samples} - ) { - external_power = true; - } - } else { - id(battery_voltage_rise_samples) = 0; - - // If a previously inferred charging state becomes flat at a - // voltage well below charge-end, release it after four samples. - // This is mainly a recovery path for a missed unplug transition. - if ( - external_power && - x < ${battery_usb_stable_voltage_threshold}f && - fabsf(delta) <= ${battery_usb_stable_delta_threshold}f - ) { - if (id(battery_voltage_stable_samples) < 255) { - id(battery_voltage_stable_samples)++; - } - - if ( - id(battery_voltage_stable_samples) >= - ${battery_usb_stable_samples} - ) { - external_power = false; - id(battery_voltage_stable_samples) = 0; - } - } else { - id(battery_voltage_stable_samples) = 0; - } - } - } - - // Near charge-end, show the full icon. A freshly charged cell that - // is booted directly on battery can also meet this threshold, which - // is harmless because the cell is genuinely full at that point. - if (x >= ${battery_usb_full_threshold}f) { - external_power = true; - } - - id(battery_previous_voltage) = x; - id(battery_external_power_present) = external_power; - id(battery_external_power).publish_state(external_power); - - // Piecewise voltage-to-percentage approximation for a single - // 4.20 V Li-ion cell. Interpolate between the surrounding points. - static const float voltage_points[] = { - 3.30f, 3.50f, 3.60f, 3.70f, 3.75f, 3.80f, 3.85f, - 3.90f, 3.95f, 4.00f, 4.05f, 4.10f, 4.15f, 4.20f - }; - static const float percentage_points[] = { - 0.0f, 5.0f, 10.0f, 20.0f, 30.0f, 40.0f, 50.0f, - 60.0f, 70.0f, 80.0f, 88.0f, 94.0f, 98.0f, 100.0f - }; - constexpr size_t point_count = - sizeof(voltage_points) / sizeof(voltage_points[0]); - - float percentage = 0.0f; - - if (x <= voltage_points[0]) { - percentage = percentage_points[0]; - } else if (x >= voltage_points[point_count - 1]) { - percentage = percentage_points[point_count - 1]; - } else { - for (size_t i = 1; i < point_count; i++) { - if (x <= voltage_points[i]) { - const float position = - (x - voltage_points[i - 1]) / - (voltage_points[i] - voltage_points[i - 1]); - percentage = - percentage_points[i - 1] + - position * - (percentage_points[i] - percentage_points[i - 1]); - break; - } - } - } - - percentage = fminf(fmaxf(percentage, 0.0f), 100.0f); - id(battery_level_percent).publish_state(percentage); - - - script.execute: refresh_clock_battery_indicator - platform: template id: battery_level_percent name: "${friendly_name} Battery Level" + update_interval: never + accuracy_decimals: 0 device_class: battery state_class: measurement unit_of_measurement: "%" - accuracy_decimals: 0 icon: mdi:battery - update_interval: never #:######################################################################################:# # TEMT6000 AMBIENT LIGHT SENSOR @@ -1657,8 +1539,10 @@ sensor: #:########################################################################################:# binary_sensor: #:######################################################################################:# - # BATTERY / EXTERNAL POWER STATUS # + # BATTERY / EXTERNAL POWER STATUS # #:######################################################################################:# + # State is published by the same interval that calculates battery voltage. + # This avoids evaluating a template lambda on every ESPHome loop iteration. - platform: template id: battery_external_power name: "${friendly_name} USB Power Estimated" @@ -1925,12 +1809,17 @@ globals: initial_value: "1" #:######################################################################################:# - # MAIN BATTERY STATUS # + # MAIN BATTERY STATUS # #:######################################################################################:# - id: battery_previous_voltage type: float restore_value: false - initial_value: "NAN" + initial_value: "0.0" + + - id: battery_previous_voltage_valid + type: bool + restore_value: false + initial_value: "false" - id: battery_external_power_present type: bool @@ -2025,7 +1914,7 @@ globals: #:########################################################################################:# script: #:######################################################################################:# - # CLOCK PAGE BATTERY INDICATOR # + # CLOCK PAGE BATTERY INDICATOR # #:######################################################################################:# - id: refresh_clock_battery_indicator mode: restart @@ -5494,6 +5383,120 @@ script: # Update minute-resolution clock values locally #:########################################################################################:# interval: + # Battery calculations are deliberately performed here rather than inside + # the ADC sensor callback. This allows the normal display and network startup + # to complete before battery state or LVGL widgets are updated. + - interval: "${battery_status_update_interval}" + then: + - lambda: |- + const float raw_voltage = id(battery_adc_voltage).state; + + if (isnan(raw_voltage)) { + return; + } + + const float voltage = + raw_voltage * ${battery_voltage_multiplier}f; + bool external_power = id(battery_external_power_present); + + if (!id(battery_previous_voltage_valid)) { + id(battery_previous_voltage) = voltage; + id(battery_previous_voltage_valid) = true; + + if (voltage >= ${battery_usb_full_threshold}f) { + external_power = true; + } + } else { + const float delta = + voltage - id(battery_previous_voltage); + + if (delta <= -${battery_usb_disconnect_drop_threshold}f) { + external_power = false; + id(battery_voltage_rise_samples) = 0; + id(battery_voltage_stable_samples) = 0; + } else if (delta >= ${battery_usb_detect_rise_threshold}f) { + if (id(battery_voltage_rise_samples) < 255) { + id(battery_voltage_rise_samples)++; + } + + id(battery_voltage_stable_samples) = 0; + + if ( + id(battery_voltage_rise_samples) >= + ${battery_usb_detect_rise_samples} + ) { + external_power = true; + } + } else { + id(battery_voltage_rise_samples) = 0; + + if ( + external_power && + voltage < ${battery_usb_stable_voltage_threshold}f && + fabsf(delta) <= ${battery_usb_stable_delta_threshold}f + ) { + if (id(battery_voltage_stable_samples) < 255) { + id(battery_voltage_stable_samples)++; + } + + if ( + id(battery_voltage_stable_samples) >= + ${battery_usb_stable_samples} + ) { + external_power = false; + id(battery_voltage_stable_samples) = 0; + } + } else { + id(battery_voltage_stable_samples) = 0; + } + } + + if (voltage >= ${battery_usb_full_threshold}f) { + external_power = true; + } + + id(battery_previous_voltage) = voltage; + } + + float percentage = 0.0f; + + static const float voltage_points[] = { + 3.30f, 3.50f, 3.60f, 3.70f, 3.75f, 3.80f, 3.85f, + 3.90f, 3.95f, 4.00f, 4.05f, 4.10f, 4.15f, 4.20f + }; + static const float percentage_points[] = { + 0.0f, 5.0f, 10.0f, 20.0f, 30.0f, 40.0f, 50.0f, + 60.0f, 70.0f, 80.0f, 88.0f, 94.0f, 98.0f, 100.0f + }; + constexpr size_t point_count = + sizeof(voltage_points) / sizeof(voltage_points[0]); + + if (voltage <= voltage_points[0]) { + percentage = percentage_points[0]; + } else if (voltage >= voltage_points[point_count - 1]) { + percentage = percentage_points[point_count - 1]; + } else { + for (size_t i = 1; i < point_count; i++) { + if (voltage <= voltage_points[i]) { + const float position = + (voltage - voltage_points[i - 1]) / + (voltage_points[i] - voltage_points[i - 1]); + + percentage = percentage_points[i - 1] + + position * + (percentage_points[i] - percentage_points[i - 1]); + break; + } + } + } + + id(battery_external_power_present) = external_power; + id(battery_voltage).publish_state(voltage); + id(battery_level_percent).publish_state(percentage); + id(battery_external_power).publish_state(external_power); + + - script.execute: refresh_clock_battery_indicator + # Re-evaluate the screen against the latest filtered lux reading. This allows # the clock to brighten during the day and dim again as the room gets darker. - interval: "${adaptive_backlight_recheck_interval}"