mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +00:00 
			
		
		
		
	Merge branch 'dev' into binary_sensor_gpio_polling
This commit is contained in:
		
							
								
								
									
										23
									
								
								.github/workflows/lock.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										23
									
								
								.github/workflows/lock.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,28 +1,11 @@ | ||||
| --- | ||||
| name: Lock | ||||
| name: Lock closed issues and PRs | ||||
|  | ||||
| on: | ||||
|   schedule: | ||||
|     - cron: "30 0 * * *" | ||||
|     - cron: "30 0 * * *"  # Run daily at 00:30 UTC | ||||
|   workflow_dispatch: | ||||
|  | ||||
| permissions: | ||||
|   issues: write | ||||
|   pull-requests: write | ||||
|  | ||||
| concurrency: | ||||
|   group: lock | ||||
|  | ||||
| jobs: | ||||
|   lock: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: dessant/lock-threads@v5.0.1 | ||||
|         with: | ||||
|           pr-inactive-days: "1" | ||||
|           pr-lock-reason: "" | ||||
|           exclude-any-pr-labels: keep-open | ||||
|  | ||||
|           issue-inactive-days: "7" | ||||
|           issue-lock-reason: "" | ||||
|           exclude-any-issue-labels: keep-open | ||||
|     uses: esphome/workflows/.github/workflows/lock.yml@main | ||||
|   | ||||
| @@ -12,7 +12,7 @@ repos: | ||||
|       # Run the formatter. | ||||
|       - id: ruff-format | ||||
|   - repo: https://github.com/PyCQA/flake8 | ||||
|     rev: 7.2.0 | ||||
|     rev: 7.3.0 | ||||
|     hooks: | ||||
|       - id: flake8 | ||||
|         additional_dependencies: | ||||
|   | ||||
| @@ -146,6 +146,7 @@ esphome/components/esp32_ble_client/* @jesserockz | ||||
| esphome/components/esp32_ble_server/* @Rapsssito @clydebarrow @jesserockz | ||||
| esphome/components/esp32_camera_web_server/* @ayufan | ||||
| esphome/components/esp32_can/* @Sympatron | ||||
| esphome/components/esp32_hosted/* @swoboda1337 | ||||
| esphome/components/esp32_improv/* @jesserockz | ||||
| esphome/components/esp32_rmt/* @jesserockz | ||||
| esphome/components/esp32_rmt_led_strip/* @jesserockz | ||||
| @@ -323,6 +324,7 @@ esphome/components/one_wire/* @ssieb | ||||
| esphome/components/online_image/* @clydebarrow @guillempages | ||||
| esphome/components/opentherm/* @olegtarasov | ||||
| esphome/components/openthread/* @mrene | ||||
| esphome/components/opt3001/* @ccutrer | ||||
| esphome/components/ota/* @esphome/core | ||||
| esphome/components/output/* @esphome/core | ||||
| esphome/components/packet_transport/* @clydebarrow | ||||
| @@ -490,7 +492,7 @@ esphome/components/vbus/* @ssieb | ||||
| esphome/components/veml3235/* @kbx81 | ||||
| esphome/components/veml7700/* @latonita | ||||
| esphome/components/version/* @esphome/core | ||||
| esphome/components/voice_assistant/* @jesserockz | ||||
| esphome/components/voice_assistant/* @jesserockz @kahrendt | ||||
| esphome/components/wake_on_lan/* @clydebarrow @willwill2will54 | ||||
| esphome/components/watchdog/* @oarcher | ||||
| esphome/components/waveshare_epaper/* @clydebarrow | ||||
|   | ||||
| @@ -14,8 +14,8 @@ from esphome.const import ( | ||||
|     CONF_WEB_SERVER, | ||||
| ) | ||||
| from esphome.core import CORE, coroutine_with_priority | ||||
| from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity | ||||
| from esphome.cpp_generator import MockObjClass | ||||
| from esphome.cpp_helpers import setup_entity | ||||
|  | ||||
| CODEOWNERS = ["@grahambrown11", "@hwstar"] | ||||
| IS_PLATFORM_COMPONENT = True | ||||
| @@ -149,6 +149,9 @@ _ALARM_CONTROL_PANEL_SCHEMA = ( | ||||
| ) | ||||
|  | ||||
|  | ||||
| _ALARM_CONTROL_PANEL_SCHEMA.add_extra(entity_duplicate_validator("alarm_control_panel")) | ||||
|  | ||||
|  | ||||
| def alarm_control_panel_schema( | ||||
|     class_: MockObjClass, | ||||
|     *, | ||||
| @@ -190,7 +193,7 @@ ALARM_CONTROL_PANEL_CONDITION_SCHEMA = maybe_simple_id( | ||||
|  | ||||
|  | ||||
| async def setup_alarm_control_panel_core_(var, config): | ||||
|     await setup_entity(var, config) | ||||
|     await setup_entity(var, config, "alarm_control_panel") | ||||
|     for conf in config.get(CONF_ON_STATE, []): | ||||
|         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) | ||||
|         await automation.build_automation(trigger, [], conf) | ||||
|   | ||||
| @@ -188,6 +188,17 @@ message DeviceInfoRequest { | ||||
|   // Empty | ||||
| } | ||||
|  | ||||
| message AreaInfo { | ||||
|   uint32 area_id = 1; | ||||
|   string name = 2; | ||||
| } | ||||
|  | ||||
| message DeviceInfo { | ||||
|   uint32 device_id = 1; | ||||
|   string name = 2; | ||||
|   uint32 area_id = 3; | ||||
| } | ||||
|  | ||||
| message DeviceInfoResponse { | ||||
|   option (id) = 10; | ||||
|   option (source) = SOURCE_SERVER; | ||||
| @@ -236,6 +247,12 @@ message DeviceInfoResponse { | ||||
|  | ||||
|   // Supports receiving and saving api encryption key | ||||
|   bool api_encryption_supported = 19; | ||||
|  | ||||
|   repeated DeviceInfo devices = 20; | ||||
|   repeated AreaInfo areas = 21; | ||||
|  | ||||
|   // Top-level area info to phase out suggested_area | ||||
|   AreaInfo area = 22; | ||||
| } | ||||
|  | ||||
| message ListEntitiesRequest { | ||||
| @@ -280,6 +297,7 @@ message ListEntitiesBinarySensorResponse { | ||||
|   bool disabled_by_default = 7; | ||||
|   string icon = 8; | ||||
|   EntityCategory entity_category = 9; | ||||
|   uint32 device_id = 10; | ||||
| } | ||||
| message BinarySensorStateResponse { | ||||
|   option (id) = 21; | ||||
| @@ -315,6 +333,7 @@ message ListEntitiesCoverResponse { | ||||
|   string icon = 10; | ||||
|   EntityCategory entity_category = 11; | ||||
|   bool supports_stop = 12; | ||||
|   uint32 device_id = 13; | ||||
| } | ||||
|  | ||||
| enum LegacyCoverState { | ||||
| @@ -388,6 +407,7 @@ message ListEntitiesFanResponse { | ||||
|   string icon = 10; | ||||
|   EntityCategory entity_category = 11; | ||||
|   repeated string supported_preset_modes = 12; | ||||
|   uint32 device_id = 13; | ||||
| } | ||||
| enum FanSpeed { | ||||
|   FAN_SPEED_LOW = 0; | ||||
| @@ -471,6 +491,7 @@ message ListEntitiesLightResponse { | ||||
|   bool disabled_by_default = 13; | ||||
|   string icon = 14; | ||||
|   EntityCategory entity_category = 15; | ||||
|   uint32 device_id = 16; | ||||
| } | ||||
| message LightStateResponse { | ||||
|   option (id) = 24; | ||||
| @@ -563,6 +584,7 @@ message ListEntitiesSensorResponse { | ||||
|   SensorLastResetType legacy_last_reset_type = 11; | ||||
|   bool disabled_by_default = 12; | ||||
|   EntityCategory entity_category = 13; | ||||
|   uint32 device_id = 14; | ||||
| } | ||||
| message SensorStateResponse { | ||||
|   option (id) = 25; | ||||
| @@ -595,6 +617,7 @@ message ListEntitiesSwitchResponse { | ||||
|   bool disabled_by_default = 7; | ||||
|   EntityCategory entity_category = 8; | ||||
|   string device_class = 9; | ||||
|   uint32 device_id = 10; | ||||
| } | ||||
| message SwitchStateResponse { | ||||
|   option (id) = 26; | ||||
| @@ -632,6 +655,7 @@ message ListEntitiesTextSensorResponse { | ||||
|   bool disabled_by_default = 6; | ||||
|   EntityCategory entity_category = 7; | ||||
|   string device_class = 8; | ||||
|   uint32 device_id = 9; | ||||
| } | ||||
| message TextSensorStateResponse { | ||||
|   option (id) = 27; | ||||
| @@ -814,6 +838,7 @@ message ListEntitiesCameraResponse { | ||||
|   bool disabled_by_default = 5; | ||||
|   string icon = 6; | ||||
|   EntityCategory entity_category = 7; | ||||
|   uint32 device_id = 8; | ||||
| } | ||||
|  | ||||
| message CameraImageResponse { | ||||
| @@ -916,6 +941,7 @@ message ListEntitiesClimateResponse { | ||||
|   bool supports_target_humidity = 23; | ||||
|   float visual_min_humidity = 24; | ||||
|   float visual_max_humidity = 25; | ||||
|   uint32 device_id = 26; | ||||
| } | ||||
| message ClimateStateResponse { | ||||
|   option (id) = 47; | ||||
| @@ -999,6 +1025,7 @@ message ListEntitiesNumberResponse { | ||||
|   string unit_of_measurement = 11; | ||||
|   NumberMode mode = 12; | ||||
|   string device_class = 13; | ||||
|   uint32 device_id = 14; | ||||
| } | ||||
| message NumberStateResponse { | ||||
|   option (id) = 50; | ||||
| @@ -1039,6 +1066,7 @@ message ListEntitiesSelectResponse { | ||||
|   repeated string options = 6; | ||||
|   bool disabled_by_default = 7; | ||||
|   EntityCategory entity_category = 8; | ||||
|   uint32 device_id = 9; | ||||
| } | ||||
| message SelectStateResponse { | ||||
|   option (id) = 53; | ||||
| @@ -1081,6 +1109,7 @@ message ListEntitiesSirenResponse { | ||||
|   bool supports_duration = 8; | ||||
|   bool supports_volume = 9; | ||||
|   EntityCategory entity_category = 10; | ||||
|   uint32 device_id = 11; | ||||
| } | ||||
| message SirenStateResponse { | ||||
|   option (id) = 56; | ||||
| @@ -1144,6 +1173,7 @@ message ListEntitiesLockResponse { | ||||
|  | ||||
|   // Not yet implemented: | ||||
|   string code_format = 11; | ||||
|   uint32 device_id = 12; | ||||
| } | ||||
| message LockStateResponse { | ||||
|   option (id) = 59; | ||||
| @@ -1183,6 +1213,7 @@ message ListEntitiesButtonResponse { | ||||
|   bool disabled_by_default = 6; | ||||
|   EntityCategory entity_category = 7; | ||||
|   string device_class = 8; | ||||
|   uint32 device_id = 9; | ||||
| } | ||||
| message ButtonCommandRequest { | ||||
|   option (id) = 62; | ||||
| @@ -1238,6 +1269,8 @@ message ListEntitiesMediaPlayerResponse { | ||||
|   bool supports_pause = 8; | ||||
|  | ||||
|   repeated MediaPlayerSupportedFormat supported_formats = 9; | ||||
|  | ||||
|   uint32 device_id = 10; | ||||
| } | ||||
| message MediaPlayerStateResponse { | ||||
|   option (id) = 64; | ||||
| @@ -1778,6 +1811,7 @@ message ListEntitiesAlarmControlPanelResponse { | ||||
|   uint32 supported_features = 8; | ||||
|   bool requires_code = 9; | ||||
|   bool requires_code_to_arm = 10; | ||||
|   uint32 device_id = 11; | ||||
| } | ||||
|  | ||||
| message AlarmControlPanelStateResponse { | ||||
| @@ -1823,6 +1857,7 @@ message ListEntitiesTextResponse { | ||||
|   uint32 max_length = 9; | ||||
|   string pattern = 10; | ||||
|   TextMode mode = 11; | ||||
|   uint32 device_id = 12; | ||||
| } | ||||
| message TextStateResponse { | ||||
|   option (id) = 98; | ||||
| @@ -1863,6 +1898,7 @@ message ListEntitiesDateResponse { | ||||
|   string icon = 5; | ||||
|   bool disabled_by_default = 6; | ||||
|   EntityCategory entity_category = 7; | ||||
|   uint32 device_id = 8; | ||||
| } | ||||
| message DateStateResponse { | ||||
|   option (id) = 101; | ||||
| @@ -1906,6 +1942,7 @@ message ListEntitiesTimeResponse { | ||||
|   string icon = 5; | ||||
|   bool disabled_by_default = 6; | ||||
|   EntityCategory entity_category = 7; | ||||
|   uint32 device_id = 8; | ||||
| } | ||||
| message TimeStateResponse { | ||||
|   option (id) = 104; | ||||
| @@ -1952,6 +1989,7 @@ message ListEntitiesEventResponse { | ||||
|   string device_class = 8; | ||||
|  | ||||
|   repeated string event_types = 9; | ||||
|   uint32 device_id = 10; | ||||
| } | ||||
| message EventResponse { | ||||
|   option (id) = 108; | ||||
| @@ -1983,6 +2021,7 @@ message ListEntitiesValveResponse { | ||||
|   bool assumed_state = 9; | ||||
|   bool supports_position = 10; | ||||
|   bool supports_stop = 11; | ||||
|   uint32 device_id = 12; | ||||
| } | ||||
|  | ||||
| enum ValveOperation { | ||||
| @@ -2029,6 +2068,7 @@ message ListEntitiesDateTimeResponse { | ||||
|   string icon = 5; | ||||
|   bool disabled_by_default = 6; | ||||
|   EntityCategory entity_category = 7; | ||||
|   uint32 device_id = 8; | ||||
| } | ||||
| message DateTimeStateResponse { | ||||
|   option (id) = 113; | ||||
| @@ -2069,6 +2109,7 @@ message ListEntitiesUpdateResponse { | ||||
|   bool disabled_by_default = 6; | ||||
|   EntityCategory entity_category = 7; | ||||
|   string device_class = 8; | ||||
|   uint32 device_id = 9; | ||||
| } | ||||
| message UpdateStateResponse { | ||||
|   option (id) = 117; | ||||
|   | ||||
| @@ -28,8 +28,19 @@ | ||||
| namespace esphome { | ||||
| namespace api { | ||||
|  | ||||
| // Read a maximum of 5 messages per loop iteration to prevent starving other components. | ||||
| // This is a balance between API responsiveness and allowing other components to run. | ||||
| // Since each message could contain multiple protobuf messages when using packet batching, | ||||
| // this limits the number of messages processed, not the number of TCP packets. | ||||
| static constexpr uint8_t MAX_MESSAGES_PER_LOOP = 5; | ||||
| static constexpr uint8_t MAX_PING_RETRIES = 60; | ||||
| static constexpr uint16_t PING_RETRY_INTERVAL = 1000; | ||||
| static constexpr uint32_t KEEPALIVE_DISCONNECT_TIMEOUT = (KEEPALIVE_TIMEOUT_MS * 5) / 2; | ||||
|  | ||||
| static const char *const TAG = "api.connection"; | ||||
| #ifdef USE_ESP32_CAMERA | ||||
| static const int ESP32_CAMERA_STOP_STREAM = 5000; | ||||
| #endif | ||||
|  | ||||
| APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent) | ||||
|     : parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) { | ||||
| @@ -84,16 +95,6 @@ APIConnection::~APIConnection() { | ||||
| } | ||||
|  | ||||
| void APIConnection::loop() { | ||||
|   if (this->remove_) | ||||
|     return; | ||||
|  | ||||
|   if (!network::is_connected()) { | ||||
|     // when network is disconnected force disconnect immediately | ||||
|     // don't wait for timeout | ||||
|     this->on_fatal_error(); | ||||
|     ESP_LOGW(TAG, "%s: Network unavailable; disconnecting", this->get_client_combined_info().c_str()); | ||||
|     return; | ||||
|   } | ||||
|   if (this->next_close_) { | ||||
|     // requested a disconnect | ||||
|     this->helper_->close(); | ||||
| @@ -109,12 +110,16 @@ void APIConnection::loop() { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   const uint32_t now = App.get_loop_component_start_time(); | ||||
|   // Check if socket has data ready before attempting to read | ||||
|   if (this->helper_->is_socket_ready()) { | ||||
|     // Read up to MAX_MESSAGES_PER_LOOP messages per loop to improve throughput | ||||
|     for (uint8_t message_count = 0; message_count < MAX_MESSAGES_PER_LOOP; message_count++) { | ||||
|       ReadPacketBuffer buffer; | ||||
|       err = this->helper_->read_packet(&buffer); | ||||
|       if (err == APIError::WOULD_BLOCK) { | ||||
|       // pass | ||||
|         // No more data available | ||||
|         break; | ||||
|       } else if (err != APIError::OK) { | ||||
|         on_fatal_error(); | ||||
|         if (err == APIError::SOCKET_READ_FAILED && errno == ECONNRESET) { | ||||
| @@ -127,7 +132,7 @@ void APIConnection::loop() { | ||||
|         } | ||||
|         return; | ||||
|       } else { | ||||
|       this->last_traffic_ = App.get_loop_component_start_time(); | ||||
|         this->last_traffic_ = now; | ||||
|         // read a packet | ||||
|         if (buffer.data_len > 0) { | ||||
|           this->read_message(buffer.data_len, buffer.type, &buffer.container[buffer.data_offset]); | ||||
| @@ -138,24 +143,23 @@ void APIConnection::loop() { | ||||
|           return; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // Process deferred batch if scheduled | ||||
|   if (this->deferred_batch_.batch_scheduled && | ||||
|       App.get_loop_component_start_time() - this->deferred_batch_.batch_start_time >= this->get_batch_delay_ms_()) { | ||||
|       now - this->deferred_batch_.batch_start_time >= this->get_batch_delay_ms_()) { | ||||
|     this->process_batch_(); | ||||
|   } | ||||
|  | ||||
|   if (!this->list_entities_iterator_.completed()) | ||||
|   if (!this->list_entities_iterator_.completed()) { | ||||
|     this->list_entities_iterator_.advance(); | ||||
|   if (!this->initial_state_iterator_.completed() && this->list_entities_iterator_.completed()) | ||||
|   } else if (!this->initial_state_iterator_.completed()) { | ||||
|     this->initial_state_iterator_.advance(); | ||||
|   } | ||||
|  | ||||
|   static uint8_t max_ping_retries = 60; | ||||
|   static uint16_t ping_retry_interval = 1000; | ||||
|   const uint32_t now = App.get_loop_component_start_time(); | ||||
|   if (this->sent_ping_) { | ||||
|     // Disconnect if not responded within 2.5*keepalive | ||||
|     if (now - this->last_traffic_ > (KEEPALIVE_TIMEOUT_MS * 5) / 2) { | ||||
|     if (now - this->last_traffic_ > KEEPALIVE_DISCONNECT_TIMEOUT) { | ||||
|       on_fatal_error(); | ||||
|       ESP_LOGW(TAG, "%s is unresponsive; disconnecting", this->get_client_combined_info().c_str()); | ||||
|     } | ||||
| @@ -163,17 +167,15 @@ void APIConnection::loop() { | ||||
|     ESP_LOGVV(TAG, "Sending keepalive PING"); | ||||
|     this->sent_ping_ = this->send_message(PingRequest()); | ||||
|     if (!this->sent_ping_) { | ||||
|       this->next_ping_retry_ = now + ping_retry_interval; | ||||
|       this->next_ping_retry_ = now + PING_RETRY_INTERVAL; | ||||
|       this->ping_retries_++; | ||||
|       std::string warn_str = str_sprintf("%s: Sending keepalive failed %u time(s);", | ||||
|                                          this->get_client_combined_info().c_str(), this->ping_retries_); | ||||
|       if (this->ping_retries_ >= max_ping_retries) { | ||||
|       if (this->ping_retries_ >= MAX_PING_RETRIES) { | ||||
|         on_fatal_error(); | ||||
|         ESP_LOGE(TAG, "%s disconnecting", warn_str.c_str()); | ||||
|         ESP_LOGE(TAG, "%s: Ping failed %u times", this->get_client_combined_info().c_str(), this->ping_retries_); | ||||
|       } else if (this->ping_retries_ >= 10) { | ||||
|         ESP_LOGW(TAG, "%s retrying in %u ms", warn_str.c_str(), ping_retry_interval); | ||||
|         ESP_LOGW(TAG, "%s: Ping retry %u", this->get_client_combined_info().c_str(), this->ping_retries_); | ||||
|       } else { | ||||
|         ESP_LOGD(TAG, "%s retrying in %u ms", warn_str.c_str(), ping_retry_interval); | ||||
|         ESP_LOGD(TAG, "%s: Ping retry %u", this->get_client_combined_info().c_str(), this->ping_retries_); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| @@ -197,22 +199,20 @@ void APIConnection::loop() { | ||||
|     // bool done = 3; | ||||
|     buffer.encode_bool(3, done); | ||||
|  | ||||
|     bool success = this->send_buffer(buffer, 44); | ||||
|     bool success = this->send_buffer(buffer, CameraImageResponse::MESSAGE_TYPE); | ||||
|  | ||||
|     if (success) { | ||||
|       this->image_reader_.consume_data(to_send); | ||||
|     } | ||||
|     if (success && done) { | ||||
|       if (done) { | ||||
|         this->image_reader_.return_image(); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| #endif | ||||
|  | ||||
|   if (state_subs_at_ != -1) { | ||||
|   if (state_subs_at_ >= 0) { | ||||
|     const auto &subs = this->parent_->get_state_subs(); | ||||
|     if (state_subs_at_ >= (int) subs.size()) { | ||||
|       state_subs_at_ = -1; | ||||
|     } else { | ||||
|     if (state_subs_at_ < static_cast<int>(subs.size())) { | ||||
|       auto &it = subs[state_subs_at_]; | ||||
|       SubscribeHomeAssistantStateResponse resp; | ||||
|       resp.entity_id = it.entity_id; | ||||
| @@ -221,6 +221,8 @@ void APIConnection::loop() { | ||||
|       if (this->send_message(resp)) { | ||||
|         state_subs_at_++; | ||||
|       } | ||||
|     } else { | ||||
|       state_subs_at_ = -1; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -274,6 +276,11 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint16_t mes | ||||
|   // Encode directly into buffer | ||||
|   msg.encode(buffer); | ||||
|  | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   // Log the message for VV debugging | ||||
|   conn->log_send_message_(msg.message_name(), msg.dump()); | ||||
| #endif | ||||
|  | ||||
|   // Calculate actual encoded size (not including header that was already added) | ||||
|   size_t actual_payload_size = shared_buf.size() - size_before_encode; | ||||
|  | ||||
| @@ -1430,7 +1437,7 @@ void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRe | ||||
|  | ||||
| #ifdef USE_EVENT | ||||
| void APIConnection::send_event(event::Event *event, const std::string &event_type) { | ||||
|   this->schedule_message_(event, MessageCreator(event_type, EventResponse::MESSAGE_TYPE), EventResponse::MESSAGE_TYPE); | ||||
|   this->schedule_message_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE); | ||||
| } | ||||
| void APIConnection::send_event_info(event::Event *event) { | ||||
|   this->schedule_message_(event, &APIConnection::try_send_event_info, ListEntitiesEventResponse::MESSAGE_TYPE); | ||||
| @@ -1619,6 +1626,23 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { | ||||
| #endif | ||||
| #ifdef USE_API_NOISE | ||||
|   resp.api_encryption_supported = true; | ||||
| #endif | ||||
| #ifdef USE_DEVICES | ||||
|   for (auto const &device : App.get_devices()) { | ||||
|     DeviceInfo device_info; | ||||
|     device_info.device_id = device->get_device_id(); | ||||
|     device_info.name = device->get_name(); | ||||
|     device_info.area_id = device->get_area_id(); | ||||
|     resp.devices.push_back(device_info); | ||||
|   } | ||||
| #endif | ||||
| #ifdef USE_AREAS | ||||
|   for (auto const &area : App.get_areas()) { | ||||
|     AreaInfo area_info; | ||||
|     area_info.area_id = area->get_area_id(); | ||||
|     area_info.name = area->get_name(); | ||||
|     resp.areas.push_back(area_info); | ||||
|   } | ||||
| #endif | ||||
|   return resp; | ||||
| } | ||||
| @@ -1768,7 +1792,8 @@ void APIConnection::process_batch_() { | ||||
|     const auto &item = this->deferred_batch_.items[0]; | ||||
|  | ||||
|     // Let the creator calculate size and encode if it fits | ||||
|     uint16_t payload_size = item.creator(item.entity, this, std::numeric_limits<uint16_t>::max(), true); | ||||
|     uint16_t payload_size = | ||||
|         item.creator(item.entity, this, std::numeric_limits<uint16_t>::max(), true, item.message_type); | ||||
|  | ||||
|     if (payload_size > 0 && | ||||
|         this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, item.message_type)) { | ||||
| @@ -1818,7 +1843,7 @@ void APIConnection::process_batch_() { | ||||
|   for (const auto &item : this->deferred_batch_.items) { | ||||
|     // Try to encode message | ||||
|     // The creator will calculate overhead to determine if the message fits | ||||
|     uint16_t payload_size = item.creator(item.entity, this, remaining_size, false); | ||||
|     uint16_t payload_size = item.creator(item.entity, this, remaining_size, false, item.message_type); | ||||
|  | ||||
|     if (payload_size == 0) { | ||||
|       // Message won't fit, stop processing | ||||
| @@ -1881,22 +1906,24 @@ void APIConnection::process_batch_() { | ||||
| } | ||||
|  | ||||
| uint16_t APIConnection::MessageCreator::operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, | ||||
|                                                    bool is_single) const { | ||||
|   switch (message_type_) { | ||||
|     case 0:  // Function pointer | ||||
|       return data_.ptr(entity, conn, remaining_size, is_single); | ||||
|  | ||||
|                                                    bool is_single, uint16_t message_type) const { | ||||
|   if (has_tagged_string_ptr_()) { | ||||
|     // Handle string-based messages | ||||
|     switch (message_type) { | ||||
| #ifdef USE_EVENT | ||||
|       case EventResponse::MESSAGE_TYPE: { | ||||
|         auto *e = static_cast<event::Event *>(entity); | ||||
|       return APIConnection::try_send_event_response(e, *data_.string_ptr, conn, remaining_size, is_single); | ||||
|         return APIConnection::try_send_event_response(e, *get_string_ptr_(), conn, remaining_size, is_single); | ||||
|       } | ||||
| #endif | ||||
|  | ||||
|       default: | ||||
|         // Should not happen, return 0 to indicate no message | ||||
|         return 0; | ||||
|     } | ||||
|   } else { | ||||
|     // Function pointer case | ||||
|     return data_.ptr(entity, conn, remaining_size, is_single); | ||||
|   } | ||||
| } | ||||
|  | ||||
| uint16_t APIConnection::try_send_list_info_done(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, | ||||
|   | ||||
| @@ -301,6 +301,9 @@ class APIConnection : public APIServerConnection { | ||||
|     response.icon = entity->get_icon(); | ||||
|     response.disabled_by_default = entity->is_disabled_by_default(); | ||||
|     response.entity_category = static_cast<enums::EntityCategory>(entity->get_entity_category()); | ||||
| #ifdef USE_DEVICES | ||||
|     response.device_id = entity->get_device_id(); | ||||
| #endif | ||||
|   } | ||||
|  | ||||
|   // Helper function to fill common entity state fields | ||||
| @@ -480,55 +483,57 @@ class APIConnection : public APIServerConnection { | ||||
|   // Function pointer type for message encoding | ||||
|   using MessageCreatorPtr = uint16_t (*)(EntityBase *, APIConnection *, uint32_t remaining_size, bool is_single); | ||||
|  | ||||
|   // Optimized MessageCreator class using union dispatch | ||||
|   // Optimized MessageCreator class using tagged pointer | ||||
|   class MessageCreator { | ||||
|     // Ensure pointer alignment allows LSB tagging | ||||
|     static_assert(alignof(std::string *) > 1, "String pointer alignment must be > 1 for LSB tagging"); | ||||
|  | ||||
|    public: | ||||
|     // Constructor for function pointer (message_type = 0) | ||||
|     MessageCreator(MessageCreatorPtr ptr) : message_type_(0) { data_.ptr = ptr; } | ||||
|     // Constructor for function pointer | ||||
|     MessageCreator(MessageCreatorPtr ptr) { | ||||
|       // Function pointers are always aligned, so LSB is 0 | ||||
|       data_.ptr = ptr; | ||||
|     } | ||||
|  | ||||
|     // Constructor for string state capture | ||||
|     MessageCreator(const std::string &value, uint16_t msg_type) : message_type_(msg_type) { | ||||
|       data_.string_ptr = new std::string(value); | ||||
|     explicit MessageCreator(const std::string &str_value) { | ||||
|       // Allocate string and tag the pointer | ||||
|       auto *str = new std::string(str_value); | ||||
|       // Set LSB to 1 to indicate string pointer | ||||
|       data_.tagged = reinterpret_cast<uintptr_t>(str) | 1; | ||||
|     } | ||||
|  | ||||
|     // Destructor | ||||
|     ~MessageCreator() { | ||||
|       // Clean up string data for string-based message types | ||||
|       if (uses_string_data_()) { | ||||
|         delete data_.string_ptr; | ||||
|       if (has_tagged_string_ptr_()) { | ||||
|         delete get_string_ptr_(); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // Copy constructor | ||||
|     MessageCreator(const MessageCreator &other) : message_type_(other.message_type_) { | ||||
|       if (message_type_ == 0) { | ||||
|         data_.ptr = other.data_.ptr; | ||||
|       } else if (uses_string_data_()) { | ||||
|         data_.string_ptr = new std::string(*other.data_.string_ptr); | ||||
|     MessageCreator(const MessageCreator &other) { | ||||
|       if (other.has_tagged_string_ptr_()) { | ||||
|         auto *str = new std::string(*other.get_string_ptr_()); | ||||
|         data_.tagged = reinterpret_cast<uintptr_t>(str) | 1; | ||||
|       } else { | ||||
|         data_ = other.data_;  // For POD types | ||||
|         data_ = other.data_; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // Move constructor | ||||
|     MessageCreator(MessageCreator &&other) noexcept : data_(other.data_), message_type_(other.message_type_) { | ||||
|       other.message_type_ = 0;  // Reset other to function pointer type | ||||
|       other.data_.ptr = nullptr; | ||||
|     } | ||||
|     MessageCreator(MessageCreator &&other) noexcept : data_(other.data_) { other.data_.ptr = nullptr; } | ||||
|  | ||||
|     // Assignment operators (needed for batch deduplication) | ||||
|     MessageCreator &operator=(const MessageCreator &other) { | ||||
|       if (this != &other) { | ||||
|         // Clean up current string data if needed | ||||
|         if (uses_string_data_()) { | ||||
|           delete data_.string_ptr; | ||||
|         if (has_tagged_string_ptr_()) { | ||||
|           delete get_string_ptr_(); | ||||
|         } | ||||
|         // Copy new data | ||||
|         message_type_ = other.message_type_; | ||||
|         if (other.message_type_ == 0) { | ||||
|           data_.ptr = other.data_.ptr; | ||||
|         } else if (other.uses_string_data_()) { | ||||
|           data_.string_ptr = new std::string(*other.data_.string_ptr); | ||||
|         if (other.has_tagged_string_ptr_()) { | ||||
|           auto *str = new std::string(*other.get_string_ptr_()); | ||||
|           data_.tagged = reinterpret_cast<uintptr_t>(str) | 1; | ||||
|         } else { | ||||
|           data_ = other.data_; | ||||
|         } | ||||
| @@ -539,30 +544,35 @@ class APIConnection : public APIServerConnection { | ||||
|     MessageCreator &operator=(MessageCreator &&other) noexcept { | ||||
|       if (this != &other) { | ||||
|         // Clean up current string data if needed | ||||
|         if (uses_string_data_()) { | ||||
|           delete data_.string_ptr; | ||||
|         if (has_tagged_string_ptr_()) { | ||||
|           delete get_string_ptr_(); | ||||
|         } | ||||
|         // Move data | ||||
|         message_type_ = other.message_type_; | ||||
|         data_ = other.data_; | ||||
|         // Reset other to safe state | ||||
|         other.message_type_ = 0; | ||||
|         other.data_.ptr = nullptr; | ||||
|       } | ||||
|       return *this; | ||||
|     } | ||||
|  | ||||
|     // Call operator | ||||
|     uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) const; | ||||
|     // Call operator - now accepts message_type as parameter | ||||
|     uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single, | ||||
|                         uint16_t message_type) const; | ||||
|  | ||||
|    private: | ||||
|     // Helper to check if this message type uses heap-allocated strings | ||||
|     bool uses_string_data_() const { return message_type_ == EventResponse::MESSAGE_TYPE; } | ||||
|     union CreatorData { | ||||
|       MessageCreatorPtr ptr;    // 8 bytes | ||||
|       std::string *string_ptr;  // 8 bytes | ||||
|     } data_;                    // 8 bytes | ||||
|     uint16_t message_type_;     // 2 bytes (0 = function ptr, >0 = state capture) | ||||
|     // Check if this contains a string pointer | ||||
|     bool has_tagged_string_ptr_() const { return (data_.tagged & 1) != 0; } | ||||
|  | ||||
|     // Get the actual string pointer (clears the tag bit) | ||||
|     std::string *get_string_ptr_() const { | ||||
|       // NOLINTNEXTLINE(performance-no-int-to-ptr) | ||||
|       return reinterpret_cast<std::string *>(data_.tagged & ~uintptr_t(1)); | ||||
|     } | ||||
|  | ||||
|     union { | ||||
|       MessageCreatorPtr ptr; | ||||
|       uintptr_t tagged; | ||||
|     } data_;  // 4 bytes on 32-bit | ||||
|   }; | ||||
|  | ||||
|   // Generic batching mechanism for both state updates and entity info | ||||
|   | ||||
| @@ -66,6 +66,17 @@ const char *api_error_to_str(APIError err) { | ||||
|   return "UNKNOWN"; | ||||
| } | ||||
|  | ||||
| // Default implementation for loop - handles sending buffered data | ||||
| APIError APIFrameHelper::loop() { | ||||
|   if (!this->tx_buf_.empty()) { | ||||
|     APIError err = try_send_tx_buf_(); | ||||
|     if (err != APIError::OK && err != APIError::WOULD_BLOCK) { | ||||
|       return err; | ||||
|     } | ||||
|   } | ||||
|   return APIError::OK;  // Convert WOULD_BLOCK to OK to avoid connection termination | ||||
| } | ||||
|  | ||||
| // Helper method to buffer data from IOVs | ||||
| void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len) { | ||||
|   SendBuffer buffer; | ||||
| @@ -274,17 +285,21 @@ APIError APINoiseFrameHelper::init() { | ||||
| } | ||||
| /// Run through handshake messages (if in that phase) | ||||
| APIError APINoiseFrameHelper::loop() { | ||||
|   // During handshake phase, process as many actions as possible until we can't progress | ||||
|   // socket_->ready() stays true until next main loop, but state_action() will return | ||||
|   // WOULD_BLOCK when no more data is available to read | ||||
|   while (state_ != State::DATA && this->socket_->ready()) { | ||||
|     APIError err = state_action_(); | ||||
|     if (err != APIError::OK && err != APIError::WOULD_BLOCK) { | ||||
|       return err; | ||||
|     } | ||||
|   if (!this->tx_buf_.empty()) { | ||||
|     err = try_send_tx_buf_(); | ||||
|     if (err != APIError::OK && err != APIError::WOULD_BLOCK) { | ||||
|       return err; | ||||
|     if (err == APIError::WOULD_BLOCK) { | ||||
|       break; | ||||
|     } | ||||
|   } | ||||
|   return APIError::OK;  // Convert WOULD_BLOCK to OK to avoid connection termination | ||||
|  | ||||
|   // Use base class implementation for buffer sending | ||||
|   return APIFrameHelper::loop(); | ||||
| } | ||||
|  | ||||
| /** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter | ||||
| @@ -330,17 +345,15 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) { | ||||
|       return APIError::WOULD_BLOCK; | ||||
|     } | ||||
|  | ||||
|     if (rx_header_buf_[0] != 0x01) { | ||||
|       state_ = State::FAILED; | ||||
|       HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]); | ||||
|       return APIError::BAD_INDICATOR; | ||||
|     } | ||||
|     // header reading done | ||||
|   } | ||||
|  | ||||
|   // read body | ||||
|   uint8_t indicator = rx_header_buf_[0]; | ||||
|   if (indicator != 0x01) { | ||||
|     state_ = State::FAILED; | ||||
|     HELPER_LOG("Bad indicator byte %u", indicator); | ||||
|     return APIError::BAD_INDICATOR; | ||||
|   } | ||||
|  | ||||
|   uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2]; | ||||
|  | ||||
|   if (state_ != State::DATA && msg_size > 128) { | ||||
| @@ -586,10 +599,6 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { | ||||
|     return APIError::BAD_DATA_PACKET; | ||||
|   } | ||||
|  | ||||
|   // uint16_t type; | ||||
|   // uint16_t data_len; | ||||
|   // uint8_t *data; | ||||
|   // uint8_t *padding;  zero or more bytes to fill up the rest of the packet | ||||
|   uint16_t type = (((uint16_t) msg_data[0]) << 8) | msg_data[1]; | ||||
|   uint16_t data_len = (((uint16_t) msg_data[2]) << 8) | msg_data[3]; | ||||
|   if (data_len > msg_size - 4) { | ||||
| @@ -822,18 +831,12 @@ APIError APIPlaintextFrameHelper::init() { | ||||
|   state_ = State::DATA; | ||||
|   return APIError::OK; | ||||
| } | ||||
| /// Not used for plaintext | ||||
| APIError APIPlaintextFrameHelper::loop() { | ||||
|   if (state_ != State::DATA) { | ||||
|     return APIError::BAD_STATE; | ||||
|   } | ||||
|   if (!this->tx_buf_.empty()) { | ||||
|     APIError err = try_send_tx_buf_(); | ||||
|     if (err != APIError::OK && err != APIError::WOULD_BLOCK) { | ||||
|       return err; | ||||
|     } | ||||
|   } | ||||
|   return APIError::OK;  // Convert WOULD_BLOCK to OK to avoid connection termination | ||||
|   // Use base class implementation for buffer sending | ||||
|   return APIFrameHelper::loop(); | ||||
| } | ||||
|  | ||||
| /** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter | ||||
|   | ||||
| @@ -38,7 +38,7 @@ struct PacketInfo { | ||||
|       : message_type(type), offset(off), payload_size(size), padding(0) {} | ||||
| }; | ||||
|  | ||||
| enum class APIError : int { | ||||
| enum class APIError : uint16_t { | ||||
|   OK = 0, | ||||
|   WOULD_BLOCK = 1001, | ||||
|   BAD_HANDSHAKE_PACKET_LEN = 1002, | ||||
| @@ -74,7 +74,7 @@ class APIFrameHelper { | ||||
|   } | ||||
|   virtual ~APIFrameHelper() = default; | ||||
|   virtual APIError init() = 0; | ||||
|   virtual APIError loop() = 0; | ||||
|   virtual APIError loop(); | ||||
|   virtual APIError read_packet(ReadPacketBuffer *buffer) = 0; | ||||
|   bool can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); } | ||||
|   std::string getpeername() { return socket_->getpeername(); } | ||||
|   | ||||
| @@ -812,6 +812,103 @@ void PingResponse::dump_to(std::string &out) const { out.append("PingResponse {} | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void DeviceInfoRequest::dump_to(std::string &out) const { out.append("DeviceInfoRequest {}"); } | ||||
| #endif | ||||
| bool AreaInfo::decode_varint(uint32_t field_id, ProtoVarInt value) { | ||||
|   switch (field_id) { | ||||
|     case 1: { | ||||
|       this->area_id = value.as_uint32(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| } | ||||
| bool AreaInfo::decode_length(uint32_t field_id, ProtoLengthDelimited value) { | ||||
|   switch (field_id) { | ||||
|     case 2: { | ||||
|       this->name = value.as_string(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| } | ||||
| void AreaInfo::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_uint32(1, this->area_id); | ||||
|   buffer.encode_string(2, this->name); | ||||
| } | ||||
| void AreaInfo::calculate_size(uint32_t &total_size) const { | ||||
|   ProtoSize::add_uint32_field(total_size, 1, this->area_id, false); | ||||
|   ProtoSize::add_string_field(total_size, 1, this->name, false); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void AreaInfo::dump_to(std::string &out) const { | ||||
|   __attribute__((unused)) char buffer[64]; | ||||
|   out.append("AreaInfo {\n"); | ||||
|   out.append("  area_id: "); | ||||
|   sprintf(buffer, "%" PRIu32, this->area_id); | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  name: "); | ||||
|   out.append("'").append(this->name).append("'"); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| bool DeviceInfo::decode_varint(uint32_t field_id, ProtoVarInt value) { | ||||
|   switch (field_id) { | ||||
|     case 1: { | ||||
|       this->device_id = value.as_uint32(); | ||||
|       return true; | ||||
|     } | ||||
|     case 3: { | ||||
|       this->area_id = value.as_uint32(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| } | ||||
| bool DeviceInfo::decode_length(uint32_t field_id, ProtoLengthDelimited value) { | ||||
|   switch (field_id) { | ||||
|     case 2: { | ||||
|       this->name = value.as_string(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| } | ||||
| void DeviceInfo::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_uint32(1, this->device_id); | ||||
|   buffer.encode_string(2, this->name); | ||||
|   buffer.encode_uint32(3, this->area_id); | ||||
| } | ||||
| void DeviceInfo::calculate_size(uint32_t &total_size) const { | ||||
|   ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); | ||||
|   ProtoSize::add_string_field(total_size, 1, this->name, false); | ||||
|   ProtoSize::add_uint32_field(total_size, 1, this->area_id, false); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void DeviceInfo::dump_to(std::string &out) const { | ||||
|   __attribute__((unused)) char buffer[64]; | ||||
|   out.append("DeviceInfo {\n"); | ||||
|   out.append("  device_id: "); | ||||
|   sprintf(buffer, "%" PRIu32, this->device_id); | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  name: "); | ||||
|   out.append("'").append(this->name).append("'"); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  area_id: "); | ||||
|   sprintf(buffer, "%" PRIu32, this->area_id); | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| bool DeviceInfoResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { | ||||
|   switch (field_id) { | ||||
|     case 1: { | ||||
| @@ -896,6 +993,18 @@ bool DeviceInfoResponse::decode_length(uint32_t field_id, ProtoLengthDelimited v | ||||
|       this->bluetooth_mac_address = value.as_string(); | ||||
|       return true; | ||||
|     } | ||||
|     case 20: { | ||||
|       this->devices.push_back(value.as_message<DeviceInfo>()); | ||||
|       return true; | ||||
|     } | ||||
|     case 21: { | ||||
|       this->areas.push_back(value.as_message<AreaInfo>()); | ||||
|       return true; | ||||
|     } | ||||
|     case 22: { | ||||
|       this->area = value.as_message<AreaInfo>(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| @@ -920,6 +1029,13 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_string(16, this->suggested_area); | ||||
|   buffer.encode_string(18, this->bluetooth_mac_address); | ||||
|   buffer.encode_bool(19, this->api_encryption_supported); | ||||
|   for (auto &it : this->devices) { | ||||
|     buffer.encode_message<DeviceInfo>(20, it, true); | ||||
|   } | ||||
|   for (auto &it : this->areas) { | ||||
|     buffer.encode_message<AreaInfo>(21, it, true); | ||||
|   } | ||||
|   buffer.encode_message<AreaInfo>(22, this->area); | ||||
| } | ||||
| void DeviceInfoResponse::calculate_size(uint32_t &total_size) const { | ||||
|   ProtoSize::add_bool_field(total_size, 1, this->uses_password, false); | ||||
| @@ -941,6 +1057,9 @@ void DeviceInfoResponse::calculate_size(uint32_t &total_size) const { | ||||
|   ProtoSize::add_string_field(total_size, 2, this->suggested_area, false); | ||||
|   ProtoSize::add_string_field(total_size, 2, this->bluetooth_mac_address, false); | ||||
|   ProtoSize::add_bool_field(total_size, 2, this->api_encryption_supported, false); | ||||
|   ProtoSize::add_repeated_message(total_size, 2, this->devices); | ||||
|   ProtoSize::add_repeated_message(total_size, 2, this->areas); | ||||
|   ProtoSize::add_message_object(total_size, 2, this->area, false); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void DeviceInfoResponse::dump_to(std::string &out) const { | ||||
| @@ -1026,6 +1145,22 @@ void DeviceInfoResponse::dump_to(std::string &out) const { | ||||
|   out.append("  api_encryption_supported: "); | ||||
|   out.append(YESNO(this->api_encryption_supported)); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   for (const auto &it : this->devices) { | ||||
|     out.append("  devices: "); | ||||
|     it.dump_to(out); | ||||
|     out.append("\n"); | ||||
|   } | ||||
|  | ||||
|   for (const auto &it : this->areas) { | ||||
|     out.append("  areas: "); | ||||
|     it.dump_to(out); | ||||
|     out.append("\n"); | ||||
|   } | ||||
|  | ||||
|   out.append("  area: "); | ||||
|   this->area.dump_to(out); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| @@ -1052,6 +1187,10 @@ bool ListEntitiesBinarySensorResponse::decode_varint(uint32_t field_id, ProtoVar | ||||
|       this->entity_category = value.as_enum<enums::EntityCategory>(); | ||||
|       return true; | ||||
|     } | ||||
|     case 10: { | ||||
|       this->device_id = value.as_uint32(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| @@ -1102,6 +1241,7 @@ void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_bool(7, this->disabled_by_default); | ||||
|   buffer.encode_string(8, this->icon); | ||||
|   buffer.encode_enum<enums::EntityCategory>(9, this->entity_category); | ||||
|   buffer.encode_uint32(10, this->device_id); | ||||
| } | ||||
| void ListEntitiesBinarySensorResponse::calculate_size(uint32_t &total_size) const { | ||||
|   ProtoSize::add_string_field(total_size, 1, this->object_id, false); | ||||
| @@ -1113,6 +1253,7 @@ void ListEntitiesBinarySensorResponse::calculate_size(uint32_t &total_size) cons | ||||
|   ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); | ||||
|   ProtoSize::add_string_field(total_size, 1, this->icon, false); | ||||
|   ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->entity_category), false); | ||||
|   ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void ListEntitiesBinarySensorResponse::dump_to(std::string &out) const { | ||||
| @@ -1154,6 +1295,11 @@ void ListEntitiesBinarySensorResponse::dump_to(std::string &out) const { | ||||
|   out.append("  entity_category: "); | ||||
|   out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category)); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  device_id: "); | ||||
|   sprintf(buffer, "%" PRIu32, this->device_id); | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| @@ -1236,6 +1382,10 @@ bool ListEntitiesCoverResponse::decode_varint(uint32_t field_id, ProtoVarInt val | ||||
|       this->supports_stop = value.as_bool(); | ||||
|       return true; | ||||
|     } | ||||
|     case 13: { | ||||
|       this->device_id = value.as_uint32(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| @@ -1289,6 +1439,7 @@ void ListEntitiesCoverResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_string(10, this->icon); | ||||
|   buffer.encode_enum<enums::EntityCategory>(11, this->entity_category); | ||||
|   buffer.encode_bool(12, this->supports_stop); | ||||
|   buffer.encode_uint32(13, this->device_id); | ||||
| } | ||||
| void ListEntitiesCoverResponse::calculate_size(uint32_t &total_size) const { | ||||
|   ProtoSize::add_string_field(total_size, 1, this->object_id, false); | ||||
| @@ -1303,6 +1454,7 @@ void ListEntitiesCoverResponse::calculate_size(uint32_t &total_size) const { | ||||
|   ProtoSize::add_string_field(total_size, 1, this->icon, false); | ||||
|   ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->entity_category), false); | ||||
|   ProtoSize::add_bool_field(total_size, 1, this->supports_stop, false); | ||||
|   ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void ListEntitiesCoverResponse::dump_to(std::string &out) const { | ||||
| @@ -1356,6 +1508,11 @@ void ListEntitiesCoverResponse::dump_to(std::string &out) const { | ||||
|   out.append("  supports_stop: "); | ||||
|   out.append(YESNO(this->supports_stop)); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  device_id: "); | ||||
|   sprintf(buffer, "%" PRIu32, this->device_id); | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| @@ -1565,6 +1722,10 @@ bool ListEntitiesFanResponse::decode_varint(uint32_t field_id, ProtoVarInt value | ||||
|       this->entity_category = value.as_enum<enums::EntityCategory>(); | ||||
|       return true; | ||||
|     } | ||||
|     case 13: { | ||||
|       this->device_id = value.as_uint32(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| @@ -1620,6 +1781,7 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   for (auto &it : this->supported_preset_modes) { | ||||
|     buffer.encode_string(12, it, true); | ||||
|   } | ||||
|   buffer.encode_uint32(13, this->device_id); | ||||
| } | ||||
| void ListEntitiesFanResponse::calculate_size(uint32_t &total_size) const { | ||||
|   ProtoSize::add_string_field(total_size, 1, this->object_id, false); | ||||
| @@ -1638,6 +1800,7 @@ void ListEntitiesFanResponse::calculate_size(uint32_t &total_size) const { | ||||
|       ProtoSize::add_string_field(total_size, 1, it, true); | ||||
|     } | ||||
|   } | ||||
|   ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void ListEntitiesFanResponse::dump_to(std::string &out) const { | ||||
| @@ -1694,6 +1857,11 @@ void ListEntitiesFanResponse::dump_to(std::string &out) const { | ||||
|     out.append("'").append(it).append("'"); | ||||
|     out.append("\n"); | ||||
|   } | ||||
|  | ||||
|   out.append("  device_id: "); | ||||
|   sprintf(buffer, "%" PRIu32, this->device_id); | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| @@ -1987,6 +2155,10 @@ bool ListEntitiesLightResponse::decode_varint(uint32_t field_id, ProtoVarInt val | ||||
|       this->entity_category = value.as_enum<enums::EntityCategory>(); | ||||
|       return true; | ||||
|     } | ||||
|     case 16: { | ||||
|       this->device_id = value.as_uint32(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| @@ -2055,6 +2227,7 @@ void ListEntitiesLightResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_bool(13, this->disabled_by_default); | ||||
|   buffer.encode_string(14, this->icon); | ||||
|   buffer.encode_enum<enums::EntityCategory>(15, this->entity_category); | ||||
|   buffer.encode_uint32(16, this->device_id); | ||||
| } | ||||
| void ListEntitiesLightResponse::calculate_size(uint32_t &total_size) const { | ||||
|   ProtoSize::add_string_field(total_size, 1, this->object_id, false); | ||||
| @@ -2080,6 +2253,7 @@ void ListEntitiesLightResponse::calculate_size(uint32_t &total_size) const { | ||||
|   ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); | ||||
|   ProtoSize::add_string_field(total_size, 1, this->icon, false); | ||||
|   ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->entity_category), false); | ||||
|   ProtoSize::add_uint32_field(total_size, 2, this->device_id, false); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void ListEntitiesLightResponse::dump_to(std::string &out) const { | ||||
| @@ -2151,6 +2325,11 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const { | ||||
|   out.append("  entity_category: "); | ||||
|   out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category)); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  device_id: "); | ||||
|   sprintf(buffer, "%" PRIu32, this->device_id); | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| @@ -2658,6 +2837,10 @@ bool ListEntitiesSensorResponse::decode_varint(uint32_t field_id, ProtoVarInt va | ||||
|       this->entity_category = value.as_enum<enums::EntityCategory>(); | ||||
|       return true; | ||||
|     } | ||||
|     case 14: { | ||||
|       this->device_id = value.as_uint32(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| @@ -2716,6 +2899,7 @@ void ListEntitiesSensorResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_enum<enums::SensorLastResetType>(11, this->legacy_last_reset_type); | ||||
|   buffer.encode_bool(12, this->disabled_by_default); | ||||
|   buffer.encode_enum<enums::EntityCategory>(13, this->entity_category); | ||||
|   buffer.encode_uint32(14, this->device_id); | ||||
| } | ||||
| void ListEntitiesSensorResponse::calculate_size(uint32_t &total_size) const { | ||||
|   ProtoSize::add_string_field(total_size, 1, this->object_id, false); | ||||
| @@ -2731,6 +2915,7 @@ void ListEntitiesSensorResponse::calculate_size(uint32_t &total_size) const { | ||||
|   ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->legacy_last_reset_type), false); | ||||
|   ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); | ||||
|   ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->entity_category), false); | ||||
|   ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void ListEntitiesSensorResponse::dump_to(std::string &out) const { | ||||
| @@ -2789,6 +2974,11 @@ void ListEntitiesSensorResponse::dump_to(std::string &out) const { | ||||
|   out.append("  entity_category: "); | ||||
|   out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category)); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  device_id: "); | ||||
|   sprintf(buffer, "%" PRIu32, this->device_id); | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| @@ -2860,6 +3050,10 @@ bool ListEntitiesSwitchResponse::decode_varint(uint32_t field_id, ProtoVarInt va | ||||
|       this->entity_category = value.as_enum<enums::EntityCategory>(); | ||||
|       return true; | ||||
|     } | ||||
|     case 10: { | ||||
|       this->device_id = value.as_uint32(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| @@ -2910,6 +3104,7 @@ void ListEntitiesSwitchResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_bool(7, this->disabled_by_default); | ||||
|   buffer.encode_enum<enums::EntityCategory>(8, this->entity_category); | ||||
|   buffer.encode_string(9, this->device_class); | ||||
|   buffer.encode_uint32(10, this->device_id); | ||||
| } | ||||
| void ListEntitiesSwitchResponse::calculate_size(uint32_t &total_size) const { | ||||
|   ProtoSize::add_string_field(total_size, 1, this->object_id, false); | ||||
| @@ -2921,6 +3116,7 @@ void ListEntitiesSwitchResponse::calculate_size(uint32_t &total_size) const { | ||||
|   ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); | ||||
|   ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->entity_category), false); | ||||
|   ProtoSize::add_string_field(total_size, 1, this->device_class, false); | ||||
|   ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void ListEntitiesSwitchResponse::dump_to(std::string &out) const { | ||||
| @@ -2962,6 +3158,11 @@ void ListEntitiesSwitchResponse::dump_to(std::string &out) const { | ||||
|   out.append("  device_class: "); | ||||
|   out.append("'").append(this->device_class).append("'"); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  device_id: "); | ||||
|   sprintf(buffer, "%" PRIu32, this->device_id); | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| @@ -3061,6 +3262,10 @@ bool ListEntitiesTextSensorResponse::decode_varint(uint32_t field_id, ProtoVarIn | ||||
|       this->entity_category = value.as_enum<enums::EntityCategory>(); | ||||
|       return true; | ||||
|     } | ||||
|     case 9: { | ||||
|       this->device_id = value.as_uint32(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| @@ -3110,6 +3315,7 @@ void ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_bool(6, this->disabled_by_default); | ||||
|   buffer.encode_enum<enums::EntityCategory>(7, this->entity_category); | ||||
|   buffer.encode_string(8, this->device_class); | ||||
|   buffer.encode_uint32(9, this->device_id); | ||||
| } | ||||
| void ListEntitiesTextSensorResponse::calculate_size(uint32_t &total_size) const { | ||||
|   ProtoSize::add_string_field(total_size, 1, this->object_id, false); | ||||
| @@ -3120,6 +3326,7 @@ void ListEntitiesTextSensorResponse::calculate_size(uint32_t &total_size) const | ||||
|   ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); | ||||
|   ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->entity_category), false); | ||||
|   ProtoSize::add_string_field(total_size, 1, this->device_class, false); | ||||
|   ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void ListEntitiesTextSensorResponse::dump_to(std::string &out) const { | ||||
| @@ -3157,6 +3364,11 @@ void ListEntitiesTextSensorResponse::dump_to(std::string &out) const { | ||||
|   out.append("  device_class: "); | ||||
|   out.append("'").append(this->device_class).append("'"); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  device_id: "); | ||||
|   sprintf(buffer, "%" PRIu32, this->device_id); | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| @@ -3922,6 +4134,10 @@ bool ListEntitiesCameraResponse::decode_varint(uint32_t field_id, ProtoVarInt va | ||||
|       this->entity_category = value.as_enum<enums::EntityCategory>(); | ||||
|       return true; | ||||
|     } | ||||
|     case 8: { | ||||
|       this->device_id = value.as_uint32(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| @@ -3966,6 +4182,7 @@ void ListEntitiesCameraResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_bool(5, this->disabled_by_default); | ||||
|   buffer.encode_string(6, this->icon); | ||||
|   buffer.encode_enum<enums::EntityCategory>(7, this->entity_category); | ||||
|   buffer.encode_uint32(8, this->device_id); | ||||
| } | ||||
| void ListEntitiesCameraResponse::calculate_size(uint32_t &total_size) const { | ||||
|   ProtoSize::add_string_field(total_size, 1, this->object_id, false); | ||||
| @@ -3975,6 +4192,7 @@ void ListEntitiesCameraResponse::calculate_size(uint32_t &total_size) const { | ||||
|   ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); | ||||
|   ProtoSize::add_string_field(total_size, 1, this->icon, false); | ||||
|   ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->entity_category), false); | ||||
|   ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void ListEntitiesCameraResponse::dump_to(std::string &out) const { | ||||
| @@ -4008,6 +4226,11 @@ void ListEntitiesCameraResponse::dump_to(std::string &out) const { | ||||
|   out.append("  entity_category: "); | ||||
|   out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category)); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  device_id: "); | ||||
|   sprintf(buffer, "%" PRIu32, this->device_id); | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| @@ -4156,6 +4379,10 @@ bool ListEntitiesClimateResponse::decode_varint(uint32_t field_id, ProtoVarInt v | ||||
|       this->supports_target_humidity = value.as_bool(); | ||||
|       return true; | ||||
|     } | ||||
|     case 26: { | ||||
|       this->device_id = value.as_uint32(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| @@ -4262,6 +4489,7 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_bool(23, this->supports_target_humidity); | ||||
|   buffer.encode_float(24, this->visual_min_humidity); | ||||
|   buffer.encode_float(25, this->visual_max_humidity); | ||||
|   buffer.encode_uint32(26, this->device_id); | ||||
| } | ||||
| void ListEntitiesClimateResponse::calculate_size(uint32_t &total_size) const { | ||||
|   ProtoSize::add_string_field(total_size, 1, this->object_id, false); | ||||
| @@ -4313,6 +4541,7 @@ void ListEntitiesClimateResponse::calculate_size(uint32_t &total_size) const { | ||||
|   ProtoSize::add_bool_field(total_size, 2, this->supports_target_humidity, false); | ||||
|   ProtoSize::add_fixed_field<4>(total_size, 2, this->visual_min_humidity != 0.0f, false); | ||||
|   ProtoSize::add_fixed_field<4>(total_size, 2, this->visual_max_humidity != 0.0f, false); | ||||
|   ProtoSize::add_uint32_field(total_size, 2, this->device_id, false); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void ListEntitiesClimateResponse::dump_to(std::string &out) const { | ||||
| @@ -4436,6 +4665,11 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { | ||||
|   sprintf(buffer, "%g", this->visual_max_humidity); | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  device_id: "); | ||||
|   sprintf(buffer, "%" PRIu32, this->device_id); | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| @@ -4901,6 +5135,10 @@ bool ListEntitiesNumberResponse::decode_varint(uint32_t field_id, ProtoVarInt va | ||||
|       this->mode = value.as_enum<enums::NumberMode>(); | ||||
|       return true; | ||||
|     } | ||||
|     case 14: { | ||||
|       this->device_id = value.as_uint32(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| @@ -4971,6 +5209,7 @@ void ListEntitiesNumberResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_string(11, this->unit_of_measurement); | ||||
|   buffer.encode_enum<enums::NumberMode>(12, this->mode); | ||||
|   buffer.encode_string(13, this->device_class); | ||||
|   buffer.encode_uint32(14, this->device_id); | ||||
| } | ||||
| void ListEntitiesNumberResponse::calculate_size(uint32_t &total_size) const { | ||||
|   ProtoSize::add_string_field(total_size, 1, this->object_id, false); | ||||
| @@ -4986,6 +5225,7 @@ void ListEntitiesNumberResponse::calculate_size(uint32_t &total_size) const { | ||||
|   ProtoSize::add_string_field(total_size, 1, this->unit_of_measurement, false); | ||||
|   ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->mode), false); | ||||
|   ProtoSize::add_string_field(total_size, 1, this->device_class, false); | ||||
|   ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void ListEntitiesNumberResponse::dump_to(std::string &out) const { | ||||
| @@ -5046,6 +5286,11 @@ void ListEntitiesNumberResponse::dump_to(std::string &out) const { | ||||
|   out.append("  device_class: "); | ||||
|   out.append("'").append(this->device_class).append("'"); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  device_id: "); | ||||
|   sprintf(buffer, "%" PRIu32, this->device_id); | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| @@ -5151,6 +5396,10 @@ bool ListEntitiesSelectResponse::decode_varint(uint32_t field_id, ProtoVarInt va | ||||
|       this->entity_category = value.as_enum<enums::EntityCategory>(); | ||||
|       return true; | ||||
|     } | ||||
|     case 9: { | ||||
|       this->device_id = value.as_uint32(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| @@ -5202,6 +5451,7 @@ void ListEntitiesSelectResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   } | ||||
|   buffer.encode_bool(7, this->disabled_by_default); | ||||
|   buffer.encode_enum<enums::EntityCategory>(8, this->entity_category); | ||||
|   buffer.encode_uint32(9, this->device_id); | ||||
| } | ||||
| void ListEntitiesSelectResponse::calculate_size(uint32_t &total_size) const { | ||||
|   ProtoSize::add_string_field(total_size, 1, this->object_id, false); | ||||
| @@ -5216,6 +5466,7 @@ void ListEntitiesSelectResponse::calculate_size(uint32_t &total_size) const { | ||||
|   } | ||||
|   ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); | ||||
|   ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->entity_category), false); | ||||
|   ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void ListEntitiesSelectResponse::dump_to(std::string &out) const { | ||||
| @@ -5255,6 +5506,11 @@ void ListEntitiesSelectResponse::dump_to(std::string &out) const { | ||||
|   out.append("  entity_category: "); | ||||
|   out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category)); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  device_id: "); | ||||
|   sprintf(buffer, "%" PRIu32, this->device_id); | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| @@ -5378,6 +5634,10 @@ bool ListEntitiesSirenResponse::decode_varint(uint32_t field_id, ProtoVarInt val | ||||
|       this->entity_category = value.as_enum<enums::EntityCategory>(); | ||||
|       return true; | ||||
|     } | ||||
|     case 11: { | ||||
|       this->device_id = value.as_uint32(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| @@ -5431,6 +5691,7 @@ void ListEntitiesSirenResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_bool(8, this->supports_duration); | ||||
|   buffer.encode_bool(9, this->supports_volume); | ||||
|   buffer.encode_enum<enums::EntityCategory>(10, this->entity_category); | ||||
|   buffer.encode_uint32(11, this->device_id); | ||||
| } | ||||
| void ListEntitiesSirenResponse::calculate_size(uint32_t &total_size) const { | ||||
|   ProtoSize::add_string_field(total_size, 1, this->object_id, false); | ||||
| @@ -5447,6 +5708,7 @@ void ListEntitiesSirenResponse::calculate_size(uint32_t &total_size) const { | ||||
|   ProtoSize::add_bool_field(total_size, 1, this->supports_duration, false); | ||||
|   ProtoSize::add_bool_field(total_size, 1, this->supports_volume, false); | ||||
|   ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->entity_category), false); | ||||
|   ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void ListEntitiesSirenResponse::dump_to(std::string &out) const { | ||||
| @@ -5494,6 +5756,11 @@ void ListEntitiesSirenResponse::dump_to(std::string &out) const { | ||||
|   out.append("  entity_category: "); | ||||
|   out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category)); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  device_id: "); | ||||
|   sprintf(buffer, "%" PRIu32, this->device_id); | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| @@ -5683,6 +5950,10 @@ bool ListEntitiesLockResponse::decode_varint(uint32_t field_id, ProtoVarInt valu | ||||
|       this->requires_code = value.as_bool(); | ||||
|       return true; | ||||
|     } | ||||
|     case 12: { | ||||
|       this->device_id = value.as_uint32(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| @@ -5735,6 +6006,7 @@ void ListEntitiesLockResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_bool(9, this->supports_open); | ||||
|   buffer.encode_bool(10, this->requires_code); | ||||
|   buffer.encode_string(11, this->code_format); | ||||
|   buffer.encode_uint32(12, this->device_id); | ||||
| } | ||||
| void ListEntitiesLockResponse::calculate_size(uint32_t &total_size) const { | ||||
|   ProtoSize::add_string_field(total_size, 1, this->object_id, false); | ||||
| @@ -5748,6 +6020,7 @@ void ListEntitiesLockResponse::calculate_size(uint32_t &total_size) const { | ||||
|   ProtoSize::add_bool_field(total_size, 1, this->supports_open, false); | ||||
|   ProtoSize::add_bool_field(total_size, 1, this->requires_code, false); | ||||
|   ProtoSize::add_string_field(total_size, 1, this->code_format, false); | ||||
|   ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void ListEntitiesLockResponse::dump_to(std::string &out) const { | ||||
| @@ -5797,6 +6070,11 @@ void ListEntitiesLockResponse::dump_to(std::string &out) const { | ||||
|   out.append("  code_format: "); | ||||
|   out.append("'").append(this->code_format).append("'"); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  device_id: "); | ||||
|   sprintf(buffer, "%" PRIu32, this->device_id); | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| @@ -5922,6 +6200,10 @@ bool ListEntitiesButtonResponse::decode_varint(uint32_t field_id, ProtoVarInt va | ||||
|       this->entity_category = value.as_enum<enums::EntityCategory>(); | ||||
|       return true; | ||||
|     } | ||||
|     case 9: { | ||||
|       this->device_id = value.as_uint32(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| @@ -5971,6 +6253,7 @@ void ListEntitiesButtonResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_bool(6, this->disabled_by_default); | ||||
|   buffer.encode_enum<enums::EntityCategory>(7, this->entity_category); | ||||
|   buffer.encode_string(8, this->device_class); | ||||
|   buffer.encode_uint32(9, this->device_id); | ||||
| } | ||||
| void ListEntitiesButtonResponse::calculate_size(uint32_t &total_size) const { | ||||
|   ProtoSize::add_string_field(total_size, 1, this->object_id, false); | ||||
| @@ -5981,6 +6264,7 @@ void ListEntitiesButtonResponse::calculate_size(uint32_t &total_size) const { | ||||
|   ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); | ||||
|   ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->entity_category), false); | ||||
|   ProtoSize::add_string_field(total_size, 1, this->device_class, false); | ||||
|   ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void ListEntitiesButtonResponse::dump_to(std::string &out) const { | ||||
| @@ -6018,6 +6302,11 @@ void ListEntitiesButtonResponse::dump_to(std::string &out) const { | ||||
|   out.append("  device_class: "); | ||||
|   out.append("'").append(this->device_class).append("'"); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  device_id: "); | ||||
|   sprintf(buffer, "%" PRIu32, this->device_id); | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| @@ -6135,6 +6424,10 @@ bool ListEntitiesMediaPlayerResponse::decode_varint(uint32_t field_id, ProtoVarI | ||||
|       this->supports_pause = value.as_bool(); | ||||
|       return true; | ||||
|     } | ||||
|     case 10: { | ||||
|       this->device_id = value.as_uint32(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| @@ -6187,6 +6480,7 @@ void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   for (auto &it : this->supported_formats) { | ||||
|     buffer.encode_message<MediaPlayerSupportedFormat>(9, it, true); | ||||
|   } | ||||
|   buffer.encode_uint32(10, this->device_id); | ||||
| } | ||||
| void ListEntitiesMediaPlayerResponse::calculate_size(uint32_t &total_size) const { | ||||
|   ProtoSize::add_string_field(total_size, 1, this->object_id, false); | ||||
| @@ -6198,6 +6492,7 @@ void ListEntitiesMediaPlayerResponse::calculate_size(uint32_t &total_size) const | ||||
|   ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->entity_category), false); | ||||
|   ProtoSize::add_bool_field(total_size, 1, this->supports_pause, false); | ||||
|   ProtoSize::add_repeated_message(total_size, 1, this->supported_formats); | ||||
|   ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void ListEntitiesMediaPlayerResponse::dump_to(std::string &out) const { | ||||
| @@ -6241,6 +6536,11 @@ void ListEntitiesMediaPlayerResponse::dump_to(std::string &out) const { | ||||
|     it.dump_to(out); | ||||
|     out.append("\n"); | ||||
|   } | ||||
|  | ||||
|   out.append("  device_id: "); | ||||
|   sprintf(buffer, "%" PRIu32, this->device_id); | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| @@ -8551,6 +8851,10 @@ bool ListEntitiesAlarmControlPanelResponse::decode_varint(uint32_t field_id, Pro | ||||
|       this->requires_code_to_arm = value.as_bool(); | ||||
|       return true; | ||||
|     } | ||||
|     case 11: { | ||||
|       this->device_id = value.as_uint32(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| @@ -8598,6 +8902,7 @@ void ListEntitiesAlarmControlPanelResponse::encode(ProtoWriteBuffer buffer) cons | ||||
|   buffer.encode_uint32(8, this->supported_features); | ||||
|   buffer.encode_bool(9, this->requires_code); | ||||
|   buffer.encode_bool(10, this->requires_code_to_arm); | ||||
|   buffer.encode_uint32(11, this->device_id); | ||||
| } | ||||
| void ListEntitiesAlarmControlPanelResponse::calculate_size(uint32_t &total_size) const { | ||||
|   ProtoSize::add_string_field(total_size, 1, this->object_id, false); | ||||
| @@ -8610,6 +8915,7 @@ void ListEntitiesAlarmControlPanelResponse::calculate_size(uint32_t &total_size) | ||||
|   ProtoSize::add_uint32_field(total_size, 1, this->supported_features, false); | ||||
|   ProtoSize::add_bool_field(total_size, 1, this->requires_code, false); | ||||
|   ProtoSize::add_bool_field(total_size, 1, this->requires_code_to_arm, false); | ||||
|   ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void ListEntitiesAlarmControlPanelResponse::dump_to(std::string &out) const { | ||||
| @@ -8656,6 +8962,11 @@ void ListEntitiesAlarmControlPanelResponse::dump_to(std::string &out) const { | ||||
|   out.append("  requires_code_to_arm: "); | ||||
|   out.append(YESNO(this->requires_code_to_arm)); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  device_id: "); | ||||
|   sprintf(buffer, "%" PRIu32, this->device_id); | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| @@ -8783,6 +9094,10 @@ bool ListEntitiesTextResponse::decode_varint(uint32_t field_id, ProtoVarInt valu | ||||
|       this->mode = value.as_enum<enums::TextMode>(); | ||||
|       return true; | ||||
|     } | ||||
|     case 12: { | ||||
|       this->device_id = value.as_uint32(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| @@ -8835,6 +9150,7 @@ void ListEntitiesTextResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_uint32(9, this->max_length); | ||||
|   buffer.encode_string(10, this->pattern); | ||||
|   buffer.encode_enum<enums::TextMode>(11, this->mode); | ||||
|   buffer.encode_uint32(12, this->device_id); | ||||
| } | ||||
| void ListEntitiesTextResponse::calculate_size(uint32_t &total_size) const { | ||||
|   ProtoSize::add_string_field(total_size, 1, this->object_id, false); | ||||
| @@ -8848,6 +9164,7 @@ void ListEntitiesTextResponse::calculate_size(uint32_t &total_size) const { | ||||
|   ProtoSize::add_uint32_field(total_size, 1, this->max_length, false); | ||||
|   ProtoSize::add_string_field(total_size, 1, this->pattern, false); | ||||
|   ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->mode), false); | ||||
|   ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void ListEntitiesTextResponse::dump_to(std::string &out) const { | ||||
| @@ -8899,6 +9216,11 @@ void ListEntitiesTextResponse::dump_to(std::string &out) const { | ||||
|   out.append("  mode: "); | ||||
|   out.append(proto_enum_to_string<enums::TextMode>(this->mode)); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  device_id: "); | ||||
|   sprintf(buffer, "%" PRIu32, this->device_id); | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| @@ -9014,6 +9336,10 @@ bool ListEntitiesDateResponse::decode_varint(uint32_t field_id, ProtoVarInt valu | ||||
|       this->entity_category = value.as_enum<enums::EntityCategory>(); | ||||
|       return true; | ||||
|     } | ||||
|     case 8: { | ||||
|       this->device_id = value.as_uint32(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| @@ -9058,6 +9384,7 @@ void ListEntitiesDateResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_string(5, this->icon); | ||||
|   buffer.encode_bool(6, this->disabled_by_default); | ||||
|   buffer.encode_enum<enums::EntityCategory>(7, this->entity_category); | ||||
|   buffer.encode_uint32(8, this->device_id); | ||||
| } | ||||
| void ListEntitiesDateResponse::calculate_size(uint32_t &total_size) const { | ||||
|   ProtoSize::add_string_field(total_size, 1, this->object_id, false); | ||||
| @@ -9067,6 +9394,7 @@ void ListEntitiesDateResponse::calculate_size(uint32_t &total_size) const { | ||||
|   ProtoSize::add_string_field(total_size, 1, this->icon, false); | ||||
|   ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); | ||||
|   ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->entity_category), false); | ||||
|   ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void ListEntitiesDateResponse::dump_to(std::string &out) const { | ||||
| @@ -9100,6 +9428,11 @@ void ListEntitiesDateResponse::dump_to(std::string &out) const { | ||||
|   out.append("  entity_category: "); | ||||
|   out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category)); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  device_id: "); | ||||
|   sprintf(buffer, "%" PRIu32, this->device_id); | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| @@ -9255,6 +9588,10 @@ bool ListEntitiesTimeResponse::decode_varint(uint32_t field_id, ProtoVarInt valu | ||||
|       this->entity_category = value.as_enum<enums::EntityCategory>(); | ||||
|       return true; | ||||
|     } | ||||
|     case 8: { | ||||
|       this->device_id = value.as_uint32(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| @@ -9299,6 +9636,7 @@ void ListEntitiesTimeResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_string(5, this->icon); | ||||
|   buffer.encode_bool(6, this->disabled_by_default); | ||||
|   buffer.encode_enum<enums::EntityCategory>(7, this->entity_category); | ||||
|   buffer.encode_uint32(8, this->device_id); | ||||
| } | ||||
| void ListEntitiesTimeResponse::calculate_size(uint32_t &total_size) const { | ||||
|   ProtoSize::add_string_field(total_size, 1, this->object_id, false); | ||||
| @@ -9308,6 +9646,7 @@ void ListEntitiesTimeResponse::calculate_size(uint32_t &total_size) const { | ||||
|   ProtoSize::add_string_field(total_size, 1, this->icon, false); | ||||
|   ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); | ||||
|   ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->entity_category), false); | ||||
|   ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void ListEntitiesTimeResponse::dump_to(std::string &out) const { | ||||
| @@ -9341,6 +9680,11 @@ void ListEntitiesTimeResponse::dump_to(std::string &out) const { | ||||
|   out.append("  entity_category: "); | ||||
|   out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category)); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  device_id: "); | ||||
|   sprintf(buffer, "%" PRIu32, this->device_id); | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| @@ -9496,6 +9840,10 @@ bool ListEntitiesEventResponse::decode_varint(uint32_t field_id, ProtoVarInt val | ||||
|       this->entity_category = value.as_enum<enums::EntityCategory>(); | ||||
|       return true; | ||||
|     } | ||||
|     case 10: { | ||||
|       this->device_id = value.as_uint32(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| @@ -9552,6 +9900,7 @@ void ListEntitiesEventResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   for (auto &it : this->event_types) { | ||||
|     buffer.encode_string(9, it, true); | ||||
|   } | ||||
|   buffer.encode_uint32(10, this->device_id); | ||||
| } | ||||
| void ListEntitiesEventResponse::calculate_size(uint32_t &total_size) const { | ||||
|   ProtoSize::add_string_field(total_size, 1, this->object_id, false); | ||||
| @@ -9567,6 +9916,7 @@ void ListEntitiesEventResponse::calculate_size(uint32_t &total_size) const { | ||||
|       ProtoSize::add_string_field(total_size, 1, it, true); | ||||
|     } | ||||
|   } | ||||
|   ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void ListEntitiesEventResponse::dump_to(std::string &out) const { | ||||
| @@ -9610,6 +9960,11 @@ void ListEntitiesEventResponse::dump_to(std::string &out) const { | ||||
|     out.append("'").append(it).append("'"); | ||||
|     out.append("\n"); | ||||
|   } | ||||
|  | ||||
|   out.append("  device_id: "); | ||||
|   sprintf(buffer, "%" PRIu32, this->device_id); | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| @@ -9678,6 +10033,10 @@ bool ListEntitiesValveResponse::decode_varint(uint32_t field_id, ProtoVarInt val | ||||
|       this->supports_stop = value.as_bool(); | ||||
|       return true; | ||||
|     } | ||||
|     case 12: { | ||||
|       this->device_id = value.as_uint32(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| @@ -9730,6 +10089,7 @@ void ListEntitiesValveResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_bool(9, this->assumed_state); | ||||
|   buffer.encode_bool(10, this->supports_position); | ||||
|   buffer.encode_bool(11, this->supports_stop); | ||||
|   buffer.encode_uint32(12, this->device_id); | ||||
| } | ||||
| void ListEntitiesValveResponse::calculate_size(uint32_t &total_size) const { | ||||
|   ProtoSize::add_string_field(total_size, 1, this->object_id, false); | ||||
| @@ -9743,6 +10103,7 @@ void ListEntitiesValveResponse::calculate_size(uint32_t &total_size) const { | ||||
|   ProtoSize::add_bool_field(total_size, 1, this->assumed_state, false); | ||||
|   ProtoSize::add_bool_field(total_size, 1, this->supports_position, false); | ||||
|   ProtoSize::add_bool_field(total_size, 1, this->supports_stop, false); | ||||
|   ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void ListEntitiesValveResponse::dump_to(std::string &out) const { | ||||
| @@ -9792,6 +10153,11 @@ void ListEntitiesValveResponse::dump_to(std::string &out) const { | ||||
|   out.append("  supports_stop: "); | ||||
|   out.append(YESNO(this->supports_stop)); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  device_id: "); | ||||
|   sprintf(buffer, "%" PRIu32, this->device_id); | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| @@ -9923,6 +10289,10 @@ bool ListEntitiesDateTimeResponse::decode_varint(uint32_t field_id, ProtoVarInt | ||||
|       this->entity_category = value.as_enum<enums::EntityCategory>(); | ||||
|       return true; | ||||
|     } | ||||
|     case 8: { | ||||
|       this->device_id = value.as_uint32(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| @@ -9967,6 +10337,7 @@ void ListEntitiesDateTimeResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_string(5, this->icon); | ||||
|   buffer.encode_bool(6, this->disabled_by_default); | ||||
|   buffer.encode_enum<enums::EntityCategory>(7, this->entity_category); | ||||
|   buffer.encode_uint32(8, this->device_id); | ||||
| } | ||||
| void ListEntitiesDateTimeResponse::calculate_size(uint32_t &total_size) const { | ||||
|   ProtoSize::add_string_field(total_size, 1, this->object_id, false); | ||||
| @@ -9976,6 +10347,7 @@ void ListEntitiesDateTimeResponse::calculate_size(uint32_t &total_size) const { | ||||
|   ProtoSize::add_string_field(total_size, 1, this->icon, false); | ||||
|   ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); | ||||
|   ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->entity_category), false); | ||||
|   ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void ListEntitiesDateTimeResponse::dump_to(std::string &out) const { | ||||
| @@ -10009,6 +10381,11 @@ void ListEntitiesDateTimeResponse::dump_to(std::string &out) const { | ||||
|   out.append("  entity_category: "); | ||||
|   out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category)); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  device_id: "); | ||||
|   sprintf(buffer, "%" PRIu32, this->device_id); | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| @@ -10114,6 +10491,10 @@ bool ListEntitiesUpdateResponse::decode_varint(uint32_t field_id, ProtoVarInt va | ||||
|       this->entity_category = value.as_enum<enums::EntityCategory>(); | ||||
|       return true; | ||||
|     } | ||||
|     case 9: { | ||||
|       this->device_id = value.as_uint32(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| @@ -10163,6 +10544,7 @@ void ListEntitiesUpdateResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_bool(6, this->disabled_by_default); | ||||
|   buffer.encode_enum<enums::EntityCategory>(7, this->entity_category); | ||||
|   buffer.encode_string(8, this->device_class); | ||||
|   buffer.encode_uint32(9, this->device_id); | ||||
| } | ||||
| void ListEntitiesUpdateResponse::calculate_size(uint32_t &total_size) const { | ||||
|   ProtoSize::add_string_field(total_size, 1, this->object_id, false); | ||||
| @@ -10173,6 +10555,7 @@ void ListEntitiesUpdateResponse::calculate_size(uint32_t &total_size) const { | ||||
|   ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); | ||||
|   ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->entity_category), false); | ||||
|   ProtoSize::add_string_field(total_size, 1, this->device_class, false); | ||||
|   ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void ListEntitiesUpdateResponse::dump_to(std::string &out) const { | ||||
| @@ -10210,6 +10593,11 @@ void ListEntitiesUpdateResponse::dump_to(std::string &out) const { | ||||
|   out.append("  device_class: "); | ||||
|   out.append("'").append(this->device_class).append("'"); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  device_id: "); | ||||
|   sprintf(buffer, "%" PRIu32, this->device_id); | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -19,7 +19,7 @@ class APIServerConnectionBase : public ProtoService { | ||||
|  | ||||
|   template<typename T> bool send_message(const T &msg) { | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|     this->log_send_message_(T::message_name(), msg.dump()); | ||||
|     this->log_send_message_(msg.message_name(), msg.dump()); | ||||
| #endif | ||||
|     return this->send_message_(msg, T::MESSAGE_TYPE); | ||||
|   } | ||||
|   | ||||
| @@ -47,6 +47,11 @@ void APIServer::setup() { | ||||
|   } | ||||
| #endif | ||||
|  | ||||
|   // Schedule reboot if no clients connect within timeout | ||||
|   if (this->reboot_timeout_ != 0) { | ||||
|     this->schedule_reboot_timeout_(); | ||||
|   } | ||||
|  | ||||
|   this->socket_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0);  // monitored for incoming connections | ||||
|   if (this->socket_ == nullptr) { | ||||
|     ESP_LOGW(TAG, "Could not create socket"); | ||||
| @@ -106,8 +111,6 @@ void APIServer::setup() { | ||||
|   } | ||||
| #endif | ||||
|  | ||||
|   this->last_connected_ = App.get_loop_component_start_time(); | ||||
|  | ||||
| #ifdef USE_ESP32_CAMERA | ||||
|   if (esp32_camera::global_esp32_camera != nullptr && !esp32_camera::global_esp32_camera->is_internal()) { | ||||
|     esp32_camera::global_esp32_camera->add_image_callback( | ||||
| @@ -121,6 +124,16 @@ void APIServer::setup() { | ||||
| #endif | ||||
| } | ||||
|  | ||||
| void APIServer::schedule_reboot_timeout_() { | ||||
|   this->status_set_warning(); | ||||
|   this->set_timeout("api_reboot", this->reboot_timeout_, []() { | ||||
|     if (!global_api_server->is_connected()) { | ||||
|       ESP_LOGE(TAG, "No clients; rebooting"); | ||||
|       App.reboot(); | ||||
|     } | ||||
|   }); | ||||
| } | ||||
|  | ||||
| void APIServer::loop() { | ||||
|   // Accept new clients only if the socket exists and has incoming connections | ||||
|   if (this->socket_ && this->socket_->ready()) { | ||||
| @@ -130,51 +143,61 @@ void APIServer::loop() { | ||||
|       auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len); | ||||
|       if (!sock) | ||||
|         break; | ||||
|       ESP_LOGD(TAG, "Accepted %s", sock->getpeername().c_str()); | ||||
|       ESP_LOGD(TAG, "Accept %s", sock->getpeername().c_str()); | ||||
|  | ||||
|       auto *conn = new APIConnection(std::move(sock), this); | ||||
|       this->clients_.emplace_back(conn); | ||||
|       conn->start(); | ||||
|  | ||||
|       // Clear warning status and cancel reboot when first client connects | ||||
|       if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) { | ||||
|         this->status_clear_warning(); | ||||
|         this->cancel_timeout("api_reboot"); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (this->clients_.empty()) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   // Process clients and remove disconnected ones in a single pass | ||||
|   if (!this->clients_.empty()) { | ||||
|   // Check network connectivity once for all clients | ||||
|   if (!network::is_connected()) { | ||||
|     // Network is down - disconnect all clients | ||||
|     for (auto &client : this->clients_) { | ||||
|       client->on_fatal_error(); | ||||
|       ESP_LOGW(TAG, "%s: Network down; disconnect", client->get_client_combined_info().c_str()); | ||||
|     } | ||||
|     // Continue to process and clean up the clients below | ||||
|   } | ||||
|  | ||||
|   size_t client_index = 0; | ||||
|   while (client_index < this->clients_.size()) { | ||||
|     auto &client = this->clients_[client_index]; | ||||
|  | ||||
|       if (client->remove_) { | ||||
|         // Handle disconnection | ||||
|     if (!client->remove_) { | ||||
|       // Common case: process active client | ||||
|       client->loop(); | ||||
|       client_index++; | ||||
|       continue; | ||||
|     } | ||||
|  | ||||
|     // Rare case: handle disconnection | ||||
|     this->client_disconnected_trigger_->trigger(client->client_info_, client->client_peername_); | ||||
|         ESP_LOGV(TAG, "Removing connection to %s", client->client_info_.c_str()); | ||||
|     ESP_LOGV(TAG, "Remove connection %s", client->client_info_.c_str()); | ||||
|  | ||||
|     // Swap with the last element and pop (avoids expensive vector shifts) | ||||
|     if (client_index < this->clients_.size() - 1) { | ||||
|       std::swap(this->clients_[client_index], this->clients_.back()); | ||||
|     } | ||||
|     this->clients_.pop_back(); | ||||
|         // Don't increment client_index since we need to process the swapped element | ||||
|       } else { | ||||
|         // Process active client | ||||
|         client->loop(); | ||||
|         client_index++;  // Move to next client | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (this->reboot_timeout_ != 0) { | ||||
|     const uint32_t now = App.get_loop_component_start_time(); | ||||
|     if (!this->is_connected()) { | ||||
|       if (now - this->last_connected_ > this->reboot_timeout_) { | ||||
|         ESP_LOGE(TAG, "No client connected; rebooting"); | ||||
|         App.reboot(); | ||||
|       } | ||||
|       this->status_set_warning(); | ||||
|     } else { | ||||
|       this->last_connected_ = now; | ||||
|       this->status_clear_warning(); | ||||
|     // Schedule reboot when last client disconnects | ||||
|     if (this->clients_.empty() && this->reboot_timeout_ != 0) { | ||||
|       this->schedule_reboot_timeout_(); | ||||
|     } | ||||
|     // Don't increment client_index since we need to process the swapped element | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -142,6 +142,7 @@ class APIServer : public Component, public Controller { | ||||
|   } | ||||
|  | ||||
|  protected: | ||||
|   void schedule_reboot_timeout_(); | ||||
|   // Pointers and pointer-like types first (4 bytes each) | ||||
|   std::unique_ptr<socket::Socket> socket_ = nullptr; | ||||
|   Trigger<std::string, std::string> *client_connected_trigger_ = new Trigger<std::string, std::string>(); | ||||
| @@ -150,7 +151,6 @@ class APIServer : public Component, public Controller { | ||||
|   // 4-byte aligned types | ||||
|   uint32_t reboot_timeout_{300000}; | ||||
|   uint32_t batch_delay_{100}; | ||||
|   uint32_t last_connected_{0}; | ||||
|  | ||||
|   // Vectors and strings (12 bytes each on 32-bit) | ||||
|   std::vector<std::unique_ptr<APIConnection>> clients_; | ||||
|   | ||||
| @@ -335,6 +335,7 @@ class ProtoMessage { | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   std::string dump() const; | ||||
|   virtual void dump_to(std::string &out) const = 0; | ||||
|   virtual const char *message_name() const { return "unknown"; } | ||||
| #endif | ||||
|  | ||||
|  protected: | ||||
|   | ||||
| @@ -312,7 +312,7 @@ FileDecoderState AudioDecoder::decode_mp3_() { | ||||
|   if (err) { | ||||
|     switch (err) { | ||||
|       case esp_audio_libs::helix_decoder::ERR_MP3_OUT_OF_MEMORY: | ||||
|         // Intentional fallthrough | ||||
|         [[fallthrough]]; | ||||
|       case esp_audio_libs::helix_decoder::ERR_MP3_NULL_POINTER: | ||||
|         return FileDecoderState::FAILED; | ||||
|         break; | ||||
|   | ||||
| @@ -5,6 +5,7 @@ | ||||
| #include "esphome/core/defines.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| #if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE | ||||
| #include "esp_crt_bundle.h" | ||||
| @@ -16,13 +17,13 @@ namespace audio { | ||||
| static const uint32_t READ_WRITE_TIMEOUT_MS = 20; | ||||
|  | ||||
| static const uint32_t CONNECTION_TIMEOUT_MS = 5000; | ||||
|  | ||||
| // The number of times the http read times out with no data before throwing an error | ||||
| static const uint32_t ERROR_COUNT_NO_DATA_READ_TIMEOUT = 100; | ||||
| static const uint8_t MAX_FETCHING_HEADER_ATTEMPTS = 6; | ||||
|  | ||||
| static const size_t HTTP_STREAM_BUFFER_SIZE = 2048; | ||||
|  | ||||
| static const uint8_t MAX_REDIRECTION = 5; | ||||
| static const uint8_t MAX_REDIRECTIONS = 5; | ||||
|  | ||||
| static const char *const TAG = "audio_reader"; | ||||
|  | ||||
| // Some common HTTP status codes - borrowed from http_request component accessed 20241224 | ||||
| enum HttpStatus { | ||||
| @@ -94,7 +95,7 @@ esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) { | ||||
|   client_config.url = uri.c_str(); | ||||
|   client_config.cert_pem = nullptr; | ||||
|   client_config.disable_auto_redirect = false; | ||||
|   client_config.max_redirection_count = 10; | ||||
|   client_config.max_redirection_count = MAX_REDIRECTIONS; | ||||
|   client_config.event_handler = http_event_handler; | ||||
|   client_config.user_data = this; | ||||
|   client_config.buffer_size = HTTP_STREAM_BUFFER_SIZE; | ||||
| @@ -116,12 +117,29 @@ esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) { | ||||
|   esp_err_t err = esp_http_client_open(this->client_, 0); | ||||
|  | ||||
|   if (err != ESP_OK) { | ||||
|     ESP_LOGE(TAG, "Failed to open URL"); | ||||
|     this->cleanup_connection_(); | ||||
|     return err; | ||||
|   } | ||||
|  | ||||
|   int64_t header_length = esp_http_client_fetch_headers(this->client_); | ||||
|   uint8_t reattempt_count = 0; | ||||
|   while ((header_length < 0) && (reattempt_count < MAX_FETCHING_HEADER_ATTEMPTS)) { | ||||
|     this->cleanup_connection_(); | ||||
|     if (header_length != -ESP_ERR_HTTP_EAGAIN) { | ||||
|       // Serious error, no recovery | ||||
|       return ESP_FAIL; | ||||
|     } else { | ||||
|       // Reconnect from a fresh state to avoid a bug where it never reads the headers even if made available | ||||
|       this->client_ = esp_http_client_init(&client_config); | ||||
|       esp_http_client_open(this->client_, 0); | ||||
|       header_length = esp_http_client_fetch_headers(this->client_); | ||||
|       ++reattempt_count; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (header_length < 0) { | ||||
|     ESP_LOGE(TAG, "Failed to fetch headers"); | ||||
|     this->cleanup_connection_(); | ||||
|     return ESP_FAIL; | ||||
|   } | ||||
| @@ -135,7 +153,7 @@ esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) { | ||||
|  | ||||
|   ssize_t redirect_count = 0; | ||||
|  | ||||
|   while ((esp_http_client_set_redirection(this->client_) == ESP_OK) && (redirect_count < MAX_REDIRECTION)) { | ||||
|   while ((esp_http_client_set_redirection(this->client_) == ESP_OK) && (redirect_count < MAX_REDIRECTIONS)) { | ||||
|     err = esp_http_client_open(this->client_, 0); | ||||
|     if (err != ESP_OK) { | ||||
|       this->cleanup_connection_(); | ||||
| @@ -267,21 +285,24 @@ AudioReaderState AudioReader::http_read_() { | ||||
|       return AudioReaderState::FINISHED; | ||||
|     } | ||||
|   } else if (this->output_transfer_buffer_->free() > 0) { | ||||
|     size_t bytes_to_read = this->output_transfer_buffer_->free(); | ||||
|     int received_len = | ||||
|         esp_http_client_read(this->client_, (char *) this->output_transfer_buffer_->get_buffer_end(), bytes_to_read); | ||||
|     int received_len = esp_http_client_read(this->client_, (char *) this->output_transfer_buffer_->get_buffer_end(), | ||||
|                                             this->output_transfer_buffer_->free()); | ||||
|  | ||||
|     if (received_len > 0) { | ||||
|       this->output_transfer_buffer_->increase_buffer_length(received_len); | ||||
|       this->last_data_read_ms_ = millis(); | ||||
|     } else if (received_len < 0) { | ||||
|       return AudioReaderState::READING; | ||||
|     } else if (received_len <= 0) { | ||||
|       // HTTP read error | ||||
|       if (received_len == -1) { | ||||
|         // A true connection error occured, no chance at recovery | ||||
|         this->cleanup_connection_(); | ||||
|         return AudioReaderState::FAILED; | ||||
|     } else { | ||||
|       if (bytes_to_read > 0) { | ||||
|         // Read timed out | ||||
|         if ((millis() - this->last_data_read_ms_) > CONNECTION_TIMEOUT_MS) { | ||||
|       } | ||||
|  | ||||
|       // Read timed out, manually verify if it has been too long since the last successful read | ||||
|       if ((millis() - this->last_data_read_ms_) > MAX_FETCHING_HEADER_ATTEMPTS * CONNECTION_TIMEOUT_MS) { | ||||
|         ESP_LOGE(TAG, "Timed out"); | ||||
|         this->cleanup_connection_(); | ||||
|         return AudioReaderState::FAILED; | ||||
|       } | ||||
| @@ -289,7 +310,6 @@ AudioReaderState AudioReader::http_read_() { | ||||
|       delay(READ_WRITE_TIMEOUT_MS); | ||||
|     } | ||||
|   } | ||||
|   } | ||||
|  | ||||
|   return AudioReaderState::READING; | ||||
| } | ||||
|   | ||||
| @@ -7,11 +7,13 @@ | ||||
|  | ||||
| extern "C" { | ||||
| #include "rtos_pub.h" | ||||
| #include "spi.h" | ||||
| // rtos_pub.h must be included before the rest of the includes | ||||
|  | ||||
| #include "arm_arch.h" | ||||
| #include "general_dma_pub.h" | ||||
| #include "gpio_pub.h" | ||||
| #include "icu_pub.h" | ||||
| #include "spi.h" | ||||
| #undef SPI_DAT | ||||
| #undef SPI_BASE | ||||
| }; | ||||
| @@ -124,7 +126,7 @@ void BekenSPILEDStripLightOutput::setup() { | ||||
|   size_t buffer_size = this->get_buffer_size_(); | ||||
|   size_t dma_buffer_size = (buffer_size * 8) + (2 * 64); | ||||
|  | ||||
|   ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE); | ||||
|   RAMAllocator<uint8_t> allocator; | ||||
|   this->buf_ = allocator.allocate(buffer_size); | ||||
|   if (this->buf_ == nullptr) { | ||||
|     ESP_LOGE(TAG, "Cannot allocate LED buffer!"); | ||||
|   | ||||
| @@ -50,7 +50,7 @@ void BH1750Sensor::read_lx_(BH1750Mode mode, uint8_t mtreg, const std::function< | ||||
|   // turn on (after one-shot sensor automatically powers down) | ||||
|   uint8_t turn_on = BH1750_COMMAND_POWER_ON; | ||||
|   if (this->write(&turn_on, 1) != i2c::ERROR_OK) { | ||||
|     ESP_LOGW(TAG, "Turning on BH1750 failed"); | ||||
|     ESP_LOGW(TAG, "Power on failed"); | ||||
|     f(NAN); | ||||
|     return; | ||||
|   } | ||||
| @@ -60,7 +60,7 @@ void BH1750Sensor::read_lx_(BH1750Mode mode, uint8_t mtreg, const std::function< | ||||
|     uint8_t mtreg_hi = BH1750_COMMAND_MT_REG_HI | ((mtreg >> 5) & 0b111); | ||||
|     uint8_t mtreg_lo = BH1750_COMMAND_MT_REG_LO | ((mtreg >> 0) & 0b11111); | ||||
|     if (this->write(&mtreg_hi, 1) != i2c::ERROR_OK || this->write(&mtreg_lo, 1) != i2c::ERROR_OK) { | ||||
|       ESP_LOGW(TAG, "Setting measurement time for BH1750 failed"); | ||||
|       ESP_LOGW(TAG, "Set measurement time failed"); | ||||
|       active_mtreg_ = 0; | ||||
|       f(NAN); | ||||
|       return; | ||||
| @@ -88,7 +88,7 @@ void BH1750Sensor::read_lx_(BH1750Mode mode, uint8_t mtreg, const std::function< | ||||
|       return; | ||||
|   } | ||||
|   if (this->write(&cmd, 1) != i2c::ERROR_OK) { | ||||
|     ESP_LOGW(TAG, "Starting measurement for BH1750 failed"); | ||||
|     ESP_LOGW(TAG, "Start measurement failed"); | ||||
|     f(NAN); | ||||
|     return; | ||||
|   } | ||||
| @@ -99,7 +99,7 @@ void BH1750Sensor::read_lx_(BH1750Mode mode, uint8_t mtreg, const std::function< | ||||
|   this->set_timeout("read", meas_time, [this, mode, mtreg, f]() { | ||||
|     uint16_t raw_value; | ||||
|     if (this->read(reinterpret_cast<uint8_t *>(&raw_value), 2) != i2c::ERROR_OK) { | ||||
|       ESP_LOGW(TAG, "Reading BH1750 data failed"); | ||||
|       ESP_LOGW(TAG, "Read data failed"); | ||||
|       f(NAN); | ||||
|       return; | ||||
|     } | ||||
| @@ -156,7 +156,7 @@ void BH1750Sensor::update() { | ||||
|         this->publish_state(NAN); | ||||
|         return; | ||||
|       } | ||||
|       ESP_LOGD(TAG, "'%s': Got illuminance=%.1flx", this->get_name().c_str(), val); | ||||
|       ESP_LOGD(TAG, "'%s': Illuminance=%.1flx", this->get_name().c_str(), val); | ||||
|       this->status_clear_warning(); | ||||
|       this->publish_state(val); | ||||
|     }); | ||||
|   | ||||
| @@ -60,8 +60,8 @@ from esphome.const import ( | ||||
|     DEVICE_CLASS_WINDOW, | ||||
| ) | ||||
| from esphome.core import CORE, coroutine_with_priority | ||||
| from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity | ||||
| from esphome.cpp_generator import MockObjClass | ||||
| from esphome.cpp_helpers import setup_entity | ||||
| from esphome.util import Registry | ||||
|  | ||||
| CODEOWNERS = ["@esphome/core"] | ||||
| @@ -148,6 +148,7 @@ BinarySensorCondition = binary_sensor_ns.class_("BinarySensorCondition", Conditi | ||||
|  | ||||
| # Filters | ||||
| Filter = binary_sensor_ns.class_("Filter") | ||||
| TimeoutFilter = binary_sensor_ns.class_("TimeoutFilter", Filter, cg.Component) | ||||
| DelayedOnOffFilter = binary_sensor_ns.class_("DelayedOnOffFilter", Filter, cg.Component) | ||||
| DelayedOnFilter = binary_sensor_ns.class_("DelayedOnFilter", Filter, cg.Component) | ||||
| DelayedOffFilter = binary_sensor_ns.class_("DelayedOffFilter", Filter, cg.Component) | ||||
| @@ -171,6 +172,19 @@ async def invert_filter_to_code(config, filter_id): | ||||
|     return cg.new_Pvariable(filter_id) | ||||
|  | ||||
|  | ||||
| @register_filter( | ||||
|     "timeout", | ||||
|     TimeoutFilter, | ||||
|     cv.templatable(cv.positive_time_period_milliseconds), | ||||
| ) | ||||
| async def timeout_filter_to_code(config, filter_id): | ||||
|     var = cg.new_Pvariable(filter_id) | ||||
|     await cg.register_component(var, {}) | ||||
|     template_ = await cg.templatable(config, [], cg.uint32) | ||||
|     cg.add(var.set_timeout_value(template_)) | ||||
|     return var | ||||
|  | ||||
|  | ||||
| @register_filter( | ||||
|     "delayed_on_off", | ||||
|     DelayedOnOffFilter, | ||||
| @@ -491,6 +505,9 @@ _BINARY_SENSOR_SCHEMA = ( | ||||
| ) | ||||
|  | ||||
|  | ||||
| _BINARY_SENSOR_SCHEMA.add_extra(entity_duplicate_validator("binary_sensor")) | ||||
|  | ||||
|  | ||||
| def binary_sensor_schema( | ||||
|     class_: MockObjClass = cv.UNDEFINED, | ||||
|     *, | ||||
| @@ -521,7 +538,7 @@ BINARY_SENSOR_SCHEMA.add_extra(cv.deprecated_schema_constant("binary_sensor")) | ||||
|  | ||||
|  | ||||
| async def setup_binary_sensor_core_(var, config): | ||||
|     await setup_entity(var, config) | ||||
|     await setup_entity(var, config, "binary_sensor") | ||||
|  | ||||
|     if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: | ||||
|         cg.add(var.set_device_class(device_class)) | ||||
|   | ||||
| @@ -25,6 +25,12 @@ void Filter::input(bool value) { | ||||
|   } | ||||
| } | ||||
|  | ||||
| void TimeoutFilter::input(bool value) { | ||||
|   this->set_timeout("timeout", this->timeout_delay_.value(), [this]() { this->parent_->invalidate_state(); }); | ||||
|   // we do not de-dup here otherwise changes from invalid to valid state will not be output | ||||
|   this->output(value); | ||||
| } | ||||
|  | ||||
| optional<bool> DelayedOnOffFilter::new_value(bool value) { | ||||
|   if (value) { | ||||
|     this->set_timeout("ON_OFF", this->on_delay_.value(), [this]() { this->output(true); }); | ||||
|   | ||||
| @@ -16,7 +16,7 @@ class Filter { | ||||
|  public: | ||||
|   virtual optional<bool> new_value(bool value) = 0; | ||||
|  | ||||
|   void input(bool value); | ||||
|   virtual void input(bool value); | ||||
|  | ||||
|   void output(bool value); | ||||
|  | ||||
| @@ -28,6 +28,16 @@ class Filter { | ||||
|   Deduplicator<bool> dedup_; | ||||
| }; | ||||
|  | ||||
| class TimeoutFilter : public Filter, public Component { | ||||
|  public: | ||||
|   optional<bool> new_value(bool value) override { return value; } | ||||
|   void input(bool value) override; | ||||
|   template<typename T> void set_timeout_value(T timeout) { this->timeout_delay_ = timeout; } | ||||
|  | ||||
|  protected: | ||||
|   TemplatableValue<uint32_t> timeout_delay_{}; | ||||
| }; | ||||
|  | ||||
| class DelayedOnOffFilter : public Filter, public Component { | ||||
|  public: | ||||
|   optional<bool> new_value(bool value) override; | ||||
|   | ||||
| @@ -18,8 +18,8 @@ from esphome.const import ( | ||||
|     DEVICE_CLASS_UPDATE, | ||||
| ) | ||||
| from esphome.core import CORE, coroutine_with_priority | ||||
| from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity | ||||
| from esphome.cpp_generator import MockObjClass | ||||
| from esphome.cpp_helpers import setup_entity | ||||
|  | ||||
| CODEOWNERS = ["@esphome/core"] | ||||
| IS_PLATFORM_COMPONENT = True | ||||
| @@ -61,6 +61,9 @@ _BUTTON_SCHEMA = ( | ||||
| ) | ||||
|  | ||||
|  | ||||
| _BUTTON_SCHEMA.add_extra(entity_duplicate_validator("button")) | ||||
|  | ||||
|  | ||||
| def button_schema( | ||||
|     class_: MockObjClass, | ||||
|     *, | ||||
| @@ -87,7 +90,7 @@ BUTTON_SCHEMA.add_extra(cv.deprecated_schema_constant("button")) | ||||
|  | ||||
|  | ||||
| async def setup_button_core_(var, config): | ||||
|     await setup_entity(var, config) | ||||
|     await setup_entity(var, config, "button") | ||||
|  | ||||
|     for conf in config.get(CONF_ON_PRESS, []): | ||||
|         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) | ||||
|   | ||||
| @@ -48,8 +48,8 @@ from esphome.const import ( | ||||
|     CONF_WEB_SERVER, | ||||
| ) | ||||
| from esphome.core import CORE, coroutine_with_priority | ||||
| from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity | ||||
| from esphome.cpp_generator import MockObjClass | ||||
| from esphome.cpp_helpers import setup_entity | ||||
|  | ||||
| IS_PLATFORM_COMPONENT = True | ||||
|  | ||||
| @@ -247,6 +247,9 @@ _CLIMATE_SCHEMA = ( | ||||
| ) | ||||
|  | ||||
|  | ||||
| _CLIMATE_SCHEMA.add_extra(entity_duplicate_validator("climate")) | ||||
|  | ||||
|  | ||||
| def climate_schema( | ||||
|     class_: MockObjClass, | ||||
|     *, | ||||
| @@ -273,7 +276,7 @@ CLIMATE_SCHEMA.add_extra(cv.deprecated_schema_constant("climate")) | ||||
|  | ||||
|  | ||||
| async def setup_climate_core_(var, config): | ||||
|     await setup_entity(var, config) | ||||
|     await setup_entity(var, config, "climate") | ||||
|  | ||||
|     visual = config[CONF_VISUAL] | ||||
|     if (min_temp := visual.get(CONF_MIN_TEMPERATURE)) is not None: | ||||
|   | ||||
| @@ -33,8 +33,8 @@ from esphome.const import ( | ||||
|     DEVICE_CLASS_WINDOW, | ||||
| ) | ||||
| from esphome.core import CORE, coroutine_with_priority | ||||
| from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity | ||||
| from esphome.cpp_generator import MockObjClass | ||||
| from esphome.cpp_helpers import setup_entity | ||||
|  | ||||
| IS_PLATFORM_COMPONENT = True | ||||
|  | ||||
| @@ -126,6 +126,9 @@ _COVER_SCHEMA = ( | ||||
| ) | ||||
|  | ||||
|  | ||||
| _COVER_SCHEMA.add_extra(entity_duplicate_validator("cover")) | ||||
|  | ||||
|  | ||||
| def cover_schema( | ||||
|     class_: MockObjClass, | ||||
|     *, | ||||
| @@ -154,7 +157,7 @@ COVER_SCHEMA.add_extra(cv.deprecated_schema_constant("cover")) | ||||
|  | ||||
|  | ||||
| async def setup_cover_core_(var, config): | ||||
|     await setup_entity(var, config) | ||||
|     await setup_entity(var, config, "cover") | ||||
|  | ||||
|     if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: | ||||
|         cg.add(var.set_device_class(device_class)) | ||||
|   | ||||
| @@ -22,8 +22,8 @@ from esphome.const import ( | ||||
|     CONF_YEAR, | ||||
| ) | ||||
| from esphome.core import CORE, coroutine_with_priority | ||||
| from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity | ||||
| from esphome.cpp_generator import MockObjClass | ||||
| from esphome.cpp_helpers import setup_entity | ||||
|  | ||||
| CODEOWNERS = ["@rfdarter", "@jesserockz"] | ||||
|  | ||||
| @@ -84,6 +84,8 @@ _DATETIME_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( | ||||
|     .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) | ||||
| ).add_extra(_validate_time_present) | ||||
|  | ||||
| _DATETIME_SCHEMA.add_extra(entity_duplicate_validator("datetime")) | ||||
|  | ||||
|  | ||||
| def date_schema(class_: MockObjClass) -> cv.Schema: | ||||
|     schema = cv.Schema( | ||||
| @@ -133,7 +135,7 @@ def datetime_schema(class_: MockObjClass) -> cv.Schema: | ||||
|  | ||||
|  | ||||
| async def setup_datetime_core_(var, config): | ||||
|     await setup_entity(var, config) | ||||
|     await setup_entity(var, config, "datetime") | ||||
|  | ||||
|     if (mqtt_id := config.get(CONF_MQTT_ID)) is not None: | ||||
|         mqtt_ = cg.new_Pvariable(mqtt_id, var) | ||||
|   | ||||
| @@ -455,7 +455,7 @@ CONFIG_SCHEMA = cv.Schema( | ||||
|                     CONF_NAME: "Demo Plain Sensor", | ||||
|                 }, | ||||
|                 { | ||||
|                     CONF_NAME: "Demo Temperature Sensor", | ||||
|                     CONF_NAME: "Demo Temperature Sensor 1", | ||||
|                     CONF_UNIT_OF_MEASUREMENT: UNIT_CELSIUS, | ||||
|                     CONF_ICON: ICON_THERMOMETER, | ||||
|                     CONF_ACCURACY_DECIMALS: 1, | ||||
| @@ -463,7 +463,7 @@ CONFIG_SCHEMA = cv.Schema( | ||||
|                     CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT, | ||||
|                 }, | ||||
|                 { | ||||
|                     CONF_NAME: "Demo Temperature Sensor", | ||||
|                     CONF_NAME: "Demo Temperature Sensor 2", | ||||
|                     CONF_UNIT_OF_MEASUREMENT: UNIT_CELSIUS, | ||||
|                     CONF_ICON: ICON_THERMOMETER, | ||||
|                     CONF_ACCURACY_DECIMALS: 1, | ||||
|   | ||||
| @@ -4,7 +4,7 @@ import logging | ||||
| import os | ||||
| from pathlib import Path | ||||
|  | ||||
| from esphome import git | ||||
| from esphome import yaml_util | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
| @@ -23,7 +23,6 @@ from esphome.const import ( | ||||
|     CONF_REFRESH, | ||||
|     CONF_SOURCE, | ||||
|     CONF_TYPE, | ||||
|     CONF_URL, | ||||
|     CONF_VARIANT, | ||||
|     CONF_VERSION, | ||||
|     KEY_CORE, | ||||
| @@ -32,14 +31,13 @@ from esphome.const import ( | ||||
|     KEY_TARGET_FRAMEWORK, | ||||
|     KEY_TARGET_PLATFORM, | ||||
|     PLATFORM_ESP32, | ||||
|     TYPE_GIT, | ||||
|     TYPE_LOCAL, | ||||
|     __version__, | ||||
| ) | ||||
| from esphome.core import CORE, HexInt, TimePeriod | ||||
| from esphome.cpp_generator import RawExpression | ||||
| import esphome.final_validate as fv | ||||
| from esphome.helpers import copy_file_if_changed, mkdir_p, write_file_if_changed | ||||
| from esphome.types import ConfigType | ||||
|  | ||||
| from .boards import BOARDS | ||||
| from .const import (  # noqa | ||||
| @@ -49,10 +47,8 @@ from .const import (  # noqa | ||||
|     KEY_EXTRA_BUILD_FILES, | ||||
|     KEY_PATH, | ||||
|     KEY_REF, | ||||
|     KEY_REFRESH, | ||||
|     KEY_REPO, | ||||
|     KEY_SDKCONFIG_OPTIONS, | ||||
|     KEY_SUBMODULES, | ||||
|     KEY_VARIANT, | ||||
|     VARIANT_ESP32, | ||||
|     VARIANT_ESP32C2, | ||||
| @@ -235,7 +231,7 @@ def add_idf_sdkconfig_option(name: str, value: SdkconfigValueType): | ||||
| def add_idf_component( | ||||
|     *, | ||||
|     name: str, | ||||
|     repo: str, | ||||
|     repo: str = None, | ||||
|     ref: str = None, | ||||
|     path: str = None, | ||||
|     refresh: TimePeriod = None, | ||||
| @@ -245,30 +241,27 @@ def add_idf_component( | ||||
|     """Add an esp-idf component to the project.""" | ||||
|     if not CORE.using_esp_idf: | ||||
|         raise ValueError("Not an esp-idf project") | ||||
|     if components is None: | ||||
|         components = [] | ||||
|     if name not in CORE.data[KEY_ESP32][KEY_COMPONENTS]: | ||||
|     if not repo and not ref and not path: | ||||
|         raise ValueError("Requires at least one of repo, ref or path") | ||||
|     if refresh or submodules or components: | ||||
|         _LOGGER.warning( | ||||
|             "The refresh, components and submodules parameters in add_idf_component() are " | ||||
|             "deprecated and will be removed in ESPHome 2026.1. If you are seeing this, report " | ||||
|             "an issue to the external_component author and ask them to update it." | ||||
|         ) | ||||
|     if components: | ||||
|         for comp in components: | ||||
|             CORE.data[KEY_ESP32][KEY_COMPONENTS][comp] = { | ||||
|                 KEY_REPO: repo, | ||||
|                 KEY_REF: ref, | ||||
|                 KEY_PATH: f"{path}/{comp}" if path else comp, | ||||
|             } | ||||
|     else: | ||||
|         CORE.data[KEY_ESP32][KEY_COMPONENTS][name] = { | ||||
|             KEY_REPO: repo, | ||||
|             KEY_REF: ref, | ||||
|             KEY_PATH: path, | ||||
|             KEY_REFRESH: refresh, | ||||
|             KEY_COMPONENTS: components, | ||||
|             KEY_SUBMODULES: submodules, | ||||
|         } | ||||
|     else: | ||||
|         component_config = CORE.data[KEY_ESP32][KEY_COMPONENTS][name] | ||||
|         if components is not None: | ||||
|             component_config[KEY_COMPONENTS] = list( | ||||
|                 set(component_config[KEY_COMPONENTS] + components) | ||||
|             ) | ||||
|         if submodules is not None: | ||||
|             if component_config[KEY_SUBMODULES] is None: | ||||
|                 component_config[KEY_SUBMODULES] = submodules | ||||
|             else: | ||||
|                 component_config[KEY_SUBMODULES] = list( | ||||
|                     set(component_config[KEY_SUBMODULES] + submodules) | ||||
|                 ) | ||||
|  | ||||
|  | ||||
| def add_extra_script(stage: str, filename: str, path: str): | ||||
| @@ -575,6 +568,17 @@ CONF_ENABLE_LWIP_DHCP_SERVER = "enable_lwip_dhcp_server" | ||||
| CONF_ENABLE_LWIP_MDNS_QUERIES = "enable_lwip_mdns_queries" | ||||
| CONF_ENABLE_LWIP_BRIDGE_INTERFACE = "enable_lwip_bridge_interface" | ||||
|  | ||||
|  | ||||
| def _validate_idf_component(config: ConfigType) -> ConfigType: | ||||
|     """Validate IDF component config and warn about deprecated options.""" | ||||
|     if CONF_REFRESH in config: | ||||
|         _LOGGER.warning( | ||||
|             "The 'refresh' option for IDF components is deprecated and has no effect. " | ||||
|             "It will be removed in ESPHome 2026.1. Please remove it from your configuration." | ||||
|         ) | ||||
|     return config | ||||
|  | ||||
|  | ||||
| ESP_IDF_FRAMEWORK_SCHEMA = cv.All( | ||||
|     cv.Schema( | ||||
|         { | ||||
| @@ -606,7 +610,7 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All( | ||||
|                         CONF_ENABLE_LWIP_DHCP_SERVER, "wifi", default=False | ||||
|                     ): cv.boolean, | ||||
|                     cv.Optional( | ||||
|                         CONF_ENABLE_LWIP_MDNS_QUERIES, default=False | ||||
|                         CONF_ENABLE_LWIP_MDNS_QUERIES, default=True | ||||
|                     ): cv.boolean, | ||||
|                     cv.Optional( | ||||
|                         CONF_ENABLE_LWIP_BRIDGE_INTERFACE, default=False | ||||
| @@ -614,15 +618,19 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All( | ||||
|                 } | ||||
|             ), | ||||
|             cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list( | ||||
|                 cv.All( | ||||
|                     cv.Schema( | ||||
|                         { | ||||
|                             cv.Required(CONF_NAME): cv.string_strict, | ||||
|                         cv.Required(CONF_SOURCE): cv.SOURCE_SCHEMA, | ||||
|                             cv.Optional(CONF_SOURCE): cv.git_ref, | ||||
|                             cv.Optional(CONF_REF): cv.string, | ||||
|                             cv.Optional(CONF_PATH): cv.string, | ||||
|                         cv.Optional(CONF_REFRESH, default="1d"): cv.All( | ||||
|                             cv.Optional(CONF_REFRESH): cv.All( | ||||
|                                 cv.string, cv.source_refresh | ||||
|                             ), | ||||
|                         } | ||||
|                     ), | ||||
|                     _validate_idf_component, | ||||
|                 ) | ||||
|             ), | ||||
|         } | ||||
| @@ -762,7 +770,7 @@ async def to_code(config): | ||||
|             and not advanced[CONF_ENABLE_LWIP_DHCP_SERVER] | ||||
|         ): | ||||
|             add_idf_sdkconfig_option("CONFIG_LWIP_DHCPS", False) | ||||
|         if not advanced.get(CONF_ENABLE_LWIP_MDNS_QUERIES, False): | ||||
|         if not advanced.get(CONF_ENABLE_LWIP_MDNS_QUERIES, True): | ||||
|             add_idf_sdkconfig_option("CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES", False) | ||||
|         if not advanced.get(CONF_ENABLE_LWIP_BRIDGE_INTERFACE, False): | ||||
|             add_idf_sdkconfig_option("CONFIG_LWIP_BRIDGEIF_MAX_PORTS", 0) | ||||
| @@ -814,18 +822,12 @@ async def to_code(config): | ||||
|             add_idf_sdkconfig_option(name, RawSdkconfigValue(value)) | ||||
|  | ||||
|         for component in conf[CONF_COMPONENTS]: | ||||
|             source = component[CONF_SOURCE] | ||||
|             if source[CONF_TYPE] == TYPE_GIT: | ||||
|             add_idf_component( | ||||
|                 name=component[CONF_NAME], | ||||
|                     repo=source[CONF_URL], | ||||
|                     ref=source.get(CONF_REF), | ||||
|                 repo=component.get(CONF_SOURCE), | ||||
|                 ref=component.get(CONF_REF), | ||||
|                 path=component.get(CONF_PATH), | ||||
|                     refresh=component[CONF_REFRESH], | ||||
|             ) | ||||
|             elif source[CONF_TYPE] == TYPE_LOCAL: | ||||
|                 _LOGGER.warning("Local components are not implemented yet.") | ||||
|  | ||||
|     elif conf[CONF_TYPE] == FRAMEWORK_ARDUINO: | ||||
|         cg.add_platformio_option("framework", "arduino") | ||||
|         cg.add_build_flag("-DUSE_ARDUINO") | ||||
| @@ -924,6 +926,26 @@ def _write_sdkconfig(): | ||||
|         write_file_if_changed(sdk_path, contents) | ||||
|  | ||||
|  | ||||
| def _write_idf_component_yml(): | ||||
|     yml_path = Path(CORE.relative_build_path("src/idf_component.yml")) | ||||
|     if CORE.data[KEY_ESP32][KEY_COMPONENTS]: | ||||
|         components: dict = CORE.data[KEY_ESP32][KEY_COMPONENTS] | ||||
|         dependencies = {} | ||||
|         for name, component in components.items(): | ||||
|             dependency = {} | ||||
|             if component[KEY_REF]: | ||||
|                 dependency["version"] = component[KEY_REF] | ||||
|             if component[KEY_REPO]: | ||||
|                 dependency["git"] = component[KEY_REPO] | ||||
|             if component[KEY_PATH]: | ||||
|                 dependency["path"] = component[KEY_PATH] | ||||
|             dependencies[name] = dependency | ||||
|         contents = yaml_util.dump({"dependencies": dependencies}) | ||||
|     else: | ||||
|         contents = "" | ||||
|     write_file_if_changed(yml_path, contents) | ||||
|  | ||||
|  | ||||
| # Called by writer.py | ||||
| def copy_files(): | ||||
|     if CORE.using_arduino: | ||||
| @@ -936,6 +958,7 @@ def copy_files(): | ||||
|             ) | ||||
|     if CORE.using_esp_idf: | ||||
|         _write_sdkconfig() | ||||
|         _write_idf_component_yml() | ||||
|         if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]: | ||||
|             write_file_if_changed( | ||||
|                 CORE.relative_build_path("partitions.csv"), | ||||
| @@ -952,55 +975,6 @@ def copy_files(): | ||||
|             __version__, | ||||
|         ) | ||||
|  | ||||
|         import shutil | ||||
|  | ||||
|         shutil.rmtree(CORE.relative_build_path("components"), ignore_errors=True) | ||||
|  | ||||
|         if CORE.data[KEY_ESP32][KEY_COMPONENTS]: | ||||
|             components: dict = CORE.data[KEY_ESP32][KEY_COMPONENTS] | ||||
|  | ||||
|             for name, component in components.items(): | ||||
|                 repo_dir, _ = git.clone_or_update( | ||||
|                     url=component[KEY_REPO], | ||||
|                     ref=component[KEY_REF], | ||||
|                     refresh=component[KEY_REFRESH], | ||||
|                     domain="idf_components", | ||||
|                     submodules=component[KEY_SUBMODULES], | ||||
|                 ) | ||||
|                 mkdir_p(CORE.relative_build_path("components")) | ||||
|                 component_dir = repo_dir | ||||
|                 if component[KEY_PATH] is not None: | ||||
|                     component_dir = component_dir / component[KEY_PATH] | ||||
|  | ||||
|                 if component[KEY_COMPONENTS] == ["*"]: | ||||
|                     shutil.copytree( | ||||
|                         component_dir, | ||||
|                         CORE.relative_build_path("components"), | ||||
|                         dirs_exist_ok=True, | ||||
|                         ignore=shutil.ignore_patterns(".git*"), | ||||
|                         symlinks=True, | ||||
|                         ignore_dangling_symlinks=True, | ||||
|                     ) | ||||
|                 elif len(component[KEY_COMPONENTS]) > 0: | ||||
|                     for comp in component[KEY_COMPONENTS]: | ||||
|                         shutil.copytree( | ||||
|                             component_dir / comp, | ||||
|                             CORE.relative_build_path(f"components/{comp}"), | ||||
|                             dirs_exist_ok=True, | ||||
|                             ignore=shutil.ignore_patterns(".git*"), | ||||
|                             symlinks=True, | ||||
|                             ignore_dangling_symlinks=True, | ||||
|                         ) | ||||
|                 else: | ||||
|                     shutil.copytree( | ||||
|                         component_dir, | ||||
|                         CORE.relative_build_path(f"components/{name}"), | ||||
|                         dirs_exist_ok=True, | ||||
|                         ignore=shutil.ignore_patterns(".git*"), | ||||
|                         symlinks=True, | ||||
|                         ignore_dangling_symlinks=True, | ||||
|                     ) | ||||
|  | ||||
|     for _, file in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES].items(): | ||||
|         if file[KEY_PATH].startswith("http"): | ||||
|             import requests | ||||
|   | ||||
| @@ -29,9 +29,9 @@ class ESP32InternalGPIOPin : public InternalGPIOPin { | ||||
|   void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; | ||||
|  | ||||
|   gpio_num_t pin_; | ||||
|   bool inverted_; | ||||
|   gpio_drive_cap_t drive_strength_; | ||||
|   gpio::Flags flags_; | ||||
|   bool inverted_; | ||||
|   // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) | ||||
|   static bool isr_service_installed; | ||||
| }; | ||||
|   | ||||
| @@ -4,6 +4,7 @@ | ||||
| #include "ble_event_pool.h" | ||||
|  | ||||
| #include "esphome/core/application.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| #include <esp_bt.h> | ||||
| @@ -516,13 +517,12 @@ void ESP32BLE::dump_config() { | ||||
|         break; | ||||
|     } | ||||
|     ESP_LOGCONFIG(TAG, | ||||
|                   "ESP32 BLE:\n" | ||||
|                   "  MAC address: %02X:%02X:%02X:%02X:%02X:%02X\n" | ||||
|                   "BLE:\n" | ||||
|                   "  MAC address: %s\n" | ||||
|                   "  IO Capability: %s", | ||||
|                   mac_address[0], mac_address[1], mac_address[2], mac_address[3], mac_address[4], mac_address[5], | ||||
|                   io_capability_s); | ||||
|                   format_mac_address_pretty(mac_address).c_str(), io_capability_s); | ||||
|   } else { | ||||
|     ESP_LOGCONFIG(TAG, "ESP32 BLE: bluetooth stack is not enabled"); | ||||
|     ESP_LOGCONFIG(TAG, "Bluetooth stack is not enabled"); | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -496,17 +496,17 @@ float BLEClientBase::parse_char_value(uint8_t *value, uint16_t length) { | ||||
|       if (length > 2) { | ||||
|         return (float) encode_uint16(value[1], value[2]); | ||||
|       } | ||||
|       // fall through | ||||
|       [[fallthrough]]; | ||||
|     case 0x7:  // uint24. | ||||
|       if (length > 3) { | ||||
|         return (float) encode_uint24(value[1], value[2], value[3]); | ||||
|       } | ||||
|       // fall through | ||||
|       [[fallthrough]]; | ||||
|     case 0x8:  // uint32. | ||||
|       if (length > 4) { | ||||
|         return (float) encode_uint32(value[1], value[2], value[3], value[4]); | ||||
|       } | ||||
|       // fall through | ||||
|       [[fallthrough]]; | ||||
|     case 0xC:  // int8. | ||||
|       return (float) ((int8_t) value[1]); | ||||
|     case 0xD:  // int12. | ||||
| @@ -514,12 +514,12 @@ float BLEClientBase::parse_char_value(uint8_t *value, uint16_t length) { | ||||
|       if (length > 2) { | ||||
|         return (float) ((int16_t) (value[1] << 8) + (int16_t) value[2]); | ||||
|       } | ||||
|       // fall through | ||||
|       [[fallthrough]]; | ||||
|     case 0xF:  // int24. | ||||
|       if (length > 3) { | ||||
|         return (float) ((int32_t) (value[1] << 16) + (int32_t) (value[2] << 8) + (int32_t) (value[3])); | ||||
|       } | ||||
|       // fall through | ||||
|       [[fallthrough]]; | ||||
|     case 0x10:  // int32. | ||||
|       if (length > 4) { | ||||
|         return (float) ((int32_t) (value[1] << 24) + (int32_t) (value[2] << 16) + (int32_t) (value[3] << 8) + | ||||
|   | ||||
| @@ -522,6 +522,7 @@ optional<ESPBLEiBeacon> ESPBLEiBeacon::from_manufacturer_data(const ServiceData | ||||
| } | ||||
|  | ||||
| void ESPBTDevice::parse_scan_rst(const BLEScanResult &scan_result) { | ||||
|   this->scan_result_ = &scan_result; | ||||
|   for (uint8_t i = 0; i < ESP_BD_ADDR_LEN; i++) | ||||
|     this->address_[i] = scan_result.bda[i]; | ||||
|   this->address_type_ = static_cast<esp_ble_addr_type_t>(scan_result.ble_addr_type); | ||||
|   | ||||
| @@ -85,6 +85,9 @@ class ESPBTDevice { | ||||
|  | ||||
|   const std::vector<ServiceData> &get_service_datas() const { return service_datas_; } | ||||
|  | ||||
|   // Exposed through a function for use in lambdas | ||||
|   const BLEScanResult &get_scan_result() const { return *scan_result_; } | ||||
|  | ||||
|   bool resolve_irk(const uint8_t *irk) const; | ||||
|  | ||||
|   optional<ESPBLEiBeacon> get_ibeacon() const { | ||||
| @@ -111,6 +114,7 @@ class ESPBTDevice { | ||||
|   std::vector<ESPBTUUID> service_uuids_{}; | ||||
|   std::vector<ServiceData> manufacturer_datas_{}; | ||||
|   std::vector<ServiceData> service_datas_{}; | ||||
|   const BLEScanResult *scan_result_{nullptr}; | ||||
| }; | ||||
|  | ||||
| class ESP32BLETracker; | ||||
|   | ||||
| @@ -19,7 +19,7 @@ from esphome.const import ( | ||||
|     CONF_VSYNC_PIN, | ||||
| ) | ||||
| from esphome.core import CORE | ||||
| from esphome.cpp_helpers import setup_entity | ||||
| from esphome.core.entity_helpers import setup_entity | ||||
|  | ||||
| DEPENDENCIES = ["esp32"] | ||||
|  | ||||
| @@ -284,7 +284,7 @@ SETTERS = { | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await setup_entity(var, config) | ||||
|     await setup_entity(var, config, "camera") | ||||
|     await cg.register_component(var, config) | ||||
|  | ||||
|     for key, setter in SETTERS.items(): | ||||
| @@ -310,11 +310,7 @@ async def to_code(config): | ||||
|     cg.add_define("USE_ESP32_CAMERA") | ||||
|  | ||||
|     if CORE.using_esp_idf: | ||||
|         add_idf_component( | ||||
|             name="esp32-camera", | ||||
|             repo="https://github.com/espressif/esp32-camera.git", | ||||
|             ref="v2.0.15", | ||||
|         ) | ||||
|         add_idf_component(name="espressif/esp32-camera", ref="2.0.15") | ||||
|  | ||||
|     for conf in config.get(CONF_ON_STREAM_START, []): | ||||
|         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) | ||||
|   | ||||
							
								
								
									
										0
									
								
								esphome/components/esp32_hall/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								esphome/components/esp32_hall/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										5
									
								
								esphome/components/esp32_hall/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								esphome/components/esp32_hall/sensor.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| import esphome.config_validation as cv | ||||
|  | ||||
| CONFIG_SCHEMA = cv.invalid( | ||||
|     "The esp32_hall component has been removed as of ESPHome 2025.7.0. See https://github.com/esphome/esphome/pull/9117 for details." | ||||
| ) | ||||
							
								
								
									
										101
									
								
								esphome/components/esp32_hosted/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								esphome/components/esp32_hosted/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,101 @@ | ||||
| import os | ||||
|  | ||||
| from esphome import pins | ||||
| from esphome.components import esp32 | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_CLK_PIN, | ||||
|     CONF_RESET_PIN, | ||||
|     CONF_VARIANT, | ||||
|     KEY_CORE, | ||||
|     KEY_FRAMEWORK_VERSION, | ||||
| ) | ||||
| from esphome.core import CORE | ||||
|  | ||||
| CODEOWNERS = ["@swoboda1337"] | ||||
|  | ||||
| CONF_ACTIVE_HIGH = "active_high" | ||||
| CONF_CMD_PIN = "cmd_pin" | ||||
| CONF_D0_PIN = "d0_pin" | ||||
| CONF_D1_PIN = "d1_pin" | ||||
| CONF_D2_PIN = "d2_pin" | ||||
| CONF_D3_PIN = "d3_pin" | ||||
| CONF_SLOT = "slot" | ||||
|  | ||||
| CONFIG_SCHEMA = cv.All( | ||||
|     cv.Schema( | ||||
|         { | ||||
|             cv.Required(CONF_VARIANT): cv.one_of(*esp32.VARIANTS, upper=True), | ||||
|             cv.Required(CONF_ACTIVE_HIGH): cv.boolean, | ||||
|             cv.Required(CONF_CLK_PIN): pins.internal_gpio_output_pin_number, | ||||
|             cv.Required(CONF_CMD_PIN): pins.internal_gpio_output_pin_number, | ||||
|             cv.Required(CONF_D0_PIN): pins.internal_gpio_output_pin_number, | ||||
|             cv.Required(CONF_D1_PIN): pins.internal_gpio_output_pin_number, | ||||
|             cv.Required(CONF_D2_PIN): pins.internal_gpio_output_pin_number, | ||||
|             cv.Required(CONF_D3_PIN): pins.internal_gpio_output_pin_number, | ||||
|             cv.Required(CONF_RESET_PIN): pins.internal_gpio_output_pin_number, | ||||
|             cv.Optional(CONF_SLOT, default=1): cv.int_range(min=0, max=1), | ||||
|         } | ||||
|     ), | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     if config[CONF_ACTIVE_HIGH]: | ||||
|         esp32.add_idf_sdkconfig_option( | ||||
|             "CONFIG_ESP_HOSTED_SDIO_RESET_ACTIVE_HIGH", | ||||
|             True, | ||||
|         ) | ||||
|     else: | ||||
|         esp32.add_idf_sdkconfig_option( | ||||
|             "CONFIG_ESP_HOSTED_SDIO_RESET_ACTIVE_LOW", | ||||
|             True, | ||||
|         ) | ||||
|     esp32.add_idf_sdkconfig_option( | ||||
|         "CONFIG_ESP_HOSTED_SDIO_GPIO_RESET_SLAVE",  # NOLINT | ||||
|         config[CONF_RESET_PIN], | ||||
|     ) | ||||
|     esp32.add_idf_sdkconfig_option( | ||||
|         f"CONFIG_SLAVE_IDF_TARGET_{config[CONF_VARIANT]}",  # NOLINT | ||||
|         True, | ||||
|     ) | ||||
|     esp32.add_idf_sdkconfig_option( | ||||
|         f"CONFIG_ESP_HOSTED_SDIO_SLOT_{config[CONF_SLOT]}", | ||||
|         True, | ||||
|     ) | ||||
|     esp32.add_idf_sdkconfig_option( | ||||
|         f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_CLK_SLOT_{config[CONF_SLOT]}", | ||||
|         config[CONF_CLK_PIN], | ||||
|     ) | ||||
|     esp32.add_idf_sdkconfig_option( | ||||
|         f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_CMD_SLOT_{config[CONF_SLOT]}", | ||||
|         config[CONF_CMD_PIN], | ||||
|     ) | ||||
|     esp32.add_idf_sdkconfig_option( | ||||
|         f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D0_SLOT_{config[CONF_SLOT]}", | ||||
|         config[CONF_D0_PIN], | ||||
|     ) | ||||
|     esp32.add_idf_sdkconfig_option( | ||||
|         f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D1_4BIT_BUS_SLOT_{config[CONF_SLOT]}", | ||||
|         config[CONF_D1_PIN], | ||||
|     ) | ||||
|     esp32.add_idf_sdkconfig_option( | ||||
|         f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D2_4BIT_BUS_SLOT_{config[CONF_SLOT]}", | ||||
|         config[CONF_D2_PIN], | ||||
|     ) | ||||
|     esp32.add_idf_sdkconfig_option( | ||||
|         f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D3_4BIT_BUS_SLOT_{config[CONF_SLOT]}", | ||||
|         config[CONF_D3_PIN], | ||||
|     ) | ||||
|     esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_CUSTOM_SDIO_PINS", True) | ||||
|  | ||||
|     framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] | ||||
|     os.environ["ESP_IDF_VERSION"] = f"{framework_ver.major}.{framework_ver.minor}" | ||||
|     esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.10.2") | ||||
|     esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0") | ||||
|     esp32.add_idf_component(name="espressif/esp_hosted", ref="2.0.11") | ||||
|     esp32.add_extra_script( | ||||
|         "post", | ||||
|         "esp32_hosted.py", | ||||
|         os.path.join(os.path.dirname(__file__), "esp32_hosted.py.script"), | ||||
|     ) | ||||
							
								
								
									
										12
									
								
								esphome/components/esp32_hosted/esp32_hosted.py.script
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								esphome/components/esp32_hosted/esp32_hosted.py.script
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| # pylint: disable=E0602 | ||||
| Import("env")  # noqa | ||||
|  | ||||
| # Workaround whole archive issue | ||||
| if "__LIB_DEPS" in env and "libespressif__esp_hosted.a" in env["__LIB_DEPS"]: | ||||
|     env.Append( | ||||
|         LINKFLAGS=[ | ||||
|             "-Wl,--whole-archive", | ||||
|             env["BUILD_DIR"] + "/esp-idf/espressif__esp_hosted/libespressif__esp_hosted.a", | ||||
|             "-Wl,--no-whole-archive", | ||||
|         ] | ||||
|     ) | ||||
| @@ -15,7 +15,7 @@ | ||||
| namespace esphome { | ||||
| namespace ethernet { | ||||
|  | ||||
| enum EthernetType { | ||||
| enum EthernetType : uint8_t { | ||||
|   ETHERNET_TYPE_UNKNOWN = 0, | ||||
|   ETHERNET_TYPE_LAN8720, | ||||
|   ETHERNET_TYPE_RTL8201, | ||||
| @@ -42,7 +42,7 @@ struct PHYRegister { | ||||
|   uint32_t page; | ||||
| }; | ||||
|  | ||||
| enum class EthernetComponentState { | ||||
| enum class EthernetComponentState : uint8_t { | ||||
|   STOPPED, | ||||
|   CONNECTING, | ||||
|   CONNECTED, | ||||
| @@ -119,25 +119,31 @@ class EthernetComponent : public Component { | ||||
|   uint32_t polling_interval_{0}; | ||||
| #endif | ||||
| #else | ||||
|   uint8_t phy_addr_{0}; | ||||
|   // Group all 32-bit members first | ||||
|   int power_pin_{-1}; | ||||
|   uint8_t mdc_pin_{23}; | ||||
|   uint8_t mdio_pin_{18}; | ||||
|   emac_rmii_clock_mode_t clk_mode_{EMAC_CLK_EXT_IN}; | ||||
|   emac_rmii_clock_gpio_t clk_gpio_{EMAC_CLK_IN_GPIO}; | ||||
|   std::vector<PHYRegister> phy_registers_{}; | ||||
| #endif | ||||
|   EthernetType type_{ETHERNET_TYPE_UNKNOWN}; | ||||
|   optional<ManualIP> manual_ip_{}; | ||||
|  | ||||
|   // Group all 8-bit members together | ||||
|   uint8_t phy_addr_{0}; | ||||
|   uint8_t mdc_pin_{23}; | ||||
|   uint8_t mdio_pin_{18}; | ||||
| #endif | ||||
|   optional<ManualIP> manual_ip_{}; | ||||
|   uint32_t connect_begin_; | ||||
|  | ||||
|   // Group all uint8_t types together (enums and bools) | ||||
|   EthernetType type_{ETHERNET_TYPE_UNKNOWN}; | ||||
|   EthernetComponentState state_{EthernetComponentState::STOPPED}; | ||||
|   bool started_{false}; | ||||
|   bool connected_{false}; | ||||
|   bool got_ipv4_address_{false}; | ||||
| #if LWIP_IPV6 | ||||
|   uint8_t ipv6_count_{0}; | ||||
| #endif /* LWIP_IPV6 */ | ||||
|   EthernetComponentState state_{EthernetComponentState::STOPPED}; | ||||
|   uint32_t connect_begin_; | ||||
|  | ||||
|   // Pointers at the end (naturally aligned) | ||||
|   esp_netif_t *eth_netif_{nullptr}; | ||||
|   esp_eth_handle_t eth_handle_; | ||||
|   esp_eth_phy_t *phy_{nullptr}; | ||||
|   | ||||
| @@ -18,8 +18,8 @@ from esphome.const import ( | ||||
|     DEVICE_CLASS_MOTION, | ||||
| ) | ||||
| from esphome.core import CORE, coroutine_with_priority | ||||
| from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity | ||||
| from esphome.cpp_generator import MockObjClass | ||||
| from esphome.cpp_helpers import setup_entity | ||||
|  | ||||
| CODEOWNERS = ["@nohat"] | ||||
| IS_PLATFORM_COMPONENT = True | ||||
| @@ -59,6 +59,9 @@ _EVENT_SCHEMA = ( | ||||
| ) | ||||
|  | ||||
|  | ||||
| _EVENT_SCHEMA.add_extra(entity_duplicate_validator("event")) | ||||
|  | ||||
|  | ||||
| def event_schema( | ||||
|     class_: MockObjClass = cv.UNDEFINED, | ||||
|     *, | ||||
| @@ -88,7 +91,7 @@ EVENT_SCHEMA.add_extra(cv.deprecated_schema_constant("event")) | ||||
|  | ||||
|  | ||||
| async def setup_event_core_(var, config, *, event_types: list[str]): | ||||
|     await setup_entity(var, config) | ||||
|     await setup_entity(var, config, "event") | ||||
|  | ||||
|     for conf in config.get(CONF_ON_EVENT, []): | ||||
|         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) | ||||
|   | ||||
| @@ -32,7 +32,7 @@ from esphome.const import ( | ||||
|     CONF_WEB_SERVER, | ||||
| ) | ||||
| from esphome.core import CORE, coroutine_with_priority | ||||
| from esphome.cpp_helpers import setup_entity | ||||
| from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity | ||||
|  | ||||
| IS_PLATFORM_COMPONENT = True | ||||
|  | ||||
| @@ -161,6 +161,9 @@ _FAN_SCHEMA = ( | ||||
| ) | ||||
|  | ||||
|  | ||||
| _FAN_SCHEMA.add_extra(entity_duplicate_validator("fan")) | ||||
|  | ||||
|  | ||||
| def fan_schema( | ||||
|     class_: cg.Pvariable, | ||||
|     *, | ||||
| @@ -225,7 +228,7 @@ def validate_preset_modes(value): | ||||
|  | ||||
|  | ||||
| async def setup_fan_core_(var, config): | ||||
|     await setup_entity(var, config) | ||||
|     await setup_entity(var, config, "fan") | ||||
|  | ||||
|     cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE])) | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| from collections.abc import MutableMapping | ||||
| import functools | ||||
| import hashlib | ||||
| from itertools import accumulate | ||||
| import logging | ||||
| import os | ||||
| from pathlib import Path | ||||
| @@ -468,8 +469,9 @@ class EFont: | ||||
|  | ||||
|  | ||||
| class GlyphInfo: | ||||
|     def __init__(self, data_len, advance, offset_x, offset_y, width, height): | ||||
|         self.data_len = data_len | ||||
|     def __init__(self, glyph, data, advance, offset_x, offset_y, width, height): | ||||
|         self.glyph = glyph | ||||
|         self.bitmap_data = data | ||||
|         self.advance = advance | ||||
|         self.offset_x = offset_x | ||||
|         self.offset_y = offset_y | ||||
| @@ -477,6 +479,62 @@ class GlyphInfo: | ||||
|         self.height = height | ||||
|  | ||||
|  | ||||
| def glyph_to_glyphinfo(glyph, font, size, bpp): | ||||
|     scale = 256 // (1 << bpp) | ||||
|     if not font.is_scalable: | ||||
|         sizes = [pt_to_px(x.size) for x in font.available_sizes] | ||||
|         if size in sizes: | ||||
|             font.select_size(sizes.index(size)) | ||||
|     else: | ||||
|         font.set_pixel_sizes(size, 0) | ||||
|     flags = FT_LOAD_RENDER | ||||
|     if bpp != 1: | ||||
|         flags |= FT_LOAD_NO_BITMAP | ||||
|     else: | ||||
|         flags |= FT_LOAD_TARGET_MONO | ||||
|     font.load_char(glyph, flags) | ||||
|     width = font.glyph.bitmap.width | ||||
|     height = font.glyph.bitmap.rows | ||||
|     buffer = font.glyph.bitmap.buffer | ||||
|     pitch = font.glyph.bitmap.pitch | ||||
|     glyph_data = [0] * ((height * width * bpp + 7) // 8) | ||||
|     src_mode = font.glyph.bitmap.pixel_mode | ||||
|     pos = 0 | ||||
|     for y in range(height): | ||||
|         for x in range(width): | ||||
|             if src_mode == ft_pixel_mode_mono: | ||||
|                 pixel = ( | ||||
|                     (1 << bpp) - 1 | ||||
|                     if buffer[y * pitch + x // 8] & (1 << (7 - x % 8)) | ||||
|                     else 0 | ||||
|                 ) | ||||
|             else: | ||||
|                 pixel = buffer[y * pitch + x] // scale | ||||
|             for bit_num in range(bpp): | ||||
|                 if pixel & (1 << (bpp - bit_num - 1)): | ||||
|                     glyph_data[pos // 8] |= 0x80 >> (pos % 8) | ||||
|                 pos += 1 | ||||
|     ascender = pt_to_px(font.size.ascender) | ||||
|     if ascender == 0: | ||||
|         if not font.is_scalable: | ||||
|             ascender = size | ||||
|         else: | ||||
|             _LOGGER.error( | ||||
|                 "Unable to determine ascender of font %s %s", | ||||
|                 font.family_name, | ||||
|                 font.style_name, | ||||
|             ) | ||||
|     return GlyphInfo( | ||||
|         glyph, | ||||
|         glyph_data, | ||||
|         pt_to_px(font.glyph.metrics.horiAdvance), | ||||
|         font.glyph.bitmap_left, | ||||
|         ascender - font.glyph.bitmap_top, | ||||
|         width, | ||||
|         height, | ||||
|     ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     """ | ||||
|     Collect all glyph codepoints, construct a map from a codepoint to a font file. | ||||
| @@ -506,98 +564,47 @@ async def to_code(config): | ||||
|  | ||||
|     codepoints = list(point_set) | ||||
|     codepoints.sort(key=functools.cmp_to_key(glyph_comparator)) | ||||
|     glyph_args = {} | ||||
|     data = [] | ||||
|     bpp = config[CONF_BPP] | ||||
|     scale = 256 // (1 << bpp) | ||||
|     size = config[CONF_SIZE] | ||||
|     # create the data array for all glyphs | ||||
|     for codepoint in codepoints: | ||||
|         font = point_font_map[codepoint] | ||||
|         if not font.is_scalable: | ||||
|             sizes = [pt_to_px(x.size) for x in font.available_sizes] | ||||
|             if size in sizes: | ||||
|                 font.select_size(sizes.index(size)) | ||||
|         else: | ||||
|             font.set_pixel_sizes(size, 0) | ||||
|         flags = FT_LOAD_RENDER | ||||
|         if bpp != 1: | ||||
|             flags |= FT_LOAD_NO_BITMAP | ||||
|         else: | ||||
|             flags |= FT_LOAD_TARGET_MONO | ||||
|         font.load_char(codepoint, flags) | ||||
|         width = font.glyph.bitmap.width | ||||
|         height = font.glyph.bitmap.rows | ||||
|         buffer = font.glyph.bitmap.buffer | ||||
|         pitch = font.glyph.bitmap.pitch | ||||
|         glyph_data = [0] * ((height * width * bpp + 7) // 8) | ||||
|         src_mode = font.glyph.bitmap.pixel_mode | ||||
|         pos = 0 | ||||
|         for y in range(height): | ||||
|             for x in range(width): | ||||
|                 if src_mode == ft_pixel_mode_mono: | ||||
|                     pixel = ( | ||||
|                         (1 << bpp) - 1 | ||||
|                         if buffer[y * pitch + x // 8] & (1 << (7 - x % 8)) | ||||
|                         else 0 | ||||
|                     ) | ||||
|                 else: | ||||
|                     pixel = buffer[y * pitch + x] // scale | ||||
|                 for bit_num in range(bpp): | ||||
|                     if pixel & (1 << (bpp - bit_num - 1)): | ||||
|                         glyph_data[pos // 8] |= 0x80 >> (pos % 8) | ||||
|                     pos += 1 | ||||
|         ascender = pt_to_px(font.size.ascender) | ||||
|         if ascender == 0: | ||||
|             if not font.is_scalable: | ||||
|                 ascender = size | ||||
|             else: | ||||
|                 _LOGGER.error( | ||||
|                     "Unable to determine ascender of font %s", config[CONF_FILE] | ||||
|                 ) | ||||
|         glyph_args[codepoint] = GlyphInfo( | ||||
|             len(data), | ||||
|             pt_to_px(font.glyph.metrics.horiAdvance), | ||||
|             font.glyph.bitmap_left, | ||||
|             ascender - font.glyph.bitmap_top, | ||||
|             width, | ||||
|             height, | ||||
|         ) | ||||
|         data += glyph_data | ||||
|  | ||||
|     rhs = [HexInt(x) for x in data] | ||||
|     glyph_args = [ | ||||
|         glyph_to_glyphinfo(x, point_font_map[x], size, bpp) for x in codepoints | ||||
|     ] | ||||
|     rhs = [HexInt(x) for x in flatten([x.bitmap_data for x in glyph_args])] | ||||
|     prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) | ||||
|  | ||||
|     # Create the glyph table that points to data in the above array. | ||||
|     glyph_initializer = [] | ||||
|     for codepoint in codepoints: | ||||
|         glyph_initializer.append( | ||||
|     glyph_initializer = [ | ||||
|         cg.StructInitializer( | ||||
|             GlyphData, | ||||
|             ( | ||||
|                 "a_char", | ||||
|                     cg.RawExpression( | ||||
|                         f"(const uint8_t *){cpp_string_escape(codepoint)}" | ||||
|                     ), | ||||
|                 cg.RawExpression(f"(const uint8_t *){cpp_string_escape(x.glyph)}"), | ||||
|             ), | ||||
|             ( | ||||
|                 "data", | ||||
|                     cg.RawExpression( | ||||
|                         f"{str(prog_arr)} + {str(glyph_args[codepoint].data_len)}" | ||||
|                 cg.RawExpression(f"{str(prog_arr)} + {str(y - len(x.bitmap_data))}"), | ||||
|             ), | ||||
|                 ), | ||||
|                 ("advance", glyph_args[codepoint].advance), | ||||
|                 ("offset_x", glyph_args[codepoint].offset_x), | ||||
|                 ("offset_y", glyph_args[codepoint].offset_y), | ||||
|                 ("width", glyph_args[codepoint].width), | ||||
|                 ("height", glyph_args[codepoint].height), | ||||
|             ("advance", x.advance), | ||||
|             ("offset_x", x.offset_x), | ||||
|             ("offset_y", x.offset_y), | ||||
|             ("width", x.width), | ||||
|             ("height", x.height), | ||||
|         ) | ||||
|         for (x, y) in zip( | ||||
|             glyph_args, list(accumulate([len(x.bitmap_data) for x in glyph_args])) | ||||
|         ) | ||||
|     ] | ||||
|  | ||||
|     glyphs = cg.static_const_array(config[CONF_RAW_GLYPH_ID], glyph_initializer) | ||||
|  | ||||
|     font_height = pt_to_px(base_font.size.height) | ||||
|     ascender = pt_to_px(base_font.size.ascender) | ||||
|     descender = abs(pt_to_px(base_font.size.descender)) | ||||
|     g = glyph_to_glyphinfo("x", base_font, size, bpp) | ||||
|     xheight = g.height if len(g.bitmap_data) > 1 else 0 | ||||
|     g = glyph_to_glyphinfo("X", base_font, size, bpp) | ||||
|     capheight = g.height if len(g.bitmap_data) > 1 else 0 | ||||
|     if font_height == 0: | ||||
|         if not base_font.is_scalable: | ||||
|             font_height = size | ||||
| @@ -610,5 +617,8 @@ async def to_code(config): | ||||
|         len(glyph_initializer), | ||||
|         ascender, | ||||
|         font_height, | ||||
|         descender, | ||||
|         xheight, | ||||
|         capheight, | ||||
|         bpp, | ||||
|     ) | ||||
|   | ||||
| @@ -45,8 +45,15 @@ void Glyph::scan_area(int *x1, int *y1, int *width, int *height) const { | ||||
|   *height = this->glyph_data_->height; | ||||
| } | ||||
|  | ||||
| Font::Font(const GlyphData *data, int data_nr, int baseline, int height, uint8_t bpp) | ||||
|     : baseline_(baseline), height_(height), bpp_(bpp) { | ||||
| Font::Font(const GlyphData *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight, | ||||
|            uint8_t bpp) | ||||
|     : baseline_(baseline), | ||||
|       height_(height), | ||||
|       descender_(descender), | ||||
|       linegap_(height - baseline - descender), | ||||
|       xheight_(xheight), | ||||
|       capheight_(capheight), | ||||
|       bpp_(bpp) { | ||||
|   glyphs_.reserve(data_nr); | ||||
|   for (int i = 0; i < data_nr; ++i) | ||||
|     glyphs_.emplace_back(&data[i]); | ||||
|   | ||||
| @@ -50,11 +50,17 @@ class Font | ||||
|  public: | ||||
|   /** Construct the font with the given glyphs. | ||||
|    * | ||||
|    * @param glyphs A vector of glyphs, must be sorted lexicographically. | ||||
|    * @param data A vector of glyphs, must be sorted lexicographically. | ||||
|    * @param data_nr The number of glyphs in data. | ||||
|    * @param baseline The y-offset from the top of the text to the baseline. | ||||
|    * @param bottom The y-offset from the top of the text to the bottom (i.e. height). | ||||
|    * @param height The y-offset from the top of the text to the bottom. | ||||
|    * @param descender The y-offset from the baseline to the lowest stroke in the font (e.g. from letters like g or p). | ||||
|    * @param xheight The height of lowercase letters, usually measured at the "x" glyph. | ||||
|    * @param capheight The height of capital letters, usually measured at the "X" glyph. | ||||
|    * @param bpp The bits per pixel used for this font. Used to read data out of the glyph bitmaps. | ||||
|    */ | ||||
|   Font(const GlyphData *data, int data_nr, int baseline, int height, uint8_t bpp = 1); | ||||
|   Font(const GlyphData *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight, | ||||
|        uint8_t bpp = 1); | ||||
|  | ||||
|   int match_next_glyph(const uint8_t *str, int *match_length); | ||||
|  | ||||
| @@ -65,6 +71,11 @@ class Font | ||||
| #endif | ||||
|   inline int get_baseline() { return this->baseline_; } | ||||
|   inline int get_height() { return this->height_; } | ||||
|   inline int get_ascender() { return this->baseline_; } | ||||
|   inline int get_descender() { return this->descender_; } | ||||
|   inline int get_linegap() { return this->linegap_; } | ||||
|   inline int get_xheight() { return this->xheight_; } | ||||
|   inline int get_capheight() { return this->capheight_; } | ||||
|   inline int get_bpp() { return this->bpp_; } | ||||
|  | ||||
|   const std::vector<Glyph, RAMAllocator<Glyph>> &get_glyphs() const { return glyphs_; } | ||||
| @@ -73,6 +84,10 @@ class Font | ||||
|   std::vector<Glyph, RAMAllocator<Glyph>> glyphs_; | ||||
|   int baseline_; | ||||
|   int height_; | ||||
|   int descender_; | ||||
|   int linegap_; | ||||
|   int xheight_; | ||||
|   int capheight_; | ||||
|   uint8_t bpp_;  // bits per pixel | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -41,6 +41,6 @@ CONFIG_SCHEMA = cv.All( | ||||
| async def to_code(config): | ||||
|     cg.add_build_flag("-DUSE_HOST") | ||||
|     cg.add_define("USE_ESPHOME_HOST_MAC_ADDRESS", config[CONF_MAC_ADDRESS].parts) | ||||
|     cg.add_build_flag("-std=c++17") | ||||
|     cg.add_build_flag("-std=gnu++17") | ||||
|     cg.add_define("ESPHOME_BOARD", "host") | ||||
|     cg.add_platformio_option("platform", "platformio/native") | ||||
|   | ||||
| @@ -8,6 +8,9 @@ | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
| #endif | ||||
|  | ||||
| #include "esphome/core/application.h" | ||||
|  | ||||
| #define CHECK_BIT(var, pos) (((var) >> (pos)) & 1) | ||||
| #define highbyte(val) (uint8_t)((val) >> 8) | ||||
| #define lowbyte(val) (uint8_t)((val) &0xff) | ||||
|  | ||||
| @@ -15,8 +18,162 @@ namespace esphome { | ||||
| namespace ld2410 { | ||||
|  | ||||
| static const char *const TAG = "ld2410"; | ||||
| static const char *const NO_MAC = "08:05:04:03:02:01"; | ||||
| static const char *const UNKNOWN_MAC = "unknown"; | ||||
| static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X"; | ||||
|  | ||||
| LD2410Component::LD2410Component() {} | ||||
| enum BaudRateStructure : uint8_t { | ||||
|   BAUD_RATE_9600 = 1, | ||||
|   BAUD_RATE_19200 = 2, | ||||
|   BAUD_RATE_38400 = 3, | ||||
|   BAUD_RATE_57600 = 4, | ||||
|   BAUD_RATE_115200 = 5, | ||||
|   BAUD_RATE_230400 = 6, | ||||
|   BAUD_RATE_256000 = 7, | ||||
|   BAUD_RATE_460800 = 8, | ||||
| }; | ||||
|  | ||||
| enum DistanceResolutionStructure : uint8_t { | ||||
|   DISTANCE_RESOLUTION_0_2 = 0x01, | ||||
|   DISTANCE_RESOLUTION_0_75 = 0x00, | ||||
| }; | ||||
|  | ||||
| enum LightFunctionStructure : uint8_t { | ||||
|   LIGHT_FUNCTION_OFF = 0x00, | ||||
|   LIGHT_FUNCTION_BELOW = 0x01, | ||||
|   LIGHT_FUNCTION_ABOVE = 0x02, | ||||
| }; | ||||
|  | ||||
| enum OutPinLevelStructure : uint8_t { | ||||
|   OUT_PIN_LEVEL_LOW = 0x00, | ||||
|   OUT_PIN_LEVEL_HIGH = 0x01, | ||||
| }; | ||||
|  | ||||
| enum PeriodicDataStructure : uint8_t { | ||||
|   DATA_TYPES = 6, | ||||
|   TARGET_STATES = 8, | ||||
|   MOVING_TARGET_LOW = 9, | ||||
|   MOVING_TARGET_HIGH = 10, | ||||
|   MOVING_ENERGY = 11, | ||||
|   STILL_TARGET_LOW = 12, | ||||
|   STILL_TARGET_HIGH = 13, | ||||
|   STILL_ENERGY = 14, | ||||
|   DETECT_DISTANCE_LOW = 15, | ||||
|   DETECT_DISTANCE_HIGH = 16, | ||||
|   MOVING_SENSOR_START = 19, | ||||
|   STILL_SENSOR_START = 28, | ||||
|   LIGHT_SENSOR = 37, | ||||
|   OUT_PIN_SENSOR = 38, | ||||
| }; | ||||
|  | ||||
| enum PeriodicDataValue : uint8_t { | ||||
|   HEAD = 0xAA, | ||||
|   END = 0x55, | ||||
|   CHECK = 0x00, | ||||
| }; | ||||
|  | ||||
| enum AckDataStructure : uint8_t { | ||||
|   COMMAND = 6, | ||||
|   COMMAND_STATUS = 7, | ||||
| }; | ||||
|  | ||||
| // Memory-efficient lookup tables | ||||
| struct StringToUint8 { | ||||
|   const char *str; | ||||
|   uint8_t value; | ||||
| }; | ||||
|  | ||||
| struct Uint8ToString { | ||||
|   uint8_t value; | ||||
|   const char *str; | ||||
| }; | ||||
|  | ||||
| constexpr StringToUint8 BAUD_RATES_BY_STR[] = { | ||||
|     {"9600", BAUD_RATE_9600},     {"19200", BAUD_RATE_19200},   {"38400", BAUD_RATE_38400}, | ||||
|     {"57600", BAUD_RATE_57600},   {"115200", BAUD_RATE_115200}, {"230400", BAUD_RATE_230400}, | ||||
|     {"256000", BAUD_RATE_256000}, {"460800", BAUD_RATE_460800}, | ||||
| }; | ||||
|  | ||||
| constexpr StringToUint8 DISTANCE_RESOLUTIONS_BY_STR[] = { | ||||
|     {"0.2m", DISTANCE_RESOLUTION_0_2}, | ||||
|     {"0.75m", DISTANCE_RESOLUTION_0_75}, | ||||
| }; | ||||
|  | ||||
| constexpr Uint8ToString DISTANCE_RESOLUTIONS_BY_UINT[] = { | ||||
|     {DISTANCE_RESOLUTION_0_2, "0.2m"}, | ||||
|     {DISTANCE_RESOLUTION_0_75, "0.75m"}, | ||||
| }; | ||||
|  | ||||
| constexpr StringToUint8 LIGHT_FUNCTIONS_BY_STR[] = { | ||||
|     {"off", LIGHT_FUNCTION_OFF}, | ||||
|     {"below", LIGHT_FUNCTION_BELOW}, | ||||
|     {"above", LIGHT_FUNCTION_ABOVE}, | ||||
| }; | ||||
|  | ||||
| constexpr Uint8ToString LIGHT_FUNCTIONS_BY_UINT[] = { | ||||
|     {LIGHT_FUNCTION_OFF, "off"}, | ||||
|     {LIGHT_FUNCTION_BELOW, "below"}, | ||||
|     {LIGHT_FUNCTION_ABOVE, "above"}, | ||||
| }; | ||||
|  | ||||
| constexpr StringToUint8 OUT_PIN_LEVELS_BY_STR[] = { | ||||
|     {"low", OUT_PIN_LEVEL_LOW}, | ||||
|     {"high", OUT_PIN_LEVEL_HIGH}, | ||||
| }; | ||||
|  | ||||
| constexpr Uint8ToString OUT_PIN_LEVELS_BY_UINT[] = { | ||||
|     {OUT_PIN_LEVEL_LOW, "low"}, | ||||
|     {OUT_PIN_LEVEL_HIGH, "high"}, | ||||
| }; | ||||
|  | ||||
| // Helper functions for lookups | ||||
| template<size_t N> uint8_t find_uint8(const StringToUint8 (&arr)[N], const std::string &str) { | ||||
|   for (const auto &entry : arr) { | ||||
|     if (str == entry.str) | ||||
|       return entry.value; | ||||
|   } | ||||
|   return 0xFF;  // Not found | ||||
| } | ||||
|  | ||||
| template<size_t N> const char *find_str(const Uint8ToString (&arr)[N], uint8_t value) { | ||||
|   for (const auto &entry : arr) { | ||||
|     if (value == entry.value) | ||||
|       return entry.str; | ||||
|   } | ||||
|   return "";  // Not found | ||||
| } | ||||
|  | ||||
| // Commands | ||||
| static const uint8_t CMD_ENABLE_CONF = 0xFF; | ||||
| static const uint8_t CMD_DISABLE_CONF = 0xFE; | ||||
| static const uint8_t CMD_ENABLE_ENG = 0x62; | ||||
| static const uint8_t CMD_DISABLE_ENG = 0x63; | ||||
| static const uint8_t CMD_MAXDIST_DURATION = 0x60; | ||||
| static const uint8_t CMD_QUERY = 0x61; | ||||
| static const uint8_t CMD_GATE_SENS = 0x64; | ||||
| static const uint8_t CMD_VERSION = 0xA0; | ||||
| static const uint8_t CMD_QUERY_DISTANCE_RESOLUTION = 0xAB; | ||||
| static const uint8_t CMD_SET_DISTANCE_RESOLUTION = 0xAA; | ||||
| static const uint8_t CMD_QUERY_LIGHT_CONTROL = 0xAE; | ||||
| static const uint8_t CMD_SET_LIGHT_CONTROL = 0xAD; | ||||
| static const uint8_t CMD_SET_BAUD_RATE = 0xA1; | ||||
| static const uint8_t CMD_BT_PASSWORD = 0xA9; | ||||
| static const uint8_t CMD_MAC = 0xA5; | ||||
| static const uint8_t CMD_RESET = 0xA2; | ||||
| static const uint8_t CMD_RESTART = 0xA3; | ||||
| static const uint8_t CMD_BLUETOOTH = 0xA4; | ||||
| // Commands values | ||||
| static const uint8_t CMD_MAX_MOVE_VALUE = 0x00; | ||||
| static const uint8_t CMD_MAX_STILL_VALUE = 0x01; | ||||
| static const uint8_t CMD_DURATION_VALUE = 0x02; | ||||
| // Command Header & Footer | ||||
| static const uint8_t CMD_FRAME_HEADER[4] = {0xFD, 0xFC, 0xFB, 0xFA}; | ||||
| static const uint8_t CMD_FRAME_END[4] = {0x04, 0x03, 0x02, 0x01}; | ||||
| // Data Header & Footer | ||||
| static const uint8_t DATA_FRAME_HEADER[4] = {0xF4, 0xF3, 0xF2, 0xF1}; | ||||
| static const uint8_t DATA_FRAME_END[4] = {0xF8, 0xF7, 0xF6, 0xF5}; | ||||
|  | ||||
| static inline int two_byte_to_int(char firstbyte, char secondbyte) { return (int16_t) (secondbyte << 8) + firstbyte; } | ||||
|  | ||||
| void LD2410Component::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "LD2410:"); | ||||
| @@ -73,10 +230,10 @@ void LD2410Component::dump_config() { | ||||
| #endif | ||||
|   this->read_all_info(); | ||||
|   ESP_LOGCONFIG(TAG, | ||||
|                 "  Throttle_ : %ums\n" | ||||
|                 "  MAC Address : %s\n" | ||||
|                 "  Firmware Version : %s", | ||||
|                 this->throttle_, const_cast<char *>(this->mac_.c_str()), const_cast<char *>(this->version_.c_str())); | ||||
|                 "  Throttle: %ums\n" | ||||
|                 "  MAC address: %s\n" | ||||
|                 "  Firmware version: %s", | ||||
|                 this->throttle_, this->mac_ == NO_MAC ? UNKNOWN_MAC : this->mac_.c_str(), this->version_.c_str()); | ||||
| } | ||||
|  | ||||
| void LD2410Component::setup() { | ||||
| @@ -153,7 +310,7 @@ void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) { | ||||
|   /* | ||||
|     Reduce data update rate to prevent home assistant database size grow fast | ||||
|   */ | ||||
|   int32_t current_millis = millis(); | ||||
|   int32_t current_millis = App.get_loop_component_start_time(); | ||||
|   if (current_millis - last_periodic_millis_ < this->throttle_) | ||||
|     return; | ||||
|   last_periodic_millis_ = current_millis; | ||||
| @@ -198,7 +355,7 @@ void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) { | ||||
|   */ | ||||
| #ifdef USE_SENSOR | ||||
|   if (this->moving_target_distance_sensor_ != nullptr) { | ||||
|     int new_moving_target_distance = this->two_byte_to_int_(buffer[MOVING_TARGET_LOW], buffer[MOVING_TARGET_HIGH]); | ||||
|     int new_moving_target_distance = ld2410::two_byte_to_int(buffer[MOVING_TARGET_LOW], buffer[MOVING_TARGET_HIGH]); | ||||
|     if (this->moving_target_distance_sensor_->get_state() != new_moving_target_distance) | ||||
|       this->moving_target_distance_sensor_->publish_state(new_moving_target_distance); | ||||
|   } | ||||
| @@ -208,7 +365,7 @@ void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) { | ||||
|       this->moving_target_energy_sensor_->publish_state(new_moving_target_energy); | ||||
|   } | ||||
|   if (this->still_target_distance_sensor_ != nullptr) { | ||||
|     int new_still_target_distance = this->two_byte_to_int_(buffer[STILL_TARGET_LOW], buffer[STILL_TARGET_HIGH]); | ||||
|     int new_still_target_distance = ld2410::two_byte_to_int(buffer[STILL_TARGET_LOW], buffer[STILL_TARGET_HIGH]); | ||||
|     if (this->still_target_distance_sensor_->get_state() != new_still_target_distance) | ||||
|       this->still_target_distance_sensor_->publish_state(new_still_target_distance); | ||||
|   } | ||||
| @@ -218,7 +375,7 @@ void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) { | ||||
|       this->still_target_energy_sensor_->publish_state(new_still_target_energy); | ||||
|   } | ||||
|   if (this->detection_distance_sensor_ != nullptr) { | ||||
|     int new_detect_distance = this->two_byte_to_int_(buffer[DETECT_DISTANCE_LOW], buffer[DETECT_DISTANCE_HIGH]); | ||||
|     int new_detect_distance = ld2410::two_byte_to_int(buffer[DETECT_DISTANCE_LOW], buffer[DETECT_DISTANCE_HIGH]); | ||||
|     if (this->detection_distance_sensor_->get_state() != new_detect_distance) | ||||
|       this->detection_distance_sensor_->publish_state(new_detect_distance); | ||||
|   } | ||||
| @@ -280,40 +437,6 @@ void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) { | ||||
| #endif | ||||
| } | ||||
|  | ||||
| const char VERSION_FMT[] = "%u.%02X.%02X%02X%02X%02X"; | ||||
|  | ||||
| std::string format_version(uint8_t *buffer) { | ||||
|   std::string::size_type version_size = 256; | ||||
|   std::string version; | ||||
|   do { | ||||
|     version.resize(version_size + 1); | ||||
|     version_size = std::snprintf(&version[0], version.size(), VERSION_FMT, buffer[13], buffer[12], buffer[17], | ||||
|                                  buffer[16], buffer[15], buffer[14]); | ||||
|   } while (version_size + 1 > version.size()); | ||||
|   version.resize(version_size); | ||||
|   return version; | ||||
| } | ||||
|  | ||||
| const char MAC_FMT[] = "%02X:%02X:%02X:%02X:%02X:%02X"; | ||||
|  | ||||
| const std::string UNKNOWN_MAC("unknown"); | ||||
| const std::string NO_MAC("08:05:04:03:02:01"); | ||||
|  | ||||
| std::string format_mac(uint8_t *buffer) { | ||||
|   std::string::size_type mac_size = 256; | ||||
|   std::string mac; | ||||
|   do { | ||||
|     mac.resize(mac_size + 1); | ||||
|     mac_size = std::snprintf(&mac[0], mac.size(), MAC_FMT, buffer[10], buffer[11], buffer[12], buffer[13], buffer[14], | ||||
|                              buffer[15]); | ||||
|   } while (mac_size + 1 > mac.size()); | ||||
|   mac.resize(mac_size); | ||||
|   if (mac == NO_MAC) { | ||||
|     return UNKNOWN_MAC; | ||||
|   } | ||||
|   return mac; | ||||
| } | ||||
|  | ||||
| #ifdef USE_NUMBER | ||||
| std::function<void(void)> set_number_value(number::Number *n, float value) { | ||||
|   float normalized_value = value * 1.0; | ||||
| @@ -328,40 +451,40 @@ std::function<void(void)> set_number_value(number::Number *n, float value) { | ||||
| bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) { | ||||
|   ESP_LOGV(TAG, "Handling ACK DATA for COMMAND %02X", buffer[COMMAND]); | ||||
|   if (len < 10) { | ||||
|     ESP_LOGE(TAG, "Error with last command : incorrect length"); | ||||
|     ESP_LOGE(TAG, "Invalid length"); | ||||
|     return true; | ||||
|   } | ||||
|   if (buffer[0] != 0xFD || buffer[1] != 0xFC || buffer[2] != 0xFB || buffer[3] != 0xFA) {  // check 4 frame start bytes | ||||
|     ESP_LOGE(TAG, "Error with last command : incorrect Header"); | ||||
|     ESP_LOGE(TAG, "Invalid header"); | ||||
|     return true; | ||||
|   } | ||||
|   if (buffer[COMMAND_STATUS] != 0x01) { | ||||
|     ESP_LOGE(TAG, "Error with last command : status != 0x01"); | ||||
|     ESP_LOGE(TAG, "Invalid status"); | ||||
|     return true; | ||||
|   } | ||||
|   if (this->two_byte_to_int_(buffer[8], buffer[9]) != 0x00) { | ||||
|     ESP_LOGE(TAG, "Error with last command , last buffer was: %u , %u", buffer[8], buffer[9]); | ||||
|   if (ld2410::two_byte_to_int(buffer[8], buffer[9]) != 0x00) { | ||||
|     ESP_LOGE(TAG, "Invalid command: %u, %u", buffer[8], buffer[9]); | ||||
|     return true; | ||||
|   } | ||||
|  | ||||
|   switch (buffer[COMMAND]) { | ||||
|     case lowbyte(CMD_ENABLE_CONF): | ||||
|       ESP_LOGV(TAG, "Handled Enable conf command"); | ||||
|       ESP_LOGV(TAG, "Enable conf"); | ||||
|       break; | ||||
|     case lowbyte(CMD_DISABLE_CONF): | ||||
|       ESP_LOGV(TAG, "Handled Disabled conf command"); | ||||
|       ESP_LOGV(TAG, "Disabled conf"); | ||||
|       break; | ||||
|     case lowbyte(CMD_SET_BAUD_RATE): | ||||
|       ESP_LOGV(TAG, "Handled baud rate change command"); | ||||
|       ESP_LOGV(TAG, "Baud rate change"); | ||||
| #ifdef USE_SELECT | ||||
|       if (this->baud_rate_select_ != nullptr) { | ||||
|         ESP_LOGE(TAG, "Change baud rate component config to %s and reinstall", this->baud_rate_select_->state.c_str()); | ||||
|         ESP_LOGE(TAG, "Configure baud rate to %s and reinstall", this->baud_rate_select_->state.c_str()); | ||||
|       } | ||||
| #endif | ||||
|       break; | ||||
|     case lowbyte(CMD_VERSION): | ||||
|       this->version_ = format_version(buffer); | ||||
|       ESP_LOGV(TAG, "FW Version is: %s", const_cast<char *>(this->version_.c_str())); | ||||
|       this->version_ = str_sprintf(VERSION_FMT, buffer[13], buffer[12], buffer[17], buffer[16], buffer[15], buffer[14]); | ||||
|       ESP_LOGV(TAG, "Firmware version: %s", this->version_.c_str()); | ||||
| #ifdef USE_TEXT_SENSOR | ||||
|       if (this->version_text_sensor_ != nullptr) { | ||||
|         this->version_text_sensor_->publish_state(this->version_); | ||||
| @@ -370,8 +493,8 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) { | ||||
|       break; | ||||
|     case lowbyte(CMD_QUERY_DISTANCE_RESOLUTION): { | ||||
|       std::string distance_resolution = | ||||
|           DISTANCE_RESOLUTION_INT_TO_ENUM.at(this->two_byte_to_int_(buffer[10], buffer[11])); | ||||
|       ESP_LOGV(TAG, "Distance resolution is: %s", const_cast<char *>(distance_resolution.c_str())); | ||||
|           find_str(DISTANCE_RESOLUTIONS_BY_UINT, ld2410::two_byte_to_int(buffer[10], buffer[11])); | ||||
|       ESP_LOGV(TAG, "Distance resolution: %s", distance_resolution.c_str()); | ||||
| #ifdef USE_SELECT | ||||
|       if (this->distance_resolution_select_ != nullptr && | ||||
|           this->distance_resolution_select_->state != distance_resolution) { | ||||
| @@ -380,12 +503,12 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) { | ||||
| #endif | ||||
|     } break; | ||||
|     case lowbyte(CMD_QUERY_LIGHT_CONTROL): { | ||||
|       this->light_function_ = LIGHT_FUNCTION_INT_TO_ENUM.at(buffer[10]); | ||||
|       this->light_function_ = find_str(LIGHT_FUNCTIONS_BY_UINT, buffer[10]); | ||||
|       this->light_threshold_ = buffer[11] * 1.0; | ||||
|       this->out_pin_level_ = OUT_PIN_LEVEL_INT_TO_ENUM.at(buffer[12]); | ||||
|       ESP_LOGV(TAG, "Light function is: %s", const_cast<char *>(this->light_function_.c_str())); | ||||
|       ESP_LOGV(TAG, "Light threshold is: %f", this->light_threshold_); | ||||
|       ESP_LOGV(TAG, "Out pin level is: %s", const_cast<char *>(this->out_pin_level_.c_str())); | ||||
|       this->out_pin_level_ = find_str(OUT_PIN_LEVELS_BY_UINT, buffer[12]); | ||||
|       ESP_LOGV(TAG, "Light function: %s", const_cast<char *>(this->light_function_.c_str())); | ||||
|       ESP_LOGV(TAG, "Light threshold: %f", this->light_threshold_); | ||||
|       ESP_LOGV(TAG, "Out pin level: %s", const_cast<char *>(this->out_pin_level_.c_str())); | ||||
| #ifdef USE_SELECT | ||||
|       if (this->light_function_select_ != nullptr && this->light_function_select_->state != this->light_function_) { | ||||
|         this->light_function_select_->publish_state(this->light_function_); | ||||
| @@ -406,33 +529,33 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) { | ||||
|       if (len < 20) { | ||||
|         return false; | ||||
|       } | ||||
|       this->mac_ = format_mac(buffer); | ||||
|       ESP_LOGV(TAG, "MAC Address is: %s", const_cast<char *>(this->mac_.c_str())); | ||||
|       this->mac_ = format_mac_address_pretty(&buffer[10]); | ||||
|       ESP_LOGV(TAG, "MAC address: %s", this->mac_.c_str()); | ||||
| #ifdef USE_TEXT_SENSOR | ||||
|       if (this->mac_text_sensor_ != nullptr) { | ||||
|         this->mac_text_sensor_->publish_state(this->mac_); | ||||
|         this->mac_text_sensor_->publish_state(this->mac_ == NO_MAC ? UNKNOWN_MAC : this->mac_); | ||||
|       } | ||||
| #endif | ||||
| #ifdef USE_SWITCH | ||||
|       if (this->bluetooth_switch_ != nullptr) { | ||||
|         this->bluetooth_switch_->publish_state(this->mac_ != UNKNOWN_MAC); | ||||
|         this->bluetooth_switch_->publish_state(this->mac_ != NO_MAC); | ||||
|       } | ||||
| #endif | ||||
|       break; | ||||
|     case lowbyte(CMD_GATE_SENS): | ||||
|       ESP_LOGV(TAG, "Handled sensitivity command"); | ||||
|       ESP_LOGV(TAG, "Sensitivity"); | ||||
|       break; | ||||
|     case lowbyte(CMD_BLUETOOTH): | ||||
|       ESP_LOGV(TAG, "Handled bluetooth command"); | ||||
|       ESP_LOGV(TAG, "Bluetooth"); | ||||
|       break; | ||||
|     case lowbyte(CMD_SET_DISTANCE_RESOLUTION): | ||||
|       ESP_LOGV(TAG, "Handled set distance resolution command"); | ||||
|       ESP_LOGV(TAG, "Set distance resolution"); | ||||
|       break; | ||||
|     case lowbyte(CMD_SET_LIGHT_CONTROL): | ||||
|       ESP_LOGV(TAG, "Handled set light control command"); | ||||
|       ESP_LOGV(TAG, "Set light control"); | ||||
|       break; | ||||
|     case lowbyte(CMD_BT_PASSWORD): | ||||
|       ESP_LOGV(TAG, "Handled set bluetooth password command"); | ||||
|       ESP_LOGV(TAG, "Set bluetooth password"); | ||||
|       break; | ||||
|     case lowbyte(CMD_QUERY):  // Query parameters response | ||||
|     { | ||||
| @@ -461,7 +584,7 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) { | ||||
|       /* | ||||
|         None Duration: 33~34th bytes | ||||
|       */ | ||||
|       updates.push_back(set_number_value(this->timeout_number_, this->two_byte_to_int_(buffer[32], buffer[33]))); | ||||
|       updates.push_back(set_number_value(this->timeout_number_, ld2410::two_byte_to_int(buffer[32], buffer[33]))); | ||||
|       for (auto &update : updates) { | ||||
|         update(); | ||||
|       } | ||||
| @@ -518,21 +641,21 @@ void LD2410Component::set_bluetooth(bool enable) { | ||||
|  | ||||
| void LD2410Component::set_distance_resolution(const std::string &state) { | ||||
|   this->set_config_mode_(true); | ||||
|   uint8_t cmd_value[2] = {DISTANCE_RESOLUTION_ENUM_TO_INT.at(state), 0x00}; | ||||
|   uint8_t cmd_value[2] = {find_uint8(DISTANCE_RESOLUTIONS_BY_STR, state), 0x00}; | ||||
|   this->send_command_(CMD_SET_DISTANCE_RESOLUTION, cmd_value, 2); | ||||
|   this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); | ||||
| } | ||||
|  | ||||
| void LD2410Component::set_baud_rate(const std::string &state) { | ||||
|   this->set_config_mode_(true); | ||||
|   uint8_t cmd_value[2] = {BAUD_RATE_ENUM_TO_INT.at(state), 0x00}; | ||||
|   uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00}; | ||||
|   this->send_command_(CMD_SET_BAUD_RATE, cmd_value, 2); | ||||
|   this->set_timeout(200, [this]() { this->restart_(); }); | ||||
| } | ||||
|  | ||||
| void LD2410Component::set_bluetooth_password(const std::string &password) { | ||||
|   if (password.length() != 6) { | ||||
|     ESP_LOGE(TAG, "set_bluetooth_password(): invalid password length, must be exactly 6 chars '%s'", password.c_str()); | ||||
|     ESP_LOGE(TAG, "Password must be exactly 6 chars"); | ||||
|     return; | ||||
|   } | ||||
|   this->set_config_mode_(true); | ||||
| @@ -544,7 +667,7 @@ void LD2410Component::set_bluetooth_password(const std::string &password) { | ||||
|  | ||||
| void LD2410Component::set_engineering_mode(bool enable) { | ||||
|   this->set_config_mode_(true); | ||||
|   last_engineering_mode_change_millis_ = millis(); | ||||
|   last_engineering_mode_change_millis_ = App.get_loop_component_start_time(); | ||||
|   uint8_t cmd = enable ? CMD_ENABLE_ENG : CMD_DISABLE_ENG; | ||||
|   this->send_command_(cmd, nullptr, 0); | ||||
|   this->set_config_mode_(false); | ||||
| @@ -659,9 +782,9 @@ void LD2410Component::set_light_out_control() { | ||||
|     return; | ||||
|   } | ||||
|   this->set_config_mode_(true); | ||||
|   uint8_t light_function = LIGHT_FUNCTION_ENUM_TO_INT.at(this->light_function_); | ||||
|   uint8_t light_function = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_); | ||||
|   uint8_t light_threshold = static_cast<uint8_t>(this->light_threshold_); | ||||
|   uint8_t out_pin_level = OUT_PIN_LEVEL_ENUM_TO_INT.at(this->out_pin_level_); | ||||
|   uint8_t out_pin_level = find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_); | ||||
|   uint8_t value[4] = {light_function, light_threshold, out_pin_level, 0x00}; | ||||
|   this->send_command_(CMD_SET_LIGHT_CONTROL, value, 4); | ||||
|   delay(50);  // NOLINT | ||||
|   | ||||
| @@ -26,114 +26,9 @@ | ||||
| #include "esphome/core/automation.h" | ||||
| #include "esphome/core/helpers.h" | ||||
|  | ||||
| #include <map> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace ld2410 { | ||||
|  | ||||
| #define CHECK_BIT(var, pos) (((var) >> (pos)) & 1) | ||||
|  | ||||
| // Commands | ||||
| static const uint8_t CMD_ENABLE_CONF = 0x00FF; | ||||
| static const uint8_t CMD_DISABLE_CONF = 0x00FE; | ||||
| static const uint8_t CMD_ENABLE_ENG = 0x0062; | ||||
| static const uint8_t CMD_DISABLE_ENG = 0x0063; | ||||
| static const uint8_t CMD_MAXDIST_DURATION = 0x0060; | ||||
| static const uint8_t CMD_QUERY = 0x0061; | ||||
| static const uint8_t CMD_GATE_SENS = 0x0064; | ||||
| static const uint8_t CMD_VERSION = 0x00A0; | ||||
| static const uint8_t CMD_QUERY_DISTANCE_RESOLUTION = 0x00AB; | ||||
| static const uint8_t CMD_SET_DISTANCE_RESOLUTION = 0x00AA; | ||||
| static const uint8_t CMD_QUERY_LIGHT_CONTROL = 0x00AE; | ||||
| static const uint8_t CMD_SET_LIGHT_CONTROL = 0x00AD; | ||||
| static const uint8_t CMD_SET_BAUD_RATE = 0x00A1; | ||||
| static const uint8_t CMD_BT_PASSWORD = 0x00A9; | ||||
| static const uint8_t CMD_MAC = 0x00A5; | ||||
| static const uint8_t CMD_RESET = 0x00A2; | ||||
| static const uint8_t CMD_RESTART = 0x00A3; | ||||
| static const uint8_t CMD_BLUETOOTH = 0x00A4; | ||||
|  | ||||
| enum BaudRateStructure : uint8_t { | ||||
|   BAUD_RATE_9600 = 1, | ||||
|   BAUD_RATE_19200 = 2, | ||||
|   BAUD_RATE_38400 = 3, | ||||
|   BAUD_RATE_57600 = 4, | ||||
|   BAUD_RATE_115200 = 5, | ||||
|   BAUD_RATE_230400 = 6, | ||||
|   BAUD_RATE_256000 = 7, | ||||
|   BAUD_RATE_460800 = 8 | ||||
| }; | ||||
|  | ||||
| static const std::map<std::string, uint8_t> BAUD_RATE_ENUM_TO_INT{ | ||||
|     {"9600", BAUD_RATE_9600},     {"19200", BAUD_RATE_19200},   {"38400", BAUD_RATE_38400}, | ||||
|     {"57600", BAUD_RATE_57600},   {"115200", BAUD_RATE_115200}, {"230400", BAUD_RATE_230400}, | ||||
|     {"256000", BAUD_RATE_256000}, {"460800", BAUD_RATE_460800}}; | ||||
|  | ||||
| enum DistanceResolutionStructure : uint8_t { DISTANCE_RESOLUTION_0_2 = 0x01, DISTANCE_RESOLUTION_0_75 = 0x00 }; | ||||
|  | ||||
| static const std::map<std::string, uint8_t> DISTANCE_RESOLUTION_ENUM_TO_INT{{"0.2m", DISTANCE_RESOLUTION_0_2}, | ||||
|                                                                             {"0.75m", DISTANCE_RESOLUTION_0_75}}; | ||||
| static const std::map<uint8_t, std::string> DISTANCE_RESOLUTION_INT_TO_ENUM{{DISTANCE_RESOLUTION_0_2, "0.2m"}, | ||||
|                                                                             {DISTANCE_RESOLUTION_0_75, "0.75m"}}; | ||||
|  | ||||
| enum LightFunctionStructure : uint8_t { | ||||
|   LIGHT_FUNCTION_OFF = 0x00, | ||||
|   LIGHT_FUNCTION_BELOW = 0x01, | ||||
|   LIGHT_FUNCTION_ABOVE = 0x02 | ||||
| }; | ||||
|  | ||||
| static const std::map<std::string, uint8_t> LIGHT_FUNCTION_ENUM_TO_INT{ | ||||
|     {"off", LIGHT_FUNCTION_OFF}, {"below", LIGHT_FUNCTION_BELOW}, {"above", LIGHT_FUNCTION_ABOVE}}; | ||||
| static const std::map<uint8_t, std::string> LIGHT_FUNCTION_INT_TO_ENUM{ | ||||
|     {LIGHT_FUNCTION_OFF, "off"}, {LIGHT_FUNCTION_BELOW, "below"}, {LIGHT_FUNCTION_ABOVE, "above"}}; | ||||
|  | ||||
| enum OutPinLevelStructure : uint8_t { OUT_PIN_LEVEL_LOW = 0x00, OUT_PIN_LEVEL_HIGH = 0x01 }; | ||||
|  | ||||
| static const std::map<std::string, uint8_t> OUT_PIN_LEVEL_ENUM_TO_INT{{"low", OUT_PIN_LEVEL_LOW}, | ||||
|                                                                       {"high", OUT_PIN_LEVEL_HIGH}}; | ||||
| static const std::map<uint8_t, std::string> OUT_PIN_LEVEL_INT_TO_ENUM{{OUT_PIN_LEVEL_LOW, "low"}, | ||||
|                                                                       {OUT_PIN_LEVEL_HIGH, "high"}}; | ||||
|  | ||||
| // Commands values | ||||
| static const uint8_t CMD_MAX_MOVE_VALUE = 0x0000; | ||||
| static const uint8_t CMD_MAX_STILL_VALUE = 0x0001; | ||||
| static const uint8_t CMD_DURATION_VALUE = 0x0002; | ||||
| // Command Header & Footer | ||||
| static const uint8_t CMD_FRAME_HEADER[4] = {0xFD, 0xFC, 0xFB, 0xFA}; | ||||
| static const uint8_t CMD_FRAME_END[4] = {0x04, 0x03, 0x02, 0x01}; | ||||
| // Data Header & Footer | ||||
| static const uint8_t DATA_FRAME_HEADER[4] = {0xF4, 0xF3, 0xF2, 0xF1}; | ||||
| static const uint8_t DATA_FRAME_END[4] = {0xF8, 0xF7, 0xF6, 0xF5}; | ||||
| /* | ||||
| Data Type: 6th byte | ||||
| Target states: 9th byte | ||||
|     Moving target distance: 10~11th bytes | ||||
|     Moving target energy: 12th byte | ||||
|     Still target distance: 13~14th bytes | ||||
|     Still target energy: 15th byte | ||||
|     Detect distance: 16~17th bytes | ||||
| */ | ||||
| enum PeriodicDataStructure : uint8_t { | ||||
|   DATA_TYPES = 6, | ||||
|   TARGET_STATES = 8, | ||||
|   MOVING_TARGET_LOW = 9, | ||||
|   MOVING_TARGET_HIGH = 10, | ||||
|   MOVING_ENERGY = 11, | ||||
|   STILL_TARGET_LOW = 12, | ||||
|   STILL_TARGET_HIGH = 13, | ||||
|   STILL_ENERGY = 14, | ||||
|   DETECT_DISTANCE_LOW = 15, | ||||
|   DETECT_DISTANCE_HIGH = 16, | ||||
|   MOVING_SENSOR_START = 19, | ||||
|   STILL_SENSOR_START = 28, | ||||
|   LIGHT_SENSOR = 37, | ||||
|   OUT_PIN_SENSOR = 38, | ||||
| }; | ||||
| enum PeriodicDataValue : uint8_t { HEAD = 0xAA, END = 0x55, CHECK = 0x00 }; | ||||
|  | ||||
| enum AckDataStructure : uint8_t { COMMAND = 6, COMMAND_STATUS = 7 }; | ||||
|  | ||||
| //  char cmd[2] = {enable ? 0xFF : 0xFE, 0x00}; | ||||
| class LD2410Component : public Component, public uart::UARTDevice { | ||||
| #ifdef USE_SENSOR | ||||
|   SUB_SENSOR(moving_target_distance) | ||||
| @@ -176,7 +71,6 @@ class LD2410Component : public Component, public uart::UARTDevice { | ||||
| #endif | ||||
|  | ||||
|  public: | ||||
|   LD2410Component(); | ||||
|   void setup() override; | ||||
|   void dump_config() override; | ||||
|   void loop() override; | ||||
| @@ -202,7 +96,6 @@ class LD2410Component : public Component, public uart::UARTDevice { | ||||
|   void factory_reset(); | ||||
|  | ||||
|  protected: | ||||
|   int two_byte_to_int_(char firstbyte, char secondbyte) { return (int16_t) (secondbyte << 8) + firstbyte; } | ||||
|   void send_command_(uint8_t command_str, const uint8_t *command_value, int command_value_len); | ||||
|   void set_config_mode_(bool enable); | ||||
|   void handle_periodic_data_(uint8_t *buffer, int len); | ||||
| @@ -215,14 +108,14 @@ class LD2410Component : public Component, public uart::UARTDevice { | ||||
|   void get_light_control_(); | ||||
|   void restart_(); | ||||
|  | ||||
|   int32_t last_periodic_millis_ = millis(); | ||||
|   int32_t last_engineering_mode_change_millis_ = millis(); | ||||
|   int32_t last_periodic_millis_ = 0; | ||||
|   int32_t last_engineering_mode_change_millis_ = 0; | ||||
|   uint16_t throttle_; | ||||
|   float light_threshold_ = -1; | ||||
|   std::string version_; | ||||
|   std::string mac_; | ||||
|   std::string out_pin_level_; | ||||
|   std::string light_function_; | ||||
|   float light_threshold_ = -1; | ||||
| #ifdef USE_NUMBER | ||||
|   std::vector<number::Number *> gate_still_threshold_numbers_ = std::vector<number::Number *>(9); | ||||
|   std::vector<number::Number *> gate_move_threshold_numbers_ = std::vector<number::Number *>(9); | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| #include "ld2420.h" | ||||
| #include "esphome/core/application.h" | ||||
| #include "esphome/core/helpers.h" | ||||
|  | ||||
| /* | ||||
| @@ -40,7 +41,7 @@ There are three documented parameters for modes: | ||||
|   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, 16 Gate Energies EE, Footer FF | ||||
|     Header HH, Length LL, Presence PP, Distance DD, 16 Gate Energies EE, Footer FF | ||||
|     HH HH HH HH LL LL PP DD DD EE EE .. 16x   .. FF FF FF FF | ||||
|     F4 F3 F2 F1 23 00 00 00 00 00 00 .. .. .. .. F8 F7 F6 F5 | ||||
|   00 00 = debug output mode | ||||
| @@ -67,10 +68,10 @@ float LD2420Component::get_setup_priority() const { return setup_priority::BUS; | ||||
| void LD2420Component::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, | ||||
|                 "LD2420:\n" | ||||
|                 "  Firmware Version : %7s\n" | ||||
|                 "LD2420 Number:", | ||||
|                 "  Firmware version: %7s", | ||||
|                 this->ld2420_firmware_ver_); | ||||
| #ifdef USE_NUMBER | ||||
|   ESP_LOGCONFIG(TAG, "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_); | ||||
| @@ -86,10 +87,10 @@ void LD2420Component::dump_config() { | ||||
|   LOG_BUTTON(TAG, "  Factory Reset:", this->factory_reset_button_); | ||||
|   LOG_BUTTON(TAG, "  Restart Module:", this->restart_module_button_); | ||||
| #endif | ||||
|   ESP_LOGCONFIG(TAG, "LD2420 Select:"); | ||||
|   ESP_LOGCONFIG(TAG, "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_); | ||||
|   if (LD2420Component::get_firmware_int(this->ld2420_firmware_ver_) < CALIBRATE_VERSION_MIN) { | ||||
|     ESP_LOGW(TAG, "Firmware version %s and older supports Simple Mode only", this->ld2420_firmware_ver_); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -102,7 +103,7 @@ uint8_t LD2420Component::calc_checksum(void *data, size_t size) { | ||||
|   return checksum; | ||||
| } | ||||
|  | ||||
| int LD2420Component::get_firmware_int_(const char *version_string) { | ||||
| 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); | ||||
| @@ -115,7 +116,7 @@ int LD2420Component::get_firmware_int_(const char *version_string) { | ||||
| void LD2420Component::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
|   if (this->set_config_mode(true) == LD2420_ERROR_TIMEOUT) { | ||||
|     ESP_LOGE(TAG, "LD2420 module has failed to respond, check baud rate and serial connections."); | ||||
|     ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
| @@ -127,7 +128,7 @@ void LD2420Component::setup() { | ||||
|   const char *pfw = this->ld2420_firmware_ver_; | ||||
|   std::string fw_str(pfw); | ||||
|  | ||||
|   for (auto &listener : listeners_) { | ||||
|   for (auto &listener : this->listeners_) { | ||||
|     listener->on_fw_version(fw_str); | ||||
|   } | ||||
|  | ||||
| @@ -137,11 +138,11 @@ void LD2420Component::setup() { | ||||
|   } | ||||
|  | ||||
|   memcpy(&this->new_config, &this->current_config, sizeof(this->current_config)); | ||||
|   if (get_firmware_int_(ld2420_firmware_ver_) < CALIBRATE_VERSION_MIN) { | ||||
|   if (LD2420Component::get_firmware_int(this->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_); | ||||
|     ESP_LOGW(TAG, "Firmware version %s and older supports Simple Mode only", this->ld2420_firmware_ver_); | ||||
|   } else { | ||||
|     this->set_mode_(CMD_SYSTEM_MODE_ENERGY); | ||||
|     this->operating_selector_->publish_state(OP_NORMAL_MODE_STRING); | ||||
| @@ -151,18 +152,17 @@ void LD2420Component::setup() { | ||||
| #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"); | ||||
|     ESP_LOGD(TAG, "No configuration change detected"); | ||||
|     return; | ||||
|   } | ||||
|   ESP_LOGCONFIG(TAG, "Reconfiguring LD2420"); | ||||
|   ESP_LOGD(TAG, "Reconfiguring"); | ||||
|   if (this->set_config_mode(true) == LD2420_ERROR_TIMEOUT) { | ||||
|     ESP_LOGE(TAG, "LD2420 module has failed to respond, check baud rate and serial connections."); | ||||
|     ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
| @@ -178,13 +178,12 @@ void LD2420Component::apply_config_action() { | ||||
|   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, "Setting factory defaults"); | ||||
|   ESP_LOGD(TAG, "Setting 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."); | ||||
|     ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
| @@ -207,18 +206,16 @@ void LD2420Component::factory_reset_action() { | ||||
|   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"); | ||||
|   ESP_LOGD(TAG, "Restarting"); | ||||
|   this->send_module_restart(); | ||||
|   this->set_timeout(250, [this]() { | ||||
|     this->set_config_mode(true); | ||||
|     this->set_system_mode(system_mode_); | ||||
|     this->set_system_mode(this->system_mode_); | ||||
|     this->set_config_mode(false); | ||||
|   }); | ||||
|   ESP_LOGCONFIG(TAG, "LD2420 Restarted."); | ||||
| } | ||||
|  | ||||
| void LD2420Component::revert_config_action() { | ||||
| @@ -226,18 +223,18 @@ void LD2420Component::revert_config_action() { | ||||
| #ifdef USE_NUMBER | ||||
|   this->init_gate_config_numbers(); | ||||
| #endif | ||||
|   ESP_LOGCONFIG(TAG, "Reverted config number edits."); | ||||
|   ESP_LOGD(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()) | ||||
|   if (!this->get_cmd_active_()) { | ||||
|     if (!this->available()) | ||||
|       return; | ||||
|     static uint8_t buffer[2048]; | ||||
|     static uint8_t rx_data; | ||||
|     while (available()) { | ||||
|       rx_data = read(); | ||||
|     while (this->available()) { | ||||
|       rx_data = this->read(); | ||||
|       this->readline_(rx_data, buffer, sizeof(buffer)); | ||||
|     } | ||||
|   } | ||||
| @@ -292,7 +289,7 @@ void LD2420Component::report_gate_data() { | ||||
|  | ||||
| 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) { | ||||
|   if (LD2420Component::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); | ||||
| @@ -365,13 +362,13 @@ void LD2420Component::handle_energy_mode_(uint8_t *buffer, int len) { | ||||
|   } | ||||
|  | ||||
|   // Resonable refresh rate for home assistant database size health | ||||
|   const int32_t current_millis = millis(); | ||||
|   const int32_t current_millis = App.get_loop_component_start_time(); | ||||
|   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_distance(this->get_distance_()); | ||||
|     listener->on_presence(this->get_presence_()); | ||||
|     listener->on_energy(this->gate_energy_, sizeof(this->gate_energy_) / sizeof(this->gate_energy_[0])); | ||||
|   } | ||||
|  | ||||
| @@ -392,9 +389,9 @@ void LD2420Component::handle_simple_mode_(const uint8_t *inbuf, int len) { | ||||
|   char outbuf[bufsize]{0}; | ||||
|   while (true) { | ||||
|     if (inbuf[pos - 2] == 'O' && inbuf[pos - 1] == 'F' && inbuf[pos] == 'F') { | ||||
|       set_presence_(false); | ||||
|       this->set_presence_(false); | ||||
|     } else if (inbuf[pos - 1] == 'O' && inbuf[pos] == 'N') { | ||||
|       set_presence_(true); | ||||
|       this->set_presence_(true); | ||||
|     } | ||||
|     if (inbuf[pos] >= '0' && inbuf[pos] <= '9') { | ||||
|       if (index < bufsize - 1) { | ||||
| @@ -411,18 +408,18 @@ void LD2420Component::handle_simple_mode_(const uint8_t *inbuf, int len) { | ||||
|   } | ||||
|   outbuf[index] = '\0'; | ||||
|   if (index > 1) | ||||
|     set_distance_(strtol(outbuf, &endptr, 10)); | ||||
|     this->set_distance_(strtol(outbuf, &endptr, 10)); | ||||
|  | ||||
|   if (get_mode_() == CMD_SYSTEM_MODE_SIMPLE) { | ||||
|   if (this->get_mode_() == CMD_SYSTEM_MODE_SIMPLE) { | ||||
|     // Resonable refresh rate for home assistant database size health | ||||
|     const int32_t current_millis = millis(); | ||||
|     const int32_t current_millis = App.get_loop_component_start_time(); | ||||
|     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_()); | ||||
|       listener->on_distance(this->get_distance_()); | ||||
|     for (auto &listener : this->listeners_) | ||||
|       listener->on_presence(get_presence_()); | ||||
|       listener->on_presence(this->get_presence_()); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -433,10 +430,10 @@ void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) { | ||||
|   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); | ||||
|     ESP_LOGW(TAG, "Reply frame too long"); | ||||
|     return; | ||||
|   } else if (this->cmd_reply_.length < 2) { | ||||
|     ESP_LOGW(TAG, "LD2420 reply - received command frame is corrupt, length is less than 2 bytes."); | ||||
|     ESP_LOGW(TAG, "Command frame too short"); | ||||
|     return; | ||||
|   } | ||||
|   memcpy(&this->cmd_reply_.error, &buffer[CMD_ERROR_WORD], sizeof(this->cmd_reply_.error)); | ||||
| @@ -447,13 +444,13 @@ void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) { | ||||
|   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); | ||||
|       ESP_LOGV(TAG, "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); | ||||
|       ESP_LOGV(TAG, "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); | ||||
|       ESP_LOGV(TAG, "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 | ||||
| @@ -465,13 +462,13 @@ void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) { | ||||
|       } | ||||
|       break; | ||||
|     case (CMD_WRITE_REGISTER): | ||||
|       ESP_LOGD(TAG, "LD2420 reply - write register: CMD = %2X %s", CMD_WRITE_REGISTER, result); | ||||
|       ESP_LOGV(TAG, "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); | ||||
|       ESP_LOGV(TAG, "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); | ||||
|       ESP_LOGV(TAG, "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)); | ||||
| @@ -483,11 +480,11 @@ void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) { | ||||
|       } | ||||
|       break; | ||||
|     case (CMD_WRITE_SYS_PARAM): | ||||
|       ESP_LOGD(TAG, "LD2420 reply - set system parameter(s): %2X %s", CMD_WRITE_SYS_PARAM, result); | ||||
|       ESP_LOGV(TAG, "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); | ||||
|       ESP_LOGV(TAG, "Firmware version: %7s %s", this->ld2420_firmware_ver_, result); | ||||
|       break; | ||||
|     default: | ||||
|       break; | ||||
| @@ -533,7 +530,7 @@ int LD2420Component::send_cmd_from_array(CmdFrameT frame) { | ||||
|     } | ||||
|  | ||||
|     while (!this->cmd_reply_.ack) { | ||||
|       while (available()) { | ||||
|       while (this->available()) { | ||||
|         this->readline_(read(), ack_buffer, sizeof(ack_buffer)); | ||||
|       } | ||||
|       delay_microseconds_safe(1450); | ||||
| @@ -548,7 +545,7 @@ int LD2420Component::send_cmd_from_array(CmdFrameT frame) { | ||||
|     if (this->cmd_reply_.ack) | ||||
|       retry = 0; | ||||
|     if (this->cmd_reply_.error > 0) | ||||
|       handle_cmd_error(error); | ||||
|       this->handle_cmd_error(error); | ||||
|   } | ||||
|   return error; | ||||
| } | ||||
| @@ -563,7 +560,7 @@ uint8_t LD2420Component::set_config_mode(bool enable) { | ||||
|     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); | ||||
|   ESP_LOGV(TAG, "Sending set config %s command: %2X", enable ? "enable" : "disable", cmd_frame.command); | ||||
|   return this->send_cmd_from_array(cmd_frame); | ||||
| } | ||||
|  | ||||
| @@ -576,7 +573,7 @@ void LD2420Component::ld2420_restart() { | ||||
|   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); | ||||
|   ESP_LOGV(TAG, "Sending restart command: %2X", cmd_frame.command); | ||||
|   this->send_cmd_from_array(cmd_frame); | ||||
| } | ||||
|  | ||||
| @@ -588,7 +585,7 @@ void LD2420Component::get_reg_value_(uint16_t reg) { | ||||
|   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); | ||||
|   ESP_LOGV(TAG, "Sending read register %4X command: %2X", reg, cmd_frame.command); | ||||
|   this->send_cmd_from_array(cmd_frame); | ||||
| } | ||||
|  | ||||
| @@ -602,11 +599,11 @@ void LD2420Component::set_reg_value(uint16_t reg, uint16_t value) { | ||||
|   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); | ||||
|   ESP_LOGV(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]); } | ||||
| void LD2420Component::handle_cmd_error(uint8_t error) { ESP_LOGE(TAG, "Command failed: %s", ERR_MESSAGE[error]); } | ||||
|  | ||||
| int LD2420Component::get_gate_threshold_(uint8_t gate) { | ||||
|   uint8_t error; | ||||
| @@ -619,7 +616,7 @@ int LD2420Component::get_gate_threshold_(uint8_t gate) { | ||||
|   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); | ||||
|   ESP_LOGV(TAG, "Sending read gate %d high/low threshold 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]; | ||||
| @@ -644,7 +641,7 @@ int LD2420Component::get_min_max_distances_timeout_() { | ||||
|          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); | ||||
|   ESP_LOGV(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]; | ||||
| @@ -667,9 +664,9 @@ void LD2420Component::set_system_mode(uint16_t 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); | ||||
|   ESP_LOGV(TAG, "Sending write system mode command: %2X", cmd_frame.command); | ||||
|   if (this->send_cmd_from_array(cmd_frame) == 0) | ||||
|     set_mode_(mode); | ||||
|     this->set_mode_(mode); | ||||
| } | ||||
|  | ||||
| void LD2420Component::get_firmware_version_() { | ||||
| @@ -679,7 +676,7 @@ void LD2420Component::get_firmware_version_() { | ||||
|   cmd_frame.command = CMD_READ_VERSION; | ||||
|   cmd_frame.footer = CMD_FRAME_FOOTER; | ||||
|  | ||||
|   ESP_LOGD(TAG, "Sending read firmware version command: %2X", cmd_frame.command); | ||||
|   ESP_LOGV(TAG, "Sending read firmware version command: %2X", cmd_frame.command); | ||||
|   this->send_cmd_from_array(cmd_frame); | ||||
| } | ||||
|  | ||||
| @@ -712,7 +709,7 @@ void LD2420Component::set_min_max_distances_timeout(uint32_t max_gate_distance, | ||||
|   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); | ||||
|   ESP_LOGV(TAG, "Sending write gate min max and timeout command: %2X", cmd_frame.command); | ||||
|   this->send_cmd_from_array(cmd_frame); | ||||
| } | ||||
|  | ||||
| @@ -738,7 +735,7 @@ void LD2420Component::set_gate_threshold(uint8_t 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); | ||||
|   ESP_LOGV(TAG, "Sending set gate %4X sensitivity command: %2X", gate, cmd_frame.command); | ||||
|   this->send_cmd_from_array(cmd_frame); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -179,7 +179,7 @@ class LD2420Component : public Component, public uart::UARTDevice { | ||||
|   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); | ||||
|   static uint8_t calc_checksum(void *data, size_t size); | ||||
|  | ||||
|   RegConfigT current_config; | ||||
|   RegConfigT new_config; | ||||
| @@ -222,7 +222,7 @@ class LD2420Component : public Component, public uart::UARTDevice { | ||||
|     volatile bool ack; | ||||
|   }; | ||||
|  | ||||
|   int get_firmware_int_(const char *version_string); | ||||
|   static 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); | ||||
|   | ||||
| @@ -6,7 +6,9 @@ | ||||
| #ifdef USE_SENSOR | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
| #endif | ||||
| #include "esphome/core/application.h" | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/helpers.h" | ||||
|  | ||||
| #define highbyte(val) (uint8_t)((val) >> 8) | ||||
| #define lowbyte(val) (uint8_t)((val) &0xff) | ||||
| @@ -15,23 +17,109 @@ namespace esphome { | ||||
| namespace ld2450 { | ||||
|  | ||||
| static const char *const TAG = "ld2450"; | ||||
| static const char *const NO_MAC("08:05:04:03:02:01"); | ||||
| static const char *const UNKNOWN_MAC("unknown"); | ||||
| static const char *const NO_MAC = "08:05:04:03:02:01"; | ||||
| static const char *const UNKNOWN_MAC = "unknown"; | ||||
| static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X"; | ||||
|  | ||||
| enum BaudRateStructure : uint8_t { | ||||
|   BAUD_RATE_9600 = 1, | ||||
|   BAUD_RATE_19200 = 2, | ||||
|   BAUD_RATE_38400 = 3, | ||||
|   BAUD_RATE_57600 = 4, | ||||
|   BAUD_RATE_115200 = 5, | ||||
|   BAUD_RATE_230400 = 6, | ||||
|   BAUD_RATE_256000 = 7, | ||||
|   BAUD_RATE_460800 = 8 | ||||
| }; | ||||
|  | ||||
| // Zone type struct | ||||
| enum ZoneTypeStructure : uint8_t { | ||||
|   ZONE_DISABLED = 0, | ||||
|   ZONE_DETECTION = 1, | ||||
|   ZONE_FILTER = 2, | ||||
| }; | ||||
|  | ||||
| enum PeriodicDataStructure : uint8_t { | ||||
|   TARGET_X = 4, | ||||
|   TARGET_Y = 6, | ||||
|   TARGET_SPEED = 8, | ||||
|   TARGET_RESOLUTION = 10, | ||||
| }; | ||||
|  | ||||
| enum PeriodicDataValue : uint8_t { | ||||
|   HEAD = 0xAA, | ||||
|   END = 0x55, | ||||
|   CHECK = 0x00, | ||||
| }; | ||||
|  | ||||
| enum AckDataStructure : uint8_t { | ||||
|   COMMAND = 6, | ||||
|   COMMAND_STATUS = 7, | ||||
| }; | ||||
|  | ||||
| // Memory-efficient lookup tables | ||||
| struct StringToUint8 { | ||||
|   const char *str; | ||||
|   uint8_t value; | ||||
| }; | ||||
|  | ||||
| struct Uint8ToString { | ||||
|   uint8_t value; | ||||
|   const char *str; | ||||
| }; | ||||
|  | ||||
| constexpr StringToUint8 BAUD_RATES_BY_STR[] = { | ||||
|     {"9600", BAUD_RATE_9600},     {"19200", BAUD_RATE_19200},   {"38400", BAUD_RATE_38400}, | ||||
|     {"57600", BAUD_RATE_57600},   {"115200", BAUD_RATE_115200}, {"230400", BAUD_RATE_230400}, | ||||
|     {"256000", BAUD_RATE_256000}, {"460800", BAUD_RATE_460800}, | ||||
| }; | ||||
|  | ||||
| constexpr Uint8ToString ZONE_TYPE_BY_UINT[] = { | ||||
|     {ZONE_DISABLED, "Disabled"}, | ||||
|     {ZONE_DETECTION, "Detection"}, | ||||
|     {ZONE_FILTER, "Filter"}, | ||||
| }; | ||||
|  | ||||
| constexpr StringToUint8 ZONE_TYPE_BY_STR[] = { | ||||
|     {"Disabled", ZONE_DISABLED}, | ||||
|     {"Detection", ZONE_DETECTION}, | ||||
|     {"Filter", ZONE_FILTER}, | ||||
| }; | ||||
|  | ||||
| // Helper functions for lookups | ||||
| template<size_t N> uint8_t find_uint8(const StringToUint8 (&arr)[N], const std::string &str) { | ||||
|   for (const auto &entry : arr) { | ||||
|     if (str == entry.str) | ||||
|       return entry.value; | ||||
|   } | ||||
|   return 0xFF;  // Not found | ||||
| } | ||||
|  | ||||
| template<size_t N> const char *find_str(const Uint8ToString (&arr)[N], uint8_t value) { | ||||
|   for (const auto &entry : arr) { | ||||
|     if (value == entry.value) | ||||
|       return entry.str; | ||||
|   } | ||||
|   return "";  // Not found | ||||
| } | ||||
|  | ||||
| // LD2450 serial command header & footer | ||||
| static const uint8_t CMD_FRAME_HEADER[4] = {0xFD, 0xFC, 0xFB, 0xFA}; | ||||
| static const uint8_t CMD_FRAME_END[4] = {0x04, 0x03, 0x02, 0x01}; | ||||
| // LD2450 UART Serial Commands | ||||
| static const uint8_t CMD_ENABLE_CONF = 0x00FF; | ||||
| static const uint8_t CMD_DISABLE_CONF = 0x00FE; | ||||
| static const uint8_t CMD_VERSION = 0x00A0; | ||||
| static const uint8_t CMD_MAC = 0x00A5; | ||||
| static const uint8_t CMD_RESET = 0x00A2; | ||||
| static const uint8_t CMD_RESTART = 0x00A3; | ||||
| static const uint8_t CMD_BLUETOOTH = 0x00A4; | ||||
| static const uint8_t CMD_SINGLE_TARGET_MODE = 0x0080; | ||||
| static const uint8_t CMD_MULTI_TARGET_MODE = 0x0090; | ||||
| static const uint8_t CMD_QUERY_TARGET_MODE = 0x0091; | ||||
| static const uint8_t CMD_SET_BAUD_RATE = 0x00A1; | ||||
| static const uint8_t CMD_QUERY_ZONE = 0x00C1; | ||||
| static const uint8_t CMD_SET_ZONE = 0x00C2; | ||||
| static const uint8_t CMD_ENABLE_CONF = 0xFF; | ||||
| static const uint8_t CMD_DISABLE_CONF = 0xFE; | ||||
| static const uint8_t CMD_VERSION = 0xA0; | ||||
| static const uint8_t CMD_MAC = 0xA5; | ||||
| static const uint8_t CMD_RESET = 0xA2; | ||||
| static const uint8_t CMD_RESTART = 0xA3; | ||||
| static const uint8_t CMD_BLUETOOTH = 0xA4; | ||||
| static const uint8_t CMD_SINGLE_TARGET_MODE = 0x80; | ||||
| static const uint8_t CMD_MULTI_TARGET_MODE = 0x90; | ||||
| static const uint8_t CMD_QUERY_TARGET_MODE = 0x91; | ||||
| static const uint8_t CMD_SET_BAUD_RATE = 0xA1; | ||||
| static const uint8_t CMD_QUERY_ZONE = 0xC1; | ||||
| static const uint8_t CMD_SET_ZONE = 0xC2; | ||||
|  | ||||
| static inline uint16_t convert_seconds_to_ms(uint16_t value) { return value * 1000; }; | ||||
|  | ||||
| @@ -96,18 +184,6 @@ static inline std::string get_direction(int16_t speed) { | ||||
|   return STATIONARY; | ||||
| } | ||||
|  | ||||
| static inline std::string format_mac(uint8_t *buffer) { | ||||
|   return str_snprintf("%02X:%02X:%02X:%02X:%02X:%02X", 17, buffer[10], buffer[11], buffer[12], buffer[13], buffer[14], | ||||
|                       buffer[15]); | ||||
| } | ||||
|  | ||||
| static inline std::string format_version(uint8_t *buffer) { | ||||
|   return str_sprintf("%u.%02X.%02X%02X%02X%02X", buffer[13], buffer[12], buffer[17], buffer[16], buffer[15], | ||||
|                      buffer[14]); | ||||
| } | ||||
|  | ||||
| LD2450Component::LD2450Component() {} | ||||
|  | ||||
| void LD2450Component::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
| #ifdef USE_NUMBER | ||||
| @@ -120,7 +196,7 @@ void LD2450Component::setup() { | ||||
| } | ||||
|  | ||||
| void LD2450Component::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "HLK-LD2450 Human motion tracking radar module:"); | ||||
|   ESP_LOGCONFIG(TAG, "LD2450:"); | ||||
| #ifdef USE_BINARY_SENSOR | ||||
|   LOG_BINARY_SENSOR("  ", "TargetBinarySensor", this->target_binary_sensor_); | ||||
|   LOG_BINARY_SENSOR("  ", "MovingTargetBinarySensor", this->moving_target_binary_sensor_); | ||||
| @@ -189,10 +265,10 @@ void LD2450Component::dump_config() { | ||||
|   LOG_NUMBER("  ", "PresenceTimeoutNumber", this->presence_timeout_number_); | ||||
| #endif | ||||
|   ESP_LOGCONFIG(TAG, | ||||
|                 "  Throttle : %ums\n" | ||||
|                 "  MAC Address : %s\n" | ||||
|                 "  Firmware version : %s", | ||||
|                 this->throttle_, const_cast<char *>(this->mac_.c_str()), const_cast<char *>(this->version_.c_str())); | ||||
|                 "  Throttle: %ums\n" | ||||
|                 "  MAC Address: %s\n" | ||||
|                 "  Firmware version: %s", | ||||
|                 this->throttle_, this->mac_ == NO_MAC ? UNKNOWN_MAC : this->mac_.c_str(), this->version_.c_str()); | ||||
| } | ||||
|  | ||||
| void LD2450Component::loop() { | ||||
| @@ -266,8 +342,7 @@ bool LD2450Component::get_timeout_status_(uint32_t check_millis) { | ||||
|   if (this->timeout_ == 0) { | ||||
|     this->timeout_ = ld2450::convert_seconds_to_ms(DEFAULT_PRESENCE_TIMEOUT); | ||||
|   } | ||||
|   auto current_millis = millis(); | ||||
|   return current_millis - check_millis >= this->timeout_; | ||||
|   return App.get_loop_component_start_time() - check_millis >= this->timeout_; | ||||
| } | ||||
|  | ||||
| // Extract, store and publish zone details LD2450 buffer | ||||
| @@ -354,25 +429,24 @@ void LD2450Component::send_command_(uint8_t command, const uint8_t *command_valu | ||||
| //   Header       Target 1                  Target 2                  Target 3                  End | ||||
| void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) { | ||||
|   if (len < 29) {  // header (4 bytes) + 8 x 3 target data + footer (2 bytes) | ||||
|     ESP_LOGE(TAG, "Periodic data: invalid message length"); | ||||
|     ESP_LOGE(TAG, "Invalid message length"); | ||||
|     return; | ||||
|   } | ||||
|   if (buffer[0] != 0xAA || buffer[1] != 0xFF || buffer[2] != 0x03 || buffer[3] != 0x00) {  // header | ||||
|     ESP_LOGE(TAG, "Periodic data: invalid message header"); | ||||
|     ESP_LOGE(TAG, "Invalid message header"); | ||||
|     return; | ||||
|   } | ||||
|   if (buffer[len - 2] != 0x55 || buffer[len - 1] != 0xCC) {  // footer | ||||
|     ESP_LOGE(TAG, "Periodic data: invalid message footer"); | ||||
|     ESP_LOGE(TAG, "Invalid message footer"); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   auto current_millis = millis(); | ||||
|   if (current_millis - this->last_periodic_millis_ < this->throttle_) { | ||||
|   if (App.get_loop_component_start_time() - this->last_periodic_millis_ < this->throttle_) { | ||||
|     ESP_LOGV(TAG, "Throttling: %d", this->throttle_); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   this->last_periodic_millis_ = current_millis; | ||||
|   this->last_periodic_millis_ = App.get_loop_component_start_time(); | ||||
|  | ||||
|   int16_t target_count = 0; | ||||
|   int16_t still_target_count = 0; | ||||
| @@ -555,13 +629,13 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) { | ||||
| #ifdef USE_SENSOR | ||||
|   // For presence timeout check | ||||
|   if (target_count > 0) { | ||||
|     this->presence_millis_ = millis(); | ||||
|     this->presence_millis_ = App.get_loop_component_start_time(); | ||||
|   } | ||||
|   if (moving_target_count > 0) { | ||||
|     this->moving_presence_millis_ = millis(); | ||||
|     this->moving_presence_millis_ = App.get_loop_component_start_time(); | ||||
|   } | ||||
|   if (still_target_count > 0) { | ||||
|     this->still_presence_millis_ = millis(); | ||||
|     this->still_presence_millis_ = App.get_loop_component_start_time(); | ||||
|   } | ||||
| #endif | ||||
| } | ||||
| @@ -569,31 +643,31 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) { | ||||
| bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) { | ||||
|   ESP_LOGV(TAG, "Handling ack data for command %02X", buffer[COMMAND]); | ||||
|   if (len < 10) { | ||||
|     ESP_LOGE(TAG, "Ack data: invalid length"); | ||||
|     ESP_LOGE(TAG, "Invalid ack length"); | ||||
|     return true; | ||||
|   } | ||||
|   if (buffer[0] != 0xFD || buffer[1] != 0xFC || buffer[2] != 0xFB || buffer[3] != 0xFA) {  // frame header | ||||
|     ESP_LOGE(TAG, "Ack data: invalid header (command %02X)", buffer[COMMAND]); | ||||
|     ESP_LOGE(TAG, "Invalid ack header (command %02X)", buffer[COMMAND]); | ||||
|     return true; | ||||
|   } | ||||
|   if (buffer[COMMAND_STATUS] != 0x01) { | ||||
|     ESP_LOGE(TAG, "Ack data: invalid status"); | ||||
|     ESP_LOGE(TAG, "Invalid ack status"); | ||||
|     return true; | ||||
|   } | ||||
|   if (buffer[8] || buffer[9]) { | ||||
|     ESP_LOGE(TAG, "Ack data: last buffer was %u, %u", buffer[8], buffer[9]); | ||||
|     ESP_LOGE(TAG, "Last buffer was %u, %u", buffer[8], buffer[9]); | ||||
|     return true; | ||||
|   } | ||||
|  | ||||
|   switch (buffer[COMMAND]) { | ||||
|     case lowbyte(CMD_ENABLE_CONF): | ||||
|       ESP_LOGV(TAG, "Got enable conf command"); | ||||
|       ESP_LOGV(TAG, "Enable conf command"); | ||||
|       break; | ||||
|     case lowbyte(CMD_DISABLE_CONF): | ||||
|       ESP_LOGV(TAG, "Got disable conf command"); | ||||
|       ESP_LOGV(TAG, "Disable conf command"); | ||||
|       break; | ||||
|     case lowbyte(CMD_SET_BAUD_RATE): | ||||
|       ESP_LOGV(TAG, "Got baud rate change command"); | ||||
|       ESP_LOGV(TAG, "Baud rate change command"); | ||||
| #ifdef USE_SELECT | ||||
|       if (this->baud_rate_select_ != nullptr) { | ||||
|         ESP_LOGV(TAG, "Change baud rate to %s", this->baud_rate_select_->state.c_str()); | ||||
| @@ -601,7 +675,7 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) { | ||||
| #endif | ||||
|       break; | ||||
|     case lowbyte(CMD_VERSION): | ||||
|       this->version_ = ld2450::format_version(buffer); | ||||
|       this->version_ = str_sprintf(VERSION_FMT, buffer[13], buffer[12], buffer[17], buffer[16], buffer[15], buffer[14]); | ||||
|       ESP_LOGV(TAG, "Firmware version: %s", this->version_.c_str()); | ||||
| #ifdef USE_TEXT_SENSOR | ||||
|       if (this->version_text_sensor_ != nullptr) { | ||||
| @@ -613,7 +687,7 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) { | ||||
|       if (len < 20) { | ||||
|         return false; | ||||
|       } | ||||
|       this->mac_ = ld2450::format_mac(buffer); | ||||
|       this->mac_ = format_mac_address_pretty(&buffer[10]); | ||||
|       ESP_LOGV(TAG, "MAC address: %s", this->mac_.c_str()); | ||||
| #ifdef USE_TEXT_SENSOR | ||||
|       if (this->mac_text_sensor_ != nullptr) { | ||||
| @@ -627,10 +701,10 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) { | ||||
| #endif | ||||
|       break; | ||||
|     case lowbyte(CMD_BLUETOOTH): | ||||
|       ESP_LOGV(TAG, "Got Bluetooth command"); | ||||
|       ESP_LOGV(TAG, "Bluetooth command"); | ||||
|       break; | ||||
|     case lowbyte(CMD_SINGLE_TARGET_MODE): | ||||
|       ESP_LOGV(TAG, "Got single target conf command"); | ||||
|       ESP_LOGV(TAG, "Single target conf command"); | ||||
| #ifdef USE_SWITCH | ||||
|       if (this->multi_target_switch_ != nullptr) { | ||||
|         this->multi_target_switch_->publish_state(false); | ||||
| @@ -638,7 +712,7 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) { | ||||
| #endif | ||||
|       break; | ||||
|     case lowbyte(CMD_MULTI_TARGET_MODE): | ||||
|       ESP_LOGV(TAG, "Got multi target conf command"); | ||||
|       ESP_LOGV(TAG, "Multi target conf command"); | ||||
| #ifdef USE_SWITCH | ||||
|       if (this->multi_target_switch_ != nullptr) { | ||||
|         this->multi_target_switch_->publish_state(true); | ||||
| @@ -646,7 +720,7 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) { | ||||
| #endif | ||||
|       break; | ||||
|     case lowbyte(CMD_QUERY_TARGET_MODE): | ||||
|       ESP_LOGV(TAG, "Got query target tracking mode command"); | ||||
|       ESP_LOGV(TAG, "Query target tracking mode command"); | ||||
| #ifdef USE_SWITCH | ||||
|       if (this->multi_target_switch_ != nullptr) { | ||||
|         this->multi_target_switch_->publish_state(buffer[10] == 0x02); | ||||
| @@ -654,7 +728,7 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) { | ||||
| #endif | ||||
|       break; | ||||
|     case lowbyte(CMD_QUERY_ZONE): | ||||
|       ESP_LOGV(TAG, "Got query zone conf command"); | ||||
|       ESP_LOGV(TAG, "Query zone conf command"); | ||||
|       this->zone_type_ = std::stoi(std::to_string(buffer[10]), nullptr, 16); | ||||
|       this->publish_zone_type(); | ||||
| #ifdef USE_SELECT | ||||
| @@ -674,7 +748,7 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) { | ||||
|       this->process_zone_(buffer); | ||||
|       break; | ||||
|     case lowbyte(CMD_SET_ZONE): | ||||
|       ESP_LOGV(TAG, "Got set zone conf command"); | ||||
|       ESP_LOGV(TAG, "Set zone conf command"); | ||||
|       this->query_zone_info(); | ||||
|       break; | ||||
|     default: | ||||
| @@ -731,7 +805,7 @@ void LD2450Component::set_bluetooth(bool enable) { | ||||
| // Set Baud rate | ||||
| void LD2450Component::set_baud_rate(const std::string &state) { | ||||
|   this->set_config_mode_(true); | ||||
|   uint8_t cmd_value[2] = {BAUD_RATE_ENUM_TO_INT.at(state), 0x00}; | ||||
|   uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00}; | ||||
|   this->send_command_(CMD_SET_BAUD_RATE, cmd_value, 2); | ||||
|   this->set_timeout(200, [this]() { this->restart_(); }); | ||||
| } | ||||
| @@ -739,7 +813,7 @@ void LD2450Component::set_baud_rate(const std::string &state) { | ||||
| // Set Zone Type - one of: Disabled, Detection, Filter | ||||
| void LD2450Component::set_zone_type(const std::string &state) { | ||||
|   ESP_LOGV(TAG, "Set zone type: %s", state.c_str()); | ||||
|   uint8_t zone_type = ZONE_TYPE_ENUM_TO_INT.at(state); | ||||
|   uint8_t zone_type = find_uint8(ZONE_TYPE_BY_STR, state); | ||||
|   this->zone_type_ = zone_type; | ||||
|   this->send_set_zone_command_(); | ||||
| } | ||||
| @@ -747,7 +821,7 @@ void LD2450Component::set_zone_type(const std::string &state) { | ||||
| // Publish Zone Type to Select component | ||||
| void LD2450Component::publish_zone_type() { | ||||
| #ifdef USE_SELECT | ||||
|   std::string zone_type = ZONE_TYPE_INT_TO_ENUM.at(static_cast<ZoneTypeStructure>(this->zone_type_)); | ||||
|   std::string zone_type = find_str(ZONE_TYPE_BY_UINT, this->zone_type_); | ||||
|   if (this->zone_type_select_ != nullptr) { | ||||
|     this->zone_type_select_->publish_state(zone_type); | ||||
|   } | ||||
|   | ||||
| @@ -1,7 +1,5 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <iomanip> | ||||
| #include <map> | ||||
| #include "esphome/components/uart/uart.h" | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/defines.h" | ||||
| @@ -66,49 +64,6 @@ struct ZoneOfNumbers { | ||||
| }; | ||||
| #endif | ||||
|  | ||||
| enum BaudRateStructure : uint8_t { | ||||
|   BAUD_RATE_9600 = 1, | ||||
|   BAUD_RATE_19200 = 2, | ||||
|   BAUD_RATE_38400 = 3, | ||||
|   BAUD_RATE_57600 = 4, | ||||
|   BAUD_RATE_115200 = 5, | ||||
|   BAUD_RATE_230400 = 6, | ||||
|   BAUD_RATE_256000 = 7, | ||||
|   BAUD_RATE_460800 = 8 | ||||
| }; | ||||
|  | ||||
| // Convert baud rate enum to int | ||||
| static const std::map<std::string, uint8_t> BAUD_RATE_ENUM_TO_INT{ | ||||
|     {"9600", BAUD_RATE_9600},     {"19200", BAUD_RATE_19200},   {"38400", BAUD_RATE_38400}, | ||||
|     {"57600", BAUD_RATE_57600},   {"115200", BAUD_RATE_115200}, {"230400", BAUD_RATE_230400}, | ||||
|     {"256000", BAUD_RATE_256000}, {"460800", BAUD_RATE_460800}}; | ||||
|  | ||||
| // Zone type struct | ||||
| enum ZoneTypeStructure : uint8_t { ZONE_DISABLED = 0, ZONE_DETECTION = 1, ZONE_FILTER = 2 }; | ||||
|  | ||||
| // Convert zone type int to enum | ||||
| static const std::map<ZoneTypeStructure, std::string> ZONE_TYPE_INT_TO_ENUM{ | ||||
|     {ZONE_DISABLED, "Disabled"}, {ZONE_DETECTION, "Detection"}, {ZONE_FILTER, "Filter"}}; | ||||
|  | ||||
| // Convert zone type enum to int | ||||
| static const std::map<std::string, uint8_t> ZONE_TYPE_ENUM_TO_INT{ | ||||
|     {"Disabled", ZONE_DISABLED}, {"Detection", ZONE_DETECTION}, {"Filter", ZONE_FILTER}}; | ||||
|  | ||||
| // LD2450 serial command header & footer | ||||
| static const uint8_t CMD_FRAME_HEADER[4] = {0xFD, 0xFC, 0xFB, 0xFA}; | ||||
| static const uint8_t CMD_FRAME_END[4] = {0x04, 0x03, 0x02, 0x01}; | ||||
|  | ||||
| enum PeriodicDataStructure : uint8_t { | ||||
|   TARGET_X = 4, | ||||
|   TARGET_Y = 6, | ||||
|   TARGET_SPEED = 8, | ||||
|   TARGET_RESOLUTION = 10, | ||||
| }; | ||||
|  | ||||
| enum PeriodicDataValue : uint8_t { HEAD = 0xAA, END = 0x55, CHECK = 0x00 }; | ||||
|  | ||||
| enum AckDataStructure : uint8_t { COMMAND = 6, COMMAND_STATUS = 7 }; | ||||
|  | ||||
| class LD2450Component : public Component, public uart::UARTDevice { | ||||
| #ifdef USE_SENSOR | ||||
|   SUB_SENSOR(target_count) | ||||
| @@ -141,7 +96,6 @@ class LD2450Component : public Component, public uart::UARTDevice { | ||||
| #endif | ||||
|  | ||||
|  public: | ||||
|   LD2450Component(); | ||||
|   void setup() override; | ||||
|   void dump_config() override; | ||||
|   void loop() override; | ||||
| @@ -197,17 +151,17 @@ class LD2450Component : public Component, public uart::UARTDevice { | ||||
|   bool get_timeout_status_(uint32_t check_millis); | ||||
|   uint8_t count_targets_in_zone_(const Zone &zone, bool is_moving); | ||||
|  | ||||
|   Target target_info_[MAX_TARGETS]; | ||||
|   Zone zone_config_[MAX_ZONES]; | ||||
|   uint8_t buffer_pos_ = 0;  // where to resume processing/populating buffer | ||||
|   uint8_t buffer_data_[MAX_LINE_LENGTH]; | ||||
|   uint32_t last_periodic_millis_ = 0; | ||||
|   uint32_t presence_millis_ = 0; | ||||
|   uint32_t still_presence_millis_ = 0; | ||||
|   uint32_t moving_presence_millis_ = 0; | ||||
|   uint16_t throttle_ = 0; | ||||
|   uint16_t timeout_ = 5; | ||||
|   uint8_t buffer_pos_ = 0;  // where to resume processing/populating buffer | ||||
|   uint8_t buffer_data_[MAX_LINE_LENGTH]; | ||||
|   uint8_t zone_type_ = 0; | ||||
|   Target target_info_[MAX_TARGETS]; | ||||
|   Zone zone_config_[MAX_ZONES]; | ||||
|   std::string version_{}; | ||||
|   std::string mac_{}; | ||||
| #ifdef USE_NUMBER | ||||
|   | ||||
| @@ -38,8 +38,8 @@ from esphome.const import ( | ||||
|     CONF_WHITE, | ||||
| ) | ||||
| from esphome.core import CORE, coroutine_with_priority | ||||
| from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity | ||||
| from esphome.cpp_generator import MockObjClass | ||||
| from esphome.cpp_helpers import setup_entity | ||||
|  | ||||
| from .automation import LIGHT_STATE_SCHEMA | ||||
| from .effects import ( | ||||
| @@ -110,6 +110,8 @@ LIGHT_SCHEMA = ( | ||||
|     ) | ||||
| ) | ||||
|  | ||||
| LIGHT_SCHEMA.add_extra(entity_duplicate_validator("light")) | ||||
|  | ||||
| BINARY_LIGHT_SCHEMA = LIGHT_SCHEMA.extend( | ||||
|     { | ||||
|         cv.Optional(CONF_EFFECTS): validate_effects(BINARY_EFFECTS), | ||||
| @@ -207,7 +209,7 @@ def validate_color_temperature_channels(value): | ||||
|  | ||||
|  | ||||
| async def setup_light_core_(light_var, output_var, config): | ||||
|     await setup_entity(light_var, config) | ||||
|     await setup_entity(light_var, config, "light") | ||||
|  | ||||
|     cg.add(light_var.set_restore_mode(config[CONF_RESTORE_MODE])) | ||||
|  | ||||
|   | ||||
| @@ -14,8 +14,8 @@ from esphome.const import ( | ||||
|     CONF_WEB_SERVER, | ||||
| ) | ||||
| from esphome.core import CORE, coroutine_with_priority | ||||
| from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity | ||||
| from esphome.cpp_generator import MockObjClass | ||||
| from esphome.cpp_helpers import setup_entity | ||||
|  | ||||
| CODEOWNERS = ["@esphome/core"] | ||||
| IS_PLATFORM_COMPONENT = True | ||||
| @@ -67,6 +67,9 @@ _LOCK_SCHEMA = ( | ||||
| ) | ||||
|  | ||||
|  | ||||
| _LOCK_SCHEMA.add_extra(entity_duplicate_validator("lock")) | ||||
|  | ||||
|  | ||||
| def lock_schema( | ||||
|     class_: MockObjClass = cv.UNDEFINED, | ||||
|     *, | ||||
| @@ -94,7 +97,7 @@ LOCK_SCHEMA.add_extra(cv.deprecated_schema_constant("lock")) | ||||
|  | ||||
|  | ||||
| async def _setup_lock_core(var, config): | ||||
|     await setup_entity(var, config) | ||||
|     await setup_entity(var, config, "lock") | ||||
|  | ||||
|     for conf in config.get(CONF_ON_LOCK, []): | ||||
|         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) | ||||
|   | ||||
| @@ -184,7 +184,9 @@ CONFIG_SCHEMA = cv.All( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(Logger), | ||||
|             cv.Optional(CONF_BAUD_RATE, default=115200): cv.positive_int, | ||||
|             cv.Optional(CONF_TX_BUFFER_SIZE, default=512): cv.validate_bytes, | ||||
|             cv.Optional(CONF_TX_BUFFER_SIZE, default=512): cv.All( | ||||
|                 cv.validate_bytes, cv.int_range(min=160, max=65535) | ||||
|             ), | ||||
|             cv.Optional(CONF_DEASSERT_RTS_DTR, default=False): cv.boolean, | ||||
|             cv.SplitDefault( | ||||
|                 CONF_TASK_LOG_BUFFER_SIZE, | ||||
|   | ||||
| @@ -24,7 +24,7 @@ static const char *const TAG = "logger"; | ||||
| //    - Messages are serialized through main loop for proper console output | ||||
| //    - Fallback to emergency console logging only if ring buffer is full | ||||
| //  - WITHOUT task log buffer: Only emergency console output, no callbacks | ||||
| void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char *format, va_list args) {  // NOLINT | ||||
| void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const char *format, va_list args) {  // NOLINT | ||||
|   if (level > this->level_for(tag)) | ||||
|     return; | ||||
|  | ||||
| @@ -46,8 +46,13 @@ void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char * | ||||
|   bool message_sent = false; | ||||
| #ifdef USE_ESPHOME_TASK_LOG_BUFFER | ||||
|   // For non-main tasks, queue the message for callbacks - but only if we have any callbacks registered | ||||
|   message_sent = this->log_buffer_->send_message_thread_safe(static_cast<uint8_t>(level), tag, | ||||
|                                                              static_cast<uint16_t>(line), current_task, format, args); | ||||
|   message_sent = | ||||
|       this->log_buffer_->send_message_thread_safe(level, tag, static_cast<uint16_t>(line), current_task, format, args); | ||||
|   if (message_sent) { | ||||
|     // Enable logger loop to process the buffered message | ||||
|     // This is safe to call from any context including ISRs | ||||
|     this->enable_loop_soon_any_context(); | ||||
|   } | ||||
| #endif  // USE_ESPHOME_TASK_LOG_BUFFER | ||||
|  | ||||
|   // Emergency console logging for non-main tasks when ring buffer is full or disabled | ||||
| @@ -58,7 +63,7 @@ void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char * | ||||
|     // Maximum size for console log messages (includes null terminator) | ||||
|     static const size_t MAX_CONSOLE_LOG_MSG_SIZE = 144; | ||||
|     char console_buffer[MAX_CONSOLE_LOG_MSG_SIZE];  // MUST be stack allocated for thread safety | ||||
|     int buffer_at = 0;                              // Initialize buffer position | ||||
|     uint16_t buffer_at = 0;                         // Initialize buffer position | ||||
|     this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, console_buffer, &buffer_at, | ||||
|                                                 MAX_CONSOLE_LOG_MSG_SIZE); | ||||
|     this->write_msg_(console_buffer); | ||||
| @@ -69,7 +74,7 @@ void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char * | ||||
| } | ||||
| #else | ||||
| // Implementation for all other platforms | ||||
| void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char *format, va_list args) {  // NOLINT | ||||
| void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const char *format, va_list args) {  // NOLINT | ||||
|   if (level > this->level_for(tag) || global_recursion_guard_) | ||||
|     return; | ||||
|  | ||||
| @@ -85,7 +90,7 @@ void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char * | ||||
| #ifdef USE_STORE_LOG_STR_IN_FLASH | ||||
| // Implementation for ESP8266 with flash string support. | ||||
| // Note: USE_STORE_LOG_STR_IN_FLASH is only defined for ESP8266. | ||||
| void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format, | ||||
| void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __FlashStringHelper *format, | ||||
|                           va_list args) {  // NOLINT | ||||
|   if (level > this->level_for(tag) || global_recursion_guard_) | ||||
|     return; | ||||
| @@ -122,7 +127,7 @@ void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStr | ||||
| } | ||||
| #endif  // USE_STORE_LOG_STR_IN_FLASH | ||||
|  | ||||
| inline int Logger::level_for(const char *tag) { | ||||
| inline uint8_t Logger::level_for(const char *tag) { | ||||
|   auto it = this->log_levels_.find(tag); | ||||
|   if (it != this->log_levels_.end()) | ||||
|     return it->second; | ||||
| @@ -139,6 +144,10 @@ Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate | ||||
| #ifdef USE_ESPHOME_TASK_LOG_BUFFER | ||||
| void Logger::init_log_buffer(size_t total_buffer_size) { | ||||
|   this->log_buffer_ = esphome::make_unique<logger::TaskLogBuffer>(total_buffer_size); | ||||
|  | ||||
|   // Start with loop disabled when using task buffer (unless using USB CDC) | ||||
|   // The loop will be enabled automatically when messages arrive | ||||
|   this->disable_loop_when_buffer_empty_(); | ||||
| } | ||||
| #endif | ||||
|  | ||||
| @@ -189,19 +198,23 @@ void Logger::loop() { | ||||
|         this->write_msg_(this->tx_buffer_); | ||||
|       } | ||||
|     } | ||||
|   } else { | ||||
|     // No messages to process, disable loop if appropriate | ||||
|     // This reduces overhead when there's no async logging activity | ||||
|     this->disable_loop_when_buffer_empty_(); | ||||
|   } | ||||
| #endif | ||||
| } | ||||
| #endif | ||||
|  | ||||
| void Logger::set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; } | ||||
| void Logger::set_log_level(const std::string &tag, int log_level) { this->log_levels_[tag] = log_level; } | ||||
| void Logger::set_log_level(const std::string &tag, uint8_t log_level) { this->log_levels_[tag] = log_level; } | ||||
|  | ||||
| #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) | ||||
| UARTSelection Logger::get_uart() const { return this->uart_; } | ||||
| #endif | ||||
|  | ||||
| void Logger::add_on_log_callback(std::function<void(int, const char *, const char *)> &&callback) { | ||||
| void Logger::add_on_log_callback(std::function<void(uint8_t, const char *, const char *)> &&callback) { | ||||
|   this->log_callback_.add(std::move(callback)); | ||||
| } | ||||
| float Logger::get_setup_priority() const { return setup_priority::BUS + 500.0f; } | ||||
| @@ -230,7 +243,7 @@ void Logger::dump_config() { | ||||
|   } | ||||
| } | ||||
|  | ||||
| void Logger::set_log_level(int level) { | ||||
| void Logger::set_log_level(uint8_t level) { | ||||
|   if (level > ESPHOME_LOG_LEVEL) { | ||||
|     level = ESPHOME_LOG_LEVEL; | ||||
|     ESP_LOGW(TAG, "Cannot set log level higher than pre-compiled %s", LOG_LEVELS[ESPHOME_LOG_LEVEL]); | ||||
|   | ||||
| @@ -61,7 +61,7 @@ static const char *const LOG_LEVEL_LETTERS[] = { | ||||
|  * | ||||
|  * Advanced configuration (pin selection, etc) is not supported. | ||||
|  */ | ||||
| enum UARTSelection { | ||||
| enum UARTSelection : uint8_t { | ||||
| #ifdef USE_LIBRETINY | ||||
|   UART_SELECTION_DEFAULT = 0, | ||||
|   UART_SELECTION_UART0, | ||||
| @@ -129,10 +129,10 @@ class Logger : public Component { | ||||
| #endif | ||||
|  | ||||
|   /// Set the default log level for this logger. | ||||
|   void set_log_level(int level); | ||||
|   void set_log_level(uint8_t level); | ||||
|   /// Set the log level of the specified tag. | ||||
|   void set_log_level(const std::string &tag, int log_level); | ||||
|   int get_log_level() { return this->current_level_; } | ||||
|   void set_log_level(const std::string &tag, uint8_t log_level); | ||||
|   uint8_t get_log_level() { return this->current_level_; } | ||||
|  | ||||
|   // ========== INTERNAL METHODS ========== | ||||
|   // (In most use cases you won't need these) | ||||
| @@ -140,19 +140,20 @@ class Logger : public Component { | ||||
|   void pre_setup(); | ||||
|   void dump_config() override; | ||||
|  | ||||
|   inline int level_for(const char *tag); | ||||
|   inline uint8_t level_for(const char *tag); | ||||
|  | ||||
|   /// Register a callback that will be called for every log message sent | ||||
|   void add_on_log_callback(std::function<void(int, const char *, const char *)> &&callback); | ||||
|   void add_on_log_callback(std::function<void(uint8_t, const char *, const char *)> &&callback); | ||||
|  | ||||
|   // add a listener for log level changes | ||||
|   void add_listener(std::function<void(int)> &&callback) { this->level_callback_.add(std::move(callback)); } | ||||
|   void add_listener(std::function<void(uint8_t)> &&callback) { this->level_callback_.add(std::move(callback)); } | ||||
|  | ||||
|   float get_setup_priority() const override; | ||||
|  | ||||
|   void log_vprintf_(int level, const char *tag, int line, const char *format, va_list args);  // NOLINT | ||||
|   void log_vprintf_(uint8_t level, const char *tag, int line, const char *format, va_list args);  // NOLINT | ||||
| #ifdef USE_STORE_LOG_STR_IN_FLASH | ||||
|   void log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format, va_list args);  // NOLINT | ||||
|   void log_vprintf_(uint8_t level, const char *tag, int line, const __FlashStringHelper *format, | ||||
|                     va_list args);  // NOLINT | ||||
| #endif | ||||
|  | ||||
|  protected: | ||||
| @@ -160,8 +161,9 @@ class Logger : public Component { | ||||
|  | ||||
|   // Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator | ||||
|   // It's the caller's responsibility to initialize buffer_at (typically to 0) | ||||
|   inline void HOT format_log_to_buffer_with_terminator_(int level, const char *tag, int line, const char *format, | ||||
|                                                         va_list args, char *buffer, int *buffer_at, int buffer_size) { | ||||
|   inline void HOT format_log_to_buffer_with_terminator_(uint8_t level, const char *tag, int line, const char *format, | ||||
|                                                         va_list args, char *buffer, uint16_t *buffer_at, | ||||
|                                                         uint16_t buffer_size) { | ||||
| #if defined(USE_ESP32) || defined(USE_LIBRETINY) | ||||
|     this->write_header_to_buffer_(level, tag, line, this->get_thread_name_(), buffer, buffer_at, buffer_size); | ||||
| #else | ||||
| @@ -180,7 +182,7 @@ class Logger : public Component { | ||||
|   } | ||||
|  | ||||
|   // Helper to format and send a log message to both console and callbacks | ||||
|   inline void HOT log_message_to_buffer_and_send_(int level, const char *tag, int line, const char *format, | ||||
|   inline void HOT log_message_to_buffer_and_send_(uint8_t level, const char *tag, int line, const char *format, | ||||
|                                                   va_list args) { | ||||
|     // Format to tx_buffer and prepare for output | ||||
|     this->tx_buffer_at_ = 0;  // Initialize buffer position | ||||
| @@ -194,11 +196,12 @@ class Logger : public Component { | ||||
|   } | ||||
|  | ||||
|   // Write the body of the log message to the buffer | ||||
|   inline void write_body_to_buffer_(const char *value, size_t length, char *buffer, int *buffer_at, int buffer_size) { | ||||
|   inline void write_body_to_buffer_(const char *value, size_t length, char *buffer, uint16_t *buffer_at, | ||||
|                                     uint16_t buffer_size) { | ||||
|     // Calculate available space | ||||
|     const int available = buffer_size - *buffer_at; | ||||
|     if (available <= 0) | ||||
|     if (*buffer_at >= buffer_size) | ||||
|       return; | ||||
|     const uint16_t available = buffer_size - *buffer_at; | ||||
|  | ||||
|     // Determine copy length (minimum of remaining capacity and string length) | ||||
|     const size_t copy_len = (length < static_cast<size_t>(available)) ? length : available; | ||||
| @@ -211,7 +214,7 @@ class Logger : public Component { | ||||
|   } | ||||
|  | ||||
|   // Format string to explicit buffer with varargs | ||||
|   inline void printf_to_buffer_(char *buffer, int *buffer_at, int buffer_size, const char *format, ...) { | ||||
|   inline void printf_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size, const char *format, ...) { | ||||
|     va_list arg; | ||||
|     va_start(arg, format); | ||||
|     this->format_body_to_buffer_(buffer, buffer_at, buffer_size, format, arg); | ||||
| @@ -222,41 +225,50 @@ class Logger : public Component { | ||||
|   const char *get_uart_selection_(); | ||||
| #endif | ||||
|  | ||||
|   // Group 4-byte aligned members first | ||||
|   uint32_t baud_rate_; | ||||
|   char *tx_buffer_{nullptr}; | ||||
|   int tx_buffer_at_{0}; | ||||
|   int tx_buffer_size_{0}; | ||||
| #ifdef USE_ARDUINO | ||||
|   Stream *hw_serial_{nullptr}; | ||||
| #endif | ||||
| #if defined(USE_ESP32) || defined(USE_LIBRETINY) | ||||
|   void *main_task_ = nullptr;  // Only used for thread name identification | ||||
| #endif | ||||
| #ifdef USE_ESP32 | ||||
|   // Task-specific recursion guards: | ||||
|   // - Main task uses a dedicated member variable for efficiency | ||||
|   // - Other tasks use pthread TLS with a dynamically created key via pthread_key_create | ||||
|   pthread_key_t log_recursion_key_;  // 4 bytes | ||||
| #endif | ||||
| #ifdef USE_ESP_IDF | ||||
|   uart_port_t uart_num_;  // 4 bytes (enum defaults to int size) | ||||
| #endif | ||||
|  | ||||
|   // Large objects (internally aligned) | ||||
|   std::map<std::string, uint8_t> log_levels_{}; | ||||
|   CallbackManager<void(uint8_t, const char *, const char *)> log_callback_{}; | ||||
|   CallbackManager<void(uint8_t)> level_callback_{}; | ||||
| #ifdef USE_ESPHOME_TASK_LOG_BUFFER | ||||
|   std::unique_ptr<logger::TaskLogBuffer> log_buffer_;  // Will be initialized with init_log_buffer | ||||
| #endif | ||||
|  | ||||
|   // Group smaller types together at the end | ||||
|   uint16_t tx_buffer_at_{0}; | ||||
|   uint16_t tx_buffer_size_{0}; | ||||
|   uint8_t current_level_{ESPHOME_LOG_LEVEL_VERY_VERBOSE}; | ||||
| #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) | ||||
|   UARTSelection uart_{UART_SELECTION_UART0}; | ||||
| #endif | ||||
| #ifdef USE_LIBRETINY | ||||
|   UARTSelection uart_{UART_SELECTION_DEFAULT}; | ||||
| #endif | ||||
| #ifdef USE_ARDUINO | ||||
|   Stream *hw_serial_{nullptr}; | ||||
| #endif | ||||
| #ifdef USE_ESP_IDF | ||||
|   uart_port_t uart_num_; | ||||
| #endif | ||||
|   std::map<std::string, int> log_levels_{}; | ||||
|   CallbackManager<void(int, const char *, const char *)> log_callback_{}; | ||||
|   int current_level_{ESPHOME_LOG_LEVEL_VERY_VERBOSE}; | ||||
| #ifdef USE_ESPHOME_TASK_LOG_BUFFER | ||||
|   std::unique_ptr<logger::TaskLogBuffer> log_buffer_;  // Will be initialized with init_log_buffer | ||||
| #endif | ||||
| #ifdef USE_ESP32 | ||||
|   // Task-specific recursion guards: | ||||
|   // - Main task uses a dedicated member variable for efficiency | ||||
|   // - Other tasks use pthread TLS with a dynamically created key via pthread_key_create | ||||
|   bool main_task_recursion_guard_{false}; | ||||
|   pthread_key_t log_recursion_key_; | ||||
| #else | ||||
|   bool global_recursion_guard_{false};  // Simple global recursion guard for single-task platforms | ||||
| #endif | ||||
|   CallbackManager<void(int)> level_callback_{}; | ||||
|  | ||||
| #if defined(USE_ESP32) || defined(USE_LIBRETINY) | ||||
|   void *main_task_ = nullptr;  // Only used for thread name identification | ||||
|   const char *HOT get_thread_name_() { | ||||
|     TaskHandle_t current_task = xTaskGetCurrentTaskHandle(); | ||||
|     if (current_task == main_task_) { | ||||
| @@ -297,11 +309,10 @@ class Logger : public Component { | ||||
|   } | ||||
| #endif | ||||
|  | ||||
|   inline void HOT write_header_to_buffer_(int level, const char *tag, int line, const char *thread_name, char *buffer, | ||||
|                                           int *buffer_at, int buffer_size) { | ||||
|   inline void HOT write_header_to_buffer_(uint8_t level, const char *tag, int line, const char *thread_name, | ||||
|                                           char *buffer, uint16_t *buffer_at, uint16_t buffer_size) { | ||||
|     // Format header | ||||
|     if (level < 0) | ||||
|       level = 0; | ||||
|     // uint8_t level is already bounded 0-255, just ensure it's <= 7 | ||||
|     if (level > 7) | ||||
|       level = 7; | ||||
|  | ||||
| @@ -320,12 +331,12 @@ class Logger : public Component { | ||||
|     this->printf_to_buffer_(buffer, buffer_at, buffer_size, "%s[%s][%s:%03u]: ", color, letter, tag, line); | ||||
|   } | ||||
|  | ||||
|   inline void HOT format_body_to_buffer_(char *buffer, int *buffer_at, int buffer_size, const char *format, | ||||
|   inline void HOT format_body_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size, const char *format, | ||||
|                                          va_list args) { | ||||
|     // Get remaining capacity in the buffer | ||||
|     const int remaining = buffer_size - *buffer_at; | ||||
|     if (remaining <= 0) | ||||
|     if (*buffer_at >= buffer_size) | ||||
|       return; | ||||
|     const uint16_t remaining = buffer_size - *buffer_at; | ||||
|  | ||||
|     const int ret = vsnprintf(buffer + *buffer_at, remaining, format, args); | ||||
|  | ||||
| @@ -334,7 +345,7 @@ class Logger : public Component { | ||||
|     } | ||||
|  | ||||
|     // Update buffer_at with the formatted length (handle truncation) | ||||
|     int formatted_len = (ret >= remaining) ? remaining : ret; | ||||
|     uint16_t formatted_len = (ret >= remaining) ? remaining : ret; | ||||
|     *buffer_at += formatted_len; | ||||
|  | ||||
|     // Remove all trailing newlines right after formatting | ||||
| @@ -343,18 +354,38 @@ class Logger : public Component { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   inline void HOT write_footer_to_buffer_(char *buffer, int *buffer_at, int buffer_size) { | ||||
|     static const int RESET_COLOR_LEN = strlen(ESPHOME_LOG_RESET_COLOR); | ||||
|   inline void HOT write_footer_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size) { | ||||
|     static const uint16_t RESET_COLOR_LEN = strlen(ESPHOME_LOG_RESET_COLOR); | ||||
|     this->write_body_to_buffer_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN, buffer, buffer_at, buffer_size); | ||||
|   } | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|   // Disable loop when task buffer is empty (with USB CDC check) | ||||
|   inline void disable_loop_when_buffer_empty_() { | ||||
|     // Thread safety note: This is safe even if another task calls enable_loop_soon_any_context() | ||||
|     // concurrently. If that happens between our check and disable_loop(), the enable request | ||||
|     // will be processed on the next main loop iteration since: | ||||
|     // - disable_loop() takes effect immediately | ||||
|     // - enable_loop_soon_any_context() sets a pending flag that's checked at loop start | ||||
| #if defined(USE_LOGGER_USB_CDC) && defined(USE_ARDUINO) | ||||
|     // Only disable if not using USB CDC (which needs loop for connection detection) | ||||
|     if (this->uart_ != UART_SELECTION_USB_CDC) { | ||||
|       this->disable_loop(); | ||||
|     } | ||||
| #else | ||||
|     // No USB CDC support, always safe to disable | ||||
|     this->disable_loop(); | ||||
| #endif | ||||
|   } | ||||
| #endif | ||||
| }; | ||||
| extern Logger *global_logger;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | ||||
|  | ||||
| class LoggerMessageTrigger : public Trigger<int, const char *, const char *> { | ||||
| class LoggerMessageTrigger : public Trigger<uint8_t, const char *, const char *> { | ||||
|  public: | ||||
|   explicit LoggerMessageTrigger(Logger *parent, int level) { | ||||
|   explicit LoggerMessageTrigger(Logger *parent, uint8_t level) { | ||||
|     this->level_ = level; | ||||
|     parent->add_on_log_callback([this](int level, const char *tag, const char *message) { | ||||
|     parent->add_on_log_callback([this](uint8_t level, const char *tag, const char *message) { | ||||
|       if (level <= this->level_) { | ||||
|         this->trigger(level, tag, message); | ||||
|       } | ||||
| @@ -362,7 +393,7 @@ class LoggerMessageTrigger : public Trigger<int, const char *, const char *> { | ||||
|   } | ||||
|  | ||||
|  protected: | ||||
|   int level_; | ||||
|   uint8_t level_; | ||||
| }; | ||||
|  | ||||
| }  // namespace logger | ||||
|   | ||||
| @@ -466,7 +466,7 @@ LVGL_SCHEMA = cv.All( | ||||
|                 ): lvalid.lv_color, | ||||
|                 cv.Optional(df.CONF_THEME): cv.Schema( | ||||
|                     { | ||||
|                         cv.Optional(name): obj_schema(w) | ||||
|                         cv.Optional(name): obj_schema(w).extend(FULL_STYLE_SCHEMA) | ||||
|                         for name, w in WIDGET_TYPES.items() | ||||
|                     } | ||||
|                 ), | ||||
|   | ||||
| @@ -21,7 +21,7 @@ from esphome.core.config import StartupTrigger | ||||
| from esphome.schema_extractors import SCHEMA_EXTRACT | ||||
|  | ||||
| from . import defines as df, lv_validation as lvalid | ||||
| from .defines import CONF_TIME_FORMAT, LV_GRAD_DIR | ||||
| from .defines import CONF_TIME_FORMAT, LV_GRAD_DIR, TYPE_GRID | ||||
| from .helpers import add_lv_use, requires_component, validate_printf | ||||
| from .lv_validation import lv_color, lv_font, lv_gradient, lv_image, opacity | ||||
| from .lvcode import LvglComponent, lv_event_t_ptr | ||||
| @@ -349,7 +349,60 @@ def obj_schema(widget_type: WidgetType): | ||||
|     ) | ||||
|  | ||||
|  | ||||
| def _validate_grid_layout(config): | ||||
|     layout = config[df.CONF_LAYOUT] | ||||
|     rows = len(layout[df.CONF_GRID_ROWS]) | ||||
|     columns = len(layout[df.CONF_GRID_COLUMNS]) | ||||
|     used_cells = [[None] * columns for _ in range(rows)] | ||||
|     for index, widget in enumerate(config[df.CONF_WIDGETS]): | ||||
|         _, w = next(iter(widget.items())) | ||||
|         if (df.CONF_GRID_CELL_COLUMN_POS in w) != (df.CONF_GRID_CELL_ROW_POS in w): | ||||
|             # pylint: disable=raise-missing-from | ||||
|             raise cv.Invalid( | ||||
|                 "Both row and column positions must be specified, or both omitted", | ||||
|                 [df.CONF_WIDGETS, index], | ||||
|             ) | ||||
|         if df.CONF_GRID_CELL_ROW_POS in w: | ||||
|             row = w[df.CONF_GRID_CELL_ROW_POS] | ||||
|             column = w[df.CONF_GRID_CELL_COLUMN_POS] | ||||
|         else: | ||||
|             try: | ||||
|                 row, column = next( | ||||
|                     (r_idx, c_idx) | ||||
|                     for r_idx, row in enumerate(used_cells) | ||||
|                     for c_idx, value in enumerate(row) | ||||
|                     if value is None | ||||
|                 ) | ||||
|             except StopIteration: | ||||
|                 # pylint: disable=raise-missing-from | ||||
|                 raise cv.Invalid( | ||||
|                     "No free cells available in grid layout", [df.CONF_WIDGETS, index] | ||||
|                 ) | ||||
|             w[df.CONF_GRID_CELL_ROW_POS] = row | ||||
|             w[df.CONF_GRID_CELL_COLUMN_POS] = column | ||||
|  | ||||
|         for i in range(w[df.CONF_GRID_CELL_ROW_SPAN]): | ||||
|             for j in range(w[df.CONF_GRID_CELL_COLUMN_SPAN]): | ||||
|                 if row + i >= rows or column + j >= columns: | ||||
|                     # pylint: disable=raise-missing-from | ||||
|                     raise cv.Invalid( | ||||
|                         f"Cell at {row}/{column} span {w[df.CONF_GRID_CELL_ROW_SPAN]}x{w[df.CONF_GRID_CELL_COLUMN_SPAN]} " | ||||
|                         f"exceeds grid size {rows}x{columns}", | ||||
|                         [df.CONF_WIDGETS, index], | ||||
|                     ) | ||||
|                 if used_cells[row + i][column + j] is not None: | ||||
|                     # pylint: disable=raise-missing-from | ||||
|                     raise cv.Invalid( | ||||
|                         f"Cell span {row + i}/{column + j} already occupied by widget at index {used_cells[row + i][column + j]}", | ||||
|                         [df.CONF_WIDGETS, index], | ||||
|                     ) | ||||
|                 used_cells[row + i][column + j] = index | ||||
|  | ||||
|     return config | ||||
|  | ||||
|  | ||||
| LAYOUT_SCHEMAS = {} | ||||
| LAYOUT_VALIDATORS = {TYPE_GRID: _validate_grid_layout} | ||||
|  | ||||
| ALIGN_TO_SCHEMA = { | ||||
|     cv.Optional(df.CONF_ALIGN_TO): cv.Schema( | ||||
| @@ -402,8 +455,8 @@ LAYOUT_SCHEMA = { | ||||
| } | ||||
|  | ||||
| GRID_CELL_SCHEMA = { | ||||
|     cv.Required(df.CONF_GRID_CELL_ROW_POS): cv.positive_int, | ||||
|     cv.Required(df.CONF_GRID_CELL_COLUMN_POS): cv.positive_int, | ||||
|     cv.Optional(df.CONF_GRID_CELL_ROW_POS): cv.positive_int, | ||||
|     cv.Optional(df.CONF_GRID_CELL_COLUMN_POS): cv.positive_int, | ||||
|     cv.Optional(df.CONF_GRID_CELL_ROW_SPAN, default=1): cv.positive_int, | ||||
|     cv.Optional(df.CONF_GRID_CELL_COLUMN_SPAN, default=1): cv.positive_int, | ||||
|     cv.Optional(df.CONF_GRID_CELL_X_ALIGN): grid_alignments, | ||||
| @@ -454,9 +507,13 @@ def container_validator(schema, widget_type: WidgetType): | ||||
|     """ | ||||
|  | ||||
|     def validator(value): | ||||
|         result = schema | ||||
|         if w_sch := widget_type.schema: | ||||
|             result = result.extend(w_sch) | ||||
|             if isinstance(w_sch, dict): | ||||
|                 w_sch = cv.Schema(w_sch) | ||||
|             # order is important here to preserve extras | ||||
|             result = w_sch.extend(schema) | ||||
|         else: | ||||
|             result = schema | ||||
|         ltype = df.TYPE_NONE | ||||
|         if value and (layout := value.get(df.CONF_LAYOUT)): | ||||
|             if not isinstance(layout, dict): | ||||
| @@ -470,7 +527,10 @@ def container_validator(schema, widget_type: WidgetType): | ||||
|         result = result.extend( | ||||
|             LAYOUT_SCHEMAS.get(ltype.lower(), LAYOUT_SCHEMAS[df.TYPE_NONE]) | ||||
|         ) | ||||
|         return result(value) | ||||
|         value = result(value) | ||||
|         if layout_validator := LAYOUT_VALIDATORS.get(ltype): | ||||
|             value = layout_validator(value) | ||||
|         return value | ||||
|  | ||||
|     return validator | ||||
|  | ||||
|   | ||||
| @@ -3,7 +3,6 @@ import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import CONF_ID | ||||
| from esphome.core import ID | ||||
| from esphome.cpp_generator import MockObj | ||||
|  | ||||
| from .defines import ( | ||||
|     CONF_STYLE_DEFINITIONS, | ||||
| @@ -13,12 +12,13 @@ from .defines import ( | ||||
|     literal, | ||||
| ) | ||||
| from .helpers import add_lv_use | ||||
| from .lvcode import LambdaContext, LocalVariable, lv, lv_assign, lv_variable | ||||
| from .lvcode import LambdaContext, LocalVariable, lv | ||||
| from .schemas import ALL_STYLES, FULL_STYLE_SCHEMA, STYLE_REMAP | ||||
| from .types import ObjUpdateAction, lv_lambda_t, lv_obj_t, lv_obj_t_ptr, lv_style_t | ||||
| from .types import ObjUpdateAction, lv_obj_t, lv_style_t | ||||
| from .widgets import ( | ||||
|     Widget, | ||||
|     add_widgets, | ||||
|     collect_parts, | ||||
|     set_obj_properties, | ||||
|     theme_widget_map, | ||||
|     wait_for_widgets, | ||||
| @@ -37,12 +37,18 @@ async def style_set(svar, style): | ||||
|             lv.call(f"style_set_{remapped_prop}", svar, literal(value)) | ||||
|  | ||||
|  | ||||
| async def create_style(style, id_name): | ||||
|     style_id = ID(id_name, True, lv_style_t) | ||||
|     svar = cg.new_Pvariable(style_id) | ||||
|     lv.style_init(svar) | ||||
|     await style_set(svar, style) | ||||
|     return svar | ||||
|  | ||||
|  | ||||
| async def styles_to_code(config): | ||||
|     """Convert styles to C__ code.""" | ||||
|     for style in config.get(CONF_STYLE_DEFINITIONS, ()): | ||||
|         svar = cg.new_Pvariable(style[CONF_ID]) | ||||
|         lv.style_init(svar) | ||||
|         await style_set(svar, style) | ||||
|         await create_style(style, style[CONF_ID].id) | ||||
|  | ||||
|  | ||||
| @automation.register_action( | ||||
| @@ -68,16 +74,18 @@ async def theme_to_code(config): | ||||
|     if theme := config.get(CONF_THEME): | ||||
|         add_lv_use(CONF_THEME) | ||||
|         for w_name, style in theme.items(): | ||||
|             if not isinstance(style, dict): | ||||
|                 continue | ||||
|  | ||||
|             lname = "lv_theme_apply_" + w_name | ||||
|             apply = lv_variable(lv_lambda_t, lname) | ||||
|             theme_widget_map[w_name] = apply | ||||
|             ow = Widget.create("obj", MockObj(ID("obj")), obj_spec) | ||||
|             async with LambdaContext([(lv_obj_t_ptr, "obj")], where=w_name) as context: | ||||
|                 await set_obj_properties(ow, style) | ||||
|             lv_assign(apply, await context.get_lambda()) | ||||
|             # Work around Python 3.10 bug with nested async comprehensions | ||||
|             # With Python 3.11 this could be simplified | ||||
|             styles = {} | ||||
|             for part, states in collect_parts(style).items(): | ||||
|                 styles[part] = { | ||||
|                     state: await create_style( | ||||
|                         props, | ||||
|                         "_lv_theme_style_" + w_name + "_" + part + "_" + state, | ||||
|                     ) | ||||
|                     for state, props in states.items() | ||||
|                 } | ||||
|             theme_widget_map[w_name] = styles | ||||
|  | ||||
|  | ||||
| async def add_top_layer(lv_component, config): | ||||
|   | ||||
| @@ -6,7 +6,7 @@ from esphome.config_validation import Invalid | ||||
| from esphome.const import CONF_DEFAULT, CONF_GROUP, CONF_ID, CONF_STATE, CONF_TYPE | ||||
| from esphome.core import ID, TimePeriod | ||||
| from esphome.coroutine import FakeAwaitable | ||||
| from esphome.cpp_generator import CallExpression, MockObj | ||||
| from esphome.cpp_generator import MockObj | ||||
|  | ||||
| from ..defines import ( | ||||
|     CONF_FLEX_ALIGN_CROSS, | ||||
| @@ -453,7 +453,17 @@ async def widget_to_code(w_cnfig, w_type: WidgetType, parent): | ||||
|  | ||||
|     w = Widget.create(wid, var, spec, w_cnfig) | ||||
|     if theme := theme_widget_map.get(w_type): | ||||
|         lv_add(CallExpression(theme, w.obj)) | ||||
|         for part, states in theme.items(): | ||||
|             part = "LV_PART_" + part.upper() | ||||
|             for state, style in states.items(): | ||||
|                 state = "LV_STATE_" + state.upper() | ||||
|                 if state == "LV_STATE_DEFAULT": | ||||
|                     lv_state = literal(part) | ||||
|                 elif part == "LV_PART_MAIN": | ||||
|                     lv_state = literal(state) | ||||
|                 else: | ||||
|                     lv_state = join_enums((state, part)) | ||||
|                 lv.obj_add_style(w.obj, style, lv_state) | ||||
|     await set_obj_properties(w, w_cnfig) | ||||
|     await add_widgets(w, w_cnfig) | ||||
|     await spec.to_code(w, w_cnfig) | ||||
|   | ||||
| @@ -1,8 +1,15 @@ | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_MODE, CONF_VALUE | ||||
|  | ||||
| from ..defines import BAR_MODES, CONF_ANIMATED, CONF_INDICATOR, CONF_MAIN, literal | ||||
| from ..lv_validation import animated, get_start_value, lv_float | ||||
| from ..defines import ( | ||||
|     BAR_MODES, | ||||
|     CONF_ANIMATED, | ||||
|     CONF_INDICATOR, | ||||
|     CONF_MAIN, | ||||
|     CONF_START_VALUE, | ||||
|     literal, | ||||
| ) | ||||
| from ..lv_validation import animated, lv_int | ||||
| from ..lvcode import lv | ||||
| from ..types import LvNumber, NumberType | ||||
| from . import Widget | ||||
| @@ -10,22 +17,30 @@ from . import Widget | ||||
| # Note this file cannot be called "bar.py" because that name is disallowed. | ||||
|  | ||||
| CONF_BAR = "bar" | ||||
| BAR_MODIFY_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.Optional(CONF_VALUE): lv_float, | ||||
|         cv.Optional(CONF_ANIMATED, default=True): animated, | ||||
|     } | ||||
| ) | ||||
|  | ||||
|  | ||||
| def validate_bar(config): | ||||
|     if config.get(CONF_MODE) != "LV_BAR_MODE_RANGE" and CONF_START_VALUE in config: | ||||
|         raise cv.Invalid( | ||||
|             f"{CONF_START_VALUE} is only allowed when {CONF_MODE} is set to 'RANGE'" | ||||
|         ) | ||||
|     if (CONF_MIN_VALUE in config) != (CONF_MAX_VALUE in config): | ||||
|         raise cv.Invalid( | ||||
|             f"If either {CONF_MIN_VALUE} or {CONF_MAX_VALUE} is set, both must be set" | ||||
|         ) | ||||
|     return config | ||||
|  | ||||
|  | ||||
| BAR_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.Optional(CONF_VALUE): lv_float, | ||||
|         cv.Optional(CONF_MIN_VALUE, default=0): cv.int_, | ||||
|         cv.Optional(CONF_MAX_VALUE, default=100): cv.int_, | ||||
|         cv.Optional(CONF_MODE, default="NORMAL"): BAR_MODES.one_of, | ||||
|         cv.Optional(CONF_VALUE): lv_int, | ||||
|         cv.Optional(CONF_START_VALUE): lv_int, | ||||
|         cv.Optional(CONF_MIN_VALUE): lv_int, | ||||
|         cv.Optional(CONF_MAX_VALUE): lv_int, | ||||
|         cv.Optional(CONF_MODE): BAR_MODES.one_of, | ||||
|         cv.Optional(CONF_ANIMATED, default=True): animated, | ||||
|     } | ||||
| ) | ||||
| ).add_extra(validate_bar) | ||||
|  | ||||
|  | ||||
| class BarType(NumberType): | ||||
| @@ -35,17 +50,23 @@ class BarType(NumberType): | ||||
|             LvNumber("lv_bar_t"), | ||||
|             parts=(CONF_MAIN, CONF_INDICATOR), | ||||
|             schema=BAR_SCHEMA, | ||||
|             modify_schema=BAR_MODIFY_SCHEMA, | ||||
|         ) | ||||
|  | ||||
|     async def to_code(self, w: Widget, config): | ||||
|         var = w.obj | ||||
|         if mode := config.get(CONF_MODE): | ||||
|             lv.bar_set_mode(var, literal(mode)) | ||||
|         is_animated = literal(config[CONF_ANIMATED]) | ||||
|         if CONF_MIN_VALUE in config: | ||||
|             lv.bar_set_range(var, config[CONF_MIN_VALUE], config[CONF_MAX_VALUE]) | ||||
|             lv.bar_set_mode(var, literal(config[CONF_MODE])) | ||||
|         value = await get_start_value(config) | ||||
|         if value is not None: | ||||
|             lv.bar_set_value(var, value, literal(config[CONF_ANIMATED])) | ||||
|             lv.bar_set_range( | ||||
|                 var, | ||||
|                 await lv_int.process(config[CONF_MIN_VALUE]), | ||||
|                 await lv_int.process(config[CONF_MAX_VALUE]), | ||||
|             ) | ||||
|         if value := await lv_int.process(config.get(CONF_VALUE)): | ||||
|             lv.bar_set_value(var, value, is_animated) | ||||
|         if start_value := await lv_int.process(config.get(CONF_START_VALUE)): | ||||
|             lv.bar_set_start_value(var, start_value, is_animated) | ||||
|  | ||||
|     @property | ||||
|     def animated(self): | ||||
|   | ||||
| @@ -3,7 +3,7 @@ import esphome.config_validation as cv | ||||
| from esphome.const import CONF_SIZE, CONF_TEXT | ||||
| from esphome.cpp_generator import MockObjClass | ||||
|  | ||||
| from ..defines import CONF_MAIN, literal | ||||
| from ..defines import CONF_MAIN | ||||
| from ..lv_validation import color, color_retmapper, lv_text | ||||
| from ..lvcode import LocalVariable, lv, lv_expr | ||||
| from ..schemas import TEXT_SCHEMA | ||||
| @@ -34,7 +34,7 @@ class QrCodeType(WidgetType): | ||||
|         ) | ||||
|  | ||||
|     def get_uses(self): | ||||
|         return ("canvas", "img") | ||||
|         return ("canvas", "img", "label") | ||||
|  | ||||
|     def obj_creator(self, parent: MockObjClass, config: dict): | ||||
|         dark_color = color_retmapper(config[CONF_DARK_COLOR]) | ||||
| @@ -45,10 +45,8 @@ class QrCodeType(WidgetType): | ||||
|     async def to_code(self, w: Widget, config): | ||||
|         if (value := config.get(CONF_TEXT)) is not None: | ||||
|             value = await lv_text.process(value) | ||||
|             with LocalVariable( | ||||
|                 "qr_text", cg.const_char_ptr, value, modifier="" | ||||
|             ) as str_obj: | ||||
|                 lv.qrcode_update(w.obj, str_obj, literal(f"strlen({str_obj})")) | ||||
|             with LocalVariable("qr_text", cg.std_string, value, modifier="") as str_obj: | ||||
|                 lv.qrcode_update(w.obj, str_obj.c_str(), str_obj.size()) | ||||
|  | ||||
|  | ||||
| qr_code_spec = QrCodeType() | ||||
|   | ||||
| @@ -8,7 +8,7 @@ namespace m5stack_8angle { | ||||
| static const char *const TAG = "m5stack_8angle.light"; | ||||
|  | ||||
| void M5Stack8AngleLightOutput::setup() { | ||||
|   ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE); | ||||
|   RAMAllocator<uint8_t> allocator; | ||||
|   this->buf_ = allocator.allocate(M5STACK_8ANGLE_NUM_LEDS * M5STACK_8ANGLE_BYTES_PER_LED); | ||||
|   if (this->buf_ == nullptr) { | ||||
|     ESP_LOGE(TAG, "Failed to allocate buffer of size %u", M5STACK_8ANGLE_NUM_LEDS * M5STACK_8ANGLE_BYTES_PER_LED); | ||||
|   | ||||
| @@ -88,12 +88,7 @@ async def to_code(config): | ||||
|     if CORE.using_esp_idf and CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] >= cv.Version( | ||||
|         5, 0, 0 | ||||
|     ): | ||||
|         add_idf_component( | ||||
|             name="mdns", | ||||
|             repo="https://github.com/espressif/esp-protocols.git", | ||||
|             ref="mdns-v1.8.2", | ||||
|             path="components/mdns", | ||||
|         ) | ||||
|         add_idf_component(name="espressif/mdns", ref="1.8.2") | ||||
|  | ||||
|     cg.add_define("USE_MDNS") | ||||
|  | ||||
|   | ||||
| @@ -11,9 +11,9 @@ from esphome.const import ( | ||||
|     CONF_VOLUME, | ||||
| ) | ||||
| from esphome.core import CORE | ||||
| from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity | ||||
| from esphome.coroutine import coroutine_with_priority | ||||
| from esphome.cpp_generator import MockObjClass | ||||
| from esphome.cpp_helpers import setup_entity | ||||
|  | ||||
| CODEOWNERS = ["@jesserockz"] | ||||
|  | ||||
| @@ -81,7 +81,7 @@ IsAnnouncingCondition = media_player_ns.class_( | ||||
|  | ||||
|  | ||||
| async def setup_media_player_core_(var, config): | ||||
|     await setup_entity(var, config) | ||||
|     await setup_entity(var, config, "media_player") | ||||
|     for conf in config.get(CONF_ON_STATE, []): | ||||
|         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) | ||||
|         await automation.build_automation(trigger, [], conf) | ||||
| @@ -143,6 +143,8 @@ _MEDIA_PLAYER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( | ||||
|     } | ||||
| ) | ||||
|  | ||||
| _MEDIA_PLAYER_SCHEMA.add_extra(entity_duplicate_validator("media_player")) | ||||
|  | ||||
|  | ||||
| def media_player_schema( | ||||
|     class_: MockObjClass, | ||||
| @@ -166,7 +168,6 @@ def media_player_schema( | ||||
| MEDIA_PLAYER_SCHEMA = media_player_schema(MediaPlayer) | ||||
| MEDIA_PLAYER_SCHEMA.add_extra(cv.deprecated_schema_constant("media_player")) | ||||
|  | ||||
|  | ||||
| MEDIA_PLAYER_ACTION_SCHEMA = automation.maybe_simple_id( | ||||
|     cv.Schema( | ||||
|         { | ||||
|   | ||||
| @@ -449,11 +449,7 @@ async def to_code(config): | ||||
|     cg.add_define("USE_MICRO_WAKE_WORD") | ||||
|     cg.add_define("USE_OTA_STATE_CALLBACK") | ||||
|  | ||||
|     esp32.add_idf_component( | ||||
|         name="esp-tflite-micro", | ||||
|         repo="https://github.com/espressif/esp-tflite-micro", | ||||
|         ref="v1.3.3.1", | ||||
|     ) | ||||
|     esp32.add_idf_component(name="espressif/esp-tflite-micro", ref="1.3.3~1") | ||||
|  | ||||
|     cg.add_build_flag("-DTF_LITE_STATIC_MEMORY") | ||||
|     cg.add_build_flag("-DTF_LITE_DISABLE_X86_NEON") | ||||
|   | ||||
| @@ -472,3 +472,4 @@ async def to_code(config): | ||||
|         cg.add(var.set_writer(lambda_)) | ||||
|     await display.register_display(var, config) | ||||
|     await spi.register_spi_device(var, config) | ||||
|     cg.add(var.set_write_only(True)) | ||||
|   | ||||
| @@ -64,6 +64,14 @@ class ModbusDevice { | ||||
|     this->parent_->send(this->address_, function, start_address, number_of_entities, payload_len, payload); | ||||
|   } | ||||
|   void send_raw(const std::vector<uint8_t> &payload) { this->parent_->send_raw(payload); } | ||||
|   void send_error(uint8_t function_code, uint8_t exception_code) { | ||||
|     std::vector<uint8_t> error_response; | ||||
|     error_response.reserve(3); | ||||
|     error_response.push_back(this->address_); | ||||
|     error_response.push_back(function_code | 0x80); | ||||
|     error_response.push_back(exception_code); | ||||
|     this->send_raw(error_response); | ||||
|   } | ||||
|   // If more than one device is connected block sending a new command before a response is received | ||||
|   bool waiting_for_response() { return parent_->waiting_for_response != 0; } | ||||
|  | ||||
|   | ||||
| @@ -112,6 +112,22 @@ TYPE_REGISTER_MAP = { | ||||
|     "FP32_R": 2, | ||||
| } | ||||
|  | ||||
| CPP_TYPE_REGISTER_MAP = { | ||||
|     "RAW": cg.uint16, | ||||
|     "U_WORD": cg.uint16, | ||||
|     "S_WORD": cg.int16, | ||||
|     "U_DWORD": cg.uint32, | ||||
|     "U_DWORD_R": cg.uint32, | ||||
|     "S_DWORD": cg.int32, | ||||
|     "S_DWORD_R": cg.int32, | ||||
|     "U_QWORD": cg.uint64, | ||||
|     "U_QWORD_R": cg.uint64, | ||||
|     "S_QWORD": cg.int64, | ||||
|     "S_QWORD_R": cg.int64, | ||||
|     "FP32": cg.float_, | ||||
|     "FP32_R": cg.float_, | ||||
| } | ||||
|  | ||||
| ModbusCommandSentTrigger = modbus_controller_ns.class_( | ||||
|     "ModbusCommandSentTrigger", automation.Trigger.template(cg.int_, cg.int_) | ||||
| ) | ||||
| @@ -285,21 +301,24 @@ async def to_code(config): | ||||
|     cg.add(var.set_offline_skip_updates(config[CONF_OFFLINE_SKIP_UPDATES])) | ||||
|     if CONF_SERVER_REGISTERS in config: | ||||
|         for server_register in config[CONF_SERVER_REGISTERS]: | ||||
|             cg.add( | ||||
|                 var.add_server_register( | ||||
|                     cg.new_Pvariable( | ||||
|             server_register_var = cg.new_Pvariable( | ||||
|                 server_register[CONF_ID], | ||||
|                 server_register[CONF_ADDRESS], | ||||
|                 server_register[CONF_VALUE_TYPE], | ||||
|                 TYPE_REGISTER_MAP[server_register[CONF_VALUE_TYPE]], | ||||
|             ) | ||||
|             cpp_type = CPP_TYPE_REGISTER_MAP[server_register[CONF_VALUE_TYPE]] | ||||
|             cg.add( | ||||
|                 server_register_var.set_read_lambda( | ||||
|                     cg.TemplateArguments(cpp_type), | ||||
|                     await cg.process_lambda( | ||||
|                         server_register[CONF_READ_LAMBDA], | ||||
|                             [], | ||||
|                             return_type=cg.float_, | ||||
|                         [(cg.uint16, "address")], | ||||
|                         return_type=cpp_type, | ||||
|                     ), | ||||
|                 ) | ||||
|             ) | ||||
|             ) | ||||
|             cg.add(var.add_server_register(server_register_var)) | ||||
|     await register_modbus_device(var, config) | ||||
|     for conf in config.get(CONF_ON_COMMAND_SENT, []): | ||||
|         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) | ||||
|   | ||||
| @@ -117,12 +117,17 @@ void ModbusController::on_modbus_read_registers(uint8_t function_code, uint16_t | ||||
|     bool found = false; | ||||
|     for (auto *server_register : this->server_registers_) { | ||||
|       if (server_register->address == current_address) { | ||||
|         float value = server_register->read_lambda(); | ||||
|         if (!server_register->read_lambda) { | ||||
|           break; | ||||
|         } | ||||
|         int64_t value = server_register->read_lambda(); | ||||
|         ESP_LOGD(TAG, "Matched register. Address: 0x%02X. Value type: %zu. Register count: %u. Value: %s.", | ||||
|                  server_register->address, static_cast<size_t>(server_register->value_type), | ||||
|                  server_register->register_count, server_register->format_value(value).c_str()); | ||||
|  | ||||
|         ESP_LOGD(TAG, "Matched register. Address: 0x%02X. Value type: %zu. Register count: %u. Value: %0.1f.", | ||||
|                  server_register->address, static_cast<uint8_t>(server_register->value_type), | ||||
|                  server_register->register_count, value); | ||||
|         std::vector<uint16_t> payload = float_to_payload(value, server_register->value_type); | ||||
|         std::vector<uint16_t> payload; | ||||
|         payload.reserve(server_register->register_count * 2); | ||||
|         number_to_payload(payload, value, server_register->value_type); | ||||
|         sixteen_bit_response.insert(sixteen_bit_response.end(), payload.cbegin(), payload.cend()); | ||||
|         current_address += server_register->register_count; | ||||
|         found = true; | ||||
| @@ -132,11 +137,7 @@ void ModbusController::on_modbus_read_registers(uint8_t function_code, uint16_t | ||||
|  | ||||
|     if (!found) { | ||||
|       ESP_LOGW(TAG, "Could not match any register to address %02X. Sending exception response.", current_address); | ||||
|       std::vector<uint8_t> error_response; | ||||
|       error_response.push_back(this->address_); | ||||
|       error_response.push_back(0x81); | ||||
|       error_response.push_back(0x02); | ||||
|       this->send_raw(error_response); | ||||
|       send_error(function_code, 0x02); | ||||
|       return; | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -63,6 +63,10 @@ enum class SensorValueType : uint8_t { | ||||
|   FP32_R = 0xD | ||||
| }; | ||||
|  | ||||
| inline bool value_type_is_float(SensorValueType v) { | ||||
|   return v == SensorValueType::FP32 || v == SensorValueType::FP32_R; | ||||
| } | ||||
|  | ||||
| inline ModbusFunctionCode modbus_register_read_function(ModbusRegisterType reg_type) { | ||||
|   switch (reg_type) { | ||||
|     case ModbusRegisterType::COIL: | ||||
| @@ -253,18 +257,53 @@ class SensorItem { | ||||
| }; | ||||
|  | ||||
| class ServerRegister { | ||||
|   using ReadLambda = std::function<int64_t()>; | ||||
|  | ||||
|  public: | ||||
|   ServerRegister(uint16_t address, SensorValueType value_type, uint8_t register_count, | ||||
|                  std::function<float()> read_lambda) { | ||||
|   ServerRegister(uint16_t address, SensorValueType value_type, uint8_t register_count) { | ||||
|     this->address = address; | ||||
|     this->value_type = value_type; | ||||
|     this->register_count = register_count; | ||||
|     this->read_lambda = std::move(read_lambda); | ||||
|   } | ||||
|  | ||||
|   template<typename T> void set_read_lambda(const std::function<T(uint16_t address)> &&user_read_lambda) { | ||||
|     this->read_lambda = [this, user_read_lambda]() -> int64_t { | ||||
|       T user_value = user_read_lambda(this->address); | ||||
|       if constexpr (std::is_same_v<T, float>) { | ||||
|         return bit_cast<uint32_t>(user_value); | ||||
|       } else { | ||||
|         return static_cast<int64_t>(user_value); | ||||
|       } | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   // Formats a raw value into a string representation based on the value type for debugging | ||||
|   std::string format_value(int64_t value) const { | ||||
|     switch (this->value_type) { | ||||
|       case SensorValueType::U_WORD: | ||||
|       case SensorValueType::U_DWORD: | ||||
|       case SensorValueType::U_DWORD_R: | ||||
|       case SensorValueType::U_QWORD: | ||||
|       case SensorValueType::U_QWORD_R: | ||||
|         return std::to_string(static_cast<uint64_t>(value)); | ||||
|       case SensorValueType::S_WORD: | ||||
|       case SensorValueType::S_DWORD: | ||||
|       case SensorValueType::S_DWORD_R: | ||||
|       case SensorValueType::S_QWORD: | ||||
|       case SensorValueType::S_QWORD_R: | ||||
|         return std::to_string(value); | ||||
|       case SensorValueType::FP32_R: | ||||
|       case SensorValueType::FP32: | ||||
|         return str_sprintf("%.1f", bit_cast<float>(static_cast<uint32_t>(value))); | ||||
|       default: | ||||
|         return std::to_string(value); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   uint16_t address{0}; | ||||
|   SensorValueType value_type{SensorValueType::RAW}; | ||||
|   uint8_t register_count{0}; | ||||
|   std::function<float()> read_lambda; | ||||
|   ReadLambda read_lambda; | ||||
| }; | ||||
|  | ||||
| // ModbusController::create_register_ranges_ tries to optimize register range | ||||
| @@ -444,7 +483,7 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice { | ||||
|   void on_modbus_data(const std::vector<uint8_t> &data) override; | ||||
|   /// called when a modbus error response was received | ||||
|   void on_modbus_error(uint8_t function_code, uint8_t exception_code) override; | ||||
|   /// called when a modbus request (function code 3 or 4) was parsed without errors | ||||
|   /// called when a modbus request (function code 0x03 or 0x04) was parsed without errors | ||||
|   void on_modbus_read_registers(uint8_t function_code, uint16_t start_address, uint16_t number_of_registers) final; | ||||
|   /// default delegate called by process_modbus_data when a response has retrieved from the incoming queue | ||||
|   void on_register_data(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data); | ||||
| @@ -529,7 +568,7 @@ inline float payload_to_float(const std::vector<uint8_t> &data, const SensorItem | ||||
|   int64_t number = payload_to_number(data, item.sensor_value_type, item.offset, item.bitmask); | ||||
|  | ||||
|   float float_value; | ||||
|   if (item.sensor_value_type == SensorValueType::FP32 || item.sensor_value_type == SensorValueType::FP32_R) { | ||||
|   if (value_type_is_float(item.sensor_value_type)) { | ||||
|     float_value = bit_cast<float>(static_cast<uint32_t>(number)); | ||||
|   } else { | ||||
|     float_value = static_cast<float>(number); | ||||
| @@ -541,7 +580,7 @@ inline float payload_to_float(const std::vector<uint8_t> &data, const SensorItem | ||||
| inline std::vector<uint16_t> float_to_payload(float value, SensorValueType value_type) { | ||||
|   int64_t val; | ||||
|  | ||||
|   if (value_type == SensorValueType::FP32 || value_type == SensorValueType::FP32_R) { | ||||
|   if (value_type_is_float(value_type)) { | ||||
|     val = bit_cast<uint32_t>(value); | ||||
|   } else { | ||||
|     val = llroundf(value); | ||||
|   | ||||
| @@ -68,6 +68,7 @@ def AUTO_LOAD(): | ||||
|  | ||||
| CONF_DISCOVER_IP = "discover_ip" | ||||
| CONF_IDF_SEND_ASYNC = "idf_send_async" | ||||
| CONF_WAIT_FOR_CONNECTION = "wait_for_connection" | ||||
|  | ||||
|  | ||||
| def validate_message_just_topic(value): | ||||
| @@ -298,6 +299,7 @@ CONFIG_SCHEMA = cv.All( | ||||
|                 } | ||||
|             ), | ||||
|             cv.Optional(CONF_PUBLISH_NAN_AS_NONE, default=False): cv.boolean, | ||||
|             cv.Optional(CONF_WAIT_FOR_CONNECTION, default=False): cv.boolean, | ||||
|         } | ||||
|     ), | ||||
|     validate_config, | ||||
| @@ -453,6 +455,8 @@ async def to_code(config): | ||||
|  | ||||
|     cg.add(var.set_publish_nan_as_none(config[CONF_PUBLISH_NAN_AS_NONE])) | ||||
|  | ||||
|     cg.add(var.set_wait_for_connection(config[CONF_WAIT_FOR_CONNECTION])) | ||||
|  | ||||
|  | ||||
| MQTT_PUBLISH_ACTION_SCHEMA = cv.Schema( | ||||
|     { | ||||
|   | ||||
| @@ -17,7 +17,8 @@ enum class MQTTClientDisconnectReason : int8_t { | ||||
|   MQTT_MALFORMED_CREDENTIALS = 4, | ||||
|   MQTT_NOT_AUTHORIZED = 5, | ||||
|   ESP8266_NOT_ENOUGH_SPACE = 6, | ||||
|   TLS_BAD_FINGERPRINT = 7 | ||||
|   TLS_BAD_FINGERPRINT = 7, | ||||
|   DNS_RESOLVE_ERROR = 8 | ||||
| }; | ||||
|  | ||||
| /// internal struct for MQTT messages. | ||||
|   | ||||
| @@ -176,7 +176,8 @@ void MQTTClientComponent::dump_config() { | ||||
|   } | ||||
| } | ||||
| bool MQTTClientComponent::can_proceed() { | ||||
|   return network::is_disabled() || this->state_ == MQTT_CLIENT_DISABLED || this->is_connected(); | ||||
|   return network::is_disabled() || this->state_ == MQTT_CLIENT_DISABLED || this->is_connected() || | ||||
|          !this->wait_for_connection_; | ||||
| } | ||||
|  | ||||
| void MQTTClientComponent::start_dnslookup_() { | ||||
| @@ -228,6 +229,8 @@ void MQTTClientComponent::check_dnslookup_() { | ||||
|   if (this->dns_resolve_error_) { | ||||
|     ESP_LOGW(TAG, "Couldn't resolve IP address for '%s'", this->credentials_.address.c_str()); | ||||
|     this->state_ = MQTT_CLIENT_DISCONNECTED; | ||||
|     this->disconnect_reason_ = MQTTClientDisconnectReason::DNS_RESOLVE_ERROR; | ||||
|     this->on_disconnect_.call(MQTTClientDisconnectReason::DNS_RESOLVE_ERROR); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
| @@ -697,7 +700,9 @@ void MQTTClientComponent::set_on_connect(mqtt_on_connect_callback_t &&callback) | ||||
| } | ||||
|  | ||||
| void MQTTClientComponent::set_on_disconnect(mqtt_on_disconnect_callback_t &&callback) { | ||||
|   auto callback_copy = callback; | ||||
|   this->mqtt_backend_.set_on_disconnect(std::forward<mqtt_on_disconnect_callback_t>(callback)); | ||||
|   this->on_disconnect_.add(std::move(callback_copy)); | ||||
| } | ||||
|  | ||||
| #if ASYNC_TCP_SSL_ENABLED | ||||
|   | ||||
| @@ -4,11 +4,12 @@ | ||||
|  | ||||
| #ifdef USE_MQTT | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/automation.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/components/json/json_util.h" | ||||
| #include "esphome/components/network/ip_address.h" | ||||
| #include "esphome/core/automation.h" | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/log.h" | ||||
| #if defined(USE_ESP32) | ||||
| #include "mqtt_backend_esp32.h" | ||||
| #elif defined(USE_ESP8266) | ||||
| @@ -267,6 +268,8 @@ class MQTTClientComponent : public Component { | ||||
|   void set_publish_nan_as_none(bool publish_nan_as_none); | ||||
|   bool is_publish_nan_as_none() const; | ||||
|  | ||||
|   void set_wait_for_connection(bool wait_for_connection) { this->wait_for_connection_ = wait_for_connection; } | ||||
|  | ||||
|  protected: | ||||
|   void send_device_info_(); | ||||
|  | ||||
| @@ -332,8 +335,10 @@ class MQTTClientComponent : public Component { | ||||
|   uint32_t connect_begin_; | ||||
|   uint32_t last_connected_{0}; | ||||
|   optional<MQTTClientDisconnectReason> disconnect_reason_{}; | ||||
|   CallbackManager<MQTTBackend::on_disconnect_callback_t> on_disconnect_; | ||||
|  | ||||
|   bool publish_nan_as_none_{false}; | ||||
|   bool wait_for_connection_{false}; | ||||
| }; | ||||
|  | ||||
| extern MQTTClientComponent *global_mqtt_client;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | ||||
|   | ||||
| @@ -71,13 +71,13 @@ bool Nextion::check_connect_() { | ||||
|     } | ||||
|     this->send_command_("connect"); | ||||
|  | ||||
|     this->comok_sent_ = millis(); | ||||
|     this->comok_sent_ = App.get_loop_component_start_time(); | ||||
|     this->ignore_is_setup_ = false; | ||||
|  | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   if (millis() - this->comok_sent_ <= 500)  // Wait 500 ms | ||||
|   if (App.get_loop_component_start_time() - this->comok_sent_ <= 500)  // Wait 500 ms | ||||
|     return false; | ||||
|  | ||||
|   std::string response; | ||||
| @@ -318,15 +318,38 @@ void Nextion::loop() { | ||||
|  | ||||
|   if (!this->nextion_reports_is_setup_) { | ||||
|     if (this->started_ms_ == 0) | ||||
|       this->started_ms_ = millis(); | ||||
|       this->started_ms_ = App.get_loop_component_start_time(); | ||||
|  | ||||
|     if (this->started_ms_ + this->startup_override_ms_ < millis()) { | ||||
|     if (this->started_ms_ + this->startup_override_ms_ < App.get_loop_component_start_time()) { | ||||
|       ESP_LOGD(TAG, "Manual ready set"); | ||||
|       this->nextion_reports_is_setup_ = true; | ||||
|     } | ||||
|   } | ||||
|  | ||||
| #ifdef USE_NEXTION_COMMAND_SPACING | ||||
|   // Try to send any pending commands if spacing allows | ||||
|   this->process_pending_in_queue_(); | ||||
| #endif  // USE_NEXTION_COMMAND_SPACING | ||||
| } | ||||
|  | ||||
| #ifdef USE_NEXTION_COMMAND_SPACING | ||||
| void Nextion::process_pending_in_queue_() { | ||||
|   if (this->nextion_queue_.empty() || !this->command_pacer_.can_send()) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   // Check if first item in queue has a pending command | ||||
|   auto *front_item = this->nextion_queue_.front(); | ||||
|   if (front_item && !front_item->pending_command.empty()) { | ||||
|     if (this->send_command_(front_item->pending_command)) { | ||||
|       // Command sent successfully, clear the pending command | ||||
|       front_item->pending_command.clear(); | ||||
|       ESP_LOGVV(TAG, "Pending command sent: %s", front_item->component->get_variable_name().c_str()); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| #endif  // USE_NEXTION_COMMAND_SPACING | ||||
|  | ||||
| bool Nextion::remove_from_q_(bool report_empty) { | ||||
|   if (this->nextion_queue_.empty()) { | ||||
|     if (report_empty) { | ||||
| @@ -409,7 +432,7 @@ void Nextion::process_nextion_commands_() { | ||||
|       case 0x01:  // instruction sent by user was successful | ||||
|  | ||||
|         ESP_LOGVV(TAG, "Cmd OK"); | ||||
|         ESP_LOGN(TAG, "this->nextion_queue_.empty() %s", this->nextion_queue_.empty() ? "True" : "False"); | ||||
|         ESP_LOGN(TAG, "this->nextion_queue_.empty() %s", YESNO(this->nextion_queue_.empty())); | ||||
|  | ||||
|         this->remove_from_q_(); | ||||
|         if (!this->is_setup_) { | ||||
| @@ -421,7 +444,7 @@ void Nextion::process_nextion_commands_() { | ||||
|         } | ||||
| #ifdef USE_NEXTION_COMMAND_SPACING | ||||
|         this->command_pacer_.mark_sent();  // Here is where we should mark the command as sent | ||||
|         ESP_LOGN(TAG, "Command spacing: marked command sent at %u ms", millis()); | ||||
|         ESP_LOGN(TAG, "Command spacing: marked command sent"); | ||||
| #endif | ||||
|         break; | ||||
|       case 0x02:  // invalid Component ID or name was used | ||||
| @@ -805,7 +828,7 @@ void Nextion::process_nextion_commands_() { | ||||
|     this->command_data_.erase(0, to_process_length + COMMAND_DELIMITER.length() + 1); | ||||
|   } | ||||
|  | ||||
|   uint32_t ms = millis(); | ||||
|   uint32_t ms = App.get_loop_component_start_time(); | ||||
|  | ||||
|   if (!this->nextion_queue_.empty() && this->nextion_queue_.front()->queue_time + this->max_q_age_ms_ < ms) { | ||||
|     for (size_t i = 0; i < this->nextion_queue_.size(); i++) { | ||||
| @@ -940,11 +963,10 @@ uint16_t Nextion::recv_ret_string_(std::string &response, uint32_t timeout, bool | ||||
|   uint16_t ret = 0; | ||||
|   uint8_t c = 0; | ||||
|   uint8_t nr_of_ff_bytes = 0; | ||||
|   uint64_t start; | ||||
|   bool exit_flag = false; | ||||
|   bool ff_flag = false; | ||||
|  | ||||
|   start = millis(); | ||||
|   const uint32_t start = millis(); | ||||
|  | ||||
|   while ((timeout == 0 && this->available()) || millis() - start <= timeout) { | ||||
|     if (!this->available()) { | ||||
| @@ -1034,9 +1056,42 @@ void Nextion::add_no_result_to_queue_with_command_(const std::string &variable_n | ||||
|  | ||||
|   if (this->send_command_(command)) { | ||||
|     this->add_no_result_to_queue_(variable_name); | ||||
| #ifdef USE_NEXTION_COMMAND_SPACING | ||||
|   } else { | ||||
|     // Command blocked by spacing, add to queue WITH the command for retry | ||||
|     this->add_no_result_to_queue_with_pending_command_(variable_name, command); | ||||
| #endif  // USE_NEXTION_COMMAND_SPACING | ||||
|   } | ||||
| } | ||||
|  | ||||
| #ifdef USE_NEXTION_COMMAND_SPACING | ||||
| void Nextion::add_no_result_to_queue_with_pending_command_(const std::string &variable_name, | ||||
|                                                            const std::string &command) { | ||||
| #ifdef USE_NEXTION_MAX_QUEUE_SIZE | ||||
|   if (this->max_queue_size_ > 0 && this->nextion_queue_.size() >= this->max_queue_size_) { | ||||
|     ESP_LOGW(TAG, "Queue full (%zu), drop: %s", this->nextion_queue_.size(), variable_name.c_str()); | ||||
|     return; | ||||
|   } | ||||
| #endif | ||||
|  | ||||
|   RAMAllocator<nextion::NextionQueue> allocator; | ||||
|   nextion::NextionQueue *nextion_queue = allocator.allocate(1); | ||||
|   if (nextion_queue == nullptr) { | ||||
|     ESP_LOGW(TAG, "Queue alloc failed"); | ||||
|     return; | ||||
|   } | ||||
|   new (nextion_queue) nextion::NextionQueue(); | ||||
|  | ||||
|   nextion_queue->component = new nextion::NextionComponentBase; | ||||
|   nextion_queue->component->set_variable_name(variable_name); | ||||
|   nextion_queue->queue_time = App.get_loop_component_start_time(); | ||||
|   nextion_queue->pending_command = command;  // Store command for retry | ||||
|  | ||||
|   this->nextion_queue_.push_back(nextion_queue); | ||||
|   ESP_LOGVV(TAG, "Queue with pending command: %s", variable_name.c_str()); | ||||
| } | ||||
| #endif  // USE_NEXTION_COMMAND_SPACING | ||||
|  | ||||
| bool Nextion::add_no_result_to_queue_with_ignore_sleep_printf_(const std::string &variable_name, const char *format, | ||||
|                                                                ...) { | ||||
|   if ((!this->is_setup() && !this->ignore_is_setup_)) | ||||
| @@ -1168,7 +1223,7 @@ void Nextion::add_to_get_queue(NextionComponentBase *component) { | ||||
|   new (nextion_queue) nextion::NextionQueue(); | ||||
|  | ||||
|   nextion_queue->component = component; | ||||
|   nextion_queue->queue_time = millis(); | ||||
|   nextion_queue->queue_time = App.get_loop_component_start_time(); | ||||
|  | ||||
|   ESP_LOGN(TAG, "Queue %s: %s", component->get_queue_type_string().c_str(), component->get_variable_name().c_str()); | ||||
|  | ||||
| @@ -1200,7 +1255,7 @@ void Nextion::add_addt_command_to_queue(NextionComponentBase *component) { | ||||
|   new (nextion_queue) nextion::NextionQueue(); | ||||
|  | ||||
|   nextion_queue->component = component; | ||||
|   nextion_queue->queue_time = millis(); | ||||
|   nextion_queue->queue_time = App.get_loop_component_start_time(); | ||||
|  | ||||
|   this->waveform_queue_.push_back(nextion_queue); | ||||
|   if (this->waveform_queue_.size() == 1) | ||||
|   | ||||
| @@ -1309,9 +1309,23 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe | ||||
| #ifdef USE_NEXTION_MAX_QUEUE_SIZE | ||||
|   size_t max_queue_size_{0}; | ||||
| #endif  // USE_NEXTION_MAX_QUEUE_SIZE | ||||
|  | ||||
| #ifdef USE_NEXTION_COMMAND_SPACING | ||||
|   NextionCommandPacer command_pacer_{0}; | ||||
|  | ||||
|   /** | ||||
|    * @brief Process any commands in the queue that are pending due to command spacing | ||||
|    * | ||||
|    * This method checks if the first item in the nextion_queue_ has a pending command | ||||
|    * that was previously blocked by command spacing. If spacing now allows and a | ||||
|    * pending command exists, it attempts to send the command. Once successfully sent, | ||||
|    * the pending command is cleared and the queue item continues normal processing. | ||||
|    * | ||||
|    * Called from loop() to retry sending commands that were delayed by spacing. | ||||
|    */ | ||||
|   void process_pending_in_queue_(); | ||||
| #endif  // USE_NEXTION_COMMAND_SPACING | ||||
|  | ||||
|   std::deque<NextionQueue *> nextion_queue_; | ||||
|   std::deque<NextionQueue *> waveform_queue_; | ||||
|   uint16_t recv_ret_string_(std::string &response, uint32_t timeout, bool recv_flag); | ||||
| @@ -1348,6 +1362,23 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe | ||||
|       __attribute__((format(printf, 3, 4))); | ||||
|   void add_no_result_to_queue_with_command_(const std::string &variable_name, const std::string &command); | ||||
|  | ||||
| #ifdef USE_NEXTION_COMMAND_SPACING | ||||
|   /** | ||||
|    * @brief Add a command to the Nextion queue with a pending command for retry | ||||
|    * | ||||
|    * This method creates a queue entry for a command that was blocked by command spacing. | ||||
|    * The command string is stored in the queue item's pending_command field so it can | ||||
|    * be retried later when spacing allows. This ensures commands are not lost when | ||||
|    * sent too quickly. | ||||
|    * | ||||
|    * If the max_queue_size limit is configured and reached, the command will be dropped. | ||||
|    * | ||||
|    * @param variable_name Name of the variable or component associated with the command | ||||
|    * @param command The actual command string to be sent when spacing allows | ||||
|    */ | ||||
|   void add_no_result_to_queue_with_pending_command_(const std::string &variable_name, const std::string &command); | ||||
| #endif  // USE_NEXTION_COMMAND_SPACING | ||||
|  | ||||
|   bool add_no_result_to_queue_with_printf_(const std::string &variable_name, const char *format, ...) | ||||
|       __attribute__((format(printf, 3, 4))); | ||||
|  | ||||
|   | ||||
| @@ -25,6 +25,9 @@ class NextionQueue { | ||||
|   virtual ~NextionQueue() = default; | ||||
|   NextionComponentBase *component; | ||||
|   uint32_t queue_time = 0; | ||||
|  | ||||
|   // Store command for retry if spacing blocked it | ||||
|   std::string pending_command;  // Empty if command was sent successfully | ||||
| }; | ||||
|  | ||||
| class NextionComponentBase { | ||||
|   | ||||
							
								
								
									
										36
									
								
								esphome/components/nextion/nextion_upload.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								esphome/components/nextion/nextion_upload.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| #include "nextion.h" | ||||
|  | ||||
| #ifdef USE_NEXTION_TFT_UPLOAD | ||||
|  | ||||
| #include "esphome/core/application.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace nextion { | ||||
| static const char *const TAG = "nextion.upload"; | ||||
|  | ||||
| bool Nextion::upload_end_(bool successful) { | ||||
|   if (successful) { | ||||
|     ESP_LOGD(TAG, "Upload successful"); | ||||
|     delay(1500);  // NOLINT | ||||
|     App.safe_reboot(); | ||||
|   } else { | ||||
|     ESP_LOGE(TAG, "Upload failed"); | ||||
|  | ||||
|     this->is_updating_ = false; | ||||
|     this->ignore_is_setup_ = false; | ||||
|  | ||||
|     uint32_t baud_rate = this->parent_->get_baud_rate(); | ||||
|     if (baud_rate != this->original_baud_rate_) { | ||||
|       ESP_LOGD(TAG, "Baud: %" PRIu32 "->%" PRIu32, baud_rate, this->original_baud_rate_); | ||||
|       this->parent_->set_baud_rate(this->original_baud_rate_); | ||||
|       this->parent_->load_settings(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return successful; | ||||
| } | ||||
|  | ||||
| }  // namespace nextion | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif  // USE_NEXTION_TFT_UPLOAD | ||||
| @@ -67,8 +67,8 @@ int Nextion::upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start) { | ||||
|     ESP_LOGV(TAG, "Fetch %" PRIu16 " bytes", buffer_size); | ||||
|     uint16_t read_len = 0; | ||||
|     int partial_read_len = 0; | ||||
|     const uint32_t start_time = millis(); | ||||
|     while (read_len < buffer_size && millis() - start_time < 5000) { | ||||
|     const uint32_t start_time = App.get_loop_component_start_time(); | ||||
|     while (read_len < buffer_size && App.get_loop_component_start_time() - start_time < 5000) { | ||||
|       if (http_client.getStreamPtr()->available() > 0) { | ||||
|         partial_read_len = | ||||
|             http_client.getStreamPtr()->readBytes(reinterpret_cast<char *>(buffer) + read_len, buffer_size - read_len); | ||||
| @@ -335,31 +335,6 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { | ||||
|   return upload_end_(true); | ||||
| } | ||||
|  | ||||
| bool Nextion::upload_end_(bool successful) { | ||||
|   ESP_LOGD(TAG, "TFT upload done: %s", YESNO(successful)); | ||||
|  | ||||
|   if (successful) { | ||||
|     ESP_LOGD(TAG, "Restart"); | ||||
|     delay(1500);  // NOLINT | ||||
|     App.safe_reboot(); | ||||
|     delay(1500);  // NOLINT | ||||
|   } else { | ||||
|     ESP_LOGE(TAG, "TFT upload failed"); | ||||
|  | ||||
|     this->is_updating_ = false; | ||||
|     this->ignore_is_setup_ = false; | ||||
|  | ||||
|     uint32_t baud_rate = this->parent_->get_baud_rate(); | ||||
|     if (baud_rate != this->original_baud_rate_) { | ||||
|       ESP_LOGD(TAG, "Baud back: %" PRIu32 "->%" PRIu32, baud_rate, this->original_baud_rate_); | ||||
|       this->parent_->set_baud_rate(this->original_baud_rate_); | ||||
|       this->parent_->load_settings(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return successful; | ||||
| } | ||||
|  | ||||
| #ifdef USE_ESP8266 | ||||
| WiFiClient *Nextion::get_wifi_client_() { | ||||
|   if (this->tft_url_.compare(0, 6, "https:") == 0) { | ||||
|   | ||||
| @@ -335,30 +335,6 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { | ||||
|   return this->upload_end_(true); | ||||
| } | ||||
|  | ||||
| bool Nextion::upload_end_(bool successful) { | ||||
|   ESP_LOGD(TAG, "TFT upload done: %s", YESNO(successful)); | ||||
|  | ||||
|   if (successful) { | ||||
|     ESP_LOGD(TAG, "Restart"); | ||||
|     delay(1500);  // NOLINT | ||||
|     App.safe_reboot(); | ||||
|   } else { | ||||
|     ESP_LOGE(TAG, "TFT upload failed"); | ||||
|  | ||||
|     this->is_updating_ = false; | ||||
|     this->ignore_is_setup_ = false; | ||||
|  | ||||
|     uint32_t baud_rate = this->parent_->get_baud_rate(); | ||||
|     if (baud_rate != this->original_baud_rate_) { | ||||
|       ESP_LOGD(TAG, "Baud back: %" PRIu32 "->%" PRIu32, baud_rate, this->original_baud_rate_); | ||||
|       this->parent_->set_baud_rate(this->original_baud_rate_); | ||||
|       this->parent_->load_settings(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return successful; | ||||
| } | ||||
|  | ||||
| }  // namespace nextion | ||||
| }  // namespace esphome | ||||
|  | ||||
|   | ||||
| @@ -76,8 +76,8 @@ from esphome.const import ( | ||||
|     DEVICE_CLASS_WIND_SPEED, | ||||
| ) | ||||
| from esphome.core import CORE, coroutine_with_priority | ||||
| from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity | ||||
| from esphome.cpp_generator import MockObjClass | ||||
| from esphome.cpp_helpers import setup_entity | ||||
|  | ||||
| CODEOWNERS = ["@esphome/core"] | ||||
| DEVICE_CLASSES = [ | ||||
| @@ -207,6 +207,9 @@ _NUMBER_SCHEMA = ( | ||||
| ) | ||||
|  | ||||
|  | ||||
| _NUMBER_SCHEMA.add_extra(entity_duplicate_validator("number")) | ||||
|  | ||||
|  | ||||
| def number_schema( | ||||
|     class_: MockObjClass, | ||||
|     *, | ||||
| @@ -237,7 +240,7 @@ NUMBER_SCHEMA.add_extra(cv.deprecated_schema_constant("number")) | ||||
| async def setup_number_core_( | ||||
|     var, config, *, min_value: float, max_value: float, step: float | ||||
| ): | ||||
|     await setup_entity(var, config) | ||||
|     await setup_entity(var, config, "number") | ||||
|  | ||||
|     cg.add(var.traits.set_min_value(min_value)) | ||||
|     cg.add(var.traits.set_max_value(max_value)) | ||||
|   | ||||
| @@ -34,6 +34,7 @@ MULTI_CONF = True | ||||
|  | ||||
| CONF_ON_DOWNLOAD_FINISHED = "on_download_finished" | ||||
| CONF_PLACEHOLDER = "placeholder" | ||||
| CONF_UPDATE = "update" | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
|  | ||||
| @@ -167,6 +168,7 @@ SET_URL_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.GenerateID(): cv.use_id(OnlineImage), | ||||
|         cv.Required(CONF_URL): cv.templatable(cv.url), | ||||
|         cv.Optional(CONF_UPDATE, default=True): cv.templatable(bool), | ||||
|     } | ||||
| ) | ||||
|  | ||||
| @@ -188,6 +190,9 @@ async def online_image_action_to_code(config, action_id, template_arg, args): | ||||
|     if CONF_URL in config: | ||||
|         template_ = await cg.templatable(config[CONF_URL], args, cg.std_string) | ||||
|         cg.add(var.set_url(template_)) | ||||
|     if CONF_UPDATE in config: | ||||
|         template_ = await cg.templatable(config[CONF_UPDATE], args, bool) | ||||
|         cg.add(var.set_update(template_)) | ||||
|     return var | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -201,10 +201,13 @@ template<typename... Ts> class OnlineImageSetUrlAction : public Action<Ts...> { | ||||
|  public: | ||||
|   OnlineImageSetUrlAction(OnlineImage *parent) : parent_(parent) {} | ||||
|   TEMPLATABLE_VALUE(std::string, url) | ||||
|   TEMPLATABLE_VALUE(bool, update) | ||||
|   void play(Ts... x) override { | ||||
|     this->parent_->set_url(this->url_.value(x...)); | ||||
|     if (this->update_.value(x...)) { | ||||
|       this->parent_->update(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|  protected: | ||||
|   OnlineImage *parent_; | ||||
|   | ||||
| @@ -46,7 +46,7 @@ def set_sdkconfig_options(config): | ||||
|     add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_PANID", config[CONF_PAN_ID]) | ||||
|     add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_CHANNEL", config[CONF_CHANNEL]) | ||||
|     add_idf_sdkconfig_option( | ||||
|         "CONFIG_OPENTHREAD_NETWORK_MASTERKEY", f"{config[CONF_NETWORK_KEY]:X}" | ||||
|         "CONFIG_OPENTHREAD_NETWORK_MASTERKEY", f"{config[CONF_NETWORK_KEY]:X}".lower() | ||||
|     ) | ||||
|  | ||||
|     if network_name := config.get(CONF_NETWORK_NAME): | ||||
| @@ -54,14 +54,14 @@ def set_sdkconfig_options(config): | ||||
|  | ||||
|     if (ext_pan_id := config.get(CONF_EXT_PAN_ID)) is not None: | ||||
|         add_idf_sdkconfig_option( | ||||
|             "CONFIG_OPENTHREAD_NETWORK_EXTPANID", f"{ext_pan_id:X}" | ||||
|             "CONFIG_OPENTHREAD_NETWORK_EXTPANID", f"{ext_pan_id:X}".lower() | ||||
|         ) | ||||
|     if (mesh_local_prefix := config.get(CONF_MESH_LOCAL_PREFIX)) is not None: | ||||
|         add_idf_sdkconfig_option( | ||||
|             "CONFIG_OPENTHREAD_MESH_LOCAL_PREFIX", f"{mesh_local_prefix:X}" | ||||
|             "CONFIG_OPENTHREAD_MESH_LOCAL_PREFIX", f"{mesh_local_prefix}".lower() | ||||
|         ) | ||||
|     if (pskc := config.get(CONF_PSKC)) is not None: | ||||
|         add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_PSKC", f"{pskc:X}") | ||||
|         add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_PSKC", f"{pskc:X}".lower()) | ||||
|  | ||||
|     if CONF_FORCE_DATASET in config: | ||||
|         if config[CONF_FORCE_DATASET]: | ||||
| @@ -98,7 +98,7 @@ _CONNECTION_SCHEMA = cv.Schema( | ||||
|         cv.Optional(CONF_EXT_PAN_ID): cv.hex_int, | ||||
|         cv.Optional(CONF_NETWORK_NAME): cv.string_strict, | ||||
|         cv.Optional(CONF_PSKC): cv.hex_int, | ||||
|         cv.Optional(CONF_MESH_LOCAL_PREFIX): cv.hex_int, | ||||
|         cv.Optional(CONF_MESH_LOCAL_PREFIX): cv.ipv6network, | ||||
|     } | ||||
| ) | ||||
|  | ||||
|   | ||||
| @@ -137,7 +137,7 @@ void OpenThreadSrpComponent::setup() { | ||||
|   // Copy the mdns services to our local instance so that the c_str pointers remain valid for the lifetime of this | ||||
|   // component | ||||
|   this->mdns_services_ = this->mdns_->get_services(); | ||||
|   ESP_LOGW(TAG, "Setting up SRP services. count = %d\n", this->mdns_services_.size()); | ||||
|   ESP_LOGD(TAG, "Setting up SRP services. count = %d\n", this->mdns_services_.size()); | ||||
|   for (const auto &service : this->mdns_services_) { | ||||
|     otSrpClientBuffersServiceEntry *entry = otSrpClientBuffersAllocateService(instance); | ||||
|     if (!entry) { | ||||
| @@ -185,11 +185,11 @@ void OpenThreadSrpComponent::setup() { | ||||
|     if (error != OT_ERROR_NONE) { | ||||
|       ESP_LOGW(TAG, "Failed to add service: %s", otThreadErrorToString(error)); | ||||
|     } | ||||
|     ESP_LOGW(TAG, "Added service: %s", full_service.c_str()); | ||||
|     ESP_LOGD(TAG, "Added service: %s", full_service.c_str()); | ||||
|   } | ||||
|  | ||||
|   otSrpClientEnableAutoStartMode(instance, srp_start_callback, nullptr); | ||||
|   ESP_LOGW(TAG, "Finished SRP setup"); | ||||
|   ESP_LOGD(TAG, "Finished SRP setup"); | ||||
| } | ||||
|  | ||||
| void *OpenThreadSrpComponent::pool_alloc_(size_t size) { | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| # Sourced from https://gist.github.com/agners/0338576e0003318b63ec1ea75adc90f9 | ||||
| import binascii | ||||
| import ipaddress | ||||
|  | ||||
| from esphome.const import CONF_CHANNEL | ||||
|  | ||||
| @@ -37,6 +38,12 @@ def parse_tlv(tlv) -> dict: | ||||
|         if tag in TLV_TYPES: | ||||
|             if tag == 3: | ||||
|                 output[TLV_TYPES[tag]] = val.decode("utf-8") | ||||
|             elif tag == 7: | ||||
|                 mesh_local_prefix = binascii.hexlify(val).decode("utf-8") | ||||
|                 mesh_local_prefix_str = f"{mesh_local_prefix}0000000000000000" | ||||
|                 ipv6_bytes = bytes.fromhex(mesh_local_prefix_str) | ||||
|                 ipv6_address = ipaddress.IPv6Address(ipv6_bytes) | ||||
|                 output[TLV_TYPES[tag]] = f"{ipv6_address}/64" | ||||
|             else: | ||||
|                 output[TLV_TYPES[tag]] = int.from_bytes(val) | ||||
|     return output | ||||
|   | ||||
							
								
								
									
										0
									
								
								esphome/components/opt3001/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								esphome/components/opt3001/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										122
									
								
								esphome/components/opt3001/opt3001.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								esphome/components/opt3001/opt3001.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,122 @@ | ||||
| #include "opt3001.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace opt3001 { | ||||
|  | ||||
| static const char *const TAG = "opt3001.sensor"; | ||||
|  | ||||
| static const uint8_t OPT3001_REG_RESULT = 0x00; | ||||
| static const uint8_t OPT3001_REG_CONFIGURATION = 0x01; | ||||
| // See datasheet for full description of each bit. | ||||
| static const uint16_t OPT3001_CONFIGURATION_RANGE_FULL = 0b1100000000000000; | ||||
| static const uint16_t OPT3001_CONFIGURATION_CONVERSION_TIME_800 = 0b100000000000; | ||||
| static const uint16_t OPT3001_CONFIGURATION_CONVERSION_MODE_MASK = 0b11000000000; | ||||
| static const uint16_t OPT3001_CONFIGURATION_CONVERSION_MODE_SINGLE_SHOT = 0b01000000000; | ||||
| static const uint16_t OPT3001_CONFIGURATION_CONVERSION_MODE_SHUTDOWN = 0b00000000000; | ||||
| // tl;dr: Configure an automatic-ranged, 800ms single shot reading, | ||||
| // with INT processing disabled | ||||
| static const uint16_t OPT3001_CONFIGURATION_FULL_RANGE_ONE_SHOT = OPT3001_CONFIGURATION_RANGE_FULL | | ||||
|                                                                   OPT3001_CONFIGURATION_CONVERSION_TIME_800 | | ||||
|                                                                   OPT3001_CONFIGURATION_CONVERSION_MODE_SINGLE_SHOT; | ||||
| static const uint16_t OPT3001_CONVERSION_TIME_800 = 825;  // give it 25 extra ms; it seems to not be ready quite often | ||||
|  | ||||
| /* | ||||
| opt3001 properties: | ||||
|  | ||||
| - e (exponent) = high 4 bits of result register | ||||
| - m (mantissa) = low 12 bits of result register | ||||
| - formula: (0.01 * 2^e) * m lx | ||||
|  | ||||
| */ | ||||
|  | ||||
| void OPT3001Sensor::read_result_(const std::function<void(float)> &f) { | ||||
|   // ensure the single shot flag is clear, indicating it's done | ||||
|   uint16_t raw_value; | ||||
|   if (this->read(reinterpret_cast<uint8_t *>(&raw_value), 2) != i2c::ERROR_OK) { | ||||
|     ESP_LOGW(TAG, "Reading configuration register failed"); | ||||
|     f(NAN); | ||||
|     return; | ||||
|   } | ||||
|   raw_value = i2c::i2ctohs(raw_value); | ||||
|  | ||||
|   if ((raw_value & OPT3001_CONFIGURATION_CONVERSION_MODE_MASK) != OPT3001_CONFIGURATION_CONVERSION_MODE_SHUTDOWN) { | ||||
|     // not ready; wait 10ms and try again | ||||
|     ESP_LOGW(TAG, "Data not ready; waiting 10ms"); | ||||
|     this->set_timeout("opt3001_wait", 10, [this, f]() { read_result_(f); }); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   if (this->read_register(OPT3001_REG_RESULT, reinterpret_cast<uint8_t *>(&raw_value), 2) != i2c::ERROR_OK) { | ||||
|     ESP_LOGW(TAG, "Reading result register failed"); | ||||
|     f(NAN); | ||||
|     return; | ||||
|   } | ||||
|   raw_value = i2c::i2ctohs(raw_value); | ||||
|  | ||||
|   uint8_t exponent = raw_value >> 12; | ||||
|   uint16_t mantissa = raw_value & 0b111111111111; | ||||
|  | ||||
|   double lx = 0.01 * pow(2.0, double(exponent)) * double(mantissa); | ||||
|   f(float(lx)); | ||||
| } | ||||
|  | ||||
| void OPT3001Sensor::read_lx_(const std::function<void(float)> &f) { | ||||
|   // turn on (after one-shot sensor automatically powers down) | ||||
|   uint16_t start_measurement = i2c::htoi2cs(OPT3001_CONFIGURATION_FULL_RANGE_ONE_SHOT); | ||||
|   if (this->write_register(OPT3001_REG_CONFIGURATION, reinterpret_cast<uint8_t *>(&start_measurement), 2) != | ||||
|       i2c::ERROR_OK) { | ||||
|     ESP_LOGW(TAG, "Triggering one shot measurement failed"); | ||||
|     f(NAN); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   this->set_timeout("read", OPT3001_CONVERSION_TIME_800, [this, f]() { | ||||
|     if (this->write(&OPT3001_REG_CONFIGURATION, 1, true) != i2c::ERROR_OK) { | ||||
|       ESP_LOGW(TAG, "Starting configuration register read failed"); | ||||
|       f(NAN); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     this->read_result_(f); | ||||
|   }); | ||||
| } | ||||
|  | ||||
| void OPT3001Sensor::dump_config() { | ||||
|   LOG_SENSOR("", "OPT3001", this); | ||||
|   LOG_I2C_DEVICE(this); | ||||
|   if (this->is_failed()) { | ||||
|     ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); | ||||
|   } | ||||
|  | ||||
|   LOG_UPDATE_INTERVAL(this); | ||||
| } | ||||
|  | ||||
| void OPT3001Sensor::update() { | ||||
|   // Set a flag and skip just in case the sensor isn't responding, | ||||
|   // and we just keep waiting for it in read_result_. | ||||
|   // This way we don't end up with potentially boundless "threads" | ||||
|   // using up memory and eventually crashing the device | ||||
|   if (this->updating_) { | ||||
|     return; | ||||
|   } | ||||
|   this->updating_ = true; | ||||
|  | ||||
|   this->read_lx_([this](float val) { | ||||
|     this->updating_ = false; | ||||
|  | ||||
|     if (std::isnan(val)) { | ||||
|       this->status_set_warning(); | ||||
|       this->publish_state(NAN); | ||||
|       return; | ||||
|     } | ||||
|     ESP_LOGD(TAG, "'%s': Illuminance=%.1flx", this->get_name().c_str(), val); | ||||
|     this->status_clear_warning(); | ||||
|     this->publish_state(val); | ||||
|   }); | ||||
| } | ||||
|  | ||||
| float OPT3001Sensor::get_setup_priority() const { return setup_priority::DATA; } | ||||
|  | ||||
| }  // namespace opt3001 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										27
									
								
								esphome/components/opt3001/opt3001.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								esphome/components/opt3001/opt3001.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
| #include "esphome/components/i2c/i2c.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace opt3001 { | ||||
|  | ||||
| /// This class implements support for the i2c-based OPT3001 ambient light sensor. | ||||
| class OPT3001Sensor : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { | ||||
|  public: | ||||
|   void dump_config() override; | ||||
|   void update() override; | ||||
|   float get_setup_priority() const override; | ||||
|  | ||||
|  protected: | ||||
|   // checks if one-shot is complete before reading the result and returning it | ||||
|   void read_result_(const std::function<void(float)> &f); | ||||
|   // begins a one-shot measurement | ||||
|   void read_lx_(const std::function<void(float)> &f); | ||||
|  | ||||
|   bool updating_{false}; | ||||
| }; | ||||
|  | ||||
| }  // namespace opt3001 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										35
									
								
								esphome/components/opt3001/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								esphome/components/opt3001/sensor.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import i2c, sensor | ||||
| from esphome.const import ( | ||||
|     DEVICE_CLASS_ILLUMINANCE, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|     UNIT_LUX, | ||||
| ) | ||||
|  | ||||
| DEPENDENCIES = ["i2c"] | ||||
| CODEOWNERS = ["@ccutrer"] | ||||
|  | ||||
| opt3001_ns = cg.esphome_ns.namespace("opt3001") | ||||
|  | ||||
| OPT3001Sensor = opt3001_ns.class_( | ||||
|     "OPT3001Sensor", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice | ||||
| ) | ||||
|  | ||||
| CONFIG_SCHEMA = ( | ||||
|     sensor.sensor_schema( | ||||
|         OPT3001Sensor, | ||||
|         unit_of_measurement=UNIT_LUX, | ||||
|         accuracy_decimals=1, | ||||
|         device_class=DEVICE_CLASS_ILLUMINANCE, | ||||
|         state_class=STATE_CLASS_MEASUREMENT, | ||||
|     ) | ||||
|     .extend(cv.polling_component_schema("60s")) | ||||
|     .extend(i2c.i2c_device_schema(0x44)) | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = await sensor.new_sensor(config) | ||||
|     await cg.register_component(var, config) | ||||
|     await i2c.register_i2c_device(var, config) | ||||
| @@ -584,7 +584,7 @@ void PN7150::nci_fsm_transition_() { | ||||
|       } else { | ||||
|         this->nci_fsm_set_state_(NCIState::NFCC_INIT); | ||||
|       } | ||||
|       // fall through | ||||
|       [[fallthrough]]; | ||||
|  | ||||
|     case NCIState::NFCC_INIT: | ||||
|       if (this->init_core_() != nfc::STATUS_OK) { | ||||
| @@ -594,7 +594,7 @@ void PN7150::nci_fsm_transition_() { | ||||
|       } else { | ||||
|         this->nci_fsm_set_state_(NCIState::NFCC_CONFIG); | ||||
|       } | ||||
|       // fall through | ||||
|       [[fallthrough]]; | ||||
|  | ||||
|     case NCIState::NFCC_CONFIG: | ||||
|       if (this->send_init_config_() != nfc::STATUS_OK) { | ||||
| @@ -605,7 +605,7 @@ void PN7150::nci_fsm_transition_() { | ||||
|         this->config_refresh_pending_ = false; | ||||
|         this->nci_fsm_set_state_(NCIState::NFCC_SET_DISCOVER_MAP); | ||||
|       } | ||||
|       // fall through | ||||
|       [[fallthrough]]; | ||||
|  | ||||
|     case NCIState::NFCC_SET_DISCOVER_MAP: | ||||
|       if (this->set_discover_map_() != nfc::STATUS_OK) { | ||||
| @@ -615,7 +615,7 @@ void PN7150::nci_fsm_transition_() { | ||||
|       } else { | ||||
|         this->nci_fsm_set_state_(NCIState::NFCC_SET_LISTEN_MODE_ROUTING); | ||||
|       } | ||||
|       // fall through | ||||
|       [[fallthrough]]; | ||||
|  | ||||
|     case NCIState::NFCC_SET_LISTEN_MODE_ROUTING: | ||||
|       if (this->set_listen_mode_routing_() != nfc::STATUS_OK) { | ||||
| @@ -625,7 +625,7 @@ void PN7150::nci_fsm_transition_() { | ||||
|       } else { | ||||
|         this->nci_fsm_set_state_(NCIState::RFST_IDLE); | ||||
|       } | ||||
|       // fall through | ||||
|       [[fallthrough]]; | ||||
|  | ||||
|     case NCIState::RFST_IDLE: | ||||
|       if (this->nci_state_error_ == NCIState::RFST_DISCOVERY) { | ||||
| @@ -650,14 +650,14 @@ void PN7150::nci_fsm_transition_() { | ||||
|  | ||||
|     case NCIState::RFST_W4_HOST_SELECT: | ||||
|       select_endpoint_(); | ||||
|       // fall through | ||||
|       [[fallthrough]]; | ||||
|  | ||||
|     // All cases below are waiting for NOTIFICATION messages | ||||
|     case NCIState::RFST_DISCOVERY: | ||||
|       if (this->config_refresh_pending_) { | ||||
|         this->refresh_core_config_(); | ||||
|       } | ||||
|       // fall through | ||||
|       [[fallthrough]]; | ||||
|  | ||||
|     case NCIState::RFST_LISTEN_ACTIVE: | ||||
|     case NCIState::RFST_LISTEN_SLEEP: | ||||
|   | ||||
| @@ -609,7 +609,7 @@ void PN7160::nci_fsm_transition_() { | ||||
|       } else { | ||||
|         this->nci_fsm_set_state_(NCIState::NFCC_INIT); | ||||
|       } | ||||
|       // fall through | ||||
|       [[fallthrough]]; | ||||
|  | ||||
|     case NCIState::NFCC_INIT: | ||||
|       if (this->init_core_() != nfc::STATUS_OK) { | ||||
| @@ -619,7 +619,7 @@ void PN7160::nci_fsm_transition_() { | ||||
|       } else { | ||||
|         this->nci_fsm_set_state_(NCIState::NFCC_CONFIG); | ||||
|       } | ||||
|       // fall through | ||||
|       [[fallthrough]]; | ||||
|  | ||||
|     case NCIState::NFCC_CONFIG: | ||||
|       if (this->send_init_config_() != nfc::STATUS_OK) { | ||||
| @@ -630,7 +630,7 @@ void PN7160::nci_fsm_transition_() { | ||||
|         this->config_refresh_pending_ = false; | ||||
|         this->nci_fsm_set_state_(NCIState::NFCC_SET_DISCOVER_MAP); | ||||
|       } | ||||
|       // fall through | ||||
|       [[fallthrough]]; | ||||
|  | ||||
|     case NCIState::NFCC_SET_DISCOVER_MAP: | ||||
|       if (this->set_discover_map_() != nfc::STATUS_OK) { | ||||
| @@ -640,7 +640,7 @@ void PN7160::nci_fsm_transition_() { | ||||
|       } else { | ||||
|         this->nci_fsm_set_state_(NCIState::NFCC_SET_LISTEN_MODE_ROUTING); | ||||
|       } | ||||
|       // fall through | ||||
|       [[fallthrough]]; | ||||
|  | ||||
|     case NCIState::NFCC_SET_LISTEN_MODE_ROUTING: | ||||
|       if (this->set_listen_mode_routing_() != nfc::STATUS_OK) { | ||||
| @@ -650,7 +650,7 @@ void PN7160::nci_fsm_transition_() { | ||||
|       } else { | ||||
|         this->nci_fsm_set_state_(NCIState::RFST_IDLE); | ||||
|       } | ||||
|       // fall through | ||||
|       [[fallthrough]]; | ||||
|  | ||||
|     case NCIState::RFST_IDLE: | ||||
|       if (this->nci_state_error_ == NCIState::RFST_DISCOVERY) { | ||||
| @@ -675,14 +675,14 @@ void PN7160::nci_fsm_transition_() { | ||||
|  | ||||
|     case NCIState::RFST_W4_HOST_SELECT: | ||||
|       select_endpoint_(); | ||||
|       // fall through | ||||
|       [[fallthrough]]; | ||||
|  | ||||
|     // All cases below are waiting for NOTIFICATION messages | ||||
|     case NCIState::RFST_DISCOVERY: | ||||
|       if (this->config_refresh_pending_) { | ||||
|         this->refresh_core_config_(); | ||||
|       } | ||||
|       // fall through | ||||
|       [[fallthrough]]; | ||||
|  | ||||
|     case NCIState::RFST_LISTEN_ACTIVE: | ||||
|     case NCIState::RFST_LISTEN_SLEEP: | ||||
|   | ||||
| @@ -9,8 +9,8 @@ | ||||
| #include <hardware/dma.h> | ||||
| #include <hardware/irq.h> | ||||
| #include <hardware/pio.h> | ||||
| #include <pico/stdlib.h> | ||||
| #include <pico/sem.h> | ||||
| #include <pico/stdlib.h> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace rp2040_pio_led_strip { | ||||
| @@ -44,7 +44,7 @@ void RP2040PIOLEDStripLightOutput::setup() { | ||||
|  | ||||
|   size_t buffer_size = this->get_buffer_size_(); | ||||
|  | ||||
|   ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE); | ||||
|   RAMAllocator<uint8_t> allocator; | ||||
|   this->buf_ = allocator.allocate(buffer_size); | ||||
|   if (this->buf_ == nullptr) { | ||||
|     ESP_LOGE(TAG, "Failed to allocate buffer of size %u", buffer_size); | ||||
|   | ||||
| @@ -33,12 +33,15 @@ class SafeModeComponent : public Component { | ||||
|   void write_rtc_(uint32_t val); | ||||
|   uint32_t read_rtc_(); | ||||
|  | ||||
|   bool boot_successful_{false};                   ///< set to true after boot is considered successful | ||||
|   // Group all 4-byte aligned members together to avoid padding | ||||
|   uint32_t safe_mode_boot_is_good_after_{60000};  ///< The amount of time after which the boot is considered successful | ||||
|   uint32_t safe_mode_enable_time_{60000};         ///< The time safe mode should remain active for | ||||
|   uint32_t safe_mode_rtc_value_{0}; | ||||
|   uint32_t safe_mode_start_time_{0};  ///< stores when safe mode was enabled | ||||
|   // Group 1-byte members together to minimize padding | ||||
|   bool boot_successful_{false};  ///< set to true after boot is considered successful | ||||
|   uint8_t safe_mode_num_attempts_{0}; | ||||
|   // Larger objects at the end | ||||
|   ESPPreferenceObject rtc_; | ||||
|   CallbackManager<void()> safe_mode_callback_{}; | ||||
|  | ||||
|   | ||||
| @@ -17,8 +17,8 @@ from esphome.const import ( | ||||
|     CONF_WEB_SERVER, | ||||
| ) | ||||
| from esphome.core import CORE, coroutine_with_priority | ||||
| from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity | ||||
| from esphome.cpp_generator import MockObjClass | ||||
| from esphome.cpp_helpers import setup_entity | ||||
|  | ||||
| CODEOWNERS = ["@esphome/core"] | ||||
| IS_PLATFORM_COMPONENT = True | ||||
| @@ -65,6 +65,9 @@ _SELECT_SCHEMA = ( | ||||
| ) | ||||
|  | ||||
|  | ||||
| _SELECT_SCHEMA.add_extra(entity_duplicate_validator("select")) | ||||
|  | ||||
|  | ||||
| def select_schema( | ||||
|     class_: MockObjClass, | ||||
|     *, | ||||
| @@ -89,7 +92,7 @@ SELECT_SCHEMA.add_extra(cv.deprecated_schema_constant("select")) | ||||
|  | ||||
|  | ||||
| async def setup_select_core_(var, config, *, options: list[str]): | ||||
|     await setup_entity(var, config) | ||||
|     await setup_entity(var, config, "select") | ||||
|  | ||||
|     cg.add(var.traits.set_options(options)) | ||||
|  | ||||
|   | ||||
| @@ -101,8 +101,8 @@ from esphome.const import ( | ||||
|     ENTITY_CATEGORY_CONFIG, | ||||
| ) | ||||
| from esphome.core import CORE, coroutine_with_priority | ||||
| from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity | ||||
| from esphome.cpp_generator import MockObjClass | ||||
| from esphome.cpp_helpers import setup_entity | ||||
| from esphome.util import Registry | ||||
|  | ||||
| CODEOWNERS = ["@esphome/core"] | ||||
| @@ -318,6 +318,8 @@ _SENSOR_SCHEMA = ( | ||||
|     ) | ||||
| ) | ||||
|  | ||||
| _SENSOR_SCHEMA.add_extra(entity_duplicate_validator("sensor")) | ||||
|  | ||||
|  | ||||
| def sensor_schema( | ||||
|     class_: MockObjClass = cv.UNDEFINED, | ||||
| @@ -787,7 +789,7 @@ async def build_filters(config): | ||||
|  | ||||
|  | ||||
| async def setup_sensor_core_(var, config): | ||||
|     await setup_entity(var, config) | ||||
|     await setup_entity(var, config, "sensor") | ||||
|  | ||||
|     if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: | ||||
|         cg.add(var.set_device_class(device_class)) | ||||
|   | ||||
| @@ -23,16 +23,22 @@ std::string state_class_to_string(StateClass state_class) { | ||||
| Sensor::Sensor() : state(NAN), raw_state(NAN) {} | ||||
|  | ||||
| int8_t Sensor::get_accuracy_decimals() { | ||||
|   if (this->accuracy_decimals_.has_value()) | ||||
|     return *this->accuracy_decimals_; | ||||
|   if (this->sensor_flags_.has_accuracy_override) | ||||
|     return this->accuracy_decimals_; | ||||
|   return 0; | ||||
| } | ||||
| void Sensor::set_accuracy_decimals(int8_t accuracy_decimals) { this->accuracy_decimals_ = accuracy_decimals; } | ||||
| void Sensor::set_accuracy_decimals(int8_t accuracy_decimals) { | ||||
|   this->accuracy_decimals_ = accuracy_decimals; | ||||
|   this->sensor_flags_.has_accuracy_override = true; | ||||
| } | ||||
|  | ||||
| void Sensor::set_state_class(StateClass state_class) { this->state_class_ = state_class; } | ||||
| void Sensor::set_state_class(StateClass state_class) { | ||||
|   this->state_class_ = state_class; | ||||
|   this->sensor_flags_.has_state_class_override = true; | ||||
| } | ||||
| StateClass Sensor::get_state_class() { | ||||
|   if (this->state_class_.has_value()) | ||||
|     return *this->state_class_; | ||||
|   if (this->sensor_flags_.has_state_class_override) | ||||
|     return this->state_class_; | ||||
|   return StateClass::STATE_CLASS_NONE; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -80,9 +80,9 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa | ||||
|    * state changes to the database when they are published, even if the state is the | ||||
|    * same as before. | ||||
|    */ | ||||
|   bool get_force_update() const { return force_update_; } | ||||
|   bool get_force_update() const { return sensor_flags_.force_update; } | ||||
|   /// Set force update mode. | ||||
|   void set_force_update(bool force_update) { force_update_ = force_update; } | ||||
|   void set_force_update(bool force_update) { sensor_flags_.force_update = force_update; } | ||||
|  | ||||
|   /// Add a filter to the filter chain. Will be appended to the back. | ||||
|   void add_filter(Filter *filter); | ||||
| @@ -155,9 +155,17 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa | ||||
|  | ||||
|   Filter *filter_list_{nullptr};  ///< Store all active filters. | ||||
|  | ||||
|   optional<int8_t> accuracy_decimals_;                  ///< Accuracy in decimals override | ||||
|   optional<StateClass> state_class_{STATE_CLASS_NONE};  ///< State class override | ||||
|   bool force_update_{false};                            ///< Force update mode | ||||
|   // Group small members together to avoid padding | ||||
|   int8_t accuracy_decimals_{-1};              ///< Accuracy in decimals (-1 = not set) | ||||
|   StateClass state_class_{STATE_CLASS_NONE};  ///< State class (STATE_CLASS_NONE = not set) | ||||
|  | ||||
|   // Bit-packed flags for sensor-specific settings | ||||
|   struct SensorFlags { | ||||
|     uint8_t has_accuracy_override : 1; | ||||
|     uint8_t has_state_class_override : 1; | ||||
|     uint8_t force_update : 1; | ||||
|     uint8_t reserved : 5;  // Reserved for future use | ||||
|   } sensor_flags_{}; | ||||
| }; | ||||
|  | ||||
| }  // namespace sensor | ||||
|   | ||||
| @@ -445,8 +445,7 @@ template<typename T> stm32_err_t stm32_check_ack_timeout(const stm32_err_t s_err | ||||
|       return STM32_ERR_OK; | ||||
|     case STM32_ERR_NACK: | ||||
|       log(); | ||||
|       // TODO: c++17 [[fallthrough]] | ||||
|       /* fallthrough */ | ||||
|       [[fallthrough]]; | ||||
|     default: | ||||
|       return STM32_ERR_UNKNOWN; | ||||
|   } | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user