Merge branch 'remoteDev' into esp-adf-take2

This commit is contained in:
X-Ryl669 2024-04-05 14:41:35 +02:00
commit 0d21eca6c0
891 changed files with 27049 additions and 1210 deletions

View File

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

View File

@ -17,12 +17,12 @@ runs:
steps:
- name: Set up Python ${{ inputs.python-version }}
id: python
uses: actions/setup-python@v5.0.0
uses: actions/setup-python@v5.1.0
with:
python-version: ${{ inputs.python-version }}
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache/restore@v4.0.1
uses: actions/cache/restore@v4.0.2
with:
path: venv
# yamllint disable-line rule:line-length

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

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

View File

@ -2,7 +2,7 @@
name: CI for docker images
# Only run when docker paths change
# yamllint disable-line rule:truthy
on:
push:
branches: [dev, beta, release]
@ -42,11 +42,11 @@ jobs:
steps:
- uses: actions/checkout@v4.1.1
- name: Set up Python
uses: actions/setup-python@v5.0.0
uses: actions/setup-python@v5.1.0
with:
python-version: "3.9"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.1.0
uses: docker/setup-buildx-action@v3.2.0
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.0.0

View File

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

View File

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

View File

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

View File

@ -1,7 +1,6 @@
---
name: Publish Release
# yamllint disable-line rule:truthy
on:
workflow_dispatch:
release:
@ -45,7 +44,7 @@ jobs:
steps:
- uses: actions/checkout@v4.1.1
- name: Set up Python
uses: actions/setup-python@v5.0.0
uses: actions/setup-python@v5.1.0
with:
python-version: "3.x"
- name: Set up python environment
@ -80,23 +79,23 @@ jobs:
steps:
- uses: actions/checkout@v4.1.1
- name: Set up Python
uses: actions/setup-python@v5.0.0
uses: actions/setup-python@v5.1.0
with:
python-version: "3.9"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.1.0
uses: docker/setup-buildx-action@v3.2.0
- name: Set up QEMU
if: matrix.platform != 'linux/amd64'
uses: docker/setup-qemu-action@v3.0.0
- name: Log in to docker hub
uses: docker/login-action@v3.0.0
uses: docker/login-action@v3.1.0
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the GitHub container registry
uses: docker/login-action@v3.0.0
uses: docker/login-action@v3.1.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@ -163,17 +162,17 @@ jobs:
name: digests-${{ matrix.image.target }}-${{ matrix.registry }}
path: /tmp/digests
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.1.0
uses: docker/setup-buildx-action@v3.2.0
- name: Log in to docker hub
if: matrix.registry == 'dockerhub'
uses: docker/login-action@v3.0.0
uses: docker/login-action@v3.1.0
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the GitHub container registry
if: matrix.registry == 'ghcr'
uses: docker/login-action@v3.0.0
uses: docker/login-action@v3.1.0
with:
registry: ghcr.io
username: ${{ github.actor }}

View File

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

View File

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

View File

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

View File

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

View File

@ -42,6 +42,7 @@ esphome/components/as5600/* @ammmze
esphome/components/as5600/sensor/* @ammmze
esphome/components/as7341/* @mrgnr
esphome/components/async_tcp/* @OttoWinter
esphome/components/at581x/* @X-Ryl669
esphome/components/atc_mithermometer/* @ahpohl
esphome/components/atm90e26/* @danieltwagner
esphome/components/b_parasite/* @rbaron
@ -86,6 +87,7 @@ esphome/components/cst816/* @clydebarrow
esphome/components/ct_clamp/* @jesserockz
esphome/components/current_based/* @djwmarcx
esphome/components/dac7678/* @NickB1
esphome/components/daikin_arc/* @MagicBear
esphome/components/daikin_brc/* @hagak
esphome/components/daly_bms/* @s1lvi0
esphome/components/dashboard_import/* @esphome/core
@ -103,6 +105,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
@ -153,6 +156,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,7 +175,9 @@ esphome/components/inkplate6/* @jesserockz
esphome/components/integration/* @OttoWinter
esphome/components/internal_temperature/* @Mat931
esphome/components/interval/* @esphome/core
esphome/components/jsn_sr04t/* @Mafus1
esphome/components/json/* @OttoWinter
esphome/components/kamstrup_kmp/* @cfeenstra1024
esphome/components/key_collector/* @ssieb
esphome/components/key_provider/* @ssieb
esphome/components/kuntze/* @ssieb
@ -270,6 +276,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
@ -283,6 +290,7 @@ esphome/components/rgbct/* @jesserockz
esphome/components/rp2040/* @jesserockz
esphome/components/rp2040_pio_led_strip/* @Papa-DMan
esphome/components/rp2040_pwm/* @jesserockz
esphome/components/rpi_dpi_rgb/* @clydebarrow
esphome/components/rtl87xx/* @kuba2k2
esphome/components/rtttl/* @glmnet
esphome/components/safe_mode/* @jsuanet @paulmonigatti
@ -290,6 +298,7 @@ esphome/components/scd4x/* @martgras @sjtrny
esphome/components/script/* @esphome/core
esphome/components/sdm_meter/* @jesserockz @polyfaces
esphome/components/sdp3x/* @Azimath
esphome/components/seeed_mr24hpc1/* @limengdu
esphome/components/selec_meter/* @sourabhjaiswal
esphome/components/select/* @esphome/core
esphome/components/sen0321/* @notjj
@ -301,6 +310,7 @@ esphome/components/sfa30/* @ghsensdev
esphome/components/sgp40/* @SenexCrenshaw
esphome/components/sgp4x/* @SenexCrenshaw @martgras
esphome/components/shelly_dimmer/* @edge90 @rnauber
esphome/components/sht3xd/* @mrtoy-me
esphome/components/sht4x/* @sjtrny
esphome/components/shutdown/* @esphome/core @jsuanet
esphome/components/sigma_delta_output/* @Cat-Ion
@ -334,11 +344,13 @@ esphome/components/ssd1351_spi/* @kbx81
esphome/components/st7567_base/* @latonita
esphome/components/st7567_i2c/* @latonita
esphome/components/st7567_spi/* @latonita
esphome/components/st7701s/* @clydebarrow
esphome/components/st7735/* @SenexCrenshaw
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
@ -379,6 +391,7 @@ esphome/components/ultrasonic/* @OttoWinter
esphome/components/uponor_smatrix/* @kroimon
esphome/components/vbus/* @ssieb
esphome/components/veml3235/* @kbx81
esphome/components/veml7700/* @latonita
esphome/components/version/* @esphome/core
esphome/components/voice_assistant/* @jesserockz
esphome/components/wake_on_lan/* @willwill2will54

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -117,7 +117,7 @@ void ATM90E26Component::setup() {
this->write16_(ATM90E26_REGISTER_ADJSTART,
0x8765); // Checks correctness of 31-3A registers and starts normal measurement if ok
uint16_t sys_status = this->read16_(ATM90E26_REGISTER_SYSSTATUS);
const uint16_t sys_status = this->read16_(ATM90E26_REGISTER_SYSSTATUS);
if (sys_status & 0xC000) { // Checksum 1 Error
ESP_LOGW(TAG, "Could not initialize ATM90E26 IC: CS1 was incorrect, expected: 0x%04X",
@ -177,27 +177,27 @@ void ATM90E26Component::write16_(uint8_t a_register, uint16_t val) {
}
float ATM90E26Component::get_line_current_() {
uint16_t current = this->read16_(ATM90E26_REGISTER_IRMS);
const uint16_t current = this->read16_(ATM90E26_REGISTER_IRMS);
return current / 1000.0f;
}
float ATM90E26Component::get_line_voltage_() {
uint16_t voltage = this->read16_(ATM90E26_REGISTER_URMS);
const uint16_t voltage = this->read16_(ATM90E26_REGISTER_URMS);
return voltage / 100.0f;
}
float ATM90E26Component::get_active_power_() {
int16_t val = this->read16_(ATM90E26_REGISTER_PMEAN); // two's complement
const int16_t val = this->read16_(ATM90E26_REGISTER_PMEAN); // two's complement
return (float) val;
}
float ATM90E26Component::get_reactive_power_() {
int16_t val = this->read16_(ATM90E26_REGISTER_QMEAN); // two's complement
const int16_t val = this->read16_(ATM90E26_REGISTER_QMEAN); // two's complement
return (float) val;
}
float ATM90E26Component::get_power_factor_() {
uint16_t val = this->read16_(ATM90E26_REGISTER_POWERF); // signed
const uint16_t val = this->read16_(ATM90E26_REGISTER_POWERF); // signed
if (val & 0x8000) {
return -(val & 0x7FF) / 1000.0f;
} else {
@ -206,7 +206,7 @@ float ATM90E26Component::get_power_factor_() {
}
float ATM90E26Component::get_forward_active_energy_() {
uint16_t val = this->read16_(ATM90E26_REGISTER_APENERGY);
const uint16_t val = this->read16_(ATM90E26_REGISTER_APENERGY);
if ((UINT32_MAX - this->cumulative_forward_active_energy_) > val) {
this->cumulative_forward_active_energy_ += val;
} else {
@ -217,7 +217,7 @@ float ATM90E26Component::get_forward_active_energy_() {
}
float ATM90E26Component::get_reverse_active_energy_() {
uint16_t val = this->read16_(ATM90E26_REGISTER_ANENERGY);
const uint16_t val = this->read16_(ATM90E26_REGISTER_ANENERGY);
if (UINT32_MAX - this->cumulative_reverse_active_energy_ > val) {
this->cumulative_reverse_active_energy_ += val;
} else {
@ -227,7 +227,7 @@ float ATM90E26Component::get_reverse_active_energy_() {
}
float ATM90E26Component::get_frequency_() {
uint16_t freq = this->read16_(ATM90E26_REGISTER_FREQ);
const uint16_t freq = this->read16_(ATM90E26_REGISTER_FREQ);
return freq / 100.0f;
}

View File

@ -7,82 +7,128 @@ namespace esphome {
namespace atm90e32 {
static const char *const TAG = "atm90e32";
void ATM90E32Component::loop() {
if (this->get_publish_interval_flag_()) {
this->set_publish_interval_flag_(false);
for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].voltage_sensor_ != nullptr) {
this->phase_[phase].voltage_ = this->get_phase_voltage_(phase);
}
}
for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].current_sensor_ != nullptr) {
this->phase_[phase].current_ = this->get_phase_current_(phase);
}
}
for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].power_sensor_ != nullptr) {
this->phase_[phase].active_power_ = this->get_phase_active_power_(phase);
}
}
for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].power_factor_sensor_ != nullptr) {
this->phase_[phase].power_factor_ = this->get_phase_power_factor_(phase);
}
}
for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].reactive_power_sensor_ != nullptr) {
this->phase_[phase].reactive_power_ = this->get_phase_reactive_power_(phase);
}
}
for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].forward_active_energy_sensor_ != nullptr) {
this->phase_[phase].forward_active_energy_ = this->get_phase_forward_active_energy_(phase);
}
}
for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].reverse_active_energy_sensor_ != nullptr) {
this->phase_[phase].reverse_active_energy_ = this->get_phase_reverse_active_energy_(phase);
}
}
for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].phase_angle_sensor_ != nullptr) {
this->phase_[phase].phase_angle_ = this->get_phase_angle_(phase);
}
}
for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].harmonic_active_power_sensor_ != nullptr) {
this->phase_[phase].harmonic_active_power_ = this->get_phase_harmonic_active_power_(phase);
}
}
for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].peak_current_sensor_ != nullptr) {
this->phase_[phase].peak_current_ = this->get_phase_peak_current_(phase);
}
}
// After the local store in collected we can publish them trusting they are withing +-1 haardware sampling
for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].voltage_sensor_ != nullptr) {
this->phase_[phase].voltage_sensor_->publish_state(this->get_local_phase_voltage_(phase));
}
}
for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].current_sensor_ != nullptr) {
this->phase_[phase].current_sensor_->publish_state(this->get_local_phase_current_(phase));
}
}
for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].power_sensor_ != nullptr) {
this->phase_[phase].power_sensor_->publish_state(this->get_local_phase_active_power_(phase));
}
}
for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].power_factor_sensor_ != nullptr) {
this->phase_[phase].power_factor_sensor_->publish_state(this->get_local_phase_power_factor_(phase));
}
}
for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].reactive_power_sensor_ != nullptr) {
this->phase_[phase].reactive_power_sensor_->publish_state(this->get_local_phase_reactive_power_(phase));
}
}
for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].forward_active_energy_sensor_ != nullptr) {
this->phase_[phase].forward_active_energy_sensor_->publish_state(
this->get_local_phase_forward_active_energy_(phase));
}
}
for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].reverse_active_energy_sensor_ != nullptr) {
this->phase_[phase].reverse_active_energy_sensor_->publish_state(
this->get_local_phase_reverse_active_energy_(phase));
}
}
for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].phase_angle_sensor_ != nullptr) {
this->phase_[phase].phase_angle_sensor_->publish_state(this->get_local_phase_angle_(phase));
}
}
for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].harmonic_active_power_sensor_ != nullptr) {
this->phase_[phase].harmonic_active_power_sensor_->publish_state(
this->get_local_phase_harmonic_active_power_(phase));
}
}
for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].peak_current_sensor_ != nullptr) {
this->phase_[phase].peak_current_sensor_->publish_state(this->get_local_phase_peak_current_(phase));
}
}
if (this->freq_sensor_ != nullptr) {
this->freq_sensor_->publish_state(this->get_frequency_());
}
if (this->chip_temperature_sensor_ != nullptr) {
this->chip_temperature_sensor_->publish_state(this->get_chip_temperature_());
}
}
}
void ATM90E32Component::update() {
if (this->read16_(ATM90E32_REGISTER_METEREN) != 1) {
this->status_set_warning();
return;
}
if (this->phase_[0].voltage_sensor_ != nullptr) {
this->phase_[0].voltage_sensor_->publish_state(this->get_line_voltage_a_());
}
if (this->phase_[1].voltage_sensor_ != nullptr) {
this->phase_[1].voltage_sensor_->publish_state(this->get_line_voltage_b_());
}
if (this->phase_[2].voltage_sensor_ != nullptr) {
this->phase_[2].voltage_sensor_->publish_state(this->get_line_voltage_c_());
}
if (this->phase_[0].current_sensor_ != nullptr) {
this->phase_[0].current_sensor_->publish_state(this->get_line_current_a_());
}
if (this->phase_[1].current_sensor_ != nullptr) {
this->phase_[1].current_sensor_->publish_state(this->get_line_current_b_());
}
if (this->phase_[2].current_sensor_ != nullptr) {
this->phase_[2].current_sensor_->publish_state(this->get_line_current_c_());
}
if (this->phase_[0].power_sensor_ != nullptr) {
this->phase_[0].power_sensor_->publish_state(this->get_active_power_a_());
}
if (this->phase_[1].power_sensor_ != nullptr) {
this->phase_[1].power_sensor_->publish_state(this->get_active_power_b_());
}
if (this->phase_[2].power_sensor_ != nullptr) {
this->phase_[2].power_sensor_->publish_state(this->get_active_power_c_());
}
if (this->phase_[0].reactive_power_sensor_ != nullptr) {
this->phase_[0].reactive_power_sensor_->publish_state(this->get_reactive_power_a_());
}
if (this->phase_[1].reactive_power_sensor_ != nullptr) {
this->phase_[1].reactive_power_sensor_->publish_state(this->get_reactive_power_b_());
}
if (this->phase_[2].reactive_power_sensor_ != nullptr) {
this->phase_[2].reactive_power_sensor_->publish_state(this->get_reactive_power_c_());
}
if (this->phase_[0].power_factor_sensor_ != nullptr) {
this->phase_[0].power_factor_sensor_->publish_state(this->get_power_factor_a_());
}
if (this->phase_[1].power_factor_sensor_ != nullptr) {
this->phase_[1].power_factor_sensor_->publish_state(this->get_power_factor_b_());
}
if (this->phase_[2].power_factor_sensor_ != nullptr) {
this->phase_[2].power_factor_sensor_->publish_state(this->get_power_factor_c_());
}
if (this->phase_[0].forward_active_energy_sensor_ != nullptr) {
this->phase_[0].forward_active_energy_sensor_->publish_state(this->get_forward_active_energy_a_());
}
if (this->phase_[1].forward_active_energy_sensor_ != nullptr) {
this->phase_[1].forward_active_energy_sensor_->publish_state(this->get_forward_active_energy_b_());
}
if (this->phase_[2].forward_active_energy_sensor_ != nullptr) {
this->phase_[2].forward_active_energy_sensor_->publish_state(this->get_forward_active_energy_c_());
}
if (this->phase_[0].reverse_active_energy_sensor_ != nullptr) {
this->phase_[0].reverse_active_energy_sensor_->publish_state(this->get_reverse_active_energy_a_());
}
if (this->phase_[1].reverse_active_energy_sensor_ != nullptr) {
this->phase_[1].reverse_active_energy_sensor_->publish_state(this->get_reverse_active_energy_b_());
}
if (this->phase_[2].reverse_active_energy_sensor_ != nullptr) {
this->phase_[2].reverse_active_energy_sensor_->publish_state(this->get_reverse_active_energy_c_());
}
if (this->freq_sensor_ != nullptr) {
this->freq_sensor_->publish_state(this->get_frequency_());
}
if (this->chip_temperature_sensor_ != nullptr) {
this->chip_temperature_sensor_->publish_state(this->get_chip_temperature_());
}
this->set_publish_interval_flag_(true);
this->status_clear_warning();
}
@ -101,29 +147,51 @@ void ATM90E32Component::setup() {
}
this->write16_(ATM90E32_REGISTER_SOFTRESET, 0x789A); // Perform soft reset
delay(6); // Wait for the minimum 5ms + 1ms
this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x55AA); // enable register config access
this->write16_(ATM90E32_REGISTER_METEREN, 0x0001); // Enable Metering
if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != 0x0001) {
if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != 0x55AA) {
ESP_LOGW(TAG, "Could not initialize ATM90E32 IC, check SPI settings");
this->mark_failed();
return;
}
this->write16_(ATM90E32_REGISTER_PLCONSTH, 0x0861); // PL Constant MSB (default) = 140625000
this->write16_(ATM90E32_REGISTER_PLCONSTL, 0xC468); // PL Constant LSB (default)
this->write16_(ATM90E32_REGISTER_ZXCONFIG, 0xD654); // ZX2, ZX1, ZX0 pin config
this->write16_(ATM90E32_REGISTER_MMODE0, mmode0); // Mode Config (frequency set in main program)
this->write16_(ATM90E32_REGISTER_MMODE1, pga_gain_); // PGA Gain Configuration for Current Channels
this->write16_(ATM90E32_REGISTER_PSTARTTH, 0x1D4C); // All Active Startup Power Threshold - 0.02A/0.00032 = 7500
this->write16_(ATM90E32_REGISTER_QSTARTTH, 0x1D4C); // All Reactive Startup Power Threshold - 50%
this->write16_(ATM90E32_REGISTER_PPHASETH, 0x02EE); // Each Phase Active Phase Threshold - 0.002A/0.00032 = 750
this->write16_(ATM90E32_REGISTER_QPHASETH, 0x02EE); // Each phase Reactive Phase Threshold - 10%
this->write16_(ATM90E32_REGISTER_UGAINA, this->phase_[0].volt_gain_); // A Voltage rms gain
this->write16_(ATM90E32_REGISTER_IGAINA, this->phase_[0].ct_gain_); // A line current gain
this->write16_(ATM90E32_REGISTER_UGAINB, this->phase_[1].volt_gain_); // B Voltage rms gain
this->write16_(ATM90E32_REGISTER_IGAINB, this->phase_[1].ct_gain_); // B line current gain
this->write16_(ATM90E32_REGISTER_UGAINC, this->phase_[2].volt_gain_); // C Voltage rms gain
this->write16_(ATM90E32_REGISTER_IGAINC, this->phase_[2].ct_gain_); // C line current gain
this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000); // end configuration
this->write16_(ATM90E32_REGISTER_METEREN, 0x0001); // Enable Metering
this->write16_(ATM90E32_REGISTER_SAGPEAKDETCFG, 0xFF3F); // Peak Detector time ms (15:8), Sag Period ms (7:0)
this->write16_(ATM90E32_REGISTER_PLCONSTH, 0x0861); // PL Constant MSB (default) = 140625000
this->write16_(ATM90E32_REGISTER_PLCONSTL, 0xC468); // PL Constant LSB (default)
this->write16_(ATM90E32_REGISTER_ZXCONFIG, 0xD654); // ZX2, ZX1, ZX0 pin config
this->write16_(ATM90E32_REGISTER_MMODE0, mmode0); // Mode Config (frequency set in main program)
this->write16_(ATM90E32_REGISTER_MMODE1, pga_gain_); // PGA Gain Configuration for Current Channels
this->write16_(ATM90E32_REGISTER_PSTARTTH, 0x1D4C); // All Active Startup Power Threshold - 0.02A/0.00032 = 7500
this->write16_(ATM90E32_REGISTER_QSTARTTH, 0x1D4C); // All Reactive Startup Power Threshold - 50%
this->write16_(ATM90E32_REGISTER_SSTARTTH, 0x1D4C); // All Reactive Startup Power Threshold - 50%
this->write16_(ATM90E32_REGISTER_PPHASETH, 0x02EE); // Each Phase Active Phase Threshold - 0.002A/0.00032 = 750
this->write16_(ATM90E32_REGISTER_QPHASETH, 0x02EE); // Each phase Reactive Phase Threshold - 10%
// Setup voltage and current calibration offsets for PHASE A
this->phase_[PHASEA].voltage_offset_ = calibrate_voltage_offset_phase(PHASEA);
this->write16_(ATM90E32_REGISTER_UOFFSETA, this->phase_[PHASEA].voltage_offset_); // A Voltage offset
this->phase_[PHASEA].current_offset_ = calibrate_current_offset_phase(PHASEA);
this->write16_(ATM90E32_REGISTER_IOFFSETA, this->phase_[PHASEA].current_offset_); // A Current offset
// Setup voltage and current gain for PHASE A
this->write16_(ATM90E32_REGISTER_UGAINA, this->phase_[PHASEA].voltage_gain_); // A Voltage rms gain
this->write16_(ATM90E32_REGISTER_IGAINA, this->phase_[PHASEA].ct_gain_); // A line current gain
// Setup voltage and current calibration offsets for PHASE B
this->phase_[PHASEB].voltage_offset_ = calibrate_voltage_offset_phase(PHASEB);
this->write16_(ATM90E32_REGISTER_UOFFSETB, this->phase_[PHASEB].voltage_offset_); // B Voltage offset
this->phase_[PHASEB].current_offset_ = calibrate_current_offset_phase(PHASEB);
this->write16_(ATM90E32_REGISTER_IOFFSETB, this->phase_[PHASEB].current_offset_); // B Current offset
// Setup voltage and current gain for PHASE B
this->write16_(ATM90E32_REGISTER_UGAINB, this->phase_[PHASEB].voltage_gain_); // B Voltage rms gain
this->write16_(ATM90E32_REGISTER_IGAINB, this->phase_[PHASEB].ct_gain_); // B line current gain
// Setup voltage and current calibration offsets for PHASE C
this->phase_[PHASEC].voltage_offset_ = calibrate_voltage_offset_phase(PHASEC);
this->write16_(ATM90E32_REGISTER_UOFFSETC, this->phase_[PHASEC].voltage_offset_); // C Voltage offset
this->phase_[PHASEC].current_offset_ = calibrate_current_offset_phase(PHASEC);
this->write16_(ATM90E32_REGISTER_IOFFSETC, this->phase_[PHASEC].current_offset_); // C Current offset
// Setup voltage and current gain for PHASE C
this->write16_(ATM90E32_REGISTER_UGAINC, this->phase_[PHASEC].voltage_gain_); // C Voltage rms gain
this->write16_(ATM90E32_REGISTER_IGAINC, this->phase_[PHASEC].ct_gain_); // C line current gain
this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000); // end configuration
}
void ATM90E32Component::dump_config() {
@ -133,43 +201,54 @@ void ATM90E32Component::dump_config() {
ESP_LOGE(TAG, "Communication with ATM90E32 failed!");
}
LOG_UPDATE_INTERVAL(this);
LOG_SENSOR(" ", "Voltage A", this->phase_[0].voltage_sensor_);
LOG_SENSOR(" ", "Current A", this->phase_[0].current_sensor_);
LOG_SENSOR(" ", "Power A", this->phase_[0].power_sensor_);
LOG_SENSOR(" ", "Reactive Power A", this->phase_[0].reactive_power_sensor_);
LOG_SENSOR(" ", "PF A", this->phase_[0].power_factor_sensor_);
LOG_SENSOR(" ", "Active Forward Energy A", this->phase_[0].forward_active_energy_sensor_);
LOG_SENSOR(" ", "Active Reverse Energy A", this->phase_[0].reverse_active_energy_sensor_);
LOG_SENSOR(" ", "Voltage B", this->phase_[1].voltage_sensor_);
LOG_SENSOR(" ", "Current B", this->phase_[1].current_sensor_);
LOG_SENSOR(" ", "Power B", this->phase_[1].power_sensor_);
LOG_SENSOR(" ", "Reactive Power B", this->phase_[1].reactive_power_sensor_);
LOG_SENSOR(" ", "PF B", this->phase_[1].power_factor_sensor_);
LOG_SENSOR(" ", "Active Forward Energy B", this->phase_[1].forward_active_energy_sensor_);
LOG_SENSOR(" ", "Active Reverse Energy B", this->phase_[1].reverse_active_energy_sensor_);
LOG_SENSOR(" ", "Voltage C", this->phase_[2].voltage_sensor_);
LOG_SENSOR(" ", "Current C", this->phase_[2].current_sensor_);
LOG_SENSOR(" ", "Power C", this->phase_[2].power_sensor_);
LOG_SENSOR(" ", "Reactive Power C", this->phase_[2].reactive_power_sensor_);
LOG_SENSOR(" ", "PF C", this->phase_[2].power_factor_sensor_);
LOG_SENSOR(" ", "Active Forward Energy C", this->phase_[2].forward_active_energy_sensor_);
LOG_SENSOR(" ", "Active Reverse Energy C", this->phase_[2].reverse_active_energy_sensor_);
LOG_SENSOR(" ", "Voltage A", this->phase_[PHASEA].voltage_sensor_);
LOG_SENSOR(" ", "Current A", this->phase_[PHASEA].current_sensor_);
LOG_SENSOR(" ", "Power A", this->phase_[PHASEA].power_sensor_);
LOG_SENSOR(" ", "Reactive Power A", this->phase_[PHASEA].reactive_power_sensor_);
LOG_SENSOR(" ", "PF A", this->phase_[PHASEA].power_factor_sensor_);
LOG_SENSOR(" ", "Active Forward Energy A", this->phase_[PHASEA].forward_active_energy_sensor_);
LOG_SENSOR(" ", "Active Reverse Energy A", this->phase_[PHASEA].reverse_active_energy_sensor_);
LOG_SENSOR(" ", "Harmonic Power A", this->phase_[PHASEA].harmonic_active_power_sensor_);
LOG_SENSOR(" ", "Phase Angle A", this->phase_[PHASEA].phase_angle_sensor_);
LOG_SENSOR(" ", "Peak Current A", this->phase_[PHASEA].peak_current_sensor_);
LOG_SENSOR(" ", "Voltage B", this->phase_[PHASEB].voltage_sensor_);
LOG_SENSOR(" ", "Current B", this->phase_[PHASEB].current_sensor_);
LOG_SENSOR(" ", "Power B", this->phase_[PHASEB].power_sensor_);
LOG_SENSOR(" ", "Reactive Power B", this->phase_[PHASEB].reactive_power_sensor_);
LOG_SENSOR(" ", "PF B", this->phase_[PHASEB].power_factor_sensor_);
LOG_SENSOR(" ", "Active Forward Energy B", this->phase_[PHASEB].forward_active_energy_sensor_);
LOG_SENSOR(" ", "Active Reverse Energy B", this->phase_[PHASEB].reverse_active_energy_sensor_);
LOG_SENSOR(" ", "Harmonic Power A", this->phase_[PHASEB].harmonic_active_power_sensor_);
LOG_SENSOR(" ", "Phase Angle A", this->phase_[PHASEB].phase_angle_sensor_);
LOG_SENSOR(" ", "Peak Current A", this->phase_[PHASEB].peak_current_sensor_);
LOG_SENSOR(" ", "Voltage C", this->phase_[PHASEC].voltage_sensor_);
LOG_SENSOR(" ", "Current C", this->phase_[PHASEC].current_sensor_);
LOG_SENSOR(" ", "Power C", this->phase_[PHASEC].power_sensor_);
LOG_SENSOR(" ", "Reactive Power C", this->phase_[PHASEC].reactive_power_sensor_);
LOG_SENSOR(" ", "PF C", this->phase_[PHASEC].power_factor_sensor_);
LOG_SENSOR(" ", "Active Forward Energy C", this->phase_[PHASEC].forward_active_energy_sensor_);
LOG_SENSOR(" ", "Active Reverse Energy C", this->phase_[PHASEC].reverse_active_energy_sensor_);
LOG_SENSOR(" ", "Harmonic Power A", this->phase_[PHASEC].harmonic_active_power_sensor_);
LOG_SENSOR(" ", "Phase Angle A", this->phase_[PHASEC].phase_angle_sensor_);
LOG_SENSOR(" ", "Peak Current A", this->phase_[PHASEC].peak_current_sensor_);
LOG_SENSOR(" ", "Frequency", this->freq_sensor_);
LOG_SENSOR(" ", "Chip Temp", this->chip_temperature_sensor_);
}
float ATM90E32Component::get_setup_priority() const { return setup_priority::DATA; }
float ATM90E32Component::get_setup_priority() const { return setup_priority::IO; }
// R/C registers can conly be cleared after the LastSPIData register is updated (register 78H)
// Peakdetect period: 05H. Bit 15:8 are PeakDet_period in ms. 7:0 are Sag_period
// Default is 143FH (20ms, 63ms)
uint16_t ATM90E32Component::read16_(uint16_t a_register) {
uint8_t addrh = (1 << 7) | ((a_register >> 8) & 0x03);
uint8_t addrl = (a_register & 0xFF);
uint8_t data[2];
uint16_t output;
this->enable();
delayMicroseconds(10);
delay_microseconds_safe(10);
this->write_byte(addrh);
this->write_byte(addrl);
delayMicroseconds(4);
this->read_array(data, 2);
this->disable();
@ -179,9 +258,9 @@ uint16_t ATM90E32Component::read16_(uint16_t a_register) {
}
int ATM90E32Component::read32_(uint16_t addr_h, uint16_t addr_l) {
uint16_t val_h = this->read16_(addr_h);
uint16_t val_l = this->read16_(addr_l);
int32_t val = (val_h << 16) | val_l;
const uint16_t val_h = this->read16_(addr_h);
const uint16_t val_l = this->read16_(addr_l);
const int32_t val = (val_h << 16) | val_l;
ESP_LOGVV(TAG,
"read32_ addr_h 0x%04" PRIX16 " val_h 0x%04" PRIX16 " addr_l 0x%04" PRIX16 " val_l 0x%04" PRIX16
@ -192,141 +271,174 @@ int ATM90E32Component::read32_(uint16_t addr_h, uint16_t addr_l) {
}
void ATM90E32Component::write16_(uint16_t a_register, uint16_t val) {
uint8_t addrh = (a_register >> 8) & 0x03;
uint8_t addrl = (a_register & 0xFF);
ESP_LOGVV(TAG, "write16_ 0x%04" PRIX16 " val 0x%04" PRIX16, a_register, val);
this->enable();
delayMicroseconds(10);
this->write_byte(addrh);
this->write_byte(addrl);
delayMicroseconds(4);
this->write_byte((val >> 8) & 0xff);
this->write_byte(val & 0xFF);
this->write_byte16(a_register);
this->write_byte16(val);
this->disable();
if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != val)
ESP_LOGW(TAG, "SPI write error 0x%04X val 0x%04X", a_register, val);
}
float ATM90E32Component::get_line_voltage_a_() {
uint16_t voltage = this->read16_(ATM90E32_REGISTER_URMSA);
float ATM90E32Component::get_local_phase_voltage_(uint8_t phase) { return this->phase_[phase].voltage_; }
float ATM90E32Component::get_local_phase_current_(uint8_t phase) { return this->phase_[phase].current_; }
float ATM90E32Component::get_local_phase_active_power_(uint8_t phase) { return this->phase_[phase].active_power_; }
float ATM90E32Component::get_local_phase_reactive_power_(uint8_t phase) { return this->phase_[phase].reactive_power_; }
float ATM90E32Component::get_local_phase_power_factor_(uint8_t phase) { return this->phase_[phase].power_factor_; }
float ATM90E32Component::get_local_phase_forward_active_energy_(uint8_t phase) {
return this->phase_[phase].forward_active_energy_;
}
float ATM90E32Component::get_local_phase_reverse_active_energy_(uint8_t phase) {
return this->phase_[phase].reverse_active_energy_;
}
float ATM90E32Component::get_local_phase_angle_(uint8_t phase) { return this->phase_[phase].phase_angle_; }
float ATM90E32Component::get_local_phase_harmonic_active_power_(uint8_t phase) {
return this->phase_[phase].harmonic_active_power_;
}
float ATM90E32Component::get_local_phase_peak_current_(uint8_t phase) { return this->phase_[phase].peak_current_; }
float ATM90E32Component::get_phase_voltage_(uint8_t phase) {
const uint16_t voltage = this->read16_(ATM90E32_REGISTER_URMS + phase);
if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != voltage)
ESP_LOGW(TAG, "SPI URMS voltage register read error.");
return (float) voltage / 100;
}
float ATM90E32Component::get_line_voltage_b_() {
uint16_t voltage = this->read16_(ATM90E32_REGISTER_URMSB);
return (float) voltage / 100;
float ATM90E32Component::get_phase_voltage_avg_(uint8_t phase) {
const uint8_t reads = 10;
uint32_t accumulation = 0;
uint16_t voltage = 0;
for (uint8_t i = 0; i < reads; i++) {
voltage = this->read16_(ATM90E32_REGISTER_URMS + phase);
if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != voltage)
ESP_LOGW(TAG, "SPI URMS voltage register read error.");
accumulation += voltage;
}
voltage = accumulation / reads;
this->phase_[phase].voltage_ = (float) voltage / 100;
return this->phase_[phase].voltage_;
}
float ATM90E32Component::get_line_voltage_c_() {
uint16_t voltage = this->read16_(ATM90E32_REGISTER_URMSC);
return (float) voltage / 100;
float ATM90E32Component::get_phase_current_avg_(uint8_t phase) {
const uint8_t reads = 10;
uint32_t accumulation = 0;
uint16_t current = 0;
for (uint8_t i = 0; i < reads; i++) {
current = this->read16_(ATM90E32_REGISTER_IRMS + phase);
if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != current)
ESP_LOGW(TAG, "SPI IRMS current register read error.");
accumulation += current;
}
current = accumulation / reads;
this->phase_[phase].current_ = (float) current / 1000;
return this->phase_[phase].current_;
}
float ATM90E32Component::get_line_current_a_() {
uint16_t current = this->read16_(ATM90E32_REGISTER_IRMSA);
float ATM90E32Component::get_phase_current_(uint8_t phase) {
const uint16_t current = this->read16_(ATM90E32_REGISTER_IRMS + phase);
if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != current)
ESP_LOGW(TAG, "SPI IRMS current register read error.");
return (float) current / 1000;
}
float ATM90E32Component::get_line_current_b_() {
uint16_t current = this->read16_(ATM90E32_REGISTER_IRMSB);
return (float) current / 1000;
}
float ATM90E32Component::get_line_current_c_() {
uint16_t current = this->read16_(ATM90E32_REGISTER_IRMSC);
return (float) current / 1000;
}
float ATM90E32Component::get_active_power_a_() {
int val = this->read32_(ATM90E32_REGISTER_PMEANA, ATM90E32_REGISTER_PMEANALSB);
float ATM90E32Component::get_phase_active_power_(uint8_t phase) {
const int val = this->read32_(ATM90E32_REGISTER_PMEAN + phase, ATM90E32_REGISTER_PMEANLSB + phase);
return val * 0.00032f;
}
float ATM90E32Component::get_active_power_b_() {
int val = this->read32_(ATM90E32_REGISTER_PMEANB, ATM90E32_REGISTER_PMEANBLSB);
float ATM90E32Component::get_phase_reactive_power_(uint8_t phase) {
const int val = this->read32_(ATM90E32_REGISTER_QMEAN + phase, ATM90E32_REGISTER_QMEANLSB + phase);
return val * 0.00032f;
}
float ATM90E32Component::get_active_power_c_() {
int val = this->read32_(ATM90E32_REGISTER_PMEANC, ATM90E32_REGISTER_PMEANCLSB);
float ATM90E32Component::get_phase_power_factor_(uint8_t phase) {
const int16_t powerfactor = this->read16_(ATM90E32_REGISTER_PFMEAN + phase);
if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != powerfactor)
ESP_LOGW(TAG, "SPI power factor read error.");
return (float) powerfactor / 1000;
}
float ATM90E32Component::get_phase_forward_active_energy_(uint8_t phase) {
const uint16_t val = this->read16_(ATM90E32_REGISTER_APENERGY + phase);
if ((UINT32_MAX - this->phase_[phase].cumulative_forward_active_energy_) > val) {
this->phase_[phase].cumulative_forward_active_energy_ += val;
} else {
this->phase_[phase].cumulative_forward_active_energy_ = val;
}
return ((float) this->phase_[phase].cumulative_forward_active_energy_ * 10 / 3200);
}
float ATM90E32Component::get_phase_reverse_active_energy_(uint8_t phase) {
const uint16_t val = this->read16_(ATM90E32_REGISTER_ANENERGY);
if (UINT32_MAX - this->phase_[phase].cumulative_reverse_active_energy_ > val) {
this->phase_[phase].cumulative_reverse_active_energy_ += val;
} else {
this->phase_[phase].cumulative_reverse_active_energy_ = val;
}
return ((float) this->phase_[phase].cumulative_reverse_active_energy_ * 10 / 3200);
}
float ATM90E32Component::get_phase_harmonic_active_power_(uint8_t phase) {
int val = this->read32_(ATM90E32_REGISTER_PMEANH + phase, ATM90E32_REGISTER_PMEANHLSB + phase);
return val * 0.00032f;
}
float ATM90E32Component::get_reactive_power_a_() {
int val = this->read32_(ATM90E32_REGISTER_QMEANA, ATM90E32_REGISTER_QMEANALSB);
return val * 0.00032f;
float ATM90E32Component::get_phase_angle_(uint8_t phase) {
uint16_t val = this->read16_(ATM90E32_REGISTER_PANGLE + phase) / 10.0;
return (float) (val > 180) ? val - 360.0 : val;
}
float ATM90E32Component::get_reactive_power_b_() {
int val = this->read32_(ATM90E32_REGISTER_QMEANB, ATM90E32_REGISTER_QMEANBLSB);
return val * 0.00032f;
}
float ATM90E32Component::get_reactive_power_c_() {
int val = this->read32_(ATM90E32_REGISTER_QMEANC, ATM90E32_REGISTER_QMEANCLSB);
return val * 0.00032f;
}
float ATM90E32Component::get_power_factor_a_() {
int16_t pf = this->read16_(ATM90E32_REGISTER_PFMEANA);
return (float) pf / 1000;
}
float ATM90E32Component::get_power_factor_b_() {
int16_t pf = this->read16_(ATM90E32_REGISTER_PFMEANB);
return (float) pf / 1000;
}
float ATM90E32Component::get_power_factor_c_() {
int16_t pf = this->read16_(ATM90E32_REGISTER_PFMEANC);
return (float) pf / 1000;
}
float ATM90E32Component::get_forward_active_energy_a_() {
uint16_t val = this->read16_(ATM90E32_REGISTER_APENERGYA);
if ((UINT32_MAX - this->phase_[0].cumulative_forward_active_energy_) > val) {
this->phase_[0].cumulative_forward_active_energy_ += val;
} else {
this->phase_[0].cumulative_forward_active_energy_ = val;
}
return ((float) this->phase_[0].cumulative_forward_active_energy_ * 10 / 3200);
}
float ATM90E32Component::get_forward_active_energy_b_() {
uint16_t val = this->read16_(ATM90E32_REGISTER_APENERGYB);
if (UINT32_MAX - this->phase_[1].cumulative_forward_active_energy_ > val) {
this->phase_[1].cumulative_forward_active_energy_ += val;
} else {
this->phase_[1].cumulative_forward_active_energy_ = val;
}
return ((float) this->phase_[1].cumulative_forward_active_energy_ * 10 / 3200);
}
float ATM90E32Component::get_forward_active_energy_c_() {
uint16_t val = this->read16_(ATM90E32_REGISTER_APENERGYC);
if (UINT32_MAX - this->phase_[2].cumulative_forward_active_energy_ > val) {
this->phase_[2].cumulative_forward_active_energy_ += val;
} else {
this->phase_[2].cumulative_forward_active_energy_ = val;
}
return ((float) this->phase_[2].cumulative_forward_active_energy_ * 10 / 3200);
}
float ATM90E32Component::get_reverse_active_energy_a_() {
uint16_t val = this->read16_(ATM90E32_REGISTER_ANENERGYA);
if (UINT32_MAX - this->phase_[0].cumulative_reverse_active_energy_ > val) {
this->phase_[0].cumulative_reverse_active_energy_ += val;
} else {
this->phase_[0].cumulative_reverse_active_energy_ = val;
}
return ((float) this->phase_[0].cumulative_reverse_active_energy_ * 10 / 3200);
}
float ATM90E32Component::get_reverse_active_energy_b_() {
uint16_t val = this->read16_(ATM90E32_REGISTER_ANENERGYB);
if (UINT32_MAX - this->phase_[1].cumulative_reverse_active_energy_ > val) {
this->phase_[1].cumulative_reverse_active_energy_ += val;
} else {
this->phase_[1].cumulative_reverse_active_energy_ = val;
}
return ((float) this->phase_[1].cumulative_reverse_active_energy_ * 10 / 3200);
}
float ATM90E32Component::get_reverse_active_energy_c_() {
uint16_t val = this->read16_(ATM90E32_REGISTER_ANENERGYC);
if (UINT32_MAX - this->phase_[2].cumulative_reverse_active_energy_ > val) {
this->phase_[2].cumulative_reverse_active_energy_ += val;
} else {
this->phase_[2].cumulative_reverse_active_energy_ = val;
}
return ((float) this->phase_[2].cumulative_reverse_active_energy_ * 10 / 3200);
float ATM90E32Component::get_phase_peak_current_(uint8_t phase) {
int16_t val = (float) this->read16_(ATM90E32_REGISTER_IPEAK + phase);
if (!this->peak_current_signed_)
val = abs(val);
// phase register * phase current gain value / 1000 * 2^13
return (float) (val * this->phase_[phase].ct_gain_ / 8192000.0);
}
float ATM90E32Component::get_frequency_() {
uint16_t freq = this->read16_(ATM90E32_REGISTER_FREQ);
const uint16_t freq = this->read16_(ATM90E32_REGISTER_FREQ);
return (float) freq / 100;
}
float ATM90E32Component::get_chip_temperature_() {
uint16_t ctemp = this->read16_(ATM90E32_REGISTER_TEMP);
const uint16_t ctemp = this->read16_(ATM90E32_REGISTER_TEMP);
return (float) ctemp;
}
uint16_t ATM90E32Component::calibrate_voltage_offset_phase(uint8_t phase) {
const uint8_t num_reads = 5;
uint64_t total_value = 0;
for (int i = 0; i < num_reads; ++i) {
const uint32_t measurement_value = read32_(ATM90E32_REGISTER_URMS + phase, ATM90E32_REGISTER_URMSLSB + phase);
total_value += measurement_value;
}
const uint32_t average_value = total_value / num_reads;
const uint32_t shifted_value = average_value >> 7;
const uint32_t voltage_offset = ~shifted_value + 1;
return voltage_offset & 0xFFFF; // Take the lower 16 bits
}
uint16_t ATM90E32Component::calibrate_current_offset_phase(uint8_t phase) {
const uint8_t num_reads = 5;
uint64_t total_value = 0;
for (int i = 0; i < num_reads; ++i) {
const uint32_t measurement_value = read32_(ATM90E32_REGISTER_IRMS + phase, ATM90E32_REGISTER_IRMSLSB + phase);
total_value += measurement_value;
}
const uint32_t average_value = total_value / num_reads;
const uint32_t current_offset = ~average_value + 1;
return current_offset & 0xFFFF; // Take the lower 16 bits
}
} // namespace atm90e32
} // namespace esphome

View File

@ -3,14 +3,19 @@
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/spi/spi.h"
#include "atm90e32_reg.h"
namespace esphome {
namespace atm90e32 {
class ATM90E32Component : public PollingComponent,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH,
spi::CLOCK_PHASE_TRAILING, spi::DATA_RATE_200KHZ> {
spi::CLOCK_PHASE_TRAILING, spi::DATA_RATE_1MHZ> {
public:
static const uint8_t PHASEA = 0;
static const uint8_t PHASEB = 1;
static const uint8_t PHASEC = 2;
void loop() override;
void setup() override;
void dump_config() override;
float get_setup_priority() const override;
@ -20,6 +25,7 @@ class ATM90E32Component : public PollingComponent,
void set_current_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].current_sensor_ = obj; }
void set_power_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].power_sensor_ = obj; }
void set_reactive_power_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].reactive_power_sensor_ = obj; }
void set_apparent_power_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].apparent_power_sensor_ = obj; }
void set_forward_active_energy_sensor(int phase, sensor::Sensor *obj) {
this->phase_[phase].forward_active_energy_sensor_ = obj;
}
@ -27,64 +33,94 @@ class ATM90E32Component : public PollingComponent,
this->phase_[phase].reverse_active_energy_sensor_ = obj;
}
void set_power_factor_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].power_factor_sensor_ = obj; }
void set_volt_gain(int phase, uint16_t gain) { this->phase_[phase].volt_gain_ = gain; }
void set_phase_angle_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].phase_angle_sensor_ = obj; }
void set_harmonic_active_power_sensor(int phase, sensor::Sensor *obj) {
this->phase_[phase].harmonic_active_power_sensor_ = obj;
}
void set_peak_current_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].peak_current_sensor_ = obj; }
void set_volt_gain(int phase, uint16_t gain) { this->phase_[phase].voltage_gain_ = gain; }
void set_ct_gain(int phase, uint16_t gain) { this->phase_[phase].ct_gain_ = gain; }
void set_freq_sensor(sensor::Sensor *freq_sensor) { freq_sensor_ = freq_sensor; }
void set_peak_current_signed(bool flag) { peak_current_signed_ = flag; }
void set_chip_temperature_sensor(sensor::Sensor *chip_temperature_sensor) {
chip_temperature_sensor_ = chip_temperature_sensor;
}
void set_line_freq(int freq) { line_freq_ = freq; }
void set_current_phases(int phases) { current_phases_ = phases; }
void set_pga_gain(uint16_t gain) { pga_gain_ = gain; }
uint16_t calibrate_voltage_offset_phase(uint8_t /*phase*/);
uint16_t calibrate_current_offset_phase(uint8_t /*phase*/);
int32_t last_periodic_millis = millis();
protected:
uint16_t read16_(uint16_t a_register);
int read32_(uint16_t addr_h, uint16_t addr_l);
void write16_(uint16_t a_register, uint16_t val);
float get_line_voltage_a_();
float get_line_voltage_b_();
float get_line_voltage_c_();
float get_line_current_a_();
float get_line_current_b_();
float get_line_current_c_();
float get_active_power_a_();
float get_active_power_b_();
float get_active_power_c_();
float get_reactive_power_a_();
float get_reactive_power_b_();
float get_reactive_power_c_();
float get_power_factor_a_();
float get_power_factor_b_();
float get_power_factor_c_();
float get_forward_active_energy_a_();
float get_forward_active_energy_b_();
float get_forward_active_energy_c_();
float get_reverse_active_energy_a_();
float get_reverse_active_energy_b_();
float get_reverse_active_energy_c_();
float get_local_phase_voltage_(uint8_t /*phase*/);
float get_local_phase_current_(uint8_t /*phase*/);
float get_local_phase_active_power_(uint8_t /*phase*/);
float get_local_phase_reactive_power_(uint8_t /*phase*/);
float get_local_phase_power_factor_(uint8_t /*phase*/);
float get_local_phase_forward_active_energy_(uint8_t /*phase*/);
float get_local_phase_reverse_active_energy_(uint8_t /*phase*/);
float get_local_phase_angle_(uint8_t /*phase*/);
float get_local_phase_harmonic_active_power_(uint8_t /*phase*/);
float get_local_phase_peak_current_(uint8_t /*phase*/);
float get_phase_voltage_(uint8_t /*phase*/);
float get_phase_voltage_avg_(uint8_t /*phase*/);
float get_phase_current_(uint8_t /*phase*/);
float get_phase_current_avg_(uint8_t /*phase*/);
float get_phase_active_power_(uint8_t /*phase*/);
float get_phase_reactive_power_(uint8_t /*phase*/);
float get_phase_power_factor_(uint8_t /*phase*/);
float get_phase_forward_active_energy_(uint8_t /*phase*/);
float get_phase_reverse_active_energy_(uint8_t /*phase*/);
float get_phase_angle_(uint8_t /*phase*/);
float get_phase_harmonic_active_power_(uint8_t /*phase*/);
float get_phase_peak_current_(uint8_t /*phase*/);
float get_frequency_();
float get_chip_temperature_();
bool get_publish_interval_flag_() { return publish_interval_flag_; };
void set_publish_interval_flag_(bool flag) { publish_interval_flag_ = flag; };
struct ATM90E32Phase {
uint16_t volt_gain_{7305};
uint16_t voltage_gain_{7305};
uint16_t ct_gain_{27961};
uint16_t voltage_offset_{0};
uint16_t current_offset_{0};
float voltage_{0};
float current_{0};
float active_power_{0};
float reactive_power_{0};
float power_factor_{0};
float forward_active_energy_{0};
float reverse_active_energy_{0};
float phase_angle_{0};
float harmonic_active_power_{0};
float peak_current_{0};
sensor::Sensor *voltage_sensor_{nullptr};
sensor::Sensor *current_sensor_{nullptr};
sensor::Sensor *power_sensor_{nullptr};
sensor::Sensor *reactive_power_sensor_{nullptr};
sensor::Sensor *apparent_power_sensor_{nullptr};
sensor::Sensor *power_factor_sensor_{nullptr};
sensor::Sensor *forward_active_energy_sensor_{nullptr};
sensor::Sensor *reverse_active_energy_sensor_{nullptr};
sensor::Sensor *phase_angle_sensor_{nullptr};
sensor::Sensor *harmonic_active_power_sensor_{nullptr};
sensor::Sensor *peak_current_sensor_{nullptr};
uint32_t cumulative_forward_active_energy_{0};
uint32_t cumulative_reverse_active_energy_{0};
} phase_[3];
sensor::Sensor *freq_sensor_{nullptr};
sensor::Sensor *chip_temperature_sensor_{nullptr};
uint16_t pga_gain_{0x15};
int line_freq_{60};
int current_phases_{3};
bool publish_interval_flag_{true};
bool peak_current_signed_{false};
};
} // namespace atm90e32

View File

@ -131,10 +131,12 @@ static const uint16_t ATM90E32_REGISTER_IOFFSETN = 0x6E; // N Current Offset
/* ENERGY REGISTERS */
static const uint16_t ATM90E32_REGISTER_APENERGYT = 0x80; // Total Forward Active
static const uint16_t ATM90E32_REGISTER_APENERGY = 0x81; // Forward Active Reg Base
static const uint16_t ATM90E32_REGISTER_APENERGYA = 0x81; // A Forward Active
static const uint16_t ATM90E32_REGISTER_APENERGYB = 0x82; // B Forward Active
static const uint16_t ATM90E32_REGISTER_APENERGYC = 0x83; // C Forward Active
static const uint16_t ATM90E32_REGISTER_ANENERGYT = 0x84; // Total Reverse Active
static const uint16_t ATM90E32_REGISTER_ANENERGY = 0x85; // Reverse Active Reg Base
static const uint16_t ATM90E32_REGISTER_ANENERGYA = 0x85; // A Reverse Active
static const uint16_t ATM90E32_REGISTER_ANENERGYB = 0x86; // B Reverse Active
static const uint16_t ATM90E32_REGISTER_ANENERGYC = 0x87; // C Reverse Active
@ -172,10 +174,12 @@ static const uint16_t ATM90E32_REGISTER_ANENERGYCH = 0xAF; // C Reverse Harm. E
/* POWER & P.F. REGISTERS */
static const uint16_t ATM90E32_REGISTER_PMEANT = 0xB0; // Total Mean Power (P)
static const uint16_t ATM90E32_REGISTER_PMEAN = 0xB1; // Mean Power Reg Base (P)
static const uint16_t ATM90E32_REGISTER_PMEANA = 0xB1; // A Mean Power (P)
static const uint16_t ATM90E32_REGISTER_PMEANB = 0xB2; // B Mean Power (P)
static const uint16_t ATM90E32_REGISTER_PMEANC = 0xB3; // C Mean Power (P)
static const uint16_t ATM90E32_REGISTER_QMEANT = 0xB4; // Total Mean Power (Q)
static const uint16_t ATM90E32_REGISTER_QMEAN = 0xB5; // Mean Power Reg Base (Q)
static const uint16_t ATM90E32_REGISTER_QMEANA = 0xB5; // A Mean Power (Q)
static const uint16_t ATM90E32_REGISTER_QMEANB = 0xB6; // B Mean Power (Q)
static const uint16_t ATM90E32_REGISTER_QMEANC = 0xB7; // C Mean Power (Q)
@ -184,15 +188,18 @@ static const uint16_t ATM90E32_REGISTER_SMEANA = 0xB9; // A Mean Power (S)
static const uint16_t ATM90E32_REGISTER_SMEANB = 0xBA; // B Mean Power (S)
static const uint16_t ATM90E32_REGISTER_SMEANC = 0xBB; // C Mean Power (S)
static const uint16_t ATM90E32_REGISTER_PFMEANT = 0xBC; // Mean Power Factor
static const uint16_t ATM90E32_REGISTER_PFMEAN = 0xBD; // Power Factor Reg Base
static const uint16_t ATM90E32_REGISTER_PFMEANA = 0xBD; // A Power Factor
static const uint16_t ATM90E32_REGISTER_PFMEANB = 0xBE; // B Power Factor
static const uint16_t ATM90E32_REGISTER_PFMEANC = 0xBF; // C Power Factor
static const uint16_t ATM90E32_REGISTER_PMEANTLSB = 0xC0; // Lower Word (Tot. Act. Power)
static const uint16_t ATM90E32_REGISTER_PMEANLSB = 0xC1; // Lower Word Reg Base (Active Power)
static const uint16_t ATM90E32_REGISTER_PMEANALSB = 0xC1; // Lower Word (A Act. Power)
static const uint16_t ATM90E32_REGISTER_PMEANBLSB = 0xC2; // Lower Word (B Act. Power)
static const uint16_t ATM90E32_REGISTER_PMEANCLSB = 0xC3; // Lower Word (C Act. Power)
static const uint16_t ATM90E32_REGISTER_QMEANTLSB = 0xC4; // Lower Word (Tot. React. Power)
static const uint16_t ATM90E32_REGISTER_QMEANLSB = 0xC5; // Lower Word Reg Base (Reactive Power)
static const uint16_t ATM90E32_REGISTER_QMEANALSB = 0xC5; // Lower Word (A React. Power)
static const uint16_t ATM90E32_REGISTER_QMEANBLSB = 0xC6; // Lower Word (B React. Power)
static const uint16_t ATM90E32_REGISTER_QMEANCLSB = 0xC7; // Lower Word (C React. Power)
@ -207,12 +214,15 @@ static const uint16_t ATM90E32_REGISTER_PMEANAF = 0xD1; // A Active Fund. Power
static const uint16_t ATM90E32_REGISTER_PMEANBF = 0xD2; // B Active Fund. Power
static const uint16_t ATM90E32_REGISTER_PMEANCF = 0xD3; // C Active Fund. Power
static const uint16_t ATM90E32_REGISTER_PMEANTH = 0xD4; // Total Active Harm. Power
static const uint16_t ATM90E32_REGISTER_PMEANH = 0xD5; // Active Harm. Power Reg Base
static const uint16_t ATM90E32_REGISTER_PMEANAH = 0xD5; // A Active Harm. Power
static const uint16_t ATM90E32_REGISTER_PMEANBH = 0xD6; // B Active Harm. Power
static const uint16_t ATM90E32_REGISTER_PMEANCH = 0xD7; // C Active Harm. Power
static const uint16_t ATM90E32_REGISTER_URMS = 0xD9; // RMS Voltage Reg Base
static const uint16_t ATM90E32_REGISTER_URMSA = 0xD9; // A RMS Voltage
static const uint16_t ATM90E32_REGISTER_URMSB = 0xDA; // B RMS Voltage
static const uint16_t ATM90E32_REGISTER_URMSC = 0xDB; // C RMS Voltage
static const uint16_t ATM90E32_REGISTER_IRMS = 0xDD; // RMS Current Reg Base
static const uint16_t ATM90E32_REGISTER_IRMSA = 0xDD; // A RMS Current
static const uint16_t ATM90E32_REGISTER_IRMSB = 0xDE; // B RMS Current
static const uint16_t ATM90E32_REGISTER_IRMSC = 0xDF; // C RMS Current
@ -223,12 +233,15 @@ static const uint16_t ATM90E32_REGISTER_PMEANAFLSB = 0xE1; // Lower Word (A Act
static const uint16_t ATM90E32_REGISTER_PMEANBFLSB = 0xE2; // Lower Word (B Act. Fund. Power)
static const uint16_t ATM90E32_REGISTER_PMEANCFLSB = 0xE3; // Lower Word (C Act. Fund. Power)
static const uint16_t ATM90E32_REGISTER_PMEANTHLSB = 0xE4; // Lower Word (Tot. Act. Harm. Power)
static const uint16_t ATM90E32_REGISTER_PMEANHLSB = 0xE5; // Lower Word (A Act. Harm. Power) Reg Base
static const uint16_t ATM90E32_REGISTER_PMEANAHLSB = 0xE5; // Lower Word (A Act. Harm. Power)
static const uint16_t ATM90E32_REGISTER_PMEANBHLSB = 0xE6; // Lower Word (B Act. Harm. Power)
static const uint16_t ATM90E32_REGISTER_PMEANCHLSB = 0xE7; // Lower Word (C Act. Harm. Power)
static const uint16_t ATM90E32_REGISTER_URMSLSB = 0xE9; // Lower Word RMS Voltage Reg Base
static const uint16_t ATM90E32_REGISTER_URMSALSB = 0xE9; // Lower Word (A RMS Voltage)
static const uint16_t ATM90E32_REGISTER_URMSBLSB = 0xEA; // Lower Word (B RMS Voltage)
static const uint16_t ATM90E32_REGISTER_URMSCLSB = 0xEB; // Lower Word (C RMS Voltage)
static const uint16_t ATM90E32_REGISTER_IRMSLSB = 0xED; // Lower Word RMS Current Reg Base
static const uint16_t ATM90E32_REGISTER_IRMSALSB = 0xED; // Lower Word (A RMS Current)
static const uint16_t ATM90E32_REGISTER_IRMSBLSB = 0xEE; // Lower Word (B RMS Current)
static const uint16_t ATM90E32_REGISTER_IRMSCLSB = 0xEF; // Lower Word (C RMS Current)
@ -237,10 +250,12 @@ static const uint16_t ATM90E32_REGISTER_IRMSCLSB = 0xEF; // Lower Word (C RMS
static const uint16_t ATM90E32_REGISTER_UPEAKA = 0xF1; // A Voltage Peak
static const uint16_t ATM90E32_REGISTER_UPEAKB = 0xF2; // B Voltage Peak
static const uint16_t ATM90E32_REGISTER_UPEAKC = 0xF3; // C Voltage Peak
static const uint16_t ATM90E32_REGISTER_IPEAK = 0xF5; // Peak Current Reg Base
static const uint16_t ATM90E32_REGISTER_IPEAKA = 0xF5; // A Current Peak
static const uint16_t ATM90E32_REGISTER_IPEAKB = 0xF6; // B Current Peak
static const uint16_t ATM90E32_REGISTER_IPEAKC = 0xF7; // C Current Peak
static const uint16_t ATM90E32_REGISTER_FREQ = 0xF8; // Frequency
static const uint16_t ATM90E32_REGISTER_PANGLE = 0xF9; // Mean Phase Angle Reg Base
static const uint16_t ATM90E32_REGISTER_PANGLEA = 0xF9; // A Mean Phase Angle
static const uint16_t ATM90E32_REGISTER_PANGLEB = 0xFA; // B Mean Phase Angle
static const uint16_t ATM90E32_REGISTER_PANGLEC = 0xFB; // C Mean Phase Angle

View File

@ -9,8 +9,10 @@ from esphome.const import (
CONF_PHASE_A,
CONF_PHASE_B,
CONF_PHASE_C,
CONF_PHASE_ANGLE,
CONF_POWER,
CONF_POWER_FACTOR,
CONF_APPARENT_POWER,
CONF_FREQUENCY,
CONF_FORWARD_ACTIVE_ENERGY,
CONF_REVERSE_ACTIVE_ENERGY,
@ -25,12 +27,13 @@ from esphome.const import (
ICON_CURRENT_AC,
STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL_INCREASING,
UNIT_AMPERE,
UNIT_DEGREES,
UNIT_CELSIUS,
UNIT_HERTZ,
UNIT_VOLT,
UNIT_AMPERE,
UNIT_WATT,
UNIT_CELSIUS,
UNIT_VOLT_AMPS_REACTIVE,
UNIT_WATT,
UNIT_WATT_HOURS,
)
@ -40,6 +43,10 @@ CONF_GAIN_PGA = "gain_pga"
CONF_CURRENT_PHASES = "current_phases"
CONF_GAIN_VOLTAGE = "gain_voltage"
CONF_GAIN_CT = "gain_ct"
CONF_HARMONIC_POWER = "harmonic_power"
CONF_PEAK_CURRENT = "peak_current"
CONF_PEAK_CURRENT_SIGNED = "peak_current_signed"
UNIT_DEG = "degrees"
LINE_FREQS = {
"50HZ": 50,
"60HZ": 60,
@ -85,6 +92,12 @@ ATM90E32_PHASE_SCHEMA = cv.Schema(
accuracy_decimals=2,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_APPARENT_POWER): sensor.sensor_schema(
unit_of_measurement=UNIT_WATT,
accuracy_decimals=2,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema(
accuracy_decimals=2,
device_class=DEVICE_CLASS_POWER_FACTOR,
@ -102,6 +115,24 @@ ATM90E32_PHASE_SCHEMA = cv.Schema(
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional(CONF_PHASE_ANGLE): sensor.sensor_schema(
unit_of_measurement=UNIT_DEGREES,
accuracy_decimals=2,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_HARMONIC_POWER): sensor.sensor_schema(
unit_of_measurement=UNIT_WATT,
accuracy_decimals=2,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PEAK_CURRENT): sensor.sensor_schema(
unit_of_measurement=UNIT_AMPERE,
accuracy_decimals=2,
device_class=DEVICE_CLASS_CURRENT,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_GAIN_VOLTAGE, default=7305): cv.uint16_t,
cv.Optional(CONF_GAIN_CT, default=27961): cv.uint16_t,
}
@ -132,6 +163,7 @@ CONFIG_SCHEMA = (
CURRENT_PHASES, upper=True
),
cv.Optional(CONF_GAIN_PGA, default="2X"): cv.enum(PGA_GAINS, upper=True),
cv.Optional(CONF_PEAK_CURRENT_SIGNED, default=False): cv.boolean,
}
)
.extend(cv.polling_component_schema("60s"))
@ -162,6 +194,9 @@ async def to_code(config):
if reactive_power_config := conf.get(CONF_REACTIVE_POWER):
sens = await sensor.new_sensor(reactive_power_config)
cg.add(var.set_reactive_power_sensor(i, sens))
if apparent_power_config := conf.get(CONF_APPARENT_POWER):
sens = await sensor.new_sensor(apparent_power_config)
cg.add(var.set_apparent_power_sensor(i, sens))
if power_factor_config := conf.get(CONF_POWER_FACTOR):
sens = await sensor.new_sensor(power_factor_config)
cg.add(var.set_power_factor_sensor(i, sens))
@ -171,6 +206,15 @@ async def to_code(config):
if reverse_active_energy_config := conf.get(CONF_REVERSE_ACTIVE_ENERGY):
sens = await sensor.new_sensor(reverse_active_energy_config)
cg.add(var.set_reverse_active_energy_sensor(i, sens))
if phase_angle_config := conf.get(CONF_PHASE_ANGLE):
sens = await sensor.new_sensor(phase_angle_config)
cg.add(var.set_phase_angle_sensor(i, sens))
if harmonic_active_power_config := conf.get(CONF_HARMONIC_POWER):
sens = await sensor.new_sensor(harmonic_active_power_config)
cg.add(var.set_harmonic_active_power_sensor(i, sens))
if peak_current_config := conf.get(CONF_PEAK_CURRENT):
sens = await sensor.new_sensor(peak_current_config)
cg.add(var.set_peak_current_sensor(i, sens))
if frequency_config := config.get(CONF_FREQUENCY):
sens = await sensor.new_sensor(frequency_config)
@ -182,3 +226,4 @@ async def to_code(config):
cg.add(var.set_line_freq(config[CONF_LINE_FREQUENCY]))
cg.add(var.set_current_phases(config[CONF_CURRENT_PHASES]))
cg.add(var.set_pga_gain(config[CONF_GAIN_PGA]))
cg.add(var.set_peak_current_signed(config[CONF_PEAK_CURRENT_SIGNED]))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -30,6 +30,8 @@ class Glyph {
void scan_area(int *x1, int *y1, int *width, int *height) const;
const GlyphData *get_glyph_data() const { return this->glyph_data_; }
protected:
friend Font;

View File

@ -71,7 +71,7 @@ class FT5x06Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice
this->x_raw_max_ = this->display_->get_native_width();
}
if (this->y_raw_max_ == this->y_raw_min_) {
this->x_raw_max_ = this->display_->get_native_height();
this->y_raw_max_ = this->display_->get_native_height();
}
}
esph_log_config(TAG, "FT5x06 Touchscreen setup complete");

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,56 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c, sensor
from esphome.const import (
CONF_HUMIDITY,
CONF_ID,
CONF_TEMPERATURE,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
UNIT_PERCENT,
)
DEPENDENCIES = ["i2c"]
htu31d_ns = cg.esphome_ns.namespace("htu31d")
HTU31DComponent = htu31d_ns.class_(
"HTU31DComponent", cg.PollingComponent, i2c.I2CDevice
)
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(HTU31DComponent),
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
accuracy_decimals=1,
device_class=DEVICE_CLASS_HUMIDITY,
state_class=STATE_CLASS_MEASUREMENT,
),
}
)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x40))
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
if temperature_config := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature_config)
cg.add(var.set_temperature(sens))
if humidity_config := config.get(CONF_HUMIDITY):
sens = await sensor.new_sensor(humidity_config)
cg.add(var.set_humidity(sens))

View File

@ -6,6 +6,7 @@ DEPENDENCIES = ["uart"]
hydreon_rgxx_ns = cg.esphome_ns.namespace("hydreon_rgxx")
RGModel = hydreon_rgxx_ns.enum("RGModel")
RG15Resolution = hydreon_rgxx_ns.enum("RG15Resolution")
HydreonRGxxComponent = hydreon_rgxx_ns.class_(
"HydreonRGxxComponent", cg.PollingComponent, uart.UARTDevice
)

View File

@ -22,6 +22,11 @@ void HydreonRGxxComponent::dump_config() {
ESP_LOGCONFIG(TAG, " Disable Led: %s", TRUEFALSE(this->disable_led_));
} else {
ESP_LOGCONFIG(TAG, " Model: RG15");
if (this->resolution_ == FORCE_HIGH) {
ESP_LOGCONFIG(TAG, " Resolution: high");
} else {
ESP_LOGCONFIG(TAG, " Resolution: low");
}
}
LOG_UPDATE_INTERVAL(this);
@ -195,7 +200,11 @@ void HydreonRGxxComponent::process_line_() {
ESP_LOGI(TAG, "Boot detected: %s", this->buffer_.substr(0, this->buffer_.size() - 2).c_str());
if (this->model_ == RG15) {
this->write_str("P\nH\nM\n"); // set sensor to (P)polling mode, (H)high res mode, (M)metric mode
if (this->resolution_ == FORCE_HIGH) {
this->write_str("P\nH\nM\n"); // set sensor to (P)polling mode, (H)high res mode, (M)metric mode
} else {
this->write_str("P\nL\nM\n"); // set sensor to (P)polling mode, (L)low res mode, (M)metric mode
}
}
if (this->model_ == RG9) {

View File

@ -16,6 +16,11 @@ enum RGModel {
RG15 = 2,
};
enum RG15Resolution {
FORCE_LOW = 1,
FORCE_HIGH = 2,
};
#ifdef HYDREON_RGXX_NUM_SENSORS
static const uint8_t NUM_SENSORS = HYDREON_RGXX_NUM_SENSORS;
#else
@ -37,6 +42,7 @@ class HydreonRGxxComponent : public PollingComponent, public uart::UARTDevice {
void set_em_sat_sensor(binary_sensor::BinarySensor *sensor) { this->em_sat_sensor_ = sensor; }
#endif
void set_model(RGModel model) { model_ = model; }
void set_resolution(RG15Resolution resolution) { resolution_ = resolution; }
void set_request_temperature(bool b) { request_temperature_ = b; }
/// Schedule data readings.
@ -68,7 +74,10 @@ class HydreonRGxxComponent : public PollingComponent, public uart::UARTDevice {
int16_t boot_count_ = 0;
int16_t no_response_count_ = 0;
std::string buffer_;
RGModel model_ = RG9;
RG15Resolution resolution_ = FORCE_HIGH;
int sw_version_ = 0;
bool too_cold_ = false;
bool lens_bad_ = false;

View File

@ -5,6 +5,7 @@ from esphome.const import (
CONF_ID,
CONF_MODEL,
CONF_MOISTURE,
CONF_RESOLUTION,
CONF_TEMPERATURE,
DEVICE_CLASS_PRECIPITATION_INTENSITY,
DEVICE_CLASS_PRECIPITATION,
@ -14,7 +15,7 @@ from esphome.const import (
ICON_THERMOMETER,
)
from . import RGModel, HydreonRGxxComponent
from . import RGModel, RG15Resolution, HydreonRGxxComponent
UNIT_INTENSITY = "intensity"
UNIT_MILLIMETERS = "mm"
@ -37,11 +38,18 @@ RG_MODELS = {
# 1.100 - https://rainsensors.com/wp-content/uploads/sites/3/2021/03/2021.03.11-rg-9_instructions.pdf
# 1.200 - https://rainsensors.com/wp-content/uploads/sites/3/2022/03/2022.02.17-rev-1.200-rg-9_instructions.pdf
}
SUPPORTED_SENSORS = {
RG15_RESOLUTION = {
"low": RG15Resolution.FORCE_LOW,
"high": RG15Resolution.FORCE_HIGH,
}
SUPPORTED_OPTIONS = {
CONF_ACC: ["RG_15"],
CONF_EVENT_ACC: ["RG_15"],
CONF_TOTAL_ACC: ["RG_15"],
CONF_R_INT: ["RG_15"],
CONF_RESOLUTION: ["RG_15"],
CONF_MOISTURE: ["RG_9"],
CONF_TEMPERATURE: ["RG_9"],
CONF_DISABLE_LED: ["RG_9"],
@ -57,7 +65,7 @@ PROTOCOL_NAMES = {
def _validate(config):
for conf, models in SUPPORTED_SENSORS.items():
for conf, models in SUPPORTED_OPTIONS.items():
if conf in config:
if config[CONF_MODEL] not in models:
raise cv.Invalid(
@ -75,6 +83,7 @@ CONFIG_SCHEMA = cv.All(
upper=True,
space="_",
),
cv.Optional(CONF_RESOLUTION): cv.enum(RG15_RESOLUTION, upper=False),
cv.Optional(CONF_ACC): sensor.sensor_schema(
unit_of_measurement=UNIT_MILLIMETERS,
accuracy_decimals=2,
@ -139,6 +148,9 @@ async def to_code(config):
cg.add(var.set_sensor(sens, i))
cg.add(var.set_model(config[CONF_MODEL]))
if CONF_RESOLUTION in config:
cg.add(var.set_resolution(config[CONF_RESOLUTION]))
cg.add(var.set_request_temperature(CONF_TEMPERATURE in config))
if CONF_DISABLE_LED in config:

View File

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

View File

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

View File

@ -50,7 +50,8 @@ class INA226Component : public PollingComponent, public i2c::I2CDevice {
void set_shunt_resistance_ohm(float shunt_resistance_ohm) { shunt_resistance_ohm_ = shunt_resistance_ohm; }
void set_max_current_a(float max_current_a) { max_current_a_ = max_current_a; }
void set_adc_time(AdcTime time) { adc_time_ = time; }
void set_adc_time_voltage(AdcTime time) { adc_time_voltage_ = time; }
void set_adc_time_current(AdcTime time) { adc_time_current_ = time; }
void set_adc_avg_samples(AdcAvgSamples samples) { adc_avg_samples_ = samples; }
void set_bus_voltage_sensor(sensor::Sensor *bus_voltage_sensor) { bus_voltage_sensor_ = bus_voltage_sensor; }
@ -61,7 +62,8 @@ class INA226Component : public PollingComponent, public i2c::I2CDevice {
protected:
float shunt_resistance_ohm_;
float max_current_a_;
AdcTime adc_time_{AdcTime::ADC_TIME_1100US};
AdcTime adc_time_voltage_{AdcTime::ADC_TIME_1100US};
AdcTime adc_time_current_{AdcTime::ADC_TIME_1100US};
AdcAvgSamples adc_avg_samples_{AdcAvgSamples::ADC_AVG_SAMPLES_4};
uint32_t calibration_lsb_;
sensor::Sensor *bus_voltage_sensor_{nullptr};

View File

@ -16,6 +16,7 @@ from esphome.const import (
UNIT_VOLT,
UNIT_AMPERE,
UNIT_WATT,
CONF_VOLTAGE,
)
DEPENDENCIES = ["i2c"]
@ -92,7 +93,15 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_MAX_CURRENT, default=3.2): cv.All(
cv.current, cv.Range(min=0.0)
),
cv.Optional(CONF_ADC_TIME, default="1100 us"): validate_adc_time,
cv.Optional(CONF_ADC_TIME, default="1100 us"): cv.Any(
validate_adc_time,
cv.Schema(
{
cv.Required(CONF_VOLTAGE): validate_adc_time,
cv.Required(CONF_CURRENT): validate_adc_time,
}
),
),
cv.Optional(CONF_ADC_AVERAGING, default=4): cv.enum(
ADC_AVG_SAMPLES, int=True
),
@ -110,7 +119,15 @@ async def to_code(config):
cg.add(var.set_shunt_resistance_ohm(config[CONF_SHUNT_RESISTANCE]))
cg.add(var.set_max_current_a(config[CONF_MAX_CURRENT]))
cg.add(var.set_adc_time(config[CONF_ADC_TIME]))
adc_time_config = config[CONF_ADC_TIME]
if isinstance(adc_time_config, dict):
cg.add(var.set_adc_time_voltage(adc_time_config[CONF_VOLTAGE]))
cg.add(var.set_adc_time_current(adc_time_config[CONF_CURRENT]))
else:
cg.add(var.set_adc_time_voltage(adc_time_config))
cg.add(var.set_adc_time_current(adc_time_config))
cg.add(var.set_adc_avg_samples(config[CONF_ADC_AVERAGING]))
if CONF_BUS_VOLTAGE in config:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -29,6 +29,14 @@ void MHZ19Component::setup() {
}
void MHZ19Component::update() {
uint32_t now_ms = millis();
uint32_t warmup_ms = this->warmup_seconds_ * 1000;
if (now_ms < warmup_ms) {
ESP_LOGW(TAG, "MHZ19 warming up, %ds left", (warmup_ms - now_ms) / 1000);
this->status_set_warning();
return;
}
uint8_t response[MHZ19_RESPONSE_LENGTH];
if (!this->mhz19_write_command_(MHZ19_COMMAND_GET_PPM, response)) {
ESP_LOGW(TAG, "Reading data from MHZ19 failed!");
@ -101,6 +109,8 @@ void MHZ19Component::dump_config() {
} else if (this->abc_boot_logic_ == MHZ19_ABC_DISABLED) {
ESP_LOGCONFIG(TAG, " Automatic baseline calibration disabled on boot");
}
ESP_LOGCONFIG(TAG, " Warmup seconds: %ds", this->warmup_seconds_);
}
} // namespace mhz19

View File

@ -25,6 +25,7 @@ class MHZ19Component : public PollingComponent, public uart::UARTDevice {
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
void set_co2_sensor(sensor::Sensor *co2_sensor) { co2_sensor_ = co2_sensor; }
void set_abc_enabled(bool abc_enabled) { abc_boot_logic_ = abc_enabled ? MHZ19_ABC_ENABLED : MHZ19_ABC_DISABLED; }
void set_warmup_seconds(uint32_t seconds) { warmup_seconds_ = seconds; }
protected:
bool mhz19_write_command_(const uint8_t *command, uint8_t *response);
@ -32,6 +33,7 @@ class MHZ19Component : public PollingComponent, public uart::UARTDevice {
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *co2_sensor_{nullptr};
MHZ19ABCLogic abc_boot_logic_{MHZ19_ABC_NONE};
uint32_t warmup_seconds_;
};
template<typename... Ts> class MHZ19CalibrateZeroAction : public Action<Ts...> {

View File

@ -18,6 +18,7 @@ from esphome.const import (
DEPENDENCIES = ["uart"]
CONF_AUTOMATIC_BASELINE_CALIBRATION = "automatic_baseline_calibration"
CONF_WARMUP_TIME = "warmup_time"
mhz19_ns = cg.esphome_ns.namespace("mhz19")
MHZ19Component = mhz19_ns.class_("MHZ19Component", cg.PollingComponent, uart.UARTDevice)
@ -45,6 +46,9 @@ CONFIG_SCHEMA = (
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_AUTOMATIC_BASELINE_CALIBRATION): cv.boolean,
cv.Optional(
CONF_WARMUP_TIME, default="75s"
): cv.positive_time_period_seconds,
}
)
.extend(cv.polling_component_schema("60s"))
@ -68,6 +72,8 @@ async def to_code(config):
if CONF_AUTOMATIC_BASELINE_CALIBRATION in config:
cg.add(var.set_abc_enabled(config[CONF_AUTOMATIC_BASELINE_CALIBRATION]))
cg.add(var.set_warmup_seconds(config[CONF_WARMUP_TIME]))
CALIBRATION_ACTION_SCHEMA = maybe_simple_id(
{

View File

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

View File

@ -93,11 +93,18 @@ int MicroWakeWord::read_microphone_() {
return 0;
}
size_t bytes_written = this->ring_buffer_->write((void *) this->input_buffer_, bytes_read);
if (bytes_written != bytes_read) {
ESP_LOGW(TAG, "Failed to write some data to ring buffer (written=%d, expected=%d)", bytes_written, bytes_read);
size_t bytes_free = this->ring_buffer_->free();
if (bytes_free < bytes_read) {
ESP_LOGW(TAG,
"Not enough free bytes in ring buffer to store incoming audio data (free bytes=%d, incoming bytes=%d). "
"Resetting the ring buffer. Wake word detection accuracy will be reduced.",
bytes_free, bytes_read);
this->ring_buffer_->reset();
}
return bytes_written;
return this->ring_buffer_->write((void *) this->input_buffer_, bytes_read);
}
void MicroWakeWord::loop() {
@ -206,12 +213,6 @@ bool MicroWakeWord::initialize_models() {
return false;
}
this->preprocessor_stride_buffer_ = audio_samples_allocator.allocate(HISTORY_SAMPLES_TO_KEEP);
if (this->preprocessor_stride_buffer_ == nullptr) {
ESP_LOGE(TAG, "Could not allocate the audio preprocessor's stride buffer.");
return false;
}
this->preprocessor_model_ = tflite::GetModel(G_AUDIO_PREPROCESSOR_INT8_TFLITE);
if (this->preprocessor_model_->version() != TFLITE_SCHEMA_VERSION) {
ESP_LOGE(TAG, "Wake word's audio preprocessor model's schema is not supported");
@ -225,7 +226,7 @@ bool MicroWakeWord::initialize_models() {
}
static tflite::MicroMutableOpResolver<18> preprocessor_op_resolver;
static tflite::MicroMutableOpResolver<14> streaming_op_resolver;
static tflite::MicroMutableOpResolver<17> streaming_op_resolver;
if (!this->register_preprocessor_ops_(preprocessor_op_resolver))
return false;
@ -329,7 +330,6 @@ bool MicroWakeWord::detect_wake_word_() {
}
// Perform inference
uint32_t streaming_size = micros();
float streaming_prob = this->perform_streaming_inference_();
// Add the most recent probability to the sliding window
@ -357,6 +357,9 @@ bool MicroWakeWord::detect_wake_word_() {
for (auto &prob : this->recent_streaming_probabilities_) {
prob = 0;
}
ESP_LOGD(TAG, "Wake word sliding average probability is %.3f and most recent probability is %.3f",
sliding_window_average, streaming_prob);
return true;
}
@ -371,23 +374,6 @@ void MicroWakeWord::set_sliding_window_average_size(size_t size) {
bool MicroWakeWord::slice_available_() {
size_t available = this->ring_buffer_->available();
size_t free = this->ring_buffer_->free();
if (free < NEW_SAMPLES_TO_GET * sizeof(int16_t)) {
// If the ring buffer is within one audio slice of being full, then wake word detection will have issues.
// If this is constantly occuring, then some possibilities why are
// 1) there are too many other slow components configured
// 2) the ESP32 isn't fast enough; e.g., an ESP32 is much slower than an ESP32-S3 at inferences.
// 3) the model is too large
// 4) the model uses operations that are not optimized
ESP_LOGW(TAG,
"Audio buffer is nearly full. Wake word detection may be less accurate and have slower reponse times. "
#if !defined(USE_ESP32_VARIANT_ESP32S3)
"microWakeWord is designed for the ESP32-S3. The current platform is too slow for this model."
#endif
);
}
return available > (NEW_SAMPLES_TO_GET * sizeof(int16_t));
}
@ -396,13 +382,12 @@ bool MicroWakeWord::stride_audio_samples_(int16_t **audio_samples) {
return false;
}
// Copy 320 bytes (160 samples over 10 ms) into preprocessor_audio_buffer_ from history in
// preprocessor_stride_buffer_
memcpy((void *) (this->preprocessor_audio_buffer_), (void *) (this->preprocessor_stride_buffer_),
// Copy the last 320 bytes (160 samples over 10 ms) from the audio buffer to the start of the audio buffer
memcpy((void *) (this->preprocessor_audio_buffer_), (void *) (this->preprocessor_audio_buffer_ + NEW_SAMPLES_TO_GET),
HISTORY_SAMPLES_TO_KEEP * sizeof(int16_t));
// Copy 640 bytes (320 samples over 20 ms) from the ring buffer
// The first 320 bytes (160 samples over 10 ms) will be from history
// Copy 640 bytes (320 samples over 20 ms) from the ring buffer into the audio buffer offset 320 bytes (160 samples
// over 10 ms)
size_t bytes_read = this->ring_buffer_->read((void *) (this->preprocessor_audio_buffer_ + HISTORY_SAMPLES_TO_KEEP),
NEW_SAMPLES_TO_GET * sizeof(int16_t), pdMS_TO_TICKS(200));
@ -415,11 +400,6 @@ bool MicroWakeWord::stride_audio_samples_(int16_t **audio_samples) {
return false;
}
// Copy the last 320 bytes (160 samples over 10 ms) from the audio buffer into history stride buffer for the next
// iteration
memcpy((void *) (this->preprocessor_stride_buffer_), (void *) (this->preprocessor_audio_buffer_ + NEW_SAMPLES_TO_GET),
HISTORY_SAMPLES_TO_KEEP * sizeof(int16_t));
*audio_samples = this->preprocessor_audio_buffer_;
return true;
}
@ -480,7 +460,7 @@ bool MicroWakeWord::register_preprocessor_ops_(tflite::MicroMutableOpResolver<18
return true;
}
bool MicroWakeWord::register_streaming_ops_(tflite::MicroMutableOpResolver<14> &op_resolver) {
bool MicroWakeWord::register_streaming_ops_(tflite::MicroMutableOpResolver<17> &op_resolver) {
if (op_resolver.AddCallOnce() != kTfLiteOk)
return false;
if (op_resolver.AddVarHandle() != kTfLiteOk)
@ -509,6 +489,12 @@ bool MicroWakeWord::register_streaming_ops_(tflite::MicroMutableOpResolver<14> &
return false;
if (op_resolver.AddQuantize() != kTfLiteOk)
return false;
if (op_resolver.AddDepthwiseConv2D() != kTfLiteOk)
return false;
if (op_resolver.AddAveragePool2D() != kTfLiteOk)
return false;
if (op_resolver.AddMaxPool2D() != kTfLiteOk)
return false;
return true;
}

View File

@ -128,7 +128,6 @@ class MicroWakeWord : public Component {
// Stores audio fed into feature generator preprocessor
int16_t *preprocessor_audio_buffer_;
int16_t *preprocessor_stride_buffer_;
bool detected_{false};
@ -181,7 +180,7 @@ class MicroWakeWord : public Component {
bool register_preprocessor_ops_(tflite::MicroMutableOpResolver<18> &op_resolver);
/// @brief Returns true if successfully registered the streaming model's TensorFlow operations
bool register_streaming_ops_(tflite::MicroMutableOpResolver<14> &op_resolver);
bool register_streaming_ops_(tflite::MicroMutableOpResolver<17> &op_resolver);
};
template<typename... Ts> class StartAction : public Action<Ts...>, public Parented<MicroWakeWord> {

View File

@ -9,9 +9,53 @@ AUTO_LOAD = ["climate_ir"]
mitsubishi_ns = cg.esphome_ns.namespace("mitsubishi")
MitsubishiClimate = mitsubishi_ns.class_("MitsubishiClimate", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_SCHEMA.extend(
CONF_SET_FAN_MODE = "set_fan_mode"
SetFanMode = mitsubishi_ns.enum("SetFanMode")
SETFANMODE = {
"quiet_4levels": SetFanMode.MITSUBISHI_FAN_Q4L,
# "5levels": SetFanMode.MITSUBISHI_FAN_5L,
"4levels": SetFanMode.MITSUBISHI_FAN_4L,
"3levels": SetFanMode.MITSUBISHI_FAN_3L,
}
CONF_SUPPORTS_DRY = "supports_dry"
CONF_SUPPORTS_FAN_ONLY = "supports_fan_only"
CONF_HORIZONTAL_DEFAULT = "horizontal_default"
HorizontalDirections = mitsubishi_ns.enum("HorizontalDirections")
HORIZONTAL_DIRECTIONS = {
"left": HorizontalDirections.HORIZONTAL_DIRECTION_LEFT,
"middle-left": HorizontalDirections.HORIZONTAL_DIRECTION_MIDDLE_LEFT,
"middle": HorizontalDirections.HORIZONTAL_DIRECTION_MIDDLE,
"middle-right": HorizontalDirections.HORIZONTAL_DIRECTION_MIDDLE_RIGHT,
"right": HorizontalDirections.HORIZONTAL_DIRECTION_RIGHT,
"split": HorizontalDirections.HORIZONTAL_DIRECTION_SPLIT,
}
CONF_VERTICAL_DEFAULT = "vertical_default"
VerticalDirections = mitsubishi_ns.enum("VerticalDirections")
VERTICAL_DIRECTIONS = {
"auto": VerticalDirections.VERTICAL_DIRECTION_AUTO,
"up": VerticalDirections.VERTICAL_DIRECTION_UP,
"middle-up": VerticalDirections.VERTICAL_DIRECTION_MIDDLE_UP,
"middle": VerticalDirections.VERTICAL_DIRECTION_MIDDLE,
"middle-down": VerticalDirections.VERTICAL_DIRECTION_MIDDLE_DOWN,
"down": VerticalDirections.VERTICAL_DIRECTION_DOWN,
}
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(MitsubishiClimate),
cv.Optional(CONF_SET_FAN_MODE, default="3levels"): cv.enum(SETFANMODE),
cv.Optional(CONF_SUPPORTS_DRY, default=False): cv.boolean,
cv.Optional(CONF_SUPPORTS_FAN_ONLY, default=False): cv.boolean,
cv.Optional(CONF_HORIZONTAL_DEFAULT, default="middle"): cv.enum(
HORIZONTAL_DIRECTIONS
),
cv.Optional(CONF_VERTICAL_DEFAULT, default="middle"): cv.enum(
VERTICAL_DIRECTIONS
),
}
)
@ -19,3 +63,9 @@ CONFIG_SCHEMA = climate_ir.CLIMATE_IR_SCHEMA.extend(
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await climate_ir.register_climate_ir(var, config)
cg.add(var.set_fan_mode(config[CONF_SET_FAN_MODE]))
cg.add(var.set_supports_dry(config[CONF_SUPPORTS_DRY]))
cg.add(var.set_supports_fan_only(config[CONF_SUPPORTS_FAN_ONLY]))
cg.add(var.set_horizontal_default(config[CONF_HORIZONTAL_DEFAULT]))
cg.add(var.set_vertical_default(config[CONF_VERTICAL_DEFAULT]))

View File

@ -8,12 +8,31 @@ static const char *const TAG = "mitsubishi.climate";
const uint32_t MITSUBISHI_OFF = 0x00;
const uint8_t MITSUBISHI_COOL = 0x18;
const uint8_t MITSUBISHI_DRY = 0x10;
const uint8_t MITSUBISHI_AUTO = 0x20;
const uint8_t MITSUBISHI_HEAT = 0x08;
const uint8_t MITSUBISHI_MODE_AUTO = 0x20;
const uint8_t MITSUBISHI_MODE_COOL = 0x18;
const uint8_t MITSUBISHI_MODE_DRY = 0x10;
const uint8_t MITSUBISHI_MODE_FAN_ONLY = 0x38;
const uint8_t MITSUBISHI_MODE_HEAT = 0x08;
const uint8_t MITSUBISHI_MODE_A_HEAT = 0x00;
const uint8_t MITSUBISHI_MODE_A_DRY = 0x02;
const uint8_t MITSUBISHI_MODE_A_COOL = 0x06;
const uint8_t MITSUBISHI_MODE_A_AUTO = 0x06;
const uint8_t MITSUBISHI_WIDE_VANE_SWING = 0xC0;
const uint8_t MITSUBISHI_FAN_AUTO = 0x00;
const uint8_t MITSUBISHI_VERTICAL_VANE_SWING = 0x38;
// const uint8_t MITSUBISHI_AUTO = 0X80;
const uint8_t MITSUBISHI_OTHERWISE = 0X40;
const uint8_t MITSUBISHI_POWERFUL = 0x08;
// Optional presets used to enable some model features
const uint8_t MITSUBISHI_ECONOCOOL = 0x20;
const uint8_t MITSUBISHI_NIGHTMODE = 0xC1;
// Pulse parameters in usec
const uint16_t MITSUBISHI_BIT_MARK = 430;
const uint16_t MITSUBISHI_ONE_SPACE = 1250;
@ -22,19 +41,97 @@ const uint16_t MITSUBISHI_HEADER_MARK = 3500;
const uint16_t MITSUBISHI_HEADER_SPACE = 1700;
const uint16_t MITSUBISHI_MIN_GAP = 17500;
// Marker bytes
const uint8_t MITSUBISHI_BYTE00 = 0X23;
const uint8_t MITSUBISHI_BYTE01 = 0XCB;
const uint8_t MITSUBISHI_BYTE02 = 0X26;
const uint8_t MITSUBISHI_BYTE03 = 0X01;
const uint8_t MITSUBISHI_BYTE04 = 0X00;
const uint8_t MITSUBISHI_BYTE13 = 0X00;
const uint8_t MITSUBISHI_BYTE16 = 0X00;
climate::ClimateTraits MitsubishiClimate::traits() {
auto traits = climate::ClimateTraits();
traits.set_supports_action(false);
traits.set_visual_min_temperature(MITSUBISHI_TEMP_MIN);
traits.set_visual_max_temperature(MITSUBISHI_TEMP_MAX);
traits.set_visual_temperature_step(1.0f);
traits.set_supported_modes({climate::CLIMATE_MODE_OFF});
if (this->supports_cool_)
traits.add_supported_mode(climate::CLIMATE_MODE_COOL);
if (this->supports_heat_)
traits.add_supported_mode(climate::CLIMATE_MODE_HEAT);
if (this->supports_cool_ && this->supports_heat_)
traits.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL);
if (this->supports_dry_)
traits.add_supported_mode(climate::CLIMATE_MODE_DRY);
if (this->supports_fan_only_)
traits.add_supported_mode(climate::CLIMATE_MODE_FAN_ONLY);
// Default to only 3 levels in ESPHome even if most unit supports 4. The 3rd level is not used.
traits.set_supported_fan_modes(
{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH});
if (this->fan_mode_ == MITSUBISHI_FAN_Q4L)
traits.add_supported_fan_mode(climate::CLIMATE_FAN_QUIET);
if (/*this->fan_mode_ == MITSUBISHI_FAN_5L ||*/ this->fan_mode_ >= MITSUBISHI_FAN_4L)
traits.add_supported_fan_mode(climate::CLIMATE_FAN_MIDDLE); // Shouldn't be used for this but it helps
traits.set_supported_swing_modes({climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH,
climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL});
traits.set_supported_presets({climate::CLIMATE_PRESET_NONE, climate::CLIMATE_PRESET_ECO,
climate::CLIMATE_PRESET_BOOST, climate::CLIMATE_PRESET_SLEEP});
return traits;
}
void MitsubishiClimate::transmit_state() {
uint32_t remote_state[18] = {0x23, 0xCB, 0x26, 0x01, 0x00, 0x20, 0x08, 0x00, 0x30,
0x58, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
// Byte 0-4: Constant: 0x23, 0xCB, 0x26, 0x01, 0x00
// Byte 5: On=0x20, Off: 0x00
// Byte 6: MODE (See MODEs above (Heat/Dry/Cool/Auto/FanOnly)
// Byte 7: TEMP bits 0,1,2,3, added to MITSUBISHI_TEMP_MIN
// Example: 0x00 = 0°C+MITSUBISHI_TEMP_MIN = 16°C; 0x07 = 7°C+MITSUBISHI_TEMP_MIN = 23°C
// Byte 8: MODE_A & Wide Vane (if present)
// MODE_A bits 0,1,2 different than Byte 6 (See MODE_As above)
// Wide Vane bits 4,5,6,7 (Middle = 0x30)
// Byte 9: FAN/Vertical Vane/Switch To Auto
// FAN (Speed) bits 0,1,2
// Vertical Vane bits 3,4,5 (Auto = 0x00)
// Switch To Auto bits 6,7
// Byte 10: CLOCK Current time as configured on remote (0x00=Not used)
// Byte 11: END CLOCK Stop time of HVAC (0x00 for no setting)
// Byte 12: START CLOCK Start time of HVAC (0x00 for no setting)
// Byte 13: Constant 0x00
// Byte 14: HVAC specfic, i.e. ECONO COOL, CLEAN MODE, always 0x00
// Byte 15: HVAC specfic, i.e. POWERFUL, SMART SET, PLASMA, always 0x00
// Byte 16: Constant 0x00
// Byte 17: Checksum: SUM[Byte0...Byte16]
uint32_t remote_state[18] = {0x23, 0xCB, 0x26, 0x01, 0x00, 0x20, 0x08, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
switch (this->mode) {
case climate::CLIMATE_MODE_COOL:
remote_state[6] = MITSUBISHI_COOL;
break;
case climate::CLIMATE_MODE_HEAT:
remote_state[6] = MITSUBISHI_HEAT;
remote_state[6] = MITSUBISHI_MODE_HEAT;
remote_state[8] = MITSUBISHI_MODE_A_HEAT;
break;
case climate::CLIMATE_MODE_DRY:
remote_state[6] = MITSUBISHI_MODE_DRY;
remote_state[8] = MITSUBISHI_MODE_A_DRY;
break;
case climate::CLIMATE_MODE_COOL:
remote_state[6] = MITSUBISHI_MODE_COOL;
remote_state[8] = MITSUBISHI_MODE_A_COOL;
break;
case climate::CLIMATE_MODE_HEAT_COOL:
remote_state[6] = MITSUBISHI_AUTO;
remote_state[6] = MITSUBISHI_MODE_AUTO;
remote_state[8] = MITSUBISHI_MODE_A_AUTO;
break;
case climate::CLIMATE_MODE_FAN_ONLY:
remote_state[6] = MITSUBISHI_MODE_FAN_ONLY;
remote_state[8] = MITSUBISHI_MODE_A_AUTO;
break;
case climate::CLIMATE_MODE_OFF:
default:
@ -42,17 +139,111 @@ void MitsubishiClimate::transmit_state() {
break;
}
remote_state[7] = (uint8_t) roundf(clamp<float>(this->target_temperature, MITSUBISHI_TEMP_MIN, MITSUBISHI_TEMP_MAX) -
MITSUBISHI_TEMP_MIN);
// Temperature
if (this->mode == climate::CLIMATE_MODE_DRY) {
remote_state[7] = 24 - MITSUBISHI_TEMP_MIN; // Remote sends always 24°C if "Dry" mode is selected
} else {
remote_state[7] = (uint8_t) roundf(
clamp<float>(this->target_temperature, MITSUBISHI_TEMP_MIN, MITSUBISHI_TEMP_MAX) - MITSUBISHI_TEMP_MIN);
}
ESP_LOGV(TAG, "Sending Mitsubishi target temp: %.1f state: %02" PRIX32 " mode: %02" PRIX32 " temp: %02" PRIX32,
this->target_temperature, remote_state[5], remote_state[6], remote_state[7]);
// Wide Vane
switch (this->swing_mode) {
case climate::CLIMATE_SWING_HORIZONTAL:
case climate::CLIMATE_SWING_BOTH:
remote_state[8] = remote_state[8] | MITSUBISHI_WIDE_VANE_SWING; // Wide Vane Swing
break;
case climate::CLIMATE_SWING_OFF:
default:
remote_state[8] = remote_state[8] | this->default_horizontal_direction_; // Off--> horizontal default position
break;
}
ESP_LOGD(TAG, "default_horizontal_direction_: %02X", this->default_horizontal_direction_);
// Fan Speed & Vertical Vane
// Map of Climate fan mode to this device expected value
// For 3Level: Low = 1, Medium = 2, High = 3
// For 4Level: Low = 1, Middle = 2, Medium = 3, High = 4
// For 5Level: Low = 1, Middle = 2, Medium = 3, High = 4
// For 4Level + Quiet: Low = 1, Middle = 2, Medium = 3, High = 4, Quiet = 5
switch (this->fan_mode.value()) {
case climate::CLIMATE_FAN_LOW:
remote_state[9] = 1;
break;
case climate::CLIMATE_FAN_MEDIUM:
if (this->fan_mode_ == MITSUBISHI_FAN_3L) {
remote_state[9] = 2;
} else {
remote_state[9] = 3;
}
break;
case climate::CLIMATE_FAN_HIGH:
if (this->fan_mode_ == MITSUBISHI_FAN_3L) {
remote_state[9] = 3;
} else {
remote_state[9] = 4;
}
break;
case climate::CLIMATE_FAN_MIDDLE:
remote_state[9] = 2;
break;
case climate::CLIMATE_FAN_QUIET:
remote_state[9] = 5;
break;
default:
remote_state[9] = MITSUBISHI_FAN_AUTO;
break;
}
ESP_LOGD(TAG, "fan: %02x state: %02x", this->fan_mode.value(), remote_state[9]);
// Vertical Vane
switch (this->swing_mode) {
case climate::CLIMATE_SWING_VERTICAL:
case climate::CLIMATE_SWING_BOTH:
remote_state[9] = remote_state[9] | MITSUBISHI_VERTICAL_VANE_SWING | MITSUBISHI_OTHERWISE; // Vane Swing
break;
case climate::CLIMATE_SWING_OFF:
default:
remote_state[9] = remote_state[9] | this->default_vertical_direction_ |
MITSUBISHI_OTHERWISE; // Off--> vertical default position
break;
}
ESP_LOGD(TAG, "default_vertical_direction_: %02X", this->default_vertical_direction_);
// Special modes
switch (this->preset.value()) {
case climate::CLIMATE_PRESET_ECO:
remote_state[6] = MITSUBISHI_MODE_COOL | MITSUBISHI_OTHERWISE;
remote_state[8] = (remote_state[8] & ~7) | MITSUBISHI_MODE_A_COOL;
remote_state[14] = MITSUBISHI_ECONOCOOL;
break;
case climate::CLIMATE_PRESET_SLEEP:
remote_state[9] = MITSUBISHI_FAN_AUTO;
remote_state[14] = MITSUBISHI_NIGHTMODE;
break;
case climate::CLIMATE_PRESET_BOOST:
remote_state[6] |= MITSUBISHI_OTHERWISE;
remote_state[15] = MITSUBISHI_POWERFUL;
break;
case climate::CLIMATE_PRESET_NONE:
default:
break;
}
// Checksum
for (int i = 0; i < 17; i++) {
remote_state[17] += remote_state[i];
}
ESP_LOGD(TAG, "sending: %02X,%02X,%02X,%02X,%02X,%02X,%02X,%02X,%02X,%02X,%02X,%02X,%02X,%02X,%02X,%02X,%02X,%02X",
remote_state[0], remote_state[1], remote_state[2], remote_state[3], remote_state[4], remote_state[5],
remote_state[6], remote_state[7], remote_state[8], remote_state[9], remote_state[10], remote_state[11],
remote_state[12], remote_state[13], remote_state[14], remote_state[15], remote_state[16], remote_state[17]);
auto transmit = this->transmitter_->transmit();
auto *data = transmit.get_data();
@ -81,5 +272,119 @@ void MitsubishiClimate::transmit_state() {
transmit.perform();
}
bool MitsubishiClimate::parse_state_frame_(const uint8_t frame[]) { return false; }
bool MitsubishiClimate::on_receive(remote_base::RemoteReceiveData data) {
uint8_t state_frame[18] = {};
if (!data.expect_item(MITSUBISHI_HEADER_MARK, MITSUBISHI_HEADER_SPACE)) {
ESP_LOGV(TAG, "Header fail");
return false;
}
for (uint8_t pos = 0; pos < 18; pos++) {
uint8_t byte = 0;
for (int8_t bit = 0; bit < 8; bit++) {
if (data.expect_item(MITSUBISHI_BIT_MARK, MITSUBISHI_ONE_SPACE)) {
byte |= 1 << bit;
} else if (!data.expect_item(MITSUBISHI_BIT_MARK, MITSUBISHI_ZERO_SPACE)) {
ESP_LOGV(TAG, "Byte %d bit %d fail", pos, bit);
return false;
}
}
state_frame[pos] = byte;
// Check Header && Footer
if ((pos == 0 && byte != MITSUBISHI_BYTE00) || (pos == 1 && byte != MITSUBISHI_BYTE01) ||
(pos == 2 && byte != MITSUBISHI_BYTE02) || (pos == 3 && byte != MITSUBISHI_BYTE03) ||
(pos == 4 && byte != MITSUBISHI_BYTE04) || (pos == 13 && byte != MITSUBISHI_BYTE13) ||
(pos == 16 && byte != MITSUBISHI_BYTE16)) {
ESP_LOGV(TAG, "Bytes 0,1,2,3,4,13 or 16 fail - invalid value");
return false;
}
}
// On/Off and Mode
if (state_frame[5] == MITSUBISHI_OFF) {
this->mode = climate::CLIMATE_MODE_OFF;
} else {
switch (state_frame[6]) {
case MITSUBISHI_MODE_HEAT:
this->mode = climate::CLIMATE_MODE_HEAT;
break;
case MITSUBISHI_MODE_DRY:
this->mode = climate::CLIMATE_MODE_DRY;
break;
case MITSUBISHI_MODE_COOL:
this->mode = climate::CLIMATE_MODE_COOL;
break;
case MITSUBISHI_MODE_FAN_ONLY:
this->mode = climate::CLIMATE_MODE_FAN_ONLY;
break;
case MITSUBISHI_MODE_AUTO:
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
break;
}
}
// Temp
this->target_temperature = state_frame[7] + MITSUBISHI_TEMP_MIN;
// Fan
uint8_t fan = state_frame[9] & 0x07; //(Bit 0,1,2 = Speed)
// Map of Climate fan mode to this device expected value
// For 3Level: Low = 1, Medium = 2, High = 3
// For 4Level: Low = 1, Middle = 2, Medium = 3, High = 4
// For 5Level: Low = 1, Middle = 2, Medium = 3, High = 4
// For 4Level + Quiet: Low = 1, Middle = 2, Medium = 3, High = 4, Quiet = 5
climate::ClimateFanMode modes_mapping[8] = {
climate::CLIMATE_FAN_AUTO,
climate::CLIMATE_FAN_LOW,
this->fan_mode_ == MITSUBISHI_FAN_3L ? climate::CLIMATE_FAN_MEDIUM : climate::CLIMATE_FAN_MIDDLE,
this->fan_mode_ == MITSUBISHI_FAN_3L ? climate::CLIMATE_FAN_HIGH : climate::CLIMATE_FAN_MEDIUM,
climate::CLIMATE_FAN_HIGH,
climate::CLIMATE_FAN_QUIET,
climate::CLIMATE_FAN_AUTO,
climate::CLIMATE_FAN_AUTO};
this->fan_mode = modes_mapping[fan];
// Wide Vane
uint8_t wide_vane = state_frame[8] & 0xF0; // Bits 4,5,6,7
switch (wide_vane) {
case MITSUBISHI_WIDE_VANE_SWING:
this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL;
break;
default:
this->swing_mode = climate::CLIMATE_SWING_OFF;
break;
}
// Vertical Vane
uint8_t vertical_vane = state_frame[9] & 0x38; // Bits 3,4,5
switch (vertical_vane) {
case MITSUBISHI_VERTICAL_VANE_SWING:
if (this->swing_mode == climate::CLIMATE_SWING_HORIZONTAL) {
this->swing_mode = climate::CLIMATE_SWING_BOTH;
} else {
this->swing_mode = climate::CLIMATE_SWING_VERTICAL;
}
break;
}
switch (state_frame[14]) {
case MITSUBISHI_ECONOCOOL:
this->preset = climate::CLIMATE_PRESET_ECO;
break;
case MITSUBISHI_NIGHTMODE:
this->preset = climate::CLIMATE_PRESET_SLEEP;
break;
}
ESP_LOGV(TAG, "Receiving: %s", format_hex_pretty(state_frame, 18).c_str());
this->publish_state();
return true;
}
} // namespace mitsubishi
} // namespace esphome

View File

@ -11,13 +11,72 @@ namespace mitsubishi {
const uint8_t MITSUBISHI_TEMP_MIN = 16; // Celsius
const uint8_t MITSUBISHI_TEMP_MAX = 31; // Celsius
// Fan mode
enum SetFanMode {
MITSUBISHI_FAN_3L = 0, // 3 levels + auto
MITSUBISHI_FAN_4L, // 4 levels + auto
MITSUBISHI_FAN_Q4L, // Quiet + 4 levels + auto
// MITSUBISHI_FAN_5L, // 5 levels + auto
};
// Enum to represent horizontal directios
enum HorizontalDirection {
HORIZONTAL_DIRECTION_LEFT = 0x10,
HORIZONTAL_DIRECTION_MIDDLE_LEFT = 0x20,
HORIZONTAL_DIRECTION_MIDDLE = 0x30,
HORIZONTAL_DIRECTION_MIDDLE_RIGHT = 0x40,
HORIZONTAL_DIRECTION_RIGHT = 0x50,
HORIZONTAL_DIRECTION_SPLIT = 0x80,
};
// Enum to represent vertical directions
enum VerticalDirection {
VERTICAL_DIRECTION_AUTO = 0x00,
VERTICAL_DIRECTION_UP = 0x08,
VERTICAL_DIRECTION_MIDDLE_UP = 0x10,
VERTICAL_DIRECTION_MIDDLE = 0x18,
VERTICAL_DIRECTION_MIDDLE_DOWN = 0x20,
VERTICAL_DIRECTION_DOWN = 0x28,
};
class MitsubishiClimate : public climate_ir::ClimateIR {
public:
MitsubishiClimate() : climate_ir::ClimateIR(MITSUBISHI_TEMP_MIN, MITSUBISHI_TEMP_MAX) {}
MitsubishiClimate()
: climate_ir::ClimateIR(MITSUBISHI_TEMP_MIN, MITSUBISHI_TEMP_MAX, 1.0f, true, true,
{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MIDDLE,
climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH, climate::CLIMATE_FAN_QUIET},
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH, climate::CLIMATE_SWING_VERTICAL,
climate::CLIMATE_SWING_HORIZONTAL},
{climate::CLIMATE_PRESET_NONE, climate::CLIMATE_PRESET_ECO, climate::CLIMATE_PRESET_BOOST,
climate::CLIMATE_PRESET_SLEEP}) {}
void set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
void set_supports_dry(bool supports_dry) { this->supports_dry_ = supports_dry; }
void set_supports_fan_only(bool supports_fan_only) { this->supports_fan_only_ = supports_fan_only; }
void set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; }
void set_fan_mode(SetFanMode fan_mode) { this->fan_mode_ = fan_mode; }
void set_horizontal_default(HorizontalDirection horizontal_direction) {
this->default_horizontal_direction_ = horizontal_direction;
}
void set_vertical_default(VerticalDirection vertical_direction) {
this->default_vertical_direction_ = vertical_direction;
}
protected:
/// Transmit via IR the state of this climate controller.
// 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_(const uint8_t frame[]);
SetFanMode fan_mode_;
HorizontalDirection default_horizontal_direction_;
VerticalDirection default_vertical_direction_;
climate::ClimateTraits traits() override;
};
} // namespace mitsubishi

View File

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

View File

@ -6,6 +6,9 @@ from esphome.components.esp32 import add_idf_sdkconfig_option
from esphome.const import (
CONF_ENABLE_IPV6,
CONF_MIN_IPV6_ADDR_COUNT,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_RP2040,
)
CODEOWNERS = ["@esphome/core"]
@ -16,25 +19,30 @@ IPAddress = network_ns.class_("IPAddress")
CONFIG_SCHEMA = cv.Schema(
{
cv.Optional(CONF_ENABLE_IPV6, default=False): cv.boolean,
cv.SplitDefault(CONF_ENABLE_IPV6): cv.All(
cv.boolean, cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040])
),
cv.Optional(CONF_MIN_IPV6_ADDR_COUNT, default=0): cv.positive_int,
}
)
async def to_code(config):
cg.add_define("USE_NETWORK_IPV6", config[CONF_ENABLE_IPV6])
cg.add_define("USE_NETWORK_MIN_IPV6_ADDR_COUNT", config[CONF_MIN_IPV6_ADDR_COUNT])
if CORE.using_esp_idf:
add_idf_sdkconfig_option("CONFIG_LWIP_IPV6", config[CONF_ENABLE_IPV6])
add_idf_sdkconfig_option(
"CONFIG_LWIP_IPV6_AUTOCONFIG", config[CONF_ENABLE_IPV6]
if CONF_ENABLE_IPV6 in config:
cg.add_define("USE_NETWORK_IPV6", config[CONF_ENABLE_IPV6])
cg.add_define(
"USE_NETWORK_MIN_IPV6_ADDR_COUNT", config[CONF_MIN_IPV6_ADDR_COUNT]
)
else:
if config[CONF_ENABLE_IPV6]:
cg.add_build_flag("-DCONFIG_LWIP_IPV6")
cg.add_build_flag("-DCONFIG_LWIP_IPV6_AUTOCONFIG")
if CORE.is_rp2040:
cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_ENABLE_IPV6")
if CORE.is_esp8266:
cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_LWIP2_IPV6_LOW_MEMORY")
if CORE.using_esp_idf:
add_idf_sdkconfig_option("CONFIG_LWIP_IPV6", config[CONF_ENABLE_IPV6])
add_idf_sdkconfig_option(
"CONFIG_LWIP_IPV6_AUTOCONFIG", config[CONF_ENABLE_IPV6]
)
else:
if config[CONF_ENABLE_IPV6]:
cg.add_build_flag("-DCONFIG_LWIP_IPV6")
cg.add_build_flag("-DCONFIG_LWIP_IPV6_AUTOCONFIG")
if CORE.is_rp2040:
cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_ENABLE_IPV6")
if CORE.is_esp8266:
cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_LWIP2_IPV6_LOW_MEMORY")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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