1
0
mirror of https://github.com/esphome/esphome.git synced 2025-04-15 07:10:33 +01:00

Merge remote-tracking branch 'upstream/dev' into bh1745

This commit is contained in:
Anton Viktorov 2024-04-07 11:44:26 +02:00
commit fdec3c7156
758 changed files with 19000 additions and 693 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

@ -17,12 +17,12 @@ runs:
steps:
- name: Set up Python ${{ inputs.python-version }}
id: python
uses: actions/setup-python@v5.0.0
uses: actions/setup-python@v5.1.0
with:
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

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

@ -0,0 +1,80 @@
name: API Proto CI
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.1.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

@ -2,7 +2,7 @@
name: CI for docker images
# Only run when docker paths change
# yamllint disable-line rule:truthy
on:
push:
branches: [dev, beta, release]
@ -42,11 +42,11 @@ jobs:
steps:
- uses: actions/checkout@v4.1.1
- name: Set up Python
uses: actions/setup-python@v5.0.0
uses: actions/setup-python@v5.1.0
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

@ -1,7 +1,6 @@
---
name: CI
# yamllint disable-line rule:truthy
on:
push:
branches: [dev, beta, release]
@ -42,12 +41,12 @@ jobs:
run: echo key="${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}" >> $GITHUB_OUTPUT
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.0.0
uses: actions/setup-python@v5.1.0
with:
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 +366,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 +397,7 @@ jobs:
runs-on: ubuntu-latest
needs:
- common
if: github.event_name == 'pull_request'
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
@ -406,10 +406,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 +423,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 +431,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

@ -1,7 +1,6 @@
---
name: Lock
# yamllint disable-line rule:truthy
on:
schedule:
- cron: "30 0 * * *"

View File

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

View File

@ -1,7 +1,6 @@
---
name: Publish Release
# yamllint disable-line rule:truthy
on:
workflow_dispatch:
release:
@ -45,7 +44,7 @@ jobs:
steps:
- uses: actions/checkout@v4.1.1
- name: Set up Python
uses: actions/setup-python@v5.0.0
uses: actions/setup-python@v5.1.0
with:
python-version: "3.x"
- name: Set up python environment
@ -80,23 +79,23 @@ jobs:
steps:
- uses: actions/checkout@v4.1.1
- name: Set up Python
uses: actions/setup-python@v5.0.0
uses: actions/setup-python@v5.1.0
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
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 +162,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

@ -1,7 +1,6 @@
---
name: Stale
# yamllint disable-line rule:truthy
on:
schedule:
- cron: "30 0 * * *"

View File

@ -1,7 +1,6 @@
---
name: Synchronise Device Classes from Home Assistant
# yamllint disable-line rule:truthy
on:
workflow_dispatch:
schedule:
@ -23,7 +22,7 @@ jobs:
path: lib/home-assistant
- name: Setup Python
uses: actions/setup-python@v5.0.0
uses: actions/setup-python@v5.1.0
with:
python-version: 3.11
@ -37,7 +36,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

@ -1,7 +1,6 @@
---
name: YAML lint
# yamllint disable-line rule:truthy
on:
push:
branches: [dev, beta, release]

View File

@ -16,3 +16,4 @@ rules:
indent-sequences: true
check-multi-line-strings: false
line-length: disable
truthy: disable

View File

@ -42,6 +42,7 @@ esphome/components/as5600/* @ammmze
esphome/components/as5600/sensor/* @ammmze
esphome/components/as7341/* @mrgnr
esphome/components/async_tcp/* @OttoWinter
esphome/components/at581x/* @X-Ryl669
esphome/components/atc_mithermometer/* @ahpohl
esphome/components/atm90e26/* @danieltwagner
esphome/components/b_parasite/* @rbaron
@ -87,6 +88,7 @@ esphome/components/cst816/* @clydebarrow
esphome/components/ct_clamp/* @jesserockz
esphome/components/current_based/* @djwmarcx
esphome/components/dac7678/* @NickB1
esphome/components/daikin_arc/* @MagicBear
esphome/components/daikin_brc/* @hagak
esphome/components/daly_bms/* @s1lvi0
esphome/components/dashboard_import/* @esphome/core
@ -173,6 +175,7 @@ esphome/components/inkplate6/* @jesserockz
esphome/components/integration/* @OttoWinter
esphome/components/internal_temperature/* @Mat931
esphome/components/interval/* @esphome/core
esphome/components/jsn_sr04t/* @Mafus1
esphome/components/json/* @OttoWinter
esphome/components/kamstrup_kmp/* @cfeenstra1024
esphome/components/key_collector/* @ssieb
@ -307,6 +310,7 @@ esphome/components/sfa30/* @ghsensdev
esphome/components/sgp40/* @SenexCrenshaw
esphome/components/sgp4x/* @SenexCrenshaw @martgras
esphome/components/shelly_dimmer/* @edge90 @rnauber
esphome/components/sht3xd/* @mrtoy-me
esphome/components/sht4x/* @sjtrny
esphome/components/shutdown/* @esphome/core @jsuanet
esphome/components/sigma_delta_output/* @Cat-Ion
@ -346,6 +350,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

@ -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
@ -41,19 +40,18 @@ void AHT10Component::setup() {
}
delay(AHT10_SOFTRESET_DELAY);
const uint8_t *init_cmd;
i2c::ErrorCode error_code = i2c::ERROR_INVALID_ARGUMENT;
switch (this->variant_) {
case AHT10Variant::AHT20:
init_cmd = AHT20_INITIALIZE_CMD;
ESP_LOGCONFIG(TAG, "Setting up AHT20");
error_code = this->write(AHT20_INITIALIZE_CMD, sizeof(AHT20_INITIALIZE_CMD));
break;
case AHT10Variant::AHT10:
default:
init_cmd = AHT10_INITIALIZE_CMD;
ESP_LOGCONFIG(TAG, "Setting up AHT10");
error_code = this->write(AHT10_INITIALIZE_CMD, sizeof(AHT10_INITIALIZE_CMD));
break;
}
if (this->write(init_cmd, sizeof(init_cmd)) != i2c::ERROR_OK) {
if (error_code != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Communication with AHT10 failed!");
this->mark_failed();
return;
@ -83,74 +81,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(data, 6) != i2c::ERROR_OK) {
ESP_LOGD(TAG, "Communication with AHT10 failed, waiting...");
continue;
}
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) {
// Unrealistic humidity (0x0)
if (this->humidity_sensor_ == nullptr) {
ESP_LOGVV(TAG, "ATH10 Unrealistic humidity (0x0), but humidity is not required");
break;
} 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();
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();
if (this->read_count_ > 1)
ESP_LOGD(TAG, "Read attempt %d at %ums", this->read_count_, (unsigned) (millis() - this->start_time_));
if (this->read(data, 6) != i2c::ERROR_OK) {
this->status_set_warning("AHT10 read failed, retrying soon");
this->restart_read_();
return;
}
if ((data[0] & 0x80) == 0x80) { // Bit[7] = 0b1, device is busy
ESP_LOGD(TAG, "AHT10 is busy, waiting...");
this->restart_read_();
return;
}
if (data[1] == 0x0 && data[2] == 0x0 && (data[3] >> 4) == 0x0) {
// Unrealistic humidity (0x0)
if (this->humidity_sensor_ == nullptr) {
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) {
this->status_set_warning("Communication with AHT10 failed!");
}
this->restart_read_();
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;
float temperature = ((200.0f * (float) raw_temperature) / 1048576.0f) - 50.0f;
float humidity;
if (raw_humidity == 0) { // unrealistic value
humidity = NAN;
} else {
humidity = (float) raw_humidity * 100.0f / 1048576.0f;
}
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 (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

@ -0,0 +1,224 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import automation, core
from esphome.components import i2c
from esphome.automation import maybe_simple_id
from esphome.const import (
CONF_ID,
CONF_FREQUENCY,
)
CODEOWNERS = ["@X-Ryl669"]
DEPENDENCIES = ["i2c"]
MULTI_CONF = True
at581x_ns = cg.esphome_ns.namespace("at581x")
AT581XComponent = at581x_ns.class_("AT581XComponent", cg.Component, i2c.I2CDevice)
CONF_AT581X_ID = "at581x_id"
CONF_SENSING_DISTANCE = "sensing_distance"
CONF_SENSITIVITY = "sensitivity"
CONF_POWERON_SELFCHECK_TIME = "poweron_selfcheck_time"
CONF_PROTECT_TIME = "protect_time"
CONF_TRIGGER_BASE = "trigger_base"
CONF_TRIGGER_KEEP = "trigger_keep"
CONF_STAGE_GAIN = "stage_gain"
CONF_POWER_CONSUMPTION = "power_consumption"
CONF_HW_FRONTEND_RESET = "hw_frontend_reset"
RADAR_ALLOWED_FREQ = [
5696e6,
5715e6,
5730e6,
5748e6,
5765e6,
5784e6,
5800e6,
5819e6,
5836e6,
5851e6,
5869e6,
5888e6,
]
RADAR_ALLOWED_CUR_CONSUMPTION = [
48e-6,
56e-6,
63e-6,
70e-6,
77e-6,
91e-6,
105e-6,
115e-6,
40e-6,
44e-6,
47e-6,
51e-6,
54e-6,
61e-6,
68e-6,
78e-6,
]
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(AT581XComponent),
}
)
CONFIG_SCHEMA = cv.All(
CONFIG_SCHEMA.extend(i2c.i2c_device_schema(0x28)).extend(cv.COMPONENT_SCHEMA)
)
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)
# Actions
AT581XResetAction = at581x_ns.class_("AT581XResetAction", automation.Action)
AT581XSettingsAction = at581x_ns.class_("AT581XSettingsAction", automation.Action)
@automation.register_action(
"at581x.reset",
AT581XResetAction,
maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(AT581XComponent),
}
),
)
async def at581x_reset_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var
RADAR_SETTINGS_SCHEMA = cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(AT581XComponent),
cv.Optional(CONF_HW_FRONTEND_RESET): cv.templatable(cv.boolean),
cv.Optional(CONF_FREQUENCY, default="5800MHz"): cv.templatable(
cv.All(cv.frequency, cv.one_of(*RADAR_ALLOWED_FREQ))
),
cv.Optional(CONF_SENSING_DISTANCE, default=823): cv.templatable(
cv.int_range(min=0, max=1023)
),
cv.Optional(CONF_POWERON_SELFCHECK_TIME, default="2000ms"): cv.templatable(
cv.All(
cv.positive_time_period_milliseconds,
cv.Range(max=core.TimePeriod(milliseconds=65535)),
)
),
cv.Optional(CONF_POWER_CONSUMPTION, default="70uA"): cv.templatable(
cv.All(cv.current, cv.one_of(*RADAR_ALLOWED_CUR_CONSUMPTION))
),
cv.Optional(CONF_PROTECT_TIME, default="1000ms"): cv.templatable(
cv.All(
cv.positive_time_period_milliseconds,
cv.Range(
min=core.TimePeriod(milliseconds=1),
max=core.TimePeriod(milliseconds=65535),
),
)
),
cv.Optional(CONF_TRIGGER_BASE, default="500ms"): cv.templatable(
cv.All(
cv.positive_time_period_milliseconds,
cv.Range(
min=core.TimePeriod(milliseconds=1),
max=core.TimePeriod(milliseconds=65535),
),
)
),
cv.Optional(CONF_TRIGGER_KEEP, default="1500ms"): cv.templatable(
cv.All(
cv.positive_time_period_milliseconds,
cv.Range(
min=core.TimePeriod(milliseconds=1),
max=core.TimePeriod(milliseconds=65535),
),
)
),
cv.Optional(CONF_STAGE_GAIN, default=3): cv.templatable(
cv.int_range(min=0, max=12)
),
}
).add_extra(
cv.has_at_least_one_key(
CONF_HW_FRONTEND_RESET,
CONF_FREQUENCY,
CONF_SENSING_DISTANCE,
)
)
@automation.register_action(
"at581x.settings",
AT581XSettingsAction,
RADAR_SETTINGS_SCHEMA,
)
async def at581x_settings_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
# Radar configuration
if frontend_reset := config.get(CONF_HW_FRONTEND_RESET):
template_ = await cg.templatable(frontend_reset, args, int)
cg.add(var.set_hw_frontend_reset(template_))
if freq := config.get(CONF_FREQUENCY):
template_ = await cg.templatable(freq, args, float)
template_ = int(template_ / 1000000)
cg.add(var.set_frequency(template_))
if sens_dist := config.get(CONF_SENSING_DISTANCE):
template_ = await cg.templatable(sens_dist, args, int)
cg.add(var.set_sensing_distance(template_))
if selfcheck := config.get(CONF_POWERON_SELFCHECK_TIME):
template_ = await cg.templatable(selfcheck, args, float)
if isinstance(template_, cv.TimePeriod):
template_ = template_.total_milliseconds
template_ = int(template_)
cg.add(var.set_poweron_selfcheck_time(template_))
if protect := config.get(CONF_PROTECT_TIME):
template_ = await cg.templatable(protect, args, float)
if isinstance(template_, cv.TimePeriod):
template_ = template_.total_milliseconds
template_ = int(template_)
cg.add(var.set_protect_time(template_))
if trig_base := config.get(CONF_TRIGGER_BASE):
template_ = await cg.templatable(trig_base, args, float)
if isinstance(template_, cv.TimePeriod):
template_ = template_.total_milliseconds
template_ = int(template_)
cg.add(var.set_trigger_base(template_))
if trig_keep := config.get(CONF_TRIGGER_KEEP):
template_ = await cg.templatable(trig_keep, args, float)
if isinstance(template_, cv.TimePeriod):
template_ = template_.total_milliseconds
template_ = int(template_)
cg.add(var.set_trigger_keep(template_))
if stage_gain := config.get(CONF_STAGE_GAIN):
template_ = await cg.templatable(stage_gain, args, int)
cg.add(var.set_stage_gain(template_))
if power := config.get(CONF_POWER_CONSUMPTION):
template_ = await cg.templatable(power, args, float)
template_ = int(template_ * 1000000)
cg.add(var.set_power_consumption(template_))
return var

View File

@ -0,0 +1,195 @@
#include "at581x.h"
#include "esphome/core/log.h"
/* Select gain for AT581X (3dB per step for level1, 6dB per step for level 2), high value = small gain. (p12) */
const uint8_t GAIN_ADDR_TABLE[] = {0x5c, 0x63};
const uint8_t GAIN5C_TABLE[] = {0x08, 0x18, 0x28, 0x38, 0x48, 0x58, 0x68, 0x78, 0x88, 0x98, 0xa8, 0xb8, 0xc8};
const uint8_t GAIN63_TABLE[] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06};
const uint8_t GAIN61_VALUE = 0xCA; // 0xC0 | 0x02 (freq present) | 0x08 (gain present)
/*!< Power consumption configuration table (p12). */
const uint8_t POWER_TABLE[] = {48, 56, 63, 70, 77, 91, 105, 115, 40, 44, 47, 51, 54, 61, 68, 78};
const uint8_t POWER67_TABLE[] = {0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7};
const uint8_t POWER68_TABLE[] = {0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8,
24, 24, 24, 24, 24, 24, 24, 24}; // See Page 12, shift by 3 bits
/*!< Frequency Configuration table (p14/15 of datasheet). */
const uint8_t FREQ_ADDR = 0x61;
const uint16_t FREQ_TABLE[] = {5696, 5715, 5730, 5748, 5765, 5784, 5800, 5819, 5836, 5851, 5869, 5888};
const uint8_t FREQ5F_TABLE[] = {0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x40, 0x41, 0x42, 0x43};
const uint8_t FREQ60_TABLE[] = {0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9e, 0x9e, 0x9e, 0x9e};
/*!< Value for RF and analog modules switch (p10). */
const uint8_t RF_OFF_TABLE[] = {0x46, 0xaa, 0x50};
const uint8_t RF_ON_TABLE[] = {0x45, 0x55, 0xA0};
const uint8_t RF_REG_ADDR[] = {0x5d, 0x62, 0x51};
/*!< Registers of Lighting delay time. Unit: ms, min 2s (p8) */
const uint8_t HIGH_LEVEL_DELAY_CONTROL_ADDR = 0x41; /*!< Time_flag_out_ctrl 0x01 */
const uint8_t HIGH_LEVEL_DELAY_VALUE_ADDR = 0x42; /*!< Time_flag_out_1 Bit<7:0> */
const uint8_t RESET_ADDR = 0x00;
/*!< Sensing distance address */
const uint8_t SIGNAL_DETECTION_THRESHOLD_ADDR_LO = 0x10;
const uint8_t SIGNAL_DETECTION_THRESHOLD_ADDR_HI = 0x11;
/*!< Bit field value for power registers */
const uint8_t POWER_THRESHOLD_ADDR_HI = 0x68;
const uint8_t POWER_THRESHOLD_ADDR_LO = 0x67;
const uint8_t PWR_WORK_TIME_EN = 8; // Reg 0x67
const uint8_t PWR_BURST_TIME_EN = 32; // Reg 0x68
const uint8_t PWR_THRESH_EN = 64; // Reg 0x68
const uint8_t PWR_THRESH_VAL_EN = 128; // Reg 0x67
/*!< Times */
const uint8_t TRIGGER_BASE_TIME_ADDR = 0x3D; // 4 bytes, so up to 0x40
const uint8_t PROTECT_TIME_ADDR = 0x4E; // 2 bytes, up to 0x4F
const uint8_t TRIGGER_KEEP_TIME_ADDR = 0x42; // 4 bytes, so up to 0x45
const uint8_t TIME41_VALUE = 1;
const uint8_t SELF_CHECK_TIME_ADDR = 0x38; // 2 bytes, up to 0x39
namespace esphome {
namespace at581x {
static const char *const TAG = "at581x";
bool AT581XComponent::i2c_write_reg(uint8_t addr, uint8_t data) {
return this->write_register(addr, &data, 1) == esphome::i2c::NO_ERROR;
}
bool AT581XComponent::i2c_write_reg(uint8_t addr, uint32_t data) {
return this->i2c_write_reg(addr + 0, uint8_t(data & 0xFF)) &&
this->i2c_write_reg(addr + 1, uint8_t((data >> 8) & 0xFF)) &&
this->i2c_write_reg(addr + 2, uint8_t((data >> 16) & 0xFF)) &&
this->i2c_write_reg(addr + 3, uint8_t((data >> 24) & 0xFF));
}
bool AT581XComponent::i2c_write_reg(uint8_t addr, uint16_t data) {
return this->i2c_write_reg(addr, uint8_t(data & 0xFF)) && this->i2c_write_reg(addr + 1, uint8_t((data >> 8) & 0xFF));
}
bool AT581XComponent::i2c_read_reg(uint8_t addr, uint8_t &data) {
return this->read_register(addr, &data, 1) == esphome::i2c::NO_ERROR;
}
void AT581XComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up AT581X..."); }
void AT581XComponent::dump_config() { LOG_I2C_DEVICE(this); }
#define ARRAY_SIZE(X) (sizeof(X) / sizeof((X)[0]))
bool AT581XComponent::i2c_write_config() {
ESP_LOGCONFIG(TAG, "Writing new config for AT581X...");
ESP_LOGCONFIG(TAG, "Frequency: %dMHz", this->freq_);
ESP_LOGCONFIG(TAG, "Sensing distance: %d", this->delta_);
ESP_LOGCONFIG(TAG, "Power: %dµA", this->power_);
ESP_LOGCONFIG(TAG, "Gain: %d", this->gain_);
ESP_LOGCONFIG(TAG, "Trigger base time: %dms", this->trigger_base_time_ms_);
ESP_LOGCONFIG(TAG, "Trigger keep time: %dms", this->trigger_keep_time_ms_);
ESP_LOGCONFIG(TAG, "Protect time: %dms", this->protect_time_ms_);
ESP_LOGCONFIG(TAG, "Self check time: %dms", this->self_check_time_ms_);
// Set frequency point
if (!this->i2c_write_reg(FREQ_ADDR, GAIN61_VALUE)) {
ESP_LOGE(TAG, "Failed to write AT581X Freq mode");
return false;
}
// Find the current frequency from the table to know what value to write
for (size_t i = 0; i < ARRAY_SIZE(FREQ_TABLE) + 1; i++) {
if (i == ARRAY_SIZE(FREQ_TABLE)) {
ESP_LOGE(TAG, "Set frequency not found");
return false;
}
if (FREQ_TABLE[i] == this->freq_) {
if (!this->i2c_write_reg(0x5F, FREQ5F_TABLE[i]) || !this->i2c_write_reg(0x60, FREQ60_TABLE[i])) {
ESP_LOGE(TAG, "Failed to write AT581X Freq value");
return false;
}
break;
}
}
// Set distance
if (!this->i2c_write_reg(SIGNAL_DETECTION_THRESHOLD_ADDR_LO, (uint8_t) (this->delta_ & 0xFF)) ||
!this->i2c_write_reg(SIGNAL_DETECTION_THRESHOLD_ADDR_HI, (uint8_t) (this->delta_ >> 8))) {
ESP_LOGE(TAG, "Failed to write AT581X sensing distance low");
return false;
}
// Set power setting
uint8_t pwr67 = PWR_THRESH_VAL_EN | PWR_WORK_TIME_EN, pwr68 = PWR_BURST_TIME_EN | PWR_THRESH_EN;
for (size_t i = 0; i < ARRAY_SIZE(POWER_TABLE) + 1; i++) {
if (i == ARRAY_SIZE(POWER_TABLE)) {
ESP_LOGE(TAG, "Set power not found");
return false;
}
if (POWER_TABLE[i] == this->power_) {
pwr67 |= POWER67_TABLE[i];
pwr68 |= POWER68_TABLE[i]; // See Page 12
break;
}
}
if (!this->i2c_write_reg(POWER_THRESHOLD_ADDR_LO, pwr67) || !this->i2c_write_reg(POWER_THRESHOLD_ADDR_HI, pwr68)) {
ESP_LOGE(TAG, "Failed to write AT581X power registers");
return false;
}
// Set gain
if (!this->i2c_write_reg(GAIN_ADDR_TABLE[0], GAIN5C_TABLE[this->gain_]) ||
!this->i2c_write_reg(GAIN_ADDR_TABLE[1], GAIN63_TABLE[this->gain_ >> 1])) {
ESP_LOGE(TAG, "Failed to write AT581X gain registers");
return false;
}
// Set times
if (!this->i2c_write_reg(TRIGGER_BASE_TIME_ADDR, (uint32_t) this->trigger_base_time_ms_)) {
ESP_LOGE(TAG, "Failed to write AT581X trigger base time registers");
return false;
}
if (!this->i2c_write_reg(TRIGGER_KEEP_TIME_ADDR, (uint32_t) this->trigger_keep_time_ms_)) {
ESP_LOGE(TAG, "Failed to write AT581X trigger keep time registers");
return false;
}
if (!this->i2c_write_reg(PROTECT_TIME_ADDR, (uint16_t) this->protect_time_ms_)) {
ESP_LOGE(TAG, "Failed to write AT581X protect time registers");
return false;
}
if (!this->i2c_write_reg(SELF_CHECK_TIME_ADDR, (uint16_t) this->self_check_time_ms_)) {
ESP_LOGE(TAG, "Failed to write AT581X self check time registers");
return false;
}
if (!this->i2c_write_reg(0x41, TIME41_VALUE)) {
ESP_LOGE(TAG, "Failed to enable AT581X time registers");
return false;
}
// Don't know why it's required in other code, it's not in datasheet
if (!this->i2c_write_reg(0x55, (uint8_t) 0x04)) {
ESP_LOGE(TAG, "Failed to enable AT581X");
return false;
}
// Ok, config is written, let's reset the chip so it's using the new config
return this->reset_hardware_frontend();
}
// float AT581XComponent::get_setup_priority() const { return 0; }
bool AT581XComponent::reset_hardware_frontend() {
if (!this->i2c_write_reg(RESET_ADDR, (uint8_t) 0) || !this->i2c_write_reg(RESET_ADDR, (uint8_t) 1)) {
ESP_LOGE(TAG, "Failed to reset AT581X hardware frontend");
return false;
}
return true;
}
void AT581XComponent::set_rf_mode(bool enable) {
const uint8_t *p = enable ? &RF_ON_TABLE[0] : &RF_OFF_TABLE[0];
for (size_t i = 0; i < ARRAY_SIZE(RF_REG_ADDR); i++) {
if (!this->i2c_write_reg(RF_REG_ADDR[i], p[i])) {
ESP_LOGE(TAG, "Failed to write AT581X RF mode");
return;
}
}
}
} // namespace at581x
} // namespace esphome

View File

@ -0,0 +1,62 @@
#pragma once
#include <utility>
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/defines.h"
#ifdef USE_SWITCH
#include "esphome/components/switch/switch.h"
#endif
#include "esphome/components/i2c/i2c.h"
namespace esphome {
namespace at581x {
class AT581XComponent : public Component, public i2c::I2CDevice {
#ifdef USE_SWITCH
protected:
switch_::Switch *rf_power_switch_{nullptr};
public:
void set_rf_power_switch(switch_::Switch *s) {
this->rf_power_switch_ = s;
s->turn_on();
}
#endif
void setup() override;
void dump_config() override;
// float get_setup_priority() const override;
void set_sensing_distance(int distance) { this->delta_ = 1023 - distance; }
void set_rf_mode(bool enabled);
void set_frequency(int frequency) { this->freq_ = frequency; }
void set_poweron_selfcheck_time(int value) { this->self_check_time_ms_ = value; }
void set_protect_time(int value) { this->protect_time_ms_ = value; }
void set_trigger_base(int value) { this->trigger_base_time_ms_ = value; }
void set_trigger_keep(int value) { this->trigger_keep_time_ms_ = value; }
void set_stage_gain(int value) { this->gain_ = value; }
void set_power_consumption(int value) { this->power_ = value; }
bool i2c_write_config();
bool reset_hardware_frontend();
bool i2c_write_reg(uint8_t addr, uint8_t data);
bool i2c_write_reg(uint8_t addr, uint32_t data);
bool i2c_write_reg(uint8_t addr, uint16_t data);
bool i2c_read_reg(uint8_t addr, uint8_t &data);
protected:
int freq_;
int self_check_time_ms_; /*!< Power-on self-test time, range: 0 ~ 65536 ms */
int protect_time_ms_; /*!< Protection time, recommended 1000 ms */
int trigger_base_time_ms_; /*!< Default: 500 ms */
int trigger_keep_time_ms_; /*!< Total trig time = TRIGGER_BASE_TIME + DEF_TRIGGER_KEEP_TIME, minimum: 1 */
int delta_; /*!< Delta value: 0 ~ 1023, the larger the value, the shorter the distance */
int gain_; /*!< Default: 9dB */
int power_; /*!< In µA */
};
} // namespace at581x
} // namespace esphome

View File

@ -0,0 +1,71 @@
#pragma once
#include "esphome/core/automation.h"
#include "esphome/core/helpers.h"
#include "at581x.h"
namespace esphome {
namespace at581x {
template<typename... Ts> class AT581XResetAction : public Action<Ts...>, public Parented<AT581XComponent> {
public:
void play(Ts... x) { this->parent_->reset_hardware_frontend(); }
};
template<typename... Ts> class AT581XSettingsAction : public Action<Ts...>, public Parented<AT581XComponent> {
public:
TEMPLATABLE_VALUE(int8_t, hw_frontend_reset)
TEMPLATABLE_VALUE(int, frequency)
TEMPLATABLE_VALUE(int, sensing_distance)
TEMPLATABLE_VALUE(int, poweron_selfcheck_time)
TEMPLATABLE_VALUE(int, power_consumption)
TEMPLATABLE_VALUE(int, protect_time)
TEMPLATABLE_VALUE(int, trigger_base)
TEMPLATABLE_VALUE(int, trigger_keep)
TEMPLATABLE_VALUE(int, stage_gain)
void play(Ts... x) {
if (this->frequency_.has_value()) {
int v = this->frequency_.value(x...);
this->parent_->set_frequency(v);
}
if (this->sensing_distance_.has_value()) {
int v = this->sensing_distance_.value(x...);
this->parent_->set_sensing_distance(v);
}
if (this->poweron_selfcheck_time_.has_value()) {
int v = this->poweron_selfcheck_time_.value(x...);
this->parent_->set_poweron_selfcheck_time(v);
}
if (this->power_consumption_.has_value()) {
int v = this->power_consumption_.value(x...);
this->parent_->set_power_consumption(v);
}
if (this->protect_time_.has_value()) {
int v = this->protect_time_.value(x...);
this->parent_->set_protect_time(v);
}
if (this->trigger_base_.has_value()) {
int v = this->trigger_base_.value(x...);
this->parent_->set_trigger_base(v);
}
if (this->trigger_keep_.has_value()) {
int v = this->trigger_keep_.value(x...);
this->parent_->set_trigger_keep(v);
}
if (this->stage_gain_.has_value()) {
int v = this->stage_gain_.value(x...);
this->parent_->set_stage_gain(v);
}
// This actually perform all the modification on the system
this->parent_->i2c_write_config();
if (this->hw_frontend_reset_.has_value() && this->hw_frontend_reset_.value(x...) == true) {
this->parent_->reset_hardware_frontend();
}
}
};
} // namespace at581x
} // namespace esphome

View File

@ -0,0 +1,31 @@
import esphome.codegen as cg
from esphome.components import switch
import esphome.config_validation as cv
from esphome.const import (
DEVICE_CLASS_SWITCH,
ICON_WIFI,
)
from .. import CONF_AT581X_ID, AT581XComponent, at581x_ns
DEPENDENCIES = ["at581x"]
RFSwitch = at581x_ns.class_("RFSwitch", switch.Switch)
CONFIG_SCHEMA = switch.switch_schema(
RFSwitch,
device_class=DEVICE_CLASS_SWITCH,
icon=ICON_WIFI,
).extend(
cv.Schema(
{
cv.GenerateID(CONF_AT581X_ID): cv.use_id(AT581XComponent),
}
)
)
async def to_code(config):
at581x_component = await cg.get_variable(config[CONF_AT581X_ID])
s = await switch.new_switch(config)
await cg.register_parented(s, config[CONF_AT581X_ID])
cg.add(at581x_component.set_rf_power_switch(s))

View File

@ -0,0 +1,12 @@
#include "rf_switch.h"
namespace esphome {
namespace at581x {
void RFSwitch::write_state(bool state) {
this->publish_state(state);
this->parent_->set_rf_mode(state);
}
} // namespace at581x
} // namespace esphome

View File

@ -0,0 +1,15 @@
#pragma once
#include "esphome/components/switch/switch.h"
#include "../at581x.h"
namespace esphome {
namespace at581x {
class RFSwitch : public switch_::Switch, public Parented<AT581XComponent> {
protected:
void write_state(bool state) override;
};
} // namespace at581x
} // namespace esphome

View File

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

View File

@ -0,0 +1,18 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import climate_ir
from esphome.const import CONF_ID
AUTO_LOAD = ["climate_ir"]
daikin_arc_ns = cg.esphome_ns.namespace("daikin_arc")
DaikinArcClimate = daikin_arc_ns.class_("DaikinArcClimate", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(
{cv.GenerateID(): cv.declare_id(DaikinArcClimate)}
)
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,487 @@
#include "daikin_arc.h"
#include <cmath>
#include "esphome/components/remote_base/remote_base.h"
#include "esphome/core/log.h"
namespace esphome {
namespace daikin_arc {
static const char *const TAG = "daikin.climate";
void DaikinArcClimate::setup() {
climate_ir::ClimateIR::setup();
// Never send nan to HA
if (std::isnan(this->target_humidity))
this->target_humidity = 0;
if (std::isnan(this->current_temperature))
this->current_temperature = 0;
if (std::isnan(this->current_humidity))
this->current_humidity = 0;
}
void DaikinArcClimate::transmit_query_() {
uint8_t remote_header[8] = {0x11, 0xDA, 0x27, 0x00, 0x84, 0x87, 0x20, 0x00};
// Calculate checksum
for (int i = 0; i < sizeof(remote_header) - 1; i++) {
remote_header[sizeof(remote_header) - 1] += remote_header[i];
}
auto transmit = this->transmitter_->transmit();
auto *data = transmit.get_data();
data->set_carrier_frequency(DAIKIN_IR_FREQUENCY);
data->mark(DAIKIN_ARC_PRE_MARK);
data->space(DAIKIN_ARC_PRE_SPACE);
data->mark(DAIKIN_HEADER_MARK);
data->space(DAIKIN_HEADER_SPACE);
for (uint8_t i : remote_header) {
for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask
data->mark(DAIKIN_BIT_MARK);
bool bit = i & mask;
data->space(bit ? DAIKIN_ONE_SPACE : DAIKIN_ZERO_SPACE);
}
}
data->mark(DAIKIN_BIT_MARK);
data->space(0);
transmit.perform();
}
void DaikinArcClimate::transmit_state() {
// 0x11, 0xDA, 0x27, 0x00, 0xC5, 0x00, 0x00, 0xD7, 0x11, 0xDA, 0x27, 0x00,
// 0x42, 0x49, 0x05, 0xA2,
uint8_t remote_header[20] = {0x11, 0xDA, 0x27, 0x00, 0x02, 0xd0, 0x02, 0x03, 0x80, 0x03, 0x82, 0x30, 0x41, 0x1f, 0x82,
0xf4,
/* とつど */
/* 0x13 */
0x00, 0x24, 0x00, 0x00};
// 05 0 [1:3]MODE 1 [OFF TMR] [ON TMR] Power
// 06-07 TEMP
// 08 [0:3] SPEED [4:7] Swing
// 09 00
// 10 00
// 11, 12: timer
// 13 [0:6] 0000000 [7] POWERMODE
// 14 0a
// 15 c4
// 16 [0:3] 8 00 [6:7] SENSOR WIND = 11 / NORMAL = 00
// 17 24
uint8_t remote_state[19] = {
0x11, 0xDA, 0x27, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x60, 0x00, 0x0a, 0xC4,
/* MODE TEMP HUMD FANH FANL
*/
/* ON
0x01 0x0a */
/* OF
0x00 0x02 */
0x80, 0x24, 0x00
/* センサー風 */
/* ON 0x83 */
/* OF 0x80 */
};
remote_state[5] = this->operation_mode_() | 0x08;
remote_state[6] = this->temperature_();
remote_state[7] = this->humidity_();
static uint8_t last_humidity = 0x66;
if (remote_state[7] != last_humidity && this->mode != climate::CLIMATE_MODE_OFF) {
ESP_LOGD(TAG, "Set Humditiy: %d, %d\n", (int) this->target_humidity, (int) remote_state[7]);
remote_header[9] |= 0x10;
last_humidity = remote_state[7];
}
uint16_t fan_speed = this->fan_speed_();
remote_state[8] = fan_speed >> 8;
remote_state[9] = fan_speed & 0xff;
// Calculate checksum
for (int i = 0; i < sizeof(remote_header) - 1; i++) {
remote_header[sizeof(remote_header) - 1] += remote_header[i];
}
// Calculate checksum
for (int i = 0; i < DAIKIN_STATE_FRAME_SIZE - 1; i++) {
remote_state[DAIKIN_STATE_FRAME_SIZE - 1] += remote_state[i];
}
auto transmit = this->transmitter_->transmit();
auto *data = transmit.get_data();
data->set_carrier_frequency(DAIKIN_IR_FREQUENCY);
data->mark(DAIKIN_ARC_PRE_MARK);
data->space(DAIKIN_ARC_PRE_SPACE);
data->mark(DAIKIN_HEADER_MARK);
data->space(DAIKIN_HEADER_SPACE);
for (uint8_t i : remote_header) {
for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask
data->mark(DAIKIN_BIT_MARK);
bool bit = i & mask;
data->space(bit ? DAIKIN_ONE_SPACE : DAIKIN_ZERO_SPACE);
}
}
data->mark(DAIKIN_BIT_MARK);
data->space(DAIKIN_MESSAGE_SPACE);
data->mark(DAIKIN_HEADER_MARK);
data->space(DAIKIN_HEADER_SPACE);
for (uint8_t i : remote_state) {
for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask
data->mark(DAIKIN_BIT_MARK);
bool bit = i & mask;
data->space(bit ? DAIKIN_ONE_SPACE : DAIKIN_ZERO_SPACE);
}
}
data->mark(DAIKIN_BIT_MARK);
data->space(0);
transmit.perform();
}
uint8_t DaikinArcClimate::operation_mode_() {
uint8_t operating_mode = DAIKIN_MODE_ON;
switch (this->mode) {
case climate::CLIMATE_MODE_COOL:
operating_mode |= DAIKIN_MODE_COOL;
break;
case climate::CLIMATE_MODE_DRY:
operating_mode |= DAIKIN_MODE_DRY;
break;
case climate::CLIMATE_MODE_HEAT:
operating_mode |= DAIKIN_MODE_HEAT;
break;
case climate::CLIMATE_MODE_HEAT_COOL:
operating_mode |= DAIKIN_MODE_AUTO;
break;
case climate::CLIMATE_MODE_FAN_ONLY:
operating_mode |= DAIKIN_MODE_FAN;
break;
case climate::CLIMATE_MODE_OFF:
default:
operating_mode = DAIKIN_MODE_OFF;
break;
}
return operating_mode;
}
uint16_t DaikinArcClimate::fan_speed_() {
uint16_t fan_speed;
switch (this->fan_mode.value()) {
case climate::CLIMATE_FAN_LOW:
fan_speed = DAIKIN_FAN_1 << 8;
break;
case climate::CLIMATE_FAN_MEDIUM:
fan_speed = DAIKIN_FAN_3 << 8;
break;
case climate::CLIMATE_FAN_HIGH:
fan_speed = DAIKIN_FAN_5 << 8;
break;
case climate::CLIMATE_FAN_AUTO:
default:
fan_speed = DAIKIN_FAN_AUTO << 8;
}
// If swing is enabled switch first 4 bits to 1111
switch (this->swing_mode) {
case climate::CLIMATE_SWING_VERTICAL:
fan_speed |= 0x0F00;
break;
case climate::CLIMATE_SWING_HORIZONTAL:
fan_speed |= 0x000F;
break;
case climate::CLIMATE_SWING_BOTH:
fan_speed |= 0x0F0F;
break;
default:
break;
}
return fan_speed;
}
uint8_t DaikinArcClimate::temperature_() {
// Force special temperatures depending on the mode
switch (this->mode) {
case climate::CLIMATE_MODE_FAN_ONLY:
return 0x32;
case climate::CLIMATE_MODE_HEAT_COOL:
case climate::CLIMATE_MODE_DRY:
return 0xc0;
default:
float new_temp = clamp<float>(this->target_temperature, DAIKIN_TEMP_MIN, DAIKIN_TEMP_MAX);
uint8_t temperature = (uint8_t) floor(new_temp);
return temperature << 1 | (new_temp - temperature > 0 ? 0x01 : 0);
}
}
uint8_t DaikinArcClimate::humidity_() {
if (this->target_humidity == 39) {
return 0;
} else if (this->target_humidity <= 40 || this->target_humidity == 44) {
return 40;
} else if (this->target_humidity <= 45 || this->target_humidity == 49) // 41 - 45
{
return 45;
} else if (this->target_humidity <= 50 || this->target_humidity == 52) // 45 - 50
{
return 50;
} else {
return 0xff;
}
}
climate::ClimateTraits DaikinArcClimate::traits() {
climate::ClimateTraits traits = climate_ir::ClimateIR::traits();
traits.set_supports_current_temperature(true);
traits.set_supports_current_humidity(false);
traits.set_supports_target_humidity(true);
traits.set_visual_min_humidity(38);
traits.set_visual_max_humidity(52);
return traits;
}
bool DaikinArcClimate::parse_state_frame_(const uint8_t frame[]) {
uint8_t checksum = 0;
for (int i = 0; i < (DAIKIN_STATE_FRAME_SIZE - 1); i++) {
checksum += frame[i];
}
if (frame[DAIKIN_STATE_FRAME_SIZE - 1] != checksum) {
ESP_LOGI(TAG, "checksum error");
return false;
}
char buf[DAIKIN_STATE_FRAME_SIZE * 3 + 1] = {0};
for (size_t i = 0; i < DAIKIN_STATE_FRAME_SIZE; i++) {
sprintf(buf, "%s%02x ", buf, frame[i]);
}
ESP_LOGD(TAG, "FRAME %s", buf);
uint8_t mode = frame[5];
if (mode & DAIKIN_MODE_ON) {
switch (mode & 0xF0) {
case DAIKIN_MODE_COOL:
this->mode = climate::CLIMATE_MODE_COOL;
break;
case DAIKIN_MODE_DRY:
this->mode = climate::CLIMATE_MODE_DRY;
break;
case DAIKIN_MODE_HEAT:
this->mode = climate::CLIMATE_MODE_HEAT;
break;
case DAIKIN_MODE_AUTO:
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
break;
case DAIKIN_MODE_FAN:
this->mode = climate::CLIMATE_MODE_FAN_ONLY;
break;
}
} else {
this->mode = climate::CLIMATE_MODE_OFF;
}
uint8_t temperature = frame[6];
if (!(temperature & 0xC0)) {
this->target_temperature = temperature >> 1;
this->target_temperature += (temperature & 0x1) ? 0.5 : 0;
}
this->target_humidity = frame[7]; // 0, 40, 45, 50, 0xff
uint8_t fan_mode = frame[8];
uint8_t swing_mode = frame[9];
if (fan_mode & 0xF && swing_mode & 0xF) {
this->swing_mode = climate::CLIMATE_SWING_BOTH;
} else if (fan_mode & 0xF) {
this->swing_mode = climate::CLIMATE_SWING_VERTICAL;
} else if (swing_mode & 0xF) {
this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL;
} else {
this->swing_mode = climate::CLIMATE_SWING_OFF;
}
switch (fan_mode & 0xF0) {
case DAIKIN_FAN_1:
case DAIKIN_FAN_2:
case DAIKIN_FAN_SILENT:
this->fan_mode = climate::CLIMATE_FAN_LOW;
break;
case DAIKIN_FAN_3:
this->fan_mode = climate::CLIMATE_FAN_MEDIUM;
break;
case DAIKIN_FAN_4:
case DAIKIN_FAN_5:
this->fan_mode = climate::CLIMATE_FAN_HIGH;
break;
case DAIKIN_FAN_AUTO:
this->fan_mode = climate::CLIMATE_FAN_AUTO;
break;
}
/*
05 0 [1:3]MODE 1 [OFF TMR] [ON TMR] Power
06-07 TEMP
08 [0:3] SPEED [4:7] Swing
09 00
10 00
11, 12: timer
13 [0:6] 0000000 [7] POWERMODE
14 0a
15 c4
16 [0:3] 8 00 [6:7] SENSOR WIND = 11 / NORMAL = 00
17 24
05 06 07 08 09 10 11 12 13 14 15 16 17 18
None FRAME 11 da 27 00 00 49 2e 00 b0 00 00 06 60 00 0a c4 80 24 11
1H FRAME 11 da 27 00 00 4d 2e 00 b0 00 00 c6 30 00 2a c4 80 24 c5
1H30 FRAME 11 da 27 00 00 4d 2e 00 b0 00 00 a6 32 00 2a c4 80 24 a7
2H FRAME 11 da 27 00 00 4d 2e 00 b0 00 00 86 34 00 2a c4 80 24 89
*/
this->publish_state();
return true;
}
bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) {
uint8_t state_frame[DAIKIN_STATE_FRAME_SIZE] = {};
bool valid_daikin_frame = false;
if (data.expect_item(DAIKIN_HEADER_MARK, DAIKIN_HEADER_SPACE)) {
valid_daikin_frame = true;
int bytes_count = data.size() / 2 / 8;
std::unique_ptr<char[]> buf(new char[bytes_count * 3 + 1]);
buf[0] = '\0';
for (size_t i = 0; i < bytes_count; i++) {
uint8_t byte = 0;
for (int8_t bit = 0; bit < 8; bit++) {
if (data.expect_item(DAIKIN_BIT_MARK, DAIKIN_ONE_SPACE)) {
byte |= 1 << bit;
} else if (!data.expect_item(DAIKIN_BIT_MARK, DAIKIN_ZERO_SPACE)) {
valid_daikin_frame = false;
break;
}
}
sprintf(buf.get(), "%s%02x ", buf.get(), byte);
}
ESP_LOGD(TAG, "WHOLE FRAME %s size: %d", buf.get(), data.size());
}
if (!valid_daikin_frame) {
char sbuf[16 * 10 + 1];
sbuf[0] = '\0';
for (size_t j = 0; j < data.size(); j++) {
if ((j - 2) % 16 == 0) {
if (j > 0) {
ESP_LOGD(TAG, "DATA %04x: %s", (j - 16 > 0xffff ? 0 : j - 16), sbuf);
}
sbuf[0] = '\0';
}
char type_ch = ' ';
// debug_tolerance = 25%
if (DAIKIN_DBG_LOWER(DAIKIN_ARC_PRE_MARK) <= data[j] && data[j] <= DAIKIN_DBG_UPPER(DAIKIN_ARC_PRE_MARK))
type_ch = 'P';
if (DAIKIN_DBG_LOWER(DAIKIN_ARC_PRE_SPACE) <= -data[j] && -data[j] <= DAIKIN_DBG_UPPER(DAIKIN_ARC_PRE_SPACE))
type_ch = 'a';
if (DAIKIN_DBG_LOWER(DAIKIN_HEADER_MARK) <= data[j] && data[j] <= DAIKIN_DBG_UPPER(DAIKIN_HEADER_MARK))
type_ch = 'H';
if (DAIKIN_DBG_LOWER(DAIKIN_HEADER_SPACE) <= -data[j] && -data[j] <= DAIKIN_DBG_UPPER(DAIKIN_HEADER_SPACE))
type_ch = 'h';
if (DAIKIN_DBG_LOWER(DAIKIN_BIT_MARK) <= data[j] && data[j] <= DAIKIN_DBG_UPPER(DAIKIN_BIT_MARK))
type_ch = 'B';
if (DAIKIN_DBG_LOWER(DAIKIN_ONE_SPACE) <= -data[j] && -data[j] <= DAIKIN_DBG_UPPER(DAIKIN_ONE_SPACE))
type_ch = '1';
if (DAIKIN_DBG_LOWER(DAIKIN_ZERO_SPACE) <= -data[j] && -data[j] <= DAIKIN_DBG_UPPER(DAIKIN_ZERO_SPACE))
type_ch = '0';
if (abs(data[j]) > 100000) {
sprintf(sbuf, "%s%-5d[%c] ", sbuf, data[j] > 0 ? 99999 : -99999, type_ch);
} else {
sprintf(sbuf, "%s%-5d[%c] ", sbuf, (int) (round(data[j] / 10.) * 10), type_ch);
}
if (j == data.size() - 1) {
ESP_LOGD(TAG, "DATA %04x: %s", (j - 8 > 0xffff ? 0 : j - 8), sbuf);
}
}
}
data.reset();
if (!data.expect_item(DAIKIN_HEADER_MARK, DAIKIN_HEADER_SPACE)) {
ESP_LOGI(TAG, "non daikin_arc expect item");
return false;
}
for (uint8_t pos = 0; pos < DAIKIN_STATE_FRAME_SIZE; pos++) {
uint8_t byte = 0;
for (int8_t bit = 0; bit < 8; bit++) {
if (data.expect_item(DAIKIN_BIT_MARK, DAIKIN_ONE_SPACE)) {
byte |= 1 << bit;
} else if (!data.expect_item(DAIKIN_BIT_MARK, DAIKIN_ZERO_SPACE)) {
ESP_LOGI(TAG, "non daikin_arc expect item pos: %d", pos);
return false;
}
}
state_frame[pos] = byte;
if (pos == 0) {
// frame header
if (byte != 0x11) {
ESP_LOGI(TAG, "non daikin_arc expect pos: %d header: %02x", pos, byte);
return false;
}
} else if (pos == 1) {
// frame header
if (byte != 0xDA) {
ESP_LOGI(TAG, "non daikin_arc expect pos: %d header: %02x", pos, byte);
return false;
}
} else if (pos == 2) {
// frame header
if (byte != 0x27) {
ESP_LOGI(TAG, "non daikin_arc expect pos: %d header: %02x", pos, byte);
return false;
}
} else if (pos == 3) { // NOLINT(bugprone-branch-clone)
// frame header
if (byte != 0x00) {
ESP_LOGI(TAG, "non daikin_arc expect pos: %d header: %02x", pos, byte);
return false;
}
} else if (pos == 4) {
// frame type
if (byte != 0x00) {
ESP_LOGI(TAG, "non daikin_arc expect pos: %d header: %02x", pos, byte);
return false;
}
} else if (pos == 5) {
if (data.size() == 385) {
/*
11 da 27 00 00 1a 0c 04 2c 21 61 07 00 07 0c 00 18 00 0e 3c 00 6c 1b 61
Inside Temp
Outside Temp
Humdidity
*/
this->current_temperature = state_frame[5]; // Inside temperature
// this->current_temperature = state_frame[6]; // Outside temperature
this->publish_state();
return true;
} else if ((byte & 0x40) != 0x40) {
ESP_LOGI(TAG, "non daikin_arc expect pos: %d header: %02x", pos, byte);
return false;
}
}
}
return this->parse_state_frame_(state_frame);
}
void DaikinArcClimate::control(const climate::ClimateCall &call) {
if (call.get_target_humidity().has_value()) {
this->target_humidity = *call.get_target_humidity();
}
climate_ir::ClimateIR::control(call);
}
} // namespace daikin_arc
} // namespace esphome

View File

@ -0,0 +1,76 @@
#pragma once
#include "esphome/components/climate_ir/climate_ir.h"
namespace esphome {
namespace daikin_arc {
// Values for Daikin ARC43XXX IR Controllers
// Temperature
const uint8_t DAIKIN_TEMP_MIN = 10; // Celsius
const uint8_t DAIKIN_TEMP_MAX = 30; // Celsius
// Modes
const uint8_t DAIKIN_MODE_AUTO = 0x00;
const uint8_t DAIKIN_MODE_COOL = 0x30;
const uint8_t DAIKIN_MODE_HEAT = 0x40;
const uint8_t DAIKIN_MODE_DRY = 0x20;
const uint8_t DAIKIN_MODE_FAN = 0x60;
const uint8_t DAIKIN_MODE_OFF = 0x00;
const uint8_t DAIKIN_MODE_ON = 0x01;
// Fan Speed
const uint8_t DAIKIN_FAN_AUTO = 0xA0;
const uint8_t DAIKIN_FAN_SILENT = 0xB0;
const uint8_t DAIKIN_FAN_1 = 0x30;
const uint8_t DAIKIN_FAN_2 = 0x40;
const uint8_t DAIKIN_FAN_3 = 0x50;
const uint8_t DAIKIN_FAN_4 = 0x60;
const uint8_t DAIKIN_FAN_5 = 0x70;
// IR Transmission
const uint32_t DAIKIN_IR_FREQUENCY = 38000;
const uint32_t DAIKIN_ARC_PRE_MARK = 9950;
const uint32_t DAIKIN_ARC_PRE_SPACE = 25100;
const uint32_t DAIKIN_HEADER_MARK = 3450;
const uint32_t DAIKIN_HEADER_SPACE = 1760;
const uint32_t DAIKIN_BIT_MARK = 400;
const uint32_t DAIKIN_ONE_SPACE = 1300;
const uint32_t DAIKIN_ZERO_SPACE = 480;
const uint32_t DAIKIN_MESSAGE_SPACE = 35000;
const uint8_t DAIKIN_DBG_TOLERANCE = 25;
#define DAIKIN_DBG_LOWER(x) ((100 - DAIKIN_DBG_TOLERANCE) * (x) / 100U)
#define DAIKIN_DBG_UPPER(x) ((100 + DAIKIN_DBG_TOLERANCE) * (x) / 100U)
// State Frame size
const uint8_t DAIKIN_STATE_FRAME_SIZE = 19;
class DaikinArcClimate : public climate_ir::ClimateIR {
public:
DaikinArcClimate()
: climate_ir::ClimateIR(DAIKIN_TEMP_MIN, DAIKIN_TEMP_MAX, 0.5f, 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,
climate::CLIMATE_SWING_HORIZONTAL, climate::CLIMATE_SWING_BOTH}) {}
void setup() override;
protected:
void control(const climate::ClimateCall &call) override;
// Transmit via IR the state of this climate controller.
void transmit_query_();
void transmit_state() override;
climate::ClimateTraits traits() override;
uint8_t operation_mode_();
uint16_t fan_speed_();
uint8_t temperature_();
uint8_t humidity_();
// Handle received IR Buffer
bool on_receive(remote_base::RemoteReceiveData data) override;
bool parse_state_frame_(const uint8_t frame[]);
};
} // namespace daikin_arc
} // namespace esphome

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,75 +95,74 @@ 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"
git_file = git.GitFile.from_shorthand(import_url)
kwargs = {
"name": name,
"friendly_name": friendly_name,
"platform": platform,
"board": board,
"ssid": "!secret wifi_ssid",
"psk": "!secret wifi_password",
if git_file.query and "full_config" in git_file.query:
url = git_file.raw_url
try:
req = requests.get(url, timeout=30)
req.raise_for_status()
except requests.exceptions.RequestException as e:
raise ValueError(f"Error while fetching {url}: {e}") from e
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}
esphome_core = {"name": "${name}", "name_add_mac_suffix": False}
if friendly_name:
substitutions["friendly_name"] = friendly_name
esphome_core["friendly_name"] = "${friendly_name}"
config = {
"substitutions": substitutions,
"packages": {project_name: import_url},
"esphome": esphome_core,
}
if encryption:
noise_psk = secrets.token_bytes(32)
key = base64.b64encode(noise_psk).decode()
kwargs["api_encryption_key"] = key
config["api"] = {"encryption": {"key": key}}
p.write_text(
wizard_file(**kwargs),
encoding="utf8",
)
else:
git_file = git.GitFile.from_shorthand(import_url)
output = dump(config)
if git_file.query and "full_config" in git_file.query:
url = git_file.raw_url
try:
req = requests.get(url, timeout=30)
req.raise_for_status()
except requests.exceptions.RequestException as e:
raise ValueError(f"Error while fetching {url}: {e}") from e
if network == CONF_WIFI:
output += WIFI_CONFIG
p.write_text(req.text, encoding="utf8")
else:
substitutions = {"name": name}
esphome_core = {"name": "${name}", "name_add_mac_suffix": False}
if friendly_name:
substitutions["friendly_name"] = friendly_name
esphome_core["friendly_name"] = "${friendly_name}"
config = {
"substitutions": substitutions,
"packages": {project_name: import_url},
"esphome": esphome_core,
}
if encryption:
noise_psk = secrets.token_bytes(32)
key = base64.b64encode(noise_psk).decode()
config["api"] = {"encryption": {"key": key}}
output = dump(config)
if network == CONF_WIFI:
output += WIFI_CONFIG
p.write_text(output, encoding="utf8")
p.write_text(output, encoding="utf8")

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

@ -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

@ -60,6 +60,8 @@ void DisplayMenuComponent::left() {
if (this->editing_) {
this->finish_editing_();
changed = true;
} else {
changed = this->leave_menu_();
}
break;
case MENU_MODE_JOYSTICK:

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

@ -155,6 +155,8 @@ CONFIG_SCHEMA = cv.All(
"DP83848": RMII_SCHEMA,
"IP101": RMII_SCHEMA,
"JL1101": RMII_SCHEMA,
"KSZ8081": RMII_SCHEMA,
"KSZ8081RNA": RMII_SCHEMA,
"W5500": SPI_SCHEMA,
},
upper=True,

View File

@ -1,36 +1,87 @@
#ifdef USE_HOST
#include <filesystem>
#include <fstream>
#include "preferences.h"
#include <cstring>
#include "esphome/core/preferences.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/defines.h"
#include "esphome/core/application.h"
namespace esphome {
namespace host {
namespace fs = std::filesystem;
static const char *const TAG = "host.preferences";
class HostPreferences : public ESPPreferences {
public:
ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override { return {}; }
void HostPreferences::setup_() {
if (this->setup_complete_)
return;
this->filename_.append(getenv("HOME"));
this->filename_.append("/.esphome");
this->filename_.append("/prefs");
fs::create_directories(this->filename_);
this->filename_.append("/");
this->filename_.append(App.get_name());
this->filename_.append(".prefs");
FILE *fp = fopen(this->filename_.c_str(), "rb");
if (fp != nullptr) {
while (!feof((fp))) {
uint32_t key;
uint8_t len;
if (fread(&key, sizeof(key), 1, fp) != 1)
break;
if (fread(&len, sizeof(len), 1, fp) != 1)
break;
uint8_t data[len];
if (fread(data, sizeof(uint8_t), len, fp) != len)
break;
std::vector vec(data, data + len);
this->data[key] = vec;
}
fclose(fp);
}
this->setup_complete_ = true;
}
ESPPreferenceObject make_preference(size_t length, uint32_t type) override { return {}; }
bool HostPreferences::sync() {
this->setup_();
FILE *fp = fopen(this->filename_.c_str(), "wb");
std::map<uint32_t, std::vector<uint8_t>>::iterator it;
bool sync() override { return true; }
bool reset() override { return true; }
for (it = this->data.begin(); it != this->data.end(); ++it) {
fwrite(&it->first, sizeof(uint32_t), 1, fp);
uint8_t len = it->second.size();
fwrite(&len, sizeof(len), 1, fp);
fwrite(it->second.data(), sizeof(uint8_t), it->second.size(), fp);
}
fclose(fp);
return true;
}
bool HostPreferences::reset() {
host_preferences->data.clear();
return true;
}
ESPPreferenceObject HostPreferences::make_preference(size_t length, uint32_t type, bool in_flash) {
auto backend = new HostPreferenceBackend(type);
return ESPPreferenceObject(backend);
};
void setup_preferences() {
auto *pref = new HostPreferences(); // NOLINT(cppcoreguidelines-owning-memory)
host_preferences = pref;
global_preferences = pref;
}
bool HostPreferenceBackend::save(const uint8_t *data, size_t len) {
return host_preferences->save(this->key_, data, len);
}
bool HostPreferenceBackend::load(uint8_t *data, size_t len) { return host_preferences->load(this->key_, data, len); }
HostPreferences *host_preferences;
} // namespace host
ESPPreferences *global_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace esphome
#endif // USE_HOST

View File

@ -2,10 +2,63 @@
#ifdef USE_HOST
#include "esphome/core/preferences.h"
#include <map>
namespace esphome {
namespace host {
class HostPreferenceBackend : public ESPPreferenceBackend {
public:
explicit HostPreferenceBackend(uint32_t key) { this->key_ = key; }
bool save(const uint8_t *data, size_t len) override;
bool load(uint8_t *data, size_t len) override;
protected:
uint32_t key_{};
};
class HostPreferences : public ESPPreferences {
public:
bool sync() override;
bool reset() override;
ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override;
ESPPreferenceObject make_preference(size_t length, uint32_t type) override {
return make_preference(length, type, false);
}
bool save(uint32_t key, const uint8_t *data, size_t len) {
if (len > 255)
return false;
this->setup_();
std::vector vec(data, data + len);
this->data[key] = vec;
return true;
}
bool load(uint32_t key, uint8_t *data, size_t len) {
if (len > 255)
return false;
this->setup_();
if (this->data.count(key) == 0)
return false;
auto vec = this->data[key];
if (vec.size() != len)
return false;
memcpy(data, vec.data(), len);
return true;
}
protected:
void setup_();
bool setup_complete_{};
std::string filename_{};
std::map<uint32_t, std::vector<uint8_t>> data{};
};
void setup_preferences();
extern HostPreferences *host_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace host
} // namespace esphome

View File

@ -12,12 +12,12 @@ static const char *const TAG = "audio";
void I2SAudioMediaPlayer::control(const media_player::MediaPlayerCall &call) {
if (call.get_media_url().has_value()) {
this->current_url_ = call.get_media_url();
if (this->state == media_player::MEDIA_PLAYER_STATE_PLAYING && this->audio_ != nullptr) {
if (this->i2s_state_ != I2S_STATE_STOPPED && this->audio_ != nullptr) {
if (this->audio_->isRunning()) {
this->audio_->stopSong();
}
this->audio_->connecttohost(this->current_url_.value().c_str());
this->state = media_player::MEDIA_PLAYER_STATE_PLAYING;
} else {
this->start();
}

View File

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

View File

@ -0,0 +1,58 @@
#include "jsn_sr04t.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <cinttypes>
// Very basic support for JSN_SR04T V3.0 distance sensor in mode 2
namespace esphome {
namespace jsn_sr04t {
static const char *const TAG = "jsn_sr04t.sensor";
void Jsnsr04tComponent::update() {
this->write_byte(0x55);
ESP_LOGV(TAG, "Request read out from sensor");
}
void Jsnsr04tComponent::loop() {
while (this->available() > 0) {
uint8_t data;
this->read_byte(&data);
ESP_LOGV(TAG, "Read byte from sensor: %x", data);
if (this->buffer_.empty() && data != 0xFF)
continue;
this->buffer_.push_back(data);
if (this->buffer_.size() == 4)
this->check_buffer_();
}
}
void Jsnsr04tComponent::check_buffer_() {
uint8_t checksum = this->buffer_[0] + this->buffer_[1] + this->buffer_[2];
if (this->buffer_[3] == checksum) {
uint16_t distance = encode_uint16(this->buffer_[1], this->buffer_[2]);
if (distance > 250) {
float meters = distance / 1000.0f;
ESP_LOGV(TAG, "Distance from sensor: %" PRIu32 "mm, %.3fm", distance, meters);
this->publish_state(meters);
} else {
ESP_LOGW(TAG, "Invalid data read from sensor: %s", format_hex_pretty(this->buffer_).c_str());
}
} else {
ESP_LOGW(TAG, "checksum failed: %02x != %02x", checksum, this->buffer_[3]);
}
this->buffer_.clear();
}
void Jsnsr04tComponent::dump_config() {
LOG_SENSOR("", "JST_SR04T Sensor", this);
LOG_UPDATE_INTERVAL(this);
}
} // namespace jsn_sr04t
} // namespace esphome

View File

@ -0,0 +1,28 @@
#pragma once
#include <vector>
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/uart/uart.h"
namespace esphome {
namespace jsn_sr04t {
class Jsnsr04tComponent : public sensor::Sensor, public PollingComponent, public uart::UARTDevice {
public:
// Nothing really public.
// ========== INTERNAL METHODS ==========
void update() override;
void loop() override;
void dump_config() override;
protected:
void check_buffer_();
std::vector<uint8_t> buffer_;
};
} // namespace jsn_sr04t
} // namespace esphome

View File

@ -0,0 +1,44 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor, uart
from esphome.const import (
STATE_CLASS_MEASUREMENT,
UNIT_METER,
ICON_ARROW_EXPAND_VERTICAL,
)
CODEOWNERS = ["@Mafus1"]
DEPENDENCIES = ["uart"]
jsn_sr04t_ns = cg.esphome_ns.namespace("jsn_sr04t")
Jsnsr04tComponent = jsn_sr04t_ns.class_(
"Jsnsr04tComponent", sensor.Sensor, cg.PollingComponent, uart.UARTDevice
)
CONFIG_SCHEMA = (
sensor.sensor_schema(
Jsnsr04tComponent,
unit_of_measurement=UNIT_METER,
icon=ICON_ARROW_EXPAND_VERTICAL,
accuracy_decimals=3,
state_class=STATE_CLASS_MEASUREMENT,
)
.extend(cv.polling_component_schema("60s"))
.extend(uart.UART_DEVICE_SCHEMA)
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"jsn_sr04t",
baud_rate=9600,
require_tx=True,
require_rx=True,
data_bits=8,
parity=None,
stop_bits=1,
)
async def to_code(config):
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
await uart.register_uart_device(var, config)

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_config_mode(true);
this->set_system_mode(system_mode_);
this->set_config_mode(false);
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,18 +528,16 @@ 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
return 0; // restart does not reply exit now
}
while (!this->cmd_reply_.ack) {
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

@ -287,7 +287,7 @@ def _load_model_data(manifest_path: Path):
except cv.Invalid as e:
raise EsphomeError(f"Invalid manifest file: {e}") from e
model_path = urljoin(str(manifest_path), manifest[CONF_MODEL])
model_path = manifest_path.parent / manifest[CONF_MODEL]
with open(model_path, "rb") as f:
model = f.read()

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

@ -23,7 +23,7 @@ class DataTrigger : public Trigger<const std::vector<int16_t> &> {
}
};
template<typename... Ts> class IsCapturingActon : public Condition<Ts...>, public Parented<Microphone> {
template<typename... Ts> class IsCapturingCondition : public Condition<Ts...>, public Parented<Microphone> {
public:
bool check(Ts... x) override { return this->parent_->is_running(); }
};

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,25 +19,30 @@ 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):
cg.add_define("USE_NETWORK_IPV6", config[CONF_ENABLE_IPV6])
cg.add_define("USE_NETWORK_MIN_IPV6_ADDR_COUNT", config[CONF_MIN_IPV6_ADDR_COUNT])
if CORE.using_esp_idf:
add_idf_sdkconfig_option("CONFIG_LWIP_IPV6", config[CONF_ENABLE_IPV6])
add_idf_sdkconfig_option(
"CONFIG_LWIP_IPV6_AUTOCONFIG", config[CONF_ENABLE_IPV6]
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]
)
else:
if config[CONF_ENABLE_IPV6]:
cg.add_build_flag("-DCONFIG_LWIP_IPV6")
cg.add_build_flag("-DCONFIG_LWIP_IPV6_AUTOCONFIG")
if CORE.is_rp2040:
cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_ENABLE_IPV6")
if CORE.is_esp8266:
cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_LWIP2_IPV6_LOW_MEMORY")
if CORE.using_esp_idf:
add_idf_sdkconfig_option("CONFIG_LWIP_IPV6", config[CONF_ENABLE_IPV6])
add_idf_sdkconfig_option(
"CONFIG_LWIP_IPV6_AUTOCONFIG", config[CONF_ENABLE_IPV6]
)
else:
if config[CONF_ENABLE_IPV6]:
cg.add_build_flag("-DCONFIG_LWIP_IPV6")
cg.add_build_flag("-DCONFIG_LWIP_IPV6_AUTOCONFIG")
if CORE.is_rp2040:
cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_ENABLE_IPV6")
if CORE.is_esp8266:
cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_LWIP2_IPV6_LOW_MEMORY")

View File

@ -4,6 +4,7 @@
#include <cstdio>
#include <array>
#include "esphome/core/macros.h"
#include "esphome/core/helpers.h"
#if defined(USE_ESP_IDF) || defined(USE_LIBRETINY) || USE_ARDUINO_VERSION_CODE > VERSION_CODE(3, 0, 0)
#include <lwip/ip_addr.h>
@ -116,7 +117,7 @@ struct IPAddress {
bool is_set() { return !ip_addr_isany(&ip_addr_); }
bool is_ip4() { return IP_IS_V4(&ip_addr_); }
bool is_ip6() { return IP_IS_V6(&ip_addr_); }
std::string str() const { return ipaddr_ntoa(&ip_addr_); }
std::string str() const { return str_lower_case(ipaddr_ntoa(&ip_addr_)); }
bool operator==(const IPAddress &other) const { return ip_addr_cmp(&ip_addr_, &other.ip_addr_); }
bool operator!=(const IPAddress &other) const { return !ip_addr_cmp(&ip_addr_, &other.ip_addr_); }
IPAddress &operator+=(uint8_t increase) {

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

@ -1,16 +1,17 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/uart/uart.h"
#include "esphome/core/component.h"
namespace esphome {
namespace pmsx003 {
// known command bytes
#define PMS_CMD_AUTO_MANUAL 0xE1 // data=0: perform measurement manually, data=1: perform measurement automatically
#define PMS_CMD_TRIG_MANUAL 0xE2 // trigger a manual measurement
#define PMS_CMD_ON_STANDBY 0xE4 // data=0: go to standby mode, data=1: go to normal mode
static const uint8_t PMS_CMD_AUTO_MANUAL =
0xE1; // data=0: perform measurement manually, data=1: perform measurement automatically
static const uint8_t PMS_CMD_TRIG_MANUAL = 0xE2; // trigger a manual measurement
static const uint8_t PMS_CMD_ON_STANDBY = 0xE4; // data=0: go to standby mode, data=1: go to normal mode
static const uint16_t PMS_STABILISING_MS = 30000; // time taken for the sensor to become stable after power on

View File

@ -72,19 +72,13 @@ void QMC5883LComponent::dump_config() {
LOG_SENSOR(" ", "Y Axis", this->y_sensor_);
LOG_SENSOR(" ", "Z Axis", this->z_sensor_);
LOG_SENSOR(" ", "Heading", this->heading_sensor_);
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
}
float QMC5883LComponent::get_setup_priority() const { return setup_priority::DATA; }
void QMC5883LComponent::update() {
uint8_t status = false;
this->read_byte(QMC5883L_REGISTER_STATUS, &status);
uint16_t raw_x, raw_y, raw_z;
if (!this->read_byte_16_(QMC5883L_REGISTER_DATA_X_LSB, &raw_x) ||
!this->read_byte_16_(QMC5883L_REGISTER_DATA_Y_LSB, &raw_y) ||
!this->read_byte_16_(QMC5883L_REGISTER_DATA_Z_LSB, &raw_z)) {
this->status_set_warning();
return;
}
if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG)
this->read_byte(QMC5883L_REGISTER_STATUS, &status);
float mg_per_bit;
switch (this->range_) {
@ -99,12 +93,49 @@ void QMC5883LComponent::update() {
}
// in µT
const float x = int16_t(raw_x) * mg_per_bit * 0.1f;
const float y = int16_t(raw_y) * mg_per_bit * 0.1f;
const float z = int16_t(raw_z) * mg_per_bit * 0.1f;
float x = NAN, y = NAN, z = NAN;
if (this->x_sensor_ != nullptr || this->heading_sensor_ != nullptr) {
uint16_t raw_x;
if (!this->read_byte_16_(QMC5883L_REGISTER_DATA_X_LSB, &raw_x)) {
this->status_set_warning();
return;
}
x = int16_t(raw_x) * mg_per_bit * 0.1f;
}
if (this->y_sensor_ != nullptr || this->heading_sensor_ != nullptr) {
uint16_t raw_y;
if (!this->read_byte_16_(QMC5883L_REGISTER_DATA_Y_LSB, &raw_y)) {
this->status_set_warning();
return;
}
y = int16_t(raw_y) * mg_per_bit * 0.1f;
}
if (this->z_sensor_ != nullptr) {
uint16_t raw_z;
if (!this->read_byte_16_(QMC5883L_REGISTER_DATA_Z_LSB, &raw_z)) {
this->status_set_warning();
return;
}
z = int16_t(raw_z) * mg_per_bit * 0.1f;
}
float heading = atan2f(0.0f - x, y) * 180.0f / M_PI;
ESP_LOGD(TAG, "Got x=%0.02fµT y=%0.02fµT z=%0.02fµT heading=%0.01f° status=%u", x, y, z, heading, status);
float heading = NAN;
if (this->heading_sensor_ != nullptr) {
heading = atan2f(0.0f - x, y) * 180.0f / M_PI;
}
float temp = NAN;
if (this->temperature_sensor_ != nullptr) {
uint16_t raw_temp;
if (!this->read_byte_16_(QMC5883L_REGISTER_TEMPERATURE_LSB, &raw_temp)) {
this->status_set_warning();
return;
}
temp = int16_t(raw_temp) * 0.01f;
}
ESP_LOGD(TAG, "Got x=%0.02fµT y=%0.02fµT z=%0.02fµT heading=%0.01f° temperature=%0.01f°C status=%u", x, y, z, heading,
temp, status);
if (this->x_sensor_ != nullptr)
this->x_sensor_->publish_state(x);
@ -114,6 +145,8 @@ void QMC5883LComponent::update() {
this->z_sensor_->publish_state(z);
if (this->heading_sensor_ != nullptr)
this->heading_sensor_->publish_state(heading);
if (this->temperature_sensor_ != nullptr)
this->temperature_sensor_->publish_state(temp);
}
bool QMC5883LComponent::read_byte_16_(uint8_t a_register, uint16_t *data) {

View File

@ -40,6 +40,7 @@ class QMC5883LComponent : public PollingComponent, public i2c::I2CDevice {
void set_y_sensor(sensor::Sensor *y_sensor) { y_sensor_ = y_sensor; }
void set_z_sensor(sensor::Sensor *z_sensor) { z_sensor_ = z_sensor; }
void set_heading_sensor(sensor::Sensor *heading_sensor) { heading_sensor_ = heading_sensor; }
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
protected:
QMC5883LDatarate datarate_{QMC5883L_DATARATE_10_HZ};
@ -49,6 +50,7 @@ class QMC5883LComponent : public PollingComponent, public i2c::I2CDevice {
sensor::Sensor *y_sensor_{nullptr};
sensor::Sensor *z_sensor_{nullptr};
sensor::Sensor *heading_sensor_{nullptr};
sensor::Sensor *temperature_sensor_{nullptr};
enum ErrorCode {
NONE = 0,
COMMUNICATION_FAILED,

View File

@ -6,12 +6,15 @@ from esphome.const import (
CONF_FIELD_STRENGTH_X,
CONF_FIELD_STRENGTH_Y,
CONF_FIELD_STRENGTH_Z,
CONF_TEMPERATURE,
CONF_ID,
CONF_OVERSAMPLING,
CONF_RANGE,
DEVICE_CLASS_TEMPERATURE,
ICON_MAGNET,
STATE_CLASS_MEASUREMENT,
UNIT_MICROTESLA,
UNIT_CELSIUS,
UNIT_DEGREES,
ICON_SCREEN_ROTATION,
CONF_UPDATE_INTERVAL,
@ -79,6 +82,12 @@ heading_schema = sensor.sensor_schema(
icon=ICON_SCREEN_ROTATION,
accuracy_decimals=1,
)
temperature_schema = sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
)
CONFIG_SCHEMA = (
cv.Schema(
@ -95,6 +104,7 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_FIELD_STRENGTH_Y): field_strength_schema,
cv.Optional(CONF_FIELD_STRENGTH_Z): field_strength_schema,
cv.Optional(CONF_HEADING): heading_schema,
cv.Optional(CONF_TEMPERATURE): temperature_schema,
}
)
.extend(cv.polling_component_schema("60s"))
@ -131,3 +141,6 @@ async def to_code(config):
if CONF_HEADING in config:
sens = await sensor.new_sensor(config[CONF_HEADING])
cg.add(var.set_heading_sensor(sens))
if CONF_TEMPERATURE in config:
sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
cg.add(var.set_temperature_sensor(sens))

View File

@ -51,5 +51,17 @@ void QrCode::draw(display::Display *buff, uint16_t x_offset, uint16_t y_offset,
}
}
}
uint8_t QrCode::get_size() {
if (this->needs_update_) {
this->generate_qr_code();
this->needs_update_ = false;
}
uint8_t size = qrcodegen_getSize(this->qr_);
return size;
}
} // namespace qr_code
} // namespace esphome

View File

@ -24,6 +24,8 @@ class QrCode : public Component {
void generate_qr_code();
uint8_t get_size();
protected:
std::string value_;
qrcodegen_Ecc ecc_;

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

@ -74,12 +74,12 @@ def _format_framework_arduino_version(ver: cv.Version) -> str:
# The default/recommended arduino framework version
# - https://github.com/earlephilhower/arduino-pico/releases
# - https://api.registry.platformio.org/v3/packages/earlephilhower/tool/framework-arduinopico
RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 6, 0)
RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 7, 2)
# The platformio/raspberrypi version to use for arduino frameworks
# - https://github.com/platformio/platform-raspberrypi/releases
# - https://api.registry.platformio.org/v3/packages/platformio/platform/raspberrypi
ARDUINO_PLATFORM_VERSION = cv.Version(1, 10, 0)
ARDUINO_PLATFORM_VERSION = cv.Version(1, 12, 0)
def _arduino_check_versions(value):

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

@ -14,6 +14,8 @@ from esphome.const import (
CONF_HEATER_ENABLED = "heater_enabled"
CODEOWNERS = ["@mrtoy-me"]
DEPENDENCIES = ["i2c"]
AUTO_LOAD = ["sensirion_common"]
@ -26,13 +28,13 @@ CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(SHT3XDComponent),
cv.Required(CONF_TEMPERATURE): sensor.sensor_schema(
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.Required(CONF_HUMIDITY): sensor.sensor_schema(
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
accuracy_decimals=1,
device_class=DEVICE_CLASS_HUMIDITY,

View File

@ -6,7 +6,11 @@ namespace sht3xd {
static const char *const TAG = "sht3xd";
static const uint16_t SHT3XD_COMMAND_READ_SERIAL_NUMBER = 0x3780;
// use read serial number register with clock stretching disabled as per other SHT3XD_COMMAND registers
// which provides support for SHT85 sensor
// SHT85 does not support clock stretching and uses same registers as SHT3xd with clock stretching disabled
static const uint16_t SHT3XD_COMMAND_READ_SERIAL_NUMBER = 0x3682;
static const uint16_t SHT3XD_COMMAND_READ_STATUS = 0xF32D;
static const uint16_t SHT3XD_COMMAND_CLEAR_STATUS = 0x3041;
static const uint16_t SHT3XD_COMMAND_HEATER_ENABLE = 0x306D;
@ -22,25 +26,32 @@ void SHT3XDComponent::setup() {
this->mark_failed();
return;
}
this->serial_number_ = (uint32_t(raw_serial_number[0]) << 16) | uint32_t(raw_serial_number[1]);
if (!this->write_command(heater_enabled_ ? SHT3XD_COMMAND_HEATER_ENABLE : SHT3XD_COMMAND_HEATER_DISABLE)) {
this->mark_failed();
return;
}
uint32_t serial_number = (uint32_t(raw_serial_number[0]) << 16) | uint32_t(raw_serial_number[1]);
ESP_LOGV(TAG, " Serial Number: 0x%08" PRIX32, serial_number);
}
void SHT3XDComponent::dump_config() {
ESP_LOGCONFIG(TAG, "SHT3xD:");
LOG_I2C_DEVICE(this);
if (this->is_failed()) {
ESP_LOGE(TAG, "Communication with SHT3xD failed!");
ESP_LOGE(TAG, " Communication with SHT3xD failed!");
return;
}
ESP_LOGD(TAG, " Serial Number: 0x%08" PRIX32, this->serial_number_);
ESP_LOGD(TAG, " Heater Enabled: %s", this->heater_enabled_ ? "true" : "false");
LOG_I2C_DEVICE(this);
LOG_UPDATE_INTERVAL(this);
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
}
float SHT3XDComponent::get_setup_priority() const { return setup_priority::DATA; }
void SHT3XDComponent::update() {
if (this->status_has_warning()) {
ESP_LOGD(TAG, "Retrying to reconnect the sensor.");

View File

@ -25,6 +25,7 @@ class SHT3XDComponent : public PollingComponent, public sensirion_common::Sensir
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *humidity_sensor_{nullptr};
bool heater_enabled_{true};
uint32_t serial_number_{0};
};
} // namespace sht3xd

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,23 +97,32 @@ void SM2135::loop() {
this->write_byte_(SM2135_ADDR_MC);
this->write_byte_(current_mask_);
if (this->update_channel_ == 3 || this->update_channel_ == 4) {
// No color so must be Cold/Warm
if (this->separate_modes_) {
if (this->update_channel_ == 3 || this->update_channel_ == 4) {
// No color so must be Cold/Warm
this->write_byte_(SM2135_CW);
this->sm2135_stop_();
delay(1);
this->sm2135_start_();
this->write_byte_(SM2135_ADDR_C);
this->write_byte_(this->pwm_amounts_[4]); // Warm
this->write_byte_(this->pwm_amounts_[3]); // Cold
this->write_byte_(SM2135_CW);
this->sm2135_stop_();
delay(1);
this->sm2135_start_();
this->write_byte_(SM2135_ADDR_C);
this->write_byte_(this->pwm_amounts_[4]); // Warm
this->write_byte_(this->pwm_amounts_[3]); // Cold
} else {
// Color
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
}
} else {
// Color
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

@ -236,6 +236,7 @@ void SSD1306::set_invert(bool invert) {
// Inverse display mode (0xA6, 0xA7)
this->command(SSD1306_COMMAND_NORMAL_DISPLAY | this->invert_);
}
float SSD1306::get_contrast() { return this->contrast_; };
void SSD1306::set_contrast(float contrast) {
// validation
this->contrast_ = clamp(contrast, 0.0F, 1.0F);
@ -243,6 +244,7 @@ void SSD1306::set_contrast(float contrast) {
this->command(SSD1306_COMMAND_SET_CONTRAST);
this->command(int(SSD1306_MAX_CONTRAST * (this->contrast_)));
}
float SSD1306::get_brightness() { return this->brightness_; };
void SSD1306::set_brightness(float brightness) {
// validation
if (!this->is_ssd1305_())

View File

@ -36,7 +36,9 @@ class SSD1306 : public display::DisplayBuffer {
void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; }
void set_external_vcc(bool external_vcc) { this->external_vcc_ = external_vcc; }
void init_contrast(float contrast) { this->contrast_ = contrast; }
float get_contrast();
void set_contrast(float contrast);
float get_brightness();
void init_brightness(float brightness) { this->brightness_ = brightness; }
void set_brightness(float brightness);
void init_flip_x(bool flip_x) { this->flip_x_ = flip_x; }

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

@ -30,37 +30,37 @@ CONFIG_SCHEMA = cv.All(
def determine_config_register(polling_period):
if polling_period >= 16.0:
if polling_period >= 16000:
# 64 averaged conversions, max conversion time
# 0000 00 111 11 00000
# 0000 0011 1110 0000
return 0x03E0
if polling_period >= 8.0:
if polling_period >= 8000:
# 64 averaged conversions, high conversion time
# 0000 00 110 11 00000
# 0000 0011 0110 0000
return 0x0360
if polling_period >= 4.0:
if polling_period >= 4000:
# 64 averaged conversions, mid conversion time
# 0000 00 101 11 00000
# 0000 0010 1110 0000
return 0x02E0
if polling_period >= 1.0:
if polling_period >= 1000:
# 64 averaged conversions, min conversion time
# 0000 00 000 11 00000
# 0000 0000 0110 0000
return 0x0060
if polling_period >= 0.5:
if polling_period >= 500:
# 32 averaged conversions, min conversion time
# 0000 00 000 10 00000
# 0000 0000 0100 0000
return 0x0040
if polling_period >= 0.25:
if polling_period >= 250:
# 8 averaged conversions, mid conversion time
# 0000 00 010 01 00000
# 0000 0001 0010 0000
return 0x0120
if polling_period >= 0.125:
if polling_period >= 125:
# 8 averaged conversions, min conversion time
# 0000 00 000 01 00000
# 0000 0000 0010 0000
@ -76,5 +76,5 @@ async def to_code(config):
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
update_period = config[CONF_UPDATE_INTERVAL].total_seconds
update_period = config[CONF_UPDATE_INTERVAL].total_milliseconds
cg.add(var.set_config(determine_config_register(update_period)))

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:
raise cv.Invalid(
f"{CONF_ACTIVE_STATE_DATAPOINT} required if using "
f"{CONF_ACTIVE_STATE_COOLING_VALUE}"
)
else:
if value[CONF_SUPPORTS_COOL] and CONF_ACTIVE_STATE_COOLING_VALUE not in value:
raise cv.Invalid(
f"{CONF_ACTIVE_STATE_COOLING_VALUE} required if using "
f"{CONF_ACTIVE_STATE_DATAPOINT} and device supports cooling"
)
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"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."
)
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"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}"
)
return value
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,
},
)
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:
cg.add(
var.set_current_temperature_multiplier(
config[CONF_CURRENT_TEMPERATURE_MULTIPLIER]
if current_temperature_multiplier := config.get(
CONF_CURRENT_TEMPERATURE_MULTIPLIER
):
cg.add(
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,8 +145,22 @@ 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();
if (this->reports_fahrenheit_)
@ -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

@ -12,6 +12,8 @@ ListEntitiesIterator::ListEntitiesIterator(WebServer *web_server) : web_server_(
#ifdef USE_BINARY_SENSOR
bool ListEntitiesIterator::on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) {
if (this->web_server_->events_.count() == 0)
return true;
this->web_server_->events_.send(
this->web_server_->binary_sensor_json(binary_sensor, binary_sensor->state, DETAIL_ALL).c_str(), "state");
return true;
@ -19,30 +21,40 @@ bool ListEntitiesIterator::on_binary_sensor(binary_sensor::BinarySensor *binary_
#endif
#ifdef USE_COVER
bool ListEntitiesIterator::on_cover(cover::Cover *cover) {
if (this->web_server_->events_.count() == 0)
return true;
this->web_server_->events_.send(this->web_server_->cover_json(cover, DETAIL_ALL).c_str(), "state");
return true;
}
#endif
#ifdef USE_FAN
bool ListEntitiesIterator::on_fan(fan::Fan *fan) {
if (this->web_server_->events_.count() == 0)
return true;
this->web_server_->events_.send(this->web_server_->fan_json(fan, DETAIL_ALL).c_str(), "state");
return true;
}
#endif
#ifdef USE_LIGHT
bool ListEntitiesIterator::on_light(light::LightState *light) {
if (this->web_server_->events_.count() == 0)
return true;
this->web_server_->events_.send(this->web_server_->light_json(light, DETAIL_ALL).c_str(), "state");
return true;
}
#endif
#ifdef USE_SENSOR
bool ListEntitiesIterator::on_sensor(sensor::Sensor *sensor) {
if (this->web_server_->events_.count() == 0)
return true;
this->web_server_->events_.send(this->web_server_->sensor_json(sensor, sensor->state, DETAIL_ALL).c_str(), "state");
return true;
}
#endif
#ifdef USE_SWITCH
bool ListEntitiesIterator::on_switch(switch_::Switch *a_switch) {
if (this->web_server_->events_.count() == 0)
return true;
this->web_server_->events_.send(this->web_server_->switch_json(a_switch, a_switch->state, DETAIL_ALL).c_str(),
"state");
return true;
@ -50,12 +62,16 @@ bool ListEntitiesIterator::on_switch(switch_::Switch *a_switch) {
#endif
#ifdef USE_BUTTON
bool ListEntitiesIterator::on_button(button::Button *button) {
if (this->web_server_->events_.count() == 0)
return true;
this->web_server_->events_.send(this->web_server_->button_json(button, DETAIL_ALL).c_str(), "state");
return true;
}
#endif
#ifdef USE_TEXT_SENSOR
bool ListEntitiesIterator::on_text_sensor(text_sensor::TextSensor *text_sensor) {
if (this->web_server_->events_.count() == 0)
return true;
this->web_server_->events_.send(
this->web_server_->text_sensor_json(text_sensor, text_sensor->state, DETAIL_ALL).c_str(), "state");
return true;
@ -63,6 +79,8 @@ bool ListEntitiesIterator::on_text_sensor(text_sensor::TextSensor *text_sensor)
#endif
#ifdef USE_LOCK
bool ListEntitiesIterator::on_lock(lock::Lock *a_lock) {
if (this->web_server_->events_.count() == 0)
return true;
this->web_server_->events_.send(this->web_server_->lock_json(a_lock, a_lock->state, DETAIL_ALL).c_str(), "state");
return true;
}
@ -70,6 +88,8 @@ bool ListEntitiesIterator::on_lock(lock::Lock *a_lock) {
#ifdef USE_CLIMATE
bool ListEntitiesIterator::on_climate(climate::Climate *climate) {
if (this->web_server_->events_.count() == 0)
return true;
this->web_server_->events_.send(this->web_server_->climate_json(climate, DETAIL_ALL).c_str(), "state");
return true;
}
@ -77,6 +97,8 @@ bool ListEntitiesIterator::on_climate(climate::Climate *climate) {
#ifdef USE_NUMBER
bool ListEntitiesIterator::on_number(number::Number *number) {
if (this->web_server_->events_.count() == 0)
return true;
this->web_server_->events_.send(this->web_server_->number_json(number, number->state, DETAIL_ALL).c_str(), "state");
return true;
}
@ -84,6 +106,8 @@ bool ListEntitiesIterator::on_number(number::Number *number) {
#ifdef USE_DATETIME_DATE
bool ListEntitiesIterator::on_date(datetime::DateEntity *date) {
if (this->web_server_->events_.count() == 0)
return true;
this->web_server_->events_.send(this->web_server_->date_json(date, DETAIL_ALL).c_str(), "state");
return true;
}
@ -91,6 +115,8 @@ bool ListEntitiesIterator::on_date(datetime::DateEntity *date) {
#ifdef USE_TEXT
bool ListEntitiesIterator::on_text(text::Text *text) {
if (this->web_server_->events_.count() == 0)
return true;
this->web_server_->events_.send(this->web_server_->text_json(text, text->state, DETAIL_ALL).c_str(), "state");
return true;
}
@ -98,6 +124,8 @@ bool ListEntitiesIterator::on_text(text::Text *text) {
#ifdef USE_SELECT
bool ListEntitiesIterator::on_select(select::Select *select) {
if (this->web_server_->events_.count() == 0)
return true;
this->web_server_->events_.send(this->web_server_->select_json(select, select->state, DETAIL_ALL).c_str(), "state");
return true;
}
@ -105,6 +133,8 @@ bool ListEntitiesIterator::on_select(select::Select *select) {
#ifdef USE_ALARM_CONTROL_PANEL
bool ListEntitiesIterator::on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) {
if (this->web_server_->events_.count() == 0)
return true;
this->web_server_->events_.send(
this->web_server_->alarm_control_panel_json(a_alarm_control_panel, a_alarm_control_panel->get_state(), DETAIL_ALL)
.c_str(),

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);
@ -416,6 +416,8 @@ void WebServer::handle_js_request(AsyncWebServerRequest *request) {
#ifdef USE_SENSOR
void WebServer::on_sensor_update(sensor::Sensor *obj, float state) {
if (this->events_.count() == 0)
return;
this->events_.send(this->sensor_json(obj, state, DETAIL_STATE).c_str(), "state");
}
void WebServer::handle_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) {
@ -449,6 +451,8 @@ std::string WebServer::sensor_json(sensor::Sensor *obj, float value, JsonDetail
#ifdef USE_TEXT_SENSOR
void WebServer::on_text_sensor_update(text_sensor::TextSensor *obj, const std::string &state) {
if (this->events_.count() == 0)
return;
this->events_.send(this->text_sensor_json(obj, state, DETAIL_STATE).c_str(), "state");
}
void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) {
@ -471,6 +475,8 @@ std::string WebServer::text_sensor_json(text_sensor::TextSensor *obj, const std:
#ifdef USE_SWITCH
void WebServer::on_switch_update(switch_::Switch *obj, bool state) {
if (this->events_.count() == 0)
return;
this->events_.send(this->switch_json(obj, state, DETAIL_STATE).c_str(), "state");
}
std::string WebServer::switch_json(switch_::Switch *obj, bool value, JsonDetail start_config) {
@ -486,7 +492,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 +523,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;
@ -532,6 +538,8 @@ void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlM
#ifdef USE_BINARY_SENSOR
void WebServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) {
if (this->events_.count() == 0)
return;
this->events_.send(this->binary_sensor_json(obj, state, DETAIL_STATE).c_str(), "state");
}
std::string WebServer::binary_sensor_json(binary_sensor::BinarySensor *obj, bool value, JsonDetail start_config) {
@ -553,7 +561,11 @@ void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, con
#endif
#ifdef USE_FAN
void WebServer::on_fan_update(fan::Fan *obj) { this->events_.send(this->fan_json(obj, DETAIL_STATE).c_str(), "state"); }
void WebServer::on_fan_update(fan::Fan *obj) {
if (this->events_.count() == 0)
return;
this->events_.send(this->fan_json(obj, DETAIL_STATE).c_str(), "state");
}
std::string WebServer::fan_json(fan::Fan *obj, JsonDetail start_config) {
return json::build_json([obj, start_config](JsonObject root) {
set_json_icon_state_value(root, obj, "fan-" + obj->get_object_id(), obj->state ? "ON" : "OFF", obj->state,
@ -572,7 +584,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") {
@ -623,6 +635,8 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc
#ifdef USE_LIGHT
void WebServer::on_light_update(light::LightState *obj) {
if (this->events_.count() == 0)
return;
this->events_.send(this->light_json(obj, DETAIL_STATE).c_str(), "state");
}
void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMatch &match) {
@ -630,7 +644,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") {
@ -729,6 +743,8 @@ std::string WebServer::light_json(light::LightState *obj, JsonDetail start_confi
#ifdef USE_COVER
void WebServer::on_cover_update(cover::Cover *obj) {
if (this->events_.count() == 0)
return;
this->events_.send(this->cover_json(obj, DETAIL_STATE).c_str(), "state");
}
void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMatch &match) {
@ -736,7 +752,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;
@ -798,6 +814,8 @@ std::string WebServer::cover_json(cover::Cover *obj, JsonDetail start_config) {
#ifdef USE_NUMBER
void WebServer::on_number_update(number::Number *obj, float state) {
if (this->events_.count() == 0)
return;
this->events_.send(this->number_json(obj, state, DETAIL_STATE).c_str(), "state");
}
void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match) {
@ -805,7 +823,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;
@ -856,6 +874,8 @@ std::string WebServer::number_json(number::Number *obj, float value, JsonDetail
#ifdef USE_DATETIME_DATE
void WebServer::on_date_update(datetime::DateEntity *obj) {
if (this->events_.count() == 0)
return;
this->events_.send(this->date_json(obj, DETAIL_STATE).c_str(), "state");
}
void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMatch &match) {
@ -903,6 +923,8 @@ std::string WebServer::date_json(datetime::DateEntity *obj, JsonDetail start_con
#ifdef USE_TEXT
void WebServer::on_text_update(text::Text *obj, const std::string &state) {
if (this->events_.count() == 0)
return;
this->events_.send(this->text_json(obj, state, DETAIL_STATE).c_str(), "state");
}
void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMatch &match) {
@ -910,7 +932,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;
@ -954,6 +976,8 @@ std::string WebServer::text_json(text::Text *obj, const std::string &value, Json
#ifdef USE_SELECT
void WebServer::on_select_update(select::Select *obj, const std::string &state, size_t index) {
if (this->events_.count() == 0)
return;
this->events_.send(this->select_json(obj, state, DETAIL_STATE).c_str(), "state");
}
void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match) {
@ -961,7 +985,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") {
@ -1008,6 +1032,8 @@ std::string WebServer::select_json(select::Select *obj, const std::string &value
#ifdef USE_CLIMATE
void WebServer::on_climate_update(climate::Climate *obj) {
if (this->events_.count() == 0)
return;
this->events_.send(this->climate_json(obj, DETAIL_STATE).c_str(), "state");
}
@ -1016,7 +1042,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;
@ -1149,6 +1175,8 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf
#ifdef USE_LOCK
void WebServer::on_lock_update(lock::Lock *obj) {
if (this->events_.count() == 0)
return;
this->events_.send(this->lock_json(obj, obj->state, DETAIL_STATE).c_str(), "state");
}
std::string WebServer::lock_json(lock::Lock *obj, lock::LockState value, JsonDetail start_config) {
@ -1162,7 +1190,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") {
@ -1185,6 +1213,8 @@ void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMat
#ifdef USE_ALARM_CONTROL_PANEL
void WebServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) {
if (this->events_.count() == 0)
return;
this->events_.send(this->alarm_control_panel_json(obj, obj->get_state(), DETAIL_STATE).c_str(), "state");
}
std::string WebServer::alarm_control_panel_json(alarm_control_panel::AlarmControlPanel *obj,
@ -1201,7 +1231,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 +1281,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

@ -3,11 +3,11 @@
#include <esp_http_server.h>
#include <string>
#include <functional>
#include <vector>
#include <map>
#include <set>
#include <string>
#include <vector>
namespace esphome {
namespace web_server_idf {
@ -251,6 +251,8 @@ class AsyncEventSource : public AsyncWebHandler {
void send(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0);
size_t count() const { return this->sessions_.size(); }
protected:
std::string url_;
std::set<AsyncEventSourceResponse *> sessions_;

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;
}
@ -582,14 +582,14 @@ void WiFiComponent::wifi_pre_setup_() {
}
WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() {
auto status = WiFiClass::status();
if (status == WL_CONNECTED) {
return WiFiSTAConnectStatus::CONNECTED;
} else if (status == WL_CONNECT_FAILED || status == WL_CONNECTION_LOST) {
if (status == WL_CONNECT_FAILED || status == WL_CONNECTION_LOST) {
return WiFiSTAConnectStatus::ERROR_CONNECT_FAILED;
} else if (status == WL_NO_SSID_AVAIL) {
return WiFiSTAConnectStatus::ERROR_NETWORK_NOT_FOUND;
} else if (s_sta_connecting) {
return WiFiSTAConnectStatus::CONNECTING;
} else if (status == WL_CONNECTED) {
return WiFiSTAConnectStatus::CONNECTED;
}
return WiFiSTAConnectStatus::IDLE;
}
@ -707,7 +707,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
*conf.ap.password = 0;
} else {
conf.ap.authmode = WIFI_AUTH_WPA2_PSK;
strncpy(reinterpret_cast<char *>(conf.ap.password), ap.get_password().c_str(), sizeof(conf.ap.ssid));
strncpy(reinterpret_cast<char *>(conf.ap.password), ap.get_password().c_str(), sizeof(conf.ap.password));
}
conf.ap.pairwise_cipher = WIFI_CIPHER_TYPE_CCMP;

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

@ -22,7 +22,7 @@ CONF_PEER_ALLOWED_IPS = "peer_allowed_ips"
CONF_PEER_PERSISTENT_KEEPALIVE = "peer_persistent_keepalive"
CONF_REQUIRE_CONNECTION_TO_PROCEED = "require_connection_to_proceed"
DEPENDENCIES = ["time", "esp32"]
DEPENDENCIES = ["time"]
CODEOWNERS = ["@lhoracek", "@droscy", "@thomas0bernard"]
# The key validation regex has been described by Jason Donenfeld himself
@ -120,7 +120,7 @@ async def to_code(config):
# the '+1' modifier is relative to the device's own address that will
# be automatically added to the provided list.
cg.add_build_flag(f"-DCONFIG_WIREGUARD_MAX_SRC_IPS={len(allowed_ips) + 1}")
cg.add_library("droscy/esp_wireguard", "0.3.2")
cg.add_library("droscy/esp_wireguard", "0.4.0")
await cg.register_component(var, config)

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