# ======================================================== # Update schedule and component behavior overview. # This table reflects the actual behavior of the E1002. # ======================================================== # Component | Day (min) | Night (min) | Notes # ------------------------ |-----------|-------------|----------------------------------------- # Internal sensors | 15 | 60 | Values pulled from Home Assistant # External sensors | 15 | 60 | Values pulled from Home Assistant # Battery state | 15 | 60 | Battery read on each scheduled refresh # Wi-Fi signal | 15 | 60 | RSSI measured on each scheduled refresh # Full display refresh | 15 | 60 | Triggered by interval or manual button # Update Counter | 15 | 60 | Increments only on full refresh events # Logo visibility | 06:00–22:00 | 06:00–22:00 | Displayed only during daytime # ======================================================== ############################################################## # GENERAL CONFIGURATION ############################################################## esphome: name: e1002 friendly_name: E1002 min_version: 2025.12.5 # Always-on device. No deep sleep. Refreshes are interval-driven. on_boot: priority: -100 then: - output.turn_on: battery_enable # Enables battery measurement circuit - delay: 50s # Allows Wi-Fi and HA sensors to update - component.update: epaper # Initial display refresh on boot esp32: board: esp32-s3-devkitc-1 framework: type: esp-idf flash_size: 16MB # Same hardware base as E1001, but with continuous operation. ############################################################## # WIFI ############################################################## wifi: ssid: !secret wifi_ssid password: !secret wifi_password power_save_mode: none # Wi-Fi always active manual_ip: static_ip: 192.168.1.28 gateway: 192.168.1.1 subnet: 255.255.255.0 ############################################################## # SERVICES ############################################################## logger: api: ota: platform: esphome # Standard services for an always-on device. ############################################################## # SPI – E-PAPER ############################################################## spi: clk_pin: GPIO7 mosi_pin: GPIO9 # SPI bus for the Seeed E1002 e‑ink panel. ############################################################## # I2C ############################################################## i2c: sda: GPIO18 scl: GPIO17 scan: true # I2C bus for internal module sensors. ############################################################## # BATTERY OUTPUT CONTROL ############################################################## output: - platform: gpio pin: GPIO21 id: battery_enable # Controls the battery voltage divider. ############################################################## # BUTTONS ############################################################## binary_sensor: - platform: gpio pin: number: GPIO3 mode: INPUT_PULLUP inverted: true id: refresh_button name: "Botón verde E1002 / Green Button" # Manual full refresh trigger. on_press: then: - lambda: |- id(update_counter).publish_state(id(update_counter).state + 1); id(epaper).update(); - platform: gpio pin: GPIO4 id: usb_power name: "USB-C conectado / USB-C connected" # Detects external USB-C power. ############################################################## # FONTS ############################################################## font: # Same font set as E1001 for consistent visual layout. - file: "fonts/Roboto-Regular.ttf" id: font_text_values size: 34 glyphs: " 0123456789.,-" - file: "fonts/Roboto-Regular.ttf" id: font_text_units size: 18 glyphs: " 0123456789%°/³CpgmUSB" - file: "fonts/Roboto-Regular.ttf" id: font_text_small size: 26 glyphs: " 0123456789:-%" - file: "fonts/MaterialDesignIconsDesktop.ttf" id: font_mdi_small size: 32 glyphs: - "\U000F0150" - "\U000F091F" - "\U000F0920" - file: "fonts/MaterialDesignIconsDesktop.ttf" id: font_mdi size: 48 glyphs: - "\U000F050F" - "\U000F058E" - "\U000F07E4" - "\U000F0C62" ############################################################## # LOGO ############################################################## image: - file: "images/logo.bmp" id: logo type: RGB resize: 280x75 # Color logo supported by the E1002 panel. ############################################################## # TIME ############################################################## time: - platform: homeassistant id: ha_time # Time used for clock display and logo visibility logic. ############################################################## # SENSORS ############################################################## sensor: # All environmental values come from Home Assistant. - platform: homeassistant id: temp_salon entity_id: sensor.calefaccion_temperatura - platform: homeassistant id: hum_interior entity_id: sensor.i_9psl_humedad - platform: homeassistant id: co2_interior entity_id: sensor.i_9psl_dioxido_de_carbono - platform: homeassistant id: pm25_interior entity_id: sensor.i_9psl_pm2_5 - platform: homeassistant id: temp_exterior entity_id: sensor.o_1pst_temperature - platform: homeassistant id: hum_exterior entity_id: sensor.o_1pst_humedad - platform: homeassistant id: co2_exterior entity_id: sensor.o_1pst_dioxido_de_carbono - platform: homeassistant id: pm25_exterior entity_id: sensor.o_1pst_pm2_5 - platform: wifi_signal id: wifi_rssi # Wi-Fi RSSI measured on each interval refresh. - platform: adc pin: GPIO1 id: battery_voltage attenuation: 12db filters: - multiply: 2.0 # Battery voltage measurement. - platform: template id: battery_percent name: "E1002 Batería / Battery" unit_of_measurement: "%" # Linear voltage-to-percentage estimation. lambda: |- float v = id(battery_voltage).state; if (isnan(v)) return NAN; float pct = (v - 3.0f) * 100.0f / 1.2f; if (pct < 0) pct = 0; if (pct > 100) pct = 100; return pct; - platform: uptime id: uptime_seconds name: "E1002 Tiempo en marcha / Uptime" # Tracks continuous runtime. - platform: template id: full_refresh_sensor name: "E1002 Full Refreshes" unit_of_measurement: "refresh" # One full refresh every 10 partial refreshes. lambda: |- return id(update_counter).state / 10.0; ############################################################## # PERSISTENT COUNTER ############################################################## number: - platform: template id: update_counter name: "E1002 Update Counter" optimistic: true restore_value: true min_value: 0 max_value: 100000 step: 1 # Persistent counter for tracking refresh cycles. ############################################################## # AUTO REFRESH ############################################################## interval: - interval: 15min # DAY then: - lambda: |- id(update_counter).publish_state(id(update_counter).state + 1); id(epaper).update(); - interval: 60min # NIGHT then: - lambda: |- id(update_counter).publish_state(id(update_counter).state + 1); id(epaper).update(); # Interval-based refresh system. No deep sleep. ############################################################## # DISPLAY – FULL LAYOUT ############################################################## display: - platform: epaper_spi model: Seeed-reTerminal-E1002 id: epaper update_interval: never full_update_every: 10 # Native driver for the Seeed E1002 e‑ink panel. lambda: |- auto safe = [&](float v, const char* fmt) -> std::string { if (isnan(v)) return "--"; char buf[16]; snprintf(buf, sizeof(buf), fmt, v); return buf; }; it.filled_rectangle(0, 0, 800, 480, Color::WHITE); auto now = id(ha_time).now(); bool ok = now.is_valid(); // TIME it.printf(20, 20, id(font_mdi_small), Color::BLACK, "\U000F0150"); it.printf(70, 22, id(font_text_small), Color::BLACK, ok ? "%02d:%02d" : "--:--", now.hour, now.minute); // WIFI float rssi = id(wifi_rssi).state; float q = isnan(rssi) ? NAN : (rssi <= -100 ? 0 : (rssi >= -50 ? 100 : 2 * (rssi + 100))); const char* wifi_icon = (!isnan(q) && q > 50) ? "\U000F091F" : "\U000F0920"; it.printf(360, 20, id(font_mdi_small), Color::BLACK, wifi_icon); it.printf(407, 22, id(font_text_small), Color::BLACK, isnan(q) ? "--" : "%.0f%%", q); // BATTERY float b = id(battery_percent).state; int bx = 650, by = 30, bw = 45, bh = 20; Color batt_color = Color(0,255,0); if (b < 20) batt_color = Color(255,0,0); else if (b < 60) batt_color = Color(255,255,0); it.rectangle(bx, by, bw, bh, Color::BLACK); it.rectangle(bx + 2, by + 2, bw - 4, bh - 4, Color::BLACK); int fill_w = (bw - 4) * b / 100.0; it.filled_rectangle(bx + 2, by + 2, fill_w, bh - 4, batt_color); it.filled_rectangle(bx + bw, by + (bh/2 - 4), 5, 8, Color::BLACK); it.printf(bx + bw + 12, 22, id(font_text_small), Color::BLACK, "%.0f%%", b); // HOUSE FRAME int hx = 60, hy = 140, hw = 340, hh = 260; it.rectangle(hx, hy, hw, hh, Color::BLACK); it.rectangle(hx + 2, hy + 2, hw - 4, hh - 4, Color::BLACK); it.line(hx, hy, hx + hw/2, hy - 70, Color::BLACK); it.line(hx + hw/2, hy - 70, hx + hw, hy, Color::BLACK); it.line(hx + 2, hy + 2, hx + hw/2, hy - 68, Color::BLACK); it.line(hx + hw/2, hy - 68, hx + hw - 2, hy + 2, Color::BLACK); // INTERNAL SENSORS int icon_x = 110; int value_x = 200; int unit_x = 300; int y1 = 170; int y2 = 230; int y3 = 290; int y4 = 350; Color INT = Color::BLACK; const char* int_icons[] = {"\U000F050F","\U000F058E","\U000F07E4","\U000F0C62"}; float int_values[] = {id(temp_salon).state, id(hum_interior).state, id(co2_interior).state, id(pm25_interior).state}; const char* int_units[] = {"°C","%","ppm","mg/m3"}; int ys[] = {y1,y2,y3,y4}; for(int i=0;i<4;i++){ int y = ys[i]; const char* fmt = (i == 2) ? "%.0f" : "%.1f"; it.printf(icon_x, y - 10, id(font_mdi), INT, int_icons[i]); it.printf(value_x, y - 8, id(font_text_values), INT, TextAlign::LEFT, "%s", safe(int_values[i], fmt).c_str()); it.printf(unit_x, y + 1, id(font_text_units), INT, "%s", int_units[i]); } // EXTERNAL SENSORS Color EXT = Color(0,0,255); int ext_icon_x = hx + hw + 50; int ext_value_x = ext_icon_x + (value_x - icon_x); int ext_unit_x = ext_value_x + (unit_x - value_x); float ext_values[] = {id(temp_exterior).state, id(hum_exterior).state, id(co2_exterior).state, id(pm25_exterior).state}; const char* ext_units[] = {"°C","%","ppm","mg/m3"}; const char* ext_icons[] = {"\U000F050F","\U000F058E","\U000F07E4","\U000F0C62"}; for(int i=0;i<4;i++){ int y = ys[i]; const char* fmt = (i == 2) ? "%.0f" : "%.1f"; it.printf(ext_icon_x, y - 10, id(font_mdi), EXT, ext_icons[i]); it.printf(ext_value_x, y - 10, id(font_text_values), EXT, TextAlign::LEFT, "%s", safe(ext_values[i], fmt).c_str()); it.printf(ext_unit_x, y + 1, id(font_text_units), EXT, "%s", ext_units[i]); } // LOGO bool show_logo = now.is_valid() && now.hour >= 6 && now.hour < 22; # Logo displayed only during daytime. if(show_logo){ int logo_width = 280; int logo_height = 75; int logo_x = (800 - logo_width) / 2; int logo_y = 480 - logo_height; it.image(logo_x, logo_y, id(logo)); }