Merge branch 'neutral-ble-client' into radon-eye-single-node

This commit is contained in:
J. Nick Koston
2026-10-02 08:19:44 -05:00
1433 changed files with 60696 additions and 28535 deletions
@@ -0,0 +1,19 @@
button:
- platform: template
name: AGS10 Zero Point
on_press:
- ags10.set_zero_point:
id: ags10_1
mode: FACTORY_DEFAULT
- ags10.set_zero_point:
id: ags10_1
mode: CUSTOM_VALUE
value: 0x1234
- ags10.set_zero_point:
id: ags10_1
mode: !lambda return ags10::CURRENT_VALUE;
value: !lambda return 0x2345;
- ags10.new_i2c_address: 0x1B
- ags10.new_i2c_address:
id: ags10_1
address: !lambda return 0x1C;
@@ -0,0 +1,4 @@
packages:
i2c_low_freq: !include ../../test_build_components/common/i2c_low_freq/esp8266-ard.yaml
ags10: !include common.yaml
actions: !include common-actions.yaml
@@ -0,0 +1,8 @@
button:
- platform: template
name: AIC3204 Auto Mute
on_press:
- aic3204.set_auto_mute_mode: 3
- aic3204.set_auto_mute_mode:
id: aic3204_dac
mode: !lambda return 7;
+3
View File
@@ -5,7 +5,10 @@ esphome:
- audio_dac.mute_on:
- audio_dac.set_volume:
volume: 50%
- audio_dac.set_volume:
volume: !lambda return id(aic3204_dac).volume() * 0.5f;
audio_dac:
- platform: aic3204
id: aic3204_dac
i2c_id: i2c_bus
@@ -0,0 +1,4 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
aic3204: !include common.yaml
actions: !include common-actions.yaml
@@ -0,0 +1,16 @@
button:
- platform: template
name: Alarm Calls
on_press:
- alarm_control_panel.pending: alarmcontrolpanel1
- alarm_control_panel.triggered: alarmcontrolpanel1
- if:
condition:
alarm_control_panel.is_armed: alarmcontrolpanel1
then:
- logger.log: Armed
- if:
condition:
alarm_control_panel.ready: alarmcontrolpanel1
then:
- logger.log: Ready
@@ -67,3 +67,28 @@ alarm_control_panel:
on_cleared:
then:
- logger.log: "### CLEARED ###"
button:
# Exercise the arm/disarm actions with a constant code, a lambda code and no code.
- platform: template
name: Alarm Panel Actions
on_press:
- alarm_control_panel.arm_away:
id: alarmcontrolpanel1
code: "1234"
- alarm_control_panel.arm_home:
id: alarmcontrolpanel1
code: !lambda |-
std::string code = "56";
code += "78";
return code;
- alarm_control_panel.arm_night:
id: alarmcontrolpanel1
code: "0000"
- alarm_control_panel.disarm:
id: alarmcontrolpanel1
code: "1234"
- alarm_control_panel.arm_away: alarmcontrolpanel2
- alarm_control_panel.arm_home: alarmcontrolpanel2
- alarm_control_panel.arm_night: alarmcontrolpanel2
- alarm_control_panel.disarm: alarmcontrolpanel2
@@ -0,0 +1,3 @@
packages:
alarm_control_panel: !include common.yaml
actions: !include common-actions.yaml
@@ -0,0 +1,9 @@
button:
- platform: template
name: Animation Actions
on_press:
- animation.next_frame: rgb_animation
- animation.prev_frame: rgb_animation
- animation.set_frame:
id: rgb_animation
frame: 2
@@ -0,0 +1,14 @@
packages:
spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml
animation: !include common.yaml
actions: !include common-actions.yaml
display:
- platform: ili9xxx
id: animation_main_lcd
spi_id: spi_bus
model: ili9342
cs_pin: 5
dc_pin: 15
reset_pin: 16
invert_colors: false
-2
View File
@@ -1,5 +1,4 @@
import esphome.codegen as cg
from esphome.core import CORE
from tests.testing_helpers import ComponentManifestOverride
@@ -12,6 +11,5 @@ def override_manifest(manifest: ComponentManifestOverride) -> None:
cg.add_define("API_MAX_SEND_QUEUE", 8)
cg.add_define("MAX_API_CONNECTIONS", 1)
cg.add_define("USE_SOCKET_IMPL_BSD_SOCKETS")
CORE.register_controller() # api_server registers with the controller registry
manifest.to_code = to_code_testing
+3 -3
View File
@@ -92,7 +92,7 @@ api:
then:
- logger.log:
# yamllint disable rule:line-length
format: "Bool: %s (%u), Int: %ld (%u), Float: %f (%u), String: %s (%u)"
format: "Bool: %s (%zu), Int: %ld (%zu), Float: %f (%zu), String: %s (%zu)"
# yamllint enable rule:line-length
args:
- YESNO(bool_arr[0])
@@ -116,7 +116,7 @@ api:
then:
- delay: 20ms
- logger.log:
format: "Delayed: %s (%u ints, %u strings)"
format: "Delayed: %s (%zu ints, %zu strings)"
args:
- name.c_str()
- int_arr.size()
@@ -139,7 +139,7 @@ api:
message: !lambda 'return name;'
on_success:
- logger.log:
format: "Notified %s (%u ints)"
format: "Notified %s (%zu ints)"
args:
- name.c_str()
- int_arr.size()
@@ -59,7 +59,7 @@ static void verify_mac(uint64_t mac, size_t expected_bytes) {
#ifdef ESPHOME_DEBUG_API
uint8_t *proto_debug_end_ = api_buf.data() + api_buf.size();
#endif
ProtoEncode::encode_varint_raw_48bit(pos PROTO_ENCODE_DEBUG_ARG, mac);
pos = ProtoEncode::encode_varint_raw_48bit(pos PROTO_ENCODE_DEBUG_ARG, mac);
size_t new_len = pos - api_buf.data();
EXPECT_EQ(new_len, expected_bytes) << "mac=0x" << std::hex << mac << std::dec;
@@ -37,6 +37,27 @@ binary_sensor:
format: "New state is %s"
args: ['x.has_value() ? ONOFF(x) : "Unknown"']
- binary_sensor.invalidate_state: binary_sensor_some_binary_sensor
- binary_sensor.template.publish:
id: binary_sensor_publish_target
state: true
- binary_sensor.template.publish:
id: binary_sensor_publish_target
state: !lambda "return x.value_or(false);"
- if:
condition:
binary_sensor.is_on: binary_sensor_publish_target
then:
- logger.log: "publish_target is on"
- if:
condition:
binary_sensor.is_off:
id: binary_sensor_publish_target
then:
- logger.log: "publish_target is off"
- platform: template
id: binary_sensor_publish_target
name: "Publish Target"
# Test autorepeat with default configuration (no timings)
- platform: template
@@ -0,0 +1,7 @@
button:
- platform: template
name: BM8563 Actions
on_press:
- bm8563.read_time:
- bm8563.start_timer:
duration: !lambda return 60;
@@ -0,0 +1,4 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
bm8563: !include common.yaml
actions: !include common-actions.yaml
@@ -0,0 +1,66 @@
#include <gtest/gtest.h>
#include <cstdint>
#include <initializer_list>
#include <vector>
#include "esphome/components/bthome_mithermometer/bthome_ble.h"
namespace esphome::bthome_mithermometer::testing {
namespace {
constexpr uint64_t SENSOR_ADDRESS = 0xA4C1384E1678ULL;
// Unencrypted BTHome v2 service data (UUID 0xFCD2) sent from SENSOR_ADDRESS.
ble_device_base::ESPBTDevice advert(std::initializer_list<uint8_t> service_data) {
std::vector<uint8_t> adv = {static_cast<uint8_t>(service_data.size() + 3), 0x16, 0xD2, 0xFC};
adv.insert(adv.end(), service_data.begin(), service_data.end());
uint8_t mac[6];
for (size_t i = 0; i < 6; i++)
mac[i] = static_cast<uint8_t>(SENSOR_ADDRESS >> (i * 8));
ble_device_base::ESPBTDevice device;
device.from_scan_result(mac, -60, 0, adv.data(), static_cast<uint16_t>(adv.size()));
return device;
}
struct Harness {
Harness() {
this->thermometer.set_address(SENSOR_ADDRESS);
this->thermometer.set_temperature(&this->temperature);
this->thermometer.set_humidity(&this->humidity);
this->thermometer.set_battery_level(&this->battery_level);
}
BTHomeMiThermometer thermometer;
sensor::Sensor temperature, humidity, battery_level;
};
} // namespace
// Shelly BLU H&T: packet id, battery 90 %, humidity 55 % (0x2E), temperature 22.5 C (0x45)
TEST(BTHomeMiThermometerObjects, DecodesShellyStyleTemperatureAndHumidity) {
Harness h;
ASSERT_TRUE(h.thermometer.parse_device(advert({0x40, 0x00, 0x01, 0x01, 0x5A, 0x2E, 0x37, 0x45, 0xE1, 0x00})));
EXPECT_NEAR(h.temperature.state, 22.5f, 0.001f);
EXPECT_FLOAT_EQ(h.humidity.state, 55.0f);
EXPECT_FLOAT_EQ(h.battery_level.state, 90.0f);
}
TEST(BTHomeMiThermometerObjects, DecodesNegativeTenthDegreeTemperature) {
Harness h;
// -5.3 C = -53 = 0xFFCB
ASSERT_TRUE(h.thermometer.parse_device(advert({0x40, 0x45, 0xCB, 0xFF})));
EXPECT_NEAR(h.temperature.state, -5.3f, 0.001f);
EXPECT_FALSE(h.humidity.has_state());
}
// PVVX firmware: temperature 22.50 C (0x02) and humidity 45.67 % (0x03) keep working
TEST(BTHomeMiThermometerObjects, StillDecodesHundredthDegreeObjects) {
Harness h;
ASSERT_TRUE(h.thermometer.parse_device(advert({0x40, 0x02, 0xCA, 0x08, 0x03, 0xD7, 0x11})));
EXPECT_NEAR(h.temperature.state, 22.5f, 0.001f);
EXPECT_NEAR(h.humidity.state, 45.67f, 0.001f);
}
} // namespace esphome::bthome_mithermometer::testing
+6
View File
@@ -4,3 +4,9 @@ button:
id: some_button
on_press:
- logger.log: Button pressed
esphome:
on_boot:
- button.press: some_button
- button.press:
id: some_button
+18 -2
View File
@@ -56,8 +56,8 @@ climate:
default_target_temperature_high: 22°C
button:
# Exercise the climate.control: action so ControlAction templates get
# build coverage. Various field combinations are tested.
# Exercise the climate.control: action so the register_apply_action codegen
# gets build coverage. Various field combinations are tested.
- platform: template
name: "Climate Control Mode"
on_press:
@@ -84,6 +84,22 @@ button:
- climate.control:
id: climate_test_thermostat
mode: "OFF"
# Custom strings take the (const char *, size_t) overload; the byte length of
# the non-ASCII value is computed at codegen time.
- platform: template
name: "Climate Control Custom Strings"
on_press:
- climate.control:
id: climate_test_thermostat
custom_fan_mode: "Über"
custom_preset: Default
- platform: template
name: "Climate Control Custom Lambdas"
on_press:
- climate.control:
id: climate_test_thermostat
custom_fan_mode: !lambda return "quiet";
custom_preset: !lambda return "Default";
# Exercise climate.control inside a trigger with non-empty Ts (number on_value
# passes float).
+6
View File
@@ -0,0 +1,6 @@
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
# ClimateIR derives from climate::Climate without declaring it as a dependency.
manifest.dependencies = manifest.dependencies + ["climate"]
@@ -0,0 +1,83 @@
#include <gtest/gtest.h>
#include "esphome/components/climate_ir/climate_ir.h"
namespace esphome::climate_ir::testing {
class TestClimateIR : public ClimateIR {
public:
explicit TestClimateIR(bool supports_dry = false, bool supports_fan_only = false)
: ClimateIR(16.0f, 30.0f, 1.0f, supports_dry, supports_fan_only) {}
using ClimateIR::traits;
protected:
void transmit_state() override {}
};
// The HEAT_COOL default is covered in tests/component_tests/climate_ir.
TEST(ClimateIRTest, HeatCoolAdvertisedWhenSupported) {
TestClimateIR climate;
climate.set_supports_heat(true);
climate.set_supports_cool(true);
climate.set_supports_heat_cool(true);
EXPECT_TRUE(climate.traits().supports_mode(climate::CLIMATE_MODE_HEAT_COOL));
}
TEST(ClimateIRTest, HeatCoolNotAdvertisedWhenUnsupported) {
TestClimateIR climate;
climate.set_supports_heat(true);
climate.set_supports_cool(true);
climate.set_supports_heat_cool(false);
EXPECT_FALSE(climate.traits().supports_mode(climate::CLIMATE_MODE_HEAT_COOL));
}
TEST(ClimateIRTest, HeatCoolAdvertisedForCoolOnlyDeviceThatSupportsIt) {
TestClimateIR climate;
climate.set_supports_heat(false);
climate.set_supports_cool(true);
climate.set_supports_heat_cool(true);
auto traits = climate.traits();
EXPECT_TRUE(traits.supports_mode(climate::CLIMATE_MODE_HEAT_COOL));
EXPECT_FALSE(traits.supports_mode(climate::CLIMATE_MODE_HEAT));
}
TEST(ClimateIRTest, HeatAndCoolModesFollowTheirOwnFlags) {
TestClimateIR climate;
climate.set_supports_heat(false);
climate.set_supports_cool(true);
climate.set_supports_heat_cool(false);
auto traits = climate.traits();
EXPECT_TRUE(traits.supports_mode(climate::CLIMATE_MODE_COOL));
EXPECT_FALSE(traits.supports_mode(climate::CLIMATE_MODE_HEAT));
EXPECT_FALSE(traits.supports_mode(climate::CLIMATE_MODE_HEAT_COOL));
EXPECT_TRUE(traits.supports_mode(climate::CLIMATE_MODE_OFF));
}
TEST(ClimateIRTest, DefaultModes) {
TestClimateIR climate;
auto traits = climate.traits();
EXPECT_TRUE(traits.supports_mode(climate::CLIMATE_MODE_OFF));
EXPECT_TRUE(traits.supports_mode(climate::CLIMATE_MODE_COOL));
EXPECT_TRUE(traits.supports_mode(climate::CLIMATE_MODE_HEAT));
EXPECT_TRUE(traits.supports_mode(climate::CLIMATE_MODE_HEAT_COOL));
EXPECT_FALSE(traits.supports_mode(climate::CLIMATE_MODE_DRY));
EXPECT_FALSE(traits.supports_mode(climate::CLIMATE_MODE_FAN_ONLY));
}
TEST(ClimateIRTest, DryAndFanOnlyFromConstructor) {
TestClimateIR climate(true, true);
auto traits = climate.traits();
EXPECT_TRUE(traits.supports_mode(climate::CLIMATE_MODE_DRY));
EXPECT_TRUE(traits.supports_mode(climate::CLIMATE_MODE_FAN_ONLY));
}
TEST(ClimateIRTest, SetterCanTurnAModeBackOn) {
TestClimateIR climate;
climate.set_supports_cool(false);
EXPECT_FALSE(climate.traits().supports_mode(climate::CLIMATE_MODE_COOL));
climate.set_supports_cool(true);
EXPECT_TRUE(climate.traits().supports_mode(climate::CLIMATE_MODE_COOL));
}
} // namespace esphome::climate_ir::testing
@@ -0,0 +1,5 @@
button:
- platform: template
name: CM1106 Calibrate Zero
on_press:
- cm1106.calibrate_zero:
@@ -0,0 +1,8 @@
substitutions:
tx_pin: GPIO0
rx_pin: GPIO2
packages:
uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml
cm1106: !include common.yaml
actions: !include common-actions.yaml
+102
View File
@@ -2,6 +2,9 @@
#include "esphome/core/string_ref.h"
#include <iterator>
#include <string>
namespace esphome::core::testing {
TEST(StringRefStartsWith, ProperPrefixMatches) {
@@ -59,4 +62,103 @@ TEST(StringRefStartsWith, RefOverloadComparesOnlyTheViewedLength) {
EXPECT_TRUE(ref.starts_with(prefix));
}
// The generated api messages start their encode only string fields as a null pointer with zero
// length; every member must treat that exactly like the default constructed empty string.
TEST(StringRefNullEmpty, BehavesAsEmptyString) {
const StringRef null_empty{nullptr, 0};
const StringRef empty;
EXPECT_TRUE(null_empty.empty());
EXPECT_EQ(null_empty.size(), 0u);
EXPECT_EQ(null_empty.c_str(), nullptr);
EXPECT_EQ(null_empty.byte(), nullptr);
EXPECT_TRUE(null_empty == empty);
EXPECT_FALSE(null_empty < empty);
EXPECT_FALSE(empty < null_empty);
EXPECT_TRUE(null_empty == ""); // NOLINT(readability-container-size-empty) - operator under test
EXPECT_TRUE(null_empty == std::string()); // NOLINT(readability-container-size-empty) - operator under test
EXPECT_EQ(null_empty.compare(empty), 0);
EXPECT_EQ(null_empty.compare(""), 0);
EXPECT_LT(null_empty.compare("a"), 0);
EXPECT_TRUE(null_empty.starts_with(""));
EXPECT_FALSE(null_empty.starts_with("a"));
EXPECT_EQ(null_empty.str(), std::string());
EXPECT_EQ(null_empty.substr(0), std::string());
EXPECT_EQ(null_empty.find('a'), std::string::npos);
EXPECT_EQ(null_empty.find("a"), std::string::npos);
char buf[4] = "xyz";
EXPECT_EQ(null_empty.copy(buf, sizeof(buf)), 0u);
EXPECT_EQ(null_empty.begin(), null_empty.end());
}
TEST(StringRefNullEmpty, ComparesAgainstText) {
const StringRef null_empty{nullptr, 0};
const StringRef text("abc", 3);
EXPECT_FALSE(null_empty == text);
EXPECT_FALSE(text == null_empty);
EXPECT_LT(null_empty.compare(text), 0);
EXPECT_GT(text.compare(null_empty), 0);
EXPECT_TRUE(null_empty < text);
EXPECT_FALSE(text < null_empty);
EXPECT_TRUE(text.starts_with(null_empty));
}
TEST(StringRefNullEmpty, TwoNullViewsAreEqual) {
const StringRef a{nullptr, 0};
const StringRef b{nullptr, 0};
EXPECT_TRUE(a == b);
EXPECT_FALSE(a < b);
EXPECT_EQ(a.compare(b), 0);
EXPECT_TRUE(a.starts_with(b));
}
// Every iterator endpoint of a null view is the same null position: nothing is dereferenced and
// no offset is applied to the null pointer, so the range is simply empty.
TEST(StringRefNullEmpty, IteratorEndpointsFormAnEmptyRange) {
const StringRef null_empty{nullptr, 0};
EXPECT_EQ(null_empty.cbegin(), null_empty.cend());
EXPECT_EQ(null_empty.rbegin(), null_empty.rend());
EXPECT_EQ(null_empty.crbegin(), null_empty.crend());
EXPECT_EQ(std::distance(null_empty.begin(), null_empty.end()), 0);
size_t visited = 0;
for (char c : null_empty) {
(void) c;
visited++;
}
EXPECT_EQ(visited, 0u);
// NOLINTNEXTLINE(bugprone-string-constructor) - empty range under test
EXPECT_EQ(std::string(null_empty.begin(), null_empty.end()), std::string());
}
// The pointer and length constructor accepts an empty range at a null pointer; the copy into a
// std::string reads nothing.
TEST(StringRefNullEmpty, ConvertsToEmptyStdString) {
const StringRef null_empty{nullptr, 0};
const std::string copy = null_empty.str();
EXPECT_TRUE(copy.empty());
EXPECT_EQ(static_cast<std::string>(null_empty), std::string());
EXPECT_EQ(null_empty.substr(0, 5), std::string());
std::string target("keep");
target += null_empty;
EXPECT_EQ(target, "keep");
}
// The number conversions hand the pointer to the C library; a null view must stop before that.
TEST(StringRefNullEmpty, NumericConversionsReturnZero) {
const StringRef null_empty{nullptr, 0};
size_t pos = 99;
EXPECT_EQ(stoi(null_empty, &pos), 0);
EXPECT_EQ(pos, 0u);
pos = 99;
EXPECT_EQ(stol(null_empty, &pos, 16), 0L);
EXPECT_EQ(pos, 0u);
pos = 99;
EXPECT_EQ(stof(null_empty, &pos), 0.0f);
EXPECT_EQ(pos, 0u);
pos = 99;
EXPECT_EQ(stod(null_empty, &pos), 0.0);
EXPECT_EQ(pos, 0u);
EXPECT_EQ(stoi(null_empty), 0);
EXPECT_EQ(stof(null_empty), 0.0f);
}
} // namespace esphome::core::testing
+30
View File
@@ -0,0 +1,30 @@
#pragma once
#include <gtest/gtest.h>
#include <cstdint>
#include <limits>
#include <utility>
#include "esphome/core/helpers.h"
#include "esphome/components/counter/counter_sensor.h"
namespace esphome::counter::testing {
constexpr int64_t INT64_MAX_VALUE = std::numeric_limits<int64_t>::max();
constexpr int64_t INT64_MIN_VALUE = std::numeric_limits<int64_t>::min();
/// Stands in for a binary sensor: reports each state to its callbacks.
struct FakeBinarySource {
template<typename F> void add_on_state_callback(F &&callback) { this->callbacks_.add(std::forward<F>(callback)); }
void publish(bool state) { this->callbacks_.call(state); }
CallbackManager<void(bool)> callbacks_;
};
// Restore is off so no preference storage is needed.
class CounterTest : public ::testing::Test {
protected:
CounterSensor counter_{false, 0};
};
} // namespace esphome::counter::testing
+37
View File
@@ -0,0 +1,37 @@
esphome:
on_boot:
then:
- counter.set_value:
id: counter_sensor
value: 100
- counter.increment: counter_sensor
- counter.increment:
id: counter_sensor
value: -5
- counter.increment:
id: counter_sensor
value: !lambda "return 5000000000LL;"
sensor:
- platform: counter
id: counter_sensor
name: Counter
- platform: counter
id: counter_no_restore
name: Counter No Restore
restore: false
initial_value: 10
- platform: template
id: source_sensor
lambda: return 1.0;
update_interval: 1s
- platform: counter
name: Counter Of Sensor
sensor: source_sensor
- platform: counter
name: Counter Of Binary Sensor
binary_sensor: source_binary_sensor
binary_sensor:
- platform: template
id: source_binary_sensor
@@ -0,0 +1,84 @@
#include "../common.h"
namespace esphome::counter::testing {
TEST_F(CounterTest, StartsAtZero) {
this->counter_.setup();
EXPECT_EQ(this->counter_.state, 0.0f);
}
TEST(CounterInitialValue, PublishedAtSetupWhenNotRestoring) {
CounterSensor counter(false, -12);
counter.setup();
EXPECT_EQ(counter.state, -12.0f);
counter.increment();
EXPECT_EQ(counter.state, -11.0f);
}
TEST_F(CounterTest, SetValuePublishesState) {
this->counter_.set_value(42);
EXPECT_EQ(this->counter_.state, 42.0f);
}
TEST_F(CounterTest, IncrementDefaultsToOne) {
this->counter_.increment();
this->counter_.increment();
EXPECT_EQ(this->counter_.state, 2.0f);
}
TEST_F(CounterTest, IncrementAcceptsNegativeAmounts) {
this->counter_.set_value(10);
this->counter_.increment(-25);
EXPECT_EQ(this->counter_.state, -15.0f);
}
TEST_F(CounterTest, ValueBeyondInt32) {
this->counter_.set_value(5000000000LL);
this->counter_.increment(5000000000LL);
EXPECT_EQ(this->counter_.state, 1.0e10f);
}
TEST_F(CounterTest, IncrementWrapsAtInt64Limits) {
this->counter_.set_value(INT64_MAX_VALUE);
this->counter_.increment(1);
EXPECT_EQ(this->counter_.state, static_cast<float>(INT64_MIN_VALUE));
this->counter_.set_value(INT64_MIN_VALUE);
this->counter_.increment(-1);
EXPECT_EQ(this->counter_.state, static_cast<float>(INT64_MAX_VALUE));
}
TEST_F(CounterTest, CountsEachPublishFromSource) {
sensor::Sensor source;
this->counter_.count_updates_from(&source);
// The counted value is unrelated to what the source publishes.
source.publish_state(10.0f);
source.publish_state(10.0f);
source.publish_state(-3.5f);
EXPECT_EQ(this->counter_.state, 3.0f);
}
TEST_F(CounterTest, SourceUpdatesAddToCurrentValue) {
sensor::Sensor source;
this->counter_.count_updates_from(&source);
this->counter_.set_value(100);
source.publish_state(1.0f);
EXPECT_EQ(this->counter_.state, 101.0f);
}
TEST_F(CounterTest, CountsOnlyTrueFromBinarySource) {
FakeBinarySource source;
this->counter_.count_true_from(&source);
source.publish(true);
source.publish(false);
EXPECT_EQ(this->counter_.state, 1.0f);
source.publish(true);
source.publish(false);
source.publish(false);
EXPECT_EQ(this->counter_.state, 2.0f);
}
} // namespace esphome::counter::testing
@@ -0,0 +1,2 @@
packages:
counter: !include common.yaml
@@ -0,0 +1,12 @@
packages:
counter: !include common.yaml
esphome:
on_shutdown:
then:
- counter.set_value:
id: counter_no_restore
value: -9223372036854775807
- counter.increment:
id: counter_no_restore
value: 9223372036854775807
+40
View File
@@ -1,3 +1,43 @@
esphome:
on_boot:
then:
- datetime.date.set:
id: datetime_test_date
date:
year: 2021
month: 1
day: 1
- datetime.date.set:
id: datetime_test_date
date: !lambda "return {.day_of_month = 1, .month = 1, .year = 2021};"
- datetime.time.set:
id: datetime_test_time
time: "12:34:56"
- datetime.time.set:
id: datetime_test_time
time: !lambda "return {.second = 56, .minute = 34, .hour = 12};"
- datetime.datetime.set:
id: datetime_test_datetime
datetime: "2021-01-01 12:34:56"
- datetime.datetime.set:
id: datetime_test_datetime
datetime: !lambda "return {.second = 56, .minute = 34, .hour = 12, .day_of_month = 1, .month = 1, .year = 2021};"
datetime:
- platform: template
name: Datetime Test Date
id: datetime_test_date
type: date
optimistic: true
- platform: template
name: Datetime Test Time
id: datetime_test_time
type: time
optimistic: true
- platform: template
name: Datetime Test DateTime
id: datetime_test_datetime
type: datetime
optimistic: true
time:
@@ -1,4 +1,5 @@
deep_sleep:
id: deep_sleep_1
run_duration:
default: 10s
gpio_wakeup_reason: 30s
@@ -1,4 +1,5 @@
deep_sleep:
id: deep_sleep_1
run_duration:
default: 10s
gpio_wakeup_reason: 30s
@@ -1,4 +1,5 @@
deep_sleep:
id: deep_sleep_1
run_duration:
default: 10s
gpio_wakeup_reason: 30s
+4
View File
@@ -4,6 +4,10 @@ esphome:
- deep_sleep.prevent
- delay: 1s
- deep_sleep.allow
- deep_sleep.prevent:
id: deep_sleep_1
- deep_sleep.allow:
id: deep_sleep_1
- if:
condition:
lambda: 'return false;'
@@ -6,6 +6,7 @@ packages:
deep_sleep: !include common.yaml
deep_sleep:
id: deep_sleep_1
run_duration: 10s
zephyr_ble_server:
@@ -1,4 +1,5 @@
deep_sleep:
id: deep_sleep_1
run_duration: 30s
sleep_duration: 12h
on_wake:
@@ -1,4 +1,5 @@
deep_sleep:
id: deep_sleep_1
run_duration: 10s
sleep_duration: 50s
on_wake:
@@ -1,4 +1,5 @@
deep_sleep:
id: deep_sleep_1
run_duration: 10s
sleep_duration: 50s
+21
View File
@@ -1,20 +1,32 @@
esphome:
on_boot:
then:
- dfplayer.play_mp3: 3
- dfplayer.play_mp3: !lambda "return id(dfplayer_test).is_playing() ? 2 : 1;"
- dfplayer.play: 5
- dfplayer.play:
file: 4
loop: true
- dfplayer.play:
file: !lambda "return id(dfplayer_test).is_playing() ? 2 : 1;"
loop: !lambda return !id(dfplayer_test).is_playing();
- dfplayer.play_folder:
folder: 1
file: 3
- dfplayer.play_folder:
folder: 1
loop: true
- dfplayer.play_folder:
folder: !lambda "return id(dfplayer_test).is_playing() ? 2 : 1;"
file: !lambda "return id(dfplayer_test).is_playing() ? 4 : 3;"
loop: !lambda return !id(dfplayer_test).is_playing();
- dfplayer.set_device:
device: TF_CARD
- dfplayer.set_device: USB
- dfplayer.set_volume: 5
- dfplayer.set_volume: !lambda "return id(dfplayer_test).is_playing() ? 10 : 5;"
- dfplayer.set_eq: ROCK
- dfplayer.set_eq: !lambda "return id(dfplayer_test).is_playing() ? dfplayer::JAZZ : dfplayer::BASS;"
- dfplayer.play_next
- dfplayer.play_previous
- dfplayer.reset
@@ -25,8 +37,17 @@ esphome:
- dfplayer.volume_up
- dfplayer.volume_down
- dfplayer.sleep
- dfplayer.set_current_track_repeat
- dfplayer.set_current_track_repeat: true
- dfplayer.set_current_track_repeat:
enable: false
- dfplayer.set_current_track_repeat:
enable: !lambda return true;
- dfplayer.set_current_track_repeat:
id: dfplayer_test
dfplayer:
id: dfplayer_test
on_finished_playback:
then:
if:
@@ -0,0 +1,7 @@
button:
- platform: template
name: DS1307 Actions
on_press:
- ds1307.write_time:
id: ds1307_time
- ds1307.read_time: ds1307_time
@@ -0,0 +1,4 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
ds1307: !include common.yaml
actions: !include common-actions.yaml
@@ -0,0 +1,15 @@
button:
- platform: template
name: Duty Time Actions
on_press:
- if:
condition:
sensor.duty_time.is_running: pump_duty_time
then:
- sensor.duty_time.stop: pump_duty_time
- if:
condition:
sensor.duty_time.is_not_running: pump_duty_time
then:
- sensor.duty_time.start: pump_duty_time
- sensor.duty_time.reset: pump_duty_time
+1
View File
@@ -9,5 +9,6 @@ binary_sensor:
sensor:
- platform: duty_time
id: pump_duty_time
name: Duty Time
sensor: duty_time_bin1
@@ -0,0 +1,3 @@
packages:
duty_time: !include common.yaml
actions: !include common-actions.yaml
@@ -0,0 +1,7 @@
button:
- platform: template
name: Send lambda command
on_press:
- emontx.send_command:
id: test_emontx
command: !lambda return "l";
@@ -0,0 +1,4 @@
packages:
uart_115200: !include ../../test_build_components/common/uart_115200/esp8266-ard.yaml
emontx: !include common.yaml
actions: !include common-actions.yaml
+50
View File
@@ -2,6 +2,9 @@
#include <gtest/gtest.h>
#include <map>
#include <vector>
#include "esphome/components/spi/spi.h"
#include "esphome/core/hal.h"
@@ -47,4 +50,51 @@ class RecordingPin : public GPIOPin {
bool level{true};
};
/// SPI delegate that records what reaches the bus, filing each payload under the command it
/// followed; commands are the bytes written while D/C is low. Optionally burns wall-clock time on
/// each data row, so a transfer can be driven past its yield deadline.
class RecordingDelegate : public spi::SPIDelegate {
public:
explicit RecordingDelegate(const RecordingPin *dc, uint32_t row_transfer_ms = 0)
: dc_(dc), row_transfer_ms_(row_transfer_ms) {}
uint8_t transfer(uint8_t data) override {
this->record_(&data, 1);
return 0;
}
void write_array(const uint8_t *ptr, size_t length) override {
this->record_(ptr, length);
if (this->dc_->level && this->row_transfer_ms_ != 0) {
const uint32_t until = millis() + this->row_transfer_ms_;
while (millis() < until) {
}
}
}
void clear() {
this->commands.clear();
this->data.clear();
}
std::vector<uint8_t> commands;
std::map<uint8_t, std::vector<uint8_t>> data;
protected:
void record_(const uint8_t *ptr, size_t length) {
if (!this->dc_->level) {
for (size_t i = 0; i != length; i++) {
this->commands.push_back(ptr[i]);
this->last_command_ = ptr[i];
}
return;
}
auto &payload = this->data[this->last_command_];
payload.insert(payload.end(), ptr, ptr + length);
}
const RecordingPin *dc_;
uint32_t row_transfer_ms_;
uint8_t last_command_{0};
};
} // namespace esphome::epaper_spi::testing
@@ -0,0 +1,298 @@
#include <gtest/gtest.h>
#include <algorithm>
#include <vector>
#include "../common.h"
#include "esphome/components/epaper_spi/epaper_spi_ssd1677_gray4.h"
namespace esphome::epaper_spi::testing {
class TestableSSD1677Gray4 : public EPaperSSD1677Gray4 {
public:
TestableSSD1677Gray4(uint16_t width, uint16_t height) : EPaperSSD1677Gray4("test", width, height, nullptr, 0) {}
void install(spi::SPIDelegate *delegate) {
this->delegate_ = delegate;
this->set_dc_pin(&this->dc);
ASSERT_TRUE(this->init_buffer_(this->buffer_length_));
}
/// As configured with monochrome_partial_updates: full_update_every > 1.
void install_with_partials(spi::SPIDelegate *delegate) {
this->install(delegate);
this->set_full_update_every(5);
this->init_comparison_frame_();
ASSERT_TRUE(this->sent_.is_valid());
}
/// What the base class would decide; 0 means the next push is a full one.
void set_update_count(uint8_t count) { this->update_count_ = count; }
/// Pretend only this rectangle changed.
void set_dirty(uint16_t x_low, uint16_t y_low, uint16_t x_high, uint16_t y_high) {
this->x_low_ = x_low;
this->y_low_ = y_low;
this->x_high_ = x_high;
this->y_high_ = y_high;
}
/// Both planes of one push; returns how many calls it took.
int run_push() {
int calls = 1;
while (!this->transfer_data())
calls++;
return calls;
}
using EPaperSSD1677Gray4::refresh_screen;
using EPaperSSD1677Gray4::transfer_data;
RecordingPin dc;
};
using Bytes = std::vector<uint8_t>;
namespace {
/// A gray that lands squarely on each of the four levels.
Color color_for_level(uint8_t level) {
static const uint8_t GRAYS[4] = {0, 64, 128, 255};
const uint8_t v = GRAYS[level];
return Color(v, v, v);
}
void draw_row(TestableSSD1677Gray4 &display, int y, const std::vector<uint8_t> &levels) {
for (size_t x = 0; x != levels.size(); x++)
display.draw_pixel_at((int) x, y, color_for_level(levels[x]));
}
} // namespace
/// Each pixel's 2-bit level is split across the RAM planes: the high bit to 0x24, the low bit to
/// 0x26, both inverted because the four-level waveform reads 1 as white.
TEST(EPaperSSD1677Gray4, SplitsEachLevelAcrossBothPlanes) {
TestableSSD1677Gray4 display(8, 1);
RecordingDelegate bus(&display.dc);
display.install(&bus);
draw_row(display, 0, {0, 1, 2, 3, 0, 1, 2, 3});
display.run_push();
// levels 0 1 2 3 0 1 2 3
// high bit 0 0 1 1 0 0 1 1 = 0x33, inverted 0xCC
// low bit 0 1 0 1 0 1 0 1 = 0x55, inverted 0xAA
EXPECT_EQ(bus.data[0x24], (Bytes{0xCC}));
EXPECT_EQ(bus.data[0x26], (Bytes{0xAA}));
}
/// Two buffer bytes (4 pixels each) make one plane byte (8 pixels), leftmost pixel in the most
/// significant bit. An asymmetric row catches a swapped pair or reversed bit order.
TEST(EPaperSSD1677Gray4, PacksPixelsLeftmostFirstAcrossSourceBytes) {
TestableSSD1677Gray4 display(16, 1);
RecordingDelegate bus(&display.dc);
display.install(&bus);
draw_row(display, 0, {3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2});
display.run_push();
// high bits: pixel 0 (3) and pixel 15 (2) -> 0x80 0x01, inverted 0x7F 0xFE
// low bits: pixel 0 (3) only -> 0x80 0x00, inverted 0x7F 0xFF
EXPECT_EQ(bus.data[0x24], (Bytes{0x7F, 0xFE}));
EXPECT_EQ(bus.data[0x26], (Bytes{0x7F, 0xFF}));
}
/// The high-bit plane goes out first, each plane exactly once per push.
TEST(EPaperSSD1677Gray4, WritesTheHighBitPlaneBeforeTheLowBitPlane) {
TestableSSD1677Gray4 display(8, 1);
RecordingDelegate bus(&display.dc);
display.install(&bus);
display.run_push();
const auto &cmds = bus.commands;
ASSERT_EQ(std::count(cmds.begin(), cmds.end(), 0x24), 1);
ASSERT_EQ(std::count(cmds.begin(), cmds.end(), 0x26), 1);
EXPECT_LT(std::find(cmds.begin(), cmds.end(), 0x24) - cmds.begin(),
std::find(cmds.begin(), cmds.end(), 0x26) - cmds.begin());
}
/// A push that yields partway through must resume the right plane at the right row.
TEST(EPaperSSD1677Gray4, ResumesBothPlanesAfterYielding) {
TestableSSD1677Gray4 display(8, 4);
RecordingDelegate bus(&display.dc, 6); // two rows exceed MAX_TRANSFER_TIME
display.install(&bus);
draw_row(display, 0, {0, 0, 0, 0, 0, 0, 0, 0});
draw_row(display, 1, {1, 1, 1, 1, 1, 1, 1, 1});
draw_row(display, 2, {2, 2, 2, 2, 2, 2, 2, 2});
draw_row(display, 3, {3, 3, 3, 3, 3, 3, 3, 3});
const int calls = display.run_push();
EXPECT_GT(calls, 2) << "the transfer never yielded, so this test proves nothing";
// rows at levels 0..3: high bits 0 0 1 1, low bits 0 1 0 1, each inverted across the row
EXPECT_EQ(bus.data[0x24], (Bytes{0xFF, 0xFF, 0x00, 0x00}));
EXPECT_EQ(bus.data[0x26], (Bytes{0xFF, 0x00, 0xFF, 0x00}));
}
/// Without partial updates enabled (the default) every refresh is the four-level sequence, even if
/// the update count says otherwise.
TEST(EPaperSSD1677Gray4, WithoutPartialUpdatesEveryRefreshIsFourLevel) {
TestableSSD1677Gray4 display(8, 1);
RecordingDelegate bus(&display.dc);
display.install(&bus);
display.set_update_count(1);
display.refresh_screen(true);
EXPECT_EQ(bus.commands, (Bytes{0x1A, 0x22, 0x20}));
EXPECT_EQ(bus.data[0x1A], (Bytes{0x67, 0x00}));
EXPECT_EQ(bus.data[0x22], (Bytes{0xD7}));
}
// --- With monochrome partial updates ------------------------------------------------------------
/// A full update is still four-level. It also records, as the frame the next partial update
/// compares against, what the panel shows in black-and-white terms: the high bit of each level.
TEST(EPaperSSD1677Gray4, FullPushRecordsTheHighBitsForTheNextPartial) {
TestableSSD1677Gray4 display(8, 1);
RecordingDelegate bus(&display.dc);
display.install_with_partials(&bus);
draw_row(display, 0, {0, 1, 2, 3, 0, 1, 2, 3});
display.set_update_count(0);
display.run_push();
EXPECT_EQ(bus.data[0x24], (Bytes{0xCC})) << "full update is no longer the four-level split";
EXPECT_EQ(bus.data[0x26], (Bytes{0xAA}));
bus.clear();
// Nothing changed: old and new planes must match, or the partial drives every pixel.
display.set_update_count(1);
display.run_push();
EXPECT_EQ(bus.data[0x26], (Bytes{0x33})) << "comparison frame is not the high bits";
EXPECT_EQ(bus.data[0x24], (Bytes{0x33}));
}
/// A partial update sends the comparison frame to 0x26 and the new frame's high bits to 0x24,
/// not inverted (it runs the black-and-white waveform), over the whole panel.
TEST(EPaperSSD1677Gray4, PartialPushSendsTheHighBitsInBlackAndWhite) {
TestableSSD1677Gray4 display(16, 2);
RecordingDelegate bus(&display.dc);
display.install_with_partials(&bus);
draw_row(display, 0, {0, 1, 2, 3, 0, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3});
draw_row(display, 1, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0});
display.set_update_count(0);
display.run_push();
bus.clear();
draw_row(display, 1, {3, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0});
display.set_dirty(0, 1, 8, 2); // only the start of the second row changed
display.set_update_count(1);
display.run_push();
EXPECT_EQ(bus.data[0x26], (Bytes{0x33, 0xFF, 0x00, 0x00})) << "old plane is not the frame on the panel";
EXPECT_EQ(bus.data[0x24], (Bytes{0x33, 0xFF, 0xF0, 0x00})) << "new plane is not the whole frame's high bits";
}
/// The new plane of a partial update is built a row at a time; a push that yields partway through
/// must resume at the right row.
TEST(EPaperSSD1677Gray4, PartialPushResumesAfterYielding) {
TestableSSD1677Gray4 display(8, 4);
RecordingDelegate bus(&display.dc, 6); // two rows exceed MAX_TRANSFER_TIME
display.install_with_partials(&bus);
display.set_update_count(0);
display.run_push();
bus.clear();
// The buffer starts white; darken all of row 0 and the right half of row 2
draw_row(display, 0, {0, 0, 0, 0, 0, 0, 0, 0});
draw_row(display, 2, {3, 3, 3, 3, 0, 0, 0, 0});
display.set_update_count(1);
const int calls = display.run_push();
EXPECT_GT(calls, 2) << "the transfer never yielded, so this test proves nothing";
EXPECT_EQ(bus.data[0x26], (Bytes{0xFF, 0xFF, 0xFF, 0xFF}));
EXPECT_EQ(bus.data[0x24], (Bytes{0x00, 0xFF, 0xF0, 0xFF}));
}
/// Regression test: a full update requested while a partial one is being sent must not switch the
/// push to the four-level transfer halfway, which misread the partial's progress and never finished.
TEST(EPaperSSD1677Gray4, FullUpdateRequestDuringAPartialPushWaitsForTheNextUpdate) {
TestableSSD1677Gray4 display(8, 4);
RecordingDelegate bus(&display.dc, 6); // two rows exceed MAX_TRANSFER_TIME
display.install_with_partials(&bus);
display.set_update_count(0);
display.run_push();
bus.clear();
draw_row(display, 0, {0, 0, 0, 0, 0, 0, 0, 0});
display.set_update_count(1);
ASSERT_FALSE(display.transfer_data());
display.request_full_update();
int calls = 1;
while (!display.transfer_data())
ASSERT_LT(++calls, 20) << "partial push never finished";
EXPECT_EQ(bus.data[0x26], (Bytes{0xFF, 0xFF, 0xFF, 0xFF}));
EXPECT_EQ(bus.data[0x24], (Bytes{0x00, 0xFF, 0xFF, 0xFF}));
bus.clear();
display.refresh_screen(true);
EXPECT_EQ(bus.data[0x22], (Bytes{0xFF})) << "refresh does not match the partial data sent";
}
/// The four-level refresh follows a reset, which loses controller RAM, so it must send the whole
/// panel even when partial updates are enabled but the comparison frame could not be allocated.
TEST(EPaperSSD1677Gray4, FourLevelPushCoversTheWholePanelWithoutAComparisonFrame) {
TestableSSD1677Gray4 display(16, 2);
RecordingDelegate bus(&display.dc);
display.install(&bus);
display.set_full_update_every(5); // partial updates on, but no comparison frame
display.set_dirty(8, 1, 16, 2);
display.set_update_count(0);
display.run_push();
EXPECT_EQ(bus.data[0x24].size(), 4u) << "four-level update did not send the whole new plane";
EXPECT_EQ(bus.data[0x26].size(), 4u) << "four-level update did not send the whole old plane";
}
/// A full update resets the controller, which does not keep RAM, so even when only part of the
/// frame changed it must send the whole panel.
TEST(EPaperSSD1677Gray4, FullPushWithPartialsEnabledCoversTheWholePanel) {
TestableSSD1677Gray4 display(16, 2);
RecordingDelegate bus(&display.dc);
display.install_with_partials(&bus);
display.set_dirty(8, 1, 16, 2);
display.set_update_count(0);
display.run_push();
EXPECT_EQ(bus.data[0x24].size(), 4u) << "full update did not send the whole new plane";
EXPECT_EQ(bus.data[0x26].size(), 4u) << "full update did not send the whole old plane";
}
/// The refresh matches what was sent: black-and-white for a partial update, four-level for a full.
TEST(EPaperSSD1677Gray4, PartialRefreshIsBlackAndWhiteAndFullIsFourLevel) {
TestableSSD1677Gray4 display(8, 1);
RecordingDelegate bus(&display.dc);
display.install_with_partials(&bus);
display.set_update_count(1);
display.refresh_screen(true);
EXPECT_EQ(bus.commands, (Bytes{0x3C, 0x22, 0x20}));
EXPECT_EQ(bus.data[0x22], (Bytes{0xFF})) << "partial update did not use the black-and-white waveform";
// The model's border setting is right for the four-level waveform only; under this one it
// would drive the border black on every partial.
EXPECT_EQ(bus.data[0x3C], (Bytes{0x01})) << "partial update did not switch the border to LUT1";
bus.clear();
display.set_update_count(0);
display.refresh_screen(false);
EXPECT_EQ(bus.data[0x22], (Bytes{0xD7})) << "full update did not use the four-level waveform";
EXPECT_EQ(bus.data.count(0x3C), 0u) << "full update overrode the model's border setting";
}
} // namespace esphome::epaper_spi::testing
@@ -0,0 +1,233 @@
#include <gtest/gtest.h>
#include <initializer_list>
#include <vector>
#include "../common.h"
#include "esphome/components/epaper_spi/epaper_spi_ssd1677.h"
namespace esphome::epaper_spi::testing {
class TestableSSD1677 : public EPaperSSD1677 {
public:
TestableSSD1677(uint16_t width, uint16_t height) : EPaperSSD1677("test", width, height, nullptr, 0) {}
void install(spi::SPIDelegate *delegate, uint8_t full_update_every) {
this->delegate_ = delegate;
this->set_dc_pin(&this->dc);
this->set_reset_pin(&this->reset_pin);
ASSERT_TRUE(this->init_buffer_(this->buffer_length_));
this->set_full_update_every(full_update_every);
this->init_comparison_frame_();
}
bool has_comparison_frame() const { return this->sent_.is_valid(); }
void set_frame(std::initializer_list<uint8_t> bytes) {
size_t i = 0;
for (const uint8_t byte : bytes)
this->buffer_[i++] = byte;
}
/// Fill the frame with a byte pattern that differs per seed; returns it.
std::vector<uint8_t> set_pattern(uint8_t seed) {
std::vector<uint8_t> frame;
for (size_t i = 0; i != this->buffer_length_; i++) {
frame.push_back((uint8_t) (seed + i * 7));
this->buffer_[i] = frame.back();
}
return frame;
}
/// What the base class would decide; 0 means the next push is a full one.
void set_update_count(uint8_t count) { this->update_count_ = count; }
/// Pretend only this rectangle changed.
void set_dirty(uint16_t x_low, uint16_t y_low, uint16_t x_high, uint16_t y_high) {
this->x_low_ = x_low;
this->y_low_ = y_low;
this->x_high_ = x_high;
this->y_high_ = y_high;
}
/// One call into the transfer; false while there is more to send.
bool step() { return this->transfer_data(); }
/// Both planes of one push; returns how many calls it took.
int run_push() {
int calls = 1;
while (!this->transfer_data())
calls++;
return calls;
}
/// Run the UPDATE state, with nothing drawn, and report whether a push follows.
bool run_update_state() {
this->set_auto_clear(false);
this->set_dirty(this->width_, this->height_, 0, 0);
this->state_ = EPaperState::UPDATE;
this->process_state_();
return this->state_ == EPaperState::RESET;
}
uint8_t update_count() const { return this->update_count_; }
bool reset_in(EPaperState state) {
this->state_ = state;
return this->reset();
}
RecordingPin dc;
RecordingPin reset_pin;
};
using Bytes = std::vector<uint8_t>;
/// A full push ignores the old-image plane, and on the first push after boot the comparison frame
/// holds nothing real yet, so the new frame goes to both planes.
TEST(EPaperSSD1677, FullPushSendsTheNewFrameToBothPlanes) {
TestableSSD1677 display(16, 2);
RecordingDelegate bus(&display.dc);
display.install(&bus, 5);
display.set_frame({0x0F, 0xF0, 0x3C, 0xC3});
display.set_update_count(0);
display.run_push();
EXPECT_EQ(bus.data[0x26], (Bytes{0x0F, 0xF0, 0x3C, 0xC3}));
EXPECT_EQ(bus.data[0x24], (Bytes{0x0F, 0xF0, 0x3C, 0xC3}));
}
/// Regression test.
///
/// A partial refresh drives every pixel from the pair (0x26 = the image on the panel, 0x24 = the
/// new image), across the whole panel whatever RAM window was written. The controller does not
/// keep its RAM intact between updates, so sending only the changed window of 0x24 - and 0x26 once
/// - leaves the pair wrong outside that window: unchanged pixels get driven on every partial and
/// wash out. Both planes must go out whole, 0x26 holding the frame actually on the panel.
TEST(EPaperSSD1677, PartialPushComparesAgainstTheFrameOnThePanel) {
TestableSSD1677 display(16, 2);
RecordingDelegate bus(&display.dc);
display.install(&bus, 5);
display.set_frame({0x0F, 0xF0, 0x3C, 0xC3});
display.set_update_count(0);
display.run_push();
bus.clear();
display.set_frame({0x0F, 0xF0, 0x3C, 0x00});
display.set_dirty(8, 1, 16, 2); // only the last byte changed
display.set_update_count(1);
display.run_push();
EXPECT_EQ(bus.data[0x26], (Bytes{0x0F, 0xF0, 0x3C, 0xC3})) << "old plane is not the frame on the panel";
EXPECT_EQ(bus.data[0x24], (Bytes{0x0F, 0xF0, 0x3C, 0x00})) << "new plane is not the whole new frame";
// The RAM window, set once per plane, must span the panel too, not the changed rectangle.
EXPECT_EQ(bus.data[0x44], (Bytes{0, 0, 15, 0, 0, 0, 15, 0})) << "x window is not the whole panel";
EXPECT_EQ(bus.data[0x45], (Bytes{0, 0, 1, 0, 0, 0, 1, 0})) << "y window is not the whole panel";
}
/// The comparison frame must record the bytes that went to 0x24, not whatever the buffer holds
/// later: LVGL can draw into the buffer while a push is in progress. Here the buffer changes
/// between the two planes of a push; the next push must compare against what was actually sent.
TEST(EPaperSSD1677, ComparisonFrameIsWhatWasSentNotTheBuffer) {
TestableSSD1677 display(16, 2);
RecordingDelegate bus(&display.dc);
display.install(&bus, 5);
display.set_frame({0x11, 0x11, 0x11, 0x11});
display.set_update_count(0);
display.run_push();
display.set_frame({0x22, 0x22, 0x22, 0x22});
display.set_update_count(1);
ASSERT_FALSE(display.step()) << "expected the old plane to go out on its own first";
display.set_frame({0x33, 0x33, 0x33, 0x33}); // drawn mid-push, before the new plane
while (!display.step()) {
}
ASSERT_EQ(bus.data[0x24].size(), 8u);
EXPECT_EQ(Bytes(bus.data[0x24].begin() + 4, bus.data[0x24].end()), (Bytes{0x33, 0x33, 0x33, 0x33}));
bus.clear();
display.set_frame({0x44, 0x44, 0x44, 0x44});
display.set_update_count(2);
display.run_push();
EXPECT_EQ(bus.data[0x26], (Bytes{0x33, 0x33, 0x33, 0x33})) << "old plane is not what was last sent";
}
/// Two full planes can take several loop iterations to send; each resumed call must continue the
/// right plane at the right byte. Planes go out in runs sized to the time slice, not row by row.
TEST(EPaperSSD1677, ResumesTheRightPlaneAfterYielding) {
// 400x100 is 5000 bytes per plane: two runs at the default 2 MHz bus
TestableSSD1677 display(400, 100);
RecordingDelegate bus(&display.dc, MAX_TRANSFER_TIME + 1); // every run overruns the time slice
display.install(&bus, 5);
const auto old_frame = display.set_pattern(1);
display.set_update_count(0);
display.run_push();
bus.clear();
const auto new_frame = display.set_pattern(2);
display.set_update_count(1);
const int calls = display.run_push();
EXPECT_EQ(calls, 4) << "expected two runs per plane, one per call";
EXPECT_EQ(bus.data[0x26], old_frame);
EXPECT_EQ(bus.data[0x24], new_frame);
}
/// A requested full update takes effect when the next update starts, and pushes the whole panel
/// even if nothing was drawn.
TEST(EPaperSSD1677, RequestedFullUpdateAppliesWhenTheNextUpdateStarts) {
TestableSSD1677 display(16, 2);
RecordingDelegate bus(&display.dc);
display.install(&bus, 5);
display.set_update_count(3);
EXPECT_FALSE(display.run_update_state()) << "an update with nothing drawn should not push";
display.request_full_update();
EXPECT_EQ(display.update_count(), 3) << "request changed the update in progress";
EXPECT_TRUE(display.run_update_state()) << "requested full update did not push";
EXPECT_EQ(display.update_count(), 0) << "requested update is not a full one";
display.set_update_count(3);
EXPECT_FALSE(display.run_update_state()) << "request was applied more than once";
}
/// Nothing a partial needs lives in controller RAM any more, so a partial push skips the reset
/// altogether; a full one still gets the hardware pulse and the software reset.
TEST(EPaperSSD1677, PartialPushSkipsTheResetAndAFullPushKeepsIt) {
TestableSSD1677 display(16, 2);
RecordingDelegate bus(&display.dc);
display.install(&bus, 5);
display.set_update_count(1);
EXPECT_TRUE(display.reset_in(EPaperState::RESET)) << "partial push waited on a reset";
EXPECT_TRUE(display.reset_pin.level) << "partial push pulsed the reset pin";
EXPECT_TRUE(bus.commands.empty()) << "partial push sent a software reset";
display.set_update_count(0);
EXPECT_FALSE(display.reset_in(EPaperState::RESET));
EXPECT_FALSE(display.reset_pin.level) << "full push did not pulse the reset pin";
EXPECT_TRUE(display.reset_in(EPaperState::RESET_END));
EXPECT_TRUE(display.reset_pin.level);
EXPECT_EQ(bus.commands, (Bytes{0x12})) << "full push did not send a software reset";
}
/// With every update a full one nothing is ever compared against 0x26, so no comparison frame is
/// allocated and the transfer is EPaperMono's.
TEST(EPaperSSD1677, NoComparisonFrameWhenEveryUpdateIsFull) {
TestableSSD1677 display(16, 2);
RecordingDelegate bus(&display.dc);
display.install(&bus, 1);
EXPECT_FALSE(display.has_comparison_frame());
display.set_frame({0x0F, 0xF0, 0x3C, 0xC3});
display.set_update_count(0);
display.run_push();
EXPECT_EQ(bus.data[0x24], (Bytes{0x0F, 0xF0, 0x3C, 0xC3}));
}
} // namespace esphome::epaper_spi::testing
@@ -1,6 +1,14 @@
packages:
spi: !include ../../test_build_components/common/spi/esp32-s3-idf.yaml
psram:
mode: octal
esphome:
on_boot:
then:
- epaper_spi.full_update_next: epaper_partial
display:
- platform: epaper_spi
spi_id: spi_bus
@@ -85,12 +93,24 @@ display:
busy_pin: 37
enable_pin: 39
- platform: epaper_spi
id: epaper_partial
model: seeed-ee04-mono-4.26
full_update_every: 10
# Override pins to avoid conflict with other display configs
busy_pin: 43
dc_pin: 42
# Seeed reTerminal Sticky, four-level grayscale (800x480, SSD1677)
# dc_pin/reset_pin overridden to avoid conflict with other display configs
# full_update_every is not supported by this model, so left at its default of 1
- platform: epaper_spi
model: seeed-reterminal-sticky-gray4
dc_pin: 45
reset_pin: 9
lambda: |-
it.filled_rectangle(0, 0, it.get_width(), it.get_height(), Color(170, 170, 170));
it.circle(it.get_width() / 2, it.get_height() / 2, 100, Color::BLACK);
# WeAct 2.13" 3-color e-paper (122x250, SSD1680)
- platform: epaper_spi
spi_id: spi_bus
@@ -0,0 +1,12 @@
# No wifi, espnow or BLE: nothing else re-includes esp_wifi for the ESP-NOW shim.
esp32_hosted:
variant: ESP32C6
slot: 1
active_high: true
reset_pin: GPIO15
cmd_pin: GPIO13
clk_pin: GPIO12
d0_pin: GPIO11
d1_pin: GPIO10
d2_pin: GPIO9
d3_pin: GPIO8
+10
View File
@@ -1,3 +1,13 @@
esphome:
on_boot:
then:
- output.esp8266_pwm.set_frequency:
id: out
frequency: 100Hz
- output.esp8266_pwm.set_frequency:
id: out2
frequency: !lambda return 200.0f;
output:
- platform: esp8266_pwm
id: out
+12
View File
@@ -0,0 +1,12 @@
from esphome.loader import FileResource
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
# to_code emits the component count the application needs
manifest.enable_codegen()
# Only the decoder is under test; its ota platform is not in this build
manifest.resources = manifest.resources + [
FileResource("esphome.components.esphome", "ota/ota_esphome_inflate.c"),
FileResource("esphome.components.esphome", "ota/ota_esphome_inflate.h"),
]
@@ -0,0 +1,324 @@
#include <gtest/gtest.h>
#include <cstdint>
#include <cstring>
#include <vector>
#include "esphome/components/esphome/ota/ota_esphome_inflate.h"
namespace esphome::testing {
// build_plain() compressed with the CLI's window (espota2.DEFLATE_WINDOW_BITS):
// DEFLATED = zlib.compress(plain, 9, wbits=-12)
// STORED = zlib.compress(plain[:300], 0, wbits=-12)
static const uint8_t DEFLATED[] = {
0xed, 0xc8, 0xf7, 0x3f, 0xd4, 0x0f, 0x03, 0x00, 0x70, 0x67, 0xaf, 0x4b, 0x67, 0x66, 0x9f, 0x90, 0x91, 0x11, 0xc2,
0x11, 0x91, 0xb8, 0xb3, 0xf7, 0x3a, 0xd9, 0x5f, 0x4e, 0x99, 0x67, 0x1e, 0xce, 0x8a, 0xac, 0xec, 0x59, 0xb8, 0xc2,
0x95, 0x5d, 0x56, 0x42, 0x67, 0x73, 0x46, 0xf6, 0xca, 0xce, 0xc8, 0xc8, 0xc8, 0x91, 0x8a, 0x7c, 0x2f, 0x7a, 0xfe,
0x86, 0xe7, 0x87, 0xe7, 0x87, 0xe7, 0xf5, 0xfa, 0xbc, 0x7f, 0x7c, 0xbb, 0x07, 0xa2, 0x1f, 0xfa, 0xf9, 0xb8, 0x43,
0xfd, 0x82, 0x5c, 0xa0, 0x6e, 0xee, 0x28, 0x6f, 0x97, 0x20, 0x77, 0xa8, 0x3b, 0x70, 0xc0, 0x01, 0x07, 0x1c, 0x70,
0xc0, 0x01, 0x07, 0x1c, 0x70, 0xc0, 0x01, 0x07, 0x1c, 0x70, 0xc0, 0x01, 0x07, 0x1c, 0x70, 0xc0, 0x01, 0x07, 0x1c,
0x70, 0xc0, 0x01, 0xf7, 0x5f, 0xdd, 0x40, 0xd4, 0x63, 0x2f, 0x03, 0xf2, 0x17, 0xb2, 0xe5, 0x69, 0xc7, 0x5a, 0x04,
0xdf, 0x46, 0x22, 0xa8, 0x4b, 0x6e, 0xd5, 0x3a, 0x80, 0x05, 0xe1, 0x81, 0x4a, 0x44, 0x56, 0x27, 0xa9, 0x21, 0xf7,
0xad, 0xd9, 0xb0, 0xd3, 0xf1, 0xa5, 0x0b, 0x6a, 0x96, 0x56, 0xb2, 0xca, 0xb5, 0xee, 0x0f, 0x96, 0x28, 0xc3, 0x9e,
0x8f, 0x6e, 0xf2, 0x84, 0xa4, 0x3b, 0x2d, 0x16, 0x64, 0x08, 0x6a, 0x28, 0x91, 0xee, 0x87, 0x1a, 0x81, 0xff, 0xed,
0x2c, 0x5f, 0xda, 0x4a, 0x48, 0x5f, 0x91, 0xf0, 0x31, 0xfe, 0x6b, 0xbd, 0x81, 0x86, 0x92, 0x33, 0x1a, 0x6c, 0x69,
0x32, 0xfb, 0x19, 0x56, 0x2c, 0xc6, 0xaa, 0xef, 0xe8, 0x16, 0x98, 0xcf, 0x98, 0xbf, 0xa0, 0x2d, 0xde, 0x7f, 0xdf,
0x4b, 0xcb, 0xf6, 0x5c, 0xaf, 0x11, 0x24, 0xf0, 0x46, 0x3e, 0x49, 0x0e, 0x67, 0x0e, 0xcf, 0xf3, 0x57, 0xca, 0xb0,
0x39, 0x55, 0xe1, 0xe7, 0xfc, 0x37, 0x29, 0xe8, 0xd7, 0xf3, 0x08, 0x2e, 0xfb, 0x7b, 0x6d, 0x3e, 0xfe, 0xe9, 0x2c,
0x68, 0xe4, 0x20, 0x88, 0x57, 0x56, 0x41, 0x3d, 0xab, 0x9b, 0xbf, 0x0c, 0x0c, 0xae, 0x51, 0x48, 0x8b, 0x72, 0xcc,
0xb8, 0xbb, 0xf3, 0x30, 0xa8, 0x5c, 0xb5, 0xa1, 0x2f, 0x07, 0x52, 0xe9, 0x36, 0xb8, 0x3c, 0xbc, 0xee, 0xbc, 0xf1,
0xa3, 0x8b, 0x81, 0xc2, 0x03, 0xbf, 0x29, 0x5a, 0x22, 0x24, 0x97, 0xf8, 0xc9, 0xb5, 0xbf, 0xd2, 0x24, 0x4a, 0x86,
0xc1, 0x2b, 0xb7, 0xb8, 0xde, 0xa7, 0xea, 0xa5, 0x1d, 0x13, 0x1c, 0x1d, 0xd3, 0x3c, 0x81, 0x60, 0x2e, 0x2f, 0x93,
0x5c, 0x78, 0x43, 0x2e, 0x39, 0x68, 0x09, 0x13, 0x09, 0x10, 0xce, 0xd6, 0x8d, 0xe9, 0x2f, 0xaf, 0x88, 0x6f, 0x99,
0x4f, 0xcd, 0xdc, 0xaa, 0xf9, 0x47, 0xdb, 0x1c, 0xb5, 0x89, 0x53, 0x10, 0x3d, 0x77, 0xac, 0x26, 0x04, 0x3a, 0x3b,
0x18, 0xf8, 0x47, 0x75, 0x56, 0xa5, 0xda, 0x70, 0xfb, 0xf7, 0x0d, 0x3b, 0x6e, 0x4b, 0x2a, 0xbc, 0x49, 0x32, 0x43,
0x95, 0x62, 0x83, 0x3d, 0xdc, 0x0a, 0x1f, 0x1d, 0xf9, 0x59, 0x6b, 0x95, 0xf0, 0x9b, 0xf5, 0x53, 0x9e, 0xb5, 0x65,
0xeb, 0x74, 0xfa, 0x81, 0x9b, 0x61, 0xa3, 0x57, 0x2f, 0xda, 0x2c, 0xcd, 0xf8, 0xb0, 0x74, 0xb9, 0x62, 0x39, 0x91,
0xd9, 0x7d, 0xb3, 0x03, 0x65, 0x2e, 0x66, 0x20, 0x18, 0xac, 0xa2, 0xeb, 0x3b, 0x35, 0x98, 0xa3, 0x28, 0x46, 0x30,
0xee, 0xd3, 0xeb, 0x47, 0x49, 0x11, 0xc5, 0xcd, 0xa0, 0xa5, 0x1c, 0x2e, 0x9d, 0x14, 0xd6, 0x46, 0x4a, 0x05, 0x7b,
0xd3, 0xb9, 0x0d, 0xeb, 0xd7, 0x6f, 0xb5, 0xf4, 0xed, 0x3f, 0x27, 0xb8, 0xec, 0x51, 0xb1, 0x6e, 0xb0, 0x14, 0xed,
0xdf, 0x90, 0x72, 0x59, 0x71, 0xd5, 0xf5, 0xf2, 0x61, 0x5f, 0xa7, 0x8a, 0xd7, 0x0f, 0x80, 0x2f, 0xe5, 0x0f, 0x45,
0xf2, 0x4d, 0xd1, 0xdc, 0xdd, 0xce, 0x46, 0xdf, 0x77, 0xf2, 0xa6, 0xa2, 0x79, 0x77, 0xf0, 0x0e, 0xaa, 0x68, 0x14,
0x85, 0x32, 0x05, 0x29, 0x75, 0x2b, 0x6c, 0xb0, 0x7c, 0x84, 0xc9, 0xec, 0x49, 0x70, 0x9b, 0x38, 0x36, 0x4c, 0xe9,
0xa5, 0xdc, 0xb1, 0xb8, 0x28, 0xd6, 0x20, 0x89, 0x8a, 0x2a, 0x62, 0xc1, 0x90, 0x3a, 0x43, 0x48, 0xaa, 0xfc, 0xec,
0x52, 0xf4, 0x02, 0xa7, 0xcd, 0x46, 0xf9, 0x78, 0x64, 0x4f, 0xad, 0x6b, 0x17, 0xdb, 0x56, 0xa2, 0xa1, 0x6a, 0xcf,
0x3b, 0x66, 0x64, 0x01, 0xc2, 0xd6, 0xae, 0x23, 0x34, 0x82, 0x60, 0x0a, 0x3a, 0xe4, 0xdd, 0x7f, 0xaa, 0x97, 0x44,
0x9a, 0x63, 0x8f, 0xaf, 0xa9, 0x97, 0x5a, 0x55, 0xcc, 0x81, 0x48, 0x83, 0xf8, 0xe6, 0xc5, 0xef, 0xdf, 0xfa, 0x5a,
0x4f, 0x7f, 0x18, 0x56, 0xc0, 0x66, 0x36, 0xad, 0x8a, 0x79, 0xab, 0xde, 0xf4, 0x7b, 0x70, 0x98, 0xea, 0xfa, 0xf4,
0xc7, 0x01, 0x31, 0x16, 0xea, 0xf1, 0x70, 0x8d, 0xee, 0x87, 0x52, 0x13, 0x01, 0x9e, 0x0c, 0xbd, 0x5a, 0x14, 0x01,
0x8e, 0xf7, 0x6e, 0xff, 0xce, 0x7f, 0xa4, 0xd1, 0xa1, 0x6a, 0x9b, 0x95, 0xb2, 0x31, 0x8a, 0x6f, 0xc8, 0xfe, 0x8c,
0x29, 0x55, 0xbc, 0xfc, 0x61, 0xd0, 0xde, 0xb0, 0xa9, 0x0c, 0x01, 0x0f, 0xba, 0x77, 0x7e, 0x77, 0xd9, 0x76, 0xa4,
0xfd, 0xab, 0x38, 0x18, 0x92, 0xec, 0xf0, 0xd3, 0x57, 0x7f, 0xf4, 0xbd, 0x65, 0xd4, 0x77, 0x8e, 0xa1, 0x92, 0x82,
0x0c, 0x7b, 0x34, 0xd3, 0x11, 0xfb, 0xc0, 0x36, 0x23, 0x8e, 0xbb, 0x89, 0xdf, 0x70, 0xdc, 0x9a, 0x76, 0x45, 0x0b,
0x22, 0xfa, 0x35, 0xdf, 0x8f, 0x45, 0x48, 0x7d, 0xed, 0xdc, 0x06, 0x01, 0x5e, 0xbb, 0xe9, 0xf9, 0x07, 0x93, 0xf6,
0x69, 0x6f, 0xf1, 0xfc, 0xfd, 0xfa, 0x41, 0xc0, 0x13, 0x9e, 0x74, 0x2d, 0x76, 0xfe, 0xa7, 0xbe, 0x4e, 0xd9, 0xaa,
0x64, 0xa6, 0xed, 0xd3, 0xad, 0xee, 0x62, 0x9d, 0x39, 0xc9, 0xb8, 0xae, 0x86, 0x31, 0x4f, 0x62, 0x57, 0xea, 0xea,
0x5a, 0xe3, 0x59, 0xd8, 0x99, 0xb3, 0xcd, 0x9f, 0x3b, 0xea, 0x58, 0x99, 0x11, 0xdc, 0x3d, 0xac, 0x58, 0xd9, 0xa6,
0xae, 0x2d, 0x07, 0x1d, 0xd7, 0xfa, 0x87, 0x78, 0xa7, 0x7f, 0x2a, 0x4f, 0x25, 0x58, 0xef, 0x53, 0x78, 0x2e, 0x93,
0xdc, 0x44, 0xcc, 0x53, 0x88, 0x77, 0x1c, 0xda, 0x14, 0xaf, 0xe1, 0x67, 0x92, 0xff, 0x36, 0x96, 0x20, 0x6f, 0x9d,
0x2c, 0x7f, 0xea, 0x31, 0xf2, 0x34, 0x50, 0xc2, 0x39, 0x84, 0xee, 0x4c, 0xbe, 0xca, 0x06, 0x6f, 0x67, 0x42, 0xea,
0x13, 0x58, 0xee, 0xdd, 0x8e, 0x29, 0x4f, 0xee, 0xd8, 0x93, 0x0d, 0x45, 0x80, 0x4d, 0xf3, 0x12, 0x79, 0xbb, 0x36,
0xa3, 0x73, 0x95, 0x95, 0xb6, 0xfc, 0x54, 0x60, 0xb2, 0xcc, 0x71, 0xaa, 0xf1, 0x6b, 0x66, 0xed, 0xba, 0x8b, 0xd6,
0x6d, 0x61, 0x79, 0x61, 0x1d, 0xb3, 0xba, 0xa6, 0x2f, 0xaa, 0xdc, 0x1d, 0xb8, 0x22, 0xd8, 0x98, 0x58, 0xed, 0x4d,
0x3c, 0xea, 0xa9, 0x37, 0x5e, 0x5e, 0x7b, 0x47, 0xa1, 0x7a, 0x39, 0x42, 0xe4, 0x3b, 0xbb, 0x69, 0x0a, 0x8b, 0x32,
0x6e, 0x63, 0xeb, 0x87, 0xf6, 0x5d, 0xaa, 0xbf, 0xbe, 0xc5, 0xb2, 0x85, 0x60, 0xdc, 0x32, 0x07, 0x85, 0x73, 0x3d,
0x96, 0x8b, 0x89, 0x71, 0x52, 0xb4, 0x93, 0xe6, 0x18, 0xad, 0xbf, 0xce, 0x21, 0x1d, 0x33, 0xb5, 0xb3, 0x35, 0x6a,
0x5b, 0xe7, 0x47, 0x13, 0x19, 0x8f, 0x53, 0xf4, 0x0c, 0x2a, 0x39, 0x16, 0x37, 0x39, 0x3b, 0x5f, 0x81, 0x51, 0xbc,
0x23, 0x92, 0x2c, 0x8d, 0xbf, 0x2f, 0xee, 0xbf, 0xed, 0x9d, 0x3f, 0xe0, 0x16, 0x21, 0x5a, 0x57, 0xa6, 0x8c, 0x58,
0x7a, 0xd5, 0xa1, 0x6d, 0xed, 0xe8, 0x90, 0x97, 0x14, 0xb4, 0x6b, 0xa5, 0x3b, 0xd7, 0x90, 0x84, 0x9e, 0x07, 0xc2,
0x7f, 0x1e, 0x08, 0xa0, 0x6f, 0x69, 0xf1, 0xcc, 0x4e, 0xca, 0x07, 0x64, 0xa2, 0xb4, 0xbc, 0x5f, 0xe0, 0xa7, 0x0e,
0x31, 0x77, 0x6f, 0x35, 0xd7, 0x66, 0x83, 0x5d, 0x64, 0x4e, 0xf1, 0x3c, 0x7a, 0xcb, 0xa5, 0xfc, 0xc9, 0x95, 0xab,
0x27, 0x30, 0x6b, 0x82, 0x57, 0xcd, 0xb0, 0x85, 0x9d, 0xc5, 0xa9, 0x0f, 0xdb, 0xe3, 0x5a, 0xc1, 0xb2, 0x3f, 0xfa,
0xea, 0xf3, 0x3a, 0xa0, 0xe4, 0xad, 0xd7, 0x14, 0xcf, 0x4f, 0xc9, 0x46, 0x09, 0x6e, 0x70, 0x27, 0xe4, 0x9b, 0x5f,
0x61, 0x4a, 0xb2, 0xfb, 0xc3, 0xf0, 0x3a, 0x53, 0x38, 0x2a, 0x59, 0x48, 0x2b, 0xab, 0x21, 0x64, 0x1a, 0x1c, 0x90,
0xb6, 0x69, 0xbb, 0x89, 0xe3, 0x9a, 0x12, 0xb7, 0xfd, 0x47, 0xcc, 0x57, 0xdb, 0x2b, 0xf6, 0x35, 0xe1, 0x79, 0xe0,
0xbf, 0x6d, 0x7f, 0x71, 0x3c, 0x21, 0x27, 0x82, 0x12, 0x05, 0xae, 0x43, 0x11, 0x0f, 0xc8, 0xe7, 0x3c, 0xf0, 0x0b,
0xe4, 0x69, 0xff, 0xb2, 0xda, 0x9e, 0x7f, 0xfe, 0xa4, 0x3f, 0xdf, 0x02, 0x58, 0xaa, 0x60, 0xd2, 0x8e, 0xd5, 0x6c,
0x92, 0x22, 0x38, 0xb1, 0xd2, 0x84, 0x80, 0xea, 0xce, 0xfe, 0x34, 0x79, 0xd6, 0x94, 0xb0, 0xad, 0x51, 0x67, 0x65,
0xf5, 0xab, 0xd5, 0x18, 0x05, 0x92, 0x8a, 0x24, 0x36, 0xc2, 0xc4, 0x99, 0xad, 0x0d, 0x26, 0xb8, 0x70, 0xea, 0x5b,
0x74, 0x35, 0x6a, 0xfa, 0xac, 0x97, 0xd8, 0x4c, 0x08, 0x6d, 0xc1, 0xfe, 0x60, 0xe4, 0xd4, 0x8f, 0x93, 0xd4, 0xaf,
0xa2, 0xec, 0xb8, 0x71, 0xf6, 0xcb, 0x3f, 0xdc, 0x3e, 0x42, 0xca, 0xe2, 0x8d, 0xb9, 0x0a, 0x2c, 0xef, 0xb6, 0x18,
0x75, 0x46, 0x8a, 0x2b, 0x21, 0xeb, 0xc5, 0x12, 0x4d, 0xd7, 0xb8, 0x8e, 0x6c, 0xec, 0x9d, 0x12, 0xde, 0xa5, 0x45,
0xab, 0xc4, 0x26, 0x47, 0xfa, 0x57, 0xad, 0x78, 0x4c, 0x5e, 0x87, 0x0b, 0x42, 0x6a, 0xbe, 0xef, 0xbd, 0xbc, 0xe5,
0xd7, 0x4c, 0x3a, 0x0f, 0x96, 0x29, 0xcb, 0x45, 0x46, 0x3f, 0x8a, 0x31, 0xda, 0x8d, 0x7f, 0x89, 0xa4, 0xd6, 0xb4,
0xe5, 0x99, 0x27, 0xa7, 0xab, 0x15, 0x82, 0x75, 0x3e, 0xc0, 0x9d, 0xae, 0x8c, 0x7f, 0x19, 0x80, 0xf5, 0xa6, 0x89,
0xbc, 0xb7, 0x36, 0x50, 0x1f, 0x63, 0xca, 0x40, 0x3e, 0xe1, 0x4c, 0x2d, 0x1e, 0x15, 0x51, 0x93, 0xf2, 0x1a, 0x97,
0x3e, 0xad, 0x5e, 0xba, 0xe2, 0xd6, 0xa7, 0x7c, 0x51, 0x45, 0xdf, 0x92, 0xbe, 0x35, 0xf3, 0xb4, 0xe8, 0x5c, 0x2e,
0x6a, 0x5c, 0xda, 0xe8, 0xf7, 0x51, 0xdc, 0xcb, 0x06, 0xd2, 0xd2, 0xa4, 0x7c, 0x7d, 0x5f, 0x20, 0xb8, 0x50, 0x04,
0xb5, 0xb1, 0x9a, 0x4e, 0xc9, 0x94, 0xe4, 0x40, 0x2b, 0x85, 0xc8, 0xd3, 0x71, 0xfa, 0x83, 0x81, 0x28, 0x53, 0xef,
0xa0, 0xb0, 0xf0, 0xc8, 0xb9, 0x52, 0xb2, 0x0f, 0x8c, 0x49, 0x93, 0xeb, 0x2a, 0x95, 0x83, 0x4f, 0x47, 0xe8, 0x0c,
0x83, 0x7b, 0x88, 0x18, 0x63, 0xa9, 0x87, 0x7f, 0xe3, 0x57, 0xa6, 0x57, 0xdd, 0x6d, 0x2a, 0x85, 0xe9, 0x79, 0x16,
0x3b, 0x07, 0x59, 0x4f, 0xed, 0x9a, 0x17, 0xe7, 0xcd, 0x35, 0xa6, 0x9b, 0x06, 0x1d, 0x18, 0x5e, 0xb2, 0x9b, 0xac,
0x18, 0xc6, 0x5b, 0xec, 0xbe, 0x29, 0xa3, 0x57, 0xc2, 0x6b, 0x3f, 0x11, 0x89, 0xd1, 0x57, 0xe4, 0xb8, 0xaf, 0x11,
0xba, 0x90, 0x93, 0x4f, 0x5e, 0x0c, 0x6b, 0x9f, 0x61, 0x5c, 0xdd, 0xb6, 0xcf, 0x18, 0xcc, 0x9a, 0x95, 0x32, 0x5f,
0x6f, 0xf1, 0x43, 0x17, 0x2d, 0xbc, 0x70, 0xbe, 0xd4, 0x76, 0x38, 0x42, 0xcc, 0xdd, 0xf0, 0xff, 0x5b, 0xe8, 0x19,
0xc9, 0xf4, 0x25, 0xda, 0xdb, 0x5f, 0xd2, 0x3c, 0xf3, 0xe0, 0xed, 0xb0, 0xa2, 0x4c, 0xdf, 0x05, 0xf2, 0x0c, 0xc4,
0x21, 0xb9, 0xc4, 0x02, 0xd2, 0x1e, 0x46, 0x2c, 0x57, 0x78, 0xb0, 0xf1, 0x85, 0x33, 0x51, 0xc5, 0x3e, 0xce, 0x69,
0x68, 0x77, 0x3e, 0xb4, 0x5a, 0x4e, 0x43, 0x89, 0x85, 0x71, 0x36, 0x84, 0x56, 0x5c, 0x6b, 0xce, 0x78, 0x4e, 0x86,
0x6a, 0x5f, 0xf1, 0x2d, 0x73, 0x4c, 0xba, 0xc1, 0x5e, 0x2b, 0x6e, 0x0d, 0xdd, 0x45, 0x1d, 0x92, 0xb4, 0x31, 0xd9,
0xce, 0x0b, 0x58, 0x67, 0xc1, 0xa6, 0x47, 0x2e, 0x5e, 0x6d, 0x6d, 0xa3, 0x78, 0xed, 0xc5, 0xcb, 0xac, 0x20, 0xb8,
0x6a, 0x14, 0x32, 0x55, 0xe8, 0xfb, 0xe0, 0xee, 0x31, 0xfc, 0x47, 0x33, 0xf6, 0xe7, 0x54, 0xd1, 0x61, 0x91, 0x74,
0x90, 0xfa, 0x44, 0x0a, 0xc8, 0xaf, 0xa2, 0xdb, 0x2c, 0x83, 0xda, 0xe8, 0xd0, 0xb7, 0x34, 0x8a, 0xc8, 0xbb, 0x86,
0x58, 0x82, 0x1a, 0x2f, 0xd6, 0xc5, 0x92, 0xe9, 0xd0, 0x21, 0x11, 0xf6, 0xeb, 0x9b, 0xed, 0xbf, 0xa9, 0x71, 0x89,
0xb2, 0x59, 0x76, 0xb3, 0xeb, 0x9c, 0xe1, 0x73, 0x79, 0x7b, 0x11, 0x3d, 0x4e, 0x33, 0xca, 0xf6, 0x7e, 0xd8, 0xcf,
0x06, 0x41, 0x39, 0xef, 0x78, 0x5a, 0x89, 0x69, 0x0c, 0xff, 0xda, 0x31, 0x16, 0x32, 0xd4, 0xe5, 0x2d, 0x8b, 0x22,
0xb9, 0x82, 0x38, 0xd8, 0x22, 0xfc, 0xe8, 0xbd, 0xbf, 0x0d, 0x4b, 0xc9, 0x0d, 0x74, 0x5e, 0x2d, 0xb2, 0x1f, 0x23,
0xa8, 0xf8, 0xa7, 0x27, 0xe1, 0x02, 0x61, 0x83, 0xc4, 0xd0, 0x63, 0x0e, 0x23, 0xf2, 0x34, 0x06, 0x69, 0xd1, 0xa3,
0xa2, 0x76, 0x76, 0x94, 0xbc, 0x1f, 0x78, 0x73, 0x5a, 0x48, 0x82, 0xb4, 0x68, 0x3d, 0x24, 0x52, 0xe8, 0x92, 0x4e,
0x58, 0xdf, 0x55, 0x3f, 0xa8, 0x48, 0x59, 0x35, 0x90, 0x9b, 0x55, 0xc9, 0xdc, 0xff, 0xcc, 0x22, 0x7c, 0xd1, 0x29,
0xe0, 0xd5, 0xd8, 0x31, 0x3d, 0x9b, 0xae, 0x38, 0x52, 0x2a, 0xa1, 0xc5, 0x86, 0x19, 0x93, 0xe7, 0x2d, 0x9b, 0xa9,
0x09, 0xbf, 0xdd, 0x89, 0x43, 0x35, 0xb2, 0x6f, 0x91, 0xba, 0x28, 0xfa, 0x2f, 0x2c, 0xab, 0xe8, 0x58, 0xe6, 0x4c,
0xbf, 0xd5, 0xaf, 0x49, 0xcd, 0x7f, 0x18, 0xa4, 0x69, 0xd3, 0xff, 0x99, 0xad, 0x9d, 0xb0, 0x93, 0x3b, 0x04, 0x01,
0xd5, 0x7d, 0x33, 0xe0, 0x66, 0xe2, 0xd4, 0xeb, 0xae, 0xcd, 0x7f, 0x66, 0xf2, 0x2c, 0xa7, 0xfe, 0x7d, 0x51, 0x8b,
0x2a, 0x25, 0xa8, 0x90, 0xb2, 0x71, 0xe5, 0x98, 0x40, 0xb8, 0xe9, 0x1f, 0x64, 0xd9, 0x90, 0xf2, 0x39, 0x25, 0x6f,
0x58, 0x35, 0x9e, 0x40, 0x25, 0x92, 0xbb, 0xa2, 0x4d, 0x45, 0xf6, 0xc3, 0x1a, 0xc6, 0xc0, 0xf6, 0xc2, 0x9b, 0x2b,
0xb5, 0xc7, 0xe9, 0xc8, 0x75, 0x03, 0x71, 0x1c, 0x76, 0x0e, 0xde, 0x5c, 0xe7, 0xf4, 0x91, 0xc4, 0x1d, 0x9f, 0xcb,
0x67, 0x1c, 0x1b, 0xc9, 0x25, 0x6d, 0x95, 0x05, 0xd7, 0x3b, 0xbf, 0x2d, 0x90, 0x96, 0xc3, 0x44, 0xa3, 0x0a, 0xee,
0x7a, 0x4c, 0x7e, 0x7f, 0xc2, 0x4e, 0x4f, 0x38, 0x88, 0xe9, 0x9f, 0x93, 0xd8, 0xd1, 0x10, 0x8c, 0x85, 0x89, 0xb2,
0xd0, 0x95, 0xc3, 0xe4, 0xd2, 0x8f, 0x2b, 0x0c, 0x82, 0xbd, 0x25, 0x74, 0x39, 0xe7, 0x3e, 0xef, 0xd2, 0x2f, 0x6f,
0xdc, 0xe0, 0x75, 0x6b, 0xf2, 0x66, 0x7d, 0x65, 0x18, 0x26, 0xb0, 0x7a, 0x7a, 0x45, 0xf0, 0xf2, 0xc4, 0xbe, 0x70,
0x6e, 0x58, 0x3a, 0x54, 0x31, 0x64, 0xf6, 0x49, 0xb5, 0xdc, 0x1c, 0x9e, 0x87, 0xdb, 0x6a, 0x92, 0x2a, 0x59, 0x73,
0x35, 0x4d, 0x59, 0xed, 0xd0, 0x8a, 0xd9, 0xac, 0xea, 0x7c, 0xc0, 0x0a, 0xfe, 0x8f, 0x29, 0xb7, 0xda, 0xdc, 0x8a,
0x55, 0x0f, 0x61, 0xc0, 0x82, 0x49, 0x4c, 0xc2, 0x51, 0x4b, 0x41, 0x7d, 0x1f, 0xf1, 0x07, 0x13, 0x23, 0x16, 0xe4,
0x88, 0x76, 0x38, 0x92, 0xbd, 0x3d, 0xe2, 0x7b, 0xab, 0x1c, 0x53, 0x35, 0xfb, 0x85, 0x76, 0x35, 0xf7, 0xfc, 0xdc,
0x3e, 0xc9, 0xae, 0x6a, 0x7c, 0x10, 0x44, 0xa5, 0xfb, 0xc4, 0x81, 0x9a, 0x6d, 0x9b, 0xf2, 0x87, 0x0a, 0x9e, 0xcd,
0x75, 0xc2, 0xd5, 0xe7, 0x0b, 0xcd, 0x5c, 0x6e, 0xf2, 0x51, 0x96, 0xa3, 0xe8, 0xd5, 0x5a, 0x17, 0xf5, 0x1a, 0x28,
0xff, 0xc8, 0xf0, 0xfc, 0x7e, 0x3b, 0xd3, 0xf3, 0x12, 0x61, 0xe9, 0x02, 0xfa, 0x7b, 0x8e, 0x6b, 0x34, 0x4a, 0x5a,
0xb6, 0xb1, 0x71, 0x7d, 0xad, 0xbc, 0x88, 0x22, 0xf6, 0x14, 0xc5, 0x57, 0x1a, 0x0b, 0xb6, 0x8b, 0xbb, 0x9a, 0x09,
0xf2, 0xe8, 0x8d, 0x32, 0xd1, 0x54, 0xa9, 0xd2, 0x9f, 0xc5, 0xf4, 0x12, 0xad, 0xb3, 0xfd, 0x52, 0xb7, 0xff, 0x0e,
0x72, 0x59, 0xcb, 0x1d, 0x8d, 0xa4, 0xf2, 0x0f, 0x39, 0x8f, 0xc9, 0x84, 0xe0, 0x5c, 0xdd, 0x9c, 0xe4, 0x42, 0x4f,
0xf2, 0x09, 0x1e, 0x05, 0xcc, 0x41, 0xdd, 0xbd, 0xc1, 0xed, 0xdc, 0x8e, 0x64, 0xc3, 0x67, 0x51, 0xe5, 0x8a, 0x9a,
0x0d, 0x09, 0xb8, 0x4b, 0xa1, 0xb2, 0xdb, 0x01, 0xde, 0xf5, 0xd1, 0x51, 0xf7, 0x9d, 0x88, 0x59, 0xfd, 0xd5, 0x13,
0x0d, 0x05, 0xd5, 0x5e, 0xfa, 0x63, 0x26, 0x25, 0xad, 0x01, 0x83, 0x6f, 0xf3, 0x7f, 0x56, 0x11, 0x89, 0xaa, 0xdb,
0x49, 0xaa, 0x8f, 0x7d, 0x10, 0x99, 0xbf, 0x8b, 0xcf, 0xf4, 0xf8, 0xc2, 0x4e, 0x36, 0xa7, 0xb1, 0xfe, 0xc5, 0xf0,
0x07, 0xc3, 0x57, 0xe3, 0xbd, 0x1e, 0xa1, 0x56, 0x46, 0x32, 0x43, 0x8f, 0x8d, 0xc7, 0x73, 0x0a, 0xa5, 0x35, 0xa9,
0x93, 0x7c, 0x53, 0x3c, 0xf2, 0x58, 0xf3, 0x6a, 0x37, 0x41, 0xb4, 0xc7, 0x9d, 0xcd, 0xb1, 0x67, 0xb7, 0xaa, 0x5f,
0x25, 0xb9, 0xc5, 0xb6, 0xa2, 0xdb, 0xce, 0x34, 0x2c, 0xea, 0x93, 0x45, 0x6e, 0x7b, 0x6f, 0xb6, 0xf6, 0x89, 0x83,
0xd1, 0x9e, 0x08, 0x4b, 0x57, 0x4e, 0xd9, 0xf3, 0xe8, 0x81, 0x03, 0x5c, 0xd6, 0x6c, 0x2c, 0xea, 0x27, 0x2d, 0xfb,
0x39, 0xbb, 0x25, 0x81, 0xbb, 0x1a, 0xc3, 0xcb, 0xf5, 0x5e, 0x69, 0x1c, 0xa4, 0x8a, 0x0e, 0x4a, 0x69, 0xdf, 0xcd,
0x6c, 0xc7, 0xce, 0x48, 0xdb, 0x7a, 0x0b, 0xcb, 0x40, 0xc8, 0x59, 0x81, 0xde, 0x71, 0x3e, 0x0d, 0x41, 0xc9, 0xa2,
0x57, 0xae, 0xb7, 0x3f, 0x8b, 0x4e, 0x7c, 0xcb, 0xf9, 0xa5, 0xcb, 0xf8, 0xee, 0x52, 0xbe, 0x54, 0xa8, 0xef, 0x99,
0xb7, 0xa1, 0x0b, 0x77, 0xa2, 0xa7, 0x89, 0x47, 0x7f, 0x48, 0xaa, 0x02, 0x36, 0xbf, 0x9d, 0x97, 0xba, 0x43, 0x5e,
0x39, 0xa6, 0x99, 0x81, 0xde, 0xe4, 0x98, 0x06, 0x6a, 0xc8, 0x2c, 0x96, 0xf5, 0x51, 0x8c, 0x95, 0xd3, 0xe4, 0x2b,
0x94, 0x9a, 0x15, 0x71, 0x9f, 0xa4, 0x24, 0xfc, 0x92, 0x97, 0x18, 0xb7, 0x56, 0xe5, 0x59, 0xe1, 0x1a, 0x79, 0x3a,
0x47, 0x77, 0x92, 0xf5, 0x75, 0x42, 0xbd, 0x2d, 0x55, 0x05, 0xcf, 0xe8, 0xea, 0x64, 0x8b, 0xff, 0x30, 0xa1, 0x1e,
0x28, 0x2c, 0xa9, 0xaa, 0xd4, 0x19, 0x1d, 0x24, 0x7a, 0xcf, 0x5d, 0x39, 0xf2, 0xe3, 0xd2, 0xab, 0x26, 0x97, 0x35,
0xf6, 0x70, 0x8b, 0x09, 0x8b, 0x00, 0xb9, 0xb8, 0xa8, 0xb7, 0x78, 0xf1, 0x96, 0x03, 0xd2, 0x72, 0x46, 0x26, 0x82,
0x41, 0xce, 0xe3, 0x73, 0xb2, 0x31, 0x4f, 0x49, 0xc8, 0xa8, 0xcf, 0xcc, 0xb8, 0xcc, 0x5d, 0xab, 0xd1, 0x1e, 0xf5,
0x5a, 0x9a, 0x54, 0xaf, 0x15, 0xed, 0x5f, 0x96, 0x2b, 0x62, 0x0a, 0x4c, 0x67, 0x21, 0x98, 0xfd, 0x29, 0xf8, 0x12,
0x9d, 0xc2, 0x34, 0x4d, 0x70, 0x0e, 0x3b, 0x5e, 0xe3, 0x91, 0x58, 0x82, 0xa8, 0x9c, 0x7d, 0x11, 0xfb, 0x5d, 0x81,
0x0c, 0x0f, 0x9d, 0x84, 0xbb, 0xeb, 0x65, 0x1d, 0xeb, 0x6e, 0x18, 0x8e, 0x93, 0x70, 0xbb, 0x9d, 0xed, 0x58, 0xb2,
0x64, 0xf2, 0x7b, 0x8f, 0x05, 0xd8, 0xfa, 0x55, 0x50, 0xef, 0x7a, 0x49, 0x0d, 0xdf, 0xca, 0x63, 0xff, 0x41, 0x1d,
0x89, 0x79, 0xca, 0x8c, 0x1b, 0xbe, 0x75, 0x6f, 0x0e, 0x6b, 0x68, 0x8c, 0x5c, 0x93, 0xc8, 0xf3, 0x62, 0x0b, 0x68,
0xb1, 0x32, 0x2f, 0x1e, 0xda, 0xe5, 0x6d, 0x45, 0xaf, 0x3d, 0xa7, 0xaf, 0x81, 0x0f, 0xda, 0x82, 0x85, 0x94, 0x06,
0xc5, 0x3f, 0xf5, 0x73, 0x5f, 0xa4, 0x43, 0x5f, 0xb3, 0xac, 0x3d, 0x16, 0xe8, 0x6f, 0x84, 0xb3, 0xb6, 0x75, 0xf7,
0x13, 0x03, 0x46, 0x67, 0x87, 0x9a, 0xf1, 0x6c, 0x2e, 0x33, 0x33, 0x14, 0x85, 0x18, 0xe9, 0xd6, 0x1b, 0xee, 0x17,
0xe9, 0xe7, 0x6b, 0xd4, 0x46, 0xaf, 0x26, 0x55, 0x8d, 0xfc, 0x3a, 0xb4, 0x61, 0x1a, 0x9e, 0x2d, 0x85, 0xa2, 0x86,
0x8c, 0x92, 0xbd, 0x8d, 0x0e, 0x4b, 0x4e, 0x0d, 0xdf, 0x4a, 0xc6, 0x66, 0xcd, 0x28, 0xff, 0xc4, 0x44, 0xba, 0x39,
0x4a, 0x04, 0x43, 0xce, 0x95, 0x47, 0x99, 0x8c, 0x9b, 0xb7, 0x72, 0x48, 0xdd, 0x9b, 0x98, 0x4f, 0xd5, 0x18, 0x81,
0xb4, 0xef, 0xfa, 0x96, 0x54, 0x9b, 0xda, 0x73, 0xe2, 0xe0, 0x27, 0x2f, 0x10, 0x0f, 0xf9, 0x0e, 0x24, 0xb4, 0xd2,
0x39, 0xf7, 0x49, 0x8c, 0x5c, 0x05, 0x37, 0xdd, 0xa7, 0x73, 0xac, 0x16, 0x85, 0x71, 0x46, 0xd1, 0x6f, 0x7a, 0x25,
0xd2, 0x5c, 0x4e, 0x70, 0x26, 0x93, 0x36, 0x22, 0x5f, 0xe5, 0x42, 0x4e, 0x89, 0xf2, 0x4c, 0x55, 0x9b, 0xd1, 0xa5,
0xaf, 0x87, 0xd8, 0x7e, 0xe8, 0xdd, 0xb9, 0x47, 0xb3, 0xda, 0xf7, 0x98, 0x52, 0x73, 0x8b, 0x3c, 0x0b, 0xe7, 0xfa,
0x14, 0x7f, 0x9a, 0xa6, 0x8e, 0xf4, 0x7c, 0xad, 0x58, 0x11, 0x73, 0xe3, 0x3a, 0x56, 0x47, 0x9b, 0x95, 0x24, 0x68,
0x35, 0xa9, 0xd9, 0x6e, 0x5c, 0x4d, 0xe8, 0xa4, 0xb8, 0x80, 0x0b, 0x73, 0xc4, 0x0f, 0x87, 0x0c, 0xa1, 0xf4, 0x5a,
0x3c, 0xce, 0x3f, 0x30, 0x3d, 0xb5, 0xc1, 0x0b, 0x43, 0x2a, 0xb1, 0xbe, 0x6b, 0x12, 0xc4, 0x4b, 0xdb, 0xcb, 0x2f,
0x77, 0x8a, 0x3a, 0xba, 0xf4, 0xa3, 0x9b, 0x05, 0xee, 0x98, 0x50, 0x77, 0xfe, 0x82, 0x5b, 0x29, 0x6d, 0x7c, 0x0c,
0x4b, 0x41, 0xa2, 0x6a, 0xb2, 0x27, 0x32, 0xf1, 0x0c, 0x1d, 0xd7, 0x6c, 0x02, 0x4d, 0xbd, 0xf2, 0x5a, 0x66, 0xd3,
0xb5, 0x30, 0xab, 0x4b, 0x7e, 0x72, 0x77, 0x30, 0x42, 0xd7, 0x70, 0x05, 0x9f, 0xeb, 0x2f, 0x22, 0x7f, 0x49, 0x2d,
0x5d, 0xcf, 0x4b, 0x84, 0x0c, 0xa5, 0xb9, 0xe5, 0x49, 0xae, 0x54, 0x2c, 0x86, 0x53, 0x4c, 0x9c, 0x42, 0x29, 0x78,
0xf0, 0x0a, 0x8a, 0x36, 0x53, 0x4d, 0x2b, 0xc9, 0xe2, 0x69, 0x0e, 0x9a, 0x37, 0x0c, 0x91, 0x2d, 0xac, 0x2f, 0x43,
0x6a, 0xf1, 0xe9, 0xfb, 0xb4, 0x68, 0xec, 0x90, 0x00, 0xff, 0x32, 0xf8, 0x58, 0xab, 0xd1, 0x73, 0x9f, 0x77, 0x72,
0x22, 0xed, 0x64, 0xac, 0x7d, 0xd0, 0xa8, 0x1f, 0x01, 0x99, 0x9f, 0xfd, 0xa6, 0xcb, 0x62, 0xca, 0x65, 0xd8, 0xf8,
0x31, 0xf0, 0xc1, 0x5e, 0xef, 0xf1, 0x4d, 0x79, 0xa9, 0xfc, 0x8f, 0x57, 0xcb, 0xc1, 0xb7, 0x03, 0x30, 0xbb, 0xad,
0xdb, 0x88, 0xf7, 0xcc, 0xa1, 0x25, 0xfd, 0x37, 0x03, 0xc5, 0xdb, 0x7f, 0xb4, 0xe0, 0xe6, 0xa3, 0x7e, 0xa4, 0x52,
0x68, 0xa7, 0x2e, 0xe6, 0x1c, 0xea, 0x91, 0x48, 0x67, 0xbd, 0x3d, 0x6d, 0xcc, 0x09, 0x49, 0x17, 0x50, 0x16, 0xb5,
0x65, 0x63, 0xd4, 0x84, 0x2c, 0x3a, 0x5f, 0x6a, 0x3c, 0x66, 0xfa, 0x24, 0xaf, 0xd1, 0xfd, 0x18, 0x66, 0xe4, 0xe8,
0x3c, 0x6c, 0x74, 0xb6, 0xc5, 0x4c, 0xf8, 0xa8, 0x15, 0x3c, 0xd0, 0xb8, 0x4a, 0x53, 0x15, 0x4b, 0x1e, 0x56, 0x7f,
0xd8, 0x40, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xfe, 0xe7, 0xdc, 0x03, 0xd1, 0x0f, 0xfd, 0x7c, 0xdc, 0xa1, 0x7e, 0x41, 0x2e, 0x50, 0x37, 0x77, 0x94, 0xb7, 0x4b,
0x90, 0x3b, 0x14, 0x38, 0xe0, 0x80, 0x03, 0x0e, 0x38, 0xe0, 0x80, 0xfb, 0xff, 0xba, 0xff, 0x00,
};
static const uint8_t STORED[] = {
0x01, 0x2c, 0x01, 0xd3, 0xfe, 0x65, 0x73, 0x70, 0x68, 0x6f, 0x6d, 0x65, 0x20, 0x6f, 0x74, 0x61, 0x20, 0x64,
0x65, 0x66, 0x6c, 0x61, 0x74, 0x65, 0x20, 0x65, 0x73, 0x70, 0x68, 0x6f, 0x6d, 0x65, 0x20, 0x6f, 0x74, 0x61,
0x20, 0x64, 0x65, 0x66, 0x6c, 0x61, 0x74, 0x65, 0x20, 0x65, 0x73, 0x70, 0x68, 0x6f, 0x6d, 0x65, 0x20, 0x6f,
0x74, 0x61, 0x20, 0x64, 0x65, 0x66, 0x6c, 0x61, 0x74, 0x65, 0x20, 0x65, 0x73, 0x70, 0x68, 0x6f, 0x6d, 0x65,
0x20, 0x6f, 0x74, 0x61, 0x20, 0x64, 0x65, 0x66, 0x6c, 0x61, 0x74, 0x65, 0x20, 0x65, 0x73, 0x70, 0x68, 0x6f,
0x6d, 0x65, 0x20, 0x6f, 0x74, 0x61, 0x20, 0x64, 0x65, 0x66, 0x6c, 0x61, 0x74, 0x65, 0x20, 0x65, 0x73, 0x70,
0x68, 0x6f, 0x6d, 0x65, 0x20, 0x6f, 0x74, 0x61, 0x20, 0x64, 0x65, 0x66, 0x6c, 0x61, 0x74, 0x65, 0x20, 0x65,
0x73, 0x70, 0x68, 0x6f, 0x6d, 0x65, 0x20, 0x6f, 0x74, 0x61, 0x20, 0x64, 0x65, 0x66, 0x6c, 0x61, 0x74, 0x65,
0x20, 0x65, 0x73, 0x70, 0x68, 0x6f, 0x6d, 0x65, 0x20, 0x6f, 0x74, 0x61, 0x20, 0x64, 0x65, 0x66, 0x6c, 0x61,
0x74, 0x65, 0x20, 0x65, 0x73, 0x70, 0x68, 0x6f, 0x6d, 0x65, 0x20, 0x6f, 0x74, 0x61, 0x20, 0x64, 0x65, 0x66,
0x6c, 0x61, 0x74, 0x65, 0x20, 0x65, 0x73, 0x70, 0x68, 0x6f, 0x6d, 0x65, 0x20, 0x6f, 0x74, 0x61, 0x20, 0x64,
0x65, 0x66, 0x6c, 0x61, 0x74, 0x65, 0x20, 0x65, 0x73, 0x70, 0x68, 0x6f, 0x6d, 0x65, 0x20, 0x6f, 0x74, 0x61,
0x20, 0x64, 0x65, 0x66, 0x6c, 0x61, 0x74, 0x65, 0x20, 0x65, 0x73, 0x70, 0x68, 0x6f, 0x6d, 0x65, 0x20, 0x6f,
0x74, 0x61, 0x20, 0x64, 0x65, 0x66, 0x6c, 0x61, 0x74, 0x65, 0x20, 0x65, 0x73, 0x70, 0x68, 0x6f, 0x6d, 0x65,
0x20, 0x6f, 0x74, 0x61, 0x20, 0x64, 0x65, 0x66, 0x6c, 0x61, 0x74, 0x65, 0x20, 0x65, 0x73, 0x70, 0x68, 0x6f,
0x6d, 0x65, 0x20, 0x6f, 0x74, 0x61, 0x20, 0x64, 0x65, 0x66, 0x6c, 0x61, 0x74, 0x65, 0x20, 0x65, 0x73, 0x70,
0x68, 0x6f, 0x6d, 0x65, 0x20, 0x6f, 0x74, 0x61, 0x20, 0x64, 0x65, 0x66, 0x6c, 0x61, 0x74, 0x65, 0x20,
};
static constexpr size_t WINDOW = 4096;
static constexpr size_t PLAIN_SIZE = 16000;
static uint8_t lcg_next(uint32_t &x) {
x = (x * 1103515245u + 12345u) & 0x7fffffffu;
return (x >> 16) & 0xff;
}
static std::vector<uint8_t> build_plain() {
std::vector<uint8_t> plain;
const char *text = "esphome ota deflate ";
for (int i = 0; i < 300; i++)
plain.insert(plain.end(), text, text + strlen(text));
uint32_t x = 1;
for (int i = 0; i < 3000; i++)
plain.push_back(lcg_next(x));
plain.insert(plain.end(), 5000, 0);
for (int i = 0; i < 100; i++)
plain.insert(plain.end(), text, text + strlen(text));
return plain;
}
// Mirrors the OTA session: chunked input through the read callback, window as output
struct Session : OtaInflateState {
const uint8_t *in;
size_t in_len;
size_t in_pos;
size_t chunk;
std::vector<uint8_t> out;
uint8_t window[WINDOW];
};
static int read_cb(OtaInflateState *d) {
auto *s = static_cast<Session *>(d);
if (s->in_pos >= s->in_len)
return -1;
size_t n = std::min(s->chunk, s->in_len - s->in_pos);
d->source = s->in + s->in_pos + 1;
d->source_limit = s->in + s->in_pos + n;
s->in_pos += n;
return s->in[s->in_pos - n];
}
// Inflates the whole input; returns the decoder result and fills s.out
static int inflate_all(Session &s, const uint8_t *in, size_t in_len, size_t chunk) {
s.in = in;
s.in_len = in_len;
s.in_pos = 0;
s.chunk = chunk;
s.out.clear();
memset(s.window, 0, sizeof(s.window));
ota_inflate_init(&s, s.window, WINDOW);
s.source_read_cb = read_cb;
int res;
do {
s.dest = s.window;
s.dest_limit = s.window + WINDOW;
res = ota_inflate(&s);
if (res < 0 || s.eof)
return res < 0 ? res : OTA_INFLATE_DATA_ERROR;
s.out.insert(s.out.end(), s.window, s.dest);
if (s.out.size() > PLAIN_SIZE)
return OTA_INFLATE_DATA_ERROR;
} while (res != OTA_INFLATE_DONE);
return res;
}
TEST(OtaInflate, RoundTripThroughWindow) {
auto s = std::make_unique<Session>();
ASSERT_EQ(inflate_all(*s, DEFLATED, sizeof(DEFLATED), 1040), OTA_INFLATE_DONE);
EXPECT_EQ(s->out, build_plain());
EXPECT_EQ(s->in_pos, sizeof(DEFLATED));
}
TEST(OtaInflate, SmallReadChunks) {
auto s = std::make_unique<Session>();
ASSERT_EQ(inflate_all(*s, DEFLATED, sizeof(DEFLATED), 7), OTA_INFLATE_DONE);
EXPECT_EQ(s->out, build_plain());
}
TEST(OtaInflate, StoredBlock) {
auto s = std::make_unique<Session>();
ASSERT_EQ(inflate_all(*s, STORED, sizeof(STORED), 64), OTA_INFLATE_DONE);
auto plain = build_plain();
plain.resize(300);
EXPECT_EQ(s->out, plain);
}
TEST(OtaInflate, TruncatedStreamFails) {
auto s = std::make_unique<Session>();
for (size_t cut : {size_t{1}, size_t{100}, size_t{1000}, sizeof(DEFLATED) - 1}) {
EXPECT_LT(inflate_all(*s, DEFLATED, cut, 1040), 0) << "cut at " << cut;
EXPECT_LE(s->out.size(), PLAIN_SIZE);
}
}
TEST(OtaInflate, TruncatedStoredBlockFails) {
auto s = std::make_unique<Session>();
EXPECT_LT(inflate_all(*s, STORED, sizeof(STORED) - 50, 64), 0);
}
TEST(OtaInflate, CorruptStreamsNeverEscapeTheWindow) {
// Flipped bytes and garbage; the sanitizers check the decoder stays in bounds
auto s = std::make_unique<Session>();
std::vector<uint8_t> bad(DEFLATED, DEFLATED + sizeof(DEFLATED));
// A coarse, non-aligned stride: neighbouring offsets hit the same paths
for (size_t i = 0; i < bad.size(); i += 29) {
bad[i] ^= 0x5a;
inflate_all(*s, bad.data(), bad.size(), 1040);
bad[i] ^= 0x5a;
}
uint32_t x = 99;
std::vector<uint8_t> garbage(2000);
for (int round = 0; round < 50; round++) {
for (auto &b : garbage)
b = lcg_next(x);
inflate_all(*s, garbage.data(), garbage.size(), 1040);
}
}
} // namespace esphome::testing
+7
View File
@@ -38,8 +38,15 @@ espnow:
data: !lambda 'return {0x01, 0x02, 0x03, 0x04, 0x05};'
- espnow.peer.add:
address: 11:22:33:44:55:66
- espnow.peer.add:
address: !lambda 'return {0x11, 0x22, 0x33, 0x44, 0x55, 0x66};'
- espnow.peer.delete:
address: 11:22:33:44:55:66
- espnow.peer.delete:
address: !lambda 'return {0x11, 0x22, 0x33, 0x44, 0x55, 0x66};'
- espnow.set_channel: 6
- espnow.set_channel:
channel: !lambda 'return 6;'
on_broadcast:
- logger.log:
format: "Broadcast from: %s = '%s' RSSI: %d"
@@ -0,0 +1,19 @@
ethernet:
type: KSZ8851SNL
clk_pin: 19
mosi_pin: 21
miso_pin: 23
cs_pin: 18
interrupt_pin: 36
reset_pin: 22
clock_speed: 10Mhz
manual_ip:
static_ip: 192.168.178.56
gateway: 192.168.178.1
subnet: 255.255.255.0
domain: .local
mac_address: "02:AA:BB:CC:DD:01"
on_connect:
- logger.log: "Ethernet connected!"
on_disconnect:
- logger.log: "Ethernet disconnected!"
@@ -0,0 +1,2 @@
packages:
ethernet: !include common-ksz8851snl.yaml
@@ -0,0 +1,15 @@
# W5500 next to wifi with PSRAM: compiles the input path that moves received frames to PSRAM
packages:
ethernet: !include common-w5500.yaml
psram:
mode: quad
wifi:
ssid: MySSID
password: password1
network:
priority:
- ethernet
- wifi
+12
View File
@@ -18,3 +18,15 @@ event:
// Log using %.*s format for StringRef
ESP_LOGD("test", "Event type: %.*s", (int) event_type.size(), event_type.c_str());
}
button:
- platform: template
name: Trigger Event
on_press:
- event.trigger:
id: some_event
event_type: template_event_type1
- event.trigger:
id: some_event
event_type: !lambda |-
return id(some_event).has_event() ? "template_event_type2" : "template_event_type1";
@@ -0,0 +1,20 @@
button:
- platform: template
name: EZO PMP Actions
on_press:
- ezo_pmp.change_i2c_address:
id: hcl_pump
address: 104
- ezo_pmp.change_i2c_address:
id: hcl_pump
address: !lambda return 105;
- ezo_pmp.dose_volume:
id: hcl_pump
volume: !lambda return 2.5;
- ezo_pmp.dose_volume_over_time:
id: hcl_pump
volume: !lambda return 2.5;
duration: !lambda return 3;
- ezo_pmp.arbitrary_command:
id: hcl_pump
command: !lambda return "D,?";
@@ -0,0 +1,4 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
ezo_pmp: !include common.yaml
actions: !include common-actions.yaml
+25 -4
View File
@@ -67,7 +67,7 @@ binary_sensor:
return false;
# Exercise fan.turn_on with various field combinations so the
# TurnOnAction codegen paths get build coverage.
# register_apply_action codegen paths get build coverage.
button:
- platform: template
name: "Fan Speed Only"
@@ -96,6 +96,23 @@ button:
- fan.turn_on:
id: test_fan
speed: !lambda 'return 1;'
- platform: template
name: "Fan Is On Off"
on_press:
- if:
condition:
fan.is_on: test_fan
then:
- fan.turn_off: test_fan
- fan.toggle: test_fan
- fan.turn_off:
id: test_fan
- if:
condition:
fan.is_off:
id: test_fan
then:
- fan.turn_on: test_fan
# Exercise fan.turn_on inside triggers with non-empty Ts:
# - number.on_value: Ts = float (Python value type; previously raised
@@ -111,6 +128,10 @@ number:
step: 1
on_value:
then:
- fan.turn_on:
id: test_fan
speed: !lambda "return (int) x;"
- if:
condition:
fan.is_on: test_fan
then:
- fan.turn_on:
id: test_fan
speed: !lambda "return (int) x;"
@@ -21,6 +21,8 @@ light:
green: 0%
blue: 0%
num_leds: 1
- color: darkred
num_leds: 1
add_led_interval: 100ms
reverse: false
- addressable_scan:
@@ -0,0 +1,16 @@
button:
- platform: template
name: Fingerprint LED Actions
on_press:
- fingerprint_grow.enroll: 3
- fingerprint_grow.enroll:
finger_id: !lambda return 4;
num_scans: !lambda return 3;
- fingerprint_grow.led_control: true
- fingerprint_grow.led_control:
state: !lambda return false;
- fingerprint_grow.aura_led_control:
state: BREATHING
speed: 200
color: BLUE
count: 2
@@ -0,0 +1,9 @@
substitutions:
tx_pin: GPIO0
rx_pin: GPIO2
sensing_pin: GPIO15
packages:
uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml
fingerprint_grow: !include common.yaml
actions: !include common-actions.yaml
@@ -0,0 +1,9 @@
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
# This component's AUTO_LOAD = ["climate_ir"] sits on the climate platform manifest, while its
# own __init__.py is empty. The unit test build resolves the bare `fujitsu_general` domain, so
# it never sees that manifest. And climate_ir itself doesn't declare `climate` even though
# ClimateIR derives from climate::Climate. Pull both in so the test can include the header.
manifest.dependencies = manifest.dependencies + ["climate_ir", "climate"]
@@ -0,0 +1,254 @@
#include <gtest/gtest.h>
#include "esphome/components/fujitsu_general/fujitsu_general.h"
namespace esphome::fujitsu_general::testing {
// The mode field of a received frame is three bits wide. The fourth bit of the same nibble belongs
// to the clean feature, so it has to be ignored when reading the mode.
TEST(FujitsuGeneralDecodeModeTest, DecodesTheAssignedModes) {
EXPECT_EQ(decode_mode(0x00, climate::CLIMATE_MODE_OFF), climate::CLIMATE_MODE_HEAT_COOL);
EXPECT_EQ(decode_mode(0x01, climate::CLIMATE_MODE_OFF), climate::CLIMATE_MODE_COOL);
EXPECT_EQ(decode_mode(0x02, climate::CLIMATE_MODE_OFF), climate::CLIMATE_MODE_DRY);
EXPECT_EQ(decode_mode(0x03, climate::CLIMATE_MODE_OFF), climate::CLIMATE_MODE_FAN_ONLY);
EXPECT_EQ(decode_mode(0x04, climate::CLIMATE_MODE_OFF), climate::CLIMATE_MODE_HEAT);
}
TEST(FujitsuGeneralDecodeModeTest, IgnoresTheCleanBit) {
// 0x0B is fan mode with the clean bit set. It used to be read as one value and reported as
// heat/cool, which is the bug this covers.
EXPECT_EQ(decode_mode(0x0B, climate::CLIMATE_MODE_OFF), climate::CLIMATE_MODE_FAN_ONLY);
EXPECT_EQ(decode_mode(0x08, climate::CLIMATE_MODE_OFF), climate::CLIMATE_MODE_HEAT_COOL);
EXPECT_EQ(decode_mode(0x09, climate::CLIMATE_MODE_OFF), climate::CLIMATE_MODE_COOL);
EXPECT_EQ(decode_mode(0x0A, climate::CLIMATE_MODE_OFF), climate::CLIMATE_MODE_DRY);
EXPECT_EQ(decode_mode(0x0C, climate::CLIMATE_MODE_OFF), climate::CLIMATE_MODE_HEAT);
}
TEST(FujitsuGeneralDecodeModeTest, KeepsTheCurrentModeForUnassignedValues) {
// 0x5 to 0x7 fit in the field but the protocol does not use them.
EXPECT_EQ(decode_mode(0x05, climate::CLIMATE_MODE_COOL), climate::CLIMATE_MODE_COOL);
EXPECT_EQ(decode_mode(0x06, climate::CLIMATE_MODE_HEAT), climate::CLIMATE_MODE_HEAT);
EXPECT_EQ(decode_mode(0x07, climate::CLIMATE_MODE_DRY), climate::CLIMATE_MODE_DRY);
// The same three with the clean bit set. Without the mask these would not reach this branch.
EXPECT_EQ(decode_mode(0x0D, climate::CLIMATE_MODE_COOL), climate::CLIMATE_MODE_COOL);
EXPECT_EQ(decode_mode(0x0E, climate::CLIMATE_MODE_HEAT), climate::CLIMATE_MODE_HEAT);
EXPECT_EQ(decode_mode(0x0F, climate::CLIMATE_MODE_FAN_ONLY), climate::CLIMATE_MODE_FAN_ONLY);
}
TEST(FujitsuGeneralDecodeModeTest, NeverReportsOffForAStateFrame) {
// A state frame describes a running unit, so keeping an off current mode would publish it as off
// and turn the next transmission into a power off command. Automatic is the least specific mode
// available, which is what the field's unassigned values decoded to before they were masked.
for (uint8_t field = 0x05; field <= 0x07; field++) {
SCOPED_TRACE(static_cast<int>(field));
EXPECT_EQ(decode_mode(field, climate::CLIMATE_MODE_OFF), climate::CLIMATE_MODE_HEAT_COOL);
EXPECT_EQ(decode_mode(field | 0b1000, climate::CLIMATE_MODE_OFF), climate::CLIMATE_MODE_HEAT_COOL);
}
}
// The fan speed field is three bits wide as well, and used to fold every value it did not
// recognise into the automatic speed.
TEST(FujitsuGeneralDecodeFanModeTest, DecodesTheAssignedSpeeds) {
EXPECT_EQ(decode_fan_mode(0x00, climate::CLIMATE_FAN_LOW), climate::CLIMATE_FAN_AUTO);
EXPECT_EQ(decode_fan_mode(0x01, climate::CLIMATE_FAN_LOW), climate::CLIMATE_FAN_HIGH);
EXPECT_EQ(decode_fan_mode(0x02, climate::CLIMATE_FAN_LOW), climate::CLIMATE_FAN_MEDIUM);
EXPECT_EQ(decode_fan_mode(0x03, climate::CLIMATE_FAN_AUTO), climate::CLIMATE_FAN_LOW);
EXPECT_EQ(decode_fan_mode(0x04, climate::CLIMATE_FAN_LOW), climate::CLIMATE_FAN_QUIET);
}
TEST(FujitsuGeneralDecodeFanModeTest, IgnoresTheFourthBit) {
EXPECT_EQ(decode_fan_mode(0x08, climate::CLIMATE_FAN_LOW), climate::CLIMATE_FAN_AUTO);
EXPECT_EQ(decode_fan_mode(0x09, climate::CLIMATE_FAN_LOW), climate::CLIMATE_FAN_HIGH);
EXPECT_EQ(decode_fan_mode(0x0A, climate::CLIMATE_FAN_LOW), climate::CLIMATE_FAN_MEDIUM);
EXPECT_EQ(decode_fan_mode(0x0B, climate::CLIMATE_FAN_AUTO), climate::CLIMATE_FAN_LOW);
EXPECT_EQ(decode_fan_mode(0x0C, climate::CLIMATE_FAN_LOW), climate::CLIMATE_FAN_QUIET);
}
TEST(FujitsuGeneralDecodeFanModeTest, KeepsTheCurrentFanModeForUnassignedValues) {
EXPECT_EQ(decode_fan_mode(0x05, climate::CLIMATE_FAN_HIGH), climate::CLIMATE_FAN_HIGH);
EXPECT_EQ(decode_fan_mode(0x06, climate::CLIMATE_FAN_MEDIUM), climate::CLIMATE_FAN_MEDIUM);
EXPECT_EQ(decode_fan_mode(0x07, climate::CLIMATE_FAN_LOW), climate::CLIMATE_FAN_LOW);
EXPECT_EQ(decode_fan_mode(0x0D, climate::CLIMATE_FAN_HIGH), climate::CLIMATE_FAN_HIGH);
EXPECT_EQ(decode_fan_mode(0x0E, climate::CLIMATE_FAN_HIGH), climate::CLIMATE_FAN_HIGH);
EXPECT_EQ(decode_fan_mode(0x0F, climate::CLIMATE_FAN_LOW), climate::CLIMATE_FAN_LOW);
}
TEST(FujitsuGeneralDecodeFanModeTest, LeavesAnUnsetFanModeUnset) {
EXPECT_FALSE(decode_fan_mode(0x05, {}).has_value());
}
// The swing field is only two bits wide. The two bits above it are reserved, and were read as part
// of the value.
TEST(FujitsuGeneralDecodeSwingModeTest, DecodesTheAssignedValues) {
EXPECT_EQ(decode_swing_mode(0x00), climate::CLIMATE_SWING_OFF);
EXPECT_EQ(decode_swing_mode(0x01), climate::CLIMATE_SWING_VERTICAL);
EXPECT_EQ(decode_swing_mode(0x02), climate::CLIMATE_SWING_HORIZONTAL);
EXPECT_EQ(decode_swing_mode(0x03), climate::CLIMATE_SWING_BOTH);
}
TEST(FujitsuGeneralDecodeSwingModeTest, IgnoresTheReservedBits) {
// Without the mask everything from 0x04 up fell through to the default branch and reported swing
// off. All twelve are covered, so the field's whole input space is asserted.
const climate::ClimateSwingMode expected[] = {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL,
climate::CLIMATE_SWING_HORIZONTAL, climate::CLIMATE_SWING_BOTH};
for (uint8_t field = 0x04; field <= 0x0F; field++) {
SCOPED_TRACE(static_cast<int>(field));
EXPECT_EQ(decode_swing_mode(field), expected[field & 0b0011]);
}
}
// Every state frame annotated in fujitsu_general.h, as the bytes those rows spell out. None of them
// sets the fourth bit of the mode or fan field, or either bit above the swing field, so the masks
// must leave all of them decoding exactly as they did before this change.
namespace {
struct CapturedFrame {
const char *label;
uint8_t bytes[16];
uint8_t temperature;
bool turn_on;
climate::ClimateMode mode;
climate::ClimateFanMode fan_mode;
climate::ClimateSwingMode swing_mode;
};
constexpr CapturedFrame CAPTURED_FRAMES[] = {
{"auto auto 18",
{0x14, 0x63, 0x00, 0x10, 0x10, 0xFE, 0x09, 0x30, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x8F},
18,
true,
climate::CLIMATE_MODE_HEAT_COOL,
climate::CLIMATE_FAN_AUTO,
climate::CLIMATE_SWING_OFF},
{"auto auto 19",
{0x14, 0x63, 0x00, 0x10, 0x10, 0xFE, 0x09, 0x30, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x7F},
19,
true,
climate::CLIMATE_MODE_HEAT_COOL,
climate::CLIMATE_FAN_AUTO,
climate::CLIMATE_SWING_OFF},
{"auto auto 30 (temperatures)",
{0x14, 0x63, 0x00, 0x10, 0x10, 0xFE, 0x09, 0x30, 0xE1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0xCF},
30,
true,
climate::CLIMATE_MODE_HEAT_COOL,
climate::CLIMATE_FAN_AUTO,
climate::CLIMATE_SWING_OFF},
{"on at 16",
{0x14, 0x63, 0x00, 0x10, 0x10, 0xFE, 0x09, 0x30, 0x01, 0x04, 0x00, 0x00, 0x00, 0x00, 0x20, 0xAB},
16,
true,
climate::CLIMATE_MODE_HEAT,
climate::CLIMATE_FAN_AUTO,
climate::CLIMATE_SWING_OFF},
{"down to 16",
{0x14, 0x63, 0x00, 0x10, 0x10, 0xFE, 0x09, 0x30, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x20, 0xAC},
16,
false,
climate::CLIMATE_MODE_HEAT,
climate::CLIMATE_FAN_AUTO,
climate::CLIMATE_SWING_OFF},
{"auto auto 30 (mode options)",
{0x14, 0x63, 0x00, 0x10, 0x10, 0xFE, 0x09, 0x30, 0xE1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0xCF},
30,
true,
climate::CLIMATE_MODE_HEAT_COOL,
climate::CLIMATE_FAN_AUTO,
climate::CLIMATE_SWING_OFF},
{"cool auto 30",
{0x14, 0x63, 0x00, 0x10, 0x10, 0xFE, 0x09, 0x30, 0xE1, 0x01, 0x00, 0x00, 0x00, 0x00, 0x20, 0xCE},
30,
true,
climate::CLIMATE_MODE_COOL,
climate::CLIMATE_FAN_AUTO,
climate::CLIMATE_SWING_OFF},
{"dry auto 30",
{0x14, 0x63, 0x00, 0x10, 0x10, 0xFE, 0x09, 0x30, 0xE1, 0x02, 0x00, 0x00, 0x00, 0x00, 0x20, 0xCD},
30,
true,
climate::CLIMATE_MODE_DRY,
climate::CLIMATE_FAN_AUTO,
climate::CLIMATE_SWING_OFF},
{"fan (auto) (30)",
{0x14, 0x63, 0x00, 0x10, 0x10, 0xFE, 0x09, 0x30, 0xE1, 0x03, 0x00, 0x00, 0x00, 0x00, 0x20, 0xCC},
30,
true,
climate::CLIMATE_MODE_FAN_ONLY,
climate::CLIMATE_FAN_AUTO,
climate::CLIMATE_SWING_OFF},
{"heat auto 30",
{0x14, 0x63, 0x00, 0x10, 0x10, 0xFE, 0x09, 0x30, 0xE1, 0x04, 0x00, 0x00, 0x00, 0x00, 0x20, 0xCB},
30,
true,
climate::CLIMATE_MODE_HEAT,
climate::CLIMATE_FAN_AUTO,
climate::CLIMATE_SWING_OFF},
{"heat 30 high",
{0x14, 0x63, 0x00, 0x10, 0x10, 0xFE, 0x09, 0x30, 0xE1, 0x04, 0x01, 0x00, 0x00, 0x00, 0x20, 0xCA},
30,
true,
climate::CLIMATE_MODE_HEAT,
climate::CLIMATE_FAN_HIGH,
climate::CLIMATE_SWING_OFF},
{"heat 30 med",
{0x14, 0x63, 0x00, 0x10, 0x10, 0xFE, 0x09, 0x30, 0xE0, 0x04, 0x02, 0x00, 0x00, 0x00, 0x20, 0xCA},
30,
false,
climate::CLIMATE_MODE_HEAT,
climate::CLIMATE_FAN_MEDIUM,
climate::CLIMATE_SWING_OFF},
{"heat 30 low",
{0x14, 0x63, 0x00, 0x10, 0x10, 0xFE, 0x09, 0x30, 0xE0, 0x04, 0x03, 0x00, 0x00, 0x00, 0x20, 0xC9},
30,
false,
climate::CLIMATE_MODE_HEAT,
climate::CLIMATE_FAN_LOW,
climate::CLIMATE_SWING_OFF},
{"heat 30 quiet",
{0x14, 0x63, 0x00, 0x10, 0x10, 0xFE, 0x09, 0x30, 0xE0, 0x04, 0x04, 0x00, 0x00, 0x00, 0x20, 0xC8},
30,
false,
climate::CLIMATE_MODE_HEAT,
climate::CLIMATE_FAN_QUIET,
climate::CLIMATE_SWING_OFF},
{"heat 30 swing vert",
{0x14, 0x63, 0x00, 0x10, 0x10, 0xFE, 0x09, 0x30, 0xE0, 0x04, 0x14, 0x00, 0x00, 0x00, 0x20, 0xB8},
30,
false,
climate::CLIMATE_MODE_HEAT,
climate::CLIMATE_FAN_QUIET,
climate::CLIMATE_SWING_VERTICAL},
{"heat 30 noswing",
{0x14, 0x63, 0x00, 0x10, 0x10, 0xFE, 0x09, 0x30, 0xE0, 0x04, 0x04, 0x00, 0x00, 0x00, 0x20, 0xC8},
30,
false,
climate::CLIMATE_MODE_HEAT,
climate::CLIMATE_FAN_QUIET,
climate::CLIMATE_SWING_OFF},
};
} // namespace
TEST(FujitsuGeneralCaptureTest, DecodesEveryCapturedFrame) {
for (const auto &frame : CAPTURED_FRAMES) {
SCOPED_TRACE(frame.label);
// Read through the component's own nibble helper and field indices, so this also fails if the
// frame layout the header records ever stops matching what on_receive() reads.
EXPECT_EQ(get_nibble(frame.bytes, FUJITSU_GENERAL_TEMPERATURE_NIBBLE) + FUJITSU_GENERAL_TEMP_MIN,
frame.temperature);
// The turn on flag is only written by transmit_state(), so this pins the frame layout rather
// than a decode path.
EXPECT_EQ(get_nibble(frame.bytes, FUJITSU_GENERAL_POWER_ON_NIBBLE) != 0, frame.turn_on);
EXPECT_EQ(decode_mode(get_nibble(frame.bytes, FUJITSU_GENERAL_MODE_NIBBLE), climate::CLIMATE_MODE_OFF), frame.mode);
EXPECT_EQ(decode_fan_mode(get_nibble(frame.bytes, FUJITSU_GENERAL_FAN_NIBBLE), climate::CLIMATE_FAN_ON),
frame.fan_mode);
EXPECT_EQ(decode_swing_mode(get_nibble(frame.bytes, FUJITSU_GENERAL_SWING_NIBBLE)), frame.swing_mode);
}
}
} // namespace esphome::fujitsu_general::testing
+2 -2
View File
@@ -4,8 +4,8 @@ esphome:
- globals.set:
id: glob_int
value: "10"
# Set a float global with an integer literal - must emit the correct
# return type so TemplatableFn stores a direct function pointer.
# Set a float global with an integer literal; the lambda must return the
# global's own type so the assignment needs no conversion.
- globals.set:
id: glob_float
value: "102"
+6
View File
@@ -0,0 +1,6 @@
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
# AUTO_LOAD sits on the climate platform, which the unit test build does not load.
manifest.dependencies = manifest.dependencies + ["climate_ir", "climate"]
+24
View File
@@ -0,0 +1,24 @@
#include <gtest/gtest.h>
#include "esphome/components/gree/gree.h"
namespace esphome::gree::testing {
TEST(GreeClimateTest, HeatCoolHiddenWithoutHeatByDefault) {
GreeClimate climate;
climate.set_supports_heat(false);
climate.set_supports_cool(true);
climate.set_supports_heat_cool(false);
EXPECT_FALSE(climate.get_traits().supports_mode(climate::CLIMATE_MODE_HEAT_COOL));
}
TEST(GreeClimateTest, HeatCoolOverrideAdvertisedWithoutHeat) {
GreeClimate climate;
climate.set_supports_heat(false);
climate.set_supports_cool(true);
climate.set_supports_heat_cool(true);
auto traits = climate.get_traits();
EXPECT_TRUE(traits.supports_mode(climate::CLIMATE_MODE_HEAT_COOL));
EXPECT_FALSE(traits.supports_mode(climate::CLIMATE_MODE_HEAT));
}
} // namespace esphome::gree::testing
@@ -0,0 +1,10 @@
packages:
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml
climate:
- platform: gree
name: GREE
transmitter_id: xmitr
model: YAN
supports_heat: false
supports_heat_cool: true
@@ -0,0 +1,9 @@
packages:
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml
climate:
- platform: gree
name: GREE
transmitter_id: xmitr
model: YAN
supports_cool: false
@@ -0,0 +1,9 @@
packages:
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml
climate:
- platform: gree
name: GREE
transmitter_id: xmitr
model: YAN
supports_heat_cool: false
@@ -0,0 +1,9 @@
packages:
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml
climate:
- platform: gree
name: GREE
transmitter_id: xmitr
model: YAN
supports_heat: false
@@ -0,0 +1,22 @@
button:
- platform: template
name: Grove Motor Actions
on_press:
- grove_tb6612fng.run:
channel: !lambda return 0;
speed: 100
direction: FORWARD
id: test_motor
- grove_tb6612fng.stop:
channel: !lambda return 1;
id: test_motor
- grove_tb6612fng.break:
channel: !lambda return 0;
id: test_motor
- grove_tb6612fng.standby:
id: test_motor
- grove_tb6612fng.no_standby:
id: test_motor
- grove_tb6612fng.change_address:
address: 0x15
id: test_motor
@@ -0,0 +1,4 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
grove_tb6612fng: !include common.yaml
actions: !include common-actions.yaml
@@ -0,0 +1,21 @@
button:
- platform: template
name: Haier Actions
on_press:
- climate.haier.display_on: haier_ac
- climate.haier.display_off: haier_ac
- climate.haier.beeper_on: haier_ac
- climate.haier.beeper_off: haier_ac
- climate.haier.start_self_cleaning: haier_ac
- climate.haier.start_steri_cleaning: haier_ac
- climate.haier.health_on: haier_ac
- climate.haier.health_off: haier_ac
- climate.haier.power_on: haier_ac
- climate.haier.power_off: haier_ac
- climate.haier.power_toggle: haier_ac
- climate.haier.set_vertical_airflow:
id: haier_ac
vertical_airflow: UP
- climate.haier.set_horizontal_airflow:
id: haier_ac
horizontal_airflow: !lambda return esphome::haier::hon_protocol::HorizontalSwingMode::LEFT;
@@ -0,0 +1,8 @@
substitutions:
tx_pin: GPIO0
rx_pin: GPIO2
packages:
uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml
haier: !include common.yaml
actions: !include common-actions.yaml
@@ -0,0 +1,5 @@
button:
- platform: template
name: Brake the fan
on_press:
- fan.hbridge.brake: fan_hbridge
@@ -0,0 +1,10 @@
substitutions:
pwm_platform: "esp8266_pwm"
output1_pin: "4"
output2_pin: "5"
output3_pin: "12"
output4_pin: "13"
packages:
hbridge: !include common.yaml
actions: !include common-actions.yaml
+7
View File
@@ -0,0 +1,7 @@
button:
- platform: template
name: HC8 Calibrate
on_press:
- hc8.calibrate:
id: hc8_sensor
baseline: !lambda return 400;
@@ -0,0 +1,8 @@
substitutions:
tx_pin: GPIO0
rx_pin: GPIO2
packages:
uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml
hc8: !include common.yaml
actions: !include common-actions.yaml
@@ -0,0 +1,12 @@
button:
- platform: template
name: HDC302x Actions
on_press:
- hdc302x.heater_on:
id: hdc302x_sensor
power: 0x03FF
duration: !lambda return 2000;
- hdc302x.heater_on:
id: hdc302x_sensor
power: !lambda return 0x3FFF;
- hdc302x.heater_off: hdc302x_sensor
@@ -0,0 +1,4 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
hdc302x: !include common.yaml
actions: !include common-actions.yaml
+7
View File
@@ -4,11 +4,18 @@ esphome:
- hlk_fm22x.enroll:
name: "Test"
direction: 1
- hlk_fm22x.delete: 5
- hlk_fm22x.delete_all:
- hlk_fm22x.reset:
- hlk_fm22x.scan:
hlk_fm22x:
on_face_scan_matched:
- logger.log: test_hlk_22x_face_scan_matched
- hlk_fm22x.enroll:
name: !lambda 'return name + "-again";'
direction: !lambda 'return face_id % 3;'
- hlk_fm22x.delete: !lambda 'return face_id;'
on_face_scan_unmatched:
- logger.log: test_hlk_22x_face_scan_unmatched
on_face_scan_invalid:
+17 -3
View File
@@ -35,11 +35,18 @@ inline void connect_controller(HoermannHcp &door) {
door.on_write_registers(COMMAND_REG, make_registers({0x0000, 0x0000}));
}
// Runs one command poll (write 2 / read 8) and returns both key-press registers.
inline std::pair<uint16_t, uint16_t> poll_command(HoermannHcp &door) {
door.on_write_registers(COMMAND_REG, make_registers({0x0000, 0x0000}));
// Runs one status poll (write 2 / read 8) and returns the whole answer. The bus controller writes its counter
// with command 0x03 here; most tests do not care and pass zero.
inline RegisterValues status_answer(HoermannHcp &door, uint16_t command_reg = 0x0000) {
door.on_write_registers(COMMAND_REG, make_registers({command_reg, 0x0000}));
RegisterValues response;
door.on_read_holding_registers(STATE_REG, 8, response);
return response;
}
// Runs one command poll (write 2 / read 8) and returns both key-press registers.
inline std::pair<uint16_t, uint16_t> poll_command(HoermannHcp &door) {
const RegisterValues response = status_answer(door);
EXPECT_EQ(response.size(), 8u);
if (response.size() != 8u)
return {0xFFFF, 0xFFFF};
@@ -59,7 +66,14 @@ class TestableHoermannHcp : public HoermannHcp {
TestableHoermannHcp() { this->key_press_delay_ms_ = 0; }
using HoermannHcp::connection_timeout_ms_;
#ifdef USE_HOERMANN_HCP_IDENTITY
using HoermannHcp::identity_asked_at_;
using HoermannHcp::identity_request_;
using HoermannHcp::firmware_unreadable_;
using HoermannHcp::serial_unreadable_;
#endif
using HoermannHcp::is_light_toggle_pending_;
using HoermannHcp::key_press_delay_ms_;
using HoermannHcp::light_toggle_released_at_;
using HoermannHcp::light_toggles_in_flight_;
using HoermannHcp::set_valid_;
@@ -22,3 +22,12 @@ button:
light:
- platform: hoermann_hcp
name: Garage Light
text_sensor:
- platform: hoermann_hcp
door_state:
name: Garage Door State
serial_number:
name: Garage Motor Serial Number
version:
name: Garage Motor Firmware Version
@@ -24,14 +24,17 @@ class CountingHoermannHcpLight : public HoermannHcpLight {
int writes{0};
};
// Drives the platform against a real LightState. ALWAYS_OFF keeps setup() clear of preferences.
// Drives the platform against a real LightState. Boots off (no persistence) by default, which
// keeps setup() clear of preferences.
struct LightFixture {
TestableHoermannHcp door;
CountingHoermannHcpLight output{&door};
light::LightState state{&output};
explicit LightFixture(light::LightRestoreMode restore_mode = light::LIGHT_ALWAYS_OFF) {
this->state.set_restore_mode(restore_mode);
explicit LightFixture(bool boot_on = false) {
if (boot_on) {
this->state.set_state_callback([](light::LightStateRTCState &s, bool /*restored*/) { s.state = true; });
}
this->output.setup();
// setup() queues the restored state for write_state(); the first settle() below delivers it, which is the
// boot ordering tests need to be able to place around the bus controller coming up.
@@ -621,10 +624,10 @@ TEST(HoermannHcpLightTest, ReleaseWithNothingOutstandingLeavesTheWatchdogDisarme
EXPECT_EQ(door.light_toggle_released_at_, 0u);
}
// A restore mode that boots the entity on replays a lit state the door has never confirmed, so it has to be
// Booting the entity on replays a lit state the door has never confirmed, so it has to be
// adopted back to what is known rather than turned into a command.
TEST(HoermannHcpLightPlatformTest, RestoredOnStateIsAdoptedNotCommanded) {
LightFixture fixture{light::LIGHT_ALWAYS_ON};
LightFixture fixture{/*boot_on=*/true};
connect_controller(fixture.door);
fixture.settle();
@@ -0,0 +1,11 @@
import esphome.codegen as cg
from esphome.types import ConfigType
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
# The platform's own to_code needs a configured hub; only its define is wanted here.
async def to_code_testing(config: ConfigType) -> None:
cg.add_define("USE_HOERMANN_HCP_IDENTITY")
manifest.to_code = to_code_testing
@@ -0,0 +1,125 @@
#include <gtest/gtest.h>
#include "esphome/components/hoermann_hcp/text_sensor/hoermann_hcp_text_sensor.h"
#include "../common.h"
namespace esphome::hoermann_hcp::testing {
namespace {
// A status broadcast with the door state in the high byte of its third register.
void broadcast_state(HoermannHcp &door, uint16_t state_reg) {
door.on_write_registers(BROADCAST_REG, make_registers({0x0000, 0x0000, state_reg}));
door.update();
}
struct DoorStateFixture {
DoorStateFixture() {
this->sensor.setup();
this->sensor.add_on_state_callback([this](const std::string & /*state*/) { this->publishes++; });
}
TestableHoermannHcp door;
HoermannHcpDoorStateTextSensor sensor{&door};
int publishes{0};
};
} // namespace
// Polls and bus scans make the connection valid before any broadcast has said where the door is, and an
// undecodable state says nothing either. None of these may show the default state.
TEST(HoermannHcpDoorStateTest, NothingBeforeTheDoorReportsAState) {
DoorStateFixture fixture;
auto &door = fixture.door;
door.update();
EXPECT_FALSE(fixture.sensor.has_state());
status_answer(door, 0x0003);
door.update();
ASSERT_TRUE(door.is_valid());
EXPECT_FALSE(fixture.sensor.has_state());
RegisterValues scan;
door.on_read_holding_registers(STATE_REG, 5, scan);
door.update();
EXPECT_FALSE(fixture.sensor.has_state());
broadcast_state(door, 0x1000);
EXPECT_FALSE(fixture.sensor.has_state());
// The first real state is shown even when it equals the default.
broadcast_state(door, 0x4000);
EXPECT_EQ(fixture.sensor.get_state(), "Closed");
}
// Every state the door reports is shown, including the vent and half-open positions and the moves to them.
TEST(HoermannHcpDoorStateTest, FollowsTheDoorState) {
DoorStateFixture fixture;
auto &door = fixture.door;
connect_controller(door);
const std::pair<uint16_t, const char *> states[] = {
{0x2000, "Open"}, {0x0200, "Closing"}, {0x4000, "Closed"}, {0x0900, "Moving to vent"},
{0x0A00, "Vent position"}, {0x0500, "Moving to half"}, {0x8000, "Half open"}, {0x0100, "Opening"},
{0x0000, "Stopped"}, {0x0061, "Vent position"},
};
for (const auto &[reg, text] : states) {
broadcast_state(door, reg);
EXPECT_EQ(fixture.sensor.get_state(), text) << "state register 0x" << std::hex << reg;
}
}
// Any hub change runs the publish path, so an unchanged door state is not published again, and a state the
// door is not known to report keeps the last one.
TEST(HoermannHcpDoorStateTest, EachStateIsPublishedOnce) {
DoorStateFixture fixture;
auto &door = fixture.door;
connect_controller(door);
broadcast_state(door, 0x2000);
ASSERT_EQ(fixture.publishes, 1);
// The lamp changes, the door state does not.
door.on_write_registers(BROADCAST_REG, make_registers({0x0000, 0x0000, 0x2000, 0x0000, 0x0000, 0x0000, 0x0010}));
door.update();
EXPECT_EQ(fixture.publishes, 1);
broadcast_state(door, 0x1000);
EXPECT_EQ(fixture.publishes, 1);
EXPECT_EQ(fixture.sensor.get_state(), "Open");
}
// While the bus controller is gone the last state stays. Once it is back, a poll alone shows nothing new; the
// next broadcast is published again even if it repeats the old state.
TEST(HoermannHcpDoorStateTest, LastStateStaysUntilTheNextBroadcast) {
DoorStateFixture fixture;
auto &door = fixture.door;
connect_controller(door);
broadcast_state(door, 0x8000);
ASSERT_EQ(fixture.publishes, 1);
door.set_valid_(false);
door.update();
EXPECT_EQ(fixture.sensor.get_state(), "Half open");
connect_controller(door);
door.update();
EXPECT_EQ(fixture.publishes, 1);
broadcast_state(door, 0x8000);
EXPECT_EQ(fixture.publishes, 2);
EXPECT_EQ(fixture.sensor.get_state(), "Half open");
}
// A sensor set up after the hub already decoded a state shows it right away.
TEST(HoermannHcpDoorStateTest, LateSetupShowsTheCurrentState) {
TestableHoermannHcp door;
connect_controller(door);
broadcast_state(door, 0x0A00);
HoermannHcpDoorStateTextSensor sensor(&door);
sensor.setup();
EXPECT_EQ(sensor.get_state(), "Vent position");
}
} // namespace esphome::hoermann_hcp::testing
@@ -0,0 +1,510 @@
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <string>
#include <thread>
#include "esphome/components/text_sensor/text_sensor.h"
#include "../common.h"
namespace esphome::hoermann_hcp::testing {
namespace {
// Made up. 26 bytes on the wire: the first 14 arrive in one transfer, the other 12 in the next.
constexpr const char *SERIAL = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
// Made up as well, padded with a space to the 12 bytes the motor sends.
constexpr const char *FIRMWARE = "FW-TEST 1.0 ";
constexpr uint8_t SUB_SERIAL = 0x0C;
constexpr uint8_t SUB_FIRMWARE = 0x0D;
constexpr uint8_t FIRST_HALF = 0x80;
// A status poll as the bus controller writes it: its counter in the high byte, command 0x03 in the low.
RegisterValues status_poll(HoermannHcp &door, uint8_t counter = 0x05) {
return status_answer(door, static_cast<uint16_t>((counter << 8) | 0x03));
}
// The write half of a payload transfer: the motor writes the bytes into the command block.
void write_transfer(HoermannHcp &door, uint8_t counter, uint8_t sub_code, const char *bytes, size_t len) {
RegisterValues written;
written.push_back(static_cast<uint16_t>((counter << 8) | 0x04));
written.push_back(static_cast<uint16_t>(sub_code << 8));
for (size_t i = 0; i < len; i += 2) {
written.push_back(
static_cast<uint16_t>((static_cast<uint8_t>(bytes[i]) << 8) | static_cast<uint8_t>(bytes[i + 1])));
}
door.on_write_registers(COMMAND_REG, written);
}
// A whole payload transfer, returning the answer the motor reads back.
RegisterValues transfer(HoermannHcp &door, uint8_t counter, uint8_t sub_code, const char *bytes, size_t len,
uint16_t read_registers = 8) {
write_transfer(door, counter, sub_code, bytes, len);
RegisterValues response;
door.on_read_holding_registers(STATE_REG, read_registers, response);
return response;
}
// The first status poll gets an ordinary answer, the next one carries the serial number request.
void request_serial(HoermannHcp &door) {
status_poll(door, 0x03);
status_poll(door, 0x04);
}
void send_serial(HoermannHcp &door) {
transfer(door, FIRST_HALF | 0x05, SUB_SERIAL, SERIAL, 14);
transfer(door, 0x06, SUB_SERIAL, SERIAL + 14, 12);
}
// A hub with both sensors configured, which is what makes it ask.
struct IdentityFixture {
IdentityFixture() {
this->door.set_serial_number_text_sensor(&this->serial);
this->door.set_version_text_sensor(&this->version);
}
// What the sensors show once the loop has turned; empty while they have no state.
std::string serial_shown() {
this->door.update();
return this->serial.has_state() ? this->serial.get_state() : "";
}
std::string version_shown() {
this->door.update();
return this->version.has_state() ? this->version.get_state() : "";
}
TestableHoermannHcp door;
text_sensor::TextSensor serial;
text_sensor::TextSensor version;
};
// The whole exchange as the motor runs it, with the loop turning in between as it would.
void run_identity_exchange(HoermannHcp &door) {
request_serial(door);
send_serial(door);
door.update();
status_poll(door, 0x07);
transfer(door, 0x08, SUB_FIRMWARE, FIRMWARE, 12);
door.update();
}
} // namespace
// With no sensor configured, polls and transfers are answered exactly as before.
TEST(HoermannHcpTextSensorTest, NothingChangesWithoutASensor) {
HoermannHcp door;
for (int poll = 0; poll < 2; poll++) {
const RegisterValues response = status_poll(door);
ASSERT_EQ(response.size(), 8u);
EXPECT_EQ(response[1], 0x0301);
EXPECT_EQ(response[2], 0x0000);
}
const RegisterValues answer = transfer(door, FIRST_HALF | 0x05, SUB_SERIAL, SERIAL, 14);
EXPECT_EQ(answer[1] & 0x00FF, 0x0001);
}
// Like Hoermann's own bus accessory, the first status poll gets an ordinary answer and the next one carries the
// request, echoing the status command like any status answer. It is asked again only once 30 s have passed.
TEST(HoermannHcpTextSensorTest, SerialNumberIsAskedForAfterOneOrdinaryAnswer) {
IdentityFixture fixture;
auto &door = fixture.door;
EXPECT_EQ(status_poll(door)[1], 0x0301);
const RegisterValues response = status_poll(door);
ASSERT_EQ(response.size(), 8u);
EXPECT_EQ(response[0], 0x0500);
EXPECT_EQ(response[1], 0x0322);
EXPECT_EQ(response[2], 0x0500);
door.identity_asked_at_ -= 29000;
EXPECT_EQ(status_poll(door)[1], 0x0301);
door.identity_asked_at_ -= 2000;
EXPECT_EQ(status_poll(door)[1], 0x0322);
}
// Three attempts at the serial number, then the firmware version is asked for anyway, three times as well.
TEST(HoermannHcpTextSensorTest, GivesUpAfterThreeAttemptsEach) {
IdentityFixture fixture;
auto &door = fixture.door;
status_poll(door);
for (int attempt = 0; attempt < 3; attempt++) {
const RegisterValues response = status_poll(door);
EXPECT_EQ(response[1], 0x0322);
EXPECT_EQ(response[2], 0x0500);
door.identity_asked_at_ -= 31000;
}
EXPECT_EQ(status_poll(door)[1], 0x0301);
for (int attempt = 0; attempt < 3; attempt++) {
const RegisterValues response = status_poll(door);
EXPECT_EQ(response[1], 0x0322);
EXPECT_EQ(response[2], 0x0600);
door.identity_asked_at_ -= 31000;
}
EXPECT_EQ(status_poll(door)[1], 0x0301);
EXPECT_EQ(door.identity_request_(), 0);
}
// A first half left behind by a serial number that never completed is not shown.
TEST(HoermannHcpTextSensorTest, HalfASerialNumberIsNeverShown) {
IdentityFixture fixture;
auto &door = fixture.door;
request_serial(door);
transfer(door, FIRST_HALF | 0x05, SUB_SERIAL, SERIAL, 14);
for (int attempt = 0; attempt < 4; attempt++) {
door.identity_asked_at_ -= 31000;
status_poll(door);
}
EXPECT_EQ(fixture.serial_shown(), "");
}
// Each half is acknowledged with the counter it came with, minus the half marker. The serial number is shown as
// soon as it is whole, and the firmware version is asked for right after.
TEST(HoermannHcpTextSensorTest, SerialNumberInTwoHalvesThenTheFirmwareVersion) {
IdentityFixture fixture;
auto &door = fixture.door;
request_serial(door);
RegisterValues answer = transfer(door, FIRST_HALF | 0x05, SUB_SERIAL, SERIAL, 14);
ASSERT_EQ(answer.size(), 8u);
EXPECT_EQ(answer[0], 0x0500);
EXPECT_EQ(answer[1], 0x04FD);
EXPECT_EQ(fixture.serial_shown(), "");
answer = transfer(door, 0x06, SUB_SERIAL, SERIAL + 14, 12);
EXPECT_EQ(answer[0], 0x0600);
EXPECT_EQ(answer[1], 0x04FD);
EXPECT_EQ(fixture.serial_shown(), SERIAL);
EXPECT_EQ(fixture.version_shown(), "");
const RegisterValues response = status_poll(door);
EXPECT_EQ(response[1], 0x0322);
EXPECT_EQ(response[2], 0x0600);
answer = transfer(door, 0x07, SUB_FIRMWARE, FIRMWARE, 12);
EXPECT_EQ(answer[1], 0x04FD);
EXPECT_EQ(fixture.version_shown(), "FW-TEST 1.0");
}
// Once both values are in, nothing more is asked, however long the device keeps running.
TEST(HoermannHcpTextSensorTest, FinishedExchangeStaysFinished) {
IdentityFixture fixture;
auto &door = fixture.door;
run_identity_exchange(door);
door.identity_asked_at_ -= 31000;
EXPECT_EQ(status_poll(door)[1], 0x0301);
EXPECT_EQ(door.identity_request_(), 0);
}
// The text ends at the first byte that is not printable. 0xFF is below the printable range where char is signed,
// as on the host, and above it where char is unsigned, as on most targets. DEL is above it either way.
TEST(HoermannHcpTextSensorTest, PaddingEndsTheText) {
for (const char pad : {'\xFF', '\x7F'}) {
IdentityFixture fixture;
auto &door = fixture.door;
request_serial(door);
const char first[] = {'1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', pad};
const char second[] = {pad, pad, pad, pad, pad, pad, pad, pad, pad, pad, pad, pad};
transfer(door, FIRST_HALF | 0x05, SUB_SERIAL, first, sizeof(first));
transfer(door, 0x06, SUB_SERIAL, second, sizeof(second));
EXPECT_EQ(fixture.serial_shown(), "1234567890123");
}
}
// A half that cannot be used is still acknowledged but not kept, so the request stays open for the retry: a
// first half too short, a second half too short, a second half without a first.
TEST(HoermannHcpTextSensorTest, UnusableSerialHalvesAreNotKept) {
{
IdentityFixture fixture;
auto &door = fixture.door;
request_serial(door);
EXPECT_EQ(transfer(door, FIRST_HALF | 0x05, SUB_SERIAL, SERIAL, 12)[1], 0x04FD);
transfer(door, 0x06, SUB_SERIAL, SERIAL + 14, 12);
EXPECT_EQ(fixture.serial_shown(), "");
EXPECT_EQ(door.identity_request_(), 0x05);
}
{
IdentityFixture fixture;
auto &door = fixture.door;
request_serial(door);
transfer(door, FIRST_HALF | 0x05, SUB_SERIAL, SERIAL, 14);
transfer(door, 0x06, SUB_SERIAL, SERIAL + 14, 10);
EXPECT_EQ(fixture.serial_shown(), "");
EXPECT_EQ(door.identity_request_(), 0x05);
}
}
// Older motors (index B1 seen) send the whole serial number in one frame, without the half marker. The bytes are
// the ones a B1 sent, with the serial number made up.
TEST(HoermannHcpTextSensorTest, SerialNumberInOneFrameThenTheFirmwareVersion) {
IdentityFixture fixture;
auto &door = fixture.door;
request_serial(door);
const char one_frame[12] = {'1', '2', '3', '4', '5', '6', '7', '8', '9', 'B', '1', 0};
EXPECT_EQ(transfer(door, 0x05, SUB_SERIAL, one_frame, 12)[1], 0x04FD);
EXPECT_EQ(door.identity_request_(), 0x06);
EXPECT_EQ(fixture.serial_shown(), "123456789B1");
auto answer = status_poll(door, 0x06);
EXPECT_EQ(answer[1], 0x0322);
EXPECT_EQ(answer[2], 0x0600);
}
// The frames as a B1 motor sends them on the bus, which reads a transfer answer back as 2 registers. The serial
// number is made up.
TEST(HoermannHcpTextSensorTest, ExchangeWithTheFrameSizesOfAB1) {
IdentityFixture fixture;
auto &door = fixture.door;
request_serial(door);
const char one_frame[12] = {'1', '2', '3', '4', '5', '6', '7', '8', '9', 'B', '1', 0};
EXPECT_THAT(transfer(door, 0x05, SUB_SERIAL, one_frame, 12, 2), ::testing::ElementsAre(0x0500, 0x04FD));
EXPECT_THAT(status_poll(door, 0x06), ::testing::ElementsAre(0x0600, 0x0322, 0x0600, 0, 0, 0, 0, 0));
const char zeros[12] = {};
EXPECT_THAT(transfer(door, 0x07, SUB_FIRMWARE, zeros, 12, 2), ::testing::ElementsAre(0x0700, 0x04FD));
EXPECT_EQ(door.identity_request_(), 0);
EXPECT_EQ(fixture.serial_shown(), "123456789B1");
EXPECT_EQ(fixture.version_shown(), "");
EXPECT_EQ(status_poll(door, 0x08)[1], 0x0301);
}
// A transfer of the value not asked for is acknowledged but not kept, and the request stays open.
TEST(HoermannHcpTextSensorTest, TheValueNotAskedForIsNotKept) {
IdentityFixture fixture;
auto &door = fixture.door;
request_serial(door);
EXPECT_EQ(transfer(door, 0x05, SUB_FIRMWARE, FIRMWARE, 12)[1], 0x04FD);
EXPECT_EQ(door.identity_request_(), 0x05);
EXPECT_EQ(fixture.version_shown(), "");
send_serial(door);
EXPECT_EQ(fixture.serial_shown(), SERIAL);
status_poll(door, 0x07);
const char other[14] = {'Z', 'Z', 'Z', 'Z', 'Z', 'Z', 'Z', 'Z', 'Z', 'Z', 'Z', 'Z', 'Z', 'Z'};
EXPECT_EQ(transfer(door, FIRST_HALF | 0x08, SUB_SERIAL, other, 14)[1], 0x04FD);
EXPECT_EQ(transfer(door, 0x09, SUB_SERIAL, other, 12)[1], 0x04FD);
EXPECT_EQ(door.identity_request_(), 0x06);
EXPECT_EQ(fixture.serial_shown(), SERIAL);
}
// A transfer before the request has gone out, as from a motor still finishing an exchange from before a restart,
// is acknowledged but not kept.
TEST(HoermannHcpTextSensorTest, ATransferBeforeTheRequestIsNotKept) {
IdentityFixture fixture;
auto &door = fixture.door;
status_poll(door);
const char one_frame[12] = {'1', '2', '3', '4', '5', '6', '7', '8', '9', 'B', '1', 0};
EXPECT_EQ(transfer(door, 0x04, SUB_SERIAL, one_frame, 12)[1], 0x04FD);
EXPECT_EQ(door.identity_request_(), 0x05);
EXPECT_EQ(fixture.serial_shown(), "");
EXPECT_EQ(status_poll(door, 0x05)[1], 0x0322);
}
// After a frame with the half marker, one without it can only be the second half, even if the first was unusable.
TEST(HoermannHcpTextSensorTest, ASecondHalfIsNeverTakenForTheWholeNumber) {
IdentityFixture fixture;
auto &door = fixture.door;
request_serial(door);
transfer(door, FIRST_HALF | 0x05, SUB_SERIAL, SERIAL, 12);
transfer(door, 0x06, SUB_SERIAL, SERIAL + 14, 12);
EXPECT_EQ(fixture.serial_shown(), "");
EXPECT_EQ(door.identity_request_(), 0x05);
}
// A serial number without any text at its start is not shown but logged, in one frame or in two halves. The
// firmware version is still asked for.
TEST(HoermannHcpTextSensorTest, SerialNumberThatIsNotTextIsLoggedNotShown) {
const char zeros[14] = {};
{
IdentityFixture fixture;
auto &door = fixture.door;
request_serial(door);
transfer(door, 0x05, SUB_SERIAL, zeros, 12);
EXPECT_TRUE(door.serial_unreadable_); // kept for the log
EXPECT_EQ(door.identity_request_(), 0x06);
EXPECT_EQ(fixture.serial_shown(), "");
EXPECT_FALSE(door.serial_unreadable_); // logged once
}
{
IdentityFixture fixture;
auto &door = fixture.door;
request_serial(door);
transfer(door, FIRST_HALF | 0x05, SUB_SERIAL, zeros, 14);
transfer(door, 0x06, SUB_SERIAL, zeros, 12);
EXPECT_TRUE(door.serial_unreadable_);
EXPECT_EQ(door.identity_request_(), 0x06);
EXPECT_EQ(fixture.serial_shown(), "");
EXPECT_FALSE(door.serial_unreadable_);
}
}
// A firmware version too short is not kept, and is asked for again.
TEST(HoermannHcpTextSensorTest, ShortFirmwareVersionIsNotKept) {
IdentityFixture fixture;
auto &door = fixture.door;
request_serial(door);
send_serial(door);
status_poll(door, 0x07);
transfer(door, 0x08, SUB_FIRMWARE, FIRMWARE, 10);
EXPECT_TRUE(door.firmware_unreadable_); // kept for the log
EXPECT_EQ(fixture.version_shown(), "");
EXPECT_FALSE(door.firmware_unreadable_); // logged once
EXPECT_EQ(door.identity_request_(), 0x06);
}
// A firmware version without any payload is logged, not shown.
TEST(HoermannHcpTextSensorTest, EmptyFirmwareVersionIsLoggedNotShown) {
IdentityFixture fixture;
auto &door = fixture.door;
request_serial(door);
send_serial(door);
status_poll(door, 0x07);
transfer(door, 0x08, SUB_FIRMWARE, FIRMWARE, 0);
EXPECT_TRUE(door.firmware_unreadable_);
EXPECT_EQ(fixture.version_shown(), "");
EXPECT_FALSE(door.firmware_unreadable_);
EXPECT_EQ(door.identity_request_(), 0x06);
}
// A readable firmware version right after a short one, before the loop has turned, is still shown.
TEST(HoermannHcpTextSensorTest, ReadableFirmwareVersionAfterAShortOneIsShown) {
IdentityFixture fixture;
auto &door = fixture.door;
request_serial(door);
send_serial(door);
status_poll(door, 0x07);
transfer(door, 0x08, SUB_FIRMWARE, FIRMWARE, 10);
transfer(door, 0x09, SUB_FIRMWARE, FIRMWARE, 12);
EXPECT_FALSE(door.firmware_unreadable_);
EXPECT_EQ(fixture.version_shown(), "FW-TEST 1.0");
}
// All zeros is how a motor that does not report its version says so: nothing shown, not asked for again.
TEST(HoermannHcpTextSensorTest, AllZeroFirmwareVersionMeansNoneIsReported) {
IdentityFixture fixture;
auto &door = fixture.door;
request_serial(door);
send_serial(door);
status_poll(door, 0x07);
const char zeros[12] = {};
transfer(door, 0x08, SUB_FIRMWARE, zeros, 12);
EXPECT_EQ(fixture.version_shown(), "");
EXPECT_FALSE(door.firmware_unreadable_);
EXPECT_EQ(door.identity_request_(), 0);
}
// A firmware version that is not text is not shown, is logged, and is not asked for again: it would come back
// the same.
TEST(HoermannHcpTextSensorTest, FirmwareVersionThatIsNotTextIsLoggedNotShown) {
IdentityFixture fixture;
auto &door = fixture.door;
request_serial(door);
send_serial(door);
status_poll(door, 0x07);
const char binary[12] = {0x01, 0x12, 0x34, 0x00, 0, 0, 0, 0, 0, 0, 0, 0};
transfer(door, 0x08, SUB_FIRMWARE, binary, 12);
EXPECT_TRUE(door.firmware_unreadable_);
EXPECT_EQ(fixture.version_shown(), ""); // the raw bytes are not published
EXPECT_FALSE(door.firmware_unreadable_);
EXPECT_EQ(door.identity_request_(), 0);
}
// A repeat of a transfer already taken, as after a lost acknowledgement, is acknowledged again. Answered as a
// status poll instead, it would carry the key press waiting in the slot.
TEST(HoermannHcpTextSensorTest, RepeatedTransferIsAcknowledgedNotAnsweredWithAKeyPress) {
IdentityFixture fixture;
auto &door = fixture.door;
run_identity_exchange(door);
connect_controller(door);
door.open_door();
const RegisterValues answer = transfer(door, 0x08, SUB_FIRMWARE, FIRMWARE, 12);
EXPECT_EQ(answer[1], 0x04FD);
EXPECT_EQ(answer[2], 0x0000);
EXPECT_EQ(status_poll(door)[2], 0x0210);
}
// An answer belongs to the frame whose write half took the transfer. A frame whose read went elsewhere leaves
// nothing behind for the next poll.
TEST(HoermannHcpTextSensorTest, AnAnswerBelongsToItsFrame) {
IdentityFixture fixture;
auto &door = fixture.door;
status_poll(door);
write_transfer(door, FIRST_HALF | 0x05, SUB_SERIAL, SERIAL, 14);
RegisterValues ignored;
door.on_read_holding_registers(COMMAND_REG, 8, ignored);
EXPECT_EQ(status_poll(door)[1] & 0x00FF, 0x0022);
}
// Only the answer to a status poll carries a request, not the answer to another frame of the same length, and
// other transfers are not this exchange's to answer.
TEST(HoermannHcpTextSensorTest, RequestRidesOnlyOnAStatusPoll) {
IdentityFixture fixture;
auto &door = fixture.door;
status_poll(door);
const RegisterValues other = transfer(door, 0x06, 0x19, "\x00\x0F", 2);
EXPECT_EQ(other[1] & 0x00FF, 0x0001);
EXPECT_EQ(status_poll(door, 0x07)[1], 0x0322);
}
// The request travels in the registers a key press would, so it waits for the press, the hold and the release.
TEST(HoermannHcpTextSensorTest, RequestWaitsForTheKeyPress) {
IdentityFixture fixture;
auto &door = fixture.door;
door.key_press_delay_ms_ = 100;
connect_controller(door);
status_poll(door);
door.open_door();
EXPECT_EQ(status_poll(door)[2], 0x0210);
const RegisterValues held = status_poll(door);
EXPECT_EQ(held[1], 0x0301);
EXPECT_EQ(held[2], 0x0000);
door.key_press_delay_ms_ = 0;
std::this_thread::sleep_for(KEY_PRESS_ELAPSED);
const RegisterValues release = status_poll(door);
EXPECT_EQ(release[1], 0x0301);
EXPECT_EQ(release[2], 0x0110);
EXPECT_EQ(status_poll(door)[1], 0x0322);
}
// Each value is published once it is in, and only once.
TEST(HoermannHcpTextSensorTest, EachValueIsPublishedOnce) {
IdentityFixture fixture;
int serial_publishes = 0;
int version_publishes = 0;
fixture.serial.add_on_state_callback([&serial_publishes](const std::string & /*state*/) { serial_publishes++; });
fixture.version.add_on_state_callback([&version_publishes](const std::string & /*state*/) { version_publishes++; });
fixture.door.update();
EXPECT_EQ(serial_publishes, 0);
EXPECT_EQ(version_publishes, 0);
run_identity_exchange(fixture.door);
EXPECT_EQ(fixture.serial.get_state(), SERIAL);
EXPECT_EQ(fixture.version.get_state(), "FW-TEST 1.0");
fixture.door.update();
fixture.door.update();
EXPECT_EQ(serial_publishes, 1);
EXPECT_EQ(version_publishes, 1);
}
// Configuring only one of the two is enough to ask.
TEST(HoermannHcpTextSensorTest, OneSensorIsEnough) {
TestableHoermannHcp version_only;
text_sensor::TextSensor version;
version_only.set_version_text_sensor(&version);
run_identity_exchange(version_only);
EXPECT_EQ(version.get_state(), "FW-TEST 1.0");
TestableHoermannHcp serial_only;
text_sensor::TextSensor serial;
serial_only.set_serial_number_text_sensor(&serial);
run_identity_exchange(serial_only);
EXPECT_EQ(serial.get_state(), SERIAL);
}
} // namespace esphome::hoermann_hcp::testing
@@ -50,11 +50,11 @@ esphome:
format: "After delay, body still: %s"
args:
- body.c_str()
# Regression test for esphome/esphome#16224: a LightControlAction
# Regression test for esphome/esphome#16224: a light.turn_on action
# nested inside on_response with capture_response: true puts
# `std::string &` into the trigger's Ts..., which exposed a codegen
# bug where the apply lambda's parameter list did not match the
# ApplyFn signature.
# bug where the generated function's parameter list did not match
# the automation's arguments.
- light.turn_on:
id: test_regression_light
brightness: 100%
@@ -59,6 +59,7 @@ display:
- platform: it8951
spi_id: spi_bus
model: seeed-reterminal-e1003
update_mode: DEFAULT
cs_pin:
allow_other_uses: true
number: GPIO5
+12
View File
@@ -1,3 +1,6 @@
import functools
from esphome.components.json import enable_arena
from tests.testing_helpers import ComponentManifestOverride
@@ -7,3 +10,12 @@ def override_manifest(manifest: ComponentManifestOverride) -> None:
# library registration to happen, otherwise json_util.cpp fails to find
# ArduinoJson.h.
manifest.enable_codegen()
# The JsonArena host test needs the arena compiled in, as a consumer would request it
real_to_code = manifest.to_code
@functools.wraps(real_to_code)
async def to_code_with_arena(config):
await real_to_code(config)
enable_arena()
manifest.to_code = to_code_with_arena
+238
View File
@@ -0,0 +1,238 @@
#include <gtest/gtest.h>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include "esphome/components/json/json_util.h"
using esphome::json::JsonArena;
using esphome::json::JsonBuilder;
namespace {
constexpr size_t ALIGN = alignof(std::max_align_t);
constexpr size_t round_up(size_t n) { return (n + ALIGN - 1) & ~(ALIGN - 1); }
// Counts what the arena could not hold
struct Counting final : ArduinoJson::Allocator {
int allocs{0};
void *allocate(size_t n) override {
this->allocs++;
return malloc(n); // NOLINT
}
void deallocate(void *p) override { free(p); } // NOLINT
void *reallocate(void *p, size_t n) override {
this->allocs++;
return realloc(p, n); // NOLINT
}
};
// Refuses everything, so a spill or a move sees the heap as exhausted
struct NoMemory final : ArduinoJson::Allocator {
void *allocate(size_t) override { return nullptr; }
void deallocate(void *) override {}
void *reallocate(void *, size_t) override { return nullptr; }
};
template<size_t N> bool inside(const JsonArena<N> &arena, const void *p) {
auto base = reinterpret_cast<uintptr_t>(&arena);
auto addr = reinterpret_cast<uintptr_t>(p);
return addr >= base && addr < base + sizeof(arena);
}
} // namespace
TEST(JsonArena, BumpsAlignedInsideTheBuffer) {
JsonArena<256> arena;
auto *a = static_cast<uint8_t *>(arena.allocate(10));
auto *b = static_cast<uint8_t *>(arena.allocate(10));
ASSERT_NE(a, nullptr);
ASSERT_NE(b, nullptr);
EXPECT_TRUE(inside(arena, a));
EXPECT_TRUE(inside(arena, b));
EXPECT_EQ(reinterpret_cast<uintptr_t>(a) % ALIGN, 0u);
EXPECT_EQ(static_cast<size_t>(b - a), round_up(10));
arena.deallocate(a);
arena.deallocate(b);
}
TEST(JsonArena, SpillsToTheHeapWhenFull) {
JsonArena<64> arena;
void *a = arena.allocate(48);
void *b = arena.allocate(48);
ASSERT_NE(a, nullptr);
ASSERT_NE(b, nullptr);
EXPECT_TRUE(inside(arena, a));
EXPECT_FALSE(inside(arena, b));
std::memset(b, 'b', 48);
arena.deallocate(b); // routed to the heap; a mismatch would trip the sanitizer
arena.deallocate(a);
}
TEST(JsonArena, NewestBlockGrowsAndShrinksInPlace) {
JsonArena<256> arena;
void *a = arena.allocate(16);
std::memset(a, 'x', 16);
EXPECT_EQ(arena.reallocate(a, 96), a);
EXPECT_EQ(std::memcmp(a, "xxxxxxxxxxxxxxxx", 16), 0);
EXPECT_EQ(arena.reallocate(a, 8), a);
auto *next = static_cast<uint8_t *>(arena.allocate(8));
EXPECT_EQ(static_cast<size_t>(next - static_cast<uint8_t *>(a)), round_up(8));
}
TEST(JsonArena, NewestBlockMovesToTheHeapAndFreesItsSpace) {
JsonArena<64> arena;
void *a = arena.allocate(32);
std::memset(a, 'q', 32);
void *moved = arena.reallocate(a, 200);
ASSERT_NE(moved, nullptr);
EXPECT_FALSE(inside(arena, moved));
EXPECT_EQ(std::memcmp(moved, "qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq", 32), 0);
EXPECT_EQ(arena.allocate(16), a); // the space it left is handed out again
arena.deallocate(moved);
}
TEST(JsonArena, OlderBlockMovesToTheHeapKeepingItsBytes) {
JsonArena<256> arena;
void *a = arena.allocate(16);
std::memset(a, 'a', 16);
auto *b = static_cast<uint8_t *>(arena.allocate(16));
void *moved = arena.reallocate(a, 64);
ASSERT_NE(moved, nullptr);
EXPECT_FALSE(inside(arena, moved));
EXPECT_EQ(std::memcmp(moved, "aaaaaaaaaaaaaaaa", 16), 0);
auto *next = static_cast<uint8_t *>(arena.allocate(8));
EXPECT_EQ(static_cast<size_t>(next - b), round_up(16)); // b's space is untouched
arena.deallocate(moved);
}
TEST(JsonArena, HeapBlocksReallocateOnTheHeap) {
JsonArena<32> arena;
void *a = arena.allocate(64); // never fit
EXPECT_FALSE(inside(arena, a));
std::memset(a, 'h', 64);
void *grown = arena.reallocate(a, 128);
ASSERT_NE(grown, nullptr);
EXPECT_EQ(std::memcmp(grown, "hhhhhhhhhhhhhhhh", 16), 0);
arena.deallocate(grown);
}
TEST(JsonArena, FailedMoveKeepsTheBlockReserved) {
NoMemory no_memory;
JsonArena<64> arena(&no_memory);
void *a = arena.allocate(32);
std::memset(a, 'k', 32);
EXPECT_EQ(arena.reallocate(a, 200), nullptr);
EXPECT_EQ(std::memcmp(a, "kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk", 32), 0);
auto *b = static_cast<uint8_t *>(arena.allocate(16)); // must not hand out a's bytes again
ASSERT_NE(b, nullptr);
EXPECT_EQ(static_cast<size_t>(b - static_cast<uint8_t *>(a)), round_up(32));
EXPECT_EQ(arena.allocate(64), nullptr); // nothing left and the fallback refuses
}
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
constexpr size_t ARENA = esphome::json::JSON_ARENA_SIZE;
// The documents the event stream sends must fit without touching the fallback, and their copied
// strings must land in the headroom above the pool
TEST(JsonArena, StateDocumentsFitWithoutTouchingTheFallback) {
Counting counting;
JsonArena<ARENA> arena(&counting);
{
// A switch state event: copied id, domain and name, bool value
JsonBuilder builder(&arena);
JsonObject root = builder.root();
char id_buf[] = "switch/SSE Toggle";
char domain_buf[] = "switch";
char name_buf[] = "SSE Toggle";
root["id"] = static_cast<const char *>(id_buf);
root["domain"] = static_cast<const char *>(domain_buf);
root["name"] = static_cast<const char *>(name_buf);
root["icon"] = "";
root["entity_category"] = 0;
root["value"] = true;
root["state"] = "ON";
root["assumed_state"] = false;
char out[256];
EXPECT_LT(builder.serialize_to(out, sizeof(out)), sizeof(out));
}
EXPECT_EQ(counting.allocs, 0);
EXPECT_GT(arena.used(), esphome::json::JSON_POOL_BYTES);
Counting counting_select;
JsonArena<ARENA> select_arena(&counting_select);
{
// A 40 option select detail document: copied id, domain, name and value, linked options
JsonBuilder builder(&select_arena);
JsonObject root = builder.root();
char id_buf[] = "select/SSE Big Select";
char domain_buf[] = "select";
char name_buf[] = "SSE Big Select";
char value_buf[] = "option number 17 padded to twenty";
root["id"] = static_cast<const char *>(id_buf);
root["domain"] = static_cast<const char *>(domain_buf);
root["name"] = static_cast<const char *>(name_buf);
root["icon"] = "";
root["entity_category"] = 0;
root["value"] = static_cast<const char *>(value_buf);
root["state"] = static_cast<const char *>(value_buf);
JsonArray options = root["option"].to<JsonArray>();
char option_bufs[40][44]; // room for any int, so -Wformat-truncation stays quiet
for (int i = 0; i < 40; i++) {
snprintf(option_bufs[i], sizeof(option_bufs[i]), "option number %02d padded to twenty", i);
options.add(JsonString(option_bufs[i], true));
}
char out[2048];
EXPECT_LT(builder.serialize_to(out, sizeof(out)), sizeof(out));
}
EXPECT_EQ(counting_select.allocs, 0);
EXPECT_GT(select_arena.used(), esphome::json::JSON_POOL_BYTES);
// The same select with its 40 options copied, as the generator does before the strings are
// linked, does not fit: the headroom is sized for linked options and the rest spills
Counting counting_copied;
JsonArena<ARENA> copied_arena(&counting_copied);
{
JsonBuilder builder(&copied_arena);
JsonArray options = builder.root()["option"].to<JsonArray>();
char option_bufs[40][44]; // room for any int, so -Wformat-truncation stays quiet
for (int i = 0; i < 40; i++) {
snprintf(option_bufs[i], sizeof(option_bufs[i]), "option number %02d padded to twenty", i);
options.add(static_cast<const char *>(option_bufs[i]));
}
char out[2048];
EXPECT_LT(builder.serialize_to(out, sizeof(out)), sizeof(out));
}
EXPECT_GT(counting_copied.allocs, 0);
EXPECT_GT(copied_arena.used(), ARENA - 64); // the arena filled up before the spill began
}
TEST(JsonArena, DocumentMatchesTheHeapAllocator) {
// 700 integers need six pools, which also grows ArduinoJson's pool list past its preallocated four
auto build = [](JsonBuilder &builder) {
JsonArray arr = builder.root()["a"].to<JsonArray>();
for (int i = 0; i < 700; i++) {
arr.add(i);
}
JsonArray strings = builder.root()["s"].to<JsonArray>();
char buf[40]; // room for any int, so -Wformat-truncation stays quiet
for (int i = 0; i < 60; i++) {
snprintf(buf, sizeof(buf), "string number %04d padded", i);
strings.add(buf);
}
};
JsonArena<ARENA> arena;
JsonBuilder with_arena(&arena);
build(with_arena);
JsonBuilder with_heap;
build(with_heap);
std::string a = with_arena.serialize();
std::string b = with_heap.serialize();
EXPECT_GT(a.size(), 4000u);
EXPECT_EQ(a, b);
}
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
@@ -0,0 +1,17 @@
button:
- platform: template
name: Menu Actions
on_press:
- display_menu.show: test_lcd_menu
- display_menu.up: test_lcd_menu
- display_menu.down: test_lcd_menu
- display_menu.left: test_lcd_menu
- display_menu.right: test_lcd_menu
- display_menu.enter: test_lcd_menu
- if:
condition:
display_menu.is_active: test_lcd_menu
then:
- display_menu.hide: test_lcd_menu
else:
- display_menu.show_main: test_lcd_menu
@@ -0,0 +1,11 @@
substitutions:
d0_pin: GPIO0
d1_pin: GPIO2
d2_pin: GPIO14
d3_pin: GPIO15
enable_pin: GPIO16
rs_pin: GPIO5
packages:
lcd_menu: !include common.yaml
actions: !include common-actions.yaml
@@ -0,0 +1,10 @@
button:
- platform: template
name: LD2410 Actions
on_press:
- bluetooth_password.set:
id: my_ld2410
password: HiLink
- bluetooth_password.set:
id: my_ld2410
password: !lambda return "HiLink";
@@ -0,0 +1,8 @@
substitutions:
tx_pin: GPIO0
rx_pin: GPIO2
packages:
uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml
ld2410: !include common.yaml
actions: !include common-actions.yaml
+3
View File
@@ -4,6 +4,9 @@ esphome:
- output.ledc.set_frequency:
id: test_ledc
frequency: 100Hz
- output.ledc.set_frequency:
id: test_ledc
frequency: !lambda return 200.0f;
output:
- platform: ledc

Some files were not shown because too many files have changed in this diff Show More