I experimented with several different things, both on the Water Bug sensor board and the Water Watcher display board. I ultimately decided that the Water Bug sensor board would send a simple text message to indicate the status of the water heater flame. As I mentioned earlier, the lux values read by the TSL2591 aren't calibrated, so it's not especially meaningful to push those around for consumption. They do happen to go to my Home Assistant server, and I also send the lux values over MQTT to be displayed in the debug log output of the Water Watcher. The latter is for my own convenience so that I didn't have to monitor both devices to see what was going on during troubleshooting.
The Water Bug reads the lux value from the sensor every 30 seconds. Based on the experimental observations I mentioned earlier, it reduces that to "pilot_on", "full_flame", "pilot_off", or the special case of "dazzled". I keep track of the last time the Water Bug saw "full_flame". If it sees "pilot_off" within a short time after that, it sends "dazzled" instead of "pilot_off". The message recipient can decide what it wants to do about "dazzled" messages.
ESPhome provides a mechanism for knowing the wall clock time, either from the Home Assistant server or from an NTP server. It makes it available in a convenient object form that measures seconds since the Unix epoch. I use the wall clock time as part of the display for my various sensor boards that have a built-in OLED display. The Water Bug doesn't have a display, and it doesn't otherwise need to know what time it is. The ESP32 provides a high-precision timer for elapsed time since boot-up, and ESPhome makes that available in its "Uptime" pseudo-sensor. It's granularity is in seconds, but that's fine for this particular use, and it let me avoid the need to deal with the ESP32's timer in native code.
Once it has decided which piece of text to use for the flame state, the Water Bug sends that as a message on a particular MQTT topic. It then sends the raw lux value with the prefix "lux: " on the same MQTT topic. The only thing the Water Bug has to remember from earlier iterations of the 30 second loop is the last time it saw "full_flame".
The Water Watcher listens on that MQTT topic and reacts to the messages. It logs every message it sees to the debug log, but it only actually reacts to flame status messages that it recognizes. And, in fact, it only reacts to recognized messages if they are different from the last recognized message that it saw. The reason for that is to avoid some visually distracting repainting of the LED matrix when there was no actual status change. The reaction takes the form of some kind of display on the LED matrix. Those are animated and keep running until a different reaction is triggered.
The animations in the display and the rapid succession of the flame status and lux messages arriving over MQTT led to some kind of timing problem. I found that the listen-and-react model was not completely stable. I hypothesize that MQTT message strings were changing out from under my code that was trying to react to them. Since I don't know the internals of ESPhome's event looping all that well, I changed my code that listens to the MQTT topic so that it immediately copies the string and pushes it onto a FIFO queue as quickly as possible. The reaction code, which is configured to be single-threaded, then reads from that FIFO queue and triggers the display update when appropriate. That at least decouples the timing and gets rid of the instability.