Merge branch 'dev' into optolink

This commit is contained in:
j0ta29 2024-03-24 09:04:39 +01:00 committed by GitHub
commit cae27ccd29
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
394 changed files with 8241 additions and 501 deletions

View file

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

View file

@ -22,7 +22,7 @@ runs:
python-version: ${{ inputs.python-version }}
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache/restore@v4.0.1
uses: actions/cache/restore@v4.0.2
with:
path: venv
# yamllint disable-line rule:line-length

81
.github/workflows/ci-api-proto.yml vendored Normal file
View file

@ -0,0 +1,81 @@
name: API Proto CI
# yamllint disable-line rule:truthy
on:
pull_request:
paths:
- "esphome/components/api/api.proto"
- "esphome/components/api/api_pb2.cpp"
- "esphome/components/api/api_pb2.h"
- "esphome/components/api/api_pb2_service.cpp"
- "esphome/components/api/api_pb2_service.h"
- "script/api_protobuf/api_protobuf.py"
- ".github/workflows/ci-api-proto.yml"
permissions:
contents: read
pull-requests: write
jobs:
check:
name: Check generated files
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4.1.1
- name: Set up Python
uses: actions/setup-python@v5.0.0
with:
python-version: "3.11"
- name: Install apt dependencies
run: |
sudo apt update
sudo apt-cache show protobuf-compiler
sudo apt install -y protobuf-compiler
protoc --version
- name: Install python dependencies
run: pip install aioesphomeapi -c requirements.txt -r requirements_dev.txt
- name: Generate files
run: script/api_protobuf/api_protobuf.py
- name: Check for changes
run: |
if ! git diff --quiet; then
echo "## Job Failed" | tee -a $GITHUB_STEP_SUMMARY
echo "You have altered the generated proto files but they do not match what is expected." | tee -a $GITHUB_STEP_SUMMARY
echo "Please run 'script/api_protobuf/api_protobuf.py' and commit the changes." | tee -a $GITHUB_STEP_SUMMARY
exit 1
fi
- if: failure()
name: Review PR
uses: actions/github-script@v7.0.1
with:
script: |
await github.rest.pulls.createReview({
pull_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
event: 'REQUEST_CHANGES',
body: 'You have altered the generated proto files but they do not match what is expected.\nPlease run "script/api_protobuf/api_protobuf.py" and commit the changes.'
})
- if: success()
name: Dismiss review
uses: actions/github-script@v7.0.1
with:
script: |
let reviews = await github.rest.pulls.listReviews({
pull_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo
});
for (let review of reviews.data) {
if (review.user.login === 'github-actions[bot]' && review.state === 'CHANGES_REQUESTED') {
await github.rest.pulls.dismissReview({
pull_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
review_id: review.id,
message: 'Files now match the expected proto files.'
});
}
}

View file

@ -46,7 +46,7 @@ jobs:
with:
python-version: "3.9"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.1.0
uses: docker/setup-buildx-action@v3.2.0
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.0.0

View file

@ -47,7 +47,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache@v4.0.1
uses: actions/cache@v4.0.2
with:
path: venv
# yamllint disable-line rule:line-length
@ -367,7 +367,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache platformio
uses: actions/cache@v4.0.1
uses: actions/cache@v4.0.2
with:
path: ~/.platformio
# yamllint disable-line rule:line-length
@ -398,6 +398,7 @@ jobs:
runs-on: ubuntu-latest
needs:
- common
if: github.event_name == 'pull_request'
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
@ -406,10 +407,14 @@ jobs:
with:
# Fetch enough history so `git merge-base refs/remotes/origin/dev HEAD` works.
fetch-depth: 500
- name: Fetch dev branch
- name: Get target branch
id: target-branch
run: |
git -c protocol.version=2 fetch --no-tags --prune --no-recurse-submodules --depth=1 origin +refs/heads/dev*:refs/remotes/origin/dev* +refs/tags/dev*:refs/tags/dev*
git merge-base refs/remotes/origin/dev HEAD
echo "branch=${{ github.event.pull_request.base.ref }}" >> $GITHUB_OUTPUT
- name: Fetch ${{ steps.target-branch.outputs.branch }} branch
run: |
git -c protocol.version=2 fetch --no-tags --prune --no-recurse-submodules --depth=1 origin +refs/heads/${{ steps.target-branch.outputs.branch }}:refs/remotes/origin/${{ steps.target-branch.outputs.branch }}
git merge-base refs/remotes/origin/${{ steps.target-branch.outputs.branch }} HEAD
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@ -419,7 +424,7 @@ jobs:
id: set-matrix
run: |
. venv/bin/activate
echo "matrix=$(script/list-components.py --changed | jq -R -s -c 'split("\n")[:-1]')" >> $GITHUB_OUTPUT
echo "matrix=$(script/list-components.py --changed --branch ${{ steps.target-branch.outputs.branch }} | jq -R -s -c 'split("\n")[:-1]')" >> $GITHUB_OUTPUT
test-build-components:
name: Component test ${{ matrix.file }}
@ -427,7 +432,7 @@ jobs:
needs:
- common
- list-components
if: ${{ needs.list-components.outputs.matrix != '[]' && needs.list-components.outputs.matrix != '' }}
if: ${{ github.event_name == 'pull_request' && needs.list-components.outputs.matrix != '[]' && needs.list-components.outputs.matrix != '' }}
strategy:
fail-fast: false
max-parallel: 2

View file

@ -85,18 +85,18 @@ jobs:
python-version: "3.9"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.1.0
uses: docker/setup-buildx-action@v3.2.0
- name: Set up QEMU
if: matrix.platform != 'linux/amd64'
uses: docker/setup-qemu-action@v3.0.0
- name: Log in to docker hub
uses: docker/login-action@v3.0.0
uses: docker/login-action@v3.1.0
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the GitHub container registry
uses: docker/login-action@v3.0.0
uses: docker/login-action@v3.1.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@ -163,17 +163,17 @@ jobs:
name: digests-${{ matrix.image.target }}-${{ matrix.registry }}
path: /tmp/digests
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.1.0
uses: docker/setup-buildx-action@v3.2.0
- name: Log in to docker hub
if: matrix.registry == 'dockerhub'
uses: docker/login-action@v3.0.0
uses: docker/login-action@v3.1.0
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the GitHub container registry
if: matrix.registry == 'ghcr'
uses: docker/login-action@v3.0.0
uses: docker/login-action@v3.1.0
with:
registry: ghcr.io
username: ${{ github.actor }}

View file

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

View file

@ -103,6 +103,7 @@ esphome/components/duty_time/* @dudanov
esphome/components/ee895/* @Stock-M
esphome/components/ektf2232/touchscreen/* @jesserockz
esphome/components/emc2101/* @ellull
esphome/components/emmeti/* @E440QF
esphome/components/ens160/* @vincentscode
esphome/components/ens210/* @itn3rd77
esphome/components/esp32/* @esphome/core
@ -152,6 +153,7 @@ esphome/components/honeywellabp2_i2c/* @jpfaff
esphome/components/host/* @esphome/core
esphome/components/hrxl_maxsonar_wr/* @netmikey
esphome/components/hte501/* @Stock-M
esphome/components/htu31d/* @betterengineering
esphome/components/hydreon_rgxx/* @functionpointer
esphome/components/hyt271/* @Philippe12
esphome/components/i2c/* @esphome/core
@ -171,6 +173,7 @@ esphome/components/integration/* @OttoWinter
esphome/components/internal_temperature/* @Mat931
esphome/components/interval/* @esphome/core
esphome/components/json/* @OttoWinter
esphome/components/kamstrup_kmp/* @cfeenstra1024
esphome/components/key_collector/* @ssieb
esphome/components/key_provider/* @ssieb
esphome/components/kuntze/* @ssieb
@ -276,6 +279,7 @@ esphome/components/pvvx_mithermometer/* @pasiz
esphome/components/pylontech/* @functionpointer
esphome/components/qmp6988/* @andrewpc
esphome/components/qr_code/* @wjtje
esphome/components/qspi_amoled/* @clydebarrow
esphome/components/qwiic_pir/* @kahrendt
esphome/components/radon_eye_ble/* @jeffeb3
esphome/components/radon_eye_rd200/* @jeffeb3
@ -348,6 +352,7 @@ esphome/components/st7789v/* @kbx81
esphome/components/st7920/* @marsjan155
esphome/components/substitutions/* @esphome/core
esphome/components/sun/* @OttoWinter
esphome/components/sun_gtil2/* @Mat931
esphome/components/switch/* @esphome/core
esphome/components/t6615/* @tylermenezes
esphome/components/tca9548a/* @andreashergert1984

View file

@ -41,6 +41,7 @@ CONF_CURRENT_GAIN_A = "current_gain_a"
CONF_CURRENT_GAIN_B = "current_gain_b"
CONF_ACTIVE_POWER_GAIN_A = "active_power_gain_a"
CONF_ACTIVE_POWER_GAIN_B = "active_power_gain_b"
CONF_USE_ACCUMULATED_ENERGY_REGISTERS = "use_accumulated_energy_registers"
PGA_GAINS = {
"1x": 0b000,
"2x": 0b001,
@ -155,6 +156,7 @@ ADE7953_CONFIG_SCHEMA = cv.Schema(
cv.Optional(CONF_ACTIVE_POWER_GAIN_B, default=0x400000): cv.hex_int_range(
min=0x100000, max=0x800000
),
cv.Optional(CONF_USE_ACCUMULATED_ENERGY_REGISTERS, default=False): cv.boolean,
}
).extend(cv.polling_component_schema("60s"))
@ -174,6 +176,9 @@ async def register_ade7953(var, config):
cg.add(var.set_bigain(config.get(CONF_CURRENT_GAIN_B)))
cg.add(var.set_awgain(config.get(CONF_ACTIVE_POWER_GAIN_A)))
cg.add(var.set_bwgain(config.get(CONF_ACTIVE_POWER_GAIN_B)))
cg.add(
var.set_use_acc_energy_regs(config.get(CONF_USE_ACCUMULATED_ENERGY_REGISTERS))
)
for key in [
CONF_VOLTAGE,

View file

@ -6,6 +6,9 @@ namespace ade7953_base {
static const char *const TAG = "ade7953";
static const float ADE_POWER_FACTOR = 154.0f;
static const float ADE_WATTSEC_POWER_FACTOR = ADE_POWER_FACTOR * ADE_POWER_FACTOR / 3600;
void ADE7953::setup() {
if (this->irq_pin_ != nullptr) {
this->irq_pin_->setup();
@ -34,6 +37,7 @@ void ADE7953::setup() {
this->ade_read_32(BIGAIN_32, &bigain_);
this->ade_read_32(AWGAIN_32, &awgain_);
this->ade_read_32(BWGAIN_32, &bwgain_);
this->last_update_ = millis();
this->is_setup_ = true;
});
}
@ -52,6 +56,7 @@ void ADE7953::dump_config() {
LOG_SENSOR(" ", "Active Power B Sensor", this->active_power_b_sensor_);
LOG_SENSOR(" ", "Rective Power A Sensor", this->reactive_power_a_sensor_);
LOG_SENSOR(" ", "Reactive Power B Sensor", this->reactive_power_b_sensor_);
ESP_LOGCONFIG(TAG, " USE_ACC_ENERGY_REGS: %d", this->use_acc_energy_regs_);
ESP_LOGCONFIG(TAG, " PGA_V_8: 0x%X", pga_v_);
ESP_LOGCONFIG(TAG, " PGA_IA_8: 0x%X", pga_ia_);
ESP_LOGCONFIG(TAG, " PGA_IB_8: 0x%X", pga_ib_);
@ -85,6 +90,7 @@ void ADE7953::update() {
uint32_t val;
uint16_t val_16;
uint16_t reg;
// Power factor
err = this->ade_read_16(0x010A, &val_16);
@ -92,23 +98,36 @@ void ADE7953::update() {
err = this->ade_read_16(0x010B, &val_16);
ADE_PUBLISH(power_factor_b, (int16_t) val_16, (0x7FFF / 100.0f));
float pf = ADE_POWER_FACTOR;
if (this->use_acc_energy_regs_) {
const uint32_t now = millis();
const auto diff = now - this->last_update_;
this->last_update_ = now;
// prevent DIV/0
pf = ADE_WATTSEC_POWER_FACTOR * (diff < 10 ? 10 : diff) / 1000;
ESP_LOGVV(TAG, "ADE7953::update() diff=%d pf=%f", diff, pf);
}
// Apparent power
err = this->ade_read_32(0x0310, &val);
ADE_PUBLISH(apparent_power_a, (int32_t) val, 154.0f);
err = this->ade_read_32(0x0311, &val);
ADE_PUBLISH(apparent_power_b, (int32_t) val, 154.0f);
reg = this->use_acc_energy_regs_ ? 0x0322 : 0x0310;
err = this->ade_read_32(reg, &val);
ADE_PUBLISH(apparent_power_a, (int32_t) val, pf);
err = this->ade_read_32(reg + 1, &val);
ADE_PUBLISH(apparent_power_b, (int32_t) val, pf);
// Active power
err = this->ade_read_32(0x0312, &val);
ADE_PUBLISH(active_power_a, (int32_t) val, 154.0f);
err = this->ade_read_32(0x0313, &val);
ADE_PUBLISH(active_power_b, (int32_t) val, 154.0f);
reg = this->use_acc_energy_regs_ ? 0x031E : 0x0312;
err = this->ade_read_32(reg, &val);
ADE_PUBLISH(active_power_a, (int32_t) val, pf);
err = this->ade_read_32(reg + 1, &val);
ADE_PUBLISH(active_power_b, (int32_t) val, pf);
// Reactive power
err = this->ade_read_32(0x0314, &val);
ADE_PUBLISH(reactive_power_a, (int32_t) val, 154.0f);
err = this->ade_read_32(0x0315, &val);
ADE_PUBLISH(reactive_power_b, (int32_t) val, 154.0f);
reg = this->use_acc_energy_regs_ ? 0x0320 : 0x0314;
err = this->ade_read_32(reg, &val);
ADE_PUBLISH(reactive_power_a, (int32_t) val, pf);
err = this->ade_read_32(reg + 1, &val);
ADE_PUBLISH(reactive_power_b, (int32_t) val, pf);
// Current
err = this->ade_read_32(0x031A, &val);

View file

@ -52,6 +52,8 @@ class ADE7953 : public PollingComponent, public sensor::Sensor {
void set_awgain(uint32_t awgain) { awgain_ = awgain; }
void set_bwgain(uint32_t bwgain) { bwgain_ = bwgain; }
void set_use_acc_energy_regs(bool use_acc_energy_regs) { use_acc_energy_regs_ = use_acc_energy_regs; }
void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; }
void set_frequency_sensor(sensor::Sensor *frequency_sensor) { frequency_sensor_ = frequency_sensor; }
@ -103,6 +105,8 @@ class ADE7953 : public PollingComponent, public sensor::Sensor {
uint32_t bigain_;
uint32_t awgain_;
uint32_t bwgain_;
bool use_acc_energy_regs_{false};
uint32_t last_update_;
virtual bool ade_write_8(uint16_t reg, uint8_t value) = 0;

View file

@ -13,29 +13,29 @@ void AdE7953I2c::dump_config() {
ade7953_base::ADE7953::dump_config();
}
bool AdE7953I2c::ade_write_8(uint16_t reg, uint8_t value) {
std::vector<uint8_t> data(3);
data.push_back(reg >> 8);
data.push_back(reg >> 0);
data.push_back(value);
return this->write(data.data(), data.size()) != i2c::ERROR_OK;
uint8_t data[3];
data[0] = reg >> 8;
data[1] = reg >> 0;
data[2] = value;
return this->write(data, 3) != i2c::ERROR_OK;
}
bool AdE7953I2c::ade_write_16(uint16_t reg, uint16_t value) {
std::vector<uint8_t> data(4);
data.push_back(reg >> 8);
data.push_back(reg >> 0);
data.push_back(value >> 8);
data.push_back(value >> 0);
return this->write(data.data(), data.size()) != i2c::ERROR_OK;
uint8_t data[4];
data[0] = reg >> 8;
data[1] = reg >> 0;
data[2] = value >> 8;
data[3] = value >> 0;
return this->write(data, 4) != i2c::ERROR_OK;
}
bool AdE7953I2c::ade_write_32(uint16_t reg, uint32_t value) {
std::vector<uint8_t> data(6);
data.push_back(reg >> 8);
data.push_back(reg >> 0);
data.push_back(value >> 24);
data.push_back(value >> 16);
data.push_back(value >> 8);
data.push_back(value >> 0);
return this->write(data.data(), data.size()) != i2c::ERROR_OK;
uint8_t data[6];
data[0] = reg >> 8;
data[1] = reg >> 0;
data[2] = value >> 24;
data[3] = value >> 16;
data[4] = value >> 8;
data[5] = value >> 0;
return this->write(data, 6) != i2c::ERROR_OK;
}
bool AdE7953I2c::ade_read_8(uint16_t reg, uint8_t *value) {
uint8_t reg_data[2];

View file

@ -15,7 +15,6 @@
#include "aht10.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include <cinttypes>
namespace esphome {
namespace aht10 {
@ -27,7 +26,7 @@ static const uint8_t AHT10_MEASURE_CMD[] = {0xAC, 0x33, 0x00};
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_READ_DELAY = 80; // ms, time to wait for conversion result
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
@ -83,74 +82,77 @@ void AHT10Component::setup() {
ESP_LOGV(TAG, "AHT10 initialization");
}
void AHT10Component::update() {
if (this->write(AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD)) != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Communication with AHT10 failed!");
this->status_set_warning();
void AHT10Component::restart_read_() {
if (this->read_count_ == AHT10_ATTEMPTS) {
this->read_count_ = 0;
this->status_set_error("Measurements reading timed-out!");
return;
}
this->read_count_++;
this->set_timeout(AHT10_READ_DELAY, [this]() { this->read_data_(); });
}
void AHT10Component::read_data_() {
uint8_t data[6];
uint8_t delay_ms = AHT10_DEFAULT_DELAY;
if (this->humidity_sensor_ != nullptr)
delay_ms = AHT10_HUMIDITY_DELAY;
bool success = false;
for (int i = 0; i < AHT10_ATTEMPTS; ++i) {
ESP_LOGVV(TAG, "Attempt %d at %6" PRIu32, i, millis());
delay(delay_ms);
if (this->read_count_ > 1)
ESP_LOGD(TAG, "Read attempt %d at %ums", this->read_count_, (unsigned) (millis() - this->start_time_));
if (this->read(data, 6) != i2c::ERROR_OK) {
ESP_LOGD(TAG, "Communication with AHT10 failed, waiting...");
continue;
this->status_set_warning("AHT10 read failed, retrying soon");
this->restart_read_();
return;
}
if ((data[0] & 0x80) == 0x80) { // Bit[7] = 0b1, device is busy
ESP_LOGD(TAG, "AHT10 is busy, waiting...");
} else if (data[1] == 0x0 && data[2] == 0x0 && (data[3] >> 4) == 0x0) {
this->restart_read_();
return;
}
if (data[1] == 0x0 && data[2] == 0x0 && (data[3] >> 4) == 0x0) {
// Unrealistic humidity (0x0)
if (this->humidity_sensor_ == nullptr) {
ESP_LOGVV(TAG, "ATH10 Unrealistic humidity (0x0), but humidity is not required");
break;
ESP_LOGV(TAG, "ATH10 Unrealistic humidity (0x0), but humidity is not required");
} else {
ESP_LOGD(TAG, "ATH10 Unrealistic humidity (0x0), retrying...");
if (this->write(AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD)) != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Communication with AHT10 failed!");
this->status_set_warning();
this->status_set_warning("Communication with AHT10 failed!");
}
this->restart_read_();
return;
}
}
} else {
// data is valid, we can break the loop
ESP_LOGVV(TAG, "Answer at %6" PRIu32, millis());
success = true;
break;
}
}
if (!success || (data[0] & 0x80) == 0x80) {
ESP_LOGE(TAG, "Measurements reading timed-out!");
this->status_set_warning();
return;
}
if (this->read_count_ > 1)
ESP_LOGD(TAG, "Success at %ums", (unsigned) (millis() - this->start_time_));
uint32_t raw_temperature = ((data[3] & 0x0F) << 16) | (data[4] << 8) | data[5];
uint32_t raw_humidity = ((data[1] << 16) | (data[2] << 8) | data[3]) >> 4;
if (this->temperature_sensor_ != nullptr) {
float temperature = ((200.0f * (float) raw_temperature) / 1048576.0f) - 50.0f;
this->temperature_sensor_->publish_state(temperature);
}
if (this->humidity_sensor_ != nullptr) {
float humidity;
if (raw_humidity == 0) { // unrealistic value
humidity = NAN;
} else {
humidity = (float) raw_humidity * 100.0f / 1048576.0f;
}
if (this->temperature_sensor_ != nullptr) {
this->temperature_sensor_->publish_state(temperature);
}
if (this->humidity_sensor_ != nullptr) {
if (std::isnan(humidity)) {
ESP_LOGW(TAG, "Invalid humidity! Sensor reported 0%% Hum");
}
this->humidity_sensor_->publish_state(humidity);
}
this->status_clear_warning();
this->read_count_ = 0;
}
void AHT10Component::update() {
if (this->read_count_ != 0)
return;
this->start_time_ = millis();
if (this->write(AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD)) != i2c::ERROR_OK) {
this->status_set_warning("Communication with AHT10 failed!");
return;
}
this->restart_read_();
}
float AHT10Component::get_setup_priority() const { return setup_priority::DATA; }

View file

@ -26,6 +26,10 @@ class AHT10Component : public PollingComponent, public i2c::I2CDevice {
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *humidity_sensor_{nullptr};
AHT10Variant variant_{};
unsigned read_count_{};
void read_data_();
void restart_read_();
uint32_t start_time_{};
};
} // namespace aht10

View file

@ -2,8 +2,10 @@ import base64
import secrets
from pathlib import Path
from typing import Optional
import re
import requests
from ruamel.yaml import YAML
import esphome.codegen as cg
import esphome.config_validation as cv
@ -11,7 +13,6 @@ import esphome.final_validate as fv
from esphome import git
from esphome.components.packages import validate_source_shorthand
from esphome.const import CONF_REF, CONF_WIFI, CONF_ESPHOME, CONF_PROJECT
from esphome.wizard import wizard_file
from esphome.yaml_util import dump
dashboard_import_ns = cg.esphome_ns.namespace("dashboard_import")
@ -94,44 +95,6 @@ def import_config(
if p.exists():
raise FileExistsError
if project_name == "esphome.web":
if "esp32c3" in import_url:
board = "esp32-c3-devkitm-1"
platform = "ESP32"
elif "esp32s2" in import_url:
board = "esp32-s2-saola-1"
platform = "ESP32"
elif "esp32s3" in import_url:
board = "esp32-s3-devkitc-1"
platform = "ESP32"
elif "esp32" in import_url:
board = "esp32dev"
platform = "ESP32"
elif "esp8266" in import_url:
board = "esp01_1m"
platform = "ESP8266"
elif "pico-w" in import_url:
board = "pico-w"
platform = "RP2040"
kwargs = {
"name": name,
"friendly_name": friendly_name,
"platform": platform,
"board": board,
"ssid": "!secret wifi_ssid",
"psk": "!secret wifi_password",
}
if encryption:
noise_psk = secrets.token_bytes(32)
key = base64.b64encode(noise_psk).decode()
kwargs["api_encryption_key"] = key
p.write_text(
wizard_file(**kwargs),
encoding="utf8",
)
else:
git_file = git.GitFile.from_shorthand(import_url)
if git_file.query and "full_config" in git_file.query:
@ -142,7 +105,44 @@ def import_config(
except requests.exceptions.RequestException as e:
raise ValueError(f"Error while fetching {url}: {e}") from e
p.write_text(req.text, encoding="utf8")
contents = req.text
yaml = YAML()
loaded_yaml = yaml.load(contents)
if (
"name_add_mac_suffix" in loaded_yaml["esphome"]
and loaded_yaml["esphome"]["name_add_mac_suffix"]
):
loaded_yaml["esphome"]["name_add_mac_suffix"] = False
name_val = loaded_yaml["esphome"]["name"]
sub_pattern = re.compile(r"\$\{?([a-zA-Z-_]+)\}?")
if match := sub_pattern.match(name_val):
name_sub = match.group(1)
if name_sub in loaded_yaml["substitutions"]:
loaded_yaml["substitutions"][name_sub] = name
else:
raise ValueError(
f"Name substitution {name_sub} not found in substitutions"
)
else:
loaded_yaml["esphome"]["name"] = name
if friendly_name is not None:
friendly_name_val = loaded_yaml["esphome"]["friendly_name"]
if match := sub_pattern.match(friendly_name_val):
friendly_name_sub = match.group(1)
if friendly_name_sub in loaded_yaml["substitutions"]:
loaded_yaml["substitutions"][friendly_name_sub] = friendly_name
else:
raise ValueError(
f"Friendly name substitution {friendly_name_sub} not found in substitutions"
)
else:
loaded_yaml["esphome"]["friendly_name"] = friendly_name
with p.open("w", encoding="utf8") as f:
yaml.dump(loaded_yaml, f)
else:
with p.open("w", encoding="utf8") as f:
f.write(contents)
else:
substitutions = {"name": name}

View file

@ -81,7 +81,7 @@ void DeepSleepComponent::set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode) {
#endif
#if defined(USE_ESP32)
#if !defined(USE_ESP32_VARIANT_ESP32C3)
#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32C6)
void DeepSleepComponent::set_ext1_wakeup(Ext1Wakeup ext1_wakeup) { this->ext1_wakeup_ = ext1_wakeup; }
@ -121,7 +121,7 @@ void DeepSleepComponent::begin_sleep(bool manual) {
App.run_safe_shutdown_hooks();
#if defined(USE_ESP32)
#if !defined(USE_ESP32_VARIANT_ESP32C3)
#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32C6)
if (this->sleep_duration_.has_value())
esp_sleep_enable_timer_wakeup(*this->sleep_duration_);
if (this->wakeup_pin_ != nullptr) {
@ -140,7 +140,7 @@ void DeepSleepComponent::begin_sleep(bool manual) {
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
}
#endif
#ifdef USE_ESP32_VARIANT_ESP32C3
#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6)
if (this->sleep_duration_.has_value())
esp_sleep_enable_timer_wakeup(*this->sleep_duration_);
if (this->wakeup_pin_ != nullptr) {

View file

@ -1,7 +1,7 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import automation
from esphome.const import CONF_ID, CONF_TRIGGER_ID, CONF_FILE, CONF_DEVICE
from esphome.const import CONF_ID, CONF_TRIGGER_ID, CONF_FILE, CONF_DEVICE, CONF_VOLUME
from esphome.components import uart
DEPENDENCIES = ["uart"]
@ -19,7 +19,6 @@ DFPlayerIsPlayingCondition = dfplayer_ns.class_(
MULTI_CONF = True
CONF_FOLDER = "folder"
CONF_LOOP = "loop"
CONF_VOLUME = "volume"
CONF_EQ_PRESET = "eq_preset"
CONF_ON_FINISHED_PLAYBACK = "on_finished_playback"

View file

@ -36,6 +36,21 @@ void HOT Display::line(int x1, int y1, int x2, int y2, Color color) {
}
}
void Display::line_at_angle(int x, int y, int angle, int length, Color color) {
this->line_at_angle(x, y, angle, 0, length, color);
}
void Display::line_at_angle(int x, int y, int angle, int start_radius, int stop_radius, Color color) {
// Calculate start and end points
int x1 = (start_radius * cos(angle * M_PI / 180)) + x;
int y1 = (start_radius * sin(angle * M_PI / 180)) + y;
int x2 = (stop_radius * cos(angle * M_PI / 180)) + x;
int y2 = (stop_radius * sin(angle * M_PI / 180)) + y;
// Draw line
this->line(x1, y1, x2, y2, color);
}
void Display::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
size_t line_stride = x_offset + w + x_pad; // length of each source line in pixels

View file

@ -258,6 +258,13 @@ class Display : public PollingComponent {
/// Draw a straight line from the point [x1,y1] to [x2,y2] with the given color.
void line(int x1, int y1, int x2, int y2, Color color = COLOR_ON);
/// Draw a straight line at the given angle based on the origin [x, y] for a specified length with the given color.
void line_at_angle(int x, int y, int angle, int length, Color color = COLOR_ON);
/// Draw a straight line at the given angle based on the origin [x, y] from a specified start and stop radius with the
/// given color.
void line_at_angle(int x, int y, int angle, int start_radius, int stop_radius, Color color = COLOR_ON);
/// Draw a horizontal line from the point [x,y] to [x+width,y] with the given color.
void horizontal_line(int x, int y, int width, Color color = COLOR_ON);

View file

View file

@ -0,0 +1,21 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import climate_ir
from esphome.const import CONF_ID
CODEOWNERS = ["@E440QF"]
AUTO_LOAD = ["climate_ir"]
emmeti_ns = cg.esphome_ns.namespace("emmeti")
EmmetiClimate = emmeti_ns.class_("EmmetiClimate", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(EmmetiClimate),
}
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await climate_ir.register_climate_ir(var, config)

View file

@ -0,0 +1,316 @@
#include "emmeti.h"
#include "esphome/components/remote_base/remote_base.h"
namespace esphome {
namespace emmeti {
static const char *const TAG = "emmeti.climate";
// setters
uint8_t EmmetiClimate::set_temp_() {
return (uint8_t) roundf(clamp<float>(this->target_temperature, EMMETI_TEMP_MIN, EMMETI_TEMP_MAX) - EMMETI_TEMP_MIN);
}
uint8_t EmmetiClimate::set_mode_() {
switch (this->mode) {
case climate::CLIMATE_MODE_COOL:
return EMMETI_MODE_COOL;
case climate::CLIMATE_MODE_DRY:
return EMMETI_MODE_DRY;
case climate::CLIMATE_MODE_HEAT:
return EMMETI_MODE_HEAT;
case climate::CLIMATE_MODE_FAN_ONLY:
return EMMETI_MODE_FAN;
case climate::CLIMATE_MODE_HEAT_COOL:
default:
return EMMETI_MODE_HEAT_COOL;
}
}
uint8_t EmmetiClimate::set_fan_speed_() {
switch (this->fan_mode.value()) {
case climate::CLIMATE_FAN_LOW:
return EMMETI_FAN_1;
case climate::CLIMATE_FAN_MEDIUM:
return EMMETI_FAN_2;
case climate::CLIMATE_FAN_HIGH:
return EMMETI_FAN_3;
case climate::CLIMATE_FAN_AUTO:
default:
return EMMETI_FAN_AUTO;
}
}
uint8_t EmmetiClimate::set_blades_() {
if (this->swing_mode == climate::CLIMATE_SWING_VERTICAL) {
switch (this->blades_) {
case EMMETI_BLADES_1:
case EMMETI_BLADES_2:
case EMMETI_BLADES_HIGH:
this->blades_ = EMMETI_BLADES_HIGH;
break;
case EMMETI_BLADES_3:
case EMMETI_BLADES_MID:
this->blades_ = EMMETI_BLADES_MID;
break;
case EMMETI_BLADES_4:
case EMMETI_BLADES_5:
case EMMETI_BLADES_LOW:
this->blades_ = EMMETI_BLADES_LOW;
break;
default:
this->blades_ = EMMETI_BLADES_FULL;
break;
}
} else {
switch (this->blades_) {
case EMMETI_BLADES_1:
case EMMETI_BLADES_2:
case EMMETI_BLADES_HIGH:
this->blades_ = EMMETI_BLADES_1;
break;
case EMMETI_BLADES_3:
case EMMETI_BLADES_MID:
this->blades_ = EMMETI_BLADES_3;
break;
case EMMETI_BLADES_4:
case EMMETI_BLADES_5:
case EMMETI_BLADES_LOW:
this->blades_ = EMMETI_BLADES_5;
break;
default:
this->blades_ = EMMETI_BLADES_STOP;
break;
}
}
return this->blades_;
}
uint8_t EmmetiClimate::gen_checksum_() { return (this->set_temp_() + this->set_mode_() + 2) % 16; }
// getters
float EmmetiClimate::get_temp_(uint8_t temp) { return (float) (temp + EMMETI_TEMP_MIN); }
climate::ClimateMode EmmetiClimate::get_mode_(uint8_t mode) {
switch (mode) {
case EMMETI_MODE_COOL:
return climate::CLIMATE_MODE_COOL;
case EMMETI_MODE_DRY:
return climate::CLIMATE_MODE_DRY;
case EMMETI_MODE_HEAT:
return climate::CLIMATE_MODE_HEAT;
case EMMETI_MODE_HEAT_COOL:
return climate::CLIMATE_MODE_HEAT_COOL;
case EMMETI_MODE_FAN:
return climate::CLIMATE_MODE_FAN_ONLY;
default:
return climate::CLIMATE_MODE_HEAT_COOL;
}
}
climate::ClimateFanMode EmmetiClimate::get_fan_speed_(uint8_t fan_speed) {
switch (fan_speed) {
case EMMETI_FAN_1:
return climate::CLIMATE_FAN_LOW;
case EMMETI_FAN_2:
return climate::CLIMATE_FAN_MEDIUM;
case EMMETI_FAN_3:
return climate::CLIMATE_FAN_HIGH;
case EMMETI_FAN_AUTO:
default:
return climate::CLIMATE_FAN_AUTO;
}
}
climate::ClimateSwingMode EmmetiClimate::get_swing_(uint8_t bitmap) {
return (bitmap >> 1) & 0x01 ? climate::CLIMATE_SWING_VERTICAL : climate::CLIMATE_SWING_OFF;
}
template<typename T> T EmmetiClimate::reverse_(T val, size_t len) {
T result = 0;
for (size_t i = 0; i < len; i++) {
result |= ((val & 1 << i) != 0) << (len - 1 - i);
}
return result;
}
template<typename T> void EmmetiClimate::add_(T val, size_t len, esphome::remote_base::RemoteTransmitData *data) {
for (size_t i = len; i > 0; i--) {
data->mark(EMMETI_BIT_MARK);
data->space((val & (1 << (i - 1))) ? EMMETI_ONE_SPACE : EMMETI_ZERO_SPACE);
}
}
template<typename T> void EmmetiClimate::add_(T val, esphome::remote_base::RemoteTransmitData *data) {
data->mark(EMMETI_BIT_MARK);
data->space((val & 1) ? EMMETI_ONE_SPACE : EMMETI_ZERO_SPACE);
}
template<typename T>
void EmmetiClimate::reverse_add_(T val, size_t len, esphome::remote_base::RemoteTransmitData *data) {
this->add_(this->reverse_(val, len), len, data);
}
bool EmmetiClimate::check_checksum_(uint8_t checksum) {
uint8_t expected = this->gen_checksum_();
ESP_LOGV(TAG, "Expected checksum: %X", expected);
ESP_LOGV(TAG, "Checksum received: %X", checksum);
return checksum == expected;
}
void EmmetiClimate::transmit_state() {
auto transmit = this->transmitter_->transmit();
auto *data = transmit.get_data();
data->set_carrier_frequency(EMMETI_IR_FREQUENCY);
data->mark(EMMETI_HEADER_MARK);
data->space(EMMETI_HEADER_SPACE);
if (this->mode != climate::CLIMATE_MODE_OFF) {
this->reverse_add_(this->set_mode_(), 3, data);
this->add_(1, data);
this->reverse_add_(this->set_fan_speed_(), 2, data);
this->add_(this->swing_mode != climate::CLIMATE_SWING_OFF, data);
this->add_(0, data); // sleep mode
this->reverse_add_(this->set_temp_(), 4, data);
this->add_(0, 8, data); // zeros
this->add_(0, data); // turbo mode
this->add_(1, data); // light
this->add_(1, data); // tree icon thingy
this->add_(0, data); // blow mode
this->add_(0x52, 11, data); // idk
data->mark(EMMETI_BIT_MARK);
data->space(EMMETI_MESSAGE_SPACE);
this->reverse_add_(this->set_blades_(), 4, data);
this->add_(0, 4, data); // zeros
this->reverse_add_(2, 2, data); // thermometer
this->add_(0, 18, data); // zeros
this->reverse_add_(this->gen_checksum_(), 4, data);
} else {
this->add_(9, 12, data);
this->add_(0, 8, data);
this->add_(0x2052, 15, data);
data->mark(EMMETI_BIT_MARK);
data->space(EMMETI_MESSAGE_SPACE);
this->add_(0, 8, data);
this->add_(1, 2, data);
this->add_(0, 18, data);
this->add_(0x0C, 4, data);
}
data->mark(EMMETI_BIT_MARK);
data->space(0);
transmit.perform();
}
bool EmmetiClimate::parse_state_frame_(EmmetiState curr_state) {
this->mode = this->get_mode_(curr_state.mode);
this->fan_mode = this->get_fan_speed_(curr_state.fan_speed);
this->target_temperature = this->get_temp_(curr_state.temp);
this->swing_mode = this->get_swing_(curr_state.bitmap);
// this->blades_ = curr_state.fan_pos;
if (!(curr_state.bitmap & 0x01)) {
this->mode = climate::CLIMATE_MODE_OFF;
}
this->publish_state();
return true;
}
bool EmmetiClimate::on_receive(remote_base::RemoteReceiveData data) {
if (!data.expect_item(EMMETI_HEADER_MARK, EMMETI_HEADER_SPACE)) {
return false;
}
ESP_LOGD(TAG, "Received emmeti frame");
EmmetiState curr_state;
for (size_t pos = 0; pos < 3; pos++) {
if (data.expect_item(EMMETI_BIT_MARK, EMMETI_ONE_SPACE)) {
curr_state.mode |= 1 << pos;
} else if (!data.expect_item(EMMETI_BIT_MARK, EMMETI_ZERO_SPACE)) {
return false;
}
}
ESP_LOGD(TAG, "Mode: %d", curr_state.mode);
if (data.expect_item(EMMETI_BIT_MARK, EMMETI_ONE_SPACE)) {
curr_state.bitmap |= 1 << 0;
} else if (!data.expect_item(EMMETI_BIT_MARK, EMMETI_ZERO_SPACE)) {
return false;
}
ESP_LOGD(TAG, "On: %d", curr_state.bitmap & 0x01);
for (size_t pos = 0; pos < 2; pos++) {
if (data.expect_item(EMMETI_BIT_MARK, EMMETI_ONE_SPACE)) {
curr_state.fan_speed |= 1 << pos;
} else if (!data.expect_item(EMMETI_BIT_MARK, EMMETI_ZERO_SPACE)) {
return false;
}
}
ESP_LOGD(TAG, "Fan speed: %d", curr_state.fan_speed);
for (size_t pos = 0; pos < 2; pos++) {
if (data.expect_item(EMMETI_BIT_MARK, EMMETI_ONE_SPACE)) {
curr_state.bitmap |= 1 << (pos + 1);
} else if (!data.expect_item(EMMETI_BIT_MARK, EMMETI_ZERO_SPACE)) {
return false;
}
}
ESP_LOGD(TAG, "Swing: %d", (curr_state.bitmap >> 1) & 0x01);
ESP_LOGD(TAG, "Sleep: %d", (curr_state.bitmap >> 2) & 0x01);
for (size_t pos = 0; pos < 4; pos++) {
if (data.expect_item(EMMETI_BIT_MARK, EMMETI_ONE_SPACE)) {
curr_state.temp |= 1 << pos;
} else if (!data.expect_item(EMMETI_BIT_MARK, EMMETI_ZERO_SPACE)) {
return false;
}
}
ESP_LOGD(TAG, "Temp: %d", curr_state.temp);
for (size_t pos = 0; pos < 8; pos++) {
if (!data.expect_item(EMMETI_BIT_MARK, EMMETI_ZERO_SPACE)) {
return false;
}
}
for (size_t pos = 0; pos < 4; pos++) {
if (data.expect_item(EMMETI_BIT_MARK, EMMETI_ONE_SPACE)) {
curr_state.bitmap |= 1 << (pos + 3);
} else if (!data.expect_item(EMMETI_BIT_MARK, EMMETI_ZERO_SPACE)) {
return false;
}
}
ESP_LOGD(TAG, "Turbo: %d", (curr_state.bitmap >> 3) & 0x01);
ESP_LOGD(TAG, "Light: %d", (curr_state.bitmap >> 4) & 0x01);
ESP_LOGD(TAG, "Tree: %d", (curr_state.bitmap >> 5) & 0x01);
ESP_LOGD(TAG, "Blow: %d", (curr_state.bitmap >> 6) & 0x01);
uint16_t control_data = 0;
for (size_t pos = 0; pos < 11; pos++) {
if (data.expect_item(EMMETI_BIT_MARK, EMMETI_ONE_SPACE)) {
control_data |= 1 << pos;
} else if (!data.expect_item(EMMETI_BIT_MARK, EMMETI_ZERO_SPACE)) {
return false;
}
}
if (control_data != 0x250) {
return false;
}
return this->parse_state_frame_(curr_state);
}
} // namespace emmeti
} // namespace esphome

View file

@ -0,0 +1,109 @@
#pragma once
#include "esphome/components/climate_ir/climate_ir.h"
namespace esphome {
namespace emmeti {
const uint8_t EMMETI_TEMP_MIN = 16; // Celsius
const uint8_t EMMETI_TEMP_MAX = 30; // Celsius
// Modes
enum EmmetiMode : uint8_t {
EMMETI_MODE_HEAT_COOL = 0x00,
EMMETI_MODE_COOL = 0x01,
EMMETI_MODE_DRY = 0x02,
EMMETI_MODE_FAN = 0x03,
EMMETI_MODE_HEAT = 0x04,
};
// Fan Speed
enum EmmetiFanMode : uint8_t {
EMMETI_FAN_AUTO = 0x00,
EMMETI_FAN_1 = 0x01,
EMMETI_FAN_2 = 0x02,
EMMETI_FAN_3 = 0x03,
};
// Fan Position
enum EmmetiBlades : uint8_t {
EMMETI_BLADES_STOP = 0x00,
EMMETI_BLADES_FULL = 0x01,
EMMETI_BLADES_1 = 0x02,
EMMETI_BLADES_2 = 0x03,
EMMETI_BLADES_3 = 0x04,
EMMETI_BLADES_4 = 0x05,
EMMETI_BLADES_5 = 0x06,
EMMETI_BLADES_LOW = 0x07,
EMMETI_BLADES_MID = 0x09,
EMMETI_BLADES_HIGH = 0x11,
};
// IR Transmission
const uint32_t EMMETI_IR_FREQUENCY = 38000;
const uint32_t EMMETI_HEADER_MARK = 9076;
const uint32_t EMMETI_HEADER_SPACE = 4408;
const uint32_t EMMETI_BIT_MARK = 660;
const uint32_t EMMETI_ONE_SPACE = 1630;
const uint32_t EMMETI_ZERO_SPACE = 530;
const uint32_t EMMETI_MESSAGE_SPACE = 20000;
struct EmmetiState {
uint8_t mode = 0;
uint8_t bitmap = 0;
uint8_t fan_speed = 0;
uint8_t temp = 0;
uint8_t fan_pos = 0;
uint8_t th = 0;
uint8_t checksum = 0;
};
class EmmetiClimate : public climate_ir::ClimateIR {
public:
EmmetiClimate()
: climate_ir::ClimateIR(EMMETI_TEMP_MIN, EMMETI_TEMP_MAX, 1.0f, true, true,
{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM,
climate::CLIMATE_FAN_HIGH},
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}) {}
protected:
// Transmit via IR the state of this climate controller
void transmit_state() override;
// Handle received IR Buffer
bool on_receive(remote_base::RemoteReceiveData data) override;
bool parse_state_frame_(EmmetiState curr_state);
// setters
uint8_t set_mode_();
uint8_t set_temp_();
uint8_t set_fan_speed_();
uint8_t gen_checksum_();
uint8_t set_blades_();
// getters
climate::ClimateMode get_mode_(uint8_t mode);
climate::ClimateFanMode get_fan_speed_(uint8_t fan);
void get_blades_(uint8_t fanpos);
// get swing
climate::ClimateSwingMode get_swing_(uint8_t bitmap);
float get_temp_(uint8_t temp);
// check if the received frame is valid
bool check_checksum_(uint8_t checksum);
template<typename T> T reverse_(T val, size_t len);
template<typename T> void add_(T val, size_t len, esphome::remote_base::RemoteTransmitData *ata);
template<typename T> void add_(T val, esphome::remote_base::RemoteTransmitData *data);
template<typename T> void reverse_add_(T val, size_t len, esphome::remote_base::RemoteTransmitData *data);
uint8_t blades_ = EMMETI_BLADES_STOP;
};
} // namespace emmeti
} // namespace esphome

View file

@ -83,20 +83,22 @@ def _format_framework_arduino_version(ver: cv.Version) -> str:
# The default/recommended arduino framework version
# - https://github.com/esp8266/Arduino/releases
# - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-arduinoespressif8266
RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 0, 2)
RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 1, 2)
# The platformio/espressif8266 version to use for arduino 2 framework versions
# - https://github.com/platformio/platform-espressif8266/releases
# - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif8266
ARDUINO_2_PLATFORM_VERSION = cv.Version(2, 6, 3)
# for arduino 3 framework versions
ARDUINO_3_PLATFORM_VERSION = cv.Version(3, 2, 0)
# for arduino 4 framework versions
ARDUINO_4_PLATFORM_VERSION = cv.Version(4, 2, 1)
def _arduino_check_versions(value):
value = value.copy()
lookups = {
"dev": (cv.Version(3, 0, 2), "https://github.com/esp8266/Arduino.git"),
"latest": (cv.Version(3, 0, 2), None),
"dev": (cv.Version(3, 1, 2), "https://github.com/esp8266/Arduino.git"),
"latest": (cv.Version(3, 1, 2), None),
"recommended": (RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, None),
}
@ -116,7 +118,9 @@ def _arduino_check_versions(value):
platform_version = value.get(CONF_PLATFORM_VERSION)
if platform_version is None:
if version >= cv.Version(3, 0, 0):
if version >= cv.Version(3, 1, 0):
platform_version = _parse_platform_version(str(ARDUINO_4_PLATFORM_VERSION))
elif version >= cv.Version(3, 0, 0):
platform_version = _parse_platform_version(str(ARDUINO_3_PLATFORM_VERSION))
elif version >= cv.Version(2, 5, 0):
platform_version = _parse_platform_version(str(ARDUINO_2_PLATFORM_VERSION))

View file

@ -1,7 +1,13 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c
from esphome.const import CONF_ADDRESS, CONF_COMMAND, CONF_ID, CONF_DURATION
from esphome.const import (
CONF_ADDRESS,
CONF_COMMAND,
CONF_ID,
CONF_DURATION,
CONF_VOLUME,
)
from esphome import automation
from esphome.automation import maybe_simple_id
@ -9,7 +15,6 @@ CODEOWNERS = ["@carlos-sarmiento"]
DEPENDENCIES = ["i2c"]
MULTI_CONF = True
CONF_VOLUME = "volume"
CONF_VOLUME_PER_MINUTE = "volume_per_minute"
ezo_pmp_ns = cg.esphome_ns.namespace("ezo_pmp")

View file

@ -1,13 +1,15 @@
import hashlib
import logging
import functools
from pathlib import Path
import hashlib
import os
import re
from packaging import version
import requests
from esphome import core
from esphome import external_files
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.helpers import (
@ -15,21 +17,26 @@ from esphome.helpers import (
cpp_string_escape,
)
from esphome.const import (
__version__,
CONF_FAMILY,
CONF_FILE,
CONF_GLYPHS,
CONF_ID,
CONF_RAW_DATA_ID,
CONF_TYPE,
CONF_REFRESH,
CONF_SIZE,
CONF_PATH,
CONF_WEIGHT,
CONF_URL,
)
from esphome.core import (
CORE,
HexInt,
)
_LOGGER = logging.getLogger(__name__)
DOMAIN = "font"
DEPENDENCIES = ["display"]
MULTI_CONF = True
@ -125,20 +132,10 @@ def validate_truetype_file(value):
return cv.file_(value)
def _compute_local_font_dir(name) -> Path:
h = hashlib.new("sha256")
h.update(name.encode())
return Path(CORE.data_dir) / DOMAIN / h.hexdigest()[:8]
def _compute_gfonts_local_path(value) -> Path:
name = f"{value[CONF_FAMILY]}@{value[CONF_WEIGHT]}@{value[CONF_ITALIC]}@v1"
return _compute_local_font_dir(name) / "font.ttf"
TYPE_LOCAL = "local"
TYPE_LOCAL_BITMAP = "local_bitmap"
TYPE_GFONTS = "gfonts"
TYPE_WEB = "web"
LOCAL_SCHEMA = cv.Schema(
{
cv.Required(CONF_PATH): validate_truetype_file,
@ -169,21 +166,64 @@ def validate_weight_name(value):
return FONT_WEIGHTS[cv.one_of(*FONT_WEIGHTS, lower=True, space="-")(value)]
def download_gfonts(value):
def _compute_local_font_path(value: dict) -> Path:
url = value[CONF_URL]
h = hashlib.new("sha256")
h.update(url.encode())
key = h.hexdigest()[:8]
base_dir = external_files.compute_local_file_dir(DOMAIN)
_LOGGER.debug("_compute_local_font_path: base_dir=%s", base_dir / key)
return base_dir / key
def get_font_path(value, type) -> Path:
if type == TYPE_GFONTS:
name = f"{value[CONF_FAMILY]}@{value[CONF_WEIGHT]}@{value[CONF_ITALIC]}@v1"
return external_files.compute_local_file_dir(DOMAIN) / f"{name}.ttf"
if type == TYPE_WEB:
return _compute_local_font_path(value) / "font.ttf"
return None
def download_content(url: str, path: Path) -> None:
if not external_files.has_remote_file_changed(url, path):
_LOGGER.debug("Remote file has not changed %s", url)
return
_LOGGER.debug(
"Remote file has changed, downloading from %s to %s",
url,
path,
)
try:
req = requests.get(
url,
timeout=external_files.NETWORK_TIMEOUT,
headers={"User-agent": f"ESPHome/{__version__} (https://esphome.io)"},
)
req.raise_for_status()
except requests.exceptions.RequestException as e:
raise cv.Invalid(f"Could not download from {url}: {e}")
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(req.content)
def download_gfont(value):
name = (
f"{value[CONF_FAMILY]}:ital,wght@{int(value[CONF_ITALIC])},{value[CONF_WEIGHT]}"
)
url = f"https://fonts.googleapis.com/css2?family={name}"
path = get_font_path(value, TYPE_GFONTS)
_LOGGER.debug("download_gfont: path=%s", path)
path = _compute_gfonts_local_path(value)
if path.is_file():
return value
try:
req = requests.get(url, timeout=30)
req = requests.get(url, timeout=external_files.NETWORK_TIMEOUT)
req.raise_for_status()
except requests.exceptions.RequestException as e:
raise cv.Invalid(
f"Could not download font for {name}, please check the fonts exists "
f"Could not download font at {url}, please check the fonts exists "
f"at google fonts ({e})"
)
match = re.search(r"src:\s+url\((.+)\)\s+format\('truetype'\);", req.text)
@ -194,26 +234,48 @@ def download_gfonts(value):
)
ttf_url = match.group(1)
try:
req = requests.get(ttf_url, timeout=30)
req.raise_for_status()
except requests.exceptions.RequestException as e:
raise cv.Invalid(f"Could not download ttf file for {name} ({ttf_url}): {e}")
_LOGGER.debug("download_gfont: ttf_url=%s", ttf_url)
path.parent.mkdir(exist_ok=True, parents=True)
path.write_bytes(req.content)
download_content(ttf_url, path)
return value
GFONTS_SCHEMA = cv.All(
def download_web_font(value):
url = value[CONF_URL]
path = get_font_path(value, TYPE_WEB)
download_content(url, path)
_LOGGER.debug("download_web_font: path=%s", path)
return value
EXTERNAL_FONT_SCHEMA = cv.Schema(
{
cv.Required(CONF_FAMILY): cv.string_strict,
cv.Optional(CONF_WEIGHT, default="regular"): cv.Any(
cv.int_, validate_weight_name
),
cv.Optional(CONF_ITALIC, default=False): cv.boolean,
},
download_gfonts,
cv.Optional(CONF_REFRESH, default="1d"): cv.All(cv.string, cv.source_refresh),
}
)
GFONTS_SCHEMA = cv.All(
EXTERNAL_FONT_SCHEMA.extend(
{
cv.Required(CONF_FAMILY): cv.string_strict,
}
),
download_gfont,
)
WEB_FONT_SCHEMA = cv.All(
EXTERNAL_FONT_SCHEMA.extend(
{
cv.Required(CONF_URL): cv.string_strict,
}
),
download_web_font,
)
@ -233,6 +295,14 @@ def validate_file_shorthand(value):
data[CONF_WEIGHT] = weight[1:]
return FILE_SCHEMA(data)
if value.startswith("http://") or value.startswith("https://"):
return FILE_SCHEMA(
{
CONF_TYPE: TYPE_WEB,
CONF_URL: value,
}
)
if value.endswith(".pcf") or value.endswith(".bdf"):
return FILE_SCHEMA(
{
@ -254,6 +324,7 @@ TYPED_FILE_SCHEMA = cv.typed_schema(
TYPE_LOCAL: LOCAL_SCHEMA,
TYPE_GFONTS: GFONTS_SCHEMA,
TYPE_LOCAL_BITMAP: LOCAL_BITMAP_SCHEMA,
TYPE_WEB: WEB_FONT_SCHEMA,
}
)
@ -264,7 +335,7 @@ def _file_schema(value):
return TYPED_FILE_SCHEMA(value)
FILE_SCHEMA = cv.Schema(_file_schema)
FILE_SCHEMA = cv.All(_file_schema)
DEFAULT_GLYPHS = (
' !"%()+=,-.:/?0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°'
@ -288,7 +359,7 @@ FONT_SCHEMA = cv.Schema(
),
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
cv.GenerateID(CONF_RAW_GLYPH_ID): cv.declare_id(GlyphData),
}
},
)
CONFIG_SCHEMA = cv.All(validate_pillow_installed, FONT_SCHEMA, merge_glyphs)
@ -343,8 +414,8 @@ class EFont:
elif ftype == TYPE_LOCAL:
path = CORE.relative_config_path(file[CONF_PATH])
font = load_ttf_font(path, size)
elif ftype == TYPE_GFONTS:
path = _compute_gfonts_local_path(file)
elif ftype in (TYPE_GFONTS, TYPE_WEB):
path = get_font_path(file, ftype)
font = load_ttf_font(path, size)
else:
raise cv.Invalid(f"Could not load font: unknown type: {ftype}")
@ -361,9 +432,9 @@ def convert_bitmap_to_pillow_font(filepath):
BdfFontFile,
)
local_bitmap_font_file = _compute_local_font_dir(filepath) / os.path.basename(
filepath
)
local_bitmap_font_file = external_files.compute_local_file_dir(
DOMAIN,
) / os.path.basename(filepath)
copy_file_if_changed(filepath, local_bitmap_font_file)

View file

@ -71,7 +71,7 @@ class FT5x06Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice
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();
this->y_raw_max_ = this->display_->get_native_height();
}
}
esph_log_config(TAG, "FT5x06 Touchscreen setup complete");

View file

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

View file

@ -0,0 +1,269 @@
/*
* This file contains source code derived from Adafruit_HTU31D which is under
* the BSD license:
* Written by Limor Fried/Ladyada for Adafruit Industries.
* BSD license, all text above must be included in any redistribution.
*
* Modifications made by Mark Spicer.
*/
#include "htu31d.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
namespace htu31d {
/** Logging prefix */
static const char *const TAG = "htu31d";
/** Default I2C address for the HTU31D. */
static const uint8_t HTU31D_DEFAULT_I2CADDR = 0x40;
/** Read temperature and humidity. */
static const uint8_t HTU31D_READTEMPHUM = 0x00;
/** Start a conversion! */
static const uint8_t HTU31D_CONVERSION = 0x40;
/** Read serial number command. */
static const uint8_t HTU31D_READSERIAL = 0x0A;
/** Enable heater */
static const uint8_t HTU31D_HEATERON = 0x04;
/** Disable heater */
static const uint8_t HTU31D_HEATEROFF = 0x02;
/** Reset command. */
static const uint8_t HTU31D_RESET = 0x1E;
/** Diagnostics command. */
static const uint8_t HTU31D_DIAGNOSTICS = 0x08;
/**
* Computes a CRC result for the provided input.
*
* @returns the computed CRC result for the provided input
*/
uint8_t compute_crc(uint32_t value) {
uint32_t polynom = 0x98800000; // x^8 + x^5 + x^4 + 1
uint32_t msb = 0x80000000;
uint32_t mask = 0xFF800000;
uint32_t threshold = 0x00000080;
uint32_t result = value;
while (msb != threshold) {
// 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;
}
/**
* Resets the sensor and ensures that the devices serial number can be read over
* I2C.
*/
void HTU31DComponent::setup() {
ESP_LOGCONFIG(TAG, "Setting up esphome/components/htu31d HTU31D...");
if (!this->reset_()) {
this->mark_failed();
return;
}
if (this->read_serial_num_() == 0) {
this->mark_failed();
return;
}
}
/**
* Called once every update interval (user configured, defaults to 60s) and sets
* the current temperature and humidity.
*/
void HTU31DComponent::update() {
ESP_LOGD(TAG, "Checking temperature and humidty values");
// Trigger a conversion. From the spec sheet: The conversion command triggers
// a single temperature and humidity conversion.
if (this->write_register(HTU31D_CONVERSION, nullptr, 0) != i2c::ERROR_OK) {
this->status_set_warning();
ESP_LOGE(TAG, "Received errror writing conversion register");
return;
}
// Wait conversion time.
this->set_timeout(20, [this]() {
uint8_t thdata[6];
if (this->read_register(HTU31D_READTEMPHUM, thdata, 6) != i2c::ERROR_OK) {
this->status_set_warning();
ESP_LOGE(TAG, "Error reading temperature/humidty register");
return;
}
// Calculate temperature value.
uint16_t raw_temp = encode_uint16(thdata[0], thdata[1]);
uint8_t crc = compute_crc((uint32_t) raw_temp << 8);
if (crc != thdata[2]) {
this->status_set_warning();
ESP_LOGE(TAG, "Error validating temperature CRC");
return;
}
float temperature = raw_temp;
temperature /= 65535.0f;
temperature *= 165;
temperature -= 40;
if (this->temperature_ != nullptr) {
this->temperature_->publish_state(temperature);
}
// Calculate humidty value.
uint16_t raw_hum = encode_uint16(thdata[3], thdata[4]);
crc = compute_crc((uint32_t) raw_hum << 8);
if (crc != thdata[5]) {
this->status_set_warning();
ESP_LOGE(TAG, "Error validating humidty CRC");
return;
}
float humidity = raw_hum;
humidity /= 65535.0f;
humidity *= 100;
if (this->humidity_ != nullptr) {
this->humidity_->publish_state(humidity);
}
ESP_LOGD(TAG, "Got Temperature=%.1f°C Humidity=%.1f%%", temperature, humidity);
this->status_clear_warning();
});
}
/**
* Logs the current compoenent config.
*/
void HTU31DComponent::dump_config() {
ESP_LOGCONFIG(TAG, "HTU31D:");
LOG_I2C_DEVICE(this);
if (this->is_failed()) {
ESP_LOGE(TAG, "Communication with HTU31D failed!");
}
LOG_UPDATE_INTERVAL(this);
LOG_SENSOR(" ", "Temperature", this->temperature_);
LOG_SENSOR(" ", "Humidity", this->humidity_);
}
/**
* Sends a 'reset' request to the HTU31D, followed by a 15ms delay.
*
* @returns True if was able to write the command successfully
*/
bool HTU31DComponent::reset_() {
if (this->write_register(HTU31D_RESET, nullptr, 0) != i2c::ERROR_OK) {
return false;
}
delay(15);
return true;
}
/**
* Reads the serial number from the device and checks the CRC.
*
* @returns the 24bit serial number from the device
*/
uint32_t HTU31DComponent::read_serial_num_() {
uint8_t reply[4];
uint32_t serial = 0;
uint8_t padding = 0;
// Verify we can read the device serial.
if (this->read_register(HTU31D_READSERIAL, reply, 4) != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Error reading device serial");
return 0;
}
serial = encode_uint32(reply[0], reply[1], reply[2], padding);
uint8_t crc = compute_crc(serial);
if (crc != reply[3]) {
ESP_LOGE(TAG, "Error validating serial CRC");
return 0;
}
ESP_LOGD(TAG, "Found serial: 0x%X", serial);
return serial;
}
/**
* Checks the diagnostics register to determine if the heater is currently
* enabled.
*
* @returns True if the heater is currently enabled, False otherwise
*/
bool HTU31DComponent::is_heater_enabled() {
uint8_t reply[1];
uint8_t heater_enabled_position = 0;
uint8_t mask = 1 << heater_enabled_position;
uint8_t diagnostics = 0;
if (this->read_register(HTU31D_DIAGNOSTICS, reply, 1) != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Error reading device serial");
return false;
}
diagnostics = reply[0];
return (diagnostics & mask) != 0;
}
/**
* Sets the heater state on or off.
*
* @param desired True for on, and False for off.
*/
void HTU31DComponent::set_heater_state(bool desired) {
bool current = this->is_heater_enabled();
// If the current state matches the desired state, there is nothing to do.
if (current == desired) {
return;
}
// Update heater state.
esphome::i2c::ErrorCode err;
if (desired) {
err = this->write_register(HTU31D_HEATERON, nullptr, 0);
} else {
err = this->write_register(HTU31D_HEATEROFF, nullptr, 0);
}
// Record any error.
if (err != i2c::ERROR_OK) {
this->status_set_warning();
ESP_LOGE(TAG, "Received error updating heater state");
return;
}
}
/**
* Sets the startup priority for this component.
*
* @returns The startup priority
*/
float HTU31DComponent::get_setup_priority() const { return setup_priority::DATA; }
} // namespace htu31d
} // namespace esphome

View file

@ -0,0 +1,33 @@
#pragma once
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
namespace esphome {
namespace htu31d {
class HTU31DComponent : public PollingComponent, public i2c::I2CDevice {
public:
void setup() override; /// Setup (reset) the sensor and check connection.
void update() override; /// Update the sensor values (temperature+humidity).
void dump_config() override; /// Dumps the configuration values.
void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; }
void set_humidity(sensor::Sensor *humidity) { this->humidity_ = humidity; }
void set_heater_state(bool desired);
bool is_heater_enabled();
float get_setup_priority() const override;
protected:
bool reset_();
uint32_t read_serial_num_();
sensor::Sensor *temperature_{nullptr};
sensor::Sensor *humidity_{nullptr};
};
} // namespace htu31d
} // namespace esphome

View file

@ -0,0 +1,56 @@
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_TEMPERATURE,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
UNIT_PERCENT,
)
DEPENDENCIES = ["i2c"]
htu31d_ns = cg.esphome_ns.namespace("htu31d")
HTU31DComponent = htu31d_ns.class_(
"HTU31DComponent", cg.PollingComponent, i2c.I2CDevice
)
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(HTU31DComponent),
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
accuracy_decimals=1,
device_class=DEVICE_CLASS_HUMIDITY,
state_class=STATE_CLASS_MEASUREMENT,
),
}
)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x40))
)
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(sens))
if humidity_config := config.get(CONF_HUMIDITY):
sens = await sensor.new_sensor(humidity_config)
cg.add(var.set_humidity(sens))

View file

@ -55,10 +55,10 @@ void INA226Component::setup() {
config.avg_samples = this->adc_avg_samples_;
// Bus Voltage Conversion Time VBUSCT Bit Settings [8:6] (100 -> 1.1ms, 111 -> 8.244 ms)
config.bus_voltage_conversion_time = this->adc_time_;
config.bus_voltage_conversion_time = this->adc_time_voltage_;
// Shunt Voltage Conversion Time VSHCT Bit Settings [5:3] (100 -> 1.1ms, 111 -> 8.244 ms)
config.shunt_voltage_conversion_time = this->adc_time_;
config.shunt_voltage_conversion_time = this->adc_time_current_;
// Mode Settings [2:0] Combinations (111 -> Shunt and Bus, Continuous)
config.mode = 0b111;
@ -93,7 +93,8 @@ 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 Conversion Time Bus Voltage: %d", INA226_ADC_TIMES[this->adc_time_voltage_ & 0b111]);
ESP_LOGCONFIG(TAG, " ADC Conversion Time Shunt Voltage: %d", INA226_ADC_TIMES[this->adc_time_current_ & 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_);

View file

@ -50,7 +50,8 @@ 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_time_voltage(AdcTime time) { adc_time_voltage_ = time; }
void set_adc_time_current(AdcTime time) { adc_time_current_ = 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; }
@ -61,7 +62,8 @@ class INA226Component : public PollingComponent, public i2c::I2CDevice {
protected:
float shunt_resistance_ohm_;
float max_current_a_;
AdcTime adc_time_{AdcTime::ADC_TIME_1100US};
AdcTime adc_time_voltage_{AdcTime::ADC_TIME_1100US};
AdcTime adc_time_current_{AdcTime::ADC_TIME_1100US};
AdcAvgSamples adc_avg_samples_{AdcAvgSamples::ADC_AVG_SAMPLES_4};
uint32_t calibration_lsb_;
sensor::Sensor *bus_voltage_sensor_{nullptr};

View file

@ -16,6 +16,7 @@ from esphome.const import (
UNIT_VOLT,
UNIT_AMPERE,
UNIT_WATT,
CONF_VOLTAGE,
)
DEPENDENCIES = ["i2c"]
@ -92,7 +93,15 @@ 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_TIME, default="1100 us"): cv.Any(
validate_adc_time,
cv.Schema(
{
cv.Required(CONF_VOLTAGE): validate_adc_time,
cv.Required(CONF_CURRENT): validate_adc_time,
}
),
),
cv.Optional(CONF_ADC_AVERAGING, default=4): cv.enum(
ADC_AVG_SAMPLES, int=True
),
@ -110,7 +119,15 @@ async def to_code(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]))
adc_time_config = config[CONF_ADC_TIME]
if isinstance(adc_time_config, dict):
cg.add(var.set_adc_time_voltage(adc_time_config[CONF_VOLTAGE]))
cg.add(var.set_adc_time_current(adc_time_config[CONF_CURRENT]))
else:
cg.add(var.set_adc_time_voltage(adc_time_config))
cg.add(var.set_adc_time_current(adc_time_config))
cg.add(var.set_adc_avg_samples(config[CONF_ADC_AVERAGING]))
if CONF_BUS_VOLTAGE in config:

View file

@ -0,0 +1,301 @@
#include "kamstrup_kmp.h"
#include "esphome/core/log.h"
namespace esphome {
namespace kamstrup_kmp {
static const char *const TAG = "kamstrup_kmp";
void KamstrupKMPComponent::dump_config() {
ESP_LOGCONFIG(TAG, "kamstrup_kmp:");
if (this->is_failed()) {
ESP_LOGE(TAG, "Communication with Kamstrup meter failed!");
}
LOG_UPDATE_INTERVAL(this);
LOG_SENSOR(" ", "Heat Energy", this->heat_energy_sensor_);
LOG_SENSOR(" ", "Power", this->power_sensor_);
LOG_SENSOR(" ", "Temperature 1", this->temp1_sensor_);
LOG_SENSOR(" ", "Temperature 2", this->temp2_sensor_);
LOG_SENSOR(" ", "Temperature Difference", this->temp_diff_sensor_);
LOG_SENSOR(" ", "Flow", this->flow_sensor_);
LOG_SENSOR(" ", "Volume", this->volume_sensor_);
for (int i = 0; i < this->custom_sensors_.size(); i++) {
LOG_SENSOR(" ", "Custom Sensor", this->custom_sensors_[i]);
ESP_LOGCONFIG(TAG, " Command: 0x%04X", this->custom_commands_[i]);
}
this->check_uart_settings(1200, 2, uart::UART_CONFIG_PARITY_NONE, 8);
}
float KamstrupKMPComponent::get_setup_priority() const { return setup_priority::DATA; }
void KamstrupKMPComponent::update() {
if (this->heat_energy_sensor_ != nullptr) {
this->command_queue_.push(CMD_HEAT_ENERGY);
}
if (this->power_sensor_ != nullptr) {
this->command_queue_.push(CMD_POWER);
}
if (this->temp1_sensor_ != nullptr) {
this->command_queue_.push(CMD_TEMP1);
}
if (this->temp2_sensor_ != nullptr) {
this->command_queue_.push(CMD_TEMP2);
}
if (this->temp_diff_sensor_ != nullptr) {
this->command_queue_.push(CMD_TEMP_DIFF);
}
if (this->flow_sensor_ != nullptr) {
this->command_queue_.push(CMD_FLOW);
}
if (this->volume_sensor_ != nullptr) {
this->command_queue_.push(CMD_VOLUME);
}
for (uint16_t custom_command : this->custom_commands_) {
this->command_queue_.push(custom_command);
}
}
void KamstrupKMPComponent::loop() {
if (!this->command_queue_.empty()) {
uint16_t command = this->command_queue_.front();
this->send_command_(command);
this->command_queue_.pop();
}
}
void KamstrupKMPComponent::send_command_(uint16_t command) {
uint32_t msg_len = 5;
uint8_t msg[msg_len];
msg[0] = 0x3F;
msg[1] = 0x10;
msg[2] = 0x01;
msg[3] = command >> 8;
msg[4] = command & 0xFF;
this->clear_uart_rx_buffer_();
this->send_message_(msg, msg_len);
this->read_command_(command);
}
void KamstrupKMPComponent::send_message_(const uint8_t *msg, int msg_len) {
int buffer_len = msg_len + 2;
uint8_t buffer[buffer_len];
// Prepare the basic message and appand CRC
for (int i = 0; i < msg_len; i++) {
buffer[i] = msg[i];
}
buffer[buffer_len - 2] = 0;
buffer[buffer_len - 1] = 0;
uint16_t crc = crc16_ccitt(buffer, buffer_len);
buffer[buffer_len - 2] = crc >> 8;
buffer[buffer_len - 1] = crc & 0xFF;
// Prepare actual TX message
uint8_t tx_msg[20];
int tx_msg_len = 1;
tx_msg[0] = 0x80; // prefix
for (int i = 0; i < buffer_len; i++) {
if (buffer[i] == 0x06 || buffer[i] == 0x0d || buffer[i] == 0x1b || buffer[i] == 0x40 || buffer[i] == 0x80) {
tx_msg[tx_msg_len++] = 0x1b;
tx_msg[tx_msg_len++] = buffer[i] ^ 0xff;
} else {
tx_msg[tx_msg_len++] = buffer[i];
}
}
tx_msg[tx_msg_len++] = 0x0D; // EOM
this->write_array(tx_msg, tx_msg_len);
}
void KamstrupKMPComponent::clear_uart_rx_buffer_() {
uint8_t tmp;
while (this->available()) {
this->read_byte(&tmp);
}
}
void KamstrupKMPComponent::read_command_(uint16_t command) {
uint8_t buffer[20] = {0};
int buffer_len = 0;
int data;
int timeout = 250; // ms
// Read the data from the UART
while (timeout > 0) {
if (this->available()) {
data = this->read();
if (data > -1) {
if (data == 0x40) { // start of message
buffer_len = 0;
}
buffer[buffer_len++] = (uint8_t) data;
if (data == 0x0D) {
break;
}
} else {
ESP_LOGE(TAG, "Error while reading from UART");
}
} else {
delay(1);
timeout--;
}
}
if (timeout == 0 || buffer_len == 0) {
ESP_LOGE(TAG, "Request timed out");
return;
}
// Validate message (prefix and suffix)
if (buffer[0] != 0x40) {
ESP_LOGE(TAG, "Received invalid message (prefix mismatch received 0x%02X, expected 0x40)", buffer[0]);
return;
}
if (buffer[buffer_len - 1] != 0x0D) {
ESP_LOGE(TAG, "Received invalid message (EOM mismatch received 0x%02X, expected 0x0D)", buffer[buffer_len - 1]);
return;
}
// Decode
uint8_t msg[20] = {0};
int msg_len = 0;
for (int i = 1; i < buffer_len - 1; i++) {
if (buffer[i] == 0x1B) {
msg[msg_len++] = buffer[i + 1] ^ 0xFF;
i++;
} else {
msg[msg_len++] = buffer[i];
}
}
// Validate CRC
if (crc16_ccitt(msg, msg_len)) {
ESP_LOGE(TAG, "Received invalid message (CRC mismatch)");
return;
}
// All seems good. Now parse the message
this->parse_command_message_(command, msg, msg_len);
}
void KamstrupKMPComponent::parse_command_message_(uint16_t command, const uint8_t *msg, int msg_len) {
// Validate the message
if (msg_len < 8) {
ESP_LOGE(TAG, "Received invalid message (message too small)");
return;
}
if (msg[0] != 0x3F || msg[1] != 0x10) {
ESP_LOGE(TAG, "Received invalid message (invalid header received 0x%02X%02X, expected 0x3F10)", msg[0], msg[1]);
return;
}
uint16_t recv_command = msg[2] << 8 | msg[3];
if (recv_command != command) {
ESP_LOGE(TAG, "Received invalid message (invalid unexpected command received 0x%04X, expected 0x%04X)",
recv_command, command);
return;
}
uint8_t unit_idx = msg[4];
uint8_t mantissa_range = msg[5];
if (mantissa_range > 4) {
ESP_LOGE(TAG, "Received invalid message (mantissa size too large %d, expected 4)", mantissa_range);
return;
}
// Calculate exponent
float exponent = msg[6] & 0x3F;
if (msg[6] & 0x40) {
exponent = -exponent;
}
exponent = powf(10, exponent);
if (msg[6] & 0x80) {
exponent = -exponent;
}
// Calculate mantissa
uint32_t mantissa = 0;
for (int i = 0; i < mantissa_range; i++) {
mantissa <<= 8;
mantissa |= msg[i + 7];
}
// Calculate the actual value
float value = mantissa * exponent;
// Set sensor value
this->set_sensor_value_(command, value, unit_idx);
}
void KamstrupKMPComponent::set_sensor_value_(uint16_t command, float value, uint8_t unit_idx) {
const char *unit = UNITS[unit_idx];
// Standard sensors
if (command == CMD_HEAT_ENERGY && this->heat_energy_sensor_ != nullptr) {
this->heat_energy_sensor_->publish_state(value);
} else if (command == CMD_POWER && this->power_sensor_ != nullptr) {
this->power_sensor_->publish_state(value);
} else if (command == CMD_TEMP1 && this->temp1_sensor_ != nullptr) {
this->temp1_sensor_->publish_state(value);
} else if (command == CMD_TEMP2 && this->temp2_sensor_ != nullptr) {
this->temp2_sensor_->publish_state(value);
} else if (command == CMD_TEMP_DIFF && this->temp_diff_sensor_ != nullptr) {
this->temp_diff_sensor_->publish_state(value);
} else if (command == CMD_FLOW && this->flow_sensor_ != nullptr) {
this->flow_sensor_->publish_state(value);
} else if (command == CMD_VOLUME && this->volume_sensor_ != nullptr) {
this->volume_sensor_->publish_state(value);
}
// Custom sensors
for (int i = 0; i < this->custom_commands_.size(); i++) {
if (command == this->custom_commands_[i]) {
this->custom_sensors_[i]->publish_state(value);
}
}
ESP_LOGD(TAG, "Received value for command 0x%04X: %.3f [%s]", command, value, unit);
}
uint16_t crc16_ccitt(const uint8_t *buffer, int len) {
uint32_t poly = 0x1021;
uint32_t reg = 0x00;
for (int i = 0; i < len; i++) {
int mask = 0x80;
while (mask > 0) {
reg <<= 1;
if (buffer[i] & mask) {
reg |= 1;
}
mask >>= 1;
if (reg & 0x10000) {
reg &= 0xffff;
reg ^= poly;
}
}
}
return (uint16_t) reg;
}
} // namespace kamstrup_kmp
} // namespace esphome

View file

@ -0,0 +1,131 @@
#pragma once
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/uart/uart.h"
#include "esphome/core/component.h"
namespace esphome {
namespace kamstrup_kmp {
/*
===========================================================================
=== KAMSTRUP KMP ===
===========================================================================
Kamstrup Meter Protocol (KMP) is a protocol used with Kamstrup district
heating meters, e.g. Kamstrup MULTICAL 403.
These devices register consumed heat from a district heating system.
It does this by measuring the incoming and outgoing water temperature
and by measuring the water flow. The temperature difference (delta T)
together with the water flow results in consumed energy, typically
in giga joule (GJ).
The Kamstrup Multical has an optical interface just above the display.
This interface is essentially an RS-232 interface using a proprietary
protocol (Kamstrup Meter Protocol [KMP]).
The integration uses this optical interface to periodically read the
configured values (sensors) from the meter. Supported sensors are:
- Heat Energy [GJ]
- Current Power Consumption [kW]
- Temperature 1 [°C]
- Temperature 2 [°C]
- Temperature Difference [°K]
- Water Flow [l/h]
- Volume [m3]
Apart from these supported 'fixed' sensors, the user can configure up to
five custom sensors. The KMP command (16 bit unsigned int) has to be
provided in that case.
Note:
The optical interface is enabled as soon as a button on the meter is pushed.
The interface stays active for a few minutes. To keep the interface 'alive'
magnets must be placed around the optical sensor.
Units:
Units are set using the regular Sensor config in the user yaml. However,
KMP does also send the correct unit with every value. When DEBUG logging
is enabled, the received value with the received unit are logged.
Acknowledgement:
This interface was inspired by:
- https://atomstar.tweakblogs.net/blog/19110/reading-out-kamstrup-multical-402-403-with-home-built-optical-head
- https://wiki.hal9k.dk/projects/kamstrup
*/
// KMP Commands
static const uint16_t CMD_HEAT_ENERGY = 0x003C;
static const uint16_t CMD_POWER = 0x0050;
static const uint16_t CMD_TEMP1 = 0x0056;
static const uint16_t CMD_TEMP2 = 0x0057;
static const uint16_t CMD_TEMP_DIFF = 0x0059;
static const uint16_t CMD_FLOW = 0x004A;
static const uint16_t CMD_VOLUME = 0x0044;
// KMP units
static const char *const UNITS[] = {
"", "Wh", "kWh", "MWh", "GWh", "J", "kJ", "MJ", "GJ", "Cal",
"kCal", "Mcal", "Gcal", "varh", "kvarh", "Mvarh", "Gvarh", "VAh", "kVAh", "MVAh",
"GVAh", "kW", "kW", "MW", "GW", "kvar", "kvar", "Mvar", "Gvar", "VA",
"kVA", "MVA", "GVA", "V", "A", "kV", "kA", "C", "K", "l",
"m3", "l/h", "m3/h", "m3xC", "ton", "ton/h", "h", "hh:mm:ss", "yy:mm:dd", "yyyy:mm:dd",
"mm:dd", "", "bar", "RTC", "ASCII", "m3 x 10", "ton x 10", "GJ x 10", "minutes", "Bitfield",
"s", "ms", "days", "RTC-Q", "Datetime"};
class KamstrupKMPComponent : public PollingComponent, public uart::UARTDevice {
public:
void set_heat_energy_sensor(sensor::Sensor *sensor) { this->heat_energy_sensor_ = sensor; }
void set_power_sensor(sensor::Sensor *sensor) { this->power_sensor_ = sensor; }
void set_temp1_sensor(sensor::Sensor *sensor) { this->temp1_sensor_ = sensor; }
void set_temp2_sensor(sensor::Sensor *sensor) { this->temp2_sensor_ = sensor; }
void set_temp_diff_sensor(sensor::Sensor *sensor) { this->temp_diff_sensor_ = sensor; }
void set_flow_sensor(sensor::Sensor *sensor) { this->flow_sensor_ = sensor; }
void set_volume_sensor(sensor::Sensor *sensor) { this->volume_sensor_ = sensor; }
void dump_config() override;
float get_setup_priority() const override;
void update() override;
void loop() override;
void add_custom_sensor(sensor::Sensor *sensor, uint16_t command) {
this->custom_sensors_.push_back(sensor);
this->custom_commands_.push_back(command);
}
protected:
// Sensors
sensor::Sensor *heat_energy_sensor_{nullptr};
sensor::Sensor *power_sensor_{nullptr};
sensor::Sensor *temp1_sensor_{nullptr};
sensor::Sensor *temp2_sensor_{nullptr};
sensor::Sensor *temp_diff_sensor_{nullptr};
sensor::Sensor *flow_sensor_{nullptr};
sensor::Sensor *volume_sensor_{nullptr};
// Custom sensors and commands
std::vector<sensor::Sensor *> custom_sensors_;
std::vector<uint16_t> custom_commands_;
// Command queue
std::queue<uint16_t> command_queue_;
// Methods
// Sends a command to the meter and receives its response
void send_command_(uint16_t command);
// Sends a message to the meter. A prefix/suffix and CRC are added
void send_message_(const uint8_t *msg, int msg_len);
// Clears and data that might be in the UART Rx buffer
void clear_uart_rx_buffer_();
// Reads and validates the response to a send command
void read_command_(uint16_t command);
// Parses a received message
void parse_command_message_(uint16_t command, const uint8_t *msg, int msg_len);
// Sets the received value to the correct sensor
void set_sensor_value_(uint16_t command, float value, uint8_t unit_idx);
};
// "true" CCITT CRC-16
uint16_t crc16_ccitt(const uint8_t *buffer, int len);
} // namespace kamstrup_kmp
} // namespace esphome

View file

@ -0,0 +1,132 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor, uart
from esphome.const import (
CONF_COMMAND,
CONF_CUSTOM,
CONF_FLOW,
CONF_ID,
CONF_POWER,
CONF_VOLUME,
DEVICE_CLASS_EMPTY,
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_POWER,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_VOLUME,
STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL_INCREASING,
UNIT_CELSIUS,
UNIT_CUBIC_METER,
UNIT_EMPTY,
UNIT_KELVIN,
UNIT_KILOWATT,
)
CODEOWNERS = ["@cfeenstra1024"]
DEPENDENCIES = ["uart"]
kamstrup_kmp_ns = cg.esphome_ns.namespace("kamstrup_kmp")
KamstrupKMPComponent = kamstrup_kmp_ns.class_(
"KamstrupKMPComponent", cg.PollingComponent, uart.UARTDevice
)
CONF_HEAT_ENERGY = "heat_energy"
CONF_TEMP1 = "temp1"
CONF_TEMP2 = "temp2"
CONF_TEMP_DIFF = "temp_diff"
UNIT_GIGA_JOULE = "GJ"
UNIT_LITRE_PER_HOUR = "l/h"
# Note: The sensor units are set automatically based un the received data from the meter
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(KamstrupKMPComponent),
cv.Optional(CONF_HEAT_ENERGY): sensor.sensor_schema(
accuracy_decimals=3,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
unit_of_measurement=UNIT_GIGA_JOULE,
),
cv.Optional(CONF_POWER): sensor.sensor_schema(
accuracy_decimals=3,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
unit_of_measurement=UNIT_KILOWATT,
),
cv.Optional(CONF_TEMP1): sensor.sensor_schema(
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
unit_of_measurement=UNIT_CELSIUS,
),
cv.Optional(CONF_TEMP2): sensor.sensor_schema(
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
unit_of_measurement=UNIT_CELSIUS,
),
cv.Optional(CONF_TEMP_DIFF): sensor.sensor_schema(
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
unit_of_measurement=UNIT_KELVIN,
),
cv.Optional(CONF_FLOW): sensor.sensor_schema(
accuracy_decimals=1,
device_class=DEVICE_CLASS_VOLUME,
state_class=STATE_CLASS_MEASUREMENT,
unit_of_measurement=UNIT_LITRE_PER_HOUR,
),
cv.Optional(CONF_VOLUME): sensor.sensor_schema(
accuracy_decimals=1,
device_class=DEVICE_CLASS_VOLUME,
state_class=STATE_CLASS_TOTAL_INCREASING,
unit_of_measurement=UNIT_CUBIC_METER,
),
cv.Optional(CONF_CUSTOM): cv.ensure_list(
sensor.sensor_schema(
accuracy_decimals=1,
device_class=DEVICE_CLASS_EMPTY,
state_class=STATE_CLASS_MEASUREMENT,
unit_of_measurement=UNIT_EMPTY,
).extend({cv.Required(CONF_COMMAND): cv.hex_uint16_t})
),
}
)
.extend(cv.polling_component_schema("60s"))
.extend(uart.UART_DEVICE_SCHEMA)
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"kamstrup_kmp", baud_rate=1200, require_rx=True, require_tx=True
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
# Standard sensors
for key in [
CONF_HEAT_ENERGY,
CONF_POWER,
CONF_TEMP1,
CONF_TEMP2,
CONF_TEMP_DIFF,
CONF_FLOW,
CONF_VOLUME,
]:
if key not in config:
continue
conf = config[key]
sens = await sensor.new_sensor(conf)
cg.add(getattr(var, f"set_{key}_sensor")(sens))
# Custom sensors
if CONF_CUSTOM in config:
for conf in config[CONF_CUSTOM]:
sens = await sensor.new_sensor(conf)
cg.add(var.add_custom_sensor(sens, conf[CONF_COMMAND]))

View file

@ -40,9 +40,9 @@ There are three documented parameters for modes:
00 04 = Energy output mode
This mode outputs detailed signal energy values for each gate and the target distance.
The data format consist of the following.
Header HH, Length LL, Persence PP, Distance DD, Range Gate GG, 16 Gate Energies EE, Footer FF
HH HH HH HH LL LL PP DD DD GG GG EE EE .. 16x .. FF FF FF FF
F4 F3 F2 F1 00 23 00 00 00 00 01 00 00 .. .. .. .. F8 F7 F6 F5
Header HH, Length LL, Persence PP, Distance DD, 16 Gate Energies EE, Footer FF
HH HH HH HH LL LL PP DD DD EE EE .. 16x .. FF FF FF FF
F4 F3 F2 F1 23 00 00 00 00 00 00 .. .. .. .. F8 F7 F6 F5
00 00 = debug output mode
This mode outputs detailed values consisting of 20 Dopplers, 16 Ranges for a total 20 * 16 * 4 bytes
The data format consist of the following.
@ -211,10 +211,11 @@ void LD2420Component::factory_reset_action() {
void LD2420Component::restart_module_action() {
ESP_LOGCONFIG(TAG, "Restarting LD2420 module...");
this->send_module_restart();
delay_microseconds_safe(45000);
this->set_timeout(250, [this]() {
this->set_config_mode(true);
this->set_system_mode(system_mode_);
this->set_config_mode(false);
});
ESP_LOGCONFIG(TAG, "LD2420 Restarted.");
}
@ -527,10 +528,8 @@ int LD2420Component::send_cmd_from_array(CmdFrameT frame) {
this->write_byte(cmd_buffer[index]);
}
delay_microseconds_safe(500); // give the module a moment to process it
error = 0;
if (frame.command == CMD_RESTART) {
delay_microseconds_safe(25000); // Wait for the restart
return 0; // restart does not reply exit now
}
@ -538,7 +537,7 @@ int LD2420Component::send_cmd_from_array(CmdFrameT frame) {
while (available()) {
this->readline_(read(), ack_buffer, sizeof(ack_buffer));
}
delay_microseconds_safe(250);
delay_microseconds_safe(1450);
if (loop_count <= 0) {
error = LD2420_ERROR_TIMEOUT;
retry--;

View file

@ -96,6 +96,12 @@ esp_err_t configure_timer_frequency(ledc_mode_t speed_mode, ledc_timer_t timer_n
}
#endif
#ifdef USE_ESP_IDF
constexpr int ledc_angle_to_htop(float angle, uint8_t bit_depth) {
return static_cast<int>(angle * ((1U << bit_depth) - 1) / 360.);
}
#endif // USE_ESP_IDF
void LEDCOutput::write_state(float state) {
if (!initialized_) {
ESP_LOGW(TAG, "LEDC output hasn't been initialized yet!");
@ -117,7 +123,8 @@ void LEDCOutput::write_state(float state) {
#ifdef USE_ESP_IDF
auto speed_mode = get_speed_mode(channel_);
auto chan_num = static_cast<ledc_channel_t>(channel_ % 8);
ledc_set_duty(speed_mode, chan_num, duty);
int hpoint = ledc_angle_to_htop(this->phase_angle_, this->bit_depth_);
ledc_set_duty_with_hpoint(speed_mode, chan_num, duty, hpoint);
ledc_update_duty(speed_mode, chan_num);
#endif
}
@ -143,8 +150,10 @@ void LEDCOutput::setup() {
this->status_set_error();
return;
}
int hpoint = ledc_angle_to_htop(this->phase_angle_, this->bit_depth_);
ESP_LOGV(TAG, "Configured frequency %f with a bit depth of %u bits", this->frequency_, this->bit_depth_);
ESP_LOGV(TAG, "Angle of %.1f° results in hpoint %u", this->phase_angle_, hpoint);
ledc_channel_config_t chan_conf{};
chan_conf.gpio_num = pin_->get_pin();
@ -153,7 +162,7 @@ void LEDCOutput::setup() {
chan_conf.intr_type = LEDC_INTR_DISABLE;
chan_conf.timer_sel = timer_num;
chan_conf.duty = inverted_ == pin_->is_inverted() ? 0 : (1U << bit_depth_);
chan_conf.hpoint = 0;
chan_conf.hpoint = hpoint;
ledc_channel_config(&chan_conf);
initialized_ = true;
this->status_clear_error();
@ -165,6 +174,7 @@ void LEDCOutput::dump_config() {
LOG_PIN(" Pin ", this->pin_);
ESP_LOGCONFIG(TAG, " LEDC Channel: %u", this->channel_);
ESP_LOGCONFIG(TAG, " PWM Frequency: %.1f Hz", this->frequency_);
ESP_LOGCONFIG(TAG, " Phase angle: %.1f°", this->phase_angle_);
ESP_LOGCONFIG(TAG, " Bit depth: %u", this->bit_depth_);
ESP_LOGV(TAG, " Max frequency for bit depth: %f", ledc_max_frequency_for_bit_depth(this->bit_depth_));
ESP_LOGV(TAG, " Min frequency for bit depth: %f",

View file

@ -19,6 +19,7 @@ class LEDCOutput : public output::FloatOutput, public Component {
void set_channel(uint8_t channel) { this->channel_ = channel; }
void set_frequency(float frequency) { this->frequency_ = frequency; }
void set_phase_angle(float angle) { this->phase_angle_ = angle; }
/// Dynamically change frequency at runtime
void update_frequency(float frequency) override;
@ -35,6 +36,7 @@ class LEDCOutput : public output::FloatOutput, public Component {
InternalGPIOPin *pin_;
uint8_t channel_{};
uint8_t bit_depth_{};
float phase_angle_{0.0f};
float frequency_{};
float duty_{0.0f};
bool initialized_ = false;

View file

@ -3,6 +3,7 @@ from esphome.components import output
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.const import (
CONF_PHASE_ANGLE,
CONF_CHANNEL,
CONF_FREQUENCY,
CONF_ID,
@ -46,6 +47,9 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema,
cv.Optional(CONF_FREQUENCY, default="1kHz"): cv.frequency,
cv.Optional(CONF_CHANNEL): cv.int_range(min=0, max=15),
cv.Optional(CONF_PHASE_ANGLE): cv.All(
cv.only_with_esp_idf, cv.angle, cv.float_range(min=0.0, max=360.0)
),
}
).extend(cv.COMPONENT_SCHEMA)
@ -58,6 +62,8 @@ async def to_code(config):
if CONF_CHANNEL in config:
cg.add(var.set_channel(config[CONF_CHANNEL]))
cg.add(var.set_frequency(config[CONF_FREQUENCY]))
if CONF_PHASE_ANGLE in config:
cg.add(var.set_phase_angle(config[CONF_PHASE_ANGLE]))
@automation.register_action(

View file

@ -129,7 +129,7 @@ void Logger::pre_setup() {
this->uart_num_ = UART_NUM_2;
break;
#endif
#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
#ifdef USE_LOGGER_USB_CDC
case UART_SELECTION_USB_CDC:
this->uart_num_ = -1;
break;

View file

@ -3,7 +3,7 @@ import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.automation import maybe_simple_id
from esphome.const import CONF_ID, CONF_ON_STATE, CONF_TRIGGER_ID
from esphome.const import CONF_ID, CONF_ON_STATE, CONF_TRIGGER_ID, CONF_VOLUME
from esphome.core import CORE
from esphome.coroutine import coroutine_with_priority
from esphome.cpp_helpers import setup_entity
@ -43,7 +43,6 @@ VolumeSetAction = media_player_ns.class_(
)
CONF_VOLUME = "volume"
CONF_ON_IDLE = "on_idle"
CONF_ON_PLAY = "on_play"
CONF_ON_PAUSE = "on_pause"

View file

@ -93,11 +93,18 @@ int MicroWakeWord::read_microphone_() {
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);
size_t bytes_free = this->ring_buffer_->free();
if (bytes_free < bytes_read) {
ESP_LOGW(TAG,
"Not enough free bytes in ring buffer to store incoming audio data (free bytes=%d, incoming bytes=%d). "
"Resetting the ring buffer. Wake word detection accuracy will be reduced.",
bytes_free, bytes_read);
this->ring_buffer_->reset();
}
return bytes_written;
return this->ring_buffer_->write((void *) this->input_buffer_, bytes_read);
}
void MicroWakeWord::loop() {
@ -206,12 +213,6 @@ bool MicroWakeWord::initialize_models() {
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");
@ -225,7 +226,7 @@ bool MicroWakeWord::initialize_models() {
}
static tflite::MicroMutableOpResolver<18> preprocessor_op_resolver;
static tflite::MicroMutableOpResolver<14> streaming_op_resolver;
static tflite::MicroMutableOpResolver<17> streaming_op_resolver;
if (!this->register_preprocessor_ops_(preprocessor_op_resolver))
return false;
@ -329,7 +330,6 @@ bool MicroWakeWord::detect_wake_word_() {
}
// Perform inference
uint32_t streaming_size = micros();
float streaming_prob = this->perform_streaming_inference_();
// Add the most recent probability to the sliding window
@ -357,6 +357,9 @@ bool MicroWakeWord::detect_wake_word_() {
for (auto &prob : this->recent_streaming_probabilities_) {
prob = 0;
}
ESP_LOGD(TAG, "Wake word sliding average probability is %.3f and most recent probability is %.3f",
sliding_window_average, streaming_prob);
return true;
}
@ -371,23 +374,6 @@ void MicroWakeWord::set_sliding_window_average_size(size_t size) {
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));
}
@ -396,13 +382,12 @@ bool MicroWakeWord::stride_audio_samples_(int16_t **audio_samples) {
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_),
// Copy the last 320 bytes (160 samples over 10 ms) from the audio buffer to the start of the audio buffer
memcpy((void *) (this->preprocessor_audio_buffer_), (void *) (this->preprocessor_audio_buffer_ + NEW_SAMPLES_TO_GET),
HISTORY_SAMPLES_TO_KEEP * sizeof(int16_t));
// Copy 640 bytes (320 samples over 20 ms) from the ring buffer
// The first 320 bytes (160 samples over 10 ms) will be from history
// Copy 640 bytes (320 samples over 20 ms) from the ring buffer into the audio buffer offset 320 bytes (160 samples
// over 10 ms)
size_t bytes_read = this->ring_buffer_->read((void *) (this->preprocessor_audio_buffer_ + HISTORY_SAMPLES_TO_KEEP),
NEW_SAMPLES_TO_GET * sizeof(int16_t), pdMS_TO_TICKS(200));
@ -415,11 +400,6 @@ bool MicroWakeWord::stride_audio_samples_(int16_t **audio_samples) {
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;
}
@ -480,7 +460,7 @@ bool MicroWakeWord::register_preprocessor_ops_(tflite::MicroMutableOpResolver<18
return true;
}
bool MicroWakeWord::register_streaming_ops_(tflite::MicroMutableOpResolver<14> &op_resolver) {
bool MicroWakeWord::register_streaming_ops_(tflite::MicroMutableOpResolver<17> &op_resolver) {
if (op_resolver.AddCallOnce() != kTfLiteOk)
return false;
if (op_resolver.AddVarHandle() != kTfLiteOk)
@ -509,6 +489,12 @@ bool MicroWakeWord::register_streaming_ops_(tflite::MicroMutableOpResolver<14> &
return false;
if (op_resolver.AddQuantize() != kTfLiteOk)
return false;
if (op_resolver.AddDepthwiseConv2D() != kTfLiteOk)
return false;
if (op_resolver.AddAveragePool2D() != kTfLiteOk)
return false;
if (op_resolver.AddMaxPool2D() != kTfLiteOk)
return false;
return true;
}

View file

@ -128,7 +128,6 @@ class MicroWakeWord : public Component {
// Stores audio fed into feature generator preprocessor
int16_t *preprocessor_audio_buffer_;
int16_t *preprocessor_stride_buffer_;
bool detected_{false};
@ -181,7 +180,7 @@ class MicroWakeWord : public Component {
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);
bool register_streaming_ops_(tflite::MicroMutableOpResolver<17> &op_resolver);
};
template<typename... Ts> class StartAction : public Action<Ts...>, public Parented<MicroWakeWord> {

View file

@ -187,11 +187,7 @@ void MQTTClientComponent::start_dnslookup_() {
default:
case ERR_ARG: {
// error
#if defined(USE_ESP8266)
ESP_LOGW(TAG, "Error resolving MQTT broker IP address: %ld", err);
#else
ESP_LOGW(TAG, "Error resolving MQTT broker IP address: %d", err);
#endif
break;
}
}

View file

@ -6,6 +6,9 @@ from esphome.components.esp32 import add_idf_sdkconfig_option
from esphome.const import (
CONF_ENABLE_IPV6,
CONF_MIN_IPV6_ADDR_COUNT,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_RP2040,
)
CODEOWNERS = ["@esphome/core"]
@ -16,15 +19,20 @@ IPAddress = network_ns.class_("IPAddress")
CONFIG_SCHEMA = cv.Schema(
{
cv.Optional(CONF_ENABLE_IPV6, default=False): cv.boolean,
cv.SplitDefault(CONF_ENABLE_IPV6): cv.All(
cv.boolean, cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040])
),
cv.Optional(CONF_MIN_IPV6_ADDR_COUNT, default=0): cv.positive_int,
}
)
async def to_code(config):
if CONF_ENABLE_IPV6 in config:
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])
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(

View file

@ -82,16 +82,16 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
/**
* Set the picture of an image component.
* @param component The component name.
* @param value The picture name.
* @param value The picture id.
*
* Example:
* ```cpp
* it.set_component_picture("pic", "4");
* it.set_component_picture("pic", 4);
* ```
*
* This will change the image of the component `pic` to the image with ID `4`.
*/
void set_component_picture(const char *component, const char *picture);
void set_component_picture(const char *component, uint8_t picture_id);
/**
* Set the background color of a component.
* @param component The component name.

View file

@ -197,8 +197,8 @@ void Nextion::disable_component_touch(const char *component) {
this->add_no_result_to_queue_with_printf_("disable_component_touch", "tsw %s,0", component);
}
void Nextion::set_component_picture(const char *component, const char *picture) {
this->add_no_result_to_queue_with_printf_("set_component_picture", "%s.val=%s", component, picture);
void Nextion::set_component_picture(const char *component, uint8_t picture_id) {
this->add_no_result_to_queue_with_printf_("set_component_picture", "%s.pic=%d", component, picture_id);
}
void Nextion::set_component_text(const char *component, const char *text) {

View file

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

View file

@ -0,0 +1,131 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import pins
from esphome.components import (
spi,
display,
)
from esphome.const import (
CONF_RESET_PIN,
CONF_ID,
CONF_DIMENSIONS,
CONF_WIDTH,
CONF_HEIGHT,
CONF_LAMBDA,
CONF_BRIGHTNESS,
CONF_ENABLE_PIN,
CONF_MODEL,
CONF_OFFSET_HEIGHT,
CONF_OFFSET_WIDTH,
CONF_INVERT_COLORS,
CONF_MIRROR_X,
CONF_MIRROR_Y,
CONF_SWAP_XY,
CONF_COLOR_ORDER,
CONF_TRANSFORM,
)
DEPENDENCIES = ["spi"]
qspi_amoled_ns = cg.esphome_ns.namespace("qspi_amoled")
QSPI_AMOLED = qspi_amoled_ns.class_(
"QspiAmoLed", display.Display, display.DisplayBuffer, cg.Component, spi.SPIDevice
)
ColorOrder = display.display_ns.enum("ColorMode")
Model = qspi_amoled_ns.enum("Model")
MODELS = {"RM690B0": Model.RM690B0, "RM67162": Model.RM67162}
COLOR_ORDERS = {
"RGB": ColorOrder.COLOR_ORDER_RGB,
"BGR": ColorOrder.COLOR_ORDER_BGR,
}
DATA_PIN_SCHEMA = pins.internal_gpio_output_pin_schema
CONFIG_SCHEMA = cv.All(
display.FULL_DISPLAY_SCHEMA.extend(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(QSPI_AMOLED),
cv.Required(CONF_MODEL): cv.enum(MODELS, upper=True),
cv.Required(CONF_DIMENSIONS): cv.Any(
cv.dimensions,
cv.Schema(
{
cv.Required(CONF_WIDTH): cv.int_,
cv.Required(CONF_HEIGHT): cv.int_,
cv.Optional(CONF_OFFSET_HEIGHT, default=0): cv.int_,
cv.Optional(CONF_OFFSET_WIDTH, default=0): cv.int_,
}
),
),
cv.Optional(CONF_TRANSFORM): cv.Schema(
{
cv.Optional(CONF_MIRROR_X, default=False): cv.boolean,
cv.Optional(CONF_MIRROR_Y, default=False): cv.boolean,
cv.Optional(CONF_SWAP_XY, default=False): cv.boolean,
}
),
cv.Optional(CONF_COLOR_ORDER, default="RGB"): cv.enum(
COLOR_ORDERS, upper=True
),
cv.Optional(CONF_INVERT_COLORS, default=False): cv.boolean,
cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
cv.Optional(CONF_ENABLE_PIN): pins.gpio_output_pin_schema,
cv.Optional(CONF_BRIGHTNESS, default=0xD0): cv.int_range(
0, 0xFF, min_included=True, max_included=True
),
}
).extend(
spi.spi_device_schema(
cs_pin_required=False,
default_mode="MODE0",
default_data_rate=10e6,
quad=True,
)
)
),
cv.only_with_esp_idf,
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await display.register_display(var, config)
await spi.register_spi_device(var, config)
cg.add(var.set_color_mode(config[CONF_COLOR_ORDER]))
cg.add(var.set_invert_colors(config[CONF_INVERT_COLORS]))
cg.add(var.set_brightness(config[CONF_BRIGHTNESS]))
cg.add(var.set_model(config[CONF_MODEL]))
if enable_pin := config.get(CONF_ENABLE_PIN):
enable = await cg.gpio_pin_expression(enable_pin)
cg.add(var.set_enable_pin(enable))
if reset_pin := config.get(CONF_RESET_PIN):
reset = await cg.gpio_pin_expression(reset_pin)
cg.add(var.set_reset_pin(reset))
if transform := config.get(CONF_TRANSFORM):
cg.add(var.set_mirror_x(transform[CONF_MIRROR_X]))
cg.add(var.set_mirror_y(transform[CONF_MIRROR_Y]))
cg.add(var.set_swap_xy(transform[CONF_SWAP_XY]))
if CONF_DIMENSIONS in config:
dimensions = config[CONF_DIMENSIONS]
if isinstance(dimensions, dict):
cg.add(var.set_dimensions(dimensions[CONF_WIDTH], dimensions[CONF_HEIGHT]))
cg.add(
var.set_offsets(
dimensions[CONF_OFFSET_WIDTH], dimensions[CONF_OFFSET_HEIGHT]
)
)
else:
(width, height) = dimensions
cg.add(var.set_dimensions(width, height))
if lamb := config.get(CONF_LAMBDA):
lambda_ = await cg.process_lambda(
lamb, [(display.DisplayRef, "it")], return_type=cg.void
)
cg.add(var.set_writer(lambda_))

View file

@ -0,0 +1,165 @@
#ifdef USE_ESP_IDF
#include "qspi_amoled.h"
#include "esphome/core/log.h"
namespace esphome {
namespace qspi_amoled {
void QspiAmoLed::setup() {
esph_log_config(TAG, "Setting up QSPI_AMOLED");
this->spi_setup();
if (this->enable_pin_ != nullptr) {
this->enable_pin_->setup();
this->enable_pin_->digital_write(true);
}
if (this->reset_pin_ != nullptr) {
this->reset_pin_->setup();
this->reset_pin_->digital_write(true);
delay(5);
this->reset_pin_->digital_write(false);
delay(5);
this->reset_pin_->digital_write(true);
}
this->set_timeout(120, [this] { this->write_command_(SLEEP_OUT); });
this->set_timeout(240, [this] { this->write_init_sequence_(); });
}
void QspiAmoLed::update() {
this->do_update_();
int w = this->x_high_ - this->x_low_ + 1;
int h = this->y_high_ - this->y_low_ + 1;
this->draw_pixels_at(this->x_low_, this->y_low_, w, h, this->buffer_, this->color_mode_, display::COLOR_BITNESS_565,
true, this->x_low_, this->y_low_, this->get_width_internal() - w - this->x_low_);
// invalidate watermarks
this->x_low_ = this->width_;
this->y_low_ = this->height_;
this->x_high_ = 0;
this->y_high_ = 0;
}
void QspiAmoLed::draw_absolute_pixel_internal(int x, int y, Color color) {
if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) {
return;
}
if (this->buffer_ == nullptr)
this->init_internal_(this->width_ * this->height_ * 2);
if (this->is_failed())
return;
uint32_t pos = (y * this->width_) + x;
uint16_t new_color;
bool updated = false;
pos = pos * 2;
new_color = display::ColorUtil::color_to_565(color, display::ColorOrder::COLOR_ORDER_RGB);
if (this->buffer_[pos] != (uint8_t) (new_color >> 8)) {
this->buffer_[pos] = (uint8_t) (new_color >> 8);
updated = true;
}
pos = pos + 1;
new_color = new_color & 0xFF;
if (this->buffer_[pos] != new_color) {
this->buffer_[pos] = new_color;
updated = true;
}
if (updated) {
// low and high watermark may speed up drawing from buffer
if (x < this->x_low_)
this->x_low_ = x;
if (y < this->y_low_)
this->y_low_ = y;
if (x > this->x_high_)
this->x_high_ = x;
if (y > this->y_high_)
this->y_high_ = y;
}
}
void QspiAmoLed::reset_params_(bool ready) {
if (!ready && !this->is_ready())
return;
this->write_command_(this->invert_colors_ ? INVERT_ON : INVERT_OFF);
// custom x/y transform and color order
uint8_t mad = this->color_mode_ == display::COLOR_ORDER_BGR ? MADCTL_BGR : MADCTL_RGB;
if (this->swap_xy_)
mad |= MADCTL_MV;
if (this->mirror_x_)
mad |= MADCTL_MX;
if (this->mirror_y_)
mad |= MADCTL_MY;
this->write_command_(MADCTL_CMD, &mad, 1);
this->write_command_(BRIGHTNESS, &this->brightness_, 1);
}
void QspiAmoLed::write_init_sequence_() {
if (this->model_ == RM690B0) {
this->write_command_(PAGESEL, 0x20);
this->write_command_(MIPI, 0x0A);
this->write_command_(WRAM, 0x80);
this->write_command_(SWIRE1, 0x51);
this->write_command_(SWIRE2, 0x2E);
this->write_command_(PAGESEL, 0x00);
this->write_command_(0xC2, 0x00);
delay(10);
this->write_command_(TEON, 0x00);
}
this->write_command_(PIXFMT, 0x55);
this->write_command_(BRIGHTNESS, 0);
this->write_command_(DISPLAY_ON);
this->reset_params_(true);
this->setup_complete_ = true;
esph_log_config(TAG, "QSPI_AMOLED setup complete");
}
void QspiAmoLed::set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) {
uint8_t buf[4];
x1 += this->offset_x_;
x2 += this->offset_x_;
y1 += this->offset_y_;
y2 += this->offset_y_;
put16_be(buf, x1);
put16_be(buf + 2, x2);
this->write_command_(CASET, buf, sizeof buf);
put16_be(buf, y1);
put16_be(buf + 2, y2);
this->write_command_(RASET, buf, sizeof buf);
}
void QspiAmoLed::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
if (!this->setup_complete_ || this->is_failed())
return;
if (w <= 0 || h <= 0)
return;
if (bitness != display::COLOR_BITNESS_565 || order != this->color_mode_ ||
big_endian != (this->bit_order_ == spi::BIT_ORDER_MSB_FIRST)) {
return display::Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset,
x_pad);
}
this->set_addr_window_(x_start, y_start, x_start + w - 1, y_start + h - 1);
this->enable();
// x_ and y_offset are offsets into the source buffer, unrelated to our own offsets into the display.
if (x_offset == 0 && x_pad == 0 && y_offset == 0) {
// we could deal here with a non-zero y_offset, but if x_offset is zero, y_offset probably will be so don't bother
this->write_cmd_addr_data(8, 0x32, 24, 0x2C00, ptr, w * h * 2, 4);
} else {
this->write_cmd_addr_data(8, 0x32, 24, 0x2C00, nullptr, 0, 4);
auto stride = x_offset + w + x_pad;
for (int y = 0; y != h; y++) {
this->write_cmd_addr_data(0, 0, 0, 0, ptr + ((y + y_offset) * stride + x_offset) * 2, w * 2, 4);
}
}
this->disable();
}
void QspiAmoLed::dump_config() {
ESP_LOGCONFIG("", "QSPI AMOLED");
ESP_LOGCONFIG(TAG, " Height: %u", this->height_);
ESP_LOGCONFIG(TAG, " Width: %u", this->width_);
LOG_PIN(" CS Pin: ", this->cs_);
LOG_PIN(" Reset Pin: ", this->reset_pin_);
ESP_LOGCONFIG(TAG, " SPI Data rate: %dMHz", (unsigned) (this->data_rate_ / 1000000));
}
} // namespace qspi_amoled
} // namespace esphome
#endif

View file

@ -0,0 +1,165 @@
//
// Created by Clyde Stubbs on 29/10/2023.
//
#pragma once
#ifdef USE_ESP_IDF
#include "esphome/core/component.h"
#include "esphome/components/spi/spi.h"
#include "esphome/components/display/display.h"
#include "esphome/components/display/display_buffer.h"
#include "esphome/components/display/display_color_utils.h"
#include "esp_lcd_panel_ops.h"
#include "esp_lcd_panel_rgb.h"
namespace esphome {
namespace qspi_amoled {
constexpr static const char *const TAG = "display.qspi_amoled";
static const uint8_t SW_RESET_CMD = 0x01;
static const uint8_t SLEEP_OUT = 0x11;
static const uint8_t INVERT_OFF = 0x20;
static const uint8_t INVERT_ON = 0x21;
static const uint8_t ALL_ON = 0x23;
static const uint8_t WRAM = 0x24;
static const uint8_t MIPI = 0x26;
static const uint8_t DISPLAY_ON = 0x29;
static const uint8_t RASET = 0x2B;
static const uint8_t CASET = 0x2A;
static const uint8_t WDATA = 0x2C;
static const uint8_t TEON = 0x35;
static const uint8_t MADCTL_CMD = 0x36;
static const uint8_t PIXFMT = 0x3A;
static const uint8_t BRIGHTNESS = 0x51;
static const uint8_t SWIRE1 = 0x5A;
static const uint8_t SWIRE2 = 0x5B;
static const uint8_t PAGESEL = 0xFE;
static const uint8_t MADCTL_MY = 0x80; ///< Bit 7 Bottom to top
static const uint8_t MADCTL_MX = 0x40; ///< Bit 6 Right to left
static const uint8_t MADCTL_MV = 0x20; ///< Bit 5 Reverse Mode
static const uint8_t MADCTL_RGB = 0x00; ///< Bit 3 Red-Green-Blue pixel order
static const uint8_t MADCTL_BGR = 0x08; ///< Bit 3 Blue-Green-Red pixel order
// store a 16 bit value in a buffer, big endian.
static inline void put16_be(uint8_t *buf, uint16_t value) {
buf[0] = value >> 8;
buf[1] = value;
}
enum Model {
RM690B0,
RM67162,
};
class QspiAmoLed : public display::DisplayBuffer,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
spi::DATA_RATE_1MHZ> {
public:
void set_model(Model model) { this->model_ = model; }
void update() override;
void setup() override;
display::ColorOrder get_color_mode() { return this->color_mode_; }
void set_color_mode(display::ColorOrder color_mode) { this->color_mode_ = color_mode; }
void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; }
void set_enable_pin(GPIOPin *enable_pin) { this->enable_pin_ = enable_pin; }
void set_width(uint16_t width) { this->width_ = width; }
void set_dimensions(uint16_t width, uint16_t height) {
this->width_ = width;
this->height_ = height;
}
int get_width() override { return this->width_; }
int get_height() override { return this->height_; }
void set_invert_colors(bool invert_colors) {
this->invert_colors_ = invert_colors;
this->reset_params_();
}
void set_mirror_x(bool mirror_x) {
this->mirror_x_ = mirror_x;
this->reset_params_();
}
void set_mirror_y(bool mirror_y) {
this->mirror_y_ = mirror_y;
this->reset_params_();
}
void set_swap_xy(bool swap_xy) {
this->swap_xy_ = swap_xy;
this->reset_params_();
}
void set_brightness(uint8_t brightness) {
this->brightness_ = brightness;
this->reset_params_();
}
void set_offsets(int16_t offset_x, int16_t offset_y) {
this->offset_x_ = offset_x;
this->offset_y_ = offset_y;
}
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
void dump_config() override;
int get_width_internal() override { return this->width_; }
int get_height_internal() override { return this->height_; }
bool can_proceed() override { return this->setup_complete_; }
protected:
void draw_absolute_pixel_internal(int x, int y, Color color) override;
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
/**
* the RM67162 in quad SPI mode seems to work like this (not in the datasheet, this is deduced from the
* sample code.)
*
* Immediately after enabling /CS send 4 bytes in single-dataline SPI mode:
* 0: either 0x2 or 0x32. The first indicates that any subsequent data bytes after the initial 4 will be
* sent in 1-dataline SPI. The second indicates quad mode.
* 1: 0x00
* 2: The command (register address) byte.
* 3: 0x00
*
* This is followed by zero or more data bytes in either 1-wire or 4-wire mode, depending on the first byte.
* At the conclusion of the write, de-assert /CS.
*
* @param cmd
* @param bytes
* @param len
*/
void write_command_(uint8_t cmd, const uint8_t *bytes, size_t len) {
this->enable();
this->write_cmd_addr_data(8, 0x02, 24, cmd << 8, bytes, len);
this->disable();
}
void write_command_(uint8_t cmd, uint8_t data) { this->write_command_(cmd, &data, 1); }
void write_command_(uint8_t cmd) { this->write_command_(cmd, &cmd, 0); }
void reset_params_(bool ready = false);
void write_init_sequence_();
void set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2);
GPIOPin *reset_pin_{nullptr};
GPIOPin *enable_pin_{nullptr};
uint16_t x_low_{0};
uint16_t y_low_{0};
uint16_t x_high_{0};
uint16_t y_high_{0};
bool setup_complete_{};
bool invert_colors_{};
display::ColorOrder color_mode_{display::COLOR_ORDER_BGR};
size_t width_{};
size_t height_{};
int16_t offset_x_{0};
int16_t offset_y_{0};
bool swap_xy_{};
bool mirror_x_{};
bool mirror_y_{};
uint8_t brightness_{0xD0};
Model model_{RM690B0};
esp_lcd_panel_handle_t handle_{};
};
} // namespace qspi_amoled
} // namespace esphome
#endif

View file

@ -32,6 +32,7 @@ from esphome.const import (
CONF_MAGNITUDE,
CONF_WAND_ID,
CONF_LEVEL,
CONF_DELTA,
)
from esphome.core import coroutine
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
@ -792,6 +793,7 @@ async def pioneer_action(var, config, args):
PRONTO_SCHEMA = cv.Schema(
{
cv.Required(CONF_DATA): cv.string,
cv.Optional(CONF_DELTA, default=-1): cv.int_,
}
)
@ -803,6 +805,7 @@ def pronto_binary_sensor(var, config):
cg.StructInitializer(
ProntoData,
("data", config[CONF_DATA]),
("delta", config[CONF_DELTA]),
)
)
)

View file

@ -1,6 +1,8 @@
#include "keeloq_protocol.h"
#include "esphome/core/log.h"
#include <cinttypes>
namespace esphome {
namespace remote_base {
@ -34,7 +36,8 @@ transmitter and nutton command is decoded.
void KeeloqProtocol::encode(RemoteTransmitData *dst, const KeeloqData &data) {
uint32_t out_data = 0x0;
ESP_LOGD(TAG, "Send Keeloq: address=%07x command=%03x encrypted=%08x", data.address, data.command, data.encrypted);
ESP_LOGD(TAG, "Send Keeloq: address=%07" PRIx32 " command=%03x encrypted=%08" PRIx32, data.address, data.command,
data.encrypted);
ESP_LOGV(TAG, "Send Keeloq: data bits (%d + %d)", NBITS_ENCRYPTED_DATA, NBITS_FIXED_DATA);
// Preamble = '01' x 12
@ -181,7 +184,7 @@ optional<KeeloqData> KeeloqProtocol::decode(RemoteReceiveData src) {
}
void KeeloqProtocol::dump(const KeeloqData &data) {
ESP_LOGD(TAG, "Received Keeloq: address=0x%08X, command=0x%02x", data.address, data.command);
ESP_LOGD(TAG, "Received Keeloq: address=0x%08" PRIx32 ", command=0x%02x", data.address, data.command);
}
} // namespace remote_base

View file

@ -49,13 +49,13 @@ bool ProntoData::operator==(const ProntoData &rhs) const {
for (std::vector<uint16_t>::size_type i = 0; i < data1.size() - 1; ++i) {
int diff = data2[i] - data1[i];
diff *= diff;
if (diff > 9)
if (rhs.delta == -1 && diff > 9)
return false;
total_diff += diff;
}
return total_diff <= data1.size() * 3;
return total_diff <= (rhs.delta == -1 ? data1.size() * 3 : rhs.delta);
}
// DO NOT EXPORT from this file
@ -222,6 +222,7 @@ optional<ProntoData> ProntoProtocol::decode(RemoteReceiveData src) {
prontodata += compensate_and_dump_sequence_(data, timebase);
out.data = prontodata;
out.delta = -1;
return out;
}

View file

@ -12,6 +12,7 @@ std::vector<uint16_t> encode_pronto(const std::string &str);
struct ProntoData {
std::string data;
int delta;
bool operator==(const ProntoData &rhs) const;
};
@ -40,10 +41,12 @@ DECLARE_REMOTE_PROTOCOL(Pronto)
template<typename... Ts> class ProntoAction : public RemoteTransmitterActionBase<Ts...> {
public:
TEMPLATABLE_VALUE(std::string, data)
TEMPLATABLE_VALUE(int, delta)
void encode(RemoteTransmitData *dst, Ts... x) override {
ProntoData data{};
data.data = this->data_.value(x...);
data.delta = this->delta_.value(x...);
ProntoProtocol().encode(dst, data);
}
};

View file

@ -16,7 +16,7 @@ RemoteRMTChannel::RemoteRMTChannel(uint8_t mem_block_num) : mem_block_num_(mem_b
}
void RemoteRMTChannel::config_rmt(rmt_config_t &rmt) {
if (rmt_channel_t(int(this->channel_) + this->mem_block_num_) >= RMT_CHANNEL_MAX) {
if (rmt_channel_t(int(this->channel_) + this->mem_block_num_) > RMT_CHANNEL_MAX) {
this->mem_block_num_ = int(RMT_CHANNEL_MAX) - int(this->channel_);
ESP_LOGW(TAG, "Not enough RMT memory blocks available, reduced to %i blocks.", this->mem_block_num_);
}

View file

@ -14,13 +14,12 @@ from esphome.const import (
CONF_PM_4_0,
CONF_STORE_BASELINE,
CONF_TEMPERATURE,
DEVICE_CLASS_AQI,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_NITROUS_OXIDE,
DEVICE_CLASS_PM1,
DEVICE_CLASS_PM10,
DEVICE_CLASS_PM25,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
ICON_CHEMICAL_WEAPON,
ICON_RADIATOR,
ICON_THERMOMETER,
@ -132,13 +131,13 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_VOC): sensor.sensor_schema(
icon=ICON_RADIATOR,
accuracy_decimals=0,
device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
device_class=DEVICE_CLASS_AQI,
state_class=STATE_CLASS_MEASUREMENT,
).extend(GAS_SENSOR),
cv.Optional(CONF_NOX): sensor.sensor_schema(
icon=ICON_RADIATOR,
accuracy_decimals=0,
device_class=DEVICE_CLASS_NITROUS_OXIDE,
device_class=DEVICE_CLASS_AQI,
state_class=STATE_CLASS_MEASUREMENT,
).extend(GAS_SENSOR),
cv.Optional(CONF_STORE_BASELINE, default=True): cv.boolean,

View file

@ -54,9 +54,9 @@ void SenseAirComponent::update() {
this->status_clear_warning();
const uint8_t length = response[2];
const uint16_t status = (uint16_t(response[3]) << 8) | response[4];
const uint16_t ppm = (uint16_t(response[length + 1]) << 8) | response[length + 2];
const int16_t ppm = int16_t((response[length + 1] << 8) | response[length + 2]);
ESP_LOGD(TAG, "SenseAir Received CO₂=%uppm Status=0x%02X", ppm, status);
ESP_LOGD(TAG, "SenseAir Received CO₂=%dppm Status=0x%02X", ppm, status);
if (this->co2_sensor_ != nullptr)
this->co2_sensor_->publish_state(ppm);
}

View file

@ -19,13 +19,28 @@ void Servo::dump_config() {
ESP_LOGCONFIG(TAG, " run duration: %" PRIu32 " ms", this->transition_length_);
}
void Servo::setup() {
float v;
if (this->restore_) {
this->rtc_ = global_preferences->make_preference<float>(global_servo_id);
global_servo_id++;
if (this->rtc_.load(&v)) {
this->target_value_ = v;
this->internal_write(v);
this->state_ = STATE_ATTACHED;
this->start_millis_ = millis();
return;
}
}
this->detach();
}
void Servo::loop() {
// check if auto_detach_time_ is set and servo reached target
if (this->auto_detach_time_ && this->state_ == STATE_TARGET_REACHED) {
if (millis() - this->start_millis_ > this->auto_detach_time_) {
this->detach();
this->start_millis_ = 0;
this->state_ = STATE_DETACHED;
ESP_LOGD(TAG, "Servo detached on auto_detach_time");
}
}
@ -54,8 +69,11 @@ void Servo::loop() {
void Servo::write(float value) {
value = clamp(value, -1.0f, 1.0f);
if (this->target_value_ == value)
if ((this->state_ == STATE_DETACHED) && (this->target_value_ == value)) {
this->internal_write(value);
} else {
this->save_level_(value);
}
this->target_value_ = value;
this->source_value_ = this->current_value_;
this->state_ = STATE_ATTACHED;
@ -72,11 +90,18 @@ void Servo::internal_write(float value) {
level = lerp(value, this->idle_level_, this->max_level_);
}
this->output_->set_level(level);
if (this->target_value_ == this->current_value_) {
this->save_level_(level);
}
this->current_value_ = value;
}
void Servo::detach() {
this->state_ = STATE_DETACHED;
this->output_->set_level(0.0f);
}
void Servo::save_level_(float v) {
if (this->restore_)
this->rtc_.save(&v);
}
} // namespace servo
} // namespace esphome

View file

@ -17,22 +17,8 @@ class Servo : public Component {
void loop() override;
void write(float value);
void internal_write(float value);
void detach() {
this->output_->set_level(0.0f);
this->save_level_(0.0f);
}
void setup() override {
float v;
if (this->restore_) {
this->rtc_ = global_preferences->make_preference<float>(global_servo_id);
global_servo_id++;
if (this->rtc_.load(&v)) {
this->output_->set_level(v);
return;
}
}
this->detach();
}
void detach();
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
void set_min_level(float min_level) { min_level_ = min_level; }
@ -42,8 +28,10 @@ class Servo : public Component {
void set_auto_detach_time(uint32_t auto_detach_time) { auto_detach_time_ = auto_detach_time; }
void set_transition_length(uint32_t transition_length) { transition_length_ = transition_length; }
bool has_reached_target() { return this->current_value_ == this->target_value_; }
protected:
void save_level_(float v) { this->rtc_.save(&v); }
void save_level_(float v);
output::FloatOutput *output_;
float min_level_ = 0.0300f;

View file

@ -15,6 +15,7 @@ SM2135 = sm2135_ns.class_("SM2135", cg.Component)
CONF_RGB_CURRENT = "rgb_current"
CONF_CW_CURRENT = "cw_current"
CONF_SEPARATE_MODES = "separate_modes"
SM2135Current = sm2135_ns.enum("SM2135Current")
@ -51,6 +52,7 @@ CONFIG_SCHEMA = cv.Schema(
cv.Required(CONF_CLOCK_PIN): pins.gpio_output_pin_schema,
cv.Optional(CONF_RGB_CURRENT, "20mA"): cv.enum(DRIVE_STRENGTHS_RGB),
cv.Optional(CONF_CW_CURRENT, "10mA"): cv.enum(DRIVE_STRENGTHS_CW),
cv.Optional(CONF_SEPARATE_MODES, default=True): cv.boolean,
}
).extend(cv.COMPONENT_SCHEMA)
@ -66,3 +68,4 @@ async def to_code(config):
cg.add(var.set_rgb_current(config[CONF_RGB_CURRENT]))
cg.add(var.set_cw_current(config[CONF_CW_CURRENT]))
cg.add(var.set_separate_modes(config[CONF_SEPARATE_MODES]))

View file

@ -97,6 +97,7 @@ void SM2135::loop() {
this->write_byte_(SM2135_ADDR_MC);
this->write_byte_(current_mask_);
if (this->separate_modes_) {
if (this->update_channel_ == 3 || this->update_channel_ == 4) {
// No color so must be Cold/Warm
@ -115,6 +116,14 @@ void SM2135::loop() {
this->write_byte_(this->pwm_amounts_[0]); // Red
this->write_byte_(this->pwm_amounts_[2]); // Blue
}
} else {
this->write_byte_(SM2135_RGB);
this->write_byte_(this->pwm_amounts_[1]); // Green
this->write_byte_(this->pwm_amounts_[0]); // Red
this->write_byte_(this->pwm_amounts_[2]); // Blue
this->write_byte_(this->pwm_amounts_[4]); // Warm
this->write_byte_(this->pwm_amounts_[3]); // Cold
}
this->sm2135_stop_();

View file

@ -39,6 +39,8 @@ class SM2135 : public Component {
this->current_mask_ = (this->rgb_current_ << 4) | this->cw_current_;
}
void set_separate_modes(bool separate_modes) { this->separate_modes_ = separate_modes; }
void setup() override;
void dump_config() override;
@ -78,6 +80,7 @@ class SM2135 : public Component {
uint8_t current_mask_;
SM2135Current rgb_current_;
SM2135Current cw_current_;
bool separate_modes_;
uint8_t update_channel_;
std::vector<uint8_t> pwm_amounts_;
bool update_{true};

View file

@ -29,7 +29,6 @@ from esphome.const import (
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_RP2040,
CONF_ALLOW_OTHER_USES,
CONF_DATA_PINS,
)
from esphome.core import (
@ -199,8 +198,6 @@ def get_hw_spi(config, available):
def validate_spi_config(config):
available = list(range(len(get_hw_interface_list())))
for spi in config:
# map pin number to schema
spi[CONF_CLK_PIN] = pins.gpio_output_pin_schema(spi[CONF_CLK_PIN])
interface = spi[CONF_INTERFACE]
if interface == "software":
pass
@ -257,21 +254,11 @@ def get_spi_interface(index):
return "new SPIClass(HSPI)"
# Do not use a pin schema for the number, as that will trigger a pin reuse error due to duplication of the
# clock pin in the standard and quad schemas.
clk_pin_validator = cv.maybe_simple_value(
{
cv.Required(CONF_NUMBER): cv.Any(cv.int_, cv.string),
cv.Optional(CONF_ALLOW_OTHER_USES): cv.boolean,
},
key=CONF_NUMBER,
)
SPI_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(SPIComponent),
cv.Required(CONF_CLK_PIN): clk_pin_validator,
cv.Required(CONF_CLK_PIN): pins.gpio_output_pin_schema,
cv.Optional(CONF_MISO_PIN): pins.gpio_input_pin_schema,
cv.Optional(CONF_MOSI_PIN): pins.gpio_output_pin_schema,
cv.Optional(CONF_FORCE_SW): cv.invalid(
@ -281,6 +268,9 @@ SPI_SCHEMA = cv.All(
*sum(get_hw_interface_list(), ["software", "hardware", "any"]),
lower=True,
),
cv.Optional(CONF_DATA_PINS): cv.invalid(
"'data_pins' should be used with 'type: quad' only"
),
}
),
cv.has_at_least_one_key(CONF_MISO_PIN, CONF_MOSI_PIN),
@ -291,7 +281,7 @@ SPI_QUAD_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(QuadSPIComponent),
cv.Required(CONF_CLK_PIN): clk_pin_validator,
cv.Required(CONF_CLK_PIN): pins.gpio_output_pin_schema,
cv.Required(CONF_DATA_PINS): cv.All(
cv.ensure_list(pins.internal_gpio_output_pin_number),
cv.Length(min=4, max=4),
@ -300,6 +290,12 @@ SPI_QUAD_SCHEMA = cv.All(
*sum(get_hw_interface_list(), ["hardware"]),
lower=True,
),
cv.Optional(CONF_MISO_PIN): cv.invalid(
"'miso_pin' should not be used with quad SPI"
),
cv.Optional(CONF_MOSI_PIN): cv.invalid(
"'mosi_pin' should not be used with quad SPI"
),
}
),
cv.only_on([PLATFORM_ESP32]),

View file

@ -1,6 +1,7 @@
#include "spi_device.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include <cinttypes>
namespace esphome {
namespace spi_device {
@ -18,9 +19,9 @@ void SPIDeviceComponent::dump_config() {
LOG_PIN(" CS pin: ", this->cs_);
ESP_LOGCONFIG(TAG, " Mode: %d", this->mode_);
if (this->data_rate_ < 1000000) {
ESP_LOGCONFIG(TAG, " Data rate: %dkHz", this->data_rate_ / 1000);
ESP_LOGCONFIG(TAG, " Data rate: %" PRId32 "kHz", this->data_rate_ / 1000);
} else {
ESP_LOGCONFIG(TAG, " Data rate: %dMHz", this->data_rate_ / 1000000);
ESP_LOGCONFIG(TAG, " Data rate: %" PRId32 "MHz", this->data_rate_ / 1000000);
}
}

View file

@ -0,0 +1,26 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import uart
from esphome.const import CONF_ID
CODEOWNERS = ["@Mat931"]
MULTI_CONF = True
DEPENDENCIES = ["uart"]
CONF_SUN_GTIL2_ID = "sun_gtil2_id"
sun_gtil2_ns = cg.esphome_ns.namespace("sun_gtil2")
SunGTIL2Component = sun_gtil2_ns.class_("SunGTIL2", cg.Component, uart.UARTDevice)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(SunGTIL2Component),
}
).extend(uart.UART_DEVICE_SCHEMA)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await uart.register_uart_device(var, config)

View file

@ -0,0 +1,87 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor
from esphome.const import (
DEVICE_CLASS_VOLTAGE,
DEVICE_CLASS_POWER,
DEVICE_CLASS_TEMPERATURE,
ICON_FLASH,
UNIT_VOLT,
ICON_THERMOMETER,
UNIT_WATT,
UNIT_CELSIUS,
CONF_TEMPERATURE,
)
from . import SunGTIL2Component, CONF_SUN_GTIL2_ID
CONF_AC_VOLTAGE = "ac_voltage"
CONF_DC_VOLTAGE = "dc_voltage"
CONF_AC_POWER = "ac_power"
CONF_DC_POWER = "dc_power"
CONF_LIMITER_POWER = "limiter_power"
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(CONF_SUN_GTIL2_ID): cv.use_id(SunGTIL2Component),
cv.Optional(CONF_AC_VOLTAGE): sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT,
icon=ICON_FLASH,
accuracy_decimals=1,
device_class=DEVICE_CLASS_VOLTAGE,
),
cv.Optional(CONF_DC_VOLTAGE): sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT,
icon=ICON_FLASH,
accuracy_decimals=1,
device_class=DEVICE_CLASS_VOLTAGE,
),
cv.Optional(CONF_AC_POWER): sensor.sensor_schema(
unit_of_measurement=UNIT_WATT,
icon=ICON_FLASH,
accuracy_decimals=1,
device_class=DEVICE_CLASS_POWER,
),
cv.Optional(CONF_DC_POWER): sensor.sensor_schema(
unit_of_measurement=UNIT_WATT,
icon=ICON_FLASH,
accuracy_decimals=1,
device_class=DEVICE_CLASS_POWER,
),
cv.Optional(CONF_LIMITER_POWER): sensor.sensor_schema(
unit_of_measurement=UNIT_WATT,
icon=ICON_FLASH,
accuracy_decimals=1,
device_class=DEVICE_CLASS_POWER,
),
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
icon=ICON_THERMOMETER,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
),
}
).extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config):
hub = await cg.get_variable(config[CONF_SUN_GTIL2_ID])
if ac_voltage_config := config.get(CONF_AC_VOLTAGE):
sens = await sensor.new_sensor(ac_voltage_config)
cg.add(hub.set_ac_voltage(sens))
if dc_voltage_config := config.get(CONF_DC_VOLTAGE):
sens = await sensor.new_sensor(dc_voltage_config)
cg.add(hub.set_dc_voltage(sens))
if ac_power_config := config.get(CONF_AC_POWER):
sens = await sensor.new_sensor(ac_power_config)
cg.add(hub.set_ac_power(sens))
if dc_power_config := config.get(CONF_DC_POWER):
sens = await sensor.new_sensor(dc_power_config)
cg.add(hub.set_dc_power(sens))
if limiter_power_config := config.get(CONF_LIMITER_POWER):
sens = await sensor.new_sensor(limiter_power_config)
cg.add(hub.set_limiter_power(sens))
if temperature_config := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature_config)
cg.add(hub.set_temperature(sens))

View file

@ -0,0 +1,135 @@
#include "sun_gtil2.h"
#include "esphome/core/log.h"
namespace esphome {
namespace sun_gtil2 {
static const char *const TAG = "sun_gtil2";
static const double NTC_A = 0.0011591051055979914;
static const double NTC_B = 0.00022878183547845582;
static const double NTC_C = 1.0396291358342124e-07;
static const float PULLUP_RESISTANCE = 10000.0f;
static const uint16_t ADC_MAX = 1023; // ADC of the inverter controller, not the ESP
struct SunGTIL2Message {
uint16_t sync;
uint8_t ac_waveform[277];
uint8_t frequency;
uint16_t ac_voltage;
uint16_t ac_power;
uint16_t dc_voltage;
uint8_t state;
uint8_t unknown1;
uint8_t unknown2;
uint8_t unknown3;
uint8_t limiter_mode;
uint8_t unknown4;
uint16_t temperature;
uint32_t limiter_power;
uint16_t dc_power;
char serial_number[10];
uint8_t unknown5;
uint8_t end[39];
} __attribute__((packed));
static const uint16_t MESSAGE_SIZE = sizeof(SunGTIL2Message);
static_assert(MESSAGE_SIZE == 350, "Expected the message size to be 350 bytes");
void SunGTIL2::setup() { this->rx_message_.reserve(MESSAGE_SIZE); }
void SunGTIL2::loop() {
while (this->available()) {
uint8_t c;
this->read_byte(&c);
this->handle_char_(c);
}
}
std::string SunGTIL2::state_to_string_(uint8_t state) {
switch (state) {
case 0x02:
return "Starting voltage too low";
case 0x07:
return "Working";
default:
return str_sprintf("Unknown (0x%02x)", state);
}
}
float SunGTIL2::calculate_temperature_(uint16_t adc_value) {
if (adc_value >= ADC_MAX || adc_value == 0) {
return NAN;
}
float ntc_resistance = PULLUP_RESISTANCE / ((static_cast<float>(ADC_MAX) / adc_value) - 1.0f);
double lr = log(double(ntc_resistance));
double v = NTC_A + NTC_B * lr + NTC_C * lr * lr * lr;
return float(1.0 / v - 273.15);
}
void SunGTIL2::handle_char_(uint8_t c) {
if (this->rx_message_.size() > 1 || c == 0x07) {
this->rx_message_.push_back(c);
} else if (!this->rx_message_.empty()) {
this->rx_message_.clear();
}
if (this->rx_message_.size() < MESSAGE_SIZE) {
return;
}
SunGTIL2Message msg;
memcpy(&msg, this->rx_message_.data(), MESSAGE_SIZE);
this->rx_message_.clear();
if (!((msg.end[0] == 0) && (msg.end[38] == 0x08)))
return;
ESP_LOGVV(TAG, "Frequency raw value: %02x", msg.frequency);
ESP_LOGVV(TAG, "Unknown values: %02x %02x %02x %02x %02x", msg.unknown1, msg.unknown2, msg.unknown3, msg.unknown4,
msg.unknown5);
#ifdef USE_SENSOR
if (this->ac_voltage_ != nullptr)
this->ac_voltage_->publish_state(__builtin_bswap16(msg.ac_voltage) / 10.0f);
if (this->dc_voltage_ != nullptr)
this->dc_voltage_->publish_state(__builtin_bswap16(msg.dc_voltage) / 8.0f);
if (this->ac_power_ != nullptr)
this->ac_power_->publish_state(__builtin_bswap16(msg.ac_power) / 10.0f);
if (this->dc_power_ != nullptr)
this->dc_power_->publish_state(__builtin_bswap16(msg.dc_power) / 10.0f);
if (this->limiter_power_ != nullptr)
this->limiter_power_->publish_state(static_cast<int32_t>(__builtin_bswap32(msg.limiter_power)) / 10.0f);
if (this->temperature_ != nullptr)
this->temperature_->publish_state(calculate_temperature_(__builtin_bswap16(msg.temperature)));
#endif
#ifdef USE_TEXT_SENSOR
if (this->state_ != nullptr) {
this->state_->publish_state(this->state_to_string_(msg.state));
}
if (this->serial_number_ != nullptr) {
std::string serial_number;
serial_number.assign(msg.serial_number, 10);
this->serial_number_->publish_state(serial_number);
}
#endif
}
void SunGTIL2::dump_config() {
#ifdef USE_SENSOR
LOG_SENSOR("", "AC Voltage", this->ac_voltage_);
LOG_SENSOR("", "DC Voltage", this->dc_voltage_);
LOG_SENSOR("", "AC Power", this->ac_power_);
LOG_SENSOR("", "DC Power", this->dc_power_);
LOG_SENSOR("", "Limiter Power", this->limiter_power_);
LOG_SENSOR("", "Temperature", this->temperature_);
#endif
#ifdef USE_TEXT_SENSOR
LOG_TEXT_SENSOR("", "State", this->state_);
LOG_TEXT_SENSOR("", "Serial Number", this->serial_number_);
#endif
}
} // namespace sun_gtil2
} // namespace esphome

View file

@ -0,0 +1,58 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#ifdef USE_SENSOR
#include "esphome/components/sensor/sensor.h"
#endif
#ifdef USE_TEXT_SENSOR
#include "esphome/components/text_sensor/text_sensor.h"
#endif
#include "esphome/components/uart/uart.h"
namespace esphome {
namespace sun_gtil2 {
class SunGTIL2 : public Component, public uart::UARTDevice {
public:
float get_setup_priority() const override { return setup_priority::LATE; }
void setup() override;
void loop() override;
void dump_config() override;
#ifdef USE_SENSOR
void set_ac_voltage(sensor::Sensor *sensor) { ac_voltage_ = sensor; }
void set_dc_voltage(sensor::Sensor *sensor) { dc_voltage_ = sensor; }
void set_ac_power(sensor::Sensor *sensor) { ac_power_ = sensor; }
void set_dc_power(sensor::Sensor *sensor) { dc_power_ = sensor; }
void set_limiter_power(sensor::Sensor *sensor) { limiter_power_ = sensor; }
void set_temperature(sensor::Sensor *sensor) { temperature_ = sensor; }
#endif
#ifdef USE_TEXT_SENSOR
void set_state(text_sensor::TextSensor *text_sensor) { state_ = text_sensor; }
void set_serial_number(text_sensor::TextSensor *text_sensor) { serial_number_ = text_sensor; }
#endif
protected:
std::string state_to_string_(uint8_t state);
#ifdef USE_SENSOR
sensor::Sensor *ac_voltage_{nullptr};
sensor::Sensor *dc_voltage_{nullptr};
sensor::Sensor *ac_power_{nullptr};
sensor::Sensor *dc_power_{nullptr};
sensor::Sensor *limiter_power_{nullptr};
sensor::Sensor *temperature_{nullptr};
#endif
#ifdef USE_TEXT_SENSOR
text_sensor::TextSensor *state_{nullptr};
text_sensor::TextSensor *serial_number_{nullptr};
#endif
float calculate_temperature_(uint16_t adc_value);
void handle_char_(uint8_t c);
std::vector<uint8_t> rx_message_;
};
} // namespace sun_gtil2
} // namespace esphome

View file

@ -0,0 +1,31 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import text_sensor
from esphome.const import CONF_STATE
from . import SunGTIL2Component, CONF_SUN_GTIL2_ID
CONF_SERIAL_NUMBER = "serial_number"
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(CONF_SUN_GTIL2_ID): cv.use_id(SunGTIL2Component),
cv.Optional(CONF_STATE): text_sensor.text_sensor_schema(
text_sensor.TextSensor
),
cv.Optional(CONF_SERIAL_NUMBER): text_sensor.text_sensor_schema(
text_sensor.TextSensor
),
}
).extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config):
hub = await cg.get_variable(config[CONF_SUN_GTIL2_ID])
if state_config := config.get(CONF_STATE):
sens = await text_sensor.new_text_sensor(state_config)
cg.add(hub.set_state(sens))
if serial_number_config := config.get(CONF_SERIAL_NUMBER):
sens = await text_sensor.new_text_sensor(serial_number_config)
cg.add(hub.set_serial_number(sens))

View file

@ -39,7 +39,6 @@ void Touchscreen::loop() {
ESP_LOGVV(TAG, "<< Do Touch loop >>");
this->first_touch_ = this->touches_.empty();
this->need_update_ = false;
this->was_touched_ = this->is_touched_;
this->is_touched_ = false;
this->skip_update_ = false;
for (auto &tp : this->touches_) {
@ -62,7 +61,11 @@ void Touchscreen::loop() {
if (this->touch_timeout_ > 0) {
// Simulate a touch after <this->touch_timeout_> ms. This will reset any existing timeout operation.
// This is to detect touch release.
if (this->is_touched_) {
this->set_timeout(TAG, this->touch_timeout_, [this]() { this->store_.touched = true; });
} else {
this->cancel_timeout(TAG);
}
}
}
}
@ -111,6 +114,7 @@ void Touchscreen::add_raw_touch_position_(uint8_t id, int16_t x_raw, int16_t y_r
void Touchscreen::send_touches_() {
TouchPoints_t touches;
ESP_LOGV(TAG, "Touch status: is_touched=%d, was_touched=%d", this->is_touched_, this->was_touched_);
for (auto tp : this->touches_) {
ESP_LOGV(TAG, "Touch status: %d/%d: raw:(%4d,%4d,%4d) calc:(%3d,%4d)", tp.second.id, tp.second.state,
tp.second.x_raw, tp.second.y_raw, tp.second.z_raw, tp.second.x, tp.second.y);
@ -124,14 +128,10 @@ void Touchscreen::send_touches_() {
}
if (!this->is_touched_) {
if (this->was_touched_) {
if (this->touch_timeout_ > 0) {
this->cancel_timeout(TAG);
}
this->release_trigger_.trigger();
for (auto *listener : this->touch_listeners_)
listener->release();
this->touches_.clear();
this->was_touched_ = false;
}
} else {
if (this->first_touch_) {
@ -142,6 +142,7 @@ void Touchscreen::send_touches_() {
}
}
}
this->was_touched_ = this->is_touched_;
}
int16_t Touchscreen::normalize_(int16_t val, int16_t min_val, int16_t max_val, bool inverted) {

View file

@ -7,15 +7,22 @@ from esphome.const import (
CONF_SWITCH_DATAPOINT,
CONF_SUPPORTS_COOL,
CONF_SUPPORTS_HEAT,
CONF_PRESET,
CONF_SWING_MODE,
CONF_FAN_MODE,
CONF_TEMPERATURE,
)
from .. import tuya_ns, CONF_TUYA_ID, Tuya
DEPENDENCIES = ["tuya"]
CODEOWNERS = ["@jesserockz"]
CONF_ACTIVE_STATE_DATAPOINT = "active_state_datapoint"
CONF_ACTIVE_STATE_HEATING_VALUE = "active_state_heating_value"
CONF_ACTIVE_STATE_COOLING_VALUE = "active_state_cooling_value"
CONF_ACTIVE_STATE = "active_state"
CONF_DATAPOINT = "datapoint"
CONF_HEATING_VALUE = "heating_value"
CONF_COOLING_VALUE = "cooling_value"
CONF_DRYING_VALUE = "drying_value"
CONF_FANONLY_VALUE = "fanonly_value"
CONF_HEATING_STATE_PIN = "heating_state_pin"
CONF_COOLING_STATE_PIN = "cooling_state_pin"
CONF_TARGET_TEMPERATURE_DATAPOINT = "target_temperature_datapoint"
@ -23,9 +30,17 @@ CONF_CURRENT_TEMPERATURE_DATAPOINT = "current_temperature_datapoint"
CONF_TEMPERATURE_MULTIPLIER = "temperature_multiplier"
CONF_CURRENT_TEMPERATURE_MULTIPLIER = "current_temperature_multiplier"
CONF_TARGET_TEMPERATURE_MULTIPLIER = "target_temperature_multiplier"
CONF_ECO_DATAPOINT = "eco_datapoint"
CONF_ECO_TEMPERATURE = "eco_temperature"
CONF_ECO = "eco"
CONF_SLEEP = "sleep"
CONF_SLEEP_DATAPOINT = "sleep_datapoint"
CONF_REPORTS_FAHRENHEIT = "reports_fahrenheit"
CONF_VERTICAL_DATAPOINT = "vertical_datapoint"
CONF_HORIZONTAL_DATAPOINT = "horizontal_datapoint"
CONF_LOW_VALUE = "low_value"
CONF_MEDIUM_VALUE = "medium_value"
CONF_MIDDLE_VALUE = "middle_value"
CONF_HIGH_VALUE = "high_value"
CONF_AUTO_VALUE = "auto_value"
TuyaClimate = tuya_ns.class_("TuyaClimate", climate.Climate, cg.Component)
@ -67,30 +82,73 @@ def validate_temperature_multipliers(value):
return value
def validate_active_state_values(value):
if CONF_ACTIVE_STATE_DATAPOINT not in value:
if CONF_ACTIVE_STATE_COOLING_VALUE in value:
def validate_cooling_values(value):
if CONF_SUPPORTS_COOL in value:
cooling_supported = value[CONF_SUPPORTS_COOL]
if not cooling_supported and CONF_ACTIVE_STATE in value:
active_state_config = value[CONF_ACTIVE_STATE]
if (
CONF_COOLING_VALUE in active_state_config
or CONF_COOLING_STATE_PIN in value
):
raise cv.Invalid(
f"{CONF_ACTIVE_STATE_DATAPOINT} required if using "
f"{CONF_ACTIVE_STATE_COOLING_VALUE}"
f"Device does not support cooling, but {CONF_COOLING_VALUE} or {CONF_COOLING_STATE_PIN} specified."
f" Please add '{CONF_SUPPORTS_COOL}: true' to your configuration."
)
else:
if value[CONF_SUPPORTS_COOL] and CONF_ACTIVE_STATE_COOLING_VALUE not in value:
elif cooling_supported and CONF_ACTIVE_STATE in value:
active_state_config = value[CONF_ACTIVE_STATE]
if (
CONF_COOLING_VALUE not in active_state_config
and CONF_COOLING_STATE_PIN not in value
):
raise cv.Invalid(
f"{CONF_ACTIVE_STATE_COOLING_VALUE} required if using "
f"{CONF_ACTIVE_STATE_DATAPOINT} and device supports cooling"
f"Either {CONF_ACTIVE_STATE} {CONF_COOLING_VALUE} or {CONF_COOLING_STATE_PIN} is required if"
f" {CONF_SUPPORTS_COOL}: true' is in your configuration."
)
return value
def validate_eco_values(value):
if CONF_ECO_TEMPERATURE in value and CONF_ECO_DATAPOINT not in value:
raise cv.Invalid(
f"{CONF_ECO_DATAPOINT} required if using {CONF_ECO_TEMPERATURE}"
ACTIVE_STATES = cv.Schema(
{
cv.Required(CONF_DATAPOINT): cv.uint8_t,
cv.Optional(CONF_HEATING_VALUE, default=1): cv.uint8_t,
cv.Optional(CONF_COOLING_VALUE): cv.uint8_t,
cv.Optional(CONF_DRYING_VALUE): cv.uint8_t,
cv.Optional(CONF_FANONLY_VALUE): cv.uint8_t,
},
)
return value
PRESETS = cv.Schema(
{
cv.Optional(CONF_ECO): {
cv.Required(CONF_DATAPOINT): cv.uint8_t,
cv.Optional(CONF_TEMPERATURE): cv.temperature,
},
cv.Optional(CONF_SLEEP): {
cv.Required(CONF_DATAPOINT): cv.uint8_t,
},
},
)
FAN_MODES = cv.Schema(
{
cv.Required(CONF_DATAPOINT): cv.uint8_t,
cv.Optional(CONF_AUTO_VALUE): cv.uint8_t,
cv.Optional(CONF_LOW_VALUE): cv.uint8_t,
cv.Optional(CONF_MEDIUM_VALUE): cv.uint8_t,
cv.Optional(CONF_MIDDLE_VALUE): cv.uint8_t,
cv.Optional(CONF_HIGH_VALUE): cv.uint8_t,
}
)
SWING_MODES = cv.Schema(
{
cv.Optional(CONF_VERTICAL_DATAPOINT): cv.uint8_t,
cv.Optional(CONF_HORIZONTAL_DATAPOINT): cv.uint8_t,
},
)
CONFIG_SCHEMA = cv.All(
climate.CLIMATE_SCHEMA.extend(
{
@ -99,9 +157,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean,
cv.Optional(CONF_SUPPORTS_COOL, default=False): cv.boolean,
cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t,
cv.Optional(CONF_ACTIVE_STATE_DATAPOINT): cv.uint8_t,
cv.Optional(CONF_ACTIVE_STATE_HEATING_VALUE, default=1): cv.uint8_t,
cv.Optional(CONF_ACTIVE_STATE_COOLING_VALUE): cv.uint8_t,
cv.Optional(CONF_ACTIVE_STATE): ACTIVE_STATES,
cv.Optional(CONF_HEATING_STATE_PIN): pins.gpio_input_pin_schema,
cv.Optional(CONF_COOLING_STATE_PIN): pins.gpio_input_pin_schema,
cv.Optional(CONF_TARGET_TEMPERATURE_DATAPOINT): cv.uint8_t,
@ -109,17 +165,32 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_TEMPERATURE_MULTIPLIER): cv.positive_float,
cv.Optional(CONF_CURRENT_TEMPERATURE_MULTIPLIER): cv.positive_float,
cv.Optional(CONF_TARGET_TEMPERATURE_MULTIPLIER): cv.positive_float,
cv.Optional(CONF_ECO_DATAPOINT): cv.uint8_t,
cv.Optional(CONF_ECO_TEMPERATURE): cv.temperature,
cv.Optional(CONF_REPORTS_FAHRENHEIT, default=False): cv.boolean,
cv.Optional(CONF_PRESET): PRESETS,
cv.Optional(CONF_FAN_MODE): FAN_MODES,
cv.Optional(CONF_SWING_MODE): SWING_MODES,
cv.Optional("active_state_datapoint"): cv.invalid(
"'active_state_datapoint' has been moved inside of the 'active_state' config block as 'datapoint'"
),
cv.Optional("active_state_heating_value"): cv.invalid(
"'active_state_heating_value' has been moved inside of the 'active_state' config block as 'heating_value'"
),
cv.Optional("active_state_cooling_value"): cv.invalid(
"'active_state_cooling_value' has been moved inside of the 'active_state' config block as 'cooling_value'"
),
cv.Optional("eco_datapoint"): cv.invalid(
"'eco_datapoint' has been moved inside of the 'eco' config block under 'preset' as 'datapoint'"
),
cv.Optional("eco_temperature"): cv.invalid(
"'eco_temperature' has been moved inside of the 'eco' config block under 'preset' as 'temperature'"
),
}
).extend(cv.COMPONENT_SCHEMA),
cv.has_at_least_one_key(CONF_TARGET_TEMPERATURE_DATAPOINT, CONF_SWITCH_DATAPOINT),
validate_temperature_multipliers,
validate_active_state_values,
cv.has_at_most_one_key(CONF_ACTIVE_STATE_DATAPOINT, CONF_HEATING_STATE_PIN),
cv.has_at_most_one_key(CONF_ACTIVE_STATE_DATAPOINT, CONF_COOLING_STATE_PIN),
validate_eco_values,
validate_cooling_values,
cv.has_at_most_one_key(CONF_ACTIVE_STATE, CONF_HEATING_STATE_PIN),
cv.has_at_most_one_key(CONF_ACTIVE_STATE, CONF_COOLING_STATE_PIN),
)
@ -133,61 +204,78 @@ async def to_code(config):
cg.add(var.set_supports_heat(config[CONF_SUPPORTS_HEAT]))
cg.add(var.set_supports_cool(config[CONF_SUPPORTS_COOL]))
if CONF_SWITCH_DATAPOINT in config:
cg.add(var.set_switch_id(config[CONF_SWITCH_DATAPOINT]))
if CONF_ACTIVE_STATE_DATAPOINT in config:
cg.add(var.set_active_state_id(config[CONF_ACTIVE_STATE_DATAPOINT]))
if CONF_ACTIVE_STATE_HEATING_VALUE in config:
cg.add(
var.set_active_state_heating_value(
config[CONF_ACTIVE_STATE_HEATING_VALUE]
)
)
if CONF_ACTIVE_STATE_COOLING_VALUE in config:
cg.add(
var.set_active_state_cooling_value(
config[CONF_ACTIVE_STATE_COOLING_VALUE]
)
)
if switch_datapoint := config.get(CONF_SWITCH_DATAPOINT):
cg.add(var.set_switch_id(switch_datapoint))
if active_state_config := config.get(CONF_ACTIVE_STATE):
cg.add(var.set_active_state_id(CONF_DATAPOINT))
if (heating_value := active_state_config.get(CONF_HEATING_VALUE)) is not None:
cg.add(var.set_active_state_heating_value(heating_value))
if (cooling_value := active_state_config.get(CONF_COOLING_VALUE)) is not None:
cg.add(var.set_active_state_cooling_value(cooling_value))
if (drying_value := active_state_config.get(CONF_DRYING_VALUE)) is not None:
cg.add(var.set_active_state_drying_value(drying_value))
if (fanonly_value := active_state_config.get(CONF_FANONLY_VALUE)) is not None:
cg.add(var.set_active_state_fanonly_value(fanonly_value))
else:
if CONF_HEATING_STATE_PIN in config:
if heating_state_pin_config := config.get(CONF_HEATING_STATE_PIN):
heating_state_pin = await cg.gpio_pin_expression(
config[CONF_HEATING_STATE_PIN]
config(heating_state_pin_config)
)
cg.add(var.set_heating_state_pin(heating_state_pin))
if CONF_COOLING_STATE_PIN in config:
if cooling_state_pin_config := config.get(CONF_COOLING_STATE_PIN):
cooling_state_pin = await cg.gpio_pin_expression(
config[CONF_COOLING_STATE_PIN]
config(cooling_state_pin_config)
)
cg.add(var.set_cooling_state_pin(cooling_state_pin))
if CONF_TARGET_TEMPERATURE_DATAPOINT in config:
cg.add(var.set_target_temperature_id(config[CONF_TARGET_TEMPERATURE_DATAPOINT]))
if CONF_CURRENT_TEMPERATURE_DATAPOINT in config:
cg.add(
var.set_current_temperature_id(config[CONF_CURRENT_TEMPERATURE_DATAPOINT])
)
if CONF_TEMPERATURE_MULTIPLIER in config:
cg.add(
var.set_target_temperature_multiplier(config[CONF_TEMPERATURE_MULTIPLIER])
)
cg.add(
var.set_current_temperature_multiplier(config[CONF_TEMPERATURE_MULTIPLIER])
)
if target_temperature_datapoint := config.get(CONF_TARGET_TEMPERATURE_DATAPOINT):
cg.add(var.set_target_temperature_id(target_temperature_datapoint))
if current_temperature_datapoint := config.get(CONF_CURRENT_TEMPERATURE_DATAPOINT):
cg.add(var.set_current_temperature_id(current_temperature_datapoint))
if temperature_multiplier := config.get(CONF_TEMPERATURE_MULTIPLIER):
cg.add(var.set_target_temperature_multiplier(temperature_multiplier))
cg.add(var.set_current_temperature_multiplier(temperature_multiplier))
else:
if current_temperature_multiplier := config.get(
CONF_CURRENT_TEMPERATURE_MULTIPLIER
):
cg.add(
var.set_current_temperature_multiplier(
config[CONF_CURRENT_TEMPERATURE_MULTIPLIER]
var.set_current_temperature_multiplier(current_temperature_multiplier)
)
)
cg.add(
var.set_target_temperature_multiplier(
config[CONF_TARGET_TEMPERATURE_MULTIPLIER]
)
)
if CONF_ECO_DATAPOINT in config:
cg.add(var.set_eco_id(config[CONF_ECO_DATAPOINT]))
if CONF_ECO_TEMPERATURE in config:
cg.add(var.set_eco_temperature(config[CONF_ECO_TEMPERATURE]))
if target_temperature_multiplier := config.get(
CONF_TARGET_TEMPERATURE_MULTIPLIER
):
cg.add(var.set_target_temperature_multiplier(target_temperature_multiplier))
if config[CONF_REPORTS_FAHRENHEIT]:
cg.add(var.set_reports_fahrenheit())
if preset_config := config.get(CONF_PRESET, {}):
if eco_config := preset_config.get(CONF_ECO, {}):
cg.add(var.set_eco_id(CONF_DATAPOINT))
if eco_temperature := eco_config.get(CONF_TEMPERATURE):
cg.add(var.set_eco_temperature(eco_temperature))
if CONF_SLEEP in preset_config:
cg.add(var.set_sleep_id(CONF_DATAPOINT))
if swing_mode_config := config.get(CONF_SWING_MODE):
if swing_vertical_datapoint := swing_mode_config.get(CONF_VERTICAL_DATAPOINT):
cg.add(var.set_swing_vertical_id(swing_vertical_datapoint))
if swing_horizontal_datapoint := swing_mode_config.get(
CONF_HORIZONTAL_DATAPOINT
):
cg.add(var.set_swing_horizontal_id(swing_horizontal_datapoint))
if fan_mode_config := config.get(CONF_FAN_MODE):
cg.add(var.set_fan_speed_id(CONF_DATAPOINT))
if (fan_auto_value := fan_mode_config.get(CONF_AUTO_VALUE)) is not None:
cg.add(var.set_fan_speed_auto_value(fan_auto_value))
if (fan_low_value := fan_mode_config.get(CONF_LOW_VALUE)) is not None:
cg.add(var.set_fan_speed_low_value(fan_low_value))
if (fan_medium_value := fan_mode_config.get(CONF_MEDIUM_VALUE)) is not None:
cg.add(var.set_fan_speed_medium_value(fan_medium_value))
if (fan_middle_value := fan_mode_config.get(CONF_MIDDLE_VALUE)) is not None:
cg.add(var.set_fan_speed_middle_value(fan_middle_value))
if (fan_high_value := fan_mode_config.get(CONF_HIGH_VALUE)) is not None:
cg.add(var.set_fan_speed_high_value(fan_high_value))

View file

@ -75,6 +75,41 @@ void TuyaClimate::setup() {
this->publish_state();
});
}
if (this->sleep_id_.has_value()) {
this->parent_->register_listener(*this->sleep_id_, [this](const TuyaDatapoint &datapoint) {
this->sleep_ = datapoint.value_bool;
ESP_LOGV(TAG, "MCU reported sleep is: %s", ONOFF(this->sleep_));
this->compute_preset_();
this->compute_target_temperature_();
this->publish_state();
});
}
if (this->swing_vertical_id_.has_value()) {
this->parent_->register_listener(*this->swing_vertical_id_, [this](const TuyaDatapoint &datapoint) {
this->swing_vertical_ = datapoint.value_bool;
ESP_LOGV(TAG, "MCU reported vertical swing is: %s", ONOFF(datapoint.value_bool));
this->compute_swingmode_();
this->publish_state();
});
}
if (this->swing_horizontal_id_.has_value()) {
this->parent_->register_listener(*this->swing_horizontal_id_, [this](const TuyaDatapoint &datapoint) {
this->swing_horizontal_ = datapoint.value_bool;
ESP_LOGV(TAG, "MCU reported horizontal swing is: %s", ONOFF(datapoint.value_bool));
this->compute_swingmode_();
this->publish_state();
});
}
if (this->fan_speed_id_.has_value()) {
this->parent_->register_listener(*this->fan_speed_id_, [this](const TuyaDatapoint &datapoint) {
ESP_LOGV(TAG, "MCU reported Fan Speed Mode is: %u", datapoint.value_enum);
this->fan_state_ = datapoint.value_enum;
this->compute_fanmode_();
this->publish_state();
});
}
}
void TuyaClimate::loop() {
@ -110,7 +145,21 @@ void TuyaClimate::control(const climate::ClimateCall &call) {
const bool switch_state = *call.get_mode() != climate::CLIMATE_MODE_OFF;
ESP_LOGV(TAG, "Setting switch: %s", ONOFF(switch_state));
this->parent_->set_boolean_datapoint_value(*this->switch_id_, switch_state);
const climate::ClimateMode new_mode = *call.get_mode();
if (new_mode == climate::CLIMATE_MODE_HEAT && this->supports_heat_) {
this->parent_->set_enum_datapoint_value(*this->active_state_id_, *this->active_state_heating_value_);
} else if (new_mode == climate::CLIMATE_MODE_COOL && this->supports_cool_) {
this->parent_->set_enum_datapoint_value(*this->active_state_id_, *this->active_state_cooling_value_);
} else if (new_mode == climate::CLIMATE_MODE_DRY && this->active_state_drying_value_.has_value()) {
this->parent_->set_enum_datapoint_value(*this->active_state_id_, *this->active_state_drying_value_);
} else if (new_mode == climate::CLIMATE_MODE_FAN_ONLY && this->active_state_fanonly_value_.has_value()) {
this->parent_->set_enum_datapoint_value(*this->active_state_id_, *this->active_state_fanonly_value_);
}
}
control_swing_mode_(call);
control_fan_mode_(call);
if (call.get_target_temperature().has_value()) {
float target_temperature = *call.get_target_temperature();
@ -129,6 +178,106 @@ void TuyaClimate::control(const climate::ClimateCall &call) {
ESP_LOGV(TAG, "Setting eco: %s", ONOFF(eco));
this->parent_->set_boolean_datapoint_value(*this->eco_id_, eco);
}
if (this->sleep_id_.has_value()) {
const bool sleep = preset == climate::CLIMATE_PRESET_SLEEP;
ESP_LOGV(TAG, "Setting sleep: %s", ONOFF(sleep));
this->parent_->set_boolean_datapoint_value(*this->sleep_id_, sleep);
}
}
}
void TuyaClimate::control_swing_mode_(const climate::ClimateCall &call) {
bool vertical_swing_changed = false;
bool horizontal_swing_changed = false;
if (call.get_swing_mode().has_value()) {
const auto swing_mode = *call.get_swing_mode();
switch (swing_mode) {
case climate::CLIMATE_SWING_OFF:
if (swing_vertical_ || swing_horizontal_) {
this->swing_vertical_ = false;
this->swing_horizontal_ = false;
vertical_swing_changed = true;
horizontal_swing_changed = true;
}
break;
case climate::CLIMATE_SWING_BOTH:
if (!swing_vertical_ || !swing_horizontal_) {
this->swing_vertical_ = true;
this->swing_horizontal_ = true;
vertical_swing_changed = true;
horizontal_swing_changed = true;
}
break;
case climate::CLIMATE_SWING_VERTICAL:
if (!swing_vertical_ || swing_horizontal_) {
this->swing_vertical_ = true;
this->swing_horizontal_ = false;
vertical_swing_changed = true;
horizontal_swing_changed = true;
}
break;
case climate::CLIMATE_SWING_HORIZONTAL:
if (swing_vertical_ || !swing_horizontal_) {
this->swing_vertical_ = false;
this->swing_horizontal_ = true;
vertical_swing_changed = true;
horizontal_swing_changed = true;
}
break;
default:
break;
}
}
if (vertical_swing_changed && this->swing_vertical_id_.has_value()) {
ESP_LOGV(TAG, "Setting vertical swing: %s", ONOFF(swing_vertical_));
this->parent_->set_boolean_datapoint_value(*this->swing_vertical_id_, swing_vertical_);
}
if (horizontal_swing_changed && this->swing_horizontal_id_.has_value()) {
ESP_LOGV(TAG, "Setting horizontal swing: %s", ONOFF(swing_horizontal_));
this->parent_->set_boolean_datapoint_value(*this->swing_horizontal_id_, swing_horizontal_);
}
// Publish the state after updating the swing mode
this->publish_state();
}
void TuyaClimate::control_fan_mode_(const climate::ClimateCall &call) {
if (call.get_fan_mode().has_value()) {
climate::ClimateFanMode fan_mode = *call.get_fan_mode();
uint8_t tuya_fan_speed;
switch (fan_mode) {
case climate::CLIMATE_FAN_LOW:
tuya_fan_speed = *fan_speed_low_value_;
break;
case climate::CLIMATE_FAN_MEDIUM:
tuya_fan_speed = *fan_speed_medium_value_;
break;
case climate::CLIMATE_FAN_MIDDLE:
tuya_fan_speed = *fan_speed_middle_value_;
break;
case climate::CLIMATE_FAN_HIGH:
tuya_fan_speed = *fan_speed_high_value_;
break;
case climate::CLIMATE_FAN_AUTO:
tuya_fan_speed = *fan_speed_auto_value_;
break;
default:
tuya_fan_speed = 0;
break;
}
if (this->fan_speed_id_.has_value()) {
this->parent_->set_enum_datapoint_value(*this->fan_speed_id_, tuya_fan_speed);
}
}
}
@ -140,10 +289,46 @@ climate::ClimateTraits TuyaClimate::traits() {
traits.add_supported_mode(climate::CLIMATE_MODE_HEAT);
if (supports_cool_)
traits.add_supported_mode(climate::CLIMATE_MODE_COOL);
if (this->active_state_drying_value_.has_value())
traits.add_supported_mode(climate::CLIMATE_MODE_DRY);
if (this->active_state_fanonly_value_.has_value())
traits.add_supported_mode(climate::CLIMATE_MODE_FAN_ONLY);
if (this->eco_id_.has_value()) {
traits.add_supported_preset(climate::CLIMATE_PRESET_NONE);
traits.add_supported_preset(climate::CLIMATE_PRESET_ECO);
}
if (this->sleep_id_.has_value()) {
traits.add_supported_preset(climate::CLIMATE_PRESET_SLEEP);
}
if (this->sleep_id_.has_value() || this->eco_id_.has_value()) {
traits.add_supported_preset(climate::CLIMATE_PRESET_NONE);
}
if (this->swing_vertical_id_.has_value() && this->swing_horizontal_id_.has_value()) {
std::set<climate::ClimateSwingMode> supported_swing_modes = {
climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH, climate::CLIMATE_SWING_VERTICAL,
climate::CLIMATE_SWING_HORIZONTAL};
traits.set_supported_swing_modes(std::move(supported_swing_modes));
} else if (this->swing_vertical_id_.has_value()) {
std::set<climate::ClimateSwingMode> supported_swing_modes = {climate::CLIMATE_SWING_OFF,
climate::CLIMATE_SWING_VERTICAL};
traits.set_supported_swing_modes(std::move(supported_swing_modes));
} else if (this->swing_horizontal_id_.has_value()) {
std::set<climate::ClimateSwingMode> supported_swing_modes = {climate::CLIMATE_SWING_OFF,
climate::CLIMATE_SWING_HORIZONTAL};
traits.set_supported_swing_modes(std::move(supported_swing_modes));
}
if (fan_speed_id_) {
if (fan_speed_low_value_)
traits.add_supported_fan_mode(climate::CLIMATE_FAN_LOW);
if (fan_speed_medium_value_)
traits.add_supported_fan_mode(climate::CLIMATE_FAN_MEDIUM);
if (fan_speed_middle_value_)
traits.add_supported_fan_mode(climate::CLIMATE_FAN_MIDDLE);
if (fan_speed_high_value_)
traits.add_supported_fan_mode(climate::CLIMATE_FAN_HIGH);
if (fan_speed_auto_value_)
traits.add_supported_fan_mode(climate::CLIMATE_FAN_AUTO);
}
return traits;
}
@ -166,16 +351,56 @@ void TuyaClimate::dump_config() {
if (this->eco_id_.has_value()) {
ESP_LOGCONFIG(TAG, " Eco has datapoint ID %u", *this->eco_id_);
}
if (this->sleep_id_.has_value()) {
ESP_LOGCONFIG(TAG, " Sleep has datapoint ID %u", *this->sleep_id_);
}
if (this->swing_vertical_id_.has_value()) {
ESP_LOGCONFIG(TAG, " Swing Vertical has datapoint ID %u", *this->swing_vertical_id_);
}
if (this->swing_horizontal_id_.has_value()) {
ESP_LOGCONFIG(TAG, " Swing Horizontal has datapoint ID %u", *this->swing_horizontal_id_);
}
}
void TuyaClimate::compute_preset_() {
if (this->eco_) {
this->preset = climate::CLIMATE_PRESET_ECO;
} else if (this->sleep_) {
this->preset = climate::CLIMATE_PRESET_SLEEP;
} else {
this->preset = climate::CLIMATE_PRESET_NONE;
}
}
void TuyaClimate::compute_swingmode_() {
if (this->swing_vertical_ && this->swing_horizontal_) {
this->swing_mode = climate::CLIMATE_SWING_BOTH;
} else if (this->swing_vertical_) {
this->swing_mode = climate::CLIMATE_SWING_VERTICAL;
} else if (this->swing_horizontal_) {
this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL;
} else {
this->swing_mode = climate::CLIMATE_SWING_OFF;
}
}
void TuyaClimate::compute_fanmode_() {
if (this->fan_speed_id_.has_value()) {
// Use state from MCU datapoint
if (this->fan_speed_auto_value_.has_value() && this->fan_state_ == this->fan_speed_auto_value_) {
this->fan_mode = climate::CLIMATE_FAN_AUTO;
} else if (this->fan_speed_high_value_.has_value() && this->fan_state_ == this->fan_speed_high_value_) {
this->fan_mode = climate::CLIMATE_FAN_HIGH;
} else if (this->fan_speed_medium_value_.has_value() && this->fan_state_ == this->fan_speed_medium_value_) {
this->fan_mode = climate::CLIMATE_FAN_MEDIUM;
} else if (this->fan_speed_middle_value_.has_value() && this->fan_state_ == this->fan_speed_middle_value_) {
this->fan_mode = climate::CLIMATE_FAN_MIDDLE;
} else if (this->fan_speed_low_value_.has_value() && this->fan_state_ == this->fan_speed_low_value_) {
this->fan_mode = climate::CLIMATE_FAN_LOW;
}
}
}
void TuyaClimate::compute_target_temperature_() {
if (this->eco_ && this->eco_temperature_.has_value()) {
this->target_temperature = *this->eco_temperature_;
@ -202,16 +427,28 @@ void TuyaClimate::compute_state_() {
if (this->supports_heat_ && this->active_state_heating_value_.has_value() &&
this->active_state_ == this->active_state_heating_value_) {
target_action = climate::CLIMATE_ACTION_HEATING;
this->mode = climate::CLIMATE_MODE_HEAT;
} else if (this->supports_cool_ && this->active_state_cooling_value_.has_value() &&
this->active_state_ == this->active_state_cooling_value_) {
target_action = climate::CLIMATE_ACTION_COOLING;
this->mode = climate::CLIMATE_MODE_COOL;
} else if (this->active_state_drying_value_.has_value() &&
this->active_state_ == this->active_state_drying_value_) {
target_action = climate::CLIMATE_ACTION_DRYING;
this->mode = climate::CLIMATE_MODE_DRY;
} else if (this->active_state_fanonly_value_.has_value() &&
this->active_state_ == this->active_state_fanonly_value_) {
target_action = climate::CLIMATE_ACTION_FAN;
this->mode = climate::CLIMATE_MODE_FAN_ONLY;
}
} else if (this->heating_state_pin_ != nullptr || this->cooling_state_pin_ != nullptr) {
// Use state from input pins
if (this->heating_state_) {
target_action = climate::CLIMATE_ACTION_HEATING;
this->mode = climate::CLIMATE_MODE_HEAT;
} else if (this->cooling_state_) {
target_action = climate::CLIMATE_ACTION_COOLING;
this->mode = climate::CLIMATE_MODE_COOL;
}
} else {
// Fallback to active state calc based on temp and hysteresis
@ -219,8 +456,10 @@ void TuyaClimate::compute_state_() {
if (std::abs(temp_diff) > this->hysteresis_) {
if (this->supports_heat_ && temp_diff > 0) {
target_action = climate::CLIMATE_ACTION_HEATING;
this->mode = climate::CLIMATE_MODE_HEAT;
} else if (this->supports_cool_ && temp_diff < 0) {
target_action = climate::CLIMATE_ACTION_COOLING;
this->mode = climate::CLIMATE_MODE_COOL;
}
}
}

View file

@ -18,8 +18,22 @@ class TuyaClimate : public climate::Climate, public Component {
void set_active_state_id(uint8_t state_id) { this->active_state_id_ = state_id; }
void set_active_state_heating_value(uint8_t value) { this->active_state_heating_value_ = value; }
void set_active_state_cooling_value(uint8_t value) { this->active_state_cooling_value_ = value; }
void set_active_state_drying_value(uint8_t value) { this->active_state_drying_value_ = value; }
void set_active_state_fanonly_value(uint8_t value) { this->active_state_fanonly_value_ = value; }
void set_heating_state_pin(GPIOPin *pin) { this->heating_state_pin_ = pin; }
void set_cooling_state_pin(GPIOPin *pin) { this->cooling_state_pin_ = pin; }
void set_swing_vertical_id(uint8_t swing_vertical_id) { this->swing_vertical_id_ = swing_vertical_id; }
void set_swing_horizontal_id(uint8_t swing_horizontal_id) { this->swing_horizontal_id_ = swing_horizontal_id; }
void set_fan_speed_id(uint8_t fan_speed_id) { this->fan_speed_id_ = fan_speed_id; }
void set_fan_speed_low_value(uint8_t fan_speed_low_value) { this->fan_speed_low_value_ = fan_speed_low_value; }
void set_fan_speed_medium_value(uint8_t fan_speed_medium_value) {
this->fan_speed_medium_value_ = fan_speed_medium_value;
}
void set_fan_speed_middle_value(uint8_t fan_speed_middle_value) {
this->fan_speed_middle_value_ = fan_speed_middle_value;
}
void set_fan_speed_high_value(uint8_t fan_speed_high_value) { this->fan_speed_high_value_ = fan_speed_high_value; }
void set_fan_speed_auto_value(uint8_t fan_speed_auto_value) { this->fan_speed_auto_value_ = fan_speed_auto_value; }
void set_target_temperature_id(uint8_t target_temperature_id) {
this->target_temperature_id_ = target_temperature_id;
}
@ -34,6 +48,7 @@ class TuyaClimate : public climate::Climate, public Component {
}
void set_eco_id(uint8_t eco_id) { this->eco_id_ = eco_id; }
void set_eco_temperature(float eco_temperature) { this->eco_temperature_ = eco_temperature; }
void set_sleep_id(uint8_t sleep_id) { this->sleep_id_ = sleep_id; }
void set_reports_fahrenheit() { this->reports_fahrenheit_ = true; }
@ -43,6 +58,12 @@ class TuyaClimate : public climate::Climate, public Component {
/// Override control to change settings of the climate device.
void control(const climate::ClimateCall &call) override;
/// Override control to change settings of swing mode.
void control_swing_mode_(const climate::ClimateCall &call);
/// Override control to change settings of fan mode.
void control_fan_mode_(const climate::ClimateCall &call);
/// Return the traits of this controller.
climate::ClimateTraits traits() override;
@ -55,6 +76,12 @@ class TuyaClimate : public climate::Climate, public Component {
/// Re-compute the state of this climate controller.
void compute_state_();
/// Re-Compute the swing mode of this climate controller.
void compute_swingmode_();
/// Re-Compute the fan mode of this climate controller.
void compute_fanmode_();
/// Switch the climate device to the given climate mode.
void switch_to_action_(climate::ClimateAction action);
@ -65,6 +92,8 @@ class TuyaClimate : public climate::Climate, public Component {
optional<uint8_t> active_state_id_{};
optional<uint8_t> active_state_heating_value_{};
optional<uint8_t> active_state_cooling_value_{};
optional<uint8_t> active_state_drying_value_{};
optional<uint8_t> active_state_fanonly_value_{};
GPIOPin *heating_state_pin_{nullptr};
GPIOPin *cooling_state_pin_{nullptr};
optional<uint8_t> target_temperature_id_{};
@ -73,12 +102,25 @@ class TuyaClimate : public climate::Climate, public Component {
float target_temperature_multiplier_{1.0f};
float hysteresis_{1.0f};
optional<uint8_t> eco_id_{};
optional<uint8_t> sleep_id_{};
optional<float> eco_temperature_{};
uint8_t active_state_;
uint8_t fan_state_;
optional<uint8_t> swing_vertical_id_{};
optional<uint8_t> swing_horizontal_id_{};
optional<uint8_t> fan_speed_id_{};
optional<uint8_t> fan_speed_low_value_{};
optional<uint8_t> fan_speed_medium_value_{};
optional<uint8_t> fan_speed_middle_value_{};
optional<uint8_t> fan_speed_high_value_{};
optional<uint8_t> fan_speed_auto_value_{};
bool swing_vertical_{false};
bool swing_horizontal_{false};
bool heating_state_{false};
bool cooling_state_{false};
float manual_temperature_;
bool eco_;
bool sleep_;
bool reports_fahrenheit_{false};
};

View file

@ -61,9 +61,11 @@ void UponorSmatrixComponent::loop() {
// Send packets during bus silence
if ((now - this->last_rx_ > 300) && (now - this->last_poll_start_ < 9500) && (now - this->last_tx_ > 200)) {
#ifdef USE_TIME
// Only build time packet when bus is silent and queue is empty to make sure we can send it right away
if (this->send_time_requested_ && this->tx_queue_.empty() && this->do_send_time_())
this->send_time_requested_ = false;
#endif
// Send the next packet in the queue
if (!this->tx_queue_.empty()) {
auto packet = std::move(this->tx_queue_.front());
@ -171,7 +173,9 @@ bool UponorSmatrixComponent::send(uint16_t device_address, const UponorSmatrixDa
return false;
// Assemble packet for send queue. All fields are big-endian except for the little-endian checksum.
std::vector<uint8_t> packet(6 + 3 * data_len);
std::vector<uint8_t> packet;
packet.reserve(6 + 3 * data_len);
packet.push_back(this->address_ >> 8);
packet.push_back(this->address_ >> 0);
packet.push_back(device_address >> 8);

View file

@ -4,6 +4,8 @@
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/core/defines.h"
#ifdef USE_TIME
#include "esphome/components/time/real_time_clock.h"
#include "esphome/core/time.h"

View file

@ -44,6 +44,11 @@ def default_url(config):
config[CONF_CSS_URL] = ""
if not (CONF_JS_URL in config):
config[CONF_JS_URL] = "https://oi.esphome.io/v2/www.js"
if config[CONF_VERSION] == 3:
if not (CONF_CSS_URL in config):
config[CONF_CSS_URL] = ""
if not (CONF_JS_URL in config):
config[CONF_JS_URL] = "https://oi.esphome.io/v3/www.js"
return config
@ -64,7 +69,7 @@ CONFIG_SCHEMA = cv.All(
{
cv.GenerateID(): cv.declare_id(WebServer),
cv.Optional(CONF_PORT, default=80): cv.port,
cv.Optional(CONF_VERSION, default=2): cv.one_of(1, 2, int=True),
cv.Optional(CONF_VERSION, default=2): cv.one_of(1, 2, 3, int=True),
cv.Optional(CONF_CSS_URL): cv.string,
cv.Optional(CONF_CSS_INCLUDE): cv.file_,
cv.Optional(CONF_JS_URL): cv.string,
@ -152,7 +157,7 @@ async def to_code(config):
cg.add_define("USE_WEBSERVER")
cg.add_define("USE_WEBSERVER_PORT", config[CONF_PORT])
cg.add_define("USE_WEBSERVER_VERSION", version)
if version == 2:
if version >= 2:
# Don't compress the index HTML as the data sizes are almost the same.
add_resource_as_progmem("INDEX_HTML", build_index_html(config), compress=False)
else:

View file

@ -358,7 +358,7 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) {
stream->print(F("</article></body></html>"));
request->send(stream);
}
#elif USE_WEBSERVER_VERSION == 2
#elif USE_WEBSERVER_VERSION >= 2
void WebServer::handle_index_request(AsyncWebServerRequest *request) {
AsyncWebServerResponse *response =
request->beginResponse_P(200, "text/html", ESPHOME_WEBSERVER_INDEX_HTML, ESPHOME_WEBSERVER_INDEX_HTML_SIZE);
@ -486,7 +486,7 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM
if (obj->get_object_id() != match.id)
continue;
if (request->method() == HTTP_GET) {
if (request->method() == HTTP_GET && match.method.empty()) {
std::string data = this->switch_json(obj, obj->state, DETAIL_STATE);
request->send(200, "application/json", data.c_str());
} else if (match.method == "toggle") {
@ -517,7 +517,7 @@ void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlM
for (button::Button *obj : App.get_buttons()) {
if (obj->get_object_id() != match.id)
continue;
if (request->method() == HTTP_POST && match.method == "press") {
if (match.method == "press") {
this->schedule_([obj]() { obj->press(); });
request->send(200);
return;
@ -572,7 +572,7 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc
if (obj->get_object_id() != match.id)
continue;
if (request->method() == HTTP_GET) {
if (request->method() == HTTP_GET && match.method.empty()) {
std::string data = this->fan_json(obj, DETAIL_STATE);
request->send(200, "application/json", data.c_str());
} else if (match.method == "toggle") {
@ -630,7 +630,7 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa
if (obj->get_object_id() != match.id)
continue;
if (request->method() == HTTP_GET) {
if (request->method() == HTTP_GET && match.method.empty()) {
std::string data = this->light_json(obj, DETAIL_STATE);
request->send(200, "application/json", data.c_str());
} else if (match.method == "toggle") {
@ -736,7 +736,7 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa
if (obj->get_object_id() != match.id)
continue;
if (request->method() == HTTP_GET) {
if (request->method() == HTTP_GET && match.method.empty()) {
std::string data = this->cover_json(obj, DETAIL_STATE);
request->send(200, "application/json", data.c_str());
continue;
@ -805,7 +805,7 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM
if (obj->get_object_id() != match.id)
continue;
if (request->method() == HTTP_GET) {
if (request->method() == HTTP_GET && match.method.empty()) {
std::string data = this->number_json(obj, obj->state, DETAIL_STATE);
request->send(200, "application/json", data.c_str());
return;
@ -910,7 +910,7 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat
if (obj->get_object_id() != match.id)
continue;
if (request->method() == HTTP_GET) {
if (request->method() == HTTP_GET && match.method.empty()) {
std::string data = this->text_json(obj, obj->state, DETAIL_STATE);
request->send(200, "text/json", data.c_str());
return;
@ -961,7 +961,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM
if (obj->get_object_id() != match.id)
continue;
if (request->method() == HTTP_GET) {
if (request->method() == HTTP_GET && match.method.empty()) {
auto detail = DETAIL_STATE;
auto *param = request->getParam("detail");
if (param && param->value() == "all") {
@ -1016,7 +1016,7 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url
if (obj->get_object_id() != match.id)
continue;
if (request->method() == HTTP_GET) {
if (request->method() == HTTP_GET && match.method.empty()) {
std::string data = this->climate_json(obj, DETAIL_STATE);
request->send(200, "application/json", data.c_str());
return;
@ -1162,7 +1162,7 @@ void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMat
if (obj->get_object_id() != match.id)
continue;
if (request->method() == HTTP_GET) {
if (request->method() == HTTP_GET && match.method.empty()) {
std::string data = this->lock_json(obj, obj->state, DETAIL_STATE);
request->send(200, "application/json", data.c_str());
} else if (match.method == "lock") {
@ -1201,7 +1201,7 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques
if (obj->get_object_id() != match.id)
continue;
if (request->method() == HTTP_GET) {
if (request->method() == HTTP_GET && match.method.empty()) {
std::string data = this->alarm_control_panel_json(obj, obj->get_state(), DETAIL_STATE);
request->send(200, "application/json", data.c_str());
return;
@ -1251,7 +1251,7 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) {
#endif
#ifdef USE_BUTTON
if (request->method() == HTTP_POST && match.domain == "button")
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "button")
return true;
#endif

View file

@ -13,7 +13,7 @@
#include <freertos/semphr.h>
#endif
#if USE_WEBSERVER_VERSION == 2
#if USE_WEBSERVER_VERSION >= 2
extern const uint8_t ESPHOME_WEBSERVER_INDEX_HTML[] PROGMEM;
extern const size_t ESPHOME_WEBSERVER_INDEX_HTML_SIZE;
#endif

View file

@ -157,7 +157,7 @@ network::IPAddresses WiFiComponent::wifi_sta_ip_addresses() {
} else {
addresses[0] = network::IPAddress(&ip.ip);
}
#if LWIP_IPV6
#if USE_NETWORK_IPV6
ip6_addr_t ipv6;
err = tcpip_adapter_get_ip6_global(TCPIP_ADAPTER_IF_STA, &ipv6);
if (err != ESP_OK) {
@ -171,7 +171,7 @@ network::IPAddresses WiFiComponent::wifi_sta_ip_addresses() {
} else {
addresses[2] = network::IPAddress(&ipv6);
}
#endif /* LWIP_IPV6 */
#endif /* USE_NETWORK_IPV6 */
return addresses;
}

View file

@ -21,10 +21,14 @@ extern "C" {
#include <AddrList.h>
#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0)
#include "LwipDhcpServer.h"
#if USE_ARDUINO_VERSION_CODE < VERSION_CODE(3, 1, 0)
#include <ESP8266WiFi.h>
#include "ESP8266WiFiAP.h"
#define wifi_softap_set_dhcps_lease(lease) dhcpSoftAP.set_dhcps_lease(lease)
#define wifi_softap_set_dhcps_lease_time(time) dhcpSoftAP.set_dhcps_lease_time(time)
#define wifi_softap_set_dhcps_offer_option(offer, mode) dhcpSoftAP.set_dhcps_offer_option(offer, mode)
#endif
#endif
}
#include "esphome/core/helpers.h"
@ -721,7 +725,7 @@ bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
return false;
}
#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0)
#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0) && USE_ARDUINO_VERSION_CODE < VERSION_CODE(3, 1, 0)
dhcpSoftAP.begin(&info);
#endif
@ -745,12 +749,16 @@ bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
return false;
}
#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 1, 0)
ESP8266WiFiClass::softAPDhcpServer().setRouter(true); // send ROUTER option with netif's gateway IP
#else
uint8_t mode = 1;
// bit0, 1 enables router information from ESP8266 SoftAP DHCP server.
if (!wifi_softap_set_dhcps_offer_option(OFFER_ROUTER, &mode)) {
ESP_LOGV(TAG, "wifi_softap_set_dhcps_offer_option failed!");
return false;
}
#endif
if (!wifi_softap_dhcps_start()) {
ESP_LOGV(TAG, "Starting SoftAP DHCPS failed!");

View file

@ -1,6 +1,6 @@
"""Constants used by esphome."""
__version__ = "2024.3.0-dev"
__version__ = "2024.4.0-dev"
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
VALID_SUBSTITUTIONS_CHARACTERS = (
@ -856,6 +856,7 @@ CONF_VISUAL = "visual"
CONF_VOLTAGE = "voltage"
CONF_VOLTAGE_ATTENUATION = "voltage_attenuation"
CONF_VOLTAGE_DIVIDER = "voltage_divider"
CONF_VOLUME = "volume"
CONF_WAIT_TIME = "wait_time"
CONF_WAIT_UNTIL = "wait_until"
CONF_WAKEUP_PIN = "wakeup_pin"

View file

@ -5,6 +5,7 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <utility>
#include <cinttypes>
namespace esphome {
@ -140,18 +141,35 @@ bool Component::is_ready() {
(this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_SETUP;
}
bool Component::can_proceed() { return true; }
bool Component::status_has_warning() { return this->component_state_ & STATUS_LED_WARNING; }
bool Component::status_has_error() { return this->component_state_ & STATUS_LED_ERROR; }
void Component::status_set_warning() {
bool Component::status_has_warning() const { return this->component_state_ & STATUS_LED_WARNING; }
bool Component::status_has_error() const { return this->component_state_ & STATUS_LED_ERROR; }
void Component::status_set_warning(const char *message) {
// Don't spam the log. This risks missing different warning messages though.
if ((this->component_state_ & STATUS_LED_WARNING) != 0)
return;
this->component_state_ |= STATUS_LED_WARNING;
App.app_state_ |= STATUS_LED_WARNING;
ESP_LOGW(this->get_component_source(), "Warning set: %s", message);
}
void Component::status_set_error() {
void Component::status_set_error(const char *message) {
if ((this->component_state_ & STATUS_LED_ERROR) != 0)
return;
this->component_state_ |= STATUS_LED_ERROR;
App.app_state_ |= STATUS_LED_ERROR;
ESP_LOGE(this->get_component_source(), "Error set: %s", message);
}
void Component::status_clear_warning() {
if ((this->component_state_ & STATUS_LED_WARNING) == 0)
return;
this->component_state_ &= ~STATUS_LED_WARNING;
ESP_LOGW(this->get_component_source(), "Warning cleared");
}
void Component::status_clear_error() {
if ((this->component_state_ & STATUS_LED_ERROR) == 0)
return;
this->component_state_ &= ~STATUS_LED_ERROR;
ESP_LOGE(this->get_component_source(), "Error cleared");
}
void Component::status_clear_warning() { this->component_state_ &= ~STATUS_LED_WARNING; }
void Component::status_clear_error() { this->component_state_ &= ~STATUS_LED_ERROR; }
void Component::status_momentary_warning(const std::string &name, uint32_t length) {
this->status_set_warning();
this->set_timeout(name, length, [this]() { this->status_clear_warning(); });
@ -211,8 +229,8 @@ WarnIfComponentBlockingGuard::~WarnIfComponentBlockingGuard() {
uint32_t now = millis();
if (now - started_ > 50) {
const char *src = component_ == nullptr ? "<null>" : component_->get_component_source();
ESP_LOGW(TAG, "Component %s took a long time for an operation (%.2f s).", src, (now - started_) / 1e3f);
ESP_LOGW(TAG, "Components should block for at most 20-30ms.");
ESP_LOGW(TAG, "Component %s took a long time for an operation (%" PRIu32 " ms).", src, (now - started_));
ESP_LOGW(TAG, "Components should block for at most 30 ms.");
;
}
}

View file

@ -124,13 +124,13 @@ class Component {
virtual bool can_proceed();
bool status_has_warning();
bool status_has_warning() const;
bool status_has_error();
bool status_has_error() const;
void status_set_warning();
void status_set_warning(const char *message = "unspecified");
void status_set_error();
void status_set_error(const char *message = "unspecified");
void status_clear_warning();

View file

@ -98,7 +98,7 @@
// ESP8266-specific feature flags
#ifdef USE_ESP8266
#define USE_ADC_SENSOR_VCC
#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 0, 2)
#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 1, 2)
#define USE_ESP8266_PREFERENCES_FLASH
#define USE_HTTP_REQUEST_ESP8266_HTTPS
#define USE_SOCKET_IMPL_LWIP_TCP

View file

@ -12,9 +12,11 @@
#include <cstring>
#ifdef USE_HOST
#ifndef _WIN32
#include <net/if.h>
#include <netinet/in.h>
#include <sys/ioctl.h>
#endif
#include <unistd.h>
#endif
#if defined(USE_ESP8266)

View file

@ -516,7 +516,8 @@ class ImportRequestHandler(BaseHandler):
self.set_status(500)
self.write("File already exists")
return
except ValueError:
except ValueError as e:
_LOGGER.error(e)
self.set_status(422)
self.write("Invalid package url")
return
@ -687,6 +688,11 @@ class MainRequestHandler(BaseHandler):
@authenticated
def get(self) -> None:
begin = bool(self.get_argument("begin", False))
if settings.using_password:
# Simply accessing the xsrf_token sets the cookie for us
self.xsrf_token # pylint: disable=pointless-statement
else:
self.clear_cookie("_xsrf")
self.render(
"index.template.html",
@ -1101,6 +1107,7 @@ def make_app(debug=get_bool_env(ENV_DEV)) -> tornado.web.Application:
"log_function": log_function,
"websocket_ping_interval": 30.0,
"template_path": get_base_frontend_path(),
"xsrf_cookies": settings.using_password,
}
rel = settings.relative_url
return tornado.web.Application(

View file

@ -33,7 +33,9 @@ def has_remote_file_changed(url, local_file_path):
IF_MODIFIED_SINCE: local_modification_time_str,
CACHE_CONTROL: CACHE_CONTROL_MAX_AGE + "3600",
}
response = requests.head(url, headers=headers, timeout=NETWORK_TIMEOUT)
response = requests.head(
url, headers=headers, timeout=NETWORK_TIMEOUT, allow_redirects=True
)
_LOGGER.debug(
"has_remote_file_changed: File %s, Local modified %s, response code %d",

View file

@ -311,10 +311,18 @@ def gpio_base_schema(
map(lambda m: (cv.Optional(m, default=mode_default), cv.boolean), modes)
)
def _number_validator(value):
if isinstance(value, str) and value.upper().startswith("GPIOX"):
raise cv.Invalid(
f"Found placeholder '{value}' when expecting a GPIO pin number.\n"
"You must replace this with an actual pin number."
)
return number_validator(value)
schema = cv.Schema(
{
cv.GenerateID(): cv.declare_id(pin_type),
cv.Required(CONF_NUMBER): number_validator,
cv.Required(CONF_NUMBER): _number_validator,
cv.Optional(CONF_ALLOW_OTHER_USES): cv.boolean,
cv.Optional(CONF_MODE, default={}): cv.All(mode_dict, mode_validator),
}

View file

@ -81,9 +81,9 @@ build_flags =
; This are common settings for the ESP8266 using Arduino.
[common:esp8266-arduino]
extends = common:arduino
platform = platformio/espressif8266@3.2.0
platform = platformio/espressif8266@4.2.1
platform_packages =
platformio/framework-arduinoespressif8266@~3.30002.0
platformio/framework-arduinoespressif8266@~3.30102.0
framework = arduino
lib_deps =

View file

@ -12,10 +12,11 @@ pyserial==3.5
platformio==6.1.13 # When updating platformio, also update Dockerfile
esptool==4.7.0
click==8.1.7
esphome-dashboard==20231107.0
aioesphomeapi==23.1.1
esphome-dashboard==20240319.0
aioesphomeapi==23.2.0
zeroconf==0.131.0
python-magic==0.4.27
ruamel.yaml==0.18.6 # dashboard_import
# esp-idf requires this, but doesn't bundle it by default
# https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24

View file

@ -8,6 +8,6 @@ pre-commit
pytest==8.1.1
pytest-cov==4.1.0
pytest-mock==3.12.0
pytest-asyncio==0.23.5.post1
pytest-asyncio==0.23.6
asyncmock==0.4.2
hypothesis==6.92.1

View file

@ -70,11 +70,11 @@ def splitlines_no_ends(string):
return [s.strip() for s in string.splitlines()]
def changed_files():
def changed_files(branch="dev"):
check_remotes = ["upstream", "origin"]
check_remotes.extend(splitlines_no_ends(get_output("git", "remote")))
for remote in check_remotes:
command = ["git", "merge-base", f"refs/remotes/{remote}/dev", "HEAD"]
command = ["git", "merge-base", f"refs/remotes/{remote}/{branch}", "HEAD"]
try:
merge_base = splitlines_no_ends(get_output(*command))[0]
break

View file

@ -120,12 +120,21 @@ def main():
parser.add_argument(
"-c", "--changed", action="store_true", help="Only run on changed files"
)
parser.add_argument(
"-b", "--branch", help="Branch to compare changed files against"
)
args = parser.parse_args()
if args.branch and not args.changed:
parser.error("--branch requires --changed")
files = git_ls_files()
files = filter(filter_component_files, files)
if args.changed:
if args.branch:
changed = changed_files(args.branch)
else:
changed = changed_files()
files = [f for f in files if f in changed]

View file

@ -1,3 +1,16 @@
esphome:
on_boot:
then:
- homeassistant.event:
event: esphome.button_pressed
data:
message: Button was pressed
- homeassistant.service:
service: notify.html5
data:
message: Button was pressed
- homeassistant.tag_scanned: pulse
wifi:
ssid: MySSID
password: password1

View file

@ -1,3 +1,16 @@
esphome:
on_boot:
then:
- homeassistant.event:
event: esphome.button_pressed
data:
message: Button was pressed
- homeassistant.service:
service: notify.html5
data:
message: Button was pressed
- homeassistant.tag_scanned: pulse
wifi:
ssid: MySSID
password: password1

View file

@ -1,3 +1,16 @@
esphome:
on_boot:
then:
- homeassistant.event:
event: esphome.button_pressed
data:
message: Button was pressed
- homeassistant.service:
service: notify.html5
data:
message: Button was pressed
- homeassistant.tag_scanned: pulse
wifi:
ssid: MySSID
password: password1

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