Merge remote-tracking branch 'origin/dev' into HEAD

This commit is contained in:
J. Nick Koston
2026-10-11 08:25:47 -10:00
170 changed files with 6151 additions and 745 deletions
+86
View File
@@ -1335,6 +1335,91 @@ jobs:
# config first, so a separate config pass is redundant.
python3 script/test_build_components.py -e compile -t esp8266-ard -c "$TEST_COMPONENTS" -f --toolchain arduino --fail-on-no-tests
test-esp8266-native-windows:
name: Compile esp8266 with the native toolchain on Windows (ccache ${{ matrix.ccache }})
runs-on: windows-latest
needs:
- common
- determine-jobs
if: github.event_name == 'pull_request' && needs.determine-jobs.outputs.esp8266-native == 'true'
# A hung compiler must fail the job, not hold the runner for six hours.
timeout-minutes: 40
strategy:
fail-fast: false
matrix:
# Both ways the build runs on Windows: ESPHome Desktop bundles
# ccache and puts it on PATH, a pip install has none.
ccache: ["1", "0"]
env:
ESPHOME_CCACHE_ENABLE: ${{ matrix.ccache }}
# The test script prints characters the cp1252 console cannot encode
PYTHONUTF8: "1"
# The exact static build ESPHome Desktop ships
CCACHE_URL: https://github.com/ccache/ccache/releases/download/v4.14/ccache-4.14-windows-x86_64.zip
CCACHE_SHA256: 2568347a697e103ca1b073981c704ad76fb2507d066c38dba038dd73399d968f
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Install ccache
if: matrix.ccache == '1'
shell: pwsh
run: |
$dir = Join-Path $env:RUNNER_TEMP "ccache"
New-Item -ItemType Directory -Force $dir | Out-Null
$zip = Join-Path $dir "ccache.zip"
Invoke-WebRequest -Uri $env:CCACHE_URL -OutFile $zip
$hash = (Get-FileHash $zip -Algorithm SHA256).Hash.ToLower()
if ($hash -ne $env:CCACHE_SHA256) {
Write-Host "::error::ccache archive sha256 $hash does not match $env:CCACHE_SHA256"
exit 1
}
Expand-Archive -Path $zip -DestinationPath $dir
$bin = Get-ChildItem -Path $dir -Recurse -Filter ccache.exe | Select-Object -First 1
Add-Content -Path $env:GITHUB_PATH -Value $bin.DirectoryName
& $bin.FullName --version
- name: Compile
shell: pwsh
# One component test: web_server brings wifi, api and the async web
# server libraries, the shape of a typical ESP8266 device
run: |
. .\venv\Scripts\Activate.ps1
$arguments = "script/test_build_components.py", "-e", "compile", "-t", "esp8266-ard", "-c", "web_server", "--base-only", "--toolchain", "arduino", "--fail-on-no-tests"
$p = Start-Process -FilePath python -ArgumentList $arguments -NoNewWindow -PassThru
# Reading the handle keeps ExitCode available after the process exits
$null = $p.Handle
if (-not $p.WaitForExit(25 * 60 * 1000)) {
Write-Host "::error::The compile did not finish in 25 minutes; processes still running:"
Get-CimInstance Win32_Process |
Where-Object { $_.CommandLine -match 'xtensa|ccache|ninja|esphome' } |
Select-Object ProcessId, ParentProcessId, CreationDate, CommandLine |
Format-List | Out-String -Width 4096 | Write-Host
Get-Process |
Where-Object { $_.Name -match 'cc1|ccache|ninja|xtensa|python' } |
Select-Object Id, Name, CPU, WorkingSet64 |
Format-Table | Out-String | Write-Host
taskkill /PID $p.Id /T /F
exit 1
}
exit $p.ExitCode
- name: Show ccache statistics
# Also after a failed or killed compile: the counters say whether
# ccache was ever reached
if: always() && matrix.ccache == '1'
shell: pwsh
# The build keeps its store under the esphome tools cache
run: |
$env:CCACHE_DIR = Join-Path $env:LOCALAPPDATA "esphome\Cache\arduino8266\ccache"
ccache -s
device-builder:
name: Test downstream esphome/device-builder
runs-on: ubuntu-24.04
@@ -1705,6 +1790,7 @@ jobs:
- test-build-components-split
- test-esp32-platformio
- test-esp8266-native
- test-esp8266-native-windows
- device-builder
- memory-impact-target-branch
- memory-impact-pr-branch
+15 -1
View File
@@ -16,16 +16,18 @@ jobs:
# No GITHUB_TOKEN permissions: the reusable workflow mints an ESPHome
# GitHub App token so the labels, comments and closures come from
# esphome[bot] instead of github-actions[bot].
uses: esphome/workflows/.github/workflows/stale.yml@cc3e76de337dc59bc1cba8da58d963cd23b873f1 # main
uses: esphome/workflows/.github/workflows/stale.yml@4fbce13026c393d6eb6b247e394dcd9ef1af76c2 # main
secrets:
ESPHOME_GITHUB_APP_PRIVATE_KEY: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
with:
# Live only on dev: a workflow_dispatch from any other branch is a dry run
dry-run: ${{ github.ref != 'refs/heads/dev' }}
days-before-stale: 90
days-before-stale-conflicting: 30
days-before-close: 7
stale-label: stale
exempt-label: not-stale
conflict-label: needs-rebase
ignored-users: esphbot,codecov-commenter
stale-pr-message: >
There hasn't been any activity on this pull request recently. This
@@ -36,6 +38,18 @@ jobs:
to keep it open. Also, please merge the latest dev branch into your
branch to ensure that it's up to date with the latest changes.
Thank you for your contribution!
conflict-pr-message: >
This pull request has merge conflicts. Please merge the latest dev
branch into your branch to resolve them.
stale-conflicting-pr-message: >
The merge conflicts on this pull request have not been resolved for
30 days, so it has been marked as stale and will be closed if they
are not resolved within 7 days.
Please merge the latest dev branch into your branch. Once the pull
request is mergeable again the stale label is removed.
Thank you for your contribution!
stale-issue-message: >
There hasn't been any activity on this issue recently. Due to the
+24
View File
@@ -67,6 +67,7 @@ file does, and it is the authority when they disagree. The most useful starting
- Function-local constants: `lower_snake_case`
- Protected/private fields: `lower_snake_case_with_trailing_underscore_`
- Favor descriptive names over abbreviations
- Device components are named brand plus model (`siemens_ira211`), without a protocol-family prefix
- Enumerator names: prefix every value of an `enum class` with the enum name converted to
`UPPER_SNAKE_CASE` (e.g. `UARTFlushResult::UART_FLUSH_RESULT_SUCCESS`). Never use bare
names like `SUCCESS`, `FAILURE`, `OK`, or `FAIL`: platform SDK headers define macros with
@@ -177,6 +178,8 @@ file does, and it is the authority when they disagree. The most useful starting
Pick the primitive by cadence: under 250 ms use a gated `loop()`; 500 ms and above use
`set_interval`. Full reasoning, including why `set_interval` costs more below 500 ms:
https://developers.esphome.io/architecture/components/advanced/#quick-rule-of-thumb
* **Repeating work:** use `set_interval`, never a `set_timeout` that re-arms itself; every re-arm
rebuilds the callback and re-queues a scheduler item, and the period drifts.
* **Scheduler ids:** name a timer only when it must be cancelled or replaced, and use a
`static constexpr uint32_t` id, never a string. Ids are per component instance and cannot clash with other
components, so number them from 0 and keep all of a component's ids together in one place.
@@ -191,6 +194,18 @@ file does, and it is the authority when they disagree. The most useful starting
// Bad - this is exactly what the base class already does
float get_setup_priority() const override { return setup_priority::DATA; }
```
* **Constant data lives in RAM on ESP8266** unless marked `PROGMEM`: `const` tables, class-scope
constant arrays, and in-class `std::array` initializers (a `.rodata` template plus a copy). Mark the
table `PROGMEM` and read it with `progmem_memcpy()` or `progmem_read_byte()` (`esphome/core/hal.h`);
for a table of structs copy the whole entry out and return the copy, never a pointer into flash.
On other platforms they are a plain `memcpy` and dereference, so no `#ifdef` is needed. Declare log tags with
`ESPHOME_LOG_TAG(TAG, "name")`, pass status text as `LOG_STR`, e.g.
`this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL))`, and give an action whose payload is a byte array
a `TEMPLATABLE_BYTES` field so a static code stays in flash.
* **Failures after `setup()`:** `mark_failed()` is for `setup()` only. A runtime read or write failure
sets `status_set_warning()` and the next success clears it; writes skip work when `is_failed()`.
* **Fixed platform facts compile to nothing:** gate on `#ifdef USE_ESP8266` and friends, not on a
runtime check of something the build already knows.
* **Logging string literals:** wrap literals passed as `%s` arguments in `LOG_STR_LITERAL()` so they
can be stored in flash rather than RAM.
```cpp
@@ -534,6 +549,7 @@ file does, and it is the authority when they disagree. The most useful starting
`cover.is_open`, `rtttl.is_playing` and `component.is_idle` are in-tree examples.
* **Type Hints:** Type-hint all function signatures, including test functions and config validators (e.g. `def validate_x(config: ConfigType) -> ConfigType:`, `def test_x() -> None:`). Import `ConfigType` from `esphome.types`.
An intentionally empty body (an unused `register_dumper` builder, for example) gets a one-line docstring, not `pass`.
* **Configuration Validation:**
* **Reuse existing validators:** Before writing a custom validator, check for an existing one in `config_validation.py` and compose it in `cv.All(...)` rather than duplicating logic across components. For example, rename a config key with `cv.rename_key(CONF_OLD, CONF_NEW, removed_in="2026.6.0")`, and reject mutually-exclusive keys with `cv.has_at_most_one_key(...)` / `cv.has_exactly_one_key(...)`. See how `api` composes `cv.has_exactly_one_key` + `cv.rename_key`.
@@ -569,6 +585,13 @@ file does, and it is the authority when they disagree. The most useful starting
* **Testing:**
* **Python:** Run unit tests with `pytest`.
* **C++:** Use `clang-tidy` for static analysis.
* **C++ unit tests:** gtests in `tests/components/<component>/*.cpp`, run with
`script/cpp_unit_test.py <component>`; see `tests/components/README.md`. Set the loop clock in a test
with `LoopBlockingGuard{nullptr, nullptr, now_ms}`; the runner resets it before every test.
* **Integration tests:** `tests/integration/` runs real firmware on the host. Serial components prove
timeouts and recovery with the `uart_mock` fixture (`tests/integration/test_uart_mock_*.py`).
* **Keep build-test variants few:** one `test.<platform>.yaml` per platform the code actually differs
on. A variant per board for platform-identical code only adds CI time.
* **Component Tests:** YAML-based compilation tests are located in `tests/`. The structure is as follows:
```
tests/
@@ -615,6 +638,7 @@ file does, and it is the authority when they disagree. The most useful starting
name: My Sensor
```
Components that define buses directly are flagged as "NEEDS MIGRATION" and cannot be grouped, increasing CI build time.
`!extend` and `!remove` in a `test.*.yaml` also make the component ungroupable; add a standalone entity with its own id instead.
* **Testing All Components Together:** To verify that all components can be tested together without ID conflicts or configuration issues, use:
```bash
+1 -1
View File
@@ -570,7 +570,7 @@ esphome/components/tca9555/* @mobrembski
esphome/components/tcl112/* @glmnet
esphome/components/tcp_uart/* @Bascht74
esphome/components/tee501/* @Stock-M
esphome/components/teleinfo/* @0hax
esphome/components/teleinfo/* @0hax @FredM67
esphome/components/tem3200/* @bakerkj
esphome/components/template/alarm_control_panel/* @grahambrown11 @hwstar
esphome/components/template/datetime/* @rfdarter
+1 -1
View File
@@ -22,7 +22,7 @@ RUN \
-r /requirements.txt
# Install the ESPHome Device Builder dashboard.
RUN uv pip install --no-cache-dir esphome-device-builder==1.23.0
RUN uv pip install --no-cache-dir esphome-device-builder==1.23.1
RUN \
platformio settings set enable_telemetry No \
+34 -2
View File
@@ -51,6 +51,7 @@ from esphome.const import (
KEY_ESP32,
KEY_VARIANT,
SECRETS_FILES,
ErrorFormat,
Toolchain,
)
from esphome.core import CORE, EsphomeError, coroutine
@@ -2205,6 +2206,16 @@ def _add_states_args(parser: argparse.ArgumentParser) -> None:
)
def _error_format(value: str) -> ErrorFormat:
try:
return ErrorFormat(value.lower())
except ValueError:
choices = ", ".join(ErrorFormat)
raise argparse.ArgumentTypeError(
f"invalid choice: '{value}' (choose from {choices})"
) from None
def parse_args(argv):
options_parser = argparse.ArgumentParser(add_help=False)
options_parser.add_argument(
@@ -2267,6 +2278,17 @@ def parse_args(argv):
),
)
options_parser.add_argument(
"--error-format",
type=_error_format,
metavar="{" + ",".join(f.value for f in ErrorFormat) + "}",
help=(
"How configuration errors are shown. 'yaml' shows them inside the "
"failing YAML; 'line' prints one 'file:line:column: error: message' "
"line per error. Default: $ESPHOME_ERROR_FORMAT, else 'yaml'."
),
)
parser = argparse.ArgumentParser(
description=f"ESPHome {const.__version__}", parents=[options_parser]
)
@@ -2570,9 +2592,18 @@ def parse_args(argv):
args, unknown_args = parser.parse_known_args(arguments)
if unknown_args:
_LOGGER.warning("Ignored unrecognized arguments: %s", unknown_args)
return args
else:
args = parser.parse_args(arguments)
return parser.parse_args(arguments)
# Read here rather than as the option default, so a bad value is reported
# against the environment variable instead of --error-format
if args.error_format is None:
env_value = os.getenv("ESPHOME_ERROR_FORMAT", ErrorFormat.YAML.value)
try:
args.error_format = _error_format(env_value)
except argparse.ArgumentTypeError as err:
parser.error(f"ESPHOME_ERROR_FORMAT: {err}")
return args
def _warn_if_source_tree_mismatch() -> None:
@@ -2645,6 +2676,7 @@ def run_esphome(argv):
args = parse_args(argv)
CORE.dashboard = args.dashboard
CORE.testing_mode = args.testing_mode
CORE.error_format = args.error_format
# Create address cache from command-line arguments
CORE.address_cache = AddressCache.from_cli_args(
+1 -1
View File
@@ -1057,7 +1057,7 @@ def write_project(paths: InstalledPaths, ccache: str | None) -> bool:
cxx = (toolchain_tool(paths.toolchain, "g++"),)
lines = [
*tool_lines((toolchain_tool(paths.toolchain, "gcc"),), cxx, ccache),
*compile_rule_lines(),
*compile_rule_lines(ccache),
*ar_rule_lines(toolchain_tool(paths.toolchain, "ar")),
*pch_rule_lines(),
"rule link",
+1 -1
View File
@@ -240,7 +240,7 @@ def write_project(compilers: HostCompilers, ccache: str | None) -> bool:
lines = [
*tool_lines(compilers.cc, compilers.cxx, ccache),
*compile_rule_lines(),
*compile_rule_lines(ccache),
*pch_rule_lines(),
"rule link",
" command = $cxx -o $out $linkflags @$out.rsp $archives $libdirflags $libflags",
+14 -7
View File
@@ -74,33 +74,40 @@ def tool_lines(
# path gets
f"python = {_q(strip_win_long_path_prefix(sys.executable))}",
f"buildtool = {_q(_BUILD_TOOL)}",
f"ccache = {_q(ccache) if ccache else ''}",
*([f"ccache = {_q(ccache)}"] if ccache else []),
"",
]
def compile_rule_lines() -> list[str]:
"""The compile rules; ``$own_includes`` is empty unless an edge sets it."""
def compile_rule_lines(ccache: str | None) -> list[str]:
"""The compile rules; ``$own_includes`` is empty unless an edge sets it.
``ccache`` is the launcher ``tool_lines`` was given; the same test here
keeps the variable and the rules in step. It is spliced in rather than
left as an empty variable: a leading space on the command line makes
CreateProcess fail on Windows.
"""
launcher = "$ccache " if ccache else ""
return [
"rule c",
" command = $ccache $cc -MMD -MF $out.d $own_includes $cflags $flags -c $in -o $out",
f" command = {launcher}$cc -MMD -MF $out.d $own_includes $cflags $flags -c $in -o $out",
" depfile = $out.d",
" deps = gcc",
" description = CC $out",
"rule cxx",
" command = $ccache $cxx -MMD -MF $out.d $own_includes $cxxflags $flags -c $in -o $out",
f" command = {launcher}$cxx -MMD -MF $out.d $own_includes $cxxflags $flags -c $in -o $out",
" depfile = $out.d",
" deps = gcc",
" description = CXX $out",
"rule aspp",
" command = $ccache $cc -MMD -MF $out.d -x assembler-with-cpp $own_includes $asflags $flags -c $in -o $out",
f" command = {launcher}$cc -MMD -MF $out.d -x assembler-with-cpp $own_includes $asflags $flags -c $in -o $out",
" depfile = $out.d",
" deps = gcc",
" description = AS $out",
# Plain assembler, as SCons's ASCOM: no preprocessor, so no
# depfile and no $flags (defines/includes) either
"rule asm",
" command = $ccache $cc -x assembler $asflags -c $in -o $out",
f" command = {launcher}$cc -x assembler $asflags -c $in -o $out",
" description = AS $out",
]
+1 -1
View File
@@ -1,4 +1,5 @@
import esphome.codegen as cg
from esphome.components.const import CONF_AQI # noqa: F401
CODEOWNERS = ["@jasstrong", "@ximex", "@freekode"]
DOMAIN = "aqi"
@@ -6,7 +7,6 @@ DOMAIN = "aqi"
aqi_ns = cg.esphome_ns.namespace("aqi")
AQICalculatorType = aqi_ns.enum("AQICalculatorType")
CONF_AQI = "aqi"
CONF_CALCULATION_TYPE = "calculation_type"
CONF_EXTENDED_RANGE = "extended_range"
@@ -3,6 +3,7 @@ from pathlib import Path
import esphome.codegen as cg
from esphome.components import audio, media_source, psram
from esphome.components.const import CONF_VERIFY_SSL
import esphome.config_validation as cv
from esphome.const import CONF_BUFFER_SIZE, CONF_ID, CONF_TASK_STACK_IN_PSRAM
import esphome.final_validate as fv
@@ -16,7 +17,6 @@ AUTO_LOAD = ["audio"]
CONF_PERSISTENT_RING_BUFFER = "persistent_ring_buffer"
CONF_CA_CERTIFICATE_PATH = "ca_certificate_path"
CONF_HTTP_REQUEST = "http_request"
CONF_VERIFY_SSL = "verify_ssl"
audio_http_ns = cg.esphome_ns.namespace("audio_http")
AudioHTTPMediaSource = audio_http_ns.class_(
+16 -4
View File
@@ -20,6 +20,9 @@ CONF_REMOTE_TRANSMISSION_REQUEST = "remote_transmission_request"
CONF_CANBUS_ID = "canbus_id"
CONF_BIT_RATE = "bit_rate"
CONF_ON_FRAME = "on_frame"
CONF_MAX_FRAMES_PER_LOOP = "max_frames_per_loop"
DEFAULT_MAX_FRAMES_PER_LOOP = 50 # Keep in sync with max_frames_per_loop_ in canbus.h
DEFAULT_BIT_RATE = "125KBPS" # Keep in sync with bit_rate_ in canbus.h
def validate_id(config: ConfigType) -> ConfigType:
@@ -87,8 +90,13 @@ CANBUS_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(CanbusComponent),
cv.Required(CONF_CAN_ID): cv.int_range(min=0, max=0x1FFFFFFF),
cv.Optional(CONF_BIT_RATE, default="125KBPS"): cv.enum(CAN_SPEEDS, upper=True),
cv.Optional(CONF_BIT_RATE, default=DEFAULT_BIT_RATE): cv.enum(
CAN_SPEEDS, upper=True
),
cv.Optional(CONF_USE_EXTENDED_ID, default=False): cv.boolean,
cv.Optional(
CONF_MAX_FRAMES_PER_LOOP, default=DEFAULT_MAX_FRAMES_PER_LOOP
): cv.positive_not_null_int,
cv.Optional(CONF_ON_FRAME): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CanbusTrigger),
@@ -109,9 +117,13 @@ CANBUS_SCHEMA.add_extra(validate_id)
async def setup_canbus_core_(var: MockObj, config: ConfigType) -> None:
await cg.register_component(var, config)
cg.add(var.set_can_id([config[CONF_CAN_ID]]))
cg.add(var.set_use_extended_id([config[CONF_USE_EXTENDED_ID]]))
cg.add(var.set_bitrate(CAN_SPEEDS[config[CONF_BIT_RATE]]))
cg.add(var.set_can_id(config[CONF_CAN_ID]))
if config[CONF_USE_EXTENDED_ID]:
cg.add(var.set_use_extended_id(True))
if (bit_rate := config[CONF_BIT_RATE]) != DEFAULT_BIT_RATE:
cg.add(var.set_bitrate(CAN_SPEEDS[bit_rate]))
if (max_frames := config[CONF_MAX_FRAMES_PER_LOOP]) != DEFAULT_MAX_FRAMES_PER_LOOP:
cg.add(var.set_max_frames_per_loop(max_frames))
for conf in config.get(CONF_ON_FRAME, []):
can_id = conf[CONF_CAN_ID]
+15 -7
View File
@@ -66,16 +66,18 @@ void Canbus::add_trigger(CanbusTrigger *trigger) {
void Canbus::loop() {
struct CanFrame can_message;
// read all messages until queue is empty
int message_counter = 0;
while (this->read_message(&can_message) == canbus::ERROR_OK) {
message_counter++;
uint32_t message_counter = 0;
for (; message_counter < this->max_frames_per_loop_; message_counter++) {
if (this->read_message(&can_message) != canbus::ERROR_OK) {
break;
}
if (can_message.use_extended_id) {
ESP_LOGD(TAG, "received can message (#%d) extended can_id=0x%" PRIx32 " size=%d", message_counter,
ESP_LOGD(TAG, "received can message (#%" PRIu32 ") extended can_id=0x%" PRIx32 " size=%d", message_counter + 1,
can_message.can_id, can_message.can_data_length_code);
} else {
ESP_LOGD(TAG, "received can message (#%d) std can_id=0x%" PRIx32 " size=%d", message_counter, can_message.can_id,
can_message.can_data_length_code);
ESP_LOGD(TAG, "received can message (#%" PRIu32 ") std can_id=0x%" PRIx32 " size=%d", message_counter + 1,
can_message.can_id, can_message.can_data_length_code);
}
std::vector<uint8_t> data;
@@ -99,6 +101,12 @@ void Canbus::loop() {
}
}
}
if (message_counter == this->max_frames_per_loop_) {
ESP_LOGW(TAG,
"Reached max_frames_per_loop=%" PRIu32 ", deferring remaining CAN frames. Some frames might be dropped",
this->max_frames_per_loop_);
}
}
} // namespace esphome::canbus
+4 -2
View File
@@ -82,6 +82,7 @@ class Canbus : public Component {
void set_can_id(uint32_t can_id) { this->can_id_ = can_id; }
void set_use_extended_id(bool use_extended_id) { this->use_extended_id_ = use_extended_id; }
void set_bitrate(CanSpeed bit_rate) { this->bit_rate_ = bit_rate; }
void set_max_frames_per_loop(uint32_t max_frames_per_loop) { this->max_frames_per_loop_ = max_frames_per_loop; }
void add_trigger(CanbusTrigger *trigger);
/**
@@ -100,8 +101,9 @@ class Canbus : public Component {
template<typename... Ts> friend class CanbusSendAction;
std::vector<CanbusTrigger *> triggers_{};
uint32_t can_id_;
bool use_extended_id_;
CanSpeed bit_rate_;
uint32_t max_frames_per_loop_{50}; // Keep in sync with DEFAULT_MAX_FRAMES_PER_LOOP in __init__.py
CanSpeed bit_rate_{CAN_125KBPS}; // Keep in sync with DEFAULT_BIT_RATE in __init__.py
bool use_extended_id_{false};
CallbackManager<void(uint32_t can_id, bool extended_id, bool rtr, const std::vector<uint8_t> &data)>
callback_manager_{};
+60 -21
View File
@@ -6,10 +6,16 @@
namespace esphome::cm1106 {
ESPHOME_LOG_TAG(TAG, "cm1106");
static const uint8_t C_M1106_CMD_GET_CO2[4] = {0x11, 0x01, 0x01, 0xED};
static const uint8_t C_M1106_CMD_SET_CO2_CALIB[6] = {0x11, 0x03, 0x03, 0x00, 0x00, 0x00};
static const uint8_t C_M1106_CMD_SET_CO2_CALIB_RESPONSE[4] = {0x16, 0x01, 0x03, 0xE6};
static constexpr uint8_t C_M1106_CMD_GET_CO2[4] = {0x11, 0x01, 0x01, 0xED};
static constexpr uint8_t C_M1106_CMD_SET_CO2_CALIB[6] = {0x11, 0x03, 0x03, 0x00, 0x00, 0x00};
static constexpr uint8_t C_M1106_CMD_SET_CO2_CALIB_RESPONSE[4] = {0x16, 0x01, 0x03, 0xE6};
// The factory default ABC calibration cycle differs between CM1106 models, so
// every ABC write applies the configured cycle and baseline explicitly.
static constexpr uint8_t C_M1106_CMD_SET_ABC_STATUS[10] = {0x11, 0x07, 0x10, 0x64, 0x00, 0x0F, 0x01, 0x90, 0x64, 0x00};
static constexpr uint8_t C_M1106_CMD_SET_ABC_STATUS_RESPONSE[4] = {0x16, 0x01, 0x10, 0xD9};
static constexpr uint8_t CM1106_ABC_FLAG_ENABLE = 0x0;
static constexpr uint8_t CM1106_ABC_FLAG_DISABLE = 0x2;
uint8_t cm1106_checksum(const uint8_t *response, size_t len) {
uint8_t crc = 0;
for (size_t i = 0; i < len - 1; i++) {
@@ -25,34 +31,46 @@ void CM1106Component::setup() {
this->mark_failed();
return;
}
if (this->abc_boot_logic_ != CM1106_ABC_NONE) {
this->abc_set_(this->abc_boot_logic_ == CM1106_ABC_ENABLED);
}
}
void CM1106Component::update() {
uint8_t response[8] = {0};
if (!this->cm1106_write_command_(C_M1106_CMD_GET_CO2, sizeof(C_M1106_CMD_GET_CO2), response, sizeof(response))) {
ESP_LOGW(TAG, "Reading data from CM1106 failed!");
ESP_LOGW(TAG, "Reading data failed!");
this->status_set_warning();
return;
}
if (response[0] != 0x16 || response[1] != 0x05 || response[2] != 0x01) {
ESP_LOGW(TAG, "Got wrong UART response from CM1106: %02X %02X %02X %02X", response[0], response[1], response[2],
response[3]);
ESP_LOGW(TAG, "Unexpected response: %02X %02X %02X %02X", response[0], response[1], response[2], response[3]);
this->status_set_warning();
return;
}
uint8_t checksum = cm1106_checksum(response, sizeof(response));
if (response[7] != checksum) {
ESP_LOGW(TAG, "CM1106 Checksum doesn't match: 0x%02X!=0x%02X", response[7], checksum);
ESP_LOGW(TAG, "Checksum doesn't match: 0x%02X!=0x%02X", response[7], checksum);
this->status_set_warning();
return;
}
const uint16_t ppm = response[3] << 8 | response[4];
const uint8_t status = response[5];
if (status & 0x1) {
ESP_LOGW(TAG, "Preheating; CO₂=%uppm", ppm);
this->status_set_warning();
return;
}
if (status) {
ESP_LOGW(TAG, "Status: %02X", status);
}
this->status_clear_warning();
uint16_t ppm = response[3] << 8 | response[4];
ESP_LOGD(TAG, "CM1106 Received CO₂=%uppm DF3=%02X DF4=%02X", ppm, response[5], response[6]);
ESP_LOGD(TAG, "CO₂=%uppm DF4=%02X", ppm, response[6]);
if (this->co2_sensor_ != nullptr)
this->co2_sensor_->publish_state(ppm);
}
@@ -62,24 +80,38 @@ void CM1106Component::calibrate_zero(uint16_t ppm) {
memcpy(cmd, C_M1106_CMD_SET_CO2_CALIB, sizeof(cmd));
cmd[3] = ppm >> 8;
cmd[4] = ppm & 0xFF;
if (this->send_command_expect_ack_(cmd, sizeof(cmd), C_M1106_CMD_SET_CO2_CALIB_RESPONSE)) {
ESP_LOGD(TAG, "Successfully calibrated sensor to %uppm", ppm);
}
}
void CM1106Component::abc_set_(bool enabled) {
ESP_LOGD(TAG, "%sabling automatic baseline calibration", enabled ? LOG_STR_LITERAL("En") : LOG_STR_LITERAL("Dis"));
uint8_t cmd[10];
memcpy(cmd, C_M1106_CMD_SET_ABC_STATUS, sizeof(cmd));
cmd[4] = enabled ? CM1106_ABC_FLAG_ENABLE : CM1106_ABC_FLAG_DISABLE;
cmd[5] = this->abc_cycle_;
cmd[6] = this->abc_baseline_ >> 8;
cmd[7] = this->abc_baseline_ & 0xFF;
if (this->send_command_expect_ack_(cmd, sizeof(cmd), C_M1106_CMD_SET_ABC_STATUS_RESPONSE)) {
ESP_LOGD(TAG, "Successfully set ABC status");
}
}
bool CM1106Component::send_command_expect_ack_(const uint8_t *command, size_t command_len, const uint8_t *ack) {
uint8_t response[4] = {0};
if (!this->cm1106_write_command_(cmd, sizeof(cmd), response, sizeof(response))) {
ESP_LOGW(TAG, "Reading data from CM1106 failed!");
if (!this->cm1106_write_command_(command, command_len, response, sizeof(response))) {
ESP_LOGW(TAG, "Command %02X failed", command[2]);
this->status_set_warning();
return;
return false;
}
// check if correct response received
if (memcmp(response, C_M1106_CMD_SET_CO2_CALIB_RESPONSE, sizeof(response)) != 0) {
ESP_LOGW(TAG, "Got wrong UART response from CM1106: %02X %02X %02X %02X", response[0], response[1], response[2],
response[3]);
if (memcmp(response, ack, sizeof(response)) != 0) {
ESP_LOGW(TAG, "Unexpected response: %02X %02X %02X %02X", response[0], response[1], response[2], response[3]);
this->status_set_warning();
return;
return false;
}
this->status_clear_warning();
ESP_LOGD(TAG, "CM1106 Successfully calibrated sensor to %uppm", ppm);
return true;
}
bool CM1106Component::cm1106_write_command_(const uint8_t *command, size_t command_len, uint8_t *response,
@@ -100,6 +132,13 @@ bool CM1106Component::cm1106_write_command_(const uint8_t *command, size_t comma
void CM1106Component::dump_config() {
ESP_LOGCONFIG(TAG, "CM1106:");
LOG_SENSOR(" ", "CO2", this->co2_sensor_);
if (this->abc_boot_logic_ != CM1106_ABC_NONE) {
ESP_LOGCONFIG(TAG,
" Automatic baseline calibration on boot: %s\n"
" ABC calibration cycle: %u days\n"
" ABC baseline: %uppm",
ONOFF(this->abc_boot_logic_ == CM1106_ABC_ENABLED), this->abc_cycle_, this->abc_baseline_);
}
if (this->is_failed()) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
}
+23 -2
View File
@@ -6,6 +6,14 @@
namespace esphome::cm1106 {
// ABC logic requested at boot; CM1106_ABC_NONE means nothing is sent and the
// sensor keeps its stored setting.
enum CM1106ABCLogic : uint8_t {
CM1106_ABC_NONE = 0,
CM1106_ABC_ENABLED,
CM1106_ABC_DISABLED,
};
class CM1106Component final : public PollingComponent, public uart::UARTDevice {
public:
void setup() override;
@@ -13,13 +21,26 @@ class CM1106Component final : public PollingComponent, public uart::UARTDevice {
void dump_config() override;
void calibrate_zero(uint16_t ppm);
void abc_enable() { this->abc_set_(true); }
void abc_disable() { this->abc_set_(false); }
void set_co2_sensor(sensor::Sensor *co2_sensor) { this->co2_sensor_ = co2_sensor; }
void set_abc(bool enabled, uint8_t cycle_days, uint16_t baseline_ppm) {
this->abc_boot_logic_ = enabled ? CM1106_ABC_ENABLED : CM1106_ABC_DISABLED;
this->abc_cycle_ = cycle_days;
this->abc_baseline_ = baseline_ppm;
}
protected:
sensor::Sensor *co2_sensor_{nullptr};
bool cm1106_write_command_(const uint8_t *command, size_t command_len, uint8_t *response, size_t response_len);
/// Sends a command and checks the sensor's four byte acknowledgement, updating the warning status.
bool send_command_expect_ack_(const uint8_t *command, size_t command_len, const uint8_t *ack);
void abc_set_(bool enabled);
sensor::Sensor *co2_sensor_{nullptr};
CM1106ABCLogic abc_boot_logic_{CM1106_ABC_NONE};
uint8_t abc_cycle_{15};
uint16_t abc_baseline_{400};
};
} // namespace esphome::cm1106
+29
View File
@@ -4,9 +4,12 @@ from esphome import automation
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
from esphome.components import sensor, uart
from esphome.components.const import CONF_AUTOMATIC_BASELINE_CALIBRATION
import esphome.config_validation as cv
from esphome.const import (
CONF_BASELINE,
CONF_CO2,
CONF_CYCLE,
CONF_ID,
DEVICE_CLASS_CARBON_DIOXIDE,
ICON_MOLECULE_CO2,
@@ -18,11 +21,22 @@ from esphome.types import ConfigType
DEPENDENCIES = ["uart"]
CODEOWNERS = ["@andrewjswan"]
# true enables ABC with these defaults, false disables it, a mapping enables it with custom values
_ABC_SETTINGS_SCHEMA = cv.Schema(
{
cv.Optional(CONF_CYCLE, default=15): cv.int_range(min=1, max=90),
cv.Optional(CONF_BASELINE, default=400): cv.int_range(min=400, max=10000),
}
)
cm1106_ns = cg.esphome_ns.namespace("cm1106")
CM1106Component = cm1106_ns.class_(
"CM1106Component", cg.PollingComponent, uart.UARTDevice
)
CONFIG_SCHEMA = (
cv.Schema(
{
@@ -34,6 +48,9 @@ CONFIG_SCHEMA = (
device_class=DEVICE_CLASS_CARBON_DIOXIDE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_AUTOMATIC_BASELINE_CALIBRATION): cv.Any(
cv.boolean, _ABC_SETTINGS_SCHEMA
),
},
)
.extend(cv.polling_component_schema("60s"))
@@ -57,6 +74,11 @@ async def to_code(config: ConfigType) -> None:
sensors = sensor.sub_sensors(config)
await sensors(CONF_CO2, var.set_co2_sensor)
if (abc := config.get(CONF_AUTOMATIC_BASELINE_CALIBRATION)) is not None:
enabled = abc if isinstance(abc, bool) else True
settings = _ABC_SETTINGS_SCHEMA({}) if isinstance(abc, bool) else abc
cg.add(var.set_abc(enabled, settings[CONF_CYCLE], settings[CONF_BASELINE]))
CALIBRATION_ACTION_SCHEMA = maybe_simple_id(
{
@@ -70,3 +92,10 @@ automation.register_apply_action(
CALIBRATION_ACTION_SCHEMA,
automation.ApplyCall("calibrate_zero(400)"),
)
for _name, _call in (
("cm1106.abc_enable", "abc_enable()"),
("cm1106.abc_disable", "abc_disable()"),
):
automation.register_apply_action(
_name, CALIBRATION_ACTION_SCHEMA, automation.ApplyCall(_call)
)
+6
View File
@@ -8,7 +8,11 @@ BYTE_ORDER_BIG = "big_endian"
CONF_ACCELEROMETER_ODR = "accelerometer_odr"
CONF_ACCELEROMETER_RANGE = "accelerometer_range"
CONF_ADC_AVERAGING = "adc_averaging"
CONF_ADC_TIME = "adc_time"
CONF_ALLOWED_IPS = "allowed_ips"
CONF_AQI = "aqi"
CONF_AUTOMATIC_BASELINE_CALIBRATION = "automatic_baseline_calibration"
CONF_B_CONSTANT = "b_constant"
CONF_BREATH_VOC_EQUIVALENT = "breath_voc_equivalent"
CONF_BYTE_ORDER = "byte_order"
@@ -56,6 +60,7 @@ CONF_STATE_SAVE_INTERVAL = "state_save_interval"
CONF_STOP_BITS = "stop_bits"
CONF_TARGET_COUNT = "target_count"
CONF_USE_PSRAM = "use_psram"
CONF_VERIFY_SSL = "verify_ssl"
CONF_VOC_INDEX = "voc_index"
CONF_VOLUME_INCREMENT = "volume_increment"
CONF_VOLUME_INITIAL = "volume_initial"
@@ -71,3 +76,4 @@ KEY_METADATA = "metadata"
UNIT_AMPERE_HOUR = "Ah"
UNIT_COUNTS = "#"
UNIT_MILLIAMPERE_HOUR = "mAh"
+114 -18
View File
@@ -1,25 +1,47 @@
#include "daikin.h"
#include "esphome/components/remote_base/remote_base.h"
#include "esphome/core/application.h"
namespace esphome::daikin {
ESPHOME_LOG_TAG(TAG, "daikin.climate");
static constexpr uint32_t BOOST_TIMEOUT_ID = 0;
static uint8_t sum_bytes(const uint8_t *data, size_t len) {
uint8_t sum = 0;
for (size_t i = 0; i < len; i++) {
sum += data[i];
}
return sum;
}
void DaikinClimate::setup() {
climate_ir::ClimateIR::setup();
// A boost preset restored from flash gets its timer too
this->update_boost_timer_(false);
}
void DaikinClimate::control(const climate::ClimateCall &call) {
const bool was_boost = this->preset == climate::CLIMATE_PRESET_BOOST;
climate_ir::ClimateIR::control(call);
this->update_boost_timer_(was_boost);
}
void DaikinClimate::transmit_state() {
uint8_t remote_state[35] = {0x11, 0xDA, 0x27, 0x00, 0xC5, 0x00, 0x00, 0xD7, 0x11, 0xDA, 0x27, 0x00,
uint8_t remote_state[35] = {0x11, 0xDA, 0x27, 0x00, 0xC5, 0x00, 0x00, 0x00, 0x11, 0xDA, 0x27, 0x00,
0x42, 0x49, 0x05, 0xA2, 0x11, 0xDA, 0x27, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00};
this->set_presets_(remote_state);
remote_state[21] = this->operation_mode_();
remote_state[22] = this->temperature_();
uint16_t fan_speed = this->fan_speed_();
remote_state[24] = fan_speed >> 8;
remote_state[25] = fan_speed & 0xff;
// Calculate checksum
for (int i = 16; i < 34; i++) {
remote_state[34] += remote_state[i];
}
remote_state[7] = sum_bytes(remote_state, 7);
remote_state[34] = sum_bytes(remote_state + 16, 18);
auto transmit = this->transmitter_->transmit();
auto *data = transmit.get_data();
@@ -136,18 +158,52 @@ uint8_t DaikinClimate::temperature_() const {
case climate::CLIMATE_MODE_DRY:
return 0xc0;
default:
uint8_t temperature = (uint8_t) roundf(clamp<float>(this->target_temperature, DAIKIN_TEMP_MIN, DAIKIN_TEMP_MAX));
return temperature << 1;
return static_cast<uint8_t>(
roundf(clamp<float>(this->target_temperature, DAIKIN_TEMP_MIN, DAIKIN_TEMP_MAX) * 2.0f));
}
}
bool DaikinClimate::parse_state_frame_(const uint8_t frame[]) {
uint8_t checksum = 0;
for (int i = 0; i < (DAIKIN_STATE_FRAME_SIZE - 1); i++) {
checksum += frame[i];
void DaikinClimate::set_presets_(uint8_t *remote_state) const {
switch (this->preset.value_or(climate::CLIMATE_PRESET_NONE)) {
case climate::CLIMATE_PRESET_BOOST:
remote_state[29] = DAIKIN_PRESET_POWERFUL_ON;
break;
case climate::CLIMATE_PRESET_SLEEP:
remote_state[29] = DAIKIN_PRESET_QUIET_ON;
break;
case climate::CLIMATE_PRESET_ECO:
remote_state[32] = DAIKIN_PRESET_ECONO_ON;
break;
case climate::CLIMATE_PRESET_ACTIVITY:
remote_state[32] = DAIKIN_PRESET_SENSOR_ON;
break;
case climate::CLIMATE_PRESET_COMFORT:
remote_state[6] = DAIKIN_PRESET_COMFORT_ON; // the only preset carried by the first frame
break;
default:
break;
}
if (frame[DAIKIN_STATE_FRAME_SIZE - 1] != checksum)
return false;
}
void DaikinClimate::update_boost_timer_(bool was_boost) {
const bool is_boost = this->preset == climate::CLIMATE_PRESET_BOOST;
if (is_boost && !was_boost) {
this->set_timeout(BOOST_TIMEOUT_ID, DAIKIN_BOOST_DURATION_MS, [this]() { this->cancel_boost_mode_(); });
} else if (was_boost && !is_boost) {
this->cancel_timeout(BOOST_TIMEOUT_ID);
}
}
void DaikinClimate::cancel_boost_mode_() {
if (this->preset != climate::CLIMATE_PRESET_BOOST)
return;
ESP_LOGD(TAG, "Boost ended");
this->preset = climate::CLIMATE_PRESET_NONE;
this->publish_state();
}
bool DaikinClimate::parse_state_frame_(const uint8_t frame[]) {
const bool was_boost = this->preset == climate::CLIMATE_PRESET_BOOST;
uint8_t mode = frame[5];
// Temperature is given in degrees celcius * 2
// only update for states that use the temperature
@@ -206,16 +262,42 @@ bool DaikinClimate::parse_state_frame_(const uint8_t frame[]) {
this->fan_mode = climate::CLIMATE_FAN_QUIET;
break;
}
// Comfort lives in the first frame, recorded as pending until this frame proves the transmission valid
// A comfort frame whose own state frame never came must not attach to a later transmission
const uint32_t now = App.get_loop_component_start_time();
const PendingComfort comfort = now - this->pending_comfort_at_ <= DAIKIN_TRANSMISSION_MS
? this->pending_comfort_
: PendingComfort::PENDING_COMFORT_UNKNOWN;
this->pending_comfort_ = PendingComfort::PENDING_COMFORT_UNKNOWN;
if (frame[13] & DAIKIN_PRESET_POWERFUL_ON) {
this->preset = climate::CLIMATE_PRESET_BOOST;
} else if (frame[13] & DAIKIN_PRESET_QUIET_ON) {
this->preset = climate::CLIMATE_PRESET_SLEEP;
} else if (frame[16] & DAIKIN_PRESET_ECONO_ON) {
this->preset = climate::CLIMATE_PRESET_ECO;
} else if (frame[16] & DAIKIN_PRESET_SENSOR_ON) {
this->preset = climate::CLIMATE_PRESET_ACTIVITY;
} else if (comfort == PendingComfort::PENDING_COMFORT_ON) {
this->preset = climate::CLIMATE_PRESET_COMFORT;
} else if (comfort == PendingComfort::PENDING_COMFORT_OFF || this->preset != climate::CLIMATE_PRESET_COMFORT) {
// Without a comfort frame in this transmission a current comfort preset is left alone
this->preset = climate::CLIMATE_PRESET_NONE;
}
this->update_boost_timer_(was_boost);
this->publish_state();
return true;
}
bool DaikinClimate::on_receive(remote_base::RemoteReceiveData data) {
uint8_t state_frame[DAIKIN_STATE_FRAME_SIZE] = {};
uint8_t frame[DAIKIN_STATE_FRAME_SIZE] = {};
if (!data.expect_item(DAIKIN_HEADER_MARK, DAIKIN_HEADER_SPACE)) {
return false;
}
for (uint8_t pos = 0; pos < DAIKIN_STATE_FRAME_SIZE; pos++) {
uint8_t frame_size = DAIKIN_STATE_FRAME_SIZE;
for (uint8_t pos = 0; pos < frame_size; pos++) {
uint8_t byte = 0;
for (int8_t bit = 0; bit < 8; bit++) {
if (data.expect_item(DAIKIN_BIT_MARK, DAIKIN_ONE_SPACE)) {
@@ -224,7 +306,7 @@ bool DaikinClimate::on_receive(remote_base::RemoteReceiveData data) {
return false;
}
}
state_frame[pos] = byte;
frame[pos] = byte;
if (pos == 0) {
// frame header
if (byte != 0x11)
@@ -243,11 +325,25 @@ bool DaikinClimate::on_receive(remote_base::RemoteReceiveData data) {
return false;
} else if (pos == 4) {
// frame type
if (byte != 0x00)
if (byte == DAIKIN_FRAME_TYPE_COMFORT) {
frame_size = DAIKIN_COMFORT_FRAME_SIZE;
} else if (byte != DAIKIN_FRAME_TYPE_STATE) {
return false;
}
}
}
return this->parse_state_frame_(state_frame);
if (frame[frame_size - 1] != sum_bytes(frame, frame_size - 1)) {
return false;
}
if (frame_size == DAIKIN_COMFORT_FRAME_SIZE) {
// Nothing changes yet: the state frame that follows applies it once it has passed its own checksum
this->pending_comfort_ = (frame[6] & DAIKIN_PRESET_COMFORT_ON) ? PendingComfort::PENDING_COMFORT_ON
: PendingComfort::PENDING_COMFORT_OFF;
this->pending_comfort_at_ = App.get_loop_component_start_time();
return true;
}
return this->parse_state_frame_(frame);
}
} // namespace esphome::daikin
+34 -6
View File
@@ -27,6 +27,17 @@ const uint8_t DAIKIN_FAN_3 = 0x50;
const uint8_t DAIKIN_FAN_4 = 0x60;
const uint8_t DAIKIN_FAN_5 = 0x70;
// Presets: bit flags in byte 6 (comfort), byte 29 (powerful, quiet) and byte 32 (sensor, econo)
const uint8_t DAIKIN_PRESET_COMFORT_ON = 0x10;
const uint8_t DAIKIN_PRESET_POWERFUL_ON = 0x01;
const uint8_t DAIKIN_PRESET_QUIET_ON = 0x20;
const uint8_t DAIKIN_PRESET_ECONO_ON = 0x04;
const uint8_t DAIKIN_PRESET_SENSOR_ON = 0x02;
// The unit leaves powerful mode by itself after 20 minutes; mirror that in the entity
const uint32_t DAIKIN_BOOST_DURATION_MS = 20 * 60 * 1000;
// The three frames of one transmission arrive well inside this window
const uint32_t DAIKIN_TRANSMISSION_MS = 1000;
// IR Transmission
const uint32_t DAIKIN_IR_FREQUENCY = 38000;
const uint32_t DAIKIN_HEADER_MARK = 3360;
@@ -36,27 +47,44 @@ const uint32_t DAIKIN_ONE_SPACE = 1370;
const uint32_t DAIKIN_ZERO_SPACE = 360;
const uint32_t DAIKIN_MESSAGE_SPACE = 32300;
// State Frame size
// Frame types (byte 4) and sizes
const uint8_t DAIKIN_FRAME_TYPE_STATE = 0x00;
const uint8_t DAIKIN_FRAME_TYPE_COMFORT = 0xC5;
const uint8_t DAIKIN_STATE_FRAME_SIZE = 19;
const uint8_t DAIKIN_COMFORT_FRAME_SIZE = 8;
class DaikinClimate final : public climate_ir::ClimateIR {
public:
DaikinClimate()
: climate_ir::ClimateIR(DAIKIN_TEMP_MIN, DAIKIN_TEMP_MAX, 1.0f, true, true,
{climate::CLIMATE_FAN_QUIET, climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW,
climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH},
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL,
climate::CLIMATE_SWING_HORIZONTAL, climate::CLIMATE_SWING_BOTH}) {}
: climate_ir::ClimateIR(
DAIKIN_TEMP_MIN, DAIKIN_TEMP_MAX, 0.5f, true, true,
{climate::CLIMATE_FAN_QUIET, climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW,
climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH},
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL,
climate::CLIMATE_SWING_BOTH},
{climate::CLIMATE_PRESET_ECO, climate::CLIMATE_PRESET_BOOST, climate::CLIMATE_PRESET_COMFORT,
climate::CLIMATE_PRESET_SLEEP, climate::CLIMATE_PRESET_ACTIVITY, climate::CLIMATE_PRESET_NONE}) {}
protected:
void setup() override;
void control(const climate::ClimateCall &call) override;
// Transmit via IR the state of this climate controller.
void transmit_state() override;
uint8_t operation_mode_() const;
uint16_t fan_speed_() const;
uint8_t temperature_() const;
void set_presets_(uint8_t *remote_state) const;
/// Arms or cancels the boost timer when the preset moves into or out of boost.
void update_boost_timer_(bool was_boost);
void cancel_boost_mode_();
// Handle received IR Buffer
bool on_receive(remote_base::RemoteReceiveData data) override;
bool parse_state_frame_(const uint8_t frame[]);
/// What the 0xC5 frame of the current transmission said about comfort, applied by the state frame
enum class PendingComfort : uint8_t { PENDING_COMFORT_UNKNOWN, PENDING_COMFORT_OFF, PENDING_COMFORT_ON };
PendingComfort pending_comfort_{PendingComfort::PENDING_COMFORT_UNKNOWN};
uint32_t pending_comfort_at_{0};
};
} // namespace esphome::daikin
+2 -2
View File
@@ -294,7 +294,7 @@ using display_writer_t = DisplayWriter<Display>;
prefix type "\n" \
"%s Rotations: %d °\n" \
"%s Dimensions: %dpx x %dpx", \
prefix, log_display_obj->rotation_, prefix, log_display_obj->get_width(), \
prefix, log_display_obj->get_rotation(), prefix, log_display_obj->get_width(), \
log_display_obj->get_height()); \
}
@@ -710,7 +710,7 @@ class Display : public PollingComponent {
// Internal method to set display auto clearing.
void set_auto_clear(bool auto_clear_enabled) { this->auto_clear_enabled_ = auto_clear_enabled; }
DisplayRotation get_rotation() const { return this->rotation_; }
virtual DisplayRotation get_rotation() const { return 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.
+20 -4
View File
@@ -44,13 +44,29 @@ def _get_data() -> EmonTxData:
return CORE.data[DOMAIN]
def _synchronous_automation(value: ConfigType) -> ConfigType:
"""Reject deferring actions: the received line and parsed JSON are only valid while the
automation runs, and DelayAction and friends keep the trigger arguments for later."""
if automation.has_non_synchronous_actions(value):
raise cv.Invalid(
"Deferring actions (delay, wait_until, script.wait, ...) are not allowed in on_json and "
"on_data: the received data is only valid while the automation runs. Copy what you need "
"into globals first, then defer in a separate script or automation."
)
return value
def _synchronous_automation_schema() -> cv.All:
return cv.All(automation.validate_automation({}), _synchronous_automation)
# Main configuration schema
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(EmonTx),
cv.Optional(CONF_ON_JSON): automation.validate_automation({}),
cv.Optional(CONF_ON_DATA): automation.validate_automation({}),
cv.Optional(CONF_ON_JSON): _synchronous_automation_schema(),
cv.Optional(CONF_ON_DATA): _synchronous_automation_schema(),
}
)
.extend(cv.COMPONENT_SCHEMA)
@@ -104,10 +120,10 @@ _CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(
CONF_ON_JSON,
"add_on_json_callback",
[(cg.JsonObject, "json"), (cg.std_string, "raw_json")],
[(cg.JsonObject, "json"), (cg.StringRef, "raw_json")],
),
automation.CallbackAutomation(
CONF_ON_DATA, "add_on_data_callback", [(cg.std_string, "data")]
CONF_ON_DATA, "add_on_data_callback", [(cg.StringRef, "data")]
),
)
+11 -6
View File
@@ -18,13 +18,15 @@ void EmonTx::setup() { this->buffer_pos_ = 0; }
void EmonTx::loop() {
// Read all available data to prevent UART buffer overflow
while (this->available() > 0) {
uint8_t received = this->read();
uint8_t received;
if (!this->read_byte(&received))
break;
if (received == '\r') {
continue; // Ignore CR
} else if (received == '\n') {
if (received == '\n') {
// End of line - process the buffer
if (this->buffer_pos_ > 0) {
if (this->discarding_line_) {
this->discarding_line_ = false;
} else if (this->buffer_pos_ > 0) {
// Null-terminate for safe logging and c_str() use
size_t len = this->buffer_pos_;
this->buffer_[len] = '\0';
@@ -42,9 +44,12 @@ void EmonTx::loop() {
this->parse_json_(this->buffer_.data(), len);
}
}
} else if (received == '\r' || this->discarding_line_) {
// Ignore CR, and the rest of a line that overflowed the buffer
} else if (this->buffer_pos_ >= MAX_LINE_LENGTH) {
ESP_LOGW(TAG, "Buffer overflow (>%zu bytes), discarding buffer", MAX_LINE_LENGTH);
ESP_LOGW(TAG, "Buffer overflow (>%zu bytes), discarding line", MAX_LINE_LENGTH);
this->buffer_pos_ = 0;
this->discarding_line_ = true;
} else {
this->buffer_[this->buffer_pos_++] = static_cast<char>(received);
}
+1
View File
@@ -55,6 +55,7 @@ class EmonTx final : public Component, public uart::UARTDevice {
LazyCallbackManager<void(JsonObject, StringRef)> json_callbacks_;
LazyCallbackManager<void(StringRef)> data_callbacks_;
uint16_t buffer_pos_{0};
bool discarding_line_{false}; // Set after an overflow until the end of that line
std::array<char, MAX_LINE_LENGTH + 1> buffer_{};
};
+1 -1
View File
@@ -1,5 +1,6 @@
import esphome.codegen as cg
from esphome.components import sensor
from esphome.components.const import CONF_AQI
import esphome.config_validation as cv
from esphome.const import (
CONF_COMPENSATION,
@@ -26,7 +27,6 @@ DOMAIN = "ens160_base"
ens160_ns = cg.esphome_ns.namespace("ens160_base")
CONF_AQI = "aqi"
CONFIG_SCHEMA_BASE = cv.Schema(
{
+98 -3
View File
@@ -551,9 +551,88 @@ assert all(variant in CPU_FREQUENCIES for variant in VARIANTS)
FULL_CPU_FREQUENCIES = set(itertools.chain.from_iterable(CPU_FREQUENCIES.values()))
_SDKCONFIG_CPU_FREQUENCY_OPTION = "CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ"
_SDKCONFIG_CPU_FREQUENCY_PREFIX = f"{_SDKCONFIG_CPU_FREQUENCY_OPTION}_"
# Older IDF versions named the CPU frequency options after the chip
_LEGACY_SDKCONFIG_CPU_FREQUENCY_VARIANTS = frozenset(
{VARIANT_ESP32, VARIANT_ESP32C3, VARIANT_ESP32S2, VARIANT_ESP32S3}
)
def _get_sdkconfig_cpu_frequencies(
sdkconfig_options: dict[str, str], variant: str
) -> tuple[set[int], set[int]]:
"""Return the CPU frequencies in MHz that sdkconfig_options selects and disables."""
# (integer option, choice option prefix) pairs that can carry a frequency
keys = [(_SDKCONFIG_CPU_FREQUENCY_OPTION, _SDKCONFIG_CPU_FREQUENCY_PREFIX)]
if variant in _LEGACY_SDKCONFIG_CPU_FREQUENCY_VARIANTS:
keys.append(
(
f"CONFIG_{variant}_DEFAULT_CPU_FREQ_MHZ",
f"CONFIG_{variant}_DEFAULT_CPU_FREQ_",
)
)
selected: set[int] = set()
disabled: set[int] = set()
for integer_key, choice_prefix in keys:
if (value := sdkconfig_options.get(integer_key)) is not None:
try:
selected.add(int(value))
except ValueError as err:
raise cv.Invalid(
f"{integer_key} must be an integer MHz value",
path=[CONF_FRAMEWORK, CONF_SDKCONFIG_OPTIONS],
) from err
for name, value in sdkconfig_options.items():
mhz = name.removeprefix(choice_prefix)
if mhz == name or not mhz.isdigit():
continue
if value.lower() == "y":
selected.add(int(mhz))
elif value.lower() == "n":
disabled.add(int(mhz))
else:
raise cv.Invalid(
f"{name} must be y or n",
path=[CONF_FRAMEWORK, CONF_SDKCONFIG_OPTIONS],
)
return selected, disabled
def set_core_data(config):
cpu_frequency = config.get(CONF_CPU_FREQUENCY, None)
cpu_frequency = config.get(CONF_CPU_FREQUENCY)
variant = config[CONF_VARIANT]
sdkconfig_path = [CONF_FRAMEWORK, CONF_SDKCONFIG_OPTIONS]
selected, disabled = _get_sdkconfig_cpu_frequencies(
config[CONF_FRAMEWORK][CONF_SDKCONFIG_OPTIONS], variant
)
if len(selected) > 1:
conflicting = ", ".join(f"{mhz}MHz" for mhz in sorted(selected))
raise cv.Invalid(
f"sdkconfig_options contains conflicting CPU frequencies ({conflicting})",
path=sdkconfig_path,
)
sdkconfig_cpu_frequency = f"{selected.pop()}MHZ" if selected else None
if cpu_frequency is None and sdkconfig_cpu_frequency is not None:
if sdkconfig_cpu_frequency not in CPU_FREQUENCIES[variant]:
raise cv.Invalid(
f"sdkconfig_options selects {sdkconfig_cpu_frequency}, which {variant} does not support",
path=sdkconfig_path,
)
_LOGGER.warning(
"sdkconfig_options contains a CPU frequency setting; using %s. "
"Set 'esp32.cpu_frequency' to configure it directly.",
sdkconfig_cpu_frequency,
)
cpu_frequency = sdkconfig_cpu_frequency
elif (
sdkconfig_cpu_frequency is not None and sdkconfig_cpu_frequency != cpu_frequency
):
raise cv.Invalid(
f"esp32.cpu_frequency ({cpu_frequency}) conflicts with sdkconfig_options "
f"({sdkconfig_cpu_frequency})",
path=sdkconfig_path,
)
# if not specified in config, default to the maximum supported frequency
# (ESP32-P4 engineering samples are limited to 360MHz, non-engineering can do 400MHz)
if cpu_frequency is None:
@@ -562,12 +641,17 @@ def set_core_data(config):
cpu_frequency = "360MHZ"
else:
cpu_frequency = choices[-1]
config[CONF_CPU_FREQUENCY] = cpu_frequency
elif cpu_frequency not in CPU_FREQUENCIES[variant]:
raise cv.Invalid(
f"Invalid CPU frequency '{cpu_frequency}' for {config[CONF_VARIANT]}",
path=[CONF_CPU_FREQUENCY],
)
if int(cpu_frequency[:-3]) in disabled:
raise cv.Invalid(
f"sdkconfig_options disables the selected CPU frequency ({cpu_frequency})",
path=sdkconfig_path,
)
config[CONF_CPU_FREQUENCY] = cpu_frequency
if variant == VARIANT_ESP32P4 and cpu_frequency == "400MHZ":
_LOGGER.warning(
@@ -1987,6 +2071,17 @@ def require_vfs_termios() -> None:
CORE.data[KEY_VFS_TERMIOS_REQUIRED] = True
def allow_insecure_tls() -> None:
"""Let esp-tls connect without verifying the server certificate.
The two sdkconfig flags are global, so a component asking for an unverified
connection switches them on for the whole build; they are never switched off
here because another component may have asked first.
"""
add_idf_sdkconfig_option("CONFIG_ESP_TLS_INSECURE", True)
add_idf_sdkconfig_option("CONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY", True)
def require_certificate_bundle() -> None:
"""Enable the mbedTLS root certificate bundle for this build.
@@ -3138,7 +3233,7 @@ async def to_code(config):
# Set default CPU frequency
add_idf_sdkconfig_option(
f"CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_{config[CONF_CPU_FREQUENCY][:-3]}", True
f"{_SDKCONFIG_CPU_FREQUENCY_PREFIX}{config[CONF_CPU_FREQUENCY][:-3]}", True
)
# Apply LWIP optimization settings
+24 -7
View File
@@ -17,6 +17,8 @@ from esphome.const import (
CONF_ON_ERROR,
CONF_TRIGGER_ID,
CONF_WIFI,
PLATFORM_ESP32,
PLATFORM_ESP8266,
)
from esphome.core import CORE, HexInt
from esphome.cpp_generator import MockObj, TemplateArgsType
@@ -24,7 +26,14 @@ import esphome.final_validate as fv
from esphome.types import ConfigType
CODEOWNERS = ["@jesserockz"]
AUTO_LOAD = ["network"]
def AUTO_LOAD() -> list[str]:
# network initialises esp_netif and the default event loop on the ESP32. The ESP8266 needs neither, and
# loading it there would pull in mdns, which requires wifi
return ["network"] if CORE.is_esp32 else []
DOMAIN = "espnow"
peer_address_t = cg.std_ns.class_("array").template(cg.uint8, 6)
@@ -121,15 +130,21 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_ADDRESS): cv.mac_address,
}
),
cv.Optional(CONF_ON_BROADCAST): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OnBroadcastTrigger),
cv.Optional(CONF_ADDRESS): cv.mac_address,
}
# The ESP8266 SDK does not report the destination address, so broadcasts are indistinguishable there
cv.Optional(CONF_ON_BROADCAST): cv.All(
cv.only_on_esp32,
automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
OnBroadcastTrigger
),
cv.Optional(CONF_ADDRESS): cv.mac_address,
}
),
),
},
).extend(cv.COMPONENT_SCHEMA),
cv.only_on_esp32,
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266]),
)
@@ -138,6 +153,8 @@ def _validate_variant(config: ConfigType) -> ConfigType:
# ESP-NOW; only the ESP32-P4 has a path, via the esp32_hosted shim that
# supplies the esp_now_* symbols. Fail here with a clear message instead of
# letting the build reach an "undefined reference to esp_now_*" link error.
if not CORE.is_esp32:
return config
variant = get_esp32_variant()
if wifi.variant_has_wifi(variant):
return config
+2 -2
View File
@@ -1,6 +1,6 @@
#pragma once
#ifdef USE_ESP32
#if defined(USE_ESP32) || defined(USE_ESP8266)
#include "espnow_component.h"
@@ -142,4 +142,4 @@ class OnBroadcastTrigger final : public Trigger<const ESPNowRecvInfo &, const ui
} // namespace esphome::espnow
#endif // USE_ESP32
#endif // USE_ESP32 || USE_ESP8266
+67 -100
View File
@@ -1,25 +1,19 @@
#include "espnow_component.h"
#ifdef USE_ESP32
#if defined(USE_ESP32) || defined(USE_ESP8266)
#include "espnow_err.h"
#include "espnow_platform.h"
#include <algorithm>
#include <cinttypes>
#include <cstring>
#include <utility>
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <esp_event.h>
#include <esp_mac.h>
#include <esp_netif.h>
#include <esp_now.h>
#include <esp_random.h>
#include <esp_wifi.h>
#include <cstring>
#include <memory>
#ifdef USE_WIFI
#include "esphome/components/wifi/wifi_component.h"
#endif
@@ -49,7 +43,7 @@ static const LogString *espnow_error_to_str(esp_err_t error) {
case ESP_ERR_ESPNOW_INTERNAL:
return LOG_STR("Internal Error");
case ESP_ERR_ESPNOW_NO_MEM:
return LOG_STR("Our of memory");
return LOG_STR("Out of memory");
case ESP_ERR_ESPNOW_NOT_FOUND:
return LOG_STR("Peer not found");
case ESP_ERR_ESPNOW_IF:
@@ -57,96 +51,91 @@ static const LogString *espnow_error_to_str(esp_err_t error) {
case ESP_OK:
return LOG_STR("OK");
case ESP_NOW_SEND_FAIL:
case ESP_FAIL:
return LOG_STR("Failed");
default:
return LOG_STR("Unknown Error");
}
}
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
void on_send_report(const esp_now_send_info_t *info, esp_now_send_status_t status)
#else
void on_send_report(const uint8_t *mac_addr, esp_now_send_status_t status)
#endif
{
// The analyzer traces a leak on the failed-push path, which cannot happen: the
// pools are sized to the queue capacity (SIZE-1), so allocate() returns nullptr
// before push() can find the ring full.
// NOLINTBEGIN(clang-analyzer-unix.Malloc)
void ESPNowComponent::send_reported(const uint8_t *mac_addr, esp_now_send_status_t status) {
// Allocate an event from the pool
ESPNowPacket *packet = global_esp_now->receive_packet_pool_.allocate();
ESPNowPacket *packet = this->receive_packet_pool_.allocate();
if (packet == nullptr) {
// No events available - queue is full or we're out of memory
global_esp_now->receive_packet_queue_.increment_dropped_count();
global_esp_now->enable_loop_soon_any_context();
this->receive_packet_queue_.increment_dropped_count();
this->enable_loop_soon_any_context();
return;
}
// Load new packet data (replaces previous packet)
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
packet->load_sent_data(info->des_addr, status);
#else
packet->load_sent_data(mac_addr, status);
#endif
// Push the packet to the queue
global_esp_now->receive_packet_queue_.push(packet);
// Push always succeeds: pool is sized to queue capacity (SIZE-1), so if
// allocate() returned non-null, the queue cannot be full.
this->receive_packet_queue_.push(packet);
// Re-enable and wake the main loop to process the ESP-NOW send event
global_esp_now->enable_loop_soon_any_context();
this->enable_loop_soon_any_context();
}
void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int size) {
void ESPNowComponent::packet_received(const uint8_t *src_addr, const uint8_t *des_addr, const uint8_t *data, int size,
int8_t rssi, uint32_t timestamp) {
// Drop oversized frames before copying. ESP-NOW v2 peers (IDF >= 5.4 builds a
// v2 stack with no opt-out) can send up to ESP_NOW_MAX_DATA_LEN_V2 (1470 B),
// but the receive buffer only fits v2 frames with ``max_payload_size``; copying a
// but the receive buffer only fits frames up to ``max_payload_size``; copying a
// larger frame would overflow packet_.receive.data.
if (size < 0 || size > ESPNOW_MAX_DATA_LEN) {
global_esp_now->receive_packet_queue_.increment_dropped_count();
global_esp_now->enable_loop_soon_any_context();
if (size < 0 || static_cast<size_t>(size) > ESPNOW_MAX_DATA_LEN) {
this->receive_packet_queue_.increment_dropped_count();
this->enable_loop_soon_any_context();
return;
}
// Allocate an event from the pool
ESPNowPacket *packet = global_esp_now->receive_packet_pool_.allocate();
ESPNowPacket *packet = this->receive_packet_pool_.allocate();
if (packet == nullptr) {
// No events available - queue is full or we're out of memory
global_esp_now->receive_packet_queue_.increment_dropped_count();
global_esp_now->enable_loop_soon_any_context();
this->receive_packet_queue_.increment_dropped_count();
this->enable_loop_soon_any_context();
return;
}
// Load new packet data (replaces previous packet)
packet->load_received_data(info, data, size);
packet->load_received_data(src_addr, des_addr, data, size, rssi, timestamp);
// Push the packet to the queue
global_esp_now->receive_packet_queue_.push(packet);
// Push always succeeds: pool is sized to queue capacity (SIZE-1), so if
// allocate() returned non-null, the queue cannot be full.
this->receive_packet_queue_.push(packet);
// Re-enable and wake the main loop to process the ESP-NOW receive event
global_esp_now->enable_loop_soon_any_context();
this->enable_loop_soon_any_context();
}
// NOLINTEND(clang-analyzer-unix.Malloc)
ESPNowComponent::ESPNowComponent() { global_esp_now = this; }
void ESPNowComponent::dump_config() {
ESP_LOGCONFIG(TAG, "espnow:");
// Only report driver details once enabled; with enable_on_boot: false the
// Wi-Fi driver is not initialized yet and esp_now_get_version() would crash,
// Wi-Fi driver is not initialized yet and the version call would crash,
// and after a failed enable_() the values would be meaningless.
if (this->state_ != ESPNOW_STATE_ENABLED) {
// OFF here means enable_() failed; the core logs the FAILED marker separately
ESP_LOGCONFIG(TAG, " %s", this->is_disabled() ? LOG_STR_LITERAL("Disabled") : LOG_STR_LITERAL("Not enabled"));
return;
}
uint32_t version = 0;
esp_now_get_version(&version);
char own_addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
format_mac_addr_upper(this->own_address_, own_addr_buf);
ESP_LOGCONFIG(TAG,
" Own address: %s\n"
" Version: v%" PRIu32 "\n"
" Wi-Fi channel: %d",
own_addr_buf, version, this->wifi_channel_);
own_addr_buf, this->wifi_channel_);
uint32_t version = platform::get_version();
if (version != 0) {
ESP_LOGCONFIG(TAG, " Version: v%" PRIu32, version);
}
#ifdef USE_WIFI
ESP_LOGCONFIG(TAG, " Wi-Fi enabled: %s", YESNO(this->is_wifi_enabled()));
#endif
@@ -183,6 +172,10 @@ void ESPNowComponent::on_wifi_connect_state(StringRef ssid, std::span<const uint
if (this->wifi_channel_ != old_channel) {
ESP_LOGI(TAG, "WiFi channel changed from %d to %d", old_channel, this->wifi_channel_);
}
// Peers added while the station was scanning were bound to the channel of that moment, so rebind on every connect
if (this->state_ == ESPNOW_STATE_ENABLED) {
platform::rebind_peers();
}
}
#endif
@@ -198,41 +191,19 @@ void ESPNowComponent::enable() {
void ESPNowComponent::enable_() {
if (!this->is_wifi_enabled()) {
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM));
ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE));
ESP_ERROR_CHECK(esp_wifi_start());
ESP_ERROR_CHECK(esp_wifi_disconnect());
platform::init_radio();
this->apply_wifi_channel();
}
this->get_wifi_channel();
esp_err_t err = esp_now_init();
esp_err_t err = platform::init();
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_now_init failed: %s", esp_err_to_name(err));
ESP_LOGE(TAG, "Init failed: %s", esp_err_to_name(err));
this->mark_failed();
return;
}
err = esp_now_register_recv_cb(on_data_received);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_now_register_recv_cb failed: %s", esp_err_to_name(err));
this->mark_failed();
return;
}
err = esp_now_register_send_cb(on_send_report);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_now_register_recv_cb failed: %s", esp_err_to_name(err));
this->mark_failed();
return;
}
esp_wifi_get_mac(WIFI_IF_STA, this->own_address_);
platform::read_mac(this->own_address_);
this->state_ = ESPNOW_STATE_ENABLED;
@@ -248,12 +219,9 @@ void ESPNowComponent::disable() {
ESP_LOGD(TAG, "Disabling");
this->state_ = ESPNOW_STATE_DISABLED;
esp_now_unregister_recv_cb();
esp_now_unregister_send_cb();
esp_err_t err = esp_now_deinit();
esp_err_t err = platform::deinit();
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_now_deinit failed! 0x%x", err);
ESP_LOGE(TAG, "Deinit failed: %s", esp_err_to_name(err));
}
}
@@ -271,9 +239,10 @@ void ESPNowComponent::apply_wifi_channel() {
}
ESP_LOGI(TAG, "Channel set to %d.", this->wifi_channel_);
esp_wifi_set_promiscuous(true);
esp_wifi_set_channel(this->wifi_channel_, WIFI_SECOND_CHAN_NONE);
esp_wifi_set_promiscuous(false);
platform::set_channel(this->wifi_channel_);
if (this->state_ == ESPNOW_STATE_ENABLED) {
platform::rebind_peers();
}
}
void ESPNowComponent::loop() {
@@ -283,7 +252,7 @@ void ESPNowComponent::loop() {
switch (packet->type_) {
case ESPNowPacket::RECEIVED: {
const ESPNowRecvInfo info = packet->get_receive_info();
if (!esp_now_is_peer_exist(info.src_addr)) {
if (!platform::peer_exists(info.src_addr)) {
bool handled = false;
for (auto *handler : this->unknown_peer_handlers_) {
if (handler->on_unknown_peer(info, packet->packet_.receive.data, packet->packet_.receive.size)) {
@@ -296,7 +265,7 @@ void ESPNowComponent::loop() {
}
}
// Intentionally left as if instead of else in case the peer is added above
if (esp_now_is_peer_exist(info.src_addr)) {
if (platform::peer_exists(info.src_addr)) {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char src_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
char dst_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
@@ -309,7 +278,8 @@ void ESPNowComponent::loop() {
format_hex_pretty_to(hex_buf, packet->packet_.receive.data,
std::min<uint16_t>(packet->packet_.receive.size, ESP_NOW_MAX_DATA_LEN)));
#endif
if (memcmp(info.des_addr, ESPNOW_BROADCAST_ADDR, ESP_NOW_ETH_ALEN) == 0) {
// Without a destination address every packet goes to the receive handlers
if (ESPNOW_REPORTS_DESTINATION && memcmp(info.des_addr, ESPNOW_BROADCAST_ADDR, ESP_NOW_ETH_ALEN) == 0) {
for (auto *handler : this->broadcast_handlers_) {
if (handler->on_broadcast(info, packet->packet_.receive.data, packet->packet_.receive.size))
break; // If a handler returns true, stop processing further handlers
@@ -374,24 +344,25 @@ void ESPNowComponent::loop() {
}
uint8_t ESPNowComponent::get_wifi_channel() {
wifi_second_chan_t dummy;
esp_wifi_get_channel(&this->wifi_channel_, &dummy);
this->wifi_channel_ = platform::get_channel();
return this->wifi_channel_;
}
// Same analyzer false positive as above: the send pool is sized to the send queue's capacity
// NOLINTBEGIN(clang-analyzer-unix.Malloc)
esp_err_t ESPNowComponent::send(const uint8_t *peer_address, const uint8_t *payload, size_t size,
const send_callback_t &callback) {
send_callback_t callback) {
if (this->state_ != ESPNOW_STATE_ENABLED) {
return ESP_ERR_ESPNOW_NOT_INIT;
} else if (this->is_failed()) {
return ESP_ERR_ESPNOW_FAILED;
} else if (peer_address == 0ULL) {
} else if (peer_address == nullptr) {
return ESP_ERR_ESPNOW_PEER_NOT_SET;
} else if (memcmp(peer_address, this->own_address_, ESP_NOW_ETH_ALEN) == 0) {
return ESP_ERR_ESPNOW_OWN_ADDRESS;
} else if (size > ESPNOW_MAX_DATA_LEN) {
return ESP_ERR_ESPNOW_DATA_SIZE;
} else if (!esp_now_is_peer_exist(peer_address)) {
} else if (!platform::peer_exists(peer_address)) {
if (memcmp(peer_address, ESPNOW_BROADCAST_ADDR, ESP_NOW_ETH_ALEN) == 0 || this->auto_add_peer_) {
esp_err_t err = this->add_peer(peer_address);
if (err != ESP_OK) {
@@ -410,7 +381,7 @@ esp_err_t ESPNowComponent::send(const uint8_t *peer_address, const uint8_t *payl
return ESP_ERR_ESPNOW_NO_MEM;
}
// Load the packet data
packet->load_data(peer_address, payload, size, callback);
packet->load_data(peer_address, payload, size, std::move(callback));
// Push the packet to the send queue
this->send_packet_queue_.push(packet);
// Loop may be disabled while idle; re-enable it to send the packet
@@ -418,6 +389,7 @@ esp_err_t ESPNowComponent::send(const uint8_t *peer_address, const uint8_t *payl
this->enable_loop_soon_any_context();
return ESP_OK;
}
// NOLINTEND(clang-analyzer-unix.Malloc)
void ESPNowComponent::send_() {
ESPNowSendPacket *packet = this->send_packet_queue_.pop();
@@ -426,7 +398,7 @@ void ESPNowComponent::send_() {
}
this->current_send_packet_ = packet;
esp_err_t err = esp_now_send(packet->address_, packet->data_, packet->size_);
esp_err_t err = platform::send(packet->address_, packet->data_, packet->size_);
if (err != ESP_OK) {
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
format_mac_addr_upper(packet->address_, addr_buf);
@@ -451,13 +423,8 @@ esp_err_t ESPNowComponent::add_peer(const uint8_t *peer) {
return ESP_ERR_INVALID_MAC;
}
if (!esp_now_is_peer_exist(peer)) {
esp_now_peer_info_t peer_info = {};
memset(&peer_info, 0, sizeof(esp_now_peer_info_t));
peer_info.ifidx = WIFI_IF_STA;
memcpy(peer_info.peer_addr, peer, ESP_NOW_ETH_ALEN);
esp_err_t err = esp_now_add_peer(&peer_info);
if (!platform::peer_exists(peer)) {
esp_err_t err = platform::add_peer(peer);
if (err != ESP_OK) {
char peer_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
format_mac_addr_upper(peer, peer_buf);
@@ -486,8 +453,8 @@ esp_err_t ESPNowComponent::del_peer(const uint8_t *peer) {
if (this->state_ != ESPNOW_STATE_ENABLED || this->is_failed()) {
return ESP_ERR_ESPNOW_NOT_INIT;
}
if (esp_now_is_peer_exist(peer)) {
esp_err_t err = esp_now_del_peer(peer);
if (platform::peer_exists(peer)) {
esp_err_t err = platform::del_peer(peer);
if (err != ESP_OK) {
char peer_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
format_mac_addr_upper(peer, peer_buf);
@@ -507,4 +474,4 @@ esp_err_t ESPNowComponent::del_peer(const uint8_t *peer) {
} // namespace esphome::espnow
#endif // USE_ESP32
#endif // USE_ESP32 || USE_ESP8266
+12 -23
View File
@@ -4,21 +4,17 @@
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#ifdef USE_ESP32
#if defined(USE_ESP32) || defined(USE_ESP8266)
#include "esphome/core/event_pool.h"
#include "esphome/core/lock_free_queue.h"
#include "espnow_packet.h"
#include "espnow_types.h"
#if defined(USE_WIFI) && defined(USE_WIFI_CONNECT_STATE_LISTENERS)
#include "esphome/components/wifi/wifi_component.h"
#endif
#include <esp_idf_version.h>
#include <esp_mac.h>
#include <esp_now.h>
#include <array>
#include <map>
#include <memory>
@@ -27,10 +23,6 @@
namespace esphome::espnow {
// Maximum size of the ESPNow event queue - must be power of 2 for lock-free queue
static constexpr size_t MAX_ESP_NOW_SEND_QUEUE_SIZE = 16;
static constexpr size_t MAX_ESP_NOW_RECEIVE_QUEUE_SIZE = 16;
using peer_address_t = std::array<uint8_t, ESP_NOW_ETH_ALEN>;
enum class ESPNowTriggers : uint8_t {
@@ -152,12 +144,10 @@ class ESPNowComponent final : public Component {
/// @param payload Data payload to send
/// @param callback Callback to call when the send operation is complete
/// @return ESP_OK on success, or an error code on failure
esp_err_t send(const uint8_t *peer_address, const std::vector<uint8_t> &payload,
const send_callback_t &callback = nullptr) {
return this->send(peer_address, payload.data(), payload.size(), callback);
esp_err_t send(const uint8_t *peer_address, const std::vector<uint8_t> &payload, send_callback_t callback = nullptr) {
return this->send(peer_address, payload.data(), payload.size(), std::move(callback));
}
esp_err_t send(const uint8_t *peer_address, const uint8_t *payload, size_t size,
const send_callback_t &callback = nullptr);
esp_err_t send(const uint8_t *peer_address, const uint8_t *payload, size_t size, send_callback_t callback = nullptr);
void register_receive_handler(ESPNowReceivedPacketHandler *handler) { this->receive_handlers_.push_back(handler); }
void register_unknown_peer_handler(ESPNowUnknownPeerHandler *handler) {
@@ -165,14 +155,13 @@ class ESPNowComponent final : public Component {
}
void register_broadcast_handler(ESPNowBroadcastHandler *handler) { this->broadcast_handlers_.push_back(handler); }
protected:
friend void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int size);
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
friend void on_send_report(const esp_now_send_info_t *info, esp_now_send_status_t status);
#else
friend void on_send_report(const uint8_t *mac_addr, esp_now_send_status_t status);
#endif
// Entry points for the radio driver callbacks in espnow_platform_*.cpp; they run outside the main loop and
// only queue the event
void packet_received(const uint8_t *src_addr, const uint8_t *des_addr, const uint8_t *data, int size, int8_t rssi,
uint32_t timestamp);
void send_reported(const uint8_t *mac_addr, esp_now_send_status_t status);
protected:
void enable_();
void send_();
@@ -205,4 +194,4 @@ extern ESPNowComponent *global_esp_now; // NOLINT(cppcoreguidelines-avoid-non-c
} // namespace esphome::espnow
#endif // USE_ESP32
#endif // USE_ESP32 || USE_ESP8266
+9 -10
View File
@@ -1,19 +1,18 @@
#pragma once
#ifdef USE_ESP32
#if defined(USE_ESP32) || defined(USE_ESP8266)
#include <esp_err.h>
#include <esp_now.h>
#include "espnow_types.h"
namespace esphome::espnow {
static const esp_err_t ESP_ERR_ESPNOW_CMP_BASE = (ESP_ERR_ESPNOW_BASE + 20);
static const esp_err_t ESP_ERR_ESPNOW_FAILED = (ESP_ERR_ESPNOW_CMP_BASE + 1);
static const esp_err_t ESP_ERR_ESPNOW_OWN_ADDRESS = (ESP_ERR_ESPNOW_CMP_BASE + 2);
static const esp_err_t ESP_ERR_ESPNOW_DATA_SIZE = (ESP_ERR_ESPNOW_CMP_BASE + 3);
static const esp_err_t ESP_ERR_ESPNOW_PEER_NOT_SET = (ESP_ERR_ESPNOW_CMP_BASE + 4);
static const esp_err_t ESP_ERR_ESPNOW_PEER_NOT_PAIRED = (ESP_ERR_ESPNOW_CMP_BASE + 5);
static constexpr esp_err_t ESP_ERR_ESPNOW_CMP_BASE = (ESP_ERR_ESPNOW_BASE + 20);
static constexpr esp_err_t ESP_ERR_ESPNOW_FAILED = (ESP_ERR_ESPNOW_CMP_BASE + 1);
static constexpr esp_err_t ESP_ERR_ESPNOW_OWN_ADDRESS = (ESP_ERR_ESPNOW_CMP_BASE + 2);
static constexpr esp_err_t ESP_ERR_ESPNOW_DATA_SIZE = (ESP_ERR_ESPNOW_CMP_BASE + 3);
static constexpr esp_err_t ESP_ERR_ESPNOW_PEER_NOT_SET = (ESP_ERR_ESPNOW_CMP_BASE + 4);
static constexpr esp_err_t ESP_ERR_ESPNOW_PEER_NOT_PAIRED = (ESP_ERR_ESPNOW_CMP_BASE + 5);
} // namespace esphome::espnow
#endif // USE_ESP32
#endif // USE_ESP32 || USE_ESP8266
+19 -51
View File
@@ -1,19 +1,17 @@
#pragma once
#ifdef USE_ESP32
#if defined(USE_ESP32) || defined(USE_ESP8266)
#include "espnow_err.h"
#include "espnow_types.h"
#include <cstdint>
#include <cstring>
#include <functional>
#include <memory>
#include <utility>
#include <vector>
#include <esp_err.h>
#include <esp_idf_version.h>
#include <esp_now.h>
namespace esphome::espnow {
static const uint8_t ESPNOW_BROADCAST_ADDR[ESP_NOW_ETH_ALEN] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
@@ -36,16 +34,10 @@ static_assert(ESPNOW_MAX_DATA_LEN <= ESP_NOW_MAX_DATA_LEN,
"espnow max_payload_size beyond 250 bytes requires an ESP-IDF with ESP-NOW v2 support (5.4+)");
#endif
struct WifiPacketRxControl {
int8_t rssi; // Received Signal Strength Indicator (RSSI) of packet, unit: dBm
uint32_t timestamp; // Timestamp in microseconds when the packet was received, precise only if modem sleep or
// light sleep is not enabled
};
struct ESPNowRecvInfo {
uint8_t src_addr[ESP_NOW_ETH_ALEN]; /**< Source address of ESPNOW packet */
uint8_t des_addr[ESP_NOW_ETH_ALEN]; /**< Destination address of ESPNOW packet */
wifi_pkt_rx_ctrl_t *rx_ctrl; /**< Rx control info of ESPNOW packet */
rx_ctrl_t *rx_ctrl; /**< Rx control info of ESPNOW packet */
};
using send_callback_t = std::function<void(esp_err_t)>;
@@ -58,34 +50,28 @@ class ESPNowPacket {
SENT,
};
// Constructor for received data
ESPNowPacket(const esp_now_recv_info_t *info, const uint8_t *data, int size) {
this->init_received_data_(info, data, size);
};
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
// Constructor for sent data
ESPNowPacket(const esp_now_send_info_t *info, esp_now_send_status_t status) {
this->init_sent_data_(info->src_addr, status);
}
#else
// Constructor for sent data
ESPNowPacket(const uint8_t *mac_addr, esp_now_send_status_t status) { this->init_sent_data_(mac_addr, status); }
#endif
// Default constructor for pre-allocation in pool
ESPNowPacket() {}
void release() {}
void load_received_data(const esp_now_recv_info_t *info, const uint8_t *data, int size) {
void load_received_data(const uint8_t *src_addr, const uint8_t *des_addr, const uint8_t *data, uint16_t size,
int8_t rssi, uint32_t timestamp) {
this->type_ = RECEIVED;
this->init_received_data_(info, data, size);
memcpy(this->packet_.receive.info.src_addr, src_addr, ESP_NOW_ETH_ALEN);
memcpy(this->packet_.receive.info.des_addr, des_addr, ESP_NOW_ETH_ALEN);
memcpy(this->packet_.receive.data, data, size);
this->packet_.receive.size = size;
this->packet_.receive.rx_ctrl.rssi = rssi;
this->packet_.receive.rx_ctrl.timestamp = timestamp;
this->packet_.receive.info.rx_ctrl = reinterpret_cast<rx_ctrl_t *>(&this->packet_.receive.rx_ctrl);
}
void load_sent_data(const uint8_t *mac_addr, esp_now_send_status_t status) {
this->type_ = SENT;
this->init_sent_data_(mac_addr, status);
memcpy(this->packet_.sent.address, mac_addr, ESP_NOW_ETH_ALEN);
this->packet_.sent.status = status;
}
// Disable copy to prevent double-delete
@@ -112,24 +98,6 @@ class ESPNowPacket {
esp_now_packet_type_t type() const { return this->type_; }
const ESPNowRecvInfo &get_receive_info() const { return this->packet_.receive.info; }
private:
void init_received_data_(const esp_now_recv_info_t *info, const uint8_t *data, int size) {
memcpy(this->packet_.receive.info.src_addr, info->src_addr, ESP_NOW_ETH_ALEN);
memcpy(this->packet_.receive.info.des_addr, info->des_addr, ESP_NOW_ETH_ALEN);
memcpy(this->packet_.receive.data, data, size);
this->packet_.receive.size = size;
this->packet_.receive.rx_ctrl.rssi = info->rx_ctrl->rssi;
this->packet_.receive.rx_ctrl.timestamp = info->rx_ctrl->timestamp;
this->packet_.receive.info.rx_ctrl = reinterpret_cast<wifi_pkt_rx_ctrl_t *>(&this->packet_.receive.rx_ctrl);
}
void init_sent_data_(const uint8_t *mac_addr, esp_now_send_status_t status) {
memcpy(this->packet_.sent.address, mac_addr, ESP_NOW_ETH_ALEN);
this->packet_.sent.status = status;
}
};
class ESPNowSendPacket {
@@ -151,9 +119,9 @@ class ESPNowSendPacket {
ESPNowSendPacket(const ESPNowSendPacket &) = delete;
ESPNowSendPacket &operator=(const ESPNowSendPacket &) = delete;
void load_data(const uint8_t *peer_address, const uint8_t *payload, size_t size, const send_callback_t &callback) {
void load_data(const uint8_t *peer_address, const uint8_t *payload, size_t size, send_callback_t &&callback) {
this->init_data_(peer_address, payload, size);
this->callback_ = callback;
this->callback_ = std::move(callback);
}
void load_data(const uint8_t *peer_address, const uint8_t *payload, size_t size) {
@@ -180,4 +148,4 @@ class ESPNowSendPacket {
} // namespace esphome::espnow
#endif // USE_ESP32
#endif // USE_ESP32 || USE_ESP8266
@@ -0,0 +1,34 @@
#pragma once
#if defined(USE_ESP32) || defined(USE_ESP8266)
#include "espnow_types.h"
#include <cstdint>
// The radio and ESP-NOW driver calls that differ between ESP-IDF and the ESP8266 NONOS SDK. Each platform
// implements the whole set in its own source file, so the component itself carries no platform checks.
namespace esphome::espnow::platform {
// Bring up the station interface when no wifi component owns the radio
void init_radio();
void set_channel(uint8_t channel);
uint8_t get_channel();
void read_mac(uint8_t *mac);
// Start the driver and route its callbacks to the component
esp_err_t init();
esp_err_t deinit();
uint32_t get_version(); // 0 where the SDK has no version call
bool peer_exists(const uint8_t *mac);
esp_err_t add_peer(const uint8_t *mac);
esp_err_t del_peer(const uint8_t *mac);
// Move the registered peers to the radio's current channel; only the NONOS SDK binds peers to a channel
void rebind_peers();
esp_err_t send(const uint8_t *mac, const uint8_t *data, uint16_t size);
} // namespace esphome::espnow::platform
#endif // USE_ESP32 || USE_ESP8266
@@ -0,0 +1,96 @@
#include "espnow_platform.h"
#ifdef USE_ESP32
#include "espnow_component.h"
#include <esp_idf_version.h>
#include <esp_now.h>
#include <esp_wifi.h>
#include <cstring>
namespace esphome::espnow::platform {
static void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int size) {
global_esp_now->packet_received(info->src_addr, info->des_addr, data, size, info->rx_ctrl->rssi,
info->rx_ctrl->timestamp);
}
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
static void on_send_report(const esp_now_send_info_t *info, esp_now_send_status_t status) {
global_esp_now->send_reported(info->des_addr, status);
}
#else
static void on_send_report(const uint8_t *mac_addr, esp_now_send_status_t status) {
global_esp_now->send_reported(mac_addr, status);
}
#endif
void init_radio() {
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM));
ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE));
ESP_ERROR_CHECK(esp_wifi_start());
ESP_ERROR_CHECK(esp_wifi_disconnect());
}
void set_channel(uint8_t channel) {
esp_wifi_set_promiscuous(true);
esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE);
esp_wifi_set_promiscuous(false);
}
uint8_t get_channel() {
uint8_t channel = 0;
wifi_second_chan_t second;
esp_wifi_get_channel(&channel, &second);
return channel;
}
void read_mac(uint8_t *mac) { esp_wifi_get_mac(WIFI_IF_STA, mac); }
esp_err_t init() {
esp_err_t err = esp_now_init();
if (err == ESP_OK) {
err = esp_now_register_recv_cb(on_data_received);
}
if (err == ESP_OK) {
err = esp_now_register_send_cb(on_send_report);
}
return err;
}
esp_err_t deinit() {
esp_now_unregister_recv_cb();
esp_now_unregister_send_cb();
return esp_now_deinit();
}
uint32_t get_version() {
uint32_t version = 0;
esp_now_get_version(&version);
return version;
}
bool peer_exists(const uint8_t *mac) { return esp_now_is_peer_exist(mac); }
esp_err_t add_peer(const uint8_t *mac) {
esp_now_peer_info_t peer_info = {};
peer_info.ifidx = WIFI_IF_STA;
memcpy(peer_info.peer_addr, mac, ESP_NOW_ETH_ALEN);
return esp_now_add_peer(&peer_info);
}
esp_err_t del_peer(const uint8_t *mac) { return esp_now_del_peer(mac); }
// Peers are added on channel 0, which ESP-IDF treats as whatever channel the radio is on
void rebind_peers() {}
esp_err_t send(const uint8_t *mac, const uint8_t *data, uint16_t size) { return esp_now_send(mac, data, size); }
} // namespace esphome::espnow::platform
#endif // USE_ESP32
@@ -0,0 +1,105 @@
#include "espnow_platform.h"
#ifdef USE_ESP8266
#include "espnow_component.h"
namespace esphome::espnow::platform {
// The SDK reports 0 for success and a non-zero value for any failure
static esp_err_t sdk_result(int result) { return result == 0 ? ESP_OK : ESP_FAIL; }
// Stands in for the destination address, which the SDK does not report
static const uint8_t UNKNOWN_ADDR[ESP_NOW_ETH_ALEN] = {0};
static void on_data_received(uint8_t *mac_addr, uint8_t *data, uint8_t size) {
global_esp_now->packet_received(mac_addr, UNKNOWN_ADDR, data, size, 0, 0);
}
static void on_send_report(uint8_t *mac_addr, uint8_t status) {
global_esp_now->send_reported(mac_addr, status == 0 ? ESP_NOW_SEND_SUCCESS : ESP_NOW_SEND_FAIL);
}
void init_radio() {
// The NONOS SDK only delivers broadcast frames to ESP-NOW while the station is associated or the
// soft-AP interface is up. Without Wi-Fi, run a hidden, open soft-AP that accepts no clients so
// broadcasts (for example from a WizMote style remote) are received.
softap_config ap{};
ap.channel = 1;
ap.authmode = AUTH_OPEN;
ap.ssid_hidden = 1;
ap.max_connection = 0;
ap.beacon_interval = 60000;
wifi_set_opmode_current(STATIONAP_MODE);
wifi_softap_set_config_current(&ap);
wifi_softap_dhcps_stop();
wifi_set_sleep_type(NONE_SLEEP_T);
wifi_station_disconnect();
}
void set_channel(uint8_t channel) {
wifi_promiscuous_enable(true);
wifi_set_channel(channel);
wifi_promiscuous_enable(false);
// Keep the soft-AP from init_radio() on the same channel so the SDK never retunes the radio to its own
softap_config ap{};
if (wifi_get_opmode() == STATIONAP_MODE && wifi_softap_get_config(&ap) && ap.channel != channel) {
ap.channel = channel;
wifi_softap_set_config_current(&ap);
}
}
uint8_t get_channel() { return wifi_get_channel(); }
void read_mac(uint8_t *mac) {
// Frames leave from the station interface unless the device runs as a soft-AP only
wifi_get_macaddr(wifi_get_opmode() == SOFTAP_MODE ? SOFTAP_IF : STATION_IF, mac);
}
esp_err_t init() {
int result = esp_now_init();
if (result == 0) {
// CONTROLLER sends from the station interface; the other roles send from the soft-AP MAC whenever
// that interface is up, so peers would no longer recognise the sender.
result = esp_now_set_self_role(ESP_NOW_ROLE_CONTROLLER);
}
if (result == 0) {
result = esp_now_register_recv_cb(on_data_received);
}
if (result == 0) {
result = esp_now_register_send_cb(on_send_report);
}
return sdk_result(result);
}
esp_err_t deinit() {
esp_now_unregister_recv_cb();
esp_now_unregister_send_cb();
return sdk_result(esp_now_deinit());
}
uint32_t get_version() { return 0; }
// The SDK returns 1 when the peer exists, 0 when it does not and a negative value on error
bool peer_exists(const uint8_t *mac) { return esp_now_is_peer_exist(const_cast<uint8_t *>(mac)) > 0; }
esp_err_t add_peer(const uint8_t *mac) {
return sdk_result(esp_now_add_peer(const_cast<uint8_t *>(mac), ESP_NOW_ROLE_COMBO, wifi_get_channel(), nullptr, 0));
}
esp_err_t del_peer(const uint8_t *mac) { return sdk_result(esp_now_del_peer(const_cast<uint8_t *>(mac))); }
void rebind_peers() {
const uint8_t channel = wifi_get_channel();
for (uint8_t *mac = esp_now_fetch_peer(true); mac != nullptr; mac = esp_now_fetch_peer(false)) {
esp_now_set_peer_channel(mac, channel);
}
}
esp_err_t send(const uint8_t *mac, const uint8_t *data, uint16_t size) {
return sdk_result(esp_now_send(const_cast<uint8_t *>(mac), const_cast<uint8_t *>(data), size));
}
} // namespace esphome::espnow::platform
#endif // USE_ESP8266
+77
View File
@@ -0,0 +1,77 @@
#pragma once
#include <cstddef>
#include <cstdint>
#if defined(USE_ESP32)
#include <esp_err.h>
#include <esp_now.h>
#elif defined(USE_ESP8266)
extern "C" {
#include <c_types.h>
#include <espnow.h>
#include <user_interface.h>
}
#endif
namespace esphome::espnow {
struct WifiPacketRxControl {
int8_t rssi; // Received Signal Strength Indicator (RSSI) of packet, unit: dBm
uint32_t timestamp; // Timestamp in microseconds when the packet was received, precise only if modem sleep or
// light sleep is not enabled
};
#if defined(USE_ESP32)
// Handlers read rx_ctrl through the ESP-IDF type so existing lambdas keep compiling
using rx_ctrl_t = wifi_pkt_rx_ctrl_t;
static constexpr bool ESPNOW_REPORTS_DESTINATION = true;
// Maximum size of the ESPNow event queues - must be a power of 2 for the lock-free queue
static constexpr size_t MAX_ESP_NOW_SEND_QUEUE_SIZE = 16;
static constexpr size_t MAX_ESP_NOW_RECEIVE_QUEUE_SIZE = 16;
#elif defined(USE_ESP8266)
// The NONOS SDK has no esp_err_t vocabulary. These are the ESP-IDF names and values the shared espnow code
// uses, so it reads the same on both platforms.
using esp_err_t = int32_t;
static constexpr esp_err_t ESP_OK = 0;
static constexpr esp_err_t ESP_FAIL = -1;
static constexpr esp_err_t ESP_ERR_INVALID_MAC = 0x10B;
static constexpr esp_err_t ESP_ERR_ESPNOW_BASE = 0x3064;
static constexpr esp_err_t ESP_ERR_ESPNOW_NOT_INIT = ESP_ERR_ESPNOW_BASE + 1;
static constexpr esp_err_t ESP_ERR_ESPNOW_ARG = ESP_ERR_ESPNOW_BASE + 2;
static constexpr esp_err_t ESP_ERR_ESPNOW_NO_MEM = ESP_ERR_ESPNOW_BASE + 3;
static constexpr esp_err_t ESP_ERR_ESPNOW_NOT_FOUND = ESP_ERR_ESPNOW_BASE + 5;
static constexpr esp_err_t ESP_ERR_ESPNOW_INTERNAL = ESP_ERR_ESPNOW_BASE + 6;
static constexpr esp_err_t ESP_ERR_ESPNOW_IF = ESP_ERR_ESPNOW_BASE + 8;
inline const char *esp_err_to_name(esp_err_t err) { return err == ESP_OK ? "ESP_OK" : "ESP_FAIL"; }
static constexpr uint8_t ESP_NOW_ETH_ALEN = 6;
static constexpr uint8_t ESP_NOW_MAX_DATA_LEN = 250;
// NOLINTNEXTLINE(readability-identifier-naming)
enum esp_now_send_status_t : uint8_t {
ESP_NOW_SEND_SUCCESS = 0,
ESP_NOW_SEND_FAIL = 1,
};
using rx_ctrl_t = WifiPacketRxControl;
// The SDK reports neither the destination address nor the signal strength of a received frame
static constexpr bool ESPNOW_REPORTS_DESTINATION = false;
// Half the ESP32 depth: the heap is far smaller and only one send is ever in flight
static constexpr size_t MAX_ESP_NOW_SEND_QUEUE_SIZE = 8;
static constexpr size_t MAX_ESP_NOW_RECEIVE_QUEUE_SIZE = 8;
#endif
} // namespace esphome::espnow
@@ -1,10 +1,12 @@
#include "espnow_transport.h"
#ifdef USE_ESP32
#if defined(USE_ESP32) || defined(USE_ESP8266)
#include "esphome/core/application.h"
#include "esphome/core/log.h"
#include <span>
namespace esphome::espnow {
ESPHOME_LOG_TAG(TAG, "espnow.transport");
@@ -64,9 +66,7 @@ bool ESPNowTransport::on_receive(const ESPNowRecvInfo &info, const uint8_t *data
return false;
}
this->packet_buffer_.resize(size);
memcpy(this->packet_buffer_.data(), data, size);
this->process_(this->packet_buffer_);
this->process_(std::span<const uint8_t>(data, size));
return false; // Allow other handlers to run
}
@@ -79,12 +79,10 @@ bool ESPNowTransport::on_broadcast(const ESPNowRecvInfo &info, const uint8_t *da
return false;
}
this->packet_buffer_.resize(size);
memcpy(this->packet_buffer_.data(), data, size);
this->process_(this->packet_buffer_);
this->process_(std::span<const uint8_t>(data, size));
return false; // Allow other handlers to run
}
} // namespace esphome::espnow
#endif // USE_ESP32
#endif // USE_ESP32 || USE_ESP8266
@@ -2,7 +2,7 @@
#include "../espnow_component.h"
#ifdef USE_ESP32
#if defined(USE_ESP32) || defined(USE_ESP8266)
#include "esphome/core/component.h"
#include "esphome/components/packet_transport/packet_transport.h"
@@ -33,9 +33,8 @@ class ESPNowTransport final : public packet_transport::PacketTransport,
bool should_send() override;
peer_address_t peer_address_{{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}};
std::vector<uint8_t> packet_buffer_;
};
} // namespace esphome::espnow
#endif // USE_ESP32
#endif // USE_ESP32 || USE_ESP8266
+22
View File
@@ -24,6 +24,7 @@ CONF_HAS_BUILT_IN_ENDSTOP = "has_built_in_endstop"
CONF_INFER_ENDSTOP_FROM_MOVEMENT = "infer_endstop_from_movement"
CONF_DIRECTION_CHANGE_WAIT_TIME = "direction_change_wait_time"
CONF_ACCELERATION_WAIT_TIME = "acceleration_wait_time"
CONF_OVERSHOOT_DURATION = "overshoot_duration"
CONF_OBSTACLE_ROLLBACK = "obstacle_rollback"
endstop_ns = cg.esphome_ns.namespace("feedback")
@@ -82,6 +83,9 @@ CONFIG_FEEDBACK_COVER_BASE_SCHEMA = (
cv.Optional(
CONF_ACCELERATION_WAIT_TIME, "0s"
): cv.positive_time_period_milliseconds,
cv.Optional(
CONF_OVERSHOOT_DURATION, "0s"
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_OBSTACLE_ROLLBACK, default="10%"): cv.percentage,
},
)
@@ -89,10 +93,25 @@ CONFIG_FEEDBACK_COVER_BASE_SCHEMA = (
)
def validate_overshoot(config: ConfigType) -> ConfigType:
if not config[CONF_OVERSHOOT_DURATION].total_milliseconds:
return config
if config[CONF_HAS_BUILT_IN_ENDSTOP]:
raise cv.Invalid(
f"{CONF_OVERSHOOT_DURATION} has no effect with {CONF_HAS_BUILT_IN_ENDSTOP}"
)
if CONF_OPEN_ENDSTOP in config and CONF_CLOSE_ENDSTOP in config:
raise cv.Invalid(
f"{CONF_OVERSHOOT_DURATION} has no effect when both endstop sensors are supplied"
)
return config
CONFIG_SCHEMA = cv.All(
CONFIG_FEEDBACK_COVER_BASE_SCHEMA,
cv.has_none_or_all_keys(CONF_OPEN_SENSOR, CONF_CLOSE_SENSOR),
validate_infer_endstop,
validate_overshoot,
)
@@ -160,4 +179,7 @@ async def to_code(config: ConfigType) -> None:
var.set_direction_change_waittime(config[CONF_DIRECTION_CHANGE_WAIT_TIME])
)
cg.add(var.set_acceleration_wait_time(config[CONF_ACCELERATION_WAIT_TIME]))
if (overshoot := config[CONF_OVERSHOOT_DURATION]).total_milliseconds:
cg.add_define("USE_FEEDBACK_COVER_OVERSHOOT")
cg.add(var.set_overshoot_duration(overshoot))
cg.add(var.set_obstacle_rollback(config[CONF_OBSTACLE_ROLLBACK]))
+24 -2
View File
@@ -79,6 +79,11 @@ void FeedbackCover::dump_config() {
if (this->acceleration_wait_time_) {
ESP_LOGCONFIG(TAG, " Acceleration wait time: %.1fs", this->acceleration_wait_time_ / 1e3f);
}
#ifdef USE_FEEDBACK_COVER_OVERSHOOT
if (this->overshoot_duration_) {
ESP_LOGCONFIG(TAG, " Overshoot duration: %.1fs", this->overshoot_duration_ / 1e3f);
}
#endif
#ifdef USE_BINARY_SENSOR
if (this->obstacle_rollback_ && (this->open_obstacle_ != nullptr || this->close_obstacle_ != nullptr)) {
ESP_LOGCONFIG(TAG, " Obstacle rollback: %.1f%%", this->obstacle_rollback_ * 100);
@@ -234,10 +239,22 @@ void FeedbackCover::loop() {
// (stoping from endstop sensor is handled in callback)
if (this->current_trigger_operation_ != COVER_OPERATION_IDLE) {
if (this->is_at_target_()) {
if (this->has_built_in_endstop_ &&
(this->target_position_ == COVER_OPEN || this->target_position_ == COVER_CLOSED)) {
const bool at_end = this->target_position_ == COVER_OPEN || this->target_position_ == COVER_CLOSED;
if (this->has_built_in_endstop_ && at_end) {
// Don't trigger stop, let the cover stop by itself.
this->set_current_operation_(COVER_OPERATION_IDLE, true);
#ifdef USE_FEEDBACK_COVER_OVERSHOOT
} else if (at_end && this->overshoot_duration_) {
// The define is global, so a cover without the option still lands here with a zero duration
// Keep driving past the end position for a while so accumulated timing error is squeezed out
if (!this->start_overshoot_time_) {
this->start_overshoot_time_ = now;
} else if (now - this->start_overshoot_time_ > this->overshoot_duration_ ||
now - this->start_dir_time_ > this->max_duration_) {
ESP_LOGD(TAG, "'%s' - Overshoot duration reached. Stopping cover.", this->name_.c_str());
this->start_direction_(COVER_OPERATION_IDLE);
}
#endif
} else {
this->start_direction_(COVER_OPERATION_IDLE);
}
@@ -340,6 +357,11 @@ void FeedbackCover::start_direction_(CoverOperation dir) {
binary_sensor::BinarySensor *obstacle{nullptr};
#endif
#ifdef USE_FEEDBACK_COVER_OVERSHOOT
// Any new command ends a running overshoot
this->start_overshoot_time_ = 0;
#endif
switch (dir) {
case COVER_OPERATION_IDLE:
trig = &this->stop_trigger_;
@@ -33,6 +33,9 @@ class FeedbackCover final : public cover::Cover, public Component {
void set_has_built_in_endstop(bool value) { this->has_built_in_endstop_ = value; }
void set_assumed_state(bool value) { this->assumed_state_ = value; }
void set_max_duration(uint32_t max_duration) { this->max_duration_ = max_duration; }
#ifdef USE_FEEDBACK_COVER_OVERSHOOT
void set_overshoot_duration(uint32_t overshoot_duration) { this->overshoot_duration_ = overshoot_duration; }
#endif
void set_obstacle_rollback(float obstacle_rollback) { this->obstacle_rollback_ = obstacle_rollback; }
void set_update_interval(uint32_t interval) { this->update_interval_ = interval; }
void set_infer_endstop(bool infer_endstop) { this->infer_endstop_ = infer_endstop; }
@@ -67,6 +70,9 @@ class FeedbackCover final : public cover::Cover, public Component {
uint32_t open_duration_{0};
uint32_t close_duration_{0};
uint32_t max_duration_{UINT32_MAX};
#ifdef USE_FEEDBACK_COVER_OVERSHOOT
uint32_t overshoot_duration_{0};
#endif
optional<uint32_t> direction_change_waittime_{};
uint32_t acceleration_wait_time_{0};
bool has_built_in_endstop_{false};
@@ -80,6 +86,9 @@ class FeedbackCover final : public cover::Cover, public Component {
uint32_t last_recompute_time_{0};
uint32_t start_dir_time_{0};
uint32_t last_publish_time_{0};
#ifdef USE_FEEDBACK_COVER_OVERSHOOT
uint32_t start_overshoot_time_{0};
#endif
float target_position_{0};
uint32_t update_interval_{1000};
};
+2 -1
View File
@@ -2,7 +2,8 @@ import logging
import esphome.codegen as cg
from esphome.components import i2c, sensor
from esphome.components.aqi import AQI_CALCULATION_TYPE, CONF_AQI, CONF_CALCULATION_TYPE
from esphome.components.aqi import AQI_CALCULATION_TYPE, CONF_CALCULATION_TYPE
from esphome.components.const import CONF_AQI
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
+3 -10
View File
@@ -4,7 +4,7 @@ from typing import Any
from esphome import automation
import esphome.codegen as cg
from esphome.components import esp32
from esphome.components.const import CONF_REQUEST_HEADERS
from esphome.components.const import CONF_REQUEST_HEADERS, CONF_VERIFY_SSL
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv
from esphome.const import (
@@ -53,7 +53,6 @@ HttpRequestResponseTrigger = http_request_ns.class_(
CONF_HTTP_REQUEST_ID = "http_request_id"
CONF_USERAGENT = "useragent"
CONF_VERIFY_SSL = "verify_ssl"
CONF_FOLLOW_REDIRECTS = "follow_redirects"
CONF_REDIRECT_LIMIT = "redirect_limit"
CONF_BUFFER_SIZE_RX = "buffer_size_rx"
@@ -227,14 +226,8 @@ async def to_code(config: ConfigType) -> None:
# use_full_certificate_bundle: true
esp32.require_certificate_bundle()
esp32.add_idf_sdkconfig_option(
"CONFIG_ESP_TLS_INSECURE",
not config.get(CONF_VERIFY_SSL),
)
esp32.add_idf_sdkconfig_option(
"CONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY",
not config.get(CONF_VERIFY_SSL),
)
else:
esp32.allow_insecure_tls()
if CORE.is_esp8266:
cg.add_library("ESP8266HTTPClient", None)
if CORE.is_rp2 and CORE.using_arduino:
+2 -4
View File
@@ -601,10 +601,8 @@ async def to_code(config: ConfigType) -> None:
pins_struct = _build_pins_struct(pin_expressions, e_pin_num)
hub75_config = _build_config_struct(config, pins_struct, min_refresh)
# Rotation is handled by the hub75 driver (config_.rotation already set above).
# Force rotation to 0 for ESPHome's Display base class to avoid double-rotation.
if CONF_ROTATION in config:
config[CONF_ROTATION] = 0
# The driver rotates the panel itself (config_.rotation above), so the base class must not rotate too
config.pop(CONF_ROTATION, None)
# Create display and register
var = cg.new_Pvariable(config[CONF_ID], hub75_config)
+40
View File
@@ -202,6 +202,46 @@ void HUB75Display::set_brightness(uint8_t brightness) {
}
}
static Hub75Rotation to_hub75_rotation(display::DisplayRotation rotation) {
switch (rotation) {
case display::DisplayRotation::DISPLAY_ROTATION_90_DEGREES:
return Hub75Rotation::ROTATE_90;
case display::DisplayRotation::DISPLAY_ROTATION_180_DEGREES:
return Hub75Rotation::ROTATE_180;
case display::DisplayRotation::DISPLAY_ROTATION_270_DEGREES:
return Hub75Rotation::ROTATE_270;
default:
return Hub75Rotation::ROTATE_0;
}
}
static display::DisplayRotation to_display_rotation(Hub75Rotation rotation) {
switch (rotation) {
case Hub75Rotation::ROTATE_90:
return display::DisplayRotation::DISPLAY_ROTATION_90_DEGREES;
case Hub75Rotation::ROTATE_180:
return display::DisplayRotation::DISPLAY_ROTATION_180_DEGREES;
case Hub75Rotation::ROTATE_270:
return display::DisplayRotation::DISPLAY_ROTATION_270_DEGREES;
default:
return display::DisplayRotation::DISPLAY_ROTATION_0_DEGREES;
}
}
// Before setup() creates the driver the configuration holds the rotation, afterwards the driver does
void HUB75Display::set_rotation(display::DisplayRotation rotation) {
const Hub75Rotation hub75_rotation = to_hub75_rotation(rotation);
if (this->driver_ == nullptr) {
this->config_.rotation = hub75_rotation;
return;
}
this->driver_->set_rotation(hub75_rotation);
}
display::DisplayRotation HUB75Display::get_rotation() const {
return to_display_rotation(this->driver_ == nullptr ? this->config_.rotation : this->driver_->get_rotation());
}
} // namespace esphome::hub75
#endif
+4 -1
View File
@@ -36,6 +36,9 @@ class HUB75Display final : public display::Display {
// Brightness control (runtime mutable)
void set_brightness(uint8_t brightness);
void set_rotation(display::DisplayRotation rotation) override;
display::DisplayRotation get_rotation() const override;
protected:
// Display internal methods
int get_width_internal() override { return this->driver_ != nullptr ? this->driver_->get_width() : 0; }
@@ -43,7 +46,7 @@ class HUB75Display final : public display::Display {
// Member variables
Hub75Driver *driver_{nullptr};
Hub75Config config_; // Immutable configuration
Hub75Config config_; // Fixed after setup(); only the rotation can change before the driver exists
// Runtime state (mutable)
uint8_t brightness_{128};
+10
View File
@@ -4,6 +4,7 @@
#include <utility>
#include <vector>
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
namespace esphome::i2c {
@@ -53,6 +54,15 @@ class I2CBus {
return this->write_readv(address, buffer, len, nullptr, 0);
}
#ifdef I2C_PORT_FREQUENCY_COUNT
/// @brief Switch the bus frequency for the transactions that follow; an external bus
/// that does not override this reports an error and keeps its frequency
virtual ErrorCode switch_frequency(uint32_t frequency) { return ERROR_UNKNOWN; }
/// @brief The frequency the bus runs at
virtual uint32_t get_frequency() const { return 0; }
#endif
protected:
/// @brief Scans the I2C bus for devices. Devices presence is kept in an array of std::pair
/// that contains the address and the corresponding bool presence flag.
+53 -40
View File
@@ -15,7 +15,7 @@ ESPHOME_LOG_TAG(TAG, "i2c");
static constexpr size_t I2C_MAX_LOG_BYTES = 32;
void ArduinoI2CBus::setup() {
recover_();
this->recover_();
#if defined(USE_ESP8266)
wire_ = new TwoWire(); // NOLINT(cppcoreguidelines-owning-memory)
@@ -25,9 +25,9 @@ void ArduinoI2CBus::setup() {
// RP2040 datasheet Table 2 (section 1.4.3): https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf
// RP2350 datasheet Table 7 (section 9.4): https://datasheets.raspberrypi.com/rp2350/rp2350-datasheet.pdf
if ((this->sda_pin_ / 2) % 2 == 0) {
wire_ = &Wire;
this->wire_ = &Wire;
} else {
wire_ = &Wire1;
this->wire_ = &Wire1;
}
#endif
@@ -42,22 +42,22 @@ void ArduinoI2CBus::setup() {
void ArduinoI2CBus::set_pins_and_clock_() {
#ifdef USE_RP2
wire_->setSDA(this->sda_pin_);
wire_->setSCL(this->scl_pin_);
wire_->begin();
this->wire_->setSDA(this->sda_pin_);
this->wire_->setSCL(this->scl_pin_);
this->wire_->begin();
#else
wire_->begin(static_cast<int>(sda_pin_), static_cast<int>(scl_pin_));
this->wire_->begin(static_cast<int>(this->sda_pin_), static_cast<int>(this->scl_pin_));
#endif
if (timeout_ > 0) { // if timeout specified in yaml
if (this->timeout_ > 0) { // if timeout specified in yaml
#if defined(USE_ESP8266)
// https://github.com/esp8266/Arduino/blob/master/libraries/Wire/Wire.h
wire_->setClockStretchLimit(timeout_); // unit: us
this->wire_->setClockStretchLimit(this->timeout_); // unit: us
#elif defined(USE_RP2)
// https://github.com/earlephilhower/ArduinoCore-API/blob/e37df85425e0ac020bfad226d927f9b00d2e0fb7/api/Stream.h
wire_->setTimeout(timeout_ / 1000); // unit: ms
this->wire_->setTimeout(this->timeout_ / 1000); // unit: ms
#endif
}
wire_->setClock(frequency_);
this->wire_->setClock(this->frequency_);
}
void ArduinoI2CBus::dump_config() {
@@ -67,7 +67,7 @@ void ArduinoI2CBus::dump_config() {
" SCL Pin: GPIO%u\n"
" Frequency: %u Hz",
this->sda_pin_, this->scl_pin_, this->frequency_);
if (timeout_ > 0) {
if (this->timeout_ > 0) {
#if defined(USE_ESP8266)
ESP_LOGCONFIG(TAG, " Timeout: %u us", this->timeout_);
#elif defined(USE_RP2)
@@ -87,10 +87,10 @@ void ArduinoI2CBus::dump_config() {
}
if (this->scan_) {
ESP_LOGI(TAG, "Results from bus scan:");
if (scan_results_.empty()) {
if (this->scan_results_.empty()) {
ESP_LOGI(TAG, "Found no devices");
} else {
for (const auto &s : scan_results_) {
for (const auto &s : this->scan_results_) {
if (s.second) {
ESP_LOGI(TAG, "Found device at address 0x%02X", s.first);
} else {
@@ -106,7 +106,7 @@ ErrorCode ArduinoI2CBus::write_readv(uint8_t address, const uint8_t *write_buffe
#if defined(USE_ESP8266)
this->set_pins_and_clock_(); // reconfigure Wire global state in case there are multiple instances
#endif
if (!initialized_) {
if (!this->initialized_) {
ESP_LOGD(TAG, "i2c bus not initialized!");
return ERROR_NOT_INITIALIZED;
}
@@ -118,22 +118,22 @@ ErrorCode ArduinoI2CBus::write_readv(uint8_t address, const uint8_t *write_buffe
uint8_t status = 0;
if (write_count != 0 || read_count == 0) {
wire_->beginTransmission(address);
size_t ret = wire_->write(write_buffer, write_count);
this->wire_->beginTransmission(address);
size_t ret = this->wire_->write(write_buffer, write_count);
if (ret != write_count) {
ESP_LOGV(TAG, "TX failed");
return ERROR_UNKNOWN;
}
status = wire_->endTransmission(read_count == 0);
status = this->wire_->endTransmission(read_count == 0);
}
if (status == 0 && read_count != 0) {
size_t ret2 = wire_->requestFrom(address, read_count, true);
size_t ret2 = this->wire_->requestFrom(address, read_count, true);
if (ret2 != read_count) {
ESP_LOGVV(TAG, "RX %u from %02X failed with error %u", read_count, address, ret2);
return ERROR_TIMEOUT;
}
for (size_t j = 0; j != read_count; j++)
read_buffer[j] = wire_->read();
read_buffer[j] = this->wire_->read();
}
// Avoid switch to prevent compiler-generated lookup table in RAM on ESP8266
if (status == 0)
@@ -154,6 +154,19 @@ ErrorCode ArduinoI2CBus::write_readv(uint8_t address, const uint8_t *write_buffe
return ERROR_UNKNOWN;
}
#ifdef I2C_PORT_FREQUENCY_COUNT
ErrorCode ArduinoI2CBus::switch_frequency(uint32_t frequency) {
if (this->frequency_ == frequency) {
return ERROR_OK;
}
this->frequency_ = frequency;
if (this->initialized_) {
this->wire_->setClock(this->frequency_);
}
return ERROR_OK;
}
#endif
/// Perform I2C bus recovery, see:
/// https://www.nxp.com/docs/en/user-guide/UM10204.pdf
/// https://www.analog.com/media/en/technical-documentation/application-notes/54305147357414AN686_0.pdf
@@ -167,15 +180,15 @@ void ArduinoI2CBus::recover_() {
const auto half_period_usec = 1000000 / 100000 / 2;
// Activate input and pull up resistor for the SCL pin.
pinMode(scl_pin_, INPUT_PULLUP); // NOLINT
pinMode(this->scl_pin_, INPUT_PULLUP); // NOLINT
// This should make the signal on the line HIGH. If SCL is pulled low
// on the I2C bus however, then some device is interfering with the SCL
// line. In that case, the I2C bus cannot be recovered.
delayMicroseconds(half_period_usec);
if (digitalRead(scl_pin_) == LOW) { // NOLINT
if (digitalRead(this->scl_pin_) == LOW) { // NOLINT
ESP_LOGE(TAG, "Recovery failed: SCL is held LOW on the bus");
recovery_result_ = RECOVERY_FAILED_SCL_LOW;
this->recovery_result_ = RECOVERY_FAILED_SCL_LOW;
return;
}
@@ -189,18 +202,18 @@ void ArduinoI2CBus::recover_() {
// Make sure that switching to output mode will make SCL low, just in
// case other code has setup the pin for a HIGH signal.
digitalWrite(scl_pin_, LOW); // NOLINT
digitalWrite(this->scl_pin_, LOW); // NOLINT
delayMicroseconds(half_period_usec);
for (auto i = 0; i < 9; i++) {
// Release pull up resistor and switch to output to make the signal LOW.
pinMode(scl_pin_, INPUT); // NOLINT
pinMode(scl_pin_, OUTPUT); // NOLINT
pinMode(this->scl_pin_, INPUT); // NOLINT
pinMode(this->scl_pin_, OUTPUT); // NOLINT
delayMicroseconds(half_period_usec);
// Release output and activate pull up resistor to make the signal HIGH.
pinMode(scl_pin_, INPUT); // NOLINT
pinMode(scl_pin_, INPUT_PULLUP); // NOLINT
pinMode(this->scl_pin_, INPUT); // NOLINT
pinMode(this->scl_pin_, INPUT_PULLUP); // NOLINT
delayMicroseconds(half_period_usec);
// When SCL is kept LOW at this point, we might be looking at a device
@@ -211,28 +224,28 @@ void ArduinoI2CBus::recover_() {
// should recover in a few ms or less else not likely to recovery at
// all.
auto wait = 250;
while (wait-- && digitalRead(scl_pin_) == LOW) { // NOLINT
while (wait-- && digitalRead(this->scl_pin_) == LOW) { // NOLINT
App.feed_wdt();
delayMicroseconds(half_period_usec * 2);
}
if (digitalRead(scl_pin_) == LOW) { // NOLINT
if (digitalRead(this->scl_pin_) == LOW) { // NOLINT
ESP_LOGE(TAG, "Recovery failed: SCL is held LOW during clock pulse cycle");
recovery_result_ = RECOVERY_FAILED_SCL_LOW;
this->recovery_result_ = RECOVERY_FAILED_SCL_LOW;
return;
}
}
// Activate input and pull resistor for the SDA pin, so we can verify
// that SDA is pulled HIGH in the following step.
pinMode(sda_pin_, INPUT_PULLUP); // NOLINT
digitalWrite(sda_pin_, LOW); // NOLINT
pinMode(this->sda_pin_, INPUT_PULLUP); // NOLINT
digitalWrite(this->sda_pin_, LOW); // NOLINT
// By now, any stuck device ought to have sent all remaining bits of its
// transaction, meaning that it should have freed up the SDA line, resulting
// in SDA being pulled up.
if (digitalRead(sda_pin_) == LOW) { // NOLINT
if (digitalRead(this->sda_pin_) == LOW) { // NOLINT
ESP_LOGE(TAG, "Recovery failed: SDA is held LOW after clock pulse cycle");
recovery_result_ = RECOVERY_FAILED_SDA_LOW;
this->recovery_result_ = RECOVERY_FAILED_SDA_LOW;
return;
}
@@ -248,8 +261,8 @@ void ArduinoI2CBus::recover_() {
// SCL and SDA are already high at this point, so we can generate a START
// condition by making the SDA signal LOW.
delayMicroseconds(half_period_usec);
pinMode(sda_pin_, INPUT); // NOLINT
pinMode(sda_pin_, OUTPUT); // NOLINT
pinMode(this->sda_pin_, INPUT); // NOLINT
pinMode(this->sda_pin_, OUTPUT); // NOLINT
// From the specification:
// "A START condition immediately followed by a STOP condition (void
@@ -258,10 +271,10 @@ void ArduinoI2CBus::recover_() {
// Finally, we'll bring the I2C bus into a starting state by generating
// a STOP condition.
delayMicroseconds(half_period_usec);
pinMode(sda_pin_, INPUT); // NOLINT
pinMode(sda_pin_, INPUT_PULLUP); // NOLINT
pinMode(this->sda_pin_, INPUT); // NOLINT
pinMode(this->sda_pin_, INPUT_PULLUP); // NOLINT
recovery_result_ = RECOVERY_COMPLETED;
this->recovery_result_ = RECOVERY_COMPLETED;
}
} // namespace esphome::i2c
+9 -5
View File
@@ -22,13 +22,17 @@ class ArduinoI2CBus final : public InternalI2CBus, public Component {
size_t read_count) override;
float get_setup_priority() const override { return setup_priority::BUS; }
void set_scan(bool scan) { scan_ = scan; }
void set_sda_pin(uint8_t sda_pin) { sda_pin_ = sda_pin; }
void set_scl_pin(uint8_t scl_pin) { scl_pin_ = scl_pin; }
void set_frequency(uint32_t frequency) { frequency_ = frequency; }
void set_timeout(uint32_t timeout) { timeout_ = timeout; }
void set_scan(bool scan) { this->scan_ = scan; }
void set_sda_pin(uint8_t sda_pin) { this->sda_pin_ = sda_pin; }
void set_scl_pin(uint8_t scl_pin) { this->scl_pin_ = scl_pin; }
void set_timeout(uint32_t timeout) { this->timeout_ = timeout; }
int get_port() const override { return 0; }
void set_frequency(uint32_t frequency) { this->frequency_ = frequency; }
#ifdef I2C_PORT_FREQUENCY_COUNT
ErrorCode switch_frequency(uint32_t frequency) override;
uint32_t get_frequency() const override { return this->frequency_; }
#endif
private:
void recover_();
+72 -24
View File
@@ -32,8 +32,8 @@ void IDFI2CBus::setup() {
i2c_master_bus_config_t bus_conf{};
memset(&bus_conf, 0, sizeof(bus_conf));
bus_conf.sda_io_num = gpio_num_t(sda_pin_);
bus_conf.scl_io_num = gpio_num_t(scl_pin_);
bus_conf.sda_io_num = gpio_num_t(this->sda_pin_);
bus_conf.scl_io_num = gpio_num_t(this->scl_pin_);
bus_conf.glitch_ignore_cnt = 7;
#if SOC_LP_I2C_SUPPORTED
if (this->lp_mode_) {
@@ -59,7 +59,7 @@ void IDFI2CBus::setup() {
}
#endif
bus_conf.i2c_port = this->port_;
bus_conf.flags.enable_internal_pullup = sda_pullup_enabled_ || scl_pullup_enabled_;
bus_conf.flags.enable_internal_pullup = this->sda_pullup_enabled_ || this->scl_pullup_enabled_;
esp_err_t err = i2c_new_master_bus(&bus_conf, &this->bus_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "i2c_new_master_bus failed: %s", esp_err_to_name(err));
@@ -67,18 +67,21 @@ void IDFI2CBus::setup() {
return;
}
i2c_device_config_t dev_conf{};
memset(&dev_conf, 0, sizeof(dev_conf));
dev_conf.dev_addr_length = I2C_ADDR_BIT_LEN_7;
dev_conf.device_address = I2C_DEVICE_ADDRESS_NOT_USED;
dev_conf.scl_speed_hz = this->frequency_;
dev_conf.scl_wait_us = this->timeout_;
err = i2c_master_bus_add_device(this->bus_, &dev_conf, &this->dev_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "i2c_master_bus_add_device failed: %s", esp_err_to_name(err));
this->dev_ = this->add_device_(this->frequency_);
if (this->dev_ == nullptr) {
this->mark_failed();
return;
}
#ifdef I2C_PORT_FREQUENCY_COUNT
this->add_frequency(this->frequency_);
for (auto &device : this->devices_) {
device.dev = device.frequency == this->frequency_ ? this->dev_ : this->add_device_(device.frequency);
if (device.dev == nullptr) {
this->mark_failed();
return;
}
}
#endif
this->initialized_ = true;
@@ -88,6 +91,21 @@ void IDFI2CBus::setup() {
}
}
i2c_master_dev_handle_t IDFI2CBus::add_device_(uint32_t frequency) {
i2c_device_config_t dev_conf{};
dev_conf.dev_addr_length = I2C_ADDR_BIT_LEN_7;
dev_conf.device_address = I2C_DEVICE_ADDRESS_NOT_USED;
dev_conf.scl_speed_hz = frequency;
dev_conf.scl_wait_us = this->timeout_;
i2c_master_dev_handle_t dev{};
const esp_err_t err = i2c_master_bus_add_device(this->bus_, &dev_conf, &dev);
if (err != ESP_OK) {
ESP_LOGW(TAG, "i2c_master_bus_add_device failed: %s", esp_err_to_name(err));
return nullptr;
}
return dev;
}
void IDFI2CBus::dump_config() {
ESP_LOGCONFIG(TAG, "I2C Bus:");
ESP_LOGCONFIG(TAG,
@@ -95,7 +113,7 @@ void IDFI2CBus::dump_config() {
" SCL Pin: GPIO%u\n"
" Frequency: %" PRIu32 " Hz",
this->sda_pin_, this->scl_pin_, this->frequency_);
if (timeout_ > 0) {
if (this->timeout_ > 0) {
ESP_LOGCONFIG(TAG, " Timeout: %" PRIu32 "us", this->timeout_);
}
switch (this->recovery_result_) {
@@ -111,10 +129,10 @@ void IDFI2CBus::dump_config() {
}
if (this->scan_) {
ESP_LOGCONFIG(TAG, "Results from bus scan:");
if (scan_results_.empty()) {
if (this->scan_results_.empty()) {
ESP_LOGCONFIG(TAG, "Found no devices");
} else {
for (const auto &s : scan_results_) {
for (const auto &s : this->scan_results_) {
if (s.second) {
ESP_LOGCONFIG(TAG, "Found device at address 0x%02X", s.first);
} else {
@@ -129,7 +147,7 @@ ErrorCode IDFI2CBus::write_readv(uint8_t address, const uint8_t *write_buffer, s
size_t read_count) {
// logging is only enabled with v level, if warnings are shown the caller
// should log them
if (!initialized_) {
if (!this->initialized_) {
ESP_LOGW(TAG, "i2c bus not initialized!");
return ERROR_NOT_INITIALIZED;
}
@@ -198,14 +216,44 @@ ErrorCode IDFI2CBus::write_readv(uint8_t address, const uint8_t *write_buffer, s
return ERROR_OK;
}
#ifdef I2C_PORT_FREQUENCY_COUNT
void IDFI2CBus::add_frequency(uint32_t frequency) {
for (const auto &device : this->devices_) {
if (device.frequency == frequency) {
return;
}
}
this->devices_.push_back({frequency, nullptr});
}
ErrorCode IDFI2CBus::switch_frequency(uint32_t frequency) {
if (this->frequency_ == frequency) {
return ERROR_OK;
}
if (!this->initialized_) {
this->frequency_ = frequency;
return ERROR_OK;
}
// Every handle exists since setup(); a frequency nobody registered has none
for (const auto &device : this->devices_) {
if (device.frequency == frequency) {
this->dev_ = device.dev;
this->frequency_ = frequency;
return ERROR_OK;
}
}
return ERROR_UNKNOWN;
}
#endif
/// Perform I2C bus recovery, see:
/// https://www.nxp.com/docs/en/user-guide/UM10204.pdf
/// https://www.analog.com/media/en/technical-documentation/application-notes/54305147357414AN686_0.pdf
void IDFI2CBus::recover_() {
ESP_LOGI(TAG, "Performing bus recovery");
const auto scl_pin = static_cast<gpio_num_t>(scl_pin_);
const auto sda_pin = static_cast<gpio_num_t>(sda_pin_);
const auto scl_pin = static_cast<gpio_num_t>(this->scl_pin_);
const auto sda_pin = static_cast<gpio_num_t>(this->sda_pin_);
// For the upcoming operations, target for a 60kHz toggle frequency.
// 1000kHz is the maximum frequency for I2C running in standard-mode,
@@ -218,7 +266,7 @@ void IDFI2CBus::recover_() {
// Configure SCL pin for open drain input/output, with a pull up resistor.
gpio_set_level(scl_pin, 1);
gpio_config_t scl_config{};
scl_config.pin_bit_mask = 1ULL << scl_pin_;
scl_config.pin_bit_mask = 1ULL << this->scl_pin_;
scl_config.mode = GPIO_MODE_INPUT_OUTPUT_OD;
scl_config.pull_up_en = GPIO_PULLUP_ENABLE;
scl_config.pull_down_en = GPIO_PULLDOWN_DISABLE;
@@ -228,7 +276,7 @@ void IDFI2CBus::recover_() {
// Configure SDA pin for open drain input/output, with a pull up resistor.
gpio_set_level(sda_pin, 1);
gpio_config_t sda_conf{};
sda_conf.pin_bit_mask = 1ULL << sda_pin_;
sda_conf.pin_bit_mask = 1ULL << this->sda_pin_;
sda_conf.mode = GPIO_MODE_INPUT_OUTPUT_OD;
sda_conf.pull_up_en = GPIO_PULLUP_ENABLE;
sda_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
@@ -240,7 +288,7 @@ void IDFI2CBus::recover_() {
delayMicroseconds(half_period_usec);
if (gpio_get_level(scl_pin) == 0) {
ESP_LOGE(TAG, "Recovery failed: SCL is held LOW on the bus");
recovery_result_ = RECOVERY_FAILED_SCL_LOW;
this->recovery_result_ = RECOVERY_FAILED_SCL_LOW;
return;
}
@@ -271,7 +319,7 @@ void IDFI2CBus::recover_() {
}
if (gpio_get_level(scl_pin) == 0) {
ESP_LOGE(TAG, "Recovery failed: SCL is held LOW during clock pulse cycle");
recovery_result_ = RECOVERY_FAILED_SCL_LOW;
this->recovery_result_ = RECOVERY_FAILED_SCL_LOW;
return;
}
}
@@ -281,7 +329,7 @@ void IDFI2CBus::recover_() {
// in SDA being pulled up.
if (gpio_get_level(sda_pin) == 0) {
ESP_LOGE(TAG, "Recovery failed: SDA is held LOW after clock pulse cycle");
recovery_result_ = RECOVERY_FAILED_SDA_LOW;
this->recovery_result_ = RECOVERY_FAILED_SDA_LOW;
return;
}
@@ -308,7 +356,7 @@ void IDFI2CBus::recover_() {
delayMicroseconds(half_period_usec);
gpio_set_level(sda_pin, 1);
recovery_result_ = RECOVERY_COMPLETED;
this->recovery_result_ = RECOVERY_COMPLETED;
}
} // namespace esphome::i2c
+15
View File
@@ -34,13 +34,28 @@ class IDFI2CBus final : public InternalI2CBus, public Component {
#endif
int get_port() const override { return this->port_; }
#ifdef I2C_PORT_FREQUENCY_COUNT
/// Register a frequency used on the bus so setup() creates its device handle
void add_frequency(uint32_t frequency);
ErrorCode switch_frequency(uint32_t frequency) override;
uint32_t get_frequency() const override { return this->frequency_; }
#endif
private:
void recover_();
i2c_master_dev_handle_t add_device_(uint32_t frequency);
RecoveryCode recovery_result_{};
protected:
i2c_master_dev_handle_t dev_{};
#ifdef I2C_PORT_FREQUENCY_COUNT
// One handle per frequency used on the bus, registered at code generation
struct FrequencyDevice {
uint32_t frequency;
i2c_master_dev_handle_t dev;
};
StaticVector<FrequencyDevice, I2C_PORT_FREQUENCY_COUNT + 1> devices_;
#endif
i2c_master_bus_handle_t bus_{};
i2c_port_t port_{};
uint8_t sda_pin_{};
+5
View File
@@ -21,7 +21,12 @@ class HostI2CBus final : public I2CBus, public Component {
void set_device(const std::string &device) { this->device_ = device; }
void set_scan(bool scan) { this->scan_ = scan; }
void set_frequency(uint32_t frequency) { this->frequency_ = frequency; }
#ifdef I2C_PORT_FREQUENCY_COUNT
// The kernel driver owns the clock, so switch_frequency keeps the base default
uint32_t get_frequency() const override { return this->frequency_; }
#endif
const std::string &get_device() const { return this->device_; }
+26 -7
View File
@@ -120,15 +120,34 @@ ErrorCode ZephyrI2CBus::write_readv(uint8_t address, const uint8_t *write_buffer
return ERROR_OK;
}
void ZephyrI2CBus::set_frequency(uint32_t frequency) {
this->dev_config_ &= ~I2C_SPEED_MASK;
if (frequency >= 400000) {
this->dev_config_ |= I2C_SPEED_SET(I2C_SPEED_FAST);
} else {
this->dev_config_ |= I2C_SPEED_SET(I2C_SPEED_STANDARD);
}
static uint32_t speed_config(uint32_t dev_config, uint32_t frequency) {
const uint32_t speed = frequency >= 400000 ? I2C_SPEED_FAST : I2C_SPEED_STANDARD;
return (dev_config & ~I2C_SPEED_MASK) | I2C_SPEED_SET(speed);
}
void ZephyrI2CBus::set_frequency(uint32_t frequency) {
#ifdef I2C_PORT_FREQUENCY_COUNT
this->frequency_ = frequency;
#endif
this->dev_config_ = speed_config(this->dev_config_, frequency);
}
#ifdef I2C_PORT_FREQUENCY_COUNT
ErrorCode ZephyrI2CBus::switch_frequency(uint32_t frequency) {
if (this->frequency_ == frequency) {
return ERROR_OK;
}
const uint32_t dev_config = speed_config(this->dev_config_, frequency);
// Until ready, setup() applies dev_config_
if (this->is_ready() && i2c_configure(this->i2c_dev_, dev_config) < 0) {
return ERROR_UNKNOWN;
}
this->dev_config_ = dev_config;
this->frequency_ = frequency;
return ERROR_OK;
}
#endif
} // namespace esphome::i2c
#endif
+7
View File
@@ -22,6 +22,10 @@ class ZephyrI2CBus final : public InternalI2CBus, public Component {
void set_sda_pin(uint8_t sda_pin) { this->sda_pin_ = sda_pin; }
void set_scl_pin(uint8_t scl_pin) { this->scl_pin_ = scl_pin; }
void set_frequency(uint32_t frequency);
#ifdef I2C_PORT_FREQUENCY_COUNT
ErrorCode switch_frequency(uint32_t frequency) override;
uint32_t get_frequency() const override { return this->frequency_; }
#endif
int get_port() const override { return 0; }
@@ -31,6 +35,9 @@ class ZephyrI2CBus final : public InternalI2CBus, public Component {
uint8_t sda_pin_{};
uint8_t scl_pin_{};
uint32_t dev_config_{};
#ifdef I2C_PORT_FREQUENCY_COUNT
uint32_t frequency_{};
#endif
};
} // namespace esphome::i2c
+1 -2
View File
@@ -2,6 +2,7 @@ from typing import Any
import esphome.codegen as cg
from esphome.components import i2c, sensor
from esphome.components.const import CONF_ADC_AVERAGING, CONF_ADC_TIME
import esphome.config_validation as cv
from esphome.const import (
CONF_BUS_VOLTAGE,
@@ -24,8 +25,6 @@ from esphome.types import ConfigType
DEPENDENCIES = ["i2c"]
CONF_ADC_AVERAGING = "adc_averaging"
CONF_ADC_TIME = "adc_time"
ina226_ns = cg.esphome_ns.namespace("ina226")
INA226Component = ina226_ns.class_(
+1 -3
View File
@@ -2,7 +2,7 @@ from typing import Any
import esphome.codegen as cg
from esphome.components import sensor
from esphome.components.const import UNIT_AMPERE_HOUR
from esphome.components.const import CONF_ADC_AVERAGING, CONF_ADC_TIME, UNIT_AMPERE_HOUR
import esphome.config_validation as cv
from esphome.const import (
CONF_BUS_VOLTAGE,
@@ -36,9 +36,7 @@ from esphome.types import ConfigType
CODEOWNERS = ["@latonita"]
DOMAIN = "ina2xx_base"
CONF_ADC_AVERAGING = "adc_averaging"
CONF_ADC_RANGE = "adc_range"
CONF_ADC_TIME = "adc_time"
CONF_CHARGE = "charge"
CONF_CHARGE_COULOMBS = "charge_coulombs"
CONF_ENERGY_JOULES = "energy_joules"
+2 -2
View File
@@ -145,8 +145,8 @@ IT8951Model(
reset_pin=12,
cs_pin=10,
# Board power-enable rails: 1.8V logic supply (GPIO21) and the EPD supply
# (GPIO11). Driven high during setup so no separate power_supply is needed.
enable_pin=[21, 11],
# (GPIO11). Driven high during setup so no separate power_supply is needed, and held low during deep_sleep.
enable_pin=[{"number": 21, "hold_state": True}, {"number": 11, "hold_state": True}],
vcom=1400,
# reTerminal E1003 panel firmware only accepts the 0x0002 VCOM SET
# selector; using the default 0x0001 leaves VCOM unchanged and breaks
+6
View File
@@ -351,6 +351,12 @@ void IT8951Display::on_safe_shutdown() {
this->spi_cmd_(TCON_SLEEP);
}
void IT8951Display::on_powerdown() {
for (auto *pin : this->enable_pins_) {
pin->digital_write(false);
}
}
// --- Init op enqueuers -------------------------------------------------------
void IT8951Display::enqueue_init_reset_() {
+1
View File
@@ -150,6 +150,7 @@ class IT8951Display : public Display,
void loop() override;
void dump_config() override;
void on_safe_shutdown() override;
void on_powerdown() override;
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
// --- Config setters (called from generated code) ---
+88 -29
View File
@@ -10,6 +10,9 @@
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include <algorithm>
#include <cmath>
namespace esphome::ld2412 {
ESPHOME_LOG_TAG(TAG, "ld2412");
@@ -73,8 +76,10 @@ enum PeriodicDataValue : uint8_t {
};
enum AckData : uint8_t {
FRAME_LENGTH = 4, // little-endian count of the bytes between it and the footer
COMMAND = 6,
COMMAND_STATUS = 7,
ACK_PAYLOAD = 10,
};
// Memory-efficient lookup tables
@@ -191,6 +196,9 @@ static constexpr uint8_t DATA_FRAME_HEADER[HEADER_FOOTER_SIZE] = {0xF4, 0xF3, 0x
static constexpr uint8_t DATA_FRAME_FOOTER[HEADER_FOOTER_SIZE] = {0xF8, 0xF7, 0xF6, 0xF5};
// MAC address the module uses when Bluetooth is disabled
static constexpr uint8_t NO_MAC[] = {0x08, 0x05, 0x04, 0x03, 0x02, 0x01};
static constexpr uint8_t GATE_SENS_ACK_SIZE = ACK_PAYLOAD + TOTAL_GATES + HEADER_FOOTER_SIZE;
// The length field counts the command word, the status word and the gates
static constexpr uint16_t GATE_SENS_ACK_LENGTH = GATE_SENS_ACK_SIZE - COMMAND - HEADER_FOOTER_SIZE;
static inline bool validate_header_footer(const uint8_t *header_footer, const uint8_t *buffer) {
return std::memcmp(header_footer, buffer, HEADER_FOOTER_SIZE) == 0;
@@ -240,10 +248,10 @@ void LD2412Component::dump_config() {
LOG_NUMBER(" ", "MaxDistanceGate", this->max_distance_gate_number_);
LOG_NUMBER(" ", "MinDistanceGate", this->min_distance_gate_number_);
LOG_NUMBER(" ", "Timeout", this->timeout_number_);
for (number::Number *n : this->gate_move_threshold_numbers_) {
for (number::Number *n : this->gate_move_thresholds_.numbers) {
LOG_NUMBER(" ", "Move Thresholds", n);
}
for (number::Number *n : this->gate_still_threshold_numbers_) {
for (number::Number *n : this->gate_still_thresholds_.numbers) {
LOG_NUMBER(" ", "Still Thresholds", n);
}
#endif
@@ -460,6 +468,13 @@ void set_number_value(number::Number *n, float value) {
n->publish_state(value);
}
}
// NaN marks a value the module never received
static inline bool has_value(number::Number *n) { return n != nullptr && n->has_state() && !std::isnan(n->state); }
// Timeout ids 0 (still) and 1 (motion) for a write waiting on a re-query
static constexpr uint32_t GATE_QUERY_TIMEOUT_ID = 0;
static constexpr uint32_t GATE_QUERY_TIMEOUT_MS = 2000;
#endif
bool LD2412Component::handle_ack_data_() {
@@ -595,19 +610,34 @@ bool LD2412Component::handle_ack_data_() {
ESP_LOGV(TAG, "Handled set light control command");
break;
case CMD_QUERY_MOTION_GATE_SENS: {
#ifdef USE_NUMBER
for (size_t i = 0; i < this->gate_move_threshold_numbers_.size() && (10 + i) < this->buffer_pos_; i++) {
set_number_value(this->gate_move_threshold_numbers_[i], this->buffer_data_[10 + i]);
}
#endif
break;
}
case CMD_QUERY_MOTION_GATE_SENS:
case CMD_QUERY_STATIC_GATE_SENS: {
#ifdef USE_NUMBER
for (size_t i = 0; i < this->gate_still_threshold_numbers_.size() && (10 + i) < this->buffer_pos_; i++) {
set_number_value(this->gate_still_threshold_numbers_[i], this->buffer_data_[10 + i]);
const uint16_t declared = encode_uint16(this->buffer_data_[FRAME_LENGTH + 1], this->buffer_data_[FRAME_LENGTH]);
if (declared != GATE_SENS_ACK_LENGTH || this->buffer_pos_ > GATE_SENS_ACK_SIZE) {
// A truncated answer merged with the next frame, or a length this driver does not know
ESP_LOGW(TAG, "Dropping gate threshold answer of %u bytes", this->buffer_pos_);
return true;
}
if (this->buffer_pos_ < GATE_SENS_ACK_SIZE) {
return false; // the footer pattern appeared inside the payload, keep reading
}
const bool motion = this->buffer_data_[COMMAND] == CMD_QUERY_MOTION_GATE_SENS;
GateThresholds &group = this->gate_thresholds_(motion);
// True only while a held-back write waits for this answer
const bool pending = this->cancel_timeout(GATE_QUERY_TIMEOUT_ID + motion);
for (size_t i = 0; i < TOTAL_GATES; i++) {
group.last_read[i] = this->buffer_data_[ACK_PAYLOAD + i];
// The pending write's user values must survive the answer
if (!pending || !has_value(group.numbers[i])) {
set_number_value(group.numbers[i], group.last_read[i]);
}
}
group.read = true;
if (pending) {
this->set_config_mode_(true);
this->send_gate_thresholds_(motion);
this->set_config_mode_(false);
}
#endif
break;
@@ -747,6 +777,11 @@ void LD2412Component::set_engineering_mode(bool enable) {
void LD2412Component::factory_reset() {
this->set_config_mode_(true);
this->send_command_(CMD_FACTORY_RESET, nullptr, 0);
#ifdef USE_NUMBER
// Reset discards the thresholds; the next write must wait for the re-read
this->gate_move_thresholds_.read = false;
this->gate_still_thresholds_.read = false;
#endif
this->set_timeout(2000, [this]() { this->restart_and_read_all_info(); });
}
@@ -805,24 +840,48 @@ void LD2412Component::set_basic_config() {
}
#ifdef USE_NUMBER
void LD2412Component::set_gate_threshold() {
if (this->gate_move_threshold_numbers_.empty() && this->gate_still_threshold_numbers_.empty()) {
return; // No gate threshold numbers set; nothing to do here
void LD2412Component::send_gate_thresholds_(bool motion) {
GateThresholds &group = this->gate_thresholds_(motion);
const uint8_t command = motion ? CMD_MOTION_GATE_SENS : CMD_STATIC_GATE_SENS;
uint8_t value[TOTAL_GATES];
uint8_t configured = 0;
for (uint8_t i = 0; i < TOTAL_GATES; i++) {
number::Number *n = group.numbers[i];
if (has_value(n)) {
value[i] = static_cast<uint8_t>(n->state);
configured++;
} else {
value[i] = group.last_read[i];
}
}
uint8_t value[TOTAL_GATES] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
if (configured < TOTAL_GATES && !group.read) {
// Unread gates would go out as zero, which is maximum sensitivity
ESP_LOGW(TAG, "Command %02X held back until the module reports its gate thresholds", command);
this->send_command_(motion ? CMD_QUERY_MOTION_GATE_SENS : CMD_QUERY_STATIC_GATE_SENS, nullptr, 0);
this->set_timeout(GATE_QUERY_TIMEOUT_ID + motion, GATE_QUERY_TIMEOUT_MS,
[this, motion]() { this->gate_query_timeout_(motion); });
return;
}
this->send_command_(command, value, sizeof(value));
}
void LD2412Component::gate_query_timeout_(bool motion) {
ESP_LOGE(TAG, "Command %02X not sent, the module did not report its gate thresholds",
motion ? CMD_MOTION_GATE_SENS : CMD_STATIC_GATE_SENS);
// The values on hand never reached the module or are stale, so forgetting them loses nothing
for (number::Number *n : this->gate_thresholds_(motion).numbers) {
if (n != nullptr) {
n->publish_state(NAN);
}
}
}
void LD2412Component::set_gate_threshold(number::Number *n) {
// Found by pointer: 14 compares per write beat a flag per number
const auto &motion_numbers = this->gate_move_thresholds_.numbers;
const bool motion = std::find(motion_numbers.begin(), motion_numbers.end(), n) != motion_numbers.end();
this->set_config_mode_(true);
if (!this->gate_move_threshold_numbers_.empty()) {
for (size_t i = 0; i < this->gate_move_threshold_numbers_.size(); i++) {
value[i] = lowbyte(static_cast<int>(this->gate_move_threshold_numbers_[i]->state));
}
this->send_command_(CMD_MOTION_GATE_SENS, value, sizeof(value));
}
if (!this->gate_still_threshold_numbers_.empty()) {
for (size_t i = 0; i < this->gate_still_threshold_numbers_.size(); i++) {
value[i] = lowbyte(static_cast<int>(this->gate_still_threshold_numbers_[i]->state));
}
this->send_command_(CMD_STATIC_GATE_SENS, value, sizeof(value));
}
this->send_gate_thresholds_(motion);
this->set_config_mode_(false);
}
+21 -6
View File
@@ -85,11 +85,13 @@ class LD2412Component final : public Component, public uart::UARTDevice {
void set_light_out_control();
void set_basic_config();
#ifdef USE_NUMBER
void set_gate_move_threshold_number(uint8_t gate, number::Number *n) { this->gate_move_threshold_numbers_[gate] = n; }
void set_gate_still_threshold_number(uint8_t gate, number::Number *n) {
this->gate_still_threshold_numbers_[gate] = n;
void set_gate_move_threshold_number(uint8_t gate, number::Number *n) {
this->gate_move_thresholds_.numbers[gate] = n;
}
void set_gate_threshold();
void set_gate_still_threshold_number(uint8_t gate, number::Number *n) {
this->gate_still_thresholds_.numbers[gate] = n;
}
void set_gate_threshold(number::Number *n);
void get_gate_threshold();
#endif
#ifdef USE_SENSOR
@@ -118,6 +120,19 @@ class LD2412Component final : public Component, public uart::UARTDevice {
void query_light_control_();
void restart_();
void query_dynamic_background_correction_();
#ifdef USE_NUMBER
// One command carries all 14 gates, so unconfigured gates get the module's last value
struct GateThresholds {
std::array<number::Number *, TOTAL_GATES> numbers{};
std::array<uint8_t, TOTAL_GATES> last_read{};
bool read{false}; // answered since boot or the last factory reset
};
GateThresholds &gate_thresholds_(bool motion) {
return motion ? this->gate_move_thresholds_ : this->gate_still_thresholds_;
}
void send_gate_thresholds_(bool motion);
void gate_query_timeout_(bool motion);
#endif
uint8_t light_function_ = 0;
uint8_t light_threshold_ = 0;
@@ -129,8 +144,8 @@ class LD2412Component final : public Component, public uart::UARTDevice {
bool bluetooth_on_{false};
bool dynamic_background_correction_active_{false};
#ifdef USE_NUMBER
std::array<number::Number *, TOTAL_GATES> gate_move_threshold_numbers_{};
std::array<number::Number *, TOTAL_GATES> gate_still_threshold_numbers_{};
GateThresholds gate_move_thresholds_;
GateThresholds gate_still_thresholds_;
#endif
#ifdef USE_SENSOR
std::array<SensorWithDedup<uint8_t>, TOTAL_GATES> gate_move_sensors_{};
@@ -4,7 +4,7 @@ namespace esphome::ld2412 {
void GateThresholdNumber::control(float value) {
this->publish_state(value);
this->parent_->set_gate_threshold();
this->parent_->set_gate_threshold(this);
}
} // namespace esphome::ld2412
+1 -4
View File
@@ -19,10 +19,7 @@ class ESPColorSettable {
virtual void lighten(uint8_t delta) = 0;
virtual void darken(uint8_t delta) = 0;
void set(const ESPHSVColor &color) { this->set_hsv(color); }
void set_hsv(const ESPHSVColor &color) {
Color rgb = color.to_rgb();
this->set_rgb(rgb.r, rgb.g, rgb.b);
}
void set_hsv(const ESPHSVColor &color) { this->set(color.to_rgb()); }
void set_rgb(uint8_t red, uint8_t green, uint8_t blue) {
this->set_red(red);
this->set_green(green);
+1 -1
View File
@@ -2,6 +2,7 @@ from esphome import automation
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
from esphome.components import sensor, uart
from esphome.components.const import CONF_AUTOMATIC_BASELINE_CALIBRATION
import esphome.config_validation as cv
from esphome.const import (
CONF_CO2,
@@ -19,7 +20,6 @@ from esphome.types import ConfigType
DEPENDENCIES = ["uart"]
CONF_AUTOMATIC_BASELINE_CALIBRATION = "automatic_baseline_calibration"
CONF_DETECTION_RANGE = "detection_range"
mhz19_ns = cg.esphome_ns.namespace("mhz19")
+4 -1
View File
@@ -1,3 +1,4 @@
from esphome.components.mipi import MODE_RGB
from esphome.const import CONF_IGNORE_STRAPPING_WARNING, CONF_NUMBER
from .ili import GC9A01A, ILI9341, ILI9342, ST7789V
@@ -21,8 +22,10 @@ ST7789V.extend(
# fmt: off
ILI9342.extend(
# ESP32-2432S028 CYD board with USB C + Micro USB, has ILI9342 controller
# ESP32-2432S028 CYD board with USB C + Micro USB, has ILI9342 controller.
# The panel on this board is wired RGB, not the BGR default.
"ESP32-2432S028-9342",
color_order=MODE_RGB,
data_rate="40MHz",
cs_pin={"number": 15, "ignore_strapping_warning": True},
dc_pin={"number": 2, "ignore_strapping_warning": True},
+7
View File
@@ -20,6 +20,7 @@ SETFANMODE = {
CONF_SUPPORTS_DRY = "supports_dry"
CONF_SUPPORTS_FAN_ONLY = "supports_fan_only"
CONF_FAHRENHEIT_COMPATIBILITY = "fahrenheit_compatibility"
CONF_HORIZONTAL_DEFAULT = "horizontal_default"
HorizontalDirections = mitsubishi_ns.enum("HorizontalDirections")
@@ -43,18 +44,22 @@ VERTICAL_DIRECTIONS = {
"down": VerticalDirections.VERTICAL_DIRECTION_DOWN,
}
CONF_VERTICAL_VANES = "vertical_vanes"
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(MitsubishiClimate).extend(
{
cv.Optional(CONF_SET_FAN_MODE, default="3levels"): cv.enum(SETFANMODE),
cv.Optional(CONF_SUPPORTS_DRY, default=False): cv.boolean,
cv.Optional(CONF_SUPPORTS_FAN_ONLY, default=False): cv.boolean,
cv.Optional(CONF_FAHRENHEIT_COMPATIBILITY, default=False): cv.boolean,
cv.Optional(CONF_HORIZONTAL_DEFAULT, default="middle"): cv.enum(
HORIZONTAL_DIRECTIONS
),
cv.Optional(CONF_VERTICAL_DEFAULT, default="middle"): cv.enum(
VERTICAL_DIRECTIONS
),
cv.Optional(CONF_VERTICAL_VANES, default=1): cv.int_range(1, 2),
}
)
@@ -65,5 +70,7 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_fan_mode(config[CONF_SET_FAN_MODE]))
cg.add(var.set_supports_dry(config[CONF_SUPPORTS_DRY]))
cg.add(var.set_supports_fan_only(config[CONF_SUPPORTS_FAN_ONLY]))
cg.add(var.set_fahrenheit_compatibility(config[CONF_FAHRENHEIT_COMPATIBILITY]))
cg.add(var.set_horizontal_default(config[CONF_HORIZONTAL_DEFAULT]))
cg.add(var.set_vertical_default(config[CONF_VERTICAL_DEFAULT]))
cg.add(var.set_vertical_vanes(config[CONF_VERTICAL_VANES]))
+44 -7
View File
@@ -1,4 +1,5 @@
#include "mitsubishi.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -26,6 +27,7 @@ const uint8_t MITSUBISHI_WIDE_VANE_SWING = 0xC0;
const uint8_t MITSUBISHI_FAN_AUTO = 0x00;
const uint8_t MITSUBISHI_VERTICAL_VANE_MASK = 0x38; // Bits 3,4,5
const uint8_t MITSUBISHI_VERTICAL_VANE_SWING = 0x38;
// const uint8_t MITSUBISHI_AUTO = 0x80;
@@ -51,7 +53,33 @@ const uint8_t MITSUBISHI_BYTE02 = 0x26;
const uint8_t MITSUBISHI_BYTE03 = 0x01;
const uint8_t MITSUBISHI_BYTE04 = 0x00;
const uint8_t MITSUBISHI_BYTE13 = 0x00;
const uint8_t MITSUBISHI_BYTE16 = 0x00;
// Byte 7 codes for 61°F through 88°F. Bit 4 is the half degree bit Mitsubishi uses so every Fahrenheit degree
// maps to its own code.
static const uint8_t FAHRENHEIT_CODES[] PROGMEM = {0x00, 0x10, 0x01, 0x11, 0x02, 0x12, 0x03, 0x04, 0x05, 0x15,
0x06, 0x16, 0x07, 0x17, 0x08, 0x18, 0x09, 0x19, 0x0a, 0x1a,
0x0b, 0x1b, 0x0c, 0x1c, 0x0d, 0x1d, 0x0e, 0x0f};
static constexpr uint8_t FAHRENHEIT_MIN = 61;
static constexpr uint8_t MITSUBISHI_HALF_DEGREE = 0x10;
static uint8_t fahrenheit_code(float celsius) {
int fahrenheit = (int) roundf(celsius_to_fahrenheit(celsius));
size_t index = clamp<int>(fahrenheit - FAHRENHEIT_MIN, 0, sizeof(FAHRENHEIT_CODES) - 1);
return progmem_read_byte(&FAHRENHEIT_CODES[index]);
}
// In Fahrenheit mode a code from the table decodes to the exact Fahrenheit value. Any other code, and every code in
// Celsius mode, is a whole degree plus the half degree bit, which a remote set to Fahrenheit also sets.
static float decode_temperature(uint8_t code, bool fahrenheit) {
if (fahrenheit) {
for (size_t i = 0; i < sizeof(FAHRENHEIT_CODES); i++) {
if (code == progmem_read_byte(&FAHRENHEIT_CODES[i])) {
return fahrenheit_to_celsius(FAHRENHEIT_MIN + i);
}
}
}
return MITSUBISHI_TEMP_MIN + (code & 0x0F) + ((code & MITSUBISHI_HALF_DEGREE) ? 0.5f : 0.0f);
}
void MitsubishiClimate::transmit_state() {
// Byte 0-4: Constant: 0x23, 0xCB, 0x26, 0x01, 0x00
@@ -72,7 +100,7 @@ void MitsubishiClimate::transmit_state() {
// Byte 13: Constant 0x00
// Byte 14: HVAC specfic, i.e. ECONO COOL, CLEAN MODE, always 0x00
// Byte 15: HVAC specfic, i.e. POWERFUL, SMART SET, PLASMA, always 0x00
// Byte 16: Constant 0x00
// Byte 16: Left vane control (for specific models). Constants match right vane controls.
// Byte 17: Checksum: SUM[Byte0...Byte16]
uint8_t remote_state[18] = {0x23, 0xCB, 0x26, 0x01, 0x00, 0x20, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
@@ -113,6 +141,8 @@ void MitsubishiClimate::transmit_state() {
// Temperature
if (this->mode == climate::CLIMATE_MODE_DRY) {
remote_state[7] = 24 - MITSUBISHI_TEMP_MIN; // Remote sends always 24°C if "Dry" mode is selected
} else if (this->fahrenheit_compatibility_) {
remote_state[7] = fahrenheit_code(this->target_temperature);
} else {
remote_state[7] = (uint8_t) roundf(
clamp<float>(this->target_temperature, MITSUBISHI_TEMP_MIN, MITSUBISHI_TEMP_MAX) - MITSUBISHI_TEMP_MIN);
@@ -184,7 +214,7 @@ void MitsubishiClimate::transmit_state() {
break;
}
ESP_LOGD(TAG, "default_vertical_direction_: %02X", this->default_vertical_direction_);
ESP_LOGD(TAG, "Vertical default: 0x%02X, vanes: %u", this->default_vertical_direction_, this->vertical_vanes_);
// Special modes
switch (this->preset.value_or(climate::CLIMATE_PRESET_NONE)) {
@@ -206,6 +236,11 @@ void MitsubishiClimate::transmit_state() {
break;
}
if (this->vertical_vanes_ > 1) {
// Heads with two vertical vanes carry the left vane in byte 16; it follows the right one, presets included
remote_state[16] = remote_state[9] & MITSUBISHI_VERTICAL_VANE_MASK;
}
// Checksum
for (int i = 0; i < 17; i++) {
remote_state[17] += remote_state[i];
@@ -266,11 +301,11 @@ bool MitsubishiClimate::on_receive(remote_base::RemoteReceiveData data) {
}
state_frame[pos] = byte;
// Check Header && Footer
// Check Header && Footer. Byte 16 only ever carries the left vane position (bits 3,4,5).
if ((pos == 0 && byte != MITSUBISHI_BYTE00) || (pos == 1 && byte != MITSUBISHI_BYTE01) ||
(pos == 2 && byte != MITSUBISHI_BYTE02) || (pos == 3 && byte != MITSUBISHI_BYTE03) ||
(pos == 4 && byte != MITSUBISHI_BYTE04) || (pos == 13 && byte != MITSUBISHI_BYTE13) ||
(pos == 16 && byte != MITSUBISHI_BYTE16)) {
(pos == 16 && (byte & ~MITSUBISHI_VERTICAL_VANE_MASK) != 0)) {
ESP_LOGV(TAG, "Bytes 0,1,2,3,4,13 or 16 fail - invalid value");
return false;
}
@@ -300,7 +335,7 @@ bool MitsubishiClimate::on_receive(remote_base::RemoteReceiveData data) {
}
// Temp
this->target_temperature = state_frame[7] + MITSUBISHI_TEMP_MIN;
this->target_temperature = decode_temperature(state_frame[7], this->fahrenheit_compatibility_);
// Fan
uint8_t fan = state_frame[9] & 0x07; //(Bit 0,1,2 = Speed)
@@ -332,7 +367,9 @@ bool MitsubishiClimate::on_receive(remote_base::RemoteReceiveData data) {
}
// Vertical Vane
uint8_t vertical_vane = state_frame[9] & 0x38; // Bits 3,4,5
// On dual vane heads, the left vane is in [16] and right vane is in [9]. Left is ignored here because
// the swing_mode enum doesn't convey that level of detail.
uint8_t vertical_vane = state_frame[9] & MITSUBISHI_VERTICAL_VANE_MASK;
switch (vertical_vane) {
case MITSUBISHI_VERTICAL_VANE_SWING:
if (this->swing_mode == climate::CLIMATE_SWING_HORIZONTAL) {
@@ -1,6 +1,7 @@
#pragma once
#include "esphome/components/climate_ir/climate_ir.h"
#include "esphome/core/helpers.h"
#include <cinttypes>
@@ -9,6 +10,8 @@ namespace esphome::mitsubishi {
// Temperature
const uint8_t MITSUBISHI_TEMP_MIN = 16; // Celsius
const uint8_t MITSUBISHI_TEMP_MAX = 31; // Celsius
// Remotes set to Fahrenheit go up to 88°F, one step past the Celsius range
static constexpr float MITSUBISHI_TEMP_MAX_FAHRENHEIT = fahrenheit_to_celsius(88.0f);
// Fan mode
enum SetFanMode : uint8_t {
@@ -55,6 +58,12 @@ class MitsubishiClimate final : public climate_ir::ClimateIR {
void set_supports_fan_only(bool supports_fan_only) {
this->set_mode_supported_(climate::CLIMATE_MODE_FAN_ONLY, supports_fan_only);
}
void set_fahrenheit_compatibility(bool fahrenheit_compatibility) {
this->fahrenheit_compatibility_ = fahrenheit_compatibility;
if (fahrenheit_compatibility) {
this->maximum_temperature_ = MITSUBISHI_TEMP_MAX_FAHRENHEIT;
}
}
void set_fan_mode(SetFanMode fan_mode) {
this->fan_mode_ = fan_mode;
@@ -71,6 +80,7 @@ class MitsubishiClimate final : public climate_ir::ClimateIR {
void set_vertical_default(VerticalDirection vertical_direction) {
this->default_vertical_direction_ = vertical_direction;
}
void set_vertical_vanes(uint8_t vertical_vanes) { this->vertical_vanes_ = vertical_vanes; }
protected:
// Transmit via IR the state of this climate controller.
@@ -80,9 +90,11 @@ class MitsubishiClimate final : public climate_ir::ClimateIR {
bool parse_state_frame_(const uint8_t frame[]);
SetFanMode fan_mode_;
bool fahrenheit_compatibility_{false};
HorizontalDirection default_horizontal_direction_;
VerticalDirection default_vertical_direction_;
uint8_t vertical_vanes_{1};
};
} // namespace esphome::mitsubishi
+5 -5
View File
@@ -950,7 +950,7 @@ bool ModbusDeviceCommand::sent() {
// on_sent() is not a terminal, so nothing is consumed.
if (this->device == nullptr)
return false;
this->device->on_sent(this->frame.pdu());
this->device->on_sent(this->frame.address(), this->frame.pdu());
return true;
}
@@ -958,7 +958,7 @@ bool ModbusDeviceCommand::notify_retired() {
if (!this->decrement_pending())
return false; // nothing owed - stop the sweep draining this entry
if (this->device != nullptr)
this->device->on_not_sent(this->frame.pdu());
this->device->on_not_sent(this->frame.address(), this->frame.pdu());
return true; // consumed one debt (delivered, or silent when device-less) - keep draining to zero
}
@@ -969,7 +969,7 @@ bool ModbusDeviceCommand::response(std::span<const uint8_t> response_pdu) {
this->decrement_pending();
if (this->device == nullptr)
return false;
this->device->on_response(this->frame.pdu(), response_pdu);
this->device->on_response(this->frame.address(), this->frame.pdu(), response_pdu);
return true;
}
@@ -979,7 +979,7 @@ bool ModbusDeviceCommand::error(ExceptionCode exception_code) {
this->decrement_pending();
if (this->device == nullptr)
return false;
this->device->on_error(this->frame.pdu(), exception_code);
this->device->on_error(this->frame.address(), this->frame.pdu(), exception_code);
return true;
}
@@ -1004,7 +1004,7 @@ bool ModbusDeviceCommand::timed_out() {
return false; // resolved, no one to tell
// A cleared frame that timed out still honors a retry: the clear is address-scoped (any device may
// call it) while the retry is the owning device's call via on_no_response - the bus obeys the owner.
if (this->device->on_no_response(this->frame.pdu()))
if (this->device->on_no_response(this->frame.address(), this->frame.pdu()))
this->increment_pending(); // granted retry = re-request (capped)
return true;
}
+15
View File
@@ -437,6 +437,21 @@ class ModbusClientDevice {
return this->on_modbus_no_response();
#pragma GCC diagnostic pop
}
/// Overloads of the hooks above with the device address of the request first, taken from the request frame, so it
/// stays correct after set_address(). The hub calls these; the defaults forward to the hooks above.
/// Override one form per hook: overriding an address form drops its forward, so the other form is no longer called.
virtual void on_response(uint8_t address, std::span<const uint8_t> request_pdu,
std::span<const uint8_t> response_pdu) {
this->on_response(request_pdu, response_pdu);
}
virtual void on_error(uint8_t address, std::span<const uint8_t> request_pdu, ExceptionCode exception_code) {
this->on_error(request_pdu, exception_code);
}
virtual void on_not_sent(uint8_t address, std::span<const uint8_t> request_pdu) { this->on_not_sent(request_pdu); }
virtual void on_sent(uint8_t address, std::span<const uint8_t> request_pdu) { this->on_sent(request_pdu); }
virtual bool on_no_response(uint8_t address, std::span<const uint8_t> request_pdu) {
return this->on_no_response(request_pdu);
}
// Remove before 2027.2.0
ESPDEPRECATED("Override on_not_sent() instead. Removed in 2027.2.0", "2026.8.0")
virtual void on_modbus_not_sent() {}
+1 -4
View File
@@ -261,10 +261,7 @@ async def to_code(config):
if CORE.is_esp32:
esp32.request_http_client()
esp32.add_idf_sdkconfig_option("CONFIG_ESP_TLS_INSECURE", True)
esp32.add_idf_sdkconfig_option(
"CONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY", True
)
esp32.allow_insecure_tls()
elif CORE.is_esp8266:
cg.add_library("ESP8266HTTPClient", None)
+5 -5
View File
@@ -732,13 +732,13 @@ def upload_program(config: ConfigType, args, host: str) -> bool:
return True # Handled: PYOCD upload
# Deferred imports: bleak/smpclient are heavy, only load for BLE/mcumgr paths
from .ble_logger import is_mac_address
from .ble_logger import is_ble_address
from .ota import smpmgr_scan, smpmgr_upload
if host == "BLE":
mcumgr_device = asyncio.run(smpmgr_scan(CORE.name))
if is_mac_address(host):
if is_ble_address(host):
mcumgr_device = host
if mcumgr_device:
@@ -753,7 +753,7 @@ def upload_program(config: ConfigType, args, host: str) -> bool:
def show_logs(config: ConfigType, args, devices: list[str]) -> bool:
address = devices[0]
from .ble_logger import is_mac_address, logger_connect, logger_scan
from .ble_logger import is_ble_address, logger_connect, logger_scan
if devices[0] == "BLE":
ble_device = asyncio.run(logger_scan(CORE.name))
@@ -762,7 +762,7 @@ def show_logs(config: ConfigType, args, devices: list[str]) -> bool:
else:
return True
if is_mac_address(address):
if is_ble_address(address):
asyncio.run(logger_connect(address))
return True
return False
@@ -835,7 +835,7 @@ _PCH_CMAKE_LINES = [
"set(esphome_kept_options)",
"set(esphome_pch_headers)",
"foreach(option IN LISTS esphome_options)",
' if(option MATCHES "imacros> (.+)$")',
' if(option MATCHES "imacros> ([^>]+)")',
' list(APPEND esphome_pch_headers "${CMAKE_MATCH_1}")',
" list(APPEND esphome_kept_options",
' "$<$<NOT:$<AND:$<COMPILE_LANGUAGE:CXX>,$<STREQUAL:$<TARGET_PROPERTY:NAME>,app>>>:${option}>")',
+20 -3
View File
@@ -4,6 +4,7 @@ import re
from typing import Final
from bleak import BleakClient, BleakScanner, BLEDevice
from bleak.backends.scanner import AdvertisementData
from bleak.exc import (
BleakCharacteristicNotFoundError,
BleakDBusError,
@@ -19,15 +20,31 @@ NUS_TX_CHAR_UUID = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"
MAC_ADDRESS_PATTERN: Final = re.compile(
r"([0-9A-F]{2}[:]){5}[0-9A-F]{2}$", flags=re.IGNORECASE
)
# macOS identifies BLE peripherals by a CoreBluetooth UUID instead of a MAC address
UUID_PATTERN: Final = re.compile(
r"[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$",
flags=re.IGNORECASE,
)
def is_mac_address(value: str) -> bool:
return MAC_ADDRESS_PATTERN.match(value)
def is_ble_address(value: str) -> bool:
return bool(MAC_ADDRESS_PATTERN.match(value) or UUID_PATTERN.match(value))
def _name_matches(device: BLEDevice, adv: AdvertisementData, name: str) -> bool:
# device.name can be a stale cached name on macOS; adv.local_name is what is on the air now
return name in (device.name, adv.local_name)
async def find_device_by_name(name: str, timeout: float = 10.0) -> BLEDevice | None:
return await BleakScanner.find_device_by_filter(
lambda device, adv: _name_matches(device, adv, name), timeout=timeout
)
async def logger_scan(name: str) -> BLEDevice | None:
_LOGGER.info("Scanning bluetooth for %s...", name)
device = await BleakScanner.find_device_by_name(name)
device = await find_device_by_name(name)
if not device:
_LOGGER.error("%s Bluetooth LE device was not found!", name)
return device
+10 -11
View File
@@ -4,7 +4,6 @@ import json
import logging
from pathlib import Path
from bleak import BleakScanner
from bleak.exc import BleakDBusError, BleakDeviceNotFoundError
from smp.exceptions import SMPBadStartDelimiter
from smpclient import SMPClient
@@ -23,9 +22,8 @@ from smpclient.transport.serial import SMPSerialTransport
from esphome.core import EsphomeError
from esphome.espota2 import ProgressBar
from .ble_logger import is_mac_address
from .ble_logger import find_device_by_name, is_ble_address
SMP_SERVICE_UUID = "8D53DC1D-1DB7-4CD3-868B-8A527460AA84"
BLE_SCAN_TIMEOUT = 10.0 # seconds
RESET_DELAY = 2.0 # seconds to wait before reset, allows on_end action to execute
@@ -45,12 +43,12 @@ def _json_state(o: object) -> object:
async def smpmgr_scan(name: str) -> str:
_LOGGER.info("Scanning bluetooth for %s...", name)
for device in await BleakScanner.discover(
timeout=BLE_SCAN_TIMEOUT, service_uuids=[SMP_SERVICE_UUID]
):
if device.name == name:
return device.address
raise EsphomeError(f"BLE device {name} with OTA service not found")
# No service filter: the SMP UUID only fits in the scan response, which macOS does not filter on.
# The SMP service is checked on connect.
device = await find_device_by_name(name, timeout=BLE_SCAN_TIMEOUT)
if device is None:
raise EsphomeError(f"BLE device {name} not found")
return device.address
async def smpmgr_upload(device: str, firmware: Path) -> None:
@@ -88,7 +86,7 @@ def _get_image_tlv_sha256(file: Path) -> bytes:
async def _smpmgr_upload(device: str, firmware: Path) -> None:
image_tlv_sha256 = _get_image_tlv_sha256(firmware)
if is_mac_address(device):
if is_ble_address(device):
smp_client = SMPClient(SMPBLETransport(), device)
else:
smp_client = SMPClient(SMPSerialTransport(), device)
@@ -105,7 +103,8 @@ async def _smpmgr_upload(device: str, firmware: Path) -> None:
) from exc
raise EsphomeError(f"BLE error connecting to {device}: {exc}") from exc
except SMPBLETransportException as exc:
raise EsphomeError(f"Connection error with {device}") from exc
# Reached when a device with the right name has no SMP service, among other causes
raise EsphomeError(f"Connection error with {device}: {exc}") from exc
_LOGGER.info("Connected %s...", device)
try:
+9 -8
View File
@@ -68,8 +68,9 @@ def raise_first_undefined(
for e, p_path, _ in errors[1:]
)
_LOGGER.debug("Additional undefined variables in %s: %s", context_label, extras)
raise cv.Invalid(
f"Undefined variable in {context_label}: {err.message}\n{format_path(err_path, err_value)}"
raise cv.SourceTraceInvalid(
f"Undefined variable in {context_label}: {err.message}",
format_path(err_path, err_value),
)
@@ -223,13 +224,13 @@ def _expand_substitutions(
errors=errors,
)
except JinjaError as err:
raise cv.Invalid(
raise cv.SourceTraceInvalid(
f"{err.error_name()} Error evaluating jinja expression"
f" '{value}': {str(err.parent())}."
f"\nEvaluation stack: (most recent evaluation last)"
f"\n{err.stack_trace_str()}"
f"\nRelevant context:\n{err.context_trace_str()}"
f"\n{format_path(path, orig_value)}",
f"\nRelevant context:\n{err.context_trace_str()}",
format_path(path, orig_value),
path,
) from err
else:
@@ -389,9 +390,9 @@ def resolve_include(
return include.load() if include.should_load() else {}
except (esphome.core.EsphomeError, cv.Invalid) as err:
resolved = f" (expanded from '{original}')" if substituted else ""
raise cv.Invalid(
f"Error including file '{filename}'{resolved}: {err}"
f"\n{format_path(path, original)}",
raise cv.SourceTraceInvalid(
f"Error including file '{filename}'{resolved}: {err}",
format_path(path, original),
path + [f"<{filename}>"],
) from err
+2
View File
@@ -17,6 +17,8 @@ SystaBus = systa_bus_ns.class_("SystaBus", cg.Component, uart.UARTDevice)
CONF_SYSTA_BUS_ID = "systa_bus_id"
CONF_SYSTASOLAR_AQUA = "systasolar_aqua"
CONFIG_SCHEMA = uart.UART_DEVICE_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(SystaBus),
@@ -13,11 +13,16 @@ from esphome.const import (
)
from esphome.types import ConfigType
from .. import CONF_SYSTA_BUS_ID, SystaBus, register_systa_bus_listener, systa_bus_ns
from .. import (
CONF_SYSTA_BUS_ID,
CONF_SYSTASOLAR_AQUA,
SystaBus,
register_systa_bus_listener,
systa_bus_ns,
)
SystaSolarAquaSensor = systa_bus_ns.class_("SystaSolarAquaSensor", cg.Component)
CONF_SYSTASOLAR_AQUA = "systasolar_aqua"
CONF_TEMPERATURE_TSA = "temperature_tsa"
CONF_TEMPERATURE_TSE = "temperature_tse"
CONF_TEMPERATURE_TWU = "temperature_twu"
+2 -1
View File
@@ -14,7 +14,8 @@ static constexpr uint8_t START_BYTE = 0xfc;
static constexpr uint8_t FRAME_OVERHEAD = 3; // start byte, length, checksum
// SystaSolar Aqua sensor data, 25 bytes: [0..1] type, [2..3] not decoded, big-endian int16 in 0.1 degrees at
// [4..5] TSA, [6..7] TSE, [8..9] TWU and [10..11] TW2, pump speed in percent at [12], [13..23] not decoded.
// [4..5] TSA, [6..7] TSE, [8..9] TWU and [10..11] TW2, pump speed in percent at [12], [13..14] not decoded,
// [15] error code (zero for no error), [16..23] not decoded.
static constexpr uint16_t MESSAGE_TYPE_AQUA_SENSOR_DATA = 0xfc16;
// The bus only buffers known message types, so the buffer is sized by the longest of them
@@ -0,0 +1,45 @@
import esphome.codegen as cg
from esphome.components import text_sensor
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_MODEL
from esphome.types import ConfigType
from .. import (
CONF_SYSTA_BUS_ID,
CONF_SYSTASOLAR_AQUA,
SystaBus,
register_systa_bus_listener,
systa_bus_ns,
)
SystaSolarAquaTextSensor = systa_bus_ns.class_("SystaSolarAquaTextSensor", cg.Component)
CONF_ERROR_CODE = "error_code"
CONFIG_SCHEMA = cv.typed_schema(
{
CONF_SYSTASOLAR_AQUA: cv.COMPONENT_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(SystaSolarAquaTextSensor),
cv.GenerateID(CONF_SYSTA_BUS_ID): cv.use_id(SystaBus),
cv.Optional(CONF_ERROR_CODE): text_sensor.text_sensor_schema(),
}
),
},
key=CONF_MODEL,
lower=True,
space="_",
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
if config[CONF_MODEL] == CONF_SYSTASOLAR_AQUA and CONF_ERROR_CODE in config:
sens = await text_sensor.new_text_sensor(config[CONF_ERROR_CODE])
cg.add(var.set_error_code_text_sensor(sens))
systa_bus = await cg.get_variable(config[CONF_SYSTA_BUS_ID])
await register_systa_bus_listener(systa_bus, var)
@@ -0,0 +1,27 @@
#include "systa_bus_text_sensor.h"
#include "esphome/core/log.h"
namespace esphome::systa_bus {
ESPHOME_LOG_TAG(TAG, "systa_bus.text_sensor");
void SystaSolarAquaTextSensor::dump_config() {
ESP_LOGCONFIG(TAG, "SystaSolar Aqua:");
LOG_TEXT_SENSOR(" ", "Error code", this->error_code_text_sensor_);
}
void SystaSolarAquaTextSensor::handle_message(std::span<const uint8_t> message) {
if (get_message_type(message) == MESSAGE_TYPE_AQUA_SENSOR_DATA) {
if (this->error_code_text_sensor_ != nullptr) {
if (message[15] == 0) {
this->error_code_text_sensor_->publish_state("--");
} else {
char buf[4];
int len = snprintf(buf, sizeof(buf), "%02d", message[15]);
this->error_code_text_sensor_->publish_state(buf, static_cast<size_t>(len));
}
}
}
}
} // namespace esphome::systa_bus
@@ -0,0 +1,20 @@
#pragma once
#include "../systa_bus.h"
#include "esphome/components/text_sensor/text_sensor.h"
#include <span>
namespace esphome::systa_bus {
class SystaSolarAquaTextSensor : public SystaBusListener, public Component {
public:
void dump_config() override;
void set_error_code_text_sensor(text_sensor::TextSensor *sensor) { this->error_code_text_sensor_ = sensor; }
void handle_message(std::span<const uint8_t> message) override;
protected:
text_sensor::TextSensor *error_code_text_sensor_{nullptr};
};
} // namespace esphome::systa_bus
+52 -1
View File
@@ -1,7 +1,14 @@
import esphome.codegen as cg
from esphome.components import i2c
import esphome.config_validation as cv
from esphome.const import CONF_CHANNEL, CONF_CHANNELS, CONF_ID
from esphome.const import (
CONF_CHANNEL,
CONF_CHANNELS,
CONF_FREQUENCY,
CONF_I2C_ID,
CONF_ID,
)
from esphome.core import CORE, ID
from esphome.types import ConfigType
CODEOWNERS = ["@andreashergert1984"]
@@ -16,14 +23,51 @@ TCA9548AChannel = tca9548a_ns.class_("TCA9548AChannel", i2c.I2CBus)
MULTI_CONF = True
CONF_BUS_ID = "bus_id"
# Sizes the root bus's frequency table; only emitted when a frequency is set
_request_port_frequency_slot = cg.slot_counter("I2C_PORT_FREQUENCY_COUNT")
def _root_bus_id(bus_id: ID) -> ID:
"""The hardware bus behind a port, through any multiplexers in between."""
parents = {
str(chan[CONF_BUS_ID]): mux[CONF_I2C_ID]
for mux in CORE.config.get(DOMAIN, [])
for chan in mux.get(CONF_CHANNELS, [])
}
while (parent := parents.get(str(bus_id))) is not None:
bus_id = parent
return bus_id
def _declared_bus_type(bus_id: ID) -> cg.MockObjClass | None:
"""The class the i2c component declared for a bus, or None for an external bus."""
for bus in CORE.config.get(i2c.DOMAIN, []):
if bus[CONF_ID].id == bus_id.id:
return bus[CONF_ID].type
return None
async def _add_frequency(root_bus_id: ID, frequency: int) -> None:
"""Count the frequency against the root bus; the built-in ESP32 bus also
creates its device handle in setup()."""
_request_port_frequency_slot(str(root_bus_id))
if _declared_bus_type(root_bus_id) is i2c.IDFI2CBus:
root = await cg.get_variable(root_bus_id)
cg.add(root.add_frequency(frequency))
_FREQUENCY = cv.All(cv.frequency, cv.Range(min=0, min_included=False))
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(TCA9548AComponent),
cv.Optional(CONF_FREQUENCY): _FREQUENCY,
cv.Optional(CONF_CHANNELS, default=[]): cv.ensure_list(
{
cv.Required(CONF_BUS_ID): cv.declare_id(TCA9548AChannel),
cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=7),
cv.Optional(CONF_FREQUENCY): _FREQUENCY,
}
),
}
@@ -37,8 +81,15 @@ async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
root_bus_id = _root_bus_id(config[CONF_I2C_ID])
if (frequency := config.get(CONF_FREQUENCY)) is not None:
await _add_frequency(root_bus_id, int(frequency))
cg.add(var.set_frequency(int(frequency)))
for conf in config[CONF_CHANNELS]:
chan = cg.new_Pvariable(conf[CONF_BUS_ID])
cg.add(chan.set_parent(var))
cg.add(chan.set_channel(conf[CONF_CHANNEL]))
if (frequency := conf.get(CONF_FREQUENCY)) is not None:
await _add_frequency(root_bus_id, int(frequency))
cg.add(chan.set_frequency(int(frequency)))
+97 -5
View File
@@ -1,4 +1,6 @@
#include "tca9548a.h"
#include <algorithm>
#include "esphome/core/log.h"
namespace esphome::tca9548a {
@@ -7,20 +9,111 @@ ESPHOME_LOG_TAG(TAG, "tca9548a");
i2c::ErrorCode TCA9548AChannel::write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count,
uint8_t *read_buffer, size_t read_count) {
auto err = this->parent_->switch_to_channel(channel_);
#ifdef I2C_PORT_FREQUENCY_COUNT
const uint32_t select_frequency = this->select_frequency_();
if (select_frequency != 0) {
return this->write_readv_at_port_frequency_(select_frequency, address, write_buffer, write_count, read_buffer,
read_count);
}
#endif
auto err = this->parent_->switch_to_channel(this->channel_);
if (err != i2c::ERROR_OK)
return err;
err = this->parent_->bus_->write_readv(address, write_buffer, write_count, read_buffer, read_count);
this->parent_->disable_all_channels();
return err;
}
#ifdef I2C_PORT_FREQUENCY_COUNT
// The multiplexer's own frequency, else the slower of the bus and the port
uint32_t TCA9548AChannel::select_frequency_() const {
if (this->parent_->frequency_ != 0)
return this->parent_->frequency_;
if (this->frequency_ == 0)
return 0;
// A bus that does not report its frequency gets the port's, and fails the switch
const uint32_t bus_frequency = this->parent_->bus_->get_frequency();
return bus_frequency != 0 ? std::min(this->frequency_, bus_frequency) : this->frequency_;
}
// The port frequency, else the multiplexer's
uint32_t TCA9548AChannel::transfer_frequency_() const {
return this->frequency_ != 0 ? this->frequency_ : this->parent_->frequency_;
}
i2c::ErrorCode TCA9548AChannel::write_readv_at_port_frequency_(uint32_t select_frequency, uint8_t address,
const uint8_t *write_buffer, size_t write_count,
uint8_t *read_buffer, size_t read_count) {
const uint32_t original_frequency = this->parent_->bus_->get_frequency();
// A failed switch leaves the bus as it was
auto err = this->switch_bus_(select_frequency);
if (err != i2c::ERROR_OK)
return err;
err = this->parent_->switch_to_channel(this->channel_);
if (err == i2c::ERROR_OK) {
err = this->transfer_(select_frequency, address, write_buffer, write_count, read_buffer, read_count);
this->parent_->disable_all_channels();
}
const i2c::ErrorCode restored = this->switch_bus_(original_frequency);
return err != i2c::ERROR_OK ? err : restored;
}
// Transfer at the port frequency, back to the select frequency for the deselect
i2c::ErrorCode TCA9548AChannel::transfer_(uint32_t select_frequency, uint8_t address, const uint8_t *write_buffer,
size_t write_count, uint8_t *read_buffer, size_t read_count) {
const uint32_t transfer_frequency = this->transfer_frequency_();
if (transfer_frequency == select_frequency) {
return this->parent_->bus_->write_readv(address, write_buffer, write_count, read_buffer, read_count);
}
auto err = this->switch_bus_(transfer_frequency);
if (err == i2c::ERROR_OK) {
err = this->parent_->bus_->write_readv(address, write_buffer, write_count, read_buffer, read_count);
}
const i2c::ErrorCode restored = this->switch_bus_(select_frequency);
return err != i2c::ERROR_OK ? err : restored;
}
// Forward to the shared upstream bus
i2c::ErrorCode TCA9548AChannel::switch_frequency(uint32_t frequency) {
return this->parent_->bus_->switch_frequency(frequency);
}
i2c::ErrorCode TCA9548AChannel::switch_bus_(uint32_t frequency) {
const i2c::ErrorCode err = this->switch_frequency(frequency);
if (err != i2c::ERROR_OK) {
this->parent_->status_set_error(LOG_STR("Failed to switch the bus frequency"));
}
return err;
}
uint32_t TCA9548AChannel::get_frequency() const { return this->parent_->bus_->get_frequency(); }
#endif
void TCA9548AComponent::setup() {
uint8_t status = 0;
if (this->read(&status, 1) != i2c::ERROR_OK) {
#ifdef I2C_PORT_FREQUENCY_COUNT
const uint32_t original_frequency = this->bus_->get_frequency();
if (this->frequency_ != 0 && this->bus_->switch_frequency(this->frequency_) != i2c::ERROR_OK) {
this->mark_failed(LOG_STR("Failed to switch the bus frequency"));
return;
}
#endif
const i2c::ErrorCode err = this->read(&status, 1);
#ifdef I2C_PORT_FREQUENCY_COUNT
const bool restored = this->frequency_ == 0 || this->bus_->switch_frequency(original_frequency) == i2c::ERROR_OK;
#endif
if (err != i2c::ERROR_OK) {
ESP_LOGE(TAG, "TCA9548A failed");
this->mark_failed();
return;
}
#ifdef I2C_PORT_FREQUENCY_COUNT
if (!restored) {
this->mark_failed(LOG_STR("Failed to switch the bus frequency"));
return;
}
#endif
ESP_LOGD(TAG, "Channels currently open: %d", status);
}
void TCA9548AComponent::dump_config() {
@@ -32,14 +125,13 @@ i2c::ErrorCode TCA9548AComponent::switch_to_channel(uint8_t channel) {
if (this->is_failed())
return i2c::ERROR_NOT_INITIALIZED;
uint8_t channel_val = 1 << channel;
const uint8_t channel_val = 1 << channel;
return this->write(&channel_val, 1);
}
void TCA9548AComponent::disable_all_channels() {
if (this->write(&TCA9548A_DISABLE_CHANNELS_COMMAND, 1) != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Failed to disable all channels.");
this->status_set_error(); // couldn't disable channels, set error status
this->status_set_error(LOG_STR("Failed to disable all channels."));
}
}
+26 -2
View File
@@ -10,13 +10,30 @@ static const uint8_t TCA9548A_DISABLE_CHANNELS_COMMAND = 0x00;
class TCA9548AComponent;
class TCA9548AChannel final : public i2c::I2CBus {
public:
void set_channel(uint8_t channel) { channel_ = channel; }
void set_parent(TCA9548AComponent *parent) { parent_ = parent; }
void set_channel(uint8_t channel) { this->channel_ = channel; }
void set_parent(TCA9548AComponent *parent) { this->parent_ = parent; }
i2c::ErrorCode write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count, uint8_t *read_buffer,
size_t read_count) override;
#ifdef I2C_PORT_FREQUENCY_COUNT
void set_frequency(uint32_t frequency) { this->frequency_ = frequency; }
i2c::ErrorCode switch_frequency(uint32_t frequency) override;
uint32_t get_frequency() const override;
#endif
protected:
#ifdef I2C_PORT_FREQUENCY_COUNT
uint32_t select_frequency_() const;
uint32_t transfer_frequency_() const;
i2c::ErrorCode write_readv_at_port_frequency_(uint32_t select_frequency, uint8_t address, const uint8_t *write_buffer,
size_t write_count, uint8_t *read_buffer, size_t read_count);
i2c::ErrorCode transfer_(uint32_t select_frequency, uint8_t address, const uint8_t *write_buffer, size_t write_count,
uint8_t *read_buffer, size_t read_count);
/// Switch the upstream bus, flagging the multiplexer on failure
i2c::ErrorCode switch_bus_(uint32_t frequency);
uint32_t frequency_{0};
#endif
uint8_t channel_;
TCA9548AComponent *parent_;
};
@@ -30,8 +47,15 @@ class TCA9548AComponent final : public Component, public i2c::I2CDevice {
i2c::ErrorCode switch_to_channel(uint8_t channel);
void disable_all_channels();
#ifdef I2C_PORT_FREQUENCY_COUNT
/// Addressing frequency, and the default for the ports
void set_frequency(uint32_t frequency) { this->frequency_ = frequency; }
#endif
protected:
friend class TCA9548AChannel;
#ifdef I2C_PORT_FREQUENCY_COUNT
uint32_t frequency_{0};
#endif
};
} // namespace esphome::tca9548a
+1 -1
View File
@@ -4,7 +4,7 @@ import esphome.config_validation as cv
from esphome.const import CONF_ID
from esphome.types import ConfigType
CODEOWNERS = ["@0hax"]
CODEOWNERS = ["@0hax", "@FredM67"]
DOMAIN = "teleinfo"
MULTI_CONF = True
+136 -8
View File
@@ -1,10 +1,32 @@
import esphome.codegen as cg
from esphome.components import sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_DEVICE_CLASS,
CONF_ICON,
CONF_ID,
CONF_STATE_CLASS,
CONF_UNIT_OF_MEASUREMENT,
DEVICE_CLASS_APPARENT_POWER,
DEVICE_CLASS_CURRENT,
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_POWER,
DEVICE_CLASS_REACTIVE_ENERGY,
DEVICE_CLASS_VOLTAGE,
ICON_FLASH,
STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL_INCREASING,
UNIT_AMPERE,
UNIT_KILOVOLT_AMPS,
UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
UNIT_KILOWATT,
UNIT_KILOWATT_HOURS,
UNIT_MILLIAMP,
UNIT_MILLIVOLT,
UNIT_VOLT,
UNIT_VOLT_AMPS,
UNIT_VOLT_AMPS_REACTIVE_HOURS,
UNIT_WATT,
UNIT_WATT_HOURS,
)
from esphome.types import ConfigType
@@ -13,19 +35,125 @@ from .. import CONF_TAG_NAME, CONF_TELEINFO_ID, TELEINFO_LISTENER_SCHEMA, telein
TeleInfoSensor = teleinfo_ns.class_("TeleInfoSensor", sensor.Sensor, cg.Component)
CONFIG_SCHEMA = sensor.sensor_schema(
TeleInfoSensor,
unit_of_measurement=UNIT_WATT_HOURS,
icon=ICON_FLASH,
accuracy_decimals=0,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
).extend(TELEINFO_LISTENER_SCHEMA)
def _preset(unit: str, device_class: str, state_class: str) -> dict[str, str]:
return {
CONF_UNIT_OF_MEASUREMENT: unit,
CONF_DEVICE_CLASS: device_class,
CONF_STATE_CLASS: state_class,
}
_ENERGY = _preset(UNIT_WATT_HOURS, DEVICE_CLASS_ENERGY, STATE_CLASS_TOTAL_INCREASING)
_REACTIVE_ENERGY = _preset(
UNIT_VOLT_AMPS_REACTIVE_HOURS,
DEVICE_CLASS_REACTIVE_ENERGY,
STATE_CLASS_TOTAL_INCREASING,
)
_CURRENT = _preset(UNIT_AMPERE, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT)
_VOLTAGE = _preset(UNIT_VOLT, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT)
_APPARENT_POWER_VA = _preset(
UNIT_VOLT_AMPS, DEVICE_CLASS_APPARENT_POWER, STATE_CLASS_MEASUREMENT
)
_APPARENT_POWER_KVA = _preset(
UNIT_KILOVOLT_AMPS, DEVICE_CLASS_APPARENT_POWER, STATE_CLASS_MEASUREMENT
)
_POWER = _preset(UNIT_WATT, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT)
# Legacy defaults, kept for tags without a known preset
_LEGACY_DEFAULT = {**_ENERGY, CONF_ICON: ICON_FLASH}
# Units that are valid for each device class, used to decide whether a preset's
# device class can be kept when the user overrides the unit (and vice versa)
_COMPATIBLE_UNITS = {
DEVICE_CLASS_ENERGY: {UNIT_WATT_HOURS, UNIT_KILOWATT_HOURS},
DEVICE_CLASS_REACTIVE_ENERGY: {
UNIT_VOLT_AMPS_REACTIVE_HOURS,
UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
},
DEVICE_CLASS_CURRENT: {UNIT_AMPERE, UNIT_MILLIAMP},
DEVICE_CLASS_VOLTAGE: {UNIT_VOLT, UNIT_MILLIVOLT},
DEVICE_CLASS_APPARENT_POWER: {UNIT_VOLT_AMPS, UNIT_KILOVOLT_AMPS},
DEVICE_CLASS_POWER: {UNIT_WATT, UNIT_KILOWATT},
}
# Presets by tag prefix. No prefix is a prefix of another, so order is irrelevant.
TIC_TAG_CONFIGS = {
# Standard mode
"EA": _ENERGY,
"ER": _REACTIVE_ENERGY,
"IRMS": _CURRENT,
"URMS": _VOLTAGE,
"UMOY": _VOLTAGE,
"SINST": _APPARENT_POWER_VA,
"SMAX": _APPARENT_POWER_VA,
"CC": _POWER,
"PREF": _APPARENT_POWER_KVA,
"PCOUP": _APPARENT_POWER_KVA,
# Historical mode
"BASE": _ENERGY,
"HCH": _ENERGY,
"EJP": _ENERGY,
"BBRH": _ENERGY,
"IINST": _CURRENT,
"IMAX": _CURRENT,
"ISOUSC": _CURRENT,
"ADPS": _CURRENT,
"ADIR": _CURRENT,
"PAPP": _APPARENT_POWER_VA,
"PMAX": _POWER,
}
def apply_tag_config(config: ConfigType) -> ConfigType:
"""Apply preset configurations based on the tag name.
Only keys not set by the user are filled in. For known tags, a preset unit or
device class is dropped if it is incompatible with the one set by the user.
Unknown tags get the legacy defaults, merged key by key as before.
"""
if CONF_TAG_NAME not in config:
return config
# Runs before cv.string, so the raw value may not be a string yet
tag_name = str(config[CONF_TAG_NAME])
preset = next(
(p for prefix, p in TIC_TAG_CONFIGS.items() if tag_name.startswith(prefix)),
None,
)
config = dict(config)
if preset is None:
for key, value in _LEGACY_DEFAULT.items():
config.setdefault(key, value)
return config
unit = config.get(CONF_UNIT_OF_MEASUREMENT, preset[CONF_UNIT_OF_MEASUREMENT])
device_class = config.get(CONF_DEVICE_CLASS, preset[CONF_DEVICE_CLASS])
compatible = unit in _COMPATIBLE_UNITS.get(device_class, ())
for key, value in preset.items():
if key in config:
continue
if key in (CONF_UNIT_OF_MEASUREMENT, CONF_DEVICE_CLASS) and not compatible:
continue
config[key] = value
return config
CONFIG_SCHEMA = cv.All(
apply_tag_config,
sensor.sensor_schema(
TeleInfoSensor,
accuracy_decimals=0,
).extend(TELEINFO_LISTENER_SCHEMA),
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID], config[CONF_TAG_NAME])
await cg.register_component(var, config)
await sensor.register_sensor(var, config)
teleinfo = await cg.get_variable(config[CONF_TELEINFO_ID])
cg.add(teleinfo.register_teleinfo_listener(var))
+3 -4
View File
@@ -116,8 +116,7 @@ void TeleInfo::loop() {
* 0xa | Tag | 0x9 | Timestamp | 0x9 | 0x9 | CRC | 0xd
*
*/
while ((buf_finger = static_cast<char *>(memchr(buf_finger, (int) 0xa, buf_index_ - 1))) &&
((buf_finger - buf_) < buf_index_)) { // NOLINT(clang-diagnostic-sign-compare)
while ((buf_finger = static_cast<char *>(memchr(buf_finger, (int) 0xa, buf_end - buf_finger)))) {
/*
* Make sure timesamp is nullified between each tag as some tags don't
* have a timestamp
@@ -168,8 +167,8 @@ void TeleInfo::loop() {
continue;
}
/* Advance buf_finger to end of group */
buf_finger += field_len + 1 + 1 + 1;
/* Resume after the group's 0xd; grp_end < buf_end, so this stays in bounds. */
buf_finger = grp_end + 1;
publish_value_(std::string(tag_), std::string(val_));
}
+2
View File
@@ -16,6 +16,8 @@ MODELS = {
"RAC-PT1411HWRU-C": Model.MODEL_RAC_PT1411HWRU_C,
"RAC-PT1411HWRU-F": Model.MODEL_RAC_PT1411HWRU_F,
"RAS-2819T": Model.MODEL_RAS_2819T,
"RAS-B10N3KV2": Model.MODEL_RAS_B10N3KV2,
"SEIYA": Model.MODEL_SEIYA,
}
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(ToshibaClimate).extend(
+209 -89
View File
@@ -1,5 +1,6 @@
#include "toshiba.h"
#include "esphome/components/remote_base/toshiba_ac_protocol.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include <vector>
@@ -163,6 +164,54 @@ const uint8_t RAS_2819T_AUTO_DRY_FAN_BYTE = 0x65;
const uint8_t RAS_2819T_AUTO_DRY_SUFFIX = 0x3A;
const uint8_t RAS_2819T_HEAT_SUFFIX = 0x3B;
// Bit timings differ per protocol family; the frame layout is shared
struct ToshibaTimings {
uint16_t header_mark;
uint16_t header_space;
uint16_t bit_mark;
uint16_t zero_space;
uint16_t one_space;
uint16_t gap_space;
bool gap_after_last; // Seiya ends on a bare mark and only puts the gap between repeats
};
// Kept in flash on ESP8266, where plain const objects referenced through a function would land in RAM
static const ToshibaTimings TOSHIBA_TIMINGS PROGMEM = {TOSHIBA_HEADER_MARK,
TOSHIBA_HEADER_SPACE,
TOSHIBA_BIT_MARK,
TOSHIBA_ZERO_SPACE,
TOSHIBA_ONE_SPACE,
TOSHIBA_GAP_SPACE,
true};
static const ToshibaTimings SEIYA_TIMINGS PROGMEM = {4630, 4450, 625, 490, 1570, 5830, false};
// The RAS-B13 remote family does not accept the generic timings; these are IRremoteESP8266's TOSHIBA_AC values
static const ToshibaTimings RAS_B10N3KV2_TIMINGS PROGMEM = {4400, 4300, 580, 490, 1600, 7400, true};
static ToshibaTimings timings_for(Model model) {
const ToshibaTimings *source = &TOSHIBA_TIMINGS;
switch (model) {
case MODEL_SEIYA:
source = &SEIYA_TIMINGS;
break;
case MODEL_RAS_B10N3KV2:
source = &RAS_B10N3KV2_TIMINGS;
break;
default:
break;
}
ToshibaTimings timings;
progmem_memcpy(&timings, source, sizeof(timings));
return timings;
}
// Seiya is the generic frame plus two bytes: a feature marker and a one-shot swing command
static constexpr uint8_t SEIYA_MESSAGE_LENGTH = 11;
static constexpr uint8_t SEIYA_FEATURE_MARKER = 0x21;
static constexpr uint8_t SEIYA_SWING_BOTH = 0x01;
static constexpr uint8_t SEIYA_SWING_OMNI = 0x02; // no ESPHome equivalent; treated as BOTH
static constexpr uint8_t SEIYA_SWING_VERTICAL = 0x08;
static constexpr uint8_t SEIYA_SWING_HORIZONTAL = 0x09;
static constexpr uint8_t SEIYA_SWING_NEUTRAL = 0x17; // no swing change in this frame
// RAS-2819T temperature codes for 18-30°C
static const uint8_t RAS_2819T_TEMP_CODES[] = {
0x10, // 18°C
@@ -404,12 +453,15 @@ void ToshibaClimate::setup() {
this->maximum_temperature_ = this->temperature_max_();
this->swing_modes_ = this->toshiba_swing_modes_();
// Ensure swing mode is always initialized to a valid value
// Ensure swing mode is always initialized to a valid value. OFF also stands for "not yet
// commanded" on models that cannot switch the swing off, such as Seiya.
if (this->swing_modes_.empty() || !this->swing_modes_.count(this->swing_mode)) {
// No swing support for this model or current swing mode not supported, reset to OFF
this->swing_mode = climate::CLIMATE_SWING_OFF;
}
// A restored swing mode is already in effect, so it must not go out as a fresh swing command
this->last_swing_mode_ = this->swing_mode;
// Ensure mode is valid - ESPHome should only use standard climate modes
if (this->mode != climate::CLIMATE_MODE_OFF && this->mode != climate::CLIMATE_MODE_HEAT &&
this->mode != climate::CLIMATE_MODE_COOL && this->mode != climate::CLIMATE_MODE_HEAT_COOL &&
@@ -451,8 +503,9 @@ void ToshibaClimate::transmit_state() {
}
void ToshibaClimate::transmit_generic_() {
const bool seiya = this->model_ == MODEL_SEIYA;
uint8_t message[16] = {0};
uint8_t message_length = 9;
const uint8_t message_length = seiya ? SEIYA_MESSAGE_LENGTH : 9;
// Header
message[0] = 0xf2;
@@ -473,6 +526,40 @@ void ToshibaClimate::transmit_generic_() {
message[5] = (temperature - static_cast<uint8_t>(TOSHIBA_GENERIC_TEMP_C_MIN)) << 4;
// Mode and fan
message[6] = this->encode_mode_fan_();
// Zero
message[7] = 0x00;
if (seiya) {
// Captured OFF frames carry no fan bits
if (this->mode == climate::CLIMATE_MODE_OFF) {
message[6] = TOSHIBA_MODE_OFF;
}
message[8] = SEIYA_FEATURE_MARKER;
// Swing is a momentary command, sent only when the mode changes, like the remote does
message[9] = SEIYA_SWING_NEUTRAL;
if (this->swing_mode != this->last_swing_mode_) {
message[9] = this->seiya_swing_code_();
this->last_swing_mode_ = this->swing_mode;
}
}
// The last byte is the xor of all bytes from [4]
for (uint8_t i = 4; i < message_length - 1; i++) {
message[message_length - 1] ^= message[i];
}
// Transmit
auto transmit = this->transmitter_->transmit();
auto *data = transmit.get_data();
this->encode_(data, message, message_length, 1);
transmit.perform();
}
uint8_t ToshibaClimate::encode_mode_fan_() const {
uint8_t mode;
switch (this->mode) {
case climate::CLIMATE_MODE_OFF:
@@ -523,27 +610,20 @@ void ToshibaClimate::transmit_generic_() {
fan = TOSHIBA_FAN_SPEED_AUTO;
break;
}
message[6] = fan | mode;
return fan | mode;
}
// Zero
message[7] = 0x00;
// If timers bit in the command is set, two extra bytes are added here
// If power bit is set in the command, one extra byte is added here
// The last byte is the xor of all bytes from [4]
for (uint8_t i = 4; i < 8; i++) {
message[8] ^= message[i];
uint8_t ToshibaClimate::seiya_swing_code_() const {
switch (this->swing_mode) {
case climate::CLIMATE_SWING_VERTICAL:
return SEIYA_SWING_VERTICAL;
case climate::CLIMATE_SWING_HORIZONTAL:
return SEIYA_SWING_HORIZONTAL;
case climate::CLIMATE_SWING_BOTH:
return SEIYA_SWING_BOTH;
default:
return SEIYA_SWING_NEUTRAL;
}
// Transmit
auto transmit = this->transmitter_->transmit();
auto *data = transmit.get_data();
this->encode_(data, message, message_length, 1);
transmit.perform();
}
void ToshibaClimate::transmit_rac_pt1411hwru_() {
@@ -1085,14 +1165,16 @@ bool ToshibaClimate::process_ras_2819t_command_(const remote_base::ToshibaAcData
bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) {
// Try modern ToshibaAcProtocol decoder first (handles RAS-2819T and potentially others)
remote_base::ToshibaAcProtocol toshiba_protocol;
auto decode_result = toshiba_protocol.decode(data);
if (this->model_ != MODEL_SEIYA) {
remote_base::ToshibaAcProtocol toshiba_protocol;
auto decode_result = toshiba_protocol.decode(data);
if (decode_result.has_value()) {
auto toshiba_data = decode_result.value();
// Validate and process RAS-2819T commands
if (is_valid_ras_2819t_command(toshiba_data.rc_code_1, toshiba_data.rc_code_2)) {
return this->process_ras_2819t_command_(toshiba_data);
if (decode_result.has_value()) {
auto toshiba_data = decode_result.value();
// Validate and process RAS-2819T commands
if (is_valid_ras_2819t_command(toshiba_data.rc_code_1, toshiba_data.rc_code_2)) {
return this->process_ras_2819t_command_(toshiba_data);
}
}
}
@@ -1101,7 +1183,8 @@ bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) {
uint8_t message_length = TOSHIBA_HEADER_LENGTH, temperature_code = 0;
// Validate header
if (!data.expect_item(TOSHIBA_HEADER_MARK, TOSHIBA_HEADER_SPACE)) {
const ToshibaTimings timings = timings_for(this->model_);
if (!data.expect_item(timings.header_mark, timings.header_space)) {
return false;
}
// Read incoming bits into buffer
@@ -1117,6 +1200,10 @@ bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) {
return false;
} else {
// First checksum was valid so continue receiving the remaining bits
// The shortest frame is 9 bytes (length byte 3); longer ones must still fit the buffer
if (message[2] < 3 || message[2] > sizeof(message) - TOSHIBA_HEADER_LENGTH - 2) {
return false;
}
message_length = message[2] + 2;
}
// Decode the remaining bytes
@@ -1276,59 +1363,11 @@ bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) {
return false;
}
// Get the mode
switch (message[6] & 0x0F) {
case TOSHIBA_MODE_OFF:
this->mode = climate::CLIMATE_MODE_OFF;
break;
case TOSHIBA_MODE_COOL:
this->mode = climate::CLIMATE_MODE_COOL;
break;
case TOSHIBA_MODE_DRY:
this->mode = climate::CLIMATE_MODE_DRY;
break;
case TOSHIBA_MODE_FAN_ONLY:
this->mode = climate::CLIMATE_MODE_FAN_ONLY;
break;
case TOSHIBA_MODE_HEAT:
this->mode = climate::CLIMATE_MODE_HEAT;
break;
case TOSHIBA_MODE_AUTO:
default:
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
this->decode_mode_fan_temperature_(message);
// The marker byte tells a Seiya frame from another checksum-valid 11 byte frame within receiver tolerance
if (this->model_ == MODEL_SEIYA && message_length == SEIYA_MESSAGE_LENGTH && message[8] == SEIYA_FEATURE_MARKER) {
this->seiya_decode_swing_(message[9]);
}
// Get the fan mode
switch (message[6] & 0xF0) {
case TOSHIBA_FAN_SPEED_QUIET:
this->fan_mode = climate::CLIMATE_FAN_QUIET;
break;
case TOSHIBA_FAN_SPEED_1:
this->fan_mode = climate::CLIMATE_FAN_LOW;
break;
case TOSHIBA_FAN_SPEED_3:
this->fan_mode = climate::CLIMATE_FAN_MEDIUM;
break;
case TOSHIBA_FAN_SPEED_5:
this->fan_mode = climate::CLIMATE_FAN_HIGH;
break;
case TOSHIBA_FAN_SPEED_AUTO:
default:
this->fan_mode = climate::CLIMATE_FAN_AUTO;
break;
}
// Get the target temperature
this->target_temperature = (message[5] >> 4) + TOSHIBA_GENERIC_TEMP_C_MIN;
}
this->publish_state();
@@ -1337,31 +1376,36 @@ bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) {
void ToshibaClimate::encode_(remote_base::RemoteTransmitData *data, const uint8_t *message, const uint8_t nbytes,
const uint8_t repeat) {
const ToshibaTimings timings = timings_for(this->model_);
data->set_carrier_frequency(TOSHIBA_CARRIER_FREQUENCY);
for (uint8_t copy = 0; copy <= repeat; copy++) {
data->item(TOSHIBA_HEADER_MARK, TOSHIBA_HEADER_SPACE);
data->item(timings.header_mark, timings.header_space);
for (uint8_t byte = 0; byte < nbytes; byte++) {
for (uint8_t bit = 0; bit < 8; bit++) {
data->mark(TOSHIBA_BIT_MARK);
data->mark(timings.bit_mark);
if (message[byte] & (1 << (7 - bit))) {
data->space(TOSHIBA_ONE_SPACE);
data->space(timings.one_space);
} else {
data->space(TOSHIBA_ZERO_SPACE);
data->space(timings.zero_space);
}
}
}
data->item(TOSHIBA_BIT_MARK, TOSHIBA_GAP_SPACE);
data->mark(timings.bit_mark);
if (copy < repeat || timings.gap_after_last) {
data->space(timings.gap_space);
}
}
}
bool ToshibaClimate::decode_(remote_base::RemoteReceiveData *data, uint8_t *message, const uint8_t nbytes) {
const ToshibaTimings timings = timings_for(this->model_);
for (uint8_t byte = 0; byte < nbytes; byte++) {
for (uint8_t bit = 0; bit < 8; bit++) {
if (data->expect_item(TOSHIBA_BIT_MARK, TOSHIBA_ONE_SPACE)) {
if (data->expect_item(timings.bit_mark, timings.one_space)) {
message[byte] |= 1 << (7 - bit);
} else if (data->expect_item(TOSHIBA_BIT_MARK, TOSHIBA_ZERO_SPACE)) {
} else if (data->expect_item(timings.bit_mark, timings.zero_space)) {
message[byte] &= static_cast<uint8_t>(~(1 << (7 - bit)));
} else {
return false;
@@ -1371,4 +1415,80 @@ bool ToshibaClimate::decode_(remote_base::RemoteReceiveData *data, uint8_t *mess
return true;
}
void ToshibaClimate::decode_mode_fan_temperature_(const uint8_t *message) {
switch (message[6] & 0x0F) {
case TOSHIBA_MODE_OFF:
this->mode = climate::CLIMATE_MODE_OFF;
break;
case TOSHIBA_MODE_COOL:
this->mode = climate::CLIMATE_MODE_COOL;
break;
case TOSHIBA_MODE_DRY:
this->mode = climate::CLIMATE_MODE_DRY;
break;
case TOSHIBA_MODE_FAN_ONLY:
this->mode = climate::CLIMATE_MODE_FAN_ONLY;
break;
case TOSHIBA_MODE_HEAT:
this->mode = climate::CLIMATE_MODE_HEAT;
break;
case TOSHIBA_MODE_AUTO:
default:
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
}
// Speeds 2 and 4 are not exposed; report the nearest mode
switch (message[6] & 0xF0) {
case TOSHIBA_FAN_SPEED_QUIET:
this->fan_mode = climate::CLIMATE_FAN_QUIET;
break;
case TOSHIBA_FAN_SPEED_1:
this->fan_mode = climate::CLIMATE_FAN_LOW;
break;
case TOSHIBA_FAN_SPEED_2:
case TOSHIBA_FAN_SPEED_3:
this->fan_mode = climate::CLIMATE_FAN_MEDIUM;
break;
case TOSHIBA_FAN_SPEED_4:
case TOSHIBA_FAN_SPEED_5:
this->fan_mode = climate::CLIMATE_FAN_HIGH;
break;
case TOSHIBA_FAN_SPEED_AUTO:
default:
this->fan_mode = climate::CLIMATE_FAN_AUTO;
break;
}
this->target_temperature = (message[5] >> 4) + TOSHIBA_GENERIC_TEMP_C_MIN;
}
void ToshibaClimate::seiya_decode_swing_(uint8_t code) {
// A momentary command: the neutral value on an ordinary update means no change, not swing off
switch (code) {
case SEIYA_SWING_BOTH:
case SEIYA_SWING_OMNI:
this->swing_mode = climate::CLIMATE_SWING_BOTH;
break;
case SEIYA_SWING_VERTICAL:
this->swing_mode = climate::CLIMATE_SWING_VERTICAL;
break;
case SEIYA_SWING_HORIZONTAL:
this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL;
break;
default:
return;
}
// Keep the tracker in sync so the received command is not sent back out
this->last_swing_mode_ = this->swing_mode;
}
} // namespace esphome::toshiba
+17 -4
View File
@@ -11,6 +11,8 @@ enum Model {
MODEL_RAC_PT1411HWRU_C = 1, // Temperature range is from 16 to 30
MODEL_RAC_PT1411HWRU_F = 2, // Temperature range is from 16 to 30
MODEL_RAS_2819T = 3, // RAS-2819T protocol variant, temperature range 18 to 30
MODEL_SEIYA = 4, // Seiya family (RAS-B13E2KVG-E etc.), generic frame with a swing byte
MODEL_RAS_B10N3KV2 = 5, // RAS-B13 remote family, generic frame with IRremoteESP8266's TOSHIBA_AC timings
};
// Supported temperature ranges
@@ -50,7 +52,7 @@ class ToshibaClimate final : public climate_ir::ClimateIR {
bool on_receive(remote_base::RemoteReceiveData data) override;
private:
// RAS-2819T state tracking for swing mode optimization
// Last swing mode sent or received, for models whose swing is a one-shot command
climate::ClimateSwingMode last_swing_mode_{climate::CLIMATE_SWING_OFF};
climate::ClimateMode last_mode_{climate::CLIMATE_MODE_OFF};
optional<climate::ClimateFanMode> last_fan_mode_{};
@@ -71,13 +73,24 @@ class ToshibaClimate final : public climate_ir::ClimateIR {
return TOSHIBA_GENERIC_TEMP_C_MAX; // Default to GENERIC for unknown models
}
climate::ClimateSwingModeMask toshiba_swing_modes_() {
return (this->model_ == MODEL_GENERIC)
? climate::ClimateSwingModeMask()
: climate::ClimateSwingModeMask{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL};
if (this->model_ == MODEL_GENERIC || this->model_ == MODEL_RAS_B10N3KV2)
return climate::ClimateSwingModeMask();
if (this->model_ == MODEL_SEIYA) {
// No captured code stops the swing, so OFF is not offered; the remote only selects a direction
return climate::ClimateSwingModeMask{climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL,
climate::CLIMATE_SWING_BOTH};
}
return climate::ClimateSwingModeMask{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL};
}
void encode_(remote_base::RemoteTransmitData *data, const uint8_t *message, uint8_t nbytes, uint8_t repeat);
bool decode_(remote_base::RemoteReceiveData *data, uint8_t *message, uint8_t nbytes);
// Shared by every model that uses the F2 0D frame
uint8_t encode_mode_fan_() const;
void decode_mode_fan_temperature_(const uint8_t *message);
uint8_t seiya_swing_code_() const;
void seiya_decode_swing_(uint8_t code);
Model model_;
};
+21 -4
View File
@@ -4,6 +4,7 @@ from esphome.components import time, uart
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_SENSOR_DATAPOINT, CONF_TIME_ID
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType
DEPENDENCIES = ["uart"]
DOMAIN = "tuya"
@@ -13,6 +14,7 @@ CONF_IGNORE_MCU_UPDATE_ON_DATAPOINTS = "ignore_mcu_update_on_datapoints"
CONF_ON_DATAPOINT_UPDATE = "on_datapoint_update"
CONF_DATAPOINT_TYPE = "datapoint_type"
CONF_STATUS_PIN = "status_pin"
CONF_FORCE_TIME_SYNC = "force_time_sync"
tuya_ns = cg.esphome_ns.namespace("tuya")
TuyaDatapointType = tuya_ns.enum("TuyaDatapointType", is_class=True)
@@ -43,11 +45,23 @@ DATAPOINT_TYPES = {
CONF_TUYA_ID = "tuya_id"
CONFIG_SCHEMA = (
def _validate_time_options(config: ConfigType) -> ConfigType:
if config[CONF_FORCE_TIME_SYNC] and CONF_TIME_ID not in config:
raise cv.Invalid(
f"{CONF_FORCE_TIME_SYNC} requires {CONF_TIME_ID}",
path=[CONF_FORCE_TIME_SYNC],
)
return config
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(Tuya),
cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
cv.Optional(CONF_FORCE_TIME_SYNC, default=False): cv.boolean,
cv.Optional(CONF_IGNORE_MCU_UPDATE_ON_DATAPOINTS): cv.ensure_list(
cv.uint8_t
),
@@ -63,7 +77,8 @@ CONFIG_SCHEMA = (
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(uart.UART_DEVICE_SCHEMA)
.extend(uart.UART_DEVICE_SCHEMA),
_validate_time_options,
)
@@ -71,9 +86,11 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
if CONF_TIME_ID in config:
time_ = await cg.get_variable(config[CONF_TIME_ID])
if (time_id := config.get(CONF_TIME_ID)) is not None:
time_ = await cg.get_variable(time_id)
cg.add(var.set_time_id(time_))
if config[CONF_FORCE_TIME_SYNC]:
cg.add(var.set_force_time_sync(True))
if CONF_STATUS_PIN in config:
status_pin_ = await cg.gpio_pin_expression(config[CONF_STATUS_PIN])
cg.add(var.set_status_pin(status_pin_))
+38 -8
View File
@@ -23,11 +23,13 @@ ESPHOME_LOG_TAG(TAG, "tuya");
static constexpr uint32_t HEARTBEAT_INTERVAL_ID = 0;
static constexpr uint32_t WIFI_STATUS_INTERVAL_ID = 1;
static constexpr uint32_t DATAPOINT_DUMP_TIMEOUT_ID = 2;
static constexpr uint32_t FORCE_TIME_SYNC_INTERVAL_ID = 3;
static const int COMMAND_DELAY = 10;
static const int RECEIVE_TIMEOUT = 300;
static const int MAX_RETRIES = 5;
// Max bytes to log for datapoint values (larger values are truncated)
static constexpr size_t MAX_DATAPOINT_LOG_BYTES = 16;
static constexpr uint32_t FORCE_TIME_SYNC_CHECK_INTERVAL = 1000;
static bool network_is_connected() {
#ifdef USE_NETWORK
@@ -39,6 +41,13 @@ static bool network_is_connected() {
void Tuya::setup() {
this->set_interval(HEARTBEAT_INTERVAL_ID, 15000, [this] { this->send_empty_command_(TuyaCommandType::HEARTBEAT); });
#ifdef USE_TIME
if (this->force_time_sync_) {
this->register_local_time_sync_callback_();
this->set_interval(FORCE_TIME_SYNC_INTERVAL_ID, FORCE_TIME_SYNC_CHECK_INTERVAL,
[this] { this->check_force_time_sync_(); });
}
#endif
if (this->status_pin_ != nullptr) {
this->status_pin_->digital_write(false);
}
@@ -99,6 +108,11 @@ void Tuya::dump_config() {
}
LOG_PIN(" Status Pin: ", this->status_pin_);
ESP_LOGCONFIG(TAG, " Product: '%s'", this->product_.c_str());
#ifdef USE_TIME
if (this->time_id_ != nullptr) {
ESP_LOGCONFIG(TAG, " Force time sync: %s", YESNO(this->force_time_sync_));
}
#endif
}
bool Tuya::validate_message_() {
@@ -306,12 +320,7 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff
#ifdef USE_TIME
if (this->time_id_ != nullptr) {
this->send_local_time_();
if (!this->time_sync_callback_registered_) {
// tuya mcu supports time, so we let them know when our time changed
this->time_id_->add_on_time_sync_callback([this] { this->send_local_time_(); });
this->time_sync_callback_registered_ = true;
}
this->register_local_time_sync_callback_();
} else
#endif
{
@@ -616,7 +625,23 @@ void Tuya::send_wifi_status_() {
}
#ifdef USE_TIME
void Tuya::send_local_time_() {
void Tuya::register_local_time_sync_callback_() {
if (this->time_sync_callback_registered_) {
return;
}
this->time_id_->add_on_time_sync_callback([this] { this->send_local_time_(); });
this->time_sync_callback_registered_ = true;
}
void Tuya::check_force_time_sync_() {
ESPTime now = this->time_id_->now();
if (!now.is_valid() || now.minute == this->last_force_time_sync_minute_)
return;
this->last_force_time_sync_minute_ = now.minute;
this->send_local_time_(true);
}
void Tuya::send_local_time_(bool periodic) {
std::vector<uint8_t> payload;
ESPTime now = this->time_id_->now();
if (now.is_valid()) {
@@ -631,7 +656,12 @@ void Tuya::send_local_time_() {
if (day_of_week == 0) {
day_of_week = 7;
}
ESP_LOGD(TAG, "Sending local time");
// The forced send repeats every minute, so it stays out of the debug log
if (periodic) {
ESP_LOGV(TAG, "Sending local time");
} else {
ESP_LOGD(TAG, "Sending local time");
}
payload = std::vector<uint8_t>{0x01, year, month, day_of_month, hour, minute, second, day_of_week};
} else {
// By spec we need to notify MCU that the time was not obtained if this is a response to a query
+6 -1
View File
@@ -113,6 +113,7 @@ class Tuya final : public Component, public uart::UARTDevice {
TuyaInitState get_init_state();
#ifdef USE_TIME
void set_time_id(time::RealTimeClock *time_id) { this->time_id_ = time_id; }
void set_force_time_sync(bool force_time_sync) { this->force_time_sync_ = force_time_sync; }
#endif
void add_ignore_mcu_update_on_datapoints(uint8_t ignore_mcu_update_on_datapoints) {
this->ignore_mcu_update_on_datapoints_.push_back(ignore_mcu_update_on_datapoints);
@@ -144,9 +145,13 @@ class Tuya final : public Component, public uart::UARTDevice {
uint8_t get_wifi_rssi_();
#ifdef USE_TIME
void send_local_time_();
void register_local_time_sync_callback_();
void check_force_time_sync_();
void send_local_time_(bool periodic = false);
void send_gmt_time_();
time::RealTimeClock *time_id_{nullptr};
bool force_time_sync_{false};
uint8_t last_force_time_sync_minute_{0xFF}; // 0xFF: no minute seen yet
bool time_sync_callback_registered_{false};
bool gmt_time_sync_callback_registered_{false};
#endif
+23 -5
View File
@@ -13,7 +13,14 @@ from esphome.components.esp32.const import (
VARIANT_ESP32S31,
)
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_INTERNAL, CONF_MODEL, CONF_NAME, CONF_ON_START
from esphome.const import (
CONF_ID,
CONF_INTERNAL,
CONF_MODEL,
CONF_NAME,
CONF_ON_START,
CONF_POLLING_INTERVAL,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.types import ConfigType
@@ -25,6 +32,7 @@ from .const import (
CONF_POWER_SOURCE,
CONF_REPORT,
CONF_ROUTER,
CONF_SLEEPY,
CONF_USE_DEVICE_TYPE,
CONF_WIPE_ON_BOOT,
KEY_ZIGBEE,
@@ -35,7 +43,6 @@ from .const import (
from .const_zephyr import (
CONF_IEEE802154_VENDOR_OUI,
CONF_MAX_EP_NUMBER_ZEPHYR,
CONF_SLEEPY,
CONF_ZIGBEE_ID,
KEY_EP_NUMBER,
)
@@ -128,8 +135,11 @@ NUMBER_SCHEMA = (
def _validate_router_sleepy(config: ConfigType) -> ConfigType:
if config.get(CONF_ROUTER) and config.get(CONF_SLEEPY):
raise cv.Invalid("router and sleepy are mutually exclusive")
if config.get(CONF_ROUTER):
if config.get(CONF_SLEEPY):
raise cv.Invalid("router and sleepy are mutually exclusive")
if config.get(CONF_POLLING_INTERVAL):
raise cv.Invalid(f"{CONF_POLLING_INTERVAL} is only valid for end devices.")
return config
@@ -160,9 +170,17 @@ CONFIG_SCHEMA = cv.All(
),
cv.requires_component("nrf52"),
),
cv.OnlyWith(CONF_SLEEPY, "nrf52", default=False): cv.All(
cv.Optional(CONF_SLEEPY, default=False): cv.All(
cv.boolean,
),
cv.Optional(CONF_POLLING_INTERVAL): cv.All(
cv.requires_component(ESP32_DOMAIN),
cv.positive_time_period_milliseconds,
cv.Range(
min=cv.TimePeriodMilliseconds(milliseconds=200),
max=cv.TimePeriodMinutes(minutes=60), # 1 hour
),
),
}
).extend(cv.COMPONENT_SCHEMA),
_validate_router_sleepy,
+1
View File
@@ -72,6 +72,7 @@ CONF_WIPE_ON_BOOT = "wipe_on_boot"
CONF_REPORT = "report"
CONF_ROUTER = "router"
CONF_POWER_SOURCE = "power_source"
CONF_SLEEPY = "sleepy"
CONF_USE_DEVICE_TYPE = "use_device_type"
POWER_SOURCE = {
"UNKNOWN": 0x00, # ZB_ZCL_BASIC_POWER_SOURCE_UNKNOWN
@@ -4,7 +4,6 @@ CONF_ZIGBEE_BINARY_SENSOR = "zigbee_binary_sensor"
CONF_ZIGBEE_SENSOR = "zigbee_sensor"
CONF_ZIGBEE_SWITCH = "zigbee_switch"
CONF_ZIGBEE_NUMBER = "zigbee_number"
CONF_SLEEPY = "sleepy"
CONF_IEEE802154_VENDOR_OUI = "ieee802154_vendor_oui"
# Keys for CORE.data storage
+24 -13
View File
@@ -319,7 +319,7 @@ void ZigbeeComponent::setup() {
#else
esp_zigbee_zed_config_s zb_zed_cfg = {
.ed_timeout = EZB_NWK_ED_TIMEOUT_64MIN,
.keep_alive = ED_KEEP_ALIVE,
.keep_alive = this->keep_alive_,
};
device_config.zed_config = zb_zed_cfg;
#endif
@@ -339,6 +339,8 @@ void ZigbeeComponent::setup() {
#ifdef CONFIG_ZB_ZCZR
ezb_bdb_set_router_rejoin_required(true);
#else
ezb_nwk_set_rx_on_when_idle(!this->sleepy_); // if sleepy, disable RX when idle
#endif
ezb_aps_secur_enable_distributed_security(false);
@@ -419,18 +421,27 @@ void ZigbeeComponent::loop() {
void ZigbeeComponent::dump_config() {
if (esp_zigbee_lock_acquire(10 / portTICK_PERIOD_MS)) {
ESP_LOGCONFIG(TAG,
"Zigbee\n"
" Model: %.*s\n"
" Router: %s\n"
" Device is joined to the network: %s\n"
" Current channel: %d\n"
" Short addr: 0x%04X\n"
" Short pan id: 0x%04X",
this->basic_cluster_data_.model[0],
reinterpret_cast<const char *>(this->basic_cluster_data_.model + 1),
YESNO(this->device_role_ == EZB_NWK_DEVICE_TYPE_ROUTER), YESNO(ezb_bdb_dev_joined()),
ezb_nwk_get_current_channel(), ezb_nwk_get_short_address(), ezb_nwk_get_panid());
ESP_LOGCONFIG(
TAG,
"Zigbee\n"
" Model: %.*s\n"
#ifdef CONFIG_ZB_ZCZR
" Router: %s\n"
#else
" Sleepy end device: %s\n"
" Poll interval: %" PRIu32 " ms\n"
#endif
" Device is joined to the network: %s\n"
" Current channel: %d\n"
" Short addr: 0x%04X\n"
" Short pan id: 0x%04X",
this->basic_cluster_data_.model[0], reinterpret_cast<const char *>(this->basic_cluster_data_.model + 1),
#ifdef CONFIG_ZB_ZCZR
YESNO(this->device_role_ == EZB_NWK_DEVICE_TYPE_ROUTER),
#else
YESNO(this->sleepy_), this->keep_alive_,
#endif
YESNO(ezb_bdb_dev_joined()), ezb_nwk_get_current_channel(), ezb_nwk_get_short_address(), ezb_nwk_get_panid());
esp_zigbee_lock_release();
} else {
ESP_LOGCONFIG(TAG,
+4 -1
View File
@@ -20,7 +20,6 @@
namespace esphome::zigbee {
/* Zigbee configuration */
static const uint16_t ED_KEEP_ALIVE = 3000; /* 3000 millisecond */
static const uint8_t MAX_CHILDREN = 10;
static const uint32_t EZB_PRIMARY_CHANNEL_MASK = 0x07FFF800U; /* channels 11-26 */
static constexpr uint8_t MAX_ZB_QUEUE_SIZE = 32;
@@ -43,6 +42,8 @@ class ZigbeeComponent final : public Component {
void loop() override;
void dump_config() override;
void set_sleepy(bool sleepy) { this->sleepy_ = sleepy; }
void set_keep_alive(uint32_t keep_alive) { this->keep_alive_ = keep_alive; }
void set_basic_cluster(const char *model, const char *manufacturer, uint8_t power_source);
void add_cluster(uint8_t endpoint_id, uint16_t cluster_id, uint8_t role);
void create_default_cluster(uint8_t endpoint_id, uint16_t device_id);
@@ -73,6 +74,8 @@ class ZigbeeComponent final : public Component {
bool is_joined() { return this->joined_; }
protected:
bool sleepy_{false};
uint32_t keep_alive_{3000};
struct {
uint8_t *model;
uint8_t *manufacturer;
@@ -24,6 +24,7 @@ from esphome.const import (
CONF_MAX_LENGTH,
CONF_MODEL,
CONF_NAME,
CONF_POLLING_INTERVAL,
CONF_TYPE,
CONF_UNIT_OF_MEASUREMENT,
CONF_VALUE,
@@ -45,6 +46,7 @@ from .const import (
CONF_POWER_SOURCE,
CONF_REPORT,
CONF_ROUTER,
CONF_SLEEPY,
CONF_USE_DEVICE_TYPE,
KEY_ZIGBEE,
POWER_SOURCE,
@@ -412,6 +414,11 @@ async def esp32_to_code(config: ConfigType) -> "MockObj":
# setup zigbee components
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
if config.get(CONF_SLEEPY):
cg.add(var.set_sleepy(True))
if (keep_alive := config.get(CONF_POLLING_INTERVAL, 3000)) != 3000:
cg.add(var.set_keep_alive(keep_alive))
cg.add(
var.set_basic_cluster(
config[CONF_MODEL],
+1 -1
View File
@@ -24,6 +24,7 @@ from .const import (
BACNET_UNITS,
CONF_POWER_SOURCE,
CONF_ROUTER,
CONF_SLEEPY,
CONF_WIPE_ON_BOOT,
KEY_ZIGBEE,
POWER_SOURCE,
@@ -35,7 +36,6 @@ from .const import (
)
from .const_zephyr import (
CONF_IEEE802154_VENDOR_OUI,
CONF_SLEEPY,
CONF_ZIGBEE_BINARY_SENSOR,
CONF_ZIGBEE_ID,
CONF_ZIGBEE_NUMBER,

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