mirror of
https://github.com/esphome/esphome.git
synced 2026-10-02 09:20:22 +00:00
[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 <nick@home-assistant.io> Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
co-authored by
pre-commit-ci-lite[bot]
J. Nick Koston
J. Nick Koston
parent
0d445fb3bc
commit
d2c33b0f6f
@@ -549,6 +549,7 @@ esphome/components/sx126x/* @swoboda1337
|
|||||||
esphome/components/sx127x/* @swoboda1337
|
esphome/components/sx127x/* @swoboda1337
|
||||||
esphome/components/sy6970/* @linkedupbits
|
esphome/components/sy6970/* @linkedupbits
|
||||||
esphome/components/syslog/* @clydebarrow
|
esphome/components/syslog/* @clydebarrow
|
||||||
|
esphome/components/systa_bus/* @Mat931
|
||||||
esphome/components/t6615/* @tylermenezes
|
esphome/components/t6615/* @tylermenezes
|
||||||
esphome/components/tas2780/* @remcom
|
esphome/components/tas2780/* @remcom
|
||||||
esphome/components/tc74/* @sethgirvan
|
esphome/components/tc74/* @sethgirvan
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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_<key>_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)
|
||||||
@@ -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<const uint8_t> message, size_t start) {
|
||||||
|
return static_cast<int16_t>(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<const uint8_t> 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
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "../systa_bus.h"
|
||||||
|
#include "esphome/components/sensor/sensor.h"
|
||||||
|
|
||||||
|
#include <span>
|
||||||
|
|
||||||
|
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<const uint8_t> 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
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
#include "systa_bus.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
|
||||||
|
namespace esphome::systa_bus {
|
||||||
|
|
||||||
|
static const char *const TAG = "systa_bus";
|
||||||
|
|
||||||
|
void SystaBus::dump_config() { ESP_LOGCONFIG(TAG, "SystaBus:"); }
|
||||||
|
|
||||||
|
static bool checksum(std::span<const uint8_t> 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<uint8_t, MAX_MESSAGE_SIZE> 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
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
#include "esphome/components/uart/uart.h"
|
||||||
|
|
||||||
|
#include <span>
|
||||||
|
|
||||||
|
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<const uint8_t> 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<const uint8_t> 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<SystaBusListener *, SYSTA_BUS_LISTENER_COUNT> listeners_;
|
||||||
|
#endif
|
||||||
|
StaticVector<uint8_t, MAX_MESSAGE_SIZE> buffer_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace esphome::systa_bus
|
||||||
@@ -175,6 +175,7 @@
|
|||||||
#define USE_REMOTE_PROTOCOL_SYMPHONY
|
#define USE_REMOTE_PROTOCOL_SYMPHONY
|
||||||
#define USE_REMOTE_PROTOCOL_TOSHIBA_AC
|
#define USE_REMOTE_PROTOCOL_TOSHIBA_AC
|
||||||
#define USE_REMOTE_PROTOCOL_TOTO
|
#define USE_REMOTE_PROTOCOL_TOTO
|
||||||
|
#define SYSTA_BUS_LISTENER_COUNT 1
|
||||||
#define SERIAL_PROXY_COUNT 2
|
#define SERIAL_PROXY_COUNT 2
|
||||||
#define SNTP_SERVER_COUNT 3
|
#define SNTP_SERVER_COUNT 3
|
||||||
#define USE_MEDIA_PLAYER
|
#define USE_MEDIA_PLAYER
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <gmock/gmock.h>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#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<uint8_t> rx;
|
||||||
|
|
||||||
|
void push_rx(const std::vector<uint8_t> &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
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "common.h"
|
||||||
|
|
||||||
|
namespace esphome::systa_bus::testing {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
class TestListener final : public SystaBusListener {
|
||||||
|
public:
|
||||||
|
void handle_message(std::span<const uint8_t> message) override {
|
||||||
|
this->messages.emplace_back(message.begin(), message.end());
|
||||||
|
}
|
||||||
|
std::vector<std::vector<uint8_t>> messages;
|
||||||
|
};
|
||||||
|
|
||||||
|
void put_i16be(std::vector<uint8_t> &frame, size_t offset, int16_t value) {
|
||||||
|
frame[offset] = static_cast<uint8_t>(static_cast<uint16_t>(value) >> 8);
|
||||||
|
frame[offset + 1] = static_cast<uint8_t>(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A 25-byte Aqua sensor frame with a valid checksum
|
||||||
|
std::vector<uint8_t> aqua_frame(int16_t tsa, int16_t tse, int16_t twu, int16_t tw2, uint8_t pump) {
|
||||||
|
std::vector<uint8_t> 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<uint8_t>(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<uint8_t> &bytes) {
|
||||||
|
this->uart_.push_rx(bytes);
|
||||||
|
this->bus_.loop();
|
||||||
|
}
|
||||||
|
|
||||||
|
MockUARTComponent uart_;
|
||||||
|
SystaBus bus_;
|
||||||
|
TestListener listener_;
|
||||||
|
};
|
||||||
|
|
||||||
|
const std::vector<uint8_t> FRAME_A = aqua_frame(512, 301, -15, 700, 60);
|
||||||
|
const std::vector<uint8_t> 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<uint8_t> 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<uint8_t> 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<uint8_t> 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<uint8_t> 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<uint8_t> first(FRAME_A.begin(), FRAME_A.begin() + 10);
|
||||||
|
std::vector<uint8_t> 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<uint8_t> 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
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
packages:
|
||||||
|
uart: !include ../../test_build_components/common/uart/esp32-idf.yaml
|
||||||
|
systa_bus: !include common.yaml
|
||||||
Reference in New Issue
Block a user