diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index aa90ef365f..25411c19f5 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -16,6 +16,7 @@ Quick description and explanation of changes ## Test Environment - [ ] ESP32 +- [ ] ESP32 IDF - [ ] ESP8266 ## Example entry for `config.yaml`: diff --git a/.github/issue-close-app.yml b/.github/issue-close-app.yml deleted file mode 100644 index 5f5fb7572d..0000000000 --- a/.github/issue-close-app.yml +++ /dev/null @@ -1,7 +0,0 @@ -comment: >- - https://github.com/esphome/esphome/issues/430 -issueConfigs: -- content: - - "OTHERWISE THE ISSUE WILL BE CLOSED AUTOMATICALLY" - -caseInsensitive: false diff --git a/.github/lock.yml b/.github/lock.yml deleted file mode 100644 index 0680577b2e..0000000000 --- a/.github/lock.yml +++ /dev/null @@ -1,36 +0,0 @@ -# Configuration for Lock Threads - https://github.com/dessant/lock-threads - -# Number of days of inactivity before a closed issue or pull request is locked -daysUntilLock: 7 - -# Skip issues and pull requests created before a given timestamp. Timestamp must -# follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable -skipCreatedBefore: false - -# Issues and pull requests with these labels will be ignored. Set to `[]` to disable -exemptLabels: - - keep-open - -# Label to add before locking, such as `outdated`. Set to `false` to disable -lockLabel: false - -# Comment to post before locking. Set to `false` to disable -lockComment: false - -# Assign `resolved` as the reason for locking. Set to `false` to disable -setLockReason: false - -# Limit to only `issues` or `pulls` -# only: issues - -# Optionally, specify configuration settings just for `issues` or `pulls` -# issues: -# exemptLabels: -# - help-wanted -# lockLabel: outdated - -# pulls: -# daysUntilLock: 30 - -# Repository to extend settings from -# _extends: repo diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index 12f5a7dfc2..1d1cc169b2 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -17,6 +17,10 @@ on: - 'requirements*.txt' - 'platformio.ini' +permissions: + contents: read + packages: read + jobs: check-docker: name: Build docker containers diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 45e2f2735c..02b64d2bf5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,9 @@ on: pull_request: +permissions: + contents: read + jobs: ci: name: ${{ matrix.name }} diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index 375b8f1db4..ceb45b2a91 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -9,13 +9,19 @@ permissions: issues: write pull-requests: write +concurrency: + group: lock + jobs: lock: runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@v2 + - uses: dessant/lock-threads@v3 with: - github-token: ${{ github.token }} - pr-lock-inactive-days: "1" + pr-inactive-days: "1" pr-lock-reason: "" - process-only: prs + exclude-any-pr-labels: keep-open + + issue-inactive-days: "7" + issue-lock-reason: "" + exclude-any-issue-labels: keep-open diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index afd893d065..d6895becc0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,6 +7,9 @@ on: schedule: - cron: "0 2 * * *" +permissions: + contents: read + jobs: init: name: Initialize build @@ -52,6 +55,9 @@ jobs: deploy-docker: name: Build and publish docker containers if: github.repository == 'esphome/esphome' + permissions: + contents: read + packages: write runs-on: ubuntu-latest needs: [init] strategy: @@ -93,6 +99,9 @@ jobs: deploy-docker-manifest: if: github.repository == 'esphome/esphome' + permissions: + contents: read + packages: write runs-on: ubuntu-latest needs: [init, deploy-docker] strategy: diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 712ae1a289..c3e450d0cf 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -9,13 +9,15 @@ permissions: issues: write pull-requests: write +concurrency: + group: lock + jobs: stale: runs-on: ubuntu-latest steps: - uses: actions/stale@v4 with: - repo-token: ${{ github.token }} days-before-pr-stale: 90 days-before-pr-close: 7 days-before-issue-stale: -1 @@ -28,3 +30,19 @@ jobs: pull request has been automatically marked as stale because of that and will be closed if no further activity occurs within 7 days. Thank you for your contributions. + + # Use stale to automatically close issues with a reference to the issue tracker + close-issues: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v4 + with: + days-before-pr-stale: -1 + days-before-pr-close: -1 + days-before-issue-stale: 1 + days-before-issue-close: 1 + remove-stale-when-updated: true + stale-issue-label: "stale" + exempt-issue-labels: "not-stale" + stale-issue-message: > + https://github.com/esphome/esphome/issues/430 diff --git a/CODEOWNERS b/CODEOWNERS index a7cf3a1b68..18b4564280 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -31,6 +31,7 @@ esphome/components/binary_sensor/* @esphome/core esphome/components/ble_client/* @buxtronix esphome/components/bme680_bsec/* @trvrnrth esphome/components/canbus/* @danielschramm @mvturnho +esphome/components/cap1188/* @MrEditor97 esphome/components/captive_portal/* @OttoWinter esphome/components/ccs811/* @habbie esphome/components/climate/* @esphome/core @@ -39,6 +40,7 @@ esphome/components/color_temperature/* @jesserockz esphome/components/coolix/* @glmnet esphome/components/cover/* @esphome/core esphome/components/cs5460a/* @balrog-kun +esphome/components/cse7761/* @berfenger esphome/components/ct_clamp/* @jesserockz esphome/components/current_based/* @djwmarcx esphome/components/daly_bms/* @s1lvi0 @@ -51,6 +53,7 @@ esphome/components/dsmr/* @glmnet @zuidwijk esphome/components/esp32/* @esphome/core esphome/components/esp32_ble/* @jesserockz esphome/components/esp32_ble_server/* @jesserockz +esphome/components/esp32_camera_web_server/* @ayufan esphome/components/esp32_improv/* @jesserockz esphome/components/esp8266/* @esphome/core esphome/components/exposure_notifications/* @OttoWinter @@ -70,6 +73,7 @@ esphome/components/homeassistant/* @OttoWinter esphome/components/hrxl_maxsonar_wr/* @netmikey esphome/components/i2c/* @esphome/core esphome/components/improv/* @jesserockz +esphome/components/improv_serial/* @esphome/core esphome/components/inkbird_ibsth1_mini/* @fkirill esphome/components/inkplate6/* @jesserockz esphome/components/integration/* @OttoWinter diff --git a/docker/Dockerfile b/docker/Dockerfile index 7928ff8901..0d30bb0267 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -5,12 +5,12 @@ # One of "docker", "hassio" ARG BASEIMGTYPE=docker -FROM ghcr.io/hassio-addons/debian-base/amd64:5.1.0 AS base-hassio-amd64 -FROM ghcr.io/hassio-addons/debian-base/aarch64:5.1.0 AS base-hassio-arm64 -FROM ghcr.io/hassio-addons/debian-base/armv7:5.1.0 AS base-hassio-armv7 -FROM debian:bullseye-20210902-slim AS base-docker-amd64 -FROM debian:bullseye-20210902-slim AS base-docker-arm64 -FROM debian:bullseye-20210902-slim AS base-docker-armv7 +FROM ghcr.io/hassio-addons/debian-base/amd64:5.1.1 AS base-hassio-amd64 +FROM ghcr.io/hassio-addons/debian-base/aarch64:5.1.1 AS base-hassio-arm64 +FROM ghcr.io/hassio-addons/debian-base/armv7:5.1.1 AS base-hassio-armv7 +FROM debian:bullseye-20211011-slim AS base-docker-amd64 +FROM debian:bullseye-20211011-slim AS base-docker-arm64 +FROM debian:bullseye-20211011-slim AS base-docker-armv7 # Use TARGETARCH/TARGETVARIANT defined by docker # https://docs.docker.com/engine/reference/builder/#automatic-platform-args-in-the-global-scope @@ -43,7 +43,7 @@ RUN \ # Ubuntu python3-pip is missing wheel pip3 install --no-cache-dir \ wheel==0.36.2 \ - platformio==5.2.1 \ + platformio==5.2.2 \ # Change some platformio settings && platformio settings set enable_telemetry No \ && platformio settings set check_libraries_interval 1000000 \ diff --git a/esphome/automation.py b/esphome/automation.py index 0768bf8869..fab998527f 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -3,6 +3,7 @@ import esphome.config_validation as cv from esphome.const import ( CONF_AUTOMATION_ID, CONF_CONDITION, + CONF_COUNT, CONF_ELSE, CONF_ID, CONF_THEN, @@ -66,6 +67,7 @@ DelayAction = cg.esphome_ns.class_("DelayAction", Action, cg.Component) LambdaAction = cg.esphome_ns.class_("LambdaAction", Action) IfAction = cg.esphome_ns.class_("IfAction", Action) WhileAction = cg.esphome_ns.class_("WhileAction", Action) +RepeatAction = cg.esphome_ns.class_("RepeatAction", Action) WaitUntilAction = cg.esphome_ns.class_("WaitUntilAction", Action, cg.Component) UpdateComponentAction = cg.esphome_ns.class_("UpdateComponentAction", Action) Automation = cg.esphome_ns.class_("Automation") @@ -241,6 +243,25 @@ async def while_action_to_code(config, action_id, template_arg, args): return var +@register_action( + "repeat", + RepeatAction, + cv.Schema( + { + cv.Required(CONF_COUNT): cv.templatable(cv.positive_not_null_int), + cv.Required(CONF_THEN): validate_action_list, + } + ), +) +async def repeat_action_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + count_template = await cg.templatable(config[CONF_COUNT], args, cg.uint32) + cg.add(var.set_count(count_template)) + actions = await build_action_list(config[CONF_THEN], template_arg, args) + cg.add(var.add_then(actions)) + return var + + def validate_wait_until(value): schema = cv.Schema( { diff --git a/esphome/codegen.py b/esphome/codegen.py index 4f9f67245d..5e1e934e58 100644 --- a/esphome/codegen.py +++ b/esphome/codegen.py @@ -81,4 +81,5 @@ from esphome.cpp_types import ( # noqa GPIOPin, InternalGPIOPin, gpio_Flags, + EntityCategory, ) diff --git a/esphome/components/adc/adc_sensor.cpp b/esphome/components/adc/adc_sensor.cpp index c8f8b0e0f6..c8242ce008 100644 --- a/esphome/components/adc/adc_sensor.cpp +++ b/esphome/components/adc/adc_sensor.cpp @@ -1,5 +1,6 @@ #include "adc_sensor.h" #include "esphome/core/log.h" +#include "esphome/core/helpers.h" #ifdef USE_ESP8266 #ifdef USE_ADC_SENSOR_VCC @@ -15,50 +16,6 @@ namespace adc { static const char *const TAG = "adc"; -#ifdef USE_ESP32 -void ADCSensor::set_attenuation(adc_atten_t attenuation) { this->attenuation_ = attenuation; } - -inline adc1_channel_t gpio_to_adc1(uint8_t pin) { -#if CONFIG_IDF_TARGET_ESP32 - switch (pin) { - case 36: - return ADC1_CHANNEL_0; - case 37: - return ADC1_CHANNEL_1; - case 38: - return ADC1_CHANNEL_2; - case 39: - return ADC1_CHANNEL_3; - case 32: - return ADC1_CHANNEL_4; - case 33: - return ADC1_CHANNEL_5; - case 34: - return ADC1_CHANNEL_6; - case 35: - return ADC1_CHANNEL_7; - default: - return ADC1_CHANNEL_MAX; - } -#elif CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32H2 - switch (pin) { - case 0: - return ADC1_CHANNEL_0; - case 1: - return ADC1_CHANNEL_1; - case 2: - return ADC1_CHANNEL_2; - case 3: - return ADC1_CHANNEL_3; - case 4: - return ADC1_CHANNEL_4; - default: - return ADC1_CHANNEL_MAX; - } -#endif -} -#endif - void ADCSensor::setup() { ESP_LOGCONFIG(TAG, "Setting up ADC '%s'...", this->get_name().c_str()); #ifndef USE_ADC_SENSOR_VCC @@ -66,13 +23,36 @@ void ADCSensor::setup() { #endif #ifdef USE_ESP32 - adc1_config_channel_atten(gpio_to_adc1(pin_->get_pin()), attenuation_); adc1_config_width(ADC_WIDTH_BIT_12); -#if !CONFIG_IDF_TARGET_ESP32C3 && !CONFIG_IDF_TARGET_ESP32H2 - adc_gpio_init(ADC_UNIT_1, (adc_channel_t) gpio_to_adc1(pin_->get_pin())); -#endif + if (!autorange_) { + adc1_config_channel_atten(channel_, attenuation_); + } + + // load characteristics for each attenuation + for (int i = 0; i < (int) ADC_ATTEN_MAX; i++) { + auto cal_value = esp_adc_cal_characterize(ADC_UNIT_1, (adc_atten_t) i, ADC_WIDTH_BIT_12, + 1100, // default vref + &cal_characteristics_[i]); + switch (cal_value) { + case ESP_ADC_CAL_VAL_EFUSE_VREF: + ESP_LOGV(TAG, "Using eFuse Vref for calibration"); + break; + case ESP_ADC_CAL_VAL_EFUSE_TP: + ESP_LOGV(TAG, "Using two-point eFuse Vref for calibration"); + break; + case ESP_ADC_CAL_VAL_DEFAULT_VREF: + default: + break; + } + } + + // adc_gpio_init doesn't exist on ESP32-C3 or ESP32-H2 +#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32H2) + adc_gpio_init(ADC_UNIT_1, (adc_channel_t) channel_); #endif +#endif // USE_ESP32 } + void ADCSensor::dump_config() { LOG_SENSOR("", "ADC Sensor", this); #ifdef USE_ESP8266 @@ -81,84 +61,107 @@ void ADCSensor::dump_config() { #else LOG_PIN(" Pin: ", pin_); #endif -#endif +#endif // USE_ESP8266 + #ifdef USE_ESP32 LOG_PIN(" Pin: ", pin_); - switch (this->attenuation_) { - case ADC_ATTEN_DB_0: - ESP_LOGCONFIG(TAG, " Attenuation: 0db (max 1.1V)"); - break; - case ADC_ATTEN_DB_2_5: - ESP_LOGCONFIG(TAG, " Attenuation: 2.5db (max 1.5V)"); - break; - case ADC_ATTEN_DB_6: - ESP_LOGCONFIG(TAG, " Attenuation: 6db (max 2.2V)"); - break; - case ADC_ATTEN_DB_11: - ESP_LOGCONFIG(TAG, " Attenuation: 11db (max 3.9V)"); - break; - default: // This is to satisfy the unused ADC_ATTEN_MAX - break; - } -#endif + if (autorange_) + ESP_LOGCONFIG(TAG, " Attenuation: auto"); + else + switch (this->attenuation_) { + case ADC_ATTEN_DB_0: + ESP_LOGCONFIG(TAG, " Attenuation: 0db (max 1.1V)"); + break; + case ADC_ATTEN_DB_2_5: + ESP_LOGCONFIG(TAG, " Attenuation: 2.5db (max 1.5V)"); + break; + case ADC_ATTEN_DB_6: + ESP_LOGCONFIG(TAG, " Attenuation: 6db (max 2.2V)"); + break; + case ADC_ATTEN_DB_11: + ESP_LOGCONFIG(TAG, " Attenuation: 11db (max 3.9V)"); + break; + default: // This is to satisfy the unused ADC_ATTEN_MAX + break; + } +#endif // USE_ESP32 LOG_UPDATE_INTERVAL(this); } + float ADCSensor::get_setup_priority() const { return setup_priority::DATA; } void ADCSensor::update() { float value_v = this->sample(); - ESP_LOGD(TAG, "'%s': Got voltage=%.2fV", this->get_name().c_str(), value_v); + ESP_LOGD(TAG, "'%s': Got voltage=%.4fV", this->get_name().c_str(), value_v); this->publish_state(value_v); } -float ADCSensor::sample() { -#ifdef USE_ESP32 - int raw = adc1_get_raw(gpio_to_adc1(pin_->get_pin())); - float value_v = raw / 4095.0f; -#if CONFIG_IDF_TARGET_ESP32 - switch (this->attenuation_) { - case ADC_ATTEN_DB_0: - value_v *= 1.1; - break; - case ADC_ATTEN_DB_2_5: - value_v *= 1.5; - break; - case ADC_ATTEN_DB_6: - value_v *= 2.2; - break; - case ADC_ATTEN_DB_11: - value_v *= 3.9; - break; - default: // This is to satisfy the unused ADC_ATTEN_MAX - break; - } -#elif CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32H2 - switch (this->attenuation_) { - case ADC_ATTEN_DB_0: - value_v *= 0.84; - break; - case ADC_ATTEN_DB_2_5: - value_v *= 1.13; - break; - case ADC_ATTEN_DB_6: - value_v *= 1.56; - break; - case ADC_ATTEN_DB_11: - value_v *= 3.0; - break; - default: // This is to satisfy the unused ADC_ATTEN_MAX - break; - } -#endif - return value_v; -#endif #ifdef USE_ESP8266 +float ADCSensor::sample() { #ifdef USE_ADC_SENSOR_VCC - return ESP.getVcc() / 1024.0f; // NOLINT(readability-static-accessed-through-instance) + int raw = ESP.getVcc(); // NOLINT(readability-static-accessed-through-instance) #else - return analogRead(this->pin_->get_pin()) / 1024.0f; // NOLINT -#endif + int raw = analogRead(this->pin_->get_pin()); // NOLINT #endif + if (output_raw_) { + return raw; + } + return raw / 1024.0f; } +#endif + +#ifdef USE_ESP32 +float ADCSensor::sample() { + if (!autorange_) { + int raw = adc1_get_raw(channel_); + if (raw == -1) { + return NAN; + } + if (output_raw_) { + return raw; + } + uint32_t mv = esp_adc_cal_raw_to_voltage(raw, &cal_characteristics_[(int) attenuation_]); + return mv / 1000.0f; + } + + int raw11, raw6 = 4095, raw2 = 4095, raw0 = 4095; + adc1_config_channel_atten(channel_, ADC_ATTEN_DB_11); + raw11 = adc1_get_raw(channel_); + if (raw11 < 4095) { + adc1_config_channel_atten(channel_, ADC_ATTEN_DB_6); + raw6 = adc1_get_raw(channel_); + if (raw6 < 4095) { + adc1_config_channel_atten(channel_, ADC_ATTEN_DB_2_5); + raw2 = adc1_get_raw(channel_); + if (raw2 < 4095) { + adc1_config_channel_atten(channel_, ADC_ATTEN_DB_0); + raw0 = adc1_get_raw(channel_); + } + } + } + + if (raw0 == -1 || raw2 == -1 || raw6 == -1 || raw11 == -1) { + return NAN; + } + + uint32_t mv11 = esp_adc_cal_raw_to_voltage(raw11, &cal_characteristics_[(int) ADC_ATTEN_DB_11]); + uint32_t mv6 = esp_adc_cal_raw_to_voltage(raw6, &cal_characteristics_[(int) ADC_ATTEN_DB_6]); + uint32_t mv2 = esp_adc_cal_raw_to_voltage(raw2, &cal_characteristics_[(int) ADC_ATTEN_DB_2_5]); + uint32_t mv0 = esp_adc_cal_raw_to_voltage(raw0, &cal_characteristics_[(int) ADC_ATTEN_DB_0]); + + // Contribution of each value, in range 0-2048 + uint32_t c11 = std::min(raw11, 2048); + uint32_t c6 = 2048 - std::abs(raw6 - 2048); + uint32_t c2 = 2048 - std::abs(raw2 - 2048); + uint32_t c0 = std::min(4095 - raw0, 2048); + // max theoretical csum value is 2048*4 = 8192 + uint32_t csum = c11 + c6 + c2 + c0; + + // each mv is max 3900; so max value is 3900*2048*4, fits in unsigned + uint32_t mv_scaled = (mv11 * c11) + (mv6 * c6) + (mv2 * c2) + (mv0 * c0); + return mv_scaled / (float) (csum * 1000U); +} +#endif // USE_ESP32 + #ifdef USE_ESP8266 std::string ADCSensor::unique_id() { return get_mac_address() + "-adc"; } #endif diff --git a/esphome/components/adc/adc_sensor.h b/esphome/components/adc/adc_sensor.h index b8c702be4e..12272a1577 100644 --- a/esphome/components/adc/adc_sensor.h +++ b/esphome/components/adc/adc_sensor.h @@ -8,6 +8,7 @@ #ifdef USE_ESP32 #include "driver/adc.h" +#include #endif namespace esphome { @@ -17,7 +18,9 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage public: #ifdef USE_ESP32 /// Set the attenuation for this pin. Only available on the ESP32. - void set_attenuation(adc_atten_t attenuation); + void set_attenuation(adc_atten_t attenuation) { attenuation_ = attenuation; } + void set_channel(adc1_channel_t channel) { channel_ = channel; } + void set_autorange(bool autorange) { autorange_ = autorange; } #endif /// Update adc values. @@ -28,6 +31,7 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage /// `HARDWARE_LATE` setup priority. float get_setup_priority() const override; void set_pin(InternalGPIOPin *pin) { this->pin_ = pin; } + void set_output_raw(bool output_raw) { output_raw_ = output_raw; } float sample() override; #ifdef USE_ESP8266 @@ -36,9 +40,13 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage protected: InternalGPIOPin *pin_; + bool output_raw_{false}; #ifdef USE_ESP32 adc_atten_t attenuation_{ADC_ATTEN_DB_0}; + adc1_channel_t channel_{}; + bool autorange_{false}; + esp_adc_cal_characteristics_t cal_characteristics_[(int) ADC_ATTEN_MAX] = {}; #endif }; diff --git a/esphome/components/adc/sensor.py b/esphome/components/adc/sensor.py index 26ef504c1a..c812e67a68 100644 --- a/esphome/components/adc/sensor.py +++ b/esphome/components/adc/sensor.py @@ -4,14 +4,24 @@ from esphome import pins from esphome.components import sensor, voltage_sampler from esphome.const import ( CONF_ATTENUATION, + CONF_RAW, CONF_ID, CONF_INPUT, + CONF_NUMBER, CONF_PIN, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT, UNIT_VOLT, ) from esphome.core import CORE +from esphome.components.esp32 import get_esp32_variant +from esphome.components.esp32.const import ( + VARIANT_ESP32, + VARIANT_ESP32C3, + VARIANT_ESP32H2, + VARIANT_ESP32S2, + VARIANT_ESP32S3, +) AUTO_LOAD = ["voltage_sampler"] @@ -21,6 +31,62 @@ ATTENUATION_MODES = { "2.5db": cg.global_ns.ADC_ATTEN_DB_2_5, "6db": cg.global_ns.ADC_ATTEN_DB_6, "11db": cg.global_ns.ADC_ATTEN_DB_11, + "auto": "auto", +} + +adc1_channel_t = cg.global_ns.enum("adc1_channel_t") + +# From https://github.com/espressif/esp-idf/blob/master/components/driver/include/driver/adc_common.h +# pin to adc1 channel mapping +ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = { + VARIANT_ESP32: { + 36: adc1_channel_t.ADC1_CHANNEL_0, + 37: adc1_channel_t.ADC1_CHANNEL_1, + 38: adc1_channel_t.ADC1_CHANNEL_2, + 39: adc1_channel_t.ADC1_CHANNEL_3, + 32: adc1_channel_t.ADC1_CHANNEL_4, + 33: adc1_channel_t.ADC1_CHANNEL_5, + 34: adc1_channel_t.ADC1_CHANNEL_6, + 35: adc1_channel_t.ADC1_CHANNEL_7, + }, + VARIANT_ESP32S2: { + 1: adc1_channel_t.ADC1_CHANNEL_0, + 2: adc1_channel_t.ADC1_CHANNEL_1, + 3: adc1_channel_t.ADC1_CHANNEL_2, + 4: adc1_channel_t.ADC1_CHANNEL_3, + 5: adc1_channel_t.ADC1_CHANNEL_4, + 6: adc1_channel_t.ADC1_CHANNEL_5, + 7: adc1_channel_t.ADC1_CHANNEL_6, + 8: adc1_channel_t.ADC1_CHANNEL_7, + 9: adc1_channel_t.ADC1_CHANNEL_8, + 10: adc1_channel_t.ADC1_CHANNEL_9, + }, + VARIANT_ESP32S3: { + 1: adc1_channel_t.ADC1_CHANNEL_0, + 2: adc1_channel_t.ADC1_CHANNEL_1, + 3: adc1_channel_t.ADC1_CHANNEL_2, + 4: adc1_channel_t.ADC1_CHANNEL_3, + 5: adc1_channel_t.ADC1_CHANNEL_4, + 6: adc1_channel_t.ADC1_CHANNEL_5, + 7: adc1_channel_t.ADC1_CHANNEL_6, + 8: adc1_channel_t.ADC1_CHANNEL_7, + 9: adc1_channel_t.ADC1_CHANNEL_8, + 10: adc1_channel_t.ADC1_CHANNEL_9, + }, + VARIANT_ESP32C3: { + 0: adc1_channel_t.ADC1_CHANNEL_0, + 1: adc1_channel_t.ADC1_CHANNEL_1, + 2: adc1_channel_t.ADC1_CHANNEL_2, + 3: adc1_channel_t.ADC1_CHANNEL_3, + 4: adc1_channel_t.ADC1_CHANNEL_4, + }, + VARIANT_ESP32H2: { + 0: adc1_channel_t.ADC1_CHANNEL_0, + 1: adc1_channel_t.ADC1_CHANNEL_1, + 2: adc1_channel_t.ADC1_CHANNEL_2, + 3: adc1_channel_t.ADC1_CHANNEL_3, + 4: adc1_channel_t.ADC1_CHANNEL_4, + }, } @@ -29,15 +95,16 @@ def validate_adc_pin(value): return cv.only_on_esp8266("VCC") if CORE.is_esp32: - from esphome.components.esp32 import is_esp32c3 - value = pins.internal_gpio_input_pin_number(value) - if is_esp32c3(): - if not (0 <= value <= 4): # ADC1 - raise cv.Invalid("ESP32-C3: Only pins 0 though 4 support ADC.") - elif not (32 <= value <= 39): # ADC1 - raise cv.Invalid("ESP32: Only pins 32 though 39 support ADC.") - elif CORE.is_esp8266: + variant = get_esp32_variant() + if variant not in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL: + raise cv.Invalid(f"This ESP32 variant ({variant}) is not supported") + + if value not in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant]: + raise cv.Invalid(f"{variant} doesn't support ADC on this pin") + return pins.internal_gpio_input_pin_schema(value) + + if CORE.is_esp8266: from esphome.components.esp8266.gpio import CONF_ANALOG value = pins.internal_gpio_pin_number({CONF_ANALOG: True, CONF_INPUT: True})( @@ -49,10 +116,14 @@ def validate_adc_pin(value): return pins.gpio_pin_schema( {CONF_ANALOG: True, CONF_INPUT: True}, internal=True )(value) - else: - raise NotImplementedError - return pins.internal_gpio_input_pin_schema(value) + raise NotImplementedError + + +def validate_config(config): + if config[CONF_RAW] and config.get(CONF_ATTENUATION, None) == "auto": + raise cv.Invalid("Automatic attenuation cannot be used when raw output is set.") + return config adc_ns = cg.esphome_ns.namespace("adc") @@ -60,7 +131,7 @@ ADCSensor = adc_ns.class_( "ADCSensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler ) -CONFIG_SCHEMA = ( +CONFIG_SCHEMA = cv.All( sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, accuracy_decimals=2, @@ -71,12 +142,14 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(ADCSensor), cv.Required(CONF_PIN): validate_adc_pin, + cv.Optional(CONF_RAW, default=False): cv.boolean, cv.SplitDefault(CONF_ATTENUATION, esp32="0db"): cv.All( cv.only_on_esp32, cv.enum(ATTENUATION_MODES, lower=True) ), } ) - .extend(cv.polling_component_schema("60s")) + .extend(cv.polling_component_schema("60s")), + validate_config, ) @@ -91,5 +164,17 @@ async def to_code(config): pin = await cg.gpio_pin_expression(config[CONF_PIN]) cg.add(var.set_pin(pin)) + if CONF_RAW in config: + cg.add(var.set_output_raw(config[CONF_RAW])) + if CONF_ATTENUATION in config: - cg.add(var.set_attenuation(config[CONF_ATTENUATION])) + if config[CONF_ATTENUATION] == "auto": + cg.add(var.set_autorange(cg.global_ns.true)) + else: + cg.add(var.set_attenuation(config[CONF_ATTENUATION])) + + if CORE.is_esp32: + variant = get_esp32_variant() + pin_num = config[CONF_PIN][CONF_NUMBER] + chan = ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant][pin_num] + cg.add(var.set_channel(chan)) diff --git a/esphome/components/aht10/aht10.cpp b/esphome/components/aht10/aht10.cpp index 713199212c..e5e04ac181 100644 --- a/esphome/components/aht10/aht10.cpp +++ b/esphome/components/aht10/aht10.cpp @@ -73,7 +73,7 @@ void AHT10Component::update() { bool success = false; for (int i = 0; i < AHT10_ATTEMPTS; ++i) { ESP_LOGVV(TAG, "Attempt %d at %6u", i, millis()); - delay_microseconds_accurate(4); + delayMicroseconds(4); uint8_t reg = 0; if (this->write(®, 1) != i2c::ERROR_OK) { diff --git a/esphome/components/am43/sensor.py b/esphome/components/am43/sensor.py index c88e529a0c..68c85d0e9c 100644 --- a/esphome/components/am43/sensor.py +++ b/esphome/components/am43/sensor.py @@ -4,7 +4,8 @@ from esphome.components import sensor, ble_client from esphome.const import ( CONF_ID, CONF_BATTERY_LEVEL, - ICON_BATTERY, + DEVICE_CLASS_BATTERY, + ENTITY_CATEGORY_DIAGNOSTIC, CONF_ILLUMINANCE, ICON_BRIGHTNESS_5, UNIT_PERCENT, @@ -20,10 +21,15 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(Am43), cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( - UNIT_PERCENT, ICON_BATTERY, 0 + unit_of_measurement=UNIT_PERCENT, + device_class=DEVICE_CLASS_BATTERY, + accuracy_decimals=0, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema( - UNIT_PERCENT, ICON_BRIGHTNESS_5, 0 + unit_of_measurement=UNIT_PERCENT, + icon=ICON_BRIGHTNESS_5, + accuracy_decimals=0, ), } ) diff --git a/esphome/components/anova/anova_base.cpp b/esphome/components/anova/anova_base.cpp index 811a34a27a..d55404089e 100644 --- a/esphome/components/anova/anova_base.cpp +++ b/esphome/components/anova/anova_base.cpp @@ -103,21 +103,21 @@ void AnovaCodec::decode(const uint8_t *data, uint16_t length) { break; } case READ_TARGET_TEMPERATURE: { - this->target_temp_ = strtof(this->buf_, nullptr); + this->target_temp_ = parse_number(this->buf_, sizeof(this->buf_)).value_or(0.0f); if (this->fahrenheit_) this->target_temp_ = ftoc(this->target_temp_); this->has_target_temp_ = true; break; } case SET_TARGET_TEMPERATURE: { - this->target_temp_ = strtof(this->buf_, nullptr); + this->target_temp_ = parse_number(this->buf_, sizeof(this->buf_)).value_or(0.0f); if (this->fahrenheit_) this->target_temp_ = ftoc(this->target_temp_); this->has_target_temp_ = true; break; } case READ_CURRENT_TEMPERATURE: { - this->current_temp_ = strtof(this->buf_, nullptr); + this->current_temp_ = parse_number(this->buf_, sizeof(this->buf_)).value_or(0.0f); if (this->fahrenheit_) this->current_temp_ = ftoc(this->current_temp_); this->has_current_temp_ = true; diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 5a6eba004c..0eb7ead735 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -182,6 +182,8 @@ message DeviceInfoResponse { // The esphome project details if set string project_name = 8; string project_version = 9; + + uint32 webserver_port = 10; } message ListEntitiesRequest { @@ -201,6 +203,14 @@ message SubscribeStatesRequest { // Empty } +// ==================== COMMON ===================== + +enum EntityCategory { + ENTITY_CATEGORY_NONE = 0; + ENTITY_CATEGORY_CONFIG = 1; + ENTITY_CATEGORY_DIAGNOSTIC = 2; +} + // ==================== BINARY SENSOR ==================== message ListEntitiesBinarySensorResponse { option (id) = 12; @@ -216,6 +226,7 @@ message ListEntitiesBinarySensorResponse { bool is_status_binary_sensor = 6; bool disabled_by_default = 7; string icon = 8; + EntityCategory entity_category = 9; } message BinarySensorStateResponse { option (id) = 21; @@ -247,6 +258,7 @@ message ListEntitiesCoverResponse { string device_class = 8; bool disabled_by_default = 9; string icon = 10; + EntityCategory entity_category = 11; } enum LegacyCoverState { @@ -316,6 +328,7 @@ message ListEntitiesFanResponse { int32 supported_speed_count = 8; bool disabled_by_default = 9; string icon = 10; + EntityCategory entity_category = 11; } enum FanSpeed { FAN_SPEED_LOW = 0; @@ -392,6 +405,7 @@ message ListEntitiesLightResponse { repeated string effects = 11; bool disabled_by_default = 13; string icon = 14; + EntityCategory entity_category = 15; } message LightStateResponse { option (id) = 24; @@ -480,6 +494,7 @@ message ListEntitiesSensorResponse { // Last reset type removed in 2021.9.0 SensorLastResetType legacy_last_reset_type = 11; bool disabled_by_default = 12; + EntityCategory entity_category = 13; } message SensorStateResponse { option (id) = 25; @@ -508,6 +523,7 @@ message ListEntitiesSwitchResponse { string icon = 5; bool assumed_state = 6; bool disabled_by_default = 7; + EntityCategory entity_category = 8; } message SwitchStateResponse { option (id) = 26; @@ -541,6 +557,7 @@ message ListEntitiesTextSensorResponse { string icon = 5; bool disabled_by_default = 6; + EntityCategory entity_category = 7; } message TextSensorStateResponse { option (id) = 27; @@ -701,6 +718,8 @@ message ListEntitiesCameraResponse { string name = 3; string unique_id = 4; bool disabled_by_default = 5; + string icon = 6; + EntityCategory entity_category = 7; } message CameraImageResponse { @@ -795,6 +814,7 @@ message ListEntitiesClimateResponse { repeated string supported_custom_presets = 17; bool disabled_by_default = 18; string icon = 19; + EntityCategory entity_category = 20; } message ClimateStateResponse { option (id) = 47; @@ -863,6 +883,7 @@ message ListEntitiesNumberResponse { float max_value = 7; float step = 8; bool disabled_by_default = 9; + EntityCategory entity_category = 10; } message NumberStateResponse { option (id) = 50; @@ -900,6 +921,7 @@ message ListEntitiesSelectResponse { string icon = 5; repeated string options = 6; bool disabled_by_default = 7; + EntityCategory entity_category = 8; } message SelectStateResponse { option (id) = 53; diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index c87ccf4dc0..715a4f48c1 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -184,6 +184,7 @@ bool APIConnection::send_binary_sensor_info(binary_sensor::BinarySensor *binary_ msg.is_status_binary_sensor = binary_sensor->is_status_binary_sensor(); msg.disabled_by_default = binary_sensor->is_disabled_by_default(); msg.icon = binary_sensor->get_icon(); + msg.entity_category = static_cast(binary_sensor->get_entity_category()); return this->send_list_entities_binary_sensor_response(msg); } #endif @@ -217,6 +218,7 @@ bool APIConnection::send_cover_info(cover::Cover *cover) { msg.device_class = cover->get_device_class(); msg.disabled_by_default = cover->is_disabled_by_default(); msg.icon = cover->get_icon(); + msg.entity_category = static_cast(cover->get_entity_category()); return this->send_list_entities_cover_response(msg); } void APIConnection::cover_command(const CoverCommandRequest &msg) { @@ -283,6 +285,7 @@ bool APIConnection::send_fan_info(fan::FanState *fan) { msg.supported_speed_count = traits.supported_speed_count(); msg.disabled_by_default = fan->is_disabled_by_default(); msg.icon = fan->get_icon(); + msg.entity_category = static_cast(fan->get_entity_category()); return this->send_list_entities_fan_response(msg); } void APIConnection::fan_command(const FanCommandRequest &msg) { @@ -346,6 +349,7 @@ bool APIConnection::send_light_info(light::LightState *light) { msg.disabled_by_default = light->is_disabled_by_default(); msg.icon = light->get_icon(); + msg.entity_category = static_cast(light->get_entity_category()); for (auto mode : traits.get_supported_color_modes()) msg.supported_color_modes.push_back(static_cast(mode)); @@ -432,7 +436,7 @@ bool APIConnection::send_sensor_info(sensor::Sensor *sensor) { msg.device_class = sensor->get_device_class(); msg.state_class = static_cast(sensor->get_state_class()); msg.disabled_by_default = sensor->is_disabled_by_default(); - + msg.entity_category = static_cast(sensor->get_entity_category()); return this->send_list_entities_sensor_response(msg); } #endif @@ -456,6 +460,7 @@ bool APIConnection::send_switch_info(switch_::Switch *a_switch) { msg.icon = a_switch->get_icon(); msg.assumed_state = a_switch->assumed_state(); msg.disabled_by_default = a_switch->is_disabled_by_default(); + msg.entity_category = static_cast(a_switch->get_entity_category()); return this->send_list_entities_switch_response(msg); } void APIConnection::switch_command(const SwitchCommandRequest &msg) { @@ -491,6 +496,7 @@ bool APIConnection::send_text_sensor_info(text_sensor::TextSensor *text_sensor) msg.unique_id = get_default_unique_id("text_sensor", text_sensor); msg.icon = text_sensor->get_icon(); msg.disabled_by_default = text_sensor->is_disabled_by_default(); + msg.entity_category = static_cast(text_sensor->get_entity_category()); return this->send_list_entities_text_sensor_response(msg); } #endif @@ -537,6 +543,7 @@ bool APIConnection::send_climate_info(climate::Climate *climate) { msg.disabled_by_default = climate->is_disabled_by_default(); msg.icon = climate->get_icon(); + msg.entity_category = static_cast(climate->get_entity_category()); msg.supports_current_temperature = traits.get_supports_current_temperature(); msg.supports_two_point_target_temperature = traits.get_supports_two_point_target_temperature(); @@ -611,6 +618,7 @@ bool APIConnection::send_number_info(number::Number *number) { msg.unique_id = get_default_unique_id("number", number); msg.icon = number->get_icon(); msg.disabled_by_default = number->is_disabled_by_default(); + msg.entity_category = static_cast(number->get_entity_category()); msg.min_value = number->traits.get_min_value(); msg.max_value = number->traits.get_max_value(); @@ -648,6 +656,7 @@ bool APIConnection::send_select_info(select::Select *select) { msg.unique_id = get_default_unique_id("select", select); msg.icon = select->get_icon(); msg.disabled_by_default = select->is_disabled_by_default(); + msg.entity_category = static_cast(select->get_entity_category()); for (const auto &option : select->traits.get_options()) msg.options.push_back(option); @@ -680,6 +689,8 @@ bool APIConnection::send_camera_info(esp32_camera::ESP32Camera *camera) { msg.name = camera->get_name(); msg.unique_id = get_default_unique_id("camera", camera); msg.disabled_by_default = camera->is_disabled_by_default(); + msg.icon = camera->get_icon(); + msg.entity_category = static_cast(camera->get_entity_category()); return this->send_list_entities_camera_response(msg); } void APIConnection::camera_image(const CameraImageRequest &msg) { @@ -758,6 +769,9 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { #ifdef ESPHOME_PROJECT_NAME resp.project_name = ESPHOME_PROJECT_NAME; resp.project_version = ESPHOME_PROJECT_VERSION; +#endif +#ifdef USE_WEBSERVER + resp.webserver_port = WEBSERVER_PORT; #endif return resp; } diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 6a87238186..7bfa1e9edb 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -6,6 +6,18 @@ namespace esphome { namespace api { +template<> const char *proto_enum_to_string(enums::EntityCategory value) { + switch (value) { + case enums::ENTITY_CATEGORY_NONE: + return "ENTITY_CATEGORY_NONE"; + case enums::ENTITY_CATEGORY_CONFIG: + return "ENTITY_CATEGORY_CONFIG"; + case enums::ENTITY_CATEGORY_DIAGNOSTIC: + return "ENTITY_CATEGORY_DIAGNOSTIC"; + default: + return "UNKNOWN"; + } +} template<> const char *proto_enum_to_string(enums::LegacyCoverState value) { switch (value) { case enums::LEGACY_COVER_STATE_OPEN: @@ -396,6 +408,10 @@ bool DeviceInfoResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { this->has_deep_sleep = value.as_bool(); return true; } + case 10: { + this->webserver_port = value.as_uint32(); + return true; + } default: return false; } @@ -444,6 +460,7 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(7, this->has_deep_sleep); buffer.encode_string(8, this->project_name); buffer.encode_string(9, this->project_version); + buffer.encode_uint32(10, this->webserver_port); } #ifdef HAS_PROTO_MESSAGE_DUMP void DeviceInfoResponse::dump_to(std::string &out) const { @@ -484,6 +501,11 @@ void DeviceInfoResponse::dump_to(std::string &out) const { out.append(" project_version: "); out.append("'").append(this->project_version).append("'"); out.append("\n"); + + out.append(" webserver_port: "); + sprintf(buffer, "%u", this->webserver_port); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif @@ -509,6 +531,10 @@ bool ListEntitiesBinarySensorResponse::decode_varint(uint32_t field_id, ProtoVar this->disabled_by_default = value.as_bool(); return true; } + case 9: { + this->entity_category = value.as_enum(); + return true; + } default: return false; } @@ -558,6 +584,7 @@ void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(6, this->is_status_binary_sensor); buffer.encode_bool(7, this->disabled_by_default); buffer.encode_string(8, this->icon); + buffer.encode_enum(9, this->entity_category); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesBinarySensorResponse::dump_to(std::string &out) const { @@ -595,6 +622,10 @@ void ListEntitiesBinarySensorResponse::dump_to(std::string &out) const { out.append(" icon: "); out.append("'").append(this->icon).append("'"); out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); out.append("}"); } #endif @@ -664,6 +695,10 @@ bool ListEntitiesCoverResponse::decode_varint(uint32_t field_id, ProtoVarInt val this->disabled_by_default = value.as_bool(); return true; } + case 11: { + this->entity_category = value.as_enum(); + return true; + } default: return false; } @@ -715,6 +750,7 @@ void ListEntitiesCoverResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(8, this->device_class); buffer.encode_bool(9, this->disabled_by_default); buffer.encode_string(10, this->icon); + buffer.encode_enum(11, this->entity_category); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesCoverResponse::dump_to(std::string &out) const { @@ -760,6 +796,10 @@ void ListEntitiesCoverResponse::dump_to(std::string &out) const { out.append(" icon: "); out.append("'").append(this->icon).append("'"); out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); out.append("}"); } #endif @@ -948,6 +988,10 @@ bool ListEntitiesFanResponse::decode_varint(uint32_t field_id, ProtoVarInt value this->disabled_by_default = value.as_bool(); return true; } + case 11: { + this->entity_category = value.as_enum(); + return true; + } default: return false; } @@ -995,6 +1039,7 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_int32(8, this->supported_speed_count); buffer.encode_bool(9, this->disabled_by_default); buffer.encode_string(10, this->icon); + buffer.encode_enum(11, this->entity_category); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesFanResponse::dump_to(std::string &out) const { @@ -1041,6 +1086,10 @@ void ListEntitiesFanResponse::dump_to(std::string &out) const { out.append(" icon: "); out.append("'").append(this->icon).append("'"); out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); out.append("}"); } #endif @@ -1267,6 +1316,10 @@ bool ListEntitiesLightResponse::decode_varint(uint32_t field_id, ProtoVarInt val this->disabled_by_default = value.as_bool(); return true; } + case 15: { + this->entity_category = value.as_enum(); + return true; + } default: return false; } @@ -1334,6 +1387,7 @@ void ListEntitiesLightResponse::encode(ProtoWriteBuffer buffer) const { } buffer.encode_bool(13, this->disabled_by_default); buffer.encode_string(14, this->icon); + buffer.encode_enum(15, this->entity_category); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesLightResponse::dump_to(std::string &out) const { @@ -1401,6 +1455,10 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const { out.append(" icon: "); out.append("'").append(this->icon).append("'"); out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); out.append("}"); } #endif @@ -1860,6 +1918,10 @@ bool ListEntitiesSensorResponse::decode_varint(uint32_t field_id, ProtoVarInt va this->disabled_by_default = value.as_bool(); return true; } + case 13: { + this->entity_category = value.as_enum(); + return true; + } default: return false; } @@ -1917,6 +1979,7 @@ void ListEntitiesSensorResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_enum(10, this->state_class); buffer.encode_enum(11, this->legacy_last_reset_type); buffer.encode_bool(12, this->disabled_by_default); + buffer.encode_enum(13, this->entity_category); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesSensorResponse::dump_to(std::string &out) const { @@ -1971,6 +2034,10 @@ void ListEntitiesSensorResponse::dump_to(std::string &out) const { out.append(" disabled_by_default: "); out.append(YESNO(this->disabled_by_default)); out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); out.append("}"); } #endif @@ -2033,6 +2100,10 @@ bool ListEntitiesSwitchResponse::decode_varint(uint32_t field_id, ProtoVarInt va this->disabled_by_default = value.as_bool(); return true; } + case 8: { + this->entity_category = value.as_enum(); + return true; + } default: return false; } @@ -2077,6 +2148,7 @@ void ListEntitiesSwitchResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(5, this->icon); buffer.encode_bool(6, this->assumed_state); buffer.encode_bool(7, this->disabled_by_default); + buffer.encode_enum(8, this->entity_category); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesSwitchResponse::dump_to(std::string &out) const { @@ -2110,6 +2182,10 @@ void ListEntitiesSwitchResponse::dump_to(std::string &out) const { out.append(" disabled_by_default: "); out.append(YESNO(this->disabled_by_default)); out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); out.append("}"); } #endif @@ -2197,6 +2273,10 @@ bool ListEntitiesTextSensorResponse::decode_varint(uint32_t field_id, ProtoVarIn this->disabled_by_default = value.as_bool(); return true; } + case 7: { + this->entity_category = value.as_enum(); + return true; + } default: return false; } @@ -2240,6 +2320,7 @@ void ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(4, this->unique_id); buffer.encode_string(5, this->icon); buffer.encode_bool(6, this->disabled_by_default); + buffer.encode_enum(7, this->entity_category); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesTextSensorResponse::dump_to(std::string &out) const { @@ -2269,6 +2350,10 @@ void ListEntitiesTextSensorResponse::dump_to(std::string &out) const { out.append(" disabled_by_default: "); out.append(YESNO(this->disabled_by_default)); out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); out.append("}"); } #endif @@ -2892,6 +2977,10 @@ bool ListEntitiesCameraResponse::decode_varint(uint32_t field_id, ProtoVarInt va this->disabled_by_default = value.as_bool(); return true; } + case 7: { + this->entity_category = value.as_enum(); + return true; + } default: return false; } @@ -2910,6 +2999,10 @@ bool ListEntitiesCameraResponse::decode_length(uint32_t field_id, ProtoLengthDel this->unique_id = value.as_string(); return true; } + case 6: { + this->icon = value.as_string(); + return true; + } default: return false; } @@ -2930,6 +3023,8 @@ void ListEntitiesCameraResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(3, this->name); buffer.encode_string(4, this->unique_id); buffer.encode_bool(5, this->disabled_by_default); + buffer.encode_string(6, this->icon); + buffer.encode_enum(7, this->entity_category); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesCameraResponse::dump_to(std::string &out) const { @@ -2955,6 +3050,14 @@ void ListEntitiesCameraResponse::dump_to(std::string &out) const { out.append(" disabled_by_default: "); out.append(YESNO(this->disabled_by_default)); out.append("\n"); + + out.append(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); out.append("}"); } #endif @@ -3082,6 +3185,10 @@ bool ListEntitiesClimateResponse::decode_varint(uint32_t field_id, ProtoVarInt v this->disabled_by_default = value.as_bool(); return true; } + case 20: { + this->entity_category = value.as_enum(); + return true; + } default: return false; } @@ -3170,6 +3277,7 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { } buffer.encode_bool(18, this->disabled_by_default); buffer.encode_string(19, this->icon); + buffer.encode_enum(20, this->entity_category); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesClimateResponse::dump_to(std::string &out) const { @@ -3266,6 +3374,10 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { out.append(" icon: "); out.append("'").append(this->icon).append("'"); out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); out.append("}"); } #endif @@ -3642,6 +3754,10 @@ bool ListEntitiesNumberResponse::decode_varint(uint32_t field_id, ProtoVarInt va this->disabled_by_default = value.as_bool(); return true; } + case 10: { + this->entity_category = value.as_enum(); + return true; + } default: return false; } @@ -3700,6 +3816,7 @@ void ListEntitiesNumberResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(7, this->max_value); buffer.encode_float(8, this->step); buffer.encode_bool(9, this->disabled_by_default); + buffer.encode_enum(10, this->entity_category); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesNumberResponse::dump_to(std::string &out) const { @@ -3744,6 +3861,10 @@ void ListEntitiesNumberResponse::dump_to(std::string &out) const { out.append(" disabled_by_default: "); out.append(YESNO(this->disabled_by_default)); out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); out.append("}"); } #endif @@ -3836,6 +3957,10 @@ bool ListEntitiesSelectResponse::decode_varint(uint32_t field_id, ProtoVarInt va this->disabled_by_default = value.as_bool(); return true; } + case 8: { + this->entity_category = value.as_enum(); + return true; + } default: return false; } @@ -3886,6 +4011,7 @@ void ListEntitiesSelectResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(6, it, true); } buffer.encode_bool(7, this->disabled_by_default); + buffer.encode_enum(8, this->entity_category); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesSelectResponse::dump_to(std::string &out) const { @@ -3921,6 +4047,10 @@ void ListEntitiesSelectResponse::dump_to(std::string &out) const { out.append(" disabled_by_default: "); out.append(YESNO(this->disabled_by_default)); out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); out.append("}"); } #endif diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 13a21c4772..ec11732c7d 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -9,6 +9,11 @@ namespace api { namespace enums { +enum EntityCategory : uint32_t { + ENTITY_CATEGORY_NONE = 0, + ENTITY_CATEGORY_CONFIG = 1, + ENTITY_CATEGORY_DIAGNOSTIC = 2, +}; enum LegacyCoverState : uint32_t { LEGACY_COVER_STATE_OPEN = 0, LEGACY_COVER_STATE_CLOSED = 1, @@ -224,6 +229,7 @@ class DeviceInfoResponse : public ProtoMessage { bool has_deep_sleep{false}; std::string project_name{}; std::string project_version{}; + uint32_t webserver_port{0}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -270,6 +276,7 @@ class ListEntitiesBinarySensorResponse : public ProtoMessage { bool is_status_binary_sensor{false}; bool disabled_by_default{false}; std::string icon{}; + enums::EntityCategory entity_category{}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -306,6 +313,7 @@ class ListEntitiesCoverResponse : public ProtoMessage { std::string device_class{}; bool disabled_by_default{false}; std::string icon{}; + enums::EntityCategory entity_category{}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -363,6 +371,7 @@ class ListEntitiesFanResponse : public ProtoMessage { int32_t supported_speed_count{0}; bool disabled_by_default{false}; std::string icon{}; + enums::EntityCategory entity_category{}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -428,6 +437,7 @@ class ListEntitiesLightResponse : public ProtoMessage { std::vector effects{}; bool disabled_by_default{false}; std::string icon{}; + enums::EntityCategory entity_category{}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -516,6 +526,7 @@ class ListEntitiesSensorResponse : public ProtoMessage { enums::SensorStateClass state_class{}; enums::SensorLastResetType legacy_last_reset_type{}; bool disabled_by_default{false}; + enums::EntityCategory entity_category{}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -549,6 +560,7 @@ class ListEntitiesSwitchResponse : public ProtoMessage { std::string icon{}; bool assumed_state{false}; bool disabled_by_default{false}; + enums::EntityCategory entity_category{}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -593,6 +605,7 @@ class ListEntitiesTextSensorResponse : public ProtoMessage { std::string unique_id{}; std::string icon{}; bool disabled_by_default{false}; + enums::EntityCategory entity_category{}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -803,6 +816,8 @@ class ListEntitiesCameraResponse : public ProtoMessage { std::string name{}; std::string unique_id{}; bool disabled_by_default{false}; + std::string icon{}; + enums::EntityCategory entity_category{}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -861,6 +876,7 @@ class ListEntitiesClimateResponse : public ProtoMessage { std::vector supported_custom_presets{}; bool disabled_by_default{false}; std::string icon{}; + enums::EntityCategory entity_category{}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -940,6 +956,7 @@ class ListEntitiesNumberResponse : public ProtoMessage { float max_value{0.0f}; float step{0.0f}; bool disabled_by_default{false}; + enums::EntityCategory entity_category{}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -985,6 +1002,7 @@ class ListEntitiesSelectResponse : public ProtoMessage { std::string icon{}; std::vector options{}; bool disabled_by_default{false}; + enums::EntityCategory entity_category{}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 4e2899d94f..25081a809a 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -77,7 +77,7 @@ void APIServer::setup() { this->last_connected_ = millis(); #ifdef USE_ESP32_CAMERA - if (esp32_camera::global_esp32_camera != nullptr) { + if (esp32_camera::global_esp32_camera != nullptr && !esp32_camera::global_esp32_camera->is_internal()) { esp32_camera::global_esp32_camera->add_image_callback( [this](const std::shared_ptr &image) { for (auto &c : this->clients_) diff --git a/esphome/components/api/client.py b/esphome/components/api/client.py index 4a3944d33e..b2920f239b 100644 --- a/esphome/components/api/client.py +++ b/esphome/components/api/client.py @@ -23,7 +23,6 @@ async def async_run_logs(config, address): _LOGGER.info("Starting log output from %s using esphome API", address) zc = zeroconf.Zeroconf() cli = APIClient( - asyncio.get_event_loop(), address, port, password, diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h index 8a72765195..26269bcae4 100644 --- a/esphome/components/api/homeassistant_service.h +++ b/esphome/components/api/homeassistant_service.h @@ -8,6 +8,18 @@ namespace esphome { namespace api { +template class TemplatableStringValue : public TemplatableValue { + public: + TemplatableStringValue() : TemplatableValue() {} + + template::value, int> = 0> + TemplatableStringValue(F value) : TemplatableValue(value) {} + + template::value, int> = 0> + TemplatableStringValue(F f) + : TemplatableValue([f](X... x) -> std::string { return to_string(f(x...)); }) {} +}; + template class TemplatableKeyValuePair { public: template TemplatableKeyValuePair(std::string key, T value) : key(std::move(key)), value(value) {} @@ -19,7 +31,8 @@ template class HomeAssistantServiceCallAction : public Action void set_service(T service) { this->service_ = service; } + template void add_data(std::string key, T value) { this->data_.push_back(TemplatableKeyValuePair(key, value)); } @@ -58,6 +71,7 @@ template class HomeAssistantServiceCallAction : public Action service_{}; std::vector> data_; std::vector> data_template_; std::vector> variables_; diff --git a/esphome/components/atc_mithermometer/sensor.py b/esphome/components/atc_mithermometer/sensor.py index 0f6cc1abcb..bde83c28b6 100644 --- a/esphome/components/atc_mithermometer/sensor.py +++ b/esphome/components/atc_mithermometer/sensor.py @@ -12,6 +12,7 @@ from esphome.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, + ENTITY_CATEGORY_DIAGNOSTIC, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_PERCENT, @@ -49,12 +50,14 @@ CONFIG_SCHEMA = ( accuracy_decimals=0, device_class=DEVICE_CLASS_BATTERY, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), cv.Optional(CONF_BATTERY_VOLTAGE): sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, accuracy_decimals=3, device_class=DEVICE_CLASS_VOLTAGE, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), } ) diff --git a/esphome/components/atm90e32/sensor.py b/esphome/components/atm90e32/sensor.py index 05e5250d89..9c876bb62c 100644 --- a/esphome/components/atm90e32/sensor.py +++ b/esphome/components/atm90e32/sensor.py @@ -17,6 +17,7 @@ from esphome.const import ( DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, + ENTITY_CATEGORY_DIAGNOSTIC, ICON_LIGHTBULB, ICON_CURRENT_AC, STATE_CLASS_MEASUREMENT, @@ -125,6 +126,7 @@ CONFIG_SCHEMA = ( accuracy_decimals=1, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), cv.Required(CONF_LINE_FREQUENCY): cv.enum(LINE_FREQS, upper=True), cv.Optional(CONF_CURRENT_PHASES, default="3"): cv.enum( diff --git a/esphome/components/b_parasite/sensor.py b/esphome/components/b_parasite/sensor.py index d51c48c602..201685adc4 100644 --- a/esphome/components/b_parasite/sensor.py +++ b/esphome/components/b_parasite/sensor.py @@ -13,6 +13,7 @@ from esphome.const import ( DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, + ENTITY_CATEGORY_DIAGNOSTIC, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_LUX, @@ -51,6 +52,7 @@ CONFIG_SCHEMA = ( accuracy_decimals=3, device_class=DEVICE_CLASS_VOLTAGE, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), cv.Optional(CONF_MOISTURE): sensor.sensor_schema( unit_of_measurement=UNIT_PERCENT, diff --git a/esphome/components/bh1750/bh1750.cpp b/esphome/components/bh1750/bh1750.cpp index 951fe3670c..4e6bb3c563 100644 --- a/esphome/components/bh1750/bh1750.cpp +++ b/esphome/components/bh1750/bh1750.cpp @@ -79,6 +79,9 @@ void BH1750Sensor::read_data_() { float lx = float(raw_value) / 1.2f; lx *= 69.0f / this->measurement_duration_; + if (this->resolution_ == BH1750_RESOLUTION_0P5_LX) { + lx /= 2.0f; + } ESP_LOGD(TAG, "'%s': Got illuminance=%.1flx", this->get_name().c_str(), lx); this->publish_state(lx); this->status_clear_warning(); diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index ec199cc5fa..3f11e18e45 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -44,10 +44,11 @@ from esphome.const import ( DEVICE_CLASS_POWER, DEVICE_CLASS_PRESENCE, DEVICE_CLASS_PROBLEM, + DEVICE_CLASS_RUNNING, DEVICE_CLASS_SAFETY, DEVICE_CLASS_SMOKE, DEVICE_CLASS_SOUND, - DEVICE_CLASS_UPDATE, + DEVICE_CLASS_TAMPER, DEVICE_CLASS_VIBRATION, DEVICE_CLASS_WINDOW, ) @@ -76,10 +77,11 @@ DEVICE_CLASSES = [ DEVICE_CLASS_POWER, DEVICE_CLASS_PRESENCE, DEVICE_CLASS_PROBLEM, + DEVICE_CLASS_RUNNING, DEVICE_CLASS_SAFETY, DEVICE_CLASS_SMOKE, DEVICE_CLASS_SOUND, - DEVICE_CLASS_UPDATE, + DEVICE_CLASS_TAMPER, DEVICE_CLASS_VIBRATION, DEVICE_CLASS_WINDOW, ] @@ -313,6 +315,7 @@ def validate_multi_click_timing(value): device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") + BINARY_SENSOR_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend( { cv.GenerateID(): cv.declare_id(BinarySensor), diff --git a/esphome/components/ble_client/sensor/__init__.py b/esphome/components/ble_client/sensor/__init__.py index efe4bf0e9a..4aa6a92ba5 100644 --- a/esphome/components/ble_client/sensor/__init__.py +++ b/esphome/components/ble_client/sensor/__init__.py @@ -67,7 +67,7 @@ async def to_code(config): var.set_service_uuid32(esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID])) ) elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid128_format): - uuid128 = esp32_ble_tracker.as_hex_array(config[CONF_SERVICE_UUID]) + uuid128 = esp32_ble_tracker.as_reversed_hex_array(config[CONF_SERVICE_UUID]) cg.add(var.set_service_uuid128(uuid128)) if len(config[CONF_CHARACTERISTIC_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): @@ -87,7 +87,9 @@ async def to_code(config): elif len(config[CONF_CHARACTERISTIC_UUID]) == len( esp32_ble_tracker.bt_uuid128_format ): - uuid128 = esp32_ble_tracker.as_hex_array(config[CONF_CHARACTERISTIC_UUID]) + uuid128 = esp32_ble_tracker.as_reversed_hex_array( + config[CONF_CHARACTERISTIC_UUID] + ) cg.add(var.set_char_uuid128(uuid128)) if CONF_DESCRIPTOR_UUID in config: @@ -108,7 +110,9 @@ async def to_code(config): elif len(config[CONF_DESCRIPTOR_UUID]) == len( esp32_ble_tracker.bt_uuid128_format ): - uuid128 = esp32_ble_tracker.as_hex_array(config[CONF_DESCRIPTOR_UUID]) + uuid128 = esp32_ble_tracker.as_reversed_hex_array( + config[CONF_DESCRIPTOR_UUID] + ) cg.add(var.set_descr_uuid128(uuid128)) if CONF_LAMBDA in config: diff --git a/esphome/components/bme680_bsec/__init__.py b/esphome/components/bme680_bsec/__init__.py index 2f844fa666..83e519f8aa 100644 --- a/esphome/components/bme680_bsec/__init__.py +++ b/esphome/components/bme680_bsec/__init__.py @@ -2,7 +2,6 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c from esphome.const import CONF_ID -from esphome.core import CORE CODEOWNERS = ["@trvrnrth"] DEPENDENCIES = ["i2c"] @@ -62,9 +61,8 @@ async def to_code(config): var.set_state_save_interval(config[CONF_STATE_SAVE_INTERVAL].total_milliseconds) ) - if CORE.is_esp32: - # Although this component does not use SPI, the BSEC library requires the SPI library - cg.add_library("SPI", None) + # Although this component does not use SPI, the BSEC library requires the SPI library + cg.add_library("SPI", None) cg.add_define("USE_BSEC") cg.add_library("boschsensortec/BSEC Software Library", "1.6.1480") diff --git a/esphome/components/cap1188/__init__.py b/esphome/components/cap1188/__init__.py new file mode 100644 index 0000000000..80794c5146 --- /dev/null +++ b/esphome/components/cap1188/__init__.py @@ -0,0 +1,45 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c +from esphome.const import CONF_ID, CONF_RESET_PIN +from esphome import pins + +CONF_TOUCH_THRESHOLD = "touch_threshold" +CONF_ALLOW_MULTIPLE_TOUCHES = "allow_multiple_touches" + +DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["binary_sensor", "output"] +CODEOWNERS = ["@MrEditor97"] + +cap1188_ns = cg.esphome_ns.namespace("cap1188") +CONF_CAP1188_ID = "cap1188_id" +CAP1188Component = cap1188_ns.class_("CAP1188Component", cg.Component, i2c.I2CDevice) + +MULTI_CONF = True +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(CAP1188Component), + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_TOUCH_THRESHOLD, default=0x20): cv.int_range( + min=0x01, max=0x80 + ), + cv.Optional(CONF_ALLOW_MULTIPLE_TOUCHES, default=False): cv.boolean, + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x29)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + cg.add(var.set_touch_threshold(config[CONF_TOUCH_THRESHOLD])) + cg.add(var.set_allow_multiple_touches(config[CONF_ALLOW_MULTIPLE_TOUCHES])) + + if CONF_RESET_PIN in config: + pin = await cg.gpio_pin_expression(config[CONF_RESET_PIN]) + cg.add(var.set_reset_pin(pin)) + + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/cap1188/binary_sensor.py b/esphome/components/cap1188/binary_sensor.py new file mode 100644 index 0000000000..c249eb7330 --- /dev/null +++ b/esphome/components/cap1188/binary_sensor.py @@ -0,0 +1,25 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from esphome.const import CONF_CHANNEL, CONF_ID +from . import cap1188_ns, CAP1188Component, CONF_CAP1188_ID + +DEPENDENCIES = ["cap1188"] +CAP1188Channel = cap1188_ns.class_("CAP1188Channel", binary_sensor.BinarySensor) + +CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(CAP1188Channel), + cv.GenerateID(CONF_CAP1188_ID): cv.use_id(CAP1188Component), + cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=7), + } +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await binary_sensor.register_binary_sensor(var, config) + hub = await cg.get_variable(config[CONF_CAP1188_ID]) + cg.add(var.set_channel(config[CONF_CHANNEL])) + + cg.add(hub.register_channel(var)) diff --git a/esphome/components/cap1188/cap1188.cpp b/esphome/components/cap1188/cap1188.cpp new file mode 100644 index 0000000000..10d8325537 --- /dev/null +++ b/esphome/components/cap1188/cap1188.cpp @@ -0,0 +1,88 @@ +#include "cap1188.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace cap1188 { + +static const char *const TAG = "cap1188"; + +void CAP1188Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up CAP1188..."); + + // Reset device using the reset pin + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->reset_pin_->digital_write(false); + delay(100); // NOLINT + this->reset_pin_->digital_write(true); + delay(100); // NOLINT + this->reset_pin_->digital_write(false); + delay(100); // NOLINT + } + + // Check if CAP1188 is actually connected + this->read_byte(CAP1188_PRODUCT_ID, &this->cap1188_product_id_); + this->read_byte(CAP1188_MANUFACTURE_ID, &this->cap1188_manufacture_id_); + this->read_byte(CAP1188_REVISION, &this->cap1188_revision_); + + if ((this->cap1188_product_id_ != 0x50) || (this->cap1188_manufacture_id_ != 0x5D)) { + this->error_code_ = COMMUNICATION_FAILED; + this->mark_failed(); + return; + } + + // Set sensitivity + uint8_t sensitivity = 0; + this->read_byte(CAP1188_SENSITVITY, &sensitivity); + sensitivity = sensitivity & 0x0f; + this->write_byte(CAP1188_SENSITVITY, sensitivity | this->touch_threshold_); + + // Allow multiple touches + this->write_byte(CAP1188_MULTI_TOUCH, this->allow_multiple_touches_); + + // Have LEDs follow touches + this->write_byte(CAP1188_LED_LINK, 0xFF); + + // Speed up a bit + this->write_byte(CAP1188_STAND_BY_CONFIGURATION, 0x30); +} + +void CAP1188Component::dump_config() { + ESP_LOGCONFIG(TAG, "CAP1188:"); + LOG_I2C_DEVICE(this); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + ESP_LOGCONFIG(TAG, " Product ID: 0x%x", this->cap1188_product_id_); + ESP_LOGCONFIG(TAG, " Manufacture ID: 0x%x", this->cap1188_manufacture_id_); + ESP_LOGCONFIG(TAG, " Revision ID: 0x%x", this->cap1188_revision_); + + switch (this->error_code_) { + case COMMUNICATION_FAILED: + ESP_LOGE(TAG, "Product ID or Manufacture ID of the connected device does not match a known CAP1188."); + break; + case NONE: + default: + break; + } +} + +void CAP1188Component::loop() { + uint8_t touched = 0; + + this->read_register(CAP1188_SENSOR_INPUT_STATUS, &touched, 1); + + if (touched) { + uint8_t data = 0; + this->read_register(CAP1188_MAIN, &data, 1); + data = data & ~CAP1188_MAIN_INT; + + this->write_register(CAP1188_MAIN, &data, 2); + } + + for (auto *channel : this->channels_) { + channel->process(touched); + } +} + +} // namespace cap1188 +} // namespace esphome diff --git a/esphome/components/cap1188/cap1188.h b/esphome/components/cap1188/cap1188.h new file mode 100644 index 0000000000..a1433deb0f --- /dev/null +++ b/esphome/components/cap1188/cap1188.h @@ -0,0 +1,68 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/output/binary_output.h" +#include "esphome/components/binary_sensor/binary_sensor.h" + +namespace esphome { +namespace cap1188 { + +enum { + CAP1188_I2CADDR = 0x29, + CAP1188_SENSOR_INPUT_STATUS = 0x3, + CAP1188_MULTI_TOUCH = 0x2A, + CAP1188_LED_LINK = 0x72, + CAP1188_PRODUCT_ID = 0xFD, + CAP1188_MANUFACTURE_ID = 0xFE, + CAP1188_STAND_BY_CONFIGURATION = 0x41, + CAP1188_REVISION = 0xFF, + CAP1188_MAIN = 0x00, + CAP1188_MAIN_INT = 0x01, + CAP1188_LEDPOL = 0x73, + CAP1188_INTERUPT_REPEAT = 0x28, + CAP1188_SENSITVITY = 0x1f, +}; + +class CAP1188Channel : public binary_sensor::BinarySensor { + public: + void set_channel(uint8_t channel) { channel_ = channel; } + void process(uint8_t data) { this->publish_state(static_cast(data & (1 << this->channel_))); } + + protected: + uint8_t channel_{0}; +}; + +class CAP1188Component : public Component, public i2c::I2CDevice { + public: + void register_channel(CAP1188Channel *channel) { this->channels_.push_back(channel); } + void set_touch_threshold(uint8_t touch_threshold) { this->touch_threshold_ = touch_threshold; }; + void set_allow_multiple_touches(bool allow_multiple_touches) { + this->allow_multiple_touches_ = allow_multiple_touches ? 0x41 : 0x80; + }; + void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void loop() override; + + protected: + std::vector channels_{}; + uint8_t touch_threshold_{0x20}; + uint8_t allow_multiple_touches_{0x80}; + + GPIOPin *reset_pin_{nullptr}; + + uint8_t cap1188_product_id_{0}; + uint8_t cap1188_manufacture_id_{0}; + uint8_t cap1188_revision_{0}; + + enum ErrorCode { + NONE = 0, + COMMUNICATION_FAILED, + } error_code_{NONE}; +}; + +} // namespace cap1188 +} // namespace esphome diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index 384a3f23a0..f024c94b01 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -36,3 +36,5 @@ async def to_code(config): if CORE.is_esp32: cg.add_library("DNSServer", None) cg.add_library("WiFi", None) + if CORE.is_esp8266: + cg.add_library("DNSServer", None) diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index 9e00adae3d..ad4c32bb1f 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -67,6 +67,7 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { ESP_LOGI(TAG, " SSID='%s'", ssid.c_str()); ESP_LOGI(TAG, " Password=" LOG_SECRET("'%s'"), psk.c_str()); wifi::global_wifi_component->save_wifi_sta(ssid, psk); + wifi::global_wifi_component->start_scanning(); request->redirect("/?save=true"); } diff --git a/esphome/components/ccs811/ccs811.cpp b/esphome/components/ccs811/ccs811.cpp index 11a66f5100..f8cee79c55 100644 --- a/esphome/components/ccs811/ccs811.cpp +++ b/esphome/components/ccs811/ccs811.cpp @@ -52,7 +52,7 @@ void CCS811Component::setup() { if (this->baseline_.has_value()) { // baseline available, write to sensor - this->write_bytes(0x11, decode_uint16(*this->baseline_)); + this->write_bytes(0x11, decode_value(*this->baseline_)); } auto hardware_version_data = this->read_bytes<1>(0x21); diff --git a/esphome/components/cse7761/__init__.py b/esphome/components/cse7761/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/cse7761/cse7761.cpp b/esphome/components/cse7761/cse7761.cpp new file mode 100644 index 0000000000..3b8364f0bc --- /dev/null +++ b/esphome/components/cse7761/cse7761.cpp @@ -0,0 +1,244 @@ +#include "cse7761.h" + +#include "esphome/core/log.h" + +namespace esphome { +namespace cse7761 { + +static const char *const TAG = "cse7761"; + +/*********************************************************************************************\ + * CSE7761 - Energy (Sonoff Dual R3 Pow v1.x) + * + * Based on Tasmota source code + * See https://github.com/arendst/Tasmota/discussions/10793 + * https://github.com/arendst/Tasmota/blob/development/tasmota/xnrg_19_cse7761.ino +\*********************************************************************************************/ + +static const int CSE7761_UREF = 42563; // RmsUc +static const int CSE7761_IREF = 52241; // RmsIAC +static const int CSE7761_PREF = 44513; // PowerPAC + +static const uint8_t CSE7761_REG_SYSCON = 0x00; // (2) System Control Register (0x0A04) +static const uint8_t CSE7761_REG_EMUCON = 0x01; // (2) Metering control register (0x0000) +static const uint8_t CSE7761_REG_EMUCON2 = 0x13; // (2) Metering control register 2 (0x0001) +static const uint8_t CSE7761_REG_PULSE1SEL = 0x1D; // (2) Pin function output select register (0x3210) + +static const uint8_t CSE7761_REG_RMSIA = 0x24; // (3) The effective value of channel A current (0x000000) +static const uint8_t CSE7761_REG_RMSIB = 0x25; // (3) The effective value of channel B current (0x000000) +static const uint8_t CSE7761_REG_RMSU = 0x26; // (3) Voltage RMS (0x000000) +static const uint8_t CSE7761_REG_POWERPA = 0x2C; // (4) Channel A active power, update rate 27.2Hz (0x00000000) +static const uint8_t CSE7761_REG_POWERPB = 0x2D; // (4) Channel B active power, update rate 27.2Hz (0x00000000) +static const uint8_t CSE7761_REG_SYSSTATUS = 0x43; // (1) System status register + +static const uint8_t CSE7761_REG_COEFFCHKSUM = 0x6F; // (2) Coefficient checksum +static const uint8_t CSE7761_REG_RMSIAC = 0x70; // (2) Channel A effective current conversion coefficient + +static const uint8_t CSE7761_SPECIAL_COMMAND = 0xEA; // Start special command +static const uint8_t CSE7761_CMD_RESET = 0x96; // Reset command, after receiving the command, the chip resets +static const uint8_t CSE7761_CMD_CLOSE_WRITE = 0xDC; // Close write operation +static const uint8_t CSE7761_CMD_ENABLE_WRITE = 0xE5; // Enable write operation + +enum CSE7761 { RMS_IAC, RMS_IBC, RMS_UC, POWER_PAC, POWER_PBC, POWER_SC, ENERGY_AC, ENERGY_BC }; + +void CSE7761Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up CSE7761..."); + this->write_(CSE7761_SPECIAL_COMMAND, CSE7761_CMD_RESET); + uint16_t syscon = this->read_(0x00, 2); // Default 0x0A04 + if ((0x0A04 == syscon) && this->chip_init_()) { + this->write_(CSE7761_SPECIAL_COMMAND, CSE7761_CMD_CLOSE_WRITE); + ESP_LOGD(TAG, "CSE7761 found"); + this->data_.ready = true; + } else { + this->mark_failed(); + } +} + +void CSE7761Component::dump_config() { + ESP_LOGCONFIG(TAG, "CSE7761:"); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with CSE7761 failed!"); + } + LOG_UPDATE_INTERVAL(this); + this->check_uart_settings(38400, 1, uart::UART_CONFIG_PARITY_EVEN, 8); +} + +float CSE7761Component::get_setup_priority() const { return setup_priority::DATA; } + +void CSE7761Component::update() { + if (this->data_.ready) { + this->get_data_(); + } +} + +void CSE7761Component::write_(uint8_t reg, uint16_t data) { + uint8_t buffer[5]; + + buffer[0] = 0xA5; + buffer[1] = reg; + uint32_t len = 2; + if (data) { + if (data < 0xFF) { + buffer[2] = data & 0xFF; + len = 3; + } else { + buffer[2] = (data >> 8) & 0xFF; + buffer[3] = data & 0xFF; + len = 4; + } + uint8_t crc = 0; + for (uint32_t i = 0; i < len; i++) { + crc += buffer[i]; + } + buffer[len] = ~crc; + len++; + } + + this->write_array(buffer, len); +} + +bool CSE7761Component::read_once_(uint8_t reg, uint8_t size, uint32_t *value) { + while (this->available()) { + this->read(); + } + + this->write_(reg, 0); + + uint8_t buffer[8] = {0}; + uint32_t rcvd = 0; + + for (uint32_t i = 0; i <= size; i++) { + int value = this->read(); + if (value > -1 && rcvd < sizeof(buffer) - 1) { + buffer[rcvd++] = value; + } + } + + if (!rcvd) { + ESP_LOGD(TAG, "Received 0 bytes for register %hhu", reg); + return false; + } + + rcvd--; + uint32_t result = 0; + // CRC check + uint8_t crc = 0xA5 + reg; + for (uint32_t i = 0; i < rcvd; i++) { + result = (result << 8) | buffer[i]; + crc += buffer[i]; + } + crc = ~crc; + if (crc != buffer[rcvd]) { + return false; + } + + *value = result; + return true; +} + +uint32_t CSE7761Component::read_(uint8_t reg, uint8_t size) { + bool result = false; // Start loop + uint8_t retry = 3; // Retry up to three times + uint32_t value = 0; // Default no value + while (!result && retry > 0) { + retry--; + if (this->read_once_(reg, size, &value)) + return value; + } + ESP_LOGE(TAG, "Reading register %hhu failed!", reg); + return value; +} + +uint32_t CSE7761Component::coefficient_by_unit_(uint32_t unit) { + switch (unit) { + case RMS_UC: + return 0x400000 * 100 / this->data_.coefficient[RMS_UC]; + case RMS_IAC: + return (0x800000 * 100 / this->data_.coefficient[RMS_IAC]) * 10; // Stay within 32 bits + case POWER_PAC: + return 0x80000000 / this->data_.coefficient[POWER_PAC]; + } + return 0; +} + +bool CSE7761Component::chip_init_() { + uint16_t calc_chksum = 0xFFFF; + for (uint32_t i = 0; i < 8; i++) { + this->data_.coefficient[i] = this->read_(CSE7761_REG_RMSIAC + i, 2); + calc_chksum += this->data_.coefficient[i]; + } + calc_chksum = ~calc_chksum; + uint16_t coeff_chksum = this->read_(CSE7761_REG_COEFFCHKSUM, 2); + if ((calc_chksum != coeff_chksum) || (!calc_chksum)) { + ESP_LOGD(TAG, "Default calibration"); + this->data_.coefficient[RMS_IAC] = CSE7761_IREF; + this->data_.coefficient[RMS_UC] = CSE7761_UREF; + this->data_.coefficient[POWER_PAC] = CSE7761_PREF; + } + + this->write_(CSE7761_SPECIAL_COMMAND, CSE7761_CMD_ENABLE_WRITE); + + uint8_t sys_status = this->read_(CSE7761_REG_SYSSTATUS, 1); + if (sys_status & 0x10) { // Write enable to protected registers (WREN) + this->write_(CSE7761_REG_SYSCON | 0x80, 0xFF04); + this->write_(CSE7761_REG_EMUCON | 0x80, 0x1183); + this->write_(CSE7761_REG_EMUCON2 | 0x80, 0x0FC1); + this->write_(CSE7761_REG_PULSE1SEL | 0x80, 0x3290); + } else { + ESP_LOGD(TAG, "Write failed at chip_init"); + return false; + } + return true; +} + +void CSE7761Component::get_data_() { + // The effective value of current and voltage Rms is a 24-bit signed number, + // the highest bit is 0 for valid data, + // and when the highest bit is 1, the reading will be processed as zero + // The active power parameter PowerA/B is in two’s complement format, 32-bit + // data, the highest bit is Sign bit. + uint32_t value = this->read_(CSE7761_REG_RMSU, 3); + this->data_.voltage_rms = (value >= 0x800000) ? 0 : value; + + value = this->read_(CSE7761_REG_RMSIA, 3); + this->data_.current_rms[0] = ((value >= 0x800000) || (value < 1600)) ? 0 : value; // No load threshold of 10mA + value = this->read_(CSE7761_REG_POWERPA, 4); + this->data_.active_power[0] = (0 == this->data_.current_rms[0]) ? 0 : ((uint32_t) abs((int) value)); + + value = this->read_(CSE7761_REG_RMSIB, 3); + this->data_.current_rms[1] = ((value >= 0x800000) || (value < 1600)) ? 0 : value; // No load threshold of 10mA + value = this->read_(CSE7761_REG_POWERPB, 4); + this->data_.active_power[1] = (0 == this->data_.current_rms[1]) ? 0 : ((uint32_t) abs((int) value)); + + // convert values and publish to sensors + + float voltage = (float) this->data_.voltage_rms / this->coefficient_by_unit_(RMS_UC); + if (this->voltage_sensor_ != nullptr) { + this->voltage_sensor_->publish_state(voltage); + } + + for (uint32_t channel = 0; channel < 2; channel++) { + // Active power = PowerPA * PowerPAC * 1000 / 0x80000000 + float active_power = (float) this->data_.active_power[channel] / this->coefficient_by_unit_(POWER_PAC); // W + float amps = (float) this->data_.current_rms[channel] / this->coefficient_by_unit_(RMS_IAC); // A + ESP_LOGD(TAG, "Channel %d power %f W, current %f A", channel + 1, active_power, amps); + if (channel == 0) { + if (this->power_sensor_1_ != nullptr) { + this->power_sensor_1_->publish_state(active_power); + } + if (this->current_sensor_1_ != nullptr) { + this->current_sensor_1_->publish_state(amps); + } + } else if (channel == 1) { + if (this->power_sensor_2_ != nullptr) { + this->power_sensor_2_->publish_state(active_power); + } + if (this->current_sensor_2_ != nullptr) { + this->current_sensor_2_->publish_state(amps); + } + } + } +} + +} // namespace cse7761 +} // namespace esphome diff --git a/esphome/components/cse7761/cse7761.h b/esphome/components/cse7761/cse7761.h new file mode 100644 index 0000000000..71846cdcab --- /dev/null +++ b/esphome/components/cse7761/cse7761.h @@ -0,0 +1,52 @@ +#pragma once + +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace cse7761 { + +struct CSE7761DataStruct { + uint32_t frequency = 0; + uint32_t voltage_rms = 0; + uint32_t current_rms[2] = {0}; + uint32_t energy[2] = {0}; + uint32_t active_power[2] = {0}; + uint16_t coefficient[8] = {0}; + uint8_t energy_update = 0; + bool ready = false; +}; + +/// This class implements support for the CSE7761 UART power sensor. +class CSE7761Component : public PollingComponent, public uart::UARTDevice { + public: + void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } + void set_active_power_1_sensor(sensor::Sensor *power_sensor_1) { power_sensor_1_ = power_sensor_1; } + void set_current_1_sensor(sensor::Sensor *current_sensor_1) { current_sensor_1_ = current_sensor_1; } + void set_active_power_2_sensor(sensor::Sensor *power_sensor_2) { power_sensor_2_ = power_sensor_2; } + void set_current_2_sensor(sensor::Sensor *current_sensor_2) { current_sensor_2_ = current_sensor_2; } + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void update() override; + + protected: + // Sensors + sensor::Sensor *voltage_sensor_{nullptr}; + sensor::Sensor *power_sensor_1_{nullptr}; + sensor::Sensor *current_sensor_1_{nullptr}; + sensor::Sensor *power_sensor_2_{nullptr}; + sensor::Sensor *current_sensor_2_{nullptr}; + CSE7761DataStruct data_; + + void write_(uint8_t reg, uint16_t data); + bool read_once_(uint8_t reg, uint8_t size, uint32_t *value); + uint32_t read_(uint8_t reg, uint8_t size); + uint32_t coefficient_by_unit_(uint32_t unit); + bool chip_init_(); + void get_data_(); +}; + +} // namespace cse7761 +} // namespace esphome diff --git a/esphome/components/cse7761/sensor.py b/esphome/components/cse7761/sensor.py new file mode 100644 index 0000000000..c5ec3e5b71 --- /dev/null +++ b/esphome/components/cse7761/sensor.py @@ -0,0 +1,90 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, uart +from esphome.const import ( + CONF_ID, + CONF_VOLTAGE, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_POWER, + DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, + UNIT_VOLT, + UNIT_AMPERE, + UNIT_WATT, +) + +CODEOWNERS = ["@berfenger"] +DEPENDENCIES = ["uart"] + +cse7761_ns = cg.esphome_ns.namespace("cse7761") +CSE7761Component = cse7761_ns.class_( + "CSE7761Component", cg.PollingComponent, uart.UARTDevice +) + +CONF_CURRENT_1 = "current_1" +CONF_CURRENT_2 = "current_2" +CONF_ACTIVE_POWER_1 = "active_power_1" +CONF_ACTIVE_POWER_2 = "active_power_2" + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(CSE7761Component), + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CURRENT_1): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CURRENT_2): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_ACTIVE_POWER_1): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_ACTIVE_POWER_2): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(uart.UART_DEVICE_SCHEMA) +) + +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "cse7761", baud_rate=38400, 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) + + for key in [ + CONF_VOLTAGE, + CONF_CURRENT_1, + CONF_CURRENT_2, + CONF_ACTIVE_POWER_1, + CONF_ACTIVE_POWER_2, + ]: + if key not in config: + continue + conf = config[key] + sens = await sensor.new_sensor(conf) + cg.add(getattr(var, f"set_{key}_sensor")(sens)) diff --git a/esphome/components/dallas/__init__.py b/esphome/components/dallas/__init__.py index 2dbc69b8e2..0f71399a7c 100644 --- a/esphome/components/dallas/__init__.py +++ b/esphome/components/dallas/__init__.py @@ -6,26 +6,20 @@ from esphome.const import CONF_ID, CONF_PIN MULTI_CONF = True AUTO_LOAD = ["sensor"] -CONF_ONE_WIRE_ID = "one_wire_id" dallas_ns = cg.esphome_ns.namespace("dallas") DallasComponent = dallas_ns.class_("DallasComponent", cg.PollingComponent) -ESPOneWire = dallas_ns.class_("ESPOneWire") -CONFIG_SCHEMA = cv.All( - cv.Schema( - { - cv.GenerateID(): cv.declare_id(DallasComponent), - cv.GenerateID(CONF_ONE_WIRE_ID): cv.declare_id(ESPOneWire), - cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema, - } - ).extend(cv.polling_component_schema("60s")), - # pin_mode call logs in esp-idf, but InterruptLock is active -> crash - cv.only_with_arduino, -) +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(DallasComponent), + cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema, + } +).extend(cv.polling_component_schema("60s")) async def to_code(config): - pin = await cg.gpio_pin_expression(config[CONF_PIN]) - one_wire = cg.new_Pvariable(config[CONF_ONE_WIRE_ID], pin) - var = cg.new_Pvariable(config[CONF_ID], one_wire) + var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) + + pin = await cg.gpio_pin_expression(config[CONF_PIN]) + cg.add(var.set_pin(pin)) diff --git a/esphome/components/dallas/dallas_component.cpp b/esphome/components/dallas/dallas_component.cpp index 0fc4108687..8d7f2e4deb 100644 --- a/esphome/components/dallas/dallas_component.cpp +++ b/esphome/components/dallas/dallas_component.cpp @@ -31,12 +31,11 @@ uint16_t DallasTemperatureSensor::millis_to_wait_for_conversion() const { void DallasComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up DallasComponent..."); - yield(); + pin_->setup(); + one_wire_ = new ESPOneWire(pin_); // NOLINT(cppcoreguidelines-owning-memory) + std::vector raw_sensors; - { - InterruptLock lock; - raw_sensors = this->one_wire_->search_vec(); - } + raw_sensors = this->one_wire_->search_vec(); for (auto &address : raw_sensors) { std::string s = uint64_to_string(address); @@ -70,7 +69,7 @@ void DallasComponent::setup() { } void DallasComponent::dump_config() { ESP_LOGCONFIG(TAG, "DallasComponent:"); - LOG_PIN(" Pin: ", this->one_wire_->get_pin()); + LOG_PIN(" Pin: ", this->pin_); LOG_UPDATE_INTERVAL(this); if (this->found_sensors_.empty()) { @@ -102,15 +101,12 @@ void DallasComponent::update() { this->status_clear_warning(); bool result; - { - InterruptLock lock; - if (!this->one_wire_->reset()) { - result = false; - } else { - result = true; - this->one_wire_->skip(); - this->one_wire_->write8(DALLAS_COMMAND_START_CONVERSION); - } + if (!this->one_wire_->reset()) { + result = false; + } else { + result = true; + this->one_wire_->skip(); + this->one_wire_->write8(DALLAS_COMMAND_START_CONVERSION); } if (!result) { @@ -121,11 +117,7 @@ void DallasComponent::update() { for (auto *sensor : this->sensors_) { this->set_timeout(sensor->get_address_name(), sensor->millis_to_wait_for_conversion(), [this, sensor] { - bool res; - { - InterruptLock lock; - res = sensor->read_scratch_pad(); - } + bool res = sensor->read_scratch_pad(); if (!res) { ESP_LOGW(TAG, "'%s' - Resetting bus for read failed!", sensor->get_name().c_str()); @@ -146,7 +138,6 @@ void DallasComponent::update() { }); } } -DallasComponent::DallasComponent(ESPOneWire *one_wire) : one_wire_(one_wire) {} void DallasTemperatureSensor::set_address(uint64_t address) { this->address_ = address; } uint8_t DallasTemperatureSensor::get_resolution() const { return this->resolution_; } @@ -162,7 +153,7 @@ const std::string &DallasTemperatureSensor::get_address_name() { return this->address_name_; } bool IRAM_ATTR DallasTemperatureSensor::read_scratch_pad() { - ESPOneWire *wire = this->parent_->one_wire_; + auto *wire = this->parent_->one_wire_; if (!wire->reset()) { return false; } @@ -176,11 +167,7 @@ bool IRAM_ATTR DallasTemperatureSensor::read_scratch_pad() { return true; } bool DallasTemperatureSensor::setup_sensor() { - bool r; - { - InterruptLock lock; - r = this->read_scratch_pad(); - } + bool r = this->read_scratch_pad(); if (!r) { ESP_LOGE(TAG, "Reading scratchpad failed: reset"); @@ -214,21 +201,18 @@ bool DallasTemperatureSensor::setup_sensor() { break; } - ESPOneWire *wire = this->parent_->one_wire_; - { - InterruptLock lock; - if (wire->reset()) { - wire->select(this->address_); - wire->write8(DALLAS_COMMAND_WRITE_SCRATCH_PAD); - wire->write8(this->scratch_pad_[2]); // high alarm temp - wire->write8(this->scratch_pad_[3]); // low alarm temp - wire->write8(this->scratch_pad_[4]); // resolution - wire->reset(); + auto *wire = this->parent_->one_wire_; + if (wire->reset()) { + wire->select(this->address_); + wire->write8(DALLAS_COMMAND_WRITE_SCRATCH_PAD); + wire->write8(this->scratch_pad_[2]); // high alarm temp + wire->write8(this->scratch_pad_[3]); // low alarm temp + wire->write8(this->scratch_pad_[4]); // resolution + wire->reset(); - // write value to EEPROM - wire->select(this->address_); - wire->write8(0x48); - } + // write value to EEPROM + wire->select(this->address_); + wire->write8(0x48); } delay(20); // allow it to finish operation diff --git a/esphome/components/dallas/dallas_component.h b/esphome/components/dallas/dallas_component.h index 8d405f6eab..37c098283a 100644 --- a/esphome/components/dallas/dallas_component.h +++ b/esphome/components/dallas/dallas_component.h @@ -11,8 +11,7 @@ class DallasTemperatureSensor; class DallasComponent : public PollingComponent { public: - explicit DallasComponent(ESPOneWire *one_wire); - + void set_pin(InternalGPIOPin *pin) { pin_ = pin; } void register_sensor(DallasTemperatureSensor *sensor); void setup() override; @@ -24,6 +23,7 @@ class DallasComponent : public PollingComponent { protected: friend DallasTemperatureSensor; + InternalGPIOPin *pin_; ESPOneWire *one_wire_; std::vector sensors_; std::vector found_sensors_; diff --git a/esphome/components/dallas/esp_one_wire.cpp b/esphome/components/dallas/esp_one_wire.cpp index 9278b83f7f..a0ab10f8a4 100644 --- a/esphome/components/dallas/esp_one_wire.cpp +++ b/esphome/components/dallas/esp_one_wire.cpp @@ -10,115 +10,123 @@ static const char *const TAG = "dallas.one_wire"; const uint8_t ONE_WIRE_ROM_SELECT = 0x55; const int ONE_WIRE_ROM_SEARCH = 0xF0; -ESPOneWire::ESPOneWire(GPIOPin *pin) : pin_(pin) {} +ESPOneWire::ESPOneWire(InternalGPIOPin *pin) { pin_ = pin->to_isr(); } bool HOT IRAM_ATTR ESPOneWire::reset() { - uint8_t retries = 125; + // See reset here: + // https://www.maximintegrated.com/en/design/technical-documents/app-notes/1/126.html + InterruptLock lock; - // Wait for communication to clear - this->pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); + // Wait for communication to clear (delay G) + pin_.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); + uint8_t retries = 125; do { if (--retries == 0) return false; delayMicroseconds(2); - } while (!this->pin_->digital_read()); + } while (!pin_.digital_read()); - // Send 480µs LOW TX reset pulse - this->pin_->pin_mode(gpio::FLAG_OUTPUT); - this->pin_->digital_write(false); + // Send 480µs LOW TX reset pulse (drive bus low, delay H) + pin_.pin_mode(gpio::FLAG_OUTPUT); + pin_.digital_write(false); delayMicroseconds(480); - // Switch into RX mode, letting the pin float - this->pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); - // after 15µs-60µs wait time, responder pulls low for 60µs-240µs - // let's have 70µs just in case + // Release the bus, delay I + pin_.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); delayMicroseconds(70); - bool r = !this->pin_->digital_read(); + // sample bus, 0=device(s) present, 1=no device present + bool r = !pin_.digital_read(); + // delay J delayMicroseconds(410); return r; } void HOT IRAM_ATTR ESPOneWire::write_bit(bool bit) { - // Initiate write/read by pulling low. - this->pin_->pin_mode(gpio::FLAG_OUTPUT); - this->pin_->digital_write(false); + // See write 1/0 bit here: + // https://www.maximintegrated.com/en/design/technical-documents/app-notes/1/126.html + InterruptLock lock; - // bus sampled within 15µs and 60µs after pulling LOW. - if (bit) { - // pull high/release within 15µs - delayMicroseconds(10); - this->pin_->digital_write(true); - // in total minimum of 60µs long - delayMicroseconds(55); - } else { - // continue pulling LOW for at least 60µs - delayMicroseconds(65); - this->pin_->digital_write(true); - // grace period, 1µs recovery time - delayMicroseconds(5); - } + // drive bus low + pin_.pin_mode(gpio::FLAG_OUTPUT); + pin_.digital_write(false); + + uint32_t delay0 = bit ? 10 : 65; + uint32_t delay1 = bit ? 55 : 5; + + // delay A/C + delayMicroseconds(delay0); + // release bus + pin_.digital_write(true); + // delay B/D + delayMicroseconds(delay1); } bool HOT IRAM_ATTR ESPOneWire::read_bit() { - // Initiate read slot by pulling LOW for at least 1µs - this->pin_->pin_mode(gpio::FLAG_OUTPUT); - this->pin_->digital_write(false); + // See read bit here: + // https://www.maximintegrated.com/en/design/technical-documents/app-notes/1/126.html + InterruptLock lock; + + // drive bus low, delay A + pin_.pin_mode(gpio::FLAG_OUTPUT); + pin_.digital_write(false); delayMicroseconds(3); - // release bus, we have to sample within 15µs of pulling low - this->pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); + // release bus, delay E + pin_.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); delayMicroseconds(10); - bool r = this->pin_->digital_read(); - // read time slot at least 60µs long + 1µs recovery time between slots + // sample bus to read bit from peer + bool r = pin_.digital_read(); + + // delay F delayMicroseconds(53); return r; } -void IRAM_ATTR ESPOneWire::write8(uint8_t val) { +void ESPOneWire::write8(uint8_t val) { for (uint8_t i = 0; i < 8; i++) { this->write_bit(bool((1u << i) & val)); } } -void IRAM_ATTR ESPOneWire::write64(uint64_t val) { +void ESPOneWire::write64(uint64_t val) { for (uint8_t i = 0; i < 64; i++) { this->write_bit(bool((1ULL << i) & val)); } } -uint8_t IRAM_ATTR ESPOneWire::read8() { +uint8_t ESPOneWire::read8() { uint8_t ret = 0; for (uint8_t i = 0; i < 8; i++) { ret |= (uint8_t(this->read_bit()) << i); } return ret; } -uint64_t IRAM_ATTR ESPOneWire::read64() { +uint64_t ESPOneWire::read64() { uint64_t ret = 0; for (uint8_t i = 0; i < 8; i++) { ret |= (uint64_t(this->read_bit()) << i); } return ret; } -void IRAM_ATTR ESPOneWire::select(uint64_t address) { +void ESPOneWire::select(uint64_t address) { this->write8(ONE_WIRE_ROM_SELECT); this->write64(address); } -void IRAM_ATTR ESPOneWire::reset_search() { +void ESPOneWire::reset_search() { this->last_discrepancy_ = 0; this->last_device_flag_ = false; this->last_family_discrepancy_ = 0; this->rom_number_ = 0; } -uint64_t HOT IRAM_ATTR ESPOneWire::search() { +uint64_t ESPOneWire::search() { if (this->last_device_flag_) { return 0u; } if (!this->reset()) { - // Reset failed + // Reset failed or no devices present this->reset_search(); return 0u; } @@ -196,7 +204,7 @@ uint64_t HOT IRAM_ATTR ESPOneWire::search() { return this->rom_number_; } -std::vector IRAM_ATTR ESPOneWire::search_vec() { +std::vector ESPOneWire::search_vec() { std::vector res; this->reset_search(); @@ -206,10 +214,9 @@ std::vector IRAM_ATTR ESPOneWire::search_vec() { return res; } -void IRAM_ATTR ESPOneWire::skip() { +void ESPOneWire::skip() { this->write8(0xCC); // skip ROM } -GPIOPin *ESPOneWire::get_pin() { return this->pin_; } uint8_t IRAM_ATTR *ESPOneWire::rom_number8_() { return reinterpret_cast(&this->rom_number_); } diff --git a/esphome/components/dallas/esp_one_wire.h b/esphome/components/dallas/esp_one_wire.h index 728fa127d3..ef6f079f02 100644 --- a/esphome/components/dallas/esp_one_wire.h +++ b/esphome/components/dallas/esp_one_wire.h @@ -1,6 +1,5 @@ #pragma once -#include "esphome/core/component.h" #include "esphome/core/hal.h" #include @@ -12,7 +11,7 @@ extern const int ONE_WIRE_ROM_SEARCH; class ESPOneWire { public: - explicit ESPOneWire(GPIOPin *pin); + explicit ESPOneWire(InternalGPIOPin *pin); /** Reset the bus, should be done before all write operations. * @@ -55,13 +54,11 @@ class ESPOneWire { /// Helper that wraps search in a std::vector. std::vector search_vec(); - GPIOPin *get_pin(); - protected: /// Helper to get the internal 64-bit unsigned rom number as a 8-bit integer pointer. inline uint8_t *rom_number8_(); - GPIOPin *pin_; + ISRInternalGPIOPin pin_; uint8_t last_discrepancy_{0}; uint8_t last_family_discrepancy_{0}; bool last_device_flag_{false}; diff --git a/esphome/components/dashboard_import/__init__.py b/esphome/components/dashboard_import/__init__.py index 2b884d3b9a..d483c77c61 100644 --- a/esphome/components/dashboard_import/__init__.py +++ b/esphome/components/dashboard_import/__init__.py @@ -29,6 +29,14 @@ CONFIG_SCHEMA = cv.Schema( } ) +WIFI_MESSAGE = """ + +# Do not forget to add your own wifi configuration before installing this configuration +# wifi: +# ssid: !secret wifi_ssid +# password: !secret wifi_password +""" + async def to_code(config): cg.add_define("USE_DASHBOARD_IMPORT") @@ -41,5 +49,12 @@ def import_config(path: str, name: str, project_name: str, import_url: str) -> N if p.exists(): raise FileExistsError - config = {"substitutions": {"name": name}, "packages": {project_name: import_url}} - p.write_text(dump(config), encoding="utf8") + config = { + "substitutions": {"name": name}, + "packages": {project_name: import_url}, + "esphome": {"name_add_mac_suffix": False}, + } + p.write_text( + dump(config) + WIFI_MESSAGE, + encoding="utf8", + ) diff --git a/esphome/components/deep_sleep/deep_sleep_component.cpp b/esphome/components/deep_sleep/deep_sleep_component.cpp index e4b1edfb7b..0998a57af3 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.cpp +++ b/esphome/components/deep_sleep/deep_sleep_component.cpp @@ -78,8 +78,9 @@ void DeepSleepComponent::begin_sleep(bool manual) { esp_sleep_enable_timer_wakeup(*this->sleep_duration_); if (this->wakeup_pin_ != nullptr) { bool level = this->wakeup_pin_->is_inverted(); - if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) + if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && !this->wakeup_pin_->digital_read()) { level = !level; + } esp_sleep_enable_ext0_wakeup(gpio_num_t(this->wakeup_pin_->get_pin()), level); } if (this->ext1_wakeup_.has_value()) { diff --git a/esphome/components/display/__init__.py b/esphome/components/display/__init__.py index 947b09a258..0d403f99f0 100644 --- a/esphome/components/display/__init__.py +++ b/esphome/components/display/__init__.py @@ -3,6 +3,7 @@ import esphome.config_validation as cv from esphome import core, automation from esphome.automation import maybe_simple_id from esphome.const import ( + CONF_AUTO_CLEAR_ENABLED, CONF_ID, CONF_LAMBDA, CONF_PAGES, @@ -79,6 +80,7 @@ FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend( cv.Optional(CONF_TO): cv.use_id(DisplayPage), } ), + cv.Optional(CONF_AUTO_CLEAR_ENABLED, default=True): cv.boolean, } ) @@ -86,6 +88,10 @@ FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend( async def setup_display_core_(var, config): if CONF_ROTATION in config: cg.add(var.set_rotation(DISPLAY_ROTATIONS[config[CONF_ROTATION]])) + + if CONF_AUTO_CLEAR_ENABLED in config: + cg.add(var.set_auto_clear(config[CONF_AUTO_CLEAR_ENABLED])) + if CONF_PAGES in config: pages = [] for conf in config[CONF_PAGES]: diff --git a/esphome/components/display/display_buffer.cpp b/esphome/components/display/display_buffer.cpp index 2ee06e379f..ac806611b5 100644 --- a/esphome/components/display/display_buffer.cpp +++ b/esphome/components/display/display_buffer.cpp @@ -336,7 +336,9 @@ void DisplayBuffer::show_page(DisplayPage *page) { void DisplayBuffer::show_next_page() { this->page_->show_next(); } void DisplayBuffer::show_prev_page() { this->page_->show_prev(); } void DisplayBuffer::do_update_() { - this->clear(); + if (this->auto_clear_enabled_) { + this->clear(); + } if (this->page_ != nullptr) { this->page_->get_writer()(*this); } else if (this->writer_.has_value()) { diff --git a/esphome/components/display/display_buffer.h b/esphome/components/display/display_buffer.h index 54488f18f7..c803180a2d 100644 --- a/esphome/components/display/display_buffer.h +++ b/esphome/components/display/display_buffer.h @@ -333,6 +333,9 @@ class DisplayBuffer { /// Internal method to set the display rotation with. void set_rotation(DisplayRotation rotation); + // Internal method to set display auto clearing. + void set_auto_clear(bool auto_clear_enabled) { this->auto_clear_enabled_ = auto_clear_enabled; } + protected: void vprintf_(int x, int y, Font *font, Color color, TextAlign align, const char *format, va_list arg); @@ -352,6 +355,7 @@ class DisplayBuffer { DisplayPage *page_{nullptr}; DisplayPage *previous_page_{nullptr}; std::vector on_page_change_triggers_; + bool auto_clear_enabled_{true}; }; class DisplayPage { diff --git a/esphome/components/dsmr/dsmr.cpp b/esphome/components/dsmr/dsmr.cpp index b798fe5d44..ea852e626e 100644 --- a/esphome/components/dsmr/dsmr.cpp +++ b/esphome/components/dsmr/dsmr.cpp @@ -19,14 +19,30 @@ void Dsmr::loop() { this->receive_encrypted_(); } +bool Dsmr::available_within_timeout_() { + uint8_t tries = READ_TIMEOUT_MS / 5; + while (tries--) { + delay(5); + if (available()) { + return true; + } + } + return false; +} + void Dsmr::receive_telegram_() { - int count = MAX_BYTES_PER_LOOP; - while (available() && count-- > 0) { + while (true) { + if (!available()) { + if (!header_found_ || !available_within_timeout_()) { + return; + } + } + const char c = read(); // Find a new telegram header, i.e. forward slash. if (c == '/') { - ESP_LOGV(TAG, "Header found"); + ESP_LOGV(TAG, "Header of telegram found"); header_found_ = true; footer_found_ = false; telegram_len_ = 0; @@ -38,7 +54,7 @@ void Dsmr::receive_telegram_() { if (telegram_len_ >= MAX_TELEGRAM_LENGTH) { header_found_ = false; footer_found_ = false; - ESP_LOGE(TAG, "Error: Message larger than buffer"); + ESP_LOGE(TAG, "Error: telegram larger than buffer (%d bytes)", MAX_TELEGRAM_LENGTH); return; } @@ -54,16 +70,17 @@ void Dsmr::receive_telegram_() { // Check for a footer, i.e. exlamation mark, followed by a hex checksum. if (c == '!') { - ESP_LOGV(TAG, "Footer found"); + ESP_LOGV(TAG, "Footer of telegram found"); footer_found_ = true; continue; } // Check for the end of the hex checksum, i.e. a newline. if (footer_found_ && c == '\n') { - header_found_ = false; // Parse the telegram and publish sensor values. - if (parse_telegram()) - return; + parse_telegram(); + + header_found_ = false; + return; } } } @@ -72,41 +89,46 @@ void Dsmr::receive_encrypted_() { // Encrypted buffer uint8_t buffer[MAX_TELEGRAM_LENGTH]; size_t buffer_length = 0; - size_t packet_size = 0; - while (available()) { - const char c = read(); - if (!header_found_) { - if ((uint8_t) c == 0xdb) { - ESP_LOGV(TAG, "Start byte 0xDB found"); - header_found_ = true; + while (true) { + if (!available()) { + if (!header_found_) { + return; + } + if (!available_within_timeout_()) { + ESP_LOGW(TAG, "Timeout while reading data for encrypted telegram"); + return; } } - // Sanity check - if (!header_found_ || buffer_length >= MAX_TELEGRAM_LENGTH) { - if (buffer_length == 0) { - ESP_LOGE(TAG, "First byte of encrypted telegram should be 0xDB, aborting."); - } else { - ESP_LOGW(TAG, "Unexpected data"); + const char c = read(); + + // Find a new telegram start byte. + if (!header_found_) { + if ((uint8_t) c != 0xDB) { + continue; } - this->status_momentary_warning("unexpected_data"); - this->flush(); - while (available()) - read(); + ESP_LOGV(TAG, "Start byte 0xDB of encrypted telegram found"); + header_found_ = true; + } + + // Check for buffer overflow. + if (buffer_length >= MAX_TELEGRAM_LENGTH) { + header_found_ = false; + ESP_LOGE(TAG, "Error: encrypted telegram larger than buffer (%d bytes)", MAX_TELEGRAM_LENGTH); return; } buffer[buffer_length++] = c; if (packet_size == 0 && buffer_length > 20) { - // Complete header + a few bytes of data - packet_size = buffer[11] << 8 | buffer[12]; + // Complete header + data bytes + packet_size = 13 + (buffer[11] << 8 | buffer[12]); + ESP_LOGV(TAG, "Encrypted telegram size: %d bytes", packet_size); } - if (buffer_length == packet_size + 13 && packet_size > 0) { - ESP_LOGV(TAG, "Encrypted data: %d bytes", buffer_length); - + if (buffer_length == packet_size && packet_size > 0) { + ESP_LOGV(TAG, "End of encrypted telegram found"); GCM *gcmaes128{new GCM()}; gcmaes128->setKey(this->decryption_key_.data(), gcmaes128->keySize()); // the iv is 8 bytes of the system title + 4 bytes frame counter @@ -123,28 +145,21 @@ void Dsmr::receive_encrypted_() { delete gcmaes128; // NOLINT(cppcoreguidelines-owning-memory) telegram_len_ = strnlen(this->telegram_, sizeof(this->telegram_)); - ESP_LOGV(TAG, "Decrypted data length: %d", telegram_len_); - ESP_LOGVV(TAG, "Decrypted data %s", this->telegram_); + ESP_LOGV(TAG, "Decrypted telegram size: %d bytes", telegram_len_); + ESP_LOGVV(TAG, "Decrypted telegram: %s", this->telegram_); parse_telegram(); + + header_found_ = false; telegram_len_ = 0; return; } - - if (!available()) { - // baud rate is 115200 for encrypted data, this means a few byte should arrive every time - // program runs faster than buffer loading then available() might return false in the middle - delay(4); // Wait for data - } - } - if (buffer_length > 0) { - ESP_LOGW(TAG, "Timeout while waiting for encrypted data or invalid data received."); } } bool Dsmr::parse_telegram() { MyData data; - ESP_LOGV(TAG, "Trying to parse"); + ESP_LOGV(TAG, "Trying to parse telegram"); ::dsmr::ParseResult res = ::dsmr::P1Parser::parse(&data, telegram_, telegram_len_, false, this->crc_check_); // Parse telegram according to data definition. Ignore unknown values. @@ -161,7 +176,7 @@ bool Dsmr::parse_telegram() { } void Dsmr::dump_config() { - ESP_LOGCONFIG(TAG, "dsmr:"); + ESP_LOGCONFIG(TAG, "DSMR:"); #define DSMR_LOG_SENSOR(s) LOG_SENSOR(" ", #s, this->s_##s##_); DSMR_SENSOR_LIST(DSMR_LOG_SENSOR, ) @@ -178,12 +193,12 @@ void Dsmr::set_decryption_key(const std::string &decryption_key) { } if (decryption_key.length() != 32) { - ESP_LOGE(TAG, "Error, decryption key must be 32 character long."); + ESP_LOGE(TAG, "Error, decryption key must be 32 character long"); return; } this->decryption_key_.clear(); - ESP_LOGI(TAG, "Decryption key is set."); + ESP_LOGI(TAG, "Decryption key is set"); // Verbose level prints decryption key ESP_LOGV(TAG, "Using decryption key: %s", decryption_key.c_str()); diff --git a/esphome/components/dsmr/dsmr.h b/esphome/components/dsmr/dsmr.h index 4f9a66b3d0..ca2c0f0877 100644 --- a/esphome/components/dsmr/dsmr.h +++ b/esphome/components/dsmr/dsmr.h @@ -17,8 +17,7 @@ namespace esphome { namespace dsmr { static constexpr uint32_t MAX_TELEGRAM_LENGTH = 1500; -static constexpr uint32_t MAX_BYTES_PER_LOOP = 50; -static constexpr uint32_t POLL_TIMEOUT = 1000; +static constexpr uint32_t READ_TIMEOUT_MS = 200; using namespace ::dsmr::fields; @@ -86,6 +85,17 @@ class Dsmr : public Component, public uart::UARTDevice { void receive_telegram_(); void receive_encrypted_(); + /// Wait for UART data to become available within the read timeout. + /// + /// The smart meter might provide data in chunks, causing available() to + /// return 0. When we're already reading a telegram, then we don't return + /// right away (to handle further data in an upcoming loop) but wait a + /// little while using this method to see if more data are incoming. + /// By not returning, we prevent other components from taking so much + /// time that the UART RX buffer overflows and bytes of the telegram get + /// lost in the process. + bool available_within_timeout_(); + // Telegram buffer char telegram_[MAX_TELEGRAM_LENGTH]; int telegram_len_{0}; diff --git a/esphome/components/duty_cycle/duty_cycle_sensor.cpp b/esphome/components/duty_cycle/duty_cycle_sensor.cpp index 3d7f731d5d..aed22312a7 100644 --- a/esphome/components/duty_cycle/duty_cycle_sensor.cpp +++ b/esphome/components/duty_cycle/duty_cycle_sensor.cpp @@ -12,7 +12,6 @@ void DutyCycleSensor::setup() { this->pin_->setup(); this->store_.pin = this->pin_->to_isr(); this->store_.last_level = this->pin_->digital_read(); - this->last_update_ = micros(); this->store_.last_interrupt = micros(); this->pin_->attach_interrupt(DutyCycleSensorStore::gpio_intr, &this->store_, gpio::INTERRUPT_ANY_EDGE); @@ -24,19 +23,20 @@ void DutyCycleSensor::dump_config() { } void DutyCycleSensor::update() { const uint32_t now = micros(); - const bool level = this->store_.last_level; - const uint32_t last_interrupt = this->store_.last_interrupt; - uint32_t on_time = this->store_.on_time; + if (this->last_update_ != 0) { + const bool level = this->store_.last_level; + const uint32_t last_interrupt = this->store_.last_interrupt; + uint32_t on_time = this->store_.on_time; - if (level) - on_time += now - last_interrupt; + if (level) + on_time += now - last_interrupt; - const float total_time = float(now - this->last_update_); - - const float value = (on_time / total_time) * 100.0f; - ESP_LOGD(TAG, "'%s' Got duty cycle=%.1f%%", this->get_name().c_str(), value); - this->publish_state(value); + const float total_time = float(now - this->last_update_); + const float value = (on_time / total_time) * 100.0f; + ESP_LOGD(TAG, "'%s' Got duty cycle=%.1f%%", this->get_name().c_str(), value); + this->publish_state(value); + } this->store_.on_time = 0; this->store_.last_interrupt = now; this->last_update_ = now; diff --git a/esphome/components/duty_cycle/duty_cycle_sensor.h b/esphome/components/duty_cycle/duty_cycle_sensor.h index 22d3588fb7..ffb1802e14 100644 --- a/esphome/components/duty_cycle/duty_cycle_sensor.h +++ b/esphome/components/duty_cycle/duty_cycle_sensor.h @@ -30,7 +30,7 @@ class DutyCycleSensor : public sensor::Sensor, public PollingComponent { InternalGPIOPin *pin_; DutyCycleSensorStore store_{}; - uint32_t last_update_; + uint32_t last_update_{0}; }; } // namespace duty_cycle diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index e1128ff227..1c249476e7 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -28,6 +28,7 @@ from .const import ( # noqa KEY_SDKCONFIG_OPTIONS, KEY_VARIANT, VARIANT_ESP32C3, + VARIANT_FRIENDLY, VARIANTS, ) from .boards import BOARD_TO_VARIANT @@ -147,8 +148,9 @@ def _arduino_check_versions(value): value[CONF_VERSION] = str(version) value[CONF_SOURCE] = source or _format_framework_arduino_version(version) - platform_version = value.get(CONF_PLATFORM_VERSION, ARDUINO_PLATFORM_VERSION) - value[CONF_PLATFORM_VERSION] = str(platform_version) + value[CONF_PLATFORM_VERSION] = value.get( + CONF_PLATFORM_VERSION, _parse_platform_version(str(ARDUINO_PLATFORM_VERSION)) + ) if version != RECOMMENDED_ARDUINO_FRAMEWORK_VERSION: _LOGGER.warning( @@ -184,8 +186,9 @@ def _esp_idf_check_versions(value): value[CONF_VERSION] = str(version) value[CONF_SOURCE] = source or _format_framework_espidf_version(version) - platform_version = value.get(CONF_PLATFORM_VERSION, ESP_IDF_PLATFORM_VERSION) - value[CONF_PLATFORM_VERSION] = str(platform_version) + value[CONF_PLATFORM_VERSION] = value.get( + CONF_PLATFORM_VERSION, _parse_platform_version(str(ESP_IDF_PLATFORM_VERSION)) + ) if version != RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION: _LOGGER.warning( @@ -196,6 +199,15 @@ def _esp_idf_check_versions(value): return value +def _parse_platform_version(value): + try: + # if platform version is a valid version constraint, prefix the default package + cv.platformio_version_constraint(value) + return f"platformio/espressif32 @ {value}" + except cv.Invalid: + return value + + def _detect_variant(value): if CONF_VARIANT not in value: board = value[CONF_BOARD] @@ -218,7 +230,7 @@ ARDUINO_FRAMEWORK_SCHEMA = cv.All( { cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict, cv.Optional(CONF_SOURCE): cv.string_strict, - cv.Optional(CONF_PLATFORM_VERSION): cv.string_strict, + cv.Optional(CONF_PLATFORM_VERSION): _parse_platform_version, } ), _arduino_check_versions, @@ -230,7 +242,7 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All( { cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict, cv.Optional(CONF_SOURCE): cv.string_strict, - cv.Optional(CONF_PLATFORM_VERSION): cv.string_strict, + cv.Optional(CONF_PLATFORM_VERSION): _parse_platform_version, cv.Optional(CONF_SDKCONFIG_OPTIONS, default={}): { cv.string_strict: cv.string_strict }, @@ -276,14 +288,14 @@ async def to_code(config): cg.add_build_flag("-DUSE_ESP32") cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{config[CONF_VARIANT]}") + cg.add_define("ESPHOME_VARIANT", VARIANT_FRIENDLY[config[CONF_VARIANT]]) cg.add_platformio_option("lib_ldf_mode", "off") conf = config[CONF_FRAMEWORK] + cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION]) + if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF: - cg.add_platformio_option( - "platform", f"espressif32 @ {conf[CONF_PLATFORM_VERSION]}" - ) cg.add_platformio_option("framework", "espidf") cg.add_build_flag("-DUSE_ESP_IDF") cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF") @@ -299,6 +311,8 @@ async def to_code(config): ) add_idf_sdkconfig_option("CONFIG_COMPILER_OPTIMIZATION_DEFAULT", False) add_idf_sdkconfig_option("CONFIG_COMPILER_OPTIMIZATION_SIZE", True) + # Increase freertos tick speed from 100Hz to 1kHz so that delay() resolution is 1ms + add_idf_sdkconfig_option("CONFIG_FREERTOS_HZ", 1000) cg.add_platformio_option("board_build.partitions", "partitions.csv") @@ -312,9 +326,6 @@ async def to_code(config): ) elif conf[CONF_TYPE] == FRAMEWORK_ARDUINO: - cg.add_platformio_option( - "platform", f"espressif32 @ {conf[CONF_PLATFORM_VERSION]}" - ) cg.add_platformio_option("framework", "arduino") cg.add_build_flag("-DUSE_ARDUINO") cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ARDUINO") diff --git a/esphome/components/esp32/const.py b/esphome/components/esp32/const.py index b82f03bf68..d92b449ee9 100644 --- a/esphome/components/esp32/const.py +++ b/esphome/components/esp32/const.py @@ -18,4 +18,12 @@ VARIANTS = [ VARIANT_ESP32H2, ] +VARIANT_FRIENDLY = { + VARIANT_ESP32: "ESP32", + VARIANT_ESP32S2: "ESP32-S2", + VARIANT_ESP32S3: "ESP32-S3", + VARIANT_ESP32C3: "ESP32-C3", + VARIANT_ESP32H2: "ESP32-H2", +} + esp32_ns = cg.esphome_ns.namespace("esp32") diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index 96047df535..359999120f 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -21,11 +21,7 @@ void IRAM_ATTR HOT yield() { vPortYield(); } uint32_t IRAM_ATTR HOT millis() { return (uint32_t)(esp_timer_get_time() / 1000ULL); } void IRAM_ATTR HOT delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); } uint32_t IRAM_ATTR HOT micros() { return (uint32_t) esp_timer_get_time(); } -void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { - auto start = (uint64_t) esp_timer_get_time(); - while (((uint64_t) esp_timer_get_time()) - start < us) - ; -} +void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } void arch_restart() { esp_restart(); // restart() doesn't always end execution diff --git a/esphome/components/esp32/gpio.py b/esphome/components/esp32/gpio.py index 93ab17db22..5819943f37 100644 --- a/esphome/components/esp32/gpio.py +++ b/esphome/components/esp32/gpio.py @@ -1,4 +1,5 @@ -import logging +from dataclasses import dataclass +from typing import Any from esphome.const import ( CONF_ID, @@ -17,10 +18,24 @@ import esphome.config_validation as cv import esphome.codegen as cg from . import boards -from .const import KEY_BOARD, KEY_ESP32, esp32_ns +from .const import ( + KEY_BOARD, + KEY_ESP32, + KEY_VARIANT, + VARIANT_ESP32, + VARIANT_ESP32C3, + VARIANT_ESP32S2, + VARIANT_ESP32S3, + VARIANT_ESP32H2, + esp32_ns, +) -_LOGGER = logging.getLogger(__name__) +from .gpio_esp32 import esp32_validate_gpio_pin, esp32_validate_supports +from .gpio_esp32_s2 import esp32_s2_validate_gpio_pin, esp32_s2_validate_supports +from .gpio_esp32_c3 import esp32_c3_validate_gpio_pin, esp32_c3_validate_supports +from .gpio_esp32_s3 import esp32_s3_validate_gpio_pin, esp32_s3_validate_supports +from .gpio_esp32_h2 import esp32_h2_validate_gpio_pin, esp32_h2_validate_supports IDFInternalGPIOPin = esp32_ns.class_("IDFInternalGPIOPin", cg.InternalGPIOPin) @@ -59,65 +74,61 @@ def _translate_pin(value): return _lookup_pin(value) -_ESP_SDIO_PINS = { - 6: "Flash Clock", - 7: "Flash Data 0", - 8: "Flash Data 1", - 11: "Flash Command", +@dataclass +class ESP32ValidationFunctions: + pin_validation: Any + usage_validation: Any + + +_esp32_validations = { + VARIANT_ESP32: ESP32ValidationFunctions( + pin_validation=esp32_validate_gpio_pin, usage_validation=esp32_validate_supports + ), + VARIANT_ESP32S2: ESP32ValidationFunctions( + pin_validation=esp32_s2_validate_gpio_pin, + usage_validation=esp32_s2_validate_supports, + ), + VARIANT_ESP32C3: ESP32ValidationFunctions( + pin_validation=esp32_c3_validate_gpio_pin, + usage_validation=esp32_c3_validate_supports, + ), + VARIANT_ESP32S3: ESP32ValidationFunctions( + pin_validation=esp32_s3_validate_gpio_pin, + usage_validation=esp32_s3_validate_supports, + ), + VARIANT_ESP32H2: ESP32ValidationFunctions( + pin_validation=esp32_h2_validate_gpio_pin, + usage_validation=esp32_h2_validate_supports, + ), } def validate_gpio_pin(value): value = _translate_pin(value) - if value < 0 or value > 39: - raise cv.Invalid(f"Invalid pin number: {value} (must be 0-39)") - if value in _ESP_SDIO_PINS: - raise cv.Invalid( - f"This pin cannot be used on ESP32s and is already used by the flash interface (function: {_ESP_SDIO_PINS[value]})" - ) - if 9 <= value <= 10: - _LOGGER.warning( - "Pin %s (9-10) might already be used by the " - "flash interface in QUAD IO flash mode.", - value, - ) - if value in (20, 24, 28, 29, 30, 31): - # These pins are not exposed in GPIO mux (reason unknown) - # but they're missing from IO_MUX list in datasheet - raise cv.Invalid(f"The pin GPIO{value} is not usable on ESP32s.") - return value + variant = CORE.data[KEY_ESP32][KEY_VARIANT] + if variant not in _esp32_validations: + raise cv.Invalid("Unsupported ESP32 variant {variant}") + + return _esp32_validations[variant].pin_validation(value) def validate_supports(value): - num = value[CONF_NUMBER] mode = value[CONF_MODE] is_input = mode[CONF_INPUT] is_output = mode[CONF_OUTPUT] is_open_drain = mode[CONF_OPEN_DRAIN] is_pullup = mode[CONF_PULLUP] is_pulldown = mode[CONF_PULLDOWN] + variant = CORE.data[KEY_ESP32][KEY_VARIANT] + if variant not in _esp32_validations: + raise cv.Invalid("Unsupported ESP32 variant {variant}") - if is_input: - # All ESP32 pins support input mode - pass - if is_output and 34 <= num <= 39: - raise cv.Invalid( - f"GPIO{num} (34-39) does not support output pin mode.", - [CONF_MODE, CONF_OUTPUT], - ) if is_open_drain and not is_output: raise cv.Invalid( "Open-drain only works with output mode", [CONF_MODE, CONF_OPEN_DRAIN] ) - if is_pullup and 34 <= num <= 39: - raise cv.Invalid( - f"GPIO{num} (34-39) does not support pullups.", [CONF_MODE, CONF_PULLUP] - ) - if is_pulldown and 34 <= num <= 39: - raise cv.Invalid( - f"GPIO{num} (34-39) does not support pulldowns.", [CONF_MODE, CONF_PULLDOWN] - ) + value = _esp32_validations[variant].usage_validation(value) if CORE.using_arduino: # (input, output, open_drain, pullup, pulldown) supported_modes = { @@ -138,7 +149,6 @@ def validate_supports(value): "This pin mode is not supported on ESP32 for arduino frameworks", [CONF_MODE], ) - return value diff --git a/esphome/components/esp32/gpio_arduino.cpp b/esphome/components/esp32/gpio_arduino.cpp index c4bb21a0aa..ba92894f97 100644 --- a/esphome/components/esp32/gpio_arduino.cpp +++ b/esphome/components/esp32/gpio_arduino.cpp @@ -9,6 +9,22 @@ namespace esp32 { static const char *const TAG = "esp32"; +static int IRAM_ATTR flags_to_mode(gpio::Flags flags) { + if (flags == gpio::FLAG_INPUT) { + return INPUT; + } else if (flags == gpio::FLAG_OUTPUT) { + return OUTPUT; + } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLUP)) { + return INPUT_PULLUP; + } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLDOWN)) { + return INPUT_PULLDOWN; + } else if (flags == (gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN)) { + return OUTPUT_OPEN_DRAIN; + } else { + return 0; + } +} + struct ISRPinArg { uint8_t pin; bool inverted; @@ -43,22 +59,9 @@ void ArduinoInternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, g attachInterruptArg(pin_, func, arg, arduino_mode); } + void ArduinoInternalGPIOPin::pin_mode(gpio::Flags flags) { - uint8_t mode; - if (flags == gpio::FLAG_INPUT) { - mode = INPUT; - } else if (flags == gpio::FLAG_OUTPUT) { - mode = OUTPUT; - } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLUP)) { - mode = INPUT_PULLUP; - } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLDOWN)) { - mode = INPUT_PULLDOWN; - } else if (flags == (gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN)) { - mode = OUTPUT_OPEN_DRAIN; - } else { - return; - } - pinMode(pin_, mode); // NOLINT + pinMode(pin_, flags_to_mode(flags)); // NOLINT } std::string ArduinoInternalGPIOPin::dump_summary() const { @@ -101,6 +104,10 @@ void IRAM_ATTR ISRInternalGPIOPin::clear_interrupt() { } #endif } +void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) { + auto *arg = reinterpret_cast(arg_); + pinMode(arg->pin, flags_to_mode(flags)); // NOLINT +} } // namespace esphome diff --git a/esphome/components/esp32/gpio_esp32.py b/esphome/components/esp32/gpio_esp32.py new file mode 100644 index 0000000000..425d77b343 --- /dev/null +++ b/esphome/components/esp32/gpio_esp32.py @@ -0,0 +1,77 @@ +import logging + +from esphome.const import ( + CONF_INPUT, + CONF_MODE, + CONF_NUMBER, + CONF_OUTPUT, + CONF_PULLDOWN, + CONF_PULLUP, +) +import esphome.config_validation as cv + + +_ESP_SDIO_PINS = { + 6: "Flash Clock", + 7: "Flash Data 0", + 8: "Flash Data 1", + 11: "Flash Command", +} + +_ESP32_STRAPPING_PINS = {0, 2, 4, 15} +_LOGGER = logging.getLogger(__name__) + + +def esp32_validate_gpio_pin(value): + if value < 0 or value > 39: + raise cv.Invalid(f"Invalid pin number: {value} (must be 0-39)") + if value in _ESP_SDIO_PINS: + raise cv.Invalid( + f"This pin cannot be used on ESP32s and is already used by the flash interface (function: {_ESP_SDIO_PINS[value]})" + ) + if 9 <= value <= 10: + _LOGGER.warning( + "Pin %s (9-10) might already be used by the " + "flash interface in QUAD IO flash mode.", + value, + ) + if value in _ESP32_STRAPPING_PINS: + _LOGGER.warning( + "GPIO%d is a Strapping PIN and should be avoided.\n" + "Attaching external pullup/down resistors to strapping pins can cause unexpected failures.\n" + "See https://esphome.io/guides/faq.html#why-am-i-getting-a-warning-about-strapping-pins", + value, + ) + if value in (20, 24, 28, 29, 30, 31): + # These pins are not exposed in GPIO mux (reason unknown) + # but they're missing from IO_MUX list in datasheet + raise cv.Invalid(f"The pin GPIO{value} is not usable on ESP32s.") + return value + + +def esp32_validate_supports(value): + num = value[CONF_NUMBER] + mode = value[CONF_MODE] + is_input = mode[CONF_INPUT] + is_output = mode[CONF_OUTPUT] + is_pullup = mode[CONF_PULLUP] + is_pulldown = mode[CONF_PULLDOWN] + + if is_input: + # All ESP32 pins support input mode + pass + if is_output and 34 <= num <= 39: + raise cv.Invalid( + f"GPIO{num} (34-39) does not support output pin mode.", + [CONF_MODE, CONF_OUTPUT], + ) + if is_pullup and 34 <= num <= 39: + raise cv.Invalid( + f"GPIO{num} (34-39) does not support pullups.", [CONF_MODE, CONF_PULLUP] + ) + if is_pulldown and 34 <= num <= 39: + raise cv.Invalid( + f"GPIO{num} (34-39) does not support pulldowns.", [CONF_MODE, CONF_PULLDOWN] + ) + + return value diff --git a/esphome/components/esp32/gpio_esp32_c3.py b/esphome/components/esp32/gpio_esp32_c3.py new file mode 100644 index 0000000000..fc1cef29e5 --- /dev/null +++ b/esphome/components/esp32/gpio_esp32_c3.py @@ -0,0 +1,53 @@ +import logging + +from esphome.const import ( + CONF_INPUT, + CONF_MODE, + CONF_NUMBER, +) +import esphome.config_validation as cv + +_ESP32C3_SPI_PSRAM_PINS = { + 12: "SPIHD", + 13: "SPIWP", + 14: "SPICS0", + 15: "SPICLK", + 16: "SPID", + 17: "SPIQ", +} + +_ESP32C3_STRAPPING_PINS = {2, 8, 9} + +_LOGGER = logging.getLogger(__name__) + + +def esp32_c3_validate_gpio_pin(value): + if value < 0 or value > 21: + raise cv.Invalid(f"Invalid pin number: {value} (must be 0-21)") + if value in _ESP32C3_SPI_PSRAM_PINS: + raise cv.Invalid( + f"This pin cannot be used on ESP32-C3s and is already used by the SPI/PSRAM interface (function: {_ESP32C3_SPI_PSRAM_PINS[value]})" + ) + if value in _ESP32C3_STRAPPING_PINS: + _LOGGER.warning( + "GPIO%d is a Strapping PIN and should be avoided.\n" + "Attaching external pullup/down resistors to strapping pins can cause unexpected failures.\n" + "See https://esphome.io/guides/faq.html#why-am-i-getting-a-warning-about-strapping-pins", + value, + ) + + return value + + +def esp32_c3_validate_supports(value): + num = value[CONF_NUMBER] + mode = value[CONF_MODE] + is_input = mode[CONF_INPUT] + + if num < 0 or num > 21: + raise cv.Invalid(f"Invalid pin number: {value} (must be 0-21)") + + if is_input: + # All ESP32 pins support input mode + pass + return value diff --git a/esphome/components/esp32/gpio_esp32_h2.py b/esphome/components/esp32/gpio_esp32_h2.py new file mode 100644 index 0000000000..5196ef0c09 --- /dev/null +++ b/esphome/components/esp32/gpio_esp32_h2.py @@ -0,0 +1,11 @@ +import esphome.config_validation as cv + + +def esp32_h2_validate_gpio_pin(value): + # ESP32-H2 not yet supported + raise cv.Invalid("ESP32-H2 isn't supported yet") + + +def esp32_h2_validate_supports(value): + # ESP32-H2 not yet supported + raise cv.Invalid("ESP32-H2 isn't supported yet") diff --git a/esphome/components/esp32/gpio_esp32_s2.py b/esphome/components/esp32/gpio_esp32_s2.py new file mode 100644 index 0000000000..db244b6259 --- /dev/null +++ b/esphome/components/esp32/gpio_esp32_s2.py @@ -0,0 +1,80 @@ +import logging + +from esphome.const import ( + CONF_INPUT, + CONF_MODE, + CONF_NUMBER, + CONF_OUTPUT, + CONF_PULLDOWN, + CONF_PULLUP, +) + +import esphome.config_validation as cv + +_ESP32S2_SPI_PSRAM_PINS = { + 26: "SPICS1", + 27: "SPIHD", + 28: "SPIWP", + 29: "SPICS0", + 30: "SPICLK", + 31: "SPIQ", + 32: "SPID", +} + +_ESP32S2_STRAPPING_PINS = {0, 45, 46} + +_LOGGER = logging.getLogger(__name__) + + +def esp32_s2_validate_gpio_pin(value): + if value < 0 or value > 46: + raise cv.Invalid(f"Invalid pin number: {value} (must be 0-46)") + + if value in _ESP32S2_SPI_PSRAM_PINS: + raise cv.Invalid( + f"This pin cannot be used on ESP32-S2s and is already used by the SPI/PSRAM interface (function: {_ESP32S2_SPI_PSRAM_PINS[value]})" + ) + if value in _ESP32S2_STRAPPING_PINS: + _LOGGER.warning( + "GPIO%d is a Strapping PIN and should be avoided.\n" + "Attaching external pullup/down resistors to strapping pins can cause unexpected failures.\n" + "See https://esphome.io/guides/faq.html#why-am-i-getting-a-warning-about-strapping-pins", + value, + ) + + if value in (22, 23, 24, 25): + # These pins are not exposed in GPIO mux (reason unknown) + # but they're missing from IO_MUX list in datasheet + raise cv.Invalid(f"The pin GPIO{value} is not usable on ESP32-S2s.") + + return value + + +def esp32_s2_validate_supports(value): + num = value[CONF_NUMBER] + mode = value[CONF_MODE] + is_input = mode[CONF_INPUT] + is_output = mode[CONF_OUTPUT] + is_pullup = mode[CONF_PULLUP] + is_pulldown = mode[CONF_PULLDOWN] + + if num < 0 or num > 46: + raise cv.Invalid(f"Invalid pin number: {num} (must be 0-46)") + if is_input: + # All ESP32 pins support input mode + pass + if is_output and num == 46: + raise cv.Invalid( + f"GPIO{num} does not support output pin mode.", + [CONF_MODE, CONF_OUTPUT], + ) + if is_pullup and num == 46: + raise cv.Invalid( + f"GPIO{num} does not support pullups.", [CONF_MODE, CONF_PULLUP] + ) + if is_pulldown and num == 46: + raise cv.Invalid( + f"GPIO{num} does not support pulldowns.", [CONF_MODE, CONF_PULLDOWN] + ) + + return value diff --git a/esphome/components/esp32/gpio_esp32_s3.py b/esphome/components/esp32/gpio_esp32_s3.py new file mode 100644 index 0000000000..f729a757c2 --- /dev/null +++ b/esphome/components/esp32/gpio_esp32_s3.py @@ -0,0 +1,74 @@ +import logging + +from esphome.const import ( + CONF_INPUT, + CONF_MODE, + CONF_NUMBER, +) + +import esphome.config_validation as cv + +_ESP_32S3_SPI_PSRAM_PINS = { + 26: "SPICS1", + 27: "SPIHD", + 28: "SPIWP", + 29: "SPICS0", + 30: "SPICLK", + 31: "SPIQ", + 32: "SPID", +} + +_ESP_32_ESP32_S3R8_PSRAM_PINS = { + 33: "SPIIO4", + 34: "SPIIO5", + 35: "SPIIO6", + 36: "SPIIO7", + 37: "SPIDQS", +} + +_ESP_32S3_STRAPPING_PINS = {0, 3, 45, 46} + +_LOGGER = logging.getLogger(__name__) + + +def esp32_s3_validate_gpio_pin(value): + if value < 0 or value > 48: + raise cv.Invalid(f"Invalid pin number: {value} (must be 0-46)") + + if value in _ESP_32S3_SPI_PSRAM_PINS: + raise cv.Invalid( + f"This pin cannot be used on ESP32-S3s and is already used by the SPI/PSRAM interface(function: {_ESP_32S3_SPI_PSRAM_PINS[value]})" + ) + if value in _ESP_32_ESP32_S3R8_PSRAM_PINS: + _LOGGER.warning( + "GPIO%d is used by the PSRAM interface on ESP32-S3R8 / ESP32-S3R8V and should be avoided on these models", + value, + ) + + if value in _ESP_32S3_STRAPPING_PINS: + _LOGGER.warning( + "GPIO%d is a Strapping PIN and should be avoided.\n" + "Attaching external pullup/down resistors to strapping pins can cause unexpected failures.\n" + "See https://esphome.io/guides/faq.html#why-am-i-getting-a-warning-about-strapping-pins", + value, + ) + + if value in (22, 23, 24, 25): + # These pins are not exposed in GPIO mux (reason unknown) + # but they're missing from IO_MUX list in datasheet + raise cv.Invalid(f"The pin GPIO{value} is not usable on ESP32-S3s.") + + return value + + +def esp32_s3_validate_supports(value): + num = value[CONF_NUMBER] + mode = value[CONF_MODE] + is_input = mode[CONF_INPUT] + + if num < 0 or num > 48: + raise cv.Invalid(f"Invalid pin number: {num} (must be 0-46)") + if is_input: + # All ESP32 pins support input mode + pass + return value diff --git a/esphome/components/esp32/gpio_idf.cpp b/esphome/components/esp32/gpio_idf.cpp index d1853e1f8b..498843ebff 100644 --- a/esphome/components/esp32/gpio_idf.cpp +++ b/esphome/components/esp32/gpio_idf.cpp @@ -10,38 +10,7 @@ static const char *const TAG = "esp32"; bool IDFInternalGPIOPin::isr_service_installed = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -struct ISRPinArg { - gpio_num_t pin; - bool inverted; -}; - -ISRInternalGPIOPin IDFInternalGPIOPin::to_isr() const { - auto *arg = new ISRPinArg{}; // NOLINT(cppcoreguidelines-owning-memory) - arg->pin = pin_; - arg->inverted = inverted_; - return ISRInternalGPIOPin((void *) arg); -} - -void IDFInternalGPIOPin::setup() { - pin_mode(flags_); - gpio_set_drive_capability(pin_, drive_strength_); -} - -void IDFInternalGPIOPin::pin_mode(gpio::Flags flags) { - gpio_config_t conf{}; - conf.pin_bit_mask = 1ULL << static_cast(pin_); - conf.mode = flags_to_mode(flags); - conf.pull_up_en = flags & gpio::FLAG_PULLUP ? GPIO_PULLUP_ENABLE : GPIO_PULLUP_DISABLE; - conf.pull_down_en = flags & gpio::FLAG_PULLDOWN ? GPIO_PULLDOWN_ENABLE : GPIO_PULLDOWN_DISABLE; - conf.intr_type = GPIO_INTR_DISABLE; - gpio_config(&conf); -} - -bool IDFInternalGPIOPin::digital_read() { return bool(gpio_get_level(pin_)) != inverted_; } - -void IDFInternalGPIOPin::digital_write(bool value) { gpio_set_level(pin_, value != inverted_ ? 1 : 0); } - -gpio_mode_t IDFInternalGPIOPin::flags_to_mode(gpio::Flags flags) { +static gpio_mode_t IRAM_ATTR flags_to_mode(gpio::Flags flags) { flags = (gpio::Flags)(flags & ~(gpio::FLAG_PULLUP | gpio::FLAG_PULLDOWN)); if (flags == gpio::FLAG_NONE) { return GPIO_MODE_DISABLE; @@ -61,6 +30,18 @@ gpio_mode_t IDFInternalGPIOPin::flags_to_mode(gpio::Flags flags) { } } +struct ISRPinArg { + gpio_num_t pin; + bool inverted; +}; + +ISRInternalGPIOPin IDFInternalGPIOPin::to_isr() const { + auto *arg = new ISRPinArg{}; // NOLINT(cppcoreguidelines-owning-memory) + arg->pin = pin_; + arg->inverted = inverted_; + return ISRInternalGPIOPin((void *) arg); +} + void IDFInternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const { gpio_int_type_t idf_type = GPIO_INTR_ANYEDGE; switch (type) { @@ -99,6 +80,35 @@ std::string IDFInternalGPIOPin::dump_summary() const { return buffer; } +void IDFInternalGPIOPin::setup() { + gpio_config_t conf{}; + conf.pin_bit_mask = 1ULL << static_cast(pin_); + conf.mode = flags_to_mode(flags_); + conf.pull_up_en = flags_ & gpio::FLAG_PULLUP ? GPIO_PULLUP_ENABLE : GPIO_PULLUP_DISABLE; + conf.pull_down_en = flags_ & gpio::FLAG_PULLDOWN ? GPIO_PULLDOWN_ENABLE : GPIO_PULLDOWN_DISABLE; + conf.intr_type = GPIO_INTR_DISABLE; + gpio_config(&conf); + gpio_set_drive_capability(pin_, drive_strength_); +} + +void IDFInternalGPIOPin::pin_mode(gpio::Flags flags) { + // can't call gpio_config here because that logs in esp-idf which may cause issues + gpio_set_direction(pin_, flags_to_mode(flags)); + gpio_pull_mode_t pull_mode = GPIO_FLOATING; + if (flags & (gpio::FLAG_PULLUP | gpio::FLAG_PULLDOWN)) { + pull_mode = GPIO_PULLUP_PULLDOWN; + } else if (flags & gpio::FLAG_PULLUP) { + pull_mode = GPIO_PULLUP_ONLY; + } else if (flags & gpio::FLAG_PULLDOWN) { + pull_mode = GPIO_PULLDOWN_ONLY; + } + gpio_set_pull_mode(pin_, pull_mode); +} + +bool IDFInternalGPIOPin::digital_read() { return bool(gpio_get_level(pin_)) != inverted_; } +void IDFInternalGPIOPin::digital_write(bool value) { gpio_set_level(pin_, value != inverted_ ? 1 : 0); } +void IDFInternalGPIOPin::detach_interrupt() const { gpio_intr_disable(pin_); } + } // namespace esp32 using namespace esp32; @@ -114,6 +124,19 @@ void IRAM_ATTR ISRInternalGPIOPin::digital_write(bool value) { void IRAM_ATTR ISRInternalGPIOPin::clear_interrupt() { // not supported } +void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) { + auto *arg = reinterpret_cast(arg_); + gpio_set_direction(arg->pin, flags_to_mode(flags)); + gpio_pull_mode_t pull_mode = GPIO_FLOATING; + if (flags & (gpio::FLAG_PULLUP | gpio::FLAG_PULLDOWN)) { + pull_mode = GPIO_PULLUP_PULLDOWN; + } else if (flags & gpio::FLAG_PULLUP) { + pull_mode = GPIO_PULLUP_ONLY; + } else if (flags & gpio::FLAG_PULLDOWN) { + pull_mode = GPIO_PULLDOWN_ONLY; + } + gpio_set_pull_mode(arg->pin, pull_mode); +} } // namespace esphome diff --git a/esphome/components/esp32/gpio_idf.h b/esphome/components/esp32/gpio_idf.h index a99571cc46..a07d11378a 100644 --- a/esphome/components/esp32/gpio_idf.h +++ b/esphome/components/esp32/gpio_idf.h @@ -18,13 +18,12 @@ class IDFInternalGPIOPin : public InternalGPIOPin { bool digital_read() override; void digital_write(bool value) override; std::string dump_summary() const override; - void detach_interrupt() const override { gpio_intr_disable(pin_); } + void detach_interrupt() const override; ISRInternalGPIOPin to_isr() const override; uint8_t get_pin() const override { return (uint8_t) pin_; } bool is_inverted() const override { return inverted_; } protected: - static gpio_mode_t flags_to_mode(gpio::Flags flags); void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; gpio_num_t pin_; diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index e3d52f345a..e647b74a8f 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -18,7 +18,6 @@ from esphome.core import CORE from esphome.components.esp32 import add_idf_sdkconfig_option DEPENDENCIES = ["esp32"] -AUTO_LOAD = ["xiaomi_ble", "ruuvi_ble"] CONF_ESP32_BLE_ID = "esp32_ble_id" CONF_SCAN_PARAMETERS = "scan_parameters" diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index 7f3aebe238..2b1890267f 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -2,10 +2,8 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins from esphome.const import ( - CONF_DISABLED_BY_DEFAULT, CONF_FREQUENCY, CONF_ID, - CONF_NAME, CONF_PIN, CONF_SCL, CONF_SDA, @@ -17,8 +15,9 @@ from esphome.const import ( ) from esphome.core import CORE from esphome.components.esp32 import add_idf_sdkconfig_option +from esphome.cpp_helpers import setup_entity -DEPENDENCIES = ["esp32", "api"] +DEPENDENCIES = ["esp32"] esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera") ESP32Camera = esp32_camera_ns.class_("ESP32Camera", cg.PollingComponent, cg.EntityBase) @@ -63,11 +62,9 @@ CONF_TEST_PATTERN = "test_pattern" camera_range_param = cv.int_range(min=-2, max=2) -CONFIG_SCHEMA = cv.Schema( +CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(ESP32Camera), - cv.Required(CONF_NAME): cv.string, - cv.Optional(CONF_DISABLED_BY_DEFAULT, default=False): cv.boolean, cv.Required(CONF_DATA_PINS): cv.All( [pins.internal_gpio_input_pin_number], cv.Length(min=8, max=8) ), @@ -127,8 +124,8 @@ SETTERS = { async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID], config[CONF_NAME]) - cg.add(var.set_disabled_by_default(config[CONF_DISABLED_BY_DEFAULT])) + var = cg.new_Pvariable(config[CONF_ID]) + await setup_entity(var, config) await cg.register_component(var, config) for key, setter in SETTERS.items(): diff --git a/esphome/components/esp32_camera/esp32_camera.cpp b/esphome/components/esp32_camera/esp32_camera.cpp index babfda4113..6f93532f47 100644 --- a/esphome/components/esp32_camera/esp32_camera.cpp +++ b/esphome/components/esp32_camera/esp32_camera.cpp @@ -45,6 +45,7 @@ void ESP32Camera::dump_config() { auto conf = this->config_; ESP_LOGCONFIG(TAG, "ESP32 Camera:"); ESP_LOGCONFIG(TAG, " Name: %s", this->name_.c_str()); + ESP_LOGCONFIG(TAG, " Internal: %s", YESNO(this->internal_)); #ifdef USE_ARDUINO ESP_LOGCONFIG(TAG, " Board Has PSRAM: %s", YESNO(psramFound())); #endif // USE_ARDUINO @@ -185,6 +186,7 @@ ESP32Camera::ESP32Camera(const std::string &name) : EntityBase(name) { global_esp32_camera = this; } +ESP32Camera::ESP32Camera() : ESP32Camera("") {} void ESP32Camera::set_data_pins(std::array pins) { this->config_.pin_d0 = pins[0]; this->config_.pin_d1 = pins[1]; diff --git a/esphome/components/esp32_camera/esp32_camera.h b/esphome/components/esp32_camera/esp32_camera.h index d0445607a4..84f8d9cbea 100644 --- a/esphome/components/esp32_camera/esp32_camera.h +++ b/esphome/components/esp32_camera/esp32_camera.h @@ -54,6 +54,7 @@ enum ESP32CameraFrameSize { class ESP32Camera : public Component, public EntityBase { public: ESP32Camera(const std::string &name); + ESP32Camera(); void set_data_pins(std::array pins); void set_vsync_pin(uint8_t pin); void set_href_pin(uint8_t pin); diff --git a/esphome/components/esp32_camera_web_server/__init__.py b/esphome/components/esp32_camera_web_server/__init__.py new file mode 100644 index 0000000000..d8afea27b4 --- /dev/null +++ b/esphome/components/esp32_camera_web_server/__init__.py @@ -0,0 +1,28 @@ +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import CONF_ID, CONF_PORT, CONF_MODE + +CODEOWNERS = ["@ayufan"] +DEPENDENCIES = ["esp32_camera"] +MULTI_CONF = True + +esp32_camera_web_server_ns = cg.esphome_ns.namespace("esp32_camera_web_server") +CameraWebServer = esp32_camera_web_server_ns.class_("CameraWebServer", cg.Component) +Mode = esp32_camera_web_server_ns.enum("Mode") + +MODES = {"STREAM": Mode.STREAM, "SNAPSHOT": Mode.SNAPSHOT} + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(CameraWebServer), + cv.Required(CONF_PORT): cv.port, + cv.Required(CONF_MODE): cv.enum(MODES, upper=True), + }, +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + server = cg.new_Pvariable(config[CONF_ID]) + cg.add(server.set_port(config[CONF_PORT])) + cg.add(server.set_mode(config[CONF_MODE])) + await cg.register_component(server, config) diff --git a/esphome/components/esp32_camera_web_server/camera_web_server.cpp b/esphome/components/esp32_camera_web_server/camera_web_server.cpp new file mode 100644 index 0000000000..c9a684c7e5 --- /dev/null +++ b/esphome/components/esp32_camera_web_server/camera_web_server.cpp @@ -0,0 +1,240 @@ +#ifdef USE_ESP32 + +#include "camera_web_server.h" +#include "esphome/core/application.h" +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include "esphome/core/util.h" + +#include +#include +#include + +namespace esphome { +namespace esp32_camera_web_server { + +static const int IMAGE_REQUEST_TIMEOUT = 2000; +static const char *const TAG = "esp32_camera_web_server"; + +#define PART_BOUNDARY "123456789000000000000987654321" +#define CONTENT_TYPE "image/jpeg" +#define CONTENT_LENGTH "Content-Length" + +static const char *const STREAM_HEADER = + "HTTP/1.1 200\r\nAccess-Control-Allow-Origin: *\r\nContent-Type: multipart/x-mixed-replace;boundary=" PART_BOUNDARY + "\r\n"; +static const char *const STREAM_500 = "HTTP/1.1 500\r\nContent-Type: text/plain\r\n\r\nNo frames send.\r\n"; +static const char *const STREAM_BOUNDARY = "\r\n--" PART_BOUNDARY "\r\n"; +static const char *const STREAM_PART = "Content-Type: " CONTENT_TYPE "\r\n" CONTENT_LENGTH ": %u\r\n\r\n"; + +CameraWebServer::CameraWebServer() {} + +CameraWebServer::~CameraWebServer() {} + +void CameraWebServer::setup() { + if (!esp32_camera::global_esp32_camera || esp32_camera::global_esp32_camera->is_failed()) { + this->mark_failed(); + return; + } + + this->semaphore_ = xSemaphoreCreateBinary(); + + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + config.server_port = this->port_; + config.ctrl_port = this->port_; + config.max_open_sockets = 1; + config.backlog_conn = 2; + + if (httpd_start(&this->httpd_, &config) != ESP_OK) { + mark_failed(); + return; + } + + httpd_uri_t uri = { + .uri = "/", + .method = HTTP_GET, + .handler = [](struct httpd_req *req) { return ((CameraWebServer *) req->user_ctx)->handler_(req); }, + .user_ctx = this}; + + httpd_register_uri_handler(this->httpd_, &uri); + + esp32_camera::global_esp32_camera->add_image_callback([this](std::shared_ptr image) { + if (this->running_) { + this->image_ = std::move(image); + xSemaphoreGive(this->semaphore_); + } + }); +} + +void CameraWebServer::on_shutdown() { + this->running_ = false; + this->image_ = nullptr; + httpd_stop(this->httpd_); + this->httpd_ = nullptr; + vSemaphoreDelete(this->semaphore_); + this->semaphore_ = nullptr; +} + +void CameraWebServer::dump_config() { + ESP_LOGCONFIG(TAG, "ESP32 Camera Web Server:"); + ESP_LOGCONFIG(TAG, " Port: %d", this->port_); + if (this->mode_ == STREAM) + ESP_LOGCONFIG(TAG, " Mode: stream"); + else + ESP_LOGCONFIG(TAG, " Mode: snapshot"); + + if (this->is_failed()) { + ESP_LOGE(TAG, " Setup Failed"); + } +} + +float CameraWebServer::get_setup_priority() const { return setup_priority::LATE; } + +void CameraWebServer::loop() { + if (!this->running_) { + this->image_ = nullptr; + } +} + +std::shared_ptr CameraWebServer::wait_for_image_() { + std::shared_ptr image; + image.swap(this->image_); + + if (!image) { + // retry as we might still be fetching image + xSemaphoreTake(this->semaphore_, IMAGE_REQUEST_TIMEOUT / portTICK_PERIOD_MS); + image.swap(this->image_); + } + + return image; +} + +esp_err_t CameraWebServer::handler_(struct httpd_req *req) { + esp_err_t res = ESP_FAIL; + + this->image_ = nullptr; + this->running_ = true; + + switch (this->mode_) { + case STREAM: + res = this->streaming_handler_(req); + break; + + case SNAPSHOT: + res = this->snapshot_handler_(req); + break; + } + + this->running_ = false; + this->image_ = nullptr; + return res; +} + +static esp_err_t httpd_send_all(httpd_req_t *r, const char *buf, size_t buf_len) { + int ret; + + while (buf_len > 0) { + ret = httpd_send(r, buf, buf_len); + if (ret < 0) { + return ESP_FAIL; + } + buf += ret; + buf_len -= ret; + } + return ESP_OK; +} + +esp_err_t CameraWebServer::streaming_handler_(struct httpd_req *req) { + esp_err_t res = ESP_OK; + char part_buf[64]; + + // This manually constructs HTTP response to avoid chunked encoding + // which is not supported by some clients + + res = httpd_send_all(req, STREAM_HEADER, strlen(STREAM_HEADER)); + if (res != ESP_OK) { + ESP_LOGW(TAG, "STREAM: failed to set HTTP header"); + return res; + } + + uint32_t last_frame = millis(); + uint32_t frames = 0; + + while (res == ESP_OK && this->running_) { + if (esp32_camera::global_esp32_camera != nullptr) { + esp32_camera::global_esp32_camera->request_stream(); + } + + auto image = this->wait_for_image_(); + + if (!image) { + ESP_LOGW(TAG, "STREAM: failed to acquire frame"); + res = ESP_FAIL; + } + if (res == ESP_OK) { + res = httpd_send_all(req, STREAM_BOUNDARY, strlen(STREAM_BOUNDARY)); + } + if (res == ESP_OK) { + size_t hlen = snprintf(part_buf, 64, STREAM_PART, image->get_data_length()); + res = httpd_send_all(req, part_buf, hlen); + } + if (res == ESP_OK) { + res = httpd_send_all(req, (const char *) image->get_data_buffer(), image->get_data_length()); + } + if (res == ESP_OK) { + frames++; + int64_t frame_time = millis() - last_frame; + last_frame = millis(); + + ESP_LOGD(TAG, "MJPG: %uB %ums (%.1ffps)", (uint32_t) image->get_data_length(), (uint32_t) frame_time, + 1000.0 / (uint32_t) frame_time); + } + } + + if (!frames) { + res = httpd_send_all(req, STREAM_500, strlen(STREAM_500)); + } + + ESP_LOGI(TAG, "STREAM: closed. Frames: %u", frames); + + return res; +} + +esp_err_t CameraWebServer::snapshot_handler_(struct httpd_req *req) { + esp_err_t res = ESP_OK; + + if (esp32_camera::global_esp32_camera != nullptr) { + esp32_camera::global_esp32_camera->request_image(); + } + + auto image = this->wait_for_image_(); + + if (!image) { + ESP_LOGW(TAG, "SNAPSHOT: failed to acquire frame"); + httpd_resp_send_500(req); + res = ESP_FAIL; + return res; + } + + res = httpd_resp_set_type(req, CONTENT_TYPE); + if (res != ESP_OK) { + ESP_LOGW(TAG, "SNAPSHOT: failed to set HTTP response type"); + return res; + } + + httpd_resp_set_hdr(req, "Content-Disposition", "inline; filename=capture.jpg"); + + if (res == ESP_OK) { + res = httpd_resp_set_hdr(req, CONTENT_LENGTH, esphome::to_string(image->get_data_length()).c_str()); + } + if (res == ESP_OK) { + res = httpd_resp_send(req, (const char *) image->get_data_buffer(), image->get_data_length()); + } + return res; +} + +} // namespace esp32_camera_web_server +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/esp32_camera_web_server/camera_web_server.h b/esphome/components/esp32_camera_web_server/camera_web_server.h new file mode 100644 index 0000000000..df30a43ed2 --- /dev/null +++ b/esphome/components/esp32_camera_web_server/camera_web_server.h @@ -0,0 +1,51 @@ +#pragma once + +#ifdef USE_ESP32 + +#include +#include + +#include "esphome/components/esp32_camera/esp32_camera.h" +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "esphome/core/preferences.h" + +struct httpd_req; + +namespace esphome { +namespace esp32_camera_web_server { + +enum Mode { STREAM, SNAPSHOT }; + +class CameraWebServer : public Component { + public: + CameraWebServer(); + ~CameraWebServer(); + + void setup() override; + void on_shutdown() override; + void dump_config() override; + float get_setup_priority() const override; + void set_port(uint16_t port) { this->port_ = port; } + void set_mode(Mode mode) { this->mode_ = mode; } + void loop() override; + + protected: + std::shared_ptr wait_for_image_(); + esp_err_t handler_(struct httpd_req *req); + esp_err_t streaming_handler_(struct httpd_req *req); + esp_err_t snapshot_handler_(struct httpd_req *req); + + protected: + uint16_t port_{0}; + void *httpd_{nullptr}; + SemaphoreHandle_t semaphore_; + std::shared_ptr image_; + bool running_{false}; + Mode mode_{STREAM}; +}; + +} // namespace esp32_camera_web_server +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index faa9ab7df6..22bebdfe98 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -11,6 +11,7 @@ namespace esphome { namespace esp32_improv { static const char *const TAG = "esp32_improv.component"; +static const char *const ESPHOME_MY_LINK = "https://my.home-assistant.io/redirect/config_flow_start?domain=esphome"; ESP32ImprovComponent::ESP32ImprovComponent() { global_improv_component = this; } @@ -124,8 +125,13 @@ void ESP32ImprovComponent::loop() { this->cancel_timeout("wifi-connect-timeout"); this->set_state_(improv::STATE_PROVISIONED); - std::string url = "https://my.home-assistant.io/redirect/config_flow_start?domain=esphome"; - std::vector data = improv::build_rpc_response(improv::WIFI_SETTINGS, {url}); + std::vector urls = {ESPHOME_MY_LINK}; +#ifdef USE_WEBSERVER + auto ip = wifi::global_wifi_component->wifi_sta_ip(); + std::string webserver_url = "http://" + ip.str() + ":" + to_string(WEBSERVER_PORT); + urls.push_back(webserver_url); +#endif + std::vector data = improv::build_rpc_response(improv::WIFI_SETTINGS, urls); this->send_response_(data); this->set_timeout("end-service", 1000, [this] { this->service_->stop(); diff --git a/esphome/components/esp32_touch/esp32_touch.cpp b/esphome/components/esp32_touch/esp32_touch.cpp index cb72820900..b225ae1a8a 100644 --- a/esphome/components/esp32_touch/esp32_touch.cpp +++ b/esphome/components/esp32_touch/esp32_touch.cpp @@ -1,6 +1,7 @@ #ifdef USE_ESP32 #include "esp32_touch.h" +#include "esphome/core/application.h" #include "esphome/core/log.h" #include "esphome/core/hal.h" @@ -93,7 +94,6 @@ void ESP32TouchComponent::dump_config() { if (this->iir_filter_enabled_()) { ESP_LOGCONFIG(TAG, " IIR Filter: %ums", this->iir_filter_); - touch_pad_filter_start(this->iir_filter_); } else { ESP_LOGCONFIG(TAG, " IIR Filter DISABLED"); } @@ -125,6 +125,8 @@ void ESP32TouchComponent::loop() { if (should_print) { ESP_LOGD(TAG, "Touch Pad '%s' (T%u): %u", child->get_name().c_str(), child->get_touch_pad(), value); } + + App.feed_wdt(); } if (should_print) { diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index b2706bd4fb..34c792499d 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -93,12 +93,12 @@ def _arduino_check_versions(value): platform_version = value.get(CONF_PLATFORM_VERSION) if platform_version is None: if version >= cv.Version(3, 0, 0): - platform_version = ARDUINO_3_PLATFORM_VERSION + platform_version = _parse_platform_version(str(ARDUINO_3_PLATFORM_VERSION)) elif version >= cv.Version(2, 5, 0): - platform_version = ARDUINO_2_PLATFORM_VERSION + platform_version = _parse_platform_version(str(ARDUINO_2_PLATFORM_VERSION)) else: - platform_version = cv.Version(1, 8, 0) - value[CONF_PLATFORM_VERSION] = str(platform_version) + platform_version = _parse_platform_version(str(cv.Version(1, 8, 0))) + value[CONF_PLATFORM_VERSION] = platform_version if version != RECOMMENDED_ARDUINO_FRAMEWORK_VERSION: _LOGGER.warning( @@ -109,13 +109,22 @@ def _arduino_check_versions(value): return value +def _parse_platform_version(value): + try: + # if platform version is a valid version constraint, prefix the default package + cv.platformio_version_constraint(value) + return f"platformio/espressif8266 @ {value}" + except cv.Invalid: + return value + + CONF_PLATFORM_VERSION = "platform_version" ARDUINO_FRAMEWORK_SCHEMA = cv.All( cv.Schema( { cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict, cv.Optional(CONF_SOURCE): cv.string_strict, - cv.Optional(CONF_PLATFORM_VERSION): cv.string_strict, + cv.Optional(CONF_PLATFORM_VERSION): _parse_platform_version, } ), _arduino_check_versions, @@ -142,21 +151,22 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): cg.add(esp8266_ns.setup_preferences()) + cg.add_platformio_option("lib_ldf_mode", "off") + cg.add_platformio_option("board", config[CONF_BOARD]) cg.add_build_flag("-DUSE_ESP8266") cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) + cg.add_define("ESPHOME_VARIANT", "ESP8266") conf = config[CONF_FRAMEWORK] cg.add_platformio_option("framework", "arduino") cg.add_build_flag("-DUSE_ARDUINO") cg.add_build_flag("-DUSE_ESP8266_FRAMEWORK_ARDUINO") + cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION]) cg.add_platformio_option( "platform_packages", [f"platformio/framework-arduinoespressif8266 @ {conf[CONF_SOURCE]}"], ) - cg.add_platformio_option( - "platform", f"platformio/espressif8266 @ {conf[CONF_PLATFORM_VERSION]}" - ) # Default for platformio is LWIP2_LOW_MEMORY with: # - MSS=536 diff --git a/esphome/components/esp8266/core.cpp b/esphome/components/esp8266/core.cpp index b78600e7a3..51f3ca50ec 100644 --- a/esphome/components/esp8266/core.cpp +++ b/esphome/components/esp8266/core.cpp @@ -12,7 +12,7 @@ void IRAM_ATTR HOT yield() { ::yield(); } uint32_t IRAM_ATTR HOT millis() { return ::millis(); } void IRAM_ATTR HOT delay(uint32_t ms) { ::delay(ms); } uint32_t IRAM_ATTR HOT micros() { return ::micros(); } -void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { ::delayMicroseconds(us); } +void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } void arch_restart() { ESP.restart(); // NOLINT(readability-static-accessed-through-instance) // restart() doesn't always end execution diff --git a/esphome/components/esp8266/gpio.cpp b/esphome/components/esp8266/gpio.cpp index 7805889b40..2660318182 100644 --- a/esphome/components/esp8266/gpio.cpp +++ b/esphome/components/esp8266/gpio.cpp @@ -8,6 +8,29 @@ namespace esp8266 { static const char *const TAG = "esp8266"; +static int IRAM_ATTR flags_to_mode(gpio::Flags flags, uint8_t pin) { + if (flags == gpio::FLAG_INPUT) { + return INPUT; + } else if (flags == gpio::FLAG_OUTPUT) { + return OUTPUT; + } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLUP)) { + if (pin == 16) { + // GPIO16 doesn't have a pullup, so pinMode would fail. + // However, sometimes this method is called with pullup mode anyway + // for example from dallas one_wire. For those cases convert this + // to a INPUT mode. + return INPUT; + } + return INPUT_PULLUP; + } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLDOWN)) { + return INPUT_PULLDOWN_16; + } else if (flags == (gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN)) { + return OUTPUT_OPEN_DRAIN; + } else { + return 0; + } +} + struct ISRPinArg { uint8_t pin; bool inverted; @@ -43,28 +66,7 @@ void ESP8266GPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::Int attachInterruptArg(pin_, func, arg, arduino_mode); } void ESP8266GPIOPin::pin_mode(gpio::Flags flags) { - uint8_t mode; - if (flags == gpio::FLAG_INPUT) { - mode = INPUT; - } else if (flags == gpio::FLAG_OUTPUT) { - mode = OUTPUT; - } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLUP)) { - mode = INPUT_PULLUP; - if (pin_ == 16) { - // GPIO16 doesn't have a pullup, so pinMode would fail. - // However, sometimes this method is called with pullup mode anyway - // for example from dallas one_wire. For those cases convert this - // to a INPUT mode. - mode = INPUT; - } - } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLDOWN)) { - mode = INPUT_PULLDOWN_16; - } else if (flags == (gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN)) { - mode = OUTPUT_OPEN_DRAIN; - } else { - return; - } - pinMode(pin_, mode); // NOLINT + pinMode(pin_, flags_to_mode(flags, pin_)); // NOLINT } std::string ESP8266GPIOPin::dump_summary() const { @@ -97,6 +99,10 @@ void IRAM_ATTR ISRInternalGPIOPin::clear_interrupt() { auto *arg = reinterpret_cast(arg_); GPIO_REG_WRITE(GPIO_STATUS_W1TC_ADDRESS, 1UL << arg->pin); } +void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) { + auto *arg = reinterpret_cast(arg_); + pinMode(arg->pin, flags_to_mode(flags, arg->pin)); // NOLINT +} } // namespace esphome diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index d55db0a7d8..00f68df2b4 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -168,7 +168,9 @@ void EthernetComponent::start_connect_() { esp_err_t err; err = tcpip_adapter_set_hostname(TCPIP_ADAPTER_IF_ETH, App.get_name().c_str()); - ESPHL_ERROR_CHECK(err, "ETH set hostname error"); + if (err != ERR_OK) { + ESP_LOGW(TAG, "tcpip_adapter_set_hostname failed: %s", esp_err_to_name(err)); + } tcpip_adapter_ip_info_t info; if (this->manual_ip_.has_value()) { diff --git a/esphome/components/external_components/__init__.py b/esphome/components/external_components/__init__.py index 110a8d95ed..e0548e8981 100644 --- a/esphome/components/external_components/__init__.py +++ b/esphome/components/external_components/__init__.py @@ -43,19 +43,27 @@ def validate_source_shorthand(value): # Regex for GitHub repo name with optional branch/tag # Note: git allows other branch/tag names as well, but never seen them used before m = re.match( - r"github://([a-zA-Z0-9\-]+)/([a-zA-Z0-9\-\._]+)(?:@([a-zA-Z0-9\-_.\./]+))?", + r"github://(?:([a-zA-Z0-9\-]+)/([a-zA-Z0-9\-\._]+)(?:@([a-zA-Z0-9\-_.\./]+))?|pr#([0-9]+))", value, ) if m is None: raise cv.Invalid( - "Source is not a file system path or in expected github://username/name[@branch-or-tag] format!" + "Source is not a file system path, in expected github://username/name[@branch-or-tag] or github://pr#1234 format!" ) - conf = { - CONF_TYPE: TYPE_GIT, - CONF_URL: f"https://github.com/{m.group(1)}/{m.group(2)}.git", - } - if m.group(3): - conf[CONF_REF] = m.group(3) + if m.group(4): + conf = { + CONF_TYPE: TYPE_GIT, + CONF_URL: "https://github.com/esphome/esphome.git", + CONF_REF: f"pull/{m.group(4)}/head", + } + else: + conf = { + CONF_TYPE: TYPE_GIT, + CONF_URL: f"https://github.com/{m.group(1)}/{m.group(2)}.git", + } + if m.group(3): + conf[CONF_REF] = m.group(3) + return SOURCE_SCHEMA(conf) diff --git a/esphome/components/ezo/ezo.cpp b/esphome/components/ezo/ezo.cpp index 81597f3466..7f7a41fb41 100644 --- a/esphome/components/ezo/ezo.cpp +++ b/esphome/components/ezo/ezo.cpp @@ -74,7 +74,7 @@ void EZOSensor::loop() { if (buf[0] != 1) return; - float val = strtof((char *) &buf[1], nullptr); + float val = parse_number((char *) &buf[1], sizeof(buf) - 1).value_or(0); this->publish_state(val); } diff --git a/esphome/components/fingerprint_grow/sensor.py b/esphome/components/fingerprint_grow/sensor.py index f359a10348..4ae670743d 100644 --- a/esphome/components/fingerprint_grow/sensor.py +++ b/esphome/components/fingerprint_grow/sensor.py @@ -8,6 +8,7 @@ from esphome.const import ( CONF_LAST_FINGER_ID, CONF_SECURITY_LEVEL, CONF_STATUS, + ENTITY_CATEGORY_DIAGNOSTIC, ICON_ACCOUNT, ICON_ACCOUNT_CHECK, ICON_DATABASE, @@ -26,30 +27,36 @@ CONFIG_SCHEMA = cv.Schema( icon=ICON_FINGERPRINT, accuracy_decimals=0, state_class=STATE_CLASS_NONE, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), cv.Optional(CONF_STATUS): sensor.sensor_schema( accuracy_decimals=0, state_class=STATE_CLASS_NONE, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), cv.Optional(CONF_CAPACITY): sensor.sensor_schema( icon=ICON_DATABASE, accuracy_decimals=0, state_class=STATE_CLASS_NONE, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), cv.Optional(CONF_SECURITY_LEVEL): sensor.sensor_schema( icon=ICON_SECURITY, accuracy_decimals=0, state_class=STATE_CLASS_NONE, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), cv.Optional(CONF_LAST_FINGER_ID): sensor.sensor_schema( icon=ICON_ACCOUNT, accuracy_decimals=0, state_class=STATE_CLASS_NONE, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), cv.Optional(CONF_LAST_CONFIDENCE): sensor.sensor_schema( icon=ICON_ACCOUNT_CHECK, accuracy_decimals=0, state_class=STATE_CLASS_NONE, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), } ) diff --git a/esphome/components/homeassistant/sensor/homeassistant_sensor.cpp b/esphome/components/homeassistant/sensor/homeassistant_sensor.cpp index b6f2acdbe4..f5e73c8854 100644 --- a/esphome/components/homeassistant/sensor/homeassistant_sensor.cpp +++ b/esphome/components/homeassistant/sensor/homeassistant_sensor.cpp @@ -10,7 +10,7 @@ static const char *const TAG = "homeassistant.sensor"; void HomeassistantSensor::setup() { api::global_api_server->subscribe_home_assistant_state( this->entity_id_, this->attribute_, [this](const std::string &state) { - auto val = parse_float(state); + auto val = parse_number(state); if (!val.has_value()) { ESP_LOGW(TAG, "Can't convert '%s' to number!", state.c_str()); this->publish_state(NAN); diff --git a/esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.cpp b/esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.cpp index cf6c9eea65..bd1c82c96b 100644 --- a/esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.cpp +++ b/esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.cpp @@ -44,7 +44,7 @@ void HrxlMaxsonarWrComponent::check_buffer_() { if (this->buffer_.length() == MAX_DATA_LENGTH_BYTES && this->buffer_[0] == 'R' && this->buffer_.back() == static_cast(ASCII_CR)) { - int millimeters = strtol(this->buffer_.substr(1, MAX_DATA_LENGTH_BYTES - 2).c_str(), nullptr, 10); + int millimeters = parse_number(this->buffer_.substr(1, MAX_DATA_LENGTH_BYTES - 2)).value_or(0); float meters = float(millimeters) / 1000.0; ESP_LOGV(TAG, "Distance from sensor: %d mm, %f m", millimeters, meters); this->publish_state(meters); diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index 6e249c4247..774d6a0f91 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -96,6 +96,8 @@ async def to_code(config): if CORE.is_esp32: cg.add_library("WiFiClientSecure", None) cg.add_library("HTTPClient", None) + if CORE.is_esp8266: + cg.add_library("ESP8266HTTPClient", None) await cg.register_component(var, config) diff --git a/esphome/components/htu21d/htu21d.cpp b/esphome/components/htu21d/htu21d.cpp index b53284ae3f..a38ec73019 100644 --- a/esphome/components/htu21d/htu21d.cpp +++ b/esphome/components/htu21d/htu21d.cpp @@ -9,8 +9,8 @@ static const char *const TAG = "htu21d"; static const uint8_t HTU21D_ADDRESS = 0x40; static const uint8_t HTU21D_REGISTER_RESET = 0xFE; -static const uint8_t HTU21D_REGISTER_TEMPERATURE = 0xE3; -static const uint8_t HTU21D_REGISTER_HUMIDITY = 0xE5; +static const uint8_t HTU21D_REGISTER_TEMPERATURE = 0xF3; +static const uint8_t HTU21D_REGISTER_HUMIDITY = 0xF5; static const uint8_t HTU21D_REGISTER_STATUS = 0xE7; void HTU21DComponent::setup() { diff --git a/esphome/components/i2c/i2c.h b/esphome/components/i2c/i2c.h index 7ee4cdd811..50a0b3ae50 100644 --- a/esphome/components/i2c/i2c.h +++ b/esphome/components/i2c/i2c.h @@ -1,6 +1,7 @@ #pragma once #include "i2c_bus.h" +#include "esphome/core/helpers.h" #include "esphome/core/optional.h" #include #include @@ -32,16 +33,8 @@ class I2CRegister { // like ntohs/htons but without including networking headers. // ("i2c" byte order is big-endian) -inline uint16_t i2ctohs(uint16_t i2cshort) { - union { - uint16_t x; - uint8_t y[2]; - } conv; - conv.x = i2cshort; - return ((uint16_t) conv.y[0] << 8) | ((uint16_t) conv.y[1] << 0); -} - -inline uint16_t htoi2cs(uint16_t hostshort) { return i2ctohs(hostshort); } +inline uint16_t i2ctohs(uint16_t i2cshort) { return convert_big_endian(i2cshort); } +inline uint16_t htoi2cs(uint16_t hostshort) { return convert_big_endian(hostshort); } class I2CDevice { public: diff --git a/esphome/components/improv/improv.cpp b/esphome/components/improv/improv.cpp index 4f6ed7702d..759962b51a 100644 --- a/esphome/components/improv/improv.cpp +++ b/esphome/components/improv/improv.cpp @@ -2,27 +2,32 @@ namespace improv { -ImprovCommand parse_improv_data(const std::vector &data) { - return parse_improv_data(data.data(), data.size()); +ImprovCommand parse_improv_data(const std::vector &data, bool check_checksum) { + return parse_improv_data(data.data(), data.size(), check_checksum); } -ImprovCommand parse_improv_data(const uint8_t *data, size_t length) { +ImprovCommand parse_improv_data(const uint8_t *data, size_t length, bool check_checksum) { + ImprovCommand improv_command; Command command = (Command) data[0]; uint8_t data_length = data[1]; - if (data_length != length - 3) { - return {.command = UNKNOWN}; + if (data_length != length - 2 - check_checksum) { + improv_command.command = UNKNOWN; + return improv_command; } - uint8_t checksum = data[length - 1]; + if (check_checksum) { + uint8_t checksum = data[length - 1]; - uint32_t calculated_checksum = 0; - for (uint8_t i = 0; i < length - 1; i++) { - calculated_checksum += data[i]; - } + uint32_t calculated_checksum = 0; + for (uint8_t i = 0; i < length - 1; i++) { + calculated_checksum += data[i]; + } - if ((uint8_t) calculated_checksum != checksum) { - return {.command = BAD_CHECKSUM}; + if ((uint8_t) calculated_checksum != checksum) { + improv_command.command = BAD_CHECKSUM; + return improv_command; + } } if (command == WIFI_SETTINGS) { @@ -39,12 +44,11 @@ ImprovCommand parse_improv_data(const uint8_t *data, size_t length) { return {.command = command, .ssid = ssid, .password = password}; } - return { - .command = command, - }; + improv_command.command = command; + return improv_command; } -std::vector build_rpc_response(Command command, const std::vector &datum) { +std::vector build_rpc_response(Command command, const std::vector &datum, bool add_checksum) { std::vector out; uint32_t length = 0; out.push_back(command); @@ -56,17 +60,19 @@ std::vector build_rpc_response(Command command, const std::vector build_rpc_response(Command command, const std::vector &datum) { +#ifdef ARDUINO +std::vector build_rpc_response(Command command, const std::vector &datum, bool add_checksum) { std::vector out; uint32_t length = 0; out.push_back(command); @@ -78,14 +84,16 @@ std::vector build_rpc_response(Command command, const std::vector #include @@ -38,6 +38,8 @@ enum Command : uint8_t { UNKNOWN = 0x00, WIFI_SETTINGS = 0x01, IDENTIFY = 0x02, + GET_CURRENT_STATE = 0x02, + GET_DEVICE_INFO = 0x03, BAD_CHECKSUM = 0xFF, }; @@ -49,12 +51,13 @@ struct ImprovCommand { std::string password; }; -ImprovCommand parse_improv_data(const std::vector &data); -ImprovCommand parse_improv_data(const uint8_t *data, size_t length); +ImprovCommand parse_improv_data(const std::vector &data, bool check_checksum = true); +ImprovCommand parse_improv_data(const uint8_t *data, size_t length, bool check_checksum = true); -std::vector build_rpc_response(Command command, const std::vector &datum); -#ifdef USE_ARDUINO -std::vector build_rpc_response(Command command, const std::vector &datum); -#endif // USE_ARDUINO +std::vector build_rpc_response(Command command, const std::vector &datum, + bool add_checksum = true); +#ifdef ARDUINO +std::vector build_rpc_response(Command command, const std::vector &datum, bool add_checksum = true); +#endif // ARDUINO } // namespace improv diff --git a/esphome/components/improv_serial/__init__.py b/esphome/components/improv_serial/__init__.py new file mode 100644 index 0000000000..b1cdc2d93e --- /dev/null +++ b/esphome/components/improv_serial/__init__.py @@ -0,0 +1,33 @@ +from esphome.const import CONF_BAUD_RATE, CONF_ID, CONF_LOGGER +import esphome.codegen as cg +import esphome.config_validation as cv +import esphome.final_validate as fv + +CODEOWNERS = ["@esphome/core"] +DEPENDENCIES = ["logger", "wifi"] +AUTO_LOAD = ["improv"] + +improv_serial_ns = cg.esphome_ns.namespace("improv_serial") + +ImprovSerialComponent = improv_serial_ns.class_("ImprovSerialComponent", cg.Component) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(ImprovSerialComponent), + } +).extend(cv.COMPONENT_SCHEMA) + + +def validate_logger_baud_rate(config): + logger_conf = fv.full_config.get()[CONF_LOGGER] + if logger_conf[CONF_BAUD_RATE] == 0: + raise cv.Invalid("improv_serial requires the logger baud_rate to be not 0") + return config + + +FINAL_VALIDATE_SCHEMA = validate_logger_baud_rate + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) diff --git a/esphome/components/improv_serial/improv_serial_component.cpp b/esphome/components/improv_serial/improv_serial_component.cpp new file mode 100644 index 0000000000..a9a7467125 --- /dev/null +++ b/esphome/components/improv_serial/improv_serial_component.cpp @@ -0,0 +1,266 @@ +#include "improv_serial_component.h" + +#include "esphome/core/application.h" +#include "esphome/core/defines.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" +#include "esphome/core/version.h" + +#include "esphome/components/logger/logger.h" + +namespace esphome { +namespace improv_serial { + +static const char *const TAG = "improv_serial"; + +void ImprovSerialComponent::setup() { + global_improv_serial_component = this; +#ifdef USE_ARDUINO + this->hw_serial_ = logger::global_logger->get_hw_serial(); +#endif +#ifdef USE_ESP_IDF + this->uart_num_ = logger::global_logger->get_uart_num(); +#endif + + if (wifi::global_wifi_component->has_sta()) { + this->state_ = improv::STATE_PROVISIONED; + } +} + +void ImprovSerialComponent::dump_config() { ESP_LOGCONFIG(TAG, "Improv Serial:"); } + +int ImprovSerialComponent::available_() { +#ifdef USE_ARDUINO + return this->hw_serial_->available(); +#endif +#ifdef USE_ESP_IDF + size_t available; + uart_get_buffered_data_len(this->uart_num_, &available); + return available; +#endif +} + +uint8_t ImprovSerialComponent::read_byte_() { + uint8_t data; +#ifdef USE_ARDUINO + this->hw_serial_->readBytes(&data, 1); +#endif +#ifdef USE_ESP_IDF + uart_read_bytes(this->uart_num_, &data, 1, 20 / portTICK_RATE_MS); +#endif + return data; +} + +void ImprovSerialComponent::write_data_(std::vector &data) { + data.push_back('\n'); +#ifdef USE_ARDUINO + this->hw_serial_->write(data.data(), data.size()); +#endif +#ifdef USE_ESP_IDF + uart_write_bytes(this->uart_num_, data.data(), data.size()); +#endif +} + +void ImprovSerialComponent::loop() { + const uint32_t now = millis(); + if (now - this->last_read_byte_ > 50) { + this->rx_buffer_.clear(); + this->last_read_byte_ = now; + } + + while (this->available_()) { + uint8_t byte = this->read_byte_(); + if (this->parse_improv_serial_byte_(byte)) { + this->last_read_byte_ = now; + } else { + this->rx_buffer_.clear(); + } + } + + if (this->state_ == improv::STATE_PROVISIONING) { + if (wifi::global_wifi_component->is_connected()) { + wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(), + this->connecting_sta_.get_password()); + this->connecting_sta_ = {}; + this->cancel_timeout("wifi-connect-timeout"); + this->set_state_(improv::STATE_PROVISIONED); + + std::vector url = this->build_rpc_settings_response_(improv::WIFI_SETTINGS); + this->send_response_(url); + } + } +} + +std::vector ImprovSerialComponent::build_rpc_settings_response_(improv::Command command) { + std::vector urls; +#ifdef USE_WEBSERVER + auto ip = wifi::global_wifi_component->wifi_sta_ip(); + std::string webserver_url = "http://" + ip.str() + ":" + to_string(WEBSERVER_PORT); + urls.push_back(webserver_url); +#endif + std::vector data = improv::build_rpc_response(command, urls, false); + return data; +} + +std::vector ImprovSerialComponent::build_version_info_() { + std::vector infos = {"ESPHome", ESPHOME_VERSION, ESPHOME_VARIANT, App.get_name()}; + std::vector data = improv::build_rpc_response(improv::GET_DEVICE_INFO, infos, false); + return data; +}; + +bool ImprovSerialComponent::parse_improv_serial_byte_(uint8_t byte) { + size_t at = this->rx_buffer_.size(); + this->rx_buffer_.push_back(byte); + ESP_LOGD(TAG, "Improv Serial byte: 0x%02X", byte); + const uint8_t *raw = &this->rx_buffer_[0]; + if (at == 0) + return byte == 'I'; + if (at == 1) + return byte == 'M'; + if (at == 2) + return byte == 'P'; + if (at == 3) + return byte == 'R'; + if (at == 4) + return byte == 'O'; + if (at == 5) + return byte == 'V'; + + if (at == 6) + return byte == IMPROV_SERIAL_VERSION; + + if (at == 7) + return true; + uint8_t type = raw[7]; + + if (at == 8) + return true; + uint8_t data_len = raw[8]; + + if (at < 8 + data_len) + return true; + + if (at == 8 + data_len) + return true; + + if (at == 8 + data_len + 1) { + uint8_t checksum = 0x00; + for (uint8_t i = 0; i < at; i++) + checksum += raw[i]; + + if (checksum != byte) { + ESP_LOGW(TAG, "Error decoding Improv payload"); + this->set_error_(improv::ERROR_INVALID_RPC); + return false; + } + + if (type == TYPE_RPC) { + this->set_error_(improv::ERROR_NONE); + auto command = improv::parse_improv_data(&raw[9], data_len, false); + return this->parse_improv_payload_(command); + } + } + + // If we got here then the command coming is is improv, but not an RPC command + return false; +} + +bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command) { + switch (command.command) { + case improv::WIFI_SETTINGS: { + wifi::WiFiAP sta{}; + sta.set_ssid(command.ssid); + sta.set_password(command.password); + this->connecting_sta_ = sta; + + wifi::global_wifi_component->set_sta(sta); + wifi::global_wifi_component->start_scanning(); + this->set_state_(improv::STATE_PROVISIONING); + ESP_LOGD(TAG, "Received Improv wifi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(), + command.password.c_str()); + + auto f = std::bind(&ImprovSerialComponent::on_wifi_connect_timeout_, this); + this->set_timeout("wifi-connect-timeout", 30000, f); + return true; + } + case improv::GET_CURRENT_STATE: + this->set_state_(this->state_); + if (this->state_ == improv::STATE_PROVISIONED) { + std::vector url = this->build_rpc_settings_response_(improv::GET_CURRENT_STATE); + this->send_response_(url); + } + return true; + case improv::GET_DEVICE_INFO: { + std::vector info = this->build_version_info_(); + this->send_response_(info); + return true; + } + default: { + ESP_LOGW(TAG, "Unknown Improv payload"); + this->set_error_(improv::ERROR_UNKNOWN_RPC); + return false; + } + } +} + +void ImprovSerialComponent::set_state_(improv::State state) { + this->state_ = state; + + std::vector data = {'I', 'M', 'P', 'R', 'O', 'V'}; + data.resize(11); + data[6] = IMPROV_SERIAL_VERSION; + data[7] = TYPE_CURRENT_STATE; + data[8] = 1; + data[9] = state; + + uint8_t checksum = 0x00; + for (uint8_t d : data) + checksum += d; + data[10] = checksum; + + this->write_data_(data); +} + +void ImprovSerialComponent::set_error_(improv::Error error) { + std::vector data = {'I', 'M', 'P', 'R', 'O', 'V'}; + data.resize(11); + data[6] = IMPROV_SERIAL_VERSION; + data[7] = TYPE_ERROR_STATE; + data[8] = 1; + data[9] = error; + + uint8_t checksum = 0x00; + for (uint8_t d : data) + checksum += d; + data[10] = checksum; + this->write_data_(data); +} + +void ImprovSerialComponent::send_response_(std::vector &response) { + std::vector data = {'I', 'M', 'P', 'R', 'O', 'V'}; + data.resize(9); + data[6] = IMPROV_SERIAL_VERSION; + data[7] = TYPE_RPC_RESPONSE; + data[8] = response.size(); + data.insert(data.end(), response.begin(), response.end()); + + uint8_t checksum = 0x00; + for (uint8_t d : data) + checksum += d; + data.push_back(checksum); + + this->write_data_(data); +} + +void ImprovSerialComponent::on_wifi_connect_timeout_() { + this->set_error_(improv::ERROR_UNABLE_TO_CONNECT); + this->set_state_(improv::STATE_AUTHORIZED); + ESP_LOGW(TAG, "Timed out trying to connect to given WiFi network"); + wifi::global_wifi_component->clear_sta(); +} + +ImprovSerialComponent *global_improv_serial_component = // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +} // namespace improv_serial +} // namespace esphome diff --git a/esphome/components/improv_serial/improv_serial_component.h b/esphome/components/improv_serial/improv_serial_component.h new file mode 100644 index 0000000000..539674e2d3 --- /dev/null +++ b/esphome/components/improv_serial/improv_serial_component.h @@ -0,0 +1,69 @@ +#pragma once + +#include "esphome/components/improv/improv.h" +#include "esphome/components/wifi/wifi_component.h" +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" + +#ifdef USE_ARDUINO +#include +#endif +#ifdef USE_ESP_IDF +#include +#endif + +namespace esphome { +namespace improv_serial { + +enum ImprovSerialType : uint8_t { + TYPE_CURRENT_STATE = 0x01, + TYPE_ERROR_STATE = 0x02, + TYPE_RPC = 0x03, + TYPE_RPC_RESPONSE = 0x04 +}; + +static const uint8_t IMPROV_SERIAL_VERSION = 1; + +class ImprovSerialComponent : public Component { + public: + void setup() override; + void loop() override; + void dump_config() override; + + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + protected: + bool parse_improv_serial_byte_(uint8_t byte); + bool parse_improv_payload_(improv::ImprovCommand &command); + + void set_state_(improv::State state); + void set_error_(improv::Error error); + void send_response_(std::vector &response); + void on_wifi_connect_timeout_(); + + std::vector build_rpc_settings_response_(improv::Command command); + std::vector build_version_info_(); + + int available_(); + uint8_t read_byte_(); + void write_data_(std::vector &data); + +#ifdef USE_ARDUINO + HardwareSerial *hw_serial_{nullptr}; +#endif +#ifdef USE_ESP_IDF + uart_port_t uart_num_; +#endif + + std::vector rx_buffer_; + uint32_t last_read_byte_{0}; + wifi::WiFiAP connecting_sta_; + improv::State state_{improv::STATE_AUTHORIZED}; +}; + +extern ImprovSerialComponent + *global_improv_serial_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +} // namespace improv_serial +} // namespace esphome diff --git a/esphome/components/inkbird_ibsth1_mini/sensor.py b/esphome/components/inkbird_ibsth1_mini/sensor.py index 0ab9f8b3e0..aa11fb3172 100644 --- a/esphome/components/inkbird_ibsth1_mini/sensor.py +++ b/esphome/components/inkbird_ibsth1_mini/sensor.py @@ -9,6 +9,7 @@ from esphome.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, + ENTITY_CATEGORY_DIAGNOSTIC, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_PERCENT, @@ -53,6 +54,7 @@ CONFIG_SCHEMA = ( accuracy_decimals=0, device_class=DEVICE_CLASS_BATTERY, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), } ) diff --git a/esphome/components/light/addressable_light.h b/esphome/components/light/addressable_light.h index 2b2c0ca7e4..8302239d6a 100644 --- a/esphome/components/light/addressable_light.h +++ b/esphome/components/light/addressable_light.h @@ -87,7 +87,7 @@ class AddressableLight : public LightOutput, public Component { void mark_shown_() { #ifdef USE_POWER_SUPPLY for (const auto &c : *this) { - if (c.get().is_on()) { + if (c.get_red_raw() > 0 || c.get_green_raw() > 0 || c.get_blue_raw() > 0 || c.get_white_raw() > 0) { this->power_.request(); return; } diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 97ad4c2cb9..11c0733701 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -221,7 +221,7 @@ UARTSelection Logger::get_uart() const { return this->uart_; } void Logger::add_on_log_callback(std::function &&callback) { this->log_callback_.add(std::move(callback)); } -float Logger::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; } +float Logger::get_setup_priority() const { return setup_priority::BUS + 500.0f; } const char *const LOG_LEVELS[] = {"NONE", "ERROR", "WARN", "INFO", "CONFIG", "DEBUG", "VERBOSE", "VERY_VERBOSE"}; #ifdef USE_ESP32 const char *const UART_SELECTIONS[] = {"UART0", "UART1", "UART2"}; diff --git a/esphome/components/max7219digit/display.py b/esphome/components/max7219digit/display.py index e1ca128699..2753f70eef 100644 --- a/esphome/components/max7219digit/display.py +++ b/esphome/components/max7219digit/display.py @@ -13,10 +13,20 @@ CONF_SCROLL_DELAY = "scroll_delay" CONF_SCROLL_ENABLE = "scroll_enable" CONF_SCROLL_MODE = "scroll_mode" CONF_REVERSE_ENABLE = "reverse_enable" +CONF_NUM_CHIP_LINES = "num_chip_lines" +CONF_CHIP_LINES_STYLE = "chip_lines_style" +integration_ns = cg.esphome_ns.namespace("max7219digit") +ChipLinesStyle = integration_ns.enum("ChipLinesStyle") +CHIP_LINES_STYLE = { + "ZIGZAG": ChipLinesStyle.ZIGZAG, + "SNAKE": ChipLinesStyle.SNAKE, +} + +ScrollMode = integration_ns.enum("ScrollMode") SCROLL_MODES = { - "CONTINUOUS": 0, - "STOP": 1, + "CONTINUOUS": ScrollMode.CONTINUOUS, + "STOP": ScrollMode.STOP, } CHIP_MODES = { @@ -37,6 +47,10 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(MAX7219Component), cv.Optional(CONF_NUM_CHIPS, default=4): cv.int_range(min=1, max=255), + cv.Optional(CONF_NUM_CHIP_LINES, default=1): cv.int_range(min=1, max=255), + cv.Optional(CONF_CHIP_LINES_STYLE, default="SNAKE"): cv.enum( + CHIP_LINES_STYLE, upper=True + ), cv.Optional(CONF_INTENSITY, default=15): cv.int_range(min=0, max=15), cv.Optional(CONF_ROTATE_CHIP, default="0"): cv.enum(CHIP_MODES, upper=True), cv.Optional(CONF_SCROLL_MODE, default="CONTINUOUS"): cv.enum( @@ -67,6 +81,8 @@ async def to_code(config): await display.register_display(var, config) cg.add(var.set_num_chips(config[CONF_NUM_CHIPS])) + cg.add(var.set_num_chip_lines(config[CONF_NUM_CHIP_LINES])) + cg.add(var.set_chip_lines_style(config[CONF_CHIP_LINES_STYLE])) cg.add(var.set_intensity(config[CONF_INTENSITY])) cg.add(var.set_chip_orientation(config[CONF_ROTATE_CHIP])) cg.add(var.set_scroll_speed(config[CONF_SCROLL_SPEED])) diff --git a/esphome/components/max7219digit/max7219digit.cpp b/esphome/components/max7219digit/max7219digit.cpp index 4fedd3d312..0f86ac635c 100644 --- a/esphome/components/max7219digit/max7219digit.cpp +++ b/esphome/components/max7219digit/max7219digit.cpp @@ -26,9 +26,12 @@ void MAX7219Component::setup() { ESP_LOGCONFIG(TAG, "Setting up MAX7219_DIGITS..."); this->spi_setup(); this->stepsleft_ = 0; - this->max_displaybuffer_.reserve(500); // Create base space to write buffer - // Initialize buffer with 0 for display so all non written pixels are blank - this->max_displaybuffer_.resize(this->num_chips_ * 8, 0); + for (int chip_line = 0; chip_line < this->num_chip_lines_; chip_line++) { + std::vector vec(1); + this->max_displaybuffer_.push_back(vec); + // Initialize buffer with 0 for display so all non written pixels are blank + this->max_displaybuffer_[chip_line].resize(get_width_internal(), 0); + } // let's assume the user has all 8 digits connected, only important in daisy chained setups anyway this->send_to_all_(MAX7219_REGISTER_SCAN_LIMIT, 7); // let's use our own ASCII -> led pattern encoding @@ -46,6 +49,8 @@ void MAX7219Component::setup() { void MAX7219Component::dump_config() { ESP_LOGCONFIG(TAG, "MAX7219DIGIT:"); ESP_LOGCONFIG(TAG, " Number of Chips: %u", this->num_chips_); + ESP_LOGCONFIG(TAG, " Number of Chips Lines: %u", this->num_chip_lines_); + ESP_LOGCONFIG(TAG, " Chips Lines Style : %u", this->chip_lines_style_); ESP_LOGCONFIG(TAG, " Intensity: %u", this->intensity_); ESP_LOGCONFIG(TAG, " Scroll Mode: %u", this->scroll_mode_); ESP_LOGCONFIG(TAG, " Scroll Speed: %u", this->scroll_speed_); @@ -59,19 +64,19 @@ void MAX7219Component::loop() { uint32_t now = millis(); // check if the buffer has shrunk past the current position since last update - if ((this->max_displaybuffer_.size() >= this->old_buffer_size_ + 3) || - (this->max_displaybuffer_.size() <= this->old_buffer_size_ - 3)) { + if ((this->max_displaybuffer_[0].size() >= this->old_buffer_size_ + 3) || + (this->max_displaybuffer_[0].size() <= this->old_buffer_size_ - 3)) { this->stepsleft_ = 0; this->display(); - this->old_buffer_size_ = this->max_displaybuffer_.size(); + this->old_buffer_size_ = this->max_displaybuffer_[0].size(); } // Reset the counter back to 0 when full string has been displayed. - if (this->stepsleft_ > this->max_displaybuffer_.size()) + if (this->stepsleft_ > this->max_displaybuffer_[0].size()) this->stepsleft_ = 0; // Return if there is no need to scroll or scroll is off - if (!this->scroll_ || (this->max_displaybuffer_.size() <= this->num_chips_ * 8)) { + if (!this->scroll_ || (this->max_displaybuffer_[0].size() <= get_width_internal())) { this->display(); return; } @@ -82,8 +87,8 @@ void MAX7219Component::loop() { } // Dwell time at end of string in case of stop at end - if (this->scroll_mode_ == 1) { - if (this->stepsleft_ >= this->max_displaybuffer_.size() - this->num_chips_ * 8 + 1) { + if (this->scroll_mode_ == ScrollMode::STOP) { + if (this->stepsleft_ >= this->max_displaybuffer_[0].size() - get_width_internal() + 1) { if (now - this->last_scroll_ >= this->scroll_dwell_) { this->stepsleft_ = 0; this->last_scroll_ = now; @@ -107,30 +112,53 @@ void MAX7219Component::display() { // Run this routine for the rows of every chip 8x row 0 top to 7 bottom // Fill the pixel parameter with display data // Send the data to the chip - for (uint8_t i = 0; i < this->num_chips_; i++) { - for (uint8_t j = 0; j < 8; j++) { - if (this->reverse_) { - pixels[j] = this->max_displaybuffer_[(this->num_chips_ - i - 1) * 8 + j]; - } else { - pixels[j] = this->max_displaybuffer_[i * 8 + j]; + for (uint8_t chip = 0; chip < this->num_chips_ / this->num_chip_lines_; chip++) { + for (uint8_t chip_line = 0; chip_line < this->num_chip_lines_; chip_line++) { + for (uint8_t j = 0; j < 8; j++) { + bool reverse = + chip_line % 2 != 0 && this->chip_lines_style_ == ChipLinesStyle::SNAKE ? !this->reverse_ : this->reverse_; + if (reverse) { + pixels[j] = + this->max_displaybuffer_[chip_line][(this->num_chips_ / this->num_chip_lines_ - chip - 1) * 8 + j]; + } else { + pixels[j] = this->max_displaybuffer_[chip_line][chip * 8 + j]; + } } + if (chip_line % 2 != 0 && this->chip_lines_style_ == ChipLinesStyle::SNAKE) + this->orientation_ = orientation_180_(); + this->send64pixels(chip_line * this->num_chips_ / this->num_chip_lines_ + chip, pixels); + if (chip_line % 2 != 0 && this->chip_lines_style_ == ChipLinesStyle::SNAKE) + this->orientation_ = orientation_180_(); } - this->send64pixels(i, pixels); + } +} + +uint8_t MAX7219Component::orientation_180_() { + switch (this->orientation_) { + case 0: + return 2; + case 1: + return 3; + case 2: + return 0; + case 3: + return 1; + default: + return 0; } } int MAX7219Component::get_height_internal() { - return 8; // TO BE DONE -> STACK TWO DISPLAYS ON TOP OF EACH OTHER - // TO BE DONE -> CREATE Virtual size of screen and scroll + return 8 * this->num_chip_lines_; // TO BE DONE -> CREATE Virtual size of screen and scroll } -int MAX7219Component::get_width_internal() { return this->num_chips_ * 8; } - -size_t MAX7219Component::get_buffer_length_() { return this->num_chips_ * 8; } +int MAX7219Component::get_width_internal() { return this->num_chips_ / this->num_chip_lines_ * 8; } void HOT MAX7219Component::draw_absolute_pixel_internal(int x, int y, Color color) { - if (x + 1 > this->max_displaybuffer_.size()) { // Extend the display buffer in case required - this->max_displaybuffer_.resize(x + 1, this->bckgrnd_); + if (x + 1 > this->max_displaybuffer_[0].size()) { // Extend the display buffer in case required + for (int chip_line = 0; chip_line < this->num_chip_lines_; chip_line++) { + this->max_displaybuffer_[chip_line].resize(x + 1, this->bckgrnd_); + } } if ((y >= this->get_height_internal()) || (y < 0) || (x < 0)) // If pixel is outside display then dont draw @@ -140,9 +168,9 @@ void HOT MAX7219Component::draw_absolute_pixel_internal(int x, int y, Color colo uint8_t subpos = y; // Y is starting at 0 top left if (color.is_on()) { - this->max_displaybuffer_[pos] |= (1 << subpos); + this->max_displaybuffer_[subpos / 8][pos] |= (1 << subpos % 8); } else { - this->max_displaybuffer_[pos] &= ~(1 << subpos); + this->max_displaybuffer_[subpos / 8][pos] &= ~(1 << subpos % 8); } } @@ -158,8 +186,10 @@ void MAX7219Component::send_to_all_(uint8_t a_register, uint8_t data) { } void MAX7219Component::update() { this->update_ = true; - this->max_displaybuffer_.clear(); - this->max_displaybuffer_.resize(this->num_chips_ * 8, this->bckgrnd_); + for (int chip_line = 0; chip_line < this->num_chip_lines_; chip_line++) { + this->max_displaybuffer_[chip_line].clear(); + this->max_displaybuffer_[chip_line].resize(get_width_internal(), this->bckgrnd_); + } if (this->writer_local_.has_value()) // insert Labda function if available (*this->writer_local_)(*this); } @@ -175,7 +205,7 @@ void MAX7219Component::turn_on_off(bool on_off) { } } -void MAX7219Component::scroll(bool on_off, uint8_t mode, uint16_t speed, uint16_t delay, uint16_t dwell) { +void MAX7219Component::scroll(bool on_off, ScrollMode mode, uint16_t speed, uint16_t delay, uint16_t dwell) { this->set_scroll(on_off); this->set_scroll_mode(mode); this->set_scroll_speed(speed); @@ -183,7 +213,7 @@ void MAX7219Component::scroll(bool on_off, uint8_t mode, uint16_t speed, uint16_ this->set_scroll_delay(delay); } -void MAX7219Component::scroll(bool on_off, uint8_t mode) { +void MAX7219Component::scroll(bool on_off, ScrollMode mode) { this->set_scroll(on_off); this->set_scroll_mode(mode); } @@ -196,24 +226,26 @@ void MAX7219Component::intensity(uint8_t intensity) { void MAX7219Component::scroll(bool on_off) { this->set_scroll(on_off); } void MAX7219Component::scroll_left() { - if (this->update_) { - this->max_displaybuffer_.push_back(this->bckgrnd_); - for (uint16_t i = 0; i < this->stepsleft_; i++) { - this->max_displaybuffer_.push_back(this->max_displaybuffer_.front()); - this->max_displaybuffer_.erase(this->max_displaybuffer_.begin()); - this->update_ = false; + for (int chip_line = 0; chip_line < this->num_chip_lines_; chip_line++) { + if (this->update_) { + this->max_displaybuffer_[chip_line].push_back(this->bckgrnd_); + for (uint16_t i = 0; i < this->stepsleft_; i++) { + this->max_displaybuffer_[chip_line].push_back(this->max_displaybuffer_[chip_line].front()); + this->max_displaybuffer_[chip_line].erase(this->max_displaybuffer_[chip_line].begin()); + } + } else { + this->max_displaybuffer_[chip_line].push_back(this->max_displaybuffer_[chip_line].front()); + this->max_displaybuffer_[chip_line].erase(this->max_displaybuffer_[chip_line].begin()); } - } else { - this->max_displaybuffer_.push_back(this->max_displaybuffer_.front()); - this->max_displaybuffer_.erase(this->max_displaybuffer_.begin()); } + this->update_ = false; this->stepsleft_++; } void MAX7219Component::send_char(uint8_t chip, uint8_t data) { // get this character from PROGMEM for (uint8_t i = 0; i < 8; i++) - this->max_displaybuffer_[chip * 8 + i] = progmem_read_byte(&MAX7219_DOT_MATRIX_FONT[data][i]); + this->max_displaybuffer_[0][chip * 8 + i] = progmem_read_byte(&MAX7219_DOT_MATRIX_FONT[data][i]); } // end of send_char // send one character (data) to position (chip) diff --git a/esphome/components/max7219digit/max7219digit.h b/esphome/components/max7219digit/max7219digit.h index 02fe8b6f42..3bf934632f 100644 --- a/esphome/components/max7219digit/max7219digit.h +++ b/esphome/components/max7219digit/max7219digit.h @@ -12,6 +12,16 @@ namespace esphome { namespace max7219digit { +enum ChipLinesStyle { + ZIGZAG = 0, + SNAKE, +}; + +enum ScrollMode { + CONTINUOUS = 0, + STOP, +}; + class MAX7219Component; using max7219_writer_t = std::function; @@ -46,20 +56,22 @@ class MAX7219Component : public PollingComponent, void set_intensity(uint8_t intensity) { this->intensity_ = intensity; }; void set_num_chips(uint8_t num_chips) { this->num_chips_ = num_chips; }; + void set_num_chip_lines(uint8_t num_chip_lines) { this->num_chip_lines_ = num_chip_lines; }; + void set_chip_lines_style(ChipLinesStyle chip_lines_style) { this->chip_lines_style_ = chip_lines_style; }; void set_chip_orientation(uint8_t rotate) { this->orientation_ = rotate; }; void set_scroll_speed(uint16_t speed) { this->scroll_speed_ = speed; }; void set_scroll_dwell(uint16_t dwell) { this->scroll_dwell_ = dwell; }; void set_scroll_delay(uint16_t delay) { this->scroll_delay_ = delay; }; void set_scroll(bool on_off) { this->scroll_ = on_off; }; - void set_scroll_mode(uint8_t mode) { this->scroll_mode_ = mode; }; + void set_scroll_mode(ScrollMode mode) { this->scroll_mode_ = mode; }; void set_reverse(bool on_off) { this->reverse_ = on_off; }; void send_char(uint8_t chip, uint8_t data); void send64pixels(uint8_t chip, const uint8_t pixels[8]); void scroll_left(); - void scroll(bool on_off, uint8_t mode, uint16_t speed, uint16_t delay, uint16_t dwell); - void scroll(bool on_off, uint8_t mode); + void scroll(bool on_off, ScrollMode mode, uint16_t speed, uint16_t delay, uint16_t dwell); + void scroll(bool on_off, ScrollMode mode); void scroll(bool on_off); void intensity(uint8_t intensity); @@ -84,9 +96,12 @@ class MAX7219Component : public PollingComponent, protected: void send_byte_(uint8_t a_register, uint8_t data); void send_to_all_(uint8_t a_register, uint8_t data); + uint8_t orientation_180_(); uint8_t intensity_; /// Intensity of the display from 0 to 15 (most) uint8_t num_chips_; + uint8_t num_chip_lines_; + ChipLinesStyle chip_lines_style_; bool scroll_; bool reverse_; bool update_{false}; @@ -94,11 +109,11 @@ class MAX7219Component : public PollingComponent, uint16_t scroll_delay_; uint16_t scroll_dwell_; uint16_t old_buffer_size_ = 0; - uint8_t scroll_mode_; + ScrollMode scroll_mode_; bool invert_ = false; uint8_t orientation_; uint8_t bckgrnd_ = 0x0; - std::vector max_displaybuffer_; + std::vector> max_displaybuffer_; uint32_t last_scroll_ = 0; uint16_t stepsleft_; size_t get_buffer_length_(); diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index 631af9eba9..915c640b06 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -2,6 +2,7 @@ #include "esphome/core/defines.h" #include "esphome/core/version.h" #include "esphome/core/application.h" +#include "esphome/core/log.h" #ifdef USE_API #include "esphome/components/api/api_server.h" @@ -13,13 +14,16 @@ namespace esphome { namespace mdns { +static const char *const TAG = "mdns"; + #ifndef WEBSERVER_PORT #define WEBSERVER_PORT 80 // NOLINT #endif -std::vector MDNSComponent::compile_services_() { - std::vector res; +void MDNSComponent::compile_records_() { + this->hostname_ = App.get_name(); + this->services_.clear(); #ifdef USE_API if (api::global_api_server != nullptr) { MDNSService service{}; @@ -50,7 +54,7 @@ std::vector MDNSComponent::compile_services_() { service.txt_records.push_back({"package_import_url", dashboard_import::get_package_import_url()}); #endif - res.push_back(service); + this->services_.push_back(service); } #endif // USE_API @@ -60,11 +64,11 @@ std::vector MDNSComponent::compile_services_() { service.service_type = "_prometheus-http"; service.proto = "_tcp"; service.port = WEBSERVER_PORT; - res.push_back(service); + this->services_.push_back(service); } #endif - if (res.empty()) { + if (this->services_.empty()) { // Publish "http" service if not using native API // This is just to have *some* mDNS service so that .local resolution works MDNSService service{}; @@ -72,11 +76,21 @@ std::vector MDNSComponent::compile_services_() { service.proto = "_tcp"; service.port = WEBSERVER_PORT; service.txt_records.push_back({"version", ESPHOME_VERSION}); - res.push_back(service); + this->services_.push_back(service); + } +} + +void MDNSComponent::dump_config() { + ESP_LOGCONFIG(TAG, "mDNS:"); + ESP_LOGCONFIG(TAG, " Hostname: %s", this->hostname_.c_str()); + ESP_LOGV(TAG, " Services:"); + for (const auto &service : this->services_) { + ESP_LOGV(TAG, " - %s, %s, %d", service.service_type.c_str(), service.proto.c_str(), service.port); + for (const auto &record : service.txt_records) { + ESP_LOGV(TAG, " TXT: %s = %s", record.key.c_str(), record.value.c_str()); + } } - return res; } -std::string MDNSComponent::compile_hostname_() { return App.get_name(); } } // namespace mdns } // namespace esphome diff --git a/esphome/components/mdns/mdns_component.h b/esphome/components/mdns/mdns_component.h index 679fb1a768..45614d509a 100644 --- a/esphome/components/mdns/mdns_component.h +++ b/esphome/components/mdns/mdns_component.h @@ -26,6 +26,7 @@ struct MDNSService { class MDNSComponent : public Component { public: void setup() override; + void dump_config() override; #if defined(USE_ESP8266) && defined(USE_ARDUINO) void loop() override; @@ -33,8 +34,9 @@ class MDNSComponent : public Component { float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } protected: - std::vector compile_services_(); - std::string compile_hostname_(); + std::vector services_{}; + std::string hostname_; + void compile_records_(); }; } // namespace mdns diff --git a/esphome/components/mdns/mdns_esp32_arduino.cpp b/esphome/components/mdns/mdns_esp32_arduino.cpp index 4d13b7321a..6a66beef92 100644 --- a/esphome/components/mdns/mdns_esp32_arduino.cpp +++ b/esphome/components/mdns/mdns_esp32_arduino.cpp @@ -7,13 +7,12 @@ namespace esphome { namespace mdns { -static const char *const TAG = "mdns"; - void MDNSComponent::setup() { - MDNS.begin(compile_hostname_().c_str()); + this->compile_records_(); - auto services = compile_services_(); - for (const auto &service : services) { + MDNS.begin(this->hostname_.c_str()); + + for (const auto &service : this->services_) { MDNS.addService(service.service_type.c_str(), service.proto.c_str(), service.port); for (const auto &record : service.txt_records) { MDNS.addServiceTxt(service.service_type.c_str(), service.proto.c_str(), record.key.c_str(), record.value.c_str()); diff --git a/esphome/components/mdns/mdns_esp8266.cpp b/esphome/components/mdns/mdns_esp8266.cpp index 1a73e4b5b3..ff305f907a 100644 --- a/esphome/components/mdns/mdns_esp8266.cpp +++ b/esphome/components/mdns/mdns_esp8266.cpp @@ -9,14 +9,13 @@ namespace esphome { namespace mdns { -static const char *const TAG = "mdns"; - void MDNSComponent::setup() { - network::IPAddress addr = network::get_ip_address(); - MDNS.begin(compile_hostname_().c_str(), (uint32_t) addr); + this->compile_records_(); - auto services = compile_services_(); - for (const auto &service : services) { + network::IPAddress addr = network::get_ip_address(); + MDNS.begin(this->hostname_.c_str(), (uint32_t) addr); + + for (const auto &service : this->services_) { // Strip the leading underscore from the proto and service_type. While it is // part of the wire protocol to have an underscore, and for example ESP-IDF // expects the underscore to be there, the ESP8266 implementation always adds diff --git a/esphome/components/mdns/mdns_esp_idf.cpp b/esphome/components/mdns/mdns_esp_idf.cpp index 17874f1ffe..40d9f1d5f3 100644 --- a/esphome/components/mdns/mdns_esp_idf.cpp +++ b/esphome/components/mdns/mdns_esp_idf.cpp @@ -11,18 +11,19 @@ namespace mdns { static const char *const TAG = "mdns"; void MDNSComponent::setup() { + this->compile_records_(); + esp_err_t err = mdns_init(); if (err != ESP_OK) { - ESP_LOGW(TAG, "MDNS init failed: %s", esp_err_to_name(err)); + ESP_LOGW(TAG, "mDNS init failed: %s", esp_err_to_name(err)); this->mark_failed(); return; } - mdns_hostname_set(compile_hostname_().c_str()); - mdns_instance_name_set(compile_hostname_().c_str()); + mdns_hostname_set(this->hostname_.c_str()); + mdns_instance_name_set(this->hostname_.c_str()); - auto services = compile_services_(); - for (const auto &service : services) { + for (const auto &service : this->services_) { std::vector txt_records; for (const auto &record : service.txt_records) { mdns_txt_item_t it{}; diff --git a/esphome/components/midea/adapter.cpp b/esphome/components/midea/ac_adapter.cpp similarity index 98% rename from esphome/components/midea/adapter.cpp rename to esphome/components/midea/ac_adapter.cpp index a3f19dbda8..2837713c35 100644 --- a/esphome/components/midea/adapter.cpp +++ b/esphome/components/midea/ac_adapter.cpp @@ -1,10 +1,11 @@ #ifdef USE_ARDUINO #include "esphome/core/log.h" -#include "adapter.h" +#include "ac_adapter.h" namespace esphome { namespace midea { +namespace ac { const char *const Constants::TAG = "midea"; const std::string Constants::FREEZE_PROTECTION = "freeze protection"; @@ -171,6 +172,7 @@ void Converters::to_climate_traits(ClimateTraits &traits, const dudanov::midea:: traits.add_supported_custom_preset(Constants::FREEZE_PROTECTION); } +} // namespace ac } // namespace midea } // namespace esphome diff --git a/esphome/components/midea/adapter.h b/esphome/components/midea/ac_adapter.h similarity index 95% rename from esphome/components/midea/adapter.h rename to esphome/components/midea/ac_adapter.h index 2497cbbe5b..c17894ae31 100644 --- a/esphome/components/midea/adapter.h +++ b/esphome/components/midea/ac_adapter.h @@ -2,12 +2,15 @@ #ifdef USE_ARDUINO +// MideaUART #include + #include "esphome/components/climate/climate_traits.h" -#include "appliance_base.h" +#include "air_conditioner.h" namespace esphome { namespace midea { +namespace ac { using MideaMode = dudanov::midea::ac::Mode; using MideaSwingMode = dudanov::midea::ac::SwingMode; @@ -41,6 +44,7 @@ class Converters { static void to_climate_traits(ClimateTraits &traits, const dudanov::midea::ac::Capabilities &capabilities); }; +} // namespace ac } // namespace midea } // namespace esphome diff --git a/esphome/components/midea/automations.h b/esphome/components/midea/ac_automations.h similarity index 97% rename from esphome/components/midea/automations.h rename to esphome/components/midea/ac_automations.h index 5b638286ac..d4ed2e7168 100644 --- a/esphome/components/midea/automations.h +++ b/esphome/components/midea/ac_automations.h @@ -7,6 +7,7 @@ namespace esphome { namespace midea { +namespace ac { template class MideaActionBase : public Action { public: @@ -55,6 +56,7 @@ template class PowerOffAction : public MideaActionBase { void play(Ts... x) override { this->parent_->do_power_off(); } }; +} // namespace ac } // namespace midea } // namespace esphome diff --git a/esphome/components/midea/air_conditioner.cpp b/esphome/components/midea/air_conditioner.cpp index 103b852936..dd48f640a2 100644 --- a/esphome/components/midea/air_conditioner.cpp +++ b/esphome/components/midea/air_conditioner.cpp @@ -2,13 +2,11 @@ #include "esphome/core/log.h" #include "air_conditioner.h" -#include "adapter.h" -#ifdef USE_REMOTE_TRANSMITTER -#include "midea_ir.h" -#endif +#include "ac_adapter.h" namespace esphome { namespace midea { +namespace ac { static void set_sensor(Sensor *sensor, float value) { if (sensor != nullptr && (!sensor->has_state() || sensor->get_raw_state() != value)) @@ -122,7 +120,7 @@ void AirConditioner::dump_config() { void AirConditioner::do_follow_me(float temperature, bool beeper) { #ifdef USE_REMOTE_TRANSMITTER IrFollowMeData data(static_cast(lroundf(temperature)), beeper); - this->transmit_ir(data); + this->transmitter_.transmit(data); #else ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component"); #endif @@ -131,7 +129,7 @@ void AirConditioner::do_follow_me(float temperature, bool beeper) { void AirConditioner::do_swing_step() { #ifdef USE_REMOTE_TRANSMITTER IrSpecialData data(0x01); - this->transmit_ir(data); + this->transmitter_.transmit(data); #else ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component"); #endif @@ -143,13 +141,14 @@ void AirConditioner::do_display_toggle() { } else { #ifdef USE_REMOTE_TRANSMITTER IrSpecialData data(0x08); - this->transmit_ir(data); + this->transmitter_.transmit(data); #else ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component"); #endif } } +} // namespace ac } // namespace midea } // namespace esphome diff --git a/esphome/components/midea/air_conditioner.h b/esphome/components/midea/air_conditioner.h index 8dfb9dcb3d..a6023b78bb 100644 --- a/esphome/components/midea/air_conditioner.h +++ b/esphome/components/midea/air_conditioner.h @@ -2,17 +2,25 @@ #ifdef USE_ARDUINO +// MideaUART #include + #include "appliance_base.h" #include "esphome/components/sensor/sensor.h" namespace esphome { namespace midea { +namespace ac { using sensor::Sensor; using climate::ClimateCall; +using climate::ClimatePreset; +using climate::ClimateTraits; +using climate::ClimateMode; +using climate::ClimateSwingMode; +using climate::ClimateFanMode; -class AirConditioner : public ApplianceBase { +class AirConditioner : public ApplianceBase, public climate::Climate { public: void dump_config() override; void set_outdoor_temperature_sensor(Sensor *sensor) { this->outdoor_sensor_ = sensor; } @@ -31,15 +39,26 @@ class AirConditioner : public ApplianceBase void do_beeper_off() { this->set_beeper_feedback(false); } void do_power_on() { this->base_.setPowerState(true); } void do_power_off() { this->base_.setPowerState(false); } + void set_supported_modes(const std::set &modes) { this->supported_modes_ = modes; } + void set_supported_swing_modes(const std::set &modes) { this->supported_swing_modes_ = modes; } + void set_supported_presets(const std::set &presets) { this->supported_presets_ = presets; } + void set_custom_presets(const std::set &presets) { this->supported_custom_presets_ = presets; } + void set_custom_fan_modes(const std::set &modes) { this->supported_custom_fan_modes_ = modes; } protected: void control(const ClimateCall &call) override; ClimateTraits traits() override; + std::set supported_modes_{}; + std::set supported_swing_modes_{}; + std::set supported_presets_{}; + std::set supported_custom_presets_{}; + std::set supported_custom_fan_modes_{}; Sensor *outdoor_sensor_{nullptr}; Sensor *humidity_sensor_{nullptr}; Sensor *power_sensor_{nullptr}; }; +} // namespace ac } // namespace midea } // namespace esphome diff --git a/esphome/components/midea/appliance_base.h b/esphome/components/midea/appliance_base.h index 88a722e389..060cbd996b 100644 --- a/esphome/components/midea/appliance_base.h +++ b/esphome/components/midea/appliance_base.h @@ -2,84 +2,97 @@ #ifdef USE_ARDUINO +// MideaUART +#include +#include + +// Include global defines +#include "esphome/core/defines.h" + #include "esphome/core/component.h" #include "esphome/core/log.h" #include "esphome/components/uart/uart.h" #include "esphome/components/climate/climate.h" -#ifdef USE_REMOTE_TRANSMITTER -#include "esphome/components/remote_base/midea_protocol.h" -#include "esphome/components/remote_transmitter/remote_transmitter.h" -#endif -#include -#include +#include "ir_transmitter.h" namespace esphome { namespace midea { -using climate::ClimatePreset; -using climate::ClimateTraits; -using climate::ClimateMode; -using climate::ClimateSwingMode; -using climate::ClimateFanMode; +/* Stream from UART component */ +class UARTStream : public Stream { + public: + void set_uart(uart::UARTComponent *uart) { this->uart_ = uart; } -template -class ApplianceBase : public Component, public uart::UARTDevice, public climate::Climate, public Stream { + /* Stream interface implementation */ + + int available() override { return this->uart_->available(); } + int read() override { + uint8_t data; + this->uart_->read_byte(&data); + return data; + } + int peek() override { + uint8_t data; + this->uart_->peek_byte(&data); + return data; + } + size_t write(uint8_t data) override { + this->uart_->write_byte(data); + return 1; + } + size_t write(const uint8_t *data, size_t size) override { + this->uart_->write_array(data, size); + return size; + } + void flush() override { this->uart_->flush(); } + + protected: + uart::UARTComponent *uart_; +}; + +template class ApplianceBase : public Component { static_assert(std::is_base_of::value, "T must derive from dudanov::midea::ApplianceBase class"); public: ApplianceBase() { - this->base_.setStream(this); + this->base_.setStream(&this->stream_); this->base_.addOnStateCallback(std::bind(&ApplianceBase::on_status_change, this)); dudanov::midea::ApplianceBase::setLogger( [](int level, const char *tag, int line, const String &format, va_list args) { esp_log_vprintf_(level, tag, line, format.c_str(), args); }); } - bool can_proceed() override { - return this->base_.getAutoconfStatus() != dudanov::midea::AutoconfStatus::AUTOCONF_PROGRESS; - } - float get_setup_priority() const override { return setup_priority::BEFORE_CONNECTION; } - void setup() override { this->base_.setup(); } - void loop() override { this->base_.loop(); } + +#ifdef USE_REMOTE_TRANSMITTER + void set_transmitter(RemoteTransmitterBase *transmitter) { this->transmitter_.set_transmitter(transmitter); } +#endif + + /* UART communication */ + + void set_uart_parent(uart::UARTComponent *parent) { this->stream_.set_uart(parent); } void set_period(uint32_t ms) { this->base_.setPeriod(ms); } void set_response_timeout(uint32_t ms) { this->base_.setTimeout(ms); } void set_request_attempts(uint32_t attempts) { this->base_.setNumAttempts(attempts); } + + /* Component methods */ + + void setup() override { this->base_.setup(); } + void loop() override { this->base_.loop(); } + float get_setup_priority() const override { return setup_priority::BEFORE_CONNECTION; } + bool can_proceed() override { + return this->base_.getAutoconfStatus() != dudanov::midea::AutoconfStatus::AUTOCONF_PROGRESS; + } + void set_beeper_feedback(bool state) { this->base_.setBeeper(state); } void set_autoconf(bool value) { this->base_.setAutoconf(value); } - void set_supported_modes(const std::set &modes) { this->supported_modes_ = modes; } - void set_supported_swing_modes(const std::set &modes) { this->supported_swing_modes_ = modes; } - void set_supported_presets(const std::set &presets) { this->supported_presets_ = presets; } - void set_custom_presets(const std::set &presets) { this->supported_custom_presets_ = presets; } - void set_custom_fan_modes(const std::set &modes) { this->supported_custom_fan_modes_ = modes; } virtual void on_status_change() = 0; -#ifdef USE_REMOTE_TRANSMITTER - void set_transmitter(remote_transmitter::RemoteTransmitterComponent *transmitter) { - this->transmitter_ = transmitter; - } - void transmit_ir(remote_base::MideaData &data) { - data.finalize(); - auto transmit = this->transmitter_->transmit(); - remote_base::MideaProtocol().encode(transmit.get_data(), data); - transmit.perform(); - } -#endif - - int available() override { return uart::UARTDevice::available(); } - int read() override { return uart::UARTDevice::read(); } - int peek() override { return uart::UARTDevice::peek(); } - void flush() override { uart::UARTDevice::flush(); } - size_t write(uint8_t data) override { return uart::UARTDevice::write(data); } protected: T base_; - std::set supported_modes_{}; - std::set supported_swing_modes_{}; - std::set supported_presets_{}; - std::set supported_custom_presets_{}; - std::set supported_custom_fan_modes_{}; + UARTStream stream_; #ifdef USE_REMOTE_TRANSMITTER - remote_transmitter::RemoteTransmitterComponent *transmitter_{nullptr}; + IrTransmitter transmitter_; #endif }; diff --git a/esphome/components/midea/climate.py b/esphome/components/midea/climate.py index 08e82025b6..46c0019efa 100644 --- a/esphome/components/midea/climate.py +++ b/esphome/components/midea/climate.py @@ -40,9 +40,9 @@ AUTO_LOAD = ["sensor"] CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature" CONF_POWER_USAGE = "power_usage" CONF_HUMIDITY_SETPOINT = "humidity_setpoint" -midea_ns = cg.esphome_ns.namespace("midea") -AirConditioner = midea_ns.class_("AirConditioner", climate.Climate, cg.Component) -Capabilities = midea_ns.namespace("Constants") +midea_ac_ns = cg.esphome_ns.namespace("midea").namespace("ac") +AirConditioner = midea_ac_ns.class_("AirConditioner", climate.Climate, cg.Component) +Capabilities = midea_ac_ns.namespace("Constants") def templatize(value): @@ -156,13 +156,13 @@ CONFIG_SCHEMA = cv.All( ) # Actions -FollowMeAction = midea_ns.class_("FollowMeAction", automation.Action) -DisplayToggleAction = midea_ns.class_("DisplayToggleAction", automation.Action) -SwingStepAction = midea_ns.class_("SwingStepAction", automation.Action) -BeeperOnAction = midea_ns.class_("BeeperOnAction", automation.Action) -BeeperOffAction = midea_ns.class_("BeeperOffAction", automation.Action) -PowerOnAction = midea_ns.class_("PowerOnAction", automation.Action) -PowerOffAction = midea_ns.class_("PowerOffAction", automation.Action) +FollowMeAction = midea_ac_ns.class_("FollowMeAction", automation.Action) +DisplayToggleAction = midea_ac_ns.class_("DisplayToggleAction", automation.Action) +SwingStepAction = midea_ac_ns.class_("SwingStepAction", automation.Action) +BeeperOnAction = midea_ac_ns.class_("BeeperOnAction", automation.Action) +BeeperOffAction = midea_ac_ns.class_("BeeperOffAction", automation.Action) +PowerOnAction = midea_ac_ns.class_("PowerOnAction", automation.Action) +PowerOffAction = midea_ac_ns.class_("PowerOffAction", automation.Action) MIDEA_ACTION_BASE_SCHEMA = cv.Schema( { diff --git a/esphome/components/midea/midea_ir.h b/esphome/components/midea/ir_transmitter.h similarity index 73% rename from esphome/components/midea/midea_ir.h rename to esphome/components/midea/ir_transmitter.h index abd4324bcc..34a9f8498e 100644 --- a/esphome/components/midea/midea_ir.h +++ b/esphome/components/midea/ir_transmitter.h @@ -7,6 +7,7 @@ namespace esphome { namespace midea { +using remote_base::RemoteTransmitterBase; using IrData = remote_base::MideaData; class IrFollowMeData : public IrData { @@ -38,6 +39,20 @@ class IrSpecialData : public IrData { IrSpecialData(uint8_t code) : IrData({MIDEA_TYPE_SPECIAL, code, 0xFF, 0xFF, 0xFF}) {} }; +class IrTransmitter { + public: + void set_transmitter(RemoteTransmitterBase *transmitter) { this->transmitter_ = transmitter; } + void transmit(IrData &data) { + data.finalize(); + auto transmit = this->transmitter_->transmit(); + remote_base::MideaProtocol().encode(transmit.get_data(), data); + transmit.perform(); + } + + protected: + RemoteTransmitterBase *transmitter_{nullptr}; +}; + } // namespace midea } // namespace esphome diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index 45c5bfb603..9524f9daf4 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -139,7 +139,9 @@ void Modbus::send(uint8_t address, uint8_t function_code, uint16_t start_address uint8_t payload_len, const uint8_t *payload) { static const size_t MAX_VALUES = 128; - if (number_of_entities > MAX_VALUES) { + // Only check max number of registers for standard function codes + // Some devices use non standard codes like 0x43 + if (number_of_entities > MAX_VALUES && function_code <= 0x10) { ESP_LOGE(TAG, "send too many values %d max=%zu", number_of_entities, MAX_VALUES); return; } diff --git a/esphome/components/modbus_controller/modbus_controller.h b/esphome/components/modbus_controller/modbus_controller.h index 4b5f4337db..222ebbd020 100644 --- a/esphome/components/modbus_controller/modbus_controller.h +++ b/esphome/components/modbus_controller/modbus_controller.h @@ -260,35 +260,11 @@ struct SensorItem { virtual void parse_and_publish(const std::vector &data) = 0; uint64_t getkey() const { return calc_key(register_type, start_address, offset, bitmask); } - size_t virtual get_register_size() const { - size_t size = 0; - switch (sensor_value_type) { - case SensorValueType::BIT: - size = 1; - break; - case SensorValueType::U_WORD: - case SensorValueType::S_WORD: - size = 2; - break; - case SensorValueType::U_DWORD: - case SensorValueType::S_DWORD: - case SensorValueType::U_DWORD_R: - case SensorValueType::S_DWORD_R: - case SensorValueType::FP32: - case SensorValueType::FP32_R: - size = 4; - break; - case SensorValueType::U_QWORD: - case SensorValueType::U_QWORD_R: - case SensorValueType::S_QWORD: - case SensorValueType::S_QWORD_R: - size = 8; - break; - case SensorValueType::RAW: - size = this->register_count * 2; - } - return size; + if (register_type == ModbusRegisterType::COIL || register_type == ModbusRegisterType::DISCRETE_INPUT) + return 1; + else + return register_count * 2; } }; diff --git a/esphome/components/modbus_controller/number/__init__.py b/esphome/components/modbus_controller/number/__init__.py index afb69f8798..4de0ffbcea 100644 --- a/esphome/components/modbus_controller/number/__init__.py +++ b/esphome/components/modbus_controller/number/__init__.py @@ -129,14 +129,14 @@ async def to_code(config): return_type=cg.optional.template(float), ) cg.add(var.set_template(template_)) - if CONF_WRITE_LAMBDA in config: - template_ = await cg.process_lambda( - config[CONF_WRITE_LAMBDA], - [ - (ModbusNumber.operator("ptr"), "item"), - (cg.float_, "x"), - (cg.std_vector.template(cg.uint16).operator("ref"), "payload"), - ], - return_type=cg.optional.template(float), - ) - cg.add(var.set_write_template(template_)) + if CONF_WRITE_LAMBDA in config: + template_ = await cg.process_lambda( + config[CONF_WRITE_LAMBDA], + [ + (ModbusNumber.operator("ptr"), "item"), + (cg.float_, "x"), + (cg.std_vector.template(cg.uint16).operator("ref"), "payload"), + ], + return_type=cg.optional.template(float), + ) + cg.add(var.set_write_template(template_)) diff --git a/esphome/components/modbus_controller/text_sensor/modbus_textsensor.h b/esphome/components/modbus_controller/text_sensor/modbus_textsensor.h index 28d0f0b241..77b5b9363a 100644 --- a/esphome/components/modbus_controller/text_sensor/modbus_textsensor.h +++ b/esphome/components/modbus_controller/text_sensor/modbus_textsensor.h @@ -25,13 +25,6 @@ class ModbusTextSensor : public Component, public text_sensor::TextSensor, publi this->sensor_value_type = SensorValueType::RAW; this->force_new_range = force_new_range; } - size_t get_register_size() const override { - if (sensor_value_type == SensorValueType::RAW) { - return this->response_bytes_; - } else { - return SensorItem::get_register_size(); - } - } void dump_config() override; diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 3d52dab67f..0f7d246473 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -34,6 +34,7 @@ from esphome.const import ( CONF_TOPIC, CONF_TOPIC_PREFIX, CONF_TRIGGER_ID, + CONF_USE_ABBREVIATIONS, CONF_USERNAME, CONF_WILL_MESSAGE, ) @@ -152,6 +153,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional( CONF_DISCOVERY_PREFIX, default="homeassistant" ): cv.publish_topic, + cv.Optional(CONF_USE_ABBREVIATIONS, default=True): cv.boolean, cv.Optional(CONF_BIRTH_MESSAGE): MQTT_MESSAGE_SCHEMA, cv.Optional(CONF_WILL_MESSAGE): MQTT_MESSAGE_SCHEMA, cv.Optional(CONF_SHUTDOWN_MESSAGE): MQTT_MESSAGE_SCHEMA, @@ -239,6 +241,9 @@ async def to_code(config): cg.add(var.set_topic_prefix(config[CONF_TOPIC_PREFIX])) + if config[CONF_USE_ABBREVIATIONS]: + cg.add_define("USE_MQTT_ABBREVIATIONS") + birth_message = config[CONF_BIRTH_MESSAGE] if not birth_message: cg.add(var.disable_birth_message()) diff --git a/esphome/components/mqtt/mqtt_binary_sensor.cpp b/esphome/components/mqtt/mqtt_binary_sensor.cpp index 188df0f7b9..0a161f89a1 100644 --- a/esphome/components/mqtt/mqtt_binary_sensor.cpp +++ b/esphome/components/mqtt/mqtt_binary_sensor.cpp @@ -1,6 +1,8 @@ #include "mqtt_binary_sensor.h" #include "esphome/core/log.h" +#include "mqtt_const.h" + #ifdef USE_MQTT #ifdef USE_BINARY_SENSOR @@ -29,11 +31,11 @@ MQTTBinarySensorComponent::MQTTBinarySensorComponent(binary_sensor::BinarySensor void MQTTBinarySensorComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) { if (!this->binary_sensor_->get_device_class().empty()) - root["device_class"] = this->binary_sensor_->get_device_class(); + root[MQTT_DEVICE_CLASS] = this->binary_sensor_->get_device_class(); if (this->binary_sensor_->is_status_binary_sensor()) - root["payload_on"] = mqtt::global_mqtt_client->get_availability().payload_available; + root[MQTT_PAYLOAD_ON] = mqtt::global_mqtt_client->get_availability().payload_available; if (this->binary_sensor_->is_status_binary_sensor()) - root["payload_off"] = mqtt::global_mqtt_client->get_availability().payload_not_available; + root[MQTT_PAYLOAD_OFF] = mqtt::global_mqtt_client->get_availability().payload_not_available; config.command_topic = false; } bool MQTTBinarySensorComponent::send_initial_state() { diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp index 47b6684dec..ebc708f444 100644 --- a/esphome/components/mqtt/mqtt_climate.cpp +++ b/esphome/components/mqtt/mqtt_climate.cpp @@ -1,6 +1,8 @@ #include "mqtt_climate.h" #include "esphome/core/log.h" +#include "mqtt_const.h" + #ifdef USE_MQTT #ifdef USE_CLIMATE @@ -16,14 +18,14 @@ void MQTTClimateComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryC // current_temperature_topic if (traits.get_supports_current_temperature()) { // current_temperature_topic - root["curr_temp_t"] = this->get_current_temperature_state_topic(); + root[MQTT_CURRENT_TEMPERATURE_TOPIC] = this->get_current_temperature_state_topic(); } // mode_command_topic - root["mode_cmd_t"] = this->get_mode_command_topic(); + root[MQTT_MODE_COMMAND_TOPIC] = this->get_mode_command_topic(); // mode_state_topic - root["mode_stat_t"] = this->get_mode_state_topic(); + root[MQTT_MODE_STATE_TOPIC] = this->get_mode_state_topic(); // modes - JsonArray &modes = root.createNestedArray("modes"); + JsonArray &modes = root.createNestedArray(MQTT_MODES); // sort array for nice UI in HA if (traits.supports_mode(CLIMATE_MODE_AUTO)) modes.add("auto"); @@ -41,45 +43,45 @@ void MQTTClimateComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryC if (traits.get_supports_two_point_target_temperature()) { // temperature_low_command_topic - root["temp_lo_cmd_t"] = this->get_target_temperature_low_command_topic(); + root[MQTT_TEMPERATURE_LOW_COMMAND_TOPIC] = this->get_target_temperature_low_command_topic(); // temperature_low_state_topic - root["temp_lo_stat_t"] = this->get_target_temperature_low_state_topic(); + root[MQTT_TEMPERATURE_LOW_STATE_TOPIC] = this->get_target_temperature_low_state_topic(); // temperature_high_command_topic - root["temp_hi_cmd_t"] = this->get_target_temperature_high_command_topic(); + root[MQTT_TEMPERATURE_HIGH_COMMAND_TOPIC] = this->get_target_temperature_high_command_topic(); // temperature_high_state_topic - root["temp_hi_stat_t"] = this->get_target_temperature_high_state_topic(); + root[MQTT_TEMPERATURE_HIGH_STATE_TOPIC] = this->get_target_temperature_high_state_topic(); } else { // temperature_command_topic - root["temp_cmd_t"] = this->get_target_temperature_command_topic(); + root[MQTT_TEMPERATURE_COMMAND_TOPIC] = this->get_target_temperature_command_topic(); // temperature_state_topic - root["temp_stat_t"] = this->get_target_temperature_state_topic(); + root[MQTT_TEMPERATURE_STATE_TOPIC] = this->get_target_temperature_state_topic(); } // min_temp - root["min_temp"] = traits.get_visual_min_temperature(); + root[MQTT_MIN_TEMP] = traits.get_visual_min_temperature(); // max_temp - root["max_temp"] = traits.get_visual_max_temperature(); + root[MQTT_MAX_TEMP] = traits.get_visual_max_temperature(); // temp_step root["temp_step"] = traits.get_visual_temperature_step(); // temperature units are always coerced to Celsius internally - root["temp_unit"] = "C"; + root[MQTT_TEMPERATURE_UNIT] = "C"; if (traits.supports_preset(CLIMATE_PRESET_AWAY)) { // away_mode_command_topic - root["away_mode_cmd_t"] = this->get_away_command_topic(); + root[MQTT_AWAY_MODE_COMMAND_TOPIC] = this->get_away_command_topic(); // away_mode_state_topic - root["away_mode_stat_t"] = this->get_away_state_topic(); + root[MQTT_AWAY_MODE_STATE_TOPIC] = this->get_away_state_topic(); } if (traits.get_supports_action()) { // action_topic - root["act_t"] = this->get_action_state_topic(); + root[MQTT_ACTION_TOPIC] = this->get_action_state_topic(); } if (traits.get_supports_fan_modes() || !traits.get_supported_custom_fan_modes().empty()) { // fan_mode_command_topic - root["fan_mode_cmd_t"] = this->get_fan_mode_command_topic(); + root[MQTT_FAN_MODE_COMMAND_TOPIC] = this->get_fan_mode_command_topic(); // fan_mode_state_topic - root["fan_mode_stat_t"] = this->get_fan_mode_state_topic(); + root[MQTT_FAN_MODE_STATE_TOPIC] = this->get_fan_mode_state_topic(); // fan_modes JsonArray &fan_modes = root.createNestedArray("fan_modes"); if (traits.supports_fan_mode(CLIMATE_FAN_ON)) @@ -106,9 +108,9 @@ void MQTTClimateComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryC if (traits.get_supports_swing_modes()) { // swing_mode_command_topic - root["swing_mode_cmd_t"] = this->get_swing_mode_command_topic(); + root[MQTT_SWING_MODE_COMMAND_TOPIC] = this->get_swing_mode_command_topic(); // swing_mode_state_topic - root["swing_mode_stat_t"] = this->get_swing_mode_state_topic(); + root[MQTT_SWING_MODE_STATE_TOPIC] = this->get_swing_mode_state_topic(); // swing_modes JsonArray &swing_modes = root.createNestedArray("swing_modes"); if (traits.supports_swing_mode(CLIMATE_SWING_OFF)) @@ -135,7 +137,7 @@ void MQTTClimateComponent::setup() { if (traits.get_supports_two_point_target_temperature()) { this->subscribe(this->get_target_temperature_low_command_topic(), [this](const std::string &topic, const std::string &payload) { - auto val = parse_float(payload); + auto val = parse_number(payload); if (!val.has_value()) { ESP_LOGW(TAG, "Can't convert '%s' to number!", payload.c_str()); return; @@ -146,7 +148,7 @@ void MQTTClimateComponent::setup() { }); this->subscribe(this->get_target_temperature_high_command_topic(), [this](const std::string &topic, const std::string &payload) { - auto val = parse_float(payload); + auto val = parse_number(payload); if (!val.has_value()) { ESP_LOGW(TAG, "Can't convert '%s' to number!", payload.c_str()); return; @@ -158,7 +160,7 @@ void MQTTClimateComponent::setup() { } else { this->subscribe(this->get_target_temperature_command_topic(), [this](const std::string &topic, const std::string &payload) { - auto val = parse_float(payload); + auto val = parse_number(payload); if (!val.has_value()) { ESP_LOGW(TAG, "Can't convert '%s' to number!", payload.c_str()); return; diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index 0ece4b3501..e3ae4dea50 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -7,6 +7,8 @@ #include "esphome/core/helpers.h" #include "esphome/core/version.h" +#include "mqtt_const.h" + namespace esphome { namespace mqtt { @@ -15,7 +17,7 @@ static const char *const TAG = "mqtt.component"; void MQTTComponent::set_retain(bool retain) { this->retain_ = retain; } std::string MQTTComponent::get_discovery_topic_(const MQTTDiscoveryInfo &discovery_info) const { - std::string sanitized_name = sanitize_string_allowlist(App.get_name(), HOSTNAME_CHARACTER_ALLOWLIST); + std::string sanitized_name = str_sanitize(App.get_name()); return discovery_info.prefix + "/" + this->component_type() + "/" + sanitized_name + "/" + this->get_default_object_id_() + "/config"; } @@ -69,49 +71,60 @@ bool MQTTComponent::send_discovery_() { this->send_discovery(root, config); // Fields from EntityBase - root["name"] = this->friendly_name(); + root[MQTT_NAME] = this->friendly_name(); if (this->is_disabled_by_default()) - root["enabled_by_default"] = false; + root[MQTT_ENABLED_BY_DEFAULT] = false; if (!this->get_icon().empty()) - root["icon"] = this->get_icon(); + root[MQTT_ICON] = this->get_icon(); + + switch (this->get_entity()->get_entity_category()) { + case ENTITY_CATEGORY_NONE: + break; + case ENTITY_CATEGORY_CONFIG: + root[MQTT_ENTITY_CATEGORY] = "config"; + break; + case ENTITY_CATEGORY_DIAGNOSTIC: + root[MQTT_ENTITY_CATEGORY] = "diagnostic"; + break; + } if (config.state_topic) - root["state_topic"] = this->get_state_topic_(); + root[MQTT_STATE_TOPIC] = this->get_state_topic_(); if (config.command_topic) - root["command_topic"] = this->get_command_topic_(); + root[MQTT_COMMAND_TOPIC] = this->get_command_topic_(); if (this->availability_ == nullptr) { if (!global_mqtt_client->get_availability().topic.empty()) { - root["availability_topic"] = global_mqtt_client->get_availability().topic; + root[MQTT_AVAILABILITY_TOPIC] = global_mqtt_client->get_availability().topic; if (global_mqtt_client->get_availability().payload_available != "online") - root["payload_available"] = global_mqtt_client->get_availability().payload_available; + root[MQTT_PAYLOAD_AVAILABLE] = global_mqtt_client->get_availability().payload_available; if (global_mqtt_client->get_availability().payload_not_available != "offline") - root["payload_not_available"] = global_mqtt_client->get_availability().payload_not_available; + root[MQTT_PAYLOAD_NOT_AVAILABLE] = global_mqtt_client->get_availability().payload_not_available; } } else if (!this->availability_->topic.empty()) { - root["availability_topic"] = this->availability_->topic; + root[MQTT_AVAILABILITY_TOPIC] = this->availability_->topic; if (this->availability_->payload_available != "online") - root["payload_available"] = this->availability_->payload_available; + root[MQTT_PAYLOAD_AVAILABLE] = this->availability_->payload_available; if (this->availability_->payload_not_available != "offline") - root["payload_not_available"] = this->availability_->payload_not_available; + root[MQTT_PAYLOAD_NOT_AVAILABLE] = this->availability_->payload_not_available; } const std::string &node_name = App.get_name(); std::string unique_id = this->unique_id(); if (!unique_id.empty()) { - root["unique_id"] = unique_id; + root[MQTT_UNIQUE_ID] = unique_id; } else { // default to almost-unique ID. It's a hack but the only way to get that // gorgeous device registry view. - root["unique_id"] = "ESP" + this->component_type() + this->get_default_object_id_(); + root[MQTT_UNIQUE_ID] = "ESP" + this->component_type() + this->get_default_object_id_(); } - JsonObject &device_info = root.createNestedObject("device"); - device_info["identifiers"] = get_mac_address(); - device_info["name"] = node_name; - device_info["sw_version"] = "esphome v" ESPHOME_VERSION " " + App.get_compilation_time(); - device_info["model"] = ESPHOME_BOARD; - device_info["manufacturer"] = "espressif"; + JsonObject &device_info = root.createNestedObject(MQTT_DEVICE); + device_info[MQTT_DEVICE_IDENTIFIERS] = get_mac_address(); + device_info[MQTT_DEVICE_NAME] = node_name; + device_info[MQTT_DEVICE_SW_VERSION] = "esphome v" ESPHOME_VERSION " " + App.get_compilation_time(); + device_info[MQTT_DEVICE_MODEL] = ESPHOME_BOARD; + device_info[MQTT_DEVICE_MANUFACTURER] = "espressif"; }, 0, discovery_info.retain); } @@ -123,7 +136,7 @@ bool MQTTComponent::is_discovery_enabled() const { } std::string MQTTComponent::get_default_object_id_() const { - return sanitize_string_allowlist(to_lowercase_underscore(this->friendly_name()), HOSTNAME_CHARACTER_ALLOWLIST); + return str_sanitize(str_snake_case(this->friendly_name())); } void MQTTComponent::subscribe(const std::string &topic, mqtt_callback_t callback, uint8_t qos) { diff --git a/esphome/components/mqtt/mqtt_const.h b/esphome/components/mqtt/mqtt_const.h new file mode 100644 index 0000000000..1d5e22efde --- /dev/null +++ b/esphome/components/mqtt/mqtt_const.h @@ -0,0 +1,521 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_MQTT + +namespace esphome { +namespace mqtt { + +#ifdef USE_MQTT_ABBREVIATIONS + +constexpr const char *const MQTT_ACTION_TOPIC = "act_t"; +constexpr const char *const MQTT_ACTION_TEMPLATE = "act_tpl"; +constexpr const char *const MQTT_AUTOMATION_TYPE = "atype"; +constexpr const char *const MQTT_AUX_COMMAND_TOPIC = "aux_cmd_t"; +constexpr const char *const MQTT_AUX_STATE_TEMPLATE = "aux_stat_tpl"; +constexpr const char *const MQTT_AUX_STATE_TOPIC = "aux_stat_t"; +constexpr const char *const MQTT_AVAILABILITY = "avty"; +constexpr const char *const MQTT_AVAILABILITY_MODE = "avty_mode"; +constexpr const char *const MQTT_AVAILABILITY_TOPIC = "avty_t"; +constexpr const char *const MQTT_AWAY_MODE_COMMAND_TOPIC = "away_mode_cmd_t"; +constexpr const char *const MQTT_AWAY_MODE_STATE_TEMPLATE = "away_mode_stat_tpl"; +constexpr const char *const MQTT_AWAY_MODE_STATE_TOPIC = "away_mode_stat_t"; +constexpr const char *const MQTT_BLUE_TEMPLATE = "b_tpl"; +constexpr const char *const MQTT_BRIGHTNESS_COMMAND_TOPIC = "bri_cmd_t"; +constexpr const char *const MQTT_BRIGHTNESS_SCALE = "bri_scl"; +constexpr const char *const MQTT_BRIGHTNESS_STATE_TOPIC = "bri_stat_t"; +constexpr const char *const MQTT_BRIGHTNESS_TEMPLATE = "bri_tpl"; +constexpr const char *const MQTT_BRIGHTNESS_VALUE_TEMPLATE = "bri_val_tpl"; +constexpr const char *const MQTT_COLOR_TEMP_COMMAND_TEMPLATE = "clr_temp_cmd_tpl"; +constexpr const char *const MQTT_BATTERY_LEVEL_TOPIC = "bat_lev_t"; +constexpr const char *const MQTT_BATTERY_LEVEL_TEMPLATE = "bat_lev_tpl"; +constexpr const char *const MQTT_CONFIGURATION_URL = "cu"; +constexpr const char *const MQTT_CHARGING_TOPIC = "chrg_t"; +constexpr const char *const MQTT_CHARGING_TEMPLATE = "chrg_tpl"; +constexpr const char *const MQTT_COLOR_MODE = "clrm"; +constexpr const char *const MQTT_COLOR_MODE_STATE_TOPIC = "clrm_stat_t"; +constexpr const char *const MQTT_COLOR_MODE_VALUE_TEMPLATE = "clrm_val_tpl"; +constexpr const char *const MQTT_COLOR_TEMP_COMMAND_TOPIC = "clr_temp_cmd_t"; +constexpr const char *const MQTT_COLOR_TEMP_STATE_TOPIC = "clr_temp_stat_t"; +constexpr const char *const MQTT_COLOR_TEMP_TEMPLATE = "clr_temp_tpl"; +constexpr const char *const MQTT_COLOR_TEMP_VALUE_TEMPLATE = "clr_temp_val_tpl"; +constexpr const char *const MQTT_CLEANING_TOPIC = "cln_t"; +constexpr const char *const MQTT_CLEANING_TEMPLATE = "cln_tpl"; +constexpr const char *const MQTT_COMMAND_OFF_TEMPLATE = "cmd_off_tpl"; +constexpr const char *const MQTT_COMMAND_ON_TEMPLATE = "cmd_on_tpl"; +constexpr const char *const MQTT_COMMAND_TOPIC = "cmd_t"; +constexpr const char *const MQTT_COMMAND_TEMPLATE = "cmd_tpl"; +constexpr const char *const MQTT_CODE_ARM_REQUIRED = "cod_arm_req"; +constexpr const char *const MQTT_CODE_DISARM_REQUIRED = "cod_dis_req"; +constexpr const char *const MQTT_CURRENT_TEMPERATURE_TOPIC = "curr_temp_t"; +constexpr const char *const MQTT_CURRENT_TEMPERATURE_TEMPLATE = "curr_temp_tpl"; +constexpr const char *const MQTT_DEVICE = "dev"; +constexpr const char *const MQTT_DEVICE_CLASS = "dev_cla"; +constexpr const char *const MQTT_DOCKED_TOPIC = "dock_t"; +constexpr const char *const MQTT_DOCKED_TEMPLATE = "dock_tpl"; +constexpr const char *const MQTT_ENABLED_BY_DEFAULT = "en"; +constexpr const char *const MQTT_ERROR_TOPIC = "err_t"; +constexpr const char *const MQTT_ERROR_TEMPLATE = "err_tpl"; +constexpr const char *const MQTT_FAN_SPEED_TOPIC = "fanspd_t"; +constexpr const char *const MQTT_FAN_SPEED_TEMPLATE = "fanspd_tpl"; +constexpr const char *const MQTT_FAN_SPEED_LIST = "fanspd_lst"; +constexpr const char *const MQTT_FLASH_TIME_LONG = "flsh_tlng"; +constexpr const char *const MQTT_FLASH_TIME_SHORT = "flsh_tsht"; +constexpr const char *const MQTT_EFFECT_COMMAND_TOPIC = "fx_cmd_t"; +constexpr const char *const MQTT_EFFECT_LIST = "fx_list"; +constexpr const char *const MQTT_EFFECT_STATE_TOPIC = "fx_stat_t"; +constexpr const char *const MQTT_EFFECT_TEMPLATE = "fx_tpl"; +constexpr const char *const MQTT_EFFECT_VALUE_TEMPLATE = "fx_val_tpl"; +constexpr const char *const MQTT_EXPIRE_AFTER = "exp_aft"; +constexpr const char *const MQTT_FAN_MODE_COMMAND_TEMPLATE = "fan_mode_cmd_tpl"; +constexpr const char *const MQTT_FAN_MODE_COMMAND_TOPIC = "fan_mode_cmd_t"; +constexpr const char *const MQTT_FAN_MODE_STATE_TEMPLATE = "fan_mode_stat_tpl"; +constexpr const char *const MQTT_FAN_MODE_STATE_TOPIC = "fan_mode_stat_t"; +constexpr const char *const MQTT_FORCE_UPDATE = "frc_upd"; +constexpr const char *const MQTT_GREEN_TEMPLATE = "g_tpl"; +constexpr const char *const MQTT_HOLD_COMMAND_TEMPLATE = "hold_cmd_tpl"; +constexpr const char *const MQTT_HOLD_COMMAND_TOPIC = "hold_cmd_t"; +constexpr const char *const MQTT_HOLD_STATE_TEMPLATE = "hold_stat_tpl"; +constexpr const char *const MQTT_HOLD_STATE_TOPIC = "hold_stat_t"; +constexpr const char *const MQTT_HS_COMMAND_TOPIC = "hs_cmd_t"; +constexpr const char *const MQTT_HS_STATE_TOPIC = "hs_stat_t"; +constexpr const char *const MQTT_HS_VALUE_TEMPLATE = "hs_val_tpl"; +constexpr const char *const MQTT_ICON = "ic"; +constexpr const char *const MQTT_INITIAL = "init"; +constexpr const char *const MQTT_TARGET_HUMIDITY_COMMAND_TOPIC = "hum_cmd_t"; +constexpr const char *const MQTT_TARGET_HUMIDITY_COMMAND_TEMPLATE = "hum_cmd_tpl"; +constexpr const char *const MQTT_TARGET_HUMIDITY_STATE_TOPIC = "hum_stat_t"; +constexpr const char *const MQTT_TARGET_HUMIDITY_STATE_TEMPLATE = "hum_state_tpl"; +constexpr const char *const MQTT_JSON_ATTRIBUTES = "json_attr"; +constexpr const char *const MQTT_JSON_ATTRIBUTES_TOPIC = "json_attr_t"; +constexpr const char *const MQTT_JSON_ATTRIBUTES_TEMPLATE = "json_attr_tpl"; +constexpr const char *const MQTT_LAST_RESET_TOPIC = "lrst_t"; +constexpr const char *const MQTT_LAST_RESET_VALUE_TEMPLATE = "lrst_val_tpl"; +constexpr const char *const MQTT_MAX = "max"; +constexpr const char *const MQTT_MIN = "min"; +constexpr const char *const MQTT_MAX_HUMIDITY = "max_hum"; +constexpr const char *const MQTT_MIN_HUMIDITY = "min_hum"; +constexpr const char *const MQTT_MAX_MIREDS = "max_mirs"; +constexpr const char *const MQTT_MIN_MIREDS = "min_mirs"; +constexpr const char *const MQTT_MAX_TEMP = "max_temp"; +constexpr const char *const MQTT_MIN_TEMP = "min_temp"; +constexpr const char *const MQTT_MODE_COMMAND_TEMPLATE = "mode_cmd_tpl"; +constexpr const char *const MQTT_MODE_COMMAND_TOPIC = "mode_cmd_t"; +constexpr const char *const MQTT_MODE_STATE_TOPIC = "mode_stat_t"; +constexpr const char *const MQTT_MODE_STATE_TEMPLATE = "mode_stat_tpl"; +constexpr const char *const MQTT_MODES = "modes"; +constexpr const char *const MQTT_NAME = "name"; +constexpr const char *const MQTT_OFF_DELAY = "off_dly"; +constexpr const char *const MQTT_ON_COMMAND_TYPE = "on_cmd_type"; +constexpr const char *const MQTT_OPTIONS = "ops"; +constexpr const char *const MQTT_OPTIMISTIC = "opt"; +constexpr const char *const MQTT_OSCILLATION_COMMAND_TOPIC = "osc_cmd_t"; +constexpr const char *const MQTT_OSCILLATION_COMMAND_TEMPLATE = "osc_cmd_tpl"; +constexpr const char *const MQTT_OSCILLATION_STATE_TOPIC = "osc_stat_t"; +constexpr const char *const MQTT_OSCILLATION_VALUE_TEMPLATE = "osc_val_tpl"; +constexpr const char *const MQTT_PERCENTAGE_COMMAND_TOPIC = "pct_cmd_t"; +constexpr const char *const MQTT_PERCENTAGE_COMMAND_TEMPLATE = "pct_cmd_tpl"; +constexpr const char *const MQTT_PERCENTAGE_STATE_TOPIC = "pct_stat_t"; +constexpr const char *const MQTT_PERCENTAGE_VALUE_TEMPLATE = "pct_val_tpl"; +constexpr const char *const MQTT_PAYLOAD = "pl"; +constexpr const char *const MQTT_PAYLOAD_ARM_AWAY = "pl_arm_away"; +constexpr const char *const MQTT_PAYLOAD_ARM_HOME = "pl_arm_home"; +constexpr const char *const MQTT_PAYLOAD_ARM_NIGHT = "pl_arm_nite"; +constexpr const char *const MQTT_PAYLOAD_ARM_VACATION = "pl_arm_vacation"; +constexpr const char *const MQTT_PAYLOAD_ARM_CUSTOM_BYPASS = "pl_arm_custom_b"; +constexpr const char *const MQTT_PAYLOAD_AVAILABLE = "pl_avail"; +constexpr const char *const MQTT_PAYLOAD_CLEAN_SPOT = "pl_cln_sp"; +constexpr const char *const MQTT_PAYLOAD_CLOSE = "pl_cls"; +constexpr const char *const MQTT_PAYLOAD_DISARM = "pl_disarm"; +constexpr const char *const MQTT_PAYLOAD_HIGH_SPEED = "pl_hi_spd"; +constexpr const char *const MQTT_PAYLOAD_HOME = "pl_home"; +constexpr const char *const MQTT_PAYLOAD_LOCK = "pl_lock"; +constexpr const char *const MQTT_PAYLOAD_LOCATE = "pl_loc"; +constexpr const char *const MQTT_PAYLOAD_LOW_SPEED = "pl_lo_spd"; +constexpr const char *const MQTT_PAYLOAD_MEDIUM_SPEED = "pl_med_spd"; +constexpr const char *const MQTT_PAYLOAD_NOT_AVAILABLE = "pl_not_avail"; +constexpr const char *const MQTT_PAYLOAD_NOT_HOME = "pl_not_home"; +constexpr const char *const MQTT_PAYLOAD_OFF = "pl_off"; +constexpr const char *const MQTT_PAYLOAD_OFF_SPEED = "pl_off_spd"; +constexpr const char *const MQTT_PAYLOAD_ON = "pl_on"; +constexpr const char *const MQTT_PAYLOAD_OPEN = "pl_open"; +constexpr const char *const MQTT_PAYLOAD_OSCILLATION_OFF = "pl_osc_off"; +constexpr const char *const MQTT_PAYLOAD_OSCILLATION_ON = "pl_osc_on"; +constexpr const char *const MQTT_PAYLOAD_PAUSE = "pl_paus"; +constexpr const char *const MQTT_PAYLOAD_RESET = "pl_rst"; +constexpr const char *const MQTT_PAYLOAD_RESET_HUMIDITY = "pl_rst_hum"; +constexpr const char *const MQTT_PAYLOAD_RESET_MODE = "pl_rst_mode"; +constexpr const char *const MQTT_PAYLOAD_RESET_PERCENTAGE = "pl_rst_pct"; +constexpr const char *const MQTT_PAYLOAD_RESET_PRESET_MODE = "pl_rst_pr_mode"; +constexpr const char *const MQTT_PAYLOAD_STOP = "pl_stop"; +constexpr const char *const MQTT_PAYLOAD_START = "pl_strt"; +constexpr const char *const MQTT_PAYLOAD_START_PAUSE = "pl_stpa"; +constexpr const char *const MQTT_PAYLOAD_RETURN_TO_BASE = "pl_ret"; +constexpr const char *const MQTT_PAYLOAD_TURN_OFF = "pl_toff"; +constexpr const char *const MQTT_PAYLOAD_TURN_ON = "pl_ton"; +constexpr const char *const MQTT_PAYLOAD_UNLOCK = "pl_unlk"; +constexpr const char *const MQTT_POSITION_CLOSED = "pos_clsd"; +constexpr const char *const MQTT_POSITION_OPEN = "pos_open"; +constexpr const char *const MQTT_POWER_COMMAND_TOPIC = "pow_cmd_t"; +constexpr const char *const MQTT_POWER_STATE_TOPIC = "pow_stat_t"; +constexpr const char *const MQTT_POWER_STATE_TEMPLATE = "pow_stat_tpl"; +constexpr const char *const MQTT_PRESET_MODE_COMMAND_TOPIC = "pr_mode_cmd_t"; +constexpr const char *const MQTT_PRESET_MODE_COMMAND_TEMPLATE = "pr_mode_cmd_tpl"; +constexpr const char *const MQTT_PRESET_MODE_STATE_TOPIC = "pr_mode_stat_t"; +constexpr const char *const MQTT_PRESET_MODE_VALUE_TEMPLATE = "pr_mode_val_tpl"; +constexpr const char *const MQTT_PRESET_MODES = "pr_modes"; +constexpr const char *const MQTT_RED_TEMPLATE = "r_tpl"; +constexpr const char *const MQTT_RETAIN = "ret"; +constexpr const char *const MQTT_RGB_COMMAND_TEMPLATE = "rgb_cmd_tpl"; +constexpr const char *const MQTT_RGB_COMMAND_TOPIC = "rgb_cmd_t"; +constexpr const char *const MQTT_RGB_STATE_TOPIC = "rgb_stat_t"; +constexpr const char *const MQTT_RGB_VALUE_TEMPLATE = "rgb_val_tpl"; +constexpr const char *const MQTT_RGBW_COMMAND_TEMPLATE = "rgbw_cmd_tpl"; +constexpr const char *const MQTT_RGBW_COMMAND_TOPIC = "rgbw_cmd_t"; +constexpr const char *const MQTT_RGBW_STATE_TOPIC = "rgbw_stat_t"; +constexpr const char *const MQTT_RGBW_VALUE_TEMPLATE = "rgbw_val_tpl"; +constexpr const char *const MQTT_RGBWW_COMMAND_TEMPLATE = "rgbww_cmd_tpl"; +constexpr const char *const MQTT_RGBWW_COMMAND_TOPIC = "rgbww_cmd_t"; +constexpr const char *const MQTT_RGBWW_STATE_TOPIC = "rgbww_stat_t"; +constexpr const char *const MQTT_RGBWW_VALUE_TEMPLATE = "rgbww_val_tpl"; +constexpr const char *const MQTT_SEND_COMMAND_TOPIC = "send_cmd_t"; +constexpr const char *const MQTT_SEND_IF_OFF = "send_if_off"; +constexpr const char *const MQTT_SET_FAN_SPEED_TOPIC = "set_fan_spd_t"; +constexpr const char *const MQTT_SET_POSITION_TEMPLATE = "set_pos_tpl"; +constexpr const char *const MQTT_SET_POSITION_TOPIC = "set_pos_t"; +constexpr const char *const MQTT_POSITION_TOPIC = "pos_t"; +constexpr const char *const MQTT_POSITION_TEMPLATE = "pos_tpl"; +constexpr const char *const MQTT_SPEED_COMMAND_TOPIC = "spd_cmd_t"; +constexpr const char *const MQTT_SPEED_STATE_TOPIC = "spd_stat_t"; +constexpr const char *const MQTT_SPEED_RANGE_MIN = "spd_rng_min"; +constexpr const char *const MQTT_SPEED_RANGE_MAX = "spd_rng_max"; +constexpr const char *const MQTT_SPEED_VALUE_TEMPLATE = "spd_val_tpl"; +constexpr const char *const MQTT_SPEEDS = "spds"; +constexpr const char *const MQTT_SOURCE_TYPE = "src_type"; +constexpr const char *const MQTT_STATE_CLASS = "stat_cla"; +constexpr const char *const MQTT_STATE_CLOSED = "stat_clsd"; +constexpr const char *const MQTT_STATE_CLOSING = "stat_closing"; +constexpr const char *const MQTT_STATE_OFF = "stat_off"; +constexpr const char *const MQTT_STATE_ON = "stat_on"; +constexpr const char *const MQTT_STATE_OPEN = "stat_open"; +constexpr const char *const MQTT_STATE_OPENING = "stat_opening"; +constexpr const char *const MQTT_STATE_STOPPED = "stat_stopped"; +constexpr const char *const MQTT_STATE_LOCKED = "stat_locked"; +constexpr const char *const MQTT_STATE_UNLOCKED = "stat_unlocked"; +constexpr const char *const MQTT_STATE_TOPIC = "stat_t"; +constexpr const char *const MQTT_STATE_TEMPLATE = "stat_tpl"; +constexpr const char *const MQTT_STATE_VALUE_TEMPLATE = "stat_val_tpl"; +constexpr const char *const MQTT_STEP = "step"; +constexpr const char *const MQTT_SUBTYPE = "stype"; +constexpr const char *const MQTT_SUPPORTED_FEATURES = "sup_feat"; +constexpr const char *const MQTT_SUPPORTED_COLOR_MODES = "sup_clrm"; +constexpr const char *const MQTT_SWING_MODE_COMMAND_TEMPLATE = "swing_mode_cmd_tpl"; +constexpr const char *const MQTT_SWING_MODE_COMMAND_TOPIC = "swing_mode_cmd_t"; +constexpr const char *const MQTT_SWING_MODE_STATE_TEMPLATE = "swing_mode_stat_tpl"; +constexpr const char *const MQTT_SWING_MODE_STATE_TOPIC = "swing_mode_stat_t"; +constexpr const char *const MQTT_TEMPERATURE_COMMAND_TEMPLATE = "temp_cmd_tpl"; +constexpr const char *const MQTT_TEMPERATURE_COMMAND_TOPIC = "temp_cmd_t"; +constexpr const char *const MQTT_TEMPERATURE_HIGH_COMMAND_TEMPLATE = "temp_hi_cmd_tpl"; +constexpr const char *const MQTT_TEMPERATURE_HIGH_COMMAND_TOPIC = "temp_hi_cmd_t"; +constexpr const char *const MQTT_TEMPERATURE_HIGH_STATE_TEMPLATE = "temp_hi_stat_tpl"; +constexpr const char *const MQTT_TEMPERATURE_HIGH_STATE_TOPIC = "temp_hi_stat_t"; +constexpr const char *const MQTT_TEMPERATURE_LOW_COMMAND_TEMPLATE = "temp_lo_cmd_tpl"; +constexpr const char *const MQTT_TEMPERATURE_LOW_COMMAND_TOPIC = "temp_lo_cmd_t"; +constexpr const char *const MQTT_TEMPERATURE_LOW_STATE_TEMPLATE = "temp_lo_stat_tpl"; +constexpr const char *const MQTT_TEMPERATURE_LOW_STATE_TOPIC = "temp_lo_stat_t"; +constexpr const char *const MQTT_TEMPERATURE_STATE_TEMPLATE = "temp_stat_tpl"; +constexpr const char *const MQTT_TEMPERATURE_STATE_TOPIC = "temp_stat_t"; +constexpr const char *const MQTT_TEMPERATURE_UNIT = "temp_unit"; +constexpr const char *const MQTT_TILT_CLOSED_VALUE = "tilt_clsd_val"; +constexpr const char *const MQTT_TILT_COMMAND_TOPIC = "tilt_cmd_t"; +constexpr const char *const MQTT_TILT_COMMAND_TEMPLATE = "tilt_cmd_tpl"; +constexpr const char *const MQTT_TILT_INVERT_STATE = "tilt_inv_stat"; +constexpr const char *const MQTT_TILT_MAX = "tilt_max"; +constexpr const char *const MQTT_TILT_MIN = "tilt_min"; +constexpr const char *const MQTT_TILT_OPENED_VALUE = "tilt_opnd_val"; +constexpr const char *const MQTT_TILT_OPTIMISTIC = "tilt_opt"; +constexpr const char *const MQTT_TILT_STATUS_TOPIC = "tilt_status_t"; +constexpr const char *const MQTT_TILT_STATUS_TEMPLATE = "tilt_status_tpl"; +constexpr const char *const MQTT_TOPIC = "t"; +constexpr const char *const MQTT_UNIQUE_ID = "uniq_id"; +constexpr const char *const MQTT_UNIT_OF_MEASUREMENT = "unit_of_meas"; +constexpr const char *const MQTT_VALUE_TEMPLATE = "val_tpl"; +constexpr const char *const MQTT_WHITE_COMMAND_TOPIC = "whit_cmd_t"; +constexpr const char *const MQTT_WHITE_SCALE = "whit_scl"; +constexpr const char *const MQTT_WHITE_VALUE_COMMAND_TOPIC = "whit_val_cmd_t"; +constexpr const char *const MQTT_WHITE_VALUE_SCALE = "whit_val_scl"; +constexpr const char *const MQTT_WHITE_VALUE_STATE_TOPIC = "whit_val_stat_t"; +constexpr const char *const MQTT_WHITE_VALUE_TEMPLATE = "whit_val_tpl"; +constexpr const char *const MQTT_XY_COMMAND_TOPIC = "xy_cmd_t"; +constexpr const char *const MQTT_XY_STATE_TOPIC = "xy_stat_t"; +constexpr const char *const MQTT_XY_VALUE_TEMPLATE = "xy_val_tpl"; + +constexpr const char *const MQTT_DEVICE_CONNECTIONS = "cns"; +constexpr const char *const MQTT_DEVICE_IDENTIFIERS = "ids"; +constexpr const char *const MQTT_DEVICE_NAME = "name"; +constexpr const char *const MQTT_DEVICE_MANUFACTURER = "mf"; +constexpr const char *const MQTT_DEVICE_MODEL = "mdl"; +constexpr const char *const MQTT_DEVICE_SW_VERSION = "sw"; +constexpr const char *const MQTT_DEVICE_SUGGESTED_AREA = "sa"; + +#else + +constexpr const char *const MQTT_ACTION_TOPIC = "action_topic"; +constexpr const char *const MQTT_ACTION_TEMPLATE = "action_template"; +constexpr const char *const MQTT_AUTOMATION_TYPE = "automation_type"; +constexpr const char *const MQTT_AUX_COMMAND_TOPIC = "aux_command_topic"; +constexpr const char *const MQTT_AUX_STATE_TEMPLATE = "aux_state_template"; +constexpr const char *const MQTT_AUX_STATE_TOPIC = "aux_state_topic"; +constexpr const char *const MQTT_AVAILABILITY = "availability"; +constexpr const char *const MQTT_AVAILABILITY_MODE = "availability_mode"; +constexpr const char *const MQTT_AVAILABILITY_TOPIC = "availability_topic"; +constexpr const char *const MQTT_AWAY_MODE_COMMAND_TOPIC = "away_mode_command_topic"; +constexpr const char *const MQTT_AWAY_MODE_STATE_TEMPLATE = "away_mode_state_template"; +constexpr const char *const MQTT_AWAY_MODE_STATE_TOPIC = "away_mode_state_topic"; +constexpr const char *const MQTT_BLUE_TEMPLATE = "blue_template"; +constexpr const char *const MQTT_BRIGHTNESS_COMMAND_TOPIC = "brightness_command_topic"; +constexpr const char *const MQTT_BRIGHTNESS_SCALE = "brightness_scale"; +constexpr const char *const MQTT_BRIGHTNESS_STATE_TOPIC = "brightness_state_topic"; +constexpr const char *const MQTT_BRIGHTNESS_TEMPLATE = "brightness_template"; +constexpr const char *const MQTT_BRIGHTNESS_VALUE_TEMPLATE = "brightness_value_template"; +constexpr const char *const MQTT_COLOR_TEMP_COMMAND_TEMPLATE = "color_temp_command_template"; +constexpr const char *const MQTT_BATTERY_LEVEL_TOPIC = "battery_level_topic"; +constexpr const char *const MQTT_BATTERY_LEVEL_TEMPLATE = "battery_level_template"; +constexpr const char *const MQTT_CONFIGURATION_URL = "configuration_url"; +constexpr const char *const MQTT_CHARGING_TOPIC = "charging_topic"; +constexpr const char *const MQTT_CHARGING_TEMPLATE = "charging_template"; +constexpr const char *const MQTT_COLOR_MODE = "color_mode"; +constexpr const char *const MQTT_COLOR_MODE_STATE_TOPIC = "color_mode_state_topic"; +constexpr const char *const MQTT_COLOR_MODE_VALUE_TEMPLATE = "color_mode_value_template"; +constexpr const char *const MQTT_COLOR_TEMP_COMMAND_TOPIC = "color_temp_command_topic"; +constexpr const char *const MQTT_COLOR_TEMP_STATE_TOPIC = "color_temp_state_topic"; +constexpr const char *const MQTT_COLOR_TEMP_TEMPLATE = "color_temp_template"; +constexpr const char *const MQTT_COLOR_TEMP_VALUE_TEMPLATE = "color_temp_value_template"; +constexpr const char *const MQTT_CLEANING_TOPIC = "cleaning_topic"; +constexpr const char *const MQTT_CLEANING_TEMPLATE = "cleaning_template"; +constexpr const char *const MQTT_COMMAND_OFF_TEMPLATE = "command_off_template"; +constexpr const char *const MQTT_COMMAND_ON_TEMPLATE = "command_on_template"; +constexpr const char *const MQTT_COMMAND_TOPIC = "command_topic"; +constexpr const char *const MQTT_COMMAND_TEMPLATE = "command_template"; +constexpr const char *const MQTT_CODE_ARM_REQUIRED = "code_arm_required"; +constexpr const char *const MQTT_CODE_DISARM_REQUIRED = "code_disarm_required"; +constexpr const char *const MQTT_CURRENT_TEMPERATURE_TOPIC = "current_temperature_topic"; +constexpr const char *const MQTT_CURRENT_TEMPERATURE_TEMPLATE = "current_temperature_template"; +constexpr const char *const MQTT_DEVICE = "device"; +constexpr const char *const MQTT_DEVICE_CLASS = "device_class"; +constexpr const char *const MQTT_DOCKED_TOPIC = "docked_topic"; +constexpr const char *const MQTT_DOCKED_TEMPLATE = "docked_template"; +constexpr const char *const MQTT_ENABLED_BY_DEFAULT = "enabled_by_default"; +constexpr const char *const MQTT_ERROR_TOPIC = "error_topic"; +constexpr const char *const MQTT_ERROR_TEMPLATE = "error_template"; +constexpr const char *const MQTT_FAN_SPEED_TOPIC = "fan_speed_topic"; +constexpr const char *const MQTT_FAN_SPEED_TEMPLATE = "fan_speed_template"; +constexpr const char *const MQTT_FAN_SPEED_LIST = "fan_speed_list"; +constexpr const char *const MQTT_FLASH_TIME_LONG = "flash_time_long"; +constexpr const char *const MQTT_FLASH_TIME_SHORT = "flash_time_short"; +constexpr const char *const MQTT_EFFECT_COMMAND_TOPIC = "effect_command_topic"; +constexpr const char *const MQTT_EFFECT_LIST = "effect_list"; +constexpr const char *const MQTT_EFFECT_STATE_TOPIC = "effect_state_topic"; +constexpr const char *const MQTT_EFFECT_TEMPLATE = "effect_template"; +constexpr const char *const MQTT_EFFECT_VALUE_TEMPLATE = "effect_value_template"; +constexpr const char *const MQTT_EXPIRE_AFTER = "expire_after"; +constexpr const char *const MQTT_FAN_MODE_COMMAND_TEMPLATE = "fan_mode_command_template"; +constexpr const char *const MQTT_FAN_MODE_COMMAND_TOPIC = "fan_mode_command_topic"; +constexpr const char *const MQTT_FAN_MODE_STATE_TEMPLATE = "fan_mode_state_template"; +constexpr const char *const MQTT_FAN_MODE_STATE_TOPIC = "fan_mode_state_topic"; +constexpr const char *const MQTT_FORCE_UPDATE = "force_update"; +constexpr const char *const MQTT_GREEN_TEMPLATE = "green_template"; +constexpr const char *const MQTT_HOLD_COMMAND_TEMPLATE = "hold_command_template"; +constexpr const char *const MQTT_HOLD_COMMAND_TOPIC = "hold_command_topic"; +constexpr const char *const MQTT_HOLD_STATE_TEMPLATE = "hold_state_template"; +constexpr const char *const MQTT_HOLD_STATE_TOPIC = "hold_state_topic"; +constexpr const char *const MQTT_HS_COMMAND_TOPIC = "hs_command_topic"; +constexpr const char *const MQTT_HS_STATE_TOPIC = "hs_state_topic"; +constexpr const char *const MQTT_HS_VALUE_TEMPLATE = "hs_value_template"; +constexpr const char *const MQTT_ICON = "icon"; +constexpr const char *const MQTT_INITIAL = "initial"; +constexpr const char *const MQTT_TARGET_HUMIDITY_COMMAND_TOPIC = "target_humidity_command_topic"; +constexpr const char *const MQTT_TARGET_HUMIDITY_COMMAND_TEMPLATE = "target_humidity_command_template"; +constexpr const char *const MQTT_TARGET_HUMIDITY_STATE_TOPIC = "target_humidity_state_topic"; +constexpr const char *const MQTT_TARGET_HUMIDITY_STATE_TEMPLATE = "target_humidity_state_template"; +constexpr const char *const MQTT_JSON_ATTRIBUTES = "json_attributes"; +constexpr const char *const MQTT_JSON_ATTRIBUTES_TOPIC = "json_attributes_topic"; +constexpr const char *const MQTT_JSON_ATTRIBUTES_TEMPLATE = "json_attributes_template"; +constexpr const char *const MQTT_LAST_RESET_TOPIC = "last_reset_topic"; +constexpr const char *const MQTT_LAST_RESET_VALUE_TEMPLATE = "last_reset_value_template"; +constexpr const char *const MQTT_MAX = "max"; +constexpr const char *const MQTT_MIN = "min"; +constexpr const char *const MQTT_MAX_HUMIDITY = "max_humidity"; +constexpr const char *const MQTT_MIN_HUMIDITY = "min_humidity"; +constexpr const char *const MQTT_MAX_MIREDS = "max_mireds"; +constexpr const char *const MQTT_MIN_MIREDS = "min_mireds"; +constexpr const char *const MQTT_MAX_TEMP = "max_temp"; +constexpr const char *const MQTT_MIN_TEMP = "min_temp"; +constexpr const char *const MQTT_MODE_COMMAND_TEMPLATE = "mode_command_template"; +constexpr const char *const MQTT_MODE_COMMAND_TOPIC = "mode_command_topic"; +constexpr const char *const MQTT_MODE_STATE_TOPIC = "mode_state_topic"; +constexpr const char *const MQTT_MODE_STATE_TEMPLATE = "mode_state_template"; +constexpr const char *const MQTT_MODES = "modes"; +constexpr const char *const MQTT_NAME = "name"; +constexpr const char *const MQTT_OFF_DELAY = "off_delay"; +constexpr const char *const MQTT_ON_COMMAND_TYPE = "on_command_type"; +constexpr const char *const MQTT_OPTIONS = "options"; +constexpr const char *const MQTT_OPTIMISTIC = "optimistic"; +constexpr const char *const MQTT_OSCILLATION_COMMAND_TOPIC = "oscillation_command_topic"; +constexpr const char *const MQTT_OSCILLATION_COMMAND_TEMPLATE = "oscillation_command_template"; +constexpr const char *const MQTT_OSCILLATION_STATE_TOPIC = "oscillation_state_topic"; +constexpr const char *const MQTT_OSCILLATION_VALUE_TEMPLATE = "oscillation_value_template"; +constexpr const char *const MQTT_PERCENTAGE_COMMAND_TOPIC = "percentage_command_topic"; +constexpr const char *const MQTT_PERCENTAGE_COMMAND_TEMPLATE = "percentage_command_template"; +constexpr const char *const MQTT_PERCENTAGE_STATE_TOPIC = "percentage_state_topic"; +constexpr const char *const MQTT_PERCENTAGE_VALUE_TEMPLATE = "percentage_value_template"; +constexpr const char *const MQTT_PAYLOAD = "payload"; +constexpr const char *const MQTT_PAYLOAD_ARM_AWAY = "payload_arm_away"; +constexpr const char *const MQTT_PAYLOAD_ARM_HOME = "payload_arm_home"; +constexpr const char *const MQTT_PAYLOAD_ARM_NIGHT = "payload_arm_night"; +constexpr const char *const MQTT_PAYLOAD_ARM_VACATION = "payload_arm_vacation"; +constexpr const char *const MQTT_PAYLOAD_ARM_CUSTOM_BYPASS = "payload_arm_custom_bypass"; +constexpr const char *const MQTT_PAYLOAD_AVAILABLE = "payload_available"; +constexpr const char *const MQTT_PAYLOAD_CLEAN_SPOT = "payload_clean_spot"; +constexpr const char *const MQTT_PAYLOAD_CLOSE = "payload_close"; +constexpr const char *const MQTT_PAYLOAD_DISARM = "payload_disarm"; +constexpr const char *const MQTT_PAYLOAD_HIGH_SPEED = "payload_high_speed"; +constexpr const char *const MQTT_PAYLOAD_HOME = "payload_home"; +constexpr const char *const MQTT_PAYLOAD_LOCK = "payload_lock"; +constexpr const char *const MQTT_PAYLOAD_LOCATE = "payload_locate"; +constexpr const char *const MQTT_PAYLOAD_LOW_SPEED = "payload_low_speed"; +constexpr const char *const MQTT_PAYLOAD_MEDIUM_SPEED = "payload_medium_speed"; +constexpr const char *const MQTT_PAYLOAD_NOT_AVAILABLE = "payload_not_available"; +constexpr const char *const MQTT_PAYLOAD_NOT_HOME = "payload_not_home"; +constexpr const char *const MQTT_PAYLOAD_OFF = "payload_off"; +constexpr const char *const MQTT_PAYLOAD_OFF_SPEED = "payload_off_speed"; +constexpr const char *const MQTT_PAYLOAD_ON = "payload_on"; +constexpr const char *const MQTT_PAYLOAD_OPEN = "payload_open"; +constexpr const char *const MQTT_PAYLOAD_OSCILLATION_OFF = "payload_oscillation_off"; +constexpr const char *const MQTT_PAYLOAD_OSCILLATION_ON = "payload_oscillation_on"; +constexpr const char *const MQTT_PAYLOAD_PAUSE = "payload_pause"; +constexpr const char *const MQTT_PAYLOAD_RESET = "payload_reset"; +constexpr const char *const MQTT_PAYLOAD_RESET_HUMIDITY = "payload_reset_humidity"; +constexpr const char *const MQTT_PAYLOAD_RESET_MODE = "payload_reset_mode"; +constexpr const char *const MQTT_PAYLOAD_RESET_PERCENTAGE = "payload_reset_percentage"; +constexpr const char *const MQTT_PAYLOAD_RESET_PRESET_MODE = "payload_reset_preset_mode"; +constexpr const char *const MQTT_PAYLOAD_STOP = "payload_stop"; +constexpr const char *const MQTT_PAYLOAD_START = "payload_start"; +constexpr const char *const MQTT_PAYLOAD_START_PAUSE = "payload_start_pause"; +constexpr const char *const MQTT_PAYLOAD_RETURN_TO_BASE = "payload_return_to_base"; +constexpr const char *const MQTT_PAYLOAD_TURN_OFF = "payload_turn_off"; +constexpr const char *const MQTT_PAYLOAD_TURN_ON = "payload_turn_on"; +constexpr const char *const MQTT_PAYLOAD_UNLOCK = "payload_unlock"; +constexpr const char *const MQTT_POSITION_CLOSED = "position_closed"; +constexpr const char *const MQTT_POSITION_OPEN = "position_open"; +constexpr const char *const MQTT_POWER_COMMAND_TOPIC = "power_command_topic"; +constexpr const char *const MQTT_POWER_STATE_TOPIC = "power_state_topic"; +constexpr const char *const MQTT_POWER_STATE_TEMPLATE = "power_state_template"; +constexpr const char *const MQTT_PRESET_MODE_COMMAND_TOPIC = "preset_mode_command_topic"; +constexpr const char *const MQTT_PRESET_MODE_COMMAND_TEMPLATE = "preset_mode_command_template"; +constexpr const char *const MQTT_PRESET_MODE_STATE_TOPIC = "preset_mode_state_topic"; +constexpr const char *const MQTT_PRESET_MODE_VALUE_TEMPLATE = "preset_mode_value_template"; +constexpr const char *const MQTT_PRESET_MODES = "preset_modes"; +constexpr const char *const MQTT_RED_TEMPLATE = "red_template"; +constexpr const char *const MQTT_RETAIN = "retain"; +constexpr const char *const MQTT_RGB_COMMAND_TEMPLATE = "rgb_command_template"; +constexpr const char *const MQTT_RGB_COMMAND_TOPIC = "rgb_command_topic"; +constexpr const char *const MQTT_RGB_STATE_TOPIC = "rgb_state_topic"; +constexpr const char *const MQTT_RGB_VALUE_TEMPLATE = "rgb_value_template"; +constexpr const char *const MQTT_RGBW_COMMAND_TEMPLATE = "rgbw_command_template"; +constexpr const char *const MQTT_RGBW_COMMAND_TOPIC = "rgbw_command_topic"; +constexpr const char *const MQTT_RGBW_STATE_TOPIC = "rgbw_state_topic"; +constexpr const char *const MQTT_RGBW_VALUE_TEMPLATE = "rgbw_value_template"; +constexpr const char *const MQTT_RGBWW_COMMAND_TEMPLATE = "rgbww_command_template"; +constexpr const char *const MQTT_RGBWW_COMMAND_TOPIC = "rgbww_command_topic"; +constexpr const char *const MQTT_RGBWW_STATE_TOPIC = "rgbww_state_topic"; +constexpr const char *const MQTT_RGBWW_VALUE_TEMPLATE = "rgbww_value_template"; +constexpr const char *const MQTT_SEND_COMMAND_TOPIC = "send_command_topic"; +constexpr const char *const MQTT_SEND_IF_OFF = "send_if_off"; +constexpr const char *const MQTT_SET_FAN_SPEED_TOPIC = "set_fan_speed_topic"; +constexpr const char *const MQTT_SET_POSITION_TEMPLATE = "set_position_template"; +constexpr const char *const MQTT_SET_POSITION_TOPIC = "set_position_topic"; +constexpr const char *const MQTT_POSITION_TOPIC = "position_topic"; +constexpr const char *const MQTT_POSITION_TEMPLATE = "position_template"; +constexpr const char *const MQTT_SPEED_COMMAND_TOPIC = "speed_command_topic"; +constexpr const char *const MQTT_SPEED_STATE_TOPIC = "speed_state_topic"; +constexpr const char *const MQTT_SPEED_RANGE_MIN = "speed_range_min"; +constexpr const char *const MQTT_SPEED_RANGE_MAX = "speed_range_max"; +constexpr const char *const MQTT_SPEED_VALUE_TEMPLATE = "speed_value_template"; +constexpr const char *const MQTT_SPEEDS = "speeds"; +constexpr const char *const MQTT_SOURCE_TYPE = "source_type"; +constexpr const char *const MQTT_STATE_CLASS = "state_class"; +constexpr const char *const MQTT_STATE_CLOSED = "state_closed"; +constexpr const char *const MQTT_STATE_CLOSING = "state_closing"; +constexpr const char *const MQTT_STATE_OFF = "state_off"; +constexpr const char *const MQTT_STATE_ON = "state_on"; +constexpr const char *const MQTT_STATE_OPEN = "state_open"; +constexpr const char *const MQTT_STATE_OPENING = "state_opening"; +constexpr const char *const MQTT_STATE_STOPPED = "state_stopped"; +constexpr const char *const MQTT_STATE_LOCKED = "state_locked"; +constexpr const char *const MQTT_STATE_UNLOCKED = "state_unlocked"; +constexpr const char *const MQTT_STATE_TOPIC = "state_topic"; +constexpr const char *const MQTT_STATE_TEMPLATE = "state_template"; +constexpr const char *const MQTT_STATE_VALUE_TEMPLATE = "state_value_template"; +constexpr const char *const MQTT_STEP = "step"; +constexpr const char *const MQTT_SUBTYPE = "subtype"; +constexpr const char *const MQTT_SUPPORTED_FEATURES = "supported_features"; +constexpr const char *const MQTT_SUPPORTED_COLOR_MODES = "supported_color_modes"; +constexpr const char *const MQTT_SWING_MODE_COMMAND_TEMPLATE = "swing_mode_command_template"; +constexpr const char *const MQTT_SWING_MODE_COMMAND_TOPIC = "swing_mode_command_topic"; +constexpr const char *const MQTT_SWING_MODE_STATE_TEMPLATE = "swing_mode_state_template"; +constexpr const char *const MQTT_SWING_MODE_STATE_TOPIC = "swing_mode_state_topic"; +constexpr const char *const MQTT_TEMPERATURE_COMMAND_TEMPLATE = "temperature_command_template"; +constexpr const char *const MQTT_TEMPERATURE_COMMAND_TOPIC = "temperature_command_topic"; +constexpr const char *const MQTT_TEMPERATURE_HIGH_COMMAND_TEMPLATE = "temperature_high_command_template"; +constexpr const char *const MQTT_TEMPERATURE_HIGH_COMMAND_TOPIC = "temperature_high_command_topic"; +constexpr const char *const MQTT_TEMPERATURE_HIGH_STATE_TEMPLATE = "temperature_high_state_template"; +constexpr const char *const MQTT_TEMPERATURE_HIGH_STATE_TOPIC = "temperature_high_state_topic"; +constexpr const char *const MQTT_TEMPERATURE_LOW_COMMAND_TEMPLATE = "temperature_low_command_template"; +constexpr const char *const MQTT_TEMPERATURE_LOW_COMMAND_TOPIC = "temperature_low_command_topic"; +constexpr const char *const MQTT_TEMPERATURE_LOW_STATE_TEMPLATE = "temperature_low_state_template"; +constexpr const char *const MQTT_TEMPERATURE_LOW_STATE_TOPIC = "temperature_low_state_topic"; +constexpr const char *const MQTT_TEMPERATURE_STATE_TEMPLATE = "temperature_state_template"; +constexpr const char *const MQTT_TEMPERATURE_STATE_TOPIC = "temperature_state_topic"; +constexpr const char *const MQTT_TEMPERATURE_UNIT = "temperature_unit"; +constexpr const char *const MQTT_TILT_CLOSED_VALUE = "tilt_closed_value"; +constexpr const char *const MQTT_TILT_COMMAND_TOPIC = "tilt_command_topic"; +constexpr const char *const MQTT_TILT_COMMAND_TEMPLATE = "tilt_command_template"; +constexpr const char *const MQTT_TILT_INVERT_STATE = "tilt_invert_state"; +constexpr const char *const MQTT_TILT_MAX = "tilt_max"; +constexpr const char *const MQTT_TILT_MIN = "tilt_min"; +constexpr const char *const MQTT_TILT_OPENED_VALUE = "tilt_opened_value"; +constexpr const char *const MQTT_TILT_OPTIMISTIC = "tilt_optimistic"; +constexpr const char *const MQTT_TILT_STATUS_TOPIC = "tilt_status_topic"; +constexpr const char *const MQTT_TILT_STATUS_TEMPLATE = "tilt_status_template"; +constexpr const char *const MQTT_TOPIC = "topic"; +constexpr const char *const MQTT_UNIQUE_ID = "unique_id"; +constexpr const char *const MQTT_UNIT_OF_MEASUREMENT = "unit_of_measurement"; +constexpr const char *const MQTT_VALUE_TEMPLATE = "value_template"; +constexpr const char *const MQTT_WHITE_COMMAND_TOPIC = "white_command_topic"; +constexpr const char *const MQTT_WHITE_SCALE = "white_scale"; +constexpr const char *const MQTT_WHITE_VALUE_COMMAND_TOPIC = "white_value_command_topic"; +constexpr const char *const MQTT_WHITE_VALUE_SCALE = "white_value_scale"; +constexpr const char *const MQTT_WHITE_VALUE_STATE_TOPIC = "white_value_state_topic"; +constexpr const char *const MQTT_WHITE_VALUE_TEMPLATE = "white_value_template"; +constexpr const char *const MQTT_XY_COMMAND_TOPIC = "xy_command_topic"; +constexpr const char *const MQTT_XY_STATE_TOPIC = "xy_state_topic"; +constexpr const char *const MQTT_XY_VALUE_TEMPLATE = "xy_value_template"; + +constexpr const char *const MQTT_DEVICE_CONNECTIONS = "connections"; +constexpr const char *const MQTT_DEVICE_IDENTIFIERS = "identifiers"; +constexpr const char *const MQTT_DEVICE_NAME = "name"; +constexpr const char *const MQTT_DEVICE_MANUFACTURER = "manufacturer"; +constexpr const char *const MQTT_DEVICE_MODEL = "model"; +constexpr const char *const MQTT_DEVICE_SW_VERSION = "sw_version"; +constexpr const char *const MQTT_DEVICE_SUGGESTED_AREA = "suggested_area"; +#endif + +// Additional MQTT fields where no abbreviation is defined in HA source +constexpr const char *const MQTT_ENTITY_CATEGORY = "entity_category"; + +} // namespace mqtt +} // namespace esphome + +#endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_cover.cpp b/esphome/components/mqtt/mqtt_cover.cpp index e8bc7f0e30..7e42abcd05 100644 --- a/esphome/components/mqtt/mqtt_cover.cpp +++ b/esphome/components/mqtt/mqtt_cover.cpp @@ -1,6 +1,8 @@ #include "mqtt_cover.h" #include "esphome/core/log.h" +#include "mqtt_const.h" + #ifdef USE_MQTT #ifdef USE_COVER @@ -22,7 +24,7 @@ void MQTTCoverComponent::setup() { }); if (traits.get_supports_position()) { this->subscribe(this->get_position_command_topic(), [this](const std::string &topic, const std::string &payload) { - auto value = parse_float(payload); + auto value = parse_number(payload); if (!value.has_value()) { ESP_LOGW(TAG, "Invalid position value: '%s'", payload.c_str()); return; @@ -34,7 +36,7 @@ void MQTTCoverComponent::setup() { } if (traits.get_supports_tilt()) { this->subscribe(this->get_tilt_command_topic(), [this](const std::string &topic, const std::string &payload) { - auto value = parse_float(payload); + auto value = parse_number(payload); if (!value.has_value()) { ESP_LOGW(TAG, "Invalid tilt value: '%s'", payload.c_str()); return; @@ -63,20 +65,20 @@ void MQTTCoverComponent::dump_config() { } void MQTTCoverComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) { if (!this->cover_->get_device_class().empty()) - root["device_class"] = this->cover_->get_device_class(); + root[MQTT_DEVICE_CLASS] = this->cover_->get_device_class(); auto traits = this->cover_->get_traits(); if (traits.get_is_assumed_state()) { - root["optimistic"] = true; + root[MQTT_OPTIMISTIC] = true; } if (traits.get_supports_position()) { config.state_topic = false; - root["position_topic"] = this->get_position_state_topic(); - root["set_position_topic"] = this->get_position_command_topic(); + root[MQTT_POSITION_TOPIC] = this->get_position_state_topic(); + root[MQTT_SET_POSITION_TOPIC] = this->get_position_command_topic(); } if (traits.get_supports_tilt()) { - root["tilt_status_topic"] = this->get_tilt_state_topic(); - root["tilt_command_topic"] = this->get_tilt_command_topic(); + root[MQTT_TILT_STATUS_TOPIC] = this->get_tilt_state_topic(); + root[MQTT_TILT_COMMAND_TOPIC] = this->get_tilt_command_topic(); } if (traits.get_supports_tilt() && !traits.get_supports_position()) { config.command_topic = false; diff --git a/esphome/components/mqtt/mqtt_fan.cpp b/esphome/components/mqtt/mqtt_fan.cpp index 1703343a77..d58e3abc88 100644 --- a/esphome/components/mqtt/mqtt_fan.cpp +++ b/esphome/components/mqtt/mqtt_fan.cpp @@ -1,6 +1,8 @@ #include "mqtt_fan.h" #include "esphome/core/log.h" +#include "mqtt_const.h" + #ifdef USE_MQTT #ifdef USE_FAN #include "esphome/components/fan/fan_helpers.h" @@ -69,7 +71,7 @@ void MQTTFanComponent::setup() { if (this->state_->get_traits().supports_speed()) { this->subscribe(this->get_speed_level_command_topic(), [this](const std::string &topic, const std::string &payload) { - optional speed_level_opt = parse_int(payload); + optional speed_level_opt = parse_number(payload); if (speed_level_opt.has_value()) { const int speed_level = speed_level_opt.value(); if (speed_level >= 0 && speed_level <= this->state_->get_traits().supported_speed_count()) { @@ -120,14 +122,14 @@ bool MQTTFanComponent::send_initial_state() { return this->publish_state(); } void MQTTFanComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) { if (this->state_->get_traits().supports_oscillation()) { - root["oscillation_command_topic"] = this->get_oscillation_command_topic(); - root["oscillation_state_topic"] = this->get_oscillation_state_topic(); + root[MQTT_OSCILLATION_COMMAND_TOPIC] = this->get_oscillation_command_topic(); + root[MQTT_OSCILLATION_STATE_TOPIC] = this->get_oscillation_state_topic(); } if (this->state_->get_traits().supports_speed()) { root["speed_level_command_topic"] = this->get_speed_level_command_topic(); root["speed_level_state_topic"] = this->get_speed_level_state_topic(); - root["speed_command_topic"] = this->get_speed_command_topic(); - root["speed_state_topic"] = this->get_speed_state_topic(); + root[MQTT_SPEED_COMMAND_TOPIC] = this->get_speed_command_topic(); + root[MQTT_SPEED_STATE_TOPIC] = this->get_speed_state_topic(); } } bool MQTTFanComponent::publish_state() { diff --git a/esphome/components/mqtt/mqtt_light.cpp b/esphome/components/mqtt/mqtt_light.cpp index a88358a6b2..54204a9e7f 100644 --- a/esphome/components/mqtt/mqtt_light.cpp +++ b/esphome/components/mqtt/mqtt_light.cpp @@ -1,6 +1,8 @@ #include "mqtt_light.h" #include "esphome/core/log.h" +#include "mqtt_const.h" + #ifdef USE_MQTT #ifdef USE_LIGHT @@ -38,7 +40,7 @@ void MQTTJSONLightComponent::send_discovery(JsonObject &root, mqtt::SendDiscover root["schema"] = "json"; auto traits = this->state_->get_traits(); - root["color_mode"] = true; + root[MQTT_COLOR_MODE] = true; JsonArray &color_modes = root.createNestedArray("supported_color_modes"); if (traits.supports_color_mode(ColorMode::ON_OFF)) color_modes.add("onoff"); @@ -64,7 +66,7 @@ void MQTTJSONLightComponent::send_discovery(JsonObject &root, mqtt::SendDiscover if (this->state_->supports_effects()) { root["effect"] = true; - JsonArray &effect_list = root.createNestedArray("effect_list"); + JsonArray &effect_list = root.createNestedArray(MQTT_EFFECT_LIST); for (auto *effect : this->state_->get_effects()) effect_list.add(effect->get_name()); effect_list.add("None"); diff --git a/esphome/components/mqtt/mqtt_number.cpp b/esphome/components/mqtt/mqtt_number.cpp index 674fd77bdf..337013055a 100644 --- a/esphome/components/mqtt/mqtt_number.cpp +++ b/esphome/components/mqtt/mqtt_number.cpp @@ -1,6 +1,8 @@ #include "mqtt_number.h" #include "esphome/core/log.h" +#include "mqtt_const.h" + #ifdef USE_MQTT #ifdef USE_NUMBER @@ -15,7 +17,7 @@ MQTTNumberComponent::MQTTNumberComponent(Number *number) : MQTTComponent(), numb void MQTTNumberComponent::setup() { this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &state) { - auto val = parse_float(state); + auto val = parse_number(state); if (!val.has_value()) { ESP_LOGW(TAG, "Can't convert '%s' to number!", state.c_str()); return; @@ -38,9 +40,9 @@ const EntityBase *MQTTNumberComponent::get_entity() const { return this->number_ void MQTTNumberComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) { const auto &traits = number_->traits; // https://www.home-assistant.io/integrations/number.mqtt/ - root["min"] = traits.get_min_value(); - root["max"] = traits.get_max_value(); - root["step"] = traits.get_step(); + root[MQTT_MIN] = traits.get_min_value(); + root[MQTT_MAX] = traits.get_max_value(); + root[MQTT_STEP] = traits.get_step(); config.command_topic = true; } diff --git a/esphome/components/mqtt/mqtt_select.cpp b/esphome/components/mqtt/mqtt_select.cpp index b499636006..b8371de00e 100644 --- a/esphome/components/mqtt/mqtt_select.cpp +++ b/esphome/components/mqtt/mqtt_select.cpp @@ -1,6 +1,8 @@ #include "mqtt_select.h" #include "esphome/core/log.h" +#include "mqtt_const.h" + #ifdef USE_MQTT #ifdef USE_SELECT @@ -33,7 +35,7 @@ const EntityBase *MQTTSelectComponent::get_entity() const { return this->select_ void MQTTSelectComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) { const auto &traits = select_->traits; // https://www.home-assistant.io/integrations/select.mqtt/ - JsonArray &options = root.createNestedArray("options"); + JsonArray &options = root.createNestedArray(MQTT_OPTIONS); for (const auto &option : traits.get_options()) options.add(option); diff --git a/esphome/components/mqtt/mqtt_sensor.cpp b/esphome/components/mqtt/mqtt_sensor.cpp index 78710ff403..dd6423e8f3 100644 --- a/esphome/components/mqtt/mqtt_sensor.cpp +++ b/esphome/components/mqtt/mqtt_sensor.cpp @@ -1,6 +1,8 @@ #include "mqtt_sensor.h" #include "esphome/core/log.h" +#include "mqtt_const.h" + #ifdef USE_MQTT #ifdef USE_SENSOR @@ -42,19 +44,19 @@ void MQTTSensorComponent::disable_expire_after() { this->expire_after_ = 0; } void MQTTSensorComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) { if (!this->sensor_->get_device_class().empty()) - root["device_class"] = this->sensor_->get_device_class(); + root[MQTT_DEVICE_CLASS] = this->sensor_->get_device_class(); if (!this->sensor_->get_unit_of_measurement().empty()) - root["unit_of_measurement"] = this->sensor_->get_unit_of_measurement(); + root[MQTT_UNIT_OF_MEASUREMENT] = this->sensor_->get_unit_of_measurement(); if (this->get_expire_after() > 0) - root["expire_after"] = this->get_expire_after() / 1000; + root[MQTT_EXPIRE_AFTER] = this->get_expire_after() / 1000; if (this->sensor_->get_force_update()) - root["force_update"] = true; + root[MQTT_FORCE_UPDATE] = true; if (this->sensor_->get_state_class() != STATE_CLASS_NONE) - root["state_class"] = state_class_to_string(this->sensor_->get_state_class()); + root[MQTT_STATE_CLASS] = state_class_to_string(this->sensor_->get_state_class()); config.command_topic = false; } diff --git a/esphome/components/mqtt/mqtt_switch.cpp b/esphome/components/mqtt/mqtt_switch.cpp index 16cf102f7e..edaa6e7859 100644 --- a/esphome/components/mqtt/mqtt_switch.cpp +++ b/esphome/components/mqtt/mqtt_switch.cpp @@ -1,6 +1,8 @@ #include "mqtt_switch.h" #include "esphome/core/log.h" +#include "mqtt_const.h" + #ifdef USE_MQTT #ifdef USE_SWITCH @@ -44,7 +46,7 @@ std::string MQTTSwitchComponent::component_type() const { return "switch"; } const EntityBase *MQTTSwitchComponent::get_entity() const { return this->switch_; } void MQTTSwitchComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) { if (this->switch_->assumed_state()) - root["optimistic"] = true; + root[MQTT_OPTIMISTIC] = true; } bool MQTTSwitchComponent::send_initial_state() { return this->publish_state(this->switch_->state); } diff --git a/esphome/components/mqtt_subscribe/sensor/mqtt_subscribe_sensor.cpp b/esphome/components/mqtt_subscribe/sensor/mqtt_subscribe_sensor.cpp index e1accf3c70..273de10376 100644 --- a/esphome/components/mqtt_subscribe/sensor/mqtt_subscribe_sensor.cpp +++ b/esphome/components/mqtt_subscribe/sensor/mqtt_subscribe_sensor.cpp @@ -13,7 +13,7 @@ void MQTTSubscribeSensor::setup() { mqtt::global_mqtt_client->subscribe( this->topic_, [this](const std::string &topic, const std::string &payload) { - auto val = parse_float(payload); + auto val = parse_number(payload); if (!val.has_value()) { ESP_LOGW(TAG, "Can't convert '%s' to number!", payload.c_str()); this->publish_state(NAN); diff --git a/esphome/components/ms5611/ms5611.cpp b/esphome/components/ms5611/ms5611.cpp index 1d7516dbe8..4b34e1d71a 100644 --- a/esphome/components/ms5611/ms5611.cpp +++ b/esphome/components/ms5611/ms5611.cpp @@ -75,30 +75,48 @@ void MS5611Component::read_pressure_(uint32_t raw_temperature) { const uint32_t raw_pressure = (uint32_t(bytes[0]) << 16) | (uint32_t(bytes[1]) << 8) | (uint32_t(bytes[2])); this->calculate_values_(raw_temperature, raw_pressure); } + +// Calculations are taken from the datasheet which can be found here: +// https://www.te.com/commerce/DocumentDelivery/DDEController?Action=showdoc&DocId=Data+Sheet%7FMS5611-01BA03%7FB3%7Fpdf%7FEnglish%7FENG_DS_MS5611-01BA03_B3.pdf%7FCAT-BLPS0036 +// Sections PRESSURE AND TEMPERATURE CALCULATION and SECOND ORDER TEMPERATURE COMPENSATION +// Variable names below match variable names from the datasheet but lowercased void MS5611Component::calculate_values_(uint32_t raw_temperature, uint32_t raw_pressure) { - const int32_t d_t = int32_t(raw_temperature) - (uint32_t(this->prom_[4]) << 8); - float temperature = (2000 + (int64_t(d_t) * this->prom_[5]) / 8388608.0f) / 100.0f; + const uint32_t c1 = uint32_t(this->prom_[0]); + const uint32_t c2 = uint32_t(this->prom_[1]); + const uint16_t c3 = uint16_t(this->prom_[2]); + const uint16_t c4 = uint16_t(this->prom_[3]); + const int32_t c5 = int32_t(this->prom_[4]); + const uint16_t c6 = uint16_t(this->prom_[5]); + const uint32_t d1 = raw_pressure; + const int32_t d2 = raw_temperature; - float pressure_offset = (uint32_t(this->prom_[1]) << 16) + ((this->prom_[3] * d_t) >> 7); - float pressure_sensitivity = (uint32_t(this->prom_[0]) << 15) + ((this->prom_[2] * d_t) >> 8); + // Promote dt to 64 bit here to make the math below cleaner + const int64_t dt = d2 - (c5 << 8); + int32_t temp = (2000 + ((dt * c6) >> 23)); - if (temperature < 20.0f) { - const float t2 = (d_t * d_t) / 2147483648.0f; - const float temp20 = (temperature - 20.0f) * 100.0f; - float pressure_offset_2 = 2.5f * temp20 * temp20; - float pressure_sensitivity_2 = 1.25f * temp20 * temp20; - if (temp20 < -15.0f) { - const float temp15 = (temperature + 15.0f) * 100.0f; - pressure_offset_2 += 7.0f * temp15; - pressure_sensitivity_2 += 5.5f * temp15; + int64_t off = (c2 << 16) + ((dt * c4) >> 7); + int64_t sens = (c1 << 15) + ((dt * c3) >> 8); + + if (temp < 2000) { + const int32_t t2 = (dt * dt) >> 31; + int32_t off2 = ((5 * (temp - 2000) * (temp - 2000)) >> 1); + int32_t sens2 = ((5 * (temp - 2000) * (temp - 2000)) >> 2); + if (temp < -1500) { + off2 = (off2 + 7 * (temp + 1500) * (temp + 1500)); + sens2 = sens2 + ((11 * (temp + 1500) * (temp + 1500)) >> 1); } - temperature -= t2; - pressure_offset -= pressure_offset_2; - pressure_sensitivity -= pressure_sensitivity_2; + temp = temp - t2; + off = off - off2; + sens = sens - sens2; } - const float pressure = ((raw_pressure * pressure_sensitivity) / 2097152.0f - pressure_offset) / 3276800.0f; + // Here we multiply unsigned 32-bit by signed 64-bit using signed 64-bit math. + // Possible ranges of D1 and SENS from the datasheet guarantee + // that this multiplication does not overflow + const int32_t p = ((((d1 * sens) >> 21) - off) >> 15); + const float temperature = temp / 100.0f; + const float pressure = p / 100.0f; ESP_LOGD(TAG, "Got temperature=%0.02f°C pressure=%0.01fhPa", temperature, pressure); if (this->temperature_sensor_ != nullptr) diff --git a/esphome/components/neopixelbus/_methods.py b/esphome/components/neopixelbus/_methods.py new file mode 100644 index 0000000000..b03544f246 --- /dev/null +++ b/esphome/components/neopixelbus/_methods.py @@ -0,0 +1,418 @@ +from dataclasses import dataclass +from typing import Any, List +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import ( + CONF_CHANNEL, + CONF_CLOCK_PIN, + CONF_DATA_PIN, + CONF_METHOD, + CONF_PIN, + CONF_SPEED, +) +from esphome.components.esp32 import get_esp32_variant +from esphome.components.esp32.const import ( + VARIANT_ESP32, + VARIANT_ESP32S2, + VARIANT_ESP32C3, +) +from esphome.core import CORE +from .const import ( + CONF_ASYNC, + CONF_BUS, + CHIP_400KBPS, + CHIP_800KBPS, + CHIP_APA106, + CHIP_DOTSTAR, + CHIP_LC8812, + CHIP_LPD6803, + CHIP_LPD8806, + CHIP_P9813, + CHIP_SK6812, + CHIP_TM1814, + CHIP_TM1829, + CHIP_TM1914, + CHIP_WS2801, + CHIP_WS2811, + CHIP_WS2812, + CHIP_WS2812X, + CHIP_WS2813, + ONE_WIRE_CHIPS, + TWO_WIRE_CHIPS, +) + +METHOD_BIT_BANG = "bit_bang" +METHOD_ESP8266_UART = "esp8266_uart" +METHOD_ESP8266_DMA = "esp8266_dma" +METHOD_ESP32_RMT = "esp32_rmt" +METHOD_ESP32_I2S = "esp32_i2s" +METHOD_SPI = "spi" + +CHANNEL_DYNAMIC = "dynamic" +BUS_DYNAMIC = "dynamic" +SPI_BUS_VSPI = "vspi" +SPI_BUS_HSPI = "hspi" +SPI_SPEEDS = [40e6, 20e6, 10e6, 5e6, 2e6, 1e6, 500e3] + + +def _esp32_rmt_default_channel(): + return { + VARIANT_ESP32S2: 1, + VARIANT_ESP32C3: 1, + }.get(get_esp32_variant(), 6) + + +def _validate_esp32_rmt_channel(value): + if isinstance(value, str) and value.lower() == CHANNEL_DYNAMIC: + value = CHANNEL_DYNAMIC + else: + value = cv.int_(value) + variant_channels = { + VARIANT_ESP32: [0, 1, 2, 3, 4, 5, 6, 7, CHANNEL_DYNAMIC], + VARIANT_ESP32S2: [0, 1, 2, 3, CHANNEL_DYNAMIC], + VARIANT_ESP32C3: [0, 1, CHANNEL_DYNAMIC], + } + variant = get_esp32_variant() + if variant not in variant_channels: + raise cv.Invalid(f"{variant} does not support the rmt method") + if value not in variant_channels[variant]: + raise cv.Invalid(f"{variant} does not support rmt channel {value}") + return value + + +def _esp32_i2s_default_bus(): + return { + VARIANT_ESP32: 1, + VARIANT_ESP32S2: 0, + }.get(get_esp32_variant(), 0) + + +def _validate_esp32_i2s_bus(value): + if isinstance(value, str) and value.lower() == CHANNEL_DYNAMIC: + value = CHANNEL_DYNAMIC + else: + value = cv.int_(value) + variant_buses = { + VARIANT_ESP32: [0, 1, BUS_DYNAMIC], + VARIANT_ESP32S2: [0, BUS_DYNAMIC], + } + variant = get_esp32_variant() + if variant not in variant_buses: + raise cv.Invalid(f"{variant} does not support the i2s method") + if value not in variant_buses[variant]: + raise cv.Invalid(f"{variant} does not support i2s bus {value}") + return value + + +neo_ns = cg.global_ns + + +def _bit_bang_to_code(config, chip: str, inverted: bool): + # https://github.com/Makuna/NeoPixelBus/blob/master/src/internal/NeoEspBitBangMethod.h + # Some chips are only aliases + chip = { + CHIP_WS2813: CHIP_WS2812X, + CHIP_LC8812: CHIP_SK6812, + CHIP_TM1914: CHIP_TM1814, + CHIP_WS2812: CHIP_800KBPS, + }.get(chip, chip) + + lookup = { + CHIP_WS2811: (neo_ns.NeoEspBitBangSpeedWs2811, False), + CHIP_WS2812X: (neo_ns.NeoEspBitBangSpeedWs2812x, False), + CHIP_SK6812: (neo_ns.NeoEspBitBangSpeedSk6812, False), + CHIP_TM1814: (neo_ns.NeoEspBitBangSpeedTm1814, True), + CHIP_TM1829: (neo_ns.NeoEspBitBangSpeedTm1829, True), + CHIP_800KBPS: (neo_ns.NeoEspBitBangSpeed800Kbps, False), + CHIP_400KBPS: (neo_ns.NeoEspBitBangSpeed400Kbps, False), + CHIP_APA106: (neo_ns.NeoEspBitBangSpeedApa106, False), + } + # For tm variants opposite of inverted is needed + speed, pinset_inverted = lookup[chip] + pinset = { + False: neo_ns.NeoEspPinset, + True: neo_ns.NeoEspPinsetInverted, + }[inverted != pinset_inverted] + return neo_ns.NeoEspBitBangMethodBase.template(speed, pinset) + + +def _bit_bang_extra_validate(config): + pin = config[CONF_PIN] + if CORE.is_esp8266 and not (0 <= pin <= 15): + # Due to use of w1ts + raise cv.Invalid("Bit bang only supports pins GPIO0-GPIO15 on ESP8266") + if CORE.is_esp32 and not (0 <= pin <= 31): + raise cv.Invalid("Bit bang only supports pins GPIO0-GPIO31 on ESP32") + + +def _esp8266_uart_to_code(config, chip: str, inverted: bool): + # https://github.com/Makuna/NeoPixelBus/blob/master/src/internal/NeoEsp8266UartMethod.h + uart_context, uart_base = { + False: (neo_ns.NeoEsp8266UartContext, neo_ns.NeoEsp8266Uart), + True: (neo_ns.NeoEsp8266UartInterruptContext, neo_ns.NeoEsp8266AsyncUart), + }[config[CONF_ASYNC]] + uart_feature = { + 0: neo_ns.UartFeature0, + 1: neo_ns.UartFeature1, + }[config[CONF_BUS]] + # Some chips are only aliases + chip = { + CHIP_WS2811: CHIP_WS2812X, + CHIP_WS2813: CHIP_WS2812X, + CHIP_LC8812: CHIP_SK6812, + CHIP_TM1914: CHIP_TM1814, + CHIP_WS2812: CHIP_800KBPS, + }.get(chip, chip) + + lookup = { + CHIP_WS2812X: (neo_ns.NeoEsp8266UartSpeedWs2812x, False), + CHIP_SK6812: (neo_ns.NeoEsp8266UartSpeedSk6812, False), + CHIP_TM1814: (neo_ns.NeoEsp8266UartSpeedTm1814, True), + CHIP_TM1829: (neo_ns.NeoEsp8266UartSpeedTm1829, True), + CHIP_800KBPS: (neo_ns.NeoEsp8266UartSpeed800Kbps, False), + CHIP_400KBPS: (neo_ns.NeoEsp8266UartSpeed400Kbps, False), + CHIP_APA106: (neo_ns.NeoEsp8266UartSpeedApa106, False), + } + speed, uart_inverted = lookup[chip] + # For tm variants opposite of inverted is needed + inv = { + False: neo_ns.NeoEsp8266UartNotInverted, + True: neo_ns.NeoEsp8266UartInverted, + }[inverted != uart_inverted] + return neo_ns.NeoEsp8266UartMethodBase.template( + speed, uart_base.template(uart_feature, uart_context), inv + ) + + +def _esp8266_uart_extra_validate(config): + pin = config[CONF_PIN] + bus = config[CONF_METHOD][CONF_BUS] + right_pin = { + 0: 1, # U0TXD + 1: 2, # U1TXD + }[bus] + if pin != right_pin: + raise cv.Invalid(f"ESP8266 uart bus {bus} only supports pin GPIO{right_pin}") + + +def _esp8266_dma_to_code(config, chip: str, inverted: bool): + # https://github.com/Makuna/NeoPixelBus/blob/master/src/internal/NeoEsp8266DmaMethod.h + # Some chips are only aliases + chip = { + CHIP_WS2811: CHIP_WS2812X, + CHIP_WS2813: CHIP_WS2812X, + CHIP_LC8812: CHIP_SK6812, + CHIP_TM1914: CHIP_TM1814, + CHIP_WS2812: CHIP_800KBPS, + }.get(chip, chip) + + lookup = { + (CHIP_WS2812X, False): neo_ns.NeoEsp8266DmaSpeedWs2812x, + (CHIP_SK6812, False): neo_ns.NeoEsp8266DmaSpeedSk6812, + (CHIP_TM1814, True): neo_ns.NeoEsp8266DmaInvertedSpeedTm1814, + (CHIP_TM1829, True): neo_ns.NeoEsp8266DmaInvertedSpeedTm1829, + (CHIP_800KBPS, False): neo_ns.NeoEsp8266DmaSpeed800Kbps, + (CHIP_400KBPS, False): neo_ns.NeoEsp8266DmaSpeed400Kbps, + (CHIP_APA106, False): neo_ns.NeoEsp8266DmaSpeedApa106, + (CHIP_WS2812X, True): neo_ns.NeoEsp8266DmaInvertedSpeedWs2812x, + (CHIP_SK6812, True): neo_ns.NeoEsp8266DmaInvertedSpeedSk6812, + (CHIP_TM1814, False): neo_ns.NeoEsp8266DmaSpeedTm1814, + (CHIP_TM1829, False): neo_ns.NeoEsp8266DmaSpeedTm1829, + (CHIP_800KBPS, True): neo_ns.NeoEsp8266DmaInvertedSpeed800Kbps, + (CHIP_400KBPS, True): neo_ns.NeoEsp8266DmaInvertedSpeed400Kbps, + (CHIP_APA106, True): neo_ns.NeoEsp8266DmaInvertedSpeedApa106, + } + speed = lookup[(chip, inverted)] + return neo_ns.NeoEsp8266DmaMethodBase.template(speed) + + +def _esp8266_dma_extra_validate(config): + if config[CONF_PIN] != 3: + raise cv.Invalid("ESP8266 dma method only supports pin GPIO3") + + +def _esp32_rmt_to_code(config, chip: str, inverted: bool): + # https://github.com/Makuna/NeoPixelBus/blob/master/src/internal/NeoEsp32RmtMethod.h + channel = { + 0: neo_ns.NeoEsp32RmtChannel0, + 1: neo_ns.NeoEsp32RmtChannel1, + 2: neo_ns.NeoEsp32RmtChannel2, + 3: neo_ns.NeoEsp32RmtChannel3, + 4: neo_ns.NeoEsp32RmtChannel4, + 5: neo_ns.NeoEsp32RmtChannel5, + 6: neo_ns.NeoEsp32RmtChannel6, + 7: neo_ns.NeoEsp32RmtChannel7, + CHANNEL_DYNAMIC: neo_ns.NeoEsp32RmtChannelN, + }[config[CONF_CHANNEL]] + # Some chips are only aliases + chip = { + CHIP_WS2813: CHIP_WS2812X, + CHIP_LC8812: CHIP_SK6812, + CHIP_WS2812: CHIP_800KBPS, + }.get(chip, chip) + + lookup = { + (CHIP_WS2811, False): neo_ns.NeoEsp32RmtSpeedWs2811, + (CHIP_WS2812X, False): neo_ns.NeoEsp32RmtSpeedWs2812x, + (CHIP_SK6812, False): neo_ns.NeoEsp32RmtSpeedSk6812, + (CHIP_TM1814, False): neo_ns.NeoEsp32RmtSpeedTm1814, + (CHIP_TM1829, False): neo_ns.NeoEsp32RmtSpeedTm1829, + (CHIP_TM1914, False): neo_ns.NeoEsp32RmtSpeedTm1914, + (CHIP_800KBPS, False): neo_ns.NeoEsp32RmtSpeed800Kbps, + (CHIP_400KBPS, False): neo_ns.NeoEsp32RmtSpeed400Kbps, + (CHIP_APA106, False): neo_ns.NeoEsp32RmtSpeedApa106, + (CHIP_WS2811, True): neo_ns.NeoEsp32RmtInvertedSpeedWs2811, + (CHIP_WS2812X, True): neo_ns.NeoEsp32RmtInvertedSpeedWs2812x, + (CHIP_SK6812, True): neo_ns.NeoEsp32RmtInvertedSpeedSk6812, + (CHIP_TM1814, True): neo_ns.NeoEsp32RmtInvertedSpeedTm1814, + (CHIP_TM1829, True): neo_ns.NeoEsp32RmtInvertedSpeedTm1829, + (CHIP_TM1914, True): neo_ns.NeoEsp32RmtInvertedSpeedTm1914, + (CHIP_800KBPS, True): neo_ns.NeoEsp32RmtInvertedSpeed800Kbps, + (CHIP_400KBPS, True): neo_ns.NeoEsp32RmtInvertedSpeed400Kbps, + (CHIP_APA106, True): neo_ns.NeoEsp32RmtInvertedSpeedApa106, + } + speed = lookup[(chip, inverted)] + return neo_ns.NeoEsp32RmtMethodBase.template(speed, channel) + + +def _esp32_i2s_to_code(config, chip: str, inverted: bool): + # https://github.com/Makuna/NeoPixelBus/blob/master/src/internal/NeoEsp32I2sMethod.h + bus = { + 0: neo_ns.NeoEsp32I2sBusZero, + 1: neo_ns.NeoEsp32I2sBusOne, + BUS_DYNAMIC: neo_ns.NeoEsp32I2sBusN, + }[config[CONF_BUS]] + # Some chips are only aliases + chip = { + CHIP_WS2811: CHIP_WS2812X, + CHIP_WS2813: CHIP_WS2812X, + CHIP_LC8812: CHIP_SK6812, + CHIP_WS2812: CHIP_800KBPS, + }.get(chip, chip) + + lookup = { + CHIP_WS2812X: (neo_ns.NeoEsp32I2sSpeedWs2812x, False), + CHIP_SK6812: (neo_ns.NeoEsp32I2sSpeedSk6812, False), + CHIP_TM1814: (neo_ns.NeoEsp32I2sSpeedTm1814, True), + CHIP_TM1914: (neo_ns.NeoEsp32I2sSpeedTm1914, True), + CHIP_TM1829: (neo_ns.NeoEsp32I2sSpeedTm1829, True), + CHIP_800KBPS: (neo_ns.NeoEsp32I2sSpeed800Kbps, False), + CHIP_400KBPS: (neo_ns.NeoEsp32I2sSpeed400Kbps, False), + CHIP_APA106: (neo_ns.NeoEsp32I2sSpeedApa106, False), + } + speed, inv_inverted = lookup[chip] + # For tm variants opposite of inverted is needed + inv = { + False: neo_ns.NeoEsp32I2sNotInverted, + True: neo_ns.NeoEsp32I2sInverted, + }[inverted != inv_inverted] + return neo_ns.NeoEsp32I2sMethodBase.template(speed, bus, inv) + + +def _spi_to_code(config, chip: str, inverted: bool): + # https://github.com/Makuna/NeoPixelBus/blob/master/src/internal/TwoWireSpiImple.h + spi_imple = { + None: neo_ns.TwoWireSpiImple, + SPI_BUS_VSPI: neo_ns.TwoWireSpiImple, + SPI_BUS_HSPI: neo_ns.TwoWireHspiImple, + }[config.get(CONF_BUS)] + spi_speed = { + 40e6: neo_ns.SpiSpeed40Mhz, + 20e6: neo_ns.SpiSpeed20Mhz, + 10e6: neo_ns.SpiSpeed10Mhz, + 5e6: neo_ns.SpiSpeed5Mhz, + 2e6: neo_ns.SpiSpeed2Mhz, + 1e6: neo_ns.SpiSpeed1Mhz, + 500e3: neo_ns.SpiSpeed500Khz, + }[config[CONF_SPEED]] + chip_method_base = { + CHIP_DOTSTAR: neo_ns.DotStarMethodBase, + CHIP_LPD6803: neo_ns.Lpd6803MethodBase, + CHIP_LPD8806: neo_ns.Lpd8806MethodBase, + CHIP_WS2801: neo_ns.Ws2801MethodBase, + CHIP_P9813: neo_ns.P9813MethodBase, + }[chip] + return chip_method_base.template(spi_imple.template(spi_speed)) + + +def _spi_extra_validate(config): + if CORE.is_esp32: + return + + if config[CONF_DATA_PIN] != 13 and config[CONF_CLOCK_PIN] != 14: + raise cv.Invalid( + "SPI only supports pins GPIO13 for data and GPIO14 for clock on ESP8266" + ) + + +@dataclass +class MethodDescriptor: + method_schema: Any + to_code: Any + supported_chips: List[str] + extra_validate: Any = None + + +METHODS = { + METHOD_BIT_BANG: MethodDescriptor( + method_schema={}, + to_code=_bit_bang_to_code, + extra_validate=_bit_bang_extra_validate, + supported_chips=ONE_WIRE_CHIPS, + ), + METHOD_ESP8266_UART: MethodDescriptor( + method_schema=cv.All( + cv.only_on_esp8266, + { + cv.Optional(CONF_ASYNC, default=False): cv.boolean, + cv.Optional(CONF_BUS, default=1): cv.int_range(min=0, max=1), + }, + ), + extra_validate=_esp8266_uart_extra_validate, + to_code=_esp8266_uart_to_code, + supported_chips=ONE_WIRE_CHIPS, + ), + METHOD_ESP8266_DMA: MethodDescriptor( + method_schema=cv.All(cv.only_on_esp8266, {}), + extra_validate=_esp8266_dma_extra_validate, + to_code=_esp8266_dma_to_code, + supported_chips=ONE_WIRE_CHIPS, + ), + METHOD_ESP32_RMT: MethodDescriptor( + method_schema=cv.All( + cv.only_on_esp32, + { + cv.Optional( + CONF_CHANNEL, default=_esp32_rmt_default_channel + ): _validate_esp32_rmt_channel, + }, + ), + to_code=_esp32_rmt_to_code, + supported_chips=ONE_WIRE_CHIPS, + ), + METHOD_ESP32_I2S: MethodDescriptor( + method_schema=cv.All( + cv.only_on_esp32, + { + cv.Optional( + CONF_BUS, default=_esp32_i2s_default_bus + ): _validate_esp32_i2s_bus, + }, + ), + to_code=_esp32_i2s_to_code, + supported_chips=ONE_WIRE_CHIPS, + ), + METHOD_SPI: MethodDescriptor( + method_schema={ + cv.Optional(CONF_BUS): cv.All( + cv.only_on_esp32, cv.one_of(SPI_BUS_VSPI, SPI_BUS_HSPI, lower=True) + ), + cv.Optional(CONF_SPEED, default="10MHz"): cv.All( + cv.frequency, cv.one_of(*SPI_SPEEDS) + ), + }, + to_code=_spi_to_code, + extra_validate=_spi_extra_validate, + supported_chips=TWO_WIRE_CHIPS, + ), +} diff --git a/esphome/components/neopixelbus/const.py b/esphome/components/neopixelbus/const.py new file mode 100644 index 0000000000..ec1bd74c29 --- /dev/null +++ b/esphome/components/neopixelbus/const.py @@ -0,0 +1,42 @@ +CHIP_DOTSTAR = "dotstar" +CHIP_WS2801 = "ws2801" +CHIP_WS2811 = "ws2811" +CHIP_WS2812 = "ws2812" +CHIP_WS2812X = "ws2812x" +CHIP_WS2813 = "ws2813" +CHIP_SK6812 = "sk6812" +CHIP_TM1814 = "tm1814" +CHIP_TM1829 = "tm1829" +CHIP_TM1914 = "tm1914" +CHIP_800KBPS = "800kbps" +CHIP_400KBPS = "400kbps" +CHIP_APA106 = "apa106" +CHIP_LC8812 = "lc8812" +CHIP_LPD8806 = "lpd8806" +CHIP_LPD6803 = "lpd6803" +CHIP_P9813 = "p9813" + +ONE_WIRE_CHIPS = [ + CHIP_WS2811, + CHIP_WS2812, + CHIP_WS2812X, + CHIP_WS2813, + CHIP_SK6812, + CHIP_TM1814, + CHIP_TM1829, + CHIP_TM1914, + CHIP_800KBPS, + CHIP_400KBPS, + CHIP_APA106, + CHIP_LC8812, +] +TWO_WIRE_CHIPS = [ + CHIP_DOTSTAR, + CHIP_WS2801, + CHIP_LPD6803, + CHIP_LPD8806, + CHIP_P9813, +] +CHIP_TYPES = [*ONE_WIRE_CHIPS, *TWO_WIRE_CHIPS] +CONF_ASYNC = "async" +CONF_BUS = "bus" diff --git a/esphome/components/neopixelbus/light.py b/esphome/components/neopixelbus/light.py index 0117f1b063..6bb1bc8f99 100644 --- a/esphome/components/neopixelbus/light.py +++ b/esphome/components/neopixelbus/light.py @@ -3,6 +3,7 @@ import esphome.config_validation as cv from esphome import pins from esphome.components import light from esphome.const import ( + CONF_CHANNEL, CONF_CLOCK_PIN, CONF_DATA_PIN, CONF_METHOD, @@ -13,7 +14,26 @@ from esphome.const import ( CONF_OUTPUT_ID, CONF_INVERT, ) +from esphome.components.esp32 import get_esp32_variant +from esphome.components.esp32.const import ( + VARIANT_ESP32C3, +) from esphome.core import CORE +from ._methods import ( + METHODS, + METHOD_SPI, + METHOD_ESP8266_UART, + METHOD_BIT_BANG, + METHOD_ESP32_I2S, + METHOD_ESP32_RMT, + METHOD_ESP8266_DMA, +) +from .const import ( + CHIP_TYPES, + CONF_ASYNC, + CONF_BUS, + ONE_WIRE_CHIPS, +) neopixelbus_ns = cg.esphome_ns.namespace("neopixelbus") NeoPixelBusLightOutputBase = neopixelbus_ns.class_( @@ -46,127 +66,115 @@ def validate_type(value): return value -def validate_variant(value): - value = cv.string(value).upper() - if value == "WS2813": - value = "WS2812X" - if value == "WS2812": - value = "800KBPS" - if value == "LC8812": - value = "SK6812" - return cv.one_of(*VARIANTS)(value) +def _choose_default_method(config): + if CONF_METHOD in config: + return config + config = config.copy() + if CONF_PIN not in config: + config[CONF_METHOD] = _validate_method(METHOD_SPI) + return config - -def validate_method(value): - if value is None: - if CORE.is_esp32: - return "ESP32_I2S_1" - if CORE.is_esp8266: - return "ESP8266_DMA" - raise NotImplementedError - - if CORE.is_esp32: - return cv.one_of(*ESP32_METHODS, upper=True, space="_")(value) + pin = config[CONF_PIN] if CORE.is_esp8266: - return cv.one_of(*ESP8266_METHODS, upper=True, space="_")(value) - raise NotImplementedError - - -def validate_method_pin(value): - method = value[CONF_METHOD] - method_pins = { - "ESP8266_DMA": [3], - "ESP8266_UART0": [1], - "ESP8266_ASYNC_UART0": [1], - "ESP8266_UART1": [2], - "ESP8266_ASYNC_UART1": [2], - "ESP32_I2S_0": list(range(0, 32)), - "ESP32_I2S_1": list(range(0, 32)), - } - if CORE.is_esp8266: - method_pins["BIT_BANG"] = list(range(0, 16)) - elif CORE.is_esp32: - method_pins["BIT_BANG"] = list(range(0, 32)) - pins_ = method_pins.get(method) - if pins_ is None: - # all pins allowed for this method - return value - - for opt in (CONF_PIN, CONF_CLOCK_PIN, CONF_DATA_PIN): - if opt in value and value[opt] not in pins_: - raise cv.Invalid( - f"Method {method} only supports pin(s) {', '.join(f'GPIO{x}' for x in pins_)}", - path=[CONF_METHOD], + if pin == 3: + config[CONF_METHOD] = _validate_method(METHOD_ESP8266_DMA) + elif pin == 1: + config[CONF_METHOD] = _validate_method( + { + CONF_TYPE: METHOD_ESP8266_UART, + CONF_BUS: 0, + } + ) + elif pin == 2: + config[CONF_METHOD] = _validate_method( + { + CONF_TYPE: METHOD_ESP8266_UART, + CONF_BUS: 1, + } ) - return value - - -VARIANTS = { - "WS2812X": "Ws2812x", - "SK6812": "Sk6812", - "800KBPS": "800Kbps", - "400KBPS": "400Kbps", -} - -ESP8266_METHODS = { - "ESP8266_DMA": "NeoEsp8266Dma{}Method", - "ESP8266_UART0": "NeoEsp8266Uart0{}Method", - "ESP8266_UART1": "NeoEsp8266Uart1{}Method", - "ESP8266_ASYNC_UART0": "NeoEsp8266AsyncUart0{}Method", - "ESP8266_ASYNC_UART1": "NeoEsp8266AsyncUart1{}Method", - "BIT_BANG": "NeoEsp8266BitBang{}Method", -} -ESP32_METHODS = { - "ESP32_I2S_0": "NeoEsp32I2s0{}Method", - "ESP32_I2S_1": "NeoEsp32I2s1{}Method", - "ESP32_RMT_0": "NeoEsp32Rmt0{}Method", - "ESP32_RMT_1": "NeoEsp32Rmt1{}Method", - "ESP32_RMT_2": "NeoEsp32Rmt2{}Method", - "ESP32_RMT_3": "NeoEsp32Rmt3{}Method", - "ESP32_RMT_4": "NeoEsp32Rmt4{}Method", - "ESP32_RMT_5": "NeoEsp32Rmt5{}Method", - "ESP32_RMT_6": "NeoEsp32Rmt6{}Method", - "ESP32_RMT_7": "NeoEsp32Rmt7{}Method", - "BIT_BANG": "NeoEsp32BitBang{}Method", -} - - -def format_method(config): - variant = VARIANTS[config[CONF_VARIANT]] - method = config[CONF_METHOD] - - if config[CONF_INVERT]: - if method == "ESP8266_DMA": - variant = f"Inverted{variant}" else: - variant += "Inverted" + config[CONF_METHOD] = _validate_method(METHOD_BIT_BANG) - if CORE.is_esp8266: - return ESP8266_METHODS[method].format(variant) if CORE.is_esp32: - return ESP32_METHODS[method].format(variant) - raise NotImplementedError + if get_esp32_variant() == VARIANT_ESP32C3: + config[CONF_METHOD] = _validate_method(METHOD_ESP32_RMT) + else: + config[CONF_METHOD] = _validate_method(METHOD_ESP32_I2S) + + return config def _validate(config): - if CONF_PIN in config: + variant = config[CONF_VARIANT] + if variant in ONE_WIRE_CHIPS: + if CONF_PIN not in config: + raise cv.Invalid( + f"Chip {variant} is a 1-wire chip and needs the [pin] option." + ) if CONF_CLOCK_PIN in config or CONF_DATA_PIN in config: - raise cv.Invalid("Cannot specify both 'pin' and 'clock_pin'+'data_pin'") - return config - if CONF_CLOCK_PIN in config: - if CONF_DATA_PIN not in config: - raise cv.Invalid("If you give clock_pin, you must also specify data_pin") - return config - raise cv.Invalid("Must specify at least one of 'pin' or 'clock_pin'+'data_pin'") + raise cv.Invalid( + f"Chip {variant} is a 1-wire chip, you need to set [pin] instead of ." + ) + else: + if CONF_PIN in config: + raise cv.Invalid( + f"Chip {variant} is a 2-wire chip and needs the [data_pin]+[clock_pin] option instead of [pin]." + ) + if CONF_CLOCK_PIN not in config or CONF_DATA_PIN not in config: + raise cv.Invalid( + f"Chip {variant} is a 2-wire chip, you need to set [data_pin]+[clock_pin]." + ) + + method_type = config[CONF_METHOD][CONF_TYPE] + method_desc = METHODS[method_type] + if variant not in method_desc.supported_chips: + raise cv.Invalid(f"Method {method_type} does not support {variant}") + if method_desc.extra_validate is not None: + method_desc.extra_validate(config) + + return config + + +def _validate_method(value): + if value is None: + # default method is determined afterwards because it depends on the chip type chosen + return None + + compat_methods = {} + for bus in [0, 1]: + for is_async in [False, True]: + compat_methods[f"ESP8266{'_ASYNC' if is_async else ''}_UART{bus}"] = { + CONF_TYPE: METHOD_ESP8266_UART, + CONF_BUS: bus, + CONF_ASYNC: is_async, + } + compat_methods[f"ESP32_I2S_{bus}"] = { + CONF_TYPE: METHOD_ESP32_I2S, + CONF_BUS: bus, + } + for channel in range(8): + compat_methods[f"ESP32_RMT_{channel}"] = { + CONF_TYPE: METHOD_ESP32_RMT, + CONF_CHANNEL: channel, + } + + if isinstance(value, str): + if value.upper() in compat_methods: + return _validate_method(compat_methods[value.upper()]) + return _validate_method({CONF_TYPE: value}) + return cv.typed_schema( + {k: v.method_schema for k, v in METHODS.items()}, lower=True + )(value) CONFIG_SCHEMA = cv.All( + cv.only_with_arduino, light.ADDRESSABLE_LIGHT_SCHEMA.extend( { cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(NeoPixelBusLightOutputBase), cv.Optional(CONF_TYPE, default="GRB"): validate_type, - cv.Optional(CONF_VARIANT, default="800KBPS"): validate_variant, - cv.Optional(CONF_METHOD, default=None): validate_method, + cv.Required(CONF_VARIANT): cv.one_of(*CHIP_TYPES, lower=True), + cv.Optional(CONF_METHOD): _validate_method, cv.Optional(CONF_INVERT, default="no"): cv.boolean, cv.Optional(CONF_PIN): pins.internal_gpio_output_pin_number, cv.Optional(CONF_CLOCK_PIN): pins.internal_gpio_output_pin_number, @@ -174,19 +182,23 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_NUM_LEDS): cv.positive_not_null_int, } ).extend(cv.COMPONENT_SCHEMA), + _choose_default_method, _validate, - validate_method_pin, - cv.only_with_arduino, ) async def to_code(config): has_white = "W" in config[CONF_TYPE] - template = cg.TemplateArguments(getattr(cg.global_ns, format_method(config))) + method = config[CONF_METHOD] + + method_template = METHODS[method[CONF_TYPE]].to_code( + method, config[CONF_VARIANT], config[CONF_INVERT] + ) + if has_white: - out_type = NeoPixelRGBWLightOutput.template(template) + out_type = NeoPixelRGBWLightOutput.template(method_template) else: - out_type = NeoPixelRGBLightOutput.template(template) + out_type = NeoPixelRGBLightOutput.template(method_template) rhs = out_type.new() var = cg.Pvariable(config[CONF_OUTPUT_ID], rhs, out_type) await light.register_light(var, config) @@ -204,4 +216,4 @@ async def to_code(config): cg.add(var.set_pixel_order(getattr(ESPNeoPixelOrder, config[CONF_TYPE]))) # https://github.com/Makuna/NeoPixelBus/blob/master/library.json - cg.add_library("makuna/NeoPixelBus", "2.6.7") + cg.add_library("makuna/NeoPixelBus", "2.6.9") diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index 2856a25ee7..1da25caafe 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -41,8 +41,7 @@ NumberInRangeCondition = number_ns.class_( icon = cv.icon - -NUMBER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend( +NUMBER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( { cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTNumberComponent), cv.GenerateID(): cv.declare_id(Number), diff --git a/esphome/components/output/switch/output_switch.h b/esphome/components/output/switch/output_switch.h index fc9540fede..a184a342fe 100644 --- a/esphome/components/output/switch/output_switch.h +++ b/esphome/components/output/switch/output_switch.h @@ -12,7 +12,7 @@ class OutputSwitch : public switch_::Switch, public Component { void set_output(BinaryOutput *output) { output_ = output; } void setup() override; - float get_setup_priority() const override { return setup_priority::HARDWARE; } + float get_setup_priority() const override { return setup_priority::HARDWARE - 1.0f; } void dump_config() override; protected: diff --git a/esphome/components/pipsolar/pipsolar.cpp b/esphome/components/pipsolar/pipsolar.cpp index 7dbbd798ad..9f8b57003a 100644 --- a/esphome/components/pipsolar/pipsolar.cpp +++ b/esphome/components/pipsolar/pipsolar.cpp @@ -656,7 +656,7 @@ void Pipsolar::loop() { case 32: fc = tmp[i]; fc += tmp[i + 1]; - this->value_fault_code_ = strtol(fc.c_str(), nullptr, 10); + this->value_fault_code_ = parse_number(fc).value_or(0); break; case 34: this->value_warnung_low_pv_energy_ = enabled; diff --git a/esphome/components/prometheus/__init__.py b/esphome/components/prometheus/__init__.py index f5f166d085..45345f06e8 100644 --- a/esphome/components/prometheus/__init__.py +++ b/esphome/components/prometheus/__init__.py @@ -15,7 +15,8 @@ CONFIG_SCHEMA = cv.Schema( cv.GenerateID(CONF_WEB_SERVER_BASE_ID): cv.use_id( web_server_base.WebServerBase ), - } + }, + cv.only_with_arduino, ).extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.cpp b/esphome/components/pulse_counter/pulse_counter_sensor.cpp index f538a4c905..d9f198f4fc 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.cpp +++ b/esphome/components/pulse_counter/pulse_counter_sensor.cpp @@ -155,16 +155,20 @@ void PulseCounterSensor::dump_config() { void PulseCounterSensor::update() { pulse_counter_t raw = this->storage_.read_raw_value(); - float value = (60000.0f * raw) / float(this->get_update_interval()); // per minute - - ESP_LOGD(TAG, "'%s': Retrieved counter: %0.2f pulses/min", this->get_name().c_str(), value); - this->publish_state(value); + uint32_t now = millis(); + if (this->last_time_ != 0) { + uint32_t interval = now - this->last_time_; + float value = (60000.0f * raw) / float(interval); // per minute + ESP_LOGD(TAG, "'%s': Retrieved counter: %0.2f pulses/min", this->get_name().c_str(), value); + this->publish_state(value); + } if (this->total_sensor_ != nullptr) { current_total_ += raw; ESP_LOGD(TAG, "'%s': Total : %i pulses", this->get_name().c_str(), current_total_); this->total_sensor_->publish_state(current_total_); } + this->last_time_ = now; } } // namespace pulse_counter diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.h b/esphome/components/pulse_counter/pulse_counter_sensor.h index 94e37bc232..9ed2159ae3 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.h +++ b/esphome/components/pulse_counter/pulse_counter_sensor.h @@ -65,7 +65,8 @@ class PulseCounterSensor : public sensor::Sensor, public PollingComponent { protected: InternalGPIOPin *pin_; PulseCounterStorage storage_; - uint32_t current_total_ = 0; + uint32_t last_time_{0}; + uint32_t current_total_{0}; sensor::Sensor *total_sensor_; }; diff --git a/esphome/components/pvvx_mithermometer/sensor.py b/esphome/components/pvvx_mithermometer/sensor.py index b17878f01b..12090bddba 100644 --- a/esphome/components/pvvx_mithermometer/sensor.py +++ b/esphome/components/pvvx_mithermometer/sensor.py @@ -12,6 +12,7 @@ from esphome.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, + ENTITY_CATEGORY_DIAGNOSTIC, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_PERCENT, @@ -49,12 +50,14 @@ CONFIG_SCHEMA = ( accuracy_decimals=0, device_class=DEVICE_CLASS_BATTERY, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), cv.Optional(CONF_BATTERY_VOLTAGE): sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, accuracy_decimals=3, device_class=DEVICE_CLASS_VOLTAGE, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), } ) diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index d2b848600d..914ce42efe 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -439,6 +439,49 @@ async def pioneer_action(var, config, args): cg.add(var.set_rc_code_2(template_)) +# Pronto +( + ProntoData, + ProntoBinarySensor, + ProntoTrigger, + ProntoAction, + ProntoDumper, +) = declare_protocol("Pronto") +PRONTO_SCHEMA = cv.Schema( + { + cv.Required(CONF_DATA): cv.string, + } +) + + +@register_binary_sensor("pronto", ProntoBinarySensor, PRONTO_SCHEMA) +def pronto_binary_sensor(var, config): + cg.add( + var.set_data( + cg.StructInitializer( + ProntoData, + ("data", config[CONF_DATA]), + ) + ) + ) + + +@register_trigger("pronto", ProntoTrigger, ProntoData) +def pronto_trigger(var, config): + pass + + +@register_dumper("pronto", ProntoDumper) +def pronto_dumper(var, config): + pass + + +@register_action("pronto", ProntoAction, PRONTO_SCHEMA) +async def pronto_action(var, config, args): + template_ = await cg.templatable(config[CONF_DATA], args, cg.std_string) + cg.add(var.set_data(template_)) + + # Sony SonyData, SonyBinarySensor, SonyTrigger, SonyAction, SonyDumper = declare_protocol( "Sony" diff --git a/esphome/components/remote_base/pronto_protocol.cpp b/esphome/components/remote_base/pronto_protocol.cpp new file mode 100644 index 0000000000..11aebb6c5d --- /dev/null +++ b/esphome/components/remote_base/pronto_protocol.cpp @@ -0,0 +1,135 @@ +/* + * @file irPronto.cpp + * @brief In this file, the functions IRrecv::compensateAndPrintPronto and IRsend::sendPronto are defined. + * + * See http://www.harctoolbox.org/Glossary.html#ProntoSemantics + * Pronto database http://www.remotecentral.com/search.htm + * + * This file is part of Arduino-IRremote https://github.com/Arduino-IRremote/Arduino-IRremote. + * + ************************************************************************************ + * MIT License + * + * Copyright (c) 2020 Bengt Martensson + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is furnished + * to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + ************************************************************************************ + */ + +#include "pronto_protocol.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace remote_base { + +static const char *const TAG = "remote.pronto"; + +// DO NOT EXPORT from this file +static const uint16_t MICROSECONDS_T_MAX = 0xFFFFU; +static const uint16_t LEARNED_TOKEN = 0x0000U; +static const uint16_t LEARNED_NON_MODULATED_TOKEN = 0x0100U; +static const uint16_t BITS_IN_HEXADECIMAL = 4U; +static const uint16_t DIGITS_IN_PRONTO_NUMBER = 4U; +static const uint16_t NUMBERS_IN_PREAMBLE = 4U; +static const uint16_t HEX_MASK = 0xFU; +static const uint32_t REFERENCE_FREQUENCY = 4145146UL; +static const uint16_t FALLBACK_FREQUENCY = 64767U; // To use with frequency = 0; +static const uint32_t MICROSECONDS_IN_SECONDS = 1000000UL; +static const uint16_t PRONTO_DEFAULT_GAP = 45000; + +static uint16_t to_frequency_k_hz(uint16_t code) { + if (code == 0) + return 0; + + return ((REFERENCE_FREQUENCY / code) + 500) / 1000; +} + +/* + * Parse the string given as Pronto Hex, and send it a number of times given as argument. + */ +void ProntoProtocol::send_pronto_(RemoteTransmitData *dst, const std::vector &data) { + if (data.size() < 4) + return; + + uint16_t timebase = (MICROSECONDS_IN_SECONDS * data[1] + REFERENCE_FREQUENCY / 2) / REFERENCE_FREQUENCY; + uint16_t khz; + switch (data[0]) { + case LEARNED_TOKEN: // normal, "learned" + khz = to_frequency_k_hz(data[1]); + break; + case LEARNED_NON_MODULATED_TOKEN: // non-demodulated, "learned" + khz = 0U; + break; + default: + return; // There are other types, but they are not handled yet. + } + ESP_LOGD(TAG, "Send Pronto: frequency=%dkHz", khz); + dst->set_carrier_frequency(khz * 1000); + + uint16_t intros = 2 * data[2]; + uint16_t repeats = 2 * data[3]; + ESP_LOGD(TAG, "Send Pronto: intros=%d", intros); + ESP_LOGD(TAG, "Send Pronto: repeats=%d", repeats); + if (NUMBERS_IN_PREAMBLE + intros + repeats != data.size()) { // inconsistent sizes + return; + } + + /* + * Generate a new microseconds timing array for sendRaw. + * If recorded by IRremote, intro contains the whole IR data and repeat is empty + */ + dst->reserve(intros + repeats); + + for (uint16_t i = 0; i < intros + repeats; i += 2) { + uint32_t duration0 = ((uint32_t) data[i + 0 + NUMBERS_IN_PREAMBLE]) * timebase; + duration0 = duration0 < MICROSECONDS_T_MAX ? duration0 : MICROSECONDS_T_MAX; + + uint32_t duration1 = ((uint32_t) data[i + 1 + NUMBERS_IN_PREAMBLE]) * timebase; + duration1 = duration1 < MICROSECONDS_T_MAX ? duration1 : MICROSECONDS_T_MAX; + + dst->item(duration0, duration1); + } +} + +void ProntoProtocol::send_pronto_(RemoteTransmitData *dst, const std::string &str) { + size_t len = str.length() / (DIGITS_IN_PRONTO_NUMBER + 1) + 1; + std::vector data; + const char *p = str.c_str(); + char *endptr[1]; + + for (uint16_t i = 0; i < len; i++) { + uint16_t x = strtol(p, endptr, 16); + if (x == 0 && i >= NUMBERS_IN_PREAMBLE) { + // Alignment error?, bail immediately (often right result). + break; + } + data.push_back(x); // If input is conforming, there can be no overflow! + p = *endptr; + } + send_pronto_(dst, data); +} + +void ProntoProtocol::encode(RemoteTransmitData *dst, const ProntoData &data) { send_pronto_(dst, data.data); } + +optional ProntoProtocol::decode(RemoteReceiveData src) { return {}; } + +void ProntoProtocol::dump(const ProntoData &data) { ESP_LOGD(TAG, "Received Pronto: data=%s", data.data.c_str()); } + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/pronto_protocol.h b/esphome/components/remote_base/pronto_protocol.h new file mode 100644 index 0000000000..e96511383f --- /dev/null +++ b/esphome/components/remote_base/pronto_protocol.h @@ -0,0 +1,40 @@ +#pragma once + +#include "esphome/core/component.h" +#include "remote_base.h" + +namespace esphome { +namespace remote_base { + +struct ProntoData { + std::string data; + + bool operator==(const ProntoData &rhs) const { return data == rhs.data; } +}; + +class ProntoProtocol : public RemoteProtocol { + private: + void send_pronto_(RemoteTransmitData *dst, const std::vector &data); + void send_pronto_(RemoteTransmitData *dst, const std::string &str); + + public: + void encode(RemoteTransmitData *dst, const ProntoData &data) override; + optional decode(RemoteReceiveData src) override; + void dump(const ProntoData &data) override; +}; + +DECLARE_REMOTE_PROTOCOL(Pronto) + +template class ProntoAction : public RemoteTransmitterActionBase { + public: + TEMPLATABLE_VALUE(std::string, data) + + void encode(RemoteTransmitData *dst, Ts... x) override { + ProntoData data{}; + data.data = this->data_.value(x...); + ProntoProtocol().encode(dst, data); + } +}; + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/rc_switch_protocol.cpp b/esphome/components/remote_base/rc_switch_protocol.cpp index 6b7d1b725a..1dc094d552 100644 --- a/esphome/components/remote_base/rc_switch_protocol.cpp +++ b/esphome/components/remote_base/rc_switch_protocol.cpp @@ -101,10 +101,13 @@ bool RCSwitchBase::expect_sync(RemoteReceiveData &src) const { if (!src.peek_space(this->sync_low_, 1)) return false; } else { - if (!src.peek_space(this->sync_high_)) - return false; - if (!src.peek_mark(this->sync_low_, 1)) + // We cant peek a space at the beginning because signals starts with a low to high transition. + // this long space at the beginning is the separation between the transmissions itself, so it is actually + // added at the end kind of artificially (by the value given to "idle:" option by the user in the yaml) + if (!src.peek_mark(this->sync_low_)) return false; + src.advance(1); + return true; } src.advance(2); return true; diff --git a/esphome/components/remote_receiver/remote_receiver_esp32.cpp b/esphome/components/remote_receiver/remote_receiver_esp32.cpp index dde9b843c9..5a7fb3c985 100644 --- a/esphome/components/remote_receiver/remote_receiver_esp32.cpp +++ b/esphome/components/remote_receiver/remote_receiver_esp32.cpp @@ -78,6 +78,7 @@ void RemoteReceiverComponent::loop() { if (this->temp_.empty()) return; + this->temp_.push_back(-this->idle_us_); this->call_listeners_dumpers_(); } } @@ -86,9 +87,10 @@ void RemoteReceiverComponent::decode_rmt_(rmt_item32_t *item, size_t len) { uint32_t prev_length = 0; this->temp_.clear(); int32_t multiplier = this->pin_->is_inverted() ? -1 : 1; + size_t item_count = len / sizeof(rmt_item32_t); ESP_LOGVV(TAG, "START:"); - for (size_t i = 0; i < len; i++) { + for (size_t i = 0; i < item_count; i++) { if (item[i].level0) { ESP_LOGVV(TAG, "%u A: ON %uus (%u ticks)", i, this->to_microseconds_(item[i].duration0), item[i].duration0); } else { @@ -102,8 +104,8 @@ void RemoteReceiverComponent::decode_rmt_(rmt_item32_t *item, size_t len) { } ESP_LOGVV(TAG, "\n"); - this->temp_.reserve(len / 4); - for (size_t i = 0; i < len; i++) { + this->temp_.reserve(item_count * 2); // each RMT item has 2 pulses + for (size_t i = 0; i < item_count; i++) { if (item[i].duration0 == 0u) { // Do nothing } else if (bool(item[i].level0) == prev_level) { @@ -120,10 +122,6 @@ void RemoteReceiverComponent::decode_rmt_(rmt_item32_t *item, size_t len) { prev_length = item[i].duration0; } - if (this->to_microseconds_(prev_length) > this->idle_us_) { - break; - } - if (item[i].duration1 == 0u) { // Do nothing } else if (bool(item[i].level1) == prev_level) { @@ -139,10 +137,6 @@ void RemoteReceiverComponent::decode_rmt_(rmt_item32_t *item, size_t len) { prev_level = bool(item[i].level1); prev_length = item[i].duration1; } - - if (this->to_microseconds_(prev_length) > this->idle_us_) { - break; - } } if (prev_length > 0) { if (prev_level) { diff --git a/esphome/components/remote_transmitter/remote_transmitter.h b/esphome/components/remote_transmitter/remote_transmitter.h index 733ac5e50d..a4235e875f 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.h +++ b/esphome/components/remote_transmitter/remote_transmitter.h @@ -32,6 +32,9 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, void mark_(uint32_t on_time, uint32_t off_time, uint32_t usec); void space_(uint32_t usec); + + void await_target_time_(); + uint32_t target_time_; #endif #ifdef USE_ESP32 diff --git a/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp b/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp index 500d7193f3..c3b61b72c2 100644 --- a/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp @@ -121,10 +121,8 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen } else { this->status_clear_warning(); } - if (i + 1 < send_times) { - delay(send_wait / 1000UL); - delayMicroseconds(send_wait % 1000UL); - } + if (i + 1 < send_times) + delayMicroseconds(send_wait); } } diff --git a/esphome/components/remote_transmitter/remote_transmitter_esp8266.cpp b/esphome/components/remote_transmitter/remote_transmitter_esp8266.cpp index 33c01985d7..39752cac5b 100644 --- a/esphome/components/remote_transmitter/remote_transmitter_esp8266.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter_esp8266.cpp @@ -33,57 +33,64 @@ void RemoteTransmitterComponent::calculate_on_off_time_(uint32_t carrier_frequen *off_time_period = period - *on_time_period; } +void RemoteTransmitterComponent::await_target_time_() { + const uint32_t current_time = micros(); + if (this->target_time_ == 0) + this->target_time_ = current_time; + else if (this->target_time_ > current_time) + delayMicroseconds(this->target_time_ - current_time); +} + void RemoteTransmitterComponent::mark_(uint32_t on_time, uint32_t off_time, uint32_t usec) { - if (this->carrier_duty_percent_ == 100 || (on_time == 0 && off_time == 0)) { - this->pin_->digital_write(true); - delay_microseconds_accurate(usec); - this->pin_->digital_write(false); - return; - } - - const uint32_t start_time = micros(); - uint32_t current_time = start_time; - - while (current_time - start_time < usec) { - const uint32_t elapsed = current_time - start_time; - this->pin_->digital_write(true); - - delay_microseconds_accurate(std::min(on_time, usec - elapsed)); - this->pin_->digital_write(false); - if (elapsed + on_time >= usec) - return; - - delay_microseconds_accurate(std::min(usec - elapsed - on_time, off_time)); - - current_time = micros(); + this->await_target_time_(); + this->pin_->digital_write(true); + + const uint32_t target = this->target_time_ + usec; + if (this->carrier_duty_percent_ < 100 && (on_time > 0 || off_time > 0)) { + while (true) { // Modulate with carrier frequency + this->target_time_ += on_time; + if (this->target_time_ >= target) + break; + this->await_target_time_(); + this->pin_->digital_write(false); + + this->target_time_ += off_time; + if (this->target_time_ >= target) + break; + this->await_target_time_(); + this->pin_->digital_write(true); + } } + this->target_time_ = target; } + void RemoteTransmitterComponent::space_(uint32_t usec) { + this->await_target_time_(); this->pin_->digital_write(false); - delay_microseconds_accurate(usec); + this->target_time_ += usec; } + void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) { ESP_LOGD(TAG, "Sending remote code..."); uint32_t on_time, off_time; this->calculate_on_off_time_(this->temp_.get_carrier_frequency(), &on_time, &off_time); + this->target_time_ = 0; for (uint32_t i = 0; i < send_times; i++) { - { - InterruptLock lock; - for (int32_t item : this->temp_.get_data()) { - if (item > 0) { - const auto length = uint32_t(item); - this->mark_(on_time, off_time, length); - } else { - const auto length = uint32_t(-item); - this->space_(length); - } - App.feed_wdt(); + for (int32_t item : this->temp_.get_data()) { + if (item > 0) { + const auto length = uint32_t(item); + this->mark_(on_time, off_time, length); + } else { + const auto length = uint32_t(-item); + this->space_(length); } + App.feed_wdt(); } + this->await_target_time_(); // wait for duration of last pulse + this->pin_->digital_write(false); - if (i + 1 < send_times) { - delay_microseconds_accurate(send_wait); - } + if (i + 1 < send_times) + this->target_time_ += send_wait; } } diff --git a/esphome/components/restart/switch.py b/esphome/components/restart/switch.py index 4f1904e273..de30392b45 100644 --- a/esphome/components/restart/switch.py +++ b/esphome/components/restart/switch.py @@ -1,7 +1,14 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import switch -from esphome.const import CONF_ID, CONF_INVERTED, CONF_ICON, ICON_RESTART +from esphome.const import ( + CONF_ENTITY_CATEGORY, + CONF_ID, + CONF_INVERTED, + CONF_ICON, + ENTITY_CATEGORY_CONFIG, + ICON_RESTART, +) restart_ns = cg.esphome_ns.namespace("restart") RestartSwitch = restart_ns.class_("RestartSwitch", switch.Switch, cg.Component) @@ -13,6 +20,9 @@ CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend( "Restart switches do not support inverted mode!" ), cv.Optional(CONF_ICON, default=ICON_RESTART): switch.icon, + cv.Optional( + CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_CONFIG + ): cv.entity_category, } ).extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/components/rotary_encoder/rotary_encoder.cpp b/esphome/components/rotary_encoder/rotary_encoder.cpp index 7c95fac98e..aff8fc381c 100644 --- a/esphome/components/rotary_encoder/rotary_encoder.cpp +++ b/esphome/components/rotary_encoder/rotary_encoder.cpp @@ -125,6 +125,22 @@ void IRAM_ATTR HOT RotaryEncoderSensorStore::gpio_intr(RotaryEncoderSensorStore void RotaryEncoderSensor::setup() { ESP_LOGCONFIG(TAG, "Setting up Rotary Encoder '%s'...", this->name_.c_str()); + + int32_t initial_value = 0; + switch (this->restore_mode_) { + case ROTARY_ENCODER_RESTORE_DEFAULT_ZERO: + this->rtc_ = global_preferences->make_preference(this->get_object_id_hash()); + if (!this->rtc_.load(&initial_value)) { + initial_value = 0; + } + break; + case ROTARY_ENCODER_ALWAYS_ZERO: + initial_value = 0; + break; + } + this->store_.counter = initial_value; + this->store_.last_read = initial_value; + this->pin_a_->setup(); this->store_.pin_a = this->pin_a_->to_isr(); this->pin_b_->setup(); @@ -142,6 +158,18 @@ void RotaryEncoderSensor::dump_config() { LOG_PIN(" Pin A: ", this->pin_a_); LOG_PIN(" Pin B: ", this->pin_b_); LOG_PIN(" Pin I: ", this->pin_i_); + + const LogString *restore_mode = LOG_STR(""); + switch (this->restore_mode_) { + case ROTARY_ENCODER_RESTORE_DEFAULT_ZERO: + restore_mode = LOG_STR("Restore (Defaults to zero)"); + break; + case ROTARY_ENCODER_ALWAYS_ZERO: + restore_mode = LOG_STR("Always zero"); + break; + } + ESP_LOGCONFIG(TAG, " Restore Mode: %s", LOG_STR_ARG(restore_mode)); + switch (this->store_.resolution) { case ROTARY_ENCODER_1_PULSE_PER_CYCLE: ESP_LOGCONFIG(TAG, " Resolution: 1 Pulse Per Cycle"); @@ -189,13 +217,20 @@ void RotaryEncoderSensor::loop() { this->store_.counter = 0; } int counter = this->store_.counter; - if (this->store_.last_read != counter) { + if (this->store_.last_read != counter || this->publish_initial_value_) { + if (this->restore_mode_ == ROTARY_ENCODER_RESTORE_DEFAULT_ZERO) { + this->rtc_.save(&counter); + } this->store_.last_read = counter; this->publish_state(counter); + this->publish_initial_value_ = false; } } float RotaryEncoderSensor::get_setup_priority() const { return setup_priority::DATA; } +void RotaryEncoderSensor::set_restore_mode(RotaryEncoderRestoreMode restore_mode) { + this->restore_mode_ = restore_mode; +} void RotaryEncoderSensor::set_resolution(RotaryEncoderResolution mode) { this->store_.resolution = mode; } void RotaryEncoderSensor::set_min_value(int32_t min_value) { this->store_.min_value = min_value; } void RotaryEncoderSensor::set_max_value(int32_t max_value) { this->store_.max_value = max_value; } diff --git a/esphome/components/rotary_encoder/rotary_encoder.h b/esphome/components/rotary_encoder/rotary_encoder.h index 4825e472a1..a69d738fa8 100644 --- a/esphome/components/rotary_encoder/rotary_encoder.h +++ b/esphome/components/rotary_encoder/rotary_encoder.h @@ -10,6 +10,12 @@ namespace esphome { namespace rotary_encoder { +/// All possible restore modes for the rotary encoder +enum RotaryEncoderRestoreMode { + ROTARY_ENCODER_RESTORE_DEFAULT_ZERO, /// try to restore counter, otherwise set to zero + ROTARY_ENCODER_ALWAYS_ZERO, /// do not restore counter, always set to zero +}; + /// All possible resolutions for the rotary encoder enum RotaryEncoderResolution { ROTARY_ENCODER_1_PULSE_PER_CYCLE = @@ -40,6 +46,15 @@ class RotaryEncoderSensor : public sensor::Sensor, public Component { void set_pin_a(InternalGPIOPin *pin_a) { pin_a_ = pin_a; } void set_pin_b(InternalGPIOPin *pin_b) { pin_b_ = pin_b; } + /** Set the restore mode of the rotary encoder. + * + * By default (if possible) the last known counter state is restored. Otherwise the value 0 is used. + * Restoring the state can also be turned off. + * + * @param restore_mode The restore mode to use. + */ + void set_restore_mode(RotaryEncoderRestoreMode restore_mode); + /** Set the resolution of the rotary encoder. * * By default, this component will increment the counter by 1 with every A-B input cycle. @@ -58,6 +73,7 @@ class RotaryEncoderSensor : public sensor::Sensor, public Component { void set_reset_pin(GPIOPin *pin_i) { this->pin_i_ = pin_i; } void set_min_value(int32_t min_value); void set_max_value(int32_t max_value); + void set_publish_initial_value(bool publish_initial_value) { publish_initial_value_ = publish_initial_value; } // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) @@ -79,6 +95,9 @@ class RotaryEncoderSensor : public sensor::Sensor, public Component { InternalGPIOPin *pin_a_; InternalGPIOPin *pin_b_; GPIOPin *pin_i_{nullptr}; /// Index pin, if this is not nullptr, the counter will reset to 0 once this pin is HIGH. + bool publish_initial_value_; + ESPPreferenceObject rtc_; + RotaryEncoderRestoreMode restore_mode_{ROTARY_ENCODER_RESTORE_DEFAULT_ZERO}; RotaryEncoderSensorStore store_{}; diff --git a/esphome/components/rotary_encoder/sensor.py b/esphome/components/rotary_encoder/sensor.py index ef1110c6d8..cd747264b3 100644 --- a/esphome/components/rotary_encoder/sensor.py +++ b/esphome/components/rotary_encoder/sensor.py @@ -14,9 +14,17 @@ from esphome.const import ( CONF_PIN_A, CONF_PIN_B, CONF_TRIGGER_ID, + CONF_RESTORE_MODE, ) rotary_encoder_ns = cg.esphome_ns.namespace("rotary_encoder") + +RotaryEncoderRestoreMode = rotary_encoder_ns.enum("RotaryEncoderRestoreMode") +RESTORE_MODES = { + "RESTORE_DEFAULT_ZERO": RotaryEncoderRestoreMode.ROTARY_ENCODER_RESTORE_DEFAULT_ZERO, + "ALWAYS_ZERO": RotaryEncoderRestoreMode.ROTARY_ENCODER_ALWAYS_ZERO, +} + RotaryEncoderResolution = rotary_encoder_ns.enum("RotaryEncoderResolution") RESOLUTIONS = { 1: RotaryEncoderResolution.ROTARY_ENCODER_1_PULSE_PER_CYCLE, @@ -27,6 +35,7 @@ RESOLUTIONS = { CONF_PIN_RESET = "pin_reset" CONF_ON_CLOCKWISE = "on_clockwise" CONF_ON_ANTICLOCKWISE = "on_anticlockwise" +CONF_PUBLISH_INITIAL_VALUE = "publish_initial_value" RotaryEncoderSensor = rotary_encoder_ns.class_( "RotaryEncoderSensor", sensor.Sensor, cg.Component @@ -70,6 +79,10 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_RESOLUTION, default=1): cv.enum(RESOLUTIONS, int=True), cv.Optional(CONF_MIN_VALUE): cv.int_, cv.Optional(CONF_MAX_VALUE): cv.int_, + cv.Optional(CONF_PUBLISH_INITIAL_VALUE, default=False): cv.boolean, + cv.Optional(CONF_RESTORE_MODE, default="RESTORE_DEFAULT_ZERO"): cv.enum( + RESTORE_MODES, upper=True, space="_" + ), cv.Optional(CONF_ON_CLOCKWISE): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( @@ -99,6 +112,8 @@ async def to_code(config): cg.add(var.set_pin_a(pin_a)) pin_b = await cg.gpio_pin_expression(config[CONF_PIN_B]) cg.add(var.set_pin_b(pin_b)) + cg.add(var.set_publish_initial_value(config[CONF_PUBLISH_INITIAL_VALUE])) + cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE])) if CONF_PIN_RESET in config: pin_i = await cg.gpio_pin_expression(config[CONF_PIN_RESET]) diff --git a/esphome/components/ruuvitag/sensor.py b/esphome/components/ruuvitag/sensor.py index 342a5eff24..2bb9549195 100644 --- a/esphome/components/ruuvitag/sensor.py +++ b/esphome/components/ruuvitag/sensor.py @@ -19,6 +19,7 @@ from esphome.const import ( DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, + ENTITY_CATEGORY_DIAGNOSTIC, STATE_CLASS_MEASUREMENT, STATE_CLASS_NONE, UNIT_CELSIUS, @@ -95,22 +96,26 @@ CONFIG_SCHEMA = ( accuracy_decimals=3, device_class=DEVICE_CLASS_VOLTAGE, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), cv.Optional(CONF_TX_POWER): sensor.sensor_schema( unit_of_measurement=UNIT_DECIBEL_MILLIWATT, accuracy_decimals=0, device_class=DEVICE_CLASS_SIGNAL_STRENGTH, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), cv.Optional(CONF_MOVEMENT_COUNTER): sensor.sensor_schema( icon=ICON_GAUGE, accuracy_decimals=0, state_class=STATE_CLASS_NONE, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), cv.Optional(CONF_MEASUREMENT_SEQUENCE_NUMBER): sensor.sensor_schema( icon=ICON_GAUGE, accuracy_decimals=0, state_class=STATE_CLASS_NONE, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), } ) diff --git a/esphome/components/safe_mode/switch/__init__.py b/esphome/components/safe_mode/switch/__init__.py index 0ad814ff4f..b6c3e852f6 100644 --- a/esphome/components/safe_mode/switch/__init__.py +++ b/esphome/components/safe_mode/switch/__init__.py @@ -3,10 +3,12 @@ import esphome.config_validation as cv from esphome.components import switch from esphome.components.ota import OTAComponent from esphome.const import ( + CONF_ENTITY_CATEGORY, CONF_ID, CONF_INVERTED, CONF_ICON, CONF_OTA, + ENTITY_CATEGORY_CONFIG, ICON_RESTART_ALERT, ) from .. import safe_mode_ns @@ -23,6 +25,9 @@ CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend( "Safe Mode Restart switches do not support inverted mode!" ), cv.Optional(CONF_ICON, default=ICON_RESTART_ALERT): switch.icon, + cv.Optional( + CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_CONFIG + ): cv.entity_category, } ).extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/components/scd30/scd30.cpp b/esphome/components/scd30/scd30.cpp index d1246d9766..e6d6ec1c1a 100644 --- a/esphome/components/scd30/scd30.cpp +++ b/esphome/components/scd30/scd30.cpp @@ -60,7 +60,7 @@ void SCD30Component::setup() { // According ESP32 clock stretching is typically 30ms and up to 150ms "due to // internal calibration processes". The I2C peripheral only supports 13ms (at // least when running at 80MHz). - // In practise it seems that clock stretching occurs during this calibration + // In practice it seems that clock stretching occurs during this calibration // calls. It also seems that delays in between calls makes them // disappear/shorter. Hence work around with delays for ESP32. // @@ -69,6 +69,16 @@ void SCD30Component::setup() { delay(30); #endif + if (!this->write_command_(SCD30_CMD_MEASUREMENT_INTERVAL, update_interval_)) { + ESP_LOGE(TAG, "Sensor SCD30 error setting update interval."); + this->error_code_ = MEASUREMENT_INIT_FAILED; + this->mark_failed(); + return; + } +#ifdef USE_ESP32 + delay(30); +#endif + // The start measurement command disables the altitude compensation, if any, so we only set it if it's turned on if (this->altitude_compensation_ != 0xFFFF) { if (!this->write_command_(SCD30_CMD_ALTITUDE_COMPENSATION, altitude_compensation_)) { @@ -99,6 +109,12 @@ void SCD30Component::setup() { this->mark_failed(); return; } + + // check each 500ms if data is ready, and read it in that case + this->set_interval("status-check", 500, [this]() { + if (this->is_data_ready_()) + this->update(); + }); } void SCD30Component::dump_config() { @@ -128,19 +144,13 @@ void SCD30Component::dump_config() { ESP_LOGCONFIG(TAG, " Automatic self calibration: %s", ONOFF(this->enable_asc_)); ESP_LOGCONFIG(TAG, " Ambient pressure compensation: %dmBar", this->ambient_pressure_compensation_); ESP_LOGCONFIG(TAG, " Temperature offset: %.2f °C", this->temperature_offset_); - LOG_UPDATE_INTERVAL(this); + ESP_LOGCONFIG(TAG, " Update interval: %ds", this->update_interval_); LOG_SENSOR(" ", "CO2", this->co2_sensor_); LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); } void SCD30Component::update() { - /// Check if measurement is ready before reading the value - if (!this->write_command_(SCD30_CMD_GET_DATA_READY_STATUS)) { - this->status_set_warning(); - return; - } - uint16_t raw_read_status[1]; if (!this->read_data_(raw_read_status, 1) || raw_read_status[0] == 0x00) { this->status_set_warning(); @@ -186,6 +196,17 @@ void SCD30Component::update() { }); } +bool SCD30Component::is_data_ready_() { + if (!this->write_command_(SCD30_CMD_GET_DATA_READY_STATUS)) { + return false; + } + uint16_t is_data_ready; + if (!this->read_data_(&is_data_ready, 1)) { + return false; + } + return is_data_ready == 1; +} + bool SCD30Component::write_command_(uint16_t command) { // Warning ugly, trick the I2Ccomponent base by setting register to the first 8 bit. return this->write_byte(command >> 8, command & 0xFF); diff --git a/esphome/components/scd30/scd30.h b/esphome/components/scd30/scd30.h index f11b7cc1f4..64193d0cb6 100644 --- a/esphome/components/scd30/scd30.h +++ b/esphome/components/scd30/scd30.h @@ -8,7 +8,7 @@ namespace esphome { namespace scd30 { /// This class implements support for the Sensirion scd30 i2c GAS (VOC and CO2eq) sensors. -class SCD30Component : public PollingComponent, public i2c::I2CDevice { +class SCD30Component : public Component, public i2c::I2CDevice { public: void set_co2_sensor(sensor::Sensor *co2) { co2_sensor_ = co2; } void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; } @@ -19,9 +19,10 @@ class SCD30Component : public PollingComponent, public i2c::I2CDevice { ambient_pressure_compensation_ = (uint16_t)(pressure * 1000); } void set_temperature_offset(float offset) { temperature_offset_ = offset; } + void set_update_interval(uint16_t interval) { update_interval_ = interval; } void setup() override; - void update() override; + void update(); void dump_config() override; float get_setup_priority() const override { return setup_priority::DATA; } @@ -30,6 +31,7 @@ class SCD30Component : public PollingComponent, public i2c::I2CDevice { bool write_command_(uint16_t command, uint16_t data); bool read_data_(uint16_t *data, uint8_t len); uint8_t sht_crc_(uint8_t data1, uint8_t data2); + bool is_data_ready_(); enum ErrorCode { COMMUNICATION_FAILED, @@ -41,6 +43,7 @@ class SCD30Component : public PollingComponent, public i2c::I2CDevice { uint16_t altitude_compensation_{0xFFFF}; uint16_t ambient_pressure_compensation_{0x0000}; float temperature_offset_{0.0}; + uint16_t update_interval_{0xFFFF}; sensor::Sensor *co2_sensor_{nullptr}; sensor::Sensor *humidity_sensor_{nullptr}; diff --git a/esphome/components/scd30/sensor.py b/esphome/components/scd30/sensor.py index c0317c96e0..cd25649f2a 100644 --- a/esphome/components/scd30/sensor.py +++ b/esphome/components/scd30/sensor.py @@ -1,3 +1,4 @@ +from esphome import core import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor @@ -6,6 +7,7 @@ from esphome.const import ( CONF_HUMIDITY, CONF_TEMPERATURE, CONF_CO2, + CONF_UPDATE_INTERVAL, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, @@ -18,7 +20,7 @@ from esphome.const import ( DEPENDENCIES = ["i2c"] scd30_ns = cg.esphome_ns.namespace("scd30") -SCD30Component = scd30_ns.class_("SCD30Component", cg.PollingComponent, i2c.I2CDevice) +SCD30Component = scd30_ns.class_("SCD30Component", cg.Component, i2c.I2CDevice) CONF_AUTOMATIC_SELF_CALIBRATION = "automatic_self_calibration" CONF_ALTITUDE_COMPENSATION = "altitude_compensation" @@ -55,9 +57,15 @@ CONFIG_SCHEMA = ( ), cv.Optional(CONF_AMBIENT_PRESSURE_COMPENSATION, default=0): cv.pressure, cv.Optional(CONF_TEMPERATURE_OFFSET): cv.temperature, + cv.Optional(CONF_UPDATE_INTERVAL, default="60s"): cv.All( + cv.positive_time_period_seconds, + cv.Range( + min=core.TimePeriod(seconds=1), max=core.TimePeriod(seconds=1800) + ), + ), } ) - .extend(cv.polling_component_schema("60s")) + .extend(cv.COMPONENT_SCHEMA) .extend(i2c.i2c_device_schema(0x61)) ) @@ -81,6 +89,8 @@ async def to_code(config): if CONF_TEMPERATURE_OFFSET in config: cg.add(var.set_temperature_offset(config[CONF_TEMPERATURE_OFFSET])) + cg.add(var.set_update_interval(config[CONF_UPDATE_INTERVAL])) + if CONF_CO2 in config: sens = await sensor.new_sensor(config[CONF_CO2]) cg.add(var.set_co2_sensor(sens)) diff --git a/esphome/components/sdp3x/sdp3x.cpp b/esphome/components/sdp3x/sdp3x.cpp index ba7a028f8e..b0d8bcc6c4 100644 --- a/esphome/components/sdp3x/sdp3x.cpp +++ b/esphome/components/sdp3x/sdp3x.cpp @@ -1,5 +1,6 @@ #include "sdp3x.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" #include "esphome/core/helpers.h" namespace esphome { @@ -25,7 +26,7 @@ void SDP3XComponent::setup() { ESP_LOGW(TAG, "Soft Reset SDP3X failed!"); // This sometimes fails for no good reason } - delay_microseconds_accurate(20000); + delayMicroseconds(20000); if (this->write(SDP3X_READ_ID1, 2) != i2c::ERROR_OK) { ESP_LOGE(TAG, "Read ID1 SDP3X failed!"); diff --git a/esphome/components/select/__init__.py b/esphome/components/select/__init__.py index 7e4047d3c8..c15036e9f9 100644 --- a/esphome/components/select/__init__.py +++ b/esphome/components/select/__init__.py @@ -30,8 +30,7 @@ SelectSetAction = select_ns.class_("SelectSetAction", automation.Action) icon = cv.icon - -SELECT_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend( +SELECT_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( { cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTSelectComponent), cv.GenerateID(): cv.declare_id(Select), diff --git a/esphome/components/senseair/senseair.cpp b/esphome/components/senseair/senseair.cpp index 610892dd9e..50b9e01f17 100644 --- a/esphome/components/senseair/senseair.cpp +++ b/esphome/components/senseair/senseair.cpp @@ -141,12 +141,16 @@ void SenseAirComponent::abc_get_period() { } bool SenseAirComponent::senseair_write_command_(const uint8_t *command, uint8_t *response, uint8_t response_length) { + // Verify we have somewhere to store the response + if (response == nullptr) { + return false; + } + // Write wake up byte required by some S8 sensor models + this->write_byte(0); this->flush(); + delay(5); this->write_array(command, SENSEAIR_REQUEST_LENGTH); - if (response == nullptr) - return true; - bool ret = this->read_array(response, response_length); this->flush(); return ret; diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 4b2e9dc019..d9d226aab6 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -10,6 +10,7 @@ from esphome.const import ( CONF_ACCURACY_DECIMALS, CONF_ALPHA, CONF_BELOW, + CONF_ENTITY_CATEGORY, CONF_EXPIRE_AFTER, CONF_FILTERS, CONF_FROM, @@ -133,7 +134,6 @@ def validate_datapoint(value): # Base -sensor_ns = cg.esphome_ns.namespace("sensor") Sensor = sensor_ns.class_("Sensor", cg.EntityBase) SensorPtr = Sensor.operator("ptr") @@ -226,6 +226,7 @@ def sensor_schema( accuracy_decimals: int = _UNDEF, device_class: str = _UNDEF, state_class: str = _UNDEF, + entity_category: str = _UNDEF, ) -> cv.Schema: schema = SENSOR_SCHEMA if unit_of_measurement is not _UNDEF: @@ -258,6 +259,14 @@ def sensor_schema( schema = schema.extend( {cv.Optional(CONF_STATE_CLASS, default=state_class): validate_state_class} ) + if entity_category is not _UNDEF: + schema = schema.extend( + { + cv.Optional( + CONF_ENTITY_CATEGORY, default=entity_category + ): cv.entity_category + } + ) return schema diff --git a/esphome/components/shutdown/switch.py b/esphome/components/shutdown/switch.py index 30c2bc2b74..49970b4c2f 100644 --- a/esphome/components/shutdown/switch.py +++ b/esphome/components/shutdown/switch.py @@ -1,7 +1,14 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import switch -from esphome.const import CONF_ID, CONF_INVERTED, CONF_ICON, ICON_POWER +from esphome.const import ( + CONF_ENTITY_CATEGORY, + CONF_ID, + CONF_INVERTED, + CONF_ICON, + ENTITY_CATEGORY_CONFIG, + ICON_POWER, +) shutdown_ns = cg.esphome_ns.namespace("shutdown") ShutdownSwitch = shutdown_ns.class_("ShutdownSwitch", switch.Switch, cg.Component) @@ -13,6 +20,9 @@ CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend( "Shutdown switches do not support inverted mode!" ), cv.Optional(CONF_ICON, default=ICON_POWER): switch.icon, + cv.Optional( + CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_CONFIG + ): cv.entity_category, } ).extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/components/sim800l/sim800l.cpp b/esphome/components/sim800l/sim800l.cpp index e48b1ac9bd..eb6d62ca33 100644 --- a/esphome/components/sim800l/sim800l.cpp +++ b/esphome/components/sim800l/sim800l.cpp @@ -128,7 +128,7 @@ void Sim800LComponent::parse_cmd_(std::string message) { if (message.compare(0, 5, "+CSQ:") == 0) { size_t comma = message.find(',', 6); if (comma != 6) { - this->rssi_ = strtol(message.substr(6, comma - 6).c_str(), nullptr, 10); + this->rssi_ = parse_number(message.substr(6, comma - 6)).value_or(0); ESP_LOGD(TAG, "RSSI: %d", this->rssi_); } } @@ -146,7 +146,7 @@ void Sim800LComponent::parse_cmd_(std::string message) { while (end != start) { item++; if (item == 1) { // Slot Index - this->parse_index_ = strtol(message.substr(start, end - start).c_str(), nullptr, 10); + this->parse_index_ = parse_number(message.substr(start, end - start)).value_or(0); } // item 2 = STATUS, usually "REC UNERAD" if (item == 3) { // recipient diff --git a/esphome/components/sntp/sntp_component.cpp b/esphome/components/sntp/sntp_component.cpp index 2b6cd10e80..96be0e9709 100644 --- a/esphome/components/sntp/sntp_component.cpp +++ b/esphome/components/sntp/sntp_component.cpp @@ -3,6 +3,9 @@ #ifdef USE_ESP32 #include "lwip/apps/sntp.h" +#ifdef USE_ESP_IDF +#include "esp_sntp.h" +#endif #endif #ifdef USE_ESP8266 #include "sntp.h" @@ -37,6 +40,9 @@ void SNTPComponent::setup() { if (!this->server_3_.empty()) { sntp_setservername(2, strdup(this->server_3_.c_str())); } +#ifdef USE_ESP_IDF + sntp_set_sync_interval(this->get_update_interval()); +#endif sntp_init(); } @@ -47,7 +53,16 @@ void SNTPComponent::dump_config() { ESP_LOGCONFIG(TAG, " Server 3: '%s'", this->server_3_.c_str()); ESP_LOGCONFIG(TAG, " Timezone: '%s'", this->timezone_.c_str()); } -void SNTPComponent::update() {} +void SNTPComponent::update() { +#ifndef USE_ESP_IDF + // force resync + if (sntp_enabled()) { + sntp_stop(); + this->has_time_ = false; + sntp_init(); + } +#endif +} void SNTPComponent::loop() { if (this->has_time_) return; @@ -56,7 +71,7 @@ void SNTPComponent::loop() { if (!time.is_valid()) return; - ESP_LOGD(TAG, "Synchronized time: %d-%d-%d %d:%d:%d", time.year, time.month, time.day_of_month, time.hour, + ESP_LOGD(TAG, "Synchronized time: %d-%d-%d %d:%02d:%02d", time.year, time.month, time.day_of_month, time.hour, time.minute, time.second); this->time_sync_callback_.call(); this->has_time_ = true; diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py index 3a96cce99b..c917fe1ad8 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -46,7 +46,9 @@ async def to_code(config): mosi = await cg.gpio_pin_expression(config[CONF_MOSI_PIN]) cg.add(var.set_mosi(mosi)) - if CORE.is_esp32: + if CORE.is_esp32 and CORE.using_arduino: + cg.add_library("SPI", None) + if CORE.is_esp8266: cg.add_library("SPI", None) diff --git a/esphome/components/ssd1306_base/__init__.py b/esphome/components/ssd1306_base/__init__.py index bc2e558f1b..e4f62e5ff9 100644 --- a/esphome/components/ssd1306_base/__init__.py +++ b/esphome/components/ssd1306_base/__init__.py @@ -26,6 +26,7 @@ MODELS = { "SSD1306_128X64": SSD1306Model.SSD1306_MODEL_128_64, "SSD1306_96X16": SSD1306Model.SSD1306_MODEL_96_16, "SSD1306_64X48": SSD1306Model.SSD1306_MODEL_64_48, + "SSD1306_64X32": SSD1306Model.SSD1306_MODEL_64_32, "SH1106_128X32": SSD1306Model.SH1106_MODEL_128_32, "SH1106_128X64": SSD1306Model.SH1106_MODEL_128_64, "SH1106_96X16": SSD1306Model.SH1106_MODEL_96_16, @@ -84,7 +85,7 @@ async def setup_ssd1306(var, config): if CONF_FLIP_X in config: cg.add(var.init_flip_x(config[CONF_FLIP_X])) if CONF_FLIP_Y in config: - cg.add(var.init_flip_y(config[CONF_FLIP_X])) + cg.add(var.init_flip_y(config[CONF_FLIP_Y])) if CONF_OFFSET_X in config: cg.add(var.init_offset_x(config[CONF_OFFSET_X])) if CONF_OFFSET_Y in config: diff --git a/esphome/components/ssd1306_base/ssd1306_base.cpp b/esphome/components/ssd1306_base/ssd1306_base.cpp index b1a2538ebd..2537133605 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.cpp +++ b/esphome/components/ssd1306_base/ssd1306_base.cpp @@ -94,6 +94,7 @@ void SSD1306::setup() { case SSD1306_MODEL_128_64: case SH1106_MODEL_128_64: case SSD1306_MODEL_64_48: + case SSD1306_MODEL_64_32: case SH1106_MODEL_64_48: case SSD1305_MODEL_128_32: case SSD1305_MODEL_128_64: @@ -141,6 +142,7 @@ void SSD1306::display() { this->command(SSD1306_COMMAND_COLUMN_ADDRESS); switch (this->model_) { case SSD1306_MODEL_64_48: + case SSD1306_MODEL_64_32: this->command(0x20 + this->offset_x_); this->command(0x20 + this->offset_x_ + this->get_width_internal() - 1); break; @@ -197,6 +199,7 @@ void SSD1306::turn_off() { int SSD1306::get_height_internal() { switch (this->model_) { case SSD1306_MODEL_128_32: + case SSD1306_MODEL_64_32: case SH1106_MODEL_128_32: case SSD1305_MODEL_128_32: return 32; @@ -227,6 +230,7 @@ int SSD1306::get_width_internal() { case SH1106_MODEL_96_16: return 96; case SSD1306_MODEL_64_48: + case SSD1306_MODEL_64_32: case SH1106_MODEL_64_48: return 64; default: @@ -271,6 +275,8 @@ const char *SSD1306::model_str_() { return "SSD1306 128x32"; case SSD1306_MODEL_128_64: return "SSD1306 128x64"; + case SSD1306_MODEL_64_32: + return "SSD1306 64x32"; case SSD1306_MODEL_96_16: return "SSD1306 96x16"; case SSD1306_MODEL_64_48: diff --git a/esphome/components/ssd1306_base/ssd1306_base.h b/esphome/components/ssd1306_base/ssd1306_base.h index 09417a2c10..c77b1985e4 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.h +++ b/esphome/components/ssd1306_base/ssd1306_base.h @@ -12,6 +12,7 @@ enum SSD1306Model { SSD1306_MODEL_128_64, SSD1306_MODEL_96_16, SSD1306_MODEL_64_48, + SSD1306_MODEL_64_32, SH1106_MODEL_128_32, SH1106_MODEL_128_64, SH1106_MODEL_96_16, diff --git a/esphome/components/status/binary_sensor.py b/esphome/components/status/binary_sensor.py index e462bc5385..9367706388 100644 --- a/esphome/components/status/binary_sensor.py +++ b/esphome/components/status/binary_sensor.py @@ -1,7 +1,13 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import binary_sensor -from esphome.const import CONF_ID, CONF_DEVICE_CLASS, DEVICE_CLASS_CONNECTIVITY +from esphome.const import ( + CONF_ENTITY_CATEGORY, + CONF_ID, + CONF_DEVICE_CLASS, + DEVICE_CLASS_CONNECTIVITY, + ENTITY_CATEGORY_DIAGNOSTIC, +) status_ns = cg.esphome_ns.namespace("status") StatusBinarySensor = status_ns.class_( @@ -14,6 +20,9 @@ CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( cv.Optional( CONF_DEVICE_CLASS, default=DEVICE_CLASS_CONNECTIVITY ): binary_sensor.device_class, + cv.Optional( + CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_DIAGNOSTIC + ): cv.entity_category, } ).extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/components/switch/__init__.py b/esphome/components/switch/__init__.py index 88341e0add..08cbccbe35 100644 --- a/esphome/components/switch/__init__.py +++ b/esphome/components/switch/__init__.py @@ -36,6 +36,7 @@ SwitchTurnOffTrigger = switch_ns.class_( icon = cv.icon + SWITCH_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( { cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTSwitchComponent), diff --git a/esphome/components/tcs34725/sensor.py b/esphome/components/tcs34725/sensor.py index 6c74c86faf..fcc56e395f 100644 --- a/esphome/components/tcs34725/sensor.py +++ b/esphome/components/tcs34725/sensor.py @@ -6,6 +6,7 @@ from esphome.const import ( CONF_GAIN, CONF_ID, CONF_ILLUMINANCE, + CONF_GLASS_ATTENUATION_FACTOR, CONF_INTEGRATION_TIME, DEVICE_CLASS_ILLUMINANCE, ICON_LIGHTBULB, @@ -34,8 +35,20 @@ TCS34725_INTEGRATION_TIMES = { "24ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_24MS, "50ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_50MS, "101ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_101MS, + "120ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_120MS, "154ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_154MS, - "700ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_700MS, + "180ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_180MS, + "199ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_199MS, + "240ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_240MS, + "300ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_300MS, + "360ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_360MS, + "401ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_401MS, + "420ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_420MS, + "480ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_480MS, + "499ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_499MS, + "540ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_540MS, + "600ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_600MS, + "614ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_614MS, } TCS34725Gain = tcs34725_ns.enum("TCS34725Gain") @@ -79,6 +92,9 @@ CONFIG_SCHEMA = ( TCS34725_INTEGRATION_TIMES, lower=True ), cv.Optional(CONF_GAIN, default="1X"): cv.enum(TCS34725_GAINS, upper=True), + cv.Optional(CONF_GLASS_ATTENUATION_FACTOR, default=1.0): cv.float_range( + min=1.0 + ), } ) .extend(cv.polling_component_schema("60s")) @@ -93,6 +109,7 @@ async def to_code(config): cg.add(var.set_integration_time(config[CONF_INTEGRATION_TIME])) cg.add(var.set_gain(config[CONF_GAIN])) + cg.add(var.set_glass_attenuation_factor(config[CONF_GLASS_ATTENUATION_FACTOR])) if CONF_RED_CHANNEL in config: sens = await sensor.new_sensor(config[CONF_RED_CHANNEL]) diff --git a/esphome/components/tcs34725/tcs34725.cpp b/esphome/components/tcs34725/tcs34725.cpp index 564d3dcda7..f7ffe2a97d 100644 --- a/esphome/components/tcs34725/tcs34725.cpp +++ b/esphome/components/tcs34725/tcs34725.cpp @@ -26,10 +26,8 @@ void TCS34725Component::setup() { return; } - uint8_t integration_reg = this->integration_time_; - uint8_t gain_reg = this->gain_; - if (!this->write_byte(TCS34725_REGISTER_ATIME, integration_reg) || - !this->write_byte(TCS34725_REGISTER_CONTROL, gain_reg)) { + if (!this->write_byte(TCS34725_REGISTER_ATIME, this->integration_reg_) || + !this->write_byte(TCS34725_REGISTER_CONTROL, this->gain_reg_)) { this->mark_failed(); return; } @@ -61,6 +59,114 @@ void TCS34725Component::dump_config() { LOG_SENSOR(" ", "Color Temperature", this->color_temperature_sensor_); } float TCS34725Component::get_setup_priority() const { return setup_priority::DATA; } + +/*! + * @brief Converts the raw R/G/B values to color temperature in degrees + * Kelvin using the algorithm described in DN40 from Taos (now AMS). + * @param r + * Red value + * @param g + * Green value + * @param b + * Blue value + * @param c + * Clear channel value + * @return Color temperature in degrees Kelvin + */ +void TCS34725Component::calculate_temperature_and_lux_(uint16_t r, uint16_t g, uint16_t b, uint16_t c) { + float r2, g2, b2; /* RGB values minus IR component */ + float sat; /* Digital saturation level */ + float ir; /* Inferred IR content */ + + this->illuminance_ = 0; // Assign 0 value before calculation + this->color_temperature_ = 0; + + const float ga = this->glass_attenuation_; // Glass Attenuation Factor + static const float DF = 310.f; // Device Factor + static const float R_COEF = 0.136f; // + static const float G_COEF = 1.f; // used in lux computation + static const float B_COEF = -0.444f; // + static const float CT_COEF = 3810.f; // Color Temperature Coefficient + static const float CT_OFFSET = 1391.f; // Color Temperatuer Offset + + if (c == 0) { + return; + } + + /* Analog/Digital saturation: + * + * (a) As light becomes brighter, the clear channel will tend to + * saturate first since R+G+B is approximately equal to C. + * (b) The TCS34725 accumulates 1024 counts per 2.4ms of integration + * time, up to a maximum values of 65535. This means analog + * saturation can occur up to an integration time of 153.6ms + * (64*2.4ms=153.6ms). + * (c) If the integration time is > 153.6ms, digital saturation will + * occur before analog saturation. Digital saturation occurs when + * the count reaches 65535. + */ + if ((256 - this->integration_reg_) > 63) { + /* Track digital saturation */ + sat = 65535.f; + } else { + /* Track analog saturation */ + sat = 1024.f * (256.f - this->integration_reg_); + } + + /* Ripple rejection: + * + * (a) An integration time of 50ms or multiples of 50ms are required to + * reject both 50Hz and 60Hz ripple. + * (b) If an integration time faster than 50ms is required, you may need + * to average a number of samples over a 50ms period to reject ripple + * from fluorescent and incandescent light sources. + * + * Ripple saturation notes: + * + * (a) If there is ripple in the received signal, the value read from C + * will be less than the max, but still have some effects of being + * saturated. This means that you can be below the 'sat' value, but + * still be saturating. At integration times >150ms this can be + * ignored, but <= 150ms you should calculate the 75% saturation + * level to avoid this problem. + */ + if (this->integration_time_ < 150) { + /* Adjust sat to 75% to avoid analog saturation if atime < 153.6ms */ + sat -= sat / 4.f; + } + + /* Check for saturation and mark the sample as invalid if true */ + if (c >= sat) { + return; + } + + /* AMS RGB sensors have no IR channel, so the IR content must be */ + /* calculated indirectly. */ + ir = ((r + g + b) > c) ? (r + g + b - c) / 2 : 0; + + /* Remove the IR component from the raw RGB values */ + r2 = r - ir; + g2 = g - ir; + b2 = b - ir; + + if (r2 == 0) { + return; + } + + // Lux Calculation (DN40 3.2) + + float g1 = R_COEF * r2 + G_COEF * g2 + B_COEF * b2; + float cpl = (this->integration_time_ * this->gain_) / (ga * DF); + this->illuminance_ = g1 / cpl; + + // Color Temperature Calculation (DN40) + /* A simple method of measuring color temp is to use the ratio of blue */ + /* to red light, taking IR cancellation into account. */ + this->color_temperature_ = (CT_COEF * b2) / /** Color temp coefficient. */ + r2 + + CT_OFFSET; /** Color temp offset. */ +} + void TCS34725Component::update() { uint16_t raw_c; uint16_t raw_r; @@ -74,6 +180,12 @@ void TCS34725Component::update() { return; } + // May need to fix endianness as the data read over I2C is big-endian, but most ESP platforms are little-endian + raw_c = i2c::i2ctohs(raw_c); + raw_r = i2c::i2ctohs(raw_r); + raw_g = i2c::i2ctohs(raw_g); + raw_b = i2c::i2ctohs(raw_b); + const float channel_c = raw_c / 655.35f; const float channel_r = raw_r / 655.35f; const float channel_g = raw_g / 655.35f; @@ -87,38 +199,54 @@ void TCS34725Component::update() { if (this->blue_sensor_ != nullptr) this->blue_sensor_->publish_state(channel_b); - // Formulae taken from Adafruit TCS35725 library - float illuminance = (-0.32466f * channel_r) + (1.57837f * channel_g) + (-0.73191f * channel_b); + if (this->illuminance_sensor_ || this->color_temperature_sensor_) { + calculate_temperature_and_lux_(raw_r, raw_g, raw_b, raw_c); + } + if (this->illuminance_sensor_ != nullptr) - this->illuminance_sensor_->publish_state(illuminance); + this->illuminance_sensor_->publish_state(this->illuminance_); - // Color temperature - // 1. Convert RGB to XYZ color space - const float x = (-0.14282f * raw_r) + (1.54924f * raw_g) + (-0.95641f * raw_b); - const float y = (-0.32466f * raw_r) + (1.57837f * raw_g) + (-0.73191f * raw_b); - const float z = (-0.68202f * raw_r) + (0.77073f * raw_g) + (0.56332f * raw_b); - - // 2. Calculate chromacity coordinates - const float xc = (x) / (x + y + z); - const float yc = (y) / (x + y + z); - - // 3. Use McCamy's formula to determine the color temperature - const float n = (xc - 0.3320f) / (0.1858f - yc); - - // 4. final color temperature in Kelvin. - const float color_temperature = (449.0f * powf(n, 3.0f)) + (3525.0f * powf(n, 2.0f)) + (6823.3f * n) + 5520.33f; if (this->color_temperature_sensor_ != nullptr) - this->color_temperature_sensor_->publish_state(color_temperature); + this->color_temperature_sensor_->publish_state(this->color_temperature_); ESP_LOGD(TAG, "Got R=%.1f%%,G=%.1f%%,B=%.1f%%,C=%.1f%% Illuminance=%.1flx Color Temperature=%.1fK", channel_r, - channel_g, channel_b, channel_c, illuminance, color_temperature); + channel_g, channel_b, channel_c, this->illuminance_, this->color_temperature_); this->status_clear_warning(); } void TCS34725Component::set_integration_time(TCS34725IntegrationTime integration_time) { - this->integration_time_ = integration_time; + this->integration_reg_ = integration_time; + this->integration_time_ = (256.f - integration_time) * 2.4f; +} +void TCS34725Component::set_gain(TCS34725Gain gain) { + this->gain_reg_ = gain; + switch (gain) { + case TCS34725Gain::TCS34725_GAIN_1X: + this->gain_ = 1.f; + break; + case TCS34725Gain::TCS34725_GAIN_4X: + this->gain_ = 4.f; + break; + case TCS34725Gain::TCS34725_GAIN_16X: + this->gain_ = 16.f; + break; + case TCS34725Gain::TCS34725_GAIN_60X: + this->gain_ = 60.f; + break; + default: + this->gain_ = 1.f; + break; + } +} + +void TCS34725Component::set_glass_attenuation_factor(float ga) { + // The Glass Attenuation (FA) factor used to compensate for lower light + // levels at the device due to the possible presence of glass. The GA is + // the inverse of the glass transmissivity (T), so GA = 1/T. A transmissivity + // of 50% gives GA = 1 / 0.50 = 2. If no glass is present, use GA = 1. + // See Application Note: DN40-Rev 1.0 + this->glass_attenuation_ = ga; } -void TCS34725Component::set_gain(TCS34725Gain gain) { this->gain_ = gain; } } // namespace tcs34725 } // namespace esphome diff --git a/esphome/components/tcs34725/tcs34725.h b/esphome/components/tcs34725/tcs34725.h index b914db0eb0..47ed2959c6 100644 --- a/esphome/components/tcs34725/tcs34725.h +++ b/esphome/components/tcs34725/tcs34725.h @@ -12,8 +12,20 @@ enum TCS34725IntegrationTime { TCS34725_INTEGRATION_TIME_24MS = 0xF6, TCS34725_INTEGRATION_TIME_50MS = 0xEB, TCS34725_INTEGRATION_TIME_101MS = 0xD5, + TCS34725_INTEGRATION_TIME_120MS = 0xCE, TCS34725_INTEGRATION_TIME_154MS = 0xC0, - TCS34725_INTEGRATION_TIME_700MS = 0x00, + TCS34725_INTEGRATION_TIME_180MS = 0xB5, + TCS34725_INTEGRATION_TIME_199MS = 0xAD, + TCS34725_INTEGRATION_TIME_240MS = 0x9C, + TCS34725_INTEGRATION_TIME_300MS = 0x83, + TCS34725_INTEGRATION_TIME_360MS = 0x6A, + TCS34725_INTEGRATION_TIME_401MS = 0x59, + TCS34725_INTEGRATION_TIME_420MS = 0x51, + TCS34725_INTEGRATION_TIME_480MS = 0x38, + TCS34725_INTEGRATION_TIME_499MS = 0x30, + TCS34725_INTEGRATION_TIME_540MS = 0x1F, + TCS34725_INTEGRATION_TIME_600MS = 0x06, + TCS34725_INTEGRATION_TIME_614MS = 0x00, }; enum TCS34725Gain { @@ -27,6 +39,7 @@ class TCS34725Component : public PollingComponent, public i2c::I2CDevice { public: void set_integration_time(TCS34725IntegrationTime integration_time); void set_gain(TCS34725Gain gain); + void set_glass_attenuation_factor(float ga); void set_clear_sensor(sensor::Sensor *clear_sensor) { clear_sensor_ = clear_sensor; } void set_red_sensor(sensor::Sensor *red_sensor) { red_sensor_ = red_sensor; } @@ -49,8 +62,16 @@ class TCS34725Component : public PollingComponent, public i2c::I2CDevice { sensor::Sensor *blue_sensor_{nullptr}; sensor::Sensor *illuminance_sensor_{nullptr}; sensor::Sensor *color_temperature_sensor_{nullptr}; - TCS34725IntegrationTime integration_time_{TCS34725_INTEGRATION_TIME_2_4MS}; - TCS34725Gain gain_{TCS34725_GAIN_1X}; + float integration_time_{2.4}; + float gain_{1.0}; + float glass_attenuation_{1.0}; + float illuminance_; + float color_temperature_; + + private: + void calculate_temperature_and_lux_(uint16_t r, uint16_t g, uint16_t b, uint16_t c); + uint8_t integration_reg_{TCS34725_INTEGRATION_TIME_2_4MS}; + uint8_t gain_reg_{TCS34725_GAIN_1X}; }; } // namespace tcs34725 diff --git a/esphome/components/teleinfo/sensor/teleinfo_sensor.cpp b/esphome/components/teleinfo/sensor/teleinfo_sensor.cpp index 4e4cd9f9e6..ad9c6dae00 100644 --- a/esphome/components/teleinfo/sensor/teleinfo_sensor.cpp +++ b/esphome/components/teleinfo/sensor/teleinfo_sensor.cpp @@ -6,8 +6,8 @@ namespace teleinfo { static const char *const TAG = "teleinfo_sensor"; TeleInfoSensor::TeleInfoSensor(const char *tag) { this->tag = std::string(tag); } void TeleInfoSensor::publish_val(const std::string &val) { - auto newval = parse_float(val); - publish_state(*newval); + auto newval = parse_number(val).value_or(0.0f); + publish_state(newval); } void TeleInfoSensor::dump_config() { LOG_SENSOR(" ", "Teleinfo Sensor", this); } } // namespace teleinfo diff --git a/esphome/components/template/number/__init__.py b/esphome/components/template/number/__init__.py index 887f6b15ad..3dec7066d3 100644 --- a/esphome/components/template/number/__init__.py +++ b/esphome/components/template/number/__init__.py @@ -35,6 +35,9 @@ def validate(config): raise cv.Invalid("initial_value cannot be used with lambda") if CONF_RESTORE_VALUE in config: raise cv.Invalid("restore_value cannot be used with lambda") + elif CONF_INITIAL_VALUE not in config: + config[CONF_INITIAL_VALUE] = config[CONF_MIN_VALUE] + if not config[CONF_OPTIMISTIC] and CONF_SET_ACTION not in config: raise cv.Invalid( "Either optimistic mode must be enabled, or set_action must be set, to handle the number being set." @@ -80,8 +83,7 @@ async def to_code(config): else: cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) - if CONF_INITIAL_VALUE in config: - cg.add(var.set_initial_value(config[CONF_INITIAL_VALUE])) + cg.add(var.set_initial_value(config[CONF_INITIAL_VALUE])) if CONF_RESTORE_VALUE in config: cg.add(var.set_restore_value(config[CONF_RESTORE_VALUE])) diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index 5c739e1d0a..a070e6f86d 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -108,6 +108,7 @@ async def substitute_filter_to_code(config, filter_id): icon = cv.icon + TEXT_SENSOR_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend( { cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTTextSensor), diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py index 5c2155d764..2d73d0aef9 100644 --- a/esphome/components/time/__init__.py +++ b/esphome/components/time/__init__.py @@ -1,7 +1,6 @@ import logging from importlib import resources from typing import Optional -from datetime import timezone import tzlocal @@ -66,14 +65,11 @@ def _extract_tz_string(tzfile: bytes) -> str: def detect_tz() -> str: - localzone = tzlocal.get_localzone() - if localzone is timezone.utc: - return "UTC0" - if not hasattr(localzone, "key"): + iana_key = tzlocal.get_localzone_name() + if iana_key is None: raise cv.Invalid( "Could not automatically determine timezone, please set timezone manually." ) - iana_key = localzone.key _LOGGER.info("Detected timezone '%s'", iana_key) tzfile = _load_tzdata(iana_key) if tzfile is None: diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index 35af3eedf7..53209dfc7b 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -14,6 +14,16 @@ from esphome.const import ( CONF_DATA, CONF_RX_BUFFER_SIZE, CONF_INVERT, + CONF_TRIGGER_ID, + CONF_SEQUENCE, + CONF_TIMEOUT, + CONF_DEBUG, + CONF_DIRECTION, + CONF_AFTER, + CONF_BYTES, + CONF_DELIMITER, + CONF_DUMMY_RECEIVER, + CONF_DUMMY_RECEIVER_ID, ) from esphome.core import CORE @@ -31,6 +41,8 @@ ESP8266UartComponent = uart_ns.class_( UARTDevice = uart_ns.class_("UARTDevice") UARTWriteAction = uart_ns.class_("UARTWriteAction", automation.Action) +UARTDebugger = uart_ns.class_("UARTDebugger", cg.Component, automation.Action) +UARTDummyReceiver = uart_ns.class_("UARTDummyReceiver", cg.Component) MULTI_CONF = True @@ -75,6 +87,34 @@ CONF_STOP_BITS = "stop_bits" CONF_DATA_BITS = "data_bits" CONF_PARITY = "parity" +UARTDirection = uart_ns.enum("UARTDirection") +UART_DIRECTIONS = { + "RX": UARTDirection.UART_DIRECTION_RX, + "TX": UARTDirection.UART_DIRECTION_TX, + "BOTH": UARTDirection.UART_DIRECTION_BOTH, +} + +DEBUG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(UARTDebugger), + cv.Optional(CONF_DIRECTION, default="BOTH"): cv.enum( + UART_DIRECTIONS, upper=True + ), + cv.Optional(CONF_AFTER): cv.Schema( + { + cv.Optional(CONF_BYTES, default=256): cv.validate_bytes, + cv.Optional( + CONF_TIMEOUT, default="100ms" + ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_DELIMITER): cv.templatable(validate_raw_data), + } + ), + cv.Required(CONF_SEQUENCE): automation.validate_automation(), + cv.Optional(CONF_DUMMY_RECEIVER, default=False): cv.boolean, + cv.GenerateID(CONF_DUMMY_RECEIVER_ID): cv.declare_id(UARTDummyReceiver), + } +) + CONFIG_SCHEMA = cv.All( cv.Schema( { @@ -91,12 +131,38 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_INVERT): cv.invalid( "This option has been removed. Please instead use invert in the tx/rx pin schemas." ), + cv.Optional(CONF_DEBUG): DEBUG_SCHEMA, } ).extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key(CONF_TX_PIN, CONF_RX_PIN), ) +async def debug_to_code(config, parent): + trigger = cg.new_Pvariable(config[CONF_TRIGGER_ID], parent) + await cg.register_component(trigger, config) + for action in config[CONF_SEQUENCE]: + await automation.build_automation( + trigger, + [(UARTDirection, "direction"), (cg.std_vector.template(cg.uint8), "bytes")], + action, + ) + cg.add(trigger.set_direction(config[CONF_DIRECTION])) + after = config[CONF_AFTER] + cg.add(trigger.set_after_bytes(after[CONF_BYTES])) + cg.add(trigger.set_after_timeout(after[CONF_TIMEOUT])) + if CONF_DELIMITER in after: + data = after[CONF_DELIMITER] + if isinstance(data, bytes): + data = list(data) + for byte in after[CONF_DELIMITER]: + cg.add(trigger.add_delimiter_byte(byte)) + if config[CONF_DUMMY_RECEIVER]: + dummy = cg.new_Pvariable(config[CONF_DUMMY_RECEIVER_ID], parent) + await cg.register_component(dummy, {}) + cg.add_define("USE_UART_DEBUGGER") + + async def to_code(config): cg.add_global(uart_ns.using) var = cg.new_Pvariable(config[CONF_ID]) @@ -115,6 +181,9 @@ async def to_code(config): cg.add(var.set_data_bits(config[CONF_DATA_BITS])) cg.add(var.set_parity(config[CONF_PARITY])) + if CONF_DEBUG in config: + await debug_to_code(config[CONF_DEBUG], var) + # A schema to use for all UART devices, all UART integrations must extend this! UART_DEVICE_SCHEMA = cv.Schema( diff --git a/esphome/components/uart/uart.h b/esphome/components/uart/uart.h index c368f9ed6b..d41dbe26e6 100644 --- a/esphome/components/uart/uart.h +++ b/esphome/components/uart/uart.h @@ -45,17 +45,17 @@ class UARTDevice { // Compat APIs int read() { uint8_t data; - if (!read_byte(&data)) + if (!this->read_byte(&data)) return -1; return data; } size_t write(uint8_t data) { - write_byte(data); + this->write_byte(data); return 1; } int peek() { uint8_t data; - if (!peek_byte(&data)) + if (!this->peek_byte(&data)) return -1; return data; } diff --git a/esphome/components/uart/uart_component.h b/esphome/components/uart/uart_component.h index de85cd2ca3..73694d3db7 100644 --- a/esphome/components/uart/uart_component.h +++ b/esphome/components/uart/uart_component.h @@ -2,9 +2,13 @@ #include #include +#include "esphome/core/defines.h" #include "esphome/core/component.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" +#ifdef USE_UART_DEBUGGER +#include "esphome/core/automation.h" +#endif namespace esphome { namespace uart { @@ -15,6 +19,14 @@ enum UARTParityOptions { UART_CONFIG_PARITY_ODD, }; +#ifdef USE_UART_DEBUGGER +enum UARTDirection { + UART_DIRECTION_RX, + UART_DIRECTION_TX, + UART_DIRECTION_BOTH, +}; +#endif + const LogString *parity_to_str(UARTParityOptions parity); class UARTComponent { @@ -50,6 +62,12 @@ class UARTComponent { void set_baud_rate(uint32_t baud_rate) { baud_rate_ = baud_rate; } uint32_t get_baud_rate() const { return baud_rate_; } +#ifdef USE_UART_DEBUGGER + void add_debug_callback(std::function &&callback) { + this->debug_callback_.add(std::move(callback)); + } +#endif + protected: virtual void check_logger_conflict() = 0; bool check_read_timeout_(size_t len = 1); @@ -61,6 +79,9 @@ class UARTComponent { uint8_t stop_bits_; uint8_t data_bits_; UARTParityOptions parity_; +#ifdef USE_UART_DEBUGGER + CallbackManager debug_callback_{}; +#endif }; } // namespace uart diff --git a/esphome/components/uart/uart_component_esp32_arduino.cpp b/esphome/components/uart/uart_component_esp32_arduino.cpp index 1b1ce382f2..95cdde4a43 100644 --- a/esphome/components/uart/uart_component_esp32_arduino.cpp +++ b/esphome/components/uart/uart_component_esp32_arduino.cpp @@ -117,26 +117,32 @@ void ESP32ArduinoUARTComponent::dump_config() { void ESP32ArduinoUARTComponent::write_array(const uint8_t *data, size_t len) { this->hw_serial_->write(data, len); +#ifdef USE_UART_DEBUGGER for (size_t i = 0; i < len; i++) { - ESP_LOGVV(TAG, " Wrote 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(data[i]), data[i]); + this->debug_callback_.call(UART_DIRECTION_TX, data[i]); } +#endif } + bool ESP32ArduinoUARTComponent::peek_byte(uint8_t *data) { if (!this->check_read_timeout_()) return false; *data = this->hw_serial_->peek(); return true; } + bool ESP32ArduinoUARTComponent::read_array(uint8_t *data, size_t len) { if (!this->check_read_timeout_(len)) return false; this->hw_serial_->readBytes(data, len); +#ifdef USE_UART_DEBUGGER for (size_t i = 0; i < len; i++) { - ESP_LOGVV(TAG, " Read 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(data[i]), data[i]); + this->debug_callback_.call(UART_DIRECTION_RX, data[i]); } - +#endif return true; } + int ESP32ArduinoUARTComponent::available() { return this->hw_serial_->available(); } void ESP32ArduinoUARTComponent::flush() { ESP_LOGVV(TAG, " Flushing..."); diff --git a/esphome/components/uart/uart_component_esp8266.cpp b/esphome/components/uart/uart_component_esp8266.cpp index 973306cde2..c367de05bb 100644 --- a/esphome/components/uart/uart_component_esp8266.cpp +++ b/esphome/components/uart/uart_component_esp8266.cpp @@ -130,9 +130,11 @@ void ESP8266UartComponent::write_array(const uint8_t *data, size_t len) { for (size_t i = 0; i < len; i++) this->sw_serial_->write_byte(data[i]); } +#ifdef USE_UART_DEBUGGER for (size_t i = 0; i < len; i++) { - ESP_LOGVV(TAG, " Wrote 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(data[i]), data[i]); + this->debug_callback_.call(UART_DIRECTION_TX, data[i]); } +#endif } bool ESP8266UartComponent::peek_byte(uint8_t *data) { if (!this->check_read_timeout_()) @@ -153,10 +155,11 @@ bool ESP8266UartComponent::read_array(uint8_t *data, size_t len) { for (size_t i = 0; i < len; i++) data[i] = this->sw_serial_->read_byte(); } +#ifdef USE_UART_DEBUGGER for (size_t i = 0; i < len; i++) { - ESP_LOGVV(TAG, " Read 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(data[i]), data[i]); + this->debug_callback_.call(UART_DIRECTION_RX, data[i]); } - +#endif return true; } int ESP8266UartComponent::available() { diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 1cccd5821e..4d6a6af0fc 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -130,10 +130,13 @@ void IDFUARTComponent::write_array(const uint8_t *data, size_t len) { xSemaphoreTake(this->lock_, portMAX_DELAY); uart_write_bytes(this->uart_num_, data, len); xSemaphoreGive(this->lock_); +#ifdef USE_UART_DEBUGGER for (size_t i = 0; i < len; i++) { - ESP_LOGVV(TAG, " Wrote 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(data[i]), data[i]); + this->debug_callback_.call(UART_DIRECTION_TX, data[i]); } +#endif } + bool IDFUARTComponent::peek_byte(uint8_t *data) { if (!this->check_read_timeout_()) return false; @@ -152,6 +155,7 @@ bool IDFUARTComponent::peek_byte(uint8_t *data) { xSemaphoreGive(this->lock_); return true; } + bool IDFUARTComponent::read_array(uint8_t *data, size_t len) { size_t length_to_read = len; if (!this->check_read_timeout_(len)) @@ -165,12 +169,12 @@ bool IDFUARTComponent::read_array(uint8_t *data, size_t len) { } if (length_to_read > 0) uart_read_bytes(this->uart_num_, data, length_to_read, 20 / portTICK_RATE_MS); - xSemaphoreGive(this->lock_); +#ifdef USE_UART_DEBUGGER for (size_t i = 0; i < len; i++) { - ESP_LOGVV(TAG, " Read 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(data[i]), data[i]); + this->debug_callback_.call(UART_DIRECTION_RX, data[i]); } - +#endif return true; } diff --git a/esphome/components/uart/uart_debugger.cpp b/esphome/components/uart/uart_debugger.cpp new file mode 100644 index 0000000000..9a535656a2 --- /dev/null +++ b/esphome/components/uart/uart_debugger.cpp @@ -0,0 +1,193 @@ +#include "esphome/core/defines.h" +#ifdef USE_UART_DEBUGGER + +#include +#include "uart_debugger.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace uart { + +static const char *const TAG = "uart_debug"; + +UARTDebugger::UARTDebugger(UARTComponent *parent) { + parent->add_debug_callback([this](UARTDirection direction, uint8_t byte) { + if (!this->is_my_direction_(direction) || this->is_recursive_()) { + return; + } + this->trigger_after_direction_change_(direction); + this->store_byte_(direction, byte); + this->trigger_after_delimiter_(byte); + this->trigger_after_bytes_(); + }); +} + +void UARTDebugger::loop() { this->trigger_after_timeout_(); } + +bool UARTDebugger::is_my_direction_(UARTDirection direction) { + return this->for_direction_ == UART_DIRECTION_BOTH || this->for_direction_ == direction; +} + +bool UARTDebugger::is_recursive_() { return this->is_triggering_; } + +void UARTDebugger::trigger_after_direction_change_(UARTDirection direction) { + if (this->has_buffered_bytes_() && this->for_direction_ == UART_DIRECTION_BOTH && + this->last_direction_ != direction) { + this->fire_trigger_(); + } +} + +void UARTDebugger::store_byte_(UARTDirection direction, uint8_t byte) { + this->bytes_.push_back(byte); + this->last_direction_ = direction; + this->last_time_ = millis(); +} + +void UARTDebugger::trigger_after_delimiter_(uint8_t byte) { + if (this->after_delimiter_.empty() || !this->has_buffered_bytes_()) { + return; + } + if (this->after_delimiter_[this->after_delimiter_pos_] != byte) { + this->after_delimiter_pos_ = 0; + return; + } + this->after_delimiter_pos_++; + if (this->after_delimiter_pos_ == this->after_delimiter_.size()) { + this->fire_trigger_(); + this->after_delimiter_pos_ = 0; + } +} + +void UARTDebugger::trigger_after_bytes_() { + if (this->has_buffered_bytes_() && this->after_bytes_ > 0 && this->bytes_.size() >= this->after_bytes_) { + this->fire_trigger_(); + } +} + +void UARTDebugger::trigger_after_timeout_() { + if (this->has_buffered_bytes_() && this->after_timeout_ > 0 && millis() - this->last_time_ >= this->after_timeout_) { + this->fire_trigger_(); + } +} + +bool UARTDebugger::has_buffered_bytes_() { return !this->bytes_.empty(); } + +void UARTDebugger::fire_trigger_() { + this->is_triggering_ = true; + trigger(this->last_direction_, this->bytes_); + this->bytes_.clear(); + this->is_triggering_ = false; +} + +void UARTDummyReceiver::loop() { + // Reading up to a limited number of bytes, to make sure that this loop() + // won't lock up the system on a continuous incoming stream of bytes. + uint8_t data; + int count = 50; + while (this->available() && count--) { + this->read_byte(&data); + } +} + +void UARTDebug::log_hex(UARTDirection direction, std::vector bytes, uint8_t separator) { + std::string res; + if (direction == UART_DIRECTION_RX) { + res += "<<< "; + } else { + res += ">>> "; + } + size_t len = bytes.size(); + char buf[5]; + for (size_t i = 0; i < len; i++) { + if (i > 0) { + res += separator; + } + sprintf(buf, "%02X", bytes[i]); + res += buf; + } + ESP_LOGD(TAG, "%s", res.c_str()); +} + +void UARTDebug::log_string(UARTDirection direction, std::vector bytes) { + std::string res; + if (direction == UART_DIRECTION_RX) { + res += "<<< \""; + } else { + res += ">>> \""; + } + size_t len = bytes.size(); + char buf[5]; + for (size_t i = 0; i < len; i++) { + if (bytes[i] == 7) { + res += "\\a"; + } else if (bytes[i] == 8) { + res += "\\b"; + } else if (bytes[i] == 9) { + res += "\\t"; + } else if (bytes[i] == 10) { + res += "\\n"; + } else if (bytes[i] == 11) { + res += "\\v"; + } else if (bytes[i] == 12) { + res += "\\f"; + } else if (bytes[i] == 13) { + res += "\\r"; + } else if (bytes[i] == 27) { + res += "\\e"; + } else if (bytes[i] == 34) { + res += "\\\""; + } else if (bytes[i] == 39) { + res += "\\'"; + } else if (bytes[i] == 92) { + res += "\\\\"; + } else if (bytes[i] < 32 || bytes[i] > 127) { + sprintf(buf, "\\x%02X", bytes[i]); + res += buf; + } else { + res += bytes[i]; + } + } + res += '"'; + ESP_LOGD(TAG, "%s", res.c_str()); +} + +void UARTDebug::log_int(UARTDirection direction, std::vector bytes, uint8_t separator) { + std::string res; + size_t len = bytes.size(); + if (direction == UART_DIRECTION_RX) { + res += "<<< "; + } else { + res += ">>> "; + } + for (size_t i = 0; i < len; i++) { + if (i > 0) { + res += separator; + } + res += to_string(bytes[i]); + } + ESP_LOGD(TAG, "%s", res.c_str()); +} + +void UARTDebug::log_binary(UARTDirection direction, std::vector bytes, uint8_t separator) { + std::string res; + size_t len = bytes.size(); + if (direction == UART_DIRECTION_RX) { + res += "<<< "; + } else { + res += ">>> "; + } + char buf[20]; + for (size_t i = 0; i < len; i++) { + if (i > 0) { + res += separator; + } + sprintf(buf, "0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(bytes[i]), bytes[i]); + res += buf; + } + ESP_LOGD(TAG, "%s", res.c_str()); +} + +} // namespace uart +} // namespace esphome +#endif diff --git a/esphome/components/uart/uart_debugger.h b/esphome/components/uart/uart_debugger.h new file mode 100644 index 0000000000..6e84bbe450 --- /dev/null +++ b/esphome/components/uart/uart_debugger.h @@ -0,0 +1,101 @@ +#pragma once +#include "esphome/core/defines.h" +#ifdef USE_UART_DEBUGGER + +#include +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "uart.h" +#include "uart_component.h" + +namespace esphome { +namespace uart { + +/// The UARTDebugger class adds debugging support to a UART bus. +/// +/// It accumulates bytes that travel over the UART bus and triggers one or +/// more actions that can log the data at an appropriate time. What +/// 'appropriate time' means exactly, is determined by a number of +/// configurable constraints. E.g. when a given number of bytes is gathered +/// and/or when no more data has been seen for a given time interval. +class UARTDebugger : public Component, public Trigger> { + public: + explicit UARTDebugger(UARTComponent *parent); + void loop() override; + + /// Set the direction in which to inspect the bytes: incoming, outgoing + /// or both. When debugging in both directions, logging will be triggered + /// when the direction of the data stream changes. + void set_direction(UARTDirection direction) { this->for_direction_ = direction; } + + /// Set the maximum number of bytes to accumulate. When the number of bytes + /// is reached, logging will be triggered. + void set_after_bytes(size_t size) { this->after_bytes_ = size; } + + /// Set a timeout for the data stream. When no new bytes are seen during + /// this timeout, logging will be triggered. + void set_after_timeout(uint32_t timeout) { this->after_timeout_ = timeout; } + + /// Add a delimiter byte. This can be called multiple times to setup a + /// multi-byte delimiter (a typical example would be '\r\n'). + /// When the constructued byte sequence is found in the data stream, + /// logging will be triggered. + void add_delimiter_byte(uint8_t byte) { this->after_delimiter_.push_back(byte); } + + protected: + UARTDirection for_direction_; + UARTDirection last_direction_{}; + std::vector bytes_{}; + size_t after_bytes_; + uint32_t after_timeout_; + uint32_t last_time_{}; + std::vector after_delimiter_{}; + size_t after_delimiter_pos_{}; + bool is_triggering_{false}; + + bool is_my_direction_(UARTDirection direction); + bool is_recursive_(); + void store_byte_(UARTDirection direction, uint8_t byte); + void trigger_after_direction_change_(UARTDirection direction); + void trigger_after_delimiter_(uint8_t byte); + void trigger_after_bytes_(); + void trigger_after_timeout_(); + bool has_buffered_bytes_(); + void fire_trigger_(); +}; + +/// This UARTDevice is used by the serial debugger to read data from a +/// serial interface when the 'dummy_receiver' option is enabled. +/// The data are not stored, nor processed. This is most useful when the +/// debugger is used to reverse engineer a serial protocol, for which no +/// specific UARTDevice implementation exists (yet), but for which the +/// incoming bytes must be read to drive the debugger. +class UARTDummyReceiver : public Component, public UARTDevice { + public: + UARTDummyReceiver(UARTComponent *parent) : UARTDevice(parent) {} + void loop() override; +}; + +/// This class contains some static methods, that can be used to easily +/// create a logging action for the debugger. +class UARTDebug { + public: + /// Log the bytes as hex values, separated by the provided separator + /// character. + static void log_hex(UARTDirection direction, std::vector bytes, uint8_t separator); + + /// Log the bytes as string values, escaping unprintable characters. + static void log_string(UARTDirection direction, std::vector bytes); + + /// Log the bytes as integer values, separated by the provided separator + /// character. + static void log_int(UARTDirection direction, std::vector bytes, uint8_t separator); + + /// Log the bytes as ' ()' values, separated by the provided + /// separator. + static void log_binary(UARTDirection direction, std::vector bytes, uint8_t separator); +}; + +} // namespace uart +} // namespace esphome +#endif diff --git a/esphome/components/uptime/sensor.py b/esphome/components/uptime/sensor.py index 7989f3befc..16a1e4c125 100644 --- a/esphome/components/uptime/sensor.py +++ b/esphome/components/uptime/sensor.py @@ -3,6 +3,7 @@ import esphome.config_validation as cv from esphome.components import sensor from esphome.const import ( CONF_ID, + ENTITY_CATEGORY_DIAGNOSTIC, STATE_CLASS_TOTAL_INCREASING, UNIT_SECOND, ICON_TIMER, @@ -17,6 +18,7 @@ CONFIG_SCHEMA = ( icon=ICON_TIMER, accuracy_decimals=0, state_class=STATE_CLASS_TOTAL_INCREASING, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ) .extend( { diff --git a/esphome/components/version/text_sensor.py b/esphome/components/version/text_sensor.py index e67f881d32..4835caf35b 100644 --- a/esphome/components/version/text_sensor.py +++ b/esphome/components/version/text_sensor.py @@ -1,7 +1,14 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import text_sensor -from esphome.const import CONF_ID, CONF_ICON, ICON_NEW_BOX, CONF_HIDE_TIMESTAMP +from esphome.const import ( + CONF_ENTITY_CATEGORY, + CONF_ID, + CONF_ICON, + ENTITY_CATEGORY_DIAGNOSTIC, + ICON_NEW_BOX, + CONF_HIDE_TIMESTAMP, +) version_ns = cg.esphome_ns.namespace("version") VersionTextSensor = version_ns.class_( @@ -13,6 +20,9 @@ CONFIG_SCHEMA = text_sensor.TEXT_SENSOR_SCHEMA.extend( cv.GenerateID(): cv.declare_id(VersionTextSensor), cv.Optional(CONF_ICON, default=ICON_NEW_BOX): text_sensor.icon, cv.Optional(CONF_HIDE_TIMESTAMP, default=False): cv.boolean, + cv.Optional( + CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_DIAGNOSTIC + ): cv.entity_category, } ).extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index 240ba7c8a0..dc652e0312 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -12,6 +12,7 @@ from esphome.const import ( CONF_AUTH, CONF_USERNAME, CONF_PASSWORD, + CONF_OTA, ) from esphome.core import CORE, coroutine_with_priority @@ -41,6 +42,7 @@ CONFIG_SCHEMA = cv.Schema( cv.GenerateID(CONF_WEB_SERVER_BASE_ID): cv.use_id( web_server_base.WebServerBase ), + cv.Optional(CONF_OTA, default=True): cv.boolean, } ).extend(cv.COMPONENT_SCHEMA) @@ -52,10 +54,14 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID], paren) await cg.register_component(var, config) + cg.add_define("USE_WEBSERVER") + cg.add(paren.set_port(config[CONF_PORT])) cg.add_define("WEBSERVER_PORT", config[CONF_PORT]) + cg.add_define("USE_WEBSERVER") cg.add(var.set_css_url(config[CONF_CSS_URL])) cg.add(var.set_js_url(config[CONF_JS_URL])) + cg.add(var.set_allow_ota(config[CONF_OTA])) if CONF_AUTH in config: cg.add(paren.set_auth_username(config[CONF_AUTH][CONF_USERNAME])) cg.add(paren.set_auth_password(config[CONF_AUTH][CONF_PASSWORD])) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 44ace38990..6f47f460af 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -152,7 +152,9 @@ void WebServer::setup() { #endif this->base_->add_handler(&this->events_); this->base_->add_handler(this); - this->base_->add_ota_handler(); + + if (this->allow_ota_) + this->base_->add_ota_handler(); this->set_interval(10000, [this]() { this->events_.send("", "ping", millis(), 30000); }); } @@ -240,10 +242,14 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) { #endif stream->print(F("

See ESPHome Web API for " - "REST API documentation.

" - "

OTA Update

" - "

Debug Log

"));
+                  "REST API documentation.

")); + if (this->allow_ota_) { + stream->print( + F("

OTA Update

")); + } + stream->print(F("

Debug Log

"));
+
 #ifdef WEBSERVER_JS_INCLUDE
   if (this->js_include_ != nullptr) {
     stream->print(F(""));
@@ -458,7 +464,7 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc
       }
       if (request->hasParam("speed_level")) {
         String speed_level = request->getParam("speed_level")->value();
-        auto val = parse_int(speed_level.c_str());
+        auto val = parse_number(speed_level.c_str());
         if (!val.has_value()) {
           ESP_LOGW(TAG, "Can't convert '%s' to number!", speed_level.c_str());
           return;
diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h
index 021d5a0646..cdfec51cf1 100644
--- a/esphome/components/web_server/web_server.h
+++ b/esphome/components/web_server/web_server.h
@@ -58,6 +58,12 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
    */
   void set_js_include(const char *js_include);
 
+  /** Set whether or not the webserver should expose the OTA form and handler.
+   *
+   * @param allow_ota.
+   */
+  void set_allow_ota(bool allow_ota) { this->allow_ota_ = allow_ota; }
+
   // ========== INTERNAL METHODS ==========
   // (In most use cases you won't need these)
   /// Setup the internal web server and register handlers.
@@ -182,6 +188,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
   const char *css_include_{nullptr};
   const char *js_url_{nullptr};
   const char *js_include_{nullptr};
+  bool allow_ota_{true};
 };
 
 }  // namespace web_server
diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py
index 4da94d990a..14fb033a56 100644
--- a/esphome/components/web_server_base/__init__.py
+++ b/esphome/components/web_server_base/__init__.py
@@ -28,4 +28,4 @@ async def to_code(config):
         cg.add_library("FS", None)
         cg.add_library("Update", None)
     # https://github.com/esphome/ESPAsyncWebServer/blob/master/library.json
-    cg.add_library("esphome/ESPAsyncWebServer-esphome", "2.0.0")
+    cg.add_library("esphome/ESPAsyncWebServer-esphome", "2.1.0")
diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py
index faf3cca280..7a9319f5e0 100644
--- a/esphome/components/wifi/__init__.py
+++ b/esphome/components/wifi/__init__.py
@@ -140,7 +140,8 @@ def final_validate(config):
     has_sta = bool(config.get(CONF_NETWORKS, True))
     has_ap = CONF_AP in config
     has_improv = "esp32_improv" in fv.full_config.get()
-    if (not has_sta) and (not has_ap) and (not has_improv):
+    has_improv_serial = "improv_serial" in fv.full_config.get()
+    if not (has_sta or has_ap or has_improv or has_improv_serial):
         raise cv.Invalid(
             "Please specify at least an SSID or an Access Point to create."
         )
diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp
index 703afa99bc..36944e3633 100644
--- a/esphome/components/wifi/wifi_component.cpp
+++ b/esphome/components/wifi/wifi_component.cpp
@@ -239,8 +239,6 @@ void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &pa
   sta.set_ssid(ssid);
   sta.set_password(password);
   this->set_sta(sta);
-
-  this->start_scanning();
 }
 
 void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) {
diff --git a/esphome/components/wifi_info/text_sensor.py b/esphome/components/wifi_info/text_sensor.py
index 1922502204..706a8967be 100644
--- a/esphome/components/wifi_info/text_sensor.py
+++ b/esphome/components/wifi_info/text_sensor.py
@@ -3,11 +3,13 @@ import esphome.config_validation as cv
 from esphome.components import text_sensor
 from esphome.const import (
     CONF_BSSID,
+    CONF_ENTITY_CATEGORY,
     CONF_ID,
     CONF_IP_ADDRESS,
     CONF_SCAN_RESULTS,
     CONF_SSID,
     CONF_MAC_ADDRESS,
+    ENTITY_CATEGORY_DIAGNOSTIC,
 )
 
 DEPENDENCIES = ["wifi"]
@@ -32,26 +34,41 @@ CONFIG_SCHEMA = cv.Schema(
         cv.Optional(CONF_IP_ADDRESS): text_sensor.TEXT_SENSOR_SCHEMA.extend(
             {
                 cv.GenerateID(): cv.declare_id(IPAddressWiFiInfo),
+                cv.Optional(
+                    CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_DIAGNOSTIC
+                ): cv.entity_category,
             }
         ),
         cv.Optional(CONF_SCAN_RESULTS): text_sensor.TEXT_SENSOR_SCHEMA.extend(
             {
                 cv.GenerateID(): cv.declare_id(ScanResultsWiFiInfo),
+                cv.Optional(
+                    CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_DIAGNOSTIC
+                ): cv.entity_category,
             }
         ).extend(cv.polling_component_schema("60s")),
         cv.Optional(CONF_SSID): text_sensor.TEXT_SENSOR_SCHEMA.extend(
             {
                 cv.GenerateID(): cv.declare_id(SSIDWiFiInfo),
+                cv.Optional(
+                    CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_DIAGNOSTIC
+                ): cv.entity_category,
             }
         ),
         cv.Optional(CONF_BSSID): text_sensor.TEXT_SENSOR_SCHEMA.extend(
             {
                 cv.GenerateID(): cv.declare_id(BSSIDWiFiInfo),
+                cv.Optional(
+                    CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_DIAGNOSTIC
+                ): cv.entity_category,
             }
         ),
         cv.Optional(CONF_MAC_ADDRESS): text_sensor.TEXT_SENSOR_SCHEMA.extend(
             {
                 cv.GenerateID(): cv.declare_id(MacAddressWifiInfo),
+                cv.Optional(
+                    CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_DIAGNOSTIC
+                ): cv.entity_category,
             }
         ),
     }
diff --git a/esphome/components/wifi_signal/sensor.py b/esphome/components/wifi_signal/sensor.py
index 37bee75928..2097c21bd7 100644
--- a/esphome/components/wifi_signal/sensor.py
+++ b/esphome/components/wifi_signal/sensor.py
@@ -4,6 +4,7 @@ from esphome.components import sensor
 from esphome.const import (
     CONF_ID,
     DEVICE_CLASS_SIGNAL_STRENGTH,
+    ENTITY_CATEGORY_DIAGNOSTIC,
     STATE_CLASS_MEASUREMENT,
     UNIT_DECIBEL_MILLIWATT,
 )
@@ -20,6 +21,7 @@ CONFIG_SCHEMA = (
         accuracy_decimals=0,
         device_class=DEVICE_CLASS_SIGNAL_STRENGTH,
         state_class=STATE_CLASS_MEASUREMENT,
+        entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
     )
     .extend(
         {
diff --git a/esphome/components/xiaomi_cgd1/sensor.py b/esphome/components/xiaomi_cgd1/sensor.py
index 774c87fee9..5b88121d7c 100644
--- a/esphome/components/xiaomi_cgd1/sensor.py
+++ b/esphome/components/xiaomi_cgd1/sensor.py
@@ -10,6 +10,7 @@ from esphome.const import (
     DEVICE_CLASS_BATTERY,
     DEVICE_CLASS_HUMIDITY,
     DEVICE_CLASS_TEMPERATURE,
+    ENTITY_CATEGORY_DIAGNOSTIC,
     STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
     UNIT_PERCENT,
@@ -47,6 +48,7 @@ CONFIG_SCHEMA = (
                 accuracy_decimals=0,
                 device_class=DEVICE_CLASS_BATTERY,
                 state_class=STATE_CLASS_MEASUREMENT,
+                entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
             ),
         }
     )
diff --git a/esphome/components/xiaomi_cgdk2/sensor.py b/esphome/components/xiaomi_cgdk2/sensor.py
index d4e7230fd0..ac487d87fc 100644
--- a/esphome/components/xiaomi_cgdk2/sensor.py
+++ b/esphome/components/xiaomi_cgdk2/sensor.py
@@ -9,6 +9,7 @@ from esphome.const import (
     DEVICE_CLASS_BATTERY,
     DEVICE_CLASS_HUMIDITY,
     DEVICE_CLASS_TEMPERATURE,
+    ENTITY_CATEGORY_DIAGNOSTIC,
     STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
     UNIT_PERCENT,
@@ -47,6 +48,7 @@ CONFIG_SCHEMA = (
                 accuracy_decimals=0,
                 device_class=DEVICE_CLASS_BATTERY,
                 state_class=STATE_CLASS_MEASUREMENT,
+                entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
             ),
         }
     )
diff --git a/esphome/components/xiaomi_cgg1/sensor.py b/esphome/components/xiaomi_cgg1/sensor.py
index 4e606d95f8..a4f9a39aff 100644
--- a/esphome/components/xiaomi_cgg1/sensor.py
+++ b/esphome/components/xiaomi_cgg1/sensor.py
@@ -11,6 +11,7 @@ from esphome.const import (
     DEVICE_CLASS_BATTERY,
     DEVICE_CLASS_HUMIDITY,
     DEVICE_CLASS_TEMPERATURE,
+    ENTITY_CATEGORY_DIAGNOSTIC,
     STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
     UNIT_PERCENT,
@@ -47,6 +48,7 @@ CONFIG_SCHEMA = (
                 accuracy_decimals=0,
                 device_class=DEVICE_CLASS_BATTERY,
                 state_class=STATE_CLASS_MEASUREMENT,
+                entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
             ),
         }
     )
diff --git a/esphome/components/xiaomi_cgpr1/binary_sensor.py b/esphome/components/xiaomi_cgpr1/binary_sensor.py
index a7f6c41225..7f0aac873d 100644
--- a/esphome/components/xiaomi_cgpr1/binary_sensor.py
+++ b/esphome/components/xiaomi_cgpr1/binary_sensor.py
@@ -10,6 +10,8 @@ from esphome.const import (
     DEVICE_CLASS_EMPTY,
     DEVICE_CLASS_BATTERY,
     DEVICE_CLASS_ILLUMINANCE,
+    DEVICE_CLASS_MOTION,
+    ENTITY_CATEGORY_DIAGNOSTIC,
     ICON_EMPTY,
     UNIT_PERCENT,
     CONF_IDLE_TIME,
@@ -37,13 +39,21 @@ CONFIG_SCHEMA = cv.All(
             cv.Required(CONF_BINDKEY): cv.bind_key,
             cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
             cv.Optional(
-                CONF_DEVICE_CLASS, default="motion"
+                CONF_DEVICE_CLASS,
+                default=DEVICE_CLASS_MOTION,
             ): binary_sensor.device_class,
             cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(
-                UNIT_PERCENT, ICON_EMPTY, 0, DEVICE_CLASS_BATTERY
+                unit_of_measurement=UNIT_PERCENT,
+                accuracy_decimals=0,
+                device_class=DEVICE_CLASS_BATTERY,
+                entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
             ),
             cv.Optional(CONF_IDLE_TIME): sensor.sensor_schema(
-                UNIT_MINUTE, ICON_TIMELAPSE, 0, DEVICE_CLASS_EMPTY
+                unit_of_measurement=UNIT_MINUTE,
+                icon=ICON_TIMELAPSE,
+                accuracy_decimals=0,
+                device_class=DEVICE_CLASS_EMPTY,
+                entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
             ),
             cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema(
                 UNIT_LUX, ICON_EMPTY, 0, DEVICE_CLASS_ILLUMINANCE
diff --git a/esphome/components/xiaomi_hhccjcy01/sensor.py b/esphome/components/xiaomi_hhccjcy01/sensor.py
index 1818731a0f..535316e246 100644
--- a/esphome/components/xiaomi_hhccjcy01/sensor.py
+++ b/esphome/components/xiaomi_hhccjcy01/sensor.py
@@ -6,6 +6,7 @@ from esphome.const import (
     CONF_TEMPERATURE,
     DEVICE_CLASS_ILLUMINANCE,
     DEVICE_CLASS_TEMPERATURE,
+    ENTITY_CATEGORY_DIAGNOSTIC,
     ICON_WATER_PERCENT,
     STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
@@ -63,6 +64,7 @@ CONFIG_SCHEMA = (
                 accuracy_decimals=0,
                 device_class=DEVICE_CLASS_BATTERY,
                 state_class=STATE_CLASS_MEASUREMENT,
+                entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
             ),
         }
     )
diff --git a/esphome/components/xiaomi_jqjcy01ym/sensor.py b/esphome/components/xiaomi_jqjcy01ym/sensor.py
index 40991c3d0f..f4d2b342fd 100644
--- a/esphome/components/xiaomi_jqjcy01ym/sensor.py
+++ b/esphome/components/xiaomi_jqjcy01ym/sensor.py
@@ -9,6 +9,7 @@ from esphome.const import (
     DEVICE_CLASS_BATTERY,
     DEVICE_CLASS_HUMIDITY,
     DEVICE_CLASS_TEMPERATURE,
+    ENTITY_CATEGORY_DIAGNOSTIC,
     STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
     UNIT_PERCENT,
@@ -54,6 +55,7 @@ CONFIG_SCHEMA = (
                 accuracy_decimals=0,
                 device_class=DEVICE_CLASS_BATTERY,
                 state_class=STATE_CLASS_MEASUREMENT,
+                entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
             ),
         }
     )
diff --git a/esphome/components/xiaomi_lywsd02/sensor.py b/esphome/components/xiaomi_lywsd02/sensor.py
index 339c5e673a..20629a0a9c 100644
--- a/esphome/components/xiaomi_lywsd02/sensor.py
+++ b/esphome/components/xiaomi_lywsd02/sensor.py
@@ -7,6 +7,7 @@ from esphome.const import (
     CONF_MAC_ADDRESS,
     CONF_TEMPERATURE,
     DEVICE_CLASS_TEMPERATURE,
+    ENTITY_CATEGORY_DIAGNOSTIC,
     STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
     UNIT_PERCENT,
@@ -45,6 +46,7 @@ CONFIG_SCHEMA = (
                 accuracy_decimals=0,
                 device_class=DEVICE_CLASS_BATTERY,
                 state_class=STATE_CLASS_MEASUREMENT,
+                entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
             ),
         }
     )
diff --git a/esphome/components/xiaomi_lywsd03mmc/sensor.py b/esphome/components/xiaomi_lywsd03mmc/sensor.py
index f27cee3800..b2784e58fc 100644
--- a/esphome/components/xiaomi_lywsd03mmc/sensor.py
+++ b/esphome/components/xiaomi_lywsd03mmc/sensor.py
@@ -6,6 +6,7 @@ from esphome.const import (
     CONF_HUMIDITY,
     CONF_MAC_ADDRESS,
     CONF_TEMPERATURE,
+    ENTITY_CATEGORY_DIAGNOSTIC,
     STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
     UNIT_PERCENT,
@@ -49,6 +50,7 @@ CONFIG_SCHEMA = (
                 accuracy_decimals=0,
                 device_class=DEVICE_CLASS_BATTERY,
                 state_class=STATE_CLASS_MEASUREMENT,
+                entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
             ),
         }
     )
diff --git a/esphome/components/xiaomi_lywsdcgq/sensor.py b/esphome/components/xiaomi_lywsdcgq/sensor.py
index 39a207327e..80f24ac0ef 100644
--- a/esphome/components/xiaomi_lywsdcgq/sensor.py
+++ b/esphome/components/xiaomi_lywsdcgq/sensor.py
@@ -6,6 +6,7 @@ from esphome.const import (
     CONF_HUMIDITY,
     CONF_MAC_ADDRESS,
     CONF_TEMPERATURE,
+    ENTITY_CATEGORY_DIAGNOSTIC,
     STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
     UNIT_PERCENT,
@@ -45,6 +46,7 @@ CONFIG_SCHEMA = (
                 accuracy_decimals=0,
                 device_class=DEVICE_CLASS_BATTERY,
                 state_class=STATE_CLASS_MEASUREMENT,
+                entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
             ),
         }
     )
diff --git a/esphome/components/xiaomi_mhoc401/sensor.py b/esphome/components/xiaomi_mhoc401/sensor.py
index 57b2190150..9e92e34230 100644
--- a/esphome/components/xiaomi_mhoc401/sensor.py
+++ b/esphome/components/xiaomi_mhoc401/sensor.py
@@ -6,6 +6,7 @@ from esphome.const import (
     CONF_HUMIDITY,
     CONF_MAC_ADDRESS,
     CONF_TEMPERATURE,
+    ENTITY_CATEGORY_DIAGNOSTIC,
     STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
     UNIT_PERCENT,
@@ -48,6 +49,7 @@ CONFIG_SCHEMA = (
                 accuracy_decimals=0,
                 device_class=DEVICE_CLASS_BATTERY,
                 state_class=STATE_CLASS_MEASUREMENT,
+                entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
             ),
         }
     )
diff --git a/esphome/components/xiaomi_mjyd02yla/binary_sensor.py b/esphome/components/xiaomi_mjyd02yla/binary_sensor.py
index fd4bae60c1..1bedae26cf 100644
--- a/esphome/components/xiaomi_mjyd02yla/binary_sensor.py
+++ b/esphome/components/xiaomi_mjyd02yla/binary_sensor.py
@@ -10,6 +10,7 @@ from esphome.const import (
     CONF_BATTERY_LEVEL,
     DEVICE_CLASS_BATTERY,
     DEVICE_CLASS_ILLUMINANCE,
+    ENTITY_CATEGORY_DIAGNOSTIC,
     STATE_CLASS_MEASUREMENT,
     STATE_CLASS_NONE,
     UNIT_PERCENT,
@@ -51,6 +52,7 @@ CONFIG_SCHEMA = cv.All(
                 accuracy_decimals=0,
                 device_class=DEVICE_CLASS_BATTERY,
                 state_class=STATE_CLASS_MEASUREMENT,
+                entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
             ),
             cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema(
                 unit_of_measurement=UNIT_LUX,
diff --git a/esphome/components/xiaomi_wx08zm/binary_sensor.py b/esphome/components/xiaomi_wx08zm/binary_sensor.py
index d2b353beff..8667794923 100644
--- a/esphome/components/xiaomi_wx08zm/binary_sensor.py
+++ b/esphome/components/xiaomi_wx08zm/binary_sensor.py
@@ -6,6 +6,7 @@ from esphome.const import (
     CONF_MAC_ADDRESS,
     CONF_TABLET,
     DEVICE_CLASS_BATTERY,
+    ENTITY_CATEGORY_DIAGNOSTIC,
     STATE_CLASS_MEASUREMENT,
     UNIT_PERCENT,
     ICON_BUG,
@@ -40,6 +41,7 @@ CONFIG_SCHEMA = cv.All(
                 accuracy_decimals=0,
                 device_class=DEVICE_CLASS_BATTERY,
                 state_class=STATE_CLASS_MEASUREMENT,
+                entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
             ),
         }
     )
diff --git a/esphome/config_validation.py b/esphome/config_validation.py
index fcec74b245..2bb45487fa 100644
--- a/esphome/config_validation.py
+++ b/esphome/config_validation.py
@@ -12,12 +12,14 @@ from string import ascii_letters, digits
 import voluptuous as vol
 
 from esphome import core
+import esphome.codegen as cg
 from esphome.const import (
     ALLOWED_NAME_CHARS,
     CONF_AVAILABILITY,
     CONF_COMMAND_TOPIC,
     CONF_DISABLED_BY_DEFAULT,
     CONF_DISCOVERY,
+    CONF_ENTITY_CATEGORY,
     CONF_ICON,
     CONF_ID,
     CONF_INTERNAL,
@@ -35,6 +37,9 @@ from esphome.const import (
     CONF_UPDATE_INTERVAL,
     CONF_TYPE_ID,
     CONF_TYPE,
+    ENTITY_CATEGORY_CONFIG,
+    ENTITY_CATEGORY_DIAGNOSTIC,
+    ENTITY_CATEGORY_NONE,
     KEY_CORE,
     KEY_FRAMEWORK_VERSION,
     KEY_TARGET_FRAMEWORK,
@@ -903,21 +908,9 @@ def validate_bytes(value):
 
 def hostname(value):
     value = string(value)
-    warned_underscore = False
-    if len(value) > 63:
-        raise Invalid("Hostnames can only be 63 characters long")
-    for c in value:
-        if not (c.isalnum() or c in "-_"):
-            raise Invalid("Hostname can only have alphanumeric characters and -")
-        if c in "_" and not warned_underscore:
-            _LOGGER.warning(
-                "'%s': Using the '_' (underscore) character in the hostname is discouraged "
-                "as it can cause problems with some DHCP and local name services. "
-                "For more information, see https://esphome.io/guides/faq.html#why-shouldn-t-i-use-underscores-in-my-device-name",
-                value,
-            )
-            warned_underscore = True
-    return value
+    if re.match(r"^[a-z0-9-]{1,63}$", value, re.IGNORECASE) is not None:
+        return value
+    raise Invalid(f"Invalid hostname: {value}")
 
 
 def domain(value):
@@ -1406,7 +1399,7 @@ def typed_schema(schemas, **kwargs):
         if schema_option is None:
             raise Invalid(f"{key} not specified!")
         key_v = key_validator(schema_option)
-        value = schemas[key_v](value)
+        value = Schema(schemas[key_v])(value)
         value[key] = key_v
         return value
 
@@ -1563,6 +1556,17 @@ def maybe_simple_value(*validators, **kwargs):
     return validate
 
 
+_ENTITY_CATEGORIES = {
+    ENTITY_CATEGORY_NONE: cg.EntityCategory.ENTITY_CATEGORY_NONE,
+    ENTITY_CATEGORY_CONFIG: cg.EntityCategory.ENTITY_CATEGORY_CONFIG,
+    ENTITY_CATEGORY_DIAGNOSTIC: cg.EntityCategory.ENTITY_CATEGORY_DIAGNOSTIC,
+}
+
+
+def entity_category(value):
+    return enum(_ENTITY_CATEGORIES, lower=True)(value)
+
+
 MQTT_COMPONENT_AVAILABILITY_SCHEMA = Schema(
     {
         Required(CONF_TOPIC): subscribe_topic,
@@ -1594,6 +1598,7 @@ ENTITY_BASE_SCHEMA = Schema(
         Optional(CONF_INTERNAL): boolean,
         Optional(CONF_DISABLED_BY_DEFAULT, default=False): boolean,
         Optional(CONF_ICON): icon,
+        Optional(CONF_ENTITY_CATEGORY): entity_category,
     }
 )
 
@@ -1680,6 +1685,25 @@ def version_number(value):
         raise Invalid("Not a version number") from e
 
 
+def platformio_version_constraint(value):
+    # for documentation on valid version constraints:
+    # https://docs.platformio.org/en/latest/core/userguide/platforms/cmd_install.html#cmd-platform-install
+
+    value = string_strict(value)
+    constraints = []
+    for item in value.split(","):
+        # find and strip prefix operator
+        op = None
+        for test_op in ("^", "~", ">=", ">", "<=", "<", "!="):
+            if item.startswith(test_op):
+                op = test_op
+                item = item[len(test_op) :]
+                break
+
+        constraints.append((op, version_number(item)))
+    return constraints
+
+
 def require_framework_version(
     *,
     esp_idf=None,
diff --git a/esphome/const.py b/esphome/const.py
index d90ff04e7f..128d12aad5 100644
--- a/esphome/const.py
+++ b/esphome/const.py
@@ -1,6 +1,6 @@
 """Constants used by esphome."""
 
-__version__ = "2021.10.3"
+__version__ = "2021.11.0"
 
 ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
 
@@ -34,6 +34,7 @@ ARDUINO_VERSION_ESP8266 = {
 SOURCE_FILE_EXTENSIONS = {".cpp", ".hpp", ".h", ".c", ".tcc", ".ino"}
 HEADER_FILE_EXTENSIONS = {".h", ".hpp", ".tcc"}
 
+
 CONF_ABOVE = "above"
 CONF_ACCELERATION = "acceleration"
 CONF_ACCELERATION_X = "acceleration_x"
@@ -47,6 +48,7 @@ CONF_ACTIVE_POWER = "active_power"
 CONF_ADDRESS = "address"
 CONF_ADDRESSABLE_LIGHT_ID = "addressable_light_id"
 CONF_ADVANCED = "advanced"
+CONF_AFTER = "after"
 CONF_ALPHA = "alpha"
 CONF_ALTITUDE = "altitude"
 CONF_AND = "and"
@@ -59,6 +61,7 @@ CONF_AT = "at"
 CONF_ATTENUATION = "attenuation"
 CONF_ATTRIBUTE = "attribute"
 CONF_AUTH = "auth"
+CONF_AUTO_CLEAR_ENABLED = "auto_clear_enabled"
 CONF_AUTO_MODE = "auto_mode"
 CONF_AUTOCONF = "autoconf"
 CONF_AUTOMATION_ID = "automation_id"
@@ -92,6 +95,7 @@ CONF_BUFFER_SIZE = "buffer_size"
 CONF_BUILD_PATH = "build_path"
 CONF_BUS_VOLTAGE = "bus_voltage"
 CONF_BUSY_PIN = "busy_pin"
+CONF_BYTES = "bytes"
 CONF_CALCULATED_LUX = "calculated_lux"
 CONF_CALIBRATE_LINEAR = "calibrate_linear"
 CONF_CALIBRATION = "calibration"
@@ -163,6 +167,7 @@ CONF_DAYS_OF_WEEK = "days_of_week"
 CONF_DC_PIN = "dc_pin"
 CONF_DEASSERT_RTS_DTR = "deassert_rts_dtr"
 CONF_DEBOUNCE = "debounce"
+CONF_DEBUG = "debug"
 CONF_DECAY_MODE = "decay_mode"
 CONF_DECELERATION = "deceleration"
 CONF_DEFAULT_MODE = "default_mode"
@@ -170,6 +175,7 @@ CONF_DEFAULT_TARGET_TEMPERATURE_HIGH = "default_target_temperature_high"
 CONF_DEFAULT_TARGET_TEMPERATURE_LOW = "default_target_temperature_low"
 CONF_DEFAULT_TRANSITION_LENGTH = "default_transition_length"
 CONF_DELAY = "delay"
+CONF_DELIMITER = "delimiter"
 CONF_DELTA = "delta"
 CONF_DEVICE = "device"
 CONF_DEVICE_CLASS = "device_class"
@@ -191,6 +197,8 @@ CONF_DNS2 = "dns2"
 CONF_DOMAIN = "domain"
 CONF_DRY_ACTION = "dry_action"
 CONF_DRY_MODE = "dry_mode"
+CONF_DUMMY_RECEIVER = "dummy_receiver"
+CONF_DUMMY_RECEIVER_ID = "dummy_receiver_id"
 CONF_DUMP = "dump"
 CONF_DURATION = "duration"
 CONF_EAP = "eap"
@@ -202,6 +210,7 @@ CONF_ELSE = "else"
 CONF_ENABLE_PIN = "enable_pin"
 CONF_ENABLE_TIME = "enable_time"
 CONF_ENERGY = "energy"
+CONF_ENTITY_CATEGORY = "entity_category"
 CONF_ENTITY_ID = "entity_id"
 CONF_ESP8266_DISABLE_SSL_SUPPORT = "esp8266_disable_ssl_support"
 CONF_ESPHOME = "esphome"
@@ -709,6 +718,7 @@ CONF_UNIT_OF_MEASUREMENT = "unit_of_measurement"
 CONF_UPDATE_INTERVAL = "update_interval"
 CONF_UPDATE_ON_BOOT = "update_on_boot"
 CONF_URL = "url"
+CONF_USE_ABBREVIATIONS = "use_abbreviations"
 CONF_USE_ADDRESS = "use_address"
 CONF_USERNAME = "username"
 CONF_UUID = "uuid"
@@ -868,10 +878,11 @@ DEVICE_CLASS_OPENING = "opening"
 DEVICE_CLASS_PLUG = "plug"
 DEVICE_CLASS_PRESENCE = "presence"
 DEVICE_CLASS_PROBLEM = "problem"
+DEVICE_CLASS_RUNNING = "running"
 DEVICE_CLASS_SAFETY = "safety"
 DEVICE_CLASS_SMOKE = "smoke"
 DEVICE_CLASS_SOUND = "sound"
-DEVICE_CLASS_UPDATE = "update"
+DEVICE_CLASS_TAMPER = "tamper"
 DEVICE_CLASS_VIBRATION = "vibration"
 DEVICE_CLASS_WINDOW = "window"
 # device classes of both binary_sensor and sensor component
@@ -917,3 +928,12 @@ KEY_CORE = "core"
 KEY_TARGET_PLATFORM = "target_platform"
 KEY_TARGET_FRAMEWORK = "target_framework"
 KEY_FRAMEWORK_VERSION = "framework_version"
+
+# Entity categories
+ENTITY_CATEGORY_NONE = ""
+
+# The entity category for configuration values/controls
+ENTITY_CATEGORY_CONFIG = "config"
+
+# The entity category for read only diagnostic values, for example RSSI, uptime or MAC Address
+ENTITY_CATEGORY_DIAGNOSTIC = "diagnostic"
diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py
index 8bdef3a4ea..addecf1326 100644
--- a/esphome/core/__init__.py
+++ b/esphome/core/__init__.py
@@ -10,6 +10,7 @@ from esphome.const import (
     CONF_USE_ADDRESS,
     CONF_ETHERNET,
     CONF_WIFI,
+    CONF_PORT,
     KEY_CORE,
     KEY_TARGET_FRAMEWORK,
     KEY_TARGET_PLATFORM,
@@ -519,6 +520,19 @@ class EsphomeCore:
 
         return None
 
+    @property
+    def web_port(self) -> Optional[int]:
+        if self.config is None:
+            raise ValueError("Config has not been loaded yet")
+
+        if "web_server" in self.config:
+            try:
+                return self.config["web_server"][CONF_PORT]
+            except KeyError:
+                return 80
+
+        return None
+
     @property
     def comment(self) -> Optional[str]:
         if self.config is None:
diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp
index a4d61f819c..f67fc826cf 100644
--- a/esphome/core/application.cpp
+++ b/esphome/core/application.cpp
@@ -109,8 +109,8 @@ void Application::loop() {
 
 void IRAM_ATTR HOT Application::feed_wdt() {
   static uint32_t last_feed = 0;
-  uint32_t now = millis();
-  if (now - last_feed > 3) {
+  uint32_t now = micros();
+  if (now - last_feed > 3000) {
     arch_feed_wdt();
     last_feed = now;
 #ifdef USE_STATUS_LED
diff --git a/esphome/core/automation.h b/esphome/core/automation.h
index 6d79480f0f..e5460bef34 100644
--- a/esphome/core/automation.h
+++ b/esphome/core/automation.h
@@ -17,14 +17,50 @@ namespace esphome {
 
 #define TEMPLATABLE_VALUE(type, name) TEMPLATABLE_VALUE_(type, name)
 
-#define TEMPLATABLE_STRING_VALUE_(name) \
- protected: \
-  TemplatableStringValue name##_{}; \
-\
- public: \
-  template void set_##name(V name) { this->name##_ = name; }
+template class TemplatableValue {
+ public:
+  TemplatableValue() : type_(EMPTY) {}
 
-#define TEMPLATABLE_STRING_VALUE(name) TEMPLATABLE_STRING_VALUE_(name)
+  template::value, int> = 0>
+  TemplatableValue(F value) : type_(VALUE), value_(value) {}
+
+  template::value, int> = 0>
+  TemplatableValue(F f) : type_(LAMBDA), f_(f) {}
+
+  bool has_value() { return this->type_ != EMPTY; }
+
+  T value(X... x) {
+    if (this->type_ == LAMBDA) {
+      return this->f_(x...);
+    }
+    // return value also when empty
+    return this->value_;
+  }
+
+  optional optional_value(X... x) {
+    if (!this->has_value()) {
+      return {};
+    }
+    return this->value(x...);
+  }
+
+  T value_or(X... x, T default_value) {
+    if (!this->has_value()) {
+      return default_value;
+    }
+    return this->value(x...);
+  }
+
+ protected:
+  enum {
+    EMPTY,
+    VALUE,
+    LAMBDA,
+  } type_;
+
+  T value_{};
+  std::function f_{};
+};
 
 /** Base class for all automation conditions.
  *
diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h
index d97d369d33..e87a4a2765 100644
--- a/esphome/core/base_automation.h
+++ b/esphome/core/base_automation.h
@@ -224,6 +224,39 @@ template class WhileAction : public Action {
   std::tuple var_{};
 };
 
+template class RepeatAction : public Action {
+ public:
+  TEMPLATABLE_VALUE(uint32_t, count)
+
+  void add_then(const std::vector *> &actions) {
+    this->then_.add_actions(actions);
+    this->then_.add_action(new LambdaAction([this](Ts... x) {
+      this->iteration_++;
+      if (this->iteration_ == this->count_.value(x...))
+        this->play_next_tuple_(this->var_);
+      else
+        this->then_.play_tuple(this->var_);
+    }));
+  }
+
+  void play_complex(Ts... x) override {
+    this->num_running_++;
+    this->var_ = std::make_tuple(x...);
+    this->iteration_ = 0;
+    this->then_.play_tuple(this->var_);
+  }
+
+  void play(Ts... x) override { /* ignore - see play_complex */
+  }
+
+  void stop() override { this->then_.stop(); }
+
+ protected:
+  uint32_t iteration_;
+  ActionList then_;
+  std::tuple var_;
+};
+
 template class WaitUntilAction : public Action, public Component {
  public:
   WaitUntilAction(Condition *condition) : condition_(condition) {}
diff --git a/esphome/core/config.py b/esphome/core/config.py
index 262451df88..68c253f7b4 100644
--- a/esphome/core/config.py
+++ b/esphome/core/config.py
@@ -55,6 +55,24 @@ CONF_NAME_ADD_MAC_SUFFIX = "name_add_mac_suffix"
 VALID_INCLUDE_EXTS = {".h", ".hpp", ".tcc", ".ino", ".cpp", ".c"}
 
 
+def validate_hostname(config):
+    max_length = 31
+    if config[CONF_NAME_ADD_MAC_SUFFIX]:
+        max_length -= 7  # "-AABBCC" is appended when add mac suffix option is used
+    if len(config[CONF_NAME]) > max_length:
+        raise cv.Invalid(
+            f"Hostnames can only be {max_length} characters long", path=[CONF_NAME]
+        )
+    if "_" in config[CONF_NAME]:
+        _LOGGER.warning(
+            "'%s': Using the '_' (underscore) character in the hostname is discouraged "
+            "as it can cause problems with some DHCP and local name services. "
+            "For more information, see https://esphome.io/guides/faq.html#why-shouldn-t-i-use-underscores-in-my-device-name",
+            config[CONF_NAME],
+        )
+    return config
+
+
 def valid_include(value):
     try:
         return cv.directory(value)
@@ -79,42 +97,47 @@ def valid_project_name(value: str):
 
 
 CONF_ESP8266_RESTORE_FROM_FLASH = "esp8266_restore_from_flash"
-CONFIG_SCHEMA = cv.Schema(
-    {
-        cv.Required(CONF_NAME): cv.hostname,
-        cv.Optional(CONF_COMMENT): cv.string,
-        cv.Required(CONF_BUILD_PATH): cv.string,
-        cv.Optional(CONF_PLATFORMIO_OPTIONS, default={}): cv.Schema(
-            {
-                cv.string_strict: cv.Any([cv.string], cv.string),
-            }
-        ),
-        cv.Optional(CONF_ON_BOOT): automation.validate_automation(
-            {
-                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StartupTrigger),
-                cv.Optional(CONF_PRIORITY, default=600.0): cv.float_,
-            }
-        ),
-        cv.Optional(CONF_ON_SHUTDOWN): automation.validate_automation(
-            {
-                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ShutdownTrigger),
-            }
-        ),
-        cv.Optional(CONF_ON_LOOP): automation.validate_automation(
-            {
-                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LoopTrigger),
-            }
-        ),
-        cv.Optional(CONF_INCLUDES, default=[]): cv.ensure_list(valid_include),
-        cv.Optional(CONF_LIBRARIES, default=[]): cv.ensure_list(cv.string_strict),
-        cv.Optional(CONF_NAME_ADD_MAC_SUFFIX, default=False): cv.boolean,
-        cv.Optional(CONF_PROJECT): cv.Schema(
-            {
-                cv.Required(CONF_NAME): cv.All(cv.string_strict, valid_project_name),
-                cv.Required(CONF_VERSION): cv.string_strict,
-            }
-        ),
-    }
+CONFIG_SCHEMA = cv.All(
+    cv.Schema(
+        {
+            cv.Required(CONF_NAME): cv.valid_name,
+            cv.Optional(CONF_COMMENT): cv.string,
+            cv.Required(CONF_BUILD_PATH): cv.string,
+            cv.Optional(CONF_PLATFORMIO_OPTIONS, default={}): cv.Schema(
+                {
+                    cv.string_strict: cv.Any([cv.string], cv.string),
+                }
+            ),
+            cv.Optional(CONF_ON_BOOT): automation.validate_automation(
+                {
+                    cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StartupTrigger),
+                    cv.Optional(CONF_PRIORITY, default=600.0): cv.float_,
+                }
+            ),
+            cv.Optional(CONF_ON_SHUTDOWN): automation.validate_automation(
+                {
+                    cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ShutdownTrigger),
+                }
+            ),
+            cv.Optional(CONF_ON_LOOP): automation.validate_automation(
+                {
+                    cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LoopTrigger),
+                }
+            ),
+            cv.Optional(CONF_INCLUDES, default=[]): cv.ensure_list(valid_include),
+            cv.Optional(CONF_LIBRARIES, default=[]): cv.ensure_list(cv.string_strict),
+            cv.Optional(CONF_NAME_ADD_MAC_SUFFIX, default=False): cv.boolean,
+            cv.Optional(CONF_PROJECT): cv.Schema(
+                {
+                    cv.Required(CONF_NAME): cv.All(
+                        cv.string_strict, valid_project_name
+                    ),
+                    cv.Required(CONF_VERSION): cv.string_strict,
+                }
+            ),
+        }
+    ),
+    validate_hostname,
 )
 
 PRELOAD_CONFIG_SCHEMA = cv.Schema(
@@ -142,7 +165,7 @@ def preload_core_config(config, result):
     CORE.data[KEY_CORE] = {}
 
     if CONF_BUILD_PATH not in conf:
-        conf[CONF_BUILD_PATH] = CORE.name
+        conf[CONF_BUILD_PATH] = f".esphome/build/{CORE.name}"
     CORE.build_path = CORE.relative_config_path(conf[CONF_BUILD_PATH])
 
     has_oldstyle = CONF_PLATFORM in conf
diff --git a/esphome/core/defines.h b/esphome/core/defines.h
index b44987a768..e679fe1cef 100644
--- a/esphome/core/defines.h
+++ b/esphome/core/defines.h
@@ -9,6 +9,7 @@
 #define ESPHOME_BOARD "dummy_board"
 #define ESPHOME_PROJECT_NAME "dummy project"
 #define ESPHOME_PROJECT_VERSION "v2"
+#define ESPHOME_VARIANT "ESP32"
 
 // Feature flags
 #define USE_API
@@ -36,8 +37,12 @@
 #define USE_SWITCH
 #define USE_TEXT_SENSOR
 #define USE_TIME
+#define USE_UART_DEBUGGER
+#define USE_WEBSERVER
 #define USE_WIFI
 
+#define WEBSERVER_PORT 80  // NOLINT
+
 // Arduino-specific feature flags
 #ifdef USE_ARDUINO
 #define USE_CAPTIVE_PORTAL
diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp
index bc94da85fe..a9e1414018 100644
--- a/esphome/core/entity_base.cpp
+++ b/esphome/core/entity_base.cpp
@@ -26,12 +26,16 @@ void EntityBase::set_disabled_by_default(bool disabled_by_default) { this->disab
 const std::string &EntityBase::get_icon() const { return this->icon_; }
 void EntityBase::set_icon(const std::string &name) { this->icon_ = name; }
 
+// Entity Category
+EntityCategory EntityBase::get_entity_category() const { return this->entity_category_; }
+void EntityBase::set_entity_category(EntityCategory entity_category) { this->entity_category_ = entity_category; }
+
 // Entity Object ID
 const std::string &EntityBase::get_object_id() { return this->object_id_; }
 
 // Calculate Object ID Hash from Entity Name
 void EntityBase::calc_object_id_() {
-  this->object_id_ = sanitize_string_allowlist(to_lowercase_underscore(this->name_), HOSTNAME_CHARACTER_ALLOWLIST);
+  this->object_id_ = str_sanitize(str_snake_case(this->name_));
   // FNV-1 hash
   this->object_id_hash_ = fnv1_hash(this->object_id_);
 }
diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h
index 263747b721..c489d71910 100644
--- a/esphome/core/entity_base.h
+++ b/esphome/core/entity_base.h
@@ -5,6 +5,12 @@
 
 namespace esphome {
 
+enum EntityCategory : uint8_t {
+  ENTITY_CATEGORY_NONE = 0,
+  ENTITY_CATEGORY_CONFIG = 1,
+  ENTITY_CATEGORY_DIAGNOSTIC = 2,
+};
+
 // The generic Entity base class that provides an interface common to all Entities.
 class EntityBase {
  public:
@@ -31,6 +37,10 @@ class EntityBase {
   bool is_disabled_by_default() const;
   void set_disabled_by_default(bool disabled_by_default);
 
+  // Get/set the entity category.
+  EntityCategory get_entity_category() const;
+  void set_entity_category(EntityCategory entity_category);
+
   // Get/set this entity's icon
   const std::string &get_icon() const;
   void set_icon(const std::string &name);
@@ -45,6 +55,7 @@ class EntityBase {
   uint32_t object_id_hash_;
   bool internal_{false};
   bool disabled_by_default_{false};
+  EntityCategory entity_category_{ENTITY_CATEGORY_NONE};
 };
 
 }  // namespace esphome
diff --git a/esphome/core/gpio.h b/esphome/core/gpio.h
index 1d3fb89805..04658d567c 100644
--- a/esphome/core/gpio.h
+++ b/esphome/core/gpio.h
@@ -70,6 +70,7 @@ class ISRInternalGPIOPin {
   bool digital_read();
   void digital_write(bool value);
   void clear_interrupt();
+  void pin_mode(gpio::Flags flags);
 
  protected:
   void *arg_ = nullptr;
diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp
index bc97259a71..27608a84c1 100644
--- a/esphome/core/helpers.cpp
+++ b/esphome/core/helpers.cpp
@@ -6,7 +6,10 @@
 #include 
 
 #if defined(USE_ESP8266)
+#ifdef USE_WIFI
 #include 
+#endif
+#include 
 #include 
 #elif defined(USE_ESP32_FRAMEWORK_ARDUINO)
 #include 
@@ -39,7 +42,7 @@ void get_mac_address_raw(uint8_t *mac) {
   esp_efuse_mac_get_default(mac);
 #endif
 #endif
-#ifdef USE_ESP8266
+#if (defined USE_ESP8266 && defined USE_WIFI)
   WiFi.macAddress(mac);
 #endif
 }
@@ -48,7 +51,11 @@ std::string get_mac_address() {
   char tmp[20];
   uint8_t mac[6];
   get_mac_address_raw(mac);
+#ifdef USE_WIFI
   sprintf(tmp, "%02x%02x%02x%02x%02x%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
+#else
+  return "";
+#endif
   return std::string(tmp);
 }
 
@@ -122,31 +129,6 @@ float gamma_uncorrect(float value, float gamma) {
   return powf(value, 1 / gamma);
 }
 
-std::string to_lowercase_underscore(std::string s) {
-  std::transform(s.begin(), s.end(), s.begin(), ::tolower);
-  std::replace(s.begin(), s.end(), ' ', '_');
-  return s;
-}
-
-std::string sanitize_string_allowlist(const std::string &s, const std::string &allowlist) {
-  std::string out(s);
-  out.erase(std::remove_if(out.begin(), out.end(),
-                           [&allowlist](const char &c) { return allowlist.find(c) == std::string::npos; }),
-            out.end());
-  return out;
-}
-
-std::string sanitize_hostname(const std::string &hostname) {
-  std::string s = sanitize_string_allowlist(hostname, HOSTNAME_CHARACTER_ALLOWLIST);
-  return truncate_string(s, 63);
-}
-
-std::string truncate_string(const std::string &s, size_t length) {
-  if (s.length() > length)
-    return s.substr(0, length);
-  return s;
-}
-
 std::string value_accuracy_to_string(float value, int8_t accuracy_decimals) {
   if (accuracy_decimals < 0) {
     auto multiplier = powf(10.0f, accuracy_decimals);
@@ -185,8 +167,6 @@ ParseOnOffState parse_on_off(const char *str, const char *on, const char *off) {
   return PARSE_NONE;
 }
 
-const char *const HOSTNAME_CHARACTER_ALLOWLIST = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
-
 uint8_t crc8(uint8_t *data, uint8_t len) {
   uint8_t crc = 0;
 
@@ -203,17 +183,18 @@ uint8_t crc8(uint8_t *data, uint8_t len) {
   return crc;
 }
 
-void delay_microseconds_accurate(uint32_t usec) {
-  if (usec == 0)
-    return;
-  if (usec < 5000UL) {
-    delayMicroseconds(usec);
-    return;
-  }
-  uint32_t start = micros();
-  while (micros() - start < usec) {
-    delay(0);
+void delay_microseconds_safe(uint32_t us) {  // avoids CPU locks that could trigger WDT or affect WiFi/BT stability
+  auto start = micros();
+  const uint32_t lag = 5000;  // microseconds, specifies the maximum time for a CPU busy-loop.
+                              // it must be larger than the worst-case duration of a delay(1) call (hardware tasks)
+                              // 5ms is conservative, it could be reduced when exact BT/WiFi stack delays are known
+  if (us > lag) {
+    delay((us - lag) / 1000UL);  // note: in disabled-interrupt contexts delay() won't actually sleep
+    while (micros() - start < us - lag)
+      delay(1);  // in those cases, this loop allows to yield for BT/WiFi stack tasks
   }
+  while (micros() - start < us)  // fine delay the remaining usecs
+    ;
 }
 
 uint8_t reverse_bits_8(uint8_t x) {
@@ -272,20 +253,6 @@ std::string to_string(long double val) {
   sprintf(buf, "%Lf", val);
   return buf;
 }
-optional parse_float(const std::string &str) {
-  char *end;
-  float value = ::strtof(str.c_str(), &end);
-  if (end == nullptr || end != str.end().base())
-    return {};
-  return value;
-}
-optional parse_int(const std::string &str) {
-  char *end;
-  int value = ::strtol(str.c_str(), &end, 10);
-  if (end == nullptr || end != str.end().base())
-    return {};
-  return value;
-}
 
 optional parse_hex(const char chr) {
   int out = chr;
@@ -384,17 +351,6 @@ std::string str_sprintf(const char *fmt, ...) {
   return str;
 }
 
-uint16_t encode_uint16(uint8_t msb, uint8_t lsb) { return (uint16_t(msb) << 8) | uint16_t(lsb); }
-std::array decode_uint16(uint16_t value) {
-  uint8_t msb = (value >> 8) & 0xFF;
-  uint8_t lsb = (value >> 0) & 0xFF;
-  return {msb, lsb};
-}
-
-uint32_t encode_uint32(uint8_t msb, uint8_t byte2, uint8_t byte3, uint8_t lsb) {
-  return (uint32_t(msb) << 24) | (uint32_t(byte2) << 16) | (uint32_t(byte3) << 8) | uint32_t(lsb);
-}
-
 std::string hexencode(const uint8_t *data, uint32_t len) {
   char buf[20];
   std::string res;
@@ -483,4 +439,24 @@ IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); }
 IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); }
 #endif
 
+// ---------------------------------------------------------------------------------------------------------------------
+
+std::string str_truncate(const std::string &str, size_t length) {
+  return str.length() > length ? str.substr(0, length) : str;
+}
+std::string str_snake_case(const std::string &str) {
+  std::string result;
+  result.resize(str.length());
+  std::transform(str.begin(), str.end(), result.begin(), ::tolower);
+  std::replace(result.begin(), result.end(), ' ', '_');
+  return result;
+}
+std::string str_sanitize(const std::string &str) {
+  std::string out;
+  std::copy_if(str.begin(), str.end(), std::back_inserter(out), [](const char &c) {
+    return c == '-' || c == '_' || (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
+  });
+  return out;
+}
+
 }  // namespace esphome
diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h
index 61cc9a9e4a..c67ad8eea3 100644
--- a/esphome/core/helpers.h
+++ b/esphome/core/helpers.h
@@ -1,5 +1,7 @@
 #pragma once
 
+#include 
+
 #include 
 #include 
 #include 
@@ -23,9 +25,6 @@
 
 namespace esphome {
 
-/// The characters that are allowed in a hostname.
-extern const char *const HOSTNAME_CHARACTER_ALLOWLIST;
-
 /// Read the raw MAC address into the provided byte array (6 bytes).
 void get_mac_address_raw(uint8_t *mac);
 
@@ -51,18 +50,8 @@ std::string to_string(unsigned long long val);  // NOLINT
 std::string to_string(float val);
 std::string to_string(double val);
 std::string to_string(long double val);
-optional parse_float(const std::string &str);
-optional parse_int(const std::string &str);
 optional parse_hex(const std::string &str, size_t start, size_t length);
 optional parse_hex(char chr);
-/// Sanitize the hostname by removing characters that are not in the allowlist and truncating it to 63 chars.
-std::string sanitize_hostname(const std::string &hostname);
-
-/// Truncate a string to a specific length
-std::string truncate_string(const std::string &s, size_t length);
-
-/// Convert the string to lowercase_underscore.
-std::string to_lowercase_underscore(std::string s);
 
 /// Compare string a to string b (ignoring case) and return whether they are equal.
 bool str_equals_case_insensitive(const std::string &a, const std::string &b);
@@ -145,20 +134,10 @@ std::string uint64_to_string(uint64_t num);
 /// Convert a uint32_t to a hex string
 std::string uint32_to_string(uint32_t num);
 
-/// Sanitizes the input string with the allowlist.
-std::string sanitize_string_allowlist(const std::string &s, const std::string &allowlist);
-
 uint8_t reverse_bits_8(uint8_t x);
 uint16_t reverse_bits_16(uint16_t x);
 uint32_t reverse_bits_32(uint32_t x);
 
-/// Encode a 16-bit unsigned integer given a most and least-significant byte.
-uint16_t encode_uint16(uint8_t msb, uint8_t lsb);
-/// Decode a 16-bit unsigned integer into an array of two values: most significant byte, least significant byte.
-std::array decode_uint16(uint16_t value);
-/// Encode a 32-bit unsigned integer given four bytes in MSB -> LSB order
-uint32_t encode_uint32(uint8_t msb, uint8_t byte2, uint8_t byte3, uint8_t lsb);
-
 /// Convert RGB floats (0-1) to hue (0-360) & saturation/value percentage (0-1)
 void rgb_to_hsv(float red, float green, float blue, int &hue, float &saturation, float &value);
 /// Convert hue (0-360) & saturation/value percentage (0-1) to RGB floats (0-1)
@@ -255,64 +234,7 @@ struct is_callable  // NOLINT
   static constexpr auto value = decltype(test(nullptr))::value;  // NOLINT
 };
 
-template class TemplatableValue {
- public:
-  TemplatableValue() : type_(EMPTY) {}
-
-  template::value, int> = 0>
-  TemplatableValue(F value) : type_(VALUE), value_(value) {}
-
-  template::value, int> = 0>
-  TemplatableValue(F f) : type_(LAMBDA), f_(f) {}
-
-  bool has_value() { return this->type_ != EMPTY; }
-
-  T value(X... x) {
-    if (this->type_ == LAMBDA) {
-      return this->f_(x...);
-    }
-    // return value also when empty
-    return this->value_;
-  }
-
-  optional optional_value(X... x) {
-    if (!this->has_value()) {
-      return {};
-    }
-    return this->value(x...);
-  }
-
-  T value_or(X... x, T default_value) {
-    if (!this->has_value()) {
-      return default_value;
-    }
-    return this->value(x...);
-  }
-
- protected:
-  enum {
-    EMPTY,
-    VALUE,
-    LAMBDA,
-  } type_;
-
-  T value_{};
-  std::function f_{};
-};
-
-template class TemplatableStringValue : public TemplatableValue {
- public:
-  TemplatableStringValue() : TemplatableValue() {}
-
-  template::value, int> = 0>
-  TemplatableStringValue(F value) : TemplatableValue(value) {}
-
-  template::value, int> = 0>
-  TemplatableStringValue(F f)
-      : TemplatableValue([f](X... x) -> std::string { return to_string(f(x...)); }) {}
-};
-
-void delay_microseconds_accurate(uint32_t usec);
+void delay_microseconds_safe(uint32_t us);
 
 template class Deduplicator {
  public:
@@ -361,4 +283,126 @@ template T *new_buffer(size_t length) {
   return buffer;
 }
 
+// ---------------------------------------------------------------------------------------------------------------------
+
+/// @name STL backports
+///@{
+
+// std::byteswap is from C++23 and technically should be a template, but this will do for now.
+constexpr uint8_t byteswap(uint8_t n) { return n; }
+constexpr uint16_t byteswap(uint16_t n) { return __builtin_bswap16(n); }
+constexpr uint32_t byteswap(uint32_t n) { return __builtin_bswap32(n); }
+constexpr uint64_t byteswap(uint64_t n) { return __builtin_bswap64(n); }
+
+///@}
+
+/// @name Bit manipulation
+///@{
+
+/// Encode a 16-bit value given the most and least significant byte.
+constexpr uint16_t encode_uint16(uint8_t msb, uint8_t lsb) {
+  return (static_cast(msb) << 8) | (static_cast(lsb));
+}
+/// Encode a 32-bit value given four bytes in most to least significant byte order.
+constexpr uint32_t encode_uint32(uint8_t byte1, uint8_t byte2, uint8_t byte3, uint8_t byte4) {
+  return (static_cast(byte1) << 24) | (static_cast(byte2) << 16) |
+         (static_cast(byte3) << 8) | (static_cast(byte4));
+}
+
+/// Encode a value from its constituent bytes (from most to least significant) in an array with length sizeof(T).
+template::value, int> = 0> inline T encode_value(const uint8_t *bytes) {
+  T val = 0;
+  for (size_t i = 0; i < sizeof(T); i++) {
+    val <<= 8;
+    val |= bytes[i];
+  }
+  return val;
+}
+/// Encode a value from its constituent bytes (from most to least significant) in an std::array with length sizeof(T).
+template::value, int> = 0>
+inline T encode_value(const std::array bytes) {
+  return encode_value(bytes.data());
+}
+/// Decode a value into its constituent bytes (from most to least significant).
+template::value, int> = 0>
+inline std::array decode_value(T val) {
+  std::array ret{};
+  for (size_t i = sizeof(T); i > 0; i--) {
+    ret[i - 1] = val & 0xFF;
+    val >>= 8;
+  }
+  return ret;
+}
+
+/// Convert a value between host byte order and big endian (most significant byte first) order.
+template::value, int> = 0> constexpr T convert_big_endian(T val) {
+#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
+  return byteswap(val);
+#else
+  return val;
+#endif
+}
+
+///@}
+
+/// @name Strings
+///@{
+
+/// Truncate a string to a specific length.
+std::string str_truncate(const std::string &str, size_t length);
+
+/// Convert the string to snake case (lowercase with underscores).
+std::string str_snake_case(const std::string &str);
+
+/// Sanitizes the input string by removing all characters but alphanumerics, dashes and underscores.
+std::string str_sanitize(const std::string &str);
+
+///@}
+
+/// @name Parsing & formatting
+///@{
+
+/// Parse a unsigned decimal number.
+template::value && std::is_unsigned::value), int> = 0>
+optional parse_number(const char *str, size_t len) {
+  char *end = nullptr;
+  unsigned long value = ::strtoul(str, &end, 10);  // NOLINT(google-runtime-int)
+  if (end == nullptr || end != str + len || value > std::numeric_limits::max())
+    return {};
+  return value;
+}
+template::value && std::is_unsigned::value), int> = 0>
+optional parse_number(const std::string &str) {
+  return parse_number(str.c_str(), str.length());
+}
+/// Parse a signed decimal number.
+template::value && std::is_signed::value), int> = 0>
+optional parse_number(const char *str, size_t len) {
+  char *end = nullptr;
+  signed long value = ::strtol(str, &end, 10);  // NOLINT(google-runtime-int)
+  if (end == nullptr || end != str + len || value < std::numeric_limits::min() ||
+      value > std::numeric_limits::max())
+    return {};
+  return value;
+}
+template::value && std::is_signed::value), int> = 0>
+optional parse_number(const std::string &str) {
+  return parse_number(str.c_str(), str.length());
+}
+/// Parse a decimal floating-point number.
+template::value), int> = 0>
+optional parse_number(const char *str, size_t len) {
+  char *end = nullptr;
+  float value = ::strtof(str, &end);
+  if (end == nullptr || end != str + len || value == HUGE_VALF)
+    return {};
+  return value;
+}
+template::value), int> = 0>
+optional parse_number(const std::string &str) {
+  return parse_number(str.c_str(), str.length());
+}
+
+///@}
+
 }  // namespace esphome
diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py
index 5b081698ad..9127f88e39 100644
--- a/esphome/cpp_helpers.py
+++ b/esphome/cpp_helpers.py
@@ -2,6 +2,7 @@ import logging
 
 from esphome.const import (
     CONF_DISABLED_BY_DEFAULT,
+    CONF_ENTITY_CATEGORY,
     CONF_ICON,
     CONF_INTERNAL,
     CONF_NAME,
@@ -102,6 +103,8 @@ async def setup_entity(var, config):
         add(var.set_internal(config[CONF_INTERNAL]))
     if CONF_ICON in config:
         add(var.set_icon(config[CONF_ICON]))
+    if CONF_ENTITY_CATEGORY in config:
+        add(var.set_entity_category(config[CONF_ENTITY_CATEGORY]))
 
 
 def extract_registry_entry_config(registry, full_config):
diff --git a/esphome/cpp_types.py b/esphome/cpp_types.py
index 888c319024..13d088e1cb 100644
--- a/esphome/cpp_types.py
+++ b/esphome/cpp_types.py
@@ -34,3 +34,4 @@ GPIOPin = esphome_ns.class_("GPIOPin")
 InternalGPIOPin = esphome_ns.class_("InternalGPIOPin", GPIOPin)
 gpio_ns = esphome_ns.namespace("gpio")
 gpio_Flags = gpio_ns.enum("Flags", is_class=True)
+EntityCategory = esphome_ns.enum("EntityCategory")
diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py
index 63378a38b5..11571ec889 100644
--- a/esphome/dashboard/dashboard.py
+++ b/esphome/dashboard/dashboard.py
@@ -509,6 +509,12 @@ class DashboardEntry:
             return None
         return self.storage.address
 
+    @property
+    def web_port(self):
+        if self.storage is None:
+            return None
+        return self.storage.web_port
+
     @property
     def name(self):
         if self.storage is None:
@@ -569,6 +575,7 @@ class ListDevicesHandler(BaseHandler):
                             "path": entry.path,
                             "comment": entry.comment,
                             "address": entry.address,
+                            "web_port": entry.web_port,
                             "target_platform": entry.target_platform,
                         }
                         for entry in entries
@@ -963,7 +970,7 @@ def start_web_server(args):
         server.add_socket(socket)
     else:
         _LOGGER.info(
-            "Starting dashboard web server on port %s and configuration dir %s...",
+            "Starting dashboard web server on http://0.0.0.0:%s and configuration dir %s...",
             args.port,
             settings.config_dir,
         )
diff --git a/esphome/git.py b/esphome/git.py
index 12c6b41648..25d893b2f5 100644
--- a/esphome/git.py
+++ b/esphome/git.py
@@ -40,15 +40,23 @@ def clone_or_update(
 ) -> Path:
     key = f"{url}@{ref}"
     repo_dir = _compute_destination_path(key, domain)
+    fetch_pr_branch = ref is not None and ref.startswith("pull/")
     if not repo_dir.is_dir():
         _LOGGER.info("Cloning %s", key)
         _LOGGER.debug("Location: %s", repo_dir)
         cmd = ["git", "clone", "--depth=1"]
-        if ref is not None:
+        if ref is not None and not fetch_pr_branch:
             cmd += ["--branch", ref]
         cmd += ["--", url, str(repo_dir)]
         run_git_command(cmd)
 
+        if fetch_pr_branch:
+            # We need to fetch the PR branch first, otherwise git will complain
+            # about missing objects
+            _LOGGER.info("Fetching %s", ref)
+            run_git_command(["git", "fetch", "--", "origin", ref], str(repo_dir))
+            run_git_command(["git", "reset", "--hard", "FETCH_HEAD"], str(repo_dir))
+
     else:
         # Check refresh needed
         file_timestamp = Path(repo_dir / ".git" / "FETCH_HEAD")
diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py
index 70e4430e71..2072e25ec5 100644
--- a/esphome/platformio_api.py
+++ b/esphome/platformio_api.py
@@ -125,7 +125,7 @@ def _run_idedata(config):
 
 def _load_idedata(config):
     platformio_ini = Path(CORE.relative_build_path("platformio.ini"))
-    temp_idedata = Path(CORE.relative_internal_path(CORE.name, "idedata.json"))
+    temp_idedata = Path(CORE.relative_internal_path("idedata", f"{CORE.name}.json"))
 
     changed = False
     if not platformio_ini.is_file() or not temp_idedata.is_file():
diff --git a/esphome/storage_json.py b/esphome/storage_json.py
index 3262559116..207a3edf57 100644
--- a/esphome/storage_json.py
+++ b/esphome/storage_json.py
@@ -41,6 +41,7 @@ class StorageJSON:
         esphome_version,
         src_version,
         address,
+        web_port,
         target_platform,
         build_path,
         firmware_bin_path,
@@ -60,6 +61,9 @@ class StorageJSON:
         self.src_version = src_version  # type: int
         # Address of the ESP, for example livingroom.local or a static IP
         self.address = address  # type: str
+        # Web server port of the ESP, for example 80
+        assert web_port is None or isinstance(web_port, int)
+        self.web_port = web_port  # type: int
         # The type of ESP in use, either ESP32 or ESP8266
         self.target_platform = target_platform  # type: str
         # The absolute path to the platformio project
@@ -78,6 +82,7 @@ class StorageJSON:
             "esphome_version": self.esphome_version,
             "src_version": self.src_version,
             "address": self.address,
+            "web_port": self.web_port,
             "esp_platform": self.target_platform,
             "build_path": self.build_path,
             "firmware_bin_path": self.firmware_bin_path,
@@ -101,6 +106,7 @@ class StorageJSON:
             esphome_version=const.__version__,
             src_version=1,
             address=esph.address,
+            web_port=esph.web_port,
             target_platform=esph.target_platform,
             build_path=esph.build_path,
             firmware_bin_path=esph.firmware_bin,
@@ -117,6 +123,7 @@ class StorageJSON:
             esphome_version=const.__version__,
             src_version=1,
             address=address,
+            web_port=None,
             target_platform=esp_platform,
             build_path=None,
             firmware_bin_path=None,
@@ -135,6 +142,7 @@ class StorageJSON:
         )
         src_version = storage.get("src_version")
         address = storage.get("address")
+        web_port = storage.get("web_port")
         esp_platform = storage.get("esp_platform")
         build_path = storage.get("build_path")
         firmware_bin_path = storage.get("firmware_bin_path")
@@ -146,6 +154,7 @@ class StorageJSON:
             esphome_version,
             src_version,
             address,
+            web_port,
             esp_platform,
             build_path,
             firmware_bin_path,
diff --git a/esphome/wizard.py b/esphome/wizard.py
index 5c35fac73a..6c87b66453 100644
--- a/esphome/wizard.py
+++ b/esphome/wizard.py
@@ -367,10 +367,9 @@ def wizard(path):
     )
     safe_print()
     safe_print("Next steps:")
+    safe_print("  > Follow the rest of the getting started guide:")
     safe_print(
-        '  > Check your Home Assistant "integrations" screen. If all goes well, you '
-        "should see your ESP being discovered automatically."
+        "  > https://esphome.io/guides/getting_started_command_line.html#adding-some-features"
     )
-    safe_print("  > Then follow the rest of the getting started guide:")
-    safe_print("  > https://esphome.io/guides/getting_started_command_line.html")
+    safe_print("  > to learn how to customize ESPHome and install it to your device.")
     return 0
diff --git a/esphome/writer.py b/esphome/writer.py
index 29532d4f64..8963572752 100644
--- a/esphome/writer.py
+++ b/esphome/writer.py
@@ -38,10 +38,8 @@ CPP_BASE_FORMAT = (
     """"
 
 void setup() {
-  // ===== DO NOT EDIT ANYTHING BELOW THIS LINE =====
   """,
     """
-  // ========= YOU CAN EDIT AFTER THIS LINE =========
   App.setup();
 }
 
@@ -59,10 +57,8 @@ lib_deps =
 build_flags =
 upload_flags =
 
-; ===== DO NOT EDIT ANYTHING BELOW THIS LINE =====
 """,
     """
-; ========= YOU CAN EDIT AFTER THIS LINE =========
 
 """,
 )
@@ -102,61 +98,6 @@ def replace_file_content(text, pattern, repl):
     return content_new, count
 
 
-def migrate_src_version_0_to_1():
-    main_cpp = CORE.relative_build_path("src", "main.cpp")
-    if not os.path.isfile(main_cpp):
-        return
-
-    content = read_file(main_cpp)
-
-    if CPP_INCLUDE_BEGIN in content:
-        return
-
-    content, count = replace_file_content(content, r"\s*delay\((?:16|20)\);", "")
-    if count != 0:
-        _LOGGER.info(
-            "Migration: Removed %s occurrence of 'delay(16);' in %s", count, main_cpp
-        )
-
-    content, count = replace_file_content(content, r"using namespace esphomelib;", "")
-    if count != 0:
-        _LOGGER.info(
-            "Migration: Removed %s occurrence of 'using namespace esphomelib;' "
-            "in %s",
-            count,
-            main_cpp,
-        )
-
-    if CPP_INCLUDE_BEGIN not in content:
-        content, count = replace_file_content(
-            content,
-            r'#include "esphomelib/application.h"',
-            f"{CPP_INCLUDE_BEGIN}\n{CPP_INCLUDE_END}",
-        )
-        if count == 0:
-            _LOGGER.error(
-                "Migration failed. ESPHome 1.10.0 needs to have a new auto-generated "
-                "include section in the %s file. Please remove %s and let it be "
-                "auto-generated again.",
-                main_cpp,
-                main_cpp,
-            )
-        _LOGGER.info("Migration: Added include section to %s", main_cpp)
-
-    write_file_if_changed(main_cpp, content)
-
-
-def migrate_src_version(old, new):
-    if old == new:
-        return
-    if old > new:
-        _LOGGER.warning("The source version rolled backwards! Ignoring.")
-        return
-
-    if old == 0:
-        migrate_src_version_0_to_1()
-
-
 def storage_should_clean(old, new):  # type: (StorageJSON, StorageJSON) -> bool
     if old is None:
         return True
@@ -175,9 +116,6 @@ def update_storage_json():
     if old == new:
         return
 
-    old_src_version = old.src_version if old is not None else 0
-    migrate_src_version(old_src_version, new.src_version)
-
     if storage_should_clean(old, new):
         _LOGGER.info("Core config or version changed, cleaning build files...")
         clean_build()
@@ -277,12 +215,12 @@ VERSION_H_TARGET = "esphome/core/version.h"
 ESPHOME_README_TXT = """
 THIS DIRECTORY IS AUTO-GENERATED, DO NOT MODIFY
 
-ESPHome automatically populates the esphome/ directory, and any
+ESPHome automatically populates the build directory, and any
 changes to this directory will be removed the next time esphome is
 run.
 
-For modifying esphome's core files, please use a development esphome install
-or use the custom_components folder.
+For modifying esphome's core files, please use a development esphome install,
+the custom_components folder or the external_components feature.
 """
 
 
@@ -339,9 +277,7 @@ def copy_src_tree():
     write_file_if_changed(
         CORE.relative_src_path("esphome", "core", "defines.h"), generate_defines_h()
     )
-    write_file_if_changed(
-        CORE.relative_src_path("esphome", "README.txt"), ESPHOME_README_TXT
-    )
+    write_file_if_changed(CORE.relative_build_path("README.txt"), ESPHOME_README_TXT)
     write_file_if_changed(
         CORE.relative_src_path("esphome.h"), ESPHOME_H_FORMAT.format(include_s)
     )
@@ -413,11 +349,6 @@ GITIGNORE_CONTENT = """# Gitignore settings for ESPHome
 # This is an example and may include too much for your use-case.
 # You can modify this file to suit your needs.
 /.esphome/
-**/.pioenvs/
-**/.piolibdeps/
-**/lib/
-**/src/
-**/platformio.ini
 /secrets.yaml
 """
 
diff --git a/esphome/zeroconf.py b/esphome/zeroconf.py
index a19fc143ec..1fbdf7e93f 100644
--- a/esphome/zeroconf.py
+++ b/esphome/zeroconf.py
@@ -13,8 +13,9 @@ from zeroconf import (
     RecordUpdateListener,
     Zeroconf,
     ServiceBrowser,
+    ServiceStateChange,
+    current_time_millis,
 )
-from zeroconf._services import ServiceStateChange
 
 _CLASS_IN = 1
 _FLAGS_QR_QUERY = 0x0000  # query
@@ -88,7 +89,7 @@ class DashboardStatus(threading.Thread):
         entries = self.zc.cache.entries_with_name(key)
         if not entries:
             return False
-        now = time.time() * 1000
+        now = current_time_millis()
 
         return any(
             (entry.created + DashboardStatus.OFFLINE_AFTER) >= now for entry in entries
@@ -99,7 +100,7 @@ class DashboardStatus(threading.Thread):
             self.on_update(
                 {key: self.host_status(host) for key, host in self.key_to_host.items()}
             )
-            now = time.time() * 1000
+            now = current_time_millis()
             for host in self.query_hosts:
                 entries = self.zc.cache.entries_with_name(host)
                 if not entries or all(
diff --git a/platformio.ini b/platformio.ini
index ee895ed882..5e89afe8e6 100644
--- a/platformio.ini
+++ b/platformio.ini
@@ -27,7 +27,7 @@ build_flags =
 [common]
 lib_deps =
     esphome/noise-c@0.1.4     ; api
-    makuna/NeoPixelBus@2.6.7  ; neopixelbus
+    makuna/NeoPixelBus@2.6.9  ; neopixelbus
 build_flags =
     -DESPHOME_LOG_LEVEL=ESPHOME_LOG_LEVEL_VERY_VERBOSE
 src_filter =
@@ -41,7 +41,7 @@ lib_deps =
     ${common.lib_deps}
     ottowinter/AsyncMqttClient-esphome@0.8.6              ; mqtt
     ottowinter/ArduinoJson-esphomelib@5.13.3              ; json
-    esphome/ESPAsyncWebServer-esphome@2.0.0               ; web_server_base
+    esphome/ESPAsyncWebServer-esphome@2.1.0               ; web_server_base
     fastled/FastLED@3.3.2                                 ; fastled_base
     mikalhart/TinyGPSPlus@1.0.2                           ; gps
     freekode/TM1651@1.0.1                                 ; tm1651
diff --git a/requirements.txt b/requirements.txt
index 51bbb445d7..c4b211283d 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,16 +1,17 @@
 voluptuous==0.12.2
-PyYAML==5.4.1
-paho-mqtt==1.5.1
+PyYAML==6.0
+paho-mqtt==1.6.1
 colorama==0.4.4
 tornado==6.1
-tzlocal==3.0    # from time
+tzlocal==4.1    # from time
 tzdata>=2021.1  # from time
 pyserial==3.5
-platformio==5.2.1  # When updating platformio, also update Dockerfile
-esptool==3.1
+platformio==5.2.2  # When updating platformio, also update Dockerfile
+esptool==3.2
 click==8.0.3
 esphome-dashboard==20211021.1
-aioesphomeapi==9.1.5
+aioesphomeapi==10.2.0
+zeroconf==0.36.13
 
 # esp-idf requires this, but doesn't bundle it by default
 # https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24
diff --git a/requirements_test.txt b/requirements_test.txt
index 8ebcf24d4d..03879c5d0e 100644
--- a/requirements_test.txt
+++ b/requirements_test.txt
@@ -1,6 +1,6 @@
 pylint==2.11.1
 flake8==4.0.1
-black==21.9b0
+black==21.10b0
 pexpect==4.8.0
 pre-commit
 
@@ -8,6 +8,6 @@ pre-commit
 pytest==6.2.5
 pytest-cov==3.0.0
 pytest-mock==3.6.1
-pytest-asyncio==0.15.1
+pytest-asyncio==0.16.0
 asyncmock==0.4.2
 hypothesis==5.49.0
diff --git a/script/ci-custom.py b/script/ci-custom.py
index 8e9ca487a6..89550afd3d 100755
--- a/script/ci-custom.py
+++ b/script/ci-custom.py
@@ -263,7 +263,11 @@ def highlight(s):
 @lint_re_check(
     r"^#define\s+([a-zA-Z0-9_]+)\s+([0-9bx]+)" + CPP_RE_EOL,
     include=cpp_include,
-    exclude=["esphome/core/log.h", "esphome/components/socket/headers.h"],
+    exclude=[
+        "esphome/core/log.h",
+        "esphome/components/socket/headers.h",
+        "esphome/core/defines.h",
+    ],
 )
 def lint_no_defines(fname, match):
     s = highlight(
diff --git a/script/clang-tidy b/script/clang-tidy
index 87ba1c84b5..ad5fdfeb04 100755
--- a/script/clang-tidy
+++ b/script/clang-tidy
@@ -32,6 +32,7 @@ def clang_options(idedata):
         '-D_PGMSPACE_H_',
         '-Dpgm_read_byte(s)=(*(const uint8_t *)(s))',
         '-Dpgm_read_byte_near(s)=(*(const uint8_t *)(s))',
+        '-Dpgm_read_word(s)=(*(const uint16_t *)(s))',
         '-Dpgm_read_dword(s)=(*(const uint32_t *)(s))',
         '-DPROGMEM=',
         '-DPGM_P=const char *',
diff --git a/sdkconfig.defaults b/sdkconfig.defaults
index 6b2d6f8f2e..26db4705b8 100644
--- a/sdkconfig.defaults
+++ b/sdkconfig.defaults
@@ -8,6 +8,7 @@ CONFIG_COMPILER_OPTIMIZATION_SIZE=y
 CONFIG_PARTITION_TABLE_CUSTOM=y
 #CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
 CONFIG_PARTITION_TABLE_SINGLE_APP=n
+CONFIG_FREERTOS_HZ=1000
 
 # esp32_ble
 CONFIG_BT_ENABLED=y
diff --git a/tests/test1.yaml b/tests/test1.yaml
index 157ccfc5d1..a7f2e24465 100644
--- a/tests/test1.yaml
+++ b/tests/test1.yaml
@@ -94,6 +94,7 @@ mqtt:
   username: 'debug'
   password: 'debug'
   client_id: someclient
+  use_abbreviations: false
   discovery: True
   discovery_retain: False
   discovery_prefix: discovery
@@ -192,6 +193,18 @@ uart:
     data_bits: 8
     stop_bits: 1
     rx_buffer_size: 512
+    debug:
+      dummy_receiver: true
+      direction: both
+      after:
+        bytes: 50
+        timeout: 500ms
+        delimiter: "\r\n"
+      sequence:
+        - lambda: UARTDebug::log_hex(direction, bytes, ':');
+        - lambda: UARTDebug::log_string(direction, bytes);
+        - lambda: UARTDebug::log_int(direction, bytes, ',');
+        - lambda: UARTDebug::log_binary(direction, bytes, ';');
 
   - id: adalight_uart
     tx_pin: GPIO25
@@ -233,6 +246,7 @@ logger:
 
 web_server:
   port: 8080
+  ota: true
   css_url: https://esphome.io/_static/webserver-v1.min.css
   js_url: https://esphome.io/_static/webserver-v1.min.js
 
@@ -2213,6 +2227,31 @@ display:
     row_start: 0
     lambda: |-
       it.rectangle(0, 0, it.get_width(), it.get_height());
+  - platform: ili9341
+    model: "TFT 2.4"
+    cs_pin: GPIO5
+    dc_pin: GPIO4
+    reset_pin: GPIO22
+    led_pin:
+      number: GPIO15
+      inverted: true
+    lambda: |-
+      it.rectangle(0, 0, it.get_width(), it.get_height());
+  - platform: ili9341
+    model: "TFT 2.4"
+    cs_pin: GPIO5
+    dc_pin: GPIO4
+    reset_pin: GPIO22
+    led_pin:
+      number: GPIO15
+      inverted: true
+    auto_clear_enabled: false
+    rotation: 90
+    lambda: |-
+      if (!id(glob_bool_processed)) {
+        it.fill(Color::WHITE);
+        id(glob_bool_processed) = true;
+      }
 
 tm1651:
   id: tm1651_battery
@@ -2392,6 +2431,10 @@ globals:
     type: std::string
     restore_value: no
     # initial_value: ""
+  - id: glob_bool_processed
+    type: bool
+    restore_value: no
+    initial_value: 'false'
 
 text_sensor:
   - platform: mqtt_subscribe
@@ -2469,3 +2512,23 @@ teleinfo:
   uart_id: uart0
   update_interval: 60s
   historical_mode: true
+
+number:
+  - platform: template
+    id: test_number
+    state_topic: livingroom/custom_state_topic
+    command_topic: livingroom/custom_command_topic
+    min_value: 0
+    step: 1
+    max_value: 10
+    optimistic: true
+
+select:
+  - platform: template
+    id: test_select
+    state_topic: livingroom/custom_state_topic
+    command_topic: livingroom/custom_command_topic
+    options:
+      - one
+      - two
+    optimistic: true
diff --git a/tests/test2.yaml b/tests/test2.yaml
index 7e71d1ab4e..f90e522b1e 100644
--- a/tests/test2.yaml
+++ b/tests/test2.yaml
@@ -401,6 +401,11 @@ ble_client:
 
 airthings_ble:
 
+ruuvi_ble:
+
+xiaomi_ble:
+
+
 #esp32_ble_beacon:
 #  type: iBeacon
 #  uuid: 'c29ce823-e67a-4e71-bff2-abaa32e77a98'
@@ -499,3 +504,9 @@ interval:
 
 display:
 
+cap1188:
+  id: cap1188_component
+  address: 0x29
+  touch_threshold: 0x20
+  allow_multiple_touches: true
+  reset_pin: 14
diff --git a/tests/test3.yaml b/tests/test3.yaml
index f7cba5e787..cf80c06aa8 100644
--- a/tests/test3.yaml
+++ b/tests/test3.yaml
@@ -250,6 +250,10 @@ uart:
     tx_pin: GPIO4
     rx_pin: GPIO5
     baud_rate: 9600
+  - id: uart7
+    tx_pin: GPIO4
+    rx_pin: GPIO5
+    baud_rate: 38400
 
 modbus:
   uart_id: uart1
@@ -264,6 +268,8 @@ logger:
   level: DEBUG
   esp8266_store_log_strings_in_flash: true
 
+improv_serial:
+
 deep_sleep:
   run_duration: 20s
   sleep_duration: 50s
@@ -403,7 +409,7 @@ sensor:
       name: Illuminance
     color_temperature:
       name: Color Temperature
-    integration_time: 700ms
+    integration_time: 614ms
     gain: 60x
   - platform: custom
     lambda: |-
@@ -549,6 +555,18 @@ sensor:
       name: 'PMS Humidity'
     formaldehyde:
       name: 'PMS Formaldehyde Concentration'
+  - platform: cse7761
+    uart_id: uart7
+    voltage:
+      name: 'CSE7761 Voltage'
+    current_1:
+      name: 'CSE7761 Current 1'
+    current_2:
+      name: 'CSE7761 Current 2'
+    active_power_1:
+      name: 'CSE7761 Active Power 1'
+    active_power_2:
+      name: 'CSE7761 Active Power 2'
   - platform: cse7766
     uart_id: uart3
     voltage:
diff --git a/tests/test4.yaml b/tests/test4.yaml
index bc249c5ecb..938145235a 100644
--- a/tests/test4.yaml
+++ b/tests/test4.yaml
@@ -45,6 +45,7 @@ logger:
   level: DEBUG
 
 web_server:
+  ota: false
   auth:
     username: admin
     password: admin
@@ -481,6 +482,12 @@ esp32_camera:
   resolution: 640x480
   jpeg_quality: 10
 
+esp32_camera_web_server:
+  - port: 8080
+    mode: stream
+  - port: 8081
+    mode: snapshot
+
 external_components:
   - source: github://esphome/esphome@dev
     refresh: 1d
diff --git a/tests/test5.yaml b/tests/test5.yaml
index 72df3ed212..f1fb786fe5 100644
--- a/tests/test5.yaml
+++ b/tests/test5.yaml
@@ -173,3 +173,11 @@ sensor:
     uart_id: uart2
     co2:
       name: CO2 Sensor
+
+script:
+  - id: automation_test
+    then:
+      - repeat:
+          count: 5
+          then:
+            - logger.log: "looping!"
diff --git a/tests/unit_tests/test_config_validation.py b/tests/unit_tests/test_config_validation.py
index e34c7064fa..16cfb16e94 100644
--- a/tests/unit_tests/test_config_validation.py
+++ b/tests/unit_tests/test_config_validation.py
@@ -40,28 +40,6 @@ def test_valid_name__invalid(value):
         config_validation.valid_name(value)
 
 
-@pytest.mark.parametrize("value", ("foo", "bar123", "foo-bar"))
-def test_hostname__valid(value):
-    actual = config_validation.hostname(value)
-
-    assert actual == value
-
-
-@pytest.mark.parametrize("value", ("foo bar", "foobar ", "foo#bar"))
-def test_hostname__invalid(value):
-    with pytest.raises(Invalid):
-        config_validation.hostname(value)
-
-
-def test_hostname__warning(caplog):
-    actual = config_validation.hostname("foo_bar")
-    assert actual == "foo_bar"
-    assert (
-        "Using the '_' (underscore) character in the hostname is discouraged"
-        in caplog.text
-    )
-
-
 @given(one_of(integers(), text()))
 def test_string__valid(value):
     actual = config_validation.string(value)