diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index ab4f8cc960..c8f94cb6bb 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,17 +1,13 @@ { "name": "ESPHome Dev", "image": "ghcr.io/esphome/esphome-lint:dev", - "postCreateCommand": [ - "script/devcontainer-post-create" - ], + "postCreateCommand": ["script/devcontainer-post-create"], "containerEnv": { - "DEVCONTAINER": "1" + "DEVCONTAINER": "1", + "PIP_BREAK_SYSTEM_PACKAGES": "1", + "PIP_ROOT_USER_ACTION": "ignore" }, - "runArgs": [ - "--privileged", - "-e", - "ESPHOME_DASHBOARD_USE_PING=1" - ], + "runArgs": ["--privileged", "-e", "ESPHOME_DASHBOARD_USE_PING=1"], "appPort": 6052, "customizations": { "vscode": { @@ -24,7 +20,7 @@ // cpp "ms-vscode.cpptools", // editorconfig - "editorconfig.editorconfig", + "editorconfig.editorconfig" ], "settings": { "python.languageServer": "Pylance", diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 70da22e57a..8d1daf922f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -221,6 +221,29 @@ jobs: id: set-matrix 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: name: Run YAML test ${{ matrix.file }} runs-on: ubuntu-latest @@ -234,6 +257,7 @@ jobs: - pytest - pyupgrade - compile-tests-list + - validate-tests strategy: fail-fast: false max-parallel: 2 diff --git a/.github/workflows/needs-docs.yml b/.github/workflows/needs-docs.yml new file mode 100644 index 0000000000..5019d64752 --- /dev/null +++ b/.github/workflows/needs-docs.yml @@ -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'); + } diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e76f8b0df2..ad8562640c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/psf/black-pre-commit-mirror - rev: 23.10.0 + rev: 23.10.1 hooks: - id: black args: @@ -11,7 +11,7 @@ repos: - --quiet files: ^((esphome|script|tests)/.+)?[^/]+\.py$ - repo: https://github.com/PyCQA/flake8 - rev: 6.0.0 + rev: 6.1.0 hooks: - id: flake8 additional_dependencies: diff --git a/CODEOWNERS b/CODEOWNERS index d48294bad3..6f87017420 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -77,6 +77,7 @@ esphome/components/dashboard_import/* @esphome/core esphome/components/debug/* @OttoWinter esphome/components/delonghi/* @grob6000 esphome/components/dfplayer/* @glmnet +esphome/components/dfrobot_sen0395/* @niklasweber esphome/components/dht/* @OttoWinter esphome/components/display_menu_base/* @numo68 esphome/components/dps310/* @kbx81 @@ -151,6 +152,7 @@ esphome/components/key_provider/* @ssieb esphome/components/kuntze/* @ssieb esphome/components/lcd_menu/* @numo68 esphome/components/ld2410/* @regevbr @sebcaps +esphome/components/ld2420/* @descipher esphome/components/ledc/* @OttoWinter esphome/components/libretiny/* @kuba2k2 esphome/components/libretiny_pwm/* @kuba2k2 @@ -182,6 +184,7 @@ esphome/components/mcp9808/* @k7hpn esphome/components/md5/* @esphome/core esphome/components/mdns/* @esphome/core esphome/components/media_player/* @jesserockz +esphome/components/micronova/* @jorre05 esphome/components/microphone/* @jesserockz esphome/components/mics_4514/* @jesserockz esphome/components/midea/* @dudanov @@ -217,7 +220,7 @@ esphome/components/optolink/* @j0ta29 esphome/components/ota/* @esphome/core esphome/components/output/* @esphome/core esphome/components/pca6416a/* @Mat931 -esphome/components/pca9554/* @hwstar +esphome/components/pca9554/* @clydebarrow @hwstar esphome/components/pcf85063/* @brogon esphome/components/pcf8563/* @KoenBreeman esphome/components/pid/* @OttoWinter diff --git a/docker/Dockerfile b/docker/Dockerfile index f076173519..72aa9d9a9c 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -43,10 +43,11 @@ RUN \ zlib1g-dev=1:1.2.13.dfsg-1 \ libjpeg-dev=1:2.1.5-2 \ 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 \ cargo=0.66.0+ds1-1 \ pkg-config=1.8.1-1; \ + gcc-arm-linux-gnueabihf=4:12.2.0-3; \ fi; \ rm -rf \ /tmp/* \ @@ -100,6 +101,10 @@ ENV USERNAME="" PASSWORD="" # Expose the dashboard to Docker 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 # The directory the user should mount their configuration files to diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 1076ebc707..d6b4416af8 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -18,6 +18,8 @@ from esphome.const import ( CONF_TRIGGER_ID, CONF_EVENT, CONF_TAG, + CONF_ON_CLIENT_CONNECTED, + CONF_ON_CLIENT_DISCONNECTED, ) from esphome.core import coroutine_with_priority @@ -87,6 +89,12 @@ CONFIG_SCHEMA = cv.Schema( 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) @@ -116,6 +124,20 @@ async def to_code(config): cg.add(var.register_user_service(trigger)) 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): decoded = base64.b64decode(encryption_config[CONF_KEY]) cg.add(var.set_noise_psk(list(decoded))) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index ca3071d6d9..2f33750686 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -217,6 +217,8 @@ message DeviceInfoResponse { string friendly_name = 13; uint32 voice_assistant_version = 14; + + string suggested_area = 16; } message ListEntitiesRequest { diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index bc61271e93..0389df215f 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -32,9 +32,9 @@ APIConnection::APIConnection(std::unique_ptr sock, APIServer *pa this->proto_write_buffer_.reserve(64); #if defined(USE_API_PLAINTEXT) - helper_ = std::unique_ptr{new APIPlaintextFrameHelper(std::move(sock))}; + this->helper_ = std::unique_ptr{new APIPlaintextFrameHelper(std::move(sock))}; #elif defined(USE_API_NOISE) - helper_ = std::unique_ptr{new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx())}; + this->helper_ = std::unique_ptr{new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx())}; #else #error "No frame helper defined" #endif @@ -42,14 +42,16 @@ APIConnection::APIConnection(std::unique_ptr sock, APIServer *pa void APIConnection::start() { this->last_traffic_ = millis(); - APIError err = helper_->init(); + APIError err = this->helper_->init(); if (err != APIError::OK) { 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; } - client_info_ = helper_->getpeername(); - helper_->set_log_info(client_info_); + this->client_info_ = helper_->getpeername(); + this->client_peername_ = this->client_info_; + this->helper_->set_log_info(this->client_info_); } APIConnection::~APIConnection() { @@ -58,6 +60,11 @@ APIConnection::~APIConnection() { bluetooth_proxy::global_bluetooth_proxy->unsubscribe_api_connection(this); } #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() { @@ -68,7 +75,7 @@ void APIConnection::loop() { // when network is disconnected force disconnect immediately // don't wait for timeout 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; } if (this->next_close_) { @@ -78,24 +85,26 @@ void APIConnection::loop() { return; } - APIError err = helper_->loop(); + APIError err = this->helper_->loop(); if (err != APIError::OK) { 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; } ReadPacketBuffer buffer; - err = helper_->read_packet(&buffer); + err = this->helper_->read_packet(&buffer); if (err == APIError::WOULD_BLOCK) { // pass } else if (err != APIError::OK) { on_fatal_error(); 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) { - ESP_LOGW(TAG, "%s: Connection closed", client_info_.c_str()); + ESP_LOGW(TAG, "%s: Connection closed", this->client_combined_info_.c_str()); } 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; } else { @@ -115,7 +124,7 @@ void APIConnection::loop() { // Disconnect if not responded within 2.5*keepalive if (now - this->last_traffic_ > (keepalive * 5) / 2) { 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) { ESP_LOGVV(TAG, "Sending keepalive PING..."); @@ -169,7 +178,7 @@ DisconnectResponse APIConnection::disconnect(const DisconnectRequest &msg) { // remote initiated disconnect_client // don't close yet, we still need to send the disconnect response // 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; DisconnectResponse resp; return resp; @@ -946,14 +955,17 @@ BluetoothConnectionsFreeResponse APIConnection::subscribe_bluetooth_connections_ #endif #ifdef USE_VOICE_ASSISTANT -bool APIConnection::request_voice_assistant(const VoiceAssistantRequest &msg) { - if (!this->voice_assistant_subscription_) - return false; - - return this->send_voice_assistant_request(msg); +void APIConnection::subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) { + if (voice_assistant::global_voice_assistant != nullptr) { + voice_assistant::global_voice_assistant->client_subscription(this, msg.subscribe); + } } void APIConnection::on_voice_assistant_response(const VoiceAssistantResponse &msg) { if (voice_assistant::global_voice_assistant != nullptr) { + if (voice_assistant::global_voice_assistant->get_api_connection() != this) { + return; + } + if (msg.error) { voice_assistant::global_voice_assistant->failed_to_start(); return; @@ -966,6 +978,10 @@ void APIConnection::on_voice_assistant_response(const VoiceAssistantResponse &ms }; void APIConnection::on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg) { 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); } } @@ -1045,12 +1061,14 @@ bool APIConnection::send_log_message(int level, const char *tag, const char *lin } HelloResponse APIConnection::hello(const HelloRequest &msg) { - this->client_info_ = msg.client_info + " (" + this->helper_->getpeername() + ")"; - this->helper_->set_log_info(client_info_); + this->client_info_ = msg.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_minor_ = msg.api_version_minor; - ESP_LOGV(TAG, "Hello from client: '%s' | API Version %" PRIu32 ".%" PRIu32, this->client_info_.c_str(), - this->client_api_version_major_, this->client_api_version_minor_); + ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->client_info_.c_str(), + this->client_peername_.c_str(), this->client_api_version_major_, this->client_api_version_minor_); HelloResponse resp; resp.api_version_major = 1; @@ -1068,9 +1086,9 @@ ConnectResponse APIConnection::connect(const ConnectRequest &msg) { // bool invalid_password = 1; resp.invalid_password = !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->parent_->get_client_connected_trigger()->trigger(this->client_info_, this->client_peername_); #ifdef USE_HOMEASSISTANT_TIME if (homeassistant::global_homeassistant_time != nullptr) { this->send_time_request(); @@ -1084,6 +1102,7 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { resp.uses_password = this->parent_->uses_password(); resp.name = App.get_name(); resp.friendly_name = App.get_friendly_name(); + resp.suggested_area = App.get_area(); resp.mac_address = get_mac_address_pretty(); resp.esphome_version = ESPHOME_VERSION; resp.compilation_time = App.get_compilation_time(); @@ -1144,10 +1163,11 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) return false; if (!this->helper_->can_write_without_blocking()) { delay(0); - APIError err = helper_->loop(); + APIError err = this->helper_->loop(); if (err != APIError::OK) { 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; } 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) { on_fatal_error(); 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 { - 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; } @@ -1177,11 +1198,11 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) } void APIConnection::on_unauthenticated_access() { 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() { 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() { this->helper_->close(); diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index c17aaab611..09b595bb71 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -126,10 +126,7 @@ class APIConnection : public APIServerConnection { #endif #ifdef USE_VOICE_ASSISTANT - void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) override { - this->voice_assistant_subscription_ = msg.subscribe; - } - bool request_voice_assistant(const VoiceAssistantRequest &msg); + void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) override; void on_voice_assistant_response(const VoiceAssistantResponse &msg) override; void on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg) override; #endif @@ -188,6 +185,8 @@ class APIConnection : public APIServerConnection { } bool send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) override; + std::string get_client_combined_info() const { return this->client_combined_info_; } + protected: friend APIServer; @@ -207,6 +206,8 @@ class APIConnection : public APIServerConnection { std::unique_ptr helper_; 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_minor_{0}; #ifdef USE_ESP32_CAMERA @@ -218,9 +219,6 @@ class APIConnection : public APIServerConnection { uint32_t last_traffic_; bool sent_ping_{false}; bool service_call_subscription_{false}; -#ifdef USE_VOICE_ASSISTANT - bool voice_assistant_subscription_{false}; -#endif bool next_close_ = false; APIServer *parent_; InitialStateIterator initial_state_iterator_; diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index c070b3c988..1e97a57bb1 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -761,6 +761,10 @@ bool DeviceInfoResponse::decode_length(uint32_t field_id, ProtoLengthDelimited v this->friendly_name = value.as_string(); return true; } + case 16: { + this->suggested_area = value.as_string(); + return true; + } default: return false; } @@ -781,6 +785,7 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(12, this->manufacturer); buffer.encode_string(13, this->friendly_name); buffer.encode_uint32(14, this->voice_assistant_version); + buffer.encode_string(16, this->suggested_area); } #ifdef HAS_PROTO_MESSAGE_DUMP 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); out.append(buffer); out.append("\n"); + + out.append(" suggested_area: "); + out.append("'").append(this->suggested_area).append("'"); + out.append("\n"); out.append("}"); } #endif diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index b935784831..a63e90b7b7 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -328,6 +328,7 @@ class DeviceInfoResponse : public ProtoMessage { std::string manufacturer{}; std::string friendly_name{}; uint32_t voice_assistant_version{0}; + std::string suggested_area{}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index c4edddc92b..0348112fcd 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -111,6 +111,7 @@ void APIServer::loop() { [](const std::unique_ptr &conn) { return !conn->remove_; }); // print disconnection messages 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()); } // resize vector @@ -331,30 +332,6 @@ void APIServer::on_shutdown() { 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 void APIServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) { if (obj->is_internal()) diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 4d359ebb79..9605a196b3 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -4,6 +4,7 @@ #include "api_pb2.h" #include "api_pb2_service.h" #include "esphome/components/socket/socket.h" +#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/controller.h" #include "esphome/core/defines.h" @@ -83,12 +84,6 @@ class APIServer : public Component, public Controller { void request_time(); #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 void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) override; #endif @@ -106,6 +101,11 @@ class APIServer : public Component, public Controller { const std::vector &get_state_subs() const; const std::vector &get_user_services() const { return this->user_services_; } + Trigger *get_client_connected_trigger() const { return this->client_connected_trigger_; } + Trigger *get_client_disconnected_trigger() const { + return this->client_disconnected_trigger_; + } + protected: std::unique_ptr socket_ = nullptr; uint16_t port_{6053}; @@ -115,6 +115,8 @@ class APIServer : public Component, public Controller { std::string password_; std::vector state_subs_; std::vector user_services_; + Trigger *client_connected_trigger_ = new Trigger(); + Trigger *client_disconnected_trigger_ = new Trigger(); #ifdef USE_API_NOISE std::shared_ptr noise_ctx_ = std::make_shared(); diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index cc78528e46..78eee4b226 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -48,7 +48,7 @@ void CaptivePortal::start() { this->dns_server_ = make_unique(); this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError); 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 this->base_->get_server()->onNotFound([this](AsyncWebServerRequest *req) { diff --git a/esphome/components/dfrobot_sen0395/__init__.py b/esphome/components/dfrobot_sen0395/__init__.py new file mode 100644 index 0000000000..e772db5a15 --- /dev/null +++ b/esphome/components/dfrobot_sen0395/__init__.py @@ -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 diff --git a/esphome/components/dfrobot_sen0395/automation.h b/esphome/components/dfrobot_sen0395/automation.h new file mode 100644 index 0000000000..1f942c02e4 --- /dev/null +++ b/esphome/components/dfrobot_sen0395/automation.h @@ -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 +class DfrobotSen0395ResetAction : public Action, public Parented { + public: + void play(Ts... x) { this->parent_->enqueue(make_unique()); } +}; + +template +class DfrobotSen0395SettingsAction : public Action, public Parented { + 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(0)); + if (this->factory_reset_.has_value() && this->factory_reset_.value(x...) == true) { + this->parent_->enqueue(make_unique()); + } + 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( + 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(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(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(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(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(val)); + } + } + this->parent_->enqueue(make_unique()); + this->parent_->enqueue(make_unique(1)); + } +}; + +} // namespace dfrobot_sen0395 +} // namespace esphome diff --git a/esphome/components/dfrobot_sen0395/binary_sensor.py b/esphome/components/dfrobot_sen0395/binary_sensor.py new file mode 100644 index 0000000000..2fd0510476 --- /dev/null +++ b/esphome/components/dfrobot_sen0395/binary_sensor.py @@ -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)) diff --git a/esphome/components/dfrobot_sen0395/commands.cpp b/esphome/components/dfrobot_sen0395/commands.cpp new file mode 100644 index 0000000000..3a89b2b71e --- /dev/null +++ b/esphome/components/dfrobot_sen0395/commands.cpp @@ -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 diff --git a/esphome/components/dfrobot_sen0395/commands.h b/esphome/components/dfrobot_sen0395/commands.h new file mode 100644 index 0000000000..7426d9732a --- /dev/null +++ b/esphome/components/dfrobot_sen0395/commands.h @@ -0,0 +1,156 @@ +#pragma once + +#include +#include + +#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 diff --git a/esphome/components/dfrobot_sen0395/dfrobot_sen0395.cpp b/esphome/components/dfrobot_sen0395/dfrobot_sen0395.cpp new file mode 100644 index 0000000000..f8ef6c7138 --- /dev/null +++ b/esphome/components/dfrobot_sen0395/dfrobot_sen0395.cpp @@ -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()); + } + + // 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 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 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 CircularCommandQueue::dequeue() { + if (this->is_empty()) + return nullptr; + std::unique_ptr 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 diff --git a/esphome/components/dfrobot_sen0395/dfrobot_sen0395.h b/esphome/components/dfrobot_sen0395/dfrobot_sen0395.h new file mode 100644 index 0000000000..d3b2ecedc3 --- /dev/null +++ b/esphome/components/dfrobot_sen0395/dfrobot_sen0395.h @@ -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 cmd); + std::unique_ptr dequeue(); + bool is_empty(); + bool is_full(); + uint8_t process(DfrobotSen0395Component *parent); + + protected: + int front_{-1}; + int rear_{-1}; + std::unique_ptr 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 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 diff --git a/esphome/components/dfrobot_sen0395/switch/__init__.py b/esphome/components/dfrobot_sen0395/switch/__init__.py new file mode 100644 index 0000000000..b1c35d27ac --- /dev/null +++ b/esphome/components/dfrobot_sen0395/switch/__init__.py @@ -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)) diff --git a/esphome/components/dfrobot_sen0395/switch/dfrobot_sen0395_switch.cpp b/esphome/components/dfrobot_sen0395/switch/dfrobot_sen0395_switch.cpp new file mode 100644 index 0000000000..ca72d94531 --- /dev/null +++ b/esphome/components/dfrobot_sen0395/switch/dfrobot_sen0395_switch.cpp @@ -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(state)); } + +void Sen0395LedSwitch::write_state(bool state) { + bool was_active = false; + if (this->parent_->is_active()) { + was_active = true; + this->parent_->enqueue(make_unique(false)); + } + this->parent_->enqueue(make_unique(state)); + this->parent_->enqueue(make_unique()); + if (was_active) { + this->parent_->enqueue(make_unique(true)); + } +} + +void Sen0395UartPresenceSwitch::write_state(bool state) { + bool was_active = false; + if (this->parent_->is_active()) { + was_active = true; + this->parent_->enqueue(make_unique(false)); + } + this->parent_->enqueue(make_unique(state)); + this->parent_->enqueue(make_unique()); + if (was_active) { + this->parent_->enqueue(make_unique(true)); + } +} + +void Sen0395StartAfterBootSwitch::write_state(bool state) { + bool was_active = false; + if (this->parent_->is_active()) { + was_active = true; + this->parent_->enqueue(make_unique(false)); + } + this->parent_->enqueue(make_unique(state)); + this->parent_->enqueue(make_unique()); + if (was_active) { + this->parent_->enqueue(make_unique(true)); + } +} + +} // namespace dfrobot_sen0395 +} // namespace esphome diff --git a/esphome/components/dfrobot_sen0395/switch/dfrobot_sen0395_switch.h b/esphome/components/dfrobot_sen0395/switch/dfrobot_sen0395_switch.h new file mode 100644 index 0000000000..ab32d81dd8 --- /dev/null +++ b/esphome/components/dfrobot_sen0395/switch/dfrobot_sen0395_switch.h @@ -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 {}; + +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 diff --git a/esphome/components/duty_time/duty_time_sensor.cpp b/esphome/components/duty_time/duty_time_sensor.cpp index 1101c4d41e..d4369c89c0 100644 --- a/esphome/components/duty_time/duty_time_sensor.cpp +++ b/esphome/components/duty_time/duty_time_sensor.cpp @@ -95,7 +95,7 @@ void DutyTimeSensor::publish_and_save_(const uint32_t sec, const uint32_t ms) { void DutyTimeSensor::dump_config() { 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_)); LOG_SENSOR(" ", "Duty Time Sensor:", this); LOG_SENSOR(" ", "Last Duty Time Sensor:", this->last_duty_time_sensor_); diff --git a/esphome/components/duty_time/duty_time_sensor.h b/esphome/components/duty_time/duty_time_sensor.h index 1ec2f7b94f..38655f104a 100644 --- a/esphome/components/duty_time/duty_time_sensor.h +++ b/esphome/components/duty_time/duty_time_sensor.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/preferences.h" diff --git a/esphome/components/esp32_camera_web_server/camera_web_server.cpp b/esphome/components/esp32_camera_web_server/camera_web_server.cpp index 3210989ff5..7ca0c56d23 100644 --- a/esphome/components/esp32_camera_web_server/camera_web_server.cpp +++ b/esphome/components/esp32_camera_web_server/camera_web_server.cpp @@ -194,8 +194,8 @@ esp_err_t CameraWebServer::streaming_handler_(struct httpd_req *req) { int64_t frame_time = millis() - last_frame; last_frame = millis(); - ESP_LOGD(TAG, "MJPG: %uB %ums (%.1ffps)", (uint32_t) image->get_data_length(), (uint32_t) frame_time, - 1000.0 / (uint32_t) frame_time); + ESP_LOGD(TAG, "MJPG: %" PRIu32 "B %" PRIu32 "ms (%.1ffps)", (uint32_t) image->get_data_length(), + (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); - ESP_LOGI(TAG, "STREAM: closed. Frames: %u", frames); + ESP_LOGI(TAG, "STREAM: closed. Frames: %" PRIu32, frames); return res; } diff --git a/esphome/components/esp32_camera_web_server/camera_web_server.h b/esphome/components/esp32_camera_web_server/camera_web_server.h index 509ca81592..f65625554c 100644 --- a/esphome/components/esp32_camera_web_server/camera_web_server.h +++ b/esphome/components/esp32_camera_web_server/camera_web_server.h @@ -2,6 +2,7 @@ #ifdef USE_ESP32 +#include #include #include diff --git a/esphome/components/font/__init__.py b/esphome/components/font/__init__.py index 2bd6beeaeb..7e34dff22d 100644 --- a/esphome/components/font/__init__.py +++ b/esphome/components/font/__init__.py @@ -137,11 +137,10 @@ def validate_weight_name(value): def download_gfonts(value): - wght = value[CONF_WEIGHT] - if value[CONF_ITALIC]: - wght = f"1,{wght}" - name = f"{value[CONF_FAMILY]}@{value[CONF_WEIGHT]}" - url = f"https://fonts.googleapis.com/css2?family={value[CONF_FAMILY]}:wght@{wght}" + name = ( + f"{value[CONF_FAMILY]}:ital,wght@{int(value[CONF_ITALIC])},{value[CONF_WEIGHT]}" + ) + url = f"https://fonts.googleapis.com/css2?family={name}" path = _compute_gfonts_local_path(value) if path.is_file(): diff --git a/esphome/components/ld2420/__init__.py b/esphome/components/ld2420/__init__.py new file mode 100644 index 0000000000..c701423081 --- /dev/null +++ b/esphome/components/ld2420/__init__.py @@ -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) diff --git a/esphome/components/ld2420/binary_sensor/__init__.py b/esphome/components/ld2420/binary_sensor/__init__.py new file mode 100644 index 0000000000..f94e4d969f --- /dev/null +++ b/esphome/components/ld2420/binary_sensor/__init__.py @@ -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)) diff --git a/esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.cpp b/esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.cpp new file mode 100644 index 0000000000..c6ea0a348b --- /dev/null +++ b/esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.cpp @@ -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 diff --git a/esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.h b/esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.h new file mode 100644 index 0000000000..ee06439090 --- /dev/null +++ b/esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.h @@ -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 diff --git a/esphome/components/ld2420/button/__init__.py b/esphome/components/ld2420/button/__init__.py new file mode 100644 index 0000000000..675e041dd4 --- /dev/null +++ b/esphome/components/ld2420/button/__init__.py @@ -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)) diff --git a/esphome/components/ld2420/button/reconfig_buttons.cpp b/esphome/components/ld2420/button/reconfig_buttons.cpp new file mode 100644 index 0000000000..3537c1d64a --- /dev/null +++ b/esphome/components/ld2420/button/reconfig_buttons.cpp @@ -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 diff --git a/esphome/components/ld2420/button/reconfig_buttons.h b/esphome/components/ld2420/button/reconfig_buttons.h new file mode 100644 index 0000000000..4e9e7a3692 --- /dev/null +++ b/esphome/components/ld2420/button/reconfig_buttons.h @@ -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 { + public: + LD2420ApplyConfigButton() = default; + + protected: + void press_action() override; +}; + +class LD2420RevertConfigButton : public button::Button, public Parented { + public: + LD2420RevertConfigButton() = default; + + protected: + void press_action() override; +}; + +class LD2420RestartModuleButton : public button::Button, public Parented { + public: + LD2420RestartModuleButton() = default; + + protected: + void press_action() override; +}; + +class LD2420FactoryResetButton : public button::Button, public Parented { + public: + LD2420FactoryResetButton() = default; + + protected: + void press_action() override; +}; + +} // namespace ld2420 +} // namespace esphome diff --git a/esphome/components/ld2420/ld2420.cpp b/esphome/components/ld2420/ld2420.cpp new file mode 100644 index 0000000000..6130617457 --- /dev/null +++ b/esphome/components/ld2420/ld2420.cpp @@ -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(¤t_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(this->gate_peak[gate]) + (move_factor * static_cast(this->gate_peak[gate]))); + this->new_config.move_thresh[gate] = static_cast(calculated_value <= 65535 ? calculated_value : 65535); + calculated_value = + (static_cast(this->gate_peak[gate]) + (still_factor * static_cast(this->gate_peak[gate]))); + this->new_config.still_thresh[gate] = static_cast(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], ®, 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(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(this->current_config.min_gate)); + if (this->max_gate_distance_number_ != nullptr) + this->max_gate_distance_number_->publish_state(static_cast(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(this->current_config.still_thresh[gate])); + } + if (this->gate_move_threshold_numbers_[gate] != nullptr) { + this->gate_move_threshold_numbers_[gate]->publish_state( + static_cast(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 diff --git a/esphome/components/ld2420/ld2420.h b/esphome/components/ld2420/ld2420.h new file mode 100644 index 0000000000..2780503776 --- /dev/null +++ b/esphome/components/ld2420/ld2420.h @@ -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 +#include + +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 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(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 gate_still_threshold_numbers_ = std::vector(16); + std::vector gate_move_threshold_numbers_ = std::vector(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 listeners_{}; +}; + +} // namespace ld2420 +} // namespace esphome diff --git a/esphome/components/ld2420/number/__init__.py b/esphome/components/ld2420/number/__init__.py new file mode 100644 index 0000000000..4ae08356fc --- /dev/null +++ b/esphome/components/ld2420/number/__init__.py @@ -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)) diff --git a/esphome/components/ld2420/number/gate_config_number.cpp b/esphome/components/ld2420/number/gate_config_number.cpp new file mode 100644 index 0000000000..e5eaafb46d --- /dev/null +++ b/esphome/components/ld2420/number/gate_config_number.cpp @@ -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 diff --git a/esphome/components/ld2420/number/gate_config_number.h b/esphome/components/ld2420/number/gate_config_number.h new file mode 100644 index 0000000000..459a8026e3 --- /dev/null +++ b/esphome/components/ld2420/number/gate_config_number.h @@ -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 { + public: + LD2420TimeoutNumber() = default; + + protected: + void control(float timeout) override; +}; + +class LD2420MinDistanceNumber : public number::Number, public Parented { + public: + LD2420MinDistanceNumber() = default; + + protected: + void control(float min_gate) override; +}; + +class LD2420MaxDistanceNumber : public number::Number, public Parented { + public: + LD2420MaxDistanceNumber() = default; + + protected: + void control(float max_gate) override; +}; + +class LD2420GateSelectNumber : public number::Number, public Parented { + public: + LD2420GateSelectNumber() = default; + + protected: + void control(float gate_select) override; +}; + +class LD2420MoveSensFactorNumber : public number::Number, public Parented { + public: + LD2420MoveSensFactorNumber() = default; + + protected: + void control(float move_factor) override; +}; + +class LD2420StillSensFactorNumber : public number::Number, public Parented { + public: + LD2420StillSensFactorNumber() = default; + + protected: + void control(float still_factor) override; +}; + +class LD2420StillThresholdNumbers : public number::Number, public Parented { + public: + LD2420StillThresholdNumbers() = default; + LD2420StillThresholdNumbers(uint8_t gate); + + protected: + uint8_t gate_; + void control(float still_threshold) override; +}; + +class LD2420MoveThresholdNumbers : public number::Number, public Parented { + public: + LD2420MoveThresholdNumbers() = default; + LD2420MoveThresholdNumbers(uint8_t gate); + + protected: + uint8_t gate_; + void control(float move_threshold) override; +}; + +} // namespace ld2420 +} // namespace esphome diff --git a/esphome/components/ld2420/select/__init__.py b/esphome/components/ld2420/select/__init__.py new file mode 100644 index 0000000000..554bd4147d --- /dev/null +++ b/esphome/components/ld2420/select/__init__.py @@ -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)) diff --git a/esphome/components/ld2420/select/operating_mode_select.cpp b/esphome/components/ld2420/select/operating_mode_select.cpp new file mode 100644 index 0000000000..1c59f443a5 --- /dev/null +++ b/esphome/components/ld2420/select/operating_mode_select.cpp @@ -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 diff --git a/esphome/components/ld2420/select/operating_mode_select.h b/esphome/components/ld2420/select/operating_mode_select.h new file mode 100644 index 0000000000..317b2af8c0 --- /dev/null +++ b/esphome/components/ld2420/select/operating_mode_select.h @@ -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 { + public: + LD2420Select() = default; + + protected: + void control(const std::string &value) override; +}; + +} // namespace ld2420 +} // namespace esphome diff --git a/esphome/components/ld2420/sensor/__init__.py b/esphome/components/ld2420/sensor/__init__.py new file mode 100644 index 0000000000..6a67d1fc41 --- /dev/null +++ b/esphome/components/ld2420/sensor/__init__.py @@ -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)) diff --git a/esphome/components/ld2420/sensor/ld2420_sensor.cpp b/esphome/components/ld2420/sensor/ld2420_sensor.cpp new file mode 100644 index 0000000000..97f0c594b7 --- /dev/null +++ b/esphome/components/ld2420/sensor/ld2420_sensor.cpp @@ -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 diff --git a/esphome/components/ld2420/sensor/ld2420_sensor.h b/esphome/components/ld2420/sensor/ld2420_sensor.h new file mode 100644 index 0000000000..4eebefe0e3 --- /dev/null +++ b/esphome/components/ld2420/sensor/ld2420_sensor.h @@ -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 energy_sensors_ = std::vector(LD2420_TOTAL_GATES); +}; + +} // namespace ld2420 +} // namespace esphome diff --git a/esphome/components/ld2420/text_sensor/__init__.py b/esphome/components/ld2420/text_sensor/__init__.py new file mode 100644 index 0000000000..b6d8c7c0e4 --- /dev/null +++ b/esphome/components/ld2420/text_sensor/__init__.py @@ -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)) diff --git a/esphome/components/ld2420/text_sensor/text_sensor.cpp b/esphome/components/ld2420/text_sensor/text_sensor.cpp new file mode 100644 index 0000000000..1dcdcf7d60 --- /dev/null +++ b/esphome/components/ld2420/text_sensor/text_sensor.cpp @@ -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 diff --git a/esphome/components/ld2420/text_sensor/text_sensor.h b/esphome/components/ld2420/text_sensor/text_sensor.h new file mode 100644 index 0000000000..073ddd5d0f --- /dev/null +++ b/esphome/components/ld2420/text_sensor/text_sensor.h @@ -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 diff --git a/esphome/components/mcp4728/__init__.py b/esphome/components/mcp4728/__init__.py index d130ceb738..a0702c415c 100644 --- a/esphome/components/mcp4728/__init__.py +++ b/esphome/components/mcp4728/__init__.py @@ -10,6 +10,7 @@ CONF_STORE_IN_EEPROM = "store_in_eeprom" mcp4728_ns = cg.esphome_ns.namespace("mcp4728") MCP4728Component = mcp4728_ns.class_("MCP4728Component", cg.Component, i2c.I2CDevice) +CONF_MCP4728_ID = "mcp4728_id" CONFIG_SCHEMA = ( cv.Schema( diff --git a/esphome/components/mcp4728/mcp4728_output.cpp b/esphome/components/mcp4728/mcp4728.cpp similarity index 90% rename from esphome/components/mcp4728/mcp4728_output.cpp rename to esphome/components/mcp4728/mcp4728.cpp index d011967624..1a8568a21c 100644 --- a/esphome/components/mcp4728/mcp4728_output.cpp +++ b/esphome/components/mcp4728/mcp4728.cpp @@ -1,4 +1,4 @@ -#include "mcp4728_output.h" +#include "mcp4728.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -110,12 +110,5 @@ void MCP4728Component::select_gain_(MCP4728ChannelIdx channel, MCP4728Gain gain) 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(duty_rounded); - this->parent_->set_channel_value_(this->channel_, duty); -} - } // namespace mcp4728 } // namespace esphome diff --git a/esphome/components/mcp4728/mcp4728_output.h b/esphome/components/mcp4728/mcp4728.h similarity index 69% rename from esphome/components/mcp4728/mcp4728_output.h rename to esphome/components/mcp4728/mcp4728.h index 55bcfdccb6..f2262f4a35 100644 --- a/esphome/components/mcp4728/mcp4728_output.h +++ b/esphome/components/mcp4728/mcp4728.h @@ -1,7 +1,6 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/components/output/float_output.h" #include "esphome/components/i2c/i2c.h" namespace esphome { @@ -64,28 +63,5 @@ class MCP4728Component : public Component, public i2c::I2CDevice { 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 esphome diff --git a/esphome/components/mcp4728/output.py b/esphome/components/mcp4728/output/__init__.py similarity index 96% rename from esphome/components/mcp4728/output.py rename to esphome/components/mcp4728/output/__init__.py index e0913ab98a..20b196ca2c 100644 --- a/esphome/components/mcp4728/output.py +++ b/esphome/components/mcp4728/output/__init__.py @@ -2,12 +2,11 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import output 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"] MCP4728Channel = mcp4728_ns.class_("MCP4728Channel", output.FloatOutput) -CONF_MCP4728_ID = "mcp4728_id" CONF_VREF = "vref" CONF_POWER_DOWN = "power_down" diff --git a/esphome/components/mcp4728/output/mcp4728_output.cpp b/esphome/components/mcp4728/output/mcp4728_output.cpp new file mode 100644 index 0000000000..b587e8801b --- /dev/null +++ b/esphome/components/mcp4728/output/mcp4728_output.cpp @@ -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(duty_rounded); + this->parent_->set_channel_value_(this->channel_, duty); +} + +} // namespace mcp4728 +} // namespace esphome diff --git a/esphome/components/mcp4728/output/mcp4728_output.h b/esphome/components/mcp4728/output/mcp4728_output.h new file mode 100644 index 0000000000..453d632f4c --- /dev/null +++ b/esphome/components/mcp4728/output/mcp4728_output.h @@ -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 diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index fbe1e1a719..9efefd58cc 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -88,7 +88,7 @@ async def to_code(config): add_idf_component( name="mdns", repo="https://github.com/espressif/esp-protocols.git", - ref="mdns-v1.2.0", + ref="mdns-v1.2.2", path="components/mdns", ) diff --git a/esphome/components/micronova/__init__.py b/esphome/components/micronova/__init__.py new file mode 100644 index 0000000000..bd253f8ebd --- /dev/null +++ b/esphome/components/micronova/__init__.py @@ -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)) diff --git a/esphome/components/micronova/button/__init__.py b/esphome/components/micronova/button/__init__.py new file mode 100644 index 0000000000..442f69c08b --- /dev/null +++ b/esphome/components/micronova/button/__init__.py @@ -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)) diff --git a/esphome/components/micronova/button/micronova_button.cpp b/esphome/components/micronova/button/micronova_button.cpp new file mode 100644 index 0000000000..c1903fd878 --- /dev/null +++ b/esphome/components/micronova/button/micronova_button.cpp @@ -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 diff --git a/esphome/components/micronova/button/micronova_button.h b/esphome/components/micronova/button/micronova_button.h new file mode 100644 index 0000000000..77649051d6 --- /dev/null +++ b/esphome/components/micronova/button/micronova_button.h @@ -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 diff --git a/esphome/components/micronova/micronova.cpp b/esphome/components/micronova/micronova.cpp new file mode 100644 index 0000000000..b96798ed12 --- /dev/null +++ b/esphome/components/micronova/micronova.cpp @@ -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 diff --git a/esphome/components/micronova/micronova.h b/esphome/components/micronova/micronova.h new file mode 100644 index 0000000000..aebef277e5 --- /dev/null +++ b/esphome/components/micronova/micronova.h @@ -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 + +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 micronova_listeners_{}; + MicroNovaSwitchListener *stove_switch_{nullptr}; +}; + +} // namespace micronova +} // namespace esphome diff --git a/esphome/components/micronova/number/__init__.py b/esphome/components/micronova/number/__init__.py new file mode 100644 index 0000000000..7124bf50d0 --- /dev/null +++ b/esphome/components/micronova/number/__init__.py @@ -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)) diff --git a/esphome/components/micronova/number/micronova_number.cpp b/esphome/components/micronova/number/micronova_number.cpp new file mode 100644 index 0000000000..244eb7ee9f --- /dev/null +++ b/esphome/components/micronova/number/micronova_number.cpp @@ -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 diff --git a/esphome/components/micronova/number/micronova_number.h b/esphome/components/micronova/number/micronova_number.h new file mode 100644 index 0000000000..49c6358255 --- /dev/null +++ b/esphome/components/micronova/number/micronova_number.h @@ -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 diff --git a/esphome/components/micronova/sensor/__init__.py b/esphome/components/micronova/sensor/__init__.py new file mode 100644 index 0000000000..32e42f3888 --- /dev/null +++ b/esphome/components/micronova/sensor/__init__.py @@ -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)) diff --git a/esphome/components/micronova/sensor/micronova_sensor.cpp b/esphome/components/micronova/sensor/micronova_sensor.cpp new file mode 100644 index 0000000000..3f0c0feaf8 --- /dev/null +++ b/esphome/components/micronova/sensor/micronova_sensor.cpp @@ -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 diff --git a/esphome/components/micronova/sensor/micronova_sensor.h b/esphome/components/micronova/sensor/micronova_sensor.h new file mode 100644 index 0000000000..9d5ae96b87 --- /dev/null +++ b/esphome/components/micronova/sensor/micronova_sensor.h @@ -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 diff --git a/esphome/components/micronova/switch/__init__.py b/esphome/components/micronova/switch/__init__.py new file mode 100644 index 0000000000..9846d46cc6 --- /dev/null +++ b/esphome/components/micronova/switch/__init__.py @@ -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)) diff --git a/esphome/components/micronova/switch/micronova_switch.cpp b/esphome/components/micronova/switch/micronova_switch.cpp new file mode 100644 index 0000000000..dcc96102db --- /dev/null +++ b/esphome/components/micronova/switch/micronova_switch.cpp @@ -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 diff --git a/esphome/components/micronova/switch/micronova_switch.h b/esphome/components/micronova/switch/micronova_switch.h new file mode 100644 index 0000000000..b0ca33b497 --- /dev/null +++ b/esphome/components/micronova/switch/micronova_switch.h @@ -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 diff --git a/esphome/components/micronova/text_sensor/__init__.py b/esphome/components/micronova/text_sensor/__init__.py new file mode 100644 index 0000000000..dc27c4f32c --- /dev/null +++ b/esphome/components/micronova/text_sensor/__init__.py @@ -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)) diff --git a/esphome/components/micronova/text_sensor/micronova_text_sensor.cpp b/esphome/components/micronova/text_sensor/micronova_text_sensor.cpp new file mode 100644 index 0000000000..03b192ffd1 --- /dev/null +++ b/esphome/components/micronova/text_sensor/micronova_text_sensor.cpp @@ -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 diff --git a/esphome/components/micronova/text_sensor/micronova_text_sensor.h b/esphome/components/micronova/text_sensor/micronova_text_sensor.h new file mode 100644 index 0000000000..b4e5de9bb3 --- /dev/null +++ b/esphome/components/micronova/text_sensor/micronova_text_sensor.h @@ -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 diff --git a/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp b/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp index 02d77a6b33..e543ceb864 100644 --- a/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp +++ b/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp @@ -69,7 +69,7 @@ bool MopekaProCheck::parse_device(const esp32_ble_tracker::ESPBTDevice &device) if ((this->distance_ != nullptr) || (this->level_ != nullptr)) { uint32_t distance_value = this->parse_distance_(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) { ESP_LOGW(TAG, "Poor read quality."); } diff --git a/esphome/components/mopeka_pro_check/mopeka_pro_check.h b/esphome/components/mopeka_pro_check/mopeka_pro_check.h index 8b126a204c..b5dff153e7 100644 --- a/esphome/components/mopeka_pro_check/mopeka_pro_check.h +++ b/esphome/components/mopeka_pro_check/mopeka_pro_check.h @@ -1,11 +1,12 @@ #pragma once +#include +#include + #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" -#include - #ifdef USE_ESP32 namespace esphome { diff --git a/esphome/components/mopeka_std_check/mopeka_std_check.cpp b/esphome/components/mopeka_std_check/mopeka_std_check.cpp index 9dd1718cb2..6685a23c41 100644 --- a/esphome/components/mopeka_std_check/mopeka_std_check.cpp +++ b/esphome/components/mopeka_std_check/mopeka_std_check.cpp @@ -16,8 +16,8 @@ static const uint16_t MANUFACTURER_ID = 0x000D; void MopekaStdCheck::dump_config() { ESP_LOGCONFIG(TAG, "Mopeka Std Check"); 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 full: %imm", this->full_mm_); + ESP_LOGCONFIG(TAG, " Tank distance empty: %" PRIi32 "mm", this->empty_mm_); + ESP_LOGCONFIG(TAG, " Tank distance full: %" PRIi32 "mm", this->full_mm_); LOG_SENSOR(" ", "Level", this->level_); LOG_SENSOR(" ", "Temperature", this->temperature_); LOG_SENSOR(" ", "Battery Level", this->battery_level_); diff --git a/esphome/components/mopeka_std_check/mopeka_std_check.h b/esphome/components/mopeka_std_check/mopeka_std_check.h index ee588c8e5f..2a1d9d2dfc 100644 --- a/esphome/components/mopeka_std_check/mopeka_std_check.h +++ b/esphome/components/mopeka_std_check/mopeka_std_check.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" diff --git a/esphome/components/mqtt/mqtt_backend_esp8266.h b/esphome/components/mqtt/mqtt_backend_esp8266.h index 981d27693f..06d4993bdf 100644 --- a/esphome/components/mqtt/mqtt_backend_esp8266.h +++ b/esphome/components/mqtt/mqtt_backend_esp8266.h @@ -19,7 +19,7 @@ class MQTTBackendESP8266 final : public MQTTBackend { void set_will(const char *topic, uint8_t qos, bool retain, const char *payload) final { 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); } #if ASYNC_TCP_SSL_ENABLED void set_secure(bool secure) { mqtt_client.setSecure(secure); } diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index 571f5c8317..95dc082e84 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -136,6 +136,7 @@ bool MQTTComponent::send_discovery_() { if (node_friendly_name.empty()) { node_friendly_name = node_name; } + const std::string &node_area = App.get_area(); JsonObject device_info = root.createNestedObject(MQTT_DEVICE); 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_MODEL] = ESPHOME_BOARD; device_info[MQTT_DEVICE_MANUFACTURER] = "espressif"; + device_info[MQTT_DEVICE_SUGGESTED_AREA] = node_area; }, 0, discovery_info.retain); } diff --git a/esphome/components/network/ip_address.h b/esphome/components/network/ip_address.h index 7bf09078be..709524c9d1 100644 --- a/esphome/components/network/ip_address.h +++ b/esphome/components/network/ip_address.h @@ -3,7 +3,11 @@ #include #include #include +#include "esphome/core/macros.h" + +#if defined(USE_ESP_IDF) || defined(USE_LIBRETINY) || USE_ARDUINO_VERSION_CODE > VERSION_CODE(3, 0, 0) #include +#endif #if USE_ARDUINO #include diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 6133ad1d7e..78c07be890 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -116,6 +116,7 @@ void Nextion::reset_(bool reset_nextion) { this->read_byte(&d); }; this->nextion_queue_.clear(); + this->waveform_queue_.clear(); } void Nextion::dump_config() { @@ -364,37 +365,21 @@ void Nextion::process_nextion_commands_() { ESP_LOGW(TAG, "Nextion reported baud rate invalid!"); break; case 0x12: // invalid Waveform ID or Channel # was used + if (this->waveform_queue_.empty()) { + ESP_LOGW(TAG, + "Nextion reported invalid Waveform ID or Channel # was used but no waveform sensor in queue found!"); + } else { + auto &nb = this->waveform_queue_.front(); + NextionComponentBase *component = nb->component; - if (!this->nextion_queue_.empty()) { - int index = 0; - int found = -1; - for (auto &nb : this->nextion_queue_) { - NextionComponentBase *component = nb->component; + ESP_LOGW(TAG, "Nextion reported invalid Waveform ID %d or Channel # %d was used!", + component->get_component_id(), component->get_wave_channel_id()); - if (component->get_queue_type() == NextionQueueType::WAVEFORM_SENSOR) { - ESP_LOGW(TAG, "Nextion reported invalid Waveform ID %d or Channel # %d was used!", - component->get_component_id(), component->get_wave_channel_id()); + ESP_LOGN(TAG, "Removing waveform from queue with component id %d and waveform id %d", + component->get_component_id(), component->get_wave_channel_id()); - ESP_LOGN(TAG, "Removing waveform from queue with component id %d and waveform id %d", - component->get_component_id(), component->get_wave_channel_id()); - - found = index; - - delete component; // NOLINT(cppcoreguidelines-owning-memory) - delete nb; // NOLINT(cppcoreguidelines-owning-memory) - - 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!"); - } + delete nb; // NOLINT(cppcoreguidelines-owning-memory) + this->waveform_queue_.pop_front(); } break; case 0x1A: // variable name invalid @@ -697,44 +682,29 @@ void Nextion::process_nextion_commands_() { } case 0xFD: { // data transparent transmit finished ESP_LOGVV(TAG, "Nextion reported data transmit finished!"); + this->check_pending_waveform_(); break; } case 0xFE: { // data transparent transmit ready ESP_LOGVV(TAG, "Nextion reported ready for transmit!"); - - int index = 0; - int found = -1; - for (auto &nb : this->nextion_queue_) { - 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() - : 255; // ADDT command can only send 255 - - this->write_array(component->get_wave_buffer().data(), static_cast(buffer_to_send)); - - 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); - - if (component->get_wave_buffer().size() <= 255) { - 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) - break; - } - ++index; - } - - if (found == -1) { + if (this->waveform_queue_.empty()) { ESP_LOGE(TAG, "No waveforms in queue to send data!"); break; - } else { - this->nextion_queue_.erase(this->nextion_queue_.begin() + found); } + + 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() + : 255; // ADDT command can only send 255 + + this->write_array(component->get_wave_buffer().data(), static_cast(buffer_to_send)); + + 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->clear_wave_buffer(buffer_to_send); + delete nb; // NOLINT(cppcoreguidelines-owning-memory) + this->waveform_queue_.pop_front(); break; } default: @@ -1093,17 +1063,28 @@ void Nextion::add_addt_command_to_queue(NextionComponentBase *component) { // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) nextion::NextionQueue *nextion_queue = new nextion::NextionQueue; - // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - nextion_queue->component = new nextion::NextionComponentBase; + nextion_queue->component = component; 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() : 255; // ADDT command can only send 255 std::string command = "addt " + to_string(component->get_component_id()) + "," + to_string(component->get_wave_channel_id()) + "," + to_string(buffer_to_send); - if (this->send_command_(command)) { - this->nextion_queue_.push_back(nextion_queue); + if (!this->send_command_(command)) { + delete nb; // NOLINT(cppcoreguidelines-owning-memory) + this->waveform_queue_.pop_front(); } } diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index 7518d7f4cb..92ff3fe235 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -740,6 +740,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe protected: std::deque nextion_queue_; + std::deque waveform_queue_; uint16_t recv_ret_string_(std::string &response, uint32_t timeout, bool recv_flag); void all_components_send_state_(bool force_update = false); 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 &state_value, bool is_sleep_safe = false); + void check_pending_waveform_(); + #ifdef USE_NEXTION_TFT_UPLOAD #ifdef USE_ESP8266 WiFiClient *wifi_client_{nullptr}; diff --git a/esphome/components/nextion/nextion_component_base.h b/esphome/components/nextion/nextion_component_base.h index e0ef8f93bc..42e1b00998 100644 --- a/esphome/components/nextion/nextion_component_base.h +++ b/esphome/components/nextion/nextion_component_base.h @@ -69,6 +69,13 @@ class NextionComponentBase { std::vector get_wave_buffer() { return this->wave_buffer_; } 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_to_send() { return this->variable_name_to_send_; } diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index 56a59c54d1..2b064a90cf 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -135,7 +135,7 @@ def _process_base_package(config: dict) -> dict: packages[file] = new_yaml except EsphomeError as e: 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 return packages diff --git a/esphome/components/pca6416a/__init__.py b/esphome/components/pca6416a/__init__.py index 574d8dce91..93be148169 100644 --- a/esphome/components/pca6416a/__init__.py +++ b/esphome/components/pca6416a/__init__.py @@ -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): var = cg.new_Pvariable(config[CONF_ID]) parent = await cg.get_variable(config[CONF_PCA6416A]) diff --git a/esphome/components/pca9554/__init__.py b/esphome/components/pca9554/__init__.py index 76d6ddaf32..fd52fafc5d 100644 --- a/esphome/components/pca9554/__init__.py +++ b/esphome/components/pca9554/__init__.py @@ -11,9 +11,10 @@ from esphome.const import ( CONF_OUTPUT, ) -CODEOWNERS = ["@hwstar"] +CODEOWNERS = ["@hwstar", "@clydebarrow"] DEPENDENCIES = ["i2c"] MULTI_CONF = True +CONF_PIN_COUNT = "pin_count" pca9554_ns = cg.esphome_ns.namespace("pca9554") PCA9554Component = pca9554_ns.class_("PCA9554Component", cg.Component, i2c.I2CDevice) @@ -23,7 +24,12 @@ PCA9554GPIOPin = pca9554_ns.class_( CONF_PCA9554 = "pca9554" 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( i2c.i2c_device_schema(0x20) @@ -33,6 +39,7 @@ CONFIG_SCHEMA = ( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) + cg.add(var.set_pin_count(config[CONF_PIN_COUNT])) await cg.register_component(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.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_INPUT, default=False): cv.boolean, @@ -58,11 +65,19 @@ PCA9554_PIN_SCHEMA = cv.All( validate_mode, ), 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): var = cg.new_Pvariable(config[CONF_ID]) parent = await cg.get_variable(config[CONF_PCA9554]) diff --git a/esphome/components/pca9554/pca9554.cpp b/esphome/components/pca9554/pca9554.cpp index 74c64dffaa..c5a4bcfb09 100644 --- a/esphome/components/pca9554/pca9554.cpp +++ b/esphome/components/pca9554/pca9554.cpp @@ -4,6 +4,7 @@ namespace esphome { namespace pca9554 { +// for 16 bit expanders, these addresses will be doubled. const uint8_t INPUT_REG = 0; const uint8_t OUTPUT_REG = 1; const uint8_t INVERT_REG = 2; @@ -13,9 +14,10 @@ static const char *const TAG = "pca9554"; void PCA9554Component::setup() { ESP_LOGCONFIG(TAG, "Setting up PCA9554/PCA9554A..."); + this->reg_width_ = (this->pin_count_ + 7) / 8; // Test to see if device exists 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(); return; } @@ -44,6 +46,7 @@ void PCA9554Component::loop() { void PCA9554Component::dump_config() { ESP_LOGCONFIG(TAG, "PCA9554:"); + ESP_LOGCONFIG(TAG, " I/O Pins: %d", this->pin_count_); LOG_I2C_DEVICE(this) if (this->is_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_() { - uint8_t inputs; + uint8_t inputs[2]; if (this->is_failed()) { ESP_LOGD(TAG, "Device marked failed"); 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(); ESP_LOGE(TAG, "read_register_(): I2C I/O error: %d", (int) this->last_error_); return false; } 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; } -bool PCA9554Component::write_register_(uint8_t reg, uint8_t value) { - if ((this->last_error_ = this->write_register(reg, &value, 1, true)) != esphome::i2c::ERROR_OK) { +bool PCA9554Component::write_register_(uint8_t reg, uint16_t value) { + 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(); ESP_LOGE(TAG, "write_register_(): I2C I/O error: %d", (int) this->last_error_); return false; diff --git a/esphome/components/pca9554/pca9554.h b/esphome/components/pca9554/pca9554.h index c2aa5c30ed..c548bec619 100644 --- a/esphome/components/pca9554/pca9554.h +++ b/esphome/components/pca9554/pca9554.h @@ -28,19 +28,25 @@ class PCA9554Component : public Component, public i2c::I2CDevice { void dump_config() override; + void set_pin_count(size_t pin_count) { this->pin_count_ = pin_count; } + protected: 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 - uint8_t config_mask_{0x00}; + uint16_t config_mask_{0x00}; /// 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 - uint8_t input_mask_{0x00}; + uint16_t input_mask_{0x00}; /// 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 esphome::i2c::ErrorCode last_error_; }; diff --git a/esphome/components/pcf8574/__init__.py b/esphome/components/pcf8574/__init__.py index a5f963707f..d44ac28364 100644 --- a/esphome/components/pcf8574/__init__.py +++ b/esphome/components/pcf8574/__init__.py @@ -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): var = cg.new_Pvariable(config[CONF_ID]) parent = await cg.get_variable(config[CONF_PCF8574]) diff --git a/esphome/components/rtttl/__init__.py b/esphome/components/rtttl/__init__.py index e9453896ac..6163129529 100644 --- a/esphome/components/rtttl/__init__.py +++ b/esphome/components/rtttl/__init__.py @@ -4,7 +4,15 @@ import esphome.config_validation as cv import esphome.final_validate as fv from esphome import automation 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__) @@ -24,17 +32,23 @@ IsPlayingCondition = rtttl_ns.class_("IsPlayingCondition", automation.Condition) MULTI_CONF = True -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(CONF_ID): cv.declare_id(Rtttl), - cv.Required(CONF_OUTPUT): cv.use_id(FloatOutput), - cv.Optional(CONF_ON_FINISHED_PLAYBACK): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FinishedPlaybackTrigger), - } - ), - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(CONF_ID): cv.declare_id(Rtttl), + 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.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + FinishedPlaybackTrigger + ), + } + ), + } + ).extend(cv.COMPONENT_SCHEMA), + cv.has_exactly_one_key(CONF_OUTPUT, CONF_SPEAKER), +) def validate_parent_output_config(value): @@ -63,9 +77,9 @@ def validate_parent_output_config(value): 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 - ) + ), }, extra=cv.ALLOW_EXTRA, ) @@ -75,8 +89,14 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - out = await cg.get_variable(config[CONF_OUTPUT]) - cg.add(var.set_output(out)) + if CONF_OUTPUT in config: + out = await cg.get_variable(config[CONF_OUTPUT]) + 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, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) diff --git a/esphome/components/rtttl/rtttl.cpp b/esphome/components/rtttl/rtttl.cpp index 6274e69ba3..199a373785 100644 --- a/esphome/components/rtttl/rtttl.cpp +++ b/esphome/components/rtttl/rtttl.cpp @@ -1,4 +1,5 @@ #include "rtttl.h" +#include #include "esphome/core/hal.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, 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::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; uint8_t num; // 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 - if (position_ == std::string::npos || position_ > 15) { + if (this->position_ == std::string::npos || this->position_ > 15) { ESP_LOGE(TAG, "Missing ':' when looking for name."); 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()); // get default duration - position_ = this->rtttl_.find("d=", position_); - if (position_ == std::string::npos) { + this->position_ = this->rtttl_.find("d=", this->position_); + if (this->position_ == std::string::npos) { ESP_LOGE(TAG, "Missing 'd='"); return; } - position_ += 2; + this->position_ += 2; num = this->get_integer_(); if (num > 0) - default_duration_ = num; + this->default_duration_ = num; // get default octave - position_ = rtttl_.find("o=", position_); - if (position_ == std::string::npos) { + this->position_ = this->rtttl_.find("o=", this->position_); + if (this->position_ == std::string::npos) { ESP_LOGE(TAG, "Missing 'o="); return; } - position_ += 2; + this->position_ += 2; num = get_integer_(); if (num >= 3 && num <= 7) - default_octave_ = num; + this->default_octave_ = num; // get BPM - position_ = rtttl_.find("b=", position_); - if (position_ == std::string::npos) { + this->position_ = this->rtttl_.find("b=", this->position_); + if (this->position_ == std::string::npos) { ESP_LOGE(TAG, "Missing b="); return; } - position_ += 2; + this->position_ += 2; num = get_integer_(); if (num != 0) bpm = num; - position_ = rtttl_.find(':', position_); - if (position_ == std::string::npos) { + this->position_ = this->rtttl_.find(':', this->position_); + if (this->position_ == std::string::npos) { ESP_LOGE(TAG, "Missing second ':'"); return; } - position_++; + this->position_++; // 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; - last_note_ = millis(); - note_duration_ = 1; + this->output_freq_ = 0; + this->last_note_ = millis(); + 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() { - if (note_duration_ == 0 || millis() - last_note_ < note_duration_) + if (this->note_duration_ == 0) return; - if (!rtttl_[position_]) { - output_->set_level(0.0); +#ifdef USE_SPEAKER + 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"); this->on_finished_playback_callback_.call(); - note_duration_ = 0; return; } // 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_] == ' ') - position_++; + while (this->rtttl_[this->position_] == ',' || this->rtttl_[this->position_] == ' ') + this->position_++; // first, get note duration, if available uint8_t num = this->get_integer_(); if (num) { - note_duration_ = wholenote_ / num; + this->note_duration_ = this->wholenote_ / num; } 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; - switch (rtttl_[position_]) { + switch (this->rtttl_[this->position_]) { case 'c': note = 1; break; @@ -138,51 +220,81 @@ void Rtttl::loop() { default: note = 0; } - position_++; + this->position_++; // now, get optional '#' sharp - if (rtttl_[position_] == '#') { + if (this->rtttl_[this->position_] == '#') { note++; - position_++; + this->position_++; } // now, get optional '.' dotted note - if (rtttl_[position_] == '.') { - note_duration_ += note_duration_ / 2; - position_++; + if (this->rtttl_[this->position_] == '.') { + this->note_duration_ += this->note_duration_ / 2; + this->position_++; } // now, get scale uint8_t scale = get_integer_(); if (scale == 0) - scale = default_octave_; + scale = this->default_octave_; + bool need_note_gap = false; // Now play the note if (note) { auto note_index = (scale - 4) * 12 + note; if (note_index < 0 || note_index >= (int) sizeof(NOTES)) { ESP_LOGE(TAG, "Note out of valid range"); + this->note_duration_ = 0; return; } auto freq = NOTES[note_index]; + need_note_gap = freq == this->output_freq_; - if (freq == output_freq_) { - // Add small silence gap between same note - output_->set_level(0.0); - delay(DOUBLE_NOTE_GAP_MS); - note_duration_ -= DOUBLE_NOTE_GAP_MS; - } - output_freq_ = freq; + // Add small silence gap between same note + this->output_freq_ = freq; - ESP_LOGVV(TAG, "playing note: %d for %dms", note, note_duration_); - output_->update_frequency(freq); - output_->set_level(0.5); + ESP_LOGVV(TAG, "playing note: %d for %dms", note, this->note_duration_); } else { - ESP_LOGVV(TAG, "waiting: %dms", note_duration_); - output_->set_level(0.0); + ESP_LOGVV(TAG, "waiting: %dms", this->note_duration_); + 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 esphome diff --git a/esphome/components/rtttl/rtttl.h b/esphome/components/rtttl/rtttl.h index ec6fe7f98f..e09b0265be 100644 --- a/esphome/components/rtttl/rtttl.h +++ b/esphome/components/rtttl/rtttl.h @@ -1,23 +1,41 @@ #pragma once -#include "esphome/core/component.h" #include "esphome/core/automation.h" +#include "esphome/core/component.h" + +#ifdef USE_OUTPUT #include "esphome/components/output/float_output.h" +#endif + +#ifdef USE_SPEAKER +#include "esphome/components/speaker/speaker.h" +#endif namespace esphome { 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 { 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 stop() { - note_duration_ = 0; - output_->set_level(0.0); - } + void stop(); void dump_config() override; - bool is_playing() { return note_duration_ != 0; } + bool is_playing() { return this->note_duration_ != 0; } void loop() override; void add_on_finished_playback_callback(std::function callback) { @@ -27,14 +45,14 @@ class Rtttl : public Component { protected: inline uint8_t get_integer_() { uint8_t ret = 0; - while (isdigit(rtttl_[position_])) { - ret = (ret * 10) + (rtttl_[position_++] - '0'); + while (isdigit(this->rtttl_[this->position_])) { + ret = (ret * 10) + (this->rtttl_[this->position_++] - '0'); } return ret; } - std::string rtttl_; - size_t position_; + std::string rtttl_{""}; + size_t position_{0}; uint16_t wholenote_; uint16_t default_duration_; uint16_t default_octave_; @@ -42,7 +60,22 @@ class Rtttl : public Component { uint16_t note_duration_; uint32_t output_freq_; + +#ifdef USE_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 on_finished_playback_callback_; }; diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index d1cb8d1c4b..b5bef4930c 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -1,8 +1,8 @@ #include "filter.h" +#include #include "esphome/core/hal.h" #include "esphome/core/log.h" #include "sensor.h" -#include namespace esphome { namespace sensor { @@ -376,9 +376,7 @@ void OrFilter::initialize(Sensor *parent, Filter *next) { // TimeoutFilter optional TimeoutFilter::new_value(float value) { this->set_timeout("timeout", this->time_period_, [this]() { this->output(this->value_); }); - this->output(value); - - return {}; + return value; } TimeoutFilter::TimeoutFilter(uint32_t time_period, float new_value) : time_period_(time_period), value_(new_value) {} diff --git a/esphome/components/sgp4x/sgp4x.cpp b/esphome/components/sgp4x/sgp4x.cpp index a48372aab7..561d41e225 100644 --- a/esphome/components/sgp4x/sgp4x.cpp +++ b/esphome/components/sgp4x/sgp4x.cpp @@ -70,15 +70,15 @@ void SGP4xComponent::setup() { if (this->pref_.load(&this->voc_baselines_storage_)) { this->voc_state0_ = this->voc_baselines_storage_.state0; 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, - voc_baselines_storage_.state1); + ESP_LOGI(TAG, "Loaded VOC baseline state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32, + this->voc_baselines_storage_.state0, voc_baselines_storage_.state1); } // Initialize storage timestamp this->seconds_since_last_store_ = 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); 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_; 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, - voc_baselines_storage_.state1); + ESP_LOGI(TAG, "Stored VOC baseline state0: 0x%04" PRIX32 " ,state1: 0x%04" PRIX32, + this->voc_baselines_storage_.state0, voc_baselines_storage_.state1); } else { 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_) { 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_); return; } diff --git a/esphome/components/sgp4x/sgp4x.h b/esphome/components/sgp4x/sgp4x.h index 3a8d8200a7..aa5ae4b9d2 100644 --- a/esphome/components/sgp4x/sgp4x.h +++ b/esphome/components/sgp4x/sgp4x.h @@ -1,5 +1,8 @@ #pragma once +#include +#include + #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/sensirion_common/i2c_sensirion.h" @@ -8,8 +11,6 @@ #include #include -#include - namespace esphome { namespace sgp4x { diff --git a/esphome/components/sn74hc165/__init__.py b/esphome/components/sn74hc165/__init__.py index 85d0220a88..0f2abd3678 100644 --- a/esphome/components/sn74hc165/__init__.py +++ b/esphome/components/sn74hc165/__init__.py @@ -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): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_parented(var, config[CONF_SN74HC165]) diff --git a/esphome/components/sn74hc595/__init__.py b/esphome/components/sn74hc595/__init__.py index 92b6d8d0e5..e98da72304 100644 --- a/esphome/components/sn74hc595/__init__.py +++ b/esphome/components/sn74hc595/__init__.py @@ -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): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_parented(var, config[CONF_SN74HC595]) diff --git a/esphome/components/tuya/text_sensor/tuya_text_sensor.cpp b/esphome/components/tuya/text_sensor/tuya_text_sensor.cpp index 602595e89d..fbe511811f 100644 --- a/esphome/components/tuya/text_sensor/tuya_text_sensor.cpp +++ b/esphome/components/tuya/text_sensor/tuya_text_sensor.cpp @@ -1,5 +1,5 @@ -#include "esphome/core/log.h" #include "tuya_text_sensor.h" +#include "esphome/core/log.h" namespace esphome { namespace tuya { @@ -19,6 +19,12 @@ void TuyaTextSensor::setup() { this->publish_state(data); break; } + case TuyaDatapointType::ENUM: { + std::string data = to_string(datapoint.value_enum); + ESP_LOGD(TAG, "MCU reported text sensor %u is: %s", datapoint.id, data.c_str()); + this->publish_state(data); + break; + } default: ESP_LOGW(TAG, "Unsupported data type for tuya text sensor %u: %#02hhX", datapoint.id, (uint8_t) datapoint.type); break; diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index daf5080e7a..da03e3faad 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -61,7 +61,7 @@ void Tuya::dump_config() { } else if (info.type == TuyaDatapointType::ENUM) { ESP_LOGCONFIG(TAG, " Datapoint %u: enum (value: %d)", info.id, info.value_enum); } else if (info.type == TuyaDatapointType::BITMASK) { - ESP_LOGCONFIG(TAG, " Datapoint %u: bitmask (value: %x)", info.id, info.value_bitmask); + ESP_LOGCONFIG(TAG, " Datapoint %u: bitmask (value: %" PRIx32 ")", info.id, info.value_bitmask); } else { ESP_LOGCONFIG(TAG, " Datapoint %u: unknown", info.id); } @@ -342,7 +342,7 @@ void Tuya::handle_datapoints_(const uint8_t *buffer, size_t len) { ESP_LOGW(TAG, "Datapoint %u has bad bitmask len %zu", datapoint.id, data_size); return; } - ESP_LOGD(TAG, "Datapoint %u update to %#08X", datapoint.id, datapoint.value_bitmask); + ESP_LOGD(TAG, "Datapoint %u update to %#08" PRIX32, datapoint.id, datapoint.value_bitmask); break; default: ESP_LOGW(TAG, "Datapoint %u has unknown type %#02hhX", datapoint.id, static_cast(datapoint.type)); @@ -594,7 +594,7 @@ optional Tuya::get_datapoint_(uint8_t datapoint_id) { void Tuya::set_numeric_datapoint_value_(uint8_t datapoint_id, TuyaDatapointType datapoint_type, const uint32_t value, uint8_t length, bool forced) { - ESP_LOGD(TAG, "Setting datapoint %u to %u", datapoint_id, value); + ESP_LOGD(TAG, "Setting datapoint %u to %" PRIu32, datapoint_id, value); optional datapoint = this->get_datapoint_(datapoint_id); if (!datapoint.has_value()) { ESP_LOGW(TAG, "Setting unknown datapoint %u", datapoint_id); diff --git a/esphome/components/tuya/tuya.h b/esphome/components/tuya/tuya.h index 26f6f65912..27a97c3dc9 100644 --- a/esphome/components/tuya/tuya.h +++ b/esphome/components/tuya/tuya.h @@ -1,5 +1,8 @@ #pragma once +#include +#include + #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/helpers.h" @@ -10,8 +13,6 @@ #include "esphome/core/time.h" #endif -#include - namespace esphome { namespace tuya { diff --git a/esphome/components/vbus/sensor/__init__.py b/esphome/components/vbus/sensor/__init__.py index 2ad9da424e..2b89da6d32 100644 --- a/esphome/components/vbus/sensor/__init__.py +++ b/esphome/components/vbus/sensor/__init__.py @@ -22,6 +22,7 @@ from esphome.const import ( ICON_THERMOMETER, ICON_TIMER, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, UNIT_CELSIUS, UNIT_HOUR, UNIT_MINUTE, @@ -128,7 +129,7 @@ CONFIG_SCHEMA = cv.typed_schema( icon=ICON_RADIATOR, accuracy_decimals=0, device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional(CONF_TIME): sensor.sensor_schema( unit_of_measurement=UNIT_MINUTE, @@ -209,7 +210,7 @@ CONFIG_SCHEMA = cv.typed_schema( icon=ICON_RADIATOR, accuracy_decimals=0, device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional(CONF_TIME): sensor.sensor_schema( unit_of_measurement=UNIT_MINUTE, @@ -290,7 +291,7 @@ CONFIG_SCHEMA = cv.typed_schema( icon=ICON_RADIATOR, accuracy_decimals=0, device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional(CONF_TIME): sensor.sensor_schema( unit_of_measurement=UNIT_MINUTE, @@ -353,7 +354,7 @@ CONFIG_SCHEMA = cv.typed_schema( icon=ICON_RADIATOR, accuracy_decimals=0, device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional(CONF_VERSION): sensor.sensor_schema( accuracy_decimals=2, @@ -433,7 +434,7 @@ CONFIG_SCHEMA = cv.typed_schema( icon=ICON_RADIATOR, accuracy_decimals=0, device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional(CONF_TIME): sensor.sensor_schema( unit_of_measurement=UNIT_MINUTE, diff --git a/esphome/components/voice_assistant/__init__.py b/esphome/components/voice_assistant/__init__.py index 14176ad7cf..3270b9f370 100644 --- a/esphome/components/voice_assistant/__init__.py +++ b/esphome/components/voice_assistant/__init__.py @@ -6,6 +6,8 @@ from esphome.const import ( CONF_MICROPHONE, CONF_SPEAKER, CONF_MEDIA_PLAYER, + CONF_ON_CLIENT_CONNECTED, + CONF_ON_CLIENT_DISCONNECTED, ) from esphome import automation from esphome.automation import register_action, register_condition @@ -80,6 +82,12 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_ON_TTS_END): automation.validate_automation(single=True), cv.Optional(CONF_ON_END): automation.validate_automation(single=True), cv.Optional(CONF_ON_ERROR): automation.validate_automation(single=True), + 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), ) @@ -155,6 +163,20 @@ async def to_code(config): config[CONF_ON_ERROR], ) + if CONF_ON_CLIENT_CONNECTED in config: + await automation.build_automation( + var.get_client_connected_trigger(), + [], + config[CONF_ON_CLIENT_CONNECTED], + ) + + if CONF_ON_CLIENT_DISCONNECTED in config: + await automation.build_automation( + var.get_client_disconnected_trigger(), + [], + config[CONF_ON_CLIENT_DISCONNECTED], + ) + cg.add_define("USE_VOICE_ASSISTANT") diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index df7853156d..d15d702d4b 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -127,8 +127,8 @@ int VoiceAssistant::read_microphone_() { } void VoiceAssistant::loop() { - if (this->state_ != State::IDLE && this->state_ != State::STOP_MICROPHONE && - this->state_ != State::STOPPING_MICROPHONE && !api::global_api_server->is_connected()) { + if (this->api_client_ == nullptr && this->state_ != State::IDLE && this->state_ != State::STOP_MICROPHONE && + this->state_ != State::STOPPING_MICROPHONE) { if (this->mic_->is_running() || this->state_ == State::STARTING_MICROPHONE) { this->set_state_(State::STOP_MICROPHONE, State::IDLE); } else { @@ -213,7 +213,14 @@ void VoiceAssistant::loop() { audio_settings.noise_suppression_level = this->noise_suppression_level_; audio_settings.auto_gain = this->auto_gain_; audio_settings.volume_multiplier = this->volume_multiplier_; - if (!api::global_api_server->start_voice_assistant(this->conversation_id_, flags, audio_settings)) { + + api::VoiceAssistantRequest msg; + msg.start = true; + msg.conversation_id = this->conversation_id_; + msg.flags = flags; + msg.audio_settings = audio_settings; + + if (this->api_client_ == nullptr || !this->api_client_->send_voice_assistant_request(msg)) { ESP_LOGW(TAG, "Could not request start."); this->error_trigger_->trigger("not-connected", "Could not request start."); this->continuous_ = false; @@ -326,6 +333,28 @@ void VoiceAssistant::loop() { } } +void VoiceAssistant::client_subscription(api::APIConnection *client, bool subscribe) { + if (!subscribe) { + if (this->api_client_ == nullptr || client != this->api_client_) { + ESP_LOGE(TAG, "Client attempting to unsubscribe that is not the current API Client"); + return; + } + this->api_client_ = nullptr; + this->client_disconnected_trigger_->trigger(); + return; + } + + if (this->api_client_ != nullptr) { + ESP_LOGE(TAG, "Multiple API Clients attempting to connect to Voice Assistant"); + ESP_LOGE(TAG, "Current client: %s", this->api_client_->get_client_combined_info().c_str()); + ESP_LOGE(TAG, "New client: %s", client->get_client_combined_info().c_str()); + return; + } + + this->api_client_ = client; + this->client_connected_trigger_->trigger(); +} + static const LogString *voice_assistant_state_to_string(State state) { switch (state) { case State::IDLE: @@ -408,7 +437,7 @@ void VoiceAssistant::start_streaming(struct sockaddr_storage *addr, uint16_t por } void VoiceAssistant::request_start(bool continuous, bool silence_detection) { - if (!api::global_api_server->is_connected()) { + if (this->api_client_ == nullptr) { ESP_LOGE(TAG, "No API client connected"); this->set_state_(State::IDLE, State::IDLE); this->continuous_ = false; @@ -459,9 +488,14 @@ void VoiceAssistant::request_stop() { } void VoiceAssistant::signal_stop_() { - ESP_LOGD(TAG, "Signaling stop..."); - api::global_api_server->stop_voice_assistant(); memset(&this->dest_addr_, 0, sizeof(this->dest_addr_)); + if (this->api_client_ == nullptr) { + return; + } + ESP_LOGD(TAG, "Signaling stop..."); + api::VoiceAssistantRequest msg; + msg.start = false; + this->api_client_->send_voice_assistant_request(msg); } void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index cd448293db..a265522bca 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -8,8 +8,8 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" +#include "esphome/components/api/api_connection.h" #include "esphome/components/api/api_pb2.h" -#include "esphome/components/api/api_server.h" #include "esphome/components/microphone/microphone.h" #ifdef USE_SPEAKER #include "esphome/components/speaker/speaker.h" @@ -109,6 +109,12 @@ class VoiceAssistant : public Component { Trigger<> *get_end_trigger() const { return this->end_trigger_; } Trigger *get_error_trigger() const { return this->error_trigger_; } + Trigger<> *get_client_connected_trigger() const { return this->client_connected_trigger_; } + Trigger<> *get_client_disconnected_trigger() const { return this->client_disconnected_trigger_; } + + void client_subscription(api::APIConnection *client, bool subscribe); + api::APIConnection *get_api_connection() const { return this->api_client_; } + protected: int read_microphone_(); void set_state_(State state); @@ -127,6 +133,11 @@ class VoiceAssistant : public Component { Trigger<> *end_trigger_ = new Trigger<>(); Trigger *error_trigger_ = new Trigger(); + Trigger<> *client_connected_trigger_ = new Trigger<>(); + Trigger<> *client_disconnected_trigger_ = new Trigger<>(); + + api::APIConnection *api_client_{nullptr}; + microphone::Microphone *mic_{nullptr}; #ifdef USE_SPEAKER speaker::Speaker *speaker_{nullptr}; diff --git a/esphome/components/xpt2046/xpt2046.cpp b/esphome/components/xpt2046/xpt2046.cpp index 6c7c55a995..078a1b01e9 100644 --- a/esphome/components/xpt2046/xpt2046.cpp +++ b/esphome/components/xpt2046/xpt2046.cpp @@ -3,6 +3,7 @@ #include "esphome/core/helpers.h" #include +#include namespace esphome { namespace xpt2046 { @@ -151,7 +152,7 @@ void XPT2046Component::dump_config() { ESP_LOGCONFIG(TAG, " Invert Y: %s", YESNO(this->invert_y_)); ESP_LOGCONFIG(TAG, " threshold: %d", this->threshold_); - ESP_LOGCONFIG(TAG, " Report interval: %u", this->report_millis_); + ESP_LOGCONFIG(TAG, " Report interval: %" PRIu32, this->report_millis_); LOG_UPDATE_INTERVAL(this); } diff --git a/esphome/config.py b/esphome/config.py index b04de020e0..a980358186 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -10,7 +10,7 @@ from contextlib import contextmanager import voluptuous as vol -from esphome import core, yaml_util, loader +from esphome import core, yaml_util, loader, pins import esphome.core.config as core_config from esphome.const import ( CONF_ESPHOME, @@ -645,14 +645,40 @@ class FinalValidateValidationStep(ConfigValidationStep): # If result already has errors, skip this step return - if self.comp.final_validate_schema is None: - return - token = fv.full_config.set(result) conf = result.get_nested_item(self.path) with result.catch_error(self.path): - self.comp.final_validate_schema(conf) + if self.comp.final_validate_schema is not None: + self.comp.final_validate_schema(conf) + + fconf = fv.full_config.get() + + def _check_pins(c): + for value in c.values(): + if not isinstance(value, dict): + continue + for key, ( + _, + _, + pin_final_validate, + ) in pins.PIN_SCHEMA_REGISTRY.items(): + if ( + key != CORE.target_platform + and key in value + and pin_final_validate is not None + ): + pin_final_validate(fconf, value) + + # Check for pin configs and a final_validate schema in the pin registry + confs = conf + if not isinstance( + confs, list + ): # Handle components like SPI that have a list instead of MULTI_CONF + confs = [conf] + for c in confs: + if c: # Some component have None or empty schemas + _check_pins(c) fv.full_config.reset(token) diff --git a/esphome/const.py b/esphome/const.py index 47eedc24b7..9457958863 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -53,6 +53,7 @@ CONF_AND = "and" CONF_AP = "ap" CONF_APPARENT_POWER = "apparent_power" CONF_ARDUINO_VERSION = "arduino_version" +CONF_AREA = "area" CONF_ARGS = "args" CONF_ASSUMED_STATE = "assumed_state" CONF_AT = "at" @@ -484,6 +485,8 @@ CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE = "on_ble_manufacturer_data_advertise" CONF_ON_BLE_SERVICE_DATA_ADVERTISE = "on_ble_service_data_advertise" CONF_ON_BOOT = "on_boot" CONF_ON_CLICK = "on_click" +CONF_ON_CLIENT_CONNECTED = "on_client_connected" +CONF_ON_CLIENT_DISCONNECTED = "on_client_disconnected" CONF_ON_CONNECT = "on_connect" CONF_ON_CONTROL = "on_control" CONF_ON_DISCONNECT = "on_disconnect" diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 52c58cb54a..0b597e0c9e 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -464,6 +464,8 @@ class EsphomeCore: self.name: Optional[str] = None # The friendly name of the node self.friendly_name: Optional[str] = None + # The area / zone of the node + self.area: Optional[str] = None # Additional data components can store temporary data in # The first key to this dict should always be the integration name self.data = {} @@ -504,6 +506,7 @@ class EsphomeCore: self.dashboard = False self.name = None self.friendly_name = None + self.area = None self.data = {} self.config_path = None self.build_path = None diff --git a/esphome/core/application.h b/esphome/core/application.h index f2dbaa4db5..059e393912 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -59,8 +59,8 @@ namespace esphome { class Application { public: - void pre_setup(const std::string &name, const std::string &friendly_name, const char *comment, - const char *compilation_time, bool name_add_mac_suffix) { + void pre_setup(const std::string &name, const std::string &friendly_name, const std::string &area, + const char *comment, const char *compilation_time, bool name_add_mac_suffix) { arch_init(); this->name_add_mac_suffix_ = name_add_mac_suffix; if (name_add_mac_suffix) { @@ -74,6 +74,7 @@ class Application { this->name_ = name; this->friendly_name_ = friendly_name; } + this->area_ = area; this->comment_ = comment; this->compilation_time_ = compilation_time; } @@ -160,6 +161,10 @@ class Application { /// Get the friendly name of this Application set by pre_setup(). const std::string &get_friendly_name() const { return this->friendly_name_; } + + /// Get the area of this Application set by pre_setup(). + const std::string &get_area() const { return this->area_; } + /// Get the comment of this Application set by pre_setup(). std::string get_comment() const { return this->comment_; } @@ -395,6 +400,7 @@ class Application { std::string name_; std::string friendly_name_; + std::string area_; const char *comment_{nullptr}; const char *compilation_time_{nullptr}; bool name_add_mac_suffix_; diff --git a/esphome/core/config.py b/esphome/core/config.py index 1625644092..e4a1fdcafa 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -8,6 +8,7 @@ import esphome.config_validation as cv from esphome import automation from esphome.const import ( CONF_ARDUINO_VERSION, + CONF_AREA, CONF_BOARD, CONF_BOARD_FLASH_MODE, CONF_BUILD_PATH, @@ -126,6 +127,7 @@ CONFIG_SCHEMA = cv.All( { cv.Required(CONF_NAME): cv.valid_name, cv.Optional(CONF_FRIENDLY_NAME, ""): cv.string, + cv.Optional(CONF_AREA, ""): cv.string, cv.Optional(CONF_COMMENT): cv.string, cv.Required(CONF_BUILD_PATH): cv.string, cv.Optional(CONF_PLATFORMIO_OPTIONS, default={}): cv.Schema( @@ -350,6 +352,7 @@ async def to_code(config): cg.App.pre_setup( config[CONF_NAME], config[CONF_FRIENDLY_NAME], + config[CONF_AREA], config.get(CONF_COMMENT, ""), cg.RawExpression('__DATE__ ", " __TIME__'), config[CONF_NAME_ADD_MAC_SUFFIX], diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 598b08063b..d4187d4c08 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -78,6 +78,7 @@ #define USE_VOICE_ASSISTANT #define USE_MICROPHONE #define USE_SPEAKER +#define USE_SPI #ifdef USE_ARDUINO #define USE_ARDUINO_VERSION_CODE VERSION_CODE(2, 0, 5) @@ -97,9 +98,7 @@ #define USE_HTTP_REQUEST_ESP8266_HTTPS #define USE_SOCKET_IMPL_LWIP_TCP -#ifdef USE_LIBRETINY -#define USE_SOCKET_IMPL_LWIP_SOCKETS -#endif +#define USE_SPI // Dummy firmware payload for shelly_dimmer #define USE_SHD_FIRMWARE_MAJOR_VERSION 56 @@ -112,6 +111,11 @@ #ifdef USE_RP2040 #define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 0) #define USE_SOCKET_IMPL_LWIP_TCP +#define USE_SPI +#endif + +#ifdef USE_LIBRETINY +#define USE_SOCKET_IMPL_LWIP_SOCKETS #endif #ifdef USE_HOST diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index cc53f491f5..4b3716e223 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -35,7 +35,7 @@ async def gpio_pin_expression(conf): return None from esphome import pins - for key, (func, _) in pins.PIN_SCHEMA_REGISTRY.items(): + for key, (func, _, _) in pins.PIN_SCHEMA_REGISTRY.items(): if key in conf: return await coroutine(func)(conf) return await coroutine(pins.PIN_SCHEMA_REGISTRY[CORE.target_platform][0])(conf) diff --git a/esphome/pins.py b/esphome/pins.py index cec715b922..0035bea4f0 100644 --- a/esphome/pins.py +++ b/esphome/pins.py @@ -11,10 +11,10 @@ from esphome.const import ( CONF_PULLUP, CONF_IGNORE_STRAPPING_WARNING, ) -from esphome.util import SimpleRegistry +from esphome.util import PinRegistry from esphome.core import CORE -PIN_SCHEMA_REGISTRY = SimpleRegistry() +PIN_SCHEMA_REGISTRY = PinRegistry() def _set_mode(value, default_mode): diff --git a/esphome/util.py b/esphome/util.py index 480618aca0..d9c8502e0e 100644 --- a/esphome/util.py +++ b/esphome/util.py @@ -57,6 +57,32 @@ class SimpleRegistry(dict): return decorator +def _final_validate(parent_id_key, fun): + def validator(fconf, pin_config): + import esphome.config_validation as cv + + parent_path = fconf.get_path_for_id(pin_config[parent_id_key])[:-1] + parent_config = fconf.get_config_for_path(parent_path) + + pin_path = fconf.get_path_for_id(pin_config[const.CONF_ID])[:-1] + with cv.prepend_path([cv.ROOT_CONFIG_PATH] + pin_path): + fun(pin_config, parent_config) + + return validator + + +class PinRegistry(dict): + def register(self, name, schema, final_validate=None): + if final_validate is not None: + final_validate = _final_validate(name, final_validate) + + def decorator(fun): + self[name] = (fun, schema, final_validate) + return fun + + return decorator + + def safe_print(message="", end="\n"): from esphome.core import CORE @@ -196,7 +222,7 @@ def run_external_command( try: sys.argv = list(cmd) sys.exit = mock_exit - return func() or 0 + retval = func() or 0 except KeyboardInterrupt: # pylint: disable=try-except-raise raise except SystemExit as err: @@ -212,9 +238,10 @@ def run_external_command( sys.stdout = orig_stdout sys.stderr = orig_stderr - if capture_stdout: - # pylint: disable=lost-exception - return cap_stdout.getvalue() + if capture_stdout: + return cap_stdout.getvalue() + + return retval def run_external_process(*cmd, **kwargs): diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index 8a03c431a7..3d3fa8c5b4 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -18,6 +18,7 @@ from esphome.core import ( MACAddress, TimePeriod, DocumentRange, + CORE, ) from esphome.helpers import add_class_to_obj from esphome.util import OrderedDict, filter_yaml_files @@ -240,7 +241,18 @@ class ESPHomeLoader(yaml.SafeLoader): @_add_data_ref def construct_secret(self, node): - secrets = _load_yaml_internal(self._rel_path(SECRET_YAML)) + try: + secrets = _load_yaml_internal(self._rel_path(SECRET_YAML)) + except EsphomeError as e: + if self.name == CORE.config_path: + raise e + try: + main_config_dir = os.path.dirname(CORE.config_path) + main_secret_yml = os.path.join(main_config_dir, SECRET_YAML) + secrets = _load_yaml_internal(main_secret_yml) + except EsphomeError as er: + raise EsphomeError(f"{e}\n{er}") from er + if node.value not in secrets: raise yaml.MarkedYAMLError( f"Secret '{node.value}' not defined", node.start_mark diff --git a/requirements.txt b/requirements.txt index 630802ee74..b9b6708ff2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,14 +3,14 @@ PyYAML==6.0.1 paho-mqtt==1.6.1 colorama==0.4.6 tornado==6.3.3 -tzlocal==5.1 # from time +tzlocal==5.2 # from time tzdata>=2021.1 # from time pyserial==3.5 platformio==6.1.11 # When updating platformio, also update Dockerfile esptool==4.6.2 click==8.1.7 esphome-dashboard==20230904.0 -aioesphomeapi==18.0.12 +aioesphomeapi==18.2.1 zeroconf==0.119.0 # esp-idf requires this, but doesn't bundle it by default diff --git a/requirements_test.txt b/requirements_test.txt index f8c66b5ea4..fade3cda3e 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,11 +1,11 @@ pylint==2.17.6 flake8==6.1.0 # also change in .pre-commit-config.yaml when updating -black==23.10.0 # also change in .pre-commit-config.yaml when updating +black==23.10.1 # also change in .pre-commit-config.yaml when updating pyupgrade==3.15.0 # also change in .pre-commit-config.yaml when updating pre-commit # Unit tests -pytest==7.4.2 +pytest==7.4.3 pytest-cov==4.1.0 pytest-mock==3.12.0 pytest-asyncio==0.21.1 diff --git a/tests/dummy_main.cpp b/tests/dummy_main.cpp index 236b9f5fc2..da5c6d10d0 100644 --- a/tests/dummy_main.cpp +++ b/tests/dummy_main.cpp @@ -12,7 +12,7 @@ using namespace esphome; void setup() { - App.pre_setup("livingroom", "LivingRoom", "comment", __DATE__ ", " __TIME__, false); + App.pre_setup("livingroom", "LivingRoom", "LivingRoomArea", "comment", __DATE__ ", " __TIME__, false); auto *log = new logger::Logger(115200, 512); // NOLINT log->pre_setup(); log->set_uart_selection(logger::UART_SELECTION_UART0); diff --git a/tests/test1.yaml b/tests/test1.yaml index 7ee1baaba5..f894f3fbe1 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -220,6 +220,16 @@ uart: baud_rate: 256000 parity: NONE stop_bits: 1 + - id: dfrobot_mmwave_uart + tx_pin: 14 + rx_pin: 27 + baud_rate: 115200 + - id: ld2420_uart + tx_pin: 17 + rx_pin: 16 + baud_rate: 115200 + parity: NONE + stop_bits: 1 - id: gcja5_uart rx_pin: GPIO10 parity: EVEN @@ -340,6 +350,14 @@ optolink: device_info: Device Info state: Component state +micronova: + enable_rx_pin: 4 + uart_id: uart_0 + +dfrobot_sen0395: + - id: mmwave + uart_id: dfrobot_mmwave_uart + sensor: - platform: pmwcs3 i2c_id: i2c_bus @@ -1441,6 +1459,9 @@ sensor: still_energy: name: g8 still energy + - platform: ld2420 + moving_distance: + name: "Moving distance (cm)" - platform: sen21231 name: "Person Sensor" i2c_id: i2c_bus @@ -1516,6 +1537,24 @@ sensor: field_strength_z: name: "Magnet Z" id: magnet_z + - platform: micronova + room_temperature: + name: Room Temperature + fumes_temperature: + name: Fumes Temperature + water_temperature: + name: Water temperature + water_pressure: + name: Water pressure + stove_power: + name: Stove Power + fan_speed: + fan_rpm_offset: 240 + name: Fan RPM + memory_address_sensor: + memory_location: 0x20 + memory_address: 0x7d + name: Adres sensor esp32_touch: setup_mode: false @@ -1834,6 +1873,9 @@ binary_sensor: - platform: qwiic_pir i2c_id: i2c_bus name: "Qwiic PIR Motion Sensor" + - platform: dfrobot_sen0395 + id: mmwave_detected_uart + dfrobot_sen0395_id: mmwave pca9685: frequency: 500 @@ -2707,6 +2749,9 @@ switch: name: "control ld2410 engineering mode" bluetooth: name: "control ld2410 bluetooth" + - platform: micronova + stove: + name: Stove on/off fan: - platform: binary @@ -3193,6 +3238,7 @@ pcf8574: pca9554: - id: pca9554_hub + pin_count: 8 address: 0x3F i2c_id: i2c_bus @@ -3347,7 +3393,12 @@ rtttl: canbus: - platform: mcp2515 id: mcp2515_can - cs_pin: GPIO17 + cs_pin: + pca9554: pca9554_hub + number: 7 + mode: + output: true + inverted: true can_id: 4 bit_rate: 50kbps on_frame: @@ -3501,6 +3552,12 @@ number: name: g8 move threshold still_threshold: name: g8 still threshold + - platform: micronova + thermostat_temperature: + name: Micronova Thermostaat + step: 1 + power_level: + name: Micronova Power level select: - platform: template @@ -3593,6 +3650,25 @@ button: name: Midea Power Inverse on_press: midea_ac.power_toggle: + - platform: template + name: Update Mmwave Sensor Settings + on_press: + - dfrobot_sen0395.settings: + id: mmwave + factory_reset: true + detection_segments: + - [0cm, 5m] + - 600cm + - !lambda |- + return 7; + output_latency: + delay_after_detect: 0s + delay_after_disappear: 0s + sensitivity: 6 + - platform: template + name: Reset Mmwave Sensor + on_press: + - dfrobot_sen0395.reset: - platform: template name: Poller component suspend test on_press: @@ -3620,11 +3696,21 @@ button: uart_id: uart_0 name: UART button data: "Pressed\r\n" + - platform: micronova + custom_button: + name: Custom Micronova Button + memory_location: 0xA0 + memory_address: 0x7D + memory_data: 0x0F ld2410: id: my_ld2410 uart_id: ld2410_uart +ld2420: + id: my_ld2420 + uart_id: ld2420_uart + lcd_menu: display_id: my_lcd_gpio mark_back: 0x5e @@ -3724,3 +3810,4 @@ alarm_control_panel: then: - lambda: !lambda |- ESP_LOGD("TEST", "State change %s", alarm_control_panel_state_to_string(id(alarmcontrolpanel1)->get_state())); +