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:
@@ -1 +1 @@
|
|||||||
ad8131b65de630ca9f89d34d35f32e5c858fc2a7b310227e5edaf946e942af0f
|
65089c03a5fa1ee23c52d3a0898f533044788b79a9be625133bcd4fa9837e6f9
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|
||||||
|
|||||||
@@ -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."
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
),
|
),
|
||||||
|
|||||||
67
esphome/components/epaper_spi/colorconv.h
Normal file
67
esphome/components/epaper_spi/colorconv.h
Normal 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
|
||||||
227
esphome/components/epaper_spi/epaper_spi_jd79660.cpp
Normal file
227
esphome/components/epaper_spi/epaper_spi_jd79660.cpp
Normal 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
|
||||||
145
esphome/components/epaper_spi/epaper_spi_jd79660.h
Normal file
145
esphome/components/epaper_spi/epaper_spi_jd79660.h
Normal 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
|
||||||
86
esphome/components/epaper_spi/models/jd79660.py
Normal file
86
esphome/components/epaper_spi/models/jd79660.py
Normal 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,),
|
||||||
|
),
|
||||||
|
)
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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()) {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
@@ -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_) {
|
||||||
|
|||||||
@@ -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_;
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user