Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .github/workflows/esphome.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ jobs:
outputs:
matrix_json: ${{ steps.set-matrix.outputs.matrix_json }}
run_compile: ${{ steps.set-matrix.outputs.run_compile }}
run_parser_native_test: ${{ steps.set-matrix.outputs.run_parser_native_test }}
steps:
- name: Checkout repository
uses: actions/checkout@v5
Expand Down Expand Up @@ -44,6 +45,12 @@ jobs:
$1 == "tests" && $2 == "components" { print $3 }
' | sort -u)

RUN_PARSER_NATIVE_TEST=false
if echo "$CHANGED_FILES" | grep -qE '^(components/uartex/|tests/components/uartex/|tests/native/uartex_parser/)'; then
RUN_PARSER_NATIVE_TEST=true
fi
echo "run_parser_native_test=$RUN_PARSER_NATIVE_TEST" >> $GITHUB_OUTPUT

# Build a JSON array of configuration files to test
# Each item has: {"component": "comp_name", "file": "test.<target>.yaml"}
MATRIX_JSON="[]"
Expand Down Expand Up @@ -145,3 +152,16 @@ jobs:
- name: Compile ESPHome Config
run: |
esphome compile "tests/components/${{ matrix.component }}/${{ matrix.file }}"

parser-native-test:
needs: detect-changes
if: ${{ needs.detect-changes.outputs.run_parser_native_test == 'true' }}
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5

- name: Build and run parser native tests
run: |
chmod +x tests/native/uartex_parser/run.sh
tests/native/uartex_parser/run.sh
41 changes: 36 additions & 5 deletions components/uartex/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from .const import CONF_RX_HEADER, CONF_RX_FOOTER, CONF_TX_HEADER, CONF_TX_FOOTER, \
CONF_RX_CHECKSUM, CONF_TX_CHECKSUM, CONF_RX_CHECKSUM_2, CONF_TX_CHECKSUM_2, \
CONF_UARTEX_ID, CONF_ERROR, CONF_LOG, CONF_ON_TX_TIMEOUT, CONF_RX_PRIORITY, \
CONF_ACK, CONF_ON_WRITE, CONF_ON_READ, \
CONF_ACK, CONF_ON_WRITE, CONF_ON_READ, CONF_RX_REPLY, CONF_COMMAND, \
CONF_STATE, CONF_MASK, CONF_MATCH, \
CONF_STATE_ON, CONF_STATE_OFF, CONF_COMMAND_ON, CONF_COMMAND_OFF, \
CONF_COMMAND_UPDATE, CONF_RX_TIMEOUT, CONF_TX_TIMEOUT, CONF_TX_RETRY_CNT, CONF_TX_COMMAND_QUEUE_SIZE, \
Expand Down Expand Up @@ -139,6 +139,17 @@ def header_schema(value):
return HEADER_SCHEMA(value)
return shorthand_header(value)

def validate_rx_header(value):
if isinstance(value, dict):
return [header_schema(value)]
if isinstance(value, list):
if not value:
raise cv.Invalid("rx_header cannot be empty")
if isinstance(value[0], (list, dict)):
return [header_schema(h) for h in value]
return [header_schema(value)]
return [header_schema(value)]

COMMAND_SCHEMA = cv.Schema({
cv.Required(CONF_DATA): validate_hex_data,
cv.Optional(CONF_ACK, default=[]): validate_hex_data,
Expand All @@ -164,6 +175,11 @@ def command_hex_schema(value):
def rx_data_length_schema(value):
return RX_DATA_LENGTH_SCHEMA(value)

RX_REPLY_SCHEMA = cv.Schema({
cv.Required(CONF_STATE): state_schema,
cv.Required(CONF_COMMAND): cv.templatable(command_hex_schema),
})

# UARTEx Schema
CONFIG_SCHEMA = cv.All(cv.Schema({
cv.GenerateID(): cv.declare_id(UARTExComponent),
Expand Down Expand Up @@ -199,8 +215,9 @@ def rx_data_length_schema(value):
cv.Optional(CONF_RX_LENGTH): cv.int_range(min=1, max=256),
cv.Optional(CONF_RX_DATA_LENGTH): rx_data_length_schema,
cv.Optional(CONF_TX_CTRL_PIN): pins.gpio_output_pin_schema,
cv.Optional(CONF_RX_HEADER): header_schema,
cv.Optional(CONF_RX_HEADER): validate_rx_header,
cv.Optional(CONF_RX_FOOTER): validate_hex_data,
cv.Optional(CONF_RX_REPLY): cv.ensure_list(RX_REPLY_SCHEMA),
cv.Optional(CONF_TX_HEADER): validate_hex_data,
cv.Optional(CONF_TX_FOOTER): validate_hex_data,
cv.Optional(CONF_RX_CHECKSUM): validate_checksum,
Expand Down Expand Up @@ -296,8 +313,19 @@ async def to_code(config):
cg.add(var.set_tx_ctrl_pin(tx_ctrl_pin))

if CONF_RX_HEADER in config:
header = header_hex_expression(config[CONF_RX_HEADER])
cg.add(var.set_rx_header(header))
for h in config[CONF_RX_HEADER]:
header = header_hex_expression(h)
cg.add(var.add_rx_header(header))

for reply in config.get(CONF_RX_REPLY, []):
state = state_hex_expression(reply[CONF_STATE])
command_conf = reply[CONF_COMMAND]
if cg.is_template(command_conf):
template_ = await cg.templatable(command_conf, [(uint8_ptr_const, 'data'), (uint16_const, 'len')], cmd_t)
cg.add(var.add_rx_reply(state, template_))
else:
command = command_hex_expression(command_conf)
cg.add(var.add_rx_reply(state, command))

if CONF_RX_FOOTER in config:
cg.add(var.set_rx_footer(config[CONF_RX_FOOTER]))
Expand All @@ -311,7 +339,10 @@ async def to_code(config):
if CONF_RX_CHECKSUM in config:
data = config[CONF_RX_CHECKSUM]
if cg.is_template(data):
template_ = await cg.templatable(data, [(uint8_ptr_const, 'data'), (uint16_const, 'len')], cg.uint8)
template_ = await cg.templatable(data, [
(uint8_ptr_const, 'data'), (uint16_const, 'len'),
(uint8_ptr_const, 'header'), (uint16_const, 'header_len'),
], cg.uint8)
cg.add(var.set_rx_checksum(template_))
else:
cg.add(var.set_rx_checksum(data))
Expand Down
48 changes: 48 additions & 0 deletions components/uartex/checksum.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#include "checksum.h"

namespace esphome {
namespace uartex {

uint16_t compute_checksum(CHECKSUM checksum, const std::vector<uint8_t> &header, const std::vector<uint8_t> &data)
{
uint16_t crc = 0;
uint8_t temp = 0;
switch (checksum)
{
case CHECKSUM_XOR:
for (uint8_t byte : header) { crc ^= byte; }
for (uint8_t byte : data) { crc ^= byte; }
break;
case CHECKSUM_ADD:
for (uint8_t byte : header) { crc += byte; }
for (uint8_t byte : data) { crc += byte; }
break;
case CHECKSUM_XOR_NO_HEADER:
for (uint8_t byte : data) { crc ^= byte; }
break;
case CHECKSUM_ADD_NO_HEADER:
for (uint8_t byte : data) { crc += byte; }
break;
case CHECKSUM_XOR_ADD:
for (uint8_t byte : header)
{
crc += byte;
temp ^= byte;
}
for (uint8_t byte : data)
{
crc += byte;
temp ^= byte;
}
crc += temp;
crc = ((uint16_t) temp << 8) | (crc & 0xFF);
break;
case CHECKSUM_NONE:
case CHECKSUM_CUSTOM:
break;
}
return crc;
}

} // namespace uartex
} // namespace esphome
21 changes: 21 additions & 0 deletions components/uartex/checksum.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#pragma once
#include <cstdint>
#include <vector>

namespace esphome {
namespace uartex {

enum CHECKSUM {
CHECKSUM_NONE,
CHECKSUM_CUSTOM,
CHECKSUM_XOR,
CHECKSUM_ADD,
CHECKSUM_XOR_NO_HEADER,
CHECKSUM_ADD_NO_HEADER,
CHECKSUM_XOR_ADD
};

uint16_t compute_checksum(CHECKSUM checksum, const std::vector<uint8_t> &header, const std::vector<uint8_t> &data);

} // namespace uartex
} // namespace esphome
2 changes: 2 additions & 0 deletions components/uartex/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@
CONF_SPEED_CNT = 'speed_cnt'
CONF_ON_WRITE = 'on_write'
CONF_ON_READ = 'on_read'
CONF_RX_REPLY = 'rx_reply'
CONF_COMMAND = 'command'

# Water Heater Commands
CONF_COMMAND_ECO = 'command_eco'
Expand Down
Loading