Compare commits

..
Author SHA1 Message Date
clyde 52c4c0310d Merge branch 'runtime-image-integer-draw' into runtime-image-gray-integer 2026-10-22 22:03:59 +11:00
clyde 547dbf18d4 Merge branch 'runtime-image-jpeg-scale' into runtime-image-integer-draw 2026-10-22 22:03:58 +11:00
clyde ba3af07a23 Merge branch 'online-image-progress' into runtime-image-jpeg-scale 2026-10-22 22:03:58 +11:00
clyde 9c6ae9c615 Merge branch 'online-image-jpeg-stream' into online-image-progress 2026-10-22 22:03:58 +11:00
clyde de02ef8555 Merge branch 'runtime-image-jpeg-steps' into online-image-jpeg-stream 2026-10-22 22:03:57 +11:00
clydeandClaude Opus 5.5 38b23152f4 [runtime_image] Put the is_finished() doc back above is_finished()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 22:03:51 +11:00
clydeandClaude Opus 5.5 0849e4b412 [runtime_image] Work out grayscale pixels with integer maths
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 21:56:41 +11:00
clyde 375e8bc39b Merge branch 'runtime-image-jpeg-scale' into runtime-image-integer-draw 2026-10-22 21:44:49 +11:00
clyde 16a91ab760 Merge branch 'online-image-progress' into runtime-image-jpeg-scale 2026-10-22 21:44:44 +11:00
clydeandClaude Opus 5.5 2456e8d686 [online_image] Use emplace_back in the download started test
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 21:44:39 +11:00
clyde 0c8e1c3f1c Merge branch 'runtime-image-jpeg-scale' into runtime-image-integer-draw 2026-10-22 21:35:06 +11:00
clyde a3c575d643 Merge branch 'online-image-progress' into runtime-image-jpeg-scale 2026-10-22 21:31:12 +11:00
clydeandClaude Opus 5.5 0573c8b568 [online_image] Add on_download_started
Fires when the server has accepted the request and the image starts to
download, before the first on_download_progress; not for a 304 or an
HTTP error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 21:24:05 +11:00
clyde d01205fa62 Merge branch 'runtime-image-jpeg-steps' into runtime-image-jpeg-scale 2026-10-22 21:17:21 +11:00
clyde de7ea89d0b Merge branch 'online-image-jpeg-stream' into online-image-progress 2026-10-22 21:17:19 +11:00
clyde 0077f68158 Merge branch 'runtime-image-jpeg-steps' into online-image-jpeg-stream 2026-10-22 21:17:18 +11:00
clydeandClaude Opus 5.5 7458f8c7d8 [runtime_image] Pin JPEGDEC to 1.8.4-esphome.3, built with -O2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 21:17:13 +11:00
clyde 0e5e0a8ff6 Merge remote-tracking branch 'upstream/dev' into runtime-image-jpeg-steps 2026-10-22 21:15:57 +11:00
clydeandClaude Opus 5.5 752c230405 [runtime_image] Check once per draw() call whether the image is scaled
An image drawn at its own size needs no span maths at all.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 21:14:33 +11:00
clydeandClaude Opus 5.5 cfc92657a2 [runtime_image] Say the generic draw() path keeps the same pixels as the RGB565 path
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 20:51:14 +11:00
clydeandClaude Opus 5.5 32f32f144d [runtime_image] Skip the span division for an image drawn at its own size
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 20:44:48 +11:00
clydeandClaude Opus 5.5 9b60e7cff2 [runtime_image] Decode a JPEG at the smallest JPEGDEC scale that covers the buffer
JPEGDEC can decode at 1/2, 1/4 or 1/8 size for much less work, but
every JPEG was decoded at full size and then shrunk to the buffer. A
3596 x 2840 image shown at 281 x 222 took 7.1 s on an ESP32-S3; at
1/8 size it takes 2.0 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 20:19:44 +11:00
clydeandClaude Opus 5.5 02b8db1f3f [runtime_image] Draw each target pixel once, with integer maths
ImageDecoder::draw() painted every source pixel into the buffer, so a
shrunk image wrote each target pixel many times, and worked out the
spans with double multiplications and ceil() for every pixel. The S3
has no double FPU, so drawing took about 6 us a pixel: 3.7 s of a 1000
x 617 PNG's 6 s. Give each source pixel the span from ceil(start *
scale) to ceil(end * scale), worked out with integers, so neighbouring
spans meet without overlapping. The PNG now loads in 3.2 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 20:17:27 +11:00
clyde f860c8a8d9 Merge branch 'online-image-jpeg-stream' into online-image-progress 2026-10-22 20:13:39 +11:00
clyde bb7a060138 Merge branch 'runtime-image-jpeg-steps' into online-image-jpeg-stream 2026-10-22 20:13:38 +11:00
clydeandClaude Opus 5.5 84839507bd [runtime_image] Pin JPEGDEC to 1.8.4-esphome.2, with the ESP32-S3 SIMD under ESP-IDF
Full-size JPEG decodes are 12-19% faster on an ESP32-S3 with ESP-IDF.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 20:13:33 +11:00
18348b8794 [vnc] Add VNC client component (#20623)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-10-21 21:14:37 -10:00
f7dc2ecb51 [wifi] [ethernet] Bridge the WISP access point to an ethernet upstream (#7147)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-22 07:02:25 +00:00
ddcc5874ed [api] Let a media_player ask for a constant bitrate (#19962)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-22 06:17:59 +00:00
clyde ef54eb14d8 Merge branch 'online-image-jpeg-stream' into online-image-progress 2026-10-22 16:29:02 +11:00
clydeandClaude Opus 5.5 60aa3e24f1 [online_image] Brace the whole-file fallback for clang-tidy
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 16:29:01 +11:00
clyde 63566999bd Merge branch 'runtime-image-jpeg-steps' into online-image-jpeg-stream 2026-10-22 16:28:45 +11:00
clydeandClaude Opus 5.5 1728d3326b [runtime_image] Log a failed open from a source at debug level, as the caller may recover
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 16:28:44 +11:00
clyde 2b99c56754 Merge branch 'online-image-jpeg-stream' into online-image-progress 2026-10-22 16:21:09 +11:00
clydeandClaude Opus 5.5 d79cd9a7eb [online_image] Download the whole JPEG when its headers do not fit the buffer
Before JPEG images were decoded as they download, the buffer grew to
hold the whole file, so an image with large EXIF or ICC data decoded
with any buffer_size. Keep that working: when the headers do not fit,
grow the buffer to the file size, keeping what has arrived, and decode
it at the end as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 16:21:08 +11:00
esphome[bot] 28013a60e1 Bump aioesphomeapi from 46.19.0 to 46.20.0 (#21362) 2026-10-21 19:19:35 -10:00
clyde fa18994fab Merge branch 'runtime-image-jpeg-steps' into online-image-jpeg-stream 2026-10-22 16:19:20 +11:00
clydeandClaude Opus 5.5 1090706216 [runtime_image] Count the bytes of a stepped decode, and say when begin_source() can be called
Report any failed read of the source as DECODE_ERROR_READ from one place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 16:19:01 +11:00
J. Nick Koston 966193cb09 [core] Emit generated lambda code after the user includes (#21307) 2026-10-21 18:28:04 -10:00
e1077748be [api][cover] Expose toggle support in native API (#16506)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-21 18:11:09 -10:00
cc89a011e2 Bump clang-tidy + clang-format from 22.1.8 to 23.1.3 (#21296)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-21 17:45:36 -10:00
clydeandClaude Opus 5.5 ced497995d [online_image] Add on_download_progress, to show how far a download has got
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 14:35:37 +11:00
clydeandClaude Opus 5.5 228c553f60 [online_image] Also give the buffer_size hint when unreadable headers end in a read error
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 14:34:15 +11:00
clydeandClaude Opus 5.5 563e197c6d [online_image] Open a streamed JPEG in a pass of its own, and only blame the buffer for unreadable headers
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 14:29:07 +11:00
clyde 9d07f1f400 Merge branch 'runtime-image-jpeg-steps' into online-image-jpeg-stream 2026-10-22 14:27:47 +11:00
clydeandClaude Opus 5.5 846fc70bdf [runtime_image] Report a progressive JPEG as an unsupported format
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 14:27:46 +11:00
Craig Andrews c76e9e1d62 [epaper_spi] Support Good Display GDEY029Z95 2.9" 3-color e-paper (128x296, SSD1680) (#21218) 2026-10-21 17:09:44 -10:00
esphome[bot] 7a3aac407f Bump aioesphomeapi from 46.18.0 to 46.19.0 (#21345) 2026-10-21 17:03:34 -10:00
J. Nick Koston b0a1054e8d [ld2410] Publish queried number values directly (#21346) 2026-10-21 16:56:51 -10:00
clyde 74212d8d04 Merge branch 'runtime-image-jpeg-steps' into online-image-jpeg-stream 2026-10-22 13:41:53 +11:00
clydeandClaude Opus 5.5 101c9cd840 [runtime_image] Take JPEGDEC from esphome-libs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 13:41:48 +11:00
clyde 8152dcf224 Merge branch 'runtime-image-jpeg-steps' into online-image-jpeg-stream 2026-10-22 13:37:13 +11:00
clydeandClaude Opus 5.5 f2d50b4aa7 [runtime_image] Drop the trailing underscore from the static JPEGDEC callbacks
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 13:37:13 +11:00
a8a8acb918 [sensor] Add Relative Humidity Correction Filter (#18287)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2026-10-21 16:33:11 -10:00
Jesse Hills 3ae5e1dfad [core] Add unit tests for the smaller core modules (#21333) 2026-10-22 15:23:22 +13:00
Jonathan Swoboda ef79d48b6f [core] Add a default label to the switches clang-tidy 23 flags as not covering every path (#21338) 2026-10-22 02:22:32 +00:00
clydeandClaude Opus 5.5 f75b3d6ded [online_image] Test decode and read errors part way through a streamed JPEG
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 13:17:00 +11:00
clyde 3cf29bf4fb Merge branch 'runtime-image-jpeg-steps' into online-image-jpeg-stream 2026-10-22 13:14:54 +11:00
clydeandClaude Opus 5.5 f752e3b150 [runtime_image] Report a failed seek to the start as a read error
Also limit the JPEGDEC pin comment to the fixes ESPHome uses: it never
crops, and refuses progressive images.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 13:14:51 +11:00
Jonathan Swoboda 94804cc741 [core] Drop IRAM_ATTR from interrupt handler declarations (#21335) 2026-10-21 16:13:18 -10:00
Jonathan Swoboda 879c5e5e14 [weikai][haier][zephyr_ble_server][wifi][improv_base] Fix the bugprone and diagnostic findings of clang-tidy 23 (#21334) 2026-10-22 02:12:43 +00:00
clyde a5bc7070d1 Merge branch 'runtime-image-jpeg-steps' into online-image-jpeg-stream 2026-10-22 13:06:41 +11:00
clydeandClaude Opus 5.5 b2eebfb010 [runtime_image] Pin JPEGDEC to the 1.8.4-esphome.1 release
Adds the upstream fix that skips building AC Huffman tables for
progressive images.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 13:06:37 +11:00
clyde a596d1d3c8 Merge branch 'runtime-image-jpeg-steps' into online-image-jpeg-stream 2026-10-22 09:09:13 +11:00
clydeandClaude Opus 5.5 6701aedae2 [runtime_image] Pin JPEGDEC with the grayscale, crop and progressive fixes
A grayscale JPEG decoded for any image type but direct RGB565 came out garbled:
JPEGDEC wrote RGB565 words where it was asked for RGB8888. The new pin fixes
that, along with cropping and a crash on progressive images with skipped blocks,
which ESPHome does not use.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 09:09:09 +11:00
clydeandClaude Opus 5.5 6fddbd1d48 [online_image] Decode a JPEG as it downloads
A JPEG had to be downloaded whole, into a buffer grown to the file size, and was
then decoded in one call. When the server gives the size, the JPEG decoder now
reads the download from the buffer itself and decodes a row of MCUs at a time,
within a time budget per loop pass. The buffer only has to hold the headers, so
buffer_size bounds the RAM used, and a slow link makes the decoder wait for the
rest rather than decode past what has arrived. Downloads without a size keep the
old path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 07:23:28 +11:00
clydeandClaude Opus 5.5 03aaa00d75 [runtime_image] Refuse a source decode without a usable image size
JPEGDEC needs the file size when it opens a source, and holds it in an int.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 07:21:14 +11:00
clydeandClaude Opus 5.5 b612eb2929 [runtime_image] Let a source-reading decoder wait for data that has not arrived
A source that is still arriving, such as a download, may have nothing to give
when the decoder asks for more. The pinned JPEGDEC now pauses before an MCU when
less input is buffered than it needs and the source has no more yet, and
decode_step() reports that as DECODE_STEP_NEED_DATA; the next call carries on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 07:04:54 +11:00
clydeandClaude Opus 5.5 63c041a95b [runtime_image] Pin JPEGDEC with the ARM NEON clipped MCU fix
Upstream's fix for the NEON path writing past the end of a row on clipped MCUs,
which overflows a framebuffer whose width is not a multiple of 16.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 22:36:44 +11:00
clydeandClaude Opus 5.5 f8d5096093 [runtime_image] Pin JPEGDEC with the ARM NEON colour order fix
The NEON 4:2:0 RGB8888 path swapped red and blue (JPEGDEC PR 121), which shows on
host builds on 64-bit ARM.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 22:26:39 +11:00
clydeandClaude Opus 5.5 f835600fa6 [runtime_image] Let a decoder read its input itself and decode JPEG a row at a time
JPEGDEC decodes a whole image in one call, from a copy of the whole file in RAM.
A JPEGDEC that can stop between rows (1.8.4 plus decodeBegin() and decodeRows(),
pinned from a fork for now) lets the JPEG decoder read a DecodeSource itself,
through JPEGDEC's own 2 KB buffer, and decode one row of MCUs per decode_step().
A component that can read its input on demand, such as a file, no longer needs a
buffer for the whole file or a main loop pass long enough to decode the image.

Decoders that cannot read a source say so, and the fed decode() path is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 22:08:01 +11:00
170 changed files with 9186 additions and 308 deletions
+8
View File
@@ -12,6 +12,7 @@ Checks: >-
-bugprone-multi-level-implicit-pointer-conversion,
-bugprone-narrowing-conversions,
-bugprone-tagged-union-member-count,
-bugprone-signed-bitwise,
-bugprone-signed-char-misuse,
-bugprone-switch-missing-default-case,
-cert-dcl50-cpp,
@@ -39,6 +40,7 @@ Checks: >-
-cppcoreguidelines-avoid-const-or-ref-data-members,
-cppcoreguidelines-avoid-do-while,
-cppcoreguidelines-avoid-magic-numbers,
-cppcoreguidelines-explicit-constructor,
-cppcoreguidelines-init-variables,
-cppcoreguidelines-macro-to-enum,
-cppcoreguidelines-macro-usage,
@@ -84,6 +86,7 @@ Checks: >-
-llvm-use-ranges,
-llvmlibc-*,
-misc-const-correctness,
-misc-explicit-constructor,
-misc-include-cleaner,
-misc-multiple-inheritance,
-misc-no-recursion,
@@ -113,6 +116,7 @@ Checks: >-
-objc-*,
-performance-enum-size,
-portability-avoid-pragma-once,
-portability-no-assembler,
-portability-template-virtual-member-function,
-readability-ambiguous-smartptr-reset-call,
-readability-avoid-nested-conditional-operator,
@@ -129,9 +133,13 @@ Checks: >-
-readability-named-parameter,
-readability-redundant-casting,
-readability-redundant-inline-specifier,
-readability-redundant-lambda-parameter-list,
-readability-redundant-member-init,
-readability-redundant-nested-if,
-readability-redundant-parentheses,
-readability-redundant-typename,
-readability-trailing-comma,
-readability-trivial-switch,
-readability-uppercase-literal-suffix,
-readability-use-anyofallof,
-readability-use-std-min-max,
+1 -1
View File
@@ -38,7 +38,7 @@ repos:
- id: yamllint
exclude: ^(\.clang-format|\.clang-tidy)$
- repo: https://github.com/pre-commit/mirrors-clang-format
rev: v22.1.8
rev: v23.1.3
hooks:
- id: clang-format
types_or: [c, c++]
+1
View File
@@ -700,6 +700,7 @@ esphome/components/veml7700/* @latonita
esphome/components/version/* @esphome/core
esphome/components/vevor_dtx/* @Bofr5555
esphome/components/vl53l1x/* @will-tm
esphome/components/vnc/* @MindFreeze
esphome/components/voice_assistant/* @jesserockz @kahrendt
esphome/components/wake_on_lan/* @clydebarrow @willwill2will54
esphome/components/watchdog/* @oarcher
@@ -111,6 +111,8 @@ static const LogString *input_to_str(uint8_t input) {
return LOG_STR("VDD");
case NRF_SAADC_INPUT_VDDHDIV5:
return LOG_STR("VDDHDIV5");
default:
break;
}
return LOG_STR("undefined input");
}
+2
View File
@@ -261,6 +261,8 @@ void AirtonClimate::parse_state_frame_(const uint8_t *frame) {
case AIRTON_FAN_AUTO:
this->fan_mode = climate::CLIMATE_FAN_AUTO;
break;
default:
break;
}
this->target_temperature = (frame[3] & 0b00001111) + AIRTON_TEMP_MIN;
+4
View File
@@ -608,6 +608,7 @@ message ListEntitiesCoverResponse {
bool supports_stop = 12;
uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"];
bool supports_stop_tilt = 14;
bool supports_toggle = 15;
}
// Deprecated in API version 1.1
@@ -670,6 +671,7 @@ message CoverCommandRequest {
bool stop = 8;
uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"];
bool stop_tilt = 10;
bool toggle = 11;
}
// ==================== FAN ====================
@@ -1783,6 +1785,8 @@ message MediaPlayerSupportedFormat {
uint32 num_channels = 3;
MediaPlayerFormatPurpose purpose = 4;
uint32 sample_bytes = 5;
// Constant bitrate in kbps for lossy formats; 0 means no preference
uint32 bitrate = 6;
}
message ListEntitiesMediaPlayerResponse {
option (id) = 63;
@@ -555,6 +555,7 @@ uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *c
msg.supports_tilt = traits.get_supports_tilt();
msg.supports_stop = traits.get_supports_stop();
msg.supports_stop_tilt = traits.get_supports_stop_tilt();
msg.supports_toggle = traits.get_supports_toggle();
return fill_and_encode_entity_info_with_device_class(cover, msg, msg.device_class, conn, remaining_size);
}
void APIConnection::on_cover_command_request(const CoverCommandRequest &msg) {
@@ -567,6 +568,8 @@ void APIConnection::on_cover_command_request(const CoverCommandRequest &msg) {
call.set_command_stop();
if (msg.stop_tilt)
call.set_command_stop_tilt();
if (msg.toggle)
call.set_command_toggle();
call.perform();
}
#endif
@@ -1128,6 +1131,7 @@ uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnec
media_format.num_channels = supported_format.num_channels;
media_format.purpose = static_cast<enums::MediaPlayerFormatPurpose>(supported_format.purpose);
media_format.sample_bytes = supported_format.sample_bytes;
media_format.bitrate = supported_format.bitrate;
}
return fill_and_encode_entity_info(media_player, msg, conn, remaining_size);
}
+129
View File
@@ -25,6 +25,8 @@ void HelloRequest::decode_field(void *self, uint32_t tag, const uint8_t *data, p
msg.outgoing_connection_target = value.as_bool();
break;
#endif
default:
break;
}
}
uint8_t *HelloResponse::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
@@ -51,6 +53,8 @@ void DisconnectRequest::decode_field(void *self, uint32_t tag, const uint8_t *da
case proto_tag(1, WIRE_TYPE_VARINT):
msg.reason = static_cast<enums::DisconnectReason>(value.as_varint());
break;
default:
break;
}
}
uint8_t *DisconnectRequest::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
@@ -396,6 +400,8 @@ void WizardInputSetRequest::decode_field(void *self, uint32_t tag, const uint8_t
case proto_tag(2, WIRE_TYPE_LENGTH_DELIMITED):
msg.entity_id = StringRef(value.data(), value.size());
break;
default:
break;
}
}
#endif
@@ -478,6 +484,7 @@ uint8_t *ListEntitiesCoverResponse::encode_msg(const void *self, uint8_t *__rest
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 13, msg.device_id);
#endif
pos = ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 14, msg.supports_stop_tilt);
pos = ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 15, msg.supports_toggle);
return pos;
}
uint32_t ListEntitiesCoverResponse::calc_size_msg(const void *self) {
@@ -500,6 +507,7 @@ uint32_t ListEntitiesCoverResponse::calc_size_msg(const void *self) {
size += ProtoSize::calc_uint32(1, msg.device_id);
#endif
size += ProtoSize::calc_bool(1, msg.supports_stop_tilt);
size += ProtoSize::calc_bool(1, msg.supports_toggle);
return size;
}
uint8_t *CoverStateResponse::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
@@ -559,6 +567,11 @@ void CoverCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t *
case proto_tag(10, WIRE_TYPE_VARINT):
msg.stop_tilt = value.as_bool();
break;
case proto_tag(11, WIRE_TYPE_VARINT):
msg.toggle = value.as_bool();
break;
default:
break;
}
}
#endif
@@ -679,6 +692,8 @@ void FanCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t *da
msg.device_id = value.as_varint();
break;
#endif
default:
break;
}
}
#endif
@@ -885,6 +900,8 @@ void LightCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t *
msg.device_id = value.as_varint();
break;
#endif
default:
break;
}
}
#endif
@@ -1028,6 +1045,8 @@ void SwitchCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t
msg.device_id = value.as_varint();
break;
#endif
default:
break;
}
}
#endif
@@ -1098,6 +1117,8 @@ void SubscribeLogsRequest::decode_field(void *self, uint32_t tag, const uint8_t
case proto_tag(2, WIRE_TYPE_VARINT):
msg.dump_config = value.as_bool();
break;
default:
break;
}
}
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
@@ -1127,6 +1148,8 @@ void NoiseEncryptionSetKeyRequest::decode_field(void *self, uint32_t tag, const
msg.key = value.data();
msg.key_len = value.size();
break;
default:
break;
}
}
uint8_t *NoiseEncryptionSetKeyResponse::encode_msg(const void *self,
@@ -1233,6 +1256,8 @@ void HomeassistantActionResponse::decode_field(void *self, uint32_t tag, const u
msg.response_data_len = value.size();
break;
#endif
default:
break;
}
}
#endif
@@ -1267,6 +1292,8 @@ void HomeAssistantStateResponse::decode_field(void *self, uint32_t tag, const ui
case proto_tag(3, WIRE_TYPE_LENGTH_DELIMITED):
msg.attribute = StringRef(value.data(), value.size());
break;
default:
break;
}
}
#endif
@@ -1292,6 +1319,8 @@ void DSTRule::decode_field(void *self, uint32_t tag, const uint8_t *data, proto_
case proto_tag(6, WIRE_TYPE_VARINT):
msg.day_of_week = value.as_varint();
break;
default:
break;
}
}
void ParsedTimezone::decode_field(void *self, uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) {
@@ -1310,6 +1339,8 @@ void ParsedTimezone::decode_field(void *self, uint32_t tag, const uint8_t *data,
case proto_tag(4, WIRE_TYPE_LENGTH_DELIMITED):
value.decode_to_message(msg.dst_end);
break;
default:
break;
}
}
void GetTimeResponse::decode_field(void *self, uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) {
@@ -1323,6 +1354,8 @@ void GetTimeResponse::decode_field(void *self, uint32_t tag, const uint8_t *data
value.decode_to_message(msg.parsed_timezone);
msg.has_parsed_timezone = true;
break;
default:
break;
}
}
#ifdef USE_API_USER_DEFINED_ACTIONS
@@ -1413,6 +1446,8 @@ void ExecuteServiceArgument::decode_field(void *self, uint32_t tag, const uint8_
case proto_tag(9, WIRE_TYPE_LENGTH_DELIMITED):
msg.string_array.push_back(value.as_string());
break;
default:
break;
}
}
void ExecuteServiceArgument::decode(const uint8_t *buffer, size_t length) {
@@ -1447,6 +1482,8 @@ void ExecuteServiceRequest::decode_field(void *self, uint32_t tag, const uint8_t
msg.return_response = value.as_bool();
break;
#endif
default:
break;
}
}
void ExecuteServiceRequest::decode(const uint8_t *buffer, size_t length) {
@@ -1541,6 +1578,8 @@ void CameraImageRequest::decode_field(void *self, uint32_t tag, const uint8_t *d
case proto_tag(2, WIRE_TYPE_VARINT):
msg.stream = value.as_bool();
break;
default:
break;
}
}
#endif
@@ -1777,6 +1816,8 @@ void ClimateCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t
msg.device_id = value.as_varint();
break;
#endif
default:
break;
}
}
#endif
@@ -1905,6 +1946,8 @@ void WaterHeaterCommandRequest::decode_field(void *self, uint32_t tag, const uin
case proto_tag(8, WIRE_TYPE_FIXED32):
msg.target_temperature_high = value.as_float();
break;
default:
break;
}
}
#endif
@@ -1996,6 +2039,8 @@ void NumberCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t
msg.device_id = value.as_varint();
break;
#endif
default:
break;
}
}
#endif
@@ -2075,6 +2120,8 @@ void SelectCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t
msg.device_id = value.as_varint();
break;
#endif
default:
break;
}
}
#endif
@@ -2177,6 +2224,8 @@ void SirenCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t *
msg.device_id = value.as_varint();
break;
#endif
default:
break;
}
}
#endif
@@ -2260,6 +2309,8 @@ void LockCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t *d
msg.device_id = value.as_varint();
break;
#endif
default:
break;
}
}
#endif
@@ -2309,6 +2360,8 @@ void ButtonCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t
msg.device_id = value.as_varint();
break;
#endif
default:
break;
}
}
#endif
@@ -2320,6 +2373,7 @@ uint8_t *MediaPlayerSupportedFormat::encode_msg(const void *self, uint8_t *__res
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, msg.num_channels);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, static_cast<uint32_t>(msg.purpose));
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 5, msg.sample_bytes);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 6, msg.bitrate);
return pos;
}
uint32_t MediaPlayerSupportedFormat::calc_size_msg(const void *self) {
@@ -2330,6 +2384,7 @@ uint32_t MediaPlayerSupportedFormat::calc_size_msg(const void *self) {
size += ProtoSize::calc_uint32(1, msg.num_channels);
size += msg.purpose ? 2 : 0;
size += ProtoSize::calc_uint32(1, msg.sample_bytes);
size += ProtoSize::calc_uint32(1, msg.bitrate);
return size;
}
uint8_t *ListEntitiesMediaPlayerResponse::encode_msg(const void *self,
@@ -2436,6 +2491,8 @@ void MediaPlayerCommandRequest::decode_field(void *self, uint32_t tag, const uin
msg.device_id = value.as_varint();
break;
#endif
default:
break;
}
}
#endif
@@ -2448,6 +2505,8 @@ void SubscribeBluetoothLEAdvertisementsRequest::decode_field(void *self, uint32_
case proto_tag(1, WIRE_TYPE_VARINT):
msg.flags = value.as_varint();
break;
default:
break;
}
}
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
@@ -2506,6 +2565,8 @@ void BluetoothDeviceRequest::decode_field(void *self, uint32_t tag, const uint8_
case proto_tag(4, WIRE_TYPE_VARINT):
msg.address_type = value.as_varint();
break;
default:
break;
}
}
uint8_t *BluetoothDeviceConnectionResponse::encode_msg(const void *self,
@@ -2534,6 +2595,8 @@ void BluetoothGATTGetServicesRequest::decode_field(void *self, uint32_t tag, con
case proto_tag(1, WIRE_TYPE_VARINT):
msg.address = value.as_varint();
break;
default:
break;
}
}
uint8_t *BluetoothGATTDescriptor::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
@@ -2660,6 +2723,8 @@ void BluetoothGATTReadRequest::decode_field(void *self, uint32_t tag, const uint
case proto_tag(2, WIRE_TYPE_VARINT):
msg.handle = value.as_varint();
break;
default:
break;
}
}
uint8_t *BluetoothGATTReadResponse::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
@@ -2695,6 +2760,8 @@ void BluetoothGATTWriteRequest::decode_field(void *self, uint32_t tag, const uin
msg.data = value.data();
msg.data_len = value.size();
break;
default:
break;
}
}
void BluetoothGATTReadDescriptorRequest::decode_field(void *self, uint32_t tag, const uint8_t *data,
@@ -2708,6 +2775,8 @@ void BluetoothGATTReadDescriptorRequest::decode_field(void *self, uint32_t tag,
case proto_tag(2, WIRE_TYPE_VARINT):
msg.handle = value.as_varint();
break;
default:
break;
}
}
void BluetoothGATTWriteDescriptorRequest::decode_field(void *self, uint32_t tag, const uint8_t *data,
@@ -2725,6 +2794,8 @@ void BluetoothGATTWriteDescriptorRequest::decode_field(void *self, uint32_t tag,
msg.data = value.data();
msg.data_len = value.size();
break;
default:
break;
}
}
void BluetoothGATTNotifyRequest::decode_field(void *self, uint32_t tag, const uint8_t *data,
@@ -2741,6 +2812,8 @@ void BluetoothGATTNotifyRequest::decode_field(void *self, uint32_t tag, const ui
case proto_tag(3, WIRE_TYPE_VARINT):
msg.enable = value.as_bool();
break;
default:
break;
}
}
uint8_t *BluetoothGATTNotifyDataResponse::encode_msg(const void *self,
@@ -2898,6 +2971,8 @@ void BluetoothScannerSetModeRequest::decode_field(void *self, uint32_t tag, cons
case proto_tag(1, WIRE_TYPE_VARINT):
msg.mode = static_cast<enums::BluetoothScannerMode>(value.as_varint());
break;
default:
break;
}
}
#endif
@@ -2913,6 +2988,8 @@ void SubscribeVoiceAssistantRequest::decode_field(void *self, uint32_t tag, cons
case proto_tag(2, WIRE_TYPE_VARINT):
msg.flags = value.as_varint();
break;
default:
break;
}
}
uint8_t *VoiceAssistantAudioSettings::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
@@ -2961,6 +3038,8 @@ void VoiceAssistantResponse::decode_field(void *self, uint32_t tag, const uint8_
case proto_tag(2, WIRE_TYPE_VARINT):
msg.error = value.as_bool();
break;
default:
break;
}
}
void VoiceAssistantEventData::decode_field(void *self, uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) {
@@ -2973,6 +3052,8 @@ void VoiceAssistantEventData::decode_field(void *self, uint32_t tag, const uint8
case proto_tag(2, WIRE_TYPE_LENGTH_DELIMITED):
msg.value = StringRef(value.data(), value.size());
break;
default:
break;
}
}
void VoiceAssistantEventResponse::decode_field(void *self, uint32_t tag, const uint8_t *data,
@@ -2987,6 +3068,8 @@ void VoiceAssistantEventResponse::decode_field(void *self, uint32_t tag, const u
msg.data.emplace_back();
value.decode_to_message(msg.data.back());
break;
default:
break;
}
}
void VoiceAssistantAudio::decode_field(void *self, uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) {
@@ -3004,6 +3087,8 @@ void VoiceAssistantAudio::decode_field(void *self, uint32_t tag, const uint8_t *
msg.data2 = value.data();
msg.data2_len = value.size();
break;
default:
break;
}
}
uint8_t *VoiceAssistantAudio::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
@@ -3044,6 +3129,8 @@ void VoiceAssistantTimerEventResponse::decode_field(void *self, uint32_t tag, co
case proto_tag(6, WIRE_TYPE_VARINT):
msg.is_active = value.as_bool();
break;
default:
break;
}
}
void VoiceAssistantAnnounceRequest::decode_field(void *self, uint32_t tag, const uint8_t *data,
@@ -3063,6 +3150,8 @@ void VoiceAssistantAnnounceRequest::decode_field(void *self, uint32_t tag, const
case proto_tag(4, WIRE_TYPE_VARINT):
msg.start_conversation = value.as_bool();
break;
default:
break;
}
}
uint8_t *VoiceAssistantAnnounceFinished::encode_msg(const void *self,
@@ -3124,6 +3213,8 @@ void VoiceAssistantExternalWakeWord::decode_field(void *self, uint32_t tag, cons
case proto_tag(7, WIRE_TYPE_LENGTH_DELIMITED):
msg.url = StringRef(value.data(), value.size());
break;
default:
break;
}
}
void VoiceAssistantConfigurationRequest::decode_field(void *self, uint32_t tag, const uint8_t *data,
@@ -3135,6 +3226,8 @@ void VoiceAssistantConfigurationRequest::decode_field(void *self, uint32_t tag,
msg.external_wake_words.emplace_back();
value.decode_to_message(msg.external_wake_words.back());
break;
default:
break;
}
}
uint8_t *VoiceAssistantConfigurationResponse::encode_msg(const void *self,
@@ -3173,6 +3266,8 @@ void VoiceAssistantSetConfiguration::decode_field(void *self, uint32_t tag, cons
case proto_tag(1, WIRE_TYPE_LENGTH_DELIMITED):
msg.active_wake_words.push_back(value.as_string());
break;
default:
break;
}
}
#endif
@@ -3254,6 +3349,8 @@ void AlarmControlPanelCommandRequest::decode_field(void *self, uint32_t tag, con
msg.device_id = value.as_varint();
break;
#endif
default:
break;
}
}
#endif
@@ -3333,6 +3430,8 @@ void TextCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t *d
msg.device_id = value.as_varint();
break;
#endif
default:
break;
}
}
#endif
@@ -3414,6 +3513,8 @@ void DateCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t *d
msg.device_id = value.as_varint();
break;
#endif
default:
break;
}
}
#endif
@@ -3495,6 +3596,8 @@ void TimeCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t *d
msg.device_id = value.as_varint();
break;
#endif
default:
break;
}
}
#endif
@@ -3644,6 +3747,8 @@ void ValveCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t *
msg.device_id = value.as_varint();
break;
#endif
default:
break;
}
}
#endif
@@ -3718,6 +3823,8 @@ void DateTimeCommandRequest::decode_field(void *self, uint32_t tag, const uint8_
msg.device_id = value.as_varint();
break;
#endif
default:
break;
}
}
#endif
@@ -3807,6 +3914,8 @@ void UpdateCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t
msg.device_id = value.as_varint();
break;
#endif
default:
break;
}
}
#endif
@@ -3819,6 +3928,8 @@ void ZWaveProxyFrame::decode_field(void *self, uint32_t tag, const uint8_t *data
msg.data = value.data();
msg.data_len = value.size();
break;
default:
break;
}
}
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
@@ -3845,6 +3956,8 @@ void ZWaveProxyRequest::decode_field(void *self, uint32_t tag, const uint8_t *da
msg.data = value.data();
msg.data_len = value.size();
break;
default:
break;
}
}
uint8_t *ZWaveProxyRequest::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
@@ -3940,6 +4053,8 @@ void InfraredRFTransmitRawTimingsRequest::decode_field(void *self, uint32_t tag,
case proto_tag(6, WIRE_TYPE_VARINT):
msg.modulation = value.as_varint();
break;
default:
break;
}
}
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
@@ -4056,6 +4171,8 @@ void SerialProxyConfigureRequest::decode_field(void *self, uint32_t tag, const u
case proto_tag(6, WIRE_TYPE_VARINT):
msg.data_size = value.as_varint();
break;
default:
break;
}
}
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
@@ -4084,6 +4201,8 @@ void SerialProxyWriteRequest::decode_field(void *self, uint32_t tag, const uint8
msg.data = value.data();
msg.data_len = value.size();
break;
default:
break;
}
}
void SerialProxySetModemPinsRequest::decode_field(void *self, uint32_t tag, const uint8_t *data,
@@ -4097,6 +4216,8 @@ void SerialProxySetModemPinsRequest::decode_field(void *self, uint32_t tag, cons
case proto_tag(2, WIRE_TYPE_VARINT):
msg.line_states = value.as_varint();
break;
default:
break;
}
}
void SerialProxyGetModemPinsRequest::decode_field(void *self, uint32_t tag, const uint8_t *data,
@@ -4107,6 +4228,8 @@ void SerialProxyGetModemPinsRequest::decode_field(void *self, uint32_t tag, cons
case proto_tag(1, WIRE_TYPE_VARINT):
msg.instance = value.as_varint();
break;
default:
break;
}
}
uint8_t *SerialProxyGetModemPinsResponse::encode_msg(const void *self,
@@ -4135,6 +4258,8 @@ void SerialProxyRequest::decode_field(void *self, uint32_t tag, const uint8_t *d
case proto_tag(2, WIRE_TYPE_VARINT):
msg.type = static_cast<enums::SerialProxyRequestType>(value.as_varint());
break;
default:
break;
}
}
uint8_t *SerialProxyRequestResponse::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
@@ -4165,6 +4290,8 @@ void SerialProxySetModeRequest::decode_field(void *self, uint32_t tag, const uin
case proto_tag(2, WIRE_TYPE_VARINT):
msg.mode = static_cast<enums::SerialProxyMode>(value.as_varint());
break;
default:
break;
}
}
uint8_t *UsbDeviceDescriptor::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
@@ -4233,6 +4360,8 @@ void BluetoothSetConnectionParamsRequest::decode_field(void *self, uint32_t tag,
case proto_tag(5, WIRE_TYPE_VARINT):
msg.timeout = value.as_varint();
break;
default:
break;
}
}
uint8_t *BluetoothSetConnectionParamsResponse::encode_msg(const void *self,
+5 -2
View File
@@ -911,7 +911,7 @@ class BinarySensorStateResponse final : public StateResponseProtoMessage {
class ListEntitiesCoverResponse final : public InfoResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 13;
static constexpr uint8_t ESTIMATED_SIZE = 59;
static constexpr uint8_t ESTIMATED_SIZE = 61;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("list_entities_cover_response"); }
#endif
@@ -921,6 +921,7 @@ class ListEntitiesCoverResponse final : public InfoResponseProtoMessage {
ProgmemStringRef device_class{};
bool supports_stop{false};
bool supports_stop_tilt{false};
bool supports_toggle{false};
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
@@ -958,7 +959,7 @@ class CoverStateResponse final : public StateResponseProtoMessage {
class CoverCommandRequest final : public CommandProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 30;
static constexpr uint8_t ESTIMATED_SIZE = 27;
static constexpr uint8_t ESTIMATED_SIZE = 29;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("cover_command_request"); }
#endif
@@ -968,6 +969,7 @@ class CoverCommandRequest final : public CommandProtoMessage {
float tilt{0.0f};
bool stop{false};
bool stop_tilt{false};
bool toggle{false};
void decode(const uint8_t *buffer, size_t length) {
ProtoDecodableMessage::decode_fields(this, buffer, length, &decode_field);
}
@@ -2261,6 +2263,7 @@ class MediaPlayerSupportedFormat final : public ProtoMessage {
uint32_t num_channels{0};
enums::MediaPlayerFormatPurpose purpose{};
uint32_t sample_bytes{0};
uint32_t bitrate{0};
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
+3
View File
@@ -1246,6 +1246,7 @@ const char *ListEntitiesCoverResponse::dump_to(DumpBuffer &out) const {
dump_field(out, ESPHOME_PSTR("device_id"), this->device_id);
#endif
dump_field(out, ESPHOME_PSTR("supports_stop_tilt"), this->supports_stop_tilt);
dump_field(out, ESPHOME_PSTR("supports_toggle"), this->supports_toggle);
return out.c_str();
}
const char *CoverStateResponse::dump_to(DumpBuffer &out) const {
@@ -1271,6 +1272,7 @@ const char *CoverCommandRequest::dump_to(DumpBuffer &out) const {
dump_field(out, ESPHOME_PSTR("device_id"), this->device_id);
#endif
dump_field(out, ESPHOME_PSTR("stop_tilt"), this->stop_tilt);
dump_field(out, ESPHOME_PSTR("toggle"), this->toggle);
return out.c_str();
}
#endif
@@ -2097,6 +2099,7 @@ const char *MediaPlayerSupportedFormat::dump_to(DumpBuffer &out) const {
dump_field(out, ESPHOME_PSTR("num_channels"), this->num_channels);
dump_field(out, ESPHOME_PSTR("purpose"), static_cast<enums::MediaPlayerFormatPurpose>(this->purpose));
dump_field(out, ESPHOME_PSTR("sample_bytes"), this->sample_bytes);
dump_field(out, ESPHOME_PSTR("bitrate"), this->bitrate);
return out.c_str();
}
const char *ListEntitiesMediaPlayerResponse::dump_to(DumpBuffer &out) const {
+1 -1
View File
@@ -301,7 +301,7 @@ constexpr uint32_t VARINT_MAX_1_BYTE = 1 << 7; // 128
constexpr uint32_t VARINT_MAX_2_BYTE = 1 << 14; // 16384
/// Generated encode body: writes the fields at pos, returns the cursor past them.
using ProtoEncodeFn = uint8_t *(*) (const void *, uint8_t *PROTO_ENCODE_DEBUG_PARAM);
using ProtoEncodeFn = uint8_t *(*)(const void *, uint8_t *PROTO_ENCODE_DEBUG_PARAM);
/// Static encode helpers for the generated encode bodies. Each takes the write cursor by value and
/// returns it advanced, so outlined calls at -Os chain through the return register instead of a
+2
View File
@@ -168,6 +168,8 @@ bool BalluClimate::on_receive(remote_base::RemoteReceiveData data) {
case BALLU_AUTO:
this->mode = climate::CLIMATE_MODE_AUTO;
break;
default:
break;
}
}
+4 -5
View File
@@ -111,10 +111,9 @@ static const uint8_t BL0906_USR_WRPROT = 0x9E;
// Reset Register
static const uint8_t BL0906_SOFT_RESET = 0x9F;
const uint8_t BL0906_INIT[2][6] = {
// Reset to default
{BL0906_WRITE_COMMAND, BL0906_SOFT_RESET, 0x5A, 0x5A, 0x5A, 0x52},
// Enable User Operation Write
{BL0906_WRITE_COMMAND, BL0906_USR_WRPROT, 0x55, 0x55, 0x00, 0xB7}};
const uint8_t BL0906_INIT[2][6] = {// Reset to default
{BL0906_WRITE_COMMAND, BL0906_SOFT_RESET, 0x5A, 0x5A, 0x5A, 0x52},
// Enable User Operation Write
{BL0906_WRITE_COMMAND, BL0906_USR_WRPROT, 0x55, 0x55, 0x00, 0xB7}};
} // namespace esphome::bl0906
@@ -268,6 +268,7 @@ int BluedroidGattClient::discover_services() {
case SearchState::REPORT_PENDING:
return 0; // One completion is already owed to this claimant.
case SearchState::NONE:
default:
break;
}
int err = this->check_and_log_error_("esp_ble_gattc_search_service",
+7 -3
View File
@@ -123,7 +123,7 @@ void BMI160Component::internal_setup_(int stage) {
bool BMI160Component::setup_stage_(int stage) {
switch (stage) {
case 0:
case 0: {
uint8_t chipid;
if (!this->read_byte(BMI160_REGISTER_CHIPID, &chipid) || (chipid != 0b11010001)) {
return false;
@@ -138,7 +138,7 @@ bool BMI160Component::setup_stage_(int stage) {
// min 5ms, 10ms
this->set_timeout(10, [this]() { this->internal_setup_(1); });
break;
}
case 1:
ESP_LOGV(TAG, " Bringing gyroscope out of sleep");
if (!this->write_byte(BMI160_REGISTER_CMD, (uint8_t) Cmd::GYRO_SET_PMU_MODE | (uint8_t) GyroPmuMode::NORMAL)) {
@@ -149,7 +149,7 @@ bool BMI160Component::setup_stage_(int stage) {
this->set_timeout(GYRO_WAKEUP_TIMEOUT_MS, [this]() { this->internal_setup_(2); });
break;
case 2:
case 2: {
ESP_LOGV(TAG, " Setting up Gyro Config");
uint8_t gyro_config = (uint8_t) GyroBandwidth::OSR4 | (uint8_t) GyroOuputDataRate::HZ_25;
ESP_LOGV(TAG, " Output gyro_config: 0b" BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(gyro_config));
@@ -178,6 +178,10 @@ bool BMI160Component::setup_stage_(int stage) {
}
this->setup_complete_ = true;
break;
}
default:
break;
}
return true;
}
+1 -1
View File
@@ -108,7 +108,7 @@ class CC1101Component final : public Component,
// GDO pin for packet reception
InternalGPIOPin *gdo0_pin_{nullptr};
static void IRAM_ATTR gpio_intr(CC1101Component *arg);
static void gpio_intr(CC1101Component *arg);
// Packet handling
void call_listeners_(const std::vector<uint8_t> &packet, float freq_offset, float rssi, uint8_t lqi);
+4 -1
View File
@@ -8,7 +8,10 @@ ESPHOME_LOG_TAG(TAG, "copy.lock");
void CopyLock::setup() {
source_->add_on_state_callback([this](lock::LockState state) { this->publish_state(state); });
this->traits = this->source_->traits;
traits.set_assumed_state(source_->traits.get_assumed_state());
traits.set_requires_code(source_->traits.get_requires_code());
traits.set_supported_states_mask(source_->traits.get_supported_states_mask());
traits.set_supports_open(source_->traits.get_supports_open());
this->publish_state(source_->state);
}
+2
View File
@@ -175,6 +175,8 @@ uint32_t CSE7761Component::coefficient_by_unit_(uint32_t unit) {
case POWER_PAC:
coeff = this->data_.coefficient[POWER_PAC] * scale;
return coeff ? 0x80000000 / coeff : 0;
default:
break;
}
return 0;
}
@@ -156,6 +156,8 @@ static const char *regout0_to_str(uint32_t value) {
return "3.0V";
case (UICR_REGOUT0_VOUT_3V3):
return "3.3V";
default:
break;
}
return "???V";
}
@@ -337,6 +339,8 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
return "QFxx - 6x6 48-pin QFN";
case 0x2005:
return "CKxx - 3.544 x 3.607 WLCSP";
default:
break;
}
return "Unspecified";
};
+2
View File
@@ -143,6 +143,8 @@ bool DelonghiClimate::parse_state_frame_(const uint8_t frame[]) {
case DELONGHI_FAN_AUTO:
this->fan_mode = climate::CLIMATE_FAN_AUTO;
break;
default:
break;
}
this->publish_state();
return true;
+2
View File
@@ -250,6 +250,8 @@ void DFPlayer::loop() {
ESP_LOGE(TAG, "Entered into sleep mode");
this->is_playing_ = false;
break;
default:
break;
}
break;
case 0x41:
@@ -0,0 +1,12 @@
#include "epaper_goodisplay_3c.h"
namespace esphome::epaper_spi {
void EPaperGoodDisplay3C::power_on() {}
void EPaperGoodDisplay3C::refresh_screen(bool partial) {
EPaperWeAct3C::refresh_screen(partial);
this->next_delay_ = REFRESH_START_MS;
}
} // namespace esphome::epaper_spi
@@ -0,0 +1,28 @@
#pragma once
#include "epaper_weact_3c.h"
namespace esphome::epaper_spi {
/**
* Good Display 3-color e-paper displays (SSD1680 controller), e.g. GDEY029Z95.
*
* Driven as EPaperWeAct3C, except that there is no separate power-on: the full refresh (0xF7) powers the panel
* up itself. Busy then only goes high a little after the refresh starts, so the next state waits before
* checking it.
*/
class EPaperGoodDisplay3C : public EPaperWeAct3C {
public:
using EPaperWeAct3C::EPaperWeAct3C;
protected:
// Time from master activation (0x20) until busy can be relied on
static constexpr uint16_t REFRESH_START_MS = 50;
// With EPaperWeAct3C's power-on (0x22 0xF8) first, this panel's refresh ends almost at once and the image
// does not change.
void power_on() override;
void refresh_screen(bool partial) override;
};
} // namespace esphome::epaper_spi
@@ -4,11 +4,17 @@ Supported models:
- weact-2.13in-3c: 122x250 pixels (2.13" display)
- weact-2.9in-3c: 128x296 pixels (2.9" display)
- weact-4.2in-3c: 400x300 pixels (4.2" display)
- goodisplay-gdey029z95-2.9: 128x296 pixels (Good Display GDEY029Z95 2.9" display)
These displays use SSD1680 or SSD1683 controller and require a specific initialization
sequence. The DRV_OUT_CTL command is calculated from the display height.
The Good Display panel is driven as the WeAct ones, except that it must not get the
separate power-on before the refresh, so it has its own driver class, EPaperGoodDisplay3C.
"""
import esphome.config_validation as cv
from esphome.const import CONF_BUSY_PIN, CONF_RESET_PIN
from . import EpaperModel
@@ -17,8 +23,10 @@ class WeActBWR(EpaperModel):
source_files = ("epaper_weact_3c.cpp",)
def __init__(self, name, **defaults):
super().__init__(name, "EPaperWeAct3C", **defaults)
def __init__(
self, name: str, class_name: str = "EPaperWeAct3C", **defaults: object
) -> None:
super().__init__(name, class_name, **defaults)
def get_init_sequence(self, config):
"""Generate initialization sequence for WeAct BWR displays.
@@ -75,3 +83,27 @@ weact_4p2in3c = WeActBWR(
data_rate="10MHz",
minimum_update_interval="10s",
)
class GoodDisplayBWR(WeActBWR):
"""Good Display Black/White/Red displays using SSD1680 controller."""
source_files = ("epaper_weact_3c.cpp", "epaper_goodisplay_3c.cpp")
def __init__(self, name: str, **defaults: object) -> None:
super().__init__(name, "EPaperGoodDisplay3C", **defaults)
def option(self, name, fallback=cv.UNDEFINED) -> cv.Optional | cv.Required:
# Only a hardware reset wakes the panel from deep sleep, and refreshes are timed by busy
if name in (CONF_RESET_PIN, CONF_BUSY_PIN):
return cv.Required(name)
return super().option(name, fallback)
# Model: Good Display GDEY029Z95 2.9" BWR - 128x296 pixels, SSD1680 controller
GoodDisplayBWR(
"goodisplay-gdey029z95-2.9",
width=128,
height=296,
minimum_update_interval="10s",
)
+21 -16
View File
@@ -46,6 +46,7 @@ from esphome.const import (
CONF_TYPE,
CONF_USE_ADDRESS,
CONF_VALUE,
CONF_WIFI,
KEY_CORE,
KEY_FRAMEWORK_VERSION,
Platform,
@@ -562,6 +563,25 @@ CONFIG_SCHEMA = cv.All(
)
def _final_validate_wifi(config: ConfigType) -> None:
"""A wifi station competes with ethernet for the primary interface, so both must be in network: priority:."""
full_config = fv.full_config.get()
if (wifi_config := full_config.get(CONF_WIFI)) is None:
return
from esphome.components.wifi import wifi_stays_ap_only
if wifi_stays_ap_only(wifi_config, full_config):
return # an access point does not claim the primary interface
priority_ifaces = get_priority_interfaces_from_full_config(full_config)
if missing := [i for i in (DOMAIN, CONF_WIFI) if i not in priority_ifaces]:
raise cv.Invalid(
"When ethernet and a wifi station are used together, 'network: priority:' "
f"must list both interfaces; missing: {', '.join(missing)}"
)
def _final_validate_spi(config: ConfigType) -> None:
if not CORE.is_esp32:
return # SPI interface validation is ESP32-only
@@ -846,22 +866,7 @@ def _final_validate_rmii_pins(config: ConfigType) -> None:
def _final_validate(config: ConfigType) -> None:
"""Final validation for Ethernet component."""
# Allow ethernet + wifi coexistence only when both are declared in network: priority:.
if "wifi" in fv.full_config.get():
priority_ifaces = get_priority_interfaces_from_full_config(fv.full_config.get())
missing = [i for i in ("ethernet", "wifi") if i not in priority_ifaces]
if missing and priority_ifaces:
# A priority list exists but is incomplete: point at what to add.
raise cv.Invalid(
"When ethernet and wifi are used together, 'network: priority:' must "
f"list both interfaces; missing: {', '.join(missing)}"
)
if missing:
raise cv.Invalid(
"Component ethernet cannot be used together with component wifi "
"unless both are listed under 'network: priority:'"
)
_final_validate_wifi(config)
_final_validate_spi(config)
_final_validate_rmii_pins(config)
+2
View File
@@ -175,6 +175,8 @@ void EzoPMP::read_command_result_() {
third_parameter_buffer[position_in_parameter_buffer] = current_char;
third_parameter_buffer[position_in_parameter_buffer + 1] = '\0';
break;
default:
break;
}
position_in_parameter_buffer++;
}
@@ -194,6 +194,8 @@ uint8_t FingerprintGrowComponent::scan_image_(uint8_t buffer) {
ESP_LOGE(TAG, "Could not find fingerprint features");
this->finger_scan_invalid_callback_.call();
break;
default:
break;
}
return send_result;
}
+6 -5
View File
@@ -318,20 +318,21 @@ void HaierClimateBase::process_protocol_reset() {
bool HaierClimateBase::prepare_pending_action() {
if (this->action_request_.has_value()) {
switch (this->action_request_.value().action) {
PendingAction &request = this->action_request_.value();
switch (request.action) {
case ActionRequest::SEND_CUSTOM_COMMAND:
return true;
case ActionRequest::TURN_POWER_ON:
this->action_request_.value().message = this->get_power_message(true);
request.message = this->get_power_message(true);
return true;
case ActionRequest::TURN_POWER_OFF:
this->action_request_.value().message = this->get_power_message(false);
request.message = this->get_power_message(false);
return true;
case ActionRequest::TOGGLE_POWER:
this->action_request_.value().message = this->get_power_message(this->mode == ClimateMode::CLIMATE_MODE_OFF);
request.message = this->get_power_message(this->mode == ClimateMode::CLIMATE_MODE_OFF);
return true;
default:
ESP_LOGW(TAG, "Unsupported action: %d", (uint8_t) this->action_request_.value().action);
ESP_LOGW(TAG, "Unsupported action: %d", (uint8_t) request.action);
this->action_request_.reset();
return false;
}
+4
View File
@@ -1071,6 +1071,8 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
case (uint8_t) hon_protocol::FanMode::FAN_HIGH:
this->fan_mode = CLIMATE_FAN_HIGH;
break;
default:
break;
}
should_publish = should_publish || (!old_fan_mode.has_value()) ||
(old_fan_mode.value_or(CLIMATE_FAN_ON) != this->fan_mode.value_or(CLIMATE_FAN_ON));
@@ -1144,6 +1146,8 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
case (uint8_t) hon_protocol::ConditioningMode::AUTO:
this->mode = CLIMATE_MODE_HEAT_COOL;
break;
default:
break;
}
}
should_publish = should_publish || (old_mode != this->mode);
@@ -40,6 +40,8 @@ void decode_mitsubishi_heavy_zmp(const uint8_t frame[MITSUBISHI_HEAVY_FRAME_LENG
case MITSUBISHI_HEAVY_ZMP_ECONO:
climate.preset = climate::CLIMATE_PRESET_ECO;
break;
default:
break;
}
uint8_t swing_h = frame[BYTE_SWING_H] & HORIZONTAL_SWING_MASK;
@@ -260,6 +260,8 @@ bool HitachiClimate::parse_mode_(const uint8_t remote_state[]) {
case HITACHI_AC344_MODE_FAN:
this->mode = climate::CLIMATE_MODE_FAN_ONLY;
break;
default:
break;
}
} else {
this->mode = climate::CLIMATE_MODE_OFF;
@@ -294,6 +296,8 @@ bool HitachiClimate::parse_fan_(const uint8_t remote_state[]) {
case HITACHI_AC344_FAN_AUTO:
this->fan_mode = climate::CLIMATE_FAN_AUTO;
break;
default:
break;
}
return true;
}
@@ -255,6 +255,8 @@ bool HitachiClimate::parse_mode_(const uint8_t remote_state[]) {
case HITACHI_AC424_MODE_FAN:
this->mode = climate::CLIMATE_MODE_FAN_ONLY;
break;
default:
break;
}
} else {
this->mode = climate::CLIMATE_MODE_OFF;
@@ -289,6 +291,8 @@ bool HitachiClimate::parse_fan_(const uint8_t remote_state[]) {
case HITACHI_AC424_FAN_AUTO:
this->fan_mode = climate::CLIMATE_FAN_AUTO;
break;
default:
break;
}
return true;
}
@@ -69,7 +69,7 @@ size_t ImprovBase::get_formatted_next_url_(char *buffer, size_t buffer_size) {
void ImprovBase::add_next_url_(improv::RpcResponseBuilder &builder, size_t max_len) {
// The builder rejects strings above 254 bytes, so anything longer than this
// buffer could never be sent anyway
char url_buffer[256];
char url_buffer[256] = {};
size_t len = this->get_formatted_next_url_(url_buffer, sizeof(url_buffer));
if (len == 0) {
return;
+2
View File
@@ -61,6 +61,8 @@ void Kuntze::on_read_holding_registers(uint16_t start_address, std::span<const u
if (this->oci_sensor_ != nullptr)
this->oci_sensor_->publish_state(value);
break;
default:
break;
}
}
+9 -16
View File
@@ -407,12 +407,10 @@ void LD2410Component::handle_periodic_data_() {
}
#ifdef USE_NUMBER
std::function<void()> set_number_value(number::Number *n, float value) {
void set_number_value(number::Number *n, float value) {
if (n != nullptr && (!n->has_state() || n->state != value)) {
n->state = value;
return [n, value]() { n->publish_state(value); };
n->publish_state(value);
}
return []() {};
}
#endif
@@ -560,29 +558,24 @@ bool LD2410Component::handle_ack_data_() {
Moving distance range: 13th byte
Still distance range: 14th byte
*/
std::vector<std::function<void()>> updates;
updates.push_back(set_number_value(this->max_move_distance_gate_number_, this->buffer_data_[12]));
updates.push_back(set_number_value(this->max_still_distance_gate_number_, this->buffer_data_[13]));
set_number_value(this->max_move_distance_gate_number_, this->buffer_data_[12]);
set_number_value(this->max_still_distance_gate_number_, this->buffer_data_[13]);
/*
Moving Sensitivities: 15~23th bytes
*/
for (std::vector<number::Number *>::size_type i = 0; i != this->gate_move_threshold_numbers_.size(); i++) {
updates.push_back(set_number_value(this->gate_move_threshold_numbers_[i], this->buffer_data_[14 + i]));
for (size_t i = 0; i < TOTAL_GATES; i++) {
set_number_value(this->gate_move_threshold_numbers_[i], this->buffer_data_[14 + i]);
}
/*
Still Sensitivities: 24~32th bytes
*/
for (std::vector<number::Number *>::size_type i = 0; i != this->gate_still_threshold_numbers_.size(); i++) {
updates.push_back(set_number_value(this->gate_still_threshold_numbers_[i], this->buffer_data_[23 + i]));
for (size_t i = 0; i < TOTAL_GATES; i++) {
set_number_value(this->gate_still_threshold_numbers_[i], this->buffer_data_[23 + i]);
}
/*
None Duration: 33~34th bytes
*/
updates.push_back(
set_number_value(this->timeout_number_, encode_uint16(this->buffer_data_[33], this->buffer_data_[32])));
for (auto &update : updates) {
update();
}
set_number_value(this->timeout_number_, encode_uint16(this->buffer_data_[33], this->buffer_data_[32]));
#endif
break;
}
+2
View File
@@ -218,6 +218,8 @@ bool LwRx::lwrx_getmessage(uint8_t *buf, uint8_t len) {
case 10:
buf[i] = j;
break;
default:
break;
}
}
} else {
+10 -14
View File
@@ -35,12 +35,12 @@ class LockTraits {
public:
LockTraits() = default;
bool get_supports_open() const { return this->flags_.supports_open; }
void set_supports_open(bool supports_open) { this->flags_.supports_open = supports_open; }
bool get_requires_code() const { return this->flags_.requires_code; }
void set_requires_code(bool requires_code) { this->flags_.requires_code = requires_code; }
bool get_assumed_state() const { return this->flags_.assumed_state; }
void set_assumed_state(bool assumed_state) { this->flags_.assumed_state = assumed_state; }
bool get_supports_open() const { return this->supports_open_; }
void set_supports_open(bool supports_open) { this->supports_open_ = supports_open; }
bool get_requires_code() const { return this->requires_code_; }
void set_requires_code(bool requires_code) { this->requires_code_ = requires_code; }
bool get_assumed_state() const { return this->assumed_state_; }
void set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; }
bool supports_state(LockState state) const { return supported_states_mask_ & (1 << state); }
void set_supported_states(std::initializer_list<LockState> states) {
@@ -54,13 +54,9 @@ class LockTraits {
void add_supported_state(LockState state) { supported_states_mask_ |= (1 << state); }
protected:
// Bit-packed flags to save memory (1 byte instead of 3)
struct LockTraitFlags {
uint8_t supports_open : 1;
uint8_t requires_code : 1;
uint8_t assumed_state : 1;
uint8_t reserved : 5;
} flags_{};
bool supports_open_{false};
bool requires_code_{false};
bool assumed_state_{false};
uint8_t supported_states_mask_{(1 << LOCK_STATE_NONE) | (1 << LOCK_STATE_LOCKED) | (1 << LOCK_STATE_UNLOCKED)};
};
@@ -180,8 +176,8 @@ class Lock : public EntityBase {
*/
virtual void control(const LockCall &call) = 0;
Deduplicator<LockState> publish_dedup_; // packs with state and traits ahead of the pointer members
LazyCallbackManager<void(LockState)> state_callback_{};
Deduplicator<LockState> publish_dedup_;
ESPPreferenceObject rtc_;
};
+1 -1
View File
@@ -38,7 +38,7 @@ class MCP23016 final : public Component, public i2c::I2CDevice, public gpio_expa
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
protected:
static void IRAM_ATTR gpio_intr(MCP23016 *arg);
static void gpio_intr(MCP23016 *arg);
// Virtual methods from CachedGpioExpander
bool digital_read_hw(uint8_t pin) override;
bool digital_read_cache(uint8_t pin) override;
@@ -10,7 +10,7 @@ enum MCP23XXXInterruptMode : uint8_t { MCP23XXX_NO_INTERRUPT = 0, MCP23XXX_CHANG
// Not a template member: GCC 10 (ESP8266) ignores section attributes on template instantiations, leaving the
// ISR in flash.
void IRAM_ATTR mcp23xxx_gpio_intr(Component *arg);
void mcp23xxx_gpio_intr(Component *arg);
template<uint8_t N> class MCP23XXXBase : public Component, public gpio_expander::CachedGpioExpander<uint8_t, N> {
public:
+2
View File
@@ -81,6 +81,8 @@ void Mcp4461Component::initialize_terminal_disabled(Mcp4461WiperIdx wiper, char
case 'w':
this->reg_[wiper_idx].terminal_w = false;
break;
default:
break;
}
}
@@ -3,6 +3,7 @@ from collections.abc import Callable
from esphome import automation
import esphome.codegen as cg
from esphome.components import audio
from esphome.components.const import CONF_BITRATE
import esphome.config_validation as cv
from esphome.const import (
CONF_ENTITY_CATEGORY,
@@ -55,6 +56,30 @@ FORMAT_MAPPING = {
}
# MP3 constant bitrates in kbps; the encoder rounds anything else to the nearest one
MP3_BITRATES = (
8,
16,
24,
32,
40,
48,
56,
64,
80,
96,
112,
128,
144, # MPEG-2 only, at 16, 22.05 and 24 kHz
160,
192,
224,
256,
320,
)
validate_mp3_bitrate = cv.one_of(*MP3_BITRATES, int=True)
def build_supported_format_struct(
format_config: ConfigType, purpose: MockObj
) -> cg.StructInitializer:
@@ -75,6 +100,10 @@ def build_supported_format_struct(
if format_config[CONF_FORMAT] != "MP3":
args.append(("sample_bytes", 2))
# Omitted means no preference, and the client keeps encoding as it always has.
if bitrate := format_config.get(CONF_BITRATE):
args.append(("bitrate", bitrate))
return cg.StructInitializer(*args)
@@ -91,6 +120,14 @@ def validate_preferred_format(
inherit_property_from(CONF_NUM_CHANNELS, audio_device_key)(config)
inherit_property_from(CONF_SAMPLE_RATE, audio_device_key)(config)
# A constant bitrate is only meaningful for the one lossy format whose
# encoder Home Assistant can be told to fix
if CONF_BITRATE in config and config.get(CONF_FORMAT) != "MP3":
raise cv.Invalid(
f"{CONF_BITRATE} is only supported for the MP3 format",
path=[CONF_BITRATE],
)
# Opus only supports 48 kHz
if config.get(CONF_FORMAT) == "OPUS" and config.get(CONF_SAMPLE_RATE) != 48000:
raise cv.Invalid("Opus only supports a sample rate of 48000 Hz")
@@ -77,6 +77,7 @@ struct MediaPlayerSupportedFormat {
uint32_t num_channels;
MediaPlayerFormatPurpose purpose;
uint32_t sample_bytes;
uint32_t bitrate;
};
// Base features always reported for all media players
+2 -2
View File
@@ -244,9 +244,9 @@ class MipiSpi : public display::Display,
char hex_buf[format_hex_pretty_size(MIPI_SPI_MAX_CMD_LOG_BYTES)];
// Don't spam the log after setup
if (this->init_sequence_len_ == 0) {
esph_log_v(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty_to(hex_buf, bytes, len));
esph_log_v(TAG, "Command %02X, length %zu, bytes %s", cmd, len, format_hex_pretty_to(hex_buf, bytes, len));
} else {
esph_log_d(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty_to(hex_buf, bytes, len));
esph_log_d(TAG, "Command %02X, length %zu, bytes %s", cmd, len, format_hex_pretty_to(hex_buf, bytes, len));
}
if constexpr (BUS_TYPE == BUS_TYPE_QUAD) {
this->enable();
+2
View File
@@ -254,6 +254,8 @@ bool NoblexClimate::on_receive(remote_base::RemoteReceiveData data) {
case IRNoblexMode::IR_NOBLEX_MODE_HEAT:
this->mode = climate::CLIMATE_MODE_HEAT;
break;
default:
break;
}
}
@@ -1,5 +1,6 @@
#include "download_buffer.h"
#include "esphome/core/log.h"
#include <algorithm>
#include <cstring>
namespace esphome::online_image {
@@ -34,6 +35,46 @@ size_t DownloadBuffer::read(size_t len) {
return this->unread_;
}
void DownloadSource::compact() {
if (this->keep_read_ || this->pos_ == this->start_)
return;
this->buffer_.read(this->pos_ - this->start_);
this->start_ = this->pos_;
}
int32_t DownloadSource::read(uint8_t *buf, uint32_t len) {
const size_t offset = this->pos_ - this->start_;
const size_t n = std::min<size_t>(len, this->buffer_.unread() - offset);
memcpy(buf, this->buffer_.data(offset), n);
this->pos_ += n;
return static_cast<int32_t>(n);
}
bool DownloadSource::seek(uint32_t position) {
// Only what is still in the buffer can be read again; a decoder never skips ahead of the download
if (position < this->start_ || position > this->start_ + this->buffer_.unread())
return false;
this->pos_ = position;
return true;
}
bool DownloadBuffer::grow(size_t size) {
if (this->size_ >= size)
return true;
RAMAllocator<uint8_t> allocator;
uint8_t *buffer = allocator.allocate(size);
if (buffer == nullptr) {
ESP_LOGE(TAG, "allocation of %zu bytes failed. Biggest block in heap: %zu Bytes", size,
allocator.get_max_free_block_size());
return false;
}
memcpy(buffer, this->buffer_, this->unread_);
allocator.deallocate(this->buffer_, this->size_);
this->buffer_ = buffer;
this->size_ = size;
return true;
}
size_t DownloadBuffer::resize(size_t size) {
if (this->size_ >= size) {
// Avoid useless reallocations; if the buffer is big enough, don't reallocate.
@@ -1,5 +1,6 @@
#pragma once
#include "esphome/components/runtime_image/image_decoder.h"
#include "esphome/core/helpers.h"
#include <cstddef>
#include <cstdint>
@@ -35,6 +36,8 @@ class DownloadBuffer {
void reset() { this->unread_ = 0; }
size_t resize(size_t size);
/// Make the buffer at least size bytes, keeping the unread data. Returns false if the allocation failed.
bool grow(size_t size);
protected:
uint8_t *buffer_;
@@ -43,4 +46,35 @@ class DownloadBuffer {
size_t unread_;
};
/**
* @brief Lets a decoder read a download as it arrives, from the download buffer.
*
* The decoder reads at its own position in the buffer. Bytes it has read stay there until compact() drops
* them, and compact() keeps everything while set_keep_read(true) is in effect, so the decoder can seek back
* while it reads the headers.
*/
class DownloadSource : public runtime_image::DecodeSource {
public:
explicit DownloadSource(DownloadBuffer &buffer) : buffer_(buffer) {}
/// Start again at the beginning of a new download, keeping what is read
void reset() {
this->start_ = 0;
this->pos_ = 0;
this->keep_read_ = true;
}
void set_keep_read(bool keep) { this->keep_read_ = keep; }
/// Drop the bytes the decoder has read, to make room for more of the download
void compact();
int32_t read(uint8_t *buf, uint32_t len) override;
bool seek(uint32_t position) override;
protected:
DownloadBuffer &buffer_;
size_t start_{0}; // Position in the download of the first byte in the buffer
size_t pos_{0}; // Position in the download the decoder reads next
bool keep_read_{true};
};
} // namespace esphome::online_image
+12
View File
@@ -22,6 +22,8 @@ DEPENDENCIES = ["http_request"]
CODEOWNERS = ["@guillempages", "@clydebarrow"]
CONF_ON_DOWNLOAD_FINISHED = "on_download_finished"
CONF_ON_DOWNLOAD_STARTED = "on_download_started"
CONF_ON_DOWNLOAD_PROGRESS = "on_download_progress"
CONF_UPDATE = "update"
online_image_ns = cg.esphome_ns.namespace("online_image")
@@ -43,7 +45,9 @@ ONLINE_IMAGE_SCHEMA = (
cv.Optional(CONF_REQUEST_HEADERS): cv.All(
cv.Schema({cv.string: cv.templatable(cv.string)})
),
cv.Optional(CONF_ON_DOWNLOAD_STARTED): automation.validate_automation({}),
cv.Optional(CONF_ON_DOWNLOAD_FINISHED): automation.validate_automation({}),
cv.Optional(CONF_ON_DOWNLOAD_PROGRESS): automation.validate_automation({}),
cv.Optional(CONF_ON_ERROR): automation.validate_automation({}),
}
)
@@ -96,9 +100,17 @@ automation.register_apply_action(
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(
CONF_ON_DOWNLOAD_STARTED, "add_on_download_started_callback"
),
automation.CallbackAutomation(
CONF_ON_DOWNLOAD_FINISHED, "add_on_finished_callback", [(bool, "cached")]
),
automation.CallbackAutomation(
CONF_ON_DOWNLOAD_PROGRESS,
"add_on_progress_callback",
[(float, "progress"), (cg.size_t, "bytes_read")],
),
automation.CallbackAutomation(CONF_ON_ERROR, "add_on_error_callback"),
)
@@ -4,6 +4,7 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <algorithm>
#include <cmath>
#include <cstdio>
ESPHOME_LOG_TAG(TAG, "online_image");
@@ -15,6 +16,11 @@ static const char *const IF_MODIFIED_SINCE_HEADER_NAME = "if-modified-since";
namespace esphome::online_image {
// Time one loop pass may spend decoding a JPEG as it downloads
static constexpr uint32_t STREAM_PASS_BUDGET_MS = 20;
// Shortest time between two on_download_progress calls
static constexpr uint32_t PROGRESS_INTERVAL_MS = 100;
OnlineImage::OnlineImage(const std::string &url, int width, int height, runtime_image::ImageFormat format,
image::ImageType type, image::Transparency transparency, image::Image *placeholder,
uint32_t buffer_size, bool is_big_endian)
@@ -140,8 +146,12 @@ void OnlineImage::update() {
return;
}
// JPEG requires the complete image in the download buffer before decoding
if (format == runtime_image::JPEG && total_size > this->download_buffer_.size() &&
// A JPEG of known size is decoded as it arrives, so the download buffer only has to hold its headers.
// Otherwise the decoder needs the complete image in the buffer.
this->streaming_ = total_size > 0 && total_size <= INT32_MAX && this->decoder_supports_source();
this->stream_started_ = false;
this->source_.reset();
if (!this->streaming_ && format == runtime_image::JPEG && total_size > this->download_buffer_.size() &&
this->download_buffer_.resize(total_size) == 0) {
// resize() logged the failed allocation; the next update() allocates the configured buffer again
this->end_connection_();
@@ -152,6 +162,8 @@ void OnlineImage::update() {
ESP_LOGI(TAG, "Downloading image (Size: %zu)", total_size);
this->start_time_ = millis();
this->last_data_time_ = this->start_time_;
this->download_started_callback_.call();
this->report_progress_(true);
this->enable_loop();
}
@@ -169,6 +181,11 @@ void OnlineImage::loop() {
return;
}
if (this->streaming_) {
this->loop_stream_();
return;
}
// Check if download is complete — use decoder's format-specific completion check
// to handle both known content-length and chunked transfer encoding
if (this->is_decode_finished() || (this->downloader_->content_length > 0 &&
@@ -188,6 +205,7 @@ void OnlineImage::loop() {
if (len > 0) {
this->last_data_time_ = App.get_loop_component_start_time();
this->download_buffer_.write(len);
this->report_progress_(false);
// Feed data to decoder
auto consumed = this->feed_data(this->download_buffer_.data(), this->download_buffer_.unread());
@@ -255,6 +273,104 @@ void OnlineImage::loop() {
}
}
void OnlineImage::loop_stream_() {
const uint32_t now = App.get_loop_component_start_time();
const size_t total = this->downloader_->content_length;
if (this->stream_started_)
this->source_.compact();
if (this->downloader_->get_bytes_read() < total) {
const size_t available = std::min(this->download_buffer_.free_capacity(), this->download_buffer_initial_size_);
if (available == 0) {
// Waiting for the decoder, not the network
this->last_data_time_ = now;
} else {
const int len = this->downloader_->read(this->download_buffer_.append(), available);
if (len > 0) {
this->last_data_time_ = now;
this->download_buffer_.write(len);
this->report_progress_(false);
} else if (len < 0) {
ESP_LOGE(TAG, "Error downloading image: %d", len);
this->fail_download_();
return;
} else if (now - this->last_data_time_ >= this->parent_->get_timeout()) {
ESP_LOGE(TAG, "No data received for %" PRIu32 " ms", this->parent_->get_timeout());
this->fail_download_();
return;
}
}
}
const bool arrived = this->downloader_->get_bytes_read() >= total;
if (!this->stream_started_) {
// The decoder reads the headers when it opens the image, so wait until they are in the buffer
if (!arrived && this->download_buffer_.free_capacity() != 0)
return;
const int err = this->begin_source(&this->source_);
if (err != 0) {
// Headers cut short by a full buffer fail to parse, or make the decoder seek past the buffer. Such an image
// still decodes once it is all in memory, as it did before JPEG images were decoded as they download.
if (!arrived && (err == runtime_image::DECODE_ERROR_INVALID_TYPE || err == runtime_image::DECODE_ERROR_READ)) {
ESP_LOGW(TAG,
"The image headers do not fit the %zu byte download buffer, so the whole image is downloaded "
"before it is decoded; increase buffer_size to decode it as it downloads",
this->download_buffer_.size());
// grow() logs a failed allocation
if (this->download_buffer_.grow(total)) {
this->streaming_ = false;
} else {
this->fail_download_();
}
return;
}
ESP_LOGE(TAG, "Error decoding image: %s", runtime_image::decode_error_to_string(err));
this->fail_download_();
return;
}
this->source_.set_keep_read(false);
this->stream_started_ = true;
// Opening the image and allocating its buffer is enough for one pass
return;
}
do {
const int result = this->decode_step();
if (result == runtime_image::DECODE_STEP_MORE)
continue;
if (result == runtime_image::DECODE_STEP_DONE) {
this->finish_download_();
} else if (result == runtime_image::DECODE_STEP_NEED_DATA) {
if (!arrived)
return;
// JPEGDEC only waits for bytes short of the size it was given, so this guards against a hang
ESP_LOGE(TAG, "Download ended before the image was complete");
this->fail_download_();
} else {
ESP_LOGE(TAG, "Error decoding image: %s", runtime_image::decode_error_to_string(result));
this->fail_download_();
}
return;
} while (millis() - now < STREAM_PASS_BUDGET_MS);
}
void OnlineImage::report_progress_(bool force) {
const uint32_t now = App.get_loop_component_start_time();
const size_t bytes = this->downloader_->get_bytes_read();
const size_t total = this->downloader_->content_length;
// The last bytes are always reported, so a progress bar reaches 100%
if (!force && now - this->last_progress_time_ < PROGRESS_INTERVAL_MS && (total == 0 || bytes < total))
return;
this->last_progress_time_ = now;
const float progress = total > 0 ? std::min(100.0f, bytes * 100.0f / total) : NAN;
this->download_progress_callback_.call(progress, bytes);
}
void OnlineImage::fail_download_() {
this->end_connection_();
this->download_error_callback_.call();
}
void OnlineImage::finish_download_() {
// Must be read before end_decode() ends the decoding session
bool decoder_finished = this->is_decode_finished();
@@ -288,6 +404,8 @@ void OnlineImage::end_connection_() {
this->downloader_ = nullptr;
}
this->download_buffer_.reset();
this->streaming_ = false;
this->stream_started_ = false;
this->disable_loop();
}
@@ -70,9 +70,17 @@ class OnlineImage final : public PollingComponent,
*/
void release();
/// Called when the server has accepted the request and the image starts to download
template<typename F> void add_on_download_started_callback(F &&callback) {
this->download_started_callback_.add(std::forward<F>(callback));
}
template<typename F> void add_on_finished_callback(F &&callback) {
this->download_finished_callback_.add(std::forward<F>(callback));
}
/// Called with the percentage downloaded (NAN when the server did not send the size) and the bytes read so far
template<typename F> void add_on_progress_callback(F &&callback) {
this->download_progress_callback_.add(std::forward<F>(callback));
}
template<typename F> void add_on_error_callback(F &&callback) {
this->download_error_callback_.add(std::forward<F>(callback));
}
@@ -81,12 +89,20 @@ class OnlineImage final : public PollingComponent,
bool validate_url_(const std::string &url);
void end_connection_();
void finish_download_();
/// loop() for a JPEG that is decoded as it downloads
void loop_stream_();
void fail_download_();
/// Reports download progress, at most every PROGRESS_INTERVAL_MS unless force is set
void report_progress_(bool force);
CallbackManager<void(bool)> download_finished_callback_{};
CallbackManager<void()> download_error_callback_{};
LazyCallbackManager<void()> download_started_callback_{};
LazyCallbackManager<void(float, size_t)> download_progress_callback_{};
std::shared_ptr<http_request::HttpContainer> downloader_{nullptr};
DownloadBuffer download_buffer_;
DownloadSource source_{download_buffer_};
/**
* This is the *initial* size of the download buffer, not the current size.
* The download buffer can be resized at runtime; the download_buffer_initial_size_
@@ -109,6 +125,9 @@ class OnlineImage final : public PollingComponent,
uint32_t start_time_{0};
uint32_t last_data_time_{0};
uint32_t last_progress_time_{0};
bool streaming_{false}; // The decoder reads the download itself, as it arrives
bool stream_started_{false}; // The decoder has read the headers
};
} // namespace esphome::online_image
+1 -1
View File
@@ -27,7 +27,7 @@ class PCA6416AComponent final : public Component,
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
protected:
static void IRAM_ATTR gpio_intr(PCA6416AComponent *arg);
static void gpio_intr(PCA6416AComponent *arg);
// Virtual methods from CachedGpioExpander
bool digital_read_hw(uint8_t pin) override;
bool digital_read_cache(uint8_t pin) override;
+1 -1
View File
@@ -29,7 +29,7 @@ class PCA9505Component final : public Component,
void dump_config() override;
protected:
static void IRAM_ATTR gpio_intr(PCA9505Component *arg);
static void gpio_intr(PCA9505Component *arg);
bool digital_read_hw(uint8_t pin) override;
bool digital_read_cache(uint8_t pin) override;
+1 -1
View File
@@ -38,7 +38,7 @@ class PCA9554Component final : public Component,
#endif
protected:
static void IRAM_ATTR gpio_intr(PCA9554Component *arg);
static void gpio_intr(PCA9554Component *arg);
bool read_inputs_();
bool write_register_(uint8_t reg, uint16_t value) { return this->write_address_(reg * this->reg_width_, value); }
+1 -1
View File
@@ -33,7 +33,7 @@ class PCF8574Component final : public Component,
void dump_config() override;
protected:
static void IRAM_ATTR gpio_intr(PCF8574Component *arg);
static void gpio_intr(PCF8574Component *arg);
bool digital_read_hw(uint8_t pin) override;
bool digital_read_cache(uint8_t pin) override;
@@ -25,7 +25,7 @@ class PI4IOE5V6408Component final : public Component,
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
protected:
static void IRAM_ATTR gpio_intr(PI4IOE5V6408Component *arg);
static void gpio_intr(PI4IOE5V6408Component *arg);
bool digital_read_hw(uint8_t pin) override;
bool digital_read_cache(uint8_t pin) override;
+2
View File
@@ -656,6 +656,8 @@ void Pipsolar::handle_qpiws_(const char *message) {
this->publish_binary_sensor_(enabled, this->warning_battery_equalization_);
value_warnings_present |= enabled.value_or(false);
break;
default:
break;
}
}
+1 -1
View File
@@ -44,7 +44,7 @@ class QMC5883LComponent final : public PollingComponent, public i2c::I2CDevice {
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
protected:
static void IRAM_ATTR gpio_intr(QMC5883LComponent *arg);
static void gpio_intr(QMC5883LComponent *arg);
void read_sensor_();
QMC5883LDatarate datarate_{QMC5883L_DATARATE_10_HZ};
+11 -1
View File
@@ -92,7 +92,17 @@ class JPEGFormat(Format):
def actions(self) -> None:
cg.add_define("USE_RUNTIME_IMAGE_JPEG")
cg.add_library("JPEGDEC", "1.8.4", "https://github.com/bitbank2/JPEGDEC#1.8.4")
# 1.8.4 plus decodeBegin()/decodeRows(), so an image can be decoded a few rows at a time and
# can wait for input that has not arrived yet, and fixes for: input that arrives in short
# reads, garbled RGB8888 output of grayscale images, and two ARM NEON faults seen on arm64 hosts
# (swapped red and blue in the 4:2:0 RGB8888 path, writes past the row on clipped MCUs). It also
# uses the ESP32-S3 SIMD colour conversion in ESP-IDF builds, not only Arduino ones, and is built
# with -O2.
cg.add_library(
"JPEGDEC",
"1.8.4",
"https://github.com/esphome-libs/JPEGDEC#1.8.4-esphome.3",
)
if CORE.is_host:
# JPEGDEC's host detection checks __MACH__/__LINUX__, but gcc only
# predefines the lowercase __linux__; without this a Linux host
@@ -2,7 +2,6 @@
#include "runtime_image.h"
#include "esphome/core/log.h"
#include <algorithm>
#include <cmath>
namespace esphome::runtime_image {
@@ -11,19 +10,48 @@ ESPHOME_LOG_TAG(TAG, "image_decoder");
bool ImageDecoder::set_size(int width, int height) {
bool success = this->image_->resize(width, height) > 0;
this->size_valid_ = success;
this->x_scale_ = static_cast<double>(this->image_->get_buffer_width()) / width;
this->y_scale_ = static_cast<double>(this->image_->get_buffer_height()) / height;
this->draw_width_ = width;
this->draw_height_ = height;
return success;
}
int ImageDecoder::scaled_x_(int x) const {
if (this->draw_width_ <= 0)
return 0;
const uint32_t buffer_width = this->image_->get_buffer_width();
return static_cast<int>((static_cast<uint32_t>(x) * buffer_width + this->draw_width_ - 1) / this->draw_width_);
}
int ImageDecoder::scaled_y_(int y) const {
if (this->draw_height_ <= 0)
return 0;
const uint32_t buffer_height = this->image_->get_buffer_height();
return static_cast<int>((static_cast<uint32_t>(y) * buffer_height + this->draw_height_ - 1) / this->draw_height_);
}
void ImageDecoder::draw(int x, int y, int w, int h, const Color &color) {
if (!this->size_valid_) {
return;
}
auto width = std::min(this->image_->get_buffer_width(), static_cast<int>(std::ceil((x + w) * this->x_scale_)));
auto height = std::min(this->image_->get_buffer_height(), static_cast<int>(std::ceil((y + h) * this->y_scale_)));
for (int i = x * this->x_scale_; i < width; i++) {
for (int j = y * this->y_scale_; j < height; j++) {
// A source pixel paints the target pixels from ceil(start * scale) up to ceil(end * scale). The spans of neighbouring
// pixels meet without overlapping, so when an image is shrunk each target pixel is painted once, by the first
// source pixel that reaches it, rather than by every pixel that lands on it. Integer maths: the S3 has no double FPU.
const int buffer_width = this->image_->get_buffer_width();
const int buffer_height = this->image_->get_buffer_height();
int x1 = x;
int y1 = y;
int x2 = x + w;
int y2 = y + h;
if (buffer_width != this->draw_width_ || buffer_height != this->draw_height_) {
x1 = this->scaled_x_(x);
y1 = this->scaled_y_(y);
x2 = this->scaled_x_(x + w);
y2 = this->scaled_y_(y + h);
}
x2 = std::min(buffer_width, x2);
y2 = std::min(buffer_height, y2);
for (int i = x1; i < x2; i++) {
for (int j = y1; j < y2; j++) {
this->image_->draw_pixel(i, j, color);
}
}
@@ -1,4 +1,6 @@
#pragma once
#include <cstdint>
#include "esphome/core/color.h"
#include "image_format.h"
@@ -9,6 +11,14 @@ enum DecodeError : int {
DECODE_ERROR_UNSUPPORTED_FORMAT = -2,
DECODE_ERROR_OUT_OF_MEMORY = -3,
DECODE_ERROR_INTERNAL_DECODER_ERROR = -4,
DECODE_ERROR_READ = -5,
};
/// Results of ImageDecoder::decode_step() other than a DecodeError
enum DecodeStep : int {
DECODE_STEP_DONE = 0,
DECODE_STEP_MORE = 1,
DECODE_STEP_NEED_DATA = 2, // The source has nothing more yet; call again once more has arrived
};
constexpr const char *decode_error_to_string(int error) {
@@ -21,6 +31,8 @@ constexpr const char *decode_error_to_string(int error) {
return "Out of memory";
case DECODE_ERROR_INTERNAL_DECODER_ERROR:
return "Internal decoder error";
case DECODE_ERROR_READ:
return "Read error";
default:
return "Unknown error";
}
@@ -28,6 +40,19 @@ constexpr const char *decode_error_to_string(int error) {
class RuntimeImage;
/// Input that a decoder reads itself as it goes, so the whole image never has to be in memory at once
class DecodeSource {
public:
virtual ~DecodeSource() = default;
/**
* Read up to len bytes at the current position. Returns the number read, which is less than len, even 0, when
* no more has arrived yet or at the end of the input, or negative on error.
*/
virtual int32_t read(uint8_t *buf, uint32_t len) = 0;
/// Move to a position counted from the start. Returns false on error.
virtual bool seek(uint32_t position) = 0;
};
/**
* @brief Class to abstract decoding different image formats.
*/
@@ -58,8 +83,8 @@ class ImageDecoder {
this->expected_size_ = 0;
this->decoded_bytes_ = 0;
this->size_valid_ = true;
this->x_scale_ = 1.0;
this->y_scale_ = 1.0;
this->draw_width_ = 0;
this->draw_height_ = 0;
}
/**
@@ -88,6 +113,28 @@ class ImageDecoder {
*/
virtual int decode(uint8_t *buffer, size_t size) = 0;
/// Whether this decoder can read a DecodeSource itself, a step at a time, instead of being fed data.
virtual bool supports_source() const { return false; }
/**
* @brief Start decoding from a source the decoder reads itself. Call after prepare(), in place of decode().
*
* The image headers (everything before the image data) must be readable from the source when this is called;
* only decode_step() can wait for more to arrive. The source must hold expected_size bytes in the end, and stay
* valid until the session ends.
* @return int 0 on success, or a {@see DecodeError} value.
*/
virtual int begin_source(DecodeSource *source) { return DECODE_ERROR_UNSUPPORTED_FORMAT; }
/**
* @brief Decode a little more of an image started with begin_source(), keeping each call short.
*
* @return int DECODE_STEP_MORE while there is more to do, DECODE_STEP_NEED_DATA when the source has not
* delivered enough to go on, DECODE_STEP_DONE when the image is complete, or a {@see DecodeError}
* value.
*/
virtual int decode_step() { return DECODE_ERROR_UNSUPPORTED_FORMAT; }
/**
* @brief Request the image to be resized once the actual dimensions are known.
* Called by the callback functions, to be able to access the parent Image class.
@@ -112,6 +159,9 @@ class ImageDecoder {
*/
void draw(int x, int y, int w, int h, const Color &color);
/// Bytes of the image the decoder has used so far
size_t get_decoded_bytes() const { return this->decoded_bytes_; }
/**
* @brief Check if the decoder has finished processing.
*
@@ -134,8 +184,13 @@ class ImageDecoder {
uint8_t *image_buffer_() const;
bool image_is_big_endian_() const;
double x_scale_ = 1.0;
double y_scale_ = 1.0;
/// The first buffer column, or row, that source column x, or row y, paints: ceil(x * buffer size / draw size)
int scaled_x_(int x) const;
int scaled_y_(int y) const;
// Size of the image as the decoder draws it, which draw() spreads over the buffer
int draw_width_ = 0;
int draw_height_ = 0;
RuntimeImage *image_;
size_t expected_size_ = 0; // Expected data size (0 if unknown)
size_t decoded_bytes_ = 0; // Bytes processed so far
+136 -24
View File
@@ -31,8 +31,8 @@ bool JpegDecoder::prepare_rgb565_scaling_(int width, int height) {
const uint32_t target_width = this->image_->get_buffer_width();
const uint32_t target_height = this->image_->get_buffer_height();
// Boundary n is the first target pixel source pixel n covers, rounded up. When shrinking, the source
// pixel whose span lands on a target pixel paints it, so an 8 to 4 scale keeps columns 0, 2, 4 and 6;
// the generic draw() path rounds down and keeps the odd ones instead
// pixel whose span lands on a target pixel paints it, so an 8 to 4 scale keeps columns 0, 2, 4 and 6,
// as the generic draw() path does
for (uint32_t x = 0; x <= static_cast<uint32_t>(width); x++) {
this->x_boundaries_.push_back(static_cast<uint16_t>((x * target_width + width - 1) / width));
}
@@ -135,13 +135,24 @@ int HOT JpegDecoder::decode(uint8_t *buffer, size_t size) {
ESP_LOGE(TAG, "Could not open image for decoding: %d", this->jpeg_.getLastError());
return DECODE_ERROR_INVALID_TYPE;
}
const int err = this->start_();
if (err != 0)
return err;
if (!this->jpeg_.decode(0, 0, this->scale_option_()))
return this->fail_();
this->decoded_bytes_ = size;
this->jpeg_.close();
return size;
}
int JpegDecoder::start_() {
auto jpeg_type = this->jpeg_.getJPEGType();
if (jpeg_type == JPEG_MODE_INVALID) {
ESP_LOGE(TAG, "Unsupported JPEG image");
return DECODE_ERROR_INVALID_TYPE;
} else if (jpeg_type == JPEG_MODE_PROGRESSIVE) {
ESP_LOGE(TAG, "Progressive JPEG images not supported");
return DECODE_ERROR_INVALID_TYPE;
return DECODE_ERROR_UNSUPPORTED_FORMAT;
}
ESP_LOGD(TAG, "Image size: %d x %d, bpp: %d", this->jpeg_.getWidth(), this->jpeg_.getHeight(), this->jpeg_.getBpp());
@@ -154,32 +165,133 @@ int HOT JpegDecoder::decode(uint8_t *buffer, size_t size) {
} else {
this->jpeg_.setPixelType(RGB8888);
}
if (!this->set_size(this->jpeg_.getWidth(), this->jpeg_.getHeight())) {
const int width = this->jpeg_.getWidth();
const int height = this->jpeg_.getHeight();
if (!this->set_size(width, height)) {
return DECODE_ERROR_OUT_OF_MEMORY;
}
if (this->direct_rgb565_ && !this->prepare_rgb565_scaling_(this->jpeg_.getWidth(), this->jpeg_.getHeight())) {
// JPEGDEC can decode at 1/2, 1/4 or 1/8 size for much less work. Use the smallest size that still has at least as
// many pixels as the buffer, so a large image shown small is not decoded in full only to be thrown away.
const int buffer_width = this->image_->get_buffer_width();
const int buffer_height = this->image_->get_buffer_height();
this->scale_ = 1;
while (this->scale_ < JPEG_SCALE_EIGHTH && width / (this->scale_ * 2) >= buffer_width &&
height / (this->scale_ * 2) >= buffer_height) {
this->scale_ *= 2;
}
const int decoded_width = (width + this->scale_ - 1) / this->scale_;
const int decoded_height = (height + this->scale_ - 1) / this->scale_;
this->draw_width_ = decoded_width;
this->draw_height_ = decoded_height;
if (this->scale_ != 1) {
ESP_LOGD(TAG, "Decoding at 1/%d size, %d x %d", this->scale_, decoded_width, decoded_height);
}
if (this->direct_rgb565_ && !this->prepare_rgb565_scaling_(decoded_width, decoded_height)) {
return DECODE_ERROR_OUT_OF_MEMORY;
}
if (!this->jpeg_.decode(0, 0, 0)) {
auto error = this->jpeg_.getLastError();
ESP_LOGE(TAG, "Error while decoding: %d", error);
this->jpeg_.close();
switch (error) {
case JPEG_ERROR_MEMORY:
return DECODE_ERROR_OUT_OF_MEMORY;
case JPEG_UNSUPPORTED_FEATURE:
return DECODE_ERROR_UNSUPPORTED_FORMAT;
case JPEG_INVALID_FILE:
case JPEG_INVALID_PARAMETER:
return DECODE_ERROR_INVALID_TYPE;
case JPEG_DECODE_ERROR:
default:
return DECODE_ERROR_INTERNAL_DECODER_ERROR;
}
}
this->decoded_bytes_ = size;
return 0;
}
int JpegDecoder::fail_() {
const bool read_failed = this->source_ != nullptr && this->source_failed_;
const int error = this->jpeg_.getLastError();
this->jpeg_.close();
return size;
this->source_ = nullptr;
if (read_failed) {
ESP_LOGE(TAG, "Reading the image failed");
return DECODE_ERROR_READ;
}
ESP_LOGE(TAG, "Error while decoding: %d", error);
switch (error) {
case JPEG_ERROR_MEMORY:
return DECODE_ERROR_OUT_OF_MEMORY;
case JPEG_UNSUPPORTED_FEATURE:
return DECODE_ERROR_UNSUPPORTED_FORMAT;
case JPEG_INVALID_FILE:
case JPEG_INVALID_PARAMETER:
return DECODE_ERROR_INVALID_TYPE;
case JPEG_DECODE_ERROR:
default:
return DECODE_ERROR_INTERNAL_DECODER_ERROR;
}
}
void JpegDecoder::reset() {
// A decode from a source that was given up part way still has the image open
if (this->source_ != nullptr) {
this->jpeg_.close();
this->source_ = nullptr;
}
ImageDecoder::reset();
}
int32_t JpegDecoder::source_read(JPEGFILE *file, uint8_t *buf, int32_t len) {
auto *decoder = static_cast<JpegDecoder *>(file->fHandle);
const int32_t n = decoder->source_->read(buf, len);
if (n < 0) {
// JPEGDEC has no way to report a failed read, so remember it and give it no data
decoder->source_failed_ = true;
return 0;
}
file->iPos += n;
return n;
}
int32_t JpegDecoder::source_seek(JPEGFILE *file, int32_t position) {
auto *decoder = static_cast<JpegDecoder *>(file->fHandle);
if (position < 0 || !decoder->source_->seek(position)) {
decoder->source_failed_ = true;
return file->iPos;
}
file->iPos = position;
return position;
}
int JpegDecoder::begin_source(DecodeSource *source) {
if (this->expected_size_ == 0 || this->expected_size_ > INT32_MAX) {
ESP_LOGE(TAG, "Image size %zu cannot be read from a source", this->expected_size_);
return DECODE_ERROR_INVALID_TYPE;
}
if (!source->seek(0)) {
ESP_LOGE(TAG, "Could not read the image");
return DECODE_ERROR_READ;
}
this->source_ = source;
this->source_failed_ = false;
if (!this->jpeg_.open(this, this->expected_size_, source_close, source_read, source_seek, draw_callback)) {
// The caller may recover, for example by reading the whole image first, so it reports the failure
ESP_LOGD(TAG, "Could not open image for decoding: %d", this->jpeg_.getLastError());
this->source_ = nullptr;
return this->source_failed_ ? DECODE_ERROR_READ : DECODE_ERROR_INVALID_TYPE;
}
const int err = this->start_();
if (err != 0) {
this->jpeg_.close();
this->source_ = nullptr;
return err;
}
if (!this->jpeg_.decodeBegin(0, 0, this->scale_option_()))
return this->fail_();
return 0;
}
int HOT JpegDecoder::decode_step() {
if (this->source_ == nullptr)
return DECODE_ERROR_INVALID_TYPE;
const int result = this->jpeg_.decodeRows(1);
// JPEGDEC may decode on with the missing data, so check the read before its result
if (this->source_failed_)
return this->fail_();
if (result == JPEG_DECODE_MORE)
return DECODE_STEP_MORE;
if (result == JPEG_DECODE_NEED_DATA)
return DECODE_STEP_NEED_DATA;
if (result == 0)
return this->fail_();
this->decoded_bytes_ = this->expected_size_;
this->jpeg_.close();
this->source_ = nullptr;
return DECODE_STEP_DONE;
}
} // namespace esphome::runtime_image
@@ -22,19 +22,41 @@ class JpegDecoder : public ImageDecoder {
~JpegDecoder() override {}
int HOT decode(uint8_t *buffer, size_t size) override;
void reset() override;
/// The source is read as the decode needs it, through JPEGDEC's own 2 KB buffer
bool supports_source() const override { return true; }
int begin_source(DecodeSource *source) override;
/// Decodes one row of MCUs (8 or 16 pixel rows), or waits part way through one for more of the source
int HOT decode_step() override;
bool uses_direct_rgb565() const { return this->direct_rgb565_; }
/// JPEGDEC decodes the image at 1/get_scale() of its size: 1, 2, 4 or 8
int get_scale() const { return this->scale_; }
void draw_rgb565(int x, int y, int w, int h, const uint8_t *pixels);
protected:
bool prepare_rgb565_scaling_(int width, int height);
/// Set up an opened image for decoding. Returns 0, or a DecodeError.
int start_();
/// The JPEGDEC option for scale_: JPEG_SCALE_HALF, _QUARTER and _EIGHTH are the divisors themselves
int scale_option_() const { return this->scale_ == 1 ? 0 : this->scale_; }
/// The DecodeError for a failed read of the source or JPEGDEC's last error, closing the image
int fail_();
static int32_t source_read(JPEGFILE *file, uint8_t *buf, int32_t len);
static int32_t source_seek(JPEGFILE *file, int32_t position);
static void source_close(void *handle) {}
JPEGDEC jpeg_{};
FixedVector<uint16_t> x_boundaries_{};
FixedVector<uint16_t> y_boundaries_{};
int source_width_{0};
int source_height_{0};
int scale_{1}; // JPEGDEC decodes at 1/scale_ of the image size
DecodeSource *source_{nullptr}; // Set while a begin_source() decode is in progress
bool direct_rgb565_{false};
bool source_failed_{false};
};
} // namespace esphome::runtime_image
@@ -116,7 +116,9 @@ void RuntimeImage::draw_pixel(int x, int y, const Color &color) {
}
case image::IMAGE_TYPE_GRAYSCALE: {
uint32_t pos = this->get_position_(x, y);
auto gray = static_cast<uint8_t>(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b);
// 0.2125, 0.7154 and 0.0721 in 1/256ths, summing to 256 so white stays 255. Integer maths: the S3 has no
// double FPU
auto gray = static_cast<uint8_t>((54 * color.r + 183 * color.g + 19 * color.b) >> 8);
if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) {
if (gray == 1) {
gray = 0;
@@ -239,6 +241,25 @@ int RuntimeImage::feed_data(uint8_t *data, size_t len) {
return consumed;
}
int RuntimeImage::begin_source(DecodeSource *source) {
if (!this->decoder_supports_source()) {
ESP_LOGE(TAG, "Decoder cannot read a source");
return DECODE_ERROR_UNSUPPORTED_FORMAT;
}
return this->decoder_->begin_source(source);
}
int RuntimeImage::decode_step() {
if (!this->is_decoding()) {
ESP_LOGE(TAG, "No decoder initialized");
return DECODE_ERROR_INVALID_TYPE;
}
const int result = this->decoder_->decode_step();
if (result == DECODE_STEP_DONE)
this->decoded_bytes_ = this->decoder_->get_decoded_bytes();
return result;
}
bool RuntimeImage::end_decode() {
if (!this->is_decoding()) {
return false;
@@ -76,6 +76,22 @@ class RuntimeImage : public image::Image {
*/
int feed_data(uint8_t *data, size_t len);
/// Whether the active decoder can read a DecodeSource itself (see begin_source()).
bool decoder_supports_source() const { return this->is_decoding() && this->decoder_->supports_source(); }
/**
* @brief Decode from a source the decoder reads itself, in place of feed_data().
*
* Call after begin_decode() when decoder_supports_source() is true, once the image headers can be read from the
* source, then call decode_step() until it
* returns DECODE_STEP_DONE, and finish with end_decode().
* @return 0 on success, or a DecodeError value.
*/
int begin_source(DecodeSource *source);
/// Decode a little more from the source; see ImageDecoder::decode_step().
int decode_step();
/**
* @brief Complete the decoding process.
*
+21
View File
@@ -39,13 +39,16 @@ from esphome.const import (
CONF_REFERENCE_TEMPERATURE,
CONF_SEND_EVERY,
CONF_SEND_FIRST_AT,
CONF_SENSOR_ID,
CONF_STATE_CLASS,
CONF_TEMPERATURE,
CONF_TEMPERATURE_OFFSET,
CONF_TIMEOUT,
CONF_TO,
CONF_TRIGGER_ID,
CONF_TYPE_ID,
CONF_UNIT_OF_MEASUREMENT,
CONF_USE_FAHRENHEIT,
CONF_VALUE,
CONF_WEB_SERVER,
CONF_WINDOW_SIZE,
@@ -310,6 +313,7 @@ OrFilter = sensor_ns.class_("OrFilter", Filter)
CalibrateLinearFilter = sensor_ns.class_("CalibrateLinearFilter", Filter)
ToNTCResistanceFilter = sensor_ns.class_("ToNTCResistanceFilter", Filter)
ToNTCTemperatureFilter = sensor_ns.class_("ToNTCTemperatureFilter", Filter)
RHCorrectionFilter = sensor_ns.class_("RHCorrectionFilter", Filter)
CalibratePolynomialFilter = sensor_ns.class_("CalibratePolynomialFilter", Filter)
SensorInRangeCondition = sensor_ns.class_("SensorInRangeCondition", Filter)
ClampFilter = sensor_ns.class_("ClampFilter", Filter)
@@ -1281,6 +1285,23 @@ async def calibrate_ntc_temperature_filter_to_code(config, filter_id):
)
@FILTER_REGISTRY.register(
"rh_correction",
RHCorrectionFilter,
cv.Schema(
{
cv.Required(CONF_TEMPERATURE_OFFSET): cv.templatable(cv.float_),
cv.Required(CONF_SENSOR_ID): cv.use_id(Sensor),
cv.Optional(CONF_USE_FAHRENHEIT, default=False): cv.boolean,
}
),
)
async def rh_correction_filter_to_code(config: ConfigType, filter_id: ID) -> MockObj:
sensor = await cg.get_variable(config[CONF_SENSOR_ID])
offset = await cg.templatable(config[CONF_TEMPERATURE_OFFSET], [], cg.float_)
return cg.new_Pvariable(filter_id, offset, sensor, config[CONF_USE_FAHRENHEIT])
def _mean(xs):
return sum(xs) / len(xs)
+42
View File
@@ -493,6 +493,48 @@ optional<float> ToNTCTemperatureFilter::new_value(float value) {
return temp;
}
// RHCorrectionFilter
static constexpr float MAGNUS_B = 17.67f, MAGNUS_C = 243.5f;
static bool rh_correction_temp_in_band(float t) { return t >= -40.0f && t <= 50.0f; }
optional<float> RHCorrectionFilter::new_value(float value) {
if (!this->temperature_sensor_->has_state()) {
if (!this->warned_) {
ESP_LOGW(TAG, "RHCorrectionFilter(%p): no temperature yet, passing humidity through uncorrected", this);
this->warned_ = true;
}
return value;
}
float offset = this->temperature_offset_.value();
float tc = this->temperature_sensor_->get_state();
if (this->use_fahrenheit_) {
tc = fahrenheit_to_celsius(tc);
offset /= 1.8f;
}
const float tnc = tc - offset;
// A NaN temperature or offset is a faulted input: keep the last corrected value
if (std::isnan(tnc)) {
if (!this->warned_) {
ESP_LOGW(TAG, "RHCorrectionFilter(%p): temperature or offset is NaN, dropping humidity updates", this);
this->warned_ = true;
}
return {};
}
if (!rh_correction_temp_in_band(tc) || !rh_correction_temp_in_band(tnc)) {
if (!this->warned_) {
ESP_LOGW(TAG, "RHCorrectionFilter(%p): Temperature out of range -40..50 °C. Tc=%f Tnc=%f", this, tc, tnc);
this->warned_ = true;
}
return value;
}
this->warned_ = false;
const float rh =
clamp(value * std::exp(MAGNUS_B * MAGNUS_C * (tnc - tc) / ((tnc + MAGNUS_C) * (tc + MAGNUS_C))), 0.0f, 100.0f);
ESP_LOGVV(TAG, "RHCorrectionFilter(%p)::new_value(%f) -> %f", this, value, rh);
return rh;
}
// StreamingFilter (base class)
StreamingFilter::StreamingFilter(uint16_t window_size, uint16_t send_first_at)
: window_size_(window_size), send_first_at_(send_first_at) {}
+20
View File
@@ -647,6 +647,26 @@ class ToNTCTemperatureFilter : public Filter {
double c_;
};
/** RH Temperature correction filter
*
* Allows for Relative Humidity Correction when a temperature offset is applied to the temperature sensor.
* Uses the Magnus formula, rh = rh_nc * exp(b*(t_nc/(c+t_nc) - t/(c+t))) with b = 17.67, c = 243.5 °C;
* see https://en.wikipedia.org/wiki/Dew_point
*/
class RHCorrectionFilter : public Filter {
public:
RHCorrectionFilter(TemplatableFn<float> offset, const Sensor *temperature_sensor, bool use_fahrenheit)
: temperature_offset_(offset), temperature_sensor_(temperature_sensor), use_fahrenheit_(use_fahrenheit) {}
optional<float> new_value(float value) override;
protected:
TemplatableFn<float> temperature_offset_;
const Sensor *temperature_sensor_;
bool use_fahrenheit_;
bool warned_{false};
};
/** Base class for streaming filters (batch windows where window_size == send_every).
*
* When window_size equals send_every, we don't need a sliding window.
@@ -48,6 +48,8 @@ void SmlTextSensor::publish_val(const ObisInfo &obis_info) {
publish_state(reinterpret_cast<const char *>(obis_info.value.begin()), obis_info.value.size());
break;
}
default:
break;
}
}
@@ -13,6 +13,7 @@ from esphome.components import (
)
from esphome.components.const import (
CONF_ANNOUNCEMENT_VOLUME,
CONF_BITRATE,
CONF_VOLUME_INCREMENT,
CONF_VOLUME_INITIAL,
CONF_VOLUME_MAX,
@@ -107,6 +108,7 @@ PIPELINE_SCHEMA = cv.Schema(
cv.Optional(CONF_FORMAT, default="FLAC"): cv.enum(audio.AUDIO_FILE_TYPE_ENUM),
cv.Optional(CONF_SAMPLE_RATE): cv.int_range(min=1),
cv.Optional(CONF_NUM_CHANNELS): cv.int_range(1, 2),
cv.Optional(CONF_BITRATE): media_player.validate_mp3_bitrate,
}
)
@@ -3,6 +3,7 @@ import esphome.codegen as cg
from esphome.components import audio, media_player, media_source, speaker
from esphome.components.const import (
CONF_ANNOUNCEMENT_VOLUME,
CONF_BITRATE,
CONF_SOURCES,
CONF_VOLUME_INCREMENT,
CONF_VOLUME_INITIAL,
@@ -78,6 +79,7 @@ PIPELINE_SCHEMA = cv.Schema(
cv.Optional(CONF_FORMAT, default="FLAC"): cv.enum(audio.AUDIO_FILE_TYPE_ENUM),
cv.Optional(CONF_SAMPLE_RATE): cv.int_range(min=1),
cv.Optional(CONF_NUM_CHANNELS): cv.int_range(1, 2),
cv.Optional(CONF_BITRATE): media_player.validate_mp3_bitrate,
}
)
+1 -1
View File
@@ -106,7 +106,7 @@ class ST25R : public PollingComponent, public nfc::Nfcc {
void finish_scan_();
void finalize_scan_();
void remove_tag_();
static void IRAM_ATTR isr(ST25R *arg);
static void isr(ST25R *arg);
GPIOPin *reset_pin_{nullptr};
InternalGPIOPin *irq_pin_{nullptr};
+1 -1
View File
@@ -107,7 +107,7 @@ class SX126x final : public Component,
Trigger<std::vector<uint8_t>, float, float> *get_packet_trigger() { return &this->packet_trigger_; }
protected:
static void IRAM_ATTR gpio_intr(SX126x *arg);
static void gpio_intr(SX126x *arg);
void configure_fsk_ook_();
void configure_lora_();
void set_packet_params_(uint8_t payload_length);
+1 -1
View File
@@ -91,7 +91,7 @@ class SX127x final : public Component,
Trigger<std::vector<uint8_t>, float, float> *get_packet_trigger() { return &this->packet_trigger_; }
protected:
static void IRAM_ATTR gpio_intr(SX127x *arg);
static void gpio_intr(SX127x *arg);
void configure_fsk_ook_();
void configure_lora_();
void set_mode_(uint8_t modulation, uint8_t mode);
+1 -1
View File
@@ -58,7 +58,7 @@ class TCA8418Component : public key_provider::KeyProvider, public Component, pub
#endif
/// Wakes the loop so queued events are read. Runs in interrupt context.
static void IRAM_ATTR interrupt_handler(TCA8418Component *arg);
static void interrupt_handler(TCA8418Component *arg);
protected:
/// Write the matrix / individual-input pin configuration. Returns false on I2C failure.
+1 -1
View File
@@ -27,7 +27,7 @@ class TCA9555Component final : public Component,
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
protected:
static void IRAM_ATTR gpio_intr(TCA9555Component *arg);
static void gpio_intr(TCA9555Component *arg);
// Virtual methods from GpioExpander base class — names come from base
bool digital_read_hw(uint8_t pin) override; // NOLINT(readability-identifier-naming)
bool digital_read_cache(uint8_t pin) override; // NOLINT(readability-identifier-naming)
+2
View File
@@ -191,6 +191,8 @@ bool Tcl112Climate::on_receive(remote_base::RemoteReceiveData data) {
case TCL112_AUTO:
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
break;
default:
break;
}
}
auto temp = TCL112_TEMP_MAX - remote_state[7];
+4
View File
@@ -39,6 +39,8 @@ void TEM3200Component::setup() {
ESP_LOGE(TAG, "STALE data. Data has not been updated since last fetch");
this->status_set_warning();
break;
default:
break;
}
}
@@ -127,6 +129,8 @@ void TEM3200Component::update() {
ESP_LOGE(TAG, "Warning: STALE data. Data has not been updated since last fetch");
this->status_set_warning();
return;
default:
break;
}
float temperature = convert_temperature(raw_temperature);
+138
View File
@@ -0,0 +1,138 @@
"""VNC (RFB) client that shows a remote screen on a display."""
from esphome import automation
import esphome.codegen as cg
from esphome.components import display, network, socket
from esphome.components.const import (
BYTE_ORDER_LITTLE,
CONF_HOST,
CONF_RECONNECT_INTERVAL,
)
import esphome.config_validation as cv
from esphome.const import (
CONF_AUTO_CLEAR_ENABLED,
CONF_DISPLAY_ID,
CONF_ID,
CONF_ON_CONNECT,
CONF_ON_DISCONNECT,
CONF_PLATFORM,
CONF_PORT,
)
import esphome.final_validate as fv
from esphome.types import ConfigType
CODEOWNERS = ["@MindFreeze"]
DEPENDENCIES = ["display", "network"]
AUTO_LOAD = ["socket"]
DOMAIN = "vnc"
MULTI_CONF = True
CONF_MAX_UPDATE_DELAY = "max_update_delay"
CONF_UPDATE_DELAY = "update_delay"
vnc_ns = cg.esphome_ns.namespace("vnc")
VncClient = vnc_ns.class_("VncClient", cg.Component)
# Drivers that cannot take another component's pixels yet.
UNSUPPORTED_DISPLAYS = dict.fromkeys(
("mipi_dsi", "mipi_rgb", "mipi_spi"), "it is not supported by vnc yet"
) | {
"waveshare_epaper": "it draws every colour except pure black as ink; use epaper_spi"
}
def _register_writer(config: ConfigType) -> ConfigType:
display.register_external_writer(config[CONF_DISPLAY_ID], DOMAIN)
return config
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(VncClient),
cv.GenerateID(CONF_DISPLAY_ID): cv.use_id(display.Display),
cv.Required(CONF_HOST): socket.ipv4_host,
cv.Optional(CONF_PORT, default=5900): cv.port,
cv.Optional(CONF_RECONNECT_INTERVAL, default="5s"): cv.All(
cv.positive_time_period_milliseconds,
cv.Range(min=cv.TimePeriod(seconds=1)),
),
cv.Optional(
CONF_UPDATE_DELAY, default="200ms"
): cv.positive_time_period_milliseconds,
cv.Optional(
CONF_MAX_UPDATE_DELAY, default="1s"
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_ON_CONNECT): automation.validate_automation({}),
cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation({}),
}
).extend(cv.COMPONENT_SCHEMA),
_register_writer,
socket.consume_sockets(1, DOMAIN),
network.require_ipv4(DOMAIN),
)
def _final_validate(config: ConfigType) -> None:
full_config = fv.full_config.get()
disp_id = config[CONF_DISPLAY_ID]
disp_conf = fv.get_declaring_config(disp_id)
if (platform := disp_conf.get(CONF_PLATFORM)) in UNSUPPORTED_DISPLAYS:
raise cv.Invalid(
f"Display '{disp_id}' uses the {platform} platform: "
f"{UNSUPPORTED_DISPLAYS[platform]}",
path=[CONF_DISPLAY_ID],
)
if (
display.get_display_metadata(disp_id).has_writer
or disp_conf.get(CONF_AUTO_CLEAR_ENABLED) is True
):
raise cv.Invalid(
f"vnc draws the whole of display '{disp_id}'; remove lambda, pages, "
"auto_clear_enabled and show_test_card from it",
path=[CONF_DISPLAY_ID],
)
if len(writers := display.get_external_writers(disp_id)) > 1:
raise cv.Invalid(
f"Display '{disp_id}' is drawn by more than one component: {', '.join(writers)}",
path=[CONF_DISPLAY_ID],
)
lvgl_configs = full_config.get("lvgl") or []
if isinstance(lvgl_configs, dict):
lvgl_configs = [lvgl_configs]
for lvgl_config in lvgl_configs:
if any(d.id == disp_id.id for d in lvgl_config.get("displays", [])):
raise cv.Invalid(
f"Display '{disp_id}' is also used by LVGL", path=[CONF_DISPLAY_ID]
)
FINAL_VALIDATE_SCHEMA = _final_validate
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(CONF_ON_CONNECT, "add_on_connect_callback"),
automation.CallbackAutomation(CONF_ON_DISCONNECT, "add_on_disconnect_callback"),
)
async def to_code(config: ConfigType) -> None:
disp = await cg.get_variable(config[CONF_DISPLAY_ID])
# Ask the server for the display's own byte order, so block-copying drivers skip the per-pixel path
meta = display.get_display_metadata(config[CONF_DISPLAY_ID])
var = cg.new_Pvariable(config[CONF_ID], disp, meta.byte_order != BYTE_ORDER_LITTLE)
await cg.register_component(var, config)
socket.require_tcp_client_link()
cg.add(var.set_host(config[CONF_HOST]))
cg.add(var.set_port(config[CONF_PORT]))
cg.add(var.set_reconnect_interval(config[CONF_RECONNECT_INTERVAL]))
cg.add(var.set_update_delay(config[CONF_UPDATE_DELAY]))
cg.add(var.set_max_update_delay(config[CONF_MAX_UPDATE_DELAY]))
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
VNC_ID_SCHEMA = automation.maybe_simple_id(
{cv.GenerateID(): cv.use_id(VncClient)},
)
automation.register_apply_condition("vnc.is_connected", VNC_ID_SCHEMA, "is_connected()")
+702
View File
@@ -0,0 +1,702 @@
#include "vnc_client.h"
#include "esphome/components/network/util.h"
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include <algorithm>
#include <cinttypes>
#include <cstring>
namespace esphome::vnc {
ESPHOME_LOG_TAG(TAG, "vnc");
// Below the 50 ms blocking warning, and the same slice it8951 uses for its transfers.
static constexpr uint32_t LOOP_BUDGET_MS = 20;
static constexpr uint32_t HANDSHAKE_TIMEOUT_MS = 15000;
static constexpr uint32_t STABLE_SESSION_MS = 60000;
static constexpr uint32_t MAX_BACKOFF_MS = 60000;
static constexpr uint32_t CONFIG_RETRY_MS = 300000;
static constexpr uint32_t HEARTBEAT_MS = 60000;
static constexpr uint32_t MIN_REQUEST_SPACING_MS = 50;
// The longest the first refresh waits for a display with a loop to park once after boot.
static constexpr uint32_t DISPLAY_START_MS = 5000;
// Longer than any driver's own busy timeout: a panel that stays busy this long is stuck
static constexpr uint32_t DISPLAY_BUSY_TIMEOUT_MS = 120000;
static constexpr uint16_t MAX_CURSOR_SIDE = 1024;
static constexpr uint32_t MAX_TEXT_LEN = 63;
static constexpr uint8_t SECURITY_NONE = 1;
static constexpr uint8_t SECURITY_VNC_AUTH = 2;
static constexpr int32_t ENCODING_RAW = 0;
static constexpr int32_t ENCODING_RICH_CURSOR = -239;
static constexpr uint8_t MSG_FRAMEBUFFER_UPDATE = 0;
static constexpr uint8_t MSG_SET_COLOUR_MAP = 1;
static constexpr uint8_t MSG_BELL = 2;
static constexpr uint8_t MSG_SERVER_CUT_TEXT = 3;
static inline uint16_t read_be16(const uint8_t *p) { return encode_uint16(p[0], p[1]); }
static inline uint32_t read_be32(const uint8_t *p) { return encode_uint32(p[0], p[1], p[2], p[3]); }
static inline void write_be16(uint8_t *p, uint16_t v) {
p[0] = v >> 8;
p[1] = v;
}
static inline void write_be32(uint8_t *p, uint32_t v) {
p[0] = v >> 24;
p[1] = v >> 16;
p[2] = v >> 8;
p[3] = v;
}
static int parse_three_digits(const uint8_t *p) {
int value = 0;
for (int i = 0; i < 3; i++) {
if (p[i] < '0' || p[i] > '9') {
return -1;
}
value = value * 10 + (p[i] - '0');
}
return value;
}
void VncClient::setup() {
this->setup_ms_ = App.get_loop_component_start_time();
this->link_.begin(TAG);
}
void VncClient::dump_config() {
ESP_LOGCONFIG(TAG,
"VNC:\n"
" Server: %s:%u\n"
" Reconnect Interval: %" PRIu32 "ms\n"
" Update Delay: %" PRIu32 "ms\n"
" Max Update Delay: %" PRIu32 "ms",
this->link_.host(), this->link_.port(), this->link_.reconnect_interval(), this->update_delay_ms_,
this->max_update_delay_ms_);
}
void VncClient::on_shutdown() { this->link_.close(); }
void VncClient::loop() {
const uint32_t now = App.get_loop_component_start_time();
this->pass_start_ms_ = millis();
if (this->display_->is_failed()) {
this->link_.close(); // a connect still in flight would otherwise keep its socket for good
if (this->state_ != VncState::VNC_STATE_IDLE) {
this->session_end_(now, VncFailure::VNC_FAILURE_TRANSIENT, LOG_STR("Display failed"));
}
this->status_set_error(LOG_STR("Display failed"));
this->disable_loop();
return;
}
// A display that parks its loop when idle is busy while that loop runs. Displays that never
// park (no loop, or one that always runs) update synchronously, so they are never busy.
if (this->display_->is_idle()) {
this->display_parks_ = true;
this->display_busy_ = false;
} else {
this->display_busy_ = this->display_parks_;
}
// A stuck panel, or another owner of the display's loop, would otherwise freeze the screen silently
if (!this->display_busy_) {
this->busy_since_ms_ = now;
if (this->busy_warned_) {
this->busy_warned_ = false;
// The connection path owns the warning while no session runs
if (this->state_ == VncState::VNC_STATE_RUNNING) {
this->status_clear_warning();
}
}
} else if (!this->busy_warned_ && now - this->busy_since_ms_ >= DISPLAY_BUSY_TIMEOUT_MS) {
this->busy_warned_ = true;
this->status_set_warning(LOG_STR("Display busy too long"));
}
if (!this->link_.connected()) {
if (this->state_ != VncState::VNC_STATE_IDLE) {
this->session_lost_(now);
}
this->connect_step_(now);
if (!this->link_.connected()) {
return;
}
this->start_handshake_(now);
}
if (this->state_ == VncState::VNC_STATE_HANDSHAKE && now - this->session_start_ms_ >= HANDSHAKE_TIMEOUT_MS) {
this->fail_(this->lost_failure_(), LOG_STR("Handshake timed out"));
return;
}
const bool blocked = this->in_update_ && this->display_busy_;
// A display still starting up (it8951) can go from its start-up straight into a refresh without parking.
const bool display_starting =
!this->display_parks_ && this->display_->has_overridden_loop() && now - this->setup_ms_ < DISPLAY_START_MS;
// Wait until the screen stops changing, so an animation becomes one refresh showing where it ended.
if (this->refresh_pending_ && !this->in_update_ && !this->display_busy_ && !display_starting &&
(now - this->last_change_ms_ >= this->update_delay_ms_ ||
now - this->first_change_ms_ >= this->max_update_delay_ms_)) {
// Synchronous displays flush inside update(), so this pass does nothing else.
this->refresh_pending_ = false;
this->display_->update();
this->display_busy_ = this->display_parks_ && !this->display_->is_idle();
} else if (!blocked && (this->rx_pending_ || this->link_.ready())) {
this->service_rx_(now);
}
if (this->state_ == VncState::VNC_STATE_RUNNING) {
// Mid-frame there is nothing to send, so a vanished server would never fail a write.
if (this->in_update_ && !blocked && !this->rx_pending_ && now - this->last_rx_ms_ >= HEARTBEAT_MS) {
this->fail_(VncFailure::VNC_FAILURE_TRANSIENT, LOG_STR("Server stopped sending"));
return;
}
this->pace_(now);
}
this->link_.flush_tx();
if (this->rx_pending_ && !(this->in_update_ && this->display_busy_)) {
this->high_freq_.start();
} else {
this->high_freq_.stop();
}
}
void VncClient::connect_step_(uint32_t now) {
const bool up = network::is_connected();
if (up && !this->network_was_up_) {
this->backoff_ms_ = 0;
this->hold_ms_ = 0;
}
this->network_was_up_ = up;
if (!up) {
return;
}
if (!this->link_.connecting() && (now - this->hold_start_ms_ < this->hold_ms_ || this->link_.in_backoff())) {
return;
}
this->link_.poll();
// DNS failures, refused connects and connect timeouts all leave no socket and start the link's backoff.
if (!this->link_.connected() && !this->link_.connecting() && this->link_.in_backoff()) {
this->session_end_(now, VncFailure::VNC_FAILURE_TRANSIENT, LOG_STR("Cannot reach the server"));
}
}
void VncClient::reset_session_() {
this->rx_pos_ = 0;
this->rx_len_ = 0;
this->rx_pending_ = false;
this->in_update_ = false;
this->request_outstanding_ = false;
this->refresh_pending_ = false;
this->frame_drawn_ = false;
this->step_ = VncStep::VNC_STEP_VERSION;
this->reason_failure_ = VncFailure::VNC_FAILURE_TRANSIENT;
this->high_freq_.stop();
}
void VncClient::start_handshake_(uint32_t now) {
this->reset_session_();
this->state_ = VncState::VNC_STATE_HANDSHAKE;
this->session_start_ms_ = now;
this->last_rx_ms_ = now;
}
VncFailure VncClient::lost_failure_() const {
if (this->state_ == VncState::VNC_STATE_HANDSHAKE) {
if (this->step_ == VncStep::VNC_STEP_REASON_LENGTH || this->step_ == VncStep::VNC_STEP_REASON) {
return this->reason_failure_;
}
}
return VncFailure::VNC_FAILURE_TRANSIENT;
}
void VncClient::session_lost_(uint32_t now) {
this->session_end_(now, this->lost_failure_(), LOG_STR("Connection lost"));
}
VncParse VncClient::fail_(VncFailure failure, const LogString *reason) {
this->link_.close();
this->link_.note_attempt();
this->session_end_(App.get_loop_component_start_time(), failure, reason);
return VncParse::VNC_PARSE_STOP;
}
void VncClient::session_end_(uint32_t now, VncFailure failure, const LogString *reason) {
const bool was_running = this->state_ == VncState::VNC_STATE_RUNNING;
this->reset_session_();
this->state_ = VncState::VNC_STATE_IDLE;
this->hold_start_ms_ = now;
if (was_running) {
const uint32_t lasted = now - this->session_start_ms_;
ESP_LOGW(TAG, "%s after %" PRIu32 "s", LOG_STR_ARG(reason), lasted / 1000);
this->disconnect_callback_.call();
this->status_set_warning();
if (lasted >= STABLE_SESSION_MS) {
this->backoff_ms_ = 0;
this->hold_ms_ = 0;
return;
}
failure = VncFailure::VNC_FAILURE_TRANSIENT;
}
switch (failure) {
case VncFailure::VNC_FAILURE_CONFIG:
this->hold_ms_ = CONFIG_RETRY_MS;
ESP_LOGE(TAG, "%s; retrying in %" PRIu32 "s", LOG_STR_ARG(reason), this->hold_ms_ / 1000);
this->status_set_error(reason);
return;
default: {
const uint32_t base = this->link_.reconnect_interval();
this->backoff_ms_ =
this->backoff_ms_ == 0 ? base : std::min(this->backoff_ms_ * 2, std::max(MAX_BACKOFF_MS, base));
this->hold_ms_ = this->backoff_ms_;
if (!was_running) {
ESP_LOGW(TAG, "%s; retrying in %" PRIu32 "s", LOG_STR_ARG(reason), this->hold_ms_ / 1000);
}
this->status_set_warning();
return;
}
}
}
bool VncClient::budget_exhausted_() const { return millis() - this->pass_start_ms_ >= LOOP_BUDGET_MS; }
void VncClient::service_rx_(uint32_t now) {
for (;;) {
VncParse result = this->parse_();
if (result == VncParse::VNC_PARSE_STOP) {
return;
}
if (result == VncParse::VNC_PARSE_PAUSE) {
this->rx_pending_ = true;
return;
}
// Less than one item is left, so the buffer always has room after compacting.
if (this->rx_pos_ != 0) {
this->rx_len_ -= this->rx_pos_;
std::memmove(this->rx_, this->rx_ + this->rx_pos_, this->rx_len_);
this->rx_pos_ = 0;
}
if (this->budget_exhausted_()) {
this->rx_pending_ = true;
return;
}
ssize_t count = this->link_.read(this->rx_ + this->rx_len_, RX_BUFFER_SIZE - this->rx_len_);
if (count < 0) {
this->session_lost_(now);
return;
}
if (count == 0) {
this->rx_pending_ = false;
return;
}
this->rx_len_ += static_cast<uint16_t>(count);
this->rx_total_ += count;
this->last_rx_ms_ = now;
}
}
void VncClient::skip_(uint32_t count, VncStep next) {
this->skip_left_ = count;
this->after_skip_ = next;
this->step_ = VncStep::VNC_STEP_SKIP;
}
bool VncClient::send_client_init_() {
const uint8_t shared = 1;
return this->send_(&shared, 1);
}
VncParse VncClient::parse_() {
for (;;) {
const uint8_t *p = this->rx_ + this->rx_pos_;
const size_t avail = this->rx_len_ - this->rx_pos_;
switch (this->step_) {
case VncStep::VNC_STEP_VERSION: {
if (avail < 12) {
return VncParse::VNC_PARSE_MORE;
}
const int major = parse_three_digits(p + 4);
const int minor = parse_three_digits(p + 8);
if (std::memcmp(p, "RFB ", 4) != 0 || p[7] != '.' || p[11] != '\n' || major < 0 || minor < 0) {
return this->fail_(VncFailure::VNC_FAILURE_CONFIG, LOG_STR("Not a VNC server"));
}
if (major < 3) {
return this->fail_(VncFailure::VNC_FAILURE_CONFIG, LOG_STR("Unsupported RFB version"));
}
this->minor_ = (major > 3 || minor >= 8) ? 8 : (minor == 7 ? 7 : 3);
this->rx_pos_ += 12;
uint8_t reply[12] = {'R', 'F', 'B', ' ', '0', '0', '3', '.', '0', '0', '8', '\n'};
reply[10] = '0' + this->minor_;
if (!this->send_(reply, sizeof(reply))) {
return this->fail_send_();
}
this->step_ = this->minor_ == 3 ? VncStep::VNC_STEP_SECURITY_TYPE_33 : VncStep::VNC_STEP_SECURITY_COUNT;
break;
}
case VncStep::VNC_STEP_SECURITY_COUNT: {
if (avail < 1) {
return VncParse::VNC_PARSE_MORE;
}
this->sec_count_ = p[0];
this->rx_pos_ += 1;
if (this->sec_count_ == 0) {
// A refusal before authentication, such as a lockout caused by another client.
this->step_ = VncStep::VNC_STEP_REASON_LENGTH;
} else {
this->step_ = VncStep::VNC_STEP_SECURITY_LIST;
}
break;
}
case VncStep::VNC_STEP_SECURITY_LIST: {
if (avail < this->sec_count_) {
return VncParse::VNC_PARSE_MORE;
}
uint8_t chosen = 0;
bool wants_password = false;
for (uint8_t i = 0; i < this->sec_count_; i++) {
if (p[i] == SECURITY_NONE) {
chosen = p[i];
break;
}
wants_password |= p[i] == SECURITY_VNC_AUTH;
}
this->rx_pos_ += this->sec_count_;
if (chosen == 0) {
return this->fail_(VncFailure::VNC_FAILURE_CONFIG, wants_password ? LOG_STR("The server requires a password")
: LOG_STR("No supported security type"));
}
if (!this->send_(&chosen, 1)) {
return this->fail_send_();
}
if (this->minor_ == 8) {
this->step_ = VncStep::VNC_STEP_SECURITY_RESULT;
} else {
if (!this->send_client_init_()) {
return this->fail_send_();
}
this->step_ = VncStep::VNC_STEP_SERVER_INIT;
}
break;
}
case VncStep::VNC_STEP_SECURITY_TYPE_33: {
if (avail < 4) {
return VncParse::VNC_PARSE_MORE;
}
const uint32_t type = read_be32(p);
this->rx_pos_ += 4;
if (type == 0) {
this->step_ = VncStep::VNC_STEP_REASON_LENGTH;
} else if (type == SECURITY_NONE) {
if (!this->send_client_init_()) {
return this->fail_send_();
}
this->step_ = VncStep::VNC_STEP_SERVER_INIT;
} else {
return this->fail_(VncFailure::VNC_FAILURE_CONFIG, type == SECURITY_VNC_AUTH
? LOG_STR("The server requires a password")
: LOG_STR("No supported security type"));
}
break;
}
case VncStep::VNC_STEP_SECURITY_RESULT: {
if (avail < 4) {
return VncParse::VNC_PARSE_MORE;
}
const uint32_t result = read_be32(p);
this->rx_pos_ += 4;
if (result == 0) {
if (!this->send_client_init_()) {
return this->fail_send_();
}
this->step_ = VncStep::VNC_STEP_SERVER_INIT;
break;
}
this->reason_failure_ = VncFailure::VNC_FAILURE_CONFIG;
this->step_ = VncStep::VNC_STEP_REASON_LENGTH;
break;
}
case VncStep::VNC_STEP_REASON_LENGTH: {
if (avail < 4) {
return VncParse::VNC_PARSE_MORE;
}
this->text_len_ = static_cast<uint8_t>(std::min(read_be32(p), MAX_TEXT_LEN));
this->rx_pos_ += 4;
this->step_ = VncStep::VNC_STEP_REASON;
break;
}
case VncStep::VNC_STEP_REASON: {
if (avail < this->text_len_) {
return VncParse::VNC_PARSE_MORE;
}
ESP_LOGE(TAG, "Server: %.*s", static_cast<int>(this->text_len_), reinterpret_cast<const char *>(p));
return this->fail_(this->reason_failure_, LOG_STR("Connection refused"));
}
case VncStep::VNC_STEP_SERVER_INIT: {
if (avail < 24) {
return VncParse::VNC_PARSE_MORE;
}
this->fb_w_ = read_be16(p);
this->fb_h_ = read_be16(p + 2);
// Reused for the length of the name that follows.
this->skip_left_ = read_be32(p + 20);
this->rx_pos_ += 24;
if (this->fb_w_ == 0 || this->fb_h_ == 0) {
return this->fail_(VncFailure::VNC_FAILURE_TRANSIENT, LOG_STR("The server screen is empty"));
}
this->text_len_ = static_cast<uint8_t>(std::min(this->skip_left_, MAX_TEXT_LEN));
this->step_ = VncStep::VNC_STEP_SERVER_NAME;
break;
}
case VncStep::VNC_STEP_SERVER_NAME: {
if (avail < this->text_len_) {
return VncParse::VNC_PARSE_MORE;
}
ESP_LOGI(TAG, "Server '%.*s', %ux%u, RFB 3.%u", static_cast<int>(this->text_len_),
reinterpret_cast<const char *>(p), this->fb_w_, this->fb_h_, this->minor_);
this->rx_pos_ += this->text_len_;
this->skip_(this->skip_left_ - this->text_len_, VncStep::VNC_STEP_SESSION_START);
break;
}
case VncStep::VNC_STEP_SESSION_START:
if (!this->start_session_(App.get_loop_component_start_time())) {
return this->fail_send_();
}
this->step_ = VncStep::VNC_STEP_MESSAGE;
break;
case VncStep::VNC_STEP_MESSAGE: {
if (avail < 1) {
return VncParse::VNC_PARSE_MORE;
}
switch (p[0]) {
case MSG_FRAMEBUFFER_UPDATE:
if (avail < 4) {
return VncParse::VNC_PARSE_MORE;
}
this->rects_left_ = read_be16(p + 2);
this->rx_pos_ += 4;
this->in_update_ = true;
this->update_start_ms_ = millis();
this->update_start_bytes_ = this->rx_total_ - (this->rx_len_ - this->rx_pos_);
this->rect_done_();
break;
case MSG_SET_COLOUR_MAP:
if (avail < 6) {
return VncParse::VNC_PARSE_MORE;
}
this->rx_pos_ += 6;
this->skip_(6u * read_be16(p + 4), VncStep::VNC_STEP_MESSAGE);
break;
case MSG_BELL:
this->rx_pos_ += 1;
break;
case MSG_SERVER_CUT_TEXT:
if (avail < 8) {
return VncParse::VNC_PARSE_MORE;
}
this->rx_pos_ += 8;
this->skip_(read_be32(p + 4), VncStep::VNC_STEP_MESSAGE);
break;
default:
ESP_LOGW(TAG, "Unknown message type %u", p[0]);
return this->fail_(VncFailure::VNC_FAILURE_TRANSIENT, LOG_STR("Protocol error"));
}
break;
}
case VncStep::VNC_STEP_RECT: {
if (this->display_busy_) {
return VncParse::VNC_PARSE_PAUSE;
}
if (avail < 12) {
return VncParse::VNC_PARSE_MORE;
}
this->rect_x_ = read_be16(p);
this->rect_y_ = read_be16(p + 2);
this->rect_w_ = read_be16(p + 4);
this->rect_h_ = read_be16(p + 6);
const auto encoding = static_cast<int32_t>(read_be32(p + 8));
this->rx_pos_ += 12;
this->rects_left_--;
if (encoding == ENCODING_RAW) {
if (static_cast<uint32_t>(this->rect_x_) + this->rect_w_ > this->fb_w_ ||
static_cast<uint32_t>(this->rect_y_) + this->rect_h_ > this->fb_h_) {
ESP_LOGW(TAG, "Rectangle %ux%u at %u,%u is outside the %ux%u screen", this->rect_w_, this->rect_h_,
this->rect_x_, this->rect_y_, this->fb_w_, this->fb_h_);
return this->fail_(VncFailure::VNC_FAILURE_TRANSIENT, LOG_STR("Protocol error"));
}
if (this->rect_w_ == 0 || this->rect_h_ == 0) {
this->rect_done_();
} else {
this->pos_x_ = 0;
this->pos_y_ = 0;
this->step_ = VncStep::VNC_STEP_RAW;
}
} else if (encoding == ENCODING_RICH_CURSOR) {
if (this->rect_w_ > MAX_CURSOR_SIDE || this->rect_h_ > MAX_CURSOR_SIDE) {
return this->fail_(VncFailure::VNC_FAILURE_TRANSIENT, LOG_STR("Protocol error"));
}
const uint32_t size = static_cast<uint32_t>(this->rect_w_) * this->rect_h_ * this->bpp_ +
((this->rect_w_ + 7u) / 8u) * this->rect_h_;
this->skip_(size, VncStep::VNC_STEP_RECT_DONE);
} else {
ESP_LOGW(TAG, "Unsupported encoding %" PRId32, encoding);
return this->fail_(VncFailure::VNC_FAILURE_TRANSIENT, LOG_STR("Protocol error"));
}
break;
}
case VncStep::VNC_STEP_RECT_DONE:
this->rect_done_();
break;
case VncStep::VNC_STEP_RAW: {
if (this->display_busy_) {
return VncParse::VNC_PARSE_PAUSE;
}
const size_t pixels = avail / this->bpp_;
if (pixels == 0) {
return VncParse::VNC_PARSE_MORE;
}
if (this->budget_exhausted_()) {
return VncParse::VNC_PARSE_PAUSE;
}
const auto count = static_cast<uint16_t>(std::min<size_t>(pixels, this->rect_w_ - this->pos_x_));
this->draw_row_(this->rect_x_ + this->pos_x_, this->rect_y_ + this->pos_y_, count, p);
this->rx_pos_ += count * this->bpp_;
this->pos_x_ += count;
if (this->pos_x_ == this->rect_w_) {
this->pos_x_ = 0;
if (++this->pos_y_ == this->rect_h_) {
this->rect_done_();
}
}
break;
}
case VncStep::VNC_STEP_SKIP: {
const auto count = static_cast<uint32_t>(std::min<size_t>(avail, this->skip_left_));
this->rx_pos_ += count;
this->skip_left_ -= count;
if (this->skip_left_ != 0) {
return VncParse::VNC_PARSE_MORE;
}
this->step_ = this->after_skip_;
break;
}
}
}
}
void VncClient::rect_done_() {
if (this->rects_left_ == 0) {
this->frame_done_();
} else {
this->step_ = VncStep::VNC_STEP_RECT;
}
}
void VncClient::frame_done_() {
this->in_update_ = false;
this->request_outstanding_ = false;
this->step_ = VncStep::VNC_STEP_MESSAGE;
if (this->frame_drawn_) {
const uint32_t now = App.get_loop_component_start_time();
this->frame_drawn_ = false;
if (!this->refresh_pending_) {
this->first_change_ms_ = now;
}
this->last_change_ms_ = now;
this->refresh_pending_ = true;
ESP_LOGD(TAG, "Update: %" PRIu32 " bytes in %" PRIu32 "ms",
this->rx_total_ - (this->rx_len_ - this->rx_pos_) - this->update_start_bytes_,
millis() - this->update_start_ms_);
}
}
void VncClient::draw_row_(uint16_t x, uint16_t y, uint16_t count, const uint8_t *pixels) {
if (y >= this->vis_h_ || x >= this->vis_w_) {
return;
}
const int visible = std::min<int>(count, this->vis_w_ - x);
this->display_->draw_pixels_at(x, y, visible, 1, pixels, this->bitness_, this->big_endian_);
this->frame_drawn_ = true;
}
bool VncClient::start_session_(uint32_t now) {
const bool rgb565 = this->display_->get_display_type() != display::DISPLAY_TYPE_BINARY;
this->bpp_ = rgb565 ? 2 : 1;
this->bitness_ = rgb565 ? display::COLOR_BITNESS_565 : display::COLOR_BITNESS_332;
const int disp_w = this->display_->get_width();
const int disp_h = this->display_->get_height();
this->vis_w_ = std::min<int>(this->fb_w_, disp_w);
this->vis_h_ = std::min<int>(this->fb_h_, disp_h);
if (this->fb_w_ != disp_w || this->fb_h_ != disp_h) {
ESP_LOGW(TAG, "Server screen is %ux%u but the display is %dx%d; showing the top-left %ux%u", this->fb_w_,
this->fb_h_, disp_w, disp_h, this->vis_w_, this->vis_h_);
}
// SetPixelFormat: true colour, in the byte order the display takes
uint8_t pixel_format[20] = {};
pixel_format[4] = rgb565 ? 16 : 8;
pixel_format[5] = rgb565 ? 16 : 8;
pixel_format[6] = this->big_endian_;
pixel_format[7] = 1;
write_be16(pixel_format + 8, rgb565 ? 31 : 7);
write_be16(pixel_format + 10, rgb565 ? 63 : 7);
write_be16(pixel_format + 12, rgb565 ? 31 : 3);
pixel_format[14] = rgb565 ? 11 : 5;
pixel_format[15] = rgb565 ? 5 : 2;
if (!this->send_(pixel_format, sizeof(pixel_format))) {
return false;
}
// SetEncodings: TightVNC uses the first real encoding it supports for every rectangle.
uint8_t encodings[12] = {2, 0, 0, 2};
write_be32(encodings + 4, ENCODING_RAW);
write_be32(encodings + 8, static_cast<uint32_t>(ENCODING_RICH_CURSOR));
if (!this->send_(encodings, sizeof(encodings)) || !this->send_update_request_(false, now)) {
return false;
}
this->state_ = VncState::VNC_STATE_RUNNING;
this->session_start_ms_ = now;
if (!this->busy_warned_) {
this->status_clear_warning(); // a stuck display keeps its warning through a reconnect
}
this->status_clear_error();
this->connect_callback_.call();
return true;
}
void VncClient::pace_(uint32_t now) {
if (this->display_busy_ || this->in_update_ || now - this->last_request_ms_ < MIN_REQUEST_SPACING_MS) {
return;
}
// Repeating a parked request costs nothing on the server and detects a dead link.
if (!this->request_outstanding_ ||
(now - this->last_rx_ms_ >= HEARTBEAT_MS && now - this->last_request_ms_ >= HEARTBEAT_MS)) {
this->send_update_request_(true, now);
}
}
bool VncClient::send_(const uint8_t *data, size_t len) {
if (this->link_.tx_free() < len) {
this->link_.flush_tx();
if (this->link_.tx_free() < len) {
return false;
}
}
this->link_.queue(data, len);
return true;
}
bool VncClient::send_update_request_(bool incremental, uint32_t now) {
uint8_t msg[10] = {3, static_cast<uint8_t>(incremental ? 1 : 0)};
write_be16(msg + 6, this->vis_w_);
write_be16(msg + 8, this->vis_h_);
if (!this->send_(msg, sizeof(msg))) {
return false;
}
this->request_outstanding_ = true;
this->last_request_ms_ = now;
return true;
}
} // namespace esphome::vnc
+162
View File
@@ -0,0 +1,162 @@
#pragma once
#include "esphome/components/display/display.h"
#include "esphome/components/socket/tcp_client_link.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include <cstddef>
#include <cstdint>
namespace esphome::vnc {
enum class VncState : uint8_t {
VNC_STATE_IDLE,
VNC_STATE_HANDSHAKE,
VNC_STATE_RUNNING,
};
enum class VncStep : uint8_t {
VNC_STEP_VERSION,
VNC_STEP_SECURITY_COUNT,
VNC_STEP_SECURITY_LIST,
VNC_STEP_SECURITY_TYPE_33,
VNC_STEP_SECURITY_RESULT,
VNC_STEP_REASON_LENGTH,
VNC_STEP_REASON,
VNC_STEP_SERVER_INIT,
VNC_STEP_SERVER_NAME,
VNC_STEP_SESSION_START,
VNC_STEP_MESSAGE,
VNC_STEP_RECT,
VNC_STEP_RECT_DONE,
VNC_STEP_RAW,
VNC_STEP_SKIP,
};
enum class VncParse : uint8_t {
VNC_PARSE_MORE, // the next step needs bytes that have not arrived
VNC_PARSE_PAUSE, // out of time, or the display is busy; resume without new data
VNC_PARSE_STOP, // the session ended
};
enum class VncFailure : uint8_t {
VNC_FAILURE_TRANSIENT,
VNC_FAILURE_CONFIG,
};
/// RFB (VNC) client that draws the remote framebuffer into a display.
class VncClient : public Component {
public:
explicit VncClient(display::Display *display, bool big_endian = false) : display_(display), big_endian_(big_endian) {}
void set_host(const char *host) { this->link_.set_host(host); }
void set_port(uint16_t port) { this->link_.set_port(port); }
void set_reconnect_interval(uint32_t ms) { this->link_.set_reconnect_interval(ms); }
void set_update_delay(uint32_t ms) { this->update_delay_ms_ = ms; }
void set_max_update_delay(uint32_t ms) { this->max_update_delay_ms_ = ms; }
void setup() override;
void loop() override;
void dump_config() override;
void on_shutdown() override;
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
bool is_connected() const { return this->state_ == VncState::VNC_STATE_RUNNING; }
template<typename F> void add_on_connect_callback(F &&callback) {
this->connect_callback_.add(std::forward<F>(callback));
}
template<typename F> void add_on_disconnect_callback(F &&callback) {
this->disconnect_callback_.add(std::forward<F>(callback));
}
protected:
static constexpr size_t RX_BUFFER_SIZE = 512;
void connect_step_(uint32_t now);
void start_handshake_(uint32_t now);
void service_rx_(uint32_t now);
VncParse parse_();
void pace_(uint32_t now);
bool start_session_(uint32_t now);
bool send_client_init_();
void frame_done_();
void rect_done_();
void draw_row_(uint16_t x, uint16_t y, uint16_t count, const uint8_t *pixels);
void skip_(uint32_t count, VncStep next);
bool budget_exhausted_() const;
bool send_(const uint8_t *data, size_t len);
bool send_update_request_(bool incremental, uint32_t now);
VncParse fail_(VncFailure failure, const LogString *reason);
VncParse fail_send_() { return this->fail_(VncFailure::VNC_FAILURE_TRANSIENT, LOG_STR("Send failed")); }
VncFailure lost_failure_() const;
void session_lost_(uint32_t now);
void session_end_(uint32_t now, VncFailure failure, const LogString *reason);
void reset_session_();
display::Display *display_;
socket::TcpClientLink link_;
LazyCallbackManager<void()> connect_callback_;
LazyCallbackManager<void()> disconnect_callback_;
HighFrequencyLoopRequester high_freq_;
uint32_t update_delay_ms_{200};
uint32_t max_update_delay_ms_{1000};
uint32_t backoff_ms_{0};
uint32_t hold_ms_{0};
uint32_t hold_start_ms_{0};
uint32_t setup_ms_{0};
uint32_t busy_since_ms_{0};
uint32_t session_start_ms_{0};
uint32_t update_start_ms_{0};
// The first and the latest frame drawn since the display last refreshed.
uint32_t first_change_ms_{0};
uint32_t last_change_ms_{0};
uint32_t rx_total_{0};
uint32_t update_start_bytes_{0};
uint32_t last_request_ms_{0};
uint32_t last_rx_ms_{0};
uint32_t pass_start_ms_{0};
uint32_t skip_left_{0};
uint16_t rx_pos_{0};
uint16_t rx_len_{0};
uint16_t fb_w_{0};
uint16_t fb_h_{0};
uint16_t vis_w_{0};
uint16_t vis_h_{0};
uint16_t rects_left_{0};
uint16_t rect_x_{0};
uint16_t rect_y_{0};
uint16_t rect_w_{0};
uint16_t rect_h_{0};
// Raw: column and row inside the rect.
uint16_t pos_x_{0};
uint16_t pos_y_{0};
uint8_t bpp_{2};
uint8_t minor_{8};
uint8_t sec_count_{0};
uint8_t text_len_{0}; // clamped to MAX_TEXT_LEN
display::ColorBitness bitness_{display::COLOR_BITNESS_565};
VncState state_{VncState::VNC_STATE_IDLE};
VncStep step_{VncStep::VNC_STEP_VERSION};
VncStep after_skip_{VncStep::VNC_STEP_MESSAGE};
VncFailure reason_failure_{VncFailure::VNC_FAILURE_TRANSIENT};
bool rx_pending_{false};
bool in_update_{false};
bool request_outstanding_{false};
bool refresh_pending_{false};
bool frame_drawn_{false};
bool display_parks_{false};
bool display_busy_{false};
bool busy_warned_{false};
bool network_was_up_{false};
bool big_endian_; // the pixel order the display takes
uint8_t rx_[RX_BUFFER_SIZE];
};
} // namespace esphome::vnc
+13 -13
View File
@@ -1043,7 +1043,7 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa
if (is_on) {
parse_cstr_param_(
request, ESPHOME_F("effect"), call,
static_cast<light::LightCall &(light::LightCall::*) (const char *, size_t)>(&decltype(call)::set_effect));
static_cast<light::LightCall &(light::LightCall::*)(const char *, size_t)>(&decltype(call)::set_effect));
}
DEFER_ACTION(call, call.perform());
@@ -1404,7 +1404,7 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat
auto call = obj->make_call();
parse_cstr_param_(
request, ESPHOME_F("value"), call,
static_cast<text::TextCall &(text::TextCall::*) (const char *, size_t)>(&decltype(call)::set_value));
static_cast<text::TextCall &(text::TextCall::*)(const char *, size_t)>(&decltype(call)::set_value));
DEFER_ACTION(call, call.perform());
request->send(200);
@@ -1464,7 +1464,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM
auto call = obj->make_call();
parse_cstr_param_(
request, ESPHOME_F("option"), call,
static_cast<select::SelectCall &(select::SelectCall::*) (const char *, size_t)>(&decltype(call)::set_option));
static_cast<select::SelectCall &(select::SelectCall::*)(const char *, size_t)>(&decltype(call)::set_option));
DEFER_ACTION(call, call.perform());
request->send(200);
@@ -1522,26 +1522,26 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url
// Parse string mode parameters
parse_cstr_param_(
request, ESPHOME_F("mode"), call,
static_cast<climate::ClimateCall &(climate::ClimateCall::*) (const char *, size_t)>(&decltype(call)::set_mode));
static_cast<climate::ClimateCall &(climate::ClimateCall::*)(const char *, size_t)>(&decltype(call)::set_mode));
parse_cstr_param_(request, ESPHOME_F("fan_mode"), call,
static_cast<climate::ClimateCall &(climate::ClimateCall::*) (const char *, size_t)>(
static_cast<climate::ClimateCall &(climate::ClimateCall::*)(const char *, size_t)>(
&decltype(call)::set_fan_mode));
parse_cstr_param_(request, ESPHOME_F("swing_mode"), call,
static_cast<climate::ClimateCall &(climate::ClimateCall::*) (const char *, size_t)>(
static_cast<climate::ClimateCall &(climate::ClimateCall::*)(const char *, size_t)>(
&decltype(call)::set_swing_mode));
parse_cstr_param_(request, ESPHOME_F("preset"), call,
static_cast<climate::ClimateCall &(climate::ClimateCall::*) (const char *, size_t)>(
static_cast<climate::ClimateCall &(climate::ClimateCall::*)(const char *, size_t)>(
&decltype(call)::set_preset));
// Parse temperature parameters
// static_cast needed to disambiguate overloaded setters (float vs optional<float>)
using ClimateCall = decltype(call);
parse_num_param_(request, ESPHOME_F("target_temperature_high"), call,
static_cast<ClimateCall &(ClimateCall::*) (float)>(&ClimateCall::set_target_temperature_high));
static_cast<ClimateCall &(ClimateCall::*)(float)>(&ClimateCall::set_target_temperature_high));
parse_num_param_(request, ESPHOME_F("target_temperature_low"), call,
static_cast<ClimateCall &(ClimateCall::*) (float)>(&ClimateCall::set_target_temperature_low));
static_cast<ClimateCall &(ClimateCall::*)(float)>(&ClimateCall::set_target_temperature_low));
parse_num_param_(request, ESPHOME_F("target_temperature"), call,
static_cast<ClimateCall &(ClimateCall::*) (float)>(&ClimateCall::set_target_temperature));
static_cast<ClimateCall &(ClimateCall::*)(float)>(&ClimateCall::set_target_temperature));
DEFER_ACTION(call, call.perform());
request->send(200);
@@ -1818,8 +1818,8 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques
auto call = obj->make_call();
parse_cstr_param_(
request, ESPHOME_F("code"), call,
static_cast<alarm_control_panel::AlarmControlPanelCall &(
alarm_control_panel::AlarmControlPanelCall::*) (const char *, size_t)>(&decltype(call)::set_code));
static_cast<alarm_control_panel::AlarmControlPanelCall &(alarm_control_panel::AlarmControlPanelCall::*)(
const char *, size_t)>(&decltype(call)::set_code));
static constexpr ActionEntry<alarm_control_panel::AlarmControlPanelCall> ACTIONS[] PROGMEM = {
{"disarm", &alarm_control_panel::AlarmControlPanelCall::disarm},
@@ -1893,7 +1893,7 @@ void WebServer::handle_water_heater_request(AsyncWebServerRequest *request, cons
// Parse mode parameter
parse_cstr_param_(
request, ESPHOME_F("mode"), base_call,
static_cast<water_heater::WaterHeaterCall &(water_heater::WaterHeaterCall::*) (const char *, size_t)>(
static_cast<water_heater::WaterHeaterCall &(water_heater::WaterHeaterCall::*)(const char *, size_t)>(
&water_heater::WaterHeaterCall::set_mode));
// Parse temperature parameters
+6 -3
View File
@@ -450,9 +450,12 @@ uart::UARTFlushResult WeikaiChannel::flush() {
size_t WeikaiChannel::xfer_fifo_to_buffer_() {
size_t total = 0;
size_t to_transfer;
size_t free;
while ((to_transfer = this->rx_in_fifo_()) && (free = this->receive_buffer_.free())) {
while (true) {
size_t to_transfer = this->rx_in_fifo_();
size_t free = this->receive_buffer_.free();
if (to_transfer == 0 || free == 0) {
break;
}
// while bytes in fifo and some room in the buffer we transfer
if (to_transfer > XFER_MAX_SIZE)
to_transfer = XFER_MAX_SIZE; // we can only do so much
@@ -309,6 +309,8 @@ bool WhirlpoolClimate::on_receive(remote_base::RemoteReceiveData data) {
case WHIRLPOOL_AUTO:
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
break;
default:
break;
}
}
+43 -9
View File
@@ -39,6 +39,7 @@ from esphome.const import (
CONF_ENABLE_BTM,
CONF_ENABLE_ON_BOOT,
CONF_ENABLE_RRM,
CONF_ETHERNET,
CONF_FAST_CONNECT,
CONF_GATEWAY,
CONF_HIDDEN,
@@ -431,8 +432,26 @@ def _apply_min_auth_mode_default(config):
return config
def wifi_has_sta(config: ConfigType) -> bool:
"""Whether the validated config has at least one station network."""
return bool(config.get(CONF_NETWORKS))
# Components that save station credentials at run time
_PROVISIONERS = ("captive_portal", "improv_ble", "improv_serial")
def wifi_stays_ap_only(config: ConfigType, full_config: ConfigType) -> bool:
"""Whether no station can appear at run time: no networks, no DPP, no provisioner."""
return not (
wifi_has_sta(config)
or CONF_DPP in config
or any(c in full_config for c in _PROVISIONERS)
)
def final_validate(config):
has_sta = bool(config.get(CONF_NETWORKS, True))
has_sta = wifi_has_sta(config)
has_ap = CONF_AP in config
has_dpp = CONF_DPP in config
full_config = fv.full_config.get()
@@ -447,9 +466,11 @@ def final_validate(config):
ap_is_wisp = has_ap and config[CONF_AP][CONF_AP_MODE] == WIFI_AP_MODE_WISP
if ap_is_wisp:
if not has_sta:
# The clients are routed through the station, or through ethernet
if not has_sta and CONF_ETHERNET not in full_config:
raise cv.Invalid(
"WISP mode requires at least one STA network (wifi.ssid or wifi.networks)"
"WISP mode needs a station network (wifi.ssid or wifi.networks), "
"or ethernet as the upstream interface"
)
if has_captive_portal:
raise cv.Invalid(
@@ -458,14 +479,27 @@ def final_validate(config):
# Every client packet passes through the WiFi driver, none through the device's own sockets
require_high_performance_wifi()
if CONF_DNS_SERVERS not in config[CONF_AP]:
# The access point passes the station's resolvers on, which a static address has to name
for network in [config, *config.get(CONF_NETWORKS, [])]:
# The access point passes the upstream's resolvers on, which a static address has to name
upstreams = [
([], config),
*(
([CONF_NETWORKS, i], network)
for i, network in enumerate(config.get(CONF_NETWORKS, []))
),
(None, full_config.get(CONF_ETHERNET, {})),
]
for path, network in upstreams:
static = network.get(CONF_MANUAL_IP)
if static and static[CONF_DNS1].is_unspecified:
if not static or not static[CONF_DNS1].is_unspecified:
continue
if path is None:
raise cv.Invalid(
"manual_ip needs dns1 in WISP mode unless ap.dns_servers is set",
path=[CONF_MANUAL_IP, CONF_DNS1],
"ethernet manual_ip needs dns1 in WISP mode unless ap.dns_servers is set"
)
raise cv.Invalid(
"manual_ip needs dns1 in WISP mode unless ap.dns_servers is set",
path=[*path, CONF_MANUAL_IP, CONF_DNS1],
)
if has_ap and not has_captive_portal and not has_web_server and not ap_is_wisp:
_LOGGER.warning(
@@ -565,7 +599,7 @@ def _report_provisioning_credentials(config):
"""
from esphome.components import provisioning
if config.get(CONF_NETWORKS):
if wifi_has_sta(config):
provisioning.report_hardcoded_credentials("wifi")
elif CONF_AP in config:
# An access point with no station credentials: the AP shuts down when the
+20 -16
View File
@@ -783,12 +783,9 @@ void WiFiComponent::start() {
// Without fast_connect: go straight to scanning (or hidden mode if all networks are hidden)
this->start_initial_connection_();
#endif
#ifdef USE_WIFI_AP
} else if (this->has_ap()) {
#if defined(USE_WIFI_AP) && !defined(USE_WIFI_AP_WISP)
} else if (this->has_ap()) { // a bridge waits in loop() for the upstream's resolver
this->setup_ap_config_();
if (!std::isnan(this->output_power_) && !this->wifi_apply_output_power_(this->output_power_)) {
ESP_LOGV(TAG, "Setting Output Power Option failed");
}
#ifdef USE_CAPTIVE_PORTAL
// Where the radio scans alongside the AP, the portal can list networks.
if (captive_portal::global_captive_portal != nullptr && !WIFI_AP_EXCLUSIVE) {
@@ -797,7 +794,7 @@ void WiFiComponent::start() {
}
#endif
this->start_ap_portal_();
#endif // USE_WIFI_AP
#endif // USE_WIFI_AP && !USE_WIFI_AP_WISP
}
#ifdef USE_WIFI_DPP
// Without networks there is nothing to try first, so listen for a configurator right away
@@ -937,15 +934,7 @@ void WiFiComponent::loop() {
return;
}
#ifdef USE_WIFI_AP_WISP
// The bridge comes up once it has a resolver to offer; a start that failed is tried again after ap_timeout
if (!this->ap_setup_ && this->ap_dns_known_() &&
(this->ap_changed_ == 0 || now - this->ap_changed_ > this->ap_timeout_)) {
ESP_LOGI(TAG, "Starting AP");
this->ap_changed_ = now;
this->setup_ap_config_();
}
#elif defined(USE_WIFI_AP)
#if defined(USE_WIFI_AP) && !defined(USE_WIFI_AP_WISP)
// Once the provisioning window has closed, don't bring up the fallback AP (or
// the captive portal on it) - the device must stay unprovisionable until it is
// power-cycled.
@@ -975,7 +964,7 @@ void WiFiComponent::loop() {
this->start_ap_portal_();
}
}
#endif // USE_WIFI_AP
#endif // USE_WIFI_AP && !USE_WIFI_AP_WISP
#ifdef USE_IMPROV_BLE
if (improv_ble::global_improv_component != nullptr && !improv_ble::global_improv_component->is_active() &&
@@ -1013,6 +1002,16 @@ void WiFiComponent::loop() {
}
}
#ifdef USE_WIFI_AP_WISP
// The bridge comes up once it has a resolver to offer; a start that failed is tried again after ap_timeout
if (!this->ap_setup_ && this->state_ != WIFI_COMPONENT_STATE_DISABLED && this->ap_dns_known_() &&
(this->ap_changed_ == 0 || now - this->ap_changed_ > this->ap_timeout_)) {
ESP_LOGI(TAG, "Starting AP");
this->ap_changed_ = now;
this->setup_ap_config_();
}
#endif
#ifdef USE_WIFI_DPP
// Without networks no cooldown starts the listen, so a failed or abandoned enrollment is listened for again
// here, at most once per timeout
@@ -1098,6 +1097,11 @@ void WiFiComponent::setup_ap_config_() {
// Reset before the AP exists: afterwards only the platform's event path writes the counter
this->ap_clients_ = 0;
this->ap_setup_ = this->wifi_start_ap_(this->ap_);
// The station path applied it in start(); without a station the radio only exists from here
if (this->ap_setup_ && !this->has_sta() && !std::isnan(this->output_power_) &&
!this->wifi_apply_output_power_(this->output_power_)) {
ESP_LOGV(TAG, "Setting Output Power Option failed");
}
#if defined(USE_WIFI_AP_EXCLUSIVE) || defined(USE_WIFI_AP_WISP)
// A failed start must not leave the radio in AP mode: on an exclusive radio it would refuse
// the STA attempts until the next try, and a bridge would take clients it cannot serve.
+3 -7
View File
@@ -16,6 +16,7 @@
#endif
#include "esphome/core/string_ref.h"
#include <algorithm>
#include <atomic>
#include <limits>
#include <span>
@@ -813,13 +814,8 @@ class WiFiComponent final : public Component {
}
bool all_networks_hidden_() const {
if (this->sta_.empty())
return false;
for (const auto &ap : this->sta_) {
if (!ap.get_hidden())
return false;
}
return true;
return !this->sta_.empty() &&
std::all_of(this->sta_.begin(), this->sta_.end(), [](const WiFiAP &ap) { return ap.get_hidden(); });
}
bool wifi_loop_();
@@ -62,7 +62,6 @@ static esp_netif_t *s_sta_netif = nullptr; // NOLINT(cppcoreguidelines-avoid
#ifdef USE_WIFI_AP
static esp_netif_t *s_ap_netif = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
#endif // USE_WIFI_AP
static bool s_sta_started = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static bool s_sta_connected = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static bool s_sta_connect_not_found = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static bool s_sta_connect_error = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
@@ -128,6 +127,10 @@ void event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, voi
#if USE_NETWORK_IPV6
} else if (event_base == IP_EVENT && event_id == IP_EVENT_GOT_IP6) {
memcpy(&event.data.ip_got_ip6, event_data, sizeof(ip_event_got_ip6_t));
#endif
#if defined(USE_WIFI_AP_WISP) && !defined(USE_WIFI_AP_WISP_STATIC_DNS)
} else if (event_base == IP_EVENT && event_id == IP_EVENT_ETH_GOT_IP) { // NOLINT(bugprone-branch-clone)
// no data: the bridge rereads the resolvers an ethernet upstream just received
#endif
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_LOST_IP) { // NOLINT(bugprone-branch-clone)
// no data
@@ -1037,7 +1040,6 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
ESP_LOGW(TAG, "esp_netif_set_hostname failed: %s", esp_err_to_name(err));
}
s_sta_started = true;
// re-apply power save mode
wifi_apply_power_save_();
#ifdef SOC_WIFI_SUPPORT_5G
@@ -1046,7 +1048,6 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
} else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_STOP) {
ESP_LOGV(TAG, "STA stop");
s_sta_started = false;
s_sta_connecting = false;
} else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_AUTHMODE_CHANGE) {
@@ -1126,6 +1127,10 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
this->notify_disconnect_state_listeners_();
#endif
#if defined(USE_WIFI_AP_WISP) && !defined(USE_WIFI_AP_WISP_STATIC_DNS)
} else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_ETH_GOT_IP) {
this->apply_ap_dns_();
#endif
} else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_STA_GOT_IP) {
const auto &it = data->data.ip_got_ip;
ESP_LOGV(TAG, "static_ip=" IPSTR " gateway=" IPSTR, IP2STR(&it.ip_info.ip), IP2STR(&it.ip_info.gw));
@@ -1400,10 +1405,6 @@ bool WiFiComponent::wifi_scan_start_(bool passive) {
bool WiFiComponent::wifi_ap_ip_config_(const optional<ManualIP> &manual_ip) {
esp_err_t err;
// enable AP
if (!this->wifi_mode_({}, true))
return false;
// Check if the AP interface is initialized before using it
if (s_ap_netif == nullptr) {
ESP_LOGW(TAG, "AP interface not initialized");
@@ -1481,8 +1482,8 @@ bool WiFiComponent::wifi_ap_ip_config_(const optional<ManualIP> &manual_ip) {
#endif
err = esp_netif_dhcps_start(s_ap_netif);
if (err != ESP_OK) {
// Before the access point is up, its start event brings the server up with these settings
if (err != ESP_OK && err != ESP_ERR_ESP_NETIF_IF_NOT_READY) {
ESP_LOGE(TAG, "esp_netif_dhcps_start failed: %d", err);
return false;
}
@@ -1491,6 +1492,17 @@ bool WiFiComponent::wifi_ap_ip_config_(const optional<ManualIP> &manual_ip) {
}
bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
// The address and DHCP settings go in before the access point starts: its start event starts the DHCP
// server on the event task, and settings written after that race it.
#ifdef USE_WIFI_MANUAL_IP
if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) {
#else
if (!this->wifi_ap_ip_config_({})) {
#endif
ESP_LOGE(TAG, "wifi_ap_ip_config_ failed:");
return false;
}
// enable AP
if (!this->wifi_mode_({}, true))
return false;
@@ -1527,19 +1539,6 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
ESP_LOGE(TAG, "esp_wifi_set_config failed: %d", err);
return false;
}
#ifdef USE_WIFI_MANUAL_IP
if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) {
ESP_LOGE(TAG, "wifi_ap_ip_config_ failed:");
return false;
}
#else
if (!this->wifi_ap_ip_config_({})) {
ESP_LOGE(TAG, "wifi_ap_ip_config_ failed:");
return false;
}
#endif
return true;
}
@@ -1684,7 +1683,7 @@ bool WiFiComponent::write_ap_dns_() {
#ifndef USE_WIFI_AP_WISP_STATIC_DNS
void WiFiComponent::apply_ap_dns_() {
if (!this->ap_setup_)
return; // loop() brings the bridge up once the station has a resolver, and its setup writes them
return; // loop() brings the bridge up once the upstream has a resolver, and its setup writes them
// The DHCP server holds what it offers, so there is nothing to cache; a failed update is noticed again with the
// next lease. A resolver the station lost stays offered: set_dns_info rejects an empty address.
bool changed = false;
@@ -166,8 +166,9 @@ WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() const {
return WiFiSTAConnectStatus::ERROR_CONNECT_FAILED;
case CYW43_LINK_NONET:
return WiFiSTAConnectStatus::ERROR_NETWORK_NOT_FOUND;
default:
return WiFiSTAConnectStatus::IDLE;
}
return WiFiSTAConnectStatus::IDLE;
}
int WiFiComponent::s_wifi_scan_result(void *env, const cyw43_ev_scan_result_t *result) {
@@ -5,7 +5,7 @@ namespace esphome::xiaomi_hhccpot002 {
ESPHOME_LOG_TAG(TAG, "xiaomi_hhccpot002");
void XiaomiHHCCPOT002 ::dump_config() {
void XiaomiHHCCPOT002::dump_config() {
ESP_LOGCONFIG(TAG, "Xiaomi HHCCPOT002");
LOG_SENSOR(" ", "Moisture", this->moisture_);
LOG_SENSOR(" ", "Conductivity", this->conductivity_);
@@ -40,8 +40,9 @@ static void advertise(k_work *work) {
truncate_name_to(name, sizeof(name), app_name.c_str(), app_name.length(),
App.is_name_add_mac_suffix_enabled() ? MAC_ADDRESS_SUFFIX_WITH_SEPARATOR_LEN : 0);
const uint8_t name_type = name_len < app_name.length() ? BT_DATA_NAME_SHORTENED : BT_DATA_NAME_COMPLETE;
static constexpr uint8_t FLAGS[] = {BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR};
const bt_data ad[] = {
BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)),
BT_DATA(BT_DATA_FLAGS, FLAGS, sizeof(FLAGS)),
BT_DATA(name_type, name, static_cast<uint8_t>(name_len)),
};
@@ -255,6 +256,8 @@ static const char *role_str(uint8_t role) {
return "Central";
case BT_CONN_ROLE_PERIPHERAL:
return "Peripheral";
default:
break;
}
return "Unknown";
@@ -80,6 +80,8 @@ static const char *swap_type_str(uint8_t type) {
return "revert";
case BOOT_SWAP_TYPE_FAIL:
return "fail";
default:
break;
}
return "unknown";
+18 -11
View File
@@ -658,6 +658,8 @@ class EsphomeCore:
self.main_statements: list[Statement] = []
# A list of statements to insert in the global block (includes and global variables)
self.global_statements: list[Statement] = []
# Generated code emitted after the user includes, so it can use what they declare
self.global_statements_after_includes: list[Statement] = []
# A map of platformio libraries to add to the project (shortname: (name, version, repository))
self.platformio_libraries: dict[str, Library] = {}
# A set of build flags to set in the platformio project
@@ -728,6 +730,7 @@ class EsphomeCore:
self.variables = {}
self.main_statements = []
self.global_statements = []
self.global_statements_after_includes = []
self.platformio_libraries = {}
self.build_flags = set()
self.cmake_args = {}
@@ -1045,8 +1048,8 @@ class EsphomeCore:
except RuntimeError as e:
raise EsphomeError(str(e)) from e
def add(self, expression, prepend=False) -> "Statement":
"""Add an expression or statement to the main setup() block."""
@staticmethod
def _as_statement(expression) -> "Statement":
from esphome.cpp_generator import Expression, Statement, statement
if isinstance(expression, Expression):
@@ -1055,7 +1058,11 @@ class EsphomeCore:
raise ValueError(
f"Add '{expression}' must be expression or statement, not {type(expression)}"
)
return expression
def add(self, expression, prepend=False) -> "Statement":
"""Add an expression or statement to the main setup() block."""
expression = self._as_statement(expression)
if prepend:
self.main_statements.insert(0, expression)
else:
@@ -1064,14 +1071,7 @@ class EsphomeCore:
return expression
def add_global(self, expression, prepend=False) -> "Statement":
from esphome.cpp_generator import Expression, Statement, statement
if isinstance(expression, Expression):
expression = statement(expression)
if not isinstance(expression, Statement):
raise ValueError(
f"Add '{expression}' must be expression or statement, not {type(expression)}"
)
expression = self._as_statement(expression)
if prepend:
self.global_statements.insert(0, expression)
else:
@@ -1079,6 +1079,13 @@ class EsphomeCore:
_LOGGER.debug("Adding global: %s", expression)
return expression
def add_global_after_includes(self, expression) -> "Statement":
"""Add a global after the user includes, for generated code that may use them."""
expression = self._as_statement(expression)
self.global_statements_after_includes.append(expression)
_LOGGER.debug("Adding global after includes: %s", expression)
return expression
def add_library(self, library: Library):
if not isinstance(library, Library):
raise TypeError(
@@ -1258,7 +1265,7 @@ class EsphomeCore:
from esphome.cpp_generator import statement
global_code = []
for exp in self.global_statements:
for exp in (*self.global_statements, *self.global_statements_after_includes):
text = str(statement(exp))
text = text.rstrip()
global_code.append(text)
+2 -1
View File
@@ -590,7 +590,8 @@ async def add_arduino_global_workaround():
@coroutine_with_priority(CoroPriority.FINAL)
async def add_includes(includes: list[str], is_c_header: bool = False) -> None:
# Add includes at the very end, so that the included files can access global variables
# After the id declarations, so the headers can use id(x); the generated function bodies
# and flash tables come later still, so their lambdas can use the headers.
for include in includes:
path = CORE.relative_config_path(include)
if path.is_dir():
+4 -2
View File
@@ -59,13 +59,15 @@ template<typename T> class PlainAtomic {
constexpr PlainAtomic(T value) : value_(value) {}
T load(std::memory_order order = std::memory_order_seq_cst) const {
T value = value_;
if (order != std::memory_order_relaxed)
if (order != std::memory_order_relaxed) {
ESPHOME_LFQ_COMPILER_BARRIER(); // acquire: later reads may not hoist above this load
}
return value;
}
void store(T value, std::memory_order order = std::memory_order_seq_cst) {
if (order != std::memory_order_relaxed)
if (order != std::memory_order_relaxed) {
ESPHOME_LFQ_COMPILER_BARRIER(); // release: earlier writes may not sink below this store
}
value_ = value;
}
T fetch_add(T amount, std::memory_order /*order*/ = std::memory_order_seq_cst) {
+20 -10
View File
@@ -536,8 +536,10 @@ def shared_progmem_array(
used |= {str(i) for i in CORE.variables}
used |= set(RESERVED_IDS) | CORE.loaded_integrations
id_ = ID(ensure_unique_string(name, used), is_declaration=True, type=type_)
# Global, so any scope can use it; anything a lambda references is already declared.
CORE.add_global(ProgmemAssignmentExpression(type_, id_, rhs, constexpr))
# After the user includes, so a lambda in the table can use what their headers declare.
CORE.add_global_after_includes(
ProgmemAssignmentExpression(type_, id_, rhs, constexpr)
)
array = MockObj(id_, ".")
CORE.register_variable(id_, array)
if share:
@@ -789,15 +791,16 @@ def static_function(
"""Emit ``static <return_type> <name>(parameters) { body }`` at global scope and return an
expression naming it, for use as a template argument or a function pointer.
Every id the body names must already be declared, which holds when the statements were
rendered through ``get_variable`` or ``process_lambda``.
The declaration goes with the other globals, so a template argument can name the function
at once; the body goes after the user includes, where every id and everything those
headers declare is in scope.
"""
params = ParameterListExpression(*parameters)
add_global(
RawStatement(
f"static {safe_exp(return_type)} {name}({params}) {{\n"
f"{indent(chr(10).join(body))}\n}}"
)
signature = (
f"static {safe_exp(return_type)} {name}({ParameterListExpression(*parameters)})"
)
add_global(RawStatement(f"{signature};"))
add_global_after_includes(
RawStatement(f"{signature} {{\n{indent(chr(10).join(body))}\n}}")
)
return RawExpression(name)
@@ -816,6 +819,13 @@ def add_global(expression: SafeExpType | Statement, prepend: bool = False):
CORE.add_global(expression, prepend)
def add_global_after_includes(expression: SafeExpType | Statement):
"""Add a global after the user's ``includes:`` headers, for generated code whose
lambdas may use what those headers declare.
"""
CORE.add_global_after_includes(expression)
def add_library(name: str, version: str | None, repository: str | None = None):
"""Add a library to the codegen library storage.
+2 -2
View File
@@ -48,7 +48,7 @@ lib_deps =
improv/Improv@1.2.7 ; improv_serial / improv_ble
kikuchan98/pngle@1.1.0 ; runtime_image
; Using the repository directly, otherwise ESP-IDF can't use the library
https://github.com/bitbank2/JPEGDEC.git#1.8.4 ; runtime_image
https://github.com/esphome-libs/JPEGDEC.git#1.8.4-esphome.3 ; runtime_image
; This dependency is used only in unit tests.
; Must coincide with PLATFORMIO_GOOGLE_TEST_LIB in scripts/cpp_unit_test.py
; See scripts/cpp_unit_test.py and tests/components/README.md
@@ -253,7 +253,7 @@ lib_deps =
heman/AsyncMqttClient-esphome@2.0.0 ; mqtt
improv/Improv@1.2.7 ; improv_serial
kikuchan98/pngle@1.1.0 ; runtime_image
https://github.com/bitbank2/JPEGDEC.git#1.8.4 ; runtime_image
https://github.com/esphome-libs/JPEGDEC.git#1.8.4-esphome.3 ; runtime_image
build_flags =
${common:arduino.build_flags}
-DUSE_LIBRETINY
+1 -1
View File
@@ -12,7 +12,7 @@ pyserial==3.5
platformio==6.2.0
esptool==5.6.0
click==8.5.0
aioesphomeapi==46.18.0
aioesphomeapi==46.20.0
aiohappyeyeballs==2.7.1 # Happy Eyeballs for requests downloads; already pulled in by aioesphomeapi
zeroconf==0.151.5
puremagic==2.2.0
+2 -2
View File
@@ -1,4 +1,4 @@
# Useful stuff when working in a development environment
clang-format==22.1.8 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py
clang-tidy==22.1.8
clang-format==23.1.3 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py
clang-tidy==23.1.3
yamllint==1.38.0 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py
+2
View File
@@ -2711,6 +2711,8 @@ def build_message_type(
o += " const ProtoFieldValue value(data, scalar);\n"
o += " switch (tag) {\n"
o += indent("\n".join(decode), " ").replace("this->", "msg.") + "\n"
o += " default:\n"
o += " break;\n"
o += " }\n"
o += "}\n"
cpp += o
+8 -14
View File
@@ -319,21 +319,15 @@ def run_tidy(
disabled_checks = []
if args.environment.startswith(("rp2", "bk72xx", "ln882h", "rtl87xx")):
# MMIO peripheral access on these bare-metal platforms is all
# fixed-address.
disabled_checks.append("-clang-analyzer-core.FixedAddressDereference")
# fixed-address. clang-tidy 23 moved the checker under optin.
disabled_checks += [
"-clang-analyzer-core.FixedAddressDereference",
"-clang-analyzer-optin.core.FixedAddressDereference",
]
if args.environment.startswith("host"):
# glibc's assert() expands to a call the check mistakes for a
# constant condition.
disabled_checks += ["-misc-static-assert", "-cert-dcl03-c"]
if args.environment.startswith(("rp2", "bk72xx", "ln882h", "rtl87xx", "host")):
# bugprone-pointer-arithmetic-on-polymorphic-object (and its
# cert-ctr56-cpp alias) crashes clang-tidy 22 with infinite matcher
# recursion on lvgl_esphome.h under the RP2 defines and on the
# all-include file under the host defines.
disabled_checks += [
"-bugprone-pointer-arithmetic-on-polymorphic-object",
"-cert-ctr56-cpp",
]
if os.path.normpath(path) in ANALYZER_SKIPPED_FILES:
disabled_checks.append("-clang-analyzer-*")
if disabled_checks:
@@ -523,7 +517,7 @@ def main():
failed_files = []
timings = []
try:
executable = get_binary("clang-tidy", 22)
executable = get_binary("clang-tidy", 23)
task_queue = queue.Queue(args.jobs)
lock = threading.Lock()
for _ in range(args.jobs):
@@ -571,13 +565,13 @@ def main():
try:
try:
subprocess.call(
["clang-apply-replacements-22", tmpdir], close_fds=False
["clang-apply-replacements-23", tmpdir], close_fds=False
)
except FileNotFoundError:
subprocess.call(["clang-apply-replacements", tmpdir], close_fds=False)
except FileNotFoundError:
print(
"Error please install clang-apply-replacements-22 or clang-apply-replacements.\n",
"Error please install clang-apply-replacements-23 or clang-apply-replacements.\n",
file=sys.stderr,
)
except:
@@ -621,6 +621,69 @@ def test_uc8279_width_must_be_a_multiple_of_8(
)
GDEY029Z95 = "goodisplay-gdey029z95-2.9"
def _gdey029z95_config(**overrides: Any) -> ConfigType:
config: ConfigType = {
"id": "test_display",
"model": GDEY029Z95,
CONF_DC_PIN: 21,
CONF_RESET_PIN: 22,
CONF_BUSY_PIN: 23,
}
config.update(overrides)
return config
def test_gdey029z95_uses_the_good_display_driver() -> None:
"""The GDEY029Z95 uses EPaperGoodDisplay3C with the WeAct init sequence."""
model = MODELS[GDEY029Z95.upper()]
assert model.class_name == "EPaperGoodDisplay3C"
assert model.get_init_sequence({}) == (
(0x01, 0x27, 0x01, 0x00),
(0x11, 0x03),
(0x3C, 0x05),
(0x18, 0x80),
(0x21, 0x00, 0x80),
)
@pytest.mark.parametrize("pin", [CONF_RESET_PIN, CONF_BUSY_PIN])
def test_gdey029z95_requires_reset_and_busy_pins(
set_core_config: SetCoreConfigCallable,
set_component_config: Callable[[str, Any], None],
pin: str,
) -> None:
"""Only a hardware reset wakes the panel from deep sleep, and refreshes are timed by busy."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19})
config = _gdey029z95_config()
del config[pin]
with pytest.raises(cv.Invalid, match=pin):
run_schema_validation(config)
def test_gdey029z95_has_no_partial_update(
set_core_config: SetCoreConfigCallable,
set_component_config: Callable[[str, Any], None],
) -> None:
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19})
run_schema_validation(_gdey029z95_config())
with pytest.raises(cv.Invalid, match="does not support partial update"):
run_schema_validation(_gdey029z95_config(full_update_every=2))
def test_uc8279_x4pro_code_generation(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],

Some files were not shown because too many files have changed in this diff Show More