mirror of
https://github.com/esphome/esphome.git
synced 2024-12-28 00:11:43 +01:00
Merge commit '5f0892dec43a4c1fdbb3d3853bac0f2a0fee1bbd' into optolink
This commit is contained in:
commit
44fa332931
55 changed files with 1018 additions and 663 deletions
|
@ -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}/* \
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -16,7 +16,7 @@ namespace api {
|
|||
class APIConnection : public APIServerConnection {
|
||||
public:
|
||||
APIConnection(std::unique_ptr<socket::Socket> 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
|
||||
|
|
|
@ -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<BluetoothLERawAdvertisement>());
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer buffer) const {
|
||||
for (auto &it : this->advertisements) {
|
||||
buffer.encode_message<BluetoothLERawAdvertisement>(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: {
|
||||
|
|
|
@ -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<BluetoothLERawAdvertisement> 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};
|
||||
|
|
|
@ -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_<BluetoothLERawAdvertisementsResponse>(msg, 93);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
bool APIServerConnectionBase::send_bluetooth_device_connection_response(const BluetoothDeviceConnectionResponse &msg) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<std::string> attribute,
|
||||
std::function<void(std::string)> f) {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
|
||||
#ifdef USE_TIME
|
||||
#include "esphome/components/time/real_time_clock.h"
|
||||
#include "esphome/core/time.h"
|
||||
#endif
|
||||
|
||||
#include <esp_gattc_api.h>
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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<uint64_t> 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
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
#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<BluetoothConnection *> 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
|
||||
|
||||
|
|
|
@ -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 <esp_sleep.h>
|
||||
|
@ -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<typename... Ts> class EnterDeepSleepAction : public Action<Ts...> {
|
|||
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);
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
#include <utility>
|
||||
#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()) {
|
||||
|
|
|
@ -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 <cstdarg>
|
||||
#include <vector>
|
||||
|
||||
#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.
|
||||
*
|
||||
|
|
|
@ -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.");
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -10,18 +10,22 @@ namespace esp32_ble_tracker {
|
|||
class ESPBTAdvertiseTrigger : public Trigger<const ESPBTDevice &>, 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<uint64_t> &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<uint64_t> address_vec_;
|
||||
};
|
||||
|
||||
class BLEServiceDataAdvertiseTrigger : public Trigger<const adv_data_t &>, public ESPBTDeviceListener {
|
||||
|
|
|
@ -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:");
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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 <map>
|
||||
#include <vector>
|
||||
|
@ -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[]);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 <vector>
|
||||
|
||||
#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; }
|
||||
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
#include <vector>
|
||||
|
||||
#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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<time::ESPTime> sunrise(time::ESPTime date, num_t zenith) const { return event(true, date, zenith); }
|
||||
optional<time::ESPTime> sunset(time::ESPTime date, num_t zenith) const { return event(false, date, zenith); }
|
||||
optional<time::ESPTime> event(bool rise, time::ESPTime date, num_t zenith) const {
|
||||
optional<ESPTime> sunrise(ESPTime date, num_t zenith) const { return event(true, date, zenith); }
|
||||
optional<ESPTime> sunset(ESPTime date, num_t zenith) const { return event(false, date, zenith); }
|
||||
optional<ESPTime> 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<time::ESPTime> Sun::calc_event_(time::ESPTime date, bool rising, double zenith) {
|
||||
optional<ESPTime> Sun::calc_event_(ESPTime date, bool rising, double zenith) {
|
||||
SunAtLocation sun{location_};
|
||||
if (!date.is_valid())
|
||||
return {};
|
||||
|
@ -301,24 +301,20 @@ optional<time::ESPTime> 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<time::ESPTime> Sun::calc_event_(bool rising, double zenith) {
|
||||
optional<ESPTime> Sun::calc_event_(bool rising, double zenith) {
|
||||
auto it = Sun::calc_event_(this->time_->utcnow(), rising, zenith);
|
||||
return it;
|
||||
}
|
||||
|
||||
optional<time::ESPTime> Sun::sunrise(double elevation) { return this->calc_event_(true, 90 - elevation); }
|
||||
optional<time::ESPTime> Sun::sunset(double elevation) { return this->calc_event_(false, 90 - elevation); }
|
||||
optional<time::ESPTime> Sun::sunrise(time::ESPTime date, double elevation) {
|
||||
return this->calc_event_(date, true, 90 - elevation);
|
||||
}
|
||||
optional<time::ESPTime> Sun::sunset(time::ESPTime date, double elevation) {
|
||||
return this->calc_event_(date, false, 90 - elevation);
|
||||
}
|
||||
optional<ESPTime> Sun::sunrise(double elevation) { return this->calc_event_(true, 90 - elevation); }
|
||||
optional<ESPTime> Sun::sunset(double elevation) { return this->calc_event_(false, 90 - elevation); }
|
||||
optional<ESPTime> Sun::sunrise(ESPTime date, double elevation) { return this->calc_event_(date, true, 90 - elevation); }
|
||||
optional<ESPTime> 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; }
|
||||
|
||||
|
|
|
@ -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<time::ESPTime> sunrise(double elevation);
|
||||
optional<time::ESPTime> sunset(double elevation);
|
||||
optional<time::ESPTime> sunrise(time::ESPTime date, double elevation);
|
||||
optional<time::ESPTime> sunset(time::ESPTime date, double elevation);
|
||||
optional<ESPTime> sunrise(double elevation);
|
||||
optional<ESPTime> sunset(double elevation);
|
||||
optional<ESPTime> sunrise(ESPTime date, double elevation);
|
||||
optional<ESPTime> sunset(ESPTime date, double elevation);
|
||||
|
||||
double elevation();
|
||||
double azimuth();
|
||||
|
||||
protected:
|
||||
internal::HorizontalCoordinate calc_coords_();
|
||||
optional<time::ESPTime> calc_event_(bool rising, double zenith);
|
||||
optional<time::ESPTime> calc_event_(time::ESPTime date, bool rising, double zenith);
|
||||
optional<ESPTime> calc_event_(bool rising, double zenith);
|
||||
optional<ESPTime> calc_event_(ESPTime date, bool rising, double zenith);
|
||||
|
||||
time::RealTimeClock *time_;
|
||||
internal::GeoLocation location_;
|
||||
|
|
|
@ -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<time::ESPTime> res;
|
||||
optional<ESPTime> res;
|
||||
if (this->sunrise_) {
|
||||
res = this->parent_->sunrise(this->elevation_);
|
||||
} else {
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
#include "automation.h"
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <cinttypes>
|
||||
|
||||
namespace esphome {
|
||||
|
|
|
@ -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 <vector>
|
||||
|
|
|
@ -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<typename T> 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
|
||||
|
|
|
@ -1,106 +1,15 @@
|
|||
#pragma once
|
||||
|
||||
#include <bitset>
|
||||
#include <cstdlib>
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include <bitset>
|
||||
#include <cstdlib>
|
||||
#include <ctime>
|
||||
#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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 <vector>
|
||||
|
||||
#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_();
|
||||
|
|
|
@ -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 ////////////////
|
||||
|
||||
|
|
|
@ -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 <vector>
|
||||
|
||||
#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);
|
||||
|
||||
|
|
|
@ -508,7 +508,7 @@ void Tuya::send_wifi_status_() {
|
|||
void Tuya::send_local_time_() {
|
||||
std::vector<uint8_t> 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;
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
#ifdef USE_TIME
|
||||
#include "esphome/components/time/real_time_clock.h"
|
||||
#include "esphome/core/time.h"
|
||||
#endif
|
||||
|
||||
#include <vector>
|
||||
|
|
171
esphome/core/time.cpp
Normal file
171
esphome/core/time.cpp
Normal file
|
@ -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<typename T> 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
|
105
esphome/core/time.h
Normal file
105
esphome/core/time.h
Normal file
|
@ -0,0 +1,105 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdlib>
|
||||
#include <ctime>
|
||||
#include <string>
|
||||
|
||||
namespace esphome {
|
||||
|
||||
template<typename T> 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
|
|
@ -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
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
pillow>4.0.0
|
||||
cairosvg>=2.2.0
|
||||
cryptography>=2.0.0,<4
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue