mirror of
https://github.com/esphome/esphome.git
synced 2026-02-09 01:01:56 +00:00
Compare commits
5 Commits
dfplayer_b
...
cswitch_sd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c4a732eb7 | ||
|
|
4e3ccb4fc5 | ||
|
|
a43e3e5948 | ||
|
|
9de91539e6 | ||
|
|
eb7aa3420f |
@@ -397,47 +397,38 @@ class MemoryAnalyzer:
|
||||
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.
|
||||
@staticmethod
|
||||
def _parse_nm_cswtch_output(
|
||||
output: str,
|
||||
base_dir: Path | None,
|
||||
cswtch_map: dict[str, list[tuple[str, int]]],
|
||||
) -> None:
|
||||
"""Parse nm output for CSWTCH symbols and add to cswtch_map.
|
||||
|
||||
Uses ``nm --print-file-name -S`` on all ``.o`` files at once.
|
||||
Output format: ``/path/to/file.o:address size type name``
|
||||
Handles both ``.o`` files and ``.a`` archives.
|
||||
|
||||
nm output formats::
|
||||
|
||||
.o files: /path/file.o:hex_addr hex_size type name
|
||||
.a files: /path/lib.a:member.o:hex_addr hex_size type name
|
||||
|
||||
For ``.o`` files, paths are made relative to *base_dir* when possible.
|
||||
For ``.a`` archives (detected by ``:`` in the file portion), paths are
|
||||
formatted as ``archive_stem/member.o`` (e.g. ``liblwip2-536-feat/lwip-esp.o``).
|
||||
|
||||
Args:
|
||||
obj_dir: Directory containing object files (.pioenvs/<env>/)
|
||||
|
||||
Returns:
|
||||
Dict mapping "CSWTCH$NNN:size" to list of (source_file, size) tuples.
|
||||
output: Raw stdout from ``nm --print-file-name -S``.
|
||||
base_dir: Base directory for computing relative paths of ``.o`` files.
|
||||
Pass ``None`` when scanning archives outside the build tree.
|
||||
cswtch_map: Dict to populate, mapping ``"CSWTCH$N:size"`` to source list.
|
||||
"""
|
||||
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():
|
||||
for line in output.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.
|
||||
# For .o: "filepath.o" : "hex_addr hex_size type name"
|
||||
# For .a: "filepath.a:member.o" : "hex_addr hex_size type name"
|
||||
parts_after_colon = line.rsplit(":", 1)
|
||||
if len(parts_after_colon) != 2:
|
||||
continue
|
||||
@@ -457,16 +448,89 @@ class MemoryAnalyzer:
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
# Get relative path from obj_dir for readability
|
||||
try:
|
||||
rel_path = str(Path(file_path).relative_to(obj_dir))
|
||||
except ValueError:
|
||||
# Determine readable source path
|
||||
# Use ".a:" to detect archive format (not bare ":" which matches
|
||||
# Windows drive letters like "C:\...\file.o").
|
||||
if ".a:" in file_path:
|
||||
# Archive format: "archive.a:member.o" → "archive_stem/member.o"
|
||||
archive_part, member = file_path.rsplit(":", 1)
|
||||
archive_name = Path(archive_part).stem
|
||||
rel_path = f"{archive_name}/{member}"
|
||||
elif base_dir is not None:
|
||||
try:
|
||||
rel_path = str(Path(file_path).relative_to(base_dir))
|
||||
except ValueError:
|
||||
rel_path = file_path
|
||||
else:
|
||||
rel_path = file_path
|
||||
|
||||
key = f"{sym_name}:{size}"
|
||||
cswtch_map[key].append((rel_path, size))
|
||||
|
||||
return cswtch_map
|
||||
def _run_nm_cswtch_scan(
|
||||
self,
|
||||
files: list[Path],
|
||||
base_dir: Path | None,
|
||||
cswtch_map: dict[str, list[tuple[str, int]]],
|
||||
) -> None:
|
||||
"""Run nm on *files* and add any CSWTCH symbols to *cswtch_map*.
|
||||
|
||||
Args:
|
||||
files: Object (``.o``) or archive (``.a``) files to scan.
|
||||
base_dir: Base directory for relative path computation (see
|
||||
:meth:`_parse_nm_cswtch_output`).
|
||||
cswtch_map: Dict to populate with results.
|
||||
"""
|
||||
if not self.nm_path or not files:
|
||||
return
|
||||
|
||||
_LOGGER.debug("Scanning %d files for CSWTCH symbols", len(files))
|
||||
|
||||
result = run_tool(
|
||||
[self.nm_path, "--print-file-name", "-S"] + [str(f) for f in files],
|
||||
timeout=30,
|
||||
)
|
||||
if result is None or result.returncode != 0:
|
||||
_LOGGER.debug(
|
||||
"nm failed or timed out scanning %d files for CSWTCH symbols",
|
||||
len(files),
|
||||
)
|
||||
return
|
||||
|
||||
self._parse_nm_cswtch_output(result.stdout, base_dir, cswtch_map)
|
||||
|
||||
def _scan_cswtch_in_sdk_archives(
|
||||
self, cswtch_map: dict[str, list[tuple[str, int]]]
|
||||
) -> None:
|
||||
"""Scan SDK library archives (.a) for CSWTCH symbols.
|
||||
|
||||
Prebuilt SDK libraries (e.g. lwip, bearssl) are not compiled from source,
|
||||
so their CSWTCH symbols only exist inside ``.a`` archives. Results are
|
||||
merged into *cswtch_map* for keys not already found in ``.o`` files.
|
||||
|
||||
The same source file (e.g. ``lwip-esp.o``) often appears in multiple
|
||||
library variants (``liblwip2-536.a``, ``liblwip2-1460-feat.a``, etc.),
|
||||
so results are deduplicated by member name.
|
||||
"""
|
||||
sdk_dirs = self._find_sdk_library_dirs()
|
||||
if not sdk_dirs:
|
||||
return
|
||||
|
||||
sdk_archives = sorted(a for sdk_dir in sdk_dirs for a in sdk_dir.glob("*.a"))
|
||||
|
||||
sdk_map: dict[str, list[tuple[str, int]]] = defaultdict(list)
|
||||
self._run_nm_cswtch_scan(sdk_archives, None, sdk_map)
|
||||
|
||||
# Merge SDK results, deduplicating by member name.
|
||||
for key, sources in sdk_map.items():
|
||||
if key in cswtch_map:
|
||||
continue
|
||||
seen: dict[str, tuple[str, int]] = {}
|
||||
for path, sz in sources:
|
||||
member = Path(path).name
|
||||
if member not in seen:
|
||||
seen[member] = (path, sz)
|
||||
cswtch_map[key] = list(seen.values())
|
||||
|
||||
def _source_file_to_component(self, source_file: str) -> str:
|
||||
"""Map a source object file path to its component name.
|
||||
@@ -505,17 +569,25 @@ class MemoryAnalyzer:
|
||||
|
||||
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.
|
||||
This method scans .o files and SDK archives 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)
|
||||
# Scan build-dir object files for CSWTCH symbols
|
||||
cswtch_map: dict[str, list[tuple[str, int]]] = defaultdict(list)
|
||||
self._run_nm_cswtch_scan(sorted(obj_dir.rglob("*.o")), obj_dir, cswtch_map)
|
||||
|
||||
# Also scan SDK library archives (.a) for CSWTCH symbols.
|
||||
# Prebuilt SDK libraries (e.g. lwip, bearssl) are not compiled from source
|
||||
# so their symbols only exist inside .a archives, not as loose .o files.
|
||||
self._scan_cswtch_in_sdk_archives(cswtch_map)
|
||||
|
||||
if not cswtch_map:
|
||||
_LOGGER.debug("No CSWTCH symbols found in object files")
|
||||
_LOGGER.debug("No CSWTCH symbols found in object files or SDK archives")
|
||||
return
|
||||
|
||||
# Collect CSWTCH symbols from the ELF (already parsed in sections)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "dfplayer.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -133,151 +132,139 @@ void DFPlayer::send_cmd_(uint8_t cmd, uint16_t argument) {
|
||||
|
||||
void DFPlayer::loop() {
|
||||
// Read message
|
||||
int avail = this->available();
|
||||
if (avail <= 0)
|
||||
return;
|
||||
while (this->available()) {
|
||||
uint8_t byte;
|
||||
this->read_byte(&byte);
|
||||
|
||||
uint8_t buf[64];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
avail -= to_read;
|
||||
for (size_t bi = 0; bi < to_read; bi++) {
|
||||
uint8_t byte = buf[bi];
|
||||
if (this->read_pos_ == DFPLAYER_READ_BUFFER_LENGTH)
|
||||
this->read_pos_ = 0;
|
||||
|
||||
if (this->read_pos_ == DFPLAYER_READ_BUFFER_LENGTH)
|
||||
this->read_pos_ = 0;
|
||||
|
||||
switch (this->read_pos_) {
|
||||
case 0: // Start mark
|
||||
if (byte != 0x7E)
|
||||
continue;
|
||||
break;
|
||||
case 1: // Version
|
||||
if (byte != 0xFF) {
|
||||
ESP_LOGW(TAG, "Expected Version 0xFF, got %#02x", byte);
|
||||
this->read_pos_ = 0;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
case 2: // Buffer length
|
||||
if (byte != 0x06) {
|
||||
ESP_LOGW(TAG, "Expected Buffer length 0x06, got %#02x", byte);
|
||||
this->read_pos_ = 0;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
case 9: // End byte
|
||||
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
|
||||
char byte_sequence[100];
|
||||
byte_sequence[0] = '\0';
|
||||
for (size_t i = 0; i < this->read_pos_ + 1; ++i) {
|
||||
snprintf(byte_sequence + strlen(byte_sequence), sizeof(byte_sequence) - strlen(byte_sequence), "%02X ",
|
||||
this->read_buffer_[i]);
|
||||
}
|
||||
ESP_LOGVV(TAG, "Received byte sequence: %s", byte_sequence);
|
||||
#endif
|
||||
if (byte != 0xEF) {
|
||||
ESP_LOGW(TAG, "Expected end byte 0xEF, got %#02x", byte);
|
||||
this->read_pos_ = 0;
|
||||
continue;
|
||||
}
|
||||
// Parse valid received command
|
||||
uint8_t cmd = this->read_buffer_[3];
|
||||
uint16_t argument = (this->read_buffer_[5] << 8) | this->read_buffer_[6];
|
||||
|
||||
ESP_LOGV(TAG, "Received message cmd: %#02x arg %#04x", cmd, argument);
|
||||
|
||||
switch (cmd) {
|
||||
case 0x3A:
|
||||
if (argument == 1) {
|
||||
ESP_LOGI(TAG, "USB loaded");
|
||||
} else if (argument == 2) {
|
||||
ESP_LOGI(TAG, "TF Card loaded");
|
||||
}
|
||||
break;
|
||||
case 0x3B:
|
||||
if (argument == 1) {
|
||||
ESP_LOGI(TAG, "USB unloaded");
|
||||
} else if (argument == 2) {
|
||||
ESP_LOGI(TAG, "TF Card unloaded");
|
||||
}
|
||||
break;
|
||||
case 0x3F:
|
||||
if (argument == 1) {
|
||||
ESP_LOGI(TAG, "USB available");
|
||||
} else if (argument == 2) {
|
||||
ESP_LOGI(TAG, "TF Card available");
|
||||
} else if (argument == 3) {
|
||||
ESP_LOGI(TAG, "USB, TF Card available");
|
||||
}
|
||||
break;
|
||||
case 0x40:
|
||||
ESP_LOGV(TAG, "Nack");
|
||||
this->ack_set_is_playing_ = false;
|
||||
this->ack_reset_is_playing_ = false;
|
||||
switch (argument) {
|
||||
case 0x01:
|
||||
ESP_LOGE(TAG, "Module is busy or uninitialized");
|
||||
break;
|
||||
case 0x02:
|
||||
ESP_LOGE(TAG, "Module is in sleep mode");
|
||||
break;
|
||||
case 0x03:
|
||||
ESP_LOGE(TAG, "Serial receive error");
|
||||
break;
|
||||
case 0x04:
|
||||
ESP_LOGE(TAG, "Checksum incorrect");
|
||||
break;
|
||||
case 0x05:
|
||||
ESP_LOGE(TAG, "Specified track is out of current track scope");
|
||||
this->is_playing_ = false;
|
||||
break;
|
||||
case 0x06:
|
||||
ESP_LOGE(TAG, "Specified track is not found");
|
||||
this->is_playing_ = false;
|
||||
break;
|
||||
case 0x07:
|
||||
ESP_LOGE(TAG,
|
||||
"Insertion error (an inserting operation only can be done when a track is being played)");
|
||||
break;
|
||||
case 0x08:
|
||||
ESP_LOGE(TAG, "SD card reading failed (SD card pulled out or damaged)");
|
||||
break;
|
||||
case 0x09:
|
||||
ESP_LOGE(TAG, "Entered into sleep mode");
|
||||
this->is_playing_ = false;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 0x41:
|
||||
ESP_LOGV(TAG, "Ack ok");
|
||||
this->is_playing_ |= this->ack_set_is_playing_;
|
||||
this->is_playing_ &= !this->ack_reset_is_playing_;
|
||||
this->ack_set_is_playing_ = false;
|
||||
this->ack_reset_is_playing_ = false;
|
||||
break;
|
||||
case 0x3C:
|
||||
ESP_LOGV(TAG, "Playback finished (USB drive)");
|
||||
this->is_playing_ = false;
|
||||
this->on_finished_playback_callback_.call();
|
||||
case 0x3D:
|
||||
ESP_LOGV(TAG, "Playback finished (SD card)");
|
||||
this->is_playing_ = false;
|
||||
this->on_finished_playback_callback_.call();
|
||||
break;
|
||||
default:
|
||||
ESP_LOGE(TAG, "Received unknown cmd %#02x arg %#04x", cmd, argument);
|
||||
}
|
||||
this->sent_cmd_ = 0;
|
||||
switch (this->read_pos_) {
|
||||
case 0: // Start mark
|
||||
if (byte != 0x7E)
|
||||
continue;
|
||||
break;
|
||||
case 1: // Version
|
||||
if (byte != 0xFF) {
|
||||
ESP_LOGW(TAG, "Expected Version 0xFF, got %#02x", byte);
|
||||
this->read_pos_ = 0;
|
||||
continue;
|
||||
}
|
||||
this->read_buffer_[this->read_pos_] = byte;
|
||||
this->read_pos_++;
|
||||
}
|
||||
break;
|
||||
case 2: // Buffer length
|
||||
if (byte != 0x06) {
|
||||
ESP_LOGW(TAG, "Expected Buffer length 0x06, got %#02x", byte);
|
||||
this->read_pos_ = 0;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
case 9: // End byte
|
||||
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
|
||||
char byte_sequence[100];
|
||||
byte_sequence[0] = '\0';
|
||||
for (size_t i = 0; i < this->read_pos_ + 1; ++i) {
|
||||
snprintf(byte_sequence + strlen(byte_sequence), sizeof(byte_sequence) - strlen(byte_sequence), "%02X ",
|
||||
this->read_buffer_[i]);
|
||||
}
|
||||
ESP_LOGVV(TAG, "Received byte sequence: %s", byte_sequence);
|
||||
#endif
|
||||
if (byte != 0xEF) {
|
||||
ESP_LOGW(TAG, "Expected end byte 0xEF, got %#02x", byte);
|
||||
this->read_pos_ = 0;
|
||||
continue;
|
||||
}
|
||||
// Parse valid received command
|
||||
uint8_t cmd = this->read_buffer_[3];
|
||||
uint16_t argument = (this->read_buffer_[5] << 8) | this->read_buffer_[6];
|
||||
|
||||
ESP_LOGV(TAG, "Received message cmd: %#02x arg %#04x", cmd, argument);
|
||||
|
||||
switch (cmd) {
|
||||
case 0x3A:
|
||||
if (argument == 1) {
|
||||
ESP_LOGI(TAG, "USB loaded");
|
||||
} else if (argument == 2) {
|
||||
ESP_LOGI(TAG, "TF Card loaded");
|
||||
}
|
||||
break;
|
||||
case 0x3B:
|
||||
if (argument == 1) {
|
||||
ESP_LOGI(TAG, "USB unloaded");
|
||||
} else if (argument == 2) {
|
||||
ESP_LOGI(TAG, "TF Card unloaded");
|
||||
}
|
||||
break;
|
||||
case 0x3F:
|
||||
if (argument == 1) {
|
||||
ESP_LOGI(TAG, "USB available");
|
||||
} else if (argument == 2) {
|
||||
ESP_LOGI(TAG, "TF Card available");
|
||||
} else if (argument == 3) {
|
||||
ESP_LOGI(TAG, "USB, TF Card available");
|
||||
}
|
||||
break;
|
||||
case 0x40:
|
||||
ESP_LOGV(TAG, "Nack");
|
||||
this->ack_set_is_playing_ = false;
|
||||
this->ack_reset_is_playing_ = false;
|
||||
switch (argument) {
|
||||
case 0x01:
|
||||
ESP_LOGE(TAG, "Module is busy or uninitialized");
|
||||
break;
|
||||
case 0x02:
|
||||
ESP_LOGE(TAG, "Module is in sleep mode");
|
||||
break;
|
||||
case 0x03:
|
||||
ESP_LOGE(TAG, "Serial receive error");
|
||||
break;
|
||||
case 0x04:
|
||||
ESP_LOGE(TAG, "Checksum incorrect");
|
||||
break;
|
||||
case 0x05:
|
||||
ESP_LOGE(TAG, "Specified track is out of current track scope");
|
||||
this->is_playing_ = false;
|
||||
break;
|
||||
case 0x06:
|
||||
ESP_LOGE(TAG, "Specified track is not found");
|
||||
this->is_playing_ = false;
|
||||
break;
|
||||
case 0x07:
|
||||
ESP_LOGE(TAG, "Insertion error (an inserting operation only can be done when a track is being played)");
|
||||
break;
|
||||
case 0x08:
|
||||
ESP_LOGE(TAG, "SD card reading failed (SD card pulled out or damaged)");
|
||||
break;
|
||||
case 0x09:
|
||||
ESP_LOGE(TAG, "Entered into sleep mode");
|
||||
this->is_playing_ = false;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 0x41:
|
||||
ESP_LOGV(TAG, "Ack ok");
|
||||
this->is_playing_ |= this->ack_set_is_playing_;
|
||||
this->is_playing_ &= !this->ack_reset_is_playing_;
|
||||
this->ack_set_is_playing_ = false;
|
||||
this->ack_reset_is_playing_ = false;
|
||||
break;
|
||||
case 0x3C:
|
||||
ESP_LOGV(TAG, "Playback finished (USB drive)");
|
||||
this->is_playing_ = false;
|
||||
this->on_finished_playback_callback_.call();
|
||||
case 0x3D:
|
||||
ESP_LOGV(TAG, "Playback finished (SD card)");
|
||||
this->is_playing_ = false;
|
||||
this->on_finished_playback_callback_.call();
|
||||
break;
|
||||
default:
|
||||
ESP_LOGE(TAG, "Received unknown cmd %#02x arg %#04x", cmd, argument);
|
||||
}
|
||||
this->sent_cmd_ = 0;
|
||||
this->read_pos_ = 0;
|
||||
continue;
|
||||
}
|
||||
this->read_buffer_[this->read_pos_] = byte;
|
||||
this->read_pos_++;
|
||||
}
|
||||
}
|
||||
void DFPlayer::dump_config() {
|
||||
|
||||
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,),
|
||||
),
|
||||
)
|
||||
@@ -46,6 +46,7 @@ CONFIG_SCHEMA = (
|
||||
RESTORE_MODES, upper=True
|
||||
),
|
||||
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_SUPPORTED_MODES): cv.ensure_list(
|
||||
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_))
|
||||
|
||||
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:
|
||||
template_ = await cg.process_lambda(
|
||||
config[CONF_MODE],
|
||||
|
||||
@@ -16,7 +16,8 @@ void TemplateWaterHeater::setup() {
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -28,6 +29,9 @@ water_heater::WaterHeaterTraits TemplateWaterHeater::traits() {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
if (new_mode.has_value()) {
|
||||
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) {
|
||||
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)); }
|
||||
|
||||
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
|
||||
Trigger<> set_trigger_;
|
||||
TemplateLambda<float> current_temperature_f_;
|
||||
TemplateLambda<float> target_temperature_f_;
|
||||
TemplateLambda<water_heater::WaterHeaterMode> mode_f_;
|
||||
TemplateWaterHeaterRestoreMode restore_mode_{WATER_HEATER_NO_RESTORE};
|
||||
water_heater::WaterHeaterModeMask supported_modes_;
|
||||
|
||||
@@ -317,6 +317,7 @@ class EsphomeCommandWebSocket(CheckOriginMixin, tornado.websocket.WebSocketHandl
|
||||
# Check if the proc was not forcibly closed
|
||||
_LOGGER.info("Process exited with return code %s", returncode)
|
||||
self.write_message({"event": "exit", "code": returncode})
|
||||
self.close()
|
||||
|
||||
def on_close(self) -> None:
|
||||
# Check if proc exists (if 'start' has been run)
|
||||
|
||||
@@ -25,6 +25,22 @@ display:
|
||||
lambda: |-
|
||||
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
|
||||
spi_id: spi_bus
|
||||
model: waveshare-2.13in-v3
|
||||
|
||||
@@ -412,6 +412,7 @@ water_heater:
|
||||
name: "Template Water Heater"
|
||||
optimistic: true
|
||||
current_temperature: !lambda "return 42.0f;"
|
||||
target_temperature: !lambda "return 60.0f;"
|
||||
mode: !lambda "return water_heater::WATER_HEATER_MODE_ECO;"
|
||||
supported_modes:
|
||||
- "OFF"
|
||||
|
||||
@@ -29,7 +29,7 @@ from esphome.dashboard.entries import (
|
||||
bool_to_entry_state,
|
||||
)
|
||||
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 .common import get_fixture_path
|
||||
@@ -1654,3 +1654,25 @@ async def test_websocket_check_origin_multiple_trusted_domains(
|
||||
assert data["event"] == "initial_state"
|
||||
finally:
|
||||
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
|
||||
optimistic: true
|
||||
current_temperature: !lambda "return 45.0f;"
|
||||
target_temperature: !lambda "return 60.0f;"
|
||||
# Note: No mode lambda - we want optimistic mode changes to stick
|
||||
# A mode lambda would override mode changes in loop()
|
||||
supported_modes:
|
||||
|
||||
@@ -85,6 +85,9 @@ async def test_water_heater_template(
|
||||
assert initial_state.current_temperature == 45.0, (
|
||||
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
|
||||
client.water_heater_command(test_water_heater.key, mode=WaterHeaterMode.GAS)
|
||||
|
||||
Reference in New Issue
Block a user