substitutions: device_name: "esp32-75epaper-hal" friendly_name: "Epaper display hal" comment: "7.5inch epaper display hal" location: "hal" api_password: !secret eink_display_api ota_password: !secret ota_password wifi_ssid: !secret wifi_ssid wifi_password: !secret wifi_password # Pins for Waveshare ePaper ESP32-S3 pin_spi_clk: GPIO7 pin_spi_mosi: GPIO9 pin_ep_cs: GPIO2 pin_ep_dc: GPIO4 pin_ep_busy: GPIO3 pin_ep_reset: GPIO1 packages: board: !include boards/esp32-S3.yaml device_base: !include common/common.yaml connection: !include common/wifi_nosens.yaml logger: !include templates/logger.yaml esphome: includes: - include/epaper75.h - include/text_utils.h external_components: # Use local patched waveshare component with red/black support - source: type: local path: components_local components: [ waveshare_epaper ] sun: latitude: !secret home_latitude longitude: !secret home_longitude globals: - id: data_updated type: bool restore_value: no initial_value: 'false' - id: initial_data_received type: bool restore_value: no initial_value: 'false' - id: recorded_display_refresh type: int restore_value: yes initial_value: '0' time: - platform: homeassistant id: esptime on_time_sync: - then: - logger.log: "Time synced with Home Assistant" on_time: - minutes: 5 then: - if: condition: lambda: 'return id(data_updated) == true;' then: - lambda: 'id(initial_data_received) = true;' - logger.log: "Sensor data updated: Refreshing display..." - component.update: eink_display - lambda: 'id(data_updated) = false;' - lambda: 'id(recorded_display_refresh) += 1;' - lambda: 'id(display_last_update).publish_state(id(esptime).now().timestamp);' else: - logger.log: "No sensors updated - skipping display refresh." button: - platform: template name: "Hallway Display Refresh" id: refresh icon: "mdi:refresh" on_press: - logger.log: "Hallway display manual refresh" - component.update: eink_display # Fonts for display font: - file: 'fonts/GothamRnd-Bold.ttf' id: font_date size: 42 glyphs: &default-glyphs ['&', '@', '!', ',', '.', '"', '%', '(', ')', '+', '-', '_', ':', '°', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', ' ', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z','|', '/'] - file: 'fonts/GothamRnd-Bold.ttf' id: font_temperature size: 48 glyphs: *default-glyphs - file: 'fonts/GothamRnd-Book.ttf' id: font_weather_condition size: 20 glyphs: *default-glyphs - file: 'fonts/GothamRnd-Bold.ttf' id: font_section_title size: 20 glyphs: *default-glyphs - file: 'fonts/GothamRnd-Bold.ttf' id: font_section_header size: 28 glyphs: *default-glyphs - file: 'fonts/GothamRnd-Book.ttf' id: font_status_text size: 16 glyphs: *default-glyphs - file: 'fonts/GothamRnd-Book.ttf' id: font_small size: 12 glyphs: *default-glyphs # Material Design Icons - file: 'fonts/materialdesignicons-webfont.ttf' id: font_mdi_large size: 64 glyphs: - "\U000F0590" # mdi-weather-cloudy - "\U000F0595" # mdi-weather-partly-cloudy - "\U000F0599" # mdi-weather-sunny - "\U000F0594" # mdi-weather-night - "\U000F0597" # mdi-weather-rainy - "\U000f010b" # mdi-car - "\U000F0575" # mdi-lock - "\U000F0576" # mdi-lock-open - "\U000F0E84" # mdi-home - "\U000F0048" # mdi-lightning-bolt sensor: - platform: template name: "${device_name} Last Update" device_class: timestamp id: display_last_update - platform: wifi_signal id: sensor_wifi_signal name: "${device_name} WiFi Signal" update_interval: 60s on_value: - component.update: sensor_wifi_signal_percentage - platform: template id: sensor_wifi_signal_percentage name: "${device_name} WiFi Signal Percentage" icon: "mdi:wifi" unit_of_measurement: "%" update_interval: never lambda: |- if (id(sensor_wifi_signal).state) { if (id(sensor_wifi_signal).state <= -100 ) { return 0; } else if (id(sensor_wifi_signal).state >= -50) { return 100; } else { return 2 * (id(sensor_wifi_signal).state + 100); } } else { return NAN; } - platform: template name: "${device_name} Display Refresh Count" lambda: 'return id(recorded_display_refresh);' unit_of_measurement: "refreshes" # Placeholder sensor - will be overwritten by HA include - platform: template name: "Weather Temperature" id: weather_temp unit_of_measurement: "°C" lambda: 'return 22.0;' # Mock data - !include { file: sensors/homeassistant.yaml, vars: { id: weather_temperature_now, entity_id: sensor.hallway_display_data }} - !include { file: sensors/homeassistant.yaml, vars: { id: car_charge_level, entity_id: sensor.hallway_display_data }} text_sensor: # Pull data from Home Assistant hallway template sensor using reusable include - !include { file: sensors/homeassistant.yaml, vars: { id: datum, entity_id: sensor.hallway_display_data }} - !include { file: sensors/homeassistant.yaml, vars: { id: weather_condition_now, entity_id: sensor.hallway_display_data }} - !include { file: sensors/homeassistant.yaml, vars: { id: door_status, entity_id: sensor.hallway_display_data }} - !include { file: sensors/homeassistant.yaml, vars: { id: presence_status, entity_id: sensor.hallway_display_data }} - !include { file: sensors/homeassistant.yaml, vars: { id: car_status, entity_id: sensor.hallway_display_data }} - !include { file: sensors/homeassistant.yaml, vars: { id: car_range, entity_id: sensor.hallway_display_data }} - !include { file: sensors/homeassistant.yaml, vars: { id: last_change, entity_id: sensor.hallway_display_data }} # Primary data source - pulls from Home Assistant hallway template sensor - platform: homeassistant id: display_data_source entity_id: sensor.hallway_display_data binary_sensor: - platform: template name: "Door Locked" id: door_locked lambda: 'return true;' # Mock data - platform: template name: "People Home" id: people_home lambda: 'return true;' # Mock data - platform: template name: "Car Charging" id: car_charging lambda: 'return false;' # Mock data # Define colors for e-paper BV2 (3-color: white background, black and red ink) color: - id: color_black red: 0% green: 0% blue: 0% white: 50% - id: color_white red: 0% green: 0% blue: 0% white: 0% - id: color_red red: 100% green: 0% blue: 0% white: 0% # SPI configuration spi: clk_pin: ${pin_spi_clk} mosi_pin: ${pin_spi_mosi} # Display configuration display: - platform: waveshare_epaper cs_pin: ${pin_ep_cs} dc_pin: ${pin_ep_dc} busy_pin: number: ${pin_ep_busy} inverted: true reset_pin: ${pin_ep_reset} reset_duration: 2ms model: 7.50in-bv2-rb update_interval: 30min id: eink_display rotation: 90° lambda: |- // Display: Waveshare 7.5" native 800x480, with rotation: 90° gives 480x800 (portrait) int w = it.get_width(); // 480 in portrait int h = it.get_height(); // 800 in portrait // Fill background white it.fill(COLOR_OFF); // ===== LAYOUT VARIABLES ===== int margin = 8; int inner_margin = 12; int section_gap = 6; int section_width = w - 2 * margin; // Sections sized to fit 800px height int header_y = margin; int header_height = 140; int door_y = header_y + header_height + section_gap; int door_height = 110; int car_y = door_y + door_height + section_gap; int car_height = 110; int presence_y = car_y + car_height + section_gap; int presence_height = 110; int energy_y = presence_y + presence_height + section_gap; int footer_height = 45; int footer_y = h - footer_height; int energy_height = footer_y - energy_y - section_gap; // ===== HEADER: Date & Weather ===== // Date - with fallback if (id(datum).has_state()) { it.printf(w / 2, header_y, id(font_date), TextAlign::TOP_CENTER, "%s", id(datum).state.c_str()); } else { it.printf(w / 2, header_y, id(font_date), TextAlign::TOP_CENTER, "Loading..."); } int weather_y = header_y + 55; int icon_x = margin + 35; it.circle(icon_x, weather_y, 25); int temp_col_x = icon_x + 45; if (id(weather_temperature_now).has_state()) { it.printf(temp_col_x, weather_y - 15, id(font_temperature), TextAlign::TOP_LEFT, "%2.0f°C", id(weather_temperature_now).state); } else { it.printf(temp_col_x, weather_y - 15, id(font_temperature), TextAlign::TOP_LEFT, "--°C"); } if (id(weather_condition_now).has_state()) { it.printf(temp_col_x, weather_y + 25, id(font_weather_condition), TextAlign::TOP_LEFT, "%s", id(weather_condition_now).state.c_str()); } else { it.printf(temp_col_x, weather_y + 25, id(font_weather_condition), TextAlign::TOP_LEFT, "---"); } // Divider line int divider_y = header_y + header_height - 10; it.line(margin, divider_y, w - margin, divider_y); // ===== SECTION 1: Door Lock Status ===== it.rectangle(margin, door_y, section_width, door_height); it.printf(margin + inner_margin, door_y + 8, id(font_section_header), TextAlign::TOP_LEFT, "[Door]"); if (id(door_status).has_state()) { it.printf(margin + inner_margin, door_y + 38, id(font_status_text), TextAlign::TOP_LEFT, "%s", id(door_status).state.c_str()); } else { it.printf(margin + inner_margin, door_y + 38, id(font_status_text), TextAlign::TOP_LEFT, "Loading..."); } // ===== SECTION 2: Presence Status ===== it.rectangle(margin, presence_y, section_width, presence_height); it.printf(margin + inner_margin, presence_y + 8, id(font_section_header), TextAlign::TOP_LEFT, "[Presence]"); if (id(presence_status).has_state()) { it.printf(margin + inner_margin, presence_y + 38, id(font_status_text), TextAlign::TOP_LEFT, "%s", id(presence_status).state.c_str()); } else { it.printf(margin + inner_margin, presence_y + 38, id(font_status_text), TextAlign::TOP_LEFT, "Loading..."); } // ===== SECTION 3: Car Battery Status ===== it.rectangle(margin, car_y, section_width, car_height); it.printf(margin + inner_margin, car_y + 8, id(font_section_header), TextAlign::TOP_LEFT, "[Car]"); if (id(car_charge_level).has_state()) { it.printf(margin + inner_margin, car_y + 38, id(font_status_text), TextAlign::TOP_LEFT, "Charge: %2.0f%%", id(car_charge_level).state); } else { it.printf(margin + inner_margin, car_y + 38, id(font_status_text), TextAlign::TOP_LEFT, "Charge: --%"); } // ===== SECTION 4: Energy Usage ===== it.rectangle(margin, energy_y, section_width, energy_height); it.printf(margin + inner_margin, energy_y + 10, id(font_section_header), TextAlign::TOP_LEFT, "[Energy]"); int graph_left = margin + inner_margin; int graph_top = energy_y + 40; int graph_width = section_width - 2 * inner_margin; int graph_height = energy_height - 50; int num_bars = 24; float bar_heights[] = { 15, 28, 42, 38, 35, 48, 52, 45, 40, 55, 60, 50, 45, 58, 62, 55, 48, 65, 70, 60, 52, 68, 75, 55 }; int bar_w = (graph_width - 6) / num_bars; for (int i = 0; i < num_bars; i++) { int bh = (bar_heights[i] * graph_height) / 80; int bx = graph_left + (i * bar_w) + 2; int by = graph_top + graph_height - bh; it.filled_rectangle(bx, by, bar_w - 1, bh); } // ===== FOOTER ===== it.rectangle(margin, footer_y, section_width, footer_height); it.printf(w / 2, footer_y + 6, id(font_section_header), TextAlign::TOP_CENTER, "All Systems OK"); it.printf(w - margin - inner_margin, footer_y + 20, id(font_small), TextAlign::TOP_RIGHT, "Updated");