mirror of
https://github.com/esphome/esphome.git
synced 2026-03-03 03:08:21 -07:00
preen
This commit is contained in:
42
.github/scripts/auto-label-pr/detectors.js
vendored
42
.github/scripts/auto-label-pr/detectors.js
vendored
@@ -1,5 +1,12 @@
|
||||
const fs = require('fs');
|
||||
const { DOCS_PR_PATTERNS } = require('./constants');
|
||||
const {
|
||||
COMPONENT_REGEX,
|
||||
detectComponents,
|
||||
hasCoreChanges,
|
||||
hasDashboardChanges,
|
||||
hasGitHubActionsChanges,
|
||||
} = require('../detect-tags');
|
||||
|
||||
// Strategy: Merge branch detection
|
||||
async function detectMergeBranch(context) {
|
||||
@@ -20,15 +27,13 @@ async function detectMergeBranch(context) {
|
||||
// Strategy: Component and platform labeling
|
||||
async function detectComponentPlatforms(changedFiles, apiData) {
|
||||
const labels = new Set();
|
||||
const componentRegex = /^esphome\/components\/([^\/]+)\//;
|
||||
const targetPlatformRegex = new RegExp(`^esphome\/components\/(${apiData.targetPlatforms.join('|')})/`);
|
||||
|
||||
for (const file of changedFiles) {
|
||||
const componentMatch = file.match(componentRegex);
|
||||
if (componentMatch) {
|
||||
labels.add(`component: ${componentMatch[1]}`);
|
||||
}
|
||||
for (const comp of detectComponents(changedFiles)) {
|
||||
labels.add(`component: ${comp}`);
|
||||
}
|
||||
|
||||
for (const file of changedFiles) {
|
||||
const platformMatch = file.match(targetPlatformRegex);
|
||||
if (platformMatch) {
|
||||
labels.add(`platform: ${platformMatch[1]}`);
|
||||
@@ -90,15 +95,9 @@ async function detectNewPlatforms(prFiles, apiData) {
|
||||
// Strategy: Core files detection
|
||||
async function detectCoreChanges(changedFiles) {
|
||||
const labels = new Set();
|
||||
const coreFiles = changedFiles.filter(file =>
|
||||
file.startsWith('esphome/core/') ||
|
||||
(file.startsWith('esphome/') && file.split('/').length === 2)
|
||||
);
|
||||
|
||||
if (coreFiles.length > 0) {
|
||||
if (hasCoreChanges(changedFiles)) {
|
||||
labels.add('core');
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
@@ -131,29 +130,18 @@ async function detectPRSize(prFiles, totalAdditions, totalDeletions, totalChange
|
||||
// Strategy: Dashboard changes
|
||||
async function detectDashboardChanges(changedFiles) {
|
||||
const labels = new Set();
|
||||
const dashboardFiles = changedFiles.filter(file =>
|
||||
file.startsWith('esphome/dashboard/') ||
|
||||
file.startsWith('esphome/components/dashboard_import/')
|
||||
);
|
||||
|
||||
if (dashboardFiles.length > 0) {
|
||||
if (hasDashboardChanges(changedFiles)) {
|
||||
labels.add('dashboard');
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
// Strategy: GitHub Actions changes
|
||||
async function detectGitHubActionsChanges(changedFiles) {
|
||||
const labels = new Set();
|
||||
const githubActionsFiles = changedFiles.filter(file =>
|
||||
file.startsWith('.github/workflows/')
|
||||
);
|
||||
|
||||
if (githubActionsFiles.length > 0) {
|
||||
if (hasGitHubActionsChanges(changedFiles)) {
|
||||
labels.add('github-actions');
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
@@ -259,7 +247,7 @@ async function detectDeprecatedComponents(github, context, changedFiles) {
|
||||
const { owner, repo } = context.repo;
|
||||
|
||||
// Compile regex once for better performance
|
||||
const componentFileRegex = /^esphome\/components\/([^\/]+)\//;
|
||||
const componentFileRegex = COMPONENT_REGEX;
|
||||
|
||||
// Get files that are modified or added in components directory
|
||||
const componentFiles = changedFiles.filter(file => componentFileRegex.test(file));
|
||||
|
||||
66
.github/scripts/detect-tags.js
vendored
Normal file
66
.github/scripts/detect-tags.js
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Shared tag detection from changed file paths.
|
||||
* Used by pr-title-check and auto-label-pr workflows.
|
||||
*/
|
||||
|
||||
const COMPONENT_REGEX = /^esphome\/components\/([^\/]+)\//;
|
||||
|
||||
/**
|
||||
* Detect component names from changed files.
|
||||
* @param {string[]} changedFiles - List of changed file paths
|
||||
* @returns {Set<string>} Set of component names
|
||||
*/
|
||||
function detectComponents(changedFiles) {
|
||||
const components = new Set();
|
||||
for (const file of changedFiles) {
|
||||
const match = file.match(COMPONENT_REGEX);
|
||||
if (match) {
|
||||
components.add(match[1]);
|
||||
}
|
||||
}
|
||||
return components;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if core files were changed.
|
||||
* Core files are in esphome/core/ or top-level esphome/ directory.
|
||||
* @param {string[]} changedFiles - List of changed file paths
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function hasCoreChanges(changedFiles) {
|
||||
return changedFiles.some(file =>
|
||||
file.startsWith('esphome/core/') ||
|
||||
(file.startsWith('esphome/') && file.split('/').length === 2)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if dashboard files were changed.
|
||||
* @param {string[]} changedFiles - List of changed file paths
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function hasDashboardChanges(changedFiles) {
|
||||
return changedFiles.some(file =>
|
||||
file.startsWith('esphome/dashboard/') ||
|
||||
file.startsWith('esphome/components/dashboard_import/')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if GitHub Actions files were changed.
|
||||
* @param {string[]} changedFiles - List of changed file paths
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function hasGitHubActionsChanges(changedFiles) {
|
||||
return changedFiles.some(file =>
|
||||
file.startsWith('.github/workflows/')
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
COMPONENT_REGEX,
|
||||
detectComponents,
|
||||
hasCoreChanges,
|
||||
hasDashboardChanges,
|
||||
hasGitHubActionsChanges,
|
||||
};
|
||||
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -945,13 +945,13 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Download target analysis JSON
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
||||
with:
|
||||
name: memory-analysis-target
|
||||
path: ./memory-analysis
|
||||
continue-on-error: true
|
||||
- name: Download PR analysis JSON
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
||||
with:
|
||||
name: memory-analysis-pr
|
||||
path: ./memory-analysis
|
||||
|
||||
76
.github/workflows/pr-title-check.yml
vendored
Normal file
76
.github/workflows/pr-title-check.yml
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
name: PR Title Check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, synchronize, reopened]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Validate PR title
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
script: |
|
||||
const {
|
||||
detectComponents,
|
||||
hasCoreChanges,
|
||||
hasDashboardChanges,
|
||||
hasGitHubActionsChanges,
|
||||
} = require('./.github/scripts/detect-tags.js');
|
||||
|
||||
const title = context.payload.pull_request.title;
|
||||
|
||||
// Block titles starting with "word:" or "word(scope):" patterns
|
||||
const commitStylePattern = /^\w+(\(.*?\))?[!]?\s*:/;
|
||||
if (commitStylePattern.test(title)) {
|
||||
core.setFailed(
|
||||
`PR title should not start with a "prefix:" style format.\n` +
|
||||
`Please use the format: [component] Brief description\n` +
|
||||
`Example: [pn532] Add health checking and auto-reset`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get changed files to detect tags
|
||||
const files = await github.paginate(github.rest.pulls.listFiles, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: context.issue.number,
|
||||
});
|
||||
const filenames = files.map(f => f.filename);
|
||||
|
||||
// Detect tags from changed files using shared logic
|
||||
const tags = new Set();
|
||||
|
||||
for (const comp of detectComponents(filenames)) {
|
||||
tags.add(comp);
|
||||
}
|
||||
if (hasCoreChanges(filenames)) tags.add('core');
|
||||
if (hasDashboardChanges(filenames)) tags.add('dashboard');
|
||||
if (hasGitHubActionsChanges(filenames)) tags.add('ci');
|
||||
|
||||
if (tags.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check title starts with [tag] prefix
|
||||
const bracketPattern = /^\[\w+\]/;
|
||||
if (!bracketPattern.test(title)) {
|
||||
const suggestion = [...tags].map(c => `[${c}]`).join('');
|
||||
// Skip if the suggested prefix would be too long for a readable title
|
||||
if (suggestion.length > 40) {
|
||||
return;
|
||||
}
|
||||
core.setFailed(
|
||||
`PR modifies: ${[...tags].join(', ')}\n` +
|
||||
`Title must start with a [tag] prefix.\n` +
|
||||
`Suggested: ${suggestion} <description>`
|
||||
);
|
||||
}
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -171,7 +171,7 @@ jobs:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
||||
with:
|
||||
pattern: digests-*
|
||||
path: /tmp/digests
|
||||
|
||||
@@ -429,6 +429,7 @@ esphome/components/select/* @esphome/core
|
||||
esphome/components/sen0321/* @notjj
|
||||
esphome/components/sen21231/* @shreyaskarnik
|
||||
esphome/components/sen5x/* @martgras
|
||||
esphome/components/sen6x/* @martgras @mebner86 @mikelawrence @tuct
|
||||
esphome/components/sensirion_common/* @martgras
|
||||
esphome/components/sensor/* @esphome/core
|
||||
esphome/components/sfa30/* @ghsensdev
|
||||
|
||||
@@ -890,10 +890,10 @@ def final_validate(config):
|
||||
)
|
||||
)
|
||||
if advanced[CONF_EXECUTE_FROM_PSRAM]:
|
||||
if config[CONF_VARIANT] != VARIANT_ESP32S3:
|
||||
if config[CONF_VARIANT] not in {VARIANT_ESP32S3, VARIANT_ESP32P4}:
|
||||
errs.append(
|
||||
cv.Invalid(
|
||||
f"'{CONF_EXECUTE_FROM_PSRAM}' is only supported on {VARIANT_ESP32S3} variant",
|
||||
f"'{CONF_EXECUTE_FROM_PSRAM}' is not available on this esp32 variant",
|
||||
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_EXECUTE_FROM_PSRAM],
|
||||
)
|
||||
)
|
||||
@@ -1627,8 +1627,13 @@ async def to_code(config):
|
||||
_configure_lwip_max_sockets(conf)
|
||||
|
||||
if advanced[CONF_EXECUTE_FROM_PSRAM]:
|
||||
add_idf_sdkconfig_option("CONFIG_SPIRAM_FETCH_INSTRUCTIONS", True)
|
||||
add_idf_sdkconfig_option("CONFIG_SPIRAM_RODATA", True)
|
||||
if variant == VARIANT_ESP32S3:
|
||||
add_idf_sdkconfig_option("CONFIG_SPIRAM_FETCH_INSTRUCTIONS", True)
|
||||
add_idf_sdkconfig_option("CONFIG_SPIRAM_RODATA", True)
|
||||
elif variant == VARIANT_ESP32P4:
|
||||
add_idf_sdkconfig_option("CONFIG_SPIRAM_XIP_FROM_PSRAM", True)
|
||||
else:
|
||||
raise ValueError("Unhandled ESP32 variant")
|
||||
|
||||
# Apply LWIP core locking for better socket performance
|
||||
# This is already enabled by default in Arduino framework, where it provides
|
||||
|
||||
@@ -98,33 +98,25 @@ async def to_code(config):
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
|
||||
if latitude_config := config.get(CONF_LATITUDE):
|
||||
sens = await sensor.new_sensor(latitude_config)
|
||||
cg.add(var.set_latitude_sensor(sens))
|
||||
# Pre-create all sensor variables so automations that reference
|
||||
# sibling sensors don't deadlock waiting for unregistered IDs.
|
||||
sensors = [
|
||||
(cg.new_Pvariable(conf[CONF_ID]), conf, setter)
|
||||
for key, setter in (
|
||||
(CONF_LATITUDE, "set_latitude_sensor"),
|
||||
(CONF_LONGITUDE, "set_longitude_sensor"),
|
||||
(CONF_SPEED, "set_speed_sensor"),
|
||||
(CONF_COURSE, "set_course_sensor"),
|
||||
(CONF_ALTITUDE, "set_altitude_sensor"),
|
||||
(CONF_SATELLITES, "set_satellites_sensor"),
|
||||
(CONF_HDOP, "set_hdop_sensor"),
|
||||
)
|
||||
if (conf := config.get(key))
|
||||
]
|
||||
|
||||
if longitude_config := config.get(CONF_LONGITUDE):
|
||||
sens = await sensor.new_sensor(longitude_config)
|
||||
cg.add(var.set_longitude_sensor(sens))
|
||||
|
||||
if speed_config := config.get(CONF_SPEED):
|
||||
sens = await sensor.new_sensor(speed_config)
|
||||
cg.add(var.set_speed_sensor(sens))
|
||||
|
||||
if course_config := config.get(CONF_COURSE):
|
||||
sens = await sensor.new_sensor(course_config)
|
||||
cg.add(var.set_course_sensor(sens))
|
||||
|
||||
if altitude_config := config.get(CONF_ALTITUDE):
|
||||
sens = await sensor.new_sensor(altitude_config)
|
||||
cg.add(var.set_altitude_sensor(sens))
|
||||
|
||||
if satellites_config := config.get(CONF_SATELLITES):
|
||||
sens = await sensor.new_sensor(satellites_config)
|
||||
cg.add(var.set_satellites_sensor(sens))
|
||||
|
||||
if hdop_config := config.get(CONF_HDOP):
|
||||
sens = await sensor.new_sensor(hdop_config)
|
||||
cg.add(var.set_hdop_sensor(sens))
|
||||
for sens, conf, setter in sensors:
|
||||
await sensor.register_sensor(sens, conf)
|
||||
cg.add(getattr(var, setter)(sens))
|
||||
|
||||
# https://platformio.org/lib/show/1655/TinyGPSPlus
|
||||
# Using fork of TinyGPSPlus patched to build on ESP-IDF
|
||||
|
||||
@@ -405,12 +405,6 @@ void MQTTComponent::process_resend() {
|
||||
this->schedule_resend_state();
|
||||
}
|
||||
}
|
||||
void MQTTComponent::call_dump_config() {
|
||||
if (this->is_internal())
|
||||
return;
|
||||
|
||||
this->dump_config();
|
||||
}
|
||||
void MQTTComponent::schedule_resend_state() { this->resend_state_ = true; }
|
||||
bool MQTTComponent::is_connected_() const { return global_mqtt_client->is_connected(); }
|
||||
|
||||
|
||||
@@ -98,8 +98,6 @@ class MQTTComponent : public Component {
|
||||
/// Override setup_ so that we can call send_discovery() when needed.
|
||||
void call_setup() override;
|
||||
|
||||
void call_dump_config() override;
|
||||
|
||||
/// Send discovery info the Home Assistant, override this.
|
||||
virtual void send_discovery(JsonObject root, SendDiscoveryConfig &config) = 0;
|
||||
|
||||
|
||||
@@ -3,19 +3,19 @@
|
||||
#include "core.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include "hardware/timer.h"
|
||||
#include "hardware/watchdog.h"
|
||||
|
||||
namespace esphome {
|
||||
|
||||
void HOT yield() { ::yield(); }
|
||||
uint32_t IRAM_ATTR HOT millis() { return ::millis(); }
|
||||
uint64_t millis_64() { return App.scheduler.millis_64_impl_(::millis()); }
|
||||
uint64_t millis_64() { return time_us_64() / 1000ULL; }
|
||||
uint32_t HOT millis() { return static_cast<uint32_t>(millis_64()); }
|
||||
void HOT delay(uint32_t ms) { ::delay(ms); }
|
||||
uint32_t IRAM_ATTR HOT micros() { return ::micros(); }
|
||||
void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); }
|
||||
uint32_t HOT micros() { return ::micros(); }
|
||||
void HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); }
|
||||
void arch_restart() {
|
||||
watchdog_reboot(0, 0, 10);
|
||||
while (1) {
|
||||
@@ -34,7 +34,7 @@ void HOT arch_feed_wdt() { watchdog_update(); }
|
||||
uint8_t progmem_read_byte(const uint8_t *addr) {
|
||||
return pgm_read_byte(addr); // NOLINT
|
||||
}
|
||||
uint32_t IRAM_ATTR HOT arch_get_cpu_cycle_count() { return ulMainGetRunTimeCounterValue(); }
|
||||
uint32_t HOT arch_get_cpu_cycle_count() { return ulMainGetRunTimeCounterValue(); }
|
||||
uint32_t arch_get_cpu_freq_hz() { return RP2040::f_cpu(); }
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
0
esphome/components/sen6x/__init__.py
Normal file
0
esphome/components/sen6x/__init__.py
Normal file
376
esphome/components/sen6x/sen6x.cpp
Normal file
376
esphome/components/sen6x/sen6x.cpp
Normal file
@@ -0,0 +1,376 @@
|
||||
#include "sen6x.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
namespace esphome::sen6x {
|
||||
|
||||
static const char *const TAG = "sen6x";
|
||||
|
||||
static constexpr uint16_t SEN6X_CMD_GET_DATA_READY_STATUS = 0x0202;
|
||||
static constexpr uint16_t SEN6X_CMD_GET_FIRMWARE_VERSION = 0xD100;
|
||||
static constexpr uint16_t SEN6X_CMD_GET_PRODUCT_NAME = 0xD014;
|
||||
static constexpr uint16_t SEN6X_CMD_GET_SERIAL_NUMBER = 0xD033;
|
||||
|
||||
static constexpr uint16_t SEN6X_CMD_READ_MEASUREMENT = 0x0300; // SEN66 only!
|
||||
static constexpr uint16_t SEN6X_CMD_READ_MEASUREMENT_SEN62 = 0x04A3;
|
||||
static constexpr uint16_t SEN6X_CMD_READ_MEASUREMENT_SEN63C = 0x0471;
|
||||
static constexpr uint16_t SEN6X_CMD_READ_MEASUREMENT_SEN65 = 0x0446;
|
||||
static constexpr uint16_t SEN6X_CMD_READ_MEASUREMENT_SEN68 = 0x0467;
|
||||
static constexpr uint16_t SEN6X_CMD_READ_MEASUREMENT_SEN69C = 0x04B5;
|
||||
|
||||
static constexpr uint16_t SEN6X_CMD_START_MEASUREMENTS = 0x0021;
|
||||
static constexpr uint16_t SEN6X_CMD_RESET = 0xD304;
|
||||
|
||||
static inline void set_read_command_and_words(SEN6XComponent::Sen6xType type, uint16_t &read_cmd, uint8_t &read_words) {
|
||||
read_cmd = SEN6X_CMD_READ_MEASUREMENT;
|
||||
read_words = 9;
|
||||
switch (type) {
|
||||
case SEN6XComponent::SEN62:
|
||||
read_cmd = SEN6X_CMD_READ_MEASUREMENT_SEN62;
|
||||
read_words = 6;
|
||||
break;
|
||||
case SEN6XComponent::SEN63C:
|
||||
read_cmd = SEN6X_CMD_READ_MEASUREMENT_SEN63C;
|
||||
read_words = 7;
|
||||
break;
|
||||
case SEN6XComponent::SEN65:
|
||||
read_cmd = SEN6X_CMD_READ_MEASUREMENT_SEN65;
|
||||
read_words = 8;
|
||||
break;
|
||||
case SEN6XComponent::SEN66:
|
||||
read_cmd = SEN6X_CMD_READ_MEASUREMENT;
|
||||
read_words = 9;
|
||||
break;
|
||||
case SEN6XComponent::SEN68:
|
||||
read_cmd = SEN6X_CMD_READ_MEASUREMENT_SEN68;
|
||||
read_words = 9;
|
||||
break;
|
||||
case SEN6XComponent::SEN69C:
|
||||
read_cmd = SEN6X_CMD_READ_MEASUREMENT_SEN69C;
|
||||
read_words = 10;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void SEN6XComponent::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up sen6x...");
|
||||
|
||||
// the sensor needs 100 ms to enter the idle state
|
||||
this->set_timeout(100, [this]() {
|
||||
// Reset the sensor to ensure a clean state regardless of prior commands or power issues
|
||||
if (!this->write_command(SEN6X_CMD_RESET)) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
|
||||
// After reset the sensor needs 100 ms to become ready
|
||||
this->set_timeout(100, [this]() {
|
||||
// Step 1: Read serial number (~25ms with I2C delay)
|
||||
uint16_t raw_serial_number[16];
|
||||
if (!this->get_register(SEN6X_CMD_GET_SERIAL_NUMBER, raw_serial_number, 16, 20)) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
this->serial_number_ = SEN6XComponent::sensirion_convert_to_string_in_place(raw_serial_number, 16);
|
||||
ESP_LOGI(TAG, "Serial number: %s", this->serial_number_.c_str());
|
||||
|
||||
// Step 2: Read product name in next loop iteration
|
||||
this->set_timeout(0, [this]() {
|
||||
uint16_t raw_product_name[16];
|
||||
if (!this->get_register(SEN6X_CMD_GET_PRODUCT_NAME, raw_product_name, 16, 20)) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
|
||||
this->product_name_ = SEN6XComponent::sensirion_convert_to_string_in_place(raw_product_name, 16);
|
||||
|
||||
Sen6xType inferred_type = this->infer_type_from_product_name_(this->product_name_);
|
||||
if (this->sen6x_type_ == UNKNOWN) {
|
||||
this->sen6x_type_ = inferred_type;
|
||||
if (inferred_type == UNKNOWN) {
|
||||
ESP_LOGE(TAG, "Unknown product '%s'", this->product_name_.c_str());
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Type inferred from product: %s", this->product_name_.c_str());
|
||||
} else if (this->sen6x_type_ != inferred_type && inferred_type != UNKNOWN) {
|
||||
ESP_LOGW(TAG, "Configured type (used) mismatches product '%s'", this->product_name_.c_str());
|
||||
}
|
||||
ESP_LOGI(TAG, "Product: %s", this->product_name_.c_str());
|
||||
|
||||
// Validate configured sensors against detected type and disable unsupported ones
|
||||
const bool has_voc_nox = (this->sen6x_type_ == SEN65 || this->sen6x_type_ == SEN66 ||
|
||||
this->sen6x_type_ == SEN68 || this->sen6x_type_ == SEN69C);
|
||||
const bool has_co2 = (this->sen6x_type_ == SEN63C || this->sen6x_type_ == SEN66 || this->sen6x_type_ == SEN69C);
|
||||
const bool has_hcho = (this->sen6x_type_ == SEN68 || this->sen6x_type_ == SEN69C);
|
||||
if (this->voc_sensor_ && !has_voc_nox) {
|
||||
ESP_LOGE(TAG, "VOC requires SEN65, SEN66, SEN68, or SEN69C");
|
||||
this->voc_sensor_ = nullptr;
|
||||
}
|
||||
if (this->nox_sensor_ && !has_voc_nox) {
|
||||
ESP_LOGE(TAG, "NOx requires SEN65, SEN66, SEN68, or SEN69C");
|
||||
this->nox_sensor_ = nullptr;
|
||||
}
|
||||
if (this->co2_sensor_ && !has_co2) {
|
||||
ESP_LOGE(TAG, "CO2 requires SEN63C, SEN66, or SEN69C");
|
||||
this->co2_sensor_ = nullptr;
|
||||
}
|
||||
if (this->hcho_sensor_ && !has_hcho) {
|
||||
ESP_LOGE(TAG, "Formaldehyde requires SEN68 or SEN69C");
|
||||
this->hcho_sensor_ = nullptr;
|
||||
}
|
||||
|
||||
// Step 3: Read firmware version and start measurements in next loop iteration
|
||||
this->set_timeout(0, [this]() {
|
||||
uint16_t raw_firmware_version = 0;
|
||||
if (!this->get_register(SEN6X_CMD_GET_FIRMWARE_VERSION, raw_firmware_version, 20)) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
this->firmware_version_major_ = (raw_firmware_version >> 8) & 0xFF;
|
||||
this->firmware_version_minor_ = raw_firmware_version & 0xFF;
|
||||
ESP_LOGI(TAG, "Firmware: %u.%u", this->firmware_version_major_, this->firmware_version_minor_);
|
||||
|
||||
if (!this->write_command(SEN6X_CMD_START_MEASUREMENTS)) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
|
||||
this->set_timeout(60000, [this]() { this->startup_complete_ = true; });
|
||||
this->initialized_ = true;
|
||||
ESP_LOGD(TAG, "Initialized");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void SEN6XComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"sen6x:\n"
|
||||
" Product: %s\n"
|
||||
" Serial: %s\n"
|
||||
" Firmware: %u.%u\n"
|
||||
" Address: 0x%02X",
|
||||
this->product_name_.c_str(), this->serial_number_.c_str(), this->firmware_version_major_,
|
||||
this->firmware_version_minor_, this->address_);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
LOG_SENSOR(" ", "PM 1.0", this->pm_1_0_sensor_);
|
||||
LOG_SENSOR(" ", "PM 2.5", this->pm_2_5_sensor_);
|
||||
LOG_SENSOR(" ", "PM 4.0", this->pm_4_0_sensor_);
|
||||
LOG_SENSOR(" ", "PM 10.0", this->pm_10_0_sensor_);
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
|
||||
LOG_SENSOR(" ", "VOC", this->voc_sensor_);
|
||||
LOG_SENSOR(" ", "NOx", this->nox_sensor_);
|
||||
LOG_SENSOR(" ", "HCHO", this->hcho_sensor_);
|
||||
LOG_SENSOR(" ", "CO2", this->co2_sensor_);
|
||||
}
|
||||
|
||||
void SEN6XComponent::update() {
|
||||
if (!this->initialized_) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t read_cmd;
|
||||
uint8_t read_words;
|
||||
set_read_command_and_words(this->sen6x_type_, read_cmd, read_words);
|
||||
|
||||
const uint8_t poll_retries = 24;
|
||||
auto poll_ready = std::make_shared<std::function<void(uint8_t)>>();
|
||||
*poll_ready = [this, poll_ready, read_cmd, read_words](uint8_t retries_left) {
|
||||
const uint8_t attempt = static_cast<uint8_t>(poll_retries - retries_left + 1);
|
||||
ESP_LOGV(TAG, "Data ready polling attempt %u", attempt);
|
||||
|
||||
if (!this->write_command(SEN6X_CMD_GET_DATA_READY_STATUS)) {
|
||||
this->status_set_warning();
|
||||
ESP_LOGD(TAG, "write data ready status error (%d)", this->last_error_);
|
||||
return;
|
||||
}
|
||||
|
||||
this->set_timeout(20, [this, poll_ready, retries_left, read_cmd, read_words]() {
|
||||
uint16_t raw_read_status;
|
||||
if (!this->read_data(&raw_read_status, 1)) {
|
||||
this->status_set_warning();
|
||||
ESP_LOGD(TAG, "read data ready status error (%d)", this->last_error_);
|
||||
return;
|
||||
}
|
||||
|
||||
if ((raw_read_status & 0x0001) == 0) {
|
||||
if (retries_left == 0) {
|
||||
this->status_set_warning();
|
||||
ESP_LOGD(TAG, "Data not ready");
|
||||
return;
|
||||
}
|
||||
this->set_timeout(50, [poll_ready, retries_left]() { (*poll_ready)(retries_left - 1); });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this->write_command(read_cmd)) {
|
||||
this->status_set_warning();
|
||||
ESP_LOGD(TAG, "Read measurement failed (%d)", this->last_error_);
|
||||
return;
|
||||
}
|
||||
|
||||
this->set_timeout(20, [this, read_words]() {
|
||||
uint16_t measurements[10];
|
||||
|
||||
if (!this->read_data(measurements, read_words)) {
|
||||
this->status_set_warning();
|
||||
ESP_LOGD(TAG, "Read data failed (%d)", this->last_error_);
|
||||
return;
|
||||
}
|
||||
int8_t voc_index = -1;
|
||||
int8_t nox_index = -1;
|
||||
int8_t hcho_index = -1;
|
||||
int8_t co2_index = -1;
|
||||
bool co2_uint16 = false;
|
||||
switch (this->sen6x_type_) {
|
||||
case SEN62:
|
||||
break;
|
||||
case SEN63C:
|
||||
co2_index = 6;
|
||||
break;
|
||||
case SEN65:
|
||||
voc_index = 6;
|
||||
nox_index = 7;
|
||||
break;
|
||||
case SEN66:
|
||||
voc_index = 6;
|
||||
nox_index = 7;
|
||||
co2_index = 8;
|
||||
co2_uint16 = true;
|
||||
break;
|
||||
case SEN68:
|
||||
voc_index = 6;
|
||||
nox_index = 7;
|
||||
hcho_index = 8;
|
||||
break;
|
||||
case SEN69C:
|
||||
voc_index = 6;
|
||||
nox_index = 7;
|
||||
hcho_index = 8;
|
||||
co2_index = 9;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
float pm_1_0 = measurements[0] / 10.0f;
|
||||
if (measurements[0] == 0xFFFF)
|
||||
pm_1_0 = NAN;
|
||||
float pm_2_5 = measurements[1] / 10.0f;
|
||||
if (measurements[1] == 0xFFFF)
|
||||
pm_2_5 = NAN;
|
||||
float pm_4_0 = measurements[2] / 10.0f;
|
||||
if (measurements[2] == 0xFFFF)
|
||||
pm_4_0 = NAN;
|
||||
float pm_10_0 = measurements[3] / 10.0f;
|
||||
if (measurements[3] == 0xFFFF)
|
||||
pm_10_0 = NAN;
|
||||
float humidity = static_cast<int16_t>(measurements[4]) / 100.0f;
|
||||
if (measurements[4] == 0x7FFF)
|
||||
humidity = NAN;
|
||||
float temperature = static_cast<int16_t>(measurements[5]) / 200.0f;
|
||||
if (measurements[5] == 0x7FFF)
|
||||
temperature = NAN;
|
||||
|
||||
float voc = NAN;
|
||||
float nox = NAN;
|
||||
float hcho = NAN;
|
||||
float co2 = NAN;
|
||||
|
||||
if (voc_index >= 0) {
|
||||
voc = static_cast<int16_t>(measurements[voc_index]) / 10.0f;
|
||||
if (measurements[voc_index] == 0x7FFF)
|
||||
voc = NAN;
|
||||
}
|
||||
if (nox_index >= 0) {
|
||||
nox = static_cast<int16_t>(measurements[nox_index]) / 10.0f;
|
||||
if (measurements[nox_index] == 0x7FFF)
|
||||
nox = NAN;
|
||||
}
|
||||
|
||||
if (hcho_index >= 0) {
|
||||
const uint16_t hcho_raw = measurements[hcho_index];
|
||||
hcho = hcho_raw / 10.0f;
|
||||
if (hcho_raw == 0xFFFF)
|
||||
hcho = NAN;
|
||||
}
|
||||
|
||||
if (co2_index >= 0) {
|
||||
if (co2_uint16) {
|
||||
const uint16_t co2_raw = measurements[co2_index];
|
||||
co2 = static_cast<float>(co2_raw);
|
||||
if (co2_raw == 0xFFFF)
|
||||
co2 = NAN;
|
||||
} else {
|
||||
const int16_t co2_raw = static_cast<int16_t>(measurements[co2_index]);
|
||||
co2 = static_cast<float>(co2_raw);
|
||||
if (co2_raw == 0x7FFF)
|
||||
co2 = NAN;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this->startup_complete_) {
|
||||
ESP_LOGD(TAG, "Startup delay, ignoring values");
|
||||
this->status_clear_warning();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->pm_1_0_sensor_ != nullptr)
|
||||
this->pm_1_0_sensor_->publish_state(pm_1_0);
|
||||
if (this->pm_2_5_sensor_ != nullptr)
|
||||
this->pm_2_5_sensor_->publish_state(pm_2_5);
|
||||
if (this->pm_4_0_sensor_ != nullptr)
|
||||
this->pm_4_0_sensor_->publish_state(pm_4_0);
|
||||
if (this->pm_10_0_sensor_ != nullptr)
|
||||
this->pm_10_0_sensor_->publish_state(pm_10_0);
|
||||
if (this->temperature_sensor_ != nullptr)
|
||||
this->temperature_sensor_->publish_state(temperature);
|
||||
if (this->humidity_sensor_ != nullptr)
|
||||
this->humidity_sensor_->publish_state(humidity);
|
||||
if (this->voc_sensor_ != nullptr)
|
||||
this->voc_sensor_->publish_state(voc);
|
||||
if (this->nox_sensor_ != nullptr)
|
||||
this->nox_sensor_->publish_state(nox);
|
||||
if (this->hcho_sensor_ != nullptr)
|
||||
this->hcho_sensor_->publish_state(hcho);
|
||||
if (this->co2_sensor_ != nullptr)
|
||||
this->co2_sensor_->publish_state(co2);
|
||||
|
||||
this->status_clear_warning();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
(*poll_ready)(poll_retries);
|
||||
}
|
||||
|
||||
SEN6XComponent::Sen6xType SEN6XComponent::infer_type_from_product_name_(const std::string &product_name) {
|
||||
if (product_name == "SEN62")
|
||||
return SEN62;
|
||||
if (product_name == "SEN63C")
|
||||
return SEN63C;
|
||||
if (product_name == "SEN65")
|
||||
return SEN65;
|
||||
if (product_name == "SEN66")
|
||||
return SEN66;
|
||||
if (product_name == "SEN68")
|
||||
return SEN68;
|
||||
if (product_name == "SEN69C")
|
||||
return SEN69C;
|
||||
return UNKNOWN;
|
||||
}
|
||||
|
||||
} // namespace esphome::sen6x
|
||||
43
esphome/components/sen6x/sen6x.h
Normal file
43
esphome/components/sen6x/sen6x.h
Normal file
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/sensirion_common/i2c_sensirion.h"
|
||||
|
||||
namespace esphome::sen6x {
|
||||
|
||||
class SEN6XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice {
|
||||
SUB_SENSOR(pm_1_0)
|
||||
SUB_SENSOR(pm_2_5)
|
||||
SUB_SENSOR(pm_4_0)
|
||||
SUB_SENSOR(pm_10_0)
|
||||
SUB_SENSOR(temperature)
|
||||
SUB_SENSOR(humidity)
|
||||
SUB_SENSOR(voc)
|
||||
SUB_SENSOR(nox)
|
||||
SUB_SENSOR(co2)
|
||||
SUB_SENSOR(hcho)
|
||||
|
||||
public:
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void update() override;
|
||||
|
||||
enum Sen6xType { SEN62, SEN63C, SEN65, SEN66, SEN68, SEN69C, UNKNOWN };
|
||||
|
||||
void set_type(const std::string &type) { sen6x_type_ = infer_type_from_product_name_(type); }
|
||||
|
||||
protected:
|
||||
Sen6xType infer_type_from_product_name_(const std::string &product_name);
|
||||
|
||||
bool initialized_{false};
|
||||
std::string product_name_;
|
||||
Sen6xType sen6x_type_{UNKNOWN};
|
||||
std::string serial_number_;
|
||||
uint8_t firmware_version_major_{0};
|
||||
uint8_t firmware_version_minor_{0};
|
||||
bool startup_complete_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::sen6x
|
||||
149
esphome/components/sen6x/sensor.py
Normal file
149
esphome/components/sen6x/sensor.py
Normal file
@@ -0,0 +1,149 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, sensirion_common, sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_CO2,
|
||||
CONF_FORMALDEHYDE,
|
||||
CONF_HUMIDITY,
|
||||
CONF_ID,
|
||||
CONF_NOX,
|
||||
CONF_PM_1_0,
|
||||
CONF_PM_2_5,
|
||||
CONF_PM_4_0,
|
||||
CONF_PM_10_0,
|
||||
CONF_TEMPERATURE,
|
||||
CONF_TYPE,
|
||||
CONF_VOC,
|
||||
DEVICE_CLASS_AQI,
|
||||
DEVICE_CLASS_CARBON_DIOXIDE,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_PM1,
|
||||
DEVICE_CLASS_PM10,
|
||||
DEVICE_CLASS_PM25,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
ICON_CHEMICAL_WEAPON,
|
||||
ICON_MOLECULE_CO2,
|
||||
ICON_RADIATOR,
|
||||
ICON_THERMOMETER,
|
||||
ICON_WATER_PERCENT,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_MICROGRAMS_PER_CUBIC_METER,
|
||||
UNIT_PARTS_PER_MILLION,
|
||||
UNIT_PERCENT,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@martgras", "@mebner86", "@mikelawrence", "@tuct"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
AUTO_LOAD = ["sensirion_common"]
|
||||
|
||||
sen6x_ns = cg.esphome_ns.namespace("sen6x")
|
||||
SEN6XComponent = sen6x_ns.class_(
|
||||
"SEN6XComponent", cg.PollingComponent, sensirion_common.SensirionI2CDevice
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(SEN6XComponent),
|
||||
cv.Optional(CONF_TYPE): cv.one_of(
|
||||
"SEN62", "SEN63C", "SEN65", "SEN66", "SEN68", "SEN69C", upper=True
|
||||
),
|
||||
cv.Optional(CONF_PM_1_0): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
|
||||
icon=ICON_CHEMICAL_WEAPON,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_PM1,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_PM_2_5): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
|
||||
icon=ICON_CHEMICAL_WEAPON,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_PM25,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_PM_4_0): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
|
||||
icon=ICON_CHEMICAL_WEAPON,
|
||||
accuracy_decimals=2,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_PM_10_0): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
|
||||
icon=ICON_CHEMICAL_WEAPON,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_PM10,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
icon=ICON_THERMOMETER,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
icon=ICON_WATER_PERCENT,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_VOC): sensor.sensor_schema(
|
||||
icon=ICON_RADIATOR,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_AQI,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_NOX): sensor.sensor_schema(
|
||||
icon=ICON_RADIATOR,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_AQI,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_CO2): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PARTS_PER_MILLION,
|
||||
icon=ICON_MOLECULE_CO2,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_CARBON_DIOXIDE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_FORMALDEHYDE): sensor.sensor_schema(
|
||||
unit_of_measurement="ppb",
|
||||
icon=ICON_RADIATOR,
|
||||
accuracy_decimals=0,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
.extend(i2c.i2c_device_schema(0x6B))
|
||||
)
|
||||
|
||||
SENSOR_MAP = {
|
||||
CONF_PM_1_0: "set_pm_1_0_sensor",
|
||||
CONF_PM_2_5: "set_pm_2_5_sensor",
|
||||
CONF_PM_4_0: "set_pm_4_0_sensor",
|
||||
CONF_PM_10_0: "set_pm_10_0_sensor",
|
||||
CONF_TEMPERATURE: "set_temperature_sensor",
|
||||
CONF_HUMIDITY: "set_humidity_sensor",
|
||||
CONF_VOC: "set_voc_sensor",
|
||||
CONF_NOX: "set_nox_sensor",
|
||||
CONF_CO2: "set_co2_sensor",
|
||||
CONF_FORMALDEHYDE: "set_hcho_sensor",
|
||||
}
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
if CONF_TYPE in config:
|
||||
cg.add(var.set_type(config[CONF_TYPE]))
|
||||
|
||||
for key, func_name in SENSOR_MAP.items():
|
||||
if cfg := config.get(key):
|
||||
sens = await sensor.new_sensor(cfg)
|
||||
cg.add(getattr(var, func_name)(sens))
|
||||
@@ -12,6 +12,8 @@ static const char *const TAG = "sht3xd";
|
||||
// To ensure compatibility, reading serial number using the register with clock stretching register enabled
|
||||
// (used originally in this component) is tried first and if that fails the alternate register address
|
||||
// with clock stretching disabled is read.
|
||||
// If both fail (e.g. some clones don't support the command), we continue so temp/humidity still work.
|
||||
// Second attempt uses 10ms delay for boards that need more time before read (max permitted by ESPHome guidelines).
|
||||
|
||||
static const uint16_t SHT3XD_COMMAND_READ_SERIAL_NUMBER_CLOCK_STRETCHING = 0x3780;
|
||||
static const uint16_t SHT3XD_COMMAND_READ_SERIAL_NUMBER = 0x3682;
|
||||
@@ -25,49 +27,28 @@ static const uint16_t SHT3XD_COMMAND_POLLING_H = 0x2400;
|
||||
static const uint16_t SHT3XD_COMMAND_FETCH_DATA = 0xE000;
|
||||
|
||||
void SHT3XDComponent::setup() {
|
||||
uint16_t raw_serial_number[2];
|
||||
uint16_t raw_serial_number[2]{0};
|
||||
if (!this->get_register(SHT3XD_COMMAND_READ_SERIAL_NUMBER_CLOCK_STRETCHING, raw_serial_number, 2)) {
|
||||
this->error_code_ = READ_SERIAL_STRETCHED_FAILED;
|
||||
if (!this->get_register(SHT3XD_COMMAND_READ_SERIAL_NUMBER, raw_serial_number, 2)) {
|
||||
this->error_code_ = READ_SERIAL_FAILED;
|
||||
this->mark_failed();
|
||||
return;
|
||||
if (!this->get_register(SHT3XD_COMMAND_READ_SERIAL_NUMBER, raw_serial_number, 2, 10)) {
|
||||
ESP_LOGW(TAG, "Serial number read failed, continuing without it (clone or non-standard sensor)");
|
||||
}
|
||||
}
|
||||
|
||||
this->serial_number_ = (uint32_t(raw_serial_number[0]) << 16) | uint32_t(raw_serial_number[1]);
|
||||
|
||||
if (!this->write_command(heater_enabled_ ? SHT3XD_COMMAND_HEATER_ENABLE : SHT3XD_COMMAND_HEATER_DISABLE)) {
|
||||
this->error_code_ = WRITE_HEATER_MODE_FAILED;
|
||||
this->mark_failed();
|
||||
if (!this->write_command(this->heater_enabled_ ? SHT3XD_COMMAND_HEATER_ENABLE : SHT3XD_COMMAND_HEATER_DISABLE)) {
|
||||
this->mark_failed(LOG_STR("Failed to set heater mode"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void SHT3XDComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "SHT3xD:");
|
||||
switch (this->error_code_) {
|
||||
case READ_SERIAL_FAILED:
|
||||
ESP_LOGD(TAG, " Error reading serial number");
|
||||
break;
|
||||
case WRITE_HEATER_MODE_FAILED:
|
||||
ESP_LOGD(TAG, " Error writing heater mode");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, " Communication with SHT3xD failed!");
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG,
|
||||
" Serial Number: 0x%08" PRIX32 "\n"
|
||||
" Heater Enabled: %s",
|
||||
this->serial_number_, TRUEFALSE(this->heater_enabled_));
|
||||
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"SHT3xD:\n"
|
||||
" Serial Number: 0x%08" PRIX32 "\n"
|
||||
" Heater Enabled: %s",
|
||||
this->serial_number_, TRUEFALSE(this->heater_enabled_));
|
||||
LOG_I2C_DEVICE(this);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/sensirion_common/i2c_sensirion.h"
|
||||
|
||||
#include <cinttypes>
|
||||
|
||||
namespace esphome {
|
||||
namespace sht3xd {
|
||||
|
||||
@@ -21,13 +19,6 @@ class SHT3XDComponent : public PollingComponent, public sensirion_common::Sensir
|
||||
void set_heater_enabled(bool heater_enabled) { heater_enabled_ = heater_enabled; }
|
||||
|
||||
protected:
|
||||
enum ErrorCode {
|
||||
NONE = 0,
|
||||
READ_SERIAL_STRETCHED_FAILED,
|
||||
READ_SERIAL_FAILED,
|
||||
WRITE_HEATER_MODE_FAILED,
|
||||
} error_code_{NONE};
|
||||
|
||||
sensor::Sensor *temperature_sensor_{nullptr};
|
||||
sensor::Sensor *humidity_sensor_{nullptr};
|
||||
bool heater_enabled_{true};
|
||||
|
||||
@@ -1545,7 +1545,7 @@ json::SerializationBuffer<> WebServer::climate_json_(climate::Climate *obj, Json
|
||||
JsonArray opt = root[ESPHOME_F("modes")].to<JsonArray>();
|
||||
for (climate::ClimateMode m : traits.get_supported_modes())
|
||||
opt.add(PSTR_LOCAL(climate::climate_mode_to_string(m)));
|
||||
if (!traits.get_supported_custom_fan_modes().empty()) {
|
||||
if (traits.get_supports_fan_modes()) {
|
||||
JsonArray opt = root[ESPHOME_F("fan_modes")].to<JsonArray>();
|
||||
for (climate::ClimateFanMode m : traits.get_supported_fan_modes())
|
||||
opt.add(PSTR_LOCAL(climate::climate_fan_mode_to_string(m)));
|
||||
@@ -1561,12 +1561,12 @@ json::SerializationBuffer<> WebServer::climate_json_(climate::Climate *obj, Json
|
||||
for (auto swing_mode : traits.get_supported_swing_modes())
|
||||
opt.add(PSTR_LOCAL(climate::climate_swing_mode_to_string(swing_mode)));
|
||||
}
|
||||
if (traits.get_supports_presets() && obj->preset.has_value()) {
|
||||
if (traits.get_supports_presets()) {
|
||||
JsonArray opt = root[ESPHOME_F("presets")].to<JsonArray>();
|
||||
for (climate::ClimatePreset m : traits.get_supported_presets())
|
||||
opt.add(PSTR_LOCAL(climate::climate_preset_to_string(m)));
|
||||
}
|
||||
if (!traits.get_supported_custom_presets().empty() && obj->has_custom_preset()) {
|
||||
if (!traits.get_supported_custom_presets().empty()) {
|
||||
JsonArray opt = root[ESPHOME_F("custom_presets")].to<JsonArray>();
|
||||
for (auto const &custom_preset : traits.get_supported_custom_presets())
|
||||
opt.add(custom_preset);
|
||||
@@ -1607,6 +1607,11 @@ json::SerializationBuffer<> WebServer::climate_json_(climate::Climate *obj, Json
|
||||
? "NA"
|
||||
: (value_accuracy_to_buf(temp_buf, obj->current_temperature, current_accuracy), temp_buf);
|
||||
}
|
||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) {
|
||||
root[ESPHOME_F("current_humidity")] = std::isnan(obj->current_humidity)
|
||||
? "NA"
|
||||
: (value_accuracy_to_buf(temp_buf, obj->current_humidity, 0), temp_buf);
|
||||
}
|
||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
|
||||
climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
|
||||
root[ESPHOME_F("target_temperature_low")] =
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include <zephyr/drivers/watchdog.h>
|
||||
#include <zephyr/sys/reboot.h>
|
||||
#include <zephyr/random/random.h>
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/defines.h"
|
||||
@@ -17,8 +16,8 @@ static const device *const WDT = DEVICE_DT_GET(DT_ALIAS(watchdog0));
|
||||
#endif
|
||||
|
||||
void yield() { ::k_yield(); }
|
||||
uint32_t millis() { return k_ticks_to_ms_floor32(k_uptime_ticks()); }
|
||||
uint64_t millis_64() { return App.scheduler.millis_64_impl_(millis()); }
|
||||
uint32_t millis() { return static_cast<uint32_t>(millis_64()); }
|
||||
uint64_t millis_64() { return static_cast<uint64_t>(k_uptime_get()); }
|
||||
uint32_t micros() { return k_ticks_to_us_floor32(k_uptime_ticks()); }
|
||||
void delayMicroseconds(uint32_t us) { ::k_usleep(us); }
|
||||
void delay(uint32_t ms) { ::k_msleep(ms); }
|
||||
|
||||
@@ -256,7 +256,7 @@ void Application::process_dump_config_() {
|
||||
#endif
|
||||
}
|
||||
|
||||
this->components_[this->dump_config_at_]->call_dump_config();
|
||||
this->components_[this->dump_config_at_]->call_dump_config_();
|
||||
this->dump_config_at_++;
|
||||
}
|
||||
|
||||
|
||||
@@ -211,7 +211,7 @@ bool Component::cancel_retry(uint32_t id) {
|
||||
|
||||
void Component::call_loop_() { this->loop(); }
|
||||
void Component::call_setup() { this->setup(); }
|
||||
void Component::call_dump_config() {
|
||||
void Component::call_dump_config_() {
|
||||
this->dump_config();
|
||||
if (this->is_failed()) {
|
||||
// Look up error message from global vector
|
||||
|
||||
@@ -291,7 +291,7 @@ class Component {
|
||||
|
||||
void call_loop_();
|
||||
virtual void call_setup();
|
||||
virtual void call_dump_config();
|
||||
void call_dump_config_();
|
||||
|
||||
/// Helper to set component state (clears state bits and sets new state)
|
||||
void set_component_state_(uint8_t state);
|
||||
|
||||
@@ -28,7 +28,7 @@ static constexpr size_t MAX_POOL_SIZE = 5;
|
||||
// Set to 5 to match the pool size - when we have as many cancelled items as our
|
||||
// pool can hold, it's time to clean up and recycle them.
|
||||
static constexpr uint32_t MAX_LOGICALLY_DELETED_ITEMS = 5;
|
||||
#if !defined(USE_ESP32) && !defined(USE_HOST)
|
||||
#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) && !defined(USE_RP2040)
|
||||
// Half the 32-bit range - used to detect rollovers vs normal time progression
|
||||
static constexpr uint32_t HALF_MAX_UINT32 = std::numeric_limits<uint32_t>::max() / 2;
|
||||
#endif
|
||||
@@ -475,12 +475,13 @@ void HOT Scheduler::call(uint32_t now) {
|
||||
if (now_64 - last_print > 2000) {
|
||||
last_print = now_64;
|
||||
std::vector<SchedulerItemPtr> old_items;
|
||||
#if !defined(USE_ESP32) && !defined(USE_HOST) && defined(ESPHOME_THREAD_MULTI_ATOMICS)
|
||||
#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) && !defined(USE_RP2040) && \
|
||||
defined(ESPHOME_THREAD_MULTI_ATOMICS)
|
||||
const auto last_dbg = this->last_millis_.load(std::memory_order_relaxed);
|
||||
const auto major_dbg = this->millis_major_.load(std::memory_order_relaxed);
|
||||
ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(),
|
||||
this->scheduler_item_pool_.size(), now_64, major_dbg, last_dbg);
|
||||
#elif !defined(USE_ESP32) && !defined(USE_HOST)
|
||||
#elif !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) && !defined(USE_RP2040)
|
||||
ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(),
|
||||
this->scheduler_item_pool_.size(), now_64, this->millis_major_, this->last_millis_);
|
||||
#else
|
||||
@@ -714,7 +715,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, NameType name_type
|
||||
return total_cancelled > 0;
|
||||
}
|
||||
|
||||
#if !defined(USE_ESP32) && !defined(USE_HOST)
|
||||
#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) && !defined(USE_RP2040)
|
||||
uint64_t Scheduler::millis_64_impl_(uint32_t now) {
|
||||
// THREAD SAFETY NOTE:
|
||||
// This function has three implementations, based on the precompiler flags
|
||||
@@ -872,7 +873,7 @@ uint64_t Scheduler::millis_64_impl_(uint32_t now) {
|
||||
"No platform threading model defined. One of ESPHOME_THREAD_SINGLE, ESPHOME_THREAD_MULTI_NO_ATOMICS, or ESPHOME_THREAD_MULTI_ATOMICS must be defined."
|
||||
#endif
|
||||
}
|
||||
#endif // !USE_ESP32 && !USE_HOST
|
||||
#endif // !USE_ESP32 && !USE_HOST && !USE_ZEPHYR && !USE_RP2040
|
||||
|
||||
bool HOT Scheduler::SchedulerItem::cmp(const SchedulerItemPtr &a, const SchedulerItemPtr &b) {
|
||||
// High bits are almost always equal (change only on 32-bit rollover ~49 days)
|
||||
|
||||
@@ -284,10 +284,10 @@ class Scheduler {
|
||||
bool cancel_retry_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id);
|
||||
|
||||
// Extend a 32-bit millis() value to 64-bit. Use when the caller already has a fresh now.
|
||||
// On ESP32, ignores now and uses esp_timer_get_time() directly (native 64-bit).
|
||||
// On non-ESP32, extends now to 64-bit using rollover tracking.
|
||||
// On ESP32, Host, Zephyr, and RP2040, ignores now and uses the native 64-bit time source via millis_64().
|
||||
// On other platforms, extends now to 64-bit using rollover tracking.
|
||||
uint64_t millis_64_from_(uint32_t now) {
|
||||
#if defined(USE_ESP32) || defined(USE_HOST)
|
||||
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_ZEPHYR) || defined(USE_RP2040)
|
||||
(void) now;
|
||||
return millis_64();
|
||||
#else
|
||||
@@ -295,7 +295,7 @@ class Scheduler {
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !defined(USE_ESP32) && !defined(USE_HOST)
|
||||
#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) && !defined(USE_RP2040)
|
||||
// On platforms without native 64-bit time, millis_64() HAL function delegates to this
|
||||
// method which tracks 32-bit millis() rollover using millis_major_ and last_millis_.
|
||||
friend uint64_t millis_64();
|
||||
@@ -567,8 +567,8 @@ class Scheduler {
|
||||
// to synchronize between tasks (see https://github.com/esphome/backlog/issues/52)
|
||||
std::vector<SchedulerItemPtr> scheduler_item_pool_;
|
||||
|
||||
#if !defined(USE_ESP32) && !defined(USE_HOST)
|
||||
// On platforms with native 64-bit time (ESP32, Host), no rollover tracking needed.
|
||||
#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) && !defined(USE_RP2040)
|
||||
// On platforms with native 64-bit time (ESP32, Host, Zephyr, RP2040), no rollover tracking needed.
|
||||
// On other platforms, these fields track 32-bit millis() rollover for millis_64_impl_().
|
||||
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
|
||||
/*
|
||||
@@ -598,7 +598,7 @@ class Scheduler {
|
||||
#else /* not ESPHOME_THREAD_MULTI_ATOMICS */
|
||||
uint16_t millis_major_{0};
|
||||
#endif /* else ESPHOME_THREAD_MULTI_ATOMICS */
|
||||
#endif /* !USE_ESP32 && !USE_HOST */
|
||||
#endif /* !USE_ESP32 && !USE_HOST && !USE_ZEPHYR && !USE_RP2040 */
|
||||
};
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
@@ -10,7 +10,7 @@ tzdata>=2021.1 # from time
|
||||
pyserial==3.5
|
||||
platformio==6.1.19
|
||||
esptool==5.2.0
|
||||
click==8.1.7
|
||||
click==8.3.1
|
||||
esphome-dashboard==20260210.0
|
||||
aioesphomeapi==44.2.0
|
||||
zeroconf==0.148.0
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
pylint==4.0.5
|
||||
flake8==7.3.0 # also change in .pre-commit-config.yaml when updating
|
||||
ruff==0.15.3 # also change in .pre-commit-config.yaml when updating
|
||||
ruff==0.15.4 # also change in .pre-commit-config.yaml when updating
|
||||
pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating
|
||||
pre-commit
|
||||
|
||||
|
||||
@@ -98,8 +98,12 @@ def run_tests(selected_components: list[str]) -> int:
|
||||
|
||||
components = sorted(components)
|
||||
|
||||
# Obtain possible dependencies for the requested components:
|
||||
components_with_dependencies = sorted(get_all_dependencies(set(components)))
|
||||
# Obtain possible dependencies for the requested components.
|
||||
# Always include 'time' because USE_TIME_TIMEZONE is defined as a build flag,
|
||||
# which causes core/time.h to include components/time/posix_tz.h.
|
||||
components_with_dependencies = sorted(
|
||||
get_all_dependencies(set(components) | {"time"})
|
||||
)
|
||||
|
||||
# Build a list of include folders, one folder per component containing tests.
|
||||
# A special replacement main.cpp is located in /tests/components/main.cpp
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
variant: esp32s3
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
psram:
|
||||
mode: octal
|
||||
@@ -0,0 +1,11 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
variant: esp32p4
|
||||
framework:
|
||||
type: esp-idf
|
||||
advanced:
|
||||
execute_from_psram: true
|
||||
|
||||
psram:
|
||||
@@ -0,0 +1,12 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
variant: esp32s3
|
||||
framework:
|
||||
type: esp-idf
|
||||
advanced:
|
||||
execute_from_psram: true
|
||||
|
||||
psram:
|
||||
mode: octal
|
||||
@@ -2,13 +2,17 @@
|
||||
Test ESP32 configuration
|
||||
"""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.esp32 import VARIANTS
|
||||
from esphome.components.esp32.const import KEY_ESP32, KEY_SDKCONFIG_OPTIONS
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ESPHOME, PlatformFramework
|
||||
from esphome.core import CORE
|
||||
from tests.component_tests.types import SetCoreConfigCallable
|
||||
|
||||
|
||||
@@ -70,7 +74,7 @@ def test_esp32_config(
|
||||
"advanced": {"execute_from_psram": True},
|
||||
},
|
||||
},
|
||||
r"'execute_from_psram' is only supported on ESP32S3 variant @ data\['framework'\]\['advanced'\]\['execute_from_psram'\]",
|
||||
r"'execute_from_psram' is not available on this esp32 variant @ data\['framework'\]\['advanced'\]\['execute_from_psram'\]",
|
||||
id="execute_from_psram_invalid_for_variant_config",
|
||||
),
|
||||
pytest.param(
|
||||
@@ -82,7 +86,18 @@ def test_esp32_config(
|
||||
},
|
||||
},
|
||||
r"'execute_from_psram' requires PSRAM to be configured @ data\['framework'\]\['advanced'\]\['execute_from_psram'\]",
|
||||
id="execute_from_psram_requires_psram_config",
|
||||
id="execute_from_psram_requires_psram_s3_config",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"variant": "esp32p4",
|
||||
"framework": {
|
||||
"type": "esp-idf",
|
||||
"advanced": {"execute_from_psram": True},
|
||||
},
|
||||
},
|
||||
r"'execute_from_psram' requires PSRAM to be configured @ data\['framework'\]\['advanced'\]\['execute_from_psram'\]",
|
||||
id="execute_from_psram_requires_psram_p4_config",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
@@ -108,3 +123,39 @@ def test_esp32_configuration_errors(
|
||||
|
||||
with pytest.raises(cv.Invalid, match=error_match):
|
||||
FINAL_VALIDATE_SCHEMA(CONFIG_SCHEMA(config))
|
||||
|
||||
|
||||
def test_execute_from_psram_s3_sdkconfig(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""Test that execute_from_psram on ESP32-S3 sets the correct sdkconfig options."""
|
||||
generate_main(component_config_path("execute_from_psram_s3.yaml"))
|
||||
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
|
||||
assert sdkconfig.get("CONFIG_SPIRAM_FETCH_INSTRUCTIONS") is True
|
||||
assert sdkconfig.get("CONFIG_SPIRAM_RODATA") is True
|
||||
assert "CONFIG_SPIRAM_XIP_FROM_PSRAM" not in sdkconfig
|
||||
|
||||
|
||||
def test_execute_from_psram_p4_sdkconfig(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""Test that execute_from_psram on ESP32-P4 sets the correct sdkconfig options."""
|
||||
generate_main(component_config_path("execute_from_psram_p4.yaml"))
|
||||
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
|
||||
assert sdkconfig.get("CONFIG_SPIRAM_XIP_FROM_PSRAM") is True
|
||||
assert "CONFIG_SPIRAM_FETCH_INSTRUCTIONS" not in sdkconfig
|
||||
assert "CONFIG_SPIRAM_RODATA" not in sdkconfig
|
||||
|
||||
|
||||
def test_execute_from_psram_disabled_sdkconfig(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""Test that without execute_from_psram, no XIP sdkconfig options are set."""
|
||||
generate_main(component_config_path("execute_from_psram_disabled.yaml"))
|
||||
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
|
||||
assert "CONFIG_SPIRAM_FETCH_INSTRUCTIONS" not in sdkconfig
|
||||
assert "CONFIG_SPIRAM_RODATA" not in sdkconfig
|
||||
assert "CONFIG_SPIRAM_XIP_FROM_PSRAM" not in sdkconfig
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
gps:
|
||||
latitude:
|
||||
name: "Latitude"
|
||||
id: gps_lat
|
||||
on_value:
|
||||
then:
|
||||
- logger.log:
|
||||
format: "%.6f, %.6f"
|
||||
args: [id(gps_lat).state, id(gps_long).state]
|
||||
longitude:
|
||||
name: "Longitude"
|
||||
id: gps_long
|
||||
altitude:
|
||||
name: "Altitude"
|
||||
speed:
|
||||
|
||||
37
tests/components/sen6x/common.yaml
Normal file
37
tests/components/sen6x/common.yaml
Normal file
@@ -0,0 +1,37 @@
|
||||
sensor:
|
||||
# Test with explicit type parameter
|
||||
- platform: sen6x
|
||||
id: sen6x_sensor
|
||||
type: SEN69C
|
||||
i2c_id: i2c_bus
|
||||
temperature:
|
||||
name: Temperature
|
||||
accuracy_decimals: 1
|
||||
humidity:
|
||||
name: Humidity
|
||||
accuracy_decimals: 0
|
||||
pm_1_0:
|
||||
name: PM <1µm Weight concentration
|
||||
id: pm_1_0
|
||||
accuracy_decimals: 1
|
||||
pm_2_5:
|
||||
name: PM <2.5µm Weight concentration
|
||||
id: pm_2_5
|
||||
accuracy_decimals: 1
|
||||
pm_4_0:
|
||||
name: PM <4µm Weight concentration
|
||||
id: pm_4_0
|
||||
accuracy_decimals: 1
|
||||
pm_10_0:
|
||||
name: PM <10µm Weight concentration
|
||||
id: pm_10_0
|
||||
accuracy_decimals: 1
|
||||
nox:
|
||||
name: NOx
|
||||
voc:
|
||||
name: VOC
|
||||
co2:
|
||||
name: Carbon Dioxide
|
||||
formaldehyde:
|
||||
name: Formaldehyde
|
||||
address: 0x6B
|
||||
4
tests/components/sen6x/test.esp32-idf.yaml
Normal file
4
tests/components/sen6x/test.esp32-idf.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
4
tests/components/sen6x/test.esp8266-ard.yaml
Normal file
4
tests/components/sen6x/test.esp8266-ard.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
4
tests/components/sen6x/test.rp2040-ard.yaml
Normal file
4
tests/components/sen6x/test.rp2040-ard.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
Reference in New Issue
Block a user