Last active
January 11, 2026 04:05
-
-
Save heytcass/27221e7507fd2f03c13d1bf397b0feb8 to your computer and use it in GitHub Desktop.
Pimoroni Inky Impression Sendspin Display
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Sendspin E-Ink Display | |
| # Based on working ESP32-S3 Box 3 Sendspin config | |
| # Only changed: board, display, removed touchscreen/LVGL | |
| substitutions: | |
| name: sendspin-eink-display | |
| friendly_name: Sendspin E-Ink Display | |
| esphome: | |
| name: ${name} | |
| friendly_name: ${friendly_name} | |
| min_version: 2025.12.2 | |
| platformio_options: | |
| board_build.flash_mode: dio | |
| board_build.arduino.memory_type: qio_opi | |
| esp32: | |
| board: esp32-s3-devkitc-1 | |
| variant: esp32s3 | |
| flash_size: 4MB | |
| framework: | |
| type: esp-idf | |
| sdkconfig_options: | |
| CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240: "y" | |
| psram: | |
| mode: quad # S3-Zero uses quad PSRAM (2MB) | |
| speed: 80MHz | |
| api: | |
| encryption: | |
| key: !secret api_encryption_key | |
| ota: | |
| - platform: esphome | |
| password: !secret ota_password | |
| logger: | |
| level: DEBUG | |
| wifi: | |
| ssid: !secret wifi_ssid | |
| password: !secret wifi_password | |
| ap: | |
| ssid: "${name} Fallback" | |
| password: !secret fallback_password | |
| captive_portal: | |
| # Time sync from Home Assistant | |
| time: | |
| - platform: homeassistant | |
| id: ha_time | |
| # External components - pinned to specific commits per Paulus (balloob) | |
| # This bypasses PR ref caching issues with micro-opus dependency | |
| external_components: | |
| - source: github://pr#10212 | |
| components: [runtime_image, online_image] | |
| - source: | |
| type: git | |
| url: https://github.com/esphome/esphome | |
| ref: bff22983a390352360796f8e1363127e0cd1f898 | |
| components: [media_player] | |
| - source: | |
| type: git | |
| url: https://github.com/esphome/esphome | |
| ref: 71c8c6bdfc8006902eb0ea7b15bc86c575cef9be | |
| components: [generic_image, mdns, sendspin] | |
| # Sendspin hub - metadata and artwork only (no audio playback) | |
| sendspin: | |
| id: sendspin_hub | |
| # Album art from Sendspin - larger size to fill display width | |
| generic_image: | |
| - platform: sendspin | |
| id: sendspin_cover_art | |
| format: jpg | |
| type: rgb565 | |
| resize: 600x600 | |
| image_source: ALBUM | |
| on_image_decoded: | |
| - script.execute: debounced_refresh | |
| on_image_error: | |
| - logger.log: "Failed to decode cover art image" | |
| # Optional: Idle wallpaper from URL (uncomment and set your URL) | |
| # online_image: | |
| # - id: idle_wallpaper | |
| # url: "https://example.com/your-wallpaper.jpg" | |
| # format: jpg | |
| # type: rgb565 | |
| # resize: 600x448 | |
| # Track if we're currently playing | |
| globals: | |
| - id: is_playing | |
| type: bool | |
| restore_value: no | |
| initial_value: 'false' | |
| - id: pending_refresh | |
| type: bool | |
| restore_value: no | |
| initial_value: 'false' | |
| # Debounce script - waits before refreshing display | |
| # If called again while waiting, timer restarts | |
| script: | |
| - id: debounced_refresh | |
| mode: restart # Key: restart timer if called again | |
| then: | |
| - globals.set: | |
| id: pending_refresh | |
| value: 'true' | |
| - delay: 5s # Wait 5 seconds for all data to arrive | |
| - globals.set: | |
| id: pending_refresh | |
| value: 'false' | |
| - component.update: eink_display | |
| - logger.log: "Display refreshed after debounce" | |
| # Update clock every 15 minutes when idle | |
| interval: | |
| - interval: 15min | |
| then: | |
| - if: | |
| condition: | |
| lambda: 'return !id(is_playing);' | |
| then: | |
| - component.update: eink_display | |
| - logger.log: "Clock updated (15min interval)" | |
| # Media player - required for Sendspin connection (even without audio output) | |
| media_player: | |
| - platform: sendspin | |
| id: sendspin_media_player | |
| name: Sendspin Player | |
| on_play: | |
| # Just update state flag - album art decode will trigger the refresh | |
| - globals.set: | |
| id: is_playing | |
| value: 'true' | |
| on_pause: | |
| - globals.set: | |
| id: is_playing | |
| value: 'false' | |
| - script.execute: debounced_refresh | |
| on_idle: | |
| - globals.set: | |
| id: is_playing | |
| value: 'false' | |
| - script.execute: debounced_refresh | |
| # Metadata text sensors | |
| # Title change triggers debounce; image decode also triggers (restarts timer) | |
| # This ensures refresh happens even if no new artwork is sent | |
| text_sensor: | |
| - platform: sendspin | |
| id: track_title | |
| type: title | |
| on_value: | |
| - script.execute: debounced_refresh | |
| - platform: sendspin | |
| id: track_artist | |
| type: artist | |
| - platform: sendspin | |
| id: album_name | |
| type: album | |
| # SPI bus for e-ink display | |
| spi: | |
| clk_pin: GPIO12 | |
| mosi_pin: GPIO11 | |
| # Font for metadata display - using glyphsets for proper character coverage | |
| font: | |
| - file: "gfonts://Roboto@500" | |
| id: font_title | |
| size: 22 | |
| glyphsets: | |
| - GF_Latin_Core | |
| - GF_Latin_Plus | |
| - file: "gfonts://Roboto@400" | |
| id: font_artist | |
| size: 16 | |
| glyphsets: | |
| - GF_Latin_Core | |
| - GF_Latin_Plus | |
| - file: "gfonts://Roboto@300" | |
| id: font_idle_title | |
| size: 48 | |
| glyphsets: | |
| - GF_Latin_Core | |
| - file: "gfonts://Roboto@300" | |
| id: font_idle_subtitle | |
| size: 24 | |
| glyphsets: | |
| - GF_Latin_Core | |
| - file: "gfonts://Roboto@100" | |
| id: font_time | |
| size: 96 | |
| glyphs: " 0123456789:AMP" | |
| - file: "gfonts://Roboto@300" | |
| id: font_date | |
| size: 28 | |
| glyphsets: | |
| - GF_Latin_Core | |
| # Colors for 7-color ACeP e-ink display | |
| color: | |
| - id: my_black | |
| red: 0% | |
| green: 0% | |
| blue: 0% | |
| - id: my_white | |
| red: 100% | |
| green: 100% | |
| blue: 100% | |
| # E-ink display (replaces ILI9xxx + LVGL from S3 Box 3) | |
| display: | |
| - platform: waveshare_epaper | |
| id: eink_display | |
| model: 5.65in-f | |
| cs_pin: GPIO10 | |
| dc_pin: GPIO9 | |
| reset_pin: GPIO8 | |
| busy_pin: | |
| number: GPIO7 | |
| inverted: true | |
| rotation: 0 | |
| update_interval: never | |
| lambda: |- | |
| // Display: 600 wide x 448 tall | |
| if (!id(is_playing)) { | |
| // ============ IDLE SCREEN ============ | |
| // Clock display with date | |
| // White background | |
| it.filled_rectangle(0, 0, 600, 448, id(my_white)); | |
| // Draw a subtle border/frame | |
| it.rectangle(20, 20, 560, 408, id(my_black)); | |
| // Time - large centered clock | |
| it.strftime(300, 160, id(font_time), id(my_black), TextAlign::CENTER, "%H:%M", id(ha_time).now()); | |
| // Date - below time | |
| it.strftime(300, 260, id(font_date), id(my_black), TextAlign::CENTER, "%A, %B %d", id(ha_time).now()); | |
| // Small "Sendspin" branding at bottom | |
| it.print(300, 380, id(font_idle_subtitle), id(my_black), TextAlign::CENTER, "Sendspin"); | |
| } else { | |
| // ============ NOW PLAYING SCREEN ============ | |
| // Layout: Full-width art at top, 56px black bar with metadata at bottom | |
| // Fill entire screen with black first | |
| it.filled_rectangle(0, 0, 600, 448, id(my_black)); | |
| // Draw album art at top-left (600x600 image, only top portion visible) | |
| it.image(0, 0, id(sendspin_cover_art)); | |
| // Draw black bar for metadata area | |
| it.filled_rectangle(0, 392, 600, 56, id(my_black)); | |
| // Get metadata | |
| std::string title = id(track_title).state; | |
| std::string artist = id(track_artist).state; | |
| std::string album = id(album_name).state; | |
| if (title.empty()) { | |
| title = "Unknown Track"; | |
| } | |
| if (title.length() > 45) { | |
| title = title.substr(0, 42) + "..."; | |
| } | |
| std::string subtitle; | |
| if (!artist.empty() && !album.empty()) { | |
| subtitle = artist + " - " + album; | |
| } else if (!artist.empty()) { | |
| subtitle = artist; | |
| } else if (!album.empty()) { | |
| subtitle = album; | |
| } | |
| if (subtitle.length() > 60) { | |
| subtitle = subtitle.substr(0, 57) + "..."; | |
| } | |
| // Draw title (white text on black bar) | |
| it.print(12, 400, id(font_title), id(my_white), TextAlign::TOP_LEFT, title.c_str()); | |
| // Draw artist/album | |
| if (!subtitle.empty()) { | |
| it.print(12, 424, id(font_artist), id(my_white), TextAlign::TOP_LEFT, subtitle.c_str()); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment