mirror of
https://github.com/esphome/esphome.git
synced 2026-10-02 01:10:22 +00:00
Merge branch 'dev' into ota-idf-lazy-erase
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# The scan list helper is header-only and needs none of the component's real
|
||||
# dependencies. Pulling them in breaks the host build: web_server_base
|
||||
# includes ESPAsyncWebServer.h and ota.web_server includes md5/md5.h, neither
|
||||
# of which exists there.
|
||||
manifest.dependencies = []
|
||||
manifest.auto_load = []
|
||||
@@ -0,0 +1,130 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/components/captive_portal/scan_list.h"
|
||||
|
||||
namespace esphome::captive_portal::testing {
|
||||
|
||||
namespace {
|
||||
|
||||
// Stand-in for wifi::WiFiScanResult, which does not compile on the host.
|
||||
struct Entry {
|
||||
std::string ssid;
|
||||
int8_t rssi;
|
||||
bool with_auth{true};
|
||||
bool is_hidden{false};
|
||||
|
||||
// Compares length and bytes like CompactString does, so an embedded NUL counts.
|
||||
bool ssid_equals(const Entry &other) const { return this->ssid == other.ssid; }
|
||||
int8_t get_rssi() const { return this->rssi; }
|
||||
bool get_with_auth() const { return this->with_auth; }
|
||||
bool get_is_hidden() const { return this->is_hidden; }
|
||||
};
|
||||
|
||||
// One row as the portal would emit it.
|
||||
struct Row {
|
||||
std::string ssid;
|
||||
int8_t rssi;
|
||||
bool lock;
|
||||
|
||||
bool operator==(const Row &rhs) const { return ssid == rhs.ssid && rssi == rhs.rssi && lock == rhs.lock; }
|
||||
};
|
||||
|
||||
// Walk the results the way handle_config does and collect the rows that survive.
|
||||
std::vector<Row> rows(const std::vector<Entry> &results) {
|
||||
std::vector<Row> out;
|
||||
for (size_t i = 0; i < results.size(); i++) {
|
||||
bool with_auth = false;
|
||||
if (!should_show_scan_entry(results, results[i], with_auth))
|
||||
continue;
|
||||
out.push_back({results[i].ssid, results[i].rssi, with_auth});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(ScanList, SingleEntryShown) {
|
||||
std::vector<Entry> results = {{"Home", -60}};
|
||||
EXPECT_EQ(rows(results), (std::vector<Row>{{"Home", -60, true}}));
|
||||
}
|
||||
|
||||
TEST(ScanList, DistinctSsidsAllShownInOrder) {
|
||||
std::vector<Entry> results = {{"Home", -60}, {"Guest", -70}, {"Cafe", -40}};
|
||||
EXPECT_EQ(rows(results), (std::vector<Row>{{"Home", -60, true}, {"Guest", -70, true}, {"Cafe", -40, true}}));
|
||||
}
|
||||
|
||||
// Results are ordered by connection preference, not RSSI, so the strongest entry
|
||||
// can sit anywhere in the list.
|
||||
TEST(ScanList, SameSsidKeepsStrongest) {
|
||||
std::vector<Entry> results = {{"Home", -70}, {"Home", -50}, {"Home", -60}};
|
||||
EXPECT_EQ(rows(results), (std::vector<Row>{{"Home", -50, true}}));
|
||||
}
|
||||
|
||||
TEST(ScanList, EqualRssiKeepsFirst) {
|
||||
std::vector<Entry> results = {{"Home", -60}, {"Home", -60}, {"Home", -60}};
|
||||
bool with_auth = false;
|
||||
EXPECT_TRUE(should_show_scan_entry(results, results[0], with_auth));
|
||||
EXPECT_FALSE(should_show_scan_entry(results, results[1], with_auth));
|
||||
EXPECT_FALSE(should_show_scan_entry(results, results[2], with_auth));
|
||||
EXPECT_EQ(rows(results), (std::vector<Row>{{"Home", -60, true}}));
|
||||
}
|
||||
|
||||
// with_auth is an out-parameter that must only be written for a shown entry.
|
||||
TEST(ScanList, WithAuthUntouchedWhenNotShown) {
|
||||
std::vector<Entry> results = {{"Home", -50, false}, {"Home", -70, true}};
|
||||
bool with_auth = false;
|
||||
EXPECT_FALSE(should_show_scan_entry(results, results[1], with_auth));
|
||||
EXPECT_FALSE(with_auth);
|
||||
}
|
||||
|
||||
TEST(ScanList, DuplicatesInterleavedWithOtherNetworks) {
|
||||
std::vector<Entry> results = {{"Home", -70}, {"Guest", -55}, {"Home", -50}, {"Guest", -65}};
|
||||
EXPECT_EQ(rows(results), (std::vector<Row>{{"Guest", -55, true}, {"Home", -50, true}}));
|
||||
}
|
||||
|
||||
// Hidden networks scan with an empty SSID. They are never listed and do not
|
||||
// collapse into each other or into anything else.
|
||||
TEST(ScanList, HiddenEntriesNeverShown) {
|
||||
std::vector<Entry> results = {{"", -40, true, true}, {"Home", -70}, {"", -30, true, true}};
|
||||
EXPECT_EQ(rows(results), (std::vector<Row>{{"Home", -70, true}}));
|
||||
}
|
||||
|
||||
// On ESP8266 the hidden flag comes from the driver alongside a real SSID, so a
|
||||
// hidden access point can share its name with a visible one. It must not
|
||||
// outrank that visible entry and leave the network unlisted.
|
||||
TEST(ScanList, HiddenEntryDoesNotSuppressVisibleSameSsid) {
|
||||
std::vector<Entry> results = {{"Home", -40, true, true}, {"Home", -70}};
|
||||
EXPECT_EQ(rows(results), (std::vector<Row>{{"Home", -70, true}}));
|
||||
}
|
||||
|
||||
// An open access point and a secured one sharing an SSID collapse to one row that
|
||||
// still asks for a password, whichever of them is strongest.
|
||||
TEST(ScanList, LockSetWhenAnyEntryRequiresAuth) {
|
||||
std::vector<Entry> open_stronger = {{"Home", -50, false}, {"Home", -70, true}};
|
||||
EXPECT_EQ(rows(open_stronger), (std::vector<Row>{{"Home", -50, true}}));
|
||||
|
||||
std::vector<Entry> secured_stronger = {{"Home", -70, false}, {"Home", -50, true}};
|
||||
EXPECT_EQ(rows(secured_stronger), (std::vector<Row>{{"Home", -50, true}}));
|
||||
}
|
||||
|
||||
TEST(ScanList, LockClearWhenEveryEntryIsOpen) {
|
||||
std::vector<Entry> results = {{"Cafe", -60, false}, {"Cafe", -50, false}};
|
||||
EXPECT_EQ(rows(results), (std::vector<Row>{{"Cafe", -50, false}}));
|
||||
}
|
||||
|
||||
// The auth flag of an unrelated network must not leak into another SSID's row.
|
||||
TEST(ScanList, LockIsPerSsid) {
|
||||
std::vector<Entry> results = {{"Cafe", -60, false}, {"Home", -50, true}};
|
||||
EXPECT_EQ(rows(results), (std::vector<Row>{{"Cafe", -60, false}, {"Home", -50, true}}));
|
||||
}
|
||||
|
||||
TEST(ScanList, EmptyListShowsNothing) {
|
||||
std::vector<Entry> results;
|
||||
EXPECT_TRUE(rows(results).empty());
|
||||
}
|
||||
|
||||
} // namespace esphome::captive_portal::testing
|
||||
@@ -1,4 +1,3 @@
|
||||
packages:
|
||||
uart_115200: !include ../../test_build_components/common/uart_115200/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
emontx: !include common.yaml
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
packages:
|
||||
uart_115200: !include ../../test_build_components/common/uart_115200/esp8266-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
emontx: !include common.yaml
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
packages:
|
||||
uart_115200: !include ../../test_build_components/common/uart_115200/rp2040-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
emontx: !include common.yaml
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
packages:
|
||||
uart_115200: !include ../../test_build_components/common/uart_115200/esp32-idf.yaml
|
||||
emontx: !include common.yaml
|
||||
|
||||
# Validate that each sensor type gets the correct default state_class,
|
||||
# unit_of_measurement, device_class, and accuracy_decimals when NO overrides
|
||||
# are provided. The values are intentionally omitted so apply_tag_defaults is
|
||||
# exercised, not the user-override path.
|
||||
|
||||
sensor:
|
||||
# Energy sensor (E prefix): expects state_class=total_increasing, unit=Wh,
|
||||
# device_class=energy, accuracy_decimals=0
|
||||
- platform: emontx
|
||||
tag_name: E1
|
||||
name: Energy 1
|
||||
emontx_id: test_emontx
|
||||
|
||||
# Power sensor (P prefix): expects state_class=measurement, unit=W,
|
||||
# device_class=power, accuracy_decimals=0
|
||||
- platform: emontx
|
||||
tag_name: P1
|
||||
name: Power 1
|
||||
emontx_id: test_emontx
|
||||
|
||||
# Voltage sensor (V prefix): expects state_class=measurement, unit=V,
|
||||
# device_class=voltage, accuracy_decimals=2
|
||||
- platform: emontx
|
||||
tag_name: V1
|
||||
name: Voltage 1
|
||||
emontx_id: test_emontx
|
||||
|
||||
# Current sensor (I prefix): expects state_class=measurement, unit=A,
|
||||
# device_class=current, accuracy_decimals=2
|
||||
- platform: emontx
|
||||
tag_name: I1
|
||||
name: Current 1
|
||||
emontx_id: test_emontx
|
||||
|
||||
# Temperature sensor (T prefix): expects state_class=measurement, unit=°C,
|
||||
# device_class=temperature, accuracy_decimals=2
|
||||
- platform: emontx
|
||||
tag_name: T1
|
||||
name: Temperature 1
|
||||
emontx_id: test_emontx
|
||||
|
||||
# Pulse sensor (PULSE pattern): expects state_class=total_increasing,
|
||||
# unit=pulses, device_class=energy, accuracy_decimals=0
|
||||
- platform: emontx
|
||||
tag_name: PULSE1
|
||||
name: Pulse 1
|
||||
emontx_id: test_emontx
|
||||
|
||||
# Power factor sensor (PF pattern): expects state_class=measurement,
|
||||
# device_class=power_factor, accuracy_decimals=2
|
||||
- platform: emontx
|
||||
tag_name: PF1
|
||||
name: Power Factor 1
|
||||
emontx_id: test_emontx
|
||||
|
||||
# Unknown tag: no prefix match, falls back to state_class=measurement,
|
||||
# accuracy_decimals=0
|
||||
- platform: emontx
|
||||
tag_name: CUSTOM1
|
||||
name: Custom sensor
|
||||
emontx_id: test_emontx
|
||||
|
||||
# User override: verify that explicit values are respected and not clobbered
|
||||
- platform: emontx
|
||||
tag_name: E2
|
||||
name: Energy 2 (user override)
|
||||
emontx_id: test_emontx
|
||||
state_class: measurement
|
||||
accuracy_decimals: 3
|
||||
@@ -6,3 +6,6 @@ update:
|
||||
type: embedded
|
||||
path: $component_dir/test_firmware.bin
|
||||
sha256: de2f256064a0af797747c2b97505dc0b9f3df0de4f489eac731c23ae9ca9cc31
|
||||
on_update_available:
|
||||
then:
|
||||
- logger.log: "Coprocessor update available"
|
||||
|
||||
@@ -8,3 +8,6 @@ update:
|
||||
type: http
|
||||
source: https://esphome.github.io/esp-hosted-firmware/manifest/esp32c6.json
|
||||
update_interval: 6h
|
||||
on_update_available:
|
||||
then:
|
||||
- logger.log: "Coprocessor update available"
|
||||
|
||||
@@ -1,7 +1,67 @@
|
||||
#include <array>
|
||||
#include <utility>
|
||||
#include "../common.h"
|
||||
|
||||
namespace esphome::mitsubishi_cn105::testing {
|
||||
|
||||
TEST(MitsubishiCN105ClimateTests, CelsiusTemperatureMappingAndTraitsMatchExpectedValues) {
|
||||
TestableMitsubishiCN105Climate sut;
|
||||
const auto mapping = TemperatureMapping();
|
||||
|
||||
for (int temperature = 16; temperature <= 31; ++temperature) {
|
||||
EXPECT_EQ(mapping.to_mitsubishi(temperature), temperature);
|
||||
EXPECT_EQ(mapping.from_mitsubishi(temperature), temperature);
|
||||
}
|
||||
|
||||
const auto traits = sut.traits();
|
||||
EXPECT_EQ(traits.get_temperature_unit(), TemperatureUnit::CELSIUS);
|
||||
EXPECT_FLOAT_EQ(traits.get_visual_min_temperature(), 16.0f);
|
||||
EXPECT_FLOAT_EQ(traits.get_visual_max_temperature(), 31.0f);
|
||||
EXPECT_FLOAT_EQ(traits.get_visual_target_temperature_step(), 1.0f);
|
||||
EXPECT_FLOAT_EQ(traits.get_visual_current_temperature_step(), 0.5f);
|
||||
}
|
||||
|
||||
TEST(MitsubishiCN105ClimateTests, FahrenheitTemperatureMappingAndTraitsMatchExpectedValues) {
|
||||
TestableMitsubishiCN105Climate sut;
|
||||
auto mapping = TemperatureMapping();
|
||||
mapping.set_use_fahrenheit(true);
|
||||
sut.set_use_fahrenheit(true);
|
||||
|
||||
const std::array cases{
|
||||
std::pair{61, 16.0f}, std::pair{62, 16.5f}, std::pair{63, 17.0f}, std::pair{64, 17.5f}, std::pair{65, 18.0f},
|
||||
std::pair{66, 18.5f}, std::pair{67, 19.0f}, std::pair{68, 20.0f}, std::pair{69, 21.0f}, std::pair{70, 21.5f},
|
||||
std::pair{71, 22.0f}, std::pair{72, 22.5f}, std::pair{73, 23.0f}, std::pair{74, 23.5f}, std::pair{75, 24.0f},
|
||||
std::pair{76, 24.5f}, std::pair{77, 25.0f}, std::pair{78, 25.5f}, std::pair{79, 26.0f}, std::pair{80, 26.5f},
|
||||
std::pair{81, 27.0f}, std::pair{82, 27.5f}, std::pair{83, 28.0f}, std::pair{84, 28.5f}, std::pair{85, 29.0f},
|
||||
std::pair{86, 29.5f}, std::pair{87, 30.0f}, std::pair{88, 30.5f},
|
||||
};
|
||||
|
||||
for (const auto &[fahrenheit, mitsubishi_celsius] : cases) {
|
||||
EXPECT_FLOAT_EQ(mapping.to_mitsubishi(fahrenheit), mitsubishi_celsius);
|
||||
EXPECT_FLOAT_EQ(mapping.from_mitsubishi(mitsubishi_celsius), fahrenheit);
|
||||
}
|
||||
const auto traits = sut.traits();
|
||||
EXPECT_EQ(traits.get_temperature_unit(), TemperatureUnit::FAHRENHEIT);
|
||||
EXPECT_FLOAT_EQ(traits.get_visual_min_temperature(), 61.0f);
|
||||
EXPECT_FLOAT_EQ(traits.get_visual_max_temperature(), 88.0f);
|
||||
EXPECT_FLOAT_EQ(traits.get_visual_target_temperature_step(), 1.0f);
|
||||
EXPECT_FLOAT_EQ(traits.get_visual_current_temperature_step(), 1.0f);
|
||||
}
|
||||
|
||||
TEST(MitsubishiCN105ClimateTests, FahrenheitTemperatureMappingUsesLinearConversionOutsideSetpointRange) {
|
||||
auto mapping = TemperatureMapping();
|
||||
mapping.set_use_fahrenheit(true);
|
||||
|
||||
const std::array cases{
|
||||
std::pair{0.0f, 32.0f}, std::pair{10.0f, 50.0f}, std::pair{15.5f, 59.9f},
|
||||
std::pair{31.0f, 87.8f}, std::pair{35.0f, 95.0f}, std::pair{40.0f, 104.0f},
|
||||
};
|
||||
|
||||
for (const auto &[celsius, fahrenheit] : cases) {
|
||||
EXPECT_FLOAT_EQ(mapping.from_mitsubishi(celsius), fahrenheit);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(MitsubishiCN105ClimateTests, SupportedSwingModeOffLeavesTraitsEmpty) {
|
||||
TestableMitsubishiCN105Climate sut;
|
||||
|
||||
|
||||
@@ -73,6 +73,7 @@ class TestableMitsubishiCN105Climate : public MitsubishiCN105Climate {
|
||||
using MitsubishiCN105Climate::last_non_swing_wide_vane_mode_;
|
||||
|
||||
MitsubishiCN105::Status &status() { return const_cast<MitsubishiCN105::Status &>(this->component_.status()); }
|
||||
void set_use_fahrenheit(bool value) { this->component_.set_use_fahrenheit(value); }
|
||||
|
||||
protected:
|
||||
MitsubishiCN105Component component_;
|
||||
|
||||
@@ -3,6 +3,7 @@ mitsubishi_cn105:
|
||||
uart_id: uart_bus
|
||||
update_interval: 30s
|
||||
telemetry_request_min_interval: 120s
|
||||
use_fahrenheit: true
|
||||
vane:
|
||||
on_state:
|
||||
- logger.log:
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
remote_transmitter:
|
||||
id: xmitr
|
||||
pin: GPIO26
|
||||
carrier_duty_percent: 50%
|
||||
|
||||
packages:
|
||||
buttons: !include common-buttons.yaml
|
||||
@@ -0,0 +1,7 @@
|
||||
remote_transmitter:
|
||||
id: xmitr
|
||||
pin: GPIO12
|
||||
carrier_duty_percent: 50%
|
||||
|
||||
packages:
|
||||
buttons: !include common-buttons.yaml
|
||||
@@ -1,6 +1,6 @@
|
||||
# `sendspin.switch` action enables the controller role, so we use a standalone test
|
||||
packages:
|
||||
base: !include common.yaml
|
||||
sendspin: !include common.yaml
|
||||
|
||||
wifi:
|
||||
on_connect:
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
packages:
|
||||
sendspin_hub: !include common-hub.yaml
|
||||
|
||||
ethernet:
|
||||
type: OPENETH
|
||||
@@ -0,0 +1,6 @@
|
||||
psram:
|
||||
mode: quad
|
||||
|
||||
sendspin:
|
||||
id: sendspin_hub_id
|
||||
task_stack_in_psram: true
|
||||
@@ -1,4 +1,5 @@
|
||||
<<: !include common.yaml
|
||||
packages:
|
||||
sendspin: !include common.yaml
|
||||
|
||||
media_player:
|
||||
- platform: sendspin
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<<: !include common.yaml
|
||||
packages:
|
||||
sendspin: !include common.yaml
|
||||
|
||||
media_source:
|
||||
- platform: sendspin
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<<: !include common.yaml
|
||||
packages:
|
||||
sendspin: !include common.yaml
|
||||
|
||||
sensor:
|
||||
- platform: sendspin
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<<: !include common.yaml
|
||||
packages:
|
||||
sendspin: !include common.yaml
|
||||
|
||||
text_sensor:
|
||||
- platform: sendspin
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
packages:
|
||||
sendspin_hub: !include common-hub.yaml
|
||||
|
||||
wifi:
|
||||
ap:
|
||||
|
||||
psram:
|
||||
mode: quad
|
||||
|
||||
sendspin:
|
||||
id: sendspin_hub_id
|
||||
task_stack_in_psram: true
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
<<: !include common-action.yaml
|
||||
packages:
|
||||
sendspin: !include common-action.yaml
|
||||
|
||||
@@ -1,9 +1,2 @@
|
||||
ethernet:
|
||||
type: OPENETH
|
||||
|
||||
psram:
|
||||
mode: quad
|
||||
|
||||
sendspin:
|
||||
id: sendspin_hub_id
|
||||
task_stack_in_psram: true
|
||||
packages:
|
||||
sendspin: !include common-ethernet.yaml
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
<<: !include common-media_player.yaml
|
||||
packages:
|
||||
sendspin: !include common-media_player.yaml
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
<<: !include common-media_source.yaml
|
||||
packages:
|
||||
sendspin: !include common-media_source.yaml
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
<<: !include common-sensor.yaml
|
||||
packages:
|
||||
sendspin: !include common-sensor.yaml
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
<<: !include common-text_sensor.yaml
|
||||
packages:
|
||||
sendspin: !include common-text_sensor.yaml
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
<<: !include common.yaml
|
||||
packages:
|
||||
sendspin: !include common.yaml
|
||||
|
||||
Reference in New Issue
Block a user