mirror of
https://github.com/esphome/esphome.git
synced 2026-07-11 09:25:34 +00:00
Merge branch 'dev' into enhancement/ethernet-spi-interface
This commit is contained in:
@@ -48,7 +48,7 @@ from esphome.coroutine import CoroPriority, coroutine_with_priority
|
||||
import esphome.final_validate as fv
|
||||
from esphome.helpers import copy_file_if_changed, rmtree, write_file_if_changed
|
||||
from esphome.types import ConfigType
|
||||
from esphome.writer import clean_cmake_cache
|
||||
from esphome.writer import clean_build, clean_cmake_cache
|
||||
|
||||
from .boards import BOARDS, STANDARD_BOARDS
|
||||
from .const import ( # noqa
|
||||
@@ -2195,6 +2195,7 @@ def _write_sdkconfig():
|
||||
if write_file_if_changed(internal_path, contents):
|
||||
# internal changed, update real one
|
||||
write_file_if_changed(sdk_path, contents)
|
||||
clean_build(clear_pio_cache=False)
|
||||
|
||||
|
||||
def _write_idf_component_yml():
|
||||
|
||||
@@ -28,7 +28,10 @@ namespace esphome::gpio_expander {
|
||||
template<typename T, uint16_t N, typename P = typename std::conditional<(N > 256), uint16_t, uint8_t>::type>
|
||||
class CachedGpioExpander {
|
||||
public:
|
||||
/// @brief Read the state of the given pin. This will invalidate the cache for the given pin number.
|
||||
/// @brief Read the state of the given pin.
|
||||
/// By default, each read invalidates the pin's cache entry so the next read
|
||||
/// of the same pin triggers a fresh hardware read. When invalidate_on_read
|
||||
/// is disabled, the cache stays valid until explicitly cleared via reset_pin_cache_().
|
||||
/// @param pin Pin number to read
|
||||
/// @return Pin state
|
||||
bool digital_read(P pin) {
|
||||
@@ -36,14 +39,17 @@ class CachedGpioExpander {
|
||||
const T pin_mask = (1 << (pin % BANK_SIZE));
|
||||
// Check if specific pin cache is valid
|
||||
if (this->read_cache_valid_[bank] & pin_mask) {
|
||||
// Invalidate pin
|
||||
this->read_cache_valid_[bank] &= ~pin_mask;
|
||||
if (this->invalidate_on_read_) {
|
||||
// Invalidate pin so next read triggers hardware read
|
||||
this->read_cache_valid_[bank] &= ~pin_mask;
|
||||
}
|
||||
} else {
|
||||
// Read whole bank from hardware
|
||||
if (!this->digital_read_hw(pin))
|
||||
return false;
|
||||
// Mark bank cache as valid except the pin that is being returned now
|
||||
this->read_cache_valid_[bank] = std::numeric_limits<T>::max() & ~pin_mask;
|
||||
// (when not invalidating on read, mark all pins including this one as valid)
|
||||
this->read_cache_valid_[bank] = std::numeric_limits<T>::max() & ~(this->invalidate_on_read_ ? pin_mask : 0);
|
||||
}
|
||||
return this->digital_read_cache(pin);
|
||||
}
|
||||
@@ -71,12 +77,18 @@ class CachedGpioExpander {
|
||||
/// @brief Invalidate cache. This function should be called in component loop().
|
||||
void reset_pin_cache_() { memset(this->read_cache_valid_, 0x00, CACHE_SIZE_BYTES); }
|
||||
|
||||
/// @brief Control whether digital_read() invalidates the pin's cache entry after reading.
|
||||
/// When enabled (default), each read self-invalidates so the next read triggers a hardware read.
|
||||
/// When disabled, cache stays valid until reset_pin_cache_() is explicitly called.
|
||||
void set_invalidate_on_read_(bool invalidate) { this->invalidate_on_read_ = invalidate; }
|
||||
|
||||
static constexpr uint16_t BITS_PER_BYTE = 8;
|
||||
static constexpr uint16_t BANK_SIZE = sizeof(T) * BITS_PER_BYTE;
|
||||
static constexpr size_t BANKS = N / BANK_SIZE;
|
||||
static constexpr size_t CACHE_SIZE_BYTES = BANKS * sizeof(T);
|
||||
|
||||
T read_cache_valid_[BANKS]{0};
|
||||
bool invalidate_on_read_{true};
|
||||
};
|
||||
|
||||
} // namespace esphome::gpio_expander
|
||||
|
||||
@@ -279,6 +279,10 @@ def _final_validate(config):
|
||||
|
||||
from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN
|
||||
|
||||
if config[CONF_BUS_MODE] == TYPE_SINGLE:
|
||||
spi.final_validate_device_schema(DOMAIN, require_miso=False, require_mosi=True)(
|
||||
config
|
||||
)
|
||||
if not requires_buffer(config) and LVGL_DOMAIN not in global_config:
|
||||
# If no drawing methods are configured, and LVGL is not enabled, show a test card
|
||||
config[CONF_SHOW_TEST_CARD] = True
|
||||
@@ -286,7 +290,7 @@ def _final_validate(config):
|
||||
if PSRAM_DOMAIN not in global_config and CONF_BUFFER_SIZE not in config:
|
||||
# If PSRAM is not enabled, choose a small buffer size by default
|
||||
if not requires_buffer(config):
|
||||
return config # No buffer needed, so no need to set a buffer size
|
||||
return # No need to pick a size
|
||||
color_depth = get_color_depth(config)
|
||||
frac = denominator(config)
|
||||
width, height, _offset_width, _offset_height = model.get_dimensions(config)
|
||||
@@ -298,8 +302,6 @@ def _final_validate(config):
|
||||
x for x in range(2, 17) if fraction >= 1 / x
|
||||
)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ static const char *const TAG = "mitsubishi_cn105.driver";
|
||||
|
||||
static constexpr uint32_t WRITE_TIMEOUT_MS = 2000;
|
||||
|
||||
static constexpr size_t REQUEST_PAYLOAD_LEN = 0x10;
|
||||
static constexpr size_t HEADER_LEN = 5;
|
||||
static constexpr uint8_t PREAMBLE = 0xFC;
|
||||
static constexpr uint8_t HEADER_BYTE_1 = 0x01;
|
||||
@@ -15,7 +16,13 @@ static constexpr uint8_t HEADER_BYTE_2 = 0x30;
|
||||
|
||||
static constexpr uint8_t PACKET_TYPE_CONNECT_REQUEST = 0x5A;
|
||||
static constexpr uint8_t PACKET_TYPE_CONNECT_RESPONSE = 0x7A;
|
||||
static constexpr std::array<uint8_t, 2> CONNECT_REQUEST_PAYLOAD = {{0xCA, 0x01}};
|
||||
static constexpr std::array<uint8_t, 2> CONNECT_REQUEST_PAYLOAD = {0xCA, 0x01};
|
||||
|
||||
static constexpr uint8_t PACKET_TYPE_STATUS_REQUEST = 0x42;
|
||||
static constexpr uint8_t PACKET_TYPE_STATUS_RESPONSE = 0x62;
|
||||
static constexpr uint8_t STATUS_MSG_SETTINGS = 0x02;
|
||||
static constexpr uint8_t STATUS_MSG_ROOM_TEMP = 0x03;
|
||||
static constexpr std::array<uint8_t, 2> STATUS_MSG_TYPES = {STATUS_MSG_SETTINGS, STATUS_MSG_ROOM_TEMP};
|
||||
|
||||
static constexpr uint8_t checksum(const uint8_t *bytes, size_t length) {
|
||||
return static_cast<uint8_t>(0xFC - std::accumulate(bytes, bytes + length, uint8_t{0}));
|
||||
@@ -30,19 +37,29 @@ static constexpr auto make_packet(uint8_t type, const std::array<uint8_t, Payloa
|
||||
return packet;
|
||||
}
|
||||
|
||||
static float decode_temperature(int temp_a, int temp_b, int delta) {
|
||||
return temp_b != 0 ? (temp_b - 128) / 2.0f : delta + temp_a;
|
||||
}
|
||||
|
||||
static constexpr auto CONNECT_PACKET = make_packet(PACKET_TYPE_CONNECT_REQUEST, CONNECT_REQUEST_PAYLOAD);
|
||||
|
||||
void MitsubishiCN105::initialize() { this->set_state_(State::CONNECTING); }
|
||||
|
||||
void MitsubishiCN105::update() {
|
||||
bool MitsubishiCN105::update() {
|
||||
if (const auto start = this->status_update_start_ms_;
|
||||
start && (get_loop_time_ms() - *start) >= this->update_interval_ms_) {
|
||||
this->cancel_waiting_and_transition_to_(State::UPDATING_STATUS);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (const auto start = this->write_timeout_start_ms_; start && (get_loop_time_ms() - *start) >= WRITE_TIMEOUT_MS) {
|
||||
this->write_timeout_start_ms_.reset();
|
||||
this->read_pos_ = 0;
|
||||
this->set_state_(State::READ_TIMEOUT);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
this->read_incoming_bytes_();
|
||||
return this->read_incoming_bytes_();
|
||||
}
|
||||
|
||||
void MitsubishiCN105::set_state_(State new_state) {
|
||||
@@ -63,9 +80,24 @@ bool MitsubishiCN105::should_transition(State from, State to) {
|
||||
return from == State::NOT_CONNECTED || from == State::READ_TIMEOUT;
|
||||
|
||||
case State::CONNECTED:
|
||||
case State::READ_TIMEOUT:
|
||||
return from == State::CONNECTING;
|
||||
|
||||
case State::UPDATING_STATUS:
|
||||
return from == State::CONNECTED || from == State::STATUS_UPDATED ||
|
||||
from == State::WAITING_FOR_SCHEDULED_STATUS_UPDATE;
|
||||
|
||||
case State::STATUS_UPDATED:
|
||||
return from == State::UPDATING_STATUS;
|
||||
|
||||
case State::SCHEDULE_NEXT_STATUS_UPDATE:
|
||||
return from == State::STATUS_UPDATED;
|
||||
|
||||
case State::WAITING_FOR_SCHEDULED_STATUS_UPDATE:
|
||||
return from == State::SCHEDULE_NEXT_STATUS_UPDATE;
|
||||
|
||||
case State::READ_TIMEOUT:
|
||||
return from == State::UPDATING_STATUS || from == State::CONNECTING;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -79,7 +111,30 @@ void MitsubishiCN105::did_transition_(State to) {
|
||||
|
||||
case State::CONNECTED:
|
||||
this->write_timeout_start_ms_.reset();
|
||||
// TODO: read AC status after connected, next PR
|
||||
this->status_msg_index_ = 0;
|
||||
this->set_state_(State::UPDATING_STATUS);
|
||||
break;
|
||||
|
||||
case State::UPDATING_STATUS:
|
||||
this->update_status_();
|
||||
break;
|
||||
|
||||
case State::STATUS_UPDATED: {
|
||||
this->write_timeout_start_ms_.reset();
|
||||
if (++this->status_msg_index_ >= STATUS_MSG_TYPES.size()) {
|
||||
this->status_msg_index_ = 0;
|
||||
}
|
||||
if (this->status_msg_index_ != 0) {
|
||||
this->set_state_(State::UPDATING_STATUS);
|
||||
} else {
|
||||
this->set_state_(State::SCHEDULE_NEXT_STATUS_UPDATE);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case State::SCHEDULE_NEXT_STATUS_UPDATE:
|
||||
this->status_update_start_ms_ = get_loop_time_ms();
|
||||
this->set_state_(State::WAITING_FOR_SCHEDULED_STATUS_UPDATE);
|
||||
break;
|
||||
|
||||
case State::READ_TIMEOUT:
|
||||
@@ -97,13 +152,24 @@ void MitsubishiCN105::send_packet_(const uint8_t *packet, size_t len) {
|
||||
this->write_timeout_start_ms_ = get_loop_time_ms();
|
||||
}
|
||||
|
||||
void MitsubishiCN105::read_incoming_bytes_() {
|
||||
void MitsubishiCN105::update_status_() {
|
||||
ESP_LOGV(TAG, "Requesting status update, index=%u", this->status_msg_index_);
|
||||
std::array<uint8_t, REQUEST_PAYLOAD_LEN> payload = {STATUS_MSG_TYPES[this->status_msg_index_]};
|
||||
this->send_packet_(make_packet(PACKET_TYPE_STATUS_REQUEST, payload));
|
||||
}
|
||||
|
||||
void MitsubishiCN105::cancel_waiting_and_transition_to_(State state) {
|
||||
this->status_update_start_ms_.reset();
|
||||
this->set_state_(state);
|
||||
}
|
||||
|
||||
bool MitsubishiCN105::read_incoming_bytes_() {
|
||||
uint8_t watchdog = 64;
|
||||
while (this->device_.available() > 0 && watchdog-- > 0) {
|
||||
uint8_t &value = this->read_buffer_[this->read_pos_];
|
||||
if (!this->device_.read_byte(&value)) {
|
||||
ESP_LOGW(TAG, "UART read failed while data available");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (++this->read_pos_) {
|
||||
@@ -149,23 +215,85 @@ void MitsubishiCN105::read_incoming_bytes_() {
|
||||
continue;
|
||||
}
|
||||
|
||||
this->process_rx_packet_(this->read_buffer_[1], this->read_buffer_ + HEADER_LEN, len_without_checksum - HEADER_LEN);
|
||||
bool processed = this->process_rx_packet_(this->read_buffer_[1], this->read_buffer_ + HEADER_LEN,
|
||||
len_without_checksum - HEADER_LEN);
|
||||
this->reset_read_position_and_dump_buffer_("RX");
|
||||
return processed;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void MitsubishiCN105::process_rx_packet_(uint8_t type, const uint8_t *payload, size_t len) {
|
||||
bool MitsubishiCN105::process_rx_packet_(uint8_t type, const uint8_t *payload, size_t len) {
|
||||
switch (type) {
|
||||
case PACKET_TYPE_CONNECT_RESPONSE:
|
||||
this->set_state_(State::CONNECTED);
|
||||
break;
|
||||
return false;
|
||||
|
||||
case PACKET_TYPE_STATUS_RESPONSE:
|
||||
return this->process_status_packet_(payload, len);
|
||||
|
||||
default:
|
||||
ESP_LOGVV(TAG, "RX unknown packet type 0x%02X", type);
|
||||
break;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool MitsubishiCN105::process_status_packet_(const uint8_t *payload, size_t len) {
|
||||
if (len == 0) {
|
||||
ESP_LOGVV(TAG, "RX status packet too short");
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto previous = this->status_;
|
||||
const auto msg_type = payload[0];
|
||||
if (!this->parse_status_payload_(msg_type, payload + 1, len - 1)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (msg_type == STATUS_MSG_TYPES[this->status_msg_index_]) {
|
||||
this->set_state_(State::STATUS_UPDATED);
|
||||
}
|
||||
|
||||
return previous != this->status_ && this->is_status_initialized();
|
||||
}
|
||||
|
||||
bool MitsubishiCN105::parse_status_payload_(uint8_t msg_type, const uint8_t *payload, size_t len) {
|
||||
switch (msg_type) {
|
||||
case STATUS_MSG_SETTINGS:
|
||||
return this->parse_status_settings_(payload, len);
|
||||
|
||||
case STATUS_MSG_ROOM_TEMP:
|
||||
return this->parse_status_room_temperature_(payload, len);
|
||||
|
||||
default:
|
||||
ESP_LOGVV(TAG, "RX unsupported status msg type 0x%02X", msg_type);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool MitsubishiCN105::parse_status_settings_(const uint8_t *payload, size_t len) {
|
||||
if (len <= 10) {
|
||||
ESP_LOGVV(TAG, "RX settings payload too short");
|
||||
return false;
|
||||
}
|
||||
|
||||
this->status_.power_on = payload[2] != 0;
|
||||
this->status_.target_temperature = decode_temperature(-payload[4], payload[10], 31);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MitsubishiCN105::parse_status_room_temperature_(const uint8_t *payload, size_t len) {
|
||||
if (len <= 5) {
|
||||
ESP_LOGVV(TAG, "RX room temperature payload too short");
|
||||
return false;
|
||||
}
|
||||
|
||||
this->status_.room_temperature = decode_temperature(payload[2], payload[5], 10);
|
||||
return true;
|
||||
}
|
||||
|
||||
void MitsubishiCN105::reset_read_position_and_dump_buffer_(const char *prefix) {
|
||||
dump_buffer_vv(prefix, this->read_buffer_, this->read_pos_);
|
||||
this->read_pos_ = 0;
|
||||
@@ -186,6 +314,14 @@ const LogString *MitsubishiCN105::state_to_string(State state) {
|
||||
return LOG_STR("Connecting");
|
||||
case State::CONNECTED:
|
||||
return LOG_STR("Connected");
|
||||
case State::UPDATING_STATUS:
|
||||
return LOG_STR("UpdatingStatus");
|
||||
case State::STATUS_UPDATED:
|
||||
return LOG_STR("StatusUpdated");
|
||||
case State::SCHEDULE_NEXT_STATUS_UPDATE:
|
||||
return LOG_STR("ScheduleNextStatusUpdate");
|
||||
case State::WAITING_FOR_SCHEDULED_STATUS_UPDATE:
|
||||
return LOG_STR("WaitingForScheduledStatusUpdate");
|
||||
case State::READ_TIMEOUT:
|
||||
return LOG_STR("ReadTimeout");
|
||||
}
|
||||
|
||||
@@ -9,23 +9,49 @@ uint32_t get_loop_time_ms();
|
||||
|
||||
class MitsubishiCN105 {
|
||||
public:
|
||||
struct Status {
|
||||
bool operator==(const Status &) const = default;
|
||||
|
||||
bool power_on{false};
|
||||
float target_temperature{NAN};
|
||||
float room_temperature{NAN};
|
||||
};
|
||||
|
||||
explicit MitsubishiCN105(uart::UARTDevice &device) : device_(device) {}
|
||||
|
||||
void initialize();
|
||||
void update();
|
||||
bool update();
|
||||
|
||||
uint32_t get_update_interval() const { return this->update_interval_ms_; }
|
||||
void set_update_interval(uint32_t interval_ms) { this->update_interval_ms_ = interval_ms; }
|
||||
|
||||
const Status &status() const { return this->status_; }
|
||||
bool is_status_initialized() const { return !std::isnan(status_.room_temperature); }
|
||||
|
||||
protected:
|
||||
enum class State : uint8_t { NOT_CONNECTED, CONNECTING, CONNECTED, READ_TIMEOUT };
|
||||
enum class State : uint8_t {
|
||||
NOT_CONNECTED,
|
||||
CONNECTING,
|
||||
CONNECTED,
|
||||
UPDATING_STATUS,
|
||||
STATUS_UPDATED,
|
||||
SCHEDULE_NEXT_STATUS_UPDATE,
|
||||
WAITING_FOR_SCHEDULED_STATUS_UPDATE,
|
||||
READ_TIMEOUT
|
||||
};
|
||||
|
||||
void set_state_(State new_state);
|
||||
void did_transition_(State to);
|
||||
void read_incoming_bytes_();
|
||||
void process_rx_packet_(uint8_t type, const uint8_t *payload, size_t len);
|
||||
bool read_incoming_bytes_();
|
||||
bool process_rx_packet_(uint8_t type, const uint8_t *payload, size_t len);
|
||||
bool process_status_packet_(const uint8_t *payload, size_t len);
|
||||
bool parse_status_payload_(uint8_t msg_type, const uint8_t *payload, size_t len);
|
||||
bool parse_status_settings_(const uint8_t *payload, size_t len);
|
||||
bool parse_status_room_temperature_(const uint8_t *payload, size_t len);
|
||||
void reset_read_position_and_dump_buffer_(const char *prefix);
|
||||
void send_packet_(const uint8_t *packet, size_t len);
|
||||
void update_status_();
|
||||
void cancel_waiting_and_transition_to_(State state);
|
||||
template<typename T> void send_packet_(const T &packet) { this->send_packet_(packet.data(), packet.size()); }
|
||||
static bool should_transition(State from, State to);
|
||||
static const LogString *state_to_string(State state);
|
||||
@@ -34,7 +60,10 @@ class MitsubishiCN105 {
|
||||
uart::UARTDevice &device_;
|
||||
uint32_t update_interval_ms_{1000};
|
||||
std::optional<uint32_t> write_timeout_start_ms_;
|
||||
std::optional<uint32_t> status_update_start_ms_;
|
||||
Status status_{};
|
||||
State state_{State::NOT_CONNECTED};
|
||||
uint8_t status_msg_index_{0};
|
||||
|
||||
private:
|
||||
static constexpr size_t READ_BUFFER_SIZE = 32;
|
||||
|
||||
@@ -17,13 +17,34 @@ void MitsubishiCN105Climate::dump_config() {
|
||||
|
||||
void MitsubishiCN105Climate::setup() { this->hp_.initialize(); }
|
||||
|
||||
void MitsubishiCN105Climate::loop() { this->hp_.update(); }
|
||||
void MitsubishiCN105Climate::loop() {
|
||||
if (this->hp_.update()) {
|
||||
this->apply_values_();
|
||||
}
|
||||
}
|
||||
|
||||
climate::ClimateTraits MitsubishiCN105Climate::traits() {
|
||||
climate::ClimateTraits traits;
|
||||
|
||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
|
||||
|
||||
traits.set_visual_min_temperature(16.0f);
|
||||
traits.set_visual_max_temperature(31.0f);
|
||||
traits.set_visual_temperature_step(1.0f);
|
||||
traits.set_visual_current_temperature_step(0.5f);
|
||||
|
||||
return traits;
|
||||
}
|
||||
|
||||
void MitsubishiCN105Climate::control(const climate::ClimateCall &call) {}
|
||||
|
||||
void MitsubishiCN105Climate::apply_values_() {
|
||||
const auto &status = this->hp_.status();
|
||||
|
||||
this->target_temperature = status.target_temperature;
|
||||
this->current_temperature = status.room_temperature;
|
||||
|
||||
this->publish_state();
|
||||
}
|
||||
|
||||
} // namespace esphome::mitsubishi_cn105
|
||||
|
||||
@@ -21,6 +21,8 @@ class MitsubishiCN105Climate : public climate::Climate, public Component, public
|
||||
void set_update_interval(uint32_t ms) { hp_.set_update_interval(ms); }
|
||||
|
||||
protected:
|
||||
void apply_values_();
|
||||
|
||||
MitsubishiCN105 hp_;
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
|
||||
namespace esphome::mitsubishi_cn105 {
|
||||
|
||||
uint32_t __attribute__((weak)) get_loop_time_ms() { return App.get_loop_component_start_time(); };
|
||||
uint32_t __attribute__((weak)) get_loop_time_ms() { return App.get_loop_component_start_time(); }
|
||||
|
||||
} // namespace esphome::mitsubishi_cn105
|
||||
|
||||
@@ -5,6 +5,7 @@ import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_INPUT,
|
||||
CONF_INTERRUPT_PIN,
|
||||
CONF_INVERTED,
|
||||
CONF_MODE,
|
||||
CONF_NUMBER,
|
||||
@@ -29,6 +30,7 @@ CONFIG_SCHEMA = (
|
||||
{
|
||||
cv.Required(CONF_ID): cv.declare_id(PCA9554Component),
|
||||
cv.Optional(CONF_PIN_COUNT, default=8): cv.one_of(4, 8, 16),
|
||||
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
@@ -43,6 +45,8 @@ async def to_code(config):
|
||||
cg.add(var.set_pin_count(config[CONF_PIN_COUNT]))
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
if interrupt_pin := config.get(CONF_INTERRUPT_PIN):
|
||||
cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin)))
|
||||
|
||||
|
||||
def validate_mode(value):
|
||||
|
||||
@@ -34,12 +34,26 @@ void PCA9554Component::setup() {
|
||||
this->read_inputs_();
|
||||
ESP_LOGD(TAG, "Initialization complete. Warning: %d, Error: %d", this->status_has_warning(),
|
||||
this->status_has_error());
|
||||
}
|
||||
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
this->interrupt_pin_->setup();
|
||||
this->interrupt_pin_->attach_interrupt(&PCA9554Component::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
|
||||
// Don't invalidate cache on read — only invalidate when interrupt fires
|
||||
this->set_invalidate_on_read_(false);
|
||||
}
|
||||
// Disable loop until an input pin is configured via pin_mode()
|
||||
// For interrupt-driven mode, loop is re-enabled by the ISR
|
||||
// For polling mode, loop is re-enabled when pin_mode() registers an input pin
|
||||
this->disable_loop();
|
||||
}
|
||||
void IRAM_ATTR PCA9554Component::gpio_intr(PCA9554Component *arg) { arg->enable_loop_soon_any_context(); }
|
||||
void PCA9554Component::loop() {
|
||||
// Invalidate the cache at the start of each loop.
|
||||
// The actual read will happen on demand when digital_read() is called
|
||||
// Invalidate the cache so the next digital_read() triggers a fresh I2C read
|
||||
this->reset_pin_cache_();
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
// Interrupt-driven: disable loop until next interrupt fires
|
||||
this->disable_loop();
|
||||
}
|
||||
}
|
||||
|
||||
void PCA9554Component::dump_config() {
|
||||
@@ -47,6 +61,7 @@ void PCA9554Component::dump_config() {
|
||||
"PCA9554:\n"
|
||||
" I/O Pins: %d",
|
||||
this->pin_count_);
|
||||
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
|
||||
LOG_I2C_DEVICE(this)
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
@@ -76,6 +91,11 @@ void PCA9554Component::pin_mode(uint8_t pin, gpio::Flags flags) {
|
||||
if (flags == gpio::FLAG_INPUT) {
|
||||
// Clear mode mask bit
|
||||
this->config_mask_ &= ~(1 << pin);
|
||||
// Enable polling loop for input pins (not needed for interrupt-driven mode
|
||||
// where the ISR handles re-enabling loop)
|
||||
if (this->interrupt_pin_ == nullptr) {
|
||||
this->enable_loop();
|
||||
}
|
||||
} else if (flags == gpio::FLAG_OUTPUT) {
|
||||
// Set mode mask bit
|
||||
this->config_mask_ |= 1 << pin;
|
||||
|
||||
@@ -16,7 +16,6 @@ class PCA9554Component : public Component,
|
||||
|
||||
/// Check i2c availability and setup masks
|
||||
void setup() override;
|
||||
/// Invalidate cache at start of each loop
|
||||
void loop() override;
|
||||
/// Helper function to set the pin mode of a pin.
|
||||
void pin_mode(uint8_t pin, gpio::Flags flags);
|
||||
@@ -26,8 +25,11 @@ class PCA9554Component : public Component,
|
||||
void dump_config() override;
|
||||
|
||||
void set_pin_count(size_t pin_count) { this->pin_count_ = pin_count; }
|
||||
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
|
||||
|
||||
protected:
|
||||
static void IRAM_ATTR gpio_intr(PCA9554Component *arg);
|
||||
|
||||
bool read_inputs_();
|
||||
bool write_register_(uint8_t reg, uint16_t value);
|
||||
|
||||
@@ -48,6 +50,7 @@ class PCA9554Component : public Component,
|
||||
uint16_t input_mask_{0x00};
|
||||
/// Storage for last I2C error seen
|
||||
esphome::i2c::ErrorCode last_error_;
|
||||
InternalGPIOPin *interrupt_pin_{nullptr};
|
||||
};
|
||||
|
||||
/// Helper class to expose a PCA9554 pin as an internal input GPIO pin.
|
||||
|
||||
@@ -5,6 +5,7 @@ import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_INPUT,
|
||||
CONF_INTERRUPT_PIN,
|
||||
CONF_INVERTED,
|
||||
CONF_MODE,
|
||||
CONF_NUMBER,
|
||||
@@ -27,6 +28,7 @@ CONFIG_SCHEMA = (
|
||||
{
|
||||
cv.Required(CONF_ID): cv.declare_id(PCF8574Component),
|
||||
cv.Optional(CONF_PCF8575, default=False): cv.boolean,
|
||||
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
@@ -39,6 +41,8 @@ async def to_code(config):
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
cg.add(var.set_pcf8575(config[CONF_PCF8575]))
|
||||
if interrupt_pin := config.get(CONF_INTERRUPT_PIN):
|
||||
cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin)))
|
||||
|
||||
|
||||
def validate_mode(value):
|
||||
|
||||
@@ -15,16 +15,33 @@ void PCF8574Component::setup() {
|
||||
|
||||
this->write_gpio_();
|
||||
this->read_gpio_();
|
||||
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
this->interrupt_pin_->setup();
|
||||
this->interrupt_pin_->attach_interrupt(&PCF8574Component::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
|
||||
// Don't invalidate cache on read — only invalidate when interrupt fires
|
||||
this->set_invalidate_on_read_(false);
|
||||
}
|
||||
// Disable loop until an input pin is configured via pin_mode()
|
||||
// For interrupt-driven mode, loop is re-enabled by the ISR
|
||||
// For polling mode, loop is re-enabled when pin_mode() registers an input pin
|
||||
this->disable_loop();
|
||||
}
|
||||
void IRAM_ATTR PCF8574Component::gpio_intr(PCF8574Component *arg) { arg->enable_loop_soon_any_context(); }
|
||||
void PCF8574Component::loop() {
|
||||
// Invalidate the cache at the start of each loop
|
||||
// Invalidate the cache so the next digital_read() triggers a fresh I2C read
|
||||
this->reset_pin_cache_();
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
// Interrupt-driven: disable loop until next interrupt fires
|
||||
this->disable_loop();
|
||||
}
|
||||
}
|
||||
void PCF8574Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"PCF8574:\n"
|
||||
" Is PCF8575: %s",
|
||||
YESNO(this->pcf8575_));
|
||||
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
|
||||
LOG_I2C_DEVICE(this)
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
@@ -51,6 +68,11 @@ void PCF8574Component::pin_mode(uint8_t pin, gpio::Flags flags) {
|
||||
this->mode_mask_ &= ~(1 << pin);
|
||||
// Write GPIO to enable input mode
|
||||
this->write_gpio_();
|
||||
// Enable polling loop for input pins (not needed for interrupt-driven mode
|
||||
// where the ISR handles re-enabling loop)
|
||||
if (this->interrupt_pin_ == nullptr) {
|
||||
this->enable_loop();
|
||||
}
|
||||
} else if (flags == gpio::FLAG_OUTPUT) {
|
||||
// Set mode mask bit
|
||||
this->mode_mask_ |= 1 << pin;
|
||||
|
||||
@@ -17,10 +17,10 @@ class PCF8574Component : public Component,
|
||||
PCF8574Component() = default;
|
||||
|
||||
void set_pcf8575(bool pcf8575) { pcf8575_ = pcf8575; }
|
||||
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
|
||||
|
||||
/// Check i2c availability and setup masks
|
||||
void setup() override;
|
||||
/// Invalidate cache at start of each loop
|
||||
void loop() override;
|
||||
/// Helper function to set the pin mode of a pin.
|
||||
void pin_mode(uint8_t pin, gpio::Flags flags);
|
||||
@@ -30,6 +30,8 @@ class PCF8574Component : public Component,
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
static void IRAM_ATTR gpio_intr(PCF8574Component *arg);
|
||||
|
||||
bool digital_read_hw(uint8_t pin) override;
|
||||
bool digital_read_cache(uint8_t pin) override;
|
||||
void digital_write_hw(uint8_t pin, bool value) override;
|
||||
@@ -44,6 +46,7 @@ class PCF8574Component : public Component,
|
||||
/// The state read in read_gpio_ - 1 means HIGH, 0 means LOW
|
||||
uint16_t input_mask_{0x00};
|
||||
bool pcf8575_; ///< TRUE->16-channel PCF8575, FALSE->8-channel PCF8574
|
||||
InternalGPIOPin *interrupt_pin_{nullptr};
|
||||
};
|
||||
|
||||
/// Helper class to expose a PCF8574 pin as an internal input GPIO pin.
|
||||
|
||||
@@ -68,7 +68,7 @@ void SPIComponent::dump_config() {
|
||||
LOG_PIN(" SDI Pin: ", this->sdi_pin_);
|
||||
LOG_PIN(" SDO Pin: ", this->sdo_pin_);
|
||||
for (size_t i = 0; i != this->data_pins_.size(); i++) {
|
||||
ESP_LOGCONFIG(TAG, " Data pin %u: GPIO%d", i, this->data_pins_[i]);
|
||||
ESP_LOGCONFIG(TAG, " Data pin %zu: GPIO%d", i, this->data_pins_[i]);
|
||||
}
|
||||
if (this->spi_bus_->is_hw()) {
|
||||
ESP_LOGCONFIG(TAG, " Using HW SPI: %s", this->interface_name_);
|
||||
@@ -118,4 +118,12 @@ uint16_t SPIDelegateBitBash::transfer_(uint16_t data, size_t num_bits) {
|
||||
return out_data;
|
||||
}
|
||||
|
||||
#if !defined(USE_ESP32) && !defined(USE_ARDUINO)
|
||||
// Stub for unsupported platforms (host, Zephyr, etc.) - hardware SPI is unavailable
|
||||
SPIBus *SPIComponent::get_bus(SPIInterface interface, GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi,
|
||||
const std::vector<uint8_t> &data_pins) {
|
||||
return nullptr;
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace esphome::spi
|
||||
|
||||
@@ -23,9 +23,9 @@ using SPIInterface = SPIClassRP2040 *;
|
||||
using SPIInterface = SPIClass *;
|
||||
#endif
|
||||
|
||||
#elif defined(CLANG_TIDY)
|
||||
#elif defined(USE_HOST) || defined(CLANG_TIDY)
|
||||
|
||||
using SPIInterface = void *; // Stub for platforms without SPI (e.g., Zephyr)
|
||||
using SPIInterface = void *; // Stub for platforms without SPI (e.g., host, Zephyr)
|
||||
|
||||
#endif // USE_ESP32 / USE_ARDUINO
|
||||
|
||||
|
||||
@@ -20,7 +20,12 @@ bool CronTrigger::matches(const ESPTime &time) {
|
||||
return time.is_valid() && this->seconds_[time.second] && this->minutes_[time.minute] && this->hours_[time.hour] &&
|
||||
this->days_of_month_[time.day_of_month] && this->months_[time.month] && this->days_of_week_[time.day_of_week];
|
||||
}
|
||||
void CronTrigger::loop() {
|
||||
void CronTrigger::setup() {
|
||||
// Cron resolution is 1 second — check once per second instead of every loop iteration
|
||||
this->set_interval(1000, [this]() { this->check_time_(); });
|
||||
}
|
||||
|
||||
void CronTrigger::check_time_() {
|
||||
ESPTime time = this->rtc_->now();
|
||||
if (!time.is_valid())
|
||||
return;
|
||||
|
||||
@@ -26,10 +26,11 @@ class CronTrigger : public Trigger<>, public Component {
|
||||
void add_day_of_week(uint8_t day_of_week);
|
||||
void add_days_of_week(const std::vector<uint8_t> &days_of_week);
|
||||
bool matches(const ESPTime &time);
|
||||
void loop() override;
|
||||
void setup() override;
|
||||
float get_setup_priority() const override;
|
||||
|
||||
protected:
|
||||
void check_time_();
|
||||
std::bitset<61> seconds_;
|
||||
std::bitset<60> minutes_;
|
||||
std::bitset<24> hours_;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Tests for mpip_spi configuration validation."""
|
||||
|
||||
from collections.abc import Callable, Generator
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -12,6 +13,16 @@ from esphome.core import CORE
|
||||
from esphome.pins import gpio_pin_schema
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_spi_final_validate():
|
||||
"""Mock spi.final_validate_device_schema since unit tests have no real SPI bus config."""
|
||||
with mock.patch(
|
||||
"esphome.components.spi.final_validate_device_schema",
|
||||
return_value=lambda config: None,
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def choose_variant_with_pins() -> Generator[Callable[[list], None]]:
|
||||
"""
|
||||
|
||||
@@ -25,7 +25,9 @@ from tests.component_tests.types import SetCoreConfigCallable
|
||||
|
||||
def validated_config(config):
|
||||
"""Run schema + final validation and return the validated config."""
|
||||
return FINAL_VALIDATE_SCHEMA(CONFIG_SCHEMA(config))
|
||||
config = CONFIG_SCHEMA(config)
|
||||
FINAL_VALIDATE_SCHEMA(config)
|
||||
return config
|
||||
|
||||
|
||||
def test_metadata_native_quad_default_test_card(
|
||||
|
||||
@@ -25,27 +25,80 @@ TEST(MitsubishiCN105Tests, InitSendsConnectPacket) {
|
||||
EXPECT_EQ(ctx.sut.write_timeout_start_ms_, std::optional<uint32_t>{123});
|
||||
}
|
||||
|
||||
TEST(MitsubishiCN105Tests, SuccessfullyConnects) {
|
||||
TEST(MitsubishiCN105Tests, ConnectAndUpdateStatus) {
|
||||
auto ctx = TestContext{};
|
||||
|
||||
ctx.sut.initialize();
|
||||
ctx.uart.tx.clear(); // Remove first connect packet bytes
|
||||
|
||||
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING);
|
||||
EXPECT_TRUE(ctx.sut.write_timeout_start_ms_.has_value());
|
||||
EXPECT_EQ(ctx.sut.write_timeout_start_ms_, std::optional<uint32_t>{0});
|
||||
EXPECT_FALSE(ctx.sut.status_update_start_ms_.has_value());
|
||||
|
||||
// Connect response
|
||||
ctx.uart.push_rx({0xFC, 0x7A, 0x01, 0x30, 0x00, 0x55});
|
||||
|
||||
ctx.sut.update();
|
||||
ctx.sut.set_current_time(200);
|
||||
ASSERT_FALSE(ctx.sut.update());
|
||||
|
||||
// All bytes from UART should be consumed and state = CONNECTED
|
||||
// All bytes from UART should be consumed
|
||||
EXPECT_TRUE(ctx.uart.rx.empty());
|
||||
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTED);
|
||||
EXPECT_FALSE(ctx.sut.write_timeout_start_ms_.has_value());
|
||||
// After successful connect we request status, first settings (0x02)
|
||||
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::UPDATING_STATUS);
|
||||
EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x42, 0x01, 0x30, 0x10, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7B));
|
||||
EXPECT_EQ(ctx.sut.write_timeout_start_ms_, std::optional<uint32_t>{200});
|
||||
EXPECT_FALSE(ctx.sut.status_update_start_ms_.has_value());
|
||||
|
||||
// Clear TX bytes.
|
||||
ctx.uart.tx.clear();
|
||||
|
||||
// Settings response
|
||||
ctx.uart.push_rx({0xFC, 0x62, 0x01, 0x30, 0x10, 0x02, 0x00, 0x00, 0x00, 0x08, 0x07,
|
||||
0x00, 0x00, 0x00, 0x00, 0x03, 0xB0, 0x00, 0x00, 0x00, 0x00, 0x99});
|
||||
|
||||
// Settings should still have initial values
|
||||
EXPECT_FALSE(ctx.sut.status().power_on);
|
||||
EXPECT_THAT(ctx.sut.status().target_temperature, ::testing::IsNan());
|
||||
|
||||
ctx.sut.set_current_time(300);
|
||||
ASSERT_FALSE(ctx.sut.update());
|
||||
EXPECT_TRUE(ctx.uart.rx.empty());
|
||||
|
||||
// Check settings that we just read from received package
|
||||
EXPECT_FALSE(ctx.sut.status().power_on);
|
||||
EXPECT_EQ(ctx.sut.status().target_temperature, 24.0f);
|
||||
|
||||
// Now fetch room temperature (0x03)
|
||||
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::UPDATING_STATUS);
|
||||
EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x42, 0x01, 0x30, 0x10, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7A));
|
||||
EXPECT_EQ(ctx.sut.write_timeout_start_ms_, std::optional<uint32_t>{300});
|
||||
EXPECT_FALSE(ctx.sut.status_update_start_ms_.has_value());
|
||||
|
||||
// Clear TX bytes.
|
||||
ctx.uart.tx.clear();
|
||||
|
||||
// Room temperature response
|
||||
ctx.uart.push_rx({0xFC, 0x62, 0x01, 0x30, 0x10, 0x03, 0x00, 0x00, 0x0B, 0x00, 0x00,
|
||||
0xAA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA5});
|
||||
|
||||
// Room temperature should still have initial value
|
||||
EXPECT_THAT(ctx.sut.status().room_temperature, ::testing::IsNan());
|
||||
|
||||
ctx.sut.set_current_time(400);
|
||||
EXPECT_FALSE(ctx.sut.is_status_initialized());
|
||||
ASSERT_TRUE(ctx.sut.update());
|
||||
EXPECT_TRUE(ctx.uart.rx.empty());
|
||||
EXPECT_TRUE(ctx.sut.is_status_initialized());
|
||||
|
||||
// Check room temperature we just read from received package
|
||||
EXPECT_EQ(ctx.sut.status().room_temperature, 21.0f);
|
||||
|
||||
// Nothing should be send to UART
|
||||
EXPECT_TRUE(ctx.uart.tx.empty());
|
||||
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::WAITING_FOR_SCHEDULED_STATUS_UPDATE);
|
||||
EXPECT_FALSE(ctx.sut.write_timeout_start_ms_.has_value());
|
||||
EXPECT_EQ(ctx.sut.status_update_start_ms_, std::optional<uint32_t>{400});
|
||||
}
|
||||
|
||||
TEST(MitsubishiCN105Tests, NoResponseTriggersReconnect) {
|
||||
@@ -55,21 +108,21 @@ TEST(MitsubishiCN105Tests, NoResponseTriggersReconnect) {
|
||||
ctx.uart.tx.clear(); // Remove first connect packet bytes
|
||||
|
||||
// No response (no RX data), no retry yet
|
||||
ctx.sut.update();
|
||||
ASSERT_FALSE(ctx.sut.update());
|
||||
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING);
|
||||
EXPECT_TRUE(ctx.uart.tx.empty());
|
||||
EXPECT_EQ(ctx.sut.write_timeout_start_ms_, std::optional<uint32_t>{0});
|
||||
|
||||
// Still no response after 1999ms, no retry yet
|
||||
ctx.sut.set_current_time(1999);
|
||||
ctx.sut.update();
|
||||
ASSERT_FALSE(ctx.sut.update());
|
||||
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING);
|
||||
EXPECT_TRUE(ctx.uart.tx.empty());
|
||||
EXPECT_EQ(ctx.sut.write_timeout_start_ms_, std::optional<uint32_t>{0});
|
||||
|
||||
// Stop waiting after 2s and retry connect
|
||||
ctx.sut.set_current_time(2000);
|
||||
ctx.sut.update();
|
||||
ASSERT_FALSE(ctx.sut.update());
|
||||
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING);
|
||||
EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x5A, 0x01, 0x30, 0x02, 0xCA, 0x01, 0xA8));
|
||||
EXPECT_EQ(ctx.sut.write_timeout_start_ms_, std::optional<uint32_t>{2000});
|
||||
@@ -92,7 +145,7 @@ TEST(MitsubishiCN105Tests, RxWatchdogLimitsProcessingPerUpdate) {
|
||||
ASSERT_GT(ctx.uart.rx.size(), 64);
|
||||
|
||||
// No valid response, no state change expected
|
||||
ctx.sut.update();
|
||||
ASSERT_FALSE(ctx.sut.update());
|
||||
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING);
|
||||
EXPECT_TRUE(ctx.uart.tx.empty());
|
||||
|
||||
@@ -100,7 +153,7 @@ TEST(MitsubishiCN105Tests, RxWatchdogLimitsProcessingPerUpdate) {
|
||||
EXPECT_FALSE(ctx.uart.rx.empty());
|
||||
|
||||
// Next update will read remaining bytes, no state change expected
|
||||
ctx.sut.update();
|
||||
ASSERT_FALSE(ctx.sut.update());
|
||||
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING);
|
||||
EXPECT_TRUE(ctx.uart.tx.empty());
|
||||
EXPECT_TRUE(ctx.uart.rx.empty());
|
||||
@@ -162,7 +215,7 @@ TEST(MitsubishiCN105Tests, ParserHandlesMixedRxStream) {
|
||||
// Drain RX - no valid response, no state change expected
|
||||
int iterations = 0;
|
||||
while (!ctx.uart.rx.empty() && iterations++ < 10) {
|
||||
ctx.sut.update();
|
||||
ASSERT_FALSE(ctx.sut.update());
|
||||
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING);
|
||||
EXPECT_TRUE(ctx.uart.tx.empty());
|
||||
}
|
||||
@@ -170,4 +223,81 @@ TEST(MitsubishiCN105Tests, ParserHandlesMixedRxStream) {
|
||||
EXPECT_TRUE(ctx.uart.rx.empty());
|
||||
}
|
||||
|
||||
TEST(MitsubishiCN105Tests, NextStatusUpdateAfterUpdateIntervalMilliseconds) {
|
||||
auto ctx = TestContext{};
|
||||
|
||||
ctx.sut.set_update_interval(2000);
|
||||
ctx.sut.set_current_time(80000);
|
||||
|
||||
// No scheduled status update
|
||||
EXPECT_FALSE(ctx.sut.status_update_start_ms_.has_value());
|
||||
|
||||
// Status update completed, schedule next status update
|
||||
ctx.sut.state_ = TestableMitsubishiCN105::State::STATUS_UPDATED;
|
||||
ctx.sut.set_state(TestableMitsubishiCN105::State::SCHEDULE_NEXT_STATUS_UPDATE);
|
||||
|
||||
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::WAITING_FOR_SCHEDULED_STATUS_UPDATE);
|
||||
EXPECT_EQ(ctx.sut.status_update_start_ms_, std::optional<uint32_t>{80000});
|
||||
|
||||
// Wait for update_interval (ms) before doing another status update
|
||||
ASSERT_FALSE(ctx.sut.update());
|
||||
EXPECT_TRUE(ctx.uart.tx.empty());
|
||||
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::WAITING_FOR_SCHEDULED_STATUS_UPDATE);
|
||||
|
||||
ctx.sut.set_current_time(81999);
|
||||
ASSERT_FALSE(ctx.sut.update());
|
||||
EXPECT_TRUE(ctx.uart.tx.empty());
|
||||
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::WAITING_FOR_SCHEDULED_STATUS_UPDATE);
|
||||
|
||||
ctx.sut.set_current_time(82000);
|
||||
ASSERT_FALSE(ctx.sut.update());
|
||||
EXPECT_FALSE(ctx.uart.tx.empty());
|
||||
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::UPDATING_STATUS);
|
||||
EXPECT_FALSE(ctx.sut.status_update_start_ms_.has_value());
|
||||
}
|
||||
|
||||
TEST(MitsubishiCN105Tests, DecodeStatusSettingsPackageTempEncodedA) {
|
||||
auto ctx = TestContext{};
|
||||
|
||||
ctx.uart.push_rx(
|
||||
{0xFC, 0x62, 0x01, 0x30, 0x0C, 0x02, 0x00, 0x00, 0x01, 0x03, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x56});
|
||||
|
||||
ctx.sut.update();
|
||||
|
||||
EXPECT_TRUE(ctx.sut.status().power_on);
|
||||
EXPECT_EQ(ctx.sut.status().target_temperature, 26.0f);
|
||||
}
|
||||
|
||||
TEST(MitsubishiCN105Tests, DecodeStatusSettingsPackageTempEncodedB) {
|
||||
auto ctx = TestContext{};
|
||||
|
||||
ctx.uart.push_rx(
|
||||
{0xFC, 0x62, 0x01, 0x30, 0x0C, 0x02, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA5, 0xB7});
|
||||
|
||||
ctx.sut.update();
|
||||
|
||||
EXPECT_FALSE(ctx.sut.status().power_on);
|
||||
EXPECT_EQ(ctx.sut.status().target_temperature, 18.5f);
|
||||
}
|
||||
|
||||
TEST(MitsubishiCN105Tests, DecodeStatusRoomTempPackageTempEncodedA) {
|
||||
auto ctx = TestContext{};
|
||||
|
||||
ctx.uart.push_rx({0xFC, 0x62, 0x01, 0x30, 0x07, 0x03, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x5D});
|
||||
|
||||
ctx.sut.update();
|
||||
|
||||
EXPECT_EQ(ctx.sut.status().room_temperature, 16.0f);
|
||||
}
|
||||
|
||||
TEST(MitsubishiCN105Tests, DecodeStatusRoomTempPackageTempEncodedB) {
|
||||
auto ctx = TestContext{};
|
||||
|
||||
ctx.uart.push_rx({0xFC, 0x62, 0x01, 0x30, 0x07, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0xBC, 0xA7});
|
||||
|
||||
ctx.sut.update();
|
||||
|
||||
EXPECT_EQ(ctx.sut.status().room_temperature, 30.0f);
|
||||
}
|
||||
|
||||
} // namespace esphome::mitsubishi_cn105::testing
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
|
||||
namespace esphome::mitsubishi_cn105 {
|
||||
|
||||
uint32_t get_loop_time_ms() { return testing::TestableMitsubishiCN105::test_loop_time_ms; };
|
||||
uint32_t get_loop_time_ms() { return testing::TestableMitsubishiCN105::test_loop_time_ms; }
|
||||
|
||||
} // namespace esphome::mitsubishi_cn105
|
||||
|
||||
@@ -44,6 +44,9 @@ class TestableMitsubishiCN105 : public MitsubishiCN105 {
|
||||
using MitsubishiCN105::State;
|
||||
using MitsubishiCN105::state_;
|
||||
using MitsubishiCN105::write_timeout_start_ms_;
|
||||
using MitsubishiCN105::status_update_start_ms_;
|
||||
|
||||
void set_state(State s) { this->set_state_(s); }
|
||||
|
||||
static inline uint32_t test_loop_time_ms = 0;
|
||||
|
||||
|
||||
@@ -3,6 +3,11 @@ pca9554:
|
||||
i2c_id: i2c_bus
|
||||
pin_count: 8
|
||||
address: 0x3F
|
||||
- id: pca9554_hub_int
|
||||
i2c_id: i2c_bus
|
||||
pin_count: 8
|
||||
address: 0x3E
|
||||
interrupt_pin: ${interrupt_pin}
|
||||
|
||||
binary_sensor:
|
||||
- platform: gpio
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
substitutions:
|
||||
interrupt_pin: GPIO15
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
substitutions:
|
||||
interrupt_pin: GPIO15
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
substitutions:
|
||||
interrupt_pin: GPIO2
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml
|
||||
|
||||
|
||||
@@ -3,6 +3,11 @@ pcf8574:
|
||||
i2c_id: i2c_bus
|
||||
address: 0x21
|
||||
pcf8575: false
|
||||
- id: pcf8574_hub_int
|
||||
i2c_id: i2c_bus
|
||||
address: 0x22
|
||||
pcf8575: false
|
||||
interrupt_pin: ${interrupt_pin}
|
||||
|
||||
binary_sensor:
|
||||
- platform: gpio
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
substitutions:
|
||||
interrupt_pin: GPIO15
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
substitutions:
|
||||
interrupt_pin: GPIO15
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
substitutions:
|
||||
interrupt_pin: GPIO2
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml
|
||||
|
||||
|
||||
@@ -6,5 +6,9 @@ api:
|
||||
|
||||
time:
|
||||
- platform: homeassistant
|
||||
on_time:
|
||||
- seconds: "0,10,20,30,40,50"
|
||||
then:
|
||||
- logger.log: "CronTrigger fired (every 10 seconds)"
|
||||
- platform: sntp
|
||||
id: sntp_time
|
||||
|
||||
Reference in New Issue
Block a user