1
0
mirror of https://github.com/esphome/esphome.git synced 2025-04-11 05:10:34 +01:00

Merge remote-tracking branch 'origin/dev' into nrf52

This commit is contained in:
Tomasz Duda 2024-03-05 20:43:46 +01:00
commit ac967c5d2e
719 changed files with 18470 additions and 812 deletions

View File

@ -7,8 +7,21 @@
"PIP_BREAK_SYSTEM_PACKAGES": "1",
"PIP_ROOT_USER_ACTION": "ignore"
},
"runArgs": ["--privileged", "-e", "ESPHOME_DASHBOARD_USE_PING=1"],
"runArgs": [
"--privileged",
"-e",
"ESPHOME_DASHBOARD_USE_PING=1"
// uncomment and edit the path in order to pass though local USB serial to the conatiner
// , "--device=/dev/ttyACM0"
],
"appPort": 6052,
// if you are using avahi in the host device, uncomment these to allow the
// devcontainer to find devices via mdns
//"mounts": [
// "type=bind,source=/dev/bus/usb,target=/dev/bus/usb",
// "type=bind,source=/var/run/dbus,target=/var/run/dbus",
// "type=bind,source=/var/run/avahi-daemon/socket,target=/var/run/avahi-daemon/socket"
//],
"customizations": {
"vscode": {
"extensions": [

View File

@ -11,6 +11,7 @@ on:
- "**"
- "!.github/workflows/*.yml"
- ".github/workflows/ci.yml"
- "!.yamllint"
merge_group:
permissions:
@ -218,7 +219,7 @@ jobs:
. venv/bin/activate
pytest -vv --cov-report=xml --tb=native tests
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}

View File

@ -1,5 +1,6 @@
name: Needs Docs
# yamllint disable-line rule:truthy
on:
pull_request:
types: [labeled, unlabeled]

View File

@ -1,6 +1,7 @@
---
name: Synchronise Device Classes from Home Assistant
# yamllint disable-line rule:truthy
on:
workflow_dispatch:
schedule:
@ -36,7 +37,7 @@ jobs:
python ./script/sync-device_class.py
- name: Commit changes
uses: peter-evans/create-pull-request@v5.0.2
uses: peter-evans/create-pull-request@v6.0.1
with:
commit-message: "Synchronise Device Classes from Home Assistant"
committer: esphomebot <esphome@nabucasa.com>

View File

@ -1,5 +1,7 @@
---
name: YAML lint
# yamllint disable-line rule:truthy
on:
push:
branches: [dev, beta, release]
@ -19,4 +21,6 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.1
- name: Run yamllint
uses: frenck/action-yamllint@v1.4.2
uses: frenck/action-yamllint@v1.5.0
with:
strict: true

View File

@ -3,7 +3,7 @@
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 23.12.1
rev: 24.2.0
hooks:
- id: black
args:
@ -27,7 +27,7 @@ repos:
- --branch=release
- --branch=beta
- repo: https://github.com/asottile/pyupgrade
rev: v3.15.0
rev: v3.15.1
hooks:
- id: pyupgrade
args: [--py39-plus]

View File

@ -1,3 +1,18 @@
---
ignore: |
venv/
extends: default
ignore-from-file: .gitignore
rules:
document-start: disable
empty-lines:
level: error
max: 1
max-start: 0
max-end: 1
indentation:
level: error
spaces: 2
indent-sequences: true
check-multi-line-strings: false
line-length: disable

View File

@ -18,6 +18,7 @@ esphome/components/ac_dimmer/* @glmnet
esphome/components/adc/* @esphome/core
esphome/components/adc128s102/* @DeerMaximum
esphome/components/addressable_light/* @justfalter
esphome/components/ade7880/* @kpfleming
esphome/components/ade7953/* @angelnu
esphome/components/ade7953_i2c/* @angelnu
esphome/components/ade7953_spi/* @angelnu
@ -155,6 +156,7 @@ esphome/components/iaqcore/* @yozik04
esphome/components/ili9xxx/* @clydebarrow @nielsnl68
esphome/components/improv_base/* @esphome/core
esphome/components/improv_serial/* @esphome/core
esphome/components/ina226/* @Sergio303 @latonita
esphome/components/ina260/* @mreditor97
esphome/components/inkbird_ibsth1_mini/* @fkirill
esphome/components/inkplate6/* @jesserockz
@ -199,6 +201,7 @@ esphome/components/mcp9808/* @k7hpn
esphome/components/md5/* @esphome/core
esphome/components/mdns/* @esphome/core
esphome/components/media_player/* @jesserockz
esphome/components/micro_wake_word/* @jesserockz @kahrendt
esphome/components/micronova/* @jorre05
esphome/components/microphone/* @jesserockz
esphome/components/mics_4514/* @jesserockz
@ -222,6 +225,7 @@ esphome/components/mopeka_pro_check/* @spbrogan
esphome/components/mopeka_std_check/* @Fabian-Schmidt
esphome/components/mpl3115a2/* @kbickar
esphome/components/mpu6886/* @fabaff
esphome/components/ms8607/* @e28eta
esphome/components/network/* @esphome/core
esphome/components/nextion/* @senexcrenshaw
esphome/components/nextion/binary_sensor/* @senexcrenshaw
@ -362,6 +366,7 @@ esphome/components/uart/button/* @ssieb
esphome/components/ufire_ec/* @pvizeli
esphome/components/ufire_ise/* @pvizeli
esphome/components/ultrasonic/* @OttoWinter
esphome/components/uponor_smatrix/* @kroimon
esphome/components/vbus/* @ssieb
esphome/components/veml3235/* @kbx81
esphome/components/version/* @esphome/core

View File

@ -35,7 +35,7 @@ RUN \
iputils-ping=3:20221126-1 \
git=1:2.39.2-1.1 \
curl=7.88.1-10+deb12u5 \
openssh-client=1:9.2p1-2+deb12u1 \
openssh-client=1:9.2p1-2+deb12u2 \
python3-cffi=1.15.1-5 \
libcairo2=1.16.0-7 \
libmagic1=1:5.44-3 \

View File

@ -21,4 +21,10 @@ export PLATFORMIO_PLATFORMS_DIR="${pio_cache_base}/platforms"
export PLATFORMIO_PACKAGES_DIR="${pio_cache_base}/packages"
export PLATFORMIO_CACHE_DIR="${pio_cache_base}/cache"
# If /build is mounted, use that as the build path
# otherwise use path in /config (so that builds aren't lost on container restart)
if [[ -d /build ]]; then
export ESPHOME_BUILD_PATH=/build
fi
exec esphome "$@"

View File

@ -0,0 +1 @@
CODEOWNERS = ["@kpfleming"]

View File

@ -0,0 +1,302 @@
// This component was developed using knowledge gathered by a number
// of people who reverse-engineered the Shelly 3EM:
//
// @AndreKR on GitHub
// Axel (@Axel830 on GitHub)
// Marko (@goodkiller on GitHub)
// Michaël Piron (@michaelpiron on GitHub)
// Theo Arends (@arendst on GitHub)
#include "ade7880.h"
#include "ade7880_registers.h"
#include "esphome/core/log.h"
namespace esphome {
namespace ade7880 {
static const char *const TAG = "ade7880";
void IRAM_ATTR ADE7880Store::gpio_intr(ADE7880Store *arg) { arg->reset_done = true; }
void ADE7880::setup() {
if (this->irq0_pin_ != nullptr) {
this->irq0_pin_->setup();
}
this->irq1_pin_->setup();
if (this->reset_pin_ != nullptr) {
this->reset_pin_->setup();
}
this->store_.irq1_pin = this->irq1_pin_->to_isr();
this->irq1_pin_->attach_interrupt(ADE7880Store::gpio_intr, &this->store_, gpio::INTERRUPT_FALLING_EDGE);
// if IRQ1 is already asserted, the cause must be determined
if (this->irq1_pin_->digital_read() == 0) {
ESP_LOGD(TAG, "IRQ1 found asserted during setup()");
auto status1 = read_u32_register16_(STATUS1);
if ((status1 & ~STATUS1_RSTDONE) != 0) {
// not safe to proceed, must initiate reset
ESP_LOGD(TAG, "IRQ1 asserted for !RSTDONE, resetting device");
this->reset_device_();
return;
}
if ((status1 & STATUS1_RSTDONE) == STATUS1_RSTDONE) {
// safe to proceed, device has just completed reset cycle
ESP_LOGD(TAG, "Acknowledging RSTDONE");
this->write_u32_register16_(STATUS0, 0xFFFF);
this->write_u32_register16_(STATUS1, 0xFFFF);
this->init_device_();
return;
}
}
this->reset_device_();
}
void ADE7880::loop() {
// check for completion of a reset cycle
if (!this->store_.reset_done) {
return;
}
ESP_LOGD(TAG, "Acknowledging RSTDONE");
this->write_u32_register16_(STATUS0, 0xFFFF);
this->write_u32_register16_(STATUS1, 0xFFFF);
this->init_device_();
this->store_.reset_done = false;
this->store_.reset_pending = false;
}
template<typename F>
void ADE7880::update_sensor_from_s24zp_register16_(sensor::Sensor *sensor, uint16_t a_register, F &&f) {
if (sensor == nullptr) {
return;
}
float val = this->read_s24zp_register16_(a_register);
sensor->publish_state(f(val));
}
template<typename F>
void ADE7880::update_sensor_from_s16_register16_(sensor::Sensor *sensor, uint16_t a_register, F &&f) {
if (sensor == nullptr) {
return;
}
float val = this->read_s16_register16_(a_register);
sensor->publish_state(f(val));
}
template<typename F>
void ADE7880::update_sensor_from_s32_register16_(sensor::Sensor *sensor, uint16_t a_register, F &&f) {
if (sensor == nullptr) {
return;
}
float val = this->read_s32_register16_(a_register);
sensor->publish_state(f(val));
}
void ADE7880::update() {
if (this->store_.reset_pending) {
return;
}
auto start = millis();
if (this->channel_n_ != nullptr) {
auto *chan = this->channel_n_;
this->update_sensor_from_s24zp_register16_(chan->current, NIRMS, [](float val) { return val / 100000.0f; });
}
if (this->channel_a_ != nullptr) {
auto *chan = this->channel_a_;
this->update_sensor_from_s24zp_register16_(chan->current, AIRMS, [](float val) { return val / 100000.0f; });
this->update_sensor_from_s24zp_register16_(chan->voltage, BVRMS, [](float val) { return val / 10000.0f; });
this->update_sensor_from_s24zp_register16_(chan->active_power, AWATT, [](float val) { return val / 100.0f; });
this->update_sensor_from_s24zp_register16_(chan->apparent_power, AVA, [](float val) { return val / 100.0f; });
this->update_sensor_from_s16_register16_(chan->power_factor, APF,
[](float val) { return std::abs(val / -327.68f); });
this->update_sensor_from_s32_register16_(chan->forward_active_energy, AFWATTHR, [&chan](float val) {
return chan->forward_active_energy_total += val / 14400.0f;
});
this->update_sensor_from_s32_register16_(chan->reverse_active_energy, AFWATTHR, [&chan](float val) {
return chan->reverse_active_energy_total += val / 14400.0f;
});
}
if (this->channel_b_ != nullptr) {
auto *chan = this->channel_b_;
this->update_sensor_from_s24zp_register16_(chan->current, BIRMS, [](float val) { return val / 100000.0f; });
this->update_sensor_from_s24zp_register16_(chan->voltage, BVRMS, [](float val) { return val / 10000.0f; });
this->update_sensor_from_s24zp_register16_(chan->active_power, BWATT, [](float val) { return val / 100.0f; });
this->update_sensor_from_s24zp_register16_(chan->apparent_power, BVA, [](float val) { return val / 100.0f; });
this->update_sensor_from_s16_register16_(chan->power_factor, BPF,
[](float val) { return std::abs(val / -327.68f); });
this->update_sensor_from_s32_register16_(chan->forward_active_energy, BFWATTHR, [&chan](float val) {
return chan->forward_active_energy_total += val / 14400.0f;
});
this->update_sensor_from_s32_register16_(chan->reverse_active_energy, BFWATTHR, [&chan](float val) {
return chan->reverse_active_energy_total += val / 14400.0f;
});
}
if (this->channel_c_ != nullptr) {
auto *chan = this->channel_c_;
this->update_sensor_from_s24zp_register16_(chan->current, CIRMS, [](float val) { return val / 100000.0f; });
this->update_sensor_from_s24zp_register16_(chan->voltage, CVRMS, [](float val) { return val / 10000.0f; });
this->update_sensor_from_s24zp_register16_(chan->active_power, CWATT, [](float val) { return val / 100.0f; });
this->update_sensor_from_s24zp_register16_(chan->apparent_power, CVA, [](float val) { return val / 100.0f; });
this->update_sensor_from_s16_register16_(chan->power_factor, CPF,
[](float val) { return std::abs(val / -327.68f); });
this->update_sensor_from_s32_register16_(chan->forward_active_energy, CFWATTHR, [&chan](float val) {
return chan->forward_active_energy_total += val / 14400.0f;
});
this->update_sensor_from_s32_register16_(chan->reverse_active_energy, CFWATTHR, [&chan](float val) {
return chan->reverse_active_energy_total += val / 14400.0f;
});
}
ESP_LOGD(TAG, "update took %u ms", millis() - start);
}
void ADE7880::dump_config() {
ESP_LOGCONFIG(TAG, "ADE7880:");
LOG_PIN(" IRQ0 Pin: ", this->irq0_pin_);
LOG_PIN(" IRQ1 Pin: ", this->irq1_pin_);
LOG_PIN(" RESET Pin: ", this->reset_pin_);
ESP_LOGCONFIG(TAG, " Frequency: %.0f Hz", this->frequency_);
if (this->channel_a_ != nullptr) {
ESP_LOGCONFIG(TAG, " Phase A:");
LOG_SENSOR(" ", "Current", this->channel_a_->current);
LOG_SENSOR(" ", "Voltage", this->channel_a_->voltage);
LOG_SENSOR(" ", "Active Power", this->channel_a_->active_power);
LOG_SENSOR(" ", "Apparent Power", this->channel_a_->apparent_power);
LOG_SENSOR(" ", "Power Factor", this->channel_a_->power_factor);
LOG_SENSOR(" ", "Forward Active Energy", this->channel_a_->forward_active_energy);
LOG_SENSOR(" ", "Reverse Active Energy", this->channel_a_->reverse_active_energy);
ESP_LOGCONFIG(TAG, " Calibration:");
ESP_LOGCONFIG(TAG, " Current: %u", this->channel_a_->current_gain_calibration);
ESP_LOGCONFIG(TAG, " Voltage: %d", this->channel_a_->voltage_gain_calibration);
ESP_LOGCONFIG(TAG, " Power: %d", this->channel_a_->power_gain_calibration);
ESP_LOGCONFIG(TAG, " Phase Angle: %u", this->channel_a_->phase_angle_calibration);
}
if (this->channel_b_ != nullptr) {
ESP_LOGCONFIG(TAG, " Phase B:");
LOG_SENSOR(" ", "Current", this->channel_b_->current);
LOG_SENSOR(" ", "Voltage", this->channel_b_->voltage);
LOG_SENSOR(" ", "Active Power", this->channel_b_->active_power);
LOG_SENSOR(" ", "Apparent Power", this->channel_b_->apparent_power);
LOG_SENSOR(" ", "Power Factor", this->channel_b_->power_factor);
LOG_SENSOR(" ", "Forward Active Energy", this->channel_b_->forward_active_energy);
LOG_SENSOR(" ", "Reverse Active Energy", this->channel_b_->reverse_active_energy);
ESP_LOGCONFIG(TAG, " Calibration:");
ESP_LOGCONFIG(TAG, " Current: %u", this->channel_b_->current_gain_calibration);
ESP_LOGCONFIG(TAG, " Voltage: %d", this->channel_b_->voltage_gain_calibration);
ESP_LOGCONFIG(TAG, " Power: %d", this->channel_b_->power_gain_calibration);
ESP_LOGCONFIG(TAG, " Phase Angle: %u", this->channel_b_->phase_angle_calibration);
}
if (this->channel_c_ != nullptr) {
ESP_LOGCONFIG(TAG, " Phase C:");
LOG_SENSOR(" ", "Current", this->channel_c_->current);
LOG_SENSOR(" ", "Voltage", this->channel_c_->voltage);
LOG_SENSOR(" ", "Active Power", this->channel_c_->active_power);
LOG_SENSOR(" ", "Apparent Power", this->channel_c_->apparent_power);
LOG_SENSOR(" ", "Power Factor", this->channel_c_->power_factor);
LOG_SENSOR(" ", "Forward Active Energy", this->channel_c_->forward_active_energy);
LOG_SENSOR(" ", "Reverse Active Energy", this->channel_c_->reverse_active_energy);
ESP_LOGCONFIG(TAG, " Calibration:");
ESP_LOGCONFIG(TAG, " Current: %u", this->channel_c_->current_gain_calibration);
ESP_LOGCONFIG(TAG, " Voltage: %d", this->channel_c_->voltage_gain_calibration);
ESP_LOGCONFIG(TAG, " Power: %d", this->channel_c_->power_gain_calibration);
ESP_LOGCONFIG(TAG, " Phase Angle: %u", this->channel_c_->phase_angle_calibration);
}
if (this->channel_n_ != nullptr) {
ESP_LOGCONFIG(TAG, " Neutral:");
LOG_SENSOR(" ", "Current", this->channel_n_->current);
ESP_LOGCONFIG(TAG, " Calibration:");
ESP_LOGCONFIG(TAG, " Current: %u", this->channel_n_->current_gain_calibration);
}
LOG_I2C_DEVICE(this);
LOG_UPDATE_INTERVAL(this);
}
void ADE7880::calibrate_s10zp_reading_(uint16_t a_register, int16_t calibration) {
if (calibration == 0) {
return;
}
this->write_s10zp_register16_(a_register, calibration);
}
void ADE7880::calibrate_s24zpse_reading_(uint16_t a_register, int32_t calibration) {
if (calibration == 0) {
return;
}
this->write_s24zpse_register16_(a_register, calibration);
}
void ADE7880::init_device_() {
this->write_u8_register16_(CONFIG2, CONFIG2_I2C_LOCK);
this->write_u16_register16_(GAIN, 0);
if (this->frequency_ > 55) {
this->write_u16_register16_(COMPMODE, COMPMODE_DEFAULT | COMPMODE_SELFREQ);
}
if (this->channel_n_ != nullptr) {
this->calibrate_s24zpse_reading_(NIGAIN, this->channel_n_->current_gain_calibration);
}
if (this->channel_a_ != nullptr) {
this->calibrate_s24zpse_reading_(AIGAIN, this->channel_a_->current_gain_calibration);
this->calibrate_s24zpse_reading_(AVGAIN, this->channel_a_->voltage_gain_calibration);
this->calibrate_s24zpse_reading_(APGAIN, this->channel_a_->power_gain_calibration);
this->calibrate_s10zp_reading_(APHCAL, this->channel_a_->phase_angle_calibration);
}
if (this->channel_b_ != nullptr) {
this->calibrate_s24zpse_reading_(BIGAIN, this->channel_b_->current_gain_calibration);
this->calibrate_s24zpse_reading_(BVGAIN, this->channel_b_->voltage_gain_calibration);
this->calibrate_s24zpse_reading_(BPGAIN, this->channel_b_->power_gain_calibration);
this->calibrate_s10zp_reading_(BPHCAL, this->channel_b_->phase_angle_calibration);
}
if (this->channel_c_ != nullptr) {
this->calibrate_s24zpse_reading_(CIGAIN, this->channel_c_->current_gain_calibration);
this->calibrate_s24zpse_reading_(CVGAIN, this->channel_c_->voltage_gain_calibration);
this->calibrate_s24zpse_reading_(CPGAIN, this->channel_c_->power_gain_calibration);
this->calibrate_s10zp_reading_(CPHCAL, this->channel_c_->phase_angle_calibration);
}
// write three default values to data memory RAM to flush the I2C write queue
this->write_s32_register16_(VLEVEL, 0);
this->write_s32_register16_(VLEVEL, 0);
this->write_s32_register16_(VLEVEL, 0);
this->write_u8_register16_(DSPWP_SEL, DSPWP_SEL_SET);
this->write_u8_register16_(DSPWP_SET, DSPWP_SET_RO);
this->write_u16_register16_(RUN, RUN_ENABLE);
}
void ADE7880::reset_device_() {
if (this->reset_pin_ != nullptr) {
ESP_LOGD(TAG, "Reset device using RESET pin");
this->reset_pin_->digital_write(false);
delay(1);
this->reset_pin_->digital_write(true);
} else {
ESP_LOGD(TAG, "Reset device using SWRST command");
this->write_u16_register16_(CONFIG, CONFIG_SWRST);
}
this->store_.reset_pending = true;
}
} // namespace ade7880
} // namespace esphome

View File

@ -0,0 +1,131 @@
#pragma once
// This component was developed using knowledge gathered by a number
// of people who reverse-engineered the Shelly 3EM:
//
// @AndreKR on GitHub
// Axel (@Axel830 on GitHub)
// Marko (@goodkiller on GitHub)
// Michaël Piron (@michaelpiron on GitHub)
// Theo Arends (@arendst on GitHub)
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/sensor/sensor.h"
#include "ade7880_registers.h"
namespace esphome {
namespace ade7880 {
struct NeutralChannel {
void set_current(sensor::Sensor *sens) { this->current = sens; }
void set_current_gain_calibration(int32_t val) { this->current_gain_calibration = val; }
sensor::Sensor *current{nullptr};
int32_t current_gain_calibration{0};
};
struct PowerChannel {
void set_current(sensor::Sensor *sens) { this->current = sens; }
void set_voltage(sensor::Sensor *sens) { this->voltage = sens; }
void set_active_power(sensor::Sensor *sens) { this->active_power = sens; }
void set_apparent_power(sensor::Sensor *sens) { this->apparent_power = sens; }
void set_power_factor(sensor::Sensor *sens) { this->power_factor = sens; }
void set_forward_active_energy(sensor::Sensor *sens) { this->forward_active_energy = sens; }
void set_reverse_active_energy(sensor::Sensor *sens) { this->reverse_active_energy = sens; }
void set_current_gain_calibration(int32_t val) { this->current_gain_calibration = val; }
void set_voltage_gain_calibration(int32_t val) { this->voltage_gain_calibration = val; }
void set_power_gain_calibration(int32_t val) { this->power_gain_calibration = val; }
void set_phase_angle_calibration(int32_t val) { this->phase_angle_calibration = val; }
sensor::Sensor *current{nullptr};
sensor::Sensor *voltage{nullptr};
sensor::Sensor *active_power{nullptr};
sensor::Sensor *apparent_power{nullptr};
sensor::Sensor *power_factor{nullptr};
sensor::Sensor *forward_active_energy{nullptr};
sensor::Sensor *reverse_active_energy{nullptr};
int32_t current_gain_calibration{0};
int32_t voltage_gain_calibration{0};
int32_t power_gain_calibration{0};
uint16_t phase_angle_calibration{0};
float forward_active_energy_total{0};
float reverse_active_energy_total{0};
};
// Store data in a class that doesn't use multiple-inheritance (no vtables in flash!)
struct ADE7880Store {
volatile bool reset_done{false};
bool reset_pending{false};
ISRInternalGPIOPin irq1_pin;
static void gpio_intr(ADE7880Store *arg);
};
class ADE7880 : public i2c::I2CDevice, public PollingComponent {
public:
void set_irq0_pin(InternalGPIOPin *pin) { this->irq0_pin_ = pin; }
void set_irq1_pin(InternalGPIOPin *pin) { this->irq1_pin_ = pin; }
void set_reset_pin(InternalGPIOPin *pin) { this->reset_pin_ = pin; }
void set_frequency(float frequency) { this->frequency_ = frequency; }
void set_channel_n(NeutralChannel *channel) { this->channel_n_ = channel; }
void set_channel_a(PowerChannel *channel) { this->channel_a_ = channel; }
void set_channel_b(PowerChannel *channel) { this->channel_b_ = channel; }
void set_channel_c(PowerChannel *channel) { this->channel_c_ = channel; }
void setup() override;
void loop() override;
void update() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
protected:
ADE7880Store store_{};
InternalGPIOPin *irq0_pin_{nullptr};
InternalGPIOPin *irq1_pin_{nullptr};
InternalGPIOPin *reset_pin_{nullptr};
float frequency_;
NeutralChannel *channel_n_{nullptr};
PowerChannel *channel_a_{nullptr};
PowerChannel *channel_b_{nullptr};
PowerChannel *channel_c_{nullptr};
void calibrate_s10zp_reading_(uint16_t a_register, int16_t calibration);
void calibrate_s24zpse_reading_(uint16_t a_register, int32_t calibration);
void init_device_();
// each of these functions allow the caller to pass in a lambda (or any other callable)
// which modifies the value read from the register before it is passed to the sensor
// the callable will be passed a 'float' value and is expected to return a 'float'
template<typename F> void update_sensor_from_s24zp_register16_(sensor::Sensor *sensor, uint16_t a_register, F &&f);
template<typename F> void update_sensor_from_s16_register16_(sensor::Sensor *sensor, uint16_t a_register, F &&f);
template<typename F> void update_sensor_from_s32_register16_(sensor::Sensor *sensor, uint16_t a_register, F &&f);
void reset_device_();
uint8_t read_u8_register16_(uint16_t a_register);
int16_t read_s16_register16_(uint16_t a_register);
uint16_t read_u16_register16_(uint16_t a_register);
int32_t read_s24zp_register16_(uint16_t a_register);
int32_t read_s32_register16_(uint16_t a_register);
uint32_t read_u32_register16_(uint16_t a_register);
void write_u8_register16_(uint16_t a_register, uint8_t value);
void write_s10zp_register16_(uint16_t a_register, int16_t value);
void write_u16_register16_(uint16_t a_register, uint16_t value);
void write_s24zpse_register16_(uint16_t a_register, int32_t value);
void write_s32_register16_(uint16_t a_register, int32_t value);
void write_u32_register16_(uint16_t a_register, uint32_t value);
};
} // namespace ade7880
} // namespace esphome

View File

@ -0,0 +1,101 @@
// This component was developed using knowledge gathered by a number
// of people who reverse-engineered the Shelly 3EM:
//
// @AndreKR on GitHub
// Axel (@Axel830 on GitHub)
// Marko (@goodkiller on GitHub)
// Michaël Piron (@michaelpiron on GitHub)
// Theo Arends (@arendst on GitHub)
#include "ade7880.h"
namespace esphome {
namespace ade7880 {
// adapted from https://stackoverflow.com/a/55912127/1886371
template<size_t Bits, typename T> inline T sign_extend(const T &v) noexcept {
using S = struct { signed Val : Bits; };
return reinterpret_cast<const S *>(&v)->Val;
}
// Register types
// unsigned 8-bit (uint8_t)
// signed 10-bit - 16-bit ZP on wire (int16_t, needs sign extension)
// unsigned 16-bit (uint16_t)
// unsigned 20-bit - 32-bit ZP on wire (uint32_t)
// signed 24-bit - 32-bit ZPSE on wire (int32_t, needs sign extension)
// signed 24-bit - 32-bit ZP on wire (int32_t, needs sign extension)
// signed 24-bit - 32-bit SE on wire (int32_t)
// signed 28-bit - 32-bit ZP on wire (int32_t, needs sign extension)
// unsigned 32-bit (uint32_t)
// signed 32-bit (int32_t)
uint8_t ADE7880::read_u8_register16_(uint16_t a_register) {
uint8_t in;
this->read_register16(a_register, &in, sizeof(in));
return in;
}
int16_t ADE7880::read_s16_register16_(uint16_t a_register) {
int16_t in;
this->read_register16(a_register, reinterpret_cast<uint8_t *>(&in), sizeof(in));
return convert_big_endian(in);
}
uint16_t ADE7880::read_u16_register16_(uint16_t a_register) {
uint16_t in;
this->read_register16(a_register, reinterpret_cast<uint8_t *>(&in), sizeof(in));
return convert_big_endian(in);
}
int32_t ADE7880::read_s24zp_register16_(uint16_t a_register) {
// s24zp means 24 bit signed value in the lower 24 bits of a 32-bit register
int32_t in;
this->read_register16(a_register, reinterpret_cast<uint8_t *>(&in), sizeof(in));
return sign_extend<24>(convert_big_endian(in));
}
int32_t ADE7880::read_s32_register16_(uint16_t a_register) {
int32_t in;
this->read_register16(a_register, reinterpret_cast<uint8_t *>(&in), sizeof(in));
return convert_big_endian(in);
}
uint32_t ADE7880::read_u32_register16_(uint16_t a_register) {
uint32_t in;
this->read_register16(a_register, reinterpret_cast<uint8_t *>(&in), sizeof(in));
return convert_big_endian(in);
}
void ADE7880::write_u8_register16_(uint16_t a_register, uint8_t value) {
this->write_register16(a_register, &value, sizeof(value));
}
void ADE7880::write_s10zp_register16_(uint16_t a_register, int16_t value) {
int16_t out = convert_big_endian(value & 0x03FF);
this->write_register16(a_register, reinterpret_cast<uint8_t *>(&out), sizeof(out));
}
void ADE7880::write_u16_register16_(uint16_t a_register, uint16_t value) {
uint16_t out = convert_big_endian(value);
this->write_register16(a_register, reinterpret_cast<uint8_t *>(&out), sizeof(out));
}
void ADE7880::write_s24zpse_register16_(uint16_t a_register, int32_t value) {
// s24zpse means a 24-bit signed value, sign-extended to 28 bits, in the lower 28 bits of a 32-bit register
int32_t out = convert_big_endian(value & 0x0FFFFFFF);
this->write_register16(a_register, reinterpret_cast<uint8_t *>(&out), sizeof(out));
}
void ADE7880::write_s32_register16_(uint16_t a_register, int32_t value) {
int32_t out = convert_big_endian(value);
this->write_register16(a_register, reinterpret_cast<uint8_t *>(&out), sizeof(out));
}
void ADE7880::write_u32_register16_(uint16_t a_register, uint32_t value) {
uint32_t out = convert_big_endian(value);
this->write_register16(a_register, reinterpret_cast<uint8_t *>(&out), sizeof(out));
}
} // namespace ade7880
} // namespace esphome

View File

@ -0,0 +1,243 @@
#pragma once
// This file is a modified version of the one created by Michaël Piron (@michaelpiron on GitHub)
// Source: https://www.analog.com/media/en/technical-documentation/application-notes/AN-1127.pdf
namespace esphome {
namespace ade7880 {
// DSP Data Memory RAM registers
constexpr uint16_t AIGAIN = 0x4380;
constexpr uint16_t AVGAIN = 0x4381;
constexpr uint16_t BIGAIN = 0x4382;
constexpr uint16_t BVGAIN = 0x4383;
constexpr uint16_t CIGAIN = 0x4384;
constexpr uint16_t CVGAIN = 0x4385;
constexpr uint16_t NIGAIN = 0x4386;
constexpr uint16_t DICOEFF = 0x4388;
constexpr uint16_t APGAIN = 0x4389;
constexpr uint16_t AWATTOS = 0x438A;
constexpr uint16_t BPGAIN = 0x438B;
constexpr uint16_t BWATTOS = 0x438C;
constexpr uint16_t CPGAIN = 0x438D;
constexpr uint16_t CWATTOS = 0x438E;
constexpr uint16_t AIRMSOS = 0x438F;
constexpr uint16_t AVRMSOS = 0x4390;
constexpr uint16_t BIRMSOS = 0x4391;
constexpr uint16_t BVRMSOS = 0x4392;
constexpr uint16_t CIRMSOS = 0x4393;
constexpr uint16_t CVRMSOS = 0x4394;
constexpr uint16_t NIRMSOS = 0x4395;
constexpr uint16_t HPGAIN = 0x4398;
constexpr uint16_t ISUMLVL = 0x4399;
constexpr uint16_t VLEVEL = 0x439F;
constexpr uint16_t AFWATTOS = 0x43A2;
constexpr uint16_t BFWATTOS = 0x43A3;
constexpr uint16_t CFWATTOS = 0x43A4;
constexpr uint16_t AFVAROS = 0x43A5;
constexpr uint16_t BFVAROS = 0x43A6;
constexpr uint16_t CFVAROS = 0x43A7;
constexpr uint16_t AFIRMSOS = 0x43A8;
constexpr uint16_t BFIRMSOS = 0x43A9;
constexpr uint16_t CFIRMSOS = 0x43AA;
constexpr uint16_t AFVRMSOS = 0x43AB;
constexpr uint16_t BFVRMSOS = 0x43AC;
constexpr uint16_t CFVRMSOS = 0x43AD;
constexpr uint16_t HXWATTOS = 0x43AE;
constexpr uint16_t HYWATTOS = 0x43AF;
constexpr uint16_t HZWATTOS = 0x43B0;
constexpr uint16_t HXVAROS = 0x43B1;
constexpr uint16_t HYVAROS = 0x43B2;
constexpr uint16_t HZVAROS = 0x43B3;
constexpr uint16_t HXIRMSOS = 0x43B4;
constexpr uint16_t HYIRMSOS = 0x43B5;
constexpr uint16_t HZIRMSOS = 0x43B6;
constexpr uint16_t HXVRMSOS = 0x43B7;
constexpr uint16_t HYVRMSOS = 0x43B8;
constexpr uint16_t HZVRMSOS = 0x43B9;
constexpr uint16_t AIRMS = 0x43C0;
constexpr uint16_t AVRMS = 0x43C1;
constexpr uint16_t BIRMS = 0x43C2;
constexpr uint16_t BVRMS = 0x43C3;
constexpr uint16_t CIRMS = 0x43C4;
constexpr uint16_t CVRMS = 0x43C5;
constexpr uint16_t NIRMS = 0x43C6;
constexpr uint16_t ISUM = 0x43C7;
// Internal DSP Memory RAM registers
constexpr uint16_t RUN = 0xE228;
constexpr uint16_t AWATTHR = 0xE400;
constexpr uint16_t BWATTHR = 0xE401;
constexpr uint16_t CWATTHR = 0xE402;
constexpr uint16_t AFWATTHR = 0xE403;
constexpr uint16_t BFWATTHR = 0xE404;
constexpr uint16_t CFWATTHR = 0xE405;
constexpr uint16_t AFVARHR = 0xE409;
constexpr uint16_t BFVARHR = 0xE40A;
constexpr uint16_t CFVARHR = 0xE40B;
constexpr uint16_t AVAHR = 0xE40C;
constexpr uint16_t BVAHR = 0xE40D;
constexpr uint16_t CVAHR = 0xE40E;
constexpr uint16_t IPEAK = 0xE500;
constexpr uint16_t VPEAK = 0xE501;
constexpr uint16_t STATUS0 = 0xE502;
constexpr uint16_t STATUS1 = 0xE503;
constexpr uint16_t AIMAV = 0xE504;
constexpr uint16_t BIMAV = 0xE505;
constexpr uint16_t CIMAV = 0xE506;
constexpr uint16_t OILVL = 0xE507;
constexpr uint16_t OVLVL = 0xE508;
constexpr uint16_t SAGLVL = 0xE509;
constexpr uint16_t MASK0 = 0xE50A;
constexpr uint16_t MASK1 = 0xE50B;
constexpr uint16_t IAWV = 0xE50C;
constexpr uint16_t IBWV = 0xE50D;
constexpr uint16_t ICWV = 0xE50E;
constexpr uint16_t INWV = 0xE50F;
constexpr uint16_t VAWV = 0xE510;
constexpr uint16_t VBWV = 0xE511;
constexpr uint16_t VCWV = 0xE512;
constexpr uint16_t AWATT = 0xE513;
constexpr uint16_t BWATT = 0xE514;
constexpr uint16_t CWATT = 0xE515;
constexpr uint16_t AFVAR = 0xE516;
constexpr uint16_t BFVAR = 0xE517;
constexpr uint16_t CFVAR = 0xE518;
constexpr uint16_t AVA = 0xE519;
constexpr uint16_t BVA = 0xE51A;
constexpr uint16_t CVA = 0xE51B;
constexpr uint16_t CHECKSUM = 0xE51F;
constexpr uint16_t VNOM = 0xE520;
constexpr uint16_t LAST_RWDATA_24BIT = 0xE5FF;
constexpr uint16_t PHSTATUS = 0xE600;
constexpr uint16_t ANGLE0 = 0xE601;
constexpr uint16_t ANGLE1 = 0xE602;
constexpr uint16_t ANGLE2 = 0xE603;
constexpr uint16_t PHNOLOAD = 0xE608;
constexpr uint16_t LINECYC = 0xE60C;
constexpr uint16_t ZXTOUT = 0xE60D;
constexpr uint16_t COMPMODE = 0xE60E;
constexpr uint16_t GAIN = 0xE60F;
constexpr uint16_t CFMODE = 0xE610;
constexpr uint16_t CF1DEN = 0xE611;
constexpr uint16_t CF2DEN = 0xE612;
constexpr uint16_t CF3DEN = 0xE613;
constexpr uint16_t APHCAL = 0xE614;
constexpr uint16_t BPHCAL = 0xE615;
constexpr uint16_t CPHCAL = 0xE616;
constexpr uint16_t PHSIGN = 0xE617;
constexpr uint16_t CONFIG = 0xE618;
constexpr uint16_t MMODE = 0xE700;
constexpr uint16_t ACCMODE = 0xE701;
constexpr uint16_t LCYCMODE = 0xE702;
constexpr uint16_t PEAKCYC = 0xE703;
constexpr uint16_t SAGCYC = 0xE704;
constexpr uint16_t CFCYC = 0xE705;
constexpr uint16_t HSDC_CFG = 0xE706;
constexpr uint16_t VERSION = 0xE707;
constexpr uint16_t DSPWP_SET = 0xE7E3;
constexpr uint16_t LAST_RWDATA_8BIT = 0xE7FD;
constexpr uint16_t DSPWP_SEL = 0xE7FE;
constexpr uint16_t FVRMS = 0xE880;
constexpr uint16_t FIRMS = 0xE881;
constexpr uint16_t FWATT = 0xE882;
constexpr uint16_t FVAR = 0xE883;
constexpr uint16_t FVA = 0xE884;
constexpr uint16_t FPF = 0xE885;
constexpr uint16_t VTHDN = 0xE886;
constexpr uint16_t ITHDN = 0xE887;
constexpr uint16_t HXVRMS = 0xE888;
constexpr uint16_t HXIRMS = 0xE889;
constexpr uint16_t HXWATT = 0xE88A;
constexpr uint16_t HXVAR = 0xE88B;
constexpr uint16_t HXVA = 0xE88C;
constexpr uint16_t HXPF = 0xE88D;
constexpr uint16_t HXVHD = 0xE88E;
constexpr uint16_t HXIHD = 0xE88F;
constexpr uint16_t HYVRMS = 0xE890;
constexpr uint16_t HYIRMS = 0xE891;
constexpr uint16_t HYWATT = 0xE892;
constexpr uint16_t HYVAR = 0xE893;
constexpr uint16_t HYVA = 0xE894;
constexpr uint16_t HYPF = 0xE895;
constexpr uint16_t HYVHD = 0xE896;
constexpr uint16_t HYIHD = 0xE897;
constexpr uint16_t HZVRMS = 0xE898;
constexpr uint16_t HZIRMS = 0xE899;
constexpr uint16_t HZWATT = 0xE89A;
constexpr uint16_t HZVAR = 0xE89B;
constexpr uint16_t HZVA = 0xE89C;
constexpr uint16_t HZPF = 0xE89D;
constexpr uint16_t HZVHD = 0xE89E;
constexpr uint16_t HZIHD = 0xE89F;
constexpr uint16_t HCONFIG = 0xE900;
constexpr uint16_t APF = 0xE902;
constexpr uint16_t BPF = 0xE903;
constexpr uint16_t CPF = 0xE904;
constexpr uint16_t APERIOD = 0xE905;
constexpr uint16_t BPERIOD = 0xE906;
constexpr uint16_t CPERIOD = 0xE907;
constexpr uint16_t APNOLOAD = 0xE908;
constexpr uint16_t VARNOLOAD = 0xE909;
constexpr uint16_t VANOLOAD = 0xE90A;
constexpr uint16_t LAST_ADD = 0xE9FE;
constexpr uint16_t LAST_RWDATA_16BIT = 0xE9FF;
constexpr uint16_t CONFIG3 = 0xEA00;
constexpr uint16_t LAST_OP = 0xEA01;
constexpr uint16_t WTHR = 0xEA02;
constexpr uint16_t VARTHR = 0xEA03;
constexpr uint16_t VATHR = 0xEA04;
constexpr uint16_t HX_REG = 0xEA08;
constexpr uint16_t HY_REG = 0xEA09;
constexpr uint16_t HZ_REG = 0xEA0A;
constexpr uint16_t LPOILVL = 0xEC00;
constexpr uint16_t CONFIG2 = 0xEC01;
// STATUS1 Register Bits
constexpr uint32_t STATUS1_RSTDONE = (1 << 15);
// CONFIG Register Bits
constexpr uint16_t CONFIG_SWRST = (1 << 7);
// CONFIG2 Register Bits
constexpr uint8_t CONFIG2_I2C_LOCK = (1 << 1);
// COMPMODE Register Bits
constexpr uint16_t COMPMODE_DEFAULT = 0x01FF;
constexpr uint16_t COMPMODE_SELFREQ = (1 << 14);
// RUN Register Bits
constexpr uint16_t RUN_ENABLE = (1 << 0);
// DSPWP_SET Register Bits
constexpr uint8_t DSPWP_SET_RO = (1 << 7);
// DSPWP_SEL Register Bits
constexpr uint8_t DSPWP_SEL_SET = 0xAD;
} // namespace ade7880
} // namespace esphome

View File

@ -0,0 +1,290 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor, i2c
from esphome import pins
from esphome.const import (
CONF_ACTIVE_POWER,
CONF_APPARENT_POWER,
CONF_CALIBRATION,
CONF_CURRENT,
CONF_FORWARD_ACTIVE_ENERGY,
CONF_FREQUENCY,
CONF_ID,
CONF_NAME,
CONF_PHASE_A,
CONF_PHASE_ANGLE,
CONF_PHASE_B,
CONF_PHASE_C,
CONF_POWER_FACTOR,
CONF_RESET_PIN,
CONF_REVERSE_ACTIVE_ENERGY,
CONF_VOLTAGE,
DEVICE_CLASS_APPARENT_POWER,
DEVICE_CLASS_CURRENT,
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_POWER,
DEVICE_CLASS_POWER_FACTOR,
DEVICE_CLASS_VOLTAGE,
STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL_INCREASING,
UNIT_AMPERE,
UNIT_PERCENT,
UNIT_VOLT,
UNIT_VOLT_AMPS,
UNIT_VOLT_AMPS_REACTIVE_HOURS,
UNIT_WATT,
UNIT_WATT_HOURS,
)
DEPENDENCIES = ["i2c"]
ade7880_ns = cg.esphome_ns.namespace("ade7880")
ADE7880 = ade7880_ns.class_("ADE7880", cg.PollingComponent, i2c.I2CDevice)
NeutralChannel = ade7880_ns.struct("NeutralChannel")
PowerChannel = ade7880_ns.struct("PowerChannel")
CONF_CURRENT_GAIN = "current_gain"
CONF_IRQ0_PIN = "irq0_pin"
CONF_IRQ1_PIN = "irq1_pin"
CONF_POWER_GAIN = "power_gain"
CONF_VOLTAGE_GAIN = "voltage_gain"
CONF_NEUTRAL = "neutral"
NEUTRAL_CHANNEL_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(NeutralChannel),
cv.Optional(CONF_NAME): cv.string_strict,
cv.Required(CONF_CURRENT): cv.maybe_simple_value(
sensor.sensor_schema(
unit_of_measurement=UNIT_AMPERE,
accuracy_decimals=2,
device_class=DEVICE_CLASS_CURRENT,
state_class=STATE_CLASS_MEASUREMENT,
),
key=CONF_NAME,
),
cv.Required(CONF_CALIBRATION): cv.Schema(
{
cv.Required(CONF_CURRENT_GAIN): cv.int_,
},
),
}
)
POWER_CHANNEL_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(PowerChannel),
cv.Optional(CONF_NAME): cv.string_strict,
cv.Optional(CONF_VOLTAGE): cv.maybe_simple_value(
sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT,
accuracy_decimals=1,
device_class=DEVICE_CLASS_VOLTAGE,
state_class=STATE_CLASS_MEASUREMENT,
),
key=CONF_NAME,
),
cv.Optional(CONF_CURRENT): cv.maybe_simple_value(
sensor.sensor_schema(
unit_of_measurement=UNIT_AMPERE,
accuracy_decimals=2,
device_class=DEVICE_CLASS_CURRENT,
state_class=STATE_CLASS_MEASUREMENT,
),
key=CONF_NAME,
),
cv.Optional(CONF_ACTIVE_POWER): cv.maybe_simple_value(
sensor.sensor_schema(
unit_of_measurement=UNIT_WATT,
accuracy_decimals=1,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
key=CONF_NAME,
),
cv.Optional(CONF_APPARENT_POWER): cv.maybe_simple_value(
sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT_AMPS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_APPARENT_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
key=CONF_NAME,
),
cv.Optional(CONF_POWER_FACTOR): cv.maybe_simple_value(
sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
accuracy_decimals=0,
device_class=DEVICE_CLASS_POWER_FACTOR,
state_class=STATE_CLASS_MEASUREMENT,
),
key=CONF_NAME,
),
cv.Optional(CONF_FORWARD_ACTIVE_ENERGY): cv.maybe_simple_value(
sensor.sensor_schema(
unit_of_measurement=UNIT_WATT_HOURS,
accuracy_decimals=2,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
key=CONF_NAME,
),
cv.Optional(CONF_REVERSE_ACTIVE_ENERGY): cv.maybe_simple_value(
sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE_HOURS,
accuracy_decimals=2,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
key=CONF_NAME,
),
cv.Required(CONF_CALIBRATION): cv.Schema(
{
cv.Required(CONF_CURRENT_GAIN): cv.int_,
cv.Required(CONF_VOLTAGE_GAIN): cv.int_,
cv.Required(CONF_POWER_GAIN): cv.int_,
cv.Required(CONF_PHASE_ANGLE): cv.int_,
},
),
}
)
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(ADE7880),
cv.Optional(CONF_FREQUENCY, default="50Hz"): cv.All(
cv.frequency, cv.Range(min=45.0, max=66.0)
),
cv.Optional(CONF_IRQ0_PIN): pins.internal_gpio_input_pin_schema,
cv.Required(CONF_IRQ1_PIN): pins.internal_gpio_input_pin_schema,
cv.Optional(CONF_RESET_PIN): pins.internal_gpio_output_pin_schema,
cv.Optional(CONF_PHASE_A): POWER_CHANNEL_SCHEMA,
cv.Optional(CONF_PHASE_B): POWER_CHANNEL_SCHEMA,
cv.Optional(CONF_PHASE_C): POWER_CHANNEL_SCHEMA,
cv.Optional(CONF_NEUTRAL): NEUTRAL_CHANNEL_SCHEMA,
}
)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x38))
)
async def neutral_channel(config):
var = cg.new_Pvariable(config[CONF_ID])
current = config[CONF_CURRENT]
sens = await sensor.new_sensor(current)
cg.add(var.set_current(sens))
cg.add(
var.set_current_gain_calibration(config[CONF_CALIBRATION][CONF_CURRENT_GAIN])
)
return var
async def power_channel(config):
var = cg.new_Pvariable(config[CONF_ID])
for sensor_type in [
CONF_CURRENT,
CONF_VOLTAGE,
CONF_ACTIVE_POWER,
CONF_APPARENT_POWER,
CONF_POWER_FACTOR,
CONF_FORWARD_ACTIVE_ENERGY,
CONF_REVERSE_ACTIVE_ENERGY,
]:
if conf := config.get(sensor_type):
sens = await sensor.new_sensor(conf)
cg.add(getattr(var, f"set_{sensor_type}")(sens))
for calib_type in [
CONF_CURRENT_GAIN,
CONF_VOLTAGE_GAIN,
CONF_POWER_GAIN,
CONF_PHASE_ANGLE,
]:
cg.add(
getattr(var, f"set_{calib_type}_calibration")(
config[CONF_CALIBRATION][calib_type]
)
)
return var
def final_validate(config):
for channel in [CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C]:
if channel := config.get(channel):
channel_name = channel.get(CONF_NAME)
for sensor_type in [
CONF_CURRENT,
CONF_VOLTAGE,
CONF_ACTIVE_POWER,
CONF_APPARENT_POWER,
CONF_POWER_FACTOR,
CONF_FORWARD_ACTIVE_ENERGY,
CONF_REVERSE_ACTIVE_ENERGY,
]:
if conf := channel.get(sensor_type):
sensor_name = conf.get(CONF_NAME)
if (
sensor_name
and channel_name
and not sensor_name.startswith(channel_name)
):
conf[CONF_NAME] = f"{channel_name} {sensor_name}"
if channel := config.get(CONF_NEUTRAL):
channel_name = channel.get(CONF_NAME)
if conf := channel.get(CONF_CURRENT):
sensor_name = conf.get(CONF_NAME)
if (
sensor_name
and channel_name
and not sensor_name.startswith(channel_name)
):
conf[CONF_NAME] = f"{channel_name} {sensor_name}"
FINAL_VALIDATE_SCHEMA = final_validate
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
if irq0_pin := config.get(CONF_IRQ0_PIN):
pin = await cg.gpio_pin_expression(irq0_pin)
cg.add(var.set_irq0_pin(pin))
pin = await cg.gpio_pin_expression(config[CONF_IRQ1_PIN])
cg.add(var.set_irq1_pin(pin))
if reset_pin := config.get(CONF_RESET_PIN):
pin = await cg.gpio_pin_expression(reset_pin)
cg.add(var.set_reset_pin(pin))
if frequency := config.get(CONF_FREQUENCY):
cg.add(var.set_frequency(frequency))
if channel := config.get(CONF_PHASE_A):
chan = await power_channel(channel)
cg.add(var.set_channel_a(chan))
if channel := config.get(CONF_PHASE_B):
chan = await power_channel(channel)
cg.add(var.set_channel_b(chan))
if channel := config.get(CONF_PHASE_C):
chan = await power_channel(channel)
cg.add(var.set_channel_c(chan))
if channel := config.get(CONF_NEUTRAL):
chan = await neutral_channel(channel)
cg.add(var.set_channel_n(chan))

View File

@ -21,30 +21,39 @@ namespace esphome {
namespace aht10 {
static const char *const TAG = "aht10";
static const size_t SIZE_CALIBRATE_CMD = 3;
static const uint8_t AHT10_CALIBRATE_CMD[] = {0xE1, 0x08, 0x00};
static const uint8_t AHT20_CALIBRATE_CMD[] = {0xBE, 0x08, 0x00};
static const uint8_t AHT10_INITIALIZE_CMD[] = {0xE1, 0x08, 0x00};
static const uint8_t AHT20_INITIALIZE_CMD[] = {0xBE, 0x08, 0x00};
static const uint8_t AHT10_MEASURE_CMD[] = {0xAC, 0x33, 0x00};
static const uint8_t AHT10_DEFAULT_DELAY = 5; // ms, for calibration and temperature measurement
static const uint8_t AHT10_HUMIDITY_DELAY = 30; // ms
static const uint8_t AHT10_ATTEMPTS = 3; // safety margin, normally 3 attempts are enough: 3*30=90ms
static const uint8_t AHT10_CAL_ATTEMPTS = 10;
static const uint8_t AHT10_SOFTRESET_CMD[] = {0xBA};
static const uint8_t AHT10_DEFAULT_DELAY = 5; // ms, for initialization and temperature measurement
static const uint8_t AHT10_HUMIDITY_DELAY = 30; // ms
static const uint8_t AHT10_SOFTRESET_DELAY = 30; // ms
static const uint8_t AHT10_ATTEMPTS = 3; // safety margin, normally 3 attempts are enough: 3*30=90ms
static const uint8_t AHT10_INIT_ATTEMPTS = 10;
static const uint8_t AHT10_STATUS_BUSY = 0x80;
void AHT10Component::setup() {
const uint8_t *calibrate_cmd;
if (this->write(AHT10_SOFTRESET_CMD, sizeof(AHT10_SOFTRESET_CMD)) != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Reset AHT10 failed!");
}
delay(AHT10_SOFTRESET_DELAY);
const uint8_t *init_cmd;
switch (this->variant_) {
case AHT10Variant::AHT20:
calibrate_cmd = AHT20_CALIBRATE_CMD;
init_cmd = AHT20_INITIALIZE_CMD;
ESP_LOGCONFIG(TAG, "Setting up AHT20");
break;
case AHT10Variant::AHT10:
default:
calibrate_cmd = AHT10_CALIBRATE_CMD;
init_cmd = AHT10_INITIALIZE_CMD;
ESP_LOGCONFIG(TAG, "Setting up AHT10");
}
if (this->write(calibrate_cmd, SIZE_CALIBRATE_CMD) != i2c::ERROR_OK) {
if (this->write(init_cmd, sizeof(init_cmd)) != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Communication with AHT10 failed!");
this->mark_failed();
return;
@ -59,19 +68,19 @@ void AHT10Component::setup() {
return;
}
++cal_attempts;
if (cal_attempts > AHT10_CAL_ATTEMPTS) {
ESP_LOGE(TAG, "AHT10 calibration timed out!");
if (cal_attempts > AHT10_INIT_ATTEMPTS) {
ESP_LOGE(TAG, "AHT10 initialization timed out!");
this->mark_failed();
return;
}
}
if ((data & 0x68) != 0x08) { // Bit[6:5] = 0b00, NORMAL mode and Bit[3] = 0b1, CALIBRATED
ESP_LOGE(TAG, "AHT10 calibration failed!");
ESP_LOGE(TAG, "AHT10 initialization failed!");
this->mark_failed();
return;
}
ESP_LOGV(TAG, "AHT10 calibrated");
ESP_LOGV(TAG, "AHT10 initialization");
}
void AHT10Component::update() {

View File

@ -600,6 +600,7 @@ message ListEntitiesTextSensorResponse {
string icon = 5;
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
string device_class = 8;
}
message TextSensorStateResponse {
option (id) = 27;
@ -1449,6 +1450,7 @@ message VoiceAssistantRequest {
string conversation_id = 2;
uint32 flags = 3;
VoiceAssistantAudioSettings audio_settings = 4;
string wake_word_phrase = 5;
}
message VoiceAssistantResponse {

View File

@ -543,6 +543,7 @@ bool APIConnection::send_text_sensor_info(text_sensor::TextSensor *text_sensor)
msg.icon = text_sensor->get_icon();
msg.disabled_by_default = text_sensor->is_disabled_by_default();
msg.entity_category = static_cast<enums::EntityCategory>(text_sensor->get_entity_category());
msg.device_class = text_sensor->get_device_class();
return this->send_list_entities_text_sensor_response(msg);
}
#endif

View File

@ -2721,6 +2721,10 @@ bool ListEntitiesTextSensorResponse::decode_length(uint32_t field_id, ProtoLengt
this->icon = value.as_string();
return true;
}
case 8: {
this->device_class = value.as_string();
return true;
}
default:
return false;
}
@ -2743,6 +2747,7 @@ void ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(5, this->icon);
buffer.encode_bool(6, this->disabled_by_default);
buffer.encode_enum<enums::EntityCategory>(7, this->entity_category);
buffer.encode_string(8, this->device_class);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void ListEntitiesTextSensorResponse::dump_to(std::string &out) const {
@ -2776,6 +2781,10 @@ void ListEntitiesTextSensorResponse::dump_to(std::string &out) const {
out.append(" entity_category: ");
out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category));
out.append("\n");
out.append(" device_class: ");
out.append("'").append(this->device_class).append("'");
out.append("\n");
out.append("}");
}
#endif
@ -6594,6 +6603,10 @@ bool VoiceAssistantRequest::decode_length(uint32_t field_id, ProtoLengthDelimite
this->audio_settings = value.as_message<VoiceAssistantAudioSettings>();
return true;
}
case 5: {
this->wake_word_phrase = value.as_string();
return true;
}
default:
return false;
}
@ -6603,6 +6616,7 @@ void VoiceAssistantRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(2, this->conversation_id);
buffer.encode_uint32(3, this->flags);
buffer.encode_message<VoiceAssistantAudioSettings>(4, this->audio_settings);
buffer.encode_string(5, this->wake_word_phrase);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void VoiceAssistantRequest::dump_to(std::string &out) const {
@ -6624,6 +6638,10 @@ void VoiceAssistantRequest::dump_to(std::string &out) const {
out.append(" audio_settings: ");
this->audio_settings.dump_to(out);
out.append("\n");
out.append(" wake_word_phrase: ");
out.append("'").append(this->wake_word_phrase).append("'");
out.append("\n");
out.append("}");
}
#endif

View File

@ -713,6 +713,7 @@ class ListEntitiesTextSensorResponse : public ProtoMessage {
std::string icon{};
bool disabled_by_default{false};
enums::EntityCategory entity_category{};
std::string device_class{};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
@ -1701,6 +1702,7 @@ class VoiceAssistantRequest : public ProtoMessage {
std::string conversation_id{};
uint32_t flags{0};
VoiceAssistantAudioSettings audio_settings{};
std::string wake_word_phrase{};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;

View File

@ -22,7 +22,7 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
if CORE.is_esp32 or CORE.is_libretiny:
# https://github.com/esphome/AsyncTCP/blob/master/library.json
cg.add_library("esphome/AsyncTCP-esphome", "2.0.1")
cg.add_library("esphome/AsyncTCP-esphome", "2.1.3")
elif CORE.is_esp8266:
# https://github.com/esphome/ESPAsyncTCP
cg.add_library("esphome/ESPAsyncTCP-esphome", "2.0.0")

View File

@ -4,6 +4,7 @@ from esphome.components import sensor, uart
from esphome.const import (
CONF_CURRENT,
CONF_ENERGY,
CONF_EXTERNAL_TEMPERATURE,
CONF_ID,
CONF_POWER,
CONF_VOLTAGE,
@ -24,7 +25,6 @@ from esphome.const import (
DEPENDENCIES = ["uart"]
CONF_INTERNAL_TEMPERATURE = "internal_temperature"
CONF_EXTERNAL_TEMPERATURE = "external_temperature"
bl0940_ns = cg.esphome_ns.namespace("bl0940")
BL0940 = bl0940_ns.class_("BL0940", cg.PollingComponent, uart.UARTDevice)

View File

@ -11,6 +11,7 @@ MULTI_CONF = True
CONF_BME680_BSEC_ID = "bme680_bsec_id"
CONF_TEMPERATURE_OFFSET = "temperature_offset"
CONF_IAQ_MODE = "iaq_mode"
CONF_SUPPLY_VOLTAGE = "supply_voltage"
CONF_SAMPLE_RATE = "sample_rate"
CONF_STATE_SAVE_INTERVAL = "state_save_interval"
@ -22,6 +23,12 @@ IAQ_MODE_OPTIONS = {
"MOBILE": IAQMode.IAQ_MODE_MOBILE,
}
SupplyVoltage = bme680_bsec_ns.enum("SupplyVoltage")
SUPPLY_VOLTAGE_OPTIONS = {
"1.8V": SupplyVoltage.SUPPLY_VOLTAGE_1V8,
"3.3V": SupplyVoltage.SUPPLY_VOLTAGE_3V3,
}
SampleRate = bme680_bsec_ns.enum("SampleRate")
SAMPLE_RATE_OPTIONS = {
"LP": SampleRate.SAMPLE_RATE_LP,
@ -40,6 +47,9 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_IAQ_MODE, default="STATIC"): cv.enum(
IAQ_MODE_OPTIONS, upper=True
),
cv.Optional(CONF_SUPPLY_VOLTAGE, default="3.3V"): cv.enum(
SUPPLY_VOLTAGE_OPTIONS, upper=True
),
cv.Optional(CONF_SAMPLE_RATE, default="LP"): cv.enum(
SAMPLE_RATE_OPTIONS, upper=True
),
@ -67,6 +77,7 @@ async def to_code(config):
cg.add(var.set_device_id(str(config[CONF_ID])))
cg.add(var.set_temperature_offset(config[CONF_TEMPERATURE_OFFSET]))
cg.add(var.set_iaq_mode(config[CONF_IAQ_MODE]))
cg.add(var.set_supply_voltage(config[CONF_SUPPLY_VOLTAGE]))
cg.add(var.set_sample_rate(config[CONF_SAMPLE_RATE]))
cg.add(
var.set_state_save_interval(config[CONF_STATE_SAVE_INTERVAL].total_milliseconds)

View File

@ -52,17 +52,33 @@ void BME680BSECComponent::setup() {
void BME680BSECComponent::set_config_() {
if (this->sample_rate_ == SAMPLE_RATE_ULP) {
const uint8_t config[] = {
if (this->supply_voltage_ == SUPPLY_VOLTAGE_3V3) {
const uint8_t config[] = {
#include "config/generic_33v_300s_28d/bsec_iaq.txt"
};
this->bsec_status_ =
bsec_set_configuration(config, BSEC_MAX_PROPERTY_BLOB_SIZE, this->work_buffer_, sizeof(this->work_buffer_));
} else {
const uint8_t config[] = {
};
this->bsec_status_ =
bsec_set_configuration(config, BSEC_MAX_PROPERTY_BLOB_SIZE, this->work_buffer_, sizeof(this->work_buffer_));
} else { // SUPPLY_VOLTAGE_1V8
const uint8_t config[] = {
#include "config/generic_18v_300s_28d/bsec_iaq.txt"
};
this->bsec_status_ =
bsec_set_configuration(config, BSEC_MAX_PROPERTY_BLOB_SIZE, this->work_buffer_, sizeof(this->work_buffer_));
}
} else { // SAMPLE_RATE_LP
if (this->supply_voltage_ == SUPPLY_VOLTAGE_3V3) {
const uint8_t config[] = {
#include "config/generic_33v_3s_28d/bsec_iaq.txt"
};
this->bsec_status_ =
bsec_set_configuration(config, BSEC_MAX_PROPERTY_BLOB_SIZE, this->work_buffer_, sizeof(this->work_buffer_));
};
this->bsec_status_ =
bsec_set_configuration(config, BSEC_MAX_PROPERTY_BLOB_SIZE, this->work_buffer_, sizeof(this->work_buffer_));
} else { // SUPPLY_VOLTAGE_1V8
const uint8_t config[] = {
#include "config/generic_18v_3s_28d/bsec_iaq.txt"
};
this->bsec_status_ =
bsec_set_configuration(config, BSEC_MAX_PROPERTY_BLOB_SIZE, this->work_buffer_, sizeof(this->work_buffer_));
}
}
}
@ -145,6 +161,7 @@ void BME680BSECComponent::dump_config() {
ESP_LOGCONFIG(TAG, " Temperature Offset: %.2f", this->temperature_offset_);
ESP_LOGCONFIG(TAG, " IAQ Mode: %s", this->iaq_mode_ == IAQ_MODE_STATIC ? "Static" : "Mobile");
ESP_LOGCONFIG(TAG, " Supply Voltage: %sV", this->supply_voltage_ == SUPPLY_VOLTAGE_3V3 ? "3.3" : "1.8");
ESP_LOGCONFIG(TAG, " Sample Rate: %s", BME680_BSEC_SAMPLE_RATE_LOG(this->sample_rate_));
ESP_LOGCONFIG(TAG, " State Save Interval: %ims", this->state_save_interval_ms_);

View File

@ -21,6 +21,11 @@ enum IAQMode {
IAQ_MODE_MOBILE = 1,
};
enum SupplyVoltage {
SUPPLY_VOLTAGE_3V3 = 0,
SUPPLY_VOLTAGE_1V8 = 1,
};
enum SampleRate {
SAMPLE_RATE_LP = 0,
SAMPLE_RATE_ULP = 1,
@ -35,6 +40,7 @@ class BME680BSECComponent : public Component, public i2c::I2CDevice {
void set_temperature_offset(float offset) { this->temperature_offset_ = offset; }
void set_iaq_mode(IAQMode iaq_mode) { this->iaq_mode_ = iaq_mode; }
void set_state_save_interval(uint32_t interval) { this->state_save_interval_ms_ = interval; }
void set_supply_voltage(SupplyVoltage supply_voltage) { this->supply_voltage_ = supply_voltage; }
void set_sample_rate(SampleRate sample_rate) { this->sample_rate_ = sample_rate; }
void set_temperature_sample_rate(SampleRate sample_rate) { this->temperature_sample_rate_ = sample_rate; }
@ -109,6 +115,7 @@ class BME680BSECComponent : public Component, public i2c::I2CDevice {
std::string device_id_;
float temperature_offset_{0};
IAQMode iaq_mode_{IAQ_MODE_STATIC};
SupplyVoltage supply_voltage_;
SampleRate sample_rate_{SAMPLE_RATE_LP}; // Core/gas sample rate
SampleRate temperature_sample_rate_{SAMPLE_RATE_DEFAULT};

View File

@ -1,6 +1,8 @@
#include "cse7766.h"
#include "esphome/core/log.h"
#include <cinttypes>
#include <iomanip>
#include <sstream>
namespace esphome {
namespace cse7766 {
@ -68,20 +70,26 @@ bool CSE7766Component::check_byte_() {
return true;
}
void CSE7766Component::parse_data_() {
ESP_LOGVV(TAG, "CSE7766 Data: ");
for (uint8_t i = 0; i < 23; i++) {
ESP_LOGVV(TAG, " %u: 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", i + 1, BYTE_TO_BINARY(this->raw_data_[i]),
this->raw_data_[i]);
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
{
std::stringstream ss;
ss << "Raw data:" << std::hex << std::uppercase << std::setfill('0');
for (uint8_t i = 0; i < 23; i++) {
ss << ' ' << std::setw(2) << static_cast<unsigned>(this->raw_data_[i]);
}
ESP_LOGVV(TAG, "%s", ss.str().c_str());
}
#endif
// Parse header
uint8_t header1 = this->raw_data_[0];
if (header1 == 0xAA) {
ESP_LOGE(TAG, "CSE7766 not calibrated!");
return;
}
bool power_cycle_exceeds_range = false;
if ((header1 & 0xF0) == 0xF0) {
if (header1 & 0xD) {
ESP_LOGE(TAG, "CSE7766 reports abnormal external circuit or chip damage: (0x%02X)", header1);
@ -94,74 +102,122 @@ void CSE7766Component::parse_data_() {
if (header1 & (1 << 0)) {
ESP_LOGE(TAG, " Coefficient storage area is abnormal.");
}
// Datasheet: voltage or current cycle exceeding range means invalid values
return;
}
power_cycle_exceeds_range = header1 & (1 << 1);
}
uint32_t voltage_calib = this->get_24_bit_uint_(2);
// Parse data frame
uint32_t voltage_coeff = this->get_24_bit_uint_(2);
uint32_t voltage_cycle = this->get_24_bit_uint_(5);
uint32_t current_calib = this->get_24_bit_uint_(8);
uint32_t current_coeff = this->get_24_bit_uint_(8);
uint32_t current_cycle = this->get_24_bit_uint_(11);
uint32_t power_calib = this->get_24_bit_uint_(14);
uint32_t power_coeff = this->get_24_bit_uint_(14);
uint32_t power_cycle = this->get_24_bit_uint_(17);
uint8_t adj = this->raw_data_[20];
uint32_t cf_pulses = (this->raw_data_[21] << 8) + this->raw_data_[22];
bool have_voltage = adj & 0x40;
if (have_voltage) {
// voltage cycle of serial port outputted is a complete cycle;
float voltage = voltage_calib / float(voltage_cycle);
if (this->voltage_sensor_ != nullptr)
this->voltage_sensor_->publish_state(voltage);
}
uint16_t cf_pulses = (this->raw_data_[21] << 8) + this->raw_data_[22];
bool have_power = adj & 0x10;
float power = 0.0f;
bool have_current = adj & 0x20;
bool have_voltage = adj & 0x40;
if (have_power) {
// power cycle of serial port outputted is a complete cycle;
// According to the user manual, power cycle exceeding range means the measured power is 0
if (!power_cycle_exceeds_range) {
power = power_calib / float(power_cycle);
float voltage = 0.0f;
if (have_voltage) {
voltage = voltage_coeff / float(voltage_cycle);
if (this->voltage_sensor_ != nullptr) {
this->voltage_sensor_->publish_state(voltage);
}
if (this->power_sensor_ != nullptr)
this->power_sensor_->publish_state(power);
}
uint32_t difference;
if (this->cf_pulses_last_ == 0) {
float energy = 0.0;
if (this->energy_sensor_ != nullptr) {
if (this->cf_pulses_last_ == 0 && !this->energy_sensor_->has_state()) {
this->cf_pulses_last_ = cf_pulses;
}
if (cf_pulses < this->cf_pulses_last_) {
difference = cf_pulses + (0x10000 - this->cf_pulses_last_);
} else {
difference = cf_pulses - this->cf_pulses_last_;
}
uint16_t cf_diff = cf_pulses - this->cf_pulses_last_;
this->cf_pulses_total_ += cf_diff;
this->cf_pulses_last_ = cf_pulses;
this->energy_total_ += difference * float(power_calib) / 1000000.0f / 3600.0f;
if (this->energy_sensor_ != nullptr)
this->energy_sensor_->publish_state(this->energy_total_);
} else if ((this->energy_sensor_ != nullptr) && !this->energy_sensor_->has_state()) {
this->energy_sensor_->publish_state(0);
energy = this->cf_pulses_total_ * float(power_coeff) / 1000000.0f / 3600.0f;
this->energy_sensor_->publish_state(energy);
}
if (adj & 0x20) {
// indicates current cycle of serial port outputted is a complete cycle;
float current = 0.0f;
if (have_voltage && !have_power) {
// Testing has shown that when we have voltage and current but not power, that means the power is 0.
// We report a power of 0, which in turn means we should report a current of 0.
if (this->power_sensor_ != nullptr)
this->power_sensor_->publish_state(0);
} else if (power != 0.0f) {
current = current_calib / float(current_cycle);
float power = 0.0f;
if (power_cycle_exceeds_range) {
// Datasheet: power cycle exceeding range means active power is 0
if (this->power_sensor_ != nullptr) {
this->power_sensor_->publish_state(0.0f);
}
} else if (have_power) {
power = power_coeff / float(power_cycle);
if (this->power_sensor_ != nullptr) {
this->power_sensor_->publish_state(power);
}
if (this->current_sensor_ != nullptr)
this->current_sensor_->publish_state(current);
}
float current = 0.0f;
float calculated_current = 0.0f;
if (have_current) {
// Assumption: if we don't have power measurement, then current is likely below 50mA
if (have_power && voltage > 1.0f) {
calculated_current = power / voltage;
}
// Datasheet: minimum measured current is 50mA
if (calculated_current > 0.05f) {
current = current_coeff / float(current_cycle);
}
if (this->current_sensor_ != nullptr) {
this->current_sensor_->publish_state(current);
}
}
if (have_voltage && have_current) {
const float apparent_power = voltage * current;
if (this->apparent_power_sensor_ != nullptr) {
this->apparent_power_sensor_->publish_state(apparent_power);
}
if (this->power_factor_sensor_ != nullptr && (have_power || power_cycle_exceeds_range)) {
float pf = NAN;
if (apparent_power > 0) {
pf = power / apparent_power;
if (pf < 0 || pf > 1) {
ESP_LOGD(TAG, "Impossible power factor: %.4f not in interval [0, 1]", pf);
pf = NAN;
}
} else if (apparent_power == 0 && power == 0) {
// No load, report ideal power factor
pf = 1.0f;
} else if (current == 0 && calculated_current <= 0.05f) {
// Datasheet: minimum measured current is 50mA
ESP_LOGV(TAG, "Can't calculate power factor (current below minimum for CSE7766)");
} else {
ESP_LOGW(TAG, "Can't calculate power factor from P = %.4f W, S = %.4f VA", power, apparent_power);
}
this->power_factor_sensor_->publish_state(pf);
}
}
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
{
std::stringstream ss;
ss << "Parsed:";
if (have_voltage) {
ss << " V=" << voltage << "V";
}
if (have_current) {
ss << " I=" << current * 1000.0f << "mA (~" << calculated_current * 1000.0f << "mA)";
}
if (have_power) {
ss << " P=" << power << "W";
}
if (energy != 0.0f) {
ss << " E=" << energy << "kWh (" << cf_pulses << ")";
}
ESP_LOGVV(TAG, "%s", ss.str().c_str());
}
#endif
}
uint32_t CSE7766Component::get_24_bit_uint_(uint8_t start_index) {
@ -175,6 +231,8 @@ void CSE7766Component::dump_config() {
LOG_SENSOR(" ", "Current", this->current_sensor_);
LOG_SENSOR(" ", "Power", this->power_sensor_);
LOG_SENSOR(" ", "Energy", this->energy_sensor_);
LOG_SENSOR(" ", "Apparent Power", this->apparent_power_sensor_);
LOG_SENSOR(" ", "Power Factor", this->power_factor_sensor_);
this->check_uart_settings(4800);
}

View File

@ -13,6 +13,10 @@ class CSE7766Component : public Component, public uart::UARTDevice {
void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; }
void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; }
void set_energy_sensor(sensor::Sensor *energy_sensor) { energy_sensor_ = energy_sensor; }
void set_apparent_power_sensor(sensor::Sensor *apparent_power_sensor) {
apparent_power_sensor_ = apparent_power_sensor;
}
void set_power_factor_sensor(sensor::Sensor *power_factor_sensor) { power_factor_sensor_ = power_factor_sensor; }
void loop() override;
float get_setup_priority() const override;
@ -30,8 +34,10 @@ class CSE7766Component : public Component, public uart::UARTDevice {
sensor::Sensor *current_sensor_{nullptr};
sensor::Sensor *power_sensor_{nullptr};
sensor::Sensor *energy_sensor_{nullptr};
float energy_total_{0.0f};
uint32_t cf_pulses_last_{0};
sensor::Sensor *apparent_power_sensor_{nullptr};
sensor::Sensor *power_factor_sensor_{nullptr};
uint32_t cf_pulses_total_{0};
uint16_t cf_pulses_last_{0};
};
} // namespace cse7766

View File

@ -2,19 +2,24 @@ import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor, uart
from esphome.const import (
CONF_APPARENT_POWER,
CONF_CURRENT,
CONF_ENERGY,
CONF_ID,
CONF_POWER,
CONF_POWER_FACTOR,
CONF_VOLTAGE,
DEVICE_CLASS_APPARENT_POWER,
DEVICE_CLASS_CURRENT,
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_POWER,
DEVICE_CLASS_POWER_FACTOR,
DEVICE_CLASS_VOLTAGE,
STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL_INCREASING,
UNIT_VOLT,
UNIT_AMPERE,
UNIT_VOLT,
UNIT_VOLT_AMPS,
UNIT_WATT,
UNIT_WATT_HOURS,
)
@ -51,6 +56,17 @@ CONFIG_SCHEMA = cv.Schema(
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional(CONF_APPARENT_POWER): sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT_AMPS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_APPARENT_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema(
accuracy_decimals=2,
device_class=DEVICE_CLASS_POWER_FACTOR,
state_class=STATE_CLASS_MEASUREMENT,
),
}
).extend(uart.UART_DEVICE_SCHEMA)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
@ -75,3 +91,9 @@ async def to_code(config):
if energy_config := config.get(CONF_ENERGY):
sens = await sensor.new_sensor(energy_config)
cg.add(var.set_energy_sensor(sens))
if apparent_power_config := config.get(CONF_APPARENT_POWER):
sens = await sensor.new_sensor(apparent_power_config)
cg.add(var.set_apparent_power_sensor(sens))
if power_factor_config := config.get(CONF_POWER_FACTOR):
sens = await sensor.new_sensor(power_factor_config)
cg.add(var.set_power_factor_sensor(sens))

View File

@ -168,10 +168,6 @@ bool IRAM_ATTR DallasTemperatureSensor::read_scratch_pad() {
if (!wire->reset()) {
return false;
}
}
{
InterruptLock lock;
wire->select(this->address_);
wire->write8(DALLAS_COMMAND_READ_SCRATCH_PAD);

View File

@ -1,7 +1,6 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import automation
from esphome import core
from esphome.automation import maybe_simple_id
from esphome.const import CONF_ID
from esphome.components import uart
@ -101,7 +100,7 @@ def range_segment_list(input):
largest_distance = -1
for distance in input:
if isinstance(distance, core.Lambda):
if isinstance(distance, cv.Lambda):
continue
m = cv.distance(distance)
if m > 9:
@ -128,14 +127,14 @@ MMWAVE_SETTINGS_SCHEMA = cv.Schema(
cv.Optional(CONF_OUTPUT_LATENCY): {
cv.Required(CONF_DELAY_AFTER_DETECT): cv.templatable(
cv.All(
cv.positive_time_period,
cv.Range(max=core.TimePeriod(seconds=1638.375)),
cv.positive_time_period_milliseconds,
cv.Range(max=cv.TimePeriod(seconds=1638.375)),
)
),
cv.Required(CONF_DELAY_AFTER_DISAPPEAR): cv.templatable(
cv.All(
cv.positive_time_period,
cv.Range(max=core.TimePeriod(seconds=1638.375)),
cv.positive_time_period_milliseconds,
cv.Range(max=cv.TimePeriod(seconds=1638.375)),
)
),
},

View File

@ -50,7 +50,7 @@ class DfrobotSen0395SettingsAction : public Action<Ts...>, public Parented<Dfrob
float detect = this->delay_after_detect_.value(x...);
float disappear = this->delay_after_disappear_.value(x...);
if (detect >= 0 && disappear >= 0) {
this->parent_->enqueue(make_unique<OutputLatencyCommand>(detect, disappear));
this->parent_->enqueue(make_unique<SetLatencyCommand>(detect, disappear));
}
}
if (this->start_after_power_on_.has_value()) {

View File

@ -1,5 +1,7 @@
#include "commands.h"
#include <cmath>
#include "esphome/core/log.h"
#include "dfrobot_sen0395.h"
@ -194,32 +196,22 @@ uint8_t DetRangeCfgCommand::on_message(std::string &message) {
return 0; // Command not done yet.
}
OutputLatencyCommand::OutputLatencyCommand(float delay_after_detection, float delay_after_disappear) {
delay_after_detection = round(delay_after_detection / 0.025) * 0.025;
delay_after_disappear = round(delay_after_disappear / 0.025) * 0.025;
if (delay_after_detection < 0)
delay_after_detection = 0;
if (delay_after_detection > 1638.375)
delay_after_detection = 1638.375;
if (delay_after_disappear < 0)
delay_after_disappear = 0;
if (delay_after_disappear > 1638.375)
delay_after_disappear = 1638.375;
this->delay_after_detection_ = delay_after_detection;
this->delay_after_disappear_ = delay_after_disappear;
this->cmd_ = str_sprintf("outputLatency -1 %.0f %.0f", delay_after_detection / 0.025, delay_after_disappear / 0.025);
SetLatencyCommand::SetLatencyCommand(float delay_after_detection, float delay_after_disappear) {
delay_after_detection = std::round(delay_after_detection / 0.025f) * 0.025f;
delay_after_disappear = std::round(delay_after_disappear / 0.025f) * 0.025f;
this->delay_after_detection_ = clamp(delay_after_detection, 0.0f, 1638.375f);
this->delay_after_disappear_ = clamp(delay_after_disappear, 0.0f, 1638.375f);
this->cmd_ = str_sprintf("setLatency %.03f %.03f", this->delay_after_detection_, this->delay_after_disappear_);
};
uint8_t OutputLatencyCommand::on_message(std::string &message) {
uint8_t SetLatencyCommand::on_message(std::string &message) {
if (message == "sensor is not stopped") {
ESP_LOGE(TAG, "Cannot configure output latency. Sensor is not stopped!");
return 1; // Command done
} else if (message == "Done") {
ESP_LOGI(TAG, "Updated output latency config:");
ESP_LOGI(TAG, "Signal that someone was detected is delayed by %.02fs.", this->delay_after_detection_);
ESP_LOGI(TAG, "Signal that nobody is detected anymore is delayed by %.02fs.", this->delay_after_disappear_);
ESP_LOGI(TAG, "Signal that someone was detected is delayed by %.03f s.", this->delay_after_detection_);
ESP_LOGI(TAG, "Signal that nobody is detected anymore is delayed by %.03f s.", this->delay_after_disappear_);
ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str());
return 1; // Command done
}

View File

@ -62,9 +62,9 @@ class DetRangeCfgCommand : public Command {
// TODO: Set min max values in component, so they can be published as sensor.
};
class OutputLatencyCommand : public Command {
class SetLatencyCommand : public Command {
public:
OutputLatencyCommand(float delay_after_detection, float delay_after_disappear);
SetLatencyCommand(float delay_after_detection, float delay_after_disappear);
uint8_t on_message(std::string &message) override;
protected:

View File

@ -257,6 +257,67 @@ void Display::filled_triangle(int x1, int y1, int x2, int y2, int x3, int y3, Co
this->filled_flat_side_triangle_(x3, y3, x2, y2, x_temp, y_temp, color);
}
}
void HOT Display::get_regular_polygon_vertex(int vertex_id, int *vertex_x, int *vertex_y, int center_x, int center_y,
int radius, int edges, RegularPolygonVariation variation,
float rotation_degrees) {
if (edges >= 2) {
// Given the orientation of the display component, an angle is measured clockwise from the x axis.
// For a regular polygon, the human reference would be the top of the polygon,
// hence we rotate the shape by 270° to orient the polygon up.
rotation_degrees += ROTATION_270_DEGREES;
// Convert the rotation to radians, easier to use in trigonometrical calculations
float rotation_radians = rotation_degrees * PI / 180;
// A pointy top variation means the first vertex of the polygon is at the top center of the shape, this requires no
// additional rotation of the shape.
// A flat top variation means the first point of the polygon has to be rotated so that the first edge is horizontal,
// this requires to rotate the shape by π/edges radians counter-clockwise so that the first point is located on the
// left side of the first horizontal edge.
rotation_radians -= (variation == VARIATION_FLAT_TOP) ? PI / edges : 0.0;
float vertex_angle = ((float) vertex_id) / edges * 2 * PI + rotation_radians;
*vertex_x = (int) round(cos(vertex_angle) * radius) + center_x;
*vertex_y = (int) round(sin(vertex_angle) * radius) + center_y;
}
}
void HOT Display::regular_polygon(int x, int y, int radius, int edges, RegularPolygonVariation variation,
float rotation_degrees, Color color, RegularPolygonDrawing drawing) {
if (edges >= 2) {
int previous_vertex_x, previous_vertex_y;
for (int current_vertex_id = 0; current_vertex_id <= edges; current_vertex_id++) {
int current_vertex_x, current_vertex_y;
get_regular_polygon_vertex(current_vertex_id, &current_vertex_x, &current_vertex_y, x, y, radius, edges,
variation, rotation_degrees);
if (current_vertex_id > 0) { // Start drawing after the 2nd vertex coordinates has been calculated
if (drawing == DRAWING_FILLED) {
this->filled_triangle(x, y, previous_vertex_x, previous_vertex_y, current_vertex_x, current_vertex_y, color);
} else if (drawing == DRAWING_OUTLINE) {
this->line(previous_vertex_x, previous_vertex_y, current_vertex_x, current_vertex_y, color);
}
}
previous_vertex_x = current_vertex_x;
previous_vertex_y = current_vertex_y;
}
}
}
void HOT Display::regular_polygon(int x, int y, int radius, int edges, RegularPolygonVariation variation, Color color,
RegularPolygonDrawing drawing) {
regular_polygon(x, y, radius, edges, variation, ROTATION_0_DEGREES, color, drawing);
}
void HOT Display::regular_polygon(int x, int y, int radius, int edges, Color color, RegularPolygonDrawing drawing) {
regular_polygon(x, y, radius, edges, VARIATION_POINTY_TOP, ROTATION_0_DEGREES, color, drawing);
}
void Display::filled_regular_polygon(int x, int y, int radius, int edges, RegularPolygonVariation variation,
float rotation_degrees, Color color) {
regular_polygon(x, y, radius, edges, variation, rotation_degrees, color, DRAWING_FILLED);
}
void Display::filled_regular_polygon(int x, int y, int radius, int edges, RegularPolygonVariation variation,
Color color) {
regular_polygon(x, y, radius, edges, variation, ROTATION_0_DEGREES, color, DRAWING_FILLED);
}
void Display::filled_regular_polygon(int x, int y, int radius, int edges, Color color) {
regular_polygon(x, y, radius, edges, VARIATION_POINTY_TOP, ROTATION_0_DEGREES, color, DRAWING_FILLED);
}
void Display::print(int x, int y, BaseFont *font, Color color, TextAlign align, const char *text) {
int x_start, y_start;

View File

@ -137,6 +137,42 @@ enum DisplayRotation {
DISPLAY_ROTATION_270_DEGREES = 270,
};
#define PI 3.1415926535897932384626433832795
const int EDGES_TRIGON = 3;
const int EDGES_TRIANGLE = 3;
const int EDGES_TETRAGON = 4;
const int EDGES_QUADRILATERAL = 4;
const int EDGES_PENTAGON = 5;
const int EDGES_HEXAGON = 6;
const int EDGES_HEPTAGON = 7;
const int EDGES_OCTAGON = 8;
const int EDGES_NONAGON = 9;
const int EDGES_ENNEAGON = 9;
const int EDGES_DECAGON = 10;
const int EDGES_HENDECAGON = 11;
const int EDGES_DODECAGON = 12;
const int EDGES_TRIDECAGON = 13;
const int EDGES_TETRADECAGON = 14;
const int EDGES_PENTADECAGON = 15;
const int EDGES_HEXADECAGON = 16;
const float ROTATION_0_DEGREES = 0.0;
const float ROTATION_45_DEGREES = 45.0;
const float ROTATION_90_DEGREES = 90.0;
const float ROTATION_180_DEGREES = 180.0;
const float ROTATION_270_DEGREES = 270.0;
enum RegularPolygonVariation {
VARIATION_POINTY_TOP = 0,
VARIATION_FLAT_TOP = 1,
};
enum RegularPolygonDrawing {
DRAWING_OUTLINE = 0,
DRAWING_FILLED = 1,
};
class Display;
class DisplayPage;
class DisplayOnPageChangeTrigger;
@ -175,10 +211,15 @@ class Display : public PollingComponent {
/// Clear the entire screen by filling it with OFF pixels.
void clear();
/// Get the width of the image in pixels with rotation applied.
virtual int get_width() = 0;
/// Get the height of the image in pixels with rotation applied.
virtual int get_height() = 0;
/// Get the calculated width of the display in pixels with rotation applied.
virtual int get_width() { return this->get_width_internal(); }
/// Get the calculated height of the display in pixels with rotation applied.
virtual int get_height() { return this->get_height_internal(); }
/// Get the native (original) width of the display in pixels.
int get_native_width() { return this->get_width_internal(); }
/// Get the native (original) height of the display in pixels.
int get_native_height() { return this->get_height_internal(); }
/// Set a single pixel at the specified coordinates to default color.
inline void draw_pixel_at(int x, int y) { this->draw_pixel_at(x, y, COLOR_ON); }
@ -242,6 +283,42 @@ class Display : public PollingComponent {
/// Fill a triangle contained between the points [x1,y1], [x2,y2] and [x3,y3] with the given color.
void filled_triangle(int x1, int y1, int x2, int y2, int x3, int y3, Color color = COLOR_ON);
/// Get the specified vertex (x,y) coordinates for the regular polygon inscribed in the circle centered on
/// [center_x,center_y] with the given radius. Vertex id are 0-indexed and rotate clockwise. In a pointy-topped
/// variation of a polygon with a 0° rotation, the vertex #0 is located at the top of the polygon. In a flat-topped
/// variation of a polygon with a 0° rotation, the vertex #0 is located on the left-side of the horizontal top
/// edge, and the vertex #1 is located on the right-side of the horizontal top edge.
/// Use the edges constants (e.g.: EDGES_HEXAGON) or any integer to specify the number of edges of the polygon.
/// Use the variation to switch between the flat-topped or the pointy-topped variation of the polygon.
/// Use the rotation in degrees to rotate the shape clockwise.
void get_regular_polygon_vertex(int vertex_id, int *vertex_x, int *vertex_y, int center_x, int center_y, int radius,
int edges, RegularPolygonVariation variation = VARIATION_POINTY_TOP,
float rotation_degrees = ROTATION_0_DEGREES);
/// Draw the outline of a regular polygon inscribed in the circle centered on [x,y] with the given
/// radius and color.
/// Use the edges constants (e.g.: EDGES_HEXAGON) or any integer to specify the number of edges of the polygon.
/// Use the variation to switch between the flat-topped or the pointy-topped variation of the polygon.
/// Use the rotation in degrees to rotate the shape clockwise.
/// Use the drawing to switch between outlining or filling the polygon.
void regular_polygon(int x, int y, int radius, int edges, RegularPolygonVariation variation = VARIATION_POINTY_TOP,
float rotation_degrees = ROTATION_0_DEGREES, Color color = COLOR_ON,
RegularPolygonDrawing drawing = DRAWING_OUTLINE);
void regular_polygon(int x, int y, int radius, int edges, RegularPolygonVariation variation, Color color,
RegularPolygonDrawing drawing = DRAWING_OUTLINE);
void regular_polygon(int x, int y, int radius, int edges, Color color,
RegularPolygonDrawing drawing = DRAWING_OUTLINE);
/// Fill a regular polygon inscribed in the circle centered on [x,y] with the given radius and color.
/// Use the edges constants (e.g.: EDGES_HEXAGON) or any integer to specify the number of edges of the polygon.
/// Use the variation to switch between the flat-topped or the pointy-topped variation of the polygon.
/// Use the rotation in degrees to rotate the shape clockwise.
void filled_regular_polygon(int x, int y, int radius, int edges,
RegularPolygonVariation variation = VARIATION_POINTY_TOP,
float rotation_degrees = ROTATION_0_DEGREES, Color color = COLOR_ON);
void filled_regular_polygon(int x, int y, int radius, int edges, RegularPolygonVariation variation, Color color);
void filled_regular_polygon(int x, int y, int radius, int edges, Color color);
/** Print `text` with the anchor point at [x,y] with `font`.
*
* @param x The x coordinate of the text alignment anchor point.
@ -538,6 +615,9 @@ class Display : public PollingComponent {
void do_update_();
void clear_clipping_();
virtual int get_height_internal() = 0;
virtual int get_width_internal() = 0;
/**
* This method fills a triangle using only integer variables by using a
* modified bresenham algorithm.

View File

@ -22,9 +22,6 @@ class DisplayBuffer : public Display {
/// Set a single pixel at the specified coordinates to the given color.
void draw_pixel_at(int x, int y, Color color) override;
virtual int get_height_internal() = 0;
virtual int get_width_internal() = 0;
protected:
virtual void draw_absolute_pixel_internal(int x, int y, Color color) = 0;

View File

@ -34,24 +34,27 @@ void EKTF2232Touchscreen::setup() {
// Get touch resolution
uint8_t received[4];
this->write(GET_X_RES, 4);
if (this->read(received, 4)) {
ESP_LOGE(TAG, "Failed to read X resolution!");
this->interrupt_pin_->detach_interrupt();
this->mark_failed();
return;
if (this->x_raw_max_ == this->x_raw_min_) {
this->write(GET_X_RES, 4);
if (this->read(received, 4)) {
ESP_LOGE(TAG, "Failed to read X resolution!");
this->interrupt_pin_->detach_interrupt();
this->mark_failed();
return;
}
this->x_raw_max_ = ((received[2])) | ((received[3] & 0xf0) << 4);
}
this->x_raw_max_ = ((received[2])) | ((received[3] & 0xf0) << 4);
this->write(GET_Y_RES, 4);
if (this->read(received, 4)) {
ESP_LOGE(TAG, "Failed to read Y resolution!");
this->interrupt_pin_->detach_interrupt();
this->mark_failed();
return;
if (this->y_raw_max_ == this->y_raw_min_) {
this->write(GET_Y_RES, 4);
if (this->read(received, 4)) {
ESP_LOGE(TAG, "Failed to read Y resolution!");
this->interrupt_pin_->detach_interrupt();
this->mark_failed();
return;
}
this->y_raw_max_ = ((received[2])) | ((received[3] & 0xf0) << 4);
}
this->y_raw_max_ = ((received[2])) | ((received[3] & 0xf0) << 4);
this->set_power_state(true);
}

View File

@ -7,6 +7,8 @@ CODEOWNERS = ["@ellull"]
DEPENDENCIES = ["i2c"]
MULTI_CONF = True
CONF_PWM = "pwm"
CONF_DIVIDER = "divider"
CONF_DAC = "dac"

View File

@ -2,6 +2,7 @@ import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor
from esphome.const import (
CONF_EXTERNAL_TEMPERATURE,
CONF_ID,
CONF_SPEED,
DEVICE_CLASS_TEMPERATURE,
@ -16,7 +17,6 @@ from .. import EMC2101_COMPONENT_SCHEMA, CONF_EMC2101_ID, emc2101_ns
DEPENDENCIES = ["emc2101"]
CONF_INTERNAL_TEMPERATURE = "internal_temperature"
CONF_EXTERNAL_TEMPERATURE = "external_temperature"
CONF_DUTY_CYCLE = "duty_cycle"
EMC2101Sensor = emc2101_ns.class_("EMC2101Sensor", cg.PollingComponent)

View File

@ -141,9 +141,13 @@ void ESP32ImprovComponent::loop() {
std::vector<std::string> urls = {ESPHOME_MY_LINK};
#ifdef USE_WEBSERVER
auto ip = wifi::global_wifi_component->wifi_sta_ip();
std::string webserver_url = "http://" + ip.str() + ":" + to_string(USE_WEBSERVER_PORT);
urls.push_back(webserver_url);
for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) {
if (ip.is_ip4()) {
std::string webserver_url = "http://" + ip.str() + ":" + to_string(USE_WEBSERVER_PORT);
urls.push_back(webserver_url);
break;
}
}
#endif
std::vector<uint8_t> data = improv::build_rpc_response(improv::WIFI_SETTINGS, urls);
this->send_response_(data);
@ -289,7 +293,7 @@ void ESP32ImprovComponent::process_incoming_data_() {
this->connecting_sta_ = sta;
wifi::global_wifi_component->set_sta(sta);
wifi::global_wifi_component->start_scanning();
wifi::global_wifi_component->start_connecting(sta, false);
this->set_state_(improv::STATE_PROVISIONING);
ESP_LOGD(TAG, "Received Improv wifi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(),
command.password.c_str());

View File

@ -160,11 +160,13 @@ light::ESPColorView ESP32RMTLEDStripLightOutput::get_view_internal(int32_t index
b = 0;
break;
}
uint8_t multiplier = this->is_rgbw_ ? 4 : 3;
return {this->buf_ + (index * multiplier) + r,
this->buf_ + (index * multiplier) + g,
this->buf_ + (index * multiplier) + b,
this->is_rgbw_ ? this->buf_ + (index * multiplier) + 3 : nullptr,
uint8_t multiplier = this->is_rgbw_ || this->is_wrgb_ ? 4 : 3;
uint8_t white = this->is_wrgb_ ? 0 : 3;
return {this->buf_ + (index * multiplier) + r + this->is_wrgb_,
this->buf_ + (index * multiplier) + g + this->is_wrgb_,
this->buf_ + (index * multiplier) + b + this->is_wrgb_,
this->is_rgbw_ || this->is_wrgb_ ? this->buf_ + (index * multiplier) + white : nullptr,
&this->effect_data_[index],
&this->correction_};
}

View File

@ -33,7 +33,7 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight {
int32_t size() const override { return this->num_leds_; }
light::LightTraits get_traits() override {
auto traits = light::LightTraits();
if (this->is_rgbw_) {
if (this->is_rgbw_ || this->is_wrgb_) {
traits.set_supported_color_modes({light::ColorMode::RGB_WHITE, light::ColorMode::WHITE});
} else {
traits.set_supported_color_modes({light::ColorMode::RGB});
@ -44,6 +44,7 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight {
void set_pin(uint8_t pin) { this->pin_ = pin; }
void set_num_leds(uint16_t num_leds) { this->num_leds_ = num_leds; }
void set_is_rgbw(bool is_rgbw) { this->is_rgbw_ = is_rgbw; }
void set_is_wrgb(bool is_wrgb) { this->is_wrgb_ = is_wrgb; }
/// Set a maximum refresh rate in µs as some lights do not like being updated too often.
void set_max_refresh_rate(uint32_t interval_us) { this->max_refresh_rate_ = interval_us; }
@ -72,6 +73,7 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight {
uint8_t pin_;
uint16_t num_leds_;
bool is_rgbw_;
bool is_wrgb_;
rmt_item32_t bit0_, bit1_;
RGBOrder rgb_order_;

View File

@ -52,6 +52,7 @@ CHIPSETS = {
CONF_IS_RGBW = "is_rgbw"
CONF_IS_WRGB = "is_wrgb"
CONF_BIT0_HIGH = "bit0_high"
CONF_BIT0_LOW = "bit0_low"
CONF_BIT1_HIGH = "bit1_high"
@ -90,6 +91,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_MAX_REFRESH_RATE): cv.positive_time_period_microseconds,
cv.Optional(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True),
cv.Optional(CONF_IS_RGBW, default=False): cv.boolean,
cv.Optional(CONF_IS_WRGB, default=False): cv.boolean,
cv.Inclusive(
CONF_BIT0_HIGH,
"custom",
@ -145,6 +147,7 @@ async def to_code(config):
cg.add(var.set_rgb_order(config[CONF_RGB_ORDER]))
cg.add(var.set_is_rgbw(config[CONF_IS_RGBW]))
cg.add(var.set_is_wrgb(config[CONF_IS_WRGB]))
cg.add(
var.set_rmt_channel(

View File

@ -1,6 +1,13 @@
from esphome import pins
import esphome.config_validation as cv
import esphome.final_validate as fv
import esphome.codegen as cg
from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant
from esphome.components.esp32.const import (
VARIANT_ESP32C3,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
)
from esphome.const import (
CONF_DOMAIN,
CONF_ID,
@ -12,9 +19,17 @@ from esphome.const import (
CONF_SUBNET,
CONF_DNS1,
CONF_DNS2,
CONF_CLK_PIN,
CONF_MISO_PIN,
CONF_MOSI_PIN,
CONF_CS_PIN,
CONF_INTERRUPT_PIN,
CONF_RESET_PIN,
CONF_SPI,
)
from esphome.core import CORE, coroutine_with_priority
from esphome.components.network import IPAddress
from esphome.components.spi import get_spi_interface, CONF_INTERFACE_INDEX
CONFLICTS_WITH = ["wifi"]
DEPENDENCIES = ["esp32"]
@ -27,6 +42,8 @@ CONF_MDIO_PIN = "mdio_pin"
CONF_CLK_MODE = "clk_mode"
CONF_POWER_PIN = "power_pin"
CONF_CLOCK_SPEED = "clock_speed"
EthernetType = ethernet_ns.enum("EthernetType")
ETHERNET_TYPES = {
"LAN8720": EthernetType.ETHERNET_TYPE_LAN8720,
@ -36,8 +53,11 @@ ETHERNET_TYPES = {
"JL1101": EthernetType.ETHERNET_TYPE_JL1101,
"KSZ8081": EthernetType.ETHERNET_TYPE_KSZ8081,
"KSZ8081RNA": EthernetType.ETHERNET_TYPE_KSZ8081RNA,
"W5500": EthernetType.ETHERNET_TYPE_W5500,
}
SPI_ETHERNET_TYPES = ["W5500"]
emac_rmii_clock_mode_t = cg.global_ns.enum("emac_rmii_clock_mode_t")
emac_rmii_clock_gpio_t = cg.global_ns.enum("emac_rmii_clock_gpio_t")
CLK_MODES = {
@ -84,11 +104,22 @@ def _validate(config):
return config
CONFIG_SCHEMA = cv.All(
BASE_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(EthernetComponent),
cv.Optional(CONF_MANUAL_IP): MANUAL_IP_SCHEMA,
cv.Optional(CONF_DOMAIN, default=".local"): cv.domain_name,
cv.Optional(CONF_USE_ADDRESS): cv.string_strict,
cv.Optional("enable_mdns"): cv.invalid(
"This option has been removed. Please use the [disabled] option under the "
"new mdns component instead."
),
}
).extend(cv.COMPONENT_SCHEMA)
RMII_SCHEMA = BASE_SCHEMA.extend(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(EthernetComponent),
cv.Required(CONF_TYPE): cv.enum(ETHERNET_TYPES, upper=True),
cv.Required(CONF_MDC_PIN): pins.internal_gpio_output_pin_number,
cv.Required(CONF_MDIO_PIN): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_CLK_MODE, default="GPIO0_IN"): cv.enum(
@ -96,19 +127,64 @@ CONFIG_SCHEMA = cv.All(
),
cv.Optional(CONF_PHY_ADDR, default=0): cv.int_range(min=0, max=31),
cv.Optional(CONF_POWER_PIN): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_MANUAL_IP): MANUAL_IP_SCHEMA,
cv.Optional(CONF_DOMAIN, default=".local"): cv.domain_name,
cv.Optional(CONF_USE_ADDRESS): cv.string_strict,
cv.Optional("enable_mdns"): cv.invalid(
"This option has been removed. Please use the [disabled] option under the "
"new mdns component instead."
}
)
)
SPI_SCHEMA = BASE_SCHEMA.extend(
cv.Schema(
{
cv.Required(CONF_CLK_PIN): pins.internal_gpio_output_pin_number,
cv.Required(CONF_MISO_PIN): pins.internal_gpio_input_pin_number,
cv.Required(CONF_MOSI_PIN): pins.internal_gpio_output_pin_number,
cv.Required(CONF_CS_PIN): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_number,
cv.Optional(CONF_RESET_PIN): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_CLOCK_SPEED, default="26.67MHz"): cv.All(
cv.frequency, cv.int_range(int(8e6), int(80e6))
),
}
).extend(cv.COMPONENT_SCHEMA),
),
)
CONFIG_SCHEMA = cv.All(
cv.typed_schema(
{
"LAN8720": RMII_SCHEMA,
"RTL8201": RMII_SCHEMA,
"DP83848": RMII_SCHEMA,
"IP101": RMII_SCHEMA,
"JL1101": RMII_SCHEMA,
"W5500": SPI_SCHEMA,
},
upper=True,
),
_validate,
)
def _final_validate(config):
if config[CONF_TYPE] not in SPI_ETHERNET_TYPES:
return
if spi_configs := fv.full_config.get().get(CONF_SPI):
variant = get_esp32_variant()
if variant in (VARIANT_ESP32C3, VARIANT_ESP32S2, VARIANT_ESP32S3):
spi_host = "SPI2_HOST"
else:
spi_host = "SPI3_HOST"
for spi_conf in spi_configs:
if (index := spi_conf.get(CONF_INTERFACE_INDEX)) is not None:
interface = get_spi_interface(index)
if interface == spi_host:
raise cv.Invalid(
f"`spi` component is using interface '{interface}'. "
f"To use {config[CONF_TYPE]}, you must change the `interface` on the `spi` component.",
)
FINAL_VALIDATE_SCHEMA = _final_validate
def manual_ip(config):
return cg.StructInitializer(
ManualIP,
@ -125,15 +201,31 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
cg.add(var.set_phy_addr(config[CONF_PHY_ADDR]))
cg.add(var.set_mdc_pin(config[CONF_MDC_PIN]))
cg.add(var.set_mdio_pin(config[CONF_MDIO_PIN]))
cg.add(var.set_type(config[CONF_TYPE]))
cg.add(var.set_clk_mode(*CLK_MODES[config[CONF_CLK_MODE]]))
cg.add(var.set_use_address(config[CONF_USE_ADDRESS]))
if config[CONF_TYPE] == "W5500":
cg.add(var.set_clk_pin(config[CONF_CLK_PIN]))
cg.add(var.set_miso_pin(config[CONF_MISO_PIN]))
cg.add(var.set_mosi_pin(config[CONF_MOSI_PIN]))
cg.add(var.set_cs_pin(config[CONF_CS_PIN]))
if CONF_INTERRUPT_PIN in config:
cg.add(var.set_interrupt_pin(config[CONF_INTERRUPT_PIN]))
if CONF_RESET_PIN in config:
cg.add(var.set_reset_pin(config[CONF_RESET_PIN]))
cg.add(var.set_clock_speed(config[CONF_CLOCK_SPEED]))
if CONF_POWER_PIN in config:
cg.add(var.set_power_pin(config[CONF_POWER_PIN]))
cg.add_define("USE_ETHERNET_SPI")
if CORE.using_esp_idf:
add_idf_sdkconfig_option("CONFIG_ETH_USE_SPI_ETHERNET", True)
add_idf_sdkconfig_option("CONFIG_ETH_SPI_ETHERNET_W5500", True)
else:
cg.add(var.set_phy_addr(config[CONF_PHY_ADDR]))
cg.add(var.set_mdc_pin(config[CONF_MDC_PIN]))
cg.add(var.set_mdio_pin(config[CONF_MDIO_PIN]))
cg.add(var.set_clk_mode(*CLK_MODES[config[CONF_CLK_MODE]]))
if CONF_POWER_PIN in config:
cg.add(var.set_power_pin(config[CONF_POWER_PIN]))
cg.add(var.set_type(ETHERNET_TYPES[config[CONF_TYPE]]))
cg.add(var.set_use_address(config[CONF_USE_ADDRESS]))
if CONF_MANUAL_IP in config:
cg.add(var.set_manual_ip(manual_ip(config[CONF_MANUAL_IP])))

View File

@ -9,6 +9,11 @@
#include <lwip/dns.h>
#include "esp_event.h"
#ifdef USE_ETHERNET_SPI
#include <driver/gpio.h>
#include <driver/spi_master.h>
#endif
namespace esphome {
namespace ethernet {
@ -33,6 +38,36 @@ void EthernetComponent::setup() {
}
esp_err_t err;
#ifdef USE_ETHERNET_SPI
// Install GPIO ISR handler to be able to service SPI Eth modules interrupts
gpio_install_isr_service(0);
spi_bus_config_t buscfg = {
.mosi_io_num = this->mosi_pin_,
.miso_io_num = this->miso_pin_,
.sclk_io_num = this->clk_pin_,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.data4_io_num = -1,
.data5_io_num = -1,
.data6_io_num = -1,
.data7_io_num = -1,
.max_transfer_sz = 0,
.flags = 0,
.intr_flags = 0,
};
#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
auto host = SPI2_HOST;
#else
auto host = SPI3_HOST;
#endif
err = spi_bus_initialize(host, &buscfg, SPI_DMA_CH_AUTO);
ESPHL_ERROR_CHECK(err, "SPI bus initialize error");
#endif
err = esp_netif_init();
ESPHL_ERROR_CHECK(err, "ETH netif init error");
err = esp_event_loop_create_default();
@ -43,10 +78,40 @@ void EthernetComponent::setup() {
// Init MAC and PHY configs to default
eth_phy_config_t phy_config = ETH_PHY_DEFAULT_CONFIG();
eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG();
#ifdef USE_ETHERNET_SPI // Configure SPI interface and Ethernet driver for specific SPI module
spi_device_interface_config_t devcfg = {
.command_bits = 16, // Actually it's the address phase in W5500 SPI frame
.address_bits = 8, // Actually it's the control phase in W5500 SPI frame
.dummy_bits = 0,
.mode = 0,
.duty_cycle_pos = 0,
.cs_ena_pretrans = 0,
.cs_ena_posttrans = 0,
.clock_speed_hz = this->clock_speed_,
.input_delay_ns = 0,
.spics_io_num = this->cs_pin_,
.flags = 0,
.queue_size = 20,
.pre_cb = nullptr,
.post_cb = nullptr,
};
spi_device_handle_t spi_handle = nullptr;
err = spi_bus_add_device(host, &devcfg, &spi_handle);
ESPHL_ERROR_CHECK(err, "SPI bus add device error");
eth_w5500_config_t w5500_config = ETH_W5500_DEFAULT_CONFIG(spi_handle);
w5500_config.int_gpio_num = this->interrupt_pin_;
phy_config.phy_addr = this->phy_addr_spi_;
phy_config.reset_gpio_num = this->reset_pin_;
esp_eth_mac_t *mac = esp_eth_mac_new_w5500(&w5500_config, &mac_config);
#else
phy_config.phy_addr = this->phy_addr_;
phy_config.reset_gpio_num = this->power_pin_;
eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG();
#if ESP_IDF_VERSION_MAJOR >= 5
eth_esp32_emac_config_t esp32_emac_config = ETH_ESP32_EMAC_DEFAULT_CONFIG();
esp32_emac_config.smi_mdc_gpio_num = this->mdc_pin_;
@ -62,9 +127,11 @@ void EthernetComponent::setup() {
mac_config.clock_config.rmii.clock_gpio = this->clk_gpio_;
esp_eth_mac_t *mac = esp_eth_mac_new_esp32(&mac_config);
#endif
#endif
switch (this->type_) {
#if CONFIG_ETH_USE_ESP32_EMAC
case ETHERNET_TYPE_LAN8720: {
this->phy_ = esp_eth_phy_new_lan87xx(&phy_config);
break;
@ -94,6 +161,13 @@ void EthernetComponent::setup() {
#endif
break;
}
#endif
#ifdef USE_ETHERNET_SPI
case ETHERNET_TYPE_W5500: {
this->phy_ = esp_eth_phy_new_w5500(&phy_config);
break;
}
#endif
default: {
this->mark_failed();
return;
@ -105,10 +179,18 @@ void EthernetComponent::setup() {
err = esp_eth_driver_install(&eth_config, &this->eth_handle_);
ESPHL_ERROR_CHECK(err, "ETH driver install error");
#ifndef USE_ETHERNET_SPI
if (this->type_ == ETHERNET_TYPE_KSZ8081RNA && this->clk_mode_ == EMAC_CLK_OUT) {
// KSZ8081RNA default is incorrect. It expects a 25MHz clock instead of the 50MHz we provide.
this->ksz8081_set_clock_reference_(mac);
}
#endif
// use ESP internal eth mac
uint8_t mac_addr[6];
esp_read_mac(mac_addr, ESP_MAC_ETH);
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_S_MAC_ADDR, mac_addr);
ESPHL_ERROR_CHECK(err, "set mac address error");
/* attach Ethernet driver to TCP/IP stack */
err = esp_netif_attach(this->eth_netif_, esp_eth_new_netif_glue(this->eth_handle_));
@ -119,10 +201,10 @@ void EthernetComponent::setup() {
ESPHL_ERROR_CHECK(err, "ETH event handler register error");
err = esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP, &EthernetComponent::got_ip_event_handler, nullptr);
ESPHL_ERROR_CHECK(err, "GOT IP event handler register error");
#if ENABLE_IPV6
#if USE_NETWORK_IPV6
err = esp_event_handler_register(IP_EVENT, IP_EVENT_GOT_IP6, &EthernetComponent::got_ip6_event_handler, nullptr);
ESPHL_ERROR_CHECK(err, "GOT IP6 event handler register error");
#endif /* ENABLE_IPV6 */
ESPHL_ERROR_CHECK(err, "GOT IPv6 event handler register error");
#endif /* USE_NETWORK_IPV6 */
/* start Ethernet driver state machine */
err = esp_eth_start(this->eth_handle_);
@ -165,20 +247,6 @@ void EthernetComponent::loop() {
this->state_ = EthernetComponentState::CONNECTING;
this->start_connect_();
}
#if ENABLE_IPV6
else if (this->got_ipv6_) {
esp_ip6_addr_t ip6_addr;
if (esp_netif_get_ip6_global(this->eth_netif_, &ip6_addr) == 0 &&
esp_netif_ip6_get_addr_type(&ip6_addr) == ESP_IP6_ADDR_IS_GLOBAL) {
ESP_LOGCONFIG(TAG, "IPv6 Addr (Global): " IPV6STR, IPV62STR(ip6_addr));
} else {
esp_netif_get_ip6_linklocal(this->eth_netif_, &ip6_addr);
ESP_LOGCONFIG(TAG, " IPv6: " IPV6STR, IPV62STR(ip6_addr));
}
this->got_ipv6_ = false;
}
#endif /* ENABLE_IPV6 */
break;
}
}
@ -214,6 +282,10 @@ void EthernetComponent::dump_config() {
eth_type = "KSZ8081RNA";
break;
case ETHERNET_TYPE_W5500:
eth_type = "W5500";
break;
default:
eth_type = "Unknown";
break;
@ -221,23 +293,51 @@ void EthernetComponent::dump_config() {
ESP_LOGCONFIG(TAG, "Ethernet:");
this->dump_connect_params_();
#ifdef USE_ETHERNET_SPI
ESP_LOGCONFIG(TAG, " CLK Pin: %u", this->clk_pin_);
ESP_LOGCONFIG(TAG, " MISO Pin: %u", this->miso_pin_);
ESP_LOGCONFIG(TAG, " MOSI Pin: %u", this->mosi_pin_);
ESP_LOGCONFIG(TAG, " CS Pin: %u", this->cs_pin_);
ESP_LOGCONFIG(TAG, " IRQ Pin: %u", this->interrupt_pin_);
ESP_LOGCONFIG(TAG, " Reset Pin: %d", this->reset_pin_);
ESP_LOGCONFIG(TAG, " Clock Speed: %d MHz", this->clock_speed_ / 1000000);
#else
if (this->power_pin_ != -1) {
ESP_LOGCONFIG(TAG, " Power Pin: %u", this->power_pin_);
}
ESP_LOGCONFIG(TAG, " MDC Pin: %u", this->mdc_pin_);
ESP_LOGCONFIG(TAG, " MDIO Pin: %u", this->mdio_pin_);
ESP_LOGCONFIG(TAG, " Type: %s", eth_type);
ESP_LOGCONFIG(TAG, " PHY addr: %u", this->phy_addr_);
#endif
ESP_LOGCONFIG(TAG, " Type: %s", eth_type);
}
float EthernetComponent::get_setup_priority() const { return setup_priority::WIFI; }
bool EthernetComponent::can_proceed() { return this->is_connected(); }
network::IPAddress EthernetComponent::get_ip_address() {
network::IPAddresses EthernetComponent::get_ip_addresses() {
network::IPAddresses addresses;
esp_netif_ip_info_t ip;
esp_netif_get_ip_info(this->eth_netif_, &ip);
return network::IPAddress(&ip.ip);
esp_err_t err = esp_netif_get_ip_info(this->eth_netif_, &ip);
if (err != ESP_OK) {
ESP_LOGV(TAG, "esp_netif_get_ip_info failed: %s", esp_err_to_name(err));
// TODO: do something smarter
// return false;
} else {
addresses[0] = network::IPAddress(&ip.ip);
}
#if USE_NETWORK_IPV6
struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES];
uint8_t count = 0;
count = esp_netif_get_all_ip6(this->eth_netif_, if_ip6s);
assert(count <= CONFIG_LWIP_IPV6_NUM_ADDRESSES);
for (int i = 0; i < count; i++) {
addresses[i + 1] = network::IPAddress(&if_ip6s[i]);
}
#endif /* USE_NETWORK_IPV6 */
return addresses;
}
void EthernetComponent::eth_event_handler(void *arg, esp_event_base_t event_base, int32_t event, void *event_data) {
@ -269,20 +369,33 @@ void EthernetComponent::eth_event_handler(void *arg, esp_event_base_t event_base
void EthernetComponent::got_ip_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id,
void *event_data) {
ip_event_got_ip_t *event = (ip_event_got_ip_t *) event_data;
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
global_eth_component->connected_ = global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT;
#else
global_eth_component->connected_ = true;
ESP_LOGV(TAG, "[Ethernet event] ETH Got IP (num=%" PRId32 ")", event_id);
#endif /* USE_NETWORK_IPV6 */
}
#if ENABLE_IPV6
#if USE_NETWORK_IPV6
void EthernetComponent::got_ip6_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id,
void *event_data) {
ESP_LOGV(TAG, "[Ethernet event] ETH Got IP6 (num=%" PRId32 ")", event_id);
global_eth_component->got_ipv6_ = true;
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;
global_eth_component->connected_ =
global_eth_component->got_ipv4_address_ && (global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT);
}
#endif /* ENABLE_IPV6 */
#endif /* USE_NETWORK_IPV6 */
void EthernetComponent::start_connect_() {
global_eth_component->got_ipv4_address_ = false;
#if USE_NETWORK_IPV6
global_eth_component->ipv6_count_ = 0;
#endif /* USE_NETWORK_IPV6 */
this->connect_begin_ = millis();
this->status_set_warning();
@ -334,12 +447,12 @@ void EthernetComponent::start_connect_() {
if (err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STARTED) {
ESPHL_ERROR_CHECK(err, "DHCPC start error");
}
#if ENABLE_IPV6
#if USE_NETWORK_IPV6
err = esp_netif_create_ip6_linklocal(this->eth_netif_);
if (err != ESP_OK) {
ESPHL_ERROR_CHECK(err, "IPv6 local failed");
ESPHL_ERROR_CHECK(err, "Enable IPv6 link local failed");
}
#endif /* ENABLE_IPV6 */
#endif /* USE_NETWORK_IPV6 */
}
this->connect_begin_ = millis();
@ -362,18 +475,15 @@ void EthernetComponent::dump_connect_params_() {
ESP_LOGCONFIG(TAG, " DNS1: %s", network::IPAddress(dns_ip1).str().c_str());
ESP_LOGCONFIG(TAG, " DNS2: %s", network::IPAddress(dns_ip2).str().c_str());
#if ENABLE_IPV6
if (this->ipv6_count_ > 0) {
esp_ip6_addr_t ip6_addr;
esp_netif_get_ip6_linklocal(this->eth_netif_, &ip6_addr);
ESP_LOGCONFIG(TAG, " IPv6: " IPV6STR, IPV62STR(ip6_addr));
if (esp_netif_get_ip6_global(this->eth_netif_, &ip6_addr) == 0 &&
esp_netif_ip6_get_addr_type(&ip6_addr) == ESP_IP6_ADDR_IS_GLOBAL) {
ESP_LOGCONFIG(TAG, "IPv6 Addr (Global): " IPV6STR, IPV62STR(ip6_addr));
}
#if USE_NETWORK_IPV6
struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES];
uint8_t count = 0;
count = esp_netif_get_all_ip6(this->eth_netif_, if_ip6s);
assert(count <= CONFIG_LWIP_IPV6_NUM_ADDRESSES);
for (int i = 0; i < count; i++) {
ESP_LOGCONFIG(TAG, " IPv6: " IPV6STR, IPV62STR(if_ip6s[i]));
}
#endif /* ENABLE_IPV6 */
#endif /* USE_NETWORK_IPV6 */
esp_err_t err;
@ -393,15 +503,25 @@ void EthernetComponent::dump_connect_params_() {
ESP_LOGCONFIG(TAG, " Link Speed: %u", speed == ETH_SPEED_100M ? 100 : 10);
}
#ifdef USE_ETHERNET_SPI
void EthernetComponent::set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; }
void EthernetComponent::set_miso_pin(uint8_t miso_pin) { this->miso_pin_ = miso_pin; }
void EthernetComponent::set_mosi_pin(uint8_t mosi_pin) { this->mosi_pin_ = mosi_pin; }
void EthernetComponent::set_cs_pin(uint8_t cs_pin) { this->cs_pin_ = cs_pin; }
void EthernetComponent::set_interrupt_pin(uint8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; }
void EthernetComponent::set_reset_pin(uint8_t reset_pin) { this->reset_pin_ = reset_pin; }
void EthernetComponent::set_clock_speed(int clock_speed) { this->clock_speed_ = clock_speed; }
#else
void EthernetComponent::set_phy_addr(uint8_t phy_addr) { this->phy_addr_ = phy_addr; }
void EthernetComponent::set_power_pin(int power_pin) { this->power_pin_ = power_pin; }
void EthernetComponent::set_mdc_pin(uint8_t mdc_pin) { this->mdc_pin_ = mdc_pin; }
void EthernetComponent::set_mdio_pin(uint8_t mdio_pin) { this->mdio_pin_ = mdio_pin; }
void EthernetComponent::set_type(EthernetType type) { this->type_ = type; }
void EthernetComponent::set_clk_mode(emac_rmii_clock_mode_t clk_mode, emac_rmii_clock_gpio_t clk_gpio) {
this->clk_mode_ = clk_mode;
this->clk_gpio_ = clk_gpio;
}
#endif
void EthernetComponent::set_type(EthernetType type) { this->type_ = type; }
void EthernetComponent::set_manual_ip(const ManualIP &manual_ip) { this->manual_ip_ = manual_ip; }
std::string EthernetComponent::get_use_address() const {
@ -428,6 +548,7 @@ bool EthernetComponent::powerdown() {
return true;
}
#ifndef USE_ETHERNET_SPI
void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) {
#define KSZ80XX_PC2R_REG_ADDR (0x1F)
@ -458,6 +579,7 @@ void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) {
#undef KSZ80XX_PC2R_REG_ADDR
}
#endif
} // namespace ethernet
} // namespace esphome

View File

@ -23,6 +23,7 @@ enum EthernetType {
ETHERNET_TYPE_JL1101,
ETHERNET_TYPE_KSZ8081,
ETHERNET_TYPE_KSZ8081RNA,
ETHERNET_TYPE_W5500,
};
struct ManualIP {
@ -50,15 +51,25 @@ class EthernetComponent : public Component {
void on_shutdown() override { powerdown(); }
bool is_connected();
#ifdef USE_ETHERNET_SPI
void set_clk_pin(uint8_t clk_pin);
void set_miso_pin(uint8_t miso_pin);
void set_mosi_pin(uint8_t mosi_pin);
void set_cs_pin(uint8_t cs_pin);
void set_interrupt_pin(uint8_t interrupt_pin);
void set_reset_pin(uint8_t reset_pin);
void set_clock_speed(int clock_speed);
#else
void set_phy_addr(uint8_t phy_addr);
void set_power_pin(int power_pin);
void set_mdc_pin(uint8_t mdc_pin);
void set_mdio_pin(uint8_t mdio_pin);
void set_type(EthernetType type);
void set_clk_mode(emac_rmii_clock_mode_t clk_mode, emac_rmii_clock_gpio_t clk_gpio);
#endif
void set_type(EthernetType type);
void set_manual_ip(const ManualIP &manual_ip);
network::IPAddress get_ip_address();
network::IPAddresses get_ip_addresses();
std::string get_use_address() const;
void set_use_address(const std::string &use_address);
bool powerdown();
@ -76,19 +87,30 @@ class EthernetComponent : public Component {
void ksz8081_set_clock_reference_(esp_eth_mac_t *mac);
std::string use_address_;
#ifdef USE_ETHERNET_SPI
uint8_t clk_pin_;
uint8_t miso_pin_;
uint8_t mosi_pin_;
uint8_t cs_pin_;
uint8_t interrupt_pin_;
int reset_pin_{-1};
int phy_addr_spi_{-1};
int clock_speed_;
#else
uint8_t phy_addr_{0};
int power_pin_{-1};
uint8_t mdc_pin_{23};
uint8_t mdio_pin_{18};
EthernetType type_{ETHERNET_TYPE_UNKNOWN};
emac_rmii_clock_mode_t clk_mode_{EMAC_CLK_EXT_IN};
emac_rmii_clock_gpio_t clk_gpio_{EMAC_CLK_IN_GPIO};
#endif
EthernetType type_{ETHERNET_TYPE_UNKNOWN};
optional<ManualIP> manual_ip_{};
bool started_{false};
bool connected_{false};
bool got_ipv4_address_{false};
#if LWIP_IPV6
bool got_ipv6_{false};
uint8_t ipv6_count_{0};
#endif /* LWIP_IPV6 */
EthernetComponentState state_{EthernetComponentState::STOPPED};

View File

@ -12,19 +12,30 @@ namespace ethernet_info {
class IPAddressEthernetInfo : public PollingComponent, public text_sensor::TextSensor {
public:
void update() override {
auto ip = ethernet::global_eth_component->get_ip_address();
if (ip != this->last_ip_) {
this->last_ip_ = ip;
this->publish_state(network::IPAddress(ip).str());
auto ips = ethernet::global_eth_component->get_ip_addresses();
if (ips != this->last_ips_) {
this->last_ips_ = ips;
this->publish_state(ips[0].str());
uint8_t sensor = 0;
for (auto &ip : ips) {
if (ip.is_set()) {
if (this->ip_sensors_[sensor] != nullptr) {
this->ip_sensors_[sensor]->publish_state(ip.str());
}
sensor++;
}
}
}
}
float get_setup_priority() const override { return setup_priority::ETHERNET; }
std::string unique_id() override { return get_mac_address() + "-ethernetinfo"; }
void dump_config() override;
void add_ip_sensors(uint8_t index, text_sensor::TextSensor *s) { this->ip_sensors_[index] = s; }
protected:
network::IPAddress last_ip_;
network::IPAddresses last_ips_;
std::array<text_sensor::TextSensor *, 5> ip_sensors_;
};
} // namespace ethernet_info

View File

@ -18,17 +18,25 @@ CONFIG_SCHEMA = cv.Schema(
{
cv.Optional(CONF_IP_ADDRESS): text_sensor.text_sensor_schema(
IPAddressEsthernetInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
).extend(cv.polling_component_schema("1s"))
)
.extend(cv.polling_component_schema("1s"))
.extend(
{
cv.Optional(f"address_{x}"): text_sensor.text_sensor_schema(
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
)
for x in range(5)
}
)
}
)
async def setup_conf(config, key):
if key in config:
conf = config[key]
var = await text_sensor.new_text_sensor(conf)
await cg.register_component(var, conf)
async def to_code(config):
await setup_conf(config, CONF_IP_ADDRESS)
if conf := config.get(CONF_IP_ADDRESS):
ip_info = await text_sensor.new_text_sensor(config[CONF_IP_ADDRESS])
await cg.register_component(ip_info, config[CONF_IP_ADDRESS])
for x in range(5):
if sensor_conf := conf.get(f"address_{x}"):
sens = await text_sensor.new_text_sensor(sensor_conf)
cg.add(ip_info.add_ip_sensors(x, sens))

View File

@ -336,6 +336,8 @@ void FingerprintGrowComponent::aura_led_control(uint8_t state, uint8_t speed, ui
}
uint8_t FingerprintGrowComponent::send_command_() {
while (this->available())
this->read();
this->write((uint8_t) (START_CODE >> 8));
this->write((uint8_t) (START_CODE & 0xFF));
this->write(this->address_[0]);

View File

@ -66,8 +66,14 @@ class FT5x06Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice
return;
}
// reading the chip registers to get max x/y does not seem to work.
this->x_raw_max_ = this->display_->get_width();
this->y_raw_max_ = this->display_->get_height();
if (this->display_ != nullptr) {
if (this->x_raw_max_ == this->x_raw_min_) {
this->x_raw_max_ = this->display_->get_native_width();
}
if (this->y_raw_max_ == this->y_raw_min_) {
this->x_raw_max_ = this->display_->get_native_height();
}
}
esph_log_config(TAG, "FT5x06 Touchscreen setup complete");
}

View File

@ -12,21 +12,23 @@
// Reference: https://focuslcds.com/content/FT6236.pdf
namespace esphome {
namespace ft63x6 {
static const uint8_t FT6X36_ADDR_DEVICE_MODE = 0x00;
static const uint8_t FT63X6_ADDR_TD_STATUS = 0x02;
static const uint8_t FT63X6_ADDR_TOUCH1_STATE = 0x03;
static const uint8_t FT63X6_ADDR_TOUCH1_X = 0x03;
static const uint8_t FT63X6_ADDR_TOUCH1_ID = 0x05;
static const uint8_t FT63X6_ADDR_TOUCH1_Y = 0x05;
static const uint8_t FT63X6_ADDR_TOUCH1_WEIGHT = 0x07;
static const uint8_t FT63X6_ADDR_TOUCH1_MISC = 0x08;
static const uint8_t FT6X36_ADDR_THRESHHOLD = 0x80;
static const uint8_t FT6X36_ADDR_TOUCHRATE_ACTIVE = 0x88;
static const uint8_t FT63X6_ADDR_CHIP_ID = 0xA3;
static const uint8_t FT63X6_ADDR_TOUCH2_STATE = 0x09;
static const uint8_t FT63X6_ADDR_TOUCH2_X = 0x09;
static const uint8_t FT63X6_ADDR_TOUCH2_ID = 0x0B;
static const uint8_t FT63X6_ADDR_TOUCH2_Y = 0x0B;
static const char *const TAG = "FT63X6Touchscreen";
static const char *const TAG = "FT63X6";
void FT63X6Touchscreen::setup() {
ESP_LOGCONFIG(TAG, "Setting up FT63X6Touchscreen Touchscreen...");
ESP_LOGCONFIG(TAG, "Setting up FT63X6 Touchscreen...");
if (this->interrupt_pin_ != nullptr) {
this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP);
this->interrupt_pin_->setup();
@ -35,10 +37,9 @@ void FT63X6Touchscreen::setup() {
if (this->reset_pin_ != nullptr) {
this->reset_pin_->setup();
this->hard_reset_();
}
this->hard_reset_();
// Get touch resolution
if (this->x_raw_max_ == this->x_raw_min_) {
this->x_raw_max_ = 320;
@ -46,6 +47,15 @@ void FT63X6Touchscreen::setup() {
if (this->y_raw_max_ == this->y_raw_min_) {
this->y_raw_max_ = 480;
}
uint8_t chip_id = this->read_byte_(FT63X6_ADDR_CHIP_ID);
if (chip_id != 0) {
ESP_LOGI(TAG, "FT6336U touch driver started chipid: %d", chip_id);
} else {
ESP_LOGE(TAG, "FT6336U touch driver failed to start");
}
this->write_byte(FT6X36_ADDR_DEVICE_MODE, 0x00);
this->write_byte(FT6X36_ADDR_THRESHHOLD, this->threshold_);
this->write_byte(FT6X36_ADDR_TOUCHRATE_ACTIVE, 0x0E);
}
void FT63X6Touchscreen::hard_reset_() {
@ -65,28 +75,61 @@ void FT63X6Touchscreen::dump_config() {
}
void FT63X6Touchscreen::update_touches() {
uint8_t data[15];
uint16_t touch_id, x, y;
if (!this->read_bytes(0x00, (uint8_t *) data, 15)) {
ESP_LOGE(TAG, "Failed to read touch data");
this->skip_update_ = true;
uint8_t touches = this->read_touch_number_();
if ((touches == 0x00) || (touches == 0xff)) {
// ESP_LOGD(TAG, "No touches detected");
return;
}
if (((data[FT63X6_ADDR_TOUCH1_STATE] >> 6) & 0x01) == 0) {
touch_id = data[FT63X6_ADDR_TOUCH1_ID] >> 4; // id1 = 0 or 1
x = encode_uint16(data[FT63X6_ADDR_TOUCH1_X] & 0x0F, data[FT63X6_ADDR_TOUCH1_X + 1]);
y = encode_uint16(data[FT63X6_ADDR_TOUCH1_Y] & 0x0F, data[FT63X6_ADDR_TOUCH1_Y + 1]);
this->add_raw_touch_position_(touch_id, x, y);
}
if (((data[FT63X6_ADDR_TOUCH2_STATE] >> 6) & 0x01) == 0) {
touch_id = data[FT63X6_ADDR_TOUCH2_ID] >> 4; // id1 = 0 or 1
x = encode_uint16(data[FT63X6_ADDR_TOUCH2_X] & 0x0F, data[FT63X6_ADDR_TOUCH2_X + 1]);
y = encode_uint16(data[FT63X6_ADDR_TOUCH2_Y] & 0x0F, data[FT63X6_ADDR_TOUCH2_Y + 1]);
this->add_raw_touch_position_(touch_id, x, y);
ESP_LOGV(TAG, "Touches found: %d", touches);
for (auto point = 0; point < touches; point++) {
if (((this->read_touch_event_(point)) & 0x01) == 0) { // checking event flag bit 6 if it is null
touch_id = this->read_touch_id_(point); // id1 = 0 or 1
x = this->read_touch_x_(point);
y = this->read_touch_y_(point);
if ((x == 0) && (y == 0)) {
ESP_LOGW(TAG, "Reporting a (0,0) touch on %d", touch_id);
}
this->add_raw_touch_position_(touch_id, x, y, this->read_touch_weight_(point));
}
}
}
uint8_t FT63X6Touchscreen::read_touch_number_() { return this->read_byte_(FT63X6_ADDR_TD_STATUS) & 0x0F; }
// Touch 1 functions
uint16_t FT63X6Touchscreen::read_touch_x_(uint8_t touch) {
uint8_t read_buf[2];
read_buf[0] = this->read_byte_(FT63X6_ADDR_TOUCH1_X + (touch * 6));
read_buf[1] = this->read_byte_(FT63X6_ADDR_TOUCH1_X + 1 + (touch * 6));
return ((read_buf[0] & 0x0f) << 8) | read_buf[1];
}
uint16_t FT63X6Touchscreen::read_touch_y_(uint8_t touch) {
uint8_t read_buf[2];
read_buf[0] = this->read_byte_(FT63X6_ADDR_TOUCH1_Y + (touch * 6));
read_buf[1] = this->read_byte_(FT63X6_ADDR_TOUCH1_Y + 1 + (touch * 6));
return ((read_buf[0] & 0x0f) << 8) | read_buf[1];
}
uint8_t FT63X6Touchscreen::read_touch_event_(uint8_t touch) {
return this->read_byte_(FT63X6_ADDR_TOUCH1_X + (touch * 6)) >> 6;
}
uint8_t FT63X6Touchscreen::read_touch_id_(uint8_t touch) {
return this->read_byte_(FT63X6_ADDR_TOUCH1_ID + (touch * 6)) >> 4;
}
uint8_t FT63X6Touchscreen::read_touch_weight_(uint8_t touch) {
return this->read_byte_(FT63X6_ADDR_TOUCH1_WEIGHT + (touch * 6));
}
uint8_t FT63X6Touchscreen::read_touch_misc_(uint8_t touch) {
return this->read_byte_(FT63X6_ADDR_TOUCH1_MISC + (touch * 6)) >> 4;
}
uint8_t FT63X6Touchscreen::read_byte_(uint8_t addr) {
uint8_t byte = 0;
this->read_byte(addr, &byte);
return byte;
}
} // namespace ft63x6
} // namespace esphome

View File

@ -16,6 +16,8 @@ namespace ft63x6 {
using namespace touchscreen;
static const uint8_t FT6X36_DEFAULT_THRESHOLD = 22;
class FT63X6Touchscreen : public Touchscreen, public i2c::I2CDevice {
public:
void setup() override;
@ -23,18 +25,26 @@ class FT63X6Touchscreen : public Touchscreen, public i2c::I2CDevice {
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; }
void set_threshold(uint8_t threshold) { this->threshold_ = threshold; }
protected:
void hard_reset_();
uint8_t read_byte_(uint8_t addr);
void update_touches() override;
InternalGPIOPin *interrupt_pin_{nullptr};
GPIOPin *reset_pin_{nullptr};
uint8_t threshold_{FT6X36_DEFAULT_THRESHOLD};
uint8_t read_touch_count_();
uint16_t read_touch_coordinate_(uint8_t coordinate);
uint8_t read_touch_id_(uint8_t id_address);
uint8_t read_touch_number_();
uint16_t read_touch_x_(uint8_t touch);
uint16_t read_touch_y_(uint8_t touch);
uint8_t read_touch_event_(uint8_t touch);
uint8_t read_touch_id_(uint8_t touch);
uint8_t read_touch_weight_(uint8_t touch);
uint8_t read_touch_misc_(uint8_t touch);
uint8_t read_byte_(uint8_t addr);
};
} // namespace ft63x6

View File

@ -3,7 +3,7 @@ import esphome.config_validation as cv
from esphome import pins
from esphome.components import i2c, touchscreen
from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_RESET_PIN
from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_RESET_PIN, CONF_THRESHOLD
CODEOWNERS = ["@gpambrozio"]
DEPENDENCIES = ["i2c"]
@ -26,6 +26,7 @@ CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend(
pins.internal_gpio_input_pin_schema
),
cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
cv.Optional(CONF_THRESHOLD): cv.uint8_t,
}
).extend(i2c.i2c_device_schema(0x38))
)

View File

@ -48,9 +48,13 @@ void GT911Touchscreen::setup() {
if (err == i2c::ERROR_OK) {
err = this->read(data, sizeof(data));
if (err == i2c::ERROR_OK) {
this->x_raw_max_ = encode_uint16(data[1], data[0]);
this->y_raw_max_ = encode_uint16(data[3], data[2]);
esph_log_d(TAG, "Read max_x/max_y %d/%d", this->x_raw_max_, this->y_raw_max_);
if (this->x_raw_max_ == this->x_raw_min_) {
this->x_raw_max_ = encode_uint16(data[1], data[0]);
}
if (this->y_raw_max_ == this->y_raw_min_) {
this->y_raw_max_ = encode_uint16(data[3], data[2]);
}
esph_log_d(TAG, "calibration max_x/max_y %d/%d", this->x_raw_max_, this->y_raw_max_);
}
}
}

View File

@ -0,0 +1,70 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import binary_sensor
from esphome.const import (
ENTITY_CATEGORY_DIAGNOSTIC,
ICON_FAN,
ICON_RADIATOR,
)
from ..climate import (
CONF_HAIER_ID,
HonClimate,
)
BinarySensorTypeEnum = HonClimate.enum("SubBinarySensorType", True)
# Haier sensors
CONF_OUTDOOR_FAN_STATUS = "outdoor_fan_status"
CONF_DEFROST_STATUS = "defrost_status"
CONF_COMPRESSOR_STATUS = "compressor_status"
CONF_INDOOR_FAN_STATUS = "indoor_fan_status"
CONF_FOUR_WAY_VALVE_STATUS = "four_way_valve_status"
CONF_INDOOR_ELECTRIC_HEATING_STATUS = "indoor_electric_heating_status"
# Additional icons
ICON_SNOWFLAKE_THERMOMETER = "mdi:snowflake-thermometer"
ICON_HVAC = "mdi:hvac"
ICON_VALVE = "mdi:valve"
SENSOR_TYPES = {
CONF_OUTDOOR_FAN_STATUS: binary_sensor.binary_sensor_schema(
icon=ICON_FAN,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
CONF_DEFROST_STATUS: binary_sensor.binary_sensor_schema(
icon=ICON_SNOWFLAKE_THERMOMETER,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
CONF_COMPRESSOR_STATUS: binary_sensor.binary_sensor_schema(
icon=ICON_HVAC,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
CONF_INDOOR_FAN_STATUS: binary_sensor.binary_sensor_schema(
icon=ICON_FAN,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
CONF_FOUR_WAY_VALVE_STATUS: binary_sensor.binary_sensor_schema(
icon=ICON_VALVE,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
CONF_INDOOR_ELECTRIC_HEATING_STATUS: binary_sensor.binary_sensor_schema(
icon=ICON_RADIATOR,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
}
CONFIG_SCHEMA = cv.Schema(
{
cv.Required(CONF_HAIER_ID): cv.use_id(HonClimate),
}
).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):
sens = await binary_sensor.new_binary_sensor(conf)
binary_sensor_type = getattr(BinarySensorTypeEnum, type.upper())
cg.add(paren.set_sub_binary_sensor(binary_sensor_type, sens))

View File

@ -2,7 +2,7 @@
import esphome.codegen as cg
import esphome.config_validation as cv
import esphome.final_validate as fv
from esphome.components import uart, sensor, climate, logger
from esphome.components import uart, climate, logger
from esphome import automation
from esphome.const import (
CONF_BEEPER,
@ -21,10 +21,6 @@ from esphome.const import (
CONF_TRIGGER_ID,
CONF_VISUAL,
CONF_WIFI,
DEVICE_CLASS_TEMPERATURE,
ICON_THERMOMETER,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
)
from esphome.components.climate import (
ClimateMode,
@ -42,7 +38,6 @@ PROTOCOL_CURRENT_TEMPERATURE_STEP = 0.5
PROTOCOL_CONTROL_PACKET_SIZE = 10
CODEOWNERS = ["@paveldn"]
AUTO_LOAD = ["sensor"]
DEPENDENCIES = ["climate", "uart"]
CONF_ALTERNATIVE_SWING_CONTROL = "alternative_swing_control"
CONF_ANSWER_TIMEOUT = "answer_timeout"
@ -58,7 +53,6 @@ CONF_WIFI_SIGNAL = "wifi_signal"
PROTOCOL_HON = "HON"
PROTOCOL_SMARTAIR2 = "SMARTAIR2"
PROTOCOLS_SUPPORTED = [PROTOCOL_HON, PROTOCOL_SMARTAIR2]
haier_ns = cg.esphome_ns.namespace("haier")
HaierClimateBase = haier_ns.class_(
@ -67,6 +61,7 @@ HaierClimateBase = haier_ns.class_(
HonClimate = haier_ns.class_("HonClimate", HaierClimateBase)
Smartair2Climate = haier_ns.class_("Smartair2Climate", HaierClimateBase)
CONF_HAIER_ID = "haier_id"
AirflowVerticalDirection = haier_ns.enum("AirflowVerticalDirection", True)
AIRFLOW_VERTICAL_DIRECTION_OPTIONS = {
@ -239,12 +234,8 @@ CONFIG_SCHEMA = cv.All(
): cv.ensure_list(
cv.enum(SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS, upper=True)
),
cv.Optional(CONF_OUTDOOR_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
icon=ICON_THERMOMETER,
accuracy_decimals=0,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
cv.Optional(CONF_OUTDOOR_TEMPERATURE): cv.invalid(
f"The {CONF_OUTDOOR_TEMPERATURE} option is deprecated, use a sensor for a haier platform instead"
),
cv.Optional(CONF_ON_ALARM_START): automation.validate_automation(
{
@ -463,9 +454,6 @@ async def to_code(config):
cg.add(var.set_beeper_state(config[CONF_BEEPER]))
if CONF_DISPLAY in config:
cg.add(var.set_display_state(config[CONF_DISPLAY]))
if CONF_OUTDOOR_TEMPERATURE in config:
sens = await sensor.new_sensor(config[CONF_OUTDOOR_TEMPERATURE])
cg.add(var.set_outdoor_temperature_sensor(sens))
if CONF_SUPPORTED_MODES in config:
cg.add(var.set_supported_modes(config[CONF_SUPPORTED_MODES]))
if CONF_SUPPORTED_SWING_MODES in config:

View File

@ -2,6 +2,7 @@
#include <string>
#include "esphome/components/climate/climate.h"
#include "esphome/components/uart/uart.h"
#include "esphome/core/helpers.h"
#include "hon_climate.h"
#include "hon_packet.h"
@ -51,10 +52,9 @@ hon_protocol::HorizontalSwingMode get_horizontal_swing_mode(AirflowHorizontalDir
}
HonClimate::HonClimate()
: cleaning_status_(CleaningState::NO_CLEANING),
got_valid_outdoor_temp_(false),
active_alarms_{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
outdoor_sensor_(nullptr) {
: 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;
@ -66,8 +66,6 @@ void HonClimate::set_beeper_state(bool state) { this->beeper_status_ = state; }
bool HonClimate::get_beeper_state() const { return this->beeper_status_; }
void HonClimate::set_outdoor_temperature_sensor(esphome::sensor::Sensor *sensor) { this->outdoor_sensor_ = sensor; }
AirflowVerticalDirection HonClimate::get_vertical_airflow() const { return this->vertical_direction_; };
void HonClimate::set_vertical_airflow(AirflowVerticalDirection direction) {
@ -368,7 +366,14 @@ void HonClimate::process_phase(std::chrono::steady_clock::time_point now) {
if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
static const haier_protocol::HaierMessage STATUS_REQUEST(
haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::GET_USER_DATA);
this->send_message_(STATUS_REQUEST, this->use_crc_);
static const haier_protocol::HaierMessage BIG_DATA_REQUEST(
haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::GET_BIG_DATA);
if ((this->protocol_phase_ == ProtocolPhases::SENDING_FIRST_STATUS_REQUEST) ||
(!this->should_get_big_data_())) {
this->send_message_(STATUS_REQUEST, this->use_crc_);
} else {
this->send_message_(BIG_DATA_REQUEST, this->use_crc_);
}
this->last_status_request_ = now;
}
break;
@ -685,9 +690,87 @@ void HonClimate::process_alarm_message_(const uint8_t *packet, uint8_t size, boo
}
}
#ifdef USE_SENSOR
void HonClimate::set_sub_sensor(SubSensorType type, sensor::Sensor *sens) {
if (type < SubSensorType::SUB_SENSOR_TYPE_COUNT) {
if (type >= SubSensorType::BIG_DATA_FRAME_SUB_SENSORS) {
if ((this->sub_sensors_[(size_t) type] != nullptr) && (sens == nullptr)) {
this->big_data_sensors_--;
} else if ((this->sub_sensors_[(size_t) type] == nullptr) && (sens != nullptr)) {
this->big_data_sensors_++;
}
}
this->sub_sensors_[(size_t) type] = sens;
}
}
void HonClimate::update_sub_sensor_(SubSensorType type, float value) {
if (type < SubSensorType::SUB_SENSOR_TYPE_COUNT) {
size_t index = (size_t) type;
if ((this->sub_sensors_[index] != nullptr) &&
((!this->sub_sensors_[index]->has_state()) || (this->sub_sensors_[index]->raw_state != value)))
this->sub_sensors_[index]->publish_state(value);
}
}
#endif // USE_SENSOR
#ifdef USE_BINARY_SENSOR
void HonClimate::set_sub_binary_sensor(SubBinarySensorType type, binary_sensor::BinarySensor *sens) {
if (type < SubBinarySensorType::SUB_BINARY_SENSOR_TYPE_COUNT) {
if ((this->sub_binary_sensors_[(size_t) type] != nullptr) && (sens == nullptr)) {
this->big_data_sensors_--;
} else if ((this->sub_binary_sensors_[(size_t) type] == nullptr) && (sens != nullptr)) {
this->big_data_sensors_++;
}
this->sub_binary_sensors_[(size_t) type] = sens;
}
}
void HonClimate::update_sub_binary_sensor_(SubBinarySensorType type, uint8_t value) {
if (value < 2) {
bool converted_value = value == 1;
size_t index = (size_t) type;
if ((this->sub_binary_sensors_[index] != nullptr) && ((!this->sub_binary_sensors_[index]->has_state()) ||
(this->sub_binary_sensors_[index]->state != converted_value)))
this->sub_binary_sensors_[index]->publish_state(converted_value);
}
}
#endif // USE_BINARY_SENSOR
haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *packet_buffer, uint8_t size) {
if (size < hon_protocol::HAIER_STATUS_FRAME_SIZE + this->extra_control_packet_bytes_)
size_t expected_size = 2 + sizeof(hon_protocol::HaierPacketControl) + sizeof(hon_protocol::HaierPacketSensors) +
this->extra_control_packet_bytes_;
if (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))) {
// Got BigData packet
const hon_protocol::HaierPacketBigData *bd_packet =
(const hon_protocol::HaierPacketBigData *) (&packet_buffer[expected_size + 4]);
#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);
this->update_sub_sensor_(SubSensorType::OUTDOOR_DEFROST_TEMPERATURE, bd_packet->outdoor_coil_temperature - 64);
this->update_sub_sensor_(SubSensorType::OUTDOOR_IN_AIR_TEMPERATURE, bd_packet->outdoor_in_air_temperature - 64);
this->update_sub_sensor_(SubSensorType::OUTDOOR_OUT_AIR_TEMPERATURE, bd_packet->outdoor_out_air_temperature - 64);
this->update_sub_sensor_(SubSensorType::POWER, encode_uint16(bd_packet->power[0], bd_packet->power[1]));
this->update_sub_sensor_(SubSensorType::COMPRESSOR_FREQUENCY, bd_packet->compressor_frequency);
this->update_sub_sensor_(SubSensorType::COMPRESSOR_CURRENT,
encode_uint16(bd_packet->compressor_current[0], bd_packet->compressor_current[1]) / 10.0);
this->update_sub_sensor_(
SubSensorType::EXPANSION_VALVE_OPEN_DEGREE,
encode_uint16(bd_packet->expansion_valve_open_degree[0], bd_packet->expansion_valve_open_degree[1]) / 4095.0);
#endif // USE_SENSOR
#ifdef USE_BINARY_SENSOR
this->update_sub_binary_sensor_(SubBinarySensorType::OUTDOOR_FAN_STATUS, bd_packet->outdoor_fan_status);
this->update_sub_binary_sensor_(SubBinarySensorType::DEFROST_STATUS, bd_packet->defrost_status);
this->update_sub_binary_sensor_(SubBinarySensorType::COMPRESSOR_STATUS, bd_packet->compressor_status);
this->update_sub_binary_sensor_(SubBinarySensorType::INDOOR_FAN_STATUS, bd_packet->indoor_fan_status);
this->update_sub_binary_sensor_(SubBinarySensorType::FOUR_WAY_VALVE_STATUS, bd_packet->four_way_valve_status);
this->update_sub_binary_sensor_(SubBinarySensorType::INDOOR_ELECTRIC_HEATING_STATUS,
bd_packet->indoor_electric_heating_status);
#endif // USE_BINARY_SENSOR
}
struct {
hon_protocol::HaierPacketControl control;
hon_protocol::HaierPacketSensors sensors;
@ -699,13 +782,17 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
if (packet.sensors.error_status != 0) {
ESP_LOGW(TAG, "HVAC error, code=0x%02X", packet.sensors.error_status);
}
if ((this->outdoor_sensor_ != nullptr) &&
#ifdef USE_SENSOR
if ((this->sub_sensors_[(size_t) SubSensorType::OUTDOOR_TEMPERATURE] != nullptr) &&
(this->got_valid_outdoor_temp_ || (packet.sensors.outdoor_temperature > 0))) {
this->got_valid_outdoor_temp_ = true;
float otemp = (float) (packet.sensors.outdoor_temperature + PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET);
if ((!this->outdoor_sensor_->has_state()) || (this->outdoor_sensor_->get_raw_state() != otemp))
this->outdoor_sensor_->publish_state(otemp);
this->update_sub_sensor_(SubSensorType::OUTDOOR_TEMPERATURE,
(float) (packet.sensors.outdoor_temperature + PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET));
}
if ((this->sub_sensors_[(size_t) SubSensorType::HUMIDITY] != nullptr) && (packet.sensors.room_humidity <= 100)) {
this->update_sub_sensor_(SubSensorType::HUMIDITY, (float) packet.sensors.room_humidity);
}
#endif // USE_SENSOR
bool should_publish = false;
{
// Extra modes/presets
@ -1009,21 +1096,22 @@ void HonClimate::fill_control_messages_queue_() {
break;
}
}
if (quiet_mode_buf[1] != 0xFF) {
auto presets = this->traits_.get_supported_presets();
if ((quiet_mode_buf[1] != 0xFF) && ((presets.find(climate::ClimatePreset::CLIMATE_PRESET_ECO) != presets.end()))) {
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::QUIET_MODE,
quiet_mode_buf, 2));
}
if (fast_mode_buf[1] != 0xFF) {
if ((fast_mode_buf[1] != 0xFF) && ((presets.find(climate::ClimatePreset::CLIMATE_PRESET_BOOST) != presets.end()))) {
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::FAST_MODE,
fast_mode_buf, 2));
}
if (away_mode_buf[1] != 0xFF) {
if ((away_mode_buf[1] != 0xFF) && ((presets.find(climate::ClimatePreset::CLIMATE_PRESET_AWAY) != presets.end()))) {
this->control_messages_queue_.push(
haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
@ -1032,7 +1120,7 @@ void HonClimate::fill_control_messages_queue_() {
}
}
// Target temperature
if (climate_control.target_temperature.has_value()) {
if (climate_control.target_temperature.has_value() && (this->mode != ClimateMode::CLIMATE_MODE_FAN_ONLY)) {
uint8_t buffer[2] = {0x00, 0x00};
buffer[1] = ((uint8_t) climate_control.target_temperature.value()) - 16;
this->control_messages_queue_.push(
@ -1119,12 +1207,24 @@ bool HonClimate::prepare_pending_action() {
void HonClimate::process_protocol_reset() {
HaierClimateBase::process_protocol_reset();
if (this->outdoor_sensor_ != nullptr) {
this->outdoor_sensor_->publish_state(NAN);
#ifdef USE_SENSOR
for (auto &sub_sensor : this->sub_sensors_) {
if ((sub_sensor != nullptr) && sub_sensor->has_state())
sub_sensor->publish_state(NAN);
}
#endif // USE_SENSOR
this->got_valid_outdoor_temp_ = false;
this->hvac_hardware_info_.reset();
}
bool HonClimate::should_get_big_data_() {
if (this->big_data_sensors_ > 0) {
static uint8_t counter = 0;
counter = (counter + 1) % 3;
return counter == 1;
}
return false;
}
} // namespace haier
} // namespace esphome

View File

@ -1,7 +1,12 @@
#pragma once
#include <chrono>
#ifdef USE_SENSOR
#include "esphome/components/sensor/sensor.h"
#endif
#ifdef USE_BINARY_SENSOR
#include "esphome/components/binary_sensor/binary_sensor.h"
#endif
#include "esphome/core/automation.h"
#include "haier_base.h"
@ -34,6 +39,48 @@ enum class CleaningState : uint8_t {
enum class HonControlMethod { MONITOR_ONLY = 0, SET_GROUP_PARAMETERS, SET_SINGLE_PARAMETER };
class HonClimate : public HaierClimateBase {
#ifdef USE_SENSOR
public:
enum class SubSensorType {
// Used data based sensors
OUTDOOR_TEMPERATURE = 0,
HUMIDITY,
// Big data based sensors
INDOOR_COIL_TEMPERATURE,
OUTDOOR_COIL_TEMPERATURE,
OUTDOOR_DEFROST_TEMPERATURE,
OUTDOOR_IN_AIR_TEMPERATURE,
OUTDOOR_OUT_AIR_TEMPERATURE,
POWER,
COMPRESSOR_FREQUENCY,
COMPRESSOR_CURRENT,
EXPANSION_VALVE_OPEN_DEGREE,
SUB_SENSOR_TYPE_COUNT,
BIG_DATA_FRAME_SUB_SENSORS = INDOOR_COIL_TEMPERATURE,
};
void set_sub_sensor(SubSensorType type, sensor::Sensor *sens);
protected:
void update_sub_sensor_(SubSensorType type, float value);
sensor::Sensor *sub_sensors_[(size_t) SubSensorType::SUB_SENSOR_TYPE_COUNT]{nullptr};
#endif
#ifdef USE_BINARY_SENSOR
public:
enum class SubBinarySensorType {
OUTDOOR_FAN_STATUS = 0,
DEFROST_STATUS,
COMPRESSOR_STATUS,
INDOOR_FAN_STATUS,
FOUR_WAY_VALVE_STATUS,
INDOOR_ELECTRIC_HEATING_STATUS,
SUB_BINARY_SENSOR_TYPE_COUNT,
};
void set_sub_binary_sensor(SubBinarySensorType type, binary_sensor::BinarySensor *sens);
protected:
void update_sub_binary_sensor_(SubBinarySensorType type, uint8_t value);
binary_sensor::BinarySensor *sub_binary_sensors_[(size_t) SubBinarySensorType::SUB_BINARY_SENSOR_TYPE_COUNT]{nullptr};
#endif
public:
HonClimate();
HonClimate(const HonClimate &) = delete;
@ -42,7 +89,6 @@ class HonClimate : public HaierClimateBase {
void dump_config() override;
void set_beeper_state(bool state);
bool get_beeper_state() const;
void set_outdoor_temperature_sensor(esphome::sensor::Sensor *sensor);
AirflowVerticalDirection get_vertical_airflow() const;
void set_vertical_airflow(AirflowVerticalDirection direction);
AirflowHorizontalDirection get_horizontal_airflow() const;
@ -64,6 +110,7 @@ class HonClimate : public HaierClimateBase {
haier_protocol::HaierMessage get_power_message(bool state) override;
bool prepare_pending_action() override;
void process_protocol_reset() override;
bool should_get_big_data_();
// Answers handlers
haier_protocol::HandlerError get_device_version_answer_handler_(haier_protocol::FrameType request_type,
@ -106,12 +153,12 @@ class HonClimate : public HaierClimateBase {
uint8_t active_alarms_[8];
int extra_control_packet_bytes_;
HonControlMethod control_method_;
esphome::sensor::Sensor *outdoor_sensor_;
std::queue<haier_protocol::HaierMessage> control_messages_queue_;
CallbackManager<void(uint8_t, const char *)> alarm_start_callback_{};
CallbackManager<void(uint8_t, const char *)> alarm_end_callback_{};
float active_alarm_count_{NAN};
std::chrono::steady_clock::time_point last_alarm_request_;
int big_data_sensors_{0};
};
class HaierAlarmStartTrigger : public Trigger<uint8_t, const char *> {

View File

@ -55,18 +55,18 @@ enum class FanMode : uint8_t { FAN_HIGH = 0x01, FAN_MID = 0x02, FAN_LOW = 0x03,
struct HaierPacketControl {
// Control bytes starts here
// 10
// 1
uint8_t set_point; // Target temperature with 16°C offset (0x00 = 16°C)
// 11
// 2
uint8_t vertical_swing_mode : 4; // See enum VerticalSwingMode
uint8_t : 0;
// 12
// 3
uint8_t fan_mode : 3; // See enum FanMode
uint8_t special_mode : 2; // See enum SpecialMode
uint8_t ac_mode : 3; // See enum ConditioningMode
// 13
// 4
uint8_t : 8;
// 14
// 5
uint8_t ten_degree : 1; // 10 degree status
uint8_t display_status : 1; // If 0 disables AC's display
uint8_t half_degree : 1; // Use half degree
@ -75,7 +75,7 @@ struct HaierPacketControl {
uint8_t use_fahrenheit : 1; // Use Fahrenheit instead of Celsius
uint8_t : 1;
uint8_t steri_clean : 1;
// 15
// 6
uint8_t ac_power : 1; // Is ac on or off
uint8_t health_mode : 1; // Health mode (negative ions) on or off
uint8_t electric_heating_status : 1; // Electric heating status
@ -84,16 +84,16 @@ struct HaierPacketControl {
uint8_t sleep_mode : 1; // Sleep mode
uint8_t lock_remote : 1; // Disable remote
uint8_t beeper_status : 1; // If 1 disables AC's command feedback beeper (need to be set on every control command)
// 16
// 7
uint8_t target_humidity; // Target humidity (0=30% .. 3C=90%, step = 1%)
// 17
// 8
uint8_t horizontal_swing_mode : 3; // See enum HorizontalSwingMode
uint8_t : 3;
uint8_t human_sensing_status : 2; // Human sensing status
// 18
// 9
uint8_t change_filter : 1; // Filter need replacement
uint8_t : 0;
// 19
// 10
uint8_t fresh_air_status : 1; // Fresh air status
uint8_t humidification_status : 1; // Humidification status
uint8_t pm2p5_cleaning_status : 1; // PM2.5 cleaning status
@ -105,40 +105,68 @@ struct HaierPacketControl {
};
struct HaierPacketSensors {
// 20
// 11
uint8_t room_temperature; // 0.5°C step
// 21
// 12
uint8_t room_humidity; // 0%-100% with 1% step
// 22
// 13
uint8_t outdoor_temperature; // 1°C step, -64°C offset (0=-64°C)
// 23
// 14
uint8_t pm2p5_level : 2; // Indoor PM2.5 grade (00: Excellent, 01: good, 02: Medium, 03: Bad)
uint8_t air_quality : 2; // Air quality grade (00: Excellent, 01: good, 02: Medium, 03: Bad)
uint8_t human_sensing : 2; // Human presence result (00: N/A, 01: not detected, 02: One, 03: Multiple)
uint8_t : 1;
uint8_t ac_type : 1; // 00 - Heat and cool, 01 - Cool only)
// 24
// 15
uint8_t error_status; // See enum ErrorStatus
// 25
// 16
uint8_t operation_source : 2; // who is controlling AC (00: Other, 01: Remote control, 02: Button, 03: ESP)
uint8_t operation_mode_hk : 2; // Homekit only, operation mode (00: Cool, 01: Dry, 02: Heat, 03: Fan)
uint8_t : 3;
uint8_t err_confirmation : 1; // If 1 clear error status
// 26
// 17
uint16_t total_cleaning_time; // Cleaning cumulative time (1h step)
// 28
// 19
uint16_t indoor_pm2p5_value; // Indoor PM2.5 value (0 ug/m3 - 4095 ug/m3, 1 ug/m3 step)
// 30
// 21
uint16_t outdoor_pm2p5_value; // Outdoor PM2.5 value (0 ug/m3 - 4095 ug/m3, 1 ug/m3 step)
// 32
// 23
uint16_t ch2o_value; // Formaldehyde value (0 ug/m3 - 10000 ug/m3, 1 ug/m3 step)
// 34
// 25
uint16_t voc_value; // VOC value (Volatile Organic Compounds) (0 ug/m3 - 1023 ug/m3, 1 ug/m3 step)
// 36
// 27
uint16_t co2_value; // CO2 value (0 PPM - 10000 PPM, 1 PPM step)
};
constexpr size_t HAIER_STATUS_FRAME_SIZE = 2 + sizeof(HaierPacketControl) + sizeof(HaierPacketSensors);
struct HaierPacketBigData {
// 29
uint8_t power[2]; // AC power consumption (0W - 65535W, 1W step)
// 31
uint8_t indoor_coil_temperature; // 0.5°C step, -20°C offset (0=-20°C)
// 32
uint8_t outdoor_out_air_temperature; // 1°C step, -64°C offset (0=-64°C)
// 33
uint8_t outdoor_coil_temperature; // 1°C step, -64°C offset (0=-64°C)
// 34
uint8_t outdoor_in_air_temperature; // 1°C step, -64°C offset (0=-64°C)
// 35
uint8_t outdoor_defrost_temperature; // 1°C step, -64°C offset (0=-64°C)
// 36
uint8_t compressor_frequency; // 1Hz step, 0Hz - 127Hz
// 37
uint8_t compressor_current[2]; // 0.1A step, 0.0A - 51.1A (0x0000 - 0x01FF)
// 39
uint8_t outdoor_fan_status : 2; // 0 - off, 1 - on, 2 - information not available
uint8_t defrost_status : 2; // 0 - off, 1 - on, 2 - information not available
uint8_t : 0;
// 40
uint8_t compressor_status : 2; // 0 - off, 1 - on, 2 - information not available
uint8_t indoor_fan_status : 2; // 0 - off, 1 - on, 2 - information not available
uint8_t four_way_valve_status : 2; // 0 - off, 1 - on, 2 - information not available
uint8_t indoor_electric_heating_status : 2; // 0 - off, 1 - on, 2 - information not available
// 41
uint8_t expansion_valve_open_degree[2]; // 0 - 4095
};
struct DeviceVersionAnswer {
char protocol_version[8];

View File

@ -0,0 +1,151 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor
from esphome.const import (
CONF_POWER,
CONF_HUMIDITY,
DEVICE_CLASS_CURRENT,
DEVICE_CLASS_FREQUENCY,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_POWER,
DEVICE_CLASS_TEMPERATURE,
ENTITY_CATEGORY_DIAGNOSTIC,
ICON_CURRENT_AC,
ICON_FLASH,
ICON_GAUGE,
ICON_HEATING_COIL,
ICON_PULSE,
ICON_THERMOMETER,
ICON_WATER_PERCENT,
ICON_WEATHER_WINDY,
STATE_CLASS_MEASUREMENT,
UNIT_AMPERE,
UNIT_CELSIUS,
UNIT_HERTZ,
UNIT_PERCENT,
UNIT_WATT,
)
from ..climate import (
CONF_HAIER_ID,
HonClimate,
)
SensorTypeEnum = HonClimate.enum("SubSensorType", True)
# Haier sensors
CONF_COMPRESSOR_CURRENT = "compressor_current"
CONF_COMPRESSOR_FREQUENCY = "compressor_frequency"
CONF_EXPANSION_VALVE_OPEN_DEGREE = "expansion_valve_open_degree"
CONF_INDOOR_COIL_TEMPERATURE = "indoor_coil_temperature"
CONF_OUTDOOR_COIL_TEMPERATURE = "outdoor_coil_temperature"
CONF_OUTDOOR_DEFROST_TEMPERATURE = "outdoor_defrost_temperature"
CONF_OUTDOOR_IN_AIR_TEMPERATURE = "outdoor_in_air_temperature"
CONF_OUTDOOR_OUT_AIR_TEMPERATURE = "outdoor_out_air_temperature"
CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature"
# Additional icons
ICON_SNOWFLAKE_THERMOMETER = "mdi:snowflake-thermometer"
SENSOR_TYPES = {
CONF_COMPRESSOR_CURRENT: sensor.sensor_schema(
unit_of_measurement=UNIT_AMPERE,
icon=ICON_CURRENT_AC,
accuracy_decimals=1,
device_class=DEVICE_CLASS_CURRENT,
state_class=STATE_CLASS_MEASUREMENT,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
CONF_COMPRESSOR_FREQUENCY: sensor.sensor_schema(
unit_of_measurement=UNIT_HERTZ,
icon=ICON_PULSE,
accuracy_decimals=0,
device_class=DEVICE_CLASS_FREQUENCY,
state_class=STATE_CLASS_MEASUREMENT,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
CONF_EXPANSION_VALVE_OPEN_DEGREE: sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
icon=ICON_GAUGE,
accuracy_decimals=2,
state_class=STATE_CLASS_MEASUREMENT,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
CONF_HUMIDITY: sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
icon=ICON_WATER_PERCENT,
accuracy_decimals=0,
device_class=DEVICE_CLASS_HUMIDITY,
state_class=STATE_CLASS_MEASUREMENT,
),
CONF_INDOOR_COIL_TEMPERATURE: sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
icon=ICON_HEATING_COIL,
accuracy_decimals=0,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
CONF_OUTDOOR_COIL_TEMPERATURE: sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
icon=ICON_HEATING_COIL,
accuracy_decimals=0,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
CONF_OUTDOOR_DEFROST_TEMPERATURE: sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
icon=ICON_SNOWFLAKE_THERMOMETER,
accuracy_decimals=0,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
CONF_OUTDOOR_IN_AIR_TEMPERATURE: sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
icon=ICON_WEATHER_WINDY,
accuracy_decimals=0,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
CONF_OUTDOOR_OUT_AIR_TEMPERATURE: sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
icon=ICON_WEATHER_WINDY,
accuracy_decimals=0,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
CONF_OUTDOOR_TEMPERATURE: sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
icon=ICON_THERMOMETER,
accuracy_decimals=0,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
),
CONF_POWER: sensor.sensor_schema(
unit_of_measurement=UNIT_WATT,
icon=ICON_FLASH,
accuracy_decimals=0,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
}
CONFIG_SCHEMA = cv.Schema(
{
cv.Required(CONF_HAIER_ID): cv.use_id(HonClimate),
}
).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):
sens = await sensor.new_sensor(conf)
sensor_type = getattr(SensorTypeEnum, type.upper())
cg.add(paren.set_sub_sensor(sensor_type, sens))

View File

@ -119,4 +119,4 @@ def to_code(config):
cg.add_library("tonia/HeatpumpIR", "1.0.23")
if CORE.is_esp8266 or CORE.is_esp32:
cg.add_library("crankyoldgit/IRremoteESP8266", "2.7.12")
cg.add_library("crankyoldgit/IRremoteESP8266", "2.8.4")

View File

@ -1,2 +1,3 @@
"""Support for Honeywell HumidIcon HIH"""
CODEOWNERS = ["@Benichou34"]

View File

@ -1,2 +1,3 @@
"""Support for Honeywell ABP2"""
CODEOWNERS = ["@jpfaff"]

View File

@ -4,6 +4,7 @@ from esphome.const import (
KEY_TARGET_FRAMEWORK,
KEY_TARGET_PLATFORM,
PLATFORM_HOST,
CONF_MAC_ADDRESS,
)
from esphome.core import CORE
from esphome.helpers import IS_MACOS
@ -28,13 +29,18 @@ def set_core_data(config):
CONFIG_SCHEMA = cv.All(
cv.Schema({}),
cv.Schema(
{
cv.Optional(CONF_MAC_ADDRESS, default="98:35:69:ab:f6:79"): cv.mac_address,
}
),
set_core_data,
)
async def to_code(config):
cg.add_build_flag("-DUSE_HOST")
cg.add_define("USE_ESPHOME_HOST_MAC_ADDRESS", config[CONF_MAC_ADDRESS].parts)
cg.add_build_flag("-std=c++17")
cg.add_build_flag("-lsodium")
if IS_MACOS:

View File

@ -24,7 +24,7 @@ void ArduinoI2CBus::setup() {
}
next_bus_num++;
#elif defined(USE_ESP8266)
wire_ = &Wire; // NOLINT(cppcoreguidelines-prefer-member-initializer)
wire_ = new TwoWire(); // NOLINT(cppcoreguidelines-owning-memory)
#elif defined(USE_RP2040)
static bool first = true;
if (first) {
@ -35,6 +35,16 @@ void ArduinoI2CBus::setup() {
}
#endif
this->set_pins_and_clock_();
this->initialized_ = true;
if (this->scan_) {
ESP_LOGV(TAG, "Scanning i2c bus for active devices...");
this->i2c_scan_();
}
}
void ArduinoI2CBus::set_pins_and_clock_() {
#ifdef USE_RP2040
wire_->setSDA(this->sda_pin_);
wire_->setSCL(this->scl_pin_);
@ -43,12 +53,8 @@ void ArduinoI2CBus::setup() {
wire_->begin(static_cast<int>(sda_pin_), static_cast<int>(scl_pin_));
#endif
wire_->setClock(frequency_);
initialized_ = true;
if (this->scan_) {
ESP_LOGV(TAG, "Scanning i2c bus for active devices...");
this->i2c_scan_();
}
}
void ArduinoI2CBus::dump_config() {
ESP_LOGCONFIG(TAG, "I2C Bus:");
ESP_LOGCONFIG(TAG, " SDA Pin: GPIO%u", this->sda_pin_);
@ -82,6 +88,10 @@ void ArduinoI2CBus::dump_config() {
}
ErrorCode ArduinoI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt) {
#if defined(USE_ESP8266)
this->set_pins_and_clock_(); // reconfigure Wire global state in case there are multiple instances
#endif
// logging is only enabled with vv level, if warnings are shown the caller
// should log them
if (!initialized_) {
@ -120,6 +130,10 @@ ErrorCode ArduinoI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt)
return ERROR_OK;
}
ErrorCode ArduinoI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cnt, bool stop) {
#if defined(USE_ESP8266)
this->set_pins_and_clock_(); // reconfigure Wire global state in case there are multiple instances
#endif
// logging is only enabled with vv level, if warnings are shown the caller
// should log them
if (!initialized_) {
@ -164,7 +178,7 @@ ErrorCode ArduinoI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cn
return ERROR_UNKNOWN;
case 2:
case 3:
ESP_LOGVV(TAG, "TX failed: not acknowledged");
ESP_LOGVV(TAG, "TX failed: not acknowledged: %d", status);
return ERROR_NOT_ACKNOWLEDGED;
case 5:
ESP_LOGVV(TAG, "TX failed: timeout");

View File

@ -30,6 +30,7 @@ class ArduinoI2CBus : public I2CBus, public Component {
private:
void recover_();
void set_pins_and_clock_();
RecoveryCode recovery_result_;
protected:

View File

@ -21,8 +21,13 @@ std::string ImprovBase::get_formatted_next_url_() {
// Ip address
pos = this->next_url_.find("{{ip_address}}");
if (pos != std::string::npos) {
std::string ip = network::get_ip_address().str();
copy.replace(pos, 14, ip);
for (auto &ip : network::get_ip_addresses()) {
if (ip.is_ip4()) {
std::string ipa = ip.str();
copy.replace(pos, 14, ipa);
break;
}
}
}
return copy;

View File

@ -155,9 +155,13 @@ std::vector<uint8_t> ImprovSerialComponent::build_rpc_settings_response_(improv:
urls.push_back(this->get_formatted_next_url_());
}
#ifdef USE_WEBSERVER
auto ip = wifi::global_wifi_component->wifi_sta_ip();
std::string webserver_url = "http://" + ip.str() + ":" + to_string(USE_WEBSERVER_PORT);
urls.push_back(webserver_url);
for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) {
if (ip.is_ip4()) {
std::string webserver_url = "http://" + ip.str() + ":" + to_string(USE_WEBSERVER_PORT);
urls.push_back(webserver_url);
break;
}
}
#endif
std::vector<uint8_t> data = improv::build_rpc_response(command, urls, false);
return data;
@ -192,7 +196,7 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
this->connecting_sta_ = sta;
wifi::global_wifi_component->set_sta(sta);
wifi::global_wifi_component->start_scanning();
wifi::global_wifi_component->start_connecting(sta, false);
this->set_state_(improv::STATE_PROVISIONING);
ESP_LOGD(TAG, "Received Improv wifi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(),
command.password.c_str());

View File

@ -0,0 +1 @@
CODEOWNERS = ["@Sergio303", "@latonita"]

View File

@ -33,31 +33,37 @@ static const uint8_t INA226_REGISTER_POWER = 0x03;
static const uint8_t INA226_REGISTER_CURRENT = 0x04;
static const uint8_t INA226_REGISTER_CALIBRATION = 0x05;
static const uint16_t INA226_ADC_TIMES[] = {140, 204, 332, 588, 1100, 2116, 4156, 8244};
static const uint16_t INA226_ADC_AVG_SAMPLES[] = {1, 4, 16, 64, 128, 256, 512, 1024};
void INA226Component::setup() {
ESP_LOGCONFIG(TAG, "Setting up INA226...");
// Config Register
// 0bx000000000000000 << 15 RESET Bit (1 -> trigger reset)
if (!this->write_byte_16(INA226_REGISTER_CONFIG, 0x8000)) {
ConfigurationRegister config;
config.reset = 1;
if (!this->write_byte_16(INA226_REGISTER_CONFIG, config.raw)) {
this->mark_failed();
return;
}
delay(1);
uint16_t config = 0x0000;
config.raw = 0;
config.reserved = 0b100; // as per datasheet
// Averaging Mode AVG Bit Settings[11:9] (000 -> 1 sample, 001 -> 4 sample, 111 -> 1024 samples)
config |= 0b0000001000000000;
config.avg_samples = this->adc_avg_samples_;
// Bus Voltage Conversion Time VBUSCT Bit Settings [8:6] (100 -> 1.1ms, 111 -> 8.244 ms)
config |= 0b0000000100000000;
config.bus_voltage_conversion_time = this->adc_time_;
// Shunt Voltage Conversion Time VSHCT Bit Settings [5:3] (100 -> 1.1ms, 111 -> 8.244 ms)
config |= 0b0000000000100000;
config.shunt_voltage_conversion_time = this->adc_time_;
// Mode Settings [2:0] Combinations (111 -> Shunt and Bus, Continuous)
config |= 0b0000000000000111;
config.mode = 0b111;
if (!this->write_byte_16(INA226_REGISTER_CONFIG, config)) {
if (!this->write_byte_16(INA226_REGISTER_CONFIG, config.raw)) {
this->mark_failed();
return;
}
@ -87,6 +93,9 @@ void INA226Component::dump_config() {
}
LOG_UPDATE_INTERVAL(this);
ESP_LOGCONFIG(TAG, " ADC Conversion Time: %d", INA226_ADC_TIMES[this->adc_time_ & 0b111]);
ESP_LOGCONFIG(TAG, " ADC Averaging Samples: %d", INA226_ADC_AVG_SAMPLES[this->adc_avg_samples_ & 0b111]);
LOG_SENSOR(" ", "Bus Voltage", this->bus_voltage_sensor_);
LOG_SENSOR(" ", "Shunt Voltage", this->shunt_voltage_sensor_);
LOG_SENSOR(" ", "Current", this->current_sensor_);
@ -102,7 +111,9 @@ void INA226Component::update() {
this->status_set_warning();
return;
}
float bus_voltage_v = int16_t(raw_bus_voltage) * 0.00125f;
// Convert for 2's compliment and signed value (though always positive)
float bus_voltage_v = this->twos_complement_(raw_bus_voltage, 16);
bus_voltage_v *= 0.00125f;
this->bus_voltage_sensor_->publish_state(bus_voltage_v);
}
@ -112,7 +123,9 @@ void INA226Component::update() {
this->status_set_warning();
return;
}
float shunt_voltage_v = int16_t(raw_shunt_voltage) * 0.0000025f;
// Convert for 2's compliment and signed value
float shunt_voltage_v = this->twos_complement_(raw_shunt_voltage, 16);
shunt_voltage_v *= 0.0000025f;
this->shunt_voltage_sensor_->publish_state(shunt_voltage_v);
}
@ -122,7 +135,9 @@ void INA226Component::update() {
this->status_set_warning();
return;
}
float current_ma = int16_t(raw_current) * (this->calibration_lsb_ / 1000.0f);
// Convert for 2's compliment and signed value
float current_ma = this->twos_complement_(raw_current, 16);
current_ma *= (this->calibration_lsb_ / 1000.0f);
this->current_sensor_->publish_state(current_ma / 1000.0f);
}
@ -139,5 +154,12 @@ void INA226Component::update() {
this->status_clear_warning();
}
int32_t INA226Component::twos_complement_(int32_t val, uint8_t bits) {
if (val & ((uint32_t) 1 << (bits - 1))) {
val -= (uint32_t) 1 << bits;
}
return val;
}
} // namespace ina226
} // namespace esphome

View File

@ -7,6 +7,40 @@
namespace esphome {
namespace ina226 {
enum AdcTime : uint16_t {
ADC_TIME_140US = 0,
ADC_TIME_204US = 1,
ADC_TIME_332US = 2,
ADC_TIME_588US = 3,
ADC_TIME_1100US = 4,
ADC_TIME_2116US = 5,
ADC_TIME_4156US = 6,
ADC_TIME_8244US = 7
};
enum AdcAvgSamples : uint16_t {
ADC_AVG_SAMPLES_1 = 0,
ADC_AVG_SAMPLES_4 = 1,
ADC_AVG_SAMPLES_16 = 2,
ADC_AVG_SAMPLES_64 = 3,
ADC_AVG_SAMPLES_128 = 4,
ADC_AVG_SAMPLES_256 = 5,
ADC_AVG_SAMPLES_512 = 6,
ADC_AVG_SAMPLES_1024 = 7
};
union ConfigurationRegister {
uint16_t raw;
struct {
uint16_t mode : 3;
AdcTime shunt_voltage_conversion_time : 3;
AdcTime bus_voltage_conversion_time : 3;
AdcAvgSamples avg_samples : 3;
uint16_t reserved : 3;
uint16_t reset : 1;
} __attribute__((packed));
};
class INA226Component : public PollingComponent, public i2c::I2CDevice {
public:
void setup() override;
@ -16,6 +50,9 @@ class INA226Component : public PollingComponent, public i2c::I2CDevice {
void set_shunt_resistance_ohm(float shunt_resistance_ohm) { shunt_resistance_ohm_ = shunt_resistance_ohm; }
void set_max_current_a(float max_current_a) { max_current_a_ = max_current_a; }
void set_adc_time(AdcTime time) { adc_time_ = time; }
void set_adc_avg_samples(AdcAvgSamples samples) { adc_avg_samples_ = samples; }
void set_bus_voltage_sensor(sensor::Sensor *bus_voltage_sensor) { bus_voltage_sensor_ = bus_voltage_sensor; }
void set_shunt_voltage_sensor(sensor::Sensor *shunt_voltage_sensor) { shunt_voltage_sensor_ = shunt_voltage_sensor; }
void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; }
@ -24,11 +61,15 @@ class INA226Component : public PollingComponent, public i2c::I2CDevice {
protected:
float shunt_resistance_ohm_;
float max_current_a_;
AdcTime adc_time_{AdcTime::ADC_TIME_1100US};
AdcAvgSamples adc_avg_samples_{AdcAvgSamples::ADC_AVG_SAMPLES_4};
uint32_t calibration_lsb_;
sensor::Sensor *bus_voltage_sensor_{nullptr};
sensor::Sensor *shunt_voltage_sensor_{nullptr};
sensor::Sensor *current_sensor_{nullptr};
sensor::Sensor *power_sensor_{nullptr};
int32_t twos_complement_(int32_t val, uint8_t bits);
};
} // namespace ina226

View File

@ -20,11 +20,44 @@ from esphome.const import (
DEPENDENCIES = ["i2c"]
CONF_ADC_AVERAGING = "adc_averaging"
CONF_ADC_TIME = "adc_time"
ina226_ns = cg.esphome_ns.namespace("ina226")
INA226Component = ina226_ns.class_(
"INA226Component", cg.PollingComponent, i2c.I2CDevice
)
AdcTime = ina226_ns.enum("AdcTime")
ADC_TIMES = {
140: AdcTime.ADC_TIME_140US,
204: AdcTime.ADC_TIME_204US,
332: AdcTime.ADC_TIME_332US,
588: AdcTime.ADC_TIME_588US,
1100: AdcTime.ADC_TIME_1100US,
2116: AdcTime.ADC_TIME_2116US,
4156: AdcTime.ADC_TIME_4156US,
8244: AdcTime.ADC_TIME_8244US,
}
AdcAvgSamples = ina226_ns.enum("AdcAvgSamples")
ADC_AVG_SAMPLES = {
1: AdcAvgSamples.ADC_AVG_SAMPLES_1,
4: AdcAvgSamples.ADC_AVG_SAMPLES_4,
16: AdcAvgSamples.ADC_AVG_SAMPLES_16,
64: AdcAvgSamples.ADC_AVG_SAMPLES_64,
128: AdcAvgSamples.ADC_AVG_SAMPLES_128,
256: AdcAvgSamples.ADC_AVG_SAMPLES_256,
512: AdcAvgSamples.ADC_AVG_SAMPLES_512,
1024: AdcAvgSamples.ADC_AVG_SAMPLES_1024,
}
def validate_adc_time(value):
value = cv.positive_time_period_microseconds(value).total_microseconds
return cv.enum(ADC_TIMES, int=True)(value)
CONFIG_SCHEMA = (
cv.Schema(
{
@ -59,6 +92,10 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_MAX_CURRENT, default=3.2): cv.All(
cv.current, cv.Range(min=0.0)
),
cv.Optional(CONF_ADC_TIME, default="1100 us"): validate_adc_time,
cv.Optional(CONF_ADC_AVERAGING, default=4): cv.enum(
ADC_AVG_SAMPLES, int=True
),
}
)
.extend(cv.polling_component_schema("60s"))
@ -72,8 +109,9 @@ async def to_code(config):
await i2c.register_i2c_device(var, config)
cg.add(var.set_shunt_resistance_ohm(config[CONF_SHUNT_RESISTANCE]))
cg.add(var.set_max_current_a(config[CONF_MAX_CURRENT]))
cg.add(var.set_adc_time(config[CONF_ADC_TIME]))
cg.add(var.set_adc_avg_samples(config[CONF_ADC_AVERAGING]))
if CONF_BUS_VOLTAGE in config:
sens = await sensor.new_sensor(config[CONF_BUS_VOLTAGE])

View File

@ -3,6 +3,7 @@ import esphome.config_validation as cv
from esphome.components import sensor, esp32_ble_tracker
from esphome.const import (
CONF_BATTERY_LEVEL,
CONF_EXTERNAL_TEMPERATURE,
CONF_HUMIDITY,
CONF_MAC_ADDRESS,
CONF_TEMPERATURE,
@ -19,8 +20,6 @@ from esphome.const import (
CODEOWNERS = ["@fkirill"]
DEPENDENCIES = ["esp32_ble_tracker"]
CONF_EXTERNAL_TEMPERATURE = "external_temperature"
inkbird_ibsth1_mini_ns = cg.esphome_ns.namespace("inkbird_ibsth1_mini")
InkbirdIbstH1Mini = inkbird_ibsth1_mini_ns.class_(
"InkbirdIbstH1Mini", esp32_ble_tracker.ESPBTDeviceListener, cg.Component

View File

@ -120,6 +120,7 @@ void LightState::loop() {
// Apply transformer (if any)
if (this->transformer_ != nullptr) {
auto values = this->transformer_->apply();
this->is_transformer_active_ = true;
if (values.has_value()) {
this->current_values = *values;
this->output_->update_state(this);
@ -131,6 +132,7 @@ void LightState::loop() {
this->current_values = this->transformer_->get_target_values();
this->transformer_->stop();
this->is_transformer_active_ = false;
this->transformer_ = nullptr;
this->target_state_reached_callback_.call();
}
@ -214,6 +216,8 @@ void LightState::current_values_as_ct(float *color_temperature, float *white_bri
this->gamma_correct_);
}
bool LightState::is_transformer_active() { return this->is_transformer_active_; }
void LightState::start_effect_(uint32_t effect_index) {
this->stop_effect_();
if (effect_index == 0)
@ -263,6 +267,7 @@ void LightState::start_flash_(const LightColorValues &target, uint32_t length, b
}
void LightState::set_immediately_(const LightColorValues &target, bool set_remote_values) {
this->is_transformer_active_ = false;
this->transformer_ = nullptr;
this->current_values = target;
if (set_remote_values) {

View File

@ -144,6 +144,17 @@ class LightState : public EntityBase, public Component {
void current_values_as_ct(float *color_temperature, float *white_brightness);
/**
* Indicator if a transformer (e.g. transition) is active. This is useful
* for effects e.g. at the start of the apply() method, add a check like:
*
* if (this->state_->is_transformer_active()) {
* // Something is already running.
* return;
* }
*/
bool is_transformer_active();
protected:
friend LightOutput;
friend LightCall;
@ -203,6 +214,9 @@ class LightState : public EntityBase, public Component {
LightRestoreMode restore_mode_;
/// List of effects for this light.
std::vector<LightEffect *> effects_;
// for effects, true if a transformer (transition) is active.
bool is_transformer_active_ = false;
};
} // namespace light

View File

@ -3,6 +3,8 @@
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include <vector>
namespace esphome {
namespace lightwaverf {

View File

@ -38,9 +38,14 @@ void LilygoT547Touchscreen::setup() {
}
this->write_register(POWER_REGISTER, WAKEUP_CMD, 1);
this->x_raw_max_ = this->get_width_();
this->y_raw_max_ = this->get_height_();
if (this->display_ != nullptr) {
if (this->x_raw_max_ == this->x_raw_min_) {
this->x_raw_max_ = this->display_->get_native_width();
}
if (this->y_raw_max_ == this->y_raw_min_) {
this->x_raw_max_ = this->display_->get_native_height();
}
}
}
void LilygoT547Touchscreen::update_touches() {

View File

@ -140,7 +140,9 @@ def uart_selection(value):
raise cv.Invalid(f"Arduino framework does not support {value}.")
variant = get_esp32_variant()
if CORE.using_esp_idf and variant == VARIANT_ESP32C3 and value == USB_CDC:
raise cv.Invalid(f"esp idf variant {variant} does not support {value}.")
raise cv.Invalid(
f"{value} is not supported for variant {variant} when using ESP-IDF."
)
if variant in UART_SELECTION_ESP32:
return cv.one_of(*UART_SELECTION_ESP32[variant], upper=True)(value)
if CORE.is_esp8266:
@ -292,6 +294,16 @@ async def to_code(config):
add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_CDC", True)
elif config[CONF_HARDWARE_UART] == USB_SERIAL_JTAG:
add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG", True)
try:
uart_selection(USB_SERIAL_JTAG)
cg.add_define("USE_LOGGER_USB_SERIAL_JTAG")
except cv.Invalid:
pass
try:
uart_selection(USB_CDC)
cg.add_define("USE_LOGGER_USB_CDC")
except cv.Invalid:
pass
try:
uart_selection(USB_SERIAL_JTAG)

View File

@ -161,26 +161,6 @@ Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate
#endif
}
#ifdef USE_USB_CDC
#ifndef USE_ZEPHYR
void Logger::loop() {
#ifdef USE_ARDUINO
if (this->uart_ != UART_SELECTION_USB_CDC) {
return;
}
static bool opened = false;
if (opened == Serial) {
return;
}
if (false == opened) {
App.schedule_dump_config();
}
opened = !opened;
#endif
}
#endif
#endif
void Logger::set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; }
void Logger::set_log_level(const std::string &tag, int log_level) {
this->log_levels_.push_back(LogLevelOverride{tag, log_level});

View File

@ -41,16 +41,14 @@ enum UARTSelection {
#else
UART_SELECTION_UART0 = 0,
#endif
#ifndef USE_NRF52
UART_SELECTION_UART1,
#endif
#if defined(USE_LIBRETINY) || defined(USE_ESP32_VARIANT_ESP32)
UART_SELECTION_UART2,
#endif
#ifdef USE_USB_CDC
#ifdef USE_LOGGER_USB_CDC
UART_SELECTION_USB_CDC,
#endif
#ifdef USE_USB_SERIAL_JTAG
#ifdef USE_LOGGER_USB_SERIAL_JTAG
UART_SELECTION_USB_SERIAL_JTAG,
#endif
#ifdef USE_ESP8266

View File

@ -8,7 +8,7 @@
#ifdef USE_ESP_IDF
#include <driver/uart.h>
#ifdef USE_USB_SERIAL_JTAG
#ifdef USE_LOGGER_USB_SERIAL_JTAG
#include <driver/usb_serial_jtag.h>
#include <esp_vfs_dev.h>
#include <esp_vfs_usb_serial_jtag.h>
@ -32,7 +32,7 @@ static const char *const TAG = "logger";
#ifdef USE_ESP_IDF
#ifdef USE_USB_SERIAL_JTAG
#ifdef USE_LOGGER_USB_SERIAL_JTAG
static void init_usb_serial_jtag_() {
setvbuf(stdin, NULL, _IONBF, 0); // Disable buffering on stdin
@ -103,7 +103,7 @@ void Logger::pre_setup() {
break;
#endif
#ifdef USE_USB_CDC
#ifdef USE_LOGGER_USB_CDC
case UART_SELECTION_USB_CDC:
this->hw_serial_ = &Serial;
#if ARDUINO_USB_CDC_ON_BOOT
@ -134,7 +134,7 @@ void Logger::pre_setup() {
this->uart_num_ = -1;
break;
#endif
#ifdef USE_USB_SERIAL_JTAG
#ifdef USE_LOGGER_USB_SERIAL_JTAG
case UART_SELECTION_USB_SERIAL_JTAG:
this->uart_num_ = -1;
init_usb_serial_jtag_();
@ -177,6 +177,8 @@ void HOT Logger::write_msg_(const char *msg) {
uart_write_bytes(this->uart_num_, "\n", 1);
}
}
#else
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
#endif
const char *const UART_SELECTIONS[] = {
@ -184,10 +186,10 @@ const char *const UART_SELECTIONS[] = {
#ifdef USE_ESP32_VARIANT_ESP32
"UART2",
#endif
#ifdef USE_USB_CDC
#ifdef USE_LOGGER_USB_CDC
"USB_CDC",
#endif
#ifdef USE_USB_SERIAL_JTAG
#ifdef USE_LOGGER_USB_SERIAL_JTAG
"USB_SERIAL_JTAG",
#endif
};

View File

@ -1,7 +1,4 @@
#ifdef USE_ESP8266
#ifndef USE_ARDUINO
#error "Only ARDUINO is supported"
#endif
#include "logger.h"
#include "esphome/core/log.h"
@ -37,6 +34,8 @@ void Logger::pre_setup() {
ESP_LOGI(TAG, "Log initialized");
}
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
const char *const UART_SELECTIONS[] = {"UART0", "UART1", "UART0_SWAP"};
const char *Logger::get_uart_selection_() { return UART_SELECTIONS[this->uart_]; }

View File

@ -0,0 +1,22 @@
#if defined(USE_HOST)
#include "logger.h"
namespace esphome {
namespace logger {
void HOT Logger::write_msg_(const char *msg) {
time_t rawtime;
struct tm *timeinfo;
char buffer[80];
time(&rawtime);
timeinfo = localtime(&rawtime);
strftime(buffer, sizeof buffer, "[%H:%M:%S]", timeinfo);
fputs(buffer, stdout);
puts(msg);
}
} // namespace logger
} // namespace esphome
#endif

View File

@ -50,6 +50,8 @@ void Logger::pre_setup() {
ESP_LOGI(TAG, "Log initialized");
}
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
const char *const UART_SELECTIONS[] = {"DEFAULT", "UART0", "UART1", "UART2"};
const char *Logger::get_uart_selection_() { return UART_SELECTIONS[this->uart_]; }

View File

@ -1,7 +1,4 @@
#ifdef USE_RP2040
#ifndef USE_ARDUINO
#error "Only ARDUINO is supported"
#endif
#include "logger.h"
#include "esphome/core/log.h"
@ -31,6 +28,8 @@ void Logger::pre_setup() {
ESP_LOGI(TAG, "Log initialized");
}
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
const char *const UART_SELECTIONS[] = {"UART0", "UART1", "USB_CDC"};
const char *Logger::get_uart_selection_() { return UART_SELECTIONS[this->uart_]; }

View File

@ -8,10 +8,23 @@ namespace ltr390 {
static const char *const TAG = "ltr390";
static const uint8_t LTR390_MAIN_CTRL = 0x00;
static const uint8_t LTR390_MEAS_RATE = 0x04;
static const uint8_t LTR390_GAIN = 0x05;
static const uint8_t LTR390_PART_ID = 0x06;
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};
// 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};
static const uint32_t MODEADDRESSES[2] = {0x0D, 0x10};
static const float SENSITIVITY_MAX = 2300;
static const float INTG_MAX = RESOLUTIONVALUE[0] * 100;
static const int GAIN_MAX = GAINVALUES[4];
uint32_t little_endian_bytes_to_int(const uint8_t *buffer, uint8_t num_bytes) {
uint32_t value = 0;
@ -58,7 +71,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_] * RESOLUTIONVALUE[this->res_])) * this->wfac_;
this->light_sensor_->publish_state(lux);
}
@ -74,7 +87,7 @@ void LTR390Component::read_uvs_() {
uint32_t uv = *val;
if (this->uvi_sensor_ != nullptr) {
this->uvi_sensor_->publish_state(uv / LTR390_SENSITIVITY * this->wfac_);
this->uvi_sensor_->publish_state((uv / this->sensitivity_) * this->wfac_);
}
if (this->uv_sensor_ != nullptr) {
@ -132,12 +145,13 @@ void LTR390Component::setup() {
// Set gain
this->reg(LTR390_GAIN) = gain_;
// Set resolution
uint8_t res = this->reg(LTR390_MEAS_RATE).get();
// resolution is in bits 5-7
res &= ~0b01110000;
res |= res << 4;
this->reg(LTR390_MEAS_RATE) = res;
// 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;
// Set sensor read state
this->reading_ = false;

View File

@ -17,14 +17,6 @@ enum LTR390CTRL {
};
// enums from https://github.com/adafruit/Adafruit_LTR390/
static const uint8_t LTR390_MAIN_CTRL = 0x00;
static const uint8_t LTR390_MEAS_RATE = 0x04;
static const uint8_t LTR390_GAIN = 0x05;
static const uint8_t LTR390_PART_ID = 0x06;
static const uint8_t LTR390_MAIN_STATUS = 0x07;
static const float LTR390_SENSITIVITY = 2300.0;
// Sensing modes
enum LTR390MODE {
LTR390_MODE_ALS,
@ -81,6 +73,7 @@ class LTR390Component : public PollingComponent, public i2c::I2CDevice {
LTR390GAIN gain_;
LTR390RESOLUTION res_;
float sensitivity_;
float wfac_;
sensor::Sensor *light_sensor_{nullptr};

View File

@ -8,6 +8,7 @@ from esphome.const import (
CONF_RESOLUTION,
UNIT_LUX,
ICON_BRIGHTNESS_5,
DEVICE_CLASS_EMPTY,
DEVICE_CLASS_ILLUMINANCE,
)
@ -61,22 +62,22 @@ CONFIG_SCHEMA = cv.All(
unit_of_measurement=UNIT_COUNTS,
icon=ICON_BRIGHTNESS_5,
accuracy_decimals=1,
device_class=DEVICE_CLASS_ILLUMINANCE,
device_class=DEVICE_CLASS_EMPTY,
),
cv.Optional(CONF_UV_INDEX): sensor.sensor_schema(
unit_of_measurement=UNIT_UVI,
icon=ICON_BRIGHTNESS_5,
accuracy_decimals=5,
device_class=DEVICE_CLASS_ILLUMINANCE,
device_class=DEVICE_CLASS_EMPTY,
),
cv.Optional(CONF_UV): sensor.sensor_schema(
unit_of_measurement=UNIT_COUNTS,
icon=ICON_BRIGHTNESS_5,
accuracy_decimals=1,
device_class=DEVICE_CLASS_ILLUMINANCE,
device_class=DEVICE_CLASS_EMPTY,
),
cv.Optional(CONF_GAIN, default="X3"): cv.enum(GAIN_OPTIONS),
cv.Optional(CONF_RESOLUTION, default=18): cv.enum(RES_OPTIONS),
cv.Optional(CONF_GAIN, default="X18"): cv.enum(GAIN_OPTIONS),
cv.Optional(CONF_RESOLUTION, default=20): cv.enum(RES_OPTIONS),
cv.Optional(CONF_WINDOW_CORRECTION_FACTOR, default=1.0): cv.float_range(
min=1.0
),

View File

@ -0,0 +1,367 @@
import logging
import json
import hashlib
from urllib.parse import urljoin
from pathlib import Path
import requests
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.core import CORE, HexInt, EsphomeError
from esphome.components import esp32, microphone
from esphome import automation, git, external_files
from esphome.automation import register_action, register_condition
from esphome.const import (
__version__,
CONF_ID,
CONF_MICROPHONE,
CONF_MODEL,
CONF_URL,
CONF_FILE,
CONF_PATH,
CONF_REF,
CONF_REFRESH,
CONF_TYPE,
CONF_USERNAME,
CONF_PASSWORD,
CONF_RAW_DATA_ID,
TYPE_GIT,
TYPE_LOCAL,
)
_LOGGER = logging.getLogger(__name__)
CODEOWNERS = ["@kahrendt", "@jesserockz"]
DEPENDENCIES = ["microphone"]
DOMAIN = "micro_wake_word"
CONF_PROBABILITY_CUTOFF = "probability_cutoff"
CONF_SLIDING_WINDOW_AVERAGE_SIZE = "sliding_window_average_size"
CONF_ON_WAKE_WORD_DETECTED = "on_wake_word_detected"
TYPE_HTTP = "http"
micro_wake_word_ns = cg.esphome_ns.namespace("micro_wake_word")
MicroWakeWord = micro_wake_word_ns.class_("MicroWakeWord", cg.Component)
StartAction = micro_wake_word_ns.class_("StartAction", automation.Action)
StopAction = micro_wake_word_ns.class_("StopAction", automation.Action)
IsRunningCondition = micro_wake_word_ns.class_(
"IsRunningCondition", automation.Condition
)
def _validate_json_filename(value):
value = cv.string(value)
if not value.endswith(".json"):
raise cv.Invalid("Manifest filename must end with .json")
return value
def _process_git_source(config):
repo_dir, _ = git.clone_or_update(
url=config[CONF_URL],
ref=config.get(CONF_REF),
refresh=config[CONF_REFRESH],
domain=DOMAIN,
username=config.get(CONF_USERNAME),
password=config.get(CONF_PASSWORD),
)
if not (repo_dir / config[CONF_FILE]).exists():
raise cv.Invalid("File does not exist in repository")
return config
CV_GIT_SCHEMA = cv.GIT_SCHEMA
if isinstance(CV_GIT_SCHEMA, dict):
CV_GIT_SCHEMA = cv.Schema(CV_GIT_SCHEMA)
GIT_SCHEMA = cv.All(
CV_GIT_SCHEMA.extend(
{
cv.Required(CONF_FILE): _validate_json_filename,
cv.Optional(CONF_REFRESH, default="1d"): cv.All(
cv.string, cv.source_refresh
),
}
),
_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"
MANIFEST_SCHEMA_V1 = cv.Schema(
{
cv.Required(CONF_TYPE): "micro",
cv.Required(KEY_WAKE_WORD): cv.string,
cv.Required(KEY_AUTHOR): cv.string,
cv.Required(KEY_WEBSITE): cv.url,
cv.Required(KEY_VERSION): cv.All(cv.int_, 1),
cv.Required(CONF_MODEL): cv.string,
cv.Required(KEY_MICRO): cv.Schema(
{
cv.Required(CONF_PROBABILITY_CUTOFF): cv.float_,
cv.Required(CONF_SLIDING_WINDOW_AVERAGE_SIZE): cv.positive_int,
cv.Optional(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]
h = hashlib.new("sha256")
h.update(url.encode())
key = h.hexdigest()[:8]
base_dir = external_files.compute_local_file_dir(DOMAIN)
return base_dir / key
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")
return path.read_bytes()
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 file from {url}: {e}") from e
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(req.content)
return req.content
def _process_http_source(config):
url = config[CONF_URL]
path = _compute_local_file_path(config)
json_path = path / "manifest.json"
json_contents = _download_file(url, json_path)
manifest_data = json.loads(json_contents)
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)
model_path = path / model
_download_file(str(model_url), model_path)
return config
HTTP_SCHEMA = cv.All(
{
cv.Required(CONF_URL): cv.url,
},
_process_http_source,
)
LOCAL_SCHEMA = cv.Schema(
{
cv.Required(CONF_PATH): cv.All(_validate_json_filename, cv.file_),
}
)
def _validate_source_model_name(value):
if not isinstance(value, str):
raise cv.Invalid("Model name must be a string")
if value.endswith(".json"):
raise cv.Invalid("Model name must not end with .json")
return MODEL_SOURCE_SCHEMA(
{
CONF_TYPE: TYPE_HTTP,
CONF_URL: f"https://github.com/esphome/micro-wake-word-models/raw/main/models/{value}.json",
}
)
def _validate_source_shorthand(value):
if not isinstance(value, str):
raise cv.Invalid("Shorthand only for strings")
try: # Test for model name
return _validate_source_model_name(value)
except cv.Invalid:
pass
try: # Test for local path
return MODEL_SOURCE_SCHEMA({CONF_TYPE: TYPE_LOCAL, CONF_PATH: value})
except cv.Invalid:
pass
try: # Test for http url
return MODEL_SOURCE_SCHEMA({CONF_TYPE: TYPE_HTTP, CONF_URL: value})
except cv.Invalid:
pass
git_file = git.GitFile.from_shorthand(value)
conf = {
CONF_TYPE: TYPE_GIT,
CONF_URL: git_file.git_url,
CONF_FILE: git_file.filename,
}
if git_file.ref:
conf[CONF_REF] = git_file.ref
try:
return MODEL_SOURCE_SCHEMA(conf)
except cv.Invalid as e:
raise cv.Invalid(
f"Could not find file '{git_file.filename}' in the repository. Please make sure it exists."
) from e
MODEL_SOURCE_SCHEMA = cv.Any(
_validate_source_shorthand,
cv.typed_schema(
{
TYPE_GIT: GIT_SCHEMA,
TYPE_LOCAL: LOCAL_SCHEMA,
TYPE_HTTP: HTTP_SCHEMA,
}
),
msg="Not a valid model name, local path, http(s) url, or github shorthand",
)
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.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),
}
).extend(cv.COMPONENT_SCHEMA),
cv.only_with_esp_idf,
)
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
model_path = urljoin(str(manifest_path), manifest[CONF_MODEL])
with open(model_path, "rb") as f:
model = f.read()
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 = []
if model_config[CONF_TYPE] == TYPE_GIT:
# compute path to model file
key = f"{model_config[CONF_URL]}@{model_config.get(CONF_REF)}"
base_dir = Path(CORE.data_dir) / DOMAIN
h = hashlib.new("sha256")
h.update(key.encode())
file: Path = base_dir / h.hexdigest()[:8] / model_config[CONF_FILE]
elif model_config[CONF_TYPE] == TYPE_LOCAL:
file = model_config[CONF_PATH]
elif model_config[CONF_TYPE] == TYPE_HTTP:
file = _compute_local_file_path(model_config) / "manifest.json"
manifest, data = _load_model_data(file)
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))
probability_cutoff = config.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],
)
cg.add(var.set_sliding_window_average_size(sliding_window_average_size))
cg.add(var.set_wake_word(manifest[KEY_WAKE_WORD]))
MICRO_WAKE_WORD_ACTION_SCHEMA = cv.Schema({cv.GenerateID(): cv.use_id(MicroWakeWord)})
@register_action("micro_wake_word.start", StartAction, MICRO_WAKE_WORD_ACTION_SCHEMA)
@register_action("micro_wake_word.stop", StopAction, MICRO_WAKE_WORD_ACTION_SCHEMA)
@register_condition(
"micro_wake_word.is_running", IsRunningCondition, MICRO_WAKE_WORD_ACTION_SCHEMA
)
async def micro_wake_word_action_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var

View File

@ -0,0 +1,493 @@
#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

@ -0,0 +1,521 @@
#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
#ifdef USE_ESP_IDF
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "audio_preprocessor_int8_model_data.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 <cmath>
namespace esphome {
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_SIZE = SAMPLE_RATE_HZ / 1000 * BUFFER_LENGTH;
static const size_t INPUT_BUFFER_SIZE = 32 * SAMPLE_RATE_HZ / 1000; // 32ms * 16kHz / 1000ms
float MicroWakeWord::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; }
static const LogString *micro_wake_word_state_to_string(State state) {
switch (state) {
case State::IDLE:
return LOG_STR("IDLE");
case State::START_MICROPHONE:
return LOG_STR("START_MICROPHONE");
case State::STARTING_MICROPHONE:
return LOG_STR("STARTING_MICROPHONE");
case State::DETECTING_WAKE_WORD:
return LOG_STR("DETECTING_WAKE_WORD");
case State::STOP_MICROPHONE:
return LOG_STR("STOP_MICROPHONE");
case State::STOPPING_MICROPHONE:
return LOG_STR("STOPPING_MICROPHONE");
default:
return LOG_STR("UNKNOWN");
}
}
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_);
}
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");
this->mark_failed();
return;
}
ESP_LOGCONFIG(TAG, "Micro Wake Word initialized");
}
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;
}
size_t bytes_written = this->ring_buffer_->write((void *) this->input_buffer_, bytes_read);
if (bytes_written != bytes_read) {
ESP_LOGW(TAG, "Failed to write some data to ring buffer (written=%d, expected=%d)", bytes_written, bytes_read);
}
return bytes_written;
}
void MicroWakeWord::loop() {
switch (this->state_) {
case State::IDLE:
break;
case State::START_MICROPHONE:
ESP_LOGD(TAG, "Starting Microphone");
this->microphone_->start();
this->set_state_(State::STARTING_MICROPHONE);
this->high_freq_.start();
break;
case State::STARTING_MICROPHONE:
if (this->microphone_->is_running()) {
this->set_state_(State::DETECTING_WAKE_WORD);
}
break;
case State::DETECTING_WAKE_WORD:
this->read_microphone_();
if (this->detect_wake_word_()) {
ESP_LOGD(TAG, "Wake Word Detected");
this->detected_ = true;
this->set_state_(State::STOP_MICROPHONE);
}
break;
case State::STOP_MICROPHONE:
ESP_LOGD(TAG, "Stopping Microphone");
this->microphone_->stop();
this->set_state_(State::STOPPING_MICROPHONE);
this->high_freq_.stop();
break;
case State::STOPPING_MICROPHONE:
if (this->microphone_->is_stopped()) {
this->set_state_(State::IDLE);
if (this->detected_) {
this->detected_ = false;
this->wake_word_detected_trigger_->trigger(this->wake_word_);
}
}
break;
}
}
void MicroWakeWord::start() {
if (this->is_failed()) {
ESP_LOGW(TAG, "Wake word component is marked as failed. Please check setup logs");
return;
}
if (this->state_ != State::IDLE) {
ESP_LOGW(TAG, "Wake word is already running");
return;
}
this->set_state_(State::START_MICROPHONE);
}
void MicroWakeWord::stop() {
if (this->state_ == State::IDLE) {
ESP_LOGW(TAG, "Wake word is already stopped");
return;
}
if (this->state_ == State::STOPPING_MICROPHONE) {
ESP_LOGW(TAG, "Wake word is already stopping");
return;
}
this->set_state_(State::STOP_MICROPHONE);
}
void MicroWakeWord::set_state_(State state) {
ESP_LOGD(TAG, "State changed from %s to %s", LOG_STR_ARG(micro_wake_word_state_to_string(this->state_)),
LOG_STR_ARG(micro_wake_word_state_to_string(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);
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.");
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) {
ESP_LOGE(TAG, "Could not allocate the audio preprocessor's buffer.");
return false;
}
this->preprocessor_stride_buffer_ = audio_samples_allocator.allocate(HISTORY_SAMPLES_TO_KEEP);
if (this->preprocessor_stride_buffer_ == nullptr) {
ESP_LOGE(TAG, "Could not allocate the audio preprocessor's stride 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");
return false;
}
static tflite::MicroMutableOpResolver<18> preprocessor_op_resolver;
static tflite::MicroMutableOpResolver<14> 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);
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");
return false;
}
ESP_LOGV(TAG, "Streaming Inference Latency=%u 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_()) {
return false;
}
// Perform inference
uint32_t streaming_size = micros();
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;
}
float sliding_window_average = sum / static_cast<float>(this->sliding_window_average_size_);
// Ensure we have enough samples since the last positive detection
this->ignore_windows_ = std::min(this->ignore_windows_ + 1, 0);
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;
}
return true;
}
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::slice_available_() {
size_t available = this->ring_buffer_->available();
size_t free = this->ring_buffer_->free();
if (free < NEW_SAMPLES_TO_GET * sizeof(int16_t)) {
// If the ring buffer is within one audio slice of being full, then wake word detection will have issues.
// If this is constantly occuring, then some possibilities why are
// 1) there are too many other slow components configured
// 2) the ESP32 isn't fast enough; e.g., an ESP32 is much slower than an ESP32-S3 at inferences.
// 3) the model is too large
// 4) the model uses operations that are not optimized
ESP_LOGW(TAG,
"Audio buffer is nearly full. Wake word detection may be less accurate and have slower reponse times. "
#if !defined(USE_ESP32_VARIANT_ESP32S3)
"microWakeWord is designed for the ESP32-S3. The current platform is too slow for this model."
#endif
);
}
return available > (NEW_SAMPLES_TO_GET * sizeof(int16_t));
}
bool MicroWakeWord::stride_audio_samples_(int16_t **audio_samples) {
if (!this->slice_available_()) {
return false;
}
// Copy 320 bytes (160 samples over 10 ms) into preprocessor_audio_buffer_ from history in
// preprocessor_stride_buffer_
memcpy((void *) (this->preprocessor_audio_buffer_), (void *) (this->preprocessor_stride_buffer_),
HISTORY_SAMPLES_TO_KEEP * sizeof(int16_t));
// Copy 640 bytes (320 samples over 20 ms) from the ring buffer
// The first 320 bytes (160 samples over 10 ms) will be from history
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));
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)) {
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)));
return false;
}
// Copy the last 320 bytes (160 samples over 10 ms) from the audio buffer into history stride buffer for the next
// iteration
memcpy((void *) (this->preprocessor_stride_buffer_), (void *) (this->preprocessor_audio_buffer_ + NEW_SAMPLES_TO_GET),
HISTORY_SAMPLES_TO_KEEP * sizeof(int16_t));
*audio_samples = this->preprocessor_audio_buffer_;
return true;
}
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;
}
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;
}
bool MicroWakeWord::register_streaming_ops_(tflite::MicroMutableOpResolver<14> &op_resolver) {
if (op_resolver.AddCallOnce() != kTfLiteOk)
return false;
if (op_resolver.AddVarHandle() != kTfLiteOk)
return false;
if (op_resolver.AddReshape() != kTfLiteOk)
return false;
if (op_resolver.AddReadVariable() != kTfLiteOk)
return false;
if (op_resolver.AddStridedSlice() != kTfLiteOk)
return false;
if (op_resolver.AddConcatenation() != kTfLiteOk)
return false;
if (op_resolver.AddAssignVariable() != kTfLiteOk)
return false;
if (op_resolver.AddConv2D() != kTfLiteOk)
return false;
if (op_resolver.AddMul() != kTfLiteOk)
return false;
if (op_resolver.AddAdd() != kTfLiteOk)
return false;
if (op_resolver.AddMean() != kTfLiteOk)
return false;
if (op_resolver.AddFullyConnected() != kTfLiteOk)
return false;
if (op_resolver.AddLogistic() != kTfLiteOk)
return false;
if (op_resolver.AddQuantize() != kTfLiteOk)
return false;
return true;
}
} // namespace micro_wake_word
} // namespace esphome
#endif // USE_ESP_IDF
#endif // CLANG_TIDY

View File

@ -0,0 +1,207 @@
#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 "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/ring_buffer.h"
#include "esphome/components/microphone/microphone.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 {
// 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,
STARTING_MICROPHONE,
DETECTING_WAKE_WORD,
STOP_MICROPHONE,
STOPPING_MICROPHONE,
};
class MicroWakeWord : public Component {
public:
void setup() override;
void loop() override;
float get_setup_priority() const override;
void dump_config() override;
void start();
void stop();
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_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; }
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};
HighFrequencyLoopRequester high_freq_;
std::unique_ptr<RingBuffer> ring_buffer_;
int16_t *input_buffer_;
const tflite::Model *preprocessor_model_{nullptr};
const tflite::Model *streaming_model_{nullptr};
tflite::MicroInterpreter *streaming_interpreter_{nullptr};
tflite::MicroInterpreter *preprocessor_interperter_{nullptr};
std::vector<float> recent_streaming_probabilities_;
size_t last_n_index_{0};
float probability_cutoff_{0.5};
size_t sliding_window_average_size_{10};
// 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
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};
tflite::MicroResourceVariables *mrv_{nullptr};
// Stores audio fed into feature generator preprocessor
int16_t *preprocessor_audio_buffer_;
int16_t *preprocessor_stride_buffer_;
bool detected_{false};
/** Detects if wake word has been said
*
* 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
*/
bool detect_wake_word_();
/// @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
*
* @param ring_buffer ring buffer containing raw audio samples
* @return True if a new slice of features was generated, false otherwise
*/
bool update_features_();
/** Generates features from 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
* @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]);
/** 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 Returns true if successfully registered the streaming model's TensorFlow operations
bool register_streaming_ops_(tflite::MicroMutableOpResolver<14> &op_resolver);
};
template<typename... Ts> class StartAction : public Action<Ts...>, public Parented<MicroWakeWord> {
public:
void play(Ts... x) override { this->parent_->start(); }
};
template<typename... Ts> class StopAction : public Action<Ts...>, public Parented<MicroWakeWord> {
public:
void play(Ts... x) override { this->parent_->stop(); }
};
template<typename... Ts> class IsRunningCondition : public Condition<Ts...>, public Parented<MicroWakeWord> {
public:
bool check(Ts... x) override { return this->parent_->is_running(); }
};
} // namespace micro_wake_word
} // namespace esphome
#endif // USE_ESP_IDF
#endif // CLANG_TIDY

View File

@ -91,8 +91,13 @@ void MQTTClientComponent::send_device_info_() {
this->publish_json(
topic,
[](JsonObject root) {
auto ip = network::get_ip_address();
root["ip"] = ip.str();
uint8_t index = 0;
for (auto &ip : network::get_ip_addresses()) {
if (ip.is_set()) {
root["ip" + (index == 0 ? "" : esphome::to_string(index))] = ip.str();
index++;
}
}
root["name"] = App.get_name();
#ifdef USE_API
root["port"] = api::global_api_server->get_port();
@ -159,14 +164,13 @@ void MQTTClientComponent::start_dnslookup_() {
this->dns_resolve_error_ = false;
this->dns_resolved_ = false;
ip_addr_t addr;
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
#if USE_NETWORK_IPV6
err_t err = dns_gethostbyname_addrtype(this->credentials_.address.c_str(), &addr,
MQTTClientComponent::dns_found_callback, this, LWIP_DNS_ADDRTYPE_IPV6_IPV4);
#else
err_t err = dns_gethostbyname_addrtype(this->credentials_.address.c_str(), &addr,
MQTTClientComponent::dns_found_callback, this, LWIP_DNS_ADDRTYPE_IPV4);
#endif
#ifdef USE_ESP8266
err_t err = dns_gethostbyname(this->credentials_.address.c_str(), &addr,
esphome::mqtt::MQTTClientComponent::dns_found_callback, this);
#endif
#endif /* USE_NETWORK_IPV6 */
switch (err) {
case ERR_OK: {
// Got IP immediately

View File

@ -1,6 +1,8 @@
#include "mqtt_text_sensor.h"
#include "esphome/core/log.h"
#include "mqtt_const.h"
#ifdef USE_MQTT
#ifdef USE_TEXT_SENSOR
@ -13,6 +15,8 @@ using namespace esphome::text_sensor;
MQTTTextSensor::MQTTTextSensor(TextSensor *sensor) : sensor_(sensor) {}
void MQTTTextSensor::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
if (!this->sensor_->get_device_class().empty())
root[MQTT_DEVICE_CLASS] = this->sensor_->get_device_class();
config.command_topic = false;
}
void MQTTTextSensor::setup() {

View File

@ -0,0 +1 @@
CODEOWNERS = ["@e28eta"]

View File

@ -0,0 +1,444 @@
#include "ms8607.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
namespace ms8607 {
/// TAG used for logging calls
static const char *const TAG = "ms8607";
/// Reset the Pressure/Temperature sensor
static const uint8_t MS8607_PT_CMD_RESET = 0x1E;
/// Beginning of PROM register addresses. Same for both i2c addresses. Each address has 16 bits of data, and
/// PROM addresses step by two, so the LSB is always 0
static const uint8_t MS8607_PROM_START = 0xA0;
/// Last PROM register address.
static const uint8_t MS8607_PROM_END = 0xAE;
/// Number of PROM registers.
static const uint8_t MS8607_PROM_COUNT = (MS8607_PROM_END - MS8607_PROM_START) >> 1;
/// Reset the Humidity sensor
static const uint8_t MS8607_CMD_H_RESET = 0xFE;
/// Read relative humidity, without holding i2c master
static const uint8_t MS8607_CMD_H_MEASURE_NO_HOLD = 0xF5;
/// Temperature correction coefficient for Relative Humidity from datasheet
static const float MS8607_H_TEMP_COEFFICIENT = -0.18;
/// Read the converted analog value, either D1 (pressure) or D2 (temperature)
static const uint8_t MS8607_CMD_ADC_READ = 0x00;
// TODO: allow OSR to be turned down for speed and/or lower power consumption via configuration.
// ms8607 supports 6 different settings
/// Request conversion of analog D1 (pressure) with OSR=8192 (highest oversampling ratio). Takes maximum of 17.2ms
static const uint8_t MS8607_CMD_CONV_D1_OSR_8K = 0x4A;
/// Request conversion of analog D2 (temperature) with OSR=8192 (highest oversampling ratio). Takes maximum of 17.2ms
static const uint8_t MS8607_CMD_CONV_D2_OSR_8K = 0x5A;
enum class MS8607Component::ErrorCode {
/// Component hasn't failed (yet?)
NONE = 0,
/// Both the Pressure/Temperature address and the Humidity address failed to reset
PTH_RESET_FAILED = 1,
/// Asking the Pressure/Temperature sensor to reset failed
PT_RESET_FAILED = 2,
/// Asking the Humidity sensor to reset failed
H_RESET_FAILED = 3,
/// Reading the PROM calibration values failed
PROM_READ_FAILED = 4,
/// The PROM calibration values failed the CRC check
PROM_CRC_FAILED = 5,
};
enum class MS8607Component::SetupStatus {
/// This component has not successfully reset the PT & H devices
NEEDS_RESET,
/// Reset commands succeeded, need to wait >= 15ms to read PROM
NEEDS_PROM_READ,
/// Successfully read PROM and ready to update sensors
SUCCESSFUL,
};
static uint8_t crc4(uint16_t *buffer, size_t length);
static uint8_t hsensor_crc_check(uint16_t value);
void MS8607Component::setup() {
ESP_LOGCONFIG(TAG, "Setting up MS8607...");
this->error_code_ = ErrorCode::NONE;
this->setup_status_ = SetupStatus::NEEDS_RESET;
// I do not know why the device sometimes NACKs the reset command, but
// try 3 times in case it's a transitory issue on this boot
this->set_retry(
"reset", 5, 3,
[this](const uint8_t remaining_setup_attempts) {
ESP_LOGD(TAG, "Resetting both I2C addresses: 0x%02X, 0x%02X", this->address_,
this->humidity_device_->get_address());
// I believe sending the reset command to both addresses is preferable to
// skipping humidity if PT fails for some reason.
// However, only consider the reset successful if they both ACK
bool const pt_successful = this->write_bytes(MS8607_PT_CMD_RESET, nullptr, 0);
bool const h_successful = this->humidity_device_->write_bytes(MS8607_CMD_H_RESET, nullptr, 0);
if (!(pt_successful && h_successful)) {
ESP_LOGE(TAG, "Resetting I2C devices failed");
if (!pt_successful && !h_successful) {
this->error_code_ = ErrorCode::PTH_RESET_FAILED;
} else if (!pt_successful) {
this->error_code_ = ErrorCode::PT_RESET_FAILED;
} else {
this->error_code_ = ErrorCode::H_RESET_FAILED;
}
if (remaining_setup_attempts > 0) {
this->status_set_error();
} else {
this->mark_failed();
}
return RetryResult::RETRY;
}
this->setup_status_ = SetupStatus::NEEDS_PROM_READ;
this->error_code_ = ErrorCode::NONE;
this->status_clear_error();
// 15ms delay matches datasheet, Adafruit_MS8607 & SparkFun_PHT_MS8607_Arduino_Library
this->set_timeout("prom-read", 15, [this]() {
if (this->read_calibration_values_from_prom_()) {
this->setup_status_ = SetupStatus::SUCCESSFUL;
this->status_clear_error();
} else {
this->mark_failed();
return;
}
});
return RetryResult::DONE;
},
5.0f); // executes at now, +5ms, +25ms
}
void MS8607Component::update() {
if (this->setup_status_ != SetupStatus::SUCCESSFUL) {
// setup is still occurring, either because reset had to retry or due to the 15ms
// delay needed between reset & reading the PROM values
return;
}
// Updating happens async and sequentially.
// Temperature, then pressure, then humidity
this->request_read_temperature_();
}
void MS8607Component::dump_config() {
ESP_LOGCONFIG(TAG, "MS8607:");
LOG_I2C_DEVICE(this);
// LOG_I2C_DEVICE doesn't work for humidity, the `address_` is protected. Log using get_address()
ESP_LOGCONFIG(TAG, " Address: 0x%02X", this->humidity_device_->get_address());
if (this->is_failed()) {
ESP_LOGE(TAG, "Communication with MS8607 failed.");
switch (this->error_code_) {
case ErrorCode::PT_RESET_FAILED:
ESP_LOGE(TAG, "Temperature/Pressure RESET failed");
break;
case ErrorCode::H_RESET_FAILED:
ESP_LOGE(TAG, "Humidity RESET failed");
break;
case ErrorCode::PTH_RESET_FAILED:
ESP_LOGE(TAG, "Temperature/Pressure && Humidity RESET failed");
break;
case ErrorCode::PROM_READ_FAILED:
ESP_LOGE(TAG, "Reading PROM failed");
break;
case ErrorCode::PROM_CRC_FAILED:
ESP_LOGE(TAG, "PROM values failed CRC");
break;
case ErrorCode::NONE:
default:
ESP_LOGE(TAG, "Error reason unknown %u", static_cast<uint8_t>(this->error_code_));
break;
}
}
LOG_UPDATE_INTERVAL(this);
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
LOG_SENSOR(" ", "Pressure", this->pressure_sensor_);
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
}
bool MS8607Component::read_calibration_values_from_prom_() {
ESP_LOGD(TAG, "Reading calibration values from PROM");
uint16_t buffer[MS8607_PROM_COUNT];
bool successful = true;
for (uint8_t idx = 0; idx < MS8607_PROM_COUNT; ++idx) {
uint8_t const address_to_read = MS8607_PROM_START + (idx * 2);
successful &= this->read_byte_16(address_to_read, &buffer[idx]);
}
if (!successful) {
ESP_LOGE(TAG, "Reading calibration values from PROM failed");
this->error_code_ = ErrorCode::PROM_READ_FAILED;
return false;
}
ESP_LOGD(TAG, "Checking CRC of calibration values from PROM");
uint8_t const expected_crc = (buffer[0] & 0xF000) >> 12; // first 4 bits
buffer[0] &= 0x0FFF; // strip CRC from buffer, in order to run CRC
uint8_t const actual_crc = crc4(buffer, MS8607_PROM_COUNT);
if (expected_crc != actual_crc) {
ESP_LOGE(TAG, "Incorrect CRC value. Provided value 0x%01X != calculated value 0x%01X", expected_crc, actual_crc);
this->error_code_ = ErrorCode::PROM_CRC_FAILED;
return false;
}
this->calibration_values_.pressure_sensitivity = buffer[1];
this->calibration_values_.pressure_offset = buffer[2];
this->calibration_values_.pressure_sensitivity_temperature_coefficient = buffer[3];
this->calibration_values_.pressure_offset_temperature_coefficient = buffer[4];
this->calibration_values_.reference_temperature = buffer[5];
this->calibration_values_.temperature_coefficient_of_temperature = buffer[6];
ESP_LOGD(TAG, "Finished reading calibration values");
// Skipping reading Humidity PROM, since it doesn't have anything interesting for us
return true;
}
/**
CRC-4 algorithm from datasheet. It operates on a buffer of 16-bit values, one byte at a time, using a 16-bit
value to collect the CRC result into.
The provided/expected CRC value must already be zeroed out from the buffer.
*/
static uint8_t crc4(uint16_t *buffer, size_t length) {
uint16_t crc_remainder = 0;
// algorithm to add a byte into the crc
auto apply_crc = [&crc_remainder](uint8_t next) {
crc_remainder ^= next;
for (uint8_t bit = 8; bit > 0; --bit) {
if (crc_remainder & 0x8000) {
crc_remainder = (crc_remainder << 1) ^ 0x3000;
} else {
crc_remainder = (crc_remainder << 1);
}
}
};
// add all the bytes
for (size_t idx = 0; idx < length; ++idx) {
for (auto byte : decode_value(buffer[idx])) {
apply_crc(byte);
}
}
// For the MS8607 CRC, add a pair of zeros to shift the last byte from `buffer` through
apply_crc(0);
apply_crc(0);
return (crc_remainder >> 12) & 0xF; // only the most significant 4 bits
}
/**
* @brief Calculates CRC value for the provided humidity (+ status bits) value
*
* CRC-8 check comes from other MS8607 libraries on github. I did not find it in the datasheet,
* and it differs from the crc8 implementation that's already part of esphome.
*
* @param value two byte humidity sensor value read from i2c
* @return uint8_t computed crc value
*/
static uint8_t hsensor_crc_check(uint16_t value) {
uint32_t polynom = 0x988000; // x^8 + x^5 + x^4 + 1
uint32_t msb = 0x800000;
uint32_t mask = 0xFF8000;
uint32_t result = (uint32_t) value << 8; // Pad with zeros as specified in spec
while (msb != 0x80) {
// Check if msb of current value is 1 and apply XOR mask
if (result & msb) {
result = ((result ^ polynom) & mask) | (result & ~mask);
}
// Shift by one
msb >>= 1;
mask >>= 1;
polynom >>= 1;
}
return result & 0xFF;
}
void MS8607Component::request_read_temperature_() {
// Tell MS8607 to start ADC conversion of temperature sensor
if (!this->write_bytes(MS8607_CMD_CONV_D2_OSR_8K, nullptr, 0)) {
this->status_set_warning();
return;
}
auto f = std::bind(&MS8607Component::read_temperature_, this);
// datasheet says 17.2ms max conversion time at OSR 8192
this->set_timeout("temperature", 20, f);
}
void MS8607Component::read_temperature_() {
uint8_t bytes[3]; // 24 bits
if (!this->read_bytes(MS8607_CMD_ADC_READ, bytes, 3)) {
this->status_set_warning();
return;
}
const uint32_t d2_raw_temperature = encode_uint32(0, bytes[0], bytes[1], bytes[2]);
this->request_read_pressure_(d2_raw_temperature);
}
void MS8607Component::request_read_pressure_(uint32_t d2_raw_temperature) {
if (!this->write_bytes(MS8607_CMD_CONV_D1_OSR_8K, nullptr, 0)) {
this->status_set_warning();
return;
}
auto f = std::bind(&MS8607Component::read_pressure_, this, d2_raw_temperature);
// datasheet says 17.2ms max conversion time at OSR 8192
this->set_timeout("pressure", 20, f);
}
void MS8607Component::read_pressure_(uint32_t d2_raw_temperature) {
uint8_t bytes[3]; // 24 bits
if (!this->read_bytes(MS8607_CMD_ADC_READ, bytes, 3)) {
this->status_set_warning();
return;
}
const uint32_t d1_raw_pressure = encode_uint32(0, bytes[0], bytes[1], bytes[2]);
this->calculate_values_(d2_raw_temperature, d1_raw_pressure);
}
void MS8607Component::request_read_humidity_(float temperature_float) {
if (!this->humidity_device_->write_bytes(MS8607_CMD_H_MEASURE_NO_HOLD, nullptr, 0)) {
ESP_LOGW(TAG, "Request to measure humidity failed");
this->status_set_warning();
return;
}
auto f = std::bind(&MS8607Component::read_humidity_, this, temperature_float);
// datasheet says 15.89ms max conversion time at OSR 8192
this->set_timeout("humidity", 20, f);
}
void MS8607Component::read_humidity_(float temperature_float) {
uint8_t bytes[3];
if (!this->humidity_device_->read_bytes_raw(bytes, 3)) {
ESP_LOGW(TAG, "Failed to read the measured humidity value");
this->status_set_warning();
return;
}
// "the measurement is stored into 14 bits. The two remaining LSBs are used for transmitting status information.
// Bit1 of the two LSBS must be set to '1'. Bit0 is currently not assigned"
uint16_t humidity = encode_uint16(bytes[0], bytes[1]);
uint8_t const expected_crc = bytes[2];
uint8_t const actual_crc = hsensor_crc_check(humidity);
if (expected_crc != actual_crc) {
ESP_LOGE(TAG, "Incorrect Humidity CRC value. Provided value 0x%01X != calculated value 0x%01X", expected_crc,
actual_crc);
this->status_set_warning();
return;
}
if (!(humidity & 0x2)) {
// data sheet says Bit1 should always set, but nothing about what happens if it isn't
ESP_LOGE(TAG, "Humidity status bit was not set to 1?");
}
humidity &= ~(0b11); // strip status & unassigned bits from data
// map 16 bit humidity value into range [-6%, 118%]
float const humidity_partial = double(humidity) / (1 << 16);
float const humidity_percentage = lerp(humidity_partial, -6.0, 118.0);
float const compensated_humidity_percentage =
humidity_percentage + (20 - temperature_float) * MS8607_H_TEMP_COEFFICIENT;
ESP_LOGD(TAG, "Compensated for temperature, humidity=%.2f%%", compensated_humidity_percentage);
if (this->humidity_sensor_ != nullptr) {
this->humidity_sensor_->publish_state(compensated_humidity_percentage);
}
this->status_clear_warning();
}
void MS8607Component::calculate_values_(uint32_t d2_raw_temperature, uint32_t d1_raw_pressure) {
// Perform the first order pressure/temperature calculation
// d_t: "difference between actual and reference temperature" = D2 - [C5] * 2**8
const int32_t d_t = int32_t(d2_raw_temperature) - (int32_t(this->calibration_values_.reference_temperature) << 8);
// actual temperature as hundredths of degree celsius in range [-4000, 8500]
// 2000 + d_t * [C6] / (2**23)
int32_t temperature =
2000 + ((int64_t(d_t) * this->calibration_values_.temperature_coefficient_of_temperature) >> 23);
// offset at actual temperature. [C2] * (2**17) + (d_t * [C4] / (2**6))
int64_t pressure_offset = (int64_t(this->calibration_values_.pressure_offset) << 17) +
((int64_t(d_t) * this->calibration_values_.pressure_offset_temperature_coefficient) >> 6);
// sensitivity at actual temperature. [C1] * (2**16) + ([C3] * d_t) / (2**7)
int64_t pressure_sensitivity =
(int64_t(this->calibration_values_.pressure_sensitivity) << 16) +
((int64_t(d_t) * this->calibration_values_.pressure_sensitivity_temperature_coefficient) >> 7);
// Perform the second order compensation, for non-linearity over temperature range
const int64_t d_t_squared = int64_t(d_t) * d_t;
int64_t temperature_2 = 0;
int32_t pressure_offset_2 = 0;
int32_t pressure_sensitivity_2 = 0;
if (temperature < 2000) {
// (TEMP - 2000)**2 / 2**4
const int32_t low_temperature_adjustment = (temperature - 2000) * (temperature - 2000) >> 4;
// T2 = 3 * (d_t**2) / 2**33
temperature_2 = (3 * d_t_squared) >> 33;
// OFF2 = 61 * (TEMP-2000)**2 / 2**4
pressure_offset_2 = 61 * low_temperature_adjustment;
// SENS2 = 29 * (TEMP-2000)**2 / 2**4
pressure_sensitivity_2 = 29 * low_temperature_adjustment;
if (temperature < -1500) {
// (TEMP+1500)**2
const int32_t very_low_temperature_adjustment = (temperature + 1500) * (temperature + 1500);
// OFF2 = OFF2 + 17 * (TEMP+1500)**2
pressure_offset_2 += 17 * very_low_temperature_adjustment;
// SENS2 = SENS2 + 9 * (TEMP+1500)**2
pressure_sensitivity_2 += 9 * very_low_temperature_adjustment;
}
} else {
// T2 = 5 * (d_t**2) / 2**38
temperature_2 = (5 * d_t_squared) >> 38;
}
temperature -= temperature_2;
pressure_offset -= pressure_offset_2;
pressure_sensitivity -= pressure_sensitivity_2;
// Temperature compensated pressure. [1000, 120000] => [10.00 mbar, 1200.00 mbar]
const int32_t pressure = (((d1_raw_pressure * pressure_sensitivity) >> 21) - pressure_offset) >> 15;
const float temperature_float = temperature / 100.0f;
const float pressure_float = pressure / 100.0f;
ESP_LOGD(TAG, "Temperature=%0.2f°C, Pressure=%0.2fhPa", temperature_float, pressure_float);
if (this->temperature_sensor_ != nullptr) {
this->temperature_sensor_->publish_state(temperature_float);
}
if (this->pressure_sensor_ != nullptr) {
this->pressure_sensor_->publish_state(pressure_float); // hPa aka mbar
}
this->status_clear_warning();
if (this->humidity_sensor_ != nullptr) {
// now that we have temperature (to compensate the humidity with), kick off that read
this->request_read_humidity_(temperature_float);
}
}
} // namespace ms8607
} // namespace esphome

View File

@ -0,0 +1,109 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome {
namespace ms8607 {
/**
Class for I2CDevice used to communicate with the Humidity sensor
on the chip. See MS8607Component instead
*/
class MS8607HumidityDevice : public i2c::I2CDevice {
public:
uint8_t get_address() { return address_; }
};
/**
Temperature, pressure, and humidity sensor.
By default, the MS8607 measures sensors at the highest resolution.
A potential enhancement would be to expose the resolution as a configurable
setting. A lower resolution speeds up ADC conversion time & uses less power.
Datasheet:
https://www.te.com/commerce/DocumentDelivery/DDEController?Action=showdoc&DocId=Data+Sheet%7FMS8607-02BA01%7FB3%7Fpdf%7FEnglish%7FENG_DS_MS8607-02BA01_B3.pdf%7FCAT-BLPS0018
Other implementations:
- https://github.com/TEConnectivity/MS8607_Generic_C_Driver
- https://github.com/adafruit/Adafruit_MS8607
- https://github.com/sparkfun/SparkFun_PHT_MS8607_Arduino_Library
*/
class MS8607Component : public PollingComponent, public i2c::I2CDevice {
public:
virtual ~MS8607Component() = default;
void setup() override;
void update() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; };
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; }
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; }
void set_humidity_device(MS8607HumidityDevice *humidity_device) { humidity_device_ = humidity_device; }
protected:
/**
Read and store the Pressure & Temperature calibration settings from the PROM.
Intended to be called during setup(), this will set the `failure_reason_`
*/
bool read_calibration_values_from_prom_();
/// Start async temperature read
void request_read_temperature_();
/// Process async temperature read
void read_temperature_();
/// start async pressure read
void request_read_pressure_(uint32_t raw_temperature);
/// process async pressure read
void read_pressure_(uint32_t raw_temperature);
/// start async humidity read
void request_read_humidity_(float temperature_float);
/// process async humidity read
void read_humidity_(float temperature_float);
/// use raw temperature & pressure to calculate & publish values
void calculate_values_(uint32_t raw_temperature, uint32_t raw_pressure);
sensor::Sensor *temperature_sensor_;
sensor::Sensor *pressure_sensor_;
sensor::Sensor *humidity_sensor_;
/** I2CDevice object to communicate with secondary I2C address for the humidity sensor
*
* The MS8607 only has one set of I2C pins, despite using two different addresses.
*
* Default address for humidity is 0x40
*/
MS8607HumidityDevice *humidity_device_;
/// This device's pressure & temperature calibration values, read from PROM
struct CalibrationValues {
/// Pressure sensitivity | SENS-T1. [C1]
uint16_t pressure_sensitivity;
/// Temperature coefficient of pressure sensitivity | TCS. [C3]
uint16_t pressure_sensitivity_temperature_coefficient;
/// Pressure offset | OFF-T1. [C2]
uint16_t pressure_offset;
/// Temperature coefficient of pressure offset | TCO. [C4]
uint16_t pressure_offset_temperature_coefficient;
/// Reference temperature | T-REF. [C5]
uint16_t reference_temperature;
/// Temperature coefficient of the temperature | TEMPSENS. [C6]
uint16_t temperature_coefficient_of_temperature;
} calibration_values_;
/// Possible failure reasons of this component
enum class ErrorCode;
/// Keep track of the reason why this component failed, to augment the dumped config
ErrorCode error_code_;
/// Current progress through required component setup
enum class SetupStatus;
/// Current step in the multi-step & possibly delayed setup() process
SetupStatus setup_status_;
};
} // namespace ms8607
} // namespace esphome

View File

@ -0,0 +1,83 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c, sensor
from esphome.const import (
CONF_HUMIDITY,
CONF_ID,
CONF_PRESSURE,
CONF_TEMPERATURE,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
UNIT_HECTOPASCAL,
UNIT_PERCENT,
)
DEPENDENCIES = ["i2c"]
ms8607_ns = cg.esphome_ns.namespace("ms8607")
MS8607Component = ms8607_ns.class_(
"MS8607Component", cg.PollingComponent, i2c.I2CDevice
)
CONF_HUMIDITY_I2C_ID = "humidity_i2c_id"
MS8607HumidityDevice = ms8607_ns.class_("MS8607HumidityDevice", i2c.I2CDevice)
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(MS8607Component),
cv.Required(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=2, # Resolution: 0.01
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Required(CONF_PRESSURE): sensor.sensor_schema(
unit_of_measurement=UNIT_HECTOPASCAL,
accuracy_decimals=2, # Resolution: 0.016
device_class=DEVICE_CLASS_PRESSURE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Required(CONF_HUMIDITY): sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
accuracy_decimals=2, # Resolution: 0.04
device_class=DEVICE_CLASS_HUMIDITY,
state_class=STATE_CLASS_MEASUREMENT,
)
.extend(
{
cv.GenerateID(CONF_HUMIDITY_I2C_ID): cv.declare_id(
MS8607HumidityDevice
),
}
)
.extend(i2c.i2c_device_schema(0x40)), # default address for humidity
}
)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x76)) # default address for temp/pressure
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
if temperature_config := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature_config)
cg.add(var.set_temperature_sensor(sens))
if pressure_config := config.get(CONF_PRESSURE):
sens = await sensor.new_sensor(pressure_config)
cg.add(var.set_pressure_sensor(sens))
if humidity_config := config.get(CONF_HUMIDITY):
sens = await sensor.new_sensor(humidity_config)
cg.add(var.set_humidity_sensor(sens))
humidity_device = cg.new_Pvariable(humidity_config[CONF_HUMIDITY_I2C_ID])
await i2c.register_i2c_device(humidity_device, humidity_config)
cg.add(var.set_humidity_device(humidity_device))

View File

@ -5,6 +5,7 @@ from esphome.components.esp32 import add_idf_sdkconfig_option
from esphome.const import (
CONF_ENABLE_IPV6,
CONF_MIN_IPV6_ADDR_COUNT,
)
CODEOWNERS = ["@esphome/core"]
@ -16,12 +17,14 @@ IPAddress = network_ns.class_("IPAddress")
CONFIG_SCHEMA = cv.Schema(
{
cv.Optional(CONF_ENABLE_IPV6, default=False): cv.boolean,
cv.Optional(CONF_MIN_IPV6_ADDR_COUNT, default=0): cv.positive_int,
}
)
async def to_code(config):
cg.add_define("ENABLE_IPV6", config[CONF_ENABLE_IPV6])
cg.add_define("USE_NETWORK_IPV6", config[CONF_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(

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