mirror of
https://github.com/esphome/esphome.git
synced 2026-10-26 16:02:13 +00:00
Compare commits
33
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2df80fbf71 | ||
|
|
d2f6b389d4 | ||
|
|
470f7eb51d | ||
|
|
e7b2dfed57 | ||
|
|
875b2076c0 | ||
|
|
83080c4560 | ||
|
|
104cb09475 | ||
|
|
a7824ed4e7 | ||
|
|
6420db90b1 | ||
|
|
d3c2a7c734 | ||
|
|
c21660251c | ||
|
|
995acb94b4 | ||
|
|
24b7eefaad | ||
|
|
2349e0be00 | ||
|
|
63349801b7 | ||
|
|
afdfa7b298 | ||
|
|
400407b135 | ||
|
|
22b32b194a | ||
|
|
6dcd989a7f | ||
|
|
2016884210 | ||
|
|
d3f4f44c7b | ||
|
|
6df15be714 | ||
|
|
a03fe3c38e | ||
|
|
afc8c51f93 | ||
|
|
ef7ed69f9d | ||
|
|
ec32802662 | ||
|
|
433d8046b1 | ||
|
|
8ecb981656 | ||
|
|
8c990573d7 | ||
|
|
bdec0c1485 | ||
|
|
70e85d939d | ||
|
|
25b3c2820a | ||
|
|
ef53e675ff |
No files matched your search
@@ -13,19 +13,10 @@ from esphome.const import (
|
||||
from esphome.core.entity_helpers import inherit_property_from
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import copy_ns
|
||||
|
||||
CopySensor = copy_ns.class_("CopySensor", sensor.Sensor, cg.Component)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
sensor.sensor_schema(CopySensor)
|
||||
.extend(
|
||||
{
|
||||
cv.Required(CONF_SOURCE_ID): cv.use_id(sensor.Sensor),
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
CONFIG_SCHEMA = sensor.sensor_schema(sensor.Sensor).extend(
|
||||
{
|
||||
cv.Required(CONF_SOURCE_ID): cv.use_id(sensor.Sensor),
|
||||
}
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = cv.All(
|
||||
@@ -40,6 +31,4 @@ FINAL_VALIDATE_SCHEMA = cv.All(
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await sensor.new_sensor(config)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
await cg.add_variable(var.set_source, config[CONF_SOURCE_ID])
|
||||
sensor.follow_sensor(var, await cg.get_variable(config[CONF_SOURCE_ID]))
|
||||
@@ -1,16 +0,0 @@
|
||||
#include "copy_sensor.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::copy {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "copy.sensor");
|
||||
|
||||
void CopySensor::setup() {
|
||||
source_->add_on_state_callback([this](float value) { this->publish_state(value); });
|
||||
if (source_->has_state())
|
||||
this->publish_state(source_->state);
|
||||
}
|
||||
|
||||
void CopySensor::dump_config() { LOG_SENSOR("", "Copy Sensor", this); }
|
||||
|
||||
} // namespace esphome::copy
|
||||
@@ -1,18 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
|
||||
namespace esphome::copy {
|
||||
|
||||
class CopySensor final : public sensor::Sensor, public Component {
|
||||
public:
|
||||
void set_source(sensor::Sensor *source) { source_ = source; }
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
sensor::Sensor *source_;
|
||||
};
|
||||
|
||||
} // namespace esphome::copy
|
||||
@@ -5,19 +5,10 @@ from esphome.const import CONF_ENTITY_CATEGORY, CONF_ICON, CONF_SOURCE_ID
|
||||
from esphome.core.entity_helpers import inherit_property_from
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import copy_ns
|
||||
|
||||
CopyTextSensor = copy_ns.class_("CopyTextSensor", text_sensor.TextSensor, cg.Component)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
text_sensor.text_sensor_schema(CopyTextSensor)
|
||||
.extend(
|
||||
{
|
||||
cv.Required(CONF_SOURCE_ID): cv.use_id(text_sensor.TextSensor),
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
CONFIG_SCHEMA = text_sensor.text_sensor_schema(text_sensor.TextSensor).extend(
|
||||
{
|
||||
cv.Required(CONF_SOURCE_ID): cv.use_id(text_sensor.TextSensor),
|
||||
}
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = cv.All(
|
||||
@@ -28,6 +19,4 @@ FINAL_VALIDATE_SCHEMA = cv.All(
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await text_sensor.new_text_sensor(config)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
await cg.add_variable(var.set_source, config[CONF_SOURCE_ID])
|
||||
text_sensor.follow_text_sensor(var, await cg.get_variable(config[CONF_SOURCE_ID]))
|
||||
@@ -1,16 +0,0 @@
|
||||
#include "copy_text_sensor.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::copy {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "copy.text_sensor");
|
||||
|
||||
void CopyTextSensor::setup() {
|
||||
source_->add_on_state_callback([this](const std::string &value) { this->publish_state(value); });
|
||||
if (source_->has_state())
|
||||
this->publish_state(source_->state);
|
||||
}
|
||||
|
||||
void CopyTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Copy Sensor", this); }
|
||||
|
||||
} // namespace esphome::copy
|
||||
@@ -1,18 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/text_sensor/text_sensor.h"
|
||||
|
||||
namespace esphome::copy {
|
||||
|
||||
class CopyTextSensor final : public text_sensor::TextSensor, public Component {
|
||||
public:
|
||||
void set_source(text_sensor::TextSensor *source) { source_ = source; }
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
text_sensor::TextSensor *source_;
|
||||
};
|
||||
|
||||
} // namespace esphome::copy
|
||||
@@ -1,6 +1,7 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Protocol
|
||||
|
||||
from esphome import automation, core
|
||||
from esphome import automation, core, pins
|
||||
from esphome.automation import maybe_simple_id
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.const import (
|
||||
@@ -13,6 +14,7 @@ import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_AUTO_CLEAR_ENABLED,
|
||||
CONF_DIMENSIONS,
|
||||
CONF_ENABLE_PIN,
|
||||
CONF_FROM,
|
||||
CONF_HEIGHT,
|
||||
CONF_ID,
|
||||
@@ -54,6 +56,33 @@ DISPLAY_ROTATIONS = {
|
||||
}
|
||||
|
||||
|
||||
class DisplayModel(Protocol):
|
||||
"""The parts of a display model used by enable_pin_schema()."""
|
||||
|
||||
def get_default(self, key: str, fallback: Any = ...) -> Any: ...
|
||||
|
||||
|
||||
def enable_pin_schema(
|
||||
model: DisplayModel, config: ConfigType
|
||||
) -> dict[cv.Optional, Any]:
|
||||
"""
|
||||
Get the schema entry for a display's enable pins.
|
||||
Pins given in the user config are used as they are. Otherwise the model's default enable
|
||||
pins are used, except any that is also used elsewhere in the config, so a user can take
|
||||
control of that pin, e.g. with a switch.
|
||||
:param config: The display config as given by the user
|
||||
"""
|
||||
schema = cv.ensure_list(pins.gpio_output_pin_schema)
|
||||
default = model.get_default(CONF_ENABLE_PIN, None)
|
||||
if default is None or CONF_ENABLE_PIN in config:
|
||||
return {cv.Optional(CONF_ENABLE_PIN): schema}
|
||||
return {
|
||||
cv.Optional(
|
||||
CONF_ENABLE_PIN, default=default
|
||||
): pins.PIN_SCHEMA_REGISTRY.optional(schema)
|
||||
}
|
||||
|
||||
|
||||
def validate_rotation(value):
|
||||
value = cv.string(value)
|
||||
value = value.removesuffix("°")
|
||||
|
||||
@@ -146,7 +146,7 @@ def model_schema(config):
|
||||
cv.GenerateID(): cv.declare_id(class_name),
|
||||
cv.GenerateID(CONF_INIT_SEQUENCE_ID): cv.declare_id(cg.uint8),
|
||||
cv_dimensions(CONF_DIMENSIONS): dimension_schema(model),
|
||||
model.option(CONF_ENABLE_PIN): cv.ensure_list(pins.gpio_output_pin_schema),
|
||||
**display.enable_pin_schema(model, config),
|
||||
model.option(CONF_INIT_SEQUENCE, cv.UNDEFINED): cv.ensure_list(
|
||||
map_sequence
|
||||
),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from typing import Any, Self
|
||||
|
||||
from esphome.components.mipi import check_model_requirements
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_DIMENSIONS, CONF_HEIGHT, CONF_OFFSET_HEIGHT, CONF_WIDTH
|
||||
from esphome.core import CORE
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import ConfigType
|
||||
|
||||
@@ -128,24 +128,5 @@ class EpaperModel:
|
||||
return model
|
||||
|
||||
def check_requirements(self) -> None:
|
||||
"""
|
||||
Raise a friendly error if any component this model requires is not configured.
|
||||
|
||||
This runs during schema validation (before ID references are resolved) so that a
|
||||
model whose default pins live on a pin expander reports the missing expander clearly
|
||||
instead of a cryptic "Couldn't find ID" from the unresolved pin reference.
|
||||
"""
|
||||
if requirements := self.get_default("requires", set()):
|
||||
# ``raw_config`` is populated before any component schema runs during a real
|
||||
# validation, so presence of a required component is simply a top-level key.
|
||||
# When it is absent (e.g. a unit test that invokes the schema directly) there
|
||||
# is no config to check against, so skip.
|
||||
global_config = CORE.raw_config
|
||||
if global_config is None:
|
||||
return
|
||||
missing = {x for x in requirements if x not in global_config}
|
||||
if missing:
|
||||
reqstr = ", ".join(f"'{x}'" for x in sorted(missing))
|
||||
raise cv.Invalid(
|
||||
f"{self.name} requires component{'s' if len(missing) > 1 else ''} {reqstr} to be configured"
|
||||
)
|
||||
"""Check that the components this model needs are configured, see check_model_requirements()."""
|
||||
check_model_requirements(self.name, self.defaults)
|
||||
@@ -31,11 +31,25 @@ static inline bool IRAM_ATTR is_code_addr(uint32_t addr) {
|
||||
}
|
||||
|
||||
#if CONFIG_IDF_TARGET_ARCH_RISCV
|
||||
// Bounds of this image's flash-mapped code, from the ESP-IDF linker script.
|
||||
extern "C" char _instruction_reserved_start;
|
||||
extern "C" char _instruction_reserved_end;
|
||||
|
||||
// Check that [start, end) lies in this image's flash-mapped code or in IRAM.
|
||||
// The IROM range is larger than what the MMU maps, and reading an unmapped
|
||||
// address faults.
|
||||
static inline bool is_readable_code(uint32_t start, uint32_t end) {
|
||||
auto irom_start = reinterpret_cast<uint32_t>(&_instruction_reserved_start);
|
||||
auto irom_end = reinterpret_cast<uint32_t>(&_instruction_reserved_end);
|
||||
return (start >= irom_start && end <= irom_end) || (start >= SOC_IRAM_LOW && end <= SOC_IRAM_HIGH);
|
||||
}
|
||||
|
||||
// Check if a code address is a real return address by verifying the preceding
|
||||
// instruction is a JAL or JALR with rd=ra (x1). Called at log time (not during
|
||||
// panic) so flash cache is available and both IRAM and IROM are safely readable.
|
||||
// panic) so flash cache is available. The address may come from an earlier
|
||||
// build, so check that the bytes before it are mapped before reading them.
|
||||
static inline bool is_return_addr(uint32_t addr) {
|
||||
if (!is_code_addr(addr) || addr < 4)
|
||||
if (!is_code_addr(addr) || !is_readable_code(addr - 4, addr))
|
||||
return false;
|
||||
// A return address on the stack points to the instruction after a call.
|
||||
// Check for 4-byte JAL/JALR call instruction before this address.
|
||||
|
||||
@@ -271,7 +271,9 @@ void ESP32Camera::loop() {
|
||||
fb->timestamp = timestamp;
|
||||
}
|
||||
#endif
|
||||
this->current_image_ = std::make_shared<ESP32CameraImage>(fb, this->single_requesters_ | this->stream_requesters_);
|
||||
// A request that arrives while this image is handed out is kept for the next frame
|
||||
const uint8_t single_requesters = this->single_requesters_.exchange(0);
|
||||
this->current_image_ = std::make_shared<ESP32CameraImage>(fb, single_requesters | this->stream_requesters_);
|
||||
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
ESP_LOGV(TAG, "Got Image: len=%u", fb->len);
|
||||
@@ -291,7 +293,6 @@ void ESP32Camera::loop() {
|
||||
listener->on_camera_image(this->current_image_);
|
||||
}
|
||||
this->last_update_ = now;
|
||||
this->single_requesters_ = 0;
|
||||
}
|
||||
|
||||
/* ---------------- constructors ---------------- */
|
||||
|
||||
@@ -373,7 +373,7 @@ void ESPHomeOTAComponent::handle_handshake_() {
|
||||
// Value initialized: a corrupt stream that back references the
|
||||
// window before it is filled then copies zeros, never stale memory.
|
||||
// Default placement, PSRAM first where present: the session lives for one
|
||||
// upload and keeps 4.9 KB of internal heap free while it runs
|
||||
// upload and keeps 6.0 KB of internal heap free while it runs
|
||||
this->inflate_ = RAMAllocator<InflateSession>().make_unique();
|
||||
if (this->inflate_ != nullptr) {
|
||||
this->handshake_buf_[1] |= SERVER_FEATURE_SUPPORTS_DEFLATE;
|
||||
|
||||
@@ -36,7 +36,10 @@
|
||||
* Altered for ESPHome: this is the raw deflate decoder from uzlib's
|
||||
* tinflate.c (v2.9.5) with the gzip/zlib header parsers, checksums,
|
||||
* runtime table builder and in-memory (non ring window) output path
|
||||
* removed, and the public names prefixed with ota_inflate.
|
||||
* removed, and the public names prefixed with ota_inflate. Symbols are
|
||||
* decoded from a 32 bit bit buffer through 9 bit lookup tables, with a
|
||||
* fast loop for when a whole symbol fits in both the input and the output,
|
||||
* and the output is written straight into the ring window.
|
||||
*/
|
||||
|
||||
#include "ota_esphome_inflate.h"
|
||||
@@ -49,16 +52,6 @@
|
||||
#define TINF_DICT_ERROR OTA_INFLATE_DICT_ERROR
|
||||
#define TINF_DATA struct OtaInflateState
|
||||
#define TINF_TREE struct OtaInflateTree
|
||||
#define TINF_ARRAY_SIZE(arr) (sizeof(arr) / sizeof(*(arr)))
|
||||
|
||||
/* every output byte also goes into the ring window */
|
||||
#define TINF_PUT(d, c) \
|
||||
{ \
|
||||
*d->dest++ = c; \
|
||||
d->dict_ring[d->dict_idx++] = c; \
|
||||
if (d->dict_idx == d->dict_size) \
|
||||
d->dict_idx = 0; \
|
||||
}
|
||||
|
||||
/* --------------------------------------------------- *
|
||||
* -- constant tables (upstream builds them at runtime) -- *
|
||||
@@ -82,34 +75,52 @@ static const unsigned char CLCIDX[] = {16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4,
|
||||
* -- utility functions -- *
|
||||
* ----------------------- */
|
||||
|
||||
/* given an array of code lengths, build a tree */
|
||||
static void tinf_build_tree(TINF_TREE *t, const unsigned char *lengths, unsigned int num) {
|
||||
unsigned short offs[16];
|
||||
unsigned int i, sum;
|
||||
enum {
|
||||
LIT_FAST_BITS = OTA_INFLATE_LIT_FAST_BITS,
|
||||
DIST_FAST_BITS = OTA_INFLATE_DIST_FAST_BITS,
|
||||
/* a lookup table entry is (code length << SYM_BITS) | symbol; 0 marks a longer code */
|
||||
SYM_BITS = 9,
|
||||
SYM_MASK = (1 << SYM_BITS) - 1,
|
||||
/* input bytes one pass of the fast loop may take: three refills of up to 4 bytes */
|
||||
FAST_IN_MIN = 12,
|
||||
};
|
||||
|
||||
/* given an array of code lengths, build a tree and its lookup table */
|
||||
static void tinf_build_tree(TINF_TREE *t, const unsigned char *lengths, unsigned int num, unsigned int fast_bits) {
|
||||
const unsigned int fast_size = 1u << fast_bits;
|
||||
unsigned short offs[16], next[16];
|
||||
unsigned int i, sum, code;
|
||||
|
||||
/* clear code length count table */
|
||||
for (i = 0; i < 16; ++i)
|
||||
t->table[i] = 0;
|
||||
|
||||
/* scan symbol lengths, and sum code length counts */
|
||||
for (i = 0; i < num; ++i)
|
||||
t->table[lengths[i]]++;
|
||||
|
||||
/* In the lengths array, 0 means unused code. So, t->table[0] now contains
|
||||
number of unused codes. But table's purpose is to contain # of codes of
|
||||
particular length, and there're 0 codes of length 0. */
|
||||
t->table[0] = 0;
|
||||
|
||||
/* compute offset table for distribution sort */
|
||||
for (sum = 0, i = 0; i < 16; ++i) {
|
||||
for (sum = 0, code = 0, i = 0; i < 16; ++i) {
|
||||
offs[i] = sum;
|
||||
sum += t->table[i];
|
||||
if (i > 0)
|
||||
code = (code + t->table[i - 1]) << 1;
|
||||
next[i] = code;
|
||||
}
|
||||
|
||||
/* create code->symbol translation table (symbols sorted by code) */
|
||||
for (i = 0; i < fast_size; ++i)
|
||||
t->fast[i] = 0;
|
||||
|
||||
for (i = 0; i < num; ++i) {
|
||||
if (lengths[i])
|
||||
t->trans[offs[lengths[i]]++] = i;
|
||||
unsigned int len = lengths[i];
|
||||
if (!len)
|
||||
continue;
|
||||
t->trans[offs[len]++] = i;
|
||||
if (len <= fast_bits) {
|
||||
/* codes arrive MSB first, the bit buffer is LSB first */
|
||||
unsigned int c = next[len]++, rev = 0, j;
|
||||
for (j = 0; j < len; ++j, c >>= 1)
|
||||
rev = (rev << 1) | (c & 1);
|
||||
for (j = rev; j < fast_size; j += 1u << len)
|
||||
t->fast[j] = (unsigned short) ((len << SYM_BITS) | i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,89 +128,78 @@ static void tinf_build_tree(TINF_TREE *t, const unsigned char *lengths, unsigned
|
||||
* -- decode functions -- *
|
||||
* ---------------------- */
|
||||
|
||||
static unsigned char uzlib_get_byte(TINF_DATA *d) {
|
||||
/* If end of source buffer is not reached, return next byte from source
|
||||
buffer. */
|
||||
if (d->source < d->source_limit) {
|
||||
return *d->source++;
|
||||
/* top the bit buffer up from the input buffer, without blocking for more */
|
||||
static inline void tinf_refill(TINF_DATA *d) {
|
||||
while (d->bitcount <= 24 && d->source < d->source_limit) {
|
||||
d->tag |= (unsigned int) *d->source++ << d->bitcount;
|
||||
d->bitcount += 8;
|
||||
}
|
||||
|
||||
/* Otherwise if there's callback and we haven't seen EOF yet, try to
|
||||
read next byte using it. (Note: the callback can also update ->source
|
||||
and ->source_limit). */
|
||||
if (!d->eof) {
|
||||
int val = d->source_read_cb(d);
|
||||
if (val >= 0) {
|
||||
return (unsigned char) val;
|
||||
}
|
||||
}
|
||||
|
||||
/* Otherwise, we hit EOF (either from ->source_read_cb() or from exhaustion
|
||||
of the buffer), and it will be "sticky", i.e. further calls to this
|
||||
function will end up here too. */
|
||||
d->eof = true;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* get one bit from source stream */
|
||||
static int tinf_getbit(TINF_DATA *d) {
|
||||
unsigned int bit;
|
||||
|
||||
/* check if tag is empty */
|
||||
if (!d->bitcount--) {
|
||||
/* load next tag */
|
||||
d->tag = uzlib_get_byte(d);
|
||||
d->bitcount = 7;
|
||||
/* make at least num (<= 16) bits available; past the end of input reads zeros and sets eof */
|
||||
static inline void tinf_need(TINF_DATA *d, unsigned int num) {
|
||||
while (d->bitcount < num) {
|
||||
int val = -1;
|
||||
if (d->source < d->source_limit) {
|
||||
val = *d->source++;
|
||||
} else if (!d->eof) {
|
||||
val = d->source_read_cb(d);
|
||||
}
|
||||
if (val < 0) {
|
||||
d->eof = true;
|
||||
val = 0;
|
||||
}
|
||||
d->tag |= (unsigned int) val << d->bitcount;
|
||||
d->bitcount += 8;
|
||||
}
|
||||
}
|
||||
|
||||
/* shift bit out of tag */
|
||||
bit = d->tag & 0x01;
|
||||
d->tag >>= 1;
|
||||
|
||||
return bit;
|
||||
static inline unsigned int tinf_take(TINF_DATA *d, unsigned int num) {
|
||||
unsigned int val = d->tag & ((1u << num) - 1);
|
||||
d->tag >>= num;
|
||||
d->bitcount -= num;
|
||||
return val;
|
||||
}
|
||||
|
||||
/* read a num bit value from a stream and add base */
|
||||
static unsigned int tinf_read_bits(TINF_DATA *d, int num, int base) {
|
||||
unsigned int val = 0;
|
||||
|
||||
/* read num bits */
|
||||
if (num) {
|
||||
unsigned int limit = 1 << (num);
|
||||
unsigned int mask;
|
||||
|
||||
for (mask = 1; mask < limit; mask *= 2)
|
||||
if (tinf_getbit(d))
|
||||
val += mask;
|
||||
}
|
||||
|
||||
return val + base;
|
||||
static inline unsigned int tinf_read_bits(TINF_DATA *d, unsigned int num, unsigned int base) {
|
||||
tinf_need(d, num);
|
||||
return tinf_take(d, num) + base;
|
||||
}
|
||||
|
||||
/* given a data stream and a tree, decode a symbol */
|
||||
static int tinf_decode_symbol(TINF_DATA *d, TINF_TREE *t) {
|
||||
int sum = 0, cur = 0, len = 0;
|
||||
|
||||
/* get more bits while code value is above sum */
|
||||
do {
|
||||
cur = 2 * cur + tinf_getbit(d);
|
||||
|
||||
if (++len == TINF_ARRAY_SIZE(t->table)) {
|
||||
return TINF_DATA_ERROR;
|
||||
/* canonical decode of the code at the bottom of tag; len gets the code length.
|
||||
The trans index stays below the tree's symbol count, which trans always holds */
|
||||
static int tinf_decode_long(const TINF_TREE *t, unsigned int tag, unsigned int *len) {
|
||||
unsigned int code = 0, first = 0, index = 0, l;
|
||||
for (l = 1; l < 16; ++l, tag >>= 1) {
|
||||
unsigned int count = t->table[l];
|
||||
code |= tag & 1;
|
||||
if (code - first < count) {
|
||||
*len = l;
|
||||
return t->trans[index + code - first];
|
||||
}
|
||||
|
||||
sum += t->table[len];
|
||||
cur -= t->table[len];
|
||||
|
||||
} while (cur >= 0);
|
||||
|
||||
sum += cur;
|
||||
if (sum < 0 || sum >= t->size) {
|
||||
return TINF_DATA_ERROR;
|
||||
index += count;
|
||||
first = (first + count) << 1;
|
||||
code <<= 1;
|
||||
}
|
||||
return TINF_DATA_ERROR;
|
||||
}
|
||||
|
||||
return t->trans[sum];
|
||||
/* decode a symbol, reading more input only while the code is not all in the bit
|
||||
buffer; bits above bitcount are zero, so a code no longer than bitcount is real */
|
||||
static int tinf_decode_symbol(TINF_DATA *d, const TINF_TREE *t) {
|
||||
tinf_refill(d);
|
||||
for (;;) {
|
||||
unsigned int len;
|
||||
int sym = tinf_decode_long(t, d->tag, &len);
|
||||
if (sym >= 0 && len <= d->bitcount) {
|
||||
tinf_take(d, len);
|
||||
return sym;
|
||||
}
|
||||
if (d->bitcount >= 15)
|
||||
return TINF_DATA_ERROR;
|
||||
tinf_need(d, d->bitcount + 1);
|
||||
}
|
||||
}
|
||||
|
||||
/* given a data stream, decode dynamic trees from it */
|
||||
@@ -230,7 +230,7 @@ static int tinf_decode_trees(TINF_DATA *d, TINF_TREE *lt, TINF_TREE *dt) {
|
||||
}
|
||||
|
||||
/* build code length tree, temporarily use length tree */
|
||||
tinf_build_tree(lt, lengths, 19);
|
||||
tinf_build_tree(lt, lengths, 19, LIT_FAST_BITS);
|
||||
|
||||
/* decode code lengths for the dynamic trees */
|
||||
hlimit = hlit + hdist;
|
||||
@@ -282,8 +282,8 @@ static int tinf_decode_trees(TINF_DATA *d, TINF_TREE *lt, TINF_TREE *dt) {
|
||||
}
|
||||
|
||||
/* build dynamic trees */
|
||||
tinf_build_tree(lt, lengths, hlit);
|
||||
tinf_build_tree(dt, lengths + hlit, hdist);
|
||||
tinf_build_tree(lt, lengths, hlit, LIT_FAST_BITS);
|
||||
tinf_build_tree(dt, lengths + hlit, hdist, DIST_FAST_BITS);
|
||||
|
||||
return TINF_OK;
|
||||
}
|
||||
@@ -302,84 +302,189 @@ static void tinf_build_fixed_trees(TINF_TREE *lt, TINF_TREE *dt) {
|
||||
lengths[i] = 7;
|
||||
for (; i < 288; ++i)
|
||||
lengths[i] = 8;
|
||||
tinf_build_tree(lt, lengths, 288);
|
||||
tinf_build_tree(lt, lengths, 288, LIT_FAST_BITS);
|
||||
|
||||
for (i = 0; i < 32; ++i)
|
||||
lengths[i] = 5;
|
||||
tinf_build_tree(dt, lengths, 32);
|
||||
tinf_build_tree(dt, lengths, 32, DIST_FAST_BITS);
|
||||
}
|
||||
|
||||
/* inflate with the bit buffer and both cursors in locals while the input holds a
|
||||
whole symbol; a match that runs past the end of the ring is left in curlen */
|
||||
static int tinf_inflate_fast(TINF_DATA *d, const TINF_TREE *lt, const TINF_TREE *dt) {
|
||||
const unsigned char *src = d->source;
|
||||
const unsigned char *const src_end = d->source_limit;
|
||||
unsigned char *const ring = d->dict_ring;
|
||||
unsigned char *const ring_end = ring + d->dict_size;
|
||||
unsigned char *out = d->dest;
|
||||
const uint16_t *const lfast = lt->fast;
|
||||
const uint16_t *const dfast = dt->fast;
|
||||
unsigned int tag = d->tag, bits = d->bitcount;
|
||||
int res = TINF_OK;
|
||||
|
||||
#define REFILL() \
|
||||
while (bits <= 24) { \
|
||||
tag |= (unsigned int) *src++ << bits; \
|
||||
bits += 8; \
|
||||
}
|
||||
#define DROP(n) (tag >>= (n), bits -= (n))
|
||||
|
||||
while (src_end - src >= FAST_IN_MIN && out < ring_end) {
|
||||
unsigned int e, len;
|
||||
int sym;
|
||||
|
||||
REFILL();
|
||||
e = lfast[tag & ((1u << LIT_FAST_BITS) - 1)];
|
||||
if (e) {
|
||||
len = e >> SYM_BITS;
|
||||
sym = e & SYM_MASK;
|
||||
} else {
|
||||
sym = tinf_decode_long(lt, tag, &len);
|
||||
if (sym < 0) {
|
||||
res = sym;
|
||||
break;
|
||||
}
|
||||
}
|
||||
DROP(len);
|
||||
|
||||
if (sym < 256) {
|
||||
*out++ = sym;
|
||||
continue;
|
||||
}
|
||||
if (sym == 256) {
|
||||
res = TINF_DONE;
|
||||
break;
|
||||
}
|
||||
sym -= 257;
|
||||
if (sym >= 29) {
|
||||
res = TINF_DATA_ERROR;
|
||||
break;
|
||||
}
|
||||
/* the lit/len code and its extra bits fit the refilled buffer together */
|
||||
unsigned int length = LENGTH_BASE[sym] + (tag & ((1u << LENGTH_BITS[sym]) - 1));
|
||||
DROP(LENGTH_BITS[sym]);
|
||||
|
||||
REFILL();
|
||||
e = dfast[tag & ((1u << DIST_FAST_BITS) - 1)];
|
||||
if (e) {
|
||||
len = e >> SYM_BITS;
|
||||
sym = e & SYM_MASK;
|
||||
} else {
|
||||
sym = tinf_decode_long(dt, tag, &len);
|
||||
}
|
||||
if (sym < 0 || sym >= 30) {
|
||||
res = TINF_DATA_ERROR;
|
||||
break;
|
||||
}
|
||||
DROP(len);
|
||||
REFILL();
|
||||
unsigned int offs = DIST_BASE[sym] + (tag & ((1u << DIST_BITS[sym]) - 1));
|
||||
DROP(DIST_BITS[sym]);
|
||||
if (offs > d->dict_size) {
|
||||
res = TINF_DICT_ERROR;
|
||||
break;
|
||||
}
|
||||
|
||||
/* wrap as an index: a pointer before the ring would be undefined */
|
||||
unsigned int at = out - ring;
|
||||
const unsigned char *from = ring + (at >= offs ? at - offs : at + d->dict_size - offs);
|
||||
unsigned int n = ring_end - out;
|
||||
if (n > length)
|
||||
n = length;
|
||||
length -= n;
|
||||
/* at most two runs: up to the end of the ring, then on from its start */
|
||||
while (n) {
|
||||
unsigned int run = ring_end - from;
|
||||
if (run > n)
|
||||
run = n;
|
||||
n -= run;
|
||||
do {
|
||||
*out++ = *from++;
|
||||
} while (--run);
|
||||
if (from == ring_end)
|
||||
from = ring;
|
||||
}
|
||||
if (length) {
|
||||
d->curlen = length;
|
||||
d->lz_off = from - ring;
|
||||
}
|
||||
}
|
||||
#undef REFILL
|
||||
#undef DROP
|
||||
|
||||
d->source = src;
|
||||
d->dest = out;
|
||||
d->tag = tag;
|
||||
d->bitcount = bits;
|
||||
return res;
|
||||
}
|
||||
|
||||
/* ----------------------------- *
|
||||
* -- block inflate functions -- *
|
||||
* ----------------------------- */
|
||||
|
||||
/* given a stream and two trees, inflate next chunk of output (a byte or more) */
|
||||
/* given a stream and two trees, inflate until the ring is full or the block ends */
|
||||
static int tinf_inflate_block_data(TINF_DATA *d, TINF_TREE *lt, TINF_TREE *dt) {
|
||||
if (d->curlen == 0) {
|
||||
unsigned char *const ring_end = d->dict_ring + d->dict_size;
|
||||
while (d->dest < ring_end) {
|
||||
if (d->curlen != 0) {
|
||||
unsigned int n = ring_end - d->dest;
|
||||
if (n > d->curlen)
|
||||
n = d->curlen;
|
||||
d->curlen -= n;
|
||||
do {
|
||||
*d->dest++ = d->dict_ring[d->lz_off];
|
||||
if ((unsigned) ++d->lz_off == d->dict_size)
|
||||
d->lz_off = 0;
|
||||
} while (--n);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (d->source_limit - d->source >= FAST_IN_MIN) {
|
||||
int res = tinf_inflate_fast(d, lt, dt);
|
||||
if (res != TINF_OK)
|
||||
return res;
|
||||
continue;
|
||||
}
|
||||
|
||||
unsigned int offs;
|
||||
int dist;
|
||||
int sym = tinf_decode_symbol(d, lt);
|
||||
|
||||
if (d->eof) {
|
||||
if (d->eof)
|
||||
return TINF_DATA_ERROR;
|
||||
}
|
||||
|
||||
if (sym < 0) {
|
||||
if (sym < 0)
|
||||
return sym;
|
||||
}
|
||||
|
||||
/* literal byte */
|
||||
if (sym < 256) {
|
||||
TINF_PUT(d, sym);
|
||||
return TINF_OK;
|
||||
*d->dest++ = sym;
|
||||
continue;
|
||||
}
|
||||
|
||||
/* end of block */
|
||||
if (sym == 256) {
|
||||
if (sym == 256)
|
||||
return TINF_DONE;
|
||||
}
|
||||
|
||||
/* substring from sliding dictionary */
|
||||
sym -= 257;
|
||||
if (sym >= 29) {
|
||||
if (sym >= 29)
|
||||
return TINF_DATA_ERROR;
|
||||
}
|
||||
|
||||
/* possibly get more bits from length code */
|
||||
d->curlen = tinf_read_bits(d, LENGTH_BITS[sym], LENGTH_BASE[sym]);
|
||||
|
||||
dist = tinf_decode_symbol(d, dt);
|
||||
if (dist < 0 || dist >= 30) {
|
||||
if (dist < 0 || dist >= 30)
|
||||
return TINF_DATA_ERROR;
|
||||
}
|
||||
|
||||
/* possibly get more bits from distance code */
|
||||
offs = tinf_read_bits(d, DIST_BITS[dist], DIST_BASE[dist]);
|
||||
|
||||
/* calculate and validate actual LZ offset to use */
|
||||
if (offs > d->dict_size) {
|
||||
if (offs > d->dict_size)
|
||||
return TINF_DICT_ERROR;
|
||||
}
|
||||
/* Note: we don't try to catch offset which points to not yet filled
|
||||
part of the dictionary here. Doing so would require keeping another
|
||||
variable to track "filled in" size of the dictionary. Appearance of
|
||||
such an offset cannot lead to accessing memory outside of the
|
||||
dictionary buffer, and clients which don't want to leak unrelated
|
||||
information, should explicitly initialize dictionary buffer passed
|
||||
to uzlib. */
|
||||
|
||||
d->lz_off = d->dict_idx - offs;
|
||||
if (d->lz_off < 0) {
|
||||
d->lz_off = (int) (d->dest - d->dict_ring) - (int) offs;
|
||||
if (d->lz_off < 0)
|
||||
d->lz_off += d->dict_size;
|
||||
}
|
||||
}
|
||||
|
||||
/* copy next byte from dict substring */
|
||||
TINF_PUT(d, d->dict_ring[d->lz_off]);
|
||||
if ((unsigned) ++d->lz_off == d->dict_size) {
|
||||
d->lz_off = 0;
|
||||
}
|
||||
d->curlen--;
|
||||
return TINF_OK;
|
||||
}
|
||||
|
||||
@@ -388,30 +493,22 @@ static int tinf_inflate_uncompressed_block(TINF_DATA *d) {
|
||||
if (d->curlen == 0) {
|
||||
unsigned int length, invlength;
|
||||
|
||||
/* get length */
|
||||
length = uzlib_get_byte(d);
|
||||
length += 256 * uzlib_get_byte(d);
|
||||
/* get one's complement of length */
|
||||
invlength = uzlib_get_byte(d);
|
||||
invlength += 256 * uzlib_get_byte(d);
|
||||
/* check length */
|
||||
/* the block starts on a byte boundary */
|
||||
tinf_take(d, d->bitcount & 7);
|
||||
length = tinf_read_bits(d, 16, 0);
|
||||
invlength = tinf_read_bits(d, 16, 0);
|
||||
if (length != (~invlength & 0x0000ffff))
|
||||
return TINF_DATA_ERROR;
|
||||
|
||||
/* increment length to properly return TINF_DONE below, without
|
||||
producing data at the same time */
|
||||
d->curlen = length + 1;
|
||||
|
||||
/* make sure we start next block on a byte boundary */
|
||||
d->bitcount = 0;
|
||||
}
|
||||
|
||||
if (--d->curlen == 0) {
|
||||
if (--d->curlen == 0)
|
||||
return TINF_DONE;
|
||||
}
|
||||
|
||||
unsigned char c = uzlib_get_byte(d);
|
||||
TINF_PUT(d, c);
|
||||
/* read first: a read that needs input may flush the output up to dest */
|
||||
unsigned char c = tinf_read_bits(d, 8, 0);
|
||||
*d->dest++ = c;
|
||||
return TINF_OK;
|
||||
}
|
||||
|
||||
@@ -419,10 +516,10 @@ static int tinf_inflate_uncompressed_block(TINF_DATA *d) {
|
||||
* -- public functions -- *
|
||||
* ---------------------- */
|
||||
|
||||
/* initialize decompression structure */
|
||||
void ota_inflate_init(TINF_DATA *d, unsigned char *dict, unsigned int dict_len) {
|
||||
d->source = NULL;
|
||||
d->source_limit = NULL;
|
||||
d->dest = dict;
|
||||
d->tag = 0;
|
||||
d->eof = 0;
|
||||
d->bitcount = 0;
|
||||
@@ -431,68 +528,56 @@ void ota_inflate_init(TINF_DATA *d, unsigned char *dict, unsigned int dict_len)
|
||||
d->btype = -1;
|
||||
d->dict_size = dict_len;
|
||||
d->dict_ring = dict;
|
||||
d->dict_idx = 0;
|
||||
d->curlen = 0;
|
||||
d->ltree.trans = d->ltrans;
|
||||
d->ltree.size = TINF_ARRAY_SIZE(d->ltrans);
|
||||
d->ltree.fast = d->lfast;
|
||||
d->dtree.trans = d->dtrans;
|
||||
d->dtree.size = TINF_ARRAY_SIZE(d->dtrans);
|
||||
d->dtree.fast = d->dfast;
|
||||
}
|
||||
|
||||
/* inflate next output bytes from compressed stream */
|
||||
int ota_inflate(TINF_DATA *d) {
|
||||
/* the caller has taken the previous ringful */
|
||||
if (d->dest == d->dict_ring + d->dict_size)
|
||||
d->dest = d->dict_ring;
|
||||
|
||||
do {
|
||||
int res;
|
||||
|
||||
/* start a new block */
|
||||
if (d->btype == -1) {
|
||||
int old_btype;
|
||||
next_blk:
|
||||
old_btype = d->btype;
|
||||
/* read final block flag */
|
||||
d->bfinal = tinf_getbit(d);
|
||||
/* read block type (2 bits) */
|
||||
d->bfinal = tinf_read_bits(d, 1, 0);
|
||||
d->btype = tinf_read_bits(d, 2, 0);
|
||||
|
||||
if (d->btype == 1 && old_btype != 1) {
|
||||
/* build fixed huffman trees */
|
||||
tinf_build_fixed_trees(&d->ltree, &d->dtree);
|
||||
} else if (d->btype == 2) {
|
||||
/* decode trees from stream */
|
||||
res = tinf_decode_trees(d, &d->ltree, &d->dtree);
|
||||
if (res != TINF_OK) {
|
||||
if (res != TINF_OK)
|
||||
return res;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* process current block */
|
||||
switch (d->btype) {
|
||||
case 0:
|
||||
/* decompress uncompressed block */
|
||||
res = tinf_inflate_uncompressed_block(d);
|
||||
break;
|
||||
case 1:
|
||||
case 2:
|
||||
/* decompress block with fixed/dynamic huffman trees */
|
||||
/* trees were decoded previously, so it's the same routine for both */
|
||||
res = tinf_inflate_block_data(d, &d->ltree, &d->dtree);
|
||||
break;
|
||||
default:
|
||||
return TINF_DATA_ERROR;
|
||||
}
|
||||
|
||||
if (res == TINF_DONE && !d->bfinal) {
|
||||
/* the block has ended (without producing more data), but we
|
||||
can't return without data, so start procesing next block */
|
||||
if (res == TINF_DONE && !d->bfinal)
|
||||
goto next_blk;
|
||||
}
|
||||
|
||||
if (res != TINF_OK) {
|
||||
if (res != TINF_OK)
|
||||
return res;
|
||||
}
|
||||
|
||||
} while (d->dest < d->dest_limit);
|
||||
} while (d->dest < d->dict_ring + d->dict_size);
|
||||
|
||||
return TINF_OK;
|
||||
}
|
||||
@@ -16,10 +16,14 @@ enum OtaInflateResult {
|
||||
OTA_INFLATE_DICT_ERROR = -5,
|
||||
};
|
||||
|
||||
/* Code bits each lookup table resolves; longer codes take the canonical decode.
|
||||
The distance alphabet has 30 symbols, so its table is kept small */
|
||||
enum { OTA_INFLATE_LIT_FAST_BITS = 9, OTA_INFLATE_DIST_FAST_BITS = 6 };
|
||||
|
||||
struct OtaInflateTree {
|
||||
uint16_t table[16]; /* table of code length counts */
|
||||
uint16_t *trans; /* code -> symbol translation table, size entries */
|
||||
uint16_t size;
|
||||
uint16_t *fast; /* (len << 9) | symbol by the next input bits, 0 = longer code */
|
||||
uint16_t *trans; /* code -> symbol translation table */
|
||||
};
|
||||
|
||||
struct OtaInflateState {
|
||||
@@ -29,9 +33,8 @@ struct OtaInflateState {
|
||||
/* Called when source is exhausted; returns the next byte or -1 at EOF.
|
||||
It may refill source/source_limit for buffered operation. */
|
||||
int (*source_read_cb)(struct OtaInflateState *d);
|
||||
/* Output cursor and one past the end of the output buffer */
|
||||
/* Output cursor in dict_ring: each call fills the ring from here */
|
||||
unsigned char *dest;
|
||||
unsigned char *dest_limit;
|
||||
/* Ring window holding the last dict_size output bytes for back references */
|
||||
unsigned char *dict_ring;
|
||||
|
||||
@@ -39,9 +42,8 @@ struct OtaInflateState {
|
||||
unsigned int curlen;
|
||||
int lz_off;
|
||||
unsigned int dict_size;
|
||||
unsigned int dict_idx;
|
||||
|
||||
/* One word: btype is -1 between blocks, bitcount never exceeds 7 */
|
||||
/* btype is -1 between blocks; bitcount is the number of valid bits in tag (<= 32) */
|
||||
int8_t btype;
|
||||
uint8_t bfinal;
|
||||
uint8_t bitcount;
|
||||
@@ -51,13 +53,15 @@ struct OtaInflateState {
|
||||
struct OtaInflateTree dtree; /* dynamic distance tree */
|
||||
uint16_t ltrans[288];
|
||||
uint16_t dtrans[32]; /* the distance alphabet has 30 symbols, so the tree is kept small */
|
||||
uint16_t lfast[1 << OTA_INFLATE_LIT_FAST_BITS];
|
||||
uint16_t dfast[1 << OTA_INFLATE_DIST_FAST_BITS];
|
||||
};
|
||||
|
||||
/* dict must cover the encoder's window (its largest back reference) */
|
||||
void ota_inflate_init(struct OtaInflateState *d, unsigned char *dict, unsigned int dict_len);
|
||||
/* Fills dest up to dest_limit (OK) or to the end of the stream (DONE). dest may
|
||||
alias dict only if dest_limit - dest == dict_len and dest is reset to dict
|
||||
exactly when a call returns OK, so the ring index and dest stay in lockstep */
|
||||
/* Inflates into dict until it is full (OK) or the stream ends (DONE); the output
|
||||
is dict up to dest. The next call after OK starts dict over, so take its
|
||||
contents first */
|
||||
int ota_inflate(struct OtaInflateState *d);
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
@@ -65,16 +65,15 @@ ota::OTAResponseTypes ESPHomeOTAComponent::inflate_data_(uint8_t *in, size_t ima
|
||||
};
|
||||
|
||||
int res;
|
||||
session.flushed = 0;
|
||||
do {
|
||||
// The ring index wrapped to 0 exactly when the window filled
|
||||
session.dest = session.window;
|
||||
session.dest_limit = session.window + OTA_INFLATE_WINDOW_SIZE;
|
||||
session.flushed = 0;
|
||||
res = ota_inflate(&session);
|
||||
// A stored block keeps emitting zeros after a failed read, hence eof
|
||||
if (res < 0 || session.eof)
|
||||
break;
|
||||
session.error = this->inflate_flush_(session);
|
||||
// A full window is all in flash; the next call starts it over
|
||||
session.flushed = 0;
|
||||
} while (res != OTA_INFLATE_DONE && session.error == ota::OTA_RESPONSE_OK);
|
||||
|
||||
// Transport and flash failures are logged where they happen
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import climate_ir
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_MODEL
|
||||
from esphome.const import CONF_MODEL, CONF_USE_FAHRENHEIT
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import gree_ns
|
||||
@@ -19,6 +19,7 @@ MODELS = Model.options(
|
||||
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(GreeClimate).extend(
|
||||
{
|
||||
cv.Required(CONF_MODEL): cv.enum(MODELS, lower=True),
|
||||
cv.Optional(CONF_USE_FAHRENHEIT, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -26,5 +27,8 @@ CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(GreeClimate).extend(
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await climate_ir.new_climate_ir(config)
|
||||
cg.add(var.set_model(config[CONF_MODEL]))
|
||||
if config[CONF_USE_FAHRENHEIT]:
|
||||
cg.add_define("USE_GREE_FAHRENHEIT")
|
||||
cg.add(var.set_use_fahrenheit(True))
|
||||
if config[CONF_MODEL] == "yaw1f":
|
||||
cg.add_define("USE_GREE_YAW1F")
|
||||
@@ -1,4 +1,6 @@
|
||||
#include "gree.h"
|
||||
|
||||
#include <cmath>
|
||||
#include "esphome/components/remote_base/remote_base.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
@@ -34,6 +36,21 @@ void GreeClimate::set_mode_bit(uint8_t bit_mask, bool enabled) {
|
||||
this->transmit_state();
|
||||
}
|
||||
|
||||
#ifdef USE_GREE_FAHRENHEIT
|
||||
uint8_t fahrenheit_half_steps(float target_celsius) {
|
||||
if (!std::isfinite(target_celsius)) {
|
||||
return 0;
|
||||
}
|
||||
// Whole degrees shown at k = 0 and at the top of the range
|
||||
static constexpr int MIN_F = static_cast<int>(celsius_to_fahrenheit(GREE_TEMP_MIN));
|
||||
static constexpr int MAX_F = static_cast<int>(celsius_to_fahrenheit(GREE_TEMP_MAX));
|
||||
const int desired_f = static_cast<int>(roundf(clamp<float>(celsius_to_fahrenheit(target_celsius), MIN_F, MAX_F)));
|
||||
// The display at k is (10 * 32 + 18 * GREE_TEMP_MIN + 9 * k) / 10; pick the smallest k that reaches desired_f
|
||||
const int tenths_above_min = 10 * desired_f - (10 * 32 + 18 * GREE_TEMP_MIN);
|
||||
return static_cast<uint8_t>((tenths_above_min + 8) / 9);
|
||||
}
|
||||
#endif
|
||||
|
||||
void GreeClimate::transmit_state() {
|
||||
uint8_t remote_state[8] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00};
|
||||
const Model model = this->model_;
|
||||
@@ -41,7 +58,12 @@ void GreeClimate::transmit_state() {
|
||||
const bool yag_layout = model == GREE_YAG || model == GREE_YAW1F;
|
||||
|
||||
remote_state[0] = this->fan_speed_() | this->operation_mode_();
|
||||
#ifdef USE_GREE_FAHRENHEIT
|
||||
const uint8_t half_steps = this->use_fahrenheit_ ? fahrenheit_half_steps(this->target_temperature) : 0;
|
||||
remote_state[1] = this->use_fahrenheit_ ? half_steps >> 1 : this->temperature_();
|
||||
#else
|
||||
remote_state[1] = this->temperature_();
|
||||
#endif
|
||||
|
||||
if (model == GREE_YAN) {
|
||||
remote_state[3] = 0x50; // bits 4..7 always 0101
|
||||
@@ -107,16 +129,15 @@ void GreeClimate::transmit_state() {
|
||||
remote_state[0] |= GREE_PRESET_SLEEP_BIT;
|
||||
}
|
||||
|
||||
// Calculate the checksum
|
||||
if (model == GREE_YAN) {
|
||||
remote_state[7] = ((remote_state[0] << 4) + (remote_state[1] << 4) + 0xC0);
|
||||
} else {
|
||||
remote_state[7] =
|
||||
((((remote_state[0] & 0x0F) + (remote_state[1] & 0x0F) + (remote_state[2] & 0x0F) + (remote_state[3] & 0x0F) +
|
||||
((remote_state[4] & 0xF0) >> 4) + ((remote_state[5] & 0xF0) >> 4) + ((remote_state[6] & 0xF0) >> 4) + 0x0A) &
|
||||
0x0F)
|
||||
<< 4);
|
||||
#ifdef USE_GREE_FAHRENHEIT
|
||||
if (this->use_fahrenheit_) {
|
||||
remote_state[3] |= GREE_FAHRENHEIT_BIT;
|
||||
if (half_steps & 1)
|
||||
remote_state[3] |= GREE_TEMP_HALF_DEGREE_BIT;
|
||||
}
|
||||
#endif
|
||||
|
||||
remote_state[7] = frame_checksum(remote_state);
|
||||
|
||||
auto transmit = this->transmitter_->transmit();
|
||||
auto *data = transmit.get_data();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/climate_ir/climate_ir.h"
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
namespace esphome::gree {
|
||||
|
||||
@@ -90,6 +91,26 @@ static constexpr uint8_t GREE_MODE_BIT_LIGHT = 0x20;
|
||||
static constexpr uint8_t GREE_MODE_BIT_HEALTH = 0x40;
|
||||
static constexpr uint8_t GREE_MODE_BIT_XFAN = 0x80;
|
||||
|
||||
// Byte 3 Fahrenheit bits
|
||||
static constexpr uint8_t GREE_FAHRENHEIT_BIT = 0x08;
|
||||
static constexpr uint8_t GREE_TEMP_HALF_DEGREE_BIT = 0x04; // Half a degree Celsius added to the temperature
|
||||
|
||||
/// Checksum of a state frame: the low nibbles of bytes 0-3 plus the high nibbles of bytes 4-6 plus 0x0A, in the high
|
||||
/// nibble.
|
||||
inline uint8_t frame_checksum(const uint8_t *remote_state) {
|
||||
return (
|
||||
(((remote_state[0] & 0x0F) + (remote_state[1] & 0x0F) + (remote_state[2] & 0x0F) + (remote_state[3] & 0x0F) +
|
||||
((remote_state[4] & 0xF0) >> 4) + ((remote_state[5] & 0xF0) >> 4) + ((remote_state[6] & 0xF0) >> 4) + 0x0A) &
|
||||
0x0F)
|
||||
<< 4);
|
||||
}
|
||||
|
||||
#ifdef USE_GREE_FAHRENHEIT
|
||||
/// Return k = 2 * temp_field + half_degree_bit (0..28) for a Celsius target. The unit displays
|
||||
/// trunc(celsius_to_fahrenheit(GREE_TEMP_MIN + k / 2)), so k is the smallest value that shows the target in F.
|
||||
uint8_t fahrenheit_half_steps(float target_celsius);
|
||||
#endif
|
||||
|
||||
// Model codes
|
||||
enum Model : uint8_t { GREE_GENERIC, GREE_YAN, GREE_YAA, GREE_YAC, GREE_YAC1FB9, GREE_YX1FF, GREE_YAG, GREE_YAW1F };
|
||||
|
||||
@@ -103,6 +124,9 @@ class GreeClimate final : public climate_ir::ClimateIR {
|
||||
climate::CLIMATE_SWING_HORIZONTAL, climate::CLIMATE_SWING_BOTH}) {}
|
||||
|
||||
void set_model(Model model);
|
||||
#ifdef USE_GREE_FAHRENHEIT
|
||||
void set_use_fahrenheit(bool use_fahrenheit) { this->use_fahrenheit_ = use_fahrenheit; }
|
||||
#endif
|
||||
void set_mode_bit(uint8_t bit_mask, bool enabled);
|
||||
|
||||
protected:
|
||||
@@ -118,6 +142,9 @@ class GreeClimate final : public climate_ir::ClimateIR {
|
||||
|
||||
Model model_{};
|
||||
uint8_t mode_bits_{GREE_MODE_BIT_LIGHT}; // The display light is on until a switch says otherwise
|
||||
#ifdef USE_GREE_FAHRENHEIT
|
||||
bool use_fahrenheit_{false};
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace esphome::gree
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "image.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
@@ -165,6 +166,21 @@ Color Image::get_pixel(int x, int y, const Color color_on, const Color color_off
|
||||
}
|
||||
#ifdef USE_LVGL
|
||||
lv_image_dsc_t *Image::get_lv_image_dsc() {
|
||||
// No pixels yet. A widget keeps this descriptor and reads it on every draw, and LVGL warns on every draw of an image
|
||||
// without data, so give it a transparent pixel instead
|
||||
static constexpr uint8_t TRANSPARENT_PIXEL = 0;
|
||||
if (this->data_start_ == nullptr) {
|
||||
if (this->dsc_.data != &TRANSPARENT_PIXEL) {
|
||||
memset(&this->dsc_, 0, sizeof(this->dsc_));
|
||||
this->dsc_.header.cf = LV_COLOR_FORMAT_A8;
|
||||
this->dsc_.header.w = 1;
|
||||
this->dsc_.header.h = 1;
|
||||
this->dsc_.header.stride = 1;
|
||||
this->dsc_.data_size = 1;
|
||||
this->dsc_.data = &TRANSPARENT_PIXEL;
|
||||
}
|
||||
return &this->dsc_;
|
||||
}
|
||||
// lazily construct lvgl image_dsc.
|
||||
if (this->dsc_.data != this->data_start_) {
|
||||
this->dsc_.data = this->data_start_;
|
||||
|
||||
@@ -43,7 +43,12 @@ class Image : public display::BaseImage {
|
||||
bool has_transparency() const { return this->transparency_ != TRANSPARENCY_OPAQUE; }
|
||||
|
||||
#ifdef USE_LVGL
|
||||
lv_image_dsc_t *get_lv_image_dsc();
|
||||
virtual lv_image_dsc_t *get_lv_image_dsc();
|
||||
#endif
|
||||
#ifdef USE_LVGL_IMAGE
|
||||
/// Called when an LVGL image widget is given this image. An image whose size can change keeps the widget, so it can
|
||||
/// give the widget its source again when the size changes: the widget keeps the size it read from the descriptor.
|
||||
virtual void add_lvgl_widget(lv_obj_t *obj) {}
|
||||
#endif
|
||||
protected:
|
||||
/// Per-pixel draw of the clipped range, rows (true) or columns (false) outer.
|
||||
|
||||
@@ -274,9 +274,7 @@ def _model_schema(config: ConfigType) -> cv.Schema:
|
||||
cv.Optional(CONF_UPDATE_MODE): update_mode,
|
||||
# One or more GPIOs driven high during setup to power on the panel
|
||||
# (e.g. board power-enable rails), before reset and init.
|
||||
cv.Optional(
|
||||
CONF_ENABLE_PIN, default=model.get_default(CONF_ENABLE_PIN, [])
|
||||
): cv.ensure_list(pins.gpio_output_pin_schema),
|
||||
**display.enable_pin_schema(model, config),
|
||||
cv.Optional(CONF_RESET_DURATION): cv.All(
|
||||
cv.positive_time_period_milliseconds,
|
||||
cv.Range(max=core.TimePeriod(milliseconds=500)),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import logging
|
||||
|
||||
from esphome import automation, pins
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
@@ -15,6 +17,8 @@ from esphome.core import CORE
|
||||
from esphome.cpp_helpers import gpio_pin_expression
|
||||
from esphome.types import ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CODEOWNERS = ["@max246"]
|
||||
DOMAIN = "lightwaverf"
|
||||
|
||||
@@ -35,10 +39,20 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
).extend(cv.polling_component_schema("1s"))
|
||||
|
||||
|
||||
def _drop_unused_name(config: ConfigType) -> ConfigType:
|
||||
# Remove before 2027.5.0
|
||||
if config.pop(CONF_NAME, None) is not None:
|
||||
_LOGGER.warning(
|
||||
"[lightwaverf] 'name' on lightwaverf.send_raw has no effect; remove it. "
|
||||
"Removed in 2027.5.0"
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
LIGHTWAVE_SEND_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(LIGHTWAVERFComponent),
|
||||
cv.Required(CONF_NAME): cv.string,
|
||||
cv.Optional(CONF_NAME): cv.string,
|
||||
cv.Required(CONF_CODE): cv.All(
|
||||
[cv.Any(cv.hex_uint8_t)],
|
||||
cv.Length(min=10),
|
||||
@@ -47,7 +61,7 @@ LIGHTWAVE_SEND_SCHEMA = cv.Schema(
|
||||
cv.Optional(CONF_INVERTED, default=False): cv.boolean,
|
||||
cv.Optional(CONF_PULSE_LENGTH, default=330): cv.int_,
|
||||
}
|
||||
)
|
||||
).add_extra(_drop_unused_name)
|
||||
|
||||
|
||||
automation.register_apply_action(
|
||||
|
||||
@@ -82,7 +82,10 @@ inline void lv_style_set_text_font(lv_style_t *style, const font::Font *font) {
|
||||
#ifdef USE_IMAGE
|
||||
#ifdef USE_LVGL_IMAGE
|
||||
// Shortcut / overload, so that the source of an image widget can easily be updated from within a lambda.
|
||||
inline void lv_image_set_src(lv_obj_t *obj, image::Image *image) { ::lv_image_set_src(obj, image->get_lv_image_dsc()); }
|
||||
inline void lv_image_set_src(lv_obj_t *obj, image::Image *image) {
|
||||
::lv_image_set_src(obj, image->get_lv_image_dsc());
|
||||
image->add_lvgl_widget(obj);
|
||||
}
|
||||
|
||||
inline void lv_obj_set_style_bitmap_mask_src(lv_obj_t *obj, image::Image *image, lv_style_selector_t selector) {
|
||||
::lv_obj_set_style_bitmap_mask_src(obj, image->get_lv_image_dsc(), selector);
|
||||
|
||||
@@ -28,6 +28,7 @@ from esphome.const import (
|
||||
CONF_X,
|
||||
CONF_Y,
|
||||
)
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.cpp_types import FixedVector
|
||||
|
||||
from ..automation import action_to_code
|
||||
@@ -41,6 +42,7 @@ from ..defines import (
|
||||
CONF_RADIUS,
|
||||
CONF_SRC,
|
||||
CONF_START_ANGLE,
|
||||
LValidator,
|
||||
addr,
|
||||
literal,
|
||||
)
|
||||
@@ -57,7 +59,7 @@ from ..lv_validation import (
|
||||
)
|
||||
from ..lvcode import LocalVariable, lv, lv_assign, lv_expr
|
||||
from ..schemas import STYLE_PROPS, TEXT_SCHEMA, point_schema, remap_property
|
||||
from ..types import LvType, ObjUpdateAction, lv_point_precise_t
|
||||
from ..types import LvType, ObjUpdateAction, lv_gradient_t, lv_point_precise_t
|
||||
from . import Widget, WidgetType, get_widgets
|
||||
from .img import CONF_IMAGE
|
||||
|
||||
@@ -221,7 +223,6 @@ RECT_PROPS = {
|
||||
"radius",
|
||||
"bg_opa",
|
||||
"bg_color",
|
||||
"bg_grad",
|
||||
"border_color",
|
||||
"border_width",
|
||||
"border_opa",
|
||||
@@ -239,6 +240,30 @@ RECT_PROPS = {
|
||||
"shadow_opa",
|
||||
)
|
||||
}
|
||||
RECT_PROPS.update(
|
||||
bg_grad=LValidator(
|
||||
cv.use_id(lv_gradient_t), lv_gradient_t, retmapper=lambda v: MockObj(f"*{v}")
|
||||
)
|
||||
)
|
||||
RECT_SCHEMA = {
|
||||
**{cv.Optional(prop): validator for prop, validator in RECT_PROPS.items()},
|
||||
cv.Optional(CONF_OPA): cv.invalid(
|
||||
"Rectangles have no single opacity; use bg_opa, border_opa, outline_opa or shadow_opa"
|
||||
),
|
||||
}
|
||||
POLYGON_PROPS = {
|
||||
p: RECT_PROPS[p] for p in ("border_color", "border_width", "border_opa")
|
||||
}
|
||||
POLYGON_SCHEMA = {
|
||||
**{cv.Optional(prop): validator for prop, validator in POLYGON_PROPS.items()},
|
||||
**{
|
||||
cv.Optional(prop): cv.invalid(
|
||||
"Polygons are drawn as an outline; use border_color, border_width and border_opa"
|
||||
)
|
||||
for prop in (*RECT_PROPS, CONF_OPA)
|
||||
if prop not in POLYGON_PROPS
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _draw_line(layer, dsc, points):
|
||||
@@ -264,10 +289,10 @@ def _draw_line(layer, dsc, points):
|
||||
ObjUpdateAction,
|
||||
cv.Schema(
|
||||
{
|
||||
**DRAW_OPA_SCHEMA,
|
||||
**DRAW_SCHEMA,
|
||||
cv.Required(CONF_WIDTH): cv.templatable(cv.int_),
|
||||
cv.Required(CONF_HEIGHT): cv.templatable(cv.int_),
|
||||
**{cv.Optional(prop): STYLE_PROPS[prop] for prop in RECT_PROPS},
|
||||
**RECT_SCHEMA,
|
||||
}
|
||||
),
|
||||
synchronous=True,
|
||||
@@ -423,8 +448,8 @@ async def canvas_draw_image(config, action_id, template_arg, args):
|
||||
LINE_PROPS = {
|
||||
"width": STYLE_PROPS["line_width"],
|
||||
"color": STYLE_PROPS["line_color"],
|
||||
"dash-width": STYLE_PROPS["line_dash_width"],
|
||||
"dash-gap": STYLE_PROPS["line_dash_gap"],
|
||||
"dash_width": STYLE_PROPS["line_dash_width"],
|
||||
"dash_gap": STYLE_PROPS["line_dash_gap"],
|
||||
"round_start": lv_bool,
|
||||
"round_end": lv_bool,
|
||||
}
|
||||
@@ -474,7 +499,7 @@ async def canvas_draw_line(config, action_id, template_arg, args):
|
||||
{
|
||||
cv.GenerateID(CONF_ID): cv.use_id(lv_canvas_t),
|
||||
cv.Required(CONF_POINTS): cv.ensure_list(point_schema),
|
||||
**{cv.Optional(prop): STYLE_PROPS[prop] for prop in RECT_PROPS},
|
||||
**POLYGON_SCHEMA,
|
||||
},
|
||||
).add_extra(_validate_points),
|
||||
synchronous=True,
|
||||
@@ -503,7 +528,7 @@ async def canvas_draw_polygon(config, action_id, template_arg, args):
|
||||
_draw_line(layer, line_dsc, points)
|
||||
|
||||
return await draw_to_code(
|
||||
config, "rect", RECT_PROPS, do_draw_polygon, action_id, template_arg, args
|
||||
config, "rect", POLYGON_PROPS, do_draw_polygon, action_id, template_arg, args
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ from esphome.const import (
|
||||
CONF_WIDTH,
|
||||
CONF_X,
|
||||
)
|
||||
from esphome.core import ID
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.cpp_types import nullptr
|
||||
|
||||
@@ -64,7 +65,7 @@ from ..lv_validation import (
|
||||
)
|
||||
from ..lvcode import LambdaContext, LocalVariable, lv, lv_add, lv_expr, lv_obj
|
||||
from ..schemas import STATE_SCHEMA
|
||||
from ..styles import LVStyle
|
||||
from ..styles import LVStyle, create_style
|
||||
from ..types import (
|
||||
LV_EVENT,
|
||||
LvCompound,
|
||||
@@ -100,6 +101,12 @@ CONF_SCALES = "scales"
|
||||
CONF_STRIDE = "stride"
|
||||
CONF_TICK_STYLE = "tick_style"
|
||||
|
||||
|
||||
def _arc_style_name(indicator_id: str) -> str:
|
||||
"""Name of the style variable generated for an arc indicator."""
|
||||
return f"meter_arc_{indicator_id}_style"
|
||||
|
||||
|
||||
# LVGL 9.4 Migration: Use scale widget instead of removed meter widget
|
||||
#
|
||||
# The lv_meter widget was removed in LVGL 9.4 and replaced with the more
|
||||
@@ -423,11 +430,9 @@ class MeterType(WidgetType):
|
||||
get_warnings().add(
|
||||
"The 'r_mod' indicator property is not supported in LVGL 9.x and will be ignored."
|
||||
)
|
||||
arc_style = LVStyle(f"meter_arc_{iid.id}", props)
|
||||
style_var = await create_style(_arc_style_name(iid.id), props)
|
||||
tvar = cg.Pvariable(iid, lv_expr.scale_add_section(scale_var))
|
||||
lv.scale_section_set_style(
|
||||
tvar, LV_PART.MAIN, await arc_style.get_var()
|
||||
)
|
||||
lv.scale_section_set_style(tvar, LV_PART.MAIN, style_var)
|
||||
lw = Widget.create(iid, tvar, arc_indicator_type)
|
||||
lw.parent = scale_var
|
||||
await set_indicator_values(scale_var, lw, v)
|
||||
@@ -610,6 +615,15 @@ async def indicator_update_to_code(config, action_id, template_arg, args):
|
||||
|
||||
async def set_value(w: Widget):
|
||||
await set_indicator_values(w.parent, w, config)
|
||||
if (opa := config.get(CONF_OPA)) is None:
|
||||
return
|
||||
opa = await opacity.process(opa)
|
||||
if w.type is arc_indicator_type:
|
||||
style = await cg.get_variable(ID(_arc_style_name(str(w.var))))
|
||||
lv.style_set_arc_opa(style, opa)
|
||||
lv_obj.invalidate(w.parent)
|
||||
else:
|
||||
lv_obj.set_style_opa(w.obj, opa, LV_PART.MAIN)
|
||||
|
||||
return await action_to_code(widget, set_value, action_id, template_arg, args)
|
||||
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
|
||||
from collections.abc import Callable
|
||||
import functools
|
||||
import importlib.util
|
||||
from typing import Any, Self
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from esphome import pins
|
||||
from esphome.components.const import CONF_COLOR_DEPTH
|
||||
from esphome.components.display import CONF_SHOW_TEST_CARD, display_ns
|
||||
import esphome.config_validation as cv
|
||||
@@ -23,6 +25,7 @@ from esphome.const import (
|
||||
CONF_MIRROR_X,
|
||||
CONF_MIRROR_Y,
|
||||
CONF_MODEL,
|
||||
CONF_NUMBER,
|
||||
CONF_OFFSET_HEIGHT,
|
||||
CONF_OFFSET_WIDTH,
|
||||
CONF_PAGES,
|
||||
@@ -306,6 +309,57 @@ def model_schema_extractor(
|
||||
return decorate
|
||||
|
||||
|
||||
def _is_pin_expander(key: str) -> bool:
|
||||
"""Check if a pin config key names a component that provides pins, e.g. an I/O expander."""
|
||||
from esphome import loader
|
||||
|
||||
if key not in pins.PIN_SCHEMA_REGISTRY:
|
||||
if importlib.util.find_spec(f"esphome.components.{key}") is None:
|
||||
return False
|
||||
# Loading the component registers its pin schema. If it fails to load (the
|
||||
# loader logs why), still require it rather than quietly dropping it.
|
||||
if loader.get_component(key) is None:
|
||||
return True
|
||||
return key in pins.PIN_SCHEMA_REGISTRY
|
||||
|
||||
|
||||
def _pin_expanders(value: Any) -> set[str]:
|
||||
"""Find the pin expander components used by any pin configs in a model's defaults."""
|
||||
if isinstance(value, (list, tuple)):
|
||||
return set().union(*(_pin_expanders(v) for v in value))
|
||||
if not isinstance(value, dict):
|
||||
return set()
|
||||
found = set().union(*(_pin_expanders(v) for v in value.values()))
|
||||
if CONF_NUMBER in value:
|
||||
found.update(k for k in value if k != CONF_NUMBER and _is_pin_expander(k))
|
||||
return found
|
||||
|
||||
|
||||
def check_model_requirements(name: str, defaults: dict[str, Any]) -> None:
|
||||
"""
|
||||
Raise a friendly error if any component a display model requires is not configured.
|
||||
These are the components in its ``requires`` default plus any pin expander used by its
|
||||
default pins, which is needed even if the user config overrides those pins.
|
||||
|
||||
This runs during schema validation (before ID references are resolved) so that a
|
||||
model whose default pins live on a pin expander reports the missing expander clearly
|
||||
instead of a cryptic "Couldn't find ID" from the unresolved pin reference.
|
||||
"""
|
||||
# ``raw_config`` is populated before any component schema runs during a real
|
||||
# validation, so presence of a required component is simply a top-level key.
|
||||
# When it is absent (e.g. a unit test that invokes the schema directly) there
|
||||
# is no config to check against, so skip.
|
||||
global_config = CORE.raw_config
|
||||
if global_config is None:
|
||||
return
|
||||
requirements = set(defaults.get("requires", ())) | _pin_expanders(defaults)
|
||||
if missing := {x for x in requirements if x not in global_config}:
|
||||
reqstr = ", ".join(f"'{x}'" for x in sorted(missing))
|
||||
raise cv.Invalid(
|
||||
f"{name} requires component{'s' if len(missing) > 1 else ''} {reqstr} to be configured"
|
||||
)
|
||||
|
||||
|
||||
class DriverChip:
|
||||
"""
|
||||
A class representing a MIPI DBI driver chip model.
|
||||
@@ -671,32 +725,14 @@ class DriverChip:
|
||||
|
||||
def check_requirements(self) -> None:
|
||||
"""
|
||||
Raise a friendly error if any component this model requires is not configured.
|
||||
|
||||
This runs during schema validation (before ID references are resolved) so that a
|
||||
model whose default pins live on a pin expander reports the missing expander clearly
|
||||
instead of a cryptic "Couldn't find ID" from the unresolved pin reference.
|
||||
|
||||
Check that the components this model needs are configured, see check_model_requirements().
|
||||
Also logs a warning if the model is deprecated.
|
||||
"""
|
||||
if deprecation_reason := self.get_default("deprecation_reason"):
|
||||
LOGGER.warning(
|
||||
"Display model %s is deprecated: %s", self.name, deprecation_reason
|
||||
)
|
||||
if requirements := self.get_default("requires", set()):
|
||||
# ``raw_config`` is populated before any component schema runs during a real
|
||||
# validation, so presence of a required component is simply a top-level key.
|
||||
# When it is absent (e.g. a unit test that invokes the schema directly) there
|
||||
# is no config to check against, so skip.
|
||||
global_config = CORE.raw_config
|
||||
if global_config is None:
|
||||
return
|
||||
missing = {x for x in requirements if x not in global_config}
|
||||
if missing:
|
||||
reqstr = ", ".join(f"'{x}'" for x in sorted(missing))
|
||||
raise cv.Invalid(
|
||||
f"{self.name} requires component{'s' if len(missing) > 1 else ''} {reqstr} to be configured"
|
||||
)
|
||||
check_model_requirements(self.name, self.defaults)
|
||||
|
||||
|
||||
def requires_buffer(config) -> bool:
|
||||
|
||||
@@ -85,7 +85,7 @@ COLOR_DEPTHS = {
|
||||
}
|
||||
|
||||
|
||||
def model_schema(config: ConfigType) -> cv.All:
|
||||
def model_schema(config: ConfigType) -> cv.Schema:
|
||||
model = MODELS[config[CONF_MODEL].upper()]
|
||||
transform = model.transform_schema()
|
||||
# CUSTOM model will need to provide a custom init sequence
|
||||
@@ -97,16 +97,14 @@ def model_schema(config: ConfigType) -> cv.All:
|
||||
# Dimensions are optional if the model has a default width
|
||||
cv_dimensions = cv.Optional if model.get_default(CONF_WIDTH) else cv.Required
|
||||
pixel_modes = (PIXEL_MODE_16BIT, PIXEL_MODE_24BIT, "16", "24")
|
||||
schema = display.FULL_DISPLAY_SCHEMA.extend(
|
||||
return display.FULL_DISPLAY_SCHEMA.extend(
|
||||
{
|
||||
model.option(CONF_RESET_PIN, cv.UNDEFINED): pins.gpio_output_pin_schema,
|
||||
cv.GenerateID(): cv.declare_id(MipiDsi),
|
||||
cv_dimensions(CONF_DIMENSIONS): dimension_schema(
|
||||
model.get_default(CONF_DRAW_ROUNDING, 1)
|
||||
),
|
||||
model.option(CONF_ENABLE_PIN, cv.UNDEFINED): cv.ensure_list(
|
||||
pins.gpio_output_pin_schema
|
||||
),
|
||||
**display.enable_pin_schema(model, config),
|
||||
model.option(CONF_COLOR_ORDER, MODE_BGR): cv.enum(COLOR_ORDERS, upper=True),
|
||||
model.option(CONF_DRAW_ROUNDING, 2): power_of_two,
|
||||
model.option(CONF_PIXEL_MODE, PIXEL_MODE_16BIT): cv.one_of(
|
||||
@@ -140,11 +138,6 @@ def model_schema(config: ConfigType) -> cv.All:
|
||||
model.option(CONF_VSYNC_FRONT_PORCH): cv.int_,
|
||||
}
|
||||
)
|
||||
return cv.All(
|
||||
schema,
|
||||
cv.only_on_esp32,
|
||||
only_on_variant(supported=[VARIANT_ESP32P4]),
|
||||
)
|
||||
|
||||
|
||||
@model_schema_extractor(MODELS, model_schema)
|
||||
@@ -155,9 +148,13 @@ def _config_schema(config: ConfigType) -> ConfigType:
|
||||
},
|
||||
extra=cv.ALLOW_EXTRA,
|
||||
)(config)
|
||||
config = model_schema(config)(config)
|
||||
config = cv.All(cv.only_on_esp32, only_on_variant(supported=[VARIANT_ESP32P4]))(
|
||||
config
|
||||
)
|
||||
# Check before the schema, which would fail on default pins of a missing expander
|
||||
model = MODELS[config[CONF_MODEL].upper()]
|
||||
model.check_requirements()
|
||||
config = model_schema(config)(config)
|
||||
width, height, _offset_width, _offset_height, _pad_width, _pad_height = (
|
||||
model.get_dimensions(config)
|
||||
)
|
||||
|
||||
@@ -14,7 +14,7 @@ DsiDriverChip(
|
||||
pclk_frequency="60MHz",
|
||||
lane_bit_rate="730Mbps",
|
||||
color_order="RGB",
|
||||
requires={"psram", "pi4ioe5v6408"},
|
||||
requires={"psram"},
|
||||
reset_pin={"pi4ioe5v6408": {"address": 0x43}, "number": 4},
|
||||
initsequence=[
|
||||
(0xFF, 0x98, 0x81, 0x01), # Select Page 1
|
||||
@@ -69,7 +69,7 @@ TAB5_ST7123 = DsiDriverChip(
|
||||
pclk_frequency="80MHz",
|
||||
lane_bit_rate="960Mbps",
|
||||
color_order="RGB",
|
||||
requires={"psram", "pi4ioe5v6408"},
|
||||
requires={"psram"},
|
||||
reset_pin={"pi4ioe5v6408": {"address": 0x43}, "number": 4},
|
||||
initsequence=[
|
||||
(0x01,),
|
||||
@@ -122,7 +122,7 @@ DsiDriverChip(
|
||||
pclk_frequency="70MHz",
|
||||
lane_bit_rate="965Mbps",
|
||||
color_order="RGB",
|
||||
requires={"psram", "pi4ioe5v6408"},
|
||||
requires={"psram"},
|
||||
reset_pin={"pi4ioe5v6408": {"address": 0x43}, "number": 4},
|
||||
initsequence=[
|
||||
(0x01,),
|
||||
|
||||
@@ -17,7 +17,7 @@ DsiDriverChip(
|
||||
color_order="RGB",
|
||||
enable_pin=[{"xl9535": None, "number": 0}, {"xl9535": None, "number": 7}],
|
||||
reset_pin={"xl9535": None, "number": 2},
|
||||
requires={"psram", "xl9535"},
|
||||
requires={"psram"},
|
||||
initsequence=(
|
||||
(0xE0, 0x00),
|
||||
(0xE1, 0x93),
|
||||
|
||||
@@ -154,9 +154,7 @@ def model_schema(config: ConfigType) -> cv.Schema:
|
||||
cv_dimensions(CONF_DIMENSIONS): dimension_schema(
|
||||
model.get_default(CONF_DRAW_ROUNDING, 1)
|
||||
),
|
||||
model.option(CONF_ENABLE_PIN, cv.UNDEFINED): cv.ensure_list(
|
||||
pins.gpio_output_pin_schema
|
||||
),
|
||||
**display.enable_pin_schema(model, config),
|
||||
model.option(CONF_COLOR_ORDER, MODE_RGB): cv.enum(COLOR_ORDERS, upper=True),
|
||||
model.option(CONF_DRAW_ROUNDING, 2): power_of_two,
|
||||
model.option(CONF_PIXEL_MODE, PIXEL_MODE_16BIT): cv.one_of(
|
||||
@@ -226,14 +224,14 @@ def _config_schema(config: ConfigType) -> ConfigType:
|
||||
},
|
||||
extra=cv.ALLOW_EXTRA,
|
||||
)(config)
|
||||
schema = model_schema(config)
|
||||
config = cv.All(
|
||||
schema,
|
||||
cv.only_on_esp32,
|
||||
only_on_variant(supported=[VARIANT_ESP32S3, VARIANT_ESP32P4, VARIANT_ESP32S31]),
|
||||
)(config)
|
||||
# Check before the schema, which would fail on default pins of a missing expander
|
||||
model = MODELS[config[CONF_MODEL].upper()]
|
||||
model.check_requirements()
|
||||
config = model_schema(config)(config)
|
||||
width, height, _offset_width, _offset_height, _pad_width, _pad_height = (
|
||||
model.get_dimensions(config)
|
||||
)
|
||||
|
||||
@@ -9,7 +9,7 @@ ST7701S(
|
||||
spi_mode="MODE3",
|
||||
cs_pin={"xl9535": None, "number": 17},
|
||||
reset_pin={"xl9535": None, "number": 5},
|
||||
requires={"psram", "xl9535"},
|
||||
requires={"psram"},
|
||||
hsync_pin=39,
|
||||
vsync_pin=40,
|
||||
pclk_pin=41,
|
||||
@@ -57,7 +57,7 @@ t_rgb = ST7701S(
|
||||
invert_colors=False,
|
||||
spi_mode="MODE3",
|
||||
cs_pin={"xl9535": None, "number": 3},
|
||||
requires={"psram", "xl9535"},
|
||||
requires={"psram"},
|
||||
de_pin=45,
|
||||
hsync_pin=47,
|
||||
vsync_pin=41,
|
||||
|
||||
@@ -115,7 +115,7 @@ st7701s.extend(
|
||||
vsync_pulse_width=8,
|
||||
vsync_back_porch=20,
|
||||
cs_pin={"pca9554": None, "number": 4},
|
||||
requires={"psram", "pca9554"},
|
||||
requires={"psram"},
|
||||
de_pin=18,
|
||||
hsync_pin=16,
|
||||
vsync_pin=17,
|
||||
|
||||
@@ -12,7 +12,7 @@ wave_4_3 = RgbDriverChip(
|
||||
pclk_frequency="16MHz",
|
||||
reset_pin={"ch422g": None, "number": 3},
|
||||
enable_pin={"ch422g": None, "number": 2},
|
||||
requires={"psram", "ch422g"},
|
||||
requires={"psram"},
|
||||
de_pin=5,
|
||||
hsync_pin={"number": 46, "ignore_strapping_warning": True},
|
||||
vsync_pin={"number": 3, "ignore_strapping_warning": True},
|
||||
|
||||
@@ -197,9 +197,7 @@ def model_schema(config: ConfigType) -> cv.All | cv.Schema:
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(MipiSpi),
|
||||
cv_dimensions(CONF_DIMENSIONS): dimension_schema(1),
|
||||
model.option(CONF_ENABLE_PIN, cv.UNDEFINED): cv.ensure_list(
|
||||
pins.gpio_output_pin_schema
|
||||
),
|
||||
**display.enable_pin_schema(model, config),
|
||||
model.option(CONF_COLOR_ORDER, MODE_BGR): cv.enum(
|
||||
COLOR_ORDERS, upper=True
|
||||
),
|
||||
|
||||
@@ -22,7 +22,6 @@ DriverChip(
|
||||
transforms={CONF_MIRROR_X, CONF_MIRROR_Y},
|
||||
use_axis_flips=True,
|
||||
reset_delay=20,
|
||||
requires={"pca9554"},
|
||||
initsequence=(
|
||||
delay(100),
|
||||
(0xFF, 0x20, 0x10, 0x10),
|
||||
|
||||
@@ -69,8 +69,9 @@ PackedBits = modbus.modbus_ns.class_("PackedBits")
|
||||
# The exception code passed to on_error handlers.
|
||||
ExceptionCode = modbus.modbus_ns.enum("ExceptionCode")
|
||||
|
||||
# Lambda argument types for the reply handlers: the device address the send targeted, and the
|
||||
# request/response PDUs (function code + data). The spans are only valid for the duration of the handler.
|
||||
# Lambda arguments: the shared handlers and send's on_response get the address of their own request first,
|
||||
# then the request/response PDUs (function code + data). The spans are only valid for the duration of the handler.
|
||||
_ADDRESS_ARG = (cg.uint8, "address")
|
||||
_PDU_SPAN = cg.std_span.template(cg.uint8.operator("const"))
|
||||
|
||||
# The pdu lambda's return type: a stack-allocated StaticVector capped at the Modbus PDU limit
|
||||
@@ -135,11 +136,11 @@ _ACTION_BASE_SCHEMA = cv.Schema(
|
||||
cv.GenerateID(modbus.CONF_MODBUS_ID): cv.use_id(modbus.ModbusClient),
|
||||
cv.Required(CONF_ADDRESS): cv.templatable(cv.hex_uint8_t),
|
||||
# Optional handlers. on_sent fires when the frame reaches the wire; the reply handlers arrive
|
||||
# later (fire-and-continue), so all run with the request/reply available - not the outer
|
||||
# automation's variables.
|
||||
# later (fire-and-continue), so all run with the address and request/reply available - not the
|
||||
# outer automation's variables.
|
||||
cv.Optional(CONF_ON_SENT): _handler_schema(),
|
||||
cv.Optional(CONF_ON_ERROR): _handler_schema(),
|
||||
# on_no_response takes either a returning lambda (`!lambda "return <bool>;"`, gets `request`,
|
||||
# on_no_response takes either a returning lambda (`!lambda "return <bool>;"`, gets `address`/`request`,
|
||||
# returns true to have the hub retry the frame) OR a `then:` automation of actions; the automation
|
||||
# form may also carry an optional `retry:` returning lambda to run actions AND decide the retry.
|
||||
cv.Optional(CONF_ON_NO_RESPONSE): cv.All(
|
||||
@@ -199,7 +200,7 @@ async def register_client_action(
|
||||
)
|
||||
if sent_conf := config.get(CONF_ON_SENT):
|
||||
await automation.build_automation(
|
||||
var.get_sent_trigger(), [(_PDU_SPAN, "request")], sent_conf
|
||||
var.get_sent_trigger(), [_ADDRESS_ARG, (_PDU_SPAN, "request")], sent_conf
|
||||
)
|
||||
if response_conf := config.get(CONF_ON_RESPONSE):
|
||||
await automation.build_automation(
|
||||
@@ -217,7 +218,7 @@ async def register_client_action(
|
||||
if error_conf := config.get(CONF_ON_ERROR):
|
||||
await automation.build_automation(
|
||||
var.get_error_trigger(),
|
||||
[(_PDU_SPAN, "request"), (ExceptionCode, "exception_code")],
|
||||
[_ADDRESS_ARG, (_PDU_SPAN, "request"), (ExceptionCode, "exception_code")],
|
||||
error_conf,
|
||||
)
|
||||
if (no_response_conf := config.get(CONF_ON_NO_RESPONSE)) is not None:
|
||||
@@ -228,18 +229,20 @@ async def register_client_action(
|
||||
else:
|
||||
await automation.build_automation(
|
||||
var.get_no_response_trigger(),
|
||||
[(_PDU_SPAN, "request")],
|
||||
[_ADDRESS_ARG, (_PDU_SPAN, "request")],
|
||||
no_response_conf,
|
||||
)
|
||||
retry_conf = no_response_conf.get(CONF_RETRY)
|
||||
if retry_conf is not None:
|
||||
retry_lambda = await cg.process_lambda(
|
||||
retry_conf, [(_PDU_SPAN, "request")], return_type=cg.bool_
|
||||
retry_conf, [_ADDRESS_ARG, (_PDU_SPAN, "request")], return_type=cg.bool_
|
||||
)
|
||||
cg.add(var.set_retry(retry_lambda))
|
||||
if not_sent_conf := config.get(CONF_ON_NOT_SENT):
|
||||
await automation.build_automation(
|
||||
var.get_not_sent_trigger(), [(_PDU_SPAN, "request")], not_sent_conf
|
||||
var.get_not_sent_trigger(),
|
||||
[_ADDRESS_ARG, (_PDU_SPAN, "request")],
|
||||
not_sent_conf,
|
||||
)
|
||||
# Wire any command options the action's schema opted into (e.g. continuous on reads). Pass the
|
||||
# matching direction so a write action never generates a read option's setter.
|
||||
@@ -265,7 +268,7 @@ async def modbus_client_send_to_code(config, action_id, template_arg, args):
|
||||
var,
|
||||
config,
|
||||
args,
|
||||
[(_PDU_SPAN, "request"), (_PDU_SPAN, "response")],
|
||||
[_ADDRESS_ARG, (_PDU_SPAN, "request"), (_PDU_SPAN, "response")],
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -13,45 +13,51 @@ namespace esphome::modbus_client {
|
||||
/// the hub routes every reply (or its lack) straight back to the action that sent it, so there is no
|
||||
/// central client object and no request matching. The device address is templatable; it is stamped on the
|
||||
/// device at play() time; the hub routes each reply by device pointer, so a changed address never
|
||||
/// mis-routes an earlier reply. (The address is not passed to the reply triggers - under overlapping
|
||||
/// sends it could misreport, and the handler can recompute the expression it configured.)
|
||||
/// mis-routes an earlier reply. The handlers wired here and send's on_response get `address` first: the
|
||||
/// address of their own request, which the hub takes from its frame, so overlapping sends each report their own.
|
||||
template<typename... Ts> class ClientActionBase : public Action<Ts...>, public modbus::ModbusClientDevice {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(uint8_t, target_address) // the modbus device address
|
||||
|
||||
Trigger<std::span<const uint8_t>> *get_sent_trigger() { return &this->sent_trigger_; }
|
||||
Trigger<std::span<const uint8_t>, modbus::ExceptionCode> *get_error_trigger() { return &this->error_trigger_; }
|
||||
Trigger<std::span<const uint8_t>> *get_no_response_trigger() { return &this->no_response_trigger_; }
|
||||
Trigger<std::span<const uint8_t>> *get_not_sent_trigger() { return &this->not_sent_trigger_; }
|
||||
Trigger<uint8_t, std::span<const uint8_t>> *get_sent_trigger() { return &this->sent_trigger_; }
|
||||
Trigger<uint8_t, std::span<const uint8_t>, modbus::ExceptionCode> *get_error_trigger() {
|
||||
return &this->error_trigger_;
|
||||
}
|
||||
Trigger<uint8_t, std::span<const uint8_t>> *get_no_response_trigger() { return &this->no_response_trigger_; }
|
||||
Trigger<uint8_t, std::span<const uint8_t>> *get_not_sent_trigger() { return &this->not_sent_trigger_; }
|
||||
|
||||
/// The retry decision for on_no_response: given the request PDU, return true to have the hub re-queue
|
||||
/// The retry decision for on_no_response: given the request (address, PDU), return true to have the hub re-queue
|
||||
/// the frame. Set from the lambda form or a then: automation's nested retry lambda; may coexist with
|
||||
/// the no_response trigger (actions run, then this decides the retry).
|
||||
using retry_func_t = bool (*)(std::span<const uint8_t>);
|
||||
using retry_func_t = bool (*)(uint8_t, std::span<const uint8_t>);
|
||||
void set_retry(retry_func_t f) { this->retry_func_ = f; }
|
||||
|
||||
/// The frame was written to the wire: fires once per transmission, before any reply, and never for a
|
||||
/// send that ended in on_not_sent. request_pdu is the PDU sent (function code + data).
|
||||
void on_sent(std::span<const uint8_t> request_pdu) override { this->sent_trigger_.trigger(request_pdu); }
|
||||
void on_sent(uint8_t address, std::span<const uint8_t> request_pdu) override {
|
||||
this->sent_trigger_.trigger(address, request_pdu);
|
||||
}
|
||||
/// Never reached the wire, from either of two sources. The hub calls this for a request it accepted
|
||||
/// and then dropped, which happens only when clear_tx_queue_for_address() retires it - a modbus
|
||||
/// device going offline, say. Everything the hub refuses at the door instead returns false from
|
||||
/// queue_pdu() with no callback at all, so send_or_resolve_() below turns those into this same
|
||||
/// callback: a full queue, a duplicate write, or an empty PDU from a rejecting builder.
|
||||
void on_not_sent(std::span<const uint8_t> request_pdu) override { this->not_sent_trigger_.trigger(request_pdu); }
|
||||
void on_not_sent(uint8_t address, std::span<const uint8_t> request_pdu) override {
|
||||
this->not_sent_trigger_.trigger(address, request_pdu);
|
||||
}
|
||||
/// A Modbus exception reply. Lives here beside its trigger so every action subclass gets the pairing:
|
||||
/// register_client_action() wires on_error for all of them, so a derived class must not have to
|
||||
/// remember the override.
|
||||
void on_error(std::span<const uint8_t> request_pdu, modbus::ExceptionCode exception_code) override {
|
||||
this->error_trigger_.trigger(request_pdu, exception_code);
|
||||
void on_error(uint8_t address, std::span<const uint8_t> request_pdu, modbus::ExceptionCode exception_code) override {
|
||||
this->error_trigger_.trigger(address, request_pdu, exception_code);
|
||||
}
|
||||
/// No reply within send_wait_time. Run the on_no_response actions (empty in the pure-lambda form),
|
||||
/// then let the retry lambda, if set, decide whether the hub re-queues the frame (true = retry). The
|
||||
/// two coexist: a then: automation can also carry a retry lambda. No lambda = no retry.
|
||||
bool on_no_response(std::span<const uint8_t> request_pdu) override {
|
||||
this->no_response_trigger_.trigger(request_pdu);
|
||||
bool on_no_response(uint8_t address, std::span<const uint8_t> request_pdu) override {
|
||||
this->no_response_trigger_.trigger(address, request_pdu);
|
||||
if (this->retry_func_ != nullptr)
|
||||
return this->retry_func_(request_pdu);
|
||||
return this->retry_func_(address, request_pdu);
|
||||
return false;
|
||||
}
|
||||
/// Stamp the templated device address before every play(): subclasses cannot forget it, and the hub
|
||||
@@ -70,13 +76,13 @@ template<typename... Ts> class ClientActionBase : public Action<Ts...>, public m
|
||||
/// a PduBuffer parameter would widen each one to the 253-byte maximum just to cross the call.
|
||||
void send_or_resolve_(std::span<const uint8_t> pdu, modbus::CommandOptions options = {}) {
|
||||
if (!this->queue_pdu(pdu, options))
|
||||
this->on_not_sent(pdu);
|
||||
this->on_not_sent(this->address_, pdu);
|
||||
}
|
||||
|
||||
Trigger<std::span<const uint8_t>> sent_trigger_;
|
||||
Trigger<std::span<const uint8_t>, modbus::ExceptionCode> error_trigger_;
|
||||
Trigger<std::span<const uint8_t>> no_response_trigger_;
|
||||
Trigger<std::span<const uint8_t>> not_sent_trigger_;
|
||||
Trigger<uint8_t, std::span<const uint8_t>> sent_trigger_;
|
||||
Trigger<uint8_t, std::span<const uint8_t>, modbus::ExceptionCode> error_trigger_;
|
||||
Trigger<uint8_t, std::span<const uint8_t>> no_response_trigger_;
|
||||
Trigger<uint8_t, std::span<const uint8_t>> not_sent_trigger_;
|
||||
retry_func_t retry_func_{nullptr};
|
||||
};
|
||||
|
||||
@@ -119,7 +125,7 @@ template<typename... Ts> class WriteCommandOptions {
|
||||
};
|
||||
|
||||
/// modbus_client.send: fire a raw PDU (function code + data; the hub adds address and CRC). The reply is
|
||||
/// delivered raw - on_response(request, response) - deliberately bypassing the typed dispatch, so
|
||||
/// delivered raw - on_response(address, request, response) - deliberately bypassing the typed dispatch, so
|
||||
/// non-standard/custom transactions pass through untouched.
|
||||
/// The PDU is a stack-allocated modbus::helpers::PduBuffer, so a pdu lambda can build one with the
|
||||
/// modbus::helpers::create_*_pdu() builders and return it directly (smaller builder results convert).
|
||||
@@ -133,7 +139,7 @@ class ModbusClientSendAction : public ClientActionBase<Ts...>,
|
||||
public:
|
||||
TEMPLATABLE_VALUE(modbus::helpers::PduBuffer, pdu)
|
||||
|
||||
Trigger<std::span<const uint8_t>, std::span<const uint8_t>> *get_response_trigger() {
|
||||
Trigger<uint8_t, std::span<const uint8_t>, std::span<const uint8_t>> *get_response_trigger() {
|
||||
return &this->response_trigger_;
|
||||
}
|
||||
|
||||
@@ -143,12 +149,13 @@ class ModbusClientSendAction : public ClientActionBase<Ts...>,
|
||||
this->send_or_resolve_(this->pdu_.value(x...), options);
|
||||
}
|
||||
|
||||
void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) override {
|
||||
this->response_trigger_.trigger(request_pdu, response_pdu);
|
||||
void on_response(uint8_t address, std::span<const uint8_t> request_pdu,
|
||||
std::span<const uint8_t> response_pdu) override {
|
||||
this->response_trigger_.trigger(address, request_pdu, response_pdu);
|
||||
}
|
||||
|
||||
protected:
|
||||
Trigger<std::span<const uint8_t>, std::span<const uint8_t>> response_trigger_;
|
||||
Trigger<uint8_t, std::span<const uint8_t>, std::span<const uint8_t>> response_trigger_;
|
||||
};
|
||||
|
||||
/// Typed actions: these do NOT override the raw on_response, so the base ModbusClientDevice default runs
|
||||
|
||||
@@ -2,24 +2,18 @@ import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_SOURCE_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import Number, number_ns
|
||||
from .. import Number
|
||||
|
||||
NumberSensor = number_ns.class_("NumberSensor", sensor.Sensor, cg.Component)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
sensor.sensor_schema(NumberSensor)
|
||||
.extend(
|
||||
{
|
||||
cv.Required(CONF_SOURCE_ID): cv.use_id(Number),
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
CONFIG_SCHEMA = sensor.sensor_schema(sensor.Sensor).extend(
|
||||
{
|
||||
cv.Required(CONF_SOURCE_ID): cv.use_id(Number),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
source = await cg.get_variable(config[CONF_SOURCE_ID])
|
||||
var = await sensor.new_sensor(config, source)
|
||||
await cg.register_component(var, config)
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await sensor.new_sensor(config)
|
||||
# Number has the same float state callback as Sensor
|
||||
sensor.follow_sensor(var, await cg.get_variable(config[CONF_SOURCE_ID]))
|
||||
@@ -1,16 +0,0 @@
|
||||
#include "number_sensor.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::number {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "number.sensor");
|
||||
|
||||
void NumberSensor::setup() {
|
||||
this->source_->add_on_state_callback([this](float value) { this->publish_state(value); });
|
||||
if (this->source_->has_state())
|
||||
this->publish_state(this->source_->state);
|
||||
}
|
||||
|
||||
void NumberSensor::dump_config() { LOG_SENSOR("", "Number Sensor", this); }
|
||||
|
||||
} // namespace esphome::number
|
||||
@@ -1,19 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "../number.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
|
||||
namespace esphome::number {
|
||||
|
||||
class NumberSensor final : public sensor::Sensor, public Component {
|
||||
public:
|
||||
explicit NumberSensor(Number *source) : source_(source) {}
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
Number *source_;
|
||||
};
|
||||
|
||||
} // namespace esphome::number
|
||||
@@ -42,6 +42,7 @@ ONLINE_IMAGE_SCHEMA = (
|
||||
# AUTO (Content-Type detection) is online_image specific; not in the shared registry
|
||||
cv.Required(CONF_FORMAT): cv.one_of(*IMAGE_FORMATS, "AUTO", upper=True),
|
||||
cv.Optional(CONF_BUFFER_SIZE, default=65536): cv.int_range(256, 65536),
|
||||
cv.Optional(runtime_image.CONF_DOUBLE_BUFFER, default=False): cv.boolean,
|
||||
cv.Optional(CONF_REQUEST_HEADERS): cv.All(
|
||||
cv.Schema({cv.string: cv.templatable(cv.string)})
|
||||
),
|
||||
@@ -141,6 +142,8 @@ async def setup_online_image(config: ConfigType) -> None:
|
||||
)
|
||||
await cg.register_component(var, config)
|
||||
await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID])
|
||||
if settings.double_buffer:
|
||||
cg.add(var.set_double_buffer(True))
|
||||
|
||||
for key, value in config.get(CONF_REQUEST_HEADERS, {}).items():
|
||||
if isinstance(value, Lambda):
|
||||
|
||||
@@ -428,9 +428,8 @@ void OnlineImage::finish_download_() {
|
||||
|
||||
void OnlineImage::end_connection_() {
|
||||
// Abort any in-progress decode; the decoder object is kept warm for the next decode.
|
||||
// Use RuntimeImage::release() directly to avoid recursion with OnlineImage::release().
|
||||
if (this->is_decoding()) {
|
||||
RuntimeImage::release();
|
||||
this->abort_decode();
|
||||
}
|
||||
if (this->downloader_) {
|
||||
this->downloader_->end();
|
||||
|
||||
@@ -3004,3 +3004,73 @@ register_protocol_action(
|
||||
(CONF_CHANNELS, "channels", cg.uint16),
|
||||
(CONF_COMMAND, "command", cg.uint8),
|
||||
)
|
||||
|
||||
|
||||
# Cardin S438
|
||||
(
|
||||
CardinS438Data,
|
||||
CardinS438BinarySensor,
|
||||
CardinS438Trigger,
|
||||
_,
|
||||
CardinS438Dumper,
|
||||
) = declare_protocol("CardinS438")
|
||||
|
||||
|
||||
CARDIN_S438_CHANNELS = {
|
||||
"A": 0x02,
|
||||
"B": 0x00,
|
||||
"C": 0x03,
|
||||
"D": 0x01,
|
||||
}
|
||||
|
||||
|
||||
def _validate_cardin_s438_code(code):
|
||||
"""Accept the DIP switches as a 14-character binary string."""
|
||||
code = cv.string_strict(code)
|
||||
if len(code) != 14 or any(bit not in "01" for bit in code):
|
||||
raise cv.Invalid("code must be exactly 14 binary digits, e.g. '10100110101010'")
|
||||
return int(code, 2)
|
||||
|
||||
|
||||
CARDIN_S438_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_CODE): _validate_cardin_s438_code,
|
||||
cv.Optional(CONF_CHANNEL, default="A"): cv.enum(
|
||||
CARDIN_S438_CHANNELS, upper=True
|
||||
),
|
||||
cv.Optional(CONF_COMMAND_REPEATS, default=3): cv.int_range(min=1, max=255),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@register_binary_sensor("cardin_s438", CardinS438BinarySensor, CARDIN_S438_SCHEMA)
|
||||
def cardin_s438_binary_sensor(var: MockObj, config: ConfigType) -> None:
|
||||
cg.add(
|
||||
var.set_data(
|
||||
cg.StructInitializer(
|
||||
CardinS438Data,
|
||||
("code", config[CONF_CODE]),
|
||||
("channel", config[CONF_CHANNEL]),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@register_trigger("cardin_s438", CardinS438Trigger, CardinS438Data)
|
||||
def cardin_s438_trigger(var: MockObj, config: ConfigType) -> None:
|
||||
"""The trigger takes no options beyond the automation."""
|
||||
|
||||
|
||||
@register_dumper("cardin_s438", CardinS438Dumper)
|
||||
def cardin_s438_dumper(var: MockObj, config: ConfigType) -> None:
|
||||
"""The dumper takes no options."""
|
||||
|
||||
|
||||
register_protocol_action(
|
||||
"cardin_s438",
|
||||
"CardinS438",
|
||||
CARDIN_S438_SCHEMA,
|
||||
(CONF_CODE, "code", cg.uint16),
|
||||
(CONF_CHANNEL, "channel", cg.uint8),
|
||||
(CONF_COMMAND_REPEATS, "repeats", cg.uint8),
|
||||
)
|
||||
@@ -0,0 +1,159 @@
|
||||
#include "cardin_s438_protocol.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::remote_base {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "remote.cardin_s438");
|
||||
|
||||
namespace {
|
||||
|
||||
/*
|
||||
* Cardin S438 wire format
|
||||
*
|
||||
* Frame: 6400 us sync mark + 40 data bits (5 bytes, LSB-first).
|
||||
* 0 = 1460 us space, 750 us mark
|
||||
* 1 = 750 us space, 1460 us mark
|
||||
*
|
||||
* Bytes:
|
||||
* [0..2] Encoded pin fields and seed
|
||||
* [2] (seed + marker 0x87), modulo 256
|
||||
* Carries propagate into bytes 1 and 0.
|
||||
* [3..4] Sum of bytes [0..2], little-endian
|
||||
*
|
||||
* Pin code: 14 bits, MSB-first, mapped to X/Y fields:
|
||||
* Y7,Y6,X7,X6,Y5,X5,Y4,X4,Y3,X3,Y2,Y1,Y0,X2
|
||||
* Channels in X bits 1:0: A=10, B=00, C=11, D=01.
|
||||
*
|
||||
* Three frames per transmission, with 63650 us LOW gaps.
|
||||
* Seed chosen randomly; no pattern found in captures.
|
||||
*/
|
||||
|
||||
// Frame format and timing, in microseconds.
|
||||
constexpr uint8_t MARKER = 0x87;
|
||||
constexpr uint16_t SYNC_MARK_US = 6400;
|
||||
constexpr uint32_t FRAME_GAP_US = 63650;
|
||||
constexpr uint8_t WIRE_SIZE = 5;
|
||||
// Each bit is a space then a mark, and both lengths carry it
|
||||
constexpr BitTimings CARDIN_TIMINGS{
|
||||
.one_space = 750, .zero_space = 1460, .one_mark = 1460, .zero_mark = 750, .space_first = true};
|
||||
|
||||
// Maps pincode bits, most-significant first, to a bit of the X or Y field: PIN_Y marks Y, the low bits are the bit
|
||||
// number. In flash, so it stays out of RAM on ESP8266.
|
||||
constexpr uint8_t PIN_Y = 0x08;
|
||||
constexpr uint8_t PINCODE_BITS = 14;
|
||||
constexpr uint8_t PINCODE_MAP[PINCODE_BITS] PROGMEM = {
|
||||
PIN_Y | 7, PIN_Y | 6, 7, 6, PIN_Y | 5, 5, PIN_Y | 4, 4, PIN_Y | 3, 3, PIN_Y | 2, PIN_Y | 1, PIN_Y | 0, 2,
|
||||
};
|
||||
|
||||
// Convert the pincode and channel to the two decoded pin fields.
|
||||
void encode_pin_fields(uint16_t pincode, uint8_t channel, uint8_t &plain0, uint8_t &plain1) {
|
||||
uint8_t x = channel;
|
||||
uint8_t y = 0;
|
||||
|
||||
for (uint8_t i = 0; i < PINCODE_BITS; ++i) {
|
||||
if (((pincode >> (PINCODE_BITS - 1 - i)) & 1U) == 0)
|
||||
continue;
|
||||
const uint8_t entry = progmem_read_byte(&PINCODE_MAP[i]);
|
||||
(entry & PIN_Y ? y : x) |= 1U << (entry & 0x07);
|
||||
}
|
||||
|
||||
plain1 = ~y;
|
||||
plain0 = plain1 - x + 1;
|
||||
}
|
||||
|
||||
// Construct the wire bytes for one seed.
|
||||
void build_wire(uint8_t wire[WIRE_SIZE], uint8_t seed, uint8_t plain0, uint8_t plain1) {
|
||||
uint16_t sum = MARKER + seed;
|
||||
wire[2] = sum;
|
||||
|
||||
uint8_t key = seed + (sum >> 8);
|
||||
|
||||
sum = plain1 + key;
|
||||
wire[1] = sum;
|
||||
key += sum >> 8;
|
||||
|
||||
wire[0] = plain0 + key;
|
||||
|
||||
// checksum over the first three bytes, little-endian.
|
||||
const uint16_t checksum = sum16_bytes(wire, 3);
|
||||
wire[3] = checksum;
|
||||
wire[4] = checksum >> 8;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void CardinS438Protocol::encode(RemoteTransmitData *dst, const CardinS438Data &data) {
|
||||
uint8_t plain0;
|
||||
uint8_t plain1;
|
||||
|
||||
encode_pin_fields(data.code & 0x3FFF, data.channel & 0x03, plain0, plain1);
|
||||
|
||||
for (uint8_t frame = 0; frame < data.repeats; ++frame) {
|
||||
uint8_t wire[WIRE_SIZE];
|
||||
uint8_t seed = static_cast<uint8_t>(random_uint32() & 0xFF);
|
||||
|
||||
build_wire(wire, seed, plain0, plain1);
|
||||
dst->mark(SYNC_MARK_US);
|
||||
dst->item_bytes<CARDIN_TIMINGS>(wire, WIRE_SIZE);
|
||||
dst->space(FRAME_GAP_US);
|
||||
}
|
||||
}
|
||||
|
||||
optional<CardinS438Data> CardinS438Protocol::decode(RemoteReceiveData src) {
|
||||
while (src.is_valid()) {
|
||||
// Find the frame's long sync mark.
|
||||
if (!src.expect_mark(SYNC_MARK_US)) {
|
||||
src.advance();
|
||||
continue;
|
||||
}
|
||||
|
||||
uint8_t wire[WIRE_SIZE];
|
||||
if (!src.expect_bytes<CARDIN_TIMINGS>(wire, WIRE_SIZE) ||
|
||||
encode_value_le<uint16_t>(wire + 3) != sum16_bytes(wire, 3)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Recover the seed and undo the encoder's carry transform.
|
||||
uint8_t seed = wire[2] - MARKER;
|
||||
|
||||
uint8_t key = seed;
|
||||
key += (seed + MARKER) >> 8;
|
||||
|
||||
uint8_t plain1 = wire[1] - key;
|
||||
key += (plain1 + key) >> 8;
|
||||
|
||||
uint8_t plain0 = wire[0] - key;
|
||||
|
||||
// Recover the original X and Y fields.
|
||||
uint8_t x = plain1 - plain0 + 1;
|
||||
uint8_t y = ~plain1;
|
||||
|
||||
// Reconstruct the 14-bit code in transmission order.
|
||||
uint16_t code = 0;
|
||||
for (const uint8_t &slot : PINCODE_MAP) {
|
||||
const uint8_t entry = progmem_read_byte(&slot);
|
||||
code = (code << 1) | (((entry & PIN_Y ? y : x) >> (entry & 0x07)) & 1U);
|
||||
}
|
||||
|
||||
return CardinS438Data{
|
||||
.code = code,
|
||||
.channel = static_cast<uint8_t>(x & 0x03),
|
||||
.seed = seed,
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void CardinS438Protocol::dump(const CardinS438Data &data) {
|
||||
// The DIP switches as the config takes them; channels as CARDIN_S438_CHANNELS in __init__.py maps them
|
||||
char code[PINCODE_BITS + 1];
|
||||
for (uint8_t i = 0; i < PINCODE_BITS; ++i)
|
||||
code[i] = (data.code >> (PINCODE_BITS - 1 - i)) & 1U ? '1' : '0';
|
||||
code[PINCODE_BITS] = '\0';
|
||||
ESP_LOGI(TAG, "Received Cardin S438: code=%s channel=%c seed=0x%02X", code, "BDAC"[data.channel & 0x03], data.seed);
|
||||
}
|
||||
|
||||
} // namespace esphome::remote_base
|
||||
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/remote_base/remote_base.h"
|
||||
|
||||
namespace esphome::remote_base {
|
||||
|
||||
struct CardinS438Data {
|
||||
uint16_t code{0};
|
||||
uint8_t channel{0};
|
||||
uint8_t seed{0};
|
||||
uint8_t repeats{3};
|
||||
|
||||
bool operator==(const CardinS438Data &rhs) const { return code == rhs.code && channel == rhs.channel; }
|
||||
};
|
||||
|
||||
class CardinS438Protocol : public RemoteProtocol<CardinS438Data> {
|
||||
public:
|
||||
void encode(RemoteTransmitData *dst, const CardinS438Data &data);
|
||||
optional<CardinS438Data> decode(RemoteReceiveData src);
|
||||
void dump(const CardinS438Data &data);
|
||||
};
|
||||
|
||||
DECLARE_REMOTE_PROTOCOL(CardinS438)
|
||||
|
||||
} // namespace esphome::remote_base
|
||||
@@ -19,6 +19,7 @@ AUTO_LOAD = ["image"]
|
||||
CODEOWNERS = ["@guillempages", "@clydebarrow", "@kahrendt"]
|
||||
DOMAIN = "runtime_image"
|
||||
|
||||
CONF_DOUBLE_BUFFER = "double_buffer"
|
||||
CONF_PLACEHOLDER = "placeholder"
|
||||
CONF_TRANSPARENCY = "transparency"
|
||||
|
||||
@@ -212,6 +213,7 @@ class RuntimeImageSettings:
|
||||
transparent: cg.MockObj
|
||||
byte_order_big_endian: bool
|
||||
placeholder: cg.MockObj | None
|
||||
double_buffer: bool
|
||||
|
||||
|
||||
async def process_runtime_image_config(config: dict) -> RuntimeImageSettings:
|
||||
@@ -256,4 +258,5 @@ async def process_runtime_image_config(config: dict) -> RuntimeImageSettings:
|
||||
transparent=transparent,
|
||||
byte_order_big_endian=byte_order_big_endian,
|
||||
placeholder=placeholder,
|
||||
double_buffer=config.get(CONF_DOUBLE_BUFFER, False),
|
||||
)
|
||||
@@ -8,6 +8,8 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <new>
|
||||
|
||||
#ifdef USE_ESP_IDF
|
||||
#include "esp_task_wdt.h"
|
||||
#endif
|
||||
@@ -94,7 +96,38 @@ static int draw_callback(JPEGDRAW *jpeg) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
JpegDecoder::JpegDecoder(RuntimeImage *image) : ImageDecoder(image, JPEG) {
|
||||
RAMAllocator<JPEGDEC> allocator;
|
||||
JPEGDEC *jpeg = allocator.allocate(1);
|
||||
if (jpeg == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to allocate memory for the JPEG decoder");
|
||||
return;
|
||||
}
|
||||
this->jpeg_ = new (jpeg) JPEGDEC();
|
||||
}
|
||||
|
||||
JpegDecoder::~JpegDecoder() {
|
||||
if (this->jpeg_ != nullptr) {
|
||||
this->jpeg_->~JPEGDEC();
|
||||
RAMAllocator<JPEGDEC> allocator;
|
||||
allocator.deallocate(this->jpeg_, 1);
|
||||
}
|
||||
}
|
||||
|
||||
int JpegDecoder::prepare(size_t expected_size) {
|
||||
// Check before the base prepare() so a failure never leaves an active session
|
||||
if (this->jpeg_ == nullptr) {
|
||||
ESP_LOGE(TAG, "JPEG decoder engine not initialized!");
|
||||
return DECODE_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
return ImageDecoder::prepare(expected_size);
|
||||
}
|
||||
|
||||
int HOT JpegDecoder::decode(uint8_t *buffer, size_t size) {
|
||||
if (this->jpeg_ == nullptr) {
|
||||
ESP_LOGE(TAG, "JPEG decoder engine not initialized!");
|
||||
return DECODE_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
// JPEG decoder requires complete data
|
||||
// If we know the expected size, wait for it
|
||||
if (this->expected_size_ > 0 && size < this->expected_size_) {
|
||||
@@ -105,22 +138,22 @@ int HOT JpegDecoder::decode(uint8_t *buffer, size_t size) {
|
||||
// If size unknown, try to decode and see if it's valid
|
||||
// The JPEGDEC library will fail gracefully if data is incomplete
|
||||
|
||||
if (!this->jpeg_.openRAM(buffer, size, draw_callback)) {
|
||||
ESP_LOGE(TAG, "Could not open image for decoding: %d", this->jpeg_.getLastError());
|
||||
if (!this->jpeg_->openRAM(buffer, size, draw_callback)) {
|
||||
ESP_LOGE(TAG, "Could not open image for decoding: %d", this->jpeg_->getLastError());
|
||||
return DECODE_ERROR_INVALID_TYPE;
|
||||
}
|
||||
const int err = this->start_();
|
||||
if (err != 0)
|
||||
return err;
|
||||
if (!this->jpeg_.decode(0, 0, this->scale_option_()))
|
||||
if (!this->jpeg_->decode(0, 0, this->scale_option_()))
|
||||
return this->fail_();
|
||||
this->decoded_bytes_ = size;
|
||||
this->jpeg_.close();
|
||||
this->jpeg_->close();
|
||||
return size;
|
||||
}
|
||||
|
||||
int JpegDecoder::start_() {
|
||||
auto jpeg_type = this->jpeg_.getJPEGType();
|
||||
auto jpeg_type = this->jpeg_->getJPEGType();
|
||||
if (jpeg_type == JPEG_MODE_INVALID) {
|
||||
ESP_LOGE(TAG, "Unsupported JPEG image");
|
||||
return DECODE_ERROR_INVALID_TYPE;
|
||||
@@ -128,11 +161,12 @@ int JpegDecoder::start_() {
|
||||
ESP_LOGE(TAG, "Progressive JPEG images not supported");
|
||||
return DECODE_ERROR_UNSUPPORTED_FORMAT;
|
||||
}
|
||||
ESP_LOGD(TAG, "Image size: %d x %d, bpp: %d", this->jpeg_.getWidth(), this->jpeg_.getHeight(), this->jpeg_.getBpp());
|
||||
ESP_LOGD(TAG, "Image size: %d x %d, bpp: %d", this->jpeg_->getWidth(), this->jpeg_->getHeight(),
|
||||
this->jpeg_->getBpp());
|
||||
|
||||
this->jpeg_.setUserPointer(this);
|
||||
const int width = this->jpeg_.getWidth();
|
||||
const int height = this->jpeg_.getHeight();
|
||||
this->jpeg_->setUserPointer(this);
|
||||
const int width = this->jpeg_->getWidth();
|
||||
const int height = this->jpeg_->getHeight();
|
||||
// The tables for drawing are only built once the decoded size is known, below
|
||||
if (!this->resize_image_(width, height)) {
|
||||
return DECODE_ERROR_OUT_OF_MEMORY;
|
||||
@@ -155,17 +189,17 @@ int JpegDecoder::start_() {
|
||||
if (this->direct_rgb565_) {
|
||||
// JPEGDEC's RGB565_BIG_ENDIAN output is not byte swapped on every build (the x86 host build
|
||||
// returns little-endian words for it), so the byte order is applied in draw_rgb565 instead
|
||||
this->jpeg_.setPixelType(RGB565_LITTLE_ENDIAN);
|
||||
this->jpeg_->setPixelType(RGB565_LITTLE_ENDIAN);
|
||||
} else {
|
||||
this->jpeg_.setPixelType(RGB8888);
|
||||
this->jpeg_->setPixelType(RGB8888);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int JpegDecoder::fail_() {
|
||||
const bool read_failed = this->source_ != nullptr && this->source_failed_;
|
||||
const int error = this->jpeg_.getLastError();
|
||||
this->jpeg_.close();
|
||||
const int error = this->jpeg_->getLastError();
|
||||
this->jpeg_->close();
|
||||
this->source_ = nullptr;
|
||||
if (read_failed) {
|
||||
ESP_LOGE(TAG, "Reading the image failed");
|
||||
@@ -189,7 +223,7 @@ int JpegDecoder::fail_() {
|
||||
void JpegDecoder::reset() {
|
||||
// A decode from a source that was given up part way still has the image open
|
||||
if (this->source_ != nullptr) {
|
||||
this->jpeg_.close();
|
||||
this->jpeg_->close();
|
||||
this->source_ = nullptr;
|
||||
}
|
||||
ImageDecoder::reset();
|
||||
@@ -218,6 +252,10 @@ int32_t JpegDecoder::source_seek(JPEGFILE *file, int32_t position) {
|
||||
}
|
||||
|
||||
int JpegDecoder::begin_source(DecodeSource *source) {
|
||||
if (this->jpeg_ == nullptr) {
|
||||
ESP_LOGE(TAG, "JPEG decoder engine not initialized!");
|
||||
return DECODE_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
if (this->expected_size_ == 0 || this->expected_size_ > INT32_MAX) {
|
||||
ESP_LOGE(TAG, "Image size %zu cannot be read from a source", this->expected_size_);
|
||||
return DECODE_ERROR_INVALID_TYPE;
|
||||
@@ -228,19 +266,19 @@ int JpegDecoder::begin_source(DecodeSource *source) {
|
||||
}
|
||||
this->source_ = source;
|
||||
this->source_failed_ = false;
|
||||
if (!this->jpeg_.open(this, this->expected_size_, source_close, source_read, source_seek, draw_callback)) {
|
||||
if (!this->jpeg_->open(this, this->expected_size_, source_close, source_read, source_seek, draw_callback)) {
|
||||
// The caller may recover, for example by reading the whole image first, so it reports the failure
|
||||
ESP_LOGD(TAG, "Could not open image for decoding: %d", this->jpeg_.getLastError());
|
||||
ESP_LOGD(TAG, "Could not open image for decoding: %d", this->jpeg_->getLastError());
|
||||
this->source_ = nullptr;
|
||||
return this->source_failed_ ? DECODE_ERROR_READ : DECODE_ERROR_INVALID_TYPE;
|
||||
}
|
||||
const int err = this->start_();
|
||||
if (err != 0) {
|
||||
this->jpeg_.close();
|
||||
this->jpeg_->close();
|
||||
this->source_ = nullptr;
|
||||
return err;
|
||||
}
|
||||
if (!this->jpeg_.decodeBegin(0, 0, this->scale_option_()))
|
||||
if (!this->jpeg_->decodeBegin(0, 0, this->scale_option_()))
|
||||
return this->fail_();
|
||||
return 0;
|
||||
}
|
||||
@@ -248,7 +286,7 @@ int JpegDecoder::begin_source(DecodeSource *source) {
|
||||
int HOT JpegDecoder::decode_step() {
|
||||
if (this->source_ == nullptr)
|
||||
return DECODE_ERROR_INVALID_TYPE;
|
||||
const int result = this->jpeg_.decodeRows(1);
|
||||
const int result = this->jpeg_->decodeRows(1);
|
||||
// JPEGDEC may decode on with the missing data, so check the read before its result
|
||||
if (this->source_failed_)
|
||||
return this->fail_();
|
||||
@@ -259,7 +297,7 @@ int HOT JpegDecoder::decode_step() {
|
||||
if (result == 0)
|
||||
return this->fail_();
|
||||
this->decoded_bytes_ = this->expected_size_;
|
||||
this->jpeg_.close();
|
||||
this->jpeg_->close();
|
||||
this->source_ = nullptr;
|
||||
return DECODE_STEP_DONE;
|
||||
}
|
||||
|
||||
@@ -18,9 +18,10 @@ class JpegDecoder : public ImageDecoder {
|
||||
*
|
||||
* @param image The RuntimeImage to decode the stream into.
|
||||
*/
|
||||
JpegDecoder(RuntimeImage *image) : ImageDecoder(image, JPEG) {}
|
||||
~JpegDecoder() override {}
|
||||
JpegDecoder(RuntimeImage *image);
|
||||
~JpegDecoder() override;
|
||||
|
||||
int prepare(size_t expected_size) override;
|
||||
int HOT decode(uint8_t *buffer, size_t size) override;
|
||||
void reset() override;
|
||||
|
||||
@@ -46,7 +47,8 @@ class JpegDecoder : public ImageDecoder {
|
||||
static int32_t source_seek(JPEGFILE *file, int32_t position);
|
||||
static void source_close(void *handle) {}
|
||||
|
||||
JPEGDEC jpeg_{};
|
||||
/// About 18 KB of decoder state, allocated like pngle in PngDecoder: PSRAM first, nullptr if out of memory
|
||||
JPEGDEC *jpeg_{nullptr};
|
||||
int scale_{1}; // JPEGDEC decodes at 1/scale_ of the image size
|
||||
DecodeSource *source_{nullptr}; // Set while a begin_source() decode is in progress
|
||||
bool source_failed_{false};
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
@@ -48,7 +49,14 @@ RuntimeImage::RuntimeImage(ImageFormat format, image::ImageType type, image::Tra
|
||||
fixed_height_(fixed_height),
|
||||
placeholder_(placeholder) {}
|
||||
|
||||
RuntimeImage::~RuntimeImage() { this->release(); }
|
||||
RuntimeImage::~RuntimeImage() {
|
||||
#ifdef USE_LVGL_IMAGE
|
||||
for (lv_obj_t *obj : this->lvgl_widgets_)
|
||||
lv_obj_remove_event_cb_with_user_data(obj, lvgl_widget_deleted, this);
|
||||
this->lvgl_widgets_.clear();
|
||||
#endif
|
||||
this->release();
|
||||
}
|
||||
|
||||
int RuntimeImage::resize(int width, int height) {
|
||||
// Use fixed dimensions if specified (0 means auto-resize)
|
||||
@@ -69,6 +77,7 @@ int RuntimeImage::resize(int width, int height) {
|
||||
this->width_ = this->buffer_width_;
|
||||
this->height_ = this->buffer_height_;
|
||||
this->data_start_ = this->buffer_;
|
||||
this->refresh_lvgl_();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -176,6 +185,52 @@ void RuntimeImage::map_chroma_key(Color &color) {
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_LVGL
|
||||
lv_image_dsc_t *RuntimeImage::get_lv_image_dsc() {
|
||||
if (this->data_start_ == nullptr && this->placeholder_ != nullptr) {
|
||||
this->dsc_ = *this->placeholder_->get_lv_image_dsc();
|
||||
return &this->dsc_;
|
||||
}
|
||||
return Image::get_lv_image_dsc();
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_LVGL_IMAGE
|
||||
void RuntimeImage::add_lvgl_widget(lv_obj_t *obj) {
|
||||
if (std::find(this->lvgl_widgets_.begin(), this->lvgl_widgets_.end(), obj) != this->lvgl_widgets_.end())
|
||||
return;
|
||||
this->lvgl_widgets_.push_back(obj);
|
||||
lv_obj_add_event_cb(obj, lvgl_widget_deleted, LV_EVENT_DELETE, this);
|
||||
}
|
||||
|
||||
void RuntimeImage::lvgl_widget_deleted(lv_event_t *event) {
|
||||
auto *image = static_cast<RuntimeImage *>(lv_event_get_user_data(event));
|
||||
auto &widgets = image->lvgl_widgets_;
|
||||
widgets.erase(std::remove(widgets.begin(), widgets.end(), lv_event_get_target_obj(event)), widgets.end());
|
||||
}
|
||||
#endif
|
||||
|
||||
void RuntimeImage::refresh_lvgl_() {
|
||||
#ifdef USE_LVGL
|
||||
// Not a virtual call: this also runs from the destructor
|
||||
lv_image_dsc_t *dsc = this->RuntimeImage::get_lv_image_dsc();
|
||||
#ifdef USE_LVGL_IMAGE
|
||||
auto &widgets = this->lvgl_widgets_;
|
||||
for (size_t i = 0; i < widgets.size();) {
|
||||
lv_obj_t *obj = widgets[i];
|
||||
if (lv_image_get_src(obj) == dsc) {
|
||||
// Setting the same source makes the widget read the size again, and redraws it
|
||||
::lv_image_set_src(obj, dsc);
|
||||
i++;
|
||||
} else {
|
||||
lv_obj_remove_event_cb_with_user_data(obj, lvgl_widget_deleted, this);
|
||||
widgets.erase(widgets.begin() + i);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
void RuntimeImage::draw(int x, int y, display::Display *display, Color color_on, Color color_off) {
|
||||
if (this->data_start_) {
|
||||
// If we have a complete image, use the base class draw method
|
||||
@@ -263,11 +318,21 @@ bool RuntimeImage::end_decode() {
|
||||
}
|
||||
|
||||
// Finalize the image for display
|
||||
if (!this->progressive_display_) {
|
||||
if (this->is_double_buffered_()) {
|
||||
if (this->buffer_ != nullptr) {
|
||||
// Show the new image, and decode into the old one's buffer next time
|
||||
std::swap(this->front_buffer_, this->buffer_);
|
||||
std::swap(this->width_, this->buffer_width_);
|
||||
std::swap(this->height_, this->buffer_height_);
|
||||
this->data_start_ = this->front_buffer_;
|
||||
this->refresh_lvgl_();
|
||||
}
|
||||
} else if (!this->progressive_display_) {
|
||||
// Only now make the image visible
|
||||
this->width_ = this->buffer_width_;
|
||||
this->height_ = this->buffer_height_;
|
||||
this->data_start_ = this->buffer_;
|
||||
this->refresh_lvgl_();
|
||||
}
|
||||
|
||||
// End the session; the decoder object stays warm so the next decode can
|
||||
@@ -284,6 +349,15 @@ bool RuntimeImage::decoder_knows_end() const { return this->is_decoding() && thi
|
||||
|
||||
void RuntimeImage::release() {
|
||||
this->release_buffer_();
|
||||
if (this->front_buffer_) {
|
||||
RAMAllocator<uint8_t> allocator;
|
||||
allocator.deallocate(this->front_buffer_, this->get_buffer_size(this->width_, this->height_));
|
||||
this->front_buffer_ = nullptr;
|
||||
this->data_start_ = nullptr;
|
||||
this->width_ = 0;
|
||||
this->height_ = 0;
|
||||
this->refresh_lvgl_();
|
||||
}
|
||||
// End any active decode session; decoders free the format-specific working buffers
|
||||
// they can (PNG), while the decoder object itself is kept warm for the next decode.
|
||||
if (this->decoder_) {
|
||||
@@ -291,6 +365,16 @@ void RuntimeImage::release() {
|
||||
}
|
||||
}
|
||||
|
||||
void RuntimeImage::abort_decode() {
|
||||
if (!this->is_double_buffered_()) {
|
||||
this->release();
|
||||
return;
|
||||
}
|
||||
if (this->decoder_) {
|
||||
this->decoder_->reset();
|
||||
}
|
||||
}
|
||||
|
||||
void RuntimeImage::release_buffer_() {
|
||||
if (this->buffer_) {
|
||||
if (this->external_buffer_) {
|
||||
@@ -302,19 +386,25 @@ void RuntimeImage::release_buffer_() {
|
||||
RAMAllocator<uint8_t> allocator;
|
||||
allocator.deallocate(this->buffer_, this->get_buffer_size(this->buffer_width_, this->buffer_height_));
|
||||
}
|
||||
const bool shown = this->data_start_ == this->buffer_;
|
||||
this->buffer_ = nullptr;
|
||||
this->data_start_ = nullptr;
|
||||
this->width_ = 0;
|
||||
this->height_ = 0;
|
||||
this->buffer_width_ = 0;
|
||||
this->buffer_height_ = 0;
|
||||
#ifdef USE_LVGL
|
||||
memset(&this->dsc_, 0, sizeof(this->dsc_));
|
||||
#endif
|
||||
// With double buffering the buffer decoded into is not the one on show
|
||||
if (shown) {
|
||||
this->data_start_ = nullptr;
|
||||
this->width_ = 0;
|
||||
this->height_ = 0;
|
||||
this->refresh_lvgl_();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool RuntimeImage::set_external_buffer(uint8_t *buffer, int width, int height) {
|
||||
if (this->is_double_buffered_()) {
|
||||
ESP_LOGE(TAG, "An external buffer cannot be double buffered");
|
||||
return false;
|
||||
}
|
||||
this->release_buffer_();
|
||||
if (buffer == nullptr || this->get_buffer_size(width, height) == 0) {
|
||||
// Keep the released state rather than remembering a buffer that cannot be decoded into: an
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#include "esphome/components/image/image.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "image_decoder.h"
|
||||
#include "image_format.h"
|
||||
|
||||
@@ -54,6 +56,13 @@ class RuntimeImage : public image::Image {
|
||||
void map_chroma_key(Color &color);
|
||||
int get_buffer_width() const { return this->buffer_width_; }
|
||||
int get_buffer_height() const { return this->buffer_height_; }
|
||||
#ifdef USE_LVGL
|
||||
/// Until an image is loaded, the placeholder's descriptor if there is one
|
||||
lv_image_dsc_t *get_lv_image_dsc() override;
|
||||
#endif
|
||||
#ifdef USE_LVGL_IMAGE
|
||||
void add_lvgl_widget(lv_obj_t *obj) override;
|
||||
#endif
|
||||
|
||||
// Image drawing interface
|
||||
void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override;
|
||||
@@ -118,7 +127,9 @@ class RuntimeImage : public image::Image {
|
||||
/**
|
||||
* @brief Check if an image is currently loaded.
|
||||
*/
|
||||
bool is_loaded() const { return this->buffer_ != nullptr; }
|
||||
bool is_loaded() const {
|
||||
return this->is_double_buffered_() ? this->front_buffer_ != nullptr : this->buffer_ != nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the image format.
|
||||
@@ -134,6 +145,14 @@ class RuntimeImage : public image::Image {
|
||||
*/
|
||||
void release();
|
||||
|
||||
/**
|
||||
* @brief End a decode that will not be finished.
|
||||
*
|
||||
* With double buffering the image on show stays and the buffer decoded into is kept for the next
|
||||
* decode. Otherwise the same as release(), since the image on show was being decoded over.
|
||||
*/
|
||||
void abort_decode();
|
||||
|
||||
/**
|
||||
* @brief Decode into a buffer the caller owns, instead of one allocated here.
|
||||
*
|
||||
@@ -157,6 +176,8 @@ class RuntimeImage : public image::Image {
|
||||
* A null buffer or dimensions the image cannot decode at are refused, leaving the image with
|
||||
* no buffer at all.
|
||||
*
|
||||
* Refused when double buffering is on.
|
||||
*
|
||||
* @param buffer Memory for a picture of the given size, at least get_buffer_size() bytes.
|
||||
* @param width Width of the buffer in pixels.
|
||||
* @param height Height of the buffer in pixels.
|
||||
@@ -179,6 +200,13 @@ class RuntimeImage : public image::Image {
|
||||
*/
|
||||
void set_progressive_display(bool progressive) { this->progressive_display_ = progressive; }
|
||||
|
||||
/**
|
||||
* @brief Set whether to decode into a second buffer, keeping the image on show until the new one is complete.
|
||||
*
|
||||
* Both buffers are kept, so a new image of the same size allocates nothing. Ignored with progressive display.
|
||||
*/
|
||||
void set_double_buffer(bool double_buffer) { this->double_buffer_ = double_buffer; }
|
||||
|
||||
protected:
|
||||
/**
|
||||
* @brief Resize the image buffer to the requested dimensions.
|
||||
@@ -196,6 +224,11 @@ class RuntimeImage : public image::Image {
|
||||
*/
|
||||
void release_buffer_();
|
||||
|
||||
bool is_double_buffered_() const { return this->double_buffer_ && !this->progressive_display_; }
|
||||
|
||||
/// Bring the LVGL descriptor up to date after the image shown changed, and give it again to the widgets showing it
|
||||
void refresh_lvgl_();
|
||||
|
||||
/**
|
||||
* @brief Get the position in the buffer for a pixel.
|
||||
*/
|
||||
@@ -210,6 +243,8 @@ class RuntimeImage : public image::Image {
|
||||
|
||||
// Memory management
|
||||
uint8_t *buffer_{nullptr};
|
||||
/// With double buffering, the buffer on show, width_ by height_. buffer_ is then the one decoded into
|
||||
uint8_t *front_buffer_{nullptr};
|
||||
|
||||
// Decoder management
|
||||
std::unique_ptr<ImageDecoder> decoder_{nullptr};
|
||||
@@ -246,8 +281,14 @@ class RuntimeImage : public image::Image {
|
||||
|
||||
// Configuration
|
||||
bool progressive_display_{false};
|
||||
bool double_buffer_{false};
|
||||
/** Whether buffer_ belongs to the caller, so it must not be freed or resized here. */
|
||||
bool external_buffer_{false};
|
||||
#ifdef USE_LVGL_IMAGE
|
||||
static void lvgl_widget_deleted(lv_event_t *event);
|
||||
/// Widgets given this image. One moved to another image is dropped at the next refresh
|
||||
std::vector<lv_obj_t *> lvgl_widgets_;
|
||||
#endif
|
||||
|
||||
friend class ImageDecoder;
|
||||
};
|
||||
|
||||
@@ -124,6 +124,7 @@ from esphome.core import (
|
||||
)
|
||||
from esphome.core.config import UNIT_OF_MEASUREMENT_MAX_LENGTH
|
||||
from esphome.core.entity_helpers import (
|
||||
FollowEntity,
|
||||
SubEntities,
|
||||
entity_duplicate_validator,
|
||||
lazy_load_validator,
|
||||
@@ -1109,6 +1110,10 @@ def sub_sensors(
|
||||
return SubEntities(new_sensor, config, parent)
|
||||
|
||||
|
||||
# Makes a plain sensor follow another sensor: follow_sensor(entity, source)
|
||||
follow_sensor = FollowEntity("add_on_state_callback", ((cg.float_, "x"),), "x")
|
||||
|
||||
|
||||
SENSOR_IN_RANGE_CONDITION_SCHEMA = cv.All(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(Sensor),
|
||||
|
||||
@@ -2,29 +2,22 @@ import esphome.codegen as cg
|
||||
from esphome.components import binary_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_SOURCE_ID
|
||||
from esphome.core.entity_helpers import FollowEntity
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import Switch, switch_ns
|
||||
from .. import Switch
|
||||
|
||||
CODEOWNERS = ["@ssieb"]
|
||||
|
||||
SwitchBinarySensor = switch_ns.class_(
|
||||
"SwitchBinarySensor", binary_sensor.BinarySensor, cg.Component
|
||||
_FOLLOW_SWITCH = FollowEntity("add_on_state_callback", ((cg.bool_, "x"),), "x")
|
||||
|
||||
CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(binary_sensor.BinarySensor).extend(
|
||||
{
|
||||
cv.Required(CONF_SOURCE_ID): cv.use_id(Switch),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
binary_sensor.binary_sensor_schema(SwitchBinarySensor)
|
||||
.extend(
|
||||
{
|
||||
cv.Required(CONF_SOURCE_ID): cv.use_id(Switch),
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await binary_sensor.new_binary_sensor(config)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
await cg.add_variable(var.set_source, config[CONF_SOURCE_ID])
|
||||
_FOLLOW_SWITCH(var, await cg.get_variable(config[CONF_SOURCE_ID]))
|
||||
@@ -1,15 +0,0 @@
|
||||
#include "switch_binary_sensor.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::switch_ {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "switch.binary_sensor");
|
||||
|
||||
void SwitchBinarySensor::setup() {
|
||||
source_->add_on_state_callback([this](bool value) { this->publish_state(value); });
|
||||
this->publish_state(source_->state);
|
||||
}
|
||||
|
||||
void SwitchBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "Switch Binary Sensor", this); }
|
||||
|
||||
} // namespace esphome::switch_
|
||||
@@ -1,19 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "../switch.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
|
||||
namespace esphome::switch_ {
|
||||
|
||||
class SwitchBinarySensor final : public binary_sensor::BinarySensor, public Component {
|
||||
public:
|
||||
void set_source(Switch *source) { source_ = source; }
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
Switch *source_;
|
||||
};
|
||||
|
||||
} // namespace esphome::switch_
|
||||
@@ -4,23 +4,16 @@ import esphome.config_validation as cv
|
||||
from esphome.const import CONF_SOURCE_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import Text, text_ns
|
||||
from .. import Text
|
||||
|
||||
TextTextSensor = text_ns.class_("TextTextSensor", text_sensor.TextSensor, cg.Component)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
text_sensor.text_sensor_schema(TextTextSensor)
|
||||
.extend(
|
||||
{
|
||||
cv.Required(CONF_SOURCE_ID): cv.use_id(Text),
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
CONFIG_SCHEMA = text_sensor.text_sensor_schema(text_sensor.TextSensor).extend(
|
||||
{
|
||||
cv.Required(CONF_SOURCE_ID): cv.use_id(Text),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
source = await cg.get_variable(config[CONF_SOURCE_ID])
|
||||
var = await text_sensor.new_text_sensor(config, source)
|
||||
await cg.register_component(var, config)
|
||||
var = await text_sensor.new_text_sensor(config)
|
||||
# Text has the same string state callback as TextSensor
|
||||
text_sensor.follow_text_sensor(var, await cg.get_variable(config[CONF_SOURCE_ID]))
|
||||
@@ -1,16 +0,0 @@
|
||||
#include "text_text_sensor.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::text {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "text.text_sensor");
|
||||
|
||||
void TextTextSensor::setup() {
|
||||
this->source_->add_on_state_callback([this](const std::string &value) { this->publish_state(value); });
|
||||
if (this->source_->has_state())
|
||||
this->publish_state(this->source_->state);
|
||||
}
|
||||
|
||||
void TextTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Text Text Sensor", this); }
|
||||
|
||||
} // namespace esphome::text
|
||||
@@ -1,19 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "../text.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/text_sensor/text_sensor.h"
|
||||
|
||||
namespace esphome::text {
|
||||
|
||||
class TextTextSensor final : public text_sensor::TextSensor, public Component {
|
||||
public:
|
||||
explicit TextTextSensor(Text *source) : source_(source) {}
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
Text *source_;
|
||||
};
|
||||
|
||||
} // namespace esphome::text
|
||||
@@ -22,6 +22,7 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import (
|
||||
FollowEntity,
|
||||
SubEntities,
|
||||
entity_duplicate_validator,
|
||||
queue_entity_register,
|
||||
@@ -238,6 +239,12 @@ def sub_text_sensors(
|
||||
return SubEntities(new_text_sensor, config, parent)
|
||||
|
||||
|
||||
# Makes a plain text sensor follow another text sensor: follow_text_sensor(entity, source)
|
||||
follow_text_sensor = FollowEntity(
|
||||
"add_on_state_callback", ((cg.std_string_ref.operator("const"), "x"),), "x"
|
||||
)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.CORE)
|
||||
async def to_code(config):
|
||||
cg.add_global(text_sensor_ns.using)
|
||||
|
||||
@@ -1264,6 +1264,18 @@ class CoreFinalValidateStep(ConfigValidationStep):
|
||||
fv.full_config.reset(token)
|
||||
|
||||
|
||||
class OptionalPinValidationStep(ConfigValidationStep):
|
||||
"""Remove optional pins that are also used elsewhere"""
|
||||
|
||||
# After the ID pass, and before component final validations which may inspect pin use
|
||||
priority = -15.0
|
||||
|
||||
def run(self, result: Config) -> None:
|
||||
if result.errors:
|
||||
return
|
||||
pins.PIN_SCHEMA_REGISTRY.remove_optional_conflicts(result)
|
||||
|
||||
|
||||
class PinUseValidationCheck(ConfigValidationStep):
|
||||
"""Check for pin reuse"""
|
||||
|
||||
@@ -1437,6 +1449,7 @@ def validate_config(
|
||||
result.add_validation_step(PrefetchRemoteFilesValidationStep())
|
||||
result.add_validation_step(IDPassValidationStep())
|
||||
result.add_validation_step(CoreFinalValidateStep())
|
||||
result.add_validation_step(OptionalPinValidationStep())
|
||||
result.add_validation_step(PinUseValidationCheck())
|
||||
|
||||
result.add_validation_step(RemoveReferenceValidationStep())
|
||||
|
||||
@@ -416,10 +416,13 @@ void Application::teardown_components(uint32_t timeout_ms) {
|
||||
}
|
||||
}
|
||||
|
||||
void Application::add_looping_components_by_state_(bool match_loop_done) {
|
||||
void Application::add_looping_components_by_state_(bool inactive) {
|
||||
for (auto *obj : this->components_) {
|
||||
if (obj->has_overridden_loop() &&
|
||||
((obj->get_component_state() & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE) == match_loop_done) {
|
||||
if (!obj->has_overridden_loop()) {
|
||||
continue;
|
||||
}
|
||||
const uint8_t state = obj->get_component_state() & COMPONENT_STATE_MASK;
|
||||
if ((state == COMPONENT_STATE_LOOP_DONE || state == COMPONENT_STATE_FAILED) == inactive) {
|
||||
this->looping_components_.push_back(obj);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -433,16 +433,15 @@ class Application {
|
||||
// FixedVector capacity was pre-initialized by codegen with the exact count
|
||||
// of components that override loop(), computed at C++ compile time.
|
||||
|
||||
// Add all components with loop override that aren't already LOOP_DONE
|
||||
// Some components (like logger) may call disable_loop() during initialization
|
||||
// before setup runs, so we need to respect their LOOP_DONE state
|
||||
// Add all components with loop override that aren't already LOOP_DONE or FAILED
|
||||
// Some components (like logger) may call disable_loop() during initialization,
|
||||
// and some call mark_failed() from their constructor, before setup runs
|
||||
this->add_looping_components_by_state_(false);
|
||||
this->looping_components_active_end_ = this->looping_components_.size();
|
||||
// Then add any components that are already LOOP_DONE to the inactive section
|
||||
// This handles components that called disable_loop() during initialization
|
||||
// Then add any components that are already LOOP_DONE or FAILED to the inactive section
|
||||
this->add_looping_components_by_state_(true);
|
||||
}
|
||||
void add_looping_components_by_state_(bool match_loop_done);
|
||||
void add_looping_components_by_state_(bool inactive);
|
||||
|
||||
// These methods are called by Component::disable_loop() and Component::enable_loop()
|
||||
// Components should not call these directly - use this->disable_loop() or this->enable_loop()
|
||||
|
||||
@@ -169,7 +169,7 @@ struct Color {
|
||||
uint8_t r = rand >> 16;
|
||||
uint8_t g = rand >> 8;
|
||||
uint8_t b = rand >> 0;
|
||||
const uint16_t max_rgb = std::max({r, g, b});
|
||||
const uint16_t max_rgb = std::max({r, g, b, uint8_t(1)});
|
||||
return Color(uint8_t((uint16_t(r) * 255U / max_rgb)), uint8_t((uint16_t(g) * 255U / max_rgb)),
|
||||
uint8_t((uint16_t(b) * 255U / max_rgb)), w);
|
||||
}
|
||||
|
||||
@@ -96,6 +96,7 @@
|
||||
#define USE_GPIO_SWITCH_INTERLOCK
|
||||
#define USE_GRAPH
|
||||
#define USE_GRAPHICAL_DISPLAY_MENU
|
||||
#define USE_GREE_FAHRENHEIT
|
||||
#define USE_GREE_YAW1F
|
||||
#define USE_HOERMANN_HCP_IDENTITY
|
||||
#define USE_HOMEASSISTANT_TIME
|
||||
@@ -201,6 +202,7 @@
|
||||
#define USE_REMOTE_PROTOCOL_BRENNENSTUHL
|
||||
#define USE_REMOTE_PROTOCOL_BYRONSX
|
||||
#define USE_REMOTE_PROTOCOL_CANALSAT
|
||||
#define USE_REMOTE_PROTOCOL_CARDIN_S438
|
||||
#define USE_REMOTE_PROTOCOL_COOLIX
|
||||
#define USE_REMOTE_PROTOCOL_DISH
|
||||
#define USE_REMOTE_PROTOCOL_DOOYA
|
||||
|
||||
@@ -45,6 +45,7 @@ from esphome.core.config import (
|
||||
UNIT_OF_MEASUREMENT_MAX_LENGTH,
|
||||
)
|
||||
from esphome.cpp_generator import (
|
||||
LambdaExpression,
|
||||
MockObj,
|
||||
MockObjClass,
|
||||
RawExpression,
|
||||
@@ -519,6 +520,23 @@ class ApplyEntity:
|
||||
_APPLY_DOMAIN = "entity_apply"
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class FollowEntity:
|
||||
"""A state callback, its parameters and the value an entity republishes from it."""
|
||||
|
||||
callback: str
|
||||
params: tuple[tuple[SafeExpType, str], ...]
|
||||
value: str
|
||||
|
||||
def __call__(self, entity: MockObj, source: MockObj, *args: SafeExpType) -> None:
|
||||
"""Make ``entity`` publish every state of ``source``; ``args`` precede the callback."""
|
||||
publish = f"{parent_ref(entity).publish_state(RawExpression(self.value))};"
|
||||
callback = LambdaExpression(
|
||||
[publish], list(self.params), capture="", return_type=cg.void
|
||||
)
|
||||
add(getattr(source, self.callback)(*args, callback))
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class SubEntities:
|
||||
"""Create the child entity configured under key, set parent (if bound) and pass it to setter.
|
||||
|
||||
@@ -483,13 +483,6 @@ uint32_t HOT Scheduler::call(uint32_t now) {
|
||||
// Not reached timeout yet, done for this call
|
||||
break;
|
||||
}
|
||||
// Don't run on failed components (is_item_failed_ exempts SELF_POINTER delays).
|
||||
if (this->is_item_failed_(item)) {
|
||||
LockGuard guard{this->lock_};
|
||||
this->recycle_item_main_loop_(this->pop_raw_locked_());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if item is marked for removal
|
||||
// This handles two cases:
|
||||
// 1. Item was marked for removal after cleanup_() but before we got here
|
||||
@@ -514,6 +507,13 @@ uint32_t HOT Scheduler::call(uint32_t now) {
|
||||
}
|
||||
#endif
|
||||
|
||||
// Don't run on failed components (is_item_failed_ exempts SELF_POINTER delays).
|
||||
if (this->is_item_failed_(item)) {
|
||||
LockGuard guard{this->lock_};
|
||||
this->recycle_item_main_loop_(this->pop_raw_locked_());
|
||||
continue;
|
||||
}
|
||||
|
||||
#ifdef ESPHOME_DEBUG_SCHEDULER
|
||||
{
|
||||
SchedulerNameLog name_log;
|
||||
|
||||
@@ -331,6 +331,8 @@ class FloatLiteral(Literal):
|
||||
def __str__(self):
|
||||
if math.isnan(self.f):
|
||||
return "NAN"
|
||||
if math.isinf(self.f):
|
||||
return "INFINITY" if self.f > 0 else "-INFINITY"
|
||||
return f"{self.f}f"
|
||||
|
||||
|
||||
|
||||
+7
-1
@@ -466,9 +466,15 @@ def receive_exactly(
|
||||
|
||||
while len(data) < amount:
|
||||
try:
|
||||
data += recv_decode(sock, amount - len(data), decode=decode) # type: ignore[operator]
|
||||
chunk = recv_decode(sock, amount - len(data), decode=decode)
|
||||
except OSError as err:
|
||||
raise OTANetworkError(f"receiving {msg}: {err}") from err
|
||||
if not chunk:
|
||||
raise OTANetworkError(
|
||||
f"receiving {msg}: device closed connection after "
|
||||
f"{len(data)} of {amount} bytes"
|
||||
)
|
||||
data += chunk # type: ignore[operator]
|
||||
return data
|
||||
|
||||
|
||||
|
||||
+108
-10
@@ -1,8 +1,10 @@
|
||||
from collections.abc import Callable, Hashable
|
||||
from contextvars import ContextVar
|
||||
from functools import reduce
|
||||
import logging
|
||||
from logging import Logger
|
||||
import operator
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
@@ -21,17 +23,64 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.core import CORE, ID
|
||||
from esphome.cpp_generator import MockObj, MockObjClass
|
||||
from esphome.schema_extractors import schema_extractor
|
||||
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
|
||||
from esphome.types import ConfigType, SafeExpType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from esphome.config import Config
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# A use of a pin: the path of the component using it, the pin provider's id and the pin config
|
||||
PinUse = tuple[list, Any, ConfigType]
|
||||
|
||||
# Set while validating pins that should give way to any other use of the same pin
|
||||
_validating_optional = ContextVar("validating_optional_pins", default=False)
|
||||
|
||||
|
||||
def _remove_value(conf: Any, target: Any) -> bool:
|
||||
"""Remove the object ``target`` from wherever it is nested in ``conf``."""
|
||||
items = conf.items() if isinstance(conf, dict) else enumerate(conf)
|
||||
for key, value in items:
|
||||
if value is target:
|
||||
del conf[key]
|
||||
return True
|
||||
if isinstance(value, (dict, list)) and _remove_value(value, target):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class PinRegistry(dict):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.pins_used = {}
|
||||
# Entries of pins_used that give way to other uses, keyed by id()
|
||||
self.optional_pins = {}
|
||||
|
||||
def reset(self):
|
||||
self.pins_used = {}
|
||||
self.optional_pins = {}
|
||||
|
||||
def optional(self, validator: Callable[[Any], Any]) -> Callable[[Any], Any]:
|
||||
"""
|
||||
Wrap a pin validator so the pins it validates give way to any other use of the
|
||||
same pin. Instead of the pin being reported as used in multiple places,
|
||||
remove_optional_conflicts() removes it from the config, unless every other use
|
||||
sets allow_other_uses, in which case the pin is shared. Use this for pins a
|
||||
component supplies as defaults that a user may want to take over.
|
||||
"""
|
||||
|
||||
@schema_extractor("schema")
|
||||
def wrapper(value: Any) -> Any:
|
||||
if value == SCHEMA_EXTRACT:
|
||||
return validator
|
||||
token = _validating_optional.set(True)
|
||||
try:
|
||||
return validator(value)
|
||||
finally:
|
||||
_validating_optional.reset(token)
|
||||
|
||||
return wrapper
|
||||
|
||||
def get_count(self, key, id, number):
|
||||
"""
|
||||
@@ -86,7 +135,10 @@ class PinRegistry(dict):
|
||||
self.pins_used[pin_key] = []
|
||||
# client_id identifies the instance of the providing component
|
||||
client_id = result.get(key)
|
||||
self.pins_used[pin_key].append((path_context.get(), client_id, result))
|
||||
entry = (path_context.get(), client_id, result)
|
||||
self.pins_used[pin_key].append(entry)
|
||||
if _validating_optional.get():
|
||||
self.optional_pins[id(entry)] = entry
|
||||
# return the validated pin config
|
||||
return result
|
||||
|
||||
@@ -110,23 +162,69 @@ class PinRegistry(dict):
|
||||
key = self.get_key(key) if isinstance(key, dict) else key
|
||||
return self[key][0]
|
||||
|
||||
def final_validate(self, fconf):
|
||||
"""
|
||||
Run the final validation for all pins, and check for reuse
|
||||
:param fconf: The full config
|
||||
"""
|
||||
def _grouped(self) -> dict[tuple, list[PinUse]]:
|
||||
"""Group pin uses by (schema key, resolved provider id, pin number)."""
|
||||
# Regroup by (schema key, resolved provider id, pin number) rather than
|
||||
# the validate()-time bucket, so the same physical pin reached via an
|
||||
# explicit id and via {address: ...} (or an omitted id) is recognized as
|
||||
# one reuse group regardless of which syntax was used to reference the hub.
|
||||
grouped: dict[tuple, list] = {}
|
||||
grouped: dict[tuple, list[PinUse]] = {}
|
||||
for (key, _, number), pin_list in self.pins_used.items():
|
||||
for entry in pin_list:
|
||||
client_id = entry[1]
|
||||
provider_id = client_id.id if isinstance(client_id, ID) else client_id
|
||||
grouped.setdefault((key, provider_id, number), []).append(entry)
|
||||
return grouped
|
||||
|
||||
for (key, _, _), pin_list in grouped.items():
|
||||
def remove_optional_conflicts(self, fconf: "Config") -> None:
|
||||
"""
|
||||
Remove optional pins that are also used elsewhere from the config, or mark them
|
||||
as shared if every other use sets allow_other_uses. This runs once ids are resolved and before the components' final validation,
|
||||
so those only see the pins that will be used.
|
||||
:param fconf: The full config
|
||||
"""
|
||||
if not self.optional_pins:
|
||||
return
|
||||
for (key, provider_id, _), pin_list in self._grouped().items():
|
||||
optional = [e for e in pin_list if id(e) in self.optional_pins]
|
||||
others = [e for e in pin_list if id(e) not in self.optional_pins]
|
||||
if not optional or not others:
|
||||
continue
|
||||
if all(e[2].get(CONF_ALLOW_OTHER_USES, False) for e in others):
|
||||
# The other uses ask to share the pin, so share it with the optional ones too
|
||||
for _, _, pin_config in optional:
|
||||
pin_config[CONF_ALLOW_OTHER_USES] = True
|
||||
continue
|
||||
for entry in optional:
|
||||
self._remove_optional(fconf, key, provider_id, entry)
|
||||
|
||||
def _remove_optional(
|
||||
self, fconf: "Config", key: str, provider_id: str | None, entry: PinUse
|
||||
) -> None:
|
||||
"""
|
||||
Remove an optional pin from the config and from the pins used. If it cannot be
|
||||
found in the config it is kept, so final_validate() reports the conflict.
|
||||
"""
|
||||
pin_path, _, pin_config = entry
|
||||
if not _remove_value(fconf.get_config_for_path(pin_path), pin_config):
|
||||
return
|
||||
for pin_list in self.pins_used.values():
|
||||
pin_list[:] = [e for e in pin_list if e is not entry]
|
||||
del self.optional_pins[id(entry)]
|
||||
pin = f"{key} {provider_id} pin" if provider_id is not None else "Pin"
|
||||
_LOGGER.info(
|
||||
"%s %s is used elsewhere, so is not applied as a default for %s",
|
||||
pin,
|
||||
pin_config[CONF_NUMBER],
|
||||
"->".join(str(p) for p in pin_path),
|
||||
)
|
||||
|
||||
def final_validate(self, fconf):
|
||||
"""
|
||||
Run the final validation for all pins, and check for reuse
|
||||
:param fconf: The full config
|
||||
"""
|
||||
for (key, _, _), pin_list in self._grouped().items():
|
||||
count = len(pin_list) # number of places same pin used.
|
||||
final_val_fun = self[key][2] # final validation function
|
||||
for pin_path, client_id, pin_config in pin_list:
|
||||
|
||||
+21
-9
@@ -326,6 +326,25 @@ def load_test_manifest_overrides(
|
||||
ManifestOverrideLoader = Callable[[list[str]], None]
|
||||
|
||||
|
||||
def resolve_dependencies_with_overrides(
|
||||
components: list[str], manifest_override_loader: ManifestOverrideLoader
|
||||
) -> list[str]:
|
||||
"""Return the components and all their dependencies, with overrides applied.
|
||||
|
||||
Overrides are applied before each dependency search, so dependencies added by
|
||||
any override_manifest(), including one of a component found only as a
|
||||
dependency, are included. The loader is called again with components it has
|
||||
already seen, so it must apply each override only once.
|
||||
"""
|
||||
resolved = sorted(components)
|
||||
while True:
|
||||
manifest_override_loader(resolved)
|
||||
expanded = sorted(get_all_dependencies(set(resolved)))
|
||||
if expanded == resolved:
|
||||
return resolved
|
||||
resolved = expanded
|
||||
|
||||
|
||||
def compile_and_get_binary(
|
||||
config: dict,
|
||||
components: list[str],
|
||||
@@ -366,19 +385,12 @@ def compile_and_get_binary(
|
||||
else:
|
||||
config.setdefault(key, value)
|
||||
|
||||
# Apply manifest overrides before dependency resolution so that any
|
||||
# dependency additions made by override_manifest() are picked up.
|
||||
manifest_override_loader(components)
|
||||
|
||||
# Obtain possible dependencies BEFORE validate_config, because
|
||||
# get_all_dependencies calls CORE.reset() which clears build_path.
|
||||
components_with_dependencies: list[str] = sorted(
|
||||
get_all_dependencies(set(components))
|
||||
components_with_dependencies = resolve_dependencies_with_overrides(
|
||||
components, manifest_override_loader
|
||||
)
|
||||
|
||||
# Apply overrides for any transitively discovered dependencies.
|
||||
manifest_override_loader(components_with_dependencies)
|
||||
|
||||
CORE.config_path = tests_dir / "dummy.yaml"
|
||||
CORE.dashboard = None
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32-s3-devkitc-1
|
||||
|
||||
psram:
|
||||
mode: octal
|
||||
|
||||
spi:
|
||||
clk_pin: GPIO7
|
||||
mosi_pin: GPIO9
|
||||
|
||||
display:
|
||||
- platform: epaper_spi
|
||||
id: epaper_display
|
||||
model: seeed-reterminal-sticky
|
||||
@@ -0,0 +1,23 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32-s3-devkitc-1
|
||||
|
||||
psram:
|
||||
mode: octal
|
||||
|
||||
spi:
|
||||
clk_pin: GPIO7
|
||||
mosi_pin: GPIO9
|
||||
|
||||
# Takes over the model's default enable pin
|
||||
switch:
|
||||
- platform: gpio
|
||||
id: panel_power
|
||||
pin: GPIO47
|
||||
|
||||
display:
|
||||
- platform: epaper_spi
|
||||
id: epaper_display
|
||||
model: seeed-reterminal-sticky
|
||||
@@ -0,0 +1,46 @@
|
||||
"""Tests for applying the default enable pins of epaper_spi models."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
|
||||
def _enable_pin_numbers(main_cpp: str) -> list[int]:
|
||||
"""Get the GPIO numbers passed to set_enable_pins() in the generated code."""
|
||||
match = re.search(r"epaper_display->set_enable_pins\(\{(.*)\}\);", main_cpp)
|
||||
if match is None:
|
||||
return []
|
||||
numbers = []
|
||||
for pin_id in match.group(1).split(", "):
|
||||
pin = re.search(rf"{pin_id}->set_pin\(::GPIO_NUM_(\d+)\);", main_cpp)
|
||||
assert pin is not None
|
||||
numbers.append(int(pin.group(1)))
|
||||
return numbers
|
||||
|
||||
|
||||
def test_default_enable_pin(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""The model's enable pin is applied when it is not used elsewhere."""
|
||||
main_cpp = generate_main(component_config_path("enable_pin_default.yaml"))
|
||||
assert _enable_pin_numbers(main_cpp) == [47]
|
||||
|
||||
|
||||
def test_default_enable_pin_used_elsewhere(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""A default enable pin used by a switch is left out instead of failing validation."""
|
||||
main_cpp = generate_main(component_config_path("enable_pin_used_elsewhere.yaml"))
|
||||
assert _enable_pin_numbers(main_cpp) == []
|
||||
assert "panel_power->set_pin(" in main_cpp
|
||||
|
||||
|
||||
def test_user_enable_pins(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""Enable pins given in the config are applied as given."""
|
||||
main_cpp = generate_main(component_config_path("enable_pin_test.yaml"))
|
||||
assert _enable_pin_numbers(main_cpp) == [25, 26]
|
||||
@@ -0,0 +1,26 @@
|
||||
esphome:
|
||||
name: it8951-enable-pins
|
||||
|
||||
esp32:
|
||||
board: esp32-s3-devkitc-1
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
psram:
|
||||
mode: octal
|
||||
|
||||
spi:
|
||||
clk_pin: 7
|
||||
mosi_pin: 9
|
||||
miso_pin: 8
|
||||
|
||||
# Takes over the model's default 1.8V logic supply enable pin
|
||||
switch:
|
||||
- platform: gpio
|
||||
id: logic_supply
|
||||
pin: GPIO21
|
||||
|
||||
display:
|
||||
- platform: it8951
|
||||
id: epaper_display
|
||||
model: seeed-reterminal-e1003
|
||||
@@ -1,6 +1,8 @@
|
||||
"""Tests for it8951 configuration validation."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from esphome.components.const import CONF_HOLD_STATE
|
||||
@@ -26,3 +28,18 @@ def test_reterminal_e1003_enable_pins_hold_state(
|
||||
enable_pins = config[CONF_ENABLE_PIN]
|
||||
assert [pin[CONF_NUMBER] for pin in enable_pins] == [21, 11]
|
||||
assert all(pin[CONF_HOLD_STATE] for pin in enable_pins)
|
||||
|
||||
|
||||
def test_reterminal_e1003_enable_pin_used_elsewhere(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_fixture_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""A default enable pin used by a switch is left out, and the other is still applied."""
|
||||
main_cpp = generate_main(component_fixture_path("enable_pins.yaml"))
|
||||
|
||||
match = re.search(r"epaper_display->set_enable_pins\(\{(.*)\}\);", main_cpp)
|
||||
assert match is not None
|
||||
pin_ids = match.group(1).split(", ")
|
||||
assert len(pin_ids) == 1
|
||||
assert f"{pin_ids[0]}->set_pin(::GPIO_NUM_11);" in main_cpp
|
||||
assert "logic_supply->set_pin(" in main_cpp
|
||||
Whitespace-only changes.
@@ -0,0 +1,24 @@
|
||||
"""lightwaverf.send_raw never used name; it is dropped with a warning until 2027.5.0."""
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.automation import ACTION_REGISTRY
|
||||
import esphome.components.lightwaverf # noqa: F401
|
||||
from esphome.const import CONF_NAME, CONF_REPEAT
|
||||
|
||||
CODE = [0x04, 0x00, 0x00, 0x00, 0x0F, 0x03, 0x0D, 0x09, 0x08, 0x08]
|
||||
|
||||
|
||||
def test_name_is_dropped_with_a_warning(caplog: pytest.LogCaptureFixture) -> None:
|
||||
config = ACTION_REGISTRY["lightwaverf.send_raw"].schema(
|
||||
{"code": CODE, CONF_NAME: "Sofa"}
|
||||
)
|
||||
assert CONF_NAME not in config
|
||||
assert "'name' on lightwaverf.send_raw has no effect" in caplog.text
|
||||
assert "2027.5.0" in caplog.text
|
||||
|
||||
|
||||
def test_name_is_no_longer_required(caplog: pytest.LogCaptureFixture) -> None:
|
||||
config = ACTION_REGISTRY["lightwaverf.send_raw"].schema({"code": CODE})
|
||||
assert config[CONF_REPEAT] == 10
|
||||
assert "lightwaverf" not in caplog.text
|
||||
@@ -0,0 +1,50 @@
|
||||
esphome:
|
||||
name: test-canvas-draw-options
|
||||
on_boot:
|
||||
- lvgl.canvas.draw_rectangle:
|
||||
id: test_canvas
|
||||
x: 0
|
||||
y: 0
|
||||
width: 20
|
||||
height: 10
|
||||
bg_grad: test_grad
|
||||
- lvgl.canvas.draw_line:
|
||||
id: test_canvas
|
||||
dash_width: 4
|
||||
dash_gap: 2
|
||||
points:
|
||||
- 0,0
|
||||
- 10,10
|
||||
|
||||
esp32:
|
||||
board: lolin_c3_mini
|
||||
|
||||
spi:
|
||||
mosi_pin:
|
||||
number: GPIO2
|
||||
ignore_strapping_warning: true
|
||||
clk_pin: GPIO1
|
||||
|
||||
display:
|
||||
- platform: mipi_spi
|
||||
data_rate: 20MHz
|
||||
model: st7735
|
||||
cs_pin:
|
||||
number: GPIO8
|
||||
ignore_strapping_warning: true
|
||||
dc_pin: GPIO3
|
||||
|
||||
lvgl:
|
||||
gradients:
|
||||
- id: test_grad
|
||||
direction: hor
|
||||
stops:
|
||||
- color: 0xFF0000
|
||||
position: 0
|
||||
- color: 0x0000FF
|
||||
position: 255
|
||||
widgets:
|
||||
- canvas:
|
||||
id: test_canvas
|
||||
width: 32
|
||||
height: 32
|
||||
@@ -0,0 +1,58 @@
|
||||
esphome:
|
||||
name: test-indicator-update-opa
|
||||
on_boot:
|
||||
- lvgl.indicator.update:
|
||||
id: test_arc
|
||||
opa: 50%
|
||||
- lvgl.indicator.update:
|
||||
id: test_needle
|
||||
value: 5
|
||||
opa: 25%
|
||||
- lvgl.indicator.update:
|
||||
id: test_image
|
||||
opa: 75%
|
||||
|
||||
esp32:
|
||||
board: lolin_c3_mini
|
||||
|
||||
spi:
|
||||
mosi_pin:
|
||||
number: GPIO2
|
||||
ignore_strapping_warning: true
|
||||
clk_pin: GPIO1
|
||||
|
||||
display:
|
||||
- platform: mipi_spi
|
||||
data_rate: 20MHz
|
||||
model: st7735
|
||||
cs_pin:
|
||||
number: GPIO8
|
||||
ignore_strapping_warning: true
|
||||
dc_pin: GPIO3
|
||||
|
||||
image:
|
||||
- platform: file
|
||||
file: image.png
|
||||
id: meter_image
|
||||
resize: 8x8
|
||||
type: binary
|
||||
|
||||
lvgl:
|
||||
widgets:
|
||||
- meter:
|
||||
height: 100px
|
||||
width: 100px
|
||||
scales:
|
||||
- id: test_scale
|
||||
range_from: 0
|
||||
range_to: 10
|
||||
indicators:
|
||||
- arc:
|
||||
id: test_arc
|
||||
start_value: 0
|
||||
end_value: 5
|
||||
- line:
|
||||
id: test_needle
|
||||
- image:
|
||||
id: test_image
|
||||
src: meter_image
|
||||
@@ -0,0 +1,69 @@
|
||||
"""Canvas draw options must map onto the LVGL 9 draw descriptor fields."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.__main__ import generate_cpp_contents
|
||||
from esphome.automation import ACTION_REGISTRY
|
||||
import esphome.components.lvgl.widgets.canvas # noqa: F401 - registers the actions
|
||||
from esphome.config import read_config
|
||||
from esphome.core import CORE
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def main_cpp(request: pytest.FixtureRequest) -> str:
|
||||
config_path = (
|
||||
Path(request.fspath).parent / "config" / "canvas_draw_options_test.yaml"
|
||||
)
|
||||
original_path = CORE.config_path
|
||||
try:
|
||||
CORE.config_path = config_path
|
||||
CORE.config = read_config({})
|
||||
generate_cpp_contents(CORE.config)
|
||||
return CORE.cpp_main_section
|
||||
finally:
|
||||
CORE.config_path = original_path
|
||||
CORE.reset()
|
||||
|
||||
|
||||
def test_rect_gradient_is_copied_by_value(main_cpp: str) -> None:
|
||||
assert ".bg_grad = *test_grad;" in main_cpp
|
||||
|
||||
|
||||
def test_line_dash_fields(main_cpp: str) -> None:
|
||||
assert ".dash_width = 4;" in main_cpp
|
||||
assert ".dash_gap = 2;" in main_cpp
|
||||
|
||||
|
||||
def test_rect_opa_names_the_per_part_keys() -> None:
|
||||
with pytest.raises(cv.Invalid, match="bg_opa"):
|
||||
ACTION_REGISTRY["lvgl.canvas.draw_rectangle"].schema(
|
||||
{"id": "cv", "x": 0, "y": 0, "width": 10, "height": 10, "opa": "50%"}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"key", ["opa", "bg_color", "bg_grad", "radius", "shadow_width"]
|
||||
)
|
||||
def test_polygon_rejects_non_border_keys(key: str) -> None:
|
||||
with pytest.raises(cv.Invalid, match="drawn as an outline"):
|
||||
ACTION_REGISTRY["lvgl.canvas.draw_polygon"].schema(
|
||||
{"id": "cv", "points": [{"x": 0, "y": 0}], key: 1}
|
||||
)
|
||||
|
||||
|
||||
def test_polygon_accepts_border_keys() -> None:
|
||||
config = ACTION_REGISTRY["lvgl.canvas.draw_polygon"].schema(
|
||||
{
|
||||
"id": "cv",
|
||||
"points": [{"x": 0, "y": 0}],
|
||||
"border_color": 0xFF0000,
|
||||
"border_width": 2,
|
||||
"border_opa": "50%",
|
||||
}
|
||||
)
|
||||
assert config["border_width"] == 2
|
||||
@@ -0,0 +1,49 @@
|
||||
"""lvgl.indicator.update must apply opa to arc, needle and image indicators."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.__main__ import generate_cpp_contents
|
||||
from esphome.config import read_config
|
||||
from esphome.core import CORE
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def main_cpp(request: pytest.FixtureRequest) -> str:
|
||||
config_path = (
|
||||
Path(request.fspath).parent / "config" / "indicator_update_opa_test.yaml"
|
||||
)
|
||||
original_path = CORE.config_path
|
||||
try:
|
||||
CORE.config_path = config_path
|
||||
CORE.config = read_config({})
|
||||
generate_cpp_contents(CORE.config)
|
||||
return CORE.cpp_main_section
|
||||
finally:
|
||||
CORE.config_path = original_path
|
||||
CORE.reset()
|
||||
|
||||
|
||||
def test_arc_section_opa_is_applied(main_cpp: str) -> None:
|
||||
assert (
|
||||
"lv_style_set_arc_opa(meter_arc_test_arc_style, static_cast<uint8_t>(127.5f));"
|
||||
in main_cpp
|
||||
)
|
||||
assert "lv_obj_invalidate(test_scale);" in main_cpp
|
||||
|
||||
|
||||
def test_needle_opa_is_applied(main_cpp: str) -> None:
|
||||
assert (
|
||||
"lv_obj_set_style_opa(test_needle->obj, static_cast<uint8_t>(63.75f), LV_PART_MAIN);"
|
||||
in main_cpp
|
||||
)
|
||||
|
||||
|
||||
def test_image_opa_is_applied(main_cpp: str) -> None:
|
||||
assert (
|
||||
"lv_obj_set_style_opa(test_image, static_cast<uint8_t>(191.25f), LV_PART_MAIN);"
|
||||
in main_cpp
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
esphome:
|
||||
name: enable-pins-address
|
||||
|
||||
esp32:
|
||||
board: esp32-p4-evboard
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
psram:
|
||||
speed: 200MHz
|
||||
|
||||
esp_ldo:
|
||||
- channel: 3
|
||||
voltage: 2.5V
|
||||
|
||||
i2c:
|
||||
sda: GPIO7
|
||||
scl: GPIO8
|
||||
|
||||
pi4ioe5v6408:
|
||||
- id: pi4ioe1
|
||||
address: 0x43
|
||||
- id: pi4ioe2
|
||||
address: 0x44
|
||||
|
||||
# Takes over the default enable pin on the hub at 0x44, given by address
|
||||
switch:
|
||||
- platform: gpio
|
||||
id: takeover
|
||||
pin:
|
||||
pi4ioe5v6408:
|
||||
address: 0x44
|
||||
number: 1
|
||||
|
||||
display:
|
||||
- platform: mipi_dsi
|
||||
id: default_pins
|
||||
model: TEST-ENABLE-BY-ADDRESS
|
||||
@@ -0,0 +1,40 @@
|
||||
"""Tests for applying the default enable pins of mipi_dsi models."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.mipi import DriverChip
|
||||
|
||||
|
||||
def test_default_enable_pins_by_address(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_fixture_path: Callable[[str], Path],
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Default enable pins naming a hub by address resolve to that hub, and only
|
||||
the pin used elsewhere is left out, not the same pin number on the other hub."""
|
||||
from esphome.components.mipi_dsi.display import MODELS
|
||||
|
||||
models = set(DriverChip.models)
|
||||
model = MODELS["M5STACK-TAB5"].extend(
|
||||
"TEST-ENABLE-BY-ADDRESS",
|
||||
enable_pin=[
|
||||
{"pi4ioe5v6408": {"address": 0x43}, "number": 1},
|
||||
{"pi4ioe5v6408": {"address": 0x44}, "number": 1},
|
||||
],
|
||||
)
|
||||
for name in set(DriverChip.models) - models:
|
||||
monkeypatch.delitem(DriverChip.models, name)
|
||||
monkeypatch.setitem(MODELS, model.name, model)
|
||||
|
||||
main_cpp = generate_main(component_fixture_path("enable_pins_address.yaml"))
|
||||
|
||||
match = re.search(r"default_pins->set_enable_pins\(\{(.*)\}\);", main_cpp)
|
||||
assert match is not None
|
||||
pin_ids = match.group(1).split(", ")
|
||||
assert len(pin_ids) == 1
|
||||
assert f"{pin_ids[0]}->set_parent(pi4ioe1);" in main_cpp
|
||||
assert f"{pin_ids[0]}->set_pin(1);" in main_cpp
|
||||
@@ -8,7 +8,7 @@ import re
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.esp32 import KEY_BOARD, VARIANT_ESP32P4
|
||||
from esphome.components.esp32 import KEY_BOARD, VARIANT_ESP32P4, VARIANT_ESP32S3
|
||||
|
||||
# Importing the I/O expanders registers their pin schemas with
|
||||
# pins.PIN_SCHEMA_REGISTRY so that models whose defaults reference expander-backed
|
||||
@@ -215,3 +215,20 @@ def test_code_generation(
|
||||
assert "p4_86->set_rotation(display::DISPLAY_ROTATION_0_DEGREES);" not in main_cpp
|
||||
assert "custom_id->set_rotation(display::DISPLAY_ROTATION_180_DEGREES);" in main_cpp
|
||||
# assert "backlight_id = new light::LightState(mipi_dsi_dsibacklight_id);" in main_cpp
|
||||
|
||||
|
||||
def test_unsupported_variant_reported_before_requirements(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
"""On an unsupported chip the variant error is shown, not the missing components."""
|
||||
from esphome.components.mipi_dsi.display import CONFIG_SCHEMA
|
||||
from esphome.core import CORE
|
||||
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
platform_data={KEY_BOARD: "esp32-s3-devkitc-1", KEY_VARIANT: VARIANT_ESP32S3},
|
||||
)
|
||||
CORE.raw_config = {}
|
||||
|
||||
with pytest.raises(cv.Invalid, match=r"This feature is only available on ESP32P4"):
|
||||
CONFIG_SCHEMA({"model": "SEEED-RETERMINAL-D1001"})
|
||||
@@ -0,0 +1,32 @@
|
||||
esphome:
|
||||
name: enable-pins-test
|
||||
|
||||
esp32:
|
||||
board: esp32-s3-devkitc-1
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
psram:
|
||||
mode: octal
|
||||
speed: 80MHz
|
||||
|
||||
i2c:
|
||||
sda: 8
|
||||
scl: 9
|
||||
|
||||
ch422g:
|
||||
- id: ch422g_hub
|
||||
|
||||
# Takes over the model's default backlight enable pin
|
||||
switch:
|
||||
- platform: gpio
|
||||
id: backlight
|
||||
pin:
|
||||
ch422g: ch422g_hub
|
||||
number: 2
|
||||
|
||||
display:
|
||||
- platform: mipi_rgb
|
||||
id: default_pins
|
||||
model: ESP32-S3-TOUCH-LCD-7-800X480
|
||||
update_interval: never
|
||||
@@ -0,0 +1,24 @@
|
||||
esphome:
|
||||
name: enable-pins-test
|
||||
|
||||
esp32:
|
||||
board: esp32-s3-devkitc-1
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
psram:
|
||||
mode: octal
|
||||
speed: 80MHz
|
||||
|
||||
i2c:
|
||||
sda: 8
|
||||
scl: 9
|
||||
|
||||
ch422g:
|
||||
- id: ch422g_hub
|
||||
|
||||
display:
|
||||
- platform: mipi_rgb
|
||||
id: default_pins
|
||||
model: ESP32-S3-TOUCH-LCD-7-800X480
|
||||
update_interval: never
|
||||
@@ -0,0 +1,36 @@
|
||||
"""Tests for applying the default enable pins of mipi_rgb models."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
|
||||
def _enable_pin_numbers(main_cpp: str) -> list[int]:
|
||||
"""Get the expander pin numbers passed to set_enable_pins() in the generated code."""
|
||||
match = re.search(r"default_pins->set_enable_pins\(\{(.*)\}\);", main_cpp)
|
||||
assert match is not None
|
||||
numbers = []
|
||||
for pin_id in match.group(1).split(", "):
|
||||
pin = re.search(rf"{pin_id}->set_pin\((\d+)\);", main_cpp)
|
||||
assert pin is not None
|
||||
numbers.append(int(pin.group(1)))
|
||||
return numbers
|
||||
|
||||
|
||||
def test_default_enable_pins(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_fixture_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""All of the model's enable pins are applied when none are used elsewhere."""
|
||||
main_cpp = generate_main(component_fixture_path("enable_pins_default.yaml"))
|
||||
assert _enable_pin_numbers(main_cpp) == [2, 6]
|
||||
|
||||
|
||||
def test_default_enable_pin_used_elsewhere(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_fixture_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""A default enable pin used by a switch is left out instead of failing validation."""
|
||||
main_cpp = generate_main(component_fixture_path("enable_pins.yaml"))
|
||||
assert _enable_pin_numbers(main_cpp) == [6]
|
||||
assert "backlight->set_pin(" in main_cpp
|
||||
@@ -8,7 +8,12 @@ from typing import Any
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.esp32 import KEY_BOARD, KEY_VARIANT, VARIANT_ESP32S3
|
||||
from esphome.components.esp32 import (
|
||||
KEY_BOARD,
|
||||
KEY_VARIANT,
|
||||
VARIANT_ESP32C3,
|
||||
VARIANT_ESP32S3,
|
||||
)
|
||||
from esphome.components.mipi_rgb.display import CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA
|
||||
|
||||
# Importing pca9554 registers its pin schema with pins.PIN_SCHEMA_REGISTRY so that
|
||||
@@ -87,3 +92,78 @@ def test_model_requires_psram_and_expander(
|
||||
"spi_id": "spi_bus",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_model_requires_expander_when_pins_overridden(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
set_component_config: Any,
|
||||
) -> None:
|
||||
"""A model with default pins on an expander requires it even when the user config
|
||||
overrides all of those pins, and without the expander being listed in ``requires``."""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
platform_data={KEY_BOARD: "esp32-s3-devkitc-1", KEY_VARIANT: VARIANT_ESP32S3},
|
||||
)
|
||||
set_component_config("psram", True)
|
||||
CORE.raw_config = {"psram": True}
|
||||
|
||||
with pytest.raises(
|
||||
cv.Invalid,
|
||||
match=r"ESP32-S3-TOUCH-LCD-7-800X480 requires component 'ch422g' to be configured",
|
||||
):
|
||||
_validated(
|
||||
{
|
||||
"model": "ESP32-S3-TOUCH-LCD-7-800X480",
|
||||
"reset_pin": 10,
|
||||
"enable_pin": [11, 12],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_model_requires_expander_with_default_pins(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
set_component_config: Any,
|
||||
) -> None:
|
||||
"""The missing expander is reported before the model's default pins on it are validated."""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
platform_data={KEY_BOARD: "esp32-s3-devkitc-1", KEY_VARIANT: VARIANT_ESP32S3},
|
||||
)
|
||||
set_component_config("psram", True)
|
||||
CORE.raw_config = {"psram": True}
|
||||
|
||||
with pytest.raises(
|
||||
cv.Invalid,
|
||||
match=r"ESP32-S3-TOUCH-LCD-7-800X480 requires component 'ch422g' to be configured",
|
||||
):
|
||||
_validated({"model": "ESP32-S3-TOUCH-LCD-7-800X480"})
|
||||
|
||||
|
||||
def test_unsupported_variant_reported_before_requirements(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
"""On an unsupported chip the variant error is shown, not the missing components."""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
platform_data={KEY_BOARD: "esp32-c3-devkitm-1", KEY_VARIANT: VARIANT_ESP32C3},
|
||||
)
|
||||
CORE.raw_config = {}
|
||||
|
||||
with pytest.raises(
|
||||
cv.Invalid,
|
||||
match=r"This feature is only available on ESP32S3, ESP32P4, ESP32S31",
|
||||
):
|
||||
_validated({"model": "ESP32-S3-TOUCH-LCD-7-800X480"})
|
||||
|
||||
|
||||
def test_expander_that_fails_to_load_is_still_required(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""An expander component that exists but fails to load is still counted as required."""
|
||||
from esphome import loader, pins
|
||||
from esphome.components import mipi
|
||||
|
||||
monkeypatch.setattr(pins, "PIN_SCHEMA_REGISTRY", pins.PinRegistry())
|
||||
monkeypatch.setattr(loader, "get_component", lambda key: None)
|
||||
defaults = {"enable_pin": {"xl9535": "hub", "number": 1, "mode": "output"}}
|
||||
assert mipi._pin_expanders(defaults) == {"xl9535"}
|
||||
@@ -36,6 +36,30 @@ esphome:
|
||||
service: notify.html5
|
||||
data:
|
||||
message: Button was pressed
|
||||
- homeassistant.service:
|
||||
service: !lambda 'return std::string("notify.html5");'
|
||||
data:
|
||||
title: Title
|
||||
data_template:
|
||||
message: The value is {{ my_value }}
|
||||
variables:
|
||||
my_value: !lambda 'return std::string("42");'
|
||||
capture_response: true
|
||||
response_template: !lambda 'return std::string("{{ response.value }}");'
|
||||
on_success:
|
||||
- logger.log: "service succeeded"
|
||||
on_error:
|
||||
- logger.log:
|
||||
format: "service failed: %s"
|
||||
args: [error.c_str()]
|
||||
- homeassistant.action:
|
||||
action: weather.get_forecasts
|
||||
data:
|
||||
entity_id: weather.forecast_home
|
||||
capture_response: true
|
||||
response_template: !lambda 'return std::string("{{ response }}");'
|
||||
on_success:
|
||||
- logger.log: "forecast received"
|
||||
- homeassistant.tag_scanned: pulse
|
||||
- homeassistant.action:
|
||||
action: weather.get_forecasts
|
||||
@@ -307,6 +331,13 @@ api:
|
||||
success: false
|
||||
error_message: !lambda 'return error_msg;'
|
||||
|
||||
- action: test_respond_lambda_success
|
||||
variables:
|
||||
ok: bool
|
||||
then:
|
||||
- api.respond:
|
||||
success: !lambda 'return ok;'
|
||||
|
||||
# ==========================================================================
|
||||
# supports_response: optional (auto-detected - api.respond with data)
|
||||
# Has call_id and return_response - client decides if it wants response
|
||||
|
||||
@@ -21,6 +21,14 @@ esphome:
|
||||
id: waveradar
|
||||
frequency: 5800MHz
|
||||
power_consumption: 91uA
|
||||
- at581x.settings:
|
||||
id: waveradar
|
||||
hw_frontend_reset: !lambda return true;
|
||||
sensing_distance: !lambda return 300;
|
||||
protect_time: !lambda return 1500;
|
||||
trigger_base: !lambda return 600;
|
||||
trigger_keep: !lambda return 12000;
|
||||
stage_gain: !lambda return 4;
|
||||
- at581x.reset:
|
||||
id: waveradar
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ esphome:
|
||||
- bk72xx_ble_tracker.start_scan
|
||||
- bk72xx_ble_tracker.start_scan:
|
||||
continuous: true
|
||||
- bk72xx_ble_tracker.start_scan:
|
||||
continuous: !lambda return false;
|
||||
- bk72xx_ble_tracker.stop_scan
|
||||
- bk72xx_ble_tracker.stop_scan: ble_tracker
|
||||
|
||||
|
||||
@@ -109,3 +109,10 @@ button:
|
||||
- logger.log: Level calibration failed
|
||||
- motion.calibrate_heading:
|
||||
id: bmi270_motion
|
||||
- motion.calibrate_heading:
|
||||
id: bmi270_motion
|
||||
save: true
|
||||
on_success:
|
||||
- logger.log: Heading calibrated
|
||||
on_error:
|
||||
- logger.log: Heading calibration failed
|
||||
@@ -9,6 +9,49 @@ esphome:
|
||||
id: bq25186_charger
|
||||
battery_regulation_voltage: 4.1V
|
||||
fast_charge_current: 200mA
|
||||
- bq25186.apply_configuration:
|
||||
id: bq25186_charger
|
||||
charge_enabled: false
|
||||
flash_charging_mode: true
|
||||
precharge_is_iterm: true
|
||||
termination_percent: 5%
|
||||
vindpm_mode: 4.7v
|
||||
thermal_regulation: 80c
|
||||
battery_ocp_limit: 1500ma
|
||||
battery_uvlo: 3.0v
|
||||
mask_charge_status_interrupt: false
|
||||
mask_ilim_interrupt: false
|
||||
mask_vindpm_interrupt: true
|
||||
ts_auto_function: false
|
||||
vlowv: 2.8v
|
||||
recharge_threshold: 200mv
|
||||
double_timer_during_dpm: true
|
||||
safety_timer: 12h
|
||||
watchdog: 160s_hardware_reset
|
||||
long_press_duration: 15s
|
||||
hw_reset_requires_vin: true
|
||||
autowake: 2s
|
||||
input_current_limit: 200ma
|
||||
- bq25186.apply_configuration:
|
||||
id: bq25186_charger
|
||||
push_button_long_press_action: shutdown_mode
|
||||
wake1_timer: 1s
|
||||
wake2_timer: 3s
|
||||
enable_push_button: false
|
||||
system_regulation: 4.6v
|
||||
sys_mode: battery_only
|
||||
watchdog_15s_enable: false
|
||||
disable_vdppm: true
|
||||
ts_hot: 65c
|
||||
ts_cold: 3c
|
||||
ts_warm_disable: true
|
||||
ts_cool_disable: true
|
||||
ts_ichg: 0.2x
|
||||
ts_vrcg: -200mv
|
||||
mask_ts_interrupt: true
|
||||
mask_treg_interrupt: false
|
||||
mask_bat_interrupt: true
|
||||
mask_pg_interrupt: true
|
||||
|
||||
bq25186:
|
||||
- id: bq25186_charger
|
||||
|
||||
@@ -14,6 +14,9 @@ button:
|
||||
id: bms
|
||||
address: 0x09
|
||||
- bq76972.program_i2c_address: !lambda "return 0x08;"
|
||||
- bq76972.program_i2c_address:
|
||||
id: bms
|
||||
address: !lambda return 0x0A;
|
||||
|
||||
sensor:
|
||||
- platform: bq76972
|
||||
|
||||
@@ -69,3 +69,7 @@ button:
|
||||
- canbus.send: !lambda |-
|
||||
uint8_t val = (uint8_t)id(canbus_test_number).state;
|
||||
return std::vector<uint8_t>{0xAA, val, 0xBB};
|
||||
- canbus.send:
|
||||
can_id: 0x123
|
||||
remote_transmission_request: true
|
||||
data: []
|
||||
@@ -5,6 +5,8 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
namespace esphome::core::testing {
|
||||
@@ -41,4 +43,40 @@ TEST(Scheduler, TimeoutAddedByACancelledCallbackIsVisibleAfterCall) {
|
||||
scheduler.trim_freelist();
|
||||
}
|
||||
|
||||
// A cancelled timeout of a component that has since failed has to be counted as removed when call()
|
||||
// reaches it, or the leftover count makes the next few cancellations trigger a full cleanup early.
|
||||
TEST(Scheduler, CancelledItemOfFailedComponentIsCountedAsRemoved) {
|
||||
Scheduler scheduler;
|
||||
SchedulerTestComponent healthy;
|
||||
SchedulerTestComponent failing;
|
||||
|
||||
// The healthy item is due first, so cleanup_() stops at it and the cancelled item is only reached in the run loop
|
||||
scheduler.set_timeout(&healthy, "first", 1, []() {});
|
||||
scheduler.set_timeout(&failing, "cancelled", 2, []() {});
|
||||
scheduler.process_to_add();
|
||||
scheduler.cancel_timeout(&failing, "cancelled");
|
||||
failing.mark_failed();
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||||
scheduler.call(millis());
|
||||
|
||||
// Four cancelled items behind a live one stay below the full cleanup threshold of five, so their callbacks are
|
||||
// released only once they reach the top of the heap
|
||||
auto token = std::make_shared<int>(0);
|
||||
scheduler.set_timeout(&healthy, "keep", 10000, []() {});
|
||||
static constexpr const char *NAMES[] = {"c0", "c1", "c2", "c3"};
|
||||
for (const char *name : NAMES)
|
||||
scheduler.set_timeout(&healthy, name, 20000, [token]() {});
|
||||
scheduler.process_to_add();
|
||||
for (const char *name : NAMES)
|
||||
scheduler.cancel_timeout(&healthy, name);
|
||||
ASSERT_EQ(token.use_count(), 5);
|
||||
scheduler.call(millis());
|
||||
EXPECT_EQ(token.use_count(), 5);
|
||||
|
||||
scheduler.cancel_timeout(&healthy, "keep");
|
||||
scheduler.call(millis());
|
||||
EXPECT_EQ(token.use_count(), 1);
|
||||
scheduler.trim_freelist();
|
||||
}
|
||||
|
||||
} // namespace esphome::core::testing
|
||||
@@ -5,6 +5,9 @@ esphome:
|
||||
id: counter_sensor
|
||||
value: 100
|
||||
- counter.increment: counter_sensor
|
||||
- counter.set_value:
|
||||
id: counter_sensor
|
||||
value: !lambda return 42LL;
|
||||
- counter.increment:
|
||||
id: counter_sensor
|
||||
value: -5
|
||||
|
||||
@@ -14,6 +14,8 @@ esphome:
|
||||
then:
|
||||
- deep_sleep.enter:
|
||||
sleep_duration: 60min
|
||||
- deep_sleep.enter:
|
||||
sleep_duration: !lambda return 60000;
|
||||
- if:
|
||||
condition:
|
||||
lambda: 'return deep_sleep::woke_from_deep_sleep();'
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
# (the default on ESP-IDF). Entering deep sleep runs the safe shutdown hooks,
|
||||
# where safe_mode confirms the running app image so the bootloader does not
|
||||
# roll back a fresh OTA update when the device goes to sleep before
|
||||
# boot_is_good_after has elapsed.
|
||||
# boot_is_good_after has elapsed. The network this needs also serves the
|
||||
# deep_sleep.enter until/time_id case, which requires a time source.
|
||||
substitutions:
|
||||
wakeup_pin: GPIO4
|
||||
|
||||
@@ -17,3 +18,16 @@ wifi:
|
||||
ota:
|
||||
- platform: esphome
|
||||
password: "superlongpasswordthatnoonewillknow"
|
||||
|
||||
time:
|
||||
- platform: sntp
|
||||
id: sntp_time
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: Sleep until
|
||||
on_press:
|
||||
- deep_sleep.enter:
|
||||
id: deep_sleep_1
|
||||
until: "06:30:00"
|
||||
time_id: sntp_time
|
||||
@@ -27,6 +27,12 @@ esphome:
|
||||
- dfplayer.set_volume: !lambda "return id(dfplayer_test).is_playing() ? 10 : 5;"
|
||||
- dfplayer.set_eq: ROCK
|
||||
- dfplayer.set_eq: !lambda "return id(dfplayer_test).is_playing() ? dfplayer::JAZZ : dfplayer::BASS;"
|
||||
- dfplayer.play_mp3:
|
||||
file: 7
|
||||
- dfplayer.set_volume:
|
||||
volume: 12
|
||||
- dfplayer.set_eq:
|
||||
eq_preset: CLASSIC
|
||||
- dfplayer.play_next
|
||||
- dfplayer.play_previous
|
||||
- dfplayer.reset
|
||||
|
||||
@@ -12,6 +12,10 @@ esphome:
|
||||
delay_after_detect: 0s
|
||||
delay_after_disappear: 0s
|
||||
sensitivity: 6
|
||||
- dfrobot_sen0395.settings:
|
||||
id: mmwave
|
||||
factory_reset: !lambda return false;
|
||||
sensitivity: !lambda return 7;
|
||||
- dfrobot_sen0395.reset
|
||||
|
||||
dfrobot_sen0395:
|
||||
|
||||
Loaded 100 of 173 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user