Merge branch 'dev' into optolink

This commit is contained in:
j0ta29 2023-11-05 16:19:14 +01:00 committed by GitHub
commit 777dcd8d34
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
122 changed files with 5079 additions and 353 deletions

View file

@ -1,17 +1,13 @@
{ {
"name": "ESPHome Dev", "name": "ESPHome Dev",
"image": "ghcr.io/esphome/esphome-lint:dev", "image": "ghcr.io/esphome/esphome-lint:dev",
"postCreateCommand": [ "postCreateCommand": ["script/devcontainer-post-create"],
"script/devcontainer-post-create"
],
"containerEnv": { "containerEnv": {
"DEVCONTAINER": "1" "DEVCONTAINER": "1",
"PIP_BREAK_SYSTEM_PACKAGES": "1",
"PIP_ROOT_USER_ACTION": "ignore"
}, },
"runArgs": [ "runArgs": ["--privileged", "-e", "ESPHOME_DASHBOARD_USE_PING=1"],
"--privileged",
"-e",
"ESPHOME_DASHBOARD_USE_PING=1"
],
"appPort": 6052, "appPort": 6052,
"customizations": { "customizations": {
"vscode": { "vscode": {
@ -24,7 +20,7 @@
// cpp // cpp
"ms-vscode.cpptools", "ms-vscode.cpptools",
// editorconfig // editorconfig
"editorconfig.editorconfig", "editorconfig.editorconfig"
], ],
"settings": { "settings": {
"python.languageServer": "Pylance", "python.languageServer": "Pylance",

View file

@ -221,6 +221,29 @@ jobs:
id: set-matrix id: set-matrix
run: echo "matrix=$(ls tests/test*.yaml | jq -R -s -c 'split("\n")[:-1]')" >> $GITHUB_OUTPUT run: echo "matrix=$(ls tests/test*.yaml | jq -R -s -c 'split("\n")[:-1]')" >> $GITHUB_OUTPUT
validate-tests:
name: Validate YAML test ${{ matrix.file }}
runs-on: ubuntu-latest
needs:
- common
- compile-tests-list
strategy:
fail-fast: false
matrix:
file: ${{ fromJson(needs.compile-tests-list.outputs.matrix) }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.1
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Run esphome config ${{ matrix.file }}
run: |
. venv/bin/activate
esphome config ${{ matrix.file }}
compile-tests: compile-tests:
name: Run YAML test ${{ matrix.file }} name: Run YAML test ${{ matrix.file }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -234,6 +257,7 @@ jobs:
- pytest - pytest
- pyupgrade - pyupgrade
- compile-tests-list - compile-tests-list
- validate-tests
strategy: strategy:
fail-fast: false fail-fast: false
max-parallel: 2 max-parallel: 2

24
.github/workflows/needs-docs.yml vendored Normal file
View file

@ -0,0 +1,24 @@
name: Needs Docs
on:
pull_request:
types: [labeled, unlabeled]
jobs:
check:
name: Check
runs-on: ubuntu-latest
steps:
- name: Check for needs-docs label
uses: actions/github-script@v6.4.1
with:
script: |
const { data: labels } = await github.rest.issues.listLabelsOnIssue({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number
});
const needsDocs = labels.find(label => label.name === 'needs-docs');
if (needsDocs) {
core.setFailed('Pull request needs docs');
}

View file

@ -3,7 +3,7 @@
# See https://pre-commit.com/hooks.html for more hooks # See https://pre-commit.com/hooks.html for more hooks
repos: repos:
- repo: https://github.com/psf/black-pre-commit-mirror - repo: https://github.com/psf/black-pre-commit-mirror
rev: 23.10.0 rev: 23.10.1
hooks: hooks:
- id: black - id: black
args: args:
@ -11,7 +11,7 @@ repos:
- --quiet - --quiet
files: ^((esphome|script|tests)/.+)?[^/]+\.py$ files: ^((esphome|script|tests)/.+)?[^/]+\.py$
- repo: https://github.com/PyCQA/flake8 - repo: https://github.com/PyCQA/flake8
rev: 6.0.0 rev: 6.1.0
hooks: hooks:
- id: flake8 - id: flake8
additional_dependencies: additional_dependencies:

View file

@ -77,6 +77,7 @@ esphome/components/dashboard_import/* @esphome/core
esphome/components/debug/* @OttoWinter esphome/components/debug/* @OttoWinter
esphome/components/delonghi/* @grob6000 esphome/components/delonghi/* @grob6000
esphome/components/dfplayer/* @glmnet esphome/components/dfplayer/* @glmnet
esphome/components/dfrobot_sen0395/* @niklasweber
esphome/components/dht/* @OttoWinter esphome/components/dht/* @OttoWinter
esphome/components/display_menu_base/* @numo68 esphome/components/display_menu_base/* @numo68
esphome/components/dps310/* @kbx81 esphome/components/dps310/* @kbx81
@ -151,6 +152,7 @@ esphome/components/key_provider/* @ssieb
esphome/components/kuntze/* @ssieb esphome/components/kuntze/* @ssieb
esphome/components/lcd_menu/* @numo68 esphome/components/lcd_menu/* @numo68
esphome/components/ld2410/* @regevbr @sebcaps esphome/components/ld2410/* @regevbr @sebcaps
esphome/components/ld2420/* @descipher
esphome/components/ledc/* @OttoWinter esphome/components/ledc/* @OttoWinter
esphome/components/libretiny/* @kuba2k2 esphome/components/libretiny/* @kuba2k2
esphome/components/libretiny_pwm/* @kuba2k2 esphome/components/libretiny_pwm/* @kuba2k2
@ -182,6 +184,7 @@ esphome/components/mcp9808/* @k7hpn
esphome/components/md5/* @esphome/core esphome/components/md5/* @esphome/core
esphome/components/mdns/* @esphome/core esphome/components/mdns/* @esphome/core
esphome/components/media_player/* @jesserockz esphome/components/media_player/* @jesserockz
esphome/components/micronova/* @jorre05
esphome/components/microphone/* @jesserockz esphome/components/microphone/* @jesserockz
esphome/components/mics_4514/* @jesserockz esphome/components/mics_4514/* @jesserockz
esphome/components/midea/* @dudanov esphome/components/midea/* @dudanov
@ -217,7 +220,7 @@ esphome/components/optolink/* @j0ta29
esphome/components/ota/* @esphome/core esphome/components/ota/* @esphome/core
esphome/components/output/* @esphome/core esphome/components/output/* @esphome/core
esphome/components/pca6416a/* @Mat931 esphome/components/pca6416a/* @Mat931
esphome/components/pca9554/* @hwstar esphome/components/pca9554/* @clydebarrow @hwstar
esphome/components/pcf85063/* @brogon esphome/components/pcf85063/* @brogon
esphome/components/pcf8563/* @KoenBreeman esphome/components/pcf8563/* @KoenBreeman
esphome/components/pid/* @OttoWinter esphome/components/pid/* @OttoWinter

View file

@ -43,10 +43,11 @@ RUN \
zlib1g-dev=1:1.2.13.dfsg-1 \ zlib1g-dev=1:1.2.13.dfsg-1 \
libjpeg-dev=1:2.1.5-2 \ libjpeg-dev=1:2.1.5-2 \
libfreetype-dev=2.12.1+dfsg-5 \ libfreetype-dev=2.12.1+dfsg-5 \
libssl-dev=3.0.11-1~deb12u1 \ libssl-dev=3.0.11-1~deb12u2 \
libffi-dev=3.4.4-1 \ libffi-dev=3.4.4-1 \
cargo=0.66.0+ds1-1 \ cargo=0.66.0+ds1-1 \
pkg-config=1.8.1-1; \ pkg-config=1.8.1-1; \
gcc-arm-linux-gnueabihf=4:12.2.0-3; \
fi; \ fi; \
rm -rf \ rm -rf \
/tmp/* \ /tmp/* \
@ -100,6 +101,10 @@ ENV USERNAME="" PASSWORD=""
# Expose the dashboard to Docker # Expose the dashboard to Docker
EXPOSE 6052 EXPOSE 6052
# Run healthcheck (heartbeat)
HEALTHCHECK --interval=30s --timeout=30s \
CMD curl --fail http://localhost:6052/version -A "HealthCheck" || exit 1
COPY docker/docker_entrypoint.sh /entrypoint.sh COPY docker/docker_entrypoint.sh /entrypoint.sh
# The directory the user should mount their configuration files to # The directory the user should mount their configuration files to

View file

@ -18,6 +18,8 @@ from esphome.const import (
CONF_TRIGGER_ID, CONF_TRIGGER_ID,
CONF_EVENT, CONF_EVENT,
CONF_TAG, CONF_TAG,
CONF_ON_CLIENT_CONNECTED,
CONF_ON_CLIENT_DISCONNECTED,
) )
from esphome.core import coroutine_with_priority from esphome.core import coroutine_with_priority
@ -87,6 +89,12 @@ CONFIG_SCHEMA = cv.Schema(
cv.Required(CONF_KEY): validate_encryption_key, cv.Required(CONF_KEY): validate_encryption_key,
} }
), ),
cv.Optional(CONF_ON_CLIENT_CONNECTED): automation.validate_automation(
single=True
),
cv.Optional(CONF_ON_CLIENT_DISCONNECTED): automation.validate_automation(
single=True
),
} }
).extend(cv.COMPONENT_SCHEMA) ).extend(cv.COMPONENT_SCHEMA)
@ -116,6 +124,20 @@ async def to_code(config):
cg.add(var.register_user_service(trigger)) cg.add(var.register_user_service(trigger))
await automation.build_automation(trigger, func_args, conf) await automation.build_automation(trigger, func_args, conf)
if CONF_ON_CLIENT_CONNECTED in config:
await automation.build_automation(
var.get_client_connected_trigger(),
[(cg.std_string, "client_info"), (cg.std_string, "client_address")],
config[CONF_ON_CLIENT_CONNECTED],
)
if CONF_ON_CLIENT_DISCONNECTED in config:
await automation.build_automation(
var.get_client_disconnected_trigger(),
[(cg.std_string, "client_info"), (cg.std_string, "client_address")],
config[CONF_ON_CLIENT_DISCONNECTED],
)
if encryption_config := config.get(CONF_ENCRYPTION): if encryption_config := config.get(CONF_ENCRYPTION):
decoded = base64.b64decode(encryption_config[CONF_KEY]) decoded = base64.b64decode(encryption_config[CONF_KEY])
cg.add(var.set_noise_psk(list(decoded))) cg.add(var.set_noise_psk(list(decoded)))

View file

@ -217,6 +217,8 @@ message DeviceInfoResponse {
string friendly_name = 13; string friendly_name = 13;
uint32 voice_assistant_version = 14; uint32 voice_assistant_version = 14;
string suggested_area = 16;
} }
message ListEntitiesRequest { message ListEntitiesRequest {

View file

@ -32,9 +32,9 @@ APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *pa
this->proto_write_buffer_.reserve(64); this->proto_write_buffer_.reserve(64);
#if defined(USE_API_PLAINTEXT) #if defined(USE_API_PLAINTEXT)
helper_ = std::unique_ptr<APIFrameHelper>{new APIPlaintextFrameHelper(std::move(sock))}; this->helper_ = std::unique_ptr<APIFrameHelper>{new APIPlaintextFrameHelper(std::move(sock))};
#elif defined(USE_API_NOISE) #elif defined(USE_API_NOISE)
helper_ = std::unique_ptr<APIFrameHelper>{new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx())}; this->helper_ = std::unique_ptr<APIFrameHelper>{new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx())};
#else #else
#error "No frame helper defined" #error "No frame helper defined"
#endif #endif
@ -42,14 +42,16 @@ APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *pa
void APIConnection::start() { void APIConnection::start() {
this->last_traffic_ = millis(); this->last_traffic_ = millis();
APIError err = helper_->init(); APIError err = this->helper_->init();
if (err != APIError::OK) { if (err != APIError::OK) {
on_fatal_error(); on_fatal_error();
ESP_LOGW(TAG, "%s: Helper init failed: %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno); ESP_LOGW(TAG, "%s: Helper init failed: %s errno=%d", this->client_combined_info_.c_str(), api_error_to_str(err),
errno);
return; return;
} }
client_info_ = helper_->getpeername(); this->client_info_ = helper_->getpeername();
helper_->set_log_info(client_info_); this->client_peername_ = this->client_info_;
this->helper_->set_log_info(this->client_info_);
} }
APIConnection::~APIConnection() { APIConnection::~APIConnection() {
@ -58,6 +60,11 @@ APIConnection::~APIConnection() {
bluetooth_proxy::global_bluetooth_proxy->unsubscribe_api_connection(this); bluetooth_proxy::global_bluetooth_proxy->unsubscribe_api_connection(this);
} }
#endif #endif
#ifdef USE_VOICE_ASSISTANT
if (voice_assistant::global_voice_assistant->get_api_connection() == this) {
voice_assistant::global_voice_assistant->client_subscription(this, false);
}
#endif
} }
void APIConnection::loop() { void APIConnection::loop() {
@ -68,7 +75,7 @@ void APIConnection::loop() {
// when network is disconnected force disconnect immediately // when network is disconnected force disconnect immediately
// don't wait for timeout // don't wait for timeout
this->on_fatal_error(); this->on_fatal_error();
ESP_LOGW(TAG, "%s: Network unavailable, disconnecting", client_info_.c_str()); ESP_LOGW(TAG, "%s: Network unavailable, disconnecting", this->client_combined_info_.c_str());
return; return;
} }
if (this->next_close_) { if (this->next_close_) {
@ -78,24 +85,26 @@ void APIConnection::loop() {
return; return;
} }
APIError err = helper_->loop(); APIError err = this->helper_->loop();
if (err != APIError::OK) { if (err != APIError::OK) {
on_fatal_error(); on_fatal_error();
ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno); ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->client_combined_info_.c_str(),
api_error_to_str(err), errno);
return; return;
} }
ReadPacketBuffer buffer; ReadPacketBuffer buffer;
err = helper_->read_packet(&buffer); err = this->helper_->read_packet(&buffer);
if (err == APIError::WOULD_BLOCK) { if (err == APIError::WOULD_BLOCK) {
// pass // pass
} else if (err != APIError::OK) { } else if (err != APIError::OK) {
on_fatal_error(); on_fatal_error();
if (err == APIError::SOCKET_READ_FAILED && errno == ECONNRESET) { if (err == APIError::SOCKET_READ_FAILED && errno == ECONNRESET) {
ESP_LOGW(TAG, "%s: Connection reset", client_info_.c_str()); ESP_LOGW(TAG, "%s: Connection reset", this->client_combined_info_.c_str());
} else if (err == APIError::CONNECTION_CLOSED) { } else if (err == APIError::CONNECTION_CLOSED) {
ESP_LOGW(TAG, "%s: Connection closed", client_info_.c_str()); ESP_LOGW(TAG, "%s: Connection closed", this->client_combined_info_.c_str());
} else { } else {
ESP_LOGW(TAG, "%s: Reading failed: %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno); ESP_LOGW(TAG, "%s: Reading failed: %s errno=%d", this->client_combined_info_.c_str(), api_error_to_str(err),
errno);
} }
return; return;
} else { } else {
@ -115,7 +124,7 @@ void APIConnection::loop() {
// Disconnect if not responded within 2.5*keepalive // Disconnect if not responded within 2.5*keepalive
if (now - this->last_traffic_ > (keepalive * 5) / 2) { if (now - this->last_traffic_ > (keepalive * 5) / 2) {
on_fatal_error(); on_fatal_error();
ESP_LOGW(TAG, "%s didn't respond to ping request in time. Disconnecting...", this->client_info_.c_str()); ESP_LOGW(TAG, "%s didn't respond to ping request in time. Disconnecting...", this->client_combined_info_.c_str());
} }
} else if (now - this->last_traffic_ > keepalive) { } else if (now - this->last_traffic_ > keepalive) {
ESP_LOGVV(TAG, "Sending keepalive PING..."); ESP_LOGVV(TAG, "Sending keepalive PING...");
@ -169,7 +178,7 @@ DisconnectResponse APIConnection::disconnect(const DisconnectRequest &msg) {
// remote initiated disconnect_client // remote initiated disconnect_client
// don't close yet, we still need to send the disconnect response // don't close yet, we still need to send the disconnect response
// close will happen on next loop // close will happen on next loop
ESP_LOGD(TAG, "%s requested disconnected", client_info_.c_str()); ESP_LOGD(TAG, "%s requested disconnected", this->client_combined_info_.c_str());
this->next_close_ = true; this->next_close_ = true;
DisconnectResponse resp; DisconnectResponse resp;
return resp; return resp;
@ -946,14 +955,17 @@ BluetoothConnectionsFreeResponse APIConnection::subscribe_bluetooth_connections_
#endif #endif
#ifdef USE_VOICE_ASSISTANT #ifdef USE_VOICE_ASSISTANT
bool APIConnection::request_voice_assistant(const VoiceAssistantRequest &msg) { void APIConnection::subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) {
if (!this->voice_assistant_subscription_) if (voice_assistant::global_voice_assistant != nullptr) {
return false; voice_assistant::global_voice_assistant->client_subscription(this, msg.subscribe);
}
return this->send_voice_assistant_request(msg);
} }
void APIConnection::on_voice_assistant_response(const VoiceAssistantResponse &msg) { void APIConnection::on_voice_assistant_response(const VoiceAssistantResponse &msg) {
if (voice_assistant::global_voice_assistant != nullptr) { if (voice_assistant::global_voice_assistant != nullptr) {
if (voice_assistant::global_voice_assistant->get_api_connection() != this) {
return;
}
if (msg.error) { if (msg.error) {
voice_assistant::global_voice_assistant->failed_to_start(); voice_assistant::global_voice_assistant->failed_to_start();
return; return;
@ -966,6 +978,10 @@ void APIConnection::on_voice_assistant_response(const VoiceAssistantResponse &ms
}; };
void APIConnection::on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg) { void APIConnection::on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg) {
if (voice_assistant::global_voice_assistant != nullptr) { if (voice_assistant::global_voice_assistant != nullptr) {
if (voice_assistant::global_voice_assistant->get_api_connection() != this) {
return;
}
voice_assistant::global_voice_assistant->on_event(msg); voice_assistant::global_voice_assistant->on_event(msg);
} }
} }
@ -1045,12 +1061,14 @@ bool APIConnection::send_log_message(int level, const char *tag, const char *lin
} }
HelloResponse APIConnection::hello(const HelloRequest &msg) { HelloResponse APIConnection::hello(const HelloRequest &msg) {
this->client_info_ = msg.client_info + " (" + this->helper_->getpeername() + ")"; this->client_info_ = msg.client_info;
this->helper_->set_log_info(client_info_); this->client_peername_ = this->helper_->getpeername();
this->client_combined_info_ = this->client_info_ + " (" + this->client_peername_ + ")";
this->helper_->set_log_info(this->client_combined_info_);
this->client_api_version_major_ = msg.api_version_major; this->client_api_version_major_ = msg.api_version_major;
this->client_api_version_minor_ = msg.api_version_minor; this->client_api_version_minor_ = msg.api_version_minor;
ESP_LOGV(TAG, "Hello from client: '%s' | API Version %" PRIu32 ".%" PRIu32, this->client_info_.c_str(), ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->client_info_.c_str(),
this->client_api_version_major_, this->client_api_version_minor_); this->client_peername_.c_str(), this->client_api_version_major_, this->client_api_version_minor_);
HelloResponse resp; HelloResponse resp;
resp.api_version_major = 1; resp.api_version_major = 1;
@ -1068,9 +1086,9 @@ ConnectResponse APIConnection::connect(const ConnectRequest &msg) {
// bool invalid_password = 1; // bool invalid_password = 1;
resp.invalid_password = !correct; resp.invalid_password = !correct;
if (correct) { if (correct) {
ESP_LOGD(TAG, "%s: Connected successfully", this->client_info_.c_str()); ESP_LOGD(TAG, "%s: Connected successfully", this->client_combined_info_.c_str());
this->connection_state_ = ConnectionState::AUTHENTICATED; this->connection_state_ = ConnectionState::AUTHENTICATED;
this->parent_->get_client_connected_trigger()->trigger(this->client_info_, this->client_peername_);
#ifdef USE_HOMEASSISTANT_TIME #ifdef USE_HOMEASSISTANT_TIME
if (homeassistant::global_homeassistant_time != nullptr) { if (homeassistant::global_homeassistant_time != nullptr) {
this->send_time_request(); this->send_time_request();
@ -1084,6 +1102,7 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) {
resp.uses_password = this->parent_->uses_password(); resp.uses_password = this->parent_->uses_password();
resp.name = App.get_name(); resp.name = App.get_name();
resp.friendly_name = App.get_friendly_name(); resp.friendly_name = App.get_friendly_name();
resp.suggested_area = App.get_area();
resp.mac_address = get_mac_address_pretty(); resp.mac_address = get_mac_address_pretty();
resp.esphome_version = ESPHOME_VERSION; resp.esphome_version = ESPHOME_VERSION;
resp.compilation_time = App.get_compilation_time(); resp.compilation_time = App.get_compilation_time();
@ -1144,10 +1163,11 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type)
return false; return false;
if (!this->helper_->can_write_without_blocking()) { if (!this->helper_->can_write_without_blocking()) {
delay(0); delay(0);
APIError err = helper_->loop(); APIError err = this->helper_->loop();
if (err != APIError::OK) { if (err != APIError::OK) {
on_fatal_error(); on_fatal_error();
ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno); ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->client_combined_info_.c_str(),
api_error_to_str(err), errno);
return false; return false;
} }
if (!this->helper_->can_write_without_blocking()) { if (!this->helper_->can_write_without_blocking()) {
@ -1166,9 +1186,10 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type)
if (err != APIError::OK) { if (err != APIError::OK) {
on_fatal_error(); on_fatal_error();
if (err == APIError::SOCKET_WRITE_FAILED && errno == ECONNRESET) { if (err == APIError::SOCKET_WRITE_FAILED && errno == ECONNRESET) {
ESP_LOGW(TAG, "%s: Connection reset", client_info_.c_str()); ESP_LOGW(TAG, "%s: Connection reset", this->client_combined_info_.c_str());
} else { } else {
ESP_LOGW(TAG, "%s: Packet write failed %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno); ESP_LOGW(TAG, "%s: Packet write failed %s errno=%d", this->client_combined_info_.c_str(), api_error_to_str(err),
errno);
} }
return false; return false;
} }
@ -1177,11 +1198,11 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type)
} }
void APIConnection::on_unauthenticated_access() { void APIConnection::on_unauthenticated_access() {
this->on_fatal_error(); this->on_fatal_error();
ESP_LOGD(TAG, "%s: tried to access without authentication.", this->client_info_.c_str()); ESP_LOGD(TAG, "%s: tried to access without authentication.", this->client_combined_info_.c_str());
} }
void APIConnection::on_no_setup_connection() { void APIConnection::on_no_setup_connection() {
this->on_fatal_error(); this->on_fatal_error();
ESP_LOGD(TAG, "%s: tried to access without full connection.", this->client_info_.c_str()); ESP_LOGD(TAG, "%s: tried to access without full connection.", this->client_combined_info_.c_str());
} }
void APIConnection::on_fatal_error() { void APIConnection::on_fatal_error() {
this->helper_->close(); this->helper_->close();

View file

@ -126,10 +126,7 @@ class APIConnection : public APIServerConnection {
#endif #endif
#ifdef USE_VOICE_ASSISTANT #ifdef USE_VOICE_ASSISTANT
void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) override { void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) override;
this->voice_assistant_subscription_ = msg.subscribe;
}
bool request_voice_assistant(const VoiceAssistantRequest &msg);
void on_voice_assistant_response(const VoiceAssistantResponse &msg) override; void on_voice_assistant_response(const VoiceAssistantResponse &msg) override;
void on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg) override; void on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg) override;
#endif #endif
@ -188,6 +185,8 @@ class APIConnection : public APIServerConnection {
} }
bool send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) override; bool send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) override;
std::string get_client_combined_info() const { return this->client_combined_info_; }
protected: protected:
friend APIServer; friend APIServer;
@ -207,6 +206,8 @@ class APIConnection : public APIServerConnection {
std::unique_ptr<APIFrameHelper> helper_; std::unique_ptr<APIFrameHelper> helper_;
std::string client_info_; std::string client_info_;
std::string client_peername_;
std::string client_combined_info_;
uint32_t client_api_version_major_{0}; uint32_t client_api_version_major_{0};
uint32_t client_api_version_minor_{0}; uint32_t client_api_version_minor_{0};
#ifdef USE_ESP32_CAMERA #ifdef USE_ESP32_CAMERA
@ -218,9 +219,6 @@ class APIConnection : public APIServerConnection {
uint32_t last_traffic_; uint32_t last_traffic_;
bool sent_ping_{false}; bool sent_ping_{false};
bool service_call_subscription_{false}; bool service_call_subscription_{false};
#ifdef USE_VOICE_ASSISTANT
bool voice_assistant_subscription_{false};
#endif
bool next_close_ = false; bool next_close_ = false;
APIServer *parent_; APIServer *parent_;
InitialStateIterator initial_state_iterator_; InitialStateIterator initial_state_iterator_;

View file

@ -761,6 +761,10 @@ bool DeviceInfoResponse::decode_length(uint32_t field_id, ProtoLengthDelimited v
this->friendly_name = value.as_string(); this->friendly_name = value.as_string();
return true; return true;
} }
case 16: {
this->suggested_area = value.as_string();
return true;
}
default: default:
return false; return false;
} }
@ -781,6 +785,7 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(12, this->manufacturer); buffer.encode_string(12, this->manufacturer);
buffer.encode_string(13, this->friendly_name); buffer.encode_string(13, this->friendly_name);
buffer.encode_uint32(14, this->voice_assistant_version); buffer.encode_uint32(14, this->voice_assistant_version);
buffer.encode_string(16, this->suggested_area);
} }
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void DeviceInfoResponse::dump_to(std::string &out) const { void DeviceInfoResponse::dump_to(std::string &out) const {
@ -849,6 +854,10 @@ void DeviceInfoResponse::dump_to(std::string &out) const {
sprintf(buffer, "%" PRIu32, this->voice_assistant_version); sprintf(buffer, "%" PRIu32, this->voice_assistant_version);
out.append(buffer); out.append(buffer);
out.append("\n"); out.append("\n");
out.append(" suggested_area: ");
out.append("'").append(this->suggested_area).append("'");
out.append("\n");
out.append("}"); out.append("}");
} }
#endif #endif

View file

@ -328,6 +328,7 @@ class DeviceInfoResponse : public ProtoMessage {
std::string manufacturer{}; std::string manufacturer{};
std::string friendly_name{}; std::string friendly_name{};
uint32_t voice_assistant_version{0}; uint32_t voice_assistant_version{0};
std::string suggested_area{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;

View file

@ -111,6 +111,7 @@ void APIServer::loop() {
[](const std::unique_ptr<APIConnection> &conn) { return !conn->remove_; }); [](const std::unique_ptr<APIConnection> &conn) { return !conn->remove_; });
// print disconnection messages // print disconnection messages
for (auto it = new_end; it != this->clients_.end(); ++it) { for (auto it = new_end; it != this->clients_.end(); ++it) {
this->client_disconnected_trigger_->trigger((*it)->client_info_, (*it)->client_peername_);
ESP_LOGV(TAG, "Removing connection to %s", (*it)->client_info_.c_str()); ESP_LOGV(TAG, "Removing connection to %s", (*it)->client_info_.c_str());
} }
// resize vector // resize vector
@ -331,30 +332,6 @@ void APIServer::on_shutdown() {
delay(10); delay(10);
} }
#ifdef USE_VOICE_ASSISTANT
bool APIServer::start_voice_assistant(const std::string &conversation_id, uint32_t flags,
const api::VoiceAssistantAudioSettings &audio_settings) {
VoiceAssistantRequest msg;
msg.start = true;
msg.conversation_id = conversation_id;
msg.flags = flags;
msg.audio_settings = audio_settings;
for (auto &c : this->clients_) {
if (c->request_voice_assistant(msg))
return true;
}
return false;
}
void APIServer::stop_voice_assistant() {
VoiceAssistantRequest msg;
msg.start = false;
for (auto &c : this->clients_) {
if (c->request_voice_assistant(msg))
return;
}
}
#endif
#ifdef USE_ALARM_CONTROL_PANEL #ifdef USE_ALARM_CONTROL_PANEL
void APIServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) { void APIServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) {
if (obj->is_internal()) if (obj->is_internal())

View file

@ -4,6 +4,7 @@
#include "api_pb2.h" #include "api_pb2.h"
#include "api_pb2_service.h" #include "api_pb2_service.h"
#include "esphome/components/socket/socket.h" #include "esphome/components/socket/socket.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/controller.h" #include "esphome/core/controller.h"
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
@ -83,12 +84,6 @@ class APIServer : public Component, public Controller {
void request_time(); void request_time();
#endif #endif
#ifdef USE_VOICE_ASSISTANT
bool start_voice_assistant(const std::string &conversation_id, uint32_t flags,
const api::VoiceAssistantAudioSettings &audio_settings);
void stop_voice_assistant();
#endif
#ifdef USE_ALARM_CONTROL_PANEL #ifdef USE_ALARM_CONTROL_PANEL
void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) override; void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) override;
#endif #endif
@ -106,6 +101,11 @@ class APIServer : public Component, public Controller {
const std::vector<HomeAssistantStateSubscription> &get_state_subs() const; const std::vector<HomeAssistantStateSubscription> &get_state_subs() const;
const std::vector<UserServiceDescriptor *> &get_user_services() const { return this->user_services_; } const std::vector<UserServiceDescriptor *> &get_user_services() const { return this->user_services_; }
Trigger<std::string, std::string> *get_client_connected_trigger() const { return this->client_connected_trigger_; }
Trigger<std::string, std::string> *get_client_disconnected_trigger() const {
return this->client_disconnected_trigger_;
}
protected: protected:
std::unique_ptr<socket::Socket> socket_ = nullptr; std::unique_ptr<socket::Socket> socket_ = nullptr;
uint16_t port_{6053}; uint16_t port_{6053};
@ -115,6 +115,8 @@ class APIServer : public Component, public Controller {
std::string password_; std::string password_;
std::vector<HomeAssistantStateSubscription> state_subs_; std::vector<HomeAssistantStateSubscription> state_subs_;
std::vector<UserServiceDescriptor *> user_services_; std::vector<UserServiceDescriptor *> user_services_;
Trigger<std::string, std::string> *client_connected_trigger_ = new Trigger<std::string, std::string>();
Trigger<std::string, std::string> *client_disconnected_trigger_ = new Trigger<std::string, std::string>();
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
std::shared_ptr<APINoiseContext> noise_ctx_ = std::make_shared<APINoiseContext>(); std::shared_ptr<APINoiseContext> noise_ctx_ = std::make_shared<APINoiseContext>();

View file

@ -48,7 +48,7 @@ void CaptivePortal::start() {
this->dns_server_ = make_unique<DNSServer>(); this->dns_server_ = make_unique<DNSServer>();
this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError); this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError);
network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip(); network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip();
this->dns_server_->start(53, "*", IPAddress(ip)); this->dns_server_->start(53, "*", ip);
#endif #endif
this->base_->get_server()->onNotFound([this](AsyncWebServerRequest *req) { this->base_->get_server()->onNotFound([this](AsyncWebServerRequest *req) {

View file

@ -0,0 +1,208 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import automation
from esphome import core
from esphome.automation import maybe_simple_id
from esphome.const import CONF_ID
from esphome.components import uart
CODEOWNERS = ["@niklasweber"]
DEPENDENCIES = ["uart"]
MULTI_CONF = True
dfrobot_sen0395_ns = cg.esphome_ns.namespace("dfrobot_sen0395")
DfrobotSen0395Component = dfrobot_sen0395_ns.class_(
"DfrobotSen0395Component", cg.Component
)
# Actions
DfrobotSen0395ResetAction = dfrobot_sen0395_ns.class_(
"DfrobotSen0395ResetAction", automation.Action
)
DfrobotSen0395SettingsAction = dfrobot_sen0395_ns.class_(
"DfrobotSen0395SettingsAction", automation.Action
)
CONF_DFROBOT_SEN0395_ID = "dfrobot_sen0395_id"
CONF_DELAY_AFTER_DETECT = "delay_after_detect"
CONF_DELAY_AFTER_DISAPPEAR = "delay_after_disappear"
CONF_DETECTION_SEGMENTS = "detection_segments"
CONF_OUTPUT_LATENCY = "output_latency"
CONF_FACTORY_RESET = "factory_reset"
CONF_SENSITIVITY = "sensitivity"
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(DfrobotSen0395Component),
}
).extend(uart.UART_DEVICE_SCHEMA)
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
@automation.register_action(
"dfrobot_sen0395.reset",
DfrobotSen0395ResetAction,
maybe_simple_id(
{
cv.GenerateID(): cv.use_id(DfrobotSen0395Component),
}
),
)
async def dfrobot_sen0395_reset_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var
def range_segment_list(input):
"""Validate input is a list of ranges which can be used to configure the dfrobot mmwave radar
A list of segments should be provided. A minimum of one segment is required and a maximum of
four segments is allowed. A segment describes a range of distances. E.g. from 0mm to 1m.
The distances need to be defined in an ascending order and they cannot contain / intersect
each other.
"""
# Flatten input to one dimensional list
flat_list = []
if isinstance(input, list):
for list_item in input:
if isinstance(list_item, list):
for item in list_item:
flat_list.append(item)
else:
flat_list.append(list_item)
else:
flat_list.append(input)
input = flat_list
if len(input) < 2:
raise cv.Invalid(
"At least two values need to be specified (start + stop distances)"
)
if len(input) % 2 != 0:
raise cv.Invalid(
"An even number of arguments must be specified (pairs of min + max)"
)
if len(input) > 8:
raise cv.Invalid(
"Maximum four segments can be specified (8 values: 4 * min + max)"
)
largest_distance = -1
for distance in input:
if isinstance(distance, core.Lambda):
continue
m = cv.distance(distance)
if m > 9:
raise cv.Invalid("Maximum distance is 9m")
if m < 0:
raise cv.Invalid("Minimum distance is 0m")
if m <= largest_distance:
raise cv.Invalid(
"Distances must be delared from small to large "
"and they cannot contain each other"
)
largest_distance = m
# Replace distance object with meters float
input[input.index(distance)] = m
return input
MMWAVE_SETTINGS_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.use_id(DfrobotSen0395Component),
cv.Optional(CONF_FACTORY_RESET): cv.templatable(cv.boolean),
cv.Optional(CONF_DETECTION_SEGMENTS): range_segment_list,
cv.Optional(CONF_OUTPUT_LATENCY): {
cv.Required(CONF_DELAY_AFTER_DETECT): cv.templatable(
cv.All(
cv.positive_time_period,
cv.Range(max=core.TimePeriod(seconds=1638.375)),
)
),
cv.Required(CONF_DELAY_AFTER_DISAPPEAR): cv.templatable(
cv.All(
cv.positive_time_period,
cv.Range(max=core.TimePeriod(seconds=1638.375)),
)
),
},
cv.Optional(CONF_SENSITIVITY): cv.templatable(cv.int_range(min=0, max=9)),
}
).add_extra(
cv.has_at_least_one_key(
CONF_FACTORY_RESET,
CONF_DETECTION_SEGMENTS,
CONF_OUTPUT_LATENCY,
CONF_SENSITIVITY,
)
)
@automation.register_action(
"dfrobot_sen0395.settings",
DfrobotSen0395SettingsAction,
MMWAVE_SETTINGS_SCHEMA,
)
async def dfrobot_sen0395_settings_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
if factory_reset_config := config.get(CONF_FACTORY_RESET):
template_ = await cg.templatable(factory_reset_config, args, int)
cg.add(var.set_factory_reset(template_))
if CONF_DETECTION_SEGMENTS in config:
segments = config[CONF_DETECTION_SEGMENTS]
if len(segments) >= 2:
template_ = await cg.templatable(segments[0], args, float)
cg.add(var.set_det_min1(template_))
template_ = await cg.templatable(segments[1], args, float)
cg.add(var.set_det_max1(template_))
if len(segments) >= 4:
template_ = await cg.templatable(segments[2], args, float)
cg.add(var.set_det_min2(template_))
template_ = await cg.templatable(segments[3], args, float)
cg.add(var.set_det_max2(template_))
if len(segments) >= 6:
template_ = await cg.templatable(segments[4], args, float)
cg.add(var.set_det_min3(template_))
template_ = await cg.templatable(segments[5], args, float)
cg.add(var.set_det_max3(template_))
if len(segments) >= 8:
template_ = await cg.templatable(segments[6], args, float)
cg.add(var.set_det_min4(template_))
template_ = await cg.templatable(segments[7], args, float)
cg.add(var.set_det_max4(template_))
if CONF_OUTPUT_LATENCY in config:
template_ = await cg.templatable(
config[CONF_OUTPUT_LATENCY][CONF_DELAY_AFTER_DETECT], args, float
)
if isinstance(template_, cv.TimePeriod):
template_ = template_.total_milliseconds / 1000
cg.add(var.set_delay_after_detect(template_))
template_ = await cg.templatable(
config[CONF_OUTPUT_LATENCY][CONF_DELAY_AFTER_DISAPPEAR], args, float
)
if isinstance(template_, cv.TimePeriod):
template_ = template_.total_milliseconds / 1000
cg.add(var.set_delay_after_disappear(template_))
if CONF_SENSITIVITY in config:
template_ = await cg.templatable(config[CONF_SENSITIVITY], args, int)
cg.add(var.set_sensitivity(template_))
return var

View file

@ -0,0 +1,89 @@
#pragma once
#include "esphome/core/automation.h"
#include "esphome/core/helpers.h"
#include "dfrobot_sen0395.h"
namespace esphome {
namespace dfrobot_sen0395 {
template<typename... Ts>
class DfrobotSen0395ResetAction : public Action<Ts...>, public Parented<DfrobotSen0395Component> {
public:
void play(Ts... x) { this->parent_->enqueue(make_unique<ResetSystemCommand>()); }
};
template<typename... Ts>
class DfrobotSen0395SettingsAction : public Action<Ts...>, public Parented<DfrobotSen0395Component> {
public:
TEMPLATABLE_VALUE(int8_t, factory_reset)
TEMPLATABLE_VALUE(int8_t, start_after_power_on)
TEMPLATABLE_VALUE(int8_t, turn_on_led)
TEMPLATABLE_VALUE(int8_t, presence_via_uart)
TEMPLATABLE_VALUE(int8_t, sensitivity)
TEMPLATABLE_VALUE(float, delay_after_detect)
TEMPLATABLE_VALUE(float, delay_after_disappear)
TEMPLATABLE_VALUE(float, det_min1)
TEMPLATABLE_VALUE(float, det_max1)
TEMPLATABLE_VALUE(float, det_min2)
TEMPLATABLE_VALUE(float, det_max2)
TEMPLATABLE_VALUE(float, det_min3)
TEMPLATABLE_VALUE(float, det_max3)
TEMPLATABLE_VALUE(float, det_min4)
TEMPLATABLE_VALUE(float, det_max4)
void play(Ts... x) {
this->parent_->enqueue(make_unique<PowerCommand>(0));
if (this->factory_reset_.has_value() && this->factory_reset_.value(x...) == true) {
this->parent_->enqueue(make_unique<FactoryResetCommand>());
}
if (this->det_min1_.has_value() && this->det_max1_.has_value()) {
if (this->det_min1_.value() >= 0 && this->det_max1_.value() >= 0) {
this->parent_->enqueue(make_unique<DetRangeCfgCommand>(
this->det_min1_.value_or(-1), this->det_max1_.value_or(-1), this->det_min2_.value_or(-1),
this->det_max2_.value_or(-1), this->det_min3_.value_or(-1), this->det_max3_.value_or(-1),
this->det_min4_.value_or(-1), this->det_max4_.value_or(-1)));
}
}
if (this->delay_after_detect_.has_value() && this->delay_after_disappear_.has_value()) {
float detect = this->delay_after_detect_.value(x...);
float disappear = this->delay_after_disappear_.value(x...);
if (detect >= 0 && disappear >= 0) {
this->parent_->enqueue(make_unique<OutputLatencyCommand>(detect, disappear));
}
}
if (this->start_after_power_on_.has_value()) {
int8_t val = this->start_after_power_on_.value(x...);
if (val >= 0) {
this->parent_->enqueue(make_unique<SensorCfgStartCommand>(val));
}
}
if (this->turn_on_led_.has_value()) {
int8_t val = this->turn_on_led_.value(x...);
if (val >= 0) {
this->parent_->enqueue(make_unique<LedModeCommand>(val));
}
}
if (this->presence_via_uart_.has_value()) {
int8_t val = this->presence_via_uart_.value(x...);
if (val >= 0) {
this->parent_->enqueue(make_unique<UartOutputCommand>(val));
}
}
if (this->sensitivity_.has_value()) {
int8_t val = this->sensitivity_.value(x...);
if (val >= 0) {
if (val > 9) {
val = 9;
}
this->parent_->enqueue(make_unique<SensitivityCommand>(val));
}
}
this->parent_->enqueue(make_unique<SaveCfgCommand>());
this->parent_->enqueue(make_unique<PowerCommand>(1));
}
};
} // namespace dfrobot_sen0395
} // namespace esphome

View file

@ -0,0 +1,22 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import binary_sensor
from esphome.const import DEVICE_CLASS_MOTION
from . import CONF_DFROBOT_SEN0395_ID, DfrobotSen0395Component
DEPENDENCIES = ["dfrobot_sen0395"]
CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_MOTION
).extend(
{
cv.GenerateID(CONF_DFROBOT_SEN0395_ID): cv.use_id(DfrobotSen0395Component),
}
)
async def to_code(config):
parent = await cg.get_variable(config[CONF_DFROBOT_SEN0395_ID])
binary_sens = await binary_sensor.new_binary_sensor(config)
cg.add(parent.set_detected_binary_sensor(binary_sens))

View file

@ -0,0 +1,329 @@
#include "commands.h"
#include "esphome/core/log.h"
#include "dfrobot_sen0395.h"
namespace esphome {
namespace dfrobot_sen0395 {
static const char *const TAG = "dfrobot_sen0395.commands";
uint8_t Command::execute(DfrobotSen0395Component *parent) {
this->parent_ = parent;
if (this->cmd_sent_) {
if (this->parent_->read_message_()) {
std::string message(this->parent_->read_buffer_);
if (message.rfind("is not recognized as a CLI command") != std::string::npos) {
ESP_LOGD(TAG, "Command not recognized properly by sensor");
if (this->retries_left_ > 0) {
this->retries_left_ -= 1;
this->cmd_sent_ = false;
ESP_LOGD(TAG, "Retrying...");
return 0;
} else {
this->parent_->find_prompt_();
return 1; // Command done
}
}
uint8_t rc = on_message(message);
if (rc == 2) {
if (this->retries_left_ > 0) {
this->retries_left_ -= 1;
this->cmd_sent_ = false;
ESP_LOGD(TAG, "Retrying...");
return 0;
} else {
this->parent_->find_prompt_();
return 1; // Command done
}
} else if (rc == 0) {
return 0;
} else {
this->parent_->find_prompt_();
return 1;
}
}
if (millis() - this->parent_->ts_last_cmd_sent_ > this->timeout_ms_) {
ESP_LOGD(TAG, "Command timeout");
if (this->retries_left_ > 0) {
this->retries_left_ -= 1;
this->cmd_sent_ = false;
ESP_LOGD(TAG, "Retrying...");
} else {
return 1; // Command done
}
}
} else if (this->parent_->send_cmd_(this->cmd_.c_str(), this->cmd_duration_ms_)) {
this->cmd_sent_ = true;
}
return 0; // Command not done yet
}
uint8_t ReadStateCommand::execute(DfrobotSen0395Component *parent) {
this->parent_ = parent;
if (this->parent_->read_message_()) {
std::string message(this->parent_->read_buffer_);
if (message.rfind("$JYBSS,0, , , *") != std::string::npos) {
this->parent_->set_detected_(false);
this->parent_->set_active(true);
return 1; // Command done
} else if (message.rfind("$JYBSS,1, , , *") != std::string::npos) {
this->parent_->set_detected_(true);
this->parent_->set_active(true);
return 1; // Command done
}
}
if (millis() - this->parent_->ts_last_cmd_sent_ > this->timeout_ms_) {
return 1; // Command done, timeout
}
return 0; // Command not done yet.
}
uint8_t ReadStateCommand::on_message(std::string &message) { return 1; }
uint8_t PowerCommand::on_message(std::string &message) {
if (message == "sensor stopped already") {
this->parent_->set_active(false);
ESP_LOGI(TAG, "Stopped sensor (already stopped)");
return 1; // Command done
} else if (message == "sensor started already") {
this->parent_->set_active(true);
ESP_LOGI(TAG, "Started sensor (already started)");
return 1; // Command done
} else if (message == "new parameter isn't save, can't startSensor") {
this->parent_->set_active(false);
ESP_LOGE(TAG, "Can't start sensor! (Use SaveCfgCommand to save config first)");
return 1; // Command done
} else if (message == "Done") {
this->parent_->set_active(this->power_on_);
if (this->power_on_) {
ESP_LOGI(TAG, "Started sensor");
} else {
ESP_LOGI(TAG, "Stopped sensor");
}
return 1; // Command done
}
return 0; // Command not done yet.
}
DetRangeCfgCommand::DetRangeCfgCommand(float min1, float max1, float min2, float max2, float min3, float max3,
float min4, float max4) {
// TODO: Print warning when values are rounded
if (min1 < 0 || max1 < 0) {
this->min1_ = min1 = 0;
this->max1_ = max1 = 0;
this->min2_ = min2 = this->max2_ = max2 = this->min3_ = min3 = this->max3_ = max3 = this->min4_ = min4 =
this->max4_ = max4 = -1;
ESP_LOGW(TAG, "DetRangeCfgCommand invalid input parameters. Using range config 0 0.");
this->cmd_ = "detRangeCfg -1 0 0";
} else if (min2 < 0 || max2 < 0) {
this->min1_ = min1 = round(min1 / 0.15) * 0.15;
this->max1_ = max1 = round(max1 / 0.15) * 0.15;
this->min2_ = min2 = this->max2_ = max2 = this->min3_ = min3 = this->max3_ = max3 = this->min4_ = min4 =
this->max4_ = max4 = -1;
this->cmd_ = str_sprintf("detRangeCfg -1 %.0f %.0f", min1 / 0.15, max1 / 0.15);
} else if (min3 < 0 || max3 < 0) {
this->min1_ = min1 = round(min1 / 0.15) * 0.15;
this->max1_ = max1 = round(max1 / 0.15) * 0.15;
this->min2_ = min2 = round(min2 / 0.15) * 0.15;
this->max2_ = max2 = round(max2 / 0.15) * 0.15;
this->min3_ = min3 = this->max3_ = max3 = this->min4_ = min4 = this->max4_ = max4 = -1;
this->cmd_ = str_sprintf("detRangeCfg -1 %.0f %.0f %.0f %.0f", min1 / 0.15, max1 / 0.15, min2 / 0.15, max2 / 0.15);
} else if (min4 < 0 || max4 < 0) {
this->min1_ = min1 = round(min1 / 0.15) * 0.15;
this->max1_ = max1 = round(max1 / 0.15) * 0.15;
this->min2_ = min2 = round(min2 / 0.15) * 0.15;
this->max2_ = max2 = round(max2 / 0.15) * 0.15;
this->min3_ = min3 = round(min3 / 0.15) * 0.15;
this->max3_ = max3 = round(max3 / 0.15) * 0.15;
this->min4_ = min4 = this->max4_ = max4 = -1;
this->cmd_ = str_sprintf("detRangeCfg -1 "
"%.0f %.0f %.0f %.0f %.0f %.0f",
min1 / 0.15, max1 / 0.15, min2 / 0.15, max2 / 0.15, min3 / 0.15, max3 / 0.15);
} else {
this->min1_ = min1 = round(min1 / 0.15) * 0.15;
this->max1_ = max1 = round(max1 / 0.15) * 0.15;
this->min2_ = min2 = round(min2 / 0.15) * 0.15;
this->max2_ = max2 = round(max2 / 0.15) * 0.15;
this->min3_ = min3 = round(min3 / 0.15) * 0.15;
this->max3_ = max3 = round(max3 / 0.15) * 0.15;
this->min4_ = min4 = round(min4 / 0.15) * 0.15;
this->max4_ = max4 = round(max4 / 0.15) * 0.15;
this->cmd_ = str_sprintf("detRangeCfg -1 "
"%.0f %.0f %.0f %.0f %.0f %.0f %.0f %.0f",
min1 / 0.15, max1 / 0.15, min2 / 0.15, max2 / 0.15, min3 / 0.15, max3 / 0.15, min4 / 0.15,
max4 / 0.15);
}
this->min1_ = min1;
this->max1_ = max1;
this->min2_ = min2;
this->max2_ = max2;
this->min3_ = min3;
this->max3_ = max3;
this->min4_ = min4;
this->max4_ = max4;
};
uint8_t DetRangeCfgCommand::on_message(std::string &message) {
if (message == "sensor is not stopped") {
ESP_LOGE(TAG, "Cannot configure range config. Sensor is not stopped!");
return 1; // Command done
} else if (message == "Done") {
ESP_LOGI(TAG, "Updated detection area config:");
ESP_LOGI(TAG, "Detection area 1 from %.02fm to %.02fm.", this->min1_, this->max1_);
if (this->min2_ >= 0 && this->max2_ >= 0) {
ESP_LOGI(TAG, "Detection area 2 from %.02fm to %.02fm.", this->min2_, this->max2_);
}
if (this->min3_ >= 0 && this->max3_ >= 0) {
ESP_LOGI(TAG, "Detection area 3 from %.02fm to %.02fm.", this->min3_, this->max3_);
}
if (this->min4_ >= 0 && this->max4_ >= 0) {
ESP_LOGI(TAG, "Detection area 4 from %.02fm to %.02fm.", this->min4_, this->max4_);
}
ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str());
return 1; // Command done
}
return 0; // Command not done yet.
}
OutputLatencyCommand::OutputLatencyCommand(float delay_after_detection, float delay_after_disappear) {
delay_after_detection = round(delay_after_detection / 0.025) * 0.025;
delay_after_disappear = round(delay_after_disappear / 0.025) * 0.025;
if (delay_after_detection < 0)
delay_after_detection = 0;
if (delay_after_detection > 1638.375)
delay_after_detection = 1638.375;
if (delay_after_disappear < 0)
delay_after_disappear = 0;
if (delay_after_disappear > 1638.375)
delay_after_disappear = 1638.375;
this->delay_after_detection_ = delay_after_detection;
this->delay_after_disappear_ = delay_after_disappear;
this->cmd_ = str_sprintf("outputLatency -1 %.0f %.0f", delay_after_detection / 0.025, delay_after_disappear / 0.025);
};
uint8_t OutputLatencyCommand::on_message(std::string &message) {
if (message == "sensor is not stopped") {
ESP_LOGE(TAG, "Cannot configure output latency. Sensor is not stopped!");
return 1; // Command done
} else if (message == "Done") {
ESP_LOGI(TAG, "Updated output latency config:");
ESP_LOGI(TAG, "Signal that someone was detected is delayed by %.02fs.", this->delay_after_detection_);
ESP_LOGI(TAG, "Signal that nobody is detected anymore is delayed by %.02fs.", this->delay_after_disappear_);
ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str());
return 1; // Command done
}
return 0; // Command not done yet
}
uint8_t SensorCfgStartCommand::on_message(std::string &message) {
if (message == "sensor is not stopped") {
ESP_LOGE(TAG, "Cannot configure sensor startup behavior. Sensor is not stopped!");
return 1; // Command done
} else if (message == "Done") {
ESP_LOGI(TAG, "Updated sensor startup behavior:");
if (startup_mode_) {
this->parent_->set_start_after_boot(true);
ESP_LOGI(TAG, "Sensor will start automatically after power-on.");
} else {
this->parent_->set_start_after_boot(false);
ESP_LOGI(TAG, "Sensor needs to be started manually after power-on.");
}
ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str());
return 1; // Command done
}
return 0; // Command not done yet
}
uint8_t FactoryResetCommand::on_message(std::string &message) {
if (message == "sensor is not stopped") {
ESP_LOGE(TAG, "Cannot factory reset. Sensor is not stopped!");
return 1; // Command done
} else if (message == "Done") {
ESP_LOGI(TAG, "Sensor factory reset done.");
return 1; // Command done
}
return 0; // Command not done yet
}
uint8_t ResetSystemCommand::on_message(std::string &message) {
if (message == "leapMMW:/>") {
ESP_LOGI(TAG, "Restarted sensor.");
return 1; // Command done
}
return 0; // Command not done yet
}
uint8_t SaveCfgCommand::on_message(std::string &message) {
if (message == "no parameter has changed") {
ESP_LOGI(TAG, "Not saving config (no parameter changed).");
return 1; // Command done
} else if (message == "Done") {
ESP_LOGI(TAG, "Saved config. Saving a lot may damage the sensor.");
return 1; // Command done
}
return 0; // Command not done yet
}
uint8_t LedModeCommand::on_message(std::string &message) {
if (message == "sensor is not stopped") {
ESP_LOGE(TAG, "Cannot set led mode. Sensor is not stopped!");
return 1; // Command done
} else if (message == "Done") {
ESP_LOGI(TAG, "Set led mode done.");
if (this->active_) {
this->parent_->set_led_active(true);
ESP_LOGI(TAG, "Sensor LED will blink.");
} else {
this->parent_->set_led_active(false);
ESP_LOGI(TAG, "Turned off LED.");
}
ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str());
return 1; // Command done
}
return 0; // Command not done yet
}
uint8_t UartOutputCommand::on_message(std::string &message) {
if (message == "sensor is not stopped") {
ESP_LOGE(TAG, "Cannot set uart output mode. Sensor is not stopped!");
return 1; // Command done
} else if (message == "Done") {
ESP_LOGI(TAG, "Set uart mode done.");
if (this->active_) {
this->parent_->set_uart_presence_active(true);
ESP_LOGI(TAG, "Presence information is sent via UART and GPIO.");
} else {
this->parent_->set_uart_presence_active(false);
ESP_LOGI(TAG, "Presence information is only sent via GPIO.");
}
ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str());
return 1; // Command done
}
return 0; // Command not done yet
}
uint8_t SensitivityCommand::on_message(std::string &message) {
if (message == "sensor is not stopped") {
ESP_LOGE(TAG, "Cannot set sensitivity. Sensor is not stopped!");
return 1; // Command done
} else if (message == "Done") {
ESP_LOGI(TAG, "Set sensitivity done. Set to value %d.", this->sensitivity_);
ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str());
return 1; // Command done
}
return 0; // Command not done yet
}
} // namespace dfrobot_sen0395
} // namespace esphome

View file

@ -0,0 +1,156 @@
#pragma once
#include <cstdint>
#include <string>
#include "esphome/core/helpers.h"
namespace esphome {
namespace dfrobot_sen0395 {
class DfrobotSen0395Component;
// Use command queue and time stamps to avoid blocking.
// When component has run time, check if minimum time (1s) between
// commands has passed. After that run a command from the queue.
class Command {
public:
virtual ~Command() = default;
virtual uint8_t execute(DfrobotSen0395Component *parent);
virtual uint8_t on_message(std::string &message) = 0;
protected:
DfrobotSen0395Component *parent_{nullptr};
std::string cmd_;
bool cmd_sent_{false};
int8_t retries_left_{2};
uint32_t cmd_duration_ms_{1000};
uint32_t timeout_ms_{1500};
};
class ReadStateCommand : public Command {
public:
uint8_t execute(DfrobotSen0395Component *parent) override;
uint8_t on_message(std::string &message) override;
protected:
uint32_t timeout_ms_{500};
};
class PowerCommand : public Command {
public:
PowerCommand(bool power_on) : power_on_(power_on) {
if (power_on) {
cmd_ = "sensorStart";
} else {
cmd_ = "sensorStop";
}
};
uint8_t on_message(std::string &message) override;
protected:
bool power_on_;
};
class DetRangeCfgCommand : public Command {
public:
DetRangeCfgCommand(float min1, float max1, float min2, float max2, float min3, float max3, float min4, float max4);
uint8_t on_message(std::string &message) override;
protected:
float min1_, max1_, min2_, max2_, min3_, max3_, min4_, max4_;
// TODO: Set min max values in component, so they can be published as sensor.
};
class OutputLatencyCommand : public Command {
public:
OutputLatencyCommand(float delay_after_detection, float delay_after_disappear);
uint8_t on_message(std::string &message) override;
protected:
float delay_after_detection_;
float delay_after_disappear_;
};
class SensorCfgStartCommand : public Command {
public:
SensorCfgStartCommand(bool startup_mode) : startup_mode_(startup_mode) {
char tmp_cmd[20] = {0};
sprintf(tmp_cmd, "sensorCfgStart %d", startup_mode);
cmd_ = std::string(tmp_cmd);
}
uint8_t on_message(std::string &message) override;
protected:
bool startup_mode_;
};
class FactoryResetCommand : public Command {
public:
FactoryResetCommand() { cmd_ = "factoryReset 0x45670123 0xCDEF89AB 0x956128C6 0xDF54AC89"; };
uint8_t on_message(std::string &message) override;
};
class ResetSystemCommand : public Command {
public:
ResetSystemCommand() { cmd_ = "resetSystem"; }
uint8_t on_message(std::string &message) override;
};
class SaveCfgCommand : public Command {
public:
SaveCfgCommand() { cmd_ = "saveCfg 0x45670123 0xCDEF89AB 0x956128C6 0xDF54AC89"; }
uint8_t on_message(std::string &message) override;
protected:
uint32_t cmd_duration_ms_{3000};
uint32_t timeout_ms_{3500};
};
class LedModeCommand : public Command {
public:
LedModeCommand(bool active) : active_(active) {
if (active) {
cmd_ = "setLedMode 1 0";
} else {
cmd_ = "setLedMode 1 1";
}
};
uint8_t on_message(std::string &message) override;
protected:
bool active_;
};
class UartOutputCommand : public Command {
public:
UartOutputCommand(bool active) : active_(active) {
if (active) {
cmd_ = "setUartOutput 1 1";
} else {
cmd_ = "setUartOutput 1 0";
}
};
uint8_t on_message(std::string &message) override;
protected:
bool active_;
};
class SensitivityCommand : public Command {
public:
SensitivityCommand(uint8_t sensitivity) : sensitivity_(sensitivity) {
if (sensitivity > 9)
sensitivity_ = sensitivity = 9;
char tmp_cmd[20] = {0};
sprintf(tmp_cmd, "setSensitivity %d", sensitivity);
cmd_ = std::string(tmp_cmd);
};
uint8_t on_message(std::string &message) override;
protected:
uint8_t sensitivity_;
};
} // namespace dfrobot_sen0395
} // namespace esphome

View file

@ -0,0 +1,142 @@
#include "dfrobot_sen0395.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
namespace dfrobot_sen0395 {
static const char *const TAG = "dfrobot_sen0395";
const char ASCII_CR = 0x0D;
const char ASCII_LF = 0x0A;
void DfrobotSen0395Component::dump_config() {
ESP_LOGCONFIG(TAG, "Dfrobot Mmwave Radar:");
#ifdef USE_BINARY_SENSOR
LOG_BINARY_SENSOR(" ", "Registered", this->detected_binary_sensor_);
#endif
#ifdef USE_SWITCH
LOG_SWITCH(" ", "Sensor Active Switch", this->sensor_active_switch_);
LOG_SWITCH(" ", "Turn on LED Switch", this->turn_on_led_switch_);
LOG_SWITCH(" ", "Presence via UART Switch", this->presence_via_uart_switch_);
LOG_SWITCH(" ", "Start after Boot Switch", this->start_after_boot_switch_);
#endif
}
void DfrobotSen0395Component::loop() {
if (cmd_queue_.is_empty()) {
// Command queue empty. Read sensor state.
cmd_queue_.enqueue(make_unique<ReadStateCommand>());
}
// Commands are non-blocking and need to be called repeatedly.
if (cmd_queue_.process(this)) {
// Dequeue if command is done
cmd_queue_.dequeue();
}
}
int8_t DfrobotSen0395Component::enqueue(std::unique_ptr<Command> cmd) {
return cmd_queue_.enqueue(std::move(cmd)); // Transfer ownership using std::move
}
uint8_t DfrobotSen0395Component::read_message_() {
while (this->available()) {
uint8_t byte;
this->read_byte(&byte);
if (this->read_pos_ == MMWAVE_READ_BUFFER_LENGTH)
this->read_pos_ = 0;
ESP_LOGVV(TAG, "Buffer pos: %u %d", this->read_pos_, byte);
if (byte == ASCII_CR)
continue;
if (byte >= 0x7F)
byte = '?'; // needs to be valid utf8 string for log functions.
this->read_buffer_[this->read_pos_] = byte;
if (this->read_pos_ == 9 && byte == '>')
this->read_buffer_[++this->read_pos_] = ASCII_LF;
if (this->read_buffer_[this->read_pos_] == ASCII_LF) {
this->read_buffer_[this->read_pos_] = 0;
this->read_pos_ = 0;
ESP_LOGV(TAG, "Message: %s", this->read_buffer_);
return 1; // Full message in buffer
} else {
this->read_pos_++;
}
}
return 0; // No full message yet
}
uint8_t DfrobotSen0395Component::find_prompt_() {
if (this->read_message_()) {
std::string message(this->read_buffer_);
if (message.rfind("leapMMW:/>") != std::string::npos) {
return 1; // Prompt found
}
}
return 0; // Not found yet
}
uint8_t DfrobotSen0395Component::send_cmd_(const char *cmd, uint32_t duration) {
// The interval between two commands must be larger than the specified duration (in ms).
if (millis() - ts_last_cmd_sent_ > duration) {
this->write_str(cmd);
ts_last_cmd_sent_ = millis();
return 1; // Command sent
}
// Could not send command yet as command duration did not fully pass yet.
return 0;
}
void DfrobotSen0395Component::set_detected_(bool detected) {
this->detected_ = detected;
#ifdef USE_BINARY_SENSOR
if (this->detected_binary_sensor_ != nullptr)
this->detected_binary_sensor_->publish_state(detected);
#endif
}
int8_t CircularCommandQueue::enqueue(std::unique_ptr<Command> cmd) {
if (this->is_full()) {
ESP_LOGE(TAG, "Command queue is full");
return -1;
} else if (this->is_empty())
front_++;
rear_ = (rear_ + 1) % COMMAND_QUEUE_SIZE;
commands_[rear_] = std::move(cmd); // Transfer ownership using std::move
return 1;
}
std::unique_ptr<Command> CircularCommandQueue::dequeue() {
if (this->is_empty())
return nullptr;
std::unique_ptr<Command> dequeued_cmd = std::move(commands_[front_]);
if (front_ == rear_) {
front_ = -1;
rear_ = -1;
} else
front_ = (front_ + 1) % COMMAND_QUEUE_SIZE;
return dequeued_cmd;
}
bool CircularCommandQueue::is_empty() { return front_ == -1; }
bool CircularCommandQueue::is_full() { return (rear_ + 1) % COMMAND_QUEUE_SIZE == front_; }
// Run execute method of first in line command.
// Execute is non-blocking and has to be called until it returns 1.
uint8_t CircularCommandQueue::process(DfrobotSen0395Component *parent) {
if (!is_empty()) {
return commands_[front_]->execute(parent);
} else {
return 1;
}
}
} // namespace dfrobot_sen0395
} // namespace esphome

View file

@ -0,0 +1,125 @@
#pragma once
#include "esphome/components/uart/uart.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#ifdef USE_BINARY_SENSOR
#include "esphome/components/binary_sensor/binary_sensor.h"
#endif
#ifdef USE_SWITCH
#include "esphome/components/switch/switch.h"
#endif
#include "commands.h"
namespace esphome {
namespace dfrobot_sen0395 {
const uint8_t MMWAVE_READ_BUFFER_LENGTH = 255;
// forward declaration due to circular dependency
class DfrobotSen0395Component;
static const uint8_t COMMAND_QUEUE_SIZE = 20;
class CircularCommandQueue {
public:
int8_t enqueue(std::unique_ptr<Command> cmd);
std::unique_ptr<Command> dequeue();
bool is_empty();
bool is_full();
uint8_t process(DfrobotSen0395Component *parent);
protected:
int front_{-1};
int rear_{-1};
std::unique_ptr<Command> commands_[COMMAND_QUEUE_SIZE];
};
class DfrobotSen0395Component : public uart::UARTDevice, public Component {
#ifdef USE_SWITCH
SUB_SWITCH(sensor_active)
SUB_SWITCH(turn_on_led)
SUB_SWITCH(presence_via_uart)
SUB_SWITCH(start_after_boot)
#endif
public:
void dump_config() override;
void loop() override;
void set_active(bool active) {
if (active != active_) {
#ifdef USE_SWITCH
if (this->sensor_active_switch_ != nullptr)
this->sensor_active_switch_->publish_state(active);
#endif
active_ = active;
}
}
bool is_active() { return active_; }
void set_led_active(bool active) {
if (led_active_ != active) {
#ifdef USE_SWITCH
if (this->turn_on_led_switch_ != nullptr)
this->turn_on_led_switch_->publish_state(active);
#endif
led_active_ = active;
}
}
bool is_led_active() { return led_active_; }
void set_uart_presence_active(bool active) {
uart_presence_active_ = active;
#ifdef USE_SWITCH
if (this->presence_via_uart_switch_ != nullptr)
this->presence_via_uart_switch_->publish_state(active);
#endif
}
bool is_uart_presence_active() { return uart_presence_active_; }
void set_start_after_boot(bool start) {
start_after_boot_ = start;
#ifdef USE_SWITCH
if (this->start_after_boot_switch_ != nullptr)
this->start_after_boot_switch_->publish_state(start);
#endif
}
bool does_start_after_boot() { return start_after_boot_; }
#ifdef USE_BINARY_SENSOR
void set_detected_binary_sensor(binary_sensor::BinarySensor *detected_binary_sensor) {
detected_binary_sensor_ = detected_binary_sensor;
}
#endif
int8_t enqueue(std::unique_ptr<Command> cmd);
protected:
#ifdef USE_BINARY_SENSOR
binary_sensor::BinarySensor *detected_binary_sensor_{nullptr};
#endif
bool detected_{false};
bool active_{false};
bool led_active_{false};
bool uart_presence_active_{false};
bool start_after_boot_{false};
char read_buffer_[MMWAVE_READ_BUFFER_LENGTH];
size_t read_pos_{0};
CircularCommandQueue cmd_queue_;
uint32_t ts_last_cmd_sent_{0};
uint8_t read_message_();
uint8_t find_prompt_();
uint8_t send_cmd_(const char *cmd, uint32_t duration);
void set_detected_(bool detected);
friend class Command;
friend class ReadStateCommand;
};
} // namespace dfrobot_sen0395
} // namespace esphome

View file

@ -0,0 +1,65 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import switch
from esphome.const import ENTITY_CATEGORY_CONFIG, CONF_TYPE
from .. import CONF_DFROBOT_SEN0395_ID, DfrobotSen0395Component
DEPENDENCIES = ["dfrobot_sen0395"]
dfrobot_sen0395_ns = cg.esphome_ns.namespace("dfrobot_sen0395")
DfrobotSen0395Switch = dfrobot_sen0395_ns.class_(
"DfrobotSen0395Switch",
switch.Switch,
cg.Component,
cg.Parented.template(DfrobotSen0395Component),
)
Sen0395PowerSwitch = dfrobot_sen0395_ns.class_(
"Sen0395PowerSwitch", DfrobotSen0395Switch
)
Sen0395LedSwitch = dfrobot_sen0395_ns.class_("Sen0395LedSwitch", DfrobotSen0395Switch)
Sen0395UartPresenceSwitch = dfrobot_sen0395_ns.class_(
"Sen0395UartPresenceSwitch", DfrobotSen0395Switch
)
Sen0395StartAfterBootSwitch = dfrobot_sen0395_ns.class_(
"Sen0395StartAfterBootSwitch", DfrobotSen0395Switch
)
_SWITCH_SCHEMA = (
switch.switch_schema(
entity_category=ENTITY_CATEGORY_CONFIG,
)
.extend(
{
cv.GenerateID(CONF_DFROBOT_SEN0395_ID): cv.use_id(DfrobotSen0395Component),
}
)
.extend(cv.COMPONENT_SCHEMA)
)
CONFIG_SCHEMA = cv.typed_schema(
{
"sensor_active": _SWITCH_SCHEMA.extend(
{cv.GenerateID(): cv.declare_id(Sen0395PowerSwitch)}
),
"turn_on_led": _SWITCH_SCHEMA.extend(
{cv.GenerateID(): cv.declare_id(Sen0395LedSwitch)}
),
"presence_via_uart": _SWITCH_SCHEMA.extend(
{cv.GenerateID(): cv.declare_id(Sen0395UartPresenceSwitch)}
),
"start_after_boot": _SWITCH_SCHEMA.extend(
{cv.GenerateID(): cv.declare_id(Sen0395StartAfterBootSwitch)}
),
}
)
async def to_code(config):
parent = await cg.get_variable(config[CONF_DFROBOT_SEN0395_ID])
var = await switch.new_switch(config)
await cg.register_component(var, config)
await cg.register_parented(var, parent)
cg.add(getattr(parent, f"set_{config[CONF_TYPE]}_switch")(var))

View file

@ -0,0 +1,48 @@
#include "dfrobot_sen0395_switch.h"
namespace esphome {
namespace dfrobot_sen0395 {
void Sen0395PowerSwitch::write_state(bool state) { this->parent_->enqueue(make_unique<PowerCommand>(state)); }
void Sen0395LedSwitch::write_state(bool state) {
bool was_active = false;
if (this->parent_->is_active()) {
was_active = true;
this->parent_->enqueue(make_unique<PowerCommand>(false));
}
this->parent_->enqueue(make_unique<LedModeCommand>(state));
this->parent_->enqueue(make_unique<SaveCfgCommand>());
if (was_active) {
this->parent_->enqueue(make_unique<PowerCommand>(true));
}
}
void Sen0395UartPresenceSwitch::write_state(bool state) {
bool was_active = false;
if (this->parent_->is_active()) {
was_active = true;
this->parent_->enqueue(make_unique<PowerCommand>(false));
}
this->parent_->enqueue(make_unique<UartOutputCommand>(state));
this->parent_->enqueue(make_unique<SaveCfgCommand>());
if (was_active) {
this->parent_->enqueue(make_unique<PowerCommand>(true));
}
}
void Sen0395StartAfterBootSwitch::write_state(bool state) {
bool was_active = false;
if (this->parent_->is_active()) {
was_active = true;
this->parent_->enqueue(make_unique<PowerCommand>(false));
}
this->parent_->enqueue(make_unique<SensorCfgStartCommand>(state));
this->parent_->enqueue(make_unique<SaveCfgCommand>());
if (was_active) {
this->parent_->enqueue(make_unique<PowerCommand>(true));
}
}
} // namespace dfrobot_sen0395
} // namespace esphome

View file

@ -0,0 +1,34 @@
#pragma once
#include "esphome/components/switch/switch.h"
#include "esphome/core/component.h"
#include "../dfrobot_sen0395.h"
namespace esphome {
namespace dfrobot_sen0395 {
class DfrobotSen0395Switch : public switch_::Switch, public Component, public Parented<DfrobotSen0395Component> {};
class Sen0395PowerSwitch : public DfrobotSen0395Switch {
public:
void write_state(bool state) override;
};
class Sen0395LedSwitch : public DfrobotSen0395Switch {
public:
void write_state(bool state) override;
};
class Sen0395UartPresenceSwitch : public DfrobotSen0395Switch {
public:
void write_state(bool state) override;
};
class Sen0395StartAfterBootSwitch : public DfrobotSen0395Switch {
public:
void write_state(bool state) override;
};
} // namespace dfrobot_sen0395
} // namespace esphome

View file

@ -95,7 +95,7 @@ void DutyTimeSensor::publish_and_save_(const uint32_t sec, const uint32_t ms) {
void DutyTimeSensor::dump_config() { void DutyTimeSensor::dump_config() {
ESP_LOGCONFIG(TAG, "Duty Time:"); ESP_LOGCONFIG(TAG, "Duty Time:");
ESP_LOGCONFIG(TAG, " Update Interval: %dms", this->get_update_interval()); ESP_LOGCONFIG(TAG, " Update Interval: %" PRId32 "ms", this->get_update_interval());
ESP_LOGCONFIG(TAG, " Restore: %s", ONOFF(this->restore_)); ESP_LOGCONFIG(TAG, " Restore: %s", ONOFF(this->restore_));
LOG_SENSOR(" ", "Duty Time Sensor:", this); LOG_SENSOR(" ", "Duty Time Sensor:", this);
LOG_SENSOR(" ", "Last Duty Time Sensor:", this->last_duty_time_sensor_); LOG_SENSOR(" ", "Last Duty Time Sensor:", this->last_duty_time_sensor_);

View file

@ -1,5 +1,7 @@
#pragma once #pragma once
#include <cinttypes>
#include "esphome/core/automation.h" #include "esphome/core/automation.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/preferences.h" #include "esphome/core/preferences.h"

View file

@ -194,8 +194,8 @@ esp_err_t CameraWebServer::streaming_handler_(struct httpd_req *req) {
int64_t frame_time = millis() - last_frame; int64_t frame_time = millis() - last_frame;
last_frame = millis(); last_frame = millis();
ESP_LOGD(TAG, "MJPG: %uB %ums (%.1ffps)", (uint32_t) image->get_data_length(), (uint32_t) frame_time, ESP_LOGD(TAG, "MJPG: %" PRIu32 "B %" PRIu32 "ms (%.1ffps)", (uint32_t) image->get_data_length(),
1000.0 / (uint32_t) frame_time); (uint32_t) frame_time, 1000.0 / (uint32_t) frame_time);
} }
} }
@ -205,7 +205,7 @@ esp_err_t CameraWebServer::streaming_handler_(struct httpd_req *req) {
esp32_camera::global_esp32_camera->stop_stream(esphome::esp32_camera::WEB_REQUESTER); esp32_camera::global_esp32_camera->stop_stream(esphome::esp32_camera::WEB_REQUESTER);
ESP_LOGI(TAG, "STREAM: closed. Frames: %u", frames); ESP_LOGI(TAG, "STREAM: closed. Frames: %" PRIu32, frames);
return res; return res;
} }

View file

@ -2,6 +2,7 @@
#ifdef USE_ESP32 #ifdef USE_ESP32
#include <cinttypes>
#include <freertos/FreeRTOS.h> #include <freertos/FreeRTOS.h>
#include <freertos/semphr.h> #include <freertos/semphr.h>

View file

@ -137,11 +137,10 @@ def validate_weight_name(value):
def download_gfonts(value): def download_gfonts(value):
wght = value[CONF_WEIGHT] name = (
if value[CONF_ITALIC]: f"{value[CONF_FAMILY]}:ital,wght@{int(value[CONF_ITALIC])},{value[CONF_WEIGHT]}"
wght = f"1,{wght}" )
name = f"{value[CONF_FAMILY]}@{value[CONF_WEIGHT]}" url = f"https://fonts.googleapis.com/css2?family={name}"
url = f"https://fonts.googleapis.com/css2?family={value[CONF_FAMILY]}:wght@{wght}"
path = _compute_gfonts_local_path(value) path = _compute_gfonts_local_path(value)
if path.is_file(): if path.is_file():

View file

@ -0,0 +1,39 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import uart
from esphome.const import CONF_ID
CODEOWNERS = ["@descipher"]
DEPENDENCIES = ["uart"]
MULTI_CONF = True
ld2420_ns = cg.esphome_ns.namespace("ld2420")
LD2420Component = ld2420_ns.class_("LD2420Component", cg.Component, uart.UARTDevice)
CONF_LD2420_ID = "ld2420_id"
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(LD2420Component),
}
)
.extend(uart.UART_DEVICE_SCHEMA)
.extend(cv.COMPONENT_SCHEMA)
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"ld2420_uart",
require_tx=True,
require_rx=True,
parity="NONE",
stop_bits=1,
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await uart.register_uart_device(var, config)

View file

@ -0,0 +1,33 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import binary_sensor
from esphome.const import CONF_ID, DEVICE_CLASS_OCCUPANCY
from .. import ld2420_ns, LD2420Component, CONF_LD2420_ID
LD2420BinarySensor = ld2420_ns.class_(
"LD2420BinarySensor", binary_sensor.BinarySensor, cg.Component
)
CONF_HAS_TARGET = "has_target"
CONFIG_SCHEMA = cv.All(
cv.COMPONENT_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(LD2420BinarySensor),
cv.GenerateID(CONF_LD2420_ID): cv.use_id(LD2420Component),
cv.Optional(CONF_HAS_TARGET): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_OCCUPANCY
),
}
),
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
if CONF_HAS_TARGET in config:
sens = await binary_sensor.new_binary_sensor(config[CONF_HAS_TARGET])
cg.add(var.set_presence_sensor(sens))
ld2420 = await cg.get_variable(config[CONF_LD2420_ID])
cg.add(ld2420.register_listener(var))

View file

@ -0,0 +1,16 @@
#include "ld2420_binary_sensor.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
namespace ld2420 {
static const char *const TAG = "LD2420.binary_sensor";
void LD2420BinarySensor::dump_config() {
ESP_LOGCONFIG(TAG, "LD2420 BinarySensor:");
LOG_BINARY_SENSOR(" ", "Presence", this->presence_bsensor_);
}
} // namespace ld2420
} // namespace esphome

View file

@ -0,0 +1,25 @@
#pragma once
#include "../ld2420.h"
#include "esphome/components/binary_sensor/binary_sensor.h"
namespace esphome {
namespace ld2420 {
class LD2420BinarySensor : public LD2420Listener, public Component, binary_sensor::BinarySensor {
public:
void dump_config() override;
void set_presence_sensor(binary_sensor::BinarySensor *bsensor) { this->presence_bsensor_ = bsensor; };
void on_presence(bool presence) override {
if (this->presence_bsensor_ != nullptr) {
if (this->presence_bsensor_->state != presence)
this->presence_bsensor_->publish_state(presence);
}
}
protected:
binary_sensor::BinarySensor *presence_bsensor_{nullptr};
};
} // namespace ld2420
} // namespace esphome

View file

@ -0,0 +1,69 @@
import esphome.codegen as cg
from esphome.components import button
import esphome.config_validation as cv
from esphome.const import (
DEVICE_CLASS_RESTART,
ENTITY_CATEGORY_DIAGNOSTIC,
ENTITY_CATEGORY_CONFIG,
ICON_RESTART,
ICON_RESTART_ALERT,
ICON_DATABASE,
)
from .. import CONF_LD2420_ID, LD2420Component, ld2420_ns
LD2420ApplyConfigButton = ld2420_ns.class_("LD2420ApplyConfigButton", button.Button)
LD2420RevertConfigButton = ld2420_ns.class_("LD2420RevertConfigButton", button.Button)
LD2420RestartModuleButton = ld2420_ns.class_("LD2420RestartModuleButton", button.Button)
LD2420FactoryResetButton = ld2420_ns.class_("LD2420FactoryResetButton", button.Button)
CONF_APPLY_CONFIG = "apply_config"
CONF_REVERT_CONFIG = "revert_config"
CONF_RESTART_MODULE = "restart_module"
CONF_FACTORY_RESET = "factory_reset"
CONFIG_SCHEMA = {
cv.GenerateID(CONF_LD2420_ID): cv.use_id(LD2420Component),
cv.Required(CONF_APPLY_CONFIG): button.button_schema(
LD2420ApplyConfigButton,
device_class=DEVICE_CLASS_RESTART,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_RESTART_ALERT,
),
cv.Optional(CONF_REVERT_CONFIG): button.button_schema(
LD2420RevertConfigButton,
device_class=DEVICE_CLASS_RESTART,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_RESTART,
),
cv.Optional(CONF_RESTART_MODULE): button.button_schema(
LD2420RestartModuleButton,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
icon=ICON_DATABASE,
),
cv.Optional(CONF_FACTORY_RESET): button.button_schema(
LD2420FactoryResetButton,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_DATABASE,
),
}
async def to_code(config):
ld2420_component = await cg.get_variable(config[CONF_LD2420_ID])
if apply_config := config.get(CONF_APPLY_CONFIG):
b = await button.new_button(apply_config)
await cg.register_parented(b, config[CONF_LD2420_ID])
cg.add(ld2420_component.set_apply_config_button(b))
if revert_config := config.get(CONF_REVERT_CONFIG):
b = await button.new_button(revert_config)
await cg.register_parented(b, config[CONF_LD2420_ID])
cg.add(ld2420_component.set_revert_config_button(b))
if restart_config := config.get(CONF_RESTART_MODULE):
b = await button.new_button(restart_config)
await cg.register_parented(b, config[CONF_LD2420_ID])
cg.add(ld2420_component.set_restart_module_button(b))
if factory_reset := config.get(CONF_FACTORY_RESET):
b = await button.new_button(factory_reset)
await cg.register_parented(b, config[CONF_LD2420_ID])
cg.add(ld2420_component.set_factory_reset_button(b))

View file

@ -0,0 +1,16 @@
#include "reconfig_buttons.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
static const char *const TAG = "LD2420.button";
namespace esphome {
namespace ld2420 {
void LD2420ApplyConfigButton::press_action() { this->parent_->apply_config_action(); }
void LD2420RevertConfigButton::press_action() { this->parent_->revert_config_action(); }
void LD2420RestartModuleButton::press_action() { this->parent_->restart_module_action(); }
void LD2420FactoryResetButton::press_action() { this->parent_->factory_reset_action(); }
} // namespace ld2420
} // namespace esphome

View file

@ -0,0 +1,42 @@
#pragma once
#include "esphome/components/button/button.h"
#include "../ld2420.h"
namespace esphome {
namespace ld2420 {
class LD2420ApplyConfigButton : public button::Button, public Parented<LD2420Component> {
public:
LD2420ApplyConfigButton() = default;
protected:
void press_action() override;
};
class LD2420RevertConfigButton : public button::Button, public Parented<LD2420Component> {
public:
LD2420RevertConfigButton() = default;
protected:
void press_action() override;
};
class LD2420RestartModuleButton : public button::Button, public Parented<LD2420Component> {
public:
LD2420RestartModuleButton() = default;
protected:
void press_action() override;
};
class LD2420FactoryResetButton : public button::Button, public Parented<LD2420Component> {
public:
LD2420FactoryResetButton() = default;
protected:
void press_action() override;
};
} // namespace ld2420
} // namespace esphome

View file

@ -0,0 +1,775 @@
#include "ld2420.h"
#include "esphome/core/helpers.h"
/*
Configure commands - little endian
No command can exceed 64 bytes, otherwise they would need be to be split up into multiple sends.
All send command frames will have:
Header = FD FC FB FA, Bytes 0 - 3, uint32_t 0xFAFBFCFD
Length, bytes 4 - 5, uint16_t 0x0002, must be at least 2 for the command byte if no addon data.
Command bytes 6 - 7, uint16_t
Footer = 04 03 02 01 - uint32_t 0x01020304, Always last 4 Bytes.
Receive
Error bytes 8-9 uint16_t, 0 = success, all other positive values = error
Enable config mode:
Send:
UART Tx: FD FC FB FA 04 00 FF 00 02 00 04 03 02 01
Command = FF 00 - uint16_t 0x00FF
Protocol version = 02 00, can be 1 or 2 - uint16_t 0x0002
Reply:
UART Rx: FD FC FB FA 06 00 FF 01 00 00 02 00 04 03 02 01
Disable config mode:
Send:
UART Tx: FD FC FB FA 02 00 FE 00 04 03 02 01
Command = FE 00 - uint16_t 0x00FE
Receive:
UART Rx: FD FC FB FA 04 00 FE 01 00 00 04 03 02 01
Configure system parameters:
UART Tx: FD FC FB FA 08 00 12 00 00 00 64 00 00 00 04 03 02 01 Set system parms
Command = 12 00 - uint16_t 0x0012, Param
There are three documented parameters for modes:
00 64 = Basic status mode
This mode outputs text as presence "ON" or "OFF" and "Range XXXX"
where XXXX is a decimal value for distance in cm
00 04 = Energy output mode
This mode outputs detailed signal energy values for each gate and the target distance.
The data format consist of the following.
Header HH, Length LL, Persence PP, Distance DD, Range Gate GG, 16 Gate Energies EE, Footer FF
HH HH HH HH LL LL PP DD DD GG GG EE EE .. 16x .. FF FF FF FF
F4 F3 F2 F1 00 23 00 00 00 00 01 00 00 .. .. .. .. F8 F7 F6 F5
00 00 = debug output mode
This mode outputs detailed values consisting of 20 Dopplers, 16 Ranges for a total 20 * 16 * 4 bytes
The data format consist of the following.
Header HH, Doppler DD, Range RR, Footer FF
HH HH HH HH DD DD DD DD .. 20x .. RR RR RR RR .. 16x .. FF FF FF FF
AA BF 10 14 00 00 00 00 .. .. .. .. 00 00 00 00 .. .. .. .. FD FC FB FA
Configure gate sensitivity parameters:
UART Tx: FD FC FB FA 0E 00 07 00 10 00 60 EA 00 00 20 00 60 EA 00 00 04 03 02 01
Command = 12 00 - uint16_t 0x0007
Gate 0 high thresh = 10 00 uint16_t 0x0010, Threshold value = 60 EA 00 00 uint32_t 0x0000EA60
Gate 0 low thresh = 20 00 uint16_t 0x0020, Threshold value = 60 EA 00 00 uint32_t 0x0000EA60
*/
namespace esphome {
namespace ld2420 {
static const char *const TAG = "ld2420";
float LD2420Component::get_setup_priority() const { return setup_priority::BUS; }
void LD2420Component::dump_config() {
ESP_LOGCONFIG(TAG, "LD2420:");
ESP_LOGCONFIG(TAG, " Firmware Version : %7s", this->ld2420_firmware_ver_);
ESP_LOGCONFIG(TAG, "LD2420 Number:");
LOG_NUMBER(TAG, " Gate Timeout:", this->gate_timeout_number_);
LOG_NUMBER(TAG, " Gate Max Distance:", this->max_gate_distance_number_);
LOG_NUMBER(TAG, " Gate Min Distance:", this->min_gate_distance_number_);
LOG_NUMBER(TAG, " Gate Select:", this->gate_select_number_);
for (uint8_t gate = 0; gate < LD2420_TOTAL_GATES; gate++) {
LOG_NUMBER(TAG, " Gate Move Threshold:", this->gate_move_threshold_numbers_[gate]);
LOG_NUMBER(TAG, " Gate Still Threshold::", this->gate_still_threshold_numbers_[gate]);
}
LOG_BUTTON(TAG, " Apply Config:", this->apply_config_button_);
LOG_BUTTON(TAG, " Revert Edits:", this->revert_config_button_);
LOG_BUTTON(TAG, " Factory Reset:", this->factory_reset_button_);
LOG_BUTTON(TAG, " Restart Module:", this->restart_module_button_);
ESP_LOGCONFIG(TAG, "LD2420 Select:");
LOG_SELECT(TAG, " Operating Mode", this->operating_selector_);
if (this->get_firmware_int_(ld2420_firmware_ver_) < CALIBRATE_VERSION_MIN) {
ESP_LOGW(TAG, "LD2420 Firmware Version %s and older are only supported in Simple Mode", ld2420_firmware_ver_);
}
}
uint8_t LD2420Component::calc_checksum(void *data, size_t size) {
uint8_t checksum = 0;
uint8_t *data_bytes = (uint8_t *) data;
for (size_t i = 0; i < size; i++) {
checksum ^= data_bytes[i]; // XOR operation
}
return checksum;
}
int LD2420Component::get_firmware_int_(const char *version_string) {
std::string version_str = version_string;
if (version_str[0] == 'v') {
version_str = version_str.substr(1);
}
version_str.erase(remove(version_str.begin(), version_str.end(), '.'), version_str.end());
int version_integer = stoi(version_str);
return version_integer;
}
void LD2420Component::setup() {
ESP_LOGCONFIG(TAG, "Setting up LD2420...");
if (this->set_config_mode(true) == LD2420_ERROR_TIMEOUT) {
ESP_LOGE(TAG, "LD2420 module has failed to respond, check baud rate and serial connections.");
this->mark_failed();
return;
}
this->get_min_max_distances_timeout_();
#ifdef USE_NUMBER
this->init_gate_config_numbers();
#endif
this->get_firmware_version_();
const char *pfw = this->ld2420_firmware_ver_;
std::string fw_str(pfw);
for (auto &listener : listeners_) {
listener->on_fw_version(fw_str);
}
for (uint8_t gate = 0; gate < LD2420_TOTAL_GATES; gate++) {
delay_microseconds_safe(125);
this->get_gate_threshold_(gate);
}
memcpy(&this->new_config, &this->current_config, sizeof(this->current_config));
if (get_firmware_int_(ld2420_firmware_ver_) < CALIBRATE_VERSION_MIN) {
this->set_operating_mode(OP_SIMPLE_MODE_STRING);
this->operating_selector_->publish_state(OP_SIMPLE_MODE_STRING);
this->set_mode_(CMD_SYSTEM_MODE_SIMPLE);
ESP_LOGW(TAG, "LD2420 Frimware Version %s and older are only supported in Simple Mode", ld2420_firmware_ver_);
} else {
this->set_mode_(CMD_SYSTEM_MODE_ENERGY);
this->operating_selector_->publish_state(OP_NORMAL_MODE_STRING);
}
#ifdef USE_NUMBER
this->init_gate_config_numbers();
#endif
this->set_system_mode(this->system_mode_);
this->set_config_mode(false);
ESP_LOGCONFIG(TAG, "LD2420 setup complete.");
}
void LD2420Component::apply_config_action() {
const uint8_t checksum = calc_checksum(&this->new_config, sizeof(this->new_config));
if (checksum == calc_checksum(&this->current_config, sizeof(this->current_config))) {
ESP_LOGCONFIG(TAG, "No configuration change detected");
return;
}
ESP_LOGCONFIG(TAG, "Reconfiguring LD2420...");
if (this->set_config_mode(true) == LD2420_ERROR_TIMEOUT) {
ESP_LOGE(TAG, "LD2420 module has failed to respond, check baud rate and serial connections.");
this->mark_failed();
return;
}
this->set_min_max_distances_timeout(this->new_config.max_gate, this->new_config.min_gate, this->new_config.timeout);
for (uint8_t gate = 0; gate < LD2420_TOTAL_GATES; gate++) {
delay_microseconds_safe(125);
this->set_gate_threshold(gate);
}
memcpy(&current_config, &new_config, sizeof(new_config));
#ifdef USE_NUMBER
this->init_gate_config_numbers();
#endif
this->set_system_mode(this->system_mode_);
this->set_config_mode(false); // Disable config mode to save new values in LD2420 nvm
this->set_operating_mode(OP_NORMAL_MODE_STRING);
ESP_LOGCONFIG(TAG, "LD2420 reconfig complete.");
}
void LD2420Component::factory_reset_action() {
ESP_LOGCONFIG(TAG, "Setiing factory defaults...");
if (this->set_config_mode(true) == LD2420_ERROR_TIMEOUT) {
ESP_LOGE(TAG, "LD2420 module has failed to respond, check baud rate and serial connections.");
this->mark_failed();
return;
}
this->set_min_max_distances_timeout(FACTORY_MAX_GATE, FACTORY_MIN_GATE, FACTORY_TIMEOUT);
this->gate_timeout_number_->state = FACTORY_TIMEOUT;
this->min_gate_distance_number_->state = FACTORY_MIN_GATE;
this->max_gate_distance_number_->state = FACTORY_MAX_GATE;
for (uint8_t gate = 0; gate < LD2420_TOTAL_GATES; gate++) {
this->new_config.move_thresh[gate] = FACTORY_MOVE_THRESH[gate];
this->new_config.still_thresh[gate] = FACTORY_STILL_THRESH[gate];
delay_microseconds_safe(125);
this->set_gate_threshold(gate);
}
memcpy(&this->current_config, &this->new_config, sizeof(this->new_config));
this->set_system_mode(this->system_mode_);
this->set_config_mode(false);
#ifdef USE_NUMBER
this->init_gate_config_numbers();
this->refresh_gate_config_numbers();
#endif
ESP_LOGCONFIG(TAG, "LD2420 factory reset complete.");
}
void LD2420Component::restart_module_action() {
ESP_LOGCONFIG(TAG, "Restarting LD2420 module...");
this->send_module_restart();
delay_microseconds_safe(45000);
this->set_config_mode(true);
this->set_system_mode(system_mode_);
this->set_config_mode(false);
ESP_LOGCONFIG(TAG, "LD2420 Restarted.");
}
void LD2420Component::revert_config_action() {
memcpy(&this->new_config, &this->current_config, sizeof(this->current_config));
#ifdef USE_NUMBER
this->init_gate_config_numbers();
#endif
ESP_LOGCONFIG(TAG, "Reverted config number edits.");
}
void LD2420Component::loop() {
// If there is a active send command do not process it here, the send command call will handle it.
if (!get_cmd_active_()) {
if (!available())
return;
static uint8_t buffer[2048];
static uint8_t rx_data;
while (available()) {
rx_data = read();
this->readline_(rx_data, buffer, sizeof(buffer));
}
}
}
void LD2420Component::update_radar_data(uint16_t const *gate_energy, uint8_t sample_number) {
for (uint8_t gate = 0; gate < LD2420_TOTAL_GATES; ++gate) {
this->radar_data[gate][sample_number] = gate_energy[gate];
}
this->total_sample_number_counter++;
}
void LD2420Component::auto_calibrate_sensitivity() {
// Calculate average and peak values for each gate
const float move_factor = gate_move_sensitivity_factor + 1;
const float still_factor = (gate_still_sensitivity_factor / 2) + 1;
for (uint8_t gate = 0; gate < LD2420_TOTAL_GATES; ++gate) {
uint32_t sum = 0;
uint16_t peak = 0;
for (uint8_t sample_number = 0; sample_number < CALIBRATE_SAMPLES; ++sample_number) {
// Calculate average
sum += this->radar_data[gate][sample_number];
// Calculate max value
if (this->radar_data[gate][sample_number] > peak) {
peak = this->radar_data[gate][sample_number];
}
}
// Store average and peak values
this->gate_avg[gate] = sum / CALIBRATE_SAMPLES;
if (this->gate_peak[gate] < peak)
this->gate_peak[gate] = peak;
uint32_t calculated_value =
(static_cast<uint32_t>(this->gate_peak[gate]) + (move_factor * static_cast<uint32_t>(this->gate_peak[gate])));
this->new_config.move_thresh[gate] = static_cast<uint16_t>(calculated_value <= 65535 ? calculated_value : 65535);
calculated_value =
(static_cast<uint32_t>(this->gate_peak[gate]) + (still_factor * static_cast<uint32_t>(this->gate_peak[gate])));
this->new_config.still_thresh[gate] = static_cast<uint16_t>(calculated_value <= 65535 ? calculated_value : 65535);
}
}
void LD2420Component::report_gate_data() {
for (uint8_t gate = 0; gate < LD2420_TOTAL_GATES; ++gate) {
// Output results
ESP_LOGI(TAG, "Gate: %2d Avg: %5d Peak: %5d", gate, this->gate_avg[gate], this->gate_peak[gate]);
}
ESP_LOGI(TAG, "Total samples: %d", this->total_sample_number_counter);
}
void LD2420Component::set_operating_mode(const std::string &state) {
// If unsupported firmware ignore mode select
if (get_firmware_int_(ld2420_firmware_ver_) >= CALIBRATE_VERSION_MIN) {
this->current_operating_mode = OP_MODE_TO_UINT.at(state);
// Entering Auto Calibrate we need to clear the privoiuos data collection
this->operating_selector_->publish_state(state);
if (current_operating_mode == OP_CALIBRATE_MODE) {
this->set_calibration_(true);
for (uint8_t gate = 0; gate < LD2420_TOTAL_GATES; gate++) {
this->gate_avg[gate] = 0;
this->gate_peak[gate] = 0;
for (uint8_t i = 0; i < CALIBRATE_SAMPLES; i++) {
this->radar_data[gate][i] = 0;
}
this->total_sample_number_counter = 0;
}
} else {
// Set the current data back so we don't have new data that can be applied in error.
if (this->get_calibration_())
memcpy(&this->new_config, &this->current_config, sizeof(this->current_config));
this->set_calibration_(false);
}
} else {
this->current_operating_mode = OP_SIMPLE_MODE;
this->operating_selector_->publish_state(OP_SIMPLE_MODE_STRING);
}
}
void LD2420Component::readline_(int rx_data, uint8_t *buffer, int len) {
static int pos = 0;
if (rx_data >= 0) {
if (pos < len - 1) {
buffer[pos++] = rx_data;
buffer[pos] = 0;
} else {
pos = 0;
}
if (pos >= 4) {
if (memcmp(&buffer[pos - 4], &CMD_FRAME_FOOTER, sizeof(CMD_FRAME_FOOTER)) == 0) {
this->set_cmd_active_(false); // Set command state to inactive after responce.
this->handle_ack_data_(buffer, pos);
pos = 0;
} else if ((buffer[pos - 2] == 0x0D && buffer[pos - 1] == 0x0A) && (get_mode_() == CMD_SYSTEM_MODE_SIMPLE)) {
this->handle_simple_mode_(buffer, pos);
pos = 0;
} else if ((memcmp(&buffer[pos - 4], &ENERGY_FRAME_FOOTER, sizeof(ENERGY_FRAME_FOOTER)) == 0) &&
(get_mode_() == CMD_SYSTEM_MODE_ENERGY)) {
this->handle_energy_mode_(buffer, pos);
pos = 0;
}
}
}
}
void LD2420Component::handle_energy_mode_(uint8_t *buffer, int len) {
uint8_t index = 6; // Start at presence byte position
uint16_t range;
const uint8_t elements = sizeof(this->gate_energy_) / sizeof(this->gate_energy_[0]);
this->set_presence_(buffer[index]);
index++;
memcpy(&range, &buffer[index], sizeof(range));
index += sizeof(range);
this->set_distance_(range);
for (uint8_t i = 0; i < elements; i++) { // NOLINT
memcpy(&this->gate_energy_[i], &buffer[index], sizeof(this->gate_energy_[0]));
index += sizeof(this->gate_energy_[0]);
}
if (this->current_operating_mode == OP_CALIBRATE_MODE) {
this->update_radar_data(gate_energy_, sample_number_counter);
this->sample_number_counter > CALIBRATE_SAMPLES ? this->sample_number_counter = 0 : this->sample_number_counter++;
}
// Resonable refresh rate for home assistant database size health
const int32_t current_millis = millis();
if (current_millis - this->last_periodic_millis < REFRESH_RATE_MS)
return;
this->last_periodic_millis = current_millis;
for (auto &listener : this->listeners_) {
listener->on_distance(get_distance_());
listener->on_presence(get_presence_());
listener->on_energy(this->gate_energy_, sizeof(this->gate_energy_) / sizeof(this->gate_energy_[0]));
}
if (this->current_operating_mode == OP_CALIBRATE_MODE) {
this->auto_calibrate_sensitivity();
if (current_millis - this->report_periodic_millis > REFRESH_RATE_MS * CALIBRATE_REPORT_INTERVAL) {
this->report_periodic_millis = current_millis;
this->report_gate_data();
}
}
}
void LD2420Component::handle_simple_mode_(const uint8_t *inbuf, int len) {
const uint8_t bufsize = 16;
uint8_t index{0};
uint8_t pos{0};
char *endptr{nullptr};
char outbuf[bufsize]{0};
while (true) {
if (inbuf[pos - 2] == 'O' && inbuf[pos - 1] == 'F' && inbuf[pos] == 'F') {
set_presence_(false);
} else if (inbuf[pos - 1] == 'O' && inbuf[pos] == 'N') {
set_presence_(true);
}
if (inbuf[pos] >= '0' && inbuf[pos] <= '9') {
if (index < bufsize - 1) {
outbuf[index++] = inbuf[pos];
pos++;
}
} else {
if (pos < len - 1) {
pos++;
} else {
break;
}
}
}
outbuf[index] = '\0';
if (index > 1)
set_distance_(strtol(outbuf, &endptr, 10));
if (get_mode_() == CMD_SYSTEM_MODE_SIMPLE) {
// Resonable refresh rate for home assistant database size health
const int32_t current_millis = millis();
if (current_millis - this->last_normal_periodic_millis < REFRESH_RATE_MS)
return;
this->last_normal_periodic_millis = current_millis;
for (auto &listener : this->listeners_)
listener->on_distance(get_distance_());
for (auto &listener : this->listeners_)
listener->on_presence(get_presence_());
}
}
void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) {
this->cmd_reply_.command = buffer[CMD_FRAME_COMMAND];
this->cmd_reply_.length = buffer[CMD_FRAME_DATA_LENGTH];
uint8_t reg_element = 0;
uint8_t data_element = 0;
uint16_t data_pos = 0;
if (this->cmd_reply_.length > CMD_MAX_BYTES) {
ESP_LOGW(TAG, "LD2420 reply - received command reply frame is corrupt, length exceeds %d bytes.", CMD_MAX_BYTES);
return;
} else if (this->cmd_reply_.length < 2) {
ESP_LOGW(TAG, "LD2420 reply - received command frame is corrupt, length is less than 2 bytes.");
return;
}
memcpy(&this->cmd_reply_.error, &buffer[CMD_ERROR_WORD], sizeof(this->cmd_reply_.error));
const char *result = this->cmd_reply_.error ? "failure" : "success";
if (this->cmd_reply_.error > 0) {
return;
};
this->cmd_reply_.ack = true;
switch ((uint16_t) this->cmd_reply_.command) {
case (CMD_ENABLE_CONF):
ESP_LOGD(TAG, "LD2420 reply - set config enable: CMD = %2X %s", CMD_ENABLE_CONF, result);
break;
case (CMD_DISABLE_CONF):
ESP_LOGD(TAG, "LD2420 reply - set config disable: CMD = %2X %s", CMD_DISABLE_CONF, result);
break;
case (CMD_READ_REGISTER):
ESP_LOGD(TAG, "LD2420 reply - read register: CMD = %2X %s", CMD_READ_REGISTER, result);
// TODO Read/Write register is not implemented yet, this will get flushed out to a proper header file
data_pos = 0x0A;
for (uint16_t index = 0; index < (CMD_REG_DATA_REPLY_SIZE * // NOLINT
((buffer[CMD_FRAME_DATA_LENGTH] - 4) / CMD_REG_DATA_REPLY_SIZE));
index += CMD_REG_DATA_REPLY_SIZE) {
memcpy(&this->cmd_reply_.data[reg_element], &buffer[data_pos + index], sizeof(CMD_REG_DATA_REPLY_SIZE));
byteswap(this->cmd_reply_.data[reg_element]);
reg_element++;
}
break;
case (CMD_WRITE_REGISTER):
ESP_LOGD(TAG, "LD2420 reply - write register: CMD = %2X %s", CMD_WRITE_REGISTER, result);
break;
case (CMD_WRITE_ABD_PARAM):
ESP_LOGD(TAG, "LD2420 reply - write gate parameter(s): %2X %s", CMD_WRITE_ABD_PARAM, result);
break;
case (CMD_READ_ABD_PARAM):
ESP_LOGD(TAG, "LD2420 reply - read gate parameter(s): %2X %s", CMD_READ_ABD_PARAM, result);
data_pos = CMD_ABD_DATA_REPLY_START;
for (uint16_t index = 0; index < (CMD_ABD_DATA_REPLY_SIZE * // NOLINT
((buffer[CMD_FRAME_DATA_LENGTH] - 4) / CMD_ABD_DATA_REPLY_SIZE));
index += CMD_ABD_DATA_REPLY_SIZE) {
memcpy(&this->cmd_reply_.data[data_element], &buffer[data_pos + index],
sizeof(this->cmd_reply_.data[data_element]));
byteswap(this->cmd_reply_.data[data_element]);
data_element++;
}
break;
case (CMD_WRITE_SYS_PARAM):
ESP_LOGD(TAG, "LD2420 reply - set system parameter(s): %2X %s", CMD_WRITE_SYS_PARAM, result);
break;
case (CMD_READ_VERSION):
memcpy(this->ld2420_firmware_ver_, &buffer[12], buffer[10]);
ESP_LOGD(TAG, "LD2420 reply - module firmware version: %7s %s", this->ld2420_firmware_ver_, result);
break;
default:
break;
}
}
int LD2420Component::send_cmd_from_array(CmdFrameT frame) {
uint8_t error = 0;
uint8_t ack_buffer[64];
uint8_t cmd_buffer[64];
uint16_t loop_count;
this->cmd_reply_.ack = false;
if (frame.command != CMD_RESTART)
this->set_cmd_active_(true); // Restart does not reply, thus no ack state required.
uint8_t retry = 3;
while (retry) {
// TODO setup a dynamic method e.g. millis time count etc. to tune for non ESP32 240Mhz devices
// this is ok for now since the module firmware is changing like the weather atm
frame.length = 0;
loop_count = 1250;
uint16_t frame_data_bytes = frame.data_length + 2; // Always add two bytes for the cmd size
memcpy(&cmd_buffer[frame.length], &frame.header, sizeof(frame.header));
frame.length += sizeof(frame.header);
memcpy(&cmd_buffer[frame.length], &frame_data_bytes, sizeof(frame.data_length));
frame.length += sizeof(frame.data_length);
memcpy(&cmd_buffer[frame.length], &frame.command, sizeof(frame.command));
frame.length += sizeof(frame.command);
for (uint16_t index = 0; index < frame.data_length; index++) {
memcpy(&cmd_buffer[frame.length], &frame.data[index], sizeof(frame.data[index]));
frame.length += sizeof(frame.data[index]);
}
memcpy(cmd_buffer + frame.length, &frame.footer, sizeof(frame.footer));
frame.length += sizeof(frame.footer);
for (uint16_t index = 0; index < frame.length; index++) {
this->write_byte(cmd_buffer[index]);
}
delay_microseconds_safe(500); // give the module a moment to process it
error = 0;
if (frame.command == CMD_RESTART) {
delay_microseconds_safe(25000); // Wait for the restart
return 0; // restart does not reply exit now
}
while (!this->cmd_reply_.ack) {
while (available()) {
this->readline_(read(), ack_buffer, sizeof(ack_buffer));
}
delay_microseconds_safe(250);
if (loop_count <= 0) {
error = LD2420_ERROR_TIMEOUT;
retry--;
break;
}
loop_count--;
}
if (this->cmd_reply_.ack)
retry = 0;
if (this->cmd_reply_.error > 0)
handle_cmd_error(error);
}
return error;
}
uint8_t LD2420Component::set_config_mode(bool enable) {
CmdFrameT cmd_frame;
cmd_frame.data_length = 0;
cmd_frame.header = CMD_FRAME_HEADER;
cmd_frame.command = enable ? CMD_ENABLE_CONF : CMD_DISABLE_CONF;
if (enable) {
memcpy(&cmd_frame.data[0], &CMD_PROTOCOL_VER, sizeof(CMD_PROTOCOL_VER));
cmd_frame.data_length += sizeof(CMD_PROTOCOL_VER);
}
cmd_frame.footer = CMD_FRAME_FOOTER;
ESP_LOGD(TAG, "Sending set config %s command: %2X", enable ? "enable" : "disable", cmd_frame.command);
return this->send_cmd_from_array(cmd_frame);
}
// Sends a restart and set system running mode to normal
void LD2420Component::send_module_restart() { this->ld2420_restart(); }
void LD2420Component::ld2420_restart() {
CmdFrameT cmd_frame;
cmd_frame.data_length = 0;
cmd_frame.header = CMD_FRAME_HEADER;
cmd_frame.command = CMD_RESTART;
cmd_frame.footer = CMD_FRAME_FOOTER;
ESP_LOGD(TAG, "Sending restart command: %2X", cmd_frame.command);
this->send_cmd_from_array(cmd_frame);
}
void LD2420Component::get_reg_value_(uint16_t reg) {
CmdFrameT cmd_frame;
cmd_frame.data_length = 0;
cmd_frame.header = CMD_FRAME_HEADER;
cmd_frame.command = CMD_READ_REGISTER;
cmd_frame.data[1] = reg;
cmd_frame.data_length += 2;
cmd_frame.footer = CMD_FRAME_FOOTER;
ESP_LOGD(TAG, "Sending read register %4X command: %2X", reg, cmd_frame.command);
this->send_cmd_from_array(cmd_frame);
}
void LD2420Component::set_reg_value(uint16_t reg, uint16_t value) {
CmdFrameT cmd_frame;
cmd_frame.data_length = 0;
cmd_frame.header = CMD_FRAME_HEADER;
cmd_frame.command = CMD_WRITE_REGISTER;
memcpy(&cmd_frame.data[cmd_frame.data_length], &reg, sizeof(CMD_REG_DATA_REPLY_SIZE));
cmd_frame.data_length += 2;
memcpy(&cmd_frame.data[cmd_frame.data_length], &value, sizeof(CMD_REG_DATA_REPLY_SIZE));
cmd_frame.data_length += 2;
cmd_frame.footer = CMD_FRAME_FOOTER;
ESP_LOGD(TAG, "Sending write register %4X command: %2X data = %4X", reg, cmd_frame.command, value);
this->send_cmd_from_array(cmd_frame);
}
void LD2420Component::handle_cmd_error(uint8_t error) { ESP_LOGI(TAG, "Command failed: %s", ERR_MESSAGE[error]); }
int LD2420Component::get_gate_threshold_(uint8_t gate) {
uint8_t error;
CmdFrameT cmd_frame;
cmd_frame.data_length = 0;
cmd_frame.header = CMD_FRAME_HEADER;
cmd_frame.command = CMD_READ_ABD_PARAM;
memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_GATE_MOVE_THRESH[gate], sizeof(CMD_GATE_MOVE_THRESH[gate]));
cmd_frame.data_length += 2;
memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_GATE_STILL_THRESH[gate], sizeof(CMD_GATE_STILL_THRESH[gate]));
cmd_frame.data_length += 2;
cmd_frame.footer = CMD_FRAME_FOOTER;
ESP_LOGD(TAG, "Sending read gate %d high/low theshold command: %2X", gate, cmd_frame.command);
error = this->send_cmd_from_array(cmd_frame);
if (error == 0) {
this->current_config.move_thresh[gate] = cmd_reply_.data[0];
this->current_config.still_thresh[gate] = cmd_reply_.data[1];
}
return error;
}
int LD2420Component::get_min_max_distances_timeout_() {
uint8_t error;
CmdFrameT cmd_frame;
cmd_frame.data_length = 0;
cmd_frame.header = CMD_FRAME_HEADER;
cmd_frame.command = CMD_READ_ABD_PARAM;
memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_MIN_GATE_REG,
sizeof(CMD_MIN_GATE_REG)); // Register: global min detect gate number
cmd_frame.data_length += sizeof(CMD_MIN_GATE_REG);
memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_MAX_GATE_REG,
sizeof(CMD_MAX_GATE_REG)); // Register: global max detect gate number
cmd_frame.data_length += sizeof(CMD_MAX_GATE_REG);
memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_TIMEOUT_REG,
sizeof(CMD_TIMEOUT_REG)); // Register: global delay time
cmd_frame.data_length += sizeof(CMD_TIMEOUT_REG);
cmd_frame.footer = CMD_FRAME_FOOTER;
ESP_LOGD(TAG, "Sending read gate min max and timeout command: %2X", cmd_frame.command);
error = this->send_cmd_from_array(cmd_frame);
if (error == 0) {
this->current_config.min_gate = (uint16_t) cmd_reply_.data[0];
this->current_config.max_gate = (uint16_t) cmd_reply_.data[1];
this->current_config.timeout = (uint16_t) cmd_reply_.data[2];
}
return error;
}
void LD2420Component::set_system_mode(uint16_t mode) {
CmdFrameT cmd_frame;
uint16_t unknown_parm = 0x0000;
cmd_frame.data_length = 0;
cmd_frame.header = CMD_FRAME_HEADER;
cmd_frame.command = CMD_WRITE_SYS_PARAM;
memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_SYSTEM_MODE, sizeof(CMD_SYSTEM_MODE));
cmd_frame.data_length += sizeof(CMD_SYSTEM_MODE);
memcpy(&cmd_frame.data[cmd_frame.data_length], &mode, sizeof(mode));
cmd_frame.data_length += sizeof(mode);
memcpy(&cmd_frame.data[cmd_frame.data_length], &unknown_parm, sizeof(unknown_parm));
cmd_frame.data_length += sizeof(unknown_parm);
cmd_frame.footer = CMD_FRAME_FOOTER;
ESP_LOGD(TAG, "Sending write system mode command: %2X", cmd_frame.command);
if (this->send_cmd_from_array(cmd_frame) == 0)
set_mode_(mode);
}
void LD2420Component::get_firmware_version_() {
CmdFrameT cmd_frame;
cmd_frame.data_length = 0;
cmd_frame.header = CMD_FRAME_HEADER;
cmd_frame.command = CMD_READ_VERSION;
cmd_frame.footer = CMD_FRAME_FOOTER;
ESP_LOGD(TAG, "Sending read firmware version command: %2X", cmd_frame.command);
this->send_cmd_from_array(cmd_frame);
}
void LD2420Component::set_min_max_distances_timeout(uint32_t max_gate_distance, uint32_t min_gate_distance, // NOLINT
uint32_t timeout) {
// Header H, Length L, Register R, Value V, Footer F
// |Min Gate |Max Gate |Timeout |
// HH HH HH HH LL LL CC CC RR RR VV VV VV VV RR RR VV VV VV VV RR RR VV VV VV VV FF FF FF FF
// FD FC FB FA 14 00 07 00 00 00 01 00 00 00 01 00 09 00 00 00 04 00 0A 00 00 00 04 03 02 01 e.g.
CmdFrameT cmd_frame;
cmd_frame.data_length = 0;
cmd_frame.header = CMD_FRAME_HEADER;
cmd_frame.command = CMD_WRITE_ABD_PARAM;
memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_MIN_GATE_REG,
sizeof(CMD_MIN_GATE_REG)); // Register: global min detect gate number
cmd_frame.data_length += sizeof(CMD_MIN_GATE_REG);
memcpy(&cmd_frame.data[cmd_frame.data_length], &min_gate_distance, sizeof(min_gate_distance));
cmd_frame.data_length += sizeof(min_gate_distance);
memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_MAX_GATE_REG,
sizeof(CMD_MAX_GATE_REG)); // Register: global max detect gate number
cmd_frame.data_length += sizeof(CMD_MAX_GATE_REG);
memcpy(&cmd_frame.data[cmd_frame.data_length], &max_gate_distance, sizeof(max_gate_distance));
cmd_frame.data_length += sizeof(max_gate_distance);
memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_TIMEOUT_REG,
sizeof(CMD_TIMEOUT_REG)); // Register: global delay time
cmd_frame.data_length += sizeof(CMD_TIMEOUT_REG);
memcpy(&cmd_frame.data[cmd_frame.data_length], &timeout, sizeof(timeout));
;
cmd_frame.data_length += sizeof(timeout);
cmd_frame.footer = CMD_FRAME_FOOTER;
ESP_LOGD(TAG, "Sending write gate min max and timeout command: %2X", cmd_frame.command);
this->send_cmd_from_array(cmd_frame);
}
void LD2420Component::set_gate_threshold(uint8_t gate) {
// Header H, Length L, Command C, Register R, Value V, Footer F
// HH HH HH HH LL LL CC CC RR RR VV VV VV VV RR RR VV VV VV VV FF FF FF FF
// FD FC FB FA 14 00 07 00 10 00 00 FF 00 00 00 01 00 0F 00 00 04 03 02 01
uint16_t move_threshold_gate = CMD_GATE_MOVE_THRESH[gate];
uint16_t still_threshold_gate = CMD_GATE_STILL_THRESH[gate];
CmdFrameT cmd_frame;
cmd_frame.data_length = 0;
cmd_frame.header = CMD_FRAME_HEADER;
cmd_frame.command = CMD_WRITE_ABD_PARAM;
memcpy(&cmd_frame.data[cmd_frame.data_length], &move_threshold_gate, sizeof(move_threshold_gate));
cmd_frame.data_length += sizeof(move_threshold_gate);
memcpy(&cmd_frame.data[cmd_frame.data_length], &this->new_config.move_thresh[gate],
sizeof(this->new_config.move_thresh[gate]));
cmd_frame.data_length += sizeof(this->new_config.move_thresh[gate]);
memcpy(&cmd_frame.data[cmd_frame.data_length], &still_threshold_gate, sizeof(still_threshold_gate));
cmd_frame.data_length += sizeof(still_threshold_gate);
memcpy(&cmd_frame.data[cmd_frame.data_length], &this->new_config.still_thresh[gate],
sizeof(this->new_config.still_thresh[gate]));
cmd_frame.data_length += sizeof(this->new_config.still_thresh[gate]);
cmd_frame.footer = CMD_FRAME_FOOTER;
ESP_LOGD(TAG, "Sending set gate %4X sensitivity command: %2X", gate, cmd_frame.command);
this->send_cmd_from_array(cmd_frame);
}
#ifdef USE_NUMBER
void LD2420Component::init_gate_config_numbers() {
if (this->gate_timeout_number_ != nullptr)
this->gate_timeout_number_->publish_state(static_cast<uint16_t>(this->current_config.timeout));
if (this->gate_select_number_ != nullptr)
this->gate_select_number_->publish_state(0);
if (this->min_gate_distance_number_ != nullptr)
this->min_gate_distance_number_->publish_state(static_cast<uint16_t>(this->current_config.min_gate));
if (this->max_gate_distance_number_ != nullptr)
this->max_gate_distance_number_->publish_state(static_cast<uint16_t>(this->current_config.max_gate));
if (this->gate_move_sensitivity_factor_number_ != nullptr)
this->gate_move_sensitivity_factor_number_->publish_state(this->gate_move_sensitivity_factor);
if (this->gate_still_sensitivity_factor_number_ != nullptr)
this->gate_still_sensitivity_factor_number_->publish_state(this->gate_still_sensitivity_factor);
for (uint8_t gate = 0; gate < LD2420_TOTAL_GATES; gate++) {
if (this->gate_still_threshold_numbers_[gate] != nullptr) {
this->gate_still_threshold_numbers_[gate]->publish_state(
static_cast<uint16_t>(this->current_config.still_thresh[gate]));
}
if (this->gate_move_threshold_numbers_[gate] != nullptr) {
this->gate_move_threshold_numbers_[gate]->publish_state(
static_cast<uint16_t>(this->current_config.move_thresh[gate]));
}
}
}
void LD2420Component::refresh_gate_config_numbers() {
this->gate_timeout_number_->publish_state(this->new_config.timeout);
this->min_gate_distance_number_->publish_state(this->new_config.min_gate);
this->max_gate_distance_number_->publish_state(this->new_config.max_gate);
}
#endif
} // namespace ld2420
} // namespace esphome

View file

@ -0,0 +1,272 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/uart/uart.h"
#include "esphome/core/automation.h"
#include "esphome/core/helpers.h"
#ifdef USE_TEXT_SENSOR
#include "esphome/components/text_sensor/text_sensor.h"
#endif
#ifdef USE_SELECT
#include "esphome/components/select/select.h"
#endif
#ifdef USE_NUMBER
#include "esphome/components/number/number.h"
#endif
#ifdef USE_BUTTON
#include "esphome/components/button/button.h"
#endif
#include <map>
#include <functional>
namespace esphome {
namespace ld2420 {
// Local const's
static const uint16_t REFRESH_RATE_MS = 1000;
// Command sets
static const uint8_t CMD_ABD_DATA_REPLY_SIZE = 0x04;
static const uint8_t CMD_ABD_DATA_REPLY_START = 0x0A;
static const uint16_t CMD_DISABLE_CONF = 0x00FE;
static const uint16_t CMD_ENABLE_CONF = 0x00FF;
static const uint8_t CMD_MAX_BYTES = 0x64;
static const uint16_t CMD_PARM_HIGH_TRESH = 0x0012;
static const uint16_t CMD_PARM_LOW_TRESH = 0x0021;
static const uint16_t CMD_PROTOCOL_VER = 0x0002;
static const uint16_t CMD_READ_ABD_PARAM = 0x0008;
static const uint16_t CMD_READ_REG_ADDR = 0x0020;
static const uint16_t CMD_READ_REGISTER = 0x0002;
static const uint16_t CMD_READ_SERIAL_NUM = 0x0011;
static const uint16_t CMD_READ_SYS_PARAM = 0x0013;
static const uint16_t CMD_READ_VERSION = 0x0000;
static const uint8_t CMD_REG_DATA_REPLY_SIZE = 0x02;
static const uint16_t CMD_RESTART = 0x0068;
static const uint16_t CMD_SYSTEM_MODE = 0x0000;
static const uint16_t CMD_SYSTEM_MODE_GR = 0x0003;
static const uint16_t CMD_SYSTEM_MODE_MTT = 0x0001;
static const uint16_t CMD_SYSTEM_MODE_SIMPLE = 0x0064;
static const uint16_t CMD_SYSTEM_MODE_DEBUG = 0x0000;
static const uint16_t CMD_SYSTEM_MODE_ENERGY = 0x0004;
static const uint16_t CMD_SYSTEM_MODE_VS = 0x0002;
static const uint16_t CMD_WRITE_ABD_PARAM = 0x0007;
static const uint16_t CMD_WRITE_REGISTER = 0x0001;
static const uint16_t CMD_WRITE_SYS_PARAM = 0x0012;
static const uint8_t LD2420_ERROR_NONE = 0x00;
static const uint8_t LD2420_ERROR_TIMEOUT = 0x02;
static const uint8_t LD2420_ERROR_UNKNOWN = 0x01;
static const uint8_t LD2420_TOTAL_GATES = 16;
static const uint8_t CALIBRATE_SAMPLES = 64;
// Register address values
static const uint16_t CMD_MIN_GATE_REG = 0x0000;
static const uint16_t CMD_MAX_GATE_REG = 0x0001;
static const uint16_t CMD_TIMEOUT_REG = 0x0004;
static const uint16_t CMD_GATE_MOVE_THRESH[LD2420_TOTAL_GATES] = {0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015,
0x0016, 0x0017, 0x0018, 0x0019, 0x001A, 0x001B,
0x001C, 0x001D, 0x001E, 0x001F};
static const uint16_t CMD_GATE_STILL_THRESH[LD2420_TOTAL_GATES] = {0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025,
0x0026, 0x0027, 0x0028, 0x0029, 0x002A, 0x002B,
0x002C, 0x002D, 0x002E, 0x002F};
static const uint32_t FACTORY_MOVE_THRESH[LD2420_TOTAL_GATES] = {60000, 30000, 400, 250, 250, 250, 250, 250,
250, 250, 250, 250, 250, 250, 250, 250};
static const uint32_t FACTORY_STILL_THRESH[LD2420_TOTAL_GATES] = {40000, 20000, 200, 200, 200, 200, 200, 150,
150, 100, 100, 100, 100, 100, 100, 100};
static const uint16_t FACTORY_TIMEOUT = 120;
static const uint16_t FACTORY_MIN_GATE = 1;
static const uint16_t FACTORY_MAX_GATE = 12;
// COMMAND_BYTE Header & Footer
static const uint8_t CMD_FRAME_COMMAND = 6;
static const uint8_t CMD_FRAME_DATA_LENGTH = 4;
static const uint32_t CMD_FRAME_FOOTER = 0x01020304;
static const uint32_t CMD_FRAME_HEADER = 0xFAFBFCFD;
static const uint32_t DEBUG_FRAME_FOOTER = 0xFAFBFCFD;
static const uint32_t DEBUG_FRAME_HEADER = 0x1410BFAA;
static const uint32_t ENERGY_FRAME_FOOTER = 0xF5F6F7F8;
static const uint32_t ENERGY_FRAME_HEADER = 0xF1F2F3F4;
static const uint8_t CMD_FRAME_STATUS = 7;
static const uint8_t CMD_ERROR_WORD = 8;
static const uint8_t ENERGY_SENSOR_START = 9;
static const uint8_t CALIBRATE_REPORT_INTERVAL = 4;
static const int CALIBRATE_VERSION_MIN = 154;
static const std::string OP_NORMAL_MODE_STRING = "Normal";
static const std::string OP_SIMPLE_MODE_STRING = "Simple";
enum OpModeStruct : uint8_t { OP_NORMAL_MODE = 1, OP_CALIBRATE_MODE = 2, OP_SIMPLE_MODE = 3 };
static const std::map<std::string, uint8_t> OP_MODE_TO_UINT{
{"Normal", OP_NORMAL_MODE}, {"Calibrate", OP_CALIBRATE_MODE}, {"Simple", OP_SIMPLE_MODE}};
static constexpr const char *ERR_MESSAGE[] = {"None", "Unknown", "Timeout"};
class LD2420Listener {
public:
virtual void on_presence(bool presence){};
virtual void on_distance(uint16_t distance){};
virtual void on_energy(uint16_t *sensor_energy, size_t size){};
virtual void on_fw_version(std::string &fw){};
};
class LD2420Component : public Component, public uart::UARTDevice {
public:
void setup() override;
void dump_config() override;
void loop() override;
#ifdef USE_SELECT
void set_operating_mode_select(select::Select *selector) { this->operating_selector_ = selector; };
#endif
#ifdef USE_NUMBER
void set_gate_timeout_number(number::Number *number) { this->gate_timeout_number_ = number; };
void set_gate_select_number(number::Number *number) { this->gate_select_number_ = number; };
void set_min_gate_distance_number(number::Number *number) { this->min_gate_distance_number_ = number; };
void set_max_gate_distance_number(number::Number *number) { this->max_gate_distance_number_ = number; };
void set_gate_move_sensitivity_factor_number(number::Number *number) {
this->gate_move_sensitivity_factor_number_ = number;
};
void set_gate_still_sensitivity_factor_number(number::Number *number) {
this->gate_still_sensitivity_factor_number_ = number;
};
void set_gate_still_threshold_numbers(int gate, number::Number *n) { this->gate_still_threshold_numbers_[gate] = n; };
void set_gate_move_threshold_numbers(int gate, number::Number *n) { this->gate_move_threshold_numbers_[gate] = n; };
bool is_gate_select() { return gate_select_number_ != nullptr; };
uint8_t get_gate_select_value() { return static_cast<uint8_t>(this->gate_select_number_->state); };
float get_min_gate_distance_value() { return min_gate_distance_number_->state; };
float get_max_gate_distance_value() { return max_gate_distance_number_->state; };
void publish_gate_move_threshold(uint8_t gate) {
// With gate_select we only use 1 number pointer, thus we hard code [0]
this->gate_move_threshold_numbers_[0]->publish_state(this->new_config.move_thresh[gate]);
};
void publish_gate_still_threshold(uint8_t gate) {
this->gate_still_threshold_numbers_[0]->publish_state(this->new_config.still_thresh[gate]);
};
void init_gate_config_numbers();
void refresh_gate_config_numbers();
#endif
#ifdef USE_BUTTON
void set_apply_config_button(button::Button *button) { this->apply_config_button_ = button; };
void set_revert_config_button(button::Button *button) { this->revert_config_button_ = button; };
void set_restart_module_button(button::Button *button) { this->restart_module_button_ = button; };
void set_factory_reset_button(button::Button *button) { this->factory_reset_button_ = button; };
#endif
void register_listener(LD2420Listener *listener) { this->listeners_.push_back(listener); }
struct CmdFrameT {
uint32_t header{0};
uint16_t length{0};
uint16_t command{0};
uint8_t data[18];
uint16_t data_length{0};
uint32_t footer{0};
};
struct RegConfigT {
uint16_t min_gate{0};
uint16_t max_gate{0};
uint16_t timeout{0};
uint32_t move_thresh[LD2420_TOTAL_GATES];
uint32_t still_thresh[LD2420_TOTAL_GATES];
};
void send_module_restart();
void restart_module_action();
void apply_config_action();
void factory_reset_action();
void revert_config_action();
float get_setup_priority() const override;
int send_cmd_from_array(CmdFrameT cmd_frame);
void report_gate_data();
void handle_cmd_error(uint8_t error);
void set_operating_mode(const std::string &state);
void auto_calibrate_sensitivity();
void update_radar_data(uint16_t const *gate_energy, uint8_t sample_number);
uint8_t calc_checksum(void *data, size_t size);
RegConfigT current_config;
RegConfigT new_config;
int32_t last_periodic_millis = millis();
int32_t report_periodic_millis = millis();
int32_t monitor_periodic_millis = millis();
int32_t last_normal_periodic_millis = millis();
bool output_energy_state{false};
uint8_t current_operating_mode{OP_NORMAL_MODE};
uint16_t radar_data[LD2420_TOTAL_GATES][CALIBRATE_SAMPLES];
uint16_t gate_avg[LD2420_TOTAL_GATES];
uint16_t gate_peak[LD2420_TOTAL_GATES];
uint8_t sample_number_counter{0};
uint16_t total_sample_number_counter{0};
float gate_move_sensitivity_factor{0.5};
float gate_still_sensitivity_factor{0.5};
#ifdef USE_SELECT
select::Select *operating_selector_{nullptr};
#endif
#ifdef USE_BUTTON
button::Button *apply_config_button_{nullptr};
button::Button *revert_config_button_{nullptr};
button::Button *restart_module_button_{nullptr};
button::Button *factory_reset_button_{nullptr};
#endif
void set_min_max_distances_timeout(uint32_t max_gate_distance, uint32_t min_gate_distance, uint32_t timeout);
void set_gate_threshold(uint8_t gate);
void set_reg_value(uint16_t reg, uint16_t value);
uint8_t set_config_mode(bool enable);
void set_system_mode(uint16_t mode);
void ld2420_restart();
protected:
struct CmdReplyT {
uint8_t command;
uint8_t status;
uint32_t data[4];
uint8_t length;
uint16_t error;
volatile bool ack;
};
int get_firmware_int_(const char *version_string);
void get_firmware_version_();
int get_gate_threshold_(uint8_t gate);
void get_reg_value_(uint16_t reg);
int get_min_max_distances_timeout_();
uint16_t get_mode_() { return this->system_mode_; };
void set_mode_(uint16_t mode) { this->system_mode_ = mode; };
bool get_presence_() { return this->presence_; };
void set_presence_(bool presence) { this->presence_ = presence; };
uint16_t get_distance_() { return this->distance_; };
void set_distance_(uint16_t distance) { this->distance_ = distance; };
bool get_cmd_active_() { return this->cmd_active_; };
void set_cmd_active_(bool active) { this->cmd_active_ = active; };
void handle_simple_mode_(const uint8_t *inbuf, int len);
void handle_energy_mode_(uint8_t *buffer, int len);
void handle_ack_data_(uint8_t *buffer, int len);
void readline_(int rx_data, uint8_t *buffer, int len);
void set_calibration_(bool state) { this->calibration_ = state; };
bool get_calibration_() { return this->calibration_; };
#ifdef USE_NUMBER
number::Number *gate_timeout_number_{nullptr};
number::Number *gate_select_number_{nullptr};
number::Number *min_gate_distance_number_{nullptr};
number::Number *max_gate_distance_number_{nullptr};
number::Number *gate_move_sensitivity_factor_number_{nullptr};
number::Number *gate_still_sensitivity_factor_number_{nullptr};
std::vector<number::Number *> gate_still_threshold_numbers_ = std::vector<number::Number *>(16);
std::vector<number::Number *> gate_move_threshold_numbers_ = std::vector<number::Number *>(16);
#endif
uint16_t gate_energy_[LD2420_TOTAL_GATES];
CmdReplyT cmd_reply_;
uint32_t timeout_;
uint32_t max_distance_gate_;
uint32_t min_distance_gate_;
uint16_t system_mode_{CMD_SYSTEM_MODE_ENERGY};
bool cmd_active_{false};
char ld2420_firmware_ver_[8];
bool presence_{false};
bool calibration_{false};
uint16_t distance_{0};
uint8_t config_checksum_{0};
std::vector<LD2420Listener *> listeners_{};
};
} // namespace ld2420
} // namespace esphome

View file

@ -0,0 +1,183 @@
import esphome.codegen as cg
from esphome.components import number
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
DEVICE_CLASS_DISTANCE,
UNIT_SECOND,
ENTITY_CATEGORY_CONFIG,
ICON_MOTION_SENSOR,
ICON_TIMELAPSE,
ICON_SCALE,
)
from .. import CONF_LD2420_ID, LD2420Component, ld2420_ns
LD2420TimeoutNumber = ld2420_ns.class_("LD2420TimeoutNumber", number.Number)
LD2420MoveSensFactorNumber = ld2420_ns.class_(
"LD2420MoveSensFactorNumber", number.Number
)
LD2420StillSensFactorNumber = ld2420_ns.class_(
"LD2420StillSensFactorNumber", number.Number
)
LD2420MinDistanceNumber = ld2420_ns.class_("LD2420MinDistanceNumber", number.Number)
LD2420MaxDistanceNumber = ld2420_ns.class_("LD2420MaxDistanceNumber", number.Number)
LD2420GateSelectNumber = ld2420_ns.class_("LD2420GateSelectNumber", number.Number)
LD2420MoveThresholdNumbers = ld2420_ns.class_(
"LD2420MoveThresholdNumbers", number.Number
)
LD2420StillThresholdNumbers = ld2420_ns.class_(
"LD2420StillThresholdNumbers", number.Number
)
CONF_MIN_GATE_DISTANCE = "min_gate_distance"
CONF_MAX_GATE_DISTANCE = "max_gate_distance"
CONF_STILL_THRESHOLD = "still_threshold"
CONF_MOVE_THRESHOLD = "move_threshold"
CONF_GATE_MOVE_SENSITIVITY = "gate_move_sensitivity"
CONF_GATE_STILL_SENSITIVITY = "gate_still_sensitivity"
CONF_GATE_SELECT = "gate_select"
CONF_PRESENCE_TIMEOUT = "presence_timeout"
GATE_GROUP = "gate_group"
TIMEOUT_GROUP = "timeout_group"
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_LD2420_ID): cv.use_id(LD2420Component),
cv.Inclusive(CONF_PRESENCE_TIMEOUT, TIMEOUT_GROUP): number.number_schema(
LD2420TimeoutNumber,
unit_of_measurement=UNIT_SECOND,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_TIMELAPSE,
),
cv.Inclusive(CONF_MIN_GATE_DISTANCE, TIMEOUT_GROUP): number.number_schema(
LD2420MinDistanceNumber,
device_class=DEVICE_CLASS_DISTANCE,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_MOTION_SENSOR,
),
cv.Inclusive(CONF_MAX_GATE_DISTANCE, TIMEOUT_GROUP): number.number_schema(
LD2420MaxDistanceNumber,
device_class=DEVICE_CLASS_DISTANCE,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_MOTION_SENSOR,
),
cv.Inclusive(CONF_GATE_SELECT, GATE_GROUP): number.number_schema(
LD2420GateSelectNumber,
device_class=DEVICE_CLASS_DISTANCE,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_MOTION_SENSOR,
),
cv.Inclusive(CONF_STILL_THRESHOLD, GATE_GROUP): number.number_schema(
LD2420StillThresholdNumbers,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_MOTION_SENSOR,
),
cv.Inclusive(CONF_MOVE_THRESHOLD, GATE_GROUP): number.number_schema(
LD2420MoveThresholdNumbers,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_MOTION_SENSOR,
),
cv.Optional(CONF_GATE_MOVE_SENSITIVITY): number.number_schema(
LD2420MoveSensFactorNumber,
device_class=DEVICE_CLASS_DISTANCE,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_SCALE,
),
cv.Optional(CONF_GATE_STILL_SENSITIVITY): number.number_schema(
LD2420StillSensFactorNumber,
device_class=DEVICE_CLASS_DISTANCE,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_SCALE,
),
}
)
CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
{
cv.Optional(f"gate_{x}"): (
{
cv.Required(CONF_MOVE_THRESHOLD): number.number_schema(
LD2420MoveThresholdNumbers,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_MOTION_SENSOR,
),
cv.Required(CONF_STILL_THRESHOLD): number.number_schema(
LD2420StillThresholdNumbers,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_MOTION_SENSOR,
),
}
)
for x in range(16)
}
)
async def to_code(config):
LD2420_component = await cg.get_variable(config[CONF_LD2420_ID])
if gate_timeout_config := config.get(CONF_PRESENCE_TIMEOUT):
n = await number.new_number(
gate_timeout_config, min_value=0, max_value=255, step=5
)
await cg.register_parented(n, config[CONF_LD2420_ID])
cg.add(LD2420_component.set_gate_timeout_number(n))
if min_distance_gate_config := config.get(CONF_MIN_GATE_DISTANCE):
n = await number.new_number(
min_distance_gate_config, min_value=0, max_value=15, step=1
)
await cg.register_parented(n, config[CONF_LD2420_ID])
cg.add(LD2420_component.set_min_gate_distance_number(n))
if max_distance_gate_config := config.get(CONF_MAX_GATE_DISTANCE):
n = await number.new_number(
max_distance_gate_config, min_value=1, max_value=15, step=1
)
await cg.register_parented(n, config[CONF_LD2420_ID])
cg.add(LD2420_component.set_max_gate_distance_number(n))
if gate_move_sensitivity_config := config.get(CONF_GATE_MOVE_SENSITIVITY):
n = await number.new_number(
gate_move_sensitivity_config, min_value=0.05, max_value=1, step=0.025
)
await cg.register_parented(n, config[CONF_LD2420_ID])
cg.add(LD2420_component.set_gate_move_sensitivity_factor_number(n))
if gate_still_sensitivity_config := config.get(CONF_GATE_STILL_SENSITIVITY):
n = await number.new_number(
gate_still_sensitivity_config, min_value=0.05, max_value=1, step=0.025
)
await cg.register_parented(n, config[CONF_LD2420_ID])
cg.add(LD2420_component.set_gate_still_sensitivity_factor_number(n))
if config.get(CONF_GATE_SELECT):
if gate_number := config.get(CONF_GATE_SELECT):
n = await number.new_number(gate_number, min_value=0, max_value=15, step=1)
await cg.register_parented(n, config[CONF_LD2420_ID])
cg.add(LD2420_component.set_gate_select_number(n))
if gate_still_threshold := config.get(CONF_STILL_THRESHOLD):
n = cg.new_Pvariable(gate_still_threshold[CONF_ID])
await number.register_number(
n, gate_still_threshold, min_value=0, max_value=65535, step=25
)
await cg.register_parented(n, config[CONF_LD2420_ID])
cg.add(LD2420_component.set_gate_still_threshold_numbers(0, n))
if gate_move_threshold := config.get(CONF_MOVE_THRESHOLD):
n = cg.new_Pvariable(gate_move_threshold[CONF_ID])
await number.register_number(
n, gate_move_threshold, min_value=0, max_value=65535, step=25
)
await cg.register_parented(n, config[CONF_LD2420_ID])
cg.add(LD2420_component.set_gate_move_threshold_numbers(0, n))
else:
for x in range(16):
if gate_conf := config.get(f"gate_{x}"):
move_config = gate_conf[CONF_MOVE_THRESHOLD]
n = cg.new_Pvariable(move_config[CONF_ID], x)
await number.register_number(
n, move_config, min_value=0, max_value=65535, step=25
)
await cg.register_parented(n, config[CONF_LD2420_ID])
cg.add(LD2420_component.set_gate_move_threshold_numbers(x, n))
still_config = gate_conf[CONF_STILL_THRESHOLD]
n = cg.new_Pvariable(still_config[CONF_ID], x)
await number.register_number(
n, still_config, min_value=0, max_value=65535, step=25
)
await cg.register_parented(n, config[CONF_LD2420_ID])
cg.add(LD2420_component.set_gate_still_threshold_numbers(x, n))

View file

@ -0,0 +1,73 @@
#include "gate_config_number.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
static const char *const TAG = "LD2420.number";
namespace esphome {
namespace ld2420 {
void LD2420TimeoutNumber::control(float timeout) {
this->publish_state(timeout);
this->parent_->new_config.timeout = timeout;
}
void LD2420MinDistanceNumber::control(float min_gate) {
if ((uint16_t) min_gate > this->parent_->new_config.max_gate) {
min_gate = this->parent_->get_min_gate_distance_value();
} else {
this->parent_->new_config.min_gate = (uint16_t) min_gate;
}
this->publish_state(min_gate);
}
void LD2420MaxDistanceNumber::control(float max_gate) {
if ((uint16_t) max_gate < this->parent_->new_config.min_gate) {
max_gate = this->parent_->get_max_gate_distance_value();
} else {
this->parent_->new_config.max_gate = (uint16_t) max_gate;
}
this->publish_state(max_gate);
}
void LD2420GateSelectNumber::control(float gate_select) {
const uint8_t gate = (uint8_t) gate_select;
this->publish_state(gate_select);
this->parent_->publish_gate_move_threshold(gate);
this->parent_->publish_gate_still_threshold(gate);
}
void LD2420MoveSensFactorNumber::control(float move_factor) {
this->publish_state(move_factor);
this->parent_->gate_move_sensitivity_factor = move_factor;
}
void LD2420StillSensFactorNumber::control(float still_factor) {
this->publish_state(still_factor);
this->parent_->gate_still_sensitivity_factor = still_factor;
}
LD2420MoveThresholdNumbers::LD2420MoveThresholdNumbers(uint8_t gate) : gate_(gate) {}
void LD2420MoveThresholdNumbers::control(float move_threshold) {
this->publish_state(move_threshold);
if (!this->parent_->is_gate_select()) {
this->parent_->new_config.move_thresh[this->gate_] = move_threshold;
} else {
this->parent_->new_config.move_thresh[this->parent_->get_gate_select_value()] = move_threshold;
}
}
LD2420StillThresholdNumbers::LD2420StillThresholdNumbers(uint8_t gate) : gate_(gate) {}
void LD2420StillThresholdNumbers::control(float still_threshold) {
this->publish_state(still_threshold);
if (!this->parent_->is_gate_select()) {
this->parent_->new_config.still_thresh[this->gate_] = still_threshold;
} else {
this->parent_->new_config.still_thresh[this->parent_->get_gate_select_value()] = still_threshold;
}
}
} // namespace ld2420
} // namespace esphome

View file

@ -0,0 +1,78 @@
#pragma once
#include "esphome/components/number/number.h"
#include "../ld2420.h"
namespace esphome {
namespace ld2420 {
class LD2420TimeoutNumber : public number::Number, public Parented<LD2420Component> {
public:
LD2420TimeoutNumber() = default;
protected:
void control(float timeout) override;
};
class LD2420MinDistanceNumber : public number::Number, public Parented<LD2420Component> {
public:
LD2420MinDistanceNumber() = default;
protected:
void control(float min_gate) override;
};
class LD2420MaxDistanceNumber : public number::Number, public Parented<LD2420Component> {
public:
LD2420MaxDistanceNumber() = default;
protected:
void control(float max_gate) override;
};
class LD2420GateSelectNumber : public number::Number, public Parented<LD2420Component> {
public:
LD2420GateSelectNumber() = default;
protected:
void control(float gate_select) override;
};
class LD2420MoveSensFactorNumber : public number::Number, public Parented<LD2420Component> {
public:
LD2420MoveSensFactorNumber() = default;
protected:
void control(float move_factor) override;
};
class LD2420StillSensFactorNumber : public number::Number, public Parented<LD2420Component> {
public:
LD2420StillSensFactorNumber() = default;
protected:
void control(float still_factor) override;
};
class LD2420StillThresholdNumbers : public number::Number, public Parented<LD2420Component> {
public:
LD2420StillThresholdNumbers() = default;
LD2420StillThresholdNumbers(uint8_t gate);
protected:
uint8_t gate_;
void control(float still_threshold) override;
};
class LD2420MoveThresholdNumbers : public number::Number, public Parented<LD2420Component> {
public:
LD2420MoveThresholdNumbers() = default;
LD2420MoveThresholdNumbers(uint8_t gate);
protected:
uint8_t gate_;
void control(float move_threshold) override;
};
} // namespace ld2420
} // namespace esphome

View file

@ -0,0 +1,33 @@
import esphome.codegen as cg
from esphome.components import select
import esphome.config_validation as cv
from esphome.const import ENTITY_CATEGORY_CONFIG
from .. import CONF_LD2420_ID, LD2420Component, ld2420_ns
CONF_OPERATING_MODE = "operating_mode"
CONF_SELECTS = [
"Normal",
"Calibrate",
"Simple",
]
LD2420Select = ld2420_ns.class_("LD2420Select", cg.Component)
CONFIG_SCHEMA = {
cv.GenerateID(CONF_LD2420_ID): cv.use_id(LD2420Component),
cv.Required(CONF_OPERATING_MODE): select.select_schema(
LD2420Select,
entity_category=ENTITY_CATEGORY_CONFIG,
),
}
async def to_code(config):
LD2420_component = await cg.get_variable(config[CONF_LD2420_ID])
if operating_mode_config := config.get(CONF_OPERATING_MODE):
sel = await select.new_select(
operating_mode_config,
options=[CONF_SELECTS],
)
await cg.register_parented(sel, config[CONF_LD2420_ID])
cg.add(LD2420_component.set_operating_mode_select(sel))

View file

@ -0,0 +1,16 @@
#include "operating_mode_select.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
namespace ld2420 {
static const char *const TAG = "LD2420.select";
void LD2420Select::control(const std::string &value) {
this->publish_state(value);
this->parent_->set_operating_mode(value);
}
} // namespace ld2420
} // namespace esphome

View file

@ -0,0 +1,18 @@
#pragma once
#include "../ld2420.h"
#include "esphome/components/select/select.h"
namespace esphome {
namespace ld2420 {
class LD2420Select : public Component, public select::Select, public Parented<LD2420Component> {
public:
LD2420Select() = default;
protected:
void control(const std::string &value) override;
};
} // namespace ld2420
} // namespace esphome

View file

@ -0,0 +1,35 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor
from esphome.const import CONF_ID, DEVICE_CLASS_DISTANCE, UNIT_CENTIMETER
from .. import ld2420_ns, LD2420Component, CONF_LD2420_ID
LD2420Sensor = ld2420_ns.class_("LD2420Sensor", sensor.Sensor, cg.Component)
CONF_MOVING_DISTANCE = "moving_distance"
CONF_GATE_ENERGY = "gate_energy"
CONFIG_SCHEMA = cv.All(
cv.COMPONENT_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(LD2420Sensor),
cv.GenerateID(CONF_LD2420_ID): cv.use_id(LD2420Component),
cv.Optional(CONF_MOVING_DISTANCE): sensor.sensor_schema(
device_class=DEVICE_CLASS_DISTANCE, unit_of_measurement=UNIT_CENTIMETER
),
}
),
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
if CONF_MOVING_DISTANCE in config:
sens = await sensor.new_sensor(config[CONF_MOVING_DISTANCE])
cg.add(var.set_distance_sensor(sens))
if CONF_GATE_ENERGY in config:
sens = await sensor.new_sensor(config[CONF_GATE_ENERGY])
cg.add(var.set_energy_sensor(sens))
ld2420 = await cg.get_variable(config[CONF_LD2420_ID])
cg.add(ld2420.register_listener(var))

View file

@ -0,0 +1,16 @@
#include "ld2420_sensor.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
namespace ld2420 {
static const char *const TAG = "LD2420.sensor";
void LD2420Sensor::dump_config() {
ESP_LOGCONFIG(TAG, "LD2420 Sensor:");
LOG_SENSOR(" ", "Distance", this->distance_sensor_);
}
} // namespace ld2420
} // namespace esphome

View file

@ -0,0 +1,34 @@
#pragma once
#include "../ld2420.h"
#include "esphome/components/sensor/sensor.h"
namespace esphome {
namespace ld2420 {
class LD2420Sensor : public LD2420Listener, public Component, sensor::Sensor {
public:
void dump_config() override;
void set_distance_sensor(sensor::Sensor *sensor) { this->distance_sensor_ = sensor; }
void on_distance(uint16_t distance) override {
if (this->distance_sensor_ != nullptr) {
if (this->distance_sensor_->get_state() != distance) {
this->distance_sensor_->publish_state(distance);
}
}
}
void on_energy(uint16_t *gate_energy, size_t size) override {
for (size_t active = 0; active < size; active++) {
if (this->energy_sensors_[active] != nullptr) {
this->energy_sensors_[active]->publish_state(gate_energy[active]);
}
}
}
protected:
sensor::Sensor *distance_sensor_{nullptr};
std::vector<sensor::Sensor *> energy_sensors_ = std::vector<sensor::Sensor *>(LD2420_TOTAL_GATES);
};
} // namespace ld2420
} // namespace esphome

View file

@ -0,0 +1,38 @@
import esphome.codegen as cg
from esphome.components import text_sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
ENTITY_CATEGORY_DIAGNOSTIC,
ICON_CHIP,
)
from .. import ld2420_ns, LD2420Component, CONF_LD2420_ID
LD2420TextSensor = ld2420_ns.class_(
"LD2420TextSensor", text_sensor.TextSensor, cg.Component
)
CONF_FW_VERSION = "fw_version"
CONFIG_SCHEMA = cv.All(
cv.COMPONENT_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(LD2420TextSensor),
cv.GenerateID(CONF_LD2420_ID): cv.use_id(LD2420Component),
cv.Optional(CONF_FW_VERSION): text_sensor.text_sensor_schema(
entity_category=ENTITY_CATEGORY_DIAGNOSTIC, icon=ICON_CHIP
),
}
),
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
if CONF_FW_VERSION in config:
sens = await text_sensor.new_text_sensor(config[CONF_FW_VERSION])
cg.add(var.set_fw_version_text_sensor(sens))
ld2420 = await cg.get_variable(config[CONF_LD2420_ID])
cg.add(ld2420.register_listener(var))

View file

@ -0,0 +1,16 @@
#include "text_sensor.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
namespace ld2420 {
static const char *const TAG = "LD2420.text_sensor";
void LD2420TextSensor::dump_config() {
ESP_LOGCONFIG(TAG, "LD2420 TextSensor:");
LOG_TEXT_SENSOR(" ", "Firmware", this->fw_version_text_sensor_);
}
} // namespace ld2420
} // namespace esphome

View file

@ -0,0 +1,24 @@
#pragma once
#include "../ld2420.h"
#include "esphome/components/text_sensor/text_sensor.h"
namespace esphome {
namespace ld2420 {
class LD2420TextSensor : public LD2420Listener, public Component, text_sensor::TextSensor {
public:
void dump_config() override;
void set_fw_version_text_sensor(text_sensor::TextSensor *tsensor) { this->fw_version_text_sensor_ = tsensor; };
void on_fw_version(std::string &fw) override {
if (this->fw_version_text_sensor_ != nullptr) {
this->fw_version_text_sensor_->publish_state(fw);
}
}
protected:
text_sensor::TextSensor *fw_version_text_sensor_{nullptr};
};
} // namespace ld2420
} // namespace esphome

View file

@ -10,6 +10,7 @@ CONF_STORE_IN_EEPROM = "store_in_eeprom"
mcp4728_ns = cg.esphome_ns.namespace("mcp4728") mcp4728_ns = cg.esphome_ns.namespace("mcp4728")
MCP4728Component = mcp4728_ns.class_("MCP4728Component", cg.Component, i2c.I2CDevice) MCP4728Component = mcp4728_ns.class_("MCP4728Component", cg.Component, i2c.I2CDevice)
CONF_MCP4728_ID = "mcp4728_id"
CONFIG_SCHEMA = ( CONFIG_SCHEMA = (
cv.Schema( cv.Schema(

View file

@ -1,4 +1,4 @@
#include "mcp4728_output.h" #include "mcp4728.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
@ -110,12 +110,5 @@ void MCP4728Component::select_gain_(MCP4728ChannelIdx channel, MCP4728Gain gain)
this->update_ = true; this->update_ = true;
} }
void MCP4728Channel::write_state(float state) {
const uint16_t max_duty = 4095;
const float duty_rounded = roundf(state * max_duty);
auto duty = static_cast<uint16_t>(duty_rounded);
this->parent_->set_channel_value_(this->channel_, duty);
}
} // namespace mcp4728 } // namespace mcp4728
} // namespace esphome } // namespace esphome

View file

@ -1,7 +1,6 @@
#pragma once #pragma once
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/components/output/float_output.h"
#include "esphome/components/i2c/i2c.h" #include "esphome/components/i2c/i2c.h"
namespace esphome { namespace esphome {
@ -64,28 +63,5 @@ class MCP4728Component : public Component, public i2c::I2CDevice {
bool update_ = false; bool update_ = false;
}; };
class MCP4728Channel : public output::FloatOutput {
public:
MCP4728Channel(MCP4728Component *parent, MCP4728ChannelIdx channel, MCP4728Vref vref, MCP4728Gain gain,
MCP4728PwrDown pwrdown)
: parent_(parent), channel_(channel), vref_(vref), gain_(gain), pwrdown_(pwrdown) {
// update VREF
parent->select_vref_(channel, vref_);
// update PD
parent->select_power_down_(channel, pwrdown_);
// update GAIN
parent->select_gain_(channel, gain_);
}
protected:
void write_state(float state) override;
MCP4728Component *parent_;
MCP4728ChannelIdx channel_;
MCP4728Vref vref_;
MCP4728Gain gain_;
MCP4728PwrDown pwrdown_;
};
} // namespace mcp4728 } // namespace mcp4728
} // namespace esphome } // namespace esphome

View file

@ -2,12 +2,11 @@ import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.components import output from esphome.components import output
from esphome.const import CONF_CHANNEL, CONF_ID, CONF_GAIN from esphome.const import CONF_CHANNEL, CONF_ID, CONF_GAIN
from . import MCP4728Component, mcp4728_ns from .. import MCP4728Component, CONF_MCP4728_ID, mcp4728_ns
DEPENDENCIES = ["mcp4728"] DEPENDENCIES = ["mcp4728"]
MCP4728Channel = mcp4728_ns.class_("MCP4728Channel", output.FloatOutput) MCP4728Channel = mcp4728_ns.class_("MCP4728Channel", output.FloatOutput)
CONF_MCP4728_ID = "mcp4728_id"
CONF_VREF = "vref" CONF_VREF = "vref"
CONF_POWER_DOWN = "power_down" CONF_POWER_DOWN = "power_down"

View file

@ -0,0 +1,17 @@
#include "mcp4728_output.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
namespace mcp4728 {
void MCP4728Channel::write_state(float state) {
const uint16_t max_duty = 4095;
const float duty_rounded = roundf(state * max_duty);
auto duty = static_cast<uint16_t>(duty_rounded);
this->parent_->set_channel_value_(this->channel_, duty);
}
} // namespace mcp4728
} // namespace esphome

View file

@ -0,0 +1,32 @@
#pragma once
#include "../mcp4728.h"
#include "esphome/core/component.h"
#include "esphome/components/output/float_output.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome {
namespace mcp4728 {
class MCP4728Channel : public output::FloatOutput {
public:
MCP4728Channel(MCP4728Component *parent, MCP4728ChannelIdx channel, MCP4728Vref vref, MCP4728Gain gain,
MCP4728PwrDown pwrdown)
: parent_(parent), channel_(channel) {
// update VREF
parent->select_vref_(channel, vref);
// update PD
parent->select_power_down_(channel, pwrdown);
// update GAIN
parent->select_gain_(channel, gain);
}
protected:
void write_state(float state) override;
MCP4728Component *parent_;
MCP4728ChannelIdx channel_;
};
} // namespace mcp4728
} // namespace esphome

View file

@ -88,7 +88,7 @@ async def to_code(config):
add_idf_component( add_idf_component(
name="mdns", name="mdns",
repo="https://github.com/espressif/esp-protocols.git", repo="https://github.com/espressif/esp-protocols.git",
ref="mdns-v1.2.0", ref="mdns-v1.2.2",
path="components/mdns", path="components/mdns",
) )

View file

@ -0,0 +1,69 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import pins
from esphome.components import uart
from esphome.const import (
CONF_ID,
)
CODEOWNERS = ["@jorre05"]
DEPENDENCIES = ["uart"]
CONF_MICRONOVA_ID = "micronova_id"
CONF_ENABLE_RX_PIN = "enable_rx_pin"
CONF_MEMORY_LOCATION = "memory_location"
CONF_MEMORY_ADDRESS = "memory_address"
micronova_ns = cg.esphome_ns.namespace("micronova")
MicroNovaFunctions = micronova_ns.enum("MicroNovaFunctions", is_class=True)
MICRONOVA_FUNCTIONS_ENUM = {
"STOVE_FUNCTION_SWITCH": MicroNovaFunctions.STOVE_FUNCTION_SWITCH,
"STOVE_FUNCTION_ROOM_TEMPERATURE": MicroNovaFunctions.STOVE_FUNCTION_ROOM_TEMPERATURE,
"STOVE_FUNCTION_THERMOSTAT_TEMPERATURE": MicroNovaFunctions.STOVE_FUNCTION_THERMOSTAT_TEMPERATURE,
"STOVE_FUNCTION_FUMES_TEMPERATURE": MicroNovaFunctions.STOVE_FUNCTION_FUMES_TEMPERATURE,
"STOVE_FUNCTION_STOVE_POWER": MicroNovaFunctions.STOVE_FUNCTION_STOVE_POWER,
"STOVE_FUNCTION_FAN_SPEED": MicroNovaFunctions.STOVE_FUNCTION_FAN_SPEED,
"STOVE_FUNCTION_STOVE_STATE": MicroNovaFunctions.STOVE_FUNCTION_STOVE_STATE,
"STOVE_FUNCTION_MEMORY_ADDRESS_SENSOR": MicroNovaFunctions.STOVE_FUNCTION_MEMORY_ADDRESS_SENSOR,
"STOVE_FUNCTION_WATER_TEMPERATURE": MicroNovaFunctions.STOVE_FUNCTION_WATER_TEMPERATURE,
"STOVE_FUNCTION_WATER_PRESSURE": MicroNovaFunctions.STOVE_FUNCTION_WATER_PRESSURE,
"STOVE_FUNCTION_POWER_LEVEL": MicroNovaFunctions.STOVE_FUNCTION_POWER_LEVEL,
"STOVE_FUNCTION_CUSTOM": MicroNovaFunctions.STOVE_FUNCTION_CUSTOM,
}
MicroNova = micronova_ns.class_("MicroNova", cg.PollingComponent, uart.UARTDevice)
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(MicroNova),
cv.Required(CONF_ENABLE_RX_PIN): pins.gpio_output_pin_schema,
}
)
.extend(uart.UART_DEVICE_SCHEMA)
.extend(cv.polling_component_schema("60s"))
)
def MICRONOVA_LISTENER_SCHEMA(default_memory_location, default_memory_address):
return cv.Schema(
{
cv.GenerateID(CONF_MICRONOVA_ID): cv.use_id(MicroNova),
cv.Optional(
CONF_MEMORY_LOCATION, default=default_memory_location
): cv.hex_int_range(),
cv.Optional(
CONF_MEMORY_ADDRESS, default=default_memory_address
): cv.hex_int_range(),
}
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
enable_rx_pin = await cg.gpio_pin_expression(config[CONF_ENABLE_RX_PIN])
cg.add(var.set_enable_rx_pin(enable_rx_pin))

View file

@ -0,0 +1,44 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import button
from .. import (
MicroNova,
MicroNovaFunctions,
CONF_MICRONOVA_ID,
CONF_MEMORY_LOCATION,
CONF_MEMORY_ADDRESS,
MICRONOVA_LISTENER_SCHEMA,
micronova_ns,
)
MicroNovaButton = micronova_ns.class_("MicroNovaButton", button.Button, cg.Component)
CONF_CUSTOM_BUTTON = "custom_button"
CONF_MEMORY_DATA = "memory_data"
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_MICRONOVA_ID): cv.use_id(MicroNova),
cv.Optional(CONF_CUSTOM_BUTTON): button.button_schema(
MicroNovaButton,
)
.extend(
MICRONOVA_LISTENER_SCHEMA(
default_memory_location=0xA0, default_memory_address=0x7D
)
)
.extend({cv.Required(CONF_MEMORY_DATA): cv.hex_int_range()}),
}
)
async def to_code(config):
mv = await cg.get_variable(config[CONF_MICRONOVA_ID])
if custom_button_config := config.get(CONF_CUSTOM_BUTTON):
bt = await button.new_button(custom_button_config, mv)
cg.add(bt.set_memory_location(custom_button_config.get(CONF_MEMORY_LOCATION)))
cg.add(bt.set_memory_address(custom_button_config.get(CONF_MEMORY_ADDRESS)))
cg.add(bt.set_memory_data(custom_button_config[CONF_MEMORY_DATA]))
cg.add(bt.set_function(MicroNovaFunctions.STOVE_FUNCTION_CUSTOM))

View file

@ -0,0 +1,18 @@
#include "micronova_button.h"
namespace esphome {
namespace micronova {
void MicroNovaButton::press_action() {
switch (this->get_function()) {
case MicroNovaFunctions::STOVE_FUNCTION_CUSTOM:
this->micronova_->write_address(this->memory_location_, this->memory_address_, this->memory_data_);
break;
default:
break;
}
this->micronova_->update();
}
} // namespace micronova
} // namespace esphome

View file

@ -0,0 +1,23 @@
#pragma once
#include "esphome/components/micronova/micronova.h"
#include "esphome/core/component.h"
#include "esphome/components/button/button.h"
namespace esphome {
namespace micronova {
class MicroNovaButton : public Component, public button::Button, public MicroNovaButtonListener {
public:
MicroNovaButton(MicroNova *m) : MicroNovaButtonListener(m) {}
void dump_config() override { LOG_BUTTON("", "Micronova button", this); }
void set_memory_data(uint8_t f) { this->memory_data_ = f; }
uint8_t get_memory_data() { return this->memory_data_; }
protected:
void press_action() override;
};
} // namespace micronova
} // namespace esphome

View file

@ -0,0 +1,148 @@
#include "micronova.h"
#include "esphome/core/log.h"
namespace esphome {
namespace micronova {
void MicroNova::setup() {
if (this->enable_rx_pin_ != nullptr) {
this->enable_rx_pin_->setup();
this->enable_rx_pin_->pin_mode(gpio::FLAG_OUTPUT);
this->enable_rx_pin_->digital_write(false);
}
this->current_transmission_.request_transmission_time = millis();
this->current_transmission_.memory_location = 0;
this->current_transmission_.memory_address = 0;
this->current_transmission_.reply_pending = false;
this->current_transmission_.initiating_listener = nullptr;
}
void MicroNova::dump_config() {
ESP_LOGCONFIG(TAG, "MicroNova:");
if (this->enable_rx_pin_ != nullptr) {
LOG_PIN(" Enable RX Pin: ", this->enable_rx_pin_);
}
for (auto &mv_sensor : this->micronova_listeners_) {
mv_sensor->dump_config();
ESP_LOGCONFIG(TAG, " sensor location:%02X, address:%02X", mv_sensor->get_memory_location(),
mv_sensor->get_memory_address());
}
}
void MicroNova::update() {
ESP_LOGD(TAG, "Schedule sensor update");
for (auto &mv_listener : this->micronova_listeners_) {
mv_listener->set_needs_update(true);
}
}
void MicroNova::loop() {
// Only read one sensor that needs update per loop
// If STOVE_REPLY_DELAY time has passed since last loop()
// check for a reply from the stove
if ((this->current_transmission_.reply_pending) &&
(millis() - this->current_transmission_.request_transmission_time > STOVE_REPLY_DELAY)) {
int stove_reply_value = this->read_stove_reply();
if (this->current_transmission_.initiating_listener != nullptr) {
this->current_transmission_.initiating_listener->process_value_from_stove(stove_reply_value);
this->current_transmission_.initiating_listener = nullptr;
}
this->current_transmission_.reply_pending = false;
return;
} else if (!this->current_transmission_.reply_pending) {
for (auto &mv_listener : this->micronova_listeners_) {
if (mv_listener->get_needs_update()) {
mv_listener->set_needs_update(false);
this->current_transmission_.initiating_listener = mv_listener;
mv_listener->request_value_from_stove();
return;
}
}
}
}
void MicroNova::request_address(uint8_t location, uint8_t address, MicroNovaSensorListener *listener) {
uint8_t write_data[2] = {0, 0};
uint8_t trash_rx;
if (this->reply_pending_mutex_.try_lock()) {
// clear rx buffer.
// Stove hickups may cause late replies in the rx
while (this->available()) {
this->read_byte(&trash_rx);
ESP_LOGW(TAG, "Reading excess byte 0x%02X", trash_rx);
}
write_data[0] = location;
write_data[1] = address;
ESP_LOGV(TAG, "Request from stove [%02X,%02X]", write_data[0], write_data[1]);
this->enable_rx_pin_->digital_write(true);
this->write_array(write_data, 2);
this->flush();
this->enable_rx_pin_->digital_write(false);
this->current_transmission_.request_transmission_time = millis();
this->current_transmission_.memory_location = location;
this->current_transmission_.memory_address = address;
this->current_transmission_.reply_pending = true;
this->current_transmission_.initiating_listener = listener;
} else {
ESP_LOGE(TAG, "Reply is pending, skipping read request");
}
}
int MicroNova::read_stove_reply() {
uint8_t reply_data[2] = {0, 0};
uint8_t checksum = 0;
// assert enable_rx_pin is false
this->read_array(reply_data, 2);
this->reply_pending_mutex_.unlock();
ESP_LOGV(TAG, "Reply from stove [%02X,%02X]", reply_data[0], reply_data[1]);
checksum = ((uint16_t) this->current_transmission_.memory_location +
(uint16_t) this->current_transmission_.memory_address + (uint16_t) reply_data[1]) &
0xFF;
if (reply_data[0] != checksum) {
ESP_LOGE(TAG, "Checksum missmatch! From [0x%02X:0x%02X] received [0x%02X,0x%02X]. Expected 0x%02X, got 0x%02X",
this->current_transmission_.memory_location, this->current_transmission_.memory_address, reply_data[0],
reply_data[1], checksum, reply_data[0]);
return -1;
}
return ((int) reply_data[1]);
}
void MicroNova::write_address(uint8_t location, uint8_t address, uint8_t data) {
uint8_t write_data[4] = {0, 0, 0, 0};
uint16_t checksum = 0;
if (this->reply_pending_mutex_.try_lock()) {
write_data[0] = location;
write_data[1] = address;
write_data[2] = data;
checksum = ((uint16_t) write_data[0] + (uint16_t) write_data[1] + (uint16_t) write_data[2]) & 0xFF;
write_data[3] = checksum;
ESP_LOGV(TAG, "Write 4 bytes [%02X,%02X,%02X,%02X]", write_data[0], write_data[1], write_data[2], write_data[3]);
this->enable_rx_pin_->digital_write(true);
this->write_array(write_data, 4);
this->flush();
this->enable_rx_pin_->digital_write(false);
this->current_transmission_.request_transmission_time = millis();
this->current_transmission_.memory_location = location;
this->current_transmission_.memory_address = address;
this->current_transmission_.reply_pending = true;
this->current_transmission_.initiating_listener = nullptr;
} else {
ESP_LOGE(TAG, "Reply is pending, skipping write");
}
}
} // namespace micronova
} // namespace esphome

View file

@ -0,0 +1,164 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/uart/uart.h"
#include "esphome/core/log.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include <vector>
namespace esphome {
namespace micronova {
static const char *const TAG = "micronova";
static const int STOVE_REPLY_DELAY = 60;
static const std::string STOVE_STATES[11] = {"Off",
"Start",
"Pellets loading",
"Ignition",
"Working",
"Brazier Cleaning",
"Final Cleaning",
"Standby",
"No pellets alarm",
"No ignition alarm",
"Undefined alarm"};
enum class MicroNovaFunctions {
STOVE_FUNCTION_VOID = 0,
STOVE_FUNCTION_SWITCH = 1,
STOVE_FUNCTION_ROOM_TEMPERATURE = 2,
STOVE_FUNCTION_THERMOSTAT_TEMPERATURE = 3,
STOVE_FUNCTION_FUMES_TEMPERATURE = 4,
STOVE_FUNCTION_STOVE_POWER = 5,
STOVE_FUNCTION_FAN_SPEED = 6,
STOVE_FUNCTION_STOVE_STATE = 7,
STOVE_FUNCTION_MEMORY_ADDRESS_SENSOR = 8,
STOVE_FUNCTION_WATER_TEMPERATURE = 9,
STOVE_FUNCTION_WATER_PRESSURE = 10,
STOVE_FUNCTION_POWER_LEVEL = 11,
STOVE_FUNCTION_CUSTOM = 12
};
class MicroNova;
//////////////////////////////////////////////////////////////////////
// Interface classes.
class MicroNovaBaseListener {
public:
MicroNovaBaseListener() {}
MicroNovaBaseListener(MicroNova *m) { this->micronova_ = m; }
virtual void dump_config();
void set_micronova_object(MicroNova *m) { this->micronova_ = m; }
void set_function(MicroNovaFunctions f) { this->function_ = f; }
MicroNovaFunctions get_function() { return this->function_; }
void set_memory_location(uint8_t l) { this->memory_location_ = l; }
uint8_t get_memory_location() { return this->memory_location_; }
void set_memory_address(uint8_t a) { this->memory_address_ = a; }
uint8_t get_memory_address() { return this->memory_address_; }
protected:
MicroNova *micronova_{nullptr};
MicroNovaFunctions function_ = MicroNovaFunctions::STOVE_FUNCTION_VOID;
uint8_t memory_location_ = 0;
uint8_t memory_address_ = 0;
};
class MicroNovaSensorListener : public MicroNovaBaseListener {
public:
MicroNovaSensorListener() {}
MicroNovaSensorListener(MicroNova *m) : MicroNovaBaseListener(m) {}
virtual void request_value_from_stove() = 0;
virtual void process_value_from_stove(int value_from_stove) = 0;
void set_needs_update(bool u) { this->needs_update_ = u; }
bool get_needs_update() { return this->needs_update_; }
protected:
bool needs_update_ = false;
};
class MicroNovaNumberListener : public MicroNovaBaseListener {
public:
MicroNovaNumberListener(MicroNova *m) : MicroNovaBaseListener(m) {}
virtual void request_value_from_stove() = 0;
virtual void process_value_from_stove(int value_from_stove) = 0;
void set_needs_update(bool u) { this->needs_update_ = u; }
bool get_needs_update() { return this->needs_update_; }
protected:
bool needs_update_ = false;
};
class MicroNovaSwitchListener : public MicroNovaBaseListener {
public:
MicroNovaSwitchListener(MicroNova *m) : MicroNovaBaseListener(m) {}
virtual void set_stove_state(bool v) = 0;
virtual bool get_stove_state() = 0;
protected:
uint8_t memory_data_on_ = 0;
uint8_t memory_data_off_ = 0;
};
class MicroNovaButtonListener : public MicroNovaBaseListener {
public:
MicroNovaButtonListener(MicroNova *m) : MicroNovaBaseListener(m) {}
protected:
uint8_t memory_data_ = 0;
};
/////////////////////////////////////////////////////////////////////
// Main component class
class MicroNova : public PollingComponent, public uart::UARTDevice {
public:
MicroNova() {}
void setup() override;
void loop() override;
void update() override;
void dump_config() override;
void register_micronova_listener(MicroNovaSensorListener *l) { this->micronova_listeners_.push_back(l); }
void request_address(uint8_t location, uint8_t address, MicroNovaSensorListener *listener);
void write_address(uint8_t location, uint8_t address, uint8_t data);
int read_stove_reply();
void set_enable_rx_pin(GPIOPin *enable_rx_pin) { this->enable_rx_pin_ = enable_rx_pin; }
void set_current_stove_state(uint8_t s) { this->current_stove_state_ = s; }
uint8_t get_current_stove_state() { return this->current_stove_state_; }
void set_stove(MicroNovaSwitchListener *s) { this->stove_switch_ = s; }
MicroNovaSwitchListener *get_stove_switch() { return this->stove_switch_; }
protected:
uint8_t current_stove_state_ = 0;
GPIOPin *enable_rx_pin_{nullptr};
struct MicroNovaSerialTransmission {
uint32_t request_transmission_time;
uint8_t memory_location;
uint8_t memory_address;
bool reply_pending;
MicroNovaSensorListener *initiating_listener;
};
Mutex reply_pending_mutex_;
MicroNovaSerialTransmission current_transmission_;
std::vector<MicroNovaSensorListener *> micronova_listeners_{};
MicroNovaSwitchListener *stove_switch_{nullptr};
};
} // namespace micronova
} // namespace esphome

View file

@ -0,0 +1,110 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import number
from esphome.const import (
DEVICE_CLASS_TEMPERATURE,
UNIT_CELSIUS,
CONF_STEP,
)
from .. import (
MicroNova,
MicroNovaFunctions,
CONF_MICRONOVA_ID,
CONF_MEMORY_LOCATION,
CONF_MEMORY_ADDRESS,
MICRONOVA_LISTENER_SCHEMA,
micronova_ns,
)
ICON_FLASH = "mdi:flash"
CONF_THERMOSTAT_TEMPERATURE = "thermostat_temperature"
CONF_POWER_LEVEL = "power_level"
CONF_MEMORY_WRITE_LOCATION = "memory_write_location"
MicroNovaNumber = micronova_ns.class_("MicroNovaNumber", number.Number, cg.Component)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_MICRONOVA_ID): cv.use_id(MicroNova),
cv.Optional(CONF_THERMOSTAT_TEMPERATURE): number.number_schema(
MicroNovaNumber,
unit_of_measurement=UNIT_CELSIUS,
device_class=DEVICE_CLASS_TEMPERATURE,
)
.extend(
MICRONOVA_LISTENER_SCHEMA(
default_memory_location=0x20, default_memory_address=0x7D
)
)
.extend(
{
cv.Optional(
CONF_MEMORY_WRITE_LOCATION, default=0xA0
): cv.hex_int_range(),
cv.Optional(CONF_STEP, default=1.0): cv.float_range(min=0.1, max=10.0),
}
),
cv.Optional(CONF_POWER_LEVEL): number.number_schema(
MicroNovaNumber,
icon=ICON_FLASH,
)
.extend(
MICRONOVA_LISTENER_SCHEMA(
default_memory_location=0x20, default_memory_address=0x7F
)
)
.extend(
{cv.Optional(CONF_MEMORY_WRITE_LOCATION, default=0xA0): cv.hex_int_range()}
),
}
)
async def to_code(config):
mv = await cg.get_variable(config[CONF_MICRONOVA_ID])
if thermostat_temperature_config := config.get(CONF_THERMOSTAT_TEMPERATURE):
numb = await number.new_number(
thermostat_temperature_config,
min_value=0,
max_value=40,
step=thermostat_temperature_config.get(CONF_STEP),
)
cg.add(numb.set_micronova_object(mv))
cg.add(mv.register_micronova_listener(numb))
cg.add(
numb.set_memory_location(
thermostat_temperature_config[CONF_MEMORY_LOCATION]
)
)
cg.add(
numb.set_memory_address(thermostat_temperature_config[CONF_MEMORY_ADDRESS])
)
cg.add(
numb.set_memory_write_location(
thermostat_temperature_config.get(CONF_MEMORY_WRITE_LOCATION)
)
)
cg.add(
numb.set_function(MicroNovaFunctions.STOVE_FUNCTION_THERMOSTAT_TEMPERATURE)
)
if power_level_config := config.get(CONF_POWER_LEVEL):
numb = await number.new_number(
power_level_config,
min_value=1,
max_value=5,
step=1,
)
cg.add(numb.set_micronova_object(mv))
cg.add(mv.register_micronova_listener(numb))
cg.add(numb.set_memory_location(power_level_config[CONF_MEMORY_LOCATION]))
cg.add(numb.set_memory_address(power_level_config[CONF_MEMORY_ADDRESS]))
cg.add(
numb.set_memory_write_location(
power_level_config.get(CONF_MEMORY_WRITE_LOCATION)
)
)
cg.add(numb.set_function(MicroNovaFunctions.STOVE_FUNCTION_POWER_LEVEL))

View file

@ -0,0 +1,45 @@
#include "micronova_number.h"
namespace esphome {
namespace micronova {
void MicroNovaNumber::process_value_from_stove(int value_from_stove) {
float new_sensor_value = 0;
if (value_from_stove == -1) {
this->publish_state(NAN);
return;
}
switch (this->get_function()) {
case MicroNovaFunctions::STOVE_FUNCTION_THERMOSTAT_TEMPERATURE:
new_sensor_value = ((float) value_from_stove) * this->traits.get_step();
break;
case MicroNovaFunctions::STOVE_FUNCTION_POWER_LEVEL:
new_sensor_value = (float) value_from_stove;
break;
default:
break;
}
this->publish_state(new_sensor_value);
}
void MicroNovaNumber::control(float value) {
uint8_t new_number = 0;
switch (this->get_function()) {
case MicroNovaFunctions::STOVE_FUNCTION_THERMOSTAT_TEMPERATURE:
new_number = (uint8_t) (value / this->traits.get_step());
break;
case MicroNovaFunctions::STOVE_FUNCTION_POWER_LEVEL:
new_number = (uint8_t) value;
break;
default:
break;
}
this->micronova_->write_address(this->memory_write_location_, this->memory_address_, new_number);
this->micronova_->update();
}
} // namespace micronova
} // namespace esphome

View file

@ -0,0 +1,28 @@
#pragma once
#include "esphome/components/micronova/micronova.h"
#include "esphome/components/number/number.h"
namespace esphome {
namespace micronova {
class MicroNovaNumber : public number::Number, public MicroNovaSensorListener {
public:
MicroNovaNumber() {}
MicroNovaNumber(MicroNova *m) : MicroNovaSensorListener(m) {}
void dump_config() override { LOG_NUMBER("", "Micronova number", this); }
void control(float value) override;
void request_value_from_stove() override {
this->micronova_->request_address(this->memory_location_, this->memory_address_, this);
}
void process_value_from_stove(int value_from_stove) override;
void set_memory_write_location(uint8_t l) { this->memory_write_location_ = l; }
uint8_t get_memory_write_location() { return this->memory_write_location_; }
protected:
uint8_t memory_write_location_ = 0;
};
} // namespace micronova
} // namespace esphome

View file

@ -0,0 +1,172 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor
from esphome.const import (
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_PRESSURE,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
UNIT_REVOLUTIONS_PER_MINUTE,
)
from .. import (
MicroNova,
MicroNovaFunctions,
CONF_MICRONOVA_ID,
CONF_MEMORY_LOCATION,
CONF_MEMORY_ADDRESS,
MICRONOVA_LISTENER_SCHEMA,
micronova_ns,
)
UNIT_BAR = "bar"
MicroNovaSensor = micronova_ns.class_("MicroNovaSensor", sensor.Sensor, cg.Component)
CONF_ROOM_TEMPERATURE = "room_temperature"
CONF_FUMES_TEMPERATURE = "fumes_temperature"
CONF_STOVE_POWER = "stove_power"
CONF_FAN_SPEED = "fan_speed"
CONF_WATER_TEMPERATURE = "water_temperature"
CONF_WATER_PRESSURE = "water_pressure"
CONF_MEMORY_ADDRESS_SENSOR = "memory_address_sensor"
CONF_FAN_RPM_OFFSET = "fan_rpm_offset"
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_MICRONOVA_ID): cv.use_id(MicroNova),
cv.Optional(CONF_ROOM_TEMPERATURE): sensor.sensor_schema(
MicroNovaSensor,
unit_of_measurement=UNIT_CELSIUS,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
accuracy_decimals=1,
).extend(
MICRONOVA_LISTENER_SCHEMA(
default_memory_location=0x00, default_memory_address=0x01
)
),
cv.Optional(CONF_FUMES_TEMPERATURE): sensor.sensor_schema(
MicroNovaSensor,
unit_of_measurement=UNIT_CELSIUS,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
accuracy_decimals=1,
).extend(
MICRONOVA_LISTENER_SCHEMA(
default_memory_location=0x00, default_memory_address=0x5A
)
),
cv.Optional(CONF_STOVE_POWER): sensor.sensor_schema(
MicroNovaSensor,
state_class=STATE_CLASS_MEASUREMENT,
accuracy_decimals=0,
).extend(
MICRONOVA_LISTENER_SCHEMA(
default_memory_location=0x00, default_memory_address=0x34
)
),
cv.Optional(CONF_FAN_SPEED): sensor.sensor_schema(
MicroNovaSensor,
state_class=STATE_CLASS_MEASUREMENT,
unit_of_measurement=UNIT_REVOLUTIONS_PER_MINUTE,
)
.extend(
MICRONOVA_LISTENER_SCHEMA(
default_memory_location=0x00, default_memory_address=0x37
)
)
.extend(
{cv.Optional(CONF_FAN_RPM_OFFSET, default=0): cv.int_range(min=0, max=255)}
),
cv.Optional(CONF_WATER_TEMPERATURE): sensor.sensor_schema(
MicroNovaSensor,
unit_of_measurement=UNIT_CELSIUS,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
accuracy_decimals=1,
).extend(
MICRONOVA_LISTENER_SCHEMA(
default_memory_location=0x00, default_memory_address=0x3B
)
),
cv.Optional(CONF_WATER_PRESSURE): sensor.sensor_schema(
MicroNovaSensor,
unit_of_measurement=UNIT_BAR,
device_class=DEVICE_CLASS_PRESSURE,
state_class=STATE_CLASS_MEASUREMENT,
accuracy_decimals=1,
).extend(
MICRONOVA_LISTENER_SCHEMA(
default_memory_location=0x00, default_memory_address=0x3C
)
),
cv.Optional(CONF_MEMORY_ADDRESS_SENSOR): sensor.sensor_schema(
MicroNovaSensor,
).extend(
MICRONOVA_LISTENER_SCHEMA(
default_memory_location=0x00, default_memory_address=0x00
)
),
}
)
async def to_code(config):
mv = await cg.get_variable(config[CONF_MICRONOVA_ID])
if room_temperature_config := config.get(CONF_ROOM_TEMPERATURE):
sens = await sensor.new_sensor(room_temperature_config, mv)
cg.add(mv.register_micronova_listener(sens))
cg.add(sens.set_memory_location(room_temperature_config[CONF_MEMORY_LOCATION]))
cg.add(sens.set_memory_address(room_temperature_config[CONF_MEMORY_ADDRESS]))
cg.add(sens.set_function(MicroNovaFunctions.STOVE_FUNCTION_ROOM_TEMPERATURE))
if fumes_temperature_config := config.get(CONF_FUMES_TEMPERATURE):
sens = await sensor.new_sensor(fumes_temperature_config, mv)
cg.add(mv.register_micronova_listener(sens))
cg.add(sens.set_memory_location(fumes_temperature_config[CONF_MEMORY_LOCATION]))
cg.add(sens.set_memory_address(fumes_temperature_config[CONF_MEMORY_ADDRESS]))
cg.add(sens.set_function(MicroNovaFunctions.STOVE_FUNCTION_FUMES_TEMPERATURE))
if stove_power_config := config.get(CONF_STOVE_POWER):
sens = await sensor.new_sensor(stove_power_config, mv)
cg.add(mv.register_micronova_listener(sens))
cg.add(sens.set_memory_location(stove_power_config[CONF_MEMORY_LOCATION]))
cg.add(sens.set_memory_address(stove_power_config[CONF_MEMORY_ADDRESS]))
cg.add(sens.set_function(MicroNovaFunctions.STOVE_FUNCTION_STOVE_POWER))
if fan_speed_config := config.get(CONF_FAN_SPEED):
sens = await sensor.new_sensor(fan_speed_config, mv)
cg.add(mv.register_micronova_listener(sens))
cg.add(sens.set_memory_location(fan_speed_config[CONF_MEMORY_LOCATION]))
cg.add(sens.set_memory_address(fan_speed_config[CONF_MEMORY_ADDRESS]))
cg.add(sens.set_function(MicroNovaFunctions.STOVE_FUNCTION_FAN_SPEED))
cg.add(sens.set_fan_speed_offset(fan_speed_config[CONF_FAN_RPM_OFFSET]))
if memory_address_sensor_config := config.get(CONF_MEMORY_ADDRESS_SENSOR):
sens = await sensor.new_sensor(memory_address_sensor_config, mv)
cg.add(mv.register_micronova_listener(sens))
cg.add(
sens.set_memory_location(memory_address_sensor_config[CONF_MEMORY_LOCATION])
)
cg.add(
sens.set_memory_address(memory_address_sensor_config[CONF_MEMORY_ADDRESS])
)
cg.add(
sens.set_function(MicroNovaFunctions.STOVE_FUNCTION_MEMORY_ADDRESS_SENSOR)
)
if water_temperature_config := config.get(CONF_WATER_TEMPERATURE):
sens = await sensor.new_sensor(water_temperature_config, mv)
cg.add(mv.register_micronova_listener(sens))
cg.add(sens.set_memory_location(water_temperature_config[CONF_MEMORY_LOCATION]))
cg.add(sens.set_memory_address(water_temperature_config[CONF_MEMORY_ADDRESS]))
cg.add(sens.set_function(MicroNovaFunctions.STOVE_FUNCTION_WATER_TEMPERATURE))
if water_pressure_config := config.get(CONF_WATER_PRESSURE):
sens = await sensor.new_sensor(water_pressure_config, mv)
cg.add(mv.register_micronova_listener(sens))
cg.add(sens.set_memory_location(water_pressure_config[CONF_MEMORY_LOCATION]))
cg.add(sens.set_memory_address(water_pressure_config[CONF_MEMORY_ADDRESS]))
cg.add(sens.set_function(MicroNovaFunctions.STOVE_FUNCTION_WATER_PRESSURE))

View file

@ -0,0 +1,35 @@
#include "micronova_sensor.h"
namespace esphome {
namespace micronova {
void MicroNovaSensor::process_value_from_stove(int value_from_stove) {
if (value_from_stove == -1) {
this->publish_state(NAN);
return;
}
float new_sensor_value = (float) value_from_stove;
switch (this->get_function()) {
case MicroNovaFunctions::STOVE_FUNCTION_ROOM_TEMPERATURE:
new_sensor_value = new_sensor_value / 2;
break;
case MicroNovaFunctions::STOVE_FUNCTION_THERMOSTAT_TEMPERATURE:
break;
case MicroNovaFunctions::STOVE_FUNCTION_FAN_SPEED:
new_sensor_value = new_sensor_value == 0 ? 0 : (new_sensor_value * 10) + this->fan_speed_offset_;
break;
case MicroNovaFunctions::STOVE_FUNCTION_WATER_TEMPERATURE:
new_sensor_value = new_sensor_value / 2;
break;
case MicroNovaFunctions::STOVE_FUNCTION_WATER_PRESSURE:
new_sensor_value = new_sensor_value / 10;
break;
default:
break;
}
this->publish_state(new_sensor_value);
}
} // namespace micronova
} // namespace esphome

View file

@ -0,0 +1,27 @@
#pragma once
#include "esphome/components/micronova/micronova.h"
#include "esphome/components/sensor/sensor.h"
namespace esphome {
namespace micronova {
class MicroNovaSensor : public sensor::Sensor, public MicroNovaSensorListener {
public:
MicroNovaSensor(MicroNova *m) : MicroNovaSensorListener(m) {}
void dump_config() override { LOG_SENSOR("", "Micronova sensor", this); }
void request_value_from_stove() override {
this->micronova_->request_address(this->memory_location_, this->memory_address_, this);
}
void process_value_from_stove(int value_from_stove) override;
void set_fan_speed_offset(uint8_t f) { this->fan_speed_offset_ = f; }
uint8_t get_set_fan_speed_offset() { return this->fan_speed_offset_; }
protected:
int fan_speed_offset_ = 0;
};
} // namespace micronova
} // namespace esphome

View file

@ -0,0 +1,56 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import switch
from esphome.const import (
ICON_POWER,
)
from .. import (
MicroNova,
MicroNovaFunctions,
CONF_MICRONOVA_ID,
CONF_MEMORY_LOCATION,
CONF_MEMORY_ADDRESS,
MICRONOVA_LISTENER_SCHEMA,
micronova_ns,
)
CONF_STOVE = "stove"
CONF_MEMORY_DATA_ON = "memory_data_on"
CONF_MEMORY_DATA_OFF = "memory_data_off"
MicroNovaSwitch = micronova_ns.class_("MicroNovaSwitch", switch.Switch, cg.Component)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_MICRONOVA_ID): cv.use_id(MicroNova),
cv.Optional(CONF_STOVE): switch.switch_schema(
MicroNovaSwitch,
icon=ICON_POWER,
)
.extend(
MICRONOVA_LISTENER_SCHEMA(
default_memory_location=0x80, default_memory_address=0x21
)
)
.extend(
{
cv.Optional(CONF_MEMORY_DATA_OFF, default=0x06): cv.hex_int_range(),
cv.Optional(CONF_MEMORY_DATA_ON, default=0x01): cv.hex_int_range(),
}
),
}
)
async def to_code(config):
mv = await cg.get_variable(config[CONF_MICRONOVA_ID])
if stove_config := config.get(CONF_STOVE):
sw = await switch.new_switch(stove_config, mv)
cg.add(mv.set_stove(sw))
cg.add(sw.set_memory_location(stove_config[CONF_MEMORY_LOCATION]))
cg.add(sw.set_memory_address(stove_config[CONF_MEMORY_ADDRESS]))
cg.add(sw.set_memory_data_on(stove_config[CONF_MEMORY_DATA_ON]))
cg.add(sw.set_memory_data_off(stove_config[CONF_MEMORY_DATA_OFF]))
cg.add(sw.set_function(MicroNovaFunctions.STOVE_FUNCTION_SWITCH))

View file

@ -0,0 +1,33 @@
#include "micronova_switch.h"
namespace esphome {
namespace micronova {
void MicroNovaSwitch::write_state(bool state) {
switch (this->get_function()) {
case MicroNovaFunctions::STOVE_FUNCTION_SWITCH:
if (state) {
// Only send power-on when current state is Off
if (this->micronova_->get_current_stove_state() == 0) {
this->micronova_->write_address(this->memory_location_, this->memory_address_, this->memory_data_on_);
this->publish_state(true);
} else
ESP_LOGW(TAG, "Unable to turn stove on, invalid state: %d", micronova_->get_current_stove_state());
} else {
// don't send power-off when status is Off or Final cleaning
if (this->micronova_->get_current_stove_state() != 0 && micronova_->get_current_stove_state() != 6) {
this->micronova_->write_address(this->memory_location_, this->memory_address_, this->memory_data_off_);
this->publish_state(false);
} else
ESP_LOGW(TAG, "Unable to turn stove off, invalid state: %d", micronova_->get_current_stove_state());
}
this->micronova_->update();
break;
default:
break;
}
}
} // namespace micronova
} // namespace esphome

View file

@ -0,0 +1,29 @@
#pragma once
#include "esphome/components/micronova/micronova.h"
#include "esphome/core/component.h"
#include "esphome/components/switch/switch.h"
namespace esphome {
namespace micronova {
class MicroNovaSwitch : public Component, public switch_::Switch, public MicroNovaSwitchListener {
public:
MicroNovaSwitch(MicroNova *m) : MicroNovaSwitchListener(m) {}
void dump_config() override { LOG_SWITCH("", "Micronova switch", this); }
void set_stove_state(bool v) override { this->publish_state(v); }
bool get_stove_state() override { return this->state; }
void set_memory_data_on(uint8_t f) { this->memory_data_on_ = f; }
uint8_t get_memory_data_on() { return this->memory_data_on_; }
void set_memory_data_off(uint8_t f) { this->memory_data_off_ = f; }
uint8_t get_memory_data_off() { return this->memory_data_off_; }
protected:
void write_state(bool state) override;
};
} // namespace micronova
} // namespace esphome

View file

@ -0,0 +1,43 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import text_sensor
from .. import (
MicroNova,
MicroNovaFunctions,
CONF_MICRONOVA_ID,
CONF_MEMORY_LOCATION,
CONF_MEMORY_ADDRESS,
MICRONOVA_LISTENER_SCHEMA,
micronova_ns,
)
CONF_STOVE_STATE = "stove_state"
MicroNovaTextSensor = micronova_ns.class_(
"MicroNovaTextSensor", text_sensor.TextSensor, cg.Component
)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_MICRONOVA_ID): cv.use_id(MicroNova),
cv.Optional(CONF_STOVE_STATE): text_sensor.text_sensor_schema(
MicroNovaTextSensor
).extend(
MICRONOVA_LISTENER_SCHEMA(
default_memory_location=0x00, default_memory_address=0x21
)
),
}
)
async def to_code(config):
mv = await cg.get_variable(config[CONF_MICRONOVA_ID])
if stove_state_config := config.get(CONF_STOVE_STATE):
sens = await text_sensor.new_text_sensor(stove_state_config, mv)
cg.add(mv.register_micronova_listener(sens))
cg.add(sens.set_memory_location(stove_state_config[CONF_MEMORY_LOCATION]))
cg.add(sens.set_memory_address(stove_state_config[CONF_MEMORY_ADDRESS]))
cg.add(sens.set_function(MicroNovaFunctions.STOVE_FUNCTION_STOVE_STATE))

View file

@ -0,0 +1,31 @@
#include "micronova_text_sensor.h"
namespace esphome {
namespace micronova {
void MicroNovaTextSensor::process_value_from_stove(int value_from_stove) {
if (value_from_stove == -1) {
this->publish_state("unknown");
return;
}
switch (this->get_function()) {
case MicroNovaFunctions::STOVE_FUNCTION_STOVE_STATE:
this->micronova_->set_current_stove_state(value_from_stove);
this->publish_state(STOVE_STATES[value_from_stove]);
// set the stove switch to on for any value but 0
if (value_from_stove != 0 && this->micronova_->get_stove_switch() != nullptr &&
!this->micronova_->get_stove_switch()->get_stove_state()) {
this->micronova_->get_stove_switch()->set_stove_state(true);
} else if (value_from_stove == 0 && this->micronova_->get_stove_switch() != nullptr &&
this->micronova_->get_stove_switch()->get_stove_state()) {
this->micronova_->get_stove_switch()->set_stove_state(false);
}
break;
default:
break;
}
}
} // namespace micronova
} // namespace esphome

View file

@ -0,0 +1,20 @@
#pragma once
#include "esphome/components/micronova/micronova.h"
#include "esphome/components/text_sensor/text_sensor.h"
namespace esphome {
namespace micronova {
class MicroNovaTextSensor : public text_sensor::TextSensor, public MicroNovaSensorListener {
public:
MicroNovaTextSensor(MicroNova *m) : MicroNovaSensorListener(m) {}
void dump_config() override { LOG_TEXT_SENSOR("", "Micronova text sensor", this); }
void request_value_from_stove() override {
this->micronova_->request_address(this->memory_location_, this->memory_address_, this);
}
void process_value_from_stove(int value_from_stove) override;
};
} // namespace micronova
} // namespace esphome

View file

@ -69,7 +69,7 @@ bool MopekaProCheck::parse_device(const esp32_ble_tracker::ESPBTDevice &device)
if ((this->distance_ != nullptr) || (this->level_ != nullptr)) { if ((this->distance_ != nullptr) || (this->level_ != nullptr)) {
uint32_t distance_value = this->parse_distance_(manu_data.data); uint32_t distance_value = this->parse_distance_(manu_data.data);
SensorReadQuality quality_value = this->parse_read_quality_(manu_data.data); SensorReadQuality quality_value = this->parse_read_quality_(manu_data.data);
ESP_LOGD(TAG, "Distance Sensor: Quality (0x%X) Distance (%dmm)", quality_value, distance_value); ESP_LOGD(TAG, "Distance Sensor: Quality (0x%X) Distance (%" PRId32 "mm)", quality_value, distance_value);
if (quality_value < QUALITY_HIGH) { if (quality_value < QUALITY_HIGH) {
ESP_LOGW(TAG, "Poor read quality."); ESP_LOGW(TAG, "Poor read quality.");
} }

View file

@ -1,11 +1,12 @@
#pragma once #pragma once
#include <cinttypes>
#include <vector>
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h" #include "esphome/components/sensor/sensor.h"
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
#include <vector>
#ifdef USE_ESP32 #ifdef USE_ESP32
namespace esphome { namespace esphome {

View file

@ -16,8 +16,8 @@ static const uint16_t MANUFACTURER_ID = 0x000D;
void MopekaStdCheck::dump_config() { void MopekaStdCheck::dump_config() {
ESP_LOGCONFIG(TAG, "Mopeka Std Check"); ESP_LOGCONFIG(TAG, "Mopeka Std Check");
ESP_LOGCONFIG(TAG, " Propane Butane mix: %.0f%%", this->propane_butane_mix_ * 100); ESP_LOGCONFIG(TAG, " Propane Butane mix: %.0f%%", this->propane_butane_mix_ * 100);
ESP_LOGCONFIG(TAG, " Tank distance empty: %imm", this->empty_mm_); ESP_LOGCONFIG(TAG, " Tank distance empty: %" PRIi32 "mm", this->empty_mm_);
ESP_LOGCONFIG(TAG, " Tank distance full: %imm", this->full_mm_); ESP_LOGCONFIG(TAG, " Tank distance full: %" PRIi32 "mm", this->full_mm_);
LOG_SENSOR(" ", "Level", this->level_); LOG_SENSOR(" ", "Level", this->level_);
LOG_SENSOR(" ", "Temperature", this->temperature_); LOG_SENSOR(" ", "Temperature", this->temperature_);
LOG_SENSOR(" ", "Battery Level", this->battery_level_); LOG_SENSOR(" ", "Battery Level", this->battery_level_);

View file

@ -1,5 +1,6 @@
#pragma once #pragma once
#include <cinttypes>
#include <vector> #include <vector>
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"

View file

@ -19,7 +19,7 @@ class MQTTBackendESP8266 final : public MQTTBackend {
void set_will(const char *topic, uint8_t qos, bool retain, const char *payload) final { void set_will(const char *topic, uint8_t qos, bool retain, const char *payload) final {
mqtt_client_.setWill(topic, qos, retain, payload); mqtt_client_.setWill(topic, qos, retain, payload);
} }
void set_server(network::IPAddress ip, uint16_t port) final { mqtt_client_.setServer(IPAddress(ip), port); } void set_server(network::IPAddress ip, uint16_t port) final { mqtt_client_.setServer(ip, port); }
void set_server(const char *host, uint16_t port) final { mqtt_client_.setServer(host, port); } void set_server(const char *host, uint16_t port) final { mqtt_client_.setServer(host, port); }
#if ASYNC_TCP_SSL_ENABLED #if ASYNC_TCP_SSL_ENABLED
void set_secure(bool secure) { mqtt_client.setSecure(secure); } void set_secure(bool secure) { mqtt_client.setSecure(secure); }

View file

@ -136,6 +136,7 @@ bool MQTTComponent::send_discovery_() {
if (node_friendly_name.empty()) { if (node_friendly_name.empty()) {
node_friendly_name = node_name; node_friendly_name = node_name;
} }
const std::string &node_area = App.get_area();
JsonObject device_info = root.createNestedObject(MQTT_DEVICE); JsonObject device_info = root.createNestedObject(MQTT_DEVICE);
device_info[MQTT_DEVICE_IDENTIFIERS] = get_mac_address(); device_info[MQTT_DEVICE_IDENTIFIERS] = get_mac_address();
@ -143,6 +144,7 @@ bool MQTTComponent::send_discovery_() {
device_info[MQTT_DEVICE_SW_VERSION] = "esphome v" ESPHOME_VERSION " " + App.get_compilation_time(); device_info[MQTT_DEVICE_SW_VERSION] = "esphome v" ESPHOME_VERSION " " + App.get_compilation_time();
device_info[MQTT_DEVICE_MODEL] = ESPHOME_BOARD; device_info[MQTT_DEVICE_MODEL] = ESPHOME_BOARD;
device_info[MQTT_DEVICE_MANUFACTURER] = "espressif"; device_info[MQTT_DEVICE_MANUFACTURER] = "espressif";
device_info[MQTT_DEVICE_SUGGESTED_AREA] = node_area;
}, },
0, discovery_info.retain); 0, discovery_info.retain);
} }

View file

@ -3,7 +3,11 @@
#include <string> #include <string>
#include <cstdio> #include <cstdio>
#include <array> #include <array>
#include "esphome/core/macros.h"
#if defined(USE_ESP_IDF) || defined(USE_LIBRETINY) || USE_ARDUINO_VERSION_CODE > VERSION_CODE(3, 0, 0)
#include <lwip/ip_addr.h> #include <lwip/ip_addr.h>
#endif
#if USE_ARDUINO #if USE_ARDUINO
#include <Arduino.h> #include <Arduino.h>

View file

@ -116,6 +116,7 @@ void Nextion::reset_(bool reset_nextion) {
this->read_byte(&d); this->read_byte(&d);
}; };
this->nextion_queue_.clear(); this->nextion_queue_.clear();
this->waveform_queue_.clear();
} }
void Nextion::dump_config() { void Nextion::dump_config() {
@ -364,37 +365,21 @@ void Nextion::process_nextion_commands_() {
ESP_LOGW(TAG, "Nextion reported baud rate invalid!"); ESP_LOGW(TAG, "Nextion reported baud rate invalid!");
break; break;
case 0x12: // invalid Waveform ID or Channel # was used case 0x12: // invalid Waveform ID or Channel # was used
if (this->waveform_queue_.empty()) {
if (!this->nextion_queue_.empty()) { ESP_LOGW(TAG,
int index = 0; "Nextion reported invalid Waveform ID or Channel # was used but no waveform sensor in queue found!");
int found = -1; } else {
for (auto &nb : this->nextion_queue_) { auto &nb = this->waveform_queue_.front();
NextionComponentBase *component = nb->component; NextionComponentBase *component = nb->component;
if (component->get_queue_type() == NextionQueueType::WAVEFORM_SENSOR) {
ESP_LOGW(TAG, "Nextion reported invalid Waveform ID %d or Channel # %d was used!", ESP_LOGW(TAG, "Nextion reported invalid Waveform ID %d or Channel # %d was used!",
component->get_component_id(), component->get_wave_channel_id()); component->get_component_id(), component->get_wave_channel_id());
ESP_LOGN(TAG, "Removing waveform from queue with component id %d and waveform id %d", ESP_LOGN(TAG, "Removing waveform from queue with component id %d and waveform id %d",
component->get_component_id(), component->get_wave_channel_id()); component->get_component_id(), component->get_wave_channel_id());
found = index;
delete component; // NOLINT(cppcoreguidelines-owning-memory)
delete nb; // NOLINT(cppcoreguidelines-owning-memory) delete nb; // NOLINT(cppcoreguidelines-owning-memory)
this->waveform_queue_.pop_front();
break;
}
++index;
}
if (found != -1) {
this->nextion_queue_.erase(this->nextion_queue_.begin() + found);
} else {
ESP_LOGW(
TAG,
"Nextion reported invalid Waveform ID or Channel # was used but no waveform sensor in queue found!");
}
} }
break; break;
case 0x1A: // variable name invalid case 0x1A: // variable name invalid
@ -697,17 +682,19 @@ void Nextion::process_nextion_commands_() {
} }
case 0xFD: { // data transparent transmit finished case 0xFD: { // data transparent transmit finished
ESP_LOGVV(TAG, "Nextion reported data transmit finished!"); ESP_LOGVV(TAG, "Nextion reported data transmit finished!");
this->check_pending_waveform_();
break; break;
} }
case 0xFE: { // data transparent transmit ready case 0xFE: { // data transparent transmit ready
ESP_LOGVV(TAG, "Nextion reported ready for transmit!"); ESP_LOGVV(TAG, "Nextion reported ready for transmit!");
if (this->waveform_queue_.empty()) {
ESP_LOGE(TAG, "No waveforms in queue to send data!");
break;
}
int index = 0; auto &nb = this->waveform_queue_.front();
int found = -1;
for (auto &nb : this->nextion_queue_) {
auto *component = nb->component; auto *component = nb->component;
if (component->get_queue_type() == NextionQueueType::WAVEFORM_SENSOR) { size_t buffer_to_send = component->get_wave_buffer_size() < 255 ? component->get_wave_buffer_size()
size_t buffer_to_send = component->get_wave_buffer().size() < 255 ? component->get_wave_buffer().size()
: 255; // ADDT command can only send 255 : 255; // ADDT command can only send 255
this->write_array(component->get_wave_buffer().data(), static_cast<int>(buffer_to_send)); this->write_array(component->get_wave_buffer().data(), static_cast<int>(buffer_to_send));
@ -715,26 +702,9 @@ void Nextion::process_nextion_commands_() {
ESP_LOGN(TAG, "Nextion sending waveform data for component id %d and waveform id %d, size %zu", ESP_LOGN(TAG, "Nextion sending waveform data for component id %d and waveform id %d, size %zu",
component->get_component_id(), component->get_wave_channel_id(), buffer_to_send); component->get_component_id(), component->get_wave_channel_id(), buffer_to_send);
if (component->get_wave_buffer().size() <= 255) { component->clear_wave_buffer(buffer_to_send);
component->get_wave_buffer().clear();
} else {
component->get_wave_buffer().erase(component->get_wave_buffer().begin(),
component->get_wave_buffer().begin() + buffer_to_send);
}
found = index;
delete component; // NOLINT(cppcoreguidelines-owning-memory)
delete nb; // NOLINT(cppcoreguidelines-owning-memory) delete nb; // NOLINT(cppcoreguidelines-owning-memory)
break; this->waveform_queue_.pop_front();
}
++index;
}
if (found == -1) {
ESP_LOGE(TAG, "No waveforms in queue to send data!");
break;
} else {
this->nextion_queue_.erase(this->nextion_queue_.begin() + found);
}
break; break;
} }
default: default:
@ -1093,17 +1063,28 @@ void Nextion::add_addt_command_to_queue(NextionComponentBase *component) {
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory) // NOLINTNEXTLINE(cppcoreguidelines-owning-memory)
nextion::NextionQueue *nextion_queue = new nextion::NextionQueue; nextion::NextionQueue *nextion_queue = new nextion::NextionQueue;
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory) nextion_queue->component = component;
nextion_queue->component = new nextion::NextionComponentBase;
nextion_queue->queue_time = millis(); nextion_queue->queue_time = millis();
this->waveform_queue_.push_back(nextion_queue);
if (this->waveform_queue_.size() == 1)
this->check_pending_waveform_();
}
void Nextion::check_pending_waveform_() {
if (this->waveform_queue_.empty())
return;
auto *nb = this->waveform_queue_.front();
auto *component = nb->component;
size_t buffer_to_send = component->get_wave_buffer_size() < 255 ? component->get_wave_buffer_size() size_t buffer_to_send = component->get_wave_buffer_size() < 255 ? component->get_wave_buffer_size()
: 255; // ADDT command can only send 255 : 255; // ADDT command can only send 255
std::string command = "addt " + to_string(component->get_component_id()) + "," + std::string command = "addt " + to_string(component->get_component_id()) + "," +
to_string(component->get_wave_channel_id()) + "," + to_string(buffer_to_send); to_string(component->get_wave_channel_id()) + "," + to_string(buffer_to_send);
if (this->send_command_(command)) { if (!this->send_command_(command)) {
this->nextion_queue_.push_back(nextion_queue); delete nb; // NOLINT(cppcoreguidelines-owning-memory)
this->waveform_queue_.pop_front();
} }
} }

View file

@ -740,6 +740,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
protected: protected:
std::deque<NextionQueue *> nextion_queue_; std::deque<NextionQueue *> nextion_queue_;
std::deque<NextionQueue *> waveform_queue_;
uint16_t recv_ret_string_(std::string &response, uint32_t timeout, bool recv_flag); uint16_t recv_ret_string_(std::string &response, uint32_t timeout, bool recv_flag);
void all_components_send_state_(bool force_update = false); void all_components_send_state_(bool force_update = false);
uint64_t comok_sent_ = 0; uint64_t comok_sent_ = 0;
@ -780,6 +781,8 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
const std::string &variable_name_to_send, const std::string &variable_name_to_send,
const std::string &state_value, bool is_sleep_safe = false); const std::string &state_value, bool is_sleep_safe = false);
void check_pending_waveform_();
#ifdef USE_NEXTION_TFT_UPLOAD #ifdef USE_NEXTION_TFT_UPLOAD
#ifdef USE_ESP8266 #ifdef USE_ESP8266
WiFiClient *wifi_client_{nullptr}; WiFiClient *wifi_client_{nullptr};

View file

@ -69,6 +69,13 @@ class NextionComponentBase {
std::vector<uint8_t> get_wave_buffer() { return this->wave_buffer_; } std::vector<uint8_t> get_wave_buffer() { return this->wave_buffer_; }
size_t get_wave_buffer_size() { return this->wave_buffer_.size(); } size_t get_wave_buffer_size() { return this->wave_buffer_.size(); }
void clear_wave_buffer(size_t buffer_sent) {
if (this->wave_buffer_.size() <= buffer_sent) {
this->wave_buffer_.clear();
} else {
this->wave_buffer_.erase(this->wave_buffer_.begin(), this->wave_buffer_.begin() + buffer_sent);
}
}
std::string get_variable_name() { return this->variable_name_; } std::string get_variable_name() { return this->variable_name_; }
std::string get_variable_name_to_send() { return this->variable_name_to_send_; } std::string get_variable_name_to_send() { return this->variable_name_to_send_; }

View file

@ -135,7 +135,7 @@ def _process_base_package(config: dict) -> dict:
packages[file] = new_yaml packages[file] = new_yaml
except EsphomeError as e: except EsphomeError as e:
raise cv.Invalid( raise cv.Invalid(
f"{file} is not a valid YAML file. Please check the file contents." f"{file} is not a valid YAML file. Please check the file contents.\n{e}"
) from e ) from e
return packages return packages

View file

@ -64,7 +64,7 @@ PCA6416A_PIN_SCHEMA = cv.All(
) )
@pins.PIN_SCHEMA_REGISTRY.register("pca6416a", PCA6416A_PIN_SCHEMA) @pins.PIN_SCHEMA_REGISTRY.register(CONF_PCA6416A, PCA6416A_PIN_SCHEMA)
async def pca6416a_pin_to_code(config): async def pca6416a_pin_to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
parent = await cg.get_variable(config[CONF_PCA6416A]) parent = await cg.get_variable(config[CONF_PCA6416A])

View file

@ -11,9 +11,10 @@ from esphome.const import (
CONF_OUTPUT, CONF_OUTPUT,
) )
CODEOWNERS = ["@hwstar"] CODEOWNERS = ["@hwstar", "@clydebarrow"]
DEPENDENCIES = ["i2c"] DEPENDENCIES = ["i2c"]
MULTI_CONF = True MULTI_CONF = True
CONF_PIN_COUNT = "pin_count"
pca9554_ns = cg.esphome_ns.namespace("pca9554") pca9554_ns = cg.esphome_ns.namespace("pca9554")
PCA9554Component = pca9554_ns.class_("PCA9554Component", cg.Component, i2c.I2CDevice) PCA9554Component = pca9554_ns.class_("PCA9554Component", cg.Component, i2c.I2CDevice)
@ -23,7 +24,12 @@ PCA9554GPIOPin = pca9554_ns.class_(
CONF_PCA9554 = "pca9554" CONF_PCA9554 = "pca9554"
CONFIG_SCHEMA = ( CONFIG_SCHEMA = (
cv.Schema({cv.Required(CONF_ID): cv.declare_id(PCA9554Component)}) cv.Schema(
{
cv.Required(CONF_ID): cv.declare_id(PCA9554Component),
cv.Optional(CONF_PIN_COUNT, default=8): cv.one_of(4, 8, 16),
}
)
.extend(cv.COMPONENT_SCHEMA) .extend(cv.COMPONENT_SCHEMA)
.extend( .extend(
i2c.i2c_device_schema(0x20) i2c.i2c_device_schema(0x20)
@ -33,6 +39,7 @@ CONFIG_SCHEMA = (
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
cg.add(var.set_pin_count(config[CONF_PIN_COUNT]))
await cg.register_component(var, config) await cg.register_component(var, config)
await i2c.register_i2c_device(var, config) await i2c.register_i2c_device(var, config)
@ -49,7 +56,7 @@ PCA9554_PIN_SCHEMA = cv.All(
{ {
cv.GenerateID(): cv.declare_id(PCA9554GPIOPin), cv.GenerateID(): cv.declare_id(PCA9554GPIOPin),
cv.Required(CONF_PCA9554): cv.use_id(PCA9554Component), cv.Required(CONF_PCA9554): cv.use_id(PCA9554Component),
cv.Required(CONF_NUMBER): cv.int_range(min=0, max=8), cv.Required(CONF_NUMBER): cv.int_range(min=0, max=15),
cv.Optional(CONF_MODE, default={}): cv.All( cv.Optional(CONF_MODE, default={}): cv.All(
{ {
cv.Optional(CONF_INPUT, default=False): cv.boolean, cv.Optional(CONF_INPUT, default=False): cv.boolean,
@ -58,11 +65,19 @@ PCA9554_PIN_SCHEMA = cv.All(
validate_mode, validate_mode,
), ),
cv.Optional(CONF_INVERTED, default=False): cv.boolean, cv.Optional(CONF_INVERTED, default=False): cv.boolean,
} },
) )
@pins.PIN_SCHEMA_REGISTRY.register("pca9554", PCA9554_PIN_SCHEMA) def pca9554_pin_final_validate(pin_config, parent_config):
count = parent_config[CONF_PIN_COUNT]
if pin_config[CONF_NUMBER] >= count:
raise cv.Invalid(f"Pin number must be in range 0-{count - 1}")
@pins.PIN_SCHEMA_REGISTRY.register(
CONF_PCA9554, PCA9554_PIN_SCHEMA, pca9554_pin_final_validate
)
async def pca9554_pin_to_code(config): async def pca9554_pin_to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
parent = await cg.get_variable(config[CONF_PCA9554]) parent = await cg.get_variable(config[CONF_PCA9554])

View file

@ -4,6 +4,7 @@
namespace esphome { namespace esphome {
namespace pca9554 { namespace pca9554 {
// for 16 bit expanders, these addresses will be doubled.
const uint8_t INPUT_REG = 0; const uint8_t INPUT_REG = 0;
const uint8_t OUTPUT_REG = 1; const uint8_t OUTPUT_REG = 1;
const uint8_t INVERT_REG = 2; const uint8_t INVERT_REG = 2;
@ -13,9 +14,10 @@ static const char *const TAG = "pca9554";
void PCA9554Component::setup() { void PCA9554Component::setup() {
ESP_LOGCONFIG(TAG, "Setting up PCA9554/PCA9554A..."); ESP_LOGCONFIG(TAG, "Setting up PCA9554/PCA9554A...");
this->reg_width_ = (this->pin_count_ + 7) / 8;
// Test to see if device exists // Test to see if device exists
if (!this->read_inputs_()) { if (!this->read_inputs_()) {
ESP_LOGE(TAG, "PCA9554 not available under 0x%02X", this->address_); ESP_LOGE(TAG, "PCA95xx not detected at 0x%02X", this->address_);
this->mark_failed(); this->mark_failed();
return; return;
} }
@ -44,6 +46,7 @@ void PCA9554Component::loop() {
void PCA9554Component::dump_config() { void PCA9554Component::dump_config() {
ESP_LOGCONFIG(TAG, "PCA9554:"); ESP_LOGCONFIG(TAG, "PCA9554:");
ESP_LOGCONFIG(TAG, " I/O Pins: %d", this->pin_count_);
LOG_I2C_DEVICE(this) LOG_I2C_DEVICE(this)
if (this->is_failed()) { if (this->is_failed()) {
ESP_LOGE(TAG, "Communication with PCA9554 failed!"); ESP_LOGE(TAG, "Communication with PCA9554 failed!");
@ -85,25 +88,33 @@ void PCA9554Component::pin_mode(uint8_t pin, gpio::Flags flags) {
} }
bool PCA9554Component::read_inputs_() { bool PCA9554Component::read_inputs_() {
uint8_t inputs; uint8_t inputs[2];
if (this->is_failed()) { if (this->is_failed()) {
ESP_LOGD(TAG, "Device marked failed"); ESP_LOGD(TAG, "Device marked failed");
return false; return false;
} }
if ((this->last_error_ = this->read_register(INPUT_REG, &inputs, 1, true)) != esphome::i2c::ERROR_OK) { if ((this->last_error_ = this->read_register(INPUT_REG * this->reg_width_, inputs, this->reg_width_, true)) !=
esphome::i2c::ERROR_OK) {
this->status_set_warning(); this->status_set_warning();
ESP_LOGE(TAG, "read_register_(): I2C I/O error: %d", (int) this->last_error_); ESP_LOGE(TAG, "read_register_(): I2C I/O error: %d", (int) this->last_error_);
return false; return false;
} }
this->status_clear_warning(); this->status_clear_warning();
this->input_mask_ = inputs; this->input_mask_ = inputs[0];
if (this->reg_width_ == 2) {
this->input_mask_ |= inputs[1] << 8;
}
return true; return true;
} }
bool PCA9554Component::write_register_(uint8_t reg, uint8_t value) { bool PCA9554Component::write_register_(uint8_t reg, uint16_t value) {
if ((this->last_error_ = this->write_register(reg, &value, 1, true)) != esphome::i2c::ERROR_OK) { uint8_t outputs[2];
outputs[0] = (uint8_t) value;
outputs[1] = (uint8_t) (value >> 8);
if ((this->last_error_ = this->write_register(reg * this->reg_width_, outputs, this->reg_width_, true)) !=
esphome::i2c::ERROR_OK) {
this->status_set_warning(); this->status_set_warning();
ESP_LOGE(TAG, "write_register_(): I2C I/O error: %d", (int) this->last_error_); ESP_LOGE(TAG, "write_register_(): I2C I/O error: %d", (int) this->last_error_);
return false; return false;

View file

@ -28,19 +28,25 @@ class PCA9554Component : public Component, public i2c::I2CDevice {
void dump_config() override; void dump_config() override;
void set_pin_count(size_t pin_count) { this->pin_count_ = pin_count; }
protected: protected:
bool read_inputs_(); bool read_inputs_();
bool write_register_(uint8_t reg, uint8_t value); bool write_register_(uint8_t reg, uint16_t value);
/// number of bits the expander has
size_t pin_count_{8};
/// width of registers
size_t reg_width_{1};
/// Mask for the pin config - 1 means OUTPUT, 0 means INPUT /// Mask for the pin config - 1 means OUTPUT, 0 means INPUT
uint8_t config_mask_{0x00}; uint16_t config_mask_{0x00};
/// The mask to write as output state - 1 means HIGH, 0 means LOW /// The mask to write as output state - 1 means HIGH, 0 means LOW
uint8_t output_mask_{0x00}; uint16_t output_mask_{0x00};
/// The state of the actual input pin states - 1 means HIGH, 0 means LOW /// The state of the actual input pin states - 1 means HIGH, 0 means LOW
uint8_t input_mask_{0x00}; uint16_t input_mask_{0x00};
/// Flags to check if read previously during this loop /// Flags to check if read previously during this loop
uint8_t was_previously_read_ = {0x00}; uint16_t was_previously_read_ = {0x00};
/// Storage for last I2C error seen /// Storage for last I2C error seen
esphome::i2c::ErrorCode last_error_; esphome::i2c::ErrorCode last_error_;
}; };

View file

@ -65,7 +65,7 @@ PCF8574_PIN_SCHEMA = cv.All(
) )
@pins.PIN_SCHEMA_REGISTRY.register("pcf8574", PCF8574_PIN_SCHEMA) @pins.PIN_SCHEMA_REGISTRY.register(CONF_PCF8574, PCF8574_PIN_SCHEMA)
async def pcf8574_pin_to_code(config): async def pcf8574_pin_to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
parent = await cg.get_variable(config[CONF_PCF8574]) parent = await cg.get_variable(config[CONF_PCF8574])

View file

@ -4,7 +4,15 @@ import esphome.config_validation as cv
import esphome.final_validate as fv import esphome.final_validate as fv
from esphome import automation from esphome import automation
from esphome.components.output import FloatOutput from esphome.components.output import FloatOutput
from esphome.const import CONF_ID, CONF_OUTPUT, CONF_PLATFORM, CONF_TRIGGER_ID from esphome.components.speaker import Speaker
from esphome.const import (
CONF_ID,
CONF_OUTPUT,
CONF_PLATFORM,
CONF_TRIGGER_ID,
CONF_SPEAKER,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -24,17 +32,23 @@ IsPlayingCondition = rtttl_ns.class_("IsPlayingCondition", automation.Condition)
MULTI_CONF = True MULTI_CONF = True
CONFIG_SCHEMA = cv.Schema( CONFIG_SCHEMA = cv.All(
cv.Schema(
{ {
cv.GenerateID(CONF_ID): cv.declare_id(Rtttl), cv.GenerateID(CONF_ID): cv.declare_id(Rtttl),
cv.Required(CONF_OUTPUT): cv.use_id(FloatOutput), cv.Optional(CONF_OUTPUT): cv.use_id(FloatOutput),
cv.Optional(CONF_SPEAKER): cv.use_id(Speaker),
cv.Optional(CONF_ON_FINISHED_PLAYBACK): automation.validate_automation( cv.Optional(CONF_ON_FINISHED_PLAYBACK): automation.validate_automation(
{ {
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FinishedPlaybackTrigger), cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
FinishedPlaybackTrigger
),
} }
), ),
} }
).extend(cv.COMPONENT_SCHEMA) ).extend(cv.COMPONENT_SCHEMA),
cv.has_exactly_one_key(CONF_OUTPUT, CONF_SPEAKER),
)
def validate_parent_output_config(value): def validate_parent_output_config(value):
@ -63,9 +77,9 @@ def validate_parent_output_config(value):
FINAL_VALIDATE_SCHEMA = cv.Schema( FINAL_VALIDATE_SCHEMA = cv.Schema(
{ {
cv.Required(CONF_OUTPUT): fv.id_declaration_match_schema( cv.Optional(CONF_OUTPUT): fv.id_declaration_match_schema(
validate_parent_output_config validate_parent_output_config
) ),
}, },
extra=cv.ALLOW_EXTRA, extra=cv.ALLOW_EXTRA,
) )
@ -75,8 +89,14 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
if CONF_OUTPUT in config:
out = await cg.get_variable(config[CONF_OUTPUT]) out = await cg.get_variable(config[CONF_OUTPUT])
cg.add(var.set_output(out)) cg.add(var.set_output(out))
cg.add_define("USE_OUTPUT")
if CONF_SPEAKER in config:
out = await cg.get_variable(config[CONF_SPEAKER])
cg.add(var.set_speaker(out))
for conf in config.get(CONF_ON_FINISHED_PLAYBACK, []): for conf in config.get(CONF_ON_FINISHED_PLAYBACK, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)

View file

@ -1,4 +1,5 @@
#include "rtttl.h" #include "rtttl.h"
#include <cmath>
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
@ -15,104 +16,185 @@ static const uint16_t NOTES[] = {0, 262, 277, 294, 311, 330, 349, 370,
1109, 1175, 1245, 1319, 1397, 1480, 1568, 1661, 1760, 1865, 1976, 2093, 2217, 1109, 1175, 1245, 1319, 1397, 1480, 1568, 1661, 1760, 1865, 1976, 2093, 2217,
2349, 2489, 2637, 2794, 2960, 3136, 3322, 3520, 3729, 3951}; 2349, 2489, 2637, 2794, 2960, 3136, 3322, 3520, 3729, 3951};
static const uint16_t I2S_SPEED = 1600;
#undef HALF_PI
static const double HALF_PI = 1.5707963267948966192313216916398;
inline double deg2rad(double degrees) {
static const double PI_ON_180 = 4.0 * atan(1.0) / 180.0;
return degrees * PI_ON_180;
}
void Rtttl::dump_config() { ESP_LOGCONFIG(TAG, "Rtttl"); } void Rtttl::dump_config() { ESP_LOGCONFIG(TAG, "Rtttl"); }
void Rtttl::play(std::string rtttl) { void Rtttl::play(std::string rtttl) {
rtttl_ = std::move(rtttl); this->rtttl_ = std::move(rtttl);
this->default_duration_ = 4;
this->default_octave_ = 6;
this->note_duration_ = 0;
default_duration_ = 4;
default_octave_ = 6;
int bpm = 63; int bpm = 63;
uint8_t num; uint8_t num;
// Get name // Get name
position_ = rtttl_.find(':'); this->position_ = rtttl_.find(':');
// it's somewhat documented to be up to 10 characters but let's be a bit flexible here // it's somewhat documented to be up to 10 characters but let's be a bit flexible here
if (position_ == std::string::npos || position_ > 15) { if (this->position_ == std::string::npos || this->position_ > 15) {
ESP_LOGE(TAG, "Missing ':' when looking for name."); ESP_LOGE(TAG, "Missing ':' when looking for name.");
return; return;
} }
auto name = this->rtttl_.substr(0, position_); auto name = this->rtttl_.substr(0, this->position_);
ESP_LOGD(TAG, "Playing song %s", name.c_str()); ESP_LOGD(TAG, "Playing song %s", name.c_str());
// get default duration // get default duration
position_ = this->rtttl_.find("d=", position_); this->position_ = this->rtttl_.find("d=", this->position_);
if (position_ == std::string::npos) { if (this->position_ == std::string::npos) {
ESP_LOGE(TAG, "Missing 'd='"); ESP_LOGE(TAG, "Missing 'd='");
return; return;
} }
position_ += 2; this->position_ += 2;
num = this->get_integer_(); num = this->get_integer_();
if (num > 0) if (num > 0)
default_duration_ = num; this->default_duration_ = num;
// get default octave // get default octave
position_ = rtttl_.find("o=", position_); this->position_ = this->rtttl_.find("o=", this->position_);
if (position_ == std::string::npos) { if (this->position_ == std::string::npos) {
ESP_LOGE(TAG, "Missing 'o="); ESP_LOGE(TAG, "Missing 'o=");
return; return;
} }
position_ += 2; this->position_ += 2;
num = get_integer_(); num = get_integer_();
if (num >= 3 && num <= 7) if (num >= 3 && num <= 7)
default_octave_ = num; this->default_octave_ = num;
// get BPM // get BPM
position_ = rtttl_.find("b=", position_); this->position_ = this->rtttl_.find("b=", this->position_);
if (position_ == std::string::npos) { if (this->position_ == std::string::npos) {
ESP_LOGE(TAG, "Missing b="); ESP_LOGE(TAG, "Missing b=");
return; return;
} }
position_ += 2; this->position_ += 2;
num = get_integer_(); num = get_integer_();
if (num != 0) if (num != 0)
bpm = num; bpm = num;
position_ = rtttl_.find(':', position_); this->position_ = this->rtttl_.find(':', this->position_);
if (position_ == std::string::npos) { if (this->position_ == std::string::npos) {
ESP_LOGE(TAG, "Missing second ':'"); ESP_LOGE(TAG, "Missing second ':'");
return; return;
} }
position_++; this->position_++;
// BPM usually expresses the number of quarter notes per minute // BPM usually expresses the number of quarter notes per minute
wholenote_ = 60 * 1000L * 4 / bpm; // this is the time for whole note (in milliseconds) this->wholenote_ = 60 * 1000L * 4 / bpm; // this is the time for whole note (in milliseconds)
output_freq_ = 0; this->output_freq_ = 0;
last_note_ = millis(); this->last_note_ = millis();
note_duration_ = 1; this->note_duration_ = 1;
#ifdef USE_SPEAKER
this->samples_sent_ = 0;
this->samples_count_ = 0;
#endif
}
void Rtttl::stop() {
this->note_duration_ = 0;
#ifdef USE_OUTPUT
if (this->output_ != nullptr) {
this->output_->set_level(0.0);
}
#endif
#ifdef USE_SPEAKER
if (this->speaker_ != nullptr) {
if (this->speaker_->is_running()) {
this->speaker_->stop();
}
}
#endif
} }
void Rtttl::loop() { void Rtttl::loop() {
if (note_duration_ == 0 || millis() - last_note_ < note_duration_) if (this->note_duration_ == 0)
return; return;
if (!rtttl_[position_]) { #ifdef USE_SPEAKER
output_->set_level(0.0); if (this->speaker_ != nullptr) {
if (this->samples_sent_ != this->samples_count_) {
SpeakerSample sample[SAMPLE_BUFFER_SIZE + 1];
int x = 0;
double rem = 0.0;
while (true) {
// Try and send out the remainder of the existing note, one per loop()
if (this->samples_per_wave_ != 0 && this->samples_sent_ >= this->samples_gap_) { // Play note//
rem = ((this->samples_sent_ << 10) % this->samples_per_wave_) * (360.0 / this->samples_per_wave_);
int16_t val = 8192 * sin(deg2rad(rem));
sample[x].left = val;
sample[x].right = val;
} else {
sample[x].left = 0;
sample[x].right = 0;
}
if (x >= SAMPLE_BUFFER_SIZE || this->samples_sent_ >= this->samples_count_) {
break;
}
this->samples_sent_++;
x++;
}
if (x > 0) {
int send = this->speaker_->play((uint8_t *) (&sample), x * 4);
if (send != x * 4) {
this->samples_sent_ -= (x - (send / 4));
}
return;
}
}
}
#endif
#ifdef USE_OUTPUT
if (this->output_ != nullptr && millis() - this->last_note_ < this->note_duration_)
return;
#endif
if (!this->rtttl_[position_]) {
this->note_duration_ = 0;
#ifdef USE_OUTPUT
if (this->output_ != nullptr) {
this->output_->set_level(0.0);
}
#endif
ESP_LOGD(TAG, "Playback finished"); ESP_LOGD(TAG, "Playback finished");
this->on_finished_playback_callback_.call(); this->on_finished_playback_callback_.call();
note_duration_ = 0;
return; return;
} }
// align to note: most rtttl's out there does not add and space after the ',' separator but just in case... // align to note: most rtttl's out there does not add and space after the ',' separator but just in case...
while (rtttl_[position_] == ',' || rtttl_[position_] == ' ') while (this->rtttl_[this->position_] == ',' || this->rtttl_[this->position_] == ' ')
position_++; this->position_++;
// first, get note duration, if available // first, get note duration, if available
uint8_t num = this->get_integer_(); uint8_t num = this->get_integer_();
if (num) { if (num) {
note_duration_ = wholenote_ / num; this->note_duration_ = this->wholenote_ / num;
} else { } else {
note_duration_ = wholenote_ / default_duration_; // we will need to check if we are a dotted note after this->note_duration_ =
this->wholenote_ / this->default_duration_; // we will need to check if we are a dotted note after
} }
uint8_t note; uint8_t note;
switch (rtttl_[position_]) { switch (this->rtttl_[this->position_]) {
case 'c': case 'c':
note = 1; note = 1;
break; break;
@ -138,51 +220,81 @@ void Rtttl::loop() {
default: default:
note = 0; note = 0;
} }
position_++; this->position_++;
// now, get optional '#' sharp // now, get optional '#' sharp
if (rtttl_[position_] == '#') { if (this->rtttl_[this->position_] == '#') {
note++; note++;
position_++; this->position_++;
} }
// now, get optional '.' dotted note // now, get optional '.' dotted note
if (rtttl_[position_] == '.') { if (this->rtttl_[this->position_] == '.') {
note_duration_ += note_duration_ / 2; this->note_duration_ += this->note_duration_ / 2;
position_++; this->position_++;
} }
// now, get scale // now, get scale
uint8_t scale = get_integer_(); uint8_t scale = get_integer_();
if (scale == 0) if (scale == 0)
scale = default_octave_; scale = this->default_octave_;
bool need_note_gap = false;
// Now play the note // Now play the note
if (note) { if (note) {
auto note_index = (scale - 4) * 12 + note; auto note_index = (scale - 4) * 12 + note;
if (note_index < 0 || note_index >= (int) sizeof(NOTES)) { if (note_index < 0 || note_index >= (int) sizeof(NOTES)) {
ESP_LOGE(TAG, "Note out of valid range"); ESP_LOGE(TAG, "Note out of valid range");
this->note_duration_ = 0;
return; return;
} }
auto freq = NOTES[note_index]; auto freq = NOTES[note_index];
need_note_gap = freq == this->output_freq_;
if (freq == output_freq_) {
// Add small silence gap between same note // Add small silence gap between same note
output_->set_level(0.0); this->output_freq_ = freq;
delay(DOUBLE_NOTE_GAP_MS);
note_duration_ -= DOUBLE_NOTE_GAP_MS;
}
output_freq_ = freq;
ESP_LOGVV(TAG, "playing note: %d for %dms", note, note_duration_); ESP_LOGVV(TAG, "playing note: %d for %dms", note, this->note_duration_);
output_->update_frequency(freq);
output_->set_level(0.5);
} else { } else {
ESP_LOGVV(TAG, "waiting: %dms", note_duration_); ESP_LOGVV(TAG, "waiting: %dms", this->note_duration_);
output_->set_level(0.0); this->output_freq_ = 0;
} }
last_note_ = millis(); #ifdef USE_OUTPUT
if (this->output_ != nullptr) {
if (need_note_gap) {
this->output_->set_level(0.0);
delay(DOUBLE_NOTE_GAP_MS);
this->note_duration_ -= DOUBLE_NOTE_GAP_MS;
} }
if (this->output_freq_ != 0) {
this->output_->update_frequency(this->output_freq_);
this->output_->set_level(0.5);
} else {
this->output_->set_level(0.0);
}
}
#endif
#ifdef USE_SPEAKER
if (this->speaker_ != nullptr) {
this->samples_sent_ = 0;
this->samples_count_ = (this->sample_rate_ * this->note_duration_) / I2S_SPEED;
// Convert from frequency in Hz to high and low samples in fixed point
if (this->output_freq_ != 0) {
this->samples_per_wave_ = (this->sample_rate_ << 10) / this->output_freq_;
} else {
this->samples_per_wave_ = 0;
}
if (need_note_gap) {
this->samples_gap_ = (this->sample_rate_ * DOUBLE_NOTE_GAP_MS) / I2S_SPEED;
} else {
this->samples_gap_ = 0;
}
}
#endif
this->last_note_ = millis();
}
} // namespace rtttl } // namespace rtttl
} // namespace esphome } // namespace esphome

View file

@ -1,23 +1,41 @@
#pragma once #pragma once
#include "esphome/core/component.h"
#include "esphome/core/automation.h" #include "esphome/core/automation.h"
#include "esphome/core/component.h"
#ifdef USE_OUTPUT
#include "esphome/components/output/float_output.h" #include "esphome/components/output/float_output.h"
#endif
#ifdef USE_SPEAKER
#include "esphome/components/speaker/speaker.h"
#endif
namespace esphome { namespace esphome {
namespace rtttl { namespace rtttl {
#ifdef USE_SPEAKER
static const size_t SAMPLE_BUFFER_SIZE = 256;
struct SpeakerSample {
int16_t left{0};
int16_t right{0};
};
#endif
class Rtttl : public Component { class Rtttl : public Component {
public: public:
void set_output(output::FloatOutput *output) { output_ = output; } #ifdef USE_OUTPUT
void set_output(output::FloatOutput *output) { this->output_ = output; }
#endif
#ifdef USE_SPEAKER
void set_speaker(speaker::Speaker *speaker) { this->speaker_ = speaker; }
#endif
void play(std::string rtttl); void play(std::string rtttl);
void stop() { void stop();
note_duration_ = 0;
output_->set_level(0.0);
}
void dump_config() override; void dump_config() override;
bool is_playing() { return note_duration_ != 0; } bool is_playing() { return this->note_duration_ != 0; }
void loop() override; void loop() override;
void add_on_finished_playback_callback(std::function<void()> callback) { void add_on_finished_playback_callback(std::function<void()> callback) {
@ -27,14 +45,14 @@ class Rtttl : public Component {
protected: protected:
inline uint8_t get_integer_() { inline uint8_t get_integer_() {
uint8_t ret = 0; uint8_t ret = 0;
while (isdigit(rtttl_[position_])) { while (isdigit(this->rtttl_[this->position_])) {
ret = (ret * 10) + (rtttl_[position_++] - '0'); ret = (ret * 10) + (this->rtttl_[this->position_++] - '0');
} }
return ret; return ret;
} }
std::string rtttl_; std::string rtttl_{""};
size_t position_; size_t position_{0};
uint16_t wholenote_; uint16_t wholenote_;
uint16_t default_duration_; uint16_t default_duration_;
uint16_t default_octave_; uint16_t default_octave_;
@ -42,7 +60,22 @@ class Rtttl : public Component {
uint16_t note_duration_; uint16_t note_duration_;
uint32_t output_freq_; uint32_t output_freq_;
#ifdef USE_OUTPUT
output::FloatOutput *output_; output::FloatOutput *output_;
#endif
void play_output_();
#ifdef USE_SPEAKER
speaker::Speaker *speaker_;
void play_speaker_();
int sample_rate_{16000};
int samples_per_wave_{0};
int samples_sent_{0};
int samples_count_{0};
int samples_gap_{0};
#endif
CallbackManager<void()> on_finished_playback_callback_; CallbackManager<void()> on_finished_playback_callback_;
}; };

View file

@ -1,8 +1,8 @@
#include "filter.h" #include "filter.h"
#include <cmath>
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "sensor.h" #include "sensor.h"
#include <cmath>
namespace esphome { namespace esphome {
namespace sensor { namespace sensor {
@ -376,9 +376,7 @@ void OrFilter::initialize(Sensor *parent, Filter *next) {
// TimeoutFilter // TimeoutFilter
optional<float> TimeoutFilter::new_value(float value) { optional<float> TimeoutFilter::new_value(float value) {
this->set_timeout("timeout", this->time_period_, [this]() { this->output(this->value_); }); this->set_timeout("timeout", this->time_period_, [this]() { this->output(this->value_); });
this->output(value); return value;
return {};
} }
TimeoutFilter::TimeoutFilter(uint32_t time_period, float new_value) : time_period_(time_period), value_(new_value) {} TimeoutFilter::TimeoutFilter(uint32_t time_period, float new_value) : time_period_(time_period), value_(new_value) {}

View file

@ -70,15 +70,15 @@ void SGP4xComponent::setup() {
if (this->pref_.load(&this->voc_baselines_storage_)) { if (this->pref_.load(&this->voc_baselines_storage_)) {
this->voc_state0_ = this->voc_baselines_storage_.state0; this->voc_state0_ = this->voc_baselines_storage_.state0;
this->voc_state1_ = this->voc_baselines_storage_.state1; this->voc_state1_ = this->voc_baselines_storage_.state1;
ESP_LOGI(TAG, "Loaded VOC baseline state0: 0x%04X, state1: 0x%04X", this->voc_baselines_storage_.state0, ESP_LOGI(TAG, "Loaded VOC baseline state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32,
voc_baselines_storage_.state1); this->voc_baselines_storage_.state0, voc_baselines_storage_.state1);
} }
// Initialize storage timestamp // Initialize storage timestamp
this->seconds_since_last_store_ = 0; this->seconds_since_last_store_ = 0;
if (this->voc_baselines_storage_.state0 > 0 && this->voc_baselines_storage_.state1 > 0) { if (this->voc_baselines_storage_.state0 > 0 && this->voc_baselines_storage_.state1 > 0) {
ESP_LOGI(TAG, "Setting VOC baseline from save state0: 0x%04X, state1: 0x%04X", ESP_LOGI(TAG, "Setting VOC baseline from save state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32,
this->voc_baselines_storage_.state0, voc_baselines_storage_.state1); this->voc_baselines_storage_.state0, voc_baselines_storage_.state1);
voc_algorithm_.set_states(this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1); voc_algorithm_.set_states(this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1);
} }
@ -178,8 +178,8 @@ bool SGP4xComponent::measure_gas_indices_(int32_t &voc, int32_t &nox) {
this->voc_baselines_storage_.state1 = this->voc_state1_; this->voc_baselines_storage_.state1 = this->voc_state1_;
if (this->pref_.save(&this->voc_baselines_storage_)) { if (this->pref_.save(&this->voc_baselines_storage_)) {
ESP_LOGI(TAG, "Stored VOC baseline state0: 0x%04X ,state1: 0x%04X", this->voc_baselines_storage_.state0, ESP_LOGI(TAG, "Stored VOC baseline state0: 0x%04" PRIX32 " ,state1: 0x%04" PRIX32,
voc_baselines_storage_.state1); this->voc_baselines_storage_.state0, voc_baselines_storage_.state1);
} else { } else {
ESP_LOGW(TAG, "Could not store VOC baselines"); ESP_LOGW(TAG, "Could not store VOC baselines");
} }
@ -273,7 +273,7 @@ void SGP4xComponent::update_gas_indices() {
} }
if (this->samples_read_ < this->samples_to_stabilize_) { if (this->samples_read_ < this->samples_to_stabilize_) {
this->samples_read_++; this->samples_read_++;
ESP_LOGD(TAG, "Sensor has not collected enough samples yet. (%d/%d) VOC index is: %u", this->samples_read_, ESP_LOGD(TAG, "Sensor has not collected enough samples yet. (%d/%d) VOC index is: %" PRIu32, this->samples_read_,
this->samples_to_stabilize_, this->voc_index_); this->samples_to_stabilize_, this->voc_index_);
return; return;
} }

View file

@ -1,5 +1,8 @@
#pragma once #pragma once
#include <cinttypes>
#include <cmath>
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h" #include "esphome/components/sensor/sensor.h"
#include "esphome/components/sensirion_common/i2c_sensirion.h" #include "esphome/components/sensirion_common/i2c_sensirion.h"
@ -8,8 +11,6 @@
#include <VOCGasIndexAlgorithm.h> #include <VOCGasIndexAlgorithm.h>
#include <NOxGasIndexAlgorithm.h> #include <NOxGasIndexAlgorithm.h>
#include <cmath>
namespace esphome { namespace esphome {
namespace sgp4x { namespace sgp4x {

View file

@ -77,7 +77,15 @@ SN74HC165_PIN_SCHEMA = cv.All(
) )
@pins.PIN_SCHEMA_REGISTRY.register(CONF_SN74HC165, SN74HC165_PIN_SCHEMA) def sn74hc165_pin_final_validate(pin_config, parent_config):
max_pins = parent_config[CONF_SR_COUNT] * 8
if pin_config[CONF_NUMBER] >= max_pins:
raise cv.Invalid(f"Pin number must be less than {max_pins}")
@pins.PIN_SCHEMA_REGISTRY.register(
CONF_SN74HC165, SN74HC165_PIN_SCHEMA, sn74hc165_pin_final_validate
)
async def sn74hc165_pin_to_code(config): async def sn74hc165_pin_to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_parented(var, config[CONF_SN74HC165]) await cg.register_parented(var, config[CONF_SN74HC165])

View file

@ -75,7 +75,15 @@ SN74HC595_PIN_SCHEMA = cv.All(
) )
@pins.PIN_SCHEMA_REGISTRY.register(CONF_SN74HC595, SN74HC595_PIN_SCHEMA) def sn74hc595_pin_final_validate(pin_config, parent_config):
max_pins = parent_config[CONF_SR_COUNT] * 8
if pin_config[CONF_NUMBER] >= max_pins:
raise cv.Invalid(f"Pin number must be less than {max_pins}")
@pins.PIN_SCHEMA_REGISTRY.register(
CONF_SN74HC595, SN74HC595_PIN_SCHEMA, sn74hc595_pin_final_validate
)
async def sn74hc595_pin_to_code(config): async def sn74hc595_pin_to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_parented(var, config[CONF_SN74HC595]) await cg.register_parented(var, config[CONF_SN74HC595])

Some files were not shown because too many files have changed in this diff Show more