mirror of
https://github.com/esphome/esphome.git
synced 2026-02-11 10:12:38 +00:00
Compare commits
10 Commits
integratio
...
2026.1.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb2f0ce62f | ||
|
|
a99f75ca71 | ||
|
|
4168e8c30d | ||
|
|
1f761902b6 | ||
|
|
0b047c334d | ||
|
|
a5dc4b0fce | ||
|
|
c1455ccc29 | ||
|
|
438a0c4289 | ||
|
|
9eee4c9924 | ||
|
|
eea7e9edff |
2
Doxyfile
2
Doxyfile
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
||||
# could be handy for archiving the generated documentation or if some version
|
||||
# control system is used.
|
||||
|
||||
PROJECT_NUMBER = 2026.1.4
|
||||
PROJECT_NUMBER = 2026.1.5
|
||||
|
||||
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
||||
# for a project that appears at the top of each page and should give viewer a
|
||||
|
||||
@@ -287,8 +287,13 @@ def has_api() -> bool:
|
||||
|
||||
|
||||
def has_ota() -> bool:
|
||||
"""Check if OTA is available."""
|
||||
return CONF_OTA in CORE.config
|
||||
"""Check if OTA upload is available (requires platform: esphome)."""
|
||||
if CONF_OTA not in CORE.config:
|
||||
return False
|
||||
return any(
|
||||
ota_item.get(CONF_PLATFORM) == CONF_ESPHOME
|
||||
for ota_item in CORE.config[CONF_OTA]
|
||||
)
|
||||
|
||||
|
||||
def has_mqtt_ip_lookup() -> bool:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include "abstract_aqi_calculator.h"
|
||||
@@ -14,7 +15,11 @@ class AQICalculator : public AbstractAQICalculator {
|
||||
float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID);
|
||||
float pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID);
|
||||
|
||||
return static_cast<uint16_t>(std::round((pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index));
|
||||
float aqi = std::max(pm2_5_index, pm10_0_index);
|
||||
if (aqi < 0.0f) {
|
||||
aqi = 0.0f;
|
||||
}
|
||||
return static_cast<uint16_t>(std::lround(aqi));
|
||||
}
|
||||
|
||||
protected:
|
||||
@@ -22,13 +27,27 @@ class AQICalculator : public AbstractAQICalculator {
|
||||
|
||||
static constexpr int INDEX_GRID[NUM_LEVELS][2] = {{0, 50}, {51, 100}, {101, 150}, {151, 200}, {201, 300}, {301, 500}};
|
||||
|
||||
static constexpr float PM2_5_GRID[NUM_LEVELS][2] = {{0.0f, 9.0f}, {9.1f, 35.4f},
|
||||
{35.5f, 55.4f}, {55.5f, 125.4f},
|
||||
{125.5f, 225.4f}, {225.5f, std::numeric_limits<float>::max()}};
|
||||
static constexpr float PM2_5_GRID[NUM_LEVELS][2] = {
|
||||
// clang-format off
|
||||
{0.0f, 9.1f},
|
||||
{9.1f, 35.5f},
|
||||
{35.5f, 55.5f},
|
||||
{55.5f, 125.5f},
|
||||
{125.5f, 225.5f},
|
||||
{225.5f, std::numeric_limits<float>::max()}
|
||||
// clang-format on
|
||||
};
|
||||
|
||||
static constexpr float PM10_0_GRID[NUM_LEVELS][2] = {{0.0f, 54.0f}, {55.0f, 154.0f},
|
||||
{155.0f, 254.0f}, {255.0f, 354.0f},
|
||||
{355.0f, 424.0f}, {425.0f, std::numeric_limits<float>::max()}};
|
||||
static constexpr float PM10_0_GRID[NUM_LEVELS][2] = {
|
||||
// clang-format off
|
||||
{0.0f, 55.0f},
|
||||
{55.0f, 155.0f},
|
||||
{155.0f, 255.0f},
|
||||
{255.0f, 355.0f},
|
||||
{355.0f, 425.0f},
|
||||
{425.0f, std::numeric_limits<float>::max()}
|
||||
// clang-format on
|
||||
};
|
||||
|
||||
static float calculate_index(float value, const float array[NUM_LEVELS][2]) {
|
||||
int grid_index = get_grid_index(value, array);
|
||||
@@ -45,7 +64,10 @@ class AQICalculator : public AbstractAQICalculator {
|
||||
|
||||
static int get_grid_index(float value, const float array[NUM_LEVELS][2]) {
|
||||
for (int i = 0; i < NUM_LEVELS; i++) {
|
||||
if (value >= array[i][0] && value <= array[i][1]) {
|
||||
const bool in_range =
|
||||
(value >= array[i][0]) && ((i == NUM_LEVELS - 1) ? (value <= array[i][1]) // last bucket inclusive
|
||||
: (value < array[i][1])); // others exclusive on hi
|
||||
if (in_range) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include "abstract_aqi_calculator.h"
|
||||
@@ -12,7 +13,11 @@ class CAQICalculator : public AbstractAQICalculator {
|
||||
float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID);
|
||||
float pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID);
|
||||
|
||||
return static_cast<uint16_t>(std::round((pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index));
|
||||
float aqi = std::max(pm2_5_index, pm10_0_index);
|
||||
if (aqi < 0.0f) {
|
||||
aqi = 0.0f;
|
||||
}
|
||||
return static_cast<uint16_t>(std::lround(aqi));
|
||||
}
|
||||
|
||||
protected:
|
||||
@@ -21,10 +26,24 @@ class CAQICalculator : public AbstractAQICalculator {
|
||||
static constexpr int INDEX_GRID[NUM_LEVELS][2] = {{0, 25}, {26, 50}, {51, 75}, {76, 100}, {101, 400}};
|
||||
|
||||
static constexpr float PM2_5_GRID[NUM_LEVELS][2] = {
|
||||
{0.0f, 15.0f}, {15.1f, 30.0f}, {30.1f, 55.0f}, {55.1f, 110.0f}, {110.1f, std::numeric_limits<float>::max()}};
|
||||
// clang-format off
|
||||
{0.0f, 15.1f},
|
||||
{15.1f, 30.1f},
|
||||
{30.1f, 55.1f},
|
||||
{55.1f, 110.1f},
|
||||
{110.1f, std::numeric_limits<float>::max()}
|
||||
// clang-format on
|
||||
};
|
||||
|
||||
static constexpr float PM10_0_GRID[NUM_LEVELS][2] = {
|
||||
{0.0f, 25.0f}, {25.1f, 50.0f}, {50.1f, 90.0f}, {90.1f, 180.0f}, {180.1f, std::numeric_limits<float>::max()}};
|
||||
// clang-format off
|
||||
{0.0f, 25.1f},
|
||||
{25.1f, 50.1f},
|
||||
{50.1f, 90.1f},
|
||||
{90.1f, 180.1f},
|
||||
{180.1f, std::numeric_limits<float>::max()}
|
||||
// clang-format on
|
||||
};
|
||||
|
||||
static float calculate_index(float value, const float array[NUM_LEVELS][2]) {
|
||||
int grid_index = get_grid_index(value, array);
|
||||
@@ -42,7 +61,10 @@ class CAQICalculator : public AbstractAQICalculator {
|
||||
|
||||
static int get_grid_index(float value, const float array[NUM_LEVELS][2]) {
|
||||
for (int i = 0; i < NUM_LEVELS; i++) {
|
||||
if (value >= array[i][0] && value <= array[i][1]) {
|
||||
const bool in_range =
|
||||
(value >= array[i][0]) && ((i == NUM_LEVELS - 1) ? (value <= array[i][1]) // last bucket inclusive
|
||||
: (value < array[i][1])); // others exclusive on hi
|
||||
if (in_range) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1026,6 +1026,10 @@ async def to_code(config):
|
||||
Path(__file__).parent / "iram_fix.py.script",
|
||||
)
|
||||
|
||||
# Set the uv cache inside the data dir so "Clean All" clears it.
|
||||
# Avoids persistent corrupted cache from mid-stream download failures.
|
||||
os.environ["UV_CACHE_DIR"] = str(CORE.relative_internal_path(".uv_cache"))
|
||||
|
||||
if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
|
||||
cg.add_platformio_option("framework", "espidf")
|
||||
cg.add_build_flag("-DUSE_ESP_IDF")
|
||||
|
||||
@@ -68,7 +68,7 @@ void HOT Logger::write_msg_(const char *msg, size_t len) {
|
||||
#ifdef CONFIG_PRINTK
|
||||
// Requires the debug component and an active SWD connection.
|
||||
// It is used for pyocd rtt -t nrf52840
|
||||
k_str_out(const_cast<char *>(msg), len);
|
||||
printk("%.*s", static_cast<int>(len), msg);
|
||||
#endif
|
||||
if (this->uart_dev_ == nullptr) {
|
||||
return;
|
||||
|
||||
@@ -436,6 +436,7 @@ def container_schema(widget_type: WidgetType, extras=None):
|
||||
schema = schema.extend(widget_type.schema)
|
||||
|
||||
def validator(value):
|
||||
value = value or {}
|
||||
return append_layout_schema(schema, value)(value)
|
||||
|
||||
return validator
|
||||
|
||||
@@ -132,18 +132,15 @@ void RD03DComponent::process_frame_() {
|
||||
// Header is 4 bytes, each target is 8 bytes
|
||||
uint8_t offset = FRAME_HEADER_SIZE + (i * TARGET_DATA_SIZE);
|
||||
|
||||
// Extract raw bytes for this target
|
||||
// Note: Despite datasheet Table 5-2 showing order as X, Y, Speed, Resolution,
|
||||
// actual radar output has Resolution before Speed (verified empirically -
|
||||
// stationary targets were showing non-zero speed with original field order)
|
||||
// Extract raw bytes for this target (per datasheet Table 5-2: X, Y, Speed, Resolution)
|
||||
uint8_t x_low = this->buffer_[offset + 0];
|
||||
uint8_t x_high = this->buffer_[offset + 1];
|
||||
uint8_t y_low = this->buffer_[offset + 2];
|
||||
uint8_t y_high = this->buffer_[offset + 3];
|
||||
uint8_t res_low = this->buffer_[offset + 4];
|
||||
uint8_t res_high = this->buffer_[offset + 5];
|
||||
uint8_t speed_low = this->buffer_[offset + 6];
|
||||
uint8_t speed_high = this->buffer_[offset + 7];
|
||||
uint8_t speed_low = this->buffer_[offset + 4];
|
||||
uint8_t speed_high = this->buffer_[offset + 5];
|
||||
uint8_t res_low = this->buffer_[offset + 6];
|
||||
uint8_t res_high = this->buffer_[offset + 7];
|
||||
|
||||
// Decode values per RD-03D format
|
||||
int16_t x = decode_value(x_low, x_high);
|
||||
|
||||
@@ -4,7 +4,7 @@ from enum import Enum
|
||||
|
||||
from esphome.enum import StrEnum
|
||||
|
||||
__version__ = "2026.1.4"
|
||||
__version__ = "2026.1.5"
|
||||
|
||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||
VALID_SUBSTITUTIONS_CHARACTERS = (
|
||||
|
||||
@@ -81,6 +81,10 @@ void Application::register_component_(Component *comp) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (this->components_.size() >= ESPHOME_COMPONENT_COUNT) {
|
||||
ESP_LOGE(TAG, "Cannot register component %s - at capacity!", LOG_STR_ARG(comp->get_component_log_str()));
|
||||
return;
|
||||
}
|
||||
this->components_.push_back(comp);
|
||||
}
|
||||
void Application::setup() {
|
||||
|
||||
@@ -317,6 +317,7 @@ class EsphomeCommandWebSocket(CheckOriginMixin, tornado.websocket.WebSocketHandl
|
||||
# Check if the proc was not forcibly closed
|
||||
_LOGGER.info("Process exited with return code %s", returncode)
|
||||
self.write_message({"event": "exit", "code": returncode})
|
||||
self.close()
|
||||
|
||||
def on_close(self) -> None:
|
||||
# Check if proc exists (if 'start' has been run)
|
||||
|
||||
@@ -20,6 +20,8 @@ lvgl:
|
||||
- id: lvgl_0
|
||||
default_font: space16
|
||||
displays: sdl0
|
||||
top_layer:
|
||||
|
||||
- id: lvgl_1
|
||||
displays: sdl1
|
||||
on_idle:
|
||||
|
||||
@@ -29,7 +29,7 @@ from esphome.dashboard.entries import (
|
||||
bool_to_entry_state,
|
||||
)
|
||||
from esphome.dashboard.models import build_importable_device_dict
|
||||
from esphome.dashboard.web_server import DashboardSubscriber
|
||||
from esphome.dashboard.web_server import DashboardSubscriber, EsphomeCommandWebSocket
|
||||
from esphome.zeroconf import DiscoveredImport
|
||||
|
||||
from .common import get_fixture_path
|
||||
@@ -1654,3 +1654,25 @@ async def test_websocket_check_origin_multiple_trusted_domains(
|
||||
assert data["event"] == "initial_state"
|
||||
finally:
|
||||
ws.close()
|
||||
|
||||
|
||||
def test_proc_on_exit_calls_close() -> None:
|
||||
"""Test _proc_on_exit sends exit event and closes the WebSocket."""
|
||||
handler = Mock(spec=EsphomeCommandWebSocket)
|
||||
handler._is_closed = False
|
||||
|
||||
EsphomeCommandWebSocket._proc_on_exit(handler, 0)
|
||||
|
||||
handler.write_message.assert_called_once_with({"event": "exit", "code": 0})
|
||||
handler.close.assert_called_once()
|
||||
|
||||
|
||||
def test_proc_on_exit_skips_when_already_closed() -> None:
|
||||
"""Test _proc_on_exit does nothing when WebSocket is already closed."""
|
||||
handler = Mock(spec=EsphomeCommandWebSocket)
|
||||
handler._is_closed = True
|
||||
|
||||
EsphomeCommandWebSocket._proc_on_exit(handler, 0)
|
||||
|
||||
handler.write_message.assert_not_called()
|
||||
handler.close.assert_not_called()
|
||||
|
||||
@@ -32,6 +32,7 @@ from esphome.__main__ import (
|
||||
has_mqtt_ip_lookup,
|
||||
has_mqtt_logging,
|
||||
has_non_ip_address,
|
||||
has_ota,
|
||||
has_resolvable_address,
|
||||
mqtt_get_ip,
|
||||
run_esphome,
|
||||
@@ -332,7 +333,9 @@ def test_choose_upload_log_host_with_mixed_hostnames_and_ips() -> None:
|
||||
|
||||
def test_choose_upload_log_host_with_ota_list() -> None:
|
||||
"""Test with OTA as the only item in the list."""
|
||||
setup_core(config={CONF_OTA: {}}, address="192.168.1.100")
|
||||
setup_core(
|
||||
config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]}, address="192.168.1.100"
|
||||
)
|
||||
|
||||
result = choose_upload_log_host(
|
||||
default=["OTA"],
|
||||
@@ -345,7 +348,7 @@ def test_choose_upload_log_host_with_ota_list() -> None:
|
||||
@pytest.mark.usefixtures("mock_has_mqtt_logging")
|
||||
def test_choose_upload_log_host_with_ota_list_mqtt_fallback() -> None:
|
||||
"""Test with OTA list falling back to MQTT when no address."""
|
||||
setup_core(config={CONF_OTA: {}, "mqtt": {}})
|
||||
setup_core(config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}], "mqtt": {}})
|
||||
|
||||
result = choose_upload_log_host(
|
||||
default=["OTA"],
|
||||
@@ -408,7 +411,9 @@ def test_choose_upload_log_host_with_serial_device_with_ports(
|
||||
|
||||
def test_choose_upload_log_host_with_ota_device_with_ota_config() -> None:
|
||||
"""Test OTA device when OTA is configured."""
|
||||
setup_core(config={CONF_OTA: {}}, address="192.168.1.100")
|
||||
setup_core(
|
||||
config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]}, address="192.168.1.100"
|
||||
)
|
||||
|
||||
result = choose_upload_log_host(
|
||||
default="OTA",
|
||||
@@ -475,7 +480,9 @@ def test_choose_upload_log_host_with_ota_device_no_fallback() -> None:
|
||||
@pytest.mark.usefixtures("mock_choose_prompt")
|
||||
def test_choose_upload_log_host_multiple_devices() -> None:
|
||||
"""Test with multiple devices including special identifiers."""
|
||||
setup_core(config={CONF_OTA: {}}, address="192.168.1.100")
|
||||
setup_core(
|
||||
config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]}, address="192.168.1.100"
|
||||
)
|
||||
|
||||
mock_ports = [MockSerialPort("/dev/ttyUSB0", "USB Serial")]
|
||||
|
||||
@@ -514,7 +521,9 @@ def test_choose_upload_log_host_no_defaults_with_serial_ports(
|
||||
@pytest.mark.usefixtures("mock_no_serial_ports")
|
||||
def test_choose_upload_log_host_no_defaults_with_ota() -> None:
|
||||
"""Test interactive mode with OTA option."""
|
||||
setup_core(config={CONF_OTA: {}}, address="192.168.1.100")
|
||||
setup_core(
|
||||
config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]}, address="192.168.1.100"
|
||||
)
|
||||
|
||||
with patch(
|
||||
"esphome.__main__.choose_prompt", return_value="192.168.1.100"
|
||||
@@ -575,7 +584,11 @@ def test_choose_upload_log_host_no_defaults_with_all_options(
|
||||
) -> None:
|
||||
"""Test interactive mode with all options available."""
|
||||
setup_core(
|
||||
config={CONF_OTA: {}, CONF_API: {}, CONF_MQTT: {CONF_BROKER: "mqtt.local"}},
|
||||
config={
|
||||
CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}],
|
||||
CONF_API: {},
|
||||
CONF_MQTT: {CONF_BROKER: "mqtt.local"},
|
||||
},
|
||||
address="192.168.1.100",
|
||||
)
|
||||
|
||||
@@ -604,7 +617,11 @@ def test_choose_upload_log_host_no_defaults_with_all_options_logging(
|
||||
) -> None:
|
||||
"""Test interactive mode with all options available."""
|
||||
setup_core(
|
||||
config={CONF_OTA: {}, CONF_API: {}, CONF_MQTT: {CONF_BROKER: "mqtt.local"}},
|
||||
config={
|
||||
CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}],
|
||||
CONF_API: {},
|
||||
CONF_MQTT: {CONF_BROKER: "mqtt.local"},
|
||||
},
|
||||
address="192.168.1.100",
|
||||
)
|
||||
|
||||
@@ -632,7 +649,9 @@ def test_choose_upload_log_host_no_defaults_with_all_options_logging(
|
||||
@pytest.mark.usefixtures("mock_no_serial_ports")
|
||||
def test_choose_upload_log_host_check_default_matches() -> None:
|
||||
"""Test when check_default matches an available option."""
|
||||
setup_core(config={CONF_OTA: {}}, address="192.168.1.100")
|
||||
setup_core(
|
||||
config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]}, address="192.168.1.100"
|
||||
)
|
||||
|
||||
result = choose_upload_log_host(
|
||||
default=None,
|
||||
@@ -704,7 +723,10 @@ def test_choose_upload_log_host_mixed_resolved_unresolved() -> None:
|
||||
|
||||
def test_choose_upload_log_host_ota_both_conditions() -> None:
|
||||
"""Test OTA device when both OTA and API are configured and enabled."""
|
||||
setup_core(config={CONF_OTA: {}, CONF_API: {}}, address="192.168.1.100")
|
||||
setup_core(
|
||||
config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}], CONF_API: {}},
|
||||
address="192.168.1.100",
|
||||
)
|
||||
|
||||
result = choose_upload_log_host(
|
||||
default="OTA",
|
||||
@@ -719,7 +741,7 @@ def test_choose_upload_log_host_ota_ip_all_options() -> None:
|
||||
"""Test OTA device when both static IP, OTA, API and MQTT are configured and enabled but MDNS not."""
|
||||
setup_core(
|
||||
config={
|
||||
CONF_OTA: {},
|
||||
CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}],
|
||||
CONF_API: {},
|
||||
CONF_MQTT: {
|
||||
CONF_BROKER: "mqtt.local",
|
||||
@@ -744,7 +766,7 @@ def test_choose_upload_log_host_ota_local_all_options() -> None:
|
||||
"""Test OTA device when both static IP, OTA, API and MQTT are configured and enabled but MDNS not."""
|
||||
setup_core(
|
||||
config={
|
||||
CONF_OTA: {},
|
||||
CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}],
|
||||
CONF_API: {},
|
||||
CONF_MQTT: {
|
||||
CONF_BROKER: "mqtt.local",
|
||||
@@ -769,7 +791,7 @@ def test_choose_upload_log_host_ota_ip_all_options_logging() -> None:
|
||||
"""Test OTA device when both static IP, OTA, API and MQTT are configured and enabled but MDNS not."""
|
||||
setup_core(
|
||||
config={
|
||||
CONF_OTA: {},
|
||||
CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}],
|
||||
CONF_API: {},
|
||||
CONF_MQTT: {
|
||||
CONF_BROKER: "mqtt.local",
|
||||
@@ -794,7 +816,7 @@ def test_choose_upload_log_host_ota_local_all_options_logging() -> None:
|
||||
"""Test OTA device when both static IP, OTA, API and MQTT are configured and enabled but MDNS not."""
|
||||
setup_core(
|
||||
config={
|
||||
CONF_OTA: {},
|
||||
CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}],
|
||||
CONF_API: {},
|
||||
CONF_MQTT: {
|
||||
CONF_BROKER: "mqtt.local",
|
||||
@@ -817,7 +839,7 @@ def test_choose_upload_log_host_ota_local_all_options_logging() -> None:
|
||||
@pytest.mark.usefixtures("mock_no_mqtt_logging")
|
||||
def test_choose_upload_log_host_no_address_with_ota_config() -> None:
|
||||
"""Test OTA device when OTA is configured but no address is set."""
|
||||
setup_core(config={CONF_OTA: {}})
|
||||
setup_core(config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]})
|
||||
|
||||
with pytest.raises(
|
||||
EsphomeError, match="All specified devices .* could not be resolved"
|
||||
@@ -1532,10 +1554,43 @@ def test_has_mqtt() -> None:
|
||||
assert has_mqtt() is False
|
||||
|
||||
# Test with other components but no MQTT
|
||||
setup_core(config={CONF_API: {}, CONF_OTA: {}})
|
||||
setup_core(config={CONF_API: {}, CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]})
|
||||
assert has_mqtt() is False
|
||||
|
||||
|
||||
def test_has_ota() -> None:
|
||||
"""Test has_ota function.
|
||||
|
||||
The has_ota function should only return True when OTA is configured
|
||||
with platform: esphome, not when only platform: http_request is configured.
|
||||
This is because CLI OTA upload only works with the esphome platform.
|
||||
"""
|
||||
# Test with OTA esphome platform configured
|
||||
setup_core(config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]})
|
||||
assert has_ota() is True
|
||||
|
||||
# Test with OTA http_request platform only (should return False)
|
||||
# This is the bug scenario from issue #13783
|
||||
setup_core(config={CONF_OTA: [{CONF_PLATFORM: "http_request"}]})
|
||||
assert has_ota() is False
|
||||
|
||||
# Test without OTA configured
|
||||
setup_core(config={})
|
||||
assert has_ota() is False
|
||||
|
||||
# Test with multiple OTA platforms including esphome
|
||||
setup_core(
|
||||
config={
|
||||
CONF_OTA: [{CONF_PLATFORM: "http_request"}, {CONF_PLATFORM: CONF_ESPHOME}]
|
||||
}
|
||||
)
|
||||
assert has_ota() is True
|
||||
|
||||
# Test with empty OTA list
|
||||
setup_core(config={CONF_OTA: []})
|
||||
assert has_ota() is False
|
||||
|
||||
|
||||
def test_get_port_type() -> None:
|
||||
"""Test get_port_type function."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user