mirror of
https://github.com/esphome/esphome.git
synced 2026-10-07 03:16:37 +00:00
Merge branch 'neutral-ble-client' into radon-eye-single-node
This commit is contained in:
@@ -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;
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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,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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
Reference in New Issue
Block a user