diff --git a/docker/Dockerfile b/docker/Dockerfile index 95b6677815..2d9a8a9ae4 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -29,6 +29,8 @@ RUN \ git=1:2.30.2-1+deb11u2 \ curl=7.74.0-1.3+deb11u7 \ openssh-client=1:8.4p1-5+deb11u1 \ + libcairo2=1.16.0-5 \ + python3-cffi=1.14.5-1 \ && rm -rf \ /tmp/* \ /var/{cache,log}/* \ diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 34678fde0f..74d08195a0 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -206,7 +206,8 @@ message DeviceInfoResponse { uint32 webserver_port = 10; - uint32 bluetooth_proxy_version = 11; + uint32 legacy_bluetooth_proxy_version = 11; + uint32 bluetooth_proxy_feature_flags = 15; string manufacturer = 12; @@ -1130,6 +1131,8 @@ message SubscribeBluetoothLEAdvertisementsRequest { option (id) = 66; option (source) = SOURCE_CLIENT; option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint32 flags = 1; } message BluetoothServiceData { @@ -1154,6 +1157,23 @@ message BluetoothLEAdvertisementResponse { uint32 address_type = 7; } +message BluetoothLERawAdvertisement { + uint64 address = 1; + sint32 rssi = 2; + uint32 address_type = 3; + + bytes data = 4; +} + +message BluetoothLERawAdvertisementsResponse { + option (id) = 93; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + option (no_delay) = true; + + repeated BluetoothLERawAdvertisement advertisements = 1; +} + enum BluetoothDeviceRequestType { BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT = 0; BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT = 1; diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 3983c6403b..53e6ccd6dc 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -51,6 +51,14 @@ void APIConnection::start() { helper_->set_log_info(client_info_); } +APIConnection::~APIConnection() { +#ifdef USE_BLUETOOTH_PROXY + if (bluetooth_proxy::global_bluetooth_proxy->get_api_connection() == this) { + bluetooth_proxy::global_bluetooth_proxy->unsubscribe_api_connection(this); + } +#endif +} + void APIConnection::loop() { if (this->remove_) return; @@ -845,9 +853,13 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) { #endif #ifdef USE_BLUETOOTH_PROXY +void APIConnection::subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) { + bluetooth_proxy::global_bluetooth_proxy->subscribe_api_connection(this, msg.flags); +} +void APIConnection::unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) { + bluetooth_proxy::global_bluetooth_proxy->unsubscribe_api_connection(this); +} bool APIConnection::send_bluetooth_le_advertisement(const BluetoothLEAdvertisementResponse &msg) { - if (!this->bluetooth_le_advertisement_subscription_) - return false; if (this->client_api_version_major_ < 1 || this->client_api_version_minor_ < 7) { BluetoothLEAdvertisementResponse resp = msg; for (auto &service : resp.service_data) { @@ -943,7 +955,7 @@ HelloResponse APIConnection::hello(const HelloRequest &msg) { HelloResponse resp; resp.api_version_major = 1; - resp.api_version_minor = 8; + resp.api_version_minor = 9; resp.server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")"; resp.name = App.get_name(); @@ -995,9 +1007,8 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { resp.webserver_port = USE_WEBSERVER_PORT; #endif #ifdef USE_BLUETOOTH_PROXY - resp.bluetooth_proxy_version = bluetooth_proxy::global_bluetooth_proxy->has_active() - ? bluetooth_proxy::ACTIVE_CONNECTIONS_VERSION - : bluetooth_proxy::PASSIVE_ONLY_VERSION; + resp.legacy_bluetooth_proxy_version = bluetooth_proxy::global_bluetooth_proxy->get_legacy_version(); + resp.bluetooth_proxy_feature_flags = bluetooth_proxy::global_bluetooth_proxy->get_feature_flags(); #endif #ifdef USE_VOICE_ASSISTANT resp.voice_assistant_version = voice_assistant::global_voice_assistant->get_version(); diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index d4e9cc656e..5398234f9f 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -16,7 +16,7 @@ namespace api { class APIConnection : public APIServerConnection { public: APIConnection(std::unique_ptr socket, APIServer *parent); - virtual ~APIConnection() = default; + virtual ~APIConnection(); void start(); void loop(); @@ -98,12 +98,8 @@ class APIConnection : public APIServerConnection { this->send_homeassistant_service_response(call); } #ifdef USE_BLUETOOTH_PROXY - void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override { - this->bluetooth_le_advertisement_subscription_ = true; - } - void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override { - this->bluetooth_le_advertisement_subscription_ = false; - } + void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override; + void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override; bool send_bluetooth_le_advertisement(const BluetoothLEAdvertisementResponse &msg); void bluetooth_device_request(const BluetoothDeviceRequest &msg) override; @@ -211,9 +207,6 @@ class APIConnection : public APIServerConnection { uint32_t last_traffic_; bool sent_ping_{false}; bool service_call_subscription_{false}; -#ifdef USE_BLUETOOTH_PROXY - bool bluetooth_le_advertisement_subscription_{false}; -#endif #ifdef USE_VOICE_ASSISTANT bool voice_assistant_subscription_{false}; #endif diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 29e8e207dd..019e653a7f 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -617,7 +617,11 @@ bool DeviceInfoResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { return true; } case 11: { - this->bluetooth_proxy_version = value.as_uint32(); + this->legacy_bluetooth_proxy_version = value.as_uint32(); + return true; + } + case 15: { + this->bluetooth_proxy_feature_flags = value.as_uint32(); return true; } case 14: { @@ -681,7 +685,8 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(8, this->project_name); buffer.encode_string(9, this->project_version); buffer.encode_uint32(10, this->webserver_port); - buffer.encode_uint32(11, this->bluetooth_proxy_version); + buffer.encode_uint32(11, this->legacy_bluetooth_proxy_version); + buffer.encode_uint32(15, this->bluetooth_proxy_feature_flags); buffer.encode_string(12, this->manufacturer); buffer.encode_string(13, this->friendly_name); buffer.encode_uint32(14, this->voice_assistant_version); @@ -731,8 +736,13 @@ void DeviceInfoResponse::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); - out.append(" bluetooth_proxy_version: "); - sprintf(buffer, "%u", this->bluetooth_proxy_version); + out.append(" legacy_bluetooth_proxy_version: "); + sprintf(buffer, "%u", this->legacy_bluetooth_proxy_version); + out.append(buffer); + out.append("\n"); + + out.append(" bluetooth_proxy_feature_flags: "); + sprintf(buffer, "%u", this->bluetooth_proxy_feature_flags); out.append(buffer); out.append("\n"); @@ -5041,10 +5051,28 @@ void MediaPlayerCommandRequest::dump_to(std::string &out) const { out.append("}"); } #endif -void SubscribeBluetoothLEAdvertisementsRequest::encode(ProtoWriteBuffer buffer) const {} +bool SubscribeBluetoothLEAdvertisementsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: { + this->flags = value.as_uint32(); + return true; + } + default: + return false; + } +} +void SubscribeBluetoothLEAdvertisementsRequest::encode(ProtoWriteBuffer buffer) const { + buffer.encode_uint32(1, this->flags); +} #ifdef HAS_PROTO_MESSAGE_DUMP void SubscribeBluetoothLEAdvertisementsRequest::dump_to(std::string &out) const { - out.append("SubscribeBluetoothLEAdvertisementsRequest {}"); + __attribute__((unused)) char buffer[64]; + out.append("SubscribeBluetoothLEAdvertisementsRequest {\n"); + out.append(" flags: "); + sprintf(buffer, "%u", this->flags); + out.append(buffer); + out.append("\n"); + out.append("}"); } #endif bool BluetoothServiceData::decode_varint(uint32_t field_id, ProtoVarInt value) { @@ -5197,6 +5225,92 @@ void BluetoothLEAdvertisementResponse::dump_to(std::string &out) const { out.append("}"); } #endif +bool BluetoothLERawAdvertisement::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: { + this->address = value.as_uint64(); + return true; + } + case 2: { + this->rssi = value.as_sint32(); + return true; + } + case 3: { + this->address_type = value.as_uint32(); + return true; + } + default: + return false; + } +} +bool BluetoothLERawAdvertisement::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 4: { + this->data = value.as_string(); + return true; + } + default: + return false; + } +} +void BluetoothLERawAdvertisement::encode(ProtoWriteBuffer buffer) const { + buffer.encode_uint64(1, this->address); + buffer.encode_sint32(2, this->rssi); + buffer.encode_uint32(3, this->address_type); + buffer.encode_string(4, this->data); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void BluetoothLERawAdvertisement::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothLERawAdvertisement {\n"); + out.append(" address: "); + sprintf(buffer, "%llu", this->address); + out.append(buffer); + out.append("\n"); + + out.append(" rssi: "); + sprintf(buffer, "%d", this->rssi); + out.append(buffer); + out.append("\n"); + + out.append(" address_type: "); + sprintf(buffer, "%u", this->address_type); + out.append(buffer); + out.append("\n"); + + out.append(" data: "); + out.append("'").append(this->data).append("'"); + out.append("\n"); + out.append("}"); +} +#endif +bool BluetoothLERawAdvertisementsResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + this->advertisements.push_back(value.as_message()); + return true; + } + default: + return false; + } +} +void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer buffer) const { + for (auto &it : this->advertisements) { + buffer.encode_message(1, it, true); + } +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void BluetoothLERawAdvertisementsResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothLERawAdvertisementsResponse {\n"); + for (const auto &it : this->advertisements) { + out.append(" advertisements: "); + it.dump_to(out); + out.append("\n"); + } + out.append("}"); +} +#endif bool BluetoothDeviceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index cd1cfb595a..49307dfce0 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -287,7 +287,8 @@ class DeviceInfoResponse : public ProtoMessage { std::string project_name{}; std::string project_version{}; uint32_t webserver_port{0}; - uint32_t bluetooth_proxy_version{0}; + uint32_t legacy_bluetooth_proxy_version{0}; + uint32_t bluetooth_proxy_feature_flags{0}; std::string manufacturer{}; std::string friendly_name{}; uint32_t voice_assistant_version{0}; @@ -1247,12 +1248,14 @@ class MediaPlayerCommandRequest : public ProtoMessage { }; class SubscribeBluetoothLEAdvertisementsRequest : public ProtoMessage { public: + uint32_t flags{0}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif protected: + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class BluetoothServiceData : public ProtoMessage { public: @@ -1286,6 +1289,32 @@ class BluetoothLEAdvertisementResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; +class BluetoothLERawAdvertisement : public ProtoMessage { + public: + uint64_t address{0}; + int32_t rssi{0}; + uint32_t address_type{0}; + std::string data{}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class BluetoothLERawAdvertisementsResponse : public ProtoMessage { + public: + std::vector advertisements{}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; +}; class BluetoothDeviceRequest : public ProtoMessage { public: uint64_t address{0}; diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index df36d0fdea..e14e22cdc1 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -339,6 +339,15 @@ bool APIServerConnectionBase::send_bluetooth_le_advertisement_response(const Blu } #endif #ifdef USE_BLUETOOTH_PROXY +bool APIServerConnectionBase::send_bluetooth_le_raw_advertisements_response( + const BluetoothLERawAdvertisementsResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_bluetooth_le_raw_advertisements_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 93); +} +#endif +#ifdef USE_BLUETOOTH_PROXY #endif #ifdef USE_BLUETOOTH_PROXY bool APIServerConnectionBase::send_bluetooth_device_connection_response(const BluetoothDeviceConnectionResponse &msg) { diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 3808f128a4..8af7a53381 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -161,6 +161,9 @@ class APIServerConnectionBase : public ProtoService { #ifdef USE_BLUETOOTH_PROXY bool send_bluetooth_le_advertisement_response(const BluetoothLEAdvertisementResponse &msg); #endif +#ifdef USE_BLUETOOTH_PROXY + bool send_bluetooth_le_raw_advertisements_response(const BluetoothLERawAdvertisementsResponse &msg); +#endif #ifdef USE_BLUETOOTH_PROXY virtual void on_bluetooth_device_request(const BluetoothDeviceRequest &value){}; #endif diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 3dd47c4dd8..27c82f7ccc 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -291,112 +291,7 @@ void APIServer::send_homeassistant_service_call(const HomeassistantServiceRespon client->send_homeassistant_service_call(call); } } -#ifdef USE_BLUETOOTH_PROXY -void APIServer::send_bluetooth_le_advertisement(const BluetoothLEAdvertisementResponse &call) { - for (auto &client : this->clients_) { - client->send_bluetooth_le_advertisement(call); - } -} -void APIServer::send_bluetooth_device_connection(uint64_t address, bool connected, uint16_t mtu, esp_err_t error) { - BluetoothDeviceConnectionResponse call; - call.address = address; - call.connected = connected; - call.mtu = mtu; - call.error = error; - for (auto &client : this->clients_) { - client->send_bluetooth_device_connection_response(call); - } -} - -void APIServer::send_bluetooth_device_pairing(uint64_t address, bool paired, esp_err_t error) { - BluetoothDevicePairingResponse call; - call.address = address; - call.paired = paired; - call.error = error; - - for (auto &client : this->clients_) { - client->send_bluetooth_device_pairing_response(call); - } -} - -void APIServer::send_bluetooth_device_unpairing(uint64_t address, bool success, esp_err_t error) { - BluetoothDeviceUnpairingResponse call; - call.address = address; - call.success = success; - call.error = error; - - for (auto &client : this->clients_) { - client->send_bluetooth_device_unpairing_response(call); - } -} - -void APIServer::send_bluetooth_device_clear_cache(uint64_t address, bool success, esp_err_t error) { - BluetoothDeviceClearCacheResponse call; - call.address = address; - call.success = success; - call.error = error; - - for (auto &client : this->clients_) { - client->send_bluetooth_device_clear_cache_response(call); - } -} - -void APIServer::send_bluetooth_connections_free(uint8_t free, uint8_t limit) { - BluetoothConnectionsFreeResponse call; - call.free = free; - call.limit = limit; - - for (auto &client : this->clients_) { - client->send_bluetooth_connections_free_response(call); - } -} - -void APIServer::send_bluetooth_gatt_read_response(const BluetoothGATTReadResponse &call) { - for (auto &client : this->clients_) { - client->send_bluetooth_gatt_read_response(call); - } -} -void APIServer::send_bluetooth_gatt_write_response(const BluetoothGATTWriteResponse &call) { - for (auto &client : this->clients_) { - client->send_bluetooth_gatt_write_response(call); - } -} -void APIServer::send_bluetooth_gatt_notify_data_response(const BluetoothGATTNotifyDataResponse &call) { - for (auto &client : this->clients_) { - client->send_bluetooth_gatt_notify_data_response(call); - } -} -void APIServer::send_bluetooth_gatt_notify_response(const BluetoothGATTNotifyResponse &call) { - for (auto &client : this->clients_) { - client->send_bluetooth_gatt_notify_response(call); - } -} -void APIServer::send_bluetooth_gatt_services(const BluetoothGATTGetServicesResponse &call) { - for (auto &client : this->clients_) { - client->send_bluetooth_gatt_get_services_response(call); - } -} -void APIServer::send_bluetooth_gatt_services_done(uint64_t address) { - BluetoothGATTGetServicesDoneResponse call; - call.address = address; - - for (auto &client : this->clients_) { - client->send_bluetooth_gatt_get_services_done_response(call); - } -} -void APIServer::send_bluetooth_gatt_error(uint64_t address, uint16_t handle, esp_err_t error) { - BluetoothGATTErrorResponse call; - call.address = address; - call.handle = handle; - call.error = error; - - for (auto &client : this->clients_) { - client->send_bluetooth_gatt_error_response(call); - } -} - -#endif APIServer::APIServer() { global_api_server = this; } void APIServer::subscribe_home_assistant_state(std::string entity_id, optional attribute, std::function f) { diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 79ba7b17f1..28e53f22e2 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -75,21 +75,6 @@ class APIServer : public Component, public Controller { void on_media_player_update(media_player::MediaPlayer *obj) override; #endif void send_homeassistant_service_call(const HomeassistantServiceResponse &call); -#ifdef USE_BLUETOOTH_PROXY - void send_bluetooth_le_advertisement(const BluetoothLEAdvertisementResponse &call); - void send_bluetooth_device_connection(uint64_t address, bool connected, uint16_t mtu = 0, esp_err_t error = ESP_OK); - void send_bluetooth_device_pairing(uint64_t address, bool paired, esp_err_t error = ESP_OK); - void send_bluetooth_device_unpairing(uint64_t address, bool success, esp_err_t error = ESP_OK); - void send_bluetooth_device_clear_cache(uint64_t address, bool success, esp_err_t error = ESP_OK); - void send_bluetooth_connections_free(uint8_t free, uint8_t limit); - void send_bluetooth_gatt_read_response(const BluetoothGATTReadResponse &call); - void send_bluetooth_gatt_write_response(const BluetoothGATTWriteResponse &call); - void send_bluetooth_gatt_notify_data_response(const BluetoothGATTNotifyDataResponse &call); - void send_bluetooth_gatt_notify_response(const BluetoothGATTNotifyResponse &call); - void send_bluetooth_gatt_services(const BluetoothGATTGetServicesResponse &call); - void send_bluetooth_gatt_services_done(uint64_t address); - void send_bluetooth_gatt_error(uint64_t address, uint16_t handle, esp_err_t error); -#endif void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } #ifdef USE_HOMEASSISTANT_TIME void request_time(); diff --git a/esphome/components/bedjet/bedjet_hub.cpp b/esphome/components/bedjet/bedjet_hub.cpp index fbd2876dc9..c355953d94 100644 --- a/esphome/components/bedjet/bedjet_hub.cpp +++ b/esphome/components/bedjet/bedjet_hub.cpp @@ -442,7 +442,7 @@ uint8_t BedJetHub::write_notify_config_descriptor_(bool enable) { void BedJetHub::send_local_time() { if (this->time_id_.has_value()) { auto *time_id = *this->time_id_; - time::ESPTime now = time_id->now(); + ESPTime now = time_id->now(); if (now.is_valid()) { this->set_clock(now.hour, now.minute); ESP_LOGD(TAG, "Using time component to set BedJet clock: %d:%02d", now.hour, now.minute); diff --git a/esphome/components/bedjet/bedjet_hub.h b/esphome/components/bedjet/bedjet_hub.h index 5809827cfa..bb1349b2ac 100644 --- a/esphome/components/bedjet/bedjet_hub.h +++ b/esphome/components/bedjet/bedjet_hub.h @@ -13,6 +13,7 @@ #ifdef USE_TIME #include "esphome/components/time/real_time_clock.h" +#include "esphome/core/time.h" #endif #include diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp index 9354ab36d6..26304325c1 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp @@ -1,6 +1,6 @@ #include "bluetooth_connection.h" -#include "esphome/components/api/api_server.h" +#include "esphome/components/api/api_pb2.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -20,24 +20,21 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga switch (event) { case ESP_GATTC_DISCONNECT_EVT: { - api::global_api_server->send_bluetooth_device_connection(this->address_, false, 0, param->disconnect.reason); + this->proxy_->send_device_connection(this->address_, false, 0, param->disconnect.reason); this->set_address(0); - api::global_api_server->send_bluetooth_connections_free(this->proxy_->get_bluetooth_connections_free(), - this->proxy_->get_bluetooth_connections_limit()); + this->proxy_->send_connections_free(); break; } case ESP_GATTC_OPEN_EVT: { if (param->open.conn_id != this->conn_id_) break; if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) { - api::global_api_server->send_bluetooth_device_connection(this->address_, false, 0, param->open.status); + this->proxy_->send_device_connection(this->address_, false, 0, param->open.status); this->set_address(0); - api::global_api_server->send_bluetooth_connections_free(this->proxy_->get_bluetooth_connections_free(), - this->proxy_->get_bluetooth_connections_limit()); + this->proxy_->send_connections_free(); } else if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) { - api::global_api_server->send_bluetooth_device_connection(this->address_, true, this->mtu_); - api::global_api_server->send_bluetooth_connections_free(this->proxy_->get_bluetooth_connections_free(), - this->proxy_->get_bluetooth_connections_limit()); + this->proxy_->send_device_connection(this->address_, true, this->mtu_); + this->proxy_->send_connections_free(); } this->seen_mtu_or_services_ = false; break; @@ -52,9 +49,8 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga this->seen_mtu_or_services_ = true; break; } - api::global_api_server->send_bluetooth_device_connection(this->address_, true, this->mtu_); - api::global_api_server->send_bluetooth_connections_free(this->proxy_->get_bluetooth_connections_free(), - this->proxy_->get_bluetooth_connections_limit()); + this->proxy_->send_device_connection(this->address_, true, this->mtu_); + this->proxy_->send_connections_free(); break; } case ESP_GATTC_SEARCH_CMPL_EVT: { @@ -67,9 +63,8 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga this->seen_mtu_or_services_ = true; break; } - api::global_api_server->send_bluetooth_device_connection(this->address_, true, this->mtu_); - api::global_api_server->send_bluetooth_connections_free(this->proxy_->get_bluetooth_connections_free(), - this->proxy_->get_bluetooth_connections_limit()); + this->proxy_->send_device_connection(this->address_, true, this->mtu_); + this->proxy_->send_connections_free(); break; } case ESP_GATTC_READ_DESCR_EVT: @@ -79,7 +74,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga if (param->read.status != ESP_GATT_OK) { ESP_LOGW(TAG, "[%d] [%s] Error reading char/descriptor at handle 0x%2X, status=%d", this->connection_index_, this->address_str_.c_str(), param->read.handle, param->read.status); - api::global_api_server->send_bluetooth_gatt_error(this->address_, param->read.handle, param->read.status); + this->proxy_->send_gatt_error(this->address_, param->read.handle, param->read.status); break; } api::BluetoothGATTReadResponse resp; @@ -89,7 +84,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga for (uint16_t i = 0; i < param->read.value_len; i++) { resp.data.push_back(param->read.value[i]); } - api::global_api_server->send_bluetooth_gatt_read_response(resp); + this->proxy_->get_api_connection()->send_bluetooth_gatt_read_response(resp); break; } case ESP_GATTC_WRITE_CHAR_EVT: @@ -99,13 +94,13 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga if (param->write.status != ESP_GATT_OK) { ESP_LOGW(TAG, "[%d] [%s] Error writing char/descriptor at handle 0x%2X, status=%d", this->connection_index_, this->address_str_.c_str(), param->write.handle, param->write.status); - api::global_api_server->send_bluetooth_gatt_error(this->address_, param->write.handle, param->write.status); + this->proxy_->send_gatt_error(this->address_, param->write.handle, param->write.status); break; } api::BluetoothGATTWriteResponse resp; resp.address = this->address_; resp.handle = param->write.handle; - api::global_api_server->send_bluetooth_gatt_write_response(resp); + this->proxy_->get_api_connection()->send_bluetooth_gatt_write_response(resp); break; } case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: { @@ -113,28 +108,26 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga ESP_LOGW(TAG, "[%d] [%s] Error unregistering notifications for handle 0x%2X, status=%d", this->connection_index_, this->address_str_.c_str(), param->unreg_for_notify.handle, param->unreg_for_notify.status); - api::global_api_server->send_bluetooth_gatt_error(this->address_, param->unreg_for_notify.handle, - param->unreg_for_notify.status); + this->proxy_->send_gatt_error(this->address_, param->unreg_for_notify.handle, param->unreg_for_notify.status); break; } api::BluetoothGATTNotifyResponse resp; resp.address = this->address_; resp.handle = param->unreg_for_notify.handle; - api::global_api_server->send_bluetooth_gatt_notify_response(resp); + this->proxy_->get_api_connection()->send_bluetooth_gatt_notify_response(resp); break; } case ESP_GATTC_REG_FOR_NOTIFY_EVT: { if (param->reg_for_notify.status != ESP_GATT_OK) { ESP_LOGW(TAG, "[%d] [%s] Error registering notifications for handle 0x%2X, status=%d", this->connection_index_, this->address_str_.c_str(), param->reg_for_notify.handle, param->reg_for_notify.status); - api::global_api_server->send_bluetooth_gatt_error(this->address_, param->reg_for_notify.handle, - param->reg_for_notify.status); + this->proxy_->send_gatt_error(this->address_, param->reg_for_notify.handle, param->reg_for_notify.status); break; } api::BluetoothGATTNotifyResponse resp; resp.address = this->address_; resp.handle = param->reg_for_notify.handle; - api::global_api_server->send_bluetooth_gatt_notify_response(resp); + this->proxy_->get_api_connection()->send_bluetooth_gatt_notify_response(resp); break; } case ESP_GATTC_NOTIFY_EVT: { @@ -149,7 +142,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga for (uint16_t i = 0; i < param->notify.value_len; i++) { resp.data.push_back(param->notify.value[i]); } - api::global_api_server->send_bluetooth_gatt_notify_data_response(resp); + this->proxy_->get_api_connection()->send_bluetooth_gatt_notify_data_response(resp); break; } default: @@ -166,10 +159,9 @@ void BluetoothConnection::gap_event_handler(esp_gap_ble_cb_event_t event, esp_bl if (memcmp(param->ble_security.auth_cmpl.bd_addr, this->remote_bda_, 6) != 0) break; if (param->ble_security.auth_cmpl.success) { - api::global_api_server->send_bluetooth_device_pairing(this->address_, true); + this->proxy_->send_device_pairing(this->address_, true); } else { - api::global_api_server->send_bluetooth_device_pairing(this->address_, false, - param->ble_security.auth_cmpl.fail_reason); + this->proxy_->send_device_pairing(this->address_, false, param->ble_security.auth_cmpl.fail_reason); } break; default: diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index 76950c944e..b633fe2430 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -1,11 +1,10 @@ #include "bluetooth_proxy.h" #include "esphome/core/log.h" +#include "esphome/core/macros.h" #ifdef USE_ESP32 -#include "esphome/components/api/api_server.h" - namespace esphome { namespace bluetooth_proxy { @@ -27,15 +26,39 @@ std::vector get_128bit_uuid_vec(esp_bt_uuid_t uuid_source) { BluetoothProxy::BluetoothProxy() { global_bluetooth_proxy = this; } bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { - if (!api::global_api_server->is_connected()) + if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr || this->raw_advertisements_) return false; + ESP_LOGV(TAG, "Proxying packet from %s - %s. RSSI: %d dB", device.get_name().c_str(), device.address_str().c_str(), device.get_rssi()); this->send_api_packet_(device); - return true; } +bool BluetoothProxy::parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) { + if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr || !this->raw_advertisements_) + return false; + + api::BluetoothLERawAdvertisementsResponse resp; + for (size_t i = 0; i < count; i++) { + auto &result = advertisements[i]; + api::BluetoothLERawAdvertisement adv; + adv.address = esp32_ble::ble_addr_to_uint64(result.bda); + adv.rssi = result.rssi; + adv.address_type = result.ble_addr_type; + + uint8_t length = result.adv_data_len + result.scan_rsp_len; + adv.data.reserve(length); + for (uint16_t i = 0; i < length; i++) { + adv.data.push_back(result.ble_adv[i]); + } + + resp.advertisements.push_back(std::move(adv)); + } + ESP_LOGV(TAG, "Proxying %d packets", count); + this->api_connection_->send_bluetooth_le_raw_advertisements_response(resp); + return true; +} void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device) { api::BluetoothLEAdvertisementResponse resp; resp.address = device.address_uint64(); @@ -58,7 +81,7 @@ void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &devi manufacturer_data.data.assign(data.data.begin(), data.data.end()); resp.manufacturer_data.push_back(std::move(manufacturer_data)); } - api::global_api_server->send_bluetooth_le_advertisement(resp); + this->api_connection_->send_bluetooth_le_advertisement(resp); } void BluetoothProxy::dump_config() { @@ -81,7 +104,7 @@ int BluetoothProxy::get_bluetooth_connections_free() { } void BluetoothProxy::loop() { - if (!api::global_api_server->is_connected()) { + if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr) { for (auto *connection : this->connections_) { if (connection->get_address() != 0) { connection->disconnect(); @@ -92,7 +115,7 @@ void BluetoothProxy::loop() { for (auto *connection : this->connections_) { if (connection->send_service_ == connection->service_count_) { connection->send_service_ = DONE_SENDING_SERVICES; - api::global_api_server->send_bluetooth_gatt_services_done(connection->get_address()); + this->send_gatt_services_done(connection->get_address()); if (connection->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE || connection->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) { connection->release_services(); @@ -170,7 +193,7 @@ void BluetoothProxy::loop() { service_resp.characteristics.push_back(std::move(characteristic_resp)); } resp.services.push_back(std::move(service_resp)); - api::global_api_server->send_bluetooth_gatt_services(resp); + this->api_connection_->send_bluetooth_gatt_get_services_response(resp); } } } @@ -208,16 +231,15 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest auto *connection = this->get_connection_(msg.address, true); if (connection == nullptr) { ESP_LOGW(TAG, "No free connections available"); - api::global_api_server->send_bluetooth_device_connection(msg.address, false); + this->send_device_connection(msg.address, false); return; } if (connection->state() == espbt::ClientState::CONNECTED || connection->state() == espbt::ClientState::ESTABLISHED) { ESP_LOGW(TAG, "[%d] [%s] Connection already established", connection->get_connection_index(), connection->address_str().c_str()); - api::global_api_server->send_bluetooth_device_connection(msg.address, true); - api::global_api_server->send_bluetooth_connections_free(this->get_bluetooth_connections_free(), - this->get_bluetooth_connections_limit()); + this->send_device_connection(msg.address, true); + this->send_connections_free(); return; } else if (connection->state() == espbt::ClientState::SEARCHING) { ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, already searching for device", @@ -263,25 +285,22 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest } else { connection->set_state(espbt::ClientState::SEARCHING); } - api::global_api_server->send_bluetooth_connections_free(this->get_bluetooth_connections_free(), - this->get_bluetooth_connections_limit()); + this->send_connections_free(); break; } case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT: { auto *connection = this->get_connection_(msg.address, false); if (connection == nullptr) { - api::global_api_server->send_bluetooth_device_connection(msg.address, false); - api::global_api_server->send_bluetooth_connections_free(this->get_bluetooth_connections_free(), - this->get_bluetooth_connections_limit()); + this->send_device_connection(msg.address, false); + this->send_connections_free(); return; } if (connection->state() != espbt::ClientState::IDLE) { connection->disconnect(); } else { connection->set_address(0); - api::global_api_server->send_bluetooth_device_connection(msg.address, false); - api::global_api_server->send_bluetooth_connections_free(this->get_bluetooth_connections_free(), - this->get_bluetooth_connections_limit()); + this->send_device_connection(msg.address, false); + this->send_connections_free(); } break; } @@ -291,10 +310,10 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest if (!connection->is_paired()) { auto err = connection->pair(); if (err != ESP_OK) { - api::global_api_server->send_bluetooth_device_pairing(msg.address, false, err); + this->send_device_pairing(msg.address, false, err); } } else { - api::global_api_server->send_bluetooth_device_pairing(msg.address, true); + this->send_device_pairing(msg.address, true); } } break; @@ -303,14 +322,20 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest esp_bd_addr_t address; uint64_to_bd_addr(msg.address, address); esp_err_t ret = esp_ble_remove_bond_device(address); - api::global_api_server->send_bluetooth_device_unpairing(msg.address, ret == ESP_OK, ret); + this->send_device_pairing(msg.address, ret == ESP_OK, ret); break; } case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CLEAR_CACHE: { esp_bd_addr_t address; uint64_to_bd_addr(msg.address, address); esp_err_t ret = esp_ble_gattc_cache_clean(address); - api::global_api_server->send_bluetooth_device_clear_cache(msg.address, ret == ESP_OK, ret); + api::BluetoothDeviceClearCacheResponse call; + call.address = msg.address; + call.success = ret == ESP_OK; + call.error = ret; + + this->api_connection_->send_bluetooth_device_clear_cache_response(call); + break; } } @@ -320,13 +345,13 @@ void BluetoothProxy::bluetooth_gatt_read(const api::BluetoothGATTReadRequest &ms auto *connection = this->get_connection_(msg.address, false); if (connection == nullptr) { ESP_LOGW(TAG, "Cannot read GATT characteristic, not connected"); - api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); + this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); return; } auto err = connection->read_characteristic(msg.handle); if (err != ESP_OK) { - api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, err); + this->send_gatt_error(msg.address, msg.handle, err); } } @@ -334,13 +359,13 @@ void BluetoothProxy::bluetooth_gatt_write(const api::BluetoothGATTWriteRequest & auto *connection = this->get_connection_(msg.address, false); if (connection == nullptr) { ESP_LOGW(TAG, "Cannot write GATT characteristic, not connected"); - api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); + this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); return; } auto err = connection->write_characteristic(msg.handle, msg.data, msg.response); if (err != ESP_OK) { - api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, err); + this->send_gatt_error(msg.address, msg.handle, err); } } @@ -348,13 +373,13 @@ void BluetoothProxy::bluetooth_gatt_read_descriptor(const api::BluetoothGATTRead auto *connection = this->get_connection_(msg.address, false); if (connection == nullptr) { ESP_LOGW(TAG, "Cannot read GATT descriptor, not connected"); - api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); + this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); return; } auto err = connection->read_descriptor(msg.handle); if (err != ESP_OK) { - api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, err); + this->send_gatt_error(msg.address, msg.handle, err); } } @@ -362,13 +387,13 @@ void BluetoothProxy::bluetooth_gatt_write_descriptor(const api::BluetoothGATTWri auto *connection = this->get_connection_(msg.address, false); if (connection == nullptr) { ESP_LOGW(TAG, "Cannot write GATT descriptor, not connected"); - api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); + this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); return; } auto err = connection->write_descriptor(msg.handle, msg.data, true); if (err != ESP_OK) { - api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, err); + this->send_gatt_error(msg.address, msg.handle, err); } } @@ -376,12 +401,12 @@ void BluetoothProxy::bluetooth_gatt_send_services(const api::BluetoothGATTGetSer auto *connection = this->get_connection_(msg.address, false); if (connection == nullptr || !connection->connected()) { ESP_LOGW(TAG, "Cannot get GATT services, not connected"); - api::global_api_server->send_bluetooth_gatt_error(msg.address, 0, ESP_GATT_NOT_CONNECTED); + this->send_gatt_error(msg.address, 0, ESP_GATT_NOT_CONNECTED); return; } if (!connection->service_count_) { ESP_LOGW(TAG, "[%d] [%s] No GATT services found", connection->connection_index_, connection->address_str().c_str()); - api::global_api_server->send_bluetooth_gatt_services_done(msg.address); + this->send_gatt_services_done(msg.address); return; } if (connection->send_service_ == @@ -393,16 +418,89 @@ void BluetoothProxy::bluetooth_gatt_notify(const api::BluetoothGATTNotifyRequest auto *connection = this->get_connection_(msg.address, false); if (connection == nullptr) { ESP_LOGW(TAG, "Cannot notify GATT characteristic, not connected"); - api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); + this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); return; } auto err = connection->notify_characteristic(msg.handle, msg.enable); if (err != ESP_OK) { - api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, err); + this->send_gatt_error(msg.address, msg.handle, err); } } +void BluetoothProxy::subscribe_api_connection(api::APIConnection *api_connection, uint32_t flags) { + if (this->api_connection_ != nullptr) { + ESP_LOGE(TAG, "Only one API subscription is allowed at a time"); + return; + } + this->api_connection_ = api_connection; + this->raw_advertisements_ = flags & BluetoothProxySubscriptionFlag::SUBSCRIPTION_RAW_ADVERTISEMENTS; +} + +void BluetoothProxy::unsubscribe_api_connection(api::APIConnection *api_connection) { + if (this->api_connection_ != api_connection) { + ESP_LOGV(TAG, "API connection is not subscribed"); + return; + } + this->api_connection_ = nullptr; + this->raw_advertisements_ = false; +} + +void BluetoothProxy::send_device_connection(uint64_t address, bool connected, uint16_t mtu, esp_err_t error) { + if (this->api_connection_ == nullptr) + return; + api::BluetoothDeviceConnectionResponse call; + call.address = address; + call.connected = connected; + call.mtu = mtu; + call.error = error; + this->api_connection_->send_bluetooth_device_connection_response(call); +} +void BluetoothProxy::send_connections_free() { + if (this->api_connection_ == nullptr) + return; + api::BluetoothConnectionsFreeResponse call; + call.free = this->get_bluetooth_connections_free(); + call.limit = this->get_bluetooth_connections_limit(); + this->api_connection_->send_bluetooth_connections_free_response(call); +} + +void BluetoothProxy::send_gatt_services_done(uint64_t address) { + if (this->api_connection_ == nullptr) + return; + api::BluetoothGATTGetServicesDoneResponse call; + call.address = address; + this->api_connection_->send_bluetooth_gatt_get_services_done_response(call); +} + +void BluetoothProxy::send_gatt_error(uint64_t address, uint16_t handle, esp_err_t error) { + if (this->api_connection_ == nullptr) + return; + api::BluetoothGATTErrorResponse call; + call.address = address; + call.handle = handle; + call.error = error; + this->api_connection_->send_bluetooth_gatt_error_response(call); +} + +void BluetoothProxy::send_device_pairing(uint64_t address, bool paired, esp_err_t error) { + api::BluetoothDevicePairingResponse call; + call.address = address; + call.paired = paired; + call.error = error; + + this->api_connection_->send_bluetooth_device_pairing_response(call); +} + +void BluetoothProxy::send_device_unpairing(uint64_t address, bool success, esp_err_t error) { + api::BluetoothDeviceUnpairingResponse call; + call.address = address; + call.success = success; + call.error = error; + + this->api_connection_->send_bluetooth_device_unpairing_response(call); +} + BluetoothProxy *global_bluetooth_proxy = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) } // namespace bluetooth_proxy diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index a582abc8a3..97b6396b55 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -5,6 +5,7 @@ #include #include +#include "esphome/components/api/api_connection.h" #include "esphome/components/api/api_pb2.h" #include "esphome/components/esp32_ble_client/ble_client_base.h" #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" @@ -21,10 +22,33 @@ static const esp_err_t ESP_GATT_NOT_CONNECTED = -1; using namespace esp32_ble_client; +// Legacy versions: +// Version 1: Initial version without active connections +// Version 2: Support for active connections +// Version 3: New connection API +// Version 4: Pairing support +// Version 5: Cache clear support +static const uint32_t LEGACY_ACTIVE_CONNECTIONS_VERSION = 5; +static const uint32_t LEGACY_PASSIVE_ONLY_VERSION = 1; + +enum BluetoothProxyFeature : uint32_t { + FEATURE_PASSIVE_SCAN = 1 << 0, + FEATURE_ACTIVE_CONNECTIONS = 1 << 1, + FEATURE_REMOTE_CACHING = 1 << 2, + FEATURE_PAIRING = 1 << 3, + FEATURE_CACHE_CLEARING = 1 << 4, + FEATURE_RAW_ADVERTISEMENTS = 1 << 5, +}; + +enum BluetoothProxySubscriptionFlag : uint32_t { + SUBSCRIPTION_RAW_ADVERTISEMENTS = 1 << 0, +}; + class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Component { public: BluetoothProxy(); bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + bool parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) override; void dump_config() override; void loop() override; @@ -44,6 +68,18 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com int get_bluetooth_connections_free(); int get_bluetooth_connections_limit() { return this->connections_.size(); } + void subscribe_api_connection(api::APIConnection *api_connection, uint32_t flags); + void unsubscribe_api_connection(api::APIConnection *api_connection); + api::APIConnection *get_api_connection() { return this->api_connection_; } + + void send_device_connection(uint64_t address, bool connected, uint16_t mtu = 0, esp_err_t error = ESP_OK); + void send_connections_free(); + void send_gatt_services_done(uint64_t address); + void send_gatt_error(uint64_t address, uint16_t handle, esp_err_t error); + void send_device_pairing(uint64_t address, bool paired, esp_err_t error = ESP_OK); + void send_device_unpairing(uint64_t address, bool success, esp_err_t error = ESP_OK); + void send_device_clear_cache(uint64_t address, bool success, esp_err_t error = ESP_OK); + static void uint64_to_bd_addr(uint64_t address, esp_bd_addr_t bd_addr) { bd_addr[0] = (address >> 40) & 0xff; bd_addr[1] = (address >> 32) & 0xff; @@ -56,6 +92,27 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com void set_active(bool active) { this->active_ = active; } bool has_active() { return this->active_; } + uint32_t get_legacy_version() const { + if (this->active_) { + return LEGACY_ACTIVE_CONNECTIONS_VERSION; + } + return LEGACY_PASSIVE_ONLY_VERSION; + } + + uint32_t get_feature_flags() const { + uint32_t flags = 0; + flags |= BluetoothProxyFeature::FEATURE_PASSIVE_SCAN; + flags |= BluetoothProxyFeature::FEATURE_RAW_ADVERTISEMENTS; + if (this->active_) { + flags |= BluetoothProxyFeature::FEATURE_ACTIVE_CONNECTIONS; + flags |= BluetoothProxyFeature::FEATURE_REMOTE_CACHING; + flags |= BluetoothProxyFeature::FEATURE_PAIRING; + flags |= BluetoothProxyFeature::FEATURE_CACHE_CLEARING; + } + + return flags; + } + protected: void send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device); @@ -64,18 +121,12 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com bool active_; std::vector connections_{}; + api::APIConnection *api_connection_{nullptr}; + bool raw_advertisements_{false}; }; extern BluetoothProxy *global_bluetooth_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -// Version 1: Initial version without active connections -// Version 2: Support for active connections -// Version 3: New connection API -// Version 4: Pairing support -// Version 5: Cache clear support -static const uint32_t ACTIVE_CONNECTIONS_VERSION = 5; -static const uint32_t PASSIVE_ONLY_VERSION = 1; - } // namespace bluetooth_proxy } // namespace esphome diff --git a/esphome/components/deep_sleep/deep_sleep_component.h b/esphome/components/deep_sleep/deep_sleep_component.h index 8dc87cece8..2e54e53c56 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.h +++ b/esphome/components/deep_sleep/deep_sleep_component.h @@ -1,9 +1,9 @@ #pragma once -#include "esphome/core/component.h" -#include "esphome/core/helpers.h" #include "esphome/core/automation.h" +#include "esphome/core/component.h" #include "esphome/core/hal.h" +#include "esphome/core/helpers.h" #ifdef USE_ESP32 #include @@ -11,6 +11,7 @@ #ifdef USE_TIME #include "esphome/components/time/real_time_clock.h" +#include "esphome/core/time.h" #endif namespace esphome { @@ -170,7 +171,7 @@ template class EnterDeepSleepAction : public Action { if (after_time) timestamp += 60 * 60 * 24; - int32_t offset = time::ESPTime::timezone_offset(); + int32_t offset = ESPTime::timezone_offset(); timestamp -= offset; // Change timestamp to utc const uint32_t ms_left = (timestamp - timestamp_now) * 1000; this->deep_sleep_->set_sleep_duration(ms_left); diff --git a/esphome/components/display/display_buffer.cpp b/esphome/components/display/display_buffer.cpp index 0d76fa09ec..1be2950d68 100644 --- a/esphome/components/display/display_buffer.cpp +++ b/esphome/components/display/display_buffer.cpp @@ -3,9 +3,9 @@ #include #include "esphome/core/application.h" #include "esphome/core/color.h" -#include "esphome/core/log.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" +#include "esphome/core/log.h" namespace esphome { namespace display { @@ -490,24 +490,21 @@ void DisplayOnPageChangeTrigger::process(DisplayPage *from, DisplayPage *to) { if ((this->from_ == nullptr || this->from_ == from) && (this->to_ == nullptr || this->to_ == to)) this->trigger(from, to); } -#ifdef USE_TIME -void DisplayBuffer::strftime(int x, int y, Font *font, Color color, TextAlign align, const char *format, - time::ESPTime time) { +void DisplayBuffer::strftime(int x, int y, Font *font, Color color, TextAlign align, const char *format, ESPTime time) { char buffer[64]; size_t ret = time.strftime(buffer, sizeof(buffer), format); if (ret > 0) this->print(x, y, font, color, align, buffer); } -void DisplayBuffer::strftime(int x, int y, Font *font, Color color, const char *format, time::ESPTime time) { +void DisplayBuffer::strftime(int x, int y, Font *font, Color color, const char *format, ESPTime time) { this->strftime(x, y, font, color, TextAlign::TOP_LEFT, format, time); } -void DisplayBuffer::strftime(int x, int y, Font *font, TextAlign align, const char *format, time::ESPTime time) { +void DisplayBuffer::strftime(int x, int y, Font *font, TextAlign align, const char *format, ESPTime time) { this->strftime(x, y, font, COLOR_ON, align, format, time); } -void DisplayBuffer::strftime(int x, int y, Font *font, const char *format, time::ESPTime time) { +void DisplayBuffer::strftime(int x, int y, Font *font, const char *format, ESPTime time) { this->strftime(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, format, time); } -#endif void DisplayBuffer::start_clipping(Rect rect) { if (!this->clipping_rectangle_.empty()) { diff --git a/esphome/components/display/display_buffer.h b/esphome/components/display/display_buffer.h index 2474d6f5a0..bcfe75f6b4 100644 --- a/esphome/components/display/display_buffer.h +++ b/esphome/components/display/display_buffer.h @@ -1,15 +1,12 @@ #pragma once -#include "esphome/core/component.h" -#include "esphome/core/defines.h" -#include "esphome/core/automation.h" -#include "display_color_utils.h" #include #include - -#ifdef USE_TIME -#include "esphome/components/time/real_time_clock.h" -#endif +#include "display_color_utils.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/core/time.h" #ifdef USE_GRAPH #include "esphome/components/graph/graph.h" @@ -263,7 +260,6 @@ class DisplayBuffer { */ void printf(int x, int y, Font *font, const char *format, ...) __attribute__((format(printf, 5, 6))); -#ifdef USE_TIME /** Evaluate the strftime-format `format` and print the result with the anchor point at [x,y] with `font`. * * @param x The x coordinate of the text alignment anchor point. @@ -274,7 +270,7 @@ class DisplayBuffer { * @param format The strftime format to use. * @param time The time to format. */ - void strftime(int x, int y, Font *font, Color color, TextAlign align, const char *format, time::ESPTime time) + void strftime(int x, int y, Font *font, Color color, TextAlign align, const char *format, ESPTime time) __attribute__((format(strftime, 7, 0))); /** Evaluate the strftime-format `format` and print the result with the top left at [x,y] with `font`. @@ -286,7 +282,7 @@ class DisplayBuffer { * @param format The strftime format to use. * @param time The time to format. */ - void strftime(int x, int y, Font *font, Color color, const char *format, time::ESPTime time) + void strftime(int x, int y, Font *font, Color color, const char *format, ESPTime time) __attribute__((format(strftime, 6, 0))); /** Evaluate the strftime-format `format` and print the result with the anchor point at [x,y] with `font`. @@ -298,7 +294,7 @@ class DisplayBuffer { * @param format The strftime format to use. * @param time The time to format. */ - void strftime(int x, int y, Font *font, TextAlign align, const char *format, time::ESPTime time) + void strftime(int x, int y, Font *font, TextAlign align, const char *format, ESPTime time) __attribute__((format(strftime, 6, 0))); /** Evaluate the strftime-format `format` and print the result with the top left at [x,y] with `font`. @@ -309,9 +305,7 @@ class DisplayBuffer { * @param format The strftime format to use. * @param time The time to format. */ - void strftime(int x, int y, Font *font, const char *format, time::ESPTime time) - __attribute__((format(strftime, 5, 0))); -#endif + void strftime(int x, int y, Font *font, const char *format, ESPTime time) __attribute__((format(strftime, 5, 0))); /** Draw the `image` with the top-left corner at [x,y] to the screen. * diff --git a/esphome/components/ds1307/ds1307.cpp b/esphome/components/ds1307/ds1307.cpp index d249e9743a..472ccc7a9a 100644 --- a/esphome/components/ds1307/ds1307.cpp +++ b/esphome/components/ds1307/ds1307.cpp @@ -37,14 +37,14 @@ void DS1307Component::read_time() { ESP_LOGW(TAG, "RTC halted, not syncing to system clock."); return; } - time::ESPTime rtc_time{.second = uint8_t(ds1307_.reg.second + 10 * ds1307_.reg.second_10), - .minute = uint8_t(ds1307_.reg.minute + 10u * ds1307_.reg.minute_10), - .hour = uint8_t(ds1307_.reg.hour + 10u * ds1307_.reg.hour_10), - .day_of_week = uint8_t(ds1307_.reg.weekday), - .day_of_month = uint8_t(ds1307_.reg.day + 10u * ds1307_.reg.day_10), - .day_of_year = 1, // ignored by recalc_timestamp_utc(false) - .month = uint8_t(ds1307_.reg.month + 10u * ds1307_.reg.month_10), - .year = uint16_t(ds1307_.reg.year + 10u * ds1307_.reg.year_10 + 2000)}; + ESPTime rtc_time{.second = uint8_t(ds1307_.reg.second + 10 * ds1307_.reg.second_10), + .minute = uint8_t(ds1307_.reg.minute + 10u * ds1307_.reg.minute_10), + .hour = uint8_t(ds1307_.reg.hour + 10u * ds1307_.reg.hour_10), + .day_of_week = uint8_t(ds1307_.reg.weekday), + .day_of_month = uint8_t(ds1307_.reg.day + 10u * ds1307_.reg.day_10), + .day_of_year = 1, // ignored by recalc_timestamp_utc(false) + .month = uint8_t(ds1307_.reg.month + 10u * ds1307_.reg.month_10), + .year = uint16_t(ds1307_.reg.year + 10u * ds1307_.reg.year_10 + 2000)}; rtc_time.recalc_timestamp_utc(false); if (!rtc_time.is_valid()) { ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock."); diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 21ec005e07..6c9124447a 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -244,6 +244,17 @@ void ESP32BLE::dump_config() { } } +uint64_t ble_addr_to_uint64(const esp_bd_addr_t address) { + uint64_t u = 0; + u |= uint64_t(address[0] & 0xFF) << 40; + u |= uint64_t(address[1] & 0xFF) << 32; + u |= uint64_t(address[2] & 0xFF) << 24; + u |= uint64_t(address[3] & 0xFF) << 16; + u |= uint64_t(address[4] & 0xFF) << 8; + u |= uint64_t(address[5] & 0xFF) << 0; + return u; +} + ESP32BLE *global_ble = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) } // namespace esp32_ble diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 11ae826544..cde17da425 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -18,6 +18,8 @@ namespace esphome { namespace esp32_ble { +uint64_t ble_addr_to_uint64(const esp_bd_addr_t address); + // NOLINTNEXTLINE(modernize-use-using) typedef struct { void *peer_device; diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 23f109b5ac..30589f1a3f 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -167,7 +167,7 @@ CONFIG_SCHEMA = cv.Schema( cv.Optional(CONF_ON_BLE_ADVERTISE): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESPBTAdvertiseTrigger), - cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_MAC_ADDRESS): cv.ensure_list(cv.mac_address), } ), cv.Optional(CONF_ON_BLE_SERVICE_DATA_ADVERTISE): automation.validate_automation( @@ -223,7 +223,10 @@ async def to_code(config): for conf in config.get(CONF_ON_BLE_ADVERTISE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) if CONF_MAC_ADDRESS in conf: - cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex)) + addr_list = [] + for it in conf[CONF_MAC_ADDRESS]: + addr_list.append(it.as_hex) + cg.add(trigger.set_addresses(addr_list)) await automation.build_automation(trigger, [(ESPBTDeviceConstRef, "x")], conf) for conf in config.get(CONF_ON_BLE_SERVICE_DATA_ADVERTISE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) diff --git a/esphome/components/esp32_ble_tracker/automation.h b/esphome/components/esp32_ble_tracker/automation.h index 6131d6ddf7..6bef9edcb3 100644 --- a/esphome/components/esp32_ble_tracker/automation.h +++ b/esphome/components/esp32_ble_tracker/automation.h @@ -10,18 +10,22 @@ namespace esp32_ble_tracker { class ESPBTAdvertiseTrigger : public Trigger, public ESPBTDeviceListener { public: explicit ESPBTAdvertiseTrigger(ESP32BLETracker *parent) { parent->register_listener(this); } - void set_address(uint64_t address) { this->address_ = address; } + void set_addresses(const std::vector &addresses) { this->address_vec_ = addresses; } bool parse_device(const ESPBTDevice &device) override { - if (this->address_ && device.address_uint64() != this->address_) { - return false; + uint64_t u64_addr = device.address_uint64(); + if (!address_vec_.empty()) { + if (std::find(address_vec_.begin(), address_vec_.end(), u64_addr) == address_vec_.end()) { + return false; + } } + this->trigger(device); return true; } protected: - uint64_t address_ = 0; + std::vector address_vec_; }; class BLEServiceDataAdvertiseTrigger : public Trigger, public ESPBTDeviceListener { diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 26687ba9cc..0f6c4117d2 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -34,17 +34,6 @@ static const char *const TAG = "esp32_ble_tracker"; ESP32BLETracker *global_esp32_ble_tracker = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -uint64_t ble_addr_to_uint64(const esp_bd_addr_t address) { - uint64_t u = 0; - u |= uint64_t(address[0] & 0xFF) << 40; - u |= uint64_t(address[1] & 0xFF) << 32; - u |= uint64_t(address[2] & 0xFF) << 24; - u |= uint64_t(address[3] & 0xFF) << 16; - u |= uint64_t(address[4] & 0xFF) << 8; - u |= uint64_t(address[5] & 0xFF) << 0; - return u; -} - float ESP32BLETracker::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; } void ESP32BLETracker::setup() { @@ -114,10 +103,20 @@ void ESP32BLETracker::loop() { if (this->scan_result_index_ && // if it looks like we have a scan result we will take the lock xSemaphoreTake(this->scan_result_lock_, 5L / portTICK_PERIOD_MS)) { uint32_t index = this->scan_result_index_; - if (index) { - if (index >= ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE) { - ESP_LOGW(TAG, "Too many BLE events to process. Some devices may not show up."); - } + if (index >= ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE) { + ESP_LOGW(TAG, "Too many BLE events to process. Some devices may not show up."); + } + + bool bulk_parsed = false; + + for (auto *listener : this->listeners_) { + bulk_parsed |= listener->parse_devices(this->scan_result_buffer_, this->scan_result_index_); + } + for (auto *client : this->clients_) { + bulk_parsed |= client->parse_devices(this->scan_result_buffer_, this->scan_result_index_); + } + + if (!bulk_parsed) { for (size_t i = 0; i < index; i++) { ESPBTDevice device; device.parse_scan_rst(this->scan_result_buffer_[i]); @@ -141,8 +140,8 @@ void ESP32BLETracker::loop() { this->print_bt_device_info(device); } } - this->scan_result_index_ = 0; } + this->scan_result_index_ = 0; xSemaphoreGive(this->scan_result_lock_); } @@ -585,7 +584,7 @@ std::string ESPBTDevice::address_str() const { this->address_[3], this->address_[4], this->address_[5]); return mac; } -uint64_t ESPBTDevice::address_uint64() const { return ble_addr_to_uint64(this->address_); } +uint64_t ESPBTDevice::address_uint64() const { return esp32_ble::ble_addr_to_uint64(this->address_); } void ESP32BLETracker::dump_config() { ESP_LOGCONFIG(TAG, "BLE Tracker:"); diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 798f68f2bd..43e88fbf2b 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -113,6 +113,9 @@ class ESPBTDeviceListener { public: virtual void on_scan_end() {} virtual bool parse_device(const ESPBTDevice &device) = 0; + virtual bool parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) { + return false; + }; void set_parent(ESP32BLETracker *parent) { parent_ = parent; } protected: diff --git a/esphome/components/gps/time/gps_time.cpp b/esphome/components/gps/time/gps_time.cpp index e46f24ba8e..0f1b989f77 100644 --- a/esphome/components/gps/time/gps_time.cpp +++ b/esphome/components/gps/time/gps_time.cpp @@ -16,7 +16,7 @@ void GPSTime::from_tiny_gps_(TinyGPSPlus &tiny_gps) { if (tiny_gps.date.year() < 2019) return; - time::ESPTime val{}; + ESPTime val{}; val.year = tiny_gps.date.year(); val.month = tiny_gps.date.month(); val.day_of_month = tiny_gps.date.day(); diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index 4260636fa9..113c5a2df1 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -1,5 +1,10 @@ import logging +import io +from pathlib import Path +import re +import requests + from esphome import core from esphome.components import display, font import esphome.config_validation as cv @@ -7,15 +12,19 @@ import esphome.codegen as cg from esphome.const import ( CONF_DITHER, CONF_FILE, + CONF_ICON, CONF_ID, + CONF_PATH, CONF_RAW_DATA_ID, CONF_RESIZE, + CONF_SOURCE, CONF_TYPE, ) from esphome.core import CORE, HexInt _LOGGER = logging.getLogger(__name__) +DOMAIN = "image" DEPENDENCIES = ["display"] MULTI_CONF = True @@ -31,9 +40,58 @@ IMAGE_TYPE = { CONF_USE_TRANSPARENCY = "use_transparency" +# If the MDI file cannot be downloaded within this time, abort. +MDI_DOWNLOAD_TIMEOUT = 30 # seconds + +SOURCE_LOCAL = "local" +SOURCE_MDI = "mdi" + Image_ = display.display_ns.class_("Image") +def _compute_local_icon_path(value) -> Path: + base_dir = Path(CORE.config_dir) / ".esphome" / DOMAIN / "mdi" + return base_dir / f"{value[CONF_ICON]}.svg" + + +def download_mdi(value): + mdi_id = value[CONF_ICON] + path = _compute_local_icon_path(value) + if path.is_file(): + return value + url = f"https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg/{mdi_id}.svg" + _LOGGER.debug("Downloading %s MDI image from %s", mdi_id, url) + try: + req = requests.get(url, timeout=MDI_DOWNLOAD_TIMEOUT) + req.raise_for_status() + except requests.exceptions.RequestException as e: + raise cv.Invalid(f"Could not download MDI image {mdi_id} from {url}: {e}") + + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(req.content) + return value + + +def validate_cairosvg_installed(value): + """Validate that cairosvg is installed""" + try: + import cairosvg + except ImportError as err: + raise cv.Invalid( + "Please install the cairosvg python package to use this feature. " + "(pip install cairosvg)" + ) from err + + major, minor, _ = cairosvg.__version__.split(".") + if major < "2" or major == "2" and minor < "2": + raise cv.Invalid( + "Please update your cairosvg installation to at least 2.2.0. " + "(pip install -U cairosvg)" + ) + + return value + + def validate_cross_dependencies(config): """ Validate fields whose possible values depend on other fields. @@ -41,6 +99,13 @@ def validate_cross_dependencies(config): have "use_transparency" set to True. Also set the default value for those kind of dependent fields. """ + is_mdi = CONF_FILE in config and config[CONF_FILE][CONF_SOURCE] == SOURCE_MDI + if CONF_TYPE not in config: + if is_mdi: + config[CONF_TYPE] = "TRANSPARENT_BINARY" + else: + config[CONF_TYPE] = "BINARY" + image_type = config[CONF_TYPE] is_transparent_type = image_type in ["TRANSPARENT_BINARY", "RGBA"] @@ -51,16 +116,74 @@ def validate_cross_dependencies(config): if is_transparent_type and not config[CONF_USE_TRANSPARENCY]: raise cv.Invalid(f"Image type {image_type} must always be transparent.") + if is_mdi and config[CONF_TYPE] not in ["BINARY", "TRANSPARENT_BINARY"]: + raise cv.Invalid("MDI images must be binary images.") + return config +def validate_file_shorthand(value): + value = cv.string_strict(value) + if value.startswith("mdi:"): + validate_cairosvg_installed(value) + + match = re.search(r"mdi:([a-zA-Z0-9\-]+)", value) + if match is None: + raise cv.Invalid("Could not parse mdi icon name.") + icon = match.group(1) + return FILE_SCHEMA( + { + CONF_SOURCE: SOURCE_MDI, + CONF_ICON: icon, + } + ) + return FILE_SCHEMA( + { + CONF_SOURCE: SOURCE_LOCAL, + CONF_PATH: value, + } + ) + + +LOCAL_SCHEMA = cv.Schema( + { + cv.Required(CONF_PATH): cv.file_, + } +) + +MDI_SCHEMA = cv.All( + { + cv.Required(CONF_ICON): cv.string, + }, + download_mdi, +) + +TYPED_FILE_SCHEMA = cv.typed_schema( + { + SOURCE_LOCAL: LOCAL_SCHEMA, + SOURCE_MDI: MDI_SCHEMA, + }, + key=CONF_SOURCE, +) + + +def _file_schema(value): + if isinstance(value, str): + return validate_file_shorthand(value) + return TYPED_FILE_SCHEMA(value) + + +FILE_SCHEMA = cv.Schema(_file_schema) + IMAGE_SCHEMA = cv.Schema( cv.All( { cv.Required(CONF_ID): cv.declare_id(Image_), - cv.Required(CONF_FILE): cv.file_, + cv.Required(CONF_FILE): FILE_SCHEMA, cv.Optional(CONF_RESIZE): cv.dimensions, - cv.Optional(CONF_TYPE, default="BINARY"): cv.enum(IMAGE_TYPE, upper=True), + # Not setting default here on purpose; the default depends on the source type + # (file or mdi), and will be set in the "validate_cross_dependencies" validator. + cv.Optional(CONF_TYPE): cv.enum(IMAGE_TYPE, upper=True), # Not setting default here on purpose; the default depends on the image type, # and thus will be set in the "validate_cross_dependencies" validator. cv.Optional(CONF_USE_TRANSPARENCY): cv.boolean, @@ -79,24 +202,43 @@ CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, IMAGE_SCHEMA) async def to_code(config): from PIL import Image - path = CORE.relative_config_path(config[CONF_FILE]) - try: - image = Image.open(path) - except Exception as e: - raise core.EsphomeError(f"Could not load image file {path}: {e}") + conf_file = config[CONF_FILE] + + if conf_file[CONF_SOURCE] == SOURCE_LOCAL: + path = CORE.relative_config_path(conf_file[CONF_PATH]) + try: + image = Image.open(path) + except Exception as e: + raise core.EsphomeError(f"Could not load image file {path}: {e}") + if CONF_RESIZE in config: + image.thumbnail(config[CONF_RESIZE]) + elif conf_file[CONF_SOURCE] == SOURCE_MDI: + # Those imports are only needed in case of MDI images; adding them + # to the top would force configurations not using MDI to also have them + # installed for no reason. + from cairosvg import svg2png + + svg_file = _compute_local_icon_path(conf_file) + if CONF_RESIZE in config: + req_width, req_height = config[CONF_RESIZE] + svg_image = svg2png( + url=svg_file.as_posix(), + output_width=req_width, + output_height=req_height, + ) + else: + svg_image = svg2png(url=svg_file.as_posix()) + + image = Image.open(io.BytesIO(svg_image)) width, height = image.size - if CONF_RESIZE in config: - image.thumbnail(config[CONF_RESIZE]) - width, height = image.size - else: - if width > 500 or height > 500: - _LOGGER.warning( - 'The image "%s" you requested is very big. Please consider' - " using the resize parameter.", - path, - ) + if CONF_RESIZE not in config and (width > 500 or height > 500): + _LOGGER.warning( + 'The image "%s" you requested is very big. Please consider' + " using the resize parameter.", + path, + ) transparent = config[CONF_USE_TRANSPARENCY] diff --git a/esphome/components/lcd_base/lcd_display.cpp b/esphome/components/lcd_base/lcd_display.cpp index 180d5e93ac..65d7aa508f 100644 --- a/esphome/components/lcd_base/lcd_display.cpp +++ b/esphome/components/lcd_base/lcd_display.cpp @@ -158,15 +158,13 @@ void LCDDisplay::clear() { for (uint8_t i = 0; i < this->rows_ * this->columns_; i++) this->buffer_[i] = ' '; } -#ifdef USE_TIME -void LCDDisplay::strftime(uint8_t column, uint8_t row, const char *format, time::ESPTime time) { +void LCDDisplay::strftime(uint8_t column, uint8_t row, const char *format, ESPTime time) { char buffer[64]; size_t ret = time.strftime(buffer, sizeof(buffer), format); if (ret > 0) this->print(column, row, buffer); } -void LCDDisplay::strftime(const char *format, time::ESPTime time) { this->strftime(0, 0, format, time); } -#endif +void LCDDisplay::strftime(const char *format, ESPTime time) { this->strftime(0, 0, format, time); } void LCDDisplay::loadchar(uint8_t location, uint8_t charmap[]) { location &= 0x7; // we only have 8 locations 0-7 this->command_(LCD_DISPLAY_COMMAND_SET_CGRAM_ADDR | (location << 3)); diff --git a/esphome/components/lcd_base/lcd_display.h b/esphome/components/lcd_base/lcd_display.h index c1dc54a9ed..473acb0bd3 100644 --- a/esphome/components/lcd_base/lcd_display.h +++ b/esphome/components/lcd_base/lcd_display.h @@ -1,11 +1,7 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/defines.h" - -#ifdef USE_TIME -#include "esphome/components/time/real_time_clock.h" -#endif +#include "esphome/core/time.h" #include #include @@ -44,13 +40,10 @@ class LCDDisplay : public PollingComponent { /// Evaluate the printf-format and print the text at column=0 and row=0. void printf(const char *format, ...) __attribute__((format(printf, 2, 3))); -#ifdef USE_TIME /// Evaluate the strftime-format and print the text at the specified column and row. - void strftime(uint8_t column, uint8_t row, const char *format, time::ESPTime time) - __attribute__((format(strftime, 4, 0))); + void strftime(uint8_t column, uint8_t row, const char *format, ESPTime time) __attribute__((format(strftime, 4, 0))); /// Evaluate the strftime-format and print the text at column=0 and row=0. - void strftime(const char *format, time::ESPTime time) __attribute__((format(strftime, 2, 0))); -#endif + void strftime(const char *format, ESPTime time) __attribute__((format(strftime, 2, 0))); /// Load custom char to given location void loadchar(uint8_t location, uint8_t charmap[]); diff --git a/esphome/components/max7219/max7219.cpp b/esphome/components/max7219/max7219.cpp index b50a78eb96..38b4a165cb 100644 --- a/esphome/components/max7219/max7219.cpp +++ b/esphome/components/max7219/max7219.cpp @@ -223,16 +223,14 @@ void MAX7219Component::set_intensity(uint8_t intensity) { } void MAX7219Component::set_num_chips(uint8_t num_chips) { this->num_chips_ = num_chips; } -#ifdef USE_TIME -uint8_t MAX7219Component::strftime(uint8_t pos, const char *format, time::ESPTime time) { +uint8_t MAX7219Component::strftime(uint8_t pos, const char *format, ESPTime time) { char buffer[64]; size_t ret = time.strftime(buffer, sizeof(buffer), format); if (ret > 0) return this->print(pos, buffer); return 0; } -uint8_t MAX7219Component::strftime(const char *format, time::ESPTime time) { return this->strftime(0, format, time); } -#endif +uint8_t MAX7219Component::strftime(const char *format, ESPTime time) { return this->strftime(0, format, time); } } // namespace max7219 } // namespace esphome diff --git a/esphome/components/max7219/max7219.h b/esphome/components/max7219/max7219.h index 47b54a4c50..1b724cef69 100644 --- a/esphome/components/max7219/max7219.h +++ b/esphome/components/max7219/max7219.h @@ -1,11 +1,7 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/defines.h" - -#ifdef USE_TIME -#include "esphome/components/time/real_time_clock.h" -#endif +#include "esphome/core/time.h" #include "esphome/components/spi/spi.h" @@ -46,13 +42,11 @@ class MAX7219Component : public PollingComponent, /// Print `str` at position 0. uint8_t print(const char *str); -#ifdef USE_TIME /// Evaluate the strftime-format and print the result at the given position. - uint8_t strftime(uint8_t pos, const char *format, time::ESPTime time) __attribute__((format(strftime, 3, 0))); + uint8_t strftime(uint8_t pos, const char *format, ESPTime time) __attribute__((format(strftime, 3, 0))); /// Evaluate the strftime-format and print the result at position 0. - uint8_t strftime(const char *format, time::ESPTime time) __attribute__((format(strftime, 2, 0))); -#endif + uint8_t strftime(const char *format, ESPTime time) __attribute__((format(strftime, 2, 0))); protected: void send_byte_(uint8_t a_register, uint8_t data); diff --git a/esphome/components/max7219digit/max7219digit.cpp b/esphome/components/max7219digit/max7219digit.cpp index c65b8e4823..7d21032b8a 100644 --- a/esphome/components/max7219digit/max7219digit.cpp +++ b/esphome/components/max7219digit/max7219digit.cpp @@ -325,18 +325,16 @@ uint8_t MAX7219Component::printdigitf(const char *format, ...) { return 0; } -#ifdef USE_TIME -uint8_t MAX7219Component::strftimedigit(uint8_t pos, const char *format, time::ESPTime time) { +uint8_t MAX7219Component::strftimedigit(uint8_t pos, const char *format, ESPTime time) { char buffer[64]; size_t ret = time.strftime(buffer, sizeof(buffer), format); if (ret > 0) return this->printdigit(pos, buffer); return 0; } -uint8_t MAX7219Component::strftimedigit(const char *format, time::ESPTime time) { +uint8_t MAX7219Component::strftimedigit(const char *format, ESPTime time) { return this->strftimedigit(0, format, time); } -#endif } // namespace max7219digit } // namespace esphome diff --git a/esphome/components/max7219digit/max7219digit.h b/esphome/components/max7219digit/max7219digit.h index 17e369a9d9..93d2af21f9 100644 --- a/esphome/components/max7219digit/max7219digit.h +++ b/esphome/components/max7219digit/max7219digit.h @@ -1,16 +1,13 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/defines.h" +#include "esphome/core/time.h" + #include "esphome/components/display/display_buffer.h" #include "esphome/components/spi/spi.h" #include -#ifdef USE_TIME -#include "esphome/components/time/real_time_clock.h" -#endif - namespace esphome { namespace max7219digit { @@ -88,13 +85,11 @@ class MAX7219Component : public PollingComponent, /// Print `str` at position 0. uint8_t printdigit(const char *str); -#ifdef USE_TIME /// Evaluate the strftime-format and print the result at the given position. - uint8_t strftimedigit(uint8_t pos, const char *format, time::ESPTime time) __attribute__((format(strftime, 3, 0))); + uint8_t strftimedigit(uint8_t pos, const char *format, ESPTime time) __attribute__((format(strftime, 3, 0))); /// Evaluate the strftime-format and print the result at position 0. - uint8_t strftimedigit(const char *format, time::ESPTime time) __attribute__((format(strftime, 2, 0))); -#endif + uint8_t strftimedigit(const char *format, ESPTime time) __attribute__((format(strftime, 2, 0))); display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_BINARY; } diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index 86610ef564..28663138d7 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -4,6 +4,8 @@ #include #include "esphome/core/defines.h" +#include "esphome/core/time.h" + #include "esphome/components/uart/uart.h" #include "nextion_base.h" #include "nextion_component.h" @@ -19,10 +21,6 @@ #endif #endif -#ifdef USE_TIME -#include "esphome/components/time/real_time_clock.h" -#endif - namespace esphome { namespace nextion { @@ -318,13 +316,11 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * Changes the font of the component named `textveiw`. Font IDs are set in the Nextion Editor. */ void set_component_font(const char *component, uint8_t font_id) override; -#ifdef USE_TIME /** * Send the current time to the nextion display. * @param time The time instance to send (get this with id(my_time).now() ). */ - void set_nextion_rtc_time(time::ESPTime time); -#endif + void set_nextion_rtc_time(ESPTime time); /** * Show the page with a given name. diff --git a/esphome/components/nextion/nextion_commands.cpp b/esphome/components/nextion/nextion_commands.cpp index 308e02bce8..0409e5ea6c 100644 --- a/esphome/components/nextion/nextion_commands.cpp +++ b/esphome/components/nextion/nextion_commands.cpp @@ -219,8 +219,7 @@ void Nextion::filled_circle(int center_x, int center_y, int radius, Color color) display::ColorUtil::color_to_565(color)); } -#ifdef USE_TIME -void Nextion::set_nextion_rtc_time(time::ESPTime time) { +void Nextion::set_nextion_rtc_time(ESPTime time) { this->add_no_result_to_queue_with_printf_("rtc0", "rtc0=%u", time.year); this->add_no_result_to_queue_with_printf_("rtc1", "rtc1=%u", time.month); this->add_no_result_to_queue_with_printf_("rtc2", "rtc2=%u", time.day_of_month); @@ -228,7 +227,6 @@ void Nextion::set_nextion_rtc_time(time::ESPTime time) { this->add_no_result_to_queue_with_printf_("rtc4", "rtc4=%u", time.minute); this->add_no_result_to_queue_with_printf_("rtc5", "rtc5=%u", time.second); } -#endif } // namespace nextion } // namespace esphome diff --git a/esphome/components/pcf85063/pcf85063.cpp b/esphome/components/pcf85063/pcf85063.cpp index 5073522655..debc007cb8 100644 --- a/esphome/components/pcf85063/pcf85063.cpp +++ b/esphome/components/pcf85063/pcf85063.cpp @@ -37,7 +37,7 @@ void PCF85063Component::read_time() { ESP_LOGW(TAG, "RTC halted, not syncing to system clock."); return; } - time::ESPTime rtc_time{ + ESPTime rtc_time{ .second = uint8_t(pcf85063_.reg.second + 10 * pcf85063_.reg.second_10), .minute = uint8_t(pcf85063_.reg.minute + 10u * pcf85063_.reg.minute_10), .hour = uint8_t(pcf85063_.reg.hour + 10u * pcf85063_.reg.hour_10), diff --git a/esphome/components/sntp/sntp_component.h b/esphome/components/sntp/sntp_component.h index 4c70a6b09f..987dd23a19 100644 --- a/esphome/components/sntp/sntp_component.h +++ b/esphome/components/sntp/sntp_component.h @@ -22,7 +22,7 @@ class SNTPComponent : public time::RealTimeClock { this->server_2_ = server_2; this->server_3_ = server_3; } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + float get_setup_priority() const override { return setup_priority::BEFORE_CONNECTION; } void update() override; void loop() override; diff --git a/esphome/components/sun/sun.cpp b/esphome/components/sun/sun.cpp index 5f9179682a..2fd9394a5e 100644 --- a/esphome/components/sun/sun.cpp +++ b/esphome/components/sun/sun.cpp @@ -37,7 +37,7 @@ num_t EquatorialCoordinate::declination_rad() const { return radians(declination num_t HorizontalCoordinate::elevation_rad() const { return radians(elevation); } num_t HorizontalCoordinate::azimuth_rad() const { return radians(azimuth); } -num_t julian_day(time::ESPTime moment) { +num_t julian_day(ESPTime moment) { // p. 59 // UT -> JD, TT -> JDE int y = moment.year; @@ -54,7 +54,7 @@ num_t julian_day(time::ESPTime moment) { int b = 2 - a + a / 4; return ((int) (365.25 * (y + 4716))) + ((int) (30.6001 * (m + 1))) + d + b - 1524.5; } -num_t delta_t(time::ESPTime moment) { +num_t delta_t(ESPTime moment) { // approximation for 2005-2050 from NASA (https://eclipse.gsfc.nasa.gov/SEhelp/deltatpoly2004.html) int t = moment.year - 2000; return 62.92 + t * (0.32217 + t * 0.005589); @@ -199,7 +199,7 @@ struct SunAtLocation { // see chapter 12, p. 87 num_t jd = moment.jd(); // eq 12.1, p.87; jd for 0h UT of this date - time::ESPTime moment_0h = moment.dt; + ESPTime moment_0h = moment.dt; moment_0h.hour = moment_0h.minute = moment_0h.second = 0; num_t jd0 = Moment{moment_0h}.jd(); num_t t = (jd0 - 2451545) / 36525; @@ -227,9 +227,9 @@ struct SunAtLocation { return HorizontalCoordinate{degrees(elevation_rad), degrees(azimuth_rad) + 180}; } - optional sunrise(time::ESPTime date, num_t zenith) const { return event(true, date, zenith); } - optional sunset(time::ESPTime date, num_t zenith) const { return event(false, date, zenith); } - optional event(bool rise, time::ESPTime date, num_t zenith) const { + optional sunrise(ESPTime date, num_t zenith) const { return event(true, date, zenith); } + optional sunset(ESPTime date, num_t zenith) const { return event(false, date, zenith); } + optional event(bool rise, ESPTime date, num_t zenith) const { // couldn't get the method described in chapter 15 to work, // so instead this is based on the algorithm in time4j // https://github.com/MenoData/Time4J/blob/master/base/src/main/java/net/time4j/calendar/astro/StdSolarCalculator.java @@ -244,7 +244,7 @@ struct SunAtLocation { new_h = *x; } while (std::abs(new_h - old_h) >= 15); time_t new_timestamp = m.timestamp + (time_t) new_h; - return time::ESPTime::from_epoch_local(new_timestamp); + return ESPTime::from_epoch_local(new_timestamp); } protected: @@ -263,14 +263,14 @@ struct SunAtLocation { return hour_angle; } - time::ESPTime local_event_(time::ESPTime date, int hour) const { + ESPTime local_event_(ESPTime date, int hour) const { // input date should be in UTC, and hour/minute/second fields 0 num_t added_d = hour / 24.0 - location.longitude / 360; num_t jd = julian_day(date) + added_d; num_t eot = SunAtTime(jd).equation_of_time() * 240; time_t new_timestamp = (time_t) (date.timestamp + added_d * 86400 - eot); - return time::ESPTime::from_epoch_utc(new_timestamp); + return ESPTime::from_epoch_utc(new_timestamp); } }; @@ -287,7 +287,7 @@ HorizontalCoordinate Sun::calc_coords_() { */ return sun.true_coordinate(m); } -optional Sun::calc_event_(time::ESPTime date, bool rising, double zenith) { +optional Sun::calc_event_(ESPTime date, bool rising, double zenith) { SunAtLocation sun{location_}; if (!date.is_valid()) return {}; @@ -301,24 +301,20 @@ optional Sun::calc_event_(time::ESPTime date, bool rising, double // We're calculating *next* sunrise/sunset, but calculated event // is today, so try again tomorrow time_t new_timestamp = today.timestamp + 24 * 60 * 60; - today = time::ESPTime::from_epoch_utc(new_timestamp); + today = ESPTime::from_epoch_utc(new_timestamp); it = sun.event(rising, today, zenith); } return it; } -optional Sun::calc_event_(bool rising, double zenith) { +optional Sun::calc_event_(bool rising, double zenith) { auto it = Sun::calc_event_(this->time_->utcnow(), rising, zenith); return it; } -optional Sun::sunrise(double elevation) { return this->calc_event_(true, 90 - elevation); } -optional Sun::sunset(double elevation) { return this->calc_event_(false, 90 - elevation); } -optional Sun::sunrise(time::ESPTime date, double elevation) { - return this->calc_event_(date, true, 90 - elevation); -} -optional Sun::sunset(time::ESPTime date, double elevation) { - return this->calc_event_(date, false, 90 - elevation); -} +optional Sun::sunrise(double elevation) { return this->calc_event_(true, 90 - elevation); } +optional Sun::sunset(double elevation) { return this->calc_event_(false, 90 - elevation); } +optional Sun::sunrise(ESPTime date, double elevation) { return this->calc_event_(date, true, 90 - elevation); } +optional Sun::sunset(ESPTime date, double elevation) { return this->calc_event_(date, false, 90 - elevation); } double Sun::elevation() { return this->calc_coords_().elevation; } double Sun::azimuth() { return this->calc_coords_().azimuth; } diff --git a/esphome/components/sun/sun.h b/esphome/components/sun/sun.h index 9547b2f280..de4801a655 100644 --- a/esphome/components/sun/sun.h +++ b/esphome/components/sun/sun.h @@ -1,8 +1,10 @@ #pragma once +#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" -#include "esphome/core/automation.h" +#include "esphome/core/time.h" + #include "esphome/components/time/real_time_clock.h" namespace esphome { @@ -26,7 +28,7 @@ struct GeoLocation { }; struct Moment { - time::ESPTime dt; + ESPTime dt; num_t jd() const; num_t jde() const; @@ -57,18 +59,18 @@ class Sun { void set_latitude(double latitude) { location_.latitude = latitude; } void set_longitude(double longitude) { location_.longitude = longitude; } - optional sunrise(double elevation); - optional sunset(double elevation); - optional sunrise(time::ESPTime date, double elevation); - optional sunset(time::ESPTime date, double elevation); + optional sunrise(double elevation); + optional sunset(double elevation); + optional sunrise(ESPTime date, double elevation); + optional sunset(ESPTime date, double elevation); double elevation(); double azimuth(); protected: internal::HorizontalCoordinate calc_coords_(); - optional calc_event_(bool rising, double zenith); - optional calc_event_(time::ESPTime date, bool rising, double zenith); + optional calc_event_(bool rising, double zenith); + optional calc_event_(ESPTime date, bool rising, double zenith); time::RealTimeClock *time_; internal::GeoLocation location_; diff --git a/esphome/components/sun/text_sensor/sun_text_sensor.h b/esphome/components/sun/text_sensor/sun_text_sensor.h index ad01d64ff1..ce7d21fb86 100644 --- a/esphome/components/sun/text_sensor/sun_text_sensor.h +++ b/esphome/components/sun/text_sensor/sun_text_sensor.h @@ -1,6 +1,8 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/time.h" + #include "esphome/components/sun/sun.h" #include "esphome/components/text_sensor/text_sensor.h" @@ -15,7 +17,7 @@ class SunTextSensor : public text_sensor::TextSensor, public PollingComponent { void set_format(const std::string &format) { format_ = format; } void update() override { - optional res; + optional res; if (this->sunrise_) { res = this->parent_->sunrise(this->elevation_); } else { diff --git a/esphome/components/time/automation.cpp b/esphome/components/time/automation.cpp index af2b6c720c..f7c1916ffe 100644 --- a/esphome/components/time/automation.cpp +++ b/esphome/components/time/automation.cpp @@ -1,5 +1,7 @@ #include "automation.h" + #include "esphome/core/log.h" + #include namespace esphome { diff --git a/esphome/components/time/automation.h b/esphome/components/time/automation.h index e97413e420..b5c8291533 100644 --- a/esphome/components/time/automation.h +++ b/esphome/components/time/automation.h @@ -1,7 +1,9 @@ #pragma once -#include "esphome/core/component.h" #include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/time.h" + #include "real_time_clock.h" #include diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index de76676a4d..10fa9597b9 100644 --- a/esphome/components/time/real_time_clock.cpp +++ b/esphome/components/time/real_time_clock.cpp @@ -52,171 +52,5 @@ void RealTimeClock::apply_timezone_() { tzset(); } -size_t ESPTime::strftime(char *buffer, size_t buffer_len, const char *format) { - struct tm c_tm = this->to_c_tm(); - return ::strftime(buffer, buffer_len, format, &c_tm); -} -ESPTime ESPTime::from_c_tm(struct tm *c_tm, time_t c_time) { - ESPTime res{}; - res.second = uint8_t(c_tm->tm_sec); - res.minute = uint8_t(c_tm->tm_min); - res.hour = uint8_t(c_tm->tm_hour); - res.day_of_week = uint8_t(c_tm->tm_wday + 1); - res.day_of_month = uint8_t(c_tm->tm_mday); - res.day_of_year = uint16_t(c_tm->tm_yday + 1); - res.month = uint8_t(c_tm->tm_mon + 1); - res.year = uint16_t(c_tm->tm_year + 1900); - res.is_dst = bool(c_tm->tm_isdst); - res.timestamp = c_time; - return res; -} -struct tm ESPTime::to_c_tm() { - struct tm c_tm {}; - c_tm.tm_sec = this->second; - c_tm.tm_min = this->minute; - c_tm.tm_hour = this->hour; - c_tm.tm_mday = this->day_of_month; - c_tm.tm_mon = this->month - 1; - c_tm.tm_year = this->year - 1900; - c_tm.tm_wday = this->day_of_week - 1; - c_tm.tm_yday = this->day_of_year - 1; - c_tm.tm_isdst = this->is_dst; - return c_tm; -} -std::string ESPTime::strftime(const std::string &format) { - std::string timestr; - timestr.resize(format.size() * 4); - struct tm c_tm = this->to_c_tm(); - size_t len = ::strftime(×tr[0], timestr.size(), format.c_str(), &c_tm); - while (len == 0) { - timestr.resize(timestr.size() * 2); - len = ::strftime(×tr[0], timestr.size(), format.c_str(), &c_tm); - } - timestr.resize(len); - return timestr; -} - -template bool increment_time_value(T ¤t, uint16_t begin, uint16_t end) { - current++; - if (current >= end) { - current = begin; - return true; - } - return false; -} - -static bool is_leap_year(uint32_t year) { return (year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0); } - -static uint8_t days_in_month(uint8_t month, uint16_t year) { - static const uint8_t DAYS_IN_MONTH[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; - uint8_t days = DAYS_IN_MONTH[month]; - if (month == 2 && is_leap_year(year)) - return 29; - return days; -} - -void ESPTime::increment_second() { - this->timestamp++; - if (!increment_time_value(this->second, 0, 60)) - return; - - // second roll-over, increment minute - if (!increment_time_value(this->minute, 0, 60)) - return; - - // minute roll-over, increment hour - if (!increment_time_value(this->hour, 0, 24)) - return; - - // hour roll-over, increment day - increment_time_value(this->day_of_week, 1, 8); - - if (increment_time_value(this->day_of_month, 1, days_in_month(this->month, this->year) + 1)) { - // day of month roll-over, increment month - increment_time_value(this->month, 1, 13); - } - - uint16_t days_in_year = (this->year % 4 == 0) ? 366 : 365; - if (increment_time_value(this->day_of_year, 1, days_in_year + 1)) { - // day of year roll-over, increment year - this->year++; - } -} -void ESPTime::increment_day() { - this->timestamp += 86400; - - // increment day - increment_time_value(this->day_of_week, 1, 8); - - if (increment_time_value(this->day_of_month, 1, days_in_month(this->month, this->year) + 1)) { - // day of month roll-over, increment month - increment_time_value(this->month, 1, 13); - } - - uint16_t days_in_year = (this->year % 4 == 0) ? 366 : 365; - if (increment_time_value(this->day_of_year, 1, days_in_year + 1)) { - // day of year roll-over, increment year - this->year++; - } -} -void ESPTime::recalc_timestamp_utc(bool use_day_of_year) { - time_t res = 0; - - if (!this->fields_in_range()) { - this->timestamp = -1; - return; - } - - for (int i = 1970; i < this->year; i++) - res += is_leap_year(i) ? 366 : 365; - - if (use_day_of_year) { - res += this->day_of_year - 1; - } else { - for (int i = 1; i < this->month; i++) - res += days_in_month(i, this->year); - - res += this->day_of_month - 1; - } - - res *= 24; - res += this->hour; - res *= 60; - res += this->minute; - res *= 60; - res += this->second; - this->timestamp = res; -} - -int32_t ESPTime::timezone_offset() { - int32_t offset = 0; - time_t now = ::time(nullptr); - auto local = ESPTime::from_epoch_local(now); - auto utc = ESPTime::from_epoch_utc(now); - bool negative = utc.hour > local.hour && local.day_of_year <= utc.day_of_year; - - if (utc.minute > local.minute) { - local.minute += 60; - local.hour -= 1; - } - offset += (local.minute - utc.minute) * 60; - - if (negative) { - offset -= (utc.hour - local.hour) * 3600; - } else { - if (utc.hour > local.hour) { - local.hour += 24; - } - offset += (local.hour - utc.hour) * 3600; - } - return offset; -} - -bool ESPTime::operator<(ESPTime other) { return this->timestamp < other.timestamp; } -bool ESPTime::operator<=(ESPTime other) { return this->timestamp <= other.timestamp; } -bool ESPTime::operator==(ESPTime other) { return this->timestamp == other.timestamp; } -bool ESPTime::operator>=(ESPTime other) { return this->timestamp >= other.timestamp; } -bool ESPTime::operator>(ESPTime other) { return this->timestamp > other.timestamp; } - } // namespace time } // namespace esphome diff --git a/esphome/components/time/real_time_clock.h b/esphome/components/time/real_time_clock.h index 7f4afee306..a17168ae6f 100644 --- a/esphome/components/time/real_time_clock.h +++ b/esphome/components/time/real_time_clock.h @@ -1,106 +1,15 @@ #pragma once +#include +#include #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" -#include -#include -#include +#include "esphome/core/time.h" namespace esphome { namespace time { -/// A more user-friendly version of struct tm from time.h -struct ESPTime { - /** seconds after the minute [0-60] - * @note second is generally 0-59; the extra range is to accommodate leap seconds. - */ - uint8_t second; - /// minutes after the hour [0-59] - uint8_t minute; - /// hours since midnight [0-23] - uint8_t hour; - /// day of the week; sunday=1 [1-7] - uint8_t day_of_week; - /// day of the month [1-31] - uint8_t day_of_month; - /// day of the year [1-366] - uint16_t day_of_year; - /// month; january=1 [1-12] - uint8_t month; - /// year - uint16_t year; - /// daylight saving time flag - bool is_dst; - /// unix epoch time (seconds since UTC Midnight January 1, 1970) - time_t timestamp; - - /** Convert this ESPTime struct to a null-terminated c string buffer as specified by the format argument. - * Up to buffer_len bytes are written. - * - * @see https://www.gnu.org/software/libc/manual/html_node/Formatting-Calendar-Time.html#index-strftime - */ - size_t strftime(char *buffer, size_t buffer_len, const char *format); - - /** Convert this ESPTime struct to a string as specified by the format argument. - * @see https://www.gnu.org/software/libc/manual/html_node/Formatting-Calendar-Time.html#index-strftime - * - * @warning This method uses dynamically allocated strings which can cause heap fragmentation with some - * microcontrollers. - */ - std::string strftime(const std::string &format); - - /// Check if this ESPTime is valid (all fields in range and year is greater than 2018) - bool is_valid() const { return this->year >= 2019 && this->fields_in_range(); } - - /// Check if all time fields of this ESPTime are in range. - bool fields_in_range() const { - return this->second < 61 && this->minute < 60 && this->hour < 24 && this->day_of_week > 0 && - this->day_of_week < 8 && this->day_of_month > 0 && this->day_of_month < 32 && this->day_of_year > 0 && - this->day_of_year < 367 && this->month > 0 && this->month < 13; - } - - /// Convert a C tm struct instance with a C unix epoch timestamp to an ESPTime instance. - static ESPTime from_c_tm(struct tm *c_tm, time_t c_time); - - /** Convert an UTC epoch timestamp to a local time ESPTime instance. - * - * @param epoch Seconds since 1st January 1970. In UTC. - * @return The generated ESPTime - */ - static ESPTime from_epoch_local(time_t epoch) { - struct tm *c_tm = ::localtime(&epoch); - return ESPTime::from_c_tm(c_tm, epoch); - } - /** Convert an UTC epoch timestamp to a UTC time ESPTime instance. - * - * @param epoch Seconds since 1st January 1970. In UTC. - * @return The generated ESPTime - */ - static ESPTime from_epoch_utc(time_t epoch) { - struct tm *c_tm = ::gmtime(&epoch); - return ESPTime::from_c_tm(c_tm, epoch); - } - - /// Recalculate the timestamp field from the other fields of this ESPTime instance (must be UTC). - void recalc_timestamp_utc(bool use_day_of_year = true); - - /// Convert this ESPTime instance back to a tm struct. - struct tm to_c_tm(); - - static int32_t timezone_offset(); - - /// Increment this clock instance by one second. - void increment_second(); - /// Increment this clock instance by one day. - void increment_day(); - bool operator<(ESPTime other); - bool operator<=(ESPTime other); - bool operator==(ESPTime other); - bool operator>=(ESPTime other); - bool operator>(ESPTime other); -}; - /// The RealTimeClock class exposes common timekeeping functions via the device's local real-time clock. /// /// \note diff --git a/esphome/components/tm1637/tm1637.cpp b/esphome/components/tm1637/tm1637.cpp index 5b8cbc6004..434c6e65f3 100644 --- a/esphome/components/tm1637/tm1637.cpp +++ b/esphome/components/tm1637/tm1637.cpp @@ -368,16 +368,14 @@ uint8_t TM1637Display::printf(const char *format, ...) { return 0; } -#ifdef USE_TIME -uint8_t TM1637Display::strftime(uint8_t pos, const char *format, time::ESPTime time) { +uint8_t TM1637Display::strftime(uint8_t pos, const char *format, ESPTime time) { char buffer[64]; size_t ret = time.strftime(buffer, sizeof(buffer), format); if (ret > 0) return this->print(pos, buffer); return 0; } -uint8_t TM1637Display::strftime(const char *format, time::ESPTime time) { return this->strftime(0, format, time); } -#endif +uint8_t TM1637Display::strftime(const char *format, ESPTime time) { return this->strftime(0, format, time); } } // namespace tm1637 } // namespace esphome diff --git a/esphome/components/tm1637/tm1637.h b/esphome/components/tm1637/tm1637.h index 2fb572bc55..aba0071b12 100644 --- a/esphome/components/tm1637/tm1637.h +++ b/esphome/components/tm1637/tm1637.h @@ -3,13 +3,10 @@ #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" +#include "esphome/core/time.h" #include -#ifdef USE_TIME -#include "esphome/components/time/real_time_clock.h" -#endif - #ifdef USE_BINARY_SENSOR #include "esphome/components/binary_sensor/binary_sensor.h" #endif @@ -61,12 +58,10 @@ class TM1637Display : public PollingComponent { void add_tm1637_key(TM1637Key *tm1637_key) { this->tm1637_keys_.push_back(tm1637_key); } #endif -#ifdef USE_TIME /// Evaluate the strftime-format and print the result at the given position. - uint8_t strftime(uint8_t pos, const char *format, time::ESPTime time) __attribute__((format(strftime, 3, 0))); + uint8_t strftime(uint8_t pos, const char *format, ESPTime time) __attribute__((format(strftime, 3, 0))); /// Evaluate the strftime-format and print the result at position 0. - uint8_t strftime(const char *format, time::ESPTime time) __attribute__((format(strftime, 2, 0))); -#endif + uint8_t strftime(const char *format, ESPTime time) __attribute__((format(strftime, 2, 0))); protected: void bit_delay_(); diff --git a/esphome/components/tm1638/tm1638.cpp b/esphome/components/tm1638/tm1638.cpp index 24cb4122bf..a15b006046 100644 --- a/esphome/components/tm1638/tm1638.cpp +++ b/esphome/components/tm1638/tm1638.cpp @@ -211,16 +211,14 @@ uint8_t TM1638Component::printf(const char *format, ...) { return 0; } -#ifdef USE_TIME -uint8_t TM1638Component::strftime(uint8_t pos, const char *format, time::ESPTime time) { +uint8_t TM1638Component::strftime(uint8_t pos, const char *format, ESPTime time) { char buffer[64]; size_t ret = time.strftime(buffer, sizeof(buffer), format); if (ret > 0) return this->print(pos, buffer); return 0; } -uint8_t TM1638Component::strftime(const char *format, time::ESPTime time) { return this->strftime(0, format, time); } -#endif +uint8_t TM1638Component::strftime(const char *format, ESPTime time) { return this->strftime(0, format, time); } //////////////// SPI //////////////// diff --git a/esphome/components/tm1638/tm1638.h b/esphome/components/tm1638/tm1638.h index 2e1ac6fad3..add72cfdf3 100644 --- a/esphome/components/tm1638/tm1638.h +++ b/esphome/components/tm1638/tm1638.h @@ -1,16 +1,13 @@ #pragma once +#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/defines.h" -#include "esphome/core/automation.h" #include "esphome/core/hal.h" +#include "esphome/core/time.h" #include -#ifdef USE_TIME -#include "esphome/components/time/real_time_clock.h" -#endif - namespace esphome { namespace tm1638 { @@ -52,12 +49,10 @@ class TM1638Component : public PollingComponent { void loop() override; uint8_t get_keys(); -#ifdef USE_TIME /// Evaluate the strftime-format and print the result at the given position. - uint8_t strftime(uint8_t pos, const char *format, time::ESPTime time) __attribute__((format(strftime, 3, 0))); + uint8_t strftime(uint8_t pos, const char *format, ESPTime time) __attribute__((format(strftime, 3, 0))); /// Evaluate the strftime-format and print the result at position 0. - uint8_t strftime(const char *format, time::ESPTime time) __attribute__((format(strftime, 2, 0))); -#endif + uint8_t strftime(const char *format, ESPTime time) __attribute__((format(strftime, 2, 0))); void set_led(int led_pos, bool led_on_off); diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index 040b9b7ed5..7e6b1d53fe 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -508,7 +508,7 @@ void Tuya::send_wifi_status_() { void Tuya::send_local_time_() { std::vector payload; auto *time_id = *this->time_id_; - time::ESPTime now = time_id->now(); + ESPTime now = time_id->now(); if (now.is_valid()) { uint8_t year = now.year - 2000; uint8_t month = now.month; diff --git a/esphome/components/tuya/tuya.h b/esphome/components/tuya/tuya.h index 8d6153482f..b9901dd5e7 100644 --- a/esphome/components/tuya/tuya.h +++ b/esphome/components/tuya/tuya.h @@ -7,6 +7,7 @@ #ifdef USE_TIME #include "esphome/components/time/real_time_clock.h" +#include "esphome/core/time.h" #endif #include diff --git a/esphome/core/time.cpp b/esphome/core/time.cpp new file mode 100644 index 0000000000..c03506fd2a --- /dev/null +++ b/esphome/core/time.cpp @@ -0,0 +1,171 @@ +#include "time.h" // NOLINT + +namespace esphome { + +size_t ESPTime::strftime(char *buffer, size_t buffer_len, const char *format) { + struct tm c_tm = this->to_c_tm(); + return ::strftime(buffer, buffer_len, format, &c_tm); +} +ESPTime ESPTime::from_c_tm(struct tm *c_tm, time_t c_time) { + ESPTime res{}; + res.second = uint8_t(c_tm->tm_sec); + res.minute = uint8_t(c_tm->tm_min); + res.hour = uint8_t(c_tm->tm_hour); + res.day_of_week = uint8_t(c_tm->tm_wday + 1); + res.day_of_month = uint8_t(c_tm->tm_mday); + res.day_of_year = uint16_t(c_tm->tm_yday + 1); + res.month = uint8_t(c_tm->tm_mon + 1); + res.year = uint16_t(c_tm->tm_year + 1900); + res.is_dst = bool(c_tm->tm_isdst); + res.timestamp = c_time; + return res; +} +struct tm ESPTime::to_c_tm() { + struct tm c_tm {}; + c_tm.tm_sec = this->second; + c_tm.tm_min = this->minute; + c_tm.tm_hour = this->hour; + c_tm.tm_mday = this->day_of_month; + c_tm.tm_mon = this->month - 1; + c_tm.tm_year = this->year - 1900; + c_tm.tm_wday = this->day_of_week - 1; + c_tm.tm_yday = this->day_of_year - 1; + c_tm.tm_isdst = this->is_dst; + return c_tm; +} +std::string ESPTime::strftime(const std::string &format) { + std::string timestr; + timestr.resize(format.size() * 4); + struct tm c_tm = this->to_c_tm(); + size_t len = ::strftime(×tr[0], timestr.size(), format.c_str(), &c_tm); + while (len == 0) { + timestr.resize(timestr.size() * 2); + len = ::strftime(×tr[0], timestr.size(), format.c_str(), &c_tm); + } + timestr.resize(len); + return timestr; +} + +void ESPTime::increment_second() { + this->timestamp++; + if (!increment_time_value(this->second, 0, 60)) + return; + + // second roll-over, increment minute + if (!increment_time_value(this->minute, 0, 60)) + return; + + // minute roll-over, increment hour + if (!increment_time_value(this->hour, 0, 24)) + return; + + // hour roll-over, increment day + increment_time_value(this->day_of_week, 1, 8); + + if (increment_time_value(this->day_of_month, 1, days_in_month(this->month, this->year) + 1)) { + // day of month roll-over, increment month + increment_time_value(this->month, 1, 13); + } + + uint16_t days_in_year = (this->year % 4 == 0) ? 366 : 365; + if (increment_time_value(this->day_of_year, 1, days_in_year + 1)) { + // day of year roll-over, increment year + this->year++; + } +} +void ESPTime::increment_day() { + this->timestamp += 86400; + + // increment day + increment_time_value(this->day_of_week, 1, 8); + + if (increment_time_value(this->day_of_month, 1, days_in_month(this->month, this->year) + 1)) { + // day of month roll-over, increment month + increment_time_value(this->month, 1, 13); + } + + uint16_t days_in_year = (this->year % 4 == 0) ? 366 : 365; + if (increment_time_value(this->day_of_year, 1, days_in_year + 1)) { + // day of year roll-over, increment year + this->year++; + } +} +void ESPTime::recalc_timestamp_utc(bool use_day_of_year) { + time_t res = 0; + + if (!this->fields_in_range()) { + this->timestamp = -1; + return; + } + + for (int i = 1970; i < this->year; i++) + res += is_leap_year(i) ? 366 : 365; + + if (use_day_of_year) { + res += this->day_of_year - 1; + } else { + for (int i = 1; i < this->month; i++) + res += days_in_month(i, this->year); + + res += this->day_of_month - 1; + } + + res *= 24; + res += this->hour; + res *= 60; + res += this->minute; + res *= 60; + res += this->second; + this->timestamp = res; +} + +int32_t ESPTime::timezone_offset() { + int32_t offset = 0; + time_t now = ::time(nullptr); + auto local = ESPTime::from_epoch_local(now); + auto utc = ESPTime::from_epoch_utc(now); + bool negative = utc.hour > local.hour && local.day_of_year <= utc.day_of_year; + + if (utc.minute > local.minute) { + local.minute += 60; + local.hour -= 1; + } + offset += (local.minute - utc.minute) * 60; + + if (negative) { + offset -= (utc.hour - local.hour) * 3600; + } else { + if (utc.hour > local.hour) { + local.hour += 24; + } + offset += (local.hour - utc.hour) * 3600; + } + return offset; +} + +bool ESPTime::operator<(ESPTime other) { return this->timestamp < other.timestamp; } +bool ESPTime::operator<=(ESPTime other) { return this->timestamp <= other.timestamp; } +bool ESPTime::operator==(ESPTime other) { return this->timestamp == other.timestamp; } +bool ESPTime::operator>=(ESPTime other) { return this->timestamp >= other.timestamp; } +bool ESPTime::operator>(ESPTime other) { return this->timestamp > other.timestamp; } + +template bool increment_time_value(T ¤t, uint16_t begin, uint16_t end) { + current++; + if (current >= end) { + current = begin; + return true; + } + return false; +} + +static bool is_leap_year(uint32_t year) { return (year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0); } + +static uint8_t days_in_month(uint8_t month, uint16_t year) { + static const uint8_t DAYS_IN_MONTH[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + uint8_t days = DAYS_IN_MONTH[month]; + if (month == 2 && is_leap_year(year)) + return 29; + return days; +} + +} // namespace esphome diff --git a/esphome/core/time.h b/esphome/core/time.h new file mode 100644 index 0000000000..e1bdc8c839 --- /dev/null +++ b/esphome/core/time.h @@ -0,0 +1,105 @@ +#pragma once + +#include +#include +#include + +namespace esphome { + +template bool increment_time_value(T ¤t, uint16_t begin, uint16_t end); + +static bool is_leap_year(uint32_t year); + +static uint8_t days_in_month(uint8_t month, uint16_t year); + +/// A more user-friendly version of struct tm from time.h +struct ESPTime { + /** seconds after the minute [0-60] + * @note second is generally 0-59; the extra range is to accommodate leap seconds. + */ + uint8_t second; + /// minutes after the hour [0-59] + uint8_t minute; + /// hours since midnight [0-23] + uint8_t hour; + /// day of the week; sunday=1 [1-7] + uint8_t day_of_week; + /// day of the month [1-31] + uint8_t day_of_month; + /// day of the year [1-366] + uint16_t day_of_year; + /// month; january=1 [1-12] + uint8_t month; + /// year + uint16_t year; + /// daylight saving time flag + bool is_dst; + /// unix epoch time (seconds since UTC Midnight January 1, 1970) + time_t timestamp; + + /** Convert this ESPTime struct to a null-terminated c string buffer as specified by the format argument. + * Up to buffer_len bytes are written. + * + * @see https://www.gnu.org/software/libc/manual/html_node/Formatting-Calendar-Time.html#index-strftime + */ + size_t strftime(char *buffer, size_t buffer_len, const char *format); + + /** Convert this ESPTime struct to a string as specified by the format argument. + * @see https://www.gnu.org/software/libc/manual/html_node/Formatting-Calendar-Time.html#index-strftime + * + * @warning This method uses dynamically allocated strings which can cause heap fragmentation with some + * microcontrollers. + */ + std::string strftime(const std::string &format); + + /// Check if this ESPTime is valid (all fields in range and year is greater than 2018) + bool is_valid() const { return this->year >= 2019 && this->fields_in_range(); } + + /// Check if all time fields of this ESPTime are in range. + bool fields_in_range() const { + return this->second < 61 && this->minute < 60 && this->hour < 24 && this->day_of_week > 0 && + this->day_of_week < 8 && this->day_of_month > 0 && this->day_of_month < 32 && this->day_of_year > 0 && + this->day_of_year < 367 && this->month > 0 && this->month < 13; + } + + /// Convert a C tm struct instance with a C unix epoch timestamp to an ESPTime instance. + static ESPTime from_c_tm(struct tm *c_tm, time_t c_time); + + /** Convert an UTC epoch timestamp to a local time ESPTime instance. + * + * @param epoch Seconds since 1st January 1970. In UTC. + * @return The generated ESPTime + */ + static ESPTime from_epoch_local(time_t epoch) { + struct tm *c_tm = ::localtime(&epoch); + return ESPTime::from_c_tm(c_tm, epoch); + } + /** Convert an UTC epoch timestamp to a UTC time ESPTime instance. + * + * @param epoch Seconds since 1st January 1970. In UTC. + * @return The generated ESPTime + */ + static ESPTime from_epoch_utc(time_t epoch) { + struct tm *c_tm = ::gmtime(&epoch); + return ESPTime::from_c_tm(c_tm, epoch); + } + + /// Recalculate the timestamp field from the other fields of this ESPTime instance (must be UTC). + void recalc_timestamp_utc(bool use_day_of_year = true); + + /// Convert this ESPTime instance back to a tm struct. + struct tm to_c_tm(); + + static int32_t timezone_offset(); + + /// Increment this clock instance by one second. + void increment_second(); + /// Increment this clock instance by one day. + void increment_day(); + bool operator<(ESPTime other); + bool operator<=(ESPTime other); + bool operator==(ESPTime other); + bool operator>=(ESPTime other); + bool operator>(ESPTime other); +}; +} // namespace esphome diff --git a/requirements.txt b/requirements.txt index a39f5afb1c..b994de1932 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ platformio==6.1.7 # When updating platformio, also update Dockerfile esptool==4.6 click==8.1.3 esphome-dashboard==20230516.0 -aioesphomeapi==13.9.0 +aioesphomeapi==14.0.0 zeroconf==0.63.0 # esp-idf requires this, but doesn't bundle it by default diff --git a/requirements_optional.txt b/requirements_optional.txt index 2c73430109..df6b3b387e 100644 --- a/requirements_optional.txt +++ b/requirements_optional.txt @@ -1,2 +1,3 @@ pillow>4.0.0 +cairosvg>=2.2.0 cryptography>=2.0.0,<4 diff --git a/tests/test2.yaml b/tests/test2.yaml index 8f56336848..2873d0e8e0 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -488,6 +488,14 @@ binary_sensor: esp32_ble_tracker: on_ble_advertise: + - mac_address: + - AA:BB:CC:DD:EE:FF + - FF:EE:DD:CC:BB:AA + then: + # yamllint disable rule:line-length + - lambda: !lambda |- + ESP_LOGD("main", "The device address (%s) exists in list", x.address_str().c_str()); + # yamllint enable rule:line-length - mac_address: AC:37:43:77:5F:4C then: # yamllint disable rule:line-length @@ -680,6 +688,13 @@ image: type: RGB565 use_transparency: no + - id: mdi_alert + file: mdi:alert-circle-outline + resize: 50x50 + - id: another_alert_icon + file: mdi:alert-outline + type: BINARY + cap1188: id: cap1188_component address: 0x29