mirror of
https://github.com/esphome/esphome.git
synced 2024-11-25 12:15:33 +01:00
Merge branch 'dev' into optolink
This commit is contained in:
commit
cae27ccd29
4
.github/actions/build-image/action.yaml
vendored
4
.github/actions/build-image/action.yaml
vendored
@ -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
|
||||
|
2
.github/actions/restore-python/action.yml
vendored
2
.github/actions/restore-python/action.yml
vendored
@ -22,7 +22,7 @@ runs:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.0.1
|
||||
uses: actions/cache/restore@v4.0.2
|
||||
with:
|
||||
path: venv
|
||||
# yamllint disable-line rule:line-length
|
||||
|
81
.github/workflows/ci-api-proto.yml
vendored
Normal file
81
.github/workflows/ci-api-proto.yml
vendored
Normal file
@ -0,0 +1,81 @@
|
||||
name: API Proto CI
|
||||
|
||||
# yamllint disable-line rule:truthy
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "esphome/components/api/api.proto"
|
||||
- "esphome/components/api/api_pb2.cpp"
|
||||
- "esphome/components/api/api_pb2.h"
|
||||
- "esphome/components/api/api_pb2_service.cpp"
|
||||
- "esphome/components/api/api_pb2_service.h"
|
||||
- "script/api_protobuf/api_protobuf.py"
|
||||
- ".github/workflows/ci-api-proto.yml"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Check generated files
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.1
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5.0.0
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Install apt dependencies
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt-cache show protobuf-compiler
|
||||
sudo apt install -y protobuf-compiler
|
||||
protoc --version
|
||||
- name: Install python dependencies
|
||||
run: pip install aioesphomeapi -c requirements.txt -r requirements_dev.txt
|
||||
- name: Generate files
|
||||
run: script/api_protobuf/api_protobuf.py
|
||||
- name: Check for changes
|
||||
run: |
|
||||
if ! git diff --quiet; then
|
||||
echo "## Job Failed" | tee -a $GITHUB_STEP_SUMMARY
|
||||
echo "You have altered the generated proto files but they do not match what is expected." | tee -a $GITHUB_STEP_SUMMARY
|
||||
echo "Please run 'script/api_protobuf/api_protobuf.py' and commit the changes." | tee -a $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
fi
|
||||
- if: failure()
|
||||
name: Review PR
|
||||
uses: actions/github-script@v7.0.1
|
||||
with:
|
||||
script: |
|
||||
await github.rest.pulls.createReview({
|
||||
pull_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
event: 'REQUEST_CHANGES',
|
||||
body: 'You have altered the generated proto files but they do not match what is expected.\nPlease run "script/api_protobuf/api_protobuf.py" and commit the changes.'
|
||||
})
|
||||
- if: success()
|
||||
name: Dismiss review
|
||||
uses: actions/github-script@v7.0.1
|
||||
with:
|
||||
script: |
|
||||
let reviews = await github.rest.pulls.listReviews({
|
||||
pull_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo
|
||||
});
|
||||
for (let review of reviews.data) {
|
||||
if (review.user.login === 'github-actions[bot]' && review.state === 'CHANGES_REQUESTED') {
|
||||
await github.rest.pulls.dismissReview({
|
||||
pull_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
review_id: review.id,
|
||||
message: 'Files now match the expected proto files.'
|
||||
});
|
||||
}
|
||||
}
|
2
.github/workflows/ci-docker.yml
vendored
2
.github/workflows/ci-docker.yml
vendored
@ -46,7 +46,7 @@ jobs:
|
||||
with:
|
||||
python-version: "3.9"
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.1.0
|
||||
uses: docker/setup-buildx-action@v3.2.0
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
|
||||
|
19
.github/workflows/ci.yml
vendored
19
.github/workflows/ci.yml
vendored
@ -47,7 +47,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v4.0.1
|
||||
uses: actions/cache@v4.0.2
|
||||
with:
|
||||
path: venv
|
||||
# yamllint disable-line rule:line-length
|
||||
@ -367,7 +367,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Cache platformio
|
||||
uses: actions/cache@v4.0.1
|
||||
uses: actions/cache@v4.0.2
|
||||
with:
|
||||
path: ~/.platformio
|
||||
# yamllint disable-line rule:line-length
|
||||
@ -398,6 +398,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- common
|
||||
if: github.event_name == 'pull_request'
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
steps:
|
||||
@ -406,10 +407,14 @@ jobs:
|
||||
with:
|
||||
# Fetch enough history so `git merge-base refs/remotes/origin/dev HEAD` works.
|
||||
fetch-depth: 500
|
||||
- name: Fetch dev branch
|
||||
- name: Get target branch
|
||||
id: target-branch
|
||||
run: |
|
||||
git -c protocol.version=2 fetch --no-tags --prune --no-recurse-submodules --depth=1 origin +refs/heads/dev*:refs/remotes/origin/dev* +refs/tags/dev*:refs/tags/dev*
|
||||
git merge-base refs/remotes/origin/dev HEAD
|
||||
echo "branch=${{ github.event.pull_request.base.ref }}" >> $GITHUB_OUTPUT
|
||||
- name: Fetch ${{ steps.target-branch.outputs.branch }} branch
|
||||
run: |
|
||||
git -c protocol.version=2 fetch --no-tags --prune --no-recurse-submodules --depth=1 origin +refs/heads/${{ steps.target-branch.outputs.branch }}:refs/remotes/origin/${{ steps.target-branch.outputs.branch }}
|
||||
git merge-base refs/remotes/origin/${{ steps.target-branch.outputs.branch }} HEAD
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
@ -419,7 +424,7 @@ jobs:
|
||||
id: set-matrix
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
echo "matrix=$(script/list-components.py --changed | jq -R -s -c 'split("\n")[:-1]')" >> $GITHUB_OUTPUT
|
||||
echo "matrix=$(script/list-components.py --changed --branch ${{ steps.target-branch.outputs.branch }} | jq -R -s -c 'split("\n")[:-1]')" >> $GITHUB_OUTPUT
|
||||
|
||||
test-build-components:
|
||||
name: Component test ${{ matrix.file }}
|
||||
@ -427,7 +432,7 @@ jobs:
|
||||
needs:
|
||||
- common
|
||||
- list-components
|
||||
if: ${{ needs.list-components.outputs.matrix != '[]' && needs.list-components.outputs.matrix != '' }}
|
||||
if: ${{ github.event_name == 'pull_request' && needs.list-components.outputs.matrix != '[]' && needs.list-components.outputs.matrix != '' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 2
|
||||
|
12
.github/workflows/release.yml
vendored
12
.github/workflows/release.yml
vendored
@ -85,18 +85,18 @@ jobs:
|
||||
python-version: "3.9"
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.1.0
|
||||
uses: docker/setup-buildx-action@v3.2.0
|
||||
- name: Set up QEMU
|
||||
if: matrix.platform != 'linux/amd64'
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
|
||||
- name: Log in to docker hub
|
||||
uses: docker/login-action@v3.0.0
|
||||
uses: docker/login-action@v3.1.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Log in to the GitHub container registry
|
||||
uses: docker/login-action@v3.0.0
|
||||
uses: docker/login-action@v3.1.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@ -163,17 +163,17 @@ jobs:
|
||||
name: digests-${{ matrix.image.target }}-${{ matrix.registry }}
|
||||
path: /tmp/digests
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.1.0
|
||||
uses: docker/setup-buildx-action@v3.2.0
|
||||
|
||||
- name: Log in to docker hub
|
||||
if: matrix.registry == 'dockerhub'
|
||||
uses: docker/login-action@v3.0.0
|
||||
uses: docker/login-action@v3.1.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Log in to the GitHub container registry
|
||||
if: matrix.registry == 'ghcr'
|
||||
uses: docker/login-action@v3.0.0
|
||||
uses: docker/login-action@v3.1.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
2
.github/workflows/sync-device-classes.yml
vendored
2
.github/workflows/sync-device-classes.yml
vendored
@ -37,7 +37,7 @@ jobs:
|
||||
python ./script/sync-device_class.py
|
||||
|
||||
- name: Commit changes
|
||||
uses: peter-evans/create-pull-request@v6.0.1
|
||||
uses: peter-evans/create-pull-request@v6.0.2
|
||||
with:
|
||||
commit-message: "Synchronise Device Classes from Home Assistant"
|
||||
committer: esphomebot <esphome@nabucasa.com>
|
||||
|
@ -103,6 +103,7 @@ esphome/components/duty_time/* @dudanov
|
||||
esphome/components/ee895/* @Stock-M
|
||||
esphome/components/ektf2232/touchscreen/* @jesserockz
|
||||
esphome/components/emc2101/* @ellull
|
||||
esphome/components/emmeti/* @E440QF
|
||||
esphome/components/ens160/* @vincentscode
|
||||
esphome/components/ens210/* @itn3rd77
|
||||
esphome/components/esp32/* @esphome/core
|
||||
@ -152,6 +153,7 @@ esphome/components/honeywellabp2_i2c/* @jpfaff
|
||||
esphome/components/host/* @esphome/core
|
||||
esphome/components/hrxl_maxsonar_wr/* @netmikey
|
||||
esphome/components/hte501/* @Stock-M
|
||||
esphome/components/htu31d/* @betterengineering
|
||||
esphome/components/hydreon_rgxx/* @functionpointer
|
||||
esphome/components/hyt271/* @Philippe12
|
||||
esphome/components/i2c/* @esphome/core
|
||||
@ -171,6 +173,7 @@ esphome/components/integration/* @OttoWinter
|
||||
esphome/components/internal_temperature/* @Mat931
|
||||
esphome/components/interval/* @esphome/core
|
||||
esphome/components/json/* @OttoWinter
|
||||
esphome/components/kamstrup_kmp/* @cfeenstra1024
|
||||
esphome/components/key_collector/* @ssieb
|
||||
esphome/components/key_provider/* @ssieb
|
||||
esphome/components/kuntze/* @ssieb
|
||||
@ -276,6 +279,7 @@ esphome/components/pvvx_mithermometer/* @pasiz
|
||||
esphome/components/pylontech/* @functionpointer
|
||||
esphome/components/qmp6988/* @andrewpc
|
||||
esphome/components/qr_code/* @wjtje
|
||||
esphome/components/qspi_amoled/* @clydebarrow
|
||||
esphome/components/qwiic_pir/* @kahrendt
|
||||
esphome/components/radon_eye_ble/* @jeffeb3
|
||||
esphome/components/radon_eye_rd200/* @jeffeb3
|
||||
@ -348,6 +352,7 @@ esphome/components/st7789v/* @kbx81
|
||||
esphome/components/st7920/* @marsjan155
|
||||
esphome/components/substitutions/* @esphome/core
|
||||
esphome/components/sun/* @OttoWinter
|
||||
esphome/components/sun_gtil2/* @Mat931
|
||||
esphome/components/switch/* @esphome/core
|
||||
esphome/components/t6615/* @tylermenezes
|
||||
esphome/components/tca9548a/* @andreashergert1984
|
||||
|
@ -41,6 +41,7 @@ CONF_CURRENT_GAIN_A = "current_gain_a"
|
||||
CONF_CURRENT_GAIN_B = "current_gain_b"
|
||||
CONF_ACTIVE_POWER_GAIN_A = "active_power_gain_a"
|
||||
CONF_ACTIVE_POWER_GAIN_B = "active_power_gain_b"
|
||||
CONF_USE_ACCUMULATED_ENERGY_REGISTERS = "use_accumulated_energy_registers"
|
||||
PGA_GAINS = {
|
||||
"1x": 0b000,
|
||||
"2x": 0b001,
|
||||
@ -155,6 +156,7 @@ ADE7953_CONFIG_SCHEMA = cv.Schema(
|
||||
cv.Optional(CONF_ACTIVE_POWER_GAIN_B, default=0x400000): cv.hex_int_range(
|
||||
min=0x100000, max=0x800000
|
||||
),
|
||||
cv.Optional(CONF_USE_ACCUMULATED_ENERGY_REGISTERS, default=False): cv.boolean,
|
||||
}
|
||||
).extend(cv.polling_component_schema("60s"))
|
||||
|
||||
@ -174,6 +176,9 @@ async def register_ade7953(var, config):
|
||||
cg.add(var.set_bigain(config.get(CONF_CURRENT_GAIN_B)))
|
||||
cg.add(var.set_awgain(config.get(CONF_ACTIVE_POWER_GAIN_A)))
|
||||
cg.add(var.set_bwgain(config.get(CONF_ACTIVE_POWER_GAIN_B)))
|
||||
cg.add(
|
||||
var.set_use_acc_energy_regs(config.get(CONF_USE_ACCUMULATED_ENERGY_REGISTERS))
|
||||
)
|
||||
|
||||
for key in [
|
||||
CONF_VOLTAGE,
|
||||
|
@ -6,6 +6,9 @@ namespace ade7953_base {
|
||||
|
||||
static const char *const TAG = "ade7953";
|
||||
|
||||
static const float ADE_POWER_FACTOR = 154.0f;
|
||||
static const float ADE_WATTSEC_POWER_FACTOR = ADE_POWER_FACTOR * ADE_POWER_FACTOR / 3600;
|
||||
|
||||
void ADE7953::setup() {
|
||||
if (this->irq_pin_ != nullptr) {
|
||||
this->irq_pin_->setup();
|
||||
@ -34,6 +37,7 @@ void ADE7953::setup() {
|
||||
this->ade_read_32(BIGAIN_32, &bigain_);
|
||||
this->ade_read_32(AWGAIN_32, &awgain_);
|
||||
this->ade_read_32(BWGAIN_32, &bwgain_);
|
||||
this->last_update_ = millis();
|
||||
this->is_setup_ = true;
|
||||
});
|
||||
}
|
||||
@ -52,6 +56,7 @@ void ADE7953::dump_config() {
|
||||
LOG_SENSOR(" ", "Active Power B Sensor", this->active_power_b_sensor_);
|
||||
LOG_SENSOR(" ", "Rective Power A Sensor", this->reactive_power_a_sensor_);
|
||||
LOG_SENSOR(" ", "Reactive Power B Sensor", this->reactive_power_b_sensor_);
|
||||
ESP_LOGCONFIG(TAG, " USE_ACC_ENERGY_REGS: %d", this->use_acc_energy_regs_);
|
||||
ESP_LOGCONFIG(TAG, " PGA_V_8: 0x%X", pga_v_);
|
||||
ESP_LOGCONFIG(TAG, " PGA_IA_8: 0x%X", pga_ia_);
|
||||
ESP_LOGCONFIG(TAG, " PGA_IB_8: 0x%X", pga_ib_);
|
||||
@ -85,6 +90,7 @@ void ADE7953::update() {
|
||||
|
||||
uint32_t val;
|
||||
uint16_t val_16;
|
||||
uint16_t reg;
|
||||
|
||||
// Power factor
|
||||
err = this->ade_read_16(0x010A, &val_16);
|
||||
@ -92,23 +98,36 @@ void ADE7953::update() {
|
||||
err = this->ade_read_16(0x010B, &val_16);
|
||||
ADE_PUBLISH(power_factor_b, (int16_t) val_16, (0x7FFF / 100.0f));
|
||||
|
||||
float pf = ADE_POWER_FACTOR;
|
||||
if (this->use_acc_energy_regs_) {
|
||||
const uint32_t now = millis();
|
||||
const auto diff = now - this->last_update_;
|
||||
this->last_update_ = now;
|
||||
// prevent DIV/0
|
||||
pf = ADE_WATTSEC_POWER_FACTOR * (diff < 10 ? 10 : diff) / 1000;
|
||||
ESP_LOGVV(TAG, "ADE7953::update() diff=%d pf=%f", diff, pf);
|
||||
}
|
||||
|
||||
// Apparent power
|
||||
err = this->ade_read_32(0x0310, &val);
|
||||
ADE_PUBLISH(apparent_power_a, (int32_t) val, 154.0f);
|
||||
err = this->ade_read_32(0x0311, &val);
|
||||
ADE_PUBLISH(apparent_power_b, (int32_t) val, 154.0f);
|
||||
reg = this->use_acc_energy_regs_ ? 0x0322 : 0x0310;
|
||||
err = this->ade_read_32(reg, &val);
|
||||
ADE_PUBLISH(apparent_power_a, (int32_t) val, pf);
|
||||
err = this->ade_read_32(reg + 1, &val);
|
||||
ADE_PUBLISH(apparent_power_b, (int32_t) val, pf);
|
||||
|
||||
// Active power
|
||||
err = this->ade_read_32(0x0312, &val);
|
||||
ADE_PUBLISH(active_power_a, (int32_t) val, 154.0f);
|
||||
err = this->ade_read_32(0x0313, &val);
|
||||
ADE_PUBLISH(active_power_b, (int32_t) val, 154.0f);
|
||||
reg = this->use_acc_energy_regs_ ? 0x031E : 0x0312;
|
||||
err = this->ade_read_32(reg, &val);
|
||||
ADE_PUBLISH(active_power_a, (int32_t) val, pf);
|
||||
err = this->ade_read_32(reg + 1, &val);
|
||||
ADE_PUBLISH(active_power_b, (int32_t) val, pf);
|
||||
|
||||
// Reactive power
|
||||
err = this->ade_read_32(0x0314, &val);
|
||||
ADE_PUBLISH(reactive_power_a, (int32_t) val, 154.0f);
|
||||
err = this->ade_read_32(0x0315, &val);
|
||||
ADE_PUBLISH(reactive_power_b, (int32_t) val, 154.0f);
|
||||
reg = this->use_acc_energy_regs_ ? 0x0320 : 0x0314;
|
||||
err = this->ade_read_32(reg, &val);
|
||||
ADE_PUBLISH(reactive_power_a, (int32_t) val, pf);
|
||||
err = this->ade_read_32(reg + 1, &val);
|
||||
ADE_PUBLISH(reactive_power_b, (int32_t) val, pf);
|
||||
|
||||
// Current
|
||||
err = this->ade_read_32(0x031A, &val);
|
||||
|
@ -52,6 +52,8 @@ class ADE7953 : public PollingComponent, public sensor::Sensor {
|
||||
void set_awgain(uint32_t awgain) { awgain_ = awgain; }
|
||||
void set_bwgain(uint32_t bwgain) { bwgain_ = bwgain; }
|
||||
|
||||
void set_use_acc_energy_regs(bool use_acc_energy_regs) { use_acc_energy_regs_ = use_acc_energy_regs; }
|
||||
|
||||
void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; }
|
||||
void set_frequency_sensor(sensor::Sensor *frequency_sensor) { frequency_sensor_ = frequency_sensor; }
|
||||
|
||||
@ -103,6 +105,8 @@ class ADE7953 : public PollingComponent, public sensor::Sensor {
|
||||
uint32_t bigain_;
|
||||
uint32_t awgain_;
|
||||
uint32_t bwgain_;
|
||||
bool use_acc_energy_regs_{false};
|
||||
uint32_t last_update_;
|
||||
|
||||
virtual bool ade_write_8(uint16_t reg, uint8_t value) = 0;
|
||||
|
||||
|
@ -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];
|
||||
|
@ -15,7 +15,6 @@
|
||||
#include "aht10.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include <cinttypes>
|
||||
|
||||
namespace esphome {
|
||||
namespace aht10 {
|
||||
@ -27,7 +26,7 @@ static const uint8_t AHT10_MEASURE_CMD[] = {0xAC, 0x33, 0x00};
|
||||
static const uint8_t AHT10_SOFTRESET_CMD[] = {0xBA};
|
||||
|
||||
static const uint8_t AHT10_DEFAULT_DELAY = 5; // ms, for initialization and temperature measurement
|
||||
static const uint8_t AHT10_HUMIDITY_DELAY = 30; // ms
|
||||
static const uint8_t AHT10_READ_DELAY = 80; // ms, time to wait for conversion result
|
||||
static const uint8_t AHT10_SOFTRESET_DELAY = 30; // ms
|
||||
|
||||
static const uint8_t AHT10_ATTEMPTS = 3; // safety margin, normally 3 attempts are enough: 3*30=90ms
|
||||
@ -83,74 +82,77 @@ void AHT10Component::setup() {
|
||||
ESP_LOGV(TAG, "AHT10 initialization");
|
||||
}
|
||||
|
||||
void AHT10Component::update() {
|
||||
if (this->write(AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD)) != i2c::ERROR_OK) {
|
||||
ESP_LOGE(TAG, "Communication with AHT10 failed!");
|
||||
this->status_set_warning();
|
||||
void AHT10Component::restart_read_() {
|
||||
if (this->read_count_ == AHT10_ATTEMPTS) {
|
||||
this->read_count_ = 0;
|
||||
this->status_set_error("Measurements reading timed-out!");
|
||||
return;
|
||||
}
|
||||
this->read_count_++;
|
||||
this->set_timeout(AHT10_READ_DELAY, [this]() { this->read_data_(); });
|
||||
}
|
||||
|
||||
void AHT10Component::read_data_() {
|
||||
uint8_t data[6];
|
||||
uint8_t delay_ms = AHT10_DEFAULT_DELAY;
|
||||
if (this->humidity_sensor_ != nullptr)
|
||||
delay_ms = AHT10_HUMIDITY_DELAY;
|
||||
bool success = false;
|
||||
for (int i = 0; i < AHT10_ATTEMPTS; ++i) {
|
||||
ESP_LOGVV(TAG, "Attempt %d at %6" PRIu32, i, millis());
|
||||
delay(delay_ms);
|
||||
if (this->read(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; }
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
|
@ -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) {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome import automation
|
||||
from esphome.const import CONF_ID, CONF_TRIGGER_ID, CONF_FILE, CONF_DEVICE
|
||||
from esphome.const import CONF_ID, CONF_TRIGGER_ID, CONF_FILE, CONF_DEVICE, CONF_VOLUME
|
||||
from esphome.components import uart
|
||||
|
||||
DEPENDENCIES = ["uart"]
|
||||
@ -19,7 +19,6 @@ DFPlayerIsPlayingCondition = dfplayer_ns.class_(
|
||||
MULTI_CONF = True
|
||||
CONF_FOLDER = "folder"
|
||||
CONF_LOOP = "loop"
|
||||
CONF_VOLUME = "volume"
|
||||
CONF_EQ_PRESET = "eq_preset"
|
||||
CONF_ON_FINISHED_PLAYBACK = "on_finished_playback"
|
||||
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
||||
|
0
esphome/components/emmeti/__init__.py
Normal file
0
esphome/components/emmeti/__init__.py
Normal file
21
esphome/components/emmeti/climate.py
Normal file
21
esphome/components/emmeti/climate.py
Normal file
@ -0,0 +1,21 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import climate_ir
|
||||
from esphome.const import CONF_ID
|
||||
|
||||
CODEOWNERS = ["@E440QF"]
|
||||
AUTO_LOAD = ["climate_ir"]
|
||||
|
||||
emmeti_ns = cg.esphome_ns.namespace("emmeti")
|
||||
EmmetiClimate = emmeti_ns.class_("EmmetiClimate", climate_ir.ClimateIR)
|
||||
|
||||
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(EmmetiClimate),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await climate_ir.register_climate_ir(var, config)
|
316
esphome/components/emmeti/emmeti.cpp
Normal file
316
esphome/components/emmeti/emmeti.cpp
Normal file
@ -0,0 +1,316 @@
|
||||
#include "emmeti.h"
|
||||
#include "esphome/components/remote_base/remote_base.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace emmeti {
|
||||
|
||||
static const char *const TAG = "emmeti.climate";
|
||||
|
||||
// setters
|
||||
uint8_t EmmetiClimate::set_temp_() {
|
||||
return (uint8_t) roundf(clamp<float>(this->target_temperature, EMMETI_TEMP_MIN, EMMETI_TEMP_MAX) - EMMETI_TEMP_MIN);
|
||||
}
|
||||
|
||||
uint8_t EmmetiClimate::set_mode_() {
|
||||
switch (this->mode) {
|
||||
case climate::CLIMATE_MODE_COOL:
|
||||
return EMMETI_MODE_COOL;
|
||||
case climate::CLIMATE_MODE_DRY:
|
||||
return EMMETI_MODE_DRY;
|
||||
case climate::CLIMATE_MODE_HEAT:
|
||||
return EMMETI_MODE_HEAT;
|
||||
case climate::CLIMATE_MODE_FAN_ONLY:
|
||||
return EMMETI_MODE_FAN;
|
||||
case climate::CLIMATE_MODE_HEAT_COOL:
|
||||
default:
|
||||
return EMMETI_MODE_HEAT_COOL;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t EmmetiClimate::set_fan_speed_() {
|
||||
switch (this->fan_mode.value()) {
|
||||
case climate::CLIMATE_FAN_LOW:
|
||||
return EMMETI_FAN_1;
|
||||
case climate::CLIMATE_FAN_MEDIUM:
|
||||
return EMMETI_FAN_2;
|
||||
case climate::CLIMATE_FAN_HIGH:
|
||||
return EMMETI_FAN_3;
|
||||
case climate::CLIMATE_FAN_AUTO:
|
||||
default:
|
||||
return EMMETI_FAN_AUTO;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t EmmetiClimate::set_blades_() {
|
||||
if (this->swing_mode == climate::CLIMATE_SWING_VERTICAL) {
|
||||
switch (this->blades_) {
|
||||
case EMMETI_BLADES_1:
|
||||
case EMMETI_BLADES_2:
|
||||
case EMMETI_BLADES_HIGH:
|
||||
this->blades_ = EMMETI_BLADES_HIGH;
|
||||
break;
|
||||
case EMMETI_BLADES_3:
|
||||
case EMMETI_BLADES_MID:
|
||||
this->blades_ = EMMETI_BLADES_MID;
|
||||
break;
|
||||
case EMMETI_BLADES_4:
|
||||
case EMMETI_BLADES_5:
|
||||
case EMMETI_BLADES_LOW:
|
||||
this->blades_ = EMMETI_BLADES_LOW;
|
||||
break;
|
||||
default:
|
||||
this->blades_ = EMMETI_BLADES_FULL;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
switch (this->blades_) {
|
||||
case EMMETI_BLADES_1:
|
||||
case EMMETI_BLADES_2:
|
||||
case EMMETI_BLADES_HIGH:
|
||||
this->blades_ = EMMETI_BLADES_1;
|
||||
break;
|
||||
case EMMETI_BLADES_3:
|
||||
case EMMETI_BLADES_MID:
|
||||
this->blades_ = EMMETI_BLADES_3;
|
||||
break;
|
||||
case EMMETI_BLADES_4:
|
||||
case EMMETI_BLADES_5:
|
||||
case EMMETI_BLADES_LOW:
|
||||
this->blades_ = EMMETI_BLADES_5;
|
||||
break;
|
||||
default:
|
||||
this->blades_ = EMMETI_BLADES_STOP;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return this->blades_;
|
||||
}
|
||||
|
||||
uint8_t EmmetiClimate::gen_checksum_() { return (this->set_temp_() + this->set_mode_() + 2) % 16; }
|
||||
|
||||
// getters
|
||||
float EmmetiClimate::get_temp_(uint8_t temp) { return (float) (temp + EMMETI_TEMP_MIN); }
|
||||
|
||||
climate::ClimateMode EmmetiClimate::get_mode_(uint8_t mode) {
|
||||
switch (mode) {
|
||||
case EMMETI_MODE_COOL:
|
||||
return climate::CLIMATE_MODE_COOL;
|
||||
case EMMETI_MODE_DRY:
|
||||
return climate::CLIMATE_MODE_DRY;
|
||||
case EMMETI_MODE_HEAT:
|
||||
return climate::CLIMATE_MODE_HEAT;
|
||||
case EMMETI_MODE_HEAT_COOL:
|
||||
return climate::CLIMATE_MODE_HEAT_COOL;
|
||||
case EMMETI_MODE_FAN:
|
||||
return climate::CLIMATE_MODE_FAN_ONLY;
|
||||
default:
|
||||
return climate::CLIMATE_MODE_HEAT_COOL;
|
||||
}
|
||||
}
|
||||
|
||||
climate::ClimateFanMode EmmetiClimate::get_fan_speed_(uint8_t fan_speed) {
|
||||
switch (fan_speed) {
|
||||
case EMMETI_FAN_1:
|
||||
return climate::CLIMATE_FAN_LOW;
|
||||
case EMMETI_FAN_2:
|
||||
return climate::CLIMATE_FAN_MEDIUM;
|
||||
case EMMETI_FAN_3:
|
||||
return climate::CLIMATE_FAN_HIGH;
|
||||
case EMMETI_FAN_AUTO:
|
||||
default:
|
||||
return climate::CLIMATE_FAN_AUTO;
|
||||
}
|
||||
}
|
||||
|
||||
climate::ClimateSwingMode EmmetiClimate::get_swing_(uint8_t bitmap) {
|
||||
return (bitmap >> 1) & 0x01 ? climate::CLIMATE_SWING_VERTICAL : climate::CLIMATE_SWING_OFF;
|
||||
}
|
||||
|
||||
template<typename T> T EmmetiClimate::reverse_(T val, size_t len) {
|
||||
T result = 0;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
result |= ((val & 1 << i) != 0) << (len - 1 - i);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
template<typename T> void EmmetiClimate::add_(T val, size_t len, esphome::remote_base::RemoteTransmitData *data) {
|
||||
for (size_t i = len; i > 0; i--) {
|
||||
data->mark(EMMETI_BIT_MARK);
|
||||
data->space((val & (1 << (i - 1))) ? EMMETI_ONE_SPACE : EMMETI_ZERO_SPACE);
|
||||
}
|
||||
}
|
||||
|
||||
template<typename T> void EmmetiClimate::add_(T val, esphome::remote_base::RemoteTransmitData *data) {
|
||||
data->mark(EMMETI_BIT_MARK);
|
||||
data->space((val & 1) ? EMMETI_ONE_SPACE : EMMETI_ZERO_SPACE);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void EmmetiClimate::reverse_add_(T val, size_t len, esphome::remote_base::RemoteTransmitData *data) {
|
||||
this->add_(this->reverse_(val, len), len, data);
|
||||
}
|
||||
|
||||
bool EmmetiClimate::check_checksum_(uint8_t checksum) {
|
||||
uint8_t expected = this->gen_checksum_();
|
||||
ESP_LOGV(TAG, "Expected checksum: %X", expected);
|
||||
ESP_LOGV(TAG, "Checksum received: %X", checksum);
|
||||
|
||||
return checksum == expected;
|
||||
}
|
||||
|
||||
void EmmetiClimate::transmit_state() {
|
||||
auto transmit = this->transmitter_->transmit();
|
||||
auto *data = transmit.get_data();
|
||||
data->set_carrier_frequency(EMMETI_IR_FREQUENCY);
|
||||
|
||||
data->mark(EMMETI_HEADER_MARK);
|
||||
data->space(EMMETI_HEADER_SPACE);
|
||||
|
||||
if (this->mode != climate::CLIMATE_MODE_OFF) {
|
||||
this->reverse_add_(this->set_mode_(), 3, data);
|
||||
this->add_(1, data);
|
||||
this->reverse_add_(this->set_fan_speed_(), 2, data);
|
||||
this->add_(this->swing_mode != climate::CLIMATE_SWING_OFF, data);
|
||||
this->add_(0, data); // sleep mode
|
||||
this->reverse_add_(this->set_temp_(), 4, data);
|
||||
this->add_(0, 8, data); // zeros
|
||||
this->add_(0, data); // turbo mode
|
||||
this->add_(1, data); // light
|
||||
this->add_(1, data); // tree icon thingy
|
||||
this->add_(0, data); // blow mode
|
||||
this->add_(0x52, 11, data); // idk
|
||||
|
||||
data->mark(EMMETI_BIT_MARK);
|
||||
data->space(EMMETI_MESSAGE_SPACE);
|
||||
|
||||
this->reverse_add_(this->set_blades_(), 4, data);
|
||||
this->add_(0, 4, data); // zeros
|
||||
this->reverse_add_(2, 2, data); // thermometer
|
||||
this->add_(0, 18, data); // zeros
|
||||
this->reverse_add_(this->gen_checksum_(), 4, data);
|
||||
} else {
|
||||
this->add_(9, 12, data);
|
||||
this->add_(0, 8, data);
|
||||
this->add_(0x2052, 15, data);
|
||||
data->mark(EMMETI_BIT_MARK);
|
||||
data->space(EMMETI_MESSAGE_SPACE);
|
||||
this->add_(0, 8, data);
|
||||
this->add_(1, 2, data);
|
||||
this->add_(0, 18, data);
|
||||
this->add_(0x0C, 4, data);
|
||||
}
|
||||
data->mark(EMMETI_BIT_MARK);
|
||||
data->space(0);
|
||||
|
||||
transmit.perform();
|
||||
}
|
||||
|
||||
bool EmmetiClimate::parse_state_frame_(EmmetiState curr_state) {
|
||||
this->mode = this->get_mode_(curr_state.mode);
|
||||
this->fan_mode = this->get_fan_speed_(curr_state.fan_speed);
|
||||
this->target_temperature = this->get_temp_(curr_state.temp);
|
||||
this->swing_mode = this->get_swing_(curr_state.bitmap);
|
||||
// this->blades_ = curr_state.fan_pos;
|
||||
if (!(curr_state.bitmap & 0x01)) {
|
||||
this->mode = climate::CLIMATE_MODE_OFF;
|
||||
}
|
||||
|
||||
this->publish_state();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool EmmetiClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
if (!data.expect_item(EMMETI_HEADER_MARK, EMMETI_HEADER_SPACE)) {
|
||||
return false;
|
||||
}
|
||||
ESP_LOGD(TAG, "Received emmeti frame");
|
||||
|
||||
EmmetiState curr_state;
|
||||
|
||||
for (size_t pos = 0; pos < 3; pos++) {
|
||||
if (data.expect_item(EMMETI_BIT_MARK, EMMETI_ONE_SPACE)) {
|
||||
curr_state.mode |= 1 << pos;
|
||||
} else if (!data.expect_item(EMMETI_BIT_MARK, EMMETI_ZERO_SPACE)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Mode: %d", curr_state.mode);
|
||||
|
||||
if (data.expect_item(EMMETI_BIT_MARK, EMMETI_ONE_SPACE)) {
|
||||
curr_state.bitmap |= 1 << 0;
|
||||
} else if (!data.expect_item(EMMETI_BIT_MARK, EMMETI_ZERO_SPACE)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "On: %d", curr_state.bitmap & 0x01);
|
||||
|
||||
for (size_t pos = 0; pos < 2; pos++) {
|
||||
if (data.expect_item(EMMETI_BIT_MARK, EMMETI_ONE_SPACE)) {
|
||||
curr_state.fan_speed |= 1 << pos;
|
||||
} else if (!data.expect_item(EMMETI_BIT_MARK, EMMETI_ZERO_SPACE)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Fan speed: %d", curr_state.fan_speed);
|
||||
|
||||
for (size_t pos = 0; pos < 2; pos++) {
|
||||
if (data.expect_item(EMMETI_BIT_MARK, EMMETI_ONE_SPACE)) {
|
||||
curr_state.bitmap |= 1 << (pos + 1);
|
||||
} else if (!data.expect_item(EMMETI_BIT_MARK, EMMETI_ZERO_SPACE)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Swing: %d", (curr_state.bitmap >> 1) & 0x01);
|
||||
ESP_LOGD(TAG, "Sleep: %d", (curr_state.bitmap >> 2) & 0x01);
|
||||
|
||||
for (size_t pos = 0; pos < 4; pos++) {
|
||||
if (data.expect_item(EMMETI_BIT_MARK, EMMETI_ONE_SPACE)) {
|
||||
curr_state.temp |= 1 << pos;
|
||||
} else if (!data.expect_item(EMMETI_BIT_MARK, EMMETI_ZERO_SPACE)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Temp: %d", curr_state.temp);
|
||||
|
||||
for (size_t pos = 0; pos < 8; pos++) {
|
||||
if (!data.expect_item(EMMETI_BIT_MARK, EMMETI_ZERO_SPACE)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t pos = 0; pos < 4; pos++) {
|
||||
if (data.expect_item(EMMETI_BIT_MARK, EMMETI_ONE_SPACE)) {
|
||||
curr_state.bitmap |= 1 << (pos + 3);
|
||||
} else if (!data.expect_item(EMMETI_BIT_MARK, EMMETI_ZERO_SPACE)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Turbo: %d", (curr_state.bitmap >> 3) & 0x01);
|
||||
ESP_LOGD(TAG, "Light: %d", (curr_state.bitmap >> 4) & 0x01);
|
||||
ESP_LOGD(TAG, "Tree: %d", (curr_state.bitmap >> 5) & 0x01);
|
||||
ESP_LOGD(TAG, "Blow: %d", (curr_state.bitmap >> 6) & 0x01);
|
||||
|
||||
uint16_t control_data = 0;
|
||||
for (size_t pos = 0; pos < 11; pos++) {
|
||||
if (data.expect_item(EMMETI_BIT_MARK, EMMETI_ONE_SPACE)) {
|
||||
control_data |= 1 << pos;
|
||||
} else if (!data.expect_item(EMMETI_BIT_MARK, EMMETI_ZERO_SPACE)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (control_data != 0x250) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this->parse_state_frame_(curr_state);
|
||||
}
|
||||
|
||||
} // namespace emmeti
|
||||
} // namespace esphome
|
109
esphome/components/emmeti/emmeti.h
Normal file
109
esphome/components/emmeti/emmeti.h
Normal file
@ -0,0 +1,109 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/climate_ir/climate_ir.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace emmeti {
|
||||
|
||||
const uint8_t EMMETI_TEMP_MIN = 16; // Celsius
|
||||
const uint8_t EMMETI_TEMP_MAX = 30; // Celsius
|
||||
|
||||
// Modes
|
||||
|
||||
enum EmmetiMode : uint8_t {
|
||||
EMMETI_MODE_HEAT_COOL = 0x00,
|
||||
EMMETI_MODE_COOL = 0x01,
|
||||
EMMETI_MODE_DRY = 0x02,
|
||||
EMMETI_MODE_FAN = 0x03,
|
||||
EMMETI_MODE_HEAT = 0x04,
|
||||
};
|
||||
|
||||
// Fan Speed
|
||||
|
||||
enum EmmetiFanMode : uint8_t {
|
||||
EMMETI_FAN_AUTO = 0x00,
|
||||
EMMETI_FAN_1 = 0x01,
|
||||
EMMETI_FAN_2 = 0x02,
|
||||
EMMETI_FAN_3 = 0x03,
|
||||
};
|
||||
|
||||
// Fan Position
|
||||
|
||||
enum EmmetiBlades : uint8_t {
|
||||
EMMETI_BLADES_STOP = 0x00,
|
||||
EMMETI_BLADES_FULL = 0x01,
|
||||
EMMETI_BLADES_1 = 0x02,
|
||||
EMMETI_BLADES_2 = 0x03,
|
||||
EMMETI_BLADES_3 = 0x04,
|
||||
EMMETI_BLADES_4 = 0x05,
|
||||
EMMETI_BLADES_5 = 0x06,
|
||||
EMMETI_BLADES_LOW = 0x07,
|
||||
EMMETI_BLADES_MID = 0x09,
|
||||
EMMETI_BLADES_HIGH = 0x11,
|
||||
};
|
||||
|
||||
// IR Transmission
|
||||
const uint32_t EMMETI_IR_FREQUENCY = 38000;
|
||||
const uint32_t EMMETI_HEADER_MARK = 9076;
|
||||
const uint32_t EMMETI_HEADER_SPACE = 4408;
|
||||
const uint32_t EMMETI_BIT_MARK = 660;
|
||||
const uint32_t EMMETI_ONE_SPACE = 1630;
|
||||
const uint32_t EMMETI_ZERO_SPACE = 530;
|
||||
const uint32_t EMMETI_MESSAGE_SPACE = 20000;
|
||||
|
||||
struct EmmetiState {
|
||||
uint8_t mode = 0;
|
||||
uint8_t bitmap = 0;
|
||||
uint8_t fan_speed = 0;
|
||||
uint8_t temp = 0;
|
||||
uint8_t fan_pos = 0;
|
||||
uint8_t th = 0;
|
||||
uint8_t checksum = 0;
|
||||
};
|
||||
|
||||
class EmmetiClimate : public climate_ir::ClimateIR {
|
||||
public:
|
||||
EmmetiClimate()
|
||||
: climate_ir::ClimateIR(EMMETI_TEMP_MIN, EMMETI_TEMP_MAX, 1.0f, true, true,
|
||||
{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM,
|
||||
climate::CLIMATE_FAN_HIGH},
|
||||
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}) {}
|
||||
|
||||
protected:
|
||||
// Transmit via IR the state of this climate controller
|
||||
void transmit_state() override;
|
||||
// Handle received IR Buffer
|
||||
bool on_receive(remote_base::RemoteReceiveData data) override;
|
||||
bool parse_state_frame_(EmmetiState curr_state);
|
||||
|
||||
// setters
|
||||
uint8_t set_mode_();
|
||||
uint8_t set_temp_();
|
||||
uint8_t set_fan_speed_();
|
||||
uint8_t gen_checksum_();
|
||||
uint8_t set_blades_();
|
||||
|
||||
// getters
|
||||
climate::ClimateMode get_mode_(uint8_t mode);
|
||||
climate::ClimateFanMode get_fan_speed_(uint8_t fan);
|
||||
void get_blades_(uint8_t fanpos);
|
||||
// get swing
|
||||
climate::ClimateSwingMode get_swing_(uint8_t bitmap);
|
||||
float get_temp_(uint8_t temp);
|
||||
|
||||
// check if the received frame is valid
|
||||
bool check_checksum_(uint8_t checksum);
|
||||
|
||||
template<typename T> T reverse_(T val, size_t len);
|
||||
|
||||
template<typename T> void add_(T val, size_t len, esphome::remote_base::RemoteTransmitData *ata);
|
||||
|
||||
template<typename T> void add_(T val, esphome::remote_base::RemoteTransmitData *data);
|
||||
|
||||
template<typename T> void reverse_add_(T val, size_t len, esphome::remote_base::RemoteTransmitData *data);
|
||||
|
||||
uint8_t blades_ = EMMETI_BLADES_STOP;
|
||||
};
|
||||
|
||||
} // namespace emmeti
|
||||
} // namespace esphome
|
@ -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))
|
||||
|
@ -1,7 +1,13 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import i2c
|
||||
from esphome.const import CONF_ADDRESS, CONF_COMMAND, CONF_ID, CONF_DURATION
|
||||
from esphome.const import (
|
||||
CONF_ADDRESS,
|
||||
CONF_COMMAND,
|
||||
CONF_ID,
|
||||
CONF_DURATION,
|
||||
CONF_VOLUME,
|
||||
)
|
||||
from esphome import automation
|
||||
from esphome.automation import maybe_simple_id
|
||||
|
||||
@ -9,7 +15,6 @@ CODEOWNERS = ["@carlos-sarmiento"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
MULTI_CONF = True
|
||||
|
||||
CONF_VOLUME = "volume"
|
||||
CONF_VOLUME_PER_MINUTE = "volume_per_minute"
|
||||
|
||||
ezo_pmp_ns = cg.esphome_ns.namespace("ezo_pmp")
|
||||
|
@ -1,13 +1,15 @@
|
||||
import hashlib
|
||||
import logging
|
||||
|
||||
import functools
|
||||
from pathlib import Path
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
from packaging import version
|
||||
|
||||
import requests
|
||||
|
||||
from esphome import core
|
||||
from esphome import external_files
|
||||
import esphome.config_validation as cv
|
||||
import esphome.codegen as cg
|
||||
from esphome.helpers import (
|
||||
@ -15,21 +17,26 @@ from esphome.helpers import (
|
||||
cpp_string_escape,
|
||||
)
|
||||
from esphome.const import (
|
||||
__version__,
|
||||
CONF_FAMILY,
|
||||
CONF_FILE,
|
||||
CONF_GLYPHS,
|
||||
CONF_ID,
|
||||
CONF_RAW_DATA_ID,
|
||||
CONF_TYPE,
|
||||
CONF_REFRESH,
|
||||
CONF_SIZE,
|
||||
CONF_PATH,
|
||||
CONF_WEIGHT,
|
||||
CONF_URL,
|
||||
)
|
||||
from esphome.core import (
|
||||
CORE,
|
||||
HexInt,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "font"
|
||||
DEPENDENCIES = ["display"]
|
||||
MULTI_CONF = True
|
||||
@ -125,20 +132,10 @@ def validate_truetype_file(value):
|
||||
return cv.file_(value)
|
||||
|
||||
|
||||
def _compute_local_font_dir(name) -> Path:
|
||||
h = hashlib.new("sha256")
|
||||
h.update(name.encode())
|
||||
return Path(CORE.data_dir) / DOMAIN / h.hexdigest()[:8]
|
||||
|
||||
|
||||
def _compute_gfonts_local_path(value) -> Path:
|
||||
name = f"{value[CONF_FAMILY]}@{value[CONF_WEIGHT]}@{value[CONF_ITALIC]}@v1"
|
||||
return _compute_local_font_dir(name) / "font.ttf"
|
||||
|
||||
|
||||
TYPE_LOCAL = "local"
|
||||
TYPE_LOCAL_BITMAP = "local_bitmap"
|
||||
TYPE_GFONTS = "gfonts"
|
||||
TYPE_WEB = "web"
|
||||
LOCAL_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_PATH): validate_truetype_file,
|
||||
@ -169,21 +166,64 @@ def validate_weight_name(value):
|
||||
return FONT_WEIGHTS[cv.one_of(*FONT_WEIGHTS, lower=True, space="-")(value)]
|
||||
|
||||
|
||||
def download_gfonts(value):
|
||||
def _compute_local_font_path(value: dict) -> Path:
|
||||
url = value[CONF_URL]
|
||||
h = hashlib.new("sha256")
|
||||
h.update(url.encode())
|
||||
key = h.hexdigest()[:8]
|
||||
base_dir = external_files.compute_local_file_dir(DOMAIN)
|
||||
_LOGGER.debug("_compute_local_font_path: base_dir=%s", base_dir / key)
|
||||
return base_dir / key
|
||||
|
||||
|
||||
def get_font_path(value, type) -> Path:
|
||||
if type == TYPE_GFONTS:
|
||||
name = f"{value[CONF_FAMILY]}@{value[CONF_WEIGHT]}@{value[CONF_ITALIC]}@v1"
|
||||
return external_files.compute_local_file_dir(DOMAIN) / f"{name}.ttf"
|
||||
if type == TYPE_WEB:
|
||||
return _compute_local_font_path(value) / "font.ttf"
|
||||
return None
|
||||
|
||||
|
||||
def download_content(url: str, path: Path) -> None:
|
||||
if not external_files.has_remote_file_changed(url, path):
|
||||
_LOGGER.debug("Remote file has not changed %s", url)
|
||||
return
|
||||
|
||||
_LOGGER.debug(
|
||||
"Remote file has changed, downloading from %s to %s",
|
||||
url,
|
||||
path,
|
||||
)
|
||||
|
||||
try:
|
||||
req = requests.get(
|
||||
url,
|
||||
timeout=external_files.NETWORK_TIMEOUT,
|
||||
headers={"User-agent": f"ESPHome/{__version__} (https://esphome.io)"},
|
||||
)
|
||||
req.raise_for_status()
|
||||
except requests.exceptions.RequestException as e:
|
||||
raise cv.Invalid(f"Could not download from {url}: {e}")
|
||||
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_bytes(req.content)
|
||||
|
||||
|
||||
def download_gfont(value):
|
||||
name = (
|
||||
f"{value[CONF_FAMILY]}:ital,wght@{int(value[CONF_ITALIC])},{value[CONF_WEIGHT]}"
|
||||
)
|
||||
url = f"https://fonts.googleapis.com/css2?family={name}"
|
||||
path = get_font_path(value, TYPE_GFONTS)
|
||||
_LOGGER.debug("download_gfont: path=%s", path)
|
||||
|
||||
path = _compute_gfonts_local_path(value)
|
||||
if path.is_file():
|
||||
return value
|
||||
try:
|
||||
req = requests.get(url, timeout=30)
|
||||
req = requests.get(url, timeout=external_files.NETWORK_TIMEOUT)
|
||||
req.raise_for_status()
|
||||
except requests.exceptions.RequestException as e:
|
||||
raise cv.Invalid(
|
||||
f"Could not download font for {name}, please check the fonts exists "
|
||||
f"Could not download font at {url}, please check the fonts exists "
|
||||
f"at google fonts ({e})"
|
||||
)
|
||||
match = re.search(r"src:\s+url\((.+)\)\s+format\('truetype'\);", req.text)
|
||||
@ -194,26 +234,48 @@ def download_gfonts(value):
|
||||
)
|
||||
|
||||
ttf_url = match.group(1)
|
||||
try:
|
||||
req = requests.get(ttf_url, timeout=30)
|
||||
req.raise_for_status()
|
||||
except requests.exceptions.RequestException as e:
|
||||
raise cv.Invalid(f"Could not download ttf file for {name} ({ttf_url}): {e}")
|
||||
_LOGGER.debug("download_gfont: ttf_url=%s", ttf_url)
|
||||
|
||||
path.parent.mkdir(exist_ok=True, parents=True)
|
||||
path.write_bytes(req.content)
|
||||
download_content(ttf_url, path)
|
||||
return value
|
||||
|
||||
|
||||
GFONTS_SCHEMA = cv.All(
|
||||
def download_web_font(value):
|
||||
url = value[CONF_URL]
|
||||
path = get_font_path(value, TYPE_WEB)
|
||||
|
||||
download_content(url, path)
|
||||
_LOGGER.debug("download_web_font: path=%s", path)
|
||||
return value
|
||||
|
||||
|
||||
EXTERNAL_FONT_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_FAMILY): cv.string_strict,
|
||||
cv.Optional(CONF_WEIGHT, default="regular"): cv.Any(
|
||||
cv.int_, validate_weight_name
|
||||
),
|
||||
cv.Optional(CONF_ITALIC, default=False): cv.boolean,
|
||||
},
|
||||
download_gfonts,
|
||||
cv.Optional(CONF_REFRESH, default="1d"): cv.All(cv.string, cv.source_refresh),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
GFONTS_SCHEMA = cv.All(
|
||||
EXTERNAL_FONT_SCHEMA.extend(
|
||||
{
|
||||
cv.Required(CONF_FAMILY): cv.string_strict,
|
||||
}
|
||||
),
|
||||
download_gfont,
|
||||
)
|
||||
|
||||
WEB_FONT_SCHEMA = cv.All(
|
||||
EXTERNAL_FONT_SCHEMA.extend(
|
||||
{
|
||||
cv.Required(CONF_URL): cv.string_strict,
|
||||
}
|
||||
),
|
||||
download_web_font,
|
||||
)
|
||||
|
||||
|
||||
@ -233,6 +295,14 @@ def validate_file_shorthand(value):
|
||||
data[CONF_WEIGHT] = weight[1:]
|
||||
return FILE_SCHEMA(data)
|
||||
|
||||
if value.startswith("http://") or value.startswith("https://"):
|
||||
return FILE_SCHEMA(
|
||||
{
|
||||
CONF_TYPE: TYPE_WEB,
|
||||
CONF_URL: value,
|
||||
}
|
||||
)
|
||||
|
||||
if value.endswith(".pcf") or value.endswith(".bdf"):
|
||||
return FILE_SCHEMA(
|
||||
{
|
||||
@ -254,6 +324,7 @@ TYPED_FILE_SCHEMA = cv.typed_schema(
|
||||
TYPE_LOCAL: LOCAL_SCHEMA,
|
||||
TYPE_GFONTS: GFONTS_SCHEMA,
|
||||
TYPE_LOCAL_BITMAP: LOCAL_BITMAP_SCHEMA,
|
||||
TYPE_WEB: WEB_FONT_SCHEMA,
|
||||
}
|
||||
)
|
||||
|
||||
@ -264,7 +335,7 @@ def _file_schema(value):
|
||||
return TYPED_FILE_SCHEMA(value)
|
||||
|
||||
|
||||
FILE_SCHEMA = cv.Schema(_file_schema)
|
||||
FILE_SCHEMA = cv.All(_file_schema)
|
||||
|
||||
DEFAULT_GLYPHS = (
|
||||
' !"%()+=,-.:/?0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°'
|
||||
@ -288,7 +359,7 @@ FONT_SCHEMA = cv.Schema(
|
||||
),
|
||||
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
|
||||
cv.GenerateID(CONF_RAW_GLYPH_ID): cv.declare_id(GlyphData),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(validate_pillow_installed, FONT_SCHEMA, merge_glyphs)
|
||||
@ -343,8 +414,8 @@ class EFont:
|
||||
elif ftype == TYPE_LOCAL:
|
||||
path = CORE.relative_config_path(file[CONF_PATH])
|
||||
font = load_ttf_font(path, size)
|
||||
elif ftype == TYPE_GFONTS:
|
||||
path = _compute_gfonts_local_path(file)
|
||||
elif ftype in (TYPE_GFONTS, TYPE_WEB):
|
||||
path = get_font_path(file, ftype)
|
||||
font = load_ttf_font(path, size)
|
||||
else:
|
||||
raise cv.Invalid(f"Could not load font: unknown type: {ftype}")
|
||||
@ -361,9 +432,9 @@ def convert_bitmap_to_pillow_font(filepath):
|
||||
BdfFontFile,
|
||||
)
|
||||
|
||||
local_bitmap_font_file = _compute_local_font_dir(filepath) / os.path.basename(
|
||||
filepath
|
||||
)
|
||||
local_bitmap_font_file = external_files.compute_local_file_dir(
|
||||
DOMAIN,
|
||||
) / os.path.basename(filepath)
|
||||
|
||||
copy_file_if_changed(filepath, local_bitmap_font_file)
|
||||
|
||||
|
@ -71,7 +71,7 @@ class FT5x06Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice
|
||||
this->x_raw_max_ = this->display_->get_native_width();
|
||||
}
|
||||
if (this->y_raw_max_ == this->y_raw_min_) {
|
||||
this->x_raw_max_ = this->display_->get_native_height();
|
||||
this->y_raw_max_ = this->display_->get_native_height();
|
||||
}
|
||||
}
|
||||
esph_log_config(TAG, "FT5x06 Touchscreen setup complete");
|
||||
|
1
esphome/components/htu31d/__init__.py
Normal file
1
esphome/components/htu31d/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
CODEOWNERS = ["@betterengineering"]
|
269
esphome/components/htu31d/htu31d.cpp
Normal file
269
esphome/components/htu31d/htu31d.cpp
Normal file
@ -0,0 +1,269 @@
|
||||
/*
|
||||
* This file contains source code derived from Adafruit_HTU31D which is under
|
||||
* the BSD license:
|
||||
* Written by Limor Fried/Ladyada for Adafruit Industries.
|
||||
* BSD license, all text above must be included in any redistribution.
|
||||
*
|
||||
* Modifications made by Mark Spicer.
|
||||
*/
|
||||
|
||||
#include "htu31d.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace htu31d {
|
||||
|
||||
/** Logging prefix */
|
||||
static const char *const TAG = "htu31d";
|
||||
|
||||
/** Default I2C address for the HTU31D. */
|
||||
static const uint8_t HTU31D_DEFAULT_I2CADDR = 0x40;
|
||||
|
||||
/** Read temperature and humidity. */
|
||||
static const uint8_t HTU31D_READTEMPHUM = 0x00;
|
||||
|
||||
/** Start a conversion! */
|
||||
static const uint8_t HTU31D_CONVERSION = 0x40;
|
||||
|
||||
/** Read serial number command. */
|
||||
static const uint8_t HTU31D_READSERIAL = 0x0A;
|
||||
|
||||
/** Enable heater */
|
||||
static const uint8_t HTU31D_HEATERON = 0x04;
|
||||
|
||||
/** Disable heater */
|
||||
static const uint8_t HTU31D_HEATEROFF = 0x02;
|
||||
|
||||
/** Reset command. */
|
||||
static const uint8_t HTU31D_RESET = 0x1E;
|
||||
|
||||
/** Diagnostics command. */
|
||||
static const uint8_t HTU31D_DIAGNOSTICS = 0x08;
|
||||
|
||||
/**
|
||||
* Computes a CRC result for the provided input.
|
||||
*
|
||||
* @returns the computed CRC result for the provided input
|
||||
*/
|
||||
uint8_t compute_crc(uint32_t value) {
|
||||
uint32_t polynom = 0x98800000; // x^8 + x^5 + x^4 + 1
|
||||
uint32_t msb = 0x80000000;
|
||||
uint32_t mask = 0xFF800000;
|
||||
uint32_t threshold = 0x00000080;
|
||||
uint32_t result = value;
|
||||
|
||||
while (msb != threshold) {
|
||||
// Check if msb of current value is 1 and apply XOR mask
|
||||
if (result & msb)
|
||||
result = ((result ^ polynom) & mask) | (result & ~mask);
|
||||
|
||||
// Shift by one
|
||||
msb >>= 1;
|
||||
mask >>= 1;
|
||||
polynom >>= 1;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the sensor and ensures that the devices serial number can be read over
|
||||
* I2C.
|
||||
*/
|
||||
void HTU31DComponent::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up esphome/components/htu31d HTU31D...");
|
||||
|
||||
if (!this->reset_()) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->read_serial_num_() == 0) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called once every update interval (user configured, defaults to 60s) and sets
|
||||
* the current temperature and humidity.
|
||||
*/
|
||||
void HTU31DComponent::update() {
|
||||
ESP_LOGD(TAG, "Checking temperature and humidty values");
|
||||
|
||||
// Trigger a conversion. From the spec sheet: The conversion command triggers
|
||||
// a single temperature and humidity conversion.
|
||||
if (this->write_register(HTU31D_CONVERSION, nullptr, 0) != i2c::ERROR_OK) {
|
||||
this->status_set_warning();
|
||||
ESP_LOGE(TAG, "Received errror writing conversion register");
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait conversion time.
|
||||
this->set_timeout(20, [this]() {
|
||||
uint8_t thdata[6];
|
||||
if (this->read_register(HTU31D_READTEMPHUM, thdata, 6) != i2c::ERROR_OK) {
|
||||
this->status_set_warning();
|
||||
ESP_LOGE(TAG, "Error reading temperature/humidty register");
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate temperature value.
|
||||
uint16_t raw_temp = encode_uint16(thdata[0], thdata[1]);
|
||||
|
||||
uint8_t crc = compute_crc((uint32_t) raw_temp << 8);
|
||||
if (crc != thdata[2]) {
|
||||
this->status_set_warning();
|
||||
ESP_LOGE(TAG, "Error validating temperature CRC");
|
||||
return;
|
||||
}
|
||||
|
||||
float temperature = raw_temp;
|
||||
temperature /= 65535.0f;
|
||||
temperature *= 165;
|
||||
temperature -= 40;
|
||||
|
||||
if (this->temperature_ != nullptr) {
|
||||
this->temperature_->publish_state(temperature);
|
||||
}
|
||||
|
||||
// Calculate humidty value.
|
||||
uint16_t raw_hum = encode_uint16(thdata[3], thdata[4]);
|
||||
|
||||
crc = compute_crc((uint32_t) raw_hum << 8);
|
||||
if (crc != thdata[5]) {
|
||||
this->status_set_warning();
|
||||
ESP_LOGE(TAG, "Error validating humidty CRC");
|
||||
return;
|
||||
}
|
||||
|
||||
float humidity = raw_hum;
|
||||
humidity /= 65535.0f;
|
||||
humidity *= 100;
|
||||
|
||||
if (this->humidity_ != nullptr) {
|
||||
this->humidity_->publish_state(humidity);
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Got Temperature=%.1f°C Humidity=%.1f%%", temperature, humidity);
|
||||
this->status_clear_warning();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs the current compoenent config.
|
||||
*/
|
||||
void HTU31DComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "HTU31D:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, "Communication with HTU31D failed!");
|
||||
}
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_);
|
||||
LOG_SENSOR(" ", "Humidity", this->humidity_);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a 'reset' request to the HTU31D, followed by a 15ms delay.
|
||||
*
|
||||
* @returns True if was able to write the command successfully
|
||||
*/
|
||||
bool HTU31DComponent::reset_() {
|
||||
if (this->write_register(HTU31D_RESET, nullptr, 0) != i2c::ERROR_OK) {
|
||||
return false;
|
||||
}
|
||||
|
||||
delay(15);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the serial number from the device and checks the CRC.
|
||||
*
|
||||
* @returns the 24bit serial number from the device
|
||||
*/
|
||||
uint32_t HTU31DComponent::read_serial_num_() {
|
||||
uint8_t reply[4];
|
||||
uint32_t serial = 0;
|
||||
uint8_t padding = 0;
|
||||
|
||||
// Verify we can read the device serial.
|
||||
if (this->read_register(HTU31D_READSERIAL, reply, 4) != i2c::ERROR_OK) {
|
||||
ESP_LOGE(TAG, "Error reading device serial");
|
||||
return 0;
|
||||
}
|
||||
|
||||
serial = encode_uint32(reply[0], reply[1], reply[2], padding);
|
||||
|
||||
uint8_t crc = compute_crc(serial);
|
||||
if (crc != reply[3]) {
|
||||
ESP_LOGE(TAG, "Error validating serial CRC");
|
||||
return 0;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Found serial: 0x%X", serial);
|
||||
|
||||
return serial;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the diagnostics register to determine if the heater is currently
|
||||
* enabled.
|
||||
*
|
||||
* @returns True if the heater is currently enabled, False otherwise
|
||||
*/
|
||||
bool HTU31DComponent::is_heater_enabled() {
|
||||
uint8_t reply[1];
|
||||
uint8_t heater_enabled_position = 0;
|
||||
uint8_t mask = 1 << heater_enabled_position;
|
||||
uint8_t diagnostics = 0;
|
||||
|
||||
if (this->read_register(HTU31D_DIAGNOSTICS, reply, 1) != i2c::ERROR_OK) {
|
||||
ESP_LOGE(TAG, "Error reading device serial");
|
||||
return false;
|
||||
}
|
||||
|
||||
diagnostics = reply[0];
|
||||
return (diagnostics & mask) != 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the heater state on or off.
|
||||
*
|
||||
* @param desired True for on, and False for off.
|
||||
*/
|
||||
void HTU31DComponent::set_heater_state(bool desired) {
|
||||
bool current = this->is_heater_enabled();
|
||||
|
||||
// If the current state matches the desired state, there is nothing to do.
|
||||
if (current == desired) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update heater state.
|
||||
esphome::i2c::ErrorCode err;
|
||||
if (desired) {
|
||||
err = this->write_register(HTU31D_HEATERON, nullptr, 0);
|
||||
} else {
|
||||
err = this->write_register(HTU31D_HEATEROFF, nullptr, 0);
|
||||
}
|
||||
|
||||
// Record any error.
|
||||
if (err != i2c::ERROR_OK) {
|
||||
this->status_set_warning();
|
||||
ESP_LOGE(TAG, "Received error updating heater state");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the startup priority for this component.
|
||||
*
|
||||
* @returns The startup priority
|
||||
*/
|
||||
float HTU31DComponent::get_setup_priority() const { return setup_priority::DATA; }
|
||||
} // namespace htu31d
|
||||
} // namespace esphome
|
33
esphome/components/htu31d/htu31d.h
Normal file
33
esphome/components/htu31d/htu31d.h
Normal file
@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace htu31d {
|
||||
|
||||
class HTU31DComponent : public PollingComponent, public i2c::I2CDevice {
|
||||
public:
|
||||
void setup() override; /// Setup (reset) the sensor and check connection.
|
||||
void update() override; /// Update the sensor values (temperature+humidity).
|
||||
void dump_config() override; /// Dumps the configuration values.
|
||||
|
||||
void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; }
|
||||
void set_humidity(sensor::Sensor *humidity) { this->humidity_ = humidity; }
|
||||
|
||||
void set_heater_state(bool desired);
|
||||
bool is_heater_enabled();
|
||||
|
||||
float get_setup_priority() const override;
|
||||
|
||||
protected:
|
||||
bool reset_();
|
||||
uint32_t read_serial_num_();
|
||||
|
||||
sensor::Sensor *temperature_{nullptr};
|
||||
sensor::Sensor *humidity_{nullptr};
|
||||
};
|
||||
} // namespace htu31d
|
||||
} // namespace esphome
|
56
esphome/components/htu31d/sensor.py
Normal file
56
esphome/components/htu31d/sensor.py
Normal file
@ -0,0 +1,56 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import i2c, sensor
|
||||
from esphome.const import (
|
||||
CONF_HUMIDITY,
|
||||
CONF_ID,
|
||||
CONF_TEMPERATURE,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_PERCENT,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
htu31d_ns = cg.esphome_ns.namespace("htu31d")
|
||||
HTU31DComponent = htu31d_ns.class_(
|
||||
"HTU31DComponent", cg.PollingComponent, i2c.I2CDevice
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(HTU31DComponent),
|
||||
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
.extend(i2c.i2c_device_schema(0x40))
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
if temperature_config := config.get(CONF_TEMPERATURE):
|
||||
sens = await sensor.new_sensor(temperature_config)
|
||||
cg.add(var.set_temperature(sens))
|
||||
|
||||
if humidity_config := config.get(CONF_HUMIDITY):
|
||||
sens = await sensor.new_sensor(humidity_config)
|
||||
cg.add(var.set_humidity(sens))
|
@ -55,10 +55,10 @@ void INA226Component::setup() {
|
||||
config.avg_samples = this->adc_avg_samples_;
|
||||
|
||||
// Bus Voltage Conversion Time VBUSCT Bit Settings [8:6] (100 -> 1.1ms, 111 -> 8.244 ms)
|
||||
config.bus_voltage_conversion_time = this->adc_time_;
|
||||
config.bus_voltage_conversion_time = this->adc_time_voltage_;
|
||||
|
||||
// Shunt Voltage Conversion Time VSHCT Bit Settings [5:3] (100 -> 1.1ms, 111 -> 8.244 ms)
|
||||
config.shunt_voltage_conversion_time = this->adc_time_;
|
||||
config.shunt_voltage_conversion_time = this->adc_time_current_;
|
||||
|
||||
// Mode Settings [2:0] Combinations (111 -> Shunt and Bus, Continuous)
|
||||
config.mode = 0b111;
|
||||
@ -93,7 +93,8 @@ void INA226Component::dump_config() {
|
||||
}
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
|
||||
ESP_LOGCONFIG(TAG, " ADC Conversion Time: %d", INA226_ADC_TIMES[this->adc_time_ & 0b111]);
|
||||
ESP_LOGCONFIG(TAG, " ADC Conversion Time Bus Voltage: %d", INA226_ADC_TIMES[this->adc_time_voltage_ & 0b111]);
|
||||
ESP_LOGCONFIG(TAG, " ADC Conversion Time Shunt Voltage: %d", INA226_ADC_TIMES[this->adc_time_current_ & 0b111]);
|
||||
ESP_LOGCONFIG(TAG, " ADC Averaging Samples: %d", INA226_ADC_AVG_SAMPLES[this->adc_avg_samples_ & 0b111]);
|
||||
|
||||
LOG_SENSOR(" ", "Bus Voltage", this->bus_voltage_sensor_);
|
||||
|
@ -50,7 +50,8 @@ class INA226Component : public PollingComponent, public i2c::I2CDevice {
|
||||
|
||||
void set_shunt_resistance_ohm(float shunt_resistance_ohm) { shunt_resistance_ohm_ = shunt_resistance_ohm; }
|
||||
void set_max_current_a(float max_current_a) { max_current_a_ = max_current_a; }
|
||||
void set_adc_time(AdcTime time) { adc_time_ = time; }
|
||||
void set_adc_time_voltage(AdcTime time) { adc_time_voltage_ = time; }
|
||||
void set_adc_time_current(AdcTime time) { adc_time_current_ = time; }
|
||||
void set_adc_avg_samples(AdcAvgSamples samples) { adc_avg_samples_ = samples; }
|
||||
|
||||
void set_bus_voltage_sensor(sensor::Sensor *bus_voltage_sensor) { bus_voltage_sensor_ = bus_voltage_sensor; }
|
||||
@ -61,7 +62,8 @@ class INA226Component : public PollingComponent, public i2c::I2CDevice {
|
||||
protected:
|
||||
float shunt_resistance_ohm_;
|
||||
float max_current_a_;
|
||||
AdcTime adc_time_{AdcTime::ADC_TIME_1100US};
|
||||
AdcTime adc_time_voltage_{AdcTime::ADC_TIME_1100US};
|
||||
AdcTime adc_time_current_{AdcTime::ADC_TIME_1100US};
|
||||
AdcAvgSamples adc_avg_samples_{AdcAvgSamples::ADC_AVG_SAMPLES_4};
|
||||
uint32_t calibration_lsb_;
|
||||
sensor::Sensor *bus_voltage_sensor_{nullptr};
|
||||
|
@ -16,6 +16,7 @@ from esphome.const import (
|
||||
UNIT_VOLT,
|
||||
UNIT_AMPERE,
|
||||
UNIT_WATT,
|
||||
CONF_VOLTAGE,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
@ -92,7 +93,15 @@ CONFIG_SCHEMA = (
|
||||
cv.Optional(CONF_MAX_CURRENT, default=3.2): cv.All(
|
||||
cv.current, cv.Range(min=0.0)
|
||||
),
|
||||
cv.Optional(CONF_ADC_TIME, default="1100 us"): validate_adc_time,
|
||||
cv.Optional(CONF_ADC_TIME, default="1100 us"): cv.Any(
|
||||
validate_adc_time,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_VOLTAGE): validate_adc_time,
|
||||
cv.Required(CONF_CURRENT): validate_adc_time,
|
||||
}
|
||||
),
|
||||
),
|
||||
cv.Optional(CONF_ADC_AVERAGING, default=4): cv.enum(
|
||||
ADC_AVG_SAMPLES, int=True
|
||||
),
|
||||
@ -110,7 +119,15 @@ async def to_code(config):
|
||||
|
||||
cg.add(var.set_shunt_resistance_ohm(config[CONF_SHUNT_RESISTANCE]))
|
||||
cg.add(var.set_max_current_a(config[CONF_MAX_CURRENT]))
|
||||
cg.add(var.set_adc_time(config[CONF_ADC_TIME]))
|
||||
|
||||
adc_time_config = config[CONF_ADC_TIME]
|
||||
if isinstance(adc_time_config, dict):
|
||||
cg.add(var.set_adc_time_voltage(adc_time_config[CONF_VOLTAGE]))
|
||||
cg.add(var.set_adc_time_current(adc_time_config[CONF_CURRENT]))
|
||||
else:
|
||||
cg.add(var.set_adc_time_voltage(adc_time_config))
|
||||
cg.add(var.set_adc_time_current(adc_time_config))
|
||||
|
||||
cg.add(var.set_adc_avg_samples(config[CONF_ADC_AVERAGING]))
|
||||
|
||||
if CONF_BUS_VOLTAGE in config:
|
||||
|
0
esphome/components/kamstrup_kmp/__init__.py
Normal file
0
esphome/components/kamstrup_kmp/__init__.py
Normal file
301
esphome/components/kamstrup_kmp/kamstrup_kmp.cpp
Normal file
301
esphome/components/kamstrup_kmp/kamstrup_kmp.cpp
Normal file
@ -0,0 +1,301 @@
|
||||
#include "kamstrup_kmp.h"
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace kamstrup_kmp {
|
||||
|
||||
static const char *const TAG = "kamstrup_kmp";
|
||||
|
||||
void KamstrupKMPComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "kamstrup_kmp:");
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, "Communication with Kamstrup meter failed!");
|
||||
}
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
|
||||
LOG_SENSOR(" ", "Heat Energy", this->heat_energy_sensor_);
|
||||
LOG_SENSOR(" ", "Power", this->power_sensor_);
|
||||
LOG_SENSOR(" ", "Temperature 1", this->temp1_sensor_);
|
||||
LOG_SENSOR(" ", "Temperature 2", this->temp2_sensor_);
|
||||
LOG_SENSOR(" ", "Temperature Difference", this->temp_diff_sensor_);
|
||||
LOG_SENSOR(" ", "Flow", this->flow_sensor_);
|
||||
LOG_SENSOR(" ", "Volume", this->volume_sensor_);
|
||||
|
||||
for (int i = 0; i < this->custom_sensors_.size(); i++) {
|
||||
LOG_SENSOR(" ", "Custom Sensor", this->custom_sensors_[i]);
|
||||
ESP_LOGCONFIG(TAG, " Command: 0x%04X", this->custom_commands_[i]);
|
||||
}
|
||||
|
||||
this->check_uart_settings(1200, 2, uart::UART_CONFIG_PARITY_NONE, 8);
|
||||
}
|
||||
|
||||
float KamstrupKMPComponent::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
void KamstrupKMPComponent::update() {
|
||||
if (this->heat_energy_sensor_ != nullptr) {
|
||||
this->command_queue_.push(CMD_HEAT_ENERGY);
|
||||
}
|
||||
|
||||
if (this->power_sensor_ != nullptr) {
|
||||
this->command_queue_.push(CMD_POWER);
|
||||
}
|
||||
|
||||
if (this->temp1_sensor_ != nullptr) {
|
||||
this->command_queue_.push(CMD_TEMP1);
|
||||
}
|
||||
|
||||
if (this->temp2_sensor_ != nullptr) {
|
||||
this->command_queue_.push(CMD_TEMP2);
|
||||
}
|
||||
|
||||
if (this->temp_diff_sensor_ != nullptr) {
|
||||
this->command_queue_.push(CMD_TEMP_DIFF);
|
||||
}
|
||||
|
||||
if (this->flow_sensor_ != nullptr) {
|
||||
this->command_queue_.push(CMD_FLOW);
|
||||
}
|
||||
|
||||
if (this->volume_sensor_ != nullptr) {
|
||||
this->command_queue_.push(CMD_VOLUME);
|
||||
}
|
||||
|
||||
for (uint16_t custom_command : this->custom_commands_) {
|
||||
this->command_queue_.push(custom_command);
|
||||
}
|
||||
}
|
||||
|
||||
void KamstrupKMPComponent::loop() {
|
||||
if (!this->command_queue_.empty()) {
|
||||
uint16_t command = this->command_queue_.front();
|
||||
this->send_command_(command);
|
||||
this->command_queue_.pop();
|
||||
}
|
||||
}
|
||||
|
||||
void KamstrupKMPComponent::send_command_(uint16_t command) {
|
||||
uint32_t msg_len = 5;
|
||||
uint8_t msg[msg_len];
|
||||
|
||||
msg[0] = 0x3F;
|
||||
msg[1] = 0x10;
|
||||
msg[2] = 0x01;
|
||||
msg[3] = command >> 8;
|
||||
msg[4] = command & 0xFF;
|
||||
|
||||
this->clear_uart_rx_buffer_();
|
||||
this->send_message_(msg, msg_len);
|
||||
this->read_command_(command);
|
||||
}
|
||||
|
||||
void KamstrupKMPComponent::send_message_(const uint8_t *msg, int msg_len) {
|
||||
int buffer_len = msg_len + 2;
|
||||
uint8_t buffer[buffer_len];
|
||||
|
||||
// Prepare the basic message and appand CRC
|
||||
for (int i = 0; i < msg_len; i++) {
|
||||
buffer[i] = msg[i];
|
||||
}
|
||||
|
||||
buffer[buffer_len - 2] = 0;
|
||||
buffer[buffer_len - 1] = 0;
|
||||
|
||||
uint16_t crc = crc16_ccitt(buffer, buffer_len);
|
||||
buffer[buffer_len - 2] = crc >> 8;
|
||||
buffer[buffer_len - 1] = crc & 0xFF;
|
||||
|
||||
// Prepare actual TX message
|
||||
uint8_t tx_msg[20];
|
||||
int tx_msg_len = 1;
|
||||
tx_msg[0] = 0x80; // prefix
|
||||
|
||||
for (int i = 0; i < buffer_len; i++) {
|
||||
if (buffer[i] == 0x06 || buffer[i] == 0x0d || buffer[i] == 0x1b || buffer[i] == 0x40 || buffer[i] == 0x80) {
|
||||
tx_msg[tx_msg_len++] = 0x1b;
|
||||
tx_msg[tx_msg_len++] = buffer[i] ^ 0xff;
|
||||
} else {
|
||||
tx_msg[tx_msg_len++] = buffer[i];
|
||||
}
|
||||
}
|
||||
|
||||
tx_msg[tx_msg_len++] = 0x0D; // EOM
|
||||
|
||||
this->write_array(tx_msg, tx_msg_len);
|
||||
}
|
||||
|
||||
void KamstrupKMPComponent::clear_uart_rx_buffer_() {
|
||||
uint8_t tmp;
|
||||
while (this->available()) {
|
||||
this->read_byte(&tmp);
|
||||
}
|
||||
}
|
||||
|
||||
void KamstrupKMPComponent::read_command_(uint16_t command) {
|
||||
uint8_t buffer[20] = {0};
|
||||
int buffer_len = 0;
|
||||
int data;
|
||||
int timeout = 250; // ms
|
||||
|
||||
// Read the data from the UART
|
||||
while (timeout > 0) {
|
||||
if (this->available()) {
|
||||
data = this->read();
|
||||
if (data > -1) {
|
||||
if (data == 0x40) { // start of message
|
||||
buffer_len = 0;
|
||||
}
|
||||
buffer[buffer_len++] = (uint8_t) data;
|
||||
if (data == 0x0D) {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Error while reading from UART");
|
||||
}
|
||||
} else {
|
||||
delay(1);
|
||||
timeout--;
|
||||
}
|
||||
}
|
||||
|
||||
if (timeout == 0 || buffer_len == 0) {
|
||||
ESP_LOGE(TAG, "Request timed out");
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate message (prefix and suffix)
|
||||
if (buffer[0] != 0x40) {
|
||||
ESP_LOGE(TAG, "Received invalid message (prefix mismatch received 0x%02X, expected 0x40)", buffer[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (buffer[buffer_len - 1] != 0x0D) {
|
||||
ESP_LOGE(TAG, "Received invalid message (EOM mismatch received 0x%02X, expected 0x0D)", buffer[buffer_len - 1]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Decode
|
||||
uint8_t msg[20] = {0};
|
||||
int msg_len = 0;
|
||||
for (int i = 1; i < buffer_len - 1; i++) {
|
||||
if (buffer[i] == 0x1B) {
|
||||
msg[msg_len++] = buffer[i + 1] ^ 0xFF;
|
||||
i++;
|
||||
} else {
|
||||
msg[msg_len++] = buffer[i];
|
||||
}
|
||||
}
|
||||
|
||||
// Validate CRC
|
||||
if (crc16_ccitt(msg, msg_len)) {
|
||||
ESP_LOGE(TAG, "Received invalid message (CRC mismatch)");
|
||||
return;
|
||||
}
|
||||
|
||||
// All seems good. Now parse the message
|
||||
this->parse_command_message_(command, msg, msg_len);
|
||||
}
|
||||
|
||||
void KamstrupKMPComponent::parse_command_message_(uint16_t command, const uint8_t *msg, int msg_len) {
|
||||
// Validate the message
|
||||
if (msg_len < 8) {
|
||||
ESP_LOGE(TAG, "Received invalid message (message too small)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg[0] != 0x3F || msg[1] != 0x10) {
|
||||
ESP_LOGE(TAG, "Received invalid message (invalid header received 0x%02X%02X, expected 0x3F10)", msg[0], msg[1]);
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t recv_command = msg[2] << 8 | msg[3];
|
||||
if (recv_command != command) {
|
||||
ESP_LOGE(TAG, "Received invalid message (invalid unexpected command received 0x%04X, expected 0x%04X)",
|
||||
recv_command, command);
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t unit_idx = msg[4];
|
||||
uint8_t mantissa_range = msg[5];
|
||||
|
||||
if (mantissa_range > 4) {
|
||||
ESP_LOGE(TAG, "Received invalid message (mantissa size too large %d, expected 4)", mantissa_range);
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate exponent
|
||||
float exponent = msg[6] & 0x3F;
|
||||
if (msg[6] & 0x40) {
|
||||
exponent = -exponent;
|
||||
}
|
||||
exponent = powf(10, exponent);
|
||||
if (msg[6] & 0x80) {
|
||||
exponent = -exponent;
|
||||
}
|
||||
|
||||
// Calculate mantissa
|
||||
uint32_t mantissa = 0;
|
||||
for (int i = 0; i < mantissa_range; i++) {
|
||||
mantissa <<= 8;
|
||||
mantissa |= msg[i + 7];
|
||||
}
|
||||
|
||||
// Calculate the actual value
|
||||
float value = mantissa * exponent;
|
||||
|
||||
// Set sensor value
|
||||
this->set_sensor_value_(command, value, unit_idx);
|
||||
}
|
||||
|
||||
void KamstrupKMPComponent::set_sensor_value_(uint16_t command, float value, uint8_t unit_idx) {
|
||||
const char *unit = UNITS[unit_idx];
|
||||
|
||||
// Standard sensors
|
||||
if (command == CMD_HEAT_ENERGY && this->heat_energy_sensor_ != nullptr) {
|
||||
this->heat_energy_sensor_->publish_state(value);
|
||||
} else if (command == CMD_POWER && this->power_sensor_ != nullptr) {
|
||||
this->power_sensor_->publish_state(value);
|
||||
} else if (command == CMD_TEMP1 && this->temp1_sensor_ != nullptr) {
|
||||
this->temp1_sensor_->publish_state(value);
|
||||
} else if (command == CMD_TEMP2 && this->temp2_sensor_ != nullptr) {
|
||||
this->temp2_sensor_->publish_state(value);
|
||||
} else if (command == CMD_TEMP_DIFF && this->temp_diff_sensor_ != nullptr) {
|
||||
this->temp_diff_sensor_->publish_state(value);
|
||||
} else if (command == CMD_FLOW && this->flow_sensor_ != nullptr) {
|
||||
this->flow_sensor_->publish_state(value);
|
||||
} else if (command == CMD_VOLUME && this->volume_sensor_ != nullptr) {
|
||||
this->volume_sensor_->publish_state(value);
|
||||
}
|
||||
|
||||
// Custom sensors
|
||||
for (int i = 0; i < this->custom_commands_.size(); i++) {
|
||||
if (command == this->custom_commands_[i]) {
|
||||
this->custom_sensors_[i]->publish_state(value);
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Received value for command 0x%04X: %.3f [%s]", command, value, unit);
|
||||
}
|
||||
|
||||
uint16_t crc16_ccitt(const uint8_t *buffer, int len) {
|
||||
uint32_t poly = 0x1021;
|
||||
uint32_t reg = 0x00;
|
||||
for (int i = 0; i < len; i++) {
|
||||
int mask = 0x80;
|
||||
while (mask > 0) {
|
||||
reg <<= 1;
|
||||
if (buffer[i] & mask) {
|
||||
reg |= 1;
|
||||
}
|
||||
mask >>= 1;
|
||||
if (reg & 0x10000) {
|
||||
reg &= 0xffff;
|
||||
reg ^= poly;
|
||||
}
|
||||
}
|
||||
}
|
||||
return (uint16_t) reg;
|
||||
}
|
||||
|
||||
} // namespace kamstrup_kmp
|
||||
} // namespace esphome
|
131
esphome/components/kamstrup_kmp/kamstrup_kmp.h
Normal file
131
esphome/components/kamstrup_kmp/kamstrup_kmp.h
Normal file
@ -0,0 +1,131 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace kamstrup_kmp {
|
||||
|
||||
/*
|
||||
===========================================================================
|
||||
=== KAMSTRUP KMP ===
|
||||
===========================================================================
|
||||
|
||||
Kamstrup Meter Protocol (KMP) is a protocol used with Kamstrup district
|
||||
heating meters, e.g. Kamstrup MULTICAL 403.
|
||||
These devices register consumed heat from a district heating system.
|
||||
It does this by measuring the incoming and outgoing water temperature
|
||||
and by measuring the water flow. The temperature difference (delta T)
|
||||
together with the water flow results in consumed energy, typically
|
||||
in giga joule (GJ).
|
||||
|
||||
The Kamstrup Multical has an optical interface just above the display.
|
||||
This interface is essentially an RS-232 interface using a proprietary
|
||||
protocol (Kamstrup Meter Protocol [KMP]).
|
||||
|
||||
The integration uses this optical interface to periodically read the
|
||||
configured values (sensors) from the meter. Supported sensors are:
|
||||
- Heat Energy [GJ]
|
||||
- Current Power Consumption [kW]
|
||||
- Temperature 1 [°C]
|
||||
- Temperature 2 [°C]
|
||||
- Temperature Difference [°K]
|
||||
- Water Flow [l/h]
|
||||
- Volume [m3]
|
||||
|
||||
Apart from these supported 'fixed' sensors, the user can configure up to
|
||||
five custom sensors. The KMP command (16 bit unsigned int) has to be
|
||||
provided in that case.
|
||||
|
||||
Note:
|
||||
The optical interface is enabled as soon as a button on the meter is pushed.
|
||||
The interface stays active for a few minutes. To keep the interface 'alive'
|
||||
magnets must be placed around the optical sensor.
|
||||
|
||||
Units:
|
||||
Units are set using the regular Sensor config in the user yaml. However,
|
||||
KMP does also send the correct unit with every value. When DEBUG logging
|
||||
is enabled, the received value with the received unit are logged.
|
||||
|
||||
Acknowledgement:
|
||||
This interface was inspired by:
|
||||
- https://atomstar.tweakblogs.net/blog/19110/reading-out-kamstrup-multical-402-403-with-home-built-optical-head
|
||||
- https://wiki.hal9k.dk/projects/kamstrup
|
||||
*/
|
||||
|
||||
// KMP Commands
|
||||
static const uint16_t CMD_HEAT_ENERGY = 0x003C;
|
||||
static const uint16_t CMD_POWER = 0x0050;
|
||||
static const uint16_t CMD_TEMP1 = 0x0056;
|
||||
static const uint16_t CMD_TEMP2 = 0x0057;
|
||||
static const uint16_t CMD_TEMP_DIFF = 0x0059;
|
||||
static const uint16_t CMD_FLOW = 0x004A;
|
||||
static const uint16_t CMD_VOLUME = 0x0044;
|
||||
|
||||
// KMP units
|
||||
static const char *const UNITS[] = {
|
||||
"", "Wh", "kWh", "MWh", "GWh", "J", "kJ", "MJ", "GJ", "Cal",
|
||||
"kCal", "Mcal", "Gcal", "varh", "kvarh", "Mvarh", "Gvarh", "VAh", "kVAh", "MVAh",
|
||||
"GVAh", "kW", "kW", "MW", "GW", "kvar", "kvar", "Mvar", "Gvar", "VA",
|
||||
"kVA", "MVA", "GVA", "V", "A", "kV", "kA", "C", "K", "l",
|
||||
"m3", "l/h", "m3/h", "m3xC", "ton", "ton/h", "h", "hh:mm:ss", "yy:mm:dd", "yyyy:mm:dd",
|
||||
"mm:dd", "", "bar", "RTC", "ASCII", "m3 x 10", "ton x 10", "GJ x 10", "minutes", "Bitfield",
|
||||
"s", "ms", "days", "RTC-Q", "Datetime"};
|
||||
|
||||
class KamstrupKMPComponent : public PollingComponent, public uart::UARTDevice {
|
||||
public:
|
||||
void set_heat_energy_sensor(sensor::Sensor *sensor) { this->heat_energy_sensor_ = sensor; }
|
||||
void set_power_sensor(sensor::Sensor *sensor) { this->power_sensor_ = sensor; }
|
||||
void set_temp1_sensor(sensor::Sensor *sensor) { this->temp1_sensor_ = sensor; }
|
||||
void set_temp2_sensor(sensor::Sensor *sensor) { this->temp2_sensor_ = sensor; }
|
||||
void set_temp_diff_sensor(sensor::Sensor *sensor) { this->temp_diff_sensor_ = sensor; }
|
||||
void set_flow_sensor(sensor::Sensor *sensor) { this->flow_sensor_ = sensor; }
|
||||
void set_volume_sensor(sensor::Sensor *sensor) { this->volume_sensor_ = sensor; }
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
void update() override;
|
||||
void loop() override;
|
||||
void add_custom_sensor(sensor::Sensor *sensor, uint16_t command) {
|
||||
this->custom_sensors_.push_back(sensor);
|
||||
this->custom_commands_.push_back(command);
|
||||
}
|
||||
|
||||
protected:
|
||||
// Sensors
|
||||
sensor::Sensor *heat_energy_sensor_{nullptr};
|
||||
sensor::Sensor *power_sensor_{nullptr};
|
||||
sensor::Sensor *temp1_sensor_{nullptr};
|
||||
sensor::Sensor *temp2_sensor_{nullptr};
|
||||
sensor::Sensor *temp_diff_sensor_{nullptr};
|
||||
sensor::Sensor *flow_sensor_{nullptr};
|
||||
sensor::Sensor *volume_sensor_{nullptr};
|
||||
|
||||
// Custom sensors and commands
|
||||
std::vector<sensor::Sensor *> custom_sensors_;
|
||||
std::vector<uint16_t> custom_commands_;
|
||||
|
||||
// Command queue
|
||||
std::queue<uint16_t> command_queue_;
|
||||
|
||||
// Methods
|
||||
|
||||
// Sends a command to the meter and receives its response
|
||||
void send_command_(uint16_t command);
|
||||
// Sends a message to the meter. A prefix/suffix and CRC are added
|
||||
void send_message_(const uint8_t *msg, int msg_len);
|
||||
// Clears and data that might be in the UART Rx buffer
|
||||
void clear_uart_rx_buffer_();
|
||||
// Reads and validates the response to a send command
|
||||
void read_command_(uint16_t command);
|
||||
// Parses a received message
|
||||
void parse_command_message_(uint16_t command, const uint8_t *msg, int msg_len);
|
||||
// Sets the received value to the correct sensor
|
||||
void set_sensor_value_(uint16_t command, float value, uint8_t unit_idx);
|
||||
};
|
||||
|
||||
// "true" CCITT CRC-16
|
||||
uint16_t crc16_ccitt(const uint8_t *buffer, int len);
|
||||
|
||||
} // namespace kamstrup_kmp
|
||||
} // namespace esphome
|
132
esphome/components/kamstrup_kmp/sensor.py
Normal file
132
esphome/components/kamstrup_kmp/sensor.py
Normal file
@ -0,0 +1,132 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import sensor, uart
|
||||
from esphome.const import (
|
||||
CONF_COMMAND,
|
||||
CONF_CUSTOM,
|
||||
CONF_FLOW,
|
||||
CONF_ID,
|
||||
CONF_POWER,
|
||||
CONF_VOLUME,
|
||||
DEVICE_CLASS_EMPTY,
|
||||
DEVICE_CLASS_ENERGY,
|
||||
DEVICE_CLASS_POWER,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
DEVICE_CLASS_VOLUME,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
STATE_CLASS_TOTAL_INCREASING,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_CUBIC_METER,
|
||||
UNIT_EMPTY,
|
||||
UNIT_KELVIN,
|
||||
UNIT_KILOWATT,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@cfeenstra1024"]
|
||||
DEPENDENCIES = ["uart"]
|
||||
|
||||
kamstrup_kmp_ns = cg.esphome_ns.namespace("kamstrup_kmp")
|
||||
KamstrupKMPComponent = kamstrup_kmp_ns.class_(
|
||||
"KamstrupKMPComponent", cg.PollingComponent, uart.UARTDevice
|
||||
)
|
||||
|
||||
CONF_HEAT_ENERGY = "heat_energy"
|
||||
CONF_TEMP1 = "temp1"
|
||||
CONF_TEMP2 = "temp2"
|
||||
CONF_TEMP_DIFF = "temp_diff"
|
||||
|
||||
UNIT_GIGA_JOULE = "GJ"
|
||||
UNIT_LITRE_PER_HOUR = "l/h"
|
||||
|
||||
# Note: The sensor units are set automatically based un the received data from the meter
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(KamstrupKMPComponent),
|
||||
cv.Optional(CONF_HEAT_ENERGY): sensor.sensor_schema(
|
||||
accuracy_decimals=3,
|
||||
device_class=DEVICE_CLASS_ENERGY,
|
||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||
unit_of_measurement=UNIT_GIGA_JOULE,
|
||||
),
|
||||
cv.Optional(CONF_POWER): sensor.sensor_schema(
|
||||
accuracy_decimals=3,
|
||||
device_class=DEVICE_CLASS_POWER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
unit_of_measurement=UNIT_KILOWATT,
|
||||
),
|
||||
cv.Optional(CONF_TEMP1): sensor.sensor_schema(
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
),
|
||||
cv.Optional(CONF_TEMP2): sensor.sensor_schema(
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
),
|
||||
cv.Optional(CONF_TEMP_DIFF): sensor.sensor_schema(
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
unit_of_measurement=UNIT_KELVIN,
|
||||
),
|
||||
cv.Optional(CONF_FLOW): sensor.sensor_schema(
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_VOLUME,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
unit_of_measurement=UNIT_LITRE_PER_HOUR,
|
||||
),
|
||||
cv.Optional(CONF_VOLUME): sensor.sensor_schema(
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_VOLUME,
|
||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||
unit_of_measurement=UNIT_CUBIC_METER,
|
||||
),
|
||||
cv.Optional(CONF_CUSTOM): cv.ensure_list(
|
||||
sensor.sensor_schema(
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_EMPTY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
unit_of_measurement=UNIT_EMPTY,
|
||||
).extend({cv.Required(CONF_COMMAND): cv.hex_uint16_t})
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
.extend(uart.UART_DEVICE_SCHEMA)
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
"kamstrup_kmp", baud_rate=1200, require_rx=True, require_tx=True
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
|
||||
# Standard sensors
|
||||
for key in [
|
||||
CONF_HEAT_ENERGY,
|
||||
CONF_POWER,
|
||||
CONF_TEMP1,
|
||||
CONF_TEMP2,
|
||||
CONF_TEMP_DIFF,
|
||||
CONF_FLOW,
|
||||
CONF_VOLUME,
|
||||
]:
|
||||
if key not in config:
|
||||
continue
|
||||
conf = config[key]
|
||||
sens = await sensor.new_sensor(conf)
|
||||
cg.add(getattr(var, f"set_{key}_sensor")(sens))
|
||||
|
||||
# Custom sensors
|
||||
if CONF_CUSTOM in config:
|
||||
for conf in config[CONF_CUSTOM]:
|
||||
sens = await sensor.new_sensor(conf)
|
||||
cg.add(var.add_custom_sensor(sens, conf[CONF_COMMAND]))
|
@ -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--;
|
||||
|
@ -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",
|
||||
|
@ -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;
|
||||
|
@ -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(
|
||||
|
@ -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;
|
||||
|
@ -3,7 +3,7 @@ import esphome.config_validation as cv
|
||||
import esphome.codegen as cg
|
||||
|
||||
from esphome.automation import maybe_simple_id
|
||||
from esphome.const import CONF_ID, CONF_ON_STATE, CONF_TRIGGER_ID
|
||||
from esphome.const import CONF_ID, CONF_ON_STATE, CONF_TRIGGER_ID, CONF_VOLUME
|
||||
from esphome.core import CORE
|
||||
from esphome.coroutine import coroutine_with_priority
|
||||
from esphome.cpp_helpers import setup_entity
|
||||
@ -43,7 +43,6 @@ VolumeSetAction = media_player_ns.class_(
|
||||
)
|
||||
|
||||
|
||||
CONF_VOLUME = "volume"
|
||||
CONF_ON_IDLE = "on_idle"
|
||||
CONF_ON_PLAY = "on_play"
|
||||
CONF_ON_PAUSE = "on_pause"
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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> {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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")
|
||||
|
@ -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.
|
||||
|
@ -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) {
|
||||
|
1
esphome/components/qspi_amoled/__init__.py
Normal file
1
esphome/components/qspi_amoled/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
CODEOWNERS = ["@clydebarrow"]
|
131
esphome/components/qspi_amoled/display.py
Normal file
131
esphome/components/qspi_amoled/display.py
Normal file
@ -0,0 +1,131 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome import pins
|
||||
from esphome.components import (
|
||||
spi,
|
||||
display,
|
||||
)
|
||||
from esphome.const import (
|
||||
CONF_RESET_PIN,
|
||||
CONF_ID,
|
||||
CONF_DIMENSIONS,
|
||||
CONF_WIDTH,
|
||||
CONF_HEIGHT,
|
||||
CONF_LAMBDA,
|
||||
CONF_BRIGHTNESS,
|
||||
CONF_ENABLE_PIN,
|
||||
CONF_MODEL,
|
||||
CONF_OFFSET_HEIGHT,
|
||||
CONF_OFFSET_WIDTH,
|
||||
CONF_INVERT_COLORS,
|
||||
CONF_MIRROR_X,
|
||||
CONF_MIRROR_Y,
|
||||
CONF_SWAP_XY,
|
||||
CONF_COLOR_ORDER,
|
||||
CONF_TRANSFORM,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["spi"]
|
||||
|
||||
qspi_amoled_ns = cg.esphome_ns.namespace("qspi_amoled")
|
||||
QSPI_AMOLED = qspi_amoled_ns.class_(
|
||||
"QspiAmoLed", display.Display, display.DisplayBuffer, cg.Component, spi.SPIDevice
|
||||
)
|
||||
ColorOrder = display.display_ns.enum("ColorMode")
|
||||
Model = qspi_amoled_ns.enum("Model")
|
||||
|
||||
MODELS = {"RM690B0": Model.RM690B0, "RM67162": Model.RM67162}
|
||||
|
||||
COLOR_ORDERS = {
|
||||
"RGB": ColorOrder.COLOR_ORDER_RGB,
|
||||
"BGR": ColorOrder.COLOR_ORDER_BGR,
|
||||
}
|
||||
DATA_PIN_SCHEMA = pins.internal_gpio_output_pin_schema
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
display.FULL_DISPLAY_SCHEMA.extend(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(QSPI_AMOLED),
|
||||
cv.Required(CONF_MODEL): cv.enum(MODELS, upper=True),
|
||||
cv.Required(CONF_DIMENSIONS): cv.Any(
|
||||
cv.dimensions,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_WIDTH): cv.int_,
|
||||
cv.Required(CONF_HEIGHT): cv.int_,
|
||||
cv.Optional(CONF_OFFSET_HEIGHT, default=0): cv.int_,
|
||||
cv.Optional(CONF_OFFSET_WIDTH, default=0): cv.int_,
|
||||
}
|
||||
),
|
||||
),
|
||||
cv.Optional(CONF_TRANSFORM): cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_MIRROR_X, default=False): cv.boolean,
|
||||
cv.Optional(CONF_MIRROR_Y, default=False): cv.boolean,
|
||||
cv.Optional(CONF_SWAP_XY, default=False): cv.boolean,
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_COLOR_ORDER, default="RGB"): cv.enum(
|
||||
COLOR_ORDERS, upper=True
|
||||
),
|
||||
cv.Optional(CONF_INVERT_COLORS, default=False): cv.boolean,
|
||||
cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
|
||||
cv.Optional(CONF_ENABLE_PIN): pins.gpio_output_pin_schema,
|
||||
cv.Optional(CONF_BRIGHTNESS, default=0xD0): cv.int_range(
|
||||
0, 0xFF, min_included=True, max_included=True
|
||||
),
|
||||
}
|
||||
).extend(
|
||||
spi.spi_device_schema(
|
||||
cs_pin_required=False,
|
||||
default_mode="MODE0",
|
||||
default_data_rate=10e6,
|
||||
quad=True,
|
||||
)
|
||||
)
|
||||
),
|
||||
cv.only_with_esp_idf,
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await display.register_display(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
|
||||
cg.add(var.set_color_mode(config[CONF_COLOR_ORDER]))
|
||||
cg.add(var.set_invert_colors(config[CONF_INVERT_COLORS]))
|
||||
cg.add(var.set_brightness(config[CONF_BRIGHTNESS]))
|
||||
cg.add(var.set_model(config[CONF_MODEL]))
|
||||
if enable_pin := config.get(CONF_ENABLE_PIN):
|
||||
enable = await cg.gpio_pin_expression(enable_pin)
|
||||
cg.add(var.set_enable_pin(enable))
|
||||
|
||||
if reset_pin := config.get(CONF_RESET_PIN):
|
||||
reset = await cg.gpio_pin_expression(reset_pin)
|
||||
cg.add(var.set_reset_pin(reset))
|
||||
|
||||
if transform := config.get(CONF_TRANSFORM):
|
||||
cg.add(var.set_mirror_x(transform[CONF_MIRROR_X]))
|
||||
cg.add(var.set_mirror_y(transform[CONF_MIRROR_Y]))
|
||||
cg.add(var.set_swap_xy(transform[CONF_SWAP_XY]))
|
||||
|
||||
if CONF_DIMENSIONS in config:
|
||||
dimensions = config[CONF_DIMENSIONS]
|
||||
if isinstance(dimensions, dict):
|
||||
cg.add(var.set_dimensions(dimensions[CONF_WIDTH], dimensions[CONF_HEIGHT]))
|
||||
cg.add(
|
||||
var.set_offsets(
|
||||
dimensions[CONF_OFFSET_WIDTH], dimensions[CONF_OFFSET_HEIGHT]
|
||||
)
|
||||
)
|
||||
else:
|
||||
(width, height) = dimensions
|
||||
cg.add(var.set_dimensions(width, height))
|
||||
|
||||
if lamb := config.get(CONF_LAMBDA):
|
||||
lambda_ = await cg.process_lambda(
|
||||
lamb, [(display.DisplayRef, "it")], return_type=cg.void
|
||||
)
|
||||
cg.add(var.set_writer(lambda_))
|
165
esphome/components/qspi_amoled/qspi_amoled.cpp
Normal file
165
esphome/components/qspi_amoled/qspi_amoled.cpp
Normal file
@ -0,0 +1,165 @@
|
||||
#ifdef USE_ESP_IDF
|
||||
#include "qspi_amoled.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace qspi_amoled {
|
||||
|
||||
void QspiAmoLed::setup() {
|
||||
esph_log_config(TAG, "Setting up QSPI_AMOLED");
|
||||
this->spi_setup();
|
||||
if (this->enable_pin_ != nullptr) {
|
||||
this->enable_pin_->setup();
|
||||
this->enable_pin_->digital_write(true);
|
||||
}
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
this->reset_pin_->setup();
|
||||
this->reset_pin_->digital_write(true);
|
||||
delay(5);
|
||||
this->reset_pin_->digital_write(false);
|
||||
delay(5);
|
||||
this->reset_pin_->digital_write(true);
|
||||
}
|
||||
this->set_timeout(120, [this] { this->write_command_(SLEEP_OUT); });
|
||||
this->set_timeout(240, [this] { this->write_init_sequence_(); });
|
||||
}
|
||||
|
||||
void QspiAmoLed::update() {
|
||||
this->do_update_();
|
||||
int w = this->x_high_ - this->x_low_ + 1;
|
||||
int h = this->y_high_ - this->y_low_ + 1;
|
||||
this->draw_pixels_at(this->x_low_, this->y_low_, w, h, this->buffer_, this->color_mode_, display::COLOR_BITNESS_565,
|
||||
true, this->x_low_, this->y_low_, this->get_width_internal() - w - this->x_low_);
|
||||
// invalidate watermarks
|
||||
this->x_low_ = this->width_;
|
||||
this->y_low_ = this->height_;
|
||||
this->x_high_ = 0;
|
||||
this->y_high_ = 0;
|
||||
}
|
||||
|
||||
void QspiAmoLed::draw_absolute_pixel_internal(int x, int y, Color color) {
|
||||
if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) {
|
||||
return;
|
||||
}
|
||||
if (this->buffer_ == nullptr)
|
||||
this->init_internal_(this->width_ * this->height_ * 2);
|
||||
if (this->is_failed())
|
||||
return;
|
||||
uint32_t pos = (y * this->width_) + x;
|
||||
uint16_t new_color;
|
||||
bool updated = false;
|
||||
pos = pos * 2;
|
||||
new_color = display::ColorUtil::color_to_565(color, display::ColorOrder::COLOR_ORDER_RGB);
|
||||
if (this->buffer_[pos] != (uint8_t) (new_color >> 8)) {
|
||||
this->buffer_[pos] = (uint8_t) (new_color >> 8);
|
||||
updated = true;
|
||||
}
|
||||
pos = pos + 1;
|
||||
new_color = new_color & 0xFF;
|
||||
|
||||
if (this->buffer_[pos] != new_color) {
|
||||
this->buffer_[pos] = new_color;
|
||||
updated = true;
|
||||
}
|
||||
if (updated) {
|
||||
// low and high watermark may speed up drawing from buffer
|
||||
if (x < this->x_low_)
|
||||
this->x_low_ = x;
|
||||
if (y < this->y_low_)
|
||||
this->y_low_ = y;
|
||||
if (x > this->x_high_)
|
||||
this->x_high_ = x;
|
||||
if (y > this->y_high_)
|
||||
this->y_high_ = y;
|
||||
}
|
||||
}
|
||||
|
||||
void QspiAmoLed::reset_params_(bool ready) {
|
||||
if (!ready && !this->is_ready())
|
||||
return;
|
||||
this->write_command_(this->invert_colors_ ? INVERT_ON : INVERT_OFF);
|
||||
// custom x/y transform and color order
|
||||
uint8_t mad = this->color_mode_ == display::COLOR_ORDER_BGR ? MADCTL_BGR : MADCTL_RGB;
|
||||
if (this->swap_xy_)
|
||||
mad |= MADCTL_MV;
|
||||
if (this->mirror_x_)
|
||||
mad |= MADCTL_MX;
|
||||
if (this->mirror_y_)
|
||||
mad |= MADCTL_MY;
|
||||
this->write_command_(MADCTL_CMD, &mad, 1);
|
||||
this->write_command_(BRIGHTNESS, &this->brightness_, 1);
|
||||
}
|
||||
|
||||
void QspiAmoLed::write_init_sequence_() {
|
||||
if (this->model_ == RM690B0) {
|
||||
this->write_command_(PAGESEL, 0x20);
|
||||
this->write_command_(MIPI, 0x0A);
|
||||
this->write_command_(WRAM, 0x80);
|
||||
this->write_command_(SWIRE1, 0x51);
|
||||
this->write_command_(SWIRE2, 0x2E);
|
||||
this->write_command_(PAGESEL, 0x00);
|
||||
this->write_command_(0xC2, 0x00);
|
||||
delay(10);
|
||||
this->write_command_(TEON, 0x00);
|
||||
}
|
||||
this->write_command_(PIXFMT, 0x55);
|
||||
this->write_command_(BRIGHTNESS, 0);
|
||||
this->write_command_(DISPLAY_ON);
|
||||
this->reset_params_(true);
|
||||
this->setup_complete_ = true;
|
||||
esph_log_config(TAG, "QSPI_AMOLED setup complete");
|
||||
}
|
||||
|
||||
void QspiAmoLed::set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) {
|
||||
uint8_t buf[4];
|
||||
x1 += this->offset_x_;
|
||||
x2 += this->offset_x_;
|
||||
y1 += this->offset_y_;
|
||||
y2 += this->offset_y_;
|
||||
put16_be(buf, x1);
|
||||
put16_be(buf + 2, x2);
|
||||
this->write_command_(CASET, buf, sizeof buf);
|
||||
put16_be(buf, y1);
|
||||
put16_be(buf + 2, y2);
|
||||
this->write_command_(RASET, buf, sizeof buf);
|
||||
}
|
||||
|
||||
void QspiAmoLed::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
|
||||
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
|
||||
if (!this->setup_complete_ || this->is_failed())
|
||||
return;
|
||||
if (w <= 0 || h <= 0)
|
||||
return;
|
||||
if (bitness != display::COLOR_BITNESS_565 || order != this->color_mode_ ||
|
||||
big_endian != (this->bit_order_ == spi::BIT_ORDER_MSB_FIRST)) {
|
||||
return display::Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset,
|
||||
x_pad);
|
||||
}
|
||||
this->set_addr_window_(x_start, y_start, x_start + w - 1, y_start + h - 1);
|
||||
this->enable();
|
||||
// x_ and y_offset are offsets into the source buffer, unrelated to our own offsets into the display.
|
||||
if (x_offset == 0 && x_pad == 0 && y_offset == 0) {
|
||||
// we could deal here with a non-zero y_offset, but if x_offset is zero, y_offset probably will be so don't bother
|
||||
this->write_cmd_addr_data(8, 0x32, 24, 0x2C00, ptr, w * h * 2, 4);
|
||||
} else {
|
||||
this->write_cmd_addr_data(8, 0x32, 24, 0x2C00, nullptr, 0, 4);
|
||||
auto stride = x_offset + w + x_pad;
|
||||
for (int y = 0; y != h; y++) {
|
||||
this->write_cmd_addr_data(0, 0, 0, 0, ptr + ((y + y_offset) * stride + x_offset) * 2, w * 2, 4);
|
||||
}
|
||||
}
|
||||
this->disable();
|
||||
}
|
||||
|
||||
void QspiAmoLed::dump_config() {
|
||||
ESP_LOGCONFIG("", "QSPI AMOLED");
|
||||
ESP_LOGCONFIG(TAG, " Height: %u", this->height_);
|
||||
ESP_LOGCONFIG(TAG, " Width: %u", this->width_);
|
||||
LOG_PIN(" CS Pin: ", this->cs_);
|
||||
LOG_PIN(" Reset Pin: ", this->reset_pin_);
|
||||
ESP_LOGCONFIG(TAG, " SPI Data rate: %dMHz", (unsigned) (this->data_rate_ / 1000000));
|
||||
}
|
||||
|
||||
} // namespace qspi_amoled
|
||||
} // namespace esphome
|
||||
#endif
|
165
esphome/components/qspi_amoled/qspi_amoled.h
Normal file
165
esphome/components/qspi_amoled/qspi_amoled.h
Normal file
@ -0,0 +1,165 @@
|
||||
//
|
||||
// Created by Clyde Stubbs on 29/10/2023.
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_ESP_IDF
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/spi/spi.h"
|
||||
#include "esphome/components/display/display.h"
|
||||
#include "esphome/components/display/display_buffer.h"
|
||||
#include "esphome/components/display/display_color_utils.h"
|
||||
#include "esp_lcd_panel_ops.h"
|
||||
|
||||
#include "esp_lcd_panel_rgb.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace qspi_amoled {
|
||||
|
||||
constexpr static const char *const TAG = "display.qspi_amoled";
|
||||
static const uint8_t SW_RESET_CMD = 0x01;
|
||||
static const uint8_t SLEEP_OUT = 0x11;
|
||||
static const uint8_t INVERT_OFF = 0x20;
|
||||
static const uint8_t INVERT_ON = 0x21;
|
||||
static const uint8_t ALL_ON = 0x23;
|
||||
static const uint8_t WRAM = 0x24;
|
||||
static const uint8_t MIPI = 0x26;
|
||||
static const uint8_t DISPLAY_ON = 0x29;
|
||||
static const uint8_t RASET = 0x2B;
|
||||
static const uint8_t CASET = 0x2A;
|
||||
static const uint8_t WDATA = 0x2C;
|
||||
static const uint8_t TEON = 0x35;
|
||||
static const uint8_t MADCTL_CMD = 0x36;
|
||||
static const uint8_t PIXFMT = 0x3A;
|
||||
static const uint8_t BRIGHTNESS = 0x51;
|
||||
static const uint8_t SWIRE1 = 0x5A;
|
||||
static const uint8_t SWIRE2 = 0x5B;
|
||||
static const uint8_t PAGESEL = 0xFE;
|
||||
|
||||
static const uint8_t MADCTL_MY = 0x80; ///< Bit 7 Bottom to top
|
||||
static const uint8_t MADCTL_MX = 0x40; ///< Bit 6 Right to left
|
||||
static const uint8_t MADCTL_MV = 0x20; ///< Bit 5 Reverse Mode
|
||||
static const uint8_t MADCTL_RGB = 0x00; ///< Bit 3 Red-Green-Blue pixel order
|
||||
static const uint8_t MADCTL_BGR = 0x08; ///< Bit 3 Blue-Green-Red pixel order
|
||||
|
||||
// store a 16 bit value in a buffer, big endian.
|
||||
static inline void put16_be(uint8_t *buf, uint16_t value) {
|
||||
buf[0] = value >> 8;
|
||||
buf[1] = value;
|
||||
}
|
||||
|
||||
enum Model {
|
||||
RM690B0,
|
||||
RM67162,
|
||||
};
|
||||
|
||||
class QspiAmoLed : public display::DisplayBuffer,
|
||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
|
||||
spi::DATA_RATE_1MHZ> {
|
||||
public:
|
||||
void set_model(Model model) { this->model_ = model; }
|
||||
void update() override;
|
||||
void setup() override;
|
||||
display::ColorOrder get_color_mode() { return this->color_mode_; }
|
||||
void set_color_mode(display::ColorOrder color_mode) { this->color_mode_ = color_mode; }
|
||||
|
||||
void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; }
|
||||
void set_enable_pin(GPIOPin *enable_pin) { this->enable_pin_ = enable_pin; }
|
||||
void set_width(uint16_t width) { this->width_ = width; }
|
||||
void set_dimensions(uint16_t width, uint16_t height) {
|
||||
this->width_ = width;
|
||||
this->height_ = height;
|
||||
}
|
||||
int get_width() override { return this->width_; }
|
||||
int get_height() override { return this->height_; }
|
||||
void set_invert_colors(bool invert_colors) {
|
||||
this->invert_colors_ = invert_colors;
|
||||
this->reset_params_();
|
||||
}
|
||||
void set_mirror_x(bool mirror_x) {
|
||||
this->mirror_x_ = mirror_x;
|
||||
this->reset_params_();
|
||||
}
|
||||
void set_mirror_y(bool mirror_y) {
|
||||
this->mirror_y_ = mirror_y;
|
||||
this->reset_params_();
|
||||
}
|
||||
void set_swap_xy(bool swap_xy) {
|
||||
this->swap_xy_ = swap_xy;
|
||||
this->reset_params_();
|
||||
}
|
||||
void set_brightness(uint8_t brightness) {
|
||||
this->brightness_ = brightness;
|
||||
this->reset_params_();
|
||||
}
|
||||
void set_offsets(int16_t offset_x, int16_t offset_y) {
|
||||
this->offset_x_ = offset_x;
|
||||
this->offset_y_ = offset_y;
|
||||
}
|
||||
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
|
||||
void dump_config() override;
|
||||
|
||||
int get_width_internal() override { return this->width_; }
|
||||
int get_height_internal() override { return this->height_; }
|
||||
bool can_proceed() override { return this->setup_complete_; }
|
||||
|
||||
protected:
|
||||
void draw_absolute_pixel_internal(int x, int y, Color color) override;
|
||||
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
|
||||
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
|
||||
/**
|
||||
* the RM67162 in quad SPI mode seems to work like this (not in the datasheet, this is deduced from the
|
||||
* sample code.)
|
||||
*
|
||||
* Immediately after enabling /CS send 4 bytes in single-dataline SPI mode:
|
||||
* 0: either 0x2 or 0x32. The first indicates that any subsequent data bytes after the initial 4 will be
|
||||
* sent in 1-dataline SPI. The second indicates quad mode.
|
||||
* 1: 0x00
|
||||
* 2: The command (register address) byte.
|
||||
* 3: 0x00
|
||||
*
|
||||
* This is followed by zero or more data bytes in either 1-wire or 4-wire mode, depending on the first byte.
|
||||
* At the conclusion of the write, de-assert /CS.
|
||||
*
|
||||
* @param cmd
|
||||
* @param bytes
|
||||
* @param len
|
||||
*/
|
||||
void write_command_(uint8_t cmd, const uint8_t *bytes, size_t len) {
|
||||
this->enable();
|
||||
this->write_cmd_addr_data(8, 0x02, 24, cmd << 8, bytes, len);
|
||||
this->disable();
|
||||
}
|
||||
|
||||
void write_command_(uint8_t cmd, uint8_t data) { this->write_command_(cmd, &data, 1); }
|
||||
void write_command_(uint8_t cmd) { this->write_command_(cmd, &cmd, 0); }
|
||||
void reset_params_(bool ready = false);
|
||||
void write_init_sequence_();
|
||||
void set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2);
|
||||
|
||||
GPIOPin *reset_pin_{nullptr};
|
||||
GPIOPin *enable_pin_{nullptr};
|
||||
uint16_t x_low_{0};
|
||||
uint16_t y_low_{0};
|
||||
uint16_t x_high_{0};
|
||||
uint16_t y_high_{0};
|
||||
bool setup_complete_{};
|
||||
|
||||
bool invert_colors_{};
|
||||
display::ColorOrder color_mode_{display::COLOR_ORDER_BGR};
|
||||
size_t width_{};
|
||||
size_t height_{};
|
||||
int16_t offset_x_{0};
|
||||
int16_t offset_y_{0};
|
||||
bool swap_xy_{};
|
||||
bool mirror_x_{};
|
||||
bool mirror_y_{};
|
||||
uint8_t brightness_{0xD0};
|
||||
Model model_{RM690B0};
|
||||
|
||||
esp_lcd_panel_handle_t handle_{};
|
||||
};
|
||||
|
||||
} // namespace qspi_amoled
|
||||
} // namespace esphome
|
||||
#endif
|
@ -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]),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
@ -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_);
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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]))
|
||||
|
@ -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_();
|
||||
|
@ -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};
|
||||
|
@ -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]),
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
26
esphome/components/sun_gtil2/__init__.py
Normal file
26
esphome/components/sun_gtil2/__init__.py
Normal 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)
|
87
esphome/components/sun_gtil2/sensor.py
Normal file
87
esphome/components/sun_gtil2/sensor.py
Normal 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))
|
135
esphome/components/sun_gtil2/sun_gtil2.cpp
Normal file
135
esphome/components/sun_gtil2/sun_gtil2.cpp
Normal 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
|
58
esphome/components/sun_gtil2/sun_gtil2.h
Normal file
58
esphome/components/sun_gtil2/sun_gtil2.h
Normal 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
|
31
esphome/components/sun_gtil2/text_sensor.py
Normal file
31
esphome/components/sun_gtil2/text_sensor.py
Normal 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))
|
@ -39,7 +39,6 @@ void Touchscreen::loop() {
|
||||
ESP_LOGVV(TAG, "<< Do Touch loop >>");
|
||||
this->first_touch_ = this->touches_.empty();
|
||||
this->need_update_ = false;
|
||||
this->was_touched_ = this->is_touched_;
|
||||
this->is_touched_ = false;
|
||||
this->skip_update_ = false;
|
||||
for (auto &tp : this->touches_) {
|
||||
@ -62,7 +61,11 @@ void Touchscreen::loop() {
|
||||
if (this->touch_timeout_ > 0) {
|
||||
// Simulate a touch after <this->touch_timeout_> ms. This will reset any existing timeout operation.
|
||||
// This is to detect touch release.
|
||||
this->set_timeout(TAG, this->touch_timeout_, [this]() { this->store_.touched = true; });
|
||||
if (this->is_touched_) {
|
||||
this->set_timeout(TAG, this->touch_timeout_, [this]() { this->store_.touched = true; });
|
||||
} else {
|
||||
this->cancel_timeout(TAG);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -111,6 +114,7 @@ void Touchscreen::add_raw_touch_position_(uint8_t id, int16_t x_raw, int16_t y_r
|
||||
|
||||
void Touchscreen::send_touches_() {
|
||||
TouchPoints_t touches;
|
||||
ESP_LOGV(TAG, "Touch status: is_touched=%d, was_touched=%d", this->is_touched_, this->was_touched_);
|
||||
for (auto tp : this->touches_) {
|
||||
ESP_LOGV(TAG, "Touch status: %d/%d: raw:(%4d,%4d,%4d) calc:(%3d,%4d)", tp.second.id, tp.second.state,
|
||||
tp.second.x_raw, tp.second.y_raw, tp.second.z_raw, tp.second.x, tp.second.y);
|
||||
@ -124,14 +128,10 @@ void Touchscreen::send_touches_() {
|
||||
}
|
||||
if (!this->is_touched_) {
|
||||
if (this->was_touched_) {
|
||||
if (this->touch_timeout_ > 0) {
|
||||
this->cancel_timeout(TAG);
|
||||
}
|
||||
this->release_trigger_.trigger();
|
||||
for (auto *listener : this->touch_listeners_)
|
||||
listener->release();
|
||||
this->touches_.clear();
|
||||
this->was_touched_ = false;
|
||||
}
|
||||
} else {
|
||||
if (this->first_touch_) {
|
||||
@ -142,6 +142,7 @@ void Touchscreen::send_touches_() {
|
||||
}
|
||||
}
|
||||
}
|
||||
this->was_touched_ = this->is_touched_;
|
||||
}
|
||||
|
||||
int16_t Touchscreen::normalize_(int16_t val, int16_t min_val, int16_t max_val, bool inverted) {
|
||||
|
@ -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))
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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};
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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"
|
||||
|
@ -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:
|
||||
|
@ -358,7 +358,7 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) {
|
||||
stream->print(F("</article></body></html>"));
|
||||
request->send(stream);
|
||||
}
|
||||
#elif USE_WEBSERVER_VERSION == 2
|
||||
#elif USE_WEBSERVER_VERSION >= 2
|
||||
void WebServer::handle_index_request(AsyncWebServerRequest *request) {
|
||||
AsyncWebServerResponse *response =
|
||||
request->beginResponse_P(200, "text/html", ESPHOME_WEBSERVER_INDEX_HTML, ESPHOME_WEBSERVER_INDEX_HTML_SIZE);
|
||||
@ -486,7 +486,7 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM
|
||||
if (obj->get_object_id() != match.id)
|
||||
continue;
|
||||
|
||||
if (request->method() == HTTP_GET) {
|
||||
if (request->method() == HTTP_GET && match.method.empty()) {
|
||||
std::string data = this->switch_json(obj, obj->state, DETAIL_STATE);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
} else if (match.method == "toggle") {
|
||||
@ -517,7 +517,7 @@ void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlM
|
||||
for (button::Button *obj : App.get_buttons()) {
|
||||
if (obj->get_object_id() != match.id)
|
||||
continue;
|
||||
if (request->method() == HTTP_POST && match.method == "press") {
|
||||
if (match.method == "press") {
|
||||
this->schedule_([obj]() { obj->press(); });
|
||||
request->send(200);
|
||||
return;
|
||||
@ -572,7 +572,7 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc
|
||||
if (obj->get_object_id() != match.id)
|
||||
continue;
|
||||
|
||||
if (request->method() == HTTP_GET) {
|
||||
if (request->method() == HTTP_GET && match.method.empty()) {
|
||||
std::string data = this->fan_json(obj, DETAIL_STATE);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
} else if (match.method == "toggle") {
|
||||
@ -630,7 +630,7 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa
|
||||
if (obj->get_object_id() != match.id)
|
||||
continue;
|
||||
|
||||
if (request->method() == HTTP_GET) {
|
||||
if (request->method() == HTTP_GET && match.method.empty()) {
|
||||
std::string data = this->light_json(obj, DETAIL_STATE);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
} else if (match.method == "toggle") {
|
||||
@ -736,7 +736,7 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa
|
||||
if (obj->get_object_id() != match.id)
|
||||
continue;
|
||||
|
||||
if (request->method() == HTTP_GET) {
|
||||
if (request->method() == HTTP_GET && match.method.empty()) {
|
||||
std::string data = this->cover_json(obj, DETAIL_STATE);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
continue;
|
||||
@ -805,7 +805,7 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM
|
||||
if (obj->get_object_id() != match.id)
|
||||
continue;
|
||||
|
||||
if (request->method() == HTTP_GET) {
|
||||
if (request->method() == HTTP_GET && match.method.empty()) {
|
||||
std::string data = this->number_json(obj, obj->state, DETAIL_STATE);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
return;
|
||||
@ -910,7 +910,7 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat
|
||||
if (obj->get_object_id() != match.id)
|
||||
continue;
|
||||
|
||||
if (request->method() == HTTP_GET) {
|
||||
if (request->method() == HTTP_GET && match.method.empty()) {
|
||||
std::string data = this->text_json(obj, obj->state, DETAIL_STATE);
|
||||
request->send(200, "text/json", data.c_str());
|
||||
return;
|
||||
@ -961,7 +961,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM
|
||||
if (obj->get_object_id() != match.id)
|
||||
continue;
|
||||
|
||||
if (request->method() == HTTP_GET) {
|
||||
if (request->method() == HTTP_GET && match.method.empty()) {
|
||||
auto detail = DETAIL_STATE;
|
||||
auto *param = request->getParam("detail");
|
||||
if (param && param->value() == "all") {
|
||||
@ -1016,7 +1016,7 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url
|
||||
if (obj->get_object_id() != match.id)
|
||||
continue;
|
||||
|
||||
if (request->method() == HTTP_GET) {
|
||||
if (request->method() == HTTP_GET && match.method.empty()) {
|
||||
std::string data = this->climate_json(obj, DETAIL_STATE);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
return;
|
||||
@ -1162,7 +1162,7 @@ void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMat
|
||||
if (obj->get_object_id() != match.id)
|
||||
continue;
|
||||
|
||||
if (request->method() == HTTP_GET) {
|
||||
if (request->method() == HTTP_GET && match.method.empty()) {
|
||||
std::string data = this->lock_json(obj, obj->state, DETAIL_STATE);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
} else if (match.method == "lock") {
|
||||
@ -1201,7 +1201,7 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques
|
||||
if (obj->get_object_id() != match.id)
|
||||
continue;
|
||||
|
||||
if (request->method() == HTTP_GET) {
|
||||
if (request->method() == HTTP_GET && match.method.empty()) {
|
||||
std::string data = this->alarm_control_panel_json(obj, obj->get_state(), DETAIL_STATE);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
return;
|
||||
@ -1251,7 +1251,7 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) {
|
||||
#endif
|
||||
|
||||
#ifdef USE_BUTTON
|
||||
if (request->method() == HTTP_POST && match.domain == "button")
|
||||
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "button")
|
||||
return true;
|
||||
#endif
|
||||
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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!");
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""Constants used by esphome."""
|
||||
|
||||
__version__ = "2024.3.0-dev"
|
||||
__version__ = "2024.4.0-dev"
|
||||
|
||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||
VALID_SUBSTITUTIONS_CHARACTERS = (
|
||||
@ -856,6 +856,7 @@ CONF_VISUAL = "visual"
|
||||
CONF_VOLTAGE = "voltage"
|
||||
CONF_VOLTAGE_ATTENUATION = "voltage_attenuation"
|
||||
CONF_VOLTAGE_DIVIDER = "voltage_divider"
|
||||
CONF_VOLUME = "volume"
|
||||
CONF_WAIT_TIME = "wait_time"
|
||||
CONF_WAIT_UNTIL = "wait_until"
|
||||
CONF_WAKEUP_PIN = "wakeup_pin"
|
||||
|
@ -5,6 +5,7 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <utility>
|
||||
#include <cinttypes>
|
||||
|
||||
namespace esphome {
|
||||
|
||||
@ -140,18 +141,35 @@ bool Component::is_ready() {
|
||||
(this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_SETUP;
|
||||
}
|
||||
bool Component::can_proceed() { return true; }
|
||||
bool Component::status_has_warning() { return this->component_state_ & STATUS_LED_WARNING; }
|
||||
bool Component::status_has_error() { return this->component_state_ & STATUS_LED_ERROR; }
|
||||
void Component::status_set_warning() {
|
||||
bool Component::status_has_warning() const { return this->component_state_ & STATUS_LED_WARNING; }
|
||||
bool Component::status_has_error() const { return this->component_state_ & STATUS_LED_ERROR; }
|
||||
void Component::status_set_warning(const char *message) {
|
||||
// Don't spam the log. This risks missing different warning messages though.
|
||||
if ((this->component_state_ & STATUS_LED_WARNING) != 0)
|
||||
return;
|
||||
this->component_state_ |= STATUS_LED_WARNING;
|
||||
App.app_state_ |= STATUS_LED_WARNING;
|
||||
ESP_LOGW(this->get_component_source(), "Warning set: %s", message);
|
||||
}
|
||||
void Component::status_set_error() {
|
||||
void Component::status_set_error(const char *message) {
|
||||
if ((this->component_state_ & STATUS_LED_ERROR) != 0)
|
||||
return;
|
||||
this->component_state_ |= STATUS_LED_ERROR;
|
||||
App.app_state_ |= STATUS_LED_ERROR;
|
||||
ESP_LOGE(this->get_component_source(), "Error set: %s", message);
|
||||
}
|
||||
void Component::status_clear_warning() {
|
||||
if ((this->component_state_ & STATUS_LED_WARNING) == 0)
|
||||
return;
|
||||
this->component_state_ &= ~STATUS_LED_WARNING;
|
||||
ESP_LOGW(this->get_component_source(), "Warning cleared");
|
||||
}
|
||||
void Component::status_clear_error() {
|
||||
if ((this->component_state_ & STATUS_LED_ERROR) == 0)
|
||||
return;
|
||||
this->component_state_ &= ~STATUS_LED_ERROR;
|
||||
ESP_LOGE(this->get_component_source(), "Error cleared");
|
||||
}
|
||||
void Component::status_clear_warning() { this->component_state_ &= ~STATUS_LED_WARNING; }
|
||||
void Component::status_clear_error() { this->component_state_ &= ~STATUS_LED_ERROR; }
|
||||
void Component::status_momentary_warning(const std::string &name, uint32_t length) {
|
||||
this->status_set_warning();
|
||||
this->set_timeout(name, length, [this]() { this->status_clear_warning(); });
|
||||
@ -211,8 +229,8 @@ WarnIfComponentBlockingGuard::~WarnIfComponentBlockingGuard() {
|
||||
uint32_t now = millis();
|
||||
if (now - started_ > 50) {
|
||||
const char *src = component_ == nullptr ? "<null>" : component_->get_component_source();
|
||||
ESP_LOGW(TAG, "Component %s took a long time for an operation (%.2f s).", src, (now - started_) / 1e3f);
|
||||
ESP_LOGW(TAG, "Components should block for at most 20-30ms.");
|
||||
ESP_LOGW(TAG, "Component %s took a long time for an operation (%" PRIu32 " ms).", src, (now - started_));
|
||||
ESP_LOGW(TAG, "Components should block for at most 30 ms.");
|
||||
;
|
||||
}
|
||||
}
|
||||
|
@ -124,13 +124,13 @@ class Component {
|
||||
|
||||
virtual bool can_proceed();
|
||||
|
||||
bool status_has_warning();
|
||||
bool status_has_warning() const;
|
||||
|
||||
bool status_has_error();
|
||||
bool status_has_error() const;
|
||||
|
||||
void status_set_warning();
|
||||
void status_set_warning(const char *message = "unspecified");
|
||||
|
||||
void status_set_error();
|
||||
void status_set_error(const char *message = "unspecified");
|
||||
|
||||
void status_clear_warning();
|
||||
|
||||
|
@ -98,7 +98,7 @@
|
||||
// ESP8266-specific feature flags
|
||||
#ifdef USE_ESP8266
|
||||
#define USE_ADC_SENSOR_VCC
|
||||
#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 0, 2)
|
||||
#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 1, 2)
|
||||
#define USE_ESP8266_PREFERENCES_FLASH
|
||||
#define USE_HTTP_REQUEST_ESP8266_HTTPS
|
||||
#define USE_SOCKET_IMPL_LWIP_TCP
|
||||
|
@ -12,9 +12,11 @@
|
||||
#include <cstring>
|
||||
|
||||
#ifdef USE_HOST
|
||||
#ifndef _WIN32
|
||||
#include <net/if.h>
|
||||
#include <netinet/in.h>
|
||||
#include <sys/ioctl.h>
|
||||
#endif
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
#if defined(USE_ESP8266)
|
||||
|
@ -516,7 +516,8 @@ class ImportRequestHandler(BaseHandler):
|
||||
self.set_status(500)
|
||||
self.write("File already exists")
|
||||
return
|
||||
except ValueError:
|
||||
except ValueError as e:
|
||||
_LOGGER.error(e)
|
||||
self.set_status(422)
|
||||
self.write("Invalid package url")
|
||||
return
|
||||
@ -687,6 +688,11 @@ class MainRequestHandler(BaseHandler):
|
||||
@authenticated
|
||||
def get(self) -> None:
|
||||
begin = bool(self.get_argument("begin", False))
|
||||
if settings.using_password:
|
||||
# Simply accessing the xsrf_token sets the cookie for us
|
||||
self.xsrf_token # pylint: disable=pointless-statement
|
||||
else:
|
||||
self.clear_cookie("_xsrf")
|
||||
|
||||
self.render(
|
||||
"index.template.html",
|
||||
@ -1101,6 +1107,7 @@ def make_app(debug=get_bool_env(ENV_DEV)) -> tornado.web.Application:
|
||||
"log_function": log_function,
|
||||
"websocket_ping_interval": 30.0,
|
||||
"template_path": get_base_frontend_path(),
|
||||
"xsrf_cookies": settings.using_password,
|
||||
}
|
||||
rel = settings.relative_url
|
||||
return tornado.web.Application(
|
||||
|
@ -33,7 +33,9 @@ def has_remote_file_changed(url, local_file_path):
|
||||
IF_MODIFIED_SINCE: local_modification_time_str,
|
||||
CACHE_CONTROL: CACHE_CONTROL_MAX_AGE + "3600",
|
||||
}
|
||||
response = requests.head(url, headers=headers, timeout=NETWORK_TIMEOUT)
|
||||
response = requests.head(
|
||||
url, headers=headers, timeout=NETWORK_TIMEOUT, allow_redirects=True
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
"has_remote_file_changed: File %s, Local modified %s, response code %d",
|
||||
|
@ -311,10 +311,18 @@ def gpio_base_schema(
|
||||
map(lambda m: (cv.Optional(m, default=mode_default), cv.boolean), modes)
|
||||
)
|
||||
|
||||
def _number_validator(value):
|
||||
if isinstance(value, str) and value.upper().startswith("GPIOX"):
|
||||
raise cv.Invalid(
|
||||
f"Found placeholder '{value}' when expecting a GPIO pin number.\n"
|
||||
"You must replace this with an actual pin number."
|
||||
)
|
||||
return number_validator(value)
|
||||
|
||||
schema = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(pin_type),
|
||||
cv.Required(CONF_NUMBER): number_validator,
|
||||
cv.Required(CONF_NUMBER): _number_validator,
|
||||
cv.Optional(CONF_ALLOW_OTHER_USES): cv.boolean,
|
||||
cv.Optional(CONF_MODE, default={}): cv.All(mode_dict, mode_validator),
|
||||
}
|
||||
|
@ -81,9 +81,9 @@ build_flags =
|
||||
; This are common settings for the ESP8266 using Arduino.
|
||||
[common:esp8266-arduino]
|
||||
extends = common:arduino
|
||||
platform = platformio/espressif8266@3.2.0
|
||||
platform = platformio/espressif8266@4.2.1
|
||||
platform_packages =
|
||||
platformio/framework-arduinoespressif8266@~3.30002.0
|
||||
platformio/framework-arduinoespressif8266@~3.30102.0
|
||||
|
||||
framework = arduino
|
||||
lib_deps =
|
||||
|
@ -12,10 +12,11 @@ pyserial==3.5
|
||||
platformio==6.1.13 # When updating platformio, also update Dockerfile
|
||||
esptool==4.7.0
|
||||
click==8.1.7
|
||||
esphome-dashboard==20231107.0
|
||||
aioesphomeapi==23.1.1
|
||||
esphome-dashboard==20240319.0
|
||||
aioesphomeapi==23.2.0
|
||||
zeroconf==0.131.0
|
||||
python-magic==0.4.27
|
||||
ruamel.yaml==0.18.6 # dashboard_import
|
||||
|
||||
# esp-idf requires this, but doesn't bundle it by default
|
||||
# https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24
|
||||
|
@ -8,6 +8,6 @@ pre-commit
|
||||
pytest==8.1.1
|
||||
pytest-cov==4.1.0
|
||||
pytest-mock==3.12.0
|
||||
pytest-asyncio==0.23.5.post1
|
||||
pytest-asyncio==0.23.6
|
||||
asyncmock==0.4.2
|
||||
hypothesis==6.92.1
|
||||
|
@ -70,11 +70,11 @@ def splitlines_no_ends(string):
|
||||
return [s.strip() for s in string.splitlines()]
|
||||
|
||||
|
||||
def changed_files():
|
||||
def changed_files(branch="dev"):
|
||||
check_remotes = ["upstream", "origin"]
|
||||
check_remotes.extend(splitlines_no_ends(get_output("git", "remote")))
|
||||
for remote in check_remotes:
|
||||
command = ["git", "merge-base", f"refs/remotes/{remote}/dev", "HEAD"]
|
||||
command = ["git", "merge-base", f"refs/remotes/{remote}/{branch}", "HEAD"]
|
||||
try:
|
||||
merge_base = splitlines_no_ends(get_output(*command))[0]
|
||||
break
|
||||
|
@ -120,13 +120,22 @@ def main():
|
||||
parser.add_argument(
|
||||
"-c", "--changed", action="store_true", help="Only run on changed files"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-b", "--branch", help="Branch to compare changed files against"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.branch and not args.changed:
|
||||
parser.error("--branch requires --changed")
|
||||
|
||||
files = git_ls_files()
|
||||
files = filter(filter_component_files, files)
|
||||
|
||||
if args.changed:
|
||||
changed = changed_files()
|
||||
if args.branch:
|
||||
changed = changed_files(args.branch)
|
||||
else:
|
||||
changed = changed_files()
|
||||
files = [f for f in files if f in changed]
|
||||
|
||||
components = extract_component_names_array_from_files_array(files)
|
||||
|
@ -1,3 +1,16 @@
|
||||
esphome:
|
||||
on_boot:
|
||||
then:
|
||||
- homeassistant.event:
|
||||
event: esphome.button_pressed
|
||||
data:
|
||||
message: Button was pressed
|
||||
- homeassistant.service:
|
||||
service: notify.html5
|
||||
data:
|
||||
message: Button was pressed
|
||||
- homeassistant.tag_scanned: pulse
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
@ -1,3 +1,16 @@
|
||||
esphome:
|
||||
on_boot:
|
||||
then:
|
||||
- homeassistant.event:
|
||||
event: esphome.button_pressed
|
||||
data:
|
||||
message: Button was pressed
|
||||
- homeassistant.service:
|
||||
service: notify.html5
|
||||
data:
|
||||
message: Button was pressed
|
||||
- homeassistant.tag_scanned: pulse
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
@ -1,3 +1,16 @@
|
||||
esphome:
|
||||
on_boot:
|
||||
then:
|
||||
- homeassistant.event:
|
||||
event: esphome.button_pressed
|
||||
data:
|
||||
message: Button was pressed
|
||||
- homeassistant.service:
|
||||
service: notify.html5
|
||||
data:
|
||||
message: Button was pressed
|
||||
- homeassistant.tag_scanned: pulse
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user