mirror of
https://github.com/esphome/esphome.git
synced 2026-09-17 10:08:40 +00:00
Merge remote-tracking branch 'upstream/dev' into integration
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
|
||||
CONF_BYTE_ORDER = "byte_order"
|
||||
CONF_CLIMATE_ID = "climate_id"
|
||||
BYTE_ORDER_LITTLE = "little_endian"
|
||||
BYTE_ORDER_BIG = "big_endian"
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
from esphome.components.const import CONF_CLIMATE_ID
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_TYPE, ICON_GAUGE, STATE_CLASS_MEASUREMENT, UNIT_PERCENT
|
||||
|
||||
@@ -21,7 +22,6 @@ PID_CLIMATE_SENSOR_TYPES = {
|
||||
"KD": PIDClimateSensorType.PID_SENSOR_TYPE_KD,
|
||||
}
|
||||
|
||||
CONF_CLIMATE_ID = "climate_id"
|
||||
CONFIG_SCHEMA = (
|
||||
sensor.sensor_schema(
|
||||
PIDClimateSensor,
|
||||
|
||||
@@ -26,6 +26,10 @@ class BmpDecoder : public ImageDecoder {
|
||||
int HOT decode(uint8_t *buffer, size_t size) override;
|
||||
|
||||
bool is_finished() const override {
|
||||
if (this->bits_per_pixel_ == 0) {
|
||||
// header not yet received, so dimensions not yet determined
|
||||
return false;
|
||||
}
|
||||
// BMP is finished when we've decoded all pixel data
|
||||
return this->paint_index_ >= static_cast<size_t>(this->width_ * this->height_);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#include <type_traits>
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#if (ESP_IDF_VERSION_MAJOR >= 5 && ESP_IDF_VERSION_MINOR >= 1)
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
|
||||
#include <esp_eap_client.h>
|
||||
#else
|
||||
#include <esp_wpa2.h>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
#endif
|
||||
|
||||
#if defined(USE_ESP32) && defined(USE_WIFI_WPA2_EAP)
|
||||
#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1)
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
|
||||
#include <esp_eap_client.h>
|
||||
#else
|
||||
#include <esp_wpa2.h>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
#ifdef USE_WIFI_WPA2_EAP
|
||||
#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1)
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
|
||||
#include <esp_eap_client.h>
|
||||
#else
|
||||
#include <esp_wpa2.h>
|
||||
@@ -75,7 +75,11 @@ struct IDFWiFiEvent {
|
||||
#if USE_NETWORK_IPV6
|
||||
ip_event_got_ip6_t ip_got_ip6;
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
|
||||
ip_event_assigned_ip_to_client_t ip_assigned_ip_to_client;
|
||||
#else
|
||||
ip_event_ap_staipassigned_t ip_ap_staipassigned;
|
||||
#endif
|
||||
} data;
|
||||
};
|
||||
|
||||
@@ -116,8 +120,13 @@ void event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, voi
|
||||
memcpy(&event.data.ap_staconnected, event_data, sizeof(wifi_event_ap_staconnected_t));
|
||||
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_STADISCONNECTED) {
|
||||
memcpy(&event.data.ap_stadisconnected, event_data, sizeof(wifi_event_ap_stadisconnected_t));
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
|
||||
} else if (event_base == IP_EVENT && event_id == IP_EVENT_ASSIGNED_IP_TO_CLIENT) {
|
||||
memcpy(&event.data.ip_assigned_ip_to_client, event_data, sizeof(ip_event_assigned_ip_to_client_t));
|
||||
#else
|
||||
} else if (event_base == IP_EVENT && event_id == IP_EVENT_AP_STAIPASSIGNED) {
|
||||
memcpy(&event.data.ip_ap_staipassigned, event_data, sizeof(ip_event_ap_staipassigned_t));
|
||||
#endif
|
||||
} else {
|
||||
// did not match any event, don't send anything
|
||||
return;
|
||||
@@ -407,7 +416,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
||||
if (eap_opt.has_value()) {
|
||||
// note: all certificates and keys have to be null terminated. Lengths are appended by +1 to include \0.
|
||||
EAPAuth eap = *eap_opt;
|
||||
#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1)
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
|
||||
err = esp_eap_client_set_identity((uint8_t *) eap.identity.c_str(), eap.identity.length());
|
||||
#else
|
||||
err = esp_wifi_sta_wpa2_ent_set_identity((uint8_t *) eap.identity.c_str(), eap.identity.length());
|
||||
@@ -419,7 +428,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
||||
int client_cert_len = strlen(eap.client_cert);
|
||||
int client_key_len = strlen(eap.client_key);
|
||||
if (ca_cert_len) {
|
||||
#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1)
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
|
||||
err = esp_eap_client_set_ca_cert((uint8_t *) eap.ca_cert, ca_cert_len + 1);
|
||||
#else
|
||||
err = esp_wifi_sta_wpa2_ent_set_ca_cert((uint8_t *) eap.ca_cert, ca_cert_len + 1);
|
||||
@@ -432,7 +441,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
||||
// validation is not required as the config tool has already validated it
|
||||
if (client_cert_len && client_key_len) {
|
||||
// if we have certs, this must be EAP-TLS
|
||||
#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1)
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
|
||||
err = esp_eap_client_set_certificate_and_key((uint8_t *) eap.client_cert, client_cert_len + 1,
|
||||
(uint8_t *) eap.client_key, client_key_len + 1,
|
||||
(uint8_t *) eap.password.c_str(), eap.password.length());
|
||||
@@ -446,7 +455,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
||||
}
|
||||
} else {
|
||||
// in the absence of certs, assume this is username/password based
|
||||
#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1)
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
|
||||
err = esp_eap_client_set_username((uint8_t *) eap.username.c_str(), eap.username.length());
|
||||
#else
|
||||
err = esp_wifi_sta_wpa2_ent_set_username((uint8_t *) eap.username.c_str(), eap.username.length());
|
||||
@@ -454,7 +463,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGV(TAG, "set_username failed %d", err);
|
||||
}
|
||||
#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1)
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
|
||||
err = esp_eap_client_set_password((uint8_t *) eap.password.c_str(), eap.password.length());
|
||||
#else
|
||||
err = esp_wifi_sta_wpa2_ent_set_password((uint8_t *) eap.password.c_str(), eap.password.length());
|
||||
@@ -463,7 +472,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
||||
ESP_LOGV(TAG, "set_password failed %d", err);
|
||||
}
|
||||
// set TTLS Phase 2, defaults to MSCHAPV2
|
||||
#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1)
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
|
||||
err = esp_eap_client_set_ttls_phase2_method(eap.ttls_phase_2);
|
||||
#else
|
||||
err = esp_wifi_sta_wpa2_ent_set_ttls_phase2_method(eap.ttls_phase_2);
|
||||
@@ -472,7 +481,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
||||
ESP_LOGV(TAG, "set_ttls_phase2_method failed %d", err);
|
||||
}
|
||||
}
|
||||
#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1)
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
|
||||
err = esp_wifi_sta_enterprise_enable();
|
||||
#else
|
||||
err = esp_wifi_sta_wpa2_ent_enable();
|
||||
@@ -628,14 +637,26 @@ const char *get_disconnect_reason_str(uint8_t reason) {
|
||||
return "Auth Expired";
|
||||
case WIFI_REASON_AUTH_LEAVE:
|
||||
return "Auth Leave";
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
|
||||
case WIFI_REASON_DISASSOC_DUE_TO_INACTIVITY:
|
||||
return "Disassociated Due to Inactivity";
|
||||
#else
|
||||
case WIFI_REASON_ASSOC_EXPIRE:
|
||||
return "Association Expired";
|
||||
#endif
|
||||
case WIFI_REASON_ASSOC_TOOMANY:
|
||||
return "Too Many Associations";
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
|
||||
case WIFI_REASON_CLASS2_FRAME_FROM_NONAUTH_STA:
|
||||
return "Class 2 Frame from Non-Authenticated STA";
|
||||
case WIFI_REASON_CLASS3_FRAME_FROM_NONASSOC_STA:
|
||||
return "Class 3 Frame from Non-Associated STA";
|
||||
#else
|
||||
case WIFI_REASON_NOT_AUTHED:
|
||||
return "Not Authenticated";
|
||||
case WIFI_REASON_NOT_ASSOCED:
|
||||
return "Not Associated";
|
||||
#endif
|
||||
case WIFI_REASON_ASSOC_LEAVE:
|
||||
return "Association Leave";
|
||||
case WIFI_REASON_ASSOC_NOT_AUTHED:
|
||||
@@ -688,7 +709,7 @@ const char *get_disconnect_reason_str(uint8_t reason) {
|
||||
return "Association comeback time too long";
|
||||
case WIFI_REASON_SA_QUERY_TIMEOUT:
|
||||
return "SA query timeout";
|
||||
#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 2)
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 2, 0)
|
||||
case WIFI_REASON_NO_AP_FOUND_W_COMPATIBLE_SECURITY:
|
||||
return "No AP found with compatible security";
|
||||
case WIFI_REASON_NO_AP_FOUND_IN_AUTHMODE_THRESHOLD:
|
||||
@@ -917,8 +938,13 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
|
||||
ESP_LOGV(TAG, "AP client disconnected MAC=%s", mac_buf);
|
||||
#endif
|
||||
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
|
||||
} else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_ASSIGNED_IP_TO_CLIENT) {
|
||||
const auto &it = data->data.ip_assigned_ip_to_client;
|
||||
#else
|
||||
} else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_AP_STAIPASSIGNED) {
|
||||
const auto &it = data->data.ip_ap_staipassigned;
|
||||
#endif
|
||||
ESP_LOGV(TAG, "AP client assigned IP " IPSTR, IP2STR(&it.ip));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -589,7 +589,10 @@ async def _add_looping_components() -> None:
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_global(cg.global_ns.namespace("esphome").using)
|
||||
# These can be used by user lambdas, put them to default scope
|
||||
# picolibc (IDF 6.0+) declares isnan in global scope, conflicting with using std::isnan
|
||||
cg.add_global(cg.RawStatement("#ifndef __PICOLIBC__"))
|
||||
cg.add_global(cg.RawExpression("using std::isnan"))
|
||||
cg.add_global(cg.RawStatement("#endif"))
|
||||
cg.add_global(cg.RawExpression("using std::min"))
|
||||
cg.add_global(cg.RawExpression("using std::max"))
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
esphome:
|
||||
name: online-image-bmp
|
||||
|
||||
host:
|
||||
|
||||
http_request:
|
||||
|
||||
display:
|
||||
|
||||
online_image:
|
||||
- url: http://127.0.0.1:HTTP_PORT/foo.bmp
|
||||
id: myimg
|
||||
format: BMP
|
||||
type: RGB
|
||||
on_download_finished:
|
||||
logger.log:
|
||||
format: "download finished. cache hit: %u"
|
||||
args: [cached]
|
||||
|
||||
api:
|
||||
actions:
|
||||
- action: fetch_image
|
||||
then:
|
||||
- component.update: myimg
|
||||
|
||||
logger:
|
||||
level: DEBUG
|
||||
@@ -0,0 +1,119 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
# black 8x8 RGB BMP, generated with
|
||||
# from PIL import Image
|
||||
# from io import BytesIO
|
||||
# b = BytesIO()
|
||||
# img = Image.new("RGB", (8, 8))
|
||||
# img.save(b, format="BMP")
|
||||
# b.getvalue()
|
||||
BMP_IMAGE = b"BM\xf6\x00\x00\x00\x00\x00\x00\x006\x00\x00\x00(\x00\x00\x00\x08\x00\x00\x00\x08\x00\x00\x00\x01\x00\x18\x00\x00\x00\x00\x00\xc0\x00\x00\x00\xc4\x0e\x00\x00\xc4\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
LEN_BMP_IMAGE = len(BMP_IMAGE)
|
||||
|
||||
|
||||
def handle_http(http_request_future):
|
||||
async def handler(reader, writer):
|
||||
try:
|
||||
async with asyncio.timeout(1.0):
|
||||
data = await reader.readuntil(b"\r\n")
|
||||
|
||||
# ensure our request matches the expectation
|
||||
expected_request = b"GET /foo.bmp HTTP/1.1\r\n"
|
||||
assert data[: len(expected_request)] == expected_request
|
||||
|
||||
# consume rest of request
|
||||
async with asyncio.timeout(1.0):
|
||||
data = await reader.readuntil(b"\r\n\r\n")
|
||||
|
||||
http_request_future.set_result(True)
|
||||
|
||||
http_response = [
|
||||
b"HTTP/1.1 200 OK",
|
||||
b"Content-Length: %d" % LEN_BMP_IMAGE,
|
||||
b"Content-Type: text/plain",
|
||||
b"Connection: close",
|
||||
b"",
|
||||
b"",
|
||||
]
|
||||
writer.write(b"\r\n".join(http_response))
|
||||
await writer.drain()
|
||||
|
||||
writer.write(BMP_IMAGE)
|
||||
|
||||
await writer.drain()
|
||||
except Exception as exc:
|
||||
if not http_request_future.done():
|
||||
http_request_future.set_exception(exc)
|
||||
raise
|
||||
finally:
|
||||
writer.close()
|
||||
|
||||
return handler
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_online_image_bmp(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Esphome shouldn't block the main loop when a http response is slow"""
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
# Track http request
|
||||
http_request_future = loop.create_future()
|
||||
download_finished_future = loop.create_future()
|
||||
downloaded_bytes_future = loop.create_future()
|
||||
|
||||
def check_output(line: str) -> None:
|
||||
"""Check log output for expected messages."""
|
||||
|
||||
if match := re.search(r"Image fully downloaded, (\d+) bytes", line):
|
||||
downloaded_bytes_future.set_result(int(match.group(1)))
|
||||
|
||||
if "download finished" in line:
|
||||
download_finished_future.set_result(True)
|
||||
|
||||
server = await asyncio.start_server(
|
||||
handle_http(http_request_future), "127.0.0.1", 0
|
||||
)
|
||||
http_server_port = server.sockets[0].getsockname()[1]
|
||||
|
||||
config = yaml_config.replace("HTTP_PORT", str(http_server_port))
|
||||
|
||||
# Run with log monitoring
|
||||
async with (
|
||||
server,
|
||||
run_compiled(config, line_callback=check_output),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
# Verify device info
|
||||
|
||||
device_info = await client.device_info()
|
||||
assert device_info is not None
|
||||
assert device_info.name == "online-image-bmp"
|
||||
|
||||
# List services to find our test service
|
||||
_, services = await client.list_entities_services()
|
||||
|
||||
# Find test service
|
||||
request_service = next((s for s in services if s.name == "fetch_image"), None)
|
||||
|
||||
assert request_service is not None, "fetch_image service not found"
|
||||
|
||||
await client.execute_service(request_service, {})
|
||||
|
||||
async with asyncio.timeout(0.1):
|
||||
await http_request_future
|
||||
|
||||
async with asyncio.timeout(0.5):
|
||||
numbytes = await downloaded_bytes_future
|
||||
assert numbytes == LEN_BMP_IMAGE
|
||||
await download_finished_future
|
||||
Reference in New Issue
Block a user