mirror of
https://github.com/esphome/esphome.git
synced 2026-10-23 06:15:58 +00:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f90fb501b | ||
|
|
dfe87daa40 | ||
|
|
29cb1e9039 | ||
|
|
831f10fdeb | ||
|
|
8a2358d0e6 | ||
|
|
f416e1edd4 |
+7
-1
@@ -175,6 +175,11 @@ esphome/components/ds2484/* @mrk-its
|
||||
esphome/components/ds248x/* @tomwellnitz
|
||||
esphome/components/ds3231/* @linkedupbits
|
||||
esphome/components/dsmr/* @glmnet @PolarGoose
|
||||
esphome/components/duco/* @kokx
|
||||
esphome/components/duco/number/* @kokx
|
||||
esphome/components/duco/select/* @kokx
|
||||
esphome/components/duco/sensor/* @kokx
|
||||
esphome/components/duco/text_sensor/* @kokx
|
||||
esphome/components/duty_time/* @dudanov
|
||||
esphome/components/ecocomfort2/* @gledian
|
||||
esphome/components/ee895/* @Stock-M
|
||||
@@ -374,7 +379,8 @@ esphome/components/m5stack_8angle/* @rnauber
|
||||
esphome/components/mapping/* @clydebarrow
|
||||
esphome/components/matrix_keypad/* @ssieb
|
||||
esphome/components/max17043/* @blacknell
|
||||
esphome/components/max17048/* @B48D81EFCC
|
||||
esphome/components/max17048/* @B48D81EFCC @ryansch
|
||||
esphome/components/max170xx/* @ryansch
|
||||
esphome/components/max31865/* @DAVe3283
|
||||
esphome/components/max31888/* @JiriPrchal
|
||||
esphome/components/max44009/* @berfenger
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "ch422g.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::ch422g {
|
||||
@@ -59,11 +60,7 @@ bool CH422GComponent::digital_read(uint8_t pin) {
|
||||
}
|
||||
|
||||
void CH422GComponent::digital_write(uint8_t pin, bool value) {
|
||||
if (value) {
|
||||
this->output_bits_ |= (1 << pin);
|
||||
} else {
|
||||
this->output_bits_ &= ~(1 << pin);
|
||||
}
|
||||
assign_bits(this->output_bits_, 1 << pin, value);
|
||||
this->write_outputs_();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "ch423.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
@@ -62,11 +63,7 @@ bool CH423Component::digital_read(uint8_t pin) {
|
||||
}
|
||||
|
||||
void CH423Component::digital_write(uint8_t pin, bool value) {
|
||||
if (value) {
|
||||
this->output_bits_ |= (1 << pin);
|
||||
} else {
|
||||
this->output_bits_ &= ~(1 << pin);
|
||||
}
|
||||
assign_bits(this->output_bits_, 1 << pin, value);
|
||||
this->write_outputs_();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import time, uart
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_DISCOVERY, CONF_ID, CONF_TIME, CONF_TIME_ID
|
||||
from esphome.core import ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@kokx"]
|
||||
DEPENDENCIES = ["uart"]
|
||||
DOMAIN = "duco"
|
||||
MULTI_CONF = True
|
||||
|
||||
CONF_DISABLE_CRC = "disable_crc"
|
||||
CONF_DUCO_ID = "duco_id"
|
||||
|
||||
duco_ns = cg.esphome_ns.namespace("duco")
|
||||
Duco = duco_ns.class_("Duco", cg.Component, uart.UARTDevice)
|
||||
# Base of every class that sends requests through the hub
|
||||
DucoDevice = duco_ns.class_(
|
||||
"DucoDevice", cg.PollingComponent, cg.Parented.template(Duco)
|
||||
)
|
||||
DucoDiscovery = duco_ns.class_("DucoDiscovery", DucoDevice)
|
||||
DucoTime = duco_ns.class_("DucoTime", cg.PollingComponent, cg.Parented.template(Duco))
|
||||
|
||||
# Entities of the platforms take the hub with this
|
||||
DUCO_COMPONENT_SCHEMA = cv.Schema({cv.GenerateID(CONF_DUCO_ID): cv.use_id(Duco)})
|
||||
|
||||
DISCOVERY_SCHEMA = cv.Schema({cv.GenerateID(): cv.declare_id(DucoDiscovery)}).extend(
|
||||
cv.COMPONENT_SCHEMA
|
||||
)
|
||||
|
||||
TIME_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(DucoTime),
|
||||
cv.GenerateID(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
|
||||
}
|
||||
).extend(cv.polling_component_schema("15min"))
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(Duco),
|
||||
cv.Optional(CONF_DISABLE_CRC, default=False): cv.boolean,
|
||||
cv.Optional(CONF_DISCOVERY): DISCOVERY_SCHEMA,
|
||||
cv.Optional(CONF_TIME): TIME_SCHEMA,
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(uart.UART_DEVICE_SCHEMA)
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
DOMAIN,
|
||||
baud_rate=57600,
|
||||
require_tx=True,
|
||||
require_rx=True,
|
||||
data_bits=8,
|
||||
parity="NONE",
|
||||
stop_bits=1,
|
||||
)
|
||||
|
||||
|
||||
_request_device_slot = cg.slot_counter("DUCO_DEVICE_COUNT")
|
||||
|
||||
|
||||
async def register_duco_device(var: cg.MockObj, config: ConfigType, hub_id: ID) -> None:
|
||||
"""Register a device that sends requests, and give it an open request slot on its hub."""
|
||||
await cg.register_component(var, config)
|
||||
hub = await cg.get_variable(hub_id)
|
||||
await cg.register_parented(var, hub)
|
||||
_request_device_slot(str(hub_id))
|
||||
cg.add(hub.register_device(var))
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await uart.new_uart_component(config)
|
||||
cg.add(var.set_disable_crc(config[CONF_DISABLE_CRC]))
|
||||
|
||||
if (discovery := config.get(CONF_DISCOVERY)) is not None:
|
||||
discovery_var = cg.new_Pvariable(discovery[CONF_ID])
|
||||
await register_duco_device(discovery_var, discovery, config[CONF_ID])
|
||||
|
||||
if (time_config := config.get(CONF_TIME)) is not None:
|
||||
clock = await cg.get_variable(time_config[CONF_TIME_ID])
|
||||
time_var = cg.new_Pvariable(time_config[CONF_ID], clock)
|
||||
await cg.register_component(time_var, time_config)
|
||||
await cg.register_parented(time_var, var)
|
||||
@@ -0,0 +1,273 @@
|
||||
#include "duco.h"
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::duco {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "duco");
|
||||
|
||||
static constexpr uint8_t HEADER_START = 0xAA;
|
||||
static constexpr uint8_t HEADER_END = 0x55;
|
||||
static constexpr uint8_t STUFFING = 0x01; // follows an 0xAA inside a message, not part of it
|
||||
static constexpr uint32_t FRAME_GAP_MS = 50;
|
||||
|
||||
static constexpr uint32_t DISCOVERY_START_ID = 0;
|
||||
static constexpr uint32_t DISCOVERY_STEP_ID = 1;
|
||||
static constexpr uint32_t DISCOVERY_START_MS = 16000;
|
||||
static constexpr uint32_t DISCOVERY_STEP_MS = 1600;
|
||||
static constexpr uint8_t DISCOVERY_MAX_ATTEMPTS = 3;
|
||||
|
||||
void Duco::loop() {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
// Read first: after a slow loop pass the rest of a message is already waiting, which is not silence
|
||||
this->read_available([this, now](uint8_t byte) {
|
||||
if (this->parse_byte_(byte)) {
|
||||
this->last_byte_ = now;
|
||||
} else {
|
||||
this->reset_frame_();
|
||||
}
|
||||
});
|
||||
|
||||
// The last message of a burst has no header after it, so it ends on silence
|
||||
if (this->rx_size_ != 0 && now - this->last_byte_ > FRAME_GAP_MS) {
|
||||
this->finalize_message_();
|
||||
this->reset_frame_();
|
||||
}
|
||||
}
|
||||
|
||||
void Duco::reset_frame_() {
|
||||
this->rx_size_ = 0;
|
||||
this->escaped_ = false;
|
||||
}
|
||||
|
||||
void Duco::close_request(DucoDevice *device) {
|
||||
#ifdef DUCO_DEVICE_COUNT
|
||||
for (auto &pending : this->pending_) {
|
||||
if (pending.device == device) {
|
||||
pending.function = 0;
|
||||
pending.missed = false;
|
||||
this->update_warning_();
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
bool Duco::parse_byte_(uint8_t byte) {
|
||||
const size_t at = this->rx_size_;
|
||||
if (at >= MAX_FRAME_SIZE) {
|
||||
ESP_LOGV(TAG, "Message longer than %u bytes dropped", static_cast<unsigned>(MAX_FRAME_SIZE));
|
||||
return false;
|
||||
}
|
||||
const uint8_t prev = at > 0 ? this->rx_buffer_[at - 1] : 0;
|
||||
|
||||
if (at == 1 && prev == HEADER_START && byte == HEADER_END)
|
||||
return false; // header of the next message
|
||||
|
||||
if (at >= 2) {
|
||||
const bool escaped = this->escaped_;
|
||||
this->escaped_ = false;
|
||||
if (prev == HEADER_START && !escaped) {
|
||||
if (byte == STUFFING) {
|
||||
this->escaped_ = true;
|
||||
return true;
|
||||
}
|
||||
if (at >= 3 && byte == HEADER_END) {
|
||||
this->rx_size_--; // drop the 0xAA of the header
|
||||
this->finalize_message_();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this->rx_buffer_[this->rx_size_++] = byte;
|
||||
return true;
|
||||
}
|
||||
|
||||
void Duco::finalize_message_() {
|
||||
const uint8_t *raw = this->rx_buffer_.data();
|
||||
// length (function, id and data), function, id, data, CRC low, CRC high
|
||||
if (this->rx_size_ < 5 || raw[0] < 2 || this->rx_size_ < raw[0] + 3u) {
|
||||
ESP_LOGV(TAG, "Malformed message dropped: %u bytes, length byte %u", this->rx_size_, raw[0]);
|
||||
return;
|
||||
}
|
||||
const uint8_t length = raw[0];
|
||||
|
||||
const uint16_t computed_crc = crc16(raw, length + 1);
|
||||
const uint16_t remote_crc = encode_uint16(raw[length + 2], raw[length + 1]);
|
||||
if (computed_crc != remote_crc) {
|
||||
if (!this->disable_crc_) {
|
||||
ESP_LOGV(TAG, "CRC check failed: %04X != %04X", computed_crc, remote_crc);
|
||||
return;
|
||||
}
|
||||
ESP_LOGV(TAG, "CRC check failed, ignored: %04X != %04X", computed_crc, remote_crc);
|
||||
}
|
||||
|
||||
#ifdef DUCO_DEVICE_COUNT
|
||||
// The box first acknowledges a request with function + 1, then answers with function + 2
|
||||
const DucoMessage message{raw[2], raw + 3, static_cast<uint8_t>(length - 2)};
|
||||
for (auto &pending : this->pending_) {
|
||||
if (pending.function != 0 && pending.id == message.id && pending.function + 2 == raw[1]) {
|
||||
pending.function = 0;
|
||||
pending.missed = false;
|
||||
this->update_warning_();
|
||||
pending.device->receive_response(message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Acknowledgements (odd function codes) are expected and never match
|
||||
if ((raw[1] & 1) == 0) {
|
||||
ESP_LOGV(TAG, "Answer %02X with id %u matches no open request", raw[1], message.id);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef DUCO_DEVICE_COUNT
|
||||
void Duco::update_warning_() {
|
||||
for (const auto &pending : this->pending_) {
|
||||
if (pending.missed) {
|
||||
this->status_set_warning(LOG_STR("No answer from the box"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
this->status_clear_warning();
|
||||
}
|
||||
#endif
|
||||
|
||||
void Duco::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Duco:\n"
|
||||
" CRC Disabled: %s",
|
||||
YESNO(this->disable_crc_));
|
||||
}
|
||||
|
||||
uint8_t Duco::next_id_() {
|
||||
do {
|
||||
this->last_id_++;
|
||||
} while (this->last_id_ == HEADER_START || this->last_id_ == HEADER_END);
|
||||
return this->last_id_;
|
||||
}
|
||||
|
||||
void Duco::send(DucoDevice *device, uint8_t function, std::initializer_list<uint8_t> data) {
|
||||
if (data.size() > MAX_REQUEST_DATA) {
|
||||
ESP_LOGE(TAG, "Request of %u bytes is too long", static_cast<unsigned>(data.size()));
|
||||
return;
|
||||
}
|
||||
// Not byte stuffed, as in the author's original and no capture shows a request with 0xAA. An 0xAA followed by any
|
||||
// byte but 0x01 or 0x55 reads the same with or without unstuffing on the box, so only those rare pairs are
|
||||
// unverified.
|
||||
const size_t size = data.size();
|
||||
const uint8_t id = this->next_id_();
|
||||
uint8_t frame[5 + MAX_REQUEST_DATA + 2] = {HEADER_START, HEADER_END, static_cast<uint8_t>(size + 2), function, id};
|
||||
std::copy_n(data.begin(), size, frame + 5);
|
||||
const uint16_t crc = crc16(frame + 2, size + 3);
|
||||
frame[size + 5] = crc & 0xFF;
|
||||
frame[size + 6] = crc >> 8;
|
||||
this->write_array(frame, size + 7);
|
||||
this->flush();
|
||||
|
||||
#ifdef DUCO_DEVICE_COUNT
|
||||
for (auto &pending : this->pending_) {
|
||||
if (pending.device == device) {
|
||||
pending.missed = pending.function != 0;
|
||||
if (pending.missed) {
|
||||
ESP_LOGV(TAG, "No answer to request %02X with id %u", pending.function, pending.id);
|
||||
this->update_warning_();
|
||||
}
|
||||
pending.id = id;
|
||||
pending.function = function;
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
ESP_LOGV_HEX(TAG, frame + 2, size + 5, sizeof(frame), "Sent %s");
|
||||
}
|
||||
|
||||
static const LogString *node_type_name(uint8_t type) {
|
||||
switch (type) {
|
||||
case 8:
|
||||
return LOG_STR("UCBAT");
|
||||
case 9:
|
||||
return LOG_STR("UC");
|
||||
case 10:
|
||||
return LOG_STR("UCRH");
|
||||
case 12:
|
||||
return LOG_STR("UCCO2");
|
||||
case 13:
|
||||
return LOG_STR("VLV");
|
||||
case 17:
|
||||
return LOG_STR("BOX");
|
||||
case 18:
|
||||
return LOG_STR("SWITCH");
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
|
||||
void DucoDiscovery::setup() {
|
||||
// Give the box time to start before walking its node addresses
|
||||
this->set_timeout(DISCOVERY_START_ID, DISCOVERY_START_MS, [this]() {
|
||||
this->set_interval(DISCOVERY_STEP_ID, DISCOVERY_STEP_MS, [this]() { this->update(); });
|
||||
});
|
||||
}
|
||||
|
||||
void DucoDiscovery::update() {
|
||||
if (this->attempts_ == DISCOVERY_MAX_ATTEMPTS) {
|
||||
ESP_LOGV(TAG, "No answer for node %u", this->next_node_);
|
||||
this->next_node_++;
|
||||
this->attempts_ = 0;
|
||||
}
|
||||
if (this->next_node_ >= NODE_COUNT) {
|
||||
this->cancel_interval(DISCOVERY_STEP_ID);
|
||||
this->parent_->close_request(this);
|
||||
return;
|
||||
}
|
||||
// Without an answer by the next step the same node is asked again; a retry is not a missed answer
|
||||
if (this->attempts_ > 0)
|
||||
this->parent_->close_request(this);
|
||||
ESP_LOGV(TAG, "Discovering node %u", this->next_node_);
|
||||
this->attempts_++;
|
||||
this->parent_->send(this, FUNCTION_NODE, {0x01, this->next_node_});
|
||||
}
|
||||
|
||||
void DucoDiscovery::receive_response(const DucoMessage &message) {
|
||||
// Only the latest attempt is open, so a late answer to an earlier one never arrives here
|
||||
if (message.size >= 1 && this->next_node_ < NODE_COUNT) {
|
||||
if (message.data[0] != 0) {
|
||||
ESP_LOGV(TAG, "Found node %u: type %u", this->next_node_, message.data[0]);
|
||||
}
|
||||
this->node_types_[this->next_node_++] = message.data[0];
|
||||
this->attempts_ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void DucoDiscovery::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Discovered nodes:");
|
||||
for (uint8_t node = 0; node < NODE_COUNT; node++) {
|
||||
const uint8_t type = this->node_types_[node];
|
||||
if (type != 0) {
|
||||
ESP_LOGCONFIG(TAG, " Node %u: type %u (%s)", node, type, LOG_STR_ARG(node_type_name(type)));
|
||||
}
|
||||
}
|
||||
if (this->next_node_ < NODE_COUNT) {
|
||||
ESP_LOGCONFIG(TAG, " Still discovering, at node %u", this->next_node_);
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_TIME
|
||||
void DucoTime::update() {
|
||||
const ESPTime now = this->clock_->now();
|
||||
if (!now.is_valid()) {
|
||||
ESP_LOGV(TAG, "Time is not valid yet");
|
||||
return;
|
||||
}
|
||||
const uint32_t ts = now.timestamp;
|
||||
this->parent_->send(nullptr, FUNCTION_PARAMETER,
|
||||
{0x05, static_cast<uint8_t>(ts), static_cast<uint8_t>(ts >> 8), static_cast<uint8_t>(ts >> 16),
|
||||
static_cast<uint8_t>(ts >> 24), 0x00, 0x00, 0x00, 0x00});
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace esphome::duco
|
||||
@@ -0,0 +1,113 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <initializer_list>
|
||||
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#ifdef USE_TIME
|
||||
#include "esphome/components/time/real_time_clock.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::duco {
|
||||
|
||||
// Function codes of the requests; the box answers a request with function + 2
|
||||
static constexpr uint8_t FUNCTION_NODE = 0x0c; // discovery, status and mode of a node
|
||||
static constexpr uint8_t FUNCTION_NODE_DATA = 0x10; // a block of a node's data (sensors, serial number)
|
||||
static constexpr uint8_t FUNCTION_PARAMETER = 0x24; // read or write a box parameter, set the clock
|
||||
|
||||
/// A received message; data points into the receive buffer and is valid only during the callback.
|
||||
struct DucoMessage {
|
||||
uint8_t id;
|
||||
const uint8_t *data;
|
||||
uint8_t size;
|
||||
};
|
||||
|
||||
class DucoDevice;
|
||||
|
||||
/// The open request of one device: the answer to it is function + 2 with the same message id.
|
||||
struct DucoPendingRequest {
|
||||
DucoDevice *device;
|
||||
uint8_t id;
|
||||
uint8_t function; // 0 while nothing is open
|
||||
bool missed; // the request before this one got no answer; sits in padding
|
||||
};
|
||||
|
||||
class Duco : public uart::UARTDevice, public Component {
|
||||
public:
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
|
||||
void set_disable_crc(bool disable_crc) { this->disable_crc_ = disable_crc; }
|
||||
|
||||
#ifdef DUCO_DEVICE_COUNT
|
||||
void register_device(DucoDevice *device) { this->pending_.push_back({device, 0, 0, false}); }
|
||||
#endif
|
||||
|
||||
/// Forget device's open request, for a device that will not send again.
|
||||
void close_request(DucoDevice *device);
|
||||
|
||||
/// Send a request; its answer goes to device, or nowhere if nullptr. A device's new request replaces its
|
||||
/// open one, so an answer is never lost to another device's traffic.
|
||||
void send(DucoDevice *device, uint8_t function, std::initializer_list<uint8_t> data);
|
||||
|
||||
protected:
|
||||
// The longest answer to a request sent here is 25 bytes (node status), the longest request 9
|
||||
static constexpr size_t MAX_FRAME_SIZE = 32;
|
||||
static constexpr size_t MAX_REQUEST_DATA = 9;
|
||||
|
||||
bool parse_byte_(uint8_t byte);
|
||||
void reset_frame_();
|
||||
#ifdef DUCO_DEVICE_COUNT
|
||||
void update_warning_(); // warn while any device's previous request went unanswered
|
||||
#endif
|
||||
void finalize_message_();
|
||||
uint8_t next_id_();
|
||||
|
||||
#ifdef DUCO_DEVICE_COUNT
|
||||
StaticVector<DucoPendingRequest, DUCO_DEVICE_COUNT> pending_;
|
||||
#endif
|
||||
std::array<uint8_t, MAX_FRAME_SIZE> rx_buffer_{};
|
||||
uint32_t last_byte_{0};
|
||||
uint8_t rx_size_{0};
|
||||
uint8_t last_id_{10};
|
||||
bool escaped_{false}; // the last 0xAA was followed by a stuffing byte, so it starts no header
|
||||
bool disable_crc_{false};
|
||||
};
|
||||
|
||||
/// Every entity polls the box; deriving from PollingComponent shares its vtable.
|
||||
class DucoDevice : public PollingComponent, public Parented<Duco> {
|
||||
public:
|
||||
virtual void receive_response(const DucoMessage &message) = 0;
|
||||
};
|
||||
|
||||
/// Walks the node addresses once after boot; the nodes found are listed in the config dump.
|
||||
class DucoDiscovery : public DucoDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void update() override; // one discovery step, run by the step interval
|
||||
void receive_response(const DucoMessage &message) override;
|
||||
|
||||
protected:
|
||||
static constexpr uint8_t NODE_COUNT = 0x45;
|
||||
|
||||
std::array<uint8_t, NODE_COUNT> node_types_{}; // 0 when no node answered at that address
|
||||
uint8_t next_node_{0};
|
||||
uint8_t attempts_{0};
|
||||
};
|
||||
|
||||
#ifdef USE_TIME
|
||||
class DucoTime : public PollingComponent, public Parented<Duco> {
|
||||
public:
|
||||
explicit DucoTime(time::RealTimeClock *clock) : clock_(clock) {}
|
||||
void update() override;
|
||||
|
||||
protected:
|
||||
time::RealTimeClock *clock_;
|
||||
};
|
||||
#endif
|
||||
|
||||
} // namespace esphome::duco
|
||||
@@ -0,0 +1,34 @@
|
||||
from esphome.components import number
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import DEVICE_CLASS_TEMPERATURE, UNIT_CELSIUS
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import (
|
||||
CONF_DUCO_ID,
|
||||
DUCO_COMPONENT_SCHEMA,
|
||||
DucoDevice,
|
||||
duco_ns,
|
||||
register_duco_device,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["duco"]
|
||||
CODEOWNERS = ["@kokx"]
|
||||
|
||||
DucoComfortTemperature = duco_ns.class_(
|
||||
"DucoComfortTemperature", DucoDevice, number.Number
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
number.number_schema(
|
||||
DucoComfortTemperature,
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
)
|
||||
.extend(DUCO_COMPONENT_SCHEMA)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await number.new_number(config, min_value=10.0, max_value=25.5, step=0.1)
|
||||
await register_duco_device(var, config, config[CONF_DUCO_ID])
|
||||
@@ -0,0 +1,26 @@
|
||||
#include "number.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace esphome::duco {
|
||||
|
||||
static constexpr uint8_t PARAMETER_COMFORT_TEMPERATURE = 0x12;
|
||||
static constexpr uint8_t GROUP_COMFORT_TEMPERATURE = 0x0a;
|
||||
|
||||
void DucoComfortTemperature::update() {
|
||||
this->parent_->send(this, FUNCTION_PARAMETER, {0x00, PARAMETER_COMFORT_TEMPERATURE, GROUP_COMFORT_TEMPERATURE});
|
||||
}
|
||||
|
||||
void DucoComfortTemperature::receive_response(const DucoMessage &message) {
|
||||
// Reads and writes get the same answer: one byte in tenths of a degree, below 10.0 is invalid
|
||||
if (message.size >= 4 && message.data[3] >= 100)
|
||||
this->publish_state(message.data[3] / 10.0f);
|
||||
}
|
||||
|
||||
void DucoComfortTemperature::control(float value) {
|
||||
const auto tenths = static_cast<uint8_t>(lroundf(value * 10.0f));
|
||||
this->parent_->send(this, FUNCTION_PARAMETER,
|
||||
{0x01, PARAMETER_COMFORT_TEMPERATURE, GROUP_COMFORT_TEMPERATURE, tenths, 0x00, 0x00, 0x00});
|
||||
}
|
||||
|
||||
} // namespace esphome::duco
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/number/number.h"
|
||||
|
||||
#include "../duco.h"
|
||||
|
||||
namespace esphome::duco {
|
||||
|
||||
class DucoComfortTemperature : public DucoDevice, public number::Number {
|
||||
public:
|
||||
void update() override;
|
||||
void receive_response(const DucoMessage &message) override;
|
||||
|
||||
protected:
|
||||
void control(float value) override;
|
||||
};
|
||||
|
||||
} // namespace esphome::duco
|
||||
@@ -0,0 +1,45 @@
|
||||
from esphome.components import select
|
||||
import esphome.config_validation as cv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import (
|
||||
CONF_DUCO_ID,
|
||||
DUCO_COMPONENT_SCHEMA,
|
||||
DucoDevice,
|
||||
duco_ns,
|
||||
register_duco_device,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["duco"]
|
||||
CODEOWNERS = ["@kokx"]
|
||||
|
||||
# Same order as MODE_CODES in select.cpp
|
||||
DUCO_MODE_OPTIONS = [
|
||||
"AUTO",
|
||||
"MAN1",
|
||||
"MAN2",
|
||||
"MAN3",
|
||||
"EMPT",
|
||||
"CNT1",
|
||||
"CNT2",
|
||||
"CNT3",
|
||||
"MAN1x2",
|
||||
"MAN2x2",
|
||||
"MAN3x2",
|
||||
"MAN1x3",
|
||||
"MAN2x3",
|
||||
"MAN3x3",
|
||||
]
|
||||
|
||||
DucoSelect = duco_ns.class_("DucoSelect", DucoDevice, select.Select)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
select.select_schema(DucoSelect)
|
||||
.extend(cv.polling_component_schema("3s"))
|
||||
.extend(DUCO_COMPONENT_SCHEMA)
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await select.new_select(config, options=DUCO_MODE_OPTIONS)
|
||||
await register_duco_device(var, config, config[CONF_DUCO_ID])
|
||||
@@ -0,0 +1,47 @@
|
||||
#include "select.h"
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::duco {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "duco.select");
|
||||
|
||||
// Box code of each option, in the order of DUCO_MODE_OPTIONS in __init__.py
|
||||
static constexpr uint8_t MODE_CODES[] PROGMEM = {
|
||||
0x00, // AUTO
|
||||
0x04, // MAN1
|
||||
0x05, // MAN2
|
||||
0x06, // MAN3
|
||||
0x07, // EMPT
|
||||
0x08, // CNT1
|
||||
0x09, // CNT2
|
||||
0x0a, // CNT3
|
||||
0x84, // MAN1x2
|
||||
0x85, // MAN2x2
|
||||
0x86, // MAN3x2
|
||||
0xc4, // MAN1x3
|
||||
0xc5, // MAN2x3
|
||||
0xc6, // MAN3x3
|
||||
};
|
||||
|
||||
void DucoSelect::update() { this->parent_->send(this, FUNCTION_NODE, {0x02, 0x01}); }
|
||||
|
||||
void DucoSelect::receive_response(const DucoMessage &message) {
|
||||
if (message.size < 1)
|
||||
return;
|
||||
for (size_t i = 0; i < sizeof(MODE_CODES); i++) {
|
||||
if (progmem_read_byte(&MODE_CODES[i]) == message.data[0]) {
|
||||
this->publish_state(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
ESP_LOGV(TAG, "Unknown mode %02X", message.data[0]);
|
||||
}
|
||||
|
||||
void DucoSelect::control(size_t index) {
|
||||
// The next update reads the new mode back
|
||||
this->parent_->send(nullptr, FUNCTION_NODE, {0x04, 0x01, progmem_read_byte(&MODE_CODES[index])});
|
||||
}
|
||||
|
||||
} // namespace esphome::duco
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/select/select.h"
|
||||
|
||||
#include "../duco.h"
|
||||
|
||||
namespace esphome::duco {
|
||||
|
||||
class DucoSelect : public DucoDevice, public select::Select {
|
||||
public:
|
||||
void update() override;
|
||||
void receive_response(const DucoMessage &message) override;
|
||||
|
||||
protected:
|
||||
void control(size_t index) override;
|
||||
};
|
||||
|
||||
} // namespace esphome::duco
|
||||
@@ -0,0 +1,156 @@
|
||||
from esphome.components import sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ADDRESS,
|
||||
CONF_CO2,
|
||||
CONF_HUMIDITY,
|
||||
CONF_TEMPERATURE,
|
||||
DEVICE_CLASS_CARBON_DIOXIDE,
|
||||
DEVICE_CLASS_DURATION,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_PARTS_PER_MILLION,
|
||||
UNIT_PERCENT,
|
||||
UNIT_SECOND,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import (
|
||||
CONF_DUCO_ID,
|
||||
DUCO_COMPONENT_SCHEMA,
|
||||
DucoDevice,
|
||||
duco_ns,
|
||||
register_duco_device,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["duco"]
|
||||
CODEOWNERS = ["@kokx"]
|
||||
|
||||
UNIT_DAYS = "d"
|
||||
|
||||
CONF_BYPASS = "bypass"
|
||||
CONF_FILTER_REMAINING = "filter_remaining"
|
||||
CONF_FLOW_LEVEL = "flow_level"
|
||||
CONF_TEMPERATURE_EHA = "temperature_eha"
|
||||
CONF_TEMPERATURE_ETA = "temperature_eta"
|
||||
CONF_TEMPERATURE_ODA = "temperature_oda"
|
||||
CONF_TEMPERATURE_SUP = "temperature_sup"
|
||||
CONF_TIME_REMAINING = "time_remaining"
|
||||
|
||||
DucoSensor = duco_ns.class_("DucoSensor", DucoDevice, sensor.Sensor)
|
||||
DucoReading = duco_ns.enum("DucoReading", is_class=True)
|
||||
|
||||
BOX_NODE = 1
|
||||
|
||||
|
||||
def _schema(update_interval: str = "60s", **kwargs) -> cv.Schema:
|
||||
return sensor.sensor_schema(
|
||||
DucoSensor, state_class=STATE_CLASS_MEASUREMENT, **kwargs
|
||||
).extend(cv.polling_component_schema(update_interval))
|
||||
|
||||
|
||||
def _node_schema(**kwargs) -> cv.All:
|
||||
"""A list of sensors, one per node address."""
|
||||
return cv.ensure_list(
|
||||
_schema(**kwargs).extend({cv.Required(CONF_ADDRESS): cv.int_range(0, 68)})
|
||||
)
|
||||
|
||||
|
||||
_TEMPERATURE = {
|
||||
"unit_of_measurement": UNIT_CELSIUS,
|
||||
"accuracy_decimals": 1,
|
||||
"device_class": DEVICE_CLASS_TEMPERATURE,
|
||||
}
|
||||
|
||||
# key: (what is read, request argument or None for the node address, schema)
|
||||
SENSORS = {
|
||||
CONF_CO2: (
|
||||
DucoReading.DUCO_READING_CO2,
|
||||
None,
|
||||
_node_schema(
|
||||
unit_of_measurement=UNIT_PARTS_PER_MILLION,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_CARBON_DIOXIDE,
|
||||
),
|
||||
),
|
||||
CONF_TEMPERATURE: (
|
||||
DucoReading.DUCO_READING_NODE_TEMPERATURE,
|
||||
None,
|
||||
_node_schema(**_TEMPERATURE),
|
||||
),
|
||||
CONF_HUMIDITY: (
|
||||
DucoReading.DUCO_READING_HUMIDITY,
|
||||
None,
|
||||
_node_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
),
|
||||
),
|
||||
CONF_FILTER_REMAINING: (
|
||||
DucoReading.DUCO_READING_FILTER_REMAINING,
|
||||
0x30,
|
||||
_schema(
|
||||
"1200s",
|
||||
unit_of_measurement=UNIT_DAYS,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_DURATION,
|
||||
),
|
||||
),
|
||||
CONF_FLOW_LEVEL: (
|
||||
DucoReading.DUCO_READING_FLOW_LEVEL,
|
||||
BOX_NODE,
|
||||
_schema(unit_of_measurement=UNIT_PERCENT, accuracy_decimals=0),
|
||||
),
|
||||
CONF_TIME_REMAINING: (
|
||||
DucoReading.DUCO_READING_TIME_REMAINING,
|
||||
BOX_NODE,
|
||||
_schema(
|
||||
unit_of_measurement=UNIT_SECOND,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_DURATION,
|
||||
),
|
||||
),
|
||||
CONF_TEMPERATURE_ODA: (
|
||||
DucoReading.DUCO_READING_BOX_TEMPERATURE,
|
||||
0x00,
|
||||
_schema(**_TEMPERATURE),
|
||||
),
|
||||
CONF_TEMPERATURE_SUP: (
|
||||
DucoReading.DUCO_READING_BOX_TEMPERATURE,
|
||||
0x01,
|
||||
_schema(**_TEMPERATURE),
|
||||
),
|
||||
CONF_TEMPERATURE_ETA: (
|
||||
DucoReading.DUCO_READING_BOX_TEMPERATURE,
|
||||
0x02,
|
||||
_schema(**_TEMPERATURE),
|
||||
),
|
||||
CONF_TEMPERATURE_EHA: (
|
||||
DucoReading.DUCO_READING_BOX_TEMPERATURE,
|
||||
0x03,
|
||||
_schema(**_TEMPERATURE),
|
||||
),
|
||||
CONF_BYPASS: (
|
||||
DucoReading.DUCO_READING_BYPASS,
|
||||
0x10,
|
||||
_schema(unit_of_measurement=UNIT_PERCENT, accuracy_decimals=0),
|
||||
),
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = DUCO_COMPONENT_SCHEMA.extend(
|
||||
{cv.Optional(key): schema for key, (_, _, schema) in SENSORS.items()}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
for key, (reading, argument, _) in SENSORS.items():
|
||||
if (confs := config.get(key)) is None:
|
||||
continue
|
||||
for conf in confs if isinstance(confs, list) else [confs]:
|
||||
var = await sensor.new_sensor(
|
||||
conf, reading, conf.get(CONF_ADDRESS, argument)
|
||||
)
|
||||
await register_duco_device(var, conf, config[CONF_DUCO_ID])
|
||||
@@ -0,0 +1,78 @@
|
||||
#include "sensor.h"
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::duco {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "duco.sensor");
|
||||
|
||||
enum class DucoValueFormat : uint8_t {
|
||||
DUCO_VALUE_FORMAT_UINT8,
|
||||
DUCO_VALUE_FORMAT_UINT16,
|
||||
DUCO_VALUE_FORMAT_INT16,
|
||||
};
|
||||
|
||||
/// A little endian field at offset in the answer. Values outside [minimum, maximum] are dropped;
|
||||
/// the box reports those for sensors that are missing or still starting.
|
||||
struct ReadingLayout {
|
||||
uint8_t function;
|
||||
uint8_t offset;
|
||||
DucoValueFormat format;
|
||||
uint8_t divisor;
|
||||
int16_t minimum;
|
||||
uint16_t maximum;
|
||||
};
|
||||
|
||||
// Indexed by DucoReading
|
||||
static constexpr ReadingLayout READINGS[] PROGMEM = {
|
||||
{FUNCTION_NODE_DATA, 4, DucoValueFormat::DUCO_VALUE_FORMAT_UINT16, 1, 300, 10000}, // CO2
|
||||
{FUNCTION_NODE_DATA, 2, DucoValueFormat::DUCO_VALUE_FORMAT_UINT16, 10, 0, 1000}, // node temperature
|
||||
{FUNCTION_NODE_DATA, 6, DucoValueFormat::DUCO_VALUE_FORMAT_UINT16, 100, 0, 10000}, // humidity
|
||||
{FUNCTION_PARAMETER, 3, DucoValueFormat::DUCO_VALUE_FORMAT_UINT8, 1, 0, 255}, // filter remaining
|
||||
{FUNCTION_NODE, 2, DucoValueFormat::DUCO_VALUE_FORMAT_UINT8, 1, 0, 255}, // flow level
|
||||
{FUNCTION_NODE, 12, DucoValueFormat::DUCO_VALUE_FORMAT_UINT16, 1, 0, 65535}, // time remaining
|
||||
{FUNCTION_PARAMETER, 3, DucoValueFormat::DUCO_VALUE_FORMAT_INT16, 10, -999, 1000}, // box temperature
|
||||
{FUNCTION_PARAMETER, 3, DucoValueFormat::DUCO_VALUE_FORMAT_UINT8, 1, 0, 100}, // bypass
|
||||
};
|
||||
|
||||
static ReadingLayout layout_of(DucoReading reading) {
|
||||
ReadingLayout layout;
|
||||
progmem_memcpy(&layout, &READINGS[static_cast<uint8_t>(reading)], sizeof(layout));
|
||||
return layout;
|
||||
}
|
||||
|
||||
void DucoSensor::update() {
|
||||
switch (layout_of(this->reading_).function) {
|
||||
case FUNCTION_NODE_DATA:
|
||||
this->parent_->send(this, FUNCTION_NODE_DATA, {0x01, this->argument_, 0x00, 0x49, 0x04});
|
||||
break;
|
||||
case FUNCTION_PARAMETER:
|
||||
this->parent_->send(this, FUNCTION_PARAMETER, {0x00, this->argument_, 0x09});
|
||||
break;
|
||||
case FUNCTION_NODE:
|
||||
this->parent_->send(this, FUNCTION_NODE, {0x02, this->argument_});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void DucoSensor::receive_response(const DucoMessage &message) {
|
||||
const ReadingLayout layout = layout_of(this->reading_);
|
||||
const bool wide = layout.format != DucoValueFormat::DUCO_VALUE_FORMAT_UINT8;
|
||||
if (message.size < layout.offset + (wide ? 2 : 1)) {
|
||||
ESP_LOGV(TAG, "Answer of %u bytes is too short", message.size);
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t *field = message.data + layout.offset;
|
||||
int32_t value = field[0];
|
||||
if (layout.format == DucoValueFormat::DUCO_VALUE_FORMAT_UINT16) {
|
||||
value = encode_value_le<uint16_t>(field);
|
||||
} else if (layout.format == DucoValueFormat::DUCO_VALUE_FORMAT_INT16) {
|
||||
value = encode_value_le<int16_t>(field);
|
||||
}
|
||||
if (value >= layout.minimum && value <= layout.maximum)
|
||||
this->publish_state(static_cast<float>(value) / layout.divisor);
|
||||
}
|
||||
|
||||
} // namespace esphome::duco
|
||||
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
|
||||
#include "../duco.h"
|
||||
|
||||
namespace esphome::duco {
|
||||
|
||||
/// What a sensor reads; the request and where the value is in the answer are in READINGS in sensor.cpp.
|
||||
enum class DucoReading : uint8_t {
|
||||
DUCO_READING_CO2,
|
||||
DUCO_READING_NODE_TEMPERATURE,
|
||||
DUCO_READING_HUMIDITY,
|
||||
DUCO_READING_FILTER_REMAINING,
|
||||
DUCO_READING_FLOW_LEVEL,
|
||||
DUCO_READING_TIME_REMAINING,
|
||||
DUCO_READING_BOX_TEMPERATURE,
|
||||
DUCO_READING_BYPASS,
|
||||
};
|
||||
|
||||
class DucoSensor : public DucoDevice, public sensor::Sensor {
|
||||
public:
|
||||
/// argument is the node address, or the parameter number for a box parameter.
|
||||
DucoSensor(DucoReading reading, uint8_t argument) : reading_(reading), argument_(argument) {}
|
||||
|
||||
void update() override;
|
||||
void receive_response(const DucoMessage &message) override;
|
||||
|
||||
protected:
|
||||
DucoReading reading_;
|
||||
uint8_t argument_;
|
||||
};
|
||||
|
||||
} // namespace esphome::duco
|
||||
@@ -0,0 +1,27 @@
|
||||
from esphome.components import text_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import (
|
||||
CONF_DUCO_ID,
|
||||
DUCO_COMPONENT_SCHEMA,
|
||||
DucoDevice,
|
||||
duco_ns,
|
||||
register_duco_device,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["duco"]
|
||||
CODEOWNERS = ["@kokx"]
|
||||
|
||||
DucoSerial = duco_ns.class_("DucoSerial", DucoDevice, text_sensor.TextSensor)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
text_sensor.text_sensor_schema(DucoSerial)
|
||||
.extend(cv.polling_component_schema("300s"))
|
||||
.extend(DUCO_COMPONENT_SCHEMA)
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await text_sensor.new_text_sensor(config)
|
||||
await register_duco_device(var, config, config[CONF_DUCO_ID])
|
||||
@@ -0,0 +1,17 @@
|
||||
#include "serial.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace esphome::duco {
|
||||
|
||||
void DucoSerial::update() { this->parent_->send(this, FUNCTION_NODE_DATA, {0x01, 0x01, 0x00, 0x1a, 0x10}); }
|
||||
|
||||
void DucoSerial::receive_response(const DucoMessage &message) {
|
||||
if (message.size > 2) {
|
||||
// The serial number starts at byte 2 and is padded with zeros
|
||||
const char *serial = reinterpret_cast<const char *>(message.data + 2);
|
||||
this->publish_state(serial, strnlen(serial, message.size - 2));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::duco
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/text_sensor/text_sensor.h"
|
||||
|
||||
#include "../duco.h"
|
||||
|
||||
namespace esphome::duco {
|
||||
|
||||
class DucoSerial : public DucoDevice, public text_sensor::TextSensor {
|
||||
public:
|
||||
void update() override;
|
||||
void receive_response(const DucoMessage &message) override;
|
||||
};
|
||||
|
||||
} // namespace esphome::duco
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "epaper_spi.h"
|
||||
#include <algorithm>
|
||||
#include <cinttypes>
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
@@ -37,6 +38,42 @@ bool EPaperBase::init_buffer_(size_t buffer_length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool EPaperBase::init_sent_frame_(size_t length) {
|
||||
if (!this->is_using_partial_update_())
|
||||
return false;
|
||||
if (this->sent_.init(length))
|
||||
return true;
|
||||
ESP_LOGW(TAG, "No memory for the previous frame; partial updates will send the whole drawn area");
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t EPaperBase::buffer_hash_() const {
|
||||
uint32_t hash = FNV1_OFFSET_BASIS;
|
||||
size_t index = 0;
|
||||
while (index != this->buffer_.size()) {
|
||||
size_t length;
|
||||
const uint8_t *data = this->buffer_.get_span(index, length);
|
||||
for (size_t i = 0; i != length; i++)
|
||||
hash = fnv1a_hash_extend(hash, data[i]);
|
||||
index += length;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
void EPaperBase::set_full_bounds_() {
|
||||
this->x_low_ = 0;
|
||||
this->y_low_ = 0;
|
||||
this->x_high_ = this->width_;
|
||||
this->y_high_ = this->height_;
|
||||
}
|
||||
|
||||
void EPaperBase::reset_bounds_() {
|
||||
this->x_low_ = this->width_;
|
||||
this->x_high_ = 0;
|
||||
this->y_low_ = this->height_;
|
||||
this->y_high_ = 0;
|
||||
}
|
||||
|
||||
void EPaperBase::setup_pins_() const {
|
||||
for (auto *pin : this->enable_pins_) {
|
||||
pin->setup();
|
||||
@@ -233,17 +270,28 @@ void EPaperBase::process_state_() {
|
||||
// Refresh the whole panel even if nothing was drawn
|
||||
this->full_update_requested_ = false;
|
||||
this->update_count_ = 0;
|
||||
this->x_low_ = 0;
|
||||
this->y_low_ = 0;
|
||||
this->x_high_ = this->width_;
|
||||
this->y_high_ = this->height_;
|
||||
this->set_full_bounds_();
|
||||
}
|
||||
if (this->x_high_ < this->x_low_ || this->y_high_ < this->y_low_) {
|
||||
this->set_state_(EPaperState::IDLE);
|
||||
return;
|
||||
}
|
||||
if (this->is_using_partial_update_()) {
|
||||
const uint32_t frame_hash = this->buffer_hash_();
|
||||
if (this->update_count_ != 0 && this->shown_hash_valid_ && frame_hash == this->shown_hash_) {
|
||||
ESP_LOGD(TAG, "Frame unchanged, refresh skipped");
|
||||
this->reset_bounds_();
|
||||
this->set_state_(EPaperState::IDLE);
|
||||
return;
|
||||
}
|
||||
this->shown_hash_ = frame_hash;
|
||||
}
|
||||
this->drawn_since_hash_ = false;
|
||||
// Valid again only once this frame is on the panel, so an abandoned update leaves nothing to match
|
||||
this->shown_hash_valid_ = false;
|
||||
// A full update resets update_count_ to zero, so a non-zero count marks a partial one
|
||||
this->partial_ = this->update_count_ != 0;
|
||||
this->sent_count_ = 0;
|
||||
this->set_state_(EPaperState::RESET);
|
||||
break;
|
||||
case EPaperState::INITIALISE:
|
||||
@@ -256,10 +304,15 @@ void EPaperBase::process_state_() {
|
||||
if (!this->transfer_data()) {
|
||||
return; // Not done yet, come back next loop
|
||||
}
|
||||
this->x_low_ = this->width_;
|
||||
this->x_high_ = 0;
|
||||
this->y_low_ = this->height_;
|
||||
this->y_high_ = 0;
|
||||
if (this->sent_.is_valid() && this->sent_count_ == this->sent_.size())
|
||||
this->sent_valid_ = true;
|
||||
// A draw during the chunked push may not have reached the panel, so the next update sends the whole frame
|
||||
this->sent_hash_intact_ = this->is_using_partial_update_() && !this->drawn_since_hash_;
|
||||
if (this->drawn_since_hash_) {
|
||||
this->set_full_bounds_();
|
||||
} else {
|
||||
this->reset_bounds_();
|
||||
}
|
||||
this->set_state_(EPaperState::POWER_ON);
|
||||
break;
|
||||
case EPaperState::POWER_ON:
|
||||
@@ -280,6 +333,7 @@ void EPaperBase::process_state_() {
|
||||
break;
|
||||
case EPaperState::DEEP_SLEEP:
|
||||
this->deep_sleep();
|
||||
this->shown_hash_valid_ = this->sent_hash_intact_;
|
||||
this->status_clear_warning();
|
||||
this->set_state_(EPaperState::IDLE);
|
||||
ESP_LOGD(TAG, "Display update took %" PRIu32 " ms", millis() - this->update_start_time_);
|
||||
@@ -365,6 +419,7 @@ bool EPaperBase::rotate_coordinates_(int &x, int &y) {
|
||||
this->x_high_ = clamp_at_least(this->x_high_, x + 1);
|
||||
this->y_low_ = clamp_at_most(this->y_low_, y);
|
||||
this->y_high_ = clamp_at_least(this->y_high_, y + 1);
|
||||
this->drawn_since_hash_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -486,6 +541,7 @@ void HOT EPaperBase::draw_rgb565_block_(uint8_t *buffer, int x_start, int y_star
|
||||
this->x_high_ = clamp_at_least(this->x_high_, x_high);
|
||||
this->y_low_ = clamp_at_most(this->y_low_, y_low);
|
||||
this->y_high_ = clamp_at_least(this->y_high_, y_high);
|
||||
this->drawn_since_hash_ = true;
|
||||
}
|
||||
|
||||
// Writes an RGB565 block of pixels, as LVGL sends them, straight into a 1 bit per pixel buffer that is one block (the
|
||||
|
||||
@@ -99,10 +99,7 @@ class EPaperBase : public Display,
|
||||
|
||||
// We store 8 pixels per byte
|
||||
this->buffer_.fill(pixel_color);
|
||||
this->x_high_ = this->width_;
|
||||
this->y_high_ = this->height_;
|
||||
this->x_low_ = 0;
|
||||
this->y_low_ = 0;
|
||||
this->mark_all_drawn_();
|
||||
}
|
||||
|
||||
void clear() override {
|
||||
@@ -139,6 +136,25 @@ class EPaperBase : public Display,
|
||||
void wait_for_idle_(bool should_wait);
|
||||
void abandon_update_();
|
||||
bool init_buffer_(size_t buffer_length);
|
||||
// Allocates the copy of the previous frame; only drivers that diff against it (needs_previous_frame) call this
|
||||
bool init_sent_frame_(size_t length);
|
||||
void record_sent_(size_t index, const uint8_t *data, size_t length) {
|
||||
if (this->sent_.is_valid()) {
|
||||
this->sent_.write(index, data, length);
|
||||
this->sent_count_ += length;
|
||||
}
|
||||
}
|
||||
// Whether the driver reads the previous frame (sent_) for its differential refresh
|
||||
virtual bool needs_previous_frame() const { return false; }
|
||||
// FNV-1a hash of the current frame buffer, to detect an unchanged frame without a full copy
|
||||
uint32_t buffer_hash_() const;
|
||||
void reset_bounds_();
|
||||
void set_full_bounds_();
|
||||
// For a draw that writes the whole buffer without going through rotate_coordinates_()
|
||||
void mark_all_drawn_() {
|
||||
this->set_full_bounds_();
|
||||
this->drawn_since_hash_ = true;
|
||||
}
|
||||
void update_effective_transform_();
|
||||
bool rotate_coordinates_(int &x, int &y);
|
||||
void draw_rgb565_block_(uint8_t *buffer, int x_start, int y_start, int w, int h, const uint8_t *ptr, bool big_endian,
|
||||
@@ -200,6 +216,13 @@ class EPaperBase : public Display,
|
||||
size_t buffer_length_;
|
||||
size_t current_data_index_{}; // used by data transfer to track progress
|
||||
split_buffer::SplitBuffer buffer_{};
|
||||
split_buffer::SplitBuffer sent_{}; // the previous frame, kept only by drivers that diff against it
|
||||
bool sent_valid_{}; // sent_ holds a whole frame
|
||||
size_t sent_count_{}; // bytes recorded into sent_ by the update in progress
|
||||
uint32_t shown_hash_{}; // hash of the frame on the panel, to skip an unchanged update
|
||||
bool shown_hash_valid_{}; // shown_hash_ holds the hash of a frame whose refresh completed
|
||||
bool sent_hash_intact_{}; // the buffer was unchanged by the end of the push of shown_hash_
|
||||
bool drawn_since_hash_{}; // something was drawn after shown_hash_ was taken
|
||||
GPIOPin *dc_pin_{};
|
||||
GPIOPin *busy_pin_{};
|
||||
GPIOPin *reset_pin_{};
|
||||
|
||||
@@ -18,11 +18,7 @@ void EPaper4bpp::fill(Color color) {
|
||||
// We store 2 pixels per byte
|
||||
this->buffer_.fill(pixel_color + (pixel_color << 4));
|
||||
|
||||
// Whole buffer just changed; mark the entire canvas dirty.
|
||||
this->x_low_ = 0;
|
||||
this->y_low_ = 0;
|
||||
this->x_high_ = this->width_;
|
||||
this->y_high_ = this->height_;
|
||||
this->mark_all_drawn_();
|
||||
}
|
||||
|
||||
void EPaper4bpp::clear() {
|
||||
|
||||
@@ -71,10 +71,7 @@ void EPaperInkplate2::fill(Color color) {
|
||||
for (size_t i = half_buffer; i < this->buffer_length_; i++)
|
||||
this->buffer_[i] = red_byte;
|
||||
|
||||
this->x_low_ = 0;
|
||||
this->y_low_ = 0;
|
||||
this->x_high_ = this->width_;
|
||||
this->y_high_ = this->height_;
|
||||
this->mark_all_drawn_();
|
||||
}
|
||||
|
||||
void EPaperInkplate2::clear() { this->fill(COLOR_ON); }
|
||||
|
||||
@@ -30,6 +30,7 @@ void EPaperJD79660::fill(Color color) {
|
||||
|
||||
// We store 4 pixels per byte
|
||||
this->buffer_.fill(pixel_color | (pixel_color << 2) | (pixel_color << 4) | (pixel_color << 6));
|
||||
this->drawn_since_hash_ = true;
|
||||
}
|
||||
|
||||
void HOT EPaperJD79660::draw_pixel_at(int x, int y, Color color) {
|
||||
|
||||
@@ -7,6 +7,12 @@
|
||||
namespace esphome::epaper_spi {
|
||||
ESPHOME_LOG_TAG(TAG, "epaper_spi.mono");
|
||||
|
||||
void EPaperMono::setup() {
|
||||
EPaperBase::setup();
|
||||
if (!this->is_failed() && this->needs_previous_frame())
|
||||
this->init_sent_frame_(this->sent_frame_length());
|
||||
}
|
||||
|
||||
void EPaperMono::refresh_screen(bool partial) {
|
||||
ESP_LOGV(TAG, "Refresh screen");
|
||||
this->cmd_data(0x22, {partial ? (uint8_t) 0xFF : (uint8_t) 0xF7});
|
||||
@@ -66,7 +72,8 @@ bool HOT EPaperMono::transfer_data() {
|
||||
ESP_LOGV(TAG, "Writing %u bytes at line %zu at %ums", row_length, this->current_data_index_, (unsigned) millis());
|
||||
this->start_data_();
|
||||
while (this->current_data_index_ != this->y_high_) {
|
||||
size_t data_idx = this->current_data_index_ * this->row_width_ + this->x_low_ / 8;
|
||||
const size_t row_start = this->current_data_index_ * this->row_width_ + this->x_low_ / 8;
|
||||
size_t data_idx = row_start;
|
||||
for (size_t i = 0; i != row_length; i++) {
|
||||
bytes_to_send[i] = this->send_red_ && !this->send_red_as_image_ ? 0 : this->buffer_[data_idx++];
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ class EPaperMono : public EPaperBase {
|
||||
size_t init_sequence_length, DisplayType display_type = DISPLAY_TYPE_BINARY, uint8_t bits_per_pixel = 1)
|
||||
: EPaperBase(name, width, height, init_sequence, init_sequence_length, display_type, bits_per_pixel) {}
|
||||
|
||||
void setup() override;
|
||||
|
||||
protected:
|
||||
void refresh_screen(bool partial) override;
|
||||
void power_on() override {}
|
||||
@@ -19,6 +21,8 @@ class EPaperMono : public EPaperBase {
|
||||
void deep_sleep() override;
|
||||
bool reset() override;
|
||||
virtual void set_window();
|
||||
// Bytes in the comparison frame; the buffer's size unless a subclass sends the frame in another layout
|
||||
virtual size_t sent_frame_length() const { return this->buffer_length_; }
|
||||
bool transfer_data() override;
|
||||
bool send_red_{true};
|
||||
bool software_reset_{true}; // send 0x12 after the hardware reset
|
||||
|
||||
@@ -6,21 +6,6 @@
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::epaper_spi {
|
||||
ESPHOME_LOG_TAG(TAG, "epaper_spi.ssd1677");
|
||||
|
||||
void EPaperSSD1677::setup() {
|
||||
EPaperMono::setup();
|
||||
if (!this->is_failed())
|
||||
this->init_comparison_frame_();
|
||||
}
|
||||
|
||||
void EPaperSSD1677::init_comparison_frame_() {
|
||||
if (!this->is_using_partial_update_())
|
||||
return;
|
||||
if (!this->sent_.init(this->plane_row_length_() * this->height_)) {
|
||||
ESP_LOGW(TAG, "No memory for the comparison frame; partial updates will degrade unchanged areas");
|
||||
}
|
||||
}
|
||||
|
||||
void EPaperSSD1677::plane_row(size_t y, uint8_t *out) {
|
||||
const size_t row_length = this->plane_row_length_();
|
||||
@@ -82,7 +67,7 @@ bool HOT EPaperSSD1677::transfer_data() {
|
||||
}
|
||||
this->write_array(data, length);
|
||||
if (this->plane_ == 1)
|
||||
this->sent_.write(this->current_data_index_, data, length);
|
||||
this->record_sent_(this->current_data_index_, data, length);
|
||||
this->current_data_index_ += length;
|
||||
if (this->current_data_index_ != plane_length && millis() - start_time > MAX_TRANSFER_TIME) {
|
||||
// Let the main loop run and come back next loop
|
||||
|
||||
@@ -26,12 +26,12 @@ class EPaperSSD1677 : public EPaperMono {
|
||||
size_t init_sequence_length, DisplayType display_type = DISPLAY_TYPE_BINARY, uint8_t bits_per_pixel = 1)
|
||||
: EPaperMono(name, width, height, init_sequence, init_sequence_length, display_type, bits_per_pixel) {}
|
||||
|
||||
void setup() override;
|
||||
|
||||
protected:
|
||||
// Allocates the comparison frame when partial updates are enabled. Separate from setup() so it
|
||||
// can run without a bus.
|
||||
void init_comparison_frame_();
|
||||
// Reads the previous frame to copy it into the other RAM bank on a full refresh
|
||||
bool needs_previous_frame() const override { return true; }
|
||||
// Allocates the comparison frame as setup() would, for tests that run without a bus.
|
||||
void init_comparison_frame_() { this->init_sent_frame_(this->sent_frame_length()); }
|
||||
size_t sent_frame_length() const override { return this->plane_row_length_() * this->height_; }
|
||||
// Bytes in one row of a RAM plane: 8 pixels per byte, whatever the buffer holds.
|
||||
size_t plane_row_length_() const { return (this->width_ + 7) / 8; }
|
||||
// Row y of the frame as a RAM plane holds it, 1 bit per pixel with 1 = white.
|
||||
@@ -39,8 +39,7 @@ class EPaperSSD1677 : public EPaperMono {
|
||||
bool reset() override;
|
||||
bool transfer_data() override;
|
||||
|
||||
split_buffer::SplitBuffer sent_{}; // the frame last sent to 0x24, i.e. what the panel shows
|
||||
uint8_t plane_{0}; // 0 while sending 0x26, 1 while sending 0x24
|
||||
uint8_t plane_{0}; // 0 while sending 0x26, 1 while sending 0x24
|
||||
};
|
||||
|
||||
} // namespace esphome::epaper_spi
|
||||
|
||||
@@ -29,10 +29,7 @@ void EPaperSSD1677Gray4::fill(Color color) {
|
||||
}
|
||||
const uint8_t level = color_to_gray4(color);
|
||||
this->buffer_.fill((uint8_t) (level | (level << 2) | (level << 4) | (level << 6)));
|
||||
this->x_low_ = 0;
|
||||
this->y_low_ = 0;
|
||||
this->x_high_ = this->width_;
|
||||
this->y_high_ = this->height_;
|
||||
this->mark_all_drawn_();
|
||||
}
|
||||
|
||||
void HOT EPaperSSD1677Gray4::draw_pixel_at(int x, int y, Color color) {
|
||||
@@ -83,14 +80,14 @@ bool HOT EPaperSSD1677Gray4::transfer_data() {
|
||||
this->start_data_();
|
||||
while (this->current_data_index_ != this->y_high_) {
|
||||
const size_t src_row = this->current_data_index_ * this->row_width_ + this->x_low_ / 4;
|
||||
for (size_t i = 0; i != plane_row_length; i++) {
|
||||
const uint8_t plane = plane_byte(this->buffer_[src_row + 2 * i], this->buffer_[src_row + 2 * i + 1], first_pass);
|
||||
// The OTP grayscale waveform treats data as inverted relative to monochrome
|
||||
bytes_to_send[i] = (uint8_t) ~plane;
|
||||
// What the next partial update compares against: the high bits, as a black-and-white frame.
|
||||
if (first_pass && this->sent_.is_valid())
|
||||
this->sent_[this->current_data_index_ * plane_row_length + i] = plane;
|
||||
}
|
||||
for (size_t i = 0; i != plane_row_length; i++)
|
||||
bytes_to_send[i] = plane_byte(this->buffer_[src_row + 2 * i], this->buffer_[src_row + 2 * i + 1], first_pass);
|
||||
// What the next partial update compares against: the high bits, as a black-and-white frame.
|
||||
if (first_pass)
|
||||
this->record_sent_(this->current_data_index_ * plane_row_length, bytes_to_send, plane_row_length);
|
||||
// The OTP grayscale waveform treats data as inverted relative to monochrome
|
||||
for (size_t i = 0; i != plane_row_length; i++)
|
||||
bytes_to_send[i] = (uint8_t) ~bytes_to_send[i];
|
||||
++this->current_data_index_;
|
||||
this->write_array(bytes_to_send, plane_row_length);
|
||||
if (millis() - start_time > MAX_TRANSFER_TIME) {
|
||||
|
||||
@@ -1,10 +1,50 @@
|
||||
#include "epaper_spi_ssd1681.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::epaper_spi {
|
||||
ESPHOME_LOG_TAG(TAG, "epaper_spi.ssd1681");
|
||||
|
||||
void EPaperSSD1681::bounds_from_changes_() {
|
||||
if (!this->sent_valid_ || this->sent_.size() != this->buffer_.size())
|
||||
return;
|
||||
bool changed = false;
|
||||
uint16_t row_low = this->height_, row_high = 0, col_low = this->row_width_, col_high = 0;
|
||||
for (uint16_t row = 0; row != this->height_; row++) {
|
||||
const size_t base = row * this->row_width_;
|
||||
const size_t end = base + this->row_width_;
|
||||
size_t index = base;
|
||||
while (index != end) {
|
||||
size_t new_length, old_length;
|
||||
const uint8_t *new_bytes = this->buffer_.get_span(index, new_length);
|
||||
const uint8_t *old_bytes = this->sent_.get_span(index, old_length);
|
||||
const size_t length = std::min({new_length, old_length, end - index});
|
||||
if (std::memcmp(new_bytes, old_bytes, length) != 0) {
|
||||
for (size_t i = 0; i != length; i++) {
|
||||
if (new_bytes[i] == old_bytes[i])
|
||||
continue;
|
||||
const auto col = static_cast<uint16_t>(index + i - base);
|
||||
changed = true;
|
||||
row_low = std::min(row_low, row);
|
||||
row_high = std::max<uint16_t>(row_high, row + 1);
|
||||
col_low = std::min(col_low, col);
|
||||
col_high = std::max<uint16_t>(col_high, col + 1);
|
||||
}
|
||||
}
|
||||
index += length;
|
||||
}
|
||||
}
|
||||
if (!changed)
|
||||
return;
|
||||
this->x_low_ = col_low * 8;
|
||||
this->x_high_ = std::min<uint16_t>(col_high * 8, this->width_);
|
||||
this->y_low_ = row_low;
|
||||
this->y_high_ = row_high;
|
||||
}
|
||||
|
||||
bool HOT EPaperSSD1681::transfer_data() {
|
||||
if (this->current_data_index_ == 0 && this->send_red_) {
|
||||
if (!this->partial_) {
|
||||
@@ -15,6 +55,7 @@ bool HOT EPaperSSD1681::transfer_data() {
|
||||
this->y_high_ = this->height_;
|
||||
} else {
|
||||
// 0x26 already holds the image on the panel: the controller copies 0x24 into it after each refresh
|
||||
this->bounds_from_changes_();
|
||||
this->set_window();
|
||||
this->send_red_ = false;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,10 @@ class EPaperSSD1681 : public EPaperSSD1683 {
|
||||
}
|
||||
|
||||
protected:
|
||||
// Reads the previous frame to compute the changed window for a partial refresh
|
||||
bool needs_previous_frame() const override { return true; }
|
||||
// Shrinks the update bounds to the bytes that differ from the frame last sent
|
||||
void bounds_from_changes_();
|
||||
bool transfer_data() override;
|
||||
void refresh_screen(bool partial) override;
|
||||
};
|
||||
|
||||
@@ -71,12 +71,15 @@ bool HOT EPaperSSD1683::transfer_data() {
|
||||
ESP_LOGV(TAG, "Writing %u bytes at line %zu at %ums", row_length, this->current_data_index_, (unsigned) millis());
|
||||
this->start_data_();
|
||||
while (this->current_data_index_ != this->y_high_) {
|
||||
size_t data_idx = this->current_data_index_ * this->row_width_ + this->x_low_;
|
||||
const size_t row_start = this->current_data_index_ * this->row_width_ + this->x_low_;
|
||||
size_t data_idx = row_start;
|
||||
for (size_t i = 0; i != row_length; i++) {
|
||||
bytes_to_send[i] = this->buffer_[data_idx++];
|
||||
}
|
||||
++this->current_data_index_;
|
||||
this->write_array(&bytes_to_send.front(), row_length); // NOLINT
|
||||
if (!this->send_red_)
|
||||
this->record_sent_(row_start, &bytes_to_send.front(), row_length);
|
||||
if (millis() - start_time > MAX_TRANSFER_TIME) {
|
||||
// Let the main loop run and come back next loop
|
||||
this->disable();
|
||||
|
||||
@@ -232,6 +232,7 @@ void EPaperT133A01::fill(Color color) {
|
||||
}
|
||||
auto pixel_color = color_to_index(color);
|
||||
this->buffer_.fill(pixel_color + (pixel_color << 4));
|
||||
this->drawn_since_hash_ = true;
|
||||
}
|
||||
|
||||
void EPaperT133A01::draw_pixel_at(int x, int y, Color color) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "epaper_waveshare_bwr.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
@@ -36,17 +37,9 @@ void EPaperWaveshareBWR::draw_pixel_at(int x, int y, Color color) {
|
||||
|
||||
const auto bwr = color_to_bwr(color);
|
||||
|
||||
if (bwr == BwrState::BWR_BLACK) {
|
||||
this->buffer_[pos] |= bit;
|
||||
} else {
|
||||
this->buffer_[pos] &= ~bit;
|
||||
}
|
||||
assign_bits(this->buffer_[pos], bit, bwr == BwrState::BWR_BLACK);
|
||||
|
||||
if (bwr == BwrState::BWR_RED) {
|
||||
this->buffer_[red_offset + pos] |= bit;
|
||||
} else {
|
||||
this->buffer_[red_offset + pos] &= ~bit;
|
||||
}
|
||||
assign_bits(this->buffer_[red_offset + pos], bit, bwr == BwrState::BWR_RED);
|
||||
}
|
||||
|
||||
void EPaperWaveshareBWR::fill(Color color) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "es8156.h"
|
||||
#include "es8156_const.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cinttypes>
|
||||
|
||||
@@ -100,11 +101,7 @@ bool ES8156::set_mute_state_(bool mute_state) {
|
||||
|
||||
ESP_LOGV(TAG, "Read ES8156_REG13_DAC_MUTE: %u", reg13);
|
||||
|
||||
if (mute_state) {
|
||||
reg13 |= BIT(1) | BIT(2);
|
||||
} else {
|
||||
reg13 &= ~(BIT(1) | BIT(2));
|
||||
}
|
||||
assign_bits(reg13, BIT(1) | BIT(2), mute_state);
|
||||
|
||||
ESP_LOGV(TAG, "Setting ES8156_REG13_DAC_MUTE to %u (muted: %s)", reg13, YESNO(mute_state));
|
||||
return this->write_byte(ES8156_REG13_DAC_MUTE, reg13);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "es8311.h"
|
||||
#include "es8311_const.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cinttypes>
|
||||
|
||||
@@ -154,11 +155,7 @@ bool ES8311::configure_clock_() {
|
||||
// Register 0x06
|
||||
uint8_t reg06;
|
||||
ES8311_ERROR_CHECK(this->read_byte(ES8311_REG06_CLK_MANAGER, ®06));
|
||||
if (this->sclk_inverted_) {
|
||||
reg06 |= BIT(5);
|
||||
} else {
|
||||
reg06 &= ~BIT(5);
|
||||
}
|
||||
assign_bits(reg06, BIT(5), this->sclk_inverted_);
|
||||
reg06 &= 0xE0;
|
||||
if (coefficient->bclk_div < 19) {
|
||||
reg06 |= (coefficient->bclk_div - 1) << 0;
|
||||
@@ -223,11 +220,7 @@ bool ES8311::set_mute_state_(bool mute_state) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mute_state) {
|
||||
reg31 |= BIT(6) | BIT(5);
|
||||
} else {
|
||||
reg31 &= ~(BIT(6) | BIT(5));
|
||||
}
|
||||
assign_bits(reg31, BIT(6) | BIT(5), mute_state);
|
||||
|
||||
return this->write_byte(ES8311_REG31_DAC, reg31);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <cinttypes>
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::es8388 {
|
||||
@@ -184,11 +185,7 @@ bool ES8388::set_mute_state_(bool mute_state) {
|
||||
// DAC settings that must be preserved. Previously muting overwrote the whole
|
||||
// register with 0x3C and unmuting never cleared the bit, so once muted the DAC
|
||||
// could not be unmuted again.
|
||||
if (mute_state) {
|
||||
value |= ES8388_DACCONTROL3_DAC_MUTE;
|
||||
} else {
|
||||
value &= ~ES8388_DACCONTROL3_DAC_MUTE;
|
||||
}
|
||||
assign_bits(value, ES8388_DACCONTROL3_DAC_MUTE, mute_state);
|
||||
|
||||
ESP_LOGV(TAG, "Setting ES8388_DACCONTROL3 to 0x%02X (muted: %s)", value, YESNO(mute_state));
|
||||
return this->write_byte(ES8388_DACCONTROL3, value);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "gree.h"
|
||||
#include "esphome/components/remote_base/remote_base.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::gree {
|
||||
|
||||
@@ -24,11 +25,7 @@ void GreeClimate::set_model(Model model) {
|
||||
}
|
||||
|
||||
void GreeClimate::set_mode_bit(uint8_t bit_mask, bool enabled) {
|
||||
if (enabled) {
|
||||
this->mode_bits_ |= bit_mask;
|
||||
} else {
|
||||
this->mode_bits_ &= ~bit_mask;
|
||||
}
|
||||
assign_bits(this->mode_bits_, bit_mask, enabled);
|
||||
this->transmit_state();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "hitachi_ac344.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::hitachi_ac344 {
|
||||
|
||||
@@ -16,15 +17,6 @@ void set_bits(uint8_t *const dst, const uint8_t offset, const uint8_t nbits, con
|
||||
*dst |= ((data & mask) << offset);
|
||||
}
|
||||
|
||||
void set_bit(uint8_t *const data, const uint8_t position, const bool on) {
|
||||
uint8_t mask = 1 << position;
|
||||
if (on) {
|
||||
*data |= mask;
|
||||
} else {
|
||||
*data &= ~mask;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t *invert_byte_pairs(uint8_t *ptr, const uint16_t length) {
|
||||
for (uint16_t i = 1; i < length; i += 2) {
|
||||
// Code done this way to avoid a compiler warning bug.
|
||||
@@ -124,7 +116,7 @@ bool HitachiClimate::get_swing_v_toggle_() { return get_button_() == HITACHI_AC3
|
||||
|
||||
void HitachiClimate::set_swing_v_(bool on) {
|
||||
set_swing_v_toggle_(on); // Set the button value.
|
||||
set_bit(&remote_state_[HITACHI_AC344_SWINGV_BYTE], HITACHI_AC344_SWINGV_OFFSET, on);
|
||||
assign_bits(this->remote_state_[HITACHI_AC344_SWINGV_BYTE], 1 << HITACHI_AC344_SWINGV_OFFSET, on);
|
||||
}
|
||||
|
||||
bool HitachiClimate::get_swing_v_() {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "hitachi_ac424.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::hitachi_ac424 {
|
||||
|
||||
@@ -16,15 +17,6 @@ void set_bits(uint8_t *const dst, const uint8_t offset, const uint8_t nbits, con
|
||||
*dst |= ((data & mask) << offset);
|
||||
}
|
||||
|
||||
void set_bit(uint8_t *const data, const uint8_t position, const bool on) {
|
||||
uint8_t mask = 1 << position;
|
||||
if (on) {
|
||||
*data |= mask;
|
||||
} else {
|
||||
*data &= ~mask;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t *invert_byte_pairs(uint8_t *ptr, const uint16_t length) {
|
||||
for (uint16_t i = 1; i < length; i += 2) {
|
||||
// Code done this way to avoid a compiler warning bug.
|
||||
@@ -124,7 +116,7 @@ bool HitachiClimate::get_swing_v_toggle_() { return get_button_() == HITACHI_AC4
|
||||
|
||||
void HitachiClimate::set_swing_v_(bool on) {
|
||||
set_swing_v_toggle_(on); // Set the button value.
|
||||
set_bit(&remote_state_[HITACHI_AC424_SWINGV_BYTE], HITACHI_AC424_SWINGV_OFFSET, on);
|
||||
assign_bits(this->remote_state_[HITACHI_AC424_SWINGV_BYTE], 1 << HITACHI_AC424_SWINGV_OFFSET, on);
|
||||
}
|
||||
|
||||
bool HitachiClimate::get_swing_v_() {
|
||||
|
||||
@@ -3,67 +3,36 @@
|
||||
|
||||
namespace esphome::max17043 {
|
||||
|
||||
// MAX174043 is a 1-Cell Fuel Gauge with ModelGauge and Low-Battery Alert
|
||||
// MAX17043 is a 1-Cell Fuel Gauge with ModelGauge and Low-Battery Alert
|
||||
// Consult the datasheet at https://www.analog.com/en/products/max17043.html
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "max17043");
|
||||
|
||||
static const uint8_t MAX17043_VCELL = 0x02;
|
||||
static const uint8_t MAX17043_SOC = 0x04;
|
||||
static const uint8_t MAX17043_CONFIG = 0x0c;
|
||||
|
||||
static const uint16_t MAX17043_CONFIG_POWER_UP_DEFAULT = 0x971C;
|
||||
static const uint16_t MAX17043_CONFIG_SAFE_MASK = 0xFF1F; // mask out sleep bit (7), unused bit (6) and alert bit (4)
|
||||
static const uint16_t MAX17043_CONFIG_SLEEP_MASK = 0x0080;
|
||||
|
||||
void MAX17043Component::update() {
|
||||
uint16_t raw_voltage, raw_percent;
|
||||
if (!this->ready_for_update_())
|
||||
return;
|
||||
|
||||
uint16_t raw;
|
||||
bool ok = true;
|
||||
|
||||
if (this->voltage_sensor_ != nullptr) {
|
||||
if (!this->read_byte_16(MAX17043_VCELL, &raw_voltage)) {
|
||||
this->status_set_warning(LOG_STR("Unable to read MAX17043_VCELL"));
|
||||
if (this->read_byte_16(max170xx::REG_VCELL, &raw)) {
|
||||
// 12 bits in the top of the word at 1.25 mV
|
||||
this->voltage_sensor_->publish_state((raw >> 4) * 1.25e-3f);
|
||||
} else {
|
||||
float voltage = (1.25f * (float) (raw_voltage >> 4)) / 1000.0f;
|
||||
this->voltage_sensor_->publish_state(voltage);
|
||||
this->status_clear_warning();
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
if (this->battery_remaining_sensor_ != nullptr) {
|
||||
if (!this->read_byte_16(MAX17043_SOC, &raw_percent)) {
|
||||
this->status_set_warning(LOG_STR("Unable to read MAX17043_SOC"));
|
||||
} else {
|
||||
float percent = (float) ((raw_percent >> 8) + 0.003906f * (raw_percent & 0x00ff));
|
||||
if (this->read_byte_16(max170xx::REG_SOC, &raw)) {
|
||||
float percent = (float) ((raw >> 8) + 0.003906f * (raw & 0x00ff));
|
||||
this->battery_remaining_sensor_->publish_state(percent);
|
||||
this->status_clear_warning();
|
||||
} else {
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MAX17043Component::setup() {
|
||||
uint16_t config_reg;
|
||||
if (this->write(&MAX17043_CONFIG, 1) != i2c::ERROR_OK) {
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->read(reinterpret_cast<uint8_t *>(&config_reg), 2) != i2c::ERROR_OK) {
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
|
||||
config_reg = i2c::i2ctohs(config_reg) & MAX17043_CONFIG_SAFE_MASK;
|
||||
ESP_LOGV(TAG, "MAX17043 CONFIG register reads 0x%X", config_reg);
|
||||
|
||||
if (config_reg != MAX17043_CONFIG_POWER_UP_DEFAULT) {
|
||||
this->mark_failed(LOG_STR("Device does not appear to be a MAX17043"));
|
||||
return;
|
||||
}
|
||||
|
||||
// need to write back to config register to reset the sleep bit
|
||||
if (!this->write_byte_16(MAX17043_CONFIG, MAX17043_CONFIG_POWER_UP_DEFAULT)) {
|
||||
this->mark_failed(LOG_STR("sleep reset failed"));
|
||||
return;
|
||||
}
|
||||
this->status_comm_result(ok, LOG_STR("Register read failed"));
|
||||
}
|
||||
|
||||
void MAX17043Component::dump_config() {
|
||||
@@ -74,13 +43,4 @@ void MAX17043Component::dump_config() {
|
||||
LOG_SENSOR(" ", "Battery Level", this->battery_remaining_sensor_);
|
||||
}
|
||||
|
||||
void MAX17043Component::sleep_mode() {
|
||||
if (!this->is_failed()) {
|
||||
if (!this->write_byte_16(MAX17043_CONFIG, MAX17043_CONFIG_POWER_UP_DEFAULT | MAX17043_CONFIG_SLEEP_MASK)) {
|
||||
ESP_LOGW(TAG, "Unable to write the sleep bit to config register");
|
||||
this->status_set_warning();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::max17043
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/max170xx/max170xx.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
|
||||
namespace esphome::max17043 {
|
||||
|
||||
class MAX17043Component final : public PollingComponent, public i2c::I2CDevice {
|
||||
class MAX17043Component final : public max170xx::MAX170xxComponent<max170xx::Model::MODEL_MAX17043> {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void update() override;
|
||||
void sleep_mode();
|
||||
|
||||
void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; }
|
||||
void set_voltage_sensor(sensor::Sensor *voltage_sensor) { this->voltage_sensor_ = voltage_sensor; }
|
||||
void set_battery_remaining_sensor(sensor::Sensor *battery_remaining_sensor) {
|
||||
battery_remaining_sensor_ = battery_remaining_sensor;
|
||||
this->battery_remaining_sensor_ = battery_remaining_sensor;
|
||||
}
|
||||
|
||||
protected:
|
||||
|
||||
@@ -16,6 +16,7 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
AUTO_LOAD = ["max170xx"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
max17043_ns = cg.esphome_ns.namespace("max17043")
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
CODEOWNERS = ["@B48D81EFCC"]
|
||||
CODEOWNERS = ["@B48D81EFCC", "@ryansch"]
|
||||
DOMAIN = "max17048"
|
||||
|
||||
@@ -8,41 +8,27 @@ namespace esphome::max17048 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "max17048");
|
||||
|
||||
static constexpr uint8_t REG_VCELL = 0x02; // cell voltage
|
||||
static constexpr uint8_t REG_SOC = 0x04; // state of charge
|
||||
static constexpr uint8_t REG_VERSION = 0x08; // 0x001x
|
||||
static constexpr uint8_t REG_CRATE = 0x16; // charge rate, signed
|
||||
|
||||
static constexpr uint16_t VERSION_MASK = 0xFFF0;
|
||||
static constexpr uint16_t VERSION_FAMILY = 0x0010;
|
||||
static constexpr uint8_t REG_CRATE = 0x16; // charge rate, signed
|
||||
|
||||
static constexpr float VCELL_VOLTS_PER_LSB = 78.125e-6f;
|
||||
static constexpr float SOC_PERCENT_PER_LSB = 1.0f / 256.0f;
|
||||
static constexpr float CRATE_PERCENT_PER_HOUR_PER_LSB = 0.208f;
|
||||
|
||||
void MAX17048Component::setup() {
|
||||
uint16_t version;
|
||||
if (!this->read_byte_16(REG_VERSION, &version)) {
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
if ((version & VERSION_MASK) != VERSION_FAMILY) {
|
||||
ESP_LOGW(TAG, "Unexpected version 0x%04X", version);
|
||||
}
|
||||
}
|
||||
|
||||
void MAX17048Component::update() {
|
||||
if (!this->ready_for_update_())
|
||||
return;
|
||||
|
||||
bool ok = true;
|
||||
uint16_t raw;
|
||||
if (this->battery_voltage_sensor_ != nullptr) {
|
||||
if (this->read_byte_16(REG_VCELL, &raw)) {
|
||||
if (this->read_byte_16(max170xx::REG_VCELL, &raw)) {
|
||||
this->battery_voltage_sensor_->publish_state(raw * VCELL_VOLTS_PER_LSB);
|
||||
} else {
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
if (this->battery_level_sensor_ != nullptr) {
|
||||
if (this->read_byte_16(REG_SOC, &raw)) {
|
||||
if (this->read_byte_16(max170xx::REG_SOC, &raw)) {
|
||||
this->battery_level_sensor_->publish_state(raw * SOC_PERCENT_PER_LSB);
|
||||
} else {
|
||||
ok = false;
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/max170xx/max170xx.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
|
||||
namespace esphome::max17048 {
|
||||
|
||||
class MAX17048Component final : public PollingComponent, public i2c::I2CDevice {
|
||||
class MAX17048Component final : public max170xx::MAX170xxComponent<max170xx::Model::MODEL_MAX17048> {
|
||||
SUB_SENSOR(battery_voltage)
|
||||
SUB_SENSOR(battery_level)
|
||||
SUB_SENSOR(rate)
|
||||
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void update() override;
|
||||
};
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
from esphome import automation
|
||||
from esphome.automation import maybe_simple_id
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BATTERY_LEVEL,
|
||||
CONF_BATTERY_VOLTAGE,
|
||||
CONF_ID,
|
||||
CONF_RATE,
|
||||
DEVICE_CLASS_BATTERY,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
@@ -14,6 +17,7 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
AUTO_LOAD = ["max170xx"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
UNIT_PERCENT_PER_HOUR = "%/h"
|
||||
@@ -61,3 +65,15 @@ async def to_code(config: ConfigType) -> None:
|
||||
await sensors(CONF_BATTERY_VOLTAGE, var.set_battery_voltage_sensor)
|
||||
await sensors(CONF_BATTERY_LEVEL, var.set_battery_level_sensor)
|
||||
await sensors(CONF_RATE, var.set_rate_sensor)
|
||||
|
||||
|
||||
MAX17048_ACTION_SCHEMA = maybe_simple_id(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(MAX17048Component),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
automation.register_apply_action(
|
||||
"max17048.sleep_mode", MAX17048_ACTION_SCHEMA, automation.ApplyCall("sleep_mode()")
|
||||
)
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
CODEOWNERS = ["@ryansch"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
DOMAIN = "max170xx"
|
||||
@@ -0,0 +1,87 @@
|
||||
#include "max170xx.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::max170xx {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "max170xx");
|
||||
|
||||
template<Model MODEL> void MAX170xxComponent<MODEL>::setup() {
|
||||
// A silent bus is retried from update(): some boards gate the I2C rail until after setup.
|
||||
// An early on_boot may already have configured the gauge and put it to sleep, so this must not configure again.
|
||||
this->ready_for_update_();
|
||||
}
|
||||
|
||||
template<Model MODEL> bool MAX170xxComponent<MODEL>::ready_for_update_() {
|
||||
if (this->configured_ || this->configure_())
|
||||
return true;
|
||||
this->status_set_warning(LOG_STR("Gauge did not answer"));
|
||||
return false;
|
||||
}
|
||||
|
||||
template<Model MODEL> bool MAX170xxComponent<MODEL>::configure_() {
|
||||
uint16_t config;
|
||||
if (!this->read_byte_16(REG_CONFIG, &config))
|
||||
return false;
|
||||
|
||||
const bool was_asleep = (config & CONFIG_SLEEP) != 0;
|
||||
// RCOMP and ATHD survive on the battery, so an earlier firmware's values are reset here, not treated as a fault
|
||||
if ((config & CONFIG_SAFE_MASK) != CONFIG_POWER_UP_DEFAULT) {
|
||||
ESP_LOGW(TAG, "CONFIG reads 0x%04X, expected 0x%04X; resetting it", config & CONFIG_SAFE_MASK,
|
||||
CONFIG_POWER_UP_DEFAULT);
|
||||
}
|
||||
|
||||
uint16_t version;
|
||||
if (this->read_byte_16(REG_VERSION, &version)) {
|
||||
ESP_LOGCONFIG(TAG, "IC version: 0x%04X", version);
|
||||
}
|
||||
|
||||
// Write back the power-up value to clear a stale sleep bit; a failed write is retried like a silent bus
|
||||
if (!this->write_byte_16(REG_CONFIG, CONFIG_POWER_UP_DEFAULT))
|
||||
return false;
|
||||
|
||||
this->configured_ = true;
|
||||
// Waking from sleep raises a false low-battery alert; a real one at an ordinary boot is left alone
|
||||
if (was_asleep)
|
||||
this->set_timeout(WAKE_ALERT_CLEAR_TIMEOUT_ID, WAKE_ALERT_CLEAR_DELAY_MS, [this]() { this->clear_wake_alert_(); });
|
||||
return true;
|
||||
}
|
||||
|
||||
template<Model MODEL> void MAX170xxComponent<MODEL>::clear_wake_alert_() {
|
||||
uint16_t config;
|
||||
if (!this->read_byte_16(REG_CONFIG, &config) || !this->write_byte_16(REG_CONFIG, config & ~CONFIG_ALRT)) {
|
||||
this->status_set_warning(LOG_STR("Wake alert clear failed"));
|
||||
return;
|
||||
}
|
||||
if constexpr (MODEL == Model::MODEL_MAX17048) {
|
||||
uint16_t status;
|
||||
if (!this->read_byte_16(REG_STATUS, &status) || !this->write_byte_16(REG_STATUS, status & ~STATUS_HD))
|
||||
this->status_set_warning(LOG_STR("Wake alert clear failed"));
|
||||
}
|
||||
}
|
||||
|
||||
template<Model MODEL> void MAX170xxComponent<MODEL>::sleep_mode() {
|
||||
if (this->is_failed())
|
||||
return;
|
||||
// The deferred configuration clears the sleep bit, so it has to run before the gauge is put to sleep
|
||||
if (!this->ready_for_update_())
|
||||
return;
|
||||
|
||||
// A clear still pending would otherwise run against a sleeping gauge
|
||||
if (this->cancel_timeout(WAKE_ALERT_CLEAR_TIMEOUT_ID))
|
||||
this->clear_wake_alert_();
|
||||
|
||||
if constexpr (MODEL == Model::MODEL_MAX17048) {
|
||||
if (!this->write_byte_16(REG_MODE, MODE_EN_SLEEP)) {
|
||||
this->status_set_warning(LOG_STR("Unable to enable sleep mode in the mode register"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this->write_byte_16(REG_CONFIG, CONFIG_POWER_UP_DEFAULT | CONFIG_SLEEP))
|
||||
this->status_set_warning(LOG_STR("Unable to write the sleep bit to config register"));
|
||||
}
|
||||
|
||||
template class MAX170xxComponent<Model::MODEL_MAX17043>;
|
||||
template class MAX170xxComponent<Model::MODEL_MAX17048>;
|
||||
|
||||
} // namespace esphome::max170xx
|
||||
@@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
|
||||
namespace esphome::max170xx {
|
||||
|
||||
// Shared base for the ModelGauge fuel gauges.
|
||||
// MAX17043: https://www.analog.com/en/products/max17043.html
|
||||
// MAX17048: https://www.analog.com/en/products/max17048.html
|
||||
// The MAX17048 is register-compatible with the MAX17043, and adds a STATUS register and a MODE.EnSleep gate in
|
||||
// front of CONFIG.SLEEP.
|
||||
|
||||
static constexpr uint8_t REG_VCELL = 0x02;
|
||||
static constexpr uint8_t REG_SOC = 0x04;
|
||||
static constexpr uint8_t REG_MODE = 0x06;
|
||||
static constexpr uint8_t REG_VERSION = 0x08;
|
||||
static constexpr uint8_t REG_CONFIG = 0x0C;
|
||||
static constexpr uint8_t REG_STATUS = 0x1A;
|
||||
|
||||
static constexpr uint16_t CONFIG_POWER_UP_DEFAULT = 0x971C;
|
||||
// Mask out SLEEP (7), ALRT (5), and bit 6, which is a don't-care on the MAX17043 but ALSC on the MAX17048.
|
||||
static constexpr uint16_t CONFIG_SAFE_MASK = 0xFF1F;
|
||||
static constexpr uint16_t CONFIG_SLEEP = 0x0080;
|
||||
static constexpr uint16_t CONFIG_ALRT = 0x0020;
|
||||
|
||||
// CONFIG.SLEEP is ignored unless MODE.EnSleep (bit 13) is set first.
|
||||
static constexpr uint16_t MODE_EN_SLEEP = 0x2000;
|
||||
static constexpr uint16_t STATUS_HD = 0x1000;
|
||||
|
||||
static constexpr uint32_t WAKE_ALERT_CLEAR_TIMEOUT_ID = 0;
|
||||
// The gauge raises its false wake alert some time after the wake write; on hardware it rose 0.8 to 1.0 s later
|
||||
static constexpr uint32_t WAKE_ALERT_CLEAR_DELAY_MS = 2000;
|
||||
|
||||
enum class Model : uint8_t {
|
||||
MODEL_MAX17043,
|
||||
MODEL_MAX17048,
|
||||
};
|
||||
|
||||
/// Defined in max170xx.cpp, which instantiates both models.
|
||||
template<Model MODEL> class MAX170xxComponent : public PollingComponent, public i2c::I2CDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
void sleep_mode();
|
||||
|
||||
protected:
|
||||
/// True once the gauge is configured; otherwise retries the configuration and warns if the gauge did not answer.
|
||||
/// Call first in update().
|
||||
bool ready_for_update_();
|
||||
/// Verify and wake the gauge; false if it did not answer.
|
||||
bool configure_();
|
||||
/// Clear the false alert raised on waking, leaving every other CONFIG and STATUS bit as it is.
|
||||
void clear_wake_alert_();
|
||||
|
||||
bool configured_{false};
|
||||
};
|
||||
|
||||
extern template class MAX170xxComponent<Model::MODEL_MAX17043>;
|
||||
extern template class MAX170xxComponent<Model::MODEL_MAX17048>;
|
||||
|
||||
} // namespace esphome::max170xx
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "mcp23016.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cstdio>
|
||||
|
||||
@@ -79,11 +80,7 @@ void MCP23016::update_reg_(uint8_t pin, bool pin_value, uint8_t reg_addr) {
|
||||
this->read_reg_(reg_addr, ®_value);
|
||||
}
|
||||
|
||||
if (pin_value) {
|
||||
reg_value |= 1 << pin;
|
||||
} else {
|
||||
reg_value &= ~(1 << pin);
|
||||
}
|
||||
assign_bits(reg_value, 1 << pin, pin_value);
|
||||
|
||||
this->write_reg_(reg_addr, reg_value);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "mcp23x08_base.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::mcp23x08_base {
|
||||
@@ -78,11 +79,7 @@ void MCP23X08Base::update_reg(uint8_t pin, bool pin_value, uint8_t reg_addr) {
|
||||
this->read_reg(reg_addr, ®_value);
|
||||
}
|
||||
|
||||
if (pin_value) {
|
||||
reg_value |= 1 << bit;
|
||||
} else {
|
||||
reg_value &= ~(1 << bit);
|
||||
}
|
||||
assign_bits(reg_value, 1 << bit, pin_value);
|
||||
|
||||
this->write_reg(reg_addr, reg_value);
|
||||
|
||||
|
||||
@@ -92,11 +92,7 @@ void MCP23X17Base::update_reg(uint8_t pin, bool pin_value, uint8_t reg_addr) {
|
||||
this->read_reg(reg_addr, ®_value);
|
||||
}
|
||||
|
||||
if (pin_value) {
|
||||
reg_value |= 1 << bit;
|
||||
} else {
|
||||
reg_value &= ~(1 << bit);
|
||||
}
|
||||
assign_bits(reg_value, 1 << bit, pin_value);
|
||||
|
||||
this->write_reg(reg_addr, reg_value);
|
||||
|
||||
|
||||
@@ -33,22 +33,22 @@ uint16_t parse_u8_hb_60(OpenthermData &data) { return data.valueHB * 60; }
|
||||
int16_t parse_s16(OpenthermData &data) { return data.get_s16(); }
|
||||
float parse_f88(OpenthermData &data) { return data.get_f88(); }
|
||||
|
||||
void write_flag8_lb_0(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 0, value); }
|
||||
void write_flag8_lb_1(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 1, value); }
|
||||
void write_flag8_lb_2(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 2, value); }
|
||||
void write_flag8_lb_3(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 3, value); }
|
||||
void write_flag8_lb_4(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 4, value); }
|
||||
void write_flag8_lb_5(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 5, value); }
|
||||
void write_flag8_lb_6(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 6, value); }
|
||||
void write_flag8_lb_7(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 7, value); }
|
||||
void write_flag8_hb_0(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 0, value); }
|
||||
void write_flag8_hb_1(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 1, value); }
|
||||
void write_flag8_hb_2(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 2, value); }
|
||||
void write_flag8_hb_3(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 3, value); }
|
||||
void write_flag8_hb_4(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 4, value); }
|
||||
void write_flag8_hb_5(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 5, value); }
|
||||
void write_flag8_hb_6(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 6, value); }
|
||||
void write_flag8_hb_7(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 7, value); }
|
||||
void write_flag8_lb_0(const bool value, OpenthermData &data) { assign_bits(data.valueLB, 1 << 0, value); }
|
||||
void write_flag8_lb_1(const bool value, OpenthermData &data) { assign_bits(data.valueLB, 1 << 1, value); }
|
||||
void write_flag8_lb_2(const bool value, OpenthermData &data) { assign_bits(data.valueLB, 1 << 2, value); }
|
||||
void write_flag8_lb_3(const bool value, OpenthermData &data) { assign_bits(data.valueLB, 1 << 3, value); }
|
||||
void write_flag8_lb_4(const bool value, OpenthermData &data) { assign_bits(data.valueLB, 1 << 4, value); }
|
||||
void write_flag8_lb_5(const bool value, OpenthermData &data) { assign_bits(data.valueLB, 1 << 5, value); }
|
||||
void write_flag8_lb_6(const bool value, OpenthermData &data) { assign_bits(data.valueLB, 1 << 6, value); }
|
||||
void write_flag8_lb_7(const bool value, OpenthermData &data) { assign_bits(data.valueLB, 1 << 7, value); }
|
||||
void write_flag8_hb_0(const bool value, OpenthermData &data) { assign_bits(data.valueHB, 1 << 0, value); }
|
||||
void write_flag8_hb_1(const bool value, OpenthermData &data) { assign_bits(data.valueHB, 1 << 1, value); }
|
||||
void write_flag8_hb_2(const bool value, OpenthermData &data) { assign_bits(data.valueHB, 1 << 2, value); }
|
||||
void write_flag8_hb_3(const bool value, OpenthermData &data) { assign_bits(data.valueHB, 1 << 3, value); }
|
||||
void write_flag8_hb_4(const bool value, OpenthermData &data) { assign_bits(data.valueHB, 1 << 4, value); }
|
||||
void write_flag8_hb_5(const bool value, OpenthermData &data) { assign_bits(data.valueHB, 1 << 5, value); }
|
||||
void write_flag8_hb_6(const bool value, OpenthermData &data) { assign_bits(data.valueHB, 1 << 6, value); }
|
||||
void write_flag8_hb_7(const bool value, OpenthermData &data) { assign_bits(data.valueHB, 1 << 7, value); }
|
||||
void write_u8_lb(const uint8_t value, OpenthermData &data) { data.valueLB = value; }
|
||||
void write_u8_hb(const uint8_t value, OpenthermData &data) { data.valueHB = value; }
|
||||
void write_s8_lb(const int8_t value, OpenthermData &data) { data.valueLB = (uint8_t) value; }
|
||||
|
||||
@@ -20,14 +20,6 @@ namespace esphome::opentherm {
|
||||
|
||||
template<class T> constexpr T read_bit(T value, uint8_t bit) { return (value >> bit) & 0x01; }
|
||||
|
||||
template<class T> constexpr T set_bit(T value, uint8_t bit) { return value |= (1UL << bit); }
|
||||
|
||||
template<class T> constexpr T clear_bit(T value, uint8_t bit) { return value &= ~(1UL << bit); }
|
||||
|
||||
template<class T> constexpr T write_bit(T value, uint8_t bit, uint8_t bit_value) {
|
||||
return bit_value ? set_bit(value, bit) : clear_bit(value, bit);
|
||||
}
|
||||
|
||||
enum OperationMode {
|
||||
IDLE = 0, // no operation
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "pca6416a.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::pca6416a {
|
||||
@@ -170,11 +171,7 @@ void PCA6416AComponent::update_register_(uint8_t pin, bool pin_value, uint8_t re
|
||||
this->read_register_(reg_addr, ®_value);
|
||||
}
|
||||
|
||||
if (pin_value) {
|
||||
reg_value |= 1 << bit;
|
||||
} else {
|
||||
reg_value &= ~(1 << bit);
|
||||
}
|
||||
assign_bits(reg_value, 1 << bit, pin_value);
|
||||
|
||||
this->write_register_(reg_addr, reg_value);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "pca9505.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::pca9505 {
|
||||
@@ -74,11 +75,7 @@ bool PCA9505Component::digital_read_cache(uint8_t pin) { return this->input_[pin
|
||||
void PCA9505Component::digital_write_hw(uint8_t pin, bool value) {
|
||||
const uint8_t bank = pin / 8;
|
||||
const uint8_t mask = 1 << (pin % 8);
|
||||
if (value) {
|
||||
this->output_[bank] |= mask;
|
||||
} else {
|
||||
this->output_[bank] &= ~mask;
|
||||
}
|
||||
assign_bits(this->output_[bank], mask, value);
|
||||
this->write_bank_(OUTPUT_REG, bank, this->output_[bank]);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "pca9554.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::pca9554 {
|
||||
@@ -99,11 +100,7 @@ bool PCA9554Component::digital_read_cache(uint8_t pin) {
|
||||
}
|
||||
|
||||
void PCA9554Component::digital_write_hw(uint8_t pin, bool value) {
|
||||
if (value) {
|
||||
this->output_mask_ |= (1 << pin);
|
||||
} else {
|
||||
this->output_mask_ &= ~(1 << pin);
|
||||
}
|
||||
assign_bits(this->output_mask_, 1 << pin, value);
|
||||
#ifdef USE_PCA9554_PCAL
|
||||
if (this->write_register_(OUTPUT_REG, this->output_mask_))
|
||||
this->retry_pcal_shadows_(); // an output-only hub never reads, so it retries here
|
||||
|
||||
@@ -90,11 +90,7 @@ void HOT PCD8544::draw_absolute_pixel_internal(int x, int y, Color color) {
|
||||
|
||||
uint16_t pos = x + (y / 8) * this->get_width_internal();
|
||||
uint8_t subpos = y % 8;
|
||||
if (color.is_on()) {
|
||||
this->buffer_[pos] |= (1 << subpos);
|
||||
} else {
|
||||
this->buffer_[pos] &= ~(1 << subpos);
|
||||
}
|
||||
assign_bits(this->buffer_[pos], 1 << subpos, color.is_on());
|
||||
}
|
||||
|
||||
void PCD8544::dump_config() {
|
||||
|
||||
@@ -60,11 +60,7 @@ bool PCF8574Component::digital_read_hw(uint8_t pin) {
|
||||
bool PCF8574Component::digital_read_cache(uint8_t pin) { return this->input_mask_ & (1 << pin); }
|
||||
|
||||
void PCF8574Component::digital_write_hw(uint8_t pin, bool value) {
|
||||
if (value) {
|
||||
this->output_mask_ |= (1 << pin);
|
||||
} else {
|
||||
this->output_mask_ &= ~(1 << pin);
|
||||
}
|
||||
assign_bits(this->output_mask_, 1 << pin, value);
|
||||
this->write_gpio_();
|
||||
}
|
||||
void PCF8574Component::pin_mode(uint8_t pin, gpio::Flags flags) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "pi4ioe5v6408.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::pi4ioe5v6408 {
|
||||
@@ -129,11 +130,7 @@ void PI4IOE5V6408Component::digital_write_hw(uint8_t pin, bool value) {
|
||||
if (this->is_failed())
|
||||
return;
|
||||
|
||||
if (value) {
|
||||
this->output_mask_ |= (1 << pin);
|
||||
} else {
|
||||
this->output_mask_ &= ~(1 << pin);
|
||||
}
|
||||
assign_bits(this->output_mask_, 1 << pin, value);
|
||||
if (!this->status_comm_result(this->write_byte(PI4IOE5V6408_REGISTER_OUT_SET, this->output_mask_),
|
||||
LOG_STR("Failed to write output register")))
|
||||
return;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "runtime_image.h"
|
||||
#include "image_decoder.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
@@ -91,11 +92,7 @@ void RuntimeImage::draw_pixel(int x, int y, const Color &color) {
|
||||
auto on = is_color_on(color);
|
||||
if (this->has_transparency() && color.w < 0x80)
|
||||
on = false;
|
||||
if (on) {
|
||||
this->buffer_[pos] |= bitno;
|
||||
} else {
|
||||
this->buffer_[pos] &= ~bitno;
|
||||
}
|
||||
assign_bits(this->buffer_[pos], bitno, on);
|
||||
break;
|
||||
}
|
||||
case image::IMAGE_TYPE_RGB111: {
|
||||
|
||||
@@ -342,11 +342,7 @@ void HOT SSD1306::draw_absolute_pixel_internal(int x, int y, Color color) {
|
||||
|
||||
uint16_t pos = x + (y / 8) * this->get_width_internal();
|
||||
uint8_t subpos = y & 0x07;
|
||||
if (color.is_on()) {
|
||||
this->buffer_[pos] |= (1 << subpos);
|
||||
} else {
|
||||
this->buffer_[pos] &= ~(1 << subpos);
|
||||
}
|
||||
assign_bits(this->buffer_[pos], 1 << subpos, color.is_on());
|
||||
}
|
||||
void SSD1306::fill(Color color) {
|
||||
// If clipping is active, fall back to base implementation
|
||||
|
||||
@@ -53,6 +53,13 @@ MODELS = DriverChip.get_models()
|
||||
DEFAULT_MODEL = "WAVESHARE-400X300"
|
||||
|
||||
|
||||
def _model_key(model: DriverChip, key: str) -> cv.Optional | cv.Required:
|
||||
"""Default the key from the model, or require it when the model has none."""
|
||||
if model.get_default(key, None) is None:
|
||||
return cv.Required(key)
|
||||
return model.option(key)
|
||||
|
||||
|
||||
def _model_schema(model: DriverChip) -> cv.Schema:
|
||||
iseqconf = (
|
||||
cv.Required(CONF_INIT_SEQUENCE)
|
||||
@@ -72,12 +79,12 @@ def _model_schema(model: DriverChip) -> cv.Schema:
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(ST7305),
|
||||
cv.Required(CONF_MODEL): cv.one_of(*MODELS, upper=True),
|
||||
model.option(CONF_CS_PIN, cv.UNDEFINED): pins.gpio_output_pin_schema,
|
||||
model.option(CONF_DC_PIN, cv.UNDEFINED): pins.gpio_output_pin_schema,
|
||||
_model_key(model, CONF_CS_PIN): pins.gpio_output_pin_schema,
|
||||
_model_key(model, CONF_DC_PIN): pins.gpio_output_pin_schema,
|
||||
model.option(CONF_RESET_PIN, cv.UNDEFINED): pins.gpio_output_pin_schema,
|
||||
cv.Optional(CONF_WIDTH): cv.int_range(min=8, max=1024),
|
||||
cv.Optional(CONF_HEIGHT): cv.int_range(min=8, max=1024),
|
||||
cv.Optional(CONF_PORTRAIT): cv.boolean,
|
||||
_model_key(model, CONF_WIDTH): cv.int_range(min=8, max=1024),
|
||||
_model_key(model, CONF_HEIGHT): cv.int_range(min=8, max=1024),
|
||||
_model_key(model, CONF_PORTRAIT): cv.boolean,
|
||||
cv.Optional(CONF_COLUMN_START): cv.int_range(min=0, max=255),
|
||||
cv.Optional(CONF_COLUMN_END): cv.int_range(min=0, max=255),
|
||||
cv.Optional(CONF_ROW_START): cv.int_range(min=0, max=255),
|
||||
@@ -89,23 +96,9 @@ def _model_schema(model: DriverChip) -> cv.Schema:
|
||||
|
||||
|
||||
def _resolve(config: ConfigType, model: DriverChip) -> None:
|
||||
"""Fill in model defaults and validate the geometry and address window."""
|
||||
if (width := config.get(CONF_WIDTH)) is None:
|
||||
if (width := model.get_default(CONF_WIDTH, None)) is None:
|
||||
raise cv.Invalid("'width' is required for this model", path=[CONF_WIDTH])
|
||||
config[CONF_WIDTH] = width
|
||||
if (height := config.get(CONF_HEIGHT)) is None:
|
||||
if (height := model.get_default(CONF_HEIGHT, None)) is None:
|
||||
raise cv.Invalid("'height' is required for this model", path=[CONF_HEIGHT])
|
||||
config[CONF_HEIGHT] = height
|
||||
|
||||
if config.get(CONF_PORTRAIT) is None:
|
||||
if (default := model.get_default(CONF_PORTRAIT, None)) is None:
|
||||
raise cv.Invalid(
|
||||
"'portrait' is required for this model", path=[CONF_PORTRAIT]
|
||||
)
|
||||
config[CONF_PORTRAIT] = default
|
||||
|
||||
"""Derive and validate the address window, and default the init sequence."""
|
||||
width = config[CONF_WIDTH]
|
||||
height = config[CONF_HEIGHT]
|
||||
if config[CONF_PORTRAIT]:
|
||||
if width % 12 or height % 2:
|
||||
raise cv.Invalid(
|
||||
@@ -152,10 +145,6 @@ def _resolve(config: ConfigType, model: DriverChip) -> None:
|
||||
if config.get(CONF_INIT_SEQUENCE) is None:
|
||||
config[CONF_INIT_SEQUENCE] = model.initsequence
|
||||
|
||||
for key, label in ((CONF_CS_PIN, "cs_pin"), (CONF_DC_PIN, "dc_pin")):
|
||||
if key not in config:
|
||||
raise cv.Invalid(f"'{label}' is required for this model", path=[key])
|
||||
|
||||
|
||||
def model_schema(config: ConfigType) -> cv.Schema:
|
||||
"""Return the schema for the model selected in the config."""
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#include "st7305.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::st7305 {
|
||||
@@ -27,7 +27,6 @@ bool ST7305::init_buffer_() {
|
||||
this->mark_failed(LOG_STR("Buffer allocation failed"));
|
||||
return false;
|
||||
}
|
||||
memset(this->buffer_, 0x00, this->buffer_length_);
|
||||
this->clear_dirty_();
|
||||
return true;
|
||||
}
|
||||
@@ -191,11 +190,7 @@ void HOT ST7305::draw_absolute_pixel_internal(int x, int y, Color color) {
|
||||
// The init sequence enables INVON, so a set bit is white and a clear bit is black.
|
||||
uint8_t &value = this->buffer_[index];
|
||||
const uint8_t previous = value;
|
||||
if (color.is_on()) {
|
||||
value |= mask;
|
||||
} else {
|
||||
value &= ~mask;
|
||||
}
|
||||
assign_bits(value, mask, color.is_on());
|
||||
if (value == previous)
|
||||
return;
|
||||
this->extend_dirty_(x, y, x, y);
|
||||
|
||||
@@ -123,11 +123,7 @@ void HOT ST7567::draw_absolute_pixel_internal(int x, int y, Color color) {
|
||||
|
||||
uint16_t pos = x + (y / 8) * this->get_width_internal();
|
||||
uint8_t subpos = y & 0x07;
|
||||
if (color.is_on()) {
|
||||
this->buffer_[pos] |= (1 << subpos);
|
||||
} else {
|
||||
this->buffer_[pos] &= ~(1 << subpos);
|
||||
}
|
||||
assign_bits(this->buffer_[pos], 1 << subpos, color.is_on());
|
||||
}
|
||||
|
||||
void ST7567::fill(Color color) {
|
||||
|
||||
@@ -92,20 +92,9 @@ void SX1509Component::digital_write_hw(uint8_t pin, bool bit_value) {
|
||||
// If the pin is an output, write high/low
|
||||
uint16_t temp_reg_data = 0;
|
||||
this->read_byte_16(REG_DATA_B, &temp_reg_data);
|
||||
if (bit_value) {
|
||||
output_state_ |= (1 << pin); // set bit in shadow register
|
||||
} else {
|
||||
output_state_ &= ~(1 << pin); // reset bit shadow register
|
||||
}
|
||||
for (uint16_t b = 0x8000; b; b >>= 1) {
|
||||
if ((~ddr_mask_) & b) { // transfer bits of outputs, but don't mess with inputs
|
||||
if (output_state_ & b) {
|
||||
temp_reg_data |= b;
|
||||
} else {
|
||||
temp_reg_data &= ~b;
|
||||
}
|
||||
}
|
||||
}
|
||||
assign_bits(this->output_state_, 1 << pin, bit_value);
|
||||
// Outputs take the shadow state, inputs keep what was read
|
||||
temp_reg_data = (temp_reg_data & this->ddr_mask_) | (this->output_state_ & ~this->ddr_mask_);
|
||||
this->write_byte_16(REG_DATA_B, temp_reg_data);
|
||||
}
|
||||
}
|
||||
@@ -148,20 +137,12 @@ void SX1509Component::pin_mode(uint8_t pin, gpio::Flags flags) {
|
||||
|
||||
// Pullup
|
||||
this->read_byte_16(REG_PULL_UP_B, &temp_word);
|
||||
if (flags & gpio::FLAG_PULLUP) {
|
||||
temp_word |= (1 << pin);
|
||||
} else {
|
||||
temp_word &= ~(1 << pin);
|
||||
}
|
||||
assign_bits(temp_word, 1 << pin, flags & gpio::FLAG_PULLUP);
|
||||
this->write_byte_16(REG_PULL_UP_B, temp_word);
|
||||
|
||||
// Pulldown
|
||||
this->read_byte_16(REG_PULL_DOWN_B, &temp_word);
|
||||
if (flags & gpio::FLAG_PULLDOWN) {
|
||||
temp_word |= (1 << pin);
|
||||
} else {
|
||||
temp_word &= ~(1 << pin);
|
||||
}
|
||||
assign_bits(temp_word, 1 << pin, flags & gpio::FLAG_PULLDOWN);
|
||||
this->write_byte_16(REG_PULL_DOWN_B, temp_word);
|
||||
|
||||
// Set direction to input
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "tca9555.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
static const uint8_t TCA9555_INPUT_PORT_REGISTER_0 = 0x00;
|
||||
@@ -108,11 +109,7 @@ void TCA9555Component::digital_write_hw(uint8_t pin, bool value) {
|
||||
if (this->is_failed())
|
||||
return;
|
||||
|
||||
if (value) {
|
||||
this->output_mask_ |= (1 << pin);
|
||||
} else {
|
||||
this->output_mask_ &= ~(1 << pin);
|
||||
}
|
||||
assign_bits(this->output_mask_, 1 << pin, value);
|
||||
|
||||
this->status_comm_result(this->write_byte_16_le(TCA9555_OUTPUT_PORT_REGISTER_0, this->output_mask_),
|
||||
LOG_STR("Failed to write output register"));
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "waveshare_io_ch32v003.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::waveshare_io_ch32v003 {
|
||||
@@ -110,11 +111,7 @@ void WaveshareIOCH32V003Component::digital_write_hw(uint8_t pin, bool value) {
|
||||
if (this->is_failed())
|
||||
return;
|
||||
|
||||
if (value) {
|
||||
this->output_mask_ |= (1 << pin);
|
||||
} else {
|
||||
this->output_mask_ &= ~(1 << pin);
|
||||
}
|
||||
assign_bits(this->output_mask_, 1 << pin, value);
|
||||
|
||||
uint8_t data = this->output_mask_;
|
||||
this->status_comm_result(this->write_byte(IO_EXTENSION_IO_OUTPUT_ADDR, data),
|
||||
|
||||
@@ -210,11 +210,7 @@ bool WeikaiComponent::read_pin_val_(uint8_t pin) {
|
||||
}
|
||||
|
||||
void WeikaiComponent::write_pin_val_(uint8_t pin, bool value) {
|
||||
if (value) {
|
||||
this->output_state_ |= (1 << pin);
|
||||
} else {
|
||||
this->output_state_ &= ~(1 << pin);
|
||||
}
|
||||
assign_bits(this->output_state_, 1 << pin, value);
|
||||
char bin_buf[9];
|
||||
ESP_LOGVV(TAG, "writing output pin %d with %d out_state %s", pin, uint8_t(value),
|
||||
format_bin_to(bin_buf, this->output_state_));
|
||||
|
||||
@@ -75,6 +75,7 @@
|
||||
#define DFROBOT_SEN0465_COMPENSATION_RANGES 3
|
||||
#define USE_DISPLAY
|
||||
#define DISPLAY_PAGE_CHANGE_CALLBACK_COUNT 1
|
||||
#define DUCO_DEVICE_COUNT 4
|
||||
#define USE_EMONTX_VERSION
|
||||
#define USE_EMONTX_VERSION_TEXT_SENSOR
|
||||
#define USE_ENTITY_DEVICE_CLASS
|
||||
|
||||
@@ -9,6 +9,7 @@ from esphome.components.st7305.display import (
|
||||
CONF_PORTRAIT,
|
||||
CONF_ROW_END,
|
||||
MODELS,
|
||||
_model_schema,
|
||||
_resolve,
|
||||
)
|
||||
from esphome.const import CONF_CS_PIN, CONF_DC_PIN, CONF_HEIGHT, CONF_WIDTH
|
||||
@@ -72,3 +73,22 @@ def test_portrait_rejects_inner_not_multiple_of_12(width: int) -> None:
|
||||
def test_window_ends_must_fit_in_a_byte() -> None:
|
||||
with pytest.raises(cv.Invalid, match="between 0 and 255"):
|
||||
_resolve(_config(**{CONF_WIDTH: 1024, CONF_HEIGHT: 300}), MODELS["CUSTOM"])
|
||||
|
||||
|
||||
MODEL_KEYS = (CONF_WIDTH, CONF_HEIGHT, CONF_PORTRAIT, CONF_CS_PIN, CONF_DC_PIN)
|
||||
|
||||
|
||||
def _schema_key(model: str, key: str) -> cv.Optional | cv.Required:
|
||||
return next(k for k in _model_schema(MODELS[model]).schema if k == key)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("key", MODEL_KEYS)
|
||||
def test_custom_requires_model_keys(key: str) -> None:
|
||||
assert isinstance(_schema_key("CUSTOM", key), cv.Required)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("key", MODEL_KEYS)
|
||||
def test_model_defaults_model_keys(key: str) -> None:
|
||||
schema_key = _schema_key("WAVESHARE-400X300", key)
|
||||
assert not isinstance(schema_key, cv.Required)
|
||||
assert schema_key.default() == MODELS["WAVESHARE-400X300"].get_default(key)
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
time:
|
||||
- platform: sntp
|
||||
id: duco_sntp_time
|
||||
|
||||
duco:
|
||||
id: duco_box
|
||||
discovery: {}
|
||||
time:
|
||||
time_id: duco_sntp_time
|
||||
|
||||
text_sensor:
|
||||
- platform: duco
|
||||
name: Serial Number
|
||||
|
||||
select:
|
||||
- platform: duco
|
||||
name: Mode
|
||||
|
||||
sensor:
|
||||
- platform: duco
|
||||
co2:
|
||||
- address: 3
|
||||
name: CO2 Bedroom
|
||||
- address: 4
|
||||
name: CO2 Livingroom
|
||||
temperature:
|
||||
- address: 3
|
||||
name: Temperature Bedroom
|
||||
humidity:
|
||||
- address: 2
|
||||
name: Humidity Bathroom
|
||||
filter_remaining:
|
||||
name: Filter Time Remaining
|
||||
flow_level:
|
||||
name: Flow Level
|
||||
update_interval: 5s
|
||||
time_remaining:
|
||||
name: Mode Time Remaining
|
||||
update_interval: 5s
|
||||
bypass:
|
||||
name: Bypass
|
||||
temperature_oda:
|
||||
name: Temperature Outdoor Air
|
||||
temperature_sup:
|
||||
name: Temperature Supply To Room
|
||||
temperature_eta:
|
||||
name: Temperature Indoor Exhaust
|
||||
temperature_eha:
|
||||
name: Temperature Outdoor Exhaust
|
||||
|
||||
number:
|
||||
- platform: duco
|
||||
name: Comfort Temperature
|
||||
update_interval: 10s
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
uart_57600: !include ../../test_build_components/common/uart_57600/esp32-idf.yaml
|
||||
duco: !include common.yaml
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
uart_57600: !include ../../test_build_components/common/uart_57600/esp8266-ard.yaml
|
||||
duco: !include common.yaml
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
uart_57600: !include ../../test_build_components/common/uart_57600/rp2040-ard.yaml
|
||||
duco: !include common.yaml
|
||||
@@ -0,0 +1,177 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <initializer_list>
|
||||
#include <vector>
|
||||
|
||||
#include "../common.h"
|
||||
#include "esphome/components/epaper_spi/epaper_spi_ssd1683.h"
|
||||
|
||||
namespace esphome::epaper_spi::testing {
|
||||
|
||||
/// Drives a driver through whole updates so the skip in the UPDATE state can be checked.
|
||||
template<typename Driver> class TestableDriver : public Driver {
|
||||
public:
|
||||
TestableDriver(uint16_t width, uint16_t height) : Driver("test", width, height, nullptr, 0) {}
|
||||
|
||||
void install(spi::SPIDelegate *delegate, uint8_t full_update_every) {
|
||||
this->delegate_ = delegate;
|
||||
this->set_dc_pin(&this->dc);
|
||||
this->set_full_update_every(full_update_every);
|
||||
ASSERT_TRUE(this->init_buffer_(this->buffer_length_));
|
||||
}
|
||||
|
||||
void set_frame(std::initializer_list<uint8_t> bytes) {
|
||||
size_t i = 0;
|
||||
for (const uint8_t byte : bytes)
|
||||
this->buffer_[i++] = byte;
|
||||
this->x_low_ = 0;
|
||||
this->y_low_ = 0;
|
||||
this->x_high_ = this->width_;
|
||||
this->y_high_ = this->height_;
|
||||
}
|
||||
|
||||
/// A whole update, with auto clear and the writer as a YAML lambda runs if asked; false if skipped.
|
||||
bool run_update(bool auto_clear = false) {
|
||||
this->set_auto_clear(auto_clear);
|
||||
this->state_ = EPaperState::UPDATE;
|
||||
this->process_state_();
|
||||
const bool pushed = this->state_ == EPaperState::RESET;
|
||||
while (this->state_ != EPaperState::IDLE)
|
||||
this->process_state_();
|
||||
return pushed;
|
||||
}
|
||||
|
||||
/// A whole update with a pixel drawn after the frame was hashed, while it is being sent.
|
||||
void run_update_drawn_during_push(int x, int y) {
|
||||
this->state_ = EPaperState::UPDATE;
|
||||
while (this->state_ != EPaperState::TRANSFER_DATA)
|
||||
this->process_state_();
|
||||
this->draw_pixel_at(x, y, Color::BLACK);
|
||||
while (this->state_ != EPaperState::IDLE)
|
||||
this->process_state_();
|
||||
}
|
||||
|
||||
/// An update in which nothing is drawn; false if it went straight back to idle.
|
||||
bool run_update_drawing_nothing() {
|
||||
this->state_ = EPaperState::UPDATE;
|
||||
this->process_state_();
|
||||
const bool pushed = this->state_ == EPaperState::RESET;
|
||||
while (this->state_ != EPaperState::IDLE)
|
||||
this->process_state_();
|
||||
return pushed;
|
||||
}
|
||||
|
||||
/// An update given up on with the busy pin stuck, after its data was sent.
|
||||
void run_update_abandoned() {
|
||||
this->state_ = EPaperState::UPDATE;
|
||||
while (this->state_ != EPaperState::POWER_OFF)
|
||||
this->process_state_();
|
||||
this->abandon_update_();
|
||||
}
|
||||
|
||||
uint8_t update_count() const { return this->update_count_; }
|
||||
bool shown_hash_valid() const { return this->shown_hash_valid_; }
|
||||
|
||||
RecordingPin dc;
|
||||
};
|
||||
|
||||
using Bytes = std::vector<uint8_t>;
|
||||
|
||||
TEST(EPaperSkipUnchanged, SSD1683SkipsAnUnchangedFrame) {
|
||||
TestableDriver<EPaperSSD1683> display(16, 2);
|
||||
RecordingDelegate bus(&display.dc);
|
||||
display.install(&bus, 5);
|
||||
|
||||
display.set_frame({0x0F, 0xF0, 0x3C, 0xC3});
|
||||
EXPECT_TRUE(display.run_update());
|
||||
bus.clear();
|
||||
|
||||
display.set_frame({0x0F, 0xF0, 0x3C, 0xC3});
|
||||
EXPECT_FALSE(display.run_update());
|
||||
EXPECT_TRUE(bus.commands.empty());
|
||||
EXPECT_EQ(display.update_count(), 1);
|
||||
|
||||
display.set_frame({0x0F, 0xF0, 0x3C, 0x00});
|
||||
EXPECT_TRUE(display.run_update());
|
||||
EXPECT_EQ(bus.data[0x24], (Bytes{0x0F, 0xF0, 0x3C, 0x00}));
|
||||
}
|
||||
|
||||
/// A frame drawn into while it was being sent is not trusted, so the frame it replaced is pushed again.
|
||||
TEST(EPaperSkipUnchanged, FrameChangedDuringThePushIsNotTrusted) {
|
||||
TestableDriver<EPaperSSD1683> display(16, 2);
|
||||
RecordingDelegate bus(&display.dc);
|
||||
display.install(&bus, 5);
|
||||
|
||||
display.set_frame({0x0F, 0xF0, 0x3C, 0xC3});
|
||||
EXPECT_TRUE(display.run_update());
|
||||
display.set_frame({0x0F, 0xF0, 0x3C, 0x00});
|
||||
display.run_update_drawn_during_push(15, 1);
|
||||
EXPECT_FALSE(display.shown_hash_valid());
|
||||
|
||||
display.set_frame({0x0F, 0xF0, 0x3C, 0x00});
|
||||
EXPECT_TRUE(display.run_update()) << "frame that never reached the panel was skipped";
|
||||
}
|
||||
|
||||
/// A draw during the push is sent by the next update even when that update draws nothing itself.
|
||||
TEST(EPaperSkipUnchanged, DrawDuringThePushIsSentByAnUpdateThatDrawsNothing) {
|
||||
TestableDriver<EPaperSSD1683> display(16, 2);
|
||||
RecordingDelegate bus(&display.dc);
|
||||
display.install(&bus, 5);
|
||||
|
||||
display.set_frame({0x0F, 0xF0, 0x3C, 0xC3});
|
||||
EXPECT_TRUE(display.run_update());
|
||||
display.set_frame({0x0F, 0xF0, 0x3C, 0x00});
|
||||
display.run_update_drawn_during_push(15, 1);
|
||||
bus.clear();
|
||||
|
||||
EXPECT_TRUE(display.run_update_drawing_nothing()) << "draw made during the push was dropped";
|
||||
EXPECT_EQ(bus.data[0x24].size(), 4u) << "whole frame not sent";
|
||||
}
|
||||
|
||||
/// An abandoned update leaves no hash to match, since its frame may never have been shown.
|
||||
TEST(EPaperSkipUnchanged, AbandonedUpdateLeavesNoHash) {
|
||||
TestableDriver<EPaperSSD1683> display(16, 2);
|
||||
RecordingDelegate bus(&display.dc);
|
||||
display.install(&bus, 5);
|
||||
|
||||
display.set_frame({0x0F, 0xF0, 0x3C, 0xC3});
|
||||
EXPECT_TRUE(display.run_update());
|
||||
EXPECT_TRUE(display.shown_hash_valid());
|
||||
|
||||
display.set_frame({0x0F, 0xF0, 0x3C, 0x00});
|
||||
display.run_update_abandoned();
|
||||
EXPECT_FALSE(display.shown_hash_valid());
|
||||
|
||||
display.set_frame({0x0F, 0xF0, 0x3C, 0x00});
|
||||
EXPECT_TRUE(display.run_update());
|
||||
}
|
||||
|
||||
/// With every update a full one, nothing is ever skipped: a full refresh always redraws.
|
||||
TEST(EPaperSkipUnchanged, NothingSkippedWithoutPartialUpdates) {
|
||||
TestableDriver<EPaperSSD1683> display(16, 2);
|
||||
RecordingDelegate bus(&display.dc);
|
||||
display.install(&bus, 1);
|
||||
|
||||
display.set_frame({0x0F, 0xF0, 0x3C, 0xC3});
|
||||
EXPECT_TRUE(display.run_update());
|
||||
display.set_frame({0x0F, 0xF0, 0x3C, 0xC3});
|
||||
EXPECT_TRUE(display.run_update());
|
||||
}
|
||||
|
||||
/// The real update path: auto clear then a writer lambda, as a YAML lambda runs. The clear makes
|
||||
/// the drawn area the whole panel every time, so only the frame contents can tell updates apart.
|
||||
TEST(EPaperSkipUnchanged, UpdatesThroughTheWriterAreSkippedWhenTheyDrawTheSameFrame) {
|
||||
TestableDriver<EPaperSSD1683> display(16, 2);
|
||||
RecordingDelegate bus(&display.dc);
|
||||
display.install(&bus, 5);
|
||||
int value = 1;
|
||||
display.set_writer([&](Display &it) { it.draw_pixel_at(value, 0, Color::BLACK); });
|
||||
|
||||
EXPECT_TRUE(display.run_update(true));
|
||||
EXPECT_FALSE(display.run_update(true)) << "same frame was pushed";
|
||||
value = 2;
|
||||
EXPECT_TRUE(display.run_update(true)) << "changed frame was skipped";
|
||||
EXPECT_FALSE(display.run_update(true));
|
||||
}
|
||||
|
||||
} // namespace esphome::epaper_spi::testing
|
||||
@@ -19,8 +19,14 @@ class TestableSSD1681 : public EPaperSSD1681 {
|
||||
this->set_reset_pin(&this->reset_pin);
|
||||
ASSERT_TRUE(this->init_buffer_(this->buffer_length_));
|
||||
this->set_full_update_every(full_update_every);
|
||||
this->init_sent_frame_(this->buffer_length_);
|
||||
}
|
||||
|
||||
bool has_comparison_frame() const { return this->sent_.is_valid(); }
|
||||
EPaperState state() const { return this->state_; }
|
||||
void step() { this->process_state_(); }
|
||||
uint8_t update_count() const { return this->update_count_; }
|
||||
|
||||
void set_frame(std::initializer_list<uint8_t> bytes) {
|
||||
size_t i = 0;
|
||||
for (const uint8_t byte : bytes)
|
||||
@@ -34,13 +40,20 @@ class TestableSSD1681 : public EPaperSSD1681 {
|
||||
this->y_high_ = y_high;
|
||||
}
|
||||
|
||||
/// Run an update from the point where the frame has been drawn through to idle.
|
||||
void run_update() {
|
||||
// What the UPDATE state decides before it hands over to RESET
|
||||
this->partial_ = this->update_count_ != 0;
|
||||
this->state_ = EPaperState::RESET;
|
||||
/// Run the UPDATE state with the frame already drawn, and report whether a push follows.
|
||||
bool run_update_state() {
|
||||
this->set_auto_clear(false);
|
||||
this->state_ = EPaperState::UPDATE;
|
||||
this->process_state_();
|
||||
return this->state_ == EPaperState::RESET;
|
||||
}
|
||||
|
||||
/// Run an update from the point where the frame has been drawn through to idle; false if skipped.
|
||||
bool run_update() {
|
||||
const bool pushed = this->run_update_state();
|
||||
while (this->state_ != EPaperState::IDLE)
|
||||
this->process_state_();
|
||||
return pushed;
|
||||
}
|
||||
|
||||
RecordingPin dc;
|
||||
@@ -134,4 +147,110 @@ TEST(EPaperSSD1681, EveryUpdateFullSleepsWithoutKeepingRAM) {
|
||||
EXPECT_EQ(bus.data[0x10], (Bytes{0x03}));
|
||||
}
|
||||
|
||||
/// An update whose frame is the one already on the panel sends nothing and does not count towards
|
||||
/// the next full refresh. The first update after boot always pushes: nothing is known to be on
|
||||
/// the panel yet.
|
||||
TEST(EPaperSSD1681, UnchangedFrameSkipsTheRefresh) {
|
||||
TestableSSD1681 display(16, 2);
|
||||
RecordingDelegate bus(&display.dc);
|
||||
display.install(&bus, 5);
|
||||
ASSERT_TRUE(display.has_comparison_frame());
|
||||
|
||||
display.set_frame({0x0F, 0xF0, 0x3C, 0xC3});
|
||||
display.set_dirty(0, 0, 16, 2);
|
||||
EXPECT_TRUE(display.run_update()) << "first update was skipped";
|
||||
EXPECT_EQ(display.update_count(), 1);
|
||||
bus.clear();
|
||||
|
||||
display.set_dirty(0, 0, 16, 2); // redrawn, but with the same content
|
||||
EXPECT_FALSE(display.run_update()) << "unchanged frame was pushed";
|
||||
EXPECT_TRUE(bus.commands.empty());
|
||||
EXPECT_EQ(display.update_count(), 1) << "skipped update counted towards the next full refresh";
|
||||
|
||||
display.set_frame({0x0F, 0xF0, 0x3C, 0x00});
|
||||
display.set_dirty(0, 0, 16, 2);
|
||||
EXPECT_TRUE(display.run_update()) << "changed frame was skipped";
|
||||
EXPECT_EQ(display.update_count(), 2);
|
||||
}
|
||||
|
||||
/// The first update sends the whole frame whatever was drawn, so the skip works without auto clear.
|
||||
TEST(EPaperSSD1681, SkipWorksWhenOnlyPartOfThePanelIsDrawn) {
|
||||
TestableSSD1681 display(16, 2);
|
||||
RecordingDelegate bus(&display.dc);
|
||||
display.install(&bus, 5);
|
||||
|
||||
display.set_frame({0x0F, 0xF0, 0x3C, 0xC3});
|
||||
display.set_dirty(8, 1, 16, 2);
|
||||
EXPECT_TRUE(display.run_update());
|
||||
bus.clear();
|
||||
|
||||
display.set_dirty(8, 1, 16, 2);
|
||||
EXPECT_FALSE(display.run_update()) << "unchanged frame was pushed";
|
||||
EXPECT_TRUE(bus.commands.empty());
|
||||
}
|
||||
|
||||
/// A requested full update pushes even if the frame is unchanged.
|
||||
TEST(EPaperSSD1681, RequestedFullUpdateIsNotSkipped) {
|
||||
TestableSSD1681 display(16, 2);
|
||||
RecordingDelegate bus(&display.dc);
|
||||
display.install(&bus, 5);
|
||||
|
||||
display.set_frame({0x0F, 0xF0, 0x3C, 0xC3});
|
||||
display.set_dirty(0, 0, 16, 2);
|
||||
display.run_update();
|
||||
bus.clear();
|
||||
|
||||
display.request_full_update();
|
||||
EXPECT_TRUE(display.run_update());
|
||||
EXPECT_EQ(bus.data[0x22], (Bytes{0xF7}));
|
||||
EXPECT_EQ(display.update_count(), 1);
|
||||
}
|
||||
|
||||
/// A partial refresh sends only the bytes that differ from the frame on the panel, whatever area
|
||||
/// was drawn: with auto clear on, the drawn area is always the whole panel.
|
||||
TEST(EPaperSSD1681, PartialRefreshSendsOnlyTheBytesThatChanged) {
|
||||
TestableSSD1681 display(16, 2);
|
||||
RecordingDelegate bus(&display.dc);
|
||||
display.install(&bus, 5);
|
||||
|
||||
display.set_frame({0x0F, 0xF0, 0x3C, 0xC3});
|
||||
display.set_dirty(0, 0, 16, 2);
|
||||
display.run_update();
|
||||
bus.clear();
|
||||
|
||||
display.set_frame({0x0F, 0xF0, 0x3C, 0x00});
|
||||
display.set_dirty(0, 0, 16, 2); // whole panel redrawn, only the last byte differs
|
||||
display.run_update();
|
||||
|
||||
EXPECT_EQ(bus.data[0x24], (Bytes{0x00}));
|
||||
EXPECT_EQ(bus.data[0x44], (Bytes{1, 1}));
|
||||
EXPECT_EQ(bus.data[0x45], (Bytes{1, 0, 1, 0}));
|
||||
}
|
||||
|
||||
/// The comparison frame holds what was sent, so a change that was drawn but not pushed yet
|
||||
/// (because it was drawn during a push) is still sent by the next update.
|
||||
TEST(EPaperSSD1681, ComparisonFrameIsWhatWasSent) {
|
||||
TestableSSD1681 display(16, 2);
|
||||
RecordingDelegate bus(&display.dc);
|
||||
display.install(&bus, 5);
|
||||
|
||||
display.set_frame({0x0F, 0xF0, 0x3C, 0xC3});
|
||||
display.set_dirty(0, 0, 16, 2);
|
||||
display.run_update();
|
||||
|
||||
display.set_frame({0x0F, 0xF0, 0x3C, 0x00});
|
||||
display.set_dirty(8, 1, 16, 2);
|
||||
display.run_update_state();
|
||||
while (display.state() != EPaperState::POWER_ON)
|
||||
display.step();
|
||||
display.draw_pixel_at(4, 0, Color::BLACK); // drawn during the refresh
|
||||
while (display.state() != EPaperState::IDLE)
|
||||
display.step();
|
||||
bus.clear();
|
||||
|
||||
display.set_dirty(0, 0, 16, 2);
|
||||
EXPECT_TRUE(display.run_update()) << "pixel drawn during the refresh was never sent";
|
||||
EXPECT_EQ(bus.data[0x24], (Bytes{0x07}));
|
||||
}
|
||||
|
||||
} // namespace esphome::epaper_spi::testing
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
esphome:
|
||||
on_boot:
|
||||
then:
|
||||
- max17048.sleep_mode: max17048_id
|
||||
|
||||
sensor:
|
||||
- platform: max17048
|
||||
i2c_id: i2c_bus
|
||||
id: max17048_id
|
||||
battery_voltage:
|
||||
name: Battery Voltage
|
||||
battery_level:
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/components/max170xx/max170xx.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
namespace esphome::max170xx::testing {
|
||||
|
||||
static constexpr uint8_t ADDRESS = 0x36;
|
||||
|
||||
using Write = std::pair<uint8_t, uint16_t>;
|
||||
|
||||
// The gauge's 16-bit registers, big-endian on the wire.
|
||||
class RegisterBus final : public i2c::I2CBus {
|
||||
public:
|
||||
i2c::ErrorCode write_readv(uint8_t address, const uint8_t *data, size_t size, uint8_t *out,
|
||||
size_t out_size) override {
|
||||
EXPECT_EQ(address, ADDRESS);
|
||||
if (this->silent || (this->fail_writes && size > 1))
|
||||
return i2c::ERROR_NOT_ACKNOWLEDGED;
|
||||
if (size == 3) {
|
||||
const uint16_t value = (data[1] << 8) | data[2];
|
||||
this->registers[data[0]] = value;
|
||||
this->writes.emplace_back(data[0], value);
|
||||
}
|
||||
if (out_size == 2) {
|
||||
const uint16_t value = this->registers[data[0]];
|
||||
out[0] = value >> 8;
|
||||
out[1] = value;
|
||||
}
|
||||
return i2c::ERROR_OK;
|
||||
}
|
||||
|
||||
bool has_write(const Write &write) const {
|
||||
return std::find(this->writes.begin(), this->writes.end(), write) != this->writes.end();
|
||||
}
|
||||
|
||||
std::map<uint8_t, uint16_t> registers;
|
||||
std::vector<Write> writes;
|
||||
bool silent{false};
|
||||
bool fail_writes{false};
|
||||
};
|
||||
|
||||
class Gauge43 final : public MAX170xxComponent<Model::MODEL_MAX17043> {
|
||||
public:
|
||||
void update() override {}
|
||||
using MAX170xxComponent::clear_wake_alert_;
|
||||
using MAX170xxComponent::configure_;
|
||||
using MAX170xxComponent::configured_;
|
||||
using MAX170xxComponent::ready_for_update_;
|
||||
bool take_pending_clear() { return this->cancel_timeout(WAKE_ALERT_CLEAR_TIMEOUT_ID); }
|
||||
};
|
||||
|
||||
class Gauge48 final : public MAX170xxComponent<Model::MODEL_MAX17048> {
|
||||
public:
|
||||
void update() override {}
|
||||
using MAX170xxComponent::clear_wake_alert_;
|
||||
using MAX170xxComponent::configure_;
|
||||
using MAX170xxComponent::configured_;
|
||||
using MAX170xxComponent::ready_for_update_;
|
||||
bool take_pending_clear() { return this->cancel_timeout(WAKE_ALERT_CLEAR_TIMEOUT_ID); }
|
||||
};
|
||||
|
||||
template<typename G> struct Fixture {
|
||||
RegisterBus bus;
|
||||
G gauge;
|
||||
|
||||
Fixture() {
|
||||
static char name[] = "test_max170xx";
|
||||
static char friendly_name[] = "";
|
||||
App.pre_setup(name, sizeof(name) - 1, friendly_name, 0);
|
||||
this->gauge.set_i2c_bus(&this->bus);
|
||||
this->gauge.set_i2c_address(ADDRESS);
|
||||
}
|
||||
|
||||
~Fixture() {
|
||||
// Drop a pending clear while the gauge is alive; App outlives each test
|
||||
this->gauge.take_pending_clear();
|
||||
App.scheduler.call(millis());
|
||||
}
|
||||
};
|
||||
|
||||
TEST(MAX170xxTest, SilentBusAtSetupIsRetried) {
|
||||
Fixture<Gauge48> f;
|
||||
f.bus.silent = true;
|
||||
f.gauge.setup();
|
||||
EXPECT_FALSE(f.gauge.is_failed());
|
||||
EXPECT_FALSE(f.gauge.configured_);
|
||||
EXPECT_TRUE(f.gauge.status_has_warning());
|
||||
EXPECT_FALSE(f.gauge.ready_for_update_());
|
||||
EXPECT_TRUE(f.gauge.status_has_warning());
|
||||
|
||||
f.bus.silent = false;
|
||||
f.bus.registers[REG_CONFIG] = 0x971C;
|
||||
EXPECT_TRUE(f.gauge.ready_for_update_());
|
||||
EXPECT_TRUE(f.bus.has_write({0x0C, 0x971C}));
|
||||
}
|
||||
|
||||
TEST(MAX170xxTest, WrongConfigIsReset) {
|
||||
Fixture<Gauge48> f;
|
||||
f.bus.registers[REG_CONFIG] = 0x1234;
|
||||
f.gauge.setup();
|
||||
EXPECT_FALSE(f.gauge.is_failed());
|
||||
EXPECT_TRUE(f.gauge.configured_);
|
||||
EXPECT_TRUE(f.bus.has_write({0x0C, 0x971C}));
|
||||
}
|
||||
|
||||
TEST(MAX170xxTest, WriteBackFailureIsRetried) {
|
||||
Fixture<Gauge48> f;
|
||||
f.bus.registers[REG_CONFIG] = 0x971C;
|
||||
f.bus.fail_writes = true;
|
||||
f.gauge.setup();
|
||||
EXPECT_FALSE(f.gauge.is_failed());
|
||||
EXPECT_FALSE(f.gauge.configured_);
|
||||
|
||||
f.bus.fail_writes = false;
|
||||
EXPECT_TRUE(f.gauge.ready_for_update_());
|
||||
}
|
||||
|
||||
TEST(MAX170xxTest, WakeFromSleepSchedulesClear) {
|
||||
Fixture<Gauge48> f;
|
||||
f.bus.registers[REG_CONFIG] = 0x979C;
|
||||
f.gauge.setup();
|
||||
EXPECT_TRUE(f.gauge.configured_);
|
||||
EXPECT_TRUE(f.bus.has_write({0x0C, 0x971C}));
|
||||
EXPECT_TRUE(f.gauge.take_pending_clear());
|
||||
}
|
||||
|
||||
TEST(MAX170xxTest, NormalBootSchedulesNoClear) {
|
||||
Fixture<Gauge48> f;
|
||||
f.bus.registers[REG_CONFIG] = 0x971C;
|
||||
f.bus.registers[REG_STATUS] = 0x1100;
|
||||
f.gauge.setup();
|
||||
EXPECT_FALSE(f.gauge.take_pending_clear());
|
||||
// A real low-battery alert at an ordinary boot is left alone
|
||||
EXPECT_EQ(f.bus.registers[REG_STATUS], 0x1100);
|
||||
EXPECT_EQ(f.bus.writes, (std::vector<Write>{{0x0C, 0x971C}}));
|
||||
}
|
||||
|
||||
TEST(MAX170xxTest, ClearWakeAlertKeepsSleepAndOtherStatusBits) {
|
||||
Fixture<Gauge48> f;
|
||||
f.bus.registers[REG_CONFIG] = 0x97BC;
|
||||
f.bus.registers[REG_STATUS] = 0x5F00;
|
||||
f.gauge.clear_wake_alert_();
|
||||
EXPECT_EQ(f.bus.registers[REG_CONFIG], 0x979C);
|
||||
EXPECT_EQ(f.bus.registers[REG_STATUS], 0x4F00);
|
||||
}
|
||||
|
||||
TEST(MAX170xxTest, ClearWakeAlertOn43LeavesStatusAlone) {
|
||||
Fixture<Gauge43> f;
|
||||
f.bus.registers[REG_CONFIG] = 0x97BC;
|
||||
f.gauge.clear_wake_alert_();
|
||||
EXPECT_EQ(f.bus.registers[REG_CONFIG], 0x979C);
|
||||
for (const Write &write : f.bus.writes)
|
||||
EXPECT_NE(write.first, 0x1A);
|
||||
}
|
||||
|
||||
TEST(MAX170xxTest, SleepModeOn48WritesEnSleepThenSleep) {
|
||||
Fixture<Gauge48> f;
|
||||
f.bus.registers[REG_CONFIG] = 0x971C;
|
||||
f.gauge.setup();
|
||||
f.bus.writes.clear();
|
||||
f.gauge.sleep_mode();
|
||||
EXPECT_EQ(f.bus.writes, (std::vector<Write>{{0x06, 0x2000}, {0x0C, 0x979C}}));
|
||||
}
|
||||
|
||||
TEST(MAX170xxTest, SleepModeOn43WritesSleepOnly) {
|
||||
Fixture<Gauge43> f;
|
||||
f.bus.registers[REG_CONFIG] = 0x971C;
|
||||
f.gauge.setup();
|
||||
f.bus.writes.clear();
|
||||
f.gauge.sleep_mode();
|
||||
EXPECT_EQ(f.bus.writes, (std::vector<Write>{{0x0C, 0x979C}}));
|
||||
}
|
||||
|
||||
TEST(MAX170xxTest, SleepModeRunsPendingClearFirst) {
|
||||
Fixture<Gauge48> f;
|
||||
f.bus.registers[REG_CONFIG] = 0x979C;
|
||||
f.gauge.setup();
|
||||
// The gauge raises the alert as it wakes
|
||||
f.bus.registers[REG_CONFIG] = 0x973C;
|
||||
f.bus.registers[REG_STATUS] = 0x1100;
|
||||
f.gauge.sleep_mode();
|
||||
EXPECT_FALSE(f.gauge.take_pending_clear());
|
||||
EXPECT_EQ(f.bus.registers[REG_STATUS], 0x0100);
|
||||
ASSERT_FALSE(f.bus.writes.empty());
|
||||
EXPECT_EQ(f.bus.writes.back(), (Write{0x0C, 0x979C}));
|
||||
}
|
||||
|
||||
TEST(MAX170xxTest, SleepModeOnSilentBusWritesNothing) {
|
||||
Fixture<Gauge48> f;
|
||||
f.bus.silent = true;
|
||||
f.gauge.setup();
|
||||
f.gauge.sleep_mode();
|
||||
EXPECT_TRUE(f.bus.writes.empty());
|
||||
EXPECT_TRUE(f.gauge.status_has_warning());
|
||||
EXPECT_FALSE(f.gauge.is_failed());
|
||||
}
|
||||
|
||||
TEST(MAX170xxTest, SetupAfterSleepModeKeepsGaugeAsleep) {
|
||||
Fixture<Gauge48> f;
|
||||
f.bus.registers[REG_CONFIG] = 0x971C;
|
||||
// An on_boot with priority above 600 runs before setup()
|
||||
f.gauge.sleep_mode();
|
||||
f.bus.writes.clear();
|
||||
f.gauge.setup();
|
||||
EXPECT_TRUE(f.bus.writes.empty());
|
||||
EXPECT_EQ(f.bus.registers[REG_CONFIG], 0x979C);
|
||||
}
|
||||
|
||||
TEST(MAX170xxTest, SleepModeWhenFailedWritesNothing) {
|
||||
Fixture<Gauge48> f;
|
||||
f.bus.registers[REG_CONFIG] = 0x971C;
|
||||
f.gauge.mark_failed();
|
||||
f.gauge.sleep_mode();
|
||||
EXPECT_TRUE(f.bus.writes.empty());
|
||||
}
|
||||
|
||||
} // namespace esphome::max170xx::testing
|
||||
@@ -0,0 +1,202 @@
|
||||
esphome:
|
||||
name: uart-mock-duco
|
||||
# More requests than the old fixed table held, all sent before the hub reads an answer
|
||||
on_boot:
|
||||
priority: -100
|
||||
then:
|
||||
- component.update: co2
|
||||
- component.update: temperature
|
||||
- component.update: humidity
|
||||
- component.update: filter_remaining
|
||||
- component.update: flow_level
|
||||
- component.update: time_remaining
|
||||
- component.update: bypass
|
||||
- component.update: temperature_oda
|
||||
- component.update: temperature_sup
|
||||
- component.update: temperature_eta
|
||||
- component.update: temperature_eha
|
||||
- component.update: serial_number
|
||||
- component.update: comfort_temperature
|
||||
|
||||
host:
|
||||
api:
|
||||
batch_delay: 0ms # Disable batching to receive all state updates
|
||||
logger:
|
||||
level: VERBOSE
|
||||
|
||||
external_components:
|
||||
- source:
|
||||
type: local
|
||||
path: EXTERNAL_COMPONENT_PATH
|
||||
|
||||
# Dummy uart entry to satisfy DEPENDENCIES = ["uart"]
|
||||
uart:
|
||||
baud_rate: 57600
|
||||
port: /dev/null
|
||||
|
||||
time:
|
||||
- platform: host
|
||||
id: host_time
|
||||
|
||||
uart_mock:
|
||||
id: mock_uart
|
||||
baud_rate: 57600
|
||||
# A model of the Duco box: an acknowledgement, then the answer, both byte stuffed
|
||||
on_tx:
|
||||
- then:
|
||||
- lambda: |-
|
||||
static std::vector<uint8_t> rx;
|
||||
static uint8_t mode = 0x04;
|
||||
static uint8_t comfort = 210;
|
||||
rx.insert(rx.end(), data.begin(), data.end());
|
||||
while (rx.size() >= 3) {
|
||||
if (rx[0] != 0xAA || rx[1] != 0x55) {
|
||||
rx.erase(rx.begin());
|
||||
continue;
|
||||
}
|
||||
const size_t total = 2 + rx[2] + 3;
|
||||
if (rx.size() < total)
|
||||
return;
|
||||
std::vector<uint8_t> frame(rx.begin() + 2, rx.begin() + total);
|
||||
rx.erase(rx.begin(), rx.begin() + total);
|
||||
const uint8_t function = frame[1];
|
||||
const uint8_t message_id = frame[2];
|
||||
const uint8_t *d = frame.data() + 3;
|
||||
|
||||
auto reply = [message_id](uint8_t fn, std::vector<uint8_t> payload) {
|
||||
std::vector<uint8_t> body{static_cast<uint8_t>(payload.size() + 2), fn, message_id};
|
||||
body.insert(body.end(), payload.begin(), payload.end());
|
||||
const uint16_t crc = crc16(body.data(), body.size());
|
||||
body.push_back(crc & 0xFF);
|
||||
body.push_back(crc >> 8);
|
||||
std::vector<uint8_t> out{0xAA, 0x55};
|
||||
for (uint8_t b : body) {
|
||||
out.push_back(b);
|
||||
if (b == 0xAA)
|
||||
out.push_back(0x01);
|
||||
}
|
||||
id(mock_uart).inject_to_rx_buffer(out);
|
||||
};
|
||||
|
||||
reply(function + 1, {});
|
||||
if (function == 0x0c && d[0] == 0x01) {
|
||||
static const uint8_t TYPES[] = {0, 17, 8, 12};
|
||||
reply(0x0e, {d[1] < sizeof(TYPES) ? TYPES[d[1]] : uint8_t(0), 0, 0, 0, 0, 0, 0});
|
||||
} else if (function == 0x0c && d[0] == 0x02) {
|
||||
std::vector<uint8_t> status(20, 0);
|
||||
status[0] = mode;
|
||||
status[2] = 45;
|
||||
status[12] = 0xAA; // 21930 s, sent as AA 01 55: an escaped header inside the data
|
||||
status[13] = 0x55;
|
||||
reply(0x0e, status);
|
||||
} else if (function == 0x0c && d[0] == 0x04) {
|
||||
mode = d[2];
|
||||
ESP_LOGI("sim", "Mode set to %02X", mode);
|
||||
reply(0x0e, {0x01});
|
||||
} else if (function == 0x10 && d[3] == 0x49) {
|
||||
// temperature 21.5, CO2 682 (0x02AA exercises the byte stuffing), humidity 54.32
|
||||
reply(0x12, {0, 0, 215, 0, 0xAA, 0x02, 0x38, 0x15});
|
||||
} else if (function == 0x10 && d[3] == 0x1a) {
|
||||
std::vector<uint8_t> serial{0, 0, 'S', 'N', '1', '2', '3', '4'};
|
||||
serial.resize(18, 0);
|
||||
reply(0x12, serial);
|
||||
} else if (function == 0x24 && d[0] == 0x00) {
|
||||
int16_t value = 0;
|
||||
if (d[2] == 0x0a && d[1] == 0x12)
|
||||
value = comfort;
|
||||
else if (d[1] == 0x00)
|
||||
value = -35;
|
||||
else if (d[1] == 0x01)
|
||||
value = 195;
|
||||
else if (d[1] == 0x02)
|
||||
value = 220;
|
||||
else if (d[1] == 0x03)
|
||||
value = 80;
|
||||
else if (d[1] == 0x10)
|
||||
value = 50;
|
||||
else if (d[1] == 0x30)
|
||||
value = 120;
|
||||
reply(0x26, {0x01, d[1], d[2], uint8_t(value & 0xFF), uint8_t((value >> 8) & 0xFF), 0, 0});
|
||||
} else if (function == 0x24 && d[0] == 0x01) {
|
||||
comfort = d[3];
|
||||
ESP_LOGI("sim", "Comfort set to %u", comfort);
|
||||
reply(0x26, {0x01, d[1], d[2], d[3], 0, 0, 0});
|
||||
} else if (function == 0x24 && d[0] == 0x05) {
|
||||
ESP_LOGI("sim", "Time set to %u", encode_uint32(d[4], d[3], d[2], d[1]));
|
||||
reply(0x26, {0x05});
|
||||
}
|
||||
}
|
||||
|
||||
duco:
|
||||
uart_id: mock_uart
|
||||
discovery: {}
|
||||
time:
|
||||
time_id: host_time
|
||||
update_interval: 2s
|
||||
|
||||
select:
|
||||
- platform: duco
|
||||
name: Mode
|
||||
update_interval: 1s
|
||||
|
||||
number:
|
||||
- platform: duco
|
||||
id: comfort_temperature
|
||||
name: Comfort Temperature
|
||||
update_interval: never
|
||||
|
||||
text_sensor:
|
||||
- platform: duco
|
||||
id: serial_number
|
||||
name: Serial Number
|
||||
update_interval: never
|
||||
|
||||
sensor:
|
||||
- platform: duco
|
||||
co2:
|
||||
- address: 3
|
||||
id: co2
|
||||
name: CO2
|
||||
update_interval: never
|
||||
temperature:
|
||||
- address: 3
|
||||
id: temperature
|
||||
name: Temperature
|
||||
update_interval: never
|
||||
humidity:
|
||||
- address: 3
|
||||
id: humidity
|
||||
name: Humidity
|
||||
update_interval: never
|
||||
filter_remaining:
|
||||
id: filter_remaining
|
||||
name: Filter Remaining
|
||||
update_interval: never
|
||||
flow_level:
|
||||
id: flow_level
|
||||
name: Flow Level
|
||||
update_interval: never
|
||||
time_remaining:
|
||||
id: time_remaining
|
||||
name: Time Remaining
|
||||
update_interval: never
|
||||
bypass:
|
||||
id: bypass
|
||||
name: Bypass
|
||||
update_interval: never
|
||||
temperature_oda:
|
||||
id: temperature_oda
|
||||
name: Temperature ODA
|
||||
update_interval: never
|
||||
temperature_sup:
|
||||
id: temperature_sup
|
||||
name: Temperature SUP
|
||||
update_interval: never
|
||||
temperature_eta:
|
||||
id: temperature_eta
|
||||
name: Temperature ETA
|
||||
update_interval: never
|
||||
temperature_eha:
|
||||
id: temperature_eha
|
||||
name: Temperature EHA
|
||||
update_interval: never
|
||||
@@ -0,0 +1,98 @@
|
||||
"""Integration test for the Duco component against a model of the ventilation box.
|
||||
|
||||
Every entity reads its value from the box, a select or number change reaches the box, the clock
|
||||
is sent and discovery reports the nodes. The CO2 answer holds an 0xAA byte and the status answer an
|
||||
escaped AA 55, so the byte stuffing is exercised too. Most entities do not poll: their values come
|
||||
from one burst of requests at boot, more than the hub could once keep open.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from aioesphomeapi import (
|
||||
EntityState,
|
||||
NumberInfo,
|
||||
SelectInfo,
|
||||
SensorInfo,
|
||||
TextSensorInfo,
|
||||
)
|
||||
import pytest
|
||||
|
||||
from .log_utils import LineWaiter
|
||||
from .state_utils import require_entity
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
EXPECTED_SENSORS = {
|
||||
"co2": 682.0,
|
||||
"temperature": 21.5,
|
||||
"humidity": 54.32,
|
||||
"filter_remaining": 120.0,
|
||||
"flow_level": 45.0,
|
||||
"time_remaining": 21930.0,
|
||||
"bypass": 50.0,
|
||||
"temperature_oda": -3.5,
|
||||
"temperature_sup": 19.5,
|
||||
"temperature_eta": 22.0,
|
||||
"temperature_eha": 8.0,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_uart_mock_duco(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""All entities read the box; mode and comfort temperature changes are written."""
|
||||
waiter = LineWaiter()
|
||||
states: dict[int, EntityState] = {}
|
||||
|
||||
async def wait_for(condition, what: str) -> None:
|
||||
try:
|
||||
async with asyncio.timeout(10.0):
|
||||
while not condition():
|
||||
await asyncio.sleep(0.05)
|
||||
except TimeoutError:
|
||||
pytest.fail(f"Timeout waiting for {what}; states: {states}")
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config, line_callback=waiter.callback),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
entities, _ = await client.list_entities_services()
|
||||
sensors = {
|
||||
key: require_entity(entities, key, SensorInfo, exact=True)
|
||||
for key in EXPECTED_SENSORS
|
||||
}
|
||||
mode = require_entity(entities, "mode", SelectInfo)
|
||||
comfort = require_entity(entities, "comfort_temperature", NumberInfo)
|
||||
serial = require_entity(entities, "serial_number", TextSensorInfo)
|
||||
|
||||
client.subscribe_states(lambda state: states.__setitem__(state.key, state))
|
||||
|
||||
def state_of(entity):
|
||||
state = states.get(entity.key)
|
||||
return None if state is None or state.missing_state else state.state
|
||||
|
||||
for key, expected in EXPECTED_SENSORS.items():
|
||||
await wait_for(
|
||||
lambda e=sensors[key], v=expected: state_of(e) == pytest.approx(v),
|
||||
key,
|
||||
)
|
||||
await wait_for(lambda: state_of(mode) == "MAN1", "mode")
|
||||
await wait_for(lambda: state_of(comfort) == pytest.approx(21.0), "comfort")
|
||||
await wait_for(lambda: state_of(serial) == "SN1234", "serial number")
|
||||
await waiter.wait_for("Time set to")
|
||||
|
||||
client.select_command(mode.key, "CNT2")
|
||||
await waiter.wait_for("Mode set to 09")
|
||||
await wait_for(lambda: state_of(mode) == "CNT2", "new mode")
|
||||
|
||||
client.number_command(comfort.key, 22.3)
|
||||
await waiter.wait_for("Comfort set to 223")
|
||||
await wait_for(lambda: state_of(comfort) == pytest.approx(22.3), "new comfort")
|
||||
|
||||
await waiter.wait_for_each(
|
||||
"Found node 1: type 17", "Found node 3: type 12", timeout=40.0
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
# Common UART configuration for ESP32 IDF tests - 57600 baud
|
||||
# For components that require UART baud rate 57600 (duco)
|
||||
|
||||
substitutions:
|
||||
tx_pin: GPIO17
|
||||
rx_pin: GPIO16
|
||||
|
||||
uart:
|
||||
- id: uart_bus
|
||||
tx_pin: ${tx_pin}
|
||||
rx_pin: ${rx_pin}
|
||||
baud_rate: 57600
|
||||
@@ -0,0 +1,11 @@
|
||||
# Common UART configuration for ESP8266 Arduino tests - 57600 baud
|
||||
|
||||
substitutions:
|
||||
tx_pin: GPIO1
|
||||
rx_pin: GPIO3
|
||||
|
||||
uart:
|
||||
- id: uart_bus
|
||||
tx_pin: ${tx_pin}
|
||||
rx_pin: ${rx_pin}
|
||||
baud_rate: 57600
|
||||
@@ -0,0 +1,11 @@
|
||||
# Common UART configuration for RP2040 Arduino tests - 57600 baud
|
||||
|
||||
substitutions:
|
||||
tx_pin: GPIO0
|
||||
rx_pin: GPIO1
|
||||
|
||||
uart:
|
||||
- id: uart_bus
|
||||
tx_pin: ${tx_pin}
|
||||
rx_pin: ${rx_pin}
|
||||
baud_rate: 57600
|
||||
Reference in New Issue
Block a user