Compare commits

..

8 Commits

Author SHA1 Message Date
J. Nick Koston
325a812202 tidy 2026-01-14 16:20:29 -10:00
J. Nick Koston
d49c06df35 Increase buffer to 128 bytes and improve docstrings 2026-01-14 16:04:22 -10:00
J. Nick Koston
0b676c0daa review 2026-01-14 16:02:42 -10:00
J. Nick Koston
d4bbad9ea2 Merge remote-tracking branch 'upstream/dev' into cse7766_stack_debug 2026-01-14 15:58:14 -10:00
J. Nick Koston
4befd86a96 review 2026-01-14 15:56:32 -10:00
J. Nick Koston
e13743a9c3 tidy 2026-01-14 14:45:55 -10:00
J. Nick Koston
d3d96afbba tweak 2026-01-14 14:30:07 -10:00
J. Nick Koston
6e77182523 [cse7766] Use stack buffer for verbose debug logging 2026-01-14 14:04:28 -10:00
7 changed files with 56 additions and 82 deletions

View File

@@ -241,10 +241,8 @@ void APIServer::handle_disconnect(APIConnection *conn) {}
void APIServer::on_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \
if (obj->is_internal()) \
return; \
for (auto &c : this->clients_) { \
if (c->flags_.state_subscription) \
c->send_##entity_name##_state(obj); \
} \
for (auto &c : this->clients_) \
c->send_##entity_name##_state(obj); \
}
#ifdef USE_BINARY_SENSOR
@@ -323,10 +321,8 @@ API_DISPATCH_UPDATE(water_heater::WaterHeater, water_heater)
void APIServer::on_event(event::Event *obj) {
if (obj->is_internal())
return;
for (auto &c : this->clients_) {
if (c->flags_.state_subscription)
c->send_event(obj);
}
for (auto &c : this->clients_)
c->send_event(obj);
}
#endif
@@ -335,10 +331,8 @@ void APIServer::on_event(event::Event *obj) {
void APIServer::on_update(update::UpdateEntity *obj) {
if (obj->is_internal())
return;
for (auto &c : this->clients_) {
if (c->flags_.state_subscription)
c->send_update_state(obj);
}
for (auto &c : this->clients_)
c->send_update_state(obj);
}
#endif

View File

@@ -2,6 +2,7 @@
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <cstdarg>
namespace esphome {
namespace cse7766 {
@@ -9,6 +10,32 @@ namespace cse7766 {
static const char *const TAG = "cse7766";
static constexpr size_t CSE7766_RAW_DATA_SIZE = 24;
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
/// @brief Safely append formatted string to buffer.
/// @param buf Destination buffer (must be non-null)
/// @param size Total buffer size in bytes
/// @param pos Current write position (0 to size-1 for valid positions, size means full)
/// @param fmt printf-style format string
/// @return New write position: pos + chars_written, capped at size when buffer is full.
/// Returns size (not size-1) when full because vsnprintf already wrote the null
/// terminator at buf[size-1]. Returning size signals "no room for more content".
/// On encoding error, returns pos unchanged (no write occurred).
__attribute__((format(printf, 4, 5))) static size_t buf_append(char *buf, size_t size, size_t pos, const char *fmt,
...) {
if (pos >= size) {
return size;
}
va_list args;
va_start(args, fmt);
int written = vsnprintf(buf + pos, size - pos, fmt, args);
va_end(args);
if (written < 0) {
return pos; // encoding error
}
return std::min(pos + static_cast<size_t>(written), size);
}
#endif
void CSE7766Component::loop() {
const uint32_t now = App.get_loop_component_start_time();
if (now - this->last_transmission_ >= 500) {
@@ -207,20 +234,23 @@ void CSE7766Component::parse_data_() {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
{
std::string buf = "Parsed:";
// Buffer: 7 + 15 + 33 + 15 + 25 = 95 chars max + null, rounded to 128 for safety margin.
// Float sizes with %.4f can be up to 11 chars for large values (e.g., 999999.9999).
char buf[128];
size_t pos = buf_append(buf, sizeof(buf), 0, "Parsed:");
if (have_voltage) {
buf += str_sprintf(" V=%fV", voltage);
pos = buf_append(buf, sizeof(buf), pos, " V=%.4fV", voltage);
}
if (have_current) {
buf += str_sprintf(" I=%fmA (~%fmA)", current * 1000.0f, calculated_current * 1000.0f);
pos = buf_append(buf, sizeof(buf), pos, " I=%.4fmA (~%.4fmA)", current * 1000.0f, calculated_current * 1000.0f);
}
if (have_power) {
buf += str_sprintf(" P=%fW", power);
pos = buf_append(buf, sizeof(buf), pos, " P=%.4fW", power);
}
if (energy != 0.0f) {
buf += str_sprintf(" E=%fkWh (%u)", energy, cf_pulses);
buf_append(buf, sizeof(buf), pos, " E=%.4fkWh (%u)", energy, cf_pulses);
}
ESP_LOGVV(TAG, "%s", buf.c_str());
ESP_LOGVV(TAG, "%s", buf);
}
#endif
}

View File

@@ -665,10 +665,15 @@ async def write_image(config, all_frames=False):
if is_svg_file(path):
import resvg_py
resize = resize or (None, None)
image_data = resvg_py.svg_to_bytes(
svg_path=str(path), width=resize[0], height=resize[1], dpi=100
)
if resize:
width, height = resize
# resvg-py allows rendering by width/height directly
image_data = resvg_py.svg_to_bytes(
svg_path=str(path), width=int(width), height=int(height)
)
else:
# Default size
image_data = resvg_py.svg_to_bytes(svg_path=str(path))
# Convert bytes to Pillow Image
image = Image.open(io.BytesIO(image_data))

View File

@@ -753,6 +753,9 @@ void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlM
}
request->send(404);
}
std::string WebServer::button_state_json_generator(WebServer *web_server, void *source) {
return web_server->button_json_((button::Button *) (source), DETAIL_STATE);
}
std::string WebServer::button_all_json_generator(WebServer *web_server, void *source) {
return web_server->button_json_((button::Button *) (source), DETAIL_ALL);
}

View File

@@ -295,7 +295,7 @@ class WebServer : public Controller,
/// Handle a button request under '/button/<id>/press'.
void handle_button_request(AsyncWebServerRequest *request, const UrlMatch &match);
// Buttons are stateless, so there is no button_state_json_generator
static std::string button_state_json_generator(WebServer *web_server, void *source);
static std::string button_all_json_generator(WebServer *web_server, void *source);
#endif

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="10mm" height="10mm" viewBox="0 0 100 100">
<rect x="0" y="0" width="100" height="100" fill="#00FF00"/>
<circle cx="50" cy="50" r="30" fill="#0000FF"/>
</svg>

Before

Width:  |  Height:  |  Size: 248 B

View File

@@ -5,21 +5,17 @@ from __future__ import annotations
from collections.abc import Callable
from pathlib import Path
from typing import Any
from unittest.mock import MagicMock, patch
import pytest
from esphome import config_validation as cv
from esphome.components.image import (
CONF_INVERT_ALPHA,
CONF_OPAQUE,
CONF_TRANSPARENCY,
CONFIG_SCHEMA,
get_all_image_metadata,
get_image_metadata,
write_image,
)
from esphome.const import CONF_DITHER, CONF_FILE, CONF_ID, CONF_RAW_DATA_ID, CONF_TYPE
from esphome.const import CONF_ID, CONF_RAW_DATA_ID, CONF_TYPE
from esphome.core import CORE
@@ -354,52 +350,3 @@ def test_get_all_image_metadata_empty() -> None:
"get_all_image_metadata should always return a dict"
)
# Length could be 0 or more depending on what's in CORE at test time
@pytest.fixture
def mock_progmem_array():
"""Mock progmem_array to avoid needing a proper ID object in tests."""
with patch("esphome.components.image.cg.progmem_array") as mock_progmem:
mock_progmem.return_value = MagicMock()
yield mock_progmem
@pytest.mark.asyncio
async def test_svg_with_mm_dimensions_succeeds(
component_config_path: Callable[[str], Path],
mock_progmem_array: MagicMock,
) -> None:
"""Test that SVG files with dimensions in mm are successfully processed."""
# Create a config for write_image without CONF_RESIZE
config = {
CONF_FILE: component_config_path("mm_dimensions.svg"),
CONF_TYPE: "BINARY",
CONF_TRANSPARENCY: CONF_OPAQUE,
CONF_DITHER: "NONE",
CONF_INVERT_ALPHA: False,
CONF_RAW_DATA_ID: "test_raw_data_id",
}
# This should succeed without raising an error
result = await write_image(config)
# Verify that write_image returns the expected tuple
assert isinstance(result, tuple), "write_image should return a tuple"
assert len(result) == 6, "write_image should return 6 values"
prog_arr, width, height, image_type, trans_value, frame_count = result
# Verify the dimensions are positive integers
# At 100 DPI, 10mm = ~39 pixels (10mm * 100dpi / 25.4mm_per_inch)
assert isinstance(width, int), "Width should be an integer"
assert isinstance(height, int), "Height should be an integer"
assert width > 0, "Width should be positive"
assert height > 0, "Height should be positive"
assert frame_count == 1, "Single image should have frame_count of 1"
# Verify we got reasonable dimensions from the mm-based SVG
assert 30 < width < 50, (
f"Width should be around 39 pixels for 10mm at 100dpi, got {width}"
)
assert 30 < height < 50, (
f"Height should be around 39 pixels for 10mm at 100dpi, got {height}"
)