mirror of
https://github.com/esphome/esphome.git
synced 2026-07-10 17:05:36 +00:00
Merge remote-tracking branch 'upstream/dev' into integration
# Conflicts: # esphome/components/esphome/ota/ota_esphome.h # esphome/components/http_request/ota/ota_http_request.cpp # esphome/components/http_request/ota/ota_http_request.h # esphome/components/ota/ota_backend_factory.h # esphome/components/web_server/ota/ota_web_server.cpp # esphome/components/wifi/wifi_component.cpp
This commit is contained in:
@@ -49,7 +49,7 @@ jobs:
|
||||
with:
|
||||
python-version: "3.11"
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
|
||||
- name: Set TAG
|
||||
run: |
|
||||
|
||||
@@ -99,15 +99,15 @@ jobs:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
|
||||
- name: Log in to docker hub
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Log in to the GitHub container registry
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -178,17 +178,17 @@ jobs:
|
||||
merge-multiple: true
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
|
||||
- name: Log in to docker hub
|
||||
if: matrix.registry == 'dockerhub'
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Log in to the GitHub container registry
|
||||
if: matrix.registry == 'ghcr'
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
||||
@@ -54,6 +54,7 @@ esphome/components/atm90e32/* @circuitsetup @descipher
|
||||
esphome/components/audio/* @kahrendt
|
||||
esphome/components/audio_adc/* @kbx81
|
||||
esphome/components/audio_dac/* @kbx81
|
||||
esphome/components/audio_file/* @kahrendt
|
||||
esphome/components/axs15231/* @clydebarrow
|
||||
esphome/components/b_parasite/* @rbaron
|
||||
esphome/components/ballu/* @bazuchan
|
||||
|
||||
@@ -125,7 +125,7 @@ void Alpha3::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc
|
||||
this->current_sensor_->publish_state(NAN);
|
||||
if (this->speed_sensor_ != nullptr)
|
||||
this->speed_sensor_->publish_state(NAN);
|
||||
if (this->speed_sensor_ != nullptr)
|
||||
if (this->voltage_sensor_ != nullptr)
|
||||
this->voltage_sensor_->publish_state(NAN);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -61,6 +61,10 @@ optional<ParseResult> ATCMiThermometer::parse_header_(const esp32_ble_tracker::S
|
||||
}
|
||||
|
||||
auto raw = service_data.data;
|
||||
if (raw.size() < 13) {
|
||||
ESP_LOGVV(TAG, "parse_header_(): service data too short (%zu).", raw.size());
|
||||
return {};
|
||||
}
|
||||
|
||||
static uint8_t last_frame_count = 0;
|
||||
if (last_frame_count == raw[12]) {
|
||||
|
||||
@@ -197,7 +197,7 @@ float ATM90E26Component::get_reactive_power_() {
|
||||
float ATM90E26Component::get_power_factor_() {
|
||||
const uint16_t val = this->read16_(ATM90E26_REGISTER_POWERF); // signed
|
||||
if (val & 0x8000) {
|
||||
return -(val & 0x7FF) / 1000.0f;
|
||||
return -(val & 0x7FFF) / 1000.0f;
|
||||
} else {
|
||||
return val / 1000.0f;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
from dataclasses import dataclass, field
|
||||
import hashlib
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
import puremagic
|
||||
|
||||
from esphome import external_files
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import audio
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_FILE,
|
||||
CONF_ID,
|
||||
CONF_PATH,
|
||||
CONF_RAW_DATA_ID,
|
||||
CONF_TYPE,
|
||||
CONF_URL,
|
||||
)
|
||||
from esphome.core import CORE, ID, HexInt
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.external_files import download_content
|
||||
from esphome.types import ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CODEOWNERS = ["@kahrendt"]
|
||||
|
||||
AUTO_LOAD = ["audio"]
|
||||
|
||||
DOMAIN = "audio_file"
|
||||
|
||||
audio_file_ns = cg.esphome_ns.namespace("audio_file")
|
||||
|
||||
TYPE_LOCAL = "local"
|
||||
TYPE_WEB = "web"
|
||||
|
||||
|
||||
@dataclass
|
||||
class AudioFileData:
|
||||
file_ids: dict[str, ID] = field(default_factory=dict)
|
||||
file_cache: dict[str, tuple[bytes, MockObj]] = field(default_factory=dict)
|
||||
|
||||
|
||||
def _get_data() -> AudioFileData:
|
||||
if DOMAIN not in CORE.data:
|
||||
CORE.data[DOMAIN] = AudioFileData()
|
||||
return CORE.data[DOMAIN]
|
||||
|
||||
|
||||
def get_audio_file_ids() -> dict[str, ID]:
|
||||
"""Get all registered audio file IDs for cross-component access."""
|
||||
return _get_data().file_ids
|
||||
|
||||
|
||||
def _compute_local_file_path(value: ConfigType) -> Path:
|
||||
url = value[CONF_URL]
|
||||
h = hashlib.new("sha256")
|
||||
h.update(url.encode())
|
||||
key = h.hexdigest()[:8]
|
||||
base_dir = external_files.compute_local_file_dir(DOMAIN)
|
||||
_LOGGER.debug("_compute_local_file_path: base_dir=%s", base_dir / key)
|
||||
return base_dir / key
|
||||
|
||||
|
||||
def _download_web_file(value: ConfigType) -> ConfigType:
|
||||
url = value[CONF_URL]
|
||||
path = _compute_local_file_path(value)
|
||||
|
||||
download_content(url, path)
|
||||
_LOGGER.debug("download_web_file: path=%s", path)
|
||||
return value
|
||||
|
||||
|
||||
def _file_schema(value: ConfigType | str) -> ConfigType:
|
||||
if isinstance(value, str):
|
||||
return _validate_file_shorthand(value)
|
||||
return TYPED_FILE_SCHEMA(value)
|
||||
|
||||
|
||||
def _validate_file_shorthand(value: str) -> ConfigType:
|
||||
value = cv.string_strict(value)
|
||||
if value.startswith("http://") or value.startswith("https://"):
|
||||
return _file_schema(
|
||||
{
|
||||
CONF_TYPE: TYPE_WEB,
|
||||
CONF_URL: value,
|
||||
}
|
||||
)
|
||||
return _file_schema(
|
||||
{
|
||||
CONF_TYPE: TYPE_LOCAL,
|
||||
CONF_PATH: value,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def read_audio_file_and_type(file_config: ConfigType) -> tuple[bytes, MockObj]:
|
||||
"""Read an audio file and determine its type. Used by this component and media_source platform."""
|
||||
conf_file = file_config[CONF_FILE]
|
||||
file_source = conf_file[CONF_TYPE]
|
||||
if file_source == TYPE_LOCAL:
|
||||
path = CORE.relative_config_path(conf_file[CONF_PATH])
|
||||
elif file_source == TYPE_WEB:
|
||||
path = _compute_local_file_path(conf_file)
|
||||
else:
|
||||
raise cv.Invalid("Unsupported file source")
|
||||
|
||||
with open(path, "rb") as f:
|
||||
data = f.read()
|
||||
|
||||
try:
|
||||
file_type: str = puremagic.from_string(data)
|
||||
file_type = file_type.removeprefix(".")
|
||||
except puremagic.PureError as e:
|
||||
raise cv.Invalid(
|
||||
f"Unable to determine audio file type of '{path}'. "
|
||||
f"Try re-encoding the file into a supported format. Details: {e}"
|
||||
)
|
||||
|
||||
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["NONE"]
|
||||
if file_type == "wav":
|
||||
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["WAV"]
|
||||
elif file_type in ("mp3", "mpeg", "mpga"):
|
||||
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["MP3"]
|
||||
elif file_type == "flac":
|
||||
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["FLAC"]
|
||||
elif (
|
||||
file_type == "ogg"
|
||||
and len(data) >= 36
|
||||
and data.startswith(b"OggS")
|
||||
and data[28:36] == b"OpusHead"
|
||||
):
|
||||
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["OPUS"]
|
||||
|
||||
return data, media_file_type
|
||||
|
||||
|
||||
LOCAL_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_PATH): cv.file_,
|
||||
}
|
||||
)
|
||||
|
||||
WEB_SCHEMA = cv.All(
|
||||
{
|
||||
cv.Required(CONF_URL): cv.url,
|
||||
},
|
||||
_download_web_file,
|
||||
)
|
||||
|
||||
|
||||
TYPED_FILE_SCHEMA = cv.typed_schema(
|
||||
{
|
||||
TYPE_LOCAL: LOCAL_SCHEMA,
|
||||
TYPE_WEB: WEB_SCHEMA,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
MEDIA_FILE_TYPE_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.declare_id(audio.AudioFile),
|
||||
cv.Required(CONF_FILE): _file_schema,
|
||||
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
MAX_FILE_SIZE = 5 * 1024 * 1024 # 5 MB
|
||||
|
||||
|
||||
def _validate_supported_local_file(config: list[ConfigType]) -> list[ConfigType]:
|
||||
for file_config in config:
|
||||
data, media_file_type = read_audio_file_and_type(file_config)
|
||||
|
||||
if len(data) > MAX_FILE_SIZE:
|
||||
file_info = file_config.get(CONF_FILE, {})
|
||||
source = (
|
||||
file_info.get(CONF_PATH) or file_info.get(CONF_URL) or "unknown source"
|
||||
)
|
||||
raise cv.Invalid(
|
||||
f"Audio file {source!r} is too large ({len(data)} bytes, max {MAX_FILE_SIZE} bytes)"
|
||||
)
|
||||
|
||||
if str(media_file_type) == str(audio.AUDIO_FILE_TYPE_ENUM["NONE"]):
|
||||
file_info = file_config.get(CONF_FILE, {})
|
||||
source = (
|
||||
file_info.get(CONF_PATH) or file_info.get(CONF_URL) or "unknown source"
|
||||
)
|
||||
raise cv.Invalid(
|
||||
f"Unsupported media file from {source!r} (detected type: {media_file_type})"
|
||||
)
|
||||
|
||||
# Cache the file data so to_code() doesn't need to re-read it
|
||||
_get_data().file_cache[str(file_config[CONF_ID])] = (data, media_file_type)
|
||||
|
||||
media_file_type_str = str(media_file_type)
|
||||
if media_file_type_str == str(audio.AUDIO_FILE_TYPE_ENUM["FLAC"]):
|
||||
audio.request_flac_support()
|
||||
elif media_file_type_str == str(audio.AUDIO_FILE_TYPE_ENUM["MP3"]):
|
||||
audio.request_mp3_support()
|
||||
elif media_file_type_str == str(audio.AUDIO_FILE_TYPE_ENUM["OPUS"]):
|
||||
audio.request_opus_support()
|
||||
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.only_on_esp32,
|
||||
cv.ensure_list(MEDIA_FILE_TYPE_SCHEMA),
|
||||
_validate_supported_local_file,
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: list[ConfigType]) -> None:
|
||||
cache = _get_data().file_cache
|
||||
|
||||
for file_config in config:
|
||||
file_id = str(file_config[CONF_ID])
|
||||
data, media_file_type = cache[file_id]
|
||||
|
||||
rhs = [HexInt(x) for x in data]
|
||||
prog_arr = cg.progmem_array(file_config[CONF_RAW_DATA_ID], rhs)
|
||||
|
||||
media_files_struct = cg.StructInitializer(
|
||||
audio.AudioFile,
|
||||
(
|
||||
"data",
|
||||
prog_arr,
|
||||
),
|
||||
(
|
||||
"length",
|
||||
len(rhs),
|
||||
),
|
||||
(
|
||||
"file_type",
|
||||
media_file_type,
|
||||
),
|
||||
)
|
||||
|
||||
cg.new_Pvariable(
|
||||
file_config[CONF_ID],
|
||||
media_files_struct,
|
||||
)
|
||||
|
||||
# Store file ID for cross-component access
|
||||
_get_data().file_ids[file_id] = file_config[CONF_ID]
|
||||
|
||||
# Register all files in the shared C++ registry
|
||||
cg.add_define("AUDIO_FILE_MAX_FILES", len(config))
|
||||
for file_config in config:
|
||||
file_id = str(file_config[CONF_ID])
|
||||
file_var = await cg.get_variable(file_config[CONF_ID])
|
||||
cg.add(audio_file_ns.add_named_audio_file(file_var, file_id))
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#ifdef AUDIO_FILE_MAX_FILES
|
||||
|
||||
#include "esphome/components/audio/audio.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::audio_file {
|
||||
|
||||
struct NamedAudioFile {
|
||||
audio::AudioFile *file;
|
||||
const char *file_id;
|
||||
};
|
||||
|
||||
inline StaticVector<NamedAudioFile, AUDIO_FILE_MAX_FILES>
|
||||
named_audio_files; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
inline void add_named_audio_file(audio::AudioFile *file, const char *file_id) {
|
||||
named_audio_files.push_back({file, file_id});
|
||||
}
|
||||
|
||||
inline const StaticVector<NamedAudioFile, AUDIO_FILE_MAX_FILES> &get_named_audio_files() { return named_audio_files; }
|
||||
|
||||
} // namespace esphome::audio_file
|
||||
|
||||
#endif // AUDIO_FILE_MAX_FILES
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "bedjet_codec.h"
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
@@ -68,6 +69,10 @@ BedjetPacket *BedjetCodec::get_set_runtime_remaining_request(const uint8_t hour,
|
||||
|
||||
/** Decodes the extra bytes that were received after being notified with a partial packet. */
|
||||
void BedjetCodec::decode_extra(const uint8_t *data, uint16_t length) {
|
||||
if (length < 5) {
|
||||
ESP_LOGVV(TAG, "Received extra: %d bytes (too short)", length);
|
||||
return;
|
||||
}
|
||||
ESP_LOGVV(TAG, "Received extra: %d bytes: %d %d %d %d", length, data[1], data[2], data[3], data[4]);
|
||||
uint8_t offset = this->last_buffer_size_;
|
||||
if (offset > 0 && length + offset <= sizeof(BedjetStatusPacket)) {
|
||||
@@ -90,14 +95,19 @@ void BedjetCodec::decode_extra(const uint8_t *data, uint16_t length) {
|
||||
* @return `true` if the packet was decoded and represents a "partial" packet; `false` otherwise.
|
||||
*/
|
||||
bool BedjetCodec::decode_notify(const uint8_t *data, uint16_t length) {
|
||||
if (length < 5) {
|
||||
ESP_LOGW(TAG, "Received short packet: %d bytes", length);
|
||||
return false;
|
||||
}
|
||||
ESP_LOGV(TAG, "Received: %d bytes: %d %d %d %d", length, data[1], data[2], data[3], data[4]);
|
||||
|
||||
if (data[1] == PACKET_FORMAT_V3_HOME && data[3] == PACKET_TYPE_STATUS) {
|
||||
// Clear old buffer
|
||||
memset(&this->buf_, 0, sizeof(BedjetStatusPacket));
|
||||
// Copy new data into buffer
|
||||
memcpy(&this->buf_, data, length);
|
||||
this->last_buffer_size_ = length;
|
||||
size_t copy_len = std::min(static_cast<size_t>(length), sizeof(BedjetStatusPacket));
|
||||
memcpy(&this->buf_, data, copy_len);
|
||||
this->last_buffer_size_ = copy_len;
|
||||
|
||||
// TODO: validate the packet checksum?
|
||||
if (this->buf_.mode < 7 && this->buf_.target_temp_step >= 38 && this->buf_.target_temp_step <= 86 &&
|
||||
@@ -113,13 +123,15 @@ bool BedjetCodec::decode_notify(const uint8_t *data, uint16_t length) {
|
||||
}
|
||||
} else if (data[1] == PACKET_FORMAT_DEBUG || data[3] == PACKET_TYPE_DEBUG) {
|
||||
// We don't actually know the packet format for this. Dump packets to log, in case a pattern presents itself.
|
||||
ESP_LOGVV(TAG,
|
||||
"received DEBUG packet: set1=%01fF, set2=%01fF, air=%01fF; [7]=%d, [8]=%d, [9]=%d, [10]=%d, [11]=%d, "
|
||||
"[12]=%d, [-1]=%d",
|
||||
bedjet_temp_to_f(data[4]), bedjet_temp_to_f(data[5]), bedjet_temp_to_f(data[6]), data[7], data[8],
|
||||
data[9], data[10], data[11], data[12], data[length - 1]);
|
||||
if (length >= 13) {
|
||||
ESP_LOGVV(TAG,
|
||||
"received DEBUG packet: set1=%01fF, set2=%01fF, air=%01fF; [7]=%d, [8]=%d, [9]=%d, [10]=%d, [11]=%d, "
|
||||
"[12]=%d, [-1]=%d",
|
||||
bedjet_temp_to_f(data[4]), bedjet_temp_to_f(data[5]), bedjet_temp_to_f(data[6]), data[7], data[8],
|
||||
data[9], data[10], data[11], data[12], data[length - 1]);
|
||||
}
|
||||
|
||||
if (this->has_status()) {
|
||||
if (this->has_status() && length >= 7) {
|
||||
this->status_packet_->ambient_temp_step = data[6];
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -260,6 +260,7 @@ void DFPlayer::loop() {
|
||||
ESP_LOGV(TAG, "Playback finished (USB drive)");
|
||||
this->is_playing_ = false;
|
||||
this->on_finished_playback_callback_.call();
|
||||
break;
|
||||
case 0x3D:
|
||||
ESP_LOGV(TAG, "Playback finished (SD card)");
|
||||
this->is_playing_ = false;
|
||||
|
||||
@@ -30,11 +30,9 @@ class Command {
|
||||
|
||||
class ReadStateCommand : public Command {
|
||||
public:
|
||||
ReadStateCommand() { timeout_ms_ = 500; }
|
||||
uint8_t execute(DfrobotSen0395Component *parent) override;
|
||||
uint8_t on_message(std::string &message) override;
|
||||
|
||||
protected:
|
||||
uint32_t timeout_ms_{500};
|
||||
};
|
||||
|
||||
class PowerCommand : public Command {
|
||||
@@ -99,12 +97,12 @@ class ResetSystemCommand : public Command {
|
||||
|
||||
class SaveCfgCommand : public Command {
|
||||
public:
|
||||
SaveCfgCommand() { cmd_ = "saveCfg 0x45670123 0xCDEF89AB 0x956128C6 0xDF54AC89"; }
|
||||
SaveCfgCommand() {
|
||||
cmd_ = "saveCfg 0x45670123 0xCDEF89AB 0x956128C6 0xDF54AC89";
|
||||
cmd_duration_ms_ = 3000;
|
||||
timeout_ms_ = 3500;
|
||||
}
|
||||
uint8_t on_message(std::string &message) override;
|
||||
|
||||
protected:
|
||||
uint32_t cmd_duration_ms_{3000};
|
||||
uint32_t timeout_ms_{3500};
|
||||
};
|
||||
|
||||
class LedModeCommand : public Command {
|
||||
|
||||
@@ -22,9 +22,9 @@ class EE895Component : public PollingComponent, public i2c::I2CDevice {
|
||||
void write_command_(uint16_t addr, uint16_t reg_cnt);
|
||||
float read_float_();
|
||||
uint16_t calc_crc16_(const uint8_t buf[], uint8_t len);
|
||||
sensor::Sensor *co2_sensor_;
|
||||
sensor::Sensor *temperature_sensor_;
|
||||
sensor::Sensor *pressure_sensor_;
|
||||
sensor::Sensor *co2_sensor_{nullptr};
|
||||
sensor::Sensor *temperature_sensor_{nullptr};
|
||||
sensor::Sensor *pressure_sensor_{nullptr};
|
||||
|
||||
enum ErrorCode { NONE = 0, COMMUNICATION_FAILED, CRC_CHECK_FAILED } error_code_{NONE};
|
||||
};
|
||||
|
||||
@@ -72,7 +72,7 @@ void Emc2101Component::setup() {
|
||||
config |= EMC2101_DAC_BIT;
|
||||
}
|
||||
if (this->inverted_) {
|
||||
config |= EMC2101_POLARITY_BIT;
|
||||
reg(EMC2101_REGISTER_FAN_CONFIG) |= EMC2101_POLARITY_BIT;
|
||||
}
|
||||
|
||||
if (this->dac_mode_) { // DAC mode configurations
|
||||
|
||||
@@ -86,17 +86,7 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
|
||||
|
||||
socket::ListenSocket *server_{nullptr};
|
||||
std::unique_ptr<socket::Socket> client_;
|
||||
#ifdef USE_ESP8266
|
||||
std::unique_ptr<ota::ESP8266OTABackend> backend_;
|
||||
#elif defined(USE_ESP32)
|
||||
std::unique_ptr<ota::IDFOTABackend> backend_;
|
||||
#elif defined(USE_RP2040)
|
||||
std::unique_ptr<ota::ArduinoRP2040OTABackend> backend_;
|
||||
#elif defined(USE_LIBRETINY)
|
||||
std::unique_ptr<ota::ArduinoLibreTinyOTABackend> backend_;
|
||||
#elif defined(USE_HOST)
|
||||
std::unique_ptr<ota::HostOTABackend> backend_;
|
||||
#endif
|
||||
ota::OTABackendPtr backend_;
|
||||
|
||||
uint32_t client_connect_time_{0};
|
||||
uint16_t port_;
|
||||
|
||||
@@ -34,7 +34,7 @@ AUTO_LOAD = ["sensor"]
|
||||
CODEOWNERS = ["@coogle", "@ximex"]
|
||||
|
||||
gps_ns = cg.esphome_ns.namespace("gps")
|
||||
GPS = gps_ns.class_("GPS", cg.Component, uart.UARTDevice)
|
||||
GPS = gps_ns.class_("GPS", cg.PollingComponent, uart.UARTDevice)
|
||||
GPSListener = gps_ns.class_("GPSListener")
|
||||
|
||||
MULTI_CONF = True
|
||||
|
||||
@@ -131,7 +131,7 @@ void GroveMotorDriveTB6612FNG::stepper_run(StepperModeTypeT mode, int16_t steps,
|
||||
buffer_[4] = ms_per_step;
|
||||
buffer_[5] = (ms_per_step >> 8);
|
||||
|
||||
if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_RUN, buffer_, 1) != i2c::ERROR_OK) {
|
||||
if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_RUN, buffer_, 6) != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "Run stepper failed!");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
|
||||
@@ -26,7 +26,7 @@ void GrowattSolar::update() {
|
||||
}
|
||||
|
||||
// The bus might be slow, or there might be other devices, or other components might be talking to our device.
|
||||
if (this->waiting_for_response()) {
|
||||
if (!this->ready_for_immediate_send()) {
|
||||
this->waiting_to_update_ = true;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -385,7 +385,7 @@ haier_protocol::HaierMessage Smartair2Climate::get_control_message() {
|
||||
}
|
||||
|
||||
haier_protocol::HandlerError Smartair2Climate::process_status_message_(const uint8_t *packet_buffer, uint8_t size) {
|
||||
if (size < sizeof(smartair2_protocol::HaierStatus))
|
||||
if (size != sizeof(smartair2_protocol::HaierStatus))
|
||||
return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE;
|
||||
smartair2_protocol::HaierStatus packet;
|
||||
memcpy(&packet, packet_buffer, size);
|
||||
|
||||
@@ -65,8 +65,7 @@ void OtaHttpRequestComponent::flash() {
|
||||
}
|
||||
}
|
||||
|
||||
void OtaHttpRequestComponent::cleanup_(decltype(ota::make_ota_backend()) backend,
|
||||
const std::shared_ptr<HttpContainer> &container) {
|
||||
void OtaHttpRequestComponent::cleanup_(ota::OTABackendPtr backend, const std::shared_ptr<HttpContainer> &container) {
|
||||
if (this->update_started_) {
|
||||
ESP_LOGV(TAG, "Aborting OTA backend");
|
||||
backend->abort();
|
||||
|
||||
@@ -39,7 +39,7 @@ class OtaHttpRequestComponent final : public ota::OTAComponent, public Parented<
|
||||
void flash();
|
||||
|
||||
protected:
|
||||
void cleanup_(decltype(ota::make_ota_backend()) backend, const std::shared_ptr<HttpContainer> &container);
|
||||
void cleanup_(ota::OTABackendPtr backend, const std::shared_ptr<HttpContainer> &container);
|
||||
uint8_t do_ota_();
|
||||
std::string get_url_with_auth_(const std::string &url);
|
||||
bool http_get_md5_();
|
||||
|
||||
@@ -136,7 +136,7 @@ void KamstrupKMPComponent::read_command_(uint16_t command) {
|
||||
int timeout = 250; // ms
|
||||
|
||||
// Read the data from the UART
|
||||
while (timeout > 0) {
|
||||
while (timeout > 0 && buffer_len < static_cast<int>(sizeof(buffer))) {
|
||||
if (this->available()) {
|
||||
data = this->read();
|
||||
if (data > -1) {
|
||||
@@ -246,7 +246,7 @@ void KamstrupKMPComponent::parse_command_message_(uint16_t command, const uint8_
|
||||
}
|
||||
|
||||
void KamstrupKMPComponent::set_sensor_value_(uint16_t command, float value, uint8_t unit_idx) {
|
||||
const char *unit = UNITS[unit_idx];
|
||||
const char *unit = unit_idx < sizeof(UNITS) / sizeof(UNITS[0]) ? UNITS[unit_idx] : "";
|
||||
|
||||
// Standard sensors
|
||||
if (command == CMD_HEAT_ENERGY && this->heat_energy_sensor_ != nullptr) {
|
||||
|
||||
@@ -99,7 +99,7 @@ void HOT LCDDisplay::display() {
|
||||
this->send(this->buffer_[this->columns_ * 2 + i], true);
|
||||
}
|
||||
|
||||
if (this->rows_ >= 1) {
|
||||
if (this->rows_ >= 2) {
|
||||
this->command_(LCD_DISPLAY_COMMAND_SET_DDRAM_ADDR | 0x40);
|
||||
|
||||
for (uint8_t i = 0; i < this->columns_; i++)
|
||||
|
||||
@@ -108,6 +108,10 @@ bool LwTx::lwtx_free() { return !this->tx_msg_active; }
|
||||
Send a LightwaveRF message (10 nibbles in bytes)
|
||||
**/
|
||||
void LwTx::lwtx_send(const std::vector<uint8_t> &msg) {
|
||||
if (msg.size() < TX_MSGLEN) {
|
||||
ESP_LOGW("lightwaverf.sensor", "Message too short: %zu < %u", msg.size(), static_cast<unsigned>(TX_MSGLEN));
|
||||
return;
|
||||
}
|
||||
if (this->tx_translate) {
|
||||
for (uint8_t i = 0; i < TX_MSGLEN; i++) {
|
||||
this->tx_buf[i] = TX_NIBBLE[msg[i] & 0xF];
|
||||
|
||||
@@ -20,6 +20,7 @@ MULTI_CONF = True
|
||||
CONF_ROLE = "role"
|
||||
CONF_MODBUS_ID = "modbus_id"
|
||||
CONF_SEND_WAIT_TIME = "send_wait_time"
|
||||
CONF_TURNAROUND_TIME = "turnaround_time"
|
||||
|
||||
ModbusRole = modbus_ns.enum("ModbusRole")
|
||||
MODBUS_ROLES = {
|
||||
@@ -36,6 +37,9 @@ CONFIG_SCHEMA = (
|
||||
cv.Optional(
|
||||
CONF_SEND_WAIT_TIME, default="250ms"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(
|
||||
CONF_TURNAROUND_TIME, default="100ms"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_DISABLE_CRC, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
@@ -57,6 +61,7 @@ async def to_code(config):
|
||||
cg.add(var.set_flow_control_pin(pin))
|
||||
|
||||
cg.add(var.set_send_wait_time(config[CONF_SEND_WAIT_TIME]))
|
||||
cg.add(var.set_turnaround_time(config[CONF_TURNAROUND_TIME]))
|
||||
cg.add(var.set_disable_crc(config[CONF_DISABLE_CRC]))
|
||||
|
||||
|
||||
|
||||
@@ -15,10 +15,69 @@ void Modbus::setup() {
|
||||
if (this->flow_control_pin_ != nullptr) {
|
||||
this->flow_control_pin_->setup();
|
||||
}
|
||||
}
|
||||
void Modbus::loop() {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
|
||||
this->frame_delay_ms_ =
|
||||
std::max(2, // 1750us minimum per spec - rounded up to 2ms.
|
||||
// 3.5 characters * 11 bits per character * 1000ms/sec / (bits/sec) (Standard modbus frame delay)
|
||||
(uint16_t) (3.5 * 11 * 1000 / this->parent_->get_baud_rate()) + 1);
|
||||
|
||||
this->long_rx_buffer_delay_ms_ =
|
||||
(this->parent_->get_rx_full_threshold() * 11 * 1000 / this->parent_->get_baud_rate()) + 1;
|
||||
}
|
||||
|
||||
void Modbus::loop() {
|
||||
// First process all available incoming data.
|
||||
this->receive_and_parse_modbus_bytes_();
|
||||
|
||||
// If the response frame is finished (including interframe delay) - we timeout.
|
||||
// The long_rx_buffer_delay accounts for long responses (larger than the UART rx_full_threshold) to avoid timeouts
|
||||
// when the buffer is filling the back half of the response
|
||||
const uint16_t timeout = std::max(
|
||||
(uint16_t) this->frame_delay_ms_,
|
||||
(uint16_t) (this->rx_buffer_.size() >= this->parent_->get_rx_full_threshold() ? this->long_rx_buffer_delay_ms_
|
||||
: 0));
|
||||
// We use millis() here and elsewhere instead of App.get_loop_component_start_time() to avoid stale timestamps
|
||||
// It's critical in all timestamp comparisons that the left timestamp comes before the right one in time
|
||||
// If we use a cached value in place of millis() and last_modbus_byte_ is updated inside our loop
|
||||
// then the comparison is backwards (small negative which wraps to large positive) and will cause a false timeout
|
||||
// So in this component we don't use any cached timestamp values to avoid these annoying bugs
|
||||
if (millis() - this->last_modbus_byte_ > timeout) {
|
||||
this->clear_rx_buffer_(LOG_STR("timeout after partial response"), true);
|
||||
}
|
||||
|
||||
// If we're past the send_wait_time timeout and response buffer doesn't have the start of the expected response
|
||||
if (this->waiting_for_response_ != 0 &&
|
||||
millis() - this->last_send_ > this->last_send_tx_offset_ + this->send_wait_time_ &&
|
||||
(this->rx_buffer_.empty() || this->rx_buffer_[0] != this->waiting_for_response_)) {
|
||||
ESP_LOGW(TAG, "Stop waiting for response from %" PRIu8 " %" PRIu32 "ms after last send",
|
||||
this->waiting_for_response_, millis() - this->last_send_);
|
||||
this->waiting_for_response_ = 0;
|
||||
}
|
||||
|
||||
// If there's no response pending and there's commands in the buffer
|
||||
this->send_next_frame_();
|
||||
}
|
||||
|
||||
bool Modbus::tx_blocked() {
|
||||
const uint32_t now = millis();
|
||||
|
||||
// We block transmission in any of these case:
|
||||
// 1. There are bytes in the UART Rx buffer
|
||||
// 2. There are bytes in our Rx buffer
|
||||
// 3. We're waiting for a response
|
||||
// 4. The last sent byte isn't more than frame_delay ms ago (i.e. wait to tell receivers that our previous Tx is done)
|
||||
// 5. The last received byte isn't more than frame_delay ms ago (i.e. wait to be sure there isn't more Rx coming)
|
||||
// 6. If we're a client - also wait for the turnaround delay, to give the servers time to process the previous message
|
||||
return this->available() || !this->rx_buffer_.empty() || (this->waiting_for_response_ != 0) ||
|
||||
(now - this->last_send_ < this->last_send_tx_offset_ + this->frame_delay_ms_ +
|
||||
(this->role == ModbusRole::CLIENT ? this->turnaround_delay_ms_ : 0)) ||
|
||||
(now - this->last_modbus_byte_ <
|
||||
this->frame_delay_ms_ + (this->role == ModbusRole::CLIENT ? this->turnaround_delay_ms_ : 0));
|
||||
}
|
||||
|
||||
bool Modbus::tx_buffer_empty() { return this->tx_buffer_.empty(); }
|
||||
|
||||
void Modbus::receive_and_parse_modbus_bytes_() {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
size_t avail = this->available();
|
||||
uint8_t buf[64];
|
||||
@@ -28,33 +87,20 @@ void Modbus::loop() {
|
||||
break;
|
||||
}
|
||||
avail -= to_read;
|
||||
|
||||
for (size_t i = 0; i < to_read; i++) {
|
||||
if (this->parse_modbus_byte_(buf[i])) {
|
||||
this->last_modbus_byte_ = now;
|
||||
if (this->rx_buffer_.empty()) {
|
||||
ESP_LOGV(TAG, "Received first byte %" PRIu8 " (0X%x) %" PRIu32 "ms after last send", buf[i], buf[i],
|
||||
millis() - this->last_send_);
|
||||
} else {
|
||||
size_t at = this->rx_buffer_.size();
|
||||
if (at > 0) {
|
||||
ESP_LOGV(TAG, "Clearing buffer of %d bytes - parse failed", at);
|
||||
this->rx_buffer_.clear();
|
||||
}
|
||||
ESP_LOGVV(TAG, "Received byte %" PRIu8 " (0X%x) %" PRIu32 "ms after last send", buf[i], buf[i],
|
||||
millis() - this->last_send_);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (now - this->last_modbus_byte_ > 50) {
|
||||
size_t at = this->rx_buffer_.size();
|
||||
if (at > 0) {
|
||||
ESP_LOGV(TAG, "Clearing buffer of %d bytes - timeout", at);
|
||||
this->rx_buffer_.clear();
|
||||
}
|
||||
|
||||
// stop blocking new send commands after sent_wait_time_ ms after response received
|
||||
if (now - this->last_send_ > send_wait_time_) {
|
||||
if (waiting_for_response > 0) {
|
||||
ESP_LOGV(TAG, "Stop waiting for response from %d", waiting_for_response);
|
||||
// If the bytes in the rx buffer do not parse, clear out the buffer
|
||||
if (!this->parse_modbus_byte_(buf[i])) {
|
||||
this->clear_rx_buffer_(LOG_STR("parse failed"), true);
|
||||
}
|
||||
waiting_for_response = 0;
|
||||
this->last_modbus_byte_ = millis();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,7 +109,7 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) {
|
||||
size_t at = this->rx_buffer_.size();
|
||||
this->rx_buffer_.push_back(byte);
|
||||
const uint8_t *raw = &this->rx_buffer_[0];
|
||||
ESP_LOGVV(TAG, "Modbus received Byte %d (0X%x)", byte, byte);
|
||||
|
||||
// Byte 0: modbus address (match all)
|
||||
if (at == 0)
|
||||
return true;
|
||||
@@ -101,7 +147,7 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) {
|
||||
if (computed_crc != remote_crc)
|
||||
return true;
|
||||
|
||||
ESP_LOGD(TAG, "Modbus user-defined function %02X found", function_code);
|
||||
ESP_LOGD(TAG, "User-defined function %02X found", function_code);
|
||||
|
||||
} else {
|
||||
// data starts at 2 and length is 4 for read registers commands
|
||||
@@ -152,9 +198,19 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) {
|
||||
uint16_t remote_crc = uint16_t(raw[data_offset + data_len]) | (uint16_t(raw[data_offset + data_len + 1]) << 8);
|
||||
if (computed_crc != remote_crc) {
|
||||
if (this->disable_crc_) {
|
||||
ESP_LOGD(TAG, "Modbus CRC Check failed, but ignored! %02X!=%02X", computed_crc, remote_crc);
|
||||
ESP_LOGD(TAG, "CRC check failed %" PRIu32 "ms after last send; ignoring", millis() - this->last_send_);
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)];
|
||||
#endif
|
||||
ESP_LOGVV(TAG, " (%02X != %02X) %s", computed_crc, remote_crc,
|
||||
format_hex_pretty_to(hex_buf, this->rx_buffer_.data(), this->rx_buffer_.size()));
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Modbus CRC Check failed! %02X!=%02X", computed_crc, remote_crc);
|
||||
ESP_LOGW(TAG, "CRC check failed %" PRIu32 "ms after last send", millis() - this->last_send_);
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)];
|
||||
#endif
|
||||
ESP_LOGVV(TAG, " (%02X != %02X) %s", computed_crc, remote_crc,
|
||||
format_hex_pretty_to(hex_buf, this->rx_buffer_.data(), this->rx_buffer_.size()));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -164,52 +220,101 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) {
|
||||
for (auto *device : this->devices_) {
|
||||
if (device->address_ == address) {
|
||||
found = true;
|
||||
// Is it an error response?
|
||||
if ((function_code & FUNCTION_CODE_EXCEPTION_MASK) == FUNCTION_CODE_EXCEPTION_MASK) {
|
||||
ESP_LOGD(TAG, "Modbus error function code: 0x%X exception: %d", function_code, raw[2]);
|
||||
if (waiting_for_response != 0) {
|
||||
device->on_modbus_error(function_code & FUNCTION_CODE_MASK, raw[2]);
|
||||
} else {
|
||||
// Ignore modbus exception not related to a pending command
|
||||
ESP_LOGD(TAG, "Ignoring Modbus error - not expecting a response");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (this->role == ModbusRole::SERVER) {
|
||||
if (function_code == ModbusFunctionCode::READ_HOLDING_REGISTERS ||
|
||||
function_code == ModbusFunctionCode::READ_INPUT_REGISTERS) {
|
||||
device->on_modbus_read_registers(function_code, uint16_t(data[1]) | (uint16_t(data[0]) << 8),
|
||||
uint16_t(data[3]) | (uint16_t(data[2]) << 8));
|
||||
continue;
|
||||
}
|
||||
if (function_code == ModbusFunctionCode::WRITE_SINGLE_REGISTER ||
|
||||
function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) {
|
||||
} else if (function_code == ModbusFunctionCode::WRITE_SINGLE_REGISTER ||
|
||||
function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) {
|
||||
device->on_modbus_write_registers(function_code, data);
|
||||
continue;
|
||||
}
|
||||
} else { // We're a client
|
||||
// Is it an error response?
|
||||
if ((function_code & FUNCTION_CODE_EXCEPTION_MASK) == FUNCTION_CODE_EXCEPTION_MASK) {
|
||||
uint8_t exception = raw[2];
|
||||
ESP_LOGW(TAG,
|
||||
"Error function code: 0x%X exception: %" PRIu8 ", address: %" PRIu8 ", %" PRIu32
|
||||
"ms after last send",
|
||||
function_code, exception, address, millis() - this->last_send_);
|
||||
if (this->waiting_for_response_ == address) {
|
||||
device->on_modbus_error(function_code & FUNCTION_CODE_MASK, exception);
|
||||
} else {
|
||||
// Ignore modbus exception not related to a pending command
|
||||
ESP_LOGD(TAG, "Ignoring error - not expecting a response from %" PRIu8 "", address);
|
||||
}
|
||||
} else { // Not an error response
|
||||
if (this->waiting_for_response_ == address) {
|
||||
device->on_modbus_data(data);
|
||||
} else {
|
||||
// Ignore modbus response not related to a pending command
|
||||
ESP_LOGW(TAG, "Ignoring response - not expecting a response from %" PRIu8 ", %" PRIu32 "ms after last send",
|
||||
address, millis() - this->last_send_);
|
||||
}
|
||||
}
|
||||
}
|
||||
// fallthrough for other function codes
|
||||
device->on_modbus_data(data);
|
||||
}
|
||||
}
|
||||
waiting_for_response = 0;
|
||||
|
||||
if (!found) {
|
||||
ESP_LOGW(TAG, "Got Modbus frame from unknown address 0x%02X! ", address);
|
||||
if (!found && this->role == ModbusRole::CLIENT) {
|
||||
ESP_LOGW(TAG, "Got frame from unknown address %" PRIu8 ", %" PRIu32 "ms after last send", address,
|
||||
millis() - this->last_send_);
|
||||
}
|
||||
|
||||
// reset buffer
|
||||
ESP_LOGV(TAG, "Clearing buffer of %d bytes - parse succeeded", at);
|
||||
this->rx_buffer_.clear();
|
||||
this->clear_rx_buffer_(LOG_STR("parse succeeded"));
|
||||
|
||||
if (this->waiting_for_response_ == address)
|
||||
this->waiting_for_response_ = 0;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Modbus::send_next_frame_() {
|
||||
if (this->tx_buffer_.empty())
|
||||
return;
|
||||
|
||||
if (this->tx_blocked())
|
||||
return;
|
||||
|
||||
const ModbusDeviceCommand &frame = this->tx_buffer_.front();
|
||||
|
||||
if (this->role == ModbusRole::CLIENT) {
|
||||
this->waiting_for_response_ = frame.data.get()[0];
|
||||
}
|
||||
|
||||
if (this->flow_control_pin_ != nullptr) {
|
||||
this->flow_control_pin_->digital_write(true);
|
||||
this->write_array(frame.data.get(), frame.size);
|
||||
this->flush();
|
||||
this->flow_control_pin_->digital_write(false);
|
||||
this->last_send_tx_offset_ = 0;
|
||||
} else {
|
||||
this->write_array(frame.data.get(), frame.size);
|
||||
this->last_send_tx_offset_ = frame.size * 11 * 1000 / this->parent_->get_baud_rate() + 1;
|
||||
}
|
||||
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)];
|
||||
#endif
|
||||
ESP_LOGV(TAG, "Write: %s %" PRIu32 "ms after last send", format_hex_pretty_to(hex_buf, frame.data.get(), frame.size),
|
||||
millis() - this->last_send_);
|
||||
this->last_send_ = millis();
|
||||
this->tx_buffer_.pop_front();
|
||||
if (!this->tx_buffer_.empty()) {
|
||||
ESP_LOGV(TAG, "Write queue contains %" PRIu32 " items.", this->tx_buffer_.size());
|
||||
}
|
||||
}
|
||||
|
||||
void Modbus::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Modbus:\n"
|
||||
" Send Wait Time: %d ms\n"
|
||||
" Turnaround Time: %d ms\n"
|
||||
" Frame Delay: %d ms\n"
|
||||
" Long Rx Buffer Delay: %d ms\n"
|
||||
" CRC Disabled: %s",
|
||||
this->send_wait_time_, YESNO(this->disable_crc_));
|
||||
this->send_wait_time_, this->turnaround_delay_ms_, this->frame_delay_ms_,
|
||||
this->long_rx_buffer_delay_ms_, YESNO(this->disable_crc_));
|
||||
LOG_PIN(" Flow Control Pin: ", this->flow_control_pin_);
|
||||
}
|
||||
float Modbus::get_setup_priority() const {
|
||||
@@ -228,15 +333,6 @@ void Modbus::send(uint8_t address, uint8_t function_code, uint16_t start_address
|
||||
return;
|
||||
}
|
||||
|
||||
static constexpr size_t ADDR_SIZE = 1;
|
||||
static constexpr size_t FC_SIZE = 1;
|
||||
static constexpr size_t START_ADDR_SIZE = 2;
|
||||
static constexpr size_t NUM_ENTITIES_SIZE = 2;
|
||||
static constexpr size_t BYTE_COUNT_SIZE = 1;
|
||||
static constexpr size_t MAX_PAYLOAD_SIZE = std::numeric_limits<uint8_t>::max();
|
||||
static constexpr size_t CRC_SIZE = 2;
|
||||
static constexpr size_t MAX_FRAME_SIZE =
|
||||
ADDR_SIZE + FC_SIZE + START_ADDR_SIZE + NUM_ENTITIES_SIZE + BYTE_COUNT_SIZE + MAX_PAYLOAD_SIZE + CRC_SIZE;
|
||||
uint8_t data[MAX_FRAME_SIZE];
|
||||
size_t pos = 0;
|
||||
|
||||
@@ -259,29 +355,16 @@ void Modbus::send(uint8_t address, uint8_t function_code, uint16_t start_address
|
||||
} else {
|
||||
payload_len = 2; // Write single register or coil
|
||||
}
|
||||
if (payload_len + pos + 2 > MAX_FRAME_SIZE) { // Check if payload fits (accounting for CRC)
|
||||
ESP_LOGE(TAG, "Payload too large to send: %d bytes", payload_len);
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < payload_len; i++) {
|
||||
data[pos++] = payload[i];
|
||||
}
|
||||
}
|
||||
|
||||
auto crc = crc16(data, pos);
|
||||
data[pos++] = crc >> 0;
|
||||
data[pos++] = crc >> 8;
|
||||
|
||||
if (this->flow_control_pin_ != nullptr)
|
||||
this->flow_control_pin_->digital_write(true);
|
||||
|
||||
this->write_array(data, pos);
|
||||
this->flush();
|
||||
|
||||
if (this->flow_control_pin_ != nullptr)
|
||||
this->flow_control_pin_->digital_write(false);
|
||||
waiting_for_response = address;
|
||||
last_send_ = millis();
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)];
|
||||
#endif
|
||||
ESP_LOGV(TAG, "Modbus write: %s", format_hex_pretty_to(hex_buf, data, pos));
|
||||
this->queue_raw_(data, pos);
|
||||
}
|
||||
|
||||
// Helper function for lambdas
|
||||
@@ -290,23 +373,44 @@ void Modbus::send_raw(const std::vector<uint8_t> &payload) {
|
||||
if (payload.empty()) {
|
||||
return;
|
||||
}
|
||||
// Frame size: payload + CRC(2)
|
||||
if (payload.size() + 2 > MAX_FRAME_SIZE) {
|
||||
ESP_LOGE(TAG, "Attempted to send frame larger than max frame size of %d bytes", MAX_FRAME_SIZE);
|
||||
return;
|
||||
}
|
||||
// Use stack buffer - Modbus frames are small and bounded
|
||||
uint8_t data[MAX_FRAME_SIZE];
|
||||
|
||||
if (this->flow_control_pin_ != nullptr)
|
||||
this->flow_control_pin_->digital_write(true);
|
||||
std::memcpy(data, payload.data(), payload.size());
|
||||
|
||||
auto crc = crc16(payload.data(), payload.size());
|
||||
this->write_array(payload);
|
||||
this->write_byte(crc & 0xFF);
|
||||
this->write_byte((crc >> 8) & 0xFF);
|
||||
this->flush();
|
||||
if (this->flow_control_pin_ != nullptr)
|
||||
this->flow_control_pin_->digital_write(false);
|
||||
waiting_for_response = payload[0];
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)];
|
||||
this->queue_raw_(data, payload.size());
|
||||
}
|
||||
|
||||
// Assume data and length is valid and append CRC, then queue for sending. Used internally to avoid unnecessary copying
|
||||
// of data into vectors
|
||||
void Modbus::queue_raw_(const uint8_t *data, uint16_t len) {
|
||||
if (this->tx_buffer_.size() < MODBUS_TX_BUFFER_SIZE) {
|
||||
this->tx_buffer_.emplace_back(data, len);
|
||||
} else {
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_ERROR
|
||||
char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)];
|
||||
#endif
|
||||
ESP_LOGV(TAG, "Modbus write raw: %s", format_hex_pretty_to(hex_buf, payload.data(), payload.size()));
|
||||
last_send_ = millis();
|
||||
ESP_LOGE(TAG, "Write buffer full, dropped: %s", format_hex_pretty_to(hex_buf, data, len));
|
||||
}
|
||||
}
|
||||
|
||||
void Modbus::clear_rx_buffer_(const LogString *reason, bool warn) {
|
||||
size_t at = this->rx_buffer_.size();
|
||||
if (at > 0) {
|
||||
if (warn) {
|
||||
ESP_LOGW(TAG, "Clearing buffer of %" PRIu32 " bytes - %s %" PRIu32 "ms after last send", at, LOG_STR_ARG(reason),
|
||||
millis() - this->last_send_);
|
||||
} else {
|
||||
ESP_LOGV(TAG, "Clearing buffer of %" PRIu32 " bytes - %s %" PRIu32 "ms after last send", at, LOG_STR_ARG(reason),
|
||||
millis() - this->last_send_);
|
||||
}
|
||||
this->rx_buffer_.clear();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace modbus
|
||||
|
||||
@@ -5,11 +5,16 @@
|
||||
|
||||
#include "esphome/components/modbus/modbus_definitions.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <queue>
|
||||
|
||||
namespace esphome {
|
||||
namespace modbus {
|
||||
|
||||
static constexpr uint16_t MODBUS_TX_BUFFER_SIZE = 15;
|
||||
|
||||
enum ModbusRole {
|
||||
CLIENT,
|
||||
SERVER,
|
||||
@@ -17,6 +22,19 @@ enum ModbusRole {
|
||||
|
||||
class ModbusDevice;
|
||||
|
||||
struct ModbusDeviceCommand {
|
||||
// Frame with exact-size allocation to avoid std::vector overhead
|
||||
std::unique_ptr<uint8_t[]> data;
|
||||
uint16_t size; // Modbus RTU max is 256 bytes
|
||||
|
||||
ModbusDeviceCommand(const uint8_t *src, uint16_t len) : data(std::make_unique<uint8_t[]>(len + 2)), size(len + 2) {
|
||||
std::memcpy(this->data.get(), src, len);
|
||||
auto crc = crc16(data.get(), len);
|
||||
data[len + 0] = crc >> 0;
|
||||
data[len + 1] = crc >> 8;
|
||||
}
|
||||
};
|
||||
|
||||
class Modbus : public uart::UARTDevice, public Component {
|
||||
public:
|
||||
Modbus() = default;
|
||||
@@ -30,28 +48,45 @@ class Modbus : public uart::UARTDevice, public Component {
|
||||
void register_device(ModbusDevice *device) { this->devices_.push_back(device); }
|
||||
|
||||
float get_setup_priority() const override;
|
||||
bool tx_buffer_empty();
|
||||
bool tx_blocked();
|
||||
|
||||
void send(uint8_t address, uint8_t function_code, uint16_t start_address, uint16_t number_of_entities,
|
||||
uint8_t payload_len = 0, const uint8_t *payload = nullptr);
|
||||
void send_raw(const std::vector<uint8_t> &payload);
|
||||
void set_role(ModbusRole role) { this->role = role; }
|
||||
void set_flow_control_pin(GPIOPin *flow_control_pin) { this->flow_control_pin_ = flow_control_pin; }
|
||||
uint8_t waiting_for_response{0};
|
||||
void set_send_wait_time(uint16_t time_in_ms) { send_wait_time_ = time_in_ms; }
|
||||
void set_disable_crc(bool disable_crc) { disable_crc_ = disable_crc; }
|
||||
void set_send_wait_time(uint16_t time_in_ms) { this->send_wait_time_ = time_in_ms; }
|
||||
void set_turnaround_time(uint16_t time_in_ms) { this->turnaround_delay_ms_ = time_in_ms; }
|
||||
void set_disable_crc(bool disable_crc) { this->disable_crc_ = disable_crc; }
|
||||
|
||||
ModbusRole role;
|
||||
|
||||
protected:
|
||||
GPIOPin *flow_control_pin_{nullptr};
|
||||
|
||||
bool parse_modbus_byte_(uint8_t byte);
|
||||
uint16_t send_wait_time_{250};
|
||||
bool disable_crc_;
|
||||
std::vector<uint8_t> rx_buffer_;
|
||||
void receive_and_parse_modbus_bytes_();
|
||||
void clear_rx_buffer_(const LogString *reason, bool warn = false);
|
||||
void send_next_frame_();
|
||||
void queue_raw_(const uint8_t *data, uint16_t len);
|
||||
|
||||
uint32_t last_modbus_byte_{0};
|
||||
uint32_t last_send_{0};
|
||||
uint32_t last_send_tx_offset_{0};
|
||||
uint16_t frame_delay_ms_{5};
|
||||
uint16_t long_rx_buffer_delay_ms_{0};
|
||||
uint16_t send_wait_time_{250};
|
||||
uint16_t turnaround_delay_ms_{100};
|
||||
uint8_t waiting_for_response_{0};
|
||||
bool disable_crc_{false};
|
||||
|
||||
GPIOPin *flow_control_pin_{nullptr};
|
||||
|
||||
std::vector<uint8_t> rx_buffer_;
|
||||
std::vector<ModbusDevice *> devices_;
|
||||
// std::deque is appropriate here since we need a FIFO buffer, and we can't know ahead of time how many
|
||||
// requests will be queued. Each modbus component may queue multiple requests, and the sequence of scheduling
|
||||
// may change at run time.
|
||||
std::deque<ModbusDeviceCommand> tx_buffer_;
|
||||
};
|
||||
|
||||
class ModbusDevice {
|
||||
@@ -76,7 +111,9 @@ class ModbusDevice {
|
||||
this->send_raw(error_response);
|
||||
}
|
||||
// If more than one device is connected block sending a new command before a response is received
|
||||
bool waiting_for_response() { return parent_->waiting_for_response != 0; }
|
||||
ESPDEPRECATED("Use ready_for_immediate_send() instead. Removed in 2026.9.0", "2026.3.0")
|
||||
bool waiting_for_response() { return !ready_for_immediate_send(); }
|
||||
bool ready_for_immediate_send() { return parent_->tx_buffer_empty() && !parent_->tx_blocked(); }
|
||||
|
||||
protected:
|
||||
friend Modbus;
|
||||
|
||||
@@ -81,6 +81,8 @@ const uint8_t MAX_NUM_OF_REGISTERS_TO_WRITE = 123; // 0x7B
|
||||
// 6.3 03 (0x03) Read Holding Registers
|
||||
// 6.4 04 (0x04) Read Input Registers
|
||||
const uint8_t MAX_NUM_OF_REGISTERS_TO_READ = 125; // 0x7D
|
||||
|
||||
static constexpr uint16_t MAX_FRAME_SIZE = 256;
|
||||
/// End of Modbus definitions
|
||||
} // namespace modbus
|
||||
} // namespace esphome
|
||||
|
||||
@@ -48,6 +48,7 @@ CONF_SERVER_REGISTERS = "server_registers"
|
||||
MULTI_CONF = True
|
||||
|
||||
modbus_controller_ns = cg.esphome_ns.namespace("modbus_controller")
|
||||
modbus_ns = cg.esphome_ns.namespace("modbus")
|
||||
ModbusController = modbus_controller_ns.class_(
|
||||
"ModbusController", cg.PollingComponent, modbus.ModbusDevice
|
||||
)
|
||||
@@ -56,7 +57,7 @@ SensorItem = modbus_controller_ns.struct("SensorItem")
|
||||
ServerCourtesyResponse = modbus_controller_ns.struct("ServerCourtesyResponse")
|
||||
ServerRegister = modbus_controller_ns.struct("ServerRegister")
|
||||
|
||||
ModbusFunctionCode_ns = modbus_controller_ns.namespace("ModbusFunctionCode")
|
||||
ModbusFunctionCode_ns = modbus_ns.namespace("ModbusFunctionCode")
|
||||
ModbusFunctionCode = ModbusFunctionCode_ns.enum("ModbusFunctionCode")
|
||||
MODBUS_FUNCTION_CODE = {
|
||||
"read_coils": ModbusFunctionCode.READ_COILS,
|
||||
|
||||
@@ -18,7 +18,7 @@ void ModbusController::setup() { this->create_register_ranges_(); }
|
||||
bool ModbusController::send_next_command_() {
|
||||
uint32_t last_send = millis() - this->last_command_timestamp_;
|
||||
|
||||
if ((last_send > this->command_throttle_) && !waiting_for_response() && !this->command_queue_.empty()) {
|
||||
if ((last_send > this->command_throttle_) && this->ready_for_immediate_send() && !this->command_queue_.empty()) {
|
||||
auto &command = this->command_queue_.front();
|
||||
|
||||
// remove from queue if command was sent too often
|
||||
|
||||
@@ -108,7 +108,7 @@ bool MopekaStdCheck::parse_device(const esp32_ble_tracker::ESPBTDevice &device)
|
||||
}
|
||||
|
||||
// Get temperature of sensor
|
||||
uint8_t temp_in_c = this->parse_temperature_(mopeka_data);
|
||||
int8_t temp_in_c = this->parse_temperature_(mopeka_data);
|
||||
if (this->temperature_ != nullptr) {
|
||||
this->temperature_->publish_state(temp_in_c);
|
||||
}
|
||||
@@ -223,12 +223,12 @@ uint8_t MopekaStdCheck::parse_battery_level_(const mopeka_std_package *message)
|
||||
return (uint8_t) percent;
|
||||
}
|
||||
|
||||
uint8_t MopekaStdCheck::parse_temperature_(const mopeka_std_package *message) {
|
||||
int8_t MopekaStdCheck::parse_temperature_(const mopeka_std_package *message) {
|
||||
uint8_t tmp = message->raw_temp;
|
||||
if (tmp == 0x0) {
|
||||
return -40;
|
||||
} else {
|
||||
return (uint8_t) ((tmp - 25.0f) * 1.776964f);
|
||||
return static_cast<int8_t>((tmp - 25.0f) * 1.776964f);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ class MopekaStdCheck : public Component, public esp32_ble_tracker::ESPBTDeviceLi
|
||||
|
||||
float get_lpg_speed_of_sound_(float temperature);
|
||||
uint8_t parse_battery_level_(const mopeka_std_package *message);
|
||||
uint8_t parse_temperature_(const mopeka_std_package *message);
|
||||
int8_t parse_temperature_(const mopeka_std_package *message);
|
||||
};
|
||||
|
||||
} // namespace mopeka_std_check
|
||||
|
||||
@@ -80,7 +80,7 @@ void MPU6886Component::setup() {
|
||||
accel_config &= 0b11100111;
|
||||
accel_config |= (MPU6886_RANGE_2G << 3);
|
||||
ESP_LOGV(TAG, " Output accel_config: 0b" BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(accel_config));
|
||||
if (!this->write_byte(MPU6886_REGISTER_GYRO_CONFIG, gyro_config)) {
|
||||
if (!this->write_byte(MPU6886_REGISTER_ACCEL_CONFIG, accel_config)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -8,8 +8,14 @@ static const char *const TAG = "nfc.ndef_message";
|
||||
|
||||
NdefMessage::NdefMessage(std::vector<uint8_t> &data) {
|
||||
ESP_LOGV(TAG, "Building NdefMessage with %zu bytes", data.size());
|
||||
uint8_t index = 0;
|
||||
while (index <= data.size()) {
|
||||
size_t index = 0;
|
||||
while (index < data.size()) {
|
||||
// Minimum record: TNF byte + type length byte + payload length (1 or 4 bytes)
|
||||
if (index + 2 >= data.size()) {
|
||||
ESP_LOGE(TAG, "Truncated record header; aborting");
|
||||
break;
|
||||
}
|
||||
|
||||
uint8_t tnf_byte = data[index++];
|
||||
bool me = tnf_byte & 0x40; // Message End bit (is set if this is the last record of the message)
|
||||
bool sr = tnf_byte & 0x10; // Short record bit (is set if payload size is less or equal to 255 bytes)
|
||||
@@ -23,6 +29,10 @@ NdefMessage::NdefMessage(std::vector<uint8_t> &data) {
|
||||
if (sr) {
|
||||
payload_length = data[index++];
|
||||
} else {
|
||||
if (index + 4 > data.size()) {
|
||||
ESP_LOGE(TAG, "Truncated payload length; aborting");
|
||||
break;
|
||||
}
|
||||
payload_length = (static_cast<uint32_t>(data[index]) << 24) | (static_cast<uint32_t>(data[index + 1]) << 16) |
|
||||
(static_cast<uint32_t>(data[index + 2]) << 8) | static_cast<uint32_t>(data[index + 3]);
|
||||
index += 4;
|
||||
@@ -30,6 +40,10 @@ NdefMessage::NdefMessage(std::vector<uint8_t> &data) {
|
||||
|
||||
uint8_t id_length = 0;
|
||||
if (il) {
|
||||
if (index >= data.size()) {
|
||||
ESP_LOGE(TAG, "Truncated ID length; aborting");
|
||||
break;
|
||||
}
|
||||
id_length = data[index++];
|
||||
}
|
||||
|
||||
|
||||
@@ -14,9 +14,12 @@ import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_CHANNEL,
|
||||
CONF_ENABLE_IPV6,
|
||||
CONF_FRAMEWORK,
|
||||
CONF_ID,
|
||||
CONF_LOG_LEVEL,
|
||||
CONF_OUTPUT_POWER,
|
||||
CONF_USE_ADDRESS,
|
||||
PLATFORM_ESP32,
|
||||
)
|
||||
from esphome.core import CORE, TimePeriodMilliseconds
|
||||
import esphome.final_validate as fv
|
||||
@@ -46,6 +49,15 @@ AUTO_LOAD = ["network"]
|
||||
CONFLICTS_WITH = ["wifi"]
|
||||
DEPENDENCIES = ["esp32"]
|
||||
|
||||
IDF_TO_OT_LOG_LEVEL = {
|
||||
"NONE": "NONE",
|
||||
"ERROR": "CRIT",
|
||||
"WARN": "WARN",
|
||||
"INFO": "NOTE",
|
||||
"DEBUG": "INFO",
|
||||
"VERBOSE": "DEBG",
|
||||
}
|
||||
|
||||
CONF_DEVICE_TYPES = [
|
||||
"FTD",
|
||||
"MTD",
|
||||
@@ -198,6 +210,15 @@ def _final_validate(_):
|
||||
"Please set `enable_ipv6: true` in the `network` configuration."
|
||||
)
|
||||
|
||||
if (
|
||||
(esp32_config := full_config.get(PLATFORM_ESP32)) is not None
|
||||
and (fw_config := esp32_config.get(CONF_FRAMEWORK)) is not None
|
||||
and (log_level := fw_config.get(CONF_LOG_LEVEL)) is not None
|
||||
):
|
||||
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_LOG_LEVEL_DYNAMIC", False)
|
||||
ot_log_level = IDF_TO_OT_LOG_LEVEL.get(log_level, log_level)
|
||||
add_idf_sdkconfig_option(f"CONFIG_OPENTHREAD_LOG_LEVEL_{ot_log_level}", True)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
@@ -21,3 +21,7 @@ struct StubOTABackend {};
|
||||
std::unique_ptr<StubOTABackend> make_ota_backend();
|
||||
} // namespace esphome::ota
|
||||
#endif
|
||||
|
||||
namespace esphome::ota {
|
||||
using OTABackendPtr = decltype(make_ota_backend());
|
||||
} // namespace esphome::ota
|
||||
|
||||
@@ -88,9 +88,10 @@ bool PN532Spi::read_response(uint8_t command, std::vector<uint8_t> &data) {
|
||||
#endif
|
||||
ESP_LOGV(TAG, "Header data: %s", format_hex_pretty_to(hex_buf, sizeof(hex_buf), header.data(), header.size()));
|
||||
|
||||
if (header[0] != 0x00 && header[1] != 0x00 && header[2] != 0xFF) {
|
||||
if (header[0] != 0x00 || header[1] != 0x00 || header[2] != 0xFF) {
|
||||
// invalid packet
|
||||
ESP_LOGV(TAG, "read data invalid preamble!");
|
||||
this->disable();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -100,15 +101,20 @@ bool PN532Spi::read_response(uint8_t command, std::vector<uint8_t> &data) {
|
||||
|
||||
if (!valid_header) {
|
||||
ESP_LOGV(TAG, "read data invalid header!");
|
||||
this->disable();
|
||||
return false;
|
||||
}
|
||||
|
||||
// full length of message, including command response
|
||||
// full length of message, including command response (minimum 2: TFI + command response)
|
||||
uint8_t full_len = header[3];
|
||||
if (full_len < 2) {
|
||||
ESP_LOGV(TAG, "read data has no payload");
|
||||
this->disable();
|
||||
return false;
|
||||
}
|
||||
|
||||
// length of data, excluding command response
|
||||
uint8_t len = full_len - 1;
|
||||
if (full_len == 0)
|
||||
len = 0;
|
||||
|
||||
ESP_LOGV(TAG, "Reading response of length %d", len);
|
||||
|
||||
|
||||
@@ -175,7 +175,8 @@ void PulseCounterSensor::setup() {
|
||||
|
||||
void PulseCounterSensor::set_total_pulses(uint32_t pulses) {
|
||||
this->current_total_ = pulses;
|
||||
this->total_sensor_->publish_state(pulses);
|
||||
if (this->total_sensor_ != nullptr)
|
||||
this->total_sensor_->publish_state(pulses);
|
||||
}
|
||||
|
||||
void PulseCounterSensor::dump_config() {
|
||||
|
||||
@@ -61,6 +61,10 @@ optional<ParseResult> PVVXMiThermometer::parse_header_(const esp32_ble_tracker::
|
||||
}
|
||||
|
||||
auto raw = service_data.data;
|
||||
if (raw.size() < 14) {
|
||||
ESP_LOGVV(TAG, "parse_header_(): service data too short (%zu).", raw.size());
|
||||
return {};
|
||||
}
|
||||
|
||||
static uint8_t last_frame_count = 0;
|
||||
if (last_frame_count == raw[13]) {
|
||||
|
||||
@@ -251,7 +251,7 @@ void QMP6988Component::set_power_mode_(uint8_t power_mode) {
|
||||
void QMP6988Component::write_filter_(QMP6988IIRFilter filter) {
|
||||
uint8_t data;
|
||||
|
||||
data = (filter & 0x03);
|
||||
data = (filter & QMP6988_CONFIG_REG_FILTER_MSK);
|
||||
this->write_byte(QMP6988_CONFIG_REG, data);
|
||||
delay(10);
|
||||
}
|
||||
|
||||
@@ -169,6 +169,7 @@ void RC522::loop() {
|
||||
default:
|
||||
ESP_LOGE(TAG, "uid_idx_ invalid, uid_idx_ = %d", uid_idx_);
|
||||
state_ = STATE_DONE;
|
||||
return;
|
||||
}
|
||||
buffer_[1] = 32;
|
||||
pcd_transceive_data_(2);
|
||||
|
||||
@@ -63,10 +63,13 @@ bool parse_ruuvi_data_byte(const esp32_ble_tracker::adv_data_t &adv_data, RuuviP
|
||||
result.acceleration_x = data[6] == 0xFF && data[7] == 0xFF ? NAN : acceleration_x;
|
||||
result.acceleration_y = data[8] == 0xFF && data[9] == 0xFF ? NAN : acceleration_y;
|
||||
result.acceleration_z = data[10] == 0xFF && data[11] == 0xFF ? NAN : acceleration_z;
|
||||
result.acceleration = result.acceleration_x == NAN || result.acceleration_y == NAN || result.acceleration_z == NAN
|
||||
? NAN
|
||||
: sqrtf(acceleration_x * acceleration_x + acceleration_y * acceleration_y +
|
||||
acceleration_z * acceleration_z);
|
||||
if ((data[6] != 0xFF || data[7] != 0xFF) && (data[8] != 0xFF || data[9] != 0xFF) &&
|
||||
(data[10] != 0xFF || data[11] != 0xFF)) {
|
||||
result.acceleration =
|
||||
sqrtf(acceleration_x * acceleration_x + acceleration_y * acceleration_y + acceleration_z * acceleration_z);
|
||||
} else {
|
||||
result.acceleration = NAN;
|
||||
}
|
||||
result.battery_voltage = (power_info >> 5) == 0x7FF ? NAN : battery_voltage;
|
||||
result.tx_power = (power_info & 0x1F) == 0x1F ? NAN : tx_power;
|
||||
result.movement_counter = movement_counter;
|
||||
|
||||
@@ -307,7 +307,7 @@ bool SCD4XComponent::start_measurement_() {
|
||||
break;
|
||||
}
|
||||
|
||||
static uint8_t remaining_retries = 3;
|
||||
uint8_t remaining_retries = 3;
|
||||
while (remaining_retries) {
|
||||
if (!this->write_command(measurement_command)) {
|
||||
ESP_LOGE(TAG, "Error starting measurements");
|
||||
@@ -316,6 +316,7 @@ bool SCD4XComponent::start_measurement_() {
|
||||
if (--remaining_retries == 0)
|
||||
return false;
|
||||
delay(50); // NOLINT wait 50 ms and try again
|
||||
continue;
|
||||
}
|
||||
this->status_clear_warning();
|
||||
return true;
|
||||
|
||||
@@ -10,7 +10,7 @@ static const uint8_t MEASURECOMMANDS[] = {0xFD, 0xF6, 0xE0};
|
||||
static const uint8_t SERIAL_NUMBER_COMMAND = 0x89;
|
||||
|
||||
void SHT4XComponent::start_heater_() {
|
||||
uint8_t cmd[] = {MEASURECOMMANDS[this->heater_command_]};
|
||||
uint8_t cmd[] = {this->heater_command_};
|
||||
|
||||
ESP_LOGD(TAG, "Heater turning on");
|
||||
if (this->write(cmd, 1) != i2c::ERROR_OK) {
|
||||
|
||||
@@ -196,7 +196,8 @@ void Sim800LComponent::parse_cmd_(std::string message) {
|
||||
case STATE_CREG_WAIT: {
|
||||
// Response: "+CREG: 0,1" -- the one there means registered ok
|
||||
// "+CREG: -,-" means not registered ok
|
||||
bool registered = message.compare(0, 6, "+CREG:") == 0 && (message[9] == '1' || message[9] == '5');
|
||||
bool registered =
|
||||
message.size() > 9 && message.compare(0, 6, "+CREG:") == 0 && (message[9] == '1' || message[9] == '5');
|
||||
if (registered) {
|
||||
if (!this->registered_) {
|
||||
ESP_LOGD(TAG, "Registered OK");
|
||||
@@ -205,7 +206,7 @@ void Sim800LComponent::parse_cmd_(std::string message) {
|
||||
this->expect_ack_ = true;
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Registration Fail");
|
||||
if (message[7] == '0') { // Network registration is disable, enable it
|
||||
if (message.size() > 7 && message[7] == '0') { // Network registration is disabled, enable it
|
||||
send_cmd_("AT+CREG=1");
|
||||
this->expect_ack_ = true;
|
||||
this->state_ = STATE_SETUP_CMGF;
|
||||
|
||||
@@ -35,6 +35,8 @@ bool SmlFile::setup_node(SmlNode *node) {
|
||||
|
||||
// Check if we need additional length bytes
|
||||
if (overlength) {
|
||||
if (this->pos_ + 1 >= this->buffer_.size())
|
||||
return false;
|
||||
// Shift the current length to the higher nibble
|
||||
// and add the lower nibble of the next byte to the length
|
||||
length = (length << 4) + (this->buffer_[this->pos_ + 1] & 0x0f);
|
||||
|
||||
@@ -169,7 +169,7 @@ void HOT SSD1322::draw_absolute_pixel_internal(int x, int y, Color color) {
|
||||
// ensure 'color4' is valid (only 4 bits aka 1 nibble) and shift the bits left when necessary
|
||||
color4 = (color4 & SSD1322_COLORMASK) << shift;
|
||||
// first mask off the nibble we must change...
|
||||
this->buffer_[pos] &= (~SSD1322_COLORMASK >> shift);
|
||||
this->buffer_[pos] &= (static_cast<uint8_t>(~SSD1322_COLORMASK) >> shift);
|
||||
// ...then lay the new nibble back on top. done!
|
||||
this->buffer_[pos] |= color4;
|
||||
}
|
||||
|
||||
@@ -202,7 +202,7 @@ void HOT SSD1325::draw_absolute_pixel_internal(int x, int y, Color color) {
|
||||
// ensure 'color4' is valid (only 4 bits aka 1 nibble) and shift the bits left when necessary
|
||||
color4 = (color4 & SSD1325_COLORMASK) << shift;
|
||||
// first mask off the nibble we must change...
|
||||
this->buffer_[pos] &= (~SSD1325_COLORMASK >> shift);
|
||||
this->buffer_[pos] &= (static_cast<uint8_t>(~SSD1325_COLORMASK) >> shift);
|
||||
// ...then lay the new nibble back on top. done!
|
||||
this->buffer_[pos] |= color4;
|
||||
}
|
||||
|
||||
@@ -145,7 +145,7 @@ void HOT SSD1327::draw_absolute_pixel_internal(int x, int y, Color color) {
|
||||
// ensure 'color4' is valid (only 4 bits aka 1 nibble) and shift the bits left when necessary
|
||||
color4 = (color4 & SSD1327_COLORMASK) << shift;
|
||||
// first mask off the nibble we must change...
|
||||
this->buffer_[pos] &= (~SSD1327_COLORMASK >> shift);
|
||||
this->buffer_[pos] &= (static_cast<uint8_t>(~SSD1327_COLORMASK) >> shift);
|
||||
// ...then lay the new nibble back on top. done!
|
||||
this->buffer_[pos] |= color4;
|
||||
}
|
||||
|
||||
@@ -56,11 +56,11 @@ void SX1509Component::loop() {
|
||||
return;
|
||||
}
|
||||
int row, col;
|
||||
for (row = 0; row < 7; row++) {
|
||||
for (row = 0; row < 8; row++) {
|
||||
if (key_data & (1 << row))
|
||||
break;
|
||||
}
|
||||
for (col = 8; col < 15; col++) {
|
||||
for (col = 8; col < 16; col++) {
|
||||
if (key_data & (1 << col))
|
||||
break;
|
||||
}
|
||||
@@ -229,7 +229,7 @@ void SX1509Component::setup_keypad_() {
|
||||
this->read_byte_16(REG_DIR_B, &this->ddr_mask_);
|
||||
for (int i = 0; i < this->rows_; i++)
|
||||
this->ddr_mask_ &= ~(1 << i);
|
||||
for (int i = 8; i < (this->cols_ * 2); i++)
|
||||
for (int i = 8; i < (8 + this->cols_); i++)
|
||||
this->ddr_mask_ |= (1 << i);
|
||||
this->write_byte_16(REG_DIR_B, this->ddr_mask_);
|
||||
|
||||
|
||||
@@ -183,6 +183,9 @@ void Tormatic::recompute_position_() {
|
||||
duration = this->close_duration_;
|
||||
}
|
||||
|
||||
if (duration == 0)
|
||||
return;
|
||||
|
||||
auto delta = direction * diff / duration;
|
||||
|
||||
this->position = clamp(this->position + delta, COVER_CLOSED, COVER_OPEN);
|
||||
|
||||
@@ -65,7 +65,12 @@ class Touchscreen : public PollingComponent {
|
||||
|
||||
void register_listener(TouchListener *listener) { this->touch_listeners_.push_back(listener); }
|
||||
|
||||
optional<TouchPoint> get_touch() { return this->touches_.begin()->second; }
|
||||
optional<TouchPoint> get_touch() {
|
||||
if (this->touches_.empty()) {
|
||||
return {};
|
||||
}
|
||||
return this->touches_.begin()->second;
|
||||
}
|
||||
|
||||
TouchPoints_t get_touches() {
|
||||
TouchPoints_t touches;
|
||||
|
||||
@@ -191,7 +191,7 @@ void IRAM_ATTR Tx20ComponentStore::gpio_intr(Tx20ComponentStore *arg) {
|
||||
arg->tx20_available = true;
|
||||
return;
|
||||
}
|
||||
if (index <= MAX_BUFFER_SIZE) {
|
||||
if (index < MAX_BUFFER_SIZE) {
|
||||
arg->buffer[index] = delay;
|
||||
}
|
||||
arg->spent_time += delay;
|
||||
|
||||
@@ -8,7 +8,7 @@ static const char *const TAG = "ufire_ec";
|
||||
|
||||
void UFireECComponent::setup() {
|
||||
uint8_t version;
|
||||
if (!this->read_byte(REGISTER_VERSION, &version) && version != 0xFF) {
|
||||
if (!this->read_byte(REGISTER_VERSION, &version) || version == 0xFF) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ static const char *const TAG = "ufire_ise";
|
||||
|
||||
void UFireISEComponent::setup() {
|
||||
uint8_t version;
|
||||
if (!this->read_byte(REGISTER_VERSION, &version) && version != 0xFF) {
|
||||
if (!this->read_byte(REGISTER_VERSION, &version) || version == 0xFF) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -161,7 +161,7 @@ void USBCDCACMInstance::setup() {
|
||||
|
||||
// Create a simple, unique task name per interface
|
||||
char task_name[] = "usb_tx_0";
|
||||
task_name[sizeof(task_name) - 1] = format_hex_char(static_cast<char>(this->itf_));
|
||||
task_name[sizeof(task_name) - 2] = format_hex_char(static_cast<char>(this->itf_));
|
||||
xTaskCreate(usb_tx_task_fn, task_name, stack_size, this, 4, &this->usb_tx_task_handle_);
|
||||
|
||||
if (this->usb_tx_task_handle_ == nullptr) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "vbus.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <algorithm>
|
||||
#include <cinttypes>
|
||||
|
||||
namespace esphome {
|
||||
@@ -106,9 +107,10 @@ void VBus::loop() {
|
||||
continue;
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char hex_buf[format_hex_size(VBUS_MAX_LOG_BYTES)];
|
||||
size_t log_bytes = std::min(this->buffer_.size(), static_cast<size_t>(VBUS_MAX_LOG_BYTES));
|
||||
#endif
|
||||
ESP_LOGV(TAG, "P2 C%04x %04x->%04x: %s", this->command_, this->source_, this->dest_,
|
||||
format_hex_to(hex_buf, this->buffer_.data(), this->buffer_.size()));
|
||||
format_hex_to(hex_buf, this->buffer_.data(), log_bytes));
|
||||
for (auto &listener : this->listeners_)
|
||||
listener->on_message(this->command_, this->source_, this->dest_, this->buffer_);
|
||||
this->state_ = 0;
|
||||
|
||||
@@ -141,7 +141,7 @@ void VEML7700Component::loop() {
|
||||
// Datasheet: 2.5 ms before the first measurement is needed, allowing for the correct start of the signal processor
|
||||
// and oscillator.
|
||||
// Reality: wait for couple integration times to have first samples captured
|
||||
this->set_timeout(2 * this->integration_time_, [this]() { this->state_ = State::IDLE; });
|
||||
this->set_timeout(2 * get_itime_ms(this->integration_time_), [this]() { this->state_ = State::IDLE; });
|
||||
}
|
||||
|
||||
if (this->is_ready()) {
|
||||
|
||||
@@ -71,7 +71,7 @@ class OTARequestHandler : public AsyncWebHandler {
|
||||
bool ota_success_{false};
|
||||
|
||||
private:
|
||||
decltype(ota::make_ota_backend()) ota_backend_{nullptr};
|
||||
ota::OTABackendPtr ota_backend_{nullptr};
|
||||
};
|
||||
|
||||
void OTARequestHandler::report_ota_progress_(AsyncWebServerRequest *request) {
|
||||
|
||||
@@ -2129,9 +2129,7 @@ bool WiFiComponent::is_connected_() const {
|
||||
return this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTED &&
|
||||
this->wifi_sta_connect_status_() == WiFiSTAConnectStatus::CONNECTED && !this->error_from_callback_;
|
||||
}
|
||||
void WiFiComponent::update_connected_state_() {
|
||||
this->connected_ = this->is_connected_();
|
||||
}
|
||||
void WiFiComponent::update_connected_state_() { this->connected_ = this->is_connected_(); }
|
||||
void WiFiComponent::set_power_save_mode(WiFiPowerSaveMode power_save) {
|
||||
this->power_save_ = power_save;
|
||||
#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
|
||||
|
||||
@@ -161,7 +161,7 @@ bool WLEDLightEffect::parse_notifier_frame_(light::AddressableLight &it, const u
|
||||
// https://kno.wled.ge/interfaces/udp-notifier/
|
||||
// https://github.com/Aircoookie/WLED/blob/main/wled00/udp.cpp
|
||||
|
||||
if (size < 34) {
|
||||
if (size <= 34) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -121,6 +121,11 @@ bool parse_xiaomi_message(const std::vector<uint8_t> &message, XiaomiParseResult
|
||||
// Byte 2: length
|
||||
// Byte 3..3+len-1: data point value
|
||||
|
||||
if (result.raw_offset < 0 || static_cast<size_t>(result.raw_offset) >= message.size()) {
|
||||
ESP_LOGVV(TAG, "parse_xiaomi_message(): raw_offset (%d) exceeds message size (%d)!", result.raw_offset,
|
||||
message.size());
|
||||
return false;
|
||||
}
|
||||
const uint8_t *payload = message.data() + result.raw_offset;
|
||||
uint8_t payload_length = message.size() - result.raw_offset;
|
||||
uint8_t payload_offset = 0;
|
||||
@@ -165,6 +170,10 @@ optional<XiaomiParseResult> parse_xiaomi_header(const esp32_ble_tracker::Service
|
||||
}
|
||||
|
||||
auto raw = service_data.data;
|
||||
if (raw.size() < 5) {
|
||||
ESP_LOGVV(TAG, "parse_xiaomi_header(): service data too short (%d).", raw.size());
|
||||
return {};
|
||||
}
|
||||
result.has_data = raw[0] & 0x40;
|
||||
result.has_capability = raw[0] & 0x20;
|
||||
result.has_encryption = raw[0] & 0x08;
|
||||
|
||||
@@ -138,6 +138,7 @@
|
||||
|
||||
// Feature flags which do not work for zephyr
|
||||
#ifndef USE_ZEPHYR
|
||||
#define AUDIO_FILE_MAX_FILES 4
|
||||
#define USE_AUDIO_DAC
|
||||
#define USE_AUDIO_FLAC_SUPPORT
|
||||
#define USE_AUDIO_MP3_SUPPORT
|
||||
|
||||
@@ -72,6 +72,7 @@ ignore_types = (
|
||||
".gif",
|
||||
".webp",
|
||||
".bin",
|
||||
".wav",
|
||||
)
|
||||
|
||||
LINT_FILE_CHECKS = []
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
audio_file:
|
||||
- id: test_audio
|
||||
file:
|
||||
type: local
|
||||
path: $component_dir/test.wav
|
||||
@@ -0,0 +1 @@
|
||||
<<: !include common.yaml
|
||||
Binary file not shown.
@@ -1,3 +1,5 @@
|
||||
modbus:
|
||||
id: mod_bus1
|
||||
flow_control_pin: ${flow_control_pin}
|
||||
send_wait_time: 500ms
|
||||
turnaround_time: 100ms
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
esp32:
|
||||
board: esp32-c6-devkitc-1
|
||||
framework:
|
||||
type: esp-idf
|
||||
log_level: DEBUG
|
||||
|
||||
network:
|
||||
enable_ipv6: true
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@ RESPONSE_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_EXPECT_TX): [cv.hex_uint8_t],
|
||||
cv.Required(CONF_INJECT_RX): [cv.hex_uint8_t],
|
||||
cv.Optional(CONF_DELAY, default="0ms"): cv.positive_time_period_milliseconds,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -151,7 +152,8 @@ async def to_code(config):
|
||||
for response in config[CONF_RESPONSES]:
|
||||
tx_data = response[CONF_EXPECT_TX]
|
||||
rx_data = response[CONF_INJECT_RX]
|
||||
cg.add(var.add_response(tx_data, rx_data))
|
||||
delay_ms = response[CONF_DELAY]
|
||||
cg.add(var.add_response(tx_data, rx_data, delay_ms))
|
||||
|
||||
for periodic in config[CONF_PERIODIC_RX]:
|
||||
data = periodic[CONF_DATA]
|
||||
|
||||
@@ -36,8 +36,8 @@ void MockUartComponent::loop() {
|
||||
// component (e.g., LD2410) a chance to process each batch independently.
|
||||
if (this->injection_index_ < this->injections_.size()) {
|
||||
auto &injection = this->injections_[this->injection_index_];
|
||||
uint32_t target_time = this->scenario_start_ms_ + this->cumulative_delay_ms_ + injection.delay_ms;
|
||||
if (now >= target_time) {
|
||||
uint32_t total_delay = this->cumulative_delay_ms_ + injection.delay_ms;
|
||||
if (now - this->scenario_start_ms_ >= total_delay) {
|
||||
ESP_LOGD(TAG, "Injecting %zu RX bytes (injection %u)", injection.rx_data.size(), this->injection_index_);
|
||||
this->inject_to_rx_buffer(injection.rx_data);
|
||||
this->cumulative_delay_ms_ += injection.delay_ms;
|
||||
@@ -52,6 +52,15 @@ void MockUartComponent::loop() {
|
||||
periodic.last_inject_ms = now;
|
||||
}
|
||||
}
|
||||
|
||||
// Process delayed responses
|
||||
for (auto &response : this->responses_) {
|
||||
if (response.delay_ms > 0 && response.last_match_ms > 0 && now - response.last_match_ms >= response.delay_ms) {
|
||||
ESP_LOGD(TAG, "Injecting %zu RX bytes for delayed response", response.inject_rx.size());
|
||||
this->inject_to_rx_buffer(response.inject_rx);
|
||||
response.last_match_ms = 0; // Reset to prevent repeated injection
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MockUartComponent::start_scenario() {
|
||||
@@ -149,8 +158,9 @@ void MockUartComponent::add_injection(const std::vector<uint8_t> &rx_data, uint3
|
||||
this->injections_.push_back({rx_data, delay_ms});
|
||||
}
|
||||
|
||||
void MockUartComponent::add_response(const std::vector<uint8_t> &expect_tx, const std::vector<uint8_t> &inject_rx) {
|
||||
this->responses_.push_back({expect_tx, inject_rx});
|
||||
void MockUartComponent::add_response(const std::vector<uint8_t> &expect_tx, const std::vector<uint8_t> &inject_rx,
|
||||
uint32_t delay_ms) {
|
||||
this->responses_.push_back({expect_tx, inject_rx, delay_ms, 0});
|
||||
}
|
||||
|
||||
void MockUartComponent::add_periodic_rx(const std::vector<uint8_t> &data, uint32_t interval_ms) {
|
||||
@@ -166,7 +176,13 @@ void MockUartComponent::try_match_response_() {
|
||||
size_t offset = this->tx_buffer_.size() - response.expect_tx.size();
|
||||
if (std::equal(response.expect_tx.begin(), response.expect_tx.end(), this->tx_buffer_.begin() + offset)) {
|
||||
ESP_LOGD(TAG, "TX match found, injecting %zu RX bytes", response.inject_rx.size());
|
||||
this->inject_to_rx_buffer(response.inject_rx);
|
||||
if (response.delay_ms > 0) {
|
||||
ESP_LOGD(TAG, "Delaying response by %u ms", response.delay_ms);
|
||||
// Schedule the response injection as a future injection
|
||||
response.last_match_ms = App.get_loop_component_start_time();
|
||||
} else {
|
||||
this->inject_to_rx_buffer(response.inject_rx);
|
||||
}
|
||||
this->tx_buffer_.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -34,7 +34,8 @@ class MockUartComponent : public uart::UARTComponent, public Component {
|
||||
|
||||
// Scenario configuration - called from generated code
|
||||
void add_injection(const std::vector<uint8_t> &rx_data, uint32_t delay_ms);
|
||||
void add_response(const std::vector<uint8_t> &expect_tx, const std::vector<uint8_t> &inject_rx);
|
||||
void add_response(const std::vector<uint8_t> &expect_tx, const std::vector<uint8_t> &inject_rx,
|
||||
uint32_t delay_ms = 0);
|
||||
void add_periodic_rx(const std::vector<uint8_t> &data, uint32_t interval_ms);
|
||||
|
||||
void start_scenario();
|
||||
@@ -64,6 +65,8 @@ class MockUartComponent : public uart::UARTComponent, public Component {
|
||||
struct Response {
|
||||
std::vector<uint8_t> expect_tx;
|
||||
std::vector<uint8_t> inject_rx;
|
||||
uint32_t delay_ms;
|
||||
uint32_t last_match_ms{0};
|
||||
};
|
||||
std::vector<Response> responses_;
|
||||
std::vector<uint8_t> tx_buffer_;
|
||||
|
||||
@@ -25,20 +25,64 @@ uart_mock:
|
||||
auto_start: false
|
||||
debug:
|
||||
responses:
|
||||
- expect_tx: [0x01, 0x03, 0x00, 0x03, 0x00, 0x01, 0x74, 0x0A] # Read holding register 1 on device 1
|
||||
- expect_tx: [0x01, 0x03, 0x00, 0x03, 0x00, 0x01, 0x74, 0x0A] # Read holding register 3 on device 1 (basic_register)
|
||||
inject_rx: [0x01, 0x03, 0x02, 0x01, 0x03, 0xF9, 0xD5] # Return value 0x0103 (hex) = 259 (dec)
|
||||
- expect_tx: [0x01, 0x03, 0x00, 0x05, 0x00, 0x01, 0x94, 0x0B] # Read holding register 5 on device 1 (delayed_response)
|
||||
delay: 100ms # Shorter than modbus send_wait_time of 200ms, should succeed
|
||||
inject_rx: [0x01, 0x03, 0x02, 0x00, 0xFF, 0xF8, 0x04] # Return value 0x00FF (hex) = 255 (dec)
|
||||
- expect_tx: [0x02, 0x03, 0x00, 0x07, 0x00, 0x01, 0x35, 0xF8] # Read holding register 7 on device 2 (late_response)
|
||||
delay: 300ms # Longer than modbus send_wait_time of 200ms, should cause timeout
|
||||
inject_rx: [0x02, 0x03, 0x02, 0x00, 0xF0, 0xFC, 0x00] # Return value 0x00F0 (hex) = 240 (dec)
|
||||
- expect_tx: [0x03, 0x03, 0x00, 0x09, 0x00, 0x01, 0x55, 0xEA] # Read holding register 9 on device 3 (no_response)
|
||||
inject_rx: [] # No response, should cause timeout
|
||||
- expect_tx: [0x01, 0x03, 0x00, 0x0A, 0x00, 0x01, 0xA4, 0x08] # Read holding register A on device 1 (exception_response)
|
||||
inject_rx: [0x01, 0x83, 0x02, 0xC0, 0xF1] # Exception response with code 2 (illegal data address)
|
||||
|
||||
modbus:
|
||||
uart_id: virtual_uart_dev
|
||||
send_wait_time: 200ms
|
||||
turnaround_time: 10ms
|
||||
|
||||
modbus_controller:
|
||||
address: 1
|
||||
- address: 1
|
||||
id: modbus_controller_ok
|
||||
max_cmd_retries: 0
|
||||
update_interval: 1s
|
||||
- address: 2
|
||||
id: modbus_controller_slow
|
||||
max_cmd_retries: 0
|
||||
update_interval: 1s
|
||||
- address: 3
|
||||
id: modbus_controller_offline
|
||||
max_cmd_retries: 0
|
||||
update_interval: 1s
|
||||
|
||||
sensor:
|
||||
- platform: modbus_controller
|
||||
name: "basic_register"
|
||||
address: 0x03
|
||||
register_type: holding
|
||||
modbus_controller_id: modbus_controller_ok
|
||||
- platform: modbus_controller
|
||||
name: "delayed_response"
|
||||
address: 0x05
|
||||
register_type: holding
|
||||
modbus_controller_id: modbus_controller_ok
|
||||
- platform: modbus_controller
|
||||
name: "late_response"
|
||||
address: 0x07
|
||||
register_type: holding
|
||||
modbus_controller_id: modbus_controller_slow
|
||||
- platform: modbus_controller
|
||||
name: "no_response"
|
||||
address: 0x09
|
||||
register_type: holding
|
||||
modbus_controller_id: modbus_controller_offline
|
||||
- platform: modbus_controller
|
||||
name: "exception_response"
|
||||
address: 0x0A
|
||||
register_type: holding
|
||||
modbus_controller_id: modbus_controller_ok
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
|
||||
@@ -46,10 +46,12 @@ uart_mock:
|
||||
|
||||
modbus:
|
||||
uart_id: virtual_uart_dev
|
||||
turnaround_time: 10ms
|
||||
|
||||
sensor:
|
||||
- platform: sdm_meter
|
||||
address: 2
|
||||
update_interval: 1s
|
||||
phase_a:
|
||||
voltage:
|
||||
name: sdm_voltage
|
||||
|
||||
@@ -39,9 +39,17 @@ async def test_uart_mock_modbus(
|
||||
# Track sensor state updates (after initial state is swallowed)
|
||||
sensor_states: dict[str, list[float]] = {
|
||||
"basic_register": [],
|
||||
"delayed_response": [],
|
||||
"late_response": [],
|
||||
"no_response": [],
|
||||
"exception_response": [],
|
||||
}
|
||||
|
||||
basic_register_changed = loop.create_future()
|
||||
delayed_response_changed = loop.create_future()
|
||||
late_response_changed = loop.create_future()
|
||||
no_response_changed = loop.create_future()
|
||||
exception_response_changed = loop.create_future()
|
||||
|
||||
def on_state(state: EntityState) -> None:
|
||||
if isinstance(state, SensorState) and not state.missing_state:
|
||||
@@ -54,6 +62,23 @@ async def test_uart_mock_modbus(
|
||||
and not basic_register_changed.done()
|
||||
):
|
||||
basic_register_changed.set_result(True)
|
||||
elif (
|
||||
sensor_name == "delayed_response"
|
||||
and state.state == 255.0
|
||||
and not delayed_response_changed.done()
|
||||
):
|
||||
delayed_response_changed.set_result(True)
|
||||
elif (
|
||||
sensor_name == "late_response" and not late_response_changed.done()
|
||||
):
|
||||
late_response_changed.set_result(True)
|
||||
elif sensor_name == "no_response" and not no_response_changed.done():
|
||||
no_response_changed.set_result(True)
|
||||
elif (
|
||||
sensor_name == "exception_response"
|
||||
and not exception_response_changed.done()
|
||||
):
|
||||
exception_response_changed.set_result(True)
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config),
|
||||
@@ -79,20 +104,52 @@ async def test_uart_mock_modbus(
|
||||
assert start_btn is not None, "Start Scenario button not found"
|
||||
client.button_command(start_btn.key)
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(delayed_response_changed, timeout=2.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"Timeout waiting for delayed_response change. Received sensor states:\n"
|
||||
f" delayed_response: {sensor_states['delayed_response']}\n"
|
||||
)
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(late_response_changed, timeout=2.0)
|
||||
pytest.fail(
|
||||
f"late_response change should not have been triggered, but was. Received sensor states:\n"
|
||||
f" late_response: {sensor_states['late_response']}\n"
|
||||
)
|
||||
except TimeoutError:
|
||||
pass # Expected timeout since we never inject a response for late_response
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(no_response_changed, timeout=2.0)
|
||||
pytest.fail(
|
||||
f"no_response change should not have been triggered, but was. Received sensor states:\n"
|
||||
f" no_response: {sensor_states['no_response']}\n"
|
||||
)
|
||||
except TimeoutError:
|
||||
pass # Expected timeout since we never inject a response for no_response
|
||||
|
||||
# Wait for basic register to be updated with successful parse
|
||||
try:
|
||||
await asyncio.wait_for(basic_register_changed, timeout=15.0)
|
||||
await asyncio.wait_for(basic_register_changed, timeout=2.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"Timeout waiting for Basic Register change. Received sensor states:\n"
|
||||
f" basic_register: {sensor_states['basic_register']}\n"
|
||||
)
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(exception_response_changed, timeout=2.0)
|
||||
pytest.fail(
|
||||
f"exception_response change should not have been triggered, but was. Received sensor states:\n"
|
||||
f" exception_response: {sensor_states['exception_response']}\n"
|
||||
)
|
||||
except TimeoutError:
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.xfail(
|
||||
reason="There is a bug in UART which will timeout for long responses."
|
||||
)
|
||||
async def test_uart_mock_modbus_timing(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
@@ -155,7 +212,7 @@ async def test_uart_mock_modbus_timing(
|
||||
|
||||
# Wait for voltage to be updated with successful parse
|
||||
try:
|
||||
await asyncio.wait_for(voltage_changed, timeout=15.0)
|
||||
await asyncio.wait_for(voltage_changed, timeout=2.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"Timeout waiting for SDM voltage change. Received sensor states:\n"
|
||||
|
||||
Reference in New Issue
Block a user