Compare commits

..
Author SHA1 Message Date
J. Nick Koston 537976a895 Keep the flags helper private and make the identity guard explicit 2026-08-28 16:30:43 -05:00
J. Nick Koston c1ec1f3ee7 Deduplicate the pch identity digest across backends 2026-08-28 16:03:46 -05:00
J. Nick Koston 86fdfe7d78 Merge branch 'esp8266-native-parallel-extract' into espidf-parallel-tool-extract 2026-08-27 23:03:25 -05:00
J. Nick Koston 5ef7e45b6a Merge remote-tracking branch 'origin/esp8266-native-parallel-extract' into esp8266-native-parallel-extract 2026-08-27 23:02:24 -05:00
J. Nick Koston a196d61e0b Merge branch 'nrf52-pch' into esp8266-native-parallel-extract 2026-08-27 23:01:19 -05:00
J. Nick Koston 33885bfddf Merge branch 'esp32-pio-pch' into nrf52-pch 2026-08-27 23:01:19 -05:00
J. Nick Koston 853ef14f10 Merge branch 'host-pch' into esp32-pio-pch 2026-08-27 23:01:19 -05:00
J. Nick Koston addcb472af Merge branch 'pch-strict-ci' into host-pch 2026-08-27 23:01:19 -05:00
J. Nick Koston 4ecbde8bbc Merge branch 'platformio-pch-libretiny' into pch-strict-ci 2026-08-27 23:01:19 -05:00
J. Nick Koston 9794eb9fc2 Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-27 23:01:18 -05:00
J. Nick Koston fc41843cd5 Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-27 23:01:18 -05:00
J. Nick Koston 0d50b57414 Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-27 23:01:18 -05:00
J. Nick Koston d09e0e3f6f Skip unverifiable archives and count distinct tools 2026-08-27 21:24:40 -05:00
J. Nick Koston f7b30d6b85 Count prefetched archives when skipping the parallel pass 2026-08-27 21:07:02 -05:00
J. Nick Koston 7ecb9d3b73 Print per-tool completion during parallel extraction 2026-08-27 20:09:58 -05:00
J. Nick Koston 1fe3475f18 Polish pre-extraction failure reporting 2026-08-27 20:02:20 -05:00
J. Nick Koston 5da2cca3e7 Clarify pre-extraction failure messages 2026-08-27 19:30:29 -05:00
J. Nick Koston 083c35bccd Cancel queued extractions on interrupt and name cached archives 2026-08-27 19:21:20 -05:00
J. Nick Koston ce15f9a331 Verify pre-existing tool archives and escalate surviving torn dirs 2026-08-27 19:21:20 -05:00
J. Nick Koston 9ea429f0ef Clean torn tool dirs and escalate total pre-extraction failure 2026-08-27 19:21:20 -05:00
J. Nick Koston aaafc1753a Share tool resolution between the idf_tools helper scripts 2026-08-27 19:21:20 -05:00
J. Nick Koston c1c9408035 Extract prefetched ESP-IDF tool archives in parallel 2026-08-27 19:21:19 -05:00
J. Nick Koston b2e626d484 Run the sequential remainder after the batch 2026-08-27 19:21:15 -05:00
J. Nick Koston 2b64db9942 Announce size-less refetches and name every install failure 2026-08-27 18:46:09 -05:00
J. Nick Koston 2c6ff030d9 Simplify batch failure logging and dedupe test scaffolding 2026-08-27 18:23:49 -05:00
J. Nick Koston 85a33d5d7f Announce batched refetches and cap preinstall workers 2026-08-27 18:17:29 -05:00
J. Nick Koston 31aae57b2d Trim docstrings and comments 2026-08-27 18:03:20 -05:00
J. Nick Koston a53ffa781a Cap extraction workers and keep batch failures traceable 2026-08-27 17:59:07 -05:00
J. Nick Koston 563083b4a9 Extract native toolchain package archives in parallel 2026-08-27 17:30:29 -05:00
J. Nick Koston 1f77384389 Merge branch 'esp32-pio-pch' into nrf52-pch 2026-08-27 15:53:00 -05:00
J. Nick Koston b97ba73757 Merge branch 'host-pch' into esp32-pio-pch 2026-08-27 15:53:00 -05:00
J. Nick Koston b5c78a4711 Merge branch 'pch-strict-ci' into host-pch 2026-08-27 15:53:00 -05:00
J. Nick Koston a114147e03 Merge branch 'platformio-pch-libretiny' into pch-strict-ci 2026-08-27 15:53:00 -05:00
J. Nick Koston 80b592fa16 Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-27 15:52:59 -05:00
J. Nick Koston 665f1d46ca Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-27 15:52:59 -05:00
J. Nick Koston 5f97c5756b Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-27 15:52:59 -05:00
J. Nick Koston 02761d72ab Merge branch 'esp32-pio-pch' into nrf52-pch 2026-08-27 14:04:03 -05:00
J. Nick Koston 5f9af0cfa5 Merge branch 'host-pch' into esp32-pio-pch 2026-08-27 14:04:03 -05:00
J. Nick Koston 5bc3102c82 Merge branch 'pch-strict-ci' into host-pch 2026-08-27 14:04:02 -05:00
J. Nick Koston a5e4882196 Merge branch 'platformio-pch-libretiny' into pch-strict-ci 2026-08-27 14:04:02 -05:00
J. Nick Koston 4f9a02f85a Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-27 14:04:02 -05:00
J. Nick Koston 8f1e581bf6 Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-27 14:04:02 -05:00
J. Nick Koston 8f8e6e95d4 Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-27 14:04:02 -05:00
J. Nick Koston 52879b8a7f Skip the doomed gch compile after header-generation failure, self-heal the placeholder from CMake 2026-08-27 13:43:36 -05:00
J. Nick Koston 330adb9ba4 Cover the placeholder-touch failure path 2026-08-27 12:53:06 -05:00
J. Nick Koston e3c1d51f4f Merge branch 'esp32-pio-pch' into nrf52-pch 2026-08-27 12:46:19 -05:00
J. Nick Koston 640f2de767 Merge user platformio option strings into list options, scope the esp32 PlatformIO trigger to the pch modules 2026-08-27 12:45:59 -05:00
J. Nick Koston f180c95a65 Merge remote-tracking branch 'origin/nrf52-pch' into nrf52-pch 2026-08-27 12:42:50 -05:00
J. Nick Koston aa25b5fb0b Annotate the compile_ctx fixture 2026-08-27 12:42:36 -05:00
J. Nick Koston c8b54a33a9 Merge remote-tracking branch 'origin/nrf52-pch' into nrf52-pch 2026-08-27 12:39:40 -05:00
J. Nick Koston 17d1874ca3 Degrade on header-generation failure, surface probe and touch errors 2026-08-27 12:38:42 -05:00
J. Nick Koston c4c6fe198e Merge branch 'pch-strict-ci' into host-pch 2026-08-27 12:38:39 -05:00
J. Nick Koston 7850fba837 Merge branch 'platformio-pch-libretiny' into pch-strict-ci 2026-08-27 12:38:39 -05:00
J. Nick Koston f66eb89f28 Merge branch 'host-pch' into esp32-pio-pch 2026-08-27 12:38:39 -05:00
J. Nick Koston 168c772e79 Merge branch 'esp32-pio-pch' into nrf52-pch 2026-08-27 12:38:39 -05:00
J. Nick Koston c2eb877a17 Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-27 12:38:32 -05:00
J. Nick Koston 72c670f5f1 Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-27 12:38:31 -05:00
J. Nick Koston ecc9248426 Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-27 12:38:31 -05:00
J. Nick Koston e56edc4af0 Drop the relative-path branch; compile databases record absolute paths 2026-08-27 12:37:01 -05:00
J. Nick Koston c6b1a41e76 Match the pch source TU by resolved path so symlinked build dirs keep the pch 2026-08-27 12:31:53 -05:00
J. Nick Koston a638ac3982 Trim pch comments to repo standard, use partial for the prepare thunk 2026-08-27 11:05:10 -05:00
J. Nick Koston d45e0074a3 Share the guarded pch prepare contract, simplify wiring and tests 2026-08-27 11:01:46 -05:00
J. Nick Koston cf084bc70f Share the CMake pch consumer block, apply review suggestions 2026-08-27 10:53:26 -05:00
J. Nick Koston 21c1c582b5 Generate the Zephyr syscall headers before compiling the pch 2026-08-27 10:44:38 -05:00
J. Nick Koston 8defc48611 Resolve the sysbuild app domain dir for the pch 2026-08-27 10:34:17 -05:00
J. Nick Koston 10ea99d4c9 Add precompiled header support to the nRF52 sdk-nrf build 2026-08-27 10:25:45 -05:00
J. Nick Koston a4136c8a17 Run copy_files to completion in the gate test, pin the build_helpers trigger 2026-08-27 10:11:55 -05:00
J. Nick Koston de0f706da4 Merge branch 'host-pch' into esp32-pio-pch 2026-08-27 09:41:01 -05:00
J. Nick Koston 41d68d2978 Merge branch 'pch-strict-ci' into host-pch 2026-08-27 09:40:58 -05:00
J. Nick Koston 59556a730d Keep the parity test hermetic, give touch the shared error contract 2026-08-27 09:40:54 -05:00
J. Nick Koston b367238273 Adopt the shared prefix constant everywhere, trigger CI on build_helpers, pin the copy_files gate 2026-08-27 09:31:31 -05:00
J. Nick Koston 5ab690cca6 Merge branch 'host-pch' into esp32-pio-pch 2026-08-27 09:13:57 -05:00
J. Nick Koston 7d4e28b5e2 Merge branch 'pch-strict-ci' into host-pch 2026-08-27 09:13:55 -05:00
J. Nick Koston 87cbe42cae Blame the baseline honestly, split sidecar unlinks, pin the script's knob and import branches 2026-08-27 09:13:51 -05:00
J. Nick Koston b4d6633b19 Enable the pch on esp32 via PlatformIO for CI speed 2026-08-27 08:47:05 -05:00
J. Nick Koston 8180134d30 Merge branch 'pch-strict-ci' into host-pch 2026-08-27 08:40:39 -05:00
J. Nick Koston 8a5d53899c Simplify: strict knob via parse_enable_env, one probe tail, latch folded into _fail, discard owns its policy 2026-08-27 08:40:35 -05:00
J. Nick Koston 92b71f8ce4 Exclude the prefix wrapper from esphome.h, state the failure-path cost honestly 2026-08-27 08:33:25 -05:00
J. Nick Koston 4605ac08a5 Merge branch 'pch-strict-ci' into host-pch 2026-08-27 08:32:13 -05:00
J. Nick Koston 3e5688d6cd Blame the pch only when a baseline passes, validate the strict knob, abort on an undiscardable stale header 2026-08-27 08:32:01 -05:00
J. Nick Koston 9580fd14da Merge branch 'pch-strict-ci' into host-pch 2026-08-27 00:40:42 -05:00
J. Nick Koston d38542e48b Escalate all strict consumers, keep cached-probe rejections unlatched, drop the dead probe knob 2026-08-27 00:40:37 -05:00
J. Nick Koston 364c530832 Merge branch 'pch-strict-ci' into host-pch 2026-08-27 00:26:00 -05:00
J. Nick Koston c89c2d975d Strict escalates consumer invalid-pch, re-probes cached headers everywhere, and defaults the matrix to strict 2026-08-27 00:25:38 -05:00
J. Nick Koston a0c3a010e4 Guard the prefix for C edges, gate it on the pch knob, pin its resolution 2026-08-27 00:21:14 -05:00
J. Nick Koston fd871b8898 Precompile a curated core-header prefix on host 2026-08-27 00:10:07 -05:00
J. Nick Koston 78eff4f485 Strict re-probes a cached .gch, probe diagnostics get the exit-code fallback, nobuild detection narrows to ImportError 2026-08-26 23:43:09 -05:00
J. Nick Koston 833af674bf Simplify: shared tool-step runner, probe args helper, buildtool touch stamp, knob-spelling parity 2026-08-26 23:20:06 -05:00
J. Nick Koston 15d4f187c7 Bake the probe stamp into the rule text; edge bindings cannot expand $out 2026-08-26 23:10:55 -05:00
J. Nick Koston 21643396c4 Add an esp8266 native-toolchain smoke entry to the strict compile matrix 2026-08-26 23:03:00 -05:00
J. Nick Koston 34daa6138a Probe failures follow the compile branch's transient rules, strict on the native esp8266 job 2026-08-26 23:02:18 -05:00
J. Nick Koston 0266b7f0fb Cover the probe spawn failure and both probe-edge flag branches 2026-08-26 22:55:52 -05:00
J. Nick Koston 5d690104b5 Strict fails on a disabled pch, narrows the projenv exemption, and covers the new funnel sites 2026-08-26 22:51:53 -05:00
J. Nick Koston 4a3b095172 Probe the built .gch on the native paths so strict CI reds on an unloadable pch 2026-08-26 22:47:42 -05:00
J. Nick Koston 54bff55642 Reraise in strict through the IDF flow, funnel the remaining degrade paths, exempt nobuild 2026-08-26 22:42:03 -05:00
J. Nick Koston 264ccc0989 Add ESPHOME_PCH_STRICT and enforce it in the esp8266/esp32/rp2 image compile tests 2026-08-26 22:29:53 -05:00
J. Nick Koston 7449623022 Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-26 22:23:01 -05:00
J. Nick Koston 2eab26052a Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-26 22:22:58 -05:00
J. Nick Koston 96beeed2c2 Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-26 22:22:55 -05:00
J. Nick Koston 1e13390b6d Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-26 22:06:04 -05:00
J. Nick Koston c916e93988 Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-26 22:06:04 -05:00
J. Nick Koston 3ecd4e3e69 Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-26 22:06:04 -05:00
J. Nick Koston 3e3655036b Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-26 14:23:26 -05:00
J. Nick Koston 58d6aec7fd Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-26 14:23:23 -05:00
J. Nick Koston c97250508a Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-26 14:23:20 -05:00
J. Nick Koston 16e4e05d92 Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-26 14:14:08 -05:00
J. Nick Koston 153bfbc47c Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-26 14:14:05 -05:00
J. Nick Koston 50c9a3a5bb Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-26 14:14:02 -05:00
J. Nick Koston c5d14b9423 Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-26 13:21:30 -05:00
J. Nick Koston 694741cd6b Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-26 13:21:28 -05:00
J. Nick Koston 1719e75e57 Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-26 13:21:25 -05:00
J. Nick Koston c2d1b1c583 Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-26 13:14:22 -05:00
J. Nick Koston 198e4abc5d Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-26 13:14:17 -05:00
J. Nick Koston 35ab90fc58 Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-26 13:14:11 -05:00
J. Nick Koston 77fc8af9f0 Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-26 11:59:22 -05:00
J. Nick Koston b8675a4b56 Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-26 11:59:18 -05:00
J. Nick Koston b497ff17a7 Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-26 11:58:36 -05:00
J. Nick Koston f4199a7510 Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-26 10:44:37 -05:00
J. Nick Koston c7f0e17409 Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-26 10:44:35 -05:00
J. Nick Koston 0ba8e3d83e Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-26 10:44:32 -05:00
J. Nick Koston c6d056b240 Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-26 10:03:33 -05:00
J. Nick Koston ca81777eba Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-26 10:03:31 -05:00
J. Nick Koston 68f2dc3219 Gate the ccache sloppiness on emission for the IDF backend too 2026-08-26 10:03:26 -05:00
J. Nick Koston c7fb26a6d1 Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-26 10:01:27 -05:00
J. Nick Koston f665ecf977 Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-26 09:58:06 -05:00
J. Nick Koston 5e95306bcc Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-26 09:58:02 -05:00
J. Nick Koston c813fe8e35 Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-26 09:57:59 -05:00
J. Nick Koston c025b875d1 Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-26 09:34:43 -05:00
J. Nick Koston 6a5a092f03 Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-26 09:34:41 -05:00
J. Nick Koston c10fb88430 Trim comments to repo standard 2026-08-26 09:34:37 -05:00
J. Nick Koston 5be8ad98e8 Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-26 09:33:15 -05:00
J. Nick Koston 639e1c2fdf Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-26 09:08:08 -05:00
J. Nick Koston 6722ec22ab Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-26 09:08:05 -05:00
J. Nick Koston 3d35a111ac Pin the compile locale in the generic pch flow too 2026-08-26 09:08:02 -05:00
J. Nick Koston a07bdf3a03 Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-26 09:07:56 -05:00
J. Nick Koston e2446cad8f Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-26 08:40:41 -05:00
J. Nick Koston 404cf7b728 Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-26 08:40:38 -05:00
J. Nick Koston 987da8f5c4 Treat resource exhaustion as transient in the generic pch flow too 2026-08-26 08:40:35 -05:00
J. Nick Koston c9963881ba Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-26 08:39:47 -05:00
J. Nick Koston 2a7245ac79 Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-26 00:46:51 -05:00
J. Nick Koston 1ca8b7066c Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-26 00:46:48 -05:00
J. Nick Koston e13aa8b775 Read the pch sidecars defensively in the generic flow too 2026-08-26 00:46:45 -05:00
J. Nick Koston 3a483e3284 Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-26 00:46:31 -05:00
J. Nick Koston 0fea10f263 Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-26 00:06:44 -05:00
J. Nick Koston 187199e9b3 Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-26 00:06:43 -05:00
J. Nick Koston fb5a042dfe Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-26 00:06:43 -05:00
J. Nick Koston a892f47f89 Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-25 23:42:31 -05:00
J. Nick Koston 9fa031f134 Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-25 23:42:31 -05:00
J. Nick Koston b255e77e76 Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-25 23:42:30 -05:00
J. Nick Koston bffbc6dce5 Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-25 23:16:53 -05:00
J. Nick Koston ba9131324a Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-25 23:16:53 -05:00
J. Nick Koston 5ab40084c9 Merge remote-tracking branch 'origin/platformio-pch-libretiny' into platformio-pch-libretiny 2026-08-25 23:16:53 -05:00
J. Nick Koston 82b9575b6f Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-25 23:16:52 -05:00
J. Nick Koston 6223ecda32 Merge remote-tracking branch 'origin/platformio-pch-rp2' into platformio-pch-rp2 2026-08-25 23:16:52 -05:00
J. Nick Koston 5bd7884ac1 Merge remote-tracking branch 'origin/esp32-idf-pch' into esp32-idf-pch 2026-08-25 23:16:52 -05:00
J. Nick Koston c1c78ebf18 Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-25 23:16:33 -05:00
J. Nick Koston dd05cdd2f8 Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-25 23:16:33 -05:00
J. Nick Koston cfcbf5b420 Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-25 23:16:33 -05:00
J. Nick Koston 80206fd232 Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-25 23:03:16 -05:00
J. Nick Koston e18bcc74f3 Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-25 23:03:13 -05:00
J. Nick Koston 40f74815f0 Widen the identity-unknown guard to UnicodeError in the generic prepare_pch 2026-08-25 23:03:11 -05:00
J. Nick Koston eb28f2630a Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-25 23:02:56 -05:00
J. Nick Koston 5b53eb9f6c Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-25 22:55:24 -05:00
J. Nick Koston 0768a1c8cf Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-25 22:55:24 -05:00
J. Nick Koston 32c1125116 Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-25 22:55:24 -05:00
J. Nick Koston 5cd9e77f3d Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-25 22:53:03 -05:00
J. Nick Koston 5a05184ee3 Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-25 22:53:03 -05:00
J. Nick Koston a9284c7d4b Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-25 22:53:03 -05:00
J. Nick Koston 53a3501bdc Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-25 22:40:46 -05:00
J. Nick Koston bcdefa873b Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-25 22:40:46 -05:00
J. Nick Koston 6c86c02769 Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-25 22:40:46 -05:00
J. Nick Koston 9304661da3 Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-25 22:39:45 -05:00
J. Nick Koston fe95bd2d0c Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-25 22:39:45 -05:00
J. Nick Koston 2a45a21ad3 Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-25 22:39:45 -05:00
J. Nick Koston 85d49bd940 Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-25 22:29:40 -05:00
J. Nick Koston 36062184a1 Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-25 22:29:40 -05:00
J. Nick Koston 2864ea8c9a Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-25 22:29:40 -05:00
J. Nick Koston 5f81a04086 Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-25 22:23:08 -05:00
J. Nick Koston ac176636db Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-25 22:23:08 -05:00
J. Nick Koston aebcc71201 Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-25 22:23:07 -05:00
J. Nick Koston 68ba1b9a7f Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-25 22:04:06 -05:00
J. Nick Koston 63e6f9bf70 Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-25 22:04:06 -05:00
J. Nick Koston daccadca3f Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-25 22:04:06 -05:00
J. Nick Koston f4a5671445 Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-25 21:45:12 -05:00
J. Nick Koston 2beabb2ca3 Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-25 21:45:09 -05:00
J. Nick Koston d2c2414425 Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-25 21:45:06 -05:00
J. Nick Koston c6f21e2447 Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-25 21:33:57 -05:00
J. Nick Koston 9be7585000 Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-25 21:33:56 -05:00
J. Nick Koston b253f32788 Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-25 21:33:56 -05:00
J. Nick Koston 80c9ccd657 Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-25 20:56:19 -05:00
J. Nick Koston c405d94f45 Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-25 20:56:16 -05:00
J. Nick Koston f4003969ba Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-25 20:56:13 -05:00
J. Nick Koston 0194e2349c Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-25 20:48:39 -05:00
J. Nick Koston cf2ddde04c Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-25 20:48:37 -05:00
J. Nick Koston 365dcba506 Skip compile-DB entries with null fields instead of raising 2026-08-25 20:48:28 -05:00
J. Nick Koston 565ffad6f4 Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-25 20:43:51 -05:00
J. Nick Koston 2145f50bb5 Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-25 20:43:48 -05:00
J. Nick Koston a7e4cf536b Drop duplicate ccache test, keep full compiler output at debug, guard empty compile-DB directory 2026-08-25 20:43:37 -05:00
J. Nick Koston 62f8451831 Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-25 20:40:38 -05:00
J. Nick Koston 120ac5e516 Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-25 20:40:35 -05:00
J. Nick Koston eaa63e2695 Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-25 20:40:32 -05:00
J. Nick Koston 259824d13d Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-25 20:26:29 -05:00
J. Nick Koston daf273b650 Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-25 20:26:29 -05:00
J. Nick Koston faa93cc6e9 Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-25 20:26:29 -05:00
J. Nick Koston 90c804886d Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-25 20:00:39 -05:00
J. Nick Koston e6e738775b Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-25 20:00:37 -05:00
J. Nick Koston 791020f778 Merge follow-through: discard the pch when its identity cannot be established 2026-08-25 20:00:27 -05:00
J. Nick Koston 0c5cad19dc Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-25 19:59:34 -05:00
J. Nick Koston c15bafacfb Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-25 19:24:00 -05:00
J. Nick Koston 7bd2dd944c Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-25 19:23:58 -05:00
J. Nick Koston 84028e787a Log the pch line only when true, fail closed on unreadable sdkconfig, treat signal-killed compiles as transient 2026-08-25 19:23:49 -05:00
J. Nick Koston 5d745f0605 Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-25 19:08:39 -05:00
J. Nick Koston 5672302b01 Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-25 19:08:37 -05:00
J. Nick Koston 9faddeb0bc Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-25 19:08:34 -05:00
J. Nick Koston e25ffaf017 Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-25 18:55:03 -05:00
J. Nick Koston 80b62ceacc Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-25 18:55:00 -05:00
J. Nick Koston 81bca96e1b Run the gch compile from its resolution dir, self-clean when disabled, log the opt-out hint, pin the knob in tests 2026-08-25 18:54:48 -05:00
J. Nick Koston 256457384c Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-25 18:20:37 -05:00
J. Nick Koston d5609f8dc6 Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-25 18:20:36 -05:00
J. Nick Koston a4dd668e69 Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-25 18:20:36 -05:00
J. Nick Koston d08bb3992a Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-25 18:19:27 -05:00
J. Nick Koston 8197fcd5a1 Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-25 18:19:27 -05:00
J. Nick Koston 85520f520e Merge remote-tracking branch 'origin/platformio-pch-libretiny' into platformio-pch-libretiny 2026-08-25 18:19:27 -05:00
J. Nick Koston 2dd850b990 Merge remote-tracking branch 'origin/platformio-pch-rp2' into platformio-pch-rp2 2026-08-25 18:19:27 -05:00
J. Nick Koston d0bbd40e3b Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-25 18:19:26 -05:00
J. Nick Koston 7c9e7a807d Merge remote-tracking branch 'origin/esp32-idf-pch' into esp32-idf-pch 2026-08-25 18:19:26 -05:00
J. Nick Koston b11b60dd86 Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-25 18:18:24 -05:00
J. Nick Koston efeb608725 Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-25 18:18:24 -05:00
J. Nick Koston bbd3e60d5b Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-25 18:18:23 -05:00
J. Nick Koston 600dab7185 Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-25 18:13:33 -05:00
J. Nick Koston 609d9dfab3 Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-25 18:13:31 -05:00
J. Nick Koston 8118575491 Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-25 18:13:29 -05:00
J. Nick Koston a28086547f Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-25 18:05:34 -05:00
J. Nick Koston 0cdd654cc6 Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-25 18:05:31 -05:00
J. Nick Koston 3e2dc30c06 Move the curated header list to build_helpers as PCH_DEFAULT_HEADERS 2026-08-25 18:05:23 -05:00
J. Nick Koston a400753d96 Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-25 18:03:02 -05:00
J. Nick Koston 0f5050a617 Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-25 18:03:00 -05:00
J. Nick Koston 7387211cf1 Move the generic pch build machinery into build_helpers/pch.py 2026-08-25 18:02:51 -05:00
J. Nick Koston 4819f986bc Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-25 17:58:31 -05:00
J. Nick Koston 904083d531 Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-25 17:58:29 -05:00
J. Nick Koston 2a41892559 Simplify: reuse discard_pch, effective_ccache_basedir, and idedata's C++ suffixes 2026-08-25 17:58:05 -05:00
J. Nick Koston e1eff7447d Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-25 17:58:05 -05:00
J. Nick Koston d8255cbece Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-25 17:48:14 -05:00
J. Nick Koston 6175ff24fe Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-25 17:48:12 -05:00
J. Nick Koston 1059ecf50c Fold header order into pch checksum, gate rebuild-forcing touch, guard malformed compile DBs 2026-08-25 17:48:01 -05:00
J. Nick Koston c5c7b598c9 Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-25 17:45:02 -05:00
J. Nick Koston 10a81076b2 Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-25 16:20:46 -05:00
J. Nick Koston 9a2644e6e6 Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-25 16:20:43 -05:00
J. Nick Koston 743f1dcb09 Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-25 16:20:06 -05:00
J. Nick Koston ceac527259 Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-25 15:54:05 -05:00
J. Nick Koston 0908d1d3c6 Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-25 15:54:02 -05:00
J. Nick Koston 278abf2771 Treat pch spawn errors as transient, keep user force-includes, fold command into checksum 2026-08-25 15:53:54 -05:00
J. Nick Koston 408dcd353a Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-25 15:44:03 -05:00
J. Nick Koston ca9500b5a8 Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-25 15:44:03 -05:00
J. Nick Koston b25e30c0b7 Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-25 15:44:02 -05:00
J. Nick Koston cce6f71076 Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-25 15:38:05 -05:00
J. Nick Koston 36790f78ab Merge branch 'platformio-pch-libretiny' of https://github.com/esphome/esphome into platformio-pch-libretiny 2026-08-25 15:38:04 -05:00
J. Nick Koston f215e3f45b Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-25 15:38:03 -05:00
J. Nick Koston fcaf17dafa Merge branch 'platformio-pch-rp2' of https://github.com/esphome/esphome into platformio-pch-rp2 2026-08-25 15:38:03 -05:00
J. Nick Koston 76ed94fe28 Merge branch 'esp32-idf-pch' of https://github.com/esphome/esphome into esp32-idf-pch 2026-08-25 15:38:02 -05:00
J. Nick Koston be60eca212 Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-25 15:37:16 -05:00
J. Nick Koston b24bfa7552 Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-25 15:37:16 -05:00
J. Nick Koston 1b325a66ce Warn when a consumer ignores the precompiled header 2026-08-25 15:37:15 -05:00
J. Nick Koston 273e5f22db Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-25 15:37:03 -05:00
pre-commit-ci-lite[bot] f198964bc5 apply automatic formatting fixes 2026-08-25 20:32:46 +00:00
pre-commit-ci-lite[bot] 6431f6da35 apply automatic formatting fixes 2026-08-25 20:32:42 +00:00
pre-commit-ci-lite[bot] 4c26f23e76 apply automatic formatting fixes 2026-08-25 20:32:17 +00:00
J. Nick Koston f438e5a19a Merge branch 'platformio-pch-libretiny' of https://github.com/esphome/esphome into platformio-pch-libretiny 2026-08-25 15:29:59 -05:00
J. Nick Koston 92233e0ff2 Merge branch 'platformio-pch-rp2' of https://github.com/esphome/esphome into platformio-pch-rp2 2026-08-25 15:29:58 -05:00
J. Nick Koston 702da28c9c Merge branch 'esp32-idf-pch' of https://github.com/esphome/esphome into esp32-idf-pch 2026-08-25 15:29:58 -05:00
J. Nick Koston 8421fe0170 Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-25 15:29:16 -05:00
J. Nick Koston 91eb7f2f7c Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-25 15:29:16 -05:00
J. Nick Koston 88bcb7ff03 Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-25 15:29:16 -05:00
J. Nick Koston 009e28998d Merge branch 'esp8266-native-pch' into platformio-pch-libretiny 2026-08-25 15:28:36 -05:00
J. Nick Koston e52af0dfa8 Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-25 15:17:14 -05:00
J. Nick Koston 250c5bf606 Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-25 15:17:14 -05:00
J. Nick Koston 649727fcfb Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-25 15:17:14 -05:00
pre-commit-ci-lite[bot] f5fd82bc42 apply automatic formatting fixes 2026-08-25 20:08:23 +00:00
J. Nick Koston 958ea20cc4 Use the shared pch registration helper 2026-08-25 15:06:29 -05:00
J. Nick Koston 243b8a871c Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-25 15:05:58 -05:00
J. Nick Koston 352129e77e Use the shared pch registration helper 2026-08-25 15:05:44 -05:00
J. Nick Koston e0a7347f41 Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-25 15:05:10 -05:00
J. Nick Koston ce37667b1d Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-25 15:05:08 -05:00
J. Nick Koston 0007dfab70 Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-25 14:38:16 -05:00
J. Nick Koston 52176c4d59 Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-25 14:38:16 -05:00
J. Nick Koston aa6e3f74fd Add the OBJECT_DEPENDS edge so a rebuilt gch recompiles its consumers 2026-08-25 14:38:04 -05:00
J. Nick Koston e0662b1551 Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny 2026-08-25 14:29:11 -05:00
J. Nick Koston 61009e1162 Merge branch 'esp32-idf-pch' into platformio-pch-rp2 2026-08-25 14:29:10 -05:00
J. Nick Koston 078e918bdb Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-25 14:29:10 -05:00
J. Nick Koston d5c044c7b3 [libretiny] Enable the PlatformIO precompiled header 2026-08-25 14:23:14 -05:00
J. Nick Koston 94b02b46f4 [rp2] Enable the PlatformIO precompiled header 2026-08-25 14:23:14 -05:00
J. Nick Koston d081632c53 Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-25 14:21:46 -05:00
J. Nick Koston 04648a8044 Merge branch 'esp32-idf-pch' of https://github.com/esphome/esphome into esp32-idf-pch 2026-08-25 14:18:56 -05:00
J. Nick Koston 21c9dd33e0 Use native separators in the pch test fixture paths 2026-08-25 14:18:46 -05:00
J. Nick Koston d3f6cbe14a Patch the direct ccache probe in the usable-binary test 2026-08-25 14:06:32 -05:00
J. Nick Koston a6c4c46930 Merge branch 'esp8266-native-pch' into esp32-idf-pch
# Conflicts:
#	esphome/espidf/framework.py
2026-08-25 14:04:35 -05:00
J. Nick Koston 337ecfb8f9 Select the pch TU by source path, expand response files, and key the failure marker on the command 2026-08-25 13:58:34 -05:00
J. Nick Koston b08615f92f Merge remote-tracking branch 'origin/esp32-idf-pch' into esp32-idf-pch 2026-08-25 13:50:13 -05:00
J. Nick Koston 8625e02b8e Cover the remaining pch branches 2026-08-25 13:50:01 -05:00
J. Nick Koston f2204d155a Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-25 13:46:35 -05:00
J. Nick Koston d46518d890 Cover the pch compile-command and failure paths 2026-08-25 13:45:43 -05:00
J. Nick Koston f866f878de Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-25 13:44:47 -05:00
J. Nick Koston 59fc8dd2bb Merge remote-tracking branch 'origin/esp32-idf-pch' into esp32-idf-pch 2026-08-25 13:39:50 -05:00
J. Nick Koston 149d4d24a1 Merge branch 'esp8266-native-pch' into esp32-idf-pch 2026-08-25 13:39:50 -05:00
J. Nick Koston 35104faaba Merge branch 'esp8266-native-pch' into esp32-idf-pch
# Conflicts:
#	esphome/espidf/framework.py
2026-08-25 13:38:03 -05:00
J. Nick Koston c27f97b8f9 [esp32] Precompile the shared core headers for src compiles 2026-08-25 13:37:42 -05:00
J. Nick Koston 71cf6049aa [esp32] Precompile the shared core headers for src compiles 2026-08-25 13:33:56 -05:00
466 changed files with 15409 additions and 15985 deletions
-16
View File
@@ -1,16 +0,0 @@
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "d=\"${CLAUDE_PROJECT_DIR:-.}\"; [ -x \"$d/venv/bin/python\" ] || { mkdir -p \"$d/.temp\"; env -u VIRTUAL_ENV \"$d/script/setup\" >\"$d/.temp/setup.log\" 2>&1 || echo '{\"systemMessage\":\"script/setup failed; see .temp/setup.log\"}'; }",
"statusMessage": "Setting up dev environment (script/setup)...",
"timeout": 900
}
]
}
]
}
}
@@ -1,50 +0,0 @@
name: Cache clang-tidy idedata
description: >
Cache the clang-tidy idedata and the headers it references under .temp
(headers only, about 30MB per env). Run after restore-python and cache-esp-idf.
inputs:
environment:
description: 'clang-tidy environment (e.g. esp32-idf-tidy).'
required: true
runs:
using: composite
steps:
- name: Compute cache key
id: key
shell: bash
run: |
. venv/bin/activate
[ -n "${{ inputs.environment }}" ] || { echo "::error::cache-clang-tidy-idedata: 'environment' input is empty"; exit 1; }
hash=$(python -c 'import sys; sys.path.insert(0, "script"); from clang_tidy_hash import idedata_cache_hash; print(idedata_cache_hash("${{ inputs.environment }}"))')
pyver=$(python -c 'import platform; print(platform.python_version())')
# Generating idedata is what installs ESP-IDF; never skip it over a missing
# install. This also skips the save, so a dev run that installs ESP-IDF
# warms the idedata cache on the next run.
if [ -d ~/.esphome-idf/frameworks ]; then
echo "skip=false" >> "$GITHUB_OUTPUT"
else
echo "ESP-IDF install missing, not using the clang-tidy idedata cache"
echo "skip=true" >> "$GITHUB_OUTPUT"
fi
echo "key=${{ runner.os }}-tidy-idedata-${{ inputs.environment }}-$hash-py$pyver" >> "$GITHUB_OUTPUT"
{
echo "path<<EOF"
printf '%s\n' '.temp/idedata-*.json' '.temp/idedata-*.hash'
printf '.temp/**/*.%s\n' h hpp hh hxx inc inl ipp tpp
echo "EOF"
} >> "$GITHUB_OUTPUT"
# Mirror cache-esp-idf: write on dev, restore-only on PRs. The post-step
# save only runs when the job succeeded, so a failed generation is never saved.
# Extend the extension list if a component ships extensionless headers.
- name: Cache clang-tidy idedata (write on dev)
if: (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write')) && steps.key.outputs.skip != 'true'
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ steps.key.outputs.path }}
key: ${{ steps.key.outputs.key }}
- name: Cache clang-tidy idedata (restore-only off dev)
if: github.ref != 'refs/heads/dev' && !contains(github.event.pull_request.labels.*.name, 'ci-cache-write') && steps.key.outputs.skip != 'true'
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ steps.key.outputs.path }}
key: ${{ steps.key.outputs.key }}
+5 -21
View File
@@ -26,9 +26,6 @@ runs:
# The native-IDF version is pinned in code, not in any file that feeds the
# other cache keys, so resolve it explicitly. Keying on it means the cache
# invalidates on a version bump (actions/cache never overwrites a key).
# Also key on the Python version: the cached IDF venv links to the
# runner's toolcache interpreter and is reinstalled every run after a
# runner image bump.
id: version
shell: bash
run: |
@@ -39,32 +36,19 @@ runs:
version=$(python -c 'from esphome.components.esp32 import ESP_IDF_FRAMEWORK_VERSION_LOOKUP as L; print(L["recommended"])')
fi
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "python-version=$(python -c 'import platform; print(platform.python_version())')" >> "$GITHUB_OUTPUT"
# Mirror the adjacent PlatformIO cache: only dev-branch runs write the
# shared cache (so it lives in the default-branch scope readable by all
# PRs), and PRs are restore-only -- they never push multi-GB artifacts into
# their own scope / the repo quota (e.g. on a version-bump PR). The
# ci-cache-write label lets a PR write into its own scope to test the hit path;
# that costs about 1GB of the repo cache quota per run, so remove it when done.
# -slim: bump when prune-esp-idf changes what it removes; a key is never overwritten.
# their own scope / the repo quota (e.g. on a version-bump PR).
- name: Cache ESP-IDF install (write on dev)
if: (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write')) && inputs.restore-only != 'true'
if: github.ref == 'refs/heads/dev' && inputs.restore-only != 'true'
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.esphome-idf
key: ${{ runner.os }}-esphome-idf-${{ steps.version.outputs.version }}-py${{ steps.version.outputs.python-version }}-slim
key: ${{ runner.os }}-esphome-idf-${{ steps.version.outputs.version }}
- name: Cache ESP-IDF install (restore-only off dev)
if: github.ref != 'refs/heads/dev' && !contains(github.event.pull_request.labels.*.name, 'ci-cache-write') || inputs.restore-only == 'true'
if: github.ref != 'refs/heads/dev' || inputs.restore-only == 'true'
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.esphome-idf
key: ${{ runner.os }}-esphome-idf-${{ steps.version.outputs.version }}-py${{ steps.version.outputs.python-version }}-slim
# Install explicitly so the prune below sees the toolchains on a cache miss
# too, instead of the install happening inside the first build step.
- name: Install ESP-IDF
shell: bash
run: |
. venv/bin/activate
python -c 'from esphome.espidf.framework import check_esp_idf_install; check_esp_idf_install("${{ steps.version.outputs.version }}")'
- name: Prune ESP-IDF install
uses: ./.github/actions/prune-esp-idf
key: ${{ runner.os }}-esphome-idf-${{ steps.version.outputs.version }}
-34
View File
@@ -1,34 +0,0 @@
name: Prune ESP-IDF install
description: >
Remove the picolibc sysroots (1.1GB of the 3.9GB install) from the native
ESP-IDF toolchains; IDF 5.x links newlib. Skipped when an IDF 6 install is
present, which links picolibc (see esp32/__init__.py).
runs:
using: composite
steps:
- name: Prune picolibc
shell: bash
run: |
shopt -s nullglob
prefix="${ESPHOME_ESP_IDF_PREFIX:-$HOME/.esphome-idf}"
prefix="${prefix/#\~/$HOME}"
for fw in "$prefix"/frameworks/*/; do
case "$(basename "$fw")" in
[6-9].*) echo "IDF $(basename "$fw") installed, keeping picolibc"; exit 0 ;;
esac
done
n=0
for dir in "$prefix"/tools/*-esp-elf/*/*-esp-elf/picolibc; do
echo "Removing $dir ($(du -sh "$dir" | cut -f1))"
rm -rf "$dir"
n=$((n + 1))
done
# The marker rides along in the cache entry so a restored slim tree stays quiet.
if [ "$n" -gt 0 ]; then
touch "$prefix/.picolibc-pruned"
elif [ -d "$prefix/tools" ] && [ ! -f "$prefix/.picolibc-pruned" ]; then
echo "::warning::no picolibc sysroots matched under $prefix/tools"
fi
if [ -d "$prefix" ]; then
du -sh "$prefix"
fi
+13
View File
@@ -21,6 +21,7 @@ on:
- "esphome/core/**"
- "esphome/writer.py"
- "esphome/build_gen/**"
- "esphome/build_helpers/**"
- "esphome/espidf/**"
- "esphome/platformio/**"
- "esphome/components/bk72xx/**"
@@ -197,6 +198,7 @@ jobs:
# the default.
id:
- esp8266-arduino
- esp8266-arduino-native
- esp32-arduino-platformio
- esp32-arduino-esp-idf
- esp32-idf-platformio
@@ -207,6 +209,16 @@ jobs:
- ln882x-arduino
- nrf52
- host
# Strict by default so a new matrix id cannot silently join in the
# degrade-quietly mode the knob exists to catch.
# Opt-outs: libretiny GCC rejects its own pch until a toolchain bump.
include:
- id: bk72xx-arduino
pch_strict: "0"
- id: rtl87xx-arduino
pch_strict: "0"
- id: ln882x-arduino
pch_strict: "0"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Download image artifact
@@ -218,6 +230,7 @@ jobs:
- name: Compile ${{ matrix.id }}
run: |
docker run --rm \
-e ESPHOME_PCH_STRICT="${{ matrix.pch_strict || '1' }}" \
-v "${{ github.workspace }}/docker/test_configs:/config" \
"ghcr.io/esphome/esphome-amd64:${{ needs.check-docker.outputs.tag }}" \
compile "${{ matrix.id }}.yaml"
+7 -101
View File
@@ -92,7 +92,6 @@ jobs:
outputs:
core-ci: ${{ steps.determine.outputs.core-ci }}
integration-tests: ${{ steps.determine.outputs.integration-tests }}
integration-run-all: ${{ steps.determine.outputs.integration-run-all }}
integration-test-buckets: ${{ steps.determine.outputs.integration-test-buckets }}
clang-tidy: ${{ steps.determine.outputs.clang-tidy }}
clang-tidy-mode: ${{ steps.determine.outputs.clang-tidy-mode }}
@@ -155,9 +154,6 @@ jobs:
# Extract individual fields
echo "core-ci=$(echo "$output" | jq -r '.core_ci')" >> $GITHUB_OUTPUT
echo "integration-tests=$(echo "$output" | jq -r '.integration_tests')" >> $GITHUB_OUTPUT
# A missing key must fail here, not silently disable the junit upload
run_all=$(echo "$output" | jq -r 'if has("integration_run_all") then .integration_run_all else error("integration_run_all missing") end')
echo "integration-run-all=${run_all}" >> $GITHUB_OUTPUT
echo "integration-test-buckets=$(echo "$output" | jq -c '.integration_test_buckets')" >> $GITHUB_OUTPUT
echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT
echo "clang-tidy-mode=$(echo "$output" | jq -r '.clang_tidy_mode')" >> $GITHUB_OUTPUT
@@ -389,24 +385,14 @@ jobs:
fail-fast: false
matrix:
bucket: ${{ fromJson(needs.determine-jobs.outputs.integration-test-buckets) }}
env:
# What the cache steps persist; libdeps is excluded (keyed per xdist
# worker and env, it never crosses runs).
INTEGRATION_PIO_CACHE_PATH: |
~/.esphome-integration-tests/platformio/platforms
~/.esphome-integration-tests/platformio/packages
~/.esphome-integration-tests/platformio/appstate.json
~/.esphome-integration-tests/platformio/.cache
~/.esphome-integration-tests/platformio/.esphome.pio.stamp.json
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install apt packages (cached)
# ccache speeds up the host compiles. A cache hit never touches apt
# (mirror outages cannot hang the job); the timeout bounds the cold
# path. Packages and version must match seed-apt-cache exactly.
# libsdl2-dev is needed by the headless display tests, which capture
# screenshots.
# path. Packages and version must match seed-apt-cache exactly;
# libsdl2-dev is unused here and carried only for cache-key parity.
timeout-minutes: 10
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
with:
@@ -417,14 +403,6 @@ jobs:
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.13"
- name: Restore integration PlatformIO cache
# Native platform + toolchain installed by shared_platformio_cache in
# tests/integration/conftest.py; a miss self-heals, so no restore-keys.
id: pio-cache
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ env.INTEGRATION_PIO_CACHE_PATH }}
key: integration-pio-v1-${{ runner.os }}-py${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt', 'tests/integration/fixtures/cache_init.yaml', 'esphome/components/host/__init__.py') }}
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
@@ -462,46 +440,12 @@ jobs:
run: |
. venv/bin/activate
mapfile -t test_files < <(echo "$BUCKET_TESTS" | jq -r '.[]')
if [ "${#test_files[@]}" -eq 0 ]; then
echo "::error::Empty integration test bucket; pytest would collect the whole tree"
exit 1
fi
echo "Bucket ${{ matrix.bucket.name }}: running ${#test_files[@]} integration tests"
pytest -vv --no-cov --tb=native --durations=30 -n auto --dist worksteal \
--junitxml=junit-integration.xml "${test_files[@]}"
- name: Upload test artifacts
# Tests that compare rendered output write the image they actually got here, so a
# failure can be looked at without reproducing the whole build locally.
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: integration-test-artifacts-${{ matrix.bucket.name }}
path: test_artifacts/
if-no-files-found: ignore
retention-days: 7
- name: Upload junit timings
# Consumed by sync-integration-durations.yml through
# script/update_integration_test_durations.py; only full matrix dev
# runs produce usable data.
if: github.ref == 'refs/heads/dev' && needs.determine-jobs.outputs.integration-run-all == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: junit-integration-${{ strategy.job-index }}
path: junit-integration.xml
if-no-files-found: error
# A full cron period of margin for the weekly refresh
retention-days: 14
pytest -vv --no-cov --tb=native --durations=30 -n auto "${test_files[@]}"
- name: Print ccache statistics
# esphome stores the PlatformIO ccache under the machine-global cache
# dir (see _ccache_env() in esphome/platformio/toolchain.py).
run: CCACHE_DIR="$HOME/.cache/esphome/platformio-ccache" ccache -s
- name: Save integration PlatformIO cache
# Bucket 0 only; the others would race the same immutable key.
if: success() && (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write')) && strategy.job-index == 0 && steps.pio-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ env.INTEGRATION_PIO_CACHE_PATH }}
key: ${{ steps.pio-cache.outputs.cache-primary-key }}
import-time:
name: Check import esphome.__main__ time
@@ -735,12 +679,6 @@ jobs:
with:
framework: arduino
- name: Cache clang-tidy idedata
if: matrix.cache_idf
uses: ./.github/actions/cache-clang-tidy-idedata
with:
environment: esp32-arduino-tidy
- name: Cache nRF Connect SDK install
if: matrix.cache_sdk_nrf
uses: ./.github/actions/cache-sdk-nrf
@@ -790,10 +728,6 @@ jobs:
# Also cache libdeps, store them in a ~/.platformio subfolder
PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps
- name: Prune ESP-IDF install before cache save
if: matrix.cache_idf && (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write'))
uses: ./.github/actions/prune-esp-idf
- name: Suggested changes
run: script/ci-suggest-changes ${{ matrix.ignore_errors && '|| true' || '' }}
# yamllint disable-line rule:line-length
@@ -826,11 +760,6 @@ jobs:
- name: Cache ESP-IDF install
uses: ./.github/actions/cache-esp-idf
- name: Cache clang-tidy idedata
uses: ./.github/actions/cache-clang-tidy-idedata
with:
environment: esp32-idf-tidy
- name: Register problem matchers
run: |
echo "::add-matcher::.github/workflows/matchers/gcc.json"
@@ -865,10 +794,6 @@ jobs:
# Also cache libdeps, store them in a ~/.platformio subfolder
PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps
- name: Prune ESP-IDF install before cache save
if: (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write'))
uses: ./.github/actions/prune-esp-idf
- name: Suggested changes
run: script/ci-suggest-changes
if: always()
@@ -914,11 +839,6 @@ jobs:
- name: Cache ESP-IDF install
uses: ./.github/actions/cache-esp-idf
- name: Cache clang-tidy idedata
uses: ./.github/actions/cache-clang-tidy-idedata
with:
environment: esp32-idf-tidy
- name: Register problem matchers
run: |
echo "::add-matcher::.github/workflows/matchers/gcc.json"
@@ -953,10 +873,6 @@ jobs:
# Also cache libdeps, store them in a ~/.platformio subfolder
PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps
- name: Prune ESP-IDF install before cache save
if: (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write'))
uses: ./.github/actions/prune-esp-idf
- name: Suggested changes
run: script/ci-suggest-changes
if: always()
@@ -980,19 +896,16 @@ jobs:
name: Run script/clang-tidy for ESP32 S3
# yamllint disable-line rule:line-length
options: --environment esp32s3-idf-tidy --grep SOC_TEMP_SENSOR_SUPPORTED --grep USE_ESP32_VARIANT_ESP32S3 --grep USE_LOGGER_USB_CDC
tidy_environment: esp32s3-idf-tidy
- id: clang-tidy
name: Run script/clang-tidy for ESP32 P4
# P4 has no native Wi-Fi/BLE; those run over the hosted co-processor,
# so their code paths differ -- lint them under the P4 build too.
# yamllint disable-line rule:line-length
options: --environment esp32p4-idf-tidy --grep USE_ESP32_VARIANT_ESP32P4 --grep USE_ESP32_HOSTED --grep USE_WIFI --grep USE_BLE
tidy_environment: esp32p4-idf-tidy
- id: clang-tidy
name: Run script/clang-tidy for ESP32 C6
# yamllint disable-line rule:line-length
options: --environment esp32c6-idf-tidy --grep SOC_LP_I2C_SUPPORTED --grep USE_ESP32_VARIANT_ESP32C6 --grep USE_OPENTHREAD --grep USE_ZIGBEE
tidy_environment: esp32c6-idf-tidy
steps:
- name: Check out code from GitHub
@@ -1010,11 +923,6 @@ jobs:
- name: Cache ESP-IDF install
uses: ./.github/actions/cache-esp-idf
- name: Cache clang-tidy idedata
uses: ./.github/actions/cache-clang-tidy-idedata
with:
environment: ${{ matrix.tidy_environment }}
- name: Register problem matchers
run: |
echo "::add-matcher::.github/workflows/matchers/gcc.json"
@@ -1048,10 +956,6 @@ jobs:
script/clang-tidy --fix --changed ${{ matrix.options }}
fi
- name: Prune ESP-IDF install before cache save
if: (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write'))
uses: ./.github/actions/prune-esp-idf
- name: Suggested changes
run: script/ci-suggest-changes
if: always()
@@ -1298,8 +1202,10 @@ jobs:
echo ""
# ESP8266 Arduino built directly (no PlatformIO); compile validates
# config first, so a separate config pass is redundant.
python3 script/test_build_components.py -e compile -t esp8266-ard -c "$TEST_COMPONENTS" -f --toolchain arduino --fail-on-no-tests
# config first, so a separate config pass is redundant. Strict pch:
# exercises the native ninja pch (and its probe edge) against real
# component configs; the docker matrix smoke-tests both toolchains.
ESPHOME_PCH_STRICT=1 python3 script/test_build_components.py -e compile -t esp8266-ard -c "$TEST_COMPONENTS" -f --toolchain arduino --fail-on-no-tests
device-builder:
name: Test downstream esphome/device-builder
+2 -2
View File
@@ -56,7 +56,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -84,6 +84,6 @@ jobs:
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
with:
category: "/language:${{matrix.language}}"
@@ -1,98 +0,0 @@
---
name: Refresh integration test durations
on:
workflow_dispatch:
schedule:
- cron: "45 5 * * 1"
# Repo writes (branch push, PR open) happen via the App token minted below,
# so the workflow's GITHUB_TOKEN does not need any write scopes.
permissions:
contents: read
actions: read # gh api / gh run download for the CI junit artifacts
jobs:
sync:
name: Refresh integration test durations
runs-on: ubuntu-latest
if: github.repository == 'esphome/esphome'
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
permission-contents: write # push the sync branch
permission-pull-requests: write # open or refresh the sync PR
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.13"
- name: Refresh from the newest usable dev run
env:
GH_TOKEN: ${{ github.token }}
run: |
# Only full matrix dev runs upload junit-integration-* artifacts
# (see the integration-tests job); the merge script re-checks
# coverage regardless.
# Newest-first candidates via their bucket-0 artifact. Fork PRs run
# their own ci.yml, so name and branch are spoofable; require
# same-repo. Assignment failures trip set -e and fail loudly.
candidates=$(
gh api "repos/${GITHUB_REPOSITORY}/actions/artifacts?name=junit-integration-0&per_page=100" \
--jq '.artifacts[] | select(.expired | not) | .workflow_run
| select(.head_branch == "dev" and .head_repository_id != null
and .head_repository_id == .repository_id)
| .id'
)
# Green runs first, then the rest newest first; a run missing a
# bucket fails the coverage check and the next one is tried
green=""
rest=""
for id in ${candidates}; do
conclusion=$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${id}" --jq '.conclusion // ""')
if [ "${conclusion}" = "success" ]; then
green="${green} ${id}"
elif [ -n "${conclusion}" ]; then
rest="${rest} ${id}"
fi
done
# helpers.py imports colorama; the script needs nothing else
pip install colorama
for id in ${green} ${rest}; do
rm -rf /tmp/junit
if ! gh run download "${id}" --repo "${GITHUB_REPOSITORY}" -p "junit-integration-*" -D /tmp/junit; then
echo "::warning::Could not download artifacts for run ${id}; trying the next"
continue
fi
status=0
python script/update_integration_test_durations.py /tmp/junit || status=$?
if [ "${status}" -eq 0 ]; then
echo "Refreshed from run ${id}"
exit 0
fi
# Only EXIT_LOW_COVERAGE (3) from the script advances to the next run
[ "${status}" -eq 3 ] || exit 1
echo "::warning::Run ${id} covers too few test files; trying the next"
done
echo "::error::No dev CI run with usable junit artifacts in range; the feed is starved"
exit 1
- name: Commit changes
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
commit-message: "[ci] Refresh integration test durations"
committer: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
author: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
branch: sync/integration-durations
delete-branch: true
title: "[ci] Refresh integration test durations"
body-path: .github/PULL_REQUEST_TEMPLATE.md
token: ${{ steps.generate-token.outputs.token }}
-2
View File
@@ -137,8 +137,6 @@ config/
!tests/component_tests/**/config/
tests/build/
tests/.esphome/
# Output kept by failing tests for inspection; uploaded by CI
test_artifacts/
/.temp-clang-tidy.cpp
/.temp/
.pio/
+2 -96
View File
@@ -44,16 +44,6 @@ This document provides essential context for AI models interacting with this pro
## 4. Coding Conventions & Style Guide
**Read the developer documentation before writing a component.** https://developers.esphome.io covers the
component lifecycle, the main loop, and the reasoning behind the rules below in far more depth than this
file does, and it is the authority when they disagree. The most useful starting points:
* https://developers.esphome.io/architecture/components/ - component lifecycle, `setup()`, `loop()`,
setup priorities, and how a component is registered.
* https://developers.esphome.io/architecture/components/advanced/ - choosing between `loop()`,
`set_interval`, `set_timeout` and `defer`; waking the loop from another thread; the RAM cost of each.
* https://developers.esphome.io/contributing/code/ - contribution rules, public API and breaking changes.
* **Formatting:**
* **Python:** Uses `ruff` and `flake8` for linting and formatting. Configuration is in `pyproject.toml`.
* **C++:** Uses `clang-format` for formatting. Configuration is in `.clang-format`.
@@ -152,47 +142,6 @@ file does, and it is the authority when they disagree. The most useful starting
* **Indentation:** Use spaces (two per indentation level), not tabs
* **Type aliases:** Prefer `using type_t = int;` over `typedef int type_t;`
* **Line length:** Wrap lines at no more than 120 characters
* **Timing in `loop()`:** Never call `millis()` in a `loop()` body. The current tick's timestamp is
already cached - use `App.get_loop_component_start_time()` (from `esphome/core/application.h`).
Only reach for `millis()` when you genuinely need sub-tick resolution inside a long operation.
* **The main loop runs every 16 ms.** A rate-limit gate shorter than that does nothing: the check
passes on essentially every pass of the loop, so it costs a comparison and buys nothing. Pick an
interval comfortably coarser than 16 ms, or drop the gate entirely and accept running every loop.
```cpp
// Bad - a 10ms gate against a 16ms loop never holds anything back
static constexpr uint32_t POLL_INTERVAL_MS = 10;
const uint32_t now = millis();
if (now - this->last_poll_ < POLL_INTERVAL_MS)
return;
this->last_poll_ = now;
```
```cpp
// Good - an interval that actually rate limits, off the cached timestamp
static constexpr uint32_t POLL_INTERVAL_MS = 100;
const uint32_t now = App.get_loop_component_start_time();
if (now - this->last_poll_ < POLL_INTERVAL_MS)
return;
this->last_poll_ = now;
```
Pick the primitive by cadence: under 250 ms use a gated `loop()`; 500 ms and above use
`set_interval`. Full reasoning, including why `set_interval` costs more below 500 ms:
https://developers.esphome.io/architecture/components/advanced/#quick-rule-of-thumb
* **Don't override a default with the same value:** if a base class method already returns what you
want, do not override it. `Component::get_setup_priority()` returns `setup_priority::DATA`, so a
component that wants `DATA` should simply leave it alone.
```cpp
// Bad - this is exactly what the base class already does
float get_setup_priority() const override { return setup_priority::DATA; }
```
* **Logging string literals:** wrap literals passed as `%s` arguments in `LOG_STR_LITERAL()` so they
can be stored in flash rather than RAM.
```cpp
// Bad
ESP_LOGV(TAG, "Key %u %s", key, pressed ? "pressed" : "released");
// Good
ESP_LOGV(TAG, "Key %u %s", key, pressed ? LOG_STR_LITERAL("pressed") : LOG_STR_LITERAL("released"));
```
* **Constructor parameters vs setters:** Component properties that are both **required** and **invariant**
(never change after construction) should be constructor parameters rather than set via setter methods.
This makes the dependency explicit and prevents use of the object in an incompletely-initialized state.
@@ -613,33 +562,6 @@ file does, and it is the authority when they disagree. The most useful starting
Use `cg.add_define("MAX_SERVICES", count)` to set the size from Python configuration.
Like `std::array` but with vector-like API (`push_back()`, `size()`) and no STL reallocation code.
**Listener and child-entity registration lists are the most common case, and the most commonly
missed.** A `register_*()` method called once per child at code generation time has a count that
is known at compile time, so it should never be a `std::vector`. Use `cg.slot_counter()`: it
returns a function that each consumer calls once per slot it will occupy, and after every
`to_code` has run it emits the define with the final count. When nothing registers, no define is
emitted and the storage plus its registration method compile out entirely.
```python
# hub component's __init__.py
_request_listener_slot = cg.slot_counter("MY_COMPONENT_LISTENER_COUNT")
async def register_listener(hub: MockObj, var: MockObj) -> None:
_request_listener_slot()
cg.add(hub.register_listener(var))
```
```cpp
#ifdef MY_COMPONENT_LISTENER_COUNT
void register_listener(MyComponentListener *listener);
#endif
protected:
#ifdef MY_COMPONENT_LISTENER_COUNT
StaticVector<MyComponentListener *, MY_COMPONENT_LISTENER_COUNT> listeners_;
#endif
```
Request slots from `to_code`, not from a job that runs after `CoroPriority.FINAL` - a late
request raises rather than silently undercounting.
3. **Runtime-known sizes:** Use `FixedVector` from `esphome/core/helpers.h` when the size is only known at runtime initialization.
```cpp
// Bad - generates STL realloc code (_M_realloc_insert)
@@ -677,25 +599,9 @@ file does, and it is the authority when they disagree. The most useful starting
```
Linear search on small datasets (1-16 elements) is often faster than hashing/tree overhead, but this depends on lookup frequency and access patterns. For frequent lookups in hot code paths, the O(1) vs O(n) complexity difference may still matter even for small datasets. `std::vector` with simple structs is usually fine—it's the heavy containers (`map`, `set`, `unordered_map`) that should be avoided for small datasets unless profiling shows otherwise.
5. **Strings set once from configuration:** Use `StringRef` (`esphome/core/string_ref.h`) rather than
`std::string`. Code generation passes a string literal that lives in flash for the life of the
program, so storing a `std::string` copies it onto the heap for nothing. `StringRef` is a
non-owning pointer plus length; it does not copy, and it must only ever refer to storage that
outlives it (a string literal, or a buffer owned elsewhere).
```cpp
// Bad - heap copy of a literal that is already in flash
void set_keys(std::string keys) { this->keys_ = std::move(keys); }
std::string keys_;
```
```cpp
// Good - no allocation
void set_keys(const char *keys) { this->keys_ = StringRef(keys); }
StringRef keys_;
```
5. **Avoid `std::deque`:** It allocates in 512-byte blocks regardless of element size, guaranteeing at least 512 bytes of RAM usage immediately. This is a major source of crashes on memory-constrained devices.
6. **Avoid `std::deque`:** It allocates in 512-byte blocks regardless of element size, guaranteeing at least 512 bytes of RAM usage immediately. This is a major source of crashes on memory-constrained devices.
7. **Detection:** Look for these patterns in compiler output:
6. **Detection:** Look for these patterns in compiler output:
- Large code sections with STL symbols (vector, map, set)
- `alloc`, `realloc`, `dealloc` in symbol names
- `_M_realloc_insert`, `_M_default_append` (vector reallocation)
-4
View File
@@ -131,7 +131,6 @@ esphome/components/cst816/* @clydebarrow
esphome/components/cst9220/* @clydebarrow
esphome/components/ct_clamp/* @jesserockz
esphome/components/current_based/* @djwmarcx
esphome/components/d01/* @ch604
esphome/components/dac7678/* @NickB1
esphome/components/daikin_arc/* @MagicBear
esphome/components/daikin_brc/* @hagak
@@ -149,7 +148,6 @@ esphome/components/display_menu_base/* @numo68
esphome/components/dlms_meter/* @latonita @PolarGoose @SimonFischer04 @Tomer27cz
esphome/components/dps310/* @kbx81
esphome/components/ds1307/* @badbadc0ffee
esphome/components/ds1603l/* @JakeLC15
esphome/components/ds2484/* @mrk-its
esphome/components/ds248x/* @tomwellnitz
esphome/components/dsmr/* @glmnet @PolarGoose
@@ -352,7 +350,6 @@ esphome/components/mipi_spi/* @clydebarrow
esphome/components/mitsubishi/* @RubyBailey
esphome/components/mitsubishi_cn105/* @crnjan
esphome/components/mixer/speaker/* @kahrendt
esphome/components/mk2pvrouter/* @FredM67
esphome/components/mlx90393/* @functionpointer
esphome/components/mlx90614/* @jesserockz
esphome/components/mmc5603/* @benhoff
@@ -496,7 +493,6 @@ esphome/components/sm2335/* @Cossid
esphome/components/sml/* @alengwenus
esphome/components/smt100/* @piechade
esphome/components/sn74hc165/* @jesserockz
esphome/components/snapshot/* @clydebarrow
esphome/components/socket/* @esphome/core
esphome/components/sonoff_d1/* @anatoly-savchenkov
esphome/components/sound_level/* @kahrendt
+1 -1
View File
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version
# control system is used.
PROJECT_NUMBER = 2026.10.0-dev
PROJECT_NUMBER = 2026.9.0-dev
# Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a
+3 -35
View File
@@ -23,8 +23,7 @@ For this repository there are two trusted inputs by design:
1. **The configuration.** Anyone who can supply or edit a YAML config is trusted
(see below).
2. **Authenticated peers of a running device** — clients holding the device's
API/OTA encryption key, API password, OTA password, or web server
credentials.
API encryption key / password, OTA password, or web server credentials.
The security boundary is therefore **unauthenticated network traffic vs. those
trusted inputs.** A bug that lets an unauthenticated attacker cross it is a
@@ -77,8 +76,8 @@ These *are* security bugs in this repo, and we want to hear about them privately
captive portal, etc.) **without** valid credentials.
- Authentication or encryption bypass on the device — reaching API calls, OTA
updates, or the web server without the configured key/password.
- Flaws that weaken the device's API or OTA encryption (Noise), OTA auth, or
web server auth below their documented guarantees.
- Flaws that weaken the device's API encryption (Noise), OTA, or web server auth
below their documented guarantees.
## The web server is an open HTTP API by design
@@ -122,37 +121,6 @@ and any memory-safety or protocol bug in the server reachable without credential
This section documents the current design and scope; it is not a judgment that the
design is optimal or that it will not change.
## OTA update encryption
The `esphome` OTA platform optionally encrypts updates with the same Noise
`NNpsk0` pattern the native API uses; one key protects the device. With an
`encryption:` block configured the guarantees are: the firmware image is
confidential in transit, the uploader is authenticated by the pre-shared key,
and the plaintext negotiation preceding the handshake is bound into the
handshake prologue, so stripping or tampering with it fails the first MAC.
Both ends fail closed with no override: a device built with a key refuses
plaintext uploads, and the CLI refuses to send plaintext when a key is
configured.
Defeating any of that without the key is in scope: a keyed device accepting a
plaintext or downgraded upload, getting past the MAC, or recovering image
contents from captured traffic.
The following are **not** vulnerabilities, by design:
- Plaintext OTA on a device with no `encryption:` block. That is the
documented default, authenticated (if at all) by the OTA password.
- The enablement window: turning encryption on takes one last upload of the
encryption-enabled firmware over the existing plaintext channel, with the
pre-existing plaintext exposure.
- The web OTA `/update` endpoint alongside encryption. The `web_server`
component keeps it always reachable, and `captive_portal:` auto-loads it
for the fallback AP window; validation warns about both combinations, and
the operator keeps the recovery path.
- CLI retry behavior on transport or MAC failures; every attempt renegotiates
a fresh handshake with fresh ephemerals, so retrying does not weaken
authentication.
## Explicitly out of scope
- Local attackers who already have shell access on the host that runs `esphome`.
+1 -1
View File
@@ -22,7 +22,7 @@ RUN \
-r /requirements.txt
# Install the ESPHome Device Builder dashboard.
RUN uv pip install --no-cache-dir esphome-device-builder==1.14.0
RUN uv pip install --no-cache-dir esphome-device-builder==1.13.1
RUN \
platformio settings set enable_telemetry No \
+1 -5
View File
@@ -1,6 +1,5 @@
#!/usr/bin/env python3
import argparse
import os
import re
CHANNEL_DEV = "dev"
@@ -65,10 +64,7 @@ def main():
suffix = f"-{args.suffix}" if args.suffix else ""
repository = (
(os.environ.get("GITHUB_REPOSITORY") or "esphome/esphome").strip().lower()
)
image_name = f"{repository}{suffix}"
image_name = f"esphome/esphome{suffix}"
print(f"channel={channel}")
@@ -0,0 +1,8 @@
esphome:
name: docker-test-esp8266-native
esp8266:
board: d1_mini
toolchain: arduino
logger:
+18 -44
View File
@@ -26,9 +26,7 @@ from esphome.const import (
CONF_DEASSERT_RTS_DTR,
CONF_DISABLED,
CONF_DISCOVER_IP,
CONF_ENCRYPTION,
CONF_ESPHOME,
CONF_KEY,
CONF_LEVEL,
CONF_LOG,
CONF_LOG_TOPIC,
@@ -1337,19 +1335,6 @@ def _upload_via_native_api(
remote_port = int(ota_conf[CONF_PORT])
password = ota_conf.get(CONF_PASSWORD)
# Fail closed: an encryption block whose key did not resolve must never
# fall back to a plaintext upload
noise_psk = None
if (encryption_conf := ota_conf.get(CONF_ENCRYPTION)) is not None:
noise_psk = encryption_conf.get(CONF_KEY)
if not noise_psk:
raise EsphomeError(
"OTA encryption is configured but no key was resolved; "
"set the key under 'ota: encryption:' or 'api: encryption:'"
)
# Ensure the key is a string, as required by the underlying OTA implementation.
# It arrives here as a SensitiveStr which aioesphomeapi rejects.
noise_psk = str(noise_psk)
def check_partition_access(option_string: str) -> None:
if not ota_conf.get("allow_partition_access"):
@@ -1380,9 +1365,7 @@ def _upload_via_native_api(
if ota_type == espota2.OTA_TYPE_UPDATE_BOOTLOADER:
_validate_bootloader_binary(binary)
return espota2.run_ota(
network_devices, remote_port, password, binary, ota_type, noise_psk
)
return espota2.run_ota(network_devices, remote_port, password, binary, ota_type)
def _upload_via_web_server(
@@ -1391,16 +1374,6 @@ def _upload_via_web_server(
from esphome import web_server_ota
from esphome.web_server_helpers import get_web_server_connection
if any(
ota_item.get(CONF_PLATFORM) == CONF_ESPHOME
and ota_item.get(CONF_ENCRYPTION) is not None
for ota_item in config.get(CONF_OTA, [])
):
_LOGGER.warning(
"This config has OTA encryption, but the web_server OTA path sends "
"the image over plaintext HTTP; use the esphome OTA platform to "
"keep it confidential"
)
remote_port, username, password = get_web_server_connection(config)
return web_server_ota.run_ota(
network_devices, remote_port, username, password, binary
@@ -1696,26 +1669,20 @@ def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None:
if exit_code != 0:
return exit_code
if CORE.is_host:
_LOGGER.info(
"Successfully compiled program to path '%s'", _host_program_path(config)
)
if CORE.using_toolchain_esp_idf:
from esphome.espidf import toolchain
program_path = str(toolchain.get_elf_path())
else:
from esphome.platformio.toolchain import get_idedata
program_path = str(get_idedata(config).firmware_elf_path)
_LOGGER.info("Successfully compiled program to path '%s'", program_path)
else:
_LOGGER.info("Successfully compiled program.")
return 0
def _host_program_path(config: ConfigType) -> str:
"""Return the compiled host ELF path."""
if CORE.using_toolchain_esp_idf:
from esphome.espidf import toolchain
return str(toolchain.get_elf_path())
from esphome.platformio.toolchain import get_idedata
# Memoized by compile_program's own call; this is a dict lookup
return str(get_idedata(config).firmware_elf_path)
def command_upload(args: ArgsProtocol, config: ConfigType) -> int | None:
# Get devices, resolving special identifiers like OTA
devices = choose_upload_log_host(
@@ -1760,7 +1727,14 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
return exit_code
_LOGGER.info("Successfully compiled program.")
if CORE.is_host:
program_path = _host_program_path(config)
if CORE.using_toolchain_esp_idf:
from esphome.espidf import toolchain
program_path = str(toolchain.get_elf_path())
else:
from esphome.platformio.toolchain import get_idedata
program_path = str(get_idedata(config).firmware_elf_path)
_LOGGER.info("Running program from path '%s'", program_path)
return run_external_process(program_path)
+12 -8
View File
@@ -23,7 +23,7 @@ from esphome.build_helpers.pch import ccache_pch_env
from esphome.build_helpers.tools_cache import ARDUINO8266_TOOLS_CACHE, tools_cache_path
from esphome.core import EsphomeError, Version
from esphome.framework_helpers import str_to_lst_of_str
from esphome.platformio.registry import install_package, prefetch_packages
from esphome.platformio.registry import install_packages, prefetch_packages
FRAMEWORK_PACKAGE = "framework-arduinoespressif8266"
TOOLCHAIN_PACKAGE = "toolchain-xtensa"
@@ -45,7 +45,8 @@ def get_arduino8266_tools_path() -> Path:
return tools_cache_path(*ARDUINO8266_TOOLS_CACHE)
# 3.1.1 rather than 3.1.0: the registry has no packages for 3.0.0, 3.0.1 or 3.1.0
# 3.1.1 rather than 3.1.0: the registry has no package for 3.1.0, and the
# encoder below cannot name 3.0.0/3.0.1 either (see its docstring)
MIN_FRAMEWORK_VERSION = Version(3, 1, 1)
@@ -53,16 +54,20 @@ def framework_package_version(ver: Version) -> str:
"""Map an Arduino core version to its registry package version (3.1.2 ->
3.30102.0; the leading 3 is the package major).
Exact registry names for 3.x cores; callers floor at MIN_FRAMEWORK_VERSION.
Exact registry names only for cores > 2.6.2 and >= 3.0.2; callers floor
at MIN_FRAMEWORK_VERSION.
"""
if ver.major > 3:
raise EsphomeError(
f"Arduino core {ver} is not supported yet; "
"the newest known core series is 3.x"
)
if ver.major < 3:
if ver <= Version(2, 6, 2):
# Cores <= 2.6.2 use the older 1.x/2.x package-major encodings (same
# boundary as _format_framework_arduino_version's era guard)
raise EsphomeError(
f"Arduino core {ver} is not supported; ESPHome requires core 3.x"
f"Arduino core {ver} uses an older package encoding than this "
"helper implements (newer than 2.6.2)"
)
return f"3.{ver.major}{ver.minor:02d}{ver.patch:02d}.0"
@@ -116,10 +121,9 @@ def check_and_install(framework_version: Version) -> InstalledPaths:
("bin", "xtensa-lx106-elf"),
),
)
# Fetch both archives at once; the installs below verify and extract
# Fetch both archives at once; the install verifies and extracts them
prefetch_packages([spec[:4] for spec in specs], downloads_dir)
for name, version, dest, mirrors, expect in specs:
install_package(name, version, dest, mirrors, downloads_dir, expect=expect)
install_packages(specs, downloads_dir)
return InstalledPaths(
framework=framework_path, toolchain=toolchain_path, ninja=ninja_path
)
+48 -35
View File
@@ -27,7 +27,6 @@ import sys
from typing import TYPE_CHECKING, NamedTuple
from esphome.arduino8266.framework import toolchain_tool
from esphome.build_helpers.ccache import effective_ccache_basedir
from esphome.build_helpers.idedata import is_joined_include
from esphome.build_helpers.ninja import (
escape as _e,
@@ -37,10 +36,16 @@ from esphome.build_helpers.ninja import (
from esphome.build_helpers.pch import (
PCH_CORE_HEADER,
PCH_HEADER_NAME,
log_pch_in_use,
mark_pch_emitted,
pch_checksum,
pch_consumer_escalation,
pch_degraded,
pch_disabled_degraded,
pch_enabled,
pch_header_text,
pch_identity,
pch_probe_args,
pch_strict,
)
from esphome.components.esp8266 import build_surgery
from esphome.components.esp8266.boards import (
@@ -1231,44 +1236,28 @@ def write_project(paths: InstalledPaths, ccache: str | None) -> bool:
"A -include in build_flags prevents the precompiled header from "
"loading; compiling without it"
)
pch_degraded("a user -include precedes the pch")
elif pch_enabled():
# C++ src edges swap the force-includes for one precompiled prefix
# header (same content plus defines.h); C/assembly keep srcflags
pch_header = build_dir / PCH_HEADER_NAME
pch_includes = (*src_includes, PCH_CORE_HEADER)
pch_text = pch_header_text(pch_includes)
# The .sum exists only for CCACHE_PCH_EXTSUM; ninja's depfile
# handles staleness
identity_ok = True
checksum = None
try:
if ccache:
# The .sum exists only for CCACHE_PCH_EXTSUM; ninja's depfile
# handles staleness. Strip resolved and raw build paths
# (symlinks) so identical configs share cache entries
flags_id = (
" ".join(cxxflags)
.replace(effective_ccache_basedir(), "")
.replace(str(CORE.build_path), "")
)
# The header text covers include order
checksum = pch_checksum(
src_dir,
pch_includes,
(
pch_text,
str(paths.framework),
str(paths.toolchain),
flags_id,
),
)
except (OSError, UnicodeError) as err:
# Identity unknown: a stale cache entry must never be served
_LOGGER.warning(
"Could not establish the pch identity; compiling without it: %s", err
)
else:
_LOGGER.info(
"Compiling with a precompiled header "
"(set ESPHOME_PCH_ENABLE=0 to disable)"
if ccache:
checksum = pch_identity(
cxxflags,
src_dir,
pch_includes,
(str(paths.framework), str(paths.toolchain)),
)
# Identity unknown: pch_identity warned and degraded
identity_ok = checksum is not None
if identity_ok:
log_pch_in_use()
write_file_if_changed(pch_header, pch_text)
sum_path = build_dir / f"{PCH_HEADER_NAME}.gch.sum"
if checksum is not None:
@@ -1283,13 +1272,37 @@ def write_project(paths: InstalledPaths, ccache: str | None) -> bool:
if src_other:
lines.append(f" flags = {' '.join(src_other)}")
# Relative -include: absolute would break cross-device ccache.
# -Wno-error keeps a rejected .gch a warning under user -Werror
# -Wno-error keeps a rejected .gch a warning under user -Werror;
# strict inverts it so any consumer rejection reds the build
# (rejection is per-process, so the probe alone cannot prove
# the consumers)
escalation = pch_consumer_escalation()
cxx_parts = src_other + [
f"-Winvalid-pch -Wno-error=invalid-pch -include {PCH_HEADER_NAME}"
f"-Winvalid-pch {escalation} -include {PCH_HEADER_NAME}"
]
lines.append(f"srccxxflags = {' '.join(cxx_parts)}")
src_cxx_override = ("$srccxxflags", gch)
pch_dep = gch
if pch_strict():
# Consumers wait on the probe stamp, so an unloadable .gch
# reds the build here instead of warning ~100 times
probe = " ".join(pch_probe_args(PCH_HEADER_NAME, source=os.devnull))
lines.append("rule pchprobe")
# $out only expands in rule text, hence the inline stamp
lines.append(
f" command = $cxx $cxxflags $flags {probe}"
" && $python $buildtool touch $out"
)
lines.append(" description = PCHPROBE $out")
# Runs when the .gch is (re)built; strict consumer -Werror
# covers a cached .gch this process cannot load
lines.append(f"build esphome_pch.probe: pchprobe {gch}")
if src_other:
lines.append(f" flags = {' '.join(src_other)}")
pch_dep = f"{gch} esphome_pch.probe"
src_cxx_override = ("$srccxxflags", pch_dep)
mark_pch_emitted()
else:
pch_disabled_degraded()
src_objs = _ninja_compile_edges(
lines,
_collect_sources(src_dir),
+11 -1
View File
@@ -6,6 +6,7 @@ started esphome and must not depend on the package being importable.
Subcommands:
ar <ar-binary> <archive> <rspfile> remove stale archive, then ``ar rcs``
copy <src> <dst> copy a file
touch <path> create/update a stamp file
The ar rspfile carries one object path per line (the generating rule must
use ``$in_newline``, never ``$in``).
@@ -83,9 +84,18 @@ def _run_copy(src: str, dst: str) -> int:
return 0
def _run_touch(path: str) -> int:
try:
Path(path).touch()
except OSError as err:
print(f"touch: {path} failed: {err}", file=sys.stderr)
return 1
return 0
# mode -> (handler, expected operand count); surplus argv means a
# mis-specified ninja rule and must error, not silently drop operands
_MODES = {"ar": (_run_ar, 3), "copy": (_run_copy, 2)}
_MODES = {"ar": (_run_ar, 3), "copy": (_run_copy, 2), "touch": (_run_touch, 1)}
def main() -> int:
+57 -1
View File
@@ -4,6 +4,14 @@ import json
import logging
from pathlib import Path
from esphome.build_helpers import pch
from esphome.build_helpers.pch import (
PCH_DEFAULT_HEADERS,
PCH_HEADER_NAME,
mark_pch_emitted,
pch_enabled,
pch_header_text,
)
from esphome.components.esp32 import (
get_esp32_variant,
get_excluded_builtin_components,
@@ -279,7 +287,47 @@ idf_component_register(
target_link_options(${{COMPONENT_LIB}} PUBLIC
{link_opts_str}
)
"""
{_pch_cmake()}"""
def _pch_cmake() -> str:
"""Consumer block for the component CMakeLists. Baked at generation:
a strict-knob flip takes effect on the next esphome compile; a
hand-run idf.py keeps the old one."""
return pch.pch_cmake_consumer("${COMPONENT_LIB}", "${app_sources}")
def prepare_pch() -> None:
"""Build the .gch right before ninja, after every reconfigure, so the
compile_commands.json flags and the sdkconfig are the settled ones."""
if not pch_enabled():
# Self-cleaning escape hatch: drop any previously built .gch
pch.discard_pch(CORE.relative_build_path("build"))
pch.pch_disabled_degraded()
return
sdkconfig_path = CORE.relative_build_path(f"sdkconfig.{CORE.name}")
try:
sdkconfig = sdkconfig_path.read_text(encoding="utf-8")
except OSError as err:
# Fail closed: the sdkconfig is the .sum's only config identity for
# sdkconfig.h-only options; a stand-in marker would collide
_LOGGER.warning(
"Could not read %s; compiling without the pch: %s", sdkconfig_path, err
)
pch.discard_pch(CORE.relative_build_path("build"))
pch.pch_degraded(f"sdkconfig unreadable: {err}")
return
pch.prepare_pch(
CORE.relative_build_path("build"),
PCH_DEFAULT_HEADERS,
(
str(idf_version()),
CORE.cpp_standard or "",
sdkconfig,
*get_project_compile_flags(),
*get_project_cxx_compile_flags(),
),
)
def write_project(
@@ -301,6 +349,14 @@ def write_project(
get_component_cmakelists(),
)
if pch_enabled():
write_file_if_changed(
CORE.relative_build_path("build", PCH_HEADER_NAME),
pch_header_text(PCH_DEFAULT_HEADERS),
)
# Consumers carry the -include; gate the ccache relaxation on it
mark_pch_emitted()
# Snapshot the exclusion set so has_outdated_files() can trigger a
# discovery reconfigure when it changes. Excluded components never
# register in project_description.json, so re-including one (e.g. a
+7 -2
View File
@@ -21,12 +21,13 @@ def _ccache_runs(ccache: str) -> bool:
)
def parse_enable_env(name: str) -> bool | None:
def parse_enable_env(name: str, strict: bool = False) -> bool | None:
"""Strictly parse an on/off environment knob; None when unset or invalid.
``bool(str)`` truthiness would flip ``no``/``off`` to enabled, so only
1/true/yes/on and 0/false/no/off count; anything else warns and reads
as unset so the caller's default policy applies.
as unset so the caller's default policy applies — or raises when
``strict`` (a typo must not silently disable a CI gate).
"""
raw = os.environ.get(name)
if raw is None:
@@ -39,6 +40,10 @@ def parse_enable_env(name: str) -> bool | None:
return True
if lowered in FALSY_ENV_STRINGS:
return False
if strict:
from esphome.core import EsphomeError
raise EsphomeError(f"Unrecognized {name}={raw!r}; use 1 or 0")
_LOGGER.warning("Ignoring unrecognized %s=%r; use 1 or 0", name, raw)
return None
+9 -9
View File
@@ -76,7 +76,7 @@ def _is_esphome_src(file: str) -> bool:
)
def _split_command(command: str) -> list[str]:
def split_command(command: str) -> list[str]:
r"""Tokenize a compile_commands.json / response-file command string.
On Windows, tokenize per Windows ``argv`` rules via ``CommandLineToArgvW``.
@@ -112,7 +112,7 @@ def _split_command(command: str) -> list[str]:
ctypes.windll.kernel32.LocalFree(argv)
def _expand_response_files(tokens: list[str], directory: Path) -> list[str]:
def expand_response_files(tokens: list[str], directory: Path) -> list[str]:
"""Inline any ``@response-file`` arguments (paths relative to ``directory``).
GCC response files embed flags that must be expanded so GCC-only flags
@@ -127,8 +127,8 @@ def _expand_response_files(tokens: list[str], directory: Path) -> list[str]:
rf = directory / rf
try:
out.extend(
_expand_response_files(
_split_command(rf.read_text(encoding="utf-8")), directory
expand_response_files(
split_command(rf.read_text(encoding="utf-8")), directory
)
)
continue
@@ -157,7 +157,7 @@ def _pick_entry(entries: list[dict]) -> dict:
_LAUNCHER_STEMS = frozenset({"ccache", "sccache", "distcc", "icecc", "buildcache"})
def _is_launcher(token: str) -> bool:
def is_launcher(token: str) -> bool:
return Path(token).stem.lower() in _LAUNCHER_STEMS
@@ -175,7 +175,7 @@ def parse_entry(
) -> tuple[str, list[str], list[str], list[str]]:
"""Parse one compile_commands entry -> (cxx_path, defines, includes, cxx_flags)."""
directory = Path(entry["directory"])
tokens = _expand_response_files(_split_command(entry["command"]), directory)
tokens = expand_response_files(split_command(entry["command"]), directory)
def _include(raw: str) -> str:
# Resolve against the entry's ``directory`` so cached idedata works
@@ -191,7 +191,7 @@ def parse_entry(
if not tokens:
# An empty command, or one that was only the launcher; fail by name
raise ValueError(f"empty compile command for {entry.get('file')}")
if _is_launcher(tokens[0]) and len(tokens) > 1 and not tokens[1].startswith("-"):
if is_launcher(tokens[0]) and len(tokens) > 1 and not tokens[1].startswith("-"):
# Stale DB built with a launcher this run no longer configures; the
# real compiler is the next token
_LOGGER.warning("Stripping unconfigured launcher %s", tokens[0])
@@ -311,7 +311,7 @@ def _cache_usable(cached: object) -> bool:
if not isinstance(cached, dict) or "cc_path" not in cached:
return False
cxx_path = cached.get("cxx_path")
if not isinstance(cxx_path, str) or _is_launcher(cxx_path):
if not isinstance(cxx_path, str) or is_launcher(cxx_path):
return False
includes = cached.get("includes")
return isinstance(includes, dict) and isinstance(includes.get("build"), list)
@@ -359,7 +359,7 @@ def load_or_build_idedata(
def reject_launcher_compiler(cxx_path: str) -> None:
"""Reject a compile DB naming a launcher (ccache) as the compiler; it
must never be probed, cached, or consumed."""
if _is_launcher(cxx_path):
if is_launcher(cxx_path):
raise EsphomeError(
f"compile_commands.json names the launcher {cxx_path} as the "
"compiler; the compile database is unusable"
+449 -3
View File
@@ -7,17 +7,26 @@ it too; Arduino.h visibility there is intended (esphome#8693).
from __future__ import annotations
from collections.abc import Iterable
from collections.abc import Callable, Iterable
from contextlib import suppress
from dataclasses import dataclass
import hashlib
import json
import logging
import os
from pathlib import Path
import posixpath
import re
import stat
import subprocess
from esphome.build_helpers.ccache import parse_enable_env
from esphome.build_helpers.ccache import effective_ccache_basedir, parse_enable_env
from esphome.build_helpers.idedata import (
CXX_SOURCE_SUFFIXES,
expand_response_files,
is_launcher,
split_command,
)
_DOMAIN = "pch"
@@ -56,6 +65,27 @@ PCH_ARTIFACT_NAMES = (
# The core defines header every backend anchors its prefix on.
PCH_CORE_HEADER = "esphome/core/defines.h"
# Guarded curated-prefix wrapper for PlatformIO backends without framework
# force-includes (host, esp32); folded by the pch script via build_src_flags.
PCH_PREFIX_HEADER = "esphome/core/pch_prefix.h"
# Prefix-header contents for backends that inject a curated set (rather
# than mirroring the TUs' own force-includes), defines.h first so USE_*
# macros exist for the rest. Deliberately hard-coded: frequency-derived
# sets measured no better and kept selecting headers that cannot compile
# standalone (X-macro, platform-variant). Every entry must be safe to
# include first in an empty TU. Caveat: application.h/automation.h become
# ambiently visible, so a TU missing those #includes still builds on such
# backends; ESPHOME_PCH_ENABLE=0 restores the strict view.
PCH_DEFAULT_HEADERS = (
PCH_CORE_HEADER,
"esphome/core/component.h",
"esphome/core/helpers.h",
"esphome/core/log.h",
"esphome/core/application.h",
"esphome/core/automation.h",
)
# ccache cannot hash through a .gch; CCACHE_PCH_EXTSUM makes it hash the
# .sum sidecar instead of the .gch bytes, which are not reproducible.
# Keep in sync with the literals in platformio/pch.py.script.
@@ -66,6 +96,9 @@ _CCACHE_PCH_ENV = {
# Both include forms: an angle include resolving under src/ must enter the
# digest too; ones that do not resolve simply end the walk
# Compiler failures that clear on their own must not latch the .failed marker
_TRANSIENT_ERRORS = ("No space left", "Cannot allocate", "Resource temporarily")
_INCLUDE_RE = re.compile(rb'^\s*#\s*include\s+["<]([^">]+)[">]', re.MULTILINE)
@@ -74,6 +107,81 @@ def pch_enabled() -> bool:
return parse_enable_env("ESPHOME_PCH_ENABLE") is not False
def pch_strict() -> bool:
"""CI knob: ``ESPHOME_PCH_STRICT=1`` turns pch degrade paths fatal.
A set-but-unrecognized value raises: a typo must not silently turn
the gate into a no-op that proves nothing.
"""
return parse_enable_env("ESPHOME_PCH_STRICT", strict=True) is True
def pch_degraded(reason: str) -> None:
"""Every degrade path funnels through here; strict mode raises."""
if pch_strict():
from esphome.core import EsphomeError
raise EsphomeError(f"ESPHOME_PCH_STRICT: {reason}")
def pch_disabled_degraded() -> None:
"""Strict CI must not read "no pch at all" as success."""
pch_degraded("pch disabled by ESPHOME_PCH_ENABLE")
def pch_probe_tail(source: str = "-") -> list[str]:
"""The syntax-only compile shared by the probe and its baseline."""
return ["-fsyntax-only", "-x", "c++", source]
def pch_probe_args(header: str, source: str = "-") -> list[str]:
"""Flags that load-check a built .gch via a syntax-only compile.
Rejection must be a nonzero exit (never just a wording match), so the
invalid-pch class is always escalated. ``source`` defaults to stdin
(host independent); the ninja probe edge passes a real file.
"""
return [
"-Winvalid-pch",
"-Werror=invalid-pch",
"-include",
header,
*pch_probe_tail(source),
]
def pch_consumer_escalation() -> str:
"""Consumer-side invalid-pch flag: strict reds the build on rejection
(per-process, so the probe alone cannot prove the consumers)."""
return "-Werror=invalid-pch" if pch_strict() else "-Wno-error=invalid-pch"
def pch_cmake_consumer(target: str, sources_var: str) -> str:
"""Emit the CMake block making ``target``'s C++ sources consume the
pch; empty when disabled. OBJECT_DEPENDS is on the header, not the
.gch (pch-baked headers drop out of TU depfiles); the -include stays
relative — an absolute path would poison ccache keys."""
if not pch_enabled():
return ""
escalation = pch_consumer_escalation()
return f"""
# ESPHome precompiled header (see esphome/build_helpers/pch.py).
# The touch keeps OBJECT_DEPENDS satisfiable when the build system itself
# wiped the build dir after the header was written (west --pristine)
if(NOT EXISTS "${{CMAKE_BINARY_DIR}}/{PCH_HEADER_NAME}")
file(TOUCH "${{CMAKE_BINARY_DIR}}/{PCH_HEADER_NAME}")
endif()
target_compile_options({target} PRIVATE
"$<$<COMPILE_LANGUAGE:CXX>:-Winvalid-pch>"
"$<$<COMPILE_LANGUAGE:CXX>:{escalation}>"
"$<$<COMPILE_LANGUAGE:CXX>:-include>"
"$<$<COMPILE_LANGUAGE:CXX>:{PCH_HEADER_NAME}>"
)
set_source_files_properties({sources_var} PROPERTIES
OBJECT_DEPENDS "${{CMAKE_BINARY_DIR}}/{PCH_HEADER_NAME}")
"""
def ccache_pch_env() -> dict[str, str]:
"""Settings ccache needs to cache compiles that consume the .gch;
empty unless this build actually emitted one. User-set values win.
@@ -104,10 +212,37 @@ def ccache_pch_env() -> dict[str, str]:
return env
def guarded_prepare(build_dir: Path, prepare: Callable[[], None]) -> None:
"""Run a backend's pch preparation; an optional speedup must never
abort the build. Strict is read first so its own knob error cannot
mask the real failure; discard_pch raises if a stale .gch survives;
the header is ensured so OBJECT_DEPENDS stays satisfiable."""
try:
prepare()
except Exception: # noqa: BLE001 # pylint: disable=broad-exception-caught
strict = pch_strict()
discard_pch(build_dir)
if strict:
raise
header = build_dir / PCH_HEADER_NAME
if not header.exists():
try:
header.touch()
except OSError as err:
# The coming OBJECT_DEPENDS error would hide the real cause
_LOGGER.warning("Could not create the pch placeholder: %s", err)
_LOGGER.warning(
"Precompiled header setup failed; compiling without it", exc_info=True
)
def pch_extra_scripts() -> list[str]:
"""The extra_scripts entries a PlatformIO platform registers for the
pch; empty when disabled (the script itself has no enable check)."""
return ["post:pch.py"] if pch_enabled() else []
if not pch_enabled():
pch_disabled_degraded()
return []
return ["post:pch.py"]
def pch_header_text(include_headers: Iterable[str]) -> str:
@@ -176,3 +311,314 @@ def pch_checksum(
digest.update(item.encode(errors="surrogateescape"))
digest.update(b"\0")
return digest.hexdigest()
# Tokens dropped when retargeting a TU's flags at the prefix header
# (the pch compile must not touch depfiles)
_PCH_STRIP_FLAGS_WITH_ARG = frozenset({"-o", "-c", "-MT", "-MF", "-MQ"})
_PCH_STRIP_FLAGS = frozenset({"-MD", "-MMD", "-MP", "-MM", "-M"})
def pch_compile_command(
build_dir: Path, header: Path, gch: Path
) -> tuple[list[str], Path] | None:
"""The exact src C++ flags from compile_commands.json retargeted at the
header, with the directory they resolve against (relative -I paths must
be expanded and executed from the same root); None (logged) when no
configured C++ TU is available yet."""
from esphome.core import CORE
try:
entries = json.loads(
(build_dir / "compile_commands.json").read_text(encoding="utf-8")
)
except (OSError, json.JSONDecodeError) as err:
# Configure already succeeded, so an unusable DB is a real anomaly
_LOGGER.warning("No usable compile database, skipping pch: %s", err)
return None
if not isinstance(entries, list):
_LOGGER.warning("Malformed compile database, skipping pch")
return None
# CMake may spell paths through a symlink differently than CORE does
# (macOS /tmp vs /private/tmp), so compare resolved paths
src_root = Path(CORE.relative_src_path()).resolve()
entry = next(
(
e
for e in entries
if isinstance(e, dict)
and isinstance(e.get("file"), str)
and e["file"].endswith(CXX_SOURCE_SUFFIXES)
and Path(e["file"]).resolve().is_relative_to(src_root)
),
None,
)
if entry is None:
_LOGGER.warning("No src C++ entry in the compile database, skipping pch")
return None
directory = entry.get("directory")
cmd_dir = Path(directory) if isinstance(directory, str) and directory else build_dir
command = entry.get("command")
tokens = expand_response_files(
split_command(command if isinstance(command, str) else ""), cmd_dir
)
# A DB recorded with ccache enabled prefixes the compiler with the
# launcher; the .gch must be compiled directly
if tokens and is_launcher(tokens[0]):
tokens = tokens[1:]
if not tokens:
# "arguments"-style or empty entries must skip, not spawn "-x ..."
_LOGGER.warning("Compile database entry has no usable command, skipping pch")
return None
args: list[str] = []
arg_it = iter(tokens)
for tok in arg_it:
if tok in _PCH_STRIP_FLAGS_WITH_ARG:
next(arg_it, None)
continue
if tok in _PCH_STRIP_FLAGS:
continue
if tok == "-include":
# Drop only the injected prefix; user force-includes must reach
# the .gch compile or GCC rejects it over the macro mismatch
inc = next(arg_it, "")
if not inc.endswith(PCH_HEADER_NAME):
args.extend(("-include", inc))
continue
args.append(tok)
return [*args, "-x", "c++-header", "-c", str(header), "-o", str(gch)], cmd_dir
def _flags_identity(tokens: Iterable[str]) -> str:
"""Flag string normalized for digest use: strip like ccache's rewriting
(user CCACHE_BASEDIR wins); the raw build path covers unresolved
(symlinked) spellings."""
from esphome.core import CORE
return (
" ".join(tokens)
.replace(effective_ccache_basedir(), "")
.replace(str(CORE.build_path), "")
)
def pch_identity(
tokens: Iterable[str],
src_dir: Path,
include_headers: tuple[str, ...],
extra: Iterable[str],
) -> str | None:
"""The .sum digest naming this exact pch build: include closure, header
text, backend identity strings, and the normalized compile command or
flags (``tokens``). None (warned and degraded) when the identity
cannot be established."""
try:
return pch_checksum(
src_dir,
include_headers,
(
# The closure is sorted, so root order only enters via the text
pch_header_text(include_headers),
*extra,
_flags_identity(tokens),
),
)
except (OSError, UnicodeError) as err:
# Identity unknown: a stale cache entry must never be served
_LOGGER.warning(
"Could not establish the pch identity; compiling without it: %s", err
)
pch_degraded(f"identity unknown: {err}")
return None
def log_pch_in_use() -> None:
# The only place a user can discover the knob; emitted only once a
# .gch is actually fresh or being built
_LOGGER.info(
"Compiling with a precompiled header (set ESPHOME_PCH_ENABLE=0 to disable)"
)
def _read_stamp(path: Path) -> str:
"""A corrupt sidecar must read as stale, not kill the pch forever."""
try:
return path.read_text(encoding="utf-8").strip()
except (OSError, UnicodeDecodeError):
return ""
def discard_pch(build_dir: Path) -> None:
"""Remove the pch sidecars so a stale .gch is never consumed.
Bumps the header only when a .gch was actually removed: TUs compiled
against it have incomplete depfiles, while a repeat failure with no
.gch must not force a full rebuild every build. A .gch that survives
an unlink failure would be consumed silently (wrong output, not a
slow build), so that raises.
"""
header = build_dir / PCH_HEADER_NAME
gch = Path(f"{header}.gch")
had_gch = gch.is_file()
errors = []
for sidecar in (gch, Path(f"{gch}.sum")):
try:
sidecar.unlink(missing_ok=True)
except OSError as err:
if sidecar.is_file():
from esphome.core import EsphomeError
raise EsphomeError(
f"Could not discard the stale precompiled header: {err}"
) from err
errors.append(err)
for err in errors:
_LOGGER.warning("Could not discard the pch sidecars: %s", err)
if had_gch and header.is_file():
with suppress(OSError):
os.utime(header)
def prepare_pch(
build_dir: Path, include_headers: tuple[str, ...], extra: Iterable[str]
) -> None:
"""Compile ``build_dir``'s .gch from compile_commands.json flags and
write its ccache .sum.
The .sum doubles as the freshness stamp and folds in the compile
command, so a flag-only change rebuilds the .gch; ``extra`` carries
backend identity (framework version, sdkconfig, ...). A failed
compile falls back to the plain header include.
"""
from esphome.core import CORE
header = build_dir / PCH_HEADER_NAME
gch = Path(f"{header}.gch")
sum_path = Path(f"{gch}.sum")
cmd_and_dir = pch_compile_command(build_dir, header, gch)
if cmd_and_dir is None:
# Freshness cannot be validated; a leftover .gch must not be consumed
discard_pch(build_dir)
pch_degraded("no usable compile command")
return
cmd, cmd_dir = cmd_and_dir
checksum = pch_identity(cmd, CORE.relative_src_path(), include_headers, extra)
if checksum is None:
discard_pch(build_dir)
return
failed_marker = Path(f"{gch}.failed")
def _run(
run_cmd: list[str], what: str, stdin: str | None = None
) -> subprocess.CompletedProcess | None:
"""Spawn one pch tool step; environmental failures discard and
degrade (None): spawn/IO/timeout errors and signal kills never
latch the marker."""
try:
proc = subprocess.run(
run_cmd,
cwd=cmd_dir,
# C locale keeps diagnostics matchable by _TRANSIENT_ERRORS
env={**os.environ, "LC_ALL": "C"},
input=stdin,
capture_output=True,
text=True,
check=False,
timeout=300,
)
except (OSError, subprocess.SubprocessError) as err:
_LOGGER.warning("Precompiled header %s did not run: %s", what, err)
discard_pch(build_dir)
pch_degraded(f"{what} did not run: {err}")
return None
if proc.returncode < 0:
# Killed by a signal (OOM, ^C): environmental, do not latch
_LOGGER.warning(
"Precompiled header %s was killed (signal %d); retrying next build",
what,
-proc.returncode,
)
discard_pch(build_dir)
pch_degraded(f"{what} killed by signal {-proc.returncode}")
return None
return proc
def _fail(error: str, reason: str, latch: bool) -> None:
"""Discard and degrade; deterministic failures latch when asked."""
_LOGGER.warning(
"Precompiled header failed; compiling without it: %s", error[:400]
)
# Latching paths keep the full compiler output recoverable
_LOGGER.debug("Full pch output: %s", error)
discard_pch(build_dir)
if latch and not any(m in error for m in _TRANSIENT_ERRORS):
# Skip retries until a header/flag/backend-identity/command change
failed_marker.write_text(checksum + "\n", encoding="utf-8")
os.utime(header)
pch_degraded(f"{reason}: {error[:200]}")
def _probe(latch: bool = True) -> None:
"""Load-check the built .gch: some toolchains build one they then
refuse to load (per-process ASLR). Dep flags are already stripped
from cmd, so no -MF is needed; cmd ends with the fixed
"-x c++-header -c -o" tail. A cached-header rejection may not
reproduce (per-process), so that caller passes latch=False."""
if cmd[-6:-4] != ["-x", "c++-header"]:
# The slice below depends on pch_compile_command's fixed tail
_LOGGER.warning("Unexpected pch command shape: %s", cmd[-6:])
discard_pch(build_dir)
pch_degraded("unexpected pch command shape")
return
base = cmd[:-6]
probe = _run([*base, *pch_probe_args(str(header))], "probe", stdin="")
if probe is None:
return
if probe.returncode != 0:
# Disambiguate: only blame the pch when the same compile passes
# without it; a failing baseline is its own (latchable) problem
baseline = _run([*base, *pch_probe_tail()], "probe baseline", stdin="")
if baseline is None:
return
if baseline.returncode == 0:
error = probe.stderr.strip() or f"exit code {probe.returncode}"
_fail(error, "toolchain cannot load the pch", latch=latch)
else:
error = baseline.stderr.strip() or f"exit code {baseline.returncode}"
_fail(error, "probe cannot run at all", latch=latch)
if gch.is_file() and _read_stamp(sum_path) == checksum:
log_pch_in_use()
if pch_strict():
# Rejection is per-process, so a cached .gch must re-prove
# loadability for the strict gate (CI-only cost); no latch,
# since the rejection may not reproduce either
_probe(latch=False)
return
if _read_stamp(failed_marker) == checksum:
_LOGGER.info(
"Precompiled header disabled after an earlier failure; delete %s to retry",
failed_marker,
)
pch_degraded("earlier failure latched")
return
log_pch_in_use()
result = _run(cmd, "compile")
if result is None:
return
error = None
if result.returncode != 0:
error = result.stderr.strip() or f"exit code {result.returncode}"
elif not gch.is_file():
error = "compiler produced no .gch"
if error is not None:
_fail(error, "compile failed", latch=True)
return
_probe()
if not gch.is_file():
# The probe discarded a rejected or unrunnable .gch
return
failed_marker.unlink(missing_ok=True)
sum_path.write_text(checksum + "\n", encoding="utf-8")
# Consumers depend on the header (depfiles cannot see through a .gch);
# bump it so users of the previous .gch recompile
os.utime(header)
-1
View File
@@ -78,7 +78,6 @@ from esphome.cpp_types import ( # noqa: F401
StringRef,
arduino_json_ns,
bool_,
char,
const_char_ptr,
double,
esphome_ns,
-23
View File
@@ -13,7 +13,6 @@ from esphome.components.esp32 import (
VARIANT_ESP32P4,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
VARIANT_ESP32S31,
get_esp32_variant,
)
import esphome.config_validation as cv
@@ -157,17 +156,6 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
9: adc_channel_t.ADC_CHANNEL_8,
10: adc_channel_t.ADC_CHANNEL_9,
},
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s31/include/soc/adc_channel.h
VARIANT_ESP32S31: {
42: adc_channel_t.ADC_CHANNEL_0,
43: adc_channel_t.ADC_CHANNEL_1,
44: adc_channel_t.ADC_CHANNEL_2,
45: adc_channel_t.ADC_CHANNEL_3,
46: adc_channel_t.ADC_CHANNEL_4,
47: adc_channel_t.ADC_CHANNEL_5,
48: adc_channel_t.ADC_CHANNEL_6,
49: adc_channel_t.ADC_CHANNEL_7,
},
}
# pin to adc2 channel mapping
@@ -237,17 +225,6 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
19: adc_channel_t.ADC_CHANNEL_8,
20: adc_channel_t.ADC_CHANNEL_9,
},
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s31/include/soc/adc_channel.h
VARIANT_ESP32S31: {
50: adc_channel_t.ADC_CHANNEL_0,
51: adc_channel_t.ADC_CHANNEL_1,
52: adc_channel_t.ADC_CHANNEL_2,
53: adc_channel_t.ADC_CHANNEL_3,
54: adc_channel_t.ADC_CHANNEL_4,
55: adc_channel_t.ADC_CHANNEL_5,
56: adc_channel_t.ADC_CHANNEL_6,
57: adc_channel_t.ADC_CHANNEL_7,
},
}
+38 -40
View File
@@ -74,7 +74,9 @@ void ADCSensor::setup() {
if (this->calibration_handle_ == nullptr) {
adc_cali_handle_t handle = nullptr;
#if defined(ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED)
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
// RISC-V variants (except C2) and S3 use curve fitting calibration
adc_cali_curve_fitting_config_t cali_config = {}; // Zero initialize first
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
cali_config.chan = this->channel_;
@@ -92,7 +94,7 @@ void ADCSensor::setup() {
ESP_LOGW(TAG, "Curve fitting calibration failed with error %d, will use uncalibrated readings", err);
this->setup_flags_.calibration_complete = false;
}
#elif defined(ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED)
#else // ESP32, ESP32-S2, and ESP32-C2 use line fitting calibration
adc_cali_line_fitting_config_t cali_config = {
.unit_id = this->adc_unit_,
.atten = this->attenuation_,
@@ -110,11 +112,7 @@ void ADCSensor::setup() {
ESP_LOGW(TAG, "Line fitting calibration failed with error %d, will use uncalibrated readings", err);
this->setup_flags_.calibration_complete = false;
}
#else // No calibration scheme available
(void) handle;
ESP_LOGD(TAG, "No calibration scheme for this variant, readings are uncalibrated");
this->setup_flags_.calibration_complete = false;
#endif
#endif // ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3
}
this->setup_flags_.init_complete = true;
@@ -123,28 +121,23 @@ void ADCSensor::setup() {
void ADCSensor::dump_config() {
LOG_SENSOR("", "ADC Sensor", this);
LOG_PIN(" Pin: ", this->pin_);
#if defined(ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED) || defined(ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED)
const char *calibration_status = this->setup_flags_.calibration_complete ? "OK" : "FAILED";
#else
const char *calibration_status = "N/A"; // This variant has no calibration scheme
#endif
ESP_LOGCONFIG(TAG,
" Channel: %d\n"
" Unit: %s\n"
" Attenuation: %s\n"
" Samples: %i\n"
" Sampling mode: %s\n"
" Setup Status:\n"
" Handle Init: %s\n"
" Config: %s\n"
" Calibration: %s\n"
" Overall Init: %s",
this->channel_, LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)),
this->autorange_ ? "Auto" : LOG_STR_ARG(attenuation_to_str(this->attenuation_)), this->sample_count_,
LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_)),
this->setup_flags_.handle_init_complete ? "OK" : "FAILED",
this->setup_flags_.config_complete ? "OK" : "FAILED", calibration_status,
this->setup_flags_.init_complete ? "OK" : "FAILED");
ESP_LOGCONFIG(
TAG,
" Channel: %d\n"
" Unit: %s\n"
" Attenuation: %s\n"
" Samples: %i\n"
" Sampling mode: %s\n"
" Setup Status:\n"
" Handle Init: %s\n"
" Config: %s\n"
" Calibration: %s\n"
" Overall Init: %s",
this->channel_, LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)),
this->autorange_ ? "Auto" : LOG_STR_ARG(attenuation_to_str(this->attenuation_)), this->sample_count_,
LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_)),
this->setup_flags_.handle_init_complete ? "OK" : "FAILED", this->setup_flags_.config_complete ? "OK" : "FAILED",
this->setup_flags_.calibration_complete ? "OK" : "FAILED", this->setup_flags_.init_complete ? "OK" : "FAILED");
LOG_UPDATE_INTERVAL(this);
}
@@ -191,11 +184,12 @@ float ADCSensor::sample_fixed_attenuation_() {
} else {
ESP_LOGW(TAG, "ADC calibration conversion failed with error %d, disabling calibration", err);
if (this->calibration_handle_ != nullptr) {
#if defined(ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED)
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
adc_cali_delete_scheme_curve_fitting(this->calibration_handle_);
#elif defined(ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED)
#else // Other ESP32 variants use line fitting calibration
adc_cali_delete_scheme_line_fitting(this->calibration_handle_);
#endif
#endif // ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3
this->calibration_handle_ = nullptr;
}
}
@@ -223,9 +217,10 @@ float ADCSensor::sample_autorange_() {
// Need to recalibrate for the new attenuation
if (this->calibration_handle_ != nullptr) {
// Delete old calibration handle
#if defined(ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED)
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
adc_cali_delete_scheme_curve_fitting(this->calibration_handle_);
#elif defined(ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED)
#else
adc_cali_delete_scheme_line_fitting(this->calibration_handle_);
#endif
this->calibration_handle_ = nullptr;
@@ -234,7 +229,8 @@ float ADCSensor::sample_autorange_() {
// Create new calibration handle for this attenuation
adc_cali_handle_t handle = nullptr;
#if defined(ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED)
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
adc_cali_curve_fitting_config_t cali_config = {};
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
cali_config.chan = this->channel_;
@@ -246,7 +242,7 @@ float ADCSensor::sample_autorange_() {
err = adc_cali_create_scheme_curve_fitting(&cali_config, &handle);
ESP_LOGVV(TAG, "Autorange atten=%d: Calibration handle creation %s (err=%d)", atten,
(err == ESP_OK) ? "SUCCESS" : "FAILED", err);
#elif defined(ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED)
#else
adc_cali_line_fitting_config_t cali_config = {
.unit_id = this->adc_unit_,
.atten = atten,
@@ -268,9 +264,10 @@ float ADCSensor::sample_autorange_() {
if (err != ESP_OK) {
ESP_LOGW(TAG, "ADC read failed in autorange with error %d", err);
if (handle != nullptr) {
#if defined(ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED)
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
adc_cali_delete_scheme_curve_fitting(handle);
#elif defined(ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED)
#else
adc_cali_delete_scheme_line_fitting(handle);
#endif
}
@@ -289,9 +286,10 @@ float ADCSensor::sample_autorange_() {
ESP_LOGVV(TAG, "Autorange atten=%d: UNCALIBRATED FALLBACK - raw=%d -> %.6fV (3.3V ref)", atten, raw, voltage);
}
// Clean up calibration handle
#if defined(ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED)
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
adc_cali_delete_scheme_curve_fitting(handle);
#elif defined(ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED)
#else
adc_cali_delete_scheme_line_fitting(handle);
#endif
} else {
-9
View File
@@ -3,7 +3,6 @@ import logging
import esphome.codegen as cg
from esphome.components import sensor, voltage_sampler
from esphome.components.esp32 import (
VARIANT_ESP32S31,
get_esp32_variant,
include_builtin_idf_component,
require_adc_oneshot_iram,
@@ -57,14 +56,6 @@ def validate_config(config: ConfigType) -> ConfigType:
if config[CONF_RAW] and config.get(CONF_ATTENUATION, None) == "auto":
raise cv.Invalid("Automatic attenuation cannot be used when raw output is set")
# The S31 ADC supports a single attenuation level (SOC_ADC_ATTEN_NUM is 1)
if (
CORE.is_esp32
and get_esp32_variant() == VARIANT_ESP32S31
and config.get(CONF_ATTENUATION, "0db") != "0db"
):
raise cv.Invalid("ESP32-S31 only supports 'attenuation: 0db'")
if config.get(CONF_ATTENUATION, None) == "auto" and config.get(CONF_SAMPLES, 1) > 1:
raise cv.Invalid(
"Automatic attenuation cannot be used when multisampling is set"
+1 -2
View File
@@ -162,9 +162,8 @@ void Alpha3::send_request_(uint8_t *request, size_t len) {
auto status =
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->geni_handle_, len,
request, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
if (status) {
if (status)
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status);
}
}
void Alpha3::update() {
+13 -125
View File
@@ -5,7 +5,6 @@ from typing import Any
from esphome import automation
from esphome.automation import Condition
import esphome.codegen as cg
from esphome.components.const import CONF_DESCRIPTION
from esphome.components.logger import request_log_listener
# ENCRYPTION_SCHEMA and validate_encryption_key are re-exported for external
@@ -42,12 +41,10 @@ from esphome.const import (
CONF_TAG,
CONF_THEN,
CONF_TRIGGER_ID,
CONF_TYPE,
CONF_VARIABLES,
)
from esphome.core import CORE, ID, CoroPriority, EsphomeError, coroutine_with_priority
from esphome.cpp_generator import MockObj, TemplateArgsType
from esphome.helpers import fnv1_hash
from esphome.types import ConfigFragmentType, ConfigType
# Compat alias: downstream consumers (e.g. device-builder) referenced the
@@ -128,7 +125,6 @@ SERVICE_ARG_FALLBACK_TYPES: dict[str, MockObj] = {
}
CONF_BATCH_DELAY = "batch_delay"
CONF_CUSTOM_SERVICES = "custom_services"
CONF_EXAMPLE = "example"
CONF_HOMEASSISTANT_SERVICES = "homeassistant_services"
CONF_HOMEASSISTANT_STATES = "homeassistant_states"
CONF_LISTEN_BACKLOG = "listen_backlog"
@@ -232,30 +228,14 @@ def _validate_supports_response(value: Any) -> str:
return cv.enum(SUPPORTS_RESPONSE_OPTIONS, lower=True)(value)
# ESP8266 copies every string of an action into a stack buffer sized by codegen; keep it small
ESP8266_ACTION_STRINGS_MAX_TOTAL = 384
VARIABLE_SCHEMA = cv.Schema(
{
cv.Required(CONF_TYPE): cv.one_of(*SERVICE_ARG_NATIVE_TYPES, lower=True),
cv.Optional(CONF_DESCRIPTION): cv.string_strict,
cv.Optional(CONF_EXAMPLE): cv.string_strict,
}
)
# Accepts the plain `name: type` shorthand or the full mapping form
validate_variable = cv.maybe_simple_value(VARIABLE_SCHEMA, key=CONF_TYPE)
ACTIONS_SCHEMA = automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(UserServiceTrigger),
cv.Exclusive(CONF_SERVICE, group_of_exclusion=CONF_ACTION): cv.valid_name,
cv.Exclusive(CONF_ACTION, group_of_exclusion=CONF_ACTION): cv.valid_name,
cv.Optional(CONF_DESCRIPTION): cv.string_strict,
cv.Optional(CONF_VARIABLES, default={}): cv.Schema(
{
cv.validate_id_name: validate_variable,
cv.validate_id_name: cv.one_of(*SERVICE_ARG_NATIVE_TYPES, lower=True),
}
),
# No default - auto-detected by _auto_detect_supports_response
@@ -372,85 +352,6 @@ CONFIG_SCHEMA = cv.All(
)
def _has_action_metadata(actions: list[ConfigType]) -> bool:
# Empty strings count as unset, matching _action_strings
return any(
conf.get(CONF_DESCRIPTION)
or any(
var_.get(CONF_DESCRIPTION) or var_.get(CONF_EXAMPLE)
for var_ in conf[CONF_VARIABLES].values()
)
for conf in actions
)
def _action_strings(conf: ConfigType, has_metadata: bool) -> list[str | None]:
"""Strings of one action in the table order UserServiceStatic (user_services.h) expects."""
# An empty description or example is treated as unset
strings: list[str | None] = [conf[CONF_ACTION]]
if has_metadata:
strings.append(conf.get(CONF_DESCRIPTION) or None)
for name, var_ in conf[CONF_VARIABLES].items():
strings.append(name)
if has_metadata:
strings += [
var_.get(CONF_DESCRIPTION) or None,
var_.get(CONF_EXAMPLE) or None,
]
return strings
def _action_strings_size(strings: list[str | None]) -> int:
"""Bytes needed to copy every string out of flash, each with its terminator."""
return sum(
len(string.encode("utf-8")) + 1 for string in strings if string is not None
)
def _validate_esp8266_action_strings(config: ConfigType) -> ConfigType:
if not CORE.is_esp8266:
return config
actions = config.get(CONF_ACTIONS, [])
has_metadata = _has_action_metadata(actions)
for conf in actions:
size = _action_strings_size(_action_strings(conf, has_metadata))
if size > ESP8266_ACTION_STRINGS_MAX_TOTAL:
raise cv.Invalid(
f"Action '{conf[CONF_ACTION]}' has {size} bytes of name, variable name, "
f"description and example text; ESP8266 allows at most "
f"{ESP8266_ACTION_STRINGS_MAX_TOTAL} bytes per action"
)
return config
FINAL_VALIDATE_SCHEMA = _validate_esp8266_action_strings
def _add_action_strings(
index: int, strings: list[str | None], interned: dict[str, MockObj]
) -> MockObj:
"""Emit the PROGMEM string table for one action.
Each string is its own PROGMEM array because on ESP8266 .rodata is RAM, and identical
strings are shared between actions through `interned`.
"""
entries: list[MockObj] = []
for string in strings:
if string is None:
entries.append(cg.nullptr)
continue
if (var := interned.get(string)) is None:
var = interned[string] = cg.progmem_array(
ID(f"api_action_str{len(interned)}", is_declaration=True, type=cg.char),
string,
)
entries.append(var)
return cg.progmem_array(
ID(f"api_action{index}_strings", is_declaration=True, type=cg.const_char_ptr),
entries,
)
@coroutine_with_priority(CoroPriority.WEB)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
@@ -470,10 +371,8 @@ async def to_code(config: ConfigType) -> None:
cg.add_define("MAX_API_CONNECTIONS", config[CONF_MAX_CONNECTIONS])
cg.add_define("API_MAX_SEND_QUEUE", config[CONF_MAX_SEND_QUEUE])
actions = config.get(CONF_ACTIONS, [])
has_user_actions = bool(actions) or config[CONF_CUSTOM_SERVICES]
# Set USE_API_USER_DEFINED_ACTIONS if any services are enabled
if has_user_actions:
if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]:
cg.add_define("USE_API_USER_DEFINED_ACTIONS")
# Set USE_API_CUSTOM_SERVICES if external components need dynamic service registration
@@ -486,17 +385,10 @@ async def to_code(config: ConfigType) -> None:
if config[CONF_HOMEASSISTANT_STATES]:
cg.add_define("USE_API_HOMEASSISTANT_STATES")
scratch_size = 0
if actions:
# Metadata is compiled in for every action once any action declares it, because the
# string table layout is fixed by the define rather than per action
has_metadata = _has_action_metadata(actions)
if has_metadata:
cg.add_define("USE_API_USER_DEFINED_ACTION_METADATA")
interned_strings: dict[str, MockObj] = {}
if actions := config.get(CONF_ACTIONS, []):
# Collect all triggers first, then register all at once with initializer_list
triggers: list[cg.MockObj] = []
for index, conf in enumerate(actions):
for conf in actions:
func_args: list[tuple[MockObj, str]] = []
service_template_args: list[MockObj] = [] # User service argument types
@@ -529,23 +421,22 @@ async def to_code(config: ConfigType) -> None:
conf.get(CONF_THEN, [])
)
service_arg_names: list[str] = []
for name, var_ in conf[CONF_VARIABLES].items():
var_type = var_[CONF_TYPE]
if has_non_synchronous and var_type in SERVICE_ARG_FALLBACK_TYPES:
native = SERVICE_ARG_FALLBACK_TYPES[var_type]
if has_non_synchronous and var_ in SERVICE_ARG_FALLBACK_TYPES:
native = SERVICE_ARG_FALLBACK_TYPES[var_]
else:
native = SERVICE_ARG_NATIVE_TYPES[var_type]
native = SERVICE_ARG_NATIVE_TYPES[var_]
service_template_args.append(native)
func_args.append((native, name))
strings = _action_strings(conf, has_metadata)
table = _add_action_strings(index, strings, interned_strings)
if CORE.is_esp8266:
scratch_size = max(scratch_size, _action_strings_size(strings))
service_arg_names.append(name)
# Template args: supports_response mode, then user service arg types
templ = cg.TemplateArguments(supports_response, *service_template_args)
# Key is hashed here because the name is not readable at runtime on ESP8266
trigger = cg.new_Pvariable(
conf[CONF_TRIGGER_ID], templ, table, fnv1_hash(conf[CONF_ACTION])
conf[CONF_TRIGGER_ID],
templ,
conf[CONF_ACTION],
service_arg_names,
)
triggers.append(trigger)
auto = await automation.build_automation(trigger, func_args, conf)
@@ -567,9 +458,6 @@ async def to_code(config: ConfigType) -> None:
cg.add(auto.add_actions([unregister_action]))
# Register all services at once - single allocation, no reallocations
cg.add(var.initialize_user_services(triggers))
if CORE.is_esp8266 and has_user_actions:
# Stack buffer that list-entities copies PROGMEM strings into, sized for the largest action
cg.add_define("API_USER_ACTION_STRINGS_SCRATCH_SIZE", max(scratch_size, 1))
if CONF_ON_CLIENT_CONNECTED in config:
cg.add_define("USE_API_CLIENT_CONNECTED_TRIGGER")
-3
View File
@@ -1034,8 +1034,6 @@ message ListEntitiesServicesArgument {
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS";
string name = 1;
ServiceArgType type = 2;
string description = 3 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_METADATA"];
string example = 4 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_METADATA"];
}
message ListEntitiesServicesResponse {
option (id) = 41;
@@ -1046,7 +1044,6 @@ message ListEntitiesServicesResponse {
fixed32 key = 2 [(force) = true];
repeated ListEntitiesServicesArgument args = 3 [(fixed_vector) = true];
SupportsResponseType supports_response = 4;
string description = 5 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_METADATA"];
}
message ExecuteServiceArgument {
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS";
+1 -2
View File
@@ -2391,9 +2391,8 @@ void APIConnection::process_batch_() {
} else if (payload_size == 0) {
// payload_size == 0 with remove set means encoding hit OOM and the
// connection is being dropped; warn only for a genuinely oversized message
if (!this->flags_.remove) {
if (!this->flags_.remove)
ESP_LOGW(TAG, "Message too large to send: type=%u", item.message_type);
}
this->clear_batch_();
}
return;
-18
View File
@@ -1275,24 +1275,12 @@ uint8_t *ListEntitiesServicesArgument::encode(ProtoWriteBuffer &buffer PROTO_ENC
uint8_t *__restrict__ pos = buffer.get_pos();
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->name);
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, static_cast<uint32_t>(this->type));
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->description);
#endif
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 4, this->example);
#endif
return pos;
}
uint32_t ListEntitiesServicesArgument::calculate_size() const {
uint32_t size = 0;
size += ProtoSize::calc_length(1, this->name.size());
size += this->type ? 2 : 0;
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
size += ProtoSize::calc_length(1, this->description.size());
#endif
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
size += ProtoSize::calc_length(1, this->example.size());
#endif
return size;
}
uint8_t *ListEntitiesServicesResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
@@ -1303,9 +1291,6 @@ uint8_t *ListEntitiesServicesResponse::encode(ProtoWriteBuffer &buffer PROTO_ENC
ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 3, it);
}
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, static_cast<uint32_t>(this->supports_response));
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->description);
#endif
return pos;
}
uint32_t ListEntitiesServicesResponse::calculate_size() const {
@@ -1318,9 +1303,6 @@ uint32_t ListEntitiesServicesResponse::calculate_size() const {
}
}
size += this->supports_response ? 2 : 0;
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
size += ProtoSize::calc_length(1, this->description.size());
#endif
return size;
}
bool ExecuteServiceArgument::decode_varint(uint32_t field_id, proto_varint_value_t value) {
+1 -10
View File
@@ -1317,12 +1317,6 @@ class ListEntitiesServicesArgument final : public ProtoMessage {
public:
StringRef name{};
enums::ServiceArgType type{};
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
StringRef description{};
#endif
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
StringRef example{};
#endif
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -1334,7 +1328,7 @@ class ListEntitiesServicesArgument final : public ProtoMessage {
class ListEntitiesServicesResponse final : public ProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 41;
static constexpr uint8_t ESTIMATED_SIZE = 59;
static constexpr uint8_t ESTIMATED_SIZE = 50;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("list_entities_services_response"); }
#endif
@@ -1342,9 +1336,6 @@ class ListEntitiesServicesResponse final : public ProtoMessage {
uint32_t key{0};
FixedVector<ListEntitiesServicesArgument> args{};
enums::SupportsResponseType supports_response{};
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
StringRef description{};
#endif
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
-9
View File
@@ -1500,12 +1500,6 @@ const char *ListEntitiesServicesArgument::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesServicesArgument"));
dump_field(out, ESPHOME_PSTR("name"), this->name);
dump_field(out, ESPHOME_PSTR("type"), static_cast<enums::ServiceArgType>(this->type));
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
dump_field(out, ESPHOME_PSTR("description"), this->description);
#endif
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
dump_field(out, ESPHOME_PSTR("example"), this->example);
#endif
return out.c_str();
}
const char *ListEntitiesServicesResponse::dump_to(DumpBuffer &out) const {
@@ -1518,9 +1512,6 @@ const char *ListEntitiesServicesResponse::dump_to(DumpBuffer &out) const {
out.append("\n");
}
dump_field(out, ESPHOME_PSTR("supports_response"), static_cast<enums::SupportsResponseType>(this->supports_response));
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
dump_field(out, ESPHOME_PSTR("description"), this->description);
#endif
return out.c_str();
}
const char *ExecuteServiceArgument::dump_to(DumpBuffer &out) const {
+2 -4
View File
@@ -433,10 +433,8 @@ void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call
// Home Assistant subscribes to actions shortly *after* authenticating, so actions
// fired right at connection time (on_client_connected, on_time_sync, ...) can
// arrive before the subscription and are lost - warn instead of failing silently.
ESP_LOGW(TAG, "Home Assistant %s '%s' dropped; %s",
call.is_event ? LOG_STR_LITERAL("event") : LOG_STR_LITERAL("action"), call.service.c_str(),
this->is_connected() ? LOG_STR_LITERAL("client has not subscribed to actions (yet)")
: LOG_STR_LITERAL("no client connected"));
ESP_LOGW(TAG, "Home Assistant %s '%s' dropped; %s", call.is_event ? "event" : "action", call.service.c_str(),
this->is_connected() ? "client has not subscribed to actions (yet)" : "no client connected");
}
}
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
+1 -2
View File
@@ -99,8 +99,7 @@ ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(clie
static constexpr uint8_t SERVICE_YIELD_INTERVAL = 3;
bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) {
UserActionScratch scratch;
auto resp = service->encode_list_service_response(scratch);
auto resp = service->encode_list_service_response();
if (!this->client_->send_message(resp))
return false;
// at_ is this service's index
-43
View File
@@ -1,52 +1,9 @@
#include "user_services.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/string_ref.h"
namespace esphome::api {
StringRef UserServiceStatic::str_(size_t idx, std::span<char> &scratch) const {
const char *s = progmem_read_ptr(&this->strings_[idx]);
if (s == nullptr)
return {};
#ifdef USE_ESP8266
// Codegen sizes the scratch buffer for the largest service; the bound only guards other callers
if (scratch.empty())
return {};
size_t len = strnlen_P(s, scratch.size() - 1);
progmem_memcpy(scratch.data(), s, len);
scratch[len] = '\0';
StringRef ref(scratch.data(), len);
scratch = scratch.subspan(len + 1);
return ref;
#else
return StringRef(s);
#endif
}
ListEntitiesServicesResponse UserServiceStatic::encode_list_service_response_(
std::span<const enums::ServiceArgType> arg_types, std::span<char> scratch) const {
ListEntitiesServicesResponse msg;
msg.name = this->str_(0, scratch);
msg.key = this->key_;
msg.supports_response = this->supports_response_;
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
msg.description = this->str_(1, scratch);
#endif
msg.args.init(arg_types.size());
for (size_t i = 0; i < arg_types.size(); i++) {
size_t base = USER_ACTION_HEADER_STRINGS + i * USER_ACTION_ARG_STRINGS;
auto &arg = msg.args.emplace_back();
arg.type = arg_types[i];
arg.name = this->str_(base, scratch);
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
arg.description = this->str_(base + 1, scratch);
arg.example = this->str_(base + 2, scratch);
#endif
}
return msg;
}
template<> bool get_execute_arg_value<bool>(const ExecuteServiceArgument &arg) { return arg.bool_; }
template<> int32_t get_execute_arg_value<int32_t>(const ExecuteServiceArgument &arg) {
if (arg.legacy_int != 0)
+36 -55
View File
@@ -1,6 +1,5 @@
#pragma once
#include <span>
#include <tuple>
#include <utility>
#include <vector>
@@ -20,9 +19,7 @@ class APIServer;
class UserServiceDescriptor {
public:
/// Build the list-entities message. On ESP8266 the strings live in PROGMEM and are copied into
/// `scratch`, so the returned message is only valid while `scratch` is; other platforms ignore it.
virtual ListEntitiesServicesResponse encode_list_service_response(std::span<char> scratch) = 0;
virtual ListEntitiesServicesResponse encode_list_service_response() = 0;
virtual bool execute_service(const ExecuteServiceRequest &req) = 0;
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
@@ -37,51 +34,29 @@ template<typename T> T get_execute_arg_value(const ExecuteServiceArgument &arg);
template<typename T> enums::ServiceArgType to_service_arg_type();
// Scratch buffer list-entities hands to encode_list_service_response(); only ESP8266 copies into it
#ifdef USE_ESP8266
using UserActionScratch = std::array<char, API_USER_ACTION_STRINGS_SCRATCH_SIZE>;
#else
using UserActionScratch = std::array<char, 0>;
#endif
// Non-template base for YAML-defined services so the list-entities encoder is compiled once.
// All strings live in one PROGMEM pointer table emitted by codegen (see _action_strings in
// __init__.py), so each service costs a single pointer of RAM. Layout: the action name, then
// each argument name; with USE_API_USER_DEFINED_ACTION_METADATA the action description follows
// the name and every argument is (name, description, example). Unset metadata is nullptr.
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
static constexpr size_t USER_ACTION_HEADER_STRINGS = 2;
static constexpr size_t USER_ACTION_ARG_STRINGS = 3;
#else
static constexpr size_t USER_ACTION_HEADER_STRINGS = 1;
static constexpr size_t USER_ACTION_ARG_STRINGS = 1;
#endif
class UserServiceStatic : public UserServiceDescriptor {
// Base class for YAML-defined services (most common case)
// Stores only pointers to string literals in flash - no heap allocation
template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
public:
UserServiceStatic(const char *const *strings, uint32_t key,
enums::SupportsResponseType supports_response = enums::SUPPORTS_RESPONSE_NONE)
: strings_(strings), key_(key), supports_response_(supports_response) {}
UserServiceBase(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names,
enums::SupportsResponseType supports_response = enums::SUPPORTS_RESPONSE_NONE)
: name_(name), arg_names_(arg_names), supports_response_(supports_response) {
this->key_ = fnv1_hash(name);
}
protected:
ListEntitiesServicesResponse encode_list_service_response_(std::span<const enums::ServiceArgType> arg_types,
std::span<char> scratch) const;
/// Reference table entry `idx`; nullptr gives an empty StringRef.
/// On ESP8266 the bytes are copied out of PROGMEM into `scratch` with a terminator, and the span
/// is advanced past the copy.
StringRef str_(size_t idx, std::span<char> &scratch) const;
const char *const *strings_; // PROGMEM pointer table, read with progmem_read_ptr()
uint32_t key_;
enums::SupportsResponseType supports_response_;
};
template<typename... Ts> class UserServiceBase : public UserServiceStatic {
public:
using UserServiceStatic::UserServiceStatic;
ListEntitiesServicesResponse encode_list_service_response(std::span<char> scratch) override {
ListEntitiesServicesResponse encode_list_service_response() override {
ListEntitiesServicesResponse msg;
msg.name = StringRef(this->name_);
msg.key = this->key_;
msg.supports_response = this->supports_response_;
std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
return this->encode_list_service_response_(arg_types, scratch);
msg.args.init(sizeof...(Ts));
for (size_t i = 0; i < sizeof...(Ts); i++) {
auto &arg = msg.args.emplace_back();
arg.type = arg_types[i];
arg.name = StringRef(this->arg_names_[i]);
}
return msg;
}
bool execute_service(const ExecuteServiceRequest &req) override {
@@ -114,6 +89,12 @@ template<typename... Ts> class UserServiceBase : public UserServiceStatic {
void execute_(const ArgsContainer &args, uint32_t call_id, bool return_response, std::index_sequence<S...> /*type*/) {
this->execute(call_id, return_response, (get_execute_arg_value<Ts>(args[S]))...);
}
// Pointers to string literals in flash - no heap allocation
const char *name_;
std::array<const char *, sizeof...(Ts)> arg_names_;
uint32_t key_{0};
enums::SupportsResponseType supports_response_{enums::SUPPORTS_RESPONSE_NONE};
};
// Separate class for custom_api_device services (rare case)
@@ -125,7 +106,7 @@ template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor
this->key_ = fnv1_hash(this->name_.c_str());
}
ListEntitiesServicesResponse encode_list_service_response(std::span<char> /*scratch*/) override {
ListEntitiesServicesResponse encode_list_service_response() override {
ListEntitiesServicesResponse msg;
msg.name = StringRef(this->name_);
msg.key = this->key_;
@@ -186,8 +167,8 @@ template<typename... Ts>
class UserServiceTrigger<enums::SUPPORTS_RESPONSE_NONE, Ts...> final : public UserServiceBase<Ts...>,
public Trigger<Ts...> {
public:
UserServiceTrigger(const char *const *strings, uint32_t key)
: UserServiceBase<Ts...>(strings, key, enums::SUPPORTS_RESPONSE_NONE) {}
UserServiceTrigger(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
: UserServiceBase<Ts...>(name, arg_names, enums::SUPPORTS_RESPONSE_NONE) {}
protected:
void execute(uint32_t /*call_id*/, bool /*return_response*/, Ts... x) override { this->trigger(x...); }
@@ -198,8 +179,8 @@ template<typename... Ts>
class UserServiceTrigger<enums::SUPPORTS_RESPONSE_OPTIONAL, Ts...> final : public UserServiceBase<Ts...>,
public Trigger<uint32_t, bool, Ts...> {
public:
UserServiceTrigger(const char *const *strings, uint32_t key)
: UserServiceBase<Ts...>(strings, key, enums::SUPPORTS_RESPONSE_OPTIONAL) {}
UserServiceTrigger(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
: UserServiceBase<Ts...>(name, arg_names, enums::SUPPORTS_RESPONSE_OPTIONAL) {}
protected:
void execute(uint32_t call_id, bool return_response, Ts... x) override {
@@ -212,8 +193,8 @@ template<typename... Ts>
class UserServiceTrigger<enums::SUPPORTS_RESPONSE_ONLY, Ts...> final : public UserServiceBase<Ts...>,
public Trigger<uint32_t, Ts...> {
public:
UserServiceTrigger(const char *const *strings, uint32_t key)
: UserServiceBase<Ts...>(strings, key, enums::SUPPORTS_RESPONSE_ONLY) {}
UserServiceTrigger(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
: UserServiceBase<Ts...>(name, arg_names, enums::SUPPORTS_RESPONSE_ONLY) {}
protected:
void execute(uint32_t call_id, bool /*return_response*/, Ts... x) override { this->trigger(call_id, x...); }
@@ -224,8 +205,8 @@ template<typename... Ts>
class UserServiceTrigger<enums::SUPPORTS_RESPONSE_STATUS, Ts...> final : public UserServiceBase<Ts...>,
public Trigger<uint32_t, Ts...> {
public:
UserServiceTrigger(const char *const *strings, uint32_t key)
: UserServiceBase<Ts...>(strings, key, enums::SUPPORTS_RESPONSE_STATUS) {}
UserServiceTrigger(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
: UserServiceBase<Ts...>(name, arg_names, enums::SUPPORTS_RESPONSE_STATUS) {}
protected:
void execute(uint32_t call_id, bool /*return_response*/, Ts... x) override { this->trigger(call_id, x...); }
+2 -4
View File
@@ -23,10 +23,8 @@ void AQISensor::setup() {
void AQISensor::dump_config() {
ESP_LOGCONFIG(TAG, "AQI Sensor:");
ESP_LOGCONFIG(TAG, " Calculation Type: %s",
this->aqi_calc_type_ == AQI_TYPE ? LOG_STR_LITERAL("AQI") : LOG_STR_LITERAL("CAQI"));
ESP_LOGCONFIG(TAG, " Extended Range: %s",
this->extended_range_ ? LOG_STR_LITERAL("enabled") : LOG_STR_LITERAL("disabled"));
ESP_LOGCONFIG(TAG, " Calculation Type: %s", this->aqi_calc_type_ == AQI_TYPE ? "AQI" : "CAQI");
ESP_LOGCONFIG(TAG, " Extended Range: %s", this->extended_range_ ? "enabled" : "disabled");
if (this->pm_2_5_sensor_ != nullptr) {
ESP_LOGCONFIG(TAG, " PM2.5 Sensor: '%s'", this->pm_2_5_sensor_->get_name().c_str());
}
@@ -100,15 +100,13 @@ void MultiClickTriggerBase::schedule_is_valid_(uint32_t min_length) {
}
this->is_valid_ = false;
this->set_timeout(MULTICLICK_IS_VALID_ID, min_length, [this]() {
ESP_LOGV(TAG, "Multi Click: You can now %s the button.",
this->parent_->state ? LOG_STR_LITERAL("RELEASE") : LOG_STR_LITERAL("PRESS"));
ESP_LOGV(TAG, "Multi Click: You can now %s the button.", this->parent_->state ? "RELEASE" : "PRESS");
this->is_valid_ = true;
});
}
void MultiClickTriggerBase::schedule_is_not_valid_(uint32_t max_length) {
this->set_timeout(MULTICLICK_IS_NOT_VALID_ID, max_length, [this]() {
ESP_LOGV(TAG, "Multi Click: You waited too long to %s.",
this->parent_->state ? LOG_STR_LITERAL("RELEASE") : LOG_STR_LITERAL("PRESS"));
ESP_LOGV(TAG, "Multi Click: You waited too long to %s.", this->parent_->state ? "RELEASE" : "PRESS");
this->is_valid_ = false;
this->schedule_cooldown_();
});
+1 -2
View File
@@ -62,9 +62,8 @@ BdkActivityState bdk_scan_state(uint8_t activity_idx) {
uint8_t bdk_scan_acquire_activity() {
uint8_t idx = app_ble_get_idle_actv_idx_handle(SCAN_ACTV);
if (idx == INVALID_ACTIVITY_IDX) {
if (idx == INVALID_ACTIVITY_IDX)
ESP_LOGE(TAG, "Scan start failed: no idle activity handle");
}
return idx;
}
+5 -10
View File
@@ -181,9 +181,8 @@ void BK72xxBLE::enable() {
break;
}
}
if (!bdaddr_live) {
if (!bdaddr_live)
ESP_LOGW(TAG, "Controller address still unset after init; BLE stack may not have started");
}
#endif
this->state_ = BLEComponentState::ACTIVE;
@@ -211,9 +210,8 @@ void BK72xxBLE::loop() {
// Re-check a settled scan; scan_start() refills the bring-up budget.
// WARN: the only report of a drop that recovers inside its budget.
if (this->scan_start(this->requested_.interval, this->requested_.window, this->requested_.active) !=
ScanOpResult::SETTLED) {
ScanOpResult::SETTLED)
ESP_LOGW(TAG, "Controller dropped the scan; restarting");
}
}
// Drain the lock-free ring filled by the BLE task; all per-report work runs
@@ -232,9 +230,8 @@ void BK72xxBLE::loop() {
// Log dropped reports — only reachable when reports were processed; drops can
// only occur while the queue is full, and only this loop drains it.
uint16_t dropped = this->report_queue_.get_and_reset_dropped_count();
if (dropped > 0) {
if (dropped > 0)
ESP_LOGW(TAG, "Dropped %u scan reports due to queue overflow", dropped);
}
}
void BK72xxBLE::get_mac_lsb_first(uint8_t out[MAC_ADDRESS_SIZE]) const {
@@ -452,9 +449,8 @@ ScanOpResult BK72xxBLE::advance_stop_(BdkActivityState state, bool ready) {
if (!ready) {
// Acting mid-operation could delete an activity whose start lands
// afterwards, leaking the slot with the radio on; wait.
if (this->last_result_ == ScanOpResult::SETTLED) {
if (this->last_result_ == ScanOpResult::SETTLED)
ESP_LOGD(TAG, "Scan stop deferred (controller busy)");
}
return ScanOpResult::PENDING;
}
// Settled, so CREATED unambiguously means "never started".
@@ -478,9 +474,8 @@ ScanOpResult BK72xxBLE::advance_start_(BdkActivityState state, bool ready) {
return ScanOpResult::PENDING;
}
if (!ready) {
if (this->last_result_ == ScanOpResult::SETTLED) {
if (this->last_result_ == ScanOpResult::SETTLED)
ESP_LOGD(TAG, "Scan start deferred (controller busy)");
}
return ScanOpResult::PENDING;
}
if (state == BdkActivityState::CREATED) {
@@ -69,9 +69,8 @@ void BK72xxBLETracker::on_ota_global_state(ota::OTAState state, float progress,
this->stop_scan();
// The transfer starves the loop; a deferred stop would leave the radio
// scanning for the whole update, so drain it here, bounded.
if (!this->parent_->flush_pending_stop(OTA_STOP_FLUSH_MS)) {
if (!this->parent_->flush_pending_stop(OTA_STOP_FLUSH_MS))
ESP_LOGE(TAG, "Scan still stopping at OTA start; the radio may contend with the update");
}
} else if (state == ota::OTA_ERROR || state == ota::OTA_ABORT) {
// On success the device reboots, so restore only on a failed/aborted update;
// loop() restarts the scan on its next iteration (continuous idle branch).
@@ -80,9 +80,8 @@ void BLEBinaryOutput::write_state(bool state) {
esp_err_t err =
esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->char_handle_,
sizeof(state_as_uint), &state_as_uint, this->write_type_, ESP_GATT_AUTH_REQ_NONE);
if (err != ESP_GATT_OK) {
if (err != ESP_GATT_OK)
ESP_LOGW(TAG, "[%s] Write error, err=%d", this->char_uuid_.to_str(char_buf), err);
}
}
} // namespace esphome::ble_client
+2 -4
View File
@@ -327,12 +327,10 @@ void BME680Component::read_data_() {
ESP_LOGD(TAG, "Got temperature=%.1f°C pressure=%.1fhPa humidity=%.1f%% gas_resistance=%.1fΩ", temperature, pressure,
humidity, gas_resistance);
if (!gas_valid) {
if (!gas_valid)
ESP_LOGW(TAG, "Gas measurement unsuccessful, reading invalid!");
}
if (!heat_stable) {
if (!heat_stable)
ESP_LOGW(TAG, "Heater unstable, reading invalid! (Normal for a few readings after a power cycle)");
}
if (this->temperature_sensor_ != nullptr)
this->temperature_sensor_->publish_state(temperature);
@@ -162,9 +162,8 @@ void BME680BSECComponent::dump_config() {
" Supply Voltage: %sV\n"
" Sample Rate: %s\n"
" State Save Interval: %" PRIu32 "ms",
this->temperature_offset_,
this->iaq_mode_ == IAQ_MODE_STATIC ? LOG_STR_LITERAL("Static") : LOG_STR_LITERAL("Mobile"),
this->supply_voltage_ == SUPPLY_VOLTAGE_3V3 ? LOG_STR_LITERAL("3.3") : LOG_STR_LITERAL("1.8"),
this->temperature_offset_, this->iaq_mode_ == IAQ_MODE_STATIC ? "Static" : "Mobile",
this->supply_voltage_ == SUPPLY_VOLTAGE_3V3 ? "3.3" : "1.8",
BME680_BSEC_SAMPLE_RATE_LOG(this->sample_rate_), this->state_save_interval_ms_);
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
+137 -137
View File
@@ -7,146 +7,146 @@ namespace esphome::captive_portal {
#ifdef USE_CAPTIVE_PORTAL_GZIP
constexpr uint8_t INDEX_GZ[] PROGMEM = {
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x95, 0x56, 0x6d, 0x8f, 0xdb, 0x36, 0x0c, 0xfe, 0xbe,
0x5f, 0xa1, 0x79, 0xdd, 0x6a, 0xaf, 0xb1, 0xfc, 0x92, 0x4b, 0xda, 0x3a, 0x96, 0x8b, 0xee, 0xd6, 0x62, 0x03, 0xd6,
0xad, 0xc0, 0xdd, 0xba, 0x0f, 0x45, 0x01, 0x2b, 0x32, 0x1d, 0xab, 0x27, 0x4b, 0x9e, 0xa4, 0xbc, 0x35, 0xc8, 0x7e,
0xfb, 0x20, 0xdb, 0xc9, 0xe5, 0x8a, 0x16, 0xd8, 0x10, 0xc4, 0xa0, 0x44, 0xf2, 0xe1, 0x8b, 0x28, 0x52, 0xf9, 0xb7,
0x95, 0x62, 0x76, 0xdf, 0x01, 0x6a, 0x6c, 0x2b, 0x8a, 0xdc, 0x7d, 0x91, 0xa0, 0x72, 0x45, 0x40, 0x16, 0x79, 0x03,
0xb4, 0x2a, 0xf2, 0x16, 0x2c, 0x45, 0xac, 0xa1, 0xda, 0x80, 0x25, 0x7f, 0xde, 0xbe, 0x0e, 0x9f, 0x15, 0xb9, 0xe0,
0xf2, 0x0e, 0x69, 0x10, 0x84, 0x33, 0x25, 0x51, 0xa3, 0xa1, 0x26, 0x15, 0xb5, 0x34, 0xe3, 0x2d, 0x5d, 0xc1, 0xa8,
0x22, 0x69, 0x0b, 0x64, 0xc3, 0x61, 0xdb, 0x29, 0x6d, 0x11, 0x53, 0xd2, 0x82, 0xb4, 0xc4, 0xdb, 0xf2, 0xca, 0x36,
0xa4, 0x82, 0x0d, 0x67, 0x10, 0xf6, 0x8b, 0x09, 0x97, 0xdc, 0x72, 0x2a, 0x42, 0xc3, 0xa8, 0x00, 0x92, 0x4c, 0xd6,
0x06, 0x74, 0xbf, 0xa0, 0x4b, 0x01, 0x44, 0x2a, 0xaf, 0xc8, 0x0d, 0xd3, 0xbc, 0xb3, 0xc8, 0xb9, 0x4a, 0x5a, 0x55,
0xad, 0x05, 0x20, 0xa6, 0x95, 0x31, 0x4a, 0xf3, 0x15, 0x97, 0x45, 0xa5, 0xd8, 0xba, 0x05, 0x69, 0xb1, 0x50, 0x8c,
0x5a, 0xae, 0x24, 0x36, 0x40, 0x35, 0x6b, 0x08, 0x21, 0xe5, 0x0b, 0x43, 0x37, 0x50, 0xfe, 0xf0, 0x83, 0x7f, 0x16,
0x5a, 0x81, 0x7d, 0x25, 0xc0, 0x91, 0xe6, 0xa7, 0xfd, 0x2d, 0x5d, 0xfd, 0x4e, 0x5b, 0xf0, 0x4b, 0x6a, 0x78, 0x05,
0x65, 0xf0, 0x3e, 0xfe, 0x80, 0x8d, 0xdd, 0x0b, 0xc0, 0x15, 0x37, 0x9d, 0xa0, 0x7b, 0x52, 0x2e, 0x85, 0x62, 0x77,
0x65, 0xb0, 0xa8, 0xd7, 0x92, 0x39, 0x70, 0x04, 0x3e, 0x04, 0x07, 0x01, 0x16, 0x49, 0xf2, 0x86, 0xda, 0x06, 0xb7,
0x74, 0xe7, 0x0f, 0x04, 0x97, 0x7e, 0xfa, 0xa3, 0x0f, 0x4f, 0x92, 0x38, 0x0e, 0x26, 0xfd, 0x27, 0x0e, 0xa2, 0x24,
0x8e, 0x17, 0x1a, 0xec, 0x5a, 0x4b, 0x64, 0xfd, 0x32, 0xef, 0xa8, 0x6d, 0x50, 0x45, 0xbc, 0x37, 0x49, 0x8a, 0x92,
0xe7, 0x38, 0x9d, 0xfd, 0x86, 0x9f, 0xa2, 0x2b, 0x9c, 0xce, 0xd8, 0xd3, 0x70, 0x86, 0x92, 0xab, 0x70, 0x86, 0xd2,
0x14, 0xcf, 0x50, 0xfc, 0xc9, 0x43, 0x35, 0x17, 0x82, 0x78, 0x52, 0x49, 0xf0, 0x90, 0xb1, 0x5a, 0xdd, 0x01, 0xf1,
0xd8, 0x5a, 0x6b, 0x90, 0xf6, 0x5a, 0x09, 0xa5, 0xbd, 0xa8, 0xf8, 0xe6, 0x7f, 0x01, 0x5a, 0x4d, 0xa5, 0xa9, 0x95,
0x6e, 0x89, 0xd7, 0xa7, 0xdb, 0x7f, 0x74, 0x90, 0x47, 0xe4, 0x3e, 0xc1, 0x05, 0x33, 0x1c, 0xf2, 0x4a, 0x3c, 0x87,
0xf8, 0xcc, 0x8b, 0x8a, 0x32, 0x38, 0x9e, 0xa3, 0xb7, 0x2e, 0xfa, 0x31, 0x1e, 0xed, 0xbf, 0x2f, 0x73, 0xb3, 0x59,
0xa1, 0x5d, 0x2b, 0xa4, 0x21, 0x5e, 0x63, 0x6d, 0x97, 0x45, 0xd1, 0x76, 0xbb, 0xc5, 0xdb, 0x29, 0x56, 0x7a, 0x15,
0xa5, 0x71, 0x1c, 0x47, 0x66, 0xb3, 0xf2, 0xd0, 0x70, 0xf2, 0x5e, 0x7a, 0xe5, 0xa1, 0x06, 0xf8, 0xaa, 0xb1, 0x3d,
0x5d, 0x3c, 0x3a, 0xc0, 0x31, 0x77, 0x12, 0x45, 0xf9, 0xe1, 0xc2, 0x8a, 0xbc, 0xb0, 0x02, 0x2f, 0x2e, 0xf2, 0xf6,
0xb8, 0x0f, 0xf3, 0x29, 0x4d, 0x51, 0x8a, 0xe2, 0xfe, 0x97, 0x86, 0x8e, 0x1e, 0x57, 0xe1, 0x67, 0x2b, 0x74, 0xb1,
0x72, 0x54, 0x3b, 0x0f, 0x9f, 0x9f, 0x75, 0x13, 0xb7, 0xb3, 0x49, 0xe2, 0xfb, 0x0d, 0xa7, 0xf0, 0xcb, 0xfc, 0x72,
0x1d, 0xa6, 0xef, 0x2e, 0x05, 0x9c, 0xb5, 0x26, 0x79, 0x37, 0xa7, 0x33, 0x34, 0x1b, 0x77, 0x66, 0xa1, 0xa3, 0xcf,
0x2b, 0x34, 0xdb, 0xa4, 0x4d, 0xd2, 0x86, 0xf3, 0x70, 0x46, 0xa7, 0x68, 0x3a, 0x3a, 0x32, 0x45, 0xd3, 0x4d, 0xda,
0xcc, 0xdf, 0xcd, 0x2f, 0xf7, 0xc2, 0xe9, 0xa7, 0xc7, 0x2e, 0xb9, 0x59, 0x59, 0xde, 0x47, 0xae, 0x2f, 0x23, 0xc7,
0x1f, 0x15, 0x97, 0x7e, 0xe9, 0xf2, 0x0f, 0x96, 0x35, 0x7e, 0x19, 0x31, 0x25, 0x6b, 0xbe, 0xc2, 0x1f, 0x8d, 0x92,
0x65, 0x80, 0x6d, 0x03, 0xd2, 0x3f, 0xa9, 0xfa, 0x36, 0x38, 0xd8, 0x9e, 0xe3, 0x7f, 0x81, 0x73, 0xae, 0x7f, 0xcb,
0xad, 0x00, 0x62, 0xb1, 0xbb, 0xa1, 0x93, 0x2f, 0xdc, 0x8a, 0x9f, 0xf6, 0xbf, 0x56, 0x7e, 0xd9, 0x52, 0x56, 0x06,
0x98, 0x4b, 0x09, 0xfa, 0x16, 0x76, 0x96, 0x94, 0x6f, 0x5e, 0x5e, 0xa3, 0x97, 0x55, 0xa5, 0xc1, 0x98, 0x0c, 0x95,
0x4f, 0x2c, 0x6e, 0x29, 0xfb, 0xba, 0x7a, 0x93, 0x3c, 0xd4, 0xfe, 0x8b, 0xbf, 0xe6, 0xe8, 0x77, 0xb0, 0x5b, 0xa5,
0xef, 0x46, 0x7d, 0x67, 0x7f, 0xe1, 0xae, 0x91, 0x26, 0x5f, 0x85, 0x91, 0x60, 0xcb, 0x60, 0xc2, 0xbf, 0x2e, 0x60,
0x0c, 0xaf, 0xca, 0x60, 0x42, 0xbf, 0x2e, 0xd1, 0x19, 0x77, 0x79, 0x2d, 0xa6, 0x9d, 0xc1, 0x46, 0x70, 0x06, 0x7e,
0x12, 0xe0, 0x5a, 0xe9, 0x57, 0x94, 0x35, 0x0f, 0x12, 0xe4, 0x5c, 0x51, 0xf7, 0x38, 0x4c, 0x03, 0xb5, 0x30, 0x42,
0xf9, 0x65, 0xc5, 0x37, 0x65, 0xb0, 0x50, 0x98, 0x09, 0x6a, 0x8c, 0x6b, 0x19, 0xc4, 0x39, 0xe7, 0xc2, 0x29, 0x27,
0x6a, 0x88, 0xf4, 0x97, 0xdb, 0x37, 0xbf, 0x91, 0x32, 0xa7, 0x43, 0x47, 0xf4, 0xbe, 0xf3, 0x50, 0x2f, 0x4c, 0xbc,
0x51, 0x30, 0x14, 0x50, 0xdb, 0xbe, 0xe2, 0x7d, 0x8b, 0xb5, 0x31, 0x3c, 0x38, 0xe6, 0xa6, 0xa3, 0xf2, 0x73, 0x31,
0x17, 0x93, 0x57, 0xe4, 0x91, 0xe3, 0x15, 0x79, 0x44, 0x8b, 0x47, 0x07, 0xe9, 0xf7, 0xcd, 0xed, 0x2e, 0x38, 0x3a,
0x6b, 0x7f, 0xaf, 0x41, 0xef, 0x6f, 0x40, 0x00, 0xb3, 0x4a, 0xfb, 0x25, 0xbe, 0x54, 0x74, 0x45, 0x01, 0x3b, 0x7b,
0x3d, 0x36, 0x5c, 0x8b, 0xdd, 0xe6, 0x44, 0x61, 0x25, 0x99, 0xe0, 0xec, 0x8e, 0x9c, 0x23, 0x0e, 0x0e, 0x1c, 0x6f,
0xa8, 0x58, 0xc3, 0x49, 0x86, 0xe2, 0x5a, 0xb1, 0xb5, 0xf1, 0x83, 0xe3, 0x44, 0x63, 0xda, 0x75, 0x20, 0xab, 0xeb,
0x86, 0x8b, 0xca, 0x57, 0xc1, 0x31, 0xb8, 0x3f, 0xe9, 0xcf, 0x8c, 0xbb, 0x59, 0xf0, 0x5e, 0x83, 0xf8, 0x87, 0x3c,
0x76, 0xd3, 0xe0, 0xf1, 0x87, 0x32, 0xc0, 0x7d, 0xfc, 0xe5, 0xfd, 0x48, 0x70, 0xd7, 0xfb, 0xc9, 0xae, 0x15, 0x13,
0x17, 0x7a, 0x38, 0x9f, 0x05, 0xc7, 0xf2, 0x18, 0x1c, 0x83, 0x45, 0x1e, 0x0d, 0x8d, 0xbd, 0xc8, 0xfb, 0x96, 0xdb,
0x8f, 0x94, 0x9e, 0x32, 0x0d, 0x80, 0x7d, 0xd0, 0xe2, 0x7f, 0x3c, 0x2c, 0xd5, 0x2e, 0x34, 0xfc, 0x13, 0x97, 0xab,
0x8c, 0xcb, 0x06, 0x34, 0xb7, 0xc7, 0x8a, 0x6f, 0x26, 0x5c, 0x76, 0x6b, 0x7b, 0xe8, 0x68, 0x55, 0x39, 0xce, 0xac,
0xdb, 0x2d, 0x6a, 0x25, 0xad, 0x93, 0x84, 0x2c, 0x81, 0xf6, 0x38, 0xf0, 0xfb, 0xe6, 0x93, 0x3d, 0x9f, 0x7d, 0x7f,
0x5c, 0xaa, 0x6a, 0x7f, 0x70, 0x19, 0x0a, 0xa9, 0xe0, 0x2b, 0x99, 0x31, 0x90, 0x16, 0xf4, 0xa0, 0x54, 0xd3, 0x96,
0x8b, 0x7d, 0x66, 0xa8, 0x34, 0xa1, 0x01, 0xcd, 0xeb, 0xe3, 0x72, 0x6d, 0xad, 0x92, 0x07, 0xe6, 0x9a, 0x6d, 0xf6,
0x5d, 0x5d, 0xd7, 0x0b, 0xb6, 0xd6, 0x46, 0xe9, 0xac, 0x53, 0xbc, 0xd7, 0x5b, 0x52, 0x76, 0xb7, 0xd2, 0x6a, 0x2d,
0xab, 0x70, 0x14, 0x4a, 0x6a, 0x3a, 0x05, 0xb6, 0x58, 0x2a, 0x5d, 0x81, 0xce, 0xe2, 0x91, 0x08, 0x35, 0xad, 0xf8,
0xda, 0x64, 0x78, 0xaa, 0xa1, 0x5d, 0x0c, 0xee, 0x24, 0x71, 0xfc, 0xfd, 0xe2, 0xe4, 0x79, 0x7c, 0xe9, 0x37, 0x4e,
0x9d, 0x94, 0xe0, 0x12, 0xc2, 0xa1, 0x57, 0x66, 0x29, 0xbe, 0xd2, 0xd0, 0x1e, 0x5b, 0xca, 0xe5, 0xa5, 0xf7, 0xae,
0xa2, 0x16, 0x2d, 0x97, 0xc3, 0x28, 0xcd, 0xd2, 0x79, 0xdc, 0xed, 0x16, 0xe3, 0xe4, 0xca, 0xb8, 0xec, 0x11, 0xfa,
0xf9, 0x75, 0x3c, 0x15, 0xc9, 0xe1, 0xe3, 0xda, 0x58, 0x5e, 0xef, 0xc3, 0x71, 0x24, 0x67, 0xa6, 0xa3, 0x0c, 0xc2,
0x25, 0xd8, 0x2d, 0x80, 0x5c, 0xf4, 0xb0, 0x21, 0xb7, 0xd0, 0x9a, 0x53, 0x6a, 0x4e, 0x70, 0xb5, 0x80, 0xdd, 0x19,
0xa6, 0xaf, 0xe5, 0xc3, 0x7f, 0x96, 0x76, 0x05, 0x76, 0x68, 0xa9, 0x5e, 0x71, 0x19, 0x2e, 0x95, 0xb5, 0xaa, 0xcd,
0xc2, 0xa7, 0xdd, 0x6e, 0x31, 0x6e, 0x39, 0xb0, 0x2c, 0x89, 0xbb, 0xdd, 0xb1, 0x1f, 0xc3, 0xa7, 0x7c, 0x5f, 0xd5,
0xcf, 0x68, 0x12, 0x7f, 0x21, 0xc7, 0x55, 0x5d, 0xa7, 0xcb, 0xfa, 0x94, 0xe3, 0xa4, 0xdb, 0x21, 0xa3, 0x04, 0xaf,
0x46, 0xb8, 0x1e, 0x09, 0xc5, 0xe7, 0xd4, 0x26, 0xb3, 0x6e, 0x87, 0x92, 0xcb, 0xcc, 0xb8, 0x89, 0xea, 0xa6, 0x8e,
0xab, 0xb5, 0x22, 0x8f, 0x86, 0x97, 0x8e, 0xab, 0x8c, 0x22, 0x77, 0x19, 0x2e, 0xf2, 0x26, 0x41, 0xbc, 0x22, 0x2d,
0x65, 0xc5, 0x45, 0xdb, 0xcb, 0xa3, 0x26, 0x39, 0xb1, 0x9a, 0xa4, 0x78, 0xd0, 0xd2, 0x06, 0x5e, 0xef, 0x7d, 0x71,
0xad, 0xa4, 0x04, 0x66, 0xb9, 0x5c, 0x21, 0xab, 0xd0, 0x98, 0x02, 0x8c, 0x71, 0xbe, 0xd4, 0xc5, 0x5b, 0x01, 0xd4,
0x00, 0xda, 0x52, 0x6e, 0x71, 0x1e, 0x0d, 0xf2, 0x43, 0x13, 0xe0, 0x15, 0x91, 0x60, 0xcf, 0xd7, 0xbe, 0x99, 0x0e,
0x06, 0x6e, 0xc0, 0x3a, 0x24, 0x67, 0x60, 0x5a, 0xe4, 0x6e, 0x3a, 0x23, 0xda, 0x5f, 0x60, 0x12, 0x6d, 0x79, 0xcd,
0xdd, 0xeb, 0xa6, 0xc8, 0xfb, 0x22, 0x77, 0x08, 0x2e, 0xcf, 0xc3, 0xd3, 0xab, 0xa7, 0x04, 0xc8, 0x95, 0x6d, 0xc8,
0x34, 0x45, 0x9d, 0xa0, 0x0c, 0x1a, 0x25, 0x2a, 0xd0, 0xe4, 0xe6, 0xe6, 0xd7, 0x9f, 0x0b, 0xe7, 0xcc, 0xbd, 0x5e,
0x67, 0xee, 0x06, 0x35, 0x47, 0x8c, 0x5a, 0xf3, 0xab, 0xe1, 0xc1, 0xd5, 0x51, 0x63, 0xb6, 0x4a, 0x57, 0x0f, 0x30,
0xde, 0x8e, 0x9b, 0x03, 0x4e, 0xff, 0xef, 0xaf, 0x4a, 0x71, 0x43, 0x37, 0x90, 0x47, 0xe3, 0x22, 0x8f, 0x9c, 0xc3,
0x03, 0xbf, 0x19, 0xe5, 0x9a, 0xa4, 0xf8, 0xe3, 0xf6, 0x25, 0xfa, 0xb3, 0xab, 0xa8, 0x85, 0x21, 0x6d, 0x7d, 0x54,
0x2d, 0xd8, 0x46, 0x55, 0xe4, 0xed, 0x1f, 0x37, 0xb7, 0xe7, 0x08, 0xd7, 0xbd, 0x10, 0x02, 0xc9, 0x86, 0xa7, 0xdf,
0x5a, 0x58, 0xde, 0x51, 0x6d, 0x7b, 0xd8, 0xd0, 0x35, 0x98, 0x53, 0x0c, 0x3d, 0xbf, 0xe6, 0x02, 0x86, 0x30, 0x06,
0xc5, 0x02, 0x9d, 0xbc, 0x3a, 0x59, 0xfb, 0xcc, 0xaf, 0x68, 0x38, 0xed, 0x68, 0x38, 0xfa, 0xa8, 0x7f, 0x05, 0xff,
0x0b, 0x54, 0xcf, 0x54, 0x8b, 0x15, 0x0b, 0x00, 0x00};
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x95, 0x16, 0x6b, 0x8f, 0xdb, 0x36, 0xf2, 0x7b, 0x7f,
0x05, 0x8f, 0x4d, 0x1b, 0xa9, 0xb1, 0xa8, 0x87, 0xd7, 0xde, 0x44, 0x96, 0x54, 0xa4, 0x7b, 0x2d, 0x5a, 0xa0, 0x69,
0x03, 0xec, 0x36, 0xf7, 0x21, 0x08, 0xb0, 0x34, 0x39, 0xb2, 0x98, 0xa5, 0x48, 0x1d, 0x49, 0xbf, 0x62, 0xf8, 0x7e,
0xfb, 0x81, 0x92, 0xec, 0xf5, 0x2e, 0x9a, 0x03, 0x0e, 0x86, 0x85, 0x19, 0xce, 0x7b, 0x38, 0x0f, 0x16, 0xff, 0xe0,
0x9a, 0xb9, 0x7d, 0x07, 0xa8, 0x71, 0xad, 0xac, 0x0a, 0xff, 0x45, 0x92, 0xaa, 0x55, 0x09, 0xaa, 0x2a, 0x1a, 0xa0,
0xbc, 0x2a, 0x5a, 0x70, 0x14, 0xb1, 0x86, 0x1a, 0x0b, 0xae, 0xfc, 0xeb, 0xee, 0x97, 0xe8, 0x75, 0x55, 0x48, 0xa1,
0x1e, 0x90, 0x01, 0x59, 0x0a, 0xa6, 0x15, 0x6a, 0x0c, 0xd4, 0x25, 0xa7, 0x8e, 0xe6, 0xa2, 0xa5, 0x2b, 0x18, 0x45,
0x14, 0x6d, 0xa1, 0xdc, 0x08, 0xd8, 0x76, 0xda, 0x38, 0xc4, 0xb4, 0x72, 0xa0, 0x5c, 0x89, 0xb7, 0x82, 0xbb, 0xa6,
0xe4, 0xb0, 0x11, 0x0c, 0xa2, 0x1e, 0x99, 0x08, 0x25, 0x9c, 0xa0, 0x32, 0xb2, 0x8c, 0x4a, 0x28, 0xd3, 0xc9, 0xda,
0x82, 0xe9, 0x11, 0xba, 0x94, 0x50, 0x2a, 0x8d, 0xab, 0xc2, 0x32, 0x23, 0x3a, 0x87, 0xbc, 0xab, 0x65, 0xab, 0xf9,
0x5a, 0x42, 0x15, 0xc7, 0xd4, 0x5a, 0x70, 0x36, 0x16, 0x8a, 0xc3, 0x8e, 0xd0, 0x6b, 0xb8, 0xa6, 0x2c, 0x4d, 0xc8,
0x67, 0xfb, 0x0d, 0xd7, 0x6c, 0xdd, 0x82, 0x72, 0x44, 0x6a, 0x46, 0x9d, 0xd0, 0x8a, 0x58, 0xa0, 0x86, 0x35, 0x65,
0x59, 0xe2, 0x1f, 0x2d, 0xdd, 0x00, 0xfe, 0xfe, 0xfb, 0xe0, 0xcc, 0xb4, 0x02, 0xf7, 0xb3, 0x04, 0x0f, 0xda, 0x9f,
0xf6, 0x77, 0x74, 0xf5, 0x07, 0x6d, 0x21, 0xc0, 0xd4, 0x0a, 0x0e, 0x38, 0xfc, 0x98, 0x7c, 0x22, 0xd6, 0xed, 0x25,
0x10, 0x2e, 0x6c, 0x27, 0xe9, 0xbe, 0xc4, 0x4b, 0xa9, 0xd9, 0x03, 0x0e, 0x17, 0xf5, 0x5a, 0x31, 0xaf, 0x1c, 0xe9,
0x00, 0xc2, 0x83, 0x04, 0x87, 0x5c, 0xf9, 0x8e, 0xba, 0x86, 0xb4, 0x74, 0x17, 0x0c, 0x80, 0x50, 0x41, 0xf6, 0x43,
0x00, 0xaf, 0xd2, 0x24, 0x09, 0x27, 0xfd, 0x27, 0x09, 0xe3, 0x34, 0x49, 0x16, 0x06, 0xdc, 0xda, 0x28, 0x44, 0x83,
0xfb, 0xa2, 0xa3, 0xae, 0x41, 0xbc, 0xc4, 0xef, 0xd2, 0x0c, 0xa5, 0x6f, 0x48, 0x36, 0xfb, 0x9d, 0x5c, 0xa3, 0x2b,
0x92, 0xcd, 0xd8, 0x75, 0x34, 0x43, 0xe9, 0x55, 0x34, 0x43, 0x59, 0x46, 0x66, 0x28, 0xf9, 0x82, 0x51, 0x2d, 0xa4,
0x2c, 0xb1, 0xd2, 0x0a, 0x30, 0xb2, 0xce, 0xe8, 0x07, 0x28, 0x31, 0x5b, 0x1b, 0x03, 0xca, 0xdd, 0x68, 0xa9, 0x0d,
0x8e, 0xab, 0x6f, 0xfe, 0x2f, 0x85, 0xce, 0x50, 0x65, 0x6b, 0x6d, 0xda, 0x12, 0xf7, 0xd9, 0x0f, 0x5e, 0x1c, 0xdc,
0x11, 0xf9, 0x4f, 0x78, 0x41, 0x8c, 0xb4, 0x11, 0x2b, 0xa1, 0x4a, 0xec, 0x35, 0xbe, 0xc6, 0x71, 0x75, 0x1f, 0x1e,
0xcf, 0xd1, 0x53, 0x1f, 0xfd, 0x18, 0x0f, 0x0f, 0x3e, 0xde, 0x17, 0x76, 0xb3, 0x42, 0xbb, 0x56, 0x2a, 0x5b, 0xe2,
0xc6, 0xb9, 0x2e, 0x8f, 0xe3, 0xed, 0x76, 0x4b, 0xb6, 0x53, 0xa2, 0xcd, 0x2a, 0xce, 0x92, 0x24, 0x89, 0xed, 0x66,
0x85, 0xd1, 0x50, 0x08, 0x38, 0xbb, 0xc2, 0xa8, 0x01, 0xb1, 0x6a, 0x5c, 0x0f, 0x57, 0x2f, 0x0e, 0x70, 0x2c, 0x3c,
0x47, 0x75, 0xff, 0xe9, 0xc2, 0x8a, 0xb9, 0xb0, 0x02, 0x3f, 0xd2, 0x00, 0x9f, 0xc2, 0x7c, 0xd9, 0x87, 0x79, 0x4d,
0x33, 0x94, 0xa1, 0xa4, 0xff, 0x65, 0x91, 0x87, 0x47, 0x2c, 0x7a, 0x86, 0xa1, 0x0b, 0xcc, 0x43, 0xed, 0x3c, 0x7a,
0x73, 0x96, 0x4d, 0xfd, 0xc9, 0x26, 0x4d, 0x1e, 0x0f, 0xbc, 0xc0, 0xaf, 0xf3, 0x4b, 0x3c, 0xca, 0x3e, 0x5c, 0x32,
0x78, 0x6b, 0x4d, 0xfa, 0x61, 0x4e, 0x67, 0x68, 0x36, 0x9e, 0xcc, 0x22, 0x0f, 0x9f, 0x31, 0x34, 0xdb, 0x64, 0x4d,
0xda, 0x46, 0xf3, 0x68, 0x46, 0xa7, 0x68, 0x3a, 0x3a, 0x32, 0x45, 0xd3, 0x4d, 0xd6, 0xcc, 0x3f, 0xcc, 0x2f, 0xcf,
0xa2, 0xe9, 0x97, 0x97, 0x71, 0x85, 0xc3, 0x1c, 0xe3, 0xc7, 0xc8, 0xf9, 0x65, 0xe4, 0xe4, 0xb3, 0x16, 0x2a, 0xc0,
0x38, 0x3c, 0xd6, 0xe0, 0x58, 0x13, 0xe0, 0x98, 0x69, 0x55, 0x8b, 0x15, 0xf9, 0x6c, 0xb5, 0xc2, 0x21, 0x71, 0x0d,
0xa8, 0xe0, 0x24, 0xea, 0x05, 0xa1, 0xa7, 0x04, 0xcf, 0x29, 0x2e, 0x3c, 0x9c, 0xeb, 0xdf, 0x09, 0x27, 0xa1, 0x74,
0xc4, 0x37, 0xec, 0xe4, 0x6f, 0xba, 0xe2, 0xa7, 0xfd, 0x6f, 0x3c, 0xc0, 0x2d, 0x65, 0x38, 0x24, 0x42, 0x29, 0x30,
0x77, 0xb0, 0x73, 0x25, 0x7e, 0xf7, 0xf6, 0x06, 0xbd, 0xe5, 0xdc, 0x80, 0xb5, 0x39, 0xc2, 0xaf, 0x1c, 0x69, 0x29,
0xfb, 0xba, 0x78, 0x93, 0x3e, 0x95, 0xfe, 0x97, 0xf8, 0x45, 0xa0, 0x3f, 0xc0, 0x6d, 0xb5, 0x79, 0x18, 0xe5, 0xbd,
0xfd, 0x85, 0x6f, 0x23, 0x56, 0x7e, 0x55, 0x8d, 0x02, 0x87, 0xc3, 0x89, 0xf8, 0x3a, 0x83, 0xb5, 0x82, 0xe3, 0x70,
0x22, 0xbf, 0xce, 0xd1, 0x59, 0xdf, 0xbc, 0x8e, 0xd0, 0xce, 0x12, 0x2b, 0x05, 0x83, 0x20, 0x0d, 0x49, 0xad, 0xcd,
0xcf, 0x94, 0x35, 0x8f, 0x09, 0xb2, 0x43, 0x47, 0xab, 0x47, 0x3d, 0xcc, 0x00, 0x75, 0x30, 0xaa, 0x0a, 0x30, 0x17,
0x1b, 0x1c, 0x2e, 0x14, 0x61, 0x92, 0x5a, 0xeb, 0x47, 0x46, 0xe9, 0x9d, 0xf3, 0xe1, 0xe0, 0x89, 0x1a, 0x22, 0xfd,
0xf5, 0xee, 0xdd, 0xef, 0xe5, 0x7d, 0x41, 0x87, 0x01, 0x89, 0xbf, 0xc5, 0xa8, 0x67, 0x3e, 0x33, 0x46, 0x12, 0x6a,
0xe7, 0x2b, 0x5e, 0x07, 0x96, 0x18, 0x6b, 0x45, 0x78, 0x2c, 0x6c, 0x47, 0xd5, 0x73, 0xb6, 0x3e, 0xa6, 0xaa, 0x88,
0x3d, 0xad, 0x2a, 0x62, 0x5a, 0xbd, 0x38, 0x98, 0xc0, 0xfa, 0xe1, 0xf6, 0x10, 0x1e, 0xef, 0x27, 0x8a, 0xfc, 0x7b,
0x0d, 0x66, 0x7f, 0x0b, 0x12, 0x98, 0xd3, 0x26, 0xc0, 0xe4, 0x89, 0x60, 0x48, 0x1c, 0xec, 0xdc, 0xcd, 0x38, 0x7f,
0x2d, 0xf1, 0x87, 0x13, 0x45, 0xb4, 0x62, 0x52, 0xb0, 0x87, 0xf2, 0x1c, 0x71, 0x78, 0x10, 0x64, 0x43, 0xe5, 0x1a,
0x4e, 0x3c, 0x92, 0xd4, 0x9a, 0xad, 0x6d, 0x10, 0x1e, 0x27, 0x8c, 0xd0, 0xae, 0x03, 0xc5, 0x6f, 0x1a, 0x21, 0x79,
0xa0, 0xc2, 0x63, 0xf8, 0x78, 0xd3, 0xcf, 0x8c, 0xfb, 0xd5, 0xf0, 0xd1, 0x80, 0xfc, 0x4f, 0xf9, 0xd2, 0x2f, 0x87,
0x97, 0x9f, 0x70, 0x48, 0xfa, 0xf8, 0xef, 0x1f, 0x37, 0x84, 0x6f, 0xef, 0x57, 0xbb, 0x56, 0x4e, 0x7c, 0xe8, 0xd1,
0x7c, 0x16, 0x1e, 0xef, 0x8f, 0xe1, 0x31, 0x5c, 0x14, 0xf1, 0x30, 0xe7, 0xab, 0xa2, 0x1f, 0xb9, 0xd5, 0x0f, 0x87,
0xa5, 0xde, 0x45, 0x56, 0x7c, 0x11, 0x6a, 0x95, 0x0b, 0xd5, 0x80, 0x11, 0xee, 0xc8, 0xc5, 0x66, 0x22, 0x54, 0xb7,
0x76, 0x87, 0x8e, 0x72, 0xee, 0x29, 0xb3, 0x6e, 0xb7, 0xa8, 0xb5, 0x72, 0x9e, 0x13, 0xf2, 0x14, 0xda, 0xe3, 0x40,
0xef, 0x27, 0x4c, 0xfe, 0x66, 0xf6, 0xdd, 0x71, 0xa9, 0xf9, 0xfe, 0xe0, 0xd3, 0x10, 0x51, 0x29, 0x56, 0x2a, 0x67,
0xa0, 0x1c, 0x98, 0x41, 0xa8, 0xa6, 0xad, 0x90, 0xfb, 0xdc, 0x52, 0x65, 0x23, 0x0b, 0x46, 0xd4, 0xc7, 0xe5, 0xda,
0x39, 0xad, 0x0e, 0x4b, 0x6d, 0x38, 0x98, 0x3c, 0x59, 0x0c, 0x40, 0x64, 0x28, 0x17, 0x6b, 0x9b, 0x93, 0xa9, 0x81,
0x76, 0xb1, 0xa4, 0xec, 0x61, 0x65, 0xf4, 0x5a, 0xf1, 0x88, 0xf9, 0xc9, 0x9b, 0x7f, 0x9b, 0xd6, 0x74, 0x0a, 0x6c,
0x31, 0x62, 0x75, 0x5d, 0x2f, 0xa4, 0x50, 0x10, 0x0d, 0xb3, 0x2d, 0xcf, 0xc8, 0x95, 0x17, 0xbb, 0x70, 0x93, 0x64,
0xfe, 0x60, 0xf0, 0x31, 0x4d, 0x92, 0xef, 0x16, 0xa7, 0x70, 0x92, 0x05, 0x5b, 0x1b, 0xab, 0x4d, 0xde, 0x69, 0xe1,
0xdd, 0x3c, 0xb6, 0x54, 0xa8, 0x4b, 0xef, 0x7d, 0xd9, 0x2c, 0xc6, 0x75, 0x94, 0x0b, 0xd5, 0x9b, 0xe9, 0x97, 0xd2,
0xa2, 0x15, 0x6a, 0xd8, 0xa9, 0x79, 0x36, 0x4f, 0xba, 0xdd, 0xf1, 0x54, 0x09, 0x87, 0x13, 0x77, 0x2d, 0x61, 0xb7,
0xf8, 0xbc, 0xb6, 0x4e, 0xd4, 0xfb, 0x68, 0xdc, 0xc9, 0xb9, 0xed, 0x28, 0x83, 0x68, 0x09, 0x6e, 0x0b, 0xa0, 0x16,
0xbd, 0x8d, 0x48, 0x38, 0x68, 0xed, 0x98, 0xa7, 0xb3, 0x9a, 0xbe, 0x60, 0x9f, 0xea, 0xfa, 0x5f, 0xdc, 0xbe, 0x8a,
0x0e, 0x2d, 0x35, 0x2b, 0xa1, 0xa2, 0xa5, 0x76, 0x4e, 0xb7, 0x79, 0x74, 0xdd, 0xed, 0x16, 0xe3, 0x91, 0x57, 0x96,
0xa7, 0xde, 0xcd, 0x7e, 0xd7, 0x9e, 0xf2, 0x9d, 0x76, 0x3b, 0x64, 0xb5, 0x14, 0x7c, 0xe4, 0xeb, 0x59, 0x50, 0x72,
0x4e, 0x4f, 0x3a, 0xeb, 0x76, 0xc8, 0x9f, 0x9d, 0x52, 0x7d, 0x55, 0xbf, 0xa6, 0x69, 0xf2, 0x37, 0x37, 0xc2, 0xeb,
0x3a, 0x5b, 0xd6, 0xe7, 0x4c, 0xf9, 0xb5, 0xe9, 0x57, 0x4b, 0x5f, 0x5a, 0x45, 0x3c, 0xbc, 0x6e, 0x7c, 0x65, 0x54,
0x85, 0xcf, 0x70, 0x55, 0x34, 0x29, 0x12, 0xbc, 0x6c, 0x29, 0xab, 0x2e, 0x66, 0x5b, 0x11, 0x37, 0xe9, 0x89, 0xd4,
0xa4, 0xd5, 0x93, 0xb9, 0x35, 0xd0, 0x7a, 0xef, 0xab, 0x1b, 0xad, 0x14, 0x30, 0x27, 0xd4, 0x0a, 0x39, 0x8d, 0xc6,
0x14, 0x10, 0x42, 0x8a, 0xa5, 0xa9, 0xde, 0x4b, 0xa0, 0x16, 0xd0, 0x96, 0x0a, 0x47, 0x8a, 0x78, 0xe0, 0x1f, 0x3a,
0x5d, 0xf0, 0x52, 0x81, 0x3b, 0xf7, 0x76, 0x33, 0x1d, 0x0c, 0xdc, 0x82, 0xf3, 0x9a, 0xbc, 0x81, 0x69, 0x55, 0xf8,
0x15, 0x8c, 0x68, 0xdf, 0xa5, 0x65, 0xbc, 0x15, 0xb5, 0xf0, 0x4f, 0x98, 0xaa, 0xe8, 0x8b, 0xdc, 0x6b, 0xf0, 0x79,
0x1e, 0x9e, 0x5b, 0x3d, 0x24, 0x41, 0xad, 0x5c, 0x53, 0x4e, 0x33, 0xd4, 0x49, 0xca, 0xa0, 0xd1, 0x92, 0x83, 0x29,
0x6f, 0x6f, 0x7f, 0xfb, 0x67, 0xe5, 0x9d, 0x79, 0x94, 0xeb, 0xec, 0xc3, 0x20, 0xe6, 0x81, 0x51, 0x6a, 0x7e, 0x35,
0x3c, 0xb2, 0x3a, 0x6a, 0xed, 0x56, 0x1b, 0xfe, 0x44, 0xc7, 0xfb, 0xf1, 0x70, 0xd0, 0xd3, 0xff, 0xfb, 0x56, 0xa9,
0x6e, 0xe9, 0x06, 0x8a, 0x78, 0x44, 0x8a, 0xd8, 0x3b, 0x3c, 0xd0, 0x9b, 0x91, 0xaf, 0x49, 0xab, 0x3f, 0xef, 0xde,
0xa2, 0xbf, 0x3a, 0x4e, 0x1d, 0x0c, 0x69, 0xeb, 0xa3, 0x6a, 0xc1, 0x35, 0x9a, 0x97, 0xef, 0xff, 0xbc, 0xbd, 0x3b,
0x47, 0xb8, 0xee, 0x99, 0x10, 0x28, 0x36, 0x3c, 0xf7, 0xd6, 0xd2, 0x89, 0x8e, 0x1a, 0xd7, 0xab, 0x8d, 0xfc, 0x14,
0x39, 0xc5, 0xd0, 0xd3, 0x6b, 0x21, 0x61, 0x08, 0x63, 0x10, 0xac, 0xd0, 0xc9, 0xab, 0x93, 0xb5, 0x67, 0x7e, 0xc5,
0xc3, 0x6d, 0xc7, 0xc3, 0xd5, 0xc7, 0xfd, 0xcb, 0xf7, 0xbf, 0x81, 0xdb, 0x13, 0xb5, 0x09, 0x0b, 0x00, 0x00};
#else // Brotli (default, smaller)
constexpr uint8_t INDEX_BR[] PROGMEM = {
0x1b, 0x14, 0x0b, 0x00, 0xe4, 0x6f, 0xcd, 0xfc, 0x7b, 0x2e, 0x27, 0xd2, 0x21, 0x2b, 0x58, 0xea, 0x16, 0xf8, 0xa5,
0xb6, 0x47, 0x14, 0xb3, 0x4c, 0x56, 0xcc, 0x20, 0x5b, 0x1d, 0xfe, 0x7d, 0xfb, 0xb5, 0x7a, 0x12, 0x0a, 0xd6, 0x48,
0x84, 0xa2, 0x21, 0x69, 0x0d, 0x77, 0x33, 0xf3, 0x77, 0xcf, 0x4c, 0x21, 0xf1, 0xde, 0xee, 0x7d, 0x44, 0xbc, 0xd3,
0xc4, 0x33, 0x89, 0x1a, 0x69, 0x68, 0x48, 0x57, 0xa7, 0x17, 0x0b, 0x4d, 0x91, 0xac, 0x30, 0x48, 0x21, 0x4d, 0x4c,
0x10, 0x57, 0x46, 0x14, 0x17, 0xd5, 0x21, 0x0e, 0xb4, 0x50, 0x87, 0xad, 0x62, 0xda, 0x11, 0x68, 0xc3, 0x20, 0x7f,
0x2d, 0xdc, 0x57, 0xeb, 0x2c, 0x36, 0xdb, 0xc4, 0x84, 0x0f, 0xac, 0xec, 0x08, 0x91, 0xa3, 0x92, 0x27, 0x45, 0xc4,
0x1e, 0xa4, 0x9e, 0x72, 0x3b, 0xc2, 0xe3, 0x20, 0x7d, 0xf8, 0x05, 0x09, 0xea, 0xe3, 0xa6, 0x3f, 0x11, 0x8b, 0x16,
0x11, 0x6b, 0x26, 0x91, 0x1a, 0x2c, 0x19, 0x24, 0xf7, 0x5d, 0x44, 0x82, 0x49, 0x44, 0x15, 0xce, 0x39, 0xdc, 0xcb,
0x8f, 0x5b, 0xe1, 0xe2, 0x42, 0x96, 0xa2, 0x10, 0x3c, 0x20, 0xa5, 0x65, 0x85, 0x49, 0x6a, 0x06, 0x08, 0xd1, 0xcb,
0x41, 0xb8, 0x49, 0x20, 0x73, 0x71, 0xfe, 0x55, 0x61, 0x45, 0xc6, 0x95, 0x72, 0xc8, 0xf0, 0xa5, 0xe9, 0xe6, 0x3a,
0xb9, 0xc3, 0x8a, 0x9f, 0xb5, 0xc1, 0xc9, 0x15, 0x56, 0x93, 0x38, 0x8a, 0x48, 0xf0, 0x4f, 0x38, 0x22, 0xe1, 0x8d,
0x55, 0xbb, 0x8c, 0xc3, 0xb0, 0x68, 0xcc, 0x7f, 0x6f, 0xf8, 0xc9, 0x9b, 0x38, 0x41, 0xf1, 0x94, 0x25, 0xf9, 0x6b,
0x56, 0xa2, 0xec, 0xa7, 0xbd, 0x2e, 0x69, 0x8e, 0xe2, 0xec, 0x36, 0x94, 0x24, 0x2c, 0x12, 0x1d, 0x4e, 0x76, 0x7e,
0x23, 0xcc, 0xf2, 0x6e, 0x35, 0x1a, 0x9c, 0xed, 0x6f, 0x15, 0x3f, 0xc9, 0x72, 0xdc, 0xfd, 0x13, 0x0f, 0x16, 0x8a,
0x23, 0x4f, 0xf9, 0x2e, 0x63, 0x44, 0x91, 0x77, 0xe0, 0xb3, 0xd1, 0x78, 0x74, 0xdb, 0x4a, 0x2c, 0xd8, 0xe8, 0xf9,
0x2c, 0x03, 0xbe, 0xae, 0xac, 0x4e, 0x42, 0x01, 0xc4, 0x4b, 0x40, 0xe7, 0x94, 0x34, 0x85, 0x2c, 0xfe, 0xf5, 0x74,
0x75, 0xd8, 0xdc, 0xec, 0x6a, 0x00, 0x3f, 0x3f, 0x81, 0x77, 0xa8, 0xcd, 0xde, 0x6d, 0x5a, 0x47, 0xa1, 0x99, 0x36,
0x87, 0xb6, 0x78, 0x35, 0x3c, 0x9a, 0x64, 0x15, 0x7c, 0x56, 0x76, 0x22, 0xce, 0x46, 0xe5, 0x17, 0x57, 0x05, 0xfc,
0x09, 0x69, 0xae, 0x39, 0xaa, 0xee, 0xc9, 0x4e, 0x7f, 0x99, 0x2a, 0x65, 0x82, 0x7e, 0xeb, 0x23, 0x4f, 0x42, 0xd5,
0xca, 0xcb, 0x42, 0x74, 0x2e, 0xd2, 0xa2, 0x62, 0x57, 0xd0, 0xa9, 0x7b, 0x4b, 0xac, 0x7b, 0x65, 0x13, 0x47, 0x0f,
0x2d, 0x3d, 0xf6, 0xbc, 0x78, 0x5c, 0xa3, 0xc9, 0x57, 0x4b, 0x10, 0x62, 0x68, 0x19, 0x7f, 0xfd, 0x1a, 0xcb, 0x51,
0x1e, 0x41, 0x39, 0x55, 0xf3, 0x97, 0x30, 0xca, 0x37, 0xb6, 0x42, 0x1d, 0x2d, 0x8c, 0xc6, 0x65, 0x8a, 0xd2, 0x89,
0x88, 0xa6, 0x28, 0xbd, 0x56, 0x7c, 0x2d, 0x5e, 0x0d, 0x2f, 0x9e, 0xc3, 0xa5, 0x80, 0xaf, 0xcc, 0x00, 0xde, 0xe6,
0x5a, 0x8b, 0xda, 0xff, 0x1f, 0x16, 0xc8, 0x83, 0x5e, 0xe5, 0xea, 0x25, 0x86, 0x70, 0x55, 0x25, 0x01, 0x14, 0x0c,
0x77, 0xd8, 0x31, 0x21, 0xcc, 0x79, 0x15, 0x3b, 0x32, 0x3a, 0xd3, 0xc5, 0x30, 0xaf, 0x03, 0xd8, 0x3e, 0x44, 0xb8,
0x63, 0xb5, 0x74, 0x4f, 0x41, 0x4b, 0xf0, 0x24, 0x74, 0xb2, 0x06, 0xb2, 0x7b, 0x06, 0x60, 0xdc, 0xc1, 0x3e, 0x0e,
0x6f, 0x1e, 0x3c, 0x42, 0xa0, 0xdb, 0x36, 0x43, 0x30, 0x71, 0xcc, 0xd6, 0x1a, 0xbd, 0x38, 0x61, 0x19, 0x3f, 0xf2,
0xdf, 0xf4, 0x53, 0x3d, 0xc1, 0x14, 0xbe, 0x50, 0x1c, 0x2c, 0xf3, 0xaa, 0x74, 0x36, 0xcb, 0xbd, 0x7a, 0x4b, 0xa3,
0x1c, 0x90, 0x40, 0x5b, 0x4a, 0x0f, 0x83, 0x6e, 0x9e, 0x96, 0x28, 0x3d, 0x77, 0x43, 0x05, 0x0e, 0x39, 0x26, 0x15,
0xb8, 0x6b, 0x4e, 0x3a, 0x62, 0xc2, 0xda, 0xde, 0x8e, 0x29, 0x29, 0x0b, 0x09, 0xa2, 0xb3, 0xa7, 0x1e, 0x9a, 0x0c,
0x8f, 0xa1, 0x46, 0x6f, 0x92, 0xf3, 0x9e, 0xb5, 0xc5, 0x7e, 0x0e, 0x8d, 0xeb, 0x55, 0x08, 0xfa, 0xc9, 0xd8, 0x4e,
0xe3, 0xd0, 0xca, 0x2a, 0x96, 0x11, 0x7e, 0xb1, 0xd4, 0x7f, 0x8f, 0x1d, 0xb3, 0xc3, 0xa0, 0x89, 0x6f, 0x93, 0x99,
0x55, 0x48, 0x97, 0x08, 0x79, 0x66, 0xe9, 0x4a, 0x6b, 0xa0, 0x29, 0xf2, 0x10, 0x1f, 0x22, 0xa2, 0x1b, 0x41, 0xdf,
0xa3, 0xde, 0x62, 0x60, 0x8e, 0xa1, 0x60, 0xc4, 0xd5, 0xce, 0x81, 0x47, 0x84, 0x3b, 0x66, 0x60, 0x74, 0xa7, 0x74,
0xcc, 0x48, 0xe0, 0xe1, 0xd5, 0x0c, 0x15, 0x98, 0x3d, 0xa7, 0x9c, 0xb2, 0xec, 0xba, 0x0f, 0x2c, 0x52, 0x14, 0x7b,
0xe2, 0x49, 0x6e, 0x0f, 0x8c, 0x00, 0xe0, 0x81, 0xf6, 0x57, 0xe4, 0x3f, 0xbf, 0x7c, 0xa9, 0x5e, 0xfe, 0x01, 0xc2,
0x64, 0xb0, 0x05, 0x60, 0x01, 0xaa, 0x78, 0x65, 0xb2, 0xeb, 0x56, 0x41, 0xf2, 0xbf, 0xa3, 0x45, 0x4e, 0x3c, 0x78,
0xe2, 0xa1, 0x0d, 0xa9, 0x2a, 0xc0, 0x8a, 0xc0, 0x6f, 0xe4, 0x66, 0xbe, 0x72, 0x35, 0x5e, 0xf7, 0x3b, 0x42, 0x53,
0xd4, 0xe6, 0x66, 0xb6, 0x78, 0xcd, 0xaa, 0x6f, 0xf4, 0x26, 0x80, 0x3a, 0x4e, 0x74, 0x80, 0x17, 0x88, 0x44, 0x23,
0xa6, 0x3a, 0x6f, 0x87, 0xb8, 0xd0, 0x4d, 0xd3, 0xfc, 0x7c, 0xd6, 0x38, 0x2a, 0x00, 0x28, 0x01, 0xa2, 0x40, 0x94,
0x6c, 0x1e, 0x8a, 0xed, 0xe3, 0x92, 0x9d, 0x90, 0xe3, 0x0d, 0x02, 0x4e, 0x15, 0x30, 0xed, 0x8f, 0x5b, 0x99, 0xaa,
0x7a, 0x4e, 0xb9, 0xec, 0x91, 0xe2, 0x9f, 0xd4, 0xca, 0x46, 0xaf, 0x87, 0x19, 0x4b, 0xad, 0xea, 0xe6, 0x6c, 0x8d,
0x53, 0x4b, 0x29, 0xee, 0x1e, 0x96, 0xd8, 0x94, 0x30, 0x3a, 0x9c, 0xb0, 0x4c, 0xdb, 0xe2, 0xa1, 0x7f, 0xc7, 0x11,
0xdd, 0xe3, 0x9d, 0x36, 0x44, 0xd3, 0x93, 0x14, 0x9c, 0x4c, 0x9d, 0xdd, 0x3a, 0x7c, 0x41, 0xb1, 0x8f, 0x14, 0xd9,
0x4e, 0x61, 0xd9, 0x3a, 0xe3, 0x0d, 0x76, 0xca, 0x6c, 0xac, 0x73, 0xaf, 0xad, 0x94, 0x87, 0x30, 0xf1, 0x30, 0x2f,
0x8b, 0xed, 0x4a, 0xed, 0xbc, 0xc2, 0xf2, 0x7c, 0x30, 0xa3, 0x0b, 0x28, 0x64, 0x3b, 0x8c, 0xd4, 0xc3, 0x42, 0xb9,
0xa3, 0x44, 0x51, 0x80, 0x07, 0x5a, 0x3d, 0x14, 0x33, 0x99, 0xbf, 0x2a, 0x6b, 0x2b, 0x19, 0x47, 0x72, 0x9e, 0xd4,
0xb4, 0x6d, 0x72, 0xdd, 0x8a, 0x4b, 0x33, 0x55, 0xbc, 0xb4, 0xcd, 0xc8, 0x2b, 0x17, 0x2f, 0x74, 0xeb, 0x22, 0x17,
0x94, 0x08, 0x27, 0x27, 0xc2, 0x5b, 0x17, 0xb4, 0xa9, 0x22, 0x16, 0x9d, 0xd4, 0xfc, 0xc7, 0x15, 0xa3, 0x9b, 0x86,
0x1f, 0xad, 0x45, 0xd3, 0x87, 0x94, 0x5b, 0x31, 0x36, 0xaa, 0xe4, 0x66, 0x8d, 0xcc, 0x31, 0x05, 0x5b, 0xc4, 0x40,
0xc0, 0xb8, 0xeb, 0x91, 0x18, 0x22, 0x8c, 0x31, 0x1e, 0xad, 0xd0, 0x3a, 0x98, 0x07, 0xb5, 0x6f, 0x11, 0xba, 0x11,
0xa6, 0x14, 0x35, 0x5a, 0xe7, 0x55, 0xdf, 0xb7, 0x4c, 0x03, 0x61, 0xa3, 0x74, 0x23, 0xdf, 0x55, 0x1f, 0x02, 0x51,
0x09, 0xb7, 0xba, 0xd5, 0x0c, 0x67, 0xab, 0x98, 0x70, 0x14, 0x64, 0x8d, 0xf4, 0x8b, 0x54, 0x44, 0x07, 0x6f, 0xe0,
0x69, 0x32, 0xca, 0x48, 0xe5, 0xd3, 0xa7, 0x17, 0x8f, 0x45, 0x84, 0x04, 0xb7, 0xd1, 0xbb, 0xe1, 0xf6, 0x01, 0x0a,
0xf6, 0xee, 0x2b, 0x32, 0xd2, 0xc5, 0xf8, 0xa6, 0xec, 0x0f, 0x1b, 0x09, 0x1d, 0xfc, 0xa2, 0xbf, 0x54, 0x5d, 0x2c,
0x62, 0xf4, 0x77, 0xde, 0xad, 0xa0, 0x50, 0x6a, 0xb4, 0xe3, 0x5f, 0xda, 0xff, 0x6b, 0xb1, 0x78, 0xf7, 0xf9, 0xc1,
0xa6, 0xa8, 0x93, 0xe8, 0xe4, 0x11, 0x56, 0xa0, 0x5b, 0x85, 0xa7, 0x92, 0x7a, 0x58, 0x45, 0x95, 0xa9, 0x63, 0x83,
0xb4, 0x1f, 0x18, 0x31, 0x7a, 0x6d, 0xa1, 0x8d, 0x8c, 0xdc, 0x91, 0x02, 0x3c, 0x9c, 0x92, 0x42, 0x8e, 0x03, 0x02,
0xc5, 0x0c, 0x43, 0x54, 0xf9, 0xb2, 0x85, 0x39, 0x2e, 0x77, 0xad, 0x00};
0x1b, 0x08, 0x0b, 0x00, 0xe4, 0x7f, 0x9b, 0xad, 0xbb, 0x97, 0x53, 0xde, 0xb7, 0x25, 0x0e, 0x69, 0xd4, 0x69, 0x89,
0xba, 0xa5, 0x55, 0x22, 0x04, 0x27, 0xeb, 0x00, 0x52, 0xac, 0xbc, 0xec, 0x5f, 0xfb, 0xb5, 0x7a, 0x12, 0x0a, 0xd6,
0x48, 0x84, 0x4a, 0x48, 0x5a, 0xcb, 0xbd, 0xb7, 0x72, 0x66, 0x4e, 0x62, 0xd8, 0xbd, 0x7f, 0x88, 0x68, 0x86, 0x28,
0xd6, 0xf1, 0xda, 0x2c, 0xa2, 0x32, 0x5c, 0xdd, 0xab, 0xb2, 0x15, 0xbc, 0x24, 0x13, 0xe6, 0xbd, 0x0c, 0x52, 0x63,
0x82, 0x88, 0x6d, 0x74, 0x71, 0x51, 0x9c, 0xe2, 0x40, 0x56, 0xea, 0xb0, 0x5c, 0xb7, 0x1d, 0x81, 0x3a, 0x0c, 0xf2,
0xd7, 0xa8, 0x5c, 0x35, 0x2d, 0x43, 0xb3, 0x42, 0x37, 0x7c, 0x60, 0xd8, 0xc1, 0x95, 0x91, 0x94, 0x3c, 0x29, 0x20,
0x96, 0x20, 0xf6, 0x24, 0xb7, 0x83, 0x4a, 0x06, 0xf1, 0xc3, 0x2f, 0x88, 0x50, 0x55, 0x35, 0x2d, 0xe8, 0xb6, 0x21,
0x38, 0x61, 0x8e, 0x44, 0x2a, 0xac, 0x39, 0xcf, 0x74, 0xaa, 0x31, 0x7f, 0x62, 0x32, 0x9b, 0x99, 0x42, 0x0a, 0xf6,
0x6f, 0xd8, 0x89, 0x3f, 0x49, 0xb1, 0xa2, 0x52, 0x0a, 0x8e, 0xb3, 0x27, 0x0b, 0x07, 0x07, 0x58, 0xb0, 0x8f, 0xaa,
0xdc, 0x68, 0x12, 0x0f, 0x95, 0xbf, 0xc7, 0x36, 0xd5, 0x60, 0x5a, 0xc7, 0x80, 0xac, 0x52, 0xf7, 0xa7, 0x2d, 0xb6,
0x64, 0xda, 0xda, 0x11, 0x8d, 0x6a, 0xe5, 0xba, 0xe9, 0xda, 0xdc, 0x62, 0xc3, 0xcb, 0xae, 0xc1, 0xe1, 0x11, 0xb6,
0x33, 0x29, 0x04, 0x09, 0xfe, 0x09, 0x08, 0xc2, 0x1f, 0x98, 0x16, 0x26, 0x0d, 0xce, 0xd7, 0x75, 0xfb, 0xd7, 0xa5,
0x82, 0xd7, 0x32, 0x44, 0x72, 0xc1, 0xc2, 0xe4, 0x15, 0xcb, 0x50, 0xfc, 0xd3, 0x58, 0x64, 0x34, 0x41, 0x32, 0xbe,
0x1f, 0x08, 0x43, 0x16, 0x14, 0xf7, 0x70, 0x2c, 0x1c, 0x61, 0x09, 0x78, 0x73, 0xd1, 0x30, 0xf6, 0xed, 0x85, 0x55,
0x50, 0x02, 0xf0, 0x68, 0x50, 0x5c, 0x1a, 0xd7, 0x3b, 0x8e, 0xf2, 0x23, 0xc8, 0x88, 0x39, 0xd0, 0x84, 0xf7, 0xa6,
0xd1, 0xa3, 0xfb, 0xe5, 0x44, 0xc0, 0x4c, 0x2f, 0x6f, 0x19, 0x70, 0x75, 0xf6, 0x1c, 0xb8, 0xce, 0x89, 0x4f, 0x01,
0x8d, 0xa1, 0x71, 0xf2, 0x97, 0xf8, 0xe7, 0xd3, 0xf1, 0xe1, 0xfa, 0xfc, 0xc8, 0x03, 0x78, 0x14, 0xa0, 0x3d, 0x28,
0xb7, 0xeb, 0x26, 0x52, 0x59, 0xa8, 0xb5, 0x0d, 0x5c, 0x8a, 0x6b, 0xe5, 0xf6, 0x30, 0xd6, 0xf7, 0x71, 0x31, 0xe8,
0xbd, 0xc9, 0xfa, 0xf5, 0x71, 0x9d, 0xff, 0xf6, 0x69, 0x62, 0x5f, 0xb5, 0xc7, 0x06, 0x43, 0x54, 0xb5, 0x87, 0xf1,
0xcc, 0x84, 0xe8, 0xb7, 0x56, 0x38, 0x43, 0x6a, 0xa6, 0x2f, 0x53, 0xd1, 0x89, 0x48, 0x8d, 0x72, 0x75, 0x4a, 0x17,
0xf6, 0x8d, 0xb2, 0xeb, 0xb1, 0x6b, 0x29, 0x1e, 0xd5, 0x74, 0xc7, 0xb3, 0xf4, 0xf1, 0x04, 0x0d, 0xbf, 0x08, 0x81,
0x8f, 0xfe, 0x8d, 0xfc, 0xf2, 0x35, 0x92, 0xa0, 0x24, 0x88, 0x12, 0x6a, 0xe6, 0x2f, 0x01, 0x94, 0x5c, 0x4b, 0xf9,
0x6b, 0x9a, 0xf6, 0x1a, 0x35, 0x11, 0x8a, 0xc6, 0x04, 0x8d, 0x50, 0x64, 0x48, 0x2d, 0x8b, 0xdf, 0xdf, 0xa1, 0xd1,
0xfd, 0x21, 0xd7, 0x40, 0x96, 0x00, 0xb1, 0x9f, 0x54, 0xc2, 0xe1, 0x00, 0x79, 0x15, 0x80, 0xf8, 0xca, 0x8e, 0xc5,
0x06, 0x03, 0xdf, 0x72, 0x49, 0xc0, 0x08, 0x27, 0x90, 0xe3, 0x14, 0xc2, 0xac, 0xc3, 0x83, 0xc9, 0xf6, 0x9d, 0x12,
0xab, 0x46, 0xae, 0x03, 0x74, 0x1d, 0x27, 0xce, 0x91, 0x1d, 0x4e, 0xb4, 0xbe, 0x80, 0xe7, 0x6a, 0x6d, 0x0a, 0x20,
0x47, 0x6b, 0x00, 0x4e, 0x25, 0x52, 0xe6, 0xf5, 0xe9, 0x43, 0x84, 0xc1, 0x0d, 0x4b, 0x04, 0xb3, 0x91, 0x49, 0xf5,
0xe9, 0xc4, 0x2e, 0x1b, 0xf9, 0x98, 0xf9, 0xea, 0x9e, 0xb8, 0xf6, 0x53, 0xf8, 0x42, 0xc2, 0x60, 0x5c, 0xa9, 0xd2,
0xfe, 0x0a, 0xe5, 0xd4, 0x7d, 0x36, 0x76, 0x04, 0x12, 0x38, 0xa1, 0xc4, 0x30, 0xb8, 0xf2, 0x69, 0x86, 0x5b, 0xe7,
0xe5, 0xa0, 0xc0, 0xfe, 0x91, 0x19, 0x03, 0x1e, 0xa9, 0x93, 0x26, 0x49, 0x58, 0xd5, 0xf6, 0x33, 0x4e, 0x0a, 0x89,
0x64, 0x1e, 0xb4, 0x7a, 0x5d, 0x0d, 0x12, 0xcf, 0x2b, 0xdd, 0x35, 0x90, 0x55, 0xc3, 0x0e, 0xcd, 0x0e, 0xad, 0x6b,
0x8f, 0x43, 0xd0, 0xb4, 0x6a, 0xdb, 0x4b, 0xe5, 0xa4, 0x9b, 0x09, 0x23, 0x1a, 0x43, 0xa9, 0xff, 0x61, 0x8b, 0x07,
0xd6, 0x0f, 0x83, 0x23, 0xbe, 0x8a, 0x66, 0xa2, 0x10, 0x2f, 0x11, 0x01, 0x0d, 0xc6, 0x96, 0xba, 0x87, 0xaa, 0xa8,
0x44, 0x7c, 0x1e, 0x34, 0xdb, 0xba, 0x41, 0x90, 0xf0, 0x7a, 0xdb, 0x63, 0x60, 0x96, 0x8d, 0x64, 0xcb, 0x2f, 0x28,
0x96, 0x04, 0xd5, 0xc0, 0x7a, 0xe8, 0xec, 0xd1, 0x61, 0x1b, 0x09, 0x4b, 0x4c, 0x6e, 0x1b, 0x31, 0x98, 0x9c, 0x6d,
0xdb, 0xd0, 0xec, 0xa4, 0x0f, 0x0a, 0xe0, 0xc8, 0x35, 0xc4, 0x93, 0xdc, 0xee, 0x19, 0x00, 0x80, 0x07, 0xf5, 0xcf,
0xe0, 0x7f, 0x75, 0xf8, 0x52, 0x3a, 0xfc, 0x0d, 0x84, 0xa5, 0xc1, 0xb2, 0x1c, 0x25, 0x40, 0xc5, 0x8b, 0xb3, 0xdb,
0x7a, 0x1b, 0x44, 0xff, 0x79, 0x9a, 0x26, 0xc4, 0xe7, 0x9e, 0x78, 0x4c, 0xa5, 0x94, 0xab, 0x78, 0x34, 0x9d, 0xb5,
0xb7, 0x24, 0x26, 0xe7, 0x9a, 0xf3, 0xe5, 0x2a, 0x35, 0x4b, 0xbe, 0x74, 0xd7, 0x01, 0xbc, 0x71, 0x72, 0x03, 0x5c,
0x40, 0x24, 0x17, 0x61, 0x5b, 0x7b, 0x19, 0xc2, 0x7f, 0x4e, 0x5b, 0x24, 0xfb, 0x8b, 0xc3, 0x51, 0x00, 0x3d, 0x09,
0x04, 0x05, 0x72, 0x64, 0xf6, 0xf0, 0x6b, 0x99, 0x38, 0x93, 0x5b, 0xac, 0x0c, 0xff, 0x40, 0x7b, 0x53, 0xba, 0xab,
0x61, 0xc9, 0xa2, 0xde, 0xd6, 0x2b, 0x0c, 0xbd, 0x85, 0xac, 0x4c, 0x64, 0x8b, 0xe6, 0x69, 0x2b, 0x56, 0x10, 0x1b,
0x08, 0x59, 0x6c, 0x55, 0x0a, 0xaa, 0x93, 0x85, 0x1d, 0x45, 0xda, 0xc6, 0x39, 0xe6, 0x6a, 0xab, 0x71, 0x73, 0x46,
0x0f, 0xf7, 0xab, 0x4e, 0x88, 0xae, 0x8c, 0xe0, 0x91, 0xda, 0x35, 0x8c, 0xd3, 0x18, 0x92, 0x3d, 0xab, 0x2f, 0x0d,
0xd6, 0xc9, 0x06, 0xdb, 0xc2, 0x62, 0xcb, 0x8a, 0x23, 0x5b, 0x28, 0x2e, 0x9b, 0x96, 0xc4, 0xc1, 0x42, 0xa9, 0x8d,
0x69, 0xe5, 0x8f, 0x89, 0x12, 0x11, 0x9a, 0x56, 0x3b, 0x3c, 0x2b, 0xb4, 0xb2, 0x7b, 0xfd, 0xdb, 0x80, 0x92, 0xb4,
0xca, 0x44, 0x37, 0x8c, 0x94, 0x2f, 0x4a, 0xb4, 0xc4, 0x28, 0x83, 0x8a, 0x78, 0xcb, 0xd2, 0x5c, 0x5c, 0x35, 0x29,
0x95, 0x35, 0x2f, 0x99, 0x28, 0x4f, 0x22, 0x18, 0x70, 0x85, 0xee, 0x2c, 0xb9, 0xef, 0x14, 0x57, 0x73, 0x23, 0x45,
0xae, 0xdc, 0x54, 0x56, 0x55, 0x78, 0x56, 0xad, 0x48, 0x26, 0x27, 0xbf, 0xcb, 0xd7, 0x54, 0xa9, 0xa8, 0xd7, 0xb5,
0x71, 0x9c, 0xe7, 0x79, 0x89, 0x5c, 0xa9, 0x6a, 0x53, 0xe8, 0xfa, 0x0d, 0x69, 0x36, 0xed, 0xad, 0x33, 0xc9, 0x75,
0x17, 0xe9, 0x8f, 0x31, 0x58, 0xa6, 0x27, 0xfc, 0x79, 0xc6, 0xb6, 0xd5, 0x65, 0x90, 0x31, 0xc6, 0x9d, 0x29, 0x95,
0x83, 0xe5, 0x4e, 0xbb, 0xd7, 0xdc, 0x8e, 0xd0, 0x3a, 0x54, 0x27, 0xce, 0xf5, 0xdb, 0xbd, 0x89, 0x3c, 0x61, 0xb3,
0x71, 0x23, 0xc7, 0x55, 0x9e, 0xea, 0x50, 0xe4, 0x37, 0xae, 0x72, 0x34, 0x66, 0xb9, 0x6e, 0x2c, 0x0a, 0x69, 0x8d,
0x94, 0x8b, 0x98, 0x08, 0x05, 0x3c, 0x45, 0x45, 0xe1, 0x6c, 0x22, 0xc5, 0x8f, 0x1f, 0x9f, 0x3f, 0xd2, 0x01, 0x12,
0xec, 0x86, 0x2f, 0x87, 0x8b, 0x47, 0x30, 0xd0, 0x77, 0x77, 0x1a, 0x13, 0x2d, 0xc6, 0x31, 0x65, 0x77, 0xd8, 0x8c,
0xe7, 0xe0, 0x16, 0xf9, 0x4b, 0xd4, 0xc5, 0xa8, 0x67, 0x79, 0xe7, 0xc3, 0x07, 0x94, 0x46, 0xa3, 0x8c, 0x67, 0xd3,
0xff, 0x5f, 0x96, 0xfa, 0xed, 0xa7, 0xd3, 0xdd, 0x4f, 0x27, 0x49, 0x27, 0xcf, 0xa4, 0x02, 0xc3, 0x27, 0x3c, 0x96,
0x64, 0x24, 0x55, 0xc8, 0x36, 0x45, 0x68, 0x90, 0xea, 0x03, 0x0b, 0x46, 0xa7, 0x8d, 0xb4, 0x26, 0x91, 0x07, 0x4c,
0x80, 0x7b, 0x93, 0xa8, 0x10, 0xcb, 0x5e, 0x8d, 0x42, 0x86, 0x3e, 0x2a, 0x7c, 0x99, 0x79, 0x8e, 0xcb, 0x43, 0x28};
// Backwards compatibility alias
#define INDEX_GZ INDEX_BR
+8 -14
View File
@@ -368,8 +368,8 @@ optional<ClimateDeviceRestoreState> Climate::restore_state_() {
}
void Climate::save_state_(const ClimateTraits &traits) {
#if (defined(USE_ESP32) || defined(USE_ESP8266)) && !defined(CLANG_TIDY)
#pragma GCC diagnostic push
#if (defined(USE_ESP32) || (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0))) && \
!defined(CLANG_TIDY)
#pragma GCC diagnostic ignored "-Wclass-memaccess"
#define TEMP_IGNORE_MEMACCESS
#endif
@@ -749,39 +749,33 @@ void Climate::dump_traits_(const char *tag) {
}
if (!traits.get_supported_modes().empty()) {
ESP_LOGCONFIG(tag, " Supported modes:");
for (ClimateMode m : traits.get_supported_modes()) {
for (ClimateMode m : traits.get_supported_modes())
ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_mode_to_string(m)));
}
}
if (!traits.get_supported_fan_modes().empty()) {
ESP_LOGCONFIG(tag, " Supported fan modes:");
for (ClimateFanMode m : traits.get_supported_fan_modes()) {
for (ClimateFanMode m : traits.get_supported_fan_modes())
ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_fan_mode_to_string(m)));
}
}
if (!traits.get_supported_custom_fan_modes().empty()) {
ESP_LOGCONFIG(tag, " Supported custom fan modes:");
for (const char *s : traits.get_supported_custom_fan_modes()) {
for (const char *s : traits.get_supported_custom_fan_modes())
ESP_LOGCONFIG(tag, " - %s", s);
}
}
if (!traits.get_supported_presets().empty()) {
ESP_LOGCONFIG(tag, " Supported presets:");
for (ClimatePreset p : traits.get_supported_presets()) {
for (ClimatePreset p : traits.get_supported_presets())
ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_preset_to_string(p)));
}
}
if (!traits.get_supported_custom_presets().empty()) {
ESP_LOGCONFIG(tag, " Supported custom presets:");
for (const char *s : traits.get_supported_custom_presets()) {
for (const char *s : traits.get_supported_custom_presets())
ESP_LOGCONFIG(tag, " - %s", s);
}
}
if (!traits.get_supported_swing_modes().empty()) {
ESP_LOGCONFIG(tag, " Supported swing modes:");
for (ClimateSwingMode m : traits.get_supported_swing_modes()) {
for (ClimateSwingMode m : traits.get_supported_swing_modes())
ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_swing_mode_to_string(m)));
}
}
}
+1
View File
@@ -100,6 +100,7 @@ bool CM1106Component::cm1106_write_command_(const uint8_t *command, size_t comma
void CM1106Component::dump_config() {
ESP_LOGCONFIG(TAG, "CM1106:");
LOG_SENSOR(" ", "CO2", this->co2_sensor_);
this->check_uart_settings(9600);
if (this->is_failed()) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
}
-8
View File
@@ -46,14 +46,6 @@ CONFIG_SCHEMA = (
.extend(uart.UART_DEVICE_SCHEMA)
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"cm1106",
baud_rate=9600,
data_bits=8,
parity="NONE",
stop_bits=1,
)
async def to_code(config: ConfigType) -> None:
"""Code generation entry point."""
-3
View File
@@ -14,10 +14,8 @@ CONF_CHANNEL_COLORS = "channel_colors"
CONF_CLIMATE_ID = "climate_id"
CONF_CO2_EQUIVALENT = "co2_equivalent"
CONF_COLOR_DEPTH = "color_depth"
CONF_COLUMNS = "columns"
CONF_CRC_ENABLE = "crc_enable"
CONF_DATA_BITS = "data_bits"
CONF_DESCRIPTION = "description"
CONF_DRAW_ROUNDING = "draw_rounding"
CONF_ENABLE_OTA_DOWNGRADE_PROTECTION = "enable_ota_downgrade_protection"
CONF_ENABLED = "enabled"
@@ -26,7 +24,6 @@ CONF_GYROSCOPE_RANGE = "gyroscope_range"
CONF_IAQ = "iaq"
CONF_IGNORE_NOT_FOUND = "ignore_not_found"
CONF_IS_WRGB = "is_wrgb"
CONF_KEYS = "keys"
CONF_LABEL = "label"
CONF_LIBRETINY = "libretiny"
CONF_LOOP = "loop"
+5 -8
View File
@@ -249,7 +249,7 @@ bool CS5460AComponent::check_status_() {
bool dir = status & (1 << 21);
if (current_gain_ < 0)
dir = !dir;
ESP_LOGI(TAG, "Energy counter %s pulse", dir ? LOG_STR_LITERAL("negative") : LOG_STR_LITERAL("positive"));
ESP_LOGI(TAG, "Energy counter %s pulse", dir ? "negative" : "positive");
clear |= 1 << 22;
}
@@ -319,9 +319,7 @@ void CS5460AComponent::dump_config() {
ESP_LOGCONFIG(TAG,
"CS5460A:\n"
" Init status: %s",
state == COMPONENT_STATE_LOOP
? LOG_STR_LITERAL("OK")
: (state == COMPONENT_STATE_FAILED ? LOG_STR_LITERAL("failed") : LOG_STR_LITERAL("other")));
state == COMPONENT_STATE_LOOP ? "OK" : (state == COMPONENT_STATE_FAILED ? "failed" : "other"));
LOG_PIN(" CS Pin: ", cs_);
ESP_LOGCONFIG(TAG,
" Samples / cycle: %" PRIu32 "\n"
@@ -332,10 +330,9 @@ void CS5460AComponent::dump_config() {
" Current HPF: %s\n"
" Voltage HPF: %s\n"
" Pulse energy: %.2f Wh",
samples_, phase_offset_,
pga_gain_ == CS5460A_PGA_GAIN_50X ? LOG_STR_LITERAL("50x") : LOG_STR_LITERAL("10x"), current_gain_,
voltage_gain_, current_hpf_ ? LOG_STR_LITERAL("enabled") : LOG_STR_LITERAL("disabled"),
voltage_hpf_ ? LOG_STR_LITERAL("enabled") : LOG_STR_LITERAL("disabled"), pulse_energy_wh_);
samples_, phase_offset_, pga_gain_ == CS5460A_PGA_GAIN_50X ? "50x" : "10x", current_gain_,
voltage_gain_, current_hpf_ ? "enabled" : "disabled", voltage_hpf_ ? "enabled" : "disabled",
pulse_energy_wh_);
LOG_SENSOR(" ", "Voltage", voltage_sensor_);
LOG_SENSOR(" ", "Current", current_sensor_);
LOG_SENSOR(" ", "Power", power_sensor_);
+1
View File
@@ -58,6 +58,7 @@ void CSE7761Component::dump_config() {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
}
LOG_UPDATE_INTERVAL(this);
this->check_uart_settings(38400, 1, uart::UART_CONFIG_PARITY_EVEN, 8);
}
void CSE7761Component::update() {
+1 -7
View File
@@ -68,13 +68,7 @@ CONFIG_SCHEMA = (
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"cse7761",
baud_rate=38400,
require_rx=True,
require_tx=True,
data_bits=8,
parity="EVEN",
stop_bits=1,
"cse7761", baud_rate=38400, require_rx=True, require_tx=True
)
+1
View File
@@ -255,6 +255,7 @@ void CSE7766Component::dump_config() {
LOG_SENSOR(" ", "Apparent Power", this->apparent_power_sensor_);
LOG_SENSOR(" ", "Reactive Power", this->reactive_power_sensor_);
LOG_SENSOR(" ", "Power Factor", this->power_factor_sensor_);
this->check_uart_settings(4800, 1, uart::UART_CONFIG_PARITY_EVEN);
}
} // namespace esphome::cse7766
+1 -6
View File
@@ -84,12 +84,7 @@ CONFIG_SCHEMA = (
.extend(cv.COMPONENT_SCHEMA)
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"cse7766",
baud_rate=4800,
require_rx=True,
data_bits=8,
parity="EVEN",
stop_bits=1,
"cse7766", baud_rate=4800, parity="EVEN", require_rx=True
)
View File
-45
View File
@@ -1,45 +0,0 @@
#include "d01.h"
#include "esphome/core/log.h"
// uart specification for d01 sensor from https://manuals.plus/ae/1005006417362019:
//
// A frame of serial output data includes 4 bytes, formatted as follows:
// __Characteristic byte: Fixed value 0xA5.
// __Data byte: DATAH is the high 7 bits of the concentration value, and DATAL is the low 7 bits of the concentration
// value.
// __Check byte: The low 7 bits of the sum of all bytes before the check byte.
//
// If the serial output is 4 bytes of data: 0*A5 0*01 0*2C 0*52, then DATAH = 0*01 = 1, DATAL = 0*2C = 44.
// Concentration value = 1*128 + 44 = 172 µg/m³.
//
// The PM2.5 dust concentration value obtained from the dust sensor needs to be calibrated with a K value coefficient
// based on the TSI instrument's photometric method. It is generally recommended to use 0.4.
namespace esphome::d01 {
static const char *const TAG = "d01";
static const uint8_t D01_FRAME_HEADER = 0xA5;
void D01SensorComponent::dump_config() { LOG_SENSOR(" ", "D01 PM2.5", this); }
void D01SensorComponent::loop() {
uint8_t buf[4];
while (this->available() >= 4) {
if (this->peek() != D01_FRAME_HEADER) {
this->read();
continue;
}
this->read_array(buf, 4);
uint8_t sum = (buf[0] + buf[1] + buf[2]) & 0x7F;
if (sum != buf[3]) {
ESP_LOGW(TAG, "checksum mismatch");
continue;
}
uint16_t latest_concentration = (buf[1] & 0x7F) * 128 + (buf[2] & 0x7F);
ESP_LOGV(TAG, "Unadjusted PM2.5 Concentration: %d µg/m³", latest_concentration);
this->publish_state(latest_concentration);
}
}
} // namespace esphome::d01
-14
View File
@@ -1,14 +0,0 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/uart/uart.h"
namespace esphome::d01 {
class D01SensorComponent final : public sensor::Sensor, public Component, public uart::UARTDevice {
public:
void dump_config() override;
void loop() override;
};
} // namespace esphome::d01
-45
View File
@@ -1,45 +0,0 @@
import esphome.codegen as cg
from esphome.components import sensor, uart
import esphome.config_validation as cv
from esphome.const import (
DEVICE_CLASS_PM25,
ICON_BLUR,
STATE_CLASS_MEASUREMENT,
UNIT_MICROGRAMS_PER_CUBIC_METER,
)
from esphome.types import ConfigType
CODEOWNERS = ["@ch604"]
DEPENDENCIES = ["uart"]
d01_ns = cg.esphome_ns.namespace("d01")
D01SensorComponent = d01_ns.class_(
"D01SensorComponent", sensor.Sensor, uart.UARTDevice, cg.Component
)
CONFIG_SCHEMA = (
sensor.sensor_schema(
D01SensorComponent,
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
icon=ICON_BLUR,
accuracy_decimals=0,
device_class=DEVICE_CLASS_PM25,
state_class=STATE_CLASS_MEASUREMENT,
)
.extend(cv.COMPONENT_SCHEMA)
.extend(uart.UART_DEVICE_SCHEMA)
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"d01",
baud_rate=9600,
require_rx=True,
require_tx=False,
)
async def to_code(config: ConfigType) -> None:
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
-8
View File
@@ -26,14 +26,6 @@ CONFIG_SCHEMA = (
.extend(cv.polling_component_schema("30s"))
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"daly_bms",
baud_rate=9600,
data_bits=8,
parity="NONE",
stop_bits=1,
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
+4 -1
View File
@@ -22,7 +22,10 @@ static const uint8_t DALY_REQUEST_TEMPERATURE = 0x96;
void DalyBmsComponent::setup() { this->next_request_ = 1; }
void DalyBmsComponent::dump_config() { ESP_LOGCONFIG(TAG, "Daly BMS:"); }
void DalyBmsComponent::dump_config() {
ESP_LOGCONFIG(TAG, "Daly BMS:");
this->check_uart_settings(9600);
}
void DalyBmsComponent::update() {
this->trigger_next_ = true;
+2 -2
View File
@@ -22,9 +22,9 @@ void DebugComponent::dump_config() {
LOG_SENSOR(" ", "Free space on heap", this->free_sensor_);
LOG_SENSOR(" ", "Largest free heap block", this->block_sensor_);
LOG_SENSOR(" ", "CPU frequency", this->cpu_frequency_sensor_);
#ifdef USE_ESP8266
#if defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)
LOG_SENSOR(" ", "Heap fragmentation", this->fragmentation_sensor_);
#endif // USE_ESP8266
#endif // defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)
#endif // USE_SENSOR
char device_info_buffer[DEVICE_INFO_BUFFER_SIZE];
+2 -2
View File
@@ -35,7 +35,7 @@ class DebugComponent final : public PollingComponent {
#ifdef USE_SENSOR
void set_free_sensor(sensor::Sensor *free_sensor) { free_sensor_ = free_sensor; }
void set_block_sensor(sensor::Sensor *block_sensor) { block_sensor_ = block_sensor; }
#if defined(USE_ESP8266) || defined(USE_ESP32)
#if (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)) || defined(USE_ESP32)
void set_fragmentation_sensor(sensor::Sensor *fragmentation_sensor) { fragmentation_sensor_ = fragmentation_sensor; }
#endif
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
@@ -61,7 +61,7 @@ class DebugComponent final : public PollingComponent {
sensor::Sensor *free_sensor_{nullptr};
sensor::Sensor *block_sensor_{nullptr};
#if defined(USE_ESP8266) || defined(USE_ESP32)
#if (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)) || defined(USE_ESP32)
sensor::Sensor *fragmentation_sensor_{nullptr};
#endif
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
@@ -159,10 +159,12 @@ void DebugComponent::update_platform_() {
// NOLINTNEXTLINE(readability-static-accessed-through-instance)
this->block_sensor_->publish_state(ESP.getMaxFreeBlockSize());
}
#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)
if (this->fragmentation_sensor_ != nullptr) {
// NOLINTNEXTLINE(readability-static-accessed-through-instance)
this->fragmentation_sensor_->publish_state(ESP.getHeapFragmentation());
}
#endif
#endif
}
+5 -2
View File
@@ -52,9 +52,12 @@ CONFIG_SCHEMA = {
),
cv.Optional(CONF_FRAGMENTATION): cv.All(
cv.Any(
cv.only_on_esp8266,
cv.All(
cv.only_on_esp8266,
cv.require_framework_version(esp8266_arduino=cv.Version(2, 5, 2)),
),
cv.only_on_esp32,
msg="This feature is only available on ESP8266 and ESP32",
msg="This feature is only available on ESP8266 (Arduino 2.5.2+) and ESP32",
),
sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
+1 -6
View File
@@ -60,12 +60,7 @@ CONFIG_SCHEMA = cv.All(
).extend(uart.UART_DEVICE_SCHEMA)
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"dfplayer",
baud_rate=9600,
require_tx=True,
data_bits=8,
parity="NONE",
stop_bits=1,
"dfplayer", baud_rate=9600, require_tx=True
)
+4 -1
View File
@@ -277,6 +277,9 @@ void DFPlayer::loop() {
}
}
}
void DFPlayer::dump_config() { ESP_LOGCONFIG(TAG, "DFPlayer:"); }
void DFPlayer::dump_config() {
ESP_LOGCONFIG(TAG, "DFPlayer:");
this->check_uart_settings(9600);
}
} // namespace esphome::dfplayer
+3 -4
View File
@@ -20,8 +20,8 @@ void DHT::dump_config() {
"DHT:\n"
" %sModel: %s\n"
" Internal pull-up: %s",
this->is_auto_detect_ ? LOG_STR_LITERAL("Auto-detected ") : "",
this->model_ == DHT_MODEL_DHT11 ? LOG_STR_LITERAL("DHT11") : LOG_STR_LITERAL("DHT22 or equivalent"),
this->is_auto_detect_ ? "Auto-detected " : "",
this->model_ == DHT_MODEL_DHT11 ? "DHT11" : "DHT22 or equivalent",
ONOFF(this->t_pin_->get_flags() & gpio::FLAG_PULLUP));
LOG_PIN(" Pin: ", this->t_pin_);
LOG_UPDATE_INTERVAL(this);
@@ -154,9 +154,8 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r
}
}
if (error_code != 0) {
if (report_errors) {
if (report_errors)
ESP_LOGW(TAG, ESP_LOG_MSG_COMM_FAIL);
}
return false;
}
-68
View File
@@ -1,68 +0,0 @@
#include "ds1603l.h"
#include <cstring>
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::ds1603l {
static const char *const TAG = "ds1603l.sensor";
void DS1603L::loop() {
// Assemble frames one byte at a time so a stream that starts mid-frame can realign
uint8_t byte;
while (this->available() > 0 && this->read_byte(&byte)) {
if (this->rx_count_ == 0 && byte != HEADER_BYTE) {
ESP_LOGV(TAG, "Skipping byte 0x%02X while looking for header", byte);
continue;
}
this->rx_buffer_[this->rx_count_++] = byte;
if (this->rx_count_ < FRAME_SIZE) {
continue;
}
if (this->parse_data_()) {
this->rx_count_ = 0;
} else {
// The header byte was part of the payload of a misaligned frame, so realign instead of dropping everything
this->resync_();
}
}
}
void DS1603L::dump_config() { LOG_SENSOR("", "DS1603L", this); }
bool DS1603L::parse_data_() {
uint8_t header = this->rx_buffer_[0];
uint8_t data_h = this->rx_buffer_[1];
uint8_t data_l = this->rx_buffer_[2];
uint8_t checksum = this->rx_buffer_[3];
uint8_t computed_checksum = (header + data_h + data_l) & 0xFF;
ESP_LOGV(TAG, "Data: Header=0x%02X, Data_H=0x%02X, Data_L=0x%02X, Checksum=0x%02X", header, data_h, data_l, checksum);
if (checksum != computed_checksum) {
ESP_LOGW(TAG, "Checksum mismatch: received 0x%02X, expected 0x%02X", checksum, computed_checksum);
return false;
}
this->publish_state(encode_uint16(data_h, data_l));
return true;
}
void DS1603L::resync_() {
// Drop the byte that was treated as the header, then look for the next candidate header in what is left
size_t start = 1;
while (start < this->rx_count_ && this->rx_buffer_[start] != HEADER_BYTE) {
start++;
}
this->rx_count_ -= start;
if (this->rx_count_ > 0) {
memmove(this->rx_buffer_, this->rx_buffer_ + start, this->rx_count_);
}
}
} // namespace esphome::ds1603l
-30
View File
@@ -1,30 +0,0 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/uart/uart.h"
#include "esphome/core/component.h"
namespace esphome::ds1603l {
class DS1603L final : public sensor::Sensor, public Component, public uart::UARTDevice {
public:
void loop() override;
void dump_config() override;
protected:
static constexpr uint8_t HEADER_BYTE = 0xFF;
static constexpr size_t FRAME_SIZE = 4;
// Validates the checksum of the frame in rx_buffer_ and publishes it. Returns false if the frame is invalid.
bool parse_data_();
// Drops the first buffered byte and realigns the buffer on the next possible header byte.
void resync_();
uint8_t rx_buffer_[FRAME_SIZE]; // Buffer for the frame being assembled
size_t rx_count_{0}; // Number of bytes currently in rx_buffer_
};
} // namespace esphome::ds1603l
-43
View File
@@ -1,43 +0,0 @@
import esphome.codegen as cg
from esphome.components import sensor, uart
import esphome.config_validation as cv
from esphome.const import (
DEVICE_CLASS_DISTANCE,
STATE_CLASS_MEASUREMENT,
UNIT_MILLIMETER,
)
from esphome.types import ConfigType
CODEOWNERS = ["@JakeLC15"]
DEPENDENCIES = ["uart"]
ds1603l_ns = cg.esphome_ns.namespace("ds1603l")
DS1603L = ds1603l_ns.class_("DS1603L", sensor.Sensor, cg.Component, uart.UARTDevice)
CONFIG_SCHEMA = (
sensor.sensor_schema(
DS1603L,
unit_of_measurement=UNIT_MILLIMETER,
accuracy_decimals=0,
device_class=DEVICE_CLASS_DISTANCE,
state_class=STATE_CLASS_MEASUREMENT,
)
.extend(uart.UART_DEVICE_SCHEMA)
.extend(cv.COMPONENT_SCHEMA)
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"ds1603l",
baud_rate=9600,
require_tx=False,
require_rx=True,
data_bits=8,
stop_bits=1,
)
async def to_code(config: ConfigType) -> None:
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
+1 -1
View File
@@ -93,7 +93,7 @@ void Emc2101Component::dump_config() {
if (this->is_failed()) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
}
ESP_LOGCONFIG(TAG, " Mode: %s", this->dac_mode_ ? LOG_STR_LITERAL("DAC") : LOG_STR_LITERAL("PWM"));
ESP_LOGCONFIG(TAG, " Mode: %s", this->dac_mode_ ? "DAC" : "PWM");
if (this->dac_mode_) {
ESP_LOGCONFIG(TAG, " DAC Conversion Rate: %X", this->dac_conversion_rate_);
} else {
+1 -1
View File
@@ -216,7 +216,7 @@ void ENS210Component::extract_measurement_(uint32_t val, int *data, int *status)
// Sets ENS210 to low (true) or high (false) power. Returns false on I2C problems.
bool ENS210Component::set_low_power_(bool enable) {
uint8_t low_power_cmd = enable ? 0x01 : 0x00;
ESP_LOGD(TAG, "Enable low power: %s", enable ? LOG_STR_LITERAL("true") : LOG_STR_LITERAL("false"));
ESP_LOGD(TAG, "Enable low power: %s", enable ? "true" : "false");
bool result = this->write_byte(ENS210_REGISTER_SYS_CTRL, low_power_cmd);
delay(ENS210_BOOTING_MS);
return result;
@@ -1,139 +0,0 @@
#include "epaper_spi_uc8179.h"
#include <algorithm>
#include "esphome/core/log.h"
namespace esphome::epaper_spi {
static constexpr const char *const TAG = "epaper_spi.uc8179";
bool EPaperUC8179::initialise(bool partial) {
EPaperBase::initialise(partial); // send the model init sequence
this->partial_ = partial;
ESP_LOGV(TAG, "Power on");
// POWER ON must precede the waveform/mode registers and the data transfer
// (the original driver powers on and busy-waits before writing them).
// The state machine busy-waits before entering TRANSFER_DATA.
this->command(0x04);
// Give the busy line time to assert before the state machine polls it
this->next_delay_ = 100;
return true;
}
// Set up the refresh mode. Must be called after power-on has completed.
void EPaperUC8179::set_refresh_mode_() {
if (!this->is_using_partial_update_()) {
return; // plain full refresh uses the mode set by the init sequence
}
// Fast and partial refresh use flipped data polarity and a floating border
this->cmd_data(0x50, {0xA9, 0x07});
// Force the waveform via the temperature registers: 0x5A selects the fast
// full-refresh waveform, 0x6E the partial-refresh waveform
this->cmd_data(0xE0, {0x02});
if (this->partial_) {
this->cmd_data(0xE5, {0x6E});
this->command(0x91); // enter partial mode
// Set the partial window to the full screen
const uint16_t x_end = this->width_ - 1;
const uint16_t y_end = this->height_ - 1;
this->cmd_data(0x90, {0x00, 0x00, static_cast<uint8_t>(x_end >> 8), static_cast<uint8_t>(x_end & 0xFF), 0x00, 0x00,
static_cast<uint8_t>(y_end >> 8), static_cast<uint8_t>(y_end & 0xFF), 0x01});
} else {
this->cmd_data(0xE5, {0x5A});
this->command(0x92); // exit partial mode
}
}
bool HOT EPaperUC8179::transfer_data() {
const uint32_t start_time = millis();
const size_t buffer_length = this->buffer_length_;
if (this->current_data_index_ == 0) {
this->set_refresh_mode_();
}
// Fast full refresh sends the previous-image plane as well, so that every pixel transitions
const bool two_pass = this->is_using_partial_update_() && !this->partial_;
// Plain full refresh sends inverted data (buffer is 1=white, the wire wants 0=white);
// in fast/partial mode the data polarity is flipped via the VCOM/data-interval
// register instead, so the new-image plane is sent unmodified
const bool invert_new_data = !this->is_using_partial_update_();
uint8_t bytes_to_send[MAX_TRANSFER_SIZE];
// Phase 1 (fast full refresh only): previous image via 0x10 (DTM1), inverse of the new image
if (two_pass && this->current_data_index_ < buffer_length) {
if (this->current_data_index_ == 0) {
this->command(0x10); // DATA START TRANSMISSION 1 (previous image)
}
this->start_data_();
while (this->current_data_index_ < buffer_length) {
const size_t bytes_to_copy = std::min(MAX_TRANSFER_SIZE, buffer_length - this->current_data_index_);
for (size_t i = 0; i < bytes_to_copy; i++) {
bytes_to_send[i] = ~this->buffer_[this->current_data_index_ + i];
}
this->write_array(bytes_to_send, bytes_to_copy);
this->current_data_index_ += bytes_to_copy;
if (millis() - start_time > MAX_TRANSFER_TIME) {
this->disable();
return false;
}
}
this->disable();
}
// Phase 2: new image via 0x13 (DTM2)
const size_t offset = two_pass ? buffer_length : 0;
const size_t total = offset + buffer_length;
if (this->current_data_index_ < total) {
if (this->current_data_index_ == offset) {
this->command(0x13); // DATA START TRANSMISSION 2 (new image)
}
this->start_data_();
while (this->current_data_index_ < total) {
const size_t bytes_to_copy = std::min(MAX_TRANSFER_SIZE, total - this->current_data_index_);
const size_t data_idx = this->current_data_index_ - offset;
for (size_t i = 0; i < bytes_to_copy; i++) {
const uint8_t byte = this->buffer_[data_idx + i];
bytes_to_send[i] = invert_new_data ? ~byte : byte;
}
this->write_array(bytes_to_send, bytes_to_copy);
this->current_data_index_ += bytes_to_copy;
if (millis() - start_time > MAX_TRANSFER_TIME) {
this->disable();
return false;
}
}
this->disable();
}
this->current_data_index_ = 0;
return true;
}
void EPaperUC8179::power_on() {
// Power-on is sent at the end of initialise() instead, because the
// waveform/mode registers and the data transfer must follow it
}
void EPaperUC8179::refresh_screen(bool /*partial*/) {
ESP_LOGV(TAG, "Refresh");
this->command(0x12); // DISPLAY REFRESH
// Delay the next busy poll: the busy line takes a short time to assert after
// the refresh command, and polling too early would read it as already idle
this->next_delay_ = 100;
}
void EPaperUC8179::power_off() {
ESP_LOGV(TAG, "Power off");
this->command(0x02); // POWER OFF
}
void EPaperUC8179::deep_sleep() {
// Deep sleep loses the previous-image RAM that partial refresh compares against
if (!this->is_using_partial_update_()) {
ESP_LOGV(TAG, "Deep sleep");
this->cmd_data(0x07, {0xA5}); // DEEP SLEEP with check code
}
}
} // namespace esphome::epaper_spi
@@ -1,52 +0,0 @@
#pragma once
#include "epaper_spi.h"
namespace esphome::epaper_spi {
/**
* Monochrome e-paper displays using the UC8179 controller.
* Supports: 7.5" V2 (EPD_7in5_V2), 800x480 pixels, as used by the
* Waveshare 7.5" V2 HAT and the Seeed reTerminal E1001.
*
* Buffer layout: 1 bit per pixel, 1=white, 0=black (the base class default).
*
* The INITIALISE state sends the panel configuration followed by power-on
* (0x04); the state machine busy-waits for power-on to complete before
* TRANSFER_DATA, which first writes the waveform/mode registers (these are
* only accepted while powered) and then the image data. The state machine
* busy-waits again before triggering REFRESH_SCREEN (0x12).
*
* Three refresh modes are used, following the Waveshare EPD_7in5_V2 examples:
* - full_update_every == 1: plain full refresh. The new image is sent
* inverted to DTM2 (0x13) and the controller uses its normal waveform.
* - full_update_every > 1, full update: fast full refresh. The data polarity
* is flipped via the VCOM/data-interval register, a fast waveform is forced
* via the temperature registers, and the image is sent to both DTM1 (0x10,
* inverted) and DTM2 (0x13) so that every pixel transitions.
* - full_update_every > 1, partial update: partial refresh. A partial-update
* waveform is forced, partial mode is entered with a full-screen window and
* only DTM2 is sent; the controller compares against its previous-image RAM.
*/
class EPaperUC8179 final : public EPaperBase {
public:
EPaperUC8179(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
size_t init_sequence_length)
: EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_BINARY) {
this->buffer_length_ = this->row_width_ * height;
}
protected:
bool initialise(bool partial) override;
bool transfer_data() override;
void refresh_screen(bool partial) override;
void power_on() override;
void power_off() override;
void deep_sleep() override;
void set_refresh_mode_();
// Set by initialise() so transfer_data() knows which planes to send
bool partial_{};
};
} // namespace esphome::epaper_spi
@@ -1,93 +0,0 @@
"""Monochrome e-paper displays using the UC8179 controller.
Supported models:
- waveshare-7.5in-v2: 7.5" mono display, 800x480 pixels (EPD_7in5_V2)
- seeed-reterminal-e1001: Seeed reTerminal E1001, which uses the same
7.5" 800x480 panel on an integrated ESP32-S3 board
Panel configuration and power-on (0x04) are both sent during the INITIALISE
state; the state machine's built-in busy wait then covers the power-on delay
before the waveform/mode registers and image data are transferred.
These displays support fast full and partial refresh: set ``full_update_every``
greater than 1 to enable it. Every ``full_update_every``-th update is a fast
full refresh, with partial refreshes in between.
"""
from typing import Any
from esphome.const import CONF_DATA_RATE
from . import EpaperModel
class UC8179(EpaperModel):
"""EpaperModel class for monochrome displays using the UC8179 controller."""
def __init__(
self,
name: str,
class_name: str = "EPaperUC8179",
data_rate: str = "10MHz",
**defaults: Any,
) -> None:
defaults.setdefault(CONF_DATA_RATE, data_rate)
super().__init__(name, class_name, **defaults)
def get_init_sequence(self, config: dict) -> tuple:
"""Generate the initialization sequence for UC8179 mono displays.
Panel configuration only the driver appends power-on (0x04) at the
end of the INITIALISE state, and the state machine busy-waits for it
to complete before the data transfer starts.
"""
width, height = self.get_dimensions(config)
return (
# POWER SETTING
(0x01, 0x07, 0x07, 0x3F, 0x3F),
# BOOSTER SOFT START
(0x06, 0x17, 0x17, 0x28, 0x17),
# PANEL SETTING (black/white mode, LUT from OTP)
(0x00, 0x1F),
# RESOLUTION SETTING (width x height)
(
0x61,
(width >> 8) & 0xFF,
width & 0xFF,
(height >> 8) & 0xFF,
height & 0xFF,
),
# DUAL SPI MODE (disabled)
(0x15, 0x00),
# VCOM AND DATA INTERVAL SETTING
(0x50, 0x10, 0x07),
# TCON SETTING
(0x60, 0x22),
)
uc8179 = UC8179("uc8179")
# Waveshare 7.5" V2 mono (EPD_7in5_V2) — 800x480, UC8179 controller
waveshare_7_5_v2 = uc8179.extend(
"waveshare-7.5in-v2",
width=800,
height=480,
)
# Seeed reTerminal E1001 — 7.5" mono e-paper (800x480), same panel as the
# Waveshare 7.5" V2, driven by an integrated ESP32-S3 board
waveshare_7_5_v2.extend(
"seeed-reterminal-e1001",
cs_pin=10,
dc_pin=11,
reset_pin=12,
busy_pin={
"number": 13,
"inverted": True,
"mode": {
"input": True,
"pullup": True,
},
},
)
+18 -31
View File
@@ -10,6 +10,7 @@ import subprocess
from typing import Any
from esphome import yaml_util
from esphome.build_helpers.pch import PCH_PREFIX_HEADER, pch_enabled, pch_extra_scripts
import esphome.codegen as cg
from esphome.components.const import CONF_ENABLE_OTA_DOWNGRADE_PROTECTION
from esphome.config_helpers import filter_source_files_from_defines
@@ -57,6 +58,7 @@ from esphome.coroutine import CoroPriority, coroutine_with_priority
from esphome.espidf.component import generate_idf_components
import esphome.final_validate as fv
from esphome.helpers import copy_file_if_changed, rmtree, write_file_if_changed
from esphome.platformio.toolchain import copy_pch_script
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from esphome.types import ConfigType
from esphome.writer import clean_build, clean_cmake_cache
@@ -182,13 +184,6 @@ SIGNED_OTA_V1_ECDSA_VARIANTS = {
VARIANT_ESP32,
}
# Variants that support execution from PSRAM
PSRAM_XIP_VARIANTS = {
VARIANT_ESP32S3,
VARIANT_ESP32P4,
VARIANT_ESP32S31,
}
# NVS encryption (HMAC peripheral scheme) is only available on variants that
# expose the HMAC peripheral (SOC_HMAC_SUPPORTED in soc_caps.h). The original
# ESP32 and ESP32-C2 do not have it. New variants with an HMAC peripheral
@@ -221,13 +216,11 @@ COMPILER_OPTIMIZATIONS = {
# builds that need them.
DEFAULT_EXCLUDED_IDF_COMPONENTS = (
"app_trace", # CPU trace/SystemView support - unused by ESPHome
"bt", # Bluetooth stack - re-included by request_bluetooth(); its REQUIRES pulls the WiFi stack back
"cmock", # Unit testing mock framework - ESPHome doesn't use IDF's testing
"console", # Console REPL - unused by ESPHome; espressif/mdns pulls it back when configured
"driver", # Legacy driver shim - only needed by esp32_touch, esp32_can for legacy headers
"esp-tls", # TLS wrapper - re-included by http_request, mqtt, web_server_idf
"esp_adc", # ADC driver - only needed by adc component
"esp_coex", # WiFi/BT coexistence - re-included by esp32_ble_tracker, zigbee; esp_wifi/bt pull it back
"esp_driver_cam", # Camera driver - the esp32-camera managed component pulls it back
"esp_driver_dac", # DAC driver - only needed by esp32_dac component
"esp_driver_gptimer", # General purpose timer - re-included by ac_dimmer, opentherm, Arduino BLE libs
@@ -245,7 +238,6 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = (
"esp_driver_twai", # TWAI/CAN driver - only needed by esp32_can component
"esp_eth", # Ethernet driver - only needed by ethernet component
"esp_gdbstub", # GDB stub panic handler - unused by ESPHome; bt pulls it back
"esp_hal_ieee802154", # 802.15.4 HAL - ieee802154 pulls it back
"esp_hid", # HID host/device support - ESPHome doesn't implement HID functionality
"esp_http_client", # HTTP client - only needed by http_request component
"esp_http_server", # HTTP server - re-included by web_server_idf, esp32_camera_web_server
@@ -253,11 +245,8 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = (
"esp_https_server", # HTTPS server - ESPHome has its own web server
"esp_lcd", # LCD controller drivers - only needed by display component
"esp_local_ctrl", # Local control over HTTPS/BLE - ESPHome has native API
"esp_phy", # RF PHY - re-included by internal_temperature on the original ESP32; esp_wifi/bt/ieee802154 pull it back
"esp_wifi", # WiFi stack - re-included by request_wifi(), espnow; bt pulls it back for BLE builds
"espcoredump", # Core dump support - ESPHome has its own debug component
"fatfs", # FAT filesystem - ESPHome doesn't use filesystem storage
"ieee802154", # 802.15.4 radio - IDF openthread and the Zigbee libs pull it back
"json", # cJSON library - ESPHome uses ArduinoJson instead
"mqtt", # ESP-IDF MQTT library - ESPHome has its own MQTT implementation
"nvs_sec_provider", # NVS encryption key provider - re-included when CONFIG_NVS_ENCRYPTION is set
@@ -273,7 +262,6 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = (
"unity", # Unit testing framework - ESPHome doesn't use IDF's testing
"wear_levelling", # Flash wear levelling for fatfs - unused since fatfs unused
"wifi_provisioning", # WiFi provisioning - ESPHome uses its own improv implementation
"wpa_supplicant", # WPA supplicant - re-included by request_wifi() for esp_eap_client.h
)
# Additional IDF managed components to exclude for Arduino framework builds
@@ -723,9 +711,6 @@ def request_wifi(ap: bool = False) -> None:
net.wifi = True
if ap:
net.wifi_ap = True
include_builtin_idf_component("esp_wifi")
# wifi_component.cpp includes esp_eap_client.h/esp_wpa2.h
include_builtin_idf_component("wpa_supplicant")
def request_ethernet() -> None:
@@ -737,14 +722,11 @@ def request_bluetooth() -> None:
"""Request the Bluetooth controller."""
net = _network_sdkconfig()
net.bluetooth = True
include_builtin_idf_component("bt")
def request_software_coexistence() -> None:
"""Request WiFi/BT software coexistence (only valid alongside WiFi)."""
_network_sdkconfig().software_coexistence = True
# Callers include esp_coexist.h directly.
include_builtin_idf_component("esp_coex")
def add_idf_component(
@@ -1530,7 +1512,7 @@ def final_validate(config) -> None:
)
)
if advanced[CONF_EXECUTE_FROM_PSRAM]:
if config[CONF_VARIANT] not in PSRAM_XIP_VARIANTS:
if config[CONF_VARIANT] not in {VARIANT_ESP32S3, VARIANT_ESP32P4}:
errs.append(
cv.Invalid(
f"'{CONF_EXECUTE_FROM_PSRAM}' is not available on this esp32 variant",
@@ -2324,8 +2306,6 @@ async def _reconcile_network_sdkconfig() -> None:
# WiFi stack: disable only when Ethernet is present and WiFi is not. WiFi
# relies on the IDF default (enabled), so it is never written True here.
# esp_wifi is excluded by default on IDF, so this only matters for Arduino
# or when bt pulls it back.
wifi_disabled = net.ethernet and not net.wifi
if wifi_disabled:
set_idf_sdkconfig_default("CONFIG_ESP_WIFI_ENABLED", False)
@@ -2455,6 +2435,11 @@ async def to_code(config):
cg.add_platformio_option("lib_ldf_mode", "off")
cg.add_platformio_option("lib_compat_mode", "strict")
# CI-speed only: this toolchain is being dropped, so the pch gets
# the same curated prefix as host with no further investment
cg.add_platformio_option("extra_scripts", pch_extra_scripts())
if pch_enabled():
cg.add_platformio_option("build_src_flags", f"-include {PCH_PREFIX_HEADER}")
cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION])
cg.add_platformio_option("board", config[CONF_BOARD])
cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE])
@@ -2734,7 +2719,13 @@ async def to_code(config):
_configure_lwip_max_sockets(conf)
if advanced[CONF_EXECUTE_FROM_PSRAM]:
add_idf_sdkconfig_option("CONFIG_SPIRAM_XIP_FROM_PSRAM", True)
if variant == VARIANT_ESP32S3:
add_idf_sdkconfig_option("CONFIG_SPIRAM_FETCH_INSTRUCTIONS", True)
add_idf_sdkconfig_option("CONFIG_SPIRAM_RODATA", True)
elif variant == VARIANT_ESP32P4:
add_idf_sdkconfig_option("CONFIG_SPIRAM_XIP_FROM_PSRAM", True)
else:
raise ValueError("Unhandled ESP32 variant")
# Apply LWIP core locking for better socket performance
# This is already enabled by default in Arduino framework, where it provides
@@ -3250,13 +3241,7 @@ def _write_sdkconfig():
if write_file_if_changed(internal_path, contents):
# internal changed, update real one
write_file_if_changed(sdk_path, contents)
if not CORE.using_toolchain_esp_idf:
# PIO's dependency tracking under-declares sdkconfig inputs
# (ldgen, linker scripts); without a clean the image can be
# unbootable (esphome#15336). The esp-idf toolchain tracks
# sdkconfig via IDF's cmake and has_outdated_files(), so a
# reconfigure suffices there; everything else fails safe.
clean_build(clear_pio_cache=False)
clean_build(clear_pio_cache=False)
def _write_idf_component_yml():
@@ -3372,6 +3357,8 @@ def _write_idf_component_yml():
def copy_files():
_write_sdkconfig()
_write_idf_component_yml()
if not CORE.using_toolchain_esp_idf:
copy_pch_script()
if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]:
flash_size = CORE.data[KEY_ESP32][KEY_FLASH_SIZE]
+4 -13
View File
@@ -5,14 +5,11 @@ import esphome.config_validation as cv
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA
from esphome.pins import check_strapping_pin
# Per the ESP32-S31 IDF DOCS and datasheet:
# https://docs.espressif.com/projects/esp-idf/en/v6.1/esp32s31/api-reference/peripherals/gpio.html
# Per the ESP32-S31 datasheet (page 96):
# https://documentation.espressif.com/esp32-s31_datasheet_en.pdf
_ESP32S31_SPI_FLASH_PINS: set[int] = {26, 27, 28, 30, 31, 32}
_ESP32S31_INVALID_PINS: set[int] = {29, 41}
# GPIO60/GPIO61 set the boot mode; GPIO37 selects the JTAG signal source;
# GPIO36 sets the VDD_SPI voltage.
_ESP32S31_STRAPPING_PINS: set[int] = {36, 37, 60, 61}
_ESP32S31_SPI_FLASH_PINS: set[int] = {27, 28, 29, 31, 32, 33}
# GPIO60/GPIO61 set the boot mode; GPIO37 selects the JTAG signal source.
_ESP32S31_STRAPPING_PINS: set[int] = {37, 60, 61}
# LP I2C is fixed to GPIO6 (SCL) / GPIO7 (SDA) per the datasheet IO MUX table.
_ESP32S31_I2C_LP_PINS = {"SDA": 7, "SCL": 6}
@@ -22,8 +19,6 @@ _LOGGER = logging.getLogger(__name__)
def esp32_s31_validate_gpio_pin(value: int) -> int:
if value < 0 or value > 61:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-61)")
if value in _ESP32S31_INVALID_PINS:
raise cv.Invalid(f"GPIO{value} does not exist on ESP32-S31.")
if value in _ESP32S31_SPI_FLASH_PINS:
raise cv.Invalid(
f"GPIO{value} is reserved for the SPI flash interface on ESP32-S31 and cannot be used."
@@ -38,10 +33,6 @@ def esp32_s31_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
if num < 0 or num > 61:
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-61)")
# Checked here as well so ignore_pin_validation_error cannot bypass it;
# these pins are not bonded and can never work
if num in _ESP32S31_INVALID_PINS:
raise cv.Invalid(f"GPIO{num} does not exist on ESP32-S31.")
if is_input:
# All ESP32 pins support input mode
pass
@@ -210,9 +210,8 @@ esp_err_t CameraWebServer::streaming_handler_(struct httpd_req *req) {
if (!image) {
// A shutdown is not a lost frame: wait_for_image_() returns empty as soon
// as running_ clears, and the loop condition below ends the stream anyway.
if (this->running_) {
if (this->running_)
ESP_LOGW(TAG, "STREAM: failed to acquire frame");
}
res = ESP_FAIL;
}
if (res == ESP_OK) {
+40 -17
View File
@@ -39,7 +39,7 @@ from esphome.platformio.toolchain import copy_ccache_script, copy_pch_script
from esphome.storage_json import StorageJSON
from esphome.types import ConfigType
from .boards import BOARDS, ESP8266_BOARD_BUILD, board_ld_script
from .boards import BOARDS, ESP8266_BOARD_BUILD, ESP8266_LD_SCRIPTS, board_ld_script
from .const import (
BUILD_FLASH_MODES,
CONF_EARLY_PIN_INIT,
@@ -48,6 +48,8 @@ from .const import (
CONF_RESTORE_FROM_FLASH,
KEY_BOARD,
KEY_ESP8266,
KEY_FLASH_SIZE,
KEY_LDSCRIPT,
KEY_PIN_INITIAL_STATES,
KEY_SCANF_FLOAT,
KEY_SERIAL1_REQUIRED,
@@ -184,6 +186,10 @@ def _format_framework_arduino_version(ver: cv.Version) -> str:
# format the given arduino (https://github.com/esp8266/Arduino/releases) version to
# a PIO platformio/framework-arduinoespressif8266 value
# List of package versions: https://api.registry.platformio.org/v3/packages/platformio/tool/framework-arduinoespressif8266
if ver <= cv.Version(2, 4, 1):
return f"~1.{ver.major}{ver.minor:02d}{ver.patch:02d}.0"
if ver <= cv.Version(2, 6, 2):
return f"~2.{ver.major}{ver.minor:02d}{ver.patch:02d}.0"
# Same encoding the native toolchain uses for its package download, so a
# custom-source check against this value cannot drift from what it fetches.
from esphome.arduino8266.framework import framework_package_version
@@ -206,9 +212,11 @@ def _format_framework_arduino_version(ver: cv.Version) -> str:
# - https://github.com/esp8266/Arduino/releases
# - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-arduinoespressif8266
RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 1, 2)
# The platformio/espressif8266 version to use for arduino 3 framework versions
# The platformio/espressif8266 version to use for arduino 2 framework versions
# - https://github.com/platformio/platform-espressif8266/releases
# - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif8266
ARDUINO_2_PLATFORM_VERSION = cv.Version(2, 6, 3)
# for arduino 3 framework versions
ARDUINO_3_PLATFORM_VERSION = cv.Version(3, 2, 0)
# for arduino 4 framework versions
ARDUINO_4_PLATFORM_VERSION = cv.Version(4, 2, 1)
@@ -233,14 +241,6 @@ def _arduino_check_versions(value: ConfigType) -> ConfigType:
version = cv.Version.parse(cv.version_number(value[CONF_VERSION]))
source = value.get(CONF_SOURCE, None)
if version < cv.Version(3, 0, 0):
raise cv.Invalid(
f"Arduino framework {version} is no longer supported; ESPHome requires "
f"C++20, which needs Arduino core 3.x. Use the recommended version "
f"({RECOMMENDED_ARDUINO_FRAMEWORK_VERSION}).",
path=[CONF_VERSION],
)
value[CONF_VERSION] = str(version)
value[CONF_SOURCE] = source or _format_framework_arduino_version(version)
@@ -248,8 +248,12 @@ def _arduino_check_versions(value: ConfigType) -> ConfigType:
if platform_version is None:
if version >= cv.Version(3, 1, 0):
platform_version = _ARDUINO_4_PLATFORM_SPEC
else:
elif version >= cv.Version(3, 0, 0):
platform_version = _parse_platform_version(str(ARDUINO_3_PLATFORM_VERSION))
elif version >= cv.Version(2, 5, 0):
platform_version = _parse_platform_version(str(ARDUINO_2_PLATFORM_VERSION))
else:
platform_version = _parse_platform_version(str(cv.Version(1, 8, 0)))
value[CONF_PLATFORM_VERSION] = platform_version
if version != RECOMMENDED_ARDUINO_FRAMEWORK_VERSION:
@@ -356,11 +360,29 @@ def check_rosetta() -> None:
)
def _choose_ld_script(board: str) -> str:
"""The flash ld to pin for this board."""
def _choose_ld_script(board: str, ver: cv.Version) -> str | None:
"""The flash ld to pin for this board and core, or None for cores
without ld-script support."""
board_data = BOARDS[board]
ld_scripts = ESP8266_LD_SCRIPTS[board_data[KEY_FLASH_SIZE]]
if ver <= cv.Version(2, 3, 0):
# No ld script support
return None
if ver <= cv.Version(2, 4, 2):
# Old ld script path; the modern per-board override names do not
# exist in this core's SDK, so the override cannot be honored.
# Substituting the size default would move _FS_end and the
# preferences sector, wiping flash-backed state on flash.
if KEY_LDSCRIPT in board_data:
raise EsphomeError(
f"Board {board} requires its {board_data[KEY_LDSCRIPT]} "
f"flash layout, which Arduino core {ver} cannot honor; "
"use a core newer than 2.4.2"
)
return ld_scripts[0]
# A per-board override preserves a layout the board shipped with
# (see d1_wroom_02 in boards.py)
return board_ld_script(BOARDS[board])
return board_ld_script(board_data)
@coroutine_with_priority(CoroPriority.PLATFORM)
@@ -493,9 +515,10 @@ async def to_code(config: ConfigType) -> None:
)
if use_platformio and config[CONF_BOARD] in BOARDS:
cg.add_platformio_option(
"board_build.ldscript", _choose_ld_script(config[CONF_BOARD])
)
ld_script = _choose_ld_script(config[CONF_BOARD], ver)
if ld_script is not None:
cg.add_platformio_option("board_build.ldscript", ld_script)
CORE.add_job(add_pin_initial_states_array)
CORE.add_job(finalize_waveform_config)
+2 -133
View File
@@ -1,20 +1,12 @@
import logging
import esphome.codegen as cg
from esphome.components.noise import (
decode_encryption_key,
encryption_schema,
is_reserved_key,
)
from esphome.components.ota import BASE_OTA_SCHEMA, OTAComponent, ota_to_code
from esphome.config_helpers import merge_config
import esphome.config_validation as cv
from esphome.const import (
CONF_API,
CONF_ENCRYPTION,
CONF_ESPHOME,
CONF_ID,
CONF_KEY,
CONF_NUM_ATTEMPTS,
CONF_OTA,
CONF_PASSWORD,
@@ -23,7 +15,6 @@ from esphome.const import (
CONF_REBOOT_TIMEOUT,
CONF_SAFE_MODE,
CONF_VERSION,
CONF_WEB_SERVER,
)
from esphome.core import CORE, coroutine_with_priority
from esphome.coroutine import CoroPriority
@@ -31,7 +22,6 @@ import esphome.final_validate as fv
from esphome.types import ConfigType
CONF_ALLOW_PARTITION_ACCESS = "allow_partition_access"
CONF_CAPTIVE_PORTAL = "captive_portal"
_LOGGER = logging.getLogger(__name__)
@@ -40,15 +30,7 @@ CODEOWNERS = ["@esphome/core"]
DEPENDENCIES = ["network"]
def AUTO_LOAD(config: ConfigType) -> list[str]:
"""Auto-load noise only when encryption is configured."""
base = ["sha256", "socket"]
# A falsy config is a tooling probe for the maximal set (None from
# dependency resolution, {} from the components-graph platform probe);
# a validated config always carries defaults, never empty
if not config or CONF_ENCRYPTION in config:
return base + ["noise"]
return base
AUTO_LOAD = ["sha256", "socket"]
esphome = cg.esphome_ns.namespace("esphome")
@@ -85,24 +67,11 @@ def ota_esphome_final_validate(config: ConfigType) -> None:
CONF_PASSWORD in merged_ota_esphome_configs_by_port[conf_port]
and CONF_PASSWORD in ota_conf
and merged_ota_esphome_configs_by_port[conf_port][CONF_PASSWORD]
!= ota_conf[CONF_PASSWORD]
!= ota_conf.get(CONF_PASSWORD)
):
raise cv.Invalid(
f"Found multiple configurations but {CONF_PASSWORD} is inconsistent"
)
# Encryption blocks conflict only when both pin a key; a bare
# `encryption:` (a package/device split) is compatible with a
# keyed one, and merge_config yields the keyed result
merged_key = (
merged_ota_esphome_configs_by_port[conf_port]
.get(CONF_ENCRYPTION, {})
.get(CONF_KEY)
)
other_key = ota_conf.get(CONF_ENCRYPTION, {}).get(CONF_KEY)
if merged_key and other_key and merged_key != other_key:
raise cv.Invalid(
f"Found multiple configurations but {CONF_ENCRYPTION} is inconsistent"
)
ports_with_merged_configs.append(conf_port)
merged_ota_esphome_configs_by_port[conf_port] = merge_config(
@@ -125,20 +94,6 @@ def ota_esphome_final_validate(config: ConfigType) -> None:
new_ota_conf.extend(merged_ota_esphome_configs_by_port.values())
api_conf = full_conf.get(CONF_API) or {}
for ota_conf in merged_ota_esphome_configs_by_port.values():
# Merging same-port blocks can combine a password from one block with
# encryption from another; re-check the exclusion on the merged result.
_validate_no_password_with_encryption(ota_conf)
if (encryption_conf := ota_conf.get(CONF_ENCRYPTION)) is not None:
_resolve_encryption_key(encryption_conf, api_conf)
if any(
conf.get(CONF_PLATFORM) == CONF_WEB_SERVER for conf in full_ota_conf
) and any(
CONF_ENCRYPTION in conf for conf in merged_ota_esphome_configs_by_port.values()
):
_warn_web_server_ota(full_conf)
full_conf[CONF_OTA] = new_ota_conf
fv.full_config.set(full_conf)
@@ -152,73 +107,6 @@ def ota_esphome_final_validate(config: ConfigType) -> None:
)
def _warn_web_server_ota(full_conf: ConfigType) -> None:
"""The web_server ota platform accepts the same image over plaintext HTTP
with basic auth, bypassing the encryption; warn rather than fail so the
operator keeps the recovery path."""
if CONF_CAPTIVE_PORTAL in full_conf and CONF_WEB_SERVER not in full_conf:
# The captive_portal auto-load: the endpoint only exists while the
# fallback AP is active
_LOGGER.warning(
"OTA encryption does not cover the %s OTA platform (auto-loaded "
"by captive_portal); the plaintext /update endpoint stays "
"reachable while the fallback AP is active",
CONF_WEB_SERVER,
)
else:
_LOGGER.warning(
"OTA encryption does not cover the %s OTA platform; its "
"plaintext /update endpoint accepts the same image",
CONF_WEB_SERVER,
)
def _resolve_encryption_key(encryption_conf: ConfigType, api_conf: ConfigType) -> None:
"""Resolve the one encryption key per device into the ota block.
An explicit ota key must match the api key, a bare block inherits it,
a runtime provisioned api key cannot be inherited, and the all-zeros
provisioning sentinel is rejected (the device treats it as no key).
"""
api_key = api_conf.get(CONF_ENCRYPTION, {}).get(CONF_KEY)
if ota_key := encryption_conf.get(CONF_KEY):
if api_key and ota_key != api_key:
raise cv.Invalid(
f"'{CONF_OTA}' {CONF_ENCRYPTION} {CONF_KEY} must match the "
f"'{CONF_API}' {CONF_ENCRYPTION} {CONF_KEY}; omit the "
f"'{CONF_OTA}' {CONF_KEY} to use the '{CONF_API}' one"
)
elif not api_key:
if CONF_ENCRYPTION in api_conf:
raise cv.Invalid(
f"the '{CONF_API}' {CONF_ENCRYPTION} {CONF_KEY} is provisioned at "
f"runtime and cannot be inherited at build time; set an explicit "
f"'{CONF_OTA}' {CONF_ENCRYPTION} {CONF_KEY}"
)
raise cv.Invalid(
f"'{CONF_OTA}' {CONF_ENCRYPTION} has no {CONF_KEY} and there is no "
f"'{CONF_API}' {CONF_ENCRYPTION} {CONF_KEY} to inherit; set one of them"
)
else:
encryption_conf[CONF_KEY] = api_key
if is_reserved_key(encryption_conf[CONF_KEY]):
raise cv.Invalid(
f"The all-zeros {CONF_KEY} is reserved and provides no protection; "
f"generate a real key with: openssl rand -base64 32"
)
# Also called on merged same-port configs in final validate, where schemas
# do not run
def _validate_no_password_with_encryption(config: ConfigType) -> ConfigType:
if CONF_PASSWORD in config and CONF_ENCRYPTION in config:
raise cv.Invalid(
f"'{CONF_PASSWORD}' cannot be combined with '{CONF_ENCRYPTION}'; the "
f"encryption key already authenticates the uploader, remove '{CONF_PASSWORD}'"
)
return config
def _consume_ota_sockets(config: ConfigType) -> ConfigType:
"""Register socket needs for OTA component."""
from esphome.components import socket
@@ -246,7 +134,6 @@ CONFIG_SCHEMA = cv.All(
): cv.port,
cv.Optional(CONF_ALLOW_PARTITION_ACCESS, default=False): cv.boolean,
cv.Optional(CONF_PASSWORD): cv.sensitive(),
cv.Optional(CONF_ENCRYPTION): encryption_schema,
cv.Optional(CONF_NUM_ATTEMPTS): cv.invalid(
f"'{CONF_SAFE_MODE}' (and its related configuration variables) has moved from 'ota' to its own component. See https://esphome.io/components/safe_mode"
),
@@ -260,24 +147,12 @@ CONFIG_SCHEMA = cv.All(
)
.extend(BASE_OTA_SCHEMA)
.extend(cv.COMPONENT_SCHEMA),
_validate_no_password_with_encryption,
_consume_ota_sockets,
)
FINAL_VALIDATE_SCHEMA = ota_esphome_final_validate
def FILTER_SOURCE_FILES() -> list[str]:
"""Filter out the noise transport when no ota entry configures encryption."""
for ota_conf in CORE.config.get(CONF_OTA, []):
if (
ota_conf.get(CONF_PLATFORM) == CONF_ESPHOME
and ota_conf.get(CONF_ENCRYPTION) is not None
):
return []
return ["ota_esphome_noise.cpp"]
@coroutine_with_priority(CoroPriority.OTA_UPDATES)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
@@ -296,12 +171,6 @@ async def to_code(config: ConfigType) -> None:
if config.get(CONF_ALLOW_PARTITION_ACCESS):
cg.add_define("USE_OTA_PARTITIONS")
if (encryption_conf := config.get(CONF_ENCRYPTION)) is not None:
# A missing key was resolved from the api component in final validate.
key = encryption_conf[CONF_KEY]
cg.add_define("USE_OTA_ENCRYPTION")
cg.add(var.set_noise_psk(list(decode_encryption_key(key))))
# Build flag so lwip_fast_select.c (a .c file that can't include defines.h) sees it.
cg.add_build_flag("-DUSE_OTA_PLATFORM_ESPHOME")
+27 -97
View File
@@ -27,6 +27,7 @@ namespace esphome {
static const char *const TAG = "esphome.ota";
static constexpr uint16_t OTA_BLOCK_SIZE = 8192;
static constexpr size_t OTA_BUFFER_SIZE = 1024; // buffer size for OTA data transfer
static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 20000; // milliseconds for initial handshake
static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 90000; // milliseconds for data transfer
@@ -104,11 +105,6 @@ void ESPHomeOTAComponent::dump_config() {
ESP_LOGCONFIG(TAG, " Password configured");
}
#endif
#ifdef USE_OTA_ENCRYPTION
if (this->noise_ctx_.has_psk()) {
ESP_LOGCONFIG(TAG, " Encryption configured");
}
#endif
#ifdef USE_OTA_PARTITIONS
ESP_LOGCONFIG(TAG,
" Partition access allowed\n"
@@ -132,8 +128,7 @@ void ESPHomeOTAComponent::dump_config() {
esp_partition_iterator_release(it);
esp_bootloader_desc_t bootloader_desc;
esp_err_t err = esp_ota_get_bootloader_description(nullptr, &bootloader_desc);
ESP_LOGCONFIG(TAG, " Bootloader: ESP-IDF %s",
(err == ESP_OK) ? bootloader_desc.idf_ver : LOG_STR_LITERAL("version unknown"));
ESP_LOGCONFIG(TAG, " Bootloader: ESP-IDF %s", (err == ESP_OK) ? bootloader_desc.idf_ver : "version unknown");
#endif // USE_ESP32
#endif // USE_OTA_PARTITIONS
}
@@ -153,10 +148,8 @@ void ESPHomeOTAComponent::loop() {
static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_COMPRESSION = 0x01;
static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_SHA256_AUTH = 0x02;
static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL = 0x04;
static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_NOISE = 0x08;
static constexpr uint8_t SERVER_FEATURE_SUPPORTS_COMPRESSION = 0x01;
static constexpr uint8_t SERVER_FEATURE_SUPPORTS_PARTITION_ACCESS = 0x02;
static constexpr uint8_t SERVER_FEATURE_SUPPORTS_NOISE = 0x04;
void ESPHomeOTAComponent::handle_handshake_() {
/// Handle the OTA handshake and authentication.
@@ -208,7 +201,8 @@ void ESPHomeOTAComponent::handle_handshake_() {
}
// Validate magic bytes
if (memcmp(this->handshake_buf_, MAGIC_BYTES, sizeof(MAGIC_BYTES)) != 0) {
static const uint8_t MAGIC_BYTES[5] = {0x6C, 0x26, 0xF7, 0x5C, 0x45};
if (memcmp(this->handshake_buf_, MAGIC_BYTES, 5) != 0) {
ESP_LOGW(TAG, "Magic bytes mismatch! 0x%02X-0x%02X-0x%02X-0x%02X-0x%02X", this->handshake_buf_[0],
this->handshake_buf_[1], this->handshake_buf_[2], this->handshake_buf_[3], this->handshake_buf_[4]);
this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_MAGIC);
@@ -240,19 +234,6 @@ void ESPHomeOTAComponent::handle_handshake_() {
}
this->ota_features_ = this->handshake_buf_[0];
ESP_LOGV(TAG, "Features: 0x%02X", this->ota_features_);
#ifdef USE_OTA_ENCRYPTION
// Fail closed: with a PSK configured the client must negotiate encryption
// (which requires the extended protocol); refuse plaintext uploads.
static constexpr uint8_t NOISE_REQUIRED_FEATURES =
CLIENT_FEATURE_SUPPORTS_NOISE | CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL;
if (this->noise_ctx_.has_psk() && (this->ota_features_ & NOISE_REQUIRED_FEATURES) != NOISE_REQUIRED_FEATURES) {
ESP_LOGW(TAG, "Client does not support encryption");
this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_ENCRYPTION_REQUIRED);
return;
}
#endif
this->transition_ota_state_(OTAState::FEATURE_ACK);
const bool supports_compression =
@@ -268,11 +249,6 @@ void ESPHomeOTAComponent::handle_handshake_() {
this->handshake_buf_[1] = (supports_compression ? SERVER_FEATURE_SUPPORTS_COMPRESSION : 0);
#ifdef USE_OTA_PARTITIONS
this->handshake_buf_[1] |= SERVER_FEATURE_SUPPORTS_PARTITION_ACCESS;
#endif
#ifdef USE_OTA_ENCRYPTION
if (this->noise_ctx_.has_psk()) {
this->handshake_buf_[1] |= SERVER_FEATURE_SUPPORTS_NOISE;
}
#endif
} else {
this->handshake_buf_[0] =
@@ -288,20 +264,6 @@ void ESPHomeOTAComponent::handle_handshake_() {
if (!this->try_write_(ack_size, LOG_STR("ack feature"))) {
return;
}
#ifdef USE_OTA_ENCRYPTION
// With a PSK configured the rest of the session runs inside the noise
// transport; the client sends the first handshake frame next, so there
// is nothing to do until data arrives.
if (this->noise_ctx_.has_psk()) {
// handshake_buf_ still holds the feature ack composed above; a
// would-block re-entry lands here without rebuilding it
if (!this->noise_start_session_(this->handshake_buf_[1])) {
return;
}
this->transition_ota_state_(OTAState::NOISE_HANDSHAKE);
return;
}
#endif
#ifdef USE_OTA_PASSWORD
// If password is set, move to auth phase
if (!this->password_.empty()) {
@@ -339,16 +301,6 @@ void ESPHomeOTAComponent::handle_handshake_() {
this->handle_data_();
return;
#ifdef USE_OTA_ENCRYPTION
case OTAState::NOISE_HANDSHAKE:
if (!this->handle_noise_handshake_()) {
return;
}
this->transition_ota_state_(OTAState::DATA);
this->handle_data_();
return;
#endif
default:
break;
}
@@ -387,8 +339,6 @@ void ESPHomeOTAComponent::handle_data_() {
/// Raw TCP (8266, RP2040): setblocking is no-op; SO_RCVTIMEO uses
/// wakeable_delay() in read();
/// write() always returns immediately
// Backend calls overwrite this with OK; reset to UNKNOWN before any
// goto error that follows a successful begin()/write()
ota::OTAResponseTypes error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN;
size_t total = 0;
uint32_t last_progress = 0;
@@ -410,11 +360,11 @@ void ESPHomeOTAComponent::handle_data_() {
this->client_->setblocking(true);
// Acknowledge auth OK - 1 byte
this->data_write_byte_(ota::OTA_RESPONSE_AUTH_OK);
this->write_byte_(ota::OTA_RESPONSE_AUTH_OK);
if (this->extended_proto_) {
// Read ota type, 1 byte
if (!this->data_readall_(buf, 1)) {
if (!this->readall_(buf, 1)) {
this->log_read_error_(LOG_STR("OTA type"));
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
}
@@ -423,7 +373,7 @@ void ESPHomeOTAComponent::handle_data_() {
ESP_LOGV(TAG, "OTA type is 0x%02x", ota_type);
// Read size, 4 bytes MSB first
if (!this->data_readall_(buf, 4)) {
if (!this->readall_(buf, 4)) {
this->log_read_error_(LOG_STR("size"));
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
}
@@ -454,12 +404,11 @@ void ESPHomeOTAComponent::handle_data_() {
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
// Acknowledge prepare OK - 1 byte
this->data_write_byte_(ota::OTA_RESPONSE_UPDATE_PREPARE_OK);
this->write_byte_(ota::OTA_RESPONSE_UPDATE_PREPARE_OK);
// Read binary MD5, 32 bytes
if (!this->data_readall_(buf, 32)) {
if (!this->readall_(buf, 32)) {
this->log_read_error_(LOG_STR("MD5 checksum"));
error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN;
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
}
sbuf[32] = '\0';
@@ -467,7 +416,7 @@ void ESPHomeOTAComponent::handle_data_() {
this->backend_->set_update_md5(sbuf);
// Acknowledge MD5 OK - 1 byte
this->data_write_byte_(ota::OTA_RESPONSE_BIN_MD5_OK);
this->write_byte_(ota::OTA_RESPONSE_BIN_MD5_OK);
// Track when we last received data so a silently-vanished peer (no FIN/RST
// delivered, e.g. uploader killed mid-transfer or NAT/router dropped state)
@@ -483,35 +432,19 @@ void ESPHomeOTAComponent::handle_data_() {
}
size_t remaining = ota_size - total;
size_t requested = remaining < OTA_BUFFER_SIZE ? remaining : OTA_BUFFER_SIZE;
ssize_t read;
#ifdef USE_OTA_ENCRYPTION
if (this->noise_ != nullptr) {
// One frame per call; noise_read_data_ waits internally (readall_), so
// there is no would-block retry here and failures are already logged.
read = this->noise_read_data_(buf, requested);
if (read <= 0) {
error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN;
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
}
} else
#endif
{
read = this->client_->read(buf, requested);
if (read == -1) {
const int err = errno;
if (this->would_block_(err)) {
// read() already waited up to SO_RCVTIMEO for data, just feed WDT
App.feed_wdt();
continue;
}
ESP_LOGW(TAG, "Read err %d", err);
error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN;
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
} else if (read == 0) {
ESP_LOGW(TAG, "Remote closed");
error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN;
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
ssize_t read = this->client_->read(buf, requested);
if (read == -1) {
const int err = errno;
if (this->would_block_(err)) {
// read() already waited up to SO_RCVTIMEO for data, just feed WDT
App.feed_wdt();
continue;
}
ESP_LOGW(TAG, "Read err %d", err);
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
} else if (read == 0) {
ESP_LOGW(TAG, "Remote closed");
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
}
last_data_ms = millis();
@@ -523,7 +456,7 @@ void ESPHomeOTAComponent::handle_data_() {
total += read;
#if USE_OTA_VERSION == 2
while (size_acknowledged + OTA_BLOCK_SIZE <= total || (total == ota_size && size_acknowledged < ota_size)) {
this->data_write_byte_(ota::OTA_RESPONSE_CHUNK_OK);
this->write_byte_(ota::OTA_RESPONSE_CHUNK_OK);
size_acknowledged += OTA_BLOCK_SIZE;
}
#endif
@@ -542,7 +475,7 @@ void ESPHomeOTAComponent::handle_data_() {
}
// Acknowledge receive OK - 1 byte
this->data_write_byte_(ota::OTA_RESPONSE_RECEIVE_OK);
this->write_byte_(ota::OTA_RESPONSE_RECEIVE_OK);
error_code = this->backend_->end();
if (error_code != ota::OTA_RESPONSE_OK) {
@@ -551,10 +484,10 @@ void ESPHomeOTAComponent::handle_data_() {
}
// Acknowledge Update end OK - 1 byte
this->data_write_byte_(ota::OTA_RESPONSE_UPDATE_END_OK);
this->write_byte_(ota::OTA_RESPONSE_UPDATE_END_OK);
// Read ACK
if (!this->data_readall_(buf, 1) || buf[0] != ota::OTA_RESPONSE_OK) {
if (!this->readall_(buf, 1) || buf[0] != ota::OTA_RESPONSE_OK) {
this->log_read_error_(LOG_STR("ack"));
// do not go to error, this is not fatal
}
@@ -577,7 +510,7 @@ void ESPHomeOTAComponent::handle_data_() {
App.safe_reboot();
error:
this->data_write_byte_(static_cast<uint8_t>(error_code));
this->write_byte_(static_cast<uint8_t>(error_code));
// Abort backend before cleanup - cleanup_connection_() destroys the backend.
// Always call abort() unconditionally: backends register external partitions before
@@ -744,9 +677,6 @@ void ESPHomeOTAComponent::cleanup_connection_() {
this->backend_ = nullptr;
#ifdef USE_OTA_PASSWORD
this->cleanup_auth_();
#endif
#ifdef USE_OTA_ENCRYPTION
this->noise_ = nullptr;
#endif
// Intentionally no disable_loop() — letting loop() run one more iteration catches
// any connection that queued on the listener mid-session (otherwise the wake flag,
+1 -69
View File
@@ -4,9 +4,6 @@
#ifdef USE_OTA
#include "esphome/components/ota/ota_backend_factory.h"
#include "esphome/components/socket/socket.h"
#ifdef USE_OTA_ENCRYPTION
#include "esphome/components/noise/noise_handshake.h"
#endif
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/preferences.h"
@@ -27,10 +24,7 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
AUTH_SEND, // Sending authentication request
AUTH_READ, // Reading authentication data
#endif // USE_OTA_PASSWORD
#ifdef USE_OTA_ENCRYPTION
NOISE_HANDSHAKE, // Exchanging Noise handshake frames
#endif
DATA, // BLOCKING! Processing OTA data (update, etc.)
DATA, // BLOCKING! Processing OTA data (update, etc.)
};
#ifdef USE_OTA_PASSWORD
void set_auth_password(const std::string &password) { password_ = password; }
@@ -44,10 +38,6 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
}
#endif // USE_OTA_PASSWORD
#ifdef USE_OTA_ENCRYPTION
void set_noise_psk(noise::psk_t psk) { this->noise_ctx_.set_psk(psk); }
#endif
/// Manually set the port OTA should listen on
void set_port(uint16_t port) { this->port_ = port; }
@@ -73,48 +63,6 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
bool writeall_(const uint8_t *buf, size_t len);
inline bool write_byte_(uint8_t byte) { return this->writeall_(&byte, 1); }
#ifdef USE_OTA_ENCRYPTION
// Heap-allocated only while an encrypted OTA session is active.
struct NoiseSession {
~NoiseSession();
noise::NoiseResponderHandshake handshake;
NoiseCipherState *send_cipher{nullptr};
NoiseCipherState *recv_cipher{nullptr};
uint16_t frame_len{0}; // total frame size once the header is parsed, 0 until then
uint16_t frame_pos{0}; // bytes read or written so far
bool writing{false}; // a produced handshake frame is still being flushed
uint8_t frame_buf[noise::FRAME_HEADER_SIZE + 1 + noise::MAX_HANDSHAKE_SIZE];
};
bool noise_start_session_(uint8_t server_feature_flags);
bool handle_noise_handshake_();
bool noise_try_read_frame_();
bool noise_try_write_frame_();
void noise_send_reject_(const LogString *reason);
ssize_t noise_decrypt_(uint8_t *buf, size_t len);
ssize_t noise_read_frame_blocking_(uint8_t *buf, size_t min_ciphertext, size_t max_ciphertext);
bool noise_readall_(uint8_t *buf, size_t len);
ssize_t noise_read_data_(uint8_t *buf, size_t capacity);
bool noise_write_byte_(uint8_t byte);
#endif // USE_OTA_ENCRYPTION
// Data-phase I/O dispatch: through the noise transport when a session is
// active, straight to the socket otherwise.
inline bool data_write_byte_(uint8_t byte) {
#ifdef USE_OTA_ENCRYPTION
if (this->noise_ != nullptr)
return this->noise_write_byte_(byte);
#endif
return this->write_byte_(byte);
}
// When encrypted, buf must have room for len + noise::MAC_SIZE bytes.
inline bool data_readall_(uint8_t *buf, size_t len) {
#ifdef USE_OTA_ENCRYPTION
if (this->noise_ != nullptr)
return this->noise_readall_(buf, len);
#endif
return this->readall_(buf, len);
}
bool try_read_(size_t to_read, const LogString *desc);
bool try_write_(size_t to_write, const LogString *desc);
@@ -143,10 +91,6 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
std::string password_;
std::unique_ptr<uint8_t[]> auth_buf_;
#endif // USE_OTA_PASSWORD
#ifdef USE_OTA_ENCRYPTION
noise::NoiseContext noise_ctx_;
std::unique_ptr<NoiseSession> noise_;
#endif // USE_OTA_ENCRYPTION
socket::ListenSocket *server_{nullptr};
std::unique_ptr<socket::Socket> client_;
@@ -154,18 +98,6 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
uint32_t client_connect_time_{0};
static constexpr size_t HANDSHAKE_BUF_SIZE = 5;
// Buffer size for OTA data transfer. The upload client derives its maximum
// encrypted frame plaintext from this (espota2.NOISE_MAX_PLAINTEXT is this
// minus the 16-byte MAC); both must change together.
static constexpr size_t OTA_BUFFER_SIZE = 1040;
#ifdef USE_OTA_ENCRYPTION
// espota2.NOISE_MAX_PLAINTEXT; shrinking the buffer would reject every
// frame a current CLI sends
static constexpr size_t NOISE_CLIENT_MAX_PLAINTEXT = 1024;
static_assert(OTA_BUFFER_SIZE >= NOISE_CLIENT_MAX_PLAINTEXT + noise::MAC_SIZE,
"OTA_BUFFER_SIZE must fit a full encrypted data frame");
#endif
static constexpr uint8_t MAGIC_BYTES[5] = {0x6C, 0x26, 0xF7, 0x5C, 0x45};
#ifdef USE_OTA_PARTITIONS
uint32_t running_app_offset_{0};
size_t running_app_size_{0};
@@ -1,279 +0,0 @@
#include "ota_esphome.h"
#ifdef USE_OTA
#ifdef USE_OTA_ENCRYPTION
#include "esphome/components/noise/noise.h"
#include "esphome/components/ota/ota_backend.h"
#include "esphome/core/log.h"
#include <cstring>
#include <new>
#ifdef USE_ESP8266
#include <pgmspace.h>
#endif
namespace esphome {
static const char *const TAG = "esphome.ota";
#ifdef USE_ESP8266
static constexpr char OTA_NOISE_PROLOGUE_INIT[] PROGMEM = "NoiseOTAInit";
#else
static constexpr char OTA_NOISE_PROLOGUE_INIT[] = "NoiseOTAInit";
#endif
static constexpr size_t OTA_NOISE_PROLOGUE_INIT_LEN = sizeof(OTA_NOISE_PROLOGUE_INIT) - 1;
ESPHomeOTAComponent::NoiseSession::~NoiseSession() {
if (this->send_cipher != nullptr) {
noise_cipherstate_free(this->send_cipher);
}
if (this->recv_cipher != nullptr) {
noise_cipherstate_free(this->recv_cipher);
}
}
/** Allocate the session and start the responder handshake.
*
* The prologue binds the whole plaintext preamble, so any tampering with the
* negotiation (a stripped feature flag, a changed version) breaks the first
* handshake MAC on either side:
* "NoiseOTAInit" | magic(5) | OK,version | client_features | FEATURE_FLAGS,server_flags
*/
bool ESPHomeOTAComponent::noise_start_session_(uint8_t server_feature_flags) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks)
this->noise_ = std::unique_ptr<NoiseSession>(new (std::nothrow) NoiseSession());
if (this->noise_ == nullptr) {
ESP_LOGW(TAG, "Session allocation failed");
this->cleanup_connection_();
return false;
}
static constexpr size_t PROLOGUE_ACK_LEN = 2; // OTA_RESPONSE_OK + version
static constexpr size_t PROLOGUE_CLIENT_FEATURES_LEN = 1;
static constexpr size_t PROLOGUE_FEATURE_ACK_LEN = 2; // OTA_RESPONSE_FEATURE_FLAGS + server flags
uint8_t prologue[OTA_NOISE_PROLOGUE_INIT_LEN + sizeof(MAGIC_BYTES) + PROLOGUE_ACK_LEN + PROLOGUE_CLIENT_FEATURES_LEN +
PROLOGUE_FEATURE_ACK_LEN];
#ifdef USE_ESP8266
memcpy_P(prologue, OTA_NOISE_PROLOGUE_INIT, OTA_NOISE_PROLOGUE_INIT_LEN);
#else
std::memcpy(prologue, OTA_NOISE_PROLOGUE_INIT, OTA_NOISE_PROLOGUE_INIT_LEN);
#endif
uint8_t *p = prologue + OTA_NOISE_PROLOGUE_INIT_LEN;
// Magic bytes, already validated in MAGIC_READ
std::memcpy(p, MAGIC_BYTES, sizeof(MAGIC_BYTES));
p += sizeof(MAGIC_BYTES);
// Our magic ack
*p++ = ota::OTA_RESPONSE_OK;
*p++ = USE_OTA_VERSION;
// The feature byte the client sent
*p++ = this->ota_features_;
// The feature ack we sent (noise requires the extended protocol)
*p++ = ota::OTA_RESPONSE_FEATURE_FLAGS;
*p++ = server_feature_flags;
int err = this->noise_->handshake.init(this->noise_ctx_.get_psk(), prologue, sizeof(prologue));
if (err != 0) {
ESP_LOGW(TAG, "Handshake init: %s", LOG_STR_ARG(noise::noise_err_to_logstr(err)));
this->cleanup_connection_();
return false;
}
return true;
}
/** Drive the non-blocking handshake from loop(); returns true once the
* transport ciphers are ready. A would-block returns false and the next
* loop() resumes from the NoiseSession cursors; on failure the connection
* is cleaned up.
*/
bool ESPHomeOTAComponent::handle_noise_handshake_() {
NoiseSession &s = *this->noise_;
while (true) {
if (s.writing) {
if (!this->noise_try_write_frame_()) {
return false; // would block, or errored and cleaned up
}
s.writing = false;
s.frame_pos = 0;
s.frame_len = 0;
}
switch (s.handshake.action()) {
case noise::NoiseResponderHandshake::Action::ACTION_READ: {
if (!this->noise_try_read_frame_()) {
return false;
}
const uint16_t payload_len = s.frame_len - noise::FRAME_HEADER_SIZE;
s.frame_pos = 0;
s.frame_len = 0;
if (s.frame_buf[noise::FRAME_HEADER_SIZE] != noise::HANDSHAKE_STATUS_OK) {
ESP_LOGW(TAG, "Bad handshake error byte: %u", s.frame_buf[noise::FRAME_HEADER_SIZE]);
this->cleanup_connection_();
return false;
}
int err = s.handshake.read_message(s.frame_buf + noise::FRAME_HEADER_SIZE + 1, payload_len - 1);
if (err != 0) {
ESP_LOGW(TAG, "Handshake read: %s", LOG_STR_ARG(noise::noise_err_to_logstr(err)));
this->noise_send_reject_(noise::reject_reason_for(err));
this->cleanup_connection_();
return false;
}
break;
}
case noise::NoiseResponderHandshake::Action::ACTION_WRITE: {
size_t msg_len = 0;
int err =
s.handshake.write_message(s.frame_buf + noise::FRAME_HEADER_SIZE + 1, noise::MAX_HANDSHAKE_SIZE, msg_len);
if (err != 0) {
ESP_LOGW(TAG, "Handshake write: %s", LOG_STR_ARG(noise::noise_err_to_logstr(err)));
this->cleanup_connection_();
return false;
}
const uint16_t payload_len = msg_len + 1;
noise::write_frame_header(s.frame_buf, payload_len);
s.frame_buf[noise::FRAME_HEADER_SIZE] = noise::HANDSHAKE_STATUS_OK;
s.frame_len = noise::FRAME_HEADER_SIZE + payload_len;
s.frame_pos = 0;
s.writing = true;
break;
}
case noise::NoiseResponderHandshake::Action::ACTION_SPLIT: {
int err = s.handshake.split(s.send_cipher, s.recv_cipher);
if (err != 0) {
ESP_LOGW(TAG, "Handshake split: %s", LOG_STR_ARG(noise::noise_err_to_logstr(err)));
this->cleanup_connection_();
return false;
}
ESP_LOGD(TAG, "Noise handshake complete");
return true;
}
default: {
ESP_LOGW(TAG, "Bad handshake state");
this->cleanup_connection_();
return false;
}
}
}
}
/// Non-blocking read of one handshake frame into the session buffer.
bool ESPHomeOTAComponent::noise_try_read_frame_() {
NoiseSession &s = *this->noise_;
while (s.frame_pos < noise::FRAME_HEADER_SIZE) {
ssize_t read = this->client_->read(s.frame_buf + s.frame_pos, noise::FRAME_HEADER_SIZE - s.frame_pos);
if (!this->handle_read_error_(read, LOG_STR("read noise header"))) {
return false;
}
s.frame_pos += read;
}
if (s.frame_len == 0) {
const uint16_t payload_len = encode_uint16(s.frame_buf[1], s.frame_buf[2]);
if (s.frame_buf[0] != noise::FRAME_INDICATOR || payload_len < 1 || payload_len > 1 + noise::MAX_HANDSHAKE_SIZE) {
ESP_LOGW(TAG, "Bad handshake frame: 0x%02X, %u bytes", s.frame_buf[0], payload_len);
this->cleanup_connection_();
return false;
}
s.frame_len = noise::FRAME_HEADER_SIZE + payload_len;
}
while (s.frame_pos < s.frame_len) {
ssize_t read = this->client_->read(s.frame_buf + s.frame_pos, s.frame_len - s.frame_pos);
if (!this->handle_read_error_(read, LOG_STR("read noise frame"))) {
return false;
}
s.frame_pos += read;
}
return true;
}
/// Non-blocking write of the pending session-buffer frame.
bool ESPHomeOTAComponent::noise_try_write_frame_() {
NoiseSession &s = *this->noise_;
while (s.frame_pos < s.frame_len) {
ssize_t written = this->client_->write(s.frame_buf + s.frame_pos, s.frame_len - s.frame_pos);
if (!this->handle_write_error_(written, LOG_STR("write noise frame"))) {
return false;
}
s.frame_pos += written;
}
return true;
}
/// Best-effort explicit reject frame so the client can log a readable reason.
void ESPHomeOTAComponent::noise_send_reject_(const LogString *reason) {
// Every reason here comes from noise::reject_reason_for(), so the exported
// floor is the exact capacity needed
uint8_t data[noise::FRAME_HEADER_SIZE + noise::MAC_FAILURE_PAYLOAD_SIZE];
const size_t payload_len =
noise::format_reject_payload(data + noise::FRAME_HEADER_SIZE, sizeof(data) - noise::FRAME_HEADER_SIZE, reason);
noise::write_frame_header(data, payload_len);
this->client_->write(data, noise::FRAME_HEADER_SIZE + payload_len); // Best effort, non-blocking
}
/// Decrypt a ciphertext in place; returns the plaintext size or -1.
ssize_t ESPHomeOTAComponent::noise_decrypt_(uint8_t *buf, size_t len) {
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_inout(mbuf, buf, len, len);
int err = noise_cipherstate_decrypt(this->noise_->recv_cipher, &mbuf);
if (err != 0) {
ESP_LOGW(TAG, "Decrypt: %s", LOG_STR_ARG(noise::noise_err_to_logstr(err)));
return -1;
}
return mbuf.size;
}
/** Blocking read of one frame whose ciphertext size must be within the given
* bounds, decrypted in place; returns the plaintext size, or -1 on error.
* buf needs max_ciphertext capacity.
*/
ssize_t ESPHomeOTAComponent::noise_read_frame_blocking_(uint8_t *buf, size_t min_ciphertext, size_t max_ciphertext) {
uint8_t header[noise::FRAME_HEADER_SIZE];
if (!this->readall_(header, sizeof(header))) {
return -1;
}
const size_t ciphertext_len = encode_uint16(header[1], header[2]);
if (header[0] != noise::FRAME_INDICATOR || ciphertext_len < min_ciphertext || ciphertext_len > max_ciphertext) {
ESP_LOGW(TAG, "Bad frame: 0x%02X, %zu bytes", header[0], ciphertext_len);
return -1;
}
if (!this->readall_(buf, ciphertext_len)) {
return -1;
}
return this->noise_decrypt_(buf, ciphertext_len);
}
/** Blocking read of one frame whose plaintext must be exactly len bytes
* (control units are one unit per frame). buf needs len + noise::MAC_SIZE
* capacity; the plaintext lands at buf[0..len).
*/
bool ESPHomeOTAComponent::noise_readall_(uint8_t *buf, size_t len) {
return this->noise_read_frame_blocking_(buf, len + noise::MAC_SIZE, len + noise::MAC_SIZE) == (ssize_t) len;
}
/** Blocking read of one data-phase frame, decrypted in place; returns the
* plaintext size, or -1 on error. buf is the OTA_BUFFER_SIZE data buffer.
* The ciphertext must fit that buffer and its plaintext must fit what the
* caller accepts (the remaining image bytes).
*/
ssize_t ESPHomeOTAComponent::noise_read_data_(uint8_t *buf, size_t capacity) {
const size_t max_ciphertext = std::min(capacity + noise::MAC_SIZE, OTA_BUFFER_SIZE);
return this->noise_read_frame_blocking_(buf, noise::MAC_SIZE + 1, max_ciphertext);
}
/// Blocking write of one response byte as an encrypted frame.
bool ESPHomeOTAComponent::noise_write_byte_(uint8_t byte) {
uint8_t frame[noise::FRAME_HEADER_SIZE + 1 + noise::MAC_SIZE];
frame[noise::FRAME_HEADER_SIZE] = byte;
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_inout(mbuf, frame + noise::FRAME_HEADER_SIZE, 1, 1 + noise::MAC_SIZE);
int err = noise_cipherstate_encrypt(this->noise_->send_cipher, &mbuf);
if (err != 0) {
ESP_LOGW(TAG, "Encrypt: %s", LOG_STR_ARG(noise::noise_err_to_logstr(err)));
return false;
}
noise::write_frame_header(frame, mbuf.size);
return this->writeall_(frame, noise::FRAME_HEADER_SIZE + mbuf.size);
}
} // namespace esphome
#endif // USE_OTA_ENCRYPTION
#endif // USE_OTA
-5
View File
@@ -155,11 +155,6 @@ async def to_code(config: ConfigType) -> None:
cg.add_define("USE_ESPNOW")
cg.add_define("USE_ESPNOW_MAX_PAYLOAD_SIZE", config[CONF_MAX_PAYLOAD_SIZE])
if CORE.is_esp32:
from esphome.components.esp32 import include_builtin_idf_component
include_builtin_idf_component("esp_wifi")
if CONF_WIFI in CORE.config:
# Track the Wi-Fi channel via connect events instead of polling every loop
wifi.request_wifi_connect_state_listener()
+1 -2
View File
@@ -334,9 +334,8 @@ void Fan::dump_traits_(const char *tag, const char *prefix) {
}
if (traits.supports_preset_modes()) {
ESP_LOGCONFIG(tag, "%s Supported presets:", prefix);
for (const char *s : traits.supported_preset_modes()) {
for (const char *s : traits.supported_preset_modes())
ESP_LOGCONFIG(tag, "%s - %s", prefix, s);
}
}
}
+7 -10
View File
@@ -93,8 +93,7 @@ void FeedbackCover::set_open_sensor(binary_sensor::BinarySensor *open_feedback)
// setup callbacks to react to sensor changes
open_feedback->add_on_state_callback([this](bool state) {
ESP_LOGD(TAG, "'%s' - Open feedback '%s'.", this->name_.c_str(),
state ? LOG_STR_LITERAL("STARTED") : LOG_STR_LITERAL("ENDED"));
ESP_LOGD(TAG, "'%s' - Open feedback '%s'.", this->name_.c_str(), state ? "STARTED" : "ENDED");
this->recompute_position_();
if (!state && this->infer_endstop_ && this->current_trigger_operation_ == COVER_OPERATION_OPENING) {
this->endstop_reached_(true);
@@ -107,8 +106,7 @@ void FeedbackCover::set_close_sensor(binary_sensor::BinarySensor *close_feedback
this->close_feedback_ = close_feedback;
close_feedback->add_on_state_callback([this](bool state) {
ESP_LOGD(TAG, "'%s' - Close feedback '%s'.", this->name_.c_str(),
state ? LOG_STR_LITERAL("STARTED") : LOG_STR_LITERAL("ENDED"));
ESP_LOGD(TAG, "'%s' - Close feedback '%s'.", this->name_.c_str(), state ? "STARTED" : "ENDED");
this->recompute_position_();
if (!state && this->infer_endstop_ && this->current_trigger_operation_ == COVER_OPERATION_CLOSING) {
this->endstop_reached_(false);
@@ -146,8 +144,7 @@ void FeedbackCover::endstop_reached_(bool open_endstop) {
// from a position slightly past the endpoint
if (this->current_trigger_operation_ == (open_endstop ? COVER_OPERATION_OPENING : COVER_OPERATION_CLOSING)) {
float dur = (now - this->start_dir_time_) / 1e3f;
ESP_LOGD(TAG, "'%s' - %s endstop reached. Took %.1fs.", this->name_.c_str(),
open_endstop ? LOG_STR_LITERAL("Open") : LOG_STR_LITERAL("Close"), dur);
ESP_LOGD(TAG, "'%s' - %s endstop reached. Took %.1fs.", this->name_.c_str(), open_endstop ? "Open" : "Close", dur);
// if there is no external mechanism, stop the cover
if (!this->has_built_in_endstop_) {
@@ -369,7 +366,7 @@ void FeedbackCover::start_direction_(CoverOperation dir) {
// the case when an obstacle appears while moving is handled in the callback
if (obstacle != nullptr && obstacle->state) {
ESP_LOGD(TAG, "'%s' - %s obstacle detected. Action not started.", this->name_.c_str(),
dir == COVER_OPERATION_OPENING ? LOG_STR_LITERAL("Open") : LOG_STR_LITERAL("Close"));
dir == COVER_OPERATION_OPENING ? "Open" : "Close");
return;
}
#endif
@@ -386,9 +383,9 @@ void FeedbackCover::start_direction_(CoverOperation dir) {
this->set_current_operation_(dir, true);
this->prev_command_trigger_ = trig;
ESP_LOGD(TAG, "'%s' - Firing '%s' trigger.", this->name_.c_str(),
dir == COVER_OPERATION_OPENING ? LOG_STR_LITERAL("OPEN")
: dir == COVER_OPERATION_CLOSING ? LOG_STR_LITERAL("CLOSE")
: LOG_STR_LITERAL("STOP"));
dir == COVER_OPERATION_OPENING ? "OPEN"
: dir == COVER_OPERATION_CLOSING ? "CLOSE"
: "STOP");
trig->trigger();
}
}
@@ -547,8 +547,8 @@ void FingerprintGrowComponent::dump_config() {
" System Identifier Code: 0x%.4X\n"
" Touch Sensing Pin: %s\n"
" Sensor Power Pin: %s",
this->system_identifier_code_, this->has_sensing_pin_ ? sensing_pin_buf : LOG_STR_LITERAL("None"),
this->has_power_pin_ ? power_pin_buf : LOG_STR_LITERAL("None"));
this->system_identifier_code_, this->has_sensing_pin_ ? sensing_pin_buf : "None",
this->has_power_pin_ ? power_pin_buf : "None");
if (this->idle_period_to_sleep_ms_ < UINT32_MAX) {
ESP_LOGCONFIG(TAG, " Idle Period to Sleep: %" PRIu32 " ms", this->idle_period_to_sleep_ms_);
} else {
@@ -35,20 +35,18 @@ void GraphicalDisplayMenu::setup() {
}
void GraphicalDisplayMenu::dump_config() {
ESP_LOGCONFIG(
TAG,
"Graphical Display Menu\n"
" Has Display: %s\n"
" Popup Mode: %s\n"
" Advanced Drawing Mode: %s\n"
" Has Font: %s\n"
" Mode: %s\n"
" Active: %s\n"
" Menu items:",
YESNO(this->display_ != nullptr), YESNO(this->display_ != nullptr), YESNO(this->display_ == nullptr),
YESNO(this->font_ != nullptr),
this->mode_ == display_menu_base::MENU_MODE_ROTARY ? LOG_STR_LITERAL("Rotary") : LOG_STR_LITERAL("Joystick"),
YESNO(this->active_));
ESP_LOGCONFIG(TAG,
"Graphical Display Menu\n"
" Has Display: %s\n"
" Popup Mode: %s\n"
" Advanced Drawing Mode: %s\n"
" Has Font: %s\n"
" Mode: %s\n"
" Active: %s\n"
" Menu items:",
YESNO(this->display_ != nullptr), YESNO(this->display_ != nullptr), YESNO(this->display_ == nullptr),
YESNO(this->font_ != nullptr),
this->mode_ == display_menu_base::MENU_MODE_ROTARY ? "Rotary" : "Joystick", YESNO(this->active_));
for (size_t i = 0; i < this->displayed_item_->items_size(); i++) {
auto *item = this->displayed_item_->get_item(i);
ESP_LOGCONFIG(TAG, " %i: %s (Type: %s, Immediate Edit: %s)", i, item->get_text().c_str(),
@@ -1,68 +1,105 @@
#include "growatt_solar.h"
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::growatt_solar {
namespace helpers = modbus::helpers;
static const char *const TAG = "growatt_solar";
static const uint8_t MODBUS_REGISTER_COUNT[] = {33, 95}; // indexed with enum GrowattProtocolVersion
void GrowattSolar::update() { this->read_input_registers(0, MODBUS_REGISTER_COUNT[this->protocol_version_]); }
void GrowattSolar::loop() {
// If update() was unable to send we retry until we can send.
if (!this->waiting_to_update_)
return;
update();
}
void GrowattSolar::on_read_input_registers(uint16_t start_address, std::span<const uint16_t> registers,
modbus::ResponseStatus status) {
if (!modbus::succeeded(status))
void GrowattSolar::update() {
// If our last send has had no reply yet, and it wasn't that long ago, do nothing.
const uint32_t now = App.get_loop_component_start_time();
if (now - this->last_send_ < this->get_update_interval() / 2) {
return;
}
// The bus might be slow, or there might be other devices, or other components might be talking to our device.
if (!this->ready_for_immediate_send()) {
this->waiting_to_update_ = true;
return;
}
this->waiting_to_update_ = false;
this->read_input_registers(0, MODBUS_REGISTER_COUNT[this->protocol_version_]);
this->last_send_ = millis();
}
void GrowattSolar::on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) {
auto data = modbus::helpers::server_pdu_payload(response_pdu);
// Other components might be sending commands to our device. But we don't get called with enough
// context to know what is what. So if we didn't do a send, we ignore the data.
if (!this->last_send_)
return;
this->last_send_ = 0;
// Also ignore the data if the message is too short. Otherwise we will publish invalid values.
if (data.size() < MODBUS_REGISTER_COUNT[this->protocol_version_] * 2)
return;
// Publish a sensor if its register(s) are in this response; skipping absent registers keeps this
// correct for any read range, so the poll may be split into multiple requests.
auto publish_1_reg_sensor_state = [&](sensor::Sensor *sensor, uint16_t reg, float unit) -> void {
auto publish_1_reg_sensor_state = [&](sensor::Sensor *sensor, size_t i, float unit) -> void {
if (sensor == nullptr)
return;
if (auto value = helpers::value_at<helpers::SensorValueType::U_WORD>(registers, start_address, reg))
sensor->publish_state(*value * unit);
float value = encode_uint16(data[i * 2], data[i * 2 + 1]) * unit;
sensor->publish_state(value);
};
auto publish_2_reg_sensor_state = [&](sensor::Sensor *sensor, uint16_t reg, float unit) -> void {
if (sensor == nullptr)
return;
if (auto value = helpers::value_at<helpers::SensorValueType::U_DWORD>(registers, start_address, reg))
sensor->publish_state(*value * unit);
auto publish_2_reg_sensor_state = [&](sensor::Sensor *sensor, size_t reg1, size_t reg2, float unit) -> void {
float value = ((encode_uint16(data[reg1 * 2], data[reg1 * 2 + 1]) << 16) +
encode_uint16(data[reg2 * 2], data[reg2 * 2 + 1])) *
unit;
if (sensor != nullptr)
sensor->publish_state(value);
};
switch (this->protocol_version_) {
case RTU: {
publish_1_reg_sensor_state(this->inverter_status_, RTU_INVERTER_STATUS, 1);
publish_2_reg_sensor_state(this->pv_active_power_sensor_, RTU_PV_ACTIVE_POWER, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->pv_active_power_sensor_, RTU_PV_ACTIVE_POWER, RTU_PV_ACTIVE_POWER + 1,
ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->pvs_[0].voltage_sensor_, RTU_PV1_VOLTAGE, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->pvs_[0].current_sensor_, RTU_PV1_CURRENT, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->pvs_[0].active_power_sensor_, RTU_PV1_ACTIVE_POWER, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->pvs_[0].active_power_sensor_, RTU_PV1_ACTIVE_POWER, RTU_PV1_ACTIVE_POWER + 1,
ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->pvs_[1].voltage_sensor_, RTU_PV2_VOLTAGE, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->pvs_[1].current_sensor_, RTU_PV2_CURRENT, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->pvs_[1].active_power_sensor_, RTU_PV2_ACTIVE_POWER, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->pvs_[1].active_power_sensor_, RTU_PV2_ACTIVE_POWER, RTU_PV2_ACTIVE_POWER + 1,
ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->grid_active_power_sensor_, RTU_GRID_ACTIVE_POWER, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->grid_active_power_sensor_, RTU_GRID_ACTIVE_POWER, RTU_GRID_ACTIVE_POWER + 1,
ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->grid_frequency_sensor_, RTU_GRID_FREQUENCY, TWO_DEC_UNIT);
publish_1_reg_sensor_state(this->phases_[0].voltage_sensor_, RTU_PHASE1_VOLTAGE, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->phases_[0].current_sensor_, RTU_PHASE1_CURRENT, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->phases_[0].active_power_sensor_, RTU_PHASE1_ACTIVE_POWER, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->phases_[0].active_power_sensor_, RTU_PHASE1_ACTIVE_POWER,
RTU_PHASE1_ACTIVE_POWER + 1, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->phases_[1].voltage_sensor_, RTU_PHASE2_VOLTAGE, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->phases_[1].current_sensor_, RTU_PHASE2_CURRENT, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->phases_[1].active_power_sensor_, RTU_PHASE2_ACTIVE_POWER, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->phases_[1].active_power_sensor_, RTU_PHASE2_ACTIVE_POWER,
RTU_PHASE2_ACTIVE_POWER + 1, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->phases_[2].voltage_sensor_, RTU_PHASE3_VOLTAGE, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->phases_[2].current_sensor_, RTU_PHASE3_CURRENT, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->phases_[2].active_power_sensor_, RTU_PHASE3_ACTIVE_POWER, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->phases_[2].active_power_sensor_, RTU_PHASE3_ACTIVE_POWER,
RTU_PHASE3_ACTIVE_POWER + 1, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->today_production_, RTU_TODAY_PRODUCTION, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->total_energy_production_, RTU_TOTAL_ENERGY_PRODUCTION, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->today_production_, RTU_TODAY_PRODUCTION, RTU_TODAY_PRODUCTION + 1, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->total_energy_production_, RTU_TOTAL_ENERGY_PRODUCTION,
RTU_TOTAL_ENERGY_PRODUCTION + 1, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->inverter_module_temp_, RTU_INVERTER_MODULE_TEMP, ONE_DEC_UNIT);
break;
@@ -70,33 +107,42 @@ void GrowattSolar::on_read_input_registers(uint16_t start_address, std::span<con
case RTU2: {
publish_1_reg_sensor_state(this->inverter_status_, RTU2_INVERTER_STATUS, 1);
publish_2_reg_sensor_state(this->pv_active_power_sensor_, RTU2_PV_ACTIVE_POWER, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->pv_active_power_sensor_, RTU2_PV_ACTIVE_POWER, RTU2_PV_ACTIVE_POWER + 1,
ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->pvs_[0].voltage_sensor_, RTU2_PV1_VOLTAGE, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->pvs_[0].current_sensor_, RTU2_PV1_CURRENT, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->pvs_[0].active_power_sensor_, RTU2_PV1_ACTIVE_POWER, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->pvs_[0].active_power_sensor_, RTU2_PV1_ACTIVE_POWER, RTU2_PV1_ACTIVE_POWER + 1,
ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->pvs_[1].voltage_sensor_, RTU2_PV2_VOLTAGE, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->pvs_[1].current_sensor_, RTU2_PV2_CURRENT, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->pvs_[1].active_power_sensor_, RTU2_PV2_ACTIVE_POWER, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->pvs_[1].active_power_sensor_, RTU2_PV2_ACTIVE_POWER, RTU2_PV2_ACTIVE_POWER + 1,
ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->grid_active_power_sensor_, RTU2_GRID_ACTIVE_POWER, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->grid_active_power_sensor_, RTU2_GRID_ACTIVE_POWER, RTU2_GRID_ACTIVE_POWER + 1,
ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->grid_frequency_sensor_, RTU2_GRID_FREQUENCY, TWO_DEC_UNIT);
publish_1_reg_sensor_state(this->phases_[0].voltage_sensor_, RTU2_PHASE1_VOLTAGE, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->phases_[0].current_sensor_, RTU2_PHASE1_CURRENT, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->phases_[0].active_power_sensor_, RTU2_PHASE1_ACTIVE_POWER, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->phases_[0].active_power_sensor_, RTU2_PHASE1_ACTIVE_POWER,
RTU2_PHASE1_ACTIVE_POWER + 1, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->phases_[1].voltage_sensor_, RTU2_PHASE2_VOLTAGE, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->phases_[1].current_sensor_, RTU2_PHASE2_CURRENT, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->phases_[1].active_power_sensor_, RTU2_PHASE2_ACTIVE_POWER, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->phases_[1].active_power_sensor_, RTU2_PHASE2_ACTIVE_POWER,
RTU2_PHASE2_ACTIVE_POWER + 1, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->phases_[2].voltage_sensor_, RTU2_PHASE3_VOLTAGE, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->phases_[2].current_sensor_, RTU2_PHASE3_CURRENT, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->phases_[2].active_power_sensor_, RTU2_PHASE3_ACTIVE_POWER, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->phases_[2].active_power_sensor_, RTU2_PHASE3_ACTIVE_POWER,
RTU2_PHASE3_ACTIVE_POWER + 1, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->today_production_, RTU2_TODAY_PRODUCTION, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->total_energy_production_, RTU2_TOTAL_ENERGY_PRODUCTION, ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->today_production_, RTU2_TODAY_PRODUCTION, RTU2_TODAY_PRODUCTION + 1,
ONE_DEC_UNIT);
publish_2_reg_sensor_state(this->total_energy_production_, RTU2_TOTAL_ENERGY_PRODUCTION,
RTU2_TOTAL_ENERGY_PRODUCTION + 1, ONE_DEC_UNIT);
publish_1_reg_sensor_state(this->inverter_module_temp_, RTU2_INVERTER_MODULE_TEMP, ONE_DEC_UNIT);
break;
@@ -17,59 +17,59 @@ enum GrowattProtocolVersion {
};
// Register addresses for the RTU protocol.
constexpr uint16_t RTU_INVERTER_STATUS = 0; // length = 1
constexpr uint16_t RTU_PV_ACTIVE_POWER = 1; // length = 2
constexpr uint16_t RTU_PV1_VOLTAGE = 3; // length = 1
constexpr uint16_t RTU_PV1_CURRENT = 4; // length = 1
constexpr uint16_t RTU_PV1_ACTIVE_POWER = 5; // length = 2
constexpr uint16_t RTU_PV2_VOLTAGE = 7; // length = 1
constexpr uint16_t RTU_PV2_CURRENT = 8; // length = 1
constexpr uint16_t RTU_PV2_ACTIVE_POWER = 9; // length = 2
constexpr uint16_t RTU_GRID_ACTIVE_POWER = 11; // length = 2
constexpr uint16_t RTU_GRID_FREQUENCY = 13; // length = 1
constexpr uint16_t RTU_PHASE1_VOLTAGE = 14; // length = 1
constexpr uint16_t RTU_PHASE1_CURRENT = 15; // length = 1
constexpr uint16_t RTU_PHASE1_ACTIVE_POWER = 16; // length = 2
constexpr uint16_t RTU_PHASE2_VOLTAGE = 18; // length = 1
constexpr uint16_t RTU_PHASE2_CURRENT = 19; // length = 1
constexpr uint16_t RTU_PHASE2_ACTIVE_POWER = 20; // length = 2
constexpr uint16_t RTU_PHASE3_VOLTAGE = 22; // length = 1
constexpr uint16_t RTU_PHASE3_CURRENT = 23; // length = 1
constexpr uint16_t RTU_PHASE3_ACTIVE_POWER = 24; // length = 2
constexpr uint16_t RTU_TODAY_PRODUCTION = 26; // length = 2
constexpr uint16_t RTU_TOTAL_ENERGY_PRODUCTION = 28; // length = 2
constexpr uint16_t RTU_INVERTER_MODULE_TEMP = 32; // length = 1
constexpr size_t RTU_INVERTER_STATUS = 0; // length = 1
constexpr size_t RTU_PV_ACTIVE_POWER = 1; // length = 2
constexpr size_t RTU_PV1_VOLTAGE = 3; // length = 1
constexpr size_t RTU_PV1_CURRENT = 4; // length = 1
constexpr size_t RTU_PV1_ACTIVE_POWER = 5; // length = 2
constexpr size_t RTU_PV2_VOLTAGE = 7; // length = 1
constexpr size_t RTU_PV2_CURRENT = 8; // length = 1
constexpr size_t RTU_PV2_ACTIVE_POWER = 9; // length = 2
constexpr size_t RTU_GRID_ACTIVE_POWER = 11; // length = 2
constexpr size_t RTU_GRID_FREQUENCY = 13; // length = 1
constexpr size_t RTU_PHASE1_VOLTAGE = 14; // length = 1
constexpr size_t RTU_PHASE1_CURRENT = 15; // length = 1
constexpr size_t RTU_PHASE1_ACTIVE_POWER = 16; // length = 2
constexpr size_t RTU_PHASE2_VOLTAGE = 18; // length = 1
constexpr size_t RTU_PHASE2_CURRENT = 19; // length = 1
constexpr size_t RTU_PHASE2_ACTIVE_POWER = 20; // length = 2
constexpr size_t RTU_PHASE3_VOLTAGE = 22; // length = 1
constexpr size_t RTU_PHASE3_CURRENT = 23; // length = 1
constexpr size_t RTU_PHASE3_ACTIVE_POWER = 24; // length = 2
constexpr size_t RTU_TODAY_PRODUCTION = 26; // length = 2
constexpr size_t RTU_TOTAL_ENERGY_PRODUCTION = 28; // length = 2
constexpr size_t RTU_INVERTER_MODULE_TEMP = 32; // length = 1
// Input register addresses for the RTU2 protocol as described
// in the "GROWATT INVERTER MODBUS PROTOCOL_II V1.39" document.
constexpr uint16_t RTU2_INVERTER_STATUS = 0; // length = 1
constexpr uint16_t RTU2_PV_ACTIVE_POWER = 1; // length = 2
constexpr uint16_t RTU2_PV1_VOLTAGE = 3; // length = 1
constexpr uint16_t RTU2_PV1_CURRENT = 4; // length = 1
constexpr uint16_t RTU2_PV1_ACTIVE_POWER = 5; // length = 2
constexpr uint16_t RTU2_PV2_VOLTAGE = 7; // length = 1
constexpr uint16_t RTU2_PV2_CURRENT = 8; // length = 1
constexpr uint16_t RTU2_PV2_ACTIVE_POWER = 9; // length = 2
constexpr uint16_t RTU2_GRID_ACTIVE_POWER = 35; // length = 2
constexpr uint16_t RTU2_GRID_FREQUENCY = 37; // length = 1
constexpr uint16_t RTU2_PHASE1_VOLTAGE = 38; // length = 1
constexpr uint16_t RTU2_PHASE1_CURRENT = 39; // length = 1
constexpr uint16_t RTU2_PHASE1_ACTIVE_POWER = 40; // length = 2
constexpr uint16_t RTU2_PHASE2_VOLTAGE = 42; // length = 1
constexpr uint16_t RTU2_PHASE2_CURRENT = 43; // length = 1
constexpr uint16_t RTU2_PHASE2_ACTIVE_POWER = 44; // length = 2
constexpr uint16_t RTU2_PHASE3_VOLTAGE = 46; // length = 1
constexpr uint16_t RTU2_PHASE3_CURRENT = 47; // length = 1
constexpr uint16_t RTU2_PHASE3_ACTIVE_POWER = 48; // length = 2
constexpr uint16_t RTU2_TODAY_PRODUCTION = 53; // length = 2
constexpr uint16_t RTU2_TOTAL_ENERGY_PRODUCTION = 55; // length = 2
constexpr uint16_t RTU2_INVERTER_MODULE_TEMP = 93; // length = 1
constexpr size_t RTU2_INVERTER_STATUS = 0; // length = 1
constexpr size_t RTU2_PV_ACTIVE_POWER = 1; // length = 2
constexpr size_t RTU2_PV1_VOLTAGE = 3; // length = 1
constexpr size_t RTU2_PV1_CURRENT = 4; // length = 1
constexpr size_t RTU2_PV1_ACTIVE_POWER = 5; // length = 2
constexpr size_t RTU2_PV2_VOLTAGE = 7; // length = 1
constexpr size_t RTU2_PV2_CURRENT = 8; // length = 1
constexpr size_t RTU2_PV2_ACTIVE_POWER = 9; // length = 2
constexpr size_t RTU2_GRID_ACTIVE_POWER = 35; // length = 2
constexpr size_t RTU2_GRID_FREQUENCY = 37; // length = 1
constexpr size_t RTU2_PHASE1_VOLTAGE = 38; // length = 1
constexpr size_t RTU2_PHASE1_CURRENT = 39; // length = 1
constexpr size_t RTU2_PHASE1_ACTIVE_POWER = 40; // length = 2
constexpr size_t RTU2_PHASE2_VOLTAGE = 42; // length = 1
constexpr size_t RTU2_PHASE2_CURRENT = 43; // length = 1
constexpr size_t RTU2_PHASE2_ACTIVE_POWER = 44; // length = 2
constexpr size_t RTU2_PHASE3_VOLTAGE = 46; // length = 1
constexpr size_t RTU2_PHASE3_CURRENT = 47; // length = 1
constexpr size_t RTU2_PHASE3_ACTIVE_POWER = 48; // length = 2
constexpr size_t RTU2_TODAY_PRODUCTION = 53; // length = 2
constexpr size_t RTU2_TOTAL_ENERGY_PRODUCTION = 55; // length = 2
constexpr size_t RTU2_INVERTER_MODULE_TEMP = 93; // length = 1
class GrowattSolar final : public PollingComponent, public modbus::ModbusClientDevice {
public:
void loop() override;
void update() override;
void on_read_input_registers(uint16_t start_address, std::span<const uint16_t> registers,
modbus::ResponseStatus status) override;
void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) override;
void dump_config() override;
void set_protocol_version(GrowattProtocolVersion protocol_version) { this->protocol_version_ = protocol_version; }
@@ -104,6 +104,9 @@ class GrowattSolar final : public PollingComponent, public modbus::ModbusClientD
}
protected:
bool waiting_to_update_{false};
uint32_t last_send_{0};
struct GrowattPhase {
sensor::Sensor *voltage_sensor_{nullptr};
sensor::Sensor *current_sensor_{nullptr};
@@ -69,12 +69,10 @@ void GT911Touchscreen::setup_internal_() {
// Direct MCU pin: attach a hardware interrupt, no polling needed.
this->attach_interrupt_(static_cast<InternalGPIOPin *>(this->interrupt_pin_),
active_high ? gpio::INTERRUPT_RISING_EDGE : gpio::INTERRUPT_FALLING_EDGE);
ESP_LOGD(TAG, "Interrupt pin: hardware interrupt, active %s",
active_high ? LOG_STR_LITERAL("HIGH") : LOG_STR_LITERAL("LOW"));
ESP_LOGD(TAG, "Interrupt pin: hardware interrupt, active %s", active_high ? "HIGH" : "LOW");
} else {
// IO expander pin: leave as output for configuration only.
ESP_LOGD(TAG, "Interrupt pin: IO expander polling mode, active %s",
active_high ? LOG_STR_LITERAL("HIGH") : LOG_STR_LITERAL("LOW"));
ESP_LOGD(TAG, "Interrupt pin: IO expander polling mode, active %s", active_high ? "HIGH" : "LOW");
}
}
}
+1 -2
View File
@@ -248,8 +248,7 @@ void HaierClimateBase::setup() {
void HaierClimateBase::dump_config() {
LOG_CLIMATE("", "Haier Climate", this);
ESP_LOGCONFIG(TAG, " Device communication status: %s",
this->valid_connection() ? LOG_STR_LITERAL("established") : LOG_STR_LITERAL("none"));
ESP_LOGCONFIG(TAG, " Device communication status: %s", this->valid_connection() ? "established" : "none");
}
void HaierClimateBase::loop() {
+5 -5
View File
@@ -343,11 +343,11 @@ void HonClimate::dump_config() {
this->hvac_hardware_info_.value().software_version_,
this->hvac_hardware_info_.value().hardware_version_, this->hvac_hardware_info_.value().device_name_);
ESP_LOGCONFIG(TAG, " Device features:%s%s%s%s%s",
(this->hvac_hardware_info_.value().functions_[0] ? LOG_STR_LITERAL(" interactive") : ""),
(this->hvac_hardware_info_.value().functions_[1] ? LOG_STR_LITERAL(" controller-device") : ""),
(this->hvac_hardware_info_.value().functions_[2] ? LOG_STR_LITERAL(" crc") : ""),
(this->hvac_hardware_info_.value().functions_[3] ? LOG_STR_LITERAL(" multinode") : ""),
(this->hvac_hardware_info_.value().functions_[4] ? LOG_STR_LITERAL(" role") : ""));
(this->hvac_hardware_info_.value().functions_[0] ? " interactive" : ""),
(this->hvac_hardware_info_.value().functions_[1] ? " controller-device" : ""),
(this->hvac_hardware_info_.value().functions_[2] ? " crc" : ""),
(this->hvac_hardware_info_.value().functions_[3] ? " multinode" : ""),
(this->hvac_hardware_info_.value().functions_[4] ? " role" : ""));
ESP_LOGCONFIG(TAG, " Active alarms: %s", buf_to_hex(this->active_alarms_, sizeof(this->active_alarms_)).c_str());
}
}
@@ -1,71 +1,124 @@
#include "havells_solar.h"
#include "havells_solar_registers.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::havells_solar {
namespace helpers = modbus::helpers;
static const char *const TAG = "havells_solar";
static const uint8_t MODBUS_REGISTER_COUNT = 48; // 48 x 16-bit registers
void HavellsSolar::on_read_holding_registers(uint16_t start_address, std::span<const uint16_t> registers,
modbus::ResponseStatus status) {
if (!modbus::succeeded(status))
return; // the hub already logs exception responses
void HavellsSolar::on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) {
auto data = modbus::helpers::server_pdu_payload(response_pdu);
if (data.size() < MODBUS_REGISTER_COUNT * 2) {
ESP_LOGW(TAG, "Invalid size for HavellsSolar!");
return;
}
// Publish a sensor if its register(s) are in this response; skipping absent registers keeps this
// correct for any read range, so the poll may be split into multiple requests.
auto publish_1_register = [&](sensor::Sensor *sensor, uint16_t reg, float unit) -> void {
if (sensor == nullptr)
return;
if (auto value = helpers::value_at<helpers::SensorValueType::U_WORD>(registers, start_address, reg))
sensor->publish_state(*value * unit);
/* Usage: returns the float value of 1 register read by modbus
Arg1: Register address * number of bytes per register
Arg2: Multiplier for final register value
*/
auto havells_solar_get_2_registers = [&](size_t i, float unit) -> float {
uint32_t temp = encode_uint32(data[i], data[i + 1], data[i + 2], data[i + 3]);
return temp * unit;
};
auto publish_2_registers = [&](sensor::Sensor *sensor, uint16_t reg, float unit) -> void {
if (sensor == nullptr)
return;
if (auto value = helpers::value_at<helpers::SensorValueType::U_DWORD>(registers, start_address, reg))
sensor->publish_state(*value * unit);
/* Usage: returns the float value of 2 registers read by modbus
Arg1: Register address * number of bytes per register
Arg2: Multiplier for final register value
*/
auto havells_solar_get_1_register = [&](size_t i, float unit) -> float {
uint16_t temp = encode_uint16(data[i], data[i + 1]);
return temp * unit;
};
for (uint8_t i = 0; i < 3; i++) {
auto &phase = this->phases_[i];
auto phase = this->phases_[i];
if (!phase.setup)
continue;
publish_1_register(phase.voltage_sensor_, HAVELLS_PHASE_1_VOLTAGE + i * 2, ONE_DEC_UNIT);
publish_1_register(phase.current_sensor_, HAVELLS_PHASE_1_CURRENT + i * 2, TWO_DEC_UNIT);
float voltage = havells_solar_get_1_register(HAVELLS_PHASE_1_VOLTAGE * 2 + (i * 4), ONE_DEC_UNIT);
float current = havells_solar_get_1_register(HAVELLS_PHASE_1_CURRENT * 2 + (i * 4), TWO_DEC_UNIT);
if (phase.voltage_sensor_ != nullptr)
phase.voltage_sensor_->publish_state(voltage);
if (phase.current_sensor_ != nullptr)
phase.current_sensor_->publish_state(current);
}
for (uint8_t i = 0; i < 2; i++) {
auto &pv = this->pvs_[i];
auto pv = this->pvs_[i];
if (!pv.setup)
continue;
publish_1_register(pv.voltage_sensor_, HAVELLS_PV_1_VOLTAGE + i * 2, ONE_DEC_UNIT);
publish_1_register(pv.current_sensor_, HAVELLS_PV_1_CURRENT + i * 2, TWO_DEC_UNIT);
publish_1_register(pv.active_power_sensor_, HAVELLS_PV_1_POWER + i, MULTIPLY_TEN_UNIT);
publish_1_register(pv.voltage_sampled_by_secondary_cpu_sensor_, HAVELLS_PV1_VOLTAGE_SAMPLED_BY_SECONDARY_CPU + i,
ONE_DEC_UNIT);
publish_1_register(pv.insulation_of_p_to_ground_sensor_, HAVELLS_PV1_INSULATION_OF_P_TO_GROUND + i, NO_DEC_UNIT);
float voltage = havells_solar_get_1_register(HAVELLS_PV_1_VOLTAGE * 2 + (i * 4), ONE_DEC_UNIT);
float current = havells_solar_get_1_register(HAVELLS_PV_1_CURRENT * 2 + (i * 4), TWO_DEC_UNIT);
float active_power = havells_solar_get_1_register(HAVELLS_PV_1_POWER * 2 + (i * 2), MULTIPLY_TEN_UNIT);
float voltage_sampled_by_secondary_cpu =
havells_solar_get_1_register(HAVELLS_PV1_VOLTAGE_SAMPLED_BY_SECONDARY_CPU * 2 + (i * 2), ONE_DEC_UNIT);
float insulation_of_p_to_ground =
havells_solar_get_1_register(HAVELLS_PV1_INSULATION_OF_P_TO_GROUND * 2 + (i * 2), NO_DEC_UNIT);
if (pv.voltage_sensor_ != nullptr)
pv.voltage_sensor_->publish_state(voltage);
if (pv.current_sensor_ != nullptr)
pv.current_sensor_->publish_state(current);
if (pv.active_power_sensor_ != nullptr)
pv.active_power_sensor_->publish_state(active_power);
if (pv.voltage_sampled_by_secondary_cpu_sensor_ != nullptr)
pv.voltage_sampled_by_secondary_cpu_sensor_->publish_state(voltage_sampled_by_secondary_cpu);
if (pv.insulation_of_p_to_ground_sensor_ != nullptr)
pv.insulation_of_p_to_ground_sensor_->publish_state(insulation_of_p_to_ground);
}
publish_1_register(this->frequency_sensor_, HAVELLS_GRID_FREQUENCY, TWO_DEC_UNIT);
publish_1_register(this->active_power_sensor_, HAVELLS_SYSTEM_ACTIVE_POWER, MULTIPLY_TEN_UNIT);
publish_1_register(this->reactive_power_sensor_, HAVELLS_SYSTEM_REACTIVE_POWER, TWO_DEC_UNIT);
publish_1_register(this->today_production_sensor_, HAVELLS_TODAY_PRODUCTION, TWO_DEC_UNIT);
publish_2_registers(this->total_energy_production_sensor_, HAVELLS_TOTAL_ENERGY_PRODUCTION, NO_DEC_UNIT);
publish_2_registers(this->total_generation_time_sensor_, HAVELLS_TOTAL_GENERATION_TIME, NO_DEC_UNIT);
publish_1_register(this->today_generation_time_sensor_, HAVELLS_TODAY_GENERATION_TIME, NO_DEC_UNIT);
publish_1_register(this->inverter_module_temp_sensor_, HAVELLS_INVERTER_MODULE_TEMP, NO_DEC_UNIT);
publish_1_register(this->inverter_inner_temp_sensor_, HAVELLS_INVERTER_INNER_TEMP, NO_DEC_UNIT);
publish_1_register(this->inverter_bus_voltage_sensor_, HAVELLS_INVERTER_BUS_VOLTAGE, NO_DEC_UNIT);
publish_1_register(this->insulation_pv_n_to_ground_sensor_, HAVELLS_INSULATION_OF_PV_N_TO_GROUND, NO_DEC_UNIT);
publish_1_register(this->gfci_value_sensor_, HAVELLS_GFCI_VALUE, NO_DEC_UNIT);
publish_1_register(this->dci_of_r_sensor_, HAVELLS_DCI_OF_R, NO_DEC_UNIT);
publish_1_register(this->dci_of_s_sensor_, HAVELLS_DCI_OF_S, NO_DEC_UNIT);
publish_1_register(this->dci_of_t_sensor_, HAVELLS_DCI_OF_T, NO_DEC_UNIT);
float frequency = havells_solar_get_1_register(HAVELLS_GRID_FREQUENCY * 2, TWO_DEC_UNIT);
float active_power = havells_solar_get_1_register(HAVELLS_SYSTEM_ACTIVE_POWER * 2, MULTIPLY_TEN_UNIT);
float reactive_power = havells_solar_get_1_register(HAVELLS_SYSTEM_REACTIVE_POWER * 2, TWO_DEC_UNIT);
float today_production = havells_solar_get_1_register(HAVELLS_TODAY_PRODUCTION * 2, TWO_DEC_UNIT);
float total_energy_production = havells_solar_get_2_registers(HAVELLS_TOTAL_ENERGY_PRODUCTION * 2, NO_DEC_UNIT);
float total_generation_time = havells_solar_get_2_registers(HAVELLS_TOTAL_GENERATION_TIME * 2, NO_DEC_UNIT);
float today_generation_time = havells_solar_get_1_register(HAVELLS_TODAY_GENERATION_TIME * 2, NO_DEC_UNIT);
float inverter_module_temp = havells_solar_get_1_register(HAVELLS_INVERTER_MODULE_TEMP * 2, NO_DEC_UNIT);
float inverter_inner_temp = havells_solar_get_1_register(HAVELLS_INVERTER_INNER_TEMP * 2, NO_DEC_UNIT);
float inverter_bus_voltage = havells_solar_get_1_register(HAVELLS_INVERTER_BUS_VOLTAGE * 2, NO_DEC_UNIT);
float insulation_pv_n_to_ground = havells_solar_get_1_register(HAVELLS_INSULATION_OF_PV_N_TO_GROUND * 2, NO_DEC_UNIT);
float gfci_value = havells_solar_get_1_register(HAVELLS_GFCI_VALUE * 2, NO_DEC_UNIT);
float dci_of_r = havells_solar_get_1_register(HAVELLS_DCI_OF_R * 2, NO_DEC_UNIT);
float dci_of_s = havells_solar_get_1_register(HAVELLS_DCI_OF_S * 2, NO_DEC_UNIT);
float dci_of_t = havells_solar_get_1_register(HAVELLS_DCI_OF_T * 2, NO_DEC_UNIT);
if (this->frequency_sensor_ != nullptr)
this->frequency_sensor_->publish_state(frequency);
if (this->active_power_sensor_ != nullptr)
this->active_power_sensor_->publish_state(active_power);
if (this->reactive_power_sensor_ != nullptr)
this->reactive_power_sensor_->publish_state(reactive_power);
if (this->today_production_sensor_ != nullptr)
this->today_production_sensor_->publish_state(today_production);
if (this->total_energy_production_sensor_ != nullptr)
this->total_energy_production_sensor_->publish_state(total_energy_production);
if (this->total_generation_time_sensor_ != nullptr)
this->total_generation_time_sensor_->publish_state(total_generation_time);
if (this->today_generation_time_sensor_ != nullptr)
this->today_generation_time_sensor_->publish_state(today_generation_time);
if (this->inverter_module_temp_sensor_ != nullptr)
this->inverter_module_temp_sensor_->publish_state(inverter_module_temp);
if (this->inverter_inner_temp_sensor_ != nullptr)
this->inverter_inner_temp_sensor_->publish_state(inverter_inner_temp);
if (this->inverter_bus_voltage_sensor_ != nullptr)
this->inverter_bus_voltage_sensor_->publish_state(inverter_bus_voltage);
if (this->insulation_pv_n_to_ground_sensor_ != nullptr)
this->insulation_pv_n_to_ground_sensor_->publish_state(insulation_pv_n_to_ground);
if (this->gfci_value_sensor_ != nullptr)
this->gfci_value_sensor_->publish_state(gfci_value);
if (this->dci_of_r_sensor_ != nullptr)
this->dci_of_r_sensor_->publish_state(dci_of_r);
if (this->dci_of_s_sensor_ != nullptr)
this->dci_of_s_sensor_->publish_state(dci_of_s);
if (this->dci_of_t_sensor_ != nullptr)
this->dci_of_t_sensor_->publish_state(dci_of_t);
}
void HavellsSolar::update() { this->read_holding_registers(0, MODBUS_REGISTER_COUNT); }
@@ -77,8 +77,7 @@ class HavellsSolar final : public PollingComponent, public modbus::ModbusClientD
void update() override;
void on_read_holding_registers(uint16_t start_address, std::span<const uint16_t> registers,
modbus::ResponseStatus status) override;
void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) override;
void dump_config() override;
@@ -29,9 +29,8 @@ void HBridgeSwitch::dump_config() {
LOG_PIN(" On Pin: ", this->on_pin_);
LOG_PIN(" Off Pin: ", this->off_pin_);
ESP_LOGCONFIG(TAG, " Pulse length: %" PRId32 " ms", this->pulse_length_);
if (this->wait_time_) {
if (this->wait_time_)
ESP_LOGCONFIG(TAG, " Wait time %" PRId32 " ms", this->wait_time_);
}
}
void HBridgeSwitch::write_state(bool state) {

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