Compare commits

...
Author SHA1 Message Date
J. Nick Koston 2ea2ffb127 Cover setup_entity_integrations 2026-10-14 10:42:15 -10:00
J. Nick Koston 4dccc1ea89 Merge remote-tracking branch 'origin/dev' into binary-sensor-lazy-integration-imports 2026-10-14 10:39:26 -10:00
J. Nick Koston a4278b8dec [ci] Run benchmarks when a component the Python import benchmark imports changes (#20763) 2026-10-14 10:35:33 -10:00
Panoplian 6d173c7371 [bq25186] Set i2c_id in the component tests (#20742) 2026-10-14 10:35:01 -10:00
esphome[bot] 1779b011c9 Bump aioesphomeapi from 46.10.0 to 46.11.0 (#20766)
Co-authored-by: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
2026-10-14 20:21:45 +00:00
PanoplianandJ. Nick Koston 3c4cf50cb6 [core] Point compile errors after a lambda at the right line (#20740)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-14 10:02:51 -10:00
J. Nick Koston e869d1f7ab [ci] Give the BK72xx clang-tidy environment the static allocation flag the real build has (#20764) 2026-10-14 09:56:09 -10:00
e1a996435d [ld2450] Add polygon zones evaluated on the device (#20154)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-14 19:29:16 +00:00
Jonathan Swoboda c5a624a1f2 [tests] Count OTA key probe retries on a fake clock (#20761) 2026-10-14 15:21:35 -04:00
Remco van Essen 15fb7f775e [tmc2209] Add chopper and PWM tuning actions (#20753) 2026-10-14 08:59:21 -10:00
J. Nick Koston 3fa72e61c6 [mqtt] Build the mqtt triggers with build_trigger_callback (#20433) 2026-10-14 08:55:39 -10:00
Kevin Ahrendt 163b09a473 [microphone] Speed up MicrophoneSource audio conversion (#20755) 2026-10-14 08:47:25 -10:00
J. Nick Koston 7afc2768f8 [esp8266] Check that every component's saved values fit together (#20686) 2026-10-14 08:45:51 -10:00
56f1a2bf46 [midea_portasplit] Add Midea PortaSplit climate component (#18033)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-14 08:45:21 -10:00
Panoplian fee47fe2f1 [pcf85063] Convert the weekday to and from the chip's 0-6 range (#20736) 2026-10-14 08:43:23 -10:00
Panoplian b994e081c3 [bm8563] Convert the weekday to and from the chip's 0-6 range (#20737) 2026-10-14 08:42:47 -10:00
Jonathan Swoboda a1f740b71b [lvgl] Keep LV_CONF_PATH one argument when the build path has a space (#20758) 2026-10-14 08:36:17 -10:00
Remco van EssenandClaude Opus 5.5 7f144365ba [fusb302b] Add USB PD sink protocol (#20632)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 08:35:29 -10:00
Marcus VoßandJ. Nick Koston 928459d16f [cse7766] Keep last power reading between measurements at low load (#20631)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-14 08:33:11 -10:00
Jonathan Swoboda 9163359445 [core] Analyze the ethernet component under ESP-IDF (#20745) 2026-10-14 08:31:39 -10:00
Jonathan Swoboda 67307f0eeb [ci] Select the feature-gated files the platform clang-tidy jobs were missing (#20743) 2026-10-14 08:31:06 -10:00
J. Nick Koston 3a8a83d9fb Merge remote-tracking branch 'origin/dev' into binary-sensor-lazy-integration-imports
# Conflicts:
#	tests/benchmarks/python/test_component_import_bench.py
2026-10-14 08:16:43 -10:00
91e0edc11d [pn532] Fix NDEF parsing greater than 255 bytes (#18188)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-14 08:15:48 -10:00
Jonathan Swoboda 9fe75e262f [ci] Add a host clang-tidy environment and job (#20747) 2026-10-14 08:09:53 -10:00
Kevin Ahrendt 0132f722a3 [audio] Bump esp-audio-libs to v4.1.0 (#20754) 2026-10-14 13:24:36 -04:00
Jonathan Swoboda 7d7a78308d [ci] Test tinyusb_keyboard in isolation (#20749) 2026-10-14 10:05:02 -04:00
J. Nick Koston 9c568525b0 [core] Add CodSpeed benchmarks for importing entity components (#20648) 2026-10-14 09:08:41 -04:00
J. Nick Koston 10f42e3e7c [shelly_dimmer] Build the STM32 firmware update path in CI (#20677) 2026-10-14 09:03:34 -04:00
J. Nick Koston 25d4b88605 [nfc] Add unit tests for the URI record prefix codes (#20724) 2026-10-14 09:01:59 -04:00
J. Nick Koston d6abf35138 [sx126x][sx127x] Stop the watchdog reset loop when a packet is sent before setup (#20727) 2026-10-14 09:01:10 -04:00
J. Nick Koston 66ea99ce65 [seeed_mr60fda2] Compile without binary sensors or selects (#20726) 2026-10-14 09:00:08 -04:00
J. Nick Koston d9298cb7b9 [seeed_mr60bha2] Compile without sensors or binary sensors (#20729) 2026-10-14 08:59:27 -04:00
J. Nick Koston 34e1cf96d5 [bmi270] Build the component on ESP8266 in CI (#20731) 2026-10-14 08:58:56 -04:00
56cc03c4d4 [haier] Haier Select component added (#16148)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Pavlo Dudnytskyi <Pavlo.Dudnytskyi@addsecure.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-13 23:27:18 -10:00
35b416c6c0 [web_server_idf] Keep active SSE sessions out of the LRU purge (#17383)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-13 22:19:06 -10:00
Petar Petrov 7f2c3ba3f5 [display] Let other components draw into a display (#20622) 2026-10-14 06:11:47 +00:00
Keith BurzinskiandClaude Opus 5.5 a97604d574 [esp32_ble] Rely on preferences for NVS init (#20708)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:10:51 -10:00
Petar Petrov 1146b77836 [socket] Add TcpClientLink::connecting() (#20722) 2026-10-13 19:19:17 -10:00
J. Nick Koston 1338902ac5 [heatpumpir] Link only the configured protocol classes (#20707) 2026-10-14 17:06:22 +13:00
60a1d70805 [core] Move deferred include processing to parse_yaml (#16344)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-13 17:46:54 -10:00
Ryan LudwigandCursor 887d4c3a83 [esp32_can] Add NO_ACK mode (#20706)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 17:36:15 -10:00
J. Nick Koston c1fa73470e [display] Walk the blit fallback in frame buffer order and resolve the bitness once (#20651) 2026-10-13 17:31:13 -10:00
J. Nick Koston e41d16ba7e [haier] Keep the alarm message table in flash (#20641) 2026-10-14 03:23:28 +00:00
J. Nick Koston 5171113c87 [core] Let PROGMEM_STRING_TABLE hold more than 255 bytes (#20646) 2026-10-13 17:17:48 -10:00
techdregsandJ. Nick Koston 5718e4b779 [tmp102] Add text sensor for diagnostics (#20629)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-13 17:06:56 -10:00
J. Nick Koston 40e12cd0d8 [image] Add CodSpeed benchmarks for image, runtime image and display block draws (#20649) 2026-10-13 17:01:47 -10:00
Brandon HarveyandJ. Nick Koston 07029d0077 [sen6x] Add measurement control and maintenance actions (#18783)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-14 15:47:31 +13:00
Christopher Reimer bb1119d83f [esp32_can] Support 20 kbit/s on ESP32 revision 2 or later (#20426) 2026-10-13 22:34:03 -04:00
7b591b26d6 [sy6970] Service the I2C watchdog so charge/LED settings persist (#19424)
Co-authored-by: Willem Oldemans <>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-13 16:19:58 -10:00
f45e6f0bf4 [midea] Add display_control and send the display toggle over UART without a transmitter (#18671)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-13 16:03:18 -10:00
Keith BurzinskiandClaude Opus 5.5 4b8af08155 [network] Notify listeners when the active interface changes (#20684)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:57:59 -05:00
613973b432 Bump ruff from 0.16.10 to 0.17.0 (#20694)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-10-13 21:53:44 -04:00
dependabot[bot] 9ee4314bc2 Bump esptool from 5.4.0 to 5.5.0 (#20693)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-13 15:41:30 -10:00
fredkotzeandJ. Nick Koston 59f55dbe9e [epaper_spi] Add Waveshare 2.13in-G (JD79660) display models (#18141)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-13 15:40:28 -10:00
dependabot[bot] 8fce95454a Bump github/codeql-action/init from 4.38.2 to 4.38.3 (#20701)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-13 15:39:49 -10:00
dependabot[bot] 16134ee993 Bump astral-sh/setup-uv from 10.2.0 to 10.3.0 (#20704)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-13 15:39:38 -10:00
dependabot[bot] bc39d56bb4 Bump CodSpeedHQ/action from 5.4.0 to 5.4.2 (#20705)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-13 15:39:29 -10:00
dependabot[bot] c5522abc71 Bump github/codeql-action/analyze from 4.38.2 to 4.38.3 (#20699)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-13 21:39:11 -04:00
dependabot[bot] 7cb5906d2a Bump actions/upload-artifact from 7.0.1 to 7.0.2 (#20702)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-14 01:38:42 +00:00
dependabot[bot] d452dbce30 Bump actions/download-artifact from 8.0.1 to 8.0.2 (#20703)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-14 01:38:39 +00:00
dependabot[bot] 81e5b50087 Bump backports-zstd from 1.7.0 to 1.8.0 (#20695)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-13 15:38:35 -10:00
dependabot[bot] 426a4a2dce Bump filelock from 4.0.12 to 4.1.0 (#20696)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-13 15:38:26 -10:00
dependabot[bot]andesphome[bot] c95c17c4e7 Bump pyupgrade from 3.21.2 to 3.22.0 (#20698)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
2026-10-13 15:38:17 -10:00
dependabot[bot] 576d3325fe Bump astral-sh/setup-uv from 10.2.0 to 10.3.0 in /.github/actions/restore-python (#20700)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-13 15:37:47 -10:00
dependabot[bot] c9cf0ad92e Bump platformdirs from 4.12.3 to 4.12.4 (#20697)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-13 21:37:36 -04:00
Felipe Guedes f505212e64 [epaper_spi] Add Waveshare 2.66" e-Paper Module (B) support (#20546) 2026-10-13 15:03:05 -10:00
J. Nick Koston 52a5e6a007 [rpi_dpi_rgb] Set removal of the deprecated component for 2027.5.0 (#20691) 2026-10-14 13:32:18 +13:00
J. Nick Koston 3b261d2cf0 [st7789v] Set removal of the deprecated component for 2027.5.0 (#20689) 2026-10-14 13:30:52 +13:00
J. Nick Koston 9238fcc727 [st7735] Set removal of the deprecated component for 2027.5.0 (#20688) 2026-10-14 13:30:21 +13:00
J. Nick Koston 96902cc122 [qspi_dbi] Set removal of the deprecated component for 2027.5.0 (#20690) 2026-10-14 13:29:52 +13:00
J. Nick Koston 95711d83d1 [ili9xxx] Set removal of the deprecated component for 2027.5.0 (#20687) 2026-10-14 13:29:29 +13:00
Panoplian 3cdcf0417a [lvgl] Make the list_outside_block tests work offline (#20676) 2026-10-13 13:31:08 -10:00
J. Nick Koston c6e90de262 [ci] Catch out-of-class member log tags and keep midea's in flash (#20674) 2026-10-14 12:21:24 +13:00
J. Nick Koston 92f6ab8367 [ci] Lint log tags in headers and core (#20668) 2026-10-14 12:21:24 +13:00
J. Nick Koston f61955baa2 [core] Declare the remaining log tags with ESPHOME_LOG_TAG (#20666) 2026-10-14 12:21:23 +13:00
J. Nick Koston 36e1d831d3 [core] Add a decimal fast path to parse_number<float> (#20639) 2026-10-14 12:18:35 +13:00
J. Nick Koston 63616bf642 [rtttl] Play a configured song from static storage without copying it (#19569) 2026-10-14 12:06:27 +13:00
d275926f65 [mdns] Advertise the connected interface in the network TXT record (#20673)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-13 17:55:22 -05:00
Andrej Friesen f265c46c21 [PCA9505] Add PCA9505/PCA9506 40-channel GPIO expander (#20667) 2026-10-13 12:50:43 -10:00
Frédéric MetrichandClaude Opus 5.5 7d735f0677 [emontx] Lock the emonTx on reboot when its firmware supports it (#20621)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 12:37:09 -10:00
Remco van Essen 5c23a827b6 [tmc2209] Add current and diagnostic sensors (#20638) 2026-10-13 12:35:55 -10:00
Jonathan Swoboda e265c0c0fc [core] Sync the static analysis dependency pins with the code (#20681) 2026-10-13 12:30:42 -10:00
J. Nick Koston a0c7d754cb [syslog] Keep the log level to severity table in flash (#20662) 2026-10-13 12:22:35 -10:00
Jesse Hills a6dbd72c53 Merge branch 'beta' into dev 2026-10-14 11:15:09 +13:00
Jesse Hills 9d530f5c56 Merge pull request #20680 from esphome/bump-2026.10.0b4
2026.10.0b4
2026-10-14 11:14:49 +13:00
Panoplian 6562c49686 [thermostat] Allow a dry-only thermostat without cool_action (#20682) 2026-10-13 12:08:13 -10:00
guillempages a97ce3a201 [image] Fix pillow deprecation warning (#20679) 2026-10-13 11:58:21 -10:00
Jesse Hills 5a78f636b9 Bump version to 2026.10.0b4 2026-10-14 10:43:50 +13:00
tomaszduda23andClaude 53d504137b [nrf52] Accept S140 7.2.0 and 7.3.0 in the sd140_v7 DFU package (#20661)
Co-authored-by: Claude <noreply@anthropic.com>
2026-10-14 10:43:49 +13:00
bb0546fc0b [script] Keep the running script.wait item out of the queue while its chain runs (#20570)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-14 10:43:49 +13:00
emes30andJ. Nick Koston b43630cf8e [rp2_pio] Use pioasm 4.1.0 so PIO programs run on RP2350 (#19260)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-14 10:43:49 +13:00
Firat HajiyevandFirat17aze c9664d4e42 [wifi] Reject unsupported WPA3 on ESP8266 (#20592)
Co-authored-by: Firat17aze <187820330+Firat17aze@users.noreply.github.com>
2026-10-14 10:43:49 +13:00
Kevin Ahrendt 39ea6a3220 [audio] Bump microFLAC to v0.3.0 (#20591) 2026-10-14 10:43:49 +13:00
J. Nick Koston f4a6d6f18b [core] Create the PlatformIO HTTP cache dir before the prefetch resolve threads run (#20579) 2026-10-14 10:43:49 +13:00
esphome[bot] 6c24d4aa43 Bump aioesphomeapi from 46.9.0 to 46.10.0 (#20563) 2026-10-14 10:43:49 +13:00
J. Nick Koston 0dc1d3b5a1 [toshiba] Move the lookup tables to PROGMEM (#20636) 2026-10-13 11:25:18 -10:00
J. Nick Koston fa070bf340 Merge branch 'component-import-benchmarks' into binary-sensor-lazy-integration-imports 2026-10-13 11:23:16 -10:00
J. Nick Koston a31ce9bd9e Drop the components package too so the re-import is real 2026-10-13 11:23:13 -10:00
J. Nick Koston fd7fcbad0f Merge branch 'component-import-benchmarks' into binary-sensor-lazy-integration-imports 2026-10-13 11:19:26 -10:00
J. Nick Koston 5860a392bd Merge remote-tracking branch 'origin/component-import-benchmarks' into component-import-benchmarks
# Conflicts:
#	tests/benchmarks/python/test_component_import_bench.py
2026-10-13 11:19:23 -10:00
J. Nick Koston 55487971c3 Point the const_zephyr comment at esphome.const 2026-10-13 11:16:29 -10:00
J. Nick Koston 82c535e0a7 Merge branch 'component-import-benchmarks' into binary-sensor-lazy-integration-imports 2026-10-13 11:16:28 -10:00
J. Nick Koston de6991144e Time only the component import and put the original modules back afterwards 2026-10-13 11:16:05 -10:00
tomaszduda23andClaude 42341d05f9 [nrf52] Accept S140 7.2.0 and 7.3.0 in the sd140_v7 DFU package (#20661)
Co-authored-by: Claude <noreply@anthropic.com>
2026-10-13 11:12:08 -10:00
J. Nick Koston f941fee5ed [homeassistant] Add sensor platform benchmarks (#20637) 2026-10-13 11:10:14 -10:00
J. Nick Koston af1285f006 Put the session's esphome modules back after each import benchmark 2026-10-13 11:08:28 -10:00
Bonne Eggleston 8721976066 [epaper_spi] Abandon an update when the busy pin never clears (#20555) 2026-10-14 07:47:09 +11:00
J. Nick Koston 2fa7775fe7 Merge branch 'component-import-benchmarks' into binary-sensor-lazy-integration-imports 2026-10-13 10:25:49 -10:00
J. Nick Koston 926f2561b4 [core] Add CodSpeed benchmarks for importing entity components 2026-10-13 10:24:07 -10:00
J. Nick Koston f24dba9de8 Merge remote-tracking branch 'origin/dev' into binary-sensor-lazy-integration-imports
# Conflicts:
#	esphome/components/binary_sensor/__init__.py
#	esphome/components/web_server/__init__.py
#	esphome/components/zigbee/__init__.py
#	esphome/components/zigbee/const.py
#	script/ci-custom.py
#	tests/unit_tests/core/test_entity_helpers.py
2026-10-13 10:14:13 -10:00
pre-commit-ci-lite[bot] 82c3e0db96 apply automatic formatting fixes 2026-09-14 15:57:18 +00:00
J. Nick Koston e207138557 Merge remote-tracking branch 'origin/dev' into binary-sensor-lazy-integration-imports
# Conflicts:
#	esphome/components/web_server/__init__.py
2026-09-14 10:54:52 -05:00
J. Nick Koston edeaded161 Consolidate zigbee schema factory and shared integration setup 2026-08-13 21:21:17 -05:00
J. Nick Koston 09a3b1a3d1 Address review: stable schema anchors, const.py home, validator hardening 2026-08-13 21:21:17 -05:00
J. Nick Koston 10d232dc40 Cover the report deprecation warning and lazy validator paths 2026-08-13 21:21:17 -05:00
J. Nick Koston 663f462877 [binary_sensor] Defer integration imports out of module scope 2026-08-13 21:20:59 -05:00
336 changed files with 11725 additions and 1019 deletions
+1 -1
View File
@@ -32,7 +32,7 @@ runs:
# detects the activated venv via ``VIRTUAL_ENV`` so the venv layout
# downstream jobs rely on is preserved.
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
enable-cache: true
# Pull request saves land in per-PR scopes nothing else can
+2 -2
View File
@@ -29,7 +29,7 @@ jobs:
- name: Set up uv
# ``--system`` (below) installs into the setup-python interpreter;
# no venv is created or restored by this workflow.
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
enable-cache: true
# Pull-request-only workflow: a save could never be shared and
@@ -97,7 +97,7 @@ jobs:
run: git diff
- if: failure()
name: Archive artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: generated-proto-files
path: |
+2 -2
View File
@@ -128,7 +128,7 @@ jobs:
- name: Upload compile-test image artifact
if: matrix.os == 'ubuntu-24.04' && matrix.build_type == 'docker'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
# The tar is already compressed, so upload it as-is. archive: false
# skips the redundant zip and makes the file name the artifact name
@@ -210,7 +210,7 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Download image artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: compile-test-image.tar.zst
- name: Load image
+36 -22
View File
@@ -49,7 +49,7 @@ jobs:
# detects the activated venv via ``VIRTUAL_ENV`` so downstream jobs
# that ``. venv/bin/activate`` see an identical layout.
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
enable-cache: true
# Pull request saves land in per-PR scopes nothing else can
@@ -81,8 +81,8 @@ jobs:
- name: Install apt packages (cached)
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
with:
packages: libsdl2-dev ccache
version: 1.1
packages: libsdl2-dev libmbedtls-dev ccache
version: 1.2
determine-jobs:
name: Determine which jobs to run
@@ -415,8 +415,8 @@ jobs:
timeout-minutes: 10
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
with:
packages: libsdl2-dev ccache
version: 1.1
packages: libsdl2-dev libmbedtls-dev ccache
version: 1.2
- name: Set up Python 3.13
id: python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
@@ -438,7 +438,7 @@ jobs:
- name: Set up uv
# Only needed on cache miss to populate the venv.
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
enable-cache: true
# Pull request saves land in per-PR scopes nothing else can
@@ -477,7 +477,7 @@ jobs:
# 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
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: integration-test-artifacts-${{ matrix.bucket.name }}
path: test_artifacts/
@@ -488,7 +488,7 @@ jobs:
# 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
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: junit-integration-${{ strategy.job-index }}
path: junit-integration.xml
@@ -528,7 +528,7 @@ jobs:
script/check_import_time.py --check --har importtime.har
- name: Upload waterfall HAR
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: import-time-waterfall
path: importtime.har
@@ -626,7 +626,7 @@ jobs:
apt-get install -y libc6-dbg
- name: Run CodSpeed benchmarks
uses: CodSpeedHQ/action@c4fd08a3a159bd0cc208da1e0edf32b8c47d75e5 # v5.4.0
uses: CodSpeedHQ/action@f842b9a3ffe213dbb81768fd7f7eeb4dfcf1c369 # v5.4.2
with:
run: |
. venv/bin/activate
@@ -680,7 +680,7 @@ jobs:
include:
- id: clang-tidy
name: Run script/clang-tidy for ESP8266
options: --environment esp8266-arduino-tidy --grep USE_ESP8266
options: --environment esp8266-arduino-tidy --grep USE_ESP8266 --grep USE_SOCKET_IMPL_LWIP_TCP --grep USE_SHD_FIRMWARE_DATA
pio_cache_key: tidyesp8266
- id: clang-tidy
name: Run script/clang-tidy for ESP32 Arduino
@@ -693,12 +693,16 @@ jobs:
ignore_errors: false
- id: clang-tidy
name: Run script/clang-tidy for RP2
options: --environment rp2-tidy --grep USE_RP2
options: --environment rp2-tidy --grep USE_RP2 --grep USE_BLE_SCAN_RESPONSE_MERGER
pio_cache_key: tidyrp2
- id: clang-tidy
name: Run script/clang-tidy for host
options: --environment host-tidy --grep USE_HOST
pio_cache_key: tidyhost
- id: clang-tidy
name: Run script/clang-tidy for LibreTiny
environments: bk72xx-tidy ln882h-tidy rtl87xxb-tidy rtl87xxc-tidy
options: --grep USE_LIBRETINY --grep USE_BK72XX --grep USE_RTL87XX --grep USE_LN882X
options: --grep USE_LIBRETINY --grep USE_BK72XX --grep USE_RTL87XX --grep USE_LN882X --grep USE_LN882H_BLE --grep USE_SOCKET_IMPL_LWIP_SOCKETS
pio_cache_key: tidylibretiny
steps:
@@ -707,6 +711,16 @@ jobs:
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
- name: Install apt packages (cached)
if: matrix.pio_cache_key == 'tidyhost'
# libsdl2-dev is needed by the sdl component on the host, libmbedtls-dev
# by dsmr and dlms_meter. Packages and version must match seed-apt-cache
# exactly.
timeout-minutes: 10
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
with:
packages: libsdl2-dev libmbedtls-dev ccache
version: 1.2
- name: Restore Python
id: restore-python
@@ -989,14 +1003,14 @@ jobs:
- id: clang-tidy
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
options: --environment esp32s3-idf-tidy --grep SOC_TEMP_SENSOR_SUPPORTED --grep USE_ESP32_VARIANT_ESP32S3 --grep USE_LOGGER_USB_CDC --grep SOC_I2S_SUPPORTS_TDM
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
options: --environment esp32p4-idf-tidy --grep USE_ESP32_VARIANT_ESP32P4 --grep CONFIG_IDF_TARGET_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
@@ -1102,8 +1116,8 @@ jobs:
timeout-minutes: 10
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
with:
packages: libsdl2-dev ccache
version: 1.1
packages: libsdl2-dev libmbedtls-dev ccache
version: 1.2
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -1448,7 +1462,7 @@ jobs:
# install step (order-of-magnitude faster on cold boots,
# with its own wheel cache). actions/setup-python still
# provides the interpreter.
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
enable-cache: true
# Pull request saves land in per-PR scopes nothing else can
@@ -1647,7 +1661,7 @@ jobs:
fi
- name: Upload memory analysis JSON
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: memory-analysis-target
path: memory-analysis-target.json
@@ -1710,7 +1724,7 @@ jobs:
--platform "$platform"
- name: Upload memory analysis JSON
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: memory-analysis-pr
path: memory-analysis-pr.json
@@ -1740,13 +1754,13 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Download target analysis JSON
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: memory-analysis-target
path: ./memory-analysis
continue-on-error: true
- name: Download PR analysis JSON
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: memory-analysis-pr
path: ./memory-analysis
+2 -2
View File
@@ -56,7 +56,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
uses: github/codeql-action/init@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
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@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
uses: github/codeql-action/analyze@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
with:
category: "/language:${{matrix.language}}"
+2 -2
View File
@@ -162,7 +162,7 @@ jobs:
# version: ${{ needs.init.outputs.tag }}
- name: Upload digests
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: digests-${{ matrix.platform.arch }}
path: /tmp/digests
@@ -195,7 +195,7 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Download digests
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
pattern: digests-*
path: /tmp/digests
+1 -1
View File
@@ -47,7 +47,7 @@ jobs:
# setup-python interpreter so subsequent ``prek`` /
# ``script/run-in-env.py`` steps find the deps without a
# ``uv run`` prefix.
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
enable-cache: true
# Pin uv version so the action does not have to fetch the
+2 -2
View File
@@ -10,7 +10,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.16.10
rev: v0.17.0
hooks:
# Run the linter.
- id: ruff
@@ -36,7 +36,7 @@ repos:
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/asottile/pyupgrade
rev: v3.21.2
rev: v3.22.0
hooks:
- id: pyupgrade
args: [--py312-plus]
+4
View File
@@ -219,6 +219,7 @@ esphome/components/friedrich/* @rwrozelle
esphome/components/fs3000/* @kahrendt
esphome/components/ft5x06/* @clydebarrow
esphome/components/ft63x6/* @gpambrozio
esphome/components/fusb302b/* @remcom
esphome/components/gcja5/* @gcormier
esphome/components/gdk101/* @Szewcson
esphome/components/gl_r01_i2c/* @pkejval
@@ -240,6 +241,7 @@ esphome/components/gt911/* @clydebarrow @jesserockz
esphome/components/haier/* @paveldn
esphome/components/haier/binary_sensor/* @paveldn
esphome/components/haier/button/* @paveldn
esphome/components/haier/select/* @paveldn
esphome/components/haier/sensor/* @paveldn
esphome/components/haier/switch/* @paveldn
esphome/components/haier/text_sensor/* @paveldn
@@ -385,6 +387,7 @@ esphome/components/microphone/* @jesserockz @kahrendt
esphome/components/mics_4514/* @jesserockz
esphome/components/midea/* @dudanov
esphome/components/midea_ir/* @dudanov
esphome/components/midea_portasplit/* @Fexiven
esphome/components/mipi_dsi/* @clydebarrow
esphome/components/mipi_rgb/* @clydebarrow
esphome/components/mipi_spi/* @clydebarrow
@@ -439,6 +442,7 @@ esphome/components/ota/* @esphome/core
esphome/components/output/* @esphome/core
esphome/components/packet_transport/* @clydebarrow
esphome/components/pca6416a/* @Mat931
esphome/components/pca9505/* @ajfriesen @ssieb
esphome/components/pca9554/* @bdraco @clydebarrow @hwstar
esphome/components/pcf85063/* @brogon
esphome/components/pcf8563/* @KoenBreeman
+1 -1
View File
@@ -338,7 +338,7 @@ async def to_code(config: ConfigType) -> None:
# HTTPS streams verify the server against the root certificate bundle
require_certificate_bundle()
add_idf_component(name="esphome/esp-audio-libs", ref="4.0.1")
add_idf_component(name="esphome/esp-audio-libs", ref="4.1.0")
data = _get_data()
+2 -1
View File
@@ -1,10 +1,11 @@
#pragma once
#include <set>
#include "esphome/core/log.h"
namespace esphome::bedjet {
static const char *const TAG = "bedjet";
ESPHOME_LOG_TAG(TAG, "bedjet");
/// Converts a BedJet fan step to a speed percentage, in the range of 5% to 100%.
inline static uint8_t bedjet_fan_step_to_speed(const uint8_t fan) {
+11 -13
View File
@@ -3,7 +3,6 @@ from logging import getLogger
from esphome import automation, core
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
from esphome.components import mqtt, web_server, zigbee
from esphome.components.const import CONF_ON_STATE_CHANGE
from esphome.config_helpers import filter_source_files_from_defines
import esphome.config_validation as cv
@@ -29,7 +28,6 @@ from esphome.const import (
CONF_STATE,
CONF_TIMING,
CONF_TRIGGER_ID,
CONF_WEB_SERVER,
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_BATTERY_CHARGING,
DEVICE_CLASS_CARBON_MONOXIDE,
@@ -61,10 +59,12 @@ from esphome.const import (
DEVICE_CLASS_VIBRATION,
DEVICE_CLASS_WINDOW,
)
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority, entity_helpers
from esphome.core.entity_helpers import (
SubEntities,
entity_duplicate_validator,
lazy_load_validator,
mqtt_component_class,
queue_entity_register,
setup_device_class,
setup_entity,
@@ -433,14 +433,14 @@ def validate_publish_initial_state(value):
_BINARY_SENSOR_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
cv.ENTITY_BASE_SCHEMA.extend(entity_helpers.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMPONENT_SCHEMA)
.extend(zigbee.BINARY_SENSOR_SCHEMA)
.extend(entity_helpers.ZIGBEE_BINARY_SENSOR_SCHEMA)
.extend(
{
cv.GenerateID(): cv.declare_id(BinarySensor),
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
mqtt.MQTTBinarySensorComponent
mqtt_component_class("MQTTBinarySensorComponent")
),
cv.Exclusive(
CONF_PUBLISH_INITIAL_STATE, CONF_TRIGGER_ON_INITIAL_STATE
@@ -508,7 +508,7 @@ _BINARY_SENSOR_SCHEMA = (
_BINARY_SENSOR_SCHEMA.add_extra(entity_duplicate_validator("binary_sensor"))
_BINARY_SENSOR_SCHEMA.add_extra(zigbee.validate_binary_sensor)
_BINARY_SENSOR_SCHEMA.add_extra(lazy_load_validator("zigbee", "validate_binary_sensor"))
def binary_sensor_schema(
@@ -619,14 +619,12 @@ async def setup_binary_sensor_core_(var, config):
CORE.add_job(_build_binary_sensor_automations, var, config)
if mqtt_id := config.get(CONF_MQTT_ID):
mqtt_ = cg.new_Pvariable(mqtt_id, var)
await mqtt.register_mqtt_component(mqtt_, config)
await entity_helpers.setup_entity_integrations(var, config)
if web_server_config := config.get(CONF_WEB_SERVER):
await web_server.add_entity_config(var, web_server_config)
if "zigbee" in CORE.loaded_integrations:
from esphome.components import zigbee
await zigbee.setup_binary_sensor(var, config)
await zigbee.setup_binary_sensor(var, config)
async def register_binary_sensor(var, config):
+1 -1
View File
@@ -4,7 +4,7 @@
namespace esphome::ble_client {
const char *const Automation::TAG = "ble_client.automation";
const char *const Automation::TAG = "ble_client.automation"; // NOLINT: ble_client never builds for ESP8266
} // namespace esphome::ble_client
+3 -2
View File
@@ -100,7 +100,8 @@ void BM8563::get_date_(ESPTime &time) {
this->read_register(DATE_FIRST_REG, buf, sizeof(buf));
time.day_of_month = this->bcd2_to_byte_(buf[0] & 0x3f);
time.day_of_week = this->bcd2_to_byte_(buf[1] & 0x07);
// The chip counts weekdays 0-6 from Sunday (not BCD), ESPTime counts 1-7
time.day_of_week = (buf[1] & 0x07) + 1;
time.month = this->bcd2_to_byte_(buf[2] & 0x1f);
uint8_t year_byte = this->bcd2_to_byte_(buf[3] & 0xff);
@@ -115,7 +116,7 @@ void BM8563::get_date_(ESPTime &time) {
void BM8563::set_date_(const ESPTime &time) {
uint8_t buf[4] = {
this->byte_to_bcd2_(time.day_of_month),
this->byte_to_bcd2_(time.day_of_week),
static_cast<uint8_t>(time.day_of_week - 1),
this->byte_to_bcd2_(time.month),
this->byte_to_bcd2_(time.year % 100),
};
+2 -1
View File
@@ -2,10 +2,11 @@
#include "esphome/components/bme280_base/bme280_base.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/core/log.h"
namespace esphome::bme280_i2c {
static const char *const TAG = "bme280_i2c.sensor";
ESPHOME_LOG_TAG(TAG, "bme280_i2c.sensor");
class BME280I2CComponent final : public esphome::bme280_base::BME280Component, public i2c::I2CDevice {
bool read_byte(uint8_t a_register, uint8_t *data) override;
+2 -1
View File
@@ -2,10 +2,11 @@
#include "esphome/components/bmp280_base/bmp280_base.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/core/log.h"
namespace esphome::bmp280_i2c {
static const char *const TAG = "bmp280_i2c.sensor";
ESPHOME_LOG_TAG(TAG, "bmp280_i2c.sensor");
/// This class implements support for the BMP280 Temperature+Pressure i2c sensor.
class BMP280I2CComponent final : public esphome::bmp280_base::BMP280Component, public i2c::I2CDevice {
+2 -1
View File
@@ -2,10 +2,11 @@
#include "esphome/components/bmp581_base/bmp581_base.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/core/log.h"
namespace esphome::bmp581_i2c {
static const char *const TAG = "bmp581_i2c.sensor";
ESPHOME_LOG_TAG(TAG, "bmp581_i2c.sensor");
/// This class implements support for the BMP581 Temperature+Pressure i2c sensor.
class BMP581I2CComponent final : public esphome::bmp581_base::BMP581Component, public i2c::I2CDevice {
@@ -8,7 +8,7 @@
namespace esphome::chsc6x {
static const char *const TAG = "chsc6x.touchscreen";
ESPHOME_LOG_TAG(TAG, "chsc6x.touchscreen");
static const uint8_t CHSC6X_REG_STATUS = 0x00;
static const uint8_t CHSC6X_REG_STATUS_TOUCH = 0x00;
+22 -4
View File
@@ -8,6 +8,9 @@ namespace esphome::cse7766 {
ESPHOME_LOG_TAG(TAG, "cse7766");
// The chip only sends the time since the last pulse once a cycle is longer than 1 s (in us)
static constexpr uint32_t MIN_HELD_POWER_CYCLE = 1000000;
void CSE7766Component::loop() {
const uint32_t now = App.get_loop_component_start_time();
if (now - this->last_transmission_ >= 500) {
@@ -154,18 +157,33 @@ void CSE7766Component::parse_data_() {
if (power_cycle_exceeds_range) {
// Datasheet: power cycle exceeding range means active power is 0
have_power = true;
this->last_power_cycle_ = 0;
if (this->power_sensor_ != nullptr) {
this->power_sensor_->publish_state(0.0f);
}
} else if (have_power) {
power = power_coeff / float(power_cycle);
this->last_power_cycle_ = power_cycle;
if (this->power_sensor_ != nullptr) {
this->power_sensor_->publish_state(power);
}
} else if (this->power_sensor_ != nullptr) {
// No valid power measurement from chip - publish 0W to avoid stale readings
// This typically happens when current is below the measurable threshold (~50mA)
this->power_sensor_->publish_state(0.0f);
} else if (this->last_power_cycle_ > MIN_HELD_POWER_CYCLE && power_cycle <= 2 * this->last_power_cycle_) {
// No new measurement in this frame. With cycles over 1 s the register holds the last
// cycle or the time elapsed since the last pulse. While that time is within twice the
// last cycle, the next pulse is not overdue, so the last measurement still applies.
// Shorter cycles never get here in normal operation and keep the previous behaviour.
have_power = true;
power = power_coeff / float(this->last_power_cycle_);
if (this->power_sensor_ != nullptr) {
this->power_sensor_->publish_state(power);
}
} else {
// The next pulse is overdue: the load has dropped, report 0 W. A load that drops to less
// than half also reads 0 W here until its first new pulse arrives.
this->last_power_cycle_ = 0;
if (this->power_sensor_ != nullptr) {
this->power_sensor_->publish_state(0.0f);
}
}
float current = 0.0f;
+3 -1
View File
@@ -36,6 +36,7 @@ class CSE7766Component final : public Component, public uart::UARTDevice {
uint8_t raw_data_[CSE7766_RAW_DATA_SIZE];
uint8_t raw_data_index_{0};
uint16_t cf_pulses_last_{0};
uint32_t last_transmission_{0};
sensor::Sensor *voltage_sensor_{nullptr};
sensor::Sensor *current_sensor_{nullptr};
@@ -45,7 +46,8 @@ class CSE7766Component final : public Component, public uart::UARTDevice {
sensor::Sensor *reactive_power_sensor_{nullptr};
sensor::Sensor *power_factor_sensor_{nullptr};
uint32_t cf_pulses_total_{0};
uint16_t cf_pulses_last_{0};
// Power cycle of the last frame that carried a new power measurement, 0 if none
uint32_t last_power_cycle_{0};
};
} // namespace esphome::cse7766
@@ -8,7 +8,7 @@
namespace esphome::cst816 {
static const char *const TAG = "cst816.touchscreen";
ESPHOME_LOG_TAG(TAG, "cst816.touchscreen");
static const uint8_t REG_STATUS = 0x00;
static const uint8_t REG_TOUCH_NUM = 0x02;
@@ -8,7 +8,7 @@
namespace esphome::cst9220 {
static const char *const TAG = "cst9220.touchscreen";
ESPHOME_LOG_TAG(TAG, "cst9220.touchscreen");
// The CST92xx family uses 16-bit (big-endian) register addresses.
static const uint16_t REG_TOUCH_DATA = 0xD000; // touch report
+36
View File
@@ -250,6 +250,42 @@ def add_metadata(
)
KEY_EXTERNAL_WRITERS = "external_writers"
def _get_external_writer_list() -> list[tuple[ID, str]]:
"""Get the raw external writer list. Each entry is (display id, owner)."""
return CORE.data.setdefault(DOMAIN, {}).setdefault(KEY_EXTERNAL_WRITERS, [])
def register_external_writer(display_id: ID, owner: str) -> None:
"""Declare that ``owner`` draws into the display from outside its lambda or pages.
Call from the owner's CONFIG_SCHEMA. The ID may still be unresolved; the ID pass
resolves it in place before final validation.
"""
_get_external_writer_list().append((display_id, owner))
def get_external_writers(display_id: ID) -> list[str]:
"""Get the owners registered for a display. Only valid once IDs are resolved."""
return [
owner
for id_, owner in _get_external_writer_list()
if id_.id is not None and id_.id == display_id.id
]
def has_external_writer(display_id: ID) -> bool:
"""Tell a driver's final validation that something else draws into the display.
LVGL anywhere in the config also counts, as the drivers assumed before.
"""
from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN
return LVGL_DOMAIN in full_config.get() or bool(get_external_writers(display_id))
async def register_display(var, config):
await cg.register_component(var, config)
await setup_display_core_(var, config)
+53 -27
View File
@@ -5,6 +5,7 @@
#include "display_color_utils.h"
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::display {
@@ -52,40 +53,65 @@ void Display::line_at_angle(int x, int y, int angle, int start_radius, int stop_
this->line(x1, y1, x2, y2, color);
}
void Display::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
size_t line_stride = x_offset + w + x_pad; // length of each source line in pixels
uint32_t color_value;
// flatten keeps to_color() inlined in the pixel loop; in PSRAM builds GCC at
// -Os stops inlining it once there are two call sites, which costs a call and
// spills per pixel. Without PSRAM there is one call site and it changes
// nothing, and draw_pixel_at() stays a virtual call either way.
template<ColorBitness BITNESS>
static void __attribute__((flatten))
draw_packed_pixels(Display *display, int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
bool big_endian, int x_offset, int y_offset, int x_pad) {
const size_t line_stride = x_offset + w + x_pad; // length of each source line in pixels
// BITNESS is a constant here, so to_color()'s three per pixel divisions
// become a multiply and shift and its bitness switch folds.
const auto draw_source_pixel = [&](int x, int y, size_t source_idx) ESPHOME_ALWAYS_INLINE {
display->draw_pixel_at(
x + x_start, y + y_start,
ColorUtil::to_color(ColorUtil::read_packed<BITNESS>(ptr, source_idx, big_endian), order, BITNESS));
};
// The source and frame buffers are row-major, so walking rows keeps the read
// and the write sequential. When the display swaps the axes before writing,
// walking columns keeps the write sequential instead; with the frame buffer
// in PSRAM a strided write costs more than a strided read.
#ifdef USE_PSRAM
if (display->pixel_axes_swapped()) {
size_t column_idx = y_offset * line_stride + x_offset;
for (int x = 0; x != w; x++, column_idx++) {
size_t source_idx = column_idx;
for (int y = 0; y != h; y++, source_idx += line_stride) {
draw_source_pixel(x, y, source_idx);
}
}
return;
}
#endif
// Row walk: builds without PSRAM, and PSRAM builds whose axes are not swapped.
for (int y = 0; y != h; y++) {
size_t source_idx = (y_offset + y) * line_stride + x_offset;
size_t source_idx_mod;
for (int x = 0; x != w; x++, source_idx++) {
switch (bitness) {
default:
color_value = ptr[source_idx];
break;
case COLOR_BITNESS_565:
source_idx_mod = source_idx * 2;
if (big_endian) {
color_value = (ptr[source_idx_mod] << 8) + ptr[source_idx_mod + 1];
} else {
color_value = ptr[source_idx_mod] + (ptr[source_idx_mod + 1] << 8);
}
break;
case COLOR_BITNESS_888:
source_idx_mod = source_idx * 3;
if (big_endian) {
color_value = (ptr[source_idx_mod + 0] << 16) + (ptr[source_idx_mod + 1] << 8) + ptr[source_idx_mod + 2];
} else {
color_value = ptr[source_idx_mod + 0] + (ptr[source_idx_mod + 1] << 8) + (ptr[source_idx_mod + 2] << 16);
}
break;
}
this->draw_pixel_at(x + x_start, y + y_start, ColorUtil::to_color(color_value, order, bitness));
draw_source_pixel(x, y, source_idx);
}
}
}
void Display::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
switch (bitness) {
case COLOR_BITNESS_565:
draw_packed_pixels<COLOR_BITNESS_565>(this, x_start, y_start, w, h, ptr, order, big_endian, x_offset, y_offset,
x_pad);
break;
case COLOR_BITNESS_888:
draw_packed_pixels<COLOR_BITNESS_888>(this, x_start, y_start, w, h, ptr, order, big_endian, x_offset, y_offset,
x_pad);
break;
default:
draw_packed_pixels<COLOR_BITNESS_332>(this, x_start, y_start, w, h, ptr, order, big_endian, x_offset, y_offset,
x_pad);
break;
}
}
void HOT Display::horizontal_line(int x, int y, int width, Color color) {
// Future: Could be made more efficient by manipulating buffer directly in certain rotations.
for (int i = x; i < x + width; i++)
+11
View File
@@ -139,6 +139,11 @@ enum DisplayRotation {
DISPLAY_ROTATION_270_DEGREES = 270,
};
/// Whether a rotation swaps the x and y axes.
constexpr bool rotation_swaps_axes(DisplayRotation rotation) {
return rotation == DISPLAY_ROTATION_90_DEGREES || rotation == DISPLAY_ROTATION_270_DEGREES;
}
const int EDGES_TRIGON = 3;
const int EDGES_TRIANGLE = 3;
const int EDGES_TETRAGON = 4;
@@ -717,6 +722,12 @@ class Display : public PollingComponent {
virtual DisplayRotation get_rotation() const { return this->rotation_; }
/// True when draw_pixel_at() swaps x and y before writing, so pixels that are
/// neighbours along x land a row apart in the frame buffer. Not the same as
/// get_width()/get_height() swapping: a display that rotates in hardware
/// reports swapped dimensions but writes unswapped pixels.
virtual bool pixel_axes_swapped() const { return rotation_swaps_axes(this->rotation_); }
/** Get the type of display that the buffer corresponds to. In case of dynamically configurable displays,
* returns the type the display is currently configured to.
*/
@@ -2,6 +2,7 @@
#include <array>
#include <cstdint>
#include "esphome/core/color.h"
#include "esphome/core/helpers.h"
namespace esphome::display {
enum ColorOrder : uint8_t { COLOR_ORDER_RGB = 0, COLOR_ORDER_BGR = 1, COLOR_ORDER_GRB = 2 };
@@ -10,6 +11,27 @@ inline static uint8_t esp_scale(uint8_t i, uint8_t scale, uint8_t max_value = 25
class ColorUtil {
public:
/// Reads one packed pixel of the given bitness from a source buffer into the
/// value to_color() takes. BITNESS is a template argument so the byte layout
/// folds; pass it on to to_color() as a constant too, and its channel scaling
/// divides by a constant, which the compiler turns into a multiply and shift.
template<ColorBitness BITNESS>
static ESPHOME_ALWAYS_INLINE uint32_t read_packed(const uint8_t *ptr, size_t index, bool big_endian) {
if constexpr (BITNESS == COLOR_BITNESS_565) {
const size_t idx = index * 2;
if (big_endian)
return (ptr[idx] << 8) + ptr[idx + 1];
return ptr[idx] + (ptr[idx + 1] << 8);
} else if constexpr (BITNESS == COLOR_BITNESS_888) {
const size_t idx = index * 3;
if (big_endian)
return (ptr[idx + 0] << 16) + (ptr[idx + 1] << 8) + ptr[idx + 2];
return ptr[idx + 0] + (ptr[idx + 1] << 8) + (ptr[idx + 2] << 16);
} else {
return ptr[index];
}
}
static Color to_color(uint32_t colorcode, ColorOrder color_order,
ColorBitness color_bitness = ColorBitness::COLOR_BITNESS_888, bool right_bit_aligned = true) {
uint8_t first_color, second_color, third_color;
+15 -3
View File
@@ -23,11 +23,18 @@ DEPENDENCIES = ["uart"]
emontx_ns = cg.esphome_ns.namespace("emontx")
EmonTx = emontx_ns.class_("EmonTx", cg.Component, uart.UARTDevice)
LockOnReboot = emontx_ns.enum("LockOnReboot", is_class=True)
CONF_EMONTX_ID = "emontx_id"
CONF_TAG_NAME = "tag_name"
CONF_ON_JSON = "on_json"
CONF_LOCK_ON_REBOOT = "lock_on_reboot"
LOCK_ON_REBOOT_AUTO = "auto"
LOCK_ON_REBOOT_MODES = {
LOCK_ON_REBOOT_AUTO: LockOnReboot.LOCK_ON_REBOOT_AUTO,
True: LockOnReboot.LOCK_ON_REBOOT_ALWAYS,
False: LockOnReboot.LOCK_ON_REBOOT_NEVER,
}
DOMAIN = "emontx"
@@ -59,7 +66,9 @@ CONFIG_SCHEMA = (
cv.GenerateID(): cv.declare_id(EmonTx),
cv.Optional(CONF_ON_JSON): _synchronous_automation_schema(),
cv.Optional(CONF_ON_DATA): _synchronous_automation_schema(),
cv.Optional(CONF_LOCK_ON_REBOOT, default=False): cv.boolean,
cv.Optional(CONF_LOCK_ON_REBOOT, default=LOCK_ON_REBOOT_AUTO): cv.Any(
cv.boolean, cv.one_of(LOCK_ON_REBOOT_AUTO, lower=True)
),
}
)
.extend(cv.COMPONENT_SCHEMA)
@@ -135,8 +144,11 @@ async def to_code(config: ConfigType) -> None:
await uart.register_uart_device(var, config)
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
if config[CONF_LOCK_ON_REBOOT]:
cg.add(var.set_lock_on_reboot(True))
lock = config[CONF_LOCK_ON_REBOOT]
cg.add(var.set_lock_on_reboot(LOCK_ON_REBOOT_MODES[lock]))
if lock == LOCK_ON_REBOOT_AUTO:
# Locking depends on the firmware version the emonTx reports
cg.add_define("USE_EMONTX_VERSION")
# Action: emontx.send_command
+129 -27
View File
@@ -7,30 +7,85 @@ namespace esphome::emontx {
ESPHOME_LOG_TAG(TAG, "emontx");
static constexpr uint32_t UNLOCK_ID = 0;
// The emonTx joins bytes received while a command is pending into that command, so the next one waits
static constexpr uint32_t COMMAND_GAP_MS = 250;
#ifdef USE_EMONTX_VERSION
static constexpr uint32_t VERSION_REQUEST_ID = 0;
// The emonTx joins bytes received while a command is pending into that command, so 'v' waits
static constexpr uint32_t VERSION_REQUEST_DELAY_MS = 250;
static constexpr uint32_t VERSION_REQUEST_ID = 1;
// Without a reply, 'v' is sent again every VERSION_RETRY_FRAMES data lines, VERSION_MAX_ATTEMPTS times in all
static constexpr uint8_t VERSION_RETRY_FRAMES = 2;
static constexpr uint8_t VERSION_MAX_ATTEMPTS = 3;
// Line of the 'v' reply (and of the boot banner) carrying the release, e.g. " - Version: v1.1.1"
static const char VERSION_PREFIX[] PROGMEM = " - Version:";
// Every data line starts with the message counter, in JSON or text format
static const char MSG_JSON_PREFIX[] PROGMEM = "{\"MSG\":";
static const char MSG_TEXT_PREFIX[] PROGMEM = "MSG:";
// First firmware with 'emonlock' and 'emonunlock'; older firmware takes them for the bootloader command
static constexpr uint32_t LOCK_MIN_MAJOR = 1;
static constexpr uint32_t LOCK_MIN_MINOR = 2;
// True if line starts with the PROGMEM prefix and has more after it
static bool starts_with(const char *line, size_t len, const char *prefix, size_t prefix_len) {
return len > prefix_len && ESPHOME_strncmp_P(line, prefix, prefix_len) == 0;
}
// Reads the decimal number (up to 9 digits) at pos and moves pos past it; false if there is none
static bool read_number(const char *&pos, const char *end, uint32_t &value) {
const char *start = pos;
value = 0;
while (pos < end && pos - start < 9 && *pos >= '0' && *pos <= '9')
value = value * 10 + (*pos++ - '0');
return pos != start;
}
// True for a release such as "v1.2.0" or "v1.3.0-rc1" from LOCK_MIN_MAJOR.LOCK_MIN_MINOR on
static bool supports_lock(const char *version, const char *end) {
if (version < end && (*version == 'v' || *version == 'V'))
version++;
uint32_t major;
if (!read_number(version, end, major) || version == end || *version != '.')
return false;
version++;
uint32_t minor;
if (!read_number(version, end, minor))
return false;
return major > LOCK_MIN_MAJOR || (major == LOCK_MIN_MAJOR && minor >= LOCK_MIN_MINOR);
}
#endif
void EmonTx::setup() {
this->buffer_pos_ = 0;
if (this->lock_on_reboot_) {
// The line feed ends any boot noise first
ESP_LOGI(TAG, "Unlocking emonTx after boot");
if (this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_ALWAYS) {
// The line feed ends any boot noise
this->write_byte('\n');
this->send_command("emonunlock");
this->set_timeout(UNLOCK_ID, COMMAND_GAP_MS, [this]() {
this->unlock_();
#ifdef USE_EMONTX_VERSION
if (this->wants_version_())
this->request_version_();
#endif
});
return;
}
#ifdef USE_EMONTX_VERSION
if (this->version_text_sensor_ != nullptr)
// With AUTO, the reply decides whether to unlock
if (this->wants_version_())
this->request_version_();
#endif
}
void EmonTx::unlock_() {
ESP_LOGI(TAG, "Unlocking emonTx");
this->send_command("emonunlock");
}
void EmonTx::on_shutdown() {
if (!this->lock_on_reboot_)
bool lock = this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_ALWAYS;
#ifdef USE_EMONTX_VERSION
lock |= this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_AUTO && this->lock_supported_;
#endif
if (!lock)
return;
if (this->is_paused()) {
ESP_LOGW(TAG, "Not locking emonTx before shutdown, UART is paused");
@@ -63,7 +118,7 @@ void EmonTx::loop() {
this->discarding_line_ = dropped != '\n';
#ifdef USE_EMONTX_VERSION
// The emonTx may have been flashed, or a request dropped, during the pause
if (this->version_text_sensor_ != nullptr)
if (this->wants_version_())
this->request_version_();
#endif
}
@@ -96,8 +151,10 @@ void EmonTx::loop() {
this->parse_json_(this->buffer_.data(), len);
}
#ifdef USE_EMONTX_VERSION
if (this->version_text_sensor_ != nullptr)
if (this->wants_version_()) {
this->parse_msg_number_(this->buffer_.data(), len);
this->parse_version_line_(this->buffer_.data(), len);
}
#endif
}
} else if (received == '\r' || this->discarding_line_) {
@@ -124,16 +181,6 @@ void EmonTx::parse_json_(const char *data, size_t len) {
}
}
#endif
#ifdef USE_EMONTX_VERSION
auto msg = root["MSG"];
if (this->version_text_sensor_ != nullptr && msg.is<uint32_t>()) {
// MSG restarts at 1 when the emonTx reboots, as it does after a firmware update
uint32_t msg_number = msg;
if (msg_number <= this->last_msg_number_)
this->request_version_();
this->last_msg_number_ = msg_number;
}
#endif
this->json_callbacks_.call(root, StringRef(data, len));
return true;
@@ -145,17 +192,50 @@ void EmonTx::parse_json_(const char *data, size_t len) {
}
#ifdef USE_EMONTX_VERSION
// The line feed ends any pending input; the reply is picked up in loop() by parse_version_line_()
void EmonTx::request_version_() {
// The firmware may have changed; until the reply, an unknown version gets no lock commands
this->lock_supported_ = false;
this->version_frames_left_ = VERSION_RETRY_FRAMES * VERSION_MAX_ATTEMPTS;
this->send_version_request_();
}
// The line feed ends any pending input; the reply is picked up in loop() by parse_version_line_()
void EmonTx::send_version_request_() {
if (this->is_paused())
return;
this->write_byte('\n');
this->set_timeout(VERSION_REQUEST_ID, VERSION_REQUEST_DELAY_MS, [this]() { this->send_command("v"); });
this->set_timeout(VERSION_REQUEST_ID, COMMAND_GAP_MS, [this]() { this->send_command("v"); });
}
void EmonTx::parse_msg_number_(const char *line, size_t len) {
const char *pos;
if (starts_with(line, len, MSG_JSON_PREFIX, sizeof(MSG_JSON_PREFIX) - 1)) {
pos = line + sizeof(MSG_JSON_PREFIX) - 1;
} else if (starts_with(line, len, MSG_TEXT_PREFIX, sizeof(MSG_TEXT_PREFIX) - 1)) {
pos = line + sizeof(MSG_TEXT_PREFIX) - 1;
} else {
return;
}
uint32_t msg_number;
if (!read_number(pos, line + len, msg_number))
return;
// MSG restarts at 1 when the emonTx reboots, as it does after a firmware update
if (msg_number <= this->last_msg_number_) {
this->request_version_();
} else if (this->version_frames_left_ > 0) {
if (--this->version_frames_left_ == 0) {
ESP_LOGW(TAG, "No firmware version from the emonTx, it may still be locked");
} else if (this->version_frames_left_ % VERSION_RETRY_FRAMES == 0) {
this->send_version_request_();
}
}
this->last_msg_number_ = msg_number;
}
void EmonTx::parse_version_line_(const char *line, size_t len) {
constexpr size_t prefix_len = sizeof(VERSION_PREFIX) - 1;
if (len <= prefix_len || ESPHOME_strncmp_P(line, VERSION_PREFIX, prefix_len) != 0)
if (!starts_with(line, len, VERSION_PREFIX, prefix_len))
return;
const char *version = line + prefix_len;
const char *end = line + len;
@@ -164,7 +244,23 @@ void EmonTx::parse_version_line_(const char *line, size_t len) {
if (version == end)
return;
ESP_LOGI(TAG, "emonTx firmware version: %.*s", static_cast<int>(end - version), version);
this->version_text_sensor_->publish_state(version, end - version);
this->version_frames_left_ = 0;
#ifdef USE_EMONTX_VERSION_TEXT_SENSOR
if (this->version_text_sensor_ != nullptr)
this->version_text_sensor_->publish_state(version, end - version);
#endif
bool supported = supports_lock(version, end);
if (this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_AUTO && supported && !this->unlock_sent_) {
// Once the rest of the reply has gone out; a pause drops it, and the version request on resume retries
this->set_timeout(UNLOCK_ID, COMMAND_GAP_MS, [this]() {
if (this->is_paused())
return;
this->unlock_();
this->unlock_sent_ = true;
});
}
this->lock_supported_ = supported;
}
#endif
@@ -173,7 +269,13 @@ void EmonTx::parse_version_line_(const char *line, size_t len) {
*/
void EmonTx::dump_config() {
ESP_LOGCONFIG(TAG, "EmonTx:");
ESP_LOGCONFIG(TAG, " Lock on reboot: %s", YESNO(this->lock_on_reboot_));
const char *lock = LOG_STR_LITERAL("no");
if (this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_ALWAYS) {
lock = LOG_STR_LITERAL("yes");
} else if (this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_AUTO) {
lock = LOG_STR_LITERAL("auto");
}
ESP_LOGCONFIG(TAG, " Lock on reboot: %s", lock);
#ifdef USE_SENSOR
ESP_LOGCONFIG(TAG, " Registered sensors: %zu", this->sensors_.size());
@@ -183,7 +285,7 @@ void EmonTx::dump_config() {
#else
ESP_LOGCONFIG(TAG, " Sensor support: DISABLED");
#endif
#ifdef USE_EMONTX_VERSION
#ifdef USE_EMONTX_VERSION_TEXT_SENSOR
LOG_TEXT_SENSOR(" ", "Firmware version", this->version_text_sensor_);
#endif
}
+24 -5
View File
@@ -13,7 +13,7 @@
#ifdef USE_SENSOR
#include "esphome/components/sensor/sensor.h"
#endif
#ifdef USE_EMONTX_VERSION
#ifdef USE_EMONTX_VERSION_TEXT_SENSOR
#include "esphome/components/text_sensor/text_sensor.h"
#endif
@@ -22,6 +22,12 @@ namespace esphome::emontx {
/// Maximum line length in bytes (plus one byte reserved for null terminator)
static constexpr size_t MAX_LINE_LENGTH = 1024;
enum class LockOnReboot : uint8_t {
LOCK_ON_REBOOT_NEVER,
LOCK_ON_REBOOT_ALWAYS,
LOCK_ON_REBOOT_AUTO, // Only when the emonTx reports firmware v1.2.0 or later
};
/**
* @class EmonTx
* @brief Main class for the EmonTx component.
@@ -30,7 +36,7 @@ static constexpr size_t MAX_LINE_LENGTH = 1024;
* extracts tags and values, and publishes them to registered sensors.
*/
class EmonTx final : public Component, public uart::UARTDevice {
#ifdef USE_EMONTX_VERSION
#ifdef USE_EMONTX_VERSION_TEXT_SENSOR
SUB_TEXT_SENSOR(version)
#endif
public:
@@ -56,8 +62,8 @@ class EmonTx final : public Component, public uart::UARTDevice {
bool is_paused() const { return this->paused_.load(std::memory_order_relaxed); }
/// Lock the emonTx ('emonlock') before a reboot, so serial garbage during the reboot is not taken
/// as a command, and unlock it in setup(). Needs emonTx firmware v1.2.0 or later.
void set_lock_on_reboot(bool lock_on_reboot) { this->lock_on_reboot_ = lock_on_reboot; }
/// as a command, and unlock it after boot. Needs emonTx firmware v1.2.0 or later, which AUTO checks.
void set_lock_on_reboot(LockOnReboot lock_on_reboot) { this->lock_on_reboot_ = lock_on_reboot; }
#ifdef USE_SENSOR
void init_sensors(size_t count) { this->sensors_.init(count); }
@@ -66,8 +72,18 @@ class EmonTx final : public Component, public uart::UARTDevice {
protected:
void parse_json_(const char *data, size_t len);
void unlock_();
#ifdef USE_EMONTX_VERSION
bool wants_version_() const {
#ifdef USE_EMONTX_VERSION_TEXT_SENSOR
if (this->version_text_sensor_ != nullptr)
return true;
#endif
return this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_AUTO;
}
void request_version_();
void send_version_request_();
void parse_msg_number_(const char *line, size_t len);
void parse_version_line_(const char *line, size_t len);
#endif
@@ -79,9 +95,12 @@ class EmonTx final : public Component, public uart::UARTDevice {
uint16_t buffer_pos_{0};
std::atomic<bool> paused_{false};
bool was_paused_{false};
bool lock_on_reboot_{false};
LockOnReboot lock_on_reboot_{LockOnReboot::LOCK_ON_REBOOT_NEVER};
bool discarding_line_{false}; // Set after an overflow or a pause until the end of that line
#ifdef USE_EMONTX_VERSION
bool lock_supported_{false}; // The last version reported is v1.2.0 or later
bool unlock_sent_{false}; // AUTO has sent 'emonunlock' since boot
uint8_t version_frames_left_{0}; // Data lines still to wait for a version reply; 0 when none is owed
uint32_t last_msg_number_{0};
#endif
std::array<char, MAX_LINE_LENGTH + 1> buffer_{};
@@ -21,3 +21,4 @@ async def to_code(config: ConfigType) -> None:
text_sensors = text_sensor.sub_text_sensors(config)
if await text_sensors(CONF_VERSION, hub.set_version_text_sensor) is not None:
cg.add_define("USE_EMONTX_VERSION")
cg.add_define("USE_EMONTX_VERSION_TEXT_SENSOR")
+1 -6
View File
@@ -40,7 +40,6 @@ from esphome.const import (
CONF_WIDTH,
)
from esphome.cpp_generator import RawExpression
from esphome.final_validate import full_config
from . import models
@@ -198,15 +197,11 @@ def _final_validate(config) -> None:
"epaper_spi", require_miso=False, require_mosi=True
)(config)
global_config = full_config.get()
from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN
if CONF_LAMBDA not in config and CONF_PAGES not in config:
if LVGL_DOMAIN in global_config:
if display.has_external_writer(config[CONF_ID]):
if CONF_UPDATE_INTERVAL not in config:
config[CONF_UPDATE_INTERVAL] = update_interval("never")
else:
# If no drawing methods are configured, and LVGL is not enabled, show a test card
config[CONF_SHOW_TEST_CARD] = True
elif CONF_UPDATE_INTERVAL not in config:
config[CONF_UPDATE_INTERVAL] = update_interval("1min")
+19 -2
View File
@@ -131,9 +131,7 @@ void EPaperBase::update() {
}
void EPaperBase::wait_for_idle_(bool should_wait) {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
this->waiting_for_idle_start_ = millis();
#endif
this->waiting_for_idle_ = should_wait;
}
@@ -156,6 +154,10 @@ void EPaperBase::loop() {
ESP_LOGV(TAG, "Screen was busy for %u ms", (unsigned) (millis() - this->waiting_for_idle_start_));
#endif
} else {
if (now - this->waiting_for_idle_start_ > this->busy_timeout_seconds_ * 1000u) {
this->abandon_update_();
return;
}
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
if (now - this->waiting_for_idle_last_print_ >= 1000) {
ESP_LOGV(TAG, "Waiting for idle in state %s", this->epaper_state_to_string_());
@@ -168,6 +170,20 @@ void EPaperBase::loop() {
this->process_state_();
}
// The busy pin never cleared: give up on this update so later ones are not refused, and make the next one full
void EPaperBase::abandon_update_() {
ESP_LOGE(TAG, "Display stayed busy for %u s in state %s; check the busy pin", (unsigned) this->busy_timeout_seconds_,
this->epaper_state_to_string_());
this->status_set_warning(LOG_STR("Busy pin timeout"));
if (this->state_ == EPaperState::TRANSFER_DATA)
this->abort_transfer();
// Leave the panel powered down rather than with its high-voltage drivers enabled
this->power_off();
this->full_update_requested_ = true;
this->current_data_index_ = 0;
this->set_state_(EPaperState::IDLE);
}
/**
* Process the state machine.
* Typical state sequence:
@@ -242,6 +258,7 @@ void EPaperBase::process_state_() {
break;
case EPaperState::DEEP_SLEEP:
this->deep_sleep();
this->status_clear_warning();
this->set_state_(EPaperState::IDLE);
ESP_LOGD(TAG, "Display update took %" PRIu32 " ms", millis() - this->update_start_time_);
break;
+8 -2
View File
@@ -106,6 +106,7 @@ class EPaperBase : public Display,
int get_width() override { return this->effective_transform_ & SWAP_XY ? this->height_ : this->width_; }
int get_height() override { return this->effective_transform_ & SWAP_XY ? this->width_ : this->height_; }
bool pixel_axes_swapped() const override { return this->effective_transform_ & SWAP_XY; }
void draw_pixel_at(int x, int y, Color color) override;
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
@@ -127,6 +128,7 @@ class EPaperBase : public Display,
virtual bool initialise(bool partial);
void send_init_sequence_(const uint8_t *sequence, size_t length);
void wait_for_idle_(bool should_wait);
void abandon_update_();
bool init_buffer_(size_t buffer_length);
void update_effective_transform_();
bool rotate_coordinates_(int &x, int &y);
@@ -143,6 +145,8 @@ class EPaperBase : public Display,
* @return true if done, false if it should be called next loop
*/
virtual bool transfer_data() = 0;
// Called when an update is abandoned in the middle of transfer_data(), to release anything it left open
virtual void abort_transfer() {}
/**
* Refresh the screen after data transfer
*/
@@ -193,18 +197,20 @@ class EPaperBase : public Display,
// point to the pixel past the last one updated, i.e. may range up to width/height.
uint16_t x_low_{}, y_low_{}, x_high_{}, y_high_{};
uint32_t waiting_for_idle_start_{};
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
uint32_t waiting_for_idle_last_print_{};
uint32_t waiting_for_idle_start_{};
#endif
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG
uint32_t update_start_time_{};
#endif
// properties with specific initialisers go last
EPaperState state_{EPaperState::IDLE};
uint32_t reset_duration_{10};
// Longer than any panel takes to refresh; past it the busy pin is assumed stuck
uint16_t busy_timeout_seconds_{120};
uint8_t full_update_every_{1};
EPaperState state_{EPaperState::IDLE};
};
} // namespace esphome::epaper_spi
@@ -364,6 +364,15 @@ bool HOT EPaperT133A01::transfer_data() {
return true;
}
void EPaperT133A01::abort_transfer() {
// A data stream is open, with one of the chip selects held low, whenever transfer_data() has started sending rows
if (this->current_data_index_ == 0)
return;
this->disable();
this->cs_pin_->digital_write(true);
this->cs1_pin_->digital_write(true);
}
void EPaperT133A01::dump_config() {
EPaperBase::dump_config();
LOG_PIN(" CS Pin: ", this->cs_pin_);
@@ -46,6 +46,7 @@ class EPaperT133A01 : public EPaperBase {
void deep_sleep() override;
bool transfer_data() override;
void abort_transfer() override;
/**
* Send a command (and optional data) selecting one or both controllers.
@@ -87,6 +87,38 @@ jd79660.extend(
),
)
# Waveshare 2.13-G (V2 hardware/init revision, PCB marked "rev 2.1" or later)
#
# Vendor init derived from vendor sample code
# <https://github.com/waveshareteam/e-Paper/blob/master/E-paper_Separate_Program/2in13_e-Paper_G/ESP32/EPD_2in13g_V2.cpp>
# Compatible MIT license, see esphome/LICENSE file.
#
# Note: busy pin uses LOW=busy, HIGH=idle. Configure with inverted: true in YAML.
# Note: panel is 122px wide, but vendor code declares the TRES register as a fixed
# 124 on this revision. width=124 is declared here (rather than 122) purely so the
# transferred bytes-per-row matches what the vendor init tells the IC to expect; the
# extra 2 columns fall outside the visible glass. The model width must equal the
# TRES value in initsequence, so overriding dimensions: in YAML brings the shear back.
#
# fmt: off
jd79660.extend(
"Waveshare-2.13in-G",
width=124,
height=250,
initsequence=(
(0x61, 0x00, 0x7C, 0x00, 0xFA,), # TRES: 124x250 fixed (124 = TRES stride value)
(0xE9, 0x01,),
# Power On (0x04): Must be early part of init seq = Disabled later!
(0x04,),
),
fast_update=(
(0xE0, 0x02,),
(0xE6, 0x5A,),
(0xA5,),
),
)
# Waveshare 7.5-H
#
# Vendor init derived from vendor sample code
@@ -130,7 +162,7 @@ jd79660.extend(
# Note: busy pin uses LOW=busy, HIGH=idle.
#
# fmt: off
jd79660.extend(
gdey0213f51 = jd79660.extend(
"goodisplay-gdey0213f51-2.13",
width=128,
height=250,
@@ -154,3 +186,13 @@ jd79660.extend(
(0x04,),
),
)
# Waveshare 2.13-G (V1 hardware/init revision, older stock): the same panel and init as the Good
# Display entry above, byte for byte, so it only adds the name. Vendor sample:
# <https://github.com/waveshareteam/e-Paper/blob/master/E-paper_Separate_Program/2in13_e-Paper_G/ESP32/EPD_2in13g.cpp>
# Compatible MIT license, see esphome/LICENSE file.
#
# Note: busy pin uses LOW=busy, HIGH=idle. Configure with inverted: true in YAML.
# Note: width=128 is the TRES value the init sequence programs, not the 122 visible columns; overriding
# dimensions: in YAML shears the image. Not tested on V1 hardware.
gdey0213f51.extend("Waveshare-2.13in-G-V1")
@@ -24,3 +24,11 @@ WaveshareB(
data_rate="10MHz",
minimum_update_interval="1s",
)
WaveshareB(
"waveshare-2.66in-b",
width=152,
height=296,
data_rate="10MHz",
minimum_update_interval="15s",
)
+1
View File
@@ -131,6 +131,7 @@ bool ESP32PreferenceBackend::load(uint8_t *data, size_t len) {
void ESP32Preferences::open() {
// Runs from app_main() before the logger is initialized; any logging here
// must be deferred. See s_open_err and make_preference() below.
// Components that need NVS check nvs_handle instead of initializing it again.
nvs_flash_init();
esp_err_t err = nvs_open("esphome", NVS_READWRITE, &this->nvs_handle);
if (err == 0)
+4 -12
View File
@@ -5,6 +5,7 @@
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/preferences.h"
#ifndef CONFIG_BT_CONTROLLER_DISABLED
#include <esp_bt.h>
@@ -28,7 +29,6 @@ extern "C" {
#include <freertos/FreeRTOS.h>
#include <freertos/FreeRTOSConfig.h>
#include <freertos/task.h>
#include <nvs_flash.h>
#ifdef USE_ARDUINO
// Prevent Arduino from releasing BT memory at startup (esp32-hal-misc.c).
@@ -77,8 +77,9 @@ static constexpr uint32_t HOSTED_BT_WDT_TIMEOUT_MS = 60000;
void ESP32BLE::setup() {
global_ble = this;
if (!ble_pre_setup_()) {
ESP_LOGE(TAG, "BLE could not be prepared for configuration");
// Preferences opens NVS in app_main(); a zero handle means it could not be opened, even after an erase
if (global_preferences->nvs_handle == 0) {
ESP_LOGE(TAG, "NVS unavailable");
this->mark_failed();
return;
}
@@ -185,15 +186,6 @@ void ESP32BLE::advertising_remove_service_uuid(ESPBTUUID uuid) {
}
#endif
bool ESP32BLE::ble_pre_setup_() {
esp_err_t err = nvs_flash_init();
if (err != ESP_OK) {
ESP_LOGE(TAG, "nvs_flash_init failed: %d", err);
return false;
}
return true;
}
#ifdef USE_ESP32_BLE_ADVERTISING
void ESP32BLE::advertising_init_() {
if (this->advertising_ != nullptr)
-1
View File
@@ -185,7 +185,6 @@ class ESP32BLE final : public Component {
}
this->ble_events_.get_and_reset_dropped_count();
}
bool ble_pre_setup_();
#ifdef USE_ESP32_BLE_ADVERTISING
void advertising_init_();
#endif
@@ -10,7 +10,6 @@
#include <esp_gap_ble_api.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <nvs_flash.h>
#include <cstring>
#include "esphome/core/hal.h"
@@ -7,7 +7,6 @@
#ifdef USE_ESP32
#include <nvs_flash.h>
#include <freertos/FreeRTOSConfig.h>
#include <esp_bt_main.h>
#ifndef CONFIG_BT_CONTROLLER_DISABLED
@@ -16,7 +16,6 @@
#include <freertos/FreeRTOS.h>
#include <freertos/FreeRTOSConfig.h>
#include <freertos/task.h>
#include <nvs_flash.h>
#include <cinttypes>
#ifdef USE_OTA
+23
View File
@@ -6,6 +6,8 @@ import esphome.codegen as cg
from esphome.components import canbus
from esphome.components.canbus import CONF_BIT_RATE, CanbusComponent, CanSpeed
from esphome.components.esp32 import (
CONF_MINIMUM_CHIP_REVISION,
KEY_ESP32,
VARIANT_ESP32,
VARIANT_ESP32C3,
VARIANT_ESP32C5,
@@ -20,6 +22,8 @@ from esphome.components.esp32 import (
)
import esphome.config_validation as cv
from esphome.const import (
CONF_ADVANCED,
CONF_FRAMEWORK,
CONF_ID,
CONF_MODE,
CONF_RX_PIN,
@@ -27,6 +31,7 @@ from esphome.const import (
CONF_TX_PIN,
CONF_TX_QUEUE_LEN,
)
import esphome.final_validate as fv
from esphome.types import ConfigType
CODEOWNERS = ["@Sympatron"]
@@ -42,6 +47,7 @@ CanMode = esp32_can_ns.enum("CanMode")
CAN_MODES = {
"NORMAL": CanMode.CAN_MODE_NORMAL,
"LISTENONLY": CanMode.CAN_MODE_LISTEN_ONLY,
"NO_ACK": CanMode.CAN_MODE_NO_ACK,
}
# Currently the driver only supports a subset of the bit rates defined in canbus
@@ -49,6 +55,7 @@ CAN_MODES = {
# See ESP-IDF Programming Guide --> API Reference --> Two-Wire Automotive Interface (TWAI)
CAN_SPEEDS_ESP32 = {
"20KBPS": CanSpeed.CAN_20KBPS,
"25KBPS": CanSpeed.CAN_25KBPS,
"50KBPS": CanSpeed.CAN_50KBPS,
"100KBPS": CanSpeed.CAN_100KBPS,
@@ -114,6 +121,22 @@ CONFIG_SCHEMA = canbus.CANBUS_SCHEMA.extend(
)
def _final_validate(config: ConfigType) -> None:
if get_esp32_variant() == VARIANT_ESP32 and config[CONF_BIT_RATE] == "20KBPS":
esp32_config = fv.full_config.get()[KEY_ESP32]
min_revision = esp32_config[CONF_FRAMEWORK][CONF_ADVANCED].get(
CONF_MINIMUM_CHIP_REVISION
)
if min_revision is None or min_revision < "2.0":
raise cv.Invalid(
"20KBPS on ESP32 requires esp32.framework.advanced.minimum_chip_revision: '2.0' or higher",
path=[CONF_BIT_RATE],
)
FINAL_VALIDATE_SCHEMA = _final_validate
def get_default_tx_enqueue_timeout(bit_rate: str) -> int:
bit_rate_numeric = canbus.get_rate(bit_rate)
bits_per_packet = 140 # ~max CAN message length
+9 -2
View File
@@ -33,6 +33,10 @@ static bool get_bitrate(canbus::CanSpeed bitrate, twai_timing_config_t *t_config
case canbus::CAN_16KBPS:
*t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_16KBITS();
return true;
#endif
#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32C61) || \
defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || \
defined(USE_ESP32_VARIANT_ESP32S3) || (defined(USE_ESP32_VARIANT_ESP32) && CONFIG_ESP32_REV_MIN_FULL >= 200)
case canbus::CAN_20KBPS:
*t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_20KBITS();
return true;
@@ -75,10 +79,13 @@ bool ESP32Can::setup_internal() {
}
// Select TWAI mode based on configuration
twai_mode_t twai_mode = (this->mode_ == CAN_MODE_LISTEN_ONLY) ? TWAI_MODE_LISTEN_ONLY : TWAI_MODE_NORMAL;
twai_mode_t twai_mode = TWAI_MODE_NORMAL;
if (this->mode_ == CAN_MODE_LISTEN_ONLY) {
twai_mode = TWAI_MODE_LISTEN_ONLY;
ESP_LOGI(TAG, "CAN bus configured in LISTEN_ONLY mode (passive, no ACKs)");
} else if (this->mode_ == CAN_MODE_NO_ACK) {
twai_mode = TWAI_MODE_NO_ACK;
ESP_LOGI(TAG, "CAN bus configured in NO_ACK mode (transmit without requiring ACK)");
}
twai_general_config_t g_config =
+1
View File
@@ -12,6 +12,7 @@ namespace esphome::esp32_can {
enum CanMode : uint8_t {
CAN_MODE_NORMAL = 0,
CAN_MODE_LISTEN_ONLY = 1,
CAN_MODE_NO_ACK = 2,
};
class ESP32Can final : public canbus::Canbus {
@@ -42,6 +42,7 @@ extern "C" {
}
#include "esp_now_hosted_rpc.h"
#include "esphome/core/log.h"
namespace {
@@ -52,6 +53,8 @@ ESPHOME_LOG_TAG(TAG, "esp_now_hosted");
// single esp-hosted RPC RX thread. Serializing requests keeps the shared
// response slot race-free; a sequence number stops a late/stale response from
// being mistaken for ours.
// NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables): the shim is
// C-ABI state shared between the main loop and the esp-hosted RX thread.
SemaphoreHandle_t g_req_mutex = nullptr;
SemaphoreHandle_t g_resp_sem = nullptr; // given when the matching RESP lands
bool g_setup_done = false; // set only after setup fully succeeds
@@ -85,6 +88,7 @@ constexpr size_t ESP_NOW_HOSTED_MAX_PEERS = 20;
uint8_t g_peer_cache[ESP_NOW_HOSTED_MAX_PEERS][6];
size_t g_peer_count = 0;
portMUX_TYPE g_peer_lock = portMUX_INITIALIZER_UNLOCKED;
// NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables)
// Caller must hold g_peer_lock.
int peer_cache_find_locked(const uint8_t *mac) {
@@ -231,12 +235,14 @@ esp_err_t ensure_setup() {
g_resp_sem = xSemaphoreCreateBinary();
if (g_req_mutex == nullptr || g_resp_sem == nullptr)
return ESP_ERR_NO_MEM;
esp_err_t err;
if ((err = esp_hosted_register_custom_callback(ESP_NOW_HOSTED_MSG_RESP, on_resp, nullptr)) != ESP_OK)
esp_err_t err = esp_hosted_register_custom_callback(ESP_NOW_HOSTED_MSG_RESP, on_resp, nullptr);
if (err != ESP_OK)
return err;
if ((err = esp_hosted_register_custom_callback(ESP_NOW_HOSTED_MSG_RECV, on_recv, nullptr)) != ESP_OK)
err = esp_hosted_register_custom_callback(ESP_NOW_HOSTED_MSG_RECV, on_recv, nullptr);
if (err != ESP_OK)
return err;
if ((err = esp_hosted_register_custom_callback(ESP_NOW_HOSTED_MSG_SEND, on_send, nullptr)) != ESP_OK)
err = esp_hosted_register_custom_callback(ESP_NOW_HOSTED_MSG_SEND, on_send, nullptr);
if (err != ESP_OK)
return err;
g_setup_done = true;
return ESP_OK;
@@ -422,10 +428,11 @@ esp_err_t esp_now_send(const uint8_t *peer_addr, const uint8_t *data, size_t len
uint8_t hdr[sizeof(esp_now_hosted_send_req_t)];
auto *s = reinterpret_cast<esp_now_hosted_send_req_t *>(hdr);
s->has_addr = peer_addr != nullptr ? 1 : 0;
if (peer_addr != nullptr)
if (peer_addr != nullptr) {
memcpy(s->peer_addr, peer_addr, 6);
else
} else {
memset(s->peer_addr, 0, 6);
}
s->data_len = static_cast<uint16_t>(len);
// Fire-and-forget (wait=false): native esp_now_send returns once the frame is
// queued, with the real TX result delivered later through the send callback.
+11
View File
@@ -53,10 +53,14 @@ from .const import (
KEY_SERIAL1_REQUIRED,
KEY_SERIAL_REQUIRED,
KEY_WAVEFORM_REQUIRED,
PREFERENCES_RTC_EBOOT_WORDS,
PREFERENCES_RTC_NORMAL_WORDS,
THROW_STUBS_HEADER,
check_preferences_fit,
enable_serial,
enable_serial1,
esp8266_ns,
preferences_flash_words,
)
from .gpio import PinInitialState, add_pin_initial_states_array
@@ -467,6 +471,12 @@ async def to_code(config: ConfigType) -> None:
# Other projects like Tasmota & ESPEasy also use this
cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH")
cg.add_define("ESP8266_PREFERENCES_RTC_NORMAL_WORDS", PREFERENCES_RTC_NORMAL_WORDS)
cg.add_define("ESP8266_PREFERENCES_RTC_EBOOT_WORDS", PREFERENCES_RTC_EBOOT_WORDS)
cg.add_define(
"ESP8266_PREFERENCES_FLASH_WORDS",
preferences_flash_words(config[CONF_RESTORE_FROM_FLASH]),
)
if config[CONF_RESTORE_FROM_FLASH]:
cg.add_define("USE_ESP8266_PREFERENCES_FLASH")
@@ -528,6 +538,7 @@ async def to_code(config: ConfigType) -> None:
CORE.add_job(add_pin_initial_states_array)
CORE.add_job(finalize_waveform_config)
CORE.add_job(finalize_serial_config)
CORE.add_job(check_preferences_fit)
@coroutine_with_priority(CoroPriority.WORKAROUNDS)
+86 -1
View File
@@ -1,9 +1,13 @@
import logging
import esphome.codegen as cg
# Re-exported from the shared definition; here it indexes the BOARDS
# metadata dicts, whose entries in boards.py spell the literal.
from esphome.const import KEY_FLASH_SIZE # noqa: F401 # pylint: disable=unused-import
from esphome.core import CORE
from esphome.core import CORE, CoroPriority, EsphomeError, coroutine_with_priority
_LOGGER = logging.getLogger(__name__)
KEY_ESP8266 = "esp8266"
KEY_BOARD = "board"
@@ -24,6 +28,87 @@ KEY_LDSCRIPT = "ldscript"
# Crash-decode tools, resolved once per run by __init__
KEY_DECODE_TOOLS = "decode_tools"
# Saved-settings space in 4-byte words; preferences.cpp gets these as defines. A value cannot span the two
# RTC regions, and the flash store is smaller when only explicit flash preferences use it.
PREFERENCES_RTC_NORMAL_WORDS = 78
PREFERENCES_RTC_EBOOT_WORDS = 32
PREFERENCES_RTC_WORDS = PREFERENCES_RTC_NORMAL_WORDS + PREFERENCES_RTC_EBOOT_WORDS
PREFERENCES_FLASH_WORDS = 128
PREFERENCES_FLASH_WORDS_WITHOUT_RESTORE = 64
KEY_PREFERENCES = "preferences"
def preferences_flash_words(restore_from_flash: bool) -> int:
"""Size of the flash store for this configuration."""
return (
PREFERENCES_FLASH_WORDS
if restore_from_flash
else PREFERENCES_FLASH_WORDS_WITHOUT_RESTORE
)
def preference_words(value_bytes: int) -> int:
"""Words a saved value takes, checksum word included (rtc_pref_bytes_to_words plus one)."""
return (value_bytes + 3) // 4 + 1
def reserve_preferences(
count: int, value_bytes: int, in_flash: bool | None = None
) -> None:
"""Record saved values a component creates; check_preferences_fit() adds them all up.
in_flash is the value's own storage, as passed to make_preference(); None is the two-argument
form, which follows restore_from_flash.
"""
if in_flash is None:
in_flash = CORE.config[KEY_ESP8266][CONF_RESTORE_FROM_FLASH]
reservations = CORE.data.setdefault(KEY_ESP8266, {}).setdefault(KEY_PREFERENCES, [])
reservations.extend([(preference_words(value_bytes), in_flash)] * count)
def _rtc_values_can_split(values: list[int]) -> bool:
"""Whether some order fills the two RTC regions without a value spanning them."""
total = sum(values)
if total <= PREFERENCES_RTC_NORMAL_WORDS:
return True
# Bit i of reachable is set when some subset of the values sums to i
reachable = 1
for words in values:
reachable |= reachable << words
lowest = max(0, total - PREFERENCES_RTC_EBOOT_WORDS)
highest = min(total, PREFERENCES_RTC_NORMAL_WORDS)
return any(reachable >> normal & 1 for normal in range(lowest, highest + 1))
@coroutine_with_priority(CoroPriority.FINAL)
async def check_preferences_fit() -> None:
"""Fail the build when the reserved saved values cannot fit the space preferences.cpp has."""
flash_store = preferences_flash_words(
CORE.config[KEY_ESP8266][CONF_RESTORE_FROM_FLASH]
)
reservations = CORE.data[KEY_ESP8266].get(KEY_PREFERENCES, [])
flash_words = sum(w for w, in_flash in reservations if in_flash)
rtc_values = [w for w, in_flash in reservations if not in_flash]
if flash_words > flash_store:
raise EsphomeError(
f"Saved values need {flash_words} words but the ESP8266 flash store has {flash_store}; "
"use fewer or smaller saved values"
)
if not _rtc_values_can_split(rtc_values):
raise EsphomeError(
f"Saved values need {sum(rtc_values)} words but ESP8266 RTC memory has {PREFERENCES_RTC_WORDS} in "
f"regions of {PREFERENCES_RTC_NORMAL_WORDS} and {PREFERENCES_RTC_EBOOT_WORDS} that a value cannot "
"span; use fewer or smaller saved values, or store them in flash"
)
if sum(rtc_values) > PREFERENCES_RTC_NORMAL_WORDS:
_LOGGER.warning(
"Saved values reserve %d of %d words of ESP8266 RTC memory; the fit depends on setup order, and "
"saved settings that do not reserve are not counted",
sum(rtc_values),
PREFERENCES_RTC_WORDS,
)
# esp8266 namespace is already defined by arduino, manually prefix esphome
esp8266_ns = cg.global_ns.namespace("esphome").namespace("esp8266")
+5 -7
View File
@@ -4,6 +4,7 @@
#ifdef USE_ESP8266_CRASH_HANDLER
#include "crash_handler.h"
#include "rtc_layout.h"
#include "esphome/core/log.h"
#include <cinttypes>
@@ -46,11 +47,8 @@ static inline bool IRAM_ATTR is_code_addr(uint32_t val) {
// Recover the actual code address from a windowed-ABI return address on the stack.
static inline uint32_t IRAM_ATTR recover_code_addr(uint32_t val) { return (val & XTENSA_ADDR_MASK) | XTENSA_CODE_BASE; }
// RTC user memory layout for crash backtrace data.
// User-accessible RTC memory: blocks 64-191 (each block = 4 bytes).
// We use blocks 174-191 (last 18 blocks, 72 bytes) to minimize conflicts.
// Crash data lives in the last esphome::esp8266::RTC_CRASH_WORDS of user RTC memory (rtc_layout.h).
// Store 16 raw candidates, filter to real return addresses at log time.
static constexpr uint8_t RTC_CRASH_BASE = 174;
static constexpr size_t MAX_BACKTRACE = 16;
// Magic word packs sentinel, version, and count into one uint32_t:
@@ -78,7 +76,7 @@ struct RtcCrashData {
uint32_t backtrace[MAX_BACKTRACE];
uint32_t epc1; // Fault PC, used to filter duplicates
};
static_assert(sizeof(RtcCrashData) == 72, "RtcCrashData must fit in 18 RTC blocks");
static_assert(sizeof(RtcCrashData) == esphome::esp8266::RTC_CRASH_WORDS * 4, "RtcCrashData must fill its RTC words");
namespace esphome::esp8266 {
@@ -129,7 +127,7 @@ static const LogString *get_reset_reason(uint32_t reason) {
// Returns the number of valid backtrace entries (0 if no data found).
static uint8_t read_rtc_backtrace(uint32_t *backtrace, size_t max_entries) {
RtcCrashData rtc_data;
if (!system_rtc_mem_read(RTC_CRASH_BASE, &rtc_data, sizeof(rtc_data)))
if (!system_rtc_mem_read(esphome::esp8266::RTC_CRASH_BASE, &rtc_data, sizeof(rtc_data)))
return 0;
uint32_t magic = rtc_data.magic;
if ((magic & CRASH_SENTINEL_MASK) != CRASH_SENTINEL || (magic & CRASH_VERSION_MASK) != CRASH_VERSION)
@@ -233,7 +231,7 @@ extern "C" void IRAM_ATTR custom_crash_callback(struct rst_info *rst_info, uint3
data.epc1 = epc1;
data.magic = CRASH_SENTINEL | CRASH_VERSION | count;
system_rtc_mem_write(RTC_CRASH_BASE, &data, sizeof(data));
system_rtc_mem_write(esphome::esp8266::RTC_CRASH_BASE, &data, sizeof(data));
}
#endif // USE_ESP8266_CRASH_HANDLER
+13 -17
View File
@@ -6,6 +6,7 @@ extern "C" {
}
#include "preferences.h"
#include "rtc_layout.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/preferences_rtc.h"
@@ -17,31 +18,26 @@ namespace esphome::esp8266 {
ESPHOME_LOG_TAG(TAG, "preferences");
static constexpr uint32_t ESP_RTC_USER_MEM_START = 0x60001200;
static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_WORDS = 128;
static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_WORDS = RTC_USER_MEM_WORDS;
static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_BYTES = ESP_RTC_USER_MEM_SIZE_WORDS * 4;
// RTC memory layout:
// RTC memory layout (sizes come from const.py through codegen):
// - Eboot region: RTC words 0-31 (reserved, mapped from preference offset 78-109)
// - Normal region: RTC words 32-109 (mapped from preference offset 0-77)
// - Crash handler: RTC words 110-127 (reserved for crash_handler.cpp backtrace data)
static constexpr uint32_t RTC_EBOOT_REGION_WORDS = 32; // Words 0-31 reserved for eboot
static constexpr uint32_t RTC_NORMAL_REGION_WORDS = 78; // Words 32-109 for normal prefs
static constexpr uint32_t RTC_EBOOT_REGION_WORDS = ESP8266_PREFERENCES_RTC_EBOOT_WORDS;
static constexpr uint32_t RTC_NORMAL_REGION_WORDS = ESP8266_PREFERENCES_RTC_NORMAL_WORDS;
static constexpr uint32_t PREF_TOTAL_WORDS = RTC_EBOOT_REGION_WORDS + RTC_NORMAL_REGION_WORDS; // 110
static_assert(PREF_TOTAL_WORDS <= RTC_CRASH_BASE - RTC_SYSTEM_USER_BASE,
"preference regions overlap the crash handler's RTC words");
// Maximum preference size in words (limited by uint8_t length_words field)
static constexpr uint32_t MAX_PREFERENCE_WORDS = 255;
#define ESP_RTC_USER_MEM ((uint32_t *) ESP_RTC_USER_MEM_START)
// Flash storage size depends on esp8266 -> restore_from_flash YAML option (default: false).
// When enabled (USE_ESP8266_PREFERENCES_FLASH), all preferences default to flash and need
// 128 words (512 bytes). When disabled, only explicit flash prefs use this storage so
// 64 words (256 bytes) suffices since most preferences go to RTC memory instead.
#ifdef USE_ESP8266_PREFERENCES_FLASH
static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = 128;
#else
static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = 64;
#endif
// Smaller without restore_from_flash, when only explicit flash preferences live here
static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = ESP8266_PREFERENCES_FLASH_WORDS;
static uint32_t
s_flash_storage[ESP8266_FLASH_STORAGE_SIZE]; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
@@ -60,7 +56,7 @@ static inline bool esp_rtc_user_mem_write(uint32_t index, uint32_t value) {
if (index >= ESP_RTC_USER_MEM_SIZE_WORDS) {
return false;
}
if (index < 32 && s_prevent_write) {
if (index < RTC_EBOOT_REGION_WORDS && s_prevent_write) {
return false;
}
@@ -123,7 +119,7 @@ static bool load_from_rtc(size_t offset, uint32_t *data, size_t len) {
// Maximum buffer for any single preference - bounded by storage sizes.
// Flash prefs: bounded by ESP8266_FLASH_STORAGE_SIZE (128 or 64 words).
// RTC prefs: bounded by RTC_NORMAL_REGION_WORDS (96) - a single pref can't span both RTC regions.
// RTC prefs: bounded by RTC_NORMAL_REGION_WORDS (78) - a single pref can't span both RTC regions.
static constexpr size_t PREF_MAX_BUFFER_WORDS =
ESP8266_FLASH_STORAGE_SIZE > RTC_NORMAL_REGION_WORDS ? ESP8266_FLASH_STORAGE_SIZE : RTC_NORMAL_REGION_WORDS;
@@ -180,8 +176,8 @@ ESPPreferenceObject ESP8266Preferences::make_preference(size_t length, uint32_t
} else {
uint32_t start = this->current_offset;
bool in_normal = start < RTC_NORMAL_REGION_WORDS;
// Normal: offset 0-95 maps to RTC offset 32-127
// Eboot: offset 96-127 maps to RTC offset 0-31
// Normal: offset 0-77 maps to RTC offset 32-109
// Eboot: offset 78-109 maps to RTC offset 0-31
if (in_normal && start + total_words > RTC_NORMAL_REGION_WORDS) {
// start is in normal but end is not -> switch to Eboot
this->current_offset = start = RTC_NORMAL_REGION_WORDS;
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#ifdef USE_ESP8266
#include <cstdint>
namespace esphome::esp8266 {
// User RTC memory is system blocks 64-191, one 4-byte word each. preferences.cpp owns the first
// words, crash_handler.cpp the last; const.py hands preferences.cpp its region sizes.
static constexpr uint32_t RTC_SYSTEM_USER_BASE = 64;
static constexpr uint32_t RTC_USER_MEM_WORDS = 128;
static constexpr uint32_t RTC_CRASH_WORDS = 18;
static constexpr uint32_t RTC_CRASH_BASE = RTC_SYSTEM_USER_BASE + RTC_USER_MEM_WORDS - RTC_CRASH_WORDS;
} // namespace esphome::esp8266
#endif // USE_ESP8266
+2 -1
View File
@@ -3,11 +3,12 @@
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/core/log.h"
#include <deque>
namespace esphome::ezo {
static const char *const TAG = "ezo.sensor";
ESPHOME_LOG_TAG(TAG, "ezo.sensor");
enum EzoCommandType : uint8_t {
EZO_READ = 0,
+3 -1
View File
@@ -330,7 +330,9 @@ async def write_image(
if byte_order is not None:
# Check for valid type has already been done in validate_settings
encoder.set_big_endian(byte_order == "BIG_ENDIAN")
pixels = encoder.convert(image.resize((width, height)), path).getdata()
pixels = encoder.convert(
image.resize((width, height)), path
).get_flattened_data()
for row in range(height):
for col in range(width):
encoder.encode(pixels[row * width + col])
+2
View File
@@ -0,0 +1,2 @@
CODEOWNERS = ["@remcom"]
DOMAIN = "fusb302b"
+330
View File
@@ -0,0 +1,330 @@
#include "pd.h"
#include <cstring>
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome::fusb302b {
ESPHOME_LOG_TAG(TAG, "fusb302b.pd");
static constexpr uint32_t AMS_TIMEOUT_MS = 2000;
// tSinkRequest: delay before repeating a request that the source answered with Wait
static constexpr uint32_t SINK_REQUEST_MS = 100;
static constexpr uint8_t MAX_WAIT_RETRIES = 10;
// USB PD 6.4.1.3.1 Sink Fixed Supply Power Data Object flags; only the first (5 V) object carries them
static constexpr uint32_t SINK_PDO_HIGHER_CAPABILITY = 1u << 28;
static constexpr uint32_t SINK_PDO_USB_COMMS_CAPABLE = 1u << 26;
// Operational current advertised for every sink object: 3 A (300 x 10 mA)
static constexpr uint16_t SINK_PDO_CURRENT = 300;
// USB PD 6.4.1.2.3 Source Fixed Supply Power Data Object; other PDO types only report their type
static PdContract parse_pdo(uint32_t pdo) {
PdContract info{};
info.type = static_cast<PdPdoType>(pdo >> 30);
if (info.type == PD_PDO_TYPE_FIXED_SUPPLY) {
info.max_v = (pdo >> 10) & 0x3FF;
info.max_i = pdo & 0x3FF;
}
return info;
}
// USB PD 6.4.1.3.1 Sink Fixed Supply Power Data Object; voltage in 50 mV units, current in 10 mA units
static constexpr uint32_t sink_fixed_pdo(uint16_t voltage, uint16_t current) {
return (static_cast<uint32_t>(PD_PDO_TYPE_FIXED_SUPPLY) << 30) | (static_cast<uint32_t>(voltage & 0x3FF) << 10) |
(current & 0x3FF);
}
void PdMsg::set_header(uint16_t header) {
this->type = header & 0x1F;
this->spec_rev = static_cast<PdSpecRevision>((header >> 6) & 0x3);
this->id = (header >> 9) & 0x7;
this->num_of_obj = (header >> 12) & 0x7;
this->extended = (header >> 15) & 0x1;
}
uint16_t PdMsg::get_coded_header() const {
// Data role UFP (bit 5 = 0), power role sink (bit 8 = 0)
return static_cast<uint16_t>(this->type & 0x1F) | (static_cast<uint16_t>(this->spec_rev) << 6) |
(static_cast<uint16_t>(this->id & 0x7) << 9) | (static_cast<uint16_t>(this->num_of_obj & 0x7) << 12) |
(static_cast<uint16_t>(this->extended) << 15);
}
PdMsg PowerDelivery::make_control_msg_(PdControlMsgType type) const {
PdMsg msg;
msg.type = type;
msg.id = this->msg_counter_ % 8;
return msg;
}
PdMsg PowerDelivery::make_data_msg_(PdDataMsgType type, const uint32_t *objects, uint8_t len) const {
PdMsg msg;
msg.type = type;
msg.id = this->msg_counter_ % 8;
msg.num_of_obj = len;
memcpy(msg.data_objects, objects, len * sizeof(uint32_t));
return msg;
}
void PowerDelivery::reset_protocol_() {
this->last_received_msg_id_ = 255;
this->msg_counter_ = 0;
this->active_ams_ = false;
this->ps_rdy_expected_ = false;
this->retry_request_pending_ = false;
this->wait_retries_ = 0;
}
void PowerDelivery::set_state_(PdState state) {
if (this->state_.exchange(state) != state)
this->on_state_changed();
}
void PowerDelivery::set_contract_(const PdContract &contract) {
this->accepted_contract_ = contract;
const uint32_t packed = PackedContract::from(contract).value;
if (this->contract_.exchange(packed) != packed)
this->on_state_changed();
}
void PowerDelivery::clear_contract_() {
this->accepted_contract_ = {};
if (this->contract_.exchange(0) != 0)
this->on_state_changed();
}
void PowerDelivery::set_state_and_contract_(PdState state, const PdContract &contract) {
this->accepted_contract_ = contract;
const uint32_t packed = PackedContract::from(contract).value;
// The contract is stored first, so a reader that sees the new state also sees the new contract
const bool contract_changed = this->contract_.exchange(packed) != packed;
const bool state_changed = this->state_.exchange(state) != state;
if (contract_changed || state_changed)
this->on_state_changed();
}
void PowerDelivery::set_ams_(bool active) {
this->active_ams_ = active;
// A new sequence has no accepted request yet, and an ended one can no longer complete
this->ps_rdy_expected_ = false;
if (active)
this->ams_start_ = millis();
}
bool PowerDelivery::check_ams_() {
if (this->active_ams_ && millis() - this->ams_start_ > AMS_TIMEOUT_MS)
this->set_ams_(false);
return this->active_ams_;
}
void PowerDelivery::check_request_retry_() {
if (!this->retry_request_pending_ || millis() - this->wait_received_ms_ < SINK_REQUEST_MS)
return;
if (this->send_request_()) {
this->retry_request_pending_ = false;
} else if (this->wait_retries_ < MAX_WAIT_RETRIES) {
// Try again after another tSinkRequest; failed attempts count against the same limit as Wait replies
this->wait_retries_++;
this->wait_received_ms_ = millis();
} else {
ESP_LOGW(TAG, "Giving up repeating the request");
this->retry_request_pending_ = false;
this->request_failed_();
}
}
bool PowerDelivery::send_request_() {
this->set_ams_(true);
if (this->send_message(this->make_data_msg_(PD_DATA_REQUEST, &this->last_rdo_, 1)))
return true;
ESP_LOGW(TAG, "Sending the request failed");
this->set_ams_(false);
return false;
}
void PowerDelivery::request_failed_() {
// A contract already in place stays valid. Without one, report that negotiation is over at default power.
if (this->state_ == PdState::PD_STATE_DEFAULT_CONTRACT)
this->set_state_(PdState::PD_STATE_PD_TIMEOUT);
}
void PowerDelivery::check_transition_timeout_() {
// The AMS that sent the request only ends on PS_RDY, so an ended AMS while in transition means a timeout
if (this->state_ != PdState::PD_STATE_TRANSITION || this->check_ams_())
return;
ESP_LOGW(TAG, "Source did not report the new supply ready, falling back to default power");
this->set_state_and_contract_(PdState::PD_STATE_DEFAULT_CONTRACT, DEFAULT_CONTRACT);
this->on_transition_timeout();
}
void PowerDelivery::handle_transmit_failed_() {
// USB PD 6.12.2.2: the counter also moves on after a failed transmission. The source may have received the
// message and only its GoodCRC was lost, so reusing the MessageID would make it drop the next one as a repeat.
ESP_LOGD(TAG, "Message %u not acknowledged", this->msg_counter_ % 8);
this->msg_counter_++;
}
void PowerDelivery::handle_message_(const PdMsg &msg) {
if (msg.num_of_obj == 0 && msg.type == PD_CNTRL_GOODCRC) {
// A GoodCRC carries the MessageID it acknowledges; a duplicate or stray one must not advance the counter
if (msg.id != this->msg_counter_ % 8) {
ESP_LOGD(TAG, "Ignoring GoodCRC for MessageID %u, expected %u", msg.id, this->msg_counter_ % 8);
return;
}
// Our last message was received, so the next one gets a new MessageID
this->msg_counter_++;
return;
}
// Soft_Reset always has MessageID 0, so it must not be dropped as a retransmission of the last message
if (msg.num_of_obj == 0 && msg.type == PD_CNTRL_SOFT_RESET) {
this->handle_soft_reset_(msg);
return;
}
// Retransmissions repeat the MessageID of a message that was already handled
if (msg.id == this->last_received_msg_id_)
return;
this->last_received_msg_id_ = msg.id;
if (msg.num_of_obj == 0) {
this->handle_control_message_(msg);
} else {
this->handle_data_message_(msg);
}
}
void PowerDelivery::handle_data_message_(const PdMsg &msg) {
if (msg.extended || msg.type != PD_DATA_SOURCE_CAP) {
// BIST and Vendor_Defined messages must not be answered with Reject, so unsupported data messages are ignored
ESP_LOGV(TAG, "Ignoring data message type %u%s", msg.type,
msg.extended ? LOG_STR_LITERAL(" (extended)") : LOG_STR_LITERAL(""));
return;
}
this->on_source_caps_received();
this->respond_to_source_caps_(msg);
}
void PowerDelivery::handle_control_message_(const PdMsg &msg) {
switch (msg.type) {
case PD_CNTRL_ACCEPT:
if (!this->check_ams_()) {
ESP_LOGD(TAG, "Ignoring Accept outside a request");
break;
}
this->ps_rdy_expected_ = true;
if (this->requested_contract_ != this->accepted_contract_)
this->set_state_and_contract_(PdState::PD_STATE_TRANSITION, this->requested_contract_);
break;
case PD_CNTRL_PS_RDY:
// check_ams_() first, so a PS_RDY that arrives after the sequence timed out is ignored too
if (!this->check_ams_() || !this->ps_rdy_expected_) {
ESP_LOGD(TAG, "Ignoring PS_RDY without an accepted request");
break;
}
this->set_ams_(false);
this->set_state_(PdState::PD_STATE_EXPLICIT_CONTRACT);
break;
case PD_CNTRL_REJECT:
if (!this->check_ams_()) {
ESP_LOGD(TAG, "Ignoring Reject outside a request");
break;
}
ESP_LOGW(TAG, "Source rejected the request");
this->set_ams_(false);
this->request_failed_();
break;
case PD_CNTRL_WAIT:
if (!this->check_ams_()) {
ESP_LOGD(TAG, "Ignoring Wait outside a request");
break;
}
this->set_ams_(false);
if (this->wait_retries_ < MAX_WAIT_RETRIES) {
ESP_LOGD(TAG, "Source asked to wait, repeating the request");
this->wait_retries_++;
this->retry_request_pending_ = true;
this->wait_received_ms_ = millis();
} else {
ESP_LOGW(TAG, "Source kept answering Wait, giving up");
this->request_failed_();
}
break;
case PD_CNTRL_PING:
case PD_CNTRL_NOT_SUPPORTED:
break;
case PD_CNTRL_GET_SINK_CAP: {
// 5 V first, then the requested voltage. Higher Capability tells the source that 5 V is not enough for
// full operation.
const uint16_t request_v = this->request_voltage_ * 20; // 50 mV units
uint32_t pdos[2];
uint8_t count = 0;
pdos[count++] = sink_fixed_pdo(100, SINK_PDO_CURRENT) | SINK_PDO_USB_COMMS_CAPABLE |
(request_v > 100 ? SINK_PDO_HIGHER_CAPABILITY : 0);
if (request_v > 100)
pdos[count++] = sink_fixed_pdo(request_v, SINK_PDO_CURRENT);
this->send_message(this->make_data_msg_(PD_DATA_SINK_CAP, pdos, count));
break;
}
default:
// Messages are sent as PD 2.0, which has no Not_Supported message
this->send_message(this->make_control_msg_(PD_CNTRL_REJECT));
break;
}
}
void PowerDelivery::handle_soft_reset_(const PdMsg &msg) {
this->reset_protocol_();
this->last_received_msg_id_ = msg.id;
// If this fails the source escalates to a hard reset. The hook still runs, so the PHY starts waiting for source
// capabilities and recovers on its own if neither follows.
if (!this->send_message(this->make_control_msg_(PD_CNTRL_ACCEPT))) {
ESP_LOGW(TAG, "Accepting the soft reset failed");
}
// A soft reset keeps the power level, but the outcome of a transition in progress is unknown
if (this->state_ == PdState::PD_STATE_TRANSITION)
this->set_state_and_contract_(PdState::PD_STATE_DEFAULT_CONTRACT, DEFAULT_CONTRACT);
this->on_soft_reset_received();
}
void PowerDelivery::respond_to_source_caps_(const PdMsg &msg) {
// Pick the highest fixed supply PDO that does not exceed the requested voltage.
// The first PDO is always the 5 V fixed supply.
const uint16_t request_v = this->request_voltage_ * 20; // 50 mV units
PdContract selected{};
uint8_t position = 0; // 1-based object position, 0 = none
for (uint8_t idx = 0; idx < msg.num_of_obj; idx++) {
PdContract info = parse_pdo(msg.data_objects[idx]);
if (info.type != PD_PDO_TYPE_FIXED_SUPPLY)
continue;
if (position == 0 || (info.max_v <= request_v && info.max_v > selected.max_v)) {
selected = info;
position = idx + 1;
}
}
// USB PD 6.4.2 Request Data Object: no USB suspend (bit 24), USB communications capable (bit 25)
uint32_t rdo = (1u << 24) | (1u << 25);
if (position == 0) {
ESP_LOGW(TAG, "No usable PDO in source capabilities, requesting 5 V");
// Object 1 (5 V fixed supply), 300 mA maximum, 100 mA operating current
selected = PdContract{PD_PDO_TYPE_FIXED_SUPPLY, 100, 30};
position = 1;
rdo |= (30u << 0) | (10u << 10);
} else {
// Request the full current the source offers
const uint32_t current = selected.max_i;
rdo |= (current << 0) | (current << 10);
}
rdo |= static_cast<uint32_t>(position) << 28;
ESP_LOGD(TAG, "Requesting PDO %u: %.2f V", position, selected.max_v * 0.05f);
this->requested_contract_ = selected;
this->last_rdo_ = rdo;
this->retry_request_pending_ = false;
this->wait_retries_ = 0;
// A repeat would come too late: the source resets if it gets no Request within tSenderResponse (24-30 ms)
if (!this->send_request_())
this->request_failed_();
}
} // namespace esphome::fusb302b
+178
View File
@@ -0,0 +1,178 @@
#pragma once
#include <algorithm>
#include <atomic>
#include <cstddef>
#include <cstdint>
namespace esphome::fusb302b {
static constexpr uint8_t PD_MAX_NUM_DATA_OBJECTS = 7;
// Standard Power Range fixed supplies, which are the only ones this sink negotiates
static constexpr uint8_t MIN_REQUEST_VOLTAGE = 5;
static constexpr uint8_t MAX_REQUEST_VOLTAGE = 20;
enum PdSpecRevision : uint8_t {
PD_SPEC_REV_1 = 0,
PD_SPEC_REV_2 = 1,
PD_SPEC_REV_3 = 2,
};
enum PdDataMsgType : uint8_t {
PD_DATA_SOURCE_CAP = 0x01,
PD_DATA_REQUEST = 0x02,
PD_DATA_SINK_CAP = 0x04,
};
enum PdControlMsgType : uint8_t {
PD_CNTRL_GOODCRC = 0x01,
PD_CNTRL_ACCEPT = 0x03,
PD_CNTRL_REJECT = 0x04,
PD_CNTRL_PING = 0x05,
PD_CNTRL_PS_RDY = 0x06,
PD_CNTRL_GET_SINK_CAP = 0x08,
PD_CNTRL_WAIT = 0x0C,
PD_CNTRL_SOFT_RESET = 0x0D,
PD_CNTRL_NOT_SUPPORTED = 0x10,
};
enum PdPdoType : uint8_t {
PD_PDO_TYPE_FIXED_SUPPLY = 0,
PD_PDO_TYPE_BATTERY = 1,
PD_PDO_TYPE_VARIABLE_SUPPLY = 2,
PD_PDO_TYPE_AUGMENTED = 3,
};
enum class PdState : uint8_t {
PD_STATE_DISCONNECTED,
/// Negotiation is over without a PD contract (no capabilities, or the request was not accepted); the source
/// supplies Type-C default power.
PD_STATE_PD_TIMEOUT,
PD_STATE_DEFAULT_CONTRACT,
PD_STATE_TRANSITION,
PD_STATE_EXPLICIT_CONTRACT,
PD_STATE_ERROR,
};
/// Returns true for the states in which a source is attached and supplying power.
inline bool is_connected_state(PdState state) {
return state == PdState::PD_STATE_DEFAULT_CONTRACT || state == PdState::PD_STATE_TRANSITION ||
state == PdState::PD_STATE_EXPLICIT_CONTRACT || state == PdState::PD_STATE_PD_TIMEOUT;
}
struct PdContract {
PdPdoType type{PD_PDO_TYPE_FIXED_SUPPLY};
uint16_t max_v{0}; // 50 mV units
uint16_t max_i{0}; // 10 mA units
bool operator==(const PdContract &other) const {
return this->max_v == other.max_v && this->max_i == other.max_i && this->type == other.type;
}
bool operator!=(const PdContract &other) const { return !(*this == other); }
};
// Type-C default power: 5 V (100 x 50 mV) at 500 mA (50 x 10 mA)
static constexpr PdContract DEFAULT_CONTRACT{PD_PDO_TYPE_FIXED_SUPPLY, 100, 50};
/// A contract packed into one word so it can be shared between tasks as an atomic.
/// 0 means there is no contract (nothing attached).
struct PackedContract {
uint32_t value{0};
static PackedContract from(const PdContract &contract) {
return {(static_cast<uint32_t>(contract.max_v) << 16) | contract.max_i};
}
bool is_set() const { return this->value != 0; }
float voltage() const { return (this->value >> 16) * 0.05f; }
float current() const { return (this->value & 0xFFFF) * 0.01f; }
};
struct PdMsg {
uint8_t type{0};
PdSpecRevision spec_rev{PD_SPEC_REV_2};
uint8_t id{0};
uint8_t num_of_obj{0};
bool extended{false};
uint32_t data_objects[PD_MAX_NUM_DATA_OBJECTS]{};
void set_header(uint16_t header);
uint16_t get_coded_header() const;
};
/// USB-PD sink protocol logic, independent of the PHY chip.
/// Everything except the getters and set_request_voltage() runs in the PHY task, which must also call
/// check_transition_timeout_() and check_request_retry_() regularly; the protocol has no timers of its own.
class PowerDelivery {
public:
void set_request_voltage(uint8_t voltage) {
this->request_voltage_ = std::clamp(voltage, MIN_REQUEST_VOLTAGE, MAX_REQUEST_VOLTAGE);
}
uint8_t get_request_voltage() const { return this->request_voltage_; }
PdState get_state() const { return this->state_; }
bool is_connected() const { return is_connected_state(this->state_); }
protected:
/// Returns false when the message could not be handed to the chip; the PHY logs the failure.
virtual bool send_message(const PdMsg &msg) = 0;
/// Called after state_ or contract_ changed, to let the main loop publish it.
virtual void on_state_changed() = 0;
/// Called after the source soft reset the protocol; it sends new source capabilities next.
virtual void on_soft_reset_received() = 0;
/// Called when source capabilities arrive, before the request for them is sent.
virtual void on_source_caps_received() = 0;
/// Called after an accepted request was not followed by PS_RDY in time and the state fell back to default power;
/// the PHY sends a soft reset so the source offers its capabilities again.
virtual void on_transition_timeout() = 0;
void handle_message_(const PdMsg &msg);
/// Called by the PHY when the chip gave up on a message after its retries without receiving a GoodCRC.
void handle_transmit_failed_();
void reset_protocol_();
void set_state_(PdState state);
void set_contract_(const PdContract &contract);
void clear_contract_();
/// Stores both before calling on_state_changed() once, so the main loop never sees the new state with the old
/// contract.
void set_state_and_contract_(PdState state, const PdContract &contract);
void set_ams_(bool active);
/// Returns true while an atomic message sequence is in progress; ends it after a timeout.
bool check_ams_();
/// Falls back to default power when the source accepted a request but did not report the new supply ready in time.
void check_transition_timeout_();
/// Sends the last request again once tSinkRequest has passed after the source answered Wait.
void check_request_retry_();
PdMsg make_control_msg_(PdControlMsgType type) const;
PdMsg make_data_msg_(PdDataMsgType type, const uint32_t *objects, uint8_t len) const;
// Shared with the main loop
std::atomic<PdState> state_{PdState::PD_STATE_DISCONNECTED};
std::atomic<uint32_t> contract_{0};
std::atomic<uint8_t> request_voltage_{5};
// PHY task only
uint32_t ams_start_{0};
uint32_t wait_received_ms_{0};
bool retry_request_pending_{false};
private:
void handle_data_message_(const PdMsg &msg);
void handle_control_message_(const PdMsg &msg);
void handle_soft_reset_(const PdMsg &msg);
void respond_to_source_caps_(const PdMsg &msg);
bool send_request_();
void request_failed_();
PdContract requested_contract_{};
PdContract accepted_contract_{};
uint32_t last_rdo_{0};
bool active_ams_{false};
// Set by Accept; PS_RDY is only valid after the source accepted the request of the current sequence
bool ps_rdy_expected_{false};
uint8_t last_received_msg_id_{255};
uint8_t msg_counter_{0};
uint8_t wait_retries_{0};
};
} // namespace esphome::fusb302b
+198 -14
View File
@@ -2,7 +2,10 @@
#include <string>
#include "esphome/components/climate/climate.h"
#include "esphome/components/uart/uart.h"
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/progmem.h"
#include "hon_climate.h"
#include "hon_packet.h"
@@ -11,6 +14,26 @@ using namespace esphome::uart;
namespace esphome::haier {
static bool vertical_axis_is_auto(climate::ClimateSwingMode swing_mode) {
return (swing_mode == climate::CLIMATE_SWING_VERTICAL) || (swing_mode == climate::CLIMATE_SWING_BOTH);
}
static bool horizontal_axis_is_auto(climate::ClimateSwingMode swing_mode) {
return (swing_mode == climate::CLIMATE_SWING_HORIZONTAL) || (swing_mode == climate::CLIMATE_SWING_BOTH);
}
static bool is_auto(hon_protocol::VerticalSwingMode mode) {
return (mode == hon_protocol::VerticalSwingMode::AUTO) || (mode == hon_protocol::VerticalSwingMode::AUTO_SPECIAL);
}
static bool is_auto(hon_protocol::HorizontalSwingMode mode) { return mode == hon_protocol::HorizontalSwingMode::AUTO; }
// A position the user just set wins over the appliance's echo of the old one for this long
constexpr uint32_t AIRFLOW_SELECT_DEBOUNCE_MS = 10000;
static bool in_debounce(uint32_t set_time, uint32_t now) {
return set_time != 0 && now - set_time < AIRFLOW_SELECT_DEBOUNCE_MS;
}
ESPHOME_LOG_TAG(TAG, "haier.climate");
constexpr size_t SIGNAL_LEVEL_UPDATE_INTERVAL_MS = 10000;
constexpr int PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET = -64;
@@ -20,6 +43,27 @@ constexpr size_t ALARM_STATUS_REQUEST_INTERVAL_MS = 600000;
const uint8_t ONE_BUF[] = {0x00, 0x01};
const uint8_t ZERO_BUF[] = {0x00, 0x00};
// Alarm texts indexed by alarm code; the last entry is the fallback for unknown codes
PROGMEM_STRING_TABLE(
HonAlarmMessages, "Outdoor module failure", "Outdoor defrost sensor failure",
"Outdoor compressor exhaust sensor failure", "Outdoor EEPROM abnormality", "Indoor coil sensor failure",
"Indoor-outdoor communication failure", "Power supply overvoltage protection",
"Communication failure between panel and indoor unit", "Outdoor compressor overheat protection",
"Outdoor environmental sensor abnormality", "Full water protection", "Indoor EEPROM failure",
"Outdoor out air sensor failure", "CBD and module communication failure", "Indoor DC fan failure",
"Outdoor DC fan failure", "Door switch failure", "Dust filter needs cleaning reminder", "Water shortage protection",
"Humidity sensor failure", "Indoor temperature sensor failure", "Manipulator limit failure",
"Indoor PM2.5 sensor failure", "Outdoor PM2.5 sensor failure", "Indoor heating overload/high load alarm",
"Outdoor AC current protection", "Outdoor compressor operation abnormality", "Outdoor DC current protection",
"Outdoor no-load failure", "CT current abnormality", "Indoor cooling freeze protection",
"High and low pressure protection", "Compressor out air temperature is too high",
"Outdoor evaporator sensor failure", "Outdoor cooling overload", "Water pump drainage failure",
"Three-phase power supply failure", "Four-way valve failure", "External alarm/scraper flow switch failure",
"Temperature cutoff protection alarm", "Different mode operation failure", "Electronic expansion valve failure",
"Dual heat source sensor Tw failure", "Communication failure with the wired controller",
"Indoor unit address duplication failure", "50Hz zero crossing failure", "Outdoor unit failure",
"Formaldehyde sensor failure", "VOC sensor failure", "CO2 sensor failure", "Firewall failure", "Unknown");
HonClimate::HonClimate()
: cleaning_status_(CleaningState::NO_CLEANING), got_valid_outdoor_temp_(false), active_alarms_{0x00, 0x00, 0x00,
0x00, 0x00, 0x00,
@@ -67,21 +111,60 @@ bool HonClimate::get_quiet_mode_state() const {
}
esphome::optional<hon_protocol::VerticalSwingMode> HonClimate::get_vertical_airflow() const {
if (!this->current_vertical_swing_.has_value()) {
return this->current_vertical_swing_;
}
// While the appliance swings, or right after the user chose a position, report the chosen position
if (is_auto(this->current_vertical_swing_.value()) ||
in_debounce(this->vertical_direction_set_time_, App.get_loop_component_start_time())) {
return this->settings_.last_vertiacal_swing;
}
return this->current_vertical_swing_;
};
}
void HonClimate::set_vertical_airflow(hon_protocol::VerticalSwingMode direction) {
if (is_auto(direction)) {
ESP_LOGW(TAG, "Vertical AUTO mode is not allowed for airflow position selector");
return;
}
if (this->settings_.last_vertiacal_swing != direction) {
this->settings_.last_vertiacal_swing = direction;
this->hon_rtc_.save(&this->settings_);
}
this->pending_vertical_direction_ = direction;
this->force_send_control_ = true;
this->vertical_direction_set_time_ = App.get_loop_component_start_time();
#ifdef USE_SELECT
this->publish_airflow_selects_();
#endif
}
esphome::optional<hon_protocol::HorizontalSwingMode> HonClimate::get_horizontal_airflow() const {
if (!this->current_horizontal_swing_.has_value()) {
return this->current_horizontal_swing_;
}
if (is_auto(this->current_horizontal_swing_.value()) ||
in_debounce(this->horizontal_direction_set_time_, App.get_loop_component_start_time())) {
return this->settings_.last_horizontal_swing;
}
return this->current_horizontal_swing_;
}
void HonClimate::set_horizontal_airflow(hon_protocol::HorizontalSwingMode direction) {
if (is_auto(direction)) {
ESP_LOGW(TAG, "Horizontal AUTO mode is not allowed for airflow position selector");
return;
}
if (this->settings_.last_horizontal_swing != direction) {
this->settings_.last_horizontal_swing = direction;
this->hon_rtc_.save(&this->settings_);
}
this->pending_horizontal_direction_ = direction;
this->force_send_control_ = true;
this->horizontal_direction_set_time_ = App.get_loop_component_start_time();
#ifdef USE_SELECT
this->publish_airflow_selects_();
#endif
}
const char *HonClimate::get_cleaning_status_text() const {
@@ -511,6 +594,18 @@ void HonClimate::initialization() {
} else {
this->settings_ = {hon_protocol::VerticalSwingMode::CENTER, hon_protocol::HorizontalSwingMode::CENTER, true, false};
}
bool settings_fixed = false;
if (is_auto(this->settings_.last_vertiacal_swing)) {
this->settings_.last_vertiacal_swing = hon_protocol::VerticalSwingMode::CENTER;
settings_fixed = true;
}
if (is_auto(this->settings_.last_horizontal_swing)) {
this->settings_.last_horizontal_swing = hon_protocol::HorizontalSwingMode::CENTER;
settings_fixed = true;
}
if (settings_fixed) {
this->hon_rtc_.save(&this->settings_);
}
this->current_vertical_swing_ = this->settings_.last_vertiacal_swing;
this->current_horizontal_swing_ = this->settings_.last_horizontal_swing;
this->quiet_mode_state_ = this->settings_.quiet_mode_state ? SwitchState::SWITCH_ON : SwitchState::SWITCH_OFF;
@@ -646,12 +741,21 @@ haier_protocol::HaierMessage HonClimate::get_control_message() {
}
}
}
const ClimateSwingMode target_swing_mode = this->current_hvac_settings_.swing_mode.value_or(this->swing_mode);
if (this->pending_vertical_direction_.has_value()) {
out_data->vertical_swing_mode = (uint8_t) this->pending_vertical_direction_.value();
if (!vertical_axis_is_auto(target_swing_mode)) {
out_data->vertical_swing_mode = (uint8_t) this->pending_vertical_direction_.value();
} else {
ESP_LOGD(TAG, "Vertical swing is on; the position is kept for when it is turned off");
}
this->pending_vertical_direction_.reset();
}
if (this->pending_horizontal_direction_.has_value()) {
out_data->horizontal_swing_mode = (uint8_t) this->pending_horizontal_direction_.value();
if (!horizontal_axis_is_auto(target_swing_mode)) {
out_data->horizontal_swing_mode = (uint8_t) this->pending_horizontal_direction_.value();
} else {
ESP_LOGD(TAG, "Horizontal swing is on; the position is kept for when it is turned off");
}
this->pending_horizontal_direction_.reset();
}
{
@@ -687,9 +791,17 @@ void HonClimate::process_alarm_message_(const uint8_t *packet, uint8_t size, boo
if ((packet[2 + i] & alarm_bit) != (this->active_alarms_[i] & alarm_bit)) {
bool alarm_status = (packet[2 + i] & alarm_bit) != 0;
int log_level = alarm_status ? ESPHOME_LOG_LEVEL_WARN : ESPHOME_LOG_LEVEL_INFO;
const char *alarm_message = alarm_code < esphome::haier::hon_protocol::HON_ALARM_COUNT
? esphome::haier::hon_protocol::HON_ALARM_MESSAGES[alarm_code].c_str()
: "Unknown";
#ifdef USE_ESP8266
// Static so a delayed on_alarm automation can still read it, but only until the next
// alarm change, including a later one in this packet; `code` stays correct
static char alarm_message[HonAlarmMessages::MAX_LENGTH + 1];
ESPHOME_strncpy_P(alarm_message,
reinterpret_cast<const char *>(
HonAlarmMessages::get_progmem_str(alarm_code, HonAlarmMessages::LAST_INDEX)),
sizeof(alarm_message));
#else
const char *alarm_message = HonAlarmMessages::get_progmem_str(alarm_code, HonAlarmMessages::LAST_INDEX);
#endif
esp_log_printf_(log_level, TAG, __LINE__, "Alarm %s (%d): %s", alarm_status ? "activated" : "deactivated",
alarm_code, alarm_message);
if (alarm_status) {
@@ -810,6 +922,41 @@ void HonClimate::set_quiet_mode_switch(switch_::Switch *sw) {
}
#endif // USE_SWITCH
#ifdef USE_SELECT
template<typename Mode, size_t N>
static void publish_airflow_index(select::Select *sel, const std::array<Mode, N> &order, Mode mode) {
if (sel == nullptr) {
return;
}
// Saved settings never hold AUTO after initialization(); any other value outside the table shows as Center
size_t index = N;
size_t center = 0;
for (size_t i = 0; i < N; i++) {
const Mode entry = airflow_order_at(order, i);
if (entry == mode) {
index = i;
} else if (entry == Mode::CENTER) {
center = i;
}
}
if (index == N) {
index = center;
}
const auto active = sel->active_index();
if (active.has_value() && *active == index) {
return;
}
sel->publish_state(index);
}
void HonClimate::publish_airflow_selects_() {
publish_airflow_index(this->vertical_airflow_select_, VERTICAL_SWING_MODES_ORDER,
this->settings_.last_vertiacal_swing);
publish_airflow_index(this->horizontal_airflow_select_, HORIZONTAL_SWING_MODES_ORDER,
this->settings_.last_horizontal_swing);
}
#endif // USE_SELECT
haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *packet_buffer, uint8_t size) {
size_t expected_size =
2 + this->status_message_header_size_ + this->real_control_packet_size_ + this->real_sensors_packet_size_;
@@ -1044,16 +1191,25 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
// Saving last known non auto mode for vertical and horizontal swing
this->current_vertical_swing_ = (hon_protocol::VerticalSwingMode) packet.control.vertical_swing_mode;
this->current_horizontal_swing_ = (hon_protocol::HorizontalSwingMode) packet.control.horizontal_swing_mode;
bool save_settings = ((this->current_vertical_swing_.value() != hon_protocol::VerticalSwingMode::AUTO) &&
(this->current_vertical_swing_.value() != hon_protocol::VerticalSwingMode::AUTO_SPECIAL) &&
(this->current_vertical_swing_.value() != this->settings_.last_vertiacal_swing)) ||
((this->current_horizontal_swing_.value() != hon_protocol::HorizontalSwingMode::AUTO) &&
(this->current_horizontal_swing_.value() != this->settings_.last_horizontal_swing));
if (save_settings) {
this->settings_.last_vertiacal_swing = this->current_vertical_swing_.value();
this->settings_.last_horizontal_swing = this->current_horizontal_swing_.value();
const uint32_t now_ms = App.get_loop_component_start_time();
bool save_vertical = !in_debounce(this->vertical_direction_set_time_, now_ms) &&
!is_auto(this->current_vertical_swing_.value()) &&
(this->current_vertical_swing_.value() != this->settings_.last_vertiacal_swing);
bool save_horizontal = !in_debounce(this->horizontal_direction_set_time_, now_ms) &&
!is_auto(this->current_horizontal_swing_.value()) &&
(this->current_horizontal_swing_.value() != this->settings_.last_horizontal_swing);
if (save_vertical || save_horizontal) {
if (save_vertical) {
this->settings_.last_vertiacal_swing = this->current_vertical_swing_.value();
}
if (save_horizontal) {
this->settings_.last_horizontal_swing = this->current_horizontal_swing_.value();
}
this->hon_rtc_.save(&this->settings_);
}
#ifdef USE_SELECT
this->publish_airflow_selects_();
#endif
should_publish = should_publish || (old_swing_mode != this->swing_mode);
}
this->last_valid_status_timestamp_ = std::chrono::steady_clock::now();
@@ -1229,8 +1385,10 @@ void HonClimate::fill_control_messages_queue_() {
(uint8_t) hon_protocol::DataParameters::SET_POINT,
buffer, 2);
}
ClimateSwingMode target_swing_mode = this->swing_mode;
// Vertical swing mode
if (climate_control.swing_mode.has_value()) {
target_swing_mode = climate_control.swing_mode.value();
uint8_t vertical_swing_buf[] = {0x00, (uint8_t) hon_protocol::VerticalSwingMode::AUTO};
uint8_t horizontal_swing_buf[] = {0x00, (uint8_t) hon_protocol::HorizontalSwingMode::AUTO};
switch (climate_control.swing_mode.value()) {
@@ -1256,6 +1414,32 @@ void HonClimate::fill_control_messages_queue_() {
(uint8_t) hon_protocol::DataParameters::VERTICAL_SWING_MODE,
vertical_swing_buf, 2);
}
// A swing change above already sent both axes from settings_.last_*, which set_*_airflow() updated
const bool swing_sent = climate_control.swing_mode.has_value();
if (this->pending_vertical_direction_.has_value()) {
if (vertical_axis_is_auto(target_swing_mode)) {
ESP_LOGD(TAG, "Vertical swing is on; the position is kept for when it is turned off");
} else if (!swing_sent) {
uint8_t vertical_swing_buf[] = {0x00, (uint8_t) this->pending_vertical_direction_.value()};
this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
(uint8_t) hon_protocol::DataParameters::VERTICAL_SWING_MODE,
vertical_swing_buf, 2);
}
this->pending_vertical_direction_.reset();
}
if (this->pending_horizontal_direction_.has_value()) {
if (horizontal_axis_is_auto(target_swing_mode)) {
ESP_LOGD(TAG, "Horizontal swing is on; the position is kept for when it is turned off");
} else if (!swing_sent) {
uint8_t horizontal_swing_buf[] = {0x00, (uint8_t) this->pending_horizontal_direction_.value()};
this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
(uint8_t) hon_protocol::DataParameters::HORIZONTAL_SWING_MODE,
horizontal_swing_buf, 2);
}
this->pending_horizontal_direction_.reset();
}
// Fan mode
if (climate_control.fan_mode.has_value()) {
switch (climate_control.fan_mode.value()) {
+38
View File
@@ -1,5 +1,6 @@
#pragma once
#include <array>
#include <chrono>
#include <queue>
#ifdef USE_SENSOR
@@ -14,12 +15,36 @@
#ifdef USE_SWITCH
#include "esphome/components/switch/switch.h"
#endif
#ifdef USE_SELECT
#include "esphome/components/select/select.h"
#endif
#include "esphome/core/automation.h"
#include "esphome/core/hal.h"
#include "haier_base.h"
#include "hon_packet.h"
namespace esphome::haier {
#ifdef USE_SELECT
// Positions in the order the select platform lists them; in flash on ESP8266, so read with airflow_order_at()
inline constexpr std::array<hon_protocol::VerticalSwingMode, 7> VERTICAL_SWING_MODES_ORDER PROGMEM{
hon_protocol::VerticalSwingMode::HEALTH_UP, hon_protocol::VerticalSwingMode::MAX_UP,
hon_protocol::VerticalSwingMode::UP, hon_protocol::VerticalSwingMode::CENTER,
hon_protocol::VerticalSwingMode::DOWN, hon_protocol::VerticalSwingMode::MAX_DOWN,
hon_protocol::VerticalSwingMode::HEALTH_DOWN,
};
inline constexpr std::array<hon_protocol::HorizontalSwingMode, 5> HORIZONTAL_SWING_MODES_ORDER PROGMEM{
hon_protocol::HorizontalSwingMode::MAX_LEFT, hon_protocol::HorizontalSwingMode::LEFT,
hon_protocol::HorizontalSwingMode::CENTER, hon_protocol::HorizontalSwingMode::RIGHT,
hon_protocol::HorizontalSwingMode::MAX_RIGHT,
};
template<typename Mode, size_t N> Mode airflow_order_at(const std::array<Mode, N> &order, size_t index) {
static_assert(sizeof(Mode) == 1);
return static_cast<Mode>(progmem_read_byte(reinterpret_cast<const uint8_t *>(order.data() + index)));
}
#endif
enum class CleaningState : uint8_t {
NO_CLEANING = 0,
SELF_CLEAN = 1,
@@ -100,6 +125,16 @@ class HonClimate final : public HaierClimateBase {
protected:
switch_::Switch *beeper_switch_{nullptr};
switch_::Switch *quiet_mode_switch_{nullptr};
#endif
#ifdef USE_SELECT
public:
void set_vertical_airflow_select(select::Select *sel) { this->vertical_airflow_select_ = sel; }
void set_horizontal_airflow_select(select::Select *sel) { this->horizontal_airflow_select_ = sel; }
protected:
void publish_airflow_selects_();
select::Select *vertical_airflow_select_{nullptr};
select::Select *horizontal_airflow_select_{nullptr};
#endif
public:
HonClimate();
@@ -195,6 +230,9 @@ class HonClimate final : public HaierClimateBase {
uint8_t big_data_counter_{0};
esphome::optional<hon_protocol::VerticalSwingMode> current_vertical_swing_{};
esphome::optional<hon_protocol::HorizontalSwingMode> current_horizontal_swing_{};
// millis() of the last user-set position per axis, 0 when none
uint32_t vertical_direction_set_time_{0};
uint32_t horizontal_direction_set_time_{0};
HonSettings settings_{};
ESPPreferenceObject hon_rtc_;
SwitchState quiet_mode_state_{SwitchState::SWITCH_OFF};
-56
View File
@@ -197,60 +197,4 @@ enum class SubcommandsControl : uint16_t {
// content: all values like in status packet)
};
const std::string HON_ALARM_MESSAGES[] = {
"Outdoor module failure",
"Outdoor defrost sensor failure",
"Outdoor compressor exhaust sensor failure",
"Outdoor EEPROM abnormality",
"Indoor coil sensor failure",
"Indoor-outdoor communication failure",
"Power supply overvoltage protection",
"Communication failure between panel and indoor unit",
"Outdoor compressor overheat protection",
"Outdoor environmental sensor abnormality",
"Full water protection",
"Indoor EEPROM failure",
"Outdoor out air sensor failure",
"CBD and module communication failure",
"Indoor DC fan failure",
"Outdoor DC fan failure",
"Door switch failure",
"Dust filter needs cleaning reminder",
"Water shortage protection",
"Humidity sensor failure",
"Indoor temperature sensor failure",
"Manipulator limit failure",
"Indoor PM2.5 sensor failure",
"Outdoor PM2.5 sensor failure",
"Indoor heating overload/high load alarm",
"Outdoor AC current protection",
"Outdoor compressor operation abnormality",
"Outdoor DC current protection",
"Outdoor no-load failure",
"CT current abnormality",
"Indoor cooling freeze protection",
"High and low pressure protection",
"Compressor out air temperature is too high",
"Outdoor evaporator sensor failure",
"Outdoor cooling overload",
"Water pump drainage failure",
"Three-phase power supply failure",
"Four-way valve failure",
"External alarm/scraper flow switch failure",
"Temperature cutoff protection alarm",
"Different mode operation failure",
"Electronic expansion valve failure",
"Dual heat source sensor Tw failure",
"Communication failure with the wired controller",
"Indoor unit address duplication failure",
"50Hz zero crossing failure",
"Outdoor unit failure",
"Formaldehyde sensor failure",
"VOC sensor failure",
"CO2 sensor failure",
"Firewall failure",
};
constexpr size_t HON_ALARM_COUNT = sizeof(HON_ALARM_MESSAGES) / sizeof(HON_ALARM_MESSAGES[0]);
} // namespace esphome::haier::hon_protocol
@@ -0,0 +1,67 @@
import esphome.codegen as cg
from esphome.components import select
import esphome.config_validation as cv
from esphome.const import ENTITY_CATEGORY_CONFIG
from esphome.types import ConfigType
from ..climate import (
CONF_HAIER_ID,
CONF_HORIZONTAL_AIRFLOW,
CONF_VERTICAL_AIRFLOW,
HonClimate,
haier_ns,
)
CODEOWNERS = ["@paveldn"]
VerticalAirflowSelect = haier_ns.class_("VerticalAirflowSelect", select.Select)
HorizontalAirflowSelect = haier_ns.class_("HorizontalAirflowSelect", select.Select)
# Same order as VERTICAL_SWING_MODES_ORDER and HORIZONTAL_SWING_MODES_ORDER in hon_climate.h
AIRFLOW_VERTICAL_DIRECTION_OPTIONS = [
"Health Up",
"Max Up",
"Up",
"Center",
"Down",
"Max Down",
"Health Down",
]
AIRFLOW_HORIZONTAL_DIRECTION_OPTIONS = [
"Max Left",
"Left",
"Center",
"Right",
"Max Right",
]
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_HAIER_ID): cv.use_id(HonClimate),
cv.Optional(CONF_VERTICAL_AIRFLOW): select.select_schema(
VerticalAirflowSelect,
icon="mdi:arrow-expand-vertical",
entity_category=ENTITY_CATEGORY_CONFIG,
),
cv.Optional(CONF_HORIZONTAL_AIRFLOW): select.select_schema(
HorizontalAirflowSelect,
icon="mdi:arrow-expand-horizontal",
entity_category=ENTITY_CATEGORY_CONFIG,
),
}
)
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_HAIER_ID])
selects = select.sub_selects(config, parent=hub)
await selects(
CONF_VERTICAL_AIRFLOW,
hub.set_vertical_airflow_select,
options=AIRFLOW_VERTICAL_DIRECTION_OPTIONS,
)
await selects(
CONF_HORIZONTAL_AIRFLOW,
hub.set_horizontal_airflow_select,
options=AIRFLOW_HORIZONTAL_DIRECTION_OPTIONS,
)
@@ -0,0 +1,15 @@
#pragma once
#include "esphome/components/select/select.h"
#include "../hon_climate.h"
namespace esphome::haier {
class HorizontalAirflowSelect : public select::Select, public Parented<HonClimate> {
protected:
void control(size_t index) override {
this->parent_->set_horizontal_airflow(airflow_order_at(HORIZONTAL_SWING_MODES_ORDER, index));
}
};
} // namespace esphome::haier
@@ -0,0 +1,15 @@
#pragma once
#include "esphome/components/select/select.h"
#include "../hon_climate.h"
namespace esphome::haier {
class VerticalAirflowSelect : public select::Select, public Parented<HonClimate> {
protected:
void control(size_t index) override {
this->parent_->set_vertical_airflow(airflow_order_at(VERTICAL_SWING_MODES_ORDER, index));
}
};
} // namespace esphome::haier
+2 -2
View File
@@ -126,8 +126,8 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config: ConfigType) -> None:
var = await climate_ir.new_climate_ir(config)
cg.add(var.set_protocol(config[CONF_PROTOCOL]))
tag = heatpumpir_ns.ProtocolTag.template(config[CONF_PROTOCOL])
var = await climate_ir.new_climate_ir(config, tag())
cg.add(var.set_horizontal_default(config[CONF_HORIZONTAL_DEFAULT]))
cg.add(var.set_vertical_default(config[CONF_VERTICAL_DEFAULT]))
cg.add(var.set_max_temperature(config[CONF_MAX_TEMPERATURE]))
+68 -64
View File
@@ -5,7 +5,6 @@
#if defined(USE_ARDUINO) || defined(USE_ESP32)
#include <cmath>
#include <map>
#include <IRSender.h>
#include <HeatpumpIRFactory.h>
#include "esphome/components/remote_base/remote_base.h"
@@ -45,71 +44,76 @@ class IRSenderESPHome : public IRSender {
ESPHOME_LOG_TAG(TAG, "heatpumpir.climate");
const std::map<Protocol, std::function<HeatpumpIR *()>> PROTOCOL_CONSTRUCTOR_MAP = {
{PROTOCOL_AUX, []() { return new AUXHeatpumpIR(); }}, // NOLINT
{PROTOCOL_BALLU, []() { return new BalluHeatpumpIR(); }}, // NOLINT
{PROTOCOL_CARRIER_MCA, []() { return new CarrierMCAHeatpumpIR(); }}, // NOLINT
{PROTOCOL_CARRIER_NQV, []() { return new CarrierNQVHeatpumpIR(); }}, // NOLINT
{PROTOCOL_DAIKIN_ARC417, []() { return new DaikinHeatpumpARC417IR(); }}, // NOLINT
{PROTOCOL_DAIKIN_ARC480, []() { return new DaikinHeatpumpARC480A14IR(); }}, // NOLINT
{PROTOCOL_DAIKIN, []() { return new DaikinHeatpumpIR(); }}, // NOLINT
{PROTOCOL_ELECTROLUXYAL, []() { return new ElectroluxYALHeatpumpIR(); }}, // NOLINT
{PROTOCOL_FUEGO, []() { return new FuegoHeatpumpIR(); }}, // NOLINT
{PROTOCOL_FUJITSU_AWYZ, []() { return new FujitsuHeatpumpIR(); }}, // NOLINT
{PROTOCOL_GREE, []() { return new GreeGenericHeatpumpIR(); }}, // NOLINT
{PROTOCOL_GREEYAA, []() { return new GreeYAAHeatpumpIR(); }}, // NOLINT
{PROTOCOL_GREEYAN, []() { return new GreeYANHeatpumpIR(); }}, // NOLINT
{PROTOCOL_GREEYAC, []() { return new GreeYACHeatpumpIR(); }}, // NOLINT
{PROTOCOL_GREEYT, []() { return new GreeYTHeatpumpIR(); }}, // NOLINT
{PROTOCOL_GREEYAP, []() { return new GreeYAPHeatpumpIR(); }}, // NOLINT
{PROTOCOL_HISENSE_AUD, []() { return new HisenseHeatpumpIR(); }}, // NOLINT
{PROTOCOL_HITACHI, []() { return new HitachiHeatpumpIR(); }}, // NOLINT
{PROTOCOL_HYUNDAI, []() { return new HyundaiHeatpumpIR(); }}, // NOLINT
{PROTOCOL_IVT, []() { return new IVTHeatpumpIR(); }}, // NOLINT
{PROTOCOL_MIDEA, []() { return new MideaHeatpumpIR(); }}, // NOLINT
{PROTOCOL_MITSUBISHI_FA, []() { return new MitsubishiFAHeatpumpIR(); }}, // NOLINT
{PROTOCOL_MITSUBISHI_FD, []() { return new MitsubishiFDHeatpumpIR(); }}, // NOLINT
{PROTOCOL_MITSUBISHI_FE, []() { return new MitsubishiFEHeatpumpIR(); }}, // NOLINT
{PROTOCOL_MITSUBISHI_HEAVY_FDTC, []() { return new MitsubishiHeavyFDTCHeatpumpIR(); }}, // NOLINT
{PROTOCOL_MITSUBISHI_HEAVY_ZJ, []() { return new MitsubishiHeavyZJHeatpumpIR(); }}, // NOLINT
{PROTOCOL_MITSUBISHI_HEAVY_ZM, []() { return new MitsubishiHeavyZMHeatpumpIR(); }}, // NOLINT
{PROTOCOL_MITSUBISHI_HEAVY_ZMP, []() { return new MitsubishiHeavyZMPHeatpumpIR(); }}, // NOLINT
{PROTOCOL_MITSUBISHI_KJ, []() { return new MitsubishiKJHeatpumpIR(); }}, // NOLINT
{PROTOCOL_MITSUBISHI_MSC, []() { return new MitsubishiMSCHeatpumpIR(); }}, // NOLINT
{PROTOCOL_MITSUBISHI_MSY, []() { return new MitsubishiMSYHeatpumpIR(); }}, // NOLINT
{PROTOCOL_MITSUBISHI_SEZ, []() { return new MitsubishiSEZKDXXHeatpumpIR(); }}, // NOLINT
{PROTOCOL_PANASONIC_CKP, []() { return new PanasonicCKPHeatpumpIR(); }}, // NOLINT
{PROTOCOL_PANASONIC_DKE, []() { return new PanasonicDKEHeatpumpIR(); }}, // NOLINT
{PROTOCOL_PANASONIC_EKE, []() { return new PanasonicEKEHeatpumpIR(); }}, // NOLINT
{PROTOCOL_PANASONIC_JKE, []() { return new PanasonicJKEHeatpumpIR(); }}, // NOLINT
{PROTOCOL_PANASONIC_LKE, []() { return new PanasonicLKEHeatpumpIR(); }}, // NOLINT
{PROTOCOL_PANASONIC_NKE, []() { return new PanasonicNKEHeatpumpIR(); }}, // NOLINT
{PROTOCOL_SAMSUNG_AQV, []() { return new SamsungAQVHeatpumpIR(); }}, // NOLINT
{PROTOCOL_SAMSUNG_FJM, []() { return new SamsungFJMHeatpumpIR(); }}, // NOLINT
{PROTOCOL_SHARP, []() { return new SharpHeatpumpIR(); }}, // NOLINT
{PROTOCOL_TOSHIBA_DAISEIKAI, []() { return new ToshibaDaiseikaiHeatpumpIR(); }}, // NOLINT
{PROTOCOL_TOSHIBA, []() { return new ToshibaHeatpumpIR(); }}, // NOLINT
{PROTOCOL_ZHLT01, []() { return new ZHLT01HeatpumpIR(); }}, // NOLINT
{PROTOCOL_NIBE, []() { return new NibeHeatpumpIR(); }}, // NOLINT
{PROTOCOL_QLIMA_1, []() { return new Qlima1HeatpumpIR(); }}, // NOLINT
{PROTOCOL_QLIMA_2, []() { return new Qlima2HeatpumpIR(); }}, // NOLINT
{PROTOCOL_SAMSUNG_AQV12MSAN, []() { return new SamsungAQV12MSANHeatpumpIR(); }}, // NOLINT
{PROTOCOL_ZHJG01, []() { return new ZHJG01HeatpumpIR(); }}, // NOLINT
{PROTOCOL_AIRWAY, []() { return new AIRWAYHeatpumpIR(); }}, // NOLINT
{PROTOCOL_BGH_AUD, []() { return new BGHHeatpumpIR(); }}, // NOLINT
{PROTOCOL_PANASONIC_ALTDKE, []() { return new PanasonicAltDKEHeatpumpIR(); }}, // NOLINT
{PROTOCOL_PHILCO_PHS32, []() { return new PhilcoPHS32HeatpumpIR(); }}, // NOLINT
{PROTOCOL_VAILLANTVAI8, []() { return new VaillantHeatpumpIR(); }}, // NOLINT
{PROTOCOL_R51M, []() { return new R51MHeatpumpIR(); }}, // NOLINT
};
template<Protocol P> struct ProtocolClass;
template<Protocol P> HeatpumpIR *create_heatpump_ir() {
return new typename ProtocolClass<P>::type(); // NOLINT
}
// Codegen calls only the configured protocols, so the linker drops the other classes
#define HEATPUMPIR_PROTOCOL(protocol, cls) \
template<> struct ProtocolClass<protocol> { using type = cls; }; \
template HeatpumpIR *create_heatpump_ir<protocol>();
HEATPUMPIR_PROTOCOL(PROTOCOL_AUX, AUXHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_BALLU, BalluHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_CARRIER_MCA, CarrierMCAHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_CARRIER_NQV, CarrierNQVHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_DAIKIN_ARC417, DaikinHeatpumpARC417IR)
HEATPUMPIR_PROTOCOL(PROTOCOL_DAIKIN_ARC480, DaikinHeatpumpARC480A14IR)
HEATPUMPIR_PROTOCOL(PROTOCOL_DAIKIN, DaikinHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_ELECTROLUXYAL, ElectroluxYALHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_FUEGO, FuegoHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_FUJITSU_AWYZ, FujitsuHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_GREE, GreeGenericHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_GREEYAA, GreeYAAHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_GREEYAN, GreeYANHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_GREEYAC, GreeYACHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_GREEYT, GreeYTHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_GREEYAP, GreeYAPHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_HISENSE_AUD, HisenseHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_HITACHI, HitachiHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_HYUNDAI, HyundaiHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_IVT, IVTHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_MIDEA, MideaHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_FA, MitsubishiFAHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_FD, MitsubishiFDHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_FE, MitsubishiFEHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_HEAVY_FDTC, MitsubishiHeavyFDTCHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_HEAVY_ZJ, MitsubishiHeavyZJHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_HEAVY_ZM, MitsubishiHeavyZMHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_HEAVY_ZMP, MitsubishiHeavyZMPHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_KJ, MitsubishiKJHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_MSC, MitsubishiMSCHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_MSY, MitsubishiMSYHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_SEZ, MitsubishiSEZKDXXHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_CKP, PanasonicCKPHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_DKE, PanasonicDKEHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_EKE, PanasonicEKEHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_JKE, PanasonicJKEHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_LKE, PanasonicLKEHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_NKE, PanasonicNKEHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_SAMSUNG_AQV, SamsungAQVHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_SAMSUNG_FJM, SamsungFJMHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_SHARP, SharpHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_TOSHIBA_DAISEIKAI, ToshibaDaiseikaiHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_TOSHIBA, ToshibaHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_ZHLT01, ZHLT01HeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_NIBE, NibeHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_QLIMA_1, Qlima1HeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_QLIMA_2, Qlima2HeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_SAMSUNG_AQV12MSAN, SamsungAQV12MSANHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_ZHJG01, ZHJG01HeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_AIRWAY, AIRWAYHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_BGH_AUD, BGHHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_ALTDKE, PanasonicAltDKEHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_PHILCO_PHS32, PhilcoPHS32HeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_VAILLANTVAI8, VaillantHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_R51M, R51MHeatpumpIR)
#undef HEATPUMPIR_PROTOCOL
void HeatpumpIRClimate::setup() {
auto protocol_constructor = PROTOCOL_CONSTRUCTOR_MAP.find(protocol_);
if (protocol_constructor == PROTOCOL_CONSTRUCTOR_MAP.end()) {
ESP_LOGE(TAG, "Invalid protocol");
return;
}
this->heatpump_ir_ = protocol_constructor->second();
climate_ir::ClimateIR::setup();
if (this->sensor_) {
this->sensor_->add_on_state_callback([this](float state) {
+20 -12
View File
@@ -89,25 +89,21 @@ enum VerticalDirection {
VERTICAL_DIRECTION_DOWN = 5,
};
/// Creates the HeatpumpIR library class for a protocol; instantiated in heatpumpir.cpp.
template<Protocol P> HeatpumpIR *create_heatpump_ir();
/// Selects the protocol at compile time so the enum and the library object always match.
template<Protocol P> struct ProtocolTag {};
// Temperature
const float TEMP_MIN = 0; // Celsius
const float TEMP_MAX = 100; // Celsius
class HeatpumpIRClimate final : public climate_ir::ClimateIR {
public:
HeatpumpIRClimate()
: climate_ir::ClimateIR(TEMP_MIN, TEMP_MAX, 1.0f, true, true,
{climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH,
climate::CLIMATE_FAN_AUTO},
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL,
climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_BOTH}) {}
template<Protocol P>
explicit HeatpumpIRClimate(ProtocolTag<P> /*tag*/) : HeatpumpIRClimate(P, create_heatpump_ir<P>()) {}
void setup() override;
void set_protocol(Protocol protocol) {
this->protocol_ = protocol;
if (this->has_fan_byte_presets_()) {
this->presets_ = {climate::CLIMATE_PRESET_NONE, climate::CLIMATE_PRESET_ECO, climate::CLIMATE_PRESET_BOOST};
}
}
void set_horizontal_default(HorizontalDirection horizontal_direction) {
this->default_horizontal_direction_ = horizontal_direction;
}
@@ -119,6 +115,18 @@ class HeatpumpIRClimate final : public climate_ir::ClimateIR {
void set_min_temperature(float temperature) { this->min_temperature_ = temperature; }
protected:
HeatpumpIRClimate(Protocol protocol, HeatpumpIR *heatpump_ir)
: climate_ir::ClimateIR(TEMP_MIN, TEMP_MAX, 1.0f, true, true,
{climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH,
climate::CLIMATE_FAN_AUTO},
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL,
climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_BOTH}),
heatpump_ir_(heatpump_ir),
protocol_(protocol) {
if (this->has_fan_byte_presets_()) {
this->presets_ = {climate::CLIMATE_PRESET_NONE, climate::CLIMATE_PRESET_ECO, climate::CLIMATE_PRESET_BOOST};
}
}
HeatpumpIR *heatpump_ir_;
void control(const climate::ClimateCall &call) override;
/// Transmit via IR the state of this climate controller.
+2 -2
View File
@@ -59,8 +59,8 @@ namespace esphome::host {
char **get_argv() { return s_argv; }
const std::string &get_exe_path() {
static const std::string empty;
return s_exe_path != nullptr ? *s_exe_path : empty;
static const std::string EMPTY;
return s_exe_path != nullptr ? *s_exe_path : EMPTY;
}
void arm_reexec(const std::string &path) {
+1 -1
View File
@@ -5,7 +5,7 @@
#include "esphome/core/log.h"
#include "core.h"
#include <time.h>
#include <ctime>
#include <unistd.h>
#include <cerrno>
#include <cstdlib>
+2 -2
View File
@@ -39,8 +39,8 @@ bool Mutex::try_lock() { return static_cast<std::mutex *>(handle_)->try_lock();
void Mutex::unlock() { static_cast<std::mutex *>(handle_)->unlock(); }
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
static const uint8_t esphome_host_mac_address[MAC_ADDRESS_SIZE] = USE_ESPHOME_HOST_MAC_ADDRESS;
memcpy(mac, esphome_host_mac_address, sizeof(esphome_host_mac_address));
static constexpr uint8_t HOST_MAC_ADDRESS[MAC_ADDRESS_SIZE] = USE_ESPHOME_HOST_MAC_ADDRESS;
memcpy(mac, HOST_MAC_ADDRESS, sizeof(HOST_MAC_ADDRESS));
}
} // namespace esphome
+4 -4
View File
@@ -2,6 +2,7 @@
#include <filesystem>
#include <fstream>
#include <utility>
#include "preferences.h"
#include "esphome/core/application.h"
#include "esphome/core/log.h"
@@ -45,11 +46,10 @@ void HostPreferences::setup_() {
break;
if (fread(&len, sizeof(len), 1, fp) != 1)
break;
uint8_t data[len];
if (fread(data, sizeof(uint8_t), len, fp) != len)
std::vector<uint8_t> data(len);
if (fread(data.data(), sizeof(uint8_t), len, fp) != len)
break;
std::vector vec(data, data + len);
this->data_[key] = vec;
this->data_[key] = std::move(data);
}
fclose(fp);
}
@@ -53,7 +53,7 @@ std::shared_ptr<HttpContainer> HttpRequestHost::perform(const char *url, const c
for (const auto &[name, value] : request_headers) {
h_headers.emplace(name, value);
}
httplib::Client client(scheme_host.c_str());
httplib::Client client(scheme_host);
if (!client.is_valid()) {
ESP_LOGE(TAG, "HTTP Request failed; Invalid URL: %s", url);
return nullptr;
@@ -114,7 +114,7 @@ std::shared_ptr<HttpContainer> HttpRequestHost::perform(const char *url, const c
}
container->content_length = container->response_body_.size();
for (auto header : response.headers) {
for (const auto &header : response.headers) {
ESP_LOGD(TAG, "Header: %s: %s", header.first.c_str(), header.second.c_str());
auto lower_name = str_lower_case(header.first); // NOLINT
if (should_collect_header(lower_case_collect_headers, lower_name)) {
+1
View File
@@ -3,4 +3,5 @@ DOMAIN = "ili9xxx"
DEPRECATED_COMPONENT = """
The 'ili9xxx' component is deprecated and no new models will be added to it.
New model PRs should target the newer and more performant 'mipi_spi' component.
It will be removed in ESPHome 2027.5.0.
"""
+3 -1
View File
@@ -220,8 +220,10 @@ FINAL_VALIDATE_SCHEMA = final_validate
async def to_code(config: ConfigType) -> None:
# Remove before 2027.5.0
LOGGER.warning(
"The 'ili9xxx' component is deprecated, it is recommended to use 'mipi_spi' instead."
"The 'ili9xxx' component is deprecated and will be removed in ESPHome 2027.5.0, "
"migrate to 'mipi_spi' instead."
)
rhs = MODELS[config[CONF_MODEL]].new()
var = cg.Pvariable(config[CONF_ID], rhs)
+2 -1
View File
@@ -2,12 +2,13 @@
#include "esphome/components/spi/spi.h"
#include "esphome/components/display/display_buffer.h"
#include "esphome/components/display/display_color_utils.h"
#include "esphome/core/log.h"
#include "ili9xxx_defines.h"
#include "ili9xxx_init.h"
namespace esphome::ili9xxx {
static const char *const TAG = "ili9xxx";
ESPHOME_LOG_TAG(TAG, "ili9xxx");
const size_t ILI9XXX_TRANSFER_BUFFER_SIZE = 126; // ensure this is divisible by 6
enum ILI9XXXColorMode {
+1 -5
View File
@@ -37,7 +37,6 @@ from esphome.const import (
CONF_WIDTH,
)
from esphome.cpp_generator import RawExpression
from esphome.final_validate import full_config
from esphome.types import ConfigType
AUTO_LOAD = ["split_buffer"]
@@ -348,11 +347,8 @@ def _final_validate(config: ConfigType) -> None:
config
)
global_config = full_config.get()
from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN
if CONF_LAMBDA not in config and CONF_PAGES not in config:
if LVGL_DOMAIN in global_config:
if display.has_external_writer(config[CONF_ID]):
if CONF_UPDATE_INTERVAL not in config:
config[CONF_UPDATE_INTERVAL] = update_interval("never")
else:
+1
View File
@@ -204,6 +204,7 @@ class IT8951Display : public Display,
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
int get_width() override { return (this->effective_transform_ & TRANSFORM_SWAP_XY) ? this->height_ : this->width_; }
int get_height() override { return (this->effective_transform_ & TRANSFORM_SWAP_XY) ? this->width_ : this->height_; }
bool pixel_axes_swapped() const override { return this->effective_transform_ & TRANSFORM_SWAP_XY; }
protected:
int get_height_internal() override { return this->height_; }
+11
View File
@@ -1,6 +1,7 @@
from esphome import automation
import esphome.codegen as cg
from esphome.components import uart
from esphome.config_helpers import filter_source_files_from_defines
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_ON_DATA, CONF_THROTTLE
from esphome.types import ConfigType
@@ -16,6 +17,8 @@ LD2450Component = ld2450_ns.class_("LD2450Component", cg.Component, uart.UARTDev
CONF_LD2450_ID = "ld2450_id"
CONF_POLYGON_ZONE_ID = "polygon_zone_id"
CONF_POLYGON_ZONES = "polygon_zones"
CONFIG_SCHEMA = cv.All(
cv.Schema(
@@ -50,6 +53,14 @@ _CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(CONF_ON_DATA, "add_on_data_callback"),
)
# Polygon zone sources build only when zones are configured
FILTER_SOURCE_FILES = filter_source_files_from_defines(
{
"polygon.cpp": "LD2450_POLYGON_ZONE_COUNT",
"polygon_zone.cpp": "LD2450_POLYGON_ZONE_COUNT",
}
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
+35 -1
View File
@@ -2,6 +2,7 @@ import esphome.codegen as cg
from esphome.components import binary_sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_FILTERS,
CONF_HAS_MOVING_TARGET,
CONF_HAS_STILL_TARGET,
CONF_HAS_TARGET,
@@ -11,7 +12,14 @@ from esphome.const import (
)
from esphome.types import ConfigType
from . import CONF_LD2450_ID, LD2450Component
from . import ( # noqa: F401 pylint: disable=unused-import
CONF_LD2450_ID,
CONF_POLYGON_ZONE_ID,
CONF_POLYGON_ZONES,
FILTER_SOURCE_FILES,
LD2450Component,
)
from .text import PolygonZone
DEPENDENCIES = ["ld2450"]
@@ -19,6 +27,18 @@ ICON_MEDITATION = "mdi:meditation"
ICON_SHIELD_ACCOUNT = "mdi:shield-account"
ICON_TARGET_ACCOUNT = "mdi:target-account"
def _reject_filters(config: ConfigType) -> ConfigType:
# Clearing a polygon makes presence unknown without going through filters, so they would get out of step
if CONF_FILTERS in config:
raise cv.Invalid(
"Polygon zone presence binary sensors can't have filters; use the ld2450 number platform's "
"'presence_timeout' to keep presence on after targets leave",
path=[CONF_FILTERS],
)
return config
CONFIG_SCHEMA = {
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component),
@@ -37,6 +57,16 @@ CONFIG_SCHEMA = {
filters=[{"settle": cv.TimePeriod(milliseconds=1000)}],
icon=ICON_MEDITATION,
),
cv.Optional(CONF_POLYGON_ZONES): cv.ensure_list(
binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_OCCUPANCY,
).extend(
{
cv.Required(CONF_POLYGON_ZONE_ID): cv.use_id(PolygonZone),
}
),
_reject_filters,
),
}
@@ -46,3 +76,7 @@ async def to_code(config: ConfigType) -> None:
await binary_sensors(CONF_HAS_TARGET, hub.set_target_binary_sensor)
await binary_sensors(CONF_HAS_MOVING_TARGET, hub.set_moving_target_binary_sensor)
await binary_sensors(CONF_HAS_STILL_TARGET, hub.set_still_target_binary_sensor)
for presence_conf in config.get(CONF_POLYGON_ZONES, []):
presence = await binary_sensor.new_binary_sensor(presence_conf)
zone = await cg.get_variable(presence_conf[CONF_POLYGON_ZONE_ID])
cg.add(zone.set_presence_binary_sensor(presence))
+31 -3
View File
@@ -179,6 +179,11 @@ void LD2450Component::setup() {
this->pref_ = this->presence_timeout_number_->make_entity_preference<float>();
this->set_presence_timeout();
}
#endif
#ifdef LD2450_POLYGON_ZONE_COUNT
for (PolygonZone *zone : this->polygon_zones_) {
zone->setup();
}
#endif
this->restart_and_read_all_info();
}
@@ -265,6 +270,12 @@ void LD2450Component::dump_config() {
LOG_BUTTON(" ", "FactoryReset", this->factory_reset_button_);
LOG_BUTTON(" ", "Restart", this->restart_button_);
#endif
#ifdef LD2450_POLYGON_ZONE_COUNT
ESP_LOGCONFIG(TAG, "Polygon Zones:");
for (PolygonZone *zone : this->polygon_zones_) {
zone->dump_config();
}
#endif
}
void LD2450Component::loop() {
@@ -465,15 +476,15 @@ void LD2450Component::handle_periodic_data_() {
#if defined(USE_BINARY_SENSOR) || defined(USE_SENSOR) || defined(USE_TEXT_SENSOR)
// Loop thru targets
for (index = 0; index < MAX_TARGETS; index++) {
#ifdef USE_SENSOR
// X
start = TARGET_X + index * 8;
is_moving = false;
// tx is used for further calculations, so always needs to be populated
// tx and ty are used for further calculations, so always need to be populated
tx = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]);
// Y
start = TARGET_Y + index * 8;
ty = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]);
#ifdef USE_SENSOR
// RESOLUTION
start = TARGET_RESOLUTION + index * 8;
res = (this->buffer_data_[start + 1] << 8) | this->buffer_data_[start];
@@ -542,6 +553,23 @@ void LD2450Component::handle_periodic_data_() {
still_target_count = target_count - moving_target_count;
#endif
#ifdef LD2450_POLYGON_ZONE_COUNT
const uint32_t now = App.get_loop_component_start_time();
for (PolygonZone *zone : this->polygon_zones_) {
if (!zone->has_polygon())
continue;
bool target_inside = false;
for (const Target &target : this->target_info_) {
// Untracked targets are stored as (0, 0)
if ((target.x != 0 || target.y != 0) && zone->contains(target.x, target.y)) {
target_inside = true;
break;
}
}
zone->update(target_inside, now, this->get_timeout_status_(zone->get_last_inside_ms()));
}
#endif
#ifdef USE_SENSOR
// Loop thru zones
uint8_t zone_still_targets = 0;
@@ -602,7 +630,7 @@ void LD2450Component::handle_periodic_data_() {
}
}
#endif
#ifdef USE_SENSOR
#ifdef USE_BINARY_SENSOR
// For presence timeout check
if (target_count > 0) {
this->presence_millis_ = App.get_loop_component_start_time();
+12 -1
View File
@@ -24,6 +24,10 @@
#include "esphome/components/binary_sensor/binary_sensor.h"
#endif
#ifdef LD2450_POLYGON_ZONE_COUNT
#include "polygon_zone.h"
#endif
#include "esphome/components/ld24xx/ld24xx.h"
#include "esphome/components/uart/uart.h"
#include "esphome/core/helpers.h"
@@ -137,6 +141,9 @@ class LD2450Component : public Component, public uart::UARTDevice {
void set_zone_target_count_sensor(uint8_t zone, sensor::Sensor *s);
void set_zone_still_target_count_sensor(uint8_t zone, sensor::Sensor *s);
void set_zone_moving_target_count_sensor(uint8_t zone, sensor::Sensor *s);
#endif
#ifdef LD2450_POLYGON_ZONE_COUNT
void register_polygon_zone(PolygonZone *zone) { this->polygon_zones_.push_back(zone); }
#endif
void reset_radar_zone();
void set_radar_zone(int32_t zone_type, int32_t zone1_x1, int32_t zone1_y1, int32_t zone1_x2, int32_t zone1_y2,
@@ -167,7 +174,7 @@ class LD2450Component : public Component, public uart::UARTDevice {
uint32_t presence_millis_ = 0;
uint32_t still_presence_millis_ = 0;
uint32_t moving_presence_millis_ = 0;
uint32_t timeout_ = 5;
uint32_t timeout_ = DEFAULT_PRESENCE_TIMEOUT * 1000; // presence_timeout, in ms
uint8_t buffer_data_[MAX_LINE_LENGTH];
uint8_t mac_address_[MAC_ADDRESS_SIZE] = {0, 0, 0, 0, 0, 0};
uint8_t version_[6] = {0, 0, 0, 0, 0, 0};
@@ -200,6 +207,10 @@ class LD2450Component : public Component, public uart::UARTDevice {
std::array<text_sensor::TextSensor *, MAX_TARGETS> direction_text_sensors_{};
#endif
#ifdef LD2450_POLYGON_ZONE_COUNT
StaticVector<PolygonZone *, LD2450_POLYGON_ZONE_COUNT> polygon_zones_;
#endif
LazyCallbackManager<void()> data_callback_;
};
+123
View File
@@ -0,0 +1,123 @@
#include "polygon.h"
#include "esphome/core/helpers.h"
#include <cstdio>
namespace esphome::ld2450 {
namespace {
class Scanner {
public:
Scanner(const char *str, size_t len) : pos_(str), end_(str + len) {}
bool at_end() const { return this->pos_ == this->end_; }
void skip_spaces() {
while (this->pos_ != this->end_ && *this->pos_ == ' ')
this->pos_++;
}
bool consume(char c) {
this->skip_spaces();
if (this->pos_ != this->end_ && *this->pos_ == c) {
this->pos_++;
return true;
}
return false;
}
/// Read an integer in [min, max]; returns false if there is none or it is out of range.
bool read_int(int16_t min, int16_t max, int16_t &out) {
bool negative = this->consume('-');
int32_t value = 0;
uint8_t digits = 0;
while (this->pos_ != this->end_ && *this->pos_ >= '0' && *this->pos_ <= '9') {
// Six digits is already beyond any valid coordinate; stop before value can overflow
if (++digits > 6)
return false;
value = value * 10 + (*this->pos_ - '0');
this->pos_++;
}
if (digits == 0)
return false;
if (negative)
value = -value;
if (value < min || value > max)
return false;
out = static_cast<int16_t>(value);
return true;
}
protected:
const char *pos_;
const char *end_;
};
} // namespace
bool Polygon::is_valid() const {
if (this->count == 0)
return true;
if (this->count < MIN_POLYGON_POINTS || this->count > MAX_POLYGON_POINTS)
return false;
for (uint8_t i = 0; i < this->count; i++) {
const Point &p = this->points[i];
if (p.x < POLYGON_MIN_X || p.x > POLYGON_MAX_X || p.y < POLYGON_MIN_Y || p.y > POLYGON_MAX_Y)
return false;
}
return true;
}
bool Polygon::contains(int16_t x, int16_t y) const {
bool inside = false;
for (uint8_t i = 0, j = this->count - 1; i < this->count; j = i++) {
const Point &a = this->points[i];
const Point &b = this->points[j];
if ((a.y > y) == (b.y > y))
continue;
// Does the edge a-b cross the ray going from (x, y) towards +x? Compare
// x - a.x < (y - a.y) * (b.x - a.x) / (b.y - a.y) without division; values fit in 32 bits.
const int32_t dy = static_cast<int32_t>(b.y) - a.y;
const int32_t lhs = (static_cast<int32_t>(x) - a.x) * dy;
const int32_t rhs = (static_cast<int32_t>(y) - a.y) * (static_cast<int32_t>(b.x) - a.x);
if (dy > 0 ? lhs < rhs : lhs > rhs)
inside = !inside;
}
return inside;
}
bool Polygon::parse(const char *str, size_t len) {
Polygon result;
Scanner scanner(str, len);
scanner.skip_spaces();
while (!scanner.at_end()) {
if (result.count == MAX_POLYGON_POINTS)
return false;
Point &p = result.points[result.count];
if (!scanner.read_int(POLYGON_MIN_X, POLYGON_MAX_X, p.x) || !scanner.consume(',') ||
!scanner.read_int(POLYGON_MIN_Y, POLYGON_MAX_Y, p.y))
return false;
result.count++;
bool separator = scanner.consume(';');
scanner.skip_spaces();
if (!separator && !scanner.at_end())
return false;
}
if (!result.is_valid())
return false;
*this = result;
return true;
}
void Polygon::format(char *buf) const {
size_t pos = 0;
buf[0] = '\0';
for (uint8_t i = 0; i < this->count; i++) {
pos = buf_append_printf(buf, POLYGON_STR_SIZE, pos, "%s%d,%d", i == 0 ? "" : ";", this->points[i].x,
this->points[i].y);
}
}
} // namespace esphome::ld2450
+53
View File
@@ -0,0 +1,53 @@
#pragma once
#include <array>
#include <cstddef>
#include <cstdint>
namespace esphome::ld2450 {
static constexpr uint8_t MIN_POLYGON_POINTS = 3;
// The most points whose text always fits Home Assistant's 255-character limit: 23 x "-4860,7560" + 22 x ';' = 252
static constexpr uint8_t MAX_POLYGON_POINTS = 23;
// Coordinate limits of the radar detection area, in mm
static constexpr int16_t POLYGON_MIN_X = -4860;
static constexpr int16_t POLYGON_MAX_X = 4860;
static constexpr int16_t POLYGON_MIN_Y = 0;
static constexpr int16_t POLYGON_MAX_Y = 7560;
// Longest point is "-4860,7560;" (11 characters), plus the null terminator
static constexpr size_t POLYGON_STR_SIZE = MAX_POLYGON_POINTS * 11 + 1;
struct Point {
int16_t x;
int16_t y;
};
/// A polygon in radar coordinates. Trivially copyable so it can be saved to preferences as-is.
struct Polygon {
std::array<Point, MAX_POLYGON_POINTS> points{};
uint8_t count{0};
/// True when the polygon has no points, which disables the zone.
bool empty() const { return this->count == 0; }
/// True when the polygon is empty, or has a valid number of points that are all inside the radar area.
bool is_valid() const;
/// Return whether the point is inside the polygon (even-odd rule). Always false for an empty polygon.
bool contains(int16_t x, int16_t y) const;
/** Parse "x,y;x,y;..." (mm) into this polygon.
*
* Spaces around numbers and a trailing ';' are allowed. An empty or all-space string gives an empty polygon.
* @return false if the text is invalid; the polygon is then left unchanged.
*/
bool parse(const char *str, size_t len);
/// Write the polygon as "x,y;x,y;..." into buf, which must hold at least POLYGON_STR_SIZE bytes.
void format(char *buf) const;
};
// text.py counts on this size
static_assert(sizeof(Polygon) == 94, "Polygon layout changed; update POLYGON_SAVED_BYTES in text.py");
} // namespace esphome::ld2450
@@ -0,0 +1,60 @@
#include "polygon_zone.h"
#ifdef LD2450_POLYGON_ZONE_COUNT
#include "esphome/core/log.h"
namespace esphome::ld2450 {
ESPHOME_LOG_TAG(TAG, "ld2450.polygon_zone");
void PolygonZone::setup() {
// Bump the version when Polygon's layout changes
this->pref_ = this->make_entity_preference<Polygon>(1);
Polygon saved;
if (this->pref_.load(&saved)) {
if (saved.is_valid()) {
this->polygon_ = saved;
} else {
ESP_LOGW(TAG, "'%s': saved polygon is invalid; zone disabled", this->get_name().c_str());
}
}
// Presence stays unknown until the first radar frame
this->publish_polygon_();
}
void PolygonZone::dump_config() {
char polygon_s[POLYGON_STR_SIZE];
this->polygon_.format(polygon_s);
LOG_TEXT(" ", "PolygonZone", this);
ESP_LOGCONFIG(TAG, " Polygon: '%s'", polygon_s);
LOG_BINARY_SENSOR(" ", "Presence", this->presence_);
}
void PolygonZone::control(const std::string &value) {
Polygon polygon;
if (!polygon.parse(value.c_str(), value.size())) {
ESP_LOGW(TAG, "'%s': invalid polygon '%s'; expected 'x,y;x,y;...' with %u to %u points", this->get_name().c_str(),
value.c_str(), MIN_POLYGON_POINTS, MAX_POLYGON_POINTS);
} else if (!this->pref_.save(&polygon)) {
// On ESP8266, the space for saved settings is small and can run out with several zones
ESP_LOGW(TAG, "'%s': could not save the polygon; keeping the previous one", this->get_name().c_str());
} else {
this->polygon_ = polygon;
// A target seen inside the previous polygon must not keep the new one on
this->last_inside_ms_ = 0;
if (this->polygon_.empty())
this->presence_->invalidate_state();
}
// On error this publishes the previous polygon again, so the frontend shows the value in use
this->publish_polygon_();
}
void PolygonZone::publish_polygon_() {
char polygon_s[POLYGON_STR_SIZE];
this->polygon_.format(polygon_s);
this->publish_state(polygon_s);
}
} // namespace esphome::ld2450
#endif // LD2450_POLYGON_ZONE_COUNT
+63
View File
@@ -0,0 +1,63 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef LD2450_POLYGON_ZONE_COUNT
#include "esphome/components/binary_sensor/binary_sensor.h"
#include "esphome/components/text/text.h"
#include "esphome/core/preferences.h"
#include "polygon.h"
namespace esphome::ld2450 {
/** A zone with any polygon shape. The text state holds the polygon, so it can be edited and read back from HA.
*
* The polygon is in the radar's raw coordinates (mm), and targets are checked against it as decoded from each
* radar frame, before any sensor filter. Filters on the target X/Y sensors change only what they publish, so a
* filter that changes their values (multiply, offset, lambda, ...) puts them in a different frame from the polygon.
*
* Like the hub's has_target binary sensors, presence stays on until presence_timeout has passed since a target was
* last inside the zone.
*/
class PolygonZone : public text::Text {
public:
/// Config validation guarantees every zone gets exactly one presence binary sensor.
void set_presence_binary_sensor(binary_sensor::BinarySensor *presence) { this->presence_ = presence; }
/// Load the saved polygon and publish it. Called from the hub's setup().
void setup();
void dump_config();
/** Publish presence for one radar frame. A zone without a polygon keeps presence unknown.
*
* @param target_inside whether any target is inside the zone in this frame.
* @param now the frame's time, in ms.
* @param timed_out whether presence_timeout has passed since get_last_inside_ms().
*/
void update(bool target_inside, uint32_t now, bool timed_out) {
if (this->polygon_.empty())
return;
if (target_inside) {
this->last_inside_ms_ = now;
this->presence_->publish_state(true);
} else if (timed_out) {
this->presence_->publish_state(false);
}
}
/// When a target was last inside the zone, in ms; 0 if never since the polygon was set.
uint32_t get_last_inside_ms() const { return this->last_inside_ms_; }
bool contains(int16_t x, int16_t y) const { return this->polygon_.contains(x, y); }
bool has_polygon() const { return !this->polygon_.empty(); }
protected:
void control(const std::string &value) override;
void publish_polygon_();
binary_sensor::BinarySensor *presence_{nullptr};
Polygon polygon_;
ESPPreferenceObject pref_;
uint32_t last_inside_ms_{0};
};
} // namespace esphome::ld2450
#endif // LD2450_POLYGON_ZONE_COUNT
+1 -1
View File
@@ -17,7 +17,7 @@ from esphome.const import (
)
from esphome.types import ConfigType
from . import CONF_LD2450_ID, LD2450Component
from . import CONF_LD2450_ID, FILTER_SOURCE_FILES, LD2450Component # noqa: F401 pylint: disable=unused-import
DEPENDENCIES = ["ld2450"]
+120
View File
@@ -0,0 +1,120 @@
from esphome import preferences
import esphome.codegen as cg
from esphome.components import text
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_PLATFORM, ENTITY_CATEGORY_CONFIG
import esphome.final_validate as fv
from esphome.types import ConfigType
from . import ( # noqa: F401 pylint: disable=unused-import
CONF_LD2450_ID,
CONF_POLYGON_ZONE_ID,
CONF_POLYGON_ZONES,
DOMAIN,
FILTER_SOURCE_FILES,
LD2450Component,
ld2450_ns,
)
DEPENDENCIES = ["ld2450"]
ICON_VECTOR_POLYGON = "mdi:vector-polygon"
# Must match MIN_POLYGON_POINTS and MAX_POLYGON_POINTS in polygon.h
MIN_POLYGON_POINTS = 3
MAX_POLYGON_POINTS = 23
# Matches what Polygon::parse() accepts, apart from coordinate ranges, which only the device checks:
# empty, or MIN_POLYGON_POINTS to MAX_POLYGON_POINTS "x,y" points separated by ';'. HA validates input with it.
# No character classes: the HA frontend compiles it with the JS "v" flag, HA core with Python re.
POLYGON_PATTERN = (
r"^ *$|^ *-?\d{1,4} *, *\d{1,4} *"
rf"(; *-?\d{{1,4}} *, *\d{{1,4}} *){{{MIN_POLYGON_POINTS - 1},{MAX_POLYGON_POINTS - 1}}};? *$"
)
# Home Assistant's limit for a state; the longest polygon the device publishes is 252 characters
POLYGON_MAX_LENGTH = 255
# sizeof(Polygon), pinned by a static_assert in polygon.h
POLYGON_SAVED_BYTES = 94
PolygonZone = ld2450_ns.class_("PolygonZone", text.Text)
_request_polygon_zone_slot = cg.slot_counter("LD2450_POLYGON_ZONE_COUNT")
def _require_manual_id(config: ConfigType) -> ConfigType:
if not config[CONF_ID].is_manual:
raise cv.Invalid(
f"'{CONF_ID}' is required, so the zone's presence binary sensor can refer to it",
path=[CONF_ID],
)
return config
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component),
# Polygons are in raw radar mm, checked before sensor filters (polygon_zone.h)
cv.Required(CONF_POLYGON_ZONES): cv.ensure_list(
text.text_schema(
PolygonZone,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_VECTOR_POLYGON,
mode="TEXT",
),
_require_manual_id,
),
}
)
def _final_validate(config: ConfigType) -> None:
"""Each polygon zone must have exactly one presence binary sensor, on the same LD2450."""
full_config = fv.full_config.get()
# Polygon zone id -> the LD2450 id of each presence binary sensor that uses it
references: dict[str, list[str]] = {}
for binary_sensor_conf in full_config.get("binary_sensor", []):
if binary_sensor_conf.get(CONF_PLATFORM) != "ld2450":
continue
hub_id = binary_sensor_conf[CONF_LD2450_ID].id
for presence_conf in binary_sensor_conf.get(CONF_POLYGON_ZONES, []):
zone_id = presence_conf[CONF_POLYGON_ZONE_ID].id
references.setdefault(zone_id, []).append(hub_id)
zone_hub_id = config[CONF_LD2450_ID].id
for index, zone_conf in enumerate(config[CONF_POLYGON_ZONES]):
zone_id = zone_conf[CONF_ID].id
hub_ids = references.get(zone_id, [])
if len(hub_ids) != 1:
problem = (
"has no presence binary sensor"
if not hub_ids
else "is used by more than one presence binary sensor"
)
raise cv.Invalid(
f"Polygon zone '{zone_id}' {problem}; each zone needs exactly one, under "
f"'binary_sensor: - platform: ld2450' -> '{CONF_POLYGON_ZONES}'",
path=[CONF_POLYGON_ZONES, index],
)
if hub_ids[0] != zone_hub_id:
raise cv.Invalid(
f"Polygon zone '{zone_id}' belongs to LD2450 '{zone_hub_id}', but its presence "
f"binary sensor is under LD2450 '{hub_ids[0]}'; use the same '{CONF_LD2450_ID}'",
path=[CONF_POLYGON_ZONES, index],
)
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD2450_ID])
# Each zone saves its polygon; the ESP8266 checks that every component's saved values fit
preferences.reserve_preferences(
len(config[CONF_POLYGON_ZONES]), POLYGON_SAVED_BYTES
)
for zone_conf in config[CONF_POLYGON_ZONES]:
zone = await text.new_text(
zone_conf, max_length=POLYGON_MAX_LENGTH, pattern=POLYGON_PATTERN
)
_request_polygon_zone_slot(str(hub))
cg.add(hub.register_polygon_zone(zone))
+1 -1
View File
@@ -14,7 +14,7 @@ from esphome.const import (
)
from esphome.types import ConfigType
from . import CONF_LD2450_ID, LD2450Component
from . import CONF_LD2450_ID, FILTER_SOURCE_FILES, LD2450Component # noqa: F401 pylint: disable=unused-import
DEPENDENCIES = ["ld2450"]
+2 -1
View File
@@ -246,7 +246,7 @@ static void ble_scan_callback(void *param) {
memcpy(slot->data, info->data, slot->data_len);
s_ble->push_scan_report(slot);
}
} // NOLINT(clang-analyzer-unix.Malloc) the slot is a pool entry handed to push_scan_report(), not a leak
BLEScanReport *LN882HBLE::allocate_scan_report() {
BLEScanReport *slot = this->report_pool_.allocate();
@@ -287,6 +287,7 @@ void LN882HBLE::enable() {
return;
this->state_ = BLEComponentState::ENABLING;
// NOLINTNEXTLINE(performance-no-int-to-ptr) - MMIO register at a fixed address
*reinterpret_cast<volatile uint32_t *>(BLE_COEX_PTI_REG_ADDR) = BLE_COEX_PTI_ENABLE_ALL;
soc_module_clk_gate_enable(CLK_G_BLE);
+11 -3
View File
@@ -335,6 +335,16 @@ def final_validation(config_list):
widget_type.final_validate(name, conf, widget_conf, path[1:])
def lv_conf_path_flag(lv_conf_h_file: Path) -> str:
"""The ``-DLV_CONF_PATH`` build flag, decoded identically by the shell
lexers (PlatformIO, host, ESP8266) and the quoted CMake arguments
(ESP-IDF, Zephyr) the build flags pass through."""
# forward slashes for Windows, then escape what shlex or a quoted CMake
# argument would not pass through literally
path = re.sub(r"[^\w/.:+=,-]", r"\\\g<0>", Path(lv_conf_h_file).as_posix())
return f'-DLV_CONF_PATH=\\"{path}\\"'
async def to_code(configs):
config_0 = configs[0]
# Global configuration
@@ -563,9 +573,7 @@ async def to_code(configs):
if write_file_if_changed(lv_conf_h_file, generate_lv_conf_h()):
clean_build(clear_pio_cache=False)
cg.add_build_flag("-DLV_CONF_H=1")
# handle windows paths in a way that doesn't break the generated C++
lv_conf_h_path = Path(lv_conf_h_file).as_posix()
cg.add_build_flag(f'-DLV_CONF_PATH=\\"{lv_conf_h_path}\\"')
cg.add_build_flag(lv_conf_path_flag(lv_conf_h_file))
cg.add_build_flag("-DLV_KCONFIG_IGNORE")
for prop in df.get_remapped_uses():
+1 -1
View File
@@ -214,7 +214,7 @@ enum class Orientation : uint8_t {
};
class LvglComponent final : public PollingComponent {
constexpr static const char *const TAG = "lvgl";
constexpr static const char *const TAG = "lvgl"; // NOLINT: lvgl does not build for ESP8266
public:
LvglComponent(std::vector<display::Display *> displays, float buffer_frac, bool full_refresh, int draw_rounding,
+1 -1
View File
@@ -19,7 +19,7 @@ using alloc_string_t = std::basic_string<char, std::char_traits<char>, RAMAlloca
* @tparam V The type of the value in the mapping. Should be a basic type or pointer.
*/
static const char *const TAG = "mapping";
ESPHOME_LOG_TAG(TAG, "mapping");
template<typename K, typename V> class Mapping {
public:
+19
View File
@@ -3,7 +3,9 @@ from esphome.components.esp32 import add_idf_component, add_idf_sdkconfig_option
from esphome.config_helpers import filter_source_files_from_platform, get_logger_level
import esphome.config_validation as cv
from esphome.const import (
CONF_API,
CONF_DISABLED,
CONF_ETHERNET,
CONF_ID,
CONF_OPENTHREAD,
CONF_PORT,
@@ -285,6 +287,23 @@ async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
# With both wifi and ethernet, the _esphomelib "network" TXT value follows the
# connected interface. OpenThread hands its services to the SRP client once,
# so it keeps the fixed value.
if (
CONF_API in CORE.config
and CONF_WIFI in CORE.config
and CONF_ETHERNET in CORE.config
and CONF_OPENTHREAD not in CORE.config
):
from esphome.components import network
cg.add_define("USE_MDNS_DYNAMIC_NETWORK_TXT")
# Where the network component tracks the default route (ESP32), follow
# it; otherwise mdns checks the connected interface every second.
if await network.register_active_interface_listener(var):
cg.add_define("USE_MDNS_ACTIVE_INTERFACE_LISTENER")
for service in config[CONF_SERVICES]:
txt_records = [
await _mdns_txt_record_templated(var, txt_key, txt_value)
+64 -1
View File
@@ -20,6 +20,10 @@
#ifdef USE_DASHBOARD_IMPORT
#include "esphome/components/dashboard_import/dashboard_import.h"
#endif
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
#include "esphome/components/ethernet/ethernet_component.h"
#include "esphome/components/wifi/wifi_component.h"
#endif
namespace esphome::mdns {
@@ -39,6 +43,57 @@ MDNS_STATIC_CONST_CHAR(SERVICE_TCP, "_tcp");
// Wrap build-time defines into flash storage
MDNS_STATIC_CONST_CHAR(VALUE_VERSION, ESPHOME_VERSION);
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
MDNS_STATIC_CONST_CHAR(NETWORK_WIFI, "wifi");
MDNS_STATIC_CONST_CHAR(NETWORK_ETHERNET, "ethernet");
// Same order as the default route in NetworkComponent::loop()
const char *MDNSComponent::connected_network_() const {
const bool ethernet = ethernet::global_eth_component->is_connected();
const bool wifi = wifi::global_wifi_component->is_connected();
#ifdef USE_NETWORK_PRIMARY_INTERFACE_WIFI
if (wifi)
return NETWORK_WIFI;
#endif
if (ethernet)
return NETWORK_ETHERNET;
if (wifi)
return NETWORK_WIFI;
return nullptr;
}
void MDNSComponent::update_network_txt_(const char *network) {
// With no interface connected, keep the last value instead of flapping to wifi
if (this->network_txt_ == nullptr || network == nullptr || network == this->network_txt_)
return;
if (!this->publish_network_txt_(network)) {
// The _esphomelib service is missing, so a retry would fail too
this->network_txt_ = nullptr;
return;
}
ESP_LOGD(TAG, "Network TXT: %s", network);
this->network_txt_ = network;
#ifdef USE_MDNS_STORE_SERVICES
this->network_record_->value = MDNS_STR(network);
#endif
}
#ifdef USE_MDNS_ACTIVE_INTERFACE_LISTENER
void MDNSComponent::on_active_interface(network::NetworkInterface interface) {
switch (interface) {
case network::NetworkInterface::NETWORK_INTERFACE_ETHERNET:
this->update_network_txt_(NETWORK_ETHERNET);
break;
case network::NetworkInterface::NETWORK_INTERFACE_WIFI:
this->update_network_txt_(NETWORK_WIFI);
break;
default:
break;
}
}
#endif
#endif
void MDNSComponent::setup_buffers_and_register_(PlatformRegisterFn platform_register) {
#ifdef USE_MDNS_STORE_SERVICES
auto &services = this->services_;
@@ -160,7 +215,15 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
txt_records.push_back({MDNS_STR(TXT_BOARD), MDNS_STR(VALUE_BOARD)});
#if defined(USE_WIFI)
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
const char *network = this->connected_network_();
this->network_txt_ = network != nullptr ? network : NETWORK_WIFI;
txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(this->network_txt_)});
#ifdef USE_MDNS_STORE_SERVICES
// services is services_ here, which never reallocates
this->network_record_ = &txt_records.back();
#endif
#elif defined(USE_WIFI)
MDNS_STATIC_CONST_CHAR(NETWORK_WIFI, "wifi");
txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_WIFI)});
#elif defined(USE_ETHERNET)
+31
View File
@@ -32,6 +32,13 @@
#define USE_MDNS_DEVICE_INFO_TXT
#endif
// USE_MDNS_DYNAMIC_NETWORK_TXT (wifi + ethernet + api): the _esphomelib "network" TXT value follows the
// connected interface. With USE_MDNS_ACTIVE_INTERFACE_LISTENER (ESP32) the network component reports
// default route changes; otherwise the value is checked every NETWORK_TXT_CHECK_INTERVAL_MS.
#ifdef USE_MDNS_ACTIVE_INTERFACE_LISTENER
#include "esphome/components/network/network_component.h"
#endif
namespace esphome::mdns {
// Helper struct that identifies strings that may be stored in flash storage (similar to LogString)
@@ -73,6 +80,10 @@ class MDNSComponent final : public Component
,
public ethernet::EthernetIPStateListener
#endif
#ifdef USE_MDNS_ACTIVE_INTERFACE_LISTENER
,
public network::ActiveInterfaceListener
#endif
{
public:
void setup() override;
@@ -134,6 +145,10 @@ class MDNSComponent final : public Component
}
#endif
#ifdef USE_MDNS_ACTIVE_INTERFACE_LISTENER
void on_active_interface(network::NetworkInterface interface) override;
#endif
#ifdef USE_MDNS_EVENT_DRIVEN_POLLING
void on_ip_state(const network::IPAddresses &ips, const network::IPAddress &dns1,
const network::IPAddress &dns2) override;
@@ -150,6 +165,22 @@ class MDNSComponent final : public Component
void setup_buffers_and_register_(PlatformRegisterFn platform_register);
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
static constexpr uint32_t NETWORK_TXT_CHECK_INTERVAL_MS = 1000;
/// The first connected interface in network: priority order ("ethernet" or "wifi"), else nullptr.
const char *connected_network_() const;
/// Publish network as the "network" TXT value if it changed; nullptr keeps the current value.
void update_network_txt_(const char *network);
/// Platform-specific: replace the "network" TXT value of the registered _esphomelib service.
bool publish_network_txt_(const char *network);
/// Advertised value; nullptr until the _esphomelib service is registered, and after a failed publish.
const char *network_txt_{nullptr};
#ifdef USE_MDNS_STORE_SERVICES
/// The "network" record in services_, kept in step with network_txt_.
MDNSTXTRecord *network_record_{nullptr};
#endif
#endif
#ifdef USE_MDNS_DYNAMIC_TXT
/// Storage for runtime-generated TXT values from user lambdas
/// Pre-sized at compile time via MDNS_DYNAMIC_TXT_COUNT to avoid heap allocations.
+19 -1
View File
@@ -89,7 +89,25 @@ bool MDNSComponent::set_service_enabled(const char *service_type, const char *pr
}
#endif // USE_MDNS_SUPPORTS_ENABLE_DISABLE && !USE_OPENTHREAD
void MDNSComponent::setup() { this->setup_buffers_and_register_(register_esp32); }
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
bool MDNSComponent::publish_network_txt_(const char *network) {
// ESP-IDF copies the value and announces the changed record
esp_err_t err = mdns_service_txt_item_set("_esphomelib", "_tcp", "network", network);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Failed to update network TXT: %s", esp_err_to_name(err));
return false;
}
return true;
}
#endif
void MDNSComponent::setup() {
this->setup_buffers_and_register_(register_esp32);
#if defined(USE_MDNS_DYNAMIC_NETWORK_TXT) && !defined(USE_MDNS_ACTIVE_INTERFACE_LISTENER)
this->set_interval(NETWORK_TXT_CHECK_INTERVAL_MS,
[this]() { this->update_network_txt_(this->connected_network_()); });
#endif
}
void MDNSComponent::on_shutdown() {
#ifndef USE_OPENTHREAD
+20
View File
@@ -46,7 +46,27 @@ void MDNSComponent::start_polling_window_() {
}
#endif
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
ESPHOME_LOG_TAG(TAG, "mdns");
bool MDNSComponent::publish_network_txt_(const char *network) {
// LEAmDNS replaces the value of an existing key; announce() sends the change
if (!MDNS.addServiceTxt("esphomelib", "tcp", "network", network)) {
ESP_LOGW(TAG, "Failed to update network TXT");
return false;
}
MDNS.announce();
this->start_polling_window_();
return true;
}
#endif
void MDNSComponent::setup() {
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
// No-op until the first IP-up event registers the services
this->set_interval(NETWORK_TXT_CHECK_INTERVAL_MS,
[this]() { this->update_network_txt_(this->connected_network_()); });
#endif
// arduino-pico stubs out LwipIntf::stateUpCB (the netif status callback LEAmDNS uses
// on ESP8266 for auto-restart), so we must drive begin()/notifyAPChange() from our
// own IP state listener. Both WiFi and Ethernet have the same listener signature —
@@ -1,9 +1,20 @@
#include "microphone_source.h"
#include <array>
#include <gain.h> // esp-audio-libs
#include <pcm_convert.h> // esp-audio-libs
namespace esphome::microphone {
static const int32_t Q25_MAX_VALUE = (1 << 25) - 1;
static const int32_t Q25_MIN_VALUE = ~Q25_MAX_VALUE;
// Builds the exact LinearGain for an integer factor without the float dB conversion: the factor is
// mantissa / 2^31 * 2^exponent, with the exponent set to the factor's bit width.
static esp_audio_libs::gain::LinearGain factor_to_linear_gain(uint32_t factor) {
esp_audio_libs::gain::LinearGain gain;
gain.exponent = 32 - __builtin_clz(factor);
gain.mantissa_q31 = static_cast<int32_t>(factor << (31 - gain.exponent));
return gain;
}
audio::AudioStreamInfo MicrophoneSource::get_audio_stream_info() {
return audio::AudioStreamInfo(this->bits_per_sample_, this->channels_.count(),
@@ -26,50 +37,26 @@ void MicrophoneSource::stop() {
}
void MicrophoneSource::process_audio_(const std::vector<uint8_t> &data, std::vector<uint8_t> &filtered_data) {
// - Bit depth conversions are obtained by truncating bits or padding with zeros - no dithering is applied.
// - In the comments, Qxx refers to a fixed point number with xx bits of precision for representing fractional values.
// For example, audio with a bit depth of 16 can store a sample in a int16, which can be considered a Q15 number.
// - All samples are converted to Q25 before applying the gain factor - this results in a small precision loss for
// data with 32 bits per sample. Since the maximum gain factor is 64 = (1<<6), this ensures that applying the gain
// will never overflow a 32 bit signed integer. This still retains more bit depth than what is audibly noticeable.
// - Loops for reading/writing data buffers are unrolled, assuming little endian, for a small performance increase.
const audio::AudioStreamInfo source_info = this->mic_->get_audio_stream_info();
const uint32_t total_frames = source_info.bytes_to_frames(data.size());
const uint8_t target_bytes_per_sample = (this->bits_per_sample_ + 7) / 8;
const size_t source_bytes_per_sample = this->mic_->get_audio_stream_info().samples_to_bytes(1);
const uint32_t source_channels = this->mic_->get_audio_stream_info().get_channels();
const size_t source_bytes_per_frame = this->mic_->get_audio_stream_info().frames_to_bytes(1);
const uint32_t total_frames = this->mic_->get_audio_stream_info().bytes_to_frames(data.size());
const size_t target_bytes_per_sample = (this->bits_per_sample_ + 7) / 8;
const size_t target_bytes_per_frame = target_bytes_per_sample * this->channels_.count();
filtered_data.resize(target_bytes_per_frame * total_frames);
uint8_t *current_data = filtered_data.data();
for (uint32_t frame_index = 0; frame_index < total_frames; ++frame_index) {
for (uint32_t channel_index = 0; channel_index < source_channels; ++channel_index) {
if (this->channels_.test(channel_index)) {
// Channel's current sample is included in the target mask. Convert bits per sample, if necessary.
const uint32_t sample_index = frame_index * source_bytes_per_frame + channel_index * source_bytes_per_sample;
int32_t sample = audio::unpack_audio_sample_to_q31(&data[sample_index], source_bytes_per_sample); // Q31
sample >>= 6; // Q31 -> Q25
// Apply gain using multiplication
sample *= this->gain_factor_; // Q25
// Clamp ``sample`` in case gain multiplication overflows 25 bits
sample = clamp<int32_t>(sample, Q25_MIN_VALUE, Q25_MAX_VALUE); // Q25
sample *= (1 << 6); // Q25 -> Q31
audio::pack_q31_as_audio_sample(sample, current_data, target_bytes_per_sample);
current_data = current_data + target_bytes_per_sample;
}
// Enabled channels in ascending order, which is their order in each output frame
std::array<uint8_t, 8> channel_map;
uint8_t target_channels = 0;
for (size_t channel = 0; channel < this->channels_.size(); ++channel) {
if (this->channels_.test(channel)) {
channel_map[target_channels++] = static_cast<uint8_t>(channel);
}
}
filtered_data.resize(static_cast<size_t>(total_frames) * target_bytes_per_sample * target_channels);
// Applies the gain at full precision before narrowing; samples saturate at full scale, and narrowing rounds
esp_audio_libs::pcm_convert::copy_frames_with_gain(data.data(), filtered_data.data(), source_info.samples_to_bytes(1),
source_info.get_channels(), target_bytes_per_sample,
channel_map.data(), target_channels,
factor_to_linear_gain(this->gain_factor_), total_frames);
}
} // namespace esphome::microphone
+1 -1
View File
@@ -5,7 +5,7 @@
namespace esphome::midea::ac {
const char *const Constants::TAG = "midea";
const char Constants::TAG[] PROGMEM = "midea";
const char *const Constants::FREEZE_PROTECTION = "Freeze Protection";
const char *const Constants::SILENT = "Silent";
const char *const Constants::TURBO = "Turbo";
+1 -1
View File
@@ -17,7 +17,7 @@ using MideaPreset = dudanov::midea::ac::Preset;
class Constants {
public:
static const char *const TAG;
static const char TAG[]; // PROGMEM, like ESPHOME_LOG_TAG
static const char *const FREEZE_PROTECTION;
static const char *const SILENT;
static const char *const TURBO;
+27 -7
View File
@@ -127,6 +127,19 @@ ClimateTraits AirConditioner::traits() {
return traits;
}
#ifdef USE_REMOTE_TRANSMITTER
static const LogString *display_control_str(DisplayControl control) {
switch (control) {
case DisplayControl::DISPLAY_CONTROL_UART:
return LOG_STR("UART");
case DisplayControl::DISPLAY_CONTROL_IR:
return LOG_STR("IR");
default:
return LOG_STR("AUTO");
}
}
#endif
void AirConditioner::dump_config() {
ESP_LOGCONFIG(Constants::TAG,
"MideaDongle:\n"
@@ -135,7 +148,10 @@ void AirConditioner::dump_config() {
" [x] Request attempts: %d",
this->base_.getPeriod(), this->base_.getTimeout(), this->base_.getNumAttempts());
#ifdef USE_REMOTE_TRANSMITTER
ESP_LOGCONFIG(Constants::TAG, " [x] Using RemoteTransmitter");
ESP_LOGCONFIG(Constants::TAG,
" [x] Using RemoteTransmitter\n"
" [x] Display control: %s",
LOG_STR_ARG(display_control_str(this->display_control_)));
#endif
if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_OK) {
this->base_.getCapabilities().dump();
@@ -183,16 +199,20 @@ void AirConditioner::do_swing_step() {
}
void AirConditioner::do_display_toggle() {
if (this->base_.getCapabilities().supportLightControl()) {
this->base_.displayToggle();
} else {
#ifdef USE_REMOTE_TRANSMITTER
// AUTO uses IR unless the appliance reports light control over UART. Without a transmitter
// built in, every setting sends the UART request: Mundoclima, Rotenso and others accept it
// without advertising it in the 0xB5 capabilities report.
const bool use_ir = this->display_control_ == DisplayControl::DISPLAY_CONTROL_IR ||
(this->display_control_ == DisplayControl::DISPLAY_CONTROL_AUTO &&
!this->base_.getCapabilities().supportLightControl());
if (use_ir) {
IrSpecialData data(0x08);
this->transmitter_.transmit(data);
#else
ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component");
#endif
return;
}
#endif
this->base_.displayToggle();
}
} // namespace esphome::midea::ac

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