1
0
mirror of https://github.com/esphome/esphome.git synced 2026-02-08 00:31:58 +00:00

[libretiny] Update LibreTiny to v1.12.0

This commit is contained in:
J. Nick Koston
2026-02-07 18:01:52 -06:00
33 changed files with 1062 additions and 345 deletions

View File

@@ -1 +1 @@
ad8131b65de630ca9f89d34d35f32e5c858fc2a7b310227e5edaf946e942af0f 65089c03a5fa1ee23c52d3a0898f533044788b79a9be625133bcd4fa9837e6f9

View File

@@ -12,7 +12,6 @@ from .const import (
CORE_SUBCATEGORY_PATTERNS, CORE_SUBCATEGORY_PATTERNS,
DEMANGLED_PATTERNS, DEMANGLED_PATTERNS,
ESPHOME_COMPONENT_PATTERN, ESPHOME_COMPONENT_PATTERN,
SECTION_TO_ATTR,
SYMBOL_PATTERNS, SYMBOL_PATTERNS,
) )
from .demangle import batch_demangle from .demangle import batch_demangle
@@ -91,6 +90,17 @@ class ComponentMemory:
bss_size: int = 0 # Uninitialized data (ram only) bss_size: int = 0 # Uninitialized data (ram only)
symbol_count: int = 0 symbol_count: int = 0
def add_section_size(self, section_name: str, size: int) -> None:
"""Add size to the appropriate attribute for a section."""
if section_name == ".text":
self.text_size += size
elif section_name == ".rodata":
self.rodata_size += size
elif section_name == ".data":
self.data_size += size
elif section_name == ".bss":
self.bss_size += size
@property @property
def flash_total(self) -> int: def flash_total(self) -> int:
"""Total flash usage (text + rodata + data).""" """Total flash usage (text + rodata + data)."""
@@ -167,12 +177,15 @@ class MemoryAnalyzer:
self._elf_symbol_names: set[str] = set() self._elf_symbol_names: set[str] = set()
# SDK symbols not in ELF (static/local symbols from closed-source libs) # SDK symbols not in ELF (static/local symbols from closed-source libs)
self._sdk_symbols: list[SDKSymbol] = [] self._sdk_symbols: list[SDKSymbol] = []
# CSWTCH symbols: list of (name, size, source_file, component)
self._cswtch_symbols: list[tuple[str, int, str, str]] = []
def analyze(self) -> dict[str, ComponentMemory]: def analyze(self) -> dict[str, ComponentMemory]:
"""Analyze the ELF file and return component memory usage.""" """Analyze the ELF file and return component memory usage."""
self._parse_sections() self._parse_sections()
self._parse_symbols() self._parse_symbols()
self._categorize_symbols() self._categorize_symbols()
self._analyze_cswtch_symbols()
self._analyze_sdk_libraries() self._analyze_sdk_libraries()
return dict(self.components) return dict(self.components)
@@ -255,8 +268,7 @@ class MemoryAnalyzer:
comp_mem.symbol_count += 1 comp_mem.symbol_count += 1
# Update the appropriate size attribute based on section # Update the appropriate size attribute based on section
if attr_name := SECTION_TO_ATTR.get(section_name): comp_mem.add_section_size(section_name, size)
setattr(comp_mem, attr_name, getattr(comp_mem, attr_name) + size)
# Track uncategorized symbols # Track uncategorized symbols
if component == "other" and size > 0: if component == "other" and size > 0:
@@ -372,6 +384,205 @@ class MemoryAnalyzer:
return "Other Core" return "Other Core"
def _find_object_files_dir(self) -> Path | None:
"""Find the directory containing object files for this build.
Returns:
Path to the directory containing .o files, or None if not found.
"""
# The ELF is typically at .pioenvs/<env>/firmware.elf
# Object files are in .pioenvs/<env>/src/ and .pioenvs/<env>/lib*/
pioenvs_dir = self.elf_path.parent
if pioenvs_dir.exists() and any(pioenvs_dir.glob("src/*.o")):
return pioenvs_dir
return None
def _scan_cswtch_in_objects(
self, obj_dir: Path
) -> dict[str, list[tuple[str, int]]]:
"""Scan object files for CSWTCH symbols using a single nm invocation.
Uses ``nm --print-file-name -S`` on all ``.o`` files at once.
Output format: ``/path/to/file.o:address size type name``
Args:
obj_dir: Directory containing object files (.pioenvs/<env>/)
Returns:
Dict mapping "CSWTCH$NNN:size" to list of (source_file, size) tuples.
"""
cswtch_map: dict[str, list[tuple[str, int]]] = defaultdict(list)
if not self.nm_path:
return cswtch_map
# Find all .o files recursively, sorted for deterministic output
obj_files = sorted(obj_dir.rglob("*.o"))
if not obj_files:
return cswtch_map
_LOGGER.debug("Scanning %d object files for CSWTCH symbols", len(obj_files))
# Single nm call with --print-file-name for all object files
result = run_tool(
[self.nm_path, "--print-file-name", "-S"] + [str(f) for f in obj_files],
timeout=30,
)
if result is None or result.returncode != 0:
return cswtch_map
for line in result.stdout.splitlines():
if "CSWTCH$" not in line:
continue
# Split on last ":" that precedes a hex address.
# nm --print-file-name format: filepath:hex_addr hex_size type name
# We split from the right: find the last colon followed by hex digits.
parts_after_colon = line.rsplit(":", 1)
if len(parts_after_colon) != 2:
continue
file_path = parts_after_colon[0]
fields = parts_after_colon[1].split()
# fields: [address, size, type, name]
if len(fields) < 4:
continue
sym_name = fields[3]
if not sym_name.startswith("CSWTCH$"):
continue
try:
size = int(fields[1], 16)
except ValueError:
continue
# Get relative path from obj_dir for readability
try:
rel_path = str(Path(file_path).relative_to(obj_dir))
except ValueError:
rel_path = file_path
key = f"{sym_name}:{size}"
cswtch_map[key].append((rel_path, size))
return cswtch_map
def _source_file_to_component(self, source_file: str) -> str:
"""Map a source object file path to its component name.
Args:
source_file: Relative path like 'src/esphome/components/wifi/wifi_component.cpp.o'
Returns:
Component name like '[esphome]wifi' or the source file if unknown.
"""
parts = Path(source_file).parts
# ESPHome component: src/esphome/components/<name>/...
if "components" in parts:
idx = parts.index("components")
if idx + 1 < len(parts):
component_name = parts[idx + 1]
if component_name in get_esphome_components():
return f"{_COMPONENT_PREFIX_ESPHOME}{component_name}"
if component_name in self.external_components:
return f"{_COMPONENT_PREFIX_EXTERNAL}{component_name}"
# ESPHome core: src/esphome/core/... or src/esphome/...
if "core" in parts and "esphome" in parts:
return _COMPONENT_CORE
if "esphome" in parts and "components" not in parts:
return _COMPONENT_CORE
# Framework/library files - return the first path component
# e.g., lib65b/ESPAsyncTCP/... -> lib65b
# FrameworkArduino/... -> FrameworkArduino
return parts[0] if parts else source_file
def _analyze_cswtch_symbols(self) -> None:
"""Analyze CSWTCH (GCC switch table) symbols by tracing to source objects.
CSWTCH symbols are compiler-generated lookup tables for switch statements.
They are local symbols, so the same name can appear in different object files.
This method scans .o files to attribute them to their source components.
"""
obj_dir = self._find_object_files_dir()
if obj_dir is None:
_LOGGER.debug("No object files directory found, skipping CSWTCH analysis")
return
# Scan object files for CSWTCH symbols
cswtch_map = self._scan_cswtch_in_objects(obj_dir)
if not cswtch_map:
_LOGGER.debug("No CSWTCH symbols found in object files")
return
# Collect CSWTCH symbols from the ELF (already parsed in sections)
# Include section_name for re-attribution of component totals
elf_cswtch = [
(symbol_name, size, section_name)
for section_name, section in self.sections.items()
for symbol_name, size, _ in section.symbols
if symbol_name.startswith("CSWTCH$")
]
_LOGGER.debug(
"Found %d CSWTCH symbols in ELF, %d unique in object files",
len(elf_cswtch),
len(cswtch_map),
)
# Match ELF CSWTCH symbols to source files and re-attribute component totals.
# _categorize_symbols() already ran and put these into "other" since CSWTCH$
# names don't match any component pattern. We move the bytes to the correct
# component based on the object file mapping.
other_mem = self.components.get("other")
for sym_name, size, section_name in elf_cswtch:
key = f"{sym_name}:{size}"
sources = cswtch_map.get(key, [])
if len(sources) == 1:
source_file = sources[0][0]
component = self._source_file_to_component(source_file)
elif len(sources) > 1:
# Ambiguous - multiple object files have same CSWTCH name+size
source_file = "ambiguous"
component = "ambiguous"
_LOGGER.debug(
"Ambiguous CSWTCH %s (%d B) found in %d files: %s",
sym_name,
size,
len(sources),
", ".join(src for src, _ in sources),
)
else:
source_file = "unknown"
component = "unknown"
self._cswtch_symbols.append((sym_name, size, source_file, component))
# Re-attribute from "other" to the correct component
if (
component not in ("other", "unknown", "ambiguous")
and other_mem is not None
):
other_mem.add_section_size(section_name, -size)
if component not in self.components:
self.components[component] = ComponentMemory(component)
self.components[component].add_section_size(section_name, size)
# Sort by size descending
self._cswtch_symbols.sort(key=lambda x: x[1], reverse=True)
total_size = sum(size for _, size, _, _ in self._cswtch_symbols)
_LOGGER.debug(
"CSWTCH analysis: %d symbols, %d bytes total",
len(self._cswtch_symbols),
total_size,
)
def get_unattributed_ram(self) -> tuple[int, int, int]: def get_unattributed_ram(self) -> tuple[int, int, int]:
"""Get unattributed RAM sizes (SDK/framework overhead). """Get unattributed RAM sizes (SDK/framework overhead).

View File

@@ -184,6 +184,52 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
f"{i + 1:>2}. {size:>7,} B {section_label:<8} {demangled_display:<{self.COL_TOP_SYMBOL_NAME}} {component}" f"{i + 1:>2}. {size:>7,} B {section_label:<8} {demangled_display:<{self.COL_TOP_SYMBOL_NAME}} {component}"
) )
def _add_cswtch_analysis(self, lines: list[str]) -> None:
"""Add CSWTCH (GCC switch table lookup) analysis section."""
self._add_section_header(lines, "CSWTCH Analysis (GCC Switch Table Lookups)")
total_size = sum(size for _, size, _, _ in self._cswtch_symbols)
lines.append(
f"Total: {len(self._cswtch_symbols)} switch table(s), {total_size:,} B"
)
lines.append("")
# Group by component
by_component: dict[str, list[tuple[str, int, str]]] = defaultdict(list)
for sym_name, size, source_file, component in self._cswtch_symbols:
by_component[component].append((sym_name, size, source_file))
# Sort components by total size descending
sorted_components = sorted(
by_component.items(),
key=lambda x: sum(s[1] for s in x[1]),
reverse=True,
)
for component, symbols in sorted_components:
comp_total = sum(s[1] for s in symbols)
lines.append(f"{component} ({comp_total:,} B, {len(symbols)} tables):")
# Group by source file within component
by_file: dict[str, list[tuple[str, int]]] = defaultdict(list)
for sym_name, size, source_file in symbols:
by_file[source_file].append((sym_name, size))
for source_file, file_symbols in sorted(
by_file.items(),
key=lambda x: sum(s[1] for s in x[1]),
reverse=True,
):
file_total = sum(s[1] for s in file_symbols)
lines.append(
f" {source_file} ({file_total:,} B, {len(file_symbols)} tables)"
)
for sym_name, size in sorted(
file_symbols, key=lambda x: x[1], reverse=True
):
lines.append(f" {size:>6,} B {sym_name}")
lines.append("")
def generate_report(self, detailed: bool = False) -> str: def generate_report(self, detailed: bool = False) -> str:
"""Generate a formatted memory report.""" """Generate a formatted memory report."""
components = sorted( components = sorted(
@@ -471,6 +517,10 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
lines.append(f" ... and {len(large_ram_syms) - 10} more") lines.append(f" ... and {len(large_ram_syms) - 10} more")
lines.append("") lines.append("")
# CSWTCH (GCC switch table) analysis
if self._cswtch_symbols:
self._add_cswtch_analysis(lines)
lines.append( lines.append(
"Note: This analysis covers symbols in the ELF file. Some runtime allocations may not be included." "Note: This analysis covers symbols in the ELF file. Some runtime allocations may not be included."
) )

View File

@@ -66,15 +66,6 @@ SECTION_MAPPING = {
), ),
} }
# Section to ComponentMemory attribute mapping
# Maps section names to the attribute name in ComponentMemory dataclass
SECTION_TO_ATTR = {
".text": "text_size",
".rodata": "rodata_size",
".data": "data_size",
".bss": "bss_size",
}
# Component identification rules # Component identification rules
# Symbol patterns: patterns found in raw symbol names # Symbol patterns: patterns found in raw symbol names
SYMBOL_PATTERNS = { SYMBOL_PATTERNS = {
@@ -513,7 +504,9 @@ SYMBOL_PATTERNS = {
"__FUNCTION__$", "__FUNCTION__$",
"DAYS_IN_MONTH", "DAYS_IN_MONTH",
"_DAYS_BEFORE_MONTH", "_DAYS_BEFORE_MONTH",
"CSWTCH$", # Note: CSWTCH$ symbols are GCC switch table lookup tables.
# They are attributed to their source object files via _analyze_cswtch_symbols()
# rather than being lumped into libc.
"dst$", "dst$",
"sulp", "sulp",
"_strtol_l", # String to long with locale "_strtol_l", # String to long with locale

View File

@@ -66,7 +66,7 @@ async def to_code(config):
cg.add_build_flag("-DDSMR_WATER_MBUS_ID=" + str(config[CONF_WATER_MBUS_ID])) cg.add_build_flag("-DDSMR_WATER_MBUS_ID=" + str(config[CONF_WATER_MBUS_ID]))
# DSMR Parser # DSMR Parser
cg.add_library("esphome/dsmr_parser", "1.0.0") cg.add_library("esphome/dsmr_parser", "1.1.0")
# Crypto # Crypto
cg.add_library("polargoose/Crypto-no-arduino", "0.4.0") cg.add_library("polargoose/Crypto-no-arduino", "0.4.0")

View File

@@ -718,14 +718,6 @@ CONFIG_SCHEMA = cv.Schema(
device_class=DEVICE_CLASS_POWER, device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT, state_class=STATE_CLASS_MEASUREMENT,
), ),
cv.Optional("fw_core_version"): sensor.sensor_schema(
accuracy_decimals=3,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("fw_module_version"): sensor.sensor_schema(
accuracy_decimals=3,
state_class=STATE_CLASS_MEASUREMENT,
),
} }
).extend(cv.COMPONENT_SCHEMA) ).extend(cv.COMPONENT_SCHEMA)

View File

@@ -26,7 +26,9 @@ CONFIG_SCHEMA = cv.Schema(
cv.Optional("sub_equipment_id"): text_sensor.text_sensor_schema(), cv.Optional("sub_equipment_id"): text_sensor.text_sensor_schema(),
cv.Optional("gas_delivered_text"): text_sensor.text_sensor_schema(), cv.Optional("gas_delivered_text"): text_sensor.text_sensor_schema(),
cv.Optional("fw_core_checksum"): text_sensor.text_sensor_schema(), cv.Optional("fw_core_checksum"): text_sensor.text_sensor_schema(),
cv.Optional("fw_core_version"): text_sensor.text_sensor_schema(),
cv.Optional("fw_module_checksum"): text_sensor.text_sensor_schema(), cv.Optional("fw_module_checksum"): text_sensor.text_sensor_schema(),
cv.Optional("fw_module_version"): text_sensor.text_sensor_schema(),
cv.Optional("telegram"): text_sensor.text_sensor_schema().extend( cv.Optional("telegram"): text_sensor.text_sensor_schema().extend(
{cv.Optional(CONF_INTERNAL, default=True): cv.boolean} {cv.Optional(CONF_INTERNAL, default=True): cv.boolean}
), ),

View File

@@ -0,0 +1,67 @@
#pragma once
#include <cstdint>
#include <algorithm>
#include "esphome/core/color.h"
/* Utility for converting internal \a Color RGB representation to supported IC hardware color keys
*
* Focus in driver layer is on efficiency.
* For optimum output quality on RGB inputs consider offline color keying/dithering.
* Also see e.g. Image component.
*/
namespace esphome::epaper_spi {
/** Delta for when to regard as gray */
static constexpr uint8_t COLORCONV_GRAY_THRESHOLD = 50;
/** Map RGB color to discrete BWYR hex 4 color key
*
* @tparam NATIVE_COLOR Type of native hardware color values
* @param color RGB color to convert from
* @param hw_black Native value for black
* @param hw_white Native value for white
* @param hw_yellow Native value for yellow
* @param hw_red Native value for red
* @return Converted native hardware color value
* @internal Constexpr. Does not depend on side effects ("pure").
*/
template<typename NATIVE_COLOR>
constexpr NATIVE_COLOR color_to_bwyr(Color color, NATIVE_COLOR hw_black, NATIVE_COLOR hw_white, NATIVE_COLOR hw_yellow,
NATIVE_COLOR hw_red) {
// --- Step 1: Check for Grayscale (Black or White) ---
// We define "grayscale" as a color where the min and max components
// are close to each other.
const auto [min_rgb, max_rgb] = std::minmax({color.r, color.g, color.b});
if ((max_rgb - min_rgb) < COLORCONV_GRAY_THRESHOLD) {
// It's a shade of gray. Map to BLACK or WHITE.
// We split the luminance at the halfway point (382 = (255*3)/2)
if ((static_cast<int>(color.r) + color.g + color.b) > 382) {
return hw_white;
}
return hw_black;
}
// --- Step 2: Check for Primary/Secondary Colors ---
// If it's not gray, it's a color. We check which components are
// "on" (over 128) vs "off". This divides the RGB cube into 8 corners.
const bool r_on = (color.r > 128);
const bool g_on = (color.g > 128);
const bool b_on = (color.b > 128);
if (r_on) {
if (!b_on) {
return g_on ? hw_yellow : hw_red;
}
// At least red+blue high (but not gray) -> White
return hw_white;
} else {
return (b_on && g_on) ? hw_white : hw_black;
}
}
} // namespace esphome::epaper_spi

View File

@@ -0,0 +1,227 @@
#include "epaper_spi_jd79660.h"
#include "colorconv.h"
#include "esphome/core/log.h"
namespace esphome::epaper_spi {
static constexpr const char *const TAG = "epaper_spi.jd79660";
/** Pixel color as 2bpp. Must match IC LUT values. */
enum JD79660Color : uint8_t {
BLACK = 0b00,
WHITE = 0b01,
YELLOW = 0b10,
RED = 0b11,
};
/** Map RGB color to JD79660 BWYR hex color keys */
static JD79660Color HOT color_to_hex(Color color) {
return color_to_bwyr(color, JD79660Color::BLACK, JD79660Color::WHITE, JD79660Color::YELLOW, JD79660Color::RED);
}
void EPaperJD79660::fill(Color color) {
// If clipping is active, fall back to base implementation
if (this->get_clipping().is_set()) {
EPaperBase::fill(color);
return;
}
const auto pixel_color = color_to_hex(color);
// We store 4 pixels per byte
this->buffer_.fill(pixel_color | (pixel_color << 2) | (pixel_color << 4) | (pixel_color << 6));
}
void HOT EPaperJD79660::draw_pixel_at(int x, int y, Color color) {
if (!this->rotate_coordinates_(x, y))
return;
const auto pixel_bits = color_to_hex(color);
const uint32_t pixel_position = x + y * this->get_width_internal();
// We store 4 pixels per byte at LSB offsets 6, 4, 2, 0
const uint32_t byte_position = pixel_position / 4;
const uint32_t bit_offset = 6 - ((pixel_position % 4) * 2);
const auto original = this->buffer_[byte_position];
this->buffer_[byte_position] = (original & (~(0b11 << bit_offset))) | // mask old 2bpp
(pixel_bits << bit_offset); // add new 2bpp
}
bool EPaperJD79660::reset() {
// On entry state RESET set step, next state will be RESET_END
if (this->state_ == EPaperState::RESET) {
this->step_ = FSMState::RESET_STEP0_H;
}
switch (this->step_) {
case FSMState::RESET_STEP0_H:
// Step #0: Reset H for some settle time.
ESP_LOGVV(TAG, "reset #0");
this->reset_pin_->digital_write(true);
this->reset_duration_ = SLEEP_MS_RESET0;
this->step_ = FSMState::RESET_STEP1_L;
return false; // another loop: step #1 below
case FSMState::RESET_STEP1_L:
// Step #1: Reset L pulse for slightly >1.5ms.
// This is actual reset trigger.
ESP_LOGVV(TAG, "reset #1");
// As commented on SLEEP_MS_RESET1: Reset pulse must happen within time window.
// So do not use FSM loop, and avoid other calls/logs during pulse below.
this->reset_pin_->digital_write(false);
delay(SLEEP_MS_RESET1);
this->reset_pin_->digital_write(true);
this->reset_duration_ = SLEEP_MS_RESET2;
this->step_ = FSMState::RESET_STEP2_IDLECHECK;
return false; // another loop: step #2 below
case FSMState::RESET_STEP2_IDLECHECK:
// Step #2: Basically finished. Check sanity, and move FSM to INITIALISE state
ESP_LOGVV(TAG, "reset #2");
if (!this->is_idle_()) {
// Expectation: Idle after reset + settle time.
// Improperly connected/unexpected hardware?
// Error path reproducable e.g. with disconnected VDD/... pins
// (optimally while busy_pin configured with local pulldown).
// -> Mark failed to avoid followup problems.
this->mark_failed(LOG_STR("Busy after reset"));
}
break; // End state loop below
default:
// Unexpected step = bug?
this->mark_failed();
}
this->step_ = FSMState::INIT_STEP0_REGULARINIT; // reset for initialize state
return true;
}
bool EPaperJD79660::initialise(bool partial) {
switch (this->step_) {
case FSMState::INIT_STEP0_REGULARINIT:
// Step #0: Regular init sequence
ESP_LOGVV(TAG, "init #0");
if (!EPaperBase::initialise(partial)) { // Call parent impl
return false; // If parent should request another loop, do so
}
// Fast init requested + supported?
if (partial && (this->fast_update_length_ > 0)) {
this->step_ = FSMState::INIT_STEP1_FASTINIT;
this->wait_for_idle_(true); // Must wait for idle before fastinit sequence in next loop
return false; // another loop: step #1 below
}
break; // End state loop below
case FSMState::INIT_STEP1_FASTINIT:
// Step #1: Fast init sequence
ESP_LOGVV(TAG, "init #1");
this->write_fastinit_();
break; // End state loop below
default:
// Unexpected step = bug?
this->mark_failed();
}
this->step_ = FSMState::NONE;
return true; // Finished: State transition waits for idle
}
bool EPaperJD79660::transfer_buffer_chunks_() {
size_t buf_idx = 0;
uint8_t bytes_to_send[MAX_TRANSFER_SIZE];
const uint32_t start_time = App.get_loop_component_start_time();
const auto buffer_length = this->buffer_length_;
while (this->current_data_index_ != buffer_length) {
bytes_to_send[buf_idx++] = this->buffer_[this->current_data_index_++];
if (buf_idx == sizeof bytes_to_send) {
this->start_data_();
this->write_array(bytes_to_send, buf_idx);
this->disable();
ESP_LOGVV(TAG, "Wrote %zu bytes at %ums", buf_idx, (unsigned) millis());
buf_idx = 0;
if (millis() - start_time > MAX_TRANSFER_TIME) {
// Let the main loop run and come back next loop
return false;
}
}
}
// Finished the entire dataset
if (buf_idx != 0) {
this->start_data_();
this->write_array(bytes_to_send, buf_idx);
this->disable();
ESP_LOGVV(TAG, "Wrote %zu bytes at %ums", buf_idx, (unsigned) millis());
}
// Cleanup for next transfer
this->current_data_index_ = 0;
// Finished with all buffer chunks
return true;
}
void EPaperJD79660::write_fastinit_() {
// Undocumented register sequence in vendor register range.
// Related to Fast Init/Update.
// Should likely happen after regular init seq and power on, but before refresh.
// Might only work for some models with certain factory MTP.
// Please do not change without knowledge to avoid breakage.
this->send_init_sequence_(this->fast_update_, this->fast_update_length_);
}
bool EPaperJD79660::transfer_data() {
// For now always send full frame buffer in chunks.
// JD79660 might support partial window transfers. But sample code missing.
// And likely minimal impact, solely on SPI transfer time into RAM.
if (this->current_data_index_ == 0) {
this->command(CMD_TRANSFER);
}
return this->transfer_buffer_chunks_();
}
void EPaperJD79660::refresh_screen([[maybe_unused]] bool partial) {
ESP_LOGV(TAG, "Refresh");
this->cmd_data(CMD_REFRESH, {(uint8_t) 0x00});
}
void EPaperJD79660::power_off() {
ESP_LOGV(TAG, "Power off");
this->cmd_data(CMD_POWEROFF, {(uint8_t) 0x00});
}
void EPaperJD79660::deep_sleep() {
ESP_LOGV(TAG, "Deep sleep");
// "Deepsleep between update": Ensure EPD sleep to avoid early hardware wearout!
this->cmd_data(CMD_DEEPSLEEP, {(uint8_t) 0xA5});
// Notes:
// - VDD: Some boards (Waveshare) with "clever reset logic" would allow switching off
// EPD VDD by pulling reset pin low for longer time.
// However, a) not all boards have this, b) reliable sequence timing is difficult,
// c) saving is not worth it after deepsleep command above.
// If needed: Better option is to drive VDD via MOSFET with separate enable pin.
//
// - Possible safe shutdown:
// EPaperBase::on_safe_shutdown() may also trigger deep_sleep() again.
// Regularly, in IDLE state, this does not make sense for this "deepsleep between update" model,
// but SPI sequence should simply be ignored by sleeping receiver.
// But if triggering during lengthy update, this quick SPI sleep sequence may have benefit.
// Optimally, EPDs should even be set all white for longer storage.
// But full sequence (>15s) not possible w/o app logic.
}
} // namespace esphome::epaper_spi

View File

@@ -0,0 +1,145 @@
#pragma once
#include "epaper_spi.h"
namespace esphome::epaper_spi {
/**
* JD7966x IC driver implementation
*
* Currently tested with:
* - JD79660 (max res: 200x200)
*
* May also work for other JD7966x chipset family members with minimal adaptations.
*
* Capabilities:
* - HW frame buffer layout:
* 4 colors (gray0..3, commonly BWYR). Bytes consist of 4px/2bpp.
* Width must be rounded to multiple of 4.
* - Fast init/update (shorter wave forms): Yes. Controlled by CONF_FULL_UPDATE_EVERY.
* Needs undocumented fastinit sequence, based on likely vendor specific MTP content.
* - Partial transfer (transfer only changed window): No. Maybe possible by HW.
* - Partial refresh (refresh only changed window): No. Likely HW limit.
*
* @internal \c final saves few bytes by devirtualization. Remove \c final when subclassing.
*/
class EPaperJD79660 final : public EPaperBase {
public:
EPaperJD79660(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
size_t init_sequence_length, const uint8_t *fast_update, uint16_t fast_update_length)
: EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_COLOR),
fast_update_(fast_update),
fast_update_length_(fast_update_length) {
this->row_width_ = (width + 3) / 4; // Fix base class calc (2bpp instead of 1bpp)
this->buffer_length_ = this->row_width_ * height;
}
void fill(Color color) override;
protected:
/** Draw colored pixel into frame buffer */
void draw_pixel_at(int x, int y, Color color) override;
/** Reset (multistep sequence)
* @pre this->reset_pin_ != nullptr // cv.Required check
* @post Should be idle on successful reset. Can mark failures.
*/
bool reset() override;
/** Initialise (multistep sequence) */
bool initialise(bool partial) override;
/** Buffer transfer */
bool transfer_data() override;
/** Power on: Already part of init sequence (likely needed there before transferring buffers).
* So nothing to do in FSM state.
*/
void power_on() override {}
/** Refresh screen
* @param partial Ignored: Needed earlier in \a ::initialize
* @pre Must be idle.
* @post Should return to idle later after processing.
*/
void refresh_screen([[maybe_unused]] bool partial) override;
/** Power off
* @pre Must be idle.
* @post Should return to idle later after processing.
* (latter will take long period like ~15-20s on actual refresh!)
*/
void power_off() override;
/** Deepsleep: Must be used to avoid hardware wearout!
* @pre Must be idle.
* @post Will go busy, and not return idle till ::reset!
*/
void deep_sleep() override;
/** Internal: Send fast init sequence via undocumented vendor registers
* @pre Must be directly after regular ::initialise sequence, before ::transfer_data
* @pre Must be idle.
* @post Should return to idle later after processing.
*/
void write_fastinit_();
/** Internal: Send raw buffer in chunks
* \retval true Finished
* \retval false Loop time elapsed. Need to call again next loop.
*/
bool transfer_buffer_chunks_();
/** @name IC commands @{ */
static constexpr uint8_t CMD_POWEROFF = 0x02;
static constexpr uint8_t CMD_DEEPSLEEP = 0x07;
static constexpr uint8_t CMD_TRANSFER = 0x10;
static constexpr uint8_t CMD_REFRESH = 0x12;
/** @} */
/** State machine constants for \a step_ */
enum class FSMState : uint8_t {
NONE = 0, //!< Initial/default value: Unused
/* Reset state steps */
RESET_STEP0_H,
RESET_STEP1_L,
RESET_STEP2_IDLECHECK,
/* Init state steps */
INIT_STEP0_REGULARINIT,
INIT_STEP1_FASTINIT,
};
/** Wait time (millisec) for first reset phase: High
*
* Wait via FSM loop.
*/
static constexpr uint16_t SLEEP_MS_RESET0 = 200;
/** Wait time (millisec) for second reset phase: Low
*
* Holding Reset Low too long may trigger "clever reset" logic
* of e.g. Waveshare Rev2 boards: VDD is shut down via MOSFET, and IC
* will not report idle anymore!
* FSM loop may spuriously increase delay, e.g. >16ms.
* Therefore, sync wait below, as allowed (code rule "delays > 10ms not permitted"),
* yet only slightly exceeding known IC min req of >1.5ms.
*/
static constexpr uint16_t SLEEP_MS_RESET1 = 2;
/** Wait time (millisec) for third reset phase: High
*
* Wait via FSM loop.
*/
static constexpr uint16_t SLEEP_MS_RESET2 = 200;
// properties initialised in the constructor
const uint8_t *const fast_update_{};
const uint16_t fast_update_length_{};
/** Counter for tracking substeps within FSM state */
FSMState step_{FSMState::NONE};
};
} // namespace esphome::epaper_spi

View File

@@ -0,0 +1,86 @@
import esphome.codegen as cg
from esphome.components.mipi import flatten_sequence
import esphome.config_validation as cv
from esphome.const import CONF_BUSY_PIN, CONF_RESET_PIN
from esphome.core import ID
from ..display import CONF_INIT_SEQUENCE_ID
from . import EpaperModel
class JD79660(EpaperModel):
def __init__(self, name, class_name="EPaperJD79660", fast_update=None, **kwargs):
super().__init__(name, class_name, **kwargs)
self.fast_update = fast_update
def option(self, name, fallback=cv.UNDEFINED) -> cv.Optional | cv.Required:
# Validate required pins, as C++ code will assume existence
if name in (CONF_RESET_PIN, CONF_BUSY_PIN):
return cv.Required(name)
# Delegate to parent
return super().option(name, fallback)
def get_constructor_args(self, config) -> tuple:
# Resembles init_sequence handling for fast_update config
if self.fast_update is None:
fast_update = cg.nullptr, 0
else:
flat_fast_update = flatten_sequence(self.fast_update)
fast_update = (
cg.static_const_array(
ID(
config[CONF_INIT_SEQUENCE_ID].id + "_fast_update", type=cg.uint8
),
flat_fast_update,
),
len(flat_fast_update),
)
return (*fast_update,)
jd79660 = JD79660(
"jd79660",
# Specified refresh times are ~20s (full) or ~15s (fast) due to BWRY.
# So disallow low update intervals (with safety margin), to avoid e.g. FSM update loops.
# Even less frequent intervals (min/h) highly recommended to optimize lifetime!
minimum_update_interval="30s",
# SPI rate: From spec comparisons, IC should allow SCL write cycles up to 10MHz rate.
# Existing code samples also prefer 10MHz. So justifies as default.
# Decrease value further in user config if needed (e.g. poor cabling).
data_rate="10MHz",
# No need to set optional reset_duration:
# Code requires multistep reset sequence with precise timings
# according to data sheet or samples.
)
# Waveshare 1.54-G
#
# Device may have specific factory provisioned MTP content to facilitate vendor register features like fast init.
# Vendor specific init derived from vendor sample code
# <https://github.com/waveshareteam/e-Paper/blob/master/E-paper_Separate_Program/1in54_e-Paper_G/ESP32/EPD_1in54g.cpp>
# Compatible MIT license, see esphome/LICENSE file.
#
# fmt: off
jd79660.extend(
"Waveshare-1.54in-G",
width=200,
height=200,
initsequence=(
(0x4D, 0x78,),
(0x00, 0x0F, 0x29,),
(0x06, 0x0d, 0x12, 0x30, 0x20, 0x19, 0x2a, 0x22,),
(0x50, 0x37,),
(0x61, 200 // 256, 200 % 256, 200 // 256, 200 % 256,), # RES: 200x200 fixed
(0xE9, 0x01,),
(0x30, 0x08,),
# Power On (0x04): Must be early part of init seq = Disabled later!
(0x04,),
),
fast_update=(
(0xE0, 0x02,),
(0xE6, 0x5D,),
(0xA5, 0x00,),
),
)

View File

@@ -445,6 +445,52 @@ ColorMode LightCall::compute_color_mode_() {
LOG_STR_ARG(color_mode_to_human(color_mode))); LOG_STR_ARG(color_mode_to_human(color_mode)));
return color_mode; return color_mode;
} }
// PROGMEM lookup table for get_suitable_color_modes_mask_().
// Maps 4-bit key (white | ct<<1 | cwww<<2 | rgb<<3) to color mode bitmask.
// Packed into uint8_t by right-shifting by PACK_SHIFT since the lower bits
// (UNKNOWN, ON_OFF, BRIGHTNESS) are never present in suitable mode masks.
static constexpr unsigned PACK_SHIFT = ColorModeBitPolicy::to_bit(ColorMode::WHITE);
// clang-format off
static constexpr uint8_t SUITABLE_COLOR_MODES[] PROGMEM = {
// [0] none - all modes with brightness
static_cast<uint8_t>(ColorModeMask({ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE,
ColorMode::RGB_COLD_WARM_WHITE, ColorMode::RGB, ColorMode::WHITE, ColorMode::COLOR_TEMPERATURE,
ColorMode::COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT),
// [1] white only
static_cast<uint8_t>(ColorModeMask({ColorMode::WHITE, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE,
ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT),
// [2] ct only
static_cast<uint8_t>(ColorModeMask({ColorMode::COLOR_TEMPERATURE, ColorMode::RGB_COLOR_TEMPERATURE,
ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT),
// [3] white + ct
static_cast<uint8_t>(ColorModeMask({ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLOR_TEMPERATURE,
ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT),
// [4] cwww only
static_cast<uint8_t>(ColorModeMask({ColorMode::COLD_WARM_WHITE,
ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT),
0, // [5] white + cwww (conflicting)
0, // [6] ct + cwww (conflicting)
0, // [7] white + ct + cwww (conflicting)
// [8] rgb only
static_cast<uint8_t>(ColorModeMask({ColorMode::RGB, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE,
ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT),
// [9] rgb + white
static_cast<uint8_t>(ColorModeMask({ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE,
ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT),
// [10] rgb + ct
static_cast<uint8_t>(ColorModeMask({ColorMode::RGB_COLOR_TEMPERATURE,
ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT),
// [11] rgb + white + ct
static_cast<uint8_t>(ColorModeMask({ColorMode::RGB_COLOR_TEMPERATURE,
ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT),
// [12] rgb + cwww
static_cast<uint8_t>(ColorModeMask({ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT),
0, // [13] rgb + white + cwww (conflicting)
0, // [14] rgb + ct + cwww (conflicting)
0, // [15] all (conflicting)
};
// clang-format on
color_mode_bitmask_t LightCall::get_suitable_color_modes_mask_() { color_mode_bitmask_t LightCall::get_suitable_color_modes_mask_() {
bool has_white = this->has_white() && this->white_ > 0.0f; bool has_white = this->has_white() && this->white_ > 0.0f;
bool has_ct = this->has_color_temperature(); bool has_ct = this->has_color_temperature();
@@ -454,46 +500,8 @@ color_mode_bitmask_t LightCall::get_suitable_color_modes_mask_() {
(this->has_red() || this->has_green() || this->has_blue()); (this->has_red() || this->has_green() || this->has_blue());
// Build key from flags: [rgb][cwww][ct][white] // Build key from flags: [rgb][cwww][ct][white]
#define KEY(white, ct, cwww, rgb) ((white) << 0 | (ct) << 1 | (cwww) << 2 | (rgb) << 3) uint8_t key = has_white | (has_ct << 1) | (has_cwww << 2) | (has_rgb << 3);
return static_cast<color_mode_bitmask_t>(progmem_read_byte(&SUITABLE_COLOR_MODES[key])) << PACK_SHIFT;
uint8_t key = KEY(has_white, has_ct, has_cwww, has_rgb);
switch (key) {
case KEY(true, false, false, false): // white only
return ColorModeMask({ColorMode::WHITE, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE,
ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE})
.get_mask();
case KEY(false, true, false, false): // ct only
return ColorModeMask({ColorMode::COLOR_TEMPERATURE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE,
ColorMode::RGB_COLD_WARM_WHITE})
.get_mask();
case KEY(true, true, false, false): // white + ct
return ColorModeMask(
{ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE})
.get_mask();
case KEY(false, false, true, false): // cwww only
return ColorModeMask({ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE}).get_mask();
case KEY(false, false, false, false): // none
return ColorModeMask({ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE,
ColorMode::RGB, ColorMode::WHITE, ColorMode::COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE})
.get_mask();
case KEY(true, false, false, true): // rgb + white
return ColorModeMask({ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE})
.get_mask();
case KEY(false, true, false, true): // rgb + ct
case KEY(true, true, false, true): // rgb + white + ct
return ColorModeMask({ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}).get_mask();
case KEY(false, false, true, true): // rgb + cwww
return ColorModeMask({ColorMode::RGB_COLD_WARM_WHITE}).get_mask();
case KEY(false, false, false, true): // rgb only
return ColorModeMask({ColorMode::RGB, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE,
ColorMode::RGB_COLD_WARM_WHITE})
.get_mask();
default:
return 0; // conflicting flags
}
#undef KEY
} }
LightCall &LightCall::set_effect(const char *effect, size_t len) { LightCall &LightCall::set_effect(const char *effect, size_t len) {

View File

@@ -13,31 +13,12 @@ static const char *const TAG = "mqtt.alarm_control_panel";
using namespace esphome::alarm_control_panel; using namespace esphome::alarm_control_panel;
// Alarm state MQTT strings indexed by AlarmControlPanelState enum (0-9)
PROGMEM_STRING_TABLE(AlarmMqttStateStrings, "disarmed", "armed_home", "armed_away", "armed_night", "armed_vacation",
"armed_custom_bypass", "pending", "arming", "disarming", "triggered", "unknown");
static ProgmemStr alarm_state_to_mqtt_str(AlarmControlPanelState state) { static ProgmemStr alarm_state_to_mqtt_str(AlarmControlPanelState state) {
switch (state) { return AlarmMqttStateStrings::get_progmem_str(static_cast<uint8_t>(state), AlarmMqttStateStrings::LAST_INDEX);
case ACP_STATE_DISARMED:
return ESPHOME_F("disarmed");
case ACP_STATE_ARMED_HOME:
return ESPHOME_F("armed_home");
case ACP_STATE_ARMED_AWAY:
return ESPHOME_F("armed_away");
case ACP_STATE_ARMED_NIGHT:
return ESPHOME_F("armed_night");
case ACP_STATE_ARMED_VACATION:
return ESPHOME_F("armed_vacation");
case ACP_STATE_ARMED_CUSTOM_BYPASS:
return ESPHOME_F("armed_custom_bypass");
case ACP_STATE_PENDING:
return ESPHOME_F("pending");
case ACP_STATE_ARMING:
return ESPHOME_F("arming");
case ACP_STATE_DISARMING:
return ESPHOME_F("disarming");
case ACP_STATE_TRIGGERED:
return ESPHOME_F("triggered");
default:
return ESPHOME_F("unknown");
}
} }
MQTTAlarmControlPanelComponent::MQTTAlarmControlPanelComponent(AlarmControlPanel *alarm_control_panel) MQTTAlarmControlPanelComponent::MQTTAlarmControlPanelComponent(AlarmControlPanel *alarm_control_panel)

View File

@@ -8,6 +8,7 @@
#include "esphome/core/entity_base.h" #include "esphome/core/entity_base.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include "esphome/core/version.h" #include "esphome/core/version.h"
#ifdef USE_LOGGER #ifdef USE_LOGGER
#include "esphome/components/logger/logger.h" #include "esphome/components/logger/logger.h"
@@ -27,6 +28,11 @@ namespace esphome::mqtt {
static const char *const TAG = "mqtt"; static const char *const TAG = "mqtt";
// Disconnect reason strings indexed by MQTTClientDisconnectReason enum (0-8)
PROGMEM_STRING_TABLE(MQTTDisconnectReasonStrings, "TCP disconnected", "Unacceptable Protocol Version",
"Identifier Rejected", "Server Unavailable", "Malformed Credentials", "Not Authorized",
"Not Enough Space", "TLS Bad Fingerprint", "DNS Resolve Error", "Unknown");
MQTTClientComponent::MQTTClientComponent() { MQTTClientComponent::MQTTClientComponent() {
global_mqtt_client = this; global_mqtt_client = this;
char mac_addr[MAC_ADDRESS_BUFFER_SIZE]; char mac_addr[MAC_ADDRESS_BUFFER_SIZE];
@@ -348,36 +354,8 @@ void MQTTClientComponent::loop() {
mqtt_backend_.loop(); mqtt_backend_.loop();
if (this->disconnect_reason_.has_value()) { if (this->disconnect_reason_.has_value()) {
const LogString *reason_s; const LogString *reason_s = MQTTDisconnectReasonStrings::get_log_str(
switch (*this->disconnect_reason_) { static_cast<uint8_t>(*this->disconnect_reason_), MQTTDisconnectReasonStrings::LAST_INDEX);
case MQTTClientDisconnectReason::TCP_DISCONNECTED:
reason_s = LOG_STR("TCP disconnected");
break;
case MQTTClientDisconnectReason::MQTT_UNACCEPTABLE_PROTOCOL_VERSION:
reason_s = LOG_STR("Unacceptable Protocol Version");
break;
case MQTTClientDisconnectReason::MQTT_IDENTIFIER_REJECTED:
reason_s = LOG_STR("Identifier Rejected");
break;
case MQTTClientDisconnectReason::MQTT_SERVER_UNAVAILABLE:
reason_s = LOG_STR("Server Unavailable");
break;
case MQTTClientDisconnectReason::MQTT_MALFORMED_CREDENTIALS:
reason_s = LOG_STR("Malformed Credentials");
break;
case MQTTClientDisconnectReason::MQTT_NOT_AUTHORIZED:
reason_s = LOG_STR("Not Authorized");
break;
case MQTTClientDisconnectReason::ESP8266_NOT_ENOUGH_SPACE:
reason_s = LOG_STR("Not Enough Space");
break;
case MQTTClientDisconnectReason::TLS_BAD_FINGERPRINT:
reason_s = LOG_STR("TLS Bad Fingerprint");
break;
default:
reason_s = LOG_STR("Unknown");
break;
}
if (!network::is_connected()) { if (!network::is_connected()) {
reason_s = LOG_STR("WiFi disconnected"); reason_s = LOG_STR("WiFi disconnected");
} }

View File

@@ -13,109 +13,44 @@ static const char *const TAG = "mqtt.climate";
using namespace esphome::climate; using namespace esphome::climate;
// Climate mode MQTT strings indexed by ClimateMode enum (0-6): OFF, HEAT_COOL, COOL, HEAT, FAN_ONLY, DRY, AUTO
PROGMEM_STRING_TABLE(ClimateMqttModeStrings, "off", "heat_cool", "cool", "heat", "fan_only", "dry", "auto", "unknown");
static ProgmemStr climate_mode_to_mqtt_str(ClimateMode mode) { static ProgmemStr climate_mode_to_mqtt_str(ClimateMode mode) {
switch (mode) { return ClimateMqttModeStrings::get_progmem_str(static_cast<uint8_t>(mode), ClimateMqttModeStrings::LAST_INDEX);
case CLIMATE_MODE_OFF:
return ESPHOME_F("off");
case CLIMATE_MODE_HEAT_COOL:
return ESPHOME_F("heat_cool");
case CLIMATE_MODE_AUTO:
return ESPHOME_F("auto");
case CLIMATE_MODE_COOL:
return ESPHOME_F("cool");
case CLIMATE_MODE_HEAT:
return ESPHOME_F("heat");
case CLIMATE_MODE_FAN_ONLY:
return ESPHOME_F("fan_only");
case CLIMATE_MODE_DRY:
return ESPHOME_F("dry");
default:
return ESPHOME_F("unknown");
}
} }
// Climate action MQTT strings indexed by ClimateAction enum (0,2-6): OFF, (gap), COOLING, HEATING, IDLE, DRYING, FAN
PROGMEM_STRING_TABLE(ClimateMqttActionStrings, "off", "unknown", "cooling", "heating", "idle", "drying", "fan",
"unknown");
static ProgmemStr climate_action_to_mqtt_str(ClimateAction action) { static ProgmemStr climate_action_to_mqtt_str(ClimateAction action) {
switch (action) { return ClimateMqttActionStrings::get_progmem_str(static_cast<uint8_t>(action), ClimateMqttActionStrings::LAST_INDEX);
case CLIMATE_ACTION_OFF:
return ESPHOME_F("off");
case CLIMATE_ACTION_COOLING:
return ESPHOME_F("cooling");
case CLIMATE_ACTION_HEATING:
return ESPHOME_F("heating");
case CLIMATE_ACTION_IDLE:
return ESPHOME_F("idle");
case CLIMATE_ACTION_DRYING:
return ESPHOME_F("drying");
case CLIMATE_ACTION_FAN:
return ESPHOME_F("fan");
default:
return ESPHOME_F("unknown");
}
} }
// Climate fan mode MQTT strings indexed by ClimateFanMode enum (0-9)
PROGMEM_STRING_TABLE(ClimateMqttFanModeStrings, "on", "off", "auto", "low", "medium", "high", "middle", "focus",
"diffuse", "quiet", "unknown");
static ProgmemStr climate_fan_mode_to_mqtt_str(ClimateFanMode fan_mode) { static ProgmemStr climate_fan_mode_to_mqtt_str(ClimateFanMode fan_mode) {
switch (fan_mode) { return ClimateMqttFanModeStrings::get_progmem_str(static_cast<uint8_t>(fan_mode),
case CLIMATE_FAN_ON: ClimateMqttFanModeStrings::LAST_INDEX);
return ESPHOME_F("on");
case CLIMATE_FAN_OFF:
return ESPHOME_F("off");
case CLIMATE_FAN_AUTO:
return ESPHOME_F("auto");
case CLIMATE_FAN_LOW:
return ESPHOME_F("low");
case CLIMATE_FAN_MEDIUM:
return ESPHOME_F("medium");
case CLIMATE_FAN_HIGH:
return ESPHOME_F("high");
case CLIMATE_FAN_MIDDLE:
return ESPHOME_F("middle");
case CLIMATE_FAN_FOCUS:
return ESPHOME_F("focus");
case CLIMATE_FAN_DIFFUSE:
return ESPHOME_F("diffuse");
case CLIMATE_FAN_QUIET:
return ESPHOME_F("quiet");
default:
return ESPHOME_F("unknown");
}
} }
// Climate swing mode MQTT strings indexed by ClimateSwingMode enum (0-3): OFF, BOTH, VERTICAL, HORIZONTAL
PROGMEM_STRING_TABLE(ClimateMqttSwingModeStrings, "off", "both", "vertical", "horizontal", "unknown");
static ProgmemStr climate_swing_mode_to_mqtt_str(ClimateSwingMode swing_mode) { static ProgmemStr climate_swing_mode_to_mqtt_str(ClimateSwingMode swing_mode) {
switch (swing_mode) { return ClimateMqttSwingModeStrings::get_progmem_str(static_cast<uint8_t>(swing_mode),
case CLIMATE_SWING_OFF: ClimateMqttSwingModeStrings::LAST_INDEX);
return ESPHOME_F("off");
case CLIMATE_SWING_BOTH:
return ESPHOME_F("both");
case CLIMATE_SWING_VERTICAL:
return ESPHOME_F("vertical");
case CLIMATE_SWING_HORIZONTAL:
return ESPHOME_F("horizontal");
default:
return ESPHOME_F("unknown");
}
} }
// Climate preset MQTT strings indexed by ClimatePreset enum (0-7)
PROGMEM_STRING_TABLE(ClimateMqttPresetStrings, "none", "home", "away", "boost", "comfort", "eco", "sleep", "activity",
"unknown");
static ProgmemStr climate_preset_to_mqtt_str(ClimatePreset preset) { static ProgmemStr climate_preset_to_mqtt_str(ClimatePreset preset) {
switch (preset) { return ClimateMqttPresetStrings::get_progmem_str(static_cast<uint8_t>(preset), ClimateMqttPresetStrings::LAST_INDEX);
case CLIMATE_PRESET_NONE:
return ESPHOME_F("none");
case CLIMATE_PRESET_HOME:
return ESPHOME_F("home");
case CLIMATE_PRESET_ECO:
return ESPHOME_F("eco");
case CLIMATE_PRESET_AWAY:
return ESPHOME_F("away");
case CLIMATE_PRESET_BOOST:
return ESPHOME_F("boost");
case CLIMATE_PRESET_COMFORT:
return ESPHOME_F("comfort");
case CLIMATE_PRESET_SLEEP:
return ESPHOME_F("sleep");
case CLIMATE_PRESET_ACTIVITY:
return ESPHOME_F("activity");
default:
return ESPHOME_F("unknown");
}
} }
void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {

View File

@@ -14,6 +14,9 @@ namespace esphome::mqtt {
static const char *const TAG = "mqtt.component"; static const char *const TAG = "mqtt.component";
// Entity category MQTT strings indexed by EntityCategory enum: NONE(0) is skipped, CONFIG(1), DIAGNOSTIC(2)
PROGMEM_STRING_TABLE(EntityCategoryMqttStrings, "", "config", "diagnostic");
// Helper functions for building topic strings on stack // Helper functions for building topic strings on stack
inline char *append_str(char *p, const char *s, size_t len) { inline char *append_str(char *p, const char *s, size_t len) {
memcpy(p, s, len); memcpy(p, s, len);
@@ -213,13 +216,9 @@ bool MQTTComponent::send_discovery_() {
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
const auto entity_category = this->get_entity()->get_entity_category(); const auto entity_category = this->get_entity()->get_entity_category();
switch (entity_category) { if (entity_category != ENTITY_CATEGORY_NONE) {
case ENTITY_CATEGORY_NONE: root[MQTT_ENTITY_CATEGORY] = EntityCategoryMqttStrings::get_progmem_str(
break; static_cast<uint8_t>(entity_category), static_cast<uint8_t>(ENTITY_CATEGORY_CONFIG));
case ENTITY_CATEGORY_CONFIG:
case ENTITY_CATEGORY_DIAGNOSTIC:
root[MQTT_ENTITY_CATEGORY] = entity_category == ENTITY_CATEGORY_CONFIG ? "config" : "diagnostic";
break;
} }
if (config.state_topic) { if (config.state_topic) {

View File

@@ -1,5 +1,6 @@
#include "mqtt_number.h" #include "mqtt_number.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include "mqtt_const.h" #include "mqtt_const.h"
@@ -12,6 +13,9 @@ static const char *const TAG = "mqtt.number";
using namespace esphome::number; using namespace esphome::number;
// Number mode MQTT strings indexed by NumberMode enum: AUTO(0) is skipped, BOX(1), SLIDER(2)
PROGMEM_STRING_TABLE(NumberMqttModeStrings, "", "box", "slider");
MQTTNumberComponent::MQTTNumberComponent(Number *number) : number_(number) {} MQTTNumberComponent::MQTTNumberComponent(Number *number) : number_(number) {}
void MQTTNumberComponent::setup() { void MQTTNumberComponent::setup() {
@@ -48,15 +52,10 @@ void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon
if (!unit_of_measurement.empty()) { if (!unit_of_measurement.empty()) {
root[MQTT_UNIT_OF_MEASUREMENT] = unit_of_measurement; root[MQTT_UNIT_OF_MEASUREMENT] = unit_of_measurement;
} }
switch (this->number_->traits.get_mode()) { const auto mode = this->number_->traits.get_mode();
case NUMBER_MODE_AUTO: if (mode != NUMBER_MODE_AUTO) {
break; root[MQTT_MODE] =
case NUMBER_MODE_BOX: NumberMqttModeStrings::get_progmem_str(static_cast<uint8_t>(mode), static_cast<uint8_t>(NUMBER_MODE_BOX));
root[MQTT_MODE] = "box";
break;
case NUMBER_MODE_SLIDER:
root[MQTT_MODE] = "slider";
break;
} }
const auto device_class = this->number_->traits.get_device_class_ref(); const auto device_class = this->number_->traits.get_device_class_ref();
if (!device_class.empty()) { if (!device_class.empty()) {

View File

@@ -1,5 +1,6 @@
#include "mqtt_text.h" #include "mqtt_text.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include "mqtt_const.h" #include "mqtt_const.h"
@@ -12,6 +13,9 @@ static const char *const TAG = "mqtt.text";
using namespace esphome::text; using namespace esphome::text;
// Text mode MQTT strings indexed by TextMode enum (0-1): TEXT, PASSWORD
PROGMEM_STRING_TABLE(TextMqttModeStrings, "text", "password");
MQTTTextComponent::MQTTTextComponent(Text *text) : text_(text) {} MQTTTextComponent::MQTTTextComponent(Text *text) : text_(text) {}
void MQTTTextComponent::setup() { void MQTTTextComponent::setup() {
@@ -34,14 +38,8 @@ const EntityBase *MQTTTextComponent::get_entity() const { return this->text_; }
void MQTTTextComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { void MQTTTextComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
switch (this->text_->traits.get_mode()) { root[MQTT_MODE] = TextMqttModeStrings::get_progmem_str(static_cast<uint8_t>(this->text_->traits.get_mode()),
case TEXT_MODE_TEXT: static_cast<uint8_t>(TEXT_MODE_TEXT));
root[MQTT_MODE] = "text";
break;
case TEXT_MODE_PASSWORD:
root[MQTT_MODE] = "password";
break;
}
config.command_topic = true; config.command_topic = true;
} }

View File

@@ -5,6 +5,7 @@
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/progmem.h"
namespace esphome::template_ { namespace esphome::template_ {
@@ -28,18 +29,11 @@ void TemplateAlarmControlPanel::add_sensor(binary_sensor::BinarySensor *sensor,
this->sensor_data_.push_back(sd); this->sensor_data_.push_back(sd);
}; };
// Alarm sensor type strings indexed by AlarmSensorType enum (0-3): DELAYED, INSTANT, DELAYED_FOLLOWER, INSTANT_ALWAYS
PROGMEM_STRING_TABLE(AlarmSensorTypeStrings, "delayed", "instant", "delayed_follower", "instant_always");
static const LogString *sensor_type_to_string(AlarmSensorType type) { static const LogString *sensor_type_to_string(AlarmSensorType type) {
switch (type) { return AlarmSensorTypeStrings::get_log_str(static_cast<uint8_t>(type), 0);
case ALARM_SENSOR_TYPE_INSTANT:
return LOG_STR("instant");
case ALARM_SENSOR_TYPE_DELAYED_FOLLOWER:
return LOG_STR("delayed_follower");
case ALARM_SENSOR_TYPE_INSTANT_ALWAYS:
return LOG_STR("instant_always");
case ALARM_SENSOR_TYPE_DELAYED:
default:
return LOG_STR("delayed");
}
} }
#endif #endif

View File

@@ -26,7 +26,7 @@ enum BinarySensorFlags : uint16_t {
BINARY_SENSOR_MODE_BYPASS_AUTO = 1 << 4, BINARY_SENSOR_MODE_BYPASS_AUTO = 1 << 4,
}; };
enum AlarmSensorType : uint16_t { enum AlarmSensorType : uint8_t {
ALARM_SENSOR_TYPE_DELAYED = 0, ALARM_SENSOR_TYPE_DELAYED = 0,
ALARM_SENSOR_TYPE_INSTANT, ALARM_SENSOR_TYPE_INSTANT,
ALARM_SENSOR_TYPE_DELAYED_FOLLOWER, ALARM_SENSOR_TYPE_DELAYED_FOLLOWER,

View File

@@ -46,6 +46,7 @@ CONFIG_SCHEMA = (
RESTORE_MODES, upper=True RESTORE_MODES, upper=True
), ),
cv.Optional(CONF_CURRENT_TEMPERATURE): cv.returning_lambda, cv.Optional(CONF_CURRENT_TEMPERATURE): cv.returning_lambda,
cv.Optional(CONF_TARGET_TEMPERATURE): cv.returning_lambda,
cv.Optional(CONF_MODE): cv.returning_lambda, cv.Optional(CONF_MODE): cv.returning_lambda,
cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list( cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list(
water_heater.validate_water_heater_mode water_heater.validate_water_heater_mode
@@ -78,6 +79,14 @@ async def to_code(config: ConfigType) -> None:
) )
cg.add(var.set_current_temperature_lambda(template_)) cg.add(var.set_current_temperature_lambda(template_))
if CONF_TARGET_TEMPERATURE in config:
template_ = await cg.process_lambda(
config[CONF_TARGET_TEMPERATURE],
[],
return_type=cg.optional.template(cg.float_),
)
cg.add(var.set_target_temperature_lambda(template_))
if CONF_MODE in config: if CONF_MODE in config:
template_ = await cg.process_lambda( template_ = await cg.process_lambda(
config[CONF_MODE], config[CONF_MODE],

View File

@@ -16,7 +16,8 @@ void TemplateWaterHeater::setup() {
restore->perform(); restore->perform();
} }
} }
if (!this->current_temperature_f_.has_value() && !this->mode_f_.has_value()) if (!this->current_temperature_f_.has_value() && !this->target_temperature_f_.has_value() &&
!this->mode_f_.has_value())
this->disable_loop(); this->disable_loop();
} }
@@ -28,6 +29,9 @@ water_heater::WaterHeaterTraits TemplateWaterHeater::traits() {
} }
traits.set_supports_current_temperature(true); traits.set_supports_current_temperature(true);
if (this->target_temperature_f_.has_value()) {
traits.add_feature_flags(water_heater::WATER_HEATER_SUPPORTS_TARGET_TEMPERATURE);
}
return traits; return traits;
} }
@@ -42,6 +46,14 @@ void TemplateWaterHeater::loop() {
} }
} }
auto target_temp = this->target_temperature_f_.call();
if (target_temp.has_value()) {
if (*target_temp != this->target_temperature_) {
this->target_temperature_ = *target_temp;
changed = true;
}
}
auto new_mode = this->mode_f_.call(); auto new_mode = this->mode_f_.call();
if (new_mode.has_value()) { if (new_mode.has_value()) {
if (*new_mode != this->mode_) { if (*new_mode != this->mode_) {

View File

@@ -20,6 +20,9 @@ class TemplateWaterHeater : public Component, public water_heater::WaterHeater {
template<typename F> void set_current_temperature_lambda(F &&f) { template<typename F> void set_current_temperature_lambda(F &&f) {
this->current_temperature_f_.set(std::forward<F>(f)); this->current_temperature_f_.set(std::forward<F>(f));
} }
template<typename F> void set_target_temperature_lambda(F &&f) {
this->target_temperature_f_.set(std::forward<F>(f));
}
template<typename F> void set_mode_lambda(F &&f) { this->mode_f_.set(std::forward<F>(f)); } template<typename F> void set_mode_lambda(F &&f) { this->mode_f_.set(std::forward<F>(f)); }
void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
@@ -44,6 +47,7 @@ class TemplateWaterHeater : public Component, public water_heater::WaterHeater {
// Ordered to minimize padding on 32-bit: 4-byte members first, then smaller // Ordered to minimize padding on 32-bit: 4-byte members first, then smaller
Trigger<> set_trigger_; Trigger<> set_trigger_;
TemplateLambda<float> current_temperature_f_; TemplateLambda<float> current_temperature_f_;
TemplateLambda<float> target_temperature_f_;
TemplateLambda<water_heater::WaterHeaterMode> mode_f_; TemplateLambda<water_heater::WaterHeaterMode> mode_f_;
TemplateWaterHeaterRestoreMode restore_mode_{WATER_HEATER_NO_RESTORE}; TemplateWaterHeaterRestoreMode restore_mode_{WATER_HEATER_NO_RESTORE};
water_heater::WaterHeaterModeMask supported_modes_; water_heater::WaterHeaterModeMask supported_modes_;

View File

@@ -236,25 +236,23 @@ static const char *const TAG = "wifi";
/// │ - Roaming fail (RECONNECTING→IDLE): counter preserved (ping-pong) │ /// │ - Roaming fail (RECONNECTING→IDLE): counter preserved (ping-pong) │
/// └──────────────────────────────────────────────────────────────────────┘ /// └──────────────────────────────────────────────────────────────────────┘
// Use if-chain instead of switch to avoid jump table in RODATA (wastes RAM on ESP8266)
static const LogString *retry_phase_to_log_string(WiFiRetryPhase phase) { static const LogString *retry_phase_to_log_string(WiFiRetryPhase phase) {
switch (phase) { if (phase == WiFiRetryPhase::INITIAL_CONNECT)
case WiFiRetryPhase::INITIAL_CONNECT: return LOG_STR("INITIAL_CONNECT");
return LOG_STR("INITIAL_CONNECT");
#ifdef USE_WIFI_FAST_CONNECT #ifdef USE_WIFI_FAST_CONNECT
case WiFiRetryPhase::FAST_CONNECT_CYCLING_APS: if (phase == WiFiRetryPhase::FAST_CONNECT_CYCLING_APS)
return LOG_STR("FAST_CONNECT_CYCLING"); return LOG_STR("FAST_CONNECT_CYCLING");
#endif #endif
case WiFiRetryPhase::EXPLICIT_HIDDEN: if (phase == WiFiRetryPhase::EXPLICIT_HIDDEN)
return LOG_STR("EXPLICIT_HIDDEN"); return LOG_STR("EXPLICIT_HIDDEN");
case WiFiRetryPhase::SCAN_CONNECTING: if (phase == WiFiRetryPhase::SCAN_CONNECTING)
return LOG_STR("SCAN_CONNECTING"); return LOG_STR("SCAN_CONNECTING");
case WiFiRetryPhase::RETRY_HIDDEN: if (phase == WiFiRetryPhase::RETRY_HIDDEN)
return LOG_STR("RETRY_HIDDEN"); return LOG_STR("RETRY_HIDDEN");
case WiFiRetryPhase::RESTARTING_ADAPTER: if (phase == WiFiRetryPhase::RESTARTING_ADAPTER)
return LOG_STR("RESTARTING"); return LOG_STR("RESTARTING");
default: return LOG_STR("UNKNOWN");
return LOG_STR("UNKNOWN");
}
} }
bool WiFiComponent::went_through_explicit_hidden_phase_() const { bool WiFiComponent::went_through_explicit_hidden_phase_() const {

View File

@@ -416,75 +416,65 @@ PROGMEM_STRING_TABLE(OpModeStrings, "OFF", "STA", "AP", "AP+STA", "UNKNOWN");
const LogString *get_op_mode_str(uint8_t mode) { return OpModeStrings::get_log_str(mode, OpModeStrings::LAST_INDEX); } const LogString *get_op_mode_str(uint8_t mode) { return OpModeStrings::get_log_str(mode, OpModeStrings::LAST_INDEX); }
// Use if-chain instead of switch to avoid jump tables in RODATA (wastes RAM on ESP8266).
// A single switch would generate a sparse lookup table with ~175 default entries, wasting 700 bytes of RAM.
// Even split switches still generate smaller jump tables in RODATA.
const LogString *get_disconnect_reason_str(uint8_t reason) { const LogString *get_disconnect_reason_str(uint8_t reason) {
/* If this were one big switch statement, GCC would generate a lookup table for it. However, the values of the if (reason == REASON_AUTH_EXPIRE)
* REASON_* constants aren't continuous, and GCC will fill in the gap with the default value -- wasting 4 bytes of RAM return LOG_STR("Auth Expired");
* per entry. As there's ~175 default entries, this wastes 700 bytes of RAM. if (reason == REASON_AUTH_LEAVE)
*/ return LOG_STR("Auth Leave");
if (reason <= REASON_CIPHER_SUITE_REJECTED) { // This must be the last constant with a value <200 if (reason == REASON_ASSOC_EXPIRE)
switch (reason) { return LOG_STR("Association Expired");
case REASON_AUTH_EXPIRE: if (reason == REASON_ASSOC_TOOMANY)
return LOG_STR("Auth Expired"); return LOG_STR("Too Many Associations");
case REASON_AUTH_LEAVE: if (reason == REASON_NOT_AUTHED)
return LOG_STR("Auth Leave"); return LOG_STR("Not Authenticated");
case REASON_ASSOC_EXPIRE: if (reason == REASON_NOT_ASSOCED)
return LOG_STR("Association Expired"); return LOG_STR("Not Associated");
case REASON_ASSOC_TOOMANY: if (reason == REASON_ASSOC_LEAVE)
return LOG_STR("Too Many Associations"); return LOG_STR("Association Leave");
case REASON_NOT_AUTHED: if (reason == REASON_ASSOC_NOT_AUTHED)
return LOG_STR("Not Authenticated"); return LOG_STR("Association not Authenticated");
case REASON_NOT_ASSOCED: if (reason == REASON_DISASSOC_PWRCAP_BAD)
return LOG_STR("Not Associated"); return LOG_STR("Disassociate Power Cap Bad");
case REASON_ASSOC_LEAVE: if (reason == REASON_DISASSOC_SUPCHAN_BAD)
return LOG_STR("Association Leave"); return LOG_STR("Disassociate Supported Channel Bad");
case REASON_ASSOC_NOT_AUTHED: if (reason == REASON_IE_INVALID)
return LOG_STR("Association not Authenticated"); return LOG_STR("IE Invalid");
case REASON_DISASSOC_PWRCAP_BAD: if (reason == REASON_MIC_FAILURE)
return LOG_STR("Disassociate Power Cap Bad"); return LOG_STR("Mic Failure");
case REASON_DISASSOC_SUPCHAN_BAD: if (reason == REASON_4WAY_HANDSHAKE_TIMEOUT)
return LOG_STR("Disassociate Supported Channel Bad"); return LOG_STR("4-Way Handshake Timeout");
case REASON_IE_INVALID: if (reason == REASON_GROUP_KEY_UPDATE_TIMEOUT)
return LOG_STR("IE Invalid"); return LOG_STR("Group Key Update Timeout");
case REASON_MIC_FAILURE: if (reason == REASON_IE_IN_4WAY_DIFFERS)
return LOG_STR("Mic Failure"); return LOG_STR("IE In 4-Way Handshake Differs");
case REASON_4WAY_HANDSHAKE_TIMEOUT: if (reason == REASON_GROUP_CIPHER_INVALID)
return LOG_STR("4-Way Handshake Timeout"); return LOG_STR("Group Cipher Invalid");
case REASON_GROUP_KEY_UPDATE_TIMEOUT: if (reason == REASON_PAIRWISE_CIPHER_INVALID)
return LOG_STR("Group Key Update Timeout"); return LOG_STR("Pairwise Cipher Invalid");
case REASON_IE_IN_4WAY_DIFFERS: if (reason == REASON_AKMP_INVALID)
return LOG_STR("IE In 4-Way Handshake Differs"); return LOG_STR("AKMP Invalid");
case REASON_GROUP_CIPHER_INVALID: if (reason == REASON_UNSUPP_RSN_IE_VERSION)
return LOG_STR("Group Cipher Invalid"); return LOG_STR("Unsupported RSN IE version");
case REASON_PAIRWISE_CIPHER_INVALID: if (reason == REASON_INVALID_RSN_IE_CAP)
return LOG_STR("Pairwise Cipher Invalid"); return LOG_STR("Invalid RSN IE Cap");
case REASON_AKMP_INVALID: if (reason == REASON_802_1X_AUTH_FAILED)
return LOG_STR("AKMP Invalid"); return LOG_STR("802.1x Authentication Failed");
case REASON_UNSUPP_RSN_IE_VERSION: if (reason == REASON_CIPHER_SUITE_REJECTED)
return LOG_STR("Unsupported RSN IE version"); return LOG_STR("Cipher Suite Rejected");
case REASON_INVALID_RSN_IE_CAP: if (reason == REASON_BEACON_TIMEOUT)
return LOG_STR("Invalid RSN IE Cap"); return LOG_STR("Beacon Timeout");
case REASON_802_1X_AUTH_FAILED: if (reason == REASON_NO_AP_FOUND)
return LOG_STR("802.1x Authentication Failed"); return LOG_STR("AP Not Found");
case REASON_CIPHER_SUITE_REJECTED: if (reason == REASON_AUTH_FAIL)
return LOG_STR("Cipher Suite Rejected"); return LOG_STR("Authentication Failed");
} if (reason == REASON_ASSOC_FAIL)
} return LOG_STR("Association Failed");
if (reason == REASON_HANDSHAKE_TIMEOUT)
switch (reason) { return LOG_STR("Handshake Failed");
case REASON_BEACON_TIMEOUT: return LOG_STR("Unspecified");
return LOG_STR("Beacon Timeout");
case REASON_NO_AP_FOUND:
return LOG_STR("AP Not Found");
case REASON_AUTH_FAIL:
return LOG_STR("Authentication Failed");
case REASON_ASSOC_FAIL:
return LOG_STR("Association Failed");
case REASON_HANDSHAKE_TIMEOUT:
return LOG_STR("Handshake Failed");
case REASON_UNSPECIFIED:
default:
return LOG_STR("Unspecified");
}
} }
// TODO: This callback runs in ESP8266 system context with limited stack (~2KB). // TODO: This callback runs in ESP8266 system context with limited stack (~2KB).
@@ -645,21 +635,15 @@ void WiFiComponent::wifi_pre_setup_() {
WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() { WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() {
station_status_t status = wifi_station_get_connect_status(); station_status_t status = wifi_station_get_connect_status();
switch (status) { if (status == STATION_GOT_IP)
case STATION_GOT_IP: return WiFiSTAConnectStatus::CONNECTED;
return WiFiSTAConnectStatus::CONNECTED; if (status == STATION_NO_AP_FOUND)
case STATION_NO_AP_FOUND: return WiFiSTAConnectStatus::ERROR_NETWORK_NOT_FOUND;
return WiFiSTAConnectStatus::ERROR_NETWORK_NOT_FOUND; if (status == STATION_CONNECT_FAIL || status == STATION_WRONG_PASSWORD)
; return WiFiSTAConnectStatus::ERROR_CONNECT_FAILED;
case STATION_CONNECT_FAIL: if (status == STATION_CONNECTING)
case STATION_WRONG_PASSWORD: return WiFiSTAConnectStatus::CONNECTING;
return WiFiSTAConnectStatus::ERROR_CONNECT_FAILED; return WiFiSTAConnectStatus::IDLE;
case STATION_CONNECTING:
return WiFiSTAConnectStatus::CONNECTING;
case STATION_IDLE:
default:
return WiFiSTAConnectStatus::IDLE;
}
} }
bool WiFiComponent::wifi_scan_start_(bool passive) { bool WiFiComponent::wifi_scan_start_(bool passive) {
static bool first_scan = false; static bool first_scan = false;

View File

@@ -317,6 +317,7 @@ class EsphomeCommandWebSocket(CheckOriginMixin, tornado.websocket.WebSocketHandl
# Check if the proc was not forcibly closed # Check if the proc was not forcibly closed
_LOGGER.info("Process exited with return code %s", returncode) _LOGGER.info("Process exited with return code %s", returncode)
self.write_message({"event": "exit", "code": returncode}) self.write_message({"event": "exit", "code": returncode})
self.close()
def on_close(self) -> None: def on_close(self) -> None:
# Check if proc exists (if 'start' has been run) # Check if proc exists (if 'start' has been run)

View File

@@ -37,7 +37,7 @@ lib_deps_base =
wjtje/qr-code-generator-library@1.7.0 ; qr_code wjtje/qr-code-generator-library@1.7.0 ; qr_code
functionpointer/arduino-MLX90393@1.0.2 ; mlx90393 functionpointer/arduino-MLX90393@1.0.2 ; mlx90393
pavlodn/HaierProtocol@0.9.31 ; haier pavlodn/HaierProtocol@0.9.31 ; haier
esphome/dsmr_parser@1.0.0 ; dsmr esphome/dsmr_parser@1.1.0 ; dsmr
polargoose/Crypto-no-arduino@0.4.0 ; dsmr polargoose/Crypto-no-arduino@0.4.0 ; dsmr
https://github.com/esphome/TinyGPSPlus.git#v1.1.0 ; gps https://github.com/esphome/TinyGPSPlus.git#v1.1.0 ; gps
; This is using the repository until a new release is published to PlatformIO ; This is using the repository until a new release is published to PlatformIO

View File

@@ -23,6 +23,7 @@ resvg-py==0.2.6
freetype-py==2.5.1 freetype-py==2.5.1
jinja2==3.1.6 jinja2==3.1.6
bleak==2.1.1 bleak==2.1.1
requests==2.32.5
# esp-idf >= 5.0 requires this # esp-idf >= 5.0 requires this
pyparsing >= 3.0 pyparsing >= 3.0

View File

@@ -25,6 +25,22 @@ display:
lambda: |- lambda: |-
it.circle(64, 64, 50, Color::BLACK); it.circle(64, 64, 50, Color::BLACK);
- platform: epaper_spi
spi_id: spi_bus
model: waveshare-1.54in-G
cs_pin:
allow_other_uses: true
number: GPIO5
dc_pin:
allow_other_uses: true
number: GPIO17
reset_pin:
allow_other_uses: true
number: GPIO16
busy_pin:
allow_other_uses: true
number: GPIO4
- platform: epaper_spi - platform: epaper_spi
spi_id: spi_bus spi_id: spi_bus
model: waveshare-2.13in-v3 model: waveshare-2.13in-v3

View File

@@ -412,6 +412,7 @@ water_heater:
name: "Template Water Heater" name: "Template Water Heater"
optimistic: true optimistic: true
current_temperature: !lambda "return 42.0f;" current_temperature: !lambda "return 42.0f;"
target_temperature: !lambda "return 60.0f;"
mode: !lambda "return water_heater::WATER_HEATER_MODE_ECO;" mode: !lambda "return water_heater::WATER_HEATER_MODE_ECO;"
supported_modes: supported_modes:
- "OFF" - "OFF"

View File

@@ -29,7 +29,7 @@ from esphome.dashboard.entries import (
bool_to_entry_state, bool_to_entry_state,
) )
from esphome.dashboard.models import build_importable_device_dict from esphome.dashboard.models import build_importable_device_dict
from esphome.dashboard.web_server import DashboardSubscriber from esphome.dashboard.web_server import DashboardSubscriber, EsphomeCommandWebSocket
from esphome.zeroconf import DiscoveredImport from esphome.zeroconf import DiscoveredImport
from .common import get_fixture_path from .common import get_fixture_path
@@ -1654,3 +1654,25 @@ async def test_websocket_check_origin_multiple_trusted_domains(
assert data["event"] == "initial_state" assert data["event"] == "initial_state"
finally: finally:
ws.close() ws.close()
def test_proc_on_exit_calls_close() -> None:
"""Test _proc_on_exit sends exit event and closes the WebSocket."""
handler = Mock(spec=EsphomeCommandWebSocket)
handler._is_closed = False
EsphomeCommandWebSocket._proc_on_exit(handler, 0)
handler.write_message.assert_called_once_with({"event": "exit", "code": 0})
handler.close.assert_called_once()
def test_proc_on_exit_skips_when_already_closed() -> None:
"""Test _proc_on_exit does nothing when WebSocket is already closed."""
handler = Mock(spec=EsphomeCommandWebSocket)
handler._is_closed = True
EsphomeCommandWebSocket._proc_on_exit(handler, 0)
handler.write_message.assert_not_called()
handler.close.assert_not_called()

View File

@@ -10,6 +10,7 @@ water_heater:
name: Test Boiler name: Test Boiler
optimistic: true optimistic: true
current_temperature: !lambda "return 45.0f;" current_temperature: !lambda "return 45.0f;"
target_temperature: !lambda "return 60.0f;"
# Note: No mode lambda - we want optimistic mode changes to stick # Note: No mode lambda - we want optimistic mode changes to stick
# A mode lambda would override mode changes in loop() # A mode lambda would override mode changes in loop()
supported_modes: supported_modes:

View File

@@ -85,6 +85,9 @@ async def test_water_heater_template(
assert initial_state.current_temperature == 45.0, ( assert initial_state.current_temperature == 45.0, (
f"Expected current temp 45.0, got {initial_state.current_temperature}" f"Expected current temp 45.0, got {initial_state.current_temperature}"
) )
assert initial_state.target_temperature == 60.0, (
f"Expected target temp 60.0, got {initial_state.target_temperature}"
)
# Test changing to GAS mode # Test changing to GAS mode
client.water_heater_command(test_water_heater.key, mode=WaterHeaterMode.GAS) client.water_heater_command(test_water_heater.key, mode=WaterHeaterMode.GAS)