mirror of
https://github.com/esphome/esphome.git
synced 2026-07-10 17:05:36 +00:00
Merge branch 'dev' into noise_api_zero_copy
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@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
|
||||
- name: Set TAG
|
||||
run: |
|
||||
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8
|
||||
uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
@@ -86,6 +86,6 @@ jobs:
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8
|
||||
uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -99,7 +99,7 @@ jobs:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
|
||||
- name: Log in to docker hub
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
@@ -178,7 +178,7 @@ jobs:
|
||||
merge-multiple: true
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
|
||||
- name: Log in to docker hub
|
||||
if: matrix.registry == 'dockerhub'
|
||||
|
||||
@@ -11,7 +11,7 @@ ci:
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.14.9
|
||||
rev: v0.14.10
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -42,6 +42,7 @@ esphome/components/animation/* @syndlex
|
||||
esphome/components/anova/* @buxtronix
|
||||
esphome/components/apds9306/* @aodrenah
|
||||
esphome/components/api/* @esphome/core
|
||||
esphome/components/aqi/* @freekode @jasstrong @ximex
|
||||
esphome/components/as5600/* @ammmze
|
||||
esphome/components/as5600/sensor/* @ammmze
|
||||
esphome/components/as7341/* @mrgnr
|
||||
|
||||
@@ -518,10 +518,49 @@ def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
|
||||
rc = platformio_api.run_compile(config, CORE.verbose)
|
||||
if rc != 0:
|
||||
return rc
|
||||
|
||||
# Check if firmware was rebuilt and emit build_info + create manifest
|
||||
_check_and_emit_build_info()
|
||||
|
||||
idedata = platformio_api.get_idedata(config)
|
||||
return 0 if idedata is not None else 1
|
||||
|
||||
|
||||
def _check_and_emit_build_info() -> None:
|
||||
"""Check if firmware was rebuilt and emit build_info."""
|
||||
import json
|
||||
|
||||
firmware_path = CORE.firmware_bin
|
||||
build_info_json_path = CORE.relative_build_path("build_info.json")
|
||||
|
||||
# Check if both files exist
|
||||
if not firmware_path.exists() or not build_info_json_path.exists():
|
||||
return
|
||||
|
||||
# Check if firmware is newer than build_info (indicating a relink occurred)
|
||||
if firmware_path.stat().st_mtime <= build_info_json_path.stat().st_mtime:
|
||||
return
|
||||
|
||||
# Read build_info from JSON
|
||||
try:
|
||||
with open(build_info_json_path, encoding="utf-8") as f:
|
||||
build_info = json.load(f)
|
||||
except (OSError, json.JSONDecodeError) as e:
|
||||
_LOGGER.debug("Failed to read build_info: %s", e)
|
||||
return
|
||||
|
||||
config_hash = build_info.get("config_hash")
|
||||
build_time_str = build_info.get("build_time_str")
|
||||
|
||||
if config_hash is None or build_time_str is None:
|
||||
return
|
||||
|
||||
# Emit build_info with human-readable time
|
||||
_LOGGER.info(
|
||||
"Build Info: config_hash=0x%08x build_time_str=%s", config_hash, build_time_str
|
||||
)
|
||||
|
||||
|
||||
def upload_using_esptool(
|
||||
config: ConfigType, port: str, file: str, speed: int
|
||||
) -> str | int:
|
||||
|
||||
@@ -35,26 +35,12 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) {
|
||||
ESP_LOGD(TAG, "Set state to: %s, previous: %s", LOG_STR_ARG(alarm_control_panel_state_to_string(state)),
|
||||
LOG_STR_ARG(alarm_control_panel_state_to_string(prev_state)));
|
||||
this->current_state_ = state;
|
||||
// Single state callback - triggers check get_state() for specific states
|
||||
this->state_callback_.call();
|
||||
#if defined(USE_ALARM_CONTROL_PANEL) && defined(USE_CONTROLLER_REGISTRY)
|
||||
ControllerRegistry::notify_alarm_control_panel_update(this);
|
||||
#endif
|
||||
if (state == ACP_STATE_TRIGGERED) {
|
||||
this->triggered_callback_.call();
|
||||
} else if (state == ACP_STATE_ARMING) {
|
||||
this->arming_callback_.call();
|
||||
} else if (state == ACP_STATE_PENDING) {
|
||||
this->pending_callback_.call();
|
||||
} else if (state == ACP_STATE_ARMED_HOME) {
|
||||
this->armed_home_callback_.call();
|
||||
} else if (state == ACP_STATE_ARMED_NIGHT) {
|
||||
this->armed_night_callback_.call();
|
||||
} else if (state == ACP_STATE_ARMED_AWAY) {
|
||||
this->armed_away_callback_.call();
|
||||
} else if (state == ACP_STATE_DISARMED) {
|
||||
this->disarmed_callback_.call();
|
||||
}
|
||||
|
||||
// Cleared fires when leaving TRIGGERED state
|
||||
if (prev_state == ACP_STATE_TRIGGERED) {
|
||||
this->cleared_callback_.call();
|
||||
}
|
||||
@@ -69,34 +55,6 @@ void AlarmControlPanel::add_on_state_callback(std::function<void()> &&callback)
|
||||
this->state_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void AlarmControlPanel::add_on_triggered_callback(std::function<void()> &&callback) {
|
||||
this->triggered_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void AlarmControlPanel::add_on_arming_callback(std::function<void()> &&callback) {
|
||||
this->arming_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void AlarmControlPanel::add_on_armed_home_callback(std::function<void()> &&callback) {
|
||||
this->armed_home_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void AlarmControlPanel::add_on_armed_night_callback(std::function<void()> &&callback) {
|
||||
this->armed_night_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void AlarmControlPanel::add_on_armed_away_callback(std::function<void()> &&callback) {
|
||||
this->armed_away_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void AlarmControlPanel::add_on_pending_callback(std::function<void()> &&callback) {
|
||||
this->pending_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void AlarmControlPanel::add_on_disarmed_callback(std::function<void()> &&callback) {
|
||||
this->disarmed_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void AlarmControlPanel::add_on_cleared_callback(std::function<void()> &&callback) {
|
||||
this->cleared_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
@@ -35,54 +35,13 @@ class AlarmControlPanel : public EntityBase {
|
||||
*/
|
||||
void publish_state(AlarmControlPanelState state);
|
||||
|
||||
/** Add a callback for when the state of the alarm_control_panel changes
|
||||
/** Add a callback for when the state of the alarm_control_panel changes.
|
||||
* Triggers can check get_state() to determine the new state.
|
||||
*
|
||||
* @param callback The callback function
|
||||
*/
|
||||
void add_on_state_callback(std::function<void()> &&callback);
|
||||
|
||||
/** Add a callback for when the state of the alarm_control_panel chanes to triggered
|
||||
*
|
||||
* @param callback The callback function
|
||||
*/
|
||||
void add_on_triggered_callback(std::function<void()> &&callback);
|
||||
|
||||
/** Add a callback for when the state of the alarm_control_panel chanes to arming
|
||||
*
|
||||
* @param callback The callback function
|
||||
*/
|
||||
void add_on_arming_callback(std::function<void()> &&callback);
|
||||
|
||||
/** Add a callback for when the state of the alarm_control_panel changes to pending
|
||||
*
|
||||
* @param callback The callback function
|
||||
*/
|
||||
void add_on_pending_callback(std::function<void()> &&callback);
|
||||
|
||||
/** Add a callback for when the state of the alarm_control_panel changes to armed_home
|
||||
*
|
||||
* @param callback The callback function
|
||||
*/
|
||||
void add_on_armed_home_callback(std::function<void()> &&callback);
|
||||
|
||||
/** Add a callback for when the state of the alarm_control_panel changes to armed_night
|
||||
*
|
||||
* @param callback The callback function
|
||||
*/
|
||||
void add_on_armed_night_callback(std::function<void()> &&callback);
|
||||
|
||||
/** Add a callback for when the state of the alarm_control_panel changes to armed_away
|
||||
*
|
||||
* @param callback The callback function
|
||||
*/
|
||||
void add_on_armed_away_callback(std::function<void()> &&callback);
|
||||
|
||||
/** Add a callback for when the state of the alarm_control_panel changes to disarmed
|
||||
*
|
||||
* @param callback The callback function
|
||||
*/
|
||||
void add_on_disarmed_callback(std::function<void()> &&callback);
|
||||
|
||||
/** Add a callback for when the state of the alarm_control_panel clears from triggered
|
||||
*
|
||||
* @param callback The callback function
|
||||
@@ -172,23 +131,9 @@ class AlarmControlPanel : public EntityBase {
|
||||
uint32_t last_update_;
|
||||
// the call control function
|
||||
virtual void control(const AlarmControlPanelCall &call) = 0;
|
||||
// state callback
|
||||
// state callback - triggers check get_state() for specific state
|
||||
CallbackManager<void()> state_callback_{};
|
||||
// trigger callback
|
||||
CallbackManager<void()> triggered_callback_{};
|
||||
// arming callback
|
||||
CallbackManager<void()> arming_callback_{};
|
||||
// pending callback
|
||||
CallbackManager<void()> pending_callback_{};
|
||||
// armed_home callback
|
||||
CallbackManager<void()> armed_home_callback_{};
|
||||
// armed_night callback
|
||||
CallbackManager<void()> armed_night_callback_{};
|
||||
// armed_away callback
|
||||
CallbackManager<void()> armed_away_callback_{};
|
||||
// disarmed callback
|
||||
CallbackManager<void()> disarmed_callback_{};
|
||||
// clear callback
|
||||
// clear callback - fires when leaving TRIGGERED state
|
||||
CallbackManager<void()> cleared_callback_{};
|
||||
// chime callback
|
||||
CallbackManager<void()> chime_callback_{};
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
namespace esphome {
|
||||
namespace alarm_control_panel {
|
||||
|
||||
/// Trigger on any state change
|
||||
class StateTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit StateTrigger(AlarmControlPanel *alarm_control_panel) {
|
||||
@@ -13,55 +14,30 @@ class StateTrigger : public Trigger<> {
|
||||
}
|
||||
};
|
||||
|
||||
class TriggeredTrigger : public Trigger<> {
|
||||
/// Template trigger that fires when entering a specific state
|
||||
template<AlarmControlPanelState State> class StateEnterTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit TriggeredTrigger(AlarmControlPanel *alarm_control_panel) {
|
||||
alarm_control_panel->add_on_triggered_callback([this]() { this->trigger(); });
|
||||
explicit StateEnterTrigger(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {
|
||||
alarm_control_panel->add_on_state_callback([this]() {
|
||||
if (this->alarm_control_panel_->get_state() == State)
|
||||
this->trigger();
|
||||
});
|
||||
}
|
||||
|
||||
protected:
|
||||
AlarmControlPanel *alarm_control_panel_;
|
||||
};
|
||||
|
||||
class ArmingTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit ArmingTrigger(AlarmControlPanel *alarm_control_panel) {
|
||||
alarm_control_panel->add_on_arming_callback([this]() { this->trigger(); });
|
||||
}
|
||||
};
|
||||
|
||||
class PendingTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit PendingTrigger(AlarmControlPanel *alarm_control_panel) {
|
||||
alarm_control_panel->add_on_pending_callback([this]() { this->trigger(); });
|
||||
}
|
||||
};
|
||||
|
||||
class ArmedHomeTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit ArmedHomeTrigger(AlarmControlPanel *alarm_control_panel) {
|
||||
alarm_control_panel->add_on_armed_home_callback([this]() { this->trigger(); });
|
||||
}
|
||||
};
|
||||
|
||||
class ArmedNightTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit ArmedNightTrigger(AlarmControlPanel *alarm_control_panel) {
|
||||
alarm_control_panel->add_on_armed_night_callback([this]() { this->trigger(); });
|
||||
}
|
||||
};
|
||||
|
||||
class ArmedAwayTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit ArmedAwayTrigger(AlarmControlPanel *alarm_control_panel) {
|
||||
alarm_control_panel->add_on_armed_away_callback([this]() { this->trigger(); });
|
||||
}
|
||||
};
|
||||
|
||||
class DisarmedTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit DisarmedTrigger(AlarmControlPanel *alarm_control_panel) {
|
||||
alarm_control_panel->add_on_disarmed_callback([this]() { this->trigger(); });
|
||||
}
|
||||
};
|
||||
// Type aliases for state-specific triggers
|
||||
using TriggeredTrigger = StateEnterTrigger<ACP_STATE_TRIGGERED>;
|
||||
using ArmingTrigger = StateEnterTrigger<ACP_STATE_ARMING>;
|
||||
using PendingTrigger = StateEnterTrigger<ACP_STATE_PENDING>;
|
||||
using ArmedHomeTrigger = StateEnterTrigger<ACP_STATE_ARMED_HOME>;
|
||||
using ArmedNightTrigger = StateEnterTrigger<ACP_STATE_ARMED_NIGHT>;
|
||||
using ArmedAwayTrigger = StateEnterTrigger<ACP_STATE_ARMED_AWAY>;
|
||||
using DisarmedTrigger = StateEnterTrigger<ACP_STATE_DISARMED>;
|
||||
|
||||
/// Trigger when leaving TRIGGERED state (alarm cleared)
|
||||
class ClearedTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit ClearedTrigger(AlarmControlPanel *alarm_control_panel) {
|
||||
@@ -69,6 +45,7 @@ class ClearedTrigger : public Trigger<> {
|
||||
}
|
||||
};
|
||||
|
||||
/// Trigger on chime event (zone opened while disarmed)
|
||||
class ChimeTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit ChimeTrigger(AlarmControlPanel *alarm_control_panel) {
|
||||
@@ -76,6 +53,7 @@ class ChimeTrigger : public Trigger<> {
|
||||
}
|
||||
};
|
||||
|
||||
/// Trigger on ready state change
|
||||
class ReadyTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit ReadyTrigger(AlarmControlPanel *alarm_control_panel) {
|
||||
|
||||
@@ -1472,7 +1472,10 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
|
||||
|
||||
resp.set_esphome_version(ESPHOME_VERSION_REF);
|
||||
|
||||
resp.set_compilation_time(App.get_compilation_time_ref());
|
||||
// Stack buffer for build time string
|
||||
char build_time_str[Application::BUILD_TIME_STR_SIZE];
|
||||
App.get_build_time_string(build_time_str);
|
||||
resp.set_compilation_time(StringRef(build_time_str));
|
||||
|
||||
// Manufacturer string - define once, handle ESP8266 PROGMEM separately
|
||||
#if defined(USE_ESP8266) || defined(USE_ESP32)
|
||||
|
||||
@@ -124,12 +124,12 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const {
|
||||
#endif
|
||||
#ifdef USE_DEVICES
|
||||
for (const auto &it : this->devices) {
|
||||
buffer.encode_message(20, it, true);
|
||||
buffer.encode_message(20, it);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_AREAS
|
||||
for (const auto &it : this->areas) {
|
||||
buffer.encode_message(21, it, true);
|
||||
buffer.encode_message(21, it);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_AREAS
|
||||
@@ -884,13 +884,13 @@ void HomeassistantServiceMap::calculate_size(ProtoSize &size) const {
|
||||
void HomeassistantActionRequest::encode(ProtoWriteBuffer buffer) const {
|
||||
buffer.encode_string(1, this->service_ref_);
|
||||
for (auto &it : this->data) {
|
||||
buffer.encode_message(2, it, true);
|
||||
buffer.encode_message(2, it);
|
||||
}
|
||||
for (auto &it : this->data_template) {
|
||||
buffer.encode_message(3, it, true);
|
||||
buffer.encode_message(3, it);
|
||||
}
|
||||
for (auto &it : this->variables) {
|
||||
buffer.encode_message(4, it, true);
|
||||
buffer.encode_message(4, it);
|
||||
}
|
||||
buffer.encode_bool(5, this->is_event);
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
@@ -1017,7 +1017,7 @@ void ListEntitiesServicesResponse::encode(ProtoWriteBuffer buffer) const {
|
||||
buffer.encode_string(1, this->name_ref_);
|
||||
buffer.encode_fixed32(2, this->key);
|
||||
for (auto &it : this->args) {
|
||||
buffer.encode_message(3, it, true);
|
||||
buffer.encode_message(3, it);
|
||||
}
|
||||
buffer.encode_uint32(4, static_cast<uint32_t>(this->supports_response));
|
||||
}
|
||||
@@ -1879,7 +1879,7 @@ void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer buffer) const {
|
||||
buffer.encode_uint32(7, static_cast<uint32_t>(this->entity_category));
|
||||
buffer.encode_bool(8, this->supports_pause);
|
||||
for (auto &it : this->supported_formats) {
|
||||
buffer.encode_message(9, it, true);
|
||||
buffer.encode_message(9, it);
|
||||
}
|
||||
#ifdef USE_DEVICES
|
||||
buffer.encode_uint32(10, this->device_id);
|
||||
@@ -1999,7 +1999,7 @@ void BluetoothLERawAdvertisement::calculate_size(ProtoSize &size) const {
|
||||
}
|
||||
void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer buffer) const {
|
||||
for (uint16_t i = 0; i < this->advertisements_len; i++) {
|
||||
buffer.encode_message(1, this->advertisements[i], true);
|
||||
buffer.encode_message(1, this->advertisements[i]);
|
||||
}
|
||||
}
|
||||
void BluetoothLERawAdvertisementsResponse::calculate_size(ProtoSize &size) const {
|
||||
@@ -2072,7 +2072,7 @@ void BluetoothGATTCharacteristic::encode(ProtoWriteBuffer buffer) const {
|
||||
buffer.encode_uint32(2, this->handle);
|
||||
buffer.encode_uint32(3, this->properties);
|
||||
for (auto &it : this->descriptors) {
|
||||
buffer.encode_message(4, it, true);
|
||||
buffer.encode_message(4, it);
|
||||
}
|
||||
buffer.encode_uint32(5, this->short_uuid);
|
||||
}
|
||||
@@ -2093,7 +2093,7 @@ void BluetoothGATTService::encode(ProtoWriteBuffer buffer) const {
|
||||
}
|
||||
buffer.encode_uint32(2, this->handle);
|
||||
for (auto &it : this->characteristics) {
|
||||
buffer.encode_message(3, it, true);
|
||||
buffer.encode_message(3, it);
|
||||
}
|
||||
buffer.encode_uint32(4, this->short_uuid);
|
||||
}
|
||||
@@ -2109,7 +2109,7 @@ void BluetoothGATTService::calculate_size(ProtoSize &size) const {
|
||||
void BluetoothGATTGetServicesResponse::encode(ProtoWriteBuffer buffer) const {
|
||||
buffer.encode_uint64(1, this->address);
|
||||
for (auto &it : this->services) {
|
||||
buffer.encode_message(2, it, true);
|
||||
buffer.encode_message(2, it);
|
||||
}
|
||||
}
|
||||
void BluetoothGATTGetServicesResponse::calculate_size(ProtoSize &size) const {
|
||||
@@ -2569,7 +2569,7 @@ bool VoiceAssistantConfigurationRequest::decode_length(uint32_t field_id, ProtoL
|
||||
}
|
||||
void VoiceAssistantConfigurationResponse::encode(ProtoWriteBuffer buffer) const {
|
||||
for (auto &it : this->available_wake_words) {
|
||||
buffer.encode_message(1, it, true);
|
||||
buffer.encode_message(1, it);
|
||||
}
|
||||
for (const auto &it : *this->active_wake_words) {
|
||||
buffer.encode_string(2, it, true);
|
||||
|
||||
@@ -334,7 +334,7 @@ class ProtoWriteBuffer {
|
||||
void encode_sint64(uint32_t field_id, int64_t value, bool force = false) {
|
||||
this->encode_uint64(field_id, encode_zigzag64(value), force);
|
||||
}
|
||||
void encode_message(uint32_t field_id, const ProtoMessage &value, bool force = false);
|
||||
void encode_message(uint32_t field_id, const ProtoMessage &value);
|
||||
std::vector<uint8_t> *get_buffer() const { return buffer_; }
|
||||
|
||||
protected:
|
||||
@@ -795,7 +795,7 @@ class ProtoSize {
|
||||
};
|
||||
|
||||
// Implementation of encode_message - must be after ProtoMessage is defined
|
||||
inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessage &value, bool force) {
|
||||
inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessage &value) {
|
||||
this->encode_field_raw(field_id, 2); // type 2: Length-delimited message
|
||||
|
||||
// Calculate the message size first
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import esphome.codegen as cg
|
||||
|
||||
CODEOWNERS = ["@jasstrong", "@ximex", "@freekode"]
|
||||
|
||||
aqi_ns = cg.esphome_ns.namespace("aqi")
|
||||
AQICalculatorType = aqi_ns.enum("AQICalculatorType")
|
||||
|
||||
CONF_AQI = "aqi"
|
||||
CONF_CALCULATION_TYPE = "calculation_type"
|
||||
|
||||
AQI_CALCULATION_TYPE = {
|
||||
"CAQI": AQICalculatorType.CAQI_TYPE,
|
||||
"AQI": AQICalculatorType.AQI_TYPE,
|
||||
}
|
||||
+2
-4
@@ -2,13 +2,11 @@
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome {
|
||||
namespace hm3301 {
|
||||
namespace esphome::aqi {
|
||||
|
||||
class AbstractAQICalculator {
|
||||
public:
|
||||
virtual uint16_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) = 0;
|
||||
};
|
||||
|
||||
} // namespace hm3301
|
||||
} // namespace esphome
|
||||
} // namespace esphome::aqi
|
||||
+7
-4
@@ -1,10 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <climits>
|
||||
#include "abstract_aqi_calculator.h"
|
||||
|
||||
// https://document.airnow.gov/technical-assistance-document-for-the-reporting-of-daily-air-quailty.pdf
|
||||
|
||||
namespace esphome {
|
||||
namespace hm3301 {
|
||||
namespace esphome::aqi {
|
||||
|
||||
class AQICalculator : public AbstractAQICalculator {
|
||||
public:
|
||||
@@ -28,6 +29,9 @@ class AQICalculator : public AbstractAQICalculator {
|
||||
|
||||
int calculate_index_(uint16_t value, int array[AMOUNT_OF_LEVELS][2]) {
|
||||
int grid_index = get_grid_index_(value, array);
|
||||
if (grid_index == -1) {
|
||||
return -1;
|
||||
}
|
||||
int aqi_lo = index_grid_[grid_index][0];
|
||||
int aqi_hi = index_grid_[grid_index][1];
|
||||
int conc_lo = array[grid_index][0];
|
||||
@@ -46,5 +50,4 @@ class AQICalculator : public AbstractAQICalculator {
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace hm3301
|
||||
} // namespace esphome
|
||||
} // namespace esphome::aqi
|
||||
+6
-8
@@ -3,8 +3,7 @@
|
||||
#include "caqi_calculator.h"
|
||||
#include "aqi_calculator.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace hm3301 {
|
||||
namespace esphome::aqi {
|
||||
|
||||
enum AQICalculatorType { CAQI_TYPE = 0, AQI_TYPE = 1 };
|
||||
|
||||
@@ -12,18 +11,17 @@ class AQICalculatorFactory {
|
||||
public:
|
||||
AbstractAQICalculator *get_calculator(AQICalculatorType type) {
|
||||
if (type == 0) {
|
||||
return caqi_calculator_;
|
||||
return &this->caqi_calculator_;
|
||||
} else if (type == 1) {
|
||||
return aqi_calculator_;
|
||||
return &this->aqi_calculator_;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
protected:
|
||||
CAQICalculator *caqi_calculator_ = new CAQICalculator();
|
||||
AQICalculator *aqi_calculator_ = new AQICalculator();
|
||||
CAQICalculator caqi_calculator_;
|
||||
AQICalculator aqi_calculator_;
|
||||
};
|
||||
|
||||
} // namespace hm3301
|
||||
} // namespace esphome
|
||||
} // namespace esphome::aqi
|
||||
+2
-4
@@ -3,8 +3,7 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "abstract_aqi_calculator.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace hm3301 {
|
||||
namespace esphome::aqi {
|
||||
|
||||
class CAQICalculator : public AbstractAQICalculator {
|
||||
public:
|
||||
@@ -48,5 +47,4 @@ class CAQICalculator : public AbstractAQICalculator {
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace hm3301
|
||||
} // namespace esphome
|
||||
} // namespace esphome::aqi
|
||||
@@ -8,17 +8,20 @@ namespace dht {
|
||||
static const char *const TAG = "dht";
|
||||
|
||||
void DHT::setup() {
|
||||
this->pin_->digital_write(true);
|
||||
this->pin_->setup();
|
||||
this->pin_->digital_write(true);
|
||||
this->t_pin_->digital_write(true);
|
||||
this->t_pin_->setup();
|
||||
#ifdef USE_ESP32
|
||||
this->t_pin_->pin_mode(this->t_pin_->get_flags() | gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN);
|
||||
#endif
|
||||
this->t_pin_->digital_write(true);
|
||||
}
|
||||
|
||||
void DHT::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "DHT:");
|
||||
LOG_PIN(" Pin: ", this->pin_);
|
||||
LOG_PIN(" Pin: ", this->t_pin_);
|
||||
ESP_LOGCONFIG(TAG, " %sModel: %s", this->is_auto_detect_ ? "Auto-detected " : "",
|
||||
this->model_ == DHT_MODEL_DHT11 ? "DHT11" : "DHT22 or equivalent");
|
||||
ESP_LOGCONFIG(TAG, " Internal pull-up: %s", ONOFF(this->pin_->get_flags() & gpio::FLAG_PULLUP));
|
||||
ESP_LOGCONFIG(TAG, " Internal pull-up: %s", ONOFF(this->t_pin_->get_flags() & gpio::FLAG_PULLUP));
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
|
||||
@@ -72,21 +75,15 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r
|
||||
int8_t i = 0;
|
||||
uint8_t data[5] = {0, 0, 0, 0, 0};
|
||||
|
||||
this->pin_->digital_write(false);
|
||||
this->pin_->pin_mode(gpio::FLAG_OUTPUT);
|
||||
this->pin_->digital_write(false);
|
||||
#ifndef USE_ESP32
|
||||
this->pin_.pin_mode(gpio::FLAG_OUTPUT);
|
||||
#endif
|
||||
this->pin_.digital_write(false);
|
||||
|
||||
if (this->model_ == DHT_MODEL_DHT11) {
|
||||
delayMicroseconds(18000);
|
||||
} else if (this->model_ == DHT_MODEL_SI7021) {
|
||||
#ifdef USE_ESP8266
|
||||
delayMicroseconds(500);
|
||||
this->pin_->digital_write(true);
|
||||
delayMicroseconds(40);
|
||||
#else
|
||||
delayMicroseconds(400);
|
||||
this->pin_->digital_write(true);
|
||||
#endif
|
||||
} else if (this->model_ == DHT_MODEL_DHT22_TYPE2) {
|
||||
delayMicroseconds(2000);
|
||||
} else if (this->model_ == DHT_MODEL_AM2120 || this->model_ == DHT_MODEL_AM2302) {
|
||||
@@ -94,7 +91,12 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r
|
||||
} else {
|
||||
delayMicroseconds(800);
|
||||
}
|
||||
this->pin_->pin_mode(this->pin_->get_flags());
|
||||
|
||||
#ifdef USE_ESP32
|
||||
this->pin_.digital_write(true);
|
||||
#else
|
||||
this->pin_.pin_mode(this->t_pin_->get_flags());
|
||||
#endif
|
||||
|
||||
{
|
||||
InterruptLock lock;
|
||||
@@ -110,7 +112,7 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r
|
||||
uint32_t start_time = micros();
|
||||
|
||||
// Wait for rising edge
|
||||
while (!this->pin_->digital_read()) {
|
||||
while (!this->pin_.digital_read()) {
|
||||
if (micros() - start_time > 90) {
|
||||
if (i < 0) {
|
||||
error_code = 1; // line didn't clear
|
||||
@@ -127,7 +129,7 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r
|
||||
uint32_t end_time = start_time;
|
||||
|
||||
// Wait for falling edge
|
||||
while (this->pin_->digital_read()) {
|
||||
while (this->pin_.digital_read()) {
|
||||
end_time = micros();
|
||||
if (end_time - start_time > 90) {
|
||||
if (i < 0) {
|
||||
|
||||
@@ -38,7 +38,10 @@ class DHT : public PollingComponent {
|
||||
*/
|
||||
void set_dht_model(DHTModel model);
|
||||
|
||||
void set_pin(InternalGPIOPin *pin) { pin_ = pin; }
|
||||
void set_pin(InternalGPIOPin *pin) {
|
||||
this->t_pin_ = pin;
|
||||
this->pin_ = pin->to_isr();
|
||||
}
|
||||
void set_model(DHTModel model) { model_ = model; }
|
||||
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
|
||||
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; }
|
||||
@@ -54,7 +57,8 @@ class DHT : public PollingComponent {
|
||||
protected:
|
||||
bool read_sensor_(float *temperature, float *humidity, bool report_errors);
|
||||
|
||||
InternalGPIOPin *pin_;
|
||||
InternalGPIOPin *t_pin_;
|
||||
ISRInternalGPIOPin pin_;
|
||||
DHTModel model_{DHT_MODEL_AUTO_DETECT};
|
||||
bool is_auto_detect_{false};
|
||||
sensor::Sensor *temperature_sensor_{nullptr};
|
||||
|
||||
@@ -130,10 +130,10 @@ class ESP32Preferences : public ESPPreferences {
|
||||
// go through vector from back to front (makes erase easier/more efficient)
|
||||
for (ssize_t i = s_pending_save.size() - 1; i >= 0; i--) {
|
||||
const auto &save = s_pending_save[i];
|
||||
ESP_LOGVV(TAG, "Checking if NVS data %" PRIu32 " has changed", save.key);
|
||||
if (this->is_changed(this->nvs_handle, save)) {
|
||||
char key_str[KEY_BUFFER_SIZE];
|
||||
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
|
||||
char key_str[KEY_BUFFER_SIZE];
|
||||
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
|
||||
ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str);
|
||||
if (this->is_changed_(this->nvs_handle, save, key_str)) {
|
||||
esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.get(), save.len);
|
||||
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.len);
|
||||
if (err != 0) {
|
||||
@@ -166,10 +166,9 @@ class ESP32Preferences : public ESPPreferences {
|
||||
|
||||
return failed == 0;
|
||||
}
|
||||
bool is_changed(const uint32_t nvs_handle, const NVSData &to_save) {
|
||||
char key_str[KEY_BUFFER_SIZE];
|
||||
snprintf(key_str, sizeof(key_str), "%" PRIu32, to_save.key);
|
||||
|
||||
protected:
|
||||
bool is_changed_(uint32_t nvs_handle, const NVSData &to_save, const char *key_str) {
|
||||
size_t actual_len;
|
||||
esp_err_t err = nvs_get_blob(nvs_handle, key_str, nullptr, &actual_len);
|
||||
if (err != 0) {
|
||||
|
||||
@@ -308,13 +308,21 @@ bool ESP32BLE::ble_setup_() {
|
||||
bool ESP32BLE::ble_dismantle_() {
|
||||
esp_err_t err = esp_bluedroid_disable();
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_bluedroid_disable failed: %d", err);
|
||||
return false;
|
||||
// ESP_ERR_INVALID_STATE means Bluedroid is already disabled, which is fine
|
||||
if (err != ESP_ERR_INVALID_STATE) {
|
||||
ESP_LOGE(TAG, "esp_bluedroid_disable failed: %d", err);
|
||||
return false;
|
||||
}
|
||||
ESP_LOGD(TAG, "Already disabled");
|
||||
}
|
||||
err = esp_bluedroid_deinit();
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_bluedroid_deinit failed: %d", err);
|
||||
return false;
|
||||
// ESP_ERR_INVALID_STATE means Bluedroid is already deinitialized, which is fine
|
||||
if (err != ESP_ERR_INVALID_STATE) {
|
||||
ESP_LOGE(TAG, "esp_bluedroid_deinit failed: %d", err);
|
||||
return false;
|
||||
}
|
||||
ESP_LOGD(TAG, "Already deinitialized");
|
||||
}
|
||||
|
||||
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
||||
|
||||
@@ -212,17 +212,23 @@ extern ESP32BLE *global_ble;
|
||||
|
||||
template<typename... Ts> class BLEEnabledCondition : public Condition<Ts...> {
|
||||
public:
|
||||
bool check(const Ts &...x) override { return global_ble->is_active(); }
|
||||
bool check(const Ts &...x) override { return global_ble != nullptr && global_ble->is_active(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class BLEEnableAction : public Action<Ts...> {
|
||||
public:
|
||||
void play(const Ts &...x) override { global_ble->enable(); }
|
||||
void play(const Ts &...x) override {
|
||||
if (global_ble != nullptr)
|
||||
global_ble->enable();
|
||||
}
|
||||
};
|
||||
|
||||
template<typename... Ts> class BLEDisableAction : public Action<Ts...> {
|
||||
public:
|
||||
void play(const Ts &...x) override { global_ble->disable(); }
|
||||
void play(const Ts &...x) override {
|
||||
if (global_ble != nullptr)
|
||||
global_ble->disable();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::esp32_ble
|
||||
|
||||
@@ -143,9 +143,8 @@ bool ESPBTUUID::operator==(const ESPBTUUID &uuid) const {
|
||||
return this->as_128bit() == uuid.as_128bit();
|
||||
}
|
||||
esp_bt_uuid_t ESPBTUUID::get_uuid() const { return this->uuid_; }
|
||||
std::string ESPBTUUID::to_string() const {
|
||||
char buf[40]; // Enough for 128-bit UUID with dashes
|
||||
char *pos = buf;
|
||||
void ESPBTUUID::to_str(std::span<char, UUID_STR_LEN> output) const {
|
||||
char *pos = output.data();
|
||||
|
||||
switch (this->uuid_.len) {
|
||||
case ESP_UUID_LEN_16:
|
||||
@@ -156,7 +155,7 @@ std::string ESPBTUUID::to_string() const {
|
||||
*pos++ = format_hex_pretty_char((this->uuid_.uuid.uuid16 >> 4) & 0x0F);
|
||||
*pos++ = format_hex_pretty_char(this->uuid_.uuid.uuid16 & 0x0F);
|
||||
*pos = '\0';
|
||||
return std::string(buf);
|
||||
return;
|
||||
|
||||
case ESP_UUID_LEN_32:
|
||||
*pos++ = '0';
|
||||
@@ -165,7 +164,7 @@ std::string ESPBTUUID::to_string() const {
|
||||
*pos++ = format_hex_pretty_char((this->uuid_.uuid.uuid32 >> shift) & 0x0F);
|
||||
}
|
||||
*pos = '\0';
|
||||
return std::string(buf);
|
||||
return;
|
||||
|
||||
default:
|
||||
case ESP_UUID_LEN_128:
|
||||
@@ -179,9 +178,13 @@ std::string ESPBTUUID::to_string() const {
|
||||
}
|
||||
}
|
||||
*pos = '\0';
|
||||
return std::string(buf);
|
||||
return;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
std::string ESPBTUUID::to_string() const {
|
||||
char buf[UUID_STR_LEN];
|
||||
this->to_str(buf);
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
} // namespace esphome::esp32_ble
|
||||
|
||||
@@ -7,11 +7,15 @@
|
||||
#ifdef USE_ESP32
|
||||
#ifdef USE_ESP32_BLE_UUID
|
||||
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <esp_bt_defs.h>
|
||||
|
||||
namespace esphome::esp32_ble {
|
||||
|
||||
/// Buffer size for UUID string: "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX\0"
|
||||
static constexpr size_t UUID_STR_LEN = 37;
|
||||
|
||||
class ESPBTUUID {
|
||||
public:
|
||||
ESPBTUUID();
|
||||
@@ -37,6 +41,7 @@ class ESPBTUUID {
|
||||
esp_bt_uuid_t get_uuid() const;
|
||||
|
||||
std::string to_string() const;
|
||||
void to_str(std::span<char, UUID_STR_LEN> output) const;
|
||||
|
||||
protected:
|
||||
esp_bt_uuid_t uuid_;
|
||||
|
||||
@@ -50,8 +50,12 @@ void BLECharacteristic::parse_descriptors() {
|
||||
desc->handle = result.handle;
|
||||
desc->characteristic = this;
|
||||
this->descriptors.push_back(desc);
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char uuid_buf[espbt::UUID_STR_LEN];
|
||||
desc->uuid.to_str(uuid_buf);
|
||||
ESP_LOGV(TAG, "[%d] [%s] descriptor %s, handle 0x%x", this->service->client->get_connection_index(),
|
||||
this->service->client->address_str(), desc->uuid.to_string().c_str(), desc->handle);
|
||||
this->service->client->address_str(), uuid_buf, desc->handle);
|
||||
#endif
|
||||
offset++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -411,12 +411,15 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
this->update_conn_params_(MEDIUM_MIN_CONN_INTERVAL, MEDIUM_MAX_CONN_INTERVAL, 0, MEDIUM_CONN_TIMEOUT, "medium");
|
||||
} else if (this->connection_type_ != espbt::ConnectionType::V3_WITH_CACHE) {
|
||||
#ifdef USE_ESP32_BLE_DEVICE
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
for (auto &svc : this->services_) {
|
||||
ESP_LOGV(TAG, "[%d] [%s] Service UUID: %s", this->connection_index_, this->address_str_,
|
||||
svc->uuid.to_string().c_str());
|
||||
char uuid_buf[espbt::UUID_STR_LEN];
|
||||
svc->uuid.to_str(uuid_buf);
|
||||
ESP_LOGV(TAG, "[%d] [%s] Service UUID: %s", this->connection_index_, this->address_str_, uuid_buf);
|
||||
ESP_LOGV(TAG, "[%d] [%s] start_handle: 0x%x end_handle: 0x%x", this->connection_index_, this->address_str_,
|
||||
svc->start_handle, svc->end_handle);
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
ESP_LOGI(TAG, "[%d] [%s] Service discovery complete", this->connection_index_, this->address_str_);
|
||||
|
||||
@@ -64,9 +64,12 @@ void BLEService::parse_characteristics() {
|
||||
characteristic->handle = result.char_handle;
|
||||
characteristic->service = this;
|
||||
this->characteristics.push_back(characteristic);
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char uuid_buf[espbt::UUID_STR_LEN];
|
||||
characteristic->uuid.to_str(uuid_buf);
|
||||
ESP_LOGV(TAG, "[%d] [%s] characteristic %s, handle 0x%x, properties 0x%x", this->client->get_connection_index(),
|
||||
this->client->address_str(), characteristic->uuid.to_string().c_str(), characteristic->handle,
|
||||
characteristic->properties);
|
||||
this->client->address_str(), uuid_buf, characteristic->handle, characteristic->properties);
|
||||
#endif
|
||||
offset++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +109,11 @@ void BLECharacteristic::do_create(BLEService *service) {
|
||||
esp_attr_control_t control;
|
||||
control.auto_rsp = ESP_GATT_RSP_BY_APP;
|
||||
|
||||
ESP_LOGV(TAG, "Creating characteristic - %s", this->uuid_.to_string().c_str());
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char uuid_buf[esp32_ble::UUID_STR_LEN];
|
||||
this->uuid_.to_str(uuid_buf);
|
||||
ESP_LOGV(TAG, "Creating characteristic - %s", uuid_buf);
|
||||
#endif
|
||||
|
||||
esp_bt_uuid_t uuid = this->uuid_.get_uuid();
|
||||
esp_err_t err = esp_ble_gatts_add_char(service->get_handle(), &uuid, static_cast<esp_gatt_perm_t>(this->permissions_),
|
||||
|
||||
@@ -34,7 +34,11 @@ void BLEDescriptor::do_create(BLECharacteristic *characteristic) {
|
||||
esp_attr_control_t control;
|
||||
control.auto_rsp = ESP_GATT_AUTO_RSP;
|
||||
|
||||
ESP_LOGV(TAG, "Creating descriptor - %s", this->uuid_.to_string().c_str());
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char uuid_buf[esp32_ble::UUID_STR_LEN];
|
||||
this->uuid_.to_str(uuid_buf);
|
||||
ESP_LOGV(TAG, "Creating descriptor - %s", uuid_buf);
|
||||
#endif
|
||||
esp_bt_uuid_t uuid = this->uuid_.get_uuid();
|
||||
esp_err_t err = esp_ble_gatts_add_char_descr(this->characteristic_->get_service()->get_handle(), &uuid,
|
||||
this->permissions_, &this->value_, &control);
|
||||
|
||||
@@ -106,7 +106,11 @@ void BLEServer::restart_advertising_() {
|
||||
}
|
||||
|
||||
BLEService *BLEServer::create_service(ESPBTUUID uuid, bool advertise, uint16_t num_handles) {
|
||||
ESP_LOGV(TAG, "Creating BLE service - %s", uuid.to_string().c_str());
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char uuid_buf[esp32_ble::UUID_STR_LEN];
|
||||
uuid.to_str(uuid_buf);
|
||||
ESP_LOGV(TAG, "Creating BLE service - %s", uuid_buf);
|
||||
#endif
|
||||
// Calculate the inst_id for the service
|
||||
uint8_t inst_id = 0;
|
||||
for (; inst_id < 0xFF; inst_id++) {
|
||||
@@ -115,7 +119,9 @@ BLEService *BLEServer::create_service(ESPBTUUID uuid, bool advertise, uint16_t n
|
||||
}
|
||||
}
|
||||
if (inst_id == 0xFF) {
|
||||
ESP_LOGW(TAG, "Could not create BLE service %s, too many instances", uuid.to_string().c_str());
|
||||
char warn_uuid_buf[esp32_ble::UUID_STR_LEN];
|
||||
uuid.to_str(warn_uuid_buf);
|
||||
ESP_LOGW(TAG, "Could not create BLE service %s, too many instances", warn_uuid_buf);
|
||||
return nullptr;
|
||||
}
|
||||
BLEService *service = // NOLINT(cppcoreguidelines-owning-memory)
|
||||
@@ -128,7 +134,11 @@ BLEService *BLEServer::create_service(ESPBTUUID uuid, bool advertise, uint16_t n
|
||||
}
|
||||
|
||||
void BLEServer::remove_service(ESPBTUUID uuid, uint8_t inst_id) {
|
||||
ESP_LOGV(TAG, "Removing BLE service - %s %d", uuid.to_string().c_str(), inst_id);
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char uuid_buf[esp32_ble::UUID_STR_LEN];
|
||||
uuid.to_str(uuid_buf);
|
||||
ESP_LOGV(TAG, "Removing BLE service - %s %d", uuid_buf, inst_id);
|
||||
#endif
|
||||
for (auto it = this->services_.begin(); it != this->services_.end(); ++it) {
|
||||
if (it->uuid == uuid && it->inst_id == inst_id) {
|
||||
it->service->do_delete();
|
||||
@@ -137,7 +147,9 @@ void BLEServer::remove_service(ESPBTUUID uuid, uint8_t inst_id) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
ESP_LOGW(TAG, "BLE service %s %d does not exist", uuid.to_string().c_str(), inst_id);
|
||||
char warn_uuid_buf[esp32_ble::UUID_STR_LEN];
|
||||
uuid.to_str(warn_uuid_buf);
|
||||
ESP_LOGW(TAG, "BLE service %s %d does not exist", warn_uuid_buf, inst_id);
|
||||
}
|
||||
|
||||
BLEService *BLEServer::get_service(ESPBTUUID uuid, uint8_t inst_id) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import logging
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import esp32_ble
|
||||
from esphome.components import esp32_ble, ota
|
||||
from esphome.components.esp32 import add_idf_sdkconfig_option
|
||||
from esphome.components.esp32_ble import (
|
||||
IDF_MAX_CONNECTIONS,
|
||||
@@ -328,7 +328,7 @@ async def to_code(config):
|
||||
# Note: CONFIG_BT_ACL_CONNECTIONS and CONFIG_BTDM_CTRL_BLE_MAX_CONN are now
|
||||
# configured in esp32_ble component based on max_connections setting
|
||||
|
||||
cg.add_define("USE_OTA_STATE_CALLBACK") # To be notified when an OTA update starts
|
||||
ota.request_ota_state_listeners() # To be notified when an OTA update starts
|
||||
cg.add_define("USE_ESP32_BLE_CLIENT")
|
||||
|
||||
CORE.add_job(_add_ble_features)
|
||||
|
||||
@@ -71,21 +71,24 @@ void ESP32BLETracker::setup() {
|
||||
|
||||
global_esp32_ble_tracker = this;
|
||||
|
||||
#ifdef USE_OTA
|
||||
ota::get_global_ota_callback()->add_on_state_callback(
|
||||
[this](ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) {
|
||||
if (state == ota::OTA_STARTED) {
|
||||
this->stop_scan();
|
||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
|
||||
for (auto *client : this->clients_) {
|
||||
client->disconnect();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
});
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
ota::get_global_ota_callback()->add_global_state_listener(this);
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
void ESP32BLETracker::on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) {
|
||||
if (state == ota::OTA_STARTED) {
|
||||
this->stop_scan();
|
||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
|
||||
for (auto *client : this->clients_) {
|
||||
client->disconnect();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void ESP32BLETracker::loop() {
|
||||
if (!this->parent_->is_active()) {
|
||||
this->ble_was_disabled_ = true;
|
||||
@@ -185,7 +188,10 @@ void ESP32BLETracker::ble_before_disabled_event_handler() { this->stop_scan_();
|
||||
|
||||
void ESP32BLETracker::stop_scan_() {
|
||||
if (this->scanner_state_ != ScannerState::RUNNING && this->scanner_state_ != ScannerState::FAILED) {
|
||||
ESP_LOGE(TAG, "Cannot stop scan: %s", this->scanner_state_to_string_(this->scanner_state_));
|
||||
// If scanner is already idle, there's nothing to stop - this is not an error
|
||||
if (this->scanner_state_ != ScannerState::IDLE) {
|
||||
ESP_LOGE(TAG, "Cannot stop scan: %s", this->scanner_state_to_string_(this->scanner_state_));
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Reset timeout state machine when stopping scan
|
||||
@@ -435,24 +441,31 @@ void ESPBTDevice::parse_scan_rst(const BLEScanResult &scan_result) {
|
||||
ESP_LOGVV(TAG, " Ad Flag: %u", *this->ad_flag_);
|
||||
}
|
||||
for (auto &uuid : this->service_uuids_) {
|
||||
ESP_LOGVV(TAG, " Service UUID: %s", uuid.to_string().c_str());
|
||||
char uuid_buf[esp32_ble::UUID_STR_LEN];
|
||||
uuid.to_str(uuid_buf);
|
||||
ESP_LOGVV(TAG, " Service UUID: %s", uuid_buf);
|
||||
}
|
||||
for (auto &data : this->manufacturer_datas_) {
|
||||
auto ibeacon = ESPBLEiBeacon::from_manufacturer_data(data);
|
||||
if (ibeacon.has_value()) {
|
||||
ESP_LOGVV(TAG, " Manufacturer iBeacon:");
|
||||
ESP_LOGVV(TAG, " UUID: %s", ibeacon.value().get_uuid().to_string().c_str());
|
||||
char uuid_buf[esp32_ble::UUID_STR_LEN];
|
||||
ibeacon.value().get_uuid().to_str(uuid_buf);
|
||||
ESP_LOGVV(TAG, " UUID: %s", uuid_buf);
|
||||
ESP_LOGVV(TAG, " Major: %u", ibeacon.value().get_major());
|
||||
ESP_LOGVV(TAG, " Minor: %u", ibeacon.value().get_minor());
|
||||
ESP_LOGVV(TAG, " TXPower: %d", ibeacon.value().get_signal_power());
|
||||
} else {
|
||||
ESP_LOGVV(TAG, " Manufacturer ID: %s, data: %s", data.uuid.to_string().c_str(),
|
||||
format_hex_pretty(data.data).c_str());
|
||||
char uuid_buf[esp32_ble::UUID_STR_LEN];
|
||||
data.uuid.to_str(uuid_buf);
|
||||
ESP_LOGVV(TAG, " Manufacturer ID: %s, data: %s", uuid_buf, format_hex_pretty(data.data).c_str());
|
||||
}
|
||||
}
|
||||
for (auto &data : this->service_datas_) {
|
||||
ESP_LOGVV(TAG, " Service data:");
|
||||
ESP_LOGVV(TAG, " UUID: %s", data.uuid.to_string().c_str());
|
||||
char uuid_buf[esp32_ble::UUID_STR_LEN];
|
||||
data.uuid.to_str(uuid_buf);
|
||||
ESP_LOGVV(TAG, " UUID: %s", uuid_buf);
|
||||
ESP_LOGVV(TAG, " Data: %s", format_hex_pretty(data.data).c_str());
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,10 @@
|
||||
#include "esphome/components/esp32_ble/ble_uuid.h"
|
||||
#include "esphome/components/esp32_ble/ble_scan_result.h"
|
||||
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
#include "esphome/components/ota/ota_backend.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::esp32_ble_tracker {
|
||||
|
||||
using namespace esp32_ble;
|
||||
@@ -241,6 +245,9 @@ class ESP32BLETracker : public Component,
|
||||
public GAPScanEventHandler,
|
||||
public GATTcEventHandler,
|
||||
public BLEStatusEventHandler,
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
public ota::OTAGlobalStateListener,
|
||||
#endif
|
||||
public Parented<ESP32BLE> {
|
||||
public:
|
||||
void set_scan_duration(uint32_t scan_duration) { scan_duration_ = scan_duration; }
|
||||
@@ -274,6 +281,10 @@ class ESP32BLETracker : public Component,
|
||||
void gap_scan_event_handler(const BLEScanResult &scan_result) override;
|
||||
void ble_before_disabled_event_handler() override;
|
||||
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
void on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) override;
|
||||
#endif
|
||||
|
||||
/// Add a listener for scanner state changes
|
||||
void add_scanner_state_listener(BLEScannerStateListener *listener) {
|
||||
this->scanner_state_listeners_.push_back(listener);
|
||||
|
||||
@@ -41,10 +41,6 @@ static constexpr size_t SHA256_HEX_SIZE = 64; // SHA256 hash as hex string (32
|
||||
#endif // USE_OTA_PASSWORD
|
||||
|
||||
void ESPHomeOTAComponent::setup() {
|
||||
#ifdef USE_OTA_STATE_CALLBACK
|
||||
ota::register_ota_platform(this);
|
||||
#endif
|
||||
|
||||
this->server_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections
|
||||
if (this->server_ == nullptr) {
|
||||
this->log_socket_error_(LOG_STR("creation"));
|
||||
@@ -297,8 +293,8 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
// accidentally trigger the update process.
|
||||
this->log_start_(LOG_STR("update"));
|
||||
this->status_set_warning();
|
||||
#ifdef USE_OTA_STATE_CALLBACK
|
||||
this->state_callback_.call(ota::OTA_STARTED, 0.0f, 0);
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
this->notify_state_(ota::OTA_STARTED, 0.0f, 0);
|
||||
#endif
|
||||
|
||||
// This will block for a few seconds as it locks flash
|
||||
@@ -357,8 +353,8 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
last_progress = now;
|
||||
float percentage = (total * 100.0f) / ota_size;
|
||||
ESP_LOGD(TAG, "Progress: %0.1f%%", percentage);
|
||||
#ifdef USE_OTA_STATE_CALLBACK
|
||||
this->state_callback_.call(ota::OTA_IN_PROGRESS, percentage, 0);
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
this->notify_state_(ota::OTA_IN_PROGRESS, percentage, 0);
|
||||
#endif
|
||||
// feed watchdog and give other tasks a chance to run
|
||||
this->yield_and_feed_watchdog_();
|
||||
@@ -387,8 +383,8 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
delay(10);
|
||||
ESP_LOGI(TAG, "Update complete");
|
||||
this->status_clear_warning();
|
||||
#ifdef USE_OTA_STATE_CALLBACK
|
||||
this->state_callback_.call(ota::OTA_COMPLETED, 100.0f, 0);
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
this->notify_state_(ota::OTA_COMPLETED, 100.0f, 0);
|
||||
#endif
|
||||
delay(100); // NOLINT
|
||||
App.safe_reboot();
|
||||
@@ -402,8 +398,8 @@ error:
|
||||
}
|
||||
|
||||
this->status_momentary_error("err", 5000);
|
||||
#ifdef USE_OTA_STATE_CALLBACK
|
||||
this->state_callback_.call(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
this->notify_state_(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ class FanTraits {
|
||||
bool supports_preset_modes() const { return !this->preset_modes_.empty(); }
|
||||
/// Find and return the matching preset mode pointer from supported modes, or nullptr if not found.
|
||||
const char *find_preset_mode(const char *preset_mode) const {
|
||||
return this->find_preset_mode(preset_mode, strlen(preset_mode));
|
||||
return this->find_preset_mode(preset_mode, preset_mode ? strlen(preset_mode) : 0);
|
||||
}
|
||||
const char *find_preset_mode(const char *preset_mode, size_t len) const {
|
||||
if (preset_mode == nullptr || len == 0)
|
||||
|
||||
@@ -63,7 +63,7 @@ void HM3301Component::update() {
|
||||
|
||||
int16_t aqi_value = -1;
|
||||
if (this->aqi_sensor_ != nullptr && pm_2_5_value != -1 && pm_10_0_value != -1) {
|
||||
AbstractAQICalculator *calculator = this->aqi_calculator_factory_.get_calculator(this->aqi_calc_type_);
|
||||
aqi::AbstractAQICalculator *calculator = this->aqi_calculator_factory_.get_calculator(this->aqi_calc_type_);
|
||||
aqi_value = calculator->get_aqi(pm_2_5_value, pm_10_0_value);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "aqi_calculator_factory.h"
|
||||
#include "esphome/components/aqi/aqi_calculator_factory.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace hm3301 {
|
||||
@@ -19,7 +19,7 @@ class HM3301Component : public PollingComponent, public i2c::I2CDevice {
|
||||
void set_pm_10_0_sensor(sensor::Sensor *pm_10_0_sensor) { pm_10_0_sensor_ = pm_10_0_sensor; }
|
||||
void set_aqi_sensor(sensor::Sensor *aqi_sensor) { aqi_sensor_ = aqi_sensor; }
|
||||
|
||||
void set_aqi_calculation_type(AQICalculatorType aqi_calc_type) { aqi_calc_type_ = aqi_calc_type; }
|
||||
void set_aqi_calculation_type(aqi::AQICalculatorType aqi_calc_type) { aqi_calc_type_ = aqi_calc_type; }
|
||||
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
@@ -41,8 +41,8 @@ class HM3301Component : public PollingComponent, public i2c::I2CDevice {
|
||||
sensor::Sensor *pm_10_0_sensor_{nullptr};
|
||||
sensor::Sensor *aqi_sensor_{nullptr};
|
||||
|
||||
AQICalculatorType aqi_calc_type_;
|
||||
AQICalculatorFactory aqi_calculator_factory_ = AQICalculatorFactory();
|
||||
aqi::AQICalculatorType aqi_calc_type_;
|
||||
aqi::AQICalculatorFactory aqi_calculator_factory_ = aqi::AQICalculatorFactory();
|
||||
|
||||
bool validate_checksum_(const uint8_t *data);
|
||||
uint16_t get_sensor_value_(const uint8_t *data, uint8_t i);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, sensor
|
||||
from esphome.components.aqi import AQI_CALCULATION_TYPE, CONF_AQI, CONF_CALCULATION_TYPE
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
@@ -16,23 +17,16 @@ from esphome.const import (
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
AUTO_LOAD = ["aqi"]
|
||||
CODEOWNERS = ["@freekode"]
|
||||
|
||||
hm3301_ns = cg.esphome_ns.namespace("hm3301")
|
||||
HM3301Component = hm3301_ns.class_(
|
||||
"HM3301Component", cg.PollingComponent, i2c.I2CDevice
|
||||
)
|
||||
AQICalculatorType = hm3301_ns.enum("AQICalculatorType")
|
||||
|
||||
CONF_AQI = "aqi"
|
||||
CONF_CALCULATION_TYPE = "calculation_type"
|
||||
UNIT_INDEX = "index"
|
||||
|
||||
AQI_CALCULATION_TYPE = {
|
||||
"CAQI": AQICalculatorType.CAQI_TYPE,
|
||||
"AQI": AQICalculatorType.AQI_TYPE,
|
||||
}
|
||||
|
||||
|
||||
def _validate(config):
|
||||
if CONF_AQI in config and CONF_PM_2_5 not in config:
|
||||
|
||||
@@ -16,12 +16,6 @@ namespace http_request {
|
||||
|
||||
static const char *const TAG = "http_request.ota";
|
||||
|
||||
void OtaHttpRequestComponent::setup() {
|
||||
#ifdef USE_OTA_STATE_CALLBACK
|
||||
ota::register_ota_platform(this);
|
||||
#endif
|
||||
}
|
||||
|
||||
void OtaHttpRequestComponent::dump_config() { ESP_LOGCONFIG(TAG, "Over-The-Air updates via HTTP request"); };
|
||||
|
||||
void OtaHttpRequestComponent::set_md5_url(const std::string &url) {
|
||||
@@ -48,24 +42,24 @@ void OtaHttpRequestComponent::flash() {
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Starting update");
|
||||
#ifdef USE_OTA_STATE_CALLBACK
|
||||
this->state_callback_.call(ota::OTA_STARTED, 0.0f, 0);
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
this->notify_state_(ota::OTA_STARTED, 0.0f, 0);
|
||||
#endif
|
||||
|
||||
auto ota_status = this->do_ota_();
|
||||
|
||||
switch (ota_status) {
|
||||
case ota::OTA_RESPONSE_OK:
|
||||
#ifdef USE_OTA_STATE_CALLBACK
|
||||
this->state_callback_.call(ota::OTA_COMPLETED, 100.0f, ota_status);
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
this->notify_state_(ota::OTA_COMPLETED, 100.0f, ota_status);
|
||||
#endif
|
||||
delay(10);
|
||||
App.safe_reboot();
|
||||
break;
|
||||
|
||||
default:
|
||||
#ifdef USE_OTA_STATE_CALLBACK
|
||||
this->state_callback_.call(ota::OTA_ERROR, 0.0f, ota_status);
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
this->notify_state_(ota::OTA_ERROR, 0.0f, ota_status);
|
||||
#endif
|
||||
this->md5_computed_.clear(); // will be reset at next attempt
|
||||
this->md5_expected_.clear(); // will be reset at next attempt
|
||||
@@ -165,8 +159,8 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
|
||||
last_progress = now;
|
||||
float percentage = container->get_bytes_read() * 100.0f / container->content_length;
|
||||
ESP_LOGD(TAG, "Progress: %0.1f%%", percentage);
|
||||
#ifdef USE_OTA_STATE_CALLBACK
|
||||
this->state_callback_.call(ota::OTA_IN_PROGRESS, percentage, 0);
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
this->notify_state_(ota::OTA_IN_PROGRESS, percentage, 0);
|
||||
#endif
|
||||
}
|
||||
} // while
|
||||
|
||||
@@ -24,7 +24,6 @@ enum OtaHttpRequestError : uint8_t {
|
||||
|
||||
class OtaHttpRequestComponent : public ota::OTAComponent, public Parented<HttpRequestComponent> {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import update
|
||||
from esphome.components import ota, update
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_SOURCE
|
||||
|
||||
@@ -38,6 +38,6 @@ async def to_code(config):
|
||||
|
||||
cg.add(var.set_source_url(config[CONF_SOURCE]))
|
||||
|
||||
cg.add_define("USE_OTA_STATE_CALLBACK")
|
||||
ota.request_ota_state_listeners()
|
||||
|
||||
await cg.register_component(var, config)
|
||||
|
||||
@@ -20,19 +20,19 @@ static const char *const TAG = "http_request.update";
|
||||
|
||||
static const size_t MAX_READ_SIZE = 256;
|
||||
|
||||
void HttpRequestUpdate::setup() {
|
||||
this->ota_parent_->add_on_state_callback([this](ota::OTAState state, float progress, uint8_t err) {
|
||||
if (state == ota::OTAState::OTA_IN_PROGRESS) {
|
||||
this->state_ = update::UPDATE_STATE_INSTALLING;
|
||||
this->update_info_.has_progress = true;
|
||||
this->update_info_.progress = progress;
|
||||
this->publish_state();
|
||||
} else if (state == ota::OTAState::OTA_ABORT || state == ota::OTAState::OTA_ERROR) {
|
||||
this->state_ = update::UPDATE_STATE_AVAILABLE;
|
||||
this->status_set_error(LOG_STR("Failed to install firmware"));
|
||||
this->publish_state();
|
||||
}
|
||||
});
|
||||
void HttpRequestUpdate::setup() { this->ota_parent_->add_state_listener(this); }
|
||||
|
||||
void HttpRequestUpdate::on_ota_state(ota::OTAState state, float progress, uint8_t error) {
|
||||
if (state == ota::OTAState::OTA_IN_PROGRESS) {
|
||||
this->state_ = update::UPDATE_STATE_INSTALLING;
|
||||
this->update_info_.has_progress = true;
|
||||
this->update_info_.progress = progress;
|
||||
this->publish_state();
|
||||
} else if (state == ota::OTAState::OTA_ABORT || state == ota::OTAState::OTA_ERROR) {
|
||||
this->state_ = update::UPDATE_STATE_AVAILABLE;
|
||||
this->status_set_error(LOG_STR("Failed to install firmware"));
|
||||
this->publish_state();
|
||||
}
|
||||
}
|
||||
|
||||
void HttpRequestUpdate::update() {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
namespace esphome {
|
||||
namespace http_request {
|
||||
|
||||
class HttpRequestUpdate : public update::UpdateEntity, public PollingComponent {
|
||||
class HttpRequestUpdate final : public update::UpdateEntity, public PollingComponent, public ota::OTAStateListener {
|
||||
public:
|
||||
void setup() override;
|
||||
void update() override;
|
||||
@@ -29,6 +29,8 @@ class HttpRequestUpdate : public update::UpdateEntity, public PollingComponent {
|
||||
|
||||
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
|
||||
|
||||
void on_ota_state(ota::OTAState state, float progress, uint8_t error) override;
|
||||
|
||||
protected:
|
||||
HttpRequestComponent *request_parent_;
|
||||
OtaHttpRequestComponent *ota_parent_;
|
||||
|
||||
@@ -120,10 +120,10 @@ class LibreTinyPreferences : public ESPPreferences {
|
||||
// go through vector from back to front (makes erase easier/more efficient)
|
||||
for (ssize_t i = s_pending_save.size() - 1; i >= 0; i--) {
|
||||
const auto &save = s_pending_save[i];
|
||||
ESP_LOGVV(TAG, "Checking if FDB data %" PRIu32 " has changed", save.key);
|
||||
if (this->is_changed(&this->db, save)) {
|
||||
char key_str[KEY_BUFFER_SIZE];
|
||||
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
|
||||
char key_str[KEY_BUFFER_SIZE];
|
||||
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
|
||||
ESP_LOGVV(TAG, "Checking if FDB data %s has changed", key_str);
|
||||
if (this->is_changed_(&this->db, save, key_str)) {
|
||||
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.len);
|
||||
fdb_blob_make(&this->blob, save.data.get(), save.len);
|
||||
fdb_err_t err = fdb_kv_set_blob(&this->db, key_str, &this->blob);
|
||||
@@ -150,10 +150,8 @@ class LibreTinyPreferences : public ESPPreferences {
|
||||
return failed == 0;
|
||||
}
|
||||
|
||||
bool is_changed(const fdb_kvdb_t db, const NVSData &to_save) {
|
||||
char key_str[KEY_BUFFER_SIZE];
|
||||
snprintf(key_str, sizeof(key_str), "%" PRIu32, to_save.key);
|
||||
|
||||
protected:
|
||||
bool is_changed_(fdb_kvdb_t db, const NVSData &to_save, const char *key_str) {
|
||||
struct fdb_kv kv;
|
||||
fdb_kv_t kvp = fdb_kv_get_obj(db, key_str, &kv);
|
||||
if (kvp == nullptr) {
|
||||
|
||||
@@ -7,7 +7,7 @@ from urllib.parse import urljoin
|
||||
from esphome import automation, external_files, git
|
||||
from esphome.automation import register_action, register_condition
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import esp32, microphone, socket
|
||||
from esphome.components import esp32, microphone, ota, socket
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_FILE,
|
||||
@@ -452,7 +452,7 @@ async def to_code(config):
|
||||
cg.add(var.set_microphone_source(mic_source))
|
||||
|
||||
cg.add_define("USE_MICRO_WAKE_WORD")
|
||||
cg.add_define("USE_OTA_STATE_CALLBACK")
|
||||
ota.request_ota_state_listeners()
|
||||
|
||||
esp32.add_idf_component(name="espressif/esp-tflite-micro", ref="1.3.3~1")
|
||||
|
||||
|
||||
@@ -119,18 +119,21 @@ void MicroWakeWord::setup() {
|
||||
}
|
||||
});
|
||||
|
||||
#ifdef USE_OTA
|
||||
ota::get_global_ota_callback()->add_on_state_callback(
|
||||
[this](ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) {
|
||||
if (state == ota::OTA_STARTED) {
|
||||
this->suspend_task_();
|
||||
} else if (state == ota::OTA_ERROR) {
|
||||
this->resume_task_();
|
||||
}
|
||||
});
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
ota::get_global_ota_callback()->add_global_state_listener(this);
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
void MicroWakeWord::on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) {
|
||||
if (state == ota::OTA_STARTED) {
|
||||
this->suspend_task_();
|
||||
} else if (state == ota::OTA_ERROR) {
|
||||
this->resume_task_();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void MicroWakeWord::inference_task(void *params) {
|
||||
MicroWakeWord *this_mww = (MicroWakeWord *) params;
|
||||
|
||||
|
||||
@@ -9,8 +9,13 @@
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/ring_buffer.h"
|
||||
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
#include "esphome/components/ota/ota_backend.h"
|
||||
#endif
|
||||
|
||||
#include <freertos/event_groups.h>
|
||||
|
||||
#include <frontend.h>
|
||||
@@ -26,13 +31,22 @@ enum State {
|
||||
STOPPED,
|
||||
};
|
||||
|
||||
class MicroWakeWord : public Component {
|
||||
class MicroWakeWord : public Component
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
,
|
||||
public ota::OTAGlobalStateListener
|
||||
#endif
|
||||
{
|
||||
public:
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
float get_setup_priority() const override;
|
||||
void dump_config() override;
|
||||
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
void on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) override;
|
||||
#endif
|
||||
|
||||
void start();
|
||||
void stop();
|
||||
|
||||
|
||||
@@ -83,6 +83,7 @@ void MMC5603Component::dump_config() {
|
||||
ESP_LOGE(TAG, "The ID registers don't match - Is this really an MMC5603?");
|
||||
}
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
ESP_LOGCONFIG(TAG, " Auto set/reset: %s", ONOFF(this->auto_set_reset_));
|
||||
|
||||
LOG_SENSOR(" ", "X Axis", this->x_sensor_);
|
||||
LOG_SENSOR(" ", "Y Axis", this->y_sensor_);
|
||||
@@ -93,7 +94,8 @@ void MMC5603Component::dump_config() {
|
||||
float MMC5603Component::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
void MMC5603Component::update() {
|
||||
if (!this->write_byte(MMC56X3_CTRL0_REG, 0x01)) {
|
||||
uint8_t ctrl0 = (this->auto_set_reset_) ? 0x21 : 0x01;
|
||||
if (!this->write_byte(MMC56X3_CTRL0_REG, ctrl0)) {
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ class MMC5603Component : public PollingComponent, public i2c::I2CDevice {
|
||||
void set_y_sensor(sensor::Sensor *y_sensor) { y_sensor_ = y_sensor; }
|
||||
void set_z_sensor(sensor::Sensor *z_sensor) { z_sensor_ = z_sensor; }
|
||||
void set_heading_sensor(sensor::Sensor *heading_sensor) { heading_sensor_ = heading_sensor; }
|
||||
void set_auto_set_reset(bool auto_set_reset) { auto_set_reset_ = auto_set_reset; }
|
||||
|
||||
protected:
|
||||
MMC5603Datarate datarate_;
|
||||
@@ -32,6 +33,7 @@ class MMC5603Component : public PollingComponent, public i2c::I2CDevice {
|
||||
sensor::Sensor *y_sensor_{nullptr};
|
||||
sensor::Sensor *z_sensor_{nullptr};
|
||||
sensor::Sensor *heading_sensor_{nullptr};
|
||||
bool auto_set_reset_{true};
|
||||
enum ErrorCode {
|
||||
NONE = 0,
|
||||
COMMUNICATION_FAILED,
|
||||
|
||||
@@ -16,6 +16,8 @@ from esphome.const import (
|
||||
UNIT_MICROTESLA,
|
||||
)
|
||||
|
||||
CONF_AUTO_SET_RESET = "auto_set_reset"
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
mmc5603_ns = cg.esphome_ns.namespace("mmc5603")
|
||||
@@ -54,6 +56,7 @@ CONFIG_SCHEMA = (
|
||||
cv.Optional(CONF_FIELD_STRENGTH_Y): field_strength_schema,
|
||||
cv.Optional(CONF_FIELD_STRENGTH_Z): field_strength_schema,
|
||||
cv.Optional(CONF_HEADING): heading_schema,
|
||||
cv.Optional(CONF_AUTO_SET_RESET, default=True): cv.boolean,
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
@@ -88,3 +91,5 @@ async def to_code(config):
|
||||
if CONF_HEADING in config:
|
||||
sens = await sensor.new_sensor(config[CONF_HEADING])
|
||||
cg.add(var.set_heading_sensor(sens))
|
||||
if CONF_AUTO_SET_RESET in config:
|
||||
cg.add(var.set_auto_set_reset(config[CONF_AUTO_SET_RESET]))
|
||||
|
||||
@@ -154,7 +154,15 @@ bool MQTTComponent::send_discovery_() {
|
||||
device_info[MQTT_DEVICE_MANUFACTURER] =
|
||||
model == nullptr ? ESPHOME_PROJECT_NAME : std::string(ESPHOME_PROJECT_NAME, model - ESPHOME_PROJECT_NAME);
|
||||
#else
|
||||
device_info[MQTT_DEVICE_SW_VERSION] = ESPHOME_VERSION " (" + App.get_compilation_time_ref() + ")";
|
||||
static const char ver_fmt[] PROGMEM = ESPHOME_VERSION " (config hash 0x%08" PRIx32 ")";
|
||||
#ifdef USE_ESP8266
|
||||
char fmt_buf[sizeof(ver_fmt)];
|
||||
strcpy_P(fmt_buf, ver_fmt);
|
||||
const char *fmt = fmt_buf;
|
||||
#else
|
||||
const char *fmt = ver_fmt;
|
||||
#endif
|
||||
device_info[MQTT_DEVICE_SW_VERSION] = str_sprintf(fmt, App.get_config_hash());
|
||||
device_info[MQTT_DEVICE_MODEL] = ESPHOME_BOARD;
|
||||
#if defined(USE_ESP8266) || defined(USE_ESP32)
|
||||
device_info[MQTT_DEVICE_MANUFACTURER] = "Espressif";
|
||||
|
||||
@@ -13,6 +13,8 @@ from esphome.const import (
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
from esphome.coroutine import CoroPriority
|
||||
|
||||
OTA_STATE_LISTENER_KEY = "ota_state_listener"
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
AUTO_LOAD = ["md5", "safe_mode"]
|
||||
|
||||
@@ -86,6 +88,7 @@ BASE_OTA_SCHEMA = cv.Schema(
|
||||
@coroutine_with_priority(CoroPriority.OTA_UPDATES)
|
||||
async def to_code(config):
|
||||
cg.add_define("USE_OTA")
|
||||
CORE.add_job(final_step)
|
||||
|
||||
if CORE.is_rp2040 and CORE.using_arduino:
|
||||
cg.add_library("Updater", None)
|
||||
@@ -119,7 +122,24 @@ async def ota_to_code(var, config):
|
||||
await automation.build_automation(trigger, [(cg.uint8, "x")], conf)
|
||||
use_state_callback = True
|
||||
if use_state_callback:
|
||||
cg.add_define("USE_OTA_STATE_CALLBACK")
|
||||
request_ota_state_listeners()
|
||||
|
||||
|
||||
def request_ota_state_listeners() -> None:
|
||||
"""Request that OTA state listeners be compiled in.
|
||||
|
||||
Components that need to be notified about OTA state changes (start, progress,
|
||||
complete, error) should call this function during their code generation.
|
||||
This enables the add_state_listener() API on OTAComponent.
|
||||
"""
|
||||
CORE.data[OTA_STATE_LISTENER_KEY] = True
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.FINAL)
|
||||
async def final_step():
|
||||
"""Final code generation step to configure optional OTA features."""
|
||||
if CORE.data.get(OTA_STATE_LISTENER_KEY, False):
|
||||
cg.add_define("USE_OTA_STATE_LISTENER")
|
||||
|
||||
|
||||
FILTER_SOURCE_FILES = filter_source_files_from_platform(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#pragma once
|
||||
#ifdef USE_OTA_STATE_CALLBACK
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
#include "ota_backend.h"
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
@@ -7,70 +7,64 @@
|
||||
namespace esphome {
|
||||
namespace ota {
|
||||
|
||||
class OTAStateChangeTrigger : public Trigger<OTAState> {
|
||||
class OTAStateChangeTrigger final : public Trigger<OTAState>, public OTAStateListener {
|
||||
public:
|
||||
explicit OTAStateChangeTrigger(OTAComponent *parent) {
|
||||
parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) {
|
||||
if (!parent->is_failed()) {
|
||||
trigger(state);
|
||||
}
|
||||
});
|
||||
explicit OTAStateChangeTrigger(OTAComponent *parent) : parent_(parent) { parent->add_state_listener(this); }
|
||||
|
||||
void on_ota_state(OTAState state, float progress, uint8_t error) override {
|
||||
if (!this->parent_->is_failed()) {
|
||||
this->trigger(state);
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
OTAComponent *parent_;
|
||||
};
|
||||
|
||||
class OTAStartTrigger : public Trigger<> {
|
||||
template<OTAState State> class OTAStateTrigger final : public Trigger<>, public OTAStateListener {
|
||||
public:
|
||||
explicit OTAStartTrigger(OTAComponent *parent) {
|
||||
parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) {
|
||||
if (state == OTA_STARTED && !parent->is_failed()) {
|
||||
trigger();
|
||||
}
|
||||
});
|
||||
explicit OTAStateTrigger(OTAComponent *parent) : parent_(parent) { parent->add_state_listener(this); }
|
||||
|
||||
void on_ota_state(OTAState state, float progress, uint8_t error) override {
|
||||
if (state == State && !this->parent_->is_failed()) {
|
||||
this->trigger();
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
OTAComponent *parent_;
|
||||
};
|
||||
|
||||
class OTAProgressTrigger : public Trigger<float> {
|
||||
using OTAStartTrigger = OTAStateTrigger<OTA_STARTED>;
|
||||
using OTAEndTrigger = OTAStateTrigger<OTA_COMPLETED>;
|
||||
using OTAAbortTrigger = OTAStateTrigger<OTA_ABORT>;
|
||||
|
||||
class OTAProgressTrigger final : public Trigger<float>, public OTAStateListener {
|
||||
public:
|
||||
explicit OTAProgressTrigger(OTAComponent *parent) {
|
||||
parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) {
|
||||
if (state == OTA_IN_PROGRESS && !parent->is_failed()) {
|
||||
trigger(progress);
|
||||
}
|
||||
});
|
||||
explicit OTAProgressTrigger(OTAComponent *parent) : parent_(parent) { parent->add_state_listener(this); }
|
||||
|
||||
void on_ota_state(OTAState state, float progress, uint8_t error) override {
|
||||
if (state == OTA_IN_PROGRESS && !this->parent_->is_failed()) {
|
||||
this->trigger(progress);
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
OTAComponent *parent_;
|
||||
};
|
||||
|
||||
class OTAEndTrigger : public Trigger<> {
|
||||
class OTAErrorTrigger final : public Trigger<uint8_t>, public OTAStateListener {
|
||||
public:
|
||||
explicit OTAEndTrigger(OTAComponent *parent) {
|
||||
parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) {
|
||||
if (state == OTA_COMPLETED && !parent->is_failed()) {
|
||||
trigger();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
explicit OTAErrorTrigger(OTAComponent *parent) : parent_(parent) { parent->add_state_listener(this); }
|
||||
|
||||
class OTAAbortTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit OTAAbortTrigger(OTAComponent *parent) {
|
||||
parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) {
|
||||
if (state == OTA_ABORT && !parent->is_failed()) {
|
||||
trigger();
|
||||
}
|
||||
});
|
||||
void on_ota_state(OTAState state, float progress, uint8_t error) override {
|
||||
if (state == OTA_ERROR && !this->parent_->is_failed()) {
|
||||
this->trigger(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class OTAErrorTrigger : public Trigger<uint8_t> {
|
||||
public:
|
||||
explicit OTAErrorTrigger(OTAComponent *parent) {
|
||||
parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) {
|
||||
if (state == OTA_ERROR && !parent->is_failed()) {
|
||||
trigger(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
protected:
|
||||
OTAComponent *parent_;
|
||||
};
|
||||
|
||||
} // namespace ota
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace esphome {
|
||||
namespace ota {
|
||||
|
||||
#ifdef USE_OTA_STATE_CALLBACK
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
OTAGlobalCallback *global_ota_callback{nullptr}; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
OTAGlobalCallback *get_global_ota_callback() {
|
||||
@@ -13,7 +13,12 @@ OTAGlobalCallback *get_global_ota_callback() {
|
||||
return global_ota_callback;
|
||||
}
|
||||
|
||||
void register_ota_platform(OTAComponent *ota_caller) { get_global_ota_callback()->register_ota(ota_caller); }
|
||||
void OTAComponent::notify_state_(OTAState state, float progress, uint8_t error) {
|
||||
for (auto *listener : this->state_listeners_) {
|
||||
listener->on_ota_state(state, progress, error);
|
||||
}
|
||||
get_global_ota_callback()->notify_ota_state(state, progress, error, this);
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace ota
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#ifdef USE_OTA_STATE_CALLBACK
|
||||
#include "esphome/core/automation.h"
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
#include <vector>
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
@@ -60,62 +60,75 @@ class OTABackend {
|
||||
virtual bool supports_compression() = 0;
|
||||
};
|
||||
|
||||
class OTAComponent : public Component {
|
||||
#ifdef USE_OTA_STATE_CALLBACK
|
||||
/** Listener interface for OTA state changes.
|
||||
*
|
||||
* Components can implement this interface to receive OTA state updates
|
||||
* without the overhead of std::function callbacks.
|
||||
*/
|
||||
class OTAStateListener {
|
||||
public:
|
||||
void add_on_state_callback(std::function<void(ota::OTAState, float, uint8_t)> &&callback) {
|
||||
this->state_callback_.add(std::move(callback));
|
||||
}
|
||||
virtual ~OTAStateListener() = default;
|
||||
virtual void on_ota_state(OTAState state, float progress, uint8_t error) = 0;
|
||||
};
|
||||
|
||||
class OTAComponent : public Component {
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
public:
|
||||
void add_state_listener(OTAStateListener *listener) { this->state_listeners_.push_back(listener); }
|
||||
|
||||
protected:
|
||||
/** Extended callback manager with deferred call support.
|
||||
void notify_state_(OTAState state, float progress, uint8_t error);
|
||||
|
||||
/** Notify state with deferral to main loop (for thread safety).
|
||||
*
|
||||
* This adds a call_deferred() method for thread-safe execution from other tasks.
|
||||
* This should be used by OTA implementations that run in separate tasks
|
||||
* (like web_server OTA) to ensure listeners execute in the main loop.
|
||||
*/
|
||||
class StateCallbackManager : public CallbackManager<void(OTAState, float, uint8_t)> {
|
||||
public:
|
||||
StateCallbackManager(OTAComponent *component) : component_(component) {}
|
||||
void notify_state_deferred_(OTAState state, float progress, uint8_t error) {
|
||||
this->defer([this, state, progress, error]() { this->notify_state_(state, progress, error); });
|
||||
}
|
||||
|
||||
/** Call callbacks with deferral to main loop (for thread safety).
|
||||
*
|
||||
* This should be used by OTA implementations that run in separate tasks
|
||||
* (like web_server OTA) to ensure callbacks execute in the main loop.
|
||||
*/
|
||||
void call_deferred(ota::OTAState state, float progress, uint8_t error) {
|
||||
component_->defer([this, state, progress, error]() { this->call(state, progress, error); });
|
||||
}
|
||||
|
||||
private:
|
||||
OTAComponent *component_;
|
||||
};
|
||||
|
||||
StateCallbackManager state_callback_{this};
|
||||
std::vector<OTAStateListener *> state_listeners_;
|
||||
#endif
|
||||
};
|
||||
|
||||
#ifdef USE_OTA_STATE_CALLBACK
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
|
||||
/** Listener interface for global OTA state changes (includes OTA component pointer).
|
||||
*
|
||||
* Used by OTAGlobalCallback to aggregate state from multiple OTA components.
|
||||
*/
|
||||
class OTAGlobalStateListener {
|
||||
public:
|
||||
virtual ~OTAGlobalStateListener() = default;
|
||||
virtual void on_ota_global_state(OTAState state, float progress, uint8_t error, OTAComponent *component) = 0;
|
||||
};
|
||||
|
||||
/** Global callback that aggregates OTA state from all OTA components.
|
||||
*
|
||||
* OTA components call notify_ota_state() directly with their pointer,
|
||||
* which forwards the event to all registered global listeners.
|
||||
*/
|
||||
class OTAGlobalCallback {
|
||||
public:
|
||||
void register_ota(OTAComponent *ota_caller) {
|
||||
ota_caller->add_on_state_callback([this, ota_caller](OTAState state, float progress, uint8_t error) {
|
||||
this->state_callback_.call(state, progress, error, ota_caller);
|
||||
});
|
||||
}
|
||||
void add_on_state_callback(std::function<void(OTAState, float, uint8_t, OTAComponent *)> &&callback) {
|
||||
this->state_callback_.add(std::move(callback));
|
||||
void add_global_state_listener(OTAGlobalStateListener *listener) { this->global_listeners_.push_back(listener); }
|
||||
|
||||
void notify_ota_state(OTAState state, float progress, uint8_t error, OTAComponent *component) {
|
||||
for (auto *listener : this->global_listeners_) {
|
||||
listener->on_ota_global_state(state, progress, error, component);
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
CallbackManager<void(OTAState, float, uint8_t, OTAComponent *)> state_callback_{};
|
||||
std::vector<OTAGlobalStateListener *> global_listeners_;
|
||||
};
|
||||
|
||||
OTAGlobalCallback *get_global_ota_callback();
|
||||
void register_ota_platform(OTAComponent *ota_caller);
|
||||
|
||||
// OTA implementations should use:
|
||||
// - state_callback_.call() when already in main loop (e.g., esphome OTA)
|
||||
// - state_callback_.call_deferred() when in separate task (e.g., web_server OTA)
|
||||
// This ensures proper callback execution in all contexts.
|
||||
// - notify_state_() when already in main loop (e.g., esphome OTA)
|
||||
// - notify_state_deferred_() when in separate task (e.g., web_server OTA)
|
||||
// This ensures proper listener execution in all contexts.
|
||||
#endif
|
||||
std::unique_ptr<ota::OTABackend> make_ota_backend();
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ static const uint16_t PMS_CMD_MEASUREMENT_MODE_ACTIVE = 0x0001; // automaticall
|
||||
static const uint16_t PMS_CMD_SLEEP_MODE_SLEEP = 0x0000; // go to sleep mode
|
||||
static const uint16_t PMS_CMD_SLEEP_MODE_WAKEUP = 0x0001; // wake up from sleep mode
|
||||
|
||||
void PMSX003Component::setup() {}
|
||||
|
||||
void PMSX003Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "PMSX003:");
|
||||
LOG_SENSOR(" ", "PM1.0STD", this->pm_1_0_std_sensor_);
|
||||
@@ -39,21 +41,36 @@ void PMSX003Component::dump_config() {
|
||||
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
|
||||
|
||||
if (this->update_interval_ <= PMS_STABILISING_MS) {
|
||||
ESP_LOGCONFIG(TAG, " Mode: active continuous (sensor default)");
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " Mode: passive with sleep/wake cycles");
|
||||
}
|
||||
|
||||
this->check_uart_settings(9600);
|
||||
}
|
||||
|
||||
void PMSX003Component::loop() {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
|
||||
// Initialize sensor mode on first loop
|
||||
if (this->initialised_ == 0) {
|
||||
if (this->update_interval_ > PMS_STABILISING_MS) {
|
||||
// Long update interval: use passive mode with sleep/wake cycles
|
||||
this->send_command_(PMS_CMD_MEASUREMENT_MODE, PMS_CMD_MEASUREMENT_MODE_PASSIVE);
|
||||
this->send_command_(PMS_CMD_SLEEP_MODE, PMS_CMD_SLEEP_MODE_WAKEUP);
|
||||
} else {
|
||||
// Short/zero update interval: use active continuous mode
|
||||
this->send_command_(PMS_CMD_MEASUREMENT_MODE, PMS_CMD_MEASUREMENT_MODE_ACTIVE);
|
||||
}
|
||||
this->initialised_ = 1;
|
||||
}
|
||||
|
||||
// If we update less often than it takes the device to stabilise, spin the fan down
|
||||
// rather than running it constantly. It does take some time to stabilise, so we
|
||||
// need to keep track of what state we're in.
|
||||
if (this->update_interval_ > PMS_STABILISING_MS) {
|
||||
if (this->initialised_ == 0) {
|
||||
this->send_command_(PMS_CMD_MEASUREMENT_MODE, PMS_CMD_MEASUREMENT_MODE_PASSIVE);
|
||||
this->send_command_(PMS_CMD_SLEEP_MODE, PMS_CMD_SLEEP_MODE_WAKEUP);
|
||||
this->initialised_ = 1;
|
||||
}
|
||||
switch (this->state_) {
|
||||
case PMSX003_STATE_IDLE:
|
||||
// Power on the sensor now so it'll be ready when we hit the update time
|
||||
@@ -248,6 +265,13 @@ void PMSX003Component::parse_data_() {
|
||||
if (this->pm_particles_25um_sensor_ != nullptr)
|
||||
this->pm_particles_25um_sensor_->publish_state(pm_particles_25um);
|
||||
|
||||
// Calculate and publish AQI if sensor is configured
|
||||
if (this->aqi_sensor_ != nullptr) {
|
||||
aqi::AbstractAQICalculator *calculator = this->aqi_calculator_factory_.get_calculator(this->aqi_calc_type_);
|
||||
int32_t aqi_value = calculator->get_aqi(pm_2_5_concentration, pm_10_0_concentration);
|
||||
this->aqi_sensor_->publish_state(aqi_value);
|
||||
}
|
||||
|
||||
if (this->type_ == PMSX003_TYPE_5003T) {
|
||||
ESP_LOGD(TAG,
|
||||
"Got PM0.3 Particles: %u Count/0.1L, PM0.5 Particles: %u Count/0.1L, PM1.0 Particles: %u Count/0.1L, "
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#include "esphome/components/aqi/aqi_calculator_factory.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace pmsx003 {
|
||||
@@ -31,6 +32,7 @@ enum PMSX003State {
|
||||
class PMSX003Component : public uart::UARTDevice, public Component {
|
||||
public:
|
||||
PMSX003Component() = default;
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void loop() override;
|
||||
|
||||
@@ -72,6 +74,10 @@ class PMSX003Component : public uart::UARTDevice, public Component {
|
||||
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; }
|
||||
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
|
||||
|
||||
void set_aqi_sensor(sensor::Sensor *aqi_sensor) { aqi_sensor_ = aqi_sensor; }
|
||||
|
||||
void set_aqi_calculation_type(aqi::AQICalculatorType aqi_calc_type) { aqi_calc_type_ = aqi_calc_type; }
|
||||
|
||||
protected:
|
||||
optional<bool> check_byte_();
|
||||
void parse_data_();
|
||||
@@ -115,6 +121,12 @@ class PMSX003Component : public uart::UARTDevice, public Component {
|
||||
// Temperature and Humidity
|
||||
sensor::Sensor *temperature_sensor_{nullptr};
|
||||
sensor::Sensor *humidity_sensor_{nullptr};
|
||||
|
||||
// AQI
|
||||
sensor::Sensor *aqi_sensor_{nullptr};
|
||||
|
||||
aqi::AQICalculatorType aqi_calc_type_;
|
||||
aqi::AQICalculatorFactory aqi_calculator_factory_ = aqi::AQICalculatorFactory();
|
||||
};
|
||||
|
||||
} // namespace pmsx003
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor, uart
|
||||
from esphome.components.aqi import AQI_CALCULATION_TYPE, CONF_AQI, CONF_CALCULATION_TYPE
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_FORMALDEHYDE,
|
||||
@@ -20,6 +21,7 @@ from esphome.const import (
|
||||
CONF_TEMPERATURE,
|
||||
CONF_TYPE,
|
||||
CONF_UPDATE_INTERVAL,
|
||||
DEVICE_CLASS_AQI,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_PM1,
|
||||
DEVICE_CLASS_PM10,
|
||||
@@ -35,11 +37,13 @@ from esphome.const import (
|
||||
|
||||
CODEOWNERS = ["@ximex"]
|
||||
DEPENDENCIES = ["uart"]
|
||||
AUTO_LOAD = ["aqi"]
|
||||
|
||||
pmsx003_ns = cg.esphome_ns.namespace("pmsx003")
|
||||
PMSX003Component = pmsx003_ns.class_("PMSX003Component", uart.UARTDevice, cg.Component)
|
||||
PMSX003Sensor = pmsx003_ns.class_("PMSX003Sensor", sensor.Sensor)
|
||||
|
||||
UNIT_INDEX = "index"
|
||||
TYPE_PMSX003 = "PMSX003"
|
||||
TYPE_PMS5003T = "PMS5003T"
|
||||
TYPE_PMS5003ST = "PMS5003ST"
|
||||
@@ -77,6 +81,10 @@ def validate_pmsx003_sensors(value):
|
||||
for key, types in SENSORS_TO_TYPE.items():
|
||||
if key in value and value[CONF_TYPE] not in types:
|
||||
raise cv.Invalid(f"{value[CONF_TYPE]} does not have {key} sensor!")
|
||||
if CONF_AQI in value and CONF_PM_2_5 not in value:
|
||||
raise cv.Invalid("AQI computation requires PM 2.5 sensor")
|
||||
if CONF_AQI in value and CONF_PM_10_0 not in value:
|
||||
raise cv.Invalid("AQI computation requires PM 10 sensor")
|
||||
return value
|
||||
|
||||
|
||||
@@ -192,6 +200,19 @@ CONFIG_SCHEMA = (
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_AQI): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_INDEX,
|
||||
icon=ICON_CHEMICAL_WEAPON,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_AQI,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
).extend(
|
||||
{
|
||||
cv.Required(CONF_CALCULATION_TYPE): cv.enum(
|
||||
AQI_CALCULATION_TYPE, upper=True
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_UPDATE_INTERVAL, default="0s"): validate_update_interval,
|
||||
}
|
||||
)
|
||||
@@ -278,4 +299,9 @@ async def to_code(config):
|
||||
sens = await sensor.new_sensor(config[CONF_HUMIDITY])
|
||||
cg.add(var.set_humidity_sensor(sens))
|
||||
|
||||
if CONF_AQI in config:
|
||||
sens = await sensor.new_sensor(config[CONF_AQI])
|
||||
cg.add(var.set_aqi_sensor(sens))
|
||||
cg.add(var.set_aqi_calculation_type(config[CONF_AQI][CONF_CALCULATION_TYPE]))
|
||||
|
||||
cg.add(var.set_update_interval(config[CONF_UPDATE_INTERVAL]))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "sen5x.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
@@ -154,10 +155,10 @@ void SEN5XComponent::setup() {
|
||||
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 compilation time and serial number
|
||||
// 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 mulitple sensors can be used without conflict
|
||||
uint32_t hash = fnv1_hash(App.get_compilation_time_ref() + std::to_string(combined_serial));
|
||||
// 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(), std::to_string(combined_serial));
|
||||
this->pref_ = global_preferences->make_preference<Sen5xBaselines>(hash, true);
|
||||
|
||||
if (this->pref_.load(&this->voc_baselines_storage_)) {
|
||||
|
||||
@@ -72,10 +72,10 @@ void SGP30Component::setup() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Hash with compilation time and serial number
|
||||
// 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 mulitple sensors can be used without conflict
|
||||
uint32_t hash = fnv1_hash(App.get_compilation_time_ref() + std::to_string(this->serial_number_));
|
||||
// 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(), std::to_string(this->serial_number_));
|
||||
this->pref_ = global_preferences->make_preference<SGP30Baselines>(hash, true);
|
||||
|
||||
if (this->store_baseline_ && this->pref_.load(&this->baselines_storage_)) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "sgp4x.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include <cinttypes>
|
||||
@@ -56,10 +57,10 @@ void SGP4xComponent::setup() {
|
||||
ESP_LOGD(TAG, "Version 0x%0X", featureset);
|
||||
|
||||
if (this->store_baseline_) {
|
||||
// Hash with compilation time and serial number
|
||||
// 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 mulitple sensors can be used without conflict
|
||||
uint32_t hash = fnv1_hash(App.get_compilation_time_ref() + std::to_string(this->serial_number_));
|
||||
// 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(), std::to_string(this->serial_number_));
|
||||
this->pref_ = global_preferences->make_preference<SGP4xBaselines>(hash, true);
|
||||
|
||||
if (this->pref_.load(&this->voc_baselines_storage_)) {
|
||||
|
||||
@@ -12,8 +12,7 @@
|
||||
#include <lwip/sockets.h>
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
namespace socket {
|
||||
namespace esphome::socket {
|
||||
|
||||
std::string format_sockaddr(const struct sockaddr_storage &storage) {
|
||||
if (storage.ss_family == AF_INET) {
|
||||
@@ -44,11 +43,11 @@ class BSDSocketImpl : public Socket {
|
||||
BSDSocketImpl(int fd, bool monitor_loop = false) : fd_(fd) {
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
// Register new socket with the application for select() if monitoring requested
|
||||
if (monitor_loop && fd_ >= 0) {
|
||||
if (monitor_loop && this->fd_ >= 0) {
|
||||
// Only set loop_monitored_ to true if registration succeeds
|
||||
loop_monitored_ = App.register_socket_fd(fd_);
|
||||
this->loop_monitored_ = App.register_socket_fd(this->fd_);
|
||||
} else {
|
||||
loop_monitored_ = false;
|
||||
this->loop_monitored_ = false;
|
||||
}
|
||||
#else
|
||||
// Without select support, ignore monitor_loop parameter
|
||||
@@ -56,70 +55,69 @@ class BSDSocketImpl : public Socket {
|
||||
#endif
|
||||
}
|
||||
~BSDSocketImpl() override {
|
||||
if (!closed_) {
|
||||
close(); // NOLINT(clang-analyzer-optin.cplusplus.VirtualCall)
|
||||
if (!this->closed_) {
|
||||
this->close(); // NOLINT(clang-analyzer-optin.cplusplus.VirtualCall)
|
||||
}
|
||||
}
|
||||
int connect(const struct sockaddr *addr, socklen_t addrlen) override { return ::connect(fd_, addr, addrlen); }
|
||||
int connect(const struct sockaddr *addr, socklen_t addrlen) override { return ::connect(this->fd_, addr, addrlen); }
|
||||
std::unique_ptr<Socket> accept(struct sockaddr *addr, socklen_t *addrlen) override {
|
||||
return accept_impl_(addr, addrlen, false);
|
||||
}
|
||||
std::unique_ptr<Socket> accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) override {
|
||||
return accept_impl_(addr, addrlen, true);
|
||||
}
|
||||
|
||||
private:
|
||||
std::unique_ptr<Socket> accept_impl_(struct sockaddr *addr, socklen_t *addrlen, bool loop_monitored) {
|
||||
int fd = ::accept(fd_, addr, addrlen);
|
||||
int fd = ::accept(this->fd_, addr, addrlen);
|
||||
if (fd == -1)
|
||||
return {};
|
||||
return make_unique<BSDSocketImpl>(fd, loop_monitored);
|
||||
return make_unique<BSDSocketImpl>(fd, false);
|
||||
}
|
||||
std::unique_ptr<Socket> accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) override {
|
||||
int fd = ::accept(this->fd_, addr, addrlen);
|
||||
if (fd == -1)
|
||||
return {};
|
||||
return make_unique<BSDSocketImpl>(fd, true);
|
||||
}
|
||||
|
||||
public:
|
||||
int bind(const struct sockaddr *addr, socklen_t addrlen) override { return ::bind(fd_, addr, addrlen); }
|
||||
int bind(const struct sockaddr *addr, socklen_t addrlen) override { return ::bind(this->fd_, addr, addrlen); }
|
||||
int close() override {
|
||||
if (!closed_) {
|
||||
if (!this->closed_) {
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
// Unregister from select() before closing if monitored
|
||||
if (loop_monitored_) {
|
||||
App.unregister_socket_fd(fd_);
|
||||
if (this->loop_monitored_) {
|
||||
App.unregister_socket_fd(this->fd_);
|
||||
}
|
||||
#endif
|
||||
int ret = ::close(fd_);
|
||||
closed_ = true;
|
||||
int ret = ::close(this->fd_);
|
||||
this->closed_ = true;
|
||||
return ret;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
int shutdown(int how) override { return ::shutdown(fd_, how); }
|
||||
int shutdown(int how) override { return ::shutdown(this->fd_, how); }
|
||||
|
||||
int getpeername(struct sockaddr *addr, socklen_t *addrlen) override { return ::getpeername(fd_, addr, addrlen); }
|
||||
int getpeername(struct sockaddr *addr, socklen_t *addrlen) override {
|
||||
return ::getpeername(this->fd_, addr, addrlen);
|
||||
}
|
||||
std::string getpeername() override {
|
||||
struct sockaddr_storage storage;
|
||||
socklen_t len = sizeof(storage);
|
||||
int err = this->getpeername((struct sockaddr *) &storage, &len);
|
||||
if (err != 0)
|
||||
if (::getpeername(this->fd_, (struct sockaddr *) &storage, &len) != 0)
|
||||
return {};
|
||||
return format_sockaddr(storage);
|
||||
}
|
||||
int getsockname(struct sockaddr *addr, socklen_t *addrlen) override { return ::getsockname(fd_, addr, addrlen); }
|
||||
int getsockname(struct sockaddr *addr, socklen_t *addrlen) override {
|
||||
return ::getsockname(this->fd_, addr, addrlen);
|
||||
}
|
||||
std::string getsockname() override {
|
||||
struct sockaddr_storage storage;
|
||||
socklen_t len = sizeof(storage);
|
||||
int err = this->getsockname((struct sockaddr *) &storage, &len);
|
||||
if (err != 0)
|
||||
if (::getsockname(this->fd_, (struct sockaddr *) &storage, &len) != 0)
|
||||
return {};
|
||||
return format_sockaddr(storage);
|
||||
}
|
||||
int getsockopt(int level, int optname, void *optval, socklen_t *optlen) override {
|
||||
return ::getsockopt(fd_, level, optname, optval, optlen);
|
||||
return ::getsockopt(this->fd_, level, optname, optval, optlen);
|
||||
}
|
||||
int setsockopt(int level, int optname, const void *optval, socklen_t optlen) override {
|
||||
return ::setsockopt(fd_, level, optname, optval, optlen);
|
||||
return ::setsockopt(this->fd_, level, optname, optval, optlen);
|
||||
}
|
||||
int listen(int backlog) override { return ::listen(fd_, backlog); }
|
||||
ssize_t read(void *buf, size_t len) override { return ::read(fd_, buf, len); }
|
||||
int listen(int backlog) override { return ::listen(this->fd_, backlog); }
|
||||
ssize_t read(void *buf, size_t len) override { return ::read(this->fd_, buf, len); }
|
||||
ssize_t recvfrom(void *buf, size_t len, sockaddr *addr, socklen_t *addr_len) override {
|
||||
#if defined(USE_ESP32) || defined(USE_HOST)
|
||||
return ::recvfrom(this->fd_, buf, len, 0, addr, addr_len);
|
||||
@@ -129,41 +127,52 @@ class BSDSocketImpl : public Socket {
|
||||
}
|
||||
ssize_t readv(const struct iovec *iov, int iovcnt) override {
|
||||
#if defined(USE_ESP32)
|
||||
return ::lwip_readv(fd_, iov, iovcnt);
|
||||
return ::lwip_readv(this->fd_, iov, iovcnt);
|
||||
#else
|
||||
return ::readv(fd_, iov, iovcnt);
|
||||
return ::readv(this->fd_, iov, iovcnt);
|
||||
#endif
|
||||
}
|
||||
ssize_t write(const void *buf, size_t len) override { return ::write(fd_, buf, len); }
|
||||
ssize_t send(void *buf, size_t len, int flags) { return ::send(fd_, buf, len, flags); }
|
||||
ssize_t write(const void *buf, size_t len) override { return ::write(this->fd_, buf, len); }
|
||||
ssize_t send(void *buf, size_t len, int flags) { return ::send(this->fd_, buf, len, flags); }
|
||||
ssize_t writev(const struct iovec *iov, int iovcnt) override {
|
||||
#if defined(USE_ESP32)
|
||||
return ::lwip_writev(fd_, iov, iovcnt);
|
||||
return ::lwip_writev(this->fd_, iov, iovcnt);
|
||||
#else
|
||||
return ::writev(fd_, iov, iovcnt);
|
||||
return ::writev(this->fd_, iov, iovcnt);
|
||||
#endif
|
||||
}
|
||||
|
||||
ssize_t sendto(const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen) override {
|
||||
return ::sendto(fd_, buf, len, flags, to, tolen); // NOLINT(readability-suspicious-call-argument)
|
||||
return ::sendto(this->fd_, buf, len, flags, to, tolen); // NOLINT(readability-suspicious-call-argument)
|
||||
}
|
||||
|
||||
int setblocking(bool blocking) override {
|
||||
int fl = ::fcntl(fd_, F_GETFL, 0);
|
||||
int fl = ::fcntl(this->fd_, F_GETFL, 0);
|
||||
if (blocking) {
|
||||
fl &= ~O_NONBLOCK;
|
||||
} else {
|
||||
fl |= O_NONBLOCK;
|
||||
}
|
||||
::fcntl(fd_, F_SETFL, fl);
|
||||
::fcntl(this->fd_, F_SETFL, fl);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int get_fd() const override { return fd_; }
|
||||
int get_fd() const override { return this->fd_; }
|
||||
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
bool ready() const override {
|
||||
if (!this->loop_monitored_)
|
||||
return true;
|
||||
return App.is_socket_ready(this->fd_);
|
||||
}
|
||||
#endif
|
||||
|
||||
protected:
|
||||
int fd_;
|
||||
bool closed_ = false;
|
||||
bool closed_{false};
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
bool loop_monitored_{false};
|
||||
#endif
|
||||
};
|
||||
|
||||
// Helper to create a socket with optional monitoring
|
||||
@@ -182,7 +191,6 @@ std::unique_ptr<Socket> socket_loop_monitored(int domain, int type, int protocol
|
||||
return create_socket(domain, type, protocol, true);
|
||||
}
|
||||
|
||||
} // namespace socket
|
||||
} // namespace esphome
|
||||
} // namespace esphome::socket
|
||||
|
||||
#endif // USE_SOCKET_IMPL_BSD_SOCKETS
|
||||
|
||||
@@ -18,8 +18,7 @@
|
||||
#include <coredecls.h> // For esp_schedule()
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
namespace socket {
|
||||
namespace esphome::socket {
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
// Flag to signal socket activity - checked by socket_delay() to exit early
|
||||
@@ -711,7 +710,6 @@ std::unique_ptr<Socket> socket_loop_monitored(int domain, int type, int protocol
|
||||
return socket(domain, type, protocol);
|
||||
}
|
||||
|
||||
} // namespace socket
|
||||
} // namespace esphome
|
||||
} // namespace esphome::socket
|
||||
|
||||
#endif // USE_SOCKET_IMPL_LWIP_TCP
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
#include <cstring>
|
||||
#include "esphome/core/application.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace socket {
|
||||
namespace esphome::socket {
|
||||
|
||||
std::string format_sockaddr(const struct sockaddr_storage &storage) {
|
||||
if (storage.ss_family == AF_INET) {
|
||||
@@ -37,11 +36,11 @@ class LwIPSocketImpl : public Socket {
|
||||
LwIPSocketImpl(int fd, bool monitor_loop = false) : fd_(fd) {
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
// Register new socket with the application for select() if monitoring requested
|
||||
if (monitor_loop && fd_ >= 0) {
|
||||
if (monitor_loop && this->fd_ >= 0) {
|
||||
// Only set loop_monitored_ to true if registration succeeds
|
||||
loop_monitored_ = App.register_socket_fd(fd_);
|
||||
this->loop_monitored_ = App.register_socket_fd(this->fd_);
|
||||
} else {
|
||||
loop_monitored_ = false;
|
||||
this->loop_monitored_ = false;
|
||||
}
|
||||
#else
|
||||
// Without select support, ignore monitor_loop parameter
|
||||
@@ -49,96 +48,108 @@ class LwIPSocketImpl : public Socket {
|
||||
#endif
|
||||
}
|
||||
~LwIPSocketImpl() override {
|
||||
if (!closed_) {
|
||||
close(); // NOLINT(clang-analyzer-optin.cplusplus.VirtualCall)
|
||||
if (!this->closed_) {
|
||||
this->close(); // NOLINT(clang-analyzer-optin.cplusplus.VirtualCall)
|
||||
}
|
||||
}
|
||||
int connect(const struct sockaddr *addr, socklen_t addrlen) override { return lwip_connect(fd_, addr, addrlen); }
|
||||
int connect(const struct sockaddr *addr, socklen_t addrlen) override {
|
||||
return lwip_connect(this->fd_, addr, addrlen);
|
||||
}
|
||||
std::unique_ptr<Socket> accept(struct sockaddr *addr, socklen_t *addrlen) override {
|
||||
return accept_impl_(addr, addrlen, false);
|
||||
}
|
||||
std::unique_ptr<Socket> accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) override {
|
||||
return accept_impl_(addr, addrlen, true);
|
||||
}
|
||||
|
||||
private:
|
||||
std::unique_ptr<Socket> accept_impl_(struct sockaddr *addr, socklen_t *addrlen, bool loop_monitored) {
|
||||
int fd = lwip_accept(fd_, addr, addrlen);
|
||||
int fd = lwip_accept(this->fd_, addr, addrlen);
|
||||
if (fd == -1)
|
||||
return {};
|
||||
return make_unique<LwIPSocketImpl>(fd, loop_monitored);
|
||||
return make_unique<LwIPSocketImpl>(fd, false);
|
||||
}
|
||||
std::unique_ptr<Socket> accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) override {
|
||||
int fd = lwip_accept(this->fd_, addr, addrlen);
|
||||
if (fd == -1)
|
||||
return {};
|
||||
return make_unique<LwIPSocketImpl>(fd, true);
|
||||
}
|
||||
|
||||
public:
|
||||
int bind(const struct sockaddr *addr, socklen_t addrlen) override { return lwip_bind(fd_, addr, addrlen); }
|
||||
int bind(const struct sockaddr *addr, socklen_t addrlen) override { return lwip_bind(this->fd_, addr, addrlen); }
|
||||
int close() override {
|
||||
if (!closed_) {
|
||||
if (!this->closed_) {
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
// Unregister from select() before closing if monitored
|
||||
if (loop_monitored_) {
|
||||
App.unregister_socket_fd(fd_);
|
||||
if (this->loop_monitored_) {
|
||||
App.unregister_socket_fd(this->fd_);
|
||||
}
|
||||
#endif
|
||||
int ret = lwip_close(fd_);
|
||||
closed_ = true;
|
||||
int ret = lwip_close(this->fd_);
|
||||
this->closed_ = true;
|
||||
return ret;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
int shutdown(int how) override { return lwip_shutdown(fd_, how); }
|
||||
int shutdown(int how) override { return lwip_shutdown(this->fd_, how); }
|
||||
|
||||
int getpeername(struct sockaddr *addr, socklen_t *addrlen) override { return lwip_getpeername(fd_, addr, addrlen); }
|
||||
int getpeername(struct sockaddr *addr, socklen_t *addrlen) override {
|
||||
return lwip_getpeername(this->fd_, addr, addrlen);
|
||||
}
|
||||
std::string getpeername() override {
|
||||
struct sockaddr_storage storage;
|
||||
socklen_t len = sizeof(storage);
|
||||
int err = this->getpeername((struct sockaddr *) &storage, &len);
|
||||
if (err != 0)
|
||||
if (lwip_getpeername(this->fd_, (struct sockaddr *) &storage, &len) != 0)
|
||||
return {};
|
||||
return format_sockaddr(storage);
|
||||
}
|
||||
int getsockname(struct sockaddr *addr, socklen_t *addrlen) override { return lwip_getsockname(fd_, addr, addrlen); }
|
||||
int getsockname(struct sockaddr *addr, socklen_t *addrlen) override {
|
||||
return lwip_getsockname(this->fd_, addr, addrlen);
|
||||
}
|
||||
std::string getsockname() override {
|
||||
struct sockaddr_storage storage;
|
||||
socklen_t len = sizeof(storage);
|
||||
int err = this->getsockname((struct sockaddr *) &storage, &len);
|
||||
if (err != 0)
|
||||
if (lwip_getsockname(this->fd_, (struct sockaddr *) &storage, &len) != 0)
|
||||
return {};
|
||||
return format_sockaddr(storage);
|
||||
}
|
||||
int getsockopt(int level, int optname, void *optval, socklen_t *optlen) override {
|
||||
return lwip_getsockopt(fd_, level, optname, optval, optlen);
|
||||
return lwip_getsockopt(this->fd_, level, optname, optval, optlen);
|
||||
}
|
||||
int setsockopt(int level, int optname, const void *optval, socklen_t optlen) override {
|
||||
return lwip_setsockopt(fd_, level, optname, optval, optlen);
|
||||
return lwip_setsockopt(this->fd_, level, optname, optval, optlen);
|
||||
}
|
||||
int listen(int backlog) override { return lwip_listen(fd_, backlog); }
|
||||
ssize_t read(void *buf, size_t len) override { return lwip_read(fd_, buf, len); }
|
||||
int listen(int backlog) override { return lwip_listen(this->fd_, backlog); }
|
||||
ssize_t read(void *buf, size_t len) override { return lwip_read(this->fd_, buf, len); }
|
||||
ssize_t recvfrom(void *buf, size_t len, sockaddr *addr, socklen_t *addr_len) override {
|
||||
return lwip_recvfrom(fd_, buf, len, 0, addr, addr_len);
|
||||
return lwip_recvfrom(this->fd_, buf, len, 0, addr, addr_len);
|
||||
}
|
||||
ssize_t readv(const struct iovec *iov, int iovcnt) override { return lwip_readv(fd_, iov, iovcnt); }
|
||||
ssize_t write(const void *buf, size_t len) override { return lwip_write(fd_, buf, len); }
|
||||
ssize_t send(void *buf, size_t len, int flags) { return lwip_send(fd_, buf, len, flags); }
|
||||
ssize_t writev(const struct iovec *iov, int iovcnt) override { return lwip_writev(fd_, iov, iovcnt); }
|
||||
ssize_t readv(const struct iovec *iov, int iovcnt) override { return lwip_readv(this->fd_, iov, iovcnt); }
|
||||
ssize_t write(const void *buf, size_t len) override { return lwip_write(this->fd_, buf, len); }
|
||||
ssize_t send(void *buf, size_t len, int flags) { return lwip_send(this->fd_, buf, len, flags); }
|
||||
ssize_t writev(const struct iovec *iov, int iovcnt) override { return lwip_writev(this->fd_, iov, iovcnt); }
|
||||
ssize_t sendto(const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen) override {
|
||||
return lwip_sendto(fd_, buf, len, flags, to, tolen);
|
||||
return lwip_sendto(this->fd_, buf, len, flags, to, tolen);
|
||||
}
|
||||
int setblocking(bool blocking) override {
|
||||
int fl = lwip_fcntl(fd_, F_GETFL, 0);
|
||||
int fl = lwip_fcntl(this->fd_, F_GETFL, 0);
|
||||
if (blocking) {
|
||||
fl &= ~O_NONBLOCK;
|
||||
} else {
|
||||
fl |= O_NONBLOCK;
|
||||
}
|
||||
lwip_fcntl(fd_, F_SETFL, fl);
|
||||
lwip_fcntl(this->fd_, F_SETFL, fl);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int get_fd() const override { return fd_; }
|
||||
int get_fd() const override { return this->fd_; }
|
||||
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
bool ready() const override {
|
||||
if (!this->loop_monitored_)
|
||||
return true;
|
||||
return App.is_socket_ready(this->fd_);
|
||||
}
|
||||
#endif
|
||||
|
||||
protected:
|
||||
int fd_;
|
||||
bool closed_ = false;
|
||||
bool closed_{false};
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
bool loop_monitored_{false};
|
||||
#endif
|
||||
};
|
||||
|
||||
// Helper to create a socket with optional monitoring
|
||||
@@ -157,7 +168,6 @@ std::unique_ptr<Socket> socket_loop_monitored(int domain, int type, int protocol
|
||||
return create_socket(domain, type, protocol, true);
|
||||
}
|
||||
|
||||
} // namespace socket
|
||||
} // namespace esphome
|
||||
} // namespace esphome::socket
|
||||
|
||||
#endif // USE_SOCKET_IMPL_LWIP_SOCKETS
|
||||
|
||||
@@ -6,33 +6,10 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/application.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace socket {
|
||||
namespace esphome::socket {
|
||||
|
||||
Socket::~Socket() {}
|
||||
|
||||
bool Socket::ready() const {
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
if (!loop_monitored_) {
|
||||
// Non-monitored sockets always return true (assume data may be available)
|
||||
return true;
|
||||
}
|
||||
|
||||
// For loop-monitored sockets, check with the Application's select() results
|
||||
int fd = this->get_fd();
|
||||
if (fd < 0) {
|
||||
// No valid file descriptor, assume ready (fallback behavior)
|
||||
return true;
|
||||
}
|
||||
|
||||
return App.is_socket_ready(fd);
|
||||
#else
|
||||
// Without select() support, we can't monitor sockets in the loop
|
||||
// Always return true (assume data may be available)
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
std::unique_ptr<Socket> socket_ip(int type, int protocol) {
|
||||
#if USE_NETWORK_IPV6
|
||||
return socket(AF_INET6, type, protocol);
|
||||
@@ -113,6 +90,5 @@ socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t po
|
||||
return sizeof(sockaddr_in);
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
}
|
||||
} // namespace socket
|
||||
} // namespace esphome
|
||||
} // namespace esphome::socket
|
||||
#endif
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
#include "headers.h"
|
||||
|
||||
#if defined(USE_SOCKET_IMPL_LWIP_TCP) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS)
|
||||
namespace esphome {
|
||||
namespace socket {
|
||||
namespace esphome::socket {
|
||||
|
||||
class Socket {
|
||||
public:
|
||||
@@ -54,12 +53,7 @@ class Socket {
|
||||
/// Check if socket has data ready to read
|
||||
/// For loop-monitored sockets, checks with the Application's select() results
|
||||
/// For non-monitored sockets, always returns true (assumes data may be available)
|
||||
bool ready() const;
|
||||
|
||||
protected:
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
bool loop_monitored_{false}; ///< Whether this socket is monitored by the event loop
|
||||
#endif
|
||||
virtual bool ready() const { return true; }
|
||||
};
|
||||
|
||||
/// Create a socket of the given domain, type and protocol.
|
||||
@@ -91,6 +85,5 @@ void socket_delay(uint32_t ms);
|
||||
void socket_wake();
|
||||
#endif
|
||||
|
||||
} // namespace socket
|
||||
} // namespace esphome
|
||||
} // namespace esphome::socket
|
||||
#endif
|
||||
|
||||
@@ -6,7 +6,7 @@ from pathlib import Path
|
||||
|
||||
from esphome import automation, external_files
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import audio, esp32, media_player, network, psram, speaker
|
||||
from esphome.components import audio, esp32, media_player, network, ota, psram, speaker
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BUFFER_SIZE,
|
||||
@@ -342,7 +342,7 @@ async def to_code(config):
|
||||
var = await media_player.new_media_player(config)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
cg.add_define("USE_OTA_STATE_CALLBACK")
|
||||
ota.request_ota_state_listeners()
|
||||
|
||||
cg.add(var.set_buffer_size(config[CONF_BUFFER_SIZE]))
|
||||
|
||||
|
||||
@@ -66,25 +66,8 @@ void SpeakerMediaPlayer::setup() {
|
||||
this->set_mute_state_(false);
|
||||
}
|
||||
|
||||
#ifdef USE_OTA
|
||||
ota::get_global_ota_callback()->add_on_state_callback(
|
||||
[this](ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) {
|
||||
if (state == ota::OTA_STARTED) {
|
||||
if (this->media_pipeline_ != nullptr) {
|
||||
this->media_pipeline_->suspend_tasks();
|
||||
}
|
||||
if (this->announcement_pipeline_ != nullptr) {
|
||||
this->announcement_pipeline_->suspend_tasks();
|
||||
}
|
||||
} else if (state == ota::OTA_ERROR) {
|
||||
if (this->media_pipeline_ != nullptr) {
|
||||
this->media_pipeline_->resume_tasks();
|
||||
}
|
||||
if (this->announcement_pipeline_ != nullptr) {
|
||||
this->announcement_pipeline_->resume_tasks();
|
||||
}
|
||||
}
|
||||
});
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
ota::get_global_ota_callback()->add_global_state_listener(this);
|
||||
#endif
|
||||
|
||||
this->announcement_pipeline_ =
|
||||
@@ -300,6 +283,27 @@ void SpeakerMediaPlayer::watch_media_commands_() {
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
void SpeakerMediaPlayer::on_ota_global_state(ota::OTAState state, float progress, uint8_t error,
|
||||
ota::OTAComponent *comp) {
|
||||
if (state == ota::OTA_STARTED) {
|
||||
if (this->media_pipeline_ != nullptr) {
|
||||
this->media_pipeline_->suspend_tasks();
|
||||
}
|
||||
if (this->announcement_pipeline_ != nullptr) {
|
||||
this->announcement_pipeline_->suspend_tasks();
|
||||
}
|
||||
} else if (state == ota::OTA_ERROR) {
|
||||
if (this->media_pipeline_ != nullptr) {
|
||||
this->media_pipeline_->resume_tasks();
|
||||
}
|
||||
if (this->announcement_pipeline_ != nullptr) {
|
||||
this->announcement_pipeline_->resume_tasks();
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void SpeakerMediaPlayer::loop() {
|
||||
this->watch_media_commands_();
|
||||
|
||||
|
||||
@@ -5,14 +5,18 @@
|
||||
#include "audio_pipeline.h"
|
||||
|
||||
#include "esphome/components/audio/audio.h"
|
||||
|
||||
#include "esphome/components/media_player/media_player.h"
|
||||
#include "esphome/components/speaker/speaker.h"
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
#include "esphome/components/ota/ota_backend.h"
|
||||
#endif
|
||||
|
||||
#include <deque>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/queue.h>
|
||||
@@ -39,12 +43,22 @@ struct VolumeRestoreState {
|
||||
bool is_muted;
|
||||
};
|
||||
|
||||
class SpeakerMediaPlayer : public Component, public media_player::MediaPlayer {
|
||||
class SpeakerMediaPlayer : public Component,
|
||||
public media_player::MediaPlayer
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
,
|
||||
public ota::OTAGlobalStateListener
|
||||
#endif
|
||||
{
|
||||
public:
|
||||
float get_setup_priority() const override { return esphome::setup_priority::PROCESSOR; }
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
void on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) override;
|
||||
#endif
|
||||
|
||||
// MediaPlayer implementations
|
||||
media_player::MediaPlayerTraits get_traits() override;
|
||||
bool is_muted() const override { return this->is_muted_; }
|
||||
|
||||
@@ -39,6 +39,7 @@ enum TemplateAlarmControlPanelRestoreMode {
|
||||
ALARM_CONTROL_PANEL_RESTORE_DEFAULT_DISARMED,
|
||||
};
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
struct SensorDataStore {
|
||||
bool last_chime_state;
|
||||
};
|
||||
@@ -49,7 +50,6 @@ struct SensorInfo {
|
||||
uint8_t store_index;
|
||||
};
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
struct AlarmSensor {
|
||||
binary_sensor::BinarySensor *sensor;
|
||||
SensorInfo info;
|
||||
@@ -139,6 +139,9 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl
|
||||
FixedVector<AlarmSensor> sensors_;
|
||||
// a list of automatically bypassed sensors
|
||||
std::vector<uint8_t> bypassed_sensor_indicies_;
|
||||
// Per sensor data store
|
||||
std::vector<SensorDataStore> sensor_data_;
|
||||
uint8_t next_store_index_ = 0;
|
||||
#endif
|
||||
TemplateAlarmControlPanelRestoreMode restore_mode_{};
|
||||
|
||||
@@ -154,14 +157,11 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl
|
||||
uint32_t trigger_time_;
|
||||
// a list of codes
|
||||
std::vector<std::string> codes_;
|
||||
// Per sensor data store
|
||||
std::vector<SensorDataStore> sensor_data_;
|
||||
// requires a code to arm
|
||||
bool requires_code_to_arm_ = false;
|
||||
bool supports_arm_home_ = false;
|
||||
bool supports_arm_night_ = false;
|
||||
bool sensors_ready_ = false;
|
||||
uint8_t next_store_index_ = 0;
|
||||
// check if the code is valid
|
||||
bool is_code_valid_(optional<std::string> code);
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#include "version_text_sensor.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/version.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace version {
|
||||
@@ -10,11 +11,26 @@ namespace version {
|
||||
static const char *const TAG = "version.text_sensor";
|
||||
|
||||
void VersionTextSensor::setup() {
|
||||
if (this->hide_timestamp_) {
|
||||
this->publish_state(ESPHOME_VERSION);
|
||||
} else {
|
||||
this->publish_state(str_sprintf(ESPHOME_VERSION " %s", App.get_compilation_time_ref().c_str()));
|
||||
static const char PREFIX[] PROGMEM = ESPHOME_VERSION " (config hash 0x";
|
||||
static const char BUILT_STR[] PROGMEM = ", built ";
|
||||
// Buffer size: PREFIX + 8 hex chars + BUILT_STR + BUILD_TIME_STR_SIZE + ")" + null
|
||||
constexpr size_t buf_size = sizeof(PREFIX) + 8 + sizeof(BUILT_STR) + esphome::Application::BUILD_TIME_STR_SIZE + 2;
|
||||
char version_str[buf_size];
|
||||
|
||||
ESPHOME_strncpy_P(version_str, PREFIX, sizeof(version_str));
|
||||
|
||||
size_t len = strlen(version_str);
|
||||
snprintf(version_str + len, sizeof(version_str) - len, "%08" PRIx32, App.get_config_hash());
|
||||
|
||||
if (!this->hide_timestamp_) {
|
||||
size_t len = strlen(version_str);
|
||||
ESPHOME_strncat_P(version_str, BUILT_STR, sizeof(version_str) - len - 1);
|
||||
ESPHOME_strncat_P(version_str, ESPHOME_BUILD_TIME_STR, sizeof(version_str) - strlen(version_str) - 1);
|
||||
}
|
||||
|
||||
strncat(version_str, ")", sizeof(version_str) - strlen(version_str) - 1);
|
||||
version_str[sizeof(version_str) - 1] = '\0';
|
||||
this->publish_state(version_str);
|
||||
}
|
||||
float VersionTextSensor::get_setup_priority() const { return setup_priority::DATA; }
|
||||
void VersionTextSensor::set_hide_timestamp(bool hide_timestamp) { this->hide_timestamp_ = hide_timestamp; }
|
||||
|
||||
@@ -84,9 +84,9 @@ void OTARequestHandler::report_ota_progress_(AsyncWebServerRequest *request) {
|
||||
} else {
|
||||
ESP_LOGD(TAG, "OTA in progress: %" PRIu32 " bytes read", this->ota_read_length_);
|
||||
}
|
||||
#ifdef USE_OTA_STATE_CALLBACK
|
||||
// Report progress - use call_deferred since we're in web server task
|
||||
this->parent_->state_callback_.call_deferred(ota::OTA_IN_PROGRESS, percentage, 0);
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
// Report progress - use notify_state_deferred_ since we're in web server task
|
||||
this->parent_->notify_state_deferred_(ota::OTA_IN_PROGRESS, percentage, 0);
|
||||
#endif
|
||||
this->last_ota_progress_ = now;
|
||||
}
|
||||
@@ -114,9 +114,9 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Platf
|
||||
// Initialize OTA on first call
|
||||
this->ota_init_(filename.c_str());
|
||||
|
||||
#ifdef USE_OTA_STATE_CALLBACK
|
||||
// Notify OTA started - use call_deferred since we're in web server task
|
||||
this->parent_->state_callback_.call_deferred(ota::OTA_STARTED, 0.0f, 0);
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
// Notify OTA started - use notify_state_deferred_ since we're in web server task
|
||||
this->parent_->notify_state_deferred_(ota::OTA_STARTED, 0.0f, 0);
|
||||
#endif
|
||||
|
||||
// Platform-specific pre-initialization
|
||||
@@ -134,9 +134,9 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Platf
|
||||
this->ota_backend_ = ota::make_ota_backend();
|
||||
if (!this->ota_backend_) {
|
||||
ESP_LOGE(TAG, "Failed to create OTA backend");
|
||||
#ifdef USE_OTA_STATE_CALLBACK
|
||||
this->parent_->state_callback_.call_deferred(ota::OTA_ERROR, 0.0f,
|
||||
static_cast<uint8_t>(ota::OTA_RESPONSE_ERROR_UNKNOWN));
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
this->parent_->notify_state_deferred_(ota::OTA_ERROR, 0.0f,
|
||||
static_cast<uint8_t>(ota::OTA_RESPONSE_ERROR_UNKNOWN));
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
@@ -148,8 +148,8 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Platf
|
||||
if (error_code != ota::OTA_RESPONSE_OK) {
|
||||
ESP_LOGE(TAG, "OTA begin failed: %d", error_code);
|
||||
this->ota_backend_.reset();
|
||||
#ifdef USE_OTA_STATE_CALLBACK
|
||||
this->parent_->state_callback_.call_deferred(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
this->parent_->notify_state_deferred_(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
@@ -166,8 +166,8 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Platf
|
||||
ESP_LOGE(TAG, "OTA write failed: %d", error_code);
|
||||
this->ota_backend_->abort();
|
||||
this->ota_backend_.reset();
|
||||
#ifdef USE_OTA_STATE_CALLBACK
|
||||
this->parent_->state_callback_.call_deferred(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
this->parent_->notify_state_deferred_(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
@@ -186,15 +186,15 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Platf
|
||||
error_code = this->ota_backend_->end();
|
||||
if (error_code == ota::OTA_RESPONSE_OK) {
|
||||
this->ota_success_ = true;
|
||||
#ifdef USE_OTA_STATE_CALLBACK
|
||||
// Report completion before reboot - use call_deferred since we're in web server task
|
||||
this->parent_->state_callback_.call_deferred(ota::OTA_COMPLETED, 100.0f, 0);
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
// Report completion before reboot - use notify_state_deferred_ since we're in web server task
|
||||
this->parent_->notify_state_deferred_(ota::OTA_COMPLETED, 100.0f, 0);
|
||||
#endif
|
||||
this->schedule_ota_reboot_();
|
||||
} else {
|
||||
ESP_LOGE(TAG, "OTA end failed: %d", error_code);
|
||||
#ifdef USE_OTA_STATE_CALLBACK
|
||||
this->parent_->state_callback_.call_deferred(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
this->parent_->notify_state_deferred_(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
|
||||
#endif
|
||||
}
|
||||
this->ota_backend_.reset();
|
||||
@@ -232,10 +232,6 @@ void WebServerOTAComponent::setup() {
|
||||
|
||||
// AsyncWebServer takes ownership of the handler and will delete it when the server is destroyed
|
||||
base->add_handler(new OTARequestHandler(this)); // NOLINT
|
||||
#ifdef USE_OTA_STATE_CALLBACK
|
||||
// Register with global OTA callback system
|
||||
ota::register_ota_platform(this);
|
||||
#endif
|
||||
}
|
||||
|
||||
void WebServerOTAComponent::dump_config() { ESP_LOGCONFIG(TAG, "Web Server OTA"); }
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#ifdef USE_WIFI
|
||||
#include <cassert>
|
||||
#include <cinttypes>
|
||||
#include <cmath>
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#if (ESP_IDF_VERSION_MAJOR >= 5 && ESP_IDF_VERSION_MINOR >= 1)
|
||||
@@ -375,7 +376,7 @@ void WiFiComponent::start() {
|
||||
get_mac_address_pretty_into_buffer(mac_s));
|
||||
this->last_connected_ = millis();
|
||||
|
||||
uint32_t hash = this->has_sta() ? fnv1_hash(App.get_compilation_time_ref().c_str()) : 88491487UL;
|
||||
uint32_t hash = this->has_sta() ? App.get_config_version_hash() : 88491487UL;
|
||||
|
||||
this->pref_ = global_preferences->make_preference<wifi::SavedWifiSettings>(hash, true);
|
||||
#ifdef USE_WIFI_FAST_CONNECT
|
||||
@@ -394,7 +395,7 @@ void WiFiComponent::start() {
|
||||
|
||||
if (this->has_sta()) {
|
||||
this->wifi_sta_pre_setup_();
|
||||
if (this->output_power_.has_value() && !this->wifi_apply_output_power_(*this->output_power_)) {
|
||||
if (!std::isnan(this->output_power_) && !this->wifi_apply_output_power_(this->output_power_)) {
|
||||
ESP_LOGV(TAG, "Setting Output Power Option failed");
|
||||
}
|
||||
|
||||
@@ -441,7 +442,7 @@ void WiFiComponent::start() {
|
||||
#ifdef USE_WIFI_AP
|
||||
} else if (this->has_ap()) {
|
||||
this->setup_ap_config_();
|
||||
if (this->output_power_.has_value() && !this->wifi_apply_output_power_(*this->output_power_)) {
|
||||
if (!std::isnan(this->output_power_) && !this->wifi_apply_output_power_(this->output_power_)) {
|
||||
ESP_LOGV(TAG, "Setting Output Power Option failed");
|
||||
}
|
||||
#ifdef USE_CAPTIVE_PORTAL
|
||||
@@ -713,8 +714,8 @@ WiFiAP WiFiComponent::build_params_for_current_phase_() {
|
||||
case WiFiRetryPhase::RETRY_HIDDEN:
|
||||
// Hidden network mode: clear BSSID/channel to trigger probe request
|
||||
// (both explicit hidden and retry hidden use same behavior)
|
||||
params.set_bssid(optional<bssid_t>{});
|
||||
params.set_channel(optional<uint8_t>{});
|
||||
params.clear_bssid();
|
||||
params.clear_channel();
|
||||
break;
|
||||
|
||||
case WiFiRetryPhase::SCAN_CONNECTING:
|
||||
@@ -766,21 +767,20 @@ void WiFiComponent::start_connecting(const WiFiAP &ap) {
|
||||
char bssid_s[18];
|
||||
int8_t priority = 0;
|
||||
|
||||
if (ap.get_bssid().has_value()) {
|
||||
format_mac_addr_upper(ap.get_bssid().value().data(), bssid_s);
|
||||
priority = this->get_sta_priority(ap.get_bssid().value());
|
||||
if (ap.has_bssid()) {
|
||||
format_mac_addr_upper(ap.get_bssid().data(), bssid_s);
|
||||
priority = this->get_sta_priority(ap.get_bssid());
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG,
|
||||
"Connecting to " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") " (priority %d, attempt %u/%u in phase %s)...",
|
||||
ap.get_ssid().c_str(), ap.get_bssid().has_value() ? bssid_s : LOG_STR_LITERAL("any"), priority,
|
||||
this->num_retried_ + 1, get_max_retries_for_phase(this->retry_phase_),
|
||||
LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_)));
|
||||
ap.get_ssid().c_str(), ap.has_bssid() ? bssid_s : LOG_STR_LITERAL("any"), priority, this->num_retried_ + 1,
|
||||
get_max_retries_for_phase(this->retry_phase_), LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_)));
|
||||
|
||||
#ifdef ESPHOME_LOG_HAS_VERBOSE
|
||||
ESP_LOGV(TAG, "Connection Params:");
|
||||
ESP_LOGV(TAG, " SSID: '%s'", ap.get_ssid().c_str());
|
||||
if (ap.get_bssid().has_value()) {
|
||||
if (ap.has_bssid()) {
|
||||
ESP_LOGV(TAG, " BSSID: %s", bssid_s);
|
||||
} else {
|
||||
ESP_LOGV(TAG, " BSSID: Not Set");
|
||||
@@ -808,8 +808,8 @@ void WiFiComponent::start_connecting(const WiFiAP &ap) {
|
||||
#ifdef USE_WIFI_WPA2_EAP
|
||||
}
|
||||
#endif
|
||||
if (ap.get_channel().has_value()) {
|
||||
ESP_LOGV(TAG, " Channel: %u", *ap.get_channel());
|
||||
if (ap.has_channel()) {
|
||||
ESP_LOGV(TAG, " Channel: %u", ap.get_channel());
|
||||
} else {
|
||||
ESP_LOGV(TAG, " Channel not set");
|
||||
}
|
||||
@@ -919,8 +919,8 @@ void WiFiComponent::print_connect_params_() {
|
||||
get_wifi_channel(), wifi_subnet_mask_().str().c_str(), wifi_gateway_ip_().str().c_str(),
|
||||
wifi_dns_ip_(0).str().c_str(), wifi_dns_ip_(1).str().c_str());
|
||||
#ifdef ESPHOME_LOG_HAS_VERBOSE
|
||||
if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_bssid().has_value()) {
|
||||
ESP_LOGV(TAG, " Priority: %d", this->get_sta_priority(*config->get_bssid()));
|
||||
if (const WiFiAP *config = this->get_selected_sta_(); config && config->has_bssid()) {
|
||||
ESP_LOGV(TAG, " Priority: %d", this->get_sta_priority(config->get_bssid()));
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_WIFI_11KV_SUPPORT
|
||||
@@ -1031,23 +1031,30 @@ template<typename VectorType> static void insertion_sort_scan_results(VectorType
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to log scan results - marked noinline to prevent re-inlining into loop
|
||||
// Helper function to log matching scan results - marked noinline to prevent re-inlining into loop
|
||||
__attribute__((noinline)) static void log_scan_result(const WiFiScanResult &res) {
|
||||
char bssid_s[18];
|
||||
auto bssid = res.get_bssid();
|
||||
format_mac_addr_upper(bssid.data(), bssid_s);
|
||||
|
||||
if (res.get_matches()) {
|
||||
ESP_LOGI(TAG, "- '%s' %s" LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(),
|
||||
res.get_is_hidden() ? LOG_STR_LITERAL("(HIDDEN) ") : LOG_STR_LITERAL(""), bssid_s,
|
||||
LOG_STR_ARG(get_signal_bars(res.get_rssi())));
|
||||
ESP_LOGD(TAG, " Channel: %2u, RSSI: %3d dB, Priority: %4d", res.get_channel(), res.get_rssi(), res.get_priority());
|
||||
} else {
|
||||
ESP_LOGD(TAG, "- " LOG_SECRET("'%s'") " " LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), bssid_s,
|
||||
LOG_STR_ARG(get_signal_bars(res.get_rssi())));
|
||||
}
|
||||
ESP_LOGI(TAG, "- '%s' %s" LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(),
|
||||
res.get_is_hidden() ? LOG_STR_LITERAL("(HIDDEN) ") : LOG_STR_LITERAL(""), bssid_s,
|
||||
LOG_STR_ARG(get_signal_bars(res.get_rssi())));
|
||||
ESP_LOGD(TAG, " Channel: %2u, RSSI: %3d dB, Priority: %4d", res.get_channel(), res.get_rssi(), res.get_priority());
|
||||
}
|
||||
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
// Helper function to log non-matching scan results at verbose level
|
||||
__attribute__((noinline)) static void log_scan_result_non_matching(const WiFiScanResult &res) {
|
||||
char bssid_s[18];
|
||||
auto bssid = res.get_bssid();
|
||||
format_mac_addr_upper(bssid.data(), bssid_s);
|
||||
|
||||
ESP_LOGV(TAG, "- " LOG_SECRET("'%s'") " " LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), bssid_s,
|
||||
LOG_STR_ARG(get_signal_bars(res.get_rssi())));
|
||||
}
|
||||
#endif
|
||||
|
||||
void WiFiComponent::check_scanning_finished() {
|
||||
if (!this->scan_done_) {
|
||||
if (millis() - this->action_started_ > WIFI_SCAN_TIMEOUT_MS) {
|
||||
@@ -1084,8 +1091,20 @@ void WiFiComponent::check_scanning_finished() {
|
||||
// Sort scan results using insertion sort for better memory efficiency
|
||||
insertion_sort_scan_results(this->scan_result_);
|
||||
|
||||
size_t non_matching_count = 0;
|
||||
for (auto &res : this->scan_result_) {
|
||||
log_scan_result(res);
|
||||
if (res.get_matches()) {
|
||||
log_scan_result(res);
|
||||
} else {
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
log_scan_result_non_matching(res);
|
||||
#else
|
||||
non_matching_count++;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
if (non_matching_count > 0) {
|
||||
ESP_LOGD(TAG, "- %zu non-matching (VERBOSE to show)", non_matching_count);
|
||||
}
|
||||
|
||||
// SYNCHRONIZATION POINT: Establish link between scan_result_[0] and selected_sta_index_
|
||||
@@ -1495,9 +1514,9 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() {
|
||||
if (this->retry_phase_ == WiFiRetryPhase::SCAN_CONNECTING && !this->scan_result_.empty()) {
|
||||
// Scan-based phase: always use best result (index 0)
|
||||
failed_bssid = this->scan_result_[0].get_bssid();
|
||||
} else if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_bssid()) {
|
||||
} else if (const WiFiAP *config = this->get_selected_sta_(); config && config->has_bssid()) {
|
||||
// Config has specific BSSID (fast_connect or user-specified)
|
||||
failed_bssid = *config->get_bssid();
|
||||
failed_bssid = config->get_bssid();
|
||||
}
|
||||
|
||||
if (!failed_bssid.has_value()) {
|
||||
@@ -1765,24 +1784,27 @@ void WiFiComponent::save_fast_connect_settings_() {
|
||||
#endif
|
||||
|
||||
void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = ssid; }
|
||||
void WiFiAP::set_bssid(bssid_t bssid) { this->bssid_ = bssid; }
|
||||
void WiFiAP::set_bssid(optional<bssid_t> bssid) { this->bssid_ = bssid; }
|
||||
void WiFiAP::set_bssid(const bssid_t &bssid) { this->bssid_ = bssid; }
|
||||
void WiFiAP::clear_bssid() { this->bssid_ = {}; }
|
||||
void WiFiAP::set_password(const std::string &password) { this->password_ = password; }
|
||||
#ifdef USE_WIFI_WPA2_EAP
|
||||
void WiFiAP::set_eap(optional<EAPAuth> eap_auth) { this->eap_ = std::move(eap_auth); }
|
||||
#endif
|
||||
void WiFiAP::set_channel(optional<uint8_t> channel) { this->channel_ = channel; }
|
||||
void WiFiAP::set_channel(uint8_t channel) { this->channel_ = channel; }
|
||||
void WiFiAP::clear_channel() { this->channel_ = 0; }
|
||||
#ifdef USE_WIFI_MANUAL_IP
|
||||
void WiFiAP::set_manual_ip(optional<ManualIP> manual_ip) { this->manual_ip_ = manual_ip; }
|
||||
#endif
|
||||
void WiFiAP::set_hidden(bool hidden) { this->hidden_ = hidden; }
|
||||
const std::string &WiFiAP::get_ssid() const { return this->ssid_; }
|
||||
const optional<bssid_t> &WiFiAP::get_bssid() const { return this->bssid_; }
|
||||
const bssid_t &WiFiAP::get_bssid() const { return this->bssid_; }
|
||||
bool WiFiAP::has_bssid() const { return this->bssid_ != bssid_t{}; }
|
||||
const std::string &WiFiAP::get_password() const { return this->password_; }
|
||||
#ifdef USE_WIFI_WPA2_EAP
|
||||
const optional<EAPAuth> &WiFiAP::get_eap() const { return this->eap_; }
|
||||
#endif
|
||||
const optional<uint8_t> &WiFiAP::get_channel() const { return this->channel_; }
|
||||
uint8_t WiFiAP::get_channel() const { return this->channel_; }
|
||||
bool WiFiAP::has_channel() const { return this->channel_ != 0; }
|
||||
#ifdef USE_WIFI_MANUAL_IP
|
||||
const optional<ManualIP> &WiFiAP::get_manual_ip() const { return this->manual_ip_; }
|
||||
#endif
|
||||
@@ -1810,7 +1832,7 @@ bool WiFiScanResult::matches(const WiFiAP &config) const {
|
||||
// network is configured without SSID - match other settings
|
||||
}
|
||||
// If BSSID configured, only match for correct BSSIDs
|
||||
if (config.get_bssid().has_value() && *config.get_bssid() != this->bssid_)
|
||||
if (config.has_bssid() && config.get_bssid() != this->bssid_)
|
||||
return false;
|
||||
|
||||
#ifdef USE_WIFI_WPA2_EAP
|
||||
@@ -1828,7 +1850,7 @@ bool WiFiScanResult::matches(const WiFiAP &config) const {
|
||||
#endif
|
||||
|
||||
// If channel configured, only match networks on that channel.
|
||||
if (config.get_channel().has_value() && *config.get_channel() != this->channel_) {
|
||||
if (config.has_channel() && config.get_channel() != this->channel_) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -151,25 +151,28 @@ template<typename T> using wifi_scan_vector_t = FixedVector<T>;
|
||||
class WiFiAP {
|
||||
public:
|
||||
void set_ssid(const std::string &ssid);
|
||||
void set_bssid(bssid_t bssid);
|
||||
void set_bssid(optional<bssid_t> bssid);
|
||||
void set_bssid(const bssid_t &bssid);
|
||||
void clear_bssid();
|
||||
void set_password(const std::string &password);
|
||||
#ifdef USE_WIFI_WPA2_EAP
|
||||
void set_eap(optional<EAPAuth> eap_auth);
|
||||
#endif // USE_WIFI_WPA2_EAP
|
||||
void set_channel(optional<uint8_t> channel);
|
||||
void set_channel(uint8_t channel);
|
||||
void clear_channel();
|
||||
void set_priority(int8_t priority) { priority_ = priority; }
|
||||
#ifdef USE_WIFI_MANUAL_IP
|
||||
void set_manual_ip(optional<ManualIP> manual_ip);
|
||||
#endif
|
||||
void set_hidden(bool hidden);
|
||||
const std::string &get_ssid() const;
|
||||
const optional<bssid_t> &get_bssid() const;
|
||||
const bssid_t &get_bssid() const;
|
||||
bool has_bssid() const;
|
||||
const std::string &get_password() const;
|
||||
#ifdef USE_WIFI_WPA2_EAP
|
||||
const optional<EAPAuth> &get_eap() const;
|
||||
#endif // USE_WIFI_WPA2_EAP
|
||||
const optional<uint8_t> &get_channel() const;
|
||||
uint8_t get_channel() const;
|
||||
bool has_channel() const;
|
||||
int8_t get_priority() const { return priority_; }
|
||||
#ifdef USE_WIFI_MANUAL_IP
|
||||
const optional<ManualIP> &get_manual_ip() const;
|
||||
@@ -179,16 +182,17 @@ class WiFiAP {
|
||||
protected:
|
||||
std::string ssid_;
|
||||
std::string password_;
|
||||
optional<bssid_t> bssid_;
|
||||
#ifdef USE_WIFI_WPA2_EAP
|
||||
optional<EAPAuth> eap_;
|
||||
#endif // USE_WIFI_WPA2_EAP
|
||||
#ifdef USE_WIFI_MANUAL_IP
|
||||
optional<ManualIP> manual_ip_;
|
||||
#endif
|
||||
optional<uint8_t> channel_;
|
||||
int8_t priority_{0};
|
||||
bool hidden_{false};
|
||||
// Group small types together to minimize padding
|
||||
bssid_t bssid_{}; // 6 bytes, all zeros = any/not set
|
||||
uint8_t channel_{0}; // 1 byte, 0 = auto/not set
|
||||
int8_t priority_{0}; // 1 byte
|
||||
bool hidden_{false}; // 1 byte (+ 3 bytes end padding to 4-byte align)
|
||||
};
|
||||
|
||||
class WiFiScanResult {
|
||||
@@ -590,7 +594,7 @@ class WiFiComponent : public Component {
|
||||
#ifdef USE_WIFI_AP
|
||||
WiFiAP ap_;
|
||||
#endif
|
||||
optional<float> output_power_;
|
||||
float output_power_{NAN};
|
||||
#ifdef USE_WIFI_LISTENERS
|
||||
std::vector<WiFiIPStateListener *> ip_state_listeners_;
|
||||
std::vector<WiFiScanResultsListener *> scan_results_listeners_;
|
||||
|
||||
@@ -257,9 +257,9 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
||||
memcpy(reinterpret_cast<char *>(conf.ssid), ap.get_ssid().c_str(), ap.get_ssid().size());
|
||||
memcpy(reinterpret_cast<char *>(conf.password), ap.get_password().c_str(), ap.get_password().size());
|
||||
|
||||
if (ap.get_bssid().has_value()) {
|
||||
if (ap.has_bssid()) {
|
||||
conf.bssid_set = 1;
|
||||
memcpy(conf.bssid, ap.get_bssid()->data(), 6);
|
||||
memcpy(conf.bssid, ap.get_bssid().data(), 6);
|
||||
} else {
|
||||
conf.bssid_set = 0;
|
||||
}
|
||||
@@ -381,8 +381,8 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
||||
}
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
|
||||
if (ap.get_channel().has_value()) {
|
||||
ret = wifi_set_channel(*ap.get_channel());
|
||||
if (ap.has_channel()) {
|
||||
ret = wifi_set_channel(ap.get_channel());
|
||||
if (!ret) {
|
||||
ESP_LOGV(TAG, "wifi_set_channel failed");
|
||||
return false;
|
||||
@@ -845,7 +845,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
|
||||
}
|
||||
memcpy(reinterpret_cast<char *>(conf.ssid), ap.get_ssid().c_str(), ap.get_ssid().size());
|
||||
conf.ssid_len = static_cast<uint8>(ap.get_ssid().size());
|
||||
conf.channel = ap.get_channel().value_or(1);
|
||||
conf.channel = ap.has_channel() ? ap.get_channel() : 1;
|
||||
conf.ssid_hidden = ap.get_hidden();
|
||||
conf.max_connection = 5;
|
||||
conf.beacon_interval = 100;
|
||||
|
||||
@@ -339,14 +339,14 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
||||
conf.sta.rm_enabled = this->rrm_;
|
||||
#endif
|
||||
|
||||
if (ap.get_bssid().has_value()) {
|
||||
if (ap.has_bssid()) {
|
||||
conf.sta.bssid_set = true;
|
||||
memcpy(conf.sta.bssid, ap.get_bssid()->data(), 6);
|
||||
memcpy(conf.sta.bssid, ap.get_bssid().data(), 6);
|
||||
} else {
|
||||
conf.sta.bssid_set = false;
|
||||
}
|
||||
if (ap.get_channel().has_value()) {
|
||||
conf.sta.channel = *ap.get_channel();
|
||||
if (ap.has_channel()) {
|
||||
conf.sta.channel = ap.get_channel();
|
||||
conf.sta.scan_method = WIFI_FAST_SCAN;
|
||||
} else {
|
||||
conf.sta.scan_method = WIFI_ALL_CHANNEL_SCAN;
|
||||
@@ -1003,7 +1003,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
|
||||
return false;
|
||||
}
|
||||
memcpy(reinterpret_cast<char *>(conf.ap.ssid), ap.get_ssid().c_str(), ap.get_ssid().size());
|
||||
conf.ap.channel = ap.get_channel().value_or(1);
|
||||
conf.ap.channel = ap.has_channel() ? ap.get_channel() : 1;
|
||||
conf.ap.ssid_hidden = ap.get_ssid().size();
|
||||
conf.ap.max_connection = 5;
|
||||
conf.ap.beacon_interval = 100;
|
||||
|
||||
@@ -139,8 +139,8 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
||||
s_sta_connecting = true;
|
||||
|
||||
WiFiStatus status = WiFi.begin(ap.get_ssid().c_str(), ap.get_password().empty() ? NULL : ap.get_password().c_str(),
|
||||
ap.get_channel().has_value() ? *ap.get_channel() : 0,
|
||||
ap.get_bssid().has_value() ? ap.get_bssid()->data() : NULL);
|
||||
ap.get_channel(), // 0 = auto
|
||||
ap.has_bssid() ? ap.get_bssid().data() : NULL);
|
||||
if (status != WL_CONNECTED) {
|
||||
ESP_LOGW(TAG, "esp_wifi_connect failed: %d", status);
|
||||
return false;
|
||||
@@ -522,7 +522,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
|
||||
yield();
|
||||
|
||||
return WiFi.softAP(ap.get_ssid().c_str(), ap.get_password().empty() ? NULL : ap.get_password().c_str(),
|
||||
ap.get_channel().value_or(1), ap.get_hidden());
|
||||
ap.has_channel() ? ap.get_channel() : 1, ap.get_hidden());
|
||||
}
|
||||
|
||||
network::IPAddress WiFiComponent::wifi_soft_ap_ip() { return {WiFi.softAPIP()}; }
|
||||
|
||||
@@ -192,7 +192,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
|
||||
}
|
||||
#endif
|
||||
|
||||
WiFi.beginAP(ap.get_ssid().c_str(), ap.get_password().c_str(), ap.get_channel().value_or(1));
|
||||
WiFi.beginAP(ap.get_ssid().c_str(), ap.get_password().c_str(), ap.has_channel() ? ap.get_channel() : 1);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -608,6 +608,8 @@ class EsphomeCore:
|
||||
self.current_component: str | None = None
|
||||
# Address cache for DNS and mDNS lookups from command line arguments
|
||||
self.address_cache: AddressCache | None = None
|
||||
# Cached config hash (computed lazily)
|
||||
self._config_hash: int | None = None
|
||||
|
||||
def reset(self):
|
||||
from esphome.pins import PIN_SCHEMA_REGISTRY
|
||||
@@ -636,6 +638,7 @@ class EsphomeCore:
|
||||
self.unique_ids = {}
|
||||
self.current_component = None
|
||||
self.address_cache = None
|
||||
self._config_hash = None
|
||||
PIN_SCHEMA_REGISTRY.reset()
|
||||
|
||||
@contextmanager
|
||||
@@ -685,6 +688,21 @@ class EsphomeCore:
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def config_hash(self) -> int:
|
||||
"""Get the FNV-1a 32-bit hash of the config.
|
||||
|
||||
The hash is computed lazily and cached for performance.
|
||||
Uses sort_keys=True to ensure deterministic ordering.
|
||||
"""
|
||||
if self._config_hash is None:
|
||||
from esphome import yaml_util
|
||||
from esphome.helpers import fnv1a_32bit_hash
|
||||
|
||||
config_str = yaml_util.dump(self.config, show_secrets=True, sort_keys=True)
|
||||
self._config_hash = fnv1a_32bit_hash(config_str)
|
||||
return self._config_hash
|
||||
|
||||
@property
|
||||
def config_dir(self) -> Path:
|
||||
if self.config_path.is_dir():
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/build_info_data.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
#include <cstring>
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
#include <pgmspace.h>
|
||||
#endif
|
||||
#include "esphome/core/version.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include <algorithm>
|
||||
@@ -191,7 +198,9 @@ void Application::loop() {
|
||||
|
||||
if (this->dump_config_at_ < this->components_.size()) {
|
||||
if (this->dump_config_at_ == 0) {
|
||||
ESP_LOGI(TAG, "ESPHome version " ESPHOME_VERSION " compiled on %s", this->compilation_time_);
|
||||
char build_time_str[Application::BUILD_TIME_STR_SIZE];
|
||||
this->get_build_time_string(build_time_str);
|
||||
ESP_LOGI(TAG, "ESPHome version " ESPHOME_VERSION " compiled on %s", build_time_str);
|
||||
#ifdef ESPHOME_PROJECT_NAME
|
||||
ESP_LOGI(TAG, "Project " ESPHOME_PROJECT_NAME " version " ESPHOME_PROJECT_VERSION);
|
||||
#endif
|
||||
@@ -711,4 +720,9 @@ void Application::wake_loop_threadsafe() {
|
||||
}
|
||||
#endif // defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
|
||||
|
||||
void Application::get_build_time_string(std::span<char, BUILD_TIME_STR_SIZE> buffer) {
|
||||
ESPHOME_strncpy_P(buffer.data(), ESPHOME_BUILD_TIME_STR, buffer.size());
|
||||
buffer[buffer.size() - 1] = '\0';
|
||||
}
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <ctime>
|
||||
#include <limits>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "esphome/core/build_info_data.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/hal.h"
|
||||
@@ -11,6 +14,7 @@
|
||||
#include "esphome/core/preferences.h"
|
||||
#include "esphome/core/scheduler.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
#include "esphome/core/version.h"
|
||||
|
||||
#ifdef USE_DEVICES
|
||||
#include "esphome/core/device.h"
|
||||
@@ -101,7 +105,7 @@ static const uint32_t TEARDOWN_TIMEOUT_REBOOT_MS = 1000; // 1 second for quick
|
||||
class Application {
|
||||
public:
|
||||
void pre_setup(const std::string &name, const std::string &friendly_name, const char *comment,
|
||||
const char *compilation_time, bool name_add_mac_suffix) {
|
||||
bool name_add_mac_suffix) {
|
||||
arch_init();
|
||||
this->name_add_mac_suffix_ = name_add_mac_suffix;
|
||||
if (name_add_mac_suffix) {
|
||||
@@ -121,7 +125,6 @@ class Application {
|
||||
this->friendly_name_ = friendly_name;
|
||||
}
|
||||
this->comment_ = comment;
|
||||
this->compilation_time_ = compilation_time;
|
||||
}
|
||||
|
||||
#ifdef USE_DEVICES
|
||||
@@ -261,9 +264,30 @@ class Application {
|
||||
|
||||
bool is_name_add_mac_suffix_enabled() const { return this->name_add_mac_suffix_; }
|
||||
|
||||
std::string get_compilation_time() const { return this->compilation_time_; }
|
||||
/// Get the compilation time as StringRef (for API usage)
|
||||
StringRef get_compilation_time_ref() const { return StringRef(this->compilation_time_); }
|
||||
/// Size of buffer required for build time string (including null terminator)
|
||||
static constexpr size_t BUILD_TIME_STR_SIZE = 26;
|
||||
|
||||
/// Get the config hash as a 32-bit integer
|
||||
constexpr uint32_t get_config_hash() { return ESPHOME_CONFIG_HASH; }
|
||||
|
||||
/// Get the config hash extended with ESPHome version
|
||||
constexpr uint32_t get_config_version_hash() { return fnv1a_hash_extend(ESPHOME_CONFIG_HASH, ESPHOME_VERSION); }
|
||||
|
||||
/// Get the build time as a Unix timestamp
|
||||
constexpr time_t get_build_time() { return ESPHOME_BUILD_TIME; }
|
||||
|
||||
/// Copy the build time string into the provided buffer
|
||||
/// Buffer must be BUILD_TIME_STR_SIZE bytes (compile-time enforced)
|
||||
void get_build_time_string(std::span<char, BUILD_TIME_STR_SIZE> buffer);
|
||||
|
||||
/// Get the build time as a string (deprecated, use get_build_time_string() instead)
|
||||
// Remove before 2026.7.0
|
||||
ESPDEPRECATED("Use get_build_time_string() instead. Removed in 2026.7.0", "2026.1.0")
|
||||
std::string get_compilation_time() {
|
||||
char buf[BUILD_TIME_STR_SIZE];
|
||||
this->get_build_time_string(buf);
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
/// Get the cached time in milliseconds from when the current component started its loop execution
|
||||
inline uint32_t IRAM_ATTR HOT get_loop_component_start_time() const { return this->loop_component_start_time_; }
|
||||
@@ -478,7 +502,6 @@ class Application {
|
||||
// Pointer-sized members first
|
||||
Component *current_component_{nullptr};
|
||||
const char *comment_{nullptr};
|
||||
const char *compilation_time_{nullptr};
|
||||
|
||||
// std::vector (3 pointers each: begin, end, capacity)
|
||||
// Partitioned vector design for looping components
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
// This file is not used by the runtime, instead, a version is generated during
|
||||
// compilation with the actual build info values.
|
||||
//
|
||||
// This file is only used by static analyzers and IDEs.
|
||||
|
||||
#define ESPHOME_CONFIG_HASH 0x12345678U // NOLINT
|
||||
#define ESPHOME_BUILD_TIME 1700000000 // NOLINT
|
||||
static const char ESPHOME_BUILD_TIME_STR[] = "2024-01-01 00:00:00 +0000";
|
||||
@@ -382,10 +382,15 @@ def include_file(path: Path, basename: Path, is_c_header: bool = False):
|
||||
|
||||
|
||||
ARDUINO_GLUE_CODE = """\
|
||||
#undef yield
|
||||
#define yield() esphome::yield()
|
||||
#undef millis
|
||||
#define millis() esphome::millis()
|
||||
#undef micros
|
||||
#define micros() esphome::micros()
|
||||
#undef delay
|
||||
#define delay(x) esphome::delay(x)
|
||||
#undef delayMicroseconds
|
||||
#define delayMicroseconds(x) esphome::delayMicroseconds(x)
|
||||
"""
|
||||
|
||||
@@ -501,7 +506,6 @@ async def to_code(config: ConfigType) -> None:
|
||||
config[CONF_NAME],
|
||||
config[CONF_FRIENDLY_NAME],
|
||||
config.get(CONF_COMMENT, ""),
|
||||
cg.RawExpression('__DATE__ ", " __TIME__'),
|
||||
config[CONF_NAME_ADD_MAC_SUFFIX],
|
||||
)
|
||||
)
|
||||
@@ -537,7 +541,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
if config[CONF_DEBUG_SCHEDULER]:
|
||||
cg.add_define("ESPHOME_DEBUG_SCHEDULER")
|
||||
|
||||
if CORE.using_arduino and not CORE.is_bk72xx:
|
||||
if CORE.using_arduino:
|
||||
CORE.add_job(add_arduino_global_workaround)
|
||||
|
||||
if config[CONF_INCLUDES]:
|
||||
|
||||
@@ -146,7 +146,7 @@
|
||||
#define USE_OTA_PASSWORD
|
||||
#define USE_OTA_SHA256
|
||||
#define ALLOW_OTA_DOWNGRADE_MD5
|
||||
#define USE_OTA_STATE_CALLBACK
|
||||
#define USE_OTA_STATE_LISTENER
|
||||
#define USE_OTA_VERSION 2
|
||||
#define USE_TIME_TIMEZONE
|
||||
#define USE_WIFI
|
||||
|
||||
@@ -155,17 +155,6 @@ uint32_t fnv1_hash(const char *str) {
|
||||
return hash;
|
||||
}
|
||||
|
||||
// FNV-1a hash - preferred for new code
|
||||
uint32_t fnv1a_hash_extend(uint32_t hash, const char *str) {
|
||||
if (str) {
|
||||
while (*str) {
|
||||
hash ^= *str++;
|
||||
hash *= FNV1_PRIME;
|
||||
}
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
float random_float() { return static_cast<float>(random_uint32()) / static_cast<float>(UINT32_MAX); }
|
||||
|
||||
// Strings
|
||||
|
||||
+10
-2
@@ -388,12 +388,20 @@ constexpr uint32_t FNV1_OFFSET_BASIS = 2166136261UL;
|
||||
constexpr uint32_t FNV1_PRIME = 16777619UL;
|
||||
|
||||
/// Extend a FNV-1a hash with additional string data.
|
||||
uint32_t fnv1a_hash_extend(uint32_t hash, const char *str);
|
||||
constexpr uint32_t fnv1a_hash_extend(uint32_t hash, const char *str) {
|
||||
if (str) {
|
||||
while (*str) {
|
||||
hash ^= *str++;
|
||||
hash *= FNV1_PRIME;
|
||||
}
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
inline uint32_t fnv1a_hash_extend(uint32_t hash, const std::string &str) {
|
||||
return fnv1a_hash_extend(hash, str.c_str());
|
||||
}
|
||||
/// Calculate a FNV-1a hash of \p str.
|
||||
inline uint32_t fnv1a_hash(const char *str) { return fnv1a_hash_extend(FNV1_OFFSET_BASIS, str); }
|
||||
constexpr uint32_t fnv1a_hash(const char *str) { return fnv1a_hash_extend(FNV1_OFFSET_BASIS, str); }
|
||||
inline uint32_t fnv1a_hash(const std::string &str) { return fnv1a_hash(str.c_str()); }
|
||||
|
||||
/// Return a random 32-bit unsigned integer.
|
||||
|
||||
@@ -9,8 +9,10 @@
|
||||
#define ESPHOME_F(string_literal) F(string_literal)
|
||||
#define ESPHOME_PGM_P PGM_P
|
||||
#define ESPHOME_strncpy_P strncpy_P
|
||||
#define ESPHOME_strncat_P strncat_P
|
||||
#else
|
||||
#define ESPHOME_F(string_literal) (string_literal)
|
||||
#define ESPHOME_PGM_P const char *
|
||||
#define ESPHOME_strncpy_P strncpy
|
||||
#define ESPHOME_strncat_P strncat
|
||||
#endif
|
||||
|
||||
+8
-3
@@ -424,9 +424,13 @@ def write_file_if_changed(path: Path, text: str) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def copy_file_if_changed(src: Path, dst: Path) -> None:
|
||||
def copy_file_if_changed(src: Path, dst: Path) -> bool:
|
||||
"""Copy file from src to dst if contents differ.
|
||||
|
||||
Returns True if file was copied, False if files already matched.
|
||||
"""
|
||||
if file_compare(src, dst):
|
||||
return
|
||||
return False
|
||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||
try:
|
||||
shutil.copyfile(src, dst)
|
||||
@@ -441,11 +445,12 @@ def copy_file_if_changed(src: Path, dst: Path) -> None:
|
||||
with suppress(OSError):
|
||||
os.unlink(dst)
|
||||
shutil.copyfile(src, dst)
|
||||
return
|
||||
return True
|
||||
|
||||
from esphome.core import EsphomeError
|
||||
|
||||
raise EsphomeError(f"Error copying file {src} to {dst}: {err}") from err
|
||||
return True
|
||||
|
||||
|
||||
def list_starts_with(list_, sub):
|
||||
|
||||
@@ -187,7 +187,14 @@ def install_meta_finder(
|
||||
|
||||
|
||||
def install_custom_components_meta_finder():
|
||||
# Remove before 2026.6.0
|
||||
custom_components_dir = (Path(CORE.config_dir) / "custom_components").resolve()
|
||||
if custom_components_dir.is_dir() and any(custom_components_dir.iterdir()):
|
||||
_LOGGER.warning(
|
||||
"The 'custom_components' folder is deprecated and will be removed in 2026.6.0. "
|
||||
"Please use 'external_components' instead. "
|
||||
"See https://esphome.io/components/external_components.html for more information."
|
||||
)
|
||||
install_meta_finder(custom_components_dir)
|
||||
|
||||
|
||||
|
||||
+100
-9
@@ -1,11 +1,13 @@
|
||||
from collections.abc import Callable
|
||||
import importlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import stat
|
||||
import time
|
||||
from types import TracebackType
|
||||
|
||||
from esphome import loader
|
||||
@@ -23,6 +25,7 @@ from esphome.helpers import (
|
||||
is_ha_addon,
|
||||
read_file,
|
||||
walk_files,
|
||||
write_file,
|
||||
write_file_if_changed,
|
||||
)
|
||||
from esphome.storage_json import StorageJSON, storage_path
|
||||
@@ -173,6 +176,7 @@ VERSION_H_FORMAT = """\
|
||||
"""
|
||||
DEFINES_H_TARGET = "esphome/core/defines.h"
|
||||
VERSION_H_TARGET = "esphome/core/version.h"
|
||||
BUILD_INFO_DATA_H_TARGET = "esphome/core/build_info_data.h"
|
||||
ESPHOME_README_TXT = """
|
||||
THIS DIRECTORY IS AUTO-GENERATED, DO NOT MODIFY
|
||||
|
||||
@@ -206,10 +210,16 @@ def copy_src_tree():
|
||||
include_s = "\n".join(include_l)
|
||||
|
||||
source_files_copy = source_files_map.copy()
|
||||
ignore_targets = [Path(x) for x in (DEFINES_H_TARGET, VERSION_H_TARGET)]
|
||||
ignore_targets = [
|
||||
Path(x) for x in (DEFINES_H_TARGET, VERSION_H_TARGET, BUILD_INFO_DATA_H_TARGET)
|
||||
]
|
||||
for t in ignore_targets:
|
||||
source_files_copy.pop(t)
|
||||
source_files_copy.pop(t, None)
|
||||
|
||||
# Files to exclude from sources_changed tracking (generated files)
|
||||
generated_files = {Path("esphome/core/build_info_data.h")}
|
||||
|
||||
sources_changed = False
|
||||
for fname in walk_files(CORE.relative_src_path("esphome")):
|
||||
p = Path(fname)
|
||||
if p.suffix not in SOURCE_FILE_EXTENSIONS:
|
||||
@@ -223,28 +233,80 @@ def copy_src_tree():
|
||||
if target not in source_files_copy:
|
||||
# Source file removed, delete target
|
||||
p.unlink()
|
||||
if target not in generated_files:
|
||||
sources_changed = True
|
||||
else:
|
||||
src_file = source_files_copy.pop(target)
|
||||
with src_file.path() as src_path:
|
||||
copy_file_if_changed(src_path, p)
|
||||
if copy_file_if_changed(src_path, p) and target not in generated_files:
|
||||
sources_changed = True
|
||||
|
||||
# Now copy new files
|
||||
for target, src_file in source_files_copy.items():
|
||||
dst_path = CORE.relative_src_path(*target.parts)
|
||||
with src_file.path() as src_path:
|
||||
copy_file_if_changed(src_path, dst_path)
|
||||
if (
|
||||
copy_file_if_changed(src_path, dst_path)
|
||||
and target not in generated_files
|
||||
):
|
||||
sources_changed = True
|
||||
|
||||
# Finally copy defines
|
||||
write_file_if_changed(
|
||||
if write_file_if_changed(
|
||||
CORE.relative_src_path("esphome", "core", "defines.h"), generate_defines_h()
|
||||
)
|
||||
):
|
||||
sources_changed = True
|
||||
write_file_if_changed(CORE.relative_build_path("README.txt"), ESPHOME_README_TXT)
|
||||
write_file_if_changed(
|
||||
if write_file_if_changed(
|
||||
CORE.relative_src_path("esphome.h"), ESPHOME_H_FORMAT.format(include_s)
|
||||
)
|
||||
write_file_if_changed(
|
||||
):
|
||||
sources_changed = True
|
||||
if write_file_if_changed(
|
||||
CORE.relative_src_path("esphome", "core", "version.h"), generate_version_h()
|
||||
):
|
||||
sources_changed = True
|
||||
|
||||
# Generate new build_info files if needed
|
||||
build_info_data_h_path = CORE.relative_src_path(
|
||||
"esphome", "core", "build_info_data.h"
|
||||
)
|
||||
build_info_json_path = CORE.relative_build_path("build_info.json")
|
||||
config_hash, build_time, build_time_str = get_build_info()
|
||||
|
||||
# Defensively force a rebuild if the build_info files don't exist, or if
|
||||
# there was a config change which didn't actually cause a source change
|
||||
if not build_info_data_h_path.exists():
|
||||
sources_changed = True
|
||||
else:
|
||||
try:
|
||||
existing = json.loads(build_info_json_path.read_text(encoding="utf-8"))
|
||||
if (
|
||||
existing.get("config_hash") != config_hash
|
||||
or existing.get("esphome_version") != __version__
|
||||
):
|
||||
sources_changed = True
|
||||
except (json.JSONDecodeError, KeyError, OSError):
|
||||
sources_changed = True
|
||||
|
||||
# Write build_info header and JSON metadata
|
||||
if sources_changed:
|
||||
write_file(
|
||||
build_info_data_h_path,
|
||||
generate_build_info_data_h(config_hash, build_time, build_time_str),
|
||||
)
|
||||
write_file(
|
||||
build_info_json_path,
|
||||
json.dumps(
|
||||
{
|
||||
"config_hash": config_hash,
|
||||
"build_time": build_time,
|
||||
"build_time_str": build_time_str,
|
||||
"esphome_version": __version__,
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
+ "\n",
|
||||
)
|
||||
|
||||
platform = "esphome.components." + CORE.target_platform
|
||||
try:
|
||||
@@ -270,6 +332,35 @@ def generate_version_h():
|
||||
)
|
||||
|
||||
|
||||
def get_build_info() -> tuple[int, int, str]:
|
||||
"""Calculate build_info values from current config.
|
||||
|
||||
Returns:
|
||||
Tuple of (config_hash, build_time, build_time_str)
|
||||
"""
|
||||
config_hash = CORE.config_hash
|
||||
build_time = int(time.time())
|
||||
build_time_str = time.strftime("%Y-%m-%d %H:%M:%S %z", time.localtime(build_time))
|
||||
return config_hash, build_time, build_time_str
|
||||
|
||||
|
||||
def generate_build_info_data_h(
|
||||
config_hash: int, build_time: int, build_time_str: str
|
||||
) -> str:
|
||||
"""Generate build_info_data.h header with config hash and build time."""
|
||||
return f"""#pragma once
|
||||
// Auto-generated build_info data
|
||||
#define ESPHOME_CONFIG_HASH 0x{config_hash:08x}U // NOLINT
|
||||
#define ESPHOME_BUILD_TIME {build_time} // NOLINT
|
||||
#ifdef USE_ESP8266
|
||||
#include <pgmspace.h>
|
||||
static const char ESPHOME_BUILD_TIME_STR[] PROGMEM = "{build_time_str}";
|
||||
#else
|
||||
static const char ESPHOME_BUILD_TIME_STR[] = "{build_time_str}";
|
||||
#endif
|
||||
"""
|
||||
|
||||
|
||||
def write_cpp(code_s):
|
||||
path = CORE.relative_src_path("main.cpp")
|
||||
if path.is_file():
|
||||
|
||||
+10
-2
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from contextlib import suppress
|
||||
import functools
|
||||
import inspect
|
||||
from io import BytesIO, TextIOBase, TextIOWrapper
|
||||
@@ -501,13 +502,17 @@ def _load_yaml_internal_with_type(
|
||||
loader.dispose()
|
||||
|
||||
|
||||
def dump(dict_, show_secrets=False):
|
||||
def dump(dict_, show_secrets=False, sort_keys=False):
|
||||
"""Dump YAML to a string and remove null."""
|
||||
if show_secrets:
|
||||
_SECRET_VALUES.clear()
|
||||
_SECRET_CACHE.clear()
|
||||
return yaml.dump(
|
||||
dict_, default_flow_style=False, allow_unicode=True, Dumper=ESPHomeDumper
|
||||
dict_,
|
||||
default_flow_style=False,
|
||||
allow_unicode=True,
|
||||
Dumper=ESPHomeDumper,
|
||||
sort_keys=sort_keys,
|
||||
)
|
||||
|
||||
|
||||
@@ -543,6 +548,9 @@ class ESPHomeDumper(yaml.SafeDumper):
|
||||
best_style = True
|
||||
if hasattr(mapping, "items"):
|
||||
mapping = list(mapping.items())
|
||||
if self.sort_keys:
|
||||
with suppress(TypeError):
|
||||
mapping = sorted(mapping)
|
||||
for item_key, item_value in mapping:
|
||||
node_key = self.represent_data(item_key)
|
||||
node_value = self.represent_data(item_value)
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
cryptography==45.0.1
|
||||
voluptuous==0.15.2
|
||||
voluptuous==0.16.0
|
||||
PyYAML==6.0.3
|
||||
paho-mqtt==1.6.1
|
||||
colorama==0.4.6
|
||||
@@ -15,7 +15,7 @@ esphome-dashboard==20251013.0
|
||||
aioesphomeapi==43.3.0
|
||||
zeroconf==0.148.0
|
||||
puremagic==1.30
|
||||
ruamel.yaml==0.18.16 # dashboard_import
|
||||
ruamel.yaml==0.18.17 # dashboard_import
|
||||
ruamel.yaml.clib==0.2.15 # dashboard_import
|
||||
esphome-glyphsets==0.2.0
|
||||
pillow==11.3.0
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
pylint==4.0.4
|
||||
flake8==7.3.0 # also change in .pre-commit-config.yaml when updating
|
||||
ruff==0.14.9 # also change in .pre-commit-config.yaml when updating
|
||||
ruff==0.14.10 # also change in .pre-commit-config.yaml when updating
|
||||
pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating
|
||||
pre-commit
|
||||
|
||||
|
||||
@@ -1215,6 +1215,9 @@ class FixedArrayRepeatedType(TypeInfo):
|
||||
"""Helper to generate encode statement for a single element."""
|
||||
if isinstance(self._ti, EnumType):
|
||||
return f"buffer.{self._ti.encode_func}({self.number}, static_cast<uint32_t>({element}), true);"
|
||||
# MessageType.encode_message doesn't have a force parameter
|
||||
if isinstance(self._ti, MessageType):
|
||||
return f"buffer.{self._ti.encode_func}({self.number}, {element});"
|
||||
return f"buffer.{self._ti.encode_func}({self.number}, {element}, true);"
|
||||
|
||||
@property
|
||||
@@ -1536,6 +1539,15 @@ class RepeatedTypeInfo(TypeInfo):
|
||||
# std::vector is specialized for bool, reference does not work
|
||||
return isinstance(self._ti, BoolType)
|
||||
|
||||
def _encode_element_call(self, element: str) -> str:
|
||||
"""Helper to generate encode call for a single element."""
|
||||
if isinstance(self._ti, EnumType):
|
||||
return f"buffer.{self._ti.encode_func}({self.number}, static_cast<uint32_t>({element}), true);"
|
||||
# MessageType.encode_message doesn't have a force parameter
|
||||
if isinstance(self._ti, MessageType):
|
||||
return f"buffer.{self._ti.encode_func}({self.number}, {element});"
|
||||
return f"buffer.{self._ti.encode_func}({self.number}, {element}, true);"
|
||||
|
||||
@property
|
||||
def encode_content(self) -> str:
|
||||
if self._use_pointer:
|
||||
@@ -1546,17 +1558,11 @@ class RepeatedTypeInfo(TypeInfo):
|
||||
o += f" buffer.{self._ti.encode_func}({self.number}, it, strlen(it), true);\n"
|
||||
else:
|
||||
o = f"for (const auto &it : *this->{self.field_name}) {{\n"
|
||||
if isinstance(self._ti, EnumType):
|
||||
o += f" buffer.{self._ti.encode_func}({self.number}, static_cast<uint32_t>(it), true);\n"
|
||||
else:
|
||||
o += f" buffer.{self._ti.encode_func}({self.number}, it, true);\n"
|
||||
o += f" {self._encode_element_call('it')}\n"
|
||||
o += "}"
|
||||
return o
|
||||
o = f"for (auto {'' if self._ti_is_bool else '&'}it : this->{self.field_name}) {{\n"
|
||||
if isinstance(self._ti, EnumType):
|
||||
o += f" buffer.{self._ti.encode_func}({self.number}, static_cast<uint32_t>(it), true);\n"
|
||||
else:
|
||||
o += f" buffer.{self._ti.encode_func}({self.number}, it, true);\n"
|
||||
o += f" {self._encode_element_call('it')}\n"
|
||||
o += "}"
|
||||
return o
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ sensor:
|
||||
- platform: mmc5603
|
||||
i2c_id: i2c_bus
|
||||
address: 0x30
|
||||
auto_set_reset: true
|
||||
field_strength_x:
|
||||
name: HMC5883L Field Strength X
|
||||
field_strength_y:
|
||||
|
||||
@@ -25,4 +25,7 @@ sensor:
|
||||
name: Particulate Count >5.0um
|
||||
pm_10_0um:
|
||||
name: Particulate Count >10.0um
|
||||
aqi:
|
||||
name: AQI
|
||||
calculation_type: AQI
|
||||
update_interval: 30s
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
using namespace esphome;
|
||||
|
||||
void setup() {
|
||||
App.pre_setup("livingroom", "LivingRoom", "comment", __DATE__ ", " __TIME__, false);
|
||||
App.pre_setup("livingroom", "LivingRoom", "comment", false);
|
||||
auto *log = new logger::Logger(115200, 512); // NOLINT
|
||||
log->pre_setup();
|
||||
log->set_uart_selection(logger::UART_SELECTION_UART0);
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
esphome:
|
||||
name: alarm-state-transitions
|
||||
friendly_name: "Alarm Control Panel State Transitions Test"
|
||||
|
||||
logger:
|
||||
|
||||
host:
|
||||
|
||||
globals:
|
||||
- id: door_sensor_state
|
||||
type: bool
|
||||
initial_value: "false"
|
||||
- id: chime_sensor_state
|
||||
type: bool
|
||||
initial_value: "false"
|
||||
|
||||
switch:
|
||||
# Switch to control the door sensor state
|
||||
- platform: template
|
||||
id: door_sensor_switch
|
||||
name: "Door Sensor Switch"
|
||||
optimistic: true
|
||||
turn_on_action:
|
||||
- globals.set:
|
||||
id: door_sensor_state
|
||||
value: "true"
|
||||
turn_off_action:
|
||||
- globals.set:
|
||||
id: door_sensor_state
|
||||
value: "false"
|
||||
# Switch to control the chime sensor state
|
||||
- platform: template
|
||||
id: chime_sensor_switch
|
||||
name: "Chime Sensor Switch"
|
||||
optimistic: true
|
||||
turn_on_action:
|
||||
- globals.set:
|
||||
id: chime_sensor_state
|
||||
value: "true"
|
||||
turn_off_action:
|
||||
- globals.set:
|
||||
id: chime_sensor_state
|
||||
value: "false"
|
||||
|
||||
binary_sensor:
|
||||
- platform: template
|
||||
id: door_sensor
|
||||
name: "Door Sensor"
|
||||
lambda: |-
|
||||
return id(door_sensor_state);
|
||||
- platform: template
|
||||
id: chime_sensor
|
||||
name: "Chime Sensor"
|
||||
lambda: |-
|
||||
return id(chime_sensor_state);
|
||||
|
||||
alarm_control_panel:
|
||||
- platform: template
|
||||
id: test_alarm
|
||||
name: "Test Alarm"
|
||||
codes:
|
||||
- "1234"
|
||||
requires_code_to_arm: true
|
||||
# Short timeouts for faster testing
|
||||
arming_away_time: 50ms
|
||||
arming_home_time: 50ms
|
||||
arming_night_time: 50ms
|
||||
pending_time: 50ms
|
||||
trigger_time: 100ms
|
||||
restore_mode: ALWAYS_DISARMED
|
||||
binary_sensors:
|
||||
- input: door_sensor
|
||||
bypass_armed_home: false
|
||||
bypass_armed_night: false
|
||||
chime: false
|
||||
trigger_mode: DELAYED
|
||||
- input: chime_sensor
|
||||
bypass_armed_home: true
|
||||
bypass_armed_night: true
|
||||
chime: true
|
||||
trigger_mode: DELAYED
|
||||
on_state:
|
||||
- logger.log: "State changed"
|
||||
on_disarmed:
|
||||
- logger.log: "Alarm disarmed"
|
||||
on_arming:
|
||||
- logger.log: "Alarm arming"
|
||||
on_armed_away:
|
||||
- logger.log: "Alarm armed away"
|
||||
on_armed_home:
|
||||
- logger.log: "Alarm armed home"
|
||||
on_armed_night:
|
||||
- logger.log: "Alarm armed night"
|
||||
on_pending:
|
||||
- logger.log: "Alarm pending"
|
||||
on_triggered:
|
||||
- logger.log: "Alarm triggered"
|
||||
on_cleared:
|
||||
- logger.log: "Alarm cleared"
|
||||
on_chime:
|
||||
- logger.log: "Chime activated"
|
||||
on_ready:
|
||||
- logger.log: "Sensors ready state changed"
|
||||
|
||||
api:
|
||||
batch_delay: 0ms
|
||||
@@ -0,0 +1,31 @@
|
||||
esphome:
|
||||
name: build-info-test
|
||||
host:
|
||||
api:
|
||||
logger:
|
||||
|
||||
text_sensor:
|
||||
- platform: template
|
||||
name: "Config Hash"
|
||||
id: config_hash_sensor
|
||||
update_interval: 100ms
|
||||
lambda: |-
|
||||
char buf[16];
|
||||
snprintf(buf, sizeof(buf), "0x%08x", App.get_config_hash());
|
||||
return std::string(buf);
|
||||
- platform: template
|
||||
name: "Build Time"
|
||||
id: build_time_sensor
|
||||
update_interval: 100ms
|
||||
lambda: |-
|
||||
char buf[32];
|
||||
snprintf(buf, sizeof(buf), "%ld", (long)App.get_build_time());
|
||||
return std::string(buf);
|
||||
- platform: template
|
||||
name: "Build Time String"
|
||||
id: build_time_str_sensor
|
||||
update_interval: 100ms
|
||||
lambda: |-
|
||||
char buf[Application::BUILD_TIME_STR_SIZE];
|
||||
App.get_build_time_string(buf);
|
||||
return std::string(buf);
|
||||
@@ -0,0 +1,319 @@
|
||||
"""Integration test for alarm control panel state transitions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import re
|
||||
|
||||
import aioesphomeapi
|
||||
from aioesphomeapi import (
|
||||
AlarmControlPanelCommand,
|
||||
AlarmControlPanelEntityState,
|
||||
AlarmControlPanelInfo,
|
||||
AlarmControlPanelState,
|
||||
SwitchInfo,
|
||||
)
|
||||
import pytest
|
||||
|
||||
from .state_utils import InitialStateHelper
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_alarm_control_panel_state_transitions(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test alarm control panel state transitions.
|
||||
|
||||
This comprehensive test verifies all state transitions and listener callbacks:
|
||||
|
||||
1. Basic arm/disarm sequences:
|
||||
- DISARMED -> ARMING -> ARMED_AWAY -> DISARMED
|
||||
- DISARMED -> ARMING -> ARMED_HOME -> DISARMED
|
||||
- DISARMED -> ARMING -> ARMED_NIGHT -> DISARMED
|
||||
|
||||
2. Wrong code rejection
|
||||
|
||||
3. Sensor triggering while armed:
|
||||
- ARMED_AWAY -> PENDING -> TRIGGERED (delayed sensor)
|
||||
- TRIGGERED -> ARMED_AWAY (auto-reset after trigger_time, fires on_cleared)
|
||||
|
||||
4. Chime functionality:
|
||||
- Sensor open while DISARMED triggers on_chime
|
||||
|
||||
5. Ready state:
|
||||
- Sensor state changes trigger on_ready
|
||||
"""
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
# Track log messages for callback verification
|
||||
log_lines: list[str] = []
|
||||
chime_future: asyncio.Future[bool] = loop.create_future()
|
||||
ready_futures: list[asyncio.Future[bool]] = []
|
||||
cleared_future: asyncio.Future[bool] = loop.create_future()
|
||||
|
||||
# Patterns to match log output from callbacks
|
||||
chime_pattern = re.compile(r"Chime activated")
|
||||
ready_pattern = re.compile(r"Sensors ready state changed")
|
||||
cleared_pattern = re.compile(r"Alarm cleared")
|
||||
|
||||
def on_log_line(line: str) -> None:
|
||||
log_lines.append(line)
|
||||
if not chime_future.done() and chime_pattern.search(line):
|
||||
chime_future.set_result(True)
|
||||
if ready_pattern.search(line):
|
||||
# Create new future for each ready event
|
||||
for fut in ready_futures:
|
||||
if not fut.done():
|
||||
fut.set_result(True)
|
||||
break
|
||||
if not cleared_future.done() and cleared_pattern.search(line):
|
||||
cleared_future.set_result(True)
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config, line_callback=on_log_line),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
entities, _ = await client.list_entities_services()
|
||||
|
||||
# Find entities
|
||||
alarm_info: AlarmControlPanelInfo | None = None
|
||||
door_switch_info: SwitchInfo | None = None
|
||||
chime_switch_info: SwitchInfo | None = None
|
||||
|
||||
for entity in entities:
|
||||
if isinstance(entity, AlarmControlPanelInfo):
|
||||
alarm_info = entity
|
||||
elif isinstance(entity, SwitchInfo):
|
||||
if entity.name == "Door Sensor Switch":
|
||||
door_switch_info = entity
|
||||
elif entity.name == "Chime Sensor Switch":
|
||||
chime_switch_info = entity
|
||||
|
||||
assert alarm_info is not None, "Alarm control panel not found"
|
||||
assert door_switch_info is not None, "Door sensor switch not found"
|
||||
assert chime_switch_info is not None, "Chime sensor switch not found"
|
||||
|
||||
# Track state changes
|
||||
states_received: list[AlarmControlPanelState] = []
|
||||
state_event = asyncio.Event()
|
||||
|
||||
def on_state(state: aioesphomeapi.EntityState) -> None:
|
||||
if (
|
||||
isinstance(state, AlarmControlPanelEntityState)
|
||||
and state.key == alarm_info.key
|
||||
):
|
||||
states_received.append(state.state)
|
||||
state_event.set()
|
||||
|
||||
# Use InitialStateHelper to handle initial state broadcast
|
||||
initial_state_helper = InitialStateHelper(entities)
|
||||
client.subscribe_states(initial_state_helper.on_state_wrapper(on_state))
|
||||
|
||||
# Wait for initial states from all entities
|
||||
await initial_state_helper.wait_for_initial_states()
|
||||
|
||||
# Verify alarm panel started in DISARMED state
|
||||
initial_alarm_state = initial_state_helper.initial_states.get(alarm_info.key)
|
||||
assert initial_alarm_state is not None, "No initial alarm state received"
|
||||
assert isinstance(initial_alarm_state, AlarmControlPanelEntityState)
|
||||
assert initial_alarm_state.state == AlarmControlPanelState.DISARMED
|
||||
|
||||
# Helper to wait for specific state
|
||||
async def wait_for_state(
|
||||
expected: AlarmControlPanelState, timeout: float = 5.0
|
||||
) -> None:
|
||||
deadline = loop.time() + timeout
|
||||
while True:
|
||||
remaining = deadline - loop.time()
|
||||
if remaining <= 0:
|
||||
raise TimeoutError(
|
||||
f"Timeout waiting for state {expected}, "
|
||||
f"last state: {states_received[-1] if states_received else 'none'}"
|
||||
)
|
||||
await asyncio.wait_for(state_event.wait(), timeout=remaining)
|
||||
state_event.clear()
|
||||
if states_received[-1] == expected:
|
||||
return
|
||||
|
||||
# ===== Test wrong code rejection =====
|
||||
client.alarm_control_panel_command(
|
||||
alarm_info.key,
|
||||
AlarmControlPanelCommand.ARM_AWAY,
|
||||
code="0000", # Wrong code
|
||||
)
|
||||
|
||||
# Should NOT transition - wait a bit and verify no state changes
|
||||
with pytest.raises(asyncio.TimeoutError):
|
||||
await asyncio.wait_for(state_event.wait(), timeout=0.5)
|
||||
# No state changes should have occurred (list is empty)
|
||||
assert len(states_received) == 0, f"Unexpected state changes: {states_received}"
|
||||
|
||||
# ===== Test ARM_AWAY sequence =====
|
||||
client.alarm_control_panel_command(
|
||||
alarm_info.key,
|
||||
AlarmControlPanelCommand.ARM_AWAY,
|
||||
code="1234",
|
||||
)
|
||||
await wait_for_state(AlarmControlPanelState.ARMING)
|
||||
await wait_for_state(AlarmControlPanelState.ARMED_AWAY)
|
||||
|
||||
# Disarm
|
||||
client.alarm_control_panel_command(
|
||||
alarm_info.key,
|
||||
AlarmControlPanelCommand.DISARM,
|
||||
code="1234",
|
||||
)
|
||||
await wait_for_state(AlarmControlPanelState.DISARMED)
|
||||
|
||||
# ===== Test ARM_HOME sequence =====
|
||||
client.alarm_control_panel_command(
|
||||
alarm_info.key,
|
||||
AlarmControlPanelCommand.ARM_HOME,
|
||||
code="1234",
|
||||
)
|
||||
await wait_for_state(AlarmControlPanelState.ARMING)
|
||||
await wait_for_state(AlarmControlPanelState.ARMED_HOME)
|
||||
|
||||
# Disarm
|
||||
client.alarm_control_panel_command(
|
||||
alarm_info.key,
|
||||
AlarmControlPanelCommand.DISARM,
|
||||
code="1234",
|
||||
)
|
||||
await wait_for_state(AlarmControlPanelState.DISARMED)
|
||||
|
||||
# ===== Test ARM_NIGHT sequence =====
|
||||
client.alarm_control_panel_command(
|
||||
alarm_info.key,
|
||||
AlarmControlPanelCommand.ARM_NIGHT,
|
||||
code="1234",
|
||||
)
|
||||
await wait_for_state(AlarmControlPanelState.ARMING)
|
||||
await wait_for_state(AlarmControlPanelState.ARMED_NIGHT)
|
||||
|
||||
# Disarm
|
||||
client.alarm_control_panel_command(
|
||||
alarm_info.key,
|
||||
AlarmControlPanelCommand.DISARM,
|
||||
code="1234",
|
||||
)
|
||||
await wait_for_state(AlarmControlPanelState.DISARMED)
|
||||
|
||||
# Verify basic state sequence (initial DISARMED is handled by InitialStateHelper)
|
||||
expected_states = [
|
||||
AlarmControlPanelState.ARMING, # Arm away
|
||||
AlarmControlPanelState.ARMED_AWAY,
|
||||
AlarmControlPanelState.DISARMED,
|
||||
AlarmControlPanelState.ARMING, # Arm home
|
||||
AlarmControlPanelState.ARMED_HOME,
|
||||
AlarmControlPanelState.DISARMED,
|
||||
AlarmControlPanelState.ARMING, # Arm night
|
||||
AlarmControlPanelState.ARMED_NIGHT,
|
||||
AlarmControlPanelState.DISARMED,
|
||||
]
|
||||
assert states_received == expected_states, (
|
||||
f"State sequence mismatch.\nExpected: {expected_states}\n"
|
||||
f"Got: {states_received}"
|
||||
)
|
||||
|
||||
# ===== Test PENDING -> TRIGGERED -> CLEARED sequence =====
|
||||
# This tests on_pending, on_triggered, and on_cleared callbacks
|
||||
|
||||
# Arm away first
|
||||
client.alarm_control_panel_command(
|
||||
alarm_info.key,
|
||||
AlarmControlPanelCommand.ARM_AWAY,
|
||||
code="1234",
|
||||
)
|
||||
await wait_for_state(AlarmControlPanelState.ARMING)
|
||||
await wait_for_state(AlarmControlPanelState.ARMED_AWAY)
|
||||
|
||||
# Trip the door sensor (delayed mode triggers PENDING first)
|
||||
client.switch_command(door_switch_info.key, True)
|
||||
|
||||
# Should go to PENDING (delayed sensor)
|
||||
await wait_for_state(AlarmControlPanelState.PENDING)
|
||||
|
||||
# Should go to TRIGGERED after pending_time (50ms)
|
||||
await wait_for_state(AlarmControlPanelState.TRIGGERED)
|
||||
|
||||
# Close the sensor
|
||||
client.switch_command(door_switch_info.key, False)
|
||||
|
||||
# Wait for trigger_time to expire and auto-reset (100ms)
|
||||
# The alarm should go back to ARMED_AWAY after trigger_time
|
||||
# This transition FROM TRIGGERED fires on_cleared
|
||||
await wait_for_state(AlarmControlPanelState.ARMED_AWAY, timeout=2.0)
|
||||
|
||||
# Verify on_cleared was logged
|
||||
try:
|
||||
await asyncio.wait_for(cleared_future, timeout=1.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(f"on_cleared callback not fired. Log lines: {log_lines[-20:]}")
|
||||
|
||||
# Disarm
|
||||
client.alarm_control_panel_command(
|
||||
alarm_info.key,
|
||||
AlarmControlPanelCommand.DISARM,
|
||||
code="1234",
|
||||
)
|
||||
await wait_for_state(AlarmControlPanelState.DISARMED)
|
||||
|
||||
# Verify trigger sequence was added
|
||||
assert AlarmControlPanelState.PENDING in states_received
|
||||
assert AlarmControlPanelState.TRIGGERED in states_received
|
||||
|
||||
# ===== Test chime (sensor open while disarmed) =====
|
||||
# The chime_sensor has chime: true, so opening it while disarmed
|
||||
# should trigger on_chime callback
|
||||
|
||||
# We're currently DISARMED - open the chime sensor
|
||||
client.switch_command(chime_switch_info.key, True)
|
||||
|
||||
# Wait for chime callback to be logged
|
||||
try:
|
||||
await asyncio.wait_for(chime_future, timeout=2.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(f"on_chime callback not fired. Log lines: {log_lines[-20:]}")
|
||||
|
||||
# Close the chime sensor
|
||||
client.switch_command(chime_switch_info.key, False)
|
||||
|
||||
# ===== Test ready state changes =====
|
||||
# Opening/closing sensors while disarmed affects ready state
|
||||
# The on_ready callback fires when sensors_ready changes
|
||||
|
||||
# Set up futures for ready state changes
|
||||
ready_future_1: asyncio.Future[bool] = loop.create_future()
|
||||
ready_future_2: asyncio.Future[bool] = loop.create_future()
|
||||
ready_futures.extend([ready_future_1, ready_future_2])
|
||||
|
||||
# Open door sensor (makes alarm not ready)
|
||||
client.switch_command(door_switch_info.key, True)
|
||||
|
||||
# Wait for first on_ready callback (not ready)
|
||||
try:
|
||||
await asyncio.wait_for(ready_future_1, timeout=2.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"on_ready callback not fired when sensor opened. "
|
||||
f"Log lines: {log_lines[-20:]}"
|
||||
)
|
||||
|
||||
# Close door sensor (makes alarm ready again)
|
||||
client.switch_command(door_switch_info.key, False)
|
||||
|
||||
# Wait for second on_ready callback (ready)
|
||||
try:
|
||||
await asyncio.wait_for(ready_future_2, timeout=2.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"on_ready callback not fired when sensor closed. "
|
||||
f"Log lines: {log_lines[-20:]}"
|
||||
)
|
||||
|
||||
# Final state should still be DISARMED
|
||||
assert states_received[-1] == AlarmControlPanelState.DISARMED
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user