commit bd9389e04a9a6ecd67c759d4e2d570c0be644a4d Author: MangaValk Date: Sat Oct 31 17:29:09 2026 +0100 ESPHome external component for Samsung 2x20 serial VFD Component (components/samsung_vfd), example device config, README and CLAUDE.md. Co-Authored-By: Claude Opus 5.5 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..de51117 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# ESPHome build output +.esphome/ +**/.esphome/ + +# Real credentials never go in the repo; see examples/secrets.yaml.example +secrets.yaml + +__pycache__/ +*.pyc +.vscode/ +.idea/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..6640401 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,49 @@ +# CLAUDE.md + +ESPHome external component for a Samsung 2×20 serial VFD (5 V TTL, 9600 8N1, +write-only). It is consumed from git by an ESPHome server through +`external_components: source: github://...`, so the layout must stay +`components/samsung_vfd/` at the repo root. + +## Layout + +- `components/samsung_vfd/__init__.py`: config schema, codegen and the action + registrations (`print`, `printf`, `clear`, `home`). +- `components/samsung_vfd/samsung_vfd.h`: the class, the control-byte constants + and the templated action classes. +- `components/samsung_vfd/samsung_vfd.cpp`: the shadow buffer, word wrap, + UTF-8 → single-byte sanitising and `flush_()`. +- `examples/samsung-vfd.yaml`: the full reference device config. It pulls the + component from GitHub; for local work, switch to the commented + `type: local, path: ../components` source. + +## Design rules + +- `lines_` is the screen as it should be, each line padded to exactly `cols_`. + Every change goes through `flush_()`. It rebuilds the payload, returns early + if the payload equals `last_payload_`, and otherwise sends `0B` (home) plus + the payload. Don't clear before redrawing: that causes flicker. +- Anything that changes the display out of band (clear, init) must reset + `last_payload_` so the next flush retransmits. +- Text sent to the display is one byte per character; always pass it through + `sanitize_()`. +- Keep the protocol constants in `samsung_vfd.h`. Only `ESC @`, `0C` and `0B` + are known to work. + +## Checking changes + +Hardware isn't available to Claude, so validate with ESPHome itself: + +``` +pip install esphome +cd examples +# switch external_components to the local source first +esphome config samsung-vfd.yaml # schema check +esphome compile samsung-vfd.yaml # full C++ build +``` + +`secrets.yaml` is gitignored. Copy `examples/secrets.yaml.example` to +`examples/secrets.yaml` to compile. + +Follow ESPHome C++ style: `this->` on members, a trailing `_` on protected +members, and the `esphome::samsung_vfd` namespace. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3345693 --- /dev/null +++ b/README.md @@ -0,0 +1,146 @@ +# Samsung serial VFD — ESPHome component + +An ESPHome external component for the Samsung 2-line × 20-character serial VFD +(vacuum fluorescent display) that takes 5 V TTL serial at 9600 baud, 8N1. + +It keeps a copy of the screen in RAM and only sends data when something +changes. Updates overwrite the screen in place instead of clearing it first, +so the clock doesn't flicker. + +``` +components/samsung_vfd/ the component (Python schema + C++) +examples/samsung-vfd.yaml full example: clock + two Home Assistant text entities +examples/secrets.yaml.example +``` + +## Using it from your ESPHome server + +Add this to your device YAML. ESPHome fetches the component from git when it +compiles. + +```yaml +external_components: + - source: github://REPO_OWNER/REPO_NAME@main + components: [samsung_vfd] + refresh: 1d + +uart: + id: uart_vfd + tx_pin: GPIO17 + baud_rate: 9600 + +time: # needed by the component (used by samsung_vfd.printf) + - platform: sntp + id: sntp_time + +samsung_vfd: + id: vfd + uart_id: uart_vfd +``` + +For a complete device, copy [examples/samsung-vfd.yaml](examples/samsung-vfd.yaml) +into your ESPHome config folder. Add the keys from +[examples/secrets.yaml.example](examples/secrets.yaml.example) to your +`secrets.yaml`. + +## Wiring + +The component only sends to the display. The ESP32's 3.3 V output is enough for +most 5 V TTL inputs. ESP32 GPIOs are **not** 5 V tolerant, so never connect the +display's TX back to the ESP32 without a level shifter or divider. + +| Display | ESP32 | +|---------|-------| +| RX | GPIO17 (UART TX) | +| GND | GND | +| 5 V | external 5 V supply | + +The display draws more current than a dev-board regulator can comfortably +supply. Power it from its own supply and connect the grounds together. + +To use GPIO1 (TX0, shared with USB logging), set `tx_pin: GPIO1` and +`logger: baud_rate: 0` so log output doesn't end up on the display. If the +display stays blank at 3.3 V levels, add a 74HCT125 or similar level shifter. + +## Configuration + +```yaml +samsung_vfd: + id: vfd + uart_id: uart_vfd + cols: 20 # default 20 (1–80) + rows: 2 # default 2 (1–8) + send_newlines: false # default false; send CR LF between lines +``` + +The UART must be 9600 baud with a `tx_pin`. The component requires a `time:` +component in the config. + +## Actions + +| Action | Options | +|--------|---------| +| `samsung_vfd.print` | `text` (templatable). Optional `line:` (0-based) replaces one line. Without `line:`, the text is word-wrapped over the whole screen; `\n` forces a new line. Optional `centered: true`. | +| `samsung_vfd.printf` | Shows the current time formatted with strftime, e.g. `format: "%H:%M:%S"`. Requires `time_id:`. Takes the same `line:` and `centered:` options. Does nothing until the clock has synced. | +| `samsung_vfd.clear` | Clears the screen. | +| `samsung_vfd.home` | Moves the cursor home. | + +```yaml +- samsung_vfd.print: + id: vfd + line: 0 + text: !lambda 'return "Temp " + to_string((int) id(temp).state) + " C";' +- samsung_vfd.printf: + id: vfd + time_id: sntp_time + line: 1 + centered: true + format: "%a %d %b %H:%M" +``` + +From a lambda you can call `id(vfd).print(...)`, `print_line(line, text, centered)`, +`clear()`, `home()` and `line_text(line)`. + +Text is converted to one byte per character. Accented Latin-1 letters lose +their accent (é → e), other non-ASCII characters become `?`, and control +characters become spaces. + +## What the example config does + +- On boot: clears the screen, shows `Hello World!` for 3 s, then restores line 1. +- Bottom line: a live clock, `%a %d %b %H:%M:%S`, updated every second (SNTP, + Europe/Amsterdam). +- Two Home Assistant `text` entities: + - **VFD line 1** (max 20 chars) is shown on the top line and kept across + reboots. + - **VFD status** (max 40 chars) shows a full-screen, word-wrapped, centred + banner for 10 s. The clock pauses meanwhile, then line 1 and the clock come + back. +- Wi-Fi with a captive-portal fallback AP, OTA updates and an encrypted native + API. + +## Protocol + +| Bytes | Effect | +|-------|--------| +| `1B 40` (`ESC @`) | init / reset | +| `0C` | clear screen | +| `0B` | cursor home | +| text | printed at the cursor; wraps to the next line by itself | + +Each update sends `0B` (home) followed by both lines, each padded to exactly +`cols` characters. + +## Troubleshooting + +- **Line 2 lands on the wrong row, or text runs together:** try + `send_newlines: true`. +- **The screen scrolls up a line after each update:** the display scrolls as + soon as the last cell is written. Use `cols: 19` together with + `send_newlines: true`. +- **A stray character appears top-left:** the display doesn't treat `0B` as + home. Change `CMD_HOME` in + [samsung_vfd.h](components/samsung_vfd/samsung_vfd.h). +- **Garbled characters:** check that `uart:` is at 9600 baud and that the + display takes 5 V TTL levels, not RS-232 levels. A real RS-232 input needs a + MAX232 inverter. diff --git a/components/samsung_vfd/__init__.py b/components/samsung_vfd/__init__.py new file mode 100644 index 0000000..5304ae7 --- /dev/null +++ b/components/samsung_vfd/__init__.py @@ -0,0 +1,124 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.components import time as time_, uart +from esphome.const import CONF_ID, CONF_TEXT, CONF_TIME_ID + +DEPENDENCIES = ["uart", "time"] + +CONF_COLS = "cols" +CONF_ROWS = "rows" +CONF_LINE = "line" +CONF_FORMAT = "format" +CONF_CENTERED = "centered" +CONF_SEND_NEWLINES = "send_newlines" + +samsung_vfd_ns = cg.esphome_ns.namespace("samsung_vfd") + +SamsungVfd = samsung_vfd_ns.class_("SamsungVfd", cg.Component, uart.UARTDevice) + +PrintAction = samsung_vfd_ns.class_("PrintAction", automation.Action) +PrintfAction = samsung_vfd_ns.class_("PrintfAction", automation.Action) +ClearAction = samsung_vfd_ns.class_("ClearAction", automation.Action) +HomeAction = samsung_vfd_ns.class_("HomeAction", automation.Action) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SamsungVfd), + cv.Optional(CONF_COLS, default=20): cv.int_range(min=1, max=80), + cv.Optional(CONF_ROWS, default=2): cv.int_range(min=1, max=8), + # Off by default: lines are padded to exactly `cols` characters and the + # display wraps to the next line by itself. + cv.Optional(CONF_SEND_NEWLINES, default=False): cv.boolean, + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(uart.UART_DEVICE_SCHEMA) +) + +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "samsung_vfd", baud_rate=9600, require_tx=True +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) + cg.add(var.set_size(config[CONF_COLS], config[CONF_ROWS])) + cg.add(var.set_send_newlines(config[CONF_SEND_NEWLINES])) + + +@automation.register_action( + "samsung_vfd.print", + PrintAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(SamsungVfd), + cv.Required(CONF_TEXT): cv.templatable(cv.string), + cv.Optional(CONF_LINE): cv.templatable(cv.int_range(min=0, max=7)), + cv.Optional(CONF_CENTERED): cv.templatable(cv.boolean), + } + ), + synchronous=True, +) +async def samsung_vfd_print_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + cg.add(var.set_text(await cg.templatable(config[CONF_TEXT], args, cg.std_string))) + if CONF_LINE in config: + cg.add(var.set_line(await cg.templatable(config[CONF_LINE], args, cg.int_))) + if CONF_CENTERED in config: + cg.add(var.set_centered(await cg.templatable(config[CONF_CENTERED], args, cg.bool_))) + return var + + +@automation.register_action( + "samsung_vfd.printf", + PrintfAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(SamsungVfd), + cv.Required(CONF_TIME_ID): cv.use_id(time_.RealTimeClock), + cv.Required(CONF_FORMAT): cv.templatable(cv.string), + cv.Optional(CONF_LINE): cv.templatable(cv.int_range(min=0, max=7)), + cv.Optional(CONF_CENTERED): cv.templatable(cv.boolean), + } + ), + synchronous=True, +) +async def samsung_vfd_printf_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + cg.add(var.set_time(await cg.get_variable(config[CONF_TIME_ID]))) + cg.add(var.set_format(await cg.templatable(config[CONF_FORMAT], args, cg.std_string))) + if CONF_LINE in config: + cg.add(var.set_line(await cg.templatable(config[CONF_LINE], args, cg.int_))) + if CONF_CENTERED in config: + cg.add(var.set_centered(await cg.templatable(config[CONF_CENTERED], args, cg.bool_))) + return var + + +@automation.register_action( + "samsung_vfd.clear", + ClearAction, + cv.Schema({cv.Required(CONF_ID): cv.use_id(SamsungVfd)}), + synchronous=True, +) +async def samsung_vfd_clear_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +@automation.register_action( + "samsung_vfd.home", + HomeAction, + cv.Schema({cv.Required(CONF_ID): cv.use_id(SamsungVfd)}), + synchronous=True, +) +async def samsung_vfd_home_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var diff --git a/components/samsung_vfd/samsung_vfd.cpp b/components/samsung_vfd/samsung_vfd.cpp new file mode 100644 index 0000000..a7b4558 --- /dev/null +++ b/components/samsung_vfd/samsung_vfd.cpp @@ -0,0 +1,158 @@ +#include "samsung_vfd.h" + +#include "esphome/core/log.h" + +#include + +namespace esphome { +namespace samsung_vfd { + +// Unaccented stand-ins for U+00C0..U+00FF (À..ÿ), one per code point. +static const char LATIN1_FOLD[] = "AAAAAAACEEEEIIIIDNOOOOOxOUUUUYPs" + "aaaaaaaceeeeiiiidnooooo/ouuuuypy"; + +void SamsungVfd::setup() { + this->resize_lines_(); + this->init_display_(); +} + +void SamsungVfd::dump_config() { + ESP_LOGCONFIG(TAG, "Samsung VFD:"); + ESP_LOGCONFIG(TAG, " Size: %u cols x %u rows", this->cols_, this->rows_); + ESP_LOGCONFIG(TAG, " Newline between lines: %s", YESNO(this->send_newlines_)); +} + +void SamsungVfd::resize_lines_() { + this->lines_.assign(this->rows_, std::string(this->cols_, ' ')); +} + +void SamsungVfd::init_display_() { + // ESC @ (reset), then clear. + this->write_byte(CMD_ESC); + this->write_byte(CMD_INIT); + this->write_byte(CMD_CLEAR); + // Force the next flush to retransmit whatever is in the shadow buffer. + this->last_payload_.clear(); +} + +void SamsungVfd::write_str_(const std::string &s) { + if (!s.empty()) + this->write_array(reinterpret_cast(s.data()), s.size()); +} + +std::string SamsungVfd::sanitize_(const std::string &text, bool keep_newlines) { + std::string out; + out.reserve(text.size()); + for (size_t i = 0; i < text.size(); i++) { + auto c = static_cast(text[i]); + if (c == '\n') { + out.push_back(keep_newlines ? '\n' : ' '); + } else if (c < 0x20 || c == 0x7F) { + out.push_back(' '); + } else if (c < 0x80) { + out.push_back(static_cast(c)); + } else if (c >= 0xC0) { + // Start of a multi-byte UTF-8 sequence: decode 2-byte Latin-1 letters, + // replace everything else with a single '?'. + size_t len = c >= 0xF0 ? 4 : c >= 0xE0 ? 3 : 2; + char repl = '?'; + if (len == 2 && i + 1 < text.size()) { + uint32_t cp = ((c & 0x1F) << 6) | (static_cast(text[i + 1]) & 0x3F); + if (cp >= 0xC0 && cp <= 0xFF) + repl = LATIN1_FOLD[cp - 0xC0]; + else if (cp == 0xA0) + repl = ' '; + } + out.push_back(repl); + i += len - 1; + } + // Stray continuation bytes (0x80..0xBF) are dropped. + } + return out; +} + +std::string SamsungVfd::fit_(const std::string &text, bool centered) const { + std::string line = text.substr(0, this->cols_); + if (centered && line.size() < this->cols_) + line.insert(0, (this->cols_ - line.size()) / 2, ' '); + line.resize(this->cols_, ' '); + return line; +} + +void SamsungVfd::print(const std::string &raw, bool centered) { + this->resize_lines_(); + const std::string text = sanitize_(raw, true); + + // Lay the text out over the lines: split on explicit newlines, then wrap each + // part into line-width chunks, breaking at the last space where possible. + uint8_t row = 0; + size_t i = 0; + while (row < this->rows_) { + size_t nl = text.find('\n', i); + size_t end = (nl == std::string::npos) ? text.size() : nl; + + // A part always occupies at least one line, so "A\nB" puts B on line 2. + do { + size_t n = std::min(this->cols_, end - i); + size_t next = i + n; + if (next < end && text[next] != ' ') { + size_t sp = text.rfind(' ', next); + if (sp != std::string::npos && sp > i) { + n = sp - i; + next = sp; + } + } + this->lines_[row++] = this->fit_(text.substr(i, n), centered); + i = next; + while (i < end && text[i] == ' ') + i++; // a wrapped line never starts with a space + } while (i < end && row < this->rows_); + + if (nl == std::string::npos) + break; + i = nl + 1; + } + + this->flush_(); +} + +void SamsungVfd::print_line(uint8_t line, const std::string &text, bool centered) { + if (line >= this->rows_) { + ESP_LOGW(TAG, "print_line: line %u out of range (has %u lines)", line, this->rows_); + return; + } + if (this->lines_.size() < this->rows_) + this->resize_lines_(); + + this->lines_[line] = this->fit_(sanitize_(text, false), centered); + this->flush_(); +} + +void SamsungVfd::clear() { + this->resize_lines_(); + this->last_payload_.clear(); + this->write_byte(CMD_CLEAR); +} + +void SamsungVfd::home() { this->write_byte(CMD_HOME); } + +void SamsungVfd::flush_() { + std::string payload; + for (uint8_t r = 0; r < this->rows_; r++) { + if (r != 0 && this->send_newlines_) + payload += "\r\n"; + payload += this->lines_[r]; + } + + if (payload == this->last_payload_) + return; // nothing changed -> no re-send + + // No clear: every line is padded to the full width, so going home and + // overwriting replaces all characters without the blank-screen flicker. + this->write_byte(CMD_HOME); + this->write_str_(payload); + this->last_payload_ = payload; +} + +} // namespace samsung_vfd +} // namespace esphome diff --git a/components/samsung_vfd/samsung_vfd.h b/components/samsung_vfd/samsung_vfd.h new file mode 100644 index 0000000..3107efc --- /dev/null +++ b/components/samsung_vfd/samsung_vfd.h @@ -0,0 +1,127 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/core/helpers.h" +#include "esphome/components/time/real_time_clock.h" +#include "esphome/components/uart/uart.h" + +#include +#include + +namespace esphome { +namespace samsung_vfd { + +static const char *const TAG = "samsung_vfd"; + +// Control bytes, taken from older Arduino sketches that drove this display. +// The one known to work only ever used ESC @ and 0x0C; 0x0B ("home") came +// from an earlier test sketch. That sketch also sent 0xB0, which is left out: +// it is most likely a printable glyph, not a command. +static const uint8_t CMD_ESC = 0x1B; +static const uint8_t CMD_INIT = 0x40; // ESC @ -> reset / init display +static const uint8_t CMD_CLEAR = 0x0C; // FF -> clear screen (cursor to home) +static const uint8_t CMD_HOME = 0x0B; // VT -> cursor home + +class SamsungVfd : public Component, public uart::UARTDevice { + public: + void setup() override; + void dump_config() override; + // Before the default (DATA) priority, so restored entity values can be shown. + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + void set_size(uint8_t cols, uint8_t rows) { + this->cols_ = cols == 0 ? 20 : cols; + this->rows_ = rows == 0 ? 2 : rows; + } + void set_send_newlines(bool v) { send_newlines_ = v; } + + uint8_t cols() const { return cols_; } + uint8_t rows() const { return rows_; } + + /// Replace the whole screen with `text`, wrapping it over the available lines. + void print(const std::string &text, bool centered = false); + /// Replace a single line (0-based). Text is truncated/padded to the column count. + void print_line(uint8_t line, const std::string &text, bool centered = false); + void clear(); + void home(); + + std::string line_text(uint8_t line) const { + return line < lines_.size() ? lines_[line] : std::string(); + } + + protected: + void init_display_(); + void resize_lines_(); + /// Fit `text` (already single-byte) into one line: truncate or pad, optionally centred. + std::string fit_(const std::string &text, bool centered) const; + /// Rebuild the wire payload from the shadow buffer and send it only if it changed. + void flush_(); + void write_str_(const std::string &s); + + /// Map UTF-8 to one byte per character: accented Latin-1 letters lose their + /// accent, control characters become spaces, anything else becomes '?'. + static std::string sanitize_(const std::string &text, bool keep_newlines); + + uint8_t cols_{20}; + uint8_t rows_{2}; + bool send_newlines_{false}; + std::vector lines_; + std::string last_payload_; +}; + +// ---------- actions ---------- + +template class PrintAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(std::string, text) + TEMPLATABLE_VALUE(int, line) + TEMPLATABLE_VALUE(bool, centered) + + void play(const Ts &...x) override { + std::string text = this->text_.value(x...); + bool centered = this->centered_.has_value() && this->centered_.value(x...); + if (this->line_.has_value()) + this->parent_->print_line(static_cast(this->line_.value(x...)), text, centered); + else + this->parent_->print(text, centered); + } +}; + +/// Like print, but formats the current time with strftime (e.g. "%H:%M:%S"). +template class PrintfAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(std::string, format) + TEMPLATABLE_VALUE(int, line) + TEMPLATABLE_VALUE(bool, centered) + + void set_time(time::RealTimeClock *rtc) { this->rtc_ = rtc; } + + void play(const Ts &...x) override { + auto now = this->rtc_->now(); + if (!now.is_valid()) + return; // clock not synchronised yet, leave the screen as it is + std::string text = now.strftime(this->format_.value(x...)); + bool centered = this->centered_.has_value() && this->centered_.value(x...); + if (this->line_.has_value()) + this->parent_->print_line(static_cast(this->line_.value(x...)), text, centered); + else + this->parent_->print(text, centered); + } + + protected: + time::RealTimeClock *rtc_{nullptr}; +}; + +template class ClearAction : public Action, public Parented { + public: + void play(const Ts &...x) override { this->parent_->clear(); } +}; + +template class HomeAction : public Action, public Parented { + public: + void play(const Ts &...x) override { this->parent_->home(); } +}; + +} // namespace samsung_vfd +} // namespace esphome diff --git a/examples/samsung-vfd.yaml b/examples/samsung-vfd.yaml new file mode 100644 index 0000000..f137741 --- /dev/null +++ b/examples/samsung-vfd.yaml @@ -0,0 +1,173 @@ +# Example ESPHome config for a Samsung 2x20 serial VFD display. +# +# Copy this file into your ESPHome config folder (and add the keys below to +# your secrets.yaml), or use it as a reference for your own device config. +# +# Wiring (display is 5V TTL serial, 9600 8N1): +# display RX <- ESP32 GPIO17 (TX) +# display GND <-> ESP32 GND +# display 5V <- 5V supply (the display needs more current than the ESP32 regulator can give) +# +# To drive the display from GPIO1 (TX0, the USB serial pin) instead, set +# `tx_pin: GPIO1` below and `baud_rate: 0` under `logger:` so log output does +# not end up on the display. + +esphome: + name: samsung-vfd + friendly_name: Samsung VFD + on_boot: + priority: -100 + then: + - samsung_vfd.clear: + id: vfd + - samsung_vfd.print: + id: vfd + text: "Hello World!" + centered: true + - delay: 3s + # Bring back the last text Home Assistant set for the top line. + - samsung_vfd.print: + id: vfd + line: 0 + text: !lambda "return id(vfd_line1).state;" + +external_components: + - source: github://REPO_OWNER/REPO_NAME@main + components: [samsung_vfd] + refresh: 1d + # While developing the component, use the checkout instead: + # - source: + # type: local + # path: ../components + +esp32: + board: esp32dev + framework: + type: arduino + +logger: + level: INFO + +api: + encryption: + key: !secret api_key + +ota: + - platform: esphome + password: !secret ota_password + +wifi: + ssid: !secret wifi_ssid + password: !secret wifi_password + # Uncomment to pin a fixed address. + # manual_ip: + # static_ip: 10.1.1.60 + # gateway: 10.1.1.1 + # subnet: 255.255.255.0 + ap: + ssid: "Samsung VFD Fallback" + password: !secret fallback_password + +captive_portal: + +# SNTP so samsung_vfd.printf can show the local time. +time: + - platform: sntp + id: sntp_time + timezone: Europe/Amsterdam + servers: + - 0.pool.ntp.org + - 1.pool.ntp.org + +uart: + id: uart_vfd + tx_pin: GPIO17 + baud_rate: 9600 + # The display is output-only for us; no rx_pin needed. + +samsung_vfd: + id: vfd + uart_id: uart_vfd + cols: 20 + rows: 2 + # Lines are padded to 20 chars and the display wraps by itself. Set to true + # only if line 2 ends up on the wrong row (sends CR LF between the lines). + send_newlines: false + +# Bottom line shows the SNTP clock every second, except while a status banner is up. +interval: + - interval: 1s + then: + - if: + condition: + not: + script.is_running: show_status + then: + - samsung_vfd.printf: + id: vfd + time_id: sntp_time + line: 1 + format: "%a %d %b %H:%M:%S" + +# Full-screen centred banner for a while, then back to line 1 + clock. +script: + - id: show_status + mode: restart + parameters: + msg: string + then: + - samsung_vfd.print: + id: vfd + text: !lambda "return msg;" + centered: true + - delay: 10s + - samsung_vfd.print: + id: vfd + line: 0 + text: !lambda "return id(vfd_line1).state;" + - samsung_vfd.printf: + id: vfd + time_id: sntp_time + line: 1 + format: "%a %d %b %H:%M:%S" + +# Two Home Assistant text entities you can type into from HA. +text: + - platform: template + id: vfd_line1 + name: "VFD line 1" + icon: mdi:text-box-outline + mode: text + max_length: 20 + optimistic: true + restore_value: true + initial_value: "" + on_value: + then: + - if: + condition: + not: + script.is_running: show_status + then: + - samsung_vfd.print: + id: vfd + line: 0 + text: !lambda "return x;" + + - platform: template + id: vfd_status + name: "VFD status" + icon: mdi:information-outline + mode: text + max_length: 40 + optimistic: true + initial_value: "" + on_value: + then: + - if: + condition: + lambda: "return !x.empty();" + then: + - script.execute: + id: show_status + msg: !lambda "return x;" diff --git a/examples/secrets.yaml.example b/examples/secrets.yaml.example new file mode 100644 index 0000000..f866d48 --- /dev/null +++ b/examples/secrets.yaml.example @@ -0,0 +1,7 @@ +# Copy to secrets.yaml (next to your device yaml) and fill in. +wifi_ssid: "your-ssid" +wifi_password: "your-wifi-password" +ota_password: "choose-an-ota-password" +fallback_password: "choose-a-fallback-ap-password" +# Generate with: python -c "import secrets,base64;print(base64.b64encode(secrets.token_bytes(32)).decode())" +api_key: "base64-32-byte-key"