From 2bb01853e0917505f5dcea44ca7e031dfe946010 Mon Sep 17 00:00:00 2001 From: Edu_Coder Date: Sat, 8 Aug 2026 20:53:10 -0400 Subject: [PATCH 1/2] [tuya] GMT time 0x0C command handler (#17158) --- esphome/components/tuya/tuya.cpp | 35 ++++++++++++++++++++++++++++++++ esphome/components/tuya/tuya.h | 3 +++ 2 files changed, 38 insertions(+) diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index 3058d82cc4..15ab4b6dc3 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -303,6 +303,22 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff ESP_LOGW(TAG, "LOCAL_TIME_QUERY is not handled because time is not configured"); } break; + case TuyaCommandType::GMT_TIME_QUERY: +#ifdef USE_TIME + if (this->time_id_ != nullptr) { + this->send_gmt_time_(); + + if (!this->gmt_time_sync_callback_registered_) { + // tuya mcu supports time, so we let them know when our time changed + this->time_id_->add_on_time_sync_callback([this] { this->send_gmt_time_(); }); + this->gmt_time_sync_callback_registered_ = true; + } + } else +#endif + { + ESP_LOGW(TAG, "GMT_TIME_QUERY is not handled because time is not configured"); + } + break; case TuyaCommandType::VACUUM_MAP_UPLOAD: this->send_command_( TuyaCommand{.cmd = TuyaCommandType::VACUUM_MAP_UPLOAD, .payload = std::vector{0x01}}); @@ -609,6 +625,25 @@ void Tuya::send_local_time_() { } this->send_command_(TuyaCommand{.cmd = TuyaCommandType::LOCAL_TIME_QUERY, .payload = payload}); } +void Tuya::send_gmt_time_() { + std::vector payload; + ESPTime now = this->time_id_->utcnow(); + if (now.is_valid()) { + uint8_t year = now.year - 2000; + uint8_t month = now.month; + uint8_t day_of_month = now.day_of_month; + uint8_t hour = now.hour; + uint8_t minute = now.minute; + uint8_t second = now.second; + ESP_LOGD(TAG, "Sending gmt time"); + payload = std::vector{0x01, year, month, day_of_month, hour, minute, second}; + } else { + // By spec we need to notify MCU that the time was not obtained if this is a response to a query + ESP_LOGW(TAG, "Sending missing gmt time"); + payload = std::vector{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + this->send_command_(TuyaCommand{.cmd = TuyaCommandType::GMT_TIME_QUERY, .payload = payload}); +} #endif void Tuya::set_raw_datapoint_value(uint8_t datapoint_id, const std::vector &value) { diff --git a/esphome/components/tuya/tuya.h b/esphome/components/tuya/tuya.h index 4e7ab5c7f9..b8bf4e0ab1 100644 --- a/esphome/components/tuya/tuya.h +++ b/esphome/components/tuya/tuya.h @@ -54,6 +54,7 @@ enum class TuyaCommandType : uint8_t { DATAPOINT_DELIVER = 0x06, DATAPOINT_REPORT_ASYNC = 0x07, DATAPOINT_QUERY = 0x08, + GMT_TIME_QUERY = 0x0C, WIFI_TEST = 0x0E, LOCAL_TIME_QUERY = 0x1C, DATAPOINT_REPORT_SYNC = 0x22, @@ -138,8 +139,10 @@ class Tuya final : public Component, public uart::UARTDevice { #ifdef USE_TIME void send_local_time_(); + void send_gmt_time_(); time::RealTimeClock *time_id_{nullptr}; bool time_sync_callback_registered_{false}; + bool gmt_time_sync_callback_registered_{false}; #endif TuyaInitState init_state_ = TuyaInitState::INIT_HEARTBEAT; bool init_failed_{false}; From 862b13c8ddf4fbe0289f562472c3602edec8d502 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Aug 2026 20:58:48 -0500 Subject: [PATCH 2/2] [esp32_ble_tracker] Retire the scanner-state listener interface (#18179) --- .../components/esp32_ble_tracker/__init__.py | 19 +-------------- .../esp32_ble_tracker/esp32_ble_tracker.cpp | 5 ---- .../esp32_ble_tracker/esp32_ble_tracker.h | 23 ------------------- esphome/core/defines.h | 1 - .../ble_device_base/test_slot_counter.py | 12 ++-------- 5 files changed, 3 insertions(+), 57 deletions(-) diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 646ce79233..b1ad07dfdd 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -66,12 +66,9 @@ def _get_required_features() -> set[BLEFeatures]: # Slot counters sizing the tracker's StaticVector storage; one request per -# registered listener, client, or scanner state listener. +# registered listener or client. _request_listener_slot = cg.slot_counter("ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT") _request_client_slot = cg.slot_counter("ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT") -_request_scanner_state_listener_slot = cg.slot_counter( - "ESPHOME_ESP32_BLE_TRACKER_SCANNER_STATE_LISTENER_COUNT" -) def register_ble_features(features: set[BLEFeatures]) -> None: @@ -386,17 +383,3 @@ async def register_raw_client( paren = await cg.get_variable(config[CONF_ESP32_BLE_ID]) cg.add(paren.register_client(var)) return var - - -async def register_scanner_state_listener( - var: cg.SafeExpType, config: ConfigType -) -> cg.SafeExpType: - """Register a listener for scanner state changes. - - The slot request here is what sizes the tracker's listener storage; a - build with no registrations compiles the storage out entirely. - """ - _request_scanner_state_listener_slot() - paren = await cg.get_variable(config[CONF_ESP32_BLE_ID]) - cg.add(paren.add_scanner_state_listener(var)) - return var diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 0950bfeb70..18b6cf022d 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -392,11 +392,6 @@ void ESP32BLETracker::set_scanner_state_(ScannerState state) { this->scanner_state_callback_.invoke(state); } #endif -#ifdef ESPHOME_ESP32_BLE_TRACKER_SCANNER_STATE_LISTENER_COUNT - for (auto *listener : this->scanner_state_listeners_) { - listener->on_scanner_state(state); - } -#endif } void ESP32BLETracker::dump_config() { diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index ee1b1429c0..9031d86c97 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -89,16 +89,6 @@ using ble_device_base::client_state_to_string; // Neutral scanner lifecycle re-exported for backward compatibility. using ScannerState = ble_device_base::ScannerState; -/** Listener interface for BLE scanner state changes. - * - * Components can implement this interface to receive scanner state updates - * without the overhead of std::function callbacks. - */ -class BLEScannerStateListener { - public: - virtual void on_scanner_state(ScannerState state) = 0; -}; - /// Base class for BLE GATT clients that connect to remote devices. /// /// State Change Tracking Design: @@ -226,15 +216,6 @@ class ESP32BLETracker final : public Component, void on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) override; #endif -#ifdef ESPHOME_ESP32_BLE_TRACKER_SCANNER_STATE_LISTENER_COUNT - /// Add a listener for scanner state changes. Only compiled when a consumer - /// requested a slot in codegen: register through - /// esp32_ble_tracker.register_scanner_state_listener() in your component's - /// to_code, which requests the slot and emits this call. - void add_scanner_state_listener(BLEScannerStateListener *listener) { - this->scanner_state_listeners_.push_back(listener); - } -#endif ScannerState get_scanner_state() const { return this->scanner_state_; } protected: @@ -300,10 +281,6 @@ class ESP32BLETracker final : public Component, #endif #ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT StaticVector clients_; -#endif -#ifdef ESPHOME_ESP32_BLE_TRACKER_SCANNER_STATE_LISTENER_COUNT - StaticVector - scanner_state_listeners_; #endif // Parsed listeners registered through the neutral BLEHub contract (migrated // sensors); dispatched alongside listeners_. diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 7ddc607c5c..fd351356df 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -307,7 +307,6 @@ #define USE_ESP32_BLE_SERVER_ON_DISCONNECT #define ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT 1 #define ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT 1 -#define ESPHOME_ESP32_BLE_TRACKER_SCANNER_STATE_LISTENER_COUNT 1 #define ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT 1 #define ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT 2 #define ESPHOME_ESP32_BLE_GAP_SCAN_EVENT_HANDLER_COUNT 1 diff --git a/tests/component_tests/ble_device_base/test_slot_counter.py b/tests/component_tests/ble_device_base/test_slot_counter.py index daa2884588..e784c9871e 100644 --- a/tests/component_tests/ble_device_base/test_slot_counter.py +++ b/tests/component_tests/ble_device_base/test_slot_counter.py @@ -94,11 +94,7 @@ def test_esp32_tracker_handler_counts( assert get_define_value("ESPHOME_ESP32_BLE_GATTC_EVENT_HANDLER_COUNT") == "1" assert get_define_value("ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT") == "1" assert get_define_value("ESPHOME_ESP32_BLE_GATTS_EVENT_HANDLER_COUNT") is None - # No consumer subscribed to scanner state, so the storage compiles out. - assert ( - get_define_value("ESPHOME_ESP32_BLE_TRACKER_SCANNER_STATE_LISTENER_COUNT") - is None - ) + # No advertisement listener or client is registered, so both storages compile out. assert get_define_value("ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT") is None assert get_define_value("ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT") is None @@ -109,12 +105,8 @@ def test_esp32_bluetooth_proxy_requests_client_slots_only( ) -> None: """The proxy requests a client slot per connection (three by default with active: true); advertisements and scanner state arrive through the hub - callbacks, so no listener or scanner-state slot exists.""" + callbacks, so no listener slot exists.""" generate_main(component_config_path("esp32_bluetooth_proxy.yaml")) - assert ( - get_define_value("ESPHOME_ESP32_BLE_TRACKER_SCANNER_STATE_LISTENER_COUNT") - is None - ) assert get_define_value("ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT") is None assert get_define_value("ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT") == "3"