mirror of
https://github.com/esphome/esphome.git
synced 2026-10-25 23:42:15 +00:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c5059818d | ||
|
|
2c7e9348c1 | ||
|
|
f0c3c35800 | ||
|
|
ee99bab08b | ||
|
|
6c1a7642f3 | ||
|
|
1ce0db00b3 | ||
|
|
a50a30414e |
No files matched your search
@@ -2,18 +2,24 @@ 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
|
||||
from .. import Number, number_ns
|
||||
|
||||
CONFIG_SCHEMA = sensor.sensor_schema(sensor.Sensor).extend(
|
||||
{
|
||||
cv.Required(CONF_SOURCE_ID): cv.use_id(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)
|
||||
)
|
||||
|
||||
|
||||
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]))
|
||||
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)
|
||||
@@ -0,0 +1,16 @@
|
||||
#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
|
||||
@@ -0,0 +1,19 @@
|
||||
#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
|
||||
@@ -15,7 +15,9 @@ from esphome.const import (
|
||||
CONF_MIN_POWER,
|
||||
CONF_POWER_SUPPLY,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.core import CORE, ID
|
||||
from esphome.core.entity_helpers import ApplyEntity, SubEntities
|
||||
from esphome.cpp_generator import MockObj, SafeExpType
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
@@ -57,6 +59,8 @@ BinaryOutput = output_ns.class_("BinaryOutput")
|
||||
BinaryOutputPtr = BinaryOutput.operator("ptr")
|
||||
FloatOutput = output_ns.class_("FloatOutput", BinaryOutput)
|
||||
FloatOutputPtr = FloatOutput.operator("ptr")
|
||||
ApplyBinaryOutput = output_ns.class_("ApplyBinaryOutput", BinaryOutput)
|
||||
ApplyFloatOutput = output_ns.class_("ApplyFloatOutput", FloatOutput)
|
||||
|
||||
|
||||
async def setup_output_platform_(obj, config):
|
||||
@@ -87,6 +91,39 @@ async def register_output(var, config):
|
||||
await setup_output_platform_(var, config)
|
||||
|
||||
|
||||
async def new_output(config: ConfigType, *args: SafeExpType) -> MockObj:
|
||||
var = cg.new_Pvariable(config[CONF_ID], *args)
|
||||
await register_output(var, config)
|
||||
return var
|
||||
|
||||
|
||||
BINARY_APPLY_ENTITY = ApplyEntity(
|
||||
ApplyBinaryOutput,
|
||||
((BinaryOutputPtr, "entity"), (cg.uint16, "index"), (cg.bool_, "state")),
|
||||
)
|
||||
FLOAT_APPLY_ENTITY = ApplyEntity(
|
||||
ApplyFloatOutput,
|
||||
((FloatOutputPtr, "entity"), (cg.uint16, "index"), (cg.float_, "state")),
|
||||
)
|
||||
|
||||
|
||||
def sub_binary_outputs(
|
||||
config: ConfigType, *, parent: MockObj | ID | None = None
|
||||
) -> SubEntities:
|
||||
"""Return a SubEntities bound to new_output; ``apply`` can use ``entity``, ``index`` and ``state``.
|
||||
|
||||
``index`` is the constructor argument (default 0), so a hub's channels share one class.
|
||||
"""
|
||||
return SubEntities(new_output, config, parent, BINARY_APPLY_ENTITY)
|
||||
|
||||
|
||||
def sub_float_outputs(
|
||||
config: ConfigType, *, parent: MockObj | ID | None = None
|
||||
) -> SubEntities:
|
||||
"""Like sub_binary_outputs; ``apply`` gets the scaled level as ``state``."""
|
||||
return SubEntities(new_output, config, parent, FLOAT_APPLY_ENTITY)
|
||||
|
||||
|
||||
BINARY_OUTPUT_ACTION_SCHEMA = maybe_simple_id(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(BinaryOutput),
|
||||
|
||||
@@ -68,4 +68,16 @@ class BinaryOutput {
|
||||
#endif
|
||||
};
|
||||
|
||||
/// Calls the codegen-generated Fn(this, index, state) on a state write; the hub is baked into Fn.
|
||||
/// The index (a hub's channel) is data, so all channels of a hub share one instantiation.
|
||||
template<auto Fn> class ApplyBinaryOutput final : public BinaryOutput {
|
||||
public:
|
||||
explicit ApplyBinaryOutput(uint16_t index = 0) : index_(index) {}
|
||||
|
||||
protected:
|
||||
void write_state(bool state) override { Fn(this, this->index_, state); }
|
||||
|
||||
uint16_t index_; // Fits in the base's tail padding
|
||||
};
|
||||
|
||||
} // namespace esphome::output
|
||||
@@ -130,4 +130,16 @@ class FloatOutput : public BinaryOutput {
|
||||
#endif
|
||||
};
|
||||
|
||||
/// Calls the codegen-generated Fn(this, index, state) with the scaled, inverted level; the hub is baked into Fn.
|
||||
/// The index (a hub's channel) is data, so all channels of a hub share one instantiation.
|
||||
template<auto Fn> class ApplyFloatOutput final : public FloatOutput {
|
||||
public:
|
||||
explicit ApplyFloatOutput(uint16_t index = 0) : index_(index) {}
|
||||
|
||||
protected:
|
||||
void write_state(float state) override { Fn(this, this->index_, state); }
|
||||
|
||||
uint16_t index_; // Fits in the base's tail padding
|
||||
};
|
||||
|
||||
} // namespace esphome::output
|
||||
@@ -8,8 +8,6 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <new>
|
||||
|
||||
#ifdef USE_ESP_IDF
|
||||
#include "esp_task_wdt.h"
|
||||
#endif
|
||||
@@ -96,38 +94,7 @@ 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_) {
|
||||
@@ -138,22 +105,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;
|
||||
@@ -161,12 +128,11 @@ 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;
|
||||
@@ -189,17 +155,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");
|
||||
@@ -223,7 +189,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();
|
||||
@@ -252,10 +218,6 @@ 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;
|
||||
@@ -266,19 +228,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;
|
||||
}
|
||||
@@ -286,7 +248,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_();
|
||||
@@ -297,7 +259,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,10 +18,9 @@ class JpegDecoder : public ImageDecoder {
|
||||
*
|
||||
* @param image The RuntimeImage to decode the stream into.
|
||||
*/
|
||||
JpegDecoder(RuntimeImage *image);
|
||||
~JpegDecoder() override;
|
||||
JpegDecoder(RuntimeImage *image) : ImageDecoder(image, JPEG) {}
|
||||
~JpegDecoder() override {}
|
||||
|
||||
int prepare(size_t expected_size) override;
|
||||
int HOT decode(uint8_t *buffer, size_t size) override;
|
||||
void reset() override;
|
||||
|
||||
@@ -47,8 +46,7 @@ class JpegDecoder : public ImageDecoder {
|
||||
static int32_t source_seek(JPEGFILE *file, int32_t position);
|
||||
static void source_close(void *handle) {}
|
||||
|
||||
/// About 18 KB of decoder state, allocated like pngle in PngDecoder: PSRAM first, nullptr if out of memory
|
||||
JPEGDEC *jpeg_{nullptr};
|
||||
JPEGDEC jpeg_{};
|
||||
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};
|
||||
|
||||
@@ -2,22 +2,29 @@ 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
|
||||
from .. import Switch, switch_ns
|
||||
|
||||
CODEOWNERS = ["@ssieb"]
|
||||
|
||||
_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),
|
||||
}
|
||||
SwitchBinarySensor = switch_ns.class_(
|
||||
"SwitchBinarySensor", binary_sensor.BinarySensor, cg.Component
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
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):
|
||||
var = await binary_sensor.new_binary_sensor(config)
|
||||
_FOLLOW_SWITCH(var, await cg.get_variable(config[CONF_SOURCE_ID]))
|
||||
await cg.register_component(var, config)
|
||||
|
||||
await cg.add_variable(var.set_source, config[CONF_SOURCE_ID])
|
||||
@@ -0,0 +1,15 @@
|
||||
#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_
|
||||
@@ -0,0 +1,19 @@
|
||||
#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,16 +4,23 @@ import esphome.config_validation as cv
|
||||
from esphome.const import CONF_SOURCE_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import Text
|
||||
from .. import Text, text_ns
|
||||
|
||||
CONFIG_SCHEMA = text_sensor.text_sensor_schema(text_sensor.TextSensor).extend(
|
||||
{
|
||||
cv.Required(CONF_SOURCE_ID): cv.use_id(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)
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
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]))
|
||||
source = await cg.get_variable(config[CONF_SOURCE_ID])
|
||||
var = await text_sensor.new_text_sensor(config, source)
|
||||
await cg.register_component(var, config)
|
||||
@@ -0,0 +1,16 @@
|
||||
#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
|
||||
@@ -0,0 +1,19 @@
|
||||
#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
|
||||
@@ -483,6 +483,13 @@ 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
|
||||
@@ -507,13 +514,6 @@ 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;
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
namespace esphome::core::testing {
|
||||
@@ -43,40 +41,4 @@ 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
|
||||
@@ -0,0 +1,11 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.types import ConfigType
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# Builds the min/max power scaling so the Apply output tests cover it
|
||||
async def to_code_testing(config: ConfigType) -> None:
|
||||
cg.add_define("USE_OUTPUT_FLOAT_POWER_SCALING")
|
||||
|
||||
manifest.to_code = to_code_testing
|
||||
@@ -0,0 +1,81 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/components/output/binary_output.h"
|
||||
#include "esphome/components/output/float_output.h"
|
||||
|
||||
namespace esphome::output::testing {
|
||||
|
||||
static void *last_output = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
static uint16_t last_index = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
static std::vector<bool> bool_writes; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
static std::vector<float> levels; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
static void on_binary_write(BinaryOutput *output, uint16_t index, bool state) {
|
||||
last_output = output;
|
||||
last_index = index;
|
||||
bool_writes.push_back(state);
|
||||
}
|
||||
|
||||
static void on_float_write(FloatOutput *output, uint16_t index, float state) {
|
||||
last_output = output;
|
||||
last_index = index;
|
||||
levels.push_back(state);
|
||||
}
|
||||
|
||||
// The index sits in the base's tail padding; only the power supply requester leaves none.
|
||||
#ifndef USE_POWER_SUPPLY
|
||||
static_assert(sizeof(ApplyBinaryOutput<on_binary_write>) == sizeof(BinaryOutput));
|
||||
static_assert(sizeof(ApplyFloatOutput<on_float_write>) == sizeof(FloatOutput));
|
||||
#endif
|
||||
|
||||
static void reset() {
|
||||
last_output = nullptr;
|
||||
last_index = 0;
|
||||
bool_writes.clear();
|
||||
levels.clear();
|
||||
}
|
||||
|
||||
TEST(ApplyBinaryOutput, WritesReachTheFunctionWithIndexAndInversion) {
|
||||
reset();
|
||||
ApplyBinaryOutput<on_binary_write> out(7);
|
||||
out.turn_on();
|
||||
out.turn_off();
|
||||
out.set_inverted(true);
|
||||
out.turn_on();
|
||||
EXPECT_EQ(bool_writes, (std::vector<bool>{true, false, false}));
|
||||
EXPECT_EQ(last_output, &out);
|
||||
EXPECT_EQ(last_index, 7);
|
||||
}
|
||||
|
||||
TEST(ApplyFloatOutput, WritesReachTheFunctionWithIndexInversionAndClamp) {
|
||||
reset();
|
||||
ApplyFloatOutput<on_float_write> out(3);
|
||||
out.set_level(0.25f);
|
||||
out.turn_on();
|
||||
out.set_inverted(true);
|
||||
out.set_level(0.25f);
|
||||
out.set_level(2.0f);
|
||||
EXPECT_EQ(levels, (std::vector<float>{0.25f, 1.0f, 0.75f, 0.0f}));
|
||||
EXPECT_EQ(last_output, &out);
|
||||
EXPECT_EQ(last_index, 3);
|
||||
}
|
||||
|
||||
#ifdef USE_OUTPUT_FLOAT_POWER_SCALING
|
||||
TEST(ApplyFloatOutput, PowerScalingIsAppliedBeforeTheFunction) {
|
||||
reset();
|
||||
ApplyFloatOutput<on_float_write> out;
|
||||
out.set_min_power(0.2f);
|
||||
out.set_max_power(0.8f);
|
||||
out.set_zero_means_zero(true);
|
||||
out.set_level(0.5f);
|
||||
out.turn_off();
|
||||
ASSERT_EQ(levels.size(), 2u);
|
||||
EXPECT_FLOAT_EQ(levels[0], 0.5f);
|
||||
EXPECT_EQ(levels[1], 0.0f);
|
||||
EXPECT_EQ(last_index, 0);
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace esphome::output::testing
|
||||
@@ -1,39 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "esphome/components/runtime_image/jpeg_decoder.h"
|
||||
#include "esphome/components/runtime_image/runtime_image.h"
|
||||
|
||||
#ifdef USE_RUNTIME_IMAGE_JPEG
|
||||
|
||||
namespace esphome::runtime_image::testing {
|
||||
|
||||
// A decoder whose JPEGDEC state is missing, as when its allocation fails. The state is handed back before the base
|
||||
// destructor runs, so the teardown stays with JpegDecoder.
|
||||
class StatelessJpegDecoder : public JpegDecoder {
|
||||
public:
|
||||
explicit StatelessJpegDecoder(RuntimeImage *image) : JpegDecoder(image), state_(this->jpeg_) {
|
||||
this->jpeg_ = nullptr;
|
||||
}
|
||||
~StatelessJpegDecoder() override { this->jpeg_ = this->state_; }
|
||||
|
||||
protected:
|
||||
JPEGDEC *state_;
|
||||
};
|
||||
|
||||
// The ~18 KB of JPEGDEC state lives in its own allocation, so it can come from PSRAM
|
||||
TEST(JpegOutOfMemory, DecoderStateIsAllocatedSeparately) { EXPECT_LT(sizeof(JpegDecoder), sizeof(JPEGDEC) / 4); }
|
||||
|
||||
TEST(JpegOutOfMemory, MissingStateFailsWithoutStartingASession) {
|
||||
RuntimeImage img(JPEG, image::IMAGE_TYPE_RGB565, image::TRANSPARENCY_OPAQUE);
|
||||
StatelessJpegDecoder decoder(&img);
|
||||
EXPECT_EQ(decoder.prepare(100), DECODE_ERROR_OUT_OF_MEMORY);
|
||||
EXPECT_FALSE(decoder.is_active());
|
||||
uint8_t data[4] = {};
|
||||
EXPECT_EQ(decoder.decode(data, sizeof(data)), DECODE_ERROR_OUT_OF_MEMORY);
|
||||
EXPECT_EQ(decoder.begin_source(nullptr), DECODE_ERROR_OUT_OF_MEMORY);
|
||||
decoder.reset();
|
||||
}
|
||||
|
||||
} // namespace esphome::runtime_image::testing
|
||||
|
||||
#endif // USE_RUNTIME_IMAGE_JPEG
|
||||
@@ -1,56 +0,0 @@
|
||||
esphome:
|
||||
name: test-source-entity-follow
|
||||
|
||||
host:
|
||||
api:
|
||||
batch_delay: 0ms
|
||||
logger:
|
||||
|
||||
switch:
|
||||
- platform: template
|
||||
id: source_switch
|
||||
name: Source Switch
|
||||
optimistic: true
|
||||
restore_mode: ALWAYS_ON
|
||||
# Publishes nothing until commanded
|
||||
- platform: template
|
||||
id: quiet_switch
|
||||
name: Quiet Switch
|
||||
optimistic: true
|
||||
restore_mode: DISABLED
|
||||
|
||||
number:
|
||||
- platform: template
|
||||
id: source_number
|
||||
name: Source Number
|
||||
optimistic: true
|
||||
min_value: 0
|
||||
max_value: 10
|
||||
step: 0.5
|
||||
initial_value: 4
|
||||
|
||||
text:
|
||||
- platform: template
|
||||
id: source_text
|
||||
name: Source Text
|
||||
optimistic: true
|
||||
mode: text
|
||||
initial_value: hello
|
||||
|
||||
binary_sensor:
|
||||
- platform: switch
|
||||
source_id: source_switch
|
||||
name: Switch State
|
||||
- platform: switch
|
||||
source_id: quiet_switch
|
||||
name: Quiet Switch State
|
||||
|
||||
sensor:
|
||||
- platform: number
|
||||
source_id: source_number
|
||||
name: Number Value
|
||||
|
||||
text_sensor:
|
||||
- platform: text
|
||||
source_id: source_text
|
||||
name: Text Value
|
||||
@@ -1,71 +0,0 @@
|
||||
"""Integration test for the switch binary sensor, number sensor and text text sensor platforms."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from aioesphomeapi import (
|
||||
BinarySensorState,
|
||||
EntityState,
|
||||
NumberInfo,
|
||||
SensorState,
|
||||
SwitchInfo,
|
||||
TextInfo,
|
||||
TextSensorState,
|
||||
)
|
||||
import pytest
|
||||
|
||||
from .state_utils import require_entity, subscribe_after_initial_states
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
INITIAL: dict[str, tuple[type[EntityState], float | bool | str | None]] = {
|
||||
"switch_state": (BinarySensorState, True),
|
||||
# A switch that has not published leaves its binary sensor unknown
|
||||
"quiet_switch_state": (BinarySensorState, None),
|
||||
"number_value": (SensorState, 4.0),
|
||||
"text_value": (TextSensorState, "hello"),
|
||||
}
|
||||
UPDATED: dict[str, tuple[type[EntityState], float | bool | str | None]] = {
|
||||
"switch_state": (BinarySensorState, False),
|
||||
"quiet_switch_state": (BinarySensorState, True),
|
||||
"number_value": (SensorState, 6.5),
|
||||
"text_value": (TextSensorState, "bye"),
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_source_entity_follow(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Each entity holds its source's state from boot, then follows it."""
|
||||
async with run_compiled(yaml_config), api_client_connected() as client:
|
||||
entities, _ = await client.list_entities_services()
|
||||
keys = {
|
||||
name: require_entity(entities, name, exact=True).key for name in INITIAL
|
||||
}
|
||||
switch = require_entity(entities, "source_switch", SwitchInfo)
|
||||
quiet = require_entity(entities, "quiet_switch", SwitchInfo)
|
||||
number = require_entity(entities, "source_number", NumberInfo)
|
||||
text = require_entity(entities, "source_text", TextInfo)
|
||||
|
||||
sub = await subscribe_after_initial_states(client, entities)
|
||||
for name, (cls, state) in INITIAL.items():
|
||||
key = keys[name]
|
||||
expected = (
|
||||
cls(key=key, missing_state=True)
|
||||
if state is None
|
||||
else cls(key=key, state=state)
|
||||
)
|
||||
assert sub.initial_states[key] == expected, name
|
||||
|
||||
names = list(UPDATED)
|
||||
waits = [sub.waiter.next_state(keys[name]) for name in names]
|
||||
client.switch_command(switch.key, False)
|
||||
client.switch_command(quiet.key, True)
|
||||
client.number_command(number.key, 6.5)
|
||||
client.text_command(text.key, "bye")
|
||||
for name, got in zip(names, await asyncio.gather(*waits), strict=True):
|
||||
cls, state = UPDATED[name]
|
||||
assert got == cls(key=keys[name], state=state), name
|
||||
@@ -11,7 +11,15 @@ import pytest
|
||||
|
||||
from esphome import final_validate as fv
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import button, number, select, sensor, switch, text_sensor
|
||||
from esphome.components import (
|
||||
button,
|
||||
number,
|
||||
output,
|
||||
select,
|
||||
sensor,
|
||||
switch,
|
||||
text_sensor,
|
||||
)
|
||||
from esphome.config import Config
|
||||
import esphome.config_validation as cv
|
||||
from esphome.config_validation import Invalid
|
||||
@@ -1663,6 +1671,30 @@ async def test_sub_entities_apply_shares_equal_functions(
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sub_outputs_share_one_function_per_hub(
|
||||
setup_test_environment: list[str], apply_functions: list[tuple[str, str, str]]
|
||||
) -> None:
|
||||
"""A hub's channels pass their channel as the index, so they share one function."""
|
||||
calls: list[tuple[Any, ...]] = []
|
||||
cls = output.ApplyFloatOutput
|
||||
config = {"a": _apply_conf("ch0", cls), "b": _apply_conf("ch5", cls)}
|
||||
sub = _apply_sub(calls, config, output.FLOAT_APPLY_ENTITY)
|
||||
await sub("a", None, 0, apply="write_channel(index, state)")
|
||||
await sub("b", None, 5, apply="write_channel(index, state)")
|
||||
assert calls == [
|
||||
("ch0", "<esphome__ch0__fn>", "0"),
|
||||
("ch5", "<esphome__ch0__fn>", "5"),
|
||||
]
|
||||
assert apply_functions == [
|
||||
(
|
||||
"esphome__ch0__fn",
|
||||
"output::FloatOutput * entity, uint16_t index, float state",
|
||||
"::hub->write_channel(index, state);",
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sub_entities_apply_parent_id(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
@@ -1838,6 +1870,18 @@ async def test_sub_entities_apply_without_parent_shares_equal_functions(
|
||||
"select::Select * entity, size_t index",
|
||||
"entity->publish_state(index);",
|
||||
),
|
||||
(
|
||||
output.sub_binary_outputs,
|
||||
output.ApplyBinaryOutput,
|
||||
"output::BinaryOutput * entity, uint16_t index, bool state",
|
||||
None,
|
||||
),
|
||||
(
|
||||
output.sub_float_outputs,
|
||||
output.ApplyFloatOutput,
|
||||
"output::FloatOutput * entity, uint16_t index, float state",
|
||||
None,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_domain_apply_entities(
|
||||
|
||||
Reference in new issue
Block a user