mirror of
https://github.com/esphome/esphome.git
synced 2026-10-25 23:42:15 +00:00
Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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)
|
||||
@@ -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(
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
@@ -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};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -52,6 +52,36 @@ button:
|
||||
minute: !lambda return 45;
|
||||
- ds3231.set_alarm_2:
|
||||
mode: every_minute
|
||||
- ds3231.set_alarm_1:
|
||||
mode: weekly
|
||||
day_of_week: 3
|
||||
hour: !lambda return 8;
|
||||
second: !lambda return 30;
|
||||
enabled: !lambda return true;
|
||||
- ds3231.set_alarm_1:
|
||||
mode: monthly
|
||||
day_of_month: !lambda return 20;
|
||||
minute: !lambda return 10;
|
||||
- ds3231.set_alarm_2:
|
||||
mode: monthly
|
||||
day_of_month: 10
|
||||
enabled: false
|
||||
- ds3231.set_alarm_2:
|
||||
mode: weekly
|
||||
day_of_week: !lambda return 5;
|
||||
hour: !lambda return 18;
|
||||
- ds3231.enable_alarm:
|
||||
alarm: !lambda return 2;
|
||||
- ds3231.disable_alarm:
|
||||
alarm: !lambda return 1;
|
||||
- if:
|
||||
condition:
|
||||
ds3231.is_alarm_triggered:
|
||||
alarm: !lambda return 1;
|
||||
then:
|
||||
- logger.log: alarm 1 triggered
|
||||
- ds3231.set_32khz_output:
|
||||
state: !lambda return false;
|
||||
- ds3231.enable_alarm:
|
||||
alarm: 1
|
||||
- ds3231.disable_alarm:
|
||||
|
||||
@@ -3,6 +3,8 @@ esphome:
|
||||
then:
|
||||
- audio_adc.set_mic_gain: 0db
|
||||
- audio_adc.set_mic_gain: !lambda 'return 4;'
|
||||
- audio_adc.set_mic_gain:
|
||||
mic_gain: 6db
|
||||
|
||||
audio_adc:
|
||||
- platform: es7210
|
||||
|
||||
@@ -35,6 +35,18 @@ espnow:
|
||||
data: [0x01, 0x02, 0x03, 0x04, 0x05]
|
||||
- espnow.broadcast:
|
||||
data: !lambda 'return {0x01, 0x02, 0x03, 0x04, 0x05};'
|
||||
- espnow.broadcast:
|
||||
address: FF:FF:FF:FF:FF:FF
|
||||
data: "options"
|
||||
wait_for_sent: true
|
||||
continue_on_error: false
|
||||
on_sent:
|
||||
- logger.log: "broadcast sent"
|
||||
on_error:
|
||||
- logger.log: "broadcast failed"
|
||||
- espnow.send:
|
||||
address: !lambda 'return {0x11, 0x22, 0x33, 0x44, 0x55, 0x66};'
|
||||
data: "lambda address"
|
||||
- espnow.send:
|
||||
address: 11:22:33:44:55:66
|
||||
data: !lambda 'return std::vector<uint8_t>(data, data + size);'
|
||||
|
||||
@@ -46,6 +46,13 @@ text_sensor:
|
||||
- ezo_pmp.set_calibration_volume:
|
||||
id: hcl_pump
|
||||
volume: 10
|
||||
- ezo_pmp.dose_with_constant_flow_rate:
|
||||
id: hcl_pump
|
||||
volume_per_minute: !lambda return 5.5f;
|
||||
duration: !lambda return 3;
|
||||
- ezo_pmp.set_calibration_volume:
|
||||
id: hcl_pump
|
||||
volume: !lambda return 12.5f;
|
||||
- ezo_pmp.find: hcl_pump
|
||||
- ezo_pmp.dose_continuously: hcl_pump
|
||||
- ezo_pmp.clear_total_volume_dosed: hcl_pump
|
||||
|
||||
@@ -14,3 +14,10 @@ button:
|
||||
speed: 200
|
||||
color: BLUE
|
||||
count: 2
|
||||
- fingerprint_grow.aura_led_control:
|
||||
state: !lambda return fingerprint_grow::FLASHING;
|
||||
speed: !lambda return 100;
|
||||
color: !lambda return fingerprint_grow::RED;
|
||||
count: !lambda return 3;
|
||||
- fingerprint_grow.delete:
|
||||
finger_id: !lambda return 2;
|
||||
@@ -9,6 +9,9 @@ hdmi_cec:
|
||||
- hdmi_cec.send:
|
||||
destination: !lambda return source;
|
||||
data: [0x90, 0x01] # Report Power Status: standby
|
||||
- hdmi_cec.send:
|
||||
destination: 0x0
|
||||
data: !lambda return {0x90, 0x01};
|
||||
- opcode: 0x8C # Give Device Vendor ID
|
||||
then:
|
||||
- lambda: |-
|
||||
|
||||
@@ -5,6 +5,8 @@ esphome:
|
||||
name: "Test"
|
||||
direction: 1
|
||||
- hlk_fm22x.delete: 5
|
||||
- hlk_fm22x.delete:
|
||||
face_id: 7
|
||||
- hlk_fm22x.delete_all:
|
||||
- hlk_fm22x.reset:
|
||||
- hlk_fm22x.scan:
|
||||
|
||||
@@ -35,6 +35,59 @@ esphome:
|
||||
- http_request.get:
|
||||
url: https://esphome.io
|
||||
timeout: !lambda "return 10000;"
|
||||
- http_request.get:
|
||||
url: !lambda 'return std::string("https://esphome.io/get");'
|
||||
method: GET
|
||||
collect_headers:
|
||||
- Content-Type
|
||||
- X-Request-Id
|
||||
max_response_buffer_size: 2kB
|
||||
capture_response: true
|
||||
on_response:
|
||||
then:
|
||||
- logger.log:
|
||||
format: "Content-Type %s, body %zu bytes"
|
||||
args:
|
||||
- response->get_response_header("content-type").c_str()
|
||||
- body.size()
|
||||
- http_request.post:
|
||||
url: !lambda 'return std::string("https://esphome.io/post");'
|
||||
method: POST
|
||||
body: !lambda 'return std::string("{\"key\": \"value\"}");'
|
||||
timeout: 5s
|
||||
collect_headers:
|
||||
- Location
|
||||
max_response_buffer_size: 512
|
||||
on_error:
|
||||
- logger.log: "POST failed"
|
||||
- http_request.post:
|
||||
url: https://esphome.io
|
||||
body: "plain body"
|
||||
timeout: !lambda "return 2500;"
|
||||
- http_request.send:
|
||||
method: PATCH
|
||||
url: !lambda 'return std::string("https://esphome.io/send");'
|
||||
json:
|
||||
key: value
|
||||
dynamic: !lambda 'return std::string("x");'
|
||||
collect_headers:
|
||||
- ETag
|
||||
max_response_buffer_size: 4kB
|
||||
capture_response: true
|
||||
timeout: !lambda "return 3000;"
|
||||
on_response:
|
||||
then:
|
||||
- logger.log:
|
||||
format: "Send status %d, body %s"
|
||||
args:
|
||||
- response->status_code
|
||||
- body.c_str()
|
||||
on_error:
|
||||
- logger.log: "Send failed"
|
||||
- http_request.send:
|
||||
method: DELETE
|
||||
url: https://esphome.io/item
|
||||
body: !lambda 'return std::string("id=1");'
|
||||
- http_request.post:
|
||||
url: https://esphome.io
|
||||
request_headers:
|
||||
|
||||
@@ -21,3 +21,9 @@ button:
|
||||
- htu21d.set_heater_level:
|
||||
id: htu21d_sensor
|
||||
level: 5
|
||||
- htu21d.set_heater:
|
||||
id: htu21d_sensor
|
||||
status: !lambda return false;
|
||||
- htu21d.set_heater_level:
|
||||
id: htu21d_sensor
|
||||
level: !lambda return 3;
|
||||
@@ -10,3 +10,6 @@ esphome:
|
||||
- hub75.set_brightness:
|
||||
id: my_hub75
|
||||
brightness: 50
|
||||
- hub75.set_brightness:
|
||||
id: my_hub75
|
||||
brightness: !lambda return 150;
|
||||
@@ -108,3 +108,6 @@ interval:
|
||||
id: m5epd_display
|
||||
mode: A2
|
||||
- it8951.update: m5epd_display
|
||||
- it8951.update:
|
||||
id: m5epd_display
|
||||
mode: !lambda return it8951::UPDATE_MODE_GL16;
|
||||
@@ -155,6 +155,33 @@ esphome:
|
||||
- light.turn_off:
|
||||
id: test_rgb_light
|
||||
transition_length: 300ms
|
||||
- light.turn_off:
|
||||
id: test_rgb_light
|
||||
state: false
|
||||
transition_length: !lambda return 200;
|
||||
- light.turn_on:
|
||||
id: test_rgb_light
|
||||
red: !lambda return 0.2f;
|
||||
green: !lambda return 0.4f;
|
||||
blue: !lambda return 0.6f;
|
||||
transition_length: !lambda return 350;
|
||||
- light.turn_on:
|
||||
id: test_cwww_light
|
||||
color_temperature: 300 mireds
|
||||
- light.turn_on:
|
||||
id: test_cwww_light
|
||||
color_temperature: !lambda return 250.0f;
|
||||
- light.control:
|
||||
id: test_rgbww_light
|
||||
cold_white: !lambda return 0.6f;
|
||||
warm_white: !lambda return 0.2f;
|
||||
- light.control:
|
||||
id: test_monochromatic_light
|
||||
effect: !lambda return "My Flicker";
|
||||
- light.dim_relative:
|
||||
id: test_monochromatic_light
|
||||
relative_brightness: !lambda return 0.1f;
|
||||
transition_length: !lambda return 150;
|
||||
# Test static effect name resolution at codegen time
|
||||
- light.turn_on:
|
||||
id: test_monochromatic_light
|
||||
|
||||
@@ -9,5 +9,12 @@ button:
|
||||
on_press:
|
||||
lightwaverf.send_raw:
|
||||
code: [0x04, 0x00, 0x00, 0x00, 0x0f, 0x03, 0x0d, 0x09, 0x08, 0x08]
|
||||
name: "Sofa"
|
||||
repeat: 1
|
||||
- platform: template
|
||||
name: "Turn off sofa inverted"
|
||||
on_press:
|
||||
lightwaverf.send_raw:
|
||||
code: [0x04, 0x00, 0x00, 0x00, 0x0f, 0x03, 0x0d, 0x09, 0x08, 0x08]
|
||||
name: "Sofa inverted"
|
||||
inverted: true
|
||||
pulse_length: 300
|
||||
@@ -5,6 +5,10 @@ esphome:
|
||||
level: warn
|
||||
format: "Warning: Logger level is %d"
|
||||
args: [id(logger_id).get_log_level()]
|
||||
- logger.log:
|
||||
tag: custom.tag
|
||||
level: info
|
||||
format: "Tagged log entry"
|
||||
- logger.set_level: WARN
|
||||
- logger.set_level:
|
||||
level: ERROR
|
||||
|
||||
@@ -41,6 +41,10 @@ script:
|
||||
id: test_list_id
|
||||
label:
|
||||
text: row
|
||||
- lvgl.list.add:
|
||||
id: test_list_id
|
||||
checkbox:
|
||||
text: list checkbox
|
||||
|
||||
esphome:
|
||||
on_boot:
|
||||
@@ -107,12 +111,28 @@ lvgl:
|
||||
id: meter_arc_indicator
|
||||
start_value: 0
|
||||
end_value: 180
|
||||
- lvgl.indicator.update:
|
||||
id: meter_arc_indicator
|
||||
value: 90
|
||||
opa: 50%
|
||||
- lvgl.indicator.update:
|
||||
id: minute_hand
|
||||
value: 30
|
||||
opa: !lambda return 128;
|
||||
- lvgl.animation.start:
|
||||
id:
|
||||
- anim_slide
|
||||
- anim_color
|
||||
duration: 3s
|
||||
loop: true
|
||||
- lvgl.animation.start:
|
||||
id: anim_slide
|
||||
start_delay: 250ms
|
||||
- lvgl.update:
|
||||
disp_bg_color: 0x101010
|
||||
disp_bg_opa: 80%
|
||||
- lvgl.update:
|
||||
disp_bg_image: cat_image
|
||||
on_invalidate_area:
|
||||
logger.log: Invalidate area
|
||||
on_resolution_change:
|
||||
@@ -684,8 +704,11 @@ lvgl:
|
||||
on_long_press:
|
||||
lvgl.matrix.button.update:
|
||||
id: [button_a, button_e, button_c]
|
||||
width: 2
|
||||
selected: true
|
||||
control:
|
||||
disabled: true
|
||||
checkable: true
|
||||
on_click:
|
||||
logger.log:
|
||||
format: "matrix button clicked: %d, is button_a = %u"
|
||||
@@ -1064,6 +1087,13 @@ lvgl:
|
||||
outline_width: 4
|
||||
outline_pad: 4
|
||||
outline_opa: cover
|
||||
- lvgl.canvas.draw_rectangle:
|
||||
x: 10
|
||||
y: 10
|
||||
width: !lambda return 120;
|
||||
height: !lambda return 80;
|
||||
shadow_ofs_x: 3
|
||||
shadow_ofs_y: 3
|
||||
- lvgl.canvas.set_pixels:
|
||||
color: red
|
||||
points:
|
||||
@@ -1097,6 +1127,12 @@ lvgl:
|
||||
text: Canvas Text
|
||||
align: center
|
||||
max_width: 150
|
||||
- lvgl.canvas.draw_text:
|
||||
x: 20
|
||||
y: 150
|
||||
font: montserrat_18
|
||||
text: Lambda Width
|
||||
max_width: !lambda return 120;
|
||||
- lvgl.canvas.draw_image:
|
||||
src: cat_image
|
||||
x: 100
|
||||
@@ -1108,6 +1144,35 @@ lvgl:
|
||||
skew_y: 5
|
||||
pivot_x: 25
|
||||
pivot_y: 25
|
||||
- lvgl.canvas.draw_image:
|
||||
src: cat_image
|
||||
x: 50
|
||||
y: 50
|
||||
angle: 45
|
||||
scale: 1.5
|
||||
opa: 70%
|
||||
recolor: red
|
||||
recolor_opa: 30%
|
||||
- lvgl.canvas.draw_image:
|
||||
src: cat_image
|
||||
x: 60
|
||||
y: 60
|
||||
zoom: 2.0
|
||||
- lvgl.canvas.draw_rectangle:
|
||||
x: 10
|
||||
y: 10
|
||||
width: 120
|
||||
height: 80
|
||||
bg_grad: color_bar
|
||||
- lvgl.canvas.draw_line:
|
||||
color: green
|
||||
width: 2
|
||||
opa: 60%
|
||||
dash_width: 6
|
||||
dash_gap: 3
|
||||
points:
|
||||
- 10,10
|
||||
- 90,90
|
||||
- lvgl.canvas.draw_line:
|
||||
color: blue
|
||||
width: 4
|
||||
@@ -1120,7 +1185,6 @@ lvgl:
|
||||
- 200, 50
|
||||
- 50,50
|
||||
- lvgl.canvas.draw_polygon:
|
||||
bg_color: teal
|
||||
border_color: white
|
||||
border_width: 2
|
||||
border_opa: cover
|
||||
@@ -1332,6 +1396,12 @@ lvgl:
|
||||
column: 1
|
||||
text: !lambda return str_sprintf("%.1f", (float) rand() / RAND_MAX * 100);
|
||||
merge_right: false
|
||||
- lvgl.table.cell.update:
|
||||
id: table_id
|
||||
row: 0
|
||||
column: 1
|
||||
text: cropped
|
||||
text_crop: true
|
||||
- lvgl.table.update:
|
||||
id: table_id
|
||||
selected_row: !lambda return (int) ((float) rand() / RAND_MAX * 2);
|
||||
@@ -1363,6 +1433,9 @@ lvgl:
|
||||
- 180, 60
|
||||
- 240, 10
|
||||
- lvgl.page.next:
|
||||
- lvgl.page.next:
|
||||
animation: move_left
|
||||
time: 200ms
|
||||
- switch:
|
||||
align: right_mid
|
||||
- checkbox:
|
||||
|
||||
@@ -75,6 +75,8 @@ lvgl:
|
||||
on_draw_end:
|
||||
- logger.log: draw ended
|
||||
- lvgl.pause:
|
||||
- lvgl.pause:
|
||||
show_snow: true
|
||||
- component.update: tft_display
|
||||
- delay: 60s
|
||||
- lvgl.resume:
|
||||
@@ -20,3 +20,6 @@ esphome:
|
||||
- max7219digit.reverse_on:
|
||||
- max7219digit.reverse_off:
|
||||
- max7219digit.intensity: 10
|
||||
- max7219digit.intensity:
|
||||
intensity: 7
|
||||
- max7219digit.intensity: !lambda return 3;
|
||||
@@ -53,6 +53,60 @@ media_player:
|
||||
- media_player.unshuffle: test_media_player
|
||||
- media_player.group_join: test_media_player
|
||||
- media_player.clear_playlist: test_media_player
|
||||
- media_player.play:
|
||||
id: test_media_player
|
||||
announcement: true
|
||||
- media_player.pause:
|
||||
id: test_media_player
|
||||
announcement: true
|
||||
- media_player.toggle:
|
||||
id: test_media_player
|
||||
announcement: true
|
||||
- media_player.turn_on:
|
||||
id: test_media_player
|
||||
announcement: true
|
||||
- media_player.turn_off:
|
||||
id: test_media_player
|
||||
announcement: true
|
||||
- media_player.next:
|
||||
id: test_media_player
|
||||
announcement: true
|
||||
- media_player.previous:
|
||||
id: test_media_player
|
||||
announcement: true
|
||||
- media_player.mute:
|
||||
id: test_media_player
|
||||
announcement: true
|
||||
- media_player.unmute:
|
||||
id: test_media_player
|
||||
announcement: true
|
||||
- media_player.repeat_off:
|
||||
id: test_media_player
|
||||
announcement: true
|
||||
- media_player.repeat_one:
|
||||
id: test_media_player
|
||||
announcement: true
|
||||
- media_player.repeat_all:
|
||||
id: test_media_player
|
||||
announcement: true
|
||||
- media_player.shuffle:
|
||||
id: test_media_player
|
||||
announcement: true
|
||||
- media_player.unshuffle:
|
||||
id: test_media_player
|
||||
announcement: true
|
||||
- media_player.group_join:
|
||||
id: test_media_player
|
||||
announcement: true
|
||||
- media_player.clear_playlist:
|
||||
id: test_media_player
|
||||
announcement: true
|
||||
- media_player.volume_up:
|
||||
id: test_media_player
|
||||
announcement: true
|
||||
- media_player.volume_down:
|
||||
id: test_media_player
|
||||
announcement: true
|
||||
- wait_until:
|
||||
media_player.is_idle: test_media_player
|
||||
- wait_until:
|
||||
@@ -86,3 +140,7 @@ media_player:
|
||||
id: test_media_player
|
||||
media_url: http://localhost/media.mp3
|
||||
announcement: true
|
||||
- media_player.enqueue:
|
||||
id: test_media_player
|
||||
media_url: http://localhost/media.mp3
|
||||
announcement: !lambda return true;
|
||||
@@ -10,6 +10,10 @@ button:
|
||||
id: midea_unit
|
||||
temperature: !lambda return id(midea_unit).target_temperature + 1.0f;
|
||||
use_fahrenheit: !lambda return false;
|
||||
- midea_ac.follow_me:
|
||||
id: midea_unit
|
||||
temperature: 24.0
|
||||
beeper: !lambda return true;
|
||||
- midea_ac.display_toggle:
|
||||
id: midea_unit
|
||||
- midea_ac.swing_step:
|
||||
|
||||
@@ -30,6 +30,9 @@ esphome:
|
||||
temperature: 22.0
|
||||
- mitsubishi_cn105.clear_remote_temperature:
|
||||
id: ac
|
||||
- mitsubishi_cn105.set_remote_temperature:
|
||||
id: ac
|
||||
temperature: !lambda return 21.5f;
|
||||
- mitsubishi_cn105.vane.control:
|
||||
id: ac
|
||||
vertical:
|
||||
|
||||
@@ -186,6 +186,215 @@ button:
|
||||
read_address: 0x0420
|
||||
read_count: 1
|
||||
|
||||
- platform: template
|
||||
name: "Typed Handlers"
|
||||
on_press:
|
||||
- modbus_client.read_coils:
|
||||
address: !lambda "return 1;"
|
||||
start_address: !lambda "return 0x03;"
|
||||
count: !lambda "return 8;"
|
||||
continuous: !lambda "return false;"
|
||||
allow_broadcast_read: !lambda "return false;"
|
||||
on_sent:
|
||||
then:
|
||||
- logger.log: "coils sent"
|
||||
on_not_sent:
|
||||
then:
|
||||
- lambda: 'ESP_LOGW("modbus_client.test", "coils not sent fc 0x%X", request.empty() ? 0 : request[0]);'
|
||||
on_no_response:
|
||||
then:
|
||||
- logger.log: "coils timeout"
|
||||
retry: !lambda "return false;"
|
||||
on_custom_response:
|
||||
then:
|
||||
- lambda: 'ESP_LOGW("modbus_client.test", "coils custom reply %u bytes", (unsigned) response.size());'
|
||||
- modbus_client.read_coils:
|
||||
address: 0
|
||||
start_address: 0x03
|
||||
allow_broadcast_read: true
|
||||
on_response:
|
||||
then:
|
||||
- lambda: 'ESP_LOGI("modbus_client.test", "broadcast coil0=%d", bits[0]);'
|
||||
- modbus_client.read_discrete_inputs:
|
||||
address: !lambda "return 1;"
|
||||
start_address: !lambda "return 0x00;"
|
||||
count: 4
|
||||
continuous: !lambda "return true;"
|
||||
on_sent:
|
||||
then:
|
||||
- logger.log: "inputs sent"
|
||||
on_not_sent:
|
||||
then:
|
||||
- logger.log: "inputs not sent"
|
||||
on_no_response: !lambda "return false;"
|
||||
on_response:
|
||||
then:
|
||||
- lambda: 'ESP_LOGI("modbus_client.test", "input0=%d n=%u", bits[0], (unsigned) bits.size());'
|
||||
on_custom_response:
|
||||
then:
|
||||
- logger.log: "inputs custom reply"
|
||||
- modbus_client.read_discrete_inputs:
|
||||
address: 0
|
||||
start_address: 0x00
|
||||
allow_broadcast_read: true
|
||||
- modbus_client.read_holding_registers:
|
||||
address: 0x01
|
||||
start_address: !lambda "return 0x10;"
|
||||
continuous: !lambda "return true;"
|
||||
on_sent:
|
||||
then:
|
||||
- logger.log: "holding sent"
|
||||
on_custom_response:
|
||||
then:
|
||||
- lambda: 'ESP_LOGW("modbus_client.test", "holding custom reply fc 0x%X", response.empty() ? 0 : response[0]);'
|
||||
- modbus_client.read_input_registers:
|
||||
address: !lambda "return 1;"
|
||||
start_address: !lambda "return 0x20;"
|
||||
count: 3
|
||||
on_sent:
|
||||
then:
|
||||
- logger.log: "inputs regs sent"
|
||||
on_not_sent:
|
||||
then:
|
||||
- logger.log: "input regs not sent"
|
||||
on_error:
|
||||
then:
|
||||
- lambda: 'ESP_LOGW("modbus_client.test", "input regs exception %d", (int) exception_code);'
|
||||
on_no_response:
|
||||
then:
|
||||
- logger.log: "input regs timeout"
|
||||
on_response:
|
||||
then:
|
||||
- lambda: 'ESP_LOGI("modbus_client.test", "input reg0=%u n=%u", values[0], (unsigned) values.size());'
|
||||
- modbus_client.read_input_registers:
|
||||
address: 0
|
||||
start_address: 0x20
|
||||
allow_broadcast_read: true
|
||||
- modbus_client.read_write_multiple_registers:
|
||||
address: !lambda "return 1;"
|
||||
write_address: !lambda "return 0x0300;"
|
||||
values: [1, 2]
|
||||
read_address: !lambda "return 0x0400;"
|
||||
read_count: !lambda "return 2;"
|
||||
allow_broadcast_read: !lambda "return false;"
|
||||
on_sent:
|
||||
then:
|
||||
- logger.log: "rw sent"
|
||||
on_not_sent:
|
||||
then:
|
||||
- logger.log: "rw not sent"
|
||||
on_error:
|
||||
then:
|
||||
- lambda: 'ESP_LOGW("modbus_client.test", "rw exception %d", (int) exception_code);'
|
||||
on_no_response:
|
||||
then:
|
||||
- logger.log: "rw timeout"
|
||||
on_custom_response:
|
||||
then:
|
||||
- logger.log: "rw custom reply"
|
||||
- modbus_client.send:
|
||||
address: 0
|
||||
pdu: [0x03, 0x00, 0x10, 0x00, 0x01]
|
||||
allow_broadcast_read: true
|
||||
- modbus_client.send:
|
||||
address: !lambda "return 1;"
|
||||
pdu: [0x06, 0x01, 0x02, 0x00, 0x2A]
|
||||
continuous: !lambda "return false;"
|
||||
expect_broadcast_write_response: !lambda "return false;"
|
||||
- modbus_client.write_multiple_coils:
|
||||
address: !lambda "return 1;"
|
||||
start_address: !lambda "return 0x0010;"
|
||||
values: [true, false]
|
||||
expect_broadcast_write_response: !lambda "return false;"
|
||||
on_sent:
|
||||
then:
|
||||
- logger.log: "coils write sent"
|
||||
on_not_sent:
|
||||
then:
|
||||
- logger.log: "coils write not sent"
|
||||
on_no_response:
|
||||
then:
|
||||
- logger.log: "coils write timeout"
|
||||
on_response:
|
||||
then:
|
||||
- logger.log: "coils write acked"
|
||||
on_custom_response:
|
||||
then:
|
||||
- logger.log: "coils write custom reply"
|
||||
- modbus_client.write_multiple_coils:
|
||||
address: 0
|
||||
start_address: 0x0010
|
||||
values: [true]
|
||||
expect_broadcast_write_response: true
|
||||
- modbus_client.write_multiple_registers:
|
||||
address: !lambda "return 1;"
|
||||
start_address: !lambda "return 0x0200;"
|
||||
values: [1, 2]
|
||||
on_sent:
|
||||
then:
|
||||
- logger.log: "regs write sent"
|
||||
on_not_sent:
|
||||
then:
|
||||
- logger.log: "regs write not sent"
|
||||
on_error:
|
||||
then:
|
||||
- lambda: 'ESP_LOGW("modbus_client.test", "regs write exception %d", (int) exception_code);'
|
||||
on_no_response:
|
||||
then:
|
||||
- logger.log: "regs write timeout"
|
||||
on_custom_response:
|
||||
then:
|
||||
- logger.log: "regs write custom reply"
|
||||
- modbus_client.write_multiple_registers:
|
||||
address: 0
|
||||
start_address: 0x0200
|
||||
values: [7]
|
||||
expect_broadcast_write_response: true
|
||||
- modbus_client.write_single_coil:
|
||||
address: !lambda "return 1;"
|
||||
start_address: !lambda "return 0x01;"
|
||||
value: !lambda "return false;"
|
||||
expect_broadcast_write_response: !lambda "return false;"
|
||||
on_sent:
|
||||
then:
|
||||
- logger.log: "coil sent"
|
||||
on_not_sent:
|
||||
then:
|
||||
- logger.log: "coil not sent"
|
||||
on_error:
|
||||
then:
|
||||
- lambda: 'ESP_LOGW("modbus_client.test", "coil exception %d", (int) exception_code);'
|
||||
on_no_response:
|
||||
then:
|
||||
- logger.log: "coil timeout"
|
||||
on_response:
|
||||
then:
|
||||
- logger.log: "coil acked"
|
||||
on_custom_response:
|
||||
then:
|
||||
- logger.log: "coil custom reply"
|
||||
- modbus_client.write_single_coil:
|
||||
address: 0
|
||||
start_address: 0x01
|
||||
value: true
|
||||
expect_broadcast_write_response: true
|
||||
- modbus_client.write_single_register:
|
||||
address: 0x01
|
||||
start_address: !lambda "return 0x0102;"
|
||||
value: 42
|
||||
expect_broadcast_write_response: !lambda "return false;"
|
||||
on_sent:
|
||||
then:
|
||||
- logger.log: "register sent"
|
||||
on_not_sent:
|
||||
then:
|
||||
- logger.log: "register not sent"
|
||||
on_no_response:
|
||||
then:
|
||||
- logger.log: "register timeout"
|
||||
on_custom_response:
|
||||
then:
|
||||
- logger.log: "register custom reply"
|
||||
number:
|
||||
- platform: template
|
||||
name: "Modbus Client Write Value"
|
||||
|
||||
@@ -8,7 +8,9 @@ esphome:
|
||||
condition:
|
||||
modem.enabled:
|
||||
then:
|
||||
modem.send_at: "AT+CGSN"
|
||||
- modem.send_at: "AT+CGSN"
|
||||
- modem.send_at:
|
||||
command: !lambda 'return std::string("AT+CSQ");'
|
||||
else:
|
||||
modem.enable:
|
||||
- if:
|
||||
|
||||
@@ -109,6 +109,18 @@ button:
|
||||
return payload;
|
||||
qos: !lambda return 1;
|
||||
retain: !lambda return false;
|
||||
- mqtt.publish_json:
|
||||
topic: some/topic/json
|
||||
payload: |-
|
||||
root["key"] = "value";
|
||||
qos: 1
|
||||
retain: true
|
||||
- mqtt.publish_json:
|
||||
topic: !lambda return "some/topic/json/lambda";
|
||||
payload: |-
|
||||
root["key"] = 1;
|
||||
qos: !lambda return 2;
|
||||
retain: !lambda return false;
|
||||
|
||||
climate:
|
||||
- platform: thermostat
|
||||
|
||||
@@ -42,6 +42,19 @@ button:
|
||||
cycle: false
|
||||
- number.to_min: number_test_number
|
||||
- number.to_max: number_test_number
|
||||
- number.increment:
|
||||
id: number_test_number
|
||||
mode: INCREMENT
|
||||
cycle: true
|
||||
- number.decrement:
|
||||
id: number_test_number
|
||||
mode: DECREMENT
|
||||
- number.to_min:
|
||||
id: number_test_number
|
||||
mode: TO_MIN
|
||||
- number.to_max:
|
||||
id: number_test_number
|
||||
mode: TO_MAX
|
||||
- number.operation:
|
||||
id: number_test_number
|
||||
operation: increment
|
||||
|
||||
Loaded 100 of 135 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user