From d2c33b0f6f2acf561e658a9ea4616fbbadfcb46f Mon Sep 17 00:00:00 2001 From: Mat931 <49403702+Mat931@users.noreply.github.com> Date: Sat, 26 Sep 2026 01:24:51 +0000 Subject: [PATCH] [systa_bus] Add systa_bus component (#15473) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- CODEOWNERS | 1 + esphome/components/systa_bus/__init__.py | 41 ++++++ .../components/systa_bus/sensor/__init__.py | 81 ++++++++++++ .../systa_bus/sensor/systa_bus_sensor.cpp | 37 ++++++ .../systa_bus/sensor/systa_bus_sensor.h | 28 ++++ esphome/components/systa_bus/systa_bus.cpp | 70 ++++++++++ esphome/components/systa_bus/systa_bus.h | 50 +++++++ esphome/core/defines.h | 1 + tests/components/systa_bus/__init__.py | 13 ++ tests/components/systa_bus/common.h | 37 ++++++ tests/components/systa_bus/common.yaml | 15 +++ tests/components/systa_bus/systa_bus_test.cpp | 125 ++++++++++++++++++ .../components/systa_bus/test.esp32-idf.yaml | 3 + 13 files changed, 502 insertions(+) create mode 100644 esphome/components/systa_bus/__init__.py create mode 100644 esphome/components/systa_bus/sensor/__init__.py create mode 100644 esphome/components/systa_bus/sensor/systa_bus_sensor.cpp create mode 100644 esphome/components/systa_bus/sensor/systa_bus_sensor.h create mode 100644 esphome/components/systa_bus/systa_bus.cpp create mode 100644 esphome/components/systa_bus/systa_bus.h create mode 100644 tests/components/systa_bus/__init__.py create mode 100644 tests/components/systa_bus/common.h create mode 100644 tests/components/systa_bus/common.yaml create mode 100644 tests/components/systa_bus/systa_bus_test.cpp create mode 100644 tests/components/systa_bus/test.esp32-idf.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 52fec0a7ac..cfe5638a61 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -549,6 +549,7 @@ esphome/components/sx126x/* @swoboda1337 esphome/components/sx127x/* @swoboda1337 esphome/components/sy6970/* @linkedupbits esphome/components/syslog/* @clydebarrow +esphome/components/systa_bus/* @Mat931 esphome/components/t6615/* @tylermenezes esphome/components/tas2780/* @remcom esphome/components/tc74/* @sethgirvan diff --git a/esphome/components/systa_bus/__init__.py b/esphome/components/systa_bus/__init__.py new file mode 100644 index 0000000000..0695b43d41 --- /dev/null +++ b/esphome/components/systa_bus/__init__.py @@ -0,0 +1,41 @@ +import esphome.codegen as cg +from esphome.components import uart +import esphome.config_validation as cv +from esphome.const import CONF_ID +from esphome.cpp_generator import MockObj +from esphome.types import ConfigType + +CODEOWNERS = ["@Mat931"] + +DEPENDENCIES = ["uart"] + +MULTI_CONF = True + +systa_bus_ns = cg.esphome_ns.namespace("systa_bus") +SystaBus = systa_bus_ns.class_("SystaBus", cg.Component, uart.UARTDevice) + +CONF_SYSTA_BUS_ID = "systa_bus_id" + +CONFIG_SCHEMA = uart.UART_DEVICE_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(SystaBus), + } +) + +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "systa_bus", baud_rate=9600, require_rx=True +) + +_request_listener_slot = cg.slot_counter("SYSTA_BUS_LISTENER_COUNT") + + +async def register_systa_bus_listener(systa_bus: MockObj, var: MockObj) -> None: + """Register a listener with its bus and count it for the compile-time listener storage.""" + _request_listener_slot(str(systa_bus)) + cg.add(systa_bus.register_listener(var)) + + +async def to_code(config: ConfigType) -> None: + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) diff --git a/esphome/components/systa_bus/sensor/__init__.py b/esphome/components/systa_bus/sensor/__init__.py new file mode 100644 index 0000000000..3def5fba12 --- /dev/null +++ b/esphome/components/systa_bus/sensor/__init__.py @@ -0,0 +1,81 @@ +import esphome.codegen as cg +from esphome.components import sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + CONF_MODEL, + DEVICE_CLASS_TEMPERATURE, + ICON_PERCENT, + ICON_THERMOMETER, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, +) +from esphome.types import ConfigType + +from .. import CONF_SYSTA_BUS_ID, SystaBus, register_systa_bus_listener, systa_bus_ns + +SystaSolarAquaSensor = systa_bus_ns.class_("SystaSolarAquaSensor", cg.Component) + +CONF_SYSTASOLAR_AQUA = "systasolar_aqua" +CONF_TEMPERATURE_TSA = "temperature_tsa" +CONF_TEMPERATURE_TSE = "temperature_tse" +CONF_TEMPERATURE_TWU = "temperature_twu" +CONF_TEMPERATURE_TW2 = "temperature_tw2" +CONF_PUMP_SPEED = "pump_speed" + +_TEMPERATURE_SCHEMA = sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, +) +_PUMP_SPEED_SCHEMA = sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_PERCENT, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, +) + +# Sensors per model, keyed by config key; the C++ setter is "set__sensor" +MODEL_SENSORS = { + CONF_SYSTASOLAR_AQUA: { + CONF_TEMPERATURE_TSA: _TEMPERATURE_SCHEMA, + CONF_TEMPERATURE_TSE: _TEMPERATURE_SCHEMA, + CONF_TEMPERATURE_TWU: _TEMPERATURE_SCHEMA, + CONF_TEMPERATURE_TW2: _TEMPERATURE_SCHEMA, + CONF_PUMP_SPEED: _PUMP_SPEED_SCHEMA, + }, +} + +CONFIG_SCHEMA = cv.typed_schema( + { + CONF_SYSTASOLAR_AQUA: cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(SystaSolarAquaSensor), + cv.GenerateID(CONF_SYSTA_BUS_ID): cv.use_id(SystaBus), + **{ + cv.Optional(key): schema + for key, schema in MODEL_SENSORS[CONF_SYSTASOLAR_AQUA].items() + }, + } + ), + }, + key=CONF_MODEL, + lower=True, + space="_", +) + + +async def to_code(config: ConfigType) -> None: + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + for key in MODEL_SENSORS[config[CONF_MODEL]]: + if (conf := config.get(key)) is not None: + sens = await sensor.new_sensor(conf) + cg.add(getattr(var, f"set_{key}_sensor")(sens)) + + systa_bus = await cg.get_variable(config[CONF_SYSTA_BUS_ID]) + await register_systa_bus_listener(systa_bus, var) diff --git a/esphome/components/systa_bus/sensor/systa_bus_sensor.cpp b/esphome/components/systa_bus/sensor/systa_bus_sensor.cpp new file mode 100644 index 0000000000..08a4677180 --- /dev/null +++ b/esphome/components/systa_bus/sensor/systa_bus_sensor.cpp @@ -0,0 +1,37 @@ +#include "systa_bus_sensor.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome::systa_bus { + +static const char *const TAG = "systa_bus.sensor"; + +static int16_t get_i16be(std::span message, size_t start) { + return static_cast(encode_uint16(message[start], message[start + 1])); +} + +void SystaSolarAquaSensor::dump_config() { + ESP_LOGCONFIG(TAG, "SystaSolar Aqua:"); + LOG_SENSOR(" ", "Temperature TSA", this->temperature_tsa_sensor_); + LOG_SENSOR(" ", "Temperature TSE", this->temperature_tse_sensor_); + LOG_SENSOR(" ", "Temperature TWU", this->temperature_twu_sensor_); + LOG_SENSOR(" ", "Temperature TW2", this->temperature_tw2_sensor_); + LOG_SENSOR(" ", "Pump Speed", this->pump_speed_sensor_); +} + +void SystaSolarAquaSensor::handle_message(std::span message) { + if (get_message_type(message) != MESSAGE_TYPE_AQUA_SENSOR_DATA) + return; + if (this->temperature_tsa_sensor_ != nullptr) + this->temperature_tsa_sensor_->publish_state(get_i16be(message, 4) * 0.1f); + if (this->temperature_tse_sensor_ != nullptr) + this->temperature_tse_sensor_->publish_state(get_i16be(message, 6) * 0.1f); + if (this->temperature_twu_sensor_ != nullptr) + this->temperature_twu_sensor_->publish_state(get_i16be(message, 8) * 0.1f); + if (this->temperature_tw2_sensor_ != nullptr) + this->temperature_tw2_sensor_->publish_state(get_i16be(message, 10) * 0.1f); + if (this->pump_speed_sensor_ != nullptr) + this->pump_speed_sensor_->publish_state(message[12]); +} + +} // namespace esphome::systa_bus diff --git a/esphome/components/systa_bus/sensor/systa_bus_sensor.h b/esphome/components/systa_bus/sensor/systa_bus_sensor.h new file mode 100644 index 0000000000..3688176d00 --- /dev/null +++ b/esphome/components/systa_bus/sensor/systa_bus_sensor.h @@ -0,0 +1,28 @@ +#pragma once + +#include "../systa_bus.h" +#include "esphome/components/sensor/sensor.h" + +#include + +namespace esphome::systa_bus { + +class SystaSolarAquaSensor : public SystaBusListener, public Component { + public: + void dump_config() override; + void set_temperature_tsa_sensor(sensor::Sensor *sensor) { this->temperature_tsa_sensor_ = sensor; } + void set_temperature_tse_sensor(sensor::Sensor *sensor) { this->temperature_tse_sensor_ = sensor; } + void set_temperature_twu_sensor(sensor::Sensor *sensor) { this->temperature_twu_sensor_ = sensor; } + void set_temperature_tw2_sensor(sensor::Sensor *sensor) { this->temperature_tw2_sensor_ = sensor; } + void set_pump_speed_sensor(sensor::Sensor *sensor) { this->pump_speed_sensor_ = sensor; } + void handle_message(std::span message) override; + + protected: + sensor::Sensor *temperature_tsa_sensor_{nullptr}; + sensor::Sensor *temperature_tse_sensor_{nullptr}; + sensor::Sensor *temperature_twu_sensor_{nullptr}; + sensor::Sensor *temperature_tw2_sensor_{nullptr}; + sensor::Sensor *pump_speed_sensor_{nullptr}; +}; + +} // namespace esphome::systa_bus diff --git a/esphome/components/systa_bus/systa_bus.cpp b/esphome/components/systa_bus/systa_bus.cpp new file mode 100644 index 0000000000..3a2f6717e9 --- /dev/null +++ b/esphome/components/systa_bus/systa_bus.cpp @@ -0,0 +1,70 @@ +#include "systa_bus.h" +#include "esphome/core/log.h" + +#include +#include + +namespace esphome::systa_bus { + +static const char *const TAG = "systa_bus"; + +void SystaBus::dump_config() { ESP_LOGCONFIG(TAG, "SystaBus:"); } + +static bool checksum(std::span data) { + uint8_t csum = 0; + for (uint8_t i : data) + csum += i; + return csum == 0; +} + +void SystaBus::loop() { + uint8_t c; + while (this->available() && this->read_byte(&c)) + this->handle_byte_(c); +} + +void SystaBus::handle_byte_(uint8_t c) { + if (this->buffer_.empty()) { + if (c == START_BYTE) + this->buffer_.push_back(c); + return; + } + this->buffer_.push_back(c); + if (this->buffer_.size() == 2) { + // The length byte is only trusted for known message types; anything else restarts the search + uint16_t message_type = get_message_type(this->buffer_); + if (message_type != MESSAGE_TYPE_AQUA_SENSOR_DATA) { + ESP_LOGV(TAG, "Unknown message type 0x%04x", message_type); + this->buffer_.clear(); + // A stray start byte followed by a real frame: keep this byte as the new start + if (c == START_BYTE) + this->buffer_.push_back(c); + } + return; + } + if (this->buffer_.size() < this->buffer_[1] + FRAME_OVERHEAD) + return; + if (!checksum(this->buffer_)) { + ESP_LOGW(TAG, "Checksum failed"); + this->resync_(); + return; + } +#ifdef SYSTA_BUS_LISTENER_COUNT + for (auto *listener : this->listeners_) + listener->handle_message(this->buffer_); +#endif + this->buffer_.clear(); +} + +// A frame that lost a byte swallows the start of the next one, so the failed bytes are fed back through the +// parser from the second byte on. They are fewer than a full frame, so this cannot fail the checksum again. +void SystaBus::resync_() { + std::array failed; + const size_t count = this->buffer_.size(); + std::copy(this->buffer_.begin(), this->buffer_.end(), failed.begin()); + this->buffer_.clear(); + for (size_t i = 1; i < count; i++) + this->handle_byte_(failed[i]); +} + +} // namespace esphome::systa_bus diff --git a/esphome/components/systa_bus/systa_bus.h b/esphome/components/systa_bus/systa_bus.h new file mode 100644 index 0000000000..99ca31590f --- /dev/null +++ b/esphome/components/systa_bus/systa_bus.h @@ -0,0 +1,50 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "esphome/components/uart/uart.h" + +#include + +namespace esphome::systa_bus { + +// A frame is a start byte, the payload length, the payload and an 8-bit checksum that makes all bytes sum to +// zero. The first two bytes together are the message type, so the type also fixes the frame length. +static constexpr uint8_t START_BYTE = 0xfc; +static constexpr uint8_t FRAME_OVERHEAD = 3; // start byte, length, checksum + +// SystaSolar Aqua sensor data, 25 bytes: [0..1] type, [2..3] not decoded, big-endian int16 in 0.1 degrees at +// [4..5] TSA, [6..7] TSE, [8..9] TWU and [10..11] TW2, pump speed in percent at [12], [13..23] not decoded. +static constexpr uint16_t MESSAGE_TYPE_AQUA_SENSOR_DATA = 0xfc16; + +// The bus only buffers known message types, so the buffer is sized by the longest of them +static constexpr size_t MAX_MESSAGE_SIZE = (MESSAGE_TYPE_AQUA_SENSOR_DATA & 0xff) + FRAME_OVERHEAD; + +inline uint16_t get_message_type(std::span message) { return encode_uint16(message[0], message[1]); } + +class SystaBusListener { + public: + // Called with every frame that passed the checksum; listeners pick their message type + virtual void handle_message(std::span message) = 0; +}; + +class SystaBus : public uart::UARTDevice, public Component { + public: + void dump_config() override; + void loop() override; + +#ifdef SYSTA_BUS_LISTENER_COUNT + void register_listener(SystaBusListener *listener) { this->listeners_.push_back(listener); } +#endif + + protected: + void handle_byte_(uint8_t c); + void resync_(); + +#ifdef SYSTA_BUS_LISTENER_COUNT + StaticVector listeners_; +#endif + StaticVector buffer_; +}; + +} // namespace esphome::systa_bus diff --git a/esphome/core/defines.h b/esphome/core/defines.h index e1ae1d5fb6..75c7a76a40 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -175,6 +175,7 @@ #define USE_REMOTE_PROTOCOL_SYMPHONY #define USE_REMOTE_PROTOCOL_TOSHIBA_AC #define USE_REMOTE_PROTOCOL_TOTO +#define SYSTA_BUS_LISTENER_COUNT 1 #define SERIAL_PROXY_COUNT 2 #define SNTP_SERVER_COUNT 3 #define USE_MEDIA_PLAYER diff --git a/tests/components/systa_bus/__init__.py b/tests/components/systa_bus/__init__.py new file mode 100644 index 0000000000..95273b5ad5 --- /dev/null +++ b/tests/components/systa_bus/__init__.py @@ -0,0 +1,13 @@ +import esphome.codegen as cg +from esphome.types import ConfigType +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + async def to_code_testing(config: ConfigType) -> None: + # Listener storage is sized by code generation; the gtests register one listener per bus. + cg.add_define("SYSTA_BUS_LISTENER_COUNT", 1) + + # A MULTI_CONF component gets no entry in the host build, so its to_code would never run. + manifest.multi_conf = False + manifest.to_code = to_code_testing diff --git a/tests/components/systa_bus/common.h b/tests/components/systa_bus/common.h new file mode 100644 index 0000000000..9824059702 --- /dev/null +++ b/tests/components/systa_bus/common.h @@ -0,0 +1,37 @@ +#pragma once + +#include +#include +#include +#include + +#include "esphome/components/systa_bus/systa_bus.h" +#include "esphome/components/uart/uart_component.h" + +namespace esphome::systa_bus::testing { + +class MockUARTComponent : public uart::UARTComponent { + public: + std::vector rx; + + void push_rx(const std::vector &data) { this->rx.insert(this->rx.end(), data.begin(), data.end()); } + + void write_array(const uint8_t *data, size_t len) override {} + bool read_array(uint8_t *data, size_t len) override { + if (this->rx.size() < len) + return false; + std::copy(this->rx.begin(), this->rx.begin() + len, data); + this->rx.erase(this->rx.begin(), this->rx.begin() + len); + return true; + } + size_t available() override { return this->rx.size(); } + + MOCK_METHOD(bool, peek_byte, (uint8_t * data), (override)); + MOCK_METHOD(uart::UARTFlushResult, flush, (), (override)); + MOCK_METHOD(void, check_logger_conflict, (), (override)); +#if defined(USE_ESP8266) || defined(USE_ESP32) + void load_settings(bool dump_config) override {} +#endif +}; + +} // namespace esphome::systa_bus::testing diff --git a/tests/components/systa_bus/common.yaml b/tests/components/systa_bus/common.yaml new file mode 100644 index 0000000000..ea97e2d840 --- /dev/null +++ b/tests/components/systa_bus/common.yaml @@ -0,0 +1,15 @@ +systa_bus: + +sensor: + - platform: systa_bus + model: systasolar_aqua + temperature_tsa: + name: TSA + temperature_tse: + name: TSE + temperature_twu: + name: TWU + temperature_tw2: + name: TW2 + pump_speed: + name: Pump Speed diff --git a/tests/components/systa_bus/systa_bus_test.cpp b/tests/components/systa_bus/systa_bus_test.cpp new file mode 100644 index 0000000000..3e6de9d280 --- /dev/null +++ b/tests/components/systa_bus/systa_bus_test.cpp @@ -0,0 +1,125 @@ +#include + +#include +#include + +#include "common.h" + +namespace esphome::systa_bus::testing { + +namespace { + +class TestListener final : public SystaBusListener { + public: + void handle_message(std::span message) override { + this->messages.emplace_back(message.begin(), message.end()); + } + std::vector> messages; +}; + +void put_i16be(std::vector &frame, size_t offset, int16_t value) { + frame[offset] = static_cast(static_cast(value) >> 8); + frame[offset + 1] = static_cast(value); +} + +// A 25-byte Aqua sensor frame with a valid checksum +std::vector aqua_frame(int16_t tsa, int16_t tse, int16_t twu, int16_t tw2, uint8_t pump) { + std::vector frame(MAX_MESSAGE_SIZE, 0); + frame[0] = START_BYTE; + frame[1] = MESSAGE_TYPE_AQUA_SENSOR_DATA & 0xff; + put_i16be(frame, 4, tsa); + put_i16be(frame, 6, tse); + put_i16be(frame, 8, twu); + put_i16be(frame, 10, tw2); + frame[12] = pump; + uint8_t sum = 0; + for (size_t i = 0; i + 1 < frame.size(); i++) + sum += frame[i]; + frame.back() = static_cast(0 - sum); + return frame; +} + +class SystaBusTest : public ::testing::Test { + protected: + void SetUp() override { + this->bus_.set_uart_parent(&this->uart_); + this->bus_.register_listener(&this->listener_); + } + void feed_(const std::vector &bytes) { + this->uart_.push_rx(bytes); + this->bus_.loop(); + } + + MockUARTComponent uart_; + SystaBus bus_; + TestListener listener_; +}; + +const std::vector FRAME_A = aqua_frame(512, 301, -15, 700, 60); +const std::vector FRAME_B = aqua_frame(513, 302, -14, 701, 61); + +} // namespace + +TEST_F(SystaBusTest, DecodesAValidFrame) { + this->feed_(FRAME_A); + ASSERT_EQ(this->listener_.messages.size(), 1u); + EXPECT_EQ(this->listener_.messages[0], FRAME_A); +} + +TEST_F(SystaBusTest, IgnoresGarbageBeforeAFrame) { + std::vector bytes = {0x00, 0x16, 0x42, 0xfc}; // includes a stray start byte + bytes.insert(bytes.end(), FRAME_A.begin(), FRAME_A.end()); + this->feed_(bytes); + ASSERT_EQ(this->listener_.messages.size(), 1u); + EXPECT_EQ(this->listener_.messages[0], FRAME_A); +} + +TEST_F(SystaBusTest, SkipsAnUnknownMessageType) { + std::vector bytes = {START_BYTE, 0x1a, 0x01, 0x02, 0x03}; + bytes.insert(bytes.end(), FRAME_A.begin(), FRAME_A.end()); + this->feed_(bytes); + ASSERT_EQ(this->listener_.messages.size(), 1u); + EXPECT_EQ(this->listener_.messages[0], FRAME_A); +} + +TEST_F(SystaBusTest, RejectsACorruptedByte) { + auto bad = FRAME_A; + bad[6] ^= 0x10; + std::vector bytes = bad; + bytes.insert(bytes.end(), FRAME_B.begin(), FRAME_B.end()); + this->feed_(bytes); + ASSERT_EQ(this->listener_.messages.size(), 1u); + EXPECT_EQ(this->listener_.messages[0], FRAME_B); +} + +// A frame that lost a byte swallows the start byte of the next frame; the parser must still find that frame. +TEST_F(SystaBusTest, RecoversTheFrameAfterADroppedByte) { + auto truncated = FRAME_A; + truncated.erase(truncated.begin() + 9); + std::vector bytes = truncated; + bytes.insert(bytes.end(), FRAME_B.begin(), FRAME_B.end()); + this->feed_(bytes); + ASSERT_EQ(this->listener_.messages.size(), 1u); + EXPECT_EQ(this->listener_.messages[0], FRAME_B); +} + +TEST_F(SystaBusTest, DecodesFramesSplitAcrossLoops) { + std::vector first(FRAME_A.begin(), FRAME_A.begin() + 10); + std::vector second(FRAME_A.begin() + 10, FRAME_A.end()); + this->feed_(first); + EXPECT_TRUE(this->listener_.messages.empty()); + this->feed_(second); + ASSERT_EQ(this->listener_.messages.size(), 1u); + EXPECT_EQ(this->listener_.messages[0], FRAME_A); +} + +TEST_F(SystaBusTest, DecodesBackToBackFrames) { + std::vector bytes = FRAME_A; + bytes.insert(bytes.end(), FRAME_B.begin(), FRAME_B.end()); + this->feed_(bytes); + ASSERT_EQ(this->listener_.messages.size(), 2u); + EXPECT_EQ(this->listener_.messages[0], FRAME_A); + EXPECT_EQ(this->listener_.messages[1], FRAME_B); +} + +} // namespace esphome::systa_bus::testing diff --git a/tests/components/systa_bus/test.esp32-idf.yaml b/tests/components/systa_bus/test.esp32-idf.yaml new file mode 100644 index 0000000000..0393d30348 --- /dev/null +++ b/tests/components/systa_bus/test.esp32-idf.yaml @@ -0,0 +1,3 @@ +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml + systa_bus: !include common.yaml