mirror of
https://github.com/esphome/esphome.git
synced 2026-10-04 18:11:29 +00:00
Merge remote-tracking branch 'origin/dev' into feat/nrf52_pwm
This commit is contained in:
@@ -28,6 +28,7 @@ create an `__init__.py` in your component's test directory and define `override_
|
||||
```python
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# Re-enable the component's own to_code (needed when the component must
|
||||
# emit C++ setup code that the test binary depends on at link time).
|
||||
@@ -39,6 +40,7 @@ Or supply a lightweight stub instead of the real `to_code`:
|
||||
```python
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
async def to_code_testing(config):
|
||||
# Only emit what the C++ tests actually need
|
||||
@@ -54,6 +56,7 @@ e.g. `tests/components/my_sensor/sensor/__init__.py`):
|
||||
```python
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
manifest.enable_codegen()
|
||||
```
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
animation:
|
||||
- id: rgb565_animation
|
||||
image:
|
||||
- platform: animation
|
||||
id: rgb565_animation
|
||||
file: $component_dir/anim.gif
|
||||
type: RGB565
|
||||
transparency: opaque
|
||||
resize: 50x50
|
||||
- id: rgb_animation
|
||||
- platform: animation
|
||||
id: rgb_animation
|
||||
file: $component_dir/anim.apng
|
||||
type: RGB
|
||||
transparency: chroma_key
|
||||
resize: 50x50
|
||||
- id: grayscale_animation
|
||||
- platform: animation
|
||||
id: grayscale_animation
|
||||
file: $component_dir/anim.apng
|
||||
type: grayscale
|
||||
|
||||
display:
|
||||
lambda: |-
|
||||
id(rgb565_animation).next_frame();
|
||||
id(rgb_animation1).next_frame();
|
||||
id(grayscale_animation2).next_frame();
|
||||
id(rgb_animation).next_frame();
|
||||
id(grayscale_animation).next_frame();
|
||||
it.image(0, 0, rgb565_animation);
|
||||
it.image(120, 0, rgb_animation1);
|
||||
it.image(240, 0, grayscale_animation2);
|
||||
it.image(120, 0, rgb_animation);
|
||||
it.image(240, 0, grayscale_animation);
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# Legacy top-level `animation:` form (deprecated; migrates to
|
||||
# `platform: animation`). Config-only test exercising the deprecation path.
|
||||
display:
|
||||
- platform: sdl
|
||||
id: animation_display
|
||||
auto_clear_enabled: false
|
||||
dimensions:
|
||||
width: 480
|
||||
height: 480
|
||||
|
||||
animation:
|
||||
- id: legacy_animation
|
||||
file: $component_dir/anim.gif
|
||||
type: RGB565
|
||||
transparency: opaque
|
||||
resize: 50x50
|
||||
@@ -1,5 +1,11 @@
|
||||
<<: !include common-base.yaml
|
||||
packages:
|
||||
common: !include common-base.yaml
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
# Encryption enabled without a key: compiles both frame helpers so the key
|
||||
# can be provisioned at runtime (zero-PSK noise or deprecated plaintext)
|
||||
api:
|
||||
encryption:
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# Declares the component graph the C++ unit test build needs so that the aqi
|
||||
# component's sources (which include sensor.h) compile. to_code is suppressed by
|
||||
# the test harness; this only pulls the sensor + aqi source/include paths in.
|
||||
# Loaded with plain yaml.safe_load, so avoid lambdas / ESPHome-tagged values here.
|
||||
sensor:
|
||||
- platform: template
|
||||
id: pm25_sensor
|
||||
name: "PM2.5"
|
||||
- platform: template
|
||||
id: pm10_sensor
|
||||
name: "PM10"
|
||||
- platform: aqi
|
||||
name: "AQI"
|
||||
pm_2_5: pm25_sensor
|
||||
pm_10_0: pm10_sensor
|
||||
calculation_type: AQI
|
||||
@@ -20,3 +20,10 @@ sensor:
|
||||
pm_2_5: pm25_sensor
|
||||
pm_10_0: pm10_sensor
|
||||
calculation_type: CAQI
|
||||
|
||||
- platform: aqi
|
||||
name: "Air Quality Index (AQI, extended)"
|
||||
pm_2_5: pm25_sensor
|
||||
pm_10_0: pm10_sensor
|
||||
calculation_type: AQI
|
||||
extended_range: true
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "esphome/components/aqi/aqi_calculator.h"
|
||||
#include "esphome/components/aqi/caqi_calculator.h"
|
||||
|
||||
namespace esphome::aqi::testing {
|
||||
|
||||
// US AQI (EPA 2024): PM2.5 225.5-500.4 -> 301-500, PM10 425-604 -> 301-500.
|
||||
|
||||
TEST(USAQI, LowRangeUnaffectedByExtendedFlag) {
|
||||
AQICalculator calc;
|
||||
// PM2.5 25 drives over PM10 50; well below the top band, so the flag changes nothing.
|
||||
EXPECT_EQ(calc.get_aqi(25.0f, 50.0f, false), 81);
|
||||
EXPECT_EQ(calc.get_aqi(25.0f, 50.0f, true), 81);
|
||||
}
|
||||
|
||||
TEST(USAQI, HazardousInterpolatesNotPinnedAt301) {
|
||||
AQICalculator calc;
|
||||
// Regression guard: the old FLT_MAX top bucket collapsed every hazardous reading to 301.
|
||||
EXPECT_EQ(calc.get_aqi(225.5f, 0.0f, false), 301); // band start
|
||||
EXPECT_EQ(calc.get_aqi(250.0f, 0.0f, false), 319); // interpolated, not 301
|
||||
EXPECT_EQ(calc.get_aqi(500.4f, 0.0f, false), 500); // band top
|
||||
}
|
||||
|
||||
TEST(USAQI, DefaultClampsAtStandardMaximum) {
|
||||
AQICalculator calc;
|
||||
EXPECT_EQ(calc.get_aqi(600.0f, 0.0f, false), 500);
|
||||
EXPECT_EQ(calc.get_aqi(1000.0f, 0.0f, false), 500);
|
||||
EXPECT_EQ(calc.get_aqi(0.0f, 604.0f, false), 500); // PM10 top breakpoint
|
||||
}
|
||||
|
||||
TEST(USAQI, ExtendedRangeExtrapolatesBeyond500) {
|
||||
AQICalculator calc;
|
||||
EXPECT_EQ(calc.get_aqi(600.0f, 0.0f, true), 572);
|
||||
EXPECT_EQ(calc.get_aqi(1000.0f, 0.0f, true), 862);
|
||||
EXPECT_EQ(calc.get_aqi(0.0f, 700.0f, true), 607); // PM10 extrapolated past 500
|
||||
}
|
||||
|
||||
TEST(USAQI, ExtendedRangeSaturatesUint16NoWraparound) {
|
||||
AQICalculator calc;
|
||||
// An absurd concentration would overflow uint16_t; it must saturate, not wrap to a small value.
|
||||
EXPECT_EQ(calc.get_aqi(100000.0f, 0.0f, true), 65535);
|
||||
}
|
||||
|
||||
TEST(USAQI, WorseOfTwoPollutantsWins) {
|
||||
AQICalculator calc;
|
||||
// PM10 604 -> 500 dominates PM2.5 25 -> 81.
|
||||
EXPECT_EQ(calc.get_aqi(25.0f, 604.0f, false), 500);
|
||||
}
|
||||
|
||||
// CAQI (CITEAIR): no maximum by spec -- the top ">100" class is open, so it is always unbounded
|
||||
// and the extended_range flag does not apply.
|
||||
|
||||
TEST(CAQI, LowRange) {
|
||||
CAQICalculator calc;
|
||||
EXPECT_EQ(calc.get_aqi(25.0f, 50.0f, false), 50);
|
||||
}
|
||||
|
||||
TEST(CAQI, ContinuousAt100NoPinAt101) {
|
||||
CAQICalculator calc;
|
||||
// Old code pinned everything above the top breakpoint to 101; now it reaches exactly 100.
|
||||
EXPECT_EQ(calc.get_aqi(110.1f, 0.0f, false), 100);
|
||||
}
|
||||
|
||||
TEST(CAQI, UnboundedAboveTopBand) {
|
||||
CAQICalculator calc;
|
||||
EXPECT_EQ(calc.get_aqi(200.0f, 0.0f, false), 139);
|
||||
EXPECT_EQ(calc.get_aqi(2000.0f, 0.0f, false), 925);
|
||||
}
|
||||
|
||||
TEST(CAQI, ExtendedRangeFlagIsIgnored) {
|
||||
CAQICalculator calc;
|
||||
// CAQI is always unbounded, so the flag must make no difference either way.
|
||||
EXPECT_EQ(calc.get_aqi(200.0f, 0.0f, true), calc.get_aqi(200.0f, 0.0f, false));
|
||||
EXPECT_EQ(calc.get_aqi(2000.0f, 0.0f, true), calc.get_aqi(2000.0f, 0.0f, false));
|
||||
}
|
||||
|
||||
TEST(CAQI, SaturatesUint16NoWraparound) {
|
||||
CAQICalculator calc;
|
||||
// CAQI is unbounded, so an extreme reading can extrapolate past uint16_t; it must saturate,
|
||||
// not wrap around to a small (falsely "good") value.
|
||||
EXPECT_EQ(calc.get_aqi(200000.0f, 0.0f, false), 65535);
|
||||
}
|
||||
|
||||
} // namespace esphome::aqi::testing
|
||||
@@ -0,0 +1,2 @@
|
||||
bk72xx_ble:
|
||||
enable_on_boot: true
|
||||
@@ -0,0 +1,2 @@
|
||||
packages:
|
||||
bk72xx_ble: !include common.yaml
|
||||
@@ -0,0 +1,11 @@
|
||||
bk72xx_ble_tracker:
|
||||
id: ble_tracker
|
||||
scan_parameters:
|
||||
# Boundary coverage: the documented 2.5 ms floor on window (expressible only
|
||||
# via the microsecond-accurate validation), a non-round interval exercising the
|
||||
# 0.625 ms unit conversion without collapsing onto the window's unit count,
|
||||
# and the non-continuous config path.
|
||||
interval: 5000us
|
||||
window: 2500us
|
||||
duration: 5min
|
||||
continuous: false
|
||||
@@ -0,0 +1,7 @@
|
||||
bk72xx_ble_tracker:
|
||||
id: ble_tracker
|
||||
scan_parameters:
|
||||
interval: 100ms
|
||||
window: 30ms
|
||||
duration: 5min
|
||||
continuous: true
|
||||
@@ -0,0 +1,2 @@
|
||||
packages:
|
||||
bk72xx_ble_tracker: !include common-boundary.yaml
|
||||
@@ -0,0 +1,2 @@
|
||||
packages:
|
||||
bk72xx_ble_tracker: !include common.yaml
|
||||
@@ -0,0 +1,12 @@
|
||||
import esphome.codegen as cg
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# resolve_irk() is compiled only when a sensor configures irk:
|
||||
# (request_irk_support() emits USE_BLE_DEVICE_IRK). The unit-test build has
|
||||
# no sensors, so emit the define here to put the real IRK path under test.
|
||||
async def to_code_testing(config):
|
||||
cg.add_define("USE_BLE_DEVICE_IRK")
|
||||
|
||||
manifest.to_code = to_code_testing
|
||||
@@ -0,0 +1,46 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "esphome/components/ble_device_base/ble_device.h"
|
||||
|
||||
namespace esphome::ble_device_base::testing {
|
||||
|
||||
// from_scan_result() ingests BLE controller order (LSB-first); the public
|
||||
// accessors must expose the historical esp32 semantics: address() in printable
|
||||
// (MSB-first) order, address_uint64() with byte 0 in the LSB, address_str()
|
||||
// printed MSB-first.
|
||||
namespace {
|
||||
// Device AA:BB:CC:DD:EE:FF — controller order delivers FF first.
|
||||
const uint8_t MAC_LSB_FIRST[6] = {0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa};
|
||||
} // namespace
|
||||
|
||||
TEST(BleDeviceAddress, AccessorsMatchEsp32Semantics) {
|
||||
ESPBTDevice device;
|
||||
device.from_scan_result(MAC_LSB_FIRST, -50, BLE_ADDR_TYPE_PUBLIC, nullptr, 0);
|
||||
|
||||
const uint8_t *raw = device.address();
|
||||
EXPECT_EQ(raw[0], 0xaa); // MSB first, like ESP-IDF's bda
|
||||
EXPECT_EQ(raw[5], 0xff);
|
||||
|
||||
EXPECT_EQ(device.address_uint64(), 0xAABBCCDDEEFFULL);
|
||||
|
||||
EXPECT_EQ(device.address_str(), "AA:BB:CC:DD:EE:FF");
|
||||
}
|
||||
|
||||
// mac_lsb_first_to_uint64() packs the controller-order bytes a raw-advertisement
|
||||
// callback delivers into the printable-order uint64 the native API speaks — the
|
||||
// value esp32_ble::ble_addr_to_uint64() has always produced for that address.
|
||||
TEST(BleDeviceAddress, MacLsbFirstToUint64MatchesWireValue) {
|
||||
EXPECT_EQ(mac_lsb_first_to_uint64(MAC_LSB_FIRST), 0xAABBCCDDEEFFULL);
|
||||
}
|
||||
|
||||
// The helper and the parsed-device accessor are two routes to the same wire
|
||||
// value: byte order must agree no matter which path an advertisement takes.
|
||||
TEST(BleDeviceAddress, MacLsbFirstToUint64AgreesWithParsedDevice) {
|
||||
ESPBTDevice device;
|
||||
device.from_scan_result(MAC_LSB_FIRST, -50, BLE_ADDR_TYPE_PUBLIC, nullptr, 0);
|
||||
EXPECT_EQ(mac_lsb_first_to_uint64(MAC_LSB_FIRST), device.address_uint64());
|
||||
}
|
||||
|
||||
} // namespace esphome::ble_device_base::testing
|
||||
@@ -0,0 +1,56 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
#include "esphome/components/ble_device_base/ble_aes_ccm.h"
|
||||
|
||||
namespace esphome::ble_device_base::testing {
|
||||
|
||||
// Reference vector generated with Python `cryptography` AESCCM(tag_length=4),
|
||||
// using the same AES-128-CCM parameters BTHome advertisements use: a 16-byte
|
||||
// key, a 13-byte nonce, a 4-byte authentication tag and no associated data.
|
||||
namespace {
|
||||
const uint8_t KEY[16] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
||||
0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f};
|
||||
const uint8_t NONCE[13] = {0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c};
|
||||
const uint8_t CIPHERTEXT[7] = {0x68, 0xb4, 0xf6, 0xc5, 0x2b, 0xf8, 0xaf};
|
||||
const uint8_t TAG[4] = {0x48, 0x4d, 0xaa, 0x56};
|
||||
const uint8_t PLAINTEXT[7] = {0x02, 0x01, 0x64, 0x03, 0x10, 0x8a, 0x01};
|
||||
} // namespace
|
||||
|
||||
TEST(BleAesCcm, DecryptsAndAuthenticatesKnownVector) {
|
||||
uint8_t out[sizeof(PLAINTEXT)] = {};
|
||||
EXPECT_TRUE(aes_ccm_auth_decrypt(KEY, NONCE, sizeof(NONCE), nullptr, 0, CIPHERTEXT, sizeof(CIPHERTEXT), out, TAG,
|
||||
sizeof(TAG)));
|
||||
EXPECT_EQ(0, memcmp(out, PLAINTEXT, sizeof(PLAINTEXT)));
|
||||
}
|
||||
|
||||
TEST(BleAesCcm, RejectsTamperedTag) {
|
||||
uint8_t bad_tag[sizeof(TAG)];
|
||||
memcpy(bad_tag, TAG, sizeof(TAG));
|
||||
bad_tag[0] ^= 0x01;
|
||||
uint8_t out[sizeof(PLAINTEXT)] = {};
|
||||
EXPECT_FALSE(aes_ccm_auth_decrypt(KEY, NONCE, sizeof(NONCE), nullptr, 0, CIPHERTEXT, sizeof(CIPHERTEXT), out, bad_tag,
|
||||
sizeof(bad_tag)));
|
||||
}
|
||||
|
||||
TEST(BleAesCcm, RejectsTamperedCiphertext) {
|
||||
uint8_t bad_ct[sizeof(CIPHERTEXT)];
|
||||
memcpy(bad_ct, CIPHERTEXT, sizeof(CIPHERTEXT));
|
||||
bad_ct[0] ^= 0x01;
|
||||
uint8_t out[sizeof(PLAINTEXT)] = {};
|
||||
EXPECT_FALSE(
|
||||
aes_ccm_auth_decrypt(KEY, NONCE, sizeof(NONCE), nullptr, 0, bad_ct, sizeof(bad_ct), out, TAG, sizeof(TAG)));
|
||||
}
|
||||
|
||||
TEST(BleAesCcm, RejectsWrongKey) {
|
||||
uint8_t bad_key[sizeof(KEY)];
|
||||
memcpy(bad_key, KEY, sizeof(KEY));
|
||||
bad_key[0] ^= 0xFF;
|
||||
uint8_t out[sizeof(PLAINTEXT)] = {};
|
||||
EXPECT_FALSE(aes_ccm_auth_decrypt(bad_key, NONCE, sizeof(NONCE), nullptr, 0, CIPHERTEXT, sizeof(CIPHERTEXT), out, TAG,
|
||||
sizeof(TAG)));
|
||||
}
|
||||
|
||||
} // namespace esphome::ble_device_base::testing
|
||||
@@ -0,0 +1,41 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
#include "esphome/components/ble_device_base/ble_device.h"
|
||||
|
||||
namespace esphome::ble_device_base::testing {
|
||||
|
||||
// A 16- or 32-bit UUID must compare equal to its 128-bit Bluetooth Base UUID form, matching
|
||||
// esp32_ble_tracker. The 128-bit raw is the base UUID (LSB-first) with the short value at
|
||||
// bytes 12.. : here 0x1234 -> bytes [12]=0x34, [13]=0x12.
|
||||
TEST(BleDeviceUuid, ShortFormMatchesEquivalentLongForm) {
|
||||
const ESPBTUUID u16 = ESPBTUUID::from_uint16(0x1234);
|
||||
const uint8_t raw128[16] = {0xFB, 0x34, 0x9B, 0x5F, 0x80, 0x00, 0x00, 0x80,
|
||||
0x00, 0x10, 0x00, 0x00, 0x34, 0x12, 0x00, 0x00};
|
||||
const ESPBTUUID u128 = ESPBTUUID::from_raw(raw128);
|
||||
EXPECT_TRUE(u16 == u128);
|
||||
EXPECT_TRUE(u128 == u16); // symmetric
|
||||
}
|
||||
|
||||
TEST(BleDeviceUuid, ThirtyTwoBitMatchesEquivalentLongForm) {
|
||||
const ESPBTUUID u32 = ESPBTUUID::from_uint32(0x1122AAFF);
|
||||
const uint8_t raw128[16] = {0xFB, 0x34, 0x9B, 0x5F, 0x80, 0x00, 0x00, 0x80,
|
||||
0x00, 0x10, 0x00, 0x00, 0xFF, 0xAA, 0x22, 0x11};
|
||||
const ESPBTUUID u128 = ESPBTUUID::from_raw(raw128);
|
||||
EXPECT_TRUE(u32 == u128);
|
||||
}
|
||||
|
||||
TEST(BleDeviceUuid, DifferentUuidsDoNotMatch) {
|
||||
EXPECT_FALSE(ESPBTUUID::from_uint16(0x1234) == ESPBTUUID::from_uint16(0x1235));
|
||||
const uint8_t raw128[16] = {0xFB, 0x34, 0x9B, 0x5F, 0x80, 0x00, 0x00, 0x80,
|
||||
0x00, 0x10, 0x00, 0x00, 0x34, 0x12, 0x00, 0x00};
|
||||
// Same low bytes but a non-base prefix is a genuinely different 128-bit UUID.
|
||||
uint8_t custom[16];
|
||||
memcpy(custom, raw128, 16);
|
||||
custom[0] ^= 0x01;
|
||||
EXPECT_FALSE(ESPBTUUID::from_uint16(0x1234) == ESPBTUUID::from_raw(custom));
|
||||
}
|
||||
|
||||
} // namespace esphome::ble_device_base::testing
|
||||
@@ -0,0 +1,48 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "esphome/components/ble_device_base/ble_device.h"
|
||||
|
||||
namespace esphome::ble_device_base::testing {
|
||||
|
||||
// Reference vector generated with Python `cryptography` AES-128-ECB following
|
||||
// the RPA resolution procedure (Bluetooth Core, Vol 3 Part H §2.2.2):
|
||||
// hash = e(IRK, prand), where prand is the top 3 address bytes and the hash
|
||||
// must equal the low 3 address bytes.
|
||||
namespace {
|
||||
const uint8_t IRK[16] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
||||
0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f};
|
||||
// 4A:2B:7C:FB:7B:21 — prand 4A:2B:7C (two MSBs = 01, an RPA), hash FB:7B:21.
|
||||
const uint8_t RPA_LSB_FIRST[6] = {0x21, 0x7b, 0xfb, 0x7c, 0x2b, 0x4a};
|
||||
|
||||
ESPBTDevice make_device(const uint8_t mac_lsb_first[6]) {
|
||||
ESPBTDevice device;
|
||||
device.from_scan_result(mac_lsb_first, /*rssi=*/-60, /*addr_type=*/BLE_ADDR_TYPE_RPA_RANDOM, nullptr, 0);
|
||||
return device;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST(BleIrk, ResolvesMatchingRpa) {
|
||||
ESPBTDevice device = make_device(RPA_LSB_FIRST);
|
||||
EXPECT_TRUE(device.resolve_irk(IRK));
|
||||
}
|
||||
|
||||
TEST(BleIrk, RejectsWrongIrk) {
|
||||
uint8_t wrong_irk[16];
|
||||
for (int i = 0; i < 16; i++)
|
||||
wrong_irk[i] = IRK[i] ^ 0xff;
|
||||
ESPBTDevice device = make_device(RPA_LSB_FIRST);
|
||||
EXPECT_FALSE(device.resolve_irk(wrong_irk));
|
||||
}
|
||||
|
||||
TEST(BleIrk, RejectsWrongAddress) {
|
||||
uint8_t other_mac[6];
|
||||
for (int i = 0; i < 6; i++)
|
||||
other_mac[i] = RPA_LSB_FIRST[i];
|
||||
other_mac[0] ^= 0x01; // corrupt one hash byte
|
||||
ESPBTDevice device = make_device(other_mac);
|
||||
EXPECT_FALSE(device.resolve_irk(IRK));
|
||||
}
|
||||
|
||||
} // namespace esphome::ble_device_base::testing
|
||||
@@ -0,0 +1,101 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "esphome/components/ble_device_base/ble_hub.h"
|
||||
|
||||
namespace esphome::ble_device_base::testing {
|
||||
|
||||
// Exercises the hub contract around RawAdvertisementCallback, not just the
|
||||
// struct: a hub stores one slot via set_raw_advertisement_callback(), fires it
|
||||
// only when set ("no subscriber" is the default-constructed slot), and a new
|
||||
// registration replaces the old ("one consumer at a time").
|
||||
//
|
||||
// The in-tree emit site (BK72xxBLETracker::on_scan_report) compiles against
|
||||
// the Beken SDK and cannot run host-side, so the guard-and-fire semantics are
|
||||
// pinned here through a minimal host BLEHub implementation instead.
|
||||
namespace {
|
||||
|
||||
class FakeHub : public BLEHub {
|
||||
public:
|
||||
void register_listener(ESPBTDeviceListener *listener) override {}
|
||||
void set_raw_advertisement_callback(RawAdvertisementCallback callback) override { this->callback_ = callback; }
|
||||
HubCapabilities get_capabilities() const override { return {false, false, false}; }
|
||||
void get_adapter_mac(uint8_t out[6]) override {}
|
||||
bool scan_running() override { return false; }
|
||||
bool scan_active() override { return false; }
|
||||
|
||||
/// The emit path every tracker implements: fire only when a subscriber is set.
|
||||
void emit(const RawAdvertisement &adv) {
|
||||
if (this->callback_.is_set())
|
||||
this->callback_.invoke(adv);
|
||||
}
|
||||
|
||||
protected:
|
||||
RawAdvertisementCallback callback_; // default-constructed: no subscriber
|
||||
};
|
||||
|
||||
struct CapturingSubscriber {
|
||||
RawAdvertisement last{};
|
||||
int calls{0};
|
||||
|
||||
static void trampoline(void *self, const RawAdvertisement &adv) {
|
||||
auto *sub = static_cast<CapturingSubscriber *>(self);
|
||||
sub->last = adv;
|
||||
sub->calls++;
|
||||
}
|
||||
};
|
||||
|
||||
// Device AA:BB:CC:DD:EE:FF — controller order delivers FF first.
|
||||
const uint8_t MAC_LSB_FIRST[6] = {0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa};
|
||||
const uint8_t ADV_DATA[4] = {0x02, 0x01, 0x06, 0x00};
|
||||
|
||||
RawAdvertisement make_test_adv() {
|
||||
return RawAdvertisement{
|
||||
.mac = MAC_LSB_FIRST, .data = ADV_DATA, .data_len = sizeof(ADV_DATA), .rssi = -63, .addr_type = 1};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(RawAdvertisementCallback, DefaultConstructedSlotIsNotSet) {
|
||||
const RawAdvertisementCallback callback{};
|
||||
EXPECT_FALSE(callback.is_set());
|
||||
}
|
||||
|
||||
TEST(RawAdvertisementCallback, SubscriberSeesFieldsUnchanged) {
|
||||
FakeHub hub;
|
||||
CapturingSubscriber subscriber;
|
||||
hub.set_raw_advertisement_callback({&subscriber, CapturingSubscriber::trampoline});
|
||||
|
||||
hub.emit(make_test_adv());
|
||||
|
||||
ASSERT_EQ(subscriber.calls, 1);
|
||||
EXPECT_EQ(subscriber.last.mac, MAC_LSB_FIRST);
|
||||
EXPECT_EQ(subscriber.last.data, ADV_DATA);
|
||||
EXPECT_EQ(subscriber.last.data_len, sizeof(ADV_DATA));
|
||||
EXPECT_EQ(subscriber.last.rssi, -63);
|
||||
EXPECT_EQ(subscriber.last.addr_type, 1);
|
||||
}
|
||||
|
||||
TEST(RawAdvertisementCallback, NoSubscriberDoesNotFire) {
|
||||
FakeHub hub;
|
||||
// No set_raw_advertisement_callback(): emitting must be a guarded no-op,
|
||||
// not a jump through a garbage pointer.
|
||||
hub.emit(make_test_adv());
|
||||
}
|
||||
|
||||
TEST(RawAdvertisementCallback, NewSubscriberReplacesOld) {
|
||||
FakeHub hub;
|
||||
CapturingSubscriber first;
|
||||
CapturingSubscriber second;
|
||||
hub.set_raw_advertisement_callback({&first, CapturingSubscriber::trampoline});
|
||||
hub.set_raw_advertisement_callback({&second, CapturingSubscriber::trampoline});
|
||||
|
||||
hub.emit(make_test_adv());
|
||||
|
||||
EXPECT_EQ(first.calls, 0); // one consumer at a time
|
||||
ASSERT_EQ(second.calls, 1);
|
||||
EXPECT_EQ(second.last.rssi, -63);
|
||||
}
|
||||
|
||||
} // namespace esphome::ble_device_base::testing
|
||||
@@ -17,6 +17,15 @@ cc1101:
|
||||
sync0: 0x91
|
||||
sync1: 0xD3
|
||||
num_preamble: 2
|
||||
foc_bs_cs_gate: true
|
||||
foc_pre_k: "2K"
|
||||
foc_post_k: "K/2"
|
||||
foc_limit: "BW/4"
|
||||
bs_pre_ki: "3KI"
|
||||
bs_pre_kp: "4KP"
|
||||
bs_post_ki: "KI/2"
|
||||
bs_post_kp: "KP"
|
||||
bs_limit: "12.5%"
|
||||
on_packet:
|
||||
then:
|
||||
- lambda: |-
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
|
||||
namespace esphome::testing {
|
||||
|
||||
namespace {
|
||||
|
||||
// Large enough that none of the inputs below are ever dropped.
|
||||
constexpr size_t TEST_BUFFER_SIZE = 64 * JSON_ESCAPE_MAX_EXPANSION + 1;
|
||||
|
||||
// Escape into a stack buffer and return the result as a string so the expectations stay readable.
|
||||
std::string escape(const std::string &value) {
|
||||
char buf[TEST_BUFFER_SIZE];
|
||||
return json_escape_into_buffer(buf, StringRef(value.c_str(), value.size()));
|
||||
}
|
||||
|
||||
// Same, but with the short control forms turned off.
|
||||
std::string escape_long(const std::string &value) {
|
||||
char buf[TEST_BUFFER_SIZE];
|
||||
return json_escape_into_buffer(buf, StringRef(value.c_str(), value.size()), false);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Plain ASCII with no special characters is passed through unchanged.
|
||||
TEST(JsonEscape, PlainStringUnchanged) {
|
||||
EXPECT_EQ(escape("MyNetwork"), "MyNetwork");
|
||||
EXPECT_EQ(escape(""), "");
|
||||
}
|
||||
|
||||
// A double quote is escaped so it does not terminate the surrounding JSON string.
|
||||
TEST(JsonEscape, EscapesDoubleQuote) {
|
||||
EXPECT_EQ(escape("a\"b"), "a\\\"b");
|
||||
// A double quote followed by other characters stays inside the JSON string.
|
||||
EXPECT_EQ(escape("\">end"), "\\\">end");
|
||||
}
|
||||
|
||||
// A backslash is doubled so it does not start an escape sequence in the output.
|
||||
TEST(JsonEscape, EscapesBackslash) {
|
||||
EXPECT_EQ(escape("a\\b"), "a\\\\b");
|
||||
// A trailing backslash must not escape the closing quote of the JSON string.
|
||||
EXPECT_EQ(escape("net\\"), "net\\\\");
|
||||
}
|
||||
|
||||
// The control characters with short JSON forms use those forms.
|
||||
TEST(JsonEscape, EscapesShortFormControls) {
|
||||
EXPECT_EQ(escape("\n"), "\\n");
|
||||
EXPECT_EQ(escape("\r"), "\\r");
|
||||
EXPECT_EQ(escape("\t"), "\\t");
|
||||
EXPECT_EQ(escape("\b"), "\\b");
|
||||
EXPECT_EQ(escape("\f"), "\\f");
|
||||
}
|
||||
|
||||
// Other control characters (< 0x20) without a short form become \u00XX with lowercase hex.
|
||||
TEST(JsonEscape, EscapesOtherControlsAsUnicode) {
|
||||
EXPECT_EQ(escape(std::string("\x00", 1)), "\\u0000");
|
||||
EXPECT_EQ(escape("\x01"), "\\u0001");
|
||||
EXPECT_EQ(escape("\x10"), "\\u0010");
|
||||
EXPECT_EQ(escape("\x1f"), "\\u001f");
|
||||
// 0x7f (DEL) is >= 0x20, so it is NOT escaped by this helper.
|
||||
EXPECT_EQ(escape("\x7f"), "\x7f");
|
||||
}
|
||||
|
||||
// With the short forms turned off, every control character is written as \u00XX instead.
|
||||
TEST(JsonEscape, LongControlEscapes) {
|
||||
EXPECT_EQ(escape_long("\n"), "\\u000a");
|
||||
EXPECT_EQ(escape_long("\r"), "\\u000d");
|
||||
EXPECT_EQ(escape_long("\t"), "\\u0009");
|
||||
EXPECT_EQ(escape_long("\b"), "\\u0008");
|
||||
EXPECT_EQ(escape_long("\f"), "\\u000c");
|
||||
// Controls without a short form are unaffected by the flag.
|
||||
EXPECT_EQ(escape_long("\x01"), "\\u0001");
|
||||
}
|
||||
|
||||
// The flag only affects control characters. A quote or backslash is never written as \u00XX, because that form is
|
||||
// no shorter and both modes have always emitted the two character escape.
|
||||
TEST(JsonEscape, LongModeStillUsesTwoCharQuoteAndBackslash) {
|
||||
EXPECT_EQ(escape_long("a\"b"), "a\\\"b");
|
||||
EXPECT_EQ(escape_long("a\\b"), "a\\\\b");
|
||||
// Ordinary text is untouched in either mode.
|
||||
EXPECT_EQ(escape_long("MyDevice"), "MyDevice");
|
||||
}
|
||||
|
||||
// Bytes >= 0x20, including multi-byte UTF-8 sequences, are passed through verbatim.
|
||||
TEST(JsonEscape, PassesThroughUtf8) {
|
||||
// "café" in UTF-8 (é == 0xC3 0xA9).
|
||||
EXPECT_EQ(escape("caf\xc3\xa9"), "caf\xc3\xa9");
|
||||
// Emoji (📶, 4-byte UTF-8) survives unchanged.
|
||||
EXPECT_EQ(escape("\xf0\x9f\x93\xb6"), "\xf0\x9f\x93\xb6");
|
||||
}
|
||||
|
||||
// A mix of special and normal characters is escaped in place without disturbing the rest.
|
||||
TEST(JsonEscape, MixedContent) { EXPECT_EQ(escape("a\"b\\c\nd"), "a\\\"b\\\\c\\nd"); }
|
||||
|
||||
// A buffer sized at JSON_ESCAPE_MAX_EXPANSION bytes per input byte holds the worst case exactly.
|
||||
TEST(JsonEscape, WorstCaseInputFitsExactly) {
|
||||
constexpr size_t input_len = 8;
|
||||
char buf[input_len * JSON_ESCAPE_MAX_EXPANSION + 1];
|
||||
const std::string input(input_len, '\x01');
|
||||
std::string expected;
|
||||
for (size_t i = 0; i < input_len; i++)
|
||||
expected += "\\u0001";
|
||||
EXPECT_EQ(json_escape_into_buffer(buf, StringRef(input.c_str(), input.size())), expected);
|
||||
}
|
||||
|
||||
// An escape sequence that would not fit is dropped whole rather than written partially, and the result stays null
|
||||
// terminated.
|
||||
TEST(JsonEscape, DropsEscapeThatWouldNotFit) {
|
||||
// Room for one \u00XX sequence plus the null terminator, but two are requested.
|
||||
char buf[JSON_ESCAPE_MAX_EXPANSION + 1];
|
||||
const std::string input(2, '\x01');
|
||||
const std::string result = json_escape_into_buffer(buf, StringRef(input.c_str(), input.size()));
|
||||
EXPECT_EQ(result, "\\u0001");
|
||||
EXPECT_EQ(buf[JSON_ESCAPE_MAX_EXPANSION], '\0');
|
||||
}
|
||||
|
||||
// Plain characters are truncated at the buffer size, leaving room for the null terminator.
|
||||
TEST(JsonEscape, TruncatesPlainInput) {
|
||||
char buf[5];
|
||||
const std::string input(20, 'a');
|
||||
EXPECT_STREQ(json_escape_into_buffer(buf, StringRef(input.c_str(), input.size())), "aaaa");
|
||||
}
|
||||
|
||||
// A zero length buffer cannot even hold a null terminator, so an empty string is returned instead of writing.
|
||||
TEST(JsonEscape, EmptyBufferIsSafe) {
|
||||
const std::string input("test");
|
||||
EXPECT_STREQ(json_escape_into_buffer(std::span<char>(), StringRef(input.c_str(), input.size())), "");
|
||||
}
|
||||
|
||||
} // namespace esphome::testing
|
||||
@@ -0,0 +1,96 @@
|
||||
// Exercises the no-atomics LockFreeQueue implementation (PlainAtomic indices —
|
||||
// the path used on cores without atomic RMW instructions, currently BK72xx).
|
||||
// The define is forced before the include so this TU deterministically compiles
|
||||
// that path regardless of the host's default thread model; no other test TU
|
||||
// instantiates this template, so the differing definition is confined here.
|
||||
#define ESPHOME_THREAD_MULTI_NO_ATOMICS
|
||||
#include "esphome/core/lock_free_queue.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
namespace esphome::core::testing {
|
||||
|
||||
TEST(LockFreeQueueNoAtomics, EmptyPopReturnsNull) {
|
||||
esphome::LockFreeQueue<int, 4> q;
|
||||
EXPECT_EQ(q.pop(), nullptr);
|
||||
EXPECT_TRUE(q.empty());
|
||||
EXPECT_FALSE(q.full());
|
||||
EXPECT_EQ(q.size(), 0u);
|
||||
}
|
||||
|
||||
TEST(LockFreeQueueNoAtomics, FifoOrder) {
|
||||
esphome::LockFreeQueue<int, 4> q;
|
||||
int a = 1, b = 2, c = 3;
|
||||
EXPECT_TRUE(q.push(&a));
|
||||
EXPECT_TRUE(q.push(&b));
|
||||
EXPECT_TRUE(q.push(&c));
|
||||
EXPECT_EQ(q.size(), 3u);
|
||||
EXPECT_EQ(q.pop(), &a);
|
||||
EXPECT_EQ(q.pop(), &b);
|
||||
EXPECT_EQ(q.pop(), &c);
|
||||
EXPECT_EQ(q.pop(), nullptr);
|
||||
}
|
||||
|
||||
TEST(LockFreeQueueNoAtomics, CapacityIsSizeMinusOne) {
|
||||
esphome::LockFreeQueue<int, 4> q;
|
||||
int v[4] = {0, 1, 2, 3};
|
||||
EXPECT_TRUE(q.push(&v[0]));
|
||||
EXPECT_TRUE(q.push(&v[1]));
|
||||
EXPECT_TRUE(q.push(&v[2]));
|
||||
EXPECT_TRUE(q.full());
|
||||
// Ring reserves one slot: the SIZEth push fails and is counted as dropped.
|
||||
EXPECT_FALSE(q.push(&v[3]));
|
||||
EXPECT_EQ(q.get_and_reset_dropped_count(), 1u);
|
||||
EXPECT_EQ(q.get_and_reset_dropped_count(), 0u); // reset is sticky
|
||||
}
|
||||
|
||||
TEST(LockFreeQueueNoAtomics, NullPushRejected) {
|
||||
esphome::LockFreeQueue<int, 4> q;
|
||||
EXPECT_FALSE(q.push(nullptr));
|
||||
EXPECT_TRUE(q.empty());
|
||||
}
|
||||
|
||||
TEST(LockFreeQueueNoAtomics, WrapAround) {
|
||||
esphome::LockFreeQueue<int, 4> q;
|
||||
int v[3] = {10, 20, 30};
|
||||
// Cycle several times the ring size to cross the wrap boundary repeatedly.
|
||||
for (int cycle = 0; cycle < 10; cycle++) {
|
||||
for (auto &value : v)
|
||||
ASSERT_TRUE(q.push(&value));
|
||||
EXPECT_TRUE(q.full());
|
||||
for (auto &value : v)
|
||||
ASSERT_EQ(q.pop(), &value);
|
||||
EXPECT_TRUE(q.empty());
|
||||
}
|
||||
EXPECT_EQ(q.get_and_reset_dropped_count(), 0u);
|
||||
}
|
||||
|
||||
TEST(LockFreeQueueNoAtomics, IncrementDroppedCount) {
|
||||
esphome::LockFreeQueue<int, 4> q;
|
||||
// Producer-side external drop accounting (pool exhausted before push).
|
||||
q.increment_dropped_count();
|
||||
q.increment_dropped_count();
|
||||
EXPECT_EQ(q.get_and_reset_dropped_count(), 2u);
|
||||
}
|
||||
|
||||
TEST(LockFreeQueueNoAtomics, InterleavedPushPop) {
|
||||
esphome::LockFreeQueue<int, 8> q;
|
||||
int v[64];
|
||||
int popped = 0;
|
||||
for (int i = 0; i < 64; i++) {
|
||||
v[i] = i;
|
||||
ASSERT_TRUE(q.push(&v[i]));
|
||||
if (i % 2 == 1) {
|
||||
int *first = q.pop();
|
||||
ASSERT_NE(first, nullptr);
|
||||
EXPECT_EQ(*first, popped++);
|
||||
int *second = q.pop();
|
||||
ASSERT_NE(second, nullptr);
|
||||
EXPECT_EQ(*second, popped++);
|
||||
}
|
||||
}
|
||||
EXPECT_TRUE(q.empty());
|
||||
EXPECT_EQ(popped, 64);
|
||||
}
|
||||
|
||||
} // namespace esphome::core::testing
|
||||
@@ -0,0 +1,22 @@
|
||||
display:
|
||||
- platform: ssd1306_i2c
|
||||
i2c_id: i2c_bus
|
||||
id: cst328_ssd1306_i2c_display
|
||||
model: SSD1306_128X64
|
||||
reset_pin: ${display_reset_pin}
|
||||
pages:
|
||||
- id: cst328_page1
|
||||
lambda: |-
|
||||
it.rectangle(0, 0, it.get_width(), it.get_height());
|
||||
|
||||
touchscreen:
|
||||
- platform: cst328
|
||||
i2c_id: i2c_bus
|
||||
id: cst328_touchscreen
|
||||
display: cst328_ssd1306_i2c_display
|
||||
interrupt_pin: ${interrupt_pin}
|
||||
reset_pin: ${reset_pin}
|
||||
|
||||
binary_sensor:
|
||||
- platform: cst328
|
||||
id: touch_key_cst328
|
||||
@@ -0,0 +1,8 @@
|
||||
substitutions:
|
||||
display_reset_pin: "4"
|
||||
interrupt_pin: "20"
|
||||
reset_pin: "21"
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
cst328: !include common.yaml
|
||||
@@ -1,14 +1,18 @@
|
||||
one_wire:
|
||||
- platform: gpio
|
||||
id: ow_dallas_temp
|
||||
pin: ${one_wire_pin}
|
||||
|
||||
sensor:
|
||||
- platform: dallas_temp
|
||||
one_wire_id: ow_dallas_temp
|
||||
address: 0x1C0000031EDD2A28
|
||||
name: Dallas Temperature 1
|
||||
resolution: 9
|
||||
- platform: dallas_temp
|
||||
one_wire_id: ow_dallas_temp
|
||||
name: Dallas Temperature 2
|
||||
- platform: dallas_temp
|
||||
one_wire_id: ow_dallas_temp
|
||||
name: Dallas Temperature 3
|
||||
index: 2
|
||||
|
||||
@@ -6,9 +6,15 @@ deep_sleep:
|
||||
sleep_duration: 50s
|
||||
wakeup_pin: ${wakeup_pin}
|
||||
wakeup_pin_mode: INVERT_WAKEUP
|
||||
on_wake:
|
||||
- logger.log:
|
||||
format: "Woke up, cause %d"
|
||||
args: ["static_cast<int>(cause)"]
|
||||
esp32_ext1_wakeup:
|
||||
pins:
|
||||
- number: GPIO2
|
||||
- pin: GPIO2
|
||||
on_wake:
|
||||
- logger.log: Woken by ext1 pin GPIO2
|
||||
- number: GPIO13
|
||||
mode: ANY_HIGH
|
||||
touch_wakeup: true
|
||||
|
||||
@@ -5,8 +5,15 @@ deep_sleep:
|
||||
sleep_duration: 50s
|
||||
wakeup_pin: ${wakeup_pin}
|
||||
wakeup_pin_mode: INVERT_WAKEUP
|
||||
on_wake:
|
||||
- logger.log:
|
||||
format: "Woke up, cause %d"
|
||||
args: ["static_cast<int>(cause)"]
|
||||
esp32_ext1_wakeup:
|
||||
pins:
|
||||
- number: GPIO2
|
||||
- pin:
|
||||
number: GPIO2
|
||||
on_wake:
|
||||
- logger.log: Woken by ext1 pin GPIO2
|
||||
- number: GPIO5
|
||||
mode: ANY_HIGH
|
||||
|
||||
@@ -5,3 +5,7 @@ deep_sleep:
|
||||
sleep_duration: 50s
|
||||
wakeup_pin: ${wakeup_pin}
|
||||
wakeup_pin_mode: INVERT_WAKEUP
|
||||
on_wake:
|
||||
- logger.log:
|
||||
format: "Woke up, cause %d"
|
||||
args: ["static_cast<int>(cause)"]
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Deep sleep combined with OTA while bootloader rollback support is enabled
|
||||
# (the default on ESP-IDF). Entering deep sleep runs the safe shutdown hooks,
|
||||
# where safe_mode confirms the running app image so the bootloader does not
|
||||
# roll back a fresh OTA update when the device goes to sleep before
|
||||
# boot_is_good_after has elapsed.
|
||||
substitutions:
|
||||
wakeup_pin: GPIO4
|
||||
|
||||
packages:
|
||||
deep_sleep: !include common.yaml
|
||||
deep_sleep_esp32: !include common-esp32.yaml
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
ota:
|
||||
- platform: esphome
|
||||
password: "superlongpasswordthatnoonewillknow"
|
||||
@@ -0,0 +1,16 @@
|
||||
# Deep sleep combined with mcumgr OTA while MCUboot image rollback is enabled
|
||||
# (the default on nRF52). Entering system-off deep sleep runs the safe
|
||||
# shutdown hooks, where safe_mode confirms the running image so MCUboot does
|
||||
# not revert a fresh OTA update on the next wake.
|
||||
packages:
|
||||
deep_sleep: !include common.yaml
|
||||
|
||||
deep_sleep:
|
||||
run_duration: 10s
|
||||
|
||||
zephyr_ble_server:
|
||||
|
||||
ota:
|
||||
- platform: zephyr_mcumgr
|
||||
transport:
|
||||
ble: true
|
||||
@@ -1,6 +1,10 @@
|
||||
deep_sleep:
|
||||
run_duration: 30s
|
||||
sleep_duration: 12h
|
||||
on_wake:
|
||||
- logger.log:
|
||||
format: "Woke up, cause %d"
|
||||
args: ["static_cast<int>(cause)"]
|
||||
wakeup_pin:
|
||||
- pin:
|
||||
number: P6
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
deep_sleep:
|
||||
run_duration: 10s
|
||||
sleep_duration: 50s
|
||||
on_wake:
|
||||
- logger.log:
|
||||
format: "Woke up, cause %d"
|
||||
args: ["static_cast<int>(cause)"]
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
one_wire:
|
||||
platform: ds2484
|
||||
i2c_id: i2c_bus
|
||||
address: 0x18
|
||||
active_pullup: true
|
||||
strong_pullup: false
|
||||
- platform: ds2484
|
||||
id: ow_ds2484
|
||||
i2c_id: i2c_bus
|
||||
address: 0x18
|
||||
active_pullup: true
|
||||
strong_pullup: false
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
# Combined DS248x test covering all chip variants and options:
|
||||
# - DS2482-100: active pullup, multiple sensors + index access
|
||||
# - DS2482-101: sleep pin, bus_sleep / hub_sleep
|
||||
# - DS2482-800: all 8 channels
|
||||
# - DS2484: adjustable 1-Wire timing + RWPU pullup resistor selection
|
||||
ds248x:
|
||||
- id: ds2482_100
|
||||
address: 0x18
|
||||
type: ds2482-100
|
||||
active_pullup: true
|
||||
- id: ds2482_101
|
||||
address: 0x19
|
||||
type: ds2482-101
|
||||
active_pullup: true
|
||||
sleep_pin:
|
||||
number: GPIO12
|
||||
inverted: false
|
||||
bus_sleep: true
|
||||
hub_sleep: true
|
||||
- id: ds2482_800
|
||||
address: 0x1a
|
||||
type: ds2482-800
|
||||
active_pullup: true
|
||||
- id: ds2484_hub
|
||||
address: 0x1b
|
||||
type: ds2484
|
||||
active_pullup: true
|
||||
# DS2484-specific 1-Wire timing parameters (optional fine-tuning)
|
||||
reset_low_time: 8 # tRSTL: Reset low time
|
||||
master_sample_time: 8 # tMSP: Master sample point
|
||||
write_0_low_time: 8 # tW0L: Write-0 low time
|
||||
recovery_time: 8 # tREC0: Recovery time
|
||||
active_pullup_resistance: 1000ohm # RWPU: weak pullup resistor selection
|
||||
|
||||
one_wire:
|
||||
- platform: ds248x
|
||||
ds248x_id: ds2482_100
|
||||
channel: 0
|
||||
id: ow_100
|
||||
- platform: ds248x
|
||||
ds248x_id: ds2482_101
|
||||
channel: 0
|
||||
id: ow_101
|
||||
- platform: ds248x
|
||||
ds248x_id: ds2482_800
|
||||
channel: 0
|
||||
id: ow_800_0
|
||||
- platform: ds248x
|
||||
ds248x_id: ds2482_800
|
||||
channel: 1
|
||||
id: ow_800_1
|
||||
- platform: ds248x
|
||||
ds248x_id: ds2482_800
|
||||
channel: 2
|
||||
id: ow_800_2
|
||||
- platform: ds248x
|
||||
ds248x_id: ds2482_800
|
||||
channel: 3
|
||||
id: ow_800_3
|
||||
- platform: ds248x
|
||||
ds248x_id: ds2482_800
|
||||
channel: 4
|
||||
id: ow_800_4
|
||||
- platform: ds248x
|
||||
ds248x_id: ds2482_800
|
||||
channel: 5
|
||||
id: ow_800_5
|
||||
- platform: ds248x
|
||||
ds248x_id: ds2482_800
|
||||
channel: 6
|
||||
id: ow_800_6
|
||||
- platform: ds248x
|
||||
ds248x_id: ds2482_800
|
||||
channel: 7
|
||||
id: ow_800_7
|
||||
- platform: ds248x
|
||||
ds248x_id: ds2484_hub
|
||||
channel: 0
|
||||
id: ow_2484
|
||||
|
||||
sensor:
|
||||
# DS2482-100: explicit address + index-based access on the same bus
|
||||
- platform: dallas_temp
|
||||
one_wire_id: ow_100
|
||||
address: 0x1c0000031edd2a28
|
||||
name: Temp 100 by address
|
||||
resolution: 12
|
||||
- platform: dallas_temp
|
||||
one_wire_id: ow_100
|
||||
index: 0
|
||||
name: Temp 100 by index
|
||||
# DS2482-101 (sleep variant)
|
||||
- platform: dallas_temp
|
||||
one_wire_id: ow_101
|
||||
address: 0x578295491f64ff28
|
||||
name: Temp 101
|
||||
# DS2482-800: sensors on a few of the eight channels
|
||||
- platform: dallas_temp
|
||||
one_wire_id: ow_800_0
|
||||
address: 0x1c0000031edd2a28
|
||||
name: Temp 800 CH0
|
||||
- platform: dallas_temp
|
||||
one_wire_id: ow_800_3
|
||||
index: 0
|
||||
name: Temp 800 CH3 by index
|
||||
- platform: dallas_temp
|
||||
one_wire_id: ow_800_7
|
||||
address: 0x2800000123456789
|
||||
name: Temp 800 CH7
|
||||
# DS2484 (adjustable timing)
|
||||
- platform: dallas_temp
|
||||
one_wire_id: ow_2484
|
||||
address: 0x1c0000031edd2a28
|
||||
name: Temp 2484
|
||||
resolution: 12
|
||||
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -76,6 +76,14 @@ display:
|
||||
|
||||
- platform: epaper_spi
|
||||
model: seeed-reterminal-e1002
|
||||
- platform: epaper_spi
|
||||
model: seeed-reterminal-e1004
|
||||
cs_pin: 33
|
||||
cs1_pin: 34
|
||||
dc_pin: 35
|
||||
reset_pin: 36
|
||||
busy_pin: 37
|
||||
enable_pin: 39
|
||||
- platform: epaper_spi
|
||||
model: seeed-ee04-mono-4.26
|
||||
full_update_every: 10
|
||||
@@ -204,6 +212,29 @@ display:
|
||||
it.circle(it.get_width() / 2, it.get_height() / 2, 20, Color::BLACK);
|
||||
it.circle(it.get_width() / 2, it.get_height() / 2, 15, Color(255, 0, 0));
|
||||
|
||||
# Soldered Inkplate 6COLOR 7-color e-paper (600x448, UC8159-family)
|
||||
- platform: epaper_spi
|
||||
spi_id: spi_bus
|
||||
model: inkplate6color
|
||||
cs_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO5
|
||||
dc_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO17
|
||||
reset_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO16
|
||||
busy_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO4
|
||||
inverted: true
|
||||
lambda: |-
|
||||
it.filled_rectangle(0, 0, it.get_width(), it.get_height(), Color::WHITE);
|
||||
it.circle(it.get_width() / 2, it.get_height() / 2, 30, Color::BLACK);
|
||||
it.circle(it.get_width() / 2, it.get_height() / 2, 20, Color(255, 0, 0));
|
||||
it.circle(it.get_width() / 2, it.get_height() / 2, 10, Color(255, 165, 0));
|
||||
|
||||
# Waveshare 7.5" V2 BWR (800x480, UC8179 controller, EDP_7in5b_V2)
|
||||
- platform: epaper_spi
|
||||
spi_id: spi_bus
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
esphome:
|
||||
name: e1004-test
|
||||
friendly_name: E1004 Test
|
||||
|
||||
esp32:
|
||||
board: esp32-s3-devkitc-1
|
||||
variant: esp32s3
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
psram:
|
||||
mode: octal
|
||||
|
||||
spi:
|
||||
- id: epaper_spi_bus
|
||||
clk_pin: GPIO7
|
||||
mosi_pin: GPIO9
|
||||
|
||||
display:
|
||||
- platform: epaper_spi
|
||||
spi_id: epaper_spi_bus
|
||||
model: seeed-reterminal-e1004
|
||||
update_interval: never
|
||||
lambda: |-
|
||||
it.fill(Color::WHITE);
|
||||
it.rectangle(10, 10, it.get_width() - 20, it.get_height() - 20, Color::BLACK);
|
||||
it.print(it.get_width() / 2, it.get_height() / 2, id(my_font), Color::BLACK, TextAlign::CENTER, "E1004 Test");
|
||||
it.circle(100, 100, 30, Color(255, 0, 0));
|
||||
it.circle(200, 100, 30, Color(0, 255, 0));
|
||||
it.circle(300, 100, 30, Color(0, 0, 255));
|
||||
it.circle(400, 100, 30, Color(255, 255, 0));
|
||||
|
||||
font:
|
||||
- file: "gfonts://Roboto"
|
||||
id: my_font
|
||||
size: 20
|
||||
|
||||
logger:
|
||||
@@ -0,0 +1,9 @@
|
||||
esp32:
|
||||
variant: esp32s3
|
||||
framework:
|
||||
type: esp-idf
|
||||
advanced:
|
||||
nvs_encryption:
|
||||
key_id: 0
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -0,0 +1,22 @@
|
||||
esphome:
|
||||
project:
|
||||
name: esphome.downgrade_test
|
||||
version: "1.2.3"
|
||||
|
||||
esp32:
|
||||
variant: esp32s3
|
||||
framework:
|
||||
type: esp-idf
|
||||
advanced:
|
||||
enable_ota_downgrade_protection: true
|
||||
signed_ota_verification:
|
||||
signing_key: ../../components/esp32/dummy_signing_key.pem
|
||||
signing_scheme: rsa3072
|
||||
|
||||
# wifi + ota so the IDF OTA backend compiles with USE_OTA_DOWNGRADE_PROTECTION.
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
ota:
|
||||
- platform: esphome
|
||||
@@ -21,7 +21,7 @@ esp32:
|
||||
disable_fatfs: true
|
||||
|
||||
ota:
|
||||
platform: esphome
|
||||
- platform: esphome
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# Secure Boot V2 schemes carry the public key inside each image's signature
|
||||
# block, so verifying externally-signed binaries needs no key in the config:
|
||||
# a bare block enables verification with the default rsa3072 scheme.
|
||||
esp32:
|
||||
variant: esp32s3
|
||||
framework:
|
||||
type: esp-idf
|
||||
advanced:
|
||||
signed_ota_verification:
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -2,6 +2,7 @@ espnow:
|
||||
id: espnow_component
|
||||
auto_add_peer: false
|
||||
channel: 1
|
||||
max_payload_size: 1470
|
||||
peers:
|
||||
- 11:22:33:44:55:66
|
||||
on_receive:
|
||||
@@ -73,6 +74,7 @@ sensor:
|
||||
id: espnow_temp_sensor
|
||||
|
||||
- platform: packet_transport
|
||||
transport_id: transport1
|
||||
provider: test-provider
|
||||
remote_id: espnow_temp_sensor
|
||||
id: remote_temp
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
image:
|
||||
- platform: file
|
||||
id: file_binary_image
|
||||
file: ../../pnglogo.png
|
||||
type: BINARY
|
||||
dither: FloydSteinberg
|
||||
- platform: file
|
||||
id: file_rgb565_image
|
||||
file: ../../pnglogo.png
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
resize: 50x50
|
||||
- platform: file
|
||||
id: file_mdi_image
|
||||
file: mdi:alert-circle-outline
|
||||
type: BINARY
|
||||
resize: 24x24
|
||||
@@ -0,0 +1,14 @@
|
||||
packages:
|
||||
spi: !include ../../test_build_components/common/spi/esp32-idf.yaml
|
||||
|
||||
display:
|
||||
- platform: ili9xxx
|
||||
id: file_main_lcd
|
||||
spi_id: spi_bus
|
||||
model: ili9342
|
||||
cs_pin: 15
|
||||
dc_pin: 13
|
||||
reset_pin: 21
|
||||
invert_colors: true
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -0,0 +1,9 @@
|
||||
display:
|
||||
- platform: sdl
|
||||
id: file_display
|
||||
auto_clear_enabled: false
|
||||
dimensions:
|
||||
width: 480
|
||||
height: 480
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -0,0 +1,22 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml
|
||||
test_display: !include ../../test_build_components/common/test_display/test_display.yaml
|
||||
|
||||
xl9535:
|
||||
id: expander
|
||||
|
||||
touchscreen:
|
||||
# Firmware downloaded from the model's default release URL and cached.
|
||||
- platform: gsl3670
|
||||
model: seeed-reterminal-d1001
|
||||
display: test_display_screen
|
||||
interrupt_pin: 18
|
||||
# Explicit firmware URL + SHA-256 override.
|
||||
- platform: gsl3670
|
||||
model: seeed-reterminal-d1001
|
||||
display: test_display_screen
|
||||
reset_pin: 10
|
||||
interrupt_pin: 11
|
||||
firmware:
|
||||
url: https://github.com/esphome-libs/gsl3670-firmware/releases/download/v1.0.0/seeed-d1001-fw.bin
|
||||
sha256: 2e50501ad83656fb6fa3d92591f9f31add4d442c8e8a79f29f5c4d335bd127a4
|
||||
@@ -1,19 +1,8 @@
|
||||
display:
|
||||
- platform: ssd1306_i2c
|
||||
i2c_id: i2c_bus
|
||||
id: gt911_ssd1306_i2c_display
|
||||
model: SSD1306_128X64
|
||||
reset_pin: ${display_reset_pin}
|
||||
pages:
|
||||
- id: gt911_page1
|
||||
lambda: |-
|
||||
it.rectangle(0, 0, it.get_width(), it.get_height());
|
||||
|
||||
touchscreen:
|
||||
- platform: gt911
|
||||
i2c_id: i2c_bus
|
||||
id: gt911_touchscreen
|
||||
display: gt911_ssd1306_i2c_display
|
||||
display: test_display_screen
|
||||
interrupt_pin: ${interrupt_pin}
|
||||
reset_pin: ${reset_pin}
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
substitutions:
|
||||
display_reset_pin: "10"
|
||||
interrupt_pin: "20"
|
||||
reset_pin: "21"
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
test_display: !include ../../test_build_components/common/test_display/test_display.yaml
|
||||
gt911: !include common.yaml
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
substitutions:
|
||||
display_reset_pin: "10"
|
||||
interrupt_pin: "12"
|
||||
reset_pin: "13"
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
test_display: !include ../../test_build_components/common/test_display/test_display.yaml
|
||||
gt911: !include common.yaml
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
substitutions:
|
||||
display_reset_pin: "10"
|
||||
interrupt_pin: "20"
|
||||
reset_pin: "21"
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
test_display: !include ../../test_build_components/common/test_display/test_display.yaml
|
||||
gt911: !include common.yaml
|
||||
|
||||
@@ -59,6 +59,24 @@ esphome:
|
||||
id: test_regression_light
|
||||
brightness: 100%
|
||||
effect: "None"
|
||||
- http_request.get:
|
||||
url: https://esphome.io
|
||||
capture_response: true
|
||||
on_response:
|
||||
then:
|
||||
# Regression test: http_request.post with json: (dict variant) inside
|
||||
# on_response of a capture_response: true request puts std::string&
|
||||
# (body) into the nested action's Ts..., which exposes a
|
||||
# const-correctness bug in HttpRequestSendAction::play() where
|
||||
# encode_json_ receives const copies of non-const reference args.
|
||||
- http_request.post:
|
||||
url: https://esphome.io
|
||||
json:
|
||||
status: "ok"
|
||||
# Same with json: lambda variant, exercises json_func_ path
|
||||
- http_request.post:
|
||||
url: https://esphome.io
|
||||
json: !lambda "root[\"status\"] = \"ok\";"
|
||||
|
||||
http_request:
|
||||
useragent: esphome/tagreader
|
||||
|
||||
@@ -1,85 +1,104 @@
|
||||
image:
|
||||
- id: binary_image
|
||||
- platform: file
|
||||
id: binary_image
|
||||
file: ../../pnglogo.png
|
||||
type: BINARY
|
||||
dither: FloydSteinberg
|
||||
- id: transparent_transparent_image
|
||||
- platform: file
|
||||
id: transparent_transparent_image
|
||||
file: ../../pnglogo.png
|
||||
type: BINARY
|
||||
transparency: chroma_key
|
||||
|
||||
- id: rgba_image
|
||||
- platform: file
|
||||
id: rgba_image
|
||||
file: ../../pnglogo.png
|
||||
type: RGB
|
||||
transparency: alpha_channel
|
||||
resize: 50x50
|
||||
- id: rgb24_image
|
||||
- platform: file
|
||||
id: rgb24_image
|
||||
file: ../../pnglogo.png
|
||||
type: RGB
|
||||
transparency: chroma_key
|
||||
- id: rgb_image
|
||||
- platform: file
|
||||
id: rgb_image
|
||||
file: ../../pnglogo.png
|
||||
type: RGB
|
||||
transparency: opaque
|
||||
|
||||
- id: rgb565_image
|
||||
- platform: file
|
||||
id: rgb565_image
|
||||
file: ../../pnglogo.png
|
||||
type: RGB565
|
||||
transparency: opaque
|
||||
- id: rgb565_ck_image
|
||||
- platform: file
|
||||
id: rgb565_ck_image
|
||||
file: ../../pnglogo.png
|
||||
type: RGB565
|
||||
transparency: chroma_key
|
||||
- id: rgb565_alpha_image
|
||||
- platform: file
|
||||
id: rgb565_alpha_image
|
||||
file: ../../pnglogo.png
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
|
||||
- id: grayscale_alpha_image
|
||||
- platform: file
|
||||
id: grayscale_alpha_image
|
||||
file: ../../pnglogo.png
|
||||
type: grayscale
|
||||
transparency: alpha_channel
|
||||
resize: 50x50
|
||||
- id: grayscale_ck_image
|
||||
- platform: file
|
||||
id: grayscale_ck_image
|
||||
file: ../../pnglogo.png
|
||||
type: grayscale
|
||||
transparency: chroma_key
|
||||
- id: grayscale_image
|
||||
- platform: file
|
||||
id: grayscale_image
|
||||
file: ../../pnglogo.png
|
||||
type: grayscale
|
||||
transparency: opaque
|
||||
|
||||
- id: web_svg_image
|
||||
- platform: file
|
||||
id: web_svg_image
|
||||
file: https://media.esphome.io/logo/logo.svg
|
||||
resize: 256x48
|
||||
type: BINARY
|
||||
transparency: chroma_key
|
||||
- id: web_tiff_image
|
||||
- platform: file
|
||||
id: web_tiff_image
|
||||
file: https://media.esphome.io/tests/images/SIPI_Jelly_Beans_4.1.07.tiff
|
||||
type: RGB
|
||||
resize: 48x48
|
||||
- id: web_redirect_image
|
||||
- platform: file
|
||||
id: web_redirect_image
|
||||
file: https://media.esphome.io/logo/logo.png
|
||||
type: RGB
|
||||
resize: 48x48
|
||||
- id: mdi_alert
|
||||
- platform: file
|
||||
id: mdi_alert
|
||||
type: BINARY
|
||||
file: mdi:alert-circle-outline
|
||||
resize: 50x50
|
||||
- id: another_alert_icon
|
||||
- platform: file
|
||||
id: another_alert_icon
|
||||
file: mdi:alert-outline
|
||||
type: BINARY
|
||||
- file: mdil:arrange-bring-to-front
|
||||
- platform: file
|
||||
file: mdil:arrange-bring-to-front
|
||||
id: mdil_id
|
||||
resize: 50x50
|
||||
type: binary
|
||||
transparency: chroma_key
|
||||
- file: mdi:beer
|
||||
- platform: file
|
||||
file: mdi:beer
|
||||
id: mdi_id
|
||||
resize: 50x50
|
||||
type: binary
|
||||
transparency: chroma_key
|
||||
- file: memory:alert-octagon
|
||||
- platform: file
|
||||
file: memory:alert-octagon
|
||||
id: memory_id
|
||||
resize: 50x50
|
||||
type: binary
|
||||
|
||||
@@ -12,12 +12,11 @@ display:
|
||||
invert_colors: true
|
||||
|
||||
image:
|
||||
defaults:
|
||||
- platform: file
|
||||
id: test_image
|
||||
file: ../../pnglogo.png
|
||||
type: rgb565
|
||||
transparency: opaque
|
||||
byte_order: little_endian
|
||||
resize: 50x50
|
||||
dither: FloydSteinberg
|
||||
images:
|
||||
- id: test_image
|
||||
file: ../../pnglogo.png
|
||||
|
||||
@@ -7,43 +7,60 @@ display:
|
||||
height: 480
|
||||
|
||||
image:
|
||||
binary:
|
||||
- id: binary_image
|
||||
file: ../../pnglogo.png
|
||||
dither: FloydSteinberg
|
||||
- id: transparent_transparent_image
|
||||
file: ../../pnglogo.png
|
||||
transparency: chroma_key
|
||||
rgb:
|
||||
alpha_channel:
|
||||
- id: rgba_image
|
||||
file: ../../pnglogo.png
|
||||
resize: 50x50
|
||||
chroma_key:
|
||||
- id: rgb24_image
|
||||
file: ../../pnglogo.png
|
||||
type: RGB
|
||||
opaque:
|
||||
- id: rgb_image
|
||||
file: ../../pnglogo.png
|
||||
rgb565:
|
||||
- id: rgb565_image
|
||||
file: ../../pnglogo.png
|
||||
transparency: opaque
|
||||
- id: rgb565_ck_image
|
||||
file: ../../pnglogo.png
|
||||
transparency: chroma_key
|
||||
- id: rgb565_alpha_image
|
||||
file: ../../pnglogo.png
|
||||
transparency: alpha_channel
|
||||
grayscale:
|
||||
- id: grayscale_alpha_image
|
||||
file: ../../pnglogo.png
|
||||
transparency: alpha_channel
|
||||
resize: 50x50
|
||||
- id: grayscale_ck_image
|
||||
file: ../../pnglogo.png
|
||||
transparency: chroma_key
|
||||
- id: grayscale_image
|
||||
file: ../../pnglogo.png
|
||||
transparency: opaque
|
||||
- platform: file
|
||||
id: binary_image
|
||||
file: ../../pnglogo.png
|
||||
type: binary
|
||||
dither: FloydSteinberg
|
||||
- platform: file
|
||||
id: transparent_transparent_image
|
||||
file: ../../pnglogo.png
|
||||
type: binary
|
||||
transparency: chroma_key
|
||||
- platform: file
|
||||
id: rgba_image
|
||||
file: ../../pnglogo.png
|
||||
type: rgb
|
||||
transparency: alpha_channel
|
||||
resize: 50x50
|
||||
- platform: file
|
||||
id: rgb24_image
|
||||
file: ../../pnglogo.png
|
||||
type: RGB
|
||||
transparency: chroma_key
|
||||
- platform: file
|
||||
id: rgb_image
|
||||
file: ../../pnglogo.png
|
||||
type: rgb
|
||||
transparency: opaque
|
||||
- platform: file
|
||||
id: rgb565_image
|
||||
file: ../../pnglogo.png
|
||||
type: rgb565
|
||||
transparency: opaque
|
||||
- platform: file
|
||||
id: rgb565_ck_image
|
||||
file: ../../pnglogo.png
|
||||
type: rgb565
|
||||
transparency: chroma_key
|
||||
- platform: file
|
||||
id: rgb565_alpha_image
|
||||
file: ../../pnglogo.png
|
||||
type: rgb565
|
||||
transparency: alpha_channel
|
||||
- platform: file
|
||||
id: grayscale_alpha_image
|
||||
file: ../../pnglogo.png
|
||||
type: grayscale
|
||||
transparency: alpha_channel
|
||||
resize: 50x50
|
||||
- platform: file
|
||||
id: grayscale_ck_image
|
||||
file: ../../pnglogo.png
|
||||
type: grayscale
|
||||
transparency: chroma_key
|
||||
- platform: file
|
||||
id: grayscale_image
|
||||
file: ../../pnglogo.png
|
||||
type: grayscale
|
||||
transparency: opaque
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# Legacy top-level `image:` defaults/images form (deprecated; migrates to
|
||||
# `platform: file`). Config-only test exercising the deprecation/migration path,
|
||||
# including the per-type byte_order drop when an entry overrides to a non-endian
|
||||
# type (binary).
|
||||
display:
|
||||
- platform: sdl
|
||||
id: image_display
|
||||
auto_clear_enabled: false
|
||||
dimensions:
|
||||
width: 480
|
||||
height: 480
|
||||
|
||||
image:
|
||||
defaults:
|
||||
type: rgb565
|
||||
transparency: opaque
|
||||
byte_order: little_endian
|
||||
resize: 50x50
|
||||
dither: FloydSteinberg
|
||||
images:
|
||||
- id: legacy_defaults_image
|
||||
file: ../../pnglogo.png
|
||||
- id: legacy_defaults_binary
|
||||
file: ../../pnglogo.png
|
||||
type: binary
|
||||
@@ -0,0 +1,24 @@
|
||||
# Legacy top-level `image:` structured form using single-dict (non-list) values
|
||||
# for `images:`, a type group, and a transparency group -- the old `ensure_list`
|
||||
# accepted a bare dict in each of these places. Deprecated; migrates to
|
||||
# `platform: file`. Config-only test exercising the deprecation/migration path.
|
||||
display:
|
||||
- platform: sdl
|
||||
id: image_display
|
||||
auto_clear_enabled: false
|
||||
dimensions:
|
||||
width: 480
|
||||
height: 480
|
||||
|
||||
image:
|
||||
images:
|
||||
id: legacy_images_single_dict
|
||||
file: ../../pnglogo.png
|
||||
type: rgb565
|
||||
rgb565:
|
||||
id: legacy_grouped_type_single_dict
|
||||
file: ../../pnglogo.png
|
||||
rgb:
|
||||
alpha_channel:
|
||||
id: legacy_grouped_transparency_single_dict
|
||||
file: ../../pnglogo.png
|
||||
@@ -0,0 +1,25 @@
|
||||
# Legacy top-level `image:` type-grouped form (deprecated; migrates to
|
||||
# `platform: file`). Config-only test exercising the deprecation/migration path.
|
||||
display:
|
||||
- platform: sdl
|
||||
id: image_display
|
||||
auto_clear_enabled: false
|
||||
dimensions:
|
||||
width: 480
|
||||
height: 480
|
||||
|
||||
image:
|
||||
binary:
|
||||
- id: legacy_grouped_binary
|
||||
file: ../../pnglogo.png
|
||||
rgb:
|
||||
alpha_channel:
|
||||
- id: legacy_grouped_rgba
|
||||
file: ../../pnglogo.png
|
||||
opaque:
|
||||
- id: legacy_grouped_rgb
|
||||
file: ../../pnglogo.png
|
||||
rgb565:
|
||||
- id: legacy_grouped_rgb565
|
||||
file: ../../pnglogo.png
|
||||
transparency: chroma_key
|
||||
@@ -0,0 +1,16 @@
|
||||
# Legacy top-level `image:` single-dict form (a bare image dict instead of a
|
||||
# list; deprecated, migrates to `platform: file`). Config-only test exercising
|
||||
# the deprecation/migration path.
|
||||
display:
|
||||
- platform: sdl
|
||||
id: image_display
|
||||
auto_clear_enabled: false
|
||||
dimensions:
|
||||
width: 480
|
||||
height: 480
|
||||
|
||||
image:
|
||||
id: legacy_single_image
|
||||
file: ../../pnglogo.png
|
||||
type: RGB565
|
||||
transparency: opaque
|
||||
@@ -0,0 +1,18 @@
|
||||
# Legacy top-level `image:` list form (deprecated; migrates to `platform: file`).
|
||||
# Config-only test exercising the deprecation/migration path.
|
||||
display:
|
||||
- platform: sdl
|
||||
id: image_display
|
||||
auto_clear_enabled: false
|
||||
dimensions:
|
||||
width: 480
|
||||
height: 480
|
||||
|
||||
image:
|
||||
- id: legacy_list_binary
|
||||
file: ../../pnglogo.png
|
||||
type: BINARY
|
||||
- id: legacy_list_rgb565
|
||||
file: ../../pnglogo.png
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
@@ -0,0 +1,120 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "esphome/components/light/light_call.h"
|
||||
#include "esphome/components/light/light_output.h"
|
||||
#include "esphome/components/light/light_state.h"
|
||||
|
||||
namespace esphome::light::testing {
|
||||
|
||||
namespace {
|
||||
|
||||
// A light that only supports ON_OFF, like the `binary` platform and `status_led`.
|
||||
class OnOffOutput : public LightOutput {
|
||||
public:
|
||||
LightTraits get_traits() override {
|
||||
LightTraits traits;
|
||||
traits.set_supported_color_modes({ColorMode::ON_OFF});
|
||||
return traits;
|
||||
}
|
||||
void write_state(LightState *state) override {}
|
||||
};
|
||||
|
||||
// A dimmable light, like the `monochromatic` platform.
|
||||
class BrightnessOutput : public LightOutput {
|
||||
public:
|
||||
LightTraits get_traits() override {
|
||||
LightTraits traits;
|
||||
traits.set_supported_color_modes({ColorMode::BRIGHTNESS});
|
||||
return traits;
|
||||
}
|
||||
void write_state(LightState *state) override {}
|
||||
};
|
||||
|
||||
// validate_() is where zero brightness is resolved against the light's capabilities.
|
||||
class TestableLightCall : public LightCall {
|
||||
public:
|
||||
using LightCall::LightCall;
|
||||
using LightCall::validate_;
|
||||
};
|
||||
|
||||
bool as_binary(const LightColorValues &values) {
|
||||
bool binary;
|
||||
values.as_binary(&binary);
|
||||
return binary;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// An ON/OFF light has no "on but dark" state, so a zero brightness -- how effects encode
|
||||
// their dark phase -- must turn the light off. Regression test for
|
||||
// https://github.com/esphome/esphome/issues/17873.
|
||||
TEST(LightCallOnOff, ZeroBrightnessTurnsOutputOff) {
|
||||
OnOffOutput output;
|
||||
LightState state(&output);
|
||||
TestableLightCall call(&state);
|
||||
|
||||
call.set_state(true).set_brightness(0.0f);
|
||||
auto values = call.validate_();
|
||||
|
||||
EXPECT_FALSE(as_binary(values));
|
||||
}
|
||||
|
||||
// The zero must not be stored, or no later turn-on could clear it: the capability check in
|
||||
// validate_() drops any brightness an ON/OFF light doesn't support, so a stored zero would
|
||||
// leave the light permanently off.
|
||||
TEST(LightCallOnOff, ZeroBrightnessIsNotStored) {
|
||||
OnOffOutput output;
|
||||
LightState state(&output);
|
||||
|
||||
TestableLightCall dark_call(&state);
|
||||
dark_call.set_state(true).set_brightness(0.0f);
|
||||
state.remote_values = dark_call.validate_();
|
||||
|
||||
EXPECT_FLOAT_EQ(state.remote_values.get_brightness(), 1.0f);
|
||||
|
||||
// A plain turn-on afterwards must switch the light back on.
|
||||
TestableLightCall on_call(&state);
|
||||
on_call.set_state(true);
|
||||
auto values = on_call.validate_();
|
||||
|
||||
EXPECT_TRUE(as_binary(values));
|
||||
}
|
||||
|
||||
// A plain turn-on with no brightness must still light up.
|
||||
TEST(LightCallOnOff, PlainTurnOnIsVisible) {
|
||||
OnOffOutput output;
|
||||
LightState state(&output);
|
||||
TestableLightCall call(&state);
|
||||
|
||||
call.set_state(true);
|
||||
auto values = call.validate_();
|
||||
|
||||
EXPECT_TRUE(as_binary(values));
|
||||
}
|
||||
|
||||
TEST(LightCallOnOff, TurnOffTurnsOutputOff) {
|
||||
OnOffOutput output;
|
||||
LightState state(&output);
|
||||
TestableLightCall call(&state);
|
||||
|
||||
call.set_state(false);
|
||||
auto values = call.validate_();
|
||||
|
||||
EXPECT_FALSE(as_binary(values));
|
||||
}
|
||||
|
||||
// A dimmable light can represent "on but dark", so zero brightness must be kept as-is and
|
||||
// must not be rewritten into a turn-off.
|
||||
TEST(LightCallBrightness, ZeroBrightnessStaysOnButDark) {
|
||||
BrightnessOutput output;
|
||||
LightState state(&output);
|
||||
TestableLightCall call(&state);
|
||||
|
||||
call.set_state(true).set_brightness(0.0f);
|
||||
auto values = call.validate_();
|
||||
|
||||
EXPECT_TRUE(values.is_on());
|
||||
EXPECT_FLOAT_EQ(values.get_brightness(), 0.0f);
|
||||
}
|
||||
|
||||
} // namespace esphome::light::testing
|
||||
@@ -0,0 +1,2 @@
|
||||
ln882h_ble:
|
||||
enable_on_boot: true
|
||||
@@ -0,0 +1,2 @@
|
||||
packages:
|
||||
ln882h_ble: !include common.yaml
|
||||
@@ -91,8 +91,8 @@ binary_sensor:
|
||||
animation: move_right
|
||||
time: 600ms
|
||||
- platform: lvgl
|
||||
id: button_checker
|
||||
name: LVGL button
|
||||
id: common_button_checker
|
||||
name: Common button
|
||||
widget: spin_up
|
||||
on_state:
|
||||
then:
|
||||
|
||||
@@ -24,20 +24,6 @@ binary_sensor:
|
||||
name: Button A checked
|
||||
widget: button_a
|
||||
state: checked
|
||||
- platform: lvgl
|
||||
id: button_checker
|
||||
name: LVGL button
|
||||
widget: button_button
|
||||
state: checked
|
||||
on_state:
|
||||
then:
|
||||
- lvgl.checkbox.update:
|
||||
id: checkbox_id
|
||||
state:
|
||||
checked: !lambda |-
|
||||
auto y = x; // block inlining of one line return
|
||||
return y;
|
||||
|
||||
- platform: lvgl
|
||||
id: button_presser
|
||||
name: Button pressed
|
||||
@@ -49,6 +35,9 @@ lvgl:
|
||||
rotation: 90
|
||||
log_level: debug
|
||||
resume_on_input: true
|
||||
paused: true
|
||||
update_when_display_idle: true
|
||||
refresh_interval: 30ms
|
||||
on_pause:
|
||||
- logger.log: LVGL is Paused
|
||||
- lvgl.display.set_rotation: 90
|
||||
@@ -57,6 +46,40 @@ lvgl:
|
||||
- lvgl.display.set_rotation:
|
||||
rotation: 0
|
||||
lvgl_id: lvgl_id
|
||||
- lvgl.display.set_rotation:
|
||||
rotation: !lambda "return 180;"
|
||||
lvgl_id: lvgl_id
|
||||
on_landscape:
|
||||
- logger.log: LVGL display is now landscape
|
||||
# Re-layout a container in response to orientation changes. The layout type
|
||||
# and grid structure are fixed at creation; only the style options change.
|
||||
- lvgl.widget.update:
|
||||
id: grid_rows_only_shorthand
|
||||
layout:
|
||||
grid_column_align: center
|
||||
grid_row_align: space_between
|
||||
pad_row: 4px
|
||||
- lvgl.update:
|
||||
top_layer:
|
||||
layout:
|
||||
flex_flow: row
|
||||
on_portrait:
|
||||
- logger.log: LVGL display is now portrait
|
||||
- lvgl.widget.update:
|
||||
id: grid_rows_only_shorthand
|
||||
layout:
|
||||
grid_column_align: start
|
||||
pad_row: 2px
|
||||
# Top-level layout applies to the active screen
|
||||
- lvgl.update:
|
||||
layout:
|
||||
flex_flow: column
|
||||
pad_row: 8px
|
||||
- lvgl.update:
|
||||
top_layer:
|
||||
layout:
|
||||
flex_flow: column
|
||||
flex_align_main: center
|
||||
|
||||
on_boot:
|
||||
- logger.log: LVGL has started
|
||||
@@ -64,6 +87,12 @@ lvgl:
|
||||
id: meter_arc_indicator
|
||||
start_value: 0
|
||||
end_value: 180
|
||||
- lvgl.animation.start:
|
||||
id:
|
||||
- anim_slide
|
||||
- anim_color
|
||||
duration: 3s
|
||||
loop: true
|
||||
on_invalidate_area:
|
||||
logger.log: Invalidate area
|
||||
on_resolution_change:
|
||||
@@ -108,6 +137,52 @@ lvgl:
|
||||
- obj:
|
||||
bg_color: 0x000000
|
||||
bg_opa: cover
|
||||
top_layer:
|
||||
widgets:
|
||||
- obj:
|
||||
id: anim_box
|
||||
x: 0
|
||||
y: 0
|
||||
width: 50
|
||||
height: 50
|
||||
bg_color: 0xFF0000
|
||||
- label:
|
||||
id: anim_label
|
||||
text: anim
|
||||
animations:
|
||||
- id: anim_slide
|
||||
duration: 1s
|
||||
start_delay: 100ms
|
||||
auto_start: true
|
||||
loop: true
|
||||
timing: ease_in_out
|
||||
on_start:
|
||||
- logger.log: anim started
|
||||
on_stop:
|
||||
- logger.log: anim stopped
|
||||
widgets:
|
||||
- id: anim_box
|
||||
x:
|
||||
from: 0
|
||||
to: 100
|
||||
y:
|
||||
from: 0
|
||||
to: !lambda "return 80;"
|
||||
opa:
|
||||
from: 50%
|
||||
to: 100%
|
||||
- id: anim_color
|
||||
duration: 2s
|
||||
timing:
|
||||
- round_trip
|
||||
- type: gravity
|
||||
bounce: 0.3
|
||||
acceleration: 0.8
|
||||
widgets:
|
||||
- id: anim_label
|
||||
text_color:
|
||||
from: 0xFF0000
|
||||
to: color_id
|
||||
theme:
|
||||
dark_mode: true
|
||||
obj:
|
||||
@@ -210,13 +285,37 @@ lvgl:
|
||||
on_click:
|
||||
then:
|
||||
- lvgl.display.set_rotation: 0
|
||||
- lvgl.animation.stop: anim_slide
|
||||
- lvgl.animation.stop:
|
||||
id:
|
||||
- anim_slide
|
||||
- anim_color
|
||||
- lvgl.widget.hide: message_box
|
||||
- lvgl.style.update:
|
||||
id: style_test
|
||||
bg_color: blue
|
||||
bg_opa: !lambda return 0.5;
|
||||
# `obj` is already themed above -- exercises updating an existing hidden style.
|
||||
- lvgl.theme.update:
|
||||
obj:
|
||||
border_width: 2
|
||||
# `label` is never mentioned under `theme:` -- exercises lazily creating the
|
||||
# hidden style and getting it attached to already-built label widgets.
|
||||
- lvgl.theme.update:
|
||||
label:
|
||||
text_color: red
|
||||
# `button` is never mentioned under `theme:`, and only a non-default state is
|
||||
# targeted here -- exercises that no spurious, empty main/default style is
|
||||
# created (and attached to every button) alongside the requested one.
|
||||
- lvgl.theme.update:
|
||||
button:
|
||||
pressed:
|
||||
bg_color: red
|
||||
- lvgl.image.update:
|
||||
id: lv_image
|
||||
src:
|
||||
mapping: image_map
|
||||
value: !lambda return round(1.0);
|
||||
scale: !lambda return 512;
|
||||
rotation: !lambda return 100;
|
||||
pivot_x: !lambda return 20;
|
||||
@@ -342,9 +441,16 @@ lvgl:
|
||||
text_font: montserrat_40
|
||||
border_post: true
|
||||
on_press:
|
||||
lvgl.label.update:
|
||||
id: hello_label
|
||||
text: Goodbye
|
||||
- lvgl.label.update:
|
||||
id: hello_label
|
||||
text:
|
||||
mapping: lvgl_string_map
|
||||
value: !lambda return 2;
|
||||
- lvgl.label.update:
|
||||
id: hello_label
|
||||
text:
|
||||
mapping: lvgl_string_map
|
||||
value: 2
|
||||
on_click:
|
||||
then:
|
||||
- lvgl.animimg.stop: anim_img
|
||||
@@ -1450,6 +1556,21 @@ image:
|
||||
invert_alpha: true
|
||||
transparency: alpha_channel
|
||||
|
||||
mapping:
|
||||
- id: image_map
|
||||
from: int
|
||||
to: image
|
||||
entries:
|
||||
0: cat_image
|
||||
1: dog_image
|
||||
- id: lvgl_string_map
|
||||
from: int
|
||||
to: string
|
||||
entries:
|
||||
0: "First"
|
||||
1: "Second"
|
||||
2: "Third"
|
||||
|
||||
color:
|
||||
- id: light_blue
|
||||
hex: "3340FF"
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
packages:
|
||||
lvgl: !include lvgl-package.yaml
|
||||
lvgl_package: !include lvgl-package.yaml
|
||||
spi: !include ../../test_build_components/common/spi/esp32-idf.yaml
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
lvgl: !include common.yaml
|
||||
|
||||
sensor:
|
||||
- platform: rotary_encoder
|
||||
@@ -77,5 +78,3 @@ lvgl:
|
||||
- component.update: tft_display
|
||||
- delay: 60s
|
||||
- lvgl.resume:
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -22,6 +22,36 @@ lvgl:
|
||||
displays: sdl0
|
||||
rotation: 180
|
||||
top_layer:
|
||||
widgets:
|
||||
- obj:
|
||||
id: anim_box
|
||||
x: 0
|
||||
y: 0
|
||||
width: 40
|
||||
height: 40
|
||||
bg_color: 0xFF0000
|
||||
animations:
|
||||
- id: anim_slide
|
||||
duration: 1s
|
||||
start_delay: 100ms
|
||||
auto_start: true
|
||||
loop: true
|
||||
timing:
|
||||
- round_trip
|
||||
- type: ease_in_out
|
||||
weight: 3
|
||||
on_start:
|
||||
- logger.log: anim started
|
||||
on_stop:
|
||||
- logger.log: anim stopped
|
||||
widgets:
|
||||
- id: anim_box
|
||||
x:
|
||||
from: 0
|
||||
to: !lambda "return 100;"
|
||||
opa:
|
||||
from: 50%
|
||||
to: 100%
|
||||
|
||||
- id: lvgl_1
|
||||
displays: sdl1
|
||||
@@ -42,7 +72,14 @@ lvgl:
|
||||
- label:
|
||||
text: Click ME
|
||||
on_click:
|
||||
logger.log: Clicked
|
||||
then:
|
||||
- logger.log: Clicked
|
||||
- lvgl.animation.stop:
|
||||
id: anim_slide
|
||||
lvgl_id: lvgl_0
|
||||
- lvgl.animation.start:
|
||||
id: anim_slide
|
||||
lvgl_id: lvgl_0
|
||||
|
||||
font:
|
||||
- file: "gfonts://Roboto"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# md5's to_code calls cg.add_define("USE_MD5"), which gates md5.h. C++ unit
|
||||
# test builds that pull md5 in transitively (e.g. ota's host backend, which
|
||||
# has an md5::MD5Digest member) need that define, otherwise md5.h compiles to
|
||||
# nothing and the dependent headers fail to find md5::MD5Digest.
|
||||
manifest.enable_codegen()
|
||||
@@ -0,0 +1,7 @@
|
||||
# No api, web_server or extra services so the fallback _http service
|
||||
# (with version, mac and config_hash TXT records) is compiled.
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
mdns:
|
||||
@@ -0,0 +1,9 @@
|
||||
# web_server without the native api so the version, mac and config_hash
|
||||
# TXT records are attached to the web_server _http service.
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
web_server:
|
||||
|
||||
mdns:
|
||||
@@ -1,5 +1,5 @@
|
||||
packages:
|
||||
- !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml
|
||||
|
||||
psram:
|
||||
mode: octal
|
||||
|
||||
@@ -37,6 +37,9 @@ class MockUARTComponent : public uart::UARTComponent {
|
||||
MOCK_METHOD(bool, peek_byte, (uint8_t * data), (override));
|
||||
MOCK_METHOD(uart::UARTFlushResult, flush, (), (override));
|
||||
MOCK_METHOD(void, check_logger_conflict, (), (override));
|
||||
#if defined(USE_ESP8266) || defined(USE_ESP32)
|
||||
void load_settings(bool dump_config) override {}
|
||||
#endif // defined(USE_ESP8266) || defined(USE_ESP32)
|
||||
};
|
||||
|
||||
class TestableMitsubishiCN105 : public MitsubishiCN105 {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include "esphome/components/uart/uart_component.h"
|
||||
|
||||
namespace esphome::modbus::testing {
|
||||
|
||||
// A UART that discards all writes, for tests that never inspect the wire.
|
||||
class NullUART : public uart::UARTComponent {
|
||||
public:
|
||||
NullUART() { this->set_baud_rate(115200); }
|
||||
void write_array(const uint8_t *data, size_t len) override {}
|
||||
bool peek_byte(uint8_t *data) override { return false; }
|
||||
bool read_array(uint8_t *data, size_t len) override { return false; }
|
||||
size_t available() override { return 0; }
|
||||
uart::UARTFlushResult flush() override { return uart::UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS; }
|
||||
#if defined(USE_ESP8266) || defined(USE_ESP32)
|
||||
void load_settings(bool dump_config) override {}
|
||||
#endif
|
||||
void check_logger_conflict() override {}
|
||||
};
|
||||
|
||||
} // namespace esphome::modbus::testing
|
||||
@@ -0,0 +1,223 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/components/modbus/modbus.h"
|
||||
|
||||
// The allocation counters rely on AddressSanitizer's malloc hooks. The cpp_unit_test harness always
|
||||
// builds with ASan, so this is exercised in CI; the fallback only applies to out-of-harness builds.
|
||||
#ifndef __has_feature
|
||||
#define __has_feature(x) 0
|
||||
#endif
|
||||
#if defined(__SANITIZE_ADDRESS__) || __has_feature(address_sanitizer)
|
||||
#define HEAP_PROBE_HAS_ASAN
|
||||
#endif
|
||||
|
||||
#ifdef HEAP_PROBE_HAS_ASAN
|
||||
|
||||
// Allocation counters fed by ASan's malloc hooks; sampled tightly around the calls under test.
|
||||
static std::atomic<size_t> g_alloc_count{0};
|
||||
static std::atomic<size_t> g_alloc_bytes{0};
|
||||
|
||||
static void malloc_hook(const volatile void *, size_t size) {
|
||||
g_alloc_count++;
|
||||
g_alloc_bytes += size;
|
||||
}
|
||||
static void free_hook(const volatile void *) {}
|
||||
|
||||
extern "C" int __sanitizer_install_malloc_and_free_hooks(void (*malloc_hook)(const volatile void *, size_t),
|
||||
void (*free_hook)(const volatile void *));
|
||||
|
||||
[[maybe_unused]] static const int g_hooks_installed = __sanitizer_install_malloc_and_free_hooks(malloc_hook, free_hook);
|
||||
|
||||
namespace esphome::modbus::testing {
|
||||
|
||||
namespace {
|
||||
|
||||
// A UART the test can inject received bytes into; sent bytes are discarded.
|
||||
class InjectableUART : public uart::UARTComponent {
|
||||
public:
|
||||
void write_array(const uint8_t *data, size_t len) override {}
|
||||
bool peek_byte(uint8_t *data) override {
|
||||
if (this->rx_.empty())
|
||||
return false;
|
||||
*data = this->rx_.front();
|
||||
return true;
|
||||
}
|
||||
bool read_array(uint8_t *data, size_t len) override {
|
||||
if (len > this->rx_.size())
|
||||
return false;
|
||||
memcpy(data, this->rx_.data(), len);
|
||||
this->rx_.erase(this->rx_.begin(), this->rx_.begin() + len);
|
||||
return true;
|
||||
}
|
||||
size_t available() override { return this->rx_.size(); }
|
||||
uart::UARTFlushResult flush() override { return uart::UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS; }
|
||||
void check_logger_conflict() override {}
|
||||
|
||||
void inject_frame(uint8_t address, std::span<const uint8_t> pdu) {
|
||||
// Wire frame: address + PDU + CRC16(low, high)
|
||||
size_t start = this->rx_.size();
|
||||
this->rx_.push_back(address);
|
||||
this->rx_.insert(this->rx_.end(), pdu.begin(), pdu.end());
|
||||
uint16_t crc = crc16(this->rx_.data() + start, this->rx_.size() - start);
|
||||
this->rx_.push_back(crc & 0xFF);
|
||||
this->rx_.push_back(crc >> 8);
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<uint8_t> rx_;
|
||||
};
|
||||
|
||||
class NullDevice : public ModbusClientDevice {
|
||||
public:
|
||||
using ModbusClientDevice::ModbusClientDevice;
|
||||
void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) override {
|
||||
this->responses++;
|
||||
}
|
||||
int responses{0};
|
||||
};
|
||||
|
||||
struct Sample {
|
||||
size_t count;
|
||||
size_t bytes;
|
||||
};
|
||||
|
||||
template<typename F> Sample sample(F &&f) {
|
||||
size_t c0 = g_alloc_count.load(), b0 = g_alloc_bytes.load();
|
||||
f();
|
||||
return {g_alloc_count.load() - c0, g_alloc_bytes.load() - b0};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Typical frames (reads and single-register/coil writes are exactly address + 5-byte PDU + CRC = 8
|
||||
// bytes) fit the SmallInlineBuffer and are built with zero heap allocations; only larger frames spill
|
||||
// to a single allocation.
|
||||
TEST(HeapProbe, TypicalFrameConstructionIsAllocationFree) {
|
||||
const uint8_t read_pdu[] = {0x03, 0x01, 0x00, 0x00, 0x02}; // 5 bytes -> 8-byte frame, inline
|
||||
Sample typical = sample([&] {
|
||||
ModbusFrame frame(0x02, read_pdu, sizeof(read_pdu));
|
||||
(void) frame;
|
||||
});
|
||||
printf("HEAPPROBE frame_typical count=%zu bytes=%zu\n", typical.count, typical.bytes);
|
||||
EXPECT_EQ(typical.count, 0u);
|
||||
|
||||
uint8_t large_pdu[250] = {0x10}; // multi-register write -> 253-byte frame, spills once
|
||||
Sample large = sample([&] {
|
||||
ModbusFrame frame(0x02, large_pdu, sizeof(large_pdu));
|
||||
(void) frame;
|
||||
});
|
||||
printf("HEAPPROBE frame_large count=%zu bytes=%zu\n", large.count, large.bytes);
|
||||
EXPECT_EQ(large.count, 1u);
|
||||
}
|
||||
|
||||
// Queueing typical commands is allocation-free within the deque's first block: the frame fits the
|
||||
// inline buffer, every entry is a plain append (ordering lives in selection, not storage), and the
|
||||
// first block is already allocated when the hub is constructed. A 512-byte deque block holds
|
||||
// 512 / sizeof(ModbusDeviceCommand) entries (16 on the 64-bit host); a deeper queue allocates more.
|
||||
TEST(HeapProbe, QueueingTypicalCommandsIsAllocationFree) {
|
||||
ModbusClientHub hub;
|
||||
ModbusClientDevice device(&hub, 0x02);
|
||||
|
||||
StaticVector<uint8_t, MAX_PDU_SIZE> req;
|
||||
const uint8_t read_pdu[] = {0x03, 0x01, 0x00, 0x00, 0x02};
|
||||
req.assign(read_pdu, read_pdu + sizeof(read_pdu));
|
||||
|
||||
constexpr int n = 12;
|
||||
static_assert(n * sizeof(ModbusDeviceCommand) < 512, "keep n within one deque block so the probe stays meaningful");
|
||||
size_t total = 0;
|
||||
for (int i = 0; i != n; i++) {
|
||||
req[2] = static_cast<uint8_t>(i); // distinct start addresses: identical frames would dedup, not enqueue
|
||||
total += sample([&] { device.send_pdu(req); }).count;
|
||||
}
|
||||
printf("HEAPPROBE queue_%d_typical_commands total_allocs=%zu\n", n, total);
|
||||
EXPECT_EQ(total, 0u);
|
||||
}
|
||||
|
||||
// A WRITE arriving behind queued reads is a plain append too - the old priority front-insert (and
|
||||
// its possible front-block allocation) is gone; the write wins transmit SELECTION instead.
|
||||
TEST(HeapProbe, WriteBehindQueuedReadsAppendsAllocationFree) {
|
||||
ModbusClientHub hub;
|
||||
ModbusClientDevice device(&hub, 0x02);
|
||||
|
||||
StaticVector<uint8_t, MAX_PDU_SIZE> req;
|
||||
const uint8_t read_pdu[] = {0x03, 0x01, 0x00, 0x00, 0x02};
|
||||
req.assign(read_pdu, read_pdu + sizeof(read_pdu));
|
||||
for (int i = 0; i != 3; i++) {
|
||||
req[2] = static_cast<uint8_t>(i); // distinct start addresses: identical frames would dedup, not enqueue
|
||||
device.send_pdu(req);
|
||||
}
|
||||
|
||||
const uint8_t write_pdu[] = {0x06, 0x00, 0x10, 0xBE, 0xEF};
|
||||
Sample append = sample([&] { device.send_pdu(write_pdu); });
|
||||
printf("HEAPPROBE write_append count=%zu bytes=%zu\n", append.count, append.bytes);
|
||||
EXPECT_EQ(append.count, 0u);
|
||||
}
|
||||
|
||||
// End to end: bytes injected at the UART travel through receive, frame parsing, response matching and
|
||||
// device dispatch. The first response may grow the hub's rx buffer once; after that warm-up, handling a
|
||||
// response performs zero heap allocations all the way to the device callback.
|
||||
TEST(HeapProbe, ResponseHandlingIsAllocationFreeAfterWarmup) {
|
||||
InjectableUART uart;
|
||||
uart.set_baud_rate(115200); // tx timing math divides by the baud rate
|
||||
ModbusClientHub hub;
|
||||
hub.set_uart_parent(&uart);
|
||||
hub.setup(); // computes frame timing from the baud rate
|
||||
NullDevice device(&hub, 0x02);
|
||||
|
||||
StaticVector<uint8_t, MAX_PDU_SIZE> req;
|
||||
const uint8_t read_pdu[] = {0x03, 0x01, 0x00, 0x00, 0x02};
|
||||
req.assign(read_pdu, read_pdu + sizeof(read_pdu));
|
||||
|
||||
// Largest possible read response first, so the rx buffer warm-up covers every later size.
|
||||
uint8_t large_resp[252] = {0x03, 250};
|
||||
const uint8_t small_resp[] = {0x03, 0x04, 0x00, 0x2A, 0x01, 0x00};
|
||||
|
||||
auto round_trip = [&](std::span<const uint8_t> response_pdu) {
|
||||
device.send_pdu(req);
|
||||
hub.loop(); // transmit; the tx queue is empty during the measured receive below
|
||||
uart.inject_frame(0x02, response_pdu);
|
||||
return sample([&] { hub.loop(); }); // receive + parse + match + dispatch
|
||||
};
|
||||
|
||||
Sample warmup = round_trip(std::span<const uint8_t>(large_resp, sizeof(large_resp)));
|
||||
Sample steady_large = round_trip(std::span<const uint8_t>(large_resp, sizeof(large_resp)));
|
||||
Sample steady_small = round_trip(small_resp);
|
||||
|
||||
printf("HEAPPROBE warmup count=%zu bytes=%zu\n", warmup.count, warmup.bytes);
|
||||
printf("HEAPPROBE steady_large count=%zu bytes=%zu\n", steady_large.count, steady_large.bytes);
|
||||
printf("HEAPPROBE steady_small count=%zu bytes=%zu\n", steady_small.count, steady_small.bytes);
|
||||
|
||||
EXPECT_EQ(device.responses, 3);
|
||||
EXPECT_LE(warmup.count, 1u); // at most the one-time rx buffer growth
|
||||
EXPECT_EQ(steady_large.count, 0u);
|
||||
EXPECT_EQ(steady_small.count, 0u);
|
||||
}
|
||||
|
||||
} // namespace esphome::modbus::testing
|
||||
|
||||
#else // !HEAP_PROBE_HAS_ASAN
|
||||
|
||||
// Stub every ASan-gated test name, so the suite's test list is identical in every build configuration.
|
||||
namespace esphome::modbus::testing {
|
||||
TEST(HeapProbe, TypicalFrameConstructionIsAllocationFree) {
|
||||
GTEST_SKIP() << "allocation counting requires an AddressSanitizer build";
|
||||
}
|
||||
TEST(HeapProbe, QueueingTypicalCommandsIsAllocationFree) {
|
||||
GTEST_SKIP() << "allocation counting requires an AddressSanitizer build";
|
||||
}
|
||||
TEST(HeapProbe, WriteBehindQueuedReadsAppendsAllocationFree) {
|
||||
GTEST_SKIP() << "allocation counting requires an AddressSanitizer build";
|
||||
}
|
||||
TEST(HeapProbe, ResponseHandlingIsAllocationFreeAfterWarmup) {
|
||||
GTEST_SKIP() << "allocation counting requires an AddressSanitizer build";
|
||||
}
|
||||
} // namespace esphome::modbus::testing
|
||||
|
||||
#endif // HEAP_PROBE_HAS_ASAN
|
||||
@@ -0,0 +1,344 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/components/modbus/modbus.h"
|
||||
|
||||
namespace esphome::modbus::testing {
|
||||
|
||||
namespace {
|
||||
|
||||
// Records every typed callback so tests can assert on the dispatch performed by the default
|
||||
// on_response()/on_error() implementations.
|
||||
class RecordingDevice : public ModbusClientDevice {
|
||||
public:
|
||||
struct ReadRegistersCall {
|
||||
uint16_t start_address;
|
||||
std::vector<uint16_t> registers;
|
||||
ResponseStatus status;
|
||||
};
|
||||
struct ReadBitsCall {
|
||||
uint16_t start_address;
|
||||
uint16_t count;
|
||||
std::vector<uint8_t> packed;
|
||||
ResponseStatus status;
|
||||
};
|
||||
struct WriteCall {
|
||||
uint16_t address;
|
||||
uint16_t value;
|
||||
ResponseStatus status;
|
||||
};
|
||||
|
||||
void on_read_holding_registers(uint16_t start_address, std::span<const uint16_t> registers,
|
||||
ResponseStatus status) override {
|
||||
this->holding_calls.push_back({start_address, {registers.begin(), registers.end()}, status});
|
||||
}
|
||||
void on_read_input_registers(uint16_t start_address, std::span<const uint16_t> registers,
|
||||
ResponseStatus status) override {
|
||||
this->input_calls.push_back({start_address, {registers.begin(), registers.end()}, status});
|
||||
}
|
||||
void on_read_coils(uint16_t start_address, PackedBits bits, ResponseStatus status) override {
|
||||
this->coil_calls.push_back({start_address, bits.size(), {bits.bytes().begin(), bits.bytes().end()}, status});
|
||||
}
|
||||
void on_read_discrete_inputs(uint16_t start_address, PackedBits bits, ResponseStatus status) override {
|
||||
this->discrete_calls.push_back({start_address, bits.size(), {bits.bytes().begin(), bits.bytes().end()}, status});
|
||||
}
|
||||
void on_write_single_register(uint16_t address, uint16_t value, ResponseStatus status) override {
|
||||
this->write_single_register_calls.push_back({address, value, status});
|
||||
}
|
||||
void on_write_single_coil(uint16_t address, bool value, ResponseStatus status) override {
|
||||
this->write_single_coil_calls.push_back({address, static_cast<uint16_t>(value), status});
|
||||
}
|
||||
void on_write_multiple_registers(uint16_t start_address, std::span<const uint16_t> registers,
|
||||
ResponseStatus status) override {
|
||||
this->write_multiple_registers_calls.push_back({start_address, {registers.begin(), registers.end()}, status});
|
||||
}
|
||||
void on_write_multiple_coils(uint16_t start_address, PackedBits bits, ResponseStatus status) override {
|
||||
this->write_multiple_coils_calls.push_back(
|
||||
{start_address, bits.size(), {bits.bytes().begin(), bits.bytes().end()}, status});
|
||||
}
|
||||
void on_custom_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu,
|
||||
ResponseStatus status) override {
|
||||
this->custom_requests.emplace_back(request_pdu.begin(), request_pdu.end());
|
||||
this->custom_responses.emplace_back(response_pdu.begin(), response_pdu.end());
|
||||
this->custom_statuses.push_back(status);
|
||||
}
|
||||
|
||||
std::vector<ReadRegistersCall> holding_calls;
|
||||
std::vector<ReadRegistersCall> input_calls;
|
||||
std::vector<ReadBitsCall> coil_calls;
|
||||
std::vector<ReadBitsCall> discrete_calls;
|
||||
std::vector<WriteCall> write_single_register_calls;
|
||||
std::vector<WriteCall> write_single_coil_calls;
|
||||
std::vector<ReadRegistersCall> write_multiple_registers_calls;
|
||||
std::vector<ReadBitsCall> write_multiple_coils_calls;
|
||||
std::vector<std::vector<uint8_t>> custom_requests;
|
||||
std::vector<std::vector<uint8_t>> custom_responses;
|
||||
std::vector<ResponseStatus> custom_statuses;
|
||||
};
|
||||
|
||||
// Overrides only the generic callbacks to verify the typed defaults delegate to them.
|
||||
class GenericDevice : public ModbusClientDevice {
|
||||
public:
|
||||
void on_read_registers(EntityType register_type, uint16_t start_address, std::span<const uint16_t> registers,
|
||||
ResponseStatus status) override {
|
||||
this->register_type = register_type;
|
||||
this->start_address = start_address;
|
||||
this->registers.assign(registers.begin(), registers.end());
|
||||
this->calls++;
|
||||
}
|
||||
void on_read_bits(EntityType register_type, uint16_t start_address, PackedBits bits, ResponseStatus status) override {
|
||||
this->register_type = register_type;
|
||||
this->start_address = start_address;
|
||||
this->bit_count = bits.size();
|
||||
this->calls++;
|
||||
}
|
||||
EntityType register_type{EntityType::CUSTOM};
|
||||
uint16_t start_address{0};
|
||||
uint16_t bit_count{0};
|
||||
std::vector<uint16_t> registers;
|
||||
int calls{0};
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(ModbusClientDeviceFanOut, ReadHoldingRegistersSuccess) {
|
||||
RecordingDevice device;
|
||||
const uint8_t request[] = {0x03, 0x01, 0x00, 0x00, 0x02}; // read 2 regs at 0x100
|
||||
const uint8_t response[] = {0x03, 0x04, 0x00, 0x2A, 0x01, 0x00}; // 0x002A, 0x0100
|
||||
device.on_response(request, response);
|
||||
|
||||
ASSERT_EQ(device.holding_calls.size(), 1u);
|
||||
const auto &call = device.holding_calls.front();
|
||||
EXPECT_EQ(call.start_address, 0x100);
|
||||
EXPECT_EQ(call.registers, (std::vector<uint16_t>{0x002A, 0x0100}));
|
||||
EXPECT_FALSE(call.status.has_value());
|
||||
}
|
||||
|
||||
TEST(ModbusClientDeviceFanOut, ReadInputRegistersDelegateToGeneric) {
|
||||
GenericDevice device;
|
||||
const uint8_t request[] = {0x04, 0x00, 0x10, 0x00, 0x01};
|
||||
const uint8_t response[] = {0x04, 0x02, 0x12, 0x34};
|
||||
device.on_response(request, response);
|
||||
|
||||
EXPECT_EQ(device.calls, 1);
|
||||
EXPECT_EQ(device.register_type, EntityType::INPUT_REGISTER);
|
||||
EXPECT_EQ(device.start_address, 0x10);
|
||||
EXPECT_EQ(device.registers, (std::vector<uint16_t>{0x1234}));
|
||||
}
|
||||
|
||||
TEST(ModbusClientDeviceFanOut, ReadDiscreteInputsDelegateToGenericBits) {
|
||||
GenericDevice device;
|
||||
const uint8_t request[] = {0x02, 0x00, 0x20, 0x00, 0x05}; // 5 inputs at 0x20
|
||||
const uint8_t response[] = {0x02, 0x01, 0x15};
|
||||
device.on_response(request, response);
|
||||
|
||||
EXPECT_EQ(device.calls, 1);
|
||||
EXPECT_EQ(device.register_type, EntityType::DISCRETE_INPUT);
|
||||
EXPECT_EQ(device.start_address, 0x20);
|
||||
EXPECT_EQ(device.bit_count, 5);
|
||||
}
|
||||
|
||||
// A CRC-valid response whose length does not match its request cannot be decoded per the
|
||||
// function-code contract: it goes to the catch-all with the raw PDUs, not to the typed callback.
|
||||
TEST(ModbusClientDeviceFanOut, ReadRegistersMismatchedLengthGoesToCatchAll) {
|
||||
RecordingDevice device;
|
||||
// Request asks for 4 registers but the response only carries 1.
|
||||
const uint8_t request[] = {0x03, 0x00, 0x00, 0x00, 0x04};
|
||||
const uint8_t response[] = {0x03, 0x02, 0xBE, 0xEF};
|
||||
device.on_response(request, response);
|
||||
|
||||
EXPECT_TRUE(device.holding_calls.empty());
|
||||
ASSERT_EQ(device.custom_responses.size(), 1u);
|
||||
EXPECT_EQ(device.custom_responses.front(), (std::vector<uint8_t>(response, response + sizeof(response))));
|
||||
}
|
||||
|
||||
// Coil responses are validated the same way: byte count must be ceil(count / 8).
|
||||
TEST(ModbusClientDeviceFanOut, ReadCoilsMismatchedLengthGoesToCatchAll) {
|
||||
RecordingDevice device;
|
||||
const uint8_t request[] = {0x01, 0x00, 0x13, 0x00, 0x13}; // 19 coils -> 3 packed bytes
|
||||
const uint8_t response[] = {0x01, 0x02, 0xCD, 0x6B}; // only 2
|
||||
device.on_response(request, response);
|
||||
|
||||
EXPECT_TRUE(device.coil_calls.empty());
|
||||
EXPECT_EQ(device.custom_responses.size(), 1u);
|
||||
}
|
||||
|
||||
TEST(ModbusClientDeviceFanOut, ReadCoilsSuccess) {
|
||||
RecordingDevice device;
|
||||
const uint8_t request[] = {0x01, 0x00, 0x13, 0x00, 0x13}; // 19 coils at 0x13
|
||||
const uint8_t response[] = {0x01, 0x03, 0xCD, 0x6B, 0x05};
|
||||
device.on_response(request, response);
|
||||
|
||||
ASSERT_EQ(device.coil_calls.size(), 1u);
|
||||
const auto &call = device.coil_calls.front();
|
||||
EXPECT_EQ(call.start_address, 0x13);
|
||||
EXPECT_EQ(call.count, 19);
|
||||
EXPECT_EQ(call.packed, (std::vector<uint8_t>{0xCD, 0x6B, 0x05}));
|
||||
EXPECT_FALSE(call.status.has_value());
|
||||
// first coil = bit 0 of byte 0
|
||||
EXPECT_TRUE(helpers::bit_from_packed(0, call.packed));
|
||||
EXPECT_FALSE(helpers::bit_from_packed(1, call.packed));
|
||||
}
|
||||
|
||||
TEST(ModbusClientDeviceFanOut, WriteSingleRegisterSuccess) {
|
||||
RecordingDevice device;
|
||||
const uint8_t request[] = {0x06, 0x00, 0x01, 0x00, 0x03};
|
||||
device.on_response(request, request); // echo
|
||||
|
||||
ASSERT_EQ(device.write_single_register_calls.size(), 1u);
|
||||
const auto &call = device.write_single_register_calls.front();
|
||||
EXPECT_EQ(call.address, 1);
|
||||
EXPECT_EQ(call.value, 3);
|
||||
EXPECT_FALSE(call.status.has_value());
|
||||
}
|
||||
|
||||
TEST(ModbusClientDeviceFanOut, WriteSingleCoilSuccess) {
|
||||
RecordingDevice device;
|
||||
const uint8_t request[] = {0x05, 0x00, 0xAC, 0xFF, 0x00};
|
||||
device.on_response(request, request);
|
||||
|
||||
ASSERT_EQ(device.write_single_coil_calls.size(), 1u);
|
||||
EXPECT_EQ(device.write_single_coil_calls.front().address, 0xAC);
|
||||
EXPECT_EQ(device.write_single_coil_calls.front().value, 1u);
|
||||
}
|
||||
|
||||
TEST(ModbusClientDeviceFanOut, WriteErrorReportsRequestArgumentsAndStatus) {
|
||||
RecordingDevice device;
|
||||
const uint8_t request[] = {0x06, 0x00, 0x01, 0x00, 0x03};
|
||||
const uint8_t exception[] = {0x86, 0x02}; // ILLEGAL_DATA_ADDRESS
|
||||
device.on_error(request, static_cast<ExceptionCode>(exception[1]));
|
||||
|
||||
ASSERT_EQ(device.write_single_register_calls.size(), 1u);
|
||||
const auto &call = device.write_single_register_calls.front();
|
||||
EXPECT_EQ(call.address, 1);
|
||||
EXPECT_EQ(call.value, 3);
|
||||
EXPECT_EQ(call.status, ExceptionCode::ILLEGAL_DATA_ADDRESS);
|
||||
}
|
||||
|
||||
TEST(ModbusClientDeviceFanOut, ReadErrorReportsEmptyDataAndStatus) {
|
||||
RecordingDevice device;
|
||||
const uint8_t request[] = {0x03, 0x01, 0x00, 0x00, 0x02};
|
||||
const uint8_t exception[] = {0x83, 0x02};
|
||||
device.on_error(request, static_cast<ExceptionCode>(exception[1]));
|
||||
|
||||
ASSERT_EQ(device.holding_calls.size(), 1u);
|
||||
const auto &call = device.holding_calls.front();
|
||||
EXPECT_EQ(call.start_address, 0x100);
|
||||
EXPECT_TRUE(call.registers.empty());
|
||||
EXPECT_EQ(call.status, ExceptionCode::ILLEGAL_DATA_ADDRESS);
|
||||
}
|
||||
|
||||
TEST(ModbusClientDeviceFanOut, CustomFunctionCodeGoesToCatchAll) {
|
||||
RecordingDevice device;
|
||||
const uint8_t request[] = {0x47, 0x01, 0x02, 0x03, 0x04};
|
||||
const uint8_t response[] = {0x47, 0xAA, 0xBB};
|
||||
device.on_response(request, response);
|
||||
|
||||
ASSERT_EQ(device.custom_requests.size(), 1u);
|
||||
EXPECT_EQ(device.custom_requests.front(), (std::vector<uint8_t>{0x47, 0x01, 0x02, 0x03, 0x04}));
|
||||
EXPECT_EQ(device.custom_responses.front(), (std::vector<uint8_t>{0x47, 0xAA, 0xBB}));
|
||||
EXPECT_FALSE(device.custom_statuses.front().has_value());
|
||||
EXPECT_TRUE(device.holding_calls.empty());
|
||||
|
||||
// On failure the catch-all receives an empty response and the status (the exception code).
|
||||
const uint8_t exception[] = {0xC7, 0x02};
|
||||
device.on_error(request, static_cast<ExceptionCode>(exception[1]));
|
||||
ASSERT_EQ(device.custom_statuses.size(), 2u);
|
||||
EXPECT_EQ(device.custom_statuses.back(), ExceptionCode::ILLEGAL_DATA_ADDRESS);
|
||||
EXPECT_TRUE(device.custom_responses.back().empty());
|
||||
}
|
||||
|
||||
// A write ack only echoes the start address and count, so the data that was written is decoded from the
|
||||
// request PDU: [0] function code, [1..2] start address, [3..4] count, [5] byte count, [6..] data.
|
||||
TEST(ModbusClientDeviceFanOut, WriteMultipleAcksReportStartAndData) {
|
||||
RecordingDevice device;
|
||||
// Write 2 registers (0x0001, 0x0002) at 0x0020: byte count 4, data from offset 6.
|
||||
const uint8_t reg_request[] = {0x10, 0x00, 0x20, 0x00, 0x02, 0x04, 0x00, 0x01, 0x00, 0x02};
|
||||
const uint8_t reg_ack[] = {0x10, 0x00, 0x20, 0x00, 0x02};
|
||||
device.on_response(reg_request, reg_ack);
|
||||
// Write 10 coils at 0x0030: byte count 2, packed bits 0xFF 0x03 from offset 6.
|
||||
const uint8_t coil_request[] = {0x0F, 0x00, 0x30, 0x00, 0x0A, 0x02, 0xFF, 0x03};
|
||||
const uint8_t coil_ack[] = {0x0F, 0x00, 0x30, 0x00, 0x0A};
|
||||
device.on_response(coil_request, coil_ack);
|
||||
|
||||
ASSERT_EQ(device.write_multiple_registers_calls.size(), 1u);
|
||||
EXPECT_EQ(device.write_multiple_registers_calls.front().start_address, 0x20);
|
||||
EXPECT_EQ(device.write_multiple_registers_calls.front().registers, (std::vector<uint16_t>{0x0001, 0x0002}));
|
||||
ASSERT_EQ(device.write_multiple_coils_calls.size(), 1u);
|
||||
EXPECT_EQ(device.write_multiple_coils_calls.front().start_address, 0x30);
|
||||
EXPECT_EQ(device.write_multiple_coils_calls.front().count, 10);
|
||||
EXPECT_EQ(device.write_multiple_coils_calls.front().packed, (std::vector<uint8_t>{0xFF, 0x03}));
|
||||
}
|
||||
|
||||
// A truncated request (byte-count header promises more data than the PDU carries) is not a standard
|
||||
// write-multiple, so it is diverted to on_custom_response() - never clamped and delivered as if complete.
|
||||
TEST(ModbusClientDeviceFanOut, WriteMultipleTruncatedRequestDispatchesAsCustom) {
|
||||
RecordingDevice device;
|
||||
// Header claims 2 registers / 4 data bytes, but only one register's worth is present.
|
||||
const uint8_t reg_request[] = {0x10, 0x00, 0x20, 0x00, 0x02, 0x04, 0x00, 0x01};
|
||||
const uint8_t reg_ack[] = {0x10, 0x00, 0x20, 0x00, 0x02};
|
||||
device.on_response(reg_request, reg_ack);
|
||||
|
||||
EXPECT_TRUE(device.write_multiple_registers_calls.empty());
|
||||
ASSERT_EQ(device.custom_requests.size(), 1u);
|
||||
EXPECT_EQ(device.custom_requests.front(), (std::vector<uint8_t>{0x10, 0x00, 0x20, 0x00, 0x02, 0x04, 0x00, 0x01}));
|
||||
}
|
||||
|
||||
// A request whose byte-count header disagrees with its own quantity field (here: 2 registers but a
|
||||
// byte count of 2 instead of 4, with matching data) is non-standard and diverted to the catch-all.
|
||||
TEST(ModbusClientDeviceFanOut, WriteMultipleInconsistentByteCountDispatchesAsCustom) {
|
||||
RecordingDevice device;
|
||||
const uint8_t reg_request[] = {0x10, 0x00, 0x20, 0x00, 0x02, 0x02, 0x00, 0x01};
|
||||
const uint8_t reg_ack[] = {0x10, 0x00, 0x20, 0x00, 0x02};
|
||||
device.on_response(reg_request, reg_ack);
|
||||
|
||||
EXPECT_TRUE(device.write_multiple_registers_calls.empty());
|
||||
EXPECT_EQ(device.custom_requests.size(), 1u);
|
||||
}
|
||||
|
||||
// An exception on a read still dispatches to the typed callback (empty data, status set): the gate must
|
||||
// not require a standard response on the failure path, because on_error() delivers an empty response by
|
||||
// design.
|
||||
TEST(ModbusClientDeviceFanOut, ReadErrorWithEmptyResponseStillDispatchesTyped) {
|
||||
RecordingDevice device;
|
||||
const uint8_t read_request[] = {0x03, 0x01, 0x00, 0x00, 0x02};
|
||||
device.on_error(read_request, ExceptionCode::ILLEGAL_DATA_ADDRESS);
|
||||
|
||||
ASSERT_EQ(device.holding_calls.size(), 1u);
|
||||
EXPECT_TRUE(device.holding_calls.front().registers.empty());
|
||||
EXPECT_EQ(device.holding_calls.front().status, ExceptionCode::ILLEGAL_DATA_ADDRESS);
|
||||
EXPECT_TRUE(device.custom_requests.empty());
|
||||
}
|
||||
|
||||
// An error on a coil read must deliver a PackedBits view whose size() is zero - the count must never
|
||||
// promise bits that have no bytes behind them (operator[] is unchecked).
|
||||
TEST(ModbusClientDeviceFanOut, ReadCoilsErrorDeliversZeroCountBits) {
|
||||
RecordingDevice device;
|
||||
const uint8_t read_request[] = {0x01, 0x01, 0x00, 0x00, 0x0A};
|
||||
device.on_error(read_request, ExceptionCode::SERVICE_DEVICE_FAILURE);
|
||||
|
||||
ASSERT_EQ(device.coil_calls.size(), 1u);
|
||||
EXPECT_EQ(device.coil_calls.front().count, 0);
|
||||
EXPECT_TRUE(device.coil_calls.front().packed.empty());
|
||||
}
|
||||
|
||||
// Single-write acks: on success the delivered value is the device's echo (real read-back);
|
||||
// on an exception it falls back to the request copy.
|
||||
TEST(ModbusTypedDispatch, SingleWriteAckPrefersTheResponseEcho) {
|
||||
RecordingDevice device;
|
||||
const uint8_t request[] = {0x06, 0x00, 0x10, 0x00, 0x2A};
|
||||
const uint8_t echo_clamped[] = {0x06, 0x00, 0x10, 0x00, 0x28}; // device clamped 42 -> 40
|
||||
device.on_response(request, echo_clamped);
|
||||
ASSERT_EQ(device.write_single_register_calls.size(), 1u);
|
||||
EXPECT_EQ(device.write_single_register_calls.front().value, 0x0028); // the echo, not the request
|
||||
|
||||
device.on_error(request, ExceptionCode::ILLEGAL_DATA_VALUE);
|
||||
ASSERT_EQ(device.write_single_register_calls.size(), 2u);
|
||||
EXPECT_EQ(device.write_single_register_calls.back().value, 0x002A); // exception: request copy
|
||||
}
|
||||
|
||||
} // namespace esphome::modbus::testing
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,12 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "esphome/components/modbus/modbus_helpers.h"
|
||||
|
||||
namespace esphome::modbus::helpers {
|
||||
|
||||
using FC = ModbusFunctionCode;
|
||||
using FC = FunctionCode;
|
||||
|
||||
// --- server_frame_length ---------------------------------------------------
|
||||
// Frame layout: address(1) + function(1) + ... + CRC(2). Fixtures borrowed from
|
||||
@@ -83,6 +85,13 @@ TEST(ModbusClientFrameLength, WriteMultipleByteCountCapped) {
|
||||
EXPECT_EQ(client_frame_length(frame, sizeof(frame)), 9 + MAX_NUM_OF_REGISTERS_TO_WRITE * 2);
|
||||
}
|
||||
|
||||
TEST(ModbusClientFrameLength, ReadWriteMultipleByteCountCappedAtSpecLimit) {
|
||||
// FC 0x17's write byte count caps at the spec 6.17 limit of 121 registers (242 bytes), deliberately
|
||||
// tighter than FC 0x10's 123, so a corrupt byte count cannot make the parser wait past the real frame.
|
||||
const uint8_t pdu[] = {0x17, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0xFF}; // claims 255 bytes
|
||||
EXPECT_EQ(client_pdu_length(pdu, sizeof(pdu)), 10 + MAX_NUM_OF_REGISTERS_TO_WRITE_RW * 2);
|
||||
}
|
||||
|
||||
TEST(ModbusClientFrameLength, WriteMultipleMissingByteCount) {
|
||||
const uint8_t frame[] = {0x01, 0x10, 0x00, 0x00, 0x00, 0x02};
|
||||
EXPECT_EQ(client_frame_length(frame, sizeof(frame)), 9);
|
||||
@@ -97,6 +106,124 @@ TEST(ModbusClientFrameLength, MiscFixedAndUnknown) {
|
||||
EXPECT_EQ(client_frame_length(unknown, sizeof(unknown)), MIN_FRAME_SIZE);
|
||||
}
|
||||
|
||||
// --- file-record length cap --------------------------------------------------
|
||||
// FC 0x14/0x15 are parsed only to keep the frame parser in sync; the byte count caps at 251
|
||||
// (MAX_PDU_SIZE - 2), reproducing the released frame-relative bound of MAX_FRAME_SIZE - 5.
|
||||
|
||||
TEST(ModbusFileRecordCap, PduLengthCapsByteCountAt251) {
|
||||
const uint8_t pdu[] = {static_cast<uint8_t>(FC::READ_FILE_RECORD), 0xFF}; // claims 255 bytes
|
||||
EXPECT_EQ(server_pdu_length(pdu, sizeof(pdu)), 2 + (MAX_PDU_SIZE - 2));
|
||||
EXPECT_EQ(client_pdu_length(pdu, sizeof(pdu)), 2 + (MAX_PDU_SIZE - 2));
|
||||
// Frame wrappers: address(1) + PDU + CRC(2) stays within the RTU 256-byte frame limit.
|
||||
const uint8_t frame[] = {0x01, static_cast<uint8_t>(FC::WRITE_FILE_RECORD), 0xFF};
|
||||
EXPECT_EQ(server_frame_length(frame, sizeof(frame)), MAX_FRAME_SIZE);
|
||||
EXPECT_EQ(client_frame_length(frame, sizeof(frame)), MAX_FRAME_SIZE);
|
||||
}
|
||||
|
||||
TEST(ModbusFileRecordCap, StandardChecksAcceptUpTo251) {
|
||||
// A full-length PDU at the cap: function(1) + byte count(1) + 251 data bytes = MAX_PDU_SIZE.
|
||||
std::vector<uint8_t> at_cap(MAX_PDU_SIZE, 0x00);
|
||||
at_cap[0] = static_cast<uint8_t>(FC::READ_FILE_RECORD);
|
||||
at_cap[1] = MAX_PDU_SIZE - 2;
|
||||
EXPECT_TRUE(is_server_pdu_standard(at_cap.data(), at_cap.size()));
|
||||
EXPECT_TRUE(is_client_pdu_standard(at_cap.data(), at_cap.size()));
|
||||
// Byte count 252 in the same 253-byte buffer: the parsed length still matches (capped), so this
|
||||
// exercises the byte-count bound itself rather than the length identity.
|
||||
at_cap[1] = MAX_PDU_SIZE - 1;
|
||||
EXPECT_FALSE(is_server_pdu_standard(at_cap.data(), at_cap.size()));
|
||||
EXPECT_FALSE(is_client_pdu_standard(at_cap.data(), at_cap.size()));
|
||||
}
|
||||
|
||||
// --- is_client_pdu_standard / is_server_pdu_standard -------------------------
|
||||
// The gatekeepers for the typed client dispatch: a PDU must be exactly its function code's standard
|
||||
// shape, with byte count, quantity, and address range all consistent.
|
||||
|
||||
TEST(ModbusPduStandard, ClientReadAndWriteConformant) {
|
||||
const uint8_t read_regs[] = {0x03, 0x01, 0x00, 0x00, 0x02};
|
||||
EXPECT_TRUE(is_client_pdu_standard(read_regs, sizeof(read_regs)));
|
||||
const uint8_t write_regs[] = {0x10, 0x00, 0x20, 0x00, 0x02, 0x04, 0x00, 0x01, 0x00, 0x02};
|
||||
EXPECT_TRUE(is_client_pdu_standard(write_regs, sizeof(write_regs)));
|
||||
// 10 coils pack into 2 data bytes - the coil formula, not the register one.
|
||||
const uint8_t write_coils[] = {0x0F, 0x00, 0x30, 0x00, 0x0A, 0x02, 0xFF, 0x03};
|
||||
EXPECT_TRUE(is_client_pdu_standard(write_coils, sizeof(write_coils)));
|
||||
}
|
||||
|
||||
TEST(ModbusPduStandard, ClientRejectsNonConformant) {
|
||||
// Truncated: header claims 4 data bytes, only 2 present.
|
||||
const uint8_t truncated[] = {0x10, 0x00, 0x20, 0x00, 0x02, 0x04, 0x00, 0x01};
|
||||
EXPECT_FALSE(is_client_pdu_standard(truncated, sizeof(truncated)));
|
||||
// Byte count disagrees with quantity (2 registers need 4 bytes, header says 2).
|
||||
const uint8_t inconsistent[] = {0x10, 0x00, 0x20, 0x00, 0x02, 0x02, 0x00, 0x01};
|
||||
EXPECT_FALSE(is_client_pdu_standard(inconsistent, sizeof(inconsistent)));
|
||||
// Coil write using the register byte-count formula (10 coils with 20 data bytes).
|
||||
const uint8_t coil_as_regs[] = {0x0F, 0x00, 0x30, 0x00, 0x0A, 0x14, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
EXPECT_FALSE(is_client_pdu_standard(coil_as_regs, sizeof(coil_as_regs)));
|
||||
// Quantity zero and quantity beyond the per-function-code maximum.
|
||||
const uint8_t zero_qty[] = {0x03, 0x01, 0x00, 0x00, 0x00};
|
||||
EXPECT_FALSE(is_client_pdu_standard(zero_qty, sizeof(zero_qty)));
|
||||
const uint8_t too_many[] = {0x03, 0x01, 0x00, 0x00, 0x7E}; // 126 > 125
|
||||
EXPECT_FALSE(is_client_pdu_standard(too_many, sizeof(too_many)));
|
||||
// Address range overflow: 0xFFFF + 2 registers exceeds the 16-bit register space.
|
||||
const uint8_t wraps[] = {0x03, 0xFF, 0xFF, 0x00, 0x02};
|
||||
EXPECT_FALSE(is_client_pdu_standard(wraps, sizeof(wraps)));
|
||||
}
|
||||
|
||||
TEST(ModbusPduStandard, ServerReadResponses) {
|
||||
const uint8_t ok[] = {0x03, 0x04, 0x00, 0x2A, 0x01, 0x00};
|
||||
EXPECT_TRUE(is_server_pdu_standard(ok, sizeof(ok)));
|
||||
// Byte-count header disagrees with the actual length.
|
||||
const uint8_t lying[] = {0x03, 0x06, 0x00, 0x2A, 0x01, 0x00};
|
||||
EXPECT_FALSE(is_server_pdu_standard(lying, sizeof(lying)));
|
||||
// An empty PDU (the on_error path) is not a standard response.
|
||||
EXPECT_FALSE(is_server_pdu_standard(ok, 0));
|
||||
}
|
||||
|
||||
TEST(ModbusPduStandard, ServerResponsesRejectDegenerateShapes) {
|
||||
// A read response always carries data: byte count zero is non-conformant.
|
||||
const uint8_t zero_bc[] = {0x03, 0x00};
|
||||
EXPECT_FALSE(is_server_pdu_standard(zero_bc, sizeof(zero_bc)));
|
||||
// Registers are 2 bytes each: an odd byte count would silently truncate a register.
|
||||
const uint8_t odd_bc[] = {0x03, 0x03, 0x00, 0x01, 0x02};
|
||||
EXPECT_FALSE(is_server_pdu_standard(odd_bc, sizeof(odd_bc)));
|
||||
// Bit reads have no parity requirement: one packed byte is a fine coil response.
|
||||
const uint8_t coil_one_byte[] = {0x01, 0x01, 0x05};
|
||||
EXPECT_TRUE(is_server_pdu_standard(coil_one_byte, sizeof(coil_one_byte)));
|
||||
// A write-multiple echo claiming 65535 registers written is bounded like the request side.
|
||||
const uint8_t wild_echo[] = {0x10, 0x00, 0x00, 0xFF, 0xFF};
|
||||
EXPECT_FALSE(is_server_pdu_standard(wild_echo, sizeof(wild_echo)));
|
||||
const uint8_t ok_echo[] = {0x10, 0x00, 0x00, 0x00, 0x02};
|
||||
EXPECT_TRUE(is_server_pdu_standard(ok_echo, sizeof(ok_echo)));
|
||||
}
|
||||
|
||||
TEST(ModbusPduStandard, SingleCoilValueMustBeCanonical) {
|
||||
// FC 0x05's value field allows exactly 0xFF00 (ON) and 0x0000 (OFF); anything else is non-standard.
|
||||
const uint8_t on[] = {0x05, 0x00, 0x10, 0xFF, 0x00};
|
||||
const uint8_t off[] = {0x05, 0x00, 0x10, 0x00, 0x00};
|
||||
const uint8_t junk[] = {0x05, 0x00, 0x10, 0x12, 0x34};
|
||||
EXPECT_TRUE(is_client_pdu_standard(on, sizeof(on)));
|
||||
EXPECT_TRUE(is_client_pdu_standard(off, sizeof(off)));
|
||||
EXPECT_FALSE(is_client_pdu_standard(junk, sizeof(junk)));
|
||||
EXPECT_TRUE(is_server_pdu_standard(on, sizeof(on))); // the response echoes the request
|
||||
EXPECT_FALSE(is_server_pdu_standard(junk, sizeof(junk)));
|
||||
}
|
||||
|
||||
TEST(ModbusPduStandard, NonStandardFunctionCodesAcceptedOnLengthAlone) {
|
||||
// Custom, unimplemented, and exception function codes have no standard shape to check: they are
|
||||
// accepted whenever the parsed length matches, so a dispatcher can still route them by function
|
||||
// code instead of having them rejected outright. This is the documented contract - see the header.
|
||||
const uint8_t custom[] = {0x42}; // user-defined space; 1 byte matches the MIN_PDU_SIZE fallback
|
||||
EXPECT_TRUE(is_client_pdu_standard(custom, sizeof(custom)));
|
||||
EXPECT_TRUE(is_server_pdu_standard(custom, sizeof(custom)));
|
||||
const uint8_t unimplemented[] = {0x07}; // READ_EXCEPTION_STATUS
|
||||
EXPECT_TRUE(is_server_pdu_standard(unimplemented, sizeof(unimplemented)));
|
||||
const uint8_t exception[] = {0x83, 0x02}; // exception response; length pinned to 2 bytes
|
||||
EXPECT_TRUE(is_server_pdu_standard(exception, sizeof(exception)));
|
||||
// The length identity still gates: extra bytes beyond the parsed fallback are non-conformant.
|
||||
const uint8_t custom_long[] = {0x42, 0x01};
|
||||
EXPECT_FALSE(is_client_pdu_standard(custom_long, sizeof(custom_long)));
|
||||
}
|
||||
|
||||
// --- create_client_pdu -----------------------------------------------------
|
||||
// PDU = function code + data (no address, no CRC).
|
||||
|
||||
@@ -179,19 +306,44 @@ TEST(ModbusCreateClientPdu, WriteMultipleOverEntityLimitReturnsEmpty) {
|
||||
EXPECT_TRUE(pdu.empty());
|
||||
}
|
||||
|
||||
// The generic write path requires the data length to agree exactly with the entity count
|
||||
// (registers: 2 bytes each; coils: 8 packed per byte) - the same rule the response dispatch
|
||||
// enforces via is_client_pdu_standard(), so a frame built here always passes that gate.
|
||||
TEST(ModbusCreateClientPdu, WriteMultipleRejectsMismatchedDataLength) {
|
||||
const uint8_t values[] = {0x00, 0x0B, 0x00, 0x16};
|
||||
// 2 registers need exactly 4 data bytes.
|
||||
EXPECT_TRUE(create_client_pdu(FC::WRITE_MULTIPLE_REGISTERS, 0x0000, 2, values, 3).empty());
|
||||
EXPECT_FALSE(create_client_pdu(FC::WRITE_MULTIPLE_REGISTERS, 0x0000, 2, values, 4).empty());
|
||||
// 10 coils pack into exactly 2 data bytes - the coil formula, not the register one.
|
||||
EXPECT_FALSE(create_client_pdu(FC::WRITE_MULTIPLE_COILS, 0x0000, 10, values, 2).empty());
|
||||
EXPECT_TRUE(create_client_pdu(FC::WRITE_MULTIPLE_COILS, 0x0000, 10, values, 4).empty());
|
||||
}
|
||||
|
||||
TEST(ModbusCreateClientPdu, WriteCoilsUseTheCoilLimitNotTheRegisterLimit) {
|
||||
// 200 coils: above the 123-register write limit but well within the 1968-coil limit; 25 data bytes.
|
||||
std::vector<uint8_t> values(25, 0xAA);
|
||||
auto pdu = create_client_pdu(FC::WRITE_MULTIPLE_COILS, 0x0000, 200, values.data(), values.size());
|
||||
ASSERT_FALSE(pdu.empty());
|
||||
EXPECT_EQ(pdu[5], 25); // byte count uses the coil formula
|
||||
EXPECT_TRUE(is_client_pdu_standard(pdu.data(), pdu.size())); // builder output passes the validator
|
||||
// Builder and validator agree at the top of the range too: 1969 coils rejected.
|
||||
std::vector<uint8_t> big((1969 + 7) / 8, 0x00);
|
||||
EXPECT_TRUE(create_client_pdu(FC::WRITE_MULTIPLE_COILS, 0x0000, 1969, big.data(), big.size()).empty());
|
||||
}
|
||||
|
||||
TEST(ModbusHelpersTest, PayloadToNumberRejectsOffsetAtEndOfBuffer) {
|
||||
const std::vector<uint8_t> data{0x12, 0x34};
|
||||
EXPECT_EQ(payload_to_number(data, SensorValueType::U_WORD, 2, 0xFFFFFFFF), 0);
|
||||
EXPECT_FALSE(payload_to_number(std::span<const uint8_t>(data), SensorValueType::U_WORD, 2, 0xFFFFFFFF).has_value());
|
||||
}
|
||||
|
||||
TEST(ModbusHelpersTest, PayloadToNumberRejectsTruncatedMultiRegisterValue) {
|
||||
const std::vector<uint8_t> data{0x12, 0x34, 0x56};
|
||||
EXPECT_EQ(payload_to_number(data, SensorValueType::U_DWORD, 0, 0xFFFFFFFF), 0);
|
||||
EXPECT_FALSE(payload_to_number(std::span<const uint8_t>(data), SensorValueType::U_DWORD, 0, 0xFFFFFFFF).has_value());
|
||||
}
|
||||
|
||||
TEST(ModbusHelpersTest, PayloadToNumberDecodesValidWord) {
|
||||
const std::vector<uint8_t> data{0x12, 0x34};
|
||||
EXPECT_EQ(payload_to_number(data, SensorValueType::U_WORD, 0, 0xFFFFFFFF), 0x1234);
|
||||
EXPECT_EQ(payload_to_number(std::span<const uint8_t>(data), SensorValueType::U_WORD, 0, 0xFFFFFFFF), 0x1234);
|
||||
}
|
||||
|
||||
// --- registers_to_number ---------------------------------------------------
|
||||
@@ -218,16 +370,194 @@ TEST(ModbusHelpersTest, RegistersToNumberMatchesPayloadToNumber) {
|
||||
const uint16_t registers[] = {0x8001, 0x0002};
|
||||
const std::vector<uint8_t> bytes{0x80, 0x01, 0x00, 0x02};
|
||||
for (auto value_type : {SensorValueType::S_DWORD, SensorValueType::U_DWORD, SensorValueType::S_DWORD_R}) {
|
||||
EXPECT_EQ(registers_to_number(registers, 2, value_type), payload_to_number(bytes, value_type, 0, 0xFFFFFFFF))
|
||||
EXPECT_EQ(registers_to_number(registers, 2, value_type),
|
||||
payload_to_number(std::span<const uint8_t>(bytes), value_type, 0, 0xFFFFFFFF))
|
||||
<< "value_type=" << static_cast<int>(value_type);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(ModbusHelpersTest, RegistersToNumberRejectsTruncatedMultiRegisterValue) {
|
||||
const uint16_t registers[] = {0x1234};
|
||||
bool error = false;
|
||||
EXPECT_EQ(registers_to_number(registers, 1, SensorValueType::U_DWORD, &error), 0);
|
||||
EXPECT_TRUE(error);
|
||||
EXPECT_FALSE(registers_to_number(registers, 1, SensorValueType::U_DWORD).has_value());
|
||||
}
|
||||
|
||||
// --- typed builders ----------------------------------------------------------
|
||||
|
||||
TEST(ModbusTypedBuilders, ReadPduWireBytes) {
|
||||
auto pdu = create_read_pdu(FC::READ_HOLDING_REGISTERS, 0x0102, 3);
|
||||
const std::vector<uint8_t> expected{0x03, 0x01, 0x02, 0x00, 0x03};
|
||||
EXPECT_EQ(std::vector<uint8_t>(pdu.begin(), pdu.end()), expected);
|
||||
EXPECT_TRUE(is_client_pdu_standard(pdu.data(), pdu.size()));
|
||||
// Reads that run past the 16-bit address space are refused.
|
||||
EXPECT_TRUE(create_read_pdu(FC::READ_HOLDING_REGISTERS, 0xFFFF, 2).empty());
|
||||
}
|
||||
|
||||
TEST(ModbusTypedBuilders, WriteSinglePduWireBytes) {
|
||||
auto reg = create_write_single_register_pdu(0x0010, 0xABCD);
|
||||
const std::vector<uint8_t> expected_reg{0x06, 0x00, 0x10, 0xAB, 0xCD};
|
||||
EXPECT_EQ(std::vector<uint8_t>(reg.begin(), reg.end()), expected_reg);
|
||||
EXPECT_TRUE(is_client_pdu_standard(reg.data(), reg.size()));
|
||||
auto coil_on = create_write_single_coil_pdu(0x0011, true);
|
||||
auto coil_off = create_write_single_coil_pdu(0x0011, false);
|
||||
const std::vector<uint8_t> expected_on{0x05, 0x00, 0x11, 0xFF, 0x00};
|
||||
const std::vector<uint8_t> expected_off{0x05, 0x00, 0x11, 0x00, 0x00};
|
||||
EXPECT_EQ(std::vector<uint8_t>(coil_on.begin(), coil_on.end()), expected_on);
|
||||
EXPECT_EQ(std::vector<uint8_t>(coil_off.begin(), coil_off.end()), expected_off);
|
||||
EXPECT_TRUE(is_client_pdu_standard(coil_on.data(), coil_on.size()));
|
||||
EXPECT_TRUE(is_client_pdu_standard(coil_off.data(), coil_off.size()));
|
||||
}
|
||||
|
||||
TEST(ModbusTypedBuilders, WriteRegistersPduWireBytes) {
|
||||
const uint16_t values[] = {0x000B, 0x0016};
|
||||
auto pdu = create_write_registers_pdu(0x0000, values);
|
||||
const std::vector<uint8_t> expected{0x10, 0x00, 0x00, 0x00, 0x02, 0x04, 0x00, 0x0B, 0x00, 0x16};
|
||||
EXPECT_EQ(std::vector<uint8_t>(pdu.begin(), pdu.end()), expected);
|
||||
EXPECT_TRUE(is_client_pdu_standard(pdu.data(), pdu.size()));
|
||||
// Writes that run past the 16-bit address space are refused.
|
||||
EXPECT_TRUE(create_write_registers_pdu(0xFFFF, values).empty());
|
||||
}
|
||||
|
||||
TEST(ModbusTypedBuilders, WriteRegistersPduRejectsOverLimit) {
|
||||
std::vector<uint16_t> values(MAX_NUM_OF_REGISTERS_TO_WRITE + 1, 0xAAAA);
|
||||
EXPECT_TRUE(create_write_registers_pdu(0x0000, values).empty());
|
||||
values.pop_back();
|
||||
EXPECT_FALSE(create_write_registers_pdu(0x0000, values).empty());
|
||||
}
|
||||
|
||||
TEST(ModbusTypedBuilders, FloatToPayloadAppendsToExistingContent) {
|
||||
// The container overload appends - the semantic every migrated caller relies on when a lambda
|
||||
// has already put words into the buffer.
|
||||
std::vector<uint16_t> data{0x1234};
|
||||
float_to_payload(data, 1.0f, SensorValueType::U_WORD);
|
||||
ASSERT_EQ(data.size(), 2u);
|
||||
EXPECT_EQ(data[0], 0x1234);
|
||||
EXPECT_EQ(data[1], 0x0001);
|
||||
}
|
||||
|
||||
TEST(ModbusCreateClientPdu, ExceptionFlaggedWriteCodesRejected) {
|
||||
// is_function_code_write() masks the exception bit; the builder must not.
|
||||
const uint8_t values[] = {0x00, 0x0B, 0x00, 0x16};
|
||||
EXPECT_TRUE(create_client_pdu(FunctionCode(0x90), 0x0000, 2, values, 4).empty());
|
||||
EXPECT_TRUE(create_client_pdu(FunctionCode(0x85), 0x0000, 1, values, 2).empty());
|
||||
}
|
||||
|
||||
TEST(ModbusTypedBuilders, BoolSpanCoilBuilderRejectsOverLimit) {
|
||||
// This early guard is what keeps the 246-byte packing buffer from overflowing - the shared core's
|
||||
// identical check runs after packing, so it cannot protect it.
|
||||
auto big = std::make_unique<bool[]>(MAX_NUM_OF_COILS_TO_WRITE + 1);
|
||||
EXPECT_TRUE(create_write_coils_pdu(0, std::span<const bool>(big.get(), MAX_NUM_OF_COILS_TO_WRITE + 1)).empty());
|
||||
}
|
||||
|
||||
TEST(ModbusCreateClientPdu, GenericCoilWriteMasksTrailingPadBits) {
|
||||
// 10 coils with junk in the pad bits of the last data byte: the generic path masks them like the
|
||||
// typed builder, so both produce identical wire bytes.
|
||||
const uint8_t values[] = {0xFF, 0xFF};
|
||||
auto pdu = create_client_pdu(FC::WRITE_MULTIPLE_COILS, 0x0000, 10, values, 2);
|
||||
ASSERT_FALSE(pdu.empty());
|
||||
EXPECT_EQ(pdu[pdu.size() - 1], 0x03); // bits 8-9 kept, pad bits 10-15 zeroed
|
||||
}
|
||||
|
||||
TEST(ModbusCreateClientPdu, SingleCoilValueValidated) {
|
||||
const uint8_t on[] = {0xFF, 0x00};
|
||||
const uint8_t junk[] = {0x01, 0x00};
|
||||
EXPECT_FALSE(create_client_pdu(FC::WRITE_SINGLE_COIL, 0x0003, 1, on, 2).empty());
|
||||
EXPECT_TRUE(create_client_pdu(FC::WRITE_SINGLE_COIL, 0x0003, 1, junk, 2).empty());
|
||||
}
|
||||
|
||||
// --- create_write_coils_pdu (packed) ---------------------------------------
|
||||
|
||||
TEST(ModbusWriteCoilsPacked, MatchesBoolBuilder) {
|
||||
const bool coils[] = {true, false, true, true, false, false, true, false, true, true};
|
||||
uint8_t packed[] = {0b01001101, 0b00000011};
|
||||
auto from_bools = create_write_coils_pdu(0x13, coils);
|
||||
auto from_packed = create_write_coils_pdu(0x13, PackedBits(packed, 10));
|
||||
ASSERT_EQ(from_packed.size(), from_bools.size());
|
||||
EXPECT_EQ(0, memcmp(from_packed.data(), from_bools.data(), from_bools.size()));
|
||||
}
|
||||
|
||||
TEST(ModbusWriteCoilsPacked, MasksUnusedTrailingBits) {
|
||||
uint8_t packed[] = {0xFF};
|
||||
auto pdu = create_write_coils_pdu(0, PackedBits(packed, 3));
|
||||
ASSERT_EQ(pdu.size(), 7u);
|
||||
EXPECT_EQ(pdu[6], 0x07);
|
||||
}
|
||||
|
||||
TEST(ModbusWriteCoilsPacked, RejectsShortBufferAndZeroCount) {
|
||||
uint8_t packed[] = {0xFF};
|
||||
EXPECT_TRUE(create_write_coils_pdu(0, PackedBits(packed, 9)).empty()); // needs 2 bytes
|
||||
EXPECT_TRUE(create_write_coils_pdu(0, PackedBits(packed, 0)).empty());
|
||||
}
|
||||
|
||||
TEST(ModbusHelpersTest, PackedBitsReadsLsbFirst) {
|
||||
const uint8_t packed[] = {0x0D, 0x03}; // bits 0,2,3 and 8,9
|
||||
PackedBits bits(packed, 11);
|
||||
EXPECT_EQ(bits.size(), 11u);
|
||||
EXPECT_TRUE(bits[0]);
|
||||
EXPECT_FALSE(bits[1]);
|
||||
EXPECT_TRUE(bits[2]);
|
||||
EXPECT_TRUE(bits[3]);
|
||||
EXPECT_FALSE(bits[7]);
|
||||
EXPECT_TRUE(bits[8]);
|
||||
EXPECT_TRUE(bits[9]);
|
||||
EXPECT_FALSE(bits[10]);
|
||||
EXPECT_EQ(bits.bytes().size(), 2u);
|
||||
}
|
||||
|
||||
TEST(ModbusHelpersTest, MutablePackedBitsSetsAndClears) {
|
||||
uint8_t packed[2] = {0x00, 0xFF};
|
||||
MutablePackedBits bits(packed, 16);
|
||||
bits.set(0, true);
|
||||
bits.set(3, true);
|
||||
bits.set(9, false);
|
||||
EXPECT_EQ(packed[0], 0x09); // bits 0 and 3
|
||||
EXPECT_EQ(packed[1], 0xFD); // bit 9 (bit 1 of byte 1) cleared
|
||||
}
|
||||
|
||||
TEST(ModbusHelpersTest, MutablePackedBitsRoundTripAndConversion) {
|
||||
const bool original[] = {true, true, false, true, false, false, false, false, true, false, true};
|
||||
constexpr uint16_t count = sizeof(original);
|
||||
uint8_t packed[(count + 7) / 8] = {};
|
||||
MutablePackedBits out(packed, count);
|
||||
for (uint16_t i = 0; i != count; i++)
|
||||
out.set(i, original[i]);
|
||||
PackedBits view = out; // implicit conversion to the read-only view
|
||||
ASSERT_EQ(view.size(), count);
|
||||
for (uint16_t i = 0; i != count; i++)
|
||||
EXPECT_EQ(view[i], original[i]) << "bit " << i;
|
||||
}
|
||||
|
||||
TEST(ModbusHelpersTest, PackedBitsViewContractsEnforced) {
|
||||
uint8_t buf[8] = {};
|
||||
PackedBits view(buf, 10); // 10 bits -> 2 bytes, over an 8-byte buffer
|
||||
EXPECT_EQ(view.bytes().size(), 2u);
|
||||
|
||||
MutablePackedBits bits(std::span<uint8_t>(buf, 2), 10);
|
||||
bits.set(9, true); // in range: lands in byte 1
|
||||
bits.set(10, true); // out of range: dropped
|
||||
bits.set(300, true); // far out of range: dropped, no write past the span
|
||||
|
||||
MutablePackedBits short_bits(std::span<uint8_t>(buf, 1), 10); // contract-violating: 10 bits over 1 byte
|
||||
short_bits.set(9, false); // within count_ but past the span: dropped (would clear bit 9 set above)
|
||||
EXPECT_EQ(buf[1], 0x02);
|
||||
for (size_t i = 2; i < sizeof(buf); i++)
|
||||
EXPECT_EQ(buf[i], 0) << "byte " << i;
|
||||
}
|
||||
|
||||
// server_pdu_payload() must never classify an exception PDU as a read: [fc|0x80, code] is 2 bytes, and a
|
||||
// read-offset of 2 would return an empty span, losing the exception code. The payload of an exception PDU
|
||||
// is the exception code byte, for reads and writes alike.
|
||||
TEST(ModbusServerPduPayload, ExceptionOfReadYieldsExceptionCode) {
|
||||
const uint8_t pdu[] = {0x83, 0x02}; // exception response to READ_HOLDING_REGISTERS
|
||||
auto payload = server_pdu_payload(pdu);
|
||||
ASSERT_EQ(payload.size(), 1u);
|
||||
EXPECT_EQ(payload[0], 0x02);
|
||||
}
|
||||
|
||||
TEST(ModbusServerPduPayload, ExceptionOfWriteYieldsExceptionCode) {
|
||||
const uint8_t pdu[] = {0x86, 0x03}; // exception response to WRITE_SINGLE_REGISTER
|
||||
auto payload = server_pdu_payload(pdu);
|
||||
ASSERT_EQ(payload.size(), 1u);
|
||||
EXPECT_EQ(payload[0], 0x03);
|
||||
}
|
||||
|
||||
} // namespace esphome::modbus::helpers
|
||||
|
||||
@@ -18,7 +18,7 @@ binary_sensor:
|
||||
modbus_controller_id: modbus_controller1
|
||||
id: modbus_binary_sensor2
|
||||
name: Test Binary Sensor with Lambda
|
||||
register_type: read
|
||||
register_type: input
|
||||
address: 0x3201
|
||||
lambda: |-
|
||||
return x;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
namespace esphome::modbus_server {
|
||||
|
||||
using modbus::ModbusExceptionCode;
|
||||
using modbus::ExceptionCode;
|
||||
using modbus::RegisterValues;
|
||||
|
||||
namespace {
|
||||
@@ -29,7 +29,7 @@ TEST(ModbusServerWrite, SingleWordSucceeds) {
|
||||
};
|
||||
server.add_server_register(®);
|
||||
|
||||
auto status = server.on_modbus_write_registers(0x0000, make_registers({0x1234}));
|
||||
auto status = server.on_write_registers(0x0000, make_registers({0x1234}));
|
||||
EXPECT_FALSE(status.has_value()); // nullopt == success
|
||||
EXPECT_EQ(written, 0x1234);
|
||||
}
|
||||
@@ -45,7 +45,7 @@ TEST(ModbusServerWrite, DwordSucceeds) {
|
||||
};
|
||||
server.add_server_register(®);
|
||||
|
||||
auto status = server.on_modbus_write_registers(0x0000, make_registers({0x1234, 0x5678}));
|
||||
auto status = server.on_write_registers(0x0000, make_registers({0x1234, 0x5678}));
|
||||
EXPECT_FALSE(status.has_value());
|
||||
EXPECT_EQ(written, 0x12345678);
|
||||
}
|
||||
@@ -70,10 +70,10 @@ TEST(ModbusServerWrite, UnderSuppliedValueAppliesNothing) {
|
||||
server.add_server_register(&dword_reg);
|
||||
|
||||
// Two words supplied: one for the WORD at 0x0000, but only one of the two the DWORD at 0x0001 needs.
|
||||
auto status = server.on_modbus_write_registers(0x0000, make_registers({0x1111, 0x2222}));
|
||||
auto status = server.on_write_registers(0x0000, make_registers({0x1111, 0x2222}));
|
||||
ASSERT_TRUE(status.has_value());
|
||||
if (status.has_value())
|
||||
EXPECT_EQ(status.value(), ModbusExceptionCode::ILLEGAL_DATA_VALUE);
|
||||
EXPECT_EQ(status.value(), ExceptionCode::ILLEGAL_DATA_VALUE);
|
||||
EXPECT_FALSE(word_written); // the writable WORD must NOT have been applied
|
||||
EXPECT_FALSE(dword_written);
|
||||
}
|
||||
@@ -84,19 +84,19 @@ TEST(ModbusServerWrite, UnwritableRegisterRejected) {
|
||||
ServerRegister read_only(0x0000, SensorValueType::U_WORD, 1); // no write_lambda set
|
||||
server.add_server_register(&read_only);
|
||||
|
||||
auto status = server.on_modbus_write_registers(0x0000, make_registers({0x1234}));
|
||||
auto status = server.on_write_registers(0x0000, make_registers({0x1234}));
|
||||
ASSERT_TRUE(status.has_value());
|
||||
if (status.has_value())
|
||||
EXPECT_EQ(status.value(), ModbusExceptionCode::ILLEGAL_DATA_ADDRESS);
|
||||
EXPECT_EQ(status.value(), ExceptionCode::ILLEGAL_DATA_ADDRESS);
|
||||
}
|
||||
|
||||
// An address with no registered register yields ILLEGAL_DATA_ADDRESS.
|
||||
TEST(ModbusServerWrite, UnmatchedAddressRejected) {
|
||||
ModbusServer server;
|
||||
auto status = server.on_modbus_write_registers(0x0005, make_registers({0x1234}));
|
||||
auto status = server.on_write_registers(0x0005, make_registers({0x1234}));
|
||||
ASSERT_TRUE(status.has_value());
|
||||
if (status.has_value())
|
||||
EXPECT_EQ(status.value(), ModbusExceptionCode::ILLEGAL_DATA_ADDRESS);
|
||||
EXPECT_EQ(status.value(), ExceptionCode::ILLEGAL_DATA_ADDRESS);
|
||||
}
|
||||
|
||||
// A write_lambda failing at runtime is the one non-atomic case: the earlier register is already
|
||||
@@ -114,14 +114,14 @@ TEST(ModbusServerWrite, CallbackFailureIsServiceDeviceFailure) {
|
||||
server.add_server_register(&first);
|
||||
server.add_server_register(&second);
|
||||
|
||||
auto status = server.on_modbus_write_registers(0x0000, make_registers({0xAAAA, 0xBBBB}));
|
||||
auto status = server.on_write_registers(0x0000, make_registers({0xAAAA, 0xBBBB}));
|
||||
ASSERT_TRUE(status.has_value());
|
||||
if (status.has_value())
|
||||
EXPECT_EQ(status.value(), ModbusExceptionCode::SERVICE_DEVICE_FAILURE);
|
||||
EXPECT_EQ(status.value(), ExceptionCode::SERVICE_DEVICE_FAILURE);
|
||||
EXPECT_TRUE(first_written); // pre-validation passed, so the first write applied before the failure
|
||||
}
|
||||
|
||||
// --- on_modbus_read_registers --------------------------------------------------
|
||||
// --- on_read_registers --------------------------------------------------
|
||||
|
||||
TEST(ModbusServerRead, SingleWordSucceeds) {
|
||||
ModbusServer server;
|
||||
@@ -130,7 +130,7 @@ TEST(ModbusServerRead, SingleWordSucceeds) {
|
||||
server.add_server_register(®);
|
||||
|
||||
RegisterValues out;
|
||||
auto status = server.on_modbus_read_registers(0x0000, 1, out);
|
||||
auto status = server.on_read_registers(0x0000, 1, out);
|
||||
EXPECT_FALSE(status.has_value());
|
||||
ASSERT_EQ(out.size(), 1u);
|
||||
EXPECT_EQ(out[0], 0x1234);
|
||||
@@ -143,7 +143,7 @@ TEST(ModbusServerRead, DwordReturnsTwoWordsHighFirst) {
|
||||
server.add_server_register(®);
|
||||
|
||||
RegisterValues out;
|
||||
auto status = server.on_modbus_read_registers(0x0000, 2, out);
|
||||
auto status = server.on_read_registers(0x0000, 2, out);
|
||||
EXPECT_FALSE(status.has_value());
|
||||
ASSERT_EQ(out.size(), 2u);
|
||||
EXPECT_EQ(out[0], 0x1234);
|
||||
@@ -165,10 +165,10 @@ TEST(ModbusServerRead, StartInsideValueRejected) {
|
||||
server.add_server_register(®);
|
||||
|
||||
RegisterValues out;
|
||||
auto status = server.on_modbus_read_registers(0x0011, 1, out); // the second cell of the DWORD
|
||||
auto status = server.on_read_registers(0x0011, 1, out); // the second cell of the DWORD
|
||||
ASSERT_TRUE(status.has_value());
|
||||
if (status.has_value())
|
||||
EXPECT_EQ(status.value(), ModbusExceptionCode::ILLEGAL_DATA_ADDRESS);
|
||||
EXPECT_EQ(status.value(), ExceptionCode::ILLEGAL_DATA_ADDRESS);
|
||||
EXPECT_FALSE(read_called);
|
||||
}
|
||||
|
||||
@@ -184,10 +184,10 @@ TEST(ModbusServerRead, ClippedTailRejected) {
|
||||
server.add_server_register(®);
|
||||
|
||||
RegisterValues out;
|
||||
auto status = server.on_modbus_read_registers(0x0000, 1, out); // only 1 of the DWORD's 2 registers
|
||||
auto status = server.on_read_registers(0x0000, 1, out); // only 1 of the DWORD's 2 registers
|
||||
ASSERT_TRUE(status.has_value());
|
||||
if (status.has_value())
|
||||
EXPECT_EQ(status.value(), ModbusExceptionCode::ILLEGAL_DATA_ADDRESS);
|
||||
EXPECT_EQ(status.value(), ExceptionCode::ILLEGAL_DATA_ADDRESS);
|
||||
EXPECT_FALSE(read_called);
|
||||
}
|
||||
|
||||
@@ -200,10 +200,10 @@ TEST(ModbusServerRead, WriteOnlyRegisterRejected) {
|
||||
server.add_server_register(®);
|
||||
|
||||
RegisterValues out;
|
||||
auto status = server.on_modbus_read_registers(0x0000, 1, out);
|
||||
auto status = server.on_read_registers(0x0000, 1, out);
|
||||
ASSERT_TRUE(status.has_value());
|
||||
if (status.has_value())
|
||||
EXPECT_EQ(status.value(), ModbusExceptionCode::ILLEGAL_DATA_ADDRESS);
|
||||
EXPECT_EQ(status.value(), ExceptionCode::ILLEGAL_DATA_ADDRESS);
|
||||
}
|
||||
|
||||
// An unregistered address with courtesy enabled returns the default value for each cell.
|
||||
@@ -213,7 +213,7 @@ TEST(ModbusServerRead, CourtesyDefaultForUnregistered) {
|
||||
ServerCourtesyResponse{.enabled = true, .register_last_address = 0xFFFF, .register_value = 0xABCD});
|
||||
|
||||
RegisterValues out;
|
||||
auto status = server.on_modbus_read_registers(0x0005, 2, out);
|
||||
auto status = server.on_read_registers(0x0005, 2, out);
|
||||
EXPECT_FALSE(status.has_value());
|
||||
ASSERT_EQ(out.size(), 2u);
|
||||
EXPECT_EQ(out[0], 0xABCD);
|
||||
@@ -224,10 +224,10 @@ TEST(ModbusServerRead, CourtesyDefaultForUnregistered) {
|
||||
TEST(ModbusServerRead, UnregisteredRejectedWithoutCourtesy) {
|
||||
ModbusServer server;
|
||||
RegisterValues out;
|
||||
auto status = server.on_modbus_read_registers(0x0005, 1, out);
|
||||
auto status = server.on_read_registers(0x0005, 1, out);
|
||||
ASSERT_TRUE(status.has_value());
|
||||
if (status.has_value())
|
||||
EXPECT_EQ(status.value(), ModbusExceptionCode::ILLEGAL_DATA_ADDRESS);
|
||||
EXPECT_EQ(status.value(), ExceptionCode::ILLEGAL_DATA_ADDRESS);
|
||||
}
|
||||
|
||||
// --- partial reads (opt-in) ----------------------------------------------------
|
||||
@@ -241,7 +241,7 @@ TEST(ModbusServerRead, PartialReadHighWord) {
|
||||
server.add_server_register(®);
|
||||
|
||||
RegisterValues out;
|
||||
auto status = server.on_modbus_read_registers(0x0010, 1, out);
|
||||
auto status = server.on_read_registers(0x0010, 1, out);
|
||||
EXPECT_FALSE(status.has_value());
|
||||
ASSERT_EQ(out.size(), 1u);
|
||||
EXPECT_EQ(out[0], 0x1234);
|
||||
@@ -256,7 +256,7 @@ TEST(ModbusServerRead, PartialReadLowWordFromInterior) {
|
||||
server.add_server_register(®);
|
||||
|
||||
RegisterValues out;
|
||||
auto status = server.on_modbus_read_registers(0x0011, 1, out);
|
||||
auto status = server.on_read_registers(0x0011, 1, out);
|
||||
EXPECT_FALSE(status.has_value());
|
||||
ASSERT_EQ(out.size(), 1u);
|
||||
EXPECT_EQ(out[0], 0x5678);
|
||||
@@ -272,12 +272,12 @@ TEST(ModbusServerRead, PartialReadReversedType) {
|
||||
server.add_server_register(®);
|
||||
|
||||
RegisterValues first;
|
||||
ASSERT_FALSE(server.on_modbus_read_registers(0x0010, 1, first).has_value());
|
||||
ASSERT_FALSE(server.on_read_registers(0x0010, 1, first).has_value());
|
||||
ASSERT_EQ(first.size(), 1u);
|
||||
EXPECT_EQ(first[0], 0x5678);
|
||||
|
||||
RegisterValues second;
|
||||
ASSERT_FALSE(server.on_modbus_read_registers(0x0011, 1, second).has_value());
|
||||
ASSERT_FALSE(server.on_read_registers(0x0011, 1, second).has_value());
|
||||
ASSERT_EQ(second.size(), 1u);
|
||||
EXPECT_EQ(second[0], 0x1234);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import esphome.codegen as cg
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
manifest.enable_codegen()
|
||||
real_to_code = manifest.to_code
|
||||
|
||||
async def to_code_testing(config):
|
||||
await real_to_code(config)
|
||||
cg.add_define("USE_NETWORK_IPV6", True)
|
||||
|
||||
manifest.to_code = to_code_testing
|
||||
@@ -0,0 +1,23 @@
|
||||
# Compiled dual-stack test: wifi + ethernet coexisting via network: priority:.
|
||||
# This is the first build path that keeps both radios' stacks compiled in, so
|
||||
# it must actually compile (not just validate) to guard the reconciler wiring.
|
||||
# WiFi is listed first so the build also exercises the wifi-primary branch in
|
||||
# network/util.cpp (the ethernet-primary branch matches the legacy order).
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
ethernet:
|
||||
type: W5500
|
||||
clk_pin: GPIO19
|
||||
mosi_pin: GPIO21
|
||||
miso_pin: GPIO23
|
||||
cs_pin: GPIO18
|
||||
interrupt_pin: GPIO36
|
||||
reset_pin: GPIO22
|
||||
clock_speed: 10Mhz
|
||||
|
||||
network:
|
||||
priority:
|
||||
- wifi
|
||||
- ethernet
|
||||
@@ -0,0 +1,208 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "esphome/components/network/ip_address.h"
|
||||
|
||||
#ifdef USE_HOST
|
||||
#if USE_NETWORK_IPV6
|
||||
|
||||
namespace esphome::network::testing {
|
||||
|
||||
// =========================================================================
|
||||
// IPv4
|
||||
// =========================================================================
|
||||
|
||||
TEST(IPAddressHost, IPv4DefaultNotSet) {
|
||||
IPAddress addr;
|
||||
EXPECT_FALSE(addr.is_set());
|
||||
}
|
||||
|
||||
TEST(IPAddressHost, IPv4DefaultIsIPv4) {
|
||||
IPAddress addr;
|
||||
EXPECT_TRUE(addr.is_ip4());
|
||||
EXPECT_FALSE(addr.is_ip6());
|
||||
}
|
||||
|
||||
TEST(IPAddressHost, IPv4ParseAndSerialize) {
|
||||
IPAddress addr("192.168.1.1");
|
||||
char buf[IP_ADDRESS_BUFFER_SIZE];
|
||||
EXPECT_STREQ(addr.str_to(buf), "192.168.1.1");
|
||||
}
|
||||
|
||||
TEST(IPAddressHost, IPv4FromOctets) {
|
||||
IPAddress addr(192, 168, 1, 1);
|
||||
char buf[IP_ADDRESS_BUFFER_SIZE];
|
||||
EXPECT_STREQ(addr.str_to(buf), "192.168.1.1");
|
||||
}
|
||||
|
||||
TEST(IPAddressHost, IPv4IsSet) {
|
||||
IPAddress addr("192.168.1.1");
|
||||
EXPECT_TRUE(addr.is_set());
|
||||
}
|
||||
|
||||
TEST(IPAddressHost, IPv4IsIp4) {
|
||||
IPAddress addr("192.168.1.1");
|
||||
EXPECT_TRUE(addr.is_ip4());
|
||||
EXPECT_FALSE(addr.is_ip6());
|
||||
}
|
||||
|
||||
TEST(IPAddressHost, IPv4MulticastDetected) {
|
||||
IPAddress addr("239.0.60.53");
|
||||
EXPECT_TRUE(addr.is_multicast());
|
||||
}
|
||||
|
||||
TEST(IPAddressHost, IPv4MulticastBoundaryLow) {
|
||||
IPAddress addr("224.0.0.0");
|
||||
EXPECT_TRUE(addr.is_multicast());
|
||||
}
|
||||
|
||||
TEST(IPAddressHost, IPv4MulticastBoundaryHigh) {
|
||||
IPAddress addr("239.255.255.255");
|
||||
EXPECT_TRUE(addr.is_multicast());
|
||||
}
|
||||
|
||||
TEST(IPAddressHost, IPv4UnicastNotMulticast) {
|
||||
IPAddress addr("192.168.1.1");
|
||||
EXPECT_FALSE(addr.is_multicast());
|
||||
}
|
||||
|
||||
TEST(IPAddressHost, IPv4EqualityMatch) {
|
||||
IPAddress a("192.168.1.1");
|
||||
IPAddress b("192.168.1.1");
|
||||
EXPECT_EQ(a, b);
|
||||
}
|
||||
|
||||
TEST(IPAddressHost, IPv4EqualityMismatch) {
|
||||
IPAddress a("192.168.1.1");
|
||||
IPAddress b("192.168.1.2");
|
||||
EXPECT_NE(a, b);
|
||||
}
|
||||
|
||||
TEST(IPAddressHost, IPv4FromOctetsMatchesParse) {
|
||||
IPAddress from_octets(192, 168, 1, 1);
|
||||
IPAddress from_string("192.168.1.1");
|
||||
EXPECT_EQ(from_octets, from_string);
|
||||
}
|
||||
|
||||
TEST(IPAddressHost, IPv4FromIPAddrT) {
|
||||
ip_addr_t raw;
|
||||
memset(&raw, 0, sizeof(raw));
|
||||
raw.u_addr.ip4.s_addr = htonl((192u << 24) | (168u << 16) | (1u << 8) | 1u);
|
||||
raw.type = IPADDR_TYPE_V4;
|
||||
IPAddress addr(&raw);
|
||||
char buf[IP_ADDRESS_BUFFER_SIZE];
|
||||
EXPECT_STREQ(addr.str_to(buf), "192.168.1.1");
|
||||
EXPECT_TRUE(addr.is_ip4());
|
||||
EXPECT_FALSE(addr.is_ip6());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// IPv6
|
||||
// =========================================================================
|
||||
|
||||
TEST(IPAddressHost, IPv6ParseAndSerialize) {
|
||||
IPAddress addr("ff12::cafe");
|
||||
char buf[IP_ADDRESS_BUFFER_SIZE];
|
||||
EXPECT_STREQ(addr.str_to(buf), "ff12::cafe");
|
||||
}
|
||||
|
||||
TEST(IPAddressHost, IPv6Loopback) {
|
||||
IPAddress addr("::1");
|
||||
char buf[IP_ADDRESS_BUFFER_SIZE];
|
||||
EXPECT_STREQ(addr.str_to(buf), "::1");
|
||||
}
|
||||
|
||||
TEST(IPAddressHost, IPv6IsIp6) {
|
||||
IPAddress addr("ff12::cafe");
|
||||
EXPECT_TRUE(addr.is_ip6());
|
||||
EXPECT_FALSE(addr.is_ip4());
|
||||
}
|
||||
|
||||
TEST(IPAddressHost, IPv6AllZerosNotSet) {
|
||||
IPAddress addr("::");
|
||||
EXPECT_FALSE(addr.is_set());
|
||||
}
|
||||
|
||||
TEST(IPAddressHost, IPv6LoopbackIsSet) {
|
||||
IPAddress addr("::1");
|
||||
EXPECT_TRUE(addr.is_set());
|
||||
}
|
||||
|
||||
TEST(IPAddressHost, IPv6MulticastDetected) {
|
||||
IPAddress addr("ff12::cafe");
|
||||
EXPECT_TRUE(addr.is_multicast());
|
||||
}
|
||||
|
||||
TEST(IPAddressHost, IPv6MulticastLinkLocal) {
|
||||
IPAddress addr("ff02::1");
|
||||
EXPECT_TRUE(addr.is_multicast());
|
||||
}
|
||||
|
||||
TEST(IPAddressHost, IPv6UnicastNotMulticast) {
|
||||
IPAddress addr("::1");
|
||||
EXPECT_FALSE(addr.is_multicast());
|
||||
}
|
||||
|
||||
TEST(IPAddressHost, IPv6EqualityMatch) {
|
||||
IPAddress a("ff12::cafe");
|
||||
IPAddress b("ff12::cafe");
|
||||
EXPECT_EQ(a, b);
|
||||
}
|
||||
|
||||
TEST(IPAddressHost, IPv6EqualityMismatch) {
|
||||
IPAddress a("ff12::cafe");
|
||||
IPAddress b("ff02::1");
|
||||
EXPECT_NE(a, b);
|
||||
}
|
||||
|
||||
TEST(IPAddressHost, IPv6OutputIsLowercase) {
|
||||
// inet_pton is case-insensitive; str_to must lowercase the output
|
||||
IPAddress addr("FF12::CAFE");
|
||||
char buf[IP_ADDRESS_BUFFER_SIZE];
|
||||
addr.str_to(buf);
|
||||
for (const char *p = buf; *p; ++p) {
|
||||
EXPECT_FALSE(*p >= 'A' && *p <= 'F') << "uppercase letter in: " << buf;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(IPAddressHost, IPv6FullAddressRoundTrip) {
|
||||
// A full 128-bit address with no compression opportunity
|
||||
const char *input = "fde0:983a:d0d3:a65e:725a:0fff:fe36:9916";
|
||||
IPAddress addr(input);
|
||||
char buf[IP_ADDRESS_BUFFER_SIZE];
|
||||
addr.str_to(buf);
|
||||
EXPECT_NE(buf[0], '\0');
|
||||
EXPECT_NE(std::string(buf).find("fde0"), std::string::npos);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Malformed input
|
||||
// =========================================================================
|
||||
|
||||
TEST(IPAddressHost, MalformedIPv4YieldsEmptyAddress) {
|
||||
IPAddress addr("not-an-ip");
|
||||
EXPECT_FALSE(addr.is_set());
|
||||
EXPECT_TRUE(addr.is_ip4());
|
||||
}
|
||||
|
||||
TEST(IPAddressHost, MalformedIPv6YieldsEmptyAddress) {
|
||||
// "gg::1" looks like IPv6 (contains ':') but fails inet_pton; addr stays
|
||||
// zeroed (type=V4 from memset) so is_set() is false and is_ip4() is true.
|
||||
IPAddress addr("gg::1");
|
||||
EXPECT_FALSE(addr.is_set());
|
||||
EXPECT_TRUE(addr.is_ip4());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Cross-family
|
||||
// =========================================================================
|
||||
|
||||
TEST(IPAddressHost, IPv4AndIPv6NotEqual) {
|
||||
IPAddress v4("192.168.1.1");
|
||||
IPAddress v6("::1");
|
||||
EXPECT_NE(v4, v6);
|
||||
}
|
||||
|
||||
} // namespace esphome::network::testing
|
||||
|
||||
#endif // USE_NETWORK_IPV6
|
||||
#endif // USE_HOST
|
||||
@@ -0,0 +1 @@
|
||||
nrf52:
|
||||
@@ -2,3 +2,5 @@ nrf52:
|
||||
dfu: true
|
||||
reg0:
|
||||
voltage: 1.8V
|
||||
framework:
|
||||
libc_nano: false
|
||||
|
||||
@@ -2,11 +2,9 @@ wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
# Purposely test that `online_image:` does auto-load `image:`
|
||||
# Keep the `image:` undefined.
|
||||
# image:
|
||||
online_image:
|
||||
- id: online_binary_image
|
||||
image:
|
||||
- platform: online_image
|
||||
id: online_binary_image
|
||||
url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png
|
||||
format: PNG
|
||||
type: BINARY
|
||||
@@ -21,34 +19,41 @@ online_image:
|
||||
} else {
|
||||
ESP_LOGD("online_image", "Cache miss: fresh download");
|
||||
}
|
||||
- id: online_binary_transparent_image
|
||||
- platform: online_image
|
||||
id: online_binary_transparent_image
|
||||
url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png
|
||||
type: BINARY
|
||||
transparency: chroma_key
|
||||
format: png
|
||||
- id: online_rgba_image
|
||||
- platform: online_image
|
||||
id: online_rgba_image
|
||||
url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png
|
||||
format: PNG
|
||||
type: RGB
|
||||
transparency: alpha_channel
|
||||
- id: online_rgb24_image
|
||||
- platform: online_image
|
||||
id: online_rgb24_image
|
||||
url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png
|
||||
format: PNG
|
||||
type: RGB
|
||||
transparency: chroma_key
|
||||
- id: online_binary_bmp
|
||||
- platform: online_image
|
||||
id: online_binary_bmp
|
||||
url: https://samples-files.com/samples/images/bmp/480-360-sample.bmp
|
||||
format: BMP
|
||||
type: BINARY
|
||||
- id: online_rgb_bmp_8bit
|
||||
- platform: online_image
|
||||
id: online_rgb_bmp_8bit
|
||||
url: https://samples-files.com/samples/images/bmp/480-360-sample.bmp
|
||||
format: BMP
|
||||
type: RGB
|
||||
- id: online_jpeg_image
|
||||
- platform: online_image
|
||||
id: online_jpeg_image
|
||||
url: http://www.faqs.org/images/library.jpg
|
||||
format: JPEG
|
||||
type: RGB
|
||||
- id: online_jpg_image
|
||||
- platform: online_image
|
||||
id: online_jpg_image
|
||||
url: http://www.faqs.org/images/library.jpg
|
||||
format: JPG
|
||||
type: RGB565
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# Legacy top-level `online_image:` form (deprecated; migrates to
|
||||
# `platform: online_image`). Config-only test exercising the deprecation path.
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
http_request:
|
||||
|
||||
display:
|
||||
- platform: sdl
|
||||
id: online_image_display
|
||||
auto_clear_enabled: false
|
||||
dimensions:
|
||||
width: 480
|
||||
height: 480
|
||||
|
||||
online_image:
|
||||
- id: legacy_online_image
|
||||
url: http://www.example.org/example.png
|
||||
format: PNG
|
||||
type: RGB565
|
||||
resize: 50x50
|
||||
@@ -28,3 +28,32 @@ text_sensor:
|
||||
name: "PAN ID"
|
||||
ext_pan_id:
|
||||
name: "Extended PAN ID"
|
||||
|
||||
sensor:
|
||||
- platform: openthread_info
|
||||
parent_average_rssi:
|
||||
name: "Parent Average RSSI"
|
||||
parent_last_rssi:
|
||||
name: "Parent Last RSSI"
|
||||
parent_link_quality_in:
|
||||
name: "Parent Link Quality In"
|
||||
parent_link_quality_out:
|
||||
name: "Parent Link Quality Out"
|
||||
tx_total:
|
||||
name: "TX Total"
|
||||
tx_retries:
|
||||
name: "TX Retries"
|
||||
tx_err_cca:
|
||||
name: "TX CCA Errors"
|
||||
tx_err_abort:
|
||||
name: "TX Abort Errors"
|
||||
rx_total:
|
||||
name: "RX Total"
|
||||
rx_err_fcs:
|
||||
name: "RX FCS Errors"
|
||||
attach_attempts:
|
||||
name: "Attach Attempts"
|
||||
parent_changes:
|
||||
name: "Parent Changes"
|
||||
partition_id_changes:
|
||||
name: "Partition ID Changes"
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
zephyr_ble_server:
|
||||
|
||||
ota:
|
||||
- platform: zephyr_mcumgr
|
||||
@@ -0,0 +1,52 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "esphome/components/ota/ota_backend.h"
|
||||
|
||||
namespace esphome::ota::testing {
|
||||
|
||||
// version_is_older(candidate, reference) == true means candidate is a downgrade
|
||||
// and should be rejected.
|
||||
|
||||
TEST(VersionIsOlder, PatchOlder) {
|
||||
EXPECT_TRUE(version_is_older("1.2.3", "1.2.4"));
|
||||
EXPECT_FALSE(version_is_older("1.2.4", "1.2.3"));
|
||||
}
|
||||
|
||||
TEST(VersionIsOlder, NumericNotLexical) {
|
||||
// "1.10.0" is newer than "1.9.0" even though '1' < '9' lexically.
|
||||
EXPECT_TRUE(version_is_older("1.9.0", "1.10.0"));
|
||||
EXPECT_FALSE(version_is_older("1.10.0", "1.9.0"));
|
||||
}
|
||||
|
||||
TEST(VersionIsOlder, MajorMinor) {
|
||||
EXPECT_TRUE(version_is_older("1.9.9", "2.0.0"));
|
||||
EXPECT_TRUE(version_is_older("1.2.9", "1.3.0"));
|
||||
EXPECT_FALSE(version_is_older("2.0.0", "1.9.9"));
|
||||
}
|
||||
|
||||
TEST(VersionIsOlder, EqualVersionsAllowed) {
|
||||
// Re-flashing the same version must be permitted.
|
||||
EXPECT_FALSE(version_is_older("1.2.3", "1.2.3"));
|
||||
EXPECT_FALSE(version_is_older("2024.1.0", "2024.1.0"));
|
||||
}
|
||||
|
||||
TEST(VersionIsOlder, DifferingComponentCounts) {
|
||||
// Missing trailing components count as 0.
|
||||
EXPECT_FALSE(version_is_older("1.2", "1.2.0"));
|
||||
EXPECT_FALSE(version_is_older("1.2.0", "1.2"));
|
||||
EXPECT_TRUE(version_is_older("1.2", "1.2.1"));
|
||||
EXPECT_FALSE(version_is_older("1.2.1", "1.2"));
|
||||
}
|
||||
|
||||
TEST(VersionIsOlder, CalendarVersions) {
|
||||
EXPECT_TRUE(version_is_older("2024.12.0", "2025.1.0"));
|
||||
EXPECT_FALSE(version_is_older("2025.1.0", "2024.12.0"));
|
||||
}
|
||||
|
||||
TEST(VersionIsOlder, NullInputsAreSafe) {
|
||||
EXPECT_FALSE(version_is_older(nullptr, "1.2.3"));
|
||||
EXPECT_FALSE(version_is_older("1.2.3", nullptr));
|
||||
EXPECT_FALSE(version_is_older(nullptr, nullptr));
|
||||
}
|
||||
|
||||
} // namespace esphome::ota::testing
|
||||
@@ -7,36 +7,40 @@ udp:
|
||||
addresses: ["239.0.60.53"]
|
||||
|
||||
packet_transport:
|
||||
platform: udp
|
||||
update_interval: 5s
|
||||
encryption: "our key goes here"
|
||||
rolling_code_enable: true
|
||||
ping_pong_enable: true
|
||||
binary_sensors:
|
||||
- binary_sensor_id1
|
||||
- id: binary_sensor_id1
|
||||
broadcast_id: other_id
|
||||
sensors:
|
||||
- sensor_id1
|
||||
- id: sensor_id1
|
||||
broadcast_id: other_id
|
||||
providers:
|
||||
- name: some-device-name
|
||||
encryption: "their key goes here"
|
||||
- platform: udp
|
||||
id: transport_udp
|
||||
update_interval: 5s
|
||||
encryption: "our key goes here"
|
||||
rolling_code_enable: true
|
||||
ping_pong_enable: true
|
||||
binary_sensors:
|
||||
- binary_sensor_id1
|
||||
- id: binary_sensor_id1
|
||||
broadcast_id: other_id
|
||||
sensors:
|
||||
- sensor_id1
|
||||
- id: sensor_id1
|
||||
broadcast_id: other_id
|
||||
providers:
|
||||
- name: some-device-name
|
||||
encryption: "their key goes here"
|
||||
|
||||
sensor:
|
||||
- platform: template
|
||||
id: sensor_id1
|
||||
- platform: packet_transport
|
||||
transport_id: transport_udp
|
||||
provider: some-device-name
|
||||
id: our_id
|
||||
remote_id: some_sensor_id
|
||||
|
||||
binary_sensor:
|
||||
- platform: packet_transport
|
||||
transport_id: transport_udp
|
||||
provider: unencrypted-device
|
||||
id: other_binary_sensor_id
|
||||
- platform: packet_transport
|
||||
transport_id: transport_udp
|
||||
provider: some-device-name
|
||||
type: status
|
||||
name: Some-Device Status
|
||||
|
||||
@@ -3,36 +3,40 @@ udp:
|
||||
addresses: ["239.0.60.53"]
|
||||
|
||||
packet_transport:
|
||||
platform: udp
|
||||
update_interval: 5s
|
||||
encryption: "our key goes here"
|
||||
rolling_code_enable: true
|
||||
ping_pong_enable: true
|
||||
binary_sensors:
|
||||
- binary_sensor_id1
|
||||
- id: binary_sensor_id1
|
||||
broadcast_id: other_id
|
||||
sensors:
|
||||
- sensor_id1
|
||||
- id: sensor_id1
|
||||
broadcast_id: other_id
|
||||
providers:
|
||||
- name: some-device-name
|
||||
encryption: "their key goes here"
|
||||
- platform: udp
|
||||
id: transport_udp
|
||||
update_interval: 5s
|
||||
encryption: "our key goes here"
|
||||
rolling_code_enable: true
|
||||
ping_pong_enable: true
|
||||
binary_sensors:
|
||||
- binary_sensor_id1
|
||||
- id: binary_sensor_id1
|
||||
broadcast_id: other_id
|
||||
sensors:
|
||||
- sensor_id1
|
||||
- id: sensor_id1
|
||||
broadcast_id: other_id
|
||||
providers:
|
||||
- name: some-device-name
|
||||
encryption: "their key goes here"
|
||||
|
||||
sensor:
|
||||
- platform: template
|
||||
id: sensor_id1
|
||||
- platform: packet_transport
|
||||
transport_id: transport_udp
|
||||
provider: some-device-name
|
||||
id: our_id
|
||||
remote_id: some_sensor_id
|
||||
|
||||
binary_sensor:
|
||||
- platform: packet_transport
|
||||
transport_id: transport_udp
|
||||
provider: unencrypted-device
|
||||
id: other_binary_sensor_id
|
||||
- platform: packet_transport
|
||||
transport_id: transport_udp
|
||||
provider: some-device-name
|
||||
type: status
|
||||
name: Some-Device Status
|
||||
|
||||
@@ -4,6 +4,11 @@ audio_dac:
|
||||
i2c_id: i2c_bus
|
||||
address: 0x4D
|
||||
bits_per_sample: 32bit
|
||||
analog_gain: -6db
|
||||
channel_mix: swapped
|
||||
volume_min_db: -60dB
|
||||
volume_max_db: -3dB
|
||||
enable_pin: GPIO12
|
||||
|
||||
output:
|
||||
- platform: gpio
|
||||
@@ -22,3 +27,9 @@ binary_sensor:
|
||||
number: 4
|
||||
mode:
|
||||
input: true
|
||||
|
||||
switch:
|
||||
- platform: pcm5122
|
||||
pcm5122: pcm5122_dac
|
||||
name: PCM5122 Power Down
|
||||
power_mode: powerdown
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# Exercises the opt-in that compiles the RTC-backed preference storage into
|
||||
# the ESP32 backend without any other option selecting it.
|
||||
preferences:
|
||||
rtc_storage: true
|
||||
@@ -0,0 +1,25 @@
|
||||
# Exercises the provisioning window: api registers as a provisioning source
|
||||
# (encryption enabled, no key), the on_timeout automation, and the wifi +
|
||||
# esp32_improv cross-component guards. improv_serial is intentionally NOT gated.
|
||||
provisioning:
|
||||
timeout: 1min
|
||||
on_timeout:
|
||||
then:
|
||||
- logger.log: "Provisioning window expired"
|
||||
|
||||
api:
|
||||
encryption:
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
improv_serial:
|
||||
|
||||
binary_sensor:
|
||||
- platform: gpio
|
||||
pin: 0
|
||||
id: io0_button
|
||||
|
||||
esp32_improv:
|
||||
authorizer: io0_button
|
||||
@@ -0,0 +1,16 @@
|
||||
# Provisioning window on ESP8266 (no BLE Improv): api as a provisioning source
|
||||
# and the wifi reboot guard. improv_serial is present and intentionally NOT gated.
|
||||
provisioning:
|
||||
timeout: 1min
|
||||
on_timeout:
|
||||
then:
|
||||
- logger.log: "Provisioning window expired"
|
||||
|
||||
api:
|
||||
encryption:
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
improv_serial:
|
||||
@@ -0,0 +1,15 @@
|
||||
# A device provisioned over the network (wifi / Improv) with no api: network
|
||||
# connectivity alone satisfies provisioning, so `provisioning:` is valid without an
|
||||
# api encryption source. Config-only -- exercises the network provisioning-source
|
||||
# validation path (the Improv-only case from the review).
|
||||
provisioning:
|
||||
timeout: 1min
|
||||
on_timeout:
|
||||
then:
|
||||
- logger.log: "Provisioning window expired"
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
improv_serial:
|
||||
@@ -198,7 +198,7 @@ button:
|
||||
0xFF,
|
||||
]
|
||||
- platform: template
|
||||
name: Haier
|
||||
name: Haier Long
|
||||
on_press:
|
||||
remote_transmitter.transmit_haier:
|
||||
code:
|
||||
@@ -217,6 +217,21 @@ button:
|
||||
0x00,
|
||||
0x05,
|
||||
]
|
||||
- platform: template
|
||||
name: Haier Short
|
||||
on_press:
|
||||
remote_transmitter.transmit_haier:
|
||||
code:
|
||||
[
|
||||
0xA6,
|
||||
0xDA,
|
||||
0x00,
|
||||
0x00,
|
||||
0x40,
|
||||
0x40,
|
||||
0x00,
|
||||
0x80,
|
||||
]
|
||||
- platform: template
|
||||
name: Mirage
|
||||
on_press:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user