mirror of
https://github.com/esphome/esphome.git
synced 2026-10-15 02:32:49 +00:00
Merge branch 'dev' into ota-udp-transport
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -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 \
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,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"
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -9,6 +9,8 @@ BYTE_ORDER_BIG = "big_endian"
|
||||
CONF_ACCELEROMETER_ODR = "accelerometer_odr"
|
||||
CONF_ACCELEROMETER_RANGE = "accelerometer_range"
|
||||
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"
|
||||
@@ -71,3 +73,4 @@ KEY_METADATA = "metadata"
|
||||
|
||||
UNIT_AMPERE_HOUR = "Ah"
|
||||
UNIT_COUNTS = "#"
|
||||
UNIT_MILLIAMPERE_HOUR = "mAh"
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
@@ -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(
|
||||
@@ -3138,7 +3222,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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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_();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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_{};
|
||||
|
||||
@@ -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_; }
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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]))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}>")',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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."));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -137,6 +137,7 @@
|
||||
#define USE_LVGL_TOUCHSCREEN
|
||||
#define USE_MDNS
|
||||
#define USE_MDNS_STORE_SERVICES
|
||||
#define I2C_PORT_FREQUENCY_COUNT 1
|
||||
#define MDNS_SERVICE_COUNT 3
|
||||
#define USE_MDNS_DYNAMIC_TXT
|
||||
#define MDNS_DYNAMIC_TXT_COUNT 2
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""ESP-IDF direct build API for ESPHome."""
|
||||
|
||||
import contextlib
|
||||
from dataclasses import dataclass, field
|
||||
import fnmatch
|
||||
import json
|
||||
@@ -124,6 +125,8 @@ def _get_idf_env(version: str | None = None) -> dict[str, str]:
|
||||
env_cache[version] = os.environ.copy()
|
||||
# Do not leak PYTHONPATH into child env
|
||||
env_cache[version].pop("PYTHONPATH", None)
|
||||
# Do not let an inherited IDF_TARGET override the variant
|
||||
env_cache[version].pop("IDF_TARGET", None)
|
||||
|
||||
# Use provided IDF framework if available
|
||||
if _esphome_manages_idf():
|
||||
@@ -227,6 +230,8 @@ FILTER_IDF_LINES: list[str] = [
|
||||
# click's boolean spellings, which idf.py applies to IDF_CCACHE_ENABLE.
|
||||
_CLICK_TRUE = frozenset({"1", "true", "t", "yes", "y", "on"})
|
||||
_CMAKECACHE_LINE = re.compile(r"^([^#/:=]+):([^:=]+)=(.*)$")
|
||||
_SDKCONFIG_TARGET_LINE = re.compile(r'^CONFIG_IDF_TARGET="([^"]*)"$', re.MULTILINE)
|
||||
_TOOLCHAIN_FILE_TARGET = re.compile(r"toolchain-(?:clang-)?(\w+)\.cmake$")
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
@@ -310,6 +315,50 @@ def tree_skips_bootloader(build_dir: Path) -> bool:
|
||||
return cache.get(SKIP_BOOTLOADER_DEFINE) == "1"
|
||||
|
||||
|
||||
def _foreign_idf_target(target: str) -> str | None:
|
||||
"""Return another IDF target recorded in the CMake caches or sdkconfig."""
|
||||
build_dir = _build_dir()
|
||||
sdkconfig = CORE.relative_build_path(f"sdkconfig.{CORE.name}")
|
||||
found = []
|
||||
for cache_path in (
|
||||
build_dir / "CMakeCache.txt",
|
||||
build_dir / "bootloader" / "CMakeCache.txt",
|
||||
):
|
||||
with contextlib.suppress(OSError, ValueError):
|
||||
cache = _parse_cmakecache(cache_path)
|
||||
found.append(cache.get("IDF_TARGET"))
|
||||
if m := _TOOLCHAIN_FILE_TARGET.search(
|
||||
cache.get("CMAKE_TOOLCHAIN_FILE", "")
|
||||
):
|
||||
found.append(m.group(1))
|
||||
with contextlib.suppress(OSError, ValueError):
|
||||
if m := _SDKCONFIG_TARGET_LINE.search(sdkconfig.read_text(encoding="utf-8")):
|
||||
found.append(m.group(1))
|
||||
return next((t for t in found if t and t != target), None)
|
||||
|
||||
|
||||
def _clean_foreign_target_tree() -> None:
|
||||
"""Clean a build configured for another IDF target, like idf.py set-target."""
|
||||
variant = CORE.data.get(KEY_ESP32, {}).get(KEY_VARIANT)
|
||||
if variant is None:
|
||||
return
|
||||
target = variant_to_idf_target(variant)
|
||||
if (old := _foreign_idf_target(target)) is None:
|
||||
return
|
||||
from esphome.writer import clean_build
|
||||
|
||||
_LOGGER.info(
|
||||
"IDF target changed from %s to %s, cleaning build files...", old, target
|
||||
)
|
||||
clean_build(clear_pio_cache=False)
|
||||
if os.environ.get("ESPHOME_SKIP_CLEAN_BUILD"):
|
||||
return
|
||||
# Restart sdkconfig from ESPHome's options
|
||||
internal = CORE.relative_build_path(f"sdkconfig.{CORE.name}.esphomeinternal")
|
||||
if internal.is_file():
|
||||
shutil.copyfile(internal, CORE.relative_build_path(f"sdkconfig.{CORE.name}"))
|
||||
|
||||
|
||||
def _skip_bootloader() -> bool:
|
||||
"""Whether this tree should not build a bootloader at all; per-run memo."""
|
||||
cache = _cache()
|
||||
@@ -804,6 +853,7 @@ def run_compile(config, verbose: bool) -> int:
|
||||
3. Run full build
|
||||
"""
|
||||
jobs = _build_jobs(config)
|
||||
_clean_foreign_target_tree()
|
||||
if need_reconfigure():
|
||||
if (rc := _configure_project(verbose)) != 0:
|
||||
return rc
|
||||
|
||||
@@ -21,6 +21,7 @@ from dataclasses import dataclass
|
||||
import hashlib
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
@@ -83,7 +84,9 @@ def show_disk_space_if_ci(esphome_command: str) -> None:
|
||||
print("=" * 80)
|
||||
# Use sys.stdout.flush() to ensure output appears immediately
|
||||
sys.stdout.flush()
|
||||
subprocess.run(["df", "-h"], check=False, stdout=sys.stdout, stderr=sys.stderr)
|
||||
# Windows has no df
|
||||
if df := shutil.which("df"):
|
||||
subprocess.run([df, "-h"], check=False, stdout=sys.stdout, stderr=sys.stderr)
|
||||
print("=" * 80 + "\n")
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
@@ -111,6 +111,245 @@ def test_esp32_config(
|
||||
assert VARIANT_FRIENDLY[variant].lower() in config["board"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"sdkconfig_options",
|
||||
"cpu_frequency",
|
||||
"expected_frequency",
|
||||
"warns",
|
||||
"variant",
|
||||
"framework_type",
|
||||
),
|
||||
[
|
||||
pytest.param(
|
||||
{
|
||||
"CONFIG_ESP32_DEFAULT_CPU_FREQ_160": "y",
|
||||
"CONFIG_ESP32_DEFAULT_CPU_FREQ_MHZ": "160",
|
||||
},
|
||||
None,
|
||||
"160MHZ",
|
||||
True,
|
||||
VARIANT_ESP32,
|
||||
"esp-idf",
|
||||
id="legacy-frequency-options-with-default",
|
||||
),
|
||||
pytest.param(
|
||||
{"CONFIG_ESP32_DEFAULT_CPU_FREQ_160": "y"},
|
||||
"160MHz",
|
||||
"160MHZ",
|
||||
False,
|
||||
VARIANT_ESP32,
|
||||
"esp-idf",
|
||||
id="explicit-cpu-frequency",
|
||||
),
|
||||
pytest.param(
|
||||
{"CONFIG_ESP32_DEFAULT_CPU_FREQ_MHZ": "160"},
|
||||
None,
|
||||
"160MHZ",
|
||||
True,
|
||||
VARIANT_ESP32,
|
||||
"esp-idf",
|
||||
id="legacy-scalar-frequency",
|
||||
),
|
||||
pytest.param(
|
||||
{"CONFIG_ESP32_DEFAULT_CPU_FREQ_160": "y"},
|
||||
None,
|
||||
"160MHZ",
|
||||
True,
|
||||
VARIANT_ESP32,
|
||||
"esp-idf",
|
||||
id="legacy-frequency-choice",
|
||||
),
|
||||
pytest.param(
|
||||
{"CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_160": "y"},
|
||||
None,
|
||||
"160MHZ",
|
||||
True,
|
||||
VARIANT_ESP32,
|
||||
"esp-idf",
|
||||
id="current-idf-frequency-choice",
|
||||
),
|
||||
pytest.param(
|
||||
{"CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_160": "n"},
|
||||
None,
|
||||
"240MHZ",
|
||||
False,
|
||||
VARIANT_ESP32,
|
||||
"esp-idf",
|
||||
id="disabled-frequency-choice",
|
||||
),
|
||||
pytest.param(
|
||||
{"CONFIG_ESP32_DEFAULT_CPU_FREQ_MHZ_240": "n"},
|
||||
"240MHz",
|
||||
"240MHZ",
|
||||
False,
|
||||
VARIANT_ESP32,
|
||||
"esp-idf",
|
||||
id="ignore-unrecognized-legacy-disabled-choice",
|
||||
),
|
||||
pytest.param(
|
||||
{"CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ": "160"},
|
||||
None,
|
||||
"160MHZ",
|
||||
True,
|
||||
VARIANT_ESP32C3,
|
||||
"esp-idf",
|
||||
id="current-scalar-frequency",
|
||||
),
|
||||
pytest.param(
|
||||
{"CONFIG_ESP32S2_DEFAULT_CPU_FREQ_MHZ": "160"},
|
||||
None,
|
||||
"160MHZ",
|
||||
True,
|
||||
VARIANT_ESP32S2,
|
||||
"esp-idf",
|
||||
id="s2-legacy-scalar-frequency",
|
||||
),
|
||||
pytest.param(
|
||||
{"CONFIG_ESP32S3_DEFAULT_CPU_FREQ_160": "y"},
|
||||
None,
|
||||
"160MHZ",
|
||||
True,
|
||||
VARIANT_ESP32S3,
|
||||
"esp-idf",
|
||||
id="s3-legacy-choice-frequency",
|
||||
),
|
||||
pytest.param(
|
||||
{"CONFIG_ESP32C3_DEFAULT_CPU_FREQ_80": "y"},
|
||||
None,
|
||||
"80MHZ",
|
||||
True,
|
||||
VARIANT_ESP32C3,
|
||||
"esp-idf",
|
||||
id="c3-legacy-choice-frequency",
|
||||
),
|
||||
pytest.param(
|
||||
{"CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ": "160"},
|
||||
None,
|
||||
"160MHZ",
|
||||
True,
|
||||
VARIANT_ESP32,
|
||||
"arduino",
|
||||
id="arduino-framework-current-scalar-frequency",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_sdkconfig_cpu_frequency_is_honored(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
sdkconfig_options: dict[str, str],
|
||||
cpu_frequency: str | None,
|
||||
expected_frequency: str,
|
||||
warns: bool,
|
||||
variant: str,
|
||||
framework_type: str,
|
||||
) -> None:
|
||||
"""SDK config frequency settings are honored and recommend the native config key."""
|
||||
platform_framework = (
|
||||
PlatformFramework.ESP32_ARDUINO
|
||||
if framework_type == "arduino"
|
||||
else PlatformFramework.ESP32_IDF
|
||||
)
|
||||
set_core_config(platform_framework)
|
||||
from esphome.components.esp32 import CONFIG_SCHEMA
|
||||
|
||||
config: dict[str, Any] = {
|
||||
"variant": variant,
|
||||
"framework": {
|
||||
"type": framework_type,
|
||||
"sdkconfig_options": sdkconfig_options,
|
||||
},
|
||||
}
|
||||
if cpu_frequency is not None:
|
||||
config["cpu_frequency"] = cpu_frequency
|
||||
|
||||
with caplog.at_level(logging.WARNING):
|
||||
config = CONFIG_SCHEMA(config)
|
||||
|
||||
assert ("CPU frequency setting" in caplog.text) is warns
|
||||
assert config["cpu_frequency"] == expected_frequency
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("variant", "cpu_frequency", "sdkconfig_options", "error_match"),
|
||||
[
|
||||
pytest.param(
|
||||
VARIANT_ESP32C3,
|
||||
None,
|
||||
{"CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ": "fast"},
|
||||
"must be an integer MHz value",
|
||||
id="integer-option-not-a-number",
|
||||
),
|
||||
pytest.param(
|
||||
VARIANT_ESP32C3,
|
||||
None,
|
||||
{"CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240": "y"},
|
||||
"ESP32C3 does not support",
|
||||
id="unsupported-frequency",
|
||||
),
|
||||
pytest.param(
|
||||
VARIANT_ESP32C3,
|
||||
None,
|
||||
{"CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_160": "yes"},
|
||||
"must be y or n",
|
||||
id="choice-option-not-y-or-n",
|
||||
),
|
||||
pytest.param(
|
||||
VARIANT_ESP32C3,
|
||||
None,
|
||||
{
|
||||
"CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_160": "y",
|
||||
"CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240": "y",
|
||||
},
|
||||
r"conflicting CPU frequencies \(160MHz, 240MHz\)",
|
||||
id="two-frequencies-selected",
|
||||
),
|
||||
pytest.param(
|
||||
VARIANT_ESP32,
|
||||
"160MHz",
|
||||
{"CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240": "y"},
|
||||
"conflicts with sdkconfig_options",
|
||||
id="conflicts-with-explicit-value",
|
||||
),
|
||||
pytest.param(
|
||||
VARIANT_ESP32,
|
||||
None,
|
||||
{"CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240": "n"},
|
||||
"disables the selected CPU frequency",
|
||||
id="default-frequency-disabled",
|
||||
),
|
||||
pytest.param(
|
||||
VARIANT_ESP32,
|
||||
"160MHz",
|
||||
{"CONFIG_ESP32_DEFAULT_CPU_FREQ_160": "n"},
|
||||
"disables the selected CPU frequency",
|
||||
id="explicit-frequency-disabled-by-legacy-option",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_sdkconfig_cpu_frequency_errors(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
variant: str,
|
||||
cpu_frequency: str | None,
|
||||
sdkconfig_options: dict[str, str],
|
||||
error_match: str,
|
||||
) -> None:
|
||||
"""Bad, conflicting or disabled sdkconfig frequencies fail at validation."""
|
||||
set_core_config(PlatformFramework.ESP32_IDF)
|
||||
from esphome.components.esp32 import CONFIG_SCHEMA
|
||||
|
||||
config: dict[str, Any] = {
|
||||
"variant": variant,
|
||||
"framework": {"type": "esp-idf", "sdkconfig_options": sdkconfig_options},
|
||||
}
|
||||
if cpu_frequency is not None:
|
||||
config["cpu_frequency"] = cpu_frequency
|
||||
|
||||
with pytest.raises(cv.Invalid, match=error_match) as exc_info:
|
||||
CONFIG_SCHEMA(config)
|
||||
assert exc_info.value.path == ["framework", "sdkconfig_options"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config_toolchain", "expected"),
|
||||
[
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
adalight:
|
||||
|
||||
light:
|
||||
- platform: neopixelbus
|
||||
id: adalight_light
|
||||
name: Neopixelbus Light
|
||||
method: esp8266_uart
|
||||
num_leds: 5
|
||||
pin: 2
|
||||
type: GRBW
|
||||
variant: SK6812
|
||||
effects:
|
||||
- adalight:
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
uart_115200: !include ../../test_build_components/common/uart_115200/esp8266-ard.yaml
|
||||
adalight: !include common.yaml
|
||||
@@ -1,5 +1,13 @@
|
||||
sensor:
|
||||
- platform: cm1106
|
||||
id: cm1106_sensor
|
||||
co2:
|
||||
name: CM1106 CO2 Value
|
||||
on_value:
|
||||
then:
|
||||
- cm1106.abc_enable: cm1106_sensor
|
||||
- cm1106.abc_disable: cm1106_sensor
|
||||
update_interval: 15s
|
||||
automatic_baseline_calibration:
|
||||
cycle: 7
|
||||
baseline: 420
|
||||
|
||||
@@ -2,3 +2,8 @@ climate:
|
||||
- platform: mitsubishi
|
||||
name: Mitsubishi
|
||||
transmitter_id: xmitr
|
||||
fahrenheit_compatibility: true
|
||||
vertical_vanes: 2
|
||||
- platform: mitsubishi
|
||||
name: Mitsubishi Celsius
|
||||
transmitter_id: xmitr
|
||||
|
||||
@@ -2,9 +2,13 @@ tca9548a:
|
||||
- id: multiplex0
|
||||
i2c_id: i2c_bus
|
||||
address: 0x70
|
||||
frequency: 400 kHz
|
||||
channels:
|
||||
- bus_id: multiplex0_chan0
|
||||
channel: 0
|
||||
- bus_id: multiplex0_chan1
|
||||
channel: 1
|
||||
frequency: 1 MHz
|
||||
- id: multiplex1
|
||||
i2c_id: i2c_bus
|
||||
address: 0x71
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/nrf52.yaml
|
||||
tca9548a: !include common.yaml
|
||||
@@ -58,8 +58,9 @@ switch:
|
||||
|
||||
zigbee:
|
||||
model: zigbee_test
|
||||
router: false
|
||||
power_source: MAINS_SINGLE_PHASE
|
||||
sleepy: true
|
||||
polling_interval: 2s
|
||||
power_source: BATTERY
|
||||
on_join:
|
||||
then:
|
||||
- logger.log: "Joined network"
|
||||
|
||||
@@ -35,6 +35,7 @@ async def test_socket_tcp_client_link(
|
||||
link_down = asyncio.Event()
|
||||
second_link_up = asyncio.Event()
|
||||
link_up_count = 0
|
||||
writers: list[asyncio.StreamWriter] = []
|
||||
|
||||
def on_log_line(line: str) -> None:
|
||||
nonlocal link_up_count
|
||||
@@ -48,6 +49,7 @@ async def test_socket_tcp_client_link(
|
||||
async def handle(
|
||||
reader: asyncio.StreamReader, writer: asyncio.StreamWriter
|
||||
) -> None:
|
||||
writers.append(writer)
|
||||
if not echo_done.is_set():
|
||||
writer.write(PAYLOAD)
|
||||
await writer.drain()
|
||||
@@ -98,5 +100,9 @@ async def test_socket_tcp_client_link(
|
||||
"Second session echo wrong; stale bytes from the first session?"
|
||||
)
|
||||
finally:
|
||||
# On Python 3.12 wait_closed() waits for every connection the handlers
|
||||
# left open; nothing else closes them once the device has exited.
|
||||
for writer in writers:
|
||||
writer.close()
|
||||
server.close()
|
||||
await server.wait_closed()
|
||||
|
||||
@@ -41,12 +41,14 @@ async def test_tcp_uart_bridge(
|
||||
service_byte_ok = asyncio.Event()
|
||||
reconnect_echo_ok = asyncio.Event()
|
||||
sessions = 0
|
||||
writers: list[asyncio.StreamWriter] = []
|
||||
|
||||
async def handle(
|
||||
reader: asyncio.StreamReader, writer: asyncio.StreamWriter
|
||||
) -> None:
|
||||
nonlocal sessions
|
||||
sessions += 1
|
||||
writers.append(writer)
|
||||
if sessions == 1:
|
||||
writer.write(GREETING)
|
||||
await writer.drain()
|
||||
@@ -98,5 +100,9 @@ async def test_tcp_uart_bridge(
|
||||
reconnect_echo_ok, 15.0, "Bridge did not reconnect and echo again"
|
||||
)
|
||||
finally:
|
||||
# On Python 3.12 wait_closed() waits for every connection the handlers
|
||||
# left open; nothing else closes them once the device has exited.
|
||||
for writer in writers:
|
||||
writer.close()
|
||||
server.close()
|
||||
await server.wait_closed()
|
||||
|
||||
@@ -888,8 +888,9 @@ def test_write_project_plain_asm_rule_skips_preprocessor(tmp_path: Path) -> None
|
||||
_set_flags()
|
||||
content = _write_ninja(paths)
|
||||
assert "lowlevel.s.o: asm " in content
|
||||
assert "rule asm\n command = $cc -x assembler $asflags -c $in -o $out" in content
|
||||
assert "rule asm\n command = $ccache $cc -x assembler $asflags -c $in -o $out" in (
|
||||
content
|
||||
_write_ninja(paths, ccache="/cc/ccache")
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -308,7 +308,10 @@ def test_write_project_pch_skipped_for_a_force_include_in_the_compiler(
|
||||
def test_write_project_without_ccache(tmp_path: Path) -> None:
|
||||
_make_src(tmp_path, "main.cpp")
|
||||
_changed, ninja = _render()
|
||||
assert "ccache = \n" in ninja
|
||||
# No launcher variable, and no leading space on the commands either:
|
||||
# CreateProcess on Windows rejects a command line that starts with one
|
||||
assert "$ccache" not in ninja
|
||||
assert " command = $cxx -MMD -MF $out.d" in ninja
|
||||
|
||||
|
||||
def test_write_project_routes_user_link_flags(tmp_path: Path) -> None:
|
||||
|
||||
@@ -55,3 +55,21 @@ def test_collect_sources_skips_excluded_and_other_files(tmp_path: Path) -> None:
|
||||
tmp_path / "b.cpp",
|
||||
tmp_path / "sub" / "c.S",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("ccache", ["/usr/bin/ccache", None])
|
||||
def test_compile_rule_lines_splice_the_launcher(ccache: str | None) -> None:
|
||||
"""Without ccache no command keeps a leading space, which CreateProcess
|
||||
on Windows rejects; with it every compile rule starts with the launcher."""
|
||||
commands = [
|
||||
line.removeprefix(" command = ")
|
||||
for line in ninja_gen.compile_rule_lines(ccache)
|
||||
if line.startswith(" command = ")
|
||||
]
|
||||
assert len(commands) == 4
|
||||
for command in commands:
|
||||
if ccache:
|
||||
assert command.startswith("$ccache $c")
|
||||
else:
|
||||
assert command.startswith("$c")
|
||||
assert "$ccache" not in command
|
||||
|
||||
@@ -7,6 +7,7 @@ from contextlib import contextmanager
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
from unittest.mock import MagicMock, call, patch
|
||||
|
||||
@@ -262,6 +263,14 @@ def test_get_idf_env_pops_inherited_pythonpath(setup_core: Path) -> None:
|
||||
assert "PYTHONPATH" not in env
|
||||
|
||||
|
||||
def test_get_idf_env_pops_inherited_idf_target(setup_core: Path) -> None:
|
||||
"""An inherited IDF_TARGET does not reach cmake."""
|
||||
toolchain._cache().env.clear()
|
||||
with patch.dict(os.environ, {"IDF_PATH": str(setup_core), "IDF_TARGET": "esp32"}):
|
||||
env = toolchain._get_idf_env(version="5.5.4")
|
||||
assert "IDF_TARGET" not in env
|
||||
|
||||
|
||||
def test_get_cmake_output_without_build_dir(setup_core: Path) -> None:
|
||||
"""A build dir that was never created raises EsphomeError.
|
||||
|
||||
@@ -1575,3 +1584,193 @@ def test_missing_image_hint_names_the_flag(setup_core: Path) -> None:
|
||||
f"{toolchain.SKIP_BOOTLOADER_DEFINE}:UNINITIALIZED=1\n"
|
||||
)
|
||||
assert "--skip-bootloader" in toolchain.missing_image_hint()
|
||||
|
||||
|
||||
def _write_target_tree(
|
||||
main: str | None = None,
|
||||
bootloader: str | None = None,
|
||||
bootloader_toolchain: str | None = None,
|
||||
sdkconfig: str | None = None,
|
||||
) -> None:
|
||||
"""Write the target into the CMake caches and sdkconfig."""
|
||||
tools = "/idf/tools/cmake"
|
||||
for rel, target, toolchain_target in (
|
||||
("build", main, main),
|
||||
("build/bootloader", bootloader, bootloader_toolchain or bootloader),
|
||||
):
|
||||
if target is None:
|
||||
continue
|
||||
cache = CORE.relative_build_path(rel, "CMakeCache.txt")
|
||||
cache.parent.mkdir(parents=True, exist_ok=True)
|
||||
cache.write_text(
|
||||
f"CMAKE_TOOLCHAIN_FILE:FILEPATH={tools}/toolchain-{toolchain_target}.cmake\n"
|
||||
f"IDF_TARGET:STRING={target}\n"
|
||||
)
|
||||
if sdkconfig is not None:
|
||||
path = CORE.relative_build_path("sdkconfig.test")
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(f'CONFIG_FOO=y\nCONFIG_IDF_TARGET="{sdkconfig}"\n')
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("tree", "expected"),
|
||||
[
|
||||
({}, None),
|
||||
({"main": "esp32c6", "bootloader": "esp32c6", "sdkconfig": "esp32c6"}, None),
|
||||
({"main": "esp32h2"}, "esp32h2"),
|
||||
({"bootloader": "esp32h2"}, "esp32h2"),
|
||||
# Failed bootloader configure: new IDF_TARGET, old toolchain file
|
||||
({"bootloader": "esp32c6", "bootloader_toolchain": "esp32h2"}, "esp32h2"),
|
||||
({"main": "esp32c6", "sdkconfig": "esp32h2"}, "esp32h2"),
|
||||
(
|
||||
{
|
||||
"main": "esp32c6",
|
||||
"bootloader": "esp32c6",
|
||||
"bootloader_toolchain": "clang-esp32c6",
|
||||
},
|
||||
None,
|
||||
),
|
||||
(
|
||||
{
|
||||
"main": "esp32c6",
|
||||
"bootloader": "esp32c6",
|
||||
"bootloader_toolchain": "clang-esp32h2",
|
||||
},
|
||||
"esp32h2",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_foreign_idf_target(
|
||||
setup_core: Path, tree: dict[str, str], expected: str | None
|
||||
) -> None:
|
||||
"""Another target in any cache or sdkconfig counts."""
|
||||
_setup_build(setup_core)
|
||||
_write_target_tree(**tree)
|
||||
assert toolchain._foreign_idf_target("esp32c6") == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("cache", "sdkconfig", "expected"),
|
||||
[
|
||||
("IDF_TARGET:STRING=esp32h2\n", None, "esp32h2"),
|
||||
(None, "CONFIG_FOO=y\n", None),
|
||||
],
|
||||
)
|
||||
def test_foreign_idf_target_partial_entries(
|
||||
setup_core: Path, cache: str | None, sdkconfig: str | None, expected: str | None
|
||||
) -> None:
|
||||
"""Missing entries are skipped."""
|
||||
_setup_build(setup_core)
|
||||
for rel, content in (
|
||||
("build/CMakeCache.txt", cache),
|
||||
("sdkconfig.test", sdkconfig),
|
||||
):
|
||||
if content is not None:
|
||||
path = CORE.relative_build_path(rel)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(content)
|
||||
assert toolchain._foreign_idf_target("esp32c6") == expected
|
||||
|
||||
|
||||
def test_foreign_idf_target_ignores_unreadable_files(setup_core: Path) -> None:
|
||||
"""Unreadable files are ignored."""
|
||||
_setup_build(setup_core)
|
||||
for rel in ("build/CMakeCache.txt", "sdkconfig.test"):
|
||||
path = CORE.relative_build_path(rel)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_bytes(b"\xff\xfe IDF_TARGET:STRING=esp32h2\n")
|
||||
assert toolchain._foreign_idf_target("esp32c6") is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("snapshot", [True, False])
|
||||
def test_clean_foreign_target_tree_cleans_and_resets_sdkconfig(
|
||||
setup_core: Path, snapshot: bool
|
||||
) -> None:
|
||||
"""A foreign tree is cleaned and sdkconfig reset from the snapshot."""
|
||||
_setup_build(setup_core)
|
||||
CORE.data[KEY_ESP32][KEY_VARIANT] = "ESP32C6"
|
||||
_write_target_tree(bootloader="esp32c6", bootloader_toolchain="esp32h2")
|
||||
_write_target_tree(sdkconfig="esp32h2")
|
||||
internal = CORE.relative_build_path("sdkconfig.test.esphomeinternal")
|
||||
if snapshot:
|
||||
internal.write_text("CONFIG_IDF_TARGET_ESP32C6=y\n")
|
||||
|
||||
with patch(
|
||||
"esphome.writer.clean_build",
|
||||
side_effect=lambda **_: shutil.rmtree(CORE.relative_build_path("build")),
|
||||
) as mock_clean:
|
||||
toolchain._clean_foreign_target_tree()
|
||||
|
||||
mock_clean.assert_called_once_with(clear_pio_cache=False)
|
||||
sdkconfig = CORE.relative_build_path("sdkconfig.test").read_text()
|
||||
if snapshot:
|
||||
assert sdkconfig == "CONFIG_IDF_TARGET_ESP32C6=y\n"
|
||||
else:
|
||||
assert 'CONFIG_IDF_TARGET="esp32h2"' in sdkconfig
|
||||
|
||||
|
||||
@pytest.mark.parametrize("variant", ["ESP32C6", None])
|
||||
def test_clean_foreign_target_tree_keeps_matching_tree(
|
||||
setup_core: Path, variant: str | None
|
||||
) -> None:
|
||||
"""A matching tree or unknown variant is left alone."""
|
||||
_setup_build(setup_core)
|
||||
CORE.data[KEY_ESP32][KEY_VARIANT] = variant
|
||||
_write_target_tree(main="esp32c6", bootloader="esp32c6", sdkconfig="esp32c6")
|
||||
internal = CORE.relative_build_path("sdkconfig.test.esphomeinternal")
|
||||
internal.write_text("CONFIG_IDF_TARGET_ESP32C6=y\n")
|
||||
sdkconfig = CORE.relative_build_path("sdkconfig.test")
|
||||
before = sdkconfig.read_text()
|
||||
|
||||
with patch("esphome.writer.clean_build") as mock_clean:
|
||||
toolchain._clean_foreign_target_tree()
|
||||
|
||||
mock_clean.assert_not_called()
|
||||
assert sdkconfig.read_text() == before
|
||||
|
||||
|
||||
@pytest.mark.parametrize("main", ["esp32h2", None])
|
||||
def test_clean_foreign_target_tree_skipped_clean_keeps_sdkconfig(
|
||||
setup_core: Path, monkeypatch: pytest.MonkeyPatch, main: str | None
|
||||
) -> None:
|
||||
"""ESPHOME_SKIP_CLEAN_BUILD keeps the tree and sdkconfig."""
|
||||
_setup_build(setup_core)
|
||||
monkeypatch.setenv("ESPHOME_SKIP_CLEAN_BUILD", "1")
|
||||
CORE.data[KEY_ESP32][KEY_VARIANT] = "ESP32C6"
|
||||
_write_target_tree(main=main, sdkconfig="esp32h2")
|
||||
CORE.relative_build_path("sdkconfig.test.esphomeinternal").write_text(
|
||||
"CONFIG_IDF_TARGET_ESP32C6=y\n"
|
||||
)
|
||||
|
||||
toolchain._clean_foreign_target_tree()
|
||||
|
||||
assert CORE.relative_build_path("build/CMakeCache.txt").is_file() == bool(main)
|
||||
assert 'CONFIG_IDF_TARGET="esp32h2"' in (
|
||||
CORE.relative_build_path("sdkconfig.test").read_text()
|
||||
)
|
||||
|
||||
|
||||
def test_run_compile_cleans_foreign_target_tree_first(setup_core: Path) -> None:
|
||||
"""run_compile cleans a foreign tree before the reconfigure check."""
|
||||
_setup_build(setup_core)
|
||||
CORE.data[KEY_ESP32][KEY_VARIANT] = "ESP32C6"
|
||||
_write_target_tree(main="esp32h2")
|
||||
calls: list[str] = []
|
||||
|
||||
with (
|
||||
patch(
|
||||
"esphome.writer.clean_build",
|
||||
side_effect=lambda **_: calls.append("clean"),
|
||||
),
|
||||
patch.object(
|
||||
toolchain,
|
||||
"need_reconfigure",
|
||||
side_effect=lambda: calls.append("check") or False,
|
||||
),
|
||||
patch.object(toolchain, "_cache_entries_changed", return_value=False),
|
||||
patch.object(toolchain, "_run_ninja", return_value=0),
|
||||
patch.object(toolchain, "print_summary"),
|
||||
):
|
||||
assert toolchain.run_compile({CONF_ESPHOME: {}}, verbose=False) == 0
|
||||
|
||||
assert calls == ["clean", "check"]
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
"""Tests for nRF52 BLE device discovery and OTA transport selection."""
|
||||
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.nrf52 import ble_logger, ota, show_logs
|
||||
from esphome.core import EsphomeError
|
||||
|
||||
MAC = "AA:BB:CC:DD:EE:FF"
|
||||
# macOS/CoreBluetooth identifies peripherals by UUID instead of MAC address
|
||||
UUID = "0FA1B2C3-D4E5-F607-1829-3A4B5C6D7E8F"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("value", "expected"),
|
||||
[
|
||||
(MAC, True),
|
||||
(MAC.lower(), True),
|
||||
(UUID, True),
|
||||
("/dev/ttyACM0", False),
|
||||
("COM3", False),
|
||||
],
|
||||
)
|
||||
def test_is_ble_address(value: str, expected: bool) -> None:
|
||||
assert ble_logger.is_ble_address(value) is expected
|
||||
|
||||
|
||||
def _scan_result(
|
||||
monkeypatch, devices: list[tuple[str, str | None, str | None]]
|
||||
) -> AsyncMock:
|
||||
"""Stub the bleak scan so the filter is applied to the given (address, name, local_name) devices."""
|
||||
|
||||
async def find_device_by_filter(filterfunc, timeout):
|
||||
for address, name, local_name in devices:
|
||||
device = SimpleNamespace(address=address, name=name)
|
||||
if filterfunc(device, SimpleNamespace(local_name=local_name)):
|
||||
return device
|
||||
return None
|
||||
|
||||
mock = AsyncMock(side_effect=find_device_by_filter)
|
||||
monkeypatch.setattr(ble_logger.BleakScanner, "find_device_by_filter", mock)
|
||||
return mock
|
||||
|
||||
|
||||
def test_scan_matches_advertised_local_name(monkeypatch) -> None:
|
||||
"""On macOS device.name is cached from an earlier connection; the live name is adv.local_name."""
|
||||
_scan_result(monkeypatch, [(UUID, "stale-old-name", "my-device")])
|
||||
assert asyncio.run(ota.smpmgr_scan("my-device")) == UUID
|
||||
|
||||
|
||||
def test_scan_matches_device_name(monkeypatch) -> None:
|
||||
_scan_result(monkeypatch, [(MAC, "my-device", None)])
|
||||
assert asyncio.run(ota.smpmgr_scan("my-device")) == MAC
|
||||
|
||||
|
||||
def test_scan_raises_when_no_device_found(monkeypatch) -> None:
|
||||
_scan_result(monkeypatch, [(MAC, "someone-else", "someone-else")])
|
||||
with pytest.raises(EsphomeError, match="not found"):
|
||||
asyncio.run(ota.smpmgr_scan("my-device"))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("device", "transport"),
|
||||
[("/dev/ttyACM0", "serial"), ("COM3", "serial"), (MAC, "ble"), (UUID, "ble")],
|
||||
)
|
||||
def test_upload_transport(monkeypatch, device: str, transport: str) -> None:
|
||||
"""Serial ports go to the serial transport; MAC addresses and macOS UUIDs to BLE."""
|
||||
captured: dict = {}
|
||||
|
||||
class FakeClient:
|
||||
def __init__(self, transport, address):
|
||||
captured["transport"] = transport
|
||||
captured["address"] = address
|
||||
|
||||
async def connect(self) -> None:
|
||||
pass
|
||||
|
||||
async def disconnect(self) -> None:
|
||||
pass
|
||||
|
||||
monkeypatch.setattr(ota, "_get_image_tlv_sha256", lambda firmware: b"")
|
||||
monkeypatch.setattr(ota, "_smpmgr_upload_connected", AsyncMock())
|
||||
monkeypatch.setattr(ota, "SMPSerialTransport", lambda: "serial")
|
||||
monkeypatch.setattr(ota, "SMPBLETransport", lambda: "ble")
|
||||
monkeypatch.setattr(ota, "SMPClient", FakeClient)
|
||||
|
||||
asyncio.run(ota._smpmgr_upload(device, Path("firmware.bin"))) # pylint: disable=protected-access
|
||||
assert captured == {"transport": transport, "address": device}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("device", "connected_to"),
|
||||
[("BLE", UUID), (MAC, MAC), (UUID, UUID), ("my-device.local", None)],
|
||||
)
|
||||
def test_show_logs_routing(monkeypatch, device: str, connected_to: str | None) -> None:
|
||||
"""A scanned device, a MAC or a UUID is handed to the BLE logger; anything else falls through."""
|
||||
scan = AsyncMock(return_value=SimpleNamespace(address=UUID))
|
||||
connect = AsyncMock(return_value=0)
|
||||
monkeypatch.setattr(ble_logger, "logger_scan", scan)
|
||||
monkeypatch.setattr(ble_logger, "logger_connect", connect)
|
||||
|
||||
handled = show_logs(config={}, args=None, devices=[device])
|
||||
|
||||
assert handled is (connected_to is not None)
|
||||
if connected_to is None:
|
||||
connect.assert_not_called()
|
||||
else:
|
||||
connect.assert_awaited_once_with(connected_to)
|
||||
|
||||
|
||||
def test_show_logs_returns_when_the_scan_finds_nothing(monkeypatch) -> None:
|
||||
monkeypatch.setattr(ble_logger, "logger_scan", AsyncMock(return_value=None))
|
||||
connect = AsyncMock()
|
||||
monkeypatch.setattr(ble_logger, "logger_connect", connect)
|
||||
|
||||
assert show_logs(config={}, args=None, devices=["BLE"]) is True
|
||||
connect.assert_not_called()
|
||||
Reference in New Issue
Block a user