Merge branch 'esphome:dev' into gsm

This commit is contained in:
Olivier ARCHER 2024-07-15 14:39:02 +02:00 committed by GitHub
commit 321594ae3d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
1963 changed files with 3551 additions and 13656 deletions

View file

@ -1,7 +1,9 @@
{
"name": "ESPHome Dev",
"image": "ghcr.io/esphome/esphome-lint:dev",
"postCreateCommand": ["script/devcontainer-post-create"],
"postCreateCommand": [
"script/devcontainer-post-create"
],
"containerEnv": {
"DEVCONTAINER": "1",
"PIP_BREAK_SYSTEM_PACKAGES": "1",
@ -27,6 +29,9 @@
"extensions": [
// python
"ms-python.python",
"ms-python.pylint",
"ms-python.flake8",
"ms-python.black-formatter",
"visualstudioexptteam.vscodeintellicode",
// yaml
"redhat.vscode-yaml",
@ -38,9 +43,21 @@
"settings": {
"python.languageServer": "Pylance",
"python.pythonPath": "/usr/bin/python3",
"python.linting.pylintEnabled": true,
"python.linting.enabled": true,
"python.formatting.provider": "black",
"pylint.args": [
"--rcfile=${workspaceFolder}/pyproject.toml"
],
"flake8.args": [
"--config=${workspaceFolder}/.flake8"
],
"black-formatter.args": [
"--config",
"${workspaceFolder}/pyproject.toml"
],
"[python]": {
// VS will say "Value is not accepted" before building the devcontainer, but the warning
// should go away after build is completed.
"editor.defaultFormatter": "ms-python.black-formatter"
},
"editor.formatOnPaste": false,
"editor.formatOnSave": true,
"editor.formatOnType": true,

View file

@ -46,7 +46,7 @@ runs:
- name: Build and push to ghcr by digest
id: build-ghcr
uses: docker/build-push-action@v5.4.0
uses: docker/build-push-action@v6.3.0
with:
context: .
file: ./docker/Dockerfile
@ -69,7 +69,7 @@ runs:
- name: Build and push to dockerhub by digest
id: build-dockerhub
uses: docker/build-push-action@v5.4.0
uses: docker/build-push-action@v6.3.0
with:
context: .
file: ./docker/Dockerfile

View file

@ -17,7 +17,7 @@ runs:
steps:
- name: Set up Python ${{ inputs.python-version }}
id: python
uses: actions/setup-python@v5.1.0
uses: actions/setup-python@v5.1.1
with:
python-version: ${{ inputs.python-version }}
- name: Restore Python virtual environment

View file

@ -46,9 +46,9 @@ jobs:
with:
python-version: "3.9"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.3.0
uses: docker/setup-buildx-action@v3.4.0
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.0.0
uses: docker/setup-qemu-action@v3.1.0
- name: Set TAG
run: |

View file

@ -248,72 +248,6 @@ jobs:
run: script/ci-suggest-changes
if: always()
compile-tests-list:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.7
- name: Find all YAML test files
id: set-matrix
run: echo "matrix=$(ls tests/test*.yaml | jq -R -s -c 'split("\n")[:-1]')" >> $GITHUB_OUTPUT
validate-tests:
name: Validate YAML test ${{ matrix.file }}
runs-on: ubuntu-latest
needs:
- common
- compile-tests-list
strategy:
fail-fast: false
matrix:
file: ${{ fromJson(needs.compile-tests-list.outputs.matrix) }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.7
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Run esphome config ${{ matrix.file }}
run: |
. venv/bin/activate
esphome config ${{ matrix.file }}
compile-tests:
name: Run YAML test ${{ matrix.file }}
runs-on: ubuntu-latest
needs:
- common
- black
- ci-custom
- clang-format
- flake8
- pylint
- pytest
- pyupgrade
- compile-tests-list
- validate-tests
strategy:
fail-fast: false
max-parallel: 2
matrix:
file: ${{ fromJson(needs.compile-tests-list.outputs.matrix) }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.7
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Run esphome compile ${{ matrix.file }}
run: |
. venv/bin/activate
esphome compile ${{ matrix.file }}
clang-tidy:
name: ${{ matrix.name }}
runs-on: ubuntu-latest
@ -550,7 +484,6 @@ jobs:
- pylint
- pytest
- pyupgrade
- compile-tests
- clang-tidy
- list-components
- test-build-components

View file

@ -65,7 +65,7 @@ jobs:
pip3 install build
python3 -m build
- name: Publish
uses: pypa/gh-action-pypi-publish@v1.8.14
uses: pypa/gh-action-pypi-publish@v1.9.0
deploy-docker:
name: Build ESPHome ${{ matrix.platform }}
@ -90,10 +90,10 @@ jobs:
python-version: "3.9"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.3.0
uses: docker/setup-buildx-action@v3.4.0
- name: Set up QEMU
if: matrix.platform != 'linux/amd64'
uses: docker/setup-qemu-action@v3.0.0
uses: docker/setup-qemu-action@v3.1.0
- name: Log in to docker hub
uses: docker/login-action@v3.2.0
@ -141,7 +141,7 @@ jobs:
echo name=$(cat /tmp/platform) >> $GITHUB_OUTPUT
- name: Upload digests
uses: actions/upload-artifact@v4.3.3
uses: actions/upload-artifact@v4.3.4
with:
name: digests-${{ steps.sanitize.outputs.name }}
path: /tmp/digests
@ -177,14 +177,14 @@ jobs:
- uses: actions/checkout@v4.1.7
- name: Download digests
uses: actions/download-artifact@v4.1.7
uses: actions/download-artifact@v4.1.8
with:
pattern: digests-*
path: /tmp/digests
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.3.0
uses: docker/setup-buildx-action@v3.4.0
- name: Log in to docker hub
if: matrix.registry == 'dockerhub'

View file

@ -36,7 +36,7 @@ jobs:
python ./script/sync-device_class.py
- name: Commit changes
uses: peter-evans/create-pull-request@v6.0.5
uses: peter-evans/create-pull-request@v6.1.0
with:
commit-message: "Synchronise Device Classes from Home Assistant"
committer: esphomebot <esphome@nabucasa.com>

View file

@ -214,7 +214,7 @@ esphome/components/lightwaverf/* @max246
esphome/components/lilygo_t5_47/touchscreen/* @jesserockz
esphome/components/lock/* @esphome/core
esphome/components/logger/* @esphome/core
esphome/components/ltr390/* @sjtrny
esphome/components/ltr390/* @latonita @sjtrny
esphome/components/ltr_als_ps/* @latonita
esphome/components/matrix_keypad/* @ssieb
esphome/components/max31865/* @DAVe3283

View file

@ -34,28 +34,32 @@ RUN \
python3-wheel=0.38.4-2 \
iputils-ping=3:20221126-1 \
git=1:2.39.2-1.1 \
curl=7.88.1-10+deb12u5 \
curl=7.88.1-10+deb12u6 \
openssh-client=1:9.2p1-2+deb12u2 \
python3-cffi=1.15.1-5 \
libcairo2=1.16.0-7 \
libmagic1=1:5.44-3 \
patch=2.7.6-7; \
if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \
patch=2.7.6-7 \
&& ( \
( \
[ "$TARGETARCH$TARGETVARIANT" = "armv7" ] && \
apt-get install -y --no-install-recommends \
build-essential=12.9 \
python3-dev=3.11.2-1+b1 \
zlib1g-dev=1:1.2.13.dfsg-1 \
libjpeg-dev=1:2.1.5-2 \
libfreetype-dev=2.12.1+dfsg-5 \
libssl-dev=3.0.11-1~deb12u2 \
libfreetype-dev=2.12.1+dfsg-5+deb12u3 \
libssl-dev=3.0.13-1~deb12u1 \
libffi-dev=3.4.4-1 \
libopenjp2-7=2.5.0-2 \
libtiff6=4.5.0-6+deb12u1 \
cargo=0.66.0+ds1-1 \
pkg-config=1.8.1-1 \
gcc-arm-linux-gnueabihf=4:12.2.0-3; \
fi; \
rm -rf \
gcc-arm-linux-gnueabihf=4:12.2.0-3 \
) \
|| [ "$TARGETARCH$TARGETVARIANT" != "armv7" ] \
) \
&& rm -rf \
/tmp/* \
/var/{cache,log}/* \
/var/lib/apt/lists/*
@ -190,8 +194,8 @@ RUN \
clang-format-13=1:13.0.1-11+b2 \
clang-tidy-14=1:14.0.6-12 \
patch=2.7.6-7 \
software-properties-common=0.99.30-4 \
nano=7.2-1 \
software-properties-common=0.99.30-4.1~deb12u1 \
nano=7.2-1+deb12u1 \
build-essential=12.9 \
python3-dev=3.11.2-1+b1 \
&& rm -rf \

View file

@ -60,6 +60,7 @@ from esphome.cpp_types import ( # noqa
std_ns,
std_shared_ptr,
std_string,
std_string_ref,
std_vector,
uint8,
uint16,

View file

@ -93,8 +93,9 @@ void AHT10Component::restart_read_() {
void AHT10Component::read_data_() {
uint8_t data[6];
if (this->read_count_ > 1)
if (this->read_count_ > 1) {
ESP_LOGD(TAG, "Read attempt %d at %ums", this->read_count_, (unsigned) (millis() - this->start_time_));
}
if (this->read(data, 6) != i2c::ERROR_OK) {
this->status_set_warning("AHT10 read failed, retrying soon");
this->restart_read_();
@ -119,8 +120,9 @@ void AHT10Component::read_data_() {
return;
}
}
if (this->read_count_ > 1)
if (this->read_count_ > 1) {
ESP_LOGD(TAG, "Success at %ums", (unsigned) (millis() - this->start_time_));
}
uint32_t raw_temperature = ((data[3] & 0x0F) << 16) | (data[4] << 8) | data[5];
uint32_t raw_humidity = ((data[1] << 16) | (data[2] << 8) | data[3]) >> 4;

View file

@ -1,7 +1,7 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c, esp32
from esphome.const import CONF_ID, CONF_TEMPERATURE_OFFSET
from esphome.const import CONF_ID, CONF_SAMPLE_RATE, CONF_TEMPERATURE_OFFSET
CODEOWNERS = ["@trvrnrth"]
DEPENDENCIES = ["i2c"]
@ -11,7 +11,6 @@ MULTI_CONF = True
CONF_BME680_BSEC_ID = "bme680_bsec_id"
CONF_IAQ_MODE = "iaq_mode"
CONF_SUPPLY_VOLTAGE = "supply_voltage"
CONF_SAMPLE_RATE = "sample_rate"
CONF_STATE_SAVE_INTERVAL = "state_save_interval"
bme680_bsec_ns = cg.esphome_ns.namespace("bme680_bsec")

View file

@ -4,33 +4,33 @@ from esphome.components import sensor
from esphome.const import (
CONF_GAS_RESISTANCE,
CONF_HUMIDITY,
CONF_IAQ_ACCURACY,
CONF_PRESSURE,
CONF_SAMPLE_RATE,
CONF_TEMPERATURE,
DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
DEVICE_CLASS_CARBON_DIOXIDE,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
ICON_GAS_CYLINDER,
ICON_GAUGE,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
UNIT_HECTOPASCAL,
UNIT_OHM,
UNIT_PARTS_PER_MILLION,
UNIT_PERCENT,
ICON_GAS_CYLINDER,
ICON_GAUGE,
)
from . import (
BME680BSECComponent,
CONF_BME680_BSEC_ID,
CONF_SAMPLE_RATE,
SAMPLE_RATE_OPTIONS,
)
DEPENDENCIES = ["bme680_bsec"]
CONF_IAQ = "iaq"
CONF_IAQ_ACCURACY = "iaq_accuracy"
CONF_CO2_EQUIVALENT = "co2_equivalent"
CONF_BREATH_VOC_EQUIVALENT = "breath_voc_equivalent"
UNIT_IAQ = "IAQ"

View file

@ -1,11 +1,11 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import text_sensor
from esphome.const import CONF_IAQ_ACCURACY
from . import BME680BSECComponent, CONF_BME680_BSEC_ID
DEPENDENCIES = ["bme680_bsec"]
CONF_IAQ_ACCURACY = "iaq_accuracy"
ICON_ACCURACY = "mdi:checkbox-marked-circle-outline"
TYPES = [CONF_IAQ_ACCURACY]

View file

@ -574,21 +574,25 @@ void Climate::dump_traits_(const char *tag) {
ESP_LOGCONFIG(tag, " - Max temperature: %.1f", traits.get_visual_max_temperature());
ESP_LOGCONFIG(tag, " - Temperature step:");
ESP_LOGCONFIG(tag, " Target: %.1f", traits.get_visual_target_temperature_step());
if (traits.get_supports_current_temperature()) {
ESP_LOGCONFIG(tag, " Current: %.1f", traits.get_visual_current_temperature_step());
}
if (traits.get_supports_target_humidity() || traits.get_supports_current_humidity()) {
ESP_LOGCONFIG(tag, " - Min humidity: %.0f", traits.get_visual_min_humidity());
ESP_LOGCONFIG(tag, " - Max humidity: %.0f", traits.get_visual_max_humidity());
if (traits.get_supports_current_temperature()) {
ESP_LOGCONFIG(tag, " [x] Supports current temperature");
}
if (traits.get_supports_current_humidity()) {
ESP_LOGCONFIG(tag, " [x] Supports current humidity");
}
if (traits.get_supports_two_point_target_temperature()) {
ESP_LOGCONFIG(tag, " [x] Supports two-point target temperature");
}
if (traits.get_supports_current_temperature()) {
ESP_LOGCONFIG(tag, " [x] Supports current temperature");
}
if (traits.get_supports_target_humidity()) {
ESP_LOGCONFIG(tag, " [x] Supports target humidity");
}
if (traits.get_supports_current_humidity()) {
ESP_LOGCONFIG(tag, " [x] Supports current humidity");
}
if (traits.get_supports_action()) {
ESP_LOGCONFIG(tag, " [x] Supports action");
}

View file

@ -73,7 +73,7 @@ class ClimateTraits {
ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
void set_supports_dry_mode(bool supports_dry_mode) { set_mode_support_(CLIMATE_MODE_DRY, supports_dry_mode); }
bool supports_mode(ClimateMode mode) const { return supported_modes_.count(mode); }
std::set<ClimateMode> get_supported_modes() const { return supported_modes_; }
const std::set<ClimateMode> &get_supported_modes() const { return supported_modes_; }
void set_supports_action(bool supports_action) { supports_action_ = supports_action; }
bool get_supports_action() const { return supports_action_; }
@ -101,7 +101,7 @@ class ClimateTraits {
void set_supports_fan_mode_diffuse(bool supported) { set_fan_mode_support_(CLIMATE_FAN_DIFFUSE, supported); }
bool supports_fan_mode(ClimateFanMode fan_mode) const { return supported_fan_modes_.count(fan_mode); }
bool get_supports_fan_modes() const { return !supported_fan_modes_.empty() || !supported_custom_fan_modes_.empty(); }
std::set<ClimateFanMode> get_supported_fan_modes() const { return supported_fan_modes_; }
const std::set<ClimateFanMode> &get_supported_fan_modes() const { return supported_fan_modes_; }
void set_supported_custom_fan_modes(std::set<std::string> supported_custom_fan_modes) {
supported_custom_fan_modes_ = std::move(supported_custom_fan_modes);
@ -140,7 +140,7 @@ class ClimateTraits {
}
bool supports_swing_mode(ClimateSwingMode swing_mode) const { return supported_swing_modes_.count(swing_mode); }
bool get_supports_swing_modes() const { return !supported_swing_modes_.empty(); }
std::set<ClimateSwingMode> get_supported_swing_modes() const { return supported_swing_modes_; }
const std::set<ClimateSwingMode> &get_supported_swing_modes() const { return supported_swing_modes_; }
float get_visual_min_temperature() const { return visual_min_temperature_; }
void set_visual_min_temperature(float visual_min_temperature) { visual_min_temperature_ = visual_min_temperature; }

View file

@ -129,13 +129,13 @@ class Cover : public EntityBase, public EntityBase_DeviceClass {
*
* This is a legacy method and may be removed later, please use `.make_call()` instead.
*/
ESPDEPRECATED("open() is deprecated, use make_call().set_command_open() instead.", "2021.9")
ESPDEPRECATED("open() is deprecated, use make_call().set_command_open().perform() instead.", "2021.9")
void open();
/** Close the cover.
*
* This is a legacy method and may be removed later, please use `.make_call()` instead.
*/
ESPDEPRECATED("close() is deprecated, use make_call().set_command_close() instead.", "2021.9")
ESPDEPRECATED("close() is deprecated, use make_call().set_command_close().perform() instead.", "2021.9")
void close();
/** Stop the cover.
*

View file

@ -145,10 +145,8 @@ bool DallasTemperatureSensor::check_scratch_pad_() {
float DallasTemperatureSensor::get_temp_c_() {
int16_t temp = (this->scratch_pad_[1] << 8) | this->scratch_pad_[0];
if ((this->address_ & 0xff) == DALLAS_MODEL_DS18S20) {
if (this->scratch_pad_[7] != 0x10)
ESP_LOGE(TAG, "unexpected COUNT_PER_C value: %u", this->scratch_pad_[7]);
temp = ((temp & 0xfff7) << 3) + (0x10 - this->scratch_pad_[6]) - 4;
} else {
return (temp >> 1) + (this->scratch_pad_[7] - this->scratch_pad_[6]) / float(this->scratch_pad_[7]) - 0.25;
}
switch (this->resolution_) {
case 9:
temp &= 0xfff8;
@ -163,7 +161,6 @@ float DallasTemperatureSensor::get_temp_c_() {
default:
break;
}
}
return temp / 16.0f;
}

View file

@ -12,7 +12,7 @@ std::string DebugComponent::get_reset_reason_() { return lt_get_reboot_reason_na
uint32_t DebugComponent::get_free_heap_() { return lt_heap_get_free(); }
void DebugComponent::get_device_info_(std::string &device_info) {
str::string reset_reason = get_reset_reason_();
std::string reset_reason = get_reset_reason_();
ESP_LOGD(TAG, "LibreTiny Version: %s", lt_get_version());
ESP_LOGD(TAG, "Chip: %s (%04x) @ %u MHz", lt_cpu_get_model_name(), lt_cpu_get_model(), lt_cpu_get_freq_mhz());
ESP_LOGD(TAG, "Chip ID: 0x%06X", lt_cpu_get_mac_id());

View file

@ -37,14 +37,18 @@ void DS1307Component::read_time() {
ESP_LOGW(TAG, "RTC halted, not syncing to system clock.");
return;
}
ESPTime rtc_time{.second = uint8_t(ds1307_.reg.second + 10 * ds1307_.reg.second_10),
ESPTime rtc_time{
.second = uint8_t(ds1307_.reg.second + 10 * ds1307_.reg.second_10),
.minute = uint8_t(ds1307_.reg.minute + 10u * ds1307_.reg.minute_10),
.hour = uint8_t(ds1307_.reg.hour + 10u * ds1307_.reg.hour_10),
.day_of_week = uint8_t(ds1307_.reg.weekday),
.day_of_month = uint8_t(ds1307_.reg.day + 10u * ds1307_.reg.day_10),
.day_of_year = 1, // ignored by recalc_timestamp_utc(false)
.month = uint8_t(ds1307_.reg.month + 10u * ds1307_.reg.month_10),
.year = uint16_t(ds1307_.reg.year + 10u * ds1307_.reg.year_10 + 2000)};
.year = uint16_t(ds1307_.reg.year + 10u * ds1307_.reg.year_10 + 2000),
.is_dst = false, // not used
.timestamp = 0 // overwritten by recalc_timestamp_utc(false)
};
rtc_time.recalc_timestamp_utc(false);
if (!rtc_time.is_valid()) {
ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock.");

View file

@ -96,16 +96,16 @@ def get_board(core_obj=None):
def get_download_types(storage_json):
return [
{
"title": "Modern format",
"title": "Factory format (Previously Modern)",
"description": "For use with ESPHome Web and other tools.",
"file": "firmware-factory.bin",
"download": f"{storage_json.name}-factory.bin",
"file": "firmware.factory.bin",
"download": f"{storage_json.name}.factory.bin",
},
{
"title": "Legacy format",
"description": "For use with ESPHome Flasher.",
"file": "firmware.bin",
"download": f"{storage_json.name}.bin",
"title": "OTA format (Previously Legacy)",
"description": "For OTA updating a device.",
"file": "firmware.ota.bin",
"download": f"{storage_json.name}.ota.bin",
},
]

View file

@ -17,17 +17,19 @@ from SCons.Script import ARGUMENTS
# Copy over the default sdkconfig.
from os import path
if path.exists("./sdkconfig.defaults"):
os.makedirs(".temp", exist_ok=True)
shutil.copy("./sdkconfig.defaults", "./.temp/sdkconfig-esp32-idf")
def esp32_create_combined_bin(source, target, env):
verbose = bool(int(ARGUMENTS.get("PIOVERBOSE", "0")))
if verbose:
print("Generating combined binary for serial flashing")
app_offset = 0x10000
new_file_name = env.subst("$BUILD_DIR/${PROGNAME}-factory.bin")
new_file_name = env.subst("$BUILD_DIR/${PROGNAME}.factory.bin")
sections = env.subst(env.get("FLASH_EXTRA_IMAGES"))
firmware_name = env.subst("$BUILD_DIR/${PROGNAME}.bin")
chip = env.get("BOARD_MCU")
@ -62,5 +64,14 @@ def esp32_create_combined_bin(source, target, env):
else:
subprocess.run(["esptool.py", *cmd])
def esp32_copy_ota_bin(source, target, env):
firmware_name = env.subst("$BUILD_DIR/${PROGNAME}.bin")
new_file_name = env.subst("$BUILD_DIR/${PROGNAME}.ota.bin")
shutil.copyfile(firmware_name, new_file_name)
# pylint: disable=E0602
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_create_combined_bin) # noqa
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_copy_ota_bin) # noqa

View file

@ -11,6 +11,7 @@ from esphome.components.esp32.const import (
VARIANT_ESP32S2,
VARIANT_ESP32S3,
VARIANT_ESP32C3,
VARIANT_ESP32C6,
VARIANT_ESP32H2,
)
@ -47,6 +48,7 @@ CAN_SPEEDS_ESP32_S2 = {
CAN_SPEEDS_ESP32_S3 = {**CAN_SPEEDS_ESP32_S2}
CAN_SPEEDS_ESP32_C3 = {**CAN_SPEEDS_ESP32_S2}
CAN_SPEEDS_ESP32_C6 = {**CAN_SPEEDS_ESP32_S2}
CAN_SPEEDS_ESP32_H2 = {**CAN_SPEEDS_ESP32_S2}
CAN_SPEEDS = {
@ -54,6 +56,7 @@ CAN_SPEEDS = {
VARIANT_ESP32S2: CAN_SPEEDS_ESP32_S2,
VARIANT_ESP32S3: CAN_SPEEDS_ESP32_S3,
VARIANT_ESP32C3: CAN_SPEEDS_ESP32_C3,
VARIANT_ESP32C6: CAN_SPEEDS_ESP32_C6,
VARIANT_ESP32H2: CAN_SPEEDS_ESP32_H2,
}

View file

@ -6,10 +6,18 @@ Import("env") # noqa
def esp8266_copy_factory_bin(source, target, env):
firmware_name = env.subst("$BUILD_DIR/${PROGNAME}.bin")
new_file_name = env.subst("$BUILD_DIR/${PROGNAME}-factory.bin")
new_file_name = env.subst("$BUILD_DIR/${PROGNAME}.factory.bin")
shutil.copyfile(firmware_name, new_file_name)
def esp8266_copy_ota_bin(source, target, env):
firmware_name = env.subst("$BUILD_DIR/${PROGNAME}.bin")
new_file_name = env.subst("$BUILD_DIR/${PROGNAME}.ota.bin")
shutil.copyfile(firmware_name, new_file_name)
# pylint: disable=E0602
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp8266_copy_factory_bin) # noqa
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp8266_copy_ota_bin) # noqa

View file

@ -1,10 +1,17 @@
import logging
import esphome.codegen as cg
import esphome.config_validation as cv
import esphome.final_validate as fv
from esphome.components.ota import BASE_OTA_SCHEMA, ota_to_code, OTAComponent
from esphome.config_helpers import merge_config
from esphome.const import (
CONF_ESPHOME,
CONF_ID,
CONF_NUM_ATTEMPTS,
CONF_OTA,
CONF_PASSWORD,
CONF_PLATFORM,
CONF_PORT,
CONF_REBOOT_TIMEOUT,
CONF_SAFE_MODE,
@ -12,6 +19,8 @@ from esphome.const import (
)
from esphome.core import coroutine_with_priority
_LOGGER = logging.getLogger(__name__)
CODEOWNERS = ["@esphome/core"]
AUTO_LOAD = ["md5", "socket"]
@ -21,6 +30,65 @@ esphome = cg.esphome_ns.namespace("esphome")
ESPHomeOTAComponent = esphome.class_("ESPHomeOTAComponent", OTAComponent)
def ota_esphome_final_validate(config):
full_conf = fv.full_config.get()
full_ota_conf = full_conf[CONF_OTA]
new_ota_conf = []
merged_ota_esphome_configs_by_port = {}
ports_with_merged_configs = []
for ota_conf in full_ota_conf:
if ota_conf.get(CONF_PLATFORM) == CONF_ESPHOME:
if (
conf_port := ota_conf.get(CONF_PORT)
) not in merged_ota_esphome_configs_by_port:
merged_ota_esphome_configs_by_port[conf_port] = ota_conf
else:
if merged_ota_esphome_configs_by_port[conf_port][
CONF_VERSION
] != ota_conf.get(CONF_VERSION):
raise cv.Invalid(
f"Found multiple configurations but {CONF_VERSION} is inconsistent"
)
if (
merged_ota_esphome_configs_by_port[conf_port][CONF_ID].is_manual
and ota_conf.get(CONF_ID).is_manual
):
raise cv.Invalid(
f"Found multiple configurations but {CONF_ID} is inconsistent"
)
if (
CONF_PASSWORD in merged_ota_esphome_configs_by_port[conf_port]
and CONF_PASSWORD in ota_conf
and merged_ota_esphome_configs_by_port[conf_port][CONF_PASSWORD]
!= ota_conf.get(CONF_PASSWORD)
):
raise cv.Invalid(
f"Found multiple configurations but {CONF_PASSWORD} is inconsistent"
)
ports_with_merged_configs.append(conf_port)
merged_ota_esphome_configs_by_port[conf_port] = merge_config(
merged_ota_esphome_configs_by_port[conf_port], ota_conf
)
else:
new_ota_conf.append(ota_conf)
for port_conf in merged_ota_esphome_configs_by_port.values():
new_ota_conf.append(port_conf)
full_conf[CONF_OTA] = new_ota_conf
fv.full_config.set(full_conf)
if len(ports_with_merged_configs) > 0:
_LOGGER.warning(
"Found and merged multiple configurations for %s %s %s port(s) %s",
CONF_OTA,
CONF_PLATFORM,
CONF_ESPHOME,
ports_with_merged_configs,
)
CONFIG_SCHEMA = (
cv.Schema(
{
@ -50,6 +118,8 @@ CONFIG_SCHEMA = (
.extend(cv.COMPONENT_SCHEMA)
)
FINAL_VALIDATE_SCHEMA = ota_esphome_final_validate
@coroutine_with_priority(52.0)
async def to_code(config):

View file

@ -65,7 +65,8 @@ void EthernetComponent::setup() {
.intr_flags = 0,
};
#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || \
defined(USE_ESP32_VARIANT_ESP32C6)
auto host = SPI2_HOST;
#else
auto host = SPI3_HOST;
@ -393,7 +394,7 @@ void EthernetComponent::got_ip_event_handler(void *arg, esp_event_base_t event_b
const esp_netif_ip_info_t *ip_info = &event->ip_info;
ESP_LOGV(TAG, "[Ethernet event] ETH Got IP " IPSTR, IP2STR(&ip_info->ip));
global_eth_component->got_ipv4_address_ = true;
#if USE_NETWORK_IPV6
#if USE_NETWORK_IPV6 && (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0)
global_eth_component->connected_ = global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT;
#else
global_eth_component->connected_ = true;
@ -406,8 +407,12 @@ void EthernetComponent::got_ip6_event_handler(void *arg, esp_event_base_t event_
ip_event_got_ip6_t *event = (ip_event_got_ip6_t *) event_data;
ESP_LOGV(TAG, "[Ethernet event] ETH Got IPv6: " IPV6STR, IPV62STR(event->ip6_info.ip));
global_eth_component->ipv6_count_ += 1;
#if (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0)
global_eth_component->connected_ =
global_eth_component->got_ipv4_address_ && (global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT);
#else
global_eth_component->connected_ = global_eth_component->got_ipv4_address_;
#endif
}
#endif /* USE_NETWORK_IPV6 */
@ -631,7 +636,7 @@ void EthernetComponent::write_phy_register_(esp_eth_mac_t *mac, PHYRegister regi
ESPHL_ERROR_CHECK(err, "Writing PHY Register failed");
if (this->type_ == ETHERNET_TYPE_RTL8201 && register_data.page) {
ESP_LOGD(TAG, "Select PHY Register Page 0x%02" PRIX32, 0x0);
ESP_LOGD(TAG, "Select PHY Register Page 0x00");
err = mac->write_phy_reg(mac, this->phy_addr_, eth_phy_psr_reg_addr, 0x0);
ESPHL_ERROR_CHECK(err, "Select PHY Register Page 0 failed");
}

View file

@ -17,7 +17,6 @@ from esphome.helpers import (
cpp_string_escape,
)
from esphome.const import (
__version__,
CONF_FAMILY,
CONF_FILE,
CONF_GLYPHS,
@ -185,31 +184,6 @@ def get_font_path(value, type) -> Path:
return None
def download_content(url: str, path: Path) -> None:
if not external_files.has_remote_file_changed(url, path):
_LOGGER.debug("Remote file has not changed %s", url)
return
_LOGGER.debug(
"Remote file has changed, downloading from %s to %s",
url,
path,
)
try:
req = requests.get(
url,
timeout=external_files.NETWORK_TIMEOUT,
headers={"User-agent": f"ESPHome/{__version__} (https://esphome.io)"},
)
req.raise_for_status()
except requests.exceptions.RequestException as e:
raise cv.Invalid(f"Could not download from {url}: {e}")
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(req.content)
def download_gfont(value):
name = (
f"{value[CONF_FAMILY]}:ital,wght@{int(value[CONF_ITALIC])},{value[CONF_WEIGHT]}"
@ -236,7 +210,7 @@ def download_gfont(value):
ttf_url = match.group(1)
_LOGGER.debug("download_gfont: ttf_url=%s", ttf_url)
download_content(ttf_url, path)
external_files.download_content(ttf_url, path)
return value
@ -244,7 +218,7 @@ def download_web_font(value):
url = value[CONF_URL]
path = get_font_path(value, TYPE_WEB)
download_content(url, path)
external_files.download_content(url, path)
_LOGGER.debug("download_web_font: path=%s", path)
return value

View file

@ -9,6 +9,10 @@ static const char *const TAG = "gpio.one_wire";
void GPIOOneWireBus::setup() {
ESP_LOGCONFIG(TAG, "Setting up 1-wire bus...");
this->t_pin_->setup();
// clear bus with 480µs high, otherwise initial reset in search might fail
this->t_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP);
delayMicroseconds(480);
this->search();
}
@ -60,7 +64,7 @@ void HOT IRAM_ATTR GPIOOneWireBus::write_bit_(bool bit) {
// recovery time: t_rec: min=1µs
// ds18b20 appears to read the bus after roughly 14µs
uint32_t delay0 = bit ? 6 : 60;
uint32_t delay1 = bit ? 54 : 5;
uint32_t delay1 = bit ? 59 : 5;
// delay A/C
delayMicroseconds(delay0);
@ -90,13 +94,15 @@ bool HOT IRAM_ATTR GPIOOneWireBus::read_bit_() {
// measure from start value directly, to get best accurate timing no matter
// how long pin_mode/delayMicroseconds took
delayMicroseconds(12 - (micros() - start));
uint32_t now = micros();
if (now - start < 12)
delayMicroseconds(12 - (now - start));
// sample bus to read bit from peer
bool r = pin_.digital_read();
// read slot is at least 60µs; get as close to 60µs to spend less time with interrupts locked
uint32_t now = micros();
now = micros();
if (now - start < 60)
delayMicroseconds(60 - (now - start));

View file

@ -16,6 +16,7 @@ MODELS = {
"yan": Model.GREE_YAN,
"yaa": Model.GREE_YAA,
"yac": Model.GREE_YAC,
"yac1fb9": Model.GREE_YAC1FB9,
}
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(

View file

@ -24,7 +24,7 @@ void GreeClimate::transmit_state() {
remote_state[4] |= (this->horizontal_swing_() << 4);
}
if (this->model_ == GREE_YAA || this->model_ == GREE_YAC) {
if (this->model_ == GREE_YAA || this->model_ == GREE_YAC || this->model_ == GREE_YAC1FB9) {
remote_state[2] = 0x20; // bits 0..3 always 0000, bits 4..7 TURBO,LIGHT,HEALTH,X-FAN
remote_state[3] = 0x50; // bits 4..7 always 0101
remote_state[6] = 0x20; // YAA1FB, FAA1FB1, YB1F2 bits 4..7 always 0010
@ -53,7 +53,11 @@ void GreeClimate::transmit_state() {
data->set_carrier_frequency(GREE_IR_FREQUENCY);
data->mark(GREE_HEADER_MARK);
if (this->model_ == GREE_YAC1FB9) {
data->space(GREE_YAC1FB9_HEADER_SPACE);
} else {
data->space(GREE_HEADER_SPACE);
}
for (int i = 0; i < 4; i++) {
for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask
@ -71,7 +75,11 @@ void GreeClimate::transmit_state() {
data->space(GREE_ZERO_SPACE);
data->mark(GREE_BIT_MARK);
if (this->model_ == GREE_YAC1FB9) {
data->space(GREE_YAC1FB9_MESSAGE_SPACE);
} else {
data->space(GREE_MESSAGE_SPACE);
}
for (int i = 4; i < 8; i++) {
for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask

View file

@ -41,6 +41,10 @@ const uint32_t GREE_YAC_HEADER_MARK = 6000;
const uint32_t GREE_YAC_HEADER_SPACE = 3000;
const uint32_t GREE_YAC_BIT_MARK = 650;
// Timing specific to YAC1FB9
const uint32_t GREE_YAC1FB9_HEADER_SPACE = 4500;
const uint32_t GREE_YAC1FB9_MESSAGE_SPACE = 19980;
// State Frame size
const uint8_t GREE_STATE_FRAME_SIZE = 8;
@ -67,7 +71,7 @@ const uint8_t GREE_HDIR_MRIGHT = 0x05;
const uint8_t GREE_HDIR_RIGHT = 0x06;
// Model codes
enum Model { GREE_GENERIC, GREE_YAN, GREE_YAA, GREE_YAC };
enum Model { GREE_GENERIC, GREE_YAN, GREE_YAA, GREE_YAC, GREE_YAC1FB9 };
class GreeClimate : public climate_ir::ClimateIR {
public:

View file

@ -56,7 +56,7 @@ SENSOR_TYPES = {
CONFIG_SCHEMA = cv.Schema(
{
cv.Required(CONF_HAIER_ID): cv.use_id(HonClimate),
cv.GenerateID(CONF_HAIER_ID): cv.use_id(HonClimate),
}
).extend({cv.Optional(type): schema for type, schema in SENSOR_TYPES.items()})
@ -64,8 +64,8 @@ CONFIG_SCHEMA = cv.Schema(
async def to_code(config):
paren = await cg.get_variable(config[CONF_HAIER_ID])
for type, _ in SENSOR_TYPES.items():
if conf := config.get(type):
for type_ in SENSOR_TYPES:
if conf := config.get(type_):
sens = await binary_sensor.new_binary_sensor(conf)
binary_sensor_type = getattr(BinarySensorTypeEnum, type.upper())
binary_sensor_type = getattr(BinarySensorTypeEnum, type_.upper())
cg.add(paren.set_sub_binary_sensor(binary_sensor_type, sens))

View file

@ -21,7 +21,7 @@ ICON_SPRAY_BOTTLE = "mdi:spray-bottle"
CONFIG_SCHEMA = cv.Schema(
{
cv.Required(CONF_HAIER_ID): cv.use_id(HonClimate),
cv.GenerateID(CONF_HAIER_ID): cv.use_id(HonClimate),
cv.Optional(CONF_SELF_CLEANING): button.button_schema(
SelfCleaningButton,
icon=ICON_SPRAY_BOTTLE,

View file

@ -38,6 +38,9 @@ PROTOCOL_MAX_TEMPERATURE = 30.0
PROTOCOL_TARGET_TEMPERATURE_STEP = 1.0
PROTOCOL_CURRENT_TEMPERATURE_STEP = 0.5
PROTOCOL_CONTROL_PACKET_SIZE = 10
PROTOCOL_MIN_SENSORS_PACKET_SIZE = 18
PROTOCOL_DEFAULT_SENSORS_PACKET_SIZE = 22
PROTOCOL_STATUS_MESSAGE_HEADER_SIZE = 0
CODEOWNERS = ["@paveldn"]
DEPENDENCIES = ["climate", "uart"]
@ -48,6 +51,9 @@ CONF_CONTROL_PACKET_SIZE = "control_packet_size"
CONF_HORIZONTAL_AIRFLOW = "horizontal_airflow"
CONF_ON_ALARM_START = "on_alarm_start"
CONF_ON_ALARM_END = "on_alarm_end"
CONF_ON_STATUS_MESSAGE = "on_status_message"
CONF_SENSORS_PACKET_SIZE = "sensors_packet_size"
CONF_STATUS_MESSAGE_HEADER_SIZE = "status_message_header_size"
CONF_VERTICAL_AIRFLOW = "vertical_airflow"
CONF_WIFI_SIGNAL = "wifi_signal"
@ -129,6 +135,11 @@ HaierAlarmEndTrigger = haier_ns.class_(
automation.Trigger.template(cg.uint8, cg.const_char_ptr),
)
StatusMessageTrigger = haier_ns.class_(
"StatusMessageTrigger",
automation.Trigger.template(cg.const_char_ptr, cg.size_t),
)
def validate_visual(config):
if CONF_VISUAL in config:
@ -183,7 +194,6 @@ BASE_CONFIG_SCHEMA = (
cv.Optional(
CONF_SUPPORTED_SWING_MODES,
default=[
"OFF",
"VERTICAL",
"HORIZONTAL",
"BOTH",
@ -194,6 +204,11 @@ BASE_CONFIG_SCHEMA = (
cv.Optional(
CONF_ANSWER_TIMEOUT,
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_ON_STATUS_MESSAGE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StatusMessageTrigger),
}
),
}
)
.extend(uart.UART_DEVICE_SCHEMA)
@ -211,7 +226,7 @@ CONFIG_SCHEMA = cv.All(
): cv.boolean,
cv.Optional(
CONF_SUPPORTED_PRESETS,
default=list(["BOOST", "COMFORT"]), # No AWAY by default
default=["BOOST", "COMFORT"], # No AWAY by default
): cv.ensure_list(
cv.enum(SUPPORTED_CLIMATE_PRESETS_SMARTAIR2_OPTIONS, upper=True)
),
@ -229,9 +244,17 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(
CONF_CONTROL_PACKET_SIZE, default=PROTOCOL_CONTROL_PACKET_SIZE
): cv.int_range(min=PROTOCOL_CONTROL_PACKET_SIZE, max=50),
cv.Optional(
CONF_SENSORS_PACKET_SIZE,
default=PROTOCOL_DEFAULT_SENSORS_PACKET_SIZE,
): cv.int_range(min=PROTOCOL_MIN_SENSORS_PACKET_SIZE, max=50),
cv.Optional(
CONF_STATUS_MESSAGE_HEADER_SIZE,
default=PROTOCOL_STATUS_MESSAGE_HEADER_SIZE,
): cv.int_range(min=PROTOCOL_STATUS_MESSAGE_HEADER_SIZE),
cv.Optional(
CONF_SUPPORTED_PRESETS,
default=list(["BOOST", "ECO", "SLEEP"]), # No AWAY by default
default=["BOOST", "ECO", "SLEEP"], # No AWAY by default
): cv.ensure_list(
cv.enum(SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS, upper=True)
),
@ -427,11 +450,7 @@ def _final_validate(config):
"No logger component found, logging for Haier protocol is disabled"
)
cg.add_build_flag("-DHAIER_LOG_LEVEL=0")
if (
(CONF_WIFI_SIGNAL in config)
and (config[CONF_WIFI_SIGNAL])
and CONF_WIFI not in full_config
):
if config.get(CONF_WIFI_SIGNAL) and CONF_WIFI not in full_config:
raise cv.Invalid(
f"No WiFi configured, if you want to use haier climate without WiFi add {CONF_WIFI_SIGNAL}: false to climate configuration"
)
@ -473,6 +492,16 @@ async def to_code(config):
config[CONF_CONTROL_PACKET_SIZE] - PROTOCOL_CONTROL_PACKET_SIZE
)
)
if CONF_SENSORS_PACKET_SIZE in config:
cg.add(
var.set_extra_sensors_packet_bytes_size(
config[CONF_SENSORS_PACKET_SIZE] - PROTOCOL_MIN_SENSORS_PACKET_SIZE
)
)
if CONF_STATUS_MESSAGE_HEADER_SIZE in config:
cg.add(
var.set_status_message_header_size(config[CONF_STATUS_MESSAGE_HEADER_SIZE])
)
for conf in config.get(CONF_ON_ALARM_START, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
@ -483,5 +512,10 @@ async def to_code(config):
await automation.build_automation(
trigger, [(cg.uint8, "code"), (cg.const_char_ptr, "message")], conf
)
for conf in config.get(CONF_ON_STATUS_MESSAGE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger, [(cg.const_char_ptr, "data"), (cg.size_t, "data_size")], conf
)
# https://github.com/paveldn/HaierProtocol
cg.add_library("pavlodn/HaierProtocol", "0.9.28")
cg.add_library("pavlodn/HaierProtocol", "0.9.31")

View file

@ -186,6 +186,10 @@ void HaierClimateBase::send_custom_command(const haier_protocol::HaierMessage &m
this->action_request_ = PendingAction({ActionRequest::SEND_CUSTOM_COMMAND, message});
}
void HaierClimateBase::add_status_message_callback(std::function<void(const char *, size_t)> &&callback) {
this->status_message_callback_.add(std::move(callback));
}
haier_protocol::HandlerError HaierClimateBase::answer_preprocess_(
haier_protocol::FrameType request_message_type, haier_protocol::FrameType expected_request_message_type,
haier_protocol::FrameType answer_message_type, haier_protocol::FrameType expected_answer_message_type,

View file

@ -4,6 +4,7 @@
#include <set>
#include "esphome/components/climate/climate.h"
#include "esphome/components/uart/uart.h"
#include "esphome/core/automation.h"
// HaierProtocol
#include <protocol/haier_protocol.h>
@ -56,6 +57,7 @@ class HaierClimateBase : public esphome::Component,
void set_answer_timeout(uint32_t timeout);
void set_send_wifi(bool send_wifi);
void send_custom_command(const haier_protocol::HaierMessage &message);
void add_status_message_callback(std::function<void(const char *, size_t)> &&callback);
protected:
enum class ProtocolPhases {
@ -140,11 +142,19 @@ class HaierClimateBase : public esphome::Component,
esphome::climate::ClimateTraits traits_;
HvacSettings current_hvac_settings_;
HvacSettings next_hvac_settings_;
std::unique_ptr<uint8_t[]> last_status_message_;
std::unique_ptr<uint8_t[]> last_status_message_{nullptr};
std::chrono::steady_clock::time_point last_request_timestamp_; // For interval between messages
std::chrono::steady_clock::time_point last_valid_status_timestamp_; // For protocol timeout
std::chrono::steady_clock::time_point last_status_request_; // To request AC status
std::chrono::steady_clock::time_point last_signal_request_; // To send WiFI signal level
CallbackManager<void(const char *, size_t)> status_message_callback_{};
};
class StatusMessageTrigger : public Trigger<const char *, size_t> {
public:
explicit StatusMessageTrigger(HaierClimateBase *parent) {
parent->add_status_message_callback([this](const char *data, size_t data_size) { this->trigger(data, data_size); });
}
};
} // namespace haier

View file

@ -18,12 +18,13 @@ constexpr int PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET = -64;
constexpr uint8_t CONTROL_MESSAGE_RETRIES = 5;
constexpr std::chrono::milliseconds CONTROL_MESSAGE_RETRIES_INTERVAL = std::chrono::milliseconds(500);
constexpr size_t ALARM_STATUS_REQUEST_INTERVAL_MS = 600000;
const uint8_t ONE_BUF[] = {0x00, 0x01};
const uint8_t ZERO_BUF[] = {0x00, 0x00};
HonClimate::HonClimate()
: cleaning_status_(CleaningState::NO_CLEANING), got_valid_outdoor_temp_(false), active_alarms_{0x00, 0x00, 0x00,
0x00, 0x00, 0x00,
0x00, 0x00} {
last_status_message_ = std::unique_ptr<uint8_t[]>(new uint8_t[sizeof(hon_protocol::HaierPacketControl)]);
this->fan_mode_speed_ = (uint8_t) hon_protocol::FanMode::FAN_MID;
this->other_modes_fan_speed_ = (uint8_t) hon_protocol::FanMode::FAN_AUTO;
}
@ -169,11 +170,18 @@ haier_protocol::HandlerError HonClimate::status_handler_(haier_protocol::FrameTy
this->action_request_.reset();
this->force_send_control_ = false;
} else {
if (data_size >= sizeof(hon_protocol::HaierPacketControl) + 2) {
memcpy(this->last_status_message_.get(), data + 2, sizeof(hon_protocol::HaierPacketControl));
if (!this->last_status_message_) {
this->real_control_packet_size_ = sizeof(hon_protocol::HaierPacketControl) + this->extra_control_packet_bytes_;
this->real_sensors_packet_size_ = sizeof(hon_protocol::HaierPacketSensors) + this->extra_sensors_packet_bytes_;
this->last_status_message_.reset();
this->last_status_message_ = std::unique_ptr<uint8_t[]>(new uint8_t[this->real_control_packet_size_]);
};
if (data_size >= this->real_control_packet_size_ + 2) {
memcpy(this->last_status_message_.get(), data + 2 + this->status_message_header_size_,
this->real_control_packet_size_);
this->status_message_callback_.call((const char *) data, data_size);
} else {
ESP_LOGW(TAG, "Status packet too small: %d (should be >= %d)", data_size,
sizeof(hon_protocol::HaierPacketControl));
ESP_LOGW(TAG, "Status packet too small: %d (should be >= %d)", data_size, this->real_control_packet_size_);
}
switch (this->protocol_phase_) {
case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST:
@ -479,8 +487,8 @@ void HonClimate::initialization() {
}
haier_protocol::HaierMessage HonClimate::get_control_message() {
uint8_t control_out_buffer[sizeof(hon_protocol::HaierPacketControl)];
memcpy(control_out_buffer, this->last_status_message_.get(), sizeof(hon_protocol::HaierPacketControl));
uint8_t control_out_buffer[haier_protocol::MAX_FRAME_SIZE];
memcpy(control_out_buffer, this->last_status_message_.get(), this->real_control_packet_size_);
hon_protocol::HaierPacketControl *out_data = (hon_protocol::HaierPacketControl *) control_out_buffer;
control_out_buffer[4] = 0; // This byte should be cleared before setting values
bool has_hvac_settings = false;
@ -636,7 +644,7 @@ haier_protocol::HaierMessage HonClimate::get_control_message() {
out_data->health_mode = this->health_mode_ ? 1 : 0;
return haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_GROUP_PARAMETERS,
control_out_buffer, sizeof(hon_protocol::HaierPacketControl));
control_out_buffer, this->real_control_packet_size_);
}
void HonClimate::process_alarm_message_(const uint8_t *packet, uint8_t size, bool check_new) {
@ -758,15 +766,17 @@ void HonClimate::update_sub_text_sensor_(SubTextSensorType type, const std::stri
#endif // USE_TEXT_SENSOR
haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *packet_buffer, uint8_t size) {
size_t expected_size = 2 + sizeof(hon_protocol::HaierPacketControl) + sizeof(hon_protocol::HaierPacketSensors) +
this->extra_control_packet_bytes_;
if (size < expected_size)
size_t expected_size =
2 + this->status_message_header_size_ + this->real_control_packet_size_ + this->real_sensors_packet_size_;
if (size < expected_size) {
ESP_LOGW(TAG, "Unexpected message size %d (expexted >= %d)", size, expected_size);
return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE;
}
uint16_t subtype = (((uint16_t) packet_buffer[0]) << 8) + packet_buffer[1];
if ((subtype == 0x7D01) && (size >= expected_size + 4 + sizeof(hon_protocol::HaierPacketBigData))) {
if ((subtype == 0x7D01) && (size >= expected_size + sizeof(hon_protocol::HaierPacketBigData))) {
// Got BigData packet
const hon_protocol::HaierPacketBigData *bd_packet =
(const hon_protocol::HaierPacketBigData *) (&packet_buffer[expected_size + 4]);
(const hon_protocol::HaierPacketBigData *) (&packet_buffer[expected_size]);
#ifdef USE_SENSOR
this->update_sub_sensor_(SubSensorType::INDOOR_COIL_TEMPERATURE, bd_packet->indoor_coil_temperature / 2.0 - 20);
this->update_sub_sensor_(SubSensorType::OUTDOOR_COIL_TEMPERATURE, bd_packet->outdoor_coil_temperature - 64);
@ -795,9 +805,9 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
hon_protocol::HaierPacketControl control;
hon_protocol::HaierPacketSensors sensors;
} packet;
memcpy(&packet.control, packet_buffer + 2, sizeof(hon_protocol::HaierPacketControl));
memcpy(&packet.sensors,
packet_buffer + 2 + sizeof(hon_protocol::HaierPacketControl) + this->extra_control_packet_bytes_,
memcpy(&packet.control, packet_buffer + 2 + this->status_message_header_size_,
sizeof(hon_protocol::HaierPacketControl));
memcpy(&packet.sensors, packet_buffer + 2 + this->status_message_header_size_ + this->real_control_packet_size_,
sizeof(hon_protocol::HaierPacketSensors));
if (packet.sensors.error_status != 0) {
ESP_LOGW(TAG, "HVAC error, code=0x%02X", packet.sensors.error_status);
@ -996,8 +1006,6 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
}
void HonClimate::fill_control_messages_queue_() {
static uint8_t one_buf[] = {0x00, 0x01};
static uint8_t zero_buf[] = {0x00, 0x00};
if (!this->current_hvac_settings_.valid && !this->force_send_control_)
return;
this->clear_control_messages_queue_();
@ -1009,7 +1017,7 @@ void HonClimate::fill_control_messages_queue_() {
haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
(uint8_t) hon_protocol::DataParameters::BEEPER_STATUS,
this->beeper_status_ ? zero_buf : one_buf, 2));
this->beeper_status_ ? ZERO_BUF : ONE_BUF, 2));
}
// Health mode
{
@ -1017,7 +1025,7 @@ void HonClimate::fill_control_messages_queue_() {
haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
(uint8_t) hon_protocol::DataParameters::HEALTH_MODE,
this->health_mode_ ? one_buf : zero_buf, 2));
this->health_mode_ ? ONE_BUF : ZERO_BUF, 2));
}
// Climate mode
bool new_power = this->mode != CLIMATE_MODE_OFF;
@ -1092,7 +1100,7 @@ void HonClimate::fill_control_messages_queue_() {
haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
(uint8_t) hon_protocol::DataParameters::AC_POWER,
new_power ? one_buf : zero_buf, 2));
new_power ? ONE_BUF : ZERO_BUF, 2));
}
// CLimate preset
{
@ -1165,6 +1173,35 @@ void HonClimate::fill_control_messages_queue_() {
(uint8_t) hon_protocol::DataParameters::SET_POINT,
buffer, 2));
}
// Vertical swing mode
if (climate_control.swing_mode.has_value()) {
uint8_t vertical_swing_buf[] = {0x00, (uint8_t) hon_protocol::VerticalSwingMode::AUTO};
uint8_t horizontal_swing_buf[] = {0x00, (uint8_t) hon_protocol::HorizontalSwingMode::AUTO};
switch (climate_control.swing_mode.value()) {
case CLIMATE_SWING_OFF:
horizontal_swing_buf[1] = (uint8_t) this->settings_.last_horizontal_swing;
vertical_swing_buf[1] = (uint8_t) this->settings_.last_vertiacal_swing;
break;
case CLIMATE_SWING_VERTICAL:
horizontal_swing_buf[1] = (uint8_t) this->settings_.last_horizontal_swing;
break;
case CLIMATE_SWING_HORIZONTAL:
vertical_swing_buf[1] = (uint8_t) this->settings_.last_vertiacal_swing;
break;
case CLIMATE_SWING_BOTH:
break;
}
this->control_messages_queue_.push(
haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
(uint8_t) hon_protocol::DataParameters::HORIZONTAL_SWING_MODE,
horizontal_swing_buf, 2));
this->control_messages_queue_.push(
haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
(uint8_t) hon_protocol::DataParameters::VERTICAL_SWING_MODE,
vertical_swing_buf, 2));
}
// Fan mode
if (climate_control.fan_mode.has_value()) {
switch (climate_control.fan_mode.value()) {
@ -1202,9 +1239,10 @@ void HonClimate::clear_control_messages_queue_() {
bool HonClimate::prepare_pending_action() {
switch (this->action_request_.value().action) {
case ActionRequest::START_SELF_CLEAN: {
uint8_t control_out_buffer[sizeof(hon_protocol::HaierPacketControl)];
memcpy(control_out_buffer, this->last_status_message_.get(), sizeof(hon_protocol::HaierPacketControl));
case ActionRequest::START_SELF_CLEAN:
if (this->control_method_ == HonControlMethod::SET_GROUP_PARAMETERS) {
uint8_t control_out_buffer[haier_protocol::MAX_FRAME_SIZE];
memcpy(control_out_buffer, this->last_status_message_.get(), this->real_control_packet_size_);
hon_protocol::HaierPacketControl *out_data = (hon_protocol::HaierPacketControl *) control_out_buffer;
out_data->self_cleaning_status = 1;
out_data->steri_clean = 0;
@ -1216,12 +1254,23 @@ bool HonClimate::prepare_pending_action() {
out_data->light_status = 0;
this->action_request_.value().message = haier_protocol::HaierMessage(
haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::SET_GROUP_PARAMETERS,
control_out_buffer, sizeof(hon_protocol::HaierPacketControl));
}
control_out_buffer, this->real_control_packet_size_);
return true;
case ActionRequest::START_STERI_CLEAN: {
uint8_t control_out_buffer[sizeof(hon_protocol::HaierPacketControl)];
memcpy(control_out_buffer, this->last_status_message_.get(), sizeof(hon_protocol::HaierPacketControl));
} else if (this->control_method_ == HonControlMethod::SET_SINGLE_PARAMETER) {
this->action_request_.value().message =
haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
(uint8_t) hon_protocol::DataParameters::SELF_CLEANING,
ONE_BUF, 2);
return true;
} else {
this->action_request_.reset();
return false;
}
case ActionRequest::START_STERI_CLEAN:
if (this->control_method_ == HonControlMethod::SET_GROUP_PARAMETERS) {
uint8_t control_out_buffer[haier_protocol::MAX_FRAME_SIZE];
memcpy(control_out_buffer, this->last_status_message_.get(), this->real_control_packet_size_);
hon_protocol::HaierPacketControl *out_data = (hon_protocol::HaierPacketControl *) control_out_buffer;
out_data->self_cleaning_status = 0;
out_data->steri_clean = 1;
@ -1233,9 +1282,13 @@ bool HonClimate::prepare_pending_action() {
out_data->light_status = 0;
this->action_request_.value().message = haier_protocol::HaierMessage(
haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::SET_GROUP_PARAMETERS,
control_out_buffer, sizeof(hon_protocol::HaierPacketControl));
}
control_out_buffer, this->real_control_packet_size_);
return true;
} else {
// No Steri clean support (yet?) in SET_SINGLE_PARAMETER
this->action_request_.reset();
return false;
}
default:
return HaierClimateBase::prepare_pending_action();
}
@ -1251,6 +1304,7 @@ void HonClimate::process_protocol_reset() {
#endif // USE_SENSOR
this->got_valid_outdoor_temp_ = false;
this->hvac_hardware_info_.reset();
this->last_status_message_.reset(nullptr);
}
bool HonClimate::should_get_big_data_() {

View file

@ -104,6 +104,8 @@ class HonClimate : public HaierClimateBase {
void start_self_cleaning();
void start_steri_cleaning();
void set_extra_control_packet_bytes_size(size_t size) { this->extra_control_packet_bytes_ = size; };
void set_extra_sensors_packet_bytes_size(size_t size) { this->extra_sensors_packet_bytes_ = size; };
void set_status_message_header_size(size_t size) { this->status_message_header_size_ = size; };
void set_control_method(HonControlMethod method) { this->control_method_ = method; };
void add_alarm_start_callback(std::function<void(uint8_t, const char *)> &&callback);
void add_alarm_end_callback(std::function<void(uint8_t, const char *)> &&callback);
@ -158,7 +160,11 @@ class HonClimate : public HaierClimateBase {
esphome::optional<hon_protocol::HorizontalSwingMode> pending_horizontal_direction_{};
esphome::optional<HardwareInfo> hvac_hardware_info_{};
uint8_t active_alarms_[8];
int extra_control_packet_bytes_;
int extra_control_packet_bytes_{0};
int extra_sensors_packet_bytes_{4};
int status_message_header_size_{0};
int real_control_packet_size_{sizeof(hon_protocol::HaierPacketControl)};
int real_sensors_packet_size_{sizeof(hon_protocol::HaierPacketSensors) + 4};
HonControlMethod control_method_;
std::queue<haier_protocol::HaierMessage> control_messages_queue_;
CallbackManager<void(uint8_t, const char *)> alarm_start_callback_{};

View file

@ -41,15 +41,20 @@ enum class ConditioningMode : uint8_t {
enum class DataParameters : uint8_t {
AC_POWER = 0x01,
SET_POINT = 0x02,
VERTICAL_SWING_MODE = 0x03,
AC_MODE = 0x04,
FAN_MODE = 0x05,
USE_FAHRENHEIT = 0x07,
DISPLAY_STATUS = 0x09,
TEN_DEGREE = 0x0A,
HEALTH_MODE = 0x0B,
HORIZONTAL_SWING_MODE = 0x0C,
SELF_CLEANING = 0x0D,
BEEPER_STATUS = 0x16,
LOCK_REMOTE = 0x17,
QUIET_MODE = 0x19,
FAST_MODE = 0x1A,
SLEEP_MODE = 0x1B,
};
enum class SpecialMode : uint8_t { NONE = 0x00, ELDERLY = 0x01, CHILDREN = 0x02, PREGNANT = 0x03 };

View file

@ -137,16 +137,16 @@ SENSOR_TYPES = {
CONFIG_SCHEMA = cv.Schema(
{
cv.Required(CONF_HAIER_ID): cv.use_id(HonClimate),
cv.GenerateID(CONF_HAIER_ID): cv.use_id(HonClimate),
}
).extend({cv.Optional(type): schema for type, schema in SENSOR_TYPES.items()})
).extend({cv.Optional(type_): schema for type_, schema in SENSOR_TYPES.items()})
async def to_code(config):
paren = await cg.get_variable(config[CONF_HAIER_ID])
for type, _ in SENSOR_TYPES.items():
if conf := config.get(type):
for type_ in SENSOR_TYPES:
if conf := config.get(type_):
sens = await sensor.new_sensor(conf)
sensor_type = getattr(SensorTypeEnum, type.upper())
sensor_type = getattr(SensorTypeEnum, type_.upper())
cg.add(paren.set_sub_sensor(sensor_type, sens))

View file

@ -37,6 +37,7 @@ haier_protocol::HandlerError Smartair2Climate::status_handler_(haier_protocol::F
} else {
if (data_size >= sizeof(smartair2_protocol::HaierPacketControl) + 2) {
memcpy(this->last_status_message_.get(), data + 2, sizeof(smartair2_protocol::HaierPacketControl));
this->status_message_callback_.call((const char *) data, data_size);
} else {
ESP_LOGW(TAG, "Status packet too small: %d (should be >= %d)", data_size,
sizeof(smartair2_protocol::HaierPacketControl));

View file

@ -39,7 +39,7 @@ TEXT_SENSOR_TYPES = {
CONFIG_SCHEMA = cv.Schema(
{
cv.Required(CONF_HAIER_ID): cv.use_id(HonClimate),
cv.GenerateID(CONF_HAIER_ID): cv.use_id(HonClimate),
}
).extend({cv.Optional(type): schema for type, schema in TEXT_SENSOR_TYPES.items()})
@ -47,8 +47,8 @@ CONFIG_SCHEMA = cv.Schema(
async def to_code(config):
paren = await cg.get_variable(config[CONF_HAIER_ID])
for type, _ in TEXT_SENSOR_TYPES.items():
if conf := config.get(type):
for type_ in TEXT_SENSOR_TYPES:
if conf := config.get(type_):
sens = await text_sensor.new_text_sensor(conf)
text_sensor_type = getattr(TextSensorTypeEnum, type.upper())
text_sensor_type = getattr(TextSensorTypeEnum, type_.upper())
cg.add(paren.set_sub_text_sensor(text_sensor_type, sens))

View file

@ -8,7 +8,6 @@ from esphome.const import (
CONF_PROTOCOL,
CONF_VISUAL,
)
from esphome.core import CORE
CODEOWNERS = ["@rob-deutsch"]
@ -34,6 +33,7 @@ PROTOCOLS = {
"greeyan": Protocol.PROTOCOL_GREEYAN,
"greeyac": Protocol.PROTOCOL_GREEYAC,
"greeyt": Protocol.PROTOCOL_GREEYT,
"greeyap": Protocol.PROTOCOL_GREEYAP,
"hisense_aud": Protocol.PROTOCOL_HISENSE_AUD,
"hitachi": Protocol.PROTOCOL_HITACHI,
"hyundai": Protocol.PROTOCOL_HYUNDAI,
@ -61,6 +61,16 @@ PROTOCOLS = {
"toshiba_daiseikai": Protocol.PROTOCOL_TOSHIBA_DAISEIKAI,
"toshiba": Protocol.PROTOCOL_TOSHIBA,
"zhlt01": Protocol.PROTOCOL_ZHLT01,
"nibe": Protocol.PROTOCOL_NIBE,
"carrier_qlima_1": Protocol.PROTOCOL_QLIMA_1,
"carrier_qlima_2": Protocol.PROTOCOL_QLIMA_2,
"samsung_aqv12msan": Protocol.PROTOCOL_SAMSUNG_AQV12MSAN,
"zhjg01": Protocol.PROTOCOL_ZHJG01,
"airway": Protocol.PROTOCOL_AIRWAY,
"bgh_aud": Protocol.PROTOCOL_BGH_AUD,
"panasonic_altdke": Protocol.PROTOCOL_PANASONIC_ALTDKE,
"vaillantvai8": Protocol.PROTOCOL_VAILLANTVAI8,
"r51m": Protocol.PROTOCOL_R51M,
}
CONF_HORIZONTAL_DEFAULT = "horizontal_default"
@ -116,7 +126,4 @@ def to_code(config):
cg.add(var.set_max_temperature(config[CONF_MAX_TEMPERATURE]))
cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE]))
cg.add_library("tonia/HeatpumpIR", "1.0.23")
if CORE.is_esp8266 or CORE.is_esp32:
cg.add_library("crankyoldgit/IRremoteESP8266", "2.8.4")
cg.add_library("tonia/HeatpumpIR", "1.0.27")

View file

@ -28,6 +28,7 @@ const std::map<Protocol, std::function<HeatpumpIR *()>> PROTOCOL_CONSTRUCTOR_MAP
{PROTOCOL_GREEYAN, []() { return new GreeYANHeatpumpIR(); }}, // NOLINT
{PROTOCOL_GREEYAC, []() { return new GreeYACHeatpumpIR(); }}, // NOLINT
{PROTOCOL_GREEYT, []() { return new GreeYTHeatpumpIR(); }}, // NOLINT
{PROTOCOL_GREEYAP, []() { return new GreeYAPHeatpumpIR(); }}, // NOLINT
{PROTOCOL_HISENSE_AUD, []() { return new HisenseHeatpumpIR(); }}, // NOLINT
{PROTOCOL_HITACHI, []() { return new HitachiHeatpumpIR(); }}, // NOLINT
{PROTOCOL_HYUNDAI, []() { return new HyundaiHeatpumpIR(); }}, // NOLINT
@ -55,6 +56,16 @@ const std::map<Protocol, std::function<HeatpumpIR *()>> PROTOCOL_CONSTRUCTOR_MAP
{PROTOCOL_TOSHIBA_DAISEIKAI, []() { return new ToshibaDaiseikaiHeatpumpIR(); }}, // NOLINT
{PROTOCOL_TOSHIBA, []() { return new ToshibaHeatpumpIR(); }}, // NOLINT
{PROTOCOL_ZHLT01, []() { return new ZHLT01HeatpumpIR(); }}, // NOLINT
{PROTOCOL_NIBE, []() { return new NibeHeatpumpIR(); }}, // NOLINT
{PROTOCOL_QLIMA_1, []() { return new Qlima1HeatpumpIR(); }}, // NOLINT
{PROTOCOL_QLIMA_2, []() { return new Qlima2HeatpumpIR(); }}, // NOLINT
{PROTOCOL_SAMSUNG_AQV12MSAN, []() { return new SamsungAQV12MSANHeatpumpIR(); }}, // NOLINT
{PROTOCOL_ZHJG01, []() { return new ZHJG01HeatpumpIR(); }}, // NOLINT
{PROTOCOL_AIRWAY, []() { return new AIRWAYHeatpumpIR(); }}, // NOLINT
{PROTOCOL_BGH_AUD, []() { return new BGHHeatpumpIR(); }}, // NOLINT
{PROTOCOL_PANASONIC_ALTDKE, []() { return new PanasonicAltDKEHeatpumpIR(); }}, // NOLINT
{PROTOCOL_VAILLANTVAI8, []() { return new VaillantHeatpumpIR(); }}, // NOLINT
{PROTOCOL_R51M, []() { return new R51MHeatpumpIR(); }}, // NOLINT
};
void HeatpumpIRClimate::setup() {

View file

@ -28,6 +28,7 @@ enum Protocol {
PROTOCOL_GREEYAN,
PROTOCOL_GREEYAC,
PROTOCOL_GREEYT,
PROTOCOL_GREEYAP,
PROTOCOL_HISENSE_AUD,
PROTOCOL_HITACHI,
PROTOCOL_HYUNDAI,
@ -55,6 +56,16 @@ enum Protocol {
PROTOCOL_TOSHIBA_DAISEIKAI,
PROTOCOL_TOSHIBA,
PROTOCOL_ZHLT01,
PROTOCOL_NIBE,
PROTOCOL_QLIMA_1,
PROTOCOL_QLIMA_2,
PROTOCOL_SAMSUNG_AQV12MSAN,
PROTOCOL_ZHJG01,
PROTOCOL_AIRWAY,
PROTOCOL_BGH_AUD,
PROTOCOL_PANASONIC_ALTDKE,
PROTOCOL_VAILLANTVAI8,
PROTOCOL_R51M,
};
// Simple enum to represent horizontal directios

View file

@ -257,7 +257,7 @@ async def http_request_action_to_code(config, action_id, template_arg, args):
trigger,
[
(cg.std_shared_ptr.template(HttpContainer), "response"),
(cg.std_string, "body"),
(cg.std_string_ref, "body"),
],
conf,
)

View file

@ -43,10 +43,10 @@ class HttpContainer : public Parented<HttpRequestComponent> {
bool secure_{false};
};
class HttpRequestResponseTrigger : public Trigger<std::shared_ptr<HttpContainer>, std::string> {
class HttpRequestResponseTrigger : public Trigger<std::shared_ptr<HttpContainer>, std::string &> {
public:
void process(std::shared_ptr<HttpContainer> container, std::string response_body) {
this->trigger(std::move(container), std::move(response_body));
void process(std::shared_ptr<HttpContainer> container, std::string &response_body) {
this->trigger(std::move(container), response_body);
}
};
@ -149,11 +149,21 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
}
response_body.reserve(read_index);
response_body.assign((char *) buf, read_index);
allocator.deallocate(buf, max_length);
}
}
if (this->response_triggers_.size() == 1) {
// if there is only one trigger, no need to copy the response body
this->response_triggers_[0]->process(container, response_body);
} else {
for (auto *trigger : this->response_triggers_) {
trigger->process(container, response_body);
// with multiple triggers, pass a copy of the response body to each
// one so that modifications made in one trigger are not visible to
// the others
auto response_body_copy = std::string(response_body);
trigger->process(container, response_body_copy);
}
}
container->end();
}

View file

@ -32,6 +32,13 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::start(std::string url, std::s
watchdog::WatchdogManager wdm(this->get_watchdog_timeout());
if (this->follow_redirects_) {
container->client_.setFollowRedirects(HTTPC_FORCE_FOLLOW_REDIRECTS);
container->client_.setRedirectLimit(this->redirect_limit_);
} else {
container->client_.setFollowRedirects(HTTPC_DISABLE_FOLLOW_REDIRECTS);
}
#if defined(USE_ESP8266)
std::unique_ptr<WiFiClient> stream_ptr;
#ifdef USE_HTTP_REQUEST_ESP8266_HTTPS
@ -59,8 +66,6 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::start(std::string url, std::s
"in your YAML, or use HTTPS");
}
#endif // USE_ARDUINO_VERSION_CODE
container->client_.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
bool status = container->client_.begin(*stream_ptr, url.c_str());
#elif defined(USE_RP2040)

View file

@ -90,7 +90,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::start(std::string url, std::strin
int write_left = body_len;
int write_index = 0;
const char *buf = body.c_str();
while (body_len > 0) {
while (write_left > 0) {
int written = esp_http_client_write(client, buf + write_index, write_left);
if (written < 0) {
err = ESP_FAIL;

View file

@ -116,19 +116,18 @@ void HttpRequestUpdate::update() {
}
}
std::string current_version = this->current_version_;
if (current_version.empty()) {
std::string current_version;
#ifdef ESPHOME_PROJECT_VERSION
current_version = ESPHOME_PROJECT_VERSION;
#else
current_version = ESPHOME_VERSION;
#endif
}
this->update_info_.current_version = current_version;
if (this->update_info_.latest_version.empty()) {
if (this->update_info_.latest_version.empty() || this->update_info_.latest_version == update_info_.current_version) {
this->state_ = update::UPDATE_STATE_NO_UPDATE;
} else if (this->update_info_.latest_version != this->current_version_) {
} else {
this->state_ = update::UPDATE_STATE_AVAILABLE;
}

View file

@ -22,15 +22,12 @@ class HttpRequestUpdate : public update::UpdateEntity, public PollingComponent {
void set_request_parent(HttpRequestComponent *request_parent) { this->request_parent_ = request_parent; }
void set_ota_parent(OtaHttpRequestComponent *ota_parent) { this->ota_parent_ = ota_parent; }
void set_current_version(const std::string &current_version) { this->current_version_ = current_version; }
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
protected:
HttpRequestComponent *request_parent_;
OtaHttpRequestComponent *ota_parent_;
std::string source_url_;
std::string current_version_{""};
};
} // namespace http_request

View file

@ -46,7 +46,7 @@ void WatchdogManager::set_timeout_(uint32_t timeout_ms) {
};
esp_task_wdt_reconfigure(&wdt_config);
#else
esp_task_wdt_init(timeout_ms, true);
esp_task_wdt_init(timeout_ms / 1000, true);
#endif // ESP_IDF_VERSION_MAJOR
#endif // USE_ESP32

View file

@ -2,7 +2,7 @@ import esphome.config_validation as cv
import esphome.codegen as cg
from esphome import pins
from esphome.const import CONF_CHANNEL, CONF_ID, CONF_NUMBER
from esphome.const import CONF_CHANNEL, CONF_ID, CONF_NUMBER, CONF_SAMPLE_RATE
from esphome.components import microphone, esp32
from esphome.components.adc import ESP32_VARIANT_ADC1_PIN_TO_CHANNEL, validate_adc_pin
@ -20,7 +20,6 @@ DEPENDENCIES = ["i2s_audio"]
CONF_ADC_PIN = "adc_pin"
CONF_ADC_TYPE = "adc_type"
CONF_PDM = "pdm"
CONF_SAMPLE_RATE = "sample_rate"
CONF_BITS_PER_SAMPLE = "bits_per_sample"
CONF_USE_APLL = "use_apll"

View file

@ -34,8 +34,8 @@ void ILI9XXXDisplay::setup() {
ESP_LOGD(TAG, "Setting up ILI9xxx");
this->setup_pins_();
this->init_lcd_(this->init_sequence_);
this->init_lcd_(this->extra_init_sequence_.data());
this->init_lcd(this->init_sequence_);
this->init_lcd(this->extra_init_sequence_.data());
switch (this->pixel_mode_) {
case PIXEL_MODE_16:
if (this->is_18bitdisplay_) {
@ -405,7 +405,7 @@ void ILI9XXXDisplay::reset_() {
}
}
void ILI9XXXDisplay::init_lcd_(const uint8_t *addr) {
void ILI9XXXDisplay::init_lcd(const uint8_t *addr) {
if (addr == nullptr)
return;
uint8_t cmd, x, num_args;
@ -427,6 +427,20 @@ void ILI9XXXDisplay::init_lcd_(const uint8_t *addr) {
}
}
void ILI9XXXGC9A01A::init_lcd(const uint8_t *addr) {
if (addr == nullptr)
return;
uint8_t cmd, x, num_args;
while ((cmd = *addr++) != 0) {
x = *addr++;
num_args = x & 0x7F;
this->send_command(cmd, addr, num_args);
addr += num_args;
if (x & 0x80)
delay(150); // NOLINT
}
}
// Tell the display controller where we want to draw pixels.
void ILI9XXXDisplay::set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) {
x1 += this->offset_x_;

View file

@ -109,7 +109,7 @@ class ILI9XXXDisplay : public display::DisplayBuffer,
virtual void set_madctl();
void display_();
void init_lcd_(const uint8_t *addr);
virtual void init_lcd(const uint8_t *addr);
void set_addr_window_(uint16_t x, uint16_t y, uint16_t x2, uint16_t y2);
void reset_();
@ -269,6 +269,7 @@ class ILI9XXXS3BoxLite : public ILI9XXXDisplay {
class ILI9XXXGC9A01A : public ILI9XXXDisplay {
public:
ILI9XXXGC9A01A() : ILI9XXXDisplay(INITCMD_GC9A01A, 240, 240, true) {}
void init_lcd(const uint8_t *addr) override;
};
//----------- ILI9XXX_24_TFT display --------------

View file

@ -6,7 +6,6 @@ import hashlib
import io
from pathlib import Path
import re
import requests
from magic import Magic
from esphome import core
@ -15,7 +14,6 @@ from esphome import external_files
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.const import (
__version__,
CONF_DITHER,
CONF_FILE,
CONF_ICON,
@ -75,31 +73,6 @@ def compute_local_image_path(value: dict) -> Path:
return base_dir / key
def download_content(url: str, path: Path) -> None:
if not external_files.has_remote_file_changed(url, path):
_LOGGER.debug("Remote file has not changed %s", url)
return
_LOGGER.debug(
"Remote file has changed, downloading from %s to %s",
url,
path,
)
try:
req = requests.get(
url,
timeout=IMAGE_DOWNLOAD_TIMEOUT,
headers={"User-agent": f"ESPHome/{__version__} (https://esphome.io)"},
)
req.raise_for_status()
except requests.exceptions.RequestException as e:
raise cv.Invalid(f"Could not download from {url}: {e}")
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(req.content)
def download_mdi(value):
validate_cairosvg_installed(value)
@ -108,7 +81,7 @@ def download_mdi(value):
url = f"https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg/{mdi_id}.svg"
download_content(url, path)
external_files.download_content(url, path, IMAGE_DOWNLOAD_TIMEOUT)
return value
@ -117,7 +90,7 @@ def download_image(value):
url = value[CONF_URL]
path = compute_local_image_path(value)
download_content(url, path)
external_files.download_content(url, path, IMAGE_DOWNLOAD_TIMEOUT)
return value

View file

@ -57,7 +57,7 @@ optional<uint8_t> ImprovSerialComponent::read_byte_() {
}
}
break;
#if defined(CONFIG_ESP_CONSOLE_USB_CDC) && (defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3))
#if defined(USE_LOGGER_USB_CDC) && defined(CONFIG_ESP_CONSOLE_USB_CDC)
case logger::UART_SELECTION_USB_CDC:
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
if (esp_usb_console_available_for_read()) {
@ -68,15 +68,15 @@ optional<uint8_t> ImprovSerialComponent::read_byte_() {
byte = data;
}
break;
#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3
#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32S3)
#endif // USE_LOGGER_USB_CDC
#ifdef USE_LOGGER_USB_SERIAL_JTAG
case logger::UART_SELECTION_USB_SERIAL_JTAG: {
if (usb_serial_jtag_read_bytes((char *) &data, 1, 0)) {
byte = data;
}
break;
}
#endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32S3
#endif // USE_LOGGER_USB_SERIAL_JTAG
default:
break;
}
@ -99,19 +99,20 @@ void ImprovSerialComponent::write_data_(std::vector<uint8_t> &data) {
#endif // !USE_ESP32_VARIANT_ESP32C3 && !USE_ESP32_VARIANT_ESP32S2 && !USE_ESP32_VARIANT_ESP32S3
uart_write_bytes(this->uart_num_, data.data(), data.size());
break;
#if defined(CONFIG_ESP_CONSOLE_USB_CDC) && (defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3))
#if defined(USE_LOGGER_USB_CDC) && defined(CONFIG_ESP_CONSOLE_USB_CDC)
case logger::UART_SELECTION_USB_CDC: {
const char *msg = (char *) data.data();
esp_usb_console_write_buf(msg, data.size());
break;
}
#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3
#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32S3)
#endif // USE_LOGGER_USB_CDC
#ifdef USE_LOGGER_USB_SERIAL_JTAG
case logger::UART_SELECTION_USB_SERIAL_JTAG:
usb_serial_jtag_write_bytes((char *) data.data(), data.size(), 20 / portTICK_PERIOD_MS);
delay(10);
usb_serial_jtag_ll_txfifo_flush(); // fixes for issue in IDF 4.4.7
break;
#endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32S3
#endif // USE_LOGGER_USB_SERIAL_JTAG
default:
break;
}

View file

@ -31,7 +31,16 @@ void Jsnsr04tComponent::loop() {
}
void Jsnsr04tComponent::check_buffer_() {
uint8_t checksum = this->buffer_[0] + this->buffer_[1] + this->buffer_[2];
uint8_t checksum = 0;
switch (this->model_) {
case JSN_SR04T:
checksum = this->buffer_[0] + this->buffer_[1] + this->buffer_[2];
break;
case AJ_SR04M:
checksum = this->buffer_[1] + this->buffer_[2];
break;
}
if (this->buffer_[3] == checksum) {
uint16_t distance = encode_uint16(this->buffer_[1], this->buffer_[2]);
if (distance > 250) {
@ -49,6 +58,14 @@ void Jsnsr04tComponent::check_buffer_() {
void Jsnsr04tComponent::dump_config() {
LOG_SENSOR("", "JST_SR04T Sensor", this);
switch (this->model_) {
case JSN_SR04T:
ESP_LOGCONFIG(TAG, " sensor model: jsn_sr04t");
break;
case AJ_SR04M:
ESP_LOGCONFIG(TAG, " sensor model: aj_sr04m");
break;
}
LOG_UPDATE_INTERVAL(this);
}

View file

@ -9,9 +9,14 @@
namespace esphome {
namespace jsn_sr04t {
enum Model {
JSN_SR04T,
AJ_SR04M,
};
class Jsnsr04tComponent : public sensor::Sensor, public PollingComponent, public uart::UARTDevice {
public:
// Nothing really public.
void set_model(Model model) { this->model_ = model; }
// ========== INTERNAL METHODS ==========
void update() override;
@ -20,6 +25,7 @@ class Jsnsr04tComponent : public sensor::Sensor, public PollingComponent, public
protected:
void check_buffer_();
Model model_;
std::vector<uint8_t> buffer_;
};

View file

@ -5,6 +5,7 @@ from esphome.const import (
STATE_CLASS_MEASUREMENT,
UNIT_METER,
ICON_ARROW_EXPAND_VERTICAL,
CONF_MODEL,
)
CODEOWNERS = ["@Mafus1"]
@ -14,6 +15,11 @@ jsn_sr04t_ns = cg.esphome_ns.namespace("jsn_sr04t")
Jsnsr04tComponent = jsn_sr04t_ns.class_(
"Jsnsr04tComponent", sensor.Sensor, cg.PollingComponent, uart.UARTDevice
)
Model = jsn_sr04t_ns.enum("Model")
MODEL = {
"jsn_sr04t": Model.JSN_SR04T,
"aj_sr04m": Model.AJ_SR04M,
}
CONFIG_SCHEMA = (
sensor.sensor_schema(
@ -25,6 +31,11 @@ CONFIG_SCHEMA = (
)
.extend(cv.polling_component_schema("60s"))
.extend(uart.UART_DEVICE_SCHEMA)
.extend(
{
cv.Optional(CONF_MODEL, default="jsn_sr04t"): cv.enum(MODEL, upper=False),
}
)
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
@ -42,3 +53,5 @@ async def to_code(config):
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
cg.add(var.set_model(config[CONF_MODEL]))

View file

@ -115,12 +115,15 @@ void LEDCOutput::write_state(float state) {
const uint32_t max_duty = (uint32_t(1) << this->bit_depth_) - 1;
const float duty_rounded = roundf(state * max_duty);
auto duty = static_cast<uint32_t>(duty_rounded);
#ifdef USE_ARDUINO
ESP_LOGV(TAG, "Setting duty: %u on channel %u", duty, this->channel_);
ledcWrite(this->channel_, duty);
#endif
#ifdef USE_ESP_IDF
// ensure that 100% on is not 99.975% on
if ((duty == max_duty) && (max_duty != 1)) {
duty = max_duty + 1;
}
auto speed_mode = get_speed_mode(channel_);
auto chan_num = static_cast<ledc_channel_t>(channel_ % 8);
int hpoint = ledc_angle_to_htop(this->phase_angle_, this->bit_depth_);

View file

@ -19,6 +19,7 @@ static const uint8_t LTR390_MAIN_STATUS = 0x07;
static const float GAINVALUES[5] = {1.0, 3.0, 6.0, 9.0, 18.0};
static const float RESOLUTIONVALUE[6] = {4.0, 2.0, 1.0, 0.5, 0.25, 0.125};
static const uint8_t RESOLUTION_BITS[6] = {20, 19, 18, 17, 16, 13};
// Request fastest measurement rate - will be slowed by device if conversion rate is slower.
static const float RESOLUTION_SETTING[6] = {0x00, 0x10, 0x20, 0x30, 0x40, 0x50};
@ -74,7 +75,7 @@ void LTR390Component::read_als_() {
uint32_t als = *val;
if (this->light_sensor_ != nullptr) {
float lux = ((0.6 * als) / (GAINVALUES[this->gain_] * RESOLUTIONVALUE[this->res_])) * this->wfac_;
float lux = ((0.6 * als) / (GAINVALUES[this->gain_als_] * RESOLUTIONVALUE[this->res_als_])) * this->wfac_;
this->light_sensor_->publish_state(lux);
}
@ -90,7 +91,7 @@ void LTR390Component::read_uvs_() {
uint32_t uv = *val;
if (this->uvi_sensor_ != nullptr) {
this->uvi_sensor_->publish_state((uv / this->sensitivity_) * this->wfac_);
this->uvi_sensor_->publish_state((uv / this->sensitivity_uv_) * this->wfac_);
}
if (this->uv_sensor_ != nullptr) {
@ -107,9 +108,23 @@ void LTR390Component::read_mode_(int mode_index) {
ctrl[LTR390_CTRL_EN] = true;
this->reg(LTR390_MAIN_CTRL) = ctrl.to_ulong();
uint32_t int_time{0};
// Set gain, resolution and measurement rate
switch (mode) {
case LTR390_MODE_ALS:
this->reg(LTR390_GAIN) = this->gain_als_;
this->reg(LTR390_MEAS_RATE) = RESOLUTION_SETTING[this->res_als_];
int_time = ((uint32_t) RESOLUTIONVALUE[this->res_als_]) * 100;
break;
case LTR390_MODE_UVS:
this->reg(LTR390_GAIN) = this->gain_uv_;
this->reg(LTR390_MEAS_RATE) = RESOLUTION_SETTING[this->res_uv_];
int_time = ((uint32_t) RESOLUTIONVALUE[this->res_uv_]) * 100;
break;
}
// After the sensor integration time do the following
this->set_timeout(((uint32_t) RESOLUTIONVALUE[this->res_]) * 100 + LTR390_WAKEUP_TIME + LTR390_SETTLE_TIME,
[this, mode_index]() {
this->set_timeout(int_time + LTR390_WAKEUP_TIME + LTR390_SETTLE_TIME, [this, mode_index]() {
// Read from the sensor
std::get<1>(this->mode_funcs_[mode_index])();
@ -151,16 +166,10 @@ void LTR390Component::setup() {
return;
}
// Set gain
this->reg(LTR390_GAIN) = gain_;
// Set resolution and measurement rate
this->reg(LTR390_MEAS_RATE) = RESOLUTION_SETTING[this->res_];
// Set sensitivity by linearly scaling against known value in the datasheet
float gain_scale = GAINVALUES[this->gain_] / GAIN_MAX;
float intg_scale = (RESOLUTIONVALUE[this->res_] * 100) / INTG_MAX;
this->sensitivity_ = SENSITIVITY_MAX * gain_scale * intg_scale;
float gain_scale_uv = GAINVALUES[this->gain_uv_] / GAIN_MAX;
float intg_scale_uv = (RESOLUTIONVALUE[this->res_uv_] * 100) / INTG_MAX;
this->sensitivity_uv_ = SENSITIVITY_MAX * gain_scale_uv * intg_scale_uv;
// Set sensor read state
this->reading_ = false;
@ -176,7 +185,13 @@ void LTR390Component::setup() {
}
}
void LTR390Component::dump_config() { LOG_I2C_DEVICE(this); }
void LTR390Component::dump_config() {
LOG_I2C_DEVICE(this);
ESP_LOGCONFIG(TAG, " ALS Gain: X%.0f", GAINVALUES[this->gain_als_]);
ESP_LOGCONFIG(TAG, " ALS Resolution: %u-bit", RESOLUTION_BITS[this->res_als_]);
ESP_LOGCONFIG(TAG, " UV Gain: X%.0f", GAINVALUES[this->gain_uv_]);
ESP_LOGCONFIG(TAG, " UV Resolution: %u-bit", RESOLUTION_BITS[this->res_uv_]);
}
void LTR390Component::update() {
if (!this->reading_ && !mode_funcs_.empty()) {

View file

@ -49,8 +49,10 @@ class LTR390Component : public PollingComponent, public i2c::I2CDevice {
void dump_config() override;
void update() override;
void set_gain_value(LTR390GAIN gain) { this->gain_ = gain; }
void set_res_value(LTR390RESOLUTION res) { this->res_ = res; }
void set_als_gain_value(LTR390GAIN gain) { this->gain_als_ = gain; }
void set_uv_gain_value(LTR390GAIN gain) { this->gain_uv_ = gain; }
void set_als_res_value(LTR390RESOLUTION res) { this->res_als_ = res; }
void set_uv_res_value(LTR390RESOLUTION res) { this->res_uv_ = res; }
void set_wfac_value(float wfac) { this->wfac_ = wfac; }
void set_light_sensor(sensor::Sensor *light_sensor) { this->light_sensor_ = light_sensor; }
@ -71,9 +73,11 @@ class LTR390Component : public PollingComponent, public i2c::I2CDevice {
// a list of modes and corresponding read functions
std::vector<std::tuple<LTR390MODE, std::function<void()>>> mode_funcs_;
LTR390GAIN gain_;
LTR390RESOLUTION res_;
float sensitivity_;
LTR390GAIN gain_als_;
LTR390GAIN gain_uv_;
LTR390RESOLUTION res_als_;
LTR390RESOLUTION res_uv_;
float sensitivity_uv_;
float wfac_;
sensor::Sensor *light_sensor_{nullptr};

View file

@ -13,7 +13,7 @@ from esphome.const import (
UNIT_LUX,
)
CODEOWNERS = ["@sjtrny"]
CODEOWNERS = ["@sjtrny", "@latonita"]
DEPENDENCIES = ["i2c"]
ltr390_ns = cg.esphome_ns.namespace("ltr390")
@ -76,8 +76,24 @@ CONFIG_SCHEMA = cv.All(
accuracy_decimals=1,
device_class=DEVICE_CLASS_EMPTY,
),
cv.Optional(CONF_GAIN, default="X18"): cv.enum(GAIN_OPTIONS),
cv.Optional(CONF_RESOLUTION, default=20): cv.enum(RES_OPTIONS),
cv.Optional(CONF_GAIN, default="X18"): cv.Any(
cv.enum(GAIN_OPTIONS),
cv.Schema(
{
cv.Required(CONF_AMBIENT_LIGHT): cv.enum(GAIN_OPTIONS),
cv.Required(CONF_UV): cv.enum(GAIN_OPTIONS),
}
),
),
cv.Optional(CONF_RESOLUTION, default=20): cv.Any(
cv.enum(RES_OPTIONS),
cv.Schema(
{
cv.Required(CONF_AMBIENT_LIGHT): cv.enum(RES_OPTIONS),
cv.Required(CONF_UV): cv.enum(RES_OPTIONS),
}
),
),
cv.Optional(CONF_WINDOW_CORRECTION_FACTOR, default=1.0): cv.float_range(
min=1.0
),
@ -101,11 +117,25 @@ async def to_code(config):
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
cg.add(var.set_gain_value(config[CONF_GAIN]))
cg.add(var.set_res_value(config[CONF_RESOLUTION]))
cg.add(var.set_wfac_value(config[CONF_WINDOW_CORRECTION_FACTOR]))
for key, funcName in TYPES.items():
if key in config:
sens = await sensor.new_sensor(config[key])
cg.add(getattr(var, funcName)(sens))
gain_value = config[CONF_GAIN]
if isinstance(gain_value, dict):
cg.add(var.set_als_gain_value(gain_value[CONF_AMBIENT_LIGHT]))
cg.add(var.set_uv_gain_value(gain_value[CONF_UV]))
else:
cg.add(var.set_als_gain_value(gain_value))
cg.add(var.set_uv_gain_value(gain_value))
res_value = config[CONF_RESOLUTION]
if isinstance(res_value, dict):
cg.add(var.set_als_res_value(res_value[CONF_AMBIENT_LIGHT]))
cg.add(var.set_uv_res_value(res_value[CONF_UV]))
else:
cg.add(var.set_als_res_value(res_value))
cg.add(var.set_uv_res_value(res_value))

View file

@ -74,6 +74,9 @@ def mdns_service(
@coroutine_with_priority(55.0)
async def to_code(config):
if config[CONF_DISABLED] is True:
return
if CORE.using_arduino:
if CORE.is_esp32:
cg.add_library("ESPmDNS", None)
@ -92,9 +95,6 @@ async def to_code(config):
path="components/mdns",
)
if config[CONF_DISABLED]:
return
cg.add_define("USE_MDNS")
var = cg.new_Pvariable(config[CONF_ID])

View file

@ -1,5 +1,6 @@
#include "mdns_component.h"
#include "esphome/core/defines.h"
#ifdef USE_MDNS
#include "mdns_component.h"
#include "esphome/core/version.h"
#include "esphome/core/application.h"
#include "esphome/core/log.h"
@ -125,3 +126,4 @@ void MDNSComponent::dump_config() {
} // namespace mdns
} // namespace esphome
#endif

View file

@ -1,5 +1,6 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_MDNS
#include <string>
#include <vector>
#include "esphome/core/component.h"
@ -46,3 +47,4 @@ class MDNSComponent : public Component {
} // namespace mdns
} // namespace esphome
#endif

View file

@ -1,4 +1,5 @@
#ifdef USE_ESP32
#include "esphome/core/defines.h"
#if defined(USE_ESP32) && defined(USE_MDNS)
#include <mdns.h>
#include <cstring>

View file

@ -1,4 +1,5 @@
#if defined(USE_ESP8266) && defined(USE_ARDUINO)
#include "esphome/core/defines.h"
#if defined(USE_ESP8266) && defined(USE_ARDUINO) && defined(USE_MDNS)
#include <ESP8266mDNS.h>
#include "esphome/components/network/ip_address.h"

View file

@ -1,4 +1,5 @@
#ifdef USE_HOST
#include "esphome/core/defines.h"
#if defined(USE_HOST) && defined(USE_MDNS)
#include "esphome/components/network/ip_address.h"
#include "esphome/components/network/util.h"

View file

@ -1,4 +1,5 @@
#ifdef USE_LIBRETINY
#include "esphome/core/defines.h"
#if defined(USE_LIBRETINY) && defined(USE_MDNS)
#include "esphome/components/network/ip_address.h"
#include "esphome/components/network/util.h"

View file

@ -1,4 +1,5 @@
#ifdef USE_RP2040
#include "esphome/core/defines.h"
#if defined(USE_RP2040) && defined(USE_MDNS)
#include "esphome/components/network/ip_address.h"
#include "esphome/components/network/util.h"

View file

@ -9,7 +9,7 @@ import requests
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.core import CORE, HexInt, EsphomeError
from esphome.core import CORE, HexInt
from esphome.components import esp32, microphone
from esphome import automation, git, external_files
@ -41,9 +41,15 @@ CODEOWNERS = ["@kahrendt", "@jesserockz"]
DEPENDENCIES = ["microphone"]
DOMAIN = "micro_wake_word"
CONF_FEATURE_STEP_SIZE = "feature_step_size"
CONF_MODELS = "models"
CONF_ON_WAKE_WORD_DETECTED = "on_wake_word_detected"
CONF_PROBABILITY_CUTOFF = "probability_cutoff"
CONF_SLIDING_WINDOW_AVERAGE_SIZE = "sliding_window_average_size"
CONF_ON_WAKE_WORD_DETECTED = "on_wake_word_detected"
CONF_SLIDING_WINDOW_SIZE = "sliding_window_size"
CONF_TENSOR_ARENA_SIZE = "tensor_arena_size"
CONF_VAD = "vad"
TYPE_HTTP = "http"
@ -98,12 +104,14 @@ GIT_SCHEMA = cv.All(
_process_git_source,
)
KEY_WAKE_WORD = "wake_word"
KEY_AUTHOR = "author"
KEY_WEBSITE = "website"
KEY_VERSION = "version"
KEY_MICRO = "micro"
KEY_MINIMUM_ESPHOME_VERSION = "minimum_esphome_version"
KEY_TRAINED_LANGUAGES = "trained_languages"
KEY_VERSION = "version"
KEY_WAKE_WORD = "wake_word"
KEY_WEBSITE = "website"
MANIFEST_SCHEMA_V1 = cv.Schema(
{
@ -125,6 +133,29 @@ MANIFEST_SCHEMA_V1 = cv.Schema(
}
)
MANIFEST_SCHEMA_V2 = cv.Schema(
{
cv.Required(CONF_TYPE): "micro",
cv.Required(CONF_MODEL): cv.string,
cv.Required(KEY_AUTHOR): cv.string,
cv.Required(KEY_VERSION): cv.All(cv.int_, 2),
cv.Required(KEY_WAKE_WORD): cv.string,
cv.Required(KEY_TRAINED_LANGUAGES): cv.ensure_list(cv.string),
cv.Optional(KEY_WEBSITE): cv.url,
cv.Required(KEY_MICRO): cv.Schema(
{
cv.Required(CONF_FEATURE_STEP_SIZE): cv.int_range(min=0, max=30),
cv.Required(CONF_TENSOR_ARENA_SIZE): cv.int_,
cv.Required(CONF_PROBABILITY_CUTOFF): cv.float_,
cv.Required(CONF_SLIDING_WINDOW_SIZE): cv.positive_int,
cv.Required(KEY_MINIMUM_ESPHOME_VERSION): cv.All(
cv.version_number, cv.validate_esphome_version
),
}
),
}
)
def _compute_local_file_path(config: dict) -> Path:
url = config[CONF_URL]
@ -135,6 +166,24 @@ def _compute_local_file_path(config: dict) -> Path:
return base_dir / key
def _convert_manifest_v1_to_v2(v1_manifest):
v2_manifest = v1_manifest.copy()
v2_manifest[KEY_VERSION] = 2
v2_manifest[KEY_MICRO][CONF_SLIDING_WINDOW_SIZE] = v1_manifest[KEY_MICRO][
CONF_SLIDING_WINDOW_AVERAGE_SIZE
]
del v2_manifest[KEY_MICRO][CONF_SLIDING_WINDOW_AVERAGE_SIZE]
v2_manifest[KEY_MICRO][
CONF_TENSOR_ARENA_SIZE
] = 45672 # Original Inception-based V1 manifest models require a minimum of 45672 bytes
v2_manifest[KEY_MICRO][
CONF_FEATURE_STEP_SIZE
] = 20 # Original Inception-based V1 manifest models use a 20 ms feature step size
return v2_manifest
def _download_file(url: str, path: Path) -> bytes:
if not external_files.has_remote_file_changed(url, path):
_LOGGER.debug("Remote file has not changed, skipping download")
@ -155,6 +204,24 @@ def _download_file(url: str, path: Path) -> bytes:
return req.content
def _validate_manifest_version(manifest_data):
if manifest_version := manifest_data.get(KEY_VERSION):
if manifest_version == 1:
try:
MANIFEST_SCHEMA_V1(manifest_data)
except cv.Invalid as e:
raise cv.Invalid(f"Invalid manifest file: {e}") from e
elif manifest_version == 2:
try:
MANIFEST_SCHEMA_V2(manifest_data)
except cv.Invalid as e:
raise cv.Invalid(f"Invalid manifest file: {e}") from e
else:
raise cv.Invalid("Invalid manifest version")
else:
raise cv.Invalid("Invalid manifest file, missing 'version' key.")
def _process_http_source(config):
url = config[CONF_URL]
path = _compute_local_file_path(config)
@ -167,11 +234,6 @@ def _process_http_source(config):
if not isinstance(manifest_data, dict):
raise cv.Invalid("Manifest file must contain a JSON object")
try:
MANIFEST_SCHEMA_V1(manifest_data)
except cv.Invalid as e:
raise cv.Invalid(f"Invalid manifest file: {e}") from e
model = manifest_data[CONF_MODEL]
model_url = urljoin(url, model)
@ -206,7 +268,7 @@ def _validate_source_model_name(value):
return MODEL_SOURCE_SCHEMA(
{
CONF_TYPE: TYPE_HTTP,
CONF_URL: f"https://github.com/esphome/micro-wake-word-models/raw/main/models/{value}.json",
CONF_URL: f"https://github.com/esphome/micro-wake-word-models/raw/main/models/v2/{value}.json",
}
)
@ -260,18 +322,55 @@ MODEL_SOURCE_SCHEMA = cv.Any(
msg="Not a valid model name, local path, http(s) url, or github shorthand",
)
MODEL_SCHEMA = cv.Schema(
{
cv.Optional(CONF_MODEL): MODEL_SOURCE_SCHEMA,
cv.Optional(CONF_PROBABILITY_CUTOFF): cv.percentage,
cv.Optional(CONF_SLIDING_WINDOW_SIZE): cv.positive_int,
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
}
)
# Provide a default VAD model that could be overridden
VAD_MODEL_SCHEMA = MODEL_SCHEMA.extend(
cv.Schema(
{
cv.Optional(
CONF_MODEL,
default="vad",
): MODEL_SOURCE_SCHEMA,
}
)
)
def _maybe_empty_vad_schema(value):
# Idea borrowed from uart/__init__.py's ``maybe_empty_debug`` function. Accessed 2 July 2024.
# Loads a default VAD model without any parameters overridden.
if value is None:
value = {}
return VAD_MODEL_SCHEMA(value)
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(MicroWakeWord),
cv.GenerateID(CONF_MICROPHONE): cv.use_id(microphone.Microphone),
cv.Optional(CONF_PROBABILITY_CUTOFF): cv.percentage,
cv.Optional(CONF_SLIDING_WINDOW_AVERAGE_SIZE): cv.positive_int,
cv.Required(CONF_MODELS): cv.ensure_list(MODEL_SCHEMA),
cv.Optional(CONF_ON_WAKE_WORD_DETECTED): automation.validate_automation(
single=True
),
cv.Required(CONF_MODEL): MODEL_SOURCE_SCHEMA,
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
cv.Optional(CONF_VAD): _maybe_empty_vad_schema,
cv.Optional(CONF_MODEL): cv.invalid(
f"The {CONF_MODEL} parameter has moved to be a list element under the {CONF_MODELS} parameter."
),
cv.Optional(CONF_PROBABILITY_CUTOFF): cv.invalid(
f"The {CONF_PROBABILITY_CUTOFF} parameter has moved to be a list element under the {CONF_MODELS} parameter."
),
cv.Optional(CONF_SLIDING_WINDOW_AVERAGE_SIZE): cv.invalid(
f"The {CONF_SLIDING_WINDOW_AVERAGE_SIZE} parameter has been renamed to {CONF_SLIDING_WINDOW_SIZE} and moved to be a list element under the {CONF_MODELS} parameter."
),
}
).extend(cv.COMPONENT_SCHEMA),
cv.only_with_esp_idf,
@ -282,44 +381,20 @@ def _load_model_data(manifest_path: Path):
with open(manifest_path, encoding="utf-8") as f:
manifest = json.load(f)
try:
MANIFEST_SCHEMA_V1(manifest)
except cv.Invalid as e:
raise EsphomeError(f"Invalid manifest file: {e}") from e
_validate_manifest_version(manifest)
model_path = manifest_path.parent / manifest[CONF_MODEL]
with open(model_path, "rb") as f:
model = f.read()
if manifest.get(KEY_VERSION) == 1:
manifest = _convert_manifest_v1_to_v2(manifest)
return manifest, model
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
mic = await cg.get_variable(config[CONF_MICROPHONE])
cg.add(var.set_microphone(mic))
if on_wake_word_detection_config := config.get(CONF_ON_WAKE_WORD_DETECTED):
await automation.build_automation(
var.get_wake_word_detected_trigger(),
[(cg.std_string, "wake_word")],
on_wake_word_detection_config,
)
esp32.add_idf_component(
name="esp-tflite-micro",
repo="https://github.com/espressif/esp-tflite-micro",
)
cg.add_build_flag("-DTF_LITE_STATIC_MEMORY")
cg.add_build_flag("-DTF_LITE_DISABLE_X86_NEON")
cg.add_build_flag("-DESP_NN")
model_config = config.get(CONF_MODEL)
data = []
def _model_config_to_manifest_data(model_config):
if model_config[CONF_TYPE] == TYPE_GIT:
# compute path to model file
key = f"{model_config[CONF_URL]}@{model_config.get(CONF_REF)}"
@ -337,23 +412,95 @@ async def to_code(config):
else:
raise ValueError("Unsupported config type: {model_config[CONF_TYPE]}")
manifest, data = _load_model_data(file)
return _load_model_data(file)
def _feature_step_size_validate(config):
features_step_size = None
for model_parameters in config[CONF_MODELS]:
model_config = model_parameters.get(CONF_MODEL)
manifest, _ = _model_config_to_manifest_data(model_config)
model_step_size = manifest[KEY_MICRO][CONF_FEATURE_STEP_SIZE]
if features_step_size is None:
features_step_size = model_step_size
elif features_step_size != model_step_size:
raise cv.Invalid("Cannot load models with different features step sizes.")
FINAL_VALIDATE_SCHEMA = _feature_step_size_validate
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
mic = await cg.get_variable(config[CONF_MICROPHONE])
cg.add(var.set_microphone(mic))
esp32.add_idf_component(
name="esp-tflite-micro",
repo="https://github.com/espressif/esp-tflite-micro",
ref="v1.3.1",
)
cg.add_build_flag("-DTF_LITE_STATIC_MEMORY")
cg.add_build_flag("-DTF_LITE_DISABLE_X86_NEON")
cg.add_build_flag("-DESP_NN")
if on_wake_word_detection_config := config.get(CONF_ON_WAKE_WORD_DETECTED):
await automation.build_automation(
var.get_wake_word_detected_trigger(),
[(cg.std_string, "wake_word")],
on_wake_word_detection_config,
)
if vad_model := config.get(CONF_VAD):
cg.add_define("USE_MICRO_WAKE_WORD_VAD")
# Use the general model loading code for the VAD codegen
config[CONF_MODELS].append(vad_model)
for model_parameters in config[CONF_MODELS]:
model_config = model_parameters.get(CONF_MODEL)
data = []
manifest, data = _model_config_to_manifest_data(model_config)
rhs = [HexInt(x) for x in data]
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
cg.add(var.set_model_start(prog_arr))
prog_arr = cg.progmem_array(model_parameters[CONF_RAW_DATA_ID], rhs)
probability_cutoff = config.get(
probability_cutoff = model_parameters.get(
CONF_PROBABILITY_CUTOFF, manifest[KEY_MICRO][CONF_PROBABILITY_CUTOFF]
)
cg.add(var.set_probability_cutoff(probability_cutoff))
sliding_window_average_size = config.get(
CONF_SLIDING_WINDOW_AVERAGE_SIZE,
manifest[KEY_MICRO][CONF_SLIDING_WINDOW_AVERAGE_SIZE],
sliding_window_size = model_parameters.get(
CONF_SLIDING_WINDOW_SIZE,
manifest[KEY_MICRO][CONF_SLIDING_WINDOW_SIZE],
)
cg.add(var.set_sliding_window_average_size(sliding_window_average_size))
cg.add(var.set_wake_word(manifest[KEY_WAKE_WORD]))
if manifest[KEY_WAKE_WORD] == "vad":
cg.add(
var.add_vad_model(
prog_arr,
probability_cutoff,
sliding_window_size,
manifest[KEY_MICRO][CONF_TENSOR_ARENA_SIZE],
)
)
else:
cg.add(
var.add_wake_word_model(
prog_arr,
probability_cutoff,
sliding_window_size,
manifest[KEY_WAKE_WORD],
manifest[KEY_MICRO][CONF_TENSOR_ARENA_SIZE],
)
)
cg.add(var.set_features_step_size(manifest[KEY_MICRO][CONF_FEATURE_STEP_SIZE]))
cg.add_library("kahrendt/ESPMicroSpeechFeatures", "1.0.0")
MICRO_WAKE_WORD_ACTION_SCHEMA = cv.Schema({cv.GenerateID(): cv.use_id(MicroWakeWord)})

View file

@ -1,493 +0,0 @@
#pragma once
#ifdef USE_ESP_IDF
// Converted audio_preprocessor_int8.tflite
// From https://github.com/tensorflow/tflite-micro/tree/main/tensorflow/lite/micro/examples/micro_speech/models accessed
// January 2024
//
// Copyright 2023 The TensorFlow Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
namespace esphome {
namespace micro_wake_word {
const unsigned char G_AUDIO_PREPROCESSOR_INT8_TFLITE[] = {
0x1c, 0x00, 0x00, 0x00, 0x54, 0x46, 0x4c, 0x33, 0x14, 0x00, 0x20, 0x00, 0x1c, 0x00, 0x18, 0x00, 0x14, 0x00, 0x10,
0x00, 0x0c, 0x00, 0x00, 0x00, 0x08, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x88, 0x00,
0x00, 0x00, 0xe0, 0x00, 0x00, 0x00, 0x80, 0x0e, 0x00, 0x00, 0x90, 0x0e, 0x00, 0x00, 0xcc, 0x1f, 0x00, 0x00, 0x03,
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xe2, 0xeb, 0xff, 0xff, 0x0c, 0x00, 0x00, 0x00,
0x1c, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67,
0x5f, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x94, 0xff,
0xff, 0xff, 0x2a, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x6f, 0x75, 0x74, 0x70, 0x75,
0x74, 0x5f, 0x30, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xc2, 0xf5, 0xff, 0xff,
0x04, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x61, 0x75, 0x64, 0x69, 0x6f, 0x5f, 0x66, 0x72, 0x61, 0x6d, 0x65,
0x00, 0x02, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xdc, 0xff, 0xff, 0xff, 0x2d, 0x00,
0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x43, 0x4f, 0x4e, 0x56, 0x45, 0x52, 0x53, 0x49, 0x4f,
0x4e, 0x5f, 0x4d, 0x45, 0x54, 0x41, 0x44, 0x41, 0x54, 0x41, 0x00, 0x08, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x04, 0x00,
0x08, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x6d, 0x69, 0x6e,
0x5f, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x00, 0x2e, 0x00,
0x00, 0x00, 0x9c, 0x0d, 0x00, 0x00, 0x94, 0x0d, 0x00, 0x00, 0xc4, 0x09, 0x00, 0x00, 0x6c, 0x09, 0x00, 0x00, 0x48,
0x09, 0x00, 0x00, 0x34, 0x09, 0x00, 0x00, 0x20, 0x09, 0x00, 0x00, 0x0c, 0x09, 0x00, 0x00, 0xf8, 0x08, 0x00, 0x00,
0xec, 0x07, 0x00, 0x00, 0x88, 0x07, 0x00, 0x00, 0x24, 0x07, 0x00, 0x00, 0xc0, 0x06, 0x00, 0x00, 0x38, 0x04, 0x00,
0x00, 0xb0, 0x01, 0x00, 0x00, 0x9c, 0x01, 0x00, 0x00, 0x88, 0x01, 0x00, 0x00, 0x74, 0x01, 0x00, 0x00, 0x60, 0x01,
0x00, 0x00, 0x4c, 0x01, 0x00, 0x00, 0x44, 0x01, 0x00, 0x00, 0x3c, 0x01, 0x00, 0x00, 0x34, 0x01, 0x00, 0x00, 0x2c,
0x01, 0x00, 0x00, 0x24, 0x01, 0x00, 0x00, 0x1c, 0x01, 0x00, 0x00, 0x14, 0x01, 0x00, 0x00, 0x0c, 0x01, 0x00, 0x00,
0x04, 0x01, 0x00, 0x00, 0xfc, 0x00, 0x00, 0x00, 0xf4, 0x00, 0x00, 0x00, 0xec, 0x00, 0x00, 0x00, 0xe4, 0x00, 0x00,
0x00, 0xdc, 0x00, 0x00, 0x00, 0xd4, 0x00, 0x00, 0x00, 0xcc, 0x00, 0x00, 0x00, 0xc4, 0x00, 0x00, 0x00, 0xbc, 0x00,
0x00, 0x00, 0xb4, 0x00, 0x00, 0x00, 0xac, 0x00, 0x00, 0x00, 0xa4, 0x00, 0x00, 0x00, 0x9c, 0x00, 0x00, 0x00, 0x94,
0x00, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, 0x6c, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xf2, 0xf6, 0xff, 0xff,
0x04, 0x00, 0x00, 0x00, 0x58, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x08, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x04,
0x00, 0x08, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x08, 0x00, 0x0c, 0x00, 0x08, 0x00,
0x07, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x0a, 0x00, 0x10, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x04, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00,
0x02, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x32, 0x2e, 0x31, 0x32, 0x2e, 0x30, 0x00,
0x00, 0x56, 0xf7, 0xff, 0xff, 0x04, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x32, 0x2e, 0x38, 0x2e, 0x30, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xd4, 0xe1, 0xff, 0xff, 0xd8, 0xe1, 0xff, 0xff, 0xdc,
0xe1, 0xff, 0xff, 0xe0, 0xe1, 0xff, 0xff, 0xe4, 0xe1, 0xff, 0xff, 0xe8, 0xe1, 0xff, 0xff, 0xec, 0xe1, 0xff, 0xff,
0xf0, 0xe1, 0xff, 0xff, 0xf4, 0xe1, 0xff, 0xff, 0xf8, 0xe1, 0xff, 0xff, 0xfc, 0xe1, 0xff, 0xff, 0x00, 0xe2, 0xff,
0xff, 0x04, 0xe2, 0xff, 0xff, 0x08, 0xe2, 0xff, 0xff, 0x0c, 0xe2, 0xff, 0xff, 0x10, 0xe2, 0xff, 0xff, 0x14, 0xe2,
0xff, 0xff, 0x18, 0xe2, 0xff, 0xff, 0x1c, 0xe2, 0xff, 0xff, 0x20, 0xe2, 0xff, 0xff, 0x24, 0xe2, 0xff, 0xff, 0x28,
0xe2, 0xff, 0xff, 0x2c, 0xe2, 0xff, 0xff, 0x30, 0xe2, 0xff, 0xff, 0xd2, 0xf7, 0xff, 0xff, 0x04, 0x00, 0x00, 0x00,
0x04, 0x00, 0x00, 0x00, 0x9a, 0x02, 0x00, 0x00, 0xe2, 0xf7, 0xff, 0xff, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00,
0x00, 0x00, 0x01, 0x00, 0x00, 0xf2, 0xf7, 0xff, 0xff, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x80, 0xff,
0xff, 0xff, 0x02, 0xf8, 0xff, 0xff, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x12,
0xf8, 0xff, 0xff, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x22, 0xf8, 0xff, 0xff,
0x04, 0x00, 0x00, 0x00, 0x78, 0x02, 0x00, 0x00, 0x00, 0x00, 0x61, 0x05, 0x00, 0x00, 0x00, 0x00, 0x23, 0x0b, 0x41,
0x01, 0x00, 0x00, 0x00, 0x00, 0xb3, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x74, 0x0e, 0x80, 0x05,
0x00, 0x00, 0x00, 0x00, 0xd1, 0x0c, 0x63, 0x04, 0x00, 0x00, 0x00, 0x00, 0x34, 0x0c, 0x3f, 0x04, 0x00, 0x00, 0x00,
0x00, 0x81, 0x0c, 0xf7, 0x04, 0x00, 0x00, 0x00, 0x00, 0x9f, 0x0d, 0x77, 0x06, 0x00, 0x00, 0x00, 0x00, 0x7b, 0x0f,
0xa9, 0x08, 0x01, 0x02, 0x7f, 0x0b, 0x22, 0x05, 0x00, 0x00, 0x00, 0x00, 0xe9, 0x0e, 0xd1, 0x08, 0xdb, 0x02, 0x00,
0x00, 0x00, 0x00, 0x03, 0x0d, 0x4a, 0x07, 0xad, 0x01, 0x2c, 0x0c, 0xc6, 0x06, 0x79, 0x01, 0x00, 0x00, 0x00, 0x00,
0x45, 0x0c, 0x29, 0x07, 0x23, 0x02, 0x34, 0x0d, 0x5b, 0x08, 0x96, 0x03, 0x00, 0x00, 0x00, 0x00, 0xe5, 0x0e, 0x48,
0x0a, 0xbd, 0x05, 0x45, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xde, 0x0c, 0x88, 0x08, 0x43, 0x04,
0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe9, 0x0b, 0xd3, 0x07, 0xcb, 0x03, 0xd2, 0x0f, 0xe7,
0x0b, 0x09, 0x08, 0x39, 0x04, 0x76, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xbf, 0x0c, 0x14, 0x09,
0x75, 0x05, 0xe2, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5a, 0x0e, 0xdd, 0x0a, 0x6b, 0x07, 0x03,
0x04, 0xa6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x53, 0x0d, 0x09, 0x0a, 0xc9, 0x06, 0x93, 0x03, 0x65, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x41, 0x0d, 0x25, 0x0a, 0x12, 0x07, 0x07, 0x04, 0x05, 0x01, 0x00, 0x00, 0x00,
0x00, 0x0a, 0x0e, 0x17, 0x0b, 0x2c, 0x08, 0x49, 0x05, 0x6d, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x98, 0x0f, 0xcb, 0x0c, 0x04, 0x0a, 0x44, 0x07, 0x8b, 0x04, 0xd8, 0x01, 0x00, 0x00, 0x00, 0x00, 0x2c, 0x0f, 0x87,
0x0c, 0xe7, 0x09, 0x4e, 0x07, 0xba, 0x04, 0x2d, 0x02, 0x00, 0x00, 0x00, 0x00, 0xa5, 0x0f, 0x23, 0x0d, 0xa7, 0x0a,
0x30, 0x08, 0xbe, 0x05, 0x52, 0x03, 0xeb, 0x00, 0x89, 0x0e, 0x2c, 0x0c, 0xd4, 0x09, 0x81, 0x07, 0x33, 0x05, 0xe9,
0x02, 0xa5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x0e, 0x29, 0x0c, 0xf1, 0x09, 0xbe, 0x07, 0x90, 0x05, 0x65, 0x03,
0x3f, 0x01, 0x1d, 0x0f, 0xff, 0x0c, 0xe5, 0x0a, 0xcf, 0x08, 0xbc, 0x06, 0xae, 0x04, 0xa3, 0x02, 0x9c, 0x00, 0x99,
0x0e, 0x99, 0x0c, 0x9d, 0x0a, 0xa4, 0x08, 0xaf, 0x06, 0xbd, 0x04, 0xcf, 0x02, 0xe4, 0x00, 0xfc, 0x0e, 0x17, 0x0d,
0x36, 0x0b, 0x57, 0x09, 0x7c, 0x07, 0xa4, 0x05, 0xcf, 0x03, 0xfd, 0x01, 0x2e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x62, 0x0e, 0x98, 0x0c, 0xd2, 0x0a, 0x0e, 0x09, 0x4d, 0x07, 0x8f, 0x05, 0xd4, 0x03, 0x1b, 0x02,
0x65, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb1, 0x0e, 0x00, 0x0d, 0x52, 0x0b, 0xa6, 0x09, 0xfd, 0x07, 0x56, 0x06, 0xb1,
0x04, 0x0f, 0x03, 0x6f, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xd2, 0x0f, 0x37, 0x0e, 0x9e, 0x0c,
0x08, 0x0b, 0x73, 0x09, 0xe1, 0x07, 0x52, 0x06, 0xc4, 0x04, 0x38, 0x03, 0xaf, 0x01, 0x28, 0x00, 0xa3, 0x0e, 0x1f,
0x0d, 0x9e, 0x0b, 0x1f, 0x0a, 0xa2, 0x08, 0x27, 0x07, 0xae, 0x05, 0x37, 0x04, 0xc2, 0x02, 0x4e, 0x01, 0x00, 0x00,
0x00, 0x00, 0xdd, 0x0f, 0x6d, 0x0e, 0xff, 0x0c, 0x93, 0x0b, 0x29, 0x0a, 0xc1, 0x08, 0x5a, 0x07, 0xf5, 0x05, 0x92,
0x04, 0x30, 0x03, 0xd1, 0x01, 0x73, 0x00, 0x16, 0x0f, 0xbc, 0x0d, 0x62, 0x0c, 0x0b, 0x0b, 0xb5, 0x09, 0x61, 0x08,
0x0e, 0x07, 0xbd, 0x05, 0x6d, 0x04, 0x1f, 0x03, 0xd3, 0x01, 0x88, 0x00, 0x3e, 0x0f, 0xf6, 0x0d, 0xaf, 0x0c, 0x6a,
0x0b, 0x27, 0x0a, 0xe4, 0x08, 0xa3, 0x07, 0x64, 0x06, 0x26, 0x05, 0xe9, 0x03, 0xae, 0x02, 0x74, 0x01, 0x3b, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x0f, 0xce, 0x0d, 0x99, 0x0c, 0x66, 0x0b, 0x34, 0x0a, 0x03,
0x09, 0xd3, 0x07, 0xa5, 0x06, 0x78, 0x05, 0x4c, 0x04, 0x22, 0x03, 0xf8, 0x01, 0xd0, 0x00, 0x00, 0x00, 0x00, 0x00,
0xa9, 0x0f, 0x83, 0x0e, 0x5f, 0x0d, 0x3b, 0x0c, 0x19, 0x0b, 0xf8, 0x09, 0xd8, 0x08, 0xb9, 0x07, 0x9b, 0x06, 0x7e,
0x05, 0x63, 0x04, 0x48, 0x03, 0x2f, 0x02, 0x17, 0x01, 0x00, 0x00, 0x00, 0x00, 0xa6, 0xfa, 0xff, 0xff, 0x04, 0x00,
0x00, 0x00, 0x78, 0x02, 0x00, 0x00, 0x00, 0x00, 0x9e, 0x0a, 0x00, 0x00, 0x00, 0x00, 0xdc, 0x04, 0xbe, 0x0e, 0x00,
0x00, 0x00, 0x00, 0x4c, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8b, 0x01, 0x7f, 0x0a, 0x00, 0x00,
0x00, 0x00, 0x2e, 0x03, 0x9c, 0x0b, 0x00, 0x00, 0x00, 0x00, 0xcb, 0x03, 0xc0, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x7e,
0x03, 0x08, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x60, 0x02, 0x88, 0x09, 0x00, 0x00, 0x00, 0x00, 0x84, 0x00, 0x56, 0x07,
0xfe, 0x0d, 0x80, 0x04, 0xdd, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x16, 0x01, 0x2e, 0x07, 0x24, 0x0d, 0x00, 0x00, 0x00,
0x00, 0xfc, 0x02, 0xb5, 0x08, 0x52, 0x0e, 0xd3, 0x03, 0x39, 0x09, 0x86, 0x0e, 0x00, 0x00, 0x00, 0x00, 0xba, 0x03,
0xd6, 0x08, 0xdc, 0x0d, 0xcb, 0x02, 0xa4, 0x07, 0x69, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x1a, 0x01, 0xb7, 0x05, 0x42,
0x0a, 0xba, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x03, 0x77, 0x07, 0xbc, 0x0b, 0xf1, 0x0f,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x04, 0x2c, 0x08, 0x34, 0x0c, 0x2d, 0x00, 0x18, 0x04, 0xf6,
0x07, 0xc6, 0x0b, 0x89, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x03, 0xeb, 0x06, 0x8a, 0x0a,
0x1d, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa5, 0x01, 0x22, 0x05, 0x94, 0x08, 0xfc, 0x0b, 0x59,
0x0f, 0x00, 0x00, 0x00, 0x00, 0xac, 0x02, 0xf6, 0x05, 0x36, 0x09, 0x6c, 0x0c, 0x9a, 0x0f, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xbe, 0x02, 0xda, 0x05, 0xed, 0x08, 0xf8, 0x0b, 0xfa, 0x0e, 0x00, 0x00, 0x00, 0x00, 0xf5,
0x01, 0xe8, 0x04, 0xd3, 0x07, 0xb6, 0x0a, 0x92, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x67, 0x00,
0x34, 0x03, 0xfb, 0x05, 0xbb, 0x08, 0x74, 0x0b, 0x27, 0x0e, 0x00, 0x00, 0x00, 0x00, 0xd3, 0x00, 0x78, 0x03, 0x18,
0x06, 0xb1, 0x08, 0x45, 0x0b, 0xd2, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x5a, 0x00, 0xdc, 0x02, 0x58, 0x05, 0xcf, 0x07,
0x41, 0x0a, 0xad, 0x0c, 0x14, 0x0f, 0x76, 0x01, 0xd3, 0x03, 0x2b, 0x06, 0x7e, 0x08, 0xcc, 0x0a, 0x16, 0x0d, 0x5a,
0x0f, 0x00, 0x00, 0x00, 0x00, 0x9b, 0x01, 0xd6, 0x03, 0x0e, 0x06, 0x41, 0x08, 0x6f, 0x0a, 0x9a, 0x0c, 0xc0, 0x0e,
0xe2, 0x00, 0x00, 0x03, 0x1a, 0x05, 0x30, 0x07, 0x43, 0x09, 0x51, 0x0b, 0x5c, 0x0d, 0x63, 0x0f, 0x66, 0x01, 0x66,
0x03, 0x62, 0x05, 0x5b, 0x07, 0x50, 0x09, 0x42, 0x0b, 0x30, 0x0d, 0x1b, 0x0f, 0x03, 0x01, 0xe8, 0x02, 0xc9, 0x04,
0xa8, 0x06, 0x83, 0x08, 0x5b, 0x0a, 0x30, 0x0c, 0x02, 0x0e, 0xd1, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x9d, 0x01, 0x67, 0x03, 0x2d, 0x05, 0xf1, 0x06, 0xb2, 0x08, 0x70, 0x0a, 0x2b, 0x0c, 0xe4, 0x0d, 0x9a, 0x0f,
0x00, 0x00, 0x00, 0x00, 0x4e, 0x01, 0xff, 0x02, 0xad, 0x04, 0x59, 0x06, 0x02, 0x08, 0xa9, 0x09, 0x4e, 0x0b, 0xf0,
0x0c, 0x90, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2d, 0x00, 0xc8, 0x01, 0x61, 0x03, 0xf7, 0x04,
0x8c, 0x06, 0x1e, 0x08, 0xad, 0x09, 0x3b, 0x0b, 0xc7, 0x0c, 0x50, 0x0e, 0xd7, 0x0f, 0x5c, 0x01, 0xe0, 0x02, 0x61,
0x04, 0xe0, 0x05, 0x5d, 0x07, 0xd8, 0x08, 0x51, 0x0a, 0xc8, 0x0b, 0x3d, 0x0d, 0xb1, 0x0e, 0x00, 0x00, 0x00, 0x00,
0x22, 0x00, 0x92, 0x01, 0x00, 0x03, 0x6c, 0x04, 0xd6, 0x05, 0x3e, 0x07, 0xa5, 0x08, 0x0a, 0x0a, 0x6d, 0x0b, 0xcf,
0x0c, 0x2e, 0x0e, 0x8c, 0x0f, 0xe9, 0x00, 0x43, 0x02, 0x9d, 0x03, 0xf4, 0x04, 0x4a, 0x06, 0x9e, 0x07, 0xf1, 0x08,
0x42, 0x0a, 0x92, 0x0b, 0xe0, 0x0c, 0x2c, 0x0e, 0x77, 0x0f, 0xc1, 0x00, 0x09, 0x02, 0x50, 0x03, 0x95, 0x04, 0xd8,
0x05, 0x1b, 0x07, 0x5c, 0x08, 0x9b, 0x09, 0xd9, 0x0a, 0x16, 0x0c, 0x51, 0x0d, 0x8b, 0x0e, 0xc4, 0x0f, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfb, 0x00, 0x31, 0x02, 0x66, 0x03, 0x99, 0x04, 0xcb, 0x05, 0xfc, 0x06, 0x2c,
0x08, 0x5a, 0x09, 0x87, 0x0a, 0xb3, 0x0b, 0xdd, 0x0c, 0x07, 0x0e, 0x2f, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x56, 0x00,
0x7c, 0x01, 0xa0, 0x02, 0xc4, 0x03, 0xe6, 0x04, 0x07, 0x06, 0x27, 0x07, 0x46, 0x08, 0x64, 0x09, 0x81, 0x0a, 0x9c,
0x0b, 0xb7, 0x0c, 0xd0, 0x0d, 0xe8, 0x0e, 0x00, 0x10, 0x00, 0x00, 0x2a, 0xfd, 0xff, 0xff, 0x04, 0x00, 0x00, 0x00,
0x52, 0x00, 0x00, 0x00, 0x04, 0x00, 0x06, 0x00, 0x08, 0x00, 0x08, 0x00, 0x0a, 0x00, 0x0c, 0x00, 0x0e, 0x00, 0x10,
0x00, 0x12, 0x00, 0x16, 0x00, 0x18, 0x00, 0x1a, 0x00, 0x1e, 0x00, 0x20, 0x00, 0x24, 0x00, 0x26, 0x00, 0x2a, 0x00,
0x2e, 0x00, 0x32, 0x00, 0x36, 0x00, 0x3a, 0x00, 0x40, 0x00, 0x44, 0x00, 0x4a, 0x00, 0x4e, 0x00, 0x54, 0x00, 0x5a,
0x00, 0x62, 0x00, 0x68, 0x00, 0x70, 0x00, 0x78, 0x00, 0x80, 0x00, 0x88, 0x00, 0x92, 0x00, 0x9a, 0x00, 0xa6, 0x00,
0xb0, 0x00, 0xbc, 0x00, 0xc8, 0x00, 0xd4, 0x00, 0xe2, 0x00, 0x00, 0x00, 0x8a, 0xfd, 0xff, 0xff, 0x04, 0x00, 0x00,
0x00, 0x52, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x08, 0x00, 0x0c, 0x00, 0x10, 0x00, 0x14, 0x00, 0x18, 0x00,
0x1c, 0x00, 0x20, 0x00, 0x24, 0x00, 0x28, 0x00, 0x2c, 0x00, 0x30, 0x00, 0x34, 0x00, 0x38, 0x00, 0x3c, 0x00, 0x44,
0x00, 0x4c, 0x00, 0x50, 0x00, 0x58, 0x00, 0x60, 0x00, 0x68, 0x00, 0x70, 0x00, 0x78, 0x00, 0x80, 0x00, 0x88, 0x00,
0x90, 0x00, 0x98, 0x00, 0xa0, 0x00, 0xa8, 0x00, 0xb0, 0x00, 0xb8, 0x00, 0xc4, 0x00, 0xd0, 0x00, 0xdc, 0x00, 0xe8,
0x00, 0xf4, 0x00, 0x00, 0x01, 0x0c, 0x01, 0x1c, 0x01, 0x2c, 0x01, 0x00, 0x00, 0xea, 0xfd, 0xff, 0xff, 0x04, 0x00,
0x00, 0x00, 0x52, 0x00, 0x00, 0x00, 0x04, 0x00, 0x04, 0x00, 0x04, 0x00, 0x04, 0x00, 0x04, 0x00, 0x04, 0x00, 0x04,
0x00, 0x04, 0x00, 0x04, 0x00, 0x04, 0x00, 0x04, 0x00, 0x04, 0x00, 0x04, 0x00, 0x04, 0x00, 0x04, 0x00, 0x08, 0x00,
0x08, 0x00, 0x04, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08,
0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x0c, 0x00, 0x0c, 0x00, 0x0c, 0x00, 0x0c, 0x00,
0x0c, 0x00, 0x0c, 0x00, 0x0c, 0x00, 0x10, 0x00, 0x10, 0x00, 0x10, 0x00, 0x00, 0x00, 0x4a, 0xfe, 0xff, 0xff, 0x04,
0x00, 0x00, 0x00, 0xfa, 0x00, 0x00, 0x00, 0x7c, 0x7f, 0x79, 0x7f, 0x76, 0x7f, 0xfa, 0xff, 0x00, 0x00, 0x00, 0x00,
0x70, 0x7f, 0xf4, 0xff, 0x00, 0x00, 0x00, 0x00, 0x64, 0x7f, 0xe9, 0xff, 0xfe, 0xff, 0x00, 0x00, 0x4b, 0x7f, 0xd0,
0xff, 0x00, 0x00, 0x00, 0x00, 0x1b, 0x7f, 0xa0, 0xff, 0x00, 0x00, 0x00, 0x00, 0xbb, 0x7e, 0x42, 0xff, 0x00, 0x00,
0x00, 0x00, 0xfd, 0x7d, 0x86, 0xfe, 0x04, 0x00, 0x00, 0x00, 0x87, 0x7c, 0x1d, 0xfd, 0x12, 0x00, 0x00, 0x00, 0xb6,
0x79, 0x7f, 0xfa, 0x3e, 0x00, 0x00, 0x00, 0x73, 0x74, 0xf9, 0xf5, 0xca, 0x00, 0x00, 0x00, 0x36, 0x6b, 0x33, 0xef,
0x32, 0x02, 0x00, 0x00, 0x9b, 0x5c, 0x87, 0xe7, 0xce, 0x04, 0x00, 0x00, 0xf0, 0x48, 0xde, 0xe2, 0xa0, 0x07, 0x00,
0x00, 0x6e, 0x33, 0x8a, 0xe4, 0xa4, 0x08, 0x00, 0x00, 0x9c, 0x20, 0x22, 0xeb, 0x4c, 0x07, 0x00, 0x00, 0x0a, 0x13,
0x7d, 0xf2, 0x02, 0x05, 0x00, 0x00, 0x89, 0x0a, 0x17, 0xf8, 0x06, 0x03, 0x00, 0x00, 0xa6, 0x05, 0xa0, 0xfb, 0xb4,
0x01, 0x00, 0x00, 0xfa, 0x02, 0xac, 0xfd, 0xe8, 0x00, 0x00, 0x00, 0x8e, 0x01, 0xc7, 0xfe, 0x7a, 0x00, 0x00, 0x00,
0xcf, 0x00, 0x5c, 0xff, 0x40, 0x00, 0x00, 0x00, 0x6b, 0x00, 0xab, 0xff, 0x22, 0x00, 0x00, 0x00, 0x38, 0x00, 0xd3,
0xff, 0x12, 0x00, 0x00, 0x00, 0x1d, 0x00, 0xea, 0xff, 0x08, 0x00, 0x00, 0x00, 0x0f, 0x00, 0xf3, 0xff, 0x06, 0x00,
0x00, 0x00, 0x08, 0x00, 0xf8, 0xff, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0xfe, 0xff, 0x00, 0x00, 0x00, 0x00, 0x02,
0x00, 0xfd, 0xff, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0xfd, 0xff,
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x52, 0xff, 0xff, 0xff, 0x04, 0x00, 0x00,
0x00, 0x04, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x62, 0xff, 0xff, 0xff, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00,
0x00, 0x00, 0xf1, 0x00, 0x00, 0x00, 0x72, 0xff, 0xff, 0xff, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01,
0x00, 0x00, 0x00, 0x82, 0xff, 0xff, 0xff, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x4d, 0x01, 0x00, 0x00,
0x92, 0xff, 0xff, 0xff, 0x04, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb2, 0xff, 0xff, 0xff, 0x04, 0x00,
0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x08, 0x00,
0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xc0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
0x00, 0x02, 0x00, 0x04, 0x00, 0x05, 0x00, 0x07, 0x00, 0x0a, 0x00, 0x0d, 0x00, 0x10, 0x00, 0x13, 0x00, 0x17, 0x00,
0x1b, 0x00, 0x20, 0x00, 0x25, 0x00, 0x2a, 0x00, 0x30, 0x00, 0x35, 0x00, 0x3c, 0x00, 0x42, 0x00, 0x49, 0x00, 0x51,
0x00, 0x58, 0x00, 0x60, 0x00, 0x68, 0x00, 0x71, 0x00, 0x7a, 0x00, 0x83, 0x00, 0x8d, 0x00, 0x97, 0x00, 0xa1, 0x00,
0xac, 0x00, 0xb7, 0x00, 0xc2, 0x00, 0xcd, 0x00, 0xd9, 0x00, 0xe5, 0x00, 0xf2, 0x00, 0xff, 0x00, 0x0c, 0x01, 0x19,
0x01, 0x27, 0x01, 0x35, 0x01, 0x43, 0x01, 0x52, 0x01, 0x61, 0x01, 0x70, 0x01, 0x7f, 0x01, 0x8f, 0x01, 0x9f, 0x01,
0xaf, 0x01, 0xc0, 0x01, 0xd1, 0x01, 0xe2, 0x01, 0xf3, 0x01, 0x05, 0x02, 0x17, 0x02, 0x29, 0x02, 0x3c, 0x02, 0x4e,
0x02, 0x61, 0x02, 0x75, 0x02, 0x88, 0x02, 0x9c, 0x02, 0xb0, 0x02, 0xc4, 0x02, 0xd8, 0x02, 0xed, 0x02, 0x02, 0x03,
0x17, 0x03, 0x2c, 0x03, 0x41, 0x03, 0x57, 0x03, 0x6d, 0x03, 0x83, 0x03, 0x99, 0x03, 0xb0, 0x03, 0xc7, 0x03, 0xdd,
0x03, 0xf4, 0x03, 0x0c, 0x04, 0x23, 0x04, 0x3b, 0x04, 0x52, 0x04, 0x6a, 0x04, 0x82, 0x04, 0x9a, 0x04, 0xb3, 0x04,
0xcb, 0x04, 0xe4, 0x04, 0xfd, 0x04, 0x16, 0x05, 0x2f, 0x05, 0x48, 0x05, 0x61, 0x05, 0x7a, 0x05, 0x94, 0x05, 0xad,
0x05, 0xc7, 0x05, 0xe1, 0x05, 0xfb, 0x05, 0x15, 0x06, 0x2f, 0x06, 0x49, 0x06, 0x63, 0x06, 0x7e, 0x06, 0x98, 0x06,
0xb2, 0x06, 0xcd, 0x06, 0xe7, 0x06, 0x02, 0x07, 0x1d, 0x07, 0x37, 0x07, 0x52, 0x07, 0x6d, 0x07, 0x87, 0x07, 0xa2,
0x07, 0xbd, 0x07, 0xd8, 0x07, 0xf3, 0x07, 0x0d, 0x08, 0x28, 0x08, 0x43, 0x08, 0x5e, 0x08, 0x79, 0x08, 0x93, 0x08,
0xae, 0x08, 0xc9, 0x08, 0xe3, 0x08, 0xfe, 0x08, 0x19, 0x09, 0x33, 0x09, 0x4e, 0x09, 0x68, 0x09, 0x82, 0x09, 0x9d,
0x09, 0xb7, 0x09, 0xd1, 0x09, 0xeb, 0x09, 0x05, 0x0a, 0x1f, 0x0a, 0x39, 0x0a, 0x53, 0x0a, 0x6c, 0x0a, 0x86, 0x0a,
0x9f, 0x0a, 0xb8, 0x0a, 0xd1, 0x0a, 0xea, 0x0a, 0x03, 0x0b, 0x1c, 0x0b, 0x35, 0x0b, 0x4d, 0x0b, 0x66, 0x0b, 0x7e,
0x0b, 0x96, 0x0b, 0xae, 0x0b, 0xc5, 0x0b, 0xdd, 0x0b, 0xf4, 0x0b, 0x0c, 0x0c, 0x23, 0x0c, 0x39, 0x0c, 0x50, 0x0c,
0x67, 0x0c, 0x7d, 0x0c, 0x93, 0x0c, 0xa9, 0x0c, 0xbf, 0x0c, 0xd4, 0x0c, 0xe9, 0x0c, 0xfe, 0x0c, 0x13, 0x0d, 0x28,
0x0d, 0x3c, 0x0d, 0x50, 0x0d, 0x64, 0x0d, 0x78, 0x0d, 0x8b, 0x0d, 0x9f, 0x0d, 0xb2, 0x0d, 0xc4, 0x0d, 0xd7, 0x0d,
0xe9, 0x0d, 0xfb, 0x0d, 0x0d, 0x0e, 0x1e, 0x0e, 0x2f, 0x0e, 0x40, 0x0e, 0x51, 0x0e, 0x61, 0x0e, 0x71, 0x0e, 0x81,
0x0e, 0x90, 0x0e, 0x9f, 0x0e, 0xae, 0x0e, 0xbd, 0x0e, 0xcb, 0x0e, 0xd9, 0x0e, 0xe7, 0x0e, 0xf4, 0x0e, 0x01, 0x0f,
0x0e, 0x0f, 0x1b, 0x0f, 0x27, 0x0f, 0x33, 0x0f, 0x3e, 0x0f, 0x49, 0x0f, 0x54, 0x0f, 0x5f, 0x0f, 0x69, 0x0f, 0x73,
0x0f, 0x7d, 0x0f, 0x86, 0x0f, 0x8f, 0x0f, 0x98, 0x0f, 0xa0, 0x0f, 0xa8, 0x0f, 0xaf, 0x0f, 0xb7, 0x0f, 0xbe, 0x0f,
0xc4, 0x0f, 0xcb, 0x0f, 0xd0, 0x0f, 0xd6, 0x0f, 0xdb, 0x0f, 0xe0, 0x0f, 0xe5, 0x0f, 0xe9, 0x0f, 0xed, 0x0f, 0xf0,
0x0f, 0xf3, 0x0f, 0xf6, 0x0f, 0xf9, 0x0f, 0xfb, 0x0f, 0xfc, 0x0f, 0xfe, 0x0f, 0xff, 0x0f, 0x00, 0x10, 0x00, 0x10,
0x00, 0x10, 0x00, 0x10, 0xff, 0x0f, 0xfe, 0x0f, 0xfc, 0x0f, 0xfb, 0x0f, 0xf9, 0x0f, 0xf6, 0x0f, 0xf3, 0x0f, 0xf0,
0x0f, 0xed, 0x0f, 0xe9, 0x0f, 0xe5, 0x0f, 0xe0, 0x0f, 0xdb, 0x0f, 0xd6, 0x0f, 0xd0, 0x0f, 0xcb, 0x0f, 0xc4, 0x0f,
0xbe, 0x0f, 0xb7, 0x0f, 0xaf, 0x0f, 0xa8, 0x0f, 0xa0, 0x0f, 0x98, 0x0f, 0x8f, 0x0f, 0x86, 0x0f, 0x7d, 0x0f, 0x73,
0x0f, 0x69, 0x0f, 0x5f, 0x0f, 0x54, 0x0f, 0x49, 0x0f, 0x3e, 0x0f, 0x33, 0x0f, 0x27, 0x0f, 0x1b, 0x0f, 0x0e, 0x0f,
0x01, 0x0f, 0xf4, 0x0e, 0xe7, 0x0e, 0xd9, 0x0e, 0xcb, 0x0e, 0xbd, 0x0e, 0xae, 0x0e, 0x9f, 0x0e, 0x90, 0x0e, 0x81,
0x0e, 0x71, 0x0e, 0x61, 0x0e, 0x51, 0x0e, 0x40, 0x0e, 0x2f, 0x0e, 0x1e, 0x0e, 0x0d, 0x0e, 0xfb, 0x0d, 0xe9, 0x0d,
0xd7, 0x0d, 0xc4, 0x0d, 0xb2, 0x0d, 0x9f, 0x0d, 0x8b, 0x0d, 0x78, 0x0d, 0x64, 0x0d, 0x50, 0x0d, 0x3c, 0x0d, 0x28,
0x0d, 0x13, 0x0d, 0xfe, 0x0c, 0xe9, 0x0c, 0xd4, 0x0c, 0xbf, 0x0c, 0xa9, 0x0c, 0x93, 0x0c, 0x7d, 0x0c, 0x67, 0x0c,
0x50, 0x0c, 0x39, 0x0c, 0x23, 0x0c, 0x0c, 0x0c, 0xf4, 0x0b, 0xdd, 0x0b, 0xc5, 0x0b, 0xae, 0x0b, 0x96, 0x0b, 0x7e,
0x0b, 0x66, 0x0b, 0x4d, 0x0b, 0x35, 0x0b, 0x1c, 0x0b, 0x03, 0x0b, 0xea, 0x0a, 0xd1, 0x0a, 0xb8, 0x0a, 0x9f, 0x0a,
0x86, 0x0a, 0x6c, 0x0a, 0x53, 0x0a, 0x39, 0x0a, 0x1f, 0x0a, 0x05, 0x0a, 0xeb, 0x09, 0xd1, 0x09, 0xb7, 0x09, 0x9d,
0x09, 0x82, 0x09, 0x68, 0x09, 0x4e, 0x09, 0x33, 0x09, 0x19, 0x09, 0xfe, 0x08, 0xe3, 0x08, 0xc9, 0x08, 0xae, 0x08,
0x93, 0x08, 0x79, 0x08, 0x5e, 0x08, 0x43, 0x08, 0x28, 0x08, 0x0d, 0x08, 0xf3, 0x07, 0xd8, 0x07, 0xbd, 0x07, 0xa2,
0x07, 0x87, 0x07, 0x6d, 0x07, 0x52, 0x07, 0x37, 0x07, 0x1d, 0x07, 0x02, 0x07, 0xe7, 0x06, 0xcd, 0x06, 0xb2, 0x06,
0x98, 0x06, 0x7e, 0x06, 0x63, 0x06, 0x49, 0x06, 0x2f, 0x06, 0x15, 0x06, 0xfb, 0x05, 0xe1, 0x05, 0xc7, 0x05, 0xad,
0x05, 0x94, 0x05, 0x7a, 0x05, 0x61, 0x05, 0x48, 0x05, 0x2f, 0x05, 0x16, 0x05, 0xfd, 0x04, 0xe4, 0x04, 0xcb, 0x04,
0xb3, 0x04, 0x9a, 0x04, 0x82, 0x04, 0x6a, 0x04, 0x52, 0x04, 0x3b, 0x04, 0x23, 0x04, 0x0c, 0x04, 0xf4, 0x03, 0xdd,
0x03, 0xc7, 0x03, 0xb0, 0x03, 0x99, 0x03, 0x83, 0x03, 0x6d, 0x03, 0x57, 0x03, 0x41, 0x03, 0x2c, 0x03, 0x17, 0x03,
0x02, 0x03, 0xed, 0x02, 0xd8, 0x02, 0xc4, 0x02, 0xb0, 0x02, 0x9c, 0x02, 0x88, 0x02, 0x75, 0x02, 0x61, 0x02, 0x4e,
0x02, 0x3c, 0x02, 0x29, 0x02, 0x17, 0x02, 0x05, 0x02, 0xf3, 0x01, 0xe2, 0x01, 0xd1, 0x01, 0xc0, 0x01, 0xaf, 0x01,
0x9f, 0x01, 0x8f, 0x01, 0x7f, 0x01, 0x70, 0x01, 0x61, 0x01, 0x52, 0x01, 0x43, 0x01, 0x35, 0x01, 0x27, 0x01, 0x19,
0x01, 0x0c, 0x01, 0xff, 0x00, 0xf2, 0x00, 0xe5, 0x00, 0xd9, 0x00, 0xcd, 0x00, 0xc2, 0x00, 0xb7, 0x00, 0xac, 0x00,
0xa1, 0x00, 0x97, 0x00, 0x8d, 0x00, 0x83, 0x00, 0x7a, 0x00, 0x71, 0x00, 0x68, 0x00, 0x60, 0x00, 0x58, 0x00, 0x51,
0x00, 0x49, 0x00, 0x42, 0x00, 0x3c, 0x00, 0x35, 0x00, 0x30, 0x00, 0x2a, 0x00, 0x25, 0x00, 0x20, 0x00, 0x1b, 0x00,
0x17, 0x00, 0x13, 0x00, 0x10, 0x00, 0x0d, 0x00, 0x0a, 0x00, 0x07, 0x00, 0x05, 0x00, 0x04, 0x00, 0x02, 0x00, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0xee, 0xff, 0xff, 0x38, 0xee, 0xff, 0xff, 0x0f, 0x00, 0x00, 0x00, 0x4d, 0x4c,
0x49, 0x52, 0x20, 0x43, 0x6f, 0x6e, 0x76, 0x65, 0x72, 0x74, 0x65, 0x64, 0x2e, 0x00, 0x01, 0x00, 0x00, 0x00, 0x14,
0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x18, 0x00, 0x14, 0x00, 0x10, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x04, 0x00,
0x0e, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0xf4, 0x05, 0x00, 0x00, 0xf8, 0x05, 0x00,
0x00, 0xfc, 0x05, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x6d, 0x61, 0x69, 0x6e, 0x00, 0x00, 0x00, 0x00, 0x16, 0x00,
0x00, 0x00, 0xa0, 0x05, 0x00, 0x00, 0x68, 0x05, 0x00, 0x00, 0x24, 0x05, 0x00, 0x00, 0xc8, 0x04, 0x00, 0x00, 0x70,
0x04, 0x00, 0x00, 0x4c, 0x04, 0x00, 0x00, 0x10, 0x04, 0x00, 0x00, 0xc8, 0x03, 0x00, 0x00, 0xa4, 0x03, 0x00, 0x00,
0x4c, 0x03, 0x00, 0x00, 0x14, 0x03, 0x00, 0x00, 0x10, 0x02, 0x00, 0x00, 0xc8, 0x01, 0x00, 0x00, 0x6c, 0x01, 0x00,
0x00, 0x48, 0x01, 0x00, 0x00, 0x14, 0x01, 0x00, 0x00, 0xe0, 0x00, 0x00, 0x00, 0xac, 0x00, 0x00, 0x00, 0x78, 0x00,
0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xf6, 0xfa, 0xff, 0xff, 0x0c,
0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00, 0x16, 0xfb, 0xff, 0xff, 0x0c, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00,
0x00, 0x11, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x28, 0x00,
0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x3a, 0xfb, 0xff, 0xff, 0x0c, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x10,
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x27, 0x00, 0x00, 0x00,
0x0e, 0x00, 0x00, 0x00, 0xa6, 0xfc, 0xff, 0xff, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0b, 0x10, 0x00, 0x00,
0x00, 0x14, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x68, 0xef, 0xff, 0xff, 0x01, 0x00, 0x00, 0x00, 0x27, 0x00,
0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0xd6, 0xfc, 0xff, 0xff, 0x14,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1d, 0x10, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00,
0x98, 0xef, 0xff, 0xff, 0x01, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x25, 0x00, 0x00,
0x00, 0x12, 0x00, 0x00, 0x00, 0x06, 0xfd, 0xff, 0xff, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0b, 0x10, 0x00,
0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0xc8, 0xef, 0xff, 0xff, 0x01, 0x00, 0x00, 0x00, 0x25,
0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x36, 0xfd, 0xff, 0xff,
0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x15, 0x10, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00,
0x00, 0xf8, 0xef, 0xff, 0xff, 0x01, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x23, 0x00,
0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x1e, 0xfc, 0xff, 0xff, 0x0c, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x05,
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00,
0x84, 0xfc, 0xff, 0xff, 0x10, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00,
0x00, 0x30, 0x00, 0x00, 0x00, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x63, 0x74, 0x69,
0x6f, 0x6e, 0x5f, 0x62, 0x69, 0x74, 0x73, 0x00, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, 0x73, 0x63, 0x61, 0x6c,
0x65, 0x00, 0x02, 0x24, 0x0f, 0x02, 0x01, 0x02, 0x03, 0x40, 0x04, 0x04, 0x04, 0x24, 0x01, 0x01, 0x00, 0x00, 0x00,
0x22, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0xdc, 0xfc, 0xff, 0xff, 0x10, 0x00, 0x00,
0x00, 0x24, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x73, 0x6e,
0x72, 0x5f, 0x73, 0x68, 0x69, 0x66, 0x74, 0x00, 0x01, 0x0b, 0x01, 0x01, 0x01, 0x06, 0x04, 0x02, 0x24, 0x01, 0x01,
0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00,
0x08, 0x00, 0x00, 0x00, 0x20, 0xfd, 0xff, 0xff, 0x10, 0x00, 0x00, 0x00, 0xe4, 0x00, 0x00, 0x00, 0xec, 0x00, 0x00,
0x00, 0x0a, 0x00, 0x00, 0x00, 0xd2, 0x00, 0x00, 0x00, 0x61, 0x6c, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x74, 0x65, 0x5f,
0x6f, 0x6e, 0x65, 0x5f, 0x6d, 0x69, 0x6e, 0x75, 0x73, 0x5f, 0x73, 0x6d, 0x6f, 0x6f, 0x74, 0x68, 0x69, 0x6e, 0x67,
0x00, 0x61, 0x6c, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x74, 0x65, 0x5f, 0x73, 0x6d, 0x6f, 0x6f, 0x74, 0x68, 0x69, 0x6e,
0x67, 0x00, 0x63, 0x6c, 0x61, 0x6d, 0x70, 0x69, 0x6e, 0x67, 0x00, 0x6d, 0x69, 0x6e, 0x5f, 0x73, 0x69, 0x67, 0x6e,
0x61, 0x6c, 0x5f, 0x72, 0x65, 0x6d, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x00, 0x6e, 0x75, 0x6d, 0x5f, 0x63, 0x68,
0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x00, 0x6f, 0x6e, 0x65, 0x5f, 0x6d, 0x69, 0x6e, 0x75, 0x73, 0x5f, 0x73, 0x6d,
0x6f, 0x6f, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x00, 0x73, 0x6d, 0x6f, 0x6f, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x00, 0x73,
0x6d, 0x6f, 0x6f, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x5f, 0x62, 0x69, 0x74, 0x73, 0x00, 0x73, 0x70, 0x65, 0x63, 0x74,
0x72, 0x61, 0x6c, 0x5f, 0x73, 0x75, 0x62, 0x74, 0x72, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x62, 0x69, 0x74,
0x73, 0x00, 0x09, 0xa5, 0x88, 0x75, 0x6d, 0x59, 0x4d, 0x3a, 0x31, 0x23, 0x09, 0x00, 0x01, 0x00, 0x09, 0x00, 0x29,
0x3c, 0xd7, 0x03, 0x00, 0x00, 0x33, 0x03, 0x28, 0x00, 0x67, 0x3e, 0x99, 0x01, 0x0a, 0x00, 0x0e, 0x00, 0x05, 0x05,
0x69, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x1b, 0x25, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00,
0x00, 0x20, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x20, 0xfe, 0xff, 0xff, 0x10, 0x00,
0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00,
0x00, 0x01, 0x00, 0x00, 0x24, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00,
0x1d, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x54, 0xfe, 0xff, 0xff, 0x10, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00,
0x00, 0x2c, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, 0x6e, 0x75, 0x6d, 0x5f, 0x63, 0x68,
0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x00, 0x01, 0x0e, 0x01, 0x01, 0x01, 0x28, 0x04, 0x02, 0x24, 0x01, 0x00, 0x01,
0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00,
0x0c, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x62, 0xfe, 0xff,
0xff, 0x0c, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x1c, 0x00,
0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x1b, 0x00, 0x00, 0x00, 0xca, 0xff, 0xff, 0xff, 0x14, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x0a, 0x10, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x8c, 0xf2, 0xff, 0xff,
0x01, 0x00, 0x00, 0x00, 0x1b, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x00,
0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x18, 0x00, 0x14, 0x00, 0x10, 0x00, 0x0c, 0x00, 0x0b, 0x00,
0x04, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x10, 0x00, 0x00, 0x00, 0x14,
0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0xd0, 0xf2, 0xff, 0xff, 0x01, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x00, 0x00,
0x04, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00,
0x00, 0xfe, 0xfe, 0xff, 0xff, 0x0c, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00,
0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x64, 0xff, 0xff, 0xff, 0x10,
0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00,
0x65, 0x6e, 0x64, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x00, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x69, 0x6e, 0x64,
0x65, 0x78, 0x00, 0x02, 0x17, 0x0e, 0x00, 0x03, 0x00, 0x01, 0x00, 0x02, 0x00, 0xf1, 0x00, 0x05, 0x00, 0x05, 0x05,
0x06, 0x25, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x17,
0x00, 0x00, 0x00, 0xb8, 0xff, 0xff, 0xff, 0x10, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00,
0x03, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x54, 0x00, 0x66, 0x66, 0x74, 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74,
0x68, 0x00, 0x02, 0x0e, 0x0d, 0x02, 0x00, 0x01, 0x00, 0x02, 0x00, 0x07, 0x00, 0x00, 0x02, 0x05, 0x05, 0x06, 0x25,
0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x10,
0x00, 0x14, 0x00, 0x10, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00,
0x10, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00,
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x24, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x16, 0x00,
0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x10, 0x00, 0x0c, 0x00, 0x08,
0x00, 0x04, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00,
0x00, 0x10, 0x00, 0x10, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x10, 0x00,
0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x73,
0x68, 0x69, 0x66, 0x74, 0x00, 0x01, 0x07, 0x01, 0x01, 0x01, 0x0c, 0x04, 0x02, 0x24, 0x01, 0x01, 0x00, 0x00, 0x00,
0x13, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
0x00, 0x2a, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0xc4, 0x0a,
0x00, 0x00, 0x74, 0x0a, 0x00, 0x00, 0x3c, 0x0a, 0x00, 0x00, 0x04, 0x0a, 0x00, 0x00, 0xd0, 0x09, 0x00, 0x00, 0x88,
0x09, 0x00, 0x00, 0x40, 0x09, 0x00, 0x00, 0xfc, 0x08, 0x00, 0x00, 0xb8, 0x08, 0x00, 0x00, 0x6c, 0x08, 0x00, 0x00,
0x20, 0x08, 0x00, 0x00, 0xd4, 0x07, 0x00, 0x00, 0x88, 0x07, 0x00, 0x00, 0x3c, 0x07, 0x00, 0x00, 0xf8, 0x06, 0x00,
0x00, 0xb8, 0x06, 0x00, 0x00, 0x84, 0x06, 0x00, 0x00, 0x50, 0x06, 0x00, 0x00, 0x1c, 0x06, 0x00, 0x00, 0xd8, 0x05,
0x00, 0x00, 0xa0, 0x05, 0x00, 0x00, 0x58, 0x05, 0x00, 0x00, 0x14, 0x05, 0x00, 0x00, 0xd8, 0x04, 0x00, 0x00, 0x98,
0x04, 0x00, 0x00, 0x60, 0x04, 0x00, 0x00, 0x20, 0x04, 0x00, 0x00, 0xe8, 0x03, 0x00, 0x00, 0xb0, 0x03, 0x00, 0x00,
0x6c, 0x03, 0x00, 0x00, 0x1c, 0x03, 0x00, 0x00, 0xc4, 0x02, 0x00, 0x00, 0x68, 0x02, 0x00, 0x00, 0x2c, 0x02, 0x00,
0x00, 0xe4, 0x01, 0x00, 0x00, 0xac, 0x01, 0x00, 0x00, 0x78, 0x01, 0x00, 0x00, 0x44, 0x01, 0x00, 0x00, 0x08, 0x01,
0x00, 0x00, 0xd0, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xfe,
0xf5, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x09, 0x20, 0x00, 0x00, 0x00, 0x44, 0xf5, 0xff, 0xff, 0x11, 0x00, 0x00, 0x00, 0x50, 0x61, 0x72,
0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x65, 0x64, 0x43, 0x61, 0x6c, 0x6c, 0x3a, 0x30, 0x00, 0x00, 0x00, 0x01, 0x00,
0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x3e, 0xf6, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14,
0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x1c, 0x00, 0x00, 0x00, 0x84, 0xf5, 0xff, 0xff,
0x0d, 0x00, 0x00, 0x00, 0x63, 0x6c, 0x69, 0x70, 0x5f, 0x62, 0x79, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x00, 0x00,
0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x7a, 0xf6, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00,
0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x24, 0x00, 0x00, 0x00, 0xc0,
0xf5, 0xff, 0xff, 0x15, 0x00, 0x00, 0x00, 0x63, 0x6c, 0x69, 0x70, 0x5f, 0x62, 0x79, 0x5f, 0x76, 0x61, 0x6c, 0x75,
0x65, 0x2f, 0x4d, 0x69, 0x6e, 0x69, 0x6d, 0x75, 0x6d, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00,
0x00, 0xbe, 0xf6, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x28, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x14, 0x00, 0x00, 0x00, 0x04, 0xf6, 0xff, 0xff, 0x05, 0x00, 0x00, 0x00, 0x61,
0x64, 0x64, 0x5f, 0x31, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0xf2, 0xf6, 0xff, 0xff,
0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x27, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x02, 0x18, 0x00, 0x00, 0x00, 0x38, 0xf6, 0xff, 0xff, 0x0b, 0x00, 0x00, 0x00, 0x54, 0x72, 0x75, 0x6e, 0x63, 0x61,
0x74, 0x65, 0x44, 0x69, 0x76, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x2a, 0xf7, 0xff, 0xff, 0x00,
0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02,
0x10, 0x00, 0x00, 0x00, 0x70, 0xf6, 0xff, 0xff, 0x03, 0x00, 0x00, 0x00, 0x61, 0x64, 0x64, 0x00, 0x01, 0x00, 0x00,
0x00, 0x28, 0x00, 0x00, 0x00, 0x5a, 0xf7, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00,
0x00, 0x00, 0x25, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x10, 0x00, 0x00, 0x00, 0xa0, 0xf6, 0xff, 0xff, 0x03,
0x00, 0x00, 0x00, 0x6d, 0x75, 0x6c, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x8a, 0xf7, 0xff, 0xff,
0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x02, 0x14, 0x00, 0x00, 0x00, 0xd0, 0xf6, 0xff, 0xff, 0x06, 0x00, 0x00, 0x00, 0x43, 0x61, 0x73, 0x74, 0x5f, 0x32,
0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0xbe, 0xf7, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14,
0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x24, 0x00, 0x00, 0x00,
0x04, 0xf7, 0xff, 0xff, 0x16, 0x00, 0x00, 0x00, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x5f, 0x66, 0x69, 0x6c, 0x74,
0x65, 0x72, 0x5f, 0x62, 0x61, 0x6e, 0x6b, 0x5f, 0x6c, 0x6f, 0x67, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00,
0x00, 0x00, 0x02, 0xf8, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x22,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x18, 0x00, 0x00, 0x00, 0x48, 0xf7, 0xff, 0xff, 0x0b, 0x00, 0x00, 0x00,
0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x5f, 0x70, 0x63, 0x61, 0x6e, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00,
0x00, 0x3a, 0xf8, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x21, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x38, 0x00, 0x00, 0x00, 0x80, 0xf7, 0xff, 0xff, 0x28, 0x00, 0x00, 0x00, 0x73,
0x69, 0x67, 0x6e, 0x61, 0x6c, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x5f, 0x62, 0x61, 0x6e, 0x6b, 0x5f, 0x73,
0x70, 0x65, 0x63, 0x74, 0x72, 0x61, 0x6c, 0x5f, 0x73, 0x75, 0x62, 0x74, 0x72, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e,
0x31, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x92, 0xf8, 0xff, 0xff, 0x00, 0x00,
0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x34,
0x00, 0x00, 0x00, 0xd8, 0xf7, 0xff, 0xff, 0x27, 0x00, 0x00, 0x00, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x5f, 0x66,
0x69, 0x6c, 0x74, 0x65, 0x72, 0x5f, 0x62, 0x61, 0x6e, 0x6b, 0x5f, 0x73, 0x70, 0x65, 0x63, 0x74, 0x72, 0x61, 0x6c,
0x5f, 0x73, 0x75, 0x62, 0x74, 0x72, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00,
0x00, 0x00, 0xe6, 0xf8, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x1f,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x2c, 0x00, 0x00, 0x00, 0x2c, 0xf8, 0xff, 0xff, 0x1e, 0x00, 0x00, 0x00,
0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x5f, 0x62, 0x61, 0x6e, 0x6b, 0x5f,
0x73, 0x71, 0x75, 0x61, 0x72, 0x65, 0x5f, 0x72, 0x6f, 0x6f, 0x74, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00,
0x00, 0x00, 0x32, 0xf9, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x1e,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x20, 0x00, 0x00, 0x00, 0x78, 0xf8, 0xff, 0xff, 0x12, 0x00, 0x00, 0x00,
0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x5f, 0x62, 0x61, 0x6e, 0x6b, 0x00,
0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x72, 0xf9, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00,
0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x14, 0x00, 0x00, 0x00, 0xb8,
0xf8, 0xff, 0xff, 0x06, 0x00, 0x00, 0x00, 0x43, 0x61, 0x73, 0x74, 0x5f, 0x31, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
0x01, 0x01, 0x00, 0x00, 0xa6, 0xf9, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00,
0x00, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x14, 0x00, 0x00, 0x00, 0xec, 0xf8, 0xff, 0xff, 0x06, 0x00,
0x00, 0x00, 0x63, 0x6f, 0x6e, 0x63, 0x61, 0x74, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0xda,
0xf9, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x1b, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x02, 0x1c, 0x00, 0x00, 0x00, 0x20, 0xf9, 0xff, 0xff, 0x0d, 0x00, 0x00, 0x00, 0x73, 0x74, 0x72,
0x69, 0x64, 0x65, 0x64, 0x5f, 0x73, 0x6c, 0x69, 0x63, 0x65, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xec, 0x00,
0x00, 0x00, 0x16, 0xfa, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x1a,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x14, 0x00, 0x00, 0x00, 0x5c, 0xf9, 0xff, 0xff, 0x04, 0x00, 0x00, 0x00,
0x43, 0x61, 0x73, 0x74, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x4a, 0xfa, 0xff,
0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x0f, 0x1c, 0x00, 0x00, 0x00, 0x90, 0xf9, 0xff, 0xff, 0x0d, 0x00, 0x00, 0x00, 0x73, 0x69, 0x67, 0x6e, 0x61,
0x6c, 0x5f, 0x65, 0x6e, 0x65, 0x72, 0x67, 0x79, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00,
0x86, 0xfa, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x07, 0x18, 0x00, 0x00, 0x00, 0xcc, 0xf9, 0xff, 0xff, 0x0b, 0x00, 0x00, 0x00, 0x73, 0x69,
0x67, 0x6e, 0x61, 0x6c, 0x5f, 0x72, 0x66, 0x66, 0x74, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x02, 0x00, 0x00, 0xbe,
0xfa, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x02, 0x24, 0x00, 0x00, 0x00, 0x04, 0xfa, 0xff, 0xff, 0x16, 0x00, 0x00, 0x00, 0x73, 0x69, 0x67,
0x6e, 0x61, 0x6c, 0x5f, 0x66, 0x66, 0x74, 0x5f, 0x61, 0x75, 0x74, 0x6f, 0x5f, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x31,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfe, 0xfa, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14,
0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x24, 0x00, 0x00, 0x00, 0x44, 0xfa, 0xff, 0xff,
0x15, 0x00, 0x00, 0x00, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x5f, 0x66, 0x66, 0x74, 0x5f, 0x61, 0x75, 0x74, 0x6f,
0x5f, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x42, 0xfb,
0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x07, 0x14, 0x00, 0x00, 0x00, 0x88, 0xfa, 0xff, 0xff, 0x07, 0x00, 0x00, 0x00, 0x52, 0x65, 0x73, 0x68,
0x61, 0x70, 0x65, 0x00, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x76, 0xfb, 0xff, 0xff, 0x00, 0x00, 0x00,
0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x1c, 0x00,
0x00, 0x00, 0xbc, 0xfa, 0xff, 0xff, 0x0d, 0x00, 0x00, 0x00, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x5f, 0x77, 0x69,
0x6e, 0x64, 0x6f, 0x77, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00,
0xb6, 0xfb, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x02, 0x14, 0x00, 0x00, 0x00, 0xfc, 0xfa, 0xff, 0xff, 0x07, 0x00, 0x00, 0x00, 0x43, 0x6f,
0x6e, 0x73, 0x74, 0x5f, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe6, 0xfb, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14,
0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x14, 0x00, 0x00, 0x00,
0x2c, 0xfb, 0xff, 0xff, 0x06, 0x00, 0x00, 0x00, 0x43, 0x6f, 0x6e, 0x73, 0x74, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x16, 0xfc, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x11, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x14, 0x00, 0x00, 0x00, 0x5c, 0xfb, 0xff, 0xff, 0x07, 0x00, 0x00, 0x00, 0x43,
0x6f, 0x6e, 0x73, 0x74, 0x5f, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46, 0xfc, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01,
0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x1c, 0x00, 0x00,
0x00, 0x8c, 0xfb, 0xff, 0xff, 0x0d, 0x00, 0x00, 0x00, 0x52, 0x65, 0x73, 0x68, 0x61, 0x70, 0x65, 0x2f, 0x73, 0x68,
0x61, 0x70, 0x65, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x82, 0xfc, 0xff, 0xff, 0x00,
0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02,
0x24, 0x00, 0x00, 0x00, 0xc8, 0xfb, 0xff, 0xff, 0x17, 0x00, 0x00, 0x00, 0x63, 0x6c, 0x69, 0x70, 0x5f, 0x62, 0x79,
0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x2f, 0x4d, 0x69, 0x6e, 0x69, 0x6d, 0x75, 0x6d, 0x2f, 0x79, 0x00, 0x00, 0x00,
0x00, 0x00, 0xc2, 0xfc, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x0e,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x28, 0x00, 0x00, 0x00, 0x08, 0xfc, 0xff, 0xff, 0x18, 0x00, 0x00, 0x00,
0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x5f, 0x62, 0x61, 0x6e, 0x6b, 0x2f,
0x43, 0x6f, 0x6e, 0x73, 0x74, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3c, 0x01, 0x00, 0x00, 0x0a, 0xfd,
0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x07, 0x28, 0x00, 0x00, 0x00, 0x50, 0xfc, 0xff, 0xff, 0x1a, 0x00, 0x00, 0x00, 0x73, 0x69, 0x67, 0x6e,
0x61, 0x6c, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x5f, 0x62, 0x61, 0x6e, 0x6b, 0x2f, 0x43, 0x6f, 0x6e, 0x73,
0x74, 0x5f, 0x31, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3c, 0x01, 0x00, 0x00, 0x52, 0xfd, 0xff, 0xff, 0x00, 0x00,
0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x28,
0x00, 0x00, 0x00, 0x98, 0xfc, 0xff, 0xff, 0x1a, 0x00, 0x00, 0x00, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x5f, 0x66,
0x69, 0x6c, 0x74, 0x65, 0x72, 0x5f, 0x62, 0x61, 0x6e, 0x6b, 0x2f, 0x43, 0x6f, 0x6e, 0x73, 0x74, 0x5f, 0x32, 0x00,
0x00, 0x01, 0x00, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00, 0x9a, 0xfd, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00,
0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x28, 0x00, 0x00, 0x00, 0xe0,
0xfc, 0xff, 0xff, 0x1a, 0x00, 0x00, 0x00, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65,
0x72, 0x5f, 0x62, 0x61, 0x6e, 0x6b, 0x2f, 0x43, 0x6f, 0x6e, 0x73, 0x74, 0x5f, 0x33, 0x00, 0x00, 0x01, 0x00, 0x00,
0x00, 0x29, 0x00, 0x00, 0x00, 0xe2, 0xfd, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00,
0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x28, 0x00, 0x00, 0x00, 0x28, 0xfd, 0xff, 0xff, 0x1a,
0x00, 0x00, 0x00, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x5f, 0x62, 0x61,
0x6e, 0x6b, 0x2f, 0x43, 0x6f, 0x6e, 0x73, 0x74, 0x5f, 0x34, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x29, 0x00, 0x00,
0x00, 0x2a, 0xfe, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x09, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x20, 0x00, 0x00, 0x00, 0x70, 0xfd, 0xff, 0xff, 0x11, 0x00, 0x00, 0x00, 0x73,
0x69, 0x67, 0x6e, 0x61, 0x6c, 0x5f, 0x70, 0x63, 0x61, 0x6e, 0x2f, 0x43, 0x6f, 0x6e, 0x73, 0x74, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00, 0x7d, 0x00, 0x00, 0x00, 0x6a, 0xfe, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00,
0x00, 0x14, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x20, 0x00, 0x00, 0x00, 0xb0, 0xfd,
0xff, 0xff, 0x13, 0x00, 0x00, 0x00, 0x73, 0x74, 0x72, 0x69, 0x64, 0x65, 0x64, 0x5f, 0x73, 0x6c, 0x69, 0x63, 0x65,
0x2f, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xaa, 0xfe, 0xff, 0xff,
0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x02, 0x24, 0x00, 0x00, 0x00, 0xf0, 0xfd, 0xff, 0xff, 0x15, 0x00, 0x00, 0x00, 0x73, 0x74, 0x72, 0x69, 0x64, 0x65,
0x64, 0x5f, 0x73, 0x6c, 0x69, 0x63, 0x65, 0x2f, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x5f, 0x31, 0x00, 0x00, 0x00, 0x01,
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xee, 0xfe, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00,
0x14, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x24, 0x00, 0x00, 0x00, 0x34, 0xfe, 0xff,
0xff, 0x15, 0x00, 0x00, 0x00, 0x73, 0x74, 0x72, 0x69, 0x64, 0x65, 0x64, 0x5f, 0x73, 0x6c, 0x69, 0x63, 0x65, 0x2f,
0x73, 0x74, 0x61, 0x63, 0x6b, 0x5f, 0x32, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x32,
0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x02, 0x14, 0x00, 0x00, 0x00, 0x78, 0xfe, 0xff, 0xff, 0x06, 0x00, 0x00, 0x00, 0x43, 0x61, 0x73,
0x74, 0x5f, 0x33, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x62, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00,
0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x14, 0x00, 0x00, 0x00, 0xa8,
0xfe, 0xff, 0xff, 0x05, 0x00, 0x00, 0x00, 0x7a, 0x65, 0x72, 0x6f, 0x73, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
0x05, 0x00, 0x00, 0x00, 0x96, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00,
0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x14, 0x00, 0x00, 0x00, 0xdc, 0xfe, 0xff, 0xff, 0x07, 0x00,
0x00, 0x00, 0x7a, 0x65, 0x72, 0x6f, 0x73, 0x5f, 0x31, 0x00, 0x01, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0xca,
0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x07, 0x14, 0x00, 0x00, 0x00, 0x10, 0xff, 0xff, 0xff, 0x05, 0x00, 0x00, 0x00, 0x43, 0x6f, 0x6e,
0x73, 0x74, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x16, 0x00, 0x1c, 0x00,
0x18, 0x00, 0x17, 0x00, 0x10, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x16,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x07, 0x2c, 0x00, 0x00, 0x00, 0x5c, 0xff, 0xff, 0xff, 0x1d, 0x00, 0x00, 0x00, 0x73, 0x65, 0x72,
0x76, 0x69, 0x6e, 0x67, 0x5f, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x61, 0x75, 0x64, 0x69, 0x6f, 0x5f,
0x66, 0x72, 0x61, 0x6d, 0x65, 0x3a, 0x30, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xe0,
0x01, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x1c, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0xc8, 0x01, 0x00, 0x00,
0xa4, 0x01, 0x00, 0x00, 0x7c, 0x01, 0x00, 0x00, 0x68, 0x01, 0x00, 0x00, 0x4c, 0x01, 0x00, 0x00, 0x3c, 0x01, 0x00,
0x00, 0x10, 0x01, 0x00, 0x00, 0xdc, 0x00, 0x00, 0x00, 0xa0, 0x00, 0x00, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x50, 0x00,
0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x04,
0x00, 0x00, 0x00, 0x50, 0xfe, 0xff, 0xff, 0x37, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x37, 0x5c, 0xfe, 0xff, 0xff,
0x39, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x39, 0x68, 0xfe, 0xff, 0xff, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x2a, 0x04, 0x00, 0x04, 0x00, 0x04, 0x00, 0x00, 0x00, 0x7c, 0xfe, 0xff, 0xff, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x12, 0x70, 0xfe, 0xff, 0xff, 0x20, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x13,
0x00, 0x00, 0x00, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x42, 0x61, 0x6e, 0x6b,
0x4c, 0x6f, 0x67, 0x00, 0x98, 0xfe, 0xff, 0xff, 0x20, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x20, 0x0a, 0x00, 0x00, 0x00, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x50, 0x43, 0x41, 0x4e, 0x00, 0x00, 0xb8, 0xfe,
0xff, 0xff, 0x20, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x23, 0x00, 0x00, 0x00, 0x53,
0x69, 0x67, 0x6e, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x42, 0x61, 0x6e, 0x6b, 0x53, 0x70, 0x65, 0x63,
0x74, 0x72, 0x61, 0x6c, 0x53, 0x75, 0x62, 0x74, 0x72, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0xf0, 0xfe, 0xff,
0xff, 0x20, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x1a, 0x00, 0x00, 0x00, 0x53, 0x69,
0x67, 0x6e, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x42, 0x61, 0x6e, 0x6b, 0x53, 0x71, 0x75, 0x61, 0x72,
0x65, 0x52, 0x6f, 0x6f, 0x74, 0x00, 0x00, 0x20, 0xff, 0xff, 0xff, 0x20, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x20, 0x10, 0x00, 0x00, 0x00, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x74, 0x65,
0x72, 0x42, 0x61, 0x6e, 0x6b, 0x00, 0x00, 0x00, 0x00, 0x60, 0xff, 0xff, 0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x02, 0x6c, 0xff, 0xff, 0xff, 0x2d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2d, 0x0c, 0x00, 0x10, 0x00, 0x0f,
0x00, 0x00, 0x00, 0x08, 0x00, 0x04, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x35, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x35, 0x7c, 0xff, 0xff, 0xff, 0x20, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x20, 0x0c, 0x00, 0x00, 0x00, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x45, 0x6e, 0x65, 0x72, 0x67, 0x79, 0x00, 0x00,
0x00, 0x00, 0xa0, 0xff, 0xff, 0xff, 0x20, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x0a,
0x00, 0x00, 0x00, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x52, 0x66, 0x66, 0x74, 0x00, 0x00, 0xc0, 0xff, 0xff, 0xff,
0x20, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x12, 0x00, 0x00, 0x00, 0x53, 0x69, 0x67,
0x6e, 0x61, 0x6c, 0x46, 0x66, 0x74, 0x41, 0x75, 0x74, 0x6f, 0x53, 0x63, 0x61, 0x6c, 0x65, 0x00, 0x00, 0x0c, 0x00,
0x0c, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x16, 0x0c, 0x00, 0x10, 0x00, 0x0f, 0x00, 0x08, 0x00, 0x00, 0x00, 0x04, 0x00, 0x0c, 0x00, 0x00, 0x00,
0x20, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x0c, 0x00, 0x00, 0x00, 0x53, 0x69, 0x67,
0x6e, 0x61, 0x6c, 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x00, 0x00, 0x00, 0x00};
} // namespace micro_wake_word
} // namespace esphome
#endif // USE_ESP_IDF

View file

@ -1,12 +1,5 @@
#include "micro_wake_word.h"
/**
* This is a workaround until we can figure out a way to get
* the tflite-micro idf component code available in CI
*
* */
//
#ifndef CLANG_TIDY
#include "streaming_model.h"
#ifdef USE_ESP_IDF
@ -14,13 +7,13 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "audio_preprocessor_int8_model_data.h"
#include <frontend.h>
#include <frontend_util.h>
#include <tensorflow/lite/core/c/common.h>
#include <tensorflow/lite/micro/micro_interpreter.h>
#include <tensorflow/lite/micro/micro_mutable_op_resolver.h>
#include <cinttypes>
#include <cmath>
namespace esphome {
@ -29,9 +22,9 @@ namespace micro_wake_word {
static const char *const TAG = "micro_wake_word";
static const size_t SAMPLE_RATE_HZ = 16000; // 16 kHz
static const size_t BUFFER_LENGTH = 500; // 0.5 seconds
static const size_t BUFFER_LENGTH = 64; // 0.064 seconds
static const size_t BUFFER_SIZE = SAMPLE_RATE_HZ / 1000 * BUFFER_LENGTH;
static const size_t INPUT_BUFFER_SIZE = 32 * SAMPLE_RATE_HZ / 1000; // 32ms * 16kHz / 1000ms
static const size_t INPUT_BUFFER_SIZE = 16 * SAMPLE_RATE_HZ / 1000; // 16ms * 16kHz / 1000ms
float MicroWakeWord::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; }
@ -56,57 +49,55 @@ static const LogString *micro_wake_word_state_to_string(State state) {
void MicroWakeWord::dump_config() {
ESP_LOGCONFIG(TAG, "microWakeWord:");
ESP_LOGCONFIG(TAG, " Wake Word: %s", this->get_wake_word().c_str());
ESP_LOGCONFIG(TAG, " Probability cutoff: %.3f", this->probability_cutoff_);
ESP_LOGCONFIG(TAG, " Sliding window size: %d", this->sliding_window_average_size_);
ESP_LOGCONFIG(TAG, " models:");
for (auto &model : this->wake_word_models_) {
model.log_model_config();
}
#ifdef USE_MICRO_WAKE_WORD_VAD
this->vad_model_->log_model_config();
#endif
}
void MicroWakeWord::setup() {
ESP_LOGCONFIG(TAG, "Setting up microWakeWord...");
if (!this->initialize_models()) {
ESP_LOGE(TAG, "Failed to initialize models");
this->mark_failed();
return;
}
ExternalRAMAllocator<int16_t> allocator(ExternalRAMAllocator<int16_t>::ALLOW_FAILURE);
this->input_buffer_ = allocator.allocate(INPUT_BUFFER_SIZE * sizeof(int16_t));
if (this->input_buffer_ == nullptr) {
ESP_LOGW(TAG, "Could not allocate input buffer");
this->mark_failed();
return;
}
this->ring_buffer_ = RingBuffer::create(BUFFER_SIZE * sizeof(int16_t));
if (this->ring_buffer_ == nullptr) {
ESP_LOGW(TAG, "Could not allocate ring buffer");
if (!this->register_streaming_ops_(this->streaming_op_resolver_)) {
this->mark_failed();
return;
}
ESP_LOGCONFIG(TAG, "Micro Wake Word initialized");
this->frontend_config_.window.size_ms = FEATURE_DURATION_MS;
this->frontend_config_.window.step_size_ms = this->features_step_size_;
this->frontend_config_.filterbank.num_channels = PREPROCESSOR_FEATURE_SIZE;
this->frontend_config_.filterbank.lower_band_limit = 125.0;
this->frontend_config_.filterbank.upper_band_limit = 7500.0;
this->frontend_config_.noise_reduction.smoothing_bits = 10;
this->frontend_config_.noise_reduction.even_smoothing = 0.025;
this->frontend_config_.noise_reduction.odd_smoothing = 0.06;
this->frontend_config_.noise_reduction.min_signal_remaining = 0.05;
this->frontend_config_.pcan_gain_control.enable_pcan = 1;
this->frontend_config_.pcan_gain_control.strength = 0.95;
this->frontend_config_.pcan_gain_control.offset = 80.0;
this->frontend_config_.pcan_gain_control.gain_bits = 21;
this->frontend_config_.log_scale.enable_log = 1;
this->frontend_config_.log_scale.scale_shift = 6;
}
int MicroWakeWord::read_microphone_() {
size_t bytes_read = this->microphone_->read(this->input_buffer_, INPUT_BUFFER_SIZE * sizeof(int16_t));
if (bytes_read == 0) {
return 0;
void MicroWakeWord::add_wake_word_model(const uint8_t *model_start, float probability_cutoff,
size_t sliding_window_average_size, const std::string &wake_word,
size_t tensor_arena_size) {
this->wake_word_models_.emplace_back(model_start, probability_cutoff, sliding_window_average_size, wake_word,
tensor_arena_size);
}
size_t bytes_free = this->ring_buffer_->free();
if (bytes_free < bytes_read) {
ESP_LOGW(TAG,
"Not enough free bytes in ring buffer to store incoming audio data (free bytes=%d, incoming bytes=%d). "
"Resetting the ring buffer. Wake word detection accuracy will be reduced.",
bytes_free, bytes_read);
this->ring_buffer_->reset();
}
return this->ring_buffer_->write((void *) this->input_buffer_, bytes_read);
#ifdef USE_MICRO_WAKE_WORD_VAD
void MicroWakeWord::add_vad_model(const uint8_t *model_start, float probability_cutoff, size_t sliding_window_size,
size_t tensor_arena_size) {
this->vad_model_ = make_unique<VADModel>(model_start, probability_cutoff, sliding_window_size, tensor_arena_size);
}
#endif
void MicroWakeWord::loop() {
switch (this->state_) {
@ -124,9 +115,12 @@ void MicroWakeWord::loop() {
}
break;
case State::DETECTING_WAKE_WORD:
while (!this->has_enough_samples_()) {
this->read_microphone_();
if (this->detect_wake_word_()) {
ESP_LOGD(TAG, "Wake Word Detected");
}
this->update_model_probabilities_();
if (this->detect_wake_words_()) {
ESP_LOGD(TAG, "Wake Word '%s' Detected", (this->detected_wake_word_).c_str());
this->detected_ = true;
this->set_state_(State::STOP_MICROPHONE);
}
@ -136,13 +130,16 @@ void MicroWakeWord::loop() {
this->microphone_->stop();
this->set_state_(State::STOPPING_MICROPHONE);
this->high_freq_.stop();
this->unload_models_();
this->deallocate_buffers_();
break;
case State::STOPPING_MICROPHONE:
if (this->microphone_->is_stopped()) {
this->set_state_(State::IDLE);
if (this->detected_) {
this->wake_word_detected_trigger_->trigger(this->detected_wake_word_);
this->detected_ = false;
this->wake_word_detected_trigger_->trigger(this->wake_word_);
this->detected_wake_word_ = "";
}
}
break;
@ -150,14 +147,34 @@ void MicroWakeWord::loop() {
}
void MicroWakeWord::start() {
if (!this->is_ready()) {
ESP_LOGW(TAG, "Wake word detection can't start as the component hasn't been setup yet");
return;
}
if (this->is_failed()) {
ESP_LOGW(TAG, "Wake word component is marked as failed. Please check setup logs");
return;
}
if (!this->load_models_() || !this->allocate_buffers_()) {
ESP_LOGE(TAG, "Failed to load the wake word model(s) or allocate buffers");
this->status_set_error();
} else {
this->status_clear_error();
}
if (this->status_has_error()) {
ESP_LOGW(TAG, "Wake word component has an error. Please check logs");
return;
}
if (this->state_ != State::IDLE) {
ESP_LOGW(TAG, "Wake word is already running");
return;
}
this->reset_states_();
this->set_state_(State::START_MICROPHONE);
}
@ -179,289 +196,218 @@ void MicroWakeWord::set_state_(State state) {
this->state_ = state;
}
bool MicroWakeWord::initialize_models() {
ExternalRAMAllocator<uint8_t> arena_allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
ExternalRAMAllocator<int8_t> features_allocator(ExternalRAMAllocator<int8_t>::ALLOW_FAILURE);
size_t MicroWakeWord::read_microphone_() {
size_t bytes_read = this->microphone_->read(this->input_buffer_, INPUT_BUFFER_SIZE * sizeof(int16_t));
if (bytes_read == 0) {
return 0;
}
size_t bytes_free = this->ring_buffer_->free();
if (bytes_free < bytes_read) {
ESP_LOGW(TAG,
"Not enough free bytes in ring buffer to store incoming audio data (free bytes=%d, incoming bytes=%d). "
"Resetting the ring buffer. Wake word detection accuracy will be reduced.",
bytes_free, bytes_read);
this->ring_buffer_->reset();
}
return this->ring_buffer_->write((void *) this->input_buffer_, bytes_read);
}
bool MicroWakeWord::allocate_buffers_() {
ExternalRAMAllocator<int16_t> audio_samples_allocator(ExternalRAMAllocator<int16_t>::ALLOW_FAILURE);
this->streaming_tensor_arena_ = arena_allocator.allocate(STREAMING_MODEL_ARENA_SIZE);
if (this->streaming_tensor_arena_ == nullptr) {
ESP_LOGE(TAG, "Could not allocate the streaming model's tensor arena.");
if (this->input_buffer_ == nullptr) {
this->input_buffer_ = audio_samples_allocator.allocate(INPUT_BUFFER_SIZE * sizeof(int16_t));
if (this->input_buffer_ == nullptr) {
ESP_LOGE(TAG, "Could not allocate input buffer");
return false;
}
this->streaming_var_arena_ = arena_allocator.allocate(STREAMING_MODEL_VARIABLE_ARENA_SIZE);
if (this->streaming_var_arena_ == nullptr) {
ESP_LOGE(TAG, "Could not allocate the streaming model variable's tensor arena.");
return false;
}
this->preprocessor_tensor_arena_ = arena_allocator.allocate(PREPROCESSOR_ARENA_SIZE);
if (this->preprocessor_tensor_arena_ == nullptr) {
ESP_LOGE(TAG, "Could not allocate the audio preprocessor model's tensor arena.");
return false;
}
this->new_features_data_ = features_allocator.allocate(PREPROCESSOR_FEATURE_SIZE);
if (this->new_features_data_ == nullptr) {
ESP_LOGE(TAG, "Could not allocate the audio features buffer.");
return false;
}
this->preprocessor_audio_buffer_ = audio_samples_allocator.allocate(SAMPLE_DURATION_COUNT);
if (this->preprocessor_audio_buffer_ == nullptr) {
this->preprocessor_audio_buffer_ = audio_samples_allocator.allocate(this->new_samples_to_get_());
if (this->preprocessor_audio_buffer_ == nullptr) {
ESP_LOGE(TAG, "Could not allocate the audio preprocessor's buffer.");
return false;
}
this->preprocessor_model_ = tflite::GetModel(G_AUDIO_PREPROCESSOR_INT8_TFLITE);
if (this->preprocessor_model_->version() != TFLITE_SCHEMA_VERSION) {
ESP_LOGE(TAG, "Wake word's audio preprocessor model's schema is not supported");
return false;
}
this->streaming_model_ = tflite::GetModel(this->model_start_);
if (this->streaming_model_->version() != TFLITE_SCHEMA_VERSION) {
ESP_LOGE(TAG, "Wake word's streaming model's schema is not supported");
if (this->ring_buffer_ == nullptr) {
this->ring_buffer_ = RingBuffer::create(BUFFER_SIZE * sizeof(int16_t));
if (this->ring_buffer_ == nullptr) {
ESP_LOGE(TAG, "Could not allocate ring buffer");
return false;
}
static tflite::MicroMutableOpResolver<18> preprocessor_op_resolver;
static tflite::MicroMutableOpResolver<17> streaming_op_resolver;
if (!this->register_preprocessor_ops_(preprocessor_op_resolver))
return false;
if (!this->register_streaming_ops_(streaming_op_resolver))
return false;
tflite::MicroAllocator *ma =
tflite::MicroAllocator::Create(this->streaming_var_arena_, STREAMING_MODEL_VARIABLE_ARENA_SIZE);
this->mrv_ = tflite::MicroResourceVariables::Create(ma, 15);
static tflite::MicroInterpreter static_preprocessor_interpreter(
this->preprocessor_model_, preprocessor_op_resolver, this->preprocessor_tensor_arena_, PREPROCESSOR_ARENA_SIZE);
static tflite::MicroInterpreter static_streaming_interpreter(this->streaming_model_, streaming_op_resolver,
this->streaming_tensor_arena_,
STREAMING_MODEL_ARENA_SIZE, this->mrv_);
this->preprocessor_interperter_ = &static_preprocessor_interpreter;
this->streaming_interpreter_ = &static_streaming_interpreter;
// Allocate tensors for each models.
if (this->preprocessor_interperter_->AllocateTensors() != kTfLiteOk) {
ESP_LOGE(TAG, "Failed to allocate tensors for the audio preprocessor");
return false;
}
if (this->streaming_interpreter_->AllocateTensors() != kTfLiteOk) {
ESP_LOGE(TAG, "Failed to allocate tensors for the streaming model");
return false;
}
// Verify input tensor matches expected values
TfLiteTensor *input = this->streaming_interpreter_->input(0);
if ((input->dims->size != 3) || (input->dims->data[0] != 1) || (input->dims->data[0] != 1) ||
(input->dims->data[1] != 1) || (input->dims->data[2] != PREPROCESSOR_FEATURE_SIZE)) {
ESP_LOGE(TAG, "Wake word detection model tensor input dimensions is not 1x1x%u", input->dims->data[2]);
return false;
}
if (input->type != kTfLiteInt8) {
ESP_LOGE(TAG, "Wake word detection model tensor input is not int8.");
return false;
}
// Verify output tensor matches expected values
TfLiteTensor *output = this->streaming_interpreter_->output(0);
if ((output->dims->size != 2) || (output->dims->data[0] != 1) || (output->dims->data[1] != 1)) {
ESP_LOGE(TAG, "Wake word detection model tensor output dimensions is not 1x1.");
}
if (output->type != kTfLiteUInt8) {
ESP_LOGE(TAG, "Wake word detection model tensor input is not uint8.");
return false;
}
this->recent_streaming_probabilities_.resize(this->sliding_window_average_size_, 0.0);
return true;
}
bool MicroWakeWord::update_features_() {
// Retrieve strided audio samples
int16_t *audio_samples = nullptr;
if (!this->stride_audio_samples_(&audio_samples)) {
return false;
}
// Compute the features for the newest audio samples
if (!this->generate_single_feature_(audio_samples, SAMPLE_DURATION_COUNT, this->new_features_data_)) {
return false;
}
return true;
}
float MicroWakeWord::perform_streaming_inference_() {
TfLiteTensor *input = this->streaming_interpreter_->input(0);
void MicroWakeWord::deallocate_buffers_() {
ExternalRAMAllocator<int16_t> audio_samples_allocator(ExternalRAMAllocator<int16_t>::ALLOW_FAILURE);
audio_samples_allocator.deallocate(this->input_buffer_, INPUT_BUFFER_SIZE * sizeof(int16_t));
this->input_buffer_ = nullptr;
audio_samples_allocator.deallocate(this->preprocessor_audio_buffer_, this->new_samples_to_get_());
this->preprocessor_audio_buffer_ = nullptr;
}
size_t bytes_to_copy = input->bytes;
memcpy((void *) (tflite::GetTensorData<int8_t>(input)), (const void *) (this->new_features_data_), bytes_to_copy);
uint32_t prior_invoke = millis();
TfLiteStatus invoke_status = this->streaming_interpreter_->Invoke();
if (invoke_status != kTfLiteOk) {
ESP_LOGW(TAG, "Streaming Interpreter Invoke failed");
bool MicroWakeWord::load_models_() {
// Setup preprocesor feature generator
if (!FrontendPopulateState(&this->frontend_config_, &this->frontend_state_, AUDIO_SAMPLE_FREQUENCY)) {
ESP_LOGD(TAG, "Failed to populate frontend state");
FrontendFreeStateContents(&this->frontend_state_);
return false;
}
ESP_LOGV(TAG, "Streaming Inference Latency=%" PRIu32 " ms", (millis() - prior_invoke));
TfLiteTensor *output = this->streaming_interpreter_->output(0);
return static_cast<float>(output->data.uint8[0]) / 255.0;
}
bool MicroWakeWord::detect_wake_word_() {
// Preprocess the newest audio samples into features
if (!this->update_features_()) {
// Setup streaming models
for (auto &model : this->wake_word_models_) {
if (!model.load_model(this->streaming_op_resolver_)) {
ESP_LOGE(TAG, "Failed to initialize a wake word model.");
return false;
}
}
#ifdef USE_MICRO_WAKE_WORD_VAD
if (!this->vad_model_->load_model(this->streaming_op_resolver_)) {
ESP_LOGE(TAG, "Failed to initialize VAD model.");
return false;
}
#endif
// Perform inference
float streaming_prob = this->perform_streaming_inference_();
// Add the most recent probability to the sliding window
this->recent_streaming_probabilities_[this->last_n_index_] = streaming_prob;
++this->last_n_index_;
if (this->last_n_index_ == this->sliding_window_average_size_)
this->last_n_index_ = 0;
float sum = 0.0;
for (auto &prob : this->recent_streaming_probabilities_) {
sum += prob;
return true;
}
float sliding_window_average = sum / static_cast<float>(this->sliding_window_average_size_);
void MicroWakeWord::unload_models_() {
FrontendFreeStateContents(&this->frontend_state_);
// Ensure we have enough samples since the last positive detection
for (auto &model : this->wake_word_models_) {
model.unload_model();
}
#ifdef USE_MICRO_WAKE_WORD_VAD
this->vad_model_->unload_model();
#endif
}
void MicroWakeWord::update_model_probabilities_() {
int8_t audio_features[PREPROCESSOR_FEATURE_SIZE];
if (!this->generate_features_for_window_(audio_features)) {
return;
}
// Increase the counter since the last positive detection
this->ignore_windows_ = std::min(this->ignore_windows_ + 1, 0);
for (auto &model : this->wake_word_models_) {
// Perform inference
model.perform_streaming_inference(audio_features);
}
#ifdef USE_MICRO_WAKE_WORD_VAD
this->vad_model_->perform_streaming_inference(audio_features);
#endif
}
bool MicroWakeWord::detect_wake_words_() {
// Verify we have processed samples since the last positive detection
if (this->ignore_windows_ < 0) {
return false;
}
// Detect the wake word if the sliding window average is above the cutoff
if (sliding_window_average > this->probability_cutoff_) {
this->ignore_windows_ = -MIN_SLICES_BEFORE_DETECTION;
for (auto &prob : this->recent_streaming_probabilities_) {
prob = 0;
}
#ifdef USE_MICRO_WAKE_WORD_VAD
bool vad_state = this->vad_model_->determine_detected();
#endif
ESP_LOGD(TAG, "Wake word sliding average probability is %.3f and most recent probability is %.3f",
sliding_window_average, streaming_prob);
for (auto &model : this->wake_word_models_) {
if (model.determine_detected()) {
#ifdef USE_MICRO_WAKE_WORD_VAD
if (vad_state) {
#endif
this->detected_wake_word_ = model.get_wake_word();
return true;
#ifdef USE_MICRO_WAKE_WORD_VAD
} else {
ESP_LOGD(TAG, "Wake word model predicts %s, but VAD model doesn't.", model.get_wake_word().c_str());
}
#endif
}
}
return false;
}
void MicroWakeWord::set_sliding_window_average_size(size_t size) {
this->sliding_window_average_size_ = size;
this->recent_streaming_probabilities_.resize(this->sliding_window_average_size_, 0.0);
bool MicroWakeWord::has_enough_samples_() {
return this->ring_buffer_->available() >=
(this->features_step_size_ * (AUDIO_SAMPLE_FREQUENCY / 1000)) * sizeof(int16_t);
}
bool MicroWakeWord::slice_available_() {
size_t available = this->ring_buffer_->available();
return available > (NEW_SAMPLES_TO_GET * sizeof(int16_t));
}
bool MicroWakeWord::stride_audio_samples_(int16_t **audio_samples) {
if (!this->slice_available_()) {
bool MicroWakeWord::generate_features_for_window_(int8_t features[PREPROCESSOR_FEATURE_SIZE]) {
// Ensure we have enough new audio samples in the ring buffer for a full window
if (!this->has_enough_samples_()) {
return false;
}
// Copy the last 320 bytes (160 samples over 10 ms) from the audio buffer to the start of the audio buffer
memcpy((void *) (this->preprocessor_audio_buffer_), (void *) (this->preprocessor_audio_buffer_ + NEW_SAMPLES_TO_GET),
HISTORY_SAMPLES_TO_KEEP * sizeof(int16_t));
// Copy 640 bytes (320 samples over 20 ms) from the ring buffer into the audio buffer offset 320 bytes (160 samples
// over 10 ms)
size_t bytes_read = this->ring_buffer_->read((void *) (this->preprocessor_audio_buffer_ + HISTORY_SAMPLES_TO_KEEP),
NEW_SAMPLES_TO_GET * sizeof(int16_t), pdMS_TO_TICKS(200));
size_t bytes_read = this->ring_buffer_->read((void *) (this->preprocessor_audio_buffer_),
this->new_samples_to_get_() * sizeof(int16_t), pdMS_TO_TICKS(200));
if (bytes_read == 0) {
ESP_LOGE(TAG, "Could not read data from Ring Buffer");
} else if (bytes_read < NEW_SAMPLES_TO_GET * sizeof(int16_t)) {
} else if (bytes_read < this->new_samples_to_get_() * sizeof(int16_t)) {
ESP_LOGD(TAG, "Partial Read of Data by Model");
ESP_LOGD(TAG, "Could only read %d bytes when required %d bytes ", bytes_read,
(int) (NEW_SAMPLES_TO_GET * sizeof(int16_t)));
(int) (this->new_samples_to_get_() * sizeof(int16_t)));
return false;
}
*audio_samples = this->preprocessor_audio_buffer_;
return true;
}
size_t num_samples_read;
struct FrontendOutput frontend_output = FrontendProcessSamples(
&this->frontend_state_, this->preprocessor_audio_buffer_, this->new_samples_to_get_(), &num_samples_read);
bool MicroWakeWord::generate_single_feature_(const int16_t *audio_data, const int audio_data_size,
int8_t feature_output[PREPROCESSOR_FEATURE_SIZE]) {
TfLiteTensor *input = this->preprocessor_interperter_->input(0);
TfLiteTensor *output = this->preprocessor_interperter_->output(0);
std::copy_n(audio_data, audio_data_size, tflite::GetTensorData<int16_t>(input));
if (this->preprocessor_interperter_->Invoke() != kTfLiteOk) {
ESP_LOGE(TAG, "Failed to preprocess audio for local wake word.");
return false;
for (size_t i = 0; i < frontend_output.size; ++i) {
// These scaling values are set to match the TFLite audio frontend int8 output.
// The feature pipeline outputs 16-bit signed integers in roughly a 0 to 670
// range. In training, these are then arbitrarily divided by 25.6 to get
// float values in the rough range of 0.0 to 26.0. This scaling is performed
// for historical reasons, to match up with the output of other feature
// generators.
// The process is then further complicated when we quantize the model. This
// means we have to scale the 0.0 to 26.0 real values to the -128 to 127
// signed integer numbers.
// All this means that to get matching values from our integer feature
// output into the tensor input, we have to perform:
// input = (((feature / 25.6) / 26.0) * 256) - 128
// To simplify this and perform it in 32-bit integer math, we rearrange to:
// input = (feature * 256) / (25.6 * 26.0) - 128
constexpr int32_t value_scale = 256;
constexpr int32_t value_div = 666; // 666 = 25.6 * 26.0 after rounding
int32_t value = ((frontend_output.values[i] * value_scale) + (value_div / 2)) / value_div;
value -= 128;
if (value < -128) {
value = -128;
}
if (value > 127) {
value = 127;
}
features[i] = value;
}
std::memcpy(feature_output, tflite::GetTensorData<int8_t>(output), PREPROCESSOR_FEATURE_SIZE * sizeof(int8_t));
return true;
}
bool MicroWakeWord::register_preprocessor_ops_(tflite::MicroMutableOpResolver<18> &op_resolver) {
if (op_resolver.AddReshape() != kTfLiteOk)
return false;
if (op_resolver.AddCast() != kTfLiteOk)
return false;
if (op_resolver.AddStridedSlice() != kTfLiteOk)
return false;
if (op_resolver.AddConcatenation() != kTfLiteOk)
return false;
if (op_resolver.AddMul() != kTfLiteOk)
return false;
if (op_resolver.AddAdd() != kTfLiteOk)
return false;
if (op_resolver.AddDiv() != kTfLiteOk)
return false;
if (op_resolver.AddMinimum() != kTfLiteOk)
return false;
if (op_resolver.AddMaximum() != kTfLiteOk)
return false;
if (op_resolver.AddWindow() != kTfLiteOk)
return false;
if (op_resolver.AddFftAutoScale() != kTfLiteOk)
return false;
if (op_resolver.AddRfft() != kTfLiteOk)
return false;
if (op_resolver.AddEnergy() != kTfLiteOk)
return false;
if (op_resolver.AddFilterBank() != kTfLiteOk)
return false;
if (op_resolver.AddFilterBankSquareRoot() != kTfLiteOk)
return false;
if (op_resolver.AddFilterBankSpectralSubtraction() != kTfLiteOk)
return false;
if (op_resolver.AddPCAN() != kTfLiteOk)
return false;
if (op_resolver.AddFilterBankLog() != kTfLiteOk)
return false;
return true;
void MicroWakeWord::reset_states_() {
ESP_LOGD(TAG, "Resetting buffers and probabilities");
this->ring_buffer_->reset();
this->ignore_windows_ = -MIN_SLICES_BEFORE_DETECTION;
for (auto &model : this->wake_word_models_) {
model.reset_probabilities();
}
#ifdef USE_MICRO_WAKE_WORD_VAD
this->vad_model_->reset_probabilities();
#endif
}
bool MicroWakeWord::register_streaming_ops_(tflite::MicroMutableOpResolver<17> &op_resolver) {
bool MicroWakeWord::register_streaming_ops_(tflite::MicroMutableOpResolver<20> &op_resolver) {
if (op_resolver.AddCallOnce() != kTfLiteOk)
return false;
if (op_resolver.AddVarHandle() != kTfLiteOk)
@ -496,6 +442,12 @@ bool MicroWakeWord::register_streaming_ops_(tflite::MicroMutableOpResolver<17> &
return false;
if (op_resolver.AddMaxPool2D() != kTfLiteOk)
return false;
if (op_resolver.AddPad() != kTfLiteOk)
return false;
if (op_resolver.AddPack() != kTfLiteOk)
return false;
if (op_resolver.AddSplitV() != kTfLiteOk)
return false;
return true;
}
@ -504,5 +456,3 @@ bool MicroWakeWord::register_streaming_ops_(tflite::MicroMutableOpResolver<17> &
} // namespace esphome
#endif // USE_ESP_IDF
#endif // CLANG_TIDY

View file

@ -1,21 +1,18 @@
#pragma once
/**
* This is a workaround until we can figure out a way to get
* the tflite-micro idf component code available in CI
*
* */
//
#ifndef CLANG_TIDY
#ifdef USE_ESP_IDF
#include "preprocessor_settings.h"
#include "streaming_model.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/ring_buffer.h"
#include "esphome/components/microphone/microphone.h"
#include <frontend_util.h>
#include <tensorflow/lite/core/c/common.h>
#include <tensorflow/lite/micro/micro_interpreter.h>
#include <tensorflow/lite/micro/micro_mutable_op_resolver.h>
@ -23,35 +20,6 @@
namespace esphome {
namespace micro_wake_word {
// The following are dictated by the preprocessor model
//
// The number of features the audio preprocessor generates per slice
static const uint8_t PREPROCESSOR_FEATURE_SIZE = 40;
// How frequently the preprocessor generates a new set of features
static const uint8_t FEATURE_STRIDE_MS = 20;
// Duration of each slice used as input into the preprocessor
static const uint8_t FEATURE_DURATION_MS = 30;
// Audio sample frequency in hertz
static const uint16_t AUDIO_SAMPLE_FREQUENCY = 16000;
// The number of old audio samples that are saved to be part of the next feature window
static const uint16_t HISTORY_SAMPLES_TO_KEEP =
((FEATURE_DURATION_MS - FEATURE_STRIDE_MS) * (AUDIO_SAMPLE_FREQUENCY / 1000));
// The number of new audio samples to receive to be included with the next feature window
static const uint16_t NEW_SAMPLES_TO_GET = (FEATURE_STRIDE_MS * (AUDIO_SAMPLE_FREQUENCY / 1000));
// The total number of audio samples included in the feature window
static const uint16_t SAMPLE_DURATION_COUNT = FEATURE_DURATION_MS * AUDIO_SAMPLE_FREQUENCY / 1000;
// Number of bytes in memory needed for the preprocessor arena
static const uint32_t PREPROCESSOR_ARENA_SIZE = 9528;
// The following configure the streaming wake word model
//
// The number of audio slices to process before accepting a positive detection
static const uint8_t MIN_SLICES_BEFORE_DETECTION = 74;
// Number of bytes in memory needed for the streaming wake word model
static const uint32_t STREAMING_MODEL_ARENA_SIZE = 64000;
static const uint32_t STREAMING_MODEL_VARIABLE_ARENA_SIZE = 1024;
enum State {
IDLE,
START_MICROPHONE,
@ -61,6 +29,9 @@ enum State {
STOPPING_MICROPHONE,
};
// The number of audio slices to process before accepting a positive detection
static const uint8_t MIN_SLICES_BEFORE_DETECTION = 74;
class MicroWakeWord : public Component {
public:
void setup() override;
@ -73,28 +44,21 @@ class MicroWakeWord : public Component {
bool is_running() const { return this->state_ != State::IDLE; }
bool initialize_models();
std::string get_wake_word() { return this->wake_word_; }
// Increasing either of these will reduce the rate of false acceptances while increasing the false rejection rate
void set_probability_cutoff(float probability_cutoff) { this->probability_cutoff_ = probability_cutoff; }
void set_sliding_window_average_size(size_t size);
void set_features_step_size(uint8_t step_size) { this->features_step_size_ = step_size; }
void set_microphone(microphone::Microphone *microphone) { this->microphone_ = microphone; }
Trigger<std::string> *get_wake_word_detected_trigger() const { return this->wake_word_detected_trigger_; }
void set_model_start(const uint8_t *model_start) { this->model_start_ = model_start; }
void set_wake_word(const std::string &wake_word) { this->wake_word_ = wake_word; }
void add_wake_word_model(const uint8_t *model_start, float probability_cutoff, size_t sliding_window_average_size,
const std::string &wake_word, size_t tensor_arena_size);
#ifdef USE_MICRO_WAKE_WORD_VAD
void add_vad_model(const uint8_t *model_start, float probability_cutoff, size_t sliding_window_size,
size_t tensor_arena_size);
#endif
protected:
void set_state_(State state);
int read_microphone_();
const uint8_t *model_start_;
std::string wake_word_;
microphone::Microphone *microphone_{nullptr};
Trigger<std::string> *wake_word_detected_trigger_ = new Trigger<std::string>();
State state_{State::IDLE};
@ -102,85 +66,93 @@ class MicroWakeWord : public Component {
std::unique_ptr<RingBuffer> ring_buffer_;
int16_t *input_buffer_;
std::vector<WakeWordModel> wake_word_models_;
const tflite::Model *preprocessor_model_{nullptr};
const tflite::Model *streaming_model_{nullptr};
tflite::MicroInterpreter *streaming_interpreter_{nullptr};
tflite::MicroInterpreter *preprocessor_interperter_{nullptr};
#ifdef USE_MICRO_WAKE_WORD_VAD
std::unique_ptr<VADModel> vad_model_;
#endif
std::vector<float> recent_streaming_probabilities_;
size_t last_n_index_{0};
tflite::MicroMutableOpResolver<20> streaming_op_resolver_;
float probability_cutoff_{0.5};
size_t sliding_window_average_size_{10};
// Audio frontend handles generating spectrogram features
struct FrontendConfig frontend_config_;
struct FrontendState frontend_state_;
// When the wake word detection first starts or after the word has been detected once, we ignore this many audio
// feature slices before accepting a positive detection again
// When the wake word detection first starts, we ignore this many audio
// feature slices before accepting a positive detection
int16_t ignore_windows_{-MIN_SLICES_BEFORE_DETECTION};
uint8_t *streaming_var_arena_{nullptr};
uint8_t *streaming_tensor_arena_{nullptr};
uint8_t *preprocessor_tensor_arena_{nullptr};
int8_t *new_features_data_{nullptr};
uint8_t features_step_size_;
tflite::MicroResourceVariables *mrv_{nullptr};
// Stores audio fed into feature generator preprocessor
int16_t *preprocessor_audio_buffer_;
// Stores audio read from the microphone before being added to the ring buffer.
int16_t *input_buffer_{nullptr};
// Stores audio to be fed into the audio frontend for generating features.
int16_t *preprocessor_audio_buffer_{nullptr};
bool detected_{false};
std::string detected_wake_word_{""};
/** Detects if wake word has been said
void set_state_(State state);
/// @brief Tests if there are enough samples in the ring buffer to generate new features.
/// @return True if enough samples, false otherwise.
bool has_enough_samples_();
/** Reads audio from microphone into the ring buffer
*
* Audio data (16000 kHz with int16 samples) is read into the input_buffer_.
* Verifies the ring buffer has enough space for all audio data. If not, it logs
* a warning and resets the ring buffer entirely.
* @return Number of bytes written to the ring buffer
*/
size_t read_microphone_();
/// @brief Allocates memory for input_buffer_, preprocessor_audio_buffer_, and ring_buffer_
/// @return True if successful, false otherwise
bool allocate_buffers_();
/// @brief Frees memory allocated for input_buffer_ and preprocessor_audio_buffer_
void deallocate_buffers_();
/// @brief Loads streaming models and prepares the feature generation frontend
/// @return True if successful, false otherwise
bool load_models_();
/// @brief Deletes each model's TFLite interpreters and frees tensor arena memory. Frees memory used by the feature
/// generation frontend.
void unload_models_();
/** Performs inference with each configured model
*
* If enough audio samples are available, it will generate one slice of new features.
* If the streaming model predicts the wake word, then the nonstreaming model confirms it.
* @param ring_Buffer Ring buffer containing raw audio samples
* @return True if the wake word is detected, false otherwise
* It then loops through and performs inference with each of the loaded models.
*/
bool detect_wake_word_();
void update_model_probabilities_();
/// @brief Returns true if there are enough audio samples in the buffer to generate another slice of features
bool slice_available_();
/** Shifts previous feature slices over by one and generates a new slice of features
/** Checks every model's recent probabilities to determine if the wake word has been predicted
*
* @param ring_buffer ring buffer containing raw audio samples
* @return True if a new slice of features was generated, false otherwise
* Verifies the models have processed enough new samples for accurate predictions.
* Sets detected_wake_word_ to the wake word, if one is detected.
* @return True if a wake word is predicted, false otherwise
*/
bool update_features_();
bool detect_wake_words_();
/** Generates features from audio samples
/** Generates features for a window of audio samples
*
* Adapted from TFLite micro speech example
* @param audio_data Pointer to array with the audio samples
* @param audio_data_size The number of samples to use as input to the preprocessor model
* @param feature_output Array that will store the features
* Reads samples from the ring buffer and feeds them into the preprocessor frontend.
* Adapted from TFLite microspeech frontend.
* @param features int8_t array to store the audio features
* @return True if successful, false otherwise.
*/
bool generate_single_feature_(const int16_t *audio_data, int audio_data_size,
int8_t feature_output[PREPROCESSOR_FEATURE_SIZE]);
bool generate_features_for_window_(int8_t features[PREPROCESSOR_FEATURE_SIZE]);
/** Performs inference over the most recent feature slice with the streaming model
*
* @return Probability of the wake word between 0.0 and 1.0
*/
float perform_streaming_inference_();
/** Strides the audio samples by keeping the last 10 ms of the previous slice
*
* Adapted from the TFLite micro speech example
* @param ring_buffer Ring buffer containing raw audio samples
* @param audio_samples Pointer to an array that will store the strided audio samples
* @return True if successful, false otherwise
*/
bool stride_audio_samples_(int16_t **audio_samples);
/// @brief Returns true if successfully registered the preprocessor's TensorFlow operations
bool register_preprocessor_ops_(tflite::MicroMutableOpResolver<18> &op_resolver);
/// @brief Resets the ring buffer, ignore_windows_, and sliding window probabilities
void reset_states_();
/// @brief Returns true if successfully registered the streaming model's TensorFlow operations
bool register_streaming_ops_(tflite::MicroMutableOpResolver<17> &op_resolver);
bool register_streaming_ops_(tflite::MicroMutableOpResolver<20> &op_resolver);
inline uint16_t new_samples_to_get_() { return (this->features_step_size_ * (AUDIO_SAMPLE_FREQUENCY / 1000)); }
};
template<typename... Ts> class StartAction : public Action<Ts...>, public Parented<MicroWakeWord> {
@ -202,5 +174,3 @@ template<typename... Ts> class IsRunningCondition : public Condition<Ts...>, pub
} // namespace esphome
#endif // USE_ESP_IDF
#endif // CLANG_TIDY

View file

@ -0,0 +1,20 @@
#pragma once
#ifdef USE_ESP_IDF
#include <cstdint>
namespace esphome {
namespace micro_wake_word {
// The number of features the audio preprocessor generates per slice
static const uint8_t PREPROCESSOR_FEATURE_SIZE = 40;
// Duration of each slice used as input into the preprocessor
static const uint8_t FEATURE_DURATION_MS = 30;
// Audio sample frequency in hertz
static const uint16_t AUDIO_SAMPLE_FREQUENCY = 16000;
} // namespace micro_wake_word
} // namespace esphome
#endif

View file

@ -0,0 +1,189 @@
#ifdef USE_ESP_IDF
#include "streaming_model.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
static const char *const TAG = "micro_wake_word";
namespace esphome {
namespace micro_wake_word {
void WakeWordModel::log_model_config() {
ESP_LOGCONFIG(TAG, " - Wake Word: %s", this->wake_word_.c_str());
ESP_LOGCONFIG(TAG, " Probability cutoff: %.3f", this->probability_cutoff_);
ESP_LOGCONFIG(TAG, " Sliding window size: %d", this->sliding_window_size_);
}
void VADModel::log_model_config() {
ESP_LOGCONFIG(TAG, " - VAD Model");
ESP_LOGCONFIG(TAG, " Probability cutoff: %.3f", this->probability_cutoff_);
ESP_LOGCONFIG(TAG, " Sliding window size: %d", this->sliding_window_size_);
}
bool StreamingModel::load_model(tflite::MicroMutableOpResolver<20> &op_resolver) {
ExternalRAMAllocator<uint8_t> arena_allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
if (this->tensor_arena_ == nullptr) {
this->tensor_arena_ = arena_allocator.allocate(this->tensor_arena_size_);
if (this->tensor_arena_ == nullptr) {
ESP_LOGE(TAG, "Could not allocate the streaming model's tensor arena.");
return false;
}
}
if (this->var_arena_ == nullptr) {
this->var_arena_ = arena_allocator.allocate(STREAMING_MODEL_VARIABLE_ARENA_SIZE);
if (this->var_arena_ == nullptr) {
ESP_LOGE(TAG, "Could not allocate the streaming model's variable tensor arena.");
return false;
}
this->ma_ = tflite::MicroAllocator::Create(this->var_arena_, STREAMING_MODEL_VARIABLE_ARENA_SIZE);
this->mrv_ = tflite::MicroResourceVariables::Create(this->ma_, 20);
}
const tflite::Model *model = tflite::GetModel(this->model_start_);
if (model->version() != TFLITE_SCHEMA_VERSION) {
ESP_LOGE(TAG, "Streaming model's schema is not supported");
return false;
}
if (this->interpreter_ == nullptr) {
this->interpreter_ = make_unique<tflite::MicroInterpreter>(
tflite::GetModel(this->model_start_), op_resolver, this->tensor_arena_, this->tensor_arena_size_, this->mrv_);
if (this->interpreter_->AllocateTensors() != kTfLiteOk) {
ESP_LOGE(TAG, "Failed to allocate tensors for the streaming model");
return false;
}
// Verify input tensor matches expected values
// Dimension 3 will represent the first layer stride, so skip it may vary
TfLiteTensor *input = this->interpreter_->input(0);
if ((input->dims->size != 3) || (input->dims->data[0] != 1) ||
(input->dims->data[2] != PREPROCESSOR_FEATURE_SIZE)) {
ESP_LOGE(TAG, "Streaming model tensor input dimensions has improper dimensions.");
return false;
}
if (input->type != kTfLiteInt8) {
ESP_LOGE(TAG, "Streaming model tensor input is not int8.");
return false;
}
// Verify output tensor matches expected values
TfLiteTensor *output = this->interpreter_->output(0);
if ((output->dims->size != 2) || (output->dims->data[0] != 1) || (output->dims->data[1] != 1)) {
ESP_LOGE(TAG, "Streaming model tensor output dimension is not 1x1.");
}
if (output->type != kTfLiteUInt8) {
ESP_LOGE(TAG, "Streaming model tensor output is not uint8.");
return false;
}
}
return true;
}
void StreamingModel::unload_model() {
this->interpreter_.reset();
ExternalRAMAllocator<uint8_t> arena_allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
arena_allocator.deallocate(this->tensor_arena_, this->tensor_arena_size_);
this->tensor_arena_ = nullptr;
arena_allocator.deallocate(this->var_arena_, STREAMING_MODEL_VARIABLE_ARENA_SIZE);
this->var_arena_ = nullptr;
}
bool StreamingModel::perform_streaming_inference(const int8_t features[PREPROCESSOR_FEATURE_SIZE]) {
if (this->interpreter_ != nullptr) {
TfLiteTensor *input = this->interpreter_->input(0);
std::memmove(
(int8_t *) (tflite::GetTensorData<int8_t>(input)) + PREPROCESSOR_FEATURE_SIZE * this->current_stride_step_,
features, PREPROCESSOR_FEATURE_SIZE);
++this->current_stride_step_;
uint8_t stride = this->interpreter_->input(0)->dims->data[1];
if (this->current_stride_step_ >= stride) {
this->current_stride_step_ = 0;
TfLiteStatus invoke_status = this->interpreter_->Invoke();
if (invoke_status != kTfLiteOk) {
ESP_LOGW(TAG, "Streaming interpreter invoke failed");
return false;
}
TfLiteTensor *output = this->interpreter_->output(0);
++this->last_n_index_;
if (this->last_n_index_ == this->sliding_window_size_)
this->last_n_index_ = 0;
this->recent_streaming_probabilities_[this->last_n_index_] = output->data.uint8[0]; // probability;
}
return true;
}
ESP_LOGE(TAG, "Streaming interpreter is not initialized.");
return false;
}
void StreamingModel::reset_probabilities() {
for (auto &prob : this->recent_streaming_probabilities_) {
prob = 0;
}
}
WakeWordModel::WakeWordModel(const uint8_t *model_start, float probability_cutoff, size_t sliding_window_average_size,
const std::string &wake_word, size_t tensor_arena_size) {
this->model_start_ = model_start;
this->probability_cutoff_ = probability_cutoff;
this->sliding_window_size_ = sliding_window_average_size;
this->recent_streaming_probabilities_.resize(sliding_window_average_size, 0);
this->wake_word_ = wake_word;
this->tensor_arena_size_ = tensor_arena_size;
};
bool WakeWordModel::determine_detected() {
int32_t sum = 0;
for (auto &prob : this->recent_streaming_probabilities_) {
sum += prob;
}
float sliding_window_average = static_cast<float>(sum) / static_cast<float>(255 * this->sliding_window_size_);
// Detect the wake word if the sliding window average is above the cutoff
if (sliding_window_average > this->probability_cutoff_) {
ESP_LOGD(TAG, "The '%s' model sliding average probability is %.3f and most recent probability is %.3f",
this->wake_word_.c_str(), sliding_window_average,
this->recent_streaming_probabilities_[this->last_n_index_] / (255.0));
return true;
}
return false;
}
VADModel::VADModel(const uint8_t *model_start, float probability_cutoff, size_t sliding_window_size,
size_t tensor_arena_size) {
this->model_start_ = model_start;
this->probability_cutoff_ = probability_cutoff;
this->sliding_window_size_ = sliding_window_size;
this->recent_streaming_probabilities_.resize(sliding_window_size, 0);
this->tensor_arena_size_ = tensor_arena_size;
};
bool VADModel::determine_detected() {
uint8_t max = 0;
for (auto &prob : this->recent_streaming_probabilities_) {
max = std::max(prob, max);
}
return max > this->probability_cutoff_;
}
} // namespace micro_wake_word
} // namespace esphome
#endif

View file

@ -0,0 +1,84 @@
#pragma once
#ifdef USE_ESP_IDF
#include "preprocessor_settings.h"
#include <tensorflow/lite/core/c/common.h>
#include <tensorflow/lite/micro/micro_interpreter.h>
#include <tensorflow/lite/micro/micro_mutable_op_resolver.h>
namespace esphome {
namespace micro_wake_word {
static const uint32_t STREAMING_MODEL_VARIABLE_ARENA_SIZE = 1024;
class StreamingModel {
public:
virtual void log_model_config() = 0;
virtual bool determine_detected() = 0;
bool perform_streaming_inference(const int8_t features[PREPROCESSOR_FEATURE_SIZE]);
/// @brief Sets all recent_streaming_probabilities to 0
void reset_probabilities();
/// @brief Allocates tensor and variable arenas and sets up the model interpreter
/// @param op_resolver MicroMutableOpResolver object that must exist until the model is unloaded
/// @return True if successful, false otherwise
bool load_model(tflite::MicroMutableOpResolver<20> &op_resolver);
/// @brief Destroys the TFLite interpreter and frees the tensor and variable arenas' memory
void unload_model();
protected:
uint8_t current_stride_step_{0};
float probability_cutoff_;
size_t sliding_window_size_;
size_t last_n_index_{0};
size_t tensor_arena_size_;
std::vector<uint8_t> recent_streaming_probabilities_;
const uint8_t *model_start_;
uint8_t *tensor_arena_{nullptr};
uint8_t *var_arena_{nullptr};
std::unique_ptr<tflite::MicroInterpreter> interpreter_;
tflite::MicroResourceVariables *mrv_{nullptr};
tflite::MicroAllocator *ma_{nullptr};
};
class WakeWordModel final : public StreamingModel {
public:
WakeWordModel(const uint8_t *model_start, float probability_cutoff, size_t sliding_window_average_size,
const std::string &wake_word, size_t tensor_arena_size);
void log_model_config() override;
/// @brief Checks for the wake word by comparing the mean probability in the sliding window with the probability
/// cutoff
/// @return True if wake word is detected, false otherwise
bool determine_detected() override;
const std::string &get_wake_word() const { return this->wake_word_; }
protected:
std::string wake_word_;
};
class VADModel final : public StreamingModel {
public:
VADModel(const uint8_t *model_start, float probability_cutoff, size_t sliding_window_size, size_t tensor_arena_size);
void log_model_config() override;
/// @brief Checks for voice activity by comparing the max probability in the sliding window with the probability
/// cutoff
/// @return True if voice activity is detected, false otherwise
bool determine_detected() override;
};
} // namespace micro_wake_word
} // namespace esphome
#endif

View file

@ -293,4 +293,4 @@ async def to_code(config):
if CONF_HUMIDITY_SETPOINT in config:
sens = await sensor.new_sensor(config[CONF_HUMIDITY_SETPOINT])
cg.add(var.set_humidity_setpoint_sensor(sens))
cg.add_library("dudanov/MideaUART", "1.1.8")
cg.add_library("dudanov/MideaUART", "1.1.9")

View file

@ -52,6 +52,7 @@ const uint8_t MITSUBISHI_BYTE16 = 0X00;
climate::ClimateTraits MitsubishiClimate::traits() {
auto traits = climate::ClimateTraits();
traits.set_supports_current_temperature(this->sensor_ != nullptr);
traits.set_supports_action(false);
traits.set_visual_min_temperature(MITSUBISHI_TEMP_MIN);
traits.set_visual_max_temperature(MITSUBISHI_TEMP_MAX);

View file

@ -116,7 +116,8 @@ void ModbusController::on_modbus_read_registers(uint8_t function_code, uint16_t
ESP_LOGD(TAG, "Matched register. Address: 0x%02X. Value type: %zu. Register count: %u. Value: %0.1f.",
server_register->address, static_cast<uint8_t>(server_register->value_type),
server_register->register_count, value);
number_to_payload(sixteen_bit_response, value, server_register->value_type);
std::vector<uint16_t> payload = float_to_payload(value, server_register->value_type);
sixteen_bit_response.insert(sixteen_bit_response.end(), payload.cbegin(), payload.cend());
current_address += server_register->register_count;
found = true;
break;

View file

@ -37,6 +37,7 @@ RAW_ENCODING = {
"NONE": RawEncoding.NONE,
"HEXBYTES": RawEncoding.HEXBYTES,
"COMMA": RawEncoding.COMMA,
"ANSI": RawEncoding.ANSI,
}
CONFIG_SCHEMA = cv.All(
@ -49,7 +50,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_REGISTER_TYPE): cv.enum(MODBUS_REGISTER_TYPE),
cv.Optional(CONF_REGISTER_COUNT, default=0): cv.positive_int,
cv.Optional(CONF_RESPONSE_SIZE, default=2): cv.positive_int,
cv.Optional(CONF_RAW_ENCODE, default="NONE"): cv.enum(RAW_ENCODING),
cv.Optional(CONF_RAW_ENCODE, default="ANSI"): cv.enum(RAW_ENCODING),
}
),
validate_modbus_register,

View file

@ -15,7 +15,7 @@ void ModbusTextSensor::parse_and_publish(const std::vector<uint8_t> &data) {
std::ostringstream output;
uint8_t items_left = this->response_bytes;
uint8_t index = this->offset;
char buffer[4];
char buffer[5];
while ((items_left > 0) && index < data.size()) {
uint8_t b = data[index];
switch (this->encode_) {
@ -27,8 +27,11 @@ void ModbusTextSensor::parse_and_publish(const std::vector<uint8_t> &data) {
sprintf(buffer, index != this->offset ? ",%d" : "%d", b);
output << buffer;
break;
case RawEncoding::ANSI:
if (b < 0x20)
break;
// FALLTHROUGH
// Anything else no encoding
case RawEncoding::NONE:
default:
output << (char) b;
break;

View file

@ -9,7 +9,7 @@
namespace esphome {
namespace modbus_controller {
enum class RawEncoding { NONE = 0, HEXBYTES = 1, COMMA = 2 };
enum class RawEncoding { NONE = 0, HEXBYTES = 1, COMMA = 2, ANSI = 3 };
class ModbusTextSensor : public Component, public text_sensor::TextSensor, public SensorItem {
public:

View file

@ -27,7 +27,7 @@ async def to_code(config):
var = await binary_sensor.new_binary_sensor(config)
hub = await cg.get_variable(config[CONF_MPR121_ID])
cg.add(var.set_channel(config[CONF_CHANNEL]))
cg.register_parented(var, hub)
await cg.register_parented(var, hub)
if CONF_TOUCH_THRESHOLD in config:
cg.add(var.set_touch_threshold(config[CONF_TOUCH_THRESHOLD]))

View file

@ -6,12 +6,12 @@
#include "mqtt_const.h"
#ifdef USE_MQTT
#ifdef USE_DATETIME_TIME
#ifdef USE_DATETIME_DATETIME
namespace esphome {
namespace mqtt {
static const char *const TAG = "mqtt.datetime.time";
static const char *const TAG = "mqtt.datetime.datetime";
using namespace esphome::datetime;
@ -80,5 +80,5 @@ bool MQTTDateTimeComponent::publish_state(uint16_t year, uint8_t month, uint8_t
} // namespace mqtt
} // namespace esphome
#endif // USE_DATETIME_TIME
#endif // USE_DATETIME_DATETIME
#endif // USE_MQTT

View file

@ -3,7 +3,7 @@
#include "esphome/core/defines.h"
#ifdef USE_MQTT
#ifdef USE_DATETIME_TIME
#ifdef USE_DATETIME_DATETIME
#include "esphome/components/datetime/datetime_entity.h"
#include "mqtt_component.h"
@ -17,7 +17,7 @@ class MQTTDateTimeComponent : public mqtt::MQTTComponent {
*
* @param time The time entity.
*/
explicit MQTTDateTimeComponent(datetime::DateTimeEntity *time);
explicit MQTTDateTimeComponent(datetime::DateTimeEntity *datetime);
// ========== INTERNAL METHODS ==========
// (In most use cases you won't need these)
@ -41,5 +41,5 @@ class MQTTDateTimeComponent : public mqtt::MQTTComponent {
} // namespace mqtt
} // namespace esphome
#endif // USE_DATETIME_DATE
#endif // USE_DATETIME_DATETIME
#endif // USE_MQTT

View file

@ -19,7 +19,12 @@ IPAddress = network_ns.class_("IPAddress")
CONFIG_SCHEMA = cv.Schema(
{
cv.SplitDefault(CONF_ENABLE_IPV6): cv.All(
cv.SplitDefault(
CONF_ENABLE_IPV6,
esp8266=False,
esp32=False,
rp2040=False,
): cv.All(
cv.boolean, cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040])
),
cv.Optional(CONF_MIN_IPV6_ADDR_COUNT, default=0): cv.positive_int,
@ -28,18 +33,17 @@ CONFIG_SCHEMA = cv.Schema(
async def to_code(config):
if CONF_ENABLE_IPV6 in config:
cg.add_define("USE_NETWORK_IPV6", config[CONF_ENABLE_IPV6])
if (enable_ipv6 := config.get(CONF_ENABLE_IPV6, None)) is not None:
cg.add_define("USE_NETWORK_IPV6", enable_ipv6)
if enable_ipv6:
cg.add_define(
"USE_NETWORK_MIN_IPV6_ADDR_COUNT", config[CONF_MIN_IPV6_ADDR_COUNT]
)
if CORE.using_esp_idf:
add_idf_sdkconfig_option("CONFIG_LWIP_IPV6", config[CONF_ENABLE_IPV6])
add_idf_sdkconfig_option(
"CONFIG_LWIP_IPV6_AUTOCONFIG", config[CONF_ENABLE_IPV6]
)
add_idf_sdkconfig_option("CONFIG_LWIP_IPV6", enable_ipv6)
add_idf_sdkconfig_option("CONFIG_LWIP_IPV6_AUTOCONFIG", enable_ipv6)
else:
if config[CONF_ENABLE_IPV6]:
if enable_ipv6:
cg.add_build_flag("-DCONFIG_LWIP_IPV6")
cg.add_build_flag("-DCONFIG_LWIP_IPV6_AUTOCONFIG")
if CORE.is_rp2040:

View file

@ -26,6 +26,7 @@ from esphome.const import (
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_CARBON_DIOXIDE,
DEVICE_CLASS_CARBON_MONOXIDE,
DEVICE_CLASS_CONDUCTIVITY,
DEVICE_CLASS_CURRENT,
DEVICE_CLASS_DATA_RATE,
DEVICE_CLASS_DATA_SIZE,
@ -82,6 +83,7 @@ DEVICE_CLASSES = [
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_CARBON_DIOXIDE,
DEVICE_CLASS_CARBON_MONOXIDE,
DEVICE_CLASS_CONDUCTIVITY,
DEVICE_CLASS_CURRENT,
DEVICE_CLASS_DATA_RATE,
DEVICE_CLASS_DATA_SIZE,

View file

@ -13,6 +13,15 @@ void PMSA003IComponent::setup() {
PM25AQIData data;
bool successful_read = this->read_data_(&data);
if (!successful_read) {
for (int i = 0; i < 3; i++) {
successful_read = this->read_data_(&data);
if (successful_read) {
break;
}
}
}
if (!successful_read) {
this->mark_failed();
return;

View file

@ -42,6 +42,14 @@ COLOR_ORDERS = {
}
DATA_PIN_SCHEMA = pins.internal_gpio_output_pin_schema
def validate_dimension(value):
value = cv.positive_int(value)
if value % 2 != 0:
raise cv.Invalid("Width/height/offset must be divisible by 2")
return value
CONFIG_SCHEMA = cv.All(
display.FULL_DISPLAY_SCHEMA.extend(
cv.Schema(
@ -52,10 +60,14 @@ CONFIG_SCHEMA = cv.All(
cv.dimensions,
cv.Schema(
{
cv.Required(CONF_WIDTH): cv.int_,
cv.Required(CONF_HEIGHT): cv.int_,
cv.Optional(CONF_OFFSET_HEIGHT, default=0): cv.int_,
cv.Optional(CONF_OFFSET_WIDTH, default=0): cv.int_,
cv.Required(CONF_WIDTH): validate_dimension,
cv.Required(CONF_HEIGHT): validate_dimension,
cv.Optional(
CONF_OFFSET_HEIGHT, default=0
): validate_dimension,
cv.Optional(
CONF_OFFSET_WIDTH, default=0
): validate_dimension,
}
),
),

View file

@ -25,7 +25,23 @@ void QspiAmoLed::setup() {
}
void QspiAmoLed::update() {
if (!this->setup_complete_) {
return;
}
this->do_update_();
// Start addresses and widths/heights must be divisible by 2 (CASET/RASET restriction in datasheet)
if (this->x_low_ % 2 == 1) {
this->x_low_--;
}
if (this->x_high_ % 2 == 0) {
this->x_high_++;
}
if (this->y_low_ % 2 == 1) {
this->y_low_--;
}
if (this->y_high_ % 2 == 0) {
this->y_high_++;
}
int w = this->x_high_ - this->x_low_ + 1;
int h = this->y_high_ - this->y_low_ + 1;
this->draw_pixels_at(this->x_low_, this->y_low_, w, h, this->buffer_, this->color_mode_, display::COLOR_BITNESS_565,

View file

@ -65,13 +65,10 @@ class QspiAmoLed : public display::DisplayBuffer,
void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; }
void set_enable_pin(GPIOPin *enable_pin) { this->enable_pin_ = enable_pin; }
void set_width(uint16_t width) { this->width_ = width; }
void set_dimensions(uint16_t width, uint16_t height) {
this->width_ = width;
this->height_ = height;
}
int get_width() override { return this->width_; }
int get_height() override { return this->height_; }
void set_invert_colors(bool invert_colors) {
this->invert_colors_ = invert_colors;
this->reset_params_();

View file

@ -8,10 +8,10 @@ static const char *const TAG = "remote.dooya";
static const uint32_t HEADER_HIGH_US = 5000;
static const uint32_t HEADER_LOW_US = 1500;
static const uint32_t BIT_ZERO_HIGH_US = 750;
static const uint32_t BIT_ZERO_LOW_US = 350;
static const uint32_t BIT_ONE_HIGH_US = 350;
static const uint32_t BIT_ONE_LOW_US = 750;
static const uint32_t BIT_ZERO_HIGH_US = 350;
static const uint32_t BIT_ZERO_LOW_US = 750;
static const uint32_t BIT_ONE_HIGH_US = 750;
static const uint32_t BIT_ONE_LOW_US = 350;
void DooyaProtocol::encode(RemoteTransmitData *dst, const DooyaData &data) {
dst->set_carrier_frequency(0);

View file

@ -54,7 +54,7 @@ void RCSwitchBase::sync(RemoteTransmitData *dst) const {
}
}
void RCSwitchBase::transmit(RemoteTransmitData *dst, uint64_t code, uint8_t len) const {
dst->set_carrier_frequency(0);
dst->set_carrier_frequency(38000);
this->sync(dst);
for (int16_t i = len - 1; i >= 0; i--) {
if (code & ((uint64_t) 1 << i)) {

View file

@ -5,6 +5,7 @@ from esphome.const import (
CONF_ID,
DEVICE_CLASS_RESTART,
ENTITY_CATEGORY_CONFIG,
ICON_RESTART,
)
restart_ns = cg.esphome_ns.namespace("restart")
@ -12,6 +13,7 @@ RestartButton = restart_ns.class_("RestartButton", button.Button, cg.Component)
CONFIG_SCHEMA = button.button_schema(
RestartButton,
icon=ICON_RESTART,
device_class=DEVICE_CLASS_RESTART,
entity_category=ENTITY_CATEGORY_CONFIG,
).extend(cv.COMPONENT_SCHEMA)

View file

@ -47,10 +47,16 @@ def set_core_data(config):
def get_download_types(storage_json):
return [
{
"title": "UF2 format",
"title": "UF2 factory format",
"description": "For copying to RP2040 over USB.",
"file": "firmware.uf2",
"download": f"{storage_json.name}.uf2",
"download": f"{storage_json.name}.factory.uf2",
},
{
"title": "OTA format",
"description": "For OTA updating a device.",
"file": "firmware.ota.bin",
"download": f"{storage_json.name}.ota.bin",
},
]
@ -160,6 +166,8 @@ async def to_code(config):
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
cg.add_define("ESPHOME_VARIANT", "RP2040")
cg.add_platformio_option("extra_scripts", ["post:post_build.py"])
conf = config[CONF_FRAMEWORK]
cg.add_platformio_option("framework", "arduino")
cg.add_build_flag("-DUSE_ARDUINO")
@ -225,4 +233,10 @@ def generate_pio_files() -> bool:
# Called by writer.py
def copy_files() -> bool:
dir = os.path.dirname(__file__)
post_build_file = os.path.join(dir, "post_build.py.script")
copy_file_if_changed(
post_build_file,
CORE.relative_build_path("post_build.py"),
)
return generate_pio_files()

View file

@ -0,0 +1,23 @@
import shutil
# pylint: disable=E0602
Import("env") # noqa
def rp2040_copy_factory_uf2(source, target, env):
firmware_name = env.subst("$BUILD_DIR/${PROGNAME}.uf2")
new_file_name = env.subst("$BUILD_DIR/${PROGNAME}.factory.uf2")
shutil.copyfile(firmware_name, new_file_name)
def rp2040_copy_ota_bin(source, target, env):
firmware_name = env.subst("$BUILD_DIR/${PROGNAME}.bin")
new_file_name = env.subst("$BUILD_DIR/${PROGNAME}.ota.bin")
shutil.copyfile(firmware_name, new_file_name)
# pylint: disable=E0602
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", rp2040_copy_factory_uf2) # noqa
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", rp2040_copy_ota_bin) # noqa

View file

@ -56,9 +56,7 @@ CONFIG_SCHEMA = cv.All(
@coroutine_with_priority(50.0)
async def to_code(config):
if config[CONF_DISABLED]:
return
if not config[CONF_DISABLED]:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
@ -72,5 +70,6 @@ async def to_code(config):
config[CONF_BOOT_IS_GOOD_AFTER],
)
cg.add(RawExpression(f"if ({condition}) return"))
CORE.data[CONF_SAFE_MODE] = {}
CORE.data[CONF_SAFE_MODE][KEY_PAST_SAFE_MODE] = True

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