Hayward AquaLogic / ProLogic automation

Well, after a lot of review and a conversation with China, V 1.5 does support scripts. Their just isn’t an easy way to verify they are there. After a wipe and a clean setup, all is working fine. Also have IOS app running as well but likely not need since HA does everything. Thanks for the response.

I implemented my first RS-485 integration today using the Waveshare ESP32-S3-RS485-CAN to integrate with my Hayward Pro Logic (V2.85 firmware). I decided to use the arjeousski/esphome_aqualogic package as it offers me configuration flexibility over the built-in aqualogic package.

I wanted to share my experience in case it helps somebody in the future, and take this opportunity to thank the folks who are responsible for making it so easy for us to monitor our pool controllers through Home Assistant.

I changed the default esphome_aqualogic waterfall nomenclature to solar (my valve 3 is for pool solar), and commented out the spa and heater sensors because I don’t use them. I changed the GPIO pins to match the Waveshare configuration, and set the baud rate for Hayward.

When I compiled the code, I received an error regarding Arduino.h and realized that @AndreiR wasn’t running on the esp-idf framework. I changed the framework setting to arduino, then after I recompiled I received an error for ArduinoJson. A quick bit of Googling led me to having to explicitly import the ArduinoJson package. This gave me a successful build. There were a handful of compilation warnings from the esphome_aqualogic code, but none of them fatal.

I did an initial test by wiring the J2 BLK and YEL pins of the Hayward circuit board to the the A+ and B- pins on the Waveshare, and using a USB-C power supply, and it worked first time. Turn off the breakers before you do the wiring, then power up when you’ve checked your wiring.

I then disconnected the USB-C power, wired the RED and GRN lines from the Hayward board to the VCC and GND pins on the Waveshare so that the device is powered directly from the pool controller. I put the Waveshare device in a waterproof case, and I’m up and running. Again, remember to turn off the breakers before you do this step.

My only issue is that the menu button doesn’t appear to be working, but I rarely change my system configuration, so it’s not a huge issue. Perhaps there’s something I’m missing in the dashboard configuration. If @AndreiR could possibly comment, I’d appreciate it.

I then added a Left Reimann integral helper to track cumulative energy use from the pump within the energy dashboard.

For those interested in my configuration, the key parts are as follows; the rest of the configuration (minus my personal tweaks) is directly from the esphome_aqualogic package:

esphome:
  ...
  # Inject the missing library dependency directly into the component
  libraries:
    - bblanchon/ArduinoJson

# Settings for this Waveshare device
esp32:
  board: esp32-s3-devkitc-1
  variant: esp32s3
  framework:
    type: arduino

# Hardware configuration optimized for the Waveshare ESP32-S3-RS485-CAN
# and baud rate settings for Hayward Pro Logic
uart:
  - id: prologic_uart
    tx_pin: GPIO17
    rx_pin: GPIO18
    flow_control_pin: GPIO21
    baud_rate: 19200
    data_bits: 8
    parity: NONE
    stop_bits: 2

aqualogic:
  uart_id: prologic_uart

One more thing. When @AndreiR mentions the ‘Available States’ in his README.md, it is not clear what he means by ‘Internal State’, nor provides an example of how to expose the state. For example, I use Aux 1 to enable super chlorination. I was able to expose the button, but not the state.

# Emulate buttons/controls
button:
  - platform: template
    name: "Aux 1"
    on_press:
      - aqualogic.send:
          key: AUX_1

After wracking my brain, I used a helper template binary_sensor linked to the flags entity, and associated it with the pool controller device. The flags field is a string that contains all of the currently enabled states, so all I do is look for the presence of the state I’m looking for:

Aux 1 Helper: {{ 'AUX_1' in states('sensor.hayward_pro_logic_flags') }}
Aux 2 Helper: {{ 'AUX_2' in states('sensor.hayward_pro_logic_flags') }}
Super Chlorinate Helper: {{ 'SUPER_CHLORINATE' in states('sensor.hayward_pro_logic_flags') }}

I now have full control and visibility to my pool state.

@b3nj1 I decided to try switching from the @AndreiR implementation to yours for comparison, and to see if the menu button works.

My system: Goldline Aqua Plus; Main SW 2.85; Display 3.11; Filter Bridge r2.15; VSC r2.00; Motherboard: GLX-PCB-MAIN

Interface is ESP32-S3-RS485-CAN powered by pins 1 & 4.

Findings:
Menu button does not react, similar to @AndreiR implementation.
Left and right buttons work causing display to move to next item
Display partially works: Filter Speed; date/time
See attached screenshot for display issues: Air temp, Pool temp, etc. are being truncated, and therefore the sensors for pool, and air temperature are not functional. The values are actually lower case, but HA is upper-casing the first letter automatically.

It looks like bus.yaml:176 is not always working - at least for this firmware revision. The first character is lost, causing the downstream sensor to fail. There has to be another piece of logic missing with the assumption:

// Text starts immediately at payload[2] -- no header byte before it
// (decode_variations.md §5.1: "no fixed header before the text")."

Looking at the @swilson implementation of the text sensor, and @AndreiR, they are taking the entire text payload after the frame type and trimming it, versus making assumptions about the start byte of the text. Perhaps you’re on a more modern version of the firmware and Hayward have changed their methodology?

Let me know if there’s anything else I can help you with.

@mrmoorey ,
Thanks for giving mine a try. I’m glad I didn’t break the main version (I have a side feature that tries to count if commands fail to land that I accidentally checked part of to the main branch).

Anyway, if you’re game, we can try to debug. My package has a packet sniffer. We should be able to see what your main controller and/or OEM wireless controller sends out for MENU. We should also be able to see what your display payloads look like.

  1. Add this to your ESPHome configuration for your device. Just add it to the end. It will enable a periodic summary (30 seconds interval set by the yaml below) of the messages seen on your bus.
rs485_frame:
 - id: !extend pool
   dump_frames: false   
   sniffer_stats: { interval: 30s, max_frame_types: 64, payload_dump_top: 20 }
  1. Install it, and keep the logs running in the ESPHome console. But turn off the “States” because they can be noisy.
  2. Press your main controller and/or OEM wireless controller’s MENU button. You can press it a few times.

Here’s what I get in my log so you know what we’re looking for. Here I show 1. a period where I pressed nothing. 2. a period where I pressed MENU on my wireless controller 7 times. I annotated the output with ### comment ###.

If you try this, please share your output. We can add some details to the configuration examples with what we learn from your system.

###### IDLE Period output ####
[11:55:59.206][I][rs485_frame.stats:232]: RS485 sniffer stats over 30000 ms (sorted by count):
[11:55:59.211][I][rs485_frame.stats:237]:   type  cnt    d-ref(min/med/max)    d-same(min/med/max)   payloads
[11:55:59.211][I][rs485_frame.stats:272]:   0101   300        -     -     -       95   100   117   1 unique
[11:55:59.211][I][rs485_frame.stats:272]:   0103    20       73    83    92       90  1992  2016   5 unique
[11:55:59.211][I][rs485_frame.stats:272]:   0C01    15       28    32    33     1997  2000  2013   1 unique
[11:55:59.215][I][rs485_frame.stats:272]:   000C    15       37    37    38     1997  2000  2017   2 unique
[11:55:59.219][I][rs485_frame.stats:272]:   0102    15       53    53    53     1999  2000  2017   1 unique
[11:55:59.219][I][rs485_frame.stats:272]:   0004     3       34    34    36       98   102   102   3 unique
[11:55:59.219][I][rs485_frame.stats:272]:   0407     1       32    32    32        -     -     -   1 unique
[11:55:59.222][I][rs485_frame.stats:272]:   04A0     1       32    32    32        -     -     -   1 unique
[11:55:59.222][I][rs485_frame.stats:272]:   04AE     1       32    32    32        -     -     -   1 unique
[11:55:59.222][I][rs485_frame.stats:307]:   0101 payloads:
[11:55:59.235][I][rs485_frame.stats:318]:       300 @ 01 01  |..|
[11:55:59.236][I][rs485_frame.stats:307]:   0103 payloads:
[11:55:59.236][I][rs485_frame.stats:318]:         4 @ 01 03 20 20 20 20 46 69 6C 74 65 72 20 53 70 65 65 64 20 20 20 20 20 20 20 33 35 25 20 53 70 65  |..    Filter Speed       35% Spe|
[11:55:59.236][I][rs485_frame.stats:318]:         4 @ 01 03 20 20 20 20 20 20 53 61 74 75 72 64 61 79 20 20 20 20 20 20 20 20 20 20 20 20 20 31 31 BA  |..      Saturday             11.|
[11:55:59.236][I][rs485_frame.stats:318]:         4 @ 01 03 20 20 50 6F 6F 6C 20 54 65 6D 70 20 20 37 39 5F 46 20 20 20 20 20 20 20 20 20 20 20 20 20  |..  Pool Temp  79_F             |
[11:55:59.236][I][rs485_frame.stats:318]:         4 @ 01 03 20 20 41 69 72 20 54 65 6D 70 20 20 20 38 31 5F 46 20 20 20 20 20 20 20 20 20 20 20 20 20  |..  Air Temp   81_F             |
[11:55:59.236][I][rs485_frame.stats:318]:         4 @ 01 03 20 20 20 20 20 20 48 65 61 74 65 72 31 20 20 20 20 20 20 20 20 20 20 20 20 4D 61 6E 75 61  |..      Heater1            Manua|
[11:55:59.236][I][rs485_frame.stats:307]:   0C01 payloads:
[11:55:59.236][I][rs485_frame.stats:318]:        15 @ 0C 01 00 23  |...#|
[11:55:59.236][I][rs485_frame.stats:307]:   000C payloads:
[11:55:59.236][I][rs485_frame.stats:318]:         3 @ 00 0C 00 00 23 01 20  |....#. |
[11:55:59.236][I][rs485_frame.stats:318]:        12 @ 00 0C 00 00 23 01 14  |....#..|
[11:55:59.236][I][rs485_frame.stats:307]:   0102 payloads:
[11:55:59.236][I][rs485_frame.stats:318]:        15 @ 01 02 28 00 00 00 00 00 00 00  |..(.......|
[11:55:59.237][I][rs485_frame.stats:307]:   0004 payloads:
[11:55:59.237][I][rs485_frame.stats:318]:         1 @ 00 04 02  |...|
[11:55:59.237][I][rs485_frame.stats:318]:         1 @ 00 04 30 33 30 30 20  |..0300 |
[11:55:59.238][I][rs485_frame.stats:318]:         1 @ 00 04 CC 6D  |...m|
[11:55:59.238][I][rs485_frame.stats:307]:   0407 payloads:
[11:55:59.238][I][rs485_frame.stats:318]:         1 @ 04 07  |..|
[11:55:59.238][I][rs485_frame.stats:307]:   04A0 payloads:
[11:55:59.242][I][rs485_frame.stats:318]:         1 @ 04 A0  |..|
[11:55:59.245][I][rs485_frame.stats:307]:   04AE payloads:
[11:55:59.250][I][rs485_frame.stats:318]:         1 @ 04 AE  |..|

##### MENU 7 times output #####
[11:51:37.247][I][rs485_frame.stats:232]: RS485 sniffer stats over 30014 ms (sorted by count):
[11:51:37.248][I][rs485_frame.stats:237]:   type  cnt    d-ref(min/med/max)    d-same(min/med/max)   payloads
[11:51:37.248][I][rs485_frame.stats:272]:   0101   300        -     -     -       67   100   134   1 unique
[11:51:37.253][I][rs485_frame.stats:272]:   0103    22      104   119   120      314  2000  2001   5 unique
[11:51:37.253][I][rs485_frame.stats:272]:   040A    22       75    82    82      306  2000  2001   9 unique
[11:51:37.253][I][rs485_frame.stats:272]:   0C01    15       29    32    33     1984  2000  2017   1 unique
[11:51:37.253][I][rs485_frame.stats:272]:   000C    15       37    37    39     1983  2000  2018   2 unique
[11:51:37.256][I][rs485_frame.stats:272]:   0102    15       90    90    91     1999  2000  2001   1 unique
[11:51:37.259][I][rs485_frame.stats:272]:   0083     7        6     7     7      800  1016 16098   1 unique
[11:51:37.262][I][rs485_frame.stats:272]:   0004     3       34    34    36       69    98    98   3 unique
[11:51:37.264][I][rs485_frame.stats:272]:   0407     1       32    32    32        -     -     -   1 unique
[11:51:37.267][I][rs485_frame.stats:272]:   04A0     1       32    32    32        -     -     -   1 unique
[11:51:37.270][I][rs485_frame.stats:272]:   04AE     1       32    32    32        -     -     -   1 unique
[11:51:37.273][I][rs485_frame.stats:307]:   0101 payloads:
[11:51:37.285][I][rs485_frame.stats:318]:       300 @ 01 01  |..|
#### Here we can see the detailed HEX and the associated ASCII text for the display payloads ####
[11:51:37.285][I][rs485_frame.stats:307]:   0103 payloads:
[11:51:37.285][I][rs485_frame.stats:318]:         4 @ 01 03 20 20 20 20 20 44 69 61 67 6E 6F 73 74 69 63 20 20 20 20 20 20 20 20 20 20 20 20 20 4D 65  |..     Diagnostic             Me|
[11:51:37.285][I][rs485_frame.stats:318]:         6 @ 01 03 20 20 20 43 6F 6E 66 69 67 75 72 61 74 69 6F 6E 20 20 20 20 20 20 20 4D 65 6E 75 2D 4C 6F  |..   Configuration       Menu-Lo|
[11:51:37.285][I][rs485_frame.stats:318]:         2 @ 01 03 20 20 20 20 20 20 44 65 66 61 75 6C 74 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 4D 65  |..      Default               Me|
[11:51:37.286][I][rs485_frame.stats:318]:         9 @ 01 03 20 20 20 20 20 20 53 65 74 74 69 6E 67 73 20 20 20 20 20 20 20 20 20 20 20 20 20 20 4D 65  |..      Settings              Me|
[11:51:37.286][I][rs485_frame.stats:318]:         1 @ 01 03 20 20 20 20 20 20 20 54 69 6D 65 72 73 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 4D 65  |..       Timers               Me|
[11:51:37.286][I][rs485_frame.stats:307]:   040A payloads:
[11:51:37.286][I][rs485_frame.stats:318]:         2 @ 04 0A 83 00 03 20 20 20 20 20 44 69 61 67 6E 6F 73 74 69 63 20 20 20 20 20 20 20 20 20 20 20 20  |.....     Diagnostic            |
[11:51:37.287][I][rs485_frame.stats:318]:         2 @ 04 0A 83 00 02 28 00 00 00 00 00 00 00 03 20 20 20 20 20 44 69 61 67 6E 6F 73 74 69 63 20 20 20  |.....(........     Diagnostic   |
[11:51:37.287][I][rs485_frame.stats:318]:         2 @ 04 0A 83 00 03 20 20 20 43 6F 6E 66 69 67 75 72 61 74 69 6F 6E 20 20 20 20 20 20 20 4D 65 6E 75  |.....   Configuration       Menu|
[11:51:37.287][I][rs485_frame.stats:318]:         1 @ 04 0A 83 00 03 20 20 20 20 20 20 44 65 66 61 75 6C 74 20 20 20 20 20 20 20 20 20 20 20 20 20 20  |.....      Default              |
[11:51:37.287][I][rs485_frame.stats:318]:         1 @ 04 0A 83 00 02 28 00 00 00 00 00 00 00 03 20 20 20 20 20 20 44 65 66 61 75 6C 74 20 20 20 20 20  |.....(........      Default     |
[11:51:37.293][I][rs485_frame.stats:318]:         1 @ 04 0A 83 00 03 20 20 20 20 20 20 53 65 74 74 69 6E 67 73 20 20 20 20 20 20 20 20 20 20 20 20 20  |.....      Settings             |
[11:51:37.293][I][rs485_frame.stats:318]:         8 @ 04 0A 83 00 02 28 00 00 00 00 00 00 00 03 20 20 20 20 20 20 53 65 74 74 69 6E 67 73 20 20 20 20  |.....(........      Settings    |
[11:51:37.293][I][rs485_frame.stats:318]:         1 @ 04 0A 83 00 03 20 20 20 20 20 20 20 54 69 6D 65 72 73 20 20 20 20 20 20 20 20 20 20 20 20 20 20  |.....       Timers              |
[11:51:37.293][I][rs485_frame.stats:318]:         4 @ 04 0A 83 00 02 28 00 00 00 00 00 00 00 03 20 20 20 43 6F 6E 66 69 67 75 72 61 74 69 6F 6E 20 20  |.....(........   Configuration  |
[11:51:37.295][I][rs485_frame.stats:307]:   0C01 payloads:
[11:51:37.295][I][rs485_frame.stats:318]:        15 @ 0C 01 00 23  |...#|
[11:51:37.296][I][rs485_frame.stats:307]:   000C payloads:
[11:51:37.298][I][rs485_frame.stats:318]:        13 @ 00 0C 00 00 23 01 14  |....#..|
[11:51:37.301][I][rs485_frame.stats:318]:         2 @ 00 0C 00 00 23 01 20  |....#. |
[11:51:37.304][I][rs485_frame.stats:307]:   0102 payloads:
[11:51:37.304][I][rs485_frame.stats:318]:        15 @ 01 02 28 00 00 00 00 00 00 00  |..(.......|
#### Here we can see the 7 MENU presses. My menu config in the yaml matches this: vars: { button_name: "Menu", button_command: [0x02000000, 0x02000000] } ####
[11:51:37.306][I][rs485_frame.stats:307]:   0083 payloads:
[11:51:37.309][I][rs485_frame.stats:318]:         7 @ 00 83 01 02 00 00 00 02 00 00 00 DB  |............|
[11:51:37.312][I][rs485_frame.stats:307]:   0004 payloads:
[11:51:37.315][I][rs485_frame.stats:318]:         1 @ 00 04 02  |...|
[11:51:37.318][I][rs485_frame.stats:318]:         1 @ 00 04 30 33 30 30 20  |..0300 |
[11:51:37.327][I][rs485_frame.stats:318]:         1 @ 00 04 CC 6D  |...m|
[11:51:37.327][I][rs485_frame.stats:307]:   0407 payloads:
[11:51:37.327][I][rs485_frame.stats:318]:         1 @ 04 07  |..|
[11:51:37.327][I][rs485_frame.stats:307]:   04A0 payloads:
[11:51:37.327][I][rs485_frame.stats:318]:         1 @ 04 A0  |..|
[11:51:37.327][I][rs485_frame.stats:307]:   04AE payloads:
[11:51:37.327][I][rs485_frame.stats:318]:         1 @ 04 AE  |..|

Thanks for getting back to me so quickly. I’ve added the sniffer, and here’s the output you requested. Cycled the menus a couple of times. Thanks in advance.

[12:35:31.559][I][rs485_frame.stats:232]: RS485 sniffer stats over 30015 ms (sorted by count):
[12:35:31.564][I][rs485_frame.stats:237]:   type  cnt    d-ref(min/med/max)    d-same(min/med/max)   payloads
[12:35:31.564][I][rs485_frame.stats:272]:   0101   300        -     -     -       64   100   116   1 unique
[12:35:31.564][I][rs485_frame.stats:272]:   0102    60       28    40    65      467   500   541   2 unique
[12:35:31.572][I][rs485_frame.stats:272]:   0103    33       44    58    89      282   690  2021   11 unique
[12:35:31.573][I][rs485_frame.stats:272]:   0002    22        3     3     4       99   700  1000   2 unique
[12:35:31.573][I][rs485_frame.stats:272]:   0C01    15       20    31    32     1992  2000  2016   1 unique
[12:35:31.573][I][rs485_frame.stats:272]:   000C    15       25    37    38     1992  2000  2017   5 unique
[12:35:31.580][I][rs485_frame.stats:272]:   0407     1       28    28    28        -     -     -   1 unique
[12:35:31.580][I][rs485_frame.stats:307]:   0101 payloads:
[12:35:31.580][I][rs485_frame.stats:318]:       300 @ 01 01  |..|
[12:35:31.586][I][rs485_frame.stats:307]:   0102 payloads:
[12:35:31.590][I][rs485_frame.stats:318]:        30 @ 01 02 2E 00 00 00 20 00 00 00  |...... ...|
[12:35:31.590][I][rs485_frame.stats:318]:        30 @ 01 02 0E 00 00 00 20 00 00 00  |...... ...|
[12:35:31.590][I][rs485_frame.stats:307]:   0103 payloads:
[12:35:31.591][I][rs485_frame.stats:318]:         4 @ 01 03 20 20 20 20 53 61 74 75 72 64 61 79 20 20 20 20 20 20 20 20 20 31 32 3A 33 34 50 20 20 20  |..    Saturday         12:34P   |
[12:35:31.595][I][rs485_frame.stats:318]:         4 @ 01 03 20 20 20 20 53 61 74 75 72 64 61 79 20 20 20 20 20 20 20 20 20 31 32 20 33 34 50 20 20 20  |..    Saturday         12 34P   |
[12:35:31.598][I][rs485_frame.stats:318]:         2 @ 01 03 50 6F 6F 6C 20 54 65 6D 70 20 20 38 39 DF 46 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20  |..Pool Temp  89.F               |
[12:35:31.603][I][rs485_frame.stats:318]:         2 @ 01 03 41 69 72 20 54 65 6D 70 20 20 31 30 33 DF 46 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20  |..Air Temp  103.F               |
[12:35:31.604][I][rs485_frame.stats:318]:         2 @ 01 03 50 6F 6F 6C 20 43 68 6C 6F 72 69 6E 61 74 6F 72 20 20 20 20 20 20 34 30 25 20 20 20 20 20  |..Pool Chlorinator      40%     |
[12:35:31.610][I][rs485_frame.stats:318]:         2 @ 01 03 43 68 6C 6F 72 69 6E 61 74 6F 72 20 4F 66 66 20 20 48 69 67 68 20 53 61 6C 74 2F 41 6D 70  |..Chlorinator Off  High Salt/Amp|
[12:35:31.613][I][rs485_frame.stats:318]:         3 @ 01 03 20 20 20 20 53 65 74 74 69 6E 67 73 20 20 20 20 20 20 20 20 20 20 4D 65 6E 75 20 20 20 20  |..    Settings          Menu    |
[12:35:31.613][I][rs485_frame.stats:318]:         3 @ 01 03 20 20 20 20 20 54 69 6D 65 72 73 20 20 20 20 20 20 20 20 20 20 20 4D 65 6E 75 20 20 20 20  |..     Timers           Menu    |
[12:35:31.613][I][rs485_frame.stats:318]:         4 @ 01 03 20 20 20 44 69 61 67 6E 6F 73 74 69 63 20 20 20 20 20 20 20 20 20 4D 65 6E 75 20 20 20 20  |..   Diagnostic         Menu    |
[12:35:31.620][I][rs485_frame.stats:318]:         3 @ 01 03 20 43 6F 6E 66 69 67 75 72 61 74 69 6F 6E 20 20 20 4D 65 6E 75 2D 4C 6F 63 6B 65 64 20 20  |.. Configuration   Menu-Locked  |
[12:35:31.620][I][rs485_frame.stats:318]:         4 @ 01 03 20 20 20 20 44 65 66 61 75 6C 74 20 20 20 20 20 20 20 20 20 20 20 4D 65 6E 75 20 20 20 20  |..    Default           Menu    |
[12:35:31.620][I][rs485_frame.stats:307]:   0002 payloads:
[12:35:31.620][I][rs485_frame.stats:318]:        14 @ 00 02 02 00 02 00  |......|
[12:35:31.624][I][rs485_frame.stats:318]:         8 @ 00 02 02 00 00 00  |......|
[12:35:31.624][I][rs485_frame.stats:307]:   0C01 payloads:
[12:35:31.624][I][rs485_frame.stats:318]:        15 @ 0C 01 00 32  |...2|
[12:35:31.624][I][rs485_frame.stats:307]:   000C payloads:
[12:35:31.633][I][rs485_frame.stats:318]:         8 @ 00 0C 00 00 32 01 59  |....2.Y|
[12:35:31.634][I][rs485_frame.stats:318]:         2 @ 00 0C 00 00 32 01 60  |....2.`|
[12:35:31.634][I][rs485_frame.stats:318]:         1 @ 00 0C 00 00 32 01 58  |....2.X|
[12:35:31.634][I][rs485_frame.stats:318]:         2 @ 00 0C 00 00 32 01 61  |....2.a|
[12:35:31.636][I][rs485_frame.stats:318]:         2 @ 00 0C 00 00 32 01 57  |....2.W|
[12:35:31.636][I][rs485_frame.stats:307]:   0407 payloads:
[12:35:31.636][I][rs485_frame.stats:318]:         1 @ 04 07  |..|
[12:35:31.636][W][component:473]: rs485_frame took a long time for an operation (77 ms), max is 70 ms

@b3nj1 I also see how your code will mess up the text display as your frame capture shows where the text begins at [0], and not [2] for a number of messages. I’m assuming this is a firmware variation compared to what you have tested on.

[12:35:01.579][I][rs485_frame.stats:307]:   0103 payloads:
[12:35:01.579][I][rs485_frame.stats:318]:         3 @ 01 03 20 20 46 69 6C 74 65 72 20 53 70 65 65 64 20 20 20 20 20 20 20 20 35 30 25 20 20 20 20 20  |..  Filter Speed        50%     |
[12:35:01.582][I][rs485_frame.stats:318]:         4 @ 01 03 20 20 20 20 53 61 74 75 72 64 61 79 20 20 20 20 20 20 20 20 20 31 32 3A 33 34 50 20 20 20  |..    Saturday         12:34P   |
[12:35:01.582][I][rs485_frame.stats:318]:         4 @ 01 03 20 20 20 20 53 61 74 75 72 64 61 79 20 20 20 20 20 20 20 20 20 31 32 20 33 34 50 20 20 20  |..    Saturday         12 34P   |
[12:35:01.587][I][rs485_frame.stats:318]:         2 @ 01 03 50 6F 6F 6C 20 54 65 6D 70 20 20 38 39 DF 46 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20  |..Pool Temp  89.F               |
[12:35:01.587][I][rs485_frame.stats:318]:         2 @ 01 03 41 69 72 20 54 65 6D 70 20 20 31 30 33 DF 46 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20  |..Air Temp  103.F               |
[12:35:01.592][I][rs485_frame.stats:318]:         2 @ 01 03 50 6F 6F 6C 20 43 68 6C 6F 72 69 6E 61 74 6F 72 20 20 20 20 20 20 34 30 25 20 20 20 20 20  |..Pool Chlorinator      40%     |
[12:35:01.592][I][rs485_frame.stats:318]:         2 @ 01 03 43 68 6C 6F 72 69 6E 61 74 6F 72 20 4F 66 66 20 20 48 69 67 68 20 53 61 6C 74 2F 41 6D 70  |..Chlorinator Off  High Salt/Amp|
[12:35:01.592][I][rs485_frame.stats:318]:         2 @ 01 03 20 20 43 68 65 63 6B 20 53 79 73 74 65 6D 20 20 20 48 69 67 68 20 53 61 6C 74 2F 41 6D 70  |..  Check System   High Salt/Amp|

@mrmoorey , I’m pretty sure this is your MENU button:

[12:35:31.620][I][rs485_frame.stats:307]:   0002 payloads:
[12:35:31.620][I][rs485_frame.stats:318]:        14 @ 00 02 02 00 02 00  |......|
[12:35:31.624][I][rs485_frame.stats:318]:         8 @ 00 02 02 00 00 00  |......|

From previous analysis, 0002 is the frame type for wired controller. The payload of 0200 twice is basically a shorter version of my 0200000.

Can you try changing your MENU button yaml to shorter commands:

button_command: [0x0200, 0x0200]

instead of button_command: [0x02000000, 0x02000000]?

If that doesn’t work, we might need to spoof your main controller. It would replace your MENU button yaml with a more elaborate setting like is done for Service:

      - path: hayward/aqualogic/button.yaml
        vars:
          button_name: "Menu"
          button_command: [0x0200, 0x0200]
          btn_preamble: [0x00, 0x02]   # wired local frame type — unquoted list
          btn_postamble: []
          btn_element_bytes: "2"
          btn_endian: "big"

For your display decoding, the payload includes the frame type (0103), so that should be why it’s starting at 2 - not because of padding. I have to look more into the mismatch because you have 2 symptoms: 1. First char is dropped; 2: Second char is converted to uppercase. However, the sniffer’s output looks good. I’ll try running your text through the decoder and see if I can figure it out. I’ll let you know after I’ve tried.

Let me know if either of the MENU tweaks work.

I agree with your analysis. I just finished analyzing this myself. Ignore the mixed up timestamps; I kept having to run out to the pool to capture the keystrokes I performed, including the unlock and plus/minus keys so we had a complete picture:

[13:14:46.085][I][rs485_frame.stats:307]:   0002 payloads:
[13:14:46.110][I][rs485_frame.stats:318]:         7 @ 00 02 01 00 01 00  |......| Right
[13:14:46.110][I][rs485_frame.stats:318]:         8 @ 00 02 01 00 00 00  |......|
[13:14:16.047][I][rs485_frame.stats:318]:         9 @ 00 02 02 00 02 00  |......| Menu
[13:14:16.047][I][rs485_frame.stats:318]:         8 @ 00 02 02 00 00 00  |......|
[13:14:46.110][I][rs485_frame.stats:318]:         2 @ 00 02 04 00 04 00  |......| Left
[13:14:46.110][I][rs485_frame.stats:318]:         2 @ 00 02 04 00 00 00  |......|
[13:14:16.063][I][rs485_frame.stats:318]:         1 @ 00 02 05 00 05 00  |......| Unlock?
[13:14:16.065][I][rs485_frame.stats:318]:        31 @ 00 02 05 00 00 00  |......|
[13:14:46.110][I][rs485_frame.stats:318]:         2 @ 00 02 10 00 10 00  |......| Minus
[13:14:46.110][I][rs485_frame.stats:318]:         2 @ 00 02 10 00 00 00  |......|
[13:14:46.110][I][rs485_frame.stats:318]:         3 @ 00 02 20 00 20 00  |.. . .| Plus
[13:14:46.110][I][rs485_frame.stats:318]:         3 @ 00 02 20 00 00 00  |.. ...|

I’ll make the changes to my configuration and get back to you.

@mrmoorey , I edited my previous message to fix a typo in the more elaborate Menu setting. We need to change the ban_element_bytes to 2.

I changed the buttons to 2 bytes vs. 4 bytes. Menu still doesn’t appear to work, and when I adjusted the right button from:

vars: { button_name: "Right", button_command: [0x01000000, 0x01000000], button_id: "right", confirm_led_bit: -1 }

To:

vars: { button_name: "Right", button_command: [0x0100, 0x0100], button_id: "right", confirm_led_bit: -1 }

It activated Valve 3 vs the right button. The left button with the shortened code appears to do nothing.

Confirming the elaborate vars setting for the menu button now works; I will change the other buttons and let you know the outcome. Thanks!

@mrmoorey , that makes sense and also relates to why your Right button didin’t work. The size of the commands sent on the bus are set by the “btn_element_bytes” not the text of “button_command” (I tried but it was going against a lot of esphome conventions). This is one of a downside of pushing smarts to the yaml vs forcing code changes. So, your “{ button_name: “Right”, button_command: [0x0100, 0x0100], button_id: “right”, confirm_led_bit: -1 }” actually was more like { button_name: "Right", button_command: [0x00000100, 0x0000100], button_id: "right", confirm_led_bit: -1 }. You might be able to just override some keys. Try { button_name: "Right", button_command: [0x0100, 0x0100], btn_element_bytes: "2", button_id: "right", confirm_led_bit: -1 } instead of the more elaborate edits.

I’ve extracted the decoder, and ran it on your values… It got the Pool temp correct. I’m still confused.

 # ./a.out "01 03 50 6F 6F 6C 20 54 65 6D 70 20 20 38 39 DF 46 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20"
uint8_t payload[32] = {0x01, 0x03, 0x50, 0x6F, 0x6F, 0x6C, 0x20, 0x54, 0x65, 0x6D, 0x70, 0x20, 0x20, 0x38, 0x39, 0xDF, 0x46, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20};
Decoded: Pool Temp  89°F

Filter speed [space][space]Filter Speed[spaces]…50%[spaces]

20 20 46 69 6C 74 65 72 20 53 70 65 65 64 20 20 20 20 20 20 20 20 35 30 25 20 20 20 20 20

Pool temperature Pool Temp[spaces]89[\xDF]F[spaces]

50 6F 6F 6C 20 54 65 6D 70 20 20 38 39 DF 46 20 20 20 20

Bus.yaml:

// Text starts immediately at payload[2] -- no header byte before it
// (decode_variations.md §5.1: "no fixed header before the text").
for (size_t i = 2; i < payload.size() && tlen < (BUF_LEN-2); i++) {
  uint8_t b = payload[i];
  isblink = bool(b > 0x7F);
  b &= 0x7f;
  if (b < 0x20 || b > 0x7E) continue;
  if (b == '_') {
    if (tlen + 2 <= (BUF_LEN-2)) {
     text[tlen++] = '\xC2'; text[tlen++] = '\xB0';
     if (isblink) { blink[blen++] = '\xC2'; blink[blen++] = '\xB0'; }
    }
  } else {
    text[tlen++] = char(b);
    if (isblink) blink[blen++] = char(b);
  }
}

For me to be correct on this, I have to assume that the array 1-based, not 0-based. Since the first byte of text is read from payload[2], Filter speed is read correctly; if we read the pool temperature from payload[2], we get ‘ool temperature’, and the subsequent code cannot find the substring it is looking for to pass to the sensor.

@mrmoorey ,

I added this to the tail of my device yaml. It prints out 0 to 3 of every display payload to the console log along with the sniffer info.

rs485_frame:
 - id: !extend pool
   on_frame:
      - frame_type: [0x01, 0x03]
        then:
          - lambda: |-
               ESP_LOGI("pool", "payload[0..3]=%02X %02X %02X %02X", payload[0], payload[1], payload[2], payload[3]);
   
   dump_frames: false          # keep INFO logs clean; rely on sniffer_stats instead
   sniffer_stats: { interval: 30s, max_frame_types: 64, payload_dump_top: 20 }

I get this which means the “frame type” of 0x0103 is included at payload[0..1], so we should start at payload 2. The “no header byte before it” is confusing - I’ll clear that up.

This doesn’t explain your problem. But what happened if you changed the size_t i = 2 to 1 or 0?

[14:52:44.100][I][pool:305]: payload[0..3]=01 03 20 20
[14:52:46.095][I][pool:305]: payload[0..3]=01 03 20 20
[14:52:46.191][I][pool:305]: payload[0..3]=01 03 20 20
[14:52:48.106][I][pool:305]: payload[0..3]=01 03 20 20

@mrmoorey ,
Can you post your full device yaml? When I try to use the example in GitHub, there’s a version problem related to the feature I mentioned slipped into the main branch. I’m curious how you solved it, and it will help me make sure I know what codes you’re running off in case that impacts the display decoding.

@b3nj1 Here’s the config minus some of the comments so we’re more compact. I’m looking at whether there’s a strange buffer issue, because the publish_state is using a static character array. If your parser is getting the entire string, why would we lose the first character unless the buffer is not being cleared properly. I also realized how I misunderstood the comments; payload is the entire frame, not just what I would call the payload. I assumed that the frame identifier had been consumed (wrong of me to assume).

substitutions:
  name: aqualogic
  friendly_name: Hayward Aqua Logic
  board: esp32-s3-devkitc-1  # see https://esphome.io/components/esp32
  variant: esp32s3
  tx_pin: GPIO17             # if no data received, try swapping tx_pin and rx_pin
  rx_pin: GPIO18
  flow_control_pin: GPIO21   # DE/RE direction pin; delete this and the uart: line below

  cmd_preamble: [0x00, 0x83, 0x01]   # unquoted YAML list — wireless default
  cmd_postamble: [0x00]              # wireless trailing pad; wired uses []
  cmd_element_bytes: "4"             # Hayward: always 4 bytes per value
  cmd_endian: "big"                  # Hayward: big-endian
  # Temperature units — passed to bus.yaml decoders.
  temp_unit: "°F"
  temp_unit_short: "F"

esphome:
  name: ${name}
  friendly_name: ${friendly_name}

esp32:
  board: ${board}
  variant: ${variant}
  framework:
    type: esp-idf

external_components:
  - source: github://b3nj1/esphome@rs485_frame
    components: [rs485_frame]

# UART — Hayward AquaLogic / ProLogic: 19200 baud, 8N2; fixed by protocol.
uart:
  id: pool_uart
  tx_pin: ${tx_pin}
  rx_pin: ${rx_pin}
  baud_rate: 19200  # Hayward AquaLogic — do not change
  data_bits: 8
  parity: NONE
  stop_bits: 2
  rx_buffer_size: 512
  flow_control_pin: ${flow_control_pin}

packages:
  rs485:
    url: https://github.com/b3nj1/rs485_frame-examples
    ref: v3.0.1          # PINNED release tag — bump deliberately to update (see README)
    refresh: 1d
    files:
      # Bus package is REQUIRED and stays uncommented.
      - hayward/aqualogic/bus.yaml

      # --- Equipment: uncomment the lines for gear you have, delete the rest. ---
      - hayward/aqualogic/pump-vsp.yaml        # Variable-speed pump — Status: tested
      # - hayward/aqualogic/heater.yaml          # Gas/heat-pump heater — Status: tested

      # --- LED indicators — rename labels to match your load; remove bits your panel lacks. ---
      # Enabled by default: states present on every AquaLogic panel.
      # Disabled by default (hidden in HA; enable in Settings > Entities as needed):
      #   AUX channels, valves, Service, Spillover, Super Chlorinate.
      # Bits 0-8 hardware-verified. Bits 9-25 confirmed by swilson, smith288, ChaseDurand.
      # Use the "LED Mask" diagnostic sensor to verify bit layout on your specific panel.
      - path: hayward/aqualogic/led.yaml
        vars: { bit: 0, led_name: "Heater Active", device_class: heat }
      - path: hayward/aqualogic/led.yaml
        vars: { bit: 1, led_name: "Valve 3", device_class: running, disabled_by_default: "true" }
      - path: hayward/aqualogic/led.yaml
        vars: { bit: 2, led_name: "Check System", device_class: problem }
      - path: hayward/aqualogic/led.yaml
        vars: { bit: 3, led_name: "Pool Mode", device_class: running }
      - path: hayward/aqualogic/led.yaml
        vars: { bit: 4, led_name: "Spa Mode", device_class: running }
      - path: hayward/aqualogic/led.yaml
        vars: { bit: 5, led_name: "Filter", device_class: running }
      - path: hayward/aqualogic/led.yaml
        vars: { bit: 6, led_name: "Lights", device_class: light }
      - path: hayward/aqualogic/led.yaml
        vars: { bit: 7, led_name: "AUX 1", device_class: running, disabled_by_default: "true" }
      - path: hayward/aqualogic/led.yaml
        vars: { bit: 8, led_name: "AUX 2", device_class: running, disabled_by_default: "true" }
      - path: hayward/aqualogic/led.yaml
        vars: { bit: 9, led_name: "Service", device_class: running, disabled_by_default: "true" }
      - path: hayward/aqualogic/led.yaml
        vars: { bit: 10, led_name: "AUX 3", device_class: running, disabled_by_default: "true" }
      - path: hayward/aqualogic/led.yaml
        vars: { bit: 11, led_name: "AUX 4", device_class: running, disabled_by_default: "true" }
      - path: hayward/aqualogic/led.yaml
        vars: { bit: 12, led_name: "AUX 5", device_class: running, disabled_by_default: "true" }
      - path: hayward/aqualogic/led.yaml
        vars: { bit: 13, led_name: "AUX 6", device_class: running, disabled_by_default: "true" }
      - path: hayward/aqualogic/led.yaml
        vars: { bit: 14, led_name: "Valve 4", device_class: running, disabled_by_default: "true" }
      - path: hayward/aqualogic/led.yaml
        vars: { bit: 15, led_name: "Spillover", device_class: running, disabled_by_default: "true" }
      - path: hayward/aqualogic/led.yaml
        vars: { bit: 16, led_name: "System Off", device_class: problem }
      - path: hayward/aqualogic/led.yaml
        vars: { bit: 17, led_name: "AUX 7", device_class: running, disabled_by_default: "true" }
      - path: hayward/aqualogic/led.yaml
        vars: { bit: 18, led_name: "AUX 8", device_class: running, disabled_by_default: "true" }
      - path: hayward/aqualogic/led.yaml
        vars: { bit: 19, led_name: "AUX 9", device_class: running, disabled_by_default: "true" }
      - path: hayward/aqualogic/led.yaml
        vars: { bit: 20, led_name: "AUX 10", device_class: running, disabled_by_default: "true" }
      - path: hayward/aqualogic/led.yaml
        vars: { bit: 21, led_name: "AUX 11", device_class: running, disabled_by_default: "true" }
      - path: hayward/aqualogic/led.yaml
        vars: { bit: 22, led_name: "AUX 12", device_class: running, disabled_by_default: "true" }
      - path: hayward/aqualogic/led.yaml
        vars: { bit: 23, led_name: "AUX 13", device_class: running, disabled_by_default: "true" }
      - path: hayward/aqualogic/led.yaml
        vars: { bit: 24, led_name: "AUX 14", device_class: running, disabled_by_default: "true" }
      - path: hayward/aqualogic/led.yaml
        vars: { bit: 25, led_name: "Super Chlorinate", device_class: running, disabled_by_default: "true" }

      # Navigation and mode: active by default on every panel.
      - path: hayward/aqualogic/button.yaml
        vars: { button_name: "Filter", button_command: [0x80000000, 0x80000000], button_id: "filter", confirm_led_bit: 5, confirm_disabled_by_default: "false" }
      - path: hayward/aqualogic/button.yaml
        # Toggles between the Pool-Mode (bit 3) and Spa-Mode (bit 4) LEDs -- a select between
        # two different bits, not one bit flipping to its opposite. button.yaml's monitor
        # models a single-bit toggle, so this doesn't fit; left at the -1 sentinel.
        vars: { button_name: "Pool Spa Mode", button_command: [0x40000000, 0x40000000], button_id: "pool_spa_mode", confirm_led_bit: -1 }
      - path: hayward/aqualogic/button.yaml
        # vars: { button_name: "Plus", button_command: [0x20000000, 0x20000000], button_id: "plus", confirm_led_bit: -1 }
        vars:
          button_name: "Plus"
          button_command: [0x2000, 0x2000]
          btn_preamble: [0x00, 0x02]   # wired local frame type — unquoted list
          btn_postamble: []
          btn_element_bytes: "2"
          btn_endian: "big"
      - path: hayward/aqualogic/button.yaml
        # vars: { button_name: "Minus", button_command: [0x10000000, 0x10000000], button_id: "minus", confirm_led_bit: -1 }
        vars:
          button_name: "Minus"
          button_command: [0x1000, 0x1000]
          btn_preamble: [0x00, 0x02]   # wired local frame type — unquoted list
          btn_postamble: []
          btn_element_bytes: "2"
          btn_endian: "big"
      - path: hayward/aqualogic/button.yaml
        # vars: { button_name: "Menu", button_command: [0x02000000, 0x02000000], button_id: "menu", confirm_led_bit: -1 }
        vars:
          button_name: "Menu"
          button_command: [0x0200, 0x0200]
          btn_preamble: [0x00, 0x02]   # wired local frame type — unquoted list
          btn_postamble: []
          btn_element_bytes: "2"
          btn_endian: "big"
      - path: hayward/aqualogic/button.yaml
        # vars: { button_name: "Right", button_command: [0x01000000, 0x01000000], button_id: "right", confirm_led_bit: -1 }
        vars:
          button_name: "Right"
          button_command: [0x0100, 0x0100]
          btn_preamble: [0x00, 0x02]   # wired local frame type — unquoted list
          btn_postamble: []
          btn_element_bytes: "2"
          btn_endian: "big"
      - path: hayward/aqualogic/button.yaml
        # vars: { button_name: "Left", button_command: [0x04000000, 0x04000000], button_id: "left", confirm_led_bit: -1 }
        vars:
          button_name: "Left"
          button_command: [0x0400, 0x0400]
          btn_preamble: [0x00, 0x02]   # wired local frame type — unquoted list
          btn_postamble: []
          btn_element_bytes: "2"
          btn_endian: "big"
      - path: hayward/aqualogic/button.yaml
        vars: { button_name: "Lights", button_command: [0x0001, 0x0001], button_id: "lights", confirm_led_bit: 6, confirm_disabled_by_default: "false" }
      # Menu Unlock — Hayward unlocks setup menus by holding Left+Right for several seconds,
      # not a quick press. Two buttons bracket the hold instead of one: "Start" sets the
      # Left+Right bits with nothing released (held), "End" releases them. Press Start, wait
      # a few seconds, then press End.
      # - path: hayward/aqualogic/button.yaml
      #   vars: { button_name: "Menu Unlock Start", button_command: [0x0500, 0x0000], button_id: "menu_unlock_start", confirm_led_bit: -1 }
      # - path: hayward/aqualogic/button.yaml
      #   vars: { button_name: "Menu Unlock End", button_command: [0x0000, 0x0500], button_id: "menu_unlock_end", confirm_led_bit: -1 }
      # Service — wired local frame (0x0002), no postamble. Disabled by default:
      # maintenance use only; most users should not trigger this accidentally.
      # LED bit 9 (Service) is community-sourced only (not proj hw-tested), so confirm_led_bit
      # stays at the -1 sentinel until someone verifies it against real hardware.
      - path: hayward/aqualogic/button.yaml
        vars:
          button_name: "Service"
          button_command: [0x08000000, 0x08000000]
          button_id: "service"
          confirm_led_bit: -1
          disabled_by_default: "true"
          btn_preamble: [0x00, 0x02]   # wired local frame type — unquoted list
          btn_postamble: []
          btn_element_bytes: "4"
          btn_endian: "big"
      # System Off — always wireless regardless of hub role.
      # LED bit 16 (System Off) is community-sourced only (not proj hw-tested), so
      # confirm_led_bit stays at the -1 sentinel until someone verifies it against real hardware.
      - path: hayward/aqualogic/button.yaml
        vars:
          button_name: "System Off"
          button_command: [0x08000000, 0x08000000]
          button_id: "system_off"
          confirm_led_bit: -1
          btn_preamble: [0x00, 0x83, 0x01]   # wireless — do not change
          btn_postamble: [0x00]
          btn_element_bytes: "4"
          btn_endian: "big"
      # AUX relays and valves — disabled by default; enable in HA Settings > Entities.
      # Rename labels to match your load (e.g. "Pool Light", "Spa Jets", "Booster Pump").
      # AUX 1-2 hardware-tested (bits 7-8, in led.yaml's proj-hw-tested 0-8 range) -- their
      # confirm_led_bit is set to that real bit, but confirm_disabled_by_default is left at its
      # default ("true") since neither was exercised by the 2026-07-26 confirm/fail capture.
      # AUX 3-7 (bits 10-13, 17) are confirmed by swilson + smith288 only, not hw-tested --
      # confirm_led_bit stays at the -1 sentinel until someone verifies these against hardware.
      - path: hayward/aqualogic/button.yaml
        vars: { button_name: "AUX 1", button_command: [0x00020000, 0x00020000], button_id: "aux_1", confirm_led_bit: 7, disabled_by_default: "true" }
      - path: hayward/aqualogic/button.yaml
        vars: { button_name: "AUX 2", button_command: [0x00040000, 0x00040000], button_id: "aux_2", confirm_led_bit: 8, disabled_by_default: "true" }
      - path: hayward/aqualogic/button.yaml
        vars: { button_name: "AUX 3", button_command: [0x00080000, 0x00080000], button_id: "aux_3", confirm_led_bit: -1, disabled_by_default: "true" }
      - path: hayward/aqualogic/button.yaml
        vars: { button_name: "AUX 4", button_command: [0x00100000, 0x00100000], button_id: "aux_4", confirm_led_bit: -1, disabled_by_default: "true" }
      - path: hayward/aqualogic/button.yaml
        vars: { button_name: "AUX 5", button_command: [0x00200000, 0x00200000], button_id: "aux_5", confirm_led_bit: -1, disabled_by_default: "true" }
      - path: hayward/aqualogic/button.yaml
        vars: { button_name: "AUX 6", button_command: [0x00400000, 0x00400000], button_id: "aux_6", confirm_led_bit: -1, disabled_by_default: "true" }
      - path: hayward/aqualogic/button.yaml
        vars: { button_name: "AUX 7", button_command: [0x00800000, 0x00800000], button_id: "aux_7", confirm_led_bit: -1, disabled_by_default: "true" }
      # AUX 8-14: command bytes are in an unusual byte position; swilson source only,
      # no independent wire-capture confirmation. Keep commented until hw-tested.
      # - path: hayward/aqualogic/button.yaml
      #   vars: { button_name: "AUX 8",  button_command: [0x00000800, 0x00000800], button_id: "aux_8", confirm_led_bit: -1, disabled_by_default: "true" }
      # - path: hayward/aqualogic/button.yaml
      #   vars: { button_name: "AUX 9",  button_command: [0x00001000, 0x00001000], button_id: "aux_9", confirm_led_bit: -1, disabled_by_default: "true" }
      # - path: hayward/aqualogic/button.yaml
      #   vars: { button_name: "AUX 10", button_command: [0x00002000, 0x00002000], button_id: "aux_10", confirm_led_bit: -1, disabled_by_default: "true" }
      # - path: hayward/aqualogic/button.yaml
      #   vars: { button_name: "AUX 11", button_command: [0x00004000, 0x00004000], button_id: "aux_11", confirm_led_bit: -1, disabled_by_default: "true" }
      # - path: hayward/aqualogic/button.yaml
      #   vars: { button_name: "AUX 12", button_command: [0x00008000, 0x00008000], button_id: "aux_12", confirm_led_bit: -1, disabled_by_default: "true" }
      # - path: hayward/aqualogic/button.yaml
      #   vars: { button_name: "AUX 13", button_command: [0x00000001, 0x00000001], button_id: "aux_13", confirm_led_bit: -1, disabled_by_default: "true" }
      # - path: hayward/aqualogic/button.yaml
      #   vars: { button_name: "AUX 14", button_command: [0x00000002, 0x00000002], button_id: "aux_14", confirm_led_bit: -1, disabled_by_default: "true" }
      # Valve 3 (bit 1) is hardware-tested (led.yaml's proj-hw-tested 0-8 range); Valve 4
      # (bit 14) is community-sourced only -- confirm_led_bit stays at the -1 sentinel there.
      - path: hayward/aqualogic/button.yaml
        vars: { button_name: "Valve 3", button_command: [0x00000100, 0x00000100], button_id: "valve_3", confirm_led_bit: 1, disabled_by_default: "true" }
      - path: hayward/aqualogic/button.yaml
        vars: { button_name: "Valve 4", button_command: [0x00000200, 0x00000200], button_id: "valve_4", confirm_led_bit: -1, disabled_by_default: "true" }

@mrmoorey , I think I see the problem.

Try changing to this ref: v3.1.0-rc1 in your yaml:

packages:
  rs485:
    url: https://github.com/b3nj1/rs485_frame-examples
    ref: v3.1.0-rc1

You’re using v3.0.1 which actually starts at payload[3]! This explains your lost character. It doesn’t explain your upper/lower case but maybe that’s a UI thing.

@b3nj1 That explains a lot!!

I now get a bunch of validation errors; here’s a couple of them:

Failed config
sensor.template: [source /data/packages/ac658772/hayward/aqualogic/button.yaml:210]
  platform: template
  ID btn_confirmed_count_unset redefined! Check sensor->16->id.
  id: btn_confirmed_count_unset
  name: Minus Confirmed
  entity_category: diagnostic
  state_class: total_increasing
  accuracy_decimals: 0
  disabled_by_default: True
  force_update: False
  update_interval: 60s
sensor.template: [source /data/packages/ac658772/hayward/aqualogic/button.yaml:221]
  platform: template
  ID btn_failed_count_unset redefined! Check sensor->17->id.
  id: btn_failed_count_unset
  name: Minus Failed
  entity_category: diagnostic
  state_class: total_increasing
  accuracy_decimals: 0
  disabled_by_default: True
  force_update: False
  update_interval: 60s

@mrmoorey , OK - sorry about that.
Give me a few minutes I’ll give you a new version.