Compare commits

..

30 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
90407aa9f2 Fix: Fetch file content from PR head using GitHub API
The workflow was checking out the base branch but trying to read files
from the filesystem, which meant it couldn't see changes in the PR.
Now using github.rest.repos.getContent with the PR head SHA to fetch
the actual file content from the PR branch.

Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2026-01-26 02:35:36 +00:00
copilot-swe-agent[bot]
a167ea8269 Fix triple-quote regex pattern for multiline strings
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2026-01-26 01:03:21 +00:00
copilot-swe-agent[bot]
354f60a9cf Address code review: add triple-quote support and improve error handling
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2026-01-26 01:01:40 +00:00
copilot-swe-agent[bot]
b8ac92c537 Improve regex and add waveshare_epaper deprecation for testing
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2026-01-26 00:59:35 +00:00
copilot-swe-agent[bot]
71390c075e Add test deprecated component for validation
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2026-01-26 00:58:09 +00:00
copilot-swe-agent[bot]
0ad82b8bbf Add deprecated component detection to auto-label workflow
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2026-01-26 00:57:23 +00:00
copilot-swe-agent[bot]
7863ac8b3a Initial plan 2026-01-26 00:53:55 +00:00
Jonathan Swoboda
011407ea8b Merge branch 'release' into dev 2026-01-25 13:21:39 -05:00
Jonathan Swoboda
1141e83a7c Merge pull request #13529 from esphome/bump-2026.1.2
2026.1.2
2026-01-25 13:21:26 -05:00
Jonathan Swoboda
214ce95cf3 Bump version to 2026.1.2 2026-01-25 12:22:18 -05:00
J. Nick Koston
3a7b83ba93 [wifi] Fix scan flag race condition causing reconnect failure on ESP8266/LibreTiny (#13514) 2026-01-25 12:22:18 -05:00
Clyde Stubbs
cc2f3d85dc [wifi] Fix watchdog timeout on P4 WiFi scan (#13520) 2026-01-25 12:22:18 -05:00
Jonathan Swoboda
723f67d5e2 [i2c] Increase ESP-IDF I2C transaction timeout from 20ms to 100ms (#13483)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 12:22:18 -05:00
Jonathan Swoboda
70e45706d9 [modbus_controller] Fix YAML serialization error with custom_command (#13482)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 12:22:18 -05:00
Jas Strong
56a2a2269f [rd03d] Fix speed and resolution field order (#13495)
Co-authored-by: jas <jas@asspa.in>
2026-01-25 12:22:18 -05:00
Keith Burzinski
d6841ba33a [light] Fix cwww state restore (#13493) 2026-01-25 12:22:18 -05:00
Clyde Stubbs
10cbd0164a [lvgl] Fix setting empty text (#13494) 2026-01-25 12:22:18 -05:00
Big Mike
d285706b41 [sen5x] Fix store baseline functionality (#13469) 2026-01-25 12:22:18 -05:00
J. Nick Koston
ef469c20df [slow_pwm] Fix dump_summary deprecation warning (#13460) 2026-01-25 12:22:18 -05:00
Clyde Stubbs
6870d3dc50 [mipi_rgb] Add software reset command to st7701s init sequence (#13470) 2026-01-25 12:22:18 -05:00
Keith Burzinski
9cc39621a6 [ir_rf_proxy] Remove unnecessary headers, add tests (#13464) 2026-01-25 12:22:18 -05:00
J. Nick Koston
c4f7d09553 [rpi_dpi_rgb] Fix dump_summary deprecation warning (#13461) 2026-01-25 12:22:18 -05:00
J. Nick Koston
ab1661ef22 [mipi_rgb] Fix dump_summary deprecation warning (#13463) 2026-01-25 12:22:18 -05:00
J. Nick Koston
ccbf17d5ab [st7701s] Fix dump_summary deprecation warning (#13462) 2026-01-25 12:22:18 -05:00
J. Nick Koston
bac96086be [wifi] Fix scan flag race condition causing reconnect failure on ESP8266/LibreTiny (#13514) 2026-01-25 12:16:07 -05:00
Clyde Stubbs
c32e4bc65b [wifi] Fix watchdog timeout on P4 WiFi scan (#13520) 2026-01-26 03:52:23 +11:00
Douwe
993765d732 [water_heater] Remove Component inheritance from base class (#13510)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 01:18:13 +00:00
Stephen Cox
8d84fe0113 [sy6970] Support for the sy6970 BMS chip (#13311) 2026-01-25 08:31:26 +11:00
Big Mike
58746b737f [sen5x] Eliminate product name string (#13489) 2026-01-24 11:07:12 -10:00
Big Mike
f93e843972 [sen5x] Fix mangled serial number (#13491) 2026-01-24 09:55:51 -10:00
46 changed files with 1158 additions and 205 deletions

View File

@@ -46,6 +46,7 @@ jobs:
const BOT_COMMENT_MARKER = '<!-- auto-label-pr-bot -->';
const CODEOWNERS_MARKER = '<!-- codeowners-request -->';
const TOO_BIG_MARKER = '<!-- too-big-request -->';
const DEPRECATED_COMPONENT_MARKER = '<!-- deprecated-component-request -->';
const MANAGED_LABELS = [
'new-component',
@@ -69,7 +70,8 @@ jobs:
'new-feature',
'breaking-change',
'developer-breaking-change',
'code-quality'
'code-quality',
'deprecated_component'
];
const DOCS_PR_PATTERNS = [
@@ -382,6 +384,75 @@ jobs:
return labels;
}
// Strategy: Deprecated component detection
async function detectDeprecatedComponents() {
const labels = new Set();
const deprecatedInfo = [];
// Compile regex once for better performance
const componentFileRegex = /^esphome\/components\/([^\/]+)\/[^/]*\.py$/;
// Get files that are modified or added in components directory
const componentFiles = changedFiles.filter(file => componentFileRegex.test(file));
if (componentFiles.length === 0) {
return { labels, deprecatedInfo };
}
// Extract unique component names using the same regex
const components = new Set();
for (const file of componentFiles) {
const match = file.match(componentFileRegex);
if (match) {
components.add(match[1]);
}
}
// Get PR head SHA to fetch files from the PR branch
const prHeadSha = context.payload.pull_request.head.sha;
// Check each component's __init__.py for DEPRECATED_COMPONENT constant
for (const component of components) {
const initFile = `esphome/components/${component}/__init__.py`;
try {
// Fetch file content from PR head using GitHub API
const { data: fileData } = await github.rest.repos.getContent({
owner,
repo,
path: initFile,
ref: prHeadSha
});
// Decode base64 content
const content = Buffer.from(fileData.content, 'base64').toString('utf8');
// Look for DEPRECATED_COMPONENT = "message" or DEPRECATED_COMPONENT = 'message'
// Support single quotes, double quotes, and triple quotes (for multiline)
const doubleQuoteMatch = content.match(/DEPRECATED_COMPONENT\s*=\s*"""([\s\S]*?)"""/s) ||
content.match(/DEPRECATED_COMPONENT\s*=\s*"((?:[^"\\]|\\.)*)"/);
const singleQuoteMatch = content.match(/DEPRECATED_COMPONENT\s*=\s*'''([\s\S]*?)'''/s) ||
content.match(/DEPRECATED_COMPONENT\s*=\s*'((?:[^'\\]|\\.)*)'/);
const deprecatedMatch = doubleQuoteMatch || singleQuoteMatch;
if (deprecatedMatch) {
labels.add('deprecated_component');
deprecatedInfo.push({
component: component,
message: deprecatedMatch[1]
});
console.log(`Found deprecated component: ${component}`);
}
} catch (error) {
// Only log if it's not a simple "file not found" error (404)
if (error.status !== 404) {
console.log(`Error reading ${initFile}:`, error.message);
}
}
}
return { labels, deprecatedInfo };
}
// Strategy: Requirements detection
async function detectRequirements(allLabels) {
const labels = new Set();
@@ -418,10 +489,26 @@ jobs:
}
// Generate review messages
function generateReviewMessages(finalLabels, originalLabelCount) {
function generateReviewMessages(finalLabels, originalLabelCount, deprecatedInfo) {
const messages = [];
const prAuthor = context.payload.pull_request.user.login;
// Deprecated component message
if (finalLabels.includes('deprecated_component') && deprecatedInfo && deprecatedInfo.length > 0) {
let message = `${DEPRECATED_COMPONENT_MARKER}\n### ⚠️ Deprecated Component\n\n`;
message += `Hey there @${prAuthor},\n`;
message += `This PR modifies one or more deprecated components. Please be aware:\n\n`;
for (const info of deprecatedInfo) {
message += `#### Component: \`${info.component}\`\n`;
message += `${info.message}\n\n`;
}
message += `Consider migrating to the recommended alternative if applicable.`;
messages.push(message);
}
// Too big message
if (finalLabels.includes('too-big')) {
const testAdditions = prFiles
@@ -468,10 +555,10 @@ jobs:
}
// Handle reviews
async function handleReviews(finalLabels, originalLabelCount) {
const reviewMessages = generateReviewMessages(finalLabels, originalLabelCount);
async function handleReviews(finalLabels, originalLabelCount, deprecatedInfo) {
const reviewMessages = generateReviewMessages(finalLabels, originalLabelCount, deprecatedInfo);
const hasReviewableLabels = finalLabels.some(label =>
['too-big', 'needs-codeowners'].includes(label)
['too-big', 'needs-codeowners', 'deprecated_component'].includes(label)
);
const { data: reviews } = await github.rest.pulls.listReviews({
@@ -580,7 +667,8 @@ jobs:
actionsLabels,
codeOwnerLabels,
testLabels,
checkboxLabels
checkboxLabels,
deprecatedResult
] = await Promise.all([
detectMergeBranch(),
detectComponentPlatforms(apiData),
@@ -592,9 +680,14 @@ jobs:
detectGitHubActionsChanges(),
detectCodeOwner(),
detectTests(),
detectPRTemplateCheckboxes()
detectPRTemplateCheckboxes(),
detectDeprecatedComponents()
]);
// Extract deprecated component info
const deprecatedLabels = deprecatedResult.labels;
const deprecatedInfo = deprecatedResult.deprecatedInfo;
// Combine all labels
const allLabels = new Set([
...branchLabels,
@@ -607,7 +700,8 @@ jobs:
...actionsLabels,
...codeOwnerLabels,
...testLabels,
...checkboxLabels
...checkboxLabels,
...deprecatedLabels
]);
// Detect requirements based on all other labels
@@ -638,7 +732,7 @@ jobs:
console.log('Computed labels:', finalLabels.join(', '));
// Handle reviews
await handleReviews(finalLabels, originalLabelCount);
await handleReviews(finalLabels, originalLabelCount, deprecatedInfo);
// Apply labels
if (finalLabels.length > 0) {

View File

@@ -482,6 +482,7 @@ esphome/components/switch/* @esphome/core
esphome/components/switch/binary_sensor/* @ssieb
esphome/components/sx126x/* @swoboda1337
esphome/components/sx127x/* @swoboda1337
esphome/components/sy6970/* @linkedupbits
esphome/components/syslog/* @clydebarrow
esphome/components/t6615/* @tylermenezes
esphome/components/tc74/* @sethgirvan

View File

@@ -12,6 +12,7 @@ from esphome.const import (
KEY_FRAMEWORK_VERSION,
)
from esphome.core import CORE
from esphome.cpp_generator import add_define
CODEOWNERS = ["@swoboda1337"]
@@ -42,6 +43,7 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
add_define("USE_ESP32_HOSTED")
if config[CONF_ACTIVE_HIGH]:
esp32.add_idf_sdkconfig_option(
"CONFIG_ESP_HOSTED_SDIO_RESET_ACTIVE_HIGH",

View File

@@ -40,7 +40,7 @@ void NfcTagBinarySensor::set_tag_name(const std::string &str) {
this->match_tag_name_ = true;
}
void NfcTagBinarySensor::set_uid(const NfcTagUid &uid) { this->uid_ = uid; }
void NfcTagBinarySensor::set_uid(const std::vector<uint8_t> &uid) { this->uid_ = uid; }
bool NfcTagBinarySensor::tag_match_ndef_string(const std::shared_ptr<NdefMessage> &msg) {
for (const auto &record : msg->get_records()) {
@@ -63,7 +63,7 @@ bool NfcTagBinarySensor::tag_match_tag_name(const std::shared_ptr<NdefMessage> &
return false;
}
bool NfcTagBinarySensor::tag_match_uid(const NfcTagUid &data) {
bool NfcTagBinarySensor::tag_match_uid(const std::vector<uint8_t> &data) {
if (data.size() != this->uid_.size()) {
return false;
}

View File

@@ -19,11 +19,11 @@ class NfcTagBinarySensor : public binary_sensor::BinarySensor,
void set_ndef_match_string(const std::string &str);
void set_tag_name(const std::string &str);
void set_uid(const NfcTagUid &uid);
void set_uid(const std::vector<uint8_t> &uid);
bool tag_match_ndef_string(const std::shared_ptr<NdefMessage> &msg);
bool tag_match_tag_name(const std::shared_ptr<NdefMessage> &msg);
bool tag_match_uid(const NfcTagUid &data);
bool tag_match_uid(const std::vector<uint8_t> &data);
void tag_off(NfcTag &tag) override;
void tag_on(NfcTag &tag) override;
@@ -31,7 +31,7 @@ class NfcTagBinarySensor : public binary_sensor::BinarySensor,
protected:
bool match_tag_name_{false};
std::string match_string_;
NfcTagUid uid_;
std::vector<uint8_t> uid_;
};
} // namespace nfc

View File

@@ -8,23 +8,19 @@ namespace nfc {
static const char *const TAG = "nfc";
char *format_uid_to(char *buffer, std::span<const uint8_t> uid) {
char *format_uid_to(char *buffer, const std::vector<uint8_t> &uid) {
return format_hex_pretty_to(buffer, FORMAT_UID_BUFFER_SIZE, uid.data(), uid.size(), '-');
}
char *format_bytes_to(char *buffer, std::span<const uint8_t> bytes) {
char *format_bytes_to(char *buffer, const std::vector<uint8_t> &bytes) {
return format_hex_pretty_to(buffer, FORMAT_BYTES_BUFFER_SIZE, bytes.data(), bytes.size(), ' ');
}
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
// Deprecated wrappers intentionally use heap-allocating version for backward compatibility
std::string format_uid(std::span<const uint8_t> uid) {
return format_hex_pretty(uid.data(), uid.size(), '-', false); // NOLINT
}
std::string format_bytes(std::span<const uint8_t> bytes) {
return format_hex_pretty(bytes.data(), bytes.size(), ' ', false); // NOLINT
}
std::string format_uid(const std::vector<uint8_t> &uid) { return format_hex_pretty(uid, '-', false); } // NOLINT
std::string format_bytes(const std::vector<uint8_t> &bytes) { return format_hex_pretty(bytes, ' ', false); } // NOLINT
#pragma GCC diagnostic pop
uint8_t guess_tag_type(uint8_t uid_length) {

View File

@@ -6,7 +6,6 @@
#include "ndef_record.h"
#include "nfc_tag.h"
#include <span>
#include <vector>
namespace esphome {
@@ -57,19 +56,19 @@ static const uint8_t MAD_KEY[6] = {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5};
/// Max UID size is 10 bytes, formatted as "XX-XX-XX-XX-XX-XX-XX-XX-XX-XX\0" = 30 chars
static constexpr size_t FORMAT_UID_BUFFER_SIZE = 30;
/// Format UID to buffer with '-' separator (e.g., "04-11-22-33"). Returns buffer for inline use.
char *format_uid_to(char *buffer, std::span<const uint8_t> uid);
char *format_uid_to(char *buffer, const std::vector<uint8_t> &uid);
/// Buffer size for format_bytes_to (64 bytes max = 192 chars with space separator)
static constexpr size_t FORMAT_BYTES_BUFFER_SIZE = 192;
/// Format bytes to buffer with ' ' separator (e.g., "04 11 22 33"). Returns buffer for inline use.
char *format_bytes_to(char *buffer, std::span<const uint8_t> bytes);
char *format_bytes_to(char *buffer, const std::vector<uint8_t> &bytes);
// Remove before 2026.6.0
ESPDEPRECATED("Use format_uid_to() with stack buffer instead. Removed in 2026.6.0", "2025.12.0")
std::string format_uid(std::span<const uint8_t> uid);
std::string format_uid(const std::vector<uint8_t> &uid);
// Remove before 2026.6.0
ESPDEPRECATED("Use format_bytes_to() with stack buffer instead. Removed in 2026.6.0", "2025.12.0")
std::string format_bytes(std::span<const uint8_t> bytes);
std::string format_bytes(const std::vector<uint8_t> &bytes);
uint8_t guess_tag_type(uint8_t uid_length);
uint8_t get_mifare_classic_ndef_start_index(std::vector<uint8_t> &data);

View File

@@ -1,6 +1,7 @@
#pragma once
#include <memory>
#include <vector>
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -9,27 +10,26 @@
namespace esphome {
namespace nfc {
// NFC UIDs are 4, 7, or 10 bytes depending on tag type
static constexpr size_t NFC_UID_MAX_LENGTH = 10;
using NfcTagUid = StaticVector<uint8_t, NFC_UID_MAX_LENGTH>;
class NfcTag {
public:
NfcTag() { this->tag_type_ = "Unknown"; };
NfcTag(NfcTagUid &uid) {
NfcTag() {
this->uid_ = {};
this->tag_type_ = "Unknown";
};
NfcTag(std::vector<uint8_t> &uid) {
this->uid_ = uid;
this->tag_type_ = "Unknown";
};
NfcTag(NfcTagUid &uid, const std::string &tag_type) {
NfcTag(std::vector<uint8_t> &uid, const std::string &tag_type) {
this->uid_ = uid;
this->tag_type_ = tag_type;
};
NfcTag(NfcTagUid &uid, const std::string &tag_type, std::unique_ptr<nfc::NdefMessage> ndef_message) {
NfcTag(std::vector<uint8_t> &uid, const std::string &tag_type, std::unique_ptr<nfc::NdefMessage> ndef_message) {
this->uid_ = uid;
this->tag_type_ = tag_type;
this->ndef_message_ = std::move(ndef_message);
};
NfcTag(NfcTagUid &uid, const std::string &tag_type, std::vector<uint8_t> &ndef_data) {
NfcTag(std::vector<uint8_t> &uid, const std::string &tag_type, std::vector<uint8_t> &ndef_data) {
this->uid_ = uid;
this->tag_type_ = tag_type;
this->ndef_message_ = make_unique<NdefMessage>(ndef_data);
@@ -41,14 +41,14 @@ class NfcTag {
ndef_message_ = make_unique<NdefMessage>(*rhs.ndef_message_);
}
NfcTagUid &get_uid() { return this->uid_; };
std::vector<uint8_t> &get_uid() { return this->uid_; };
const std::string &get_tag_type() { return this->tag_type_; };
bool has_ndef_message() { return this->ndef_message_ != nullptr; };
const std::shared_ptr<NdefMessage> &get_ndef_message() { return this->ndef_message_; };
void set_ndef_message(std::unique_ptr<NdefMessage> ndef_message) { this->ndef_message_ = std::move(ndef_message); };
protected:
NfcTagUid uid_;
std::vector<uint8_t> uid_;
std::string tag_type_;
std::shared_ptr<NdefMessage> ndef_message_;
};

View File

@@ -168,7 +168,7 @@ void PN532::loop() {
}
uint8_t nfcid_length = read[5];
nfc::NfcTagUid nfcid(read.begin() + 6, read.begin() + 6 + nfcid_length);
std::vector<uint8_t> nfcid(read.begin() + 6, read.begin() + 6 + nfcid_length);
if (read.size() < 6U + nfcid_length) {
// oops, pn532 returned invalid data
return;
@@ -358,7 +358,7 @@ void PN532::turn_off_rf_() {
});
}
std::unique_ptr<nfc::NfcTag> PN532::read_tag_(nfc::NfcTagUid &uid) {
std::unique_ptr<nfc::NfcTag> PN532::read_tag_(std::vector<uint8_t> &uid) {
uint8_t type = nfc::guess_tag_type(uid.size());
if (type == nfc::TAG_TYPE_MIFARE_CLASSIC) {
@@ -393,7 +393,7 @@ void PN532::write_mode(nfc::NdefMessage *message) {
ESP_LOGD(TAG, "Waiting to write next tag");
}
bool PN532::clean_tag_(nfc::NfcTagUid &uid) {
bool PN532::clean_tag_(std::vector<uint8_t> &uid) {
uint8_t type = nfc::guess_tag_type(uid.size());
if (type == nfc::TAG_TYPE_MIFARE_CLASSIC) {
return this->format_mifare_classic_mifare_(uid);
@@ -404,7 +404,7 @@ bool PN532::clean_tag_(nfc::NfcTagUid &uid) {
return false;
}
bool PN532::format_tag_(nfc::NfcTagUid &uid) {
bool PN532::format_tag_(std::vector<uint8_t> &uid) {
uint8_t type = nfc::guess_tag_type(uid.size());
if (type == nfc::TAG_TYPE_MIFARE_CLASSIC) {
return this->format_mifare_classic_ndef_(uid);
@@ -415,7 +415,7 @@ bool PN532::format_tag_(nfc::NfcTagUid &uid) {
return false;
}
bool PN532::write_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *message) {
bool PN532::write_tag_(std::vector<uint8_t> &uid, nfc::NdefMessage *message) {
uint8_t type = nfc::guess_tag_type(uid.size());
if (type == nfc::TAG_TYPE_MIFARE_CLASSIC) {
return this->write_mifare_classic_tag_(uid, message);
@@ -448,7 +448,7 @@ void PN532::dump_config() {
}
}
bool PN532BinarySensor::process(nfc::NfcTagUid &data) {
bool PN532BinarySensor::process(std::vector<uint8_t> &data) {
if (data.size() != this->uid_.size())
return false;

View File

@@ -69,28 +69,28 @@ class PN532 : public PollingComponent {
virtual bool read_data(std::vector<uint8_t> &data, uint8_t len) = 0;
virtual bool read_response(uint8_t command, std::vector<uint8_t> &data) = 0;
std::unique_ptr<nfc::NfcTag> read_tag_(nfc::NfcTagUid &uid);
std::unique_ptr<nfc::NfcTag> read_tag_(std::vector<uint8_t> &uid);
bool format_tag_(nfc::NfcTagUid &uid);
bool clean_tag_(nfc::NfcTagUid &uid);
bool write_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *message);
bool format_tag_(std::vector<uint8_t> &uid);
bool clean_tag_(std::vector<uint8_t> &uid);
bool write_tag_(std::vector<uint8_t> &uid, nfc::NdefMessage *message);
std::unique_ptr<nfc::NfcTag> read_mifare_classic_tag_(nfc::NfcTagUid &uid);
std::unique_ptr<nfc::NfcTag> read_mifare_classic_tag_(std::vector<uint8_t> &uid);
bool read_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &data);
bool write_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &data);
bool auth_mifare_classic_block_(nfc::NfcTagUid &uid, uint8_t block_num, uint8_t key_num, const uint8_t *key);
bool format_mifare_classic_mifare_(nfc::NfcTagUid &uid);
bool format_mifare_classic_ndef_(nfc::NfcTagUid &uid);
bool write_mifare_classic_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *message);
bool auth_mifare_classic_block_(std::vector<uint8_t> &uid, uint8_t block_num, uint8_t key_num, const uint8_t *key);
bool format_mifare_classic_mifare_(std::vector<uint8_t> &uid);
bool format_mifare_classic_ndef_(std::vector<uint8_t> &uid);
bool write_mifare_classic_tag_(std::vector<uint8_t> &uid, nfc::NdefMessage *message);
std::unique_ptr<nfc::NfcTag> read_mifare_ultralight_tag_(nfc::NfcTagUid &uid);
std::unique_ptr<nfc::NfcTag> read_mifare_ultralight_tag_(std::vector<uint8_t> &uid);
bool read_mifare_ultralight_bytes_(uint8_t start_page, uint16_t num_bytes, std::vector<uint8_t> &data);
bool is_mifare_ultralight_formatted_(const std::vector<uint8_t> &page_3_to_6);
uint16_t read_mifare_ultralight_capacity_();
bool find_mifare_ultralight_ndef_(const std::vector<uint8_t> &page_3_to_6, uint8_t &message_length,
uint8_t &message_start_index);
bool write_mifare_ultralight_page_(uint8_t page_num, std::vector<uint8_t> &write_data);
bool write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *message);
bool write_mifare_ultralight_tag_(std::vector<uint8_t> &uid, nfc::NdefMessage *message);
bool clean_mifare_ultralight_();
bool updates_enabled_{true};
@@ -98,7 +98,7 @@ class PN532 : public PollingComponent {
std::vector<PN532BinarySensor *> binary_sensors_;
std::vector<nfc::NfcOnTagTrigger *> triggers_ontag_;
std::vector<nfc::NfcOnTagTrigger *> triggers_ontagremoved_;
nfc::NfcTagUid current_uid_;
std::vector<uint8_t> current_uid_;
nfc::NdefMessage *next_task_message_to_write_;
uint32_t rd_start_time_{0};
enum PN532ReadReady rd_ready_ { WOULDBLOCK };
@@ -118,9 +118,9 @@ class PN532 : public PollingComponent {
class PN532BinarySensor : public binary_sensor::BinarySensor {
public:
void set_uid(const nfc::NfcTagUid &uid) { uid_ = uid; }
void set_uid(const std::vector<uint8_t> &uid) { uid_ = uid; }
bool process(nfc::NfcTagUid &data);
bool process(std::vector<uint8_t> &data);
void on_scan_end() {
if (!this->found_) {
@@ -130,7 +130,7 @@ class PN532BinarySensor : public binary_sensor::BinarySensor {
}
protected:
nfc::NfcTagUid uid_;
std::vector<uint8_t> uid_;
bool found_{false};
};

View File

@@ -8,7 +8,7 @@ namespace pn532 {
static const char *const TAG = "pn532.mifare_classic";
std::unique_ptr<nfc::NfcTag> PN532::read_mifare_classic_tag_(nfc::NfcTagUid &uid) {
std::unique_ptr<nfc::NfcTag> PN532::read_mifare_classic_tag_(std::vector<uint8_t> &uid) {
uint8_t current_block = 4;
uint8_t message_start_index = 0;
uint32_t message_length = 0;
@@ -82,7 +82,8 @@ bool PN532::read_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &
return true;
}
bool PN532::auth_mifare_classic_block_(nfc::NfcTagUid &uid, uint8_t block_num, uint8_t key_num, const uint8_t *key) {
bool PN532::auth_mifare_classic_block_(std::vector<uint8_t> &uid, uint8_t block_num, uint8_t key_num,
const uint8_t *key) {
std::vector<uint8_t> data({
PN532_COMMAND_INDATAEXCHANGE,
0x01, // One card
@@ -105,7 +106,7 @@ bool PN532::auth_mifare_classic_block_(nfc::NfcTagUid &uid, uint8_t block_num, u
return true;
}
bool PN532::format_mifare_classic_mifare_(nfc::NfcTagUid &uid) {
bool PN532::format_mifare_classic_mifare_(std::vector<uint8_t> &uid) {
std::vector<uint8_t> blank_buffer(
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00});
std::vector<uint8_t> trailer_buffer(
@@ -140,7 +141,7 @@ bool PN532::format_mifare_classic_mifare_(nfc::NfcTagUid &uid) {
return !error;
}
bool PN532::format_mifare_classic_ndef_(nfc::NfcTagUid &uid) {
bool PN532::format_mifare_classic_ndef_(std::vector<uint8_t> &uid) {
std::vector<uint8_t> empty_ndef_message(
{0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00});
std::vector<uint8_t> blank_block(
@@ -215,7 +216,7 @@ bool PN532::write_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t>
return true;
}
bool PN532::write_mifare_classic_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *message) {
bool PN532::write_mifare_classic_tag_(std::vector<uint8_t> &uid, nfc::NdefMessage *message) {
auto encoded = message->encode();
uint32_t message_length = encoded.size();

View File

@@ -8,7 +8,7 @@ namespace pn532 {
static const char *const TAG = "pn532.mifare_ultralight";
std::unique_ptr<nfc::NfcTag> PN532::read_mifare_ultralight_tag_(nfc::NfcTagUid &uid) {
std::unique_ptr<nfc::NfcTag> PN532::read_mifare_ultralight_tag_(std::vector<uint8_t> &uid) {
std::vector<uint8_t> data;
// pages 3 to 6 contain various info we are interested in -- do one read to grab it all
if (!this->read_mifare_ultralight_bytes_(3, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE * nfc::MIFARE_ULTRALIGHT_READ_SIZE,
@@ -114,7 +114,7 @@ bool PN532::find_mifare_ultralight_ndef_(const std::vector<uint8_t> &page_3_to_6
return false;
}
bool PN532::write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *message) {
bool PN532::write_mifare_ultralight_tag_(std::vector<uint8_t> &uid, nfc::NdefMessage *message) {
uint32_t capacity = this->read_mifare_ultralight_capacity_();
auto encoded = message->encode();

View File

@@ -478,7 +478,7 @@ uint8_t PN7150::read_endpoint_data_(nfc::NfcTag &tag) {
return nfc::STATUS_FAILED;
}
uint8_t PN7150::clean_endpoint_(nfc::NfcTagUid &uid) {
uint8_t PN7150::clean_endpoint_(std::vector<uint8_t> &uid) {
uint8_t type = nfc::guess_tag_type(uid.size());
switch (type) {
case nfc::TAG_TYPE_MIFARE_CLASSIC:
@@ -494,7 +494,7 @@ uint8_t PN7150::clean_endpoint_(nfc::NfcTagUid &uid) {
return nfc::STATUS_FAILED;
}
uint8_t PN7150::format_endpoint_(nfc::NfcTagUid &uid) {
uint8_t PN7150::format_endpoint_(std::vector<uint8_t> &uid) {
uint8_t type = nfc::guess_tag_type(uid.size());
switch (type) {
case nfc::TAG_TYPE_MIFARE_CLASSIC:
@@ -510,7 +510,7 @@ uint8_t PN7150::format_endpoint_(nfc::NfcTagUid &uid) {
return nfc::STATUS_FAILED;
}
uint8_t PN7150::write_endpoint_(nfc::NfcTagUid &uid, std::shared_ptr<nfc::NdefMessage> &message) {
uint8_t PN7150::write_endpoint_(std::vector<uint8_t> &uid, std::shared_ptr<nfc::NdefMessage> &message) {
uint8_t type = nfc::guess_tag_type(uid.size());
switch (type) {
case nfc::TAG_TYPE_MIFARE_CLASSIC:
@@ -534,7 +534,7 @@ std::unique_ptr<nfc::NfcTag> PN7150::build_tag_(const uint8_t mode_tech, const s
ESP_LOGE(TAG, "UID length cannot be zero");
return nullptr;
}
nfc::NfcTagUid uid(data.begin() + 3, data.begin() + 3 + uid_length);
std::vector<uint8_t> uid(data.begin() + 3, data.begin() + 3 + uid_length);
const auto *tag_type_str =
nfc::guess_tag_type(uid_length) == nfc::TAG_TYPE_MIFARE_CLASSIC ? nfc::MIFARE_CLASSIC : nfc::NFC_FORUM_TYPE_2;
return make_unique<nfc::NfcTag>(uid, tag_type_str);
@@ -543,7 +543,7 @@ std::unique_ptr<nfc::NfcTag> PN7150::build_tag_(const uint8_t mode_tech, const s
return nullptr;
}
optional<size_t> PN7150::find_tag_uid_(const nfc::NfcTagUid &uid) {
optional<size_t> PN7150::find_tag_uid_(const std::vector<uint8_t> &uid) {
if (!this->discovered_endpoint_.empty()) {
for (size_t i = 0; i < this->discovered_endpoint_.size(); i++) {
auto existing_tag_uid = this->discovered_endpoint_[i].tag->get_uid();

View File

@@ -203,12 +203,12 @@ class PN7150 : public nfc::Nfcc, public Component {
void select_endpoint_();
uint8_t read_endpoint_data_(nfc::NfcTag &tag);
uint8_t clean_endpoint_(nfc::NfcTagUid &uid);
uint8_t format_endpoint_(nfc::NfcTagUid &uid);
uint8_t write_endpoint_(nfc::NfcTagUid &uid, std::shared_ptr<nfc::NdefMessage> &message);
uint8_t clean_endpoint_(std::vector<uint8_t> &uid);
uint8_t format_endpoint_(std::vector<uint8_t> &uid);
uint8_t write_endpoint_(std::vector<uint8_t> &uid, std::shared_ptr<nfc::NdefMessage> &message);
std::unique_ptr<nfc::NfcTag> build_tag_(uint8_t mode_tech, const std::vector<uint8_t> &data);
optional<size_t> find_tag_uid_(const nfc::NfcTagUid &uid);
optional<size_t> find_tag_uid_(const std::vector<uint8_t> &uid);
void purge_old_tags_();
void erase_tag_(uint8_t tag_index);
@@ -251,7 +251,7 @@ class PN7150 : public nfc::Nfcc, public Component {
uint8_t find_mifare_ultralight_ndef_(const std::vector<uint8_t> &page_3_to_6, uint8_t &message_length,
uint8_t &message_start_index);
uint8_t write_mifare_ultralight_page_(uint8_t page_num, std::vector<uint8_t> &write_data);
uint8_t write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::shared_ptr<nfc::NdefMessage> &message);
uint8_t write_mifare_ultralight_tag_(std::vector<uint8_t> &uid, const std::shared_ptr<nfc::NdefMessage> &message);
uint8_t clean_mifare_ultralight_();
enum NfcTask : uint8_t {

View File

@@ -115,7 +115,8 @@ uint8_t PN7150::find_mifare_ultralight_ndef_(const std::vector<uint8_t> &page_3_
return nfc::STATUS_FAILED;
}
uint8_t PN7150::write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::shared_ptr<nfc::NdefMessage> &message) {
uint8_t PN7150::write_mifare_ultralight_tag_(std::vector<uint8_t> &uid,
const std::shared_ptr<nfc::NdefMessage> &message) {
uint32_t capacity = this->read_mifare_ultralight_capacity_();
auto encoded = message->encode();

View File

@@ -506,7 +506,7 @@ uint8_t PN7160::read_endpoint_data_(nfc::NfcTag &tag) {
return nfc::STATUS_FAILED;
}
uint8_t PN7160::clean_endpoint_(nfc::NfcTagUid &uid) {
uint8_t PN7160::clean_endpoint_(std::vector<uint8_t> &uid) {
uint8_t type = nfc::guess_tag_type(uid.size());
switch (type) {
case nfc::TAG_TYPE_MIFARE_CLASSIC:
@@ -522,7 +522,7 @@ uint8_t PN7160::clean_endpoint_(nfc::NfcTagUid &uid) {
return nfc::STATUS_FAILED;
}
uint8_t PN7160::format_endpoint_(nfc::NfcTagUid &uid) {
uint8_t PN7160::format_endpoint_(std::vector<uint8_t> &uid) {
uint8_t type = nfc::guess_tag_type(uid.size());
switch (type) {
case nfc::TAG_TYPE_MIFARE_CLASSIC:
@@ -538,7 +538,7 @@ uint8_t PN7160::format_endpoint_(nfc::NfcTagUid &uid) {
return nfc::STATUS_FAILED;
}
uint8_t PN7160::write_endpoint_(nfc::NfcTagUid &uid, std::shared_ptr<nfc::NdefMessage> &message) {
uint8_t PN7160::write_endpoint_(std::vector<uint8_t> &uid, std::shared_ptr<nfc::NdefMessage> &message) {
uint8_t type = nfc::guess_tag_type(uid.size());
switch (type) {
case nfc::TAG_TYPE_MIFARE_CLASSIC:
@@ -562,7 +562,7 @@ std::unique_ptr<nfc::NfcTag> PN7160::build_tag_(const uint8_t mode_tech, const s
ESP_LOGE(TAG, "UID length cannot be zero");
return nullptr;
}
nfc::NfcTagUid uid(data.begin() + 3, data.begin() + 3 + uid_length);
std::vector<uint8_t> uid(data.begin() + 3, data.begin() + 3 + uid_length);
const auto *tag_type_str =
nfc::guess_tag_type(uid_length) == nfc::TAG_TYPE_MIFARE_CLASSIC ? nfc::MIFARE_CLASSIC : nfc::NFC_FORUM_TYPE_2;
return make_unique<nfc::NfcTag>(uid, tag_type_str);
@@ -571,7 +571,7 @@ std::unique_ptr<nfc::NfcTag> PN7160::build_tag_(const uint8_t mode_tech, const s
return nullptr;
}
optional<size_t> PN7160::find_tag_uid_(const nfc::NfcTagUid &uid) {
optional<size_t> PN7160::find_tag_uid_(const std::vector<uint8_t> &uid) {
if (!this->discovered_endpoint_.empty()) {
for (size_t i = 0; i < this->discovered_endpoint_.size(); i++) {
auto existing_tag_uid = this->discovered_endpoint_[i].tag->get_uid();

View File

@@ -220,12 +220,12 @@ class PN7160 : public nfc::Nfcc, public Component {
void select_endpoint_();
uint8_t read_endpoint_data_(nfc::NfcTag &tag);
uint8_t clean_endpoint_(nfc::NfcTagUid &uid);
uint8_t format_endpoint_(nfc::NfcTagUid &uid);
uint8_t write_endpoint_(nfc::NfcTagUid &uid, std::shared_ptr<nfc::NdefMessage> &message);
uint8_t clean_endpoint_(std::vector<uint8_t> &uid);
uint8_t format_endpoint_(std::vector<uint8_t> &uid);
uint8_t write_endpoint_(std::vector<uint8_t> &uid, std::shared_ptr<nfc::NdefMessage> &message);
std::unique_ptr<nfc::NfcTag> build_tag_(uint8_t mode_tech, const std::vector<uint8_t> &data);
optional<size_t> find_tag_uid_(const nfc::NfcTagUid &uid);
optional<size_t> find_tag_uid_(const std::vector<uint8_t> &uid);
void purge_old_tags_();
void erase_tag_(uint8_t tag_index);
@@ -268,7 +268,7 @@ class PN7160 : public nfc::Nfcc, public Component {
uint8_t find_mifare_ultralight_ndef_(const std::vector<uint8_t> &page_3_to_6, uint8_t &message_length,
uint8_t &message_start_index);
uint8_t write_mifare_ultralight_page_(uint8_t page_num, std::vector<uint8_t> &write_data);
uint8_t write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::shared_ptr<nfc::NdefMessage> &message);
uint8_t write_mifare_ultralight_tag_(std::vector<uint8_t> &uid, const std::shared_ptr<nfc::NdefMessage> &message);
uint8_t clean_mifare_ultralight_();
enum NfcTask : uint8_t {

View File

@@ -115,7 +115,8 @@ uint8_t PN7160::find_mifare_ultralight_ndef_(const std::vector<uint8_t> &page_3_
return nfc::STATUS_FAILED;
}
uint8_t PN7160::write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::shared_ptr<nfc::NdefMessage> &message) {
uint8_t PN7160::write_mifare_ultralight_tag_(std::vector<uint8_t> &uid,
const std::shared_ptr<nfc::NdefMessage> &message) {
uint32_t capacity = this->read_mifare_ultralight_capacity_();
auto encoded = message->encode();

View File

@@ -30,6 +30,19 @@ static const int8_t SEN5X_INDEX_SCALE_FACTOR = 10; //
static const int8_t SEN5X_MIN_INDEX_VALUE = 1 * SEN5X_INDEX_SCALE_FACTOR; // must be adjusted by the scale factor
static const int16_t SEN5X_MAX_INDEX_VALUE = 500 * SEN5X_INDEX_SCALE_FACTOR; // must be adjusted by the scale factor
static const LogString *type_to_string(Sen5xType type) {
switch (type) {
case Sen5xType::SEN50:
return LOG_STR("SEN50");
case Sen5xType::SEN54:
return LOG_STR("SEN54");
case Sen5xType::SEN55:
return LOG_STR("SEN55");
default:
return LOG_STR("UNKNOWN");
}
}
static const LogString *rht_accel_mode_to_string(RhtAccelerationMode mode) {
switch (mode) {
case LOW_ACCELERATION:
@@ -43,6 +56,15 @@ static const LogString *rht_accel_mode_to_string(RhtAccelerationMode mode) {
}
}
// This function performs an in-place conversion of the provided buffer
// from uint16_t values to big endianness
static inline const char *sensirion_convert_to_string_in_place(uint16_t *array, size_t length) {
for (size_t i = 0; i < length; i++) {
array[i] = convert_big_endian(array[i]);
}
return reinterpret_cast<const char *>(array);
}
void SEN5XComponent::setup() {
// the sensor needs 1000 ms to enter the idle state
this->set_timeout(1000, [this]() {
@@ -75,18 +97,18 @@ void SEN5XComponent::setup() {
stop_measurement_delay = 200;
}
this->set_timeout(stop_measurement_delay, [this]() {
uint16_t raw_serial_number[3];
if (!this->get_register(SEN5X_CMD_GET_SERIAL_NUMBER, raw_serial_number, 3, 20)) {
// note: serial number register is actually 32-bytes long but we grab only the first 16-bytes,
// this appears to be all that Sensirion uses for serial numbers, this could change
uint16_t raw_serial_number[8];
if (!this->get_register(SEN5X_CMD_GET_SERIAL_NUMBER, raw_serial_number, 8, 20)) {
ESP_LOGE(TAG, "Failed to read serial number");
this->error_code_ = SERIAL_NUMBER_IDENTIFICATION_FAILED;
this->mark_failed();
return;
}
this->serial_number_[0] = static_cast<bool>(uint16_t(raw_serial_number[0]) & 0xFF);
this->serial_number_[1] = static_cast<uint16_t>(raw_serial_number[0] & 0xFF);
this->serial_number_[2] = static_cast<uint16_t>(raw_serial_number[1] >> 8);
ESP_LOGV(TAG, "Serial number %02d.%02d.%02d", this->serial_number_[0], this->serial_number_[1],
this->serial_number_[2]);
const char *serial_number = sensirion_convert_to_string_in_place(raw_serial_number, 8);
snprintf(this->serial_number_, sizeof(this->serial_number_), "%s", serial_number);
ESP_LOGV(TAG, "Serial number %s", this->serial_number_);
uint16_t raw_product_name[16];
if (!this->get_register(SEN5X_CMD_GET_PRODUCT_NAME, raw_product_name, 16, 20)) {
@@ -95,50 +117,35 @@ void SEN5XComponent::setup() {
this->mark_failed();
return;
}
// 2 ASCII bytes are encoded in an int
const uint16_t *current_int = raw_product_name;
char current_char;
uint8_t max = 16;
do {
// first char
current_char = *current_int >> 8;
if (current_char) {
this->product_name_.push_back(current_char);
// second char
current_char = *current_int & 0xFF;
if (current_char) {
this->product_name_.push_back(current_char);
}
}
current_int++;
} while (current_char && --max);
Sen5xType sen5x_type = UNKNOWN;
if (this->product_name_ == "SEN50") {
sen5x_type = SEN50;
const char *product_name = sensirion_convert_to_string_in_place(raw_product_name, 16);
if (strncmp(product_name, "SEN50", 5) == 0) {
this->type_ = Sen5xType::SEN50;
} else if (strncmp(product_name, "SEN54", 5) == 0) {
this->type_ = Sen5xType::SEN54;
} else if (strncmp(product_name, "SEN55", 5) == 0) {
this->type_ = Sen5xType::SEN55;
} else {
if (this->product_name_ == "SEN54") {
sen5x_type = SEN54;
} else {
if (this->product_name_ == "SEN55") {
sen5x_type = SEN55;
}
}
this->type_ = Sen5xType::UNKNOWN;
ESP_LOGE(TAG, "Unknown product name: %.32s", product_name);
this->error_code_ = PRODUCT_NAME_FAILED;
this->mark_failed();
return;
}
ESP_LOGD(TAG, "Product name: %s", this->product_name_.c_str());
if (this->humidity_sensor_ && sen5x_type == SEN50) {
ESP_LOGD(TAG, "Type: %s", LOG_STR_ARG(type_to_string(this->type_)));
if (this->humidity_sensor_ && this->type_ == Sen5xType::SEN50) {
ESP_LOGE(TAG, "Relative humidity requires a SEN54 or SEN55");
this->humidity_sensor_ = nullptr; // mark as not used
}
if (this->temperature_sensor_ && sen5x_type == SEN50) {
if (this->temperature_sensor_ && this->type_ == Sen5xType::SEN50) {
ESP_LOGE(TAG, "Temperature requires a SEN54 or SEN55");
this->temperature_sensor_ = nullptr; // mark as not used
}
if (this->voc_sensor_ && sen5x_type == SEN50) {
if (this->voc_sensor_ && this->type_ == Sen5xType::SEN50) {
ESP_LOGE(TAG, "VOC requires a SEN54 or SEN55");
this->voc_sensor_ = nullptr; // mark as not used
}
if (this->nox_sensor_ && sen5x_type != SEN55) {
if (this->nox_sensor_ && this->type_ != Sen5xType::SEN55) {
ESP_LOGE(TAG, "NOx requires a SEN55");
this->nox_sensor_ = nullptr; // mark as not used
}
@@ -153,12 +160,8 @@ void SEN5XComponent::setup() {
ESP_LOGV(TAG, "Firmware version %d", this->firmware_version_);
if (this->voc_sensor_ && this->store_baseline_) {
uint32_t combined_serial =
encode_uint24(this->serial_number_[0], this->serial_number_[1], this->serial_number_[2]);
// Hash with config hash, version, and serial number
// This ensures the baseline storage is cleared after OTA
// Serial numbers are unique to each sensor, so multiple sensors can be used without conflict
uint32_t hash = fnv1a_hash_extend(App.get_config_version_hash(), combined_serial);
// Hash with serial number, serial numbers are unique, so multiple sensors can be used without conflict
uint32_t hash = fnv1a_hash(this->serial_number_);
this->pref_ = global_preferences->make_preference<uint16_t[4]>(hash, true);
this->voc_baseline_time_ = App.get_loop_component_start_time();
if (this->pref_.load(&this->voc_baseline_state_)) {
@@ -262,11 +265,10 @@ void SEN5XComponent::dump_config() {
}
}
ESP_LOGCONFIG(TAG,
" Product name: %s\n"
" Type: %s\n"
" Firmware version: %d\n"
" Serial number %02d.%02d.%02d",
this->product_name_.c_str(), this->firmware_version_, this->serial_number_[0], this->serial_number_[1],
this->serial_number_[2]);
" Serial number: %s",
LOG_STR_ARG(type_to_string(this->type_)), this->firmware_version_, this->serial_number_);
if (this->auto_cleaning_interval_.has_value()) {
ESP_LOGCONFIG(TAG, " Auto cleaning interval: %" PRId32 "s", this->auto_cleaning_interval_.value());
}

View File

@@ -24,6 +24,8 @@ enum RhtAccelerationMode : uint16_t {
HIGH_ACCELERATION = 2,
};
enum class Sen5xType : uint8_t { SEN50, SEN54, SEN55, UNKNOWN };
struct GasTuning {
uint16_t index_offset;
uint16_t learning_time_offset_hours;
@@ -49,8 +51,6 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri
void dump_config() override;
void update() override;
enum Sen5xType { SEN50, SEN54, SEN55, UNKNOWN };
void set_pm_1_0_sensor(sensor::Sensor *pm_1_0) { this->pm_1_0_sensor_ = pm_1_0; }
void set_pm_2_5_sensor(sensor::Sensor *pm_2_5) { this->pm_2_5_sensor_ = pm_2_5; }
void set_pm_4_0_sensor(sensor::Sensor *pm_4_0) { this->pm_4_0_sensor_ = pm_4_0; }
@@ -102,11 +102,12 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri
bool write_tuning_parameters_(uint16_t i2c_command, const GasTuning &tuning);
bool write_temperature_compensation_(const TemperatureCompensation &compensation);
char serial_number_[17] = "UNKNOWN";
uint16_t voc_baseline_state_[4]{0};
uint32_t voc_baseline_time_;
uint16_t firmware_version_;
Sen5xType type_{Sen5xType::UNKNOWN};
ERRORCODE error_code_;
uint8_t serial_number_[4];
bool initialized_{false};
bool store_baseline_;
@@ -127,7 +128,6 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri
optional<GasTuning> nox_tuning_params_;
optional<TemperatureCompensation> temperature_compensation_;
ESPPreferenceObject pref_;
std::string product_name_;
};
} // namespace sen5x

View File

@@ -0,0 +1,63 @@
import esphome.codegen as cg
from esphome.components import i2c
import esphome.config_validation as cv
from esphome.const import CONF_ID
CODEOWNERS = ["@linkedupbits"]
DEPENDENCIES = ["i2c"]
MULTI_CONF = True
CONF_SY6970_ID = "sy6970_id"
CONF_ENABLE_STATUS_LED = "enable_status_led"
CONF_INPUT_CURRENT_LIMIT = "input_current_limit"
CONF_CHARGE_VOLTAGE = "charge_voltage"
CONF_CHARGE_CURRENT = "charge_current"
CONF_PRECHARGE_CURRENT = "precharge_current"
CONF_CHARGE_ENABLED = "charge_enabled"
CONF_ENABLE_ADC = "enable_adc"
sy6970_ns = cg.esphome_ns.namespace("sy6970")
SY6970Component = sy6970_ns.class_(
"SY6970Component", cg.PollingComponent, i2c.I2CDevice
)
SY6970Listener = sy6970_ns.class_("SY6970Listener")
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(SY6970Component),
cv.Optional(CONF_ENABLE_STATUS_LED, default=True): cv.boolean,
cv.Optional(CONF_INPUT_CURRENT_LIMIT, default=500): cv.int_range(
min=100, max=3200
),
cv.Optional(CONF_CHARGE_VOLTAGE, default=4208): cv.int_range(
min=3840, max=4608
),
cv.Optional(CONF_CHARGE_CURRENT, default=2048): cv.int_range(
min=0, max=5056
),
cv.Optional(CONF_PRECHARGE_CURRENT, default=128): cv.int_range(
min=64, max=1024
),
cv.Optional(CONF_CHARGE_ENABLED, default=True): cv.boolean,
cv.Optional(CONF_ENABLE_ADC, default=True): cv.boolean,
}
)
.extend(cv.polling_component_schema("5s"))
.extend(i2c.i2c_device_schema(0x6A))
)
async def to_code(config):
var = cg.new_Pvariable(
config[CONF_ID],
config[CONF_ENABLE_STATUS_LED],
config[CONF_INPUT_CURRENT_LIMIT],
config[CONF_CHARGE_VOLTAGE],
config[CONF_CHARGE_CURRENT],
config[CONF_PRECHARGE_CURRENT],
config[CONF_CHARGE_ENABLED],
config[CONF_ENABLE_ADC],
)
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)

View File

@@ -0,0 +1,56 @@
import esphome.codegen as cg
from esphome.components import binary_sensor
import esphome.config_validation as cv
from esphome.const import DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_POWER
from .. import CONF_SY6970_ID, SY6970Component, sy6970_ns
DEPENDENCIES = ["sy6970"]
CONF_VBUS_CONNECTED = "vbus_connected"
CONF_CHARGING = "charging"
CONF_CHARGE_DONE = "charge_done"
SY6970VbusConnectedBinarySensor = sy6970_ns.class_(
"SY6970VbusConnectedBinarySensor", binary_sensor.BinarySensor
)
SY6970ChargingBinarySensor = sy6970_ns.class_(
"SY6970ChargingBinarySensor", binary_sensor.BinarySensor
)
SY6970ChargeDoneBinarySensor = sy6970_ns.class_(
"SY6970ChargeDoneBinarySensor", binary_sensor.BinarySensor
)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_SY6970_ID): cv.use_id(SY6970Component),
cv.Optional(CONF_VBUS_CONNECTED): binary_sensor.binary_sensor_schema(
SY6970VbusConnectedBinarySensor,
device_class=DEVICE_CLASS_CONNECTIVITY,
),
cv.Optional(CONF_CHARGING): binary_sensor.binary_sensor_schema(
SY6970ChargingBinarySensor,
device_class=DEVICE_CLASS_POWER,
),
cv.Optional(CONF_CHARGE_DONE): binary_sensor.binary_sensor_schema(
SY6970ChargeDoneBinarySensor,
device_class=DEVICE_CLASS_POWER,
),
}
)
async def to_code(config):
parent = await cg.get_variable(config[CONF_SY6970_ID])
if vbus_connected_config := config.get(CONF_VBUS_CONNECTED):
sens = await binary_sensor.new_binary_sensor(vbus_connected_config)
cg.add(parent.add_listener(sens))
if charging_config := config.get(CONF_CHARGING):
sens = await binary_sensor.new_binary_sensor(charging_config)
cg.add(parent.add_listener(sens))
if charge_done_config := config.get(CONF_CHARGE_DONE):
sens = await binary_sensor.new_binary_sensor(charge_done_config)
cg.add(parent.add_listener(sens))

View File

@@ -0,0 +1,43 @@
#pragma once
#include "../sy6970.h"
#include "esphome/components/binary_sensor/binary_sensor.h"
namespace esphome::sy6970 {
template<uint8_t REG, uint8_t SHIFT, uint8_t MASK, uint8_t TRUE_VALUE>
class StatusBinarySensor : public SY6970Listener, public binary_sensor::BinarySensor {
public:
void on_data(const SY6970Data &data) override {
uint8_t value = (data.registers[REG] >> SHIFT) & MASK;
this->publish_state(value == TRUE_VALUE);
}
};
template<uint8_t REG, uint8_t SHIFT, uint8_t MASK, uint8_t FALSE_VALUE>
class InverseStatusBinarySensor : public SY6970Listener, public binary_sensor::BinarySensor {
public:
void on_data(const SY6970Data &data) override {
uint8_t value = (data.registers[REG] >> SHIFT) & MASK;
this->publish_state(value != FALSE_VALUE);
}
};
// Custom binary sensor for charging (true when pre-charge or fast charge)
class SY6970ChargingBinarySensor : public SY6970Listener, public binary_sensor::BinarySensor {
public:
void on_data(const SY6970Data &data) override {
uint8_t chrg_stat = (data.registers[SY6970_REG_STATUS] >> 3) & 0x03;
bool charging = chrg_stat != CHARGE_STATUS_NOT_CHARGING && chrg_stat != CHARGE_STATUS_CHARGE_DONE;
this->publish_state(charging);
}
};
// Specialized sensor types using templates
// VBUS connected: BUS_STATUS != NO_INPUT
using SY6970VbusConnectedBinarySensor = InverseStatusBinarySensor<SY6970_REG_STATUS, 5, 0x07, BUS_STATUS_NO_INPUT>;
// Charge done: CHARGE_STATUS == CHARGE_DONE
using SY6970ChargeDoneBinarySensor = StatusBinarySensor<SY6970_REG_STATUS, 3, 0x03, CHARGE_STATUS_CHARGE_DONE>;
} // namespace esphome::sy6970

View File

@@ -0,0 +1,95 @@
import esphome.codegen as cg
from esphome.components import sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_BATTERY_VOLTAGE,
DEVICE_CLASS_CURRENT,
DEVICE_CLASS_VOLTAGE,
STATE_CLASS_MEASUREMENT,
UNIT_MILLIAMP,
UNIT_VOLT,
)
from .. import CONF_SY6970_ID, SY6970Component, sy6970_ns
DEPENDENCIES = ["sy6970"]
CONF_VBUS_VOLTAGE = "vbus_voltage"
CONF_SYSTEM_VOLTAGE = "system_voltage"
CONF_CHARGE_CURRENT = "charge_current"
CONF_PRECHARGE_CURRENT = "precharge_current"
SY6970VbusVoltageSensor = sy6970_ns.class_("SY6970VbusVoltageSensor", sensor.Sensor)
SY6970BatteryVoltageSensor = sy6970_ns.class_(
"SY6970BatteryVoltageSensor", sensor.Sensor
)
SY6970SystemVoltageSensor = sy6970_ns.class_("SY6970SystemVoltageSensor", sensor.Sensor)
SY6970ChargeCurrentSensor = sy6970_ns.class_("SY6970ChargeCurrentSensor", sensor.Sensor)
SY6970PrechargeCurrentSensor = sy6970_ns.class_(
"SY6970PrechargeCurrentSensor", sensor.Sensor
)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_SY6970_ID): cv.use_id(SY6970Component),
cv.Optional(CONF_VBUS_VOLTAGE): sensor.sensor_schema(
SY6970VbusVoltageSensor,
unit_of_measurement=UNIT_VOLT,
accuracy_decimals=2,
device_class=DEVICE_CLASS_VOLTAGE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_BATTERY_VOLTAGE): sensor.sensor_schema(
SY6970BatteryVoltageSensor,
unit_of_measurement=UNIT_VOLT,
accuracy_decimals=2,
device_class=DEVICE_CLASS_VOLTAGE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_SYSTEM_VOLTAGE): sensor.sensor_schema(
SY6970SystemVoltageSensor,
unit_of_measurement=UNIT_VOLT,
accuracy_decimals=2,
device_class=DEVICE_CLASS_VOLTAGE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_CHARGE_CURRENT): sensor.sensor_schema(
SY6970ChargeCurrentSensor,
unit_of_measurement=UNIT_MILLIAMP,
accuracy_decimals=0,
device_class=DEVICE_CLASS_CURRENT,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PRECHARGE_CURRENT): sensor.sensor_schema(
SY6970PrechargeCurrentSensor,
unit_of_measurement=UNIT_MILLIAMP,
accuracy_decimals=0,
device_class=DEVICE_CLASS_CURRENT,
state_class=STATE_CLASS_MEASUREMENT,
),
}
)
async def to_code(config):
parent = await cg.get_variable(config[CONF_SY6970_ID])
if vbus_voltage_config := config.get(CONF_VBUS_VOLTAGE):
sens = await sensor.new_sensor(vbus_voltage_config)
cg.add(parent.add_listener(sens))
if battery_voltage_config := config.get(CONF_BATTERY_VOLTAGE):
sens = await sensor.new_sensor(battery_voltage_config)
cg.add(parent.add_listener(sens))
if system_voltage_config := config.get(CONF_SYSTEM_VOLTAGE):
sens = await sensor.new_sensor(system_voltage_config)
cg.add(parent.add_listener(sens))
if charge_current_config := config.get(CONF_CHARGE_CURRENT):
sens = await sensor.new_sensor(charge_current_config)
cg.add(parent.add_listener(sens))
if precharge_current_config := config.get(CONF_PRECHARGE_CURRENT):
sens = await sensor.new_sensor(precharge_current_config)
cg.add(parent.add_listener(sens))

View File

@@ -0,0 +1,46 @@
#pragma once
#include "../sy6970.h"
#include "esphome/components/sensor/sensor.h"
namespace esphome::sy6970 {
// Template for voltage sensors (converts mV to V)
template<uint8_t REG, uint8_t MASK, uint16_t BASE, uint16_t STEP>
class VoltageSensor : public SY6970Listener, public sensor::Sensor {
public:
void on_data(const SY6970Data &data) override {
uint8_t val = data.registers[REG] & MASK;
uint16_t voltage_mv = BASE + (val * STEP);
this->publish_state(voltage_mv * 0.001f); // Convert mV to V
}
};
// Template for current sensors (returns mA)
template<uint8_t REG, uint8_t MASK, uint16_t BASE, uint16_t STEP>
class CurrentSensor : public SY6970Listener, public sensor::Sensor {
public:
void on_data(const SY6970Data &data) override {
uint8_t val = data.registers[REG] & MASK;
uint16_t current_ma = BASE + (val * STEP);
this->publish_state(current_ma);
}
};
// Specialized sensor types using templates
using SY6970VbusVoltageSensor = VoltageSensor<SY6970_REG_VBUS_VOLTAGE, 0x7F, VBUS_BASE_MV, VBUS_STEP_MV>;
using SY6970BatteryVoltageSensor = VoltageSensor<SY6970_REG_BATV, 0x7F, VBAT_BASE_MV, VBAT_STEP_MV>;
using SY6970SystemVoltageSensor = VoltageSensor<SY6970_REG_VINDPM_STATUS, 0x7F, VSYS_BASE_MV, VSYS_STEP_MV>;
using SY6970ChargeCurrentSensor = CurrentSensor<SY6970_REG_CHARGE_CURRENT_MONITOR, 0x7F, 0, CHG_CURRENT_STEP_MA>;
// Precharge current sensor needs special handling (bit shift)
class SY6970PrechargeCurrentSensor : public SY6970Listener, public sensor::Sensor {
public:
void on_data(const SY6970Data &data) override {
uint8_t iprechg = (data.registers[SY6970_REG_PRECHARGE_CURRENT] >> 4) & 0x0F;
uint16_t iprechg_ma = PRE_CHG_BASE_MA + (iprechg * PRE_CHG_STEP_MA);
this->publish_state(iprechg_ma);
}
};
} // namespace esphome::sy6970

View File

@@ -0,0 +1,201 @@
#include "sy6970.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome::sy6970 {
static const char *const TAG = "sy6970";
bool SY6970Component::read_all_registers_() {
// Read all registers from 0x00 to 0x14 in one transaction (21 bytes)
// This includes unused registers 0x0F, 0x10 for performance
if (!this->read_bytes(SY6970_REG_INPUT_CURRENT_LIMIT, this->data_.registers, 21)) {
ESP_LOGW(TAG, "Failed to read registers 0x00-0x14");
return false;
}
return true;
}
bool SY6970Component::write_register_(uint8_t reg, uint8_t value) {
if (!this->write_byte(reg, value)) {
ESP_LOGW(TAG, "Failed to write register 0x%02X", reg);
return false;
}
return true;
}
bool SY6970Component::update_register_(uint8_t reg, uint8_t mask, uint8_t value) {
uint8_t reg_value;
if (!this->read_byte(reg, &reg_value)) {
ESP_LOGW(TAG, "Failed to read register 0x%02X for update", reg);
return false;
}
reg_value = (reg_value & ~mask) | (value & mask);
return this->write_register_(reg, reg_value);
}
void SY6970Component::setup() {
ESP_LOGV(TAG, "Setting up SY6970...");
// Try to read chip ID
uint8_t reg_value;
if (!this->read_byte(SY6970_REG_DEVICE_ID, &reg_value)) {
ESP_LOGE(TAG, "Failed to communicate with SY6970");
this->mark_failed();
return;
}
uint8_t chip_id = reg_value & 0x03;
if (chip_id != 0x00) {
ESP_LOGW(TAG, "Unexpected chip ID: 0x%02X (expected 0x00)", chip_id);
}
// Apply configuration options (all have defaults now)
ESP_LOGV(TAG, "Setting LED enabled to %s", ONOFF(this->led_enabled_));
this->set_led_enabled(this->led_enabled_);
ESP_LOGV(TAG, "Setting input current limit to %u mA", this->input_current_limit_);
this->set_input_current_limit(this->input_current_limit_);
ESP_LOGV(TAG, "Setting charge voltage to %u mV", this->charge_voltage_);
this->set_charge_target_voltage(this->charge_voltage_);
ESP_LOGV(TAG, "Setting charge current to %u mA", this->charge_current_);
this->set_charge_current(this->charge_current_);
ESP_LOGV(TAG, "Setting precharge current to %u mA", this->precharge_current_);
this->set_precharge_current(this->precharge_current_);
ESP_LOGV(TAG, "Setting charge enabled to %s", ONOFF(this->charge_enabled_));
this->set_charge_enabled(this->charge_enabled_);
ESP_LOGV(TAG, "Setting ADC measurements to %s", ONOFF(this->enable_adc_));
this->set_enable_adc_measure(this->enable_adc_);
ESP_LOGV(TAG, "SY6970 initialized successfully");
}
void SY6970Component::dump_config() {
ESP_LOGCONFIG(TAG,
"SY6970:\n"
" LED Enabled: %s\n"
" Input Current Limit: %u mA\n"
" Charge Voltage: %u mV\n"
" Charge Current: %u mA\n"
" Precharge Current: %u mA\n"
" Charge Enabled: %s\n"
" ADC Enabled: %s",
ONOFF(this->led_enabled_), this->input_current_limit_, this->charge_voltage_, this->charge_current_,
this->precharge_current_, ONOFF(this->charge_enabled_), ONOFF(this->enable_adc_));
LOG_I2C_DEVICE(this);
LOG_UPDATE_INTERVAL(this);
if (this->is_failed()) {
ESP_LOGE(TAG, "Communication with SY6970 failed!");
}
}
void SY6970Component::update() {
if (this->is_failed()) {
return;
}
// Read all registers in one transaction
if (!this->read_all_registers_()) {
ESP_LOGW(TAG, "Failed to read registers during update");
this->status_set_warning();
return;
}
this->status_clear_warning();
// Notify all listeners with the new data
for (auto *listener : this->listeners_) {
listener->on_data(this->data_);
}
}
void SY6970Component::set_input_current_limit(uint16_t milliamps) {
if (this->is_failed())
return;
if (milliamps < INPUT_CURRENT_MIN) {
milliamps = INPUT_CURRENT_MIN;
}
uint8_t val = (milliamps - INPUT_CURRENT_MIN) / INPUT_CURRENT_STEP;
if (val > 0x3F) {
val = 0x3F;
}
this->update_register_(SY6970_REG_INPUT_CURRENT_LIMIT, 0x3F, val);
}
void SY6970Component::set_charge_target_voltage(uint16_t millivolts) {
if (this->is_failed())
return;
if (millivolts < CHG_VOLTAGE_BASE) {
millivolts = CHG_VOLTAGE_BASE;
}
uint8_t val = (millivolts - CHG_VOLTAGE_BASE) / CHG_VOLTAGE_STEP;
if (val > 0x3F) {
val = 0x3F;
}
this->update_register_(SY6970_REG_CHARGE_VOLTAGE, 0xFC, val << 2);
}
void SY6970Component::set_precharge_current(uint16_t milliamps) {
if (this->is_failed())
return;
if (milliamps < PRE_CHG_BASE_MA) {
milliamps = PRE_CHG_BASE_MA;
}
uint8_t val = (milliamps - PRE_CHG_BASE_MA) / PRE_CHG_STEP_MA;
if (val > 0x0F) {
val = 0x0F;
}
this->update_register_(SY6970_REG_PRECHARGE_CURRENT, 0xF0, val << 4);
}
void SY6970Component::set_charge_current(uint16_t milliamps) {
if (this->is_failed())
return;
uint8_t val = milliamps / 64;
if (val > 0x7F) {
val = 0x7F;
}
this->update_register_(SY6970_REG_CHARGE_CURRENT, 0x7F, val);
}
void SY6970Component::set_charge_enabled(bool enabled) {
if (this->is_failed())
return;
this->update_register_(SY6970_REG_SYS_CONTROL, 0x10, enabled ? 0x10 : 0x00);
}
void SY6970Component::set_led_enabled(bool enabled) {
if (this->is_failed())
return;
// Bit 6: 0 = LED enabled, 1 = LED disabled
this->update_register_(SY6970_REG_TIMER_CONTROL, 0x40, enabled ? 0x00 : 0x40);
}
void SY6970Component::set_enable_adc_measure(bool enabled) {
if (this->is_failed())
return;
// Set bits to enable ADC conversion
this->update_register_(SY6970_REG_ADC_CONTROL, 0xC0, enabled ? 0xC0 : 0x00);
}
} // namespace esphome::sy6970

View File

@@ -0,0 +1,122 @@
#pragma once
#include "esphome/components/i2c/i2c.h"
#include "esphome/core/component.h"
#include <vector>
namespace esphome::sy6970 {
// SY6970 Register addresses with descriptive names
static const uint8_t SY6970_REG_INPUT_CURRENT_LIMIT = 0x00; // Input current limit control
static const uint8_t SY6970_REG_VINDPM = 0x01; // Input voltage limit
static const uint8_t SY6970_REG_ADC_CONTROL = 0x02; // ADC control and function disable
static const uint8_t SY6970_REG_SYS_CONTROL = 0x03; // Charge enable and system config
static const uint8_t SY6970_REG_CHARGE_CURRENT = 0x04; // Fast charge current limit
static const uint8_t SY6970_REG_PRECHARGE_CURRENT = 0x05; // Pre-charge/termination current
static const uint8_t SY6970_REG_CHARGE_VOLTAGE = 0x06; // Charge voltage limit
static const uint8_t SY6970_REG_TIMER_CONTROL = 0x07; // Charge timer and status LED control
static const uint8_t SY6970_REG_IR_COMP = 0x08; // IR compensation
static const uint8_t SY6970_REG_FORCE_DPDM = 0x09; // Force DPDM detection
static const uint8_t SY6970_REG_BOOST_CONTROL = 0x0A; // Boost mode voltage/current
static const uint8_t SY6970_REG_STATUS = 0x0B; // System status (bus, charge status)
static const uint8_t SY6970_REG_FAULT = 0x0C; // Fault status (NTC)
static const uint8_t SY6970_REG_VINDPM_STATUS = 0x0D; // Input voltage limit status (also sys voltage)
static const uint8_t SY6970_REG_BATV = 0x0E; // Battery voltage
static const uint8_t SY6970_REG_VBUS_VOLTAGE = 0x11; // VBUS voltage
static const uint8_t SY6970_REG_CHARGE_CURRENT_MONITOR = 0x12; // Charge current
static const uint8_t SY6970_REG_INPUT_VOLTAGE_LIMIT = 0x13; // Input voltage limit
static const uint8_t SY6970_REG_DEVICE_ID = 0x14; // Part information
// Constants for voltage and current calculations
static const uint16_t VBUS_BASE_MV = 2600; // mV
static const uint16_t VBUS_STEP_MV = 100; // mV
static const uint16_t VBAT_BASE_MV = 2304; // mV
static const uint16_t VBAT_STEP_MV = 20; // mV
static const uint16_t VSYS_BASE_MV = 2304; // mV
static const uint16_t VSYS_STEP_MV = 20; // mV
static const uint16_t CHG_CURRENT_STEP_MA = 50; // mA
static const uint16_t PRE_CHG_BASE_MA = 64; // mA
static const uint16_t PRE_CHG_STEP_MA = 64; // mA
static const uint16_t CHG_VOLTAGE_BASE = 3840; // mV
static const uint16_t CHG_VOLTAGE_STEP = 16; // mV
static const uint16_t INPUT_CURRENT_MIN = 100; // mA
static const uint16_t INPUT_CURRENT_STEP = 50; // mA
// Bus Status values (REG_0B[7:5])
enum BusStatus {
BUS_STATUS_NO_INPUT = 0,
BUS_STATUS_USB_SDP = 1,
BUS_STATUS_USB_CDP = 2,
BUS_STATUS_USB_DCP = 3,
BUS_STATUS_HVDCP = 4,
BUS_STATUS_ADAPTER = 5,
BUS_STATUS_NO_STD_ADAPTER = 6,
BUS_STATUS_OTG = 7,
};
// Charge Status values (REG_0B[4:3])
enum ChargeStatus {
CHARGE_STATUS_NOT_CHARGING = 0,
CHARGE_STATUS_PRE_CHARGE = 1,
CHARGE_STATUS_FAST_CHARGE = 2,
CHARGE_STATUS_CHARGE_DONE = 3,
};
// Structure to hold all register data read in one transaction
struct SY6970Data {
uint8_t registers[21]; // Registers 0x00-0x14 (includes unused 0x0F, 0x10)
};
// Listener interface for components that want to receive SY6970 data updates
class SY6970Listener {
public:
virtual void on_data(const SY6970Data &data) = 0;
};
class SY6970Component : public PollingComponent, public i2c::I2CDevice {
public:
SY6970Component(bool led_enabled, uint16_t input_current_limit, uint16_t charge_voltage, uint16_t charge_current,
uint16_t precharge_current, bool charge_enabled, bool enable_adc)
: led_enabled_(led_enabled),
input_current_limit_(input_current_limit),
charge_voltage_(charge_voltage),
charge_current_(charge_current),
precharge_current_(precharge_current),
charge_enabled_(charge_enabled),
enable_adc_(enable_adc) {}
void setup() override;
void dump_config() override;
void update() override;
float get_setup_priority() const override { return setup_priority::DATA; }
// Listener registration
void add_listener(SY6970Listener *listener) { this->listeners_.push_back(listener); }
// Configuration methods to be called from lambdas
void set_input_current_limit(uint16_t milliamps);
void set_charge_target_voltage(uint16_t millivolts);
void set_precharge_current(uint16_t milliamps);
void set_charge_current(uint16_t milliamps);
void set_charge_enabled(bool enabled);
void set_led_enabled(bool enabled);
void set_enable_adc_measure(bool enabled = true);
protected:
bool read_all_registers_();
bool write_register_(uint8_t reg, uint8_t value);
bool update_register_(uint8_t reg, uint8_t mask, uint8_t value);
SY6970Data data_{};
std::vector<SY6970Listener *> listeners_;
// Configuration values to set during setup()
bool led_enabled_;
uint16_t input_current_limit_;
uint16_t charge_voltage_;
uint16_t charge_current_;
uint16_t precharge_current_;
bool charge_enabled_;
bool enable_adc_;
};
} // namespace esphome::sy6970

View File

@@ -0,0 +1,52 @@
import esphome.codegen as cg
from esphome.components import text_sensor
import esphome.config_validation as cv
from .. import CONF_SY6970_ID, SY6970Component, sy6970_ns
DEPENDENCIES = ["sy6970"]
CONF_BUS_STATUS = "bus_status"
CONF_CHARGE_STATUS = "charge_status"
CONF_NTC_STATUS = "ntc_status"
SY6970BusStatusTextSensor = sy6970_ns.class_(
"SY6970BusStatusTextSensor", text_sensor.TextSensor
)
SY6970ChargeStatusTextSensor = sy6970_ns.class_(
"SY6970ChargeStatusTextSensor", text_sensor.TextSensor
)
SY6970NtcStatusTextSensor = sy6970_ns.class_(
"SY6970NtcStatusTextSensor", text_sensor.TextSensor
)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_SY6970_ID): cv.use_id(SY6970Component),
cv.Optional(CONF_BUS_STATUS): text_sensor.text_sensor_schema(
SY6970BusStatusTextSensor
),
cv.Optional(CONF_CHARGE_STATUS): text_sensor.text_sensor_schema(
SY6970ChargeStatusTextSensor
),
cv.Optional(CONF_NTC_STATUS): text_sensor.text_sensor_schema(
SY6970NtcStatusTextSensor
),
}
)
async def to_code(config):
parent = await cg.get_variable(config[CONF_SY6970_ID])
if bus_status_config := config.get(CONF_BUS_STATUS):
sens = await text_sensor.new_text_sensor(bus_status_config)
cg.add(parent.add_listener(sens))
if charge_status_config := config.get(CONF_CHARGE_STATUS):
sens = await text_sensor.new_text_sensor(charge_status_config)
cg.add(parent.add_listener(sens))
if ntc_status_config := config.get(CONF_NTC_STATUS):
sens = await text_sensor.new_text_sensor(ntc_status_config)
cg.add(parent.add_listener(sens))

View File

@@ -0,0 +1,96 @@
#pragma once
#include "../sy6970.h"
#include "esphome/components/text_sensor/text_sensor.h"
namespace esphome::sy6970 {
// Bus status text sensor
class SY6970BusStatusTextSensor : public SY6970Listener, public text_sensor::TextSensor {
public:
void on_data(const SY6970Data &data) override {
uint8_t status = (data.registers[SY6970_REG_STATUS] >> 5) & 0x07;
const char *status_str = this->get_bus_status_string_(status);
this->publish_state(status_str);
}
protected:
const char *get_bus_status_string_(uint8_t status) {
switch (status) {
case BUS_STATUS_NO_INPUT:
return "No Input";
case BUS_STATUS_USB_SDP:
return "USB SDP";
case BUS_STATUS_USB_CDP:
return "USB CDP";
case BUS_STATUS_USB_DCP:
return "USB DCP";
case BUS_STATUS_HVDCP:
return "HVDCP";
case BUS_STATUS_ADAPTER:
return "Adapter";
case BUS_STATUS_NO_STD_ADAPTER:
return "Non-Standard Adapter";
case BUS_STATUS_OTG:
return "OTG";
default:
return "Unknown";
}
}
};
// Charge status text sensor
class SY6970ChargeStatusTextSensor : public SY6970Listener, public text_sensor::TextSensor {
public:
void on_data(const SY6970Data &data) override {
uint8_t status = (data.registers[SY6970_REG_STATUS] >> 3) & 0x03;
const char *status_str = this->get_charge_status_string_(status);
this->publish_state(status_str);
}
protected:
const char *get_charge_status_string_(uint8_t status) {
switch (status) {
case CHARGE_STATUS_NOT_CHARGING:
return "Not Charging";
case CHARGE_STATUS_PRE_CHARGE:
return "Pre-charge";
case CHARGE_STATUS_FAST_CHARGE:
return "Fast Charge";
case CHARGE_STATUS_CHARGE_DONE:
return "Charge Done";
default:
return "Unknown";
}
}
};
// NTC status text sensor
class SY6970NtcStatusTextSensor : public SY6970Listener, public text_sensor::TextSensor {
public:
void on_data(const SY6970Data &data) override {
uint8_t status = data.registers[SY6970_REG_FAULT] & 0x07;
const char *status_str = this->get_ntc_status_string_(status);
this->publish_state(status_str);
}
protected:
const char *get_ntc_status_string_(uint8_t status) {
switch (status) {
case 0:
return "Normal";
case 2:
return "Warm";
case 3:
return "Cool";
case 5:
return "Cold";
case 6:
return "Hot";
default:
return "Unknown";
}
}
};
} // namespace esphome::sy6970

View File

@@ -20,7 +20,7 @@ from .. import template_ns
CONF_CURRENT_TEMPERATURE = "current_temperature"
TemplateWaterHeater = template_ns.class_(
"TemplateWaterHeater", water_heater.WaterHeater
"TemplateWaterHeater", cg.Component, water_heater.WaterHeater
)
TemplateWaterHeaterPublishAction = template_ns.class_(
@@ -36,24 +36,29 @@ RESTORE_MODES = {
"RESTORE_AND_CALL": TemplateWaterHeaterRestoreMode.WATER_HEATER_RESTORE_AND_CALL,
}
CONFIG_SCHEMA = water_heater.water_heater_schema(TemplateWaterHeater).extend(
{
cv.Optional(CONF_OPTIMISTIC, default=True): cv.boolean,
cv.Optional(CONF_SET_ACTION): automation.validate_automation(single=True),
cv.Optional(CONF_RESTORE_MODE, default="NO_RESTORE"): cv.enum(
RESTORE_MODES, upper=True
),
cv.Optional(CONF_CURRENT_TEMPERATURE): cv.returning_lambda,
cv.Optional(CONF_MODE): cv.returning_lambda,
cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list(
water_heater.validate_water_heater_mode
),
}
CONFIG_SCHEMA = (
water_heater.water_heater_schema(TemplateWaterHeater)
.extend(
{
cv.Optional(CONF_OPTIMISTIC, default=True): cv.boolean,
cv.Optional(CONF_SET_ACTION): automation.validate_automation(single=True),
cv.Optional(CONF_RESTORE_MODE, default="NO_RESTORE"): cv.enum(
RESTORE_MODES, upper=True
),
cv.Optional(CONF_CURRENT_TEMPERATURE): cv.returning_lambda,
cv.Optional(CONF_MODE): cv.returning_lambda,
cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list(
water_heater.validate_water_heater_mode
),
}
)
.extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await water_heater.register_water_heater(var, config)
cg.add(var.set_optimistic(config[CONF_OPTIMISTIC]))

View File

@@ -10,7 +10,7 @@ TemplateWaterHeater::TemplateWaterHeater() : set_trigger_(new Trigger<>()) {}
void TemplateWaterHeater::setup() {
if (this->restore_mode_ == TemplateWaterHeaterRestoreMode::WATER_HEATER_RESTORE ||
this->restore_mode_ == TemplateWaterHeaterRestoreMode::WATER_HEATER_RESTORE_AND_CALL) {
auto restore = this->restore_state();
auto restore = this->restore_state_();
if (restore.has_value()) {
restore->perform();

View File

@@ -13,7 +13,7 @@ enum TemplateWaterHeaterRestoreMode {
WATER_HEATER_RESTORE_AND_CALL,
};
class TemplateWaterHeater : public water_heater::WaterHeater {
class TemplateWaterHeater : public Component, public water_heater::WaterHeater {
public:
TemplateWaterHeater();

View File

@@ -0,0 +1,17 @@
# Test Deprecated Component
This is a test component to validate the `deprecated_component` label functionality in the auto-label workflow.
## Purpose
This component demonstrates how the `DEPRECATED_COMPONENT` constant should be used:
1. The component is still functional and usable
2. It's deprecated and not actively maintained
3. Users should migrate to an alternative when possible
## Usage
This is different from components that use `cv.invalid()`, which are completely unusable.
Components with `DEPRECATED_COMPONENT` are still functional but discouraged for new projects.

View File

@@ -0,0 +1,6 @@
"""Test deprecated component for validation."""
CODEOWNERS = ["@test"]
DEPRECATED_COMPONENT = "This component is deprecated and will be removed in a future release. Please use the newer_component instead."
# Component is still functional but deprecated

View File

@@ -0,0 +1,5 @@
"""Test sensor platform for deprecated component."""
import esphome.config_validation as cv
# This is a simple placeholder - component still works
CONFIG_SCHEMA = cv.Schema({})

View File

@@ -18,7 +18,7 @@ CODEOWNERS = ["@dhoeben"]
IS_PLATFORM_COMPONENT = True
water_heater_ns = cg.esphome_ns.namespace("water_heater")
WaterHeater = water_heater_ns.class_("WaterHeater", cg.EntityBase, cg.Component)
WaterHeater = water_heater_ns.class_("WaterHeater", cg.EntityBase)
WaterHeaterCall = water_heater_ns.class_("WaterHeaterCall")
WaterHeaterTraits = water_heater_ns.class_("WaterHeaterTraits")
@@ -46,7 +46,7 @@ _WATER_HEATER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
}
),
}
).extend(cv.COMPONENT_SCHEMA)
)
_WATER_HEATER_SCHEMA.add_extra(entity_duplicate_validator("water_heater"))
@@ -91,8 +91,6 @@ async def register_water_heater(var: cg.Pvariable, config: ConfigType) -> cg.Pva
cg.add_define("USE_WATER_HEATER")
await cg.register_component(var, config)
cg.add(cg.App.register_water_heater(var))
CORE.register_platform_component("water_heater", var)

View File

@@ -146,10 +146,6 @@ void WaterHeaterCall::validate_() {
}
}
void WaterHeater::setup() {
this->pref_ = global_preferences->make_preference<SavedWaterHeaterState>(this->get_preference_hash());
}
void WaterHeater::publish_state() {
auto traits = this->get_traits();
ESP_LOGD(TAG,
@@ -188,7 +184,8 @@ void WaterHeater::publish_state() {
this->pref_.save(&saved);
}
optional<WaterHeaterCall> WaterHeater::restore_state() {
optional<WaterHeaterCall> WaterHeater::restore_state_() {
this->pref_ = global_preferences->make_preference<SavedWaterHeaterState>(this->get_preference_hash());
SavedWaterHeaterState recovered{};
if (!this->pref_.load(&recovered))
return {};

View File

@@ -177,7 +177,7 @@ class WaterHeaterTraits {
WaterHeaterModeMask supported_modes_;
};
class WaterHeater : public EntityBase, public Component {
class WaterHeater : public EntityBase {
public:
WaterHeaterMode get_mode() const { return this->mode_; }
float get_current_temperature() const { return this->current_temperature_; }
@@ -204,16 +204,15 @@ class WaterHeater : public EntityBase, public Component {
#endif
virtual void control(const WaterHeaterCall &call) = 0;
void setup() override;
optional<WaterHeaterCall> restore_state();
protected:
virtual WaterHeaterTraits traits() = 0;
/// Log the traits of this water heater for dump_config().
void dump_traits_(const char *tag);
/// Restore the state of the water heater, call this from your setup() method.
optional<WaterHeaterCall> restore_state_();
/// Set the mode of the water heater. Should only be called from control().
void set_mode_(WaterHeaterMode mode) { this->mode_ = mode; }
/// Set the target temperature of the water heater. Should only be called from control().

View File

@@ -1 +1,2 @@
CODEOWNERS = ["@clydebarrow"]
DEPRECATED_COMPONENT = "The waveshare_epaper component is deprecated and no new models will be added. For new epaper displays use the epaper_spi component"

View File

@@ -698,6 +698,10 @@ bool WiFiComponent::wifi_scan_start_(bool passive) {
if (!this->wifi_mode_(true, {}))
return false;
// Reset scan_done_ before starting new scan to prevent stale flag from previous scan
// (e.g., roaming scan completed just before unexpected disconnect)
this->scan_done_ = false;
struct scan_config config {};
memset(&config, 0, sizeof(config));
config.ssid = nullptr;

View File

@@ -14,6 +14,7 @@
#include <algorithm>
#include <cinttypes>
#include <memory>
#include <utility>
#ifdef USE_WIFI_WPA2_EAP
#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1)
@@ -828,16 +829,29 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
uint16_t number = it.number;
scan_result_.init(number);
// Process one record at a time to avoid large buffer allocation
wifi_ap_record_t record;
#ifdef USE_ESP32_HOSTED
// getting records one at a time fails on P4 with hosted esp32 WiFi coprocessor
// Presumably an upstream bug, work-around by getting all records at once
auto records = std::make_unique<wifi_ap_record_t[]>(number);
err = esp_wifi_scan_get_ap_records(&number, records.get());
if (err != ESP_OK) {
esp_wifi_clear_ap_list();
ESP_LOGW(TAG, "esp_wifi_scan_get_ap_records failed: %s", esp_err_to_name(err));
return;
}
for (uint16_t i = 0; i < number; i++) {
wifi_ap_record_t &record = records[i];
#else
// Process one record at a time to avoid large buffer allocation
for (uint16_t i = 0; i < number; i++) {
wifi_ap_record_t record;
err = esp_wifi_scan_get_ap_record(&record);
if (err != ESP_OK) {
ESP_LOGW(TAG, "esp_wifi_scan_get_ap_record failed: %s", esp_err_to_name(err));
esp_wifi_clear_ap_list(); // Free remaining records not yet retrieved
break;
}
#endif // USE_ESP32_HOSTED
bssid_t bssid;
std::copy(record.bssid, record.bssid + 6, bssid.begin());
std::string ssid(reinterpret_cast<const char *>(record.ssid));

View File

@@ -649,6 +649,10 @@ bool WiFiComponent::wifi_scan_start_(bool passive) {
if (!this->wifi_mode_(true, {}))
return false;
// Reset scan_done_ before starting new scan to prevent stale flag from previous scan
// (e.g., roaming scan completed just before unexpected disconnect)
this->scan_done_ = false;
// need to use WiFi because of WiFiScanClass allocations :(
int16_t err = WiFi.scanNetworks(true, true, passive, 200);
if (err != WIFI_SCAN_RUNNING) {

View File

@@ -42,6 +42,7 @@
#define USE_DEVICES
#define USE_DISPLAY
#define USE_ENTITY_ICON
#define USE_ESP32_HOSTED
#define USE_ESP32_IMPROV_STATE_CALLBACK
#define USE_EVENT
#define USE_FAN

View File

@@ -148,25 +148,6 @@ template<typename T, size_t N> class StaticVector {
size_t count_{0};
public:
// Default constructor
StaticVector() = default;
// Iterator range constructor
template<typename InputIt> StaticVector(InputIt first, InputIt last) {
while (first != last && count_ < N) {
data_[count_++] = *first++;
}
}
// Initializer list constructor
StaticVector(std::initializer_list<T> init) {
for (const auto &val : init) {
if (count_ >= N)
break;
data_[count_++] = val;
}
}
// Minimal vector-compatible interface - only what we actually use
void push_back(const T &value) {
if (count_ < N) {
@@ -174,17 +155,6 @@ template<typename T, size_t N> class StaticVector {
}
}
// Clear all elements
void clear() { count_ = 0; }
// Assign from iterator range
template<typename InputIt> void assign(InputIt first, InputIt last) {
count_ = 0;
while (first != last && count_ < N) {
data_[count_++] = *first++;
}
}
// Return reference to next element and increment count (with bounds checking)
T &emplace_next() {
if (count_ >= N) {

View File

@@ -0,0 +1,57 @@
sy6970:
id: sy6970_component
i2c_id: i2c_bus
address: 0x6A
enable_status_led: true
input_current_limit: 1000
charge_voltage: 4200
charge_current: 500
precharge_current: 128
charge_enabled: true
enable_adc: true
update_interval: 5s
sensor:
- platform: sy6970
sy6970_id: sy6970_component
vbus_voltage:
name: "VBUS Voltage"
id: vbus_voltage_sensor
battery_voltage:
name: "Battery Voltage"
id: battery_voltage_sensor
system_voltage:
name: "System Voltage"
id: system_voltage_sensor
charge_current:
name: "Charge Current"
id: charge_current_sensor
precharge_current:
name: "Precharge Current"
id: precharge_current_sensor
binary_sensor:
- platform: sy6970
sy6970_id: sy6970_component
vbus_connected:
name: "VBUS Connected"
id: vbus_connected_binary
charging:
name: "Charging"
id: charging_binary
charge_done:
name: "Charge Done"
id: charge_done_binary
text_sensor:
- platform: sy6970
sy6970_id: sy6970_component
bus_status:
name: "Bus Status"
id: bus_status_text
charge_status:
name: "Charge Status"
id: charge_status_text
ntc_status:
name: "NTC Status"
id: ntc_status_text

View File

@@ -0,0 +1,4 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
<<: !include common.yaml